diff options
19 files changed, 197 insertions, 94 deletions
diff --git a/client/proxy.config.json b/client/proxy.config.json index c6300a412..1c5a84c85 100644 --- a/client/proxy.config.json +++ b/client/proxy.config.json | |||
@@ -7,6 +7,10 @@ | |||
7 | "target": "http://localhost:9000", | 7 | "target": "http://localhost:9000", |
8 | "secure": false | 8 | "secure": false |
9 | }, | 9 | }, |
10 | "/themes": { | ||
11 | "target": "http://localhost:9000", | ||
12 | "secure": false | ||
13 | }, | ||
10 | "/static": { | 14 | "/static": { |
11 | "target": "http://localhost:9000", | 15 | "target": "http://localhost:9000", |
12 | "secure": false | 16 | "secure": false |
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 --> |
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 088234074..81518bbb5 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -4,7 +4,7 @@ import { ServerConfig, UserRight } from '../../../shared' | |||
4 | import { About } from '../../../shared/models/server/about.model' | 4 | import { About } from '../../../shared/models/server/about.model' |
5 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' | 5 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' |
6 | import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup' | 6 | import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup' |
7 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 7 | import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME } from '../../initializers/constants' |
8 | import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' | 8 | import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' |
9 | import { customConfigUpdateValidator } from '../../middlewares/validators/config' | 9 | import { customConfigUpdateValidator } from '../../middlewares/validators/config' |
10 | import { ClientHtml } from '../../lib/client-html' | 10 | import { ClientHtml } from '../../lib/client-html' |
@@ -69,10 +69,11 @@ async function getConfig (req: express.Request, res: express.Response) { | |||
69 | name: t.name, | 69 | name: t.name, |
70 | version: t.version, | 70 | version: t.version, |
71 | description: t.description, | 71 | description: t.description, |
72 | css: t.css, | ||
72 | clientScripts: t.clientScripts | 73 | clientScripts: t.clientScripts |
73 | })) | 74 | })) |
74 | 75 | ||
75 | const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT) | 76 | const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME) |
76 | 77 | ||
77 | const json: ServerConfig = { | 78 | const json: ServerConfig = { |
78 | instance: { | 79 | instance: { |
diff --git a/server/controllers/themes.ts b/server/controllers/themes.ts index 20e7062d0..104c285ad 100644 --- a/server/controllers/themes.ts +++ b/server/controllers/themes.ts | |||
@@ -1,13 +1,11 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' | ||
3 | import { join } from 'path' | 2 | import { join } from 'path' |
4 | import { RegisteredPlugin } from '../lib/plugins/plugin-manager' | 3 | import { RegisteredPlugin } from '../lib/plugins/plugin-manager' |
5 | import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins' | ||
6 | import { serveThemeCSSValidator } from '../middlewares/validators/themes' | 4 | import { serveThemeCSSValidator } from '../middlewares/validators/themes' |
7 | 5 | ||
8 | const themesRouter = express.Router() | 6 | const themesRouter = express.Router() |
9 | 7 | ||
10 | themesRouter.get('/:themeName/:themeVersion/css/:staticEndpoint', | 8 | themesRouter.get('/:themeName/:themeVersion/css/:staticEndpoint(*)', |
11 | serveThemeCSSValidator, | 9 | serveThemeCSSValidator, |
12 | serveThemeCSSDirectory | 10 | serveThemeCSSDirectory |
13 | ) | 11 | ) |
@@ -24,5 +22,9 @@ function serveThemeCSSDirectory (req: express.Request, res: express.Response) { | |||
24 | const plugin: RegisteredPlugin = res.locals.registeredPlugin | 22 | const plugin: RegisteredPlugin = res.locals.registeredPlugin |
25 | const staticEndpoint = req.params.staticEndpoint | 23 | const staticEndpoint = req.params.staticEndpoint |
26 | 24 | ||
27 | return express.static(join(plugin.path, staticEndpoint), { fallthrough: false }) | 25 | if (plugin.css.includes(staticEndpoint) === false) { |
26 | return res.sendStatus(404) | ||
27 | } | ||
28 | |||
29 | return res.sendFile(join(plugin.path, staticEndpoint)) | ||
28 | } | 30 | } |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 9d61ed537..e5f88b71d 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -585,7 +585,8 @@ const P2P_MEDIA_LOADER_PEER_VERSION = 2 | |||
585 | const PLUGIN_GLOBAL_CSS_FILE_NAME = 'plugins-global.css' | 585 | const PLUGIN_GLOBAL_CSS_FILE_NAME = 'plugins-global.css' |
586 | const PLUGIN_GLOBAL_CSS_PATH = join(CONFIG.STORAGE.TMP_DIR, PLUGIN_GLOBAL_CSS_FILE_NAME) | 586 | const PLUGIN_GLOBAL_CSS_PATH = join(CONFIG.STORAGE.TMP_DIR, PLUGIN_GLOBAL_CSS_FILE_NAME) |
587 | 587 | ||
588 | const DEFAULT_THEME = 'default' | 588 | const DEFAULT_THEME_NAME = 'default' |
589 | const DEFAULT_USER_THEME_NAME = 'instance-default' | ||
589 | 590 | ||
590 | // --------------------------------------------------------------------------- | 591 | // --------------------------------------------------------------------------- |
591 | 592 | ||
@@ -660,6 +661,7 @@ export { | |||
660 | PREVIEWS_SIZE, | 661 | PREVIEWS_SIZE, |
661 | REMOTE_SCHEME, | 662 | REMOTE_SCHEME, |
662 | FOLLOW_STATES, | 663 | FOLLOW_STATES, |
664 | DEFAULT_USER_THEME_NAME, | ||
663 | SERVER_ACTOR_NAME, | 665 | SERVER_ACTOR_NAME, |
664 | PLUGIN_GLOBAL_CSS_FILE_NAME, | 666 | PLUGIN_GLOBAL_CSS_FILE_NAME, |
665 | PLUGIN_GLOBAL_CSS_PATH, | 667 | PLUGIN_GLOBAL_CSS_PATH, |
@@ -669,7 +671,7 @@ export { | |||
669 | HLS_STREAMING_PLAYLIST_DIRECTORY, | 671 | HLS_STREAMING_PLAYLIST_DIRECTORY, |
670 | FEEDS, | 672 | FEEDS, |
671 | JOB_TTL, | 673 | JOB_TTL, |
672 | DEFAULT_THEME, | 674 | DEFAULT_THEME_NAME, |
673 | NSFW_POLICY_TYPES, | 675 | NSFW_POLICY_TYPES, |
674 | STATIC_MAX_AGE, | 676 | STATIC_MAX_AGE, |
675 | STATIC_PATHS, | 677 | STATIC_PATHS, |
diff --git a/server/initializers/migrations/0400-user-theme.ts b/server/initializers/migrations/0400-user-theme.ts index 2c1763890..f74d76115 100644 --- a/server/initializers/migrations/0400-user-theme.ts +++ b/server/initializers/migrations/0400-user-theme.ts | |||
@@ -9,7 +9,7 @@ async function up (utils: { | |||
9 | const data = { | 9 | const data = { |
10 | type: Sequelize.STRING, | 10 | type: Sequelize.STRING, |
11 | allowNull: false, | 11 | allowNull: false, |
12 | defaultValue: 'default' | 12 | defaultValue: 'instance-default' |
13 | } | 13 | } |
14 | 14 | ||
15 | await utils.queryInterface.addColumn('user', 'theme', data) | 15 | await utils.queryInterface.addColumn('user', 'theme', data) |
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index 516827a05..ccc963514 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -92,6 +92,7 @@ export class ClientHtml { | |||
92 | let html = buffer.toString() | 92 | let html = buffer.toString() |
93 | 93 | ||
94 | html = ClientHtml.addCustomCSS(html) | 94 | html = ClientHtml.addCustomCSS(html) |
95 | html = ClientHtml.addPluginCSS(html) | ||
95 | 96 | ||
96 | ClientHtml.htmlCache[ path ] = html | 97 | ClientHtml.htmlCache[ path ] = html |
97 | 98 | ||
@@ -138,11 +139,17 @@ export class ClientHtml { | |||
138 | } | 139 | } |
139 | 140 | ||
140 | private static addCustomCSS (htmlStringPage: string) { | 141 | private static addCustomCSS (htmlStringPage: string) { |
141 | const styleTag = '<style class="custom-css-style">' + CONFIG.INSTANCE.CUSTOMIZATIONS.CSS + '</style>' | 142 | const styleTag = `<style class="custom-css-style">${CONFIG.INSTANCE.CUSTOMIZATIONS.CSS}</style>` |
142 | 143 | ||
143 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.CUSTOM_CSS, styleTag) | 144 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.CUSTOM_CSS, styleTag) |
144 | } | 145 | } |
145 | 146 | ||
147 | private static addPluginCSS (htmlStringPage: string) { | ||
148 | const linkTag = `<link rel="stylesheet" href="/plugins/global.css" />` | ||
149 | |||
150 | return htmlStringPage.replace('</head>', linkTag + '</head>') | ||
151 | } | ||
152 | |||
146 | private static addVideoOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) { | 153 | private static addVideoOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) { |
147 | const previewUrl = WEBSERVER.URL + video.getPreviewStaticPath() | 154 | const previewUrl = WEBSERVER.URL + video.getPreviewStaticPath() |
148 | const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() | 155 | const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() |
diff --git a/server/lib/plugins/theme-utils.ts b/server/lib/plugins/theme-utils.ts index 066339e65..76c671f1c 100644 --- a/server/lib/plugins/theme-utils.ts +++ b/server/lib/plugins/theme-utils.ts | |||
@@ -1,18 +1,18 @@ | |||
1 | import { DEFAULT_THEME } from '../../initializers/constants' | 1 | import { DEFAULT_THEME_NAME, DEFAULT_USER_THEME_NAME } from '../../initializers/constants' |
2 | import { PluginManager } from './plugin-manager' | 2 | import { PluginManager } from './plugin-manager' |
3 | import { CONFIG } from '../../initializers/config' | 3 | import { CONFIG } from '../../initializers/config' |
4 | 4 | ||
5 | function getThemeOrDefault (name: string) { | 5 | function getThemeOrDefault (name: string, defaultTheme: string) { |
6 | if (isThemeRegistered(name)) return name | 6 | if (isThemeRegistered(name)) return name |
7 | 7 | ||
8 | // Fallback to admin default theme | 8 | // Fallback to admin default theme |
9 | if (name !== CONFIG.THEME.DEFAULT) return getThemeOrDefault(CONFIG.THEME.DEFAULT) | 9 | if (name !== CONFIG.THEME.DEFAULT) return getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME) |
10 | 10 | ||
11 | return DEFAULT_THEME | 11 | return defaultTheme |
12 | } | 12 | } |
13 | 13 | ||
14 | function isThemeRegistered (name: string) { | 14 | function isThemeRegistered (name: string) { |
15 | if (name === DEFAULT_THEME) return true | 15 | if (name === DEFAULT_THEME_NAME || name === DEFAULT_USER_THEME_NAME) return true |
16 | 16 | ||
17 | return !!PluginManager.Instance.getRegisteredThemes() | 17 | return !!PluginManager.Instance.getRegisteredThemes() |
18 | .find(r => r.name === name) | 18 | .find(r => r.name === name) |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index b8ca1dd5c..6f0b0e00f 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -44,7 +44,7 @@ import { VideoChannelModel } from '../video/video-channel' | |||
44 | import { AccountModel } from './account' | 44 | import { AccountModel } from './account' |
45 | import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' | 45 | import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' |
46 | import { values } from 'lodash' | 46 | import { values } from 'lodash' |
47 | import { DEFAULT_THEME, NSFW_POLICY_TYPES } from '../../initializers/constants' | 47 | import { DEFAULT_THEME_NAME, DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' |
48 | import { clearCacheByUserId } from '../../lib/oauth-model' | 48 | import { clearCacheByUserId } from '../../lib/oauth-model' |
49 | import { UserNotificationSettingModel } from './user-notification-setting' | 49 | import { UserNotificationSettingModel } from './user-notification-setting' |
50 | import { VideoModel } from '../video/video' | 50 | import { VideoModel } from '../video/video' |
@@ -190,7 +190,7 @@ export class UserModel extends Model<UserModel> { | |||
190 | videoQuotaDaily: number | 190 | videoQuotaDaily: number |
191 | 191 | ||
192 | @AllowNull(false) | 192 | @AllowNull(false) |
193 | @Default(DEFAULT_THEME) | 193 | @Default(DEFAULT_THEME_NAME) |
194 | @Is('UserTheme', value => throwIfNotValid(value, isThemeValid, 'theme')) | 194 | @Is('UserTheme', value => throwIfNotValid(value, isThemeValid, 'theme')) |
195 | @Column | 195 | @Column |
196 | theme: string | 196 | theme: string |
@@ -568,7 +568,7 @@ export class UserModel extends Model<UserModel> { | |||
568 | autoPlayVideo: this.autoPlayVideo, | 568 | autoPlayVideo: this.autoPlayVideo, |
569 | videoLanguages: this.videoLanguages, | 569 | videoLanguages: this.videoLanguages, |
570 | role: this.role, | 570 | role: this.role, |
571 | theme: getThemeOrDefault(this.theme), | 571 | theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME), |
572 | roleLabel: USER_ROLE_LABELS[ this.role ], | 572 | roleLabel: USER_ROLE_LABELS[ this.role ], |
573 | videoQuota: this.videoQuota, | 573 | videoQuota: this.videoQuota, |
574 | videoQuotaDaily: this.videoQuotaDaily, | 574 | videoQuotaDaily: this.videoQuotaDaily, |
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts index d6c660aac..3498f86d7 100644 --- a/shared/models/server/server-config.model.ts +++ b/shared/models/server/server-config.model.ts | |||
@@ -1,13 +1,17 @@ | |||
1 | import { NSFWPolicyType } from '../videos/nsfw-policy.type' | 1 | import { NSFWPolicyType } from '../videos/nsfw-policy.type' |
2 | import { ClientScript } from '../plugins/plugin-package-json.model' | 2 | import { ClientScript } from '../plugins/plugin-package-json.model' |
3 | 3 | ||
4 | export type ServerConfigPlugin = { | 4 | export interface ServerConfigPlugin { |
5 | name: string | 5 | name: string |
6 | version: string | 6 | version: string |
7 | description: string | 7 | description: string |
8 | clientScripts: { [name: string]: ClientScript } | 8 | clientScripts: { [name: string]: ClientScript } |
9 | } | 9 | } |
10 | 10 | ||
11 | export interface ServerConfigTheme extends ServerConfigPlugin { | ||
12 | css: string[] | ||
13 | } | ||
14 | |||
11 | export interface ServerConfig { | 15 | export interface ServerConfig { |
12 | serverVersion: string | 16 | serverVersion: string |
13 | serverCommit?: string | 17 | serverCommit?: string |
@@ -29,7 +33,7 @@ export interface ServerConfig { | |||
29 | } | 33 | } |
30 | 34 | ||
31 | theme: { | 35 | theme: { |
32 | registered: ServerConfigPlugin[] | 36 | registered: ServerConfigTheme[] |
33 | default: string | 37 | default: string |
34 | } | 38 | } |
35 | 39 | ||
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index b5823b47a..de9825e1f 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts | |||
@@ -25,6 +25,9 @@ export interface User { | |||
25 | videoQuota: number | 25 | videoQuota: number |
26 | videoQuotaDaily: number | 26 | videoQuotaDaily: number |
27 | createdAt: Date | 27 | createdAt: Date |
28 | |||
29 | theme: string | ||
30 | |||
28 | account: Account | 31 | account: Account |
29 | notificationSettings?: UserNotificationSetting | 32 | notificationSettings?: UserNotificationSetting |
30 | videoChannels?: VideoChannel[] | 33 | videoChannels?: VideoChannel[] |