From 059b0af6e73e2bb05466e42df7e3091a85301dcf Mon Sep 17 00:00:00 2001 From: Lukas Heiligenbrunner Date: Sun, 14 Mar 2021 12:49:24 +0000 Subject: [PATCH] fix incorrect gui refresh if theme is changed implement custom clientstore add new Password page if password is set force entering password to successfully receive the token add a new unsafe api call for init call only --- apiGo/api/ApiBase.go | 8 ++ apiGo/api/Init.go | 33 +++++++ apiGo/api/Settings.go | 37 -------- apiGo/api/oauth/CustomClientStore.go | 56 ++++++++++++ apiGo/api/oauth/Oauth.go | 22 +++-- apiGo/database/settings/DBSettings.go | 49 ++++++++++ apiGo/go.mod | 1 - apiGo/go.sum | 1 - apiGo/main.go | 1 + src/App.test.js | 28 +++--- src/App.tsx | 91 +++++++++++++------ .../AuthenticationPage.module.css | 52 +++++++++++ .../AuthenticationPage.test.js | 21 +++++ .../AuthenticationPage/AuthenticationPage.tsx | 46 ++++++++++ src/setupTests.js | 1 + src/types/ApiTypes.ts | 2 +- src/utils/Api.ts | 71 +++++++++++---- src/utils/GlobalInfos.ts | 8 ++ 18 files changed, 422 insertions(+), 106 deletions(-) create mode 100644 apiGo/api/Init.go create mode 100644 apiGo/api/oauth/CustomClientStore.go create mode 100644 apiGo/database/settings/DBSettings.go create mode 100644 src/pages/AuthenticationPage/AuthenticationPage.module.css create mode 100644 src/pages/AuthenticationPage/AuthenticationPage.test.js create mode 100644 src/pages/AuthenticationPage/AuthenticationPage.tsx diff --git a/apiGo/api/ApiBase.go b/apiGo/api/ApiBase.go index d77f086..26e0e1e 100644 --- a/apiGo/api/ApiBase.go +++ b/apiGo/api/ApiBase.go @@ -16,6 +16,7 @@ const ( TagNode = iota SettingsNode = iota ActorNode = iota + InitNode = iota ) type actionStruct struct { @@ -42,6 +43,9 @@ func ServerInit(port uint16) { http.Handle(APIPREFIX+"/settings", oauth.ValidateToken(settingsHandler)) http.Handle(APIPREFIX+"/actor", oauth.ValidateToken(actorHandler)) + // initialization api calls to check if password is neccessaray + http.Handle(APIPREFIX+"/init", http.HandlerFunc(initHandler)) + // initialize oauth service and add corresponding auth routes oauth.InitOAuth() @@ -85,6 +89,10 @@ func settingsHandler(rw http.ResponseWriter, req *http.Request) { handlefunc(rw, req, SettingsNode) } +func initHandler(rw http.ResponseWriter, req *http.Request) { + handlefunc(rw, req, InitNode) +} + func handlefunc(rw http.ResponseWriter, req *http.Request, node int) { // only allow post requests if req.Method != "POST" { diff --git a/apiGo/api/Init.go b/apiGo/api/Init.go new file mode 100644 index 0000000..0306154 --- /dev/null +++ b/apiGo/api/Init.go @@ -0,0 +1,33 @@ +package api + +import ( + "encoding/json" + "openmediacenter/apiGo/database/settings" +) + +func AddInitHandlers() { + passwordNeeded() +} + +func passwordNeeded() { + AddHandler("loadInitialData", InitNode, nil, func() []byte { + sett := settings.LoadSettings() + + type InitialDataTypeResponse struct { + DarkMode bool + Pasword bool + MediacenterName string + VideoPath string + } + + res := InitialDataTypeResponse{ + DarkMode: sett.DarkMode, + Pasword: sett.Pasword != "-1", + MediacenterName: sett.Mediacenter_name, + VideoPath: sett.VideoPath, + } + + str, _ := json.Marshal(res) + return str + }) +} diff --git a/apiGo/api/Settings.go b/apiGo/api/Settings.go index 9d8d81a..3383f38 100644 --- a/apiGo/api/Settings.go +++ b/apiGo/api/Settings.go @@ -1,8 +1,6 @@ package api import ( - "encoding/json" - "fmt" "openmediacenter/apiGo/api/types" "openmediacenter/apiGo/database" "openmediacenter/apiGo/videoparser" @@ -15,41 +13,6 @@ func AddSettingsHandlers() { } func getSettingsFromDB() { - AddHandler("loadInitialData", SettingsNode, nil, func() []byte { - query := "SELECT DarkMode, password, mediacenter_name, video_path from settings" - - type InitialDataType struct { - DarkMode int - Pasword int - Mediacenter_name string - VideoPath string - } - - result := InitialDataType{} - - err := database.QueryRow(query).Scan(&result.DarkMode, &result.Pasword, &result.Mediacenter_name, &result.VideoPath) - if err != nil { - fmt.Println("error while parsing db data: " + err.Error()) - } - - type InitialDataTypeResponse struct { - DarkMode bool - Pasword bool - Mediacenter_name string - VideoPath string - } - - res := InitialDataTypeResponse{ - DarkMode: result.DarkMode != 0, - Pasword: result.Pasword != -1, - Mediacenter_name: result.Mediacenter_name, - VideoPath: result.VideoPath, - } - - str, _ := json.Marshal(res) - return str - }) - AddHandler("loadGeneralSettings", SettingsNode, nil, func() []byte { result := database.GetSettings() return jsonify(result) diff --git a/apiGo/api/oauth/CustomClientStore.go b/apiGo/api/oauth/CustomClientStore.go new file mode 100644 index 0000000..56857b1 --- /dev/null +++ b/apiGo/api/oauth/CustomClientStore.go @@ -0,0 +1,56 @@ +package oauth + +import ( + "gopkg.in/oauth2.v3" + "openmediacenter/apiGo/database/settings" +) + +type CustomClientStore struct { + oauth2.ClientStore +} + +type CustomClientInfo struct { + oauth2.ClientInfo + ID string + Secret string + Domain string + UserID string +} + +func NewCustomStore() oauth2.ClientStore { + s := new(CustomClientStore) + return s +} + +func (a *CustomClientStore) GetByID(id string) (oauth2.ClientInfo, error) { + password := settings.GetPassword() + if password == nil { + defaultpassword := "openmediacenter" + password = &defaultpassword + } + + clientinfo := CustomClientInfo{ + ID: "openmediacenter", + Secret: *password, + Domain: "http://localhost:8081", + UserID: "openmediacenter", + } + + return &clientinfo, nil +} + +func (a *CustomClientInfo) GetID() string { + return a.ID +} + +func (a *CustomClientInfo) GetSecret() string { + return a.Secret +} + +func (a *CustomClientInfo) GetDomain() string { + return a.Domain +} + +func (a *CustomClientInfo) GetUserID() string { + return a.UserID +} diff --git a/apiGo/api/oauth/Oauth.go b/apiGo/api/oauth/Oauth.go index 0ecacc7..5d25d69 100644 --- a/apiGo/api/oauth/Oauth.go +++ b/apiGo/api/oauth/Oauth.go @@ -3,7 +3,7 @@ package oauth import ( "gopkg.in/oauth2.v3/errors" "gopkg.in/oauth2.v3/manage" - "gopkg.in/oauth2.v3/models" + //"gopkg.in/oauth2.v3/models" "gopkg.in/oauth2.v3/server" "gopkg.in/oauth2.v3/store" "log" @@ -17,15 +17,19 @@ func InitOAuth() { // token store manager.MustTokenStorage(store.NewMemoryTokenStore()) - clientStore := store.NewClientStore() - // todo we need to check here if a password is enabled in db -- when yes set it here! - clientStore.Set("openmediacenter", &models.Client{ - ID: "openmediacenter", - Secret: "openmediacenter", - Domain: "http://localhost:8081", - }) + //clientStore := store.NewClientStore() + //// todo we need to check here if a password is enabled in db -- when yes set it here! + //clientStore.Set("openmediacenter", &models.Client{ + // ID: "openmediacenter", + // Secret: "openmediacenter", + // Domain: "http://localhost:8081", + //}) + // + //manager.MapClientStorage(clientStore) + + strtest := NewCustomStore() + manager.MapClientStorage(strtest) - manager.MapClientStorage(clientStore) srv = server.NewServer(server.NewConfig(), manager) srv.SetClientInfoHandler(server.ClientFormHandler) manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg) diff --git a/apiGo/database/settings/DBSettings.go b/apiGo/database/settings/DBSettings.go new file mode 100644 index 0000000..5323ace --- /dev/null +++ b/apiGo/database/settings/DBSettings.go @@ -0,0 +1,49 @@ +package settings + +import ( + "fmt" + "openmediacenter/apiGo/database" +) + +func GetPassword() *string { + pwd := LoadSettings().Pasword + if pwd == "-1" { + return nil + } else { + return &pwd + } +} + +type SettingsType struct { + DarkMode bool + Pasword string + Mediacenter_name string + VideoPath string +} + +func LoadSettings() *SettingsType { + query := "SELECT DarkMode, password, mediacenter_name, video_path from settings" + + type RawSettingsType struct { + DarkMode int + Pasword string + Mediacenter_name string + VideoPath string + } + + result := RawSettingsType{} + + err := database.QueryRow(query).Scan(&result.DarkMode, &result.Pasword, &result.Mediacenter_name, &result.VideoPath) + if err != nil { + fmt.Println("error while parsing db data: " + err.Error()) + } + + res := SettingsType{ + DarkMode: result.DarkMode != 0, + Pasword: result.Pasword, + Mediacenter_name: result.Mediacenter_name, + VideoPath: result.VideoPath, + } + + return &res +} diff --git a/apiGo/go.mod b/apiGo/go.mod index 28d8db6..d97fa64 100644 --- a/apiGo/go.mod +++ b/apiGo/go.mod @@ -3,7 +3,6 @@ module openmediacenter/apiGo go 1.16 require ( - github.com/go-session/session v3.1.2+incompatible github.com/go-sql-driver/mysql v1.5.0 gopkg.in/oauth2.v3 v3.12.0 ) diff --git a/apiGo/go.sum b/apiGo/go.sum index b0f2563..a8a6512 100644 --- a/apiGo/go.sum +++ b/apiGo/go.sum @@ -11,7 +11,6 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= -github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg= github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= diff --git a/apiGo/main.go b/apiGo/main.go index 7db3c84..68674c8 100644 --- a/apiGo/main.go +++ b/apiGo/main.go @@ -26,6 +26,7 @@ func main() { api.AddSettingsHandlers() api.AddTagHandlers() api.AddActorsHandlers() + api.AddInitHandlers() api.ServerInit(8081) } diff --git a/src/App.test.js b/src/App.test.js index 3162040..204011d 100644 --- a/src/App.test.js +++ b/src/App.test.js @@ -1,6 +1,7 @@ import React from 'react'; import App from './App'; import {shallow} from 'enzyme'; +import GlobalInfos from "./utils/GlobalInfos"; describe('', function () { it('renders without crashing ', function () { @@ -10,34 +11,37 @@ describe('', function () { it('renders title', () => { const wrapper = shallow(); + wrapper.setState({password: false}); expect(wrapper.find('.navbrand').text()).toBe('OpenMediaCenter'); }); it('are navlinks correct', function () { const wrapper = shallow(); + wrapper.setState({password: false}); expect(wrapper.find('.navitem')).toHaveLength(4); }); it('test initial fetch from api', done => { - global.fetch = global.prepareFetchApi({ - generalSettingsLoaded: true, - passwordsupport: true, - mediacentername: 'testname' - }); + callAPIMock({ + MediacenterName: 'testname' + }) + + GlobalInfos.enableDarkTheme = jest.fn((r) => {}) const wrapper = shallow(); - - const func = jest.fn(); - wrapper.instance().setState = func; - - expect(global.fetch).toBeCalledTimes(1); - process.nextTick(() => { - expect(func).toBeCalledTimes(1); + expect(document.title).toBe('testname'); global.fetch.mockClear(); done(); }); }); + + it('test render of password page', function () { + const wrapper = shallow(); + wrapper.setState({password: true}); + + expect(wrapper.find('AuthenticationPage')).toHaveLength(1); + }); }); diff --git a/src/App.tsx b/src/App.tsx index 3e57f86..fa0448c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,7 +9,7 @@ import style from './App.module.css'; import SettingsPage from './pages/SettingsPage/SettingsPage'; import CategoryPage from './pages/CategoryPage/CategoryPage'; -import {APINode, callAPI} from './utils/Api'; +import {APINode, apiTokenValid, callApiUnsafe, refreshAPIToken} from './utils/Api'; import {NoBackendConnectionPopup} from './elements/Popups/NoBackendConnectionPopup/NoBackendConnectionPopup'; import {BrowserRouter as Router, NavLink, Route, Switch} from 'react-router-dom'; @@ -17,10 +17,10 @@ import Player from './pages/Player/Player'; import ActorOverviewPage from './pages/ActorOverviewPage/ActorOverviewPage'; import ActorPage from './pages/ActorPage/ActorPage'; import {SettingsTypes} from './types/ApiTypes'; +import AuthenticationPage from "./pages/AuthenticationPage/AuthenticationPage"; interface state { - generalSettingsLoaded: boolean; - passwordsupport: boolean; + password: boolean | null; // null if uninitialized - true if pwd needed false if not needed mediacentername: string; onapierror: boolean; } @@ -31,30 +31,48 @@ interface state { class App extends React.Component<{}, state> { constructor(props: {}) { super(props); + + let pwdneeded: boolean | null = null; + + if (apiTokenValid()) { + pwdneeded = false; + } else { + refreshAPIToken((err) => { + if (err === 'invalid_client') { + this.setState({password: true}) + } else if (err === '') { + this.setState({password: false}) + } else { + console.log("unimplemented token error: " + err) + } + }) + } + this.state = { - generalSettingsLoaded: false, - passwordsupport: false, mediacentername: 'OpenMediaCenter', - onapierror: false + onapierror: false, + password: pwdneeded }; + + GlobalInfos.onThemeChange(() => { + this.forceUpdate(); + }) } initialAPICall(): void { // this is the first api call so if it fails we know there is no connection to backend - callAPI(APINode.Settings, {action: 'loadInitialData'}, (result: SettingsTypes.initialApiCallData) => { + callApiUnsafe(APINode.Init, {action: 'loadInitialData'}, (result: SettingsTypes.initialApiCallData) => { // set theme GlobalInfos.enableDarkTheme(result.DarkMode); GlobalInfos.setVideoPath(result.VideoPath); this.setState({ - generalSettingsLoaded: true, - passwordsupport: result.Password, - mediacentername: result.Mediacenter_name, + mediacentername: result.MediacenterName, onapierror: false }); // set tab title to received mediacenter name - document.title = result.Mediacenter_name; + document.title = result.MediacenterName; }, error => { this.setState({onapierror: true}); }); @@ -70,23 +88,44 @@ class App extends React.Component<{}, state> { // add the main theme to the page body document.body.className = themeStyle.backgroundcolor; - return ( - -
-
-
{this.state.mediacentername}
- Home - Random - Video + if (this.state.password === true) { + return ( + { + refreshAPIToken((error) => { + if (error !== '') { + console.log("wrong password!!!"); + } else { + this.setState({password: false}); + } + }, password); + }}/> + ); + } else if (this.state.password === false) { + return ( + +
+
+
{this.state.mediacentername}
+ Home + Random + Video - Categories - Settings + Categories + Settings +
+ {this.routing()}
- {this.routing()} -
- {this.state.onapierror ? this.ApiError() : null} - - ); + {this.state.onapierror ? this.ApiError() : null} + + ); + } else { + return (<>still loading...); + } } routing(): JSX.Element { diff --git a/src/pages/AuthenticationPage/AuthenticationPage.module.css b/src/pages/AuthenticationPage/AuthenticationPage.module.css new file mode 100644 index 0000000..1fce23a --- /dev/null +++ b/src/pages/AuthenticationPage/AuthenticationPage.module.css @@ -0,0 +1,52 @@ +.main { + background-color: #00b3ff; + margin-left: calc(50% - 125px); + margin-top: 5%; + padding-bottom: 15px; + width: 250px; + text-align: center; + border-radius: 10px; +} + +.loginText { + font-size: xx-large; + text-align: center; + margin-bottom: 15px; + font-weight: bolder; +} + +.openmediacenterlabel { + margin-top: 5%; + text-align: center; + font-size: xxx-large; + font-weight: bold; + text-transform: capitalize; + color: white; +} + +.input { + margin-left: 10px; + margin-right: 10px; + width: calc(100% - 20px); + background: transparent; + border-width: 0 0 1px 0; + color: #505050; + border-color: #505050; + text-align: center; + margin-bottom: 25px; + font-size: larger; +} + +::placeholder { + color: #505050; + opacity: 1; +} + +*:focus { + outline: none; +} + +.input:focus { + color: black; + border-color: black; +} \ No newline at end of file diff --git a/src/pages/AuthenticationPage/AuthenticationPage.test.js b/src/pages/AuthenticationPage/AuthenticationPage.test.js new file mode 100644 index 0000000..0b6675c --- /dev/null +++ b/src/pages/AuthenticationPage/AuthenticationPage.test.js @@ -0,0 +1,21 @@ +import React from 'react'; +import AuthenticationPage from './AuthenticationPage'; +import {shallow} from 'enzyme'; + +describe('', function () { + it('renders without crashing ', function () { + const wrapper = shallow( {}}/>); + wrapper.unmount(); + }); + + it('test button click', function () { + let pass; + const func = jest.fn((pwd) => {pass = pwd}); + const wrapper = shallow(); + wrapper.setState({pwdText: 'testpwd'}); + wrapper.find('Button').simulate('click'); + + expect(func).toHaveBeenCalledTimes(1); + expect(pass).toBe('testpwd'); + }); +}); diff --git a/src/pages/AuthenticationPage/AuthenticationPage.tsx b/src/pages/AuthenticationPage/AuthenticationPage.tsx new file mode 100644 index 0000000..abe2dc3 --- /dev/null +++ b/src/pages/AuthenticationPage/AuthenticationPage.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import {Button} from "../../elements/GPElements/Button"; +import style from './AuthenticationPage.module.css' + +interface state { + pwdText: string +} + +interface props { + submit: (password: string) => void +} + +class AuthenticationPage extends React.Component { + constructor(props: props) { + super(props); + + this.state = { + pwdText: '' + } + } + + render(): JSX.Element { + return ( + <> +
OpenMediaCenter
+
+
Login
+
+ this.setState({pwdText: ch.target.value})} + value={this.state.pwdText}/> +
+
+
+
+ + ); + } +} + +export default AuthenticationPage; \ No newline at end of file diff --git a/src/setupTests.js b/src/setupTests.js index 2135359..0ebb3a3 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -37,6 +37,7 @@ global.prepareFailingFetchApi = () => { global.callAPIMock = (resonse) => { const helpers = require('./utils/Api'); helpers.callAPI = jest.fn().mockImplementation((_, __, func1) => {func1(resonse);}); + helpers.callApiUnsafe = jest.fn().mockImplementation((_, __, func1) => {func1(resonse);}); }; // code to run before each test diff --git a/src/types/ApiTypes.ts b/src/types/ApiTypes.ts index 51fd3e4..1895a44 100644 --- a/src/types/ApiTypes.ts +++ b/src/types/ApiTypes.ts @@ -33,7 +33,7 @@ export namespace SettingsTypes { export interface initialApiCallData { DarkMode: boolean; Password: boolean; - Mediacenter_name: string; + MediacenterName: string; VideoPath: string; } diff --git a/src/utils/Api.ts b/src/utils/Api.ts index bb72ef3..c0ae974 100644 --- a/src/utils/Api.ts +++ b/src/utils/Api.ts @@ -47,13 +47,14 @@ interface ApiBaseRequest { let apiToken = '' // a callback que to be called after api token refresh -let callQue: (() => void)[] = [] +let callQue: ((error: string) => void)[] = [] // flag to check wheter a api refresh is currently pending let refreshInProcess = false; // store the expire seconds of token let expireSeconds = -1; interface APIToken { + error?: string; access_token: string; expires_in: number; scope: string; @@ -63,8 +64,9 @@ interface APIToken { /** * refresh the api token or use that one in cookie if still valid * @param callback to be called after successful refresh + * @param password */ -export function refreshAPIToken(callback: () => void): void { +export function refreshAPIToken(callback: (error: string) => void, password?: string): void { callQue.push(callback); // check if already is a token refresh is in process @@ -76,30 +78,26 @@ export function refreshAPIToken(callback: () => void): void { refreshInProcess = true; } - // check if a cookie with token is available - const token = getTokenCookie(); - if (token !== null) { - // check if token is at least valid for the next minute - if (token.expire > (new Date().getTime() / 1000) + 60) { - apiToken = token.token; - expireSeconds = token.expire; - callback(); - console.log("token still valid...") - callFuncQue(); - return; - } + if (apiTokenValid()) { + console.log("token still valid...") + callFuncQue(''); + return; } const formData = new FormData(); formData.append("grant_type", "client_credentials"); formData.append("client_id", "openmediacenter"); - formData.append("client_secret", 'openmediacenter'); + formData.append("client_secret", password ? password : 'openmediacenter'); formData.append("scope", 'all'); fetch(getBackendDomain() + '/token', {method: 'POST', body: formData}) .then((response) => response.json() .then((result: APIToken) => { + if (result.error) { + callFuncQue(result.error); + return; + } console.log(result) // set api token apiToken = result.access_token; @@ -107,17 +105,32 @@ export function refreshAPIToken(callback: () => void): void { expireSeconds = (new Date().getTime() / 1000) + result.expires_in; setTokenCookie(apiToken, expireSeconds); // call all handlers and release flag - callFuncQue(); + callFuncQue(''); })); } +export function apiTokenValid(): boolean { + // check if a cookie with token is available + const token = getTokenCookie(); + if (token !== null) { + // check if token is at least valid for the next minute + if (token.expire > (new Date().getTime() / 1000) + 60) { + apiToken = token.token; + expireSeconds = token.expire; + + return true; + } + } + return false; +} + /** * call all qued callbacks */ -function callFuncQue(): void { +function callFuncQue(error: string): void { // call all pending handlers callQue.map(func => { - return func(); + return func(error); }) // reset pending que callQue = [] @@ -221,6 +234,25 @@ export function callAPI(apinode: APINode, }) } +/** + * make a public unsafe api call (without token) -- use as rare as possible only for initialization (eg. check if pwd is neccessary) + * @param apinode + * @param fd + * @param callback + */ +export function callApiUnsafe(apinode: APINode, fd: ApiBaseRequest, callback: (_: T) => void, errorcallback?: (_: string) => void): void { + fetch(getAPIDomain() + apinode, {method: 'POST', body: JSON.stringify(fd),}).then((response) => { + if (response.status !== 200) { + console.log('Error: ' + response.statusText); + // todo place error popup here + } else { + response.json().then((result: T) => { + callback(result); + }) + } + }).catch(reason => errorcallback ? errorcallback(reason) : {}) +} + /** * A backend api call * @param apinode which api backend handler to call @@ -249,5 +281,6 @@ export enum APINode { Settings = 'settings', Tags = 'tags', Actor = 'actor', - Video = 'video' + Video = 'video', + Init = 'init' } diff --git a/src/utils/GlobalInfos.ts b/src/utils/GlobalInfos.ts index 0416d27..8c9a7c4 100644 --- a/src/utils/GlobalInfos.ts +++ b/src/utils/GlobalInfos.ts @@ -23,6 +23,9 @@ class StaticInfos { */ enableDarkTheme(enable = true): void { this.darktheme = enable; + this.handlers.map(func => { + return func(); + }) } /** @@ -33,6 +36,11 @@ class StaticInfos { return this.isDarkTheme() ? darktheme : lighttheme; } + handlers: (() => void)[] = []; + onThemeChange(func: () => void): void { + this.handlers.push(func); + } + /** * set the current videopath * @param vidpath videopath with beginning and ending slash