WIP plugins: load theme on client side
This commit is contained in:
parent
7cd4d2ba10
commit
ffb321bedc
|
@ -7,6 +7,10 @@
|
||||||
"target": "http://localhost:9000",
|
"target": "http://localhost:9000",
|
||||||
"secure": false
|
"secure": false
|
||||||
},
|
},
|
||||||
|
"/themes": {
|
||||||
|
"target": "http://localhost:9000",
|
||||||
|
"secure": false
|
||||||
|
},
|
||||||
"/static": {
|
"/static": {
|
||||||
"target": "http://localhost:9000",
|
"target": "http://localhost:9000",
|
||||||
"secure": false
|
"secure": false
|
||||||
|
|
|
@ -75,6 +75,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
|
|
||||||
get availableThemes () {
|
get availableThemes () {
|
||||||
return this.serverService.getConfig().theme.registered
|
return this.serverService.getConfig().theme.registered
|
||||||
|
.map(t => t.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
getResolutionKey (resolution: string) {
|
getResolutionKey (resolution: string) {
|
||||||
|
|
|
@ -4,10 +4,13 @@
|
||||||
|
|
||||||
<div class="peertube-select-container">
|
<div class="peertube-select-container">
|
||||||
<select formControlName="theme" id="theme">
|
<select formControlName="theme" id="theme">
|
||||||
<option i18n value="default">default</option>
|
<option i18n value="instance-default">instance default</option>
|
||||||
|
<option i18n value="default">peertube default</option>
|
||||||
|
|
||||||
<option *ngFor="let theme of availableThemes" [value]="theme">{{ theme }}</option>
|
<option *ngFor="let theme of availableThemes" [value]="theme">{{ theme }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="submit" i18n-value value="Save" [disabled]="!form.valid">
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -29,6 +29,7 @@ export class MyAccountInterfaceSettingsComponent extends FormReactive implements
|
||||||
|
|
||||||
get availableThemes () {
|
get availableThemes () {
|
||||||
return this.serverService.getConfig().theme.registered
|
return this.serverService.getConfig().theme.registered
|
||||||
|
.map(t => t.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
|
@ -53,9 +54,9 @@ export class MyAccountInterfaceSettingsComponent extends FormReactive implements
|
||||||
|
|
||||||
this.userService.updateMyProfile(details).subscribe(
|
this.userService.updateMyProfile(details).subscribe(
|
||||||
() => {
|
() => {
|
||||||
this.notifier.success(this.i18n('Interface settings updated.'))
|
this.authService.refreshUserInformation()
|
||||||
|
|
||||||
window.location.reload()
|
this.notifier.success(this.i18n('Interface settings updated.'))
|
||||||
},
|
},
|
||||||
|
|
||||||
err => this.notifier.error(err.message)
|
err => this.notifier.error(err.message)
|
||||||
|
|
|
@ -72,6 +72,7 @@ export class AppComponent implements OnInit {
|
||||||
this.serverService.loadVideoPlaylistPrivacies()
|
this.serverService.loadVideoPlaylistPrivacies()
|
||||||
|
|
||||||
this.loadPlugins()
|
this.loadPlugins()
|
||||||
|
this.themeService.initialize()
|
||||||
|
|
||||||
// Do not display menu on small screens
|
// Do not display menu on small screens
|
||||||
if (this.screenService.isInSmallView()) {
|
if (this.screenService.isInSmallView()) {
|
||||||
|
@ -237,11 +238,7 @@ export class AppComponent implements OnInit {
|
||||||
new Hotkey('g u', (event: KeyboardEvent): boolean => {
|
new Hotkey('g u', (event: KeyboardEvent): boolean => {
|
||||||
this.router.navigate([ '/videos/upload' ])
|
this.router.navigate([ '/videos/upload' ])
|
||||||
return false
|
return false
|
||||||
}, undefined, this.i18n('Go to the videos upload page')),
|
}, undefined, this.i18n('Go to the videos upload page'))
|
||||||
new Hotkey('shift+t', (event: KeyboardEvent): boolean => {
|
|
||||||
this.themeService.toggleDarkTheme()
|
|
||||||
return false
|
|
||||||
}, undefined, this.i18n('Toggle Dark theme'))
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { PluginScope } from '@shared/models/plugins/plugin-scope.type'
|
||||||
import { environment } from '../../../environments/environment'
|
import { environment } from '../../../environments/environment'
|
||||||
import { RegisterHookOptions } from '@shared/models/plugins/register.model'
|
import { RegisterHookOptions } from '@shared/models/plugins/register.model'
|
||||||
import { ReplaySubject } from 'rxjs'
|
import { ReplaySubject } from 'rxjs'
|
||||||
import { first } from 'rxjs/operators'
|
import { first, shareReplay } from 'rxjs/operators'
|
||||||
|
|
||||||
interface HookStructValue extends RegisterHookOptions {
|
interface HookStructValue extends RegisterHookOptions {
|
||||||
plugin: ServerConfigPlugin
|
plugin: ServerConfigPlugin
|
||||||
|
@ -21,6 +21,7 @@ export class PluginService {
|
||||||
private plugins: ServerConfigPlugin[] = []
|
private plugins: ServerConfigPlugin[] = []
|
||||||
private scopes: { [ scopeName: string ]: { plugin: ServerConfigPlugin, clientScript: ClientScript }[] } = {}
|
private scopes: { [ scopeName: string ]: { plugin: ServerConfigPlugin, clientScript: ClientScript }[] } = {}
|
||||||
private loadedScripts: { [ script: string ]: boolean } = {}
|
private loadedScripts: { [ script: string ]: boolean } = {}
|
||||||
|
private loadedScopes: PluginScope[] = []
|
||||||
|
|
||||||
private hooks: { [ name: string ]: HookStructValue[] } = {}
|
private hooks: { [ name: string ]: HookStructValue[] } = {}
|
||||||
|
|
||||||
|
@ -43,14 +44,48 @@ export class PluginService {
|
||||||
|
|
||||||
ensurePluginsAreLoaded () {
|
ensurePluginsAreLoaded () {
|
||||||
return this.pluginsLoaded.asObservable()
|
return this.pluginsLoaded.asObservable()
|
||||||
.pipe(first())
|
.pipe(first(), shareReplay())
|
||||||
.toPromise()
|
.toPromise()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addPlugin (plugin: ServerConfigPlugin) {
|
||||||
|
for (const key of Object.keys(plugin.clientScripts)) {
|
||||||
|
const clientScript = plugin.clientScripts[key]
|
||||||
|
|
||||||
|
for (const scope of clientScript.scopes) {
|
||||||
|
if (!this.scopes[scope]) this.scopes[scope] = []
|
||||||
|
|
||||||
|
this.scopes[scope].push({
|
||||||
|
plugin,
|
||||||
|
clientScript: {
|
||||||
|
script: environment.apiUrl + `/plugins/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`,
|
||||||
|
scopes: clientScript.scopes
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.loadedScripts[clientScript.script] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removePlugin (plugin: ServerConfigPlugin) {
|
||||||
|
for (const key of Object.keys(this.scopes)) {
|
||||||
|
this.scopes[key] = this.scopes[key].filter(o => o.plugin.name !== plugin.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async reloadLoadedScopes () {
|
||||||
|
for (const scope of this.loadedScopes) {
|
||||||
|
await this.loadPluginsByScope(scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async loadPluginsByScope (scope: PluginScope) {
|
async loadPluginsByScope (scope: PluginScope) {
|
||||||
try {
|
try {
|
||||||
await this.ensurePluginsAreLoaded()
|
await this.ensurePluginsAreLoaded()
|
||||||
|
|
||||||
|
this.loadedScopes.push(scope)
|
||||||
|
|
||||||
const toLoad = this.scopes[ scope ]
|
const toLoad = this.scopes[ scope ]
|
||||||
if (!Array.isArray(toLoad)) return
|
if (!Array.isArray(toLoad)) return
|
||||||
|
|
||||||
|
@ -63,7 +98,7 @@ export class PluginService {
|
||||||
this.loadedScripts[ clientScript.script ] = true
|
this.loadedScripts[ clientScript.script ] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(promises)
|
await Promise.all(promises)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Cannot load plugins by scope %s.', scope, err)
|
console.error('Cannot load plugins by scope %s.', scope, err)
|
||||||
}
|
}
|
||||||
|
@ -101,29 +136,14 @@ export class PluginService {
|
||||||
|
|
||||||
console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name)
|
console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name)
|
||||||
|
|
||||||
const url = environment.apiUrl + `/plugins/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`
|
return import(/* webpackIgnore: true */ clientScript.script)
|
||||||
|
|
||||||
return import(/* webpackIgnore: true */ url)
|
|
||||||
.then(script => script.register({ registerHook }))
|
.then(script => script.register({ registerHook }))
|
||||||
.then(() => this.sortHooksByPriority())
|
.then(() => this.sortHooksByPriority())
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildScopeStruct () {
|
private buildScopeStruct () {
|
||||||
for (const plugin of this.plugins) {
|
for (const plugin of this.plugins) {
|
||||||
for (const key of Object.keys(plugin.clientScripts)) {
|
this.addPlugin(plugin)
|
||||||
const clientScript = plugin.clientScripts[key]
|
|
||||||
|
|
||||||
for (const scope of clientScript.scopes) {
|
|
||||||
if (!this.scopes[scope]) this.scopes[scope] = []
|
|
||||||
|
|
||||||
this.scopes[scope].push({
|
|
||||||
plugin,
|
|
||||||
clientScript
|
|
||||||
})
|
|
||||||
|
|
||||||
this.loadedScripts[clientScript.script] = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,41 +1,105 @@
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
|
import { AuthService } from '@app/core/auth'
|
||||||
|
import { ServerService } from '@app/core/server'
|
||||||
|
import { environment } from '../../../environments/environment'
|
||||||
|
import { PluginService } from '@app/core/plugins/plugin.service'
|
||||||
|
import { ServerConfigTheme } from '@shared/models'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ThemeService {
|
export class ThemeService {
|
||||||
private theme = document.querySelector('body')
|
|
||||||
private darkTheme = false
|
|
||||||
private previousTheme: { [ id: string ]: string } = {}
|
|
||||||
|
|
||||||
constructor () {
|
private oldThemeName: string
|
||||||
// initialise the alternative theme with dark theme colors
|
private themes: ServerConfigTheme[] = []
|
||||||
this.previousTheme['mainBackgroundColor'] = '#111111'
|
|
||||||
this.previousTheme['mainForegroundColor'] = '#fff'
|
|
||||||
this.previousTheme['submenuColor'] = 'rgb(32,32,32)'
|
|
||||||
this.previousTheme['inputColor'] = 'gray'
|
|
||||||
this.previousTheme['inputPlaceholderColor'] = '#fff'
|
|
||||||
|
|
||||||
this.darkTheme = (peertubeLocalStorage.getItem('theme') === 'dark')
|
constructor (
|
||||||
if (this.darkTheme) this.toggleDarkTheme(false)
|
private auth: AuthService,
|
||||||
|
private pluginService: PluginService,
|
||||||
|
private server: ServerService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
initialize () {
|
||||||
|
this.server.configLoaded
|
||||||
|
.subscribe(() => {
|
||||||
|
this.injectThemes()
|
||||||
|
|
||||||
|
this.listenUserTheme()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleDarkTheme (setLocalStorage = true) {
|
private injectThemes () {
|
||||||
// switch properties
|
this.themes = this.server.getConfig().theme.registered
|
||||||
this.switchProperty('mainBackgroundColor')
|
|
||||||
this.switchProperty('mainForegroundColor')
|
|
||||||
this.switchProperty('submenuColor')
|
|
||||||
this.switchProperty('inputColor')
|
|
||||||
this.switchProperty('inputPlaceholderColor')
|
|
||||||
|
|
||||||
if (setLocalStorage) {
|
console.log('Injecting %d themes.', this.themes.length)
|
||||||
this.darkTheme = !this.darkTheme
|
|
||||||
peertubeLocalStorage.setItem('theme', (this.darkTheme) ? 'dark' : 'default')
|
const head = document.getElementsByTagName('head')[0]
|
||||||
|
|
||||||
|
for (const theme of this.themes) {
|
||||||
|
|
||||||
|
for (const css of theme.css) {
|
||||||
|
const link = document.createElement('link')
|
||||||
|
|
||||||
|
const href = environment.apiUrl + `/themes/${theme.name}/${theme.version}/css/${css}`
|
||||||
|
link.setAttribute('href', href)
|
||||||
|
link.setAttribute('rel', 'alternate stylesheet')
|
||||||
|
link.setAttribute('type', 'text/css')
|
||||||
|
link.setAttribute('title', theme.name)
|
||||||
|
link.setAttribute('disabled', '')
|
||||||
|
|
||||||
|
head.appendChild(link)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private switchProperty (property: string, newValue?: string) {
|
private getCurrentTheme () {
|
||||||
const propertyOldvalue = window.getComputedStyle(this.theme).getPropertyValue('--' + property)
|
if (this.auth.isLoggedIn()) {
|
||||||
this.theme.style.setProperty('--' + property, (newValue) ? newValue : this.previousTheme[property])
|
const theme = this.auth.getUser().theme
|
||||||
this.previousTheme[property] = propertyOldvalue
|
if (theme !== 'instance-default') return theme
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.server.getConfig().theme.default
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadTheme (name: string) {
|
||||||
|
const links = document.getElementsByTagName('link')
|
||||||
|
for (let i = 0; i < links.length; i++) {
|
||||||
|
const link = links[ i ]
|
||||||
|
if (link.getAttribute('rel').indexOf('style') !== -1 && link.getAttribute('title')) {
|
||||||
|
link.disabled = link.getAttribute('title') !== name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCurrentTheme () {
|
||||||
|
if (this.oldThemeName) {
|
||||||
|
const oldTheme = this.getTheme(this.oldThemeName)
|
||||||
|
if (oldTheme) {
|
||||||
|
console.log('Removing scripts of old theme %s.', this.oldThemeName)
|
||||||
|
this.pluginService.removePlugin(oldTheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTheme = this.getCurrentTheme()
|
||||||
|
|
||||||
|
console.log('Enabling %s theme.', currentTheme)
|
||||||
|
|
||||||
|
this.loadTheme(currentTheme)
|
||||||
|
const theme = this.getTheme(currentTheme)
|
||||||
|
if (theme) {
|
||||||
|
console.log('Adding scripts of theme %s.', currentTheme)
|
||||||
|
this.pluginService.addPlugin(theme)
|
||||||
|
|
||||||
|
this.pluginService.reloadLoadedScopes()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.oldThemeName = currentTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
private listenUserTheme () {
|
||||||
|
this.auth.userInformationLoaded
|
||||||
|
.subscribe(() => this.updateCurrentTheme())
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTheme (name: string) {
|
||||||
|
return this.themes.find(t => t.name === name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,12 +101,10 @@
|
||||||
<span class="language">
|
<span class="language">
|
||||||
<span tabindex="0" (keyup.enter)="openLanguageChooser()" (click)="openLanguageChooser()" i18n-title title="Change the language" class="icon icon-language"></span>
|
<span tabindex="0" (keyup.enter)="openLanguageChooser()" (click)="openLanguageChooser()" i18n-title title="Change the language" class="icon icon-language"></span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="shortcuts">
|
<span class="shortcuts">
|
||||||
<span tabindex="0" (keyup.enter)="openHotkeysCheatSheet()" (click)="openHotkeysCheatSheet()" i18n-title title="Show keyboard shortcuts" class="icon icon-shortcuts"></span>
|
<span tabindex="0" (keyup.enter)="openHotkeysCheatSheet()" (click)="openHotkeysCheatSheet()" i18n-title title="Show keyboard shortcuts" class="icon icon-shortcuts"></span>
|
||||||
</span>
|
</span>
|
||||||
<span class="color-palette">
|
|
||||||
<span tabindex="0" (keyup.enter)="toggleDarkTheme()" (click)="toggleDarkTheme()" i18n-title title="Toggle dark interface" class="icon icon-moonsun"></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</menu>
|
</menu>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -112,10 +112,6 @@ export class MenuComponent implements OnInit {
|
||||||
this.hotkeysService.cheatSheetToggle.next(!this.helpVisible)
|
this.hotkeysService.cheatSheetToggle.next(!this.helpVisible)
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleDarkTheme () {
|
|
||||||
this.themeService.toggleDarkTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
private computeIsUserHasAdminAccess () {
|
private computeIsUserHasAdminAccess () {
|
||||||
const right = this.getFirstAdminRightAvailable()
|
const right = this.getFirstAdminRightAvailable()
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,11 @@
|
||||||
<!-- Web Manifest file -->
|
<!-- Web Manifest file -->
|
||||||
<link rel="manifest" href="/manifest.webmanifest">
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" href="/client/assets/images/favicon.png" />
|
||||||
|
|
||||||
|
<!-- base url -->
|
||||||
|
<base href="/">
|
||||||
|
|
||||||
<!-- /!\ The following comment is used by the server to prerender some tags /!\ -->
|
<!-- /!\ The following comment is used by the server to prerender some tags /!\ -->
|
||||||
|
|
||||||
<!-- title tag -->
|
<!-- title tag -->
|
||||||
|
@ -17,11 +22,6 @@
|
||||||
<!-- meta tags -->
|
<!-- meta tags -->
|
||||||
|
|
||||||
<!-- /!\ Do not remove it /!\ -->
|
<!-- /!\ Do not remove it /!\ -->
|
||||||
|
|
||||||
<link rel="icon" type="image/png" href="/client/assets/images/favicon.png" />
|
|
||||||
|
|
||||||
<!-- base url -->
|
|
||||||
<base href="/">
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<!-- 3. Display the application -->
|
<!-- 3. Display the application -->
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { ServerConfig, UserRight } from '../../../shared'
|
||||||
import { About } from '../../../shared/models/server/about.model'
|
import { About } from '../../../shared/models/server/about.model'
|
||||||
import { CustomConfig } from '../../../shared/models/server/custom-config.model'
|
import { CustomConfig } from '../../../shared/models/server/custom-config.model'
|
||||||
import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
|
import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
|
||||||
import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
|
import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME } from '../../initializers/constants'
|
||||||
import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
|
import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
|
||||||
import { customConfigUpdateValidator } from '../../middlewares/validators/config'
|
import { customConfigUpdateValidator } from '../../middlewares/validators/config'
|
||||||
import { ClientHtml } from '../../lib/client-html'
|
import { ClientHtml } from '../../lib/client-html'
|
||||||
|
@ -69,10 +69,11 @@ async function getConfig (req: express.Request, res: express.Response) {
|
||||||
name: t.name,
|
name: t.name,
|
||||||
version: t.version,
|
version: t.version,
|
||||||
description: t.description,
|
description: t.description,
|
||||||
|
css: t.css,
|
||||||
clientScripts: t.clientScripts
|
clientScripts: t.clientScripts
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT)
|
const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
|
||||||
|
|
||||||
const json: ServerConfig = {
|
const json: ServerConfig = {
|
||||||
instance: {
|
instance: {
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
|
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { RegisteredPlugin } from '../lib/plugins/plugin-manager'
|
import { RegisteredPlugin } from '../lib/plugins/plugin-manager'
|
||||||
import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins'
|
|
||||||
import { serveThemeCSSValidator } from '../middlewares/validators/themes'
|
import { serveThemeCSSValidator } from '../middlewares/validators/themes'
|
||||||
|
|
||||||
const themesRouter = express.Router()
|
const themesRouter = express.Router()
|
||||||
|
|
||||||
themesRouter.get('/:themeName/:themeVersion/css/:staticEndpoint',
|
themesRouter.get('/:themeName/:themeVersion/css/:staticEndpoint(*)',
|
||||||
serveThemeCSSValidator,
|
serveThemeCSSValidator,
|
||||||
serveThemeCSSDirectory
|
serveThemeCSSDirectory
|
||||||
)
|
)
|
||||||
|
@ -24,5 +22,9 @@ function serveThemeCSSDirectory (req: express.Request, res: express.Response) {
|
||||||
const plugin: RegisteredPlugin = res.locals.registeredPlugin
|
const plugin: RegisteredPlugin = res.locals.registeredPlugin
|
||||||
const staticEndpoint = req.params.staticEndpoint
|
const staticEndpoint = req.params.staticEndpoint
|
||||||
|
|
||||||
return express.static(join(plugin.path, staticEndpoint), { fallthrough: false })
|
if (plugin.css.includes(staticEndpoint) === false) {
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.sendFile(join(plugin.path, staticEndpoint))
|
||||||
}
|
}
|
||||||
|
|
|
@ -585,7 +585,8 @@ const P2P_MEDIA_LOADER_PEER_VERSION = 2
|
||||||
const PLUGIN_GLOBAL_CSS_FILE_NAME = 'plugins-global.css'
|
const PLUGIN_GLOBAL_CSS_FILE_NAME = 'plugins-global.css'
|
||||||
const PLUGIN_GLOBAL_CSS_PATH = join(CONFIG.STORAGE.TMP_DIR, PLUGIN_GLOBAL_CSS_FILE_NAME)
|
const PLUGIN_GLOBAL_CSS_PATH = join(CONFIG.STORAGE.TMP_DIR, PLUGIN_GLOBAL_CSS_FILE_NAME)
|
||||||
|
|
||||||
const DEFAULT_THEME = 'default'
|
const DEFAULT_THEME_NAME = 'default'
|
||||||
|
const DEFAULT_USER_THEME_NAME = 'instance-default'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -660,6 +661,7 @@ export {
|
||||||
PREVIEWS_SIZE,
|
PREVIEWS_SIZE,
|
||||||
REMOTE_SCHEME,
|
REMOTE_SCHEME,
|
||||||
FOLLOW_STATES,
|
FOLLOW_STATES,
|
||||||
|
DEFAULT_USER_THEME_NAME,
|
||||||
SERVER_ACTOR_NAME,
|
SERVER_ACTOR_NAME,
|
||||||
PLUGIN_GLOBAL_CSS_FILE_NAME,
|
PLUGIN_GLOBAL_CSS_FILE_NAME,
|
||||||
PLUGIN_GLOBAL_CSS_PATH,
|
PLUGIN_GLOBAL_CSS_PATH,
|
||||||
|
@ -669,7 +671,7 @@ export {
|
||||||
HLS_STREAMING_PLAYLIST_DIRECTORY,
|
HLS_STREAMING_PLAYLIST_DIRECTORY,
|
||||||
FEEDS,
|
FEEDS,
|
||||||
JOB_TTL,
|
JOB_TTL,
|
||||||
DEFAULT_THEME,
|
DEFAULT_THEME_NAME,
|
||||||
NSFW_POLICY_TYPES,
|
NSFW_POLICY_TYPES,
|
||||||
STATIC_MAX_AGE,
|
STATIC_MAX_AGE,
|
||||||
STATIC_PATHS,
|
STATIC_PATHS,
|
||||||
|
|
|
@ -9,7 +9,7 @@ async function up (utils: {
|
||||||
const data = {
|
const data = {
|
||||||
type: Sequelize.STRING,
|
type: Sequelize.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 'default'
|
defaultValue: 'instance-default'
|
||||||
}
|
}
|
||||||
|
|
||||||
await utils.queryInterface.addColumn('user', 'theme', data)
|
await utils.queryInterface.addColumn('user', 'theme', data)
|
||||||
|
|
|
@ -92,6 +92,7 @@ export class ClientHtml {
|
||||||
let html = buffer.toString()
|
let html = buffer.toString()
|
||||||
|
|
||||||
html = ClientHtml.addCustomCSS(html)
|
html = ClientHtml.addCustomCSS(html)
|
||||||
|
html = ClientHtml.addPluginCSS(html)
|
||||||
|
|
||||||
ClientHtml.htmlCache[ path ] = html
|
ClientHtml.htmlCache[ path ] = html
|
||||||
|
|
||||||
|
@ -138,11 +139,17 @@ export class ClientHtml {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static addCustomCSS (htmlStringPage: string) {
|
private static addCustomCSS (htmlStringPage: string) {
|
||||||
const styleTag = '<style class="custom-css-style">' + CONFIG.INSTANCE.CUSTOMIZATIONS.CSS + '</style>'
|
const styleTag = `<style class="custom-css-style">${CONFIG.INSTANCE.CUSTOMIZATIONS.CSS}</style>`
|
||||||
|
|
||||||
return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.CUSTOM_CSS, styleTag)
|
return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.CUSTOM_CSS, styleTag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static addPluginCSS (htmlStringPage: string) {
|
||||||
|
const linkTag = `<link rel="stylesheet" href="/plugins/global.css" />`
|
||||||
|
|
||||||
|
return htmlStringPage.replace('</head>', linkTag + '</head>')
|
||||||
|
}
|
||||||
|
|
||||||
private static addVideoOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) {
|
private static addVideoOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) {
|
||||||
const previewUrl = WEBSERVER.URL + video.getPreviewStaticPath()
|
const previewUrl = WEBSERVER.URL + video.getPreviewStaticPath()
|
||||||
const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
|
const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import { DEFAULT_THEME } from '../../initializers/constants'
|
import { DEFAULT_THEME_NAME, DEFAULT_USER_THEME_NAME } from '../../initializers/constants'
|
||||||
import { PluginManager } from './plugin-manager'
|
import { PluginManager } from './plugin-manager'
|
||||||
import { CONFIG } from '../../initializers/config'
|
import { CONFIG } from '../../initializers/config'
|
||||||
|
|
||||||
function getThemeOrDefault (name: string) {
|
function getThemeOrDefault (name: string, defaultTheme: string) {
|
||||||
if (isThemeRegistered(name)) return name
|
if (isThemeRegistered(name)) return name
|
||||||
|
|
||||||
// Fallback to admin default theme
|
// Fallback to admin default theme
|
||||||
if (name !== CONFIG.THEME.DEFAULT) return getThemeOrDefault(CONFIG.THEME.DEFAULT)
|
if (name !== CONFIG.THEME.DEFAULT) return getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
|
||||||
|
|
||||||
return DEFAULT_THEME
|
return defaultTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
function isThemeRegistered (name: string) {
|
function isThemeRegistered (name: string) {
|
||||||
if (name === DEFAULT_THEME) return true
|
if (name === DEFAULT_THEME_NAME || name === DEFAULT_USER_THEME_NAME) return true
|
||||||
|
|
||||||
return !!PluginManager.Instance.getRegisteredThemes()
|
return !!PluginManager.Instance.getRegisteredThemes()
|
||||||
.find(r => r.name === name)
|
.find(r => r.name === name)
|
||||||
|
|
|
@ -44,7 +44,7 @@ import { VideoChannelModel } from '../video/video-channel'
|
||||||
import { AccountModel } from './account'
|
import { AccountModel } from './account'
|
||||||
import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
|
import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
|
||||||
import { values } from 'lodash'
|
import { values } from 'lodash'
|
||||||
import { DEFAULT_THEME, NSFW_POLICY_TYPES } from '../../initializers/constants'
|
import { DEFAULT_THEME_NAME, DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants'
|
||||||
import { clearCacheByUserId } from '../../lib/oauth-model'
|
import { clearCacheByUserId } from '../../lib/oauth-model'
|
||||||
import { UserNotificationSettingModel } from './user-notification-setting'
|
import { UserNotificationSettingModel } from './user-notification-setting'
|
||||||
import { VideoModel } from '../video/video'
|
import { VideoModel } from '../video/video'
|
||||||
|
@ -190,7 +190,7 @@ export class UserModel extends Model<UserModel> {
|
||||||
videoQuotaDaily: number
|
videoQuotaDaily: number
|
||||||
|
|
||||||
@AllowNull(false)
|
@AllowNull(false)
|
||||||
@Default(DEFAULT_THEME)
|
@Default(DEFAULT_THEME_NAME)
|
||||||
@Is('UserTheme', value => throwIfNotValid(value, isThemeValid, 'theme'))
|
@Is('UserTheme', value => throwIfNotValid(value, isThemeValid, 'theme'))
|
||||||
@Column
|
@Column
|
||||||
theme: string
|
theme: string
|
||||||
|
@ -568,7 +568,7 @@ export class UserModel extends Model<UserModel> {
|
||||||
autoPlayVideo: this.autoPlayVideo,
|
autoPlayVideo: this.autoPlayVideo,
|
||||||
videoLanguages: this.videoLanguages,
|
videoLanguages: this.videoLanguages,
|
||||||
role: this.role,
|
role: this.role,
|
||||||
theme: getThemeOrDefault(this.theme),
|
theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME),
|
||||||
roleLabel: USER_ROLE_LABELS[ this.role ],
|
roleLabel: USER_ROLE_LABELS[ this.role ],
|
||||||
videoQuota: this.videoQuota,
|
videoQuota: this.videoQuota,
|
||||||
videoQuotaDaily: this.videoQuotaDaily,
|
videoQuotaDaily: this.videoQuotaDaily,
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
|
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
|
||||||
import { ClientScript } from '../plugins/plugin-package-json.model'
|
import { ClientScript } from '../plugins/plugin-package-json.model'
|
||||||
|
|
||||||
export type ServerConfigPlugin = {
|
export interface ServerConfigPlugin {
|
||||||
name: string
|
name: string
|
||||||
version: string
|
version: string
|
||||||
description: string
|
description: string
|
||||||
clientScripts: { [name: string]: ClientScript }
|
clientScripts: { [name: string]: ClientScript }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ServerConfigTheme extends ServerConfigPlugin {
|
||||||
|
css: string[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServerConfig {
|
export interface ServerConfig {
|
||||||
serverVersion: string
|
serverVersion: string
|
||||||
serverCommit?: string
|
serverCommit?: string
|
||||||
|
@ -29,7 +33,7 @@ export interface ServerConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
theme: {
|
theme: {
|
||||||
registered: ServerConfigPlugin[]
|
registered: ServerConfigTheme[]
|
||||||
default: string
|
default: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,9 @@ export interface User {
|
||||||
videoQuota: number
|
videoQuota: number
|
||||||
videoQuotaDaily: number
|
videoQuotaDaily: number
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
|
|
||||||
|
theme: string
|
||||||
|
|
||||||
account: Account
|
account: Account
|
||||||
notificationSettings?: UserNotificationSetting
|
notificationSettings?: UserNotificationSetting
|
||||||
videoChannels?: VideoChannel[]
|
videoChannels?: VideoChannel[]
|
||||||
|
|
Loading…
Reference in New Issue