diff options
author | Chocobozzz <me@florianbigard.com> | 2019-07-10 14:06:19 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2019-07-24 10:58:16 +0200 |
commit | ffb321bedca46d6987c7b31dd58e5dea96ea2ea2 (patch) | |
tree | 019f0427c1860ae0b00694c43f1be8d5fe1aa995 /client/src/app/core | |
parent | 7cd4d2ba10106c10602c86f74f55743ded588896 (diff) | |
download | PeerTube-ffb321bedca46d6987c7b31dd58e5dea96ea2ea2.tar.gz PeerTube-ffb321bedca46d6987c7b31dd58e5dea96ea2ea2.tar.zst PeerTube-ffb321bedca46d6987c7b31dd58e5dea96ea2ea2.zip |
WIP plugins: load theme on client side
Diffstat (limited to 'client/src/app/core')
-rw-r--r-- | client/src/app/core/plugins/plugin.service.ts | 60 | ||||
-rw-r--r-- | client/src/app/core/theme/theme.service.ts | 124 |
2 files changed, 134 insertions, 50 deletions
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts index 7f751f479..4abe9ee8d 100644 --- a/client/src/app/core/plugins/plugin.service.ts +++ b/client/src/app/core/plugins/plugin.service.ts | |||
@@ -7,7 +7,7 @@ import { PluginScope } from '@shared/models/plugins/plugin-scope.type' | |||
7 | import { environment } from '../../../environments/environment' | 7 | import { environment } from '../../../environments/environment' |
8 | import { RegisterHookOptions } from '@shared/models/plugins/register.model' | 8 | import { RegisterHookOptions } from '@shared/models/plugins/register.model' |
9 | import { ReplaySubject } from 'rxjs' | 9 | import { ReplaySubject } from 'rxjs' |
10 | import { first } from 'rxjs/operators' | 10 | import { first, shareReplay } from 'rxjs/operators' |
11 | 11 | ||
12 | interface HookStructValue extends RegisterHookOptions { | 12 | interface HookStructValue extends RegisterHookOptions { |
13 | plugin: ServerConfigPlugin | 13 | plugin: ServerConfigPlugin |
@@ -21,6 +21,7 @@ export class PluginService { | |||
21 | private plugins: ServerConfigPlugin[] = [] | 21 | private plugins: ServerConfigPlugin[] = [] |
22 | private scopes: { [ scopeName: string ]: { plugin: ServerConfigPlugin, clientScript: ClientScript }[] } = {} | 22 | private scopes: { [ scopeName: string ]: { plugin: ServerConfigPlugin, clientScript: ClientScript }[] } = {} |
23 | private loadedScripts: { [ script: string ]: boolean } = {} | 23 | private loadedScripts: { [ script: string ]: boolean } = {} |
24 | private loadedScopes: PluginScope[] = [] | ||
24 | 25 | ||
25 | private hooks: { [ name: string ]: HookStructValue[] } = {} | 26 | private hooks: { [ name: string ]: HookStructValue[] } = {} |
26 | 27 | ||
@@ -43,14 +44,48 @@ export class PluginService { | |||
43 | 44 | ||
44 | ensurePluginsAreLoaded () { | 45 | ensurePluginsAreLoaded () { |
45 | return this.pluginsLoaded.asObservable() | 46 | return this.pluginsLoaded.asObservable() |
46 | .pipe(first()) | 47 | .pipe(first(), shareReplay()) |
47 | .toPromise() | 48 | .toPromise() |
48 | } | 49 | } |
49 | 50 | ||
51 | addPlugin (plugin: ServerConfigPlugin) { | ||
52 | for (const key of Object.keys(plugin.clientScripts)) { | ||
53 | const clientScript = plugin.clientScripts[key] | ||
54 | |||
55 | for (const scope of clientScript.scopes) { | ||
56 | if (!this.scopes[scope]) this.scopes[scope] = [] | ||
57 | |||
58 | this.scopes[scope].push({ | ||
59 | plugin, | ||
60 | clientScript: { | ||
61 | script: environment.apiUrl + `/plugins/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`, | ||
62 | scopes: clientScript.scopes | ||
63 | } | ||
64 | }) | ||
65 | |||
66 | this.loadedScripts[clientScript.script] = false | ||
67 | } | ||
68 | } | ||
69 | } | ||
70 | |||
71 | removePlugin (plugin: ServerConfigPlugin) { | ||
72 | for (const key of Object.keys(this.scopes)) { | ||
73 | this.scopes[key] = this.scopes[key].filter(o => o.plugin.name !== plugin.name) | ||
74 | } | ||
75 | } | ||
76 | |||
77 | async reloadLoadedScopes () { | ||
78 | for (const scope of this.loadedScopes) { | ||
79 | await this.loadPluginsByScope(scope) | ||
80 | } | ||
81 | } | ||
82 | |||
50 | async loadPluginsByScope (scope: PluginScope) { | 83 | async loadPluginsByScope (scope: PluginScope) { |
51 | try { | 84 | try { |
52 | await this.ensurePluginsAreLoaded() | 85 | await this.ensurePluginsAreLoaded() |
53 | 86 | ||
87 | this.loadedScopes.push(scope) | ||
88 | |||
54 | const toLoad = this.scopes[ scope ] | 89 | const toLoad = this.scopes[ scope ] |
55 | if (!Array.isArray(toLoad)) return | 90 | if (!Array.isArray(toLoad)) return |
56 | 91 | ||
@@ -63,7 +98,7 @@ export class PluginService { | |||
63 | this.loadedScripts[ clientScript.script ] = true | 98 | this.loadedScripts[ clientScript.script ] = true |
64 | } | 99 | } |
65 | 100 | ||
66 | return Promise.all(promises) | 101 | await Promise.all(promises) |
67 | } catch (err) { | 102 | } catch (err) { |
68 | console.error('Cannot load plugins by scope %s.', scope, err) | 103 | console.error('Cannot load plugins by scope %s.', scope, err) |
69 | } | 104 | } |
@@ -101,29 +136,14 @@ export class PluginService { | |||
101 | 136 | ||
102 | console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name) | 137 | console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name) |
103 | 138 | ||
104 | const url = environment.apiUrl + `/plugins/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}` | 139 | return import(/* webpackIgnore: true */ clientScript.script) |
105 | |||
106 | return import(/* webpackIgnore: true */ url) | ||
107 | .then(script => script.register({ registerHook })) | 140 | .then(script => script.register({ registerHook })) |
108 | .then(() => this.sortHooksByPriority()) | 141 | .then(() => this.sortHooksByPriority()) |
109 | } | 142 | } |
110 | 143 | ||
111 | private buildScopeStruct () { | 144 | private buildScopeStruct () { |
112 | for (const plugin of this.plugins) { | 145 | for (const plugin of this.plugins) { |
113 | for (const key of Object.keys(plugin.clientScripts)) { | 146 | this.addPlugin(plugin) |
114 | const clientScript = plugin.clientScripts[key] | ||
115 | |||
116 | for (const scope of clientScript.scopes) { | ||
117 | if (!this.scopes[scope]) this.scopes[scope] = [] | ||
118 | |||
119 | this.scopes[scope].push({ | ||
120 | plugin, | ||
121 | clientScript | ||
122 | }) | ||
123 | |||
124 | this.loadedScripts[clientScript.script] = false | ||
125 | } | ||
126 | } | ||
127 | } | 147 | } |
128 | } | 148 | } |
129 | 149 | ||
diff --git a/client/src/app/core/theme/theme.service.ts b/client/src/app/core/theme/theme.service.ts index 50c19ecac..ad59c203b 100644 --- a/client/src/app/core/theme/theme.service.ts +++ b/client/src/app/core/theme/theme.service.ts | |||
@@ -1,41 +1,105 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' | 2 | import { AuthService } from '@app/core/auth' |
3 | import { ServerService } from '@app/core/server' | ||
4 | import { environment } from '../../../environments/environment' | ||
5 | import { PluginService } from '@app/core/plugins/plugin.service' | ||
6 | import { ServerConfigTheme } from '@shared/models' | ||
3 | 7 | ||
4 | @Injectable() | 8 | @Injectable() |
5 | export class ThemeService { | 9 | export class ThemeService { |
6 | private theme = document.querySelector('body') | 10 | |
7 | private darkTheme = false | 11 | private oldThemeName: string |
8 | private previousTheme: { [ id: string ]: string } = {} | 12 | private themes: ServerConfigTheme[] = [] |
9 | 13 | ||
10 | constructor () { | 14 | constructor ( |
11 | // initialise the alternative theme with dark theme colors | 15 | private auth: AuthService, |
12 | this.previousTheme['mainBackgroundColor'] = '#111111' | 16 | private pluginService: PluginService, |
13 | this.previousTheme['mainForegroundColor'] = '#fff' | 17 | private server: ServerService |
14 | this.previousTheme['submenuColor'] = 'rgb(32,32,32)' | 18 | ) {} |
15 | this.previousTheme['inputColor'] = 'gray' | 19 | |
16 | this.previousTheme['inputPlaceholderColor'] = '#fff' | 20 | initialize () { |
17 | 21 | this.server.configLoaded | |
18 | this.darkTheme = (peertubeLocalStorage.getItem('theme') === 'dark') | 22 | .subscribe(() => { |
19 | if (this.darkTheme) this.toggleDarkTheme(false) | 23 | this.injectThemes() |
24 | |||
25 | this.listenUserTheme() | ||
26 | }) | ||
27 | } | ||
28 | |||
29 | private injectThemes () { | ||
30 | this.themes = this.server.getConfig().theme.registered | ||
31 | |||
32 | console.log('Injecting %d themes.', this.themes.length) | ||
33 | |||
34 | const head = document.getElementsByTagName('head')[0] | ||
35 | |||
36 | for (const theme of this.themes) { | ||
37 | |||
38 | for (const css of theme.css) { | ||
39 | const link = document.createElement('link') | ||
40 | |||
41 | const href = environment.apiUrl + `/themes/${theme.name}/${theme.version}/css/${css}` | ||
42 | link.setAttribute('href', href) | ||
43 | link.setAttribute('rel', 'alternate stylesheet') | ||
44 | link.setAttribute('type', 'text/css') | ||
45 | link.setAttribute('title', theme.name) | ||
46 | link.setAttribute('disabled', '') | ||
47 | |||
48 | head.appendChild(link) | ||
49 | } | ||
50 | } | ||
51 | } | ||
52 | |||
53 | private getCurrentTheme () { | ||
54 | if (this.auth.isLoggedIn()) { | ||
55 | const theme = this.auth.getUser().theme | ||
56 | if (theme !== 'instance-default') return theme | ||
57 | } | ||
58 | |||
59 | return this.server.getConfig().theme.default | ||
20 | } | 60 | } |
21 | 61 | ||
22 | toggleDarkTheme (setLocalStorage = true) { | 62 | private loadTheme (name: string) { |
23 | // switch properties | 63 | const links = document.getElementsByTagName('link') |
24 | this.switchProperty('mainBackgroundColor') | 64 | for (let i = 0; i < links.length; i++) { |
25 | this.switchProperty('mainForegroundColor') | 65 | const link = links[ i ] |
26 | this.switchProperty('submenuColor') | 66 | if (link.getAttribute('rel').indexOf('style') !== -1 && link.getAttribute('title')) { |
27 | this.switchProperty('inputColor') | 67 | link.disabled = link.getAttribute('title') !== name |
28 | this.switchProperty('inputPlaceholderColor') | 68 | } |
29 | 69 | } | |
30 | if (setLocalStorage) { | 70 | } |
31 | this.darkTheme = !this.darkTheme | 71 | |
32 | peertubeLocalStorage.setItem('theme', (this.darkTheme) ? 'dark' : 'default') | 72 | private updateCurrentTheme () { |
73 | if (this.oldThemeName) { | ||
74 | const oldTheme = this.getTheme(this.oldThemeName) | ||
75 | if (oldTheme) { | ||
76 | console.log('Removing scripts of old theme %s.', this.oldThemeName) | ||
77 | this.pluginService.removePlugin(oldTheme) | ||
78 | } | ||
79 | } | ||
80 | |||
81 | const currentTheme = this.getCurrentTheme() | ||
82 | |||
83 | console.log('Enabling %s theme.', currentTheme) | ||
84 | |||
85 | this.loadTheme(currentTheme) | ||
86 | const theme = this.getTheme(currentTheme) | ||
87 | if (theme) { | ||
88 | console.log('Adding scripts of theme %s.', currentTheme) | ||
89 | this.pluginService.addPlugin(theme) | ||
90 | |||
91 | this.pluginService.reloadLoadedScopes() | ||
33 | } | 92 | } |
93 | |||
94 | this.oldThemeName = currentTheme | ||
95 | } | ||
96 | |||
97 | private listenUserTheme () { | ||
98 | this.auth.userInformationLoaded | ||
99 | .subscribe(() => this.updateCurrentTheme()) | ||
34 | } | 100 | } |
35 | 101 | ||
36 | private switchProperty (property: string, newValue?: string) { | 102 | private getTheme (name: string) { |
37 | const propertyOldvalue = window.getComputedStyle(this.theme).getPropertyValue('--' + property) | 103 | return this.themes.find(t => t.name === name) |
38 | this.theme.style.setProperty('--' + property, (newValue) ? newValue : this.previousTheme[property]) | ||
39 | this.previousTheme[property] = propertyOldvalue | ||
40 | } | 104 | } |
41 | } | 105 | } |