diff options
Diffstat (limited to 'client/src')
9 files changed, 154 insertions, 74 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index 19a408425..8bd7f7cf6 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts | |||
@@ -75,6 +75,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
75 | 75 | ||
76 | get availableThemes () { | 76 | get availableThemes () { |
77 | return this.serverService.getConfig().theme.registered | 77 | return this.serverService.getConfig().theme.registered |
78 | .map(t => t.name) | ||
78 | } | 79 | } |
79 | 80 | ||
80 | getResolutionKey (resolution: string) { | 81 | getResolutionKey (resolution: string) { |
diff --git a/client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.html index f34e77f6a..f034c6bb3 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.html +++ b/client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.html | |||
@@ -4,10 +4,13 @@ | |||
4 | 4 | ||
5 | <div class="peertube-select-container"> | 5 | <div class="peertube-select-container"> |
6 | <select formControlName="theme" id="theme"> | 6 | <select formControlName="theme" id="theme"> |
7 | <option i18n value="default">default</option> | 7 | <option i18n value="instance-default">instance default</option> |
8 | <option i18n value="default">peertube default</option> | ||
8 | 9 | ||
9 | <option *ngFor="let theme of availableThemes" [value]="theme">{{ theme }}</option> | 10 | <option *ngFor="let theme of availableThemes" [value]="theme">{{ theme }}</option> |
10 | </select> | 11 | </select> |
11 | </div> | 12 | </div> |
12 | </div> | 13 | </div> |
14 | |||
15 | <input type="submit" i18n-value value="Save" [disabled]="!form.valid"> | ||
13 | </form> | 16 | </form> |
diff --git a/client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.ts index f7055072f..5ec1c9f8f 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.ts | |||
@@ -29,6 +29,7 @@ export class MyAccountInterfaceSettingsComponent extends FormReactive implements | |||
29 | 29 | ||
30 | get availableThemes () { | 30 | get availableThemes () { |
31 | return this.serverService.getConfig().theme.registered | 31 | return this.serverService.getConfig().theme.registered |
32 | .map(t => t.name) | ||
32 | } | 33 | } |
33 | 34 | ||
34 | ngOnInit () { | 35 | ngOnInit () { |
@@ -53,9 +54,9 @@ export class MyAccountInterfaceSettingsComponent extends FormReactive implements | |||
53 | 54 | ||
54 | this.userService.updateMyProfile(details).subscribe( | 55 | this.userService.updateMyProfile(details).subscribe( |
55 | () => { | 56 | () => { |
56 | this.notifier.success(this.i18n('Interface settings updated.')) | 57 | this.authService.refreshUserInformation() |
57 | 58 | ||
58 | window.location.reload() | 59 | this.notifier.success(this.i18n('Interface settings updated.')) |
59 | }, | 60 | }, |
60 | 61 | ||
61 | err => this.notifier.error(err.message) | 62 | err => this.notifier.error(err.message) |
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 548173f61..0ebd628fc 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts | |||
@@ -72,6 +72,7 @@ export class AppComponent implements OnInit { | |||
72 | this.serverService.loadVideoPlaylistPrivacies() | 72 | this.serverService.loadVideoPlaylistPrivacies() |
73 | 73 | ||
74 | this.loadPlugins() | 74 | this.loadPlugins() |
75 | this.themeService.initialize() | ||
75 | 76 | ||
76 | // Do not display menu on small screens | 77 | // Do not display menu on small screens |
77 | if (this.screenService.isInSmallView()) { | 78 | if (this.screenService.isInSmallView()) { |
@@ -237,11 +238,7 @@ export class AppComponent implements OnInit { | |||
237 | new Hotkey('g u', (event: KeyboardEvent): boolean => { | 238 | new Hotkey('g u', (event: KeyboardEvent): boolean => { |
238 | this.router.navigate([ '/videos/upload' ]) | 239 | this.router.navigate([ '/videos/upload' ]) |
239 | return false | 240 | return false |
240 | }, undefined, this.i18n('Go to the videos upload page')), | 241 | }, undefined, this.i18n('Go to the videos upload page')) |
241 | new Hotkey('shift+t', (event: KeyboardEvent): boolean => { | ||
242 | this.themeService.toggleDarkTheme() | ||
243 | return false | ||
244 | }, undefined, this.i18n('Toggle Dark theme')) | ||
245 | ]) | 242 | ]) |
246 | } | 243 | } |
247 | } | 244 | } |
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 | } |
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index 588cb8548..7eb6f7b35 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html | |||
@@ -101,12 +101,10 @@ | |||
101 | <span class="language"> | 101 | <span class="language"> |
102 | <span tabindex="0" (keyup.enter)="openLanguageChooser()" (click)="openLanguageChooser()" i18n-title title="Change the language" class="icon icon-language"></span> | 102 | <span tabindex="0" (keyup.enter)="openLanguageChooser()" (click)="openLanguageChooser()" i18n-title title="Change the language" class="icon icon-language"></span> |
103 | </span> | 103 | </span> |
104 | |||
104 | <span class="shortcuts"> | 105 | <span class="shortcuts"> |
105 | <span tabindex="0" (keyup.enter)="openHotkeysCheatSheet()" (click)="openHotkeysCheatSheet()" i18n-title title="Show keyboard shortcuts" class="icon icon-shortcuts"></span> | 106 | <span tabindex="0" (keyup.enter)="openHotkeysCheatSheet()" (click)="openHotkeysCheatSheet()" i18n-title title="Show keyboard shortcuts" class="icon icon-shortcuts"></span> |
106 | </span> | 107 | </span> |
107 | <span class="color-palette"> | ||
108 | <span tabindex="0" (keyup.enter)="toggleDarkTheme()" (click)="toggleDarkTheme()" i18n-title title="Toggle dark interface" class="icon icon-moonsun"></span> | ||
109 | </span> | ||
110 | </div> | 108 | </div> |
111 | </menu> | 109 | </menu> |
112 | </div> | 110 | </div> |
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index 371beb4a5..ede64b7eb 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts | |||
@@ -112,10 +112,6 @@ export class MenuComponent implements OnInit { | |||
112 | this.hotkeysService.cheatSheetToggle.next(!this.helpVisible) | 112 | this.hotkeysService.cheatSheetToggle.next(!this.helpVisible) |
113 | } | 113 | } |
114 | 114 | ||
115 | toggleDarkTheme () { | ||
116 | this.themeService.toggleDarkTheme() | ||
117 | } | ||
118 | |||
119 | private computeIsUserHasAdminAccess () { | 115 | private computeIsUserHasAdminAccess () { |
120 | const right = this.getFirstAdminRightAvailable() | 116 | const right = this.getFirstAdminRightAvailable() |
121 | 117 | ||
diff --git a/client/src/index.html b/client/src/index.html index 6aa885eb7..0b610c55a 100644 --- a/client/src/index.html +++ b/client/src/index.html | |||
@@ -9,19 +9,19 @@ | |||
9 | <!-- Web Manifest file --> | 9 | <!-- Web Manifest file --> |
10 | <link rel="manifest" href="/manifest.webmanifest"> | 10 | <link rel="manifest" href="/manifest.webmanifest"> |
11 | 11 | ||
12 | <!-- /!\ The following comment is used by the server to prerender some tags /!\ --> | ||
13 | |||
14 | <!-- title tag --> | ||
15 | <!-- description tag --> | ||
16 | <!-- custom css tag --> | ||
17 | <!-- meta tags --> | ||
18 | |||
19 | <!-- /!\ Do not remove it /!\ --> | ||
20 | |||
21 | <link rel="icon" type="image/png" href="/client/assets/images/favicon.png" /> | 12 | <link rel="icon" type="image/png" href="/client/assets/images/favicon.png" /> |
22 | 13 | ||
23 | <!-- base url --> | 14 | <!-- base url --> |
24 | <base href="/"> | 15 | <base href="/"> |
16 | |||
17 | <!-- /!\ The following comment is used by the server to prerender some tags /!\ --> | ||
18 | |||
19 | <!-- title tag --> | ||
20 | <!-- description tag --> | ||
21 | <!-- custom css tag --> | ||
22 | <!-- meta tags --> | ||
23 | |||
24 | <!-- /!\ Do not remove it /!\ --> | ||
25 | </head> | 25 | </head> |
26 | 26 | ||
27 | <!-- 3. Display the application --> | 27 | <!-- 3. Display the application --> |