WIP plugins: load theme on client side
This commit is contained in:
parent
7cd4d2ba10
commit
ffb321bedc
19 changed files with 194 additions and 91 deletions
|
@ -7,6 +7,10 @@
|
|||
"target": "http://localhost:9000",
|
||||
"secure": false
|
||||
},
|
||||
"/themes": {
|
||||
"target": "http://localhost:9000",
|
||||
"secure": false
|
||||
},
|
||||
"/static": {
|
||||
"target": "http://localhost:9000",
|
||||
"secure": false
|
||||
|
|
|
@ -75,6 +75,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
|
||||
get availableThemes () {
|
||||
return this.serverService.getConfig().theme.registered
|
||||
.map(t => t.name)
|
||||
}
|
||||
|
||||
getResolutionKey (resolution: string) {
|
||||
|
|
|
@ -4,10 +4,13 @@
|
|||
|
||||
<div class="peertube-select-container">
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="submit" i18n-value value="Save" [disabled]="!form.valid">
|
||||
</form>
|
||||
|
|
|
@ -29,6 +29,7 @@ export class MyAccountInterfaceSettingsComponent extends FormReactive implements
|
|||
|
||||
get availableThemes () {
|
||||
return this.serverService.getConfig().theme.registered
|
||||
.map(t => t.name)
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
|
@ -53,9 +54,9 @@ export class MyAccountInterfaceSettingsComponent extends FormReactive implements
|
|||
|
||||
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)
|
||||
|
|
|
@ -72,6 +72,7 @@ export class AppComponent implements OnInit {
|
|||
this.serverService.loadVideoPlaylistPrivacies()
|
||||
|
||||
this.loadPlugins()
|
||||
this.themeService.initialize()
|
||||
|
||||
// Do not display menu on small screens
|
||||
if (this.screenService.isInSmallView()) {
|
||||
|
@ -237,11 +238,7 @@ export class AppComponent implements OnInit {
|
|||
new Hotkey('g u', (event: KeyboardEvent): boolean => {
|
||||
this.router.navigate([ '/videos/upload' ])
|
||||
return false
|
||||
}, 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'))
|
||||
}, undefined, this.i18n('Go to the videos upload page'))
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { PluginScope } from '@shared/models/plugins/plugin-scope.type'
|
|||
import { environment } from '../../../environments/environment'
|
||||
import { RegisterHookOptions } from '@shared/models/plugins/register.model'
|
||||
import { ReplaySubject } from 'rxjs'
|
||||
import { first } from 'rxjs/operators'
|
||||
import { first, shareReplay } from 'rxjs/operators'
|
||||
|
||||
interface HookStructValue extends RegisterHookOptions {
|
||||
plugin: ServerConfigPlugin
|
||||
|
@ -21,6 +21,7 @@ export class PluginService {
|
|||
private plugins: ServerConfigPlugin[] = []
|
||||
private scopes: { [ scopeName: string ]: { plugin: ServerConfigPlugin, clientScript: ClientScript }[] } = {}
|
||||
private loadedScripts: { [ script: string ]: boolean } = {}
|
||||
private loadedScopes: PluginScope[] = []
|
||||
|
||||
private hooks: { [ name: string ]: HookStructValue[] } = {}
|
||||
|
||||
|
@ -43,14 +44,48 @@ export class PluginService {
|
|||
|
||||
ensurePluginsAreLoaded () {
|
||||
return this.pluginsLoaded.asObservable()
|
||||
.pipe(first())
|
||||
.pipe(first(), shareReplay())
|
||||
.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) {
|
||||
try {
|
||||
await this.ensurePluginsAreLoaded()
|
||||
|
||||
this.loadedScopes.push(scope)
|
||||
|
||||
const toLoad = this.scopes[ scope ]
|
||||
if (!Array.isArray(toLoad)) return
|
||||
|
||||
|
@ -63,7 +98,7 @@ export class PluginService {
|
|||
this.loadedScripts[ clientScript.script ] = true
|
||||
}
|
||||
|
||||
return Promise.all(promises)
|
||||
await Promise.all(promises)
|
||||
} catch (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)
|
||||
|
||||
const url = environment.apiUrl + `/plugins/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`
|
||||
|
||||
return import(/* webpackIgnore: true */ url)
|
||||
return import(/* webpackIgnore: true */ clientScript.script)
|
||||
.then(script => script.register({ registerHook }))
|
||||
.then(() => this.sortHooksByPriority())
|
||||
}
|
||||
|
||||
private buildScopeStruct () {
|
||||
for (const plugin of this.plugins) {
|
||||
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
|
||||
})
|
||||
|
||||
this.loadedScripts[clientScript.script] = false
|
||||
}
|
||||
}
|
||||
this.addPlugin(plugin)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,41 +1,105 @@
|
|||
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()
|
||||
export class ThemeService {
|
||||
private theme = document.querySelector('body')
|
||||
private darkTheme = false
|
||||
private previousTheme: { [ id: string ]: string } = {}
|
||||
|
||||
constructor () {
|
||||
// initialise the alternative theme with dark theme colors
|
||||
this.previousTheme['mainBackgroundColor'] = '#111111'
|
||||
this.previousTheme['mainForegroundColor'] = '#fff'
|
||||
this.previousTheme['submenuColor'] = 'rgb(32,32,32)'
|
||||
this.previousTheme['inputColor'] = 'gray'
|
||||
this.previousTheme['inputPlaceholderColor'] = '#fff'
|
||||
private oldThemeName: string
|
||||
private themes: ServerConfigTheme[] = []
|
||||
|
||||
this.darkTheme = (peertubeLocalStorage.getItem('theme') === 'dark')
|
||||
if (this.darkTheme) this.toggleDarkTheme(false)
|
||||
constructor (
|
||||
private auth: AuthService,
|
||||
private pluginService: PluginService,
|
||||
private server: ServerService
|
||||
) {}
|
||||
|
||||
initialize () {
|
||||
this.server.configLoaded
|
||||
.subscribe(() => {
|
||||
this.injectThemes()
|
||||
|
||||
this.listenUserTheme()
|
||||
})
|
||||
}
|
||||
|
||||
toggleDarkTheme (setLocalStorage = true) {
|
||||
// switch properties
|
||||
this.switchProperty('mainBackgroundColor')
|
||||
this.switchProperty('mainForegroundColor')
|
||||
this.switchProperty('submenuColor')
|
||||
this.switchProperty('inputColor')
|
||||
this.switchProperty('inputPlaceholderColor')
|
||||
private injectThemes () {
|
||||
this.themes = this.server.getConfig().theme.registered
|
||||
|
||||
if (setLocalStorage) {
|
||||
this.darkTheme = !this.darkTheme
|
||||
peertubeLocalStorage.setItem('theme', (this.darkTheme) ? 'dark' : 'default')
|
||||
console.log('Injecting %d themes.', this.themes.length)
|
||||
|
||||
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) {
|
||||
const propertyOldvalue = window.getComputedStyle(this.theme).getPropertyValue('--' + property)
|
||||
this.theme.style.setProperty('--' + property, (newValue) ? newValue : this.previousTheme[property])
|
||||
this.previousTheme[property] = propertyOldvalue
|
||||
private getCurrentTheme () {
|
||||
if (this.auth.isLoggedIn()) {
|
||||
const theme = this.auth.getUser().theme
|
||||
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 tabindex="0" (keyup.enter)="openLanguageChooser()" (click)="openLanguageChooser()" i18n-title title="Change the language" class="icon icon-language"></span>
|
||||
</span>
|
||||
|
||||
<span class="shortcuts">
|
||||
<span tabindex="0" (keyup.enter)="openHotkeysCheatSheet()" (click)="openHotkeysCheatSheet()" i18n-title title="Show keyboard shortcuts" class="icon icon-shortcuts"></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>
|
||||
</menu>
|
||||
</div>
|
||||
|
|
|
@ -112,10 +112,6 @@ export class MenuComponent implements OnInit {
|
|||
this.hotkeysService.cheatSheetToggle.next(!this.helpVisible)
|
||||
}
|
||||
|
||||
toggleDarkTheme () {
|
||||
this.themeService.toggleDarkTheme()
|
||||
}
|
||||
|
||||
private computeIsUserHasAdminAccess () {
|
||||
const right = this.getFirstAdminRightAvailable()
|
||||
|
||||
|
|
|
@ -9,19 +9,19 @@
|
|||
<!-- Web Manifest file -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
|
||||
<!-- /!\ The following comment is used by the server to prerender some tags /!\ -->
|
||||
|
||||
<!-- title tag -->
|
||||
<!-- description tag -->
|
||||
<!-- custom css tag -->
|
||||
<!-- meta tags -->
|
||||
|
||||
<!-- /!\ Do not remove it /!\ -->
|
||||
|
||||
<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 /!\ -->
|
||||
|
||||
<!-- title tag -->
|
||||
<!-- description tag -->
|
||||
<!-- custom css tag -->
|
||||
<!-- meta tags -->
|
||||
|
||||
<!-- /!\ Do not remove it /!\ -->
|
||||
</head>
|
||||
|
||||
<!-- 3. Display the application -->
|
||||
|
|
|
@ -4,7 +4,7 @@ import { ServerConfig, UserRight } from '../../../shared'
|
|||
import { About } from '../../../shared/models/server/about.model'
|
||||
import { CustomConfig } from '../../../shared/models/server/custom-config.model'
|
||||
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 { customConfigUpdateValidator } from '../../middlewares/validators/config'
|
||||
import { ClientHtml } from '../../lib/client-html'
|
||||
|
@ -69,10 +69,11 @@ async function getConfig (req: express.Request, res: express.Response) {
|
|||
name: t.name,
|
||||
version: t.version,
|
||||
description: t.description,
|
||||
css: t.css,
|
||||
clientScripts: t.clientScripts
|
||||
}))
|
||||
|
||||
const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT)
|
||||
const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
|
||||
|
||||
const json: ServerConfig = {
|
||||
instance: {
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import * as express from 'express'
|
||||
import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
|
||||
import { join } from 'path'
|
||||
import { RegisteredPlugin } from '../lib/plugins/plugin-manager'
|
||||
import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins'
|
||||
import { serveThemeCSSValidator } from '../middlewares/validators/themes'
|
||||
|
||||
const themesRouter = express.Router()
|
||||
|
||||
themesRouter.get('/:themeName/:themeVersion/css/:staticEndpoint',
|
||||
themesRouter.get('/:themeName/:themeVersion/css/:staticEndpoint(*)',
|
||||
serveThemeCSSValidator,
|
||||
serveThemeCSSDirectory
|
||||
)
|
||||
|
@ -24,5 +22,9 @@ function serveThemeCSSDirectory (req: express.Request, res: express.Response) {
|
|||
const plugin: RegisteredPlugin = res.locals.registeredPlugin
|
||||
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_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,
|
||||
REMOTE_SCHEME,
|
||||
FOLLOW_STATES,
|
||||
DEFAULT_USER_THEME_NAME,
|
||||
SERVER_ACTOR_NAME,
|
||||
PLUGIN_GLOBAL_CSS_FILE_NAME,
|
||||
PLUGIN_GLOBAL_CSS_PATH,
|
||||
|
@ -669,7 +671,7 @@ export {
|
|||
HLS_STREAMING_PLAYLIST_DIRECTORY,
|
||||
FEEDS,
|
||||
JOB_TTL,
|
||||
DEFAULT_THEME,
|
||||
DEFAULT_THEME_NAME,
|
||||
NSFW_POLICY_TYPES,
|
||||
STATIC_MAX_AGE,
|
||||
STATIC_PATHS,
|
||||
|
|
|
@ -9,7 +9,7 @@ async function up (utils: {
|
|||
const data = {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'default'
|
||||
defaultValue: 'instance-default'
|
||||
}
|
||||
|
||||
await utils.queryInterface.addColumn('user', 'theme', data)
|
||||
|
|
|
@ -92,6 +92,7 @@ export class ClientHtml {
|
|||
let html = buffer.toString()
|
||||
|
||||
html = ClientHtml.addCustomCSS(html)
|
||||
html = ClientHtml.addPluginCSS(html)
|
||||
|
||||
ClientHtml.htmlCache[ path ] = html
|
||||
|
||||
|
@ -138,11 +139,17 @@ export class ClientHtml {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
const previewUrl = WEBSERVER.URL + video.getPreviewStaticPath()
|
||||
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 { CONFIG } from '../../initializers/config'
|
||||
|
||||
function getThemeOrDefault (name: string) {
|
||||
function getThemeOrDefault (name: string, defaultTheme: string) {
|
||||
if (isThemeRegistered(name)) return name
|
||||
|
||||
// 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) {
|
||||
if (name === DEFAULT_THEME) return true
|
||||
if (name === DEFAULT_THEME_NAME || name === DEFAULT_USER_THEME_NAME) return true
|
||||
|
||||
return !!PluginManager.Instance.getRegisteredThemes()
|
||||
.find(r => r.name === name)
|
||||
|
|
|
@ -44,7 +44,7 @@ import { VideoChannelModel } from '../video/video-channel'
|
|||
import { AccountModel } from './account'
|
||||
import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
|
||||
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 { UserNotificationSettingModel } from './user-notification-setting'
|
||||
import { VideoModel } from '../video/video'
|
||||
|
@ -190,7 +190,7 @@ export class UserModel extends Model<UserModel> {
|
|||
videoQuotaDaily: number
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(DEFAULT_THEME)
|
||||
@Default(DEFAULT_THEME_NAME)
|
||||
@Is('UserTheme', value => throwIfNotValid(value, isThemeValid, 'theme'))
|
||||
@Column
|
||||
theme: string
|
||||
|
@ -568,7 +568,7 @@ export class UserModel extends Model<UserModel> {
|
|||
autoPlayVideo: this.autoPlayVideo,
|
||||
videoLanguages: this.videoLanguages,
|
||||
role: this.role,
|
||||
theme: getThemeOrDefault(this.theme),
|
||||
theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME),
|
||||
roleLabel: USER_ROLE_LABELS[ this.role ],
|
||||
videoQuota: this.videoQuota,
|
||||
videoQuotaDaily: this.videoQuotaDaily,
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
|
||||
import { ClientScript } from '../plugins/plugin-package-json.model'
|
||||
|
||||
export type ServerConfigPlugin = {
|
||||
export interface ServerConfigPlugin {
|
||||
name: string
|
||||
version: string
|
||||
description: string
|
||||
clientScripts: { [name: string]: ClientScript }
|
||||
}
|
||||
|
||||
export interface ServerConfigTheme extends ServerConfigPlugin {
|
||||
css: string[]
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
serverVersion: string
|
||||
serverCommit?: string
|
||||
|
@ -29,7 +33,7 @@ export interface ServerConfig {
|
|||
}
|
||||
|
||||
theme: {
|
||||
registered: ServerConfigPlugin[]
|
||||
registered: ServerConfigTheme[]
|
||||
default: string
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,9 @@ export interface User {
|
|||
videoQuota: number
|
||||
videoQuotaDaily: number
|
||||
createdAt: Date
|
||||
|
||||
theme: string
|
||||
|
||||
account: Account
|
||||
notificationSettings?: UserNotificationSetting
|
||||
videoChannels?: VideoChannel[]
|
||||
|
|
Loading…
Reference in a new issue