From 7cd4d2ba10106c10602c86f74f55743ded588896 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 9 Jul 2019 11:45:19 +0200 Subject: WIP plugins: add theme support --- .../edit-custom-config.component.html | 17 ++++++ .../edit-custom-config.component.ts | 7 +++ .../my-account-interface/index.ts | 1 + .../my-account-interface-settings.component.html | 13 +++++ .../my-account-interface-settings.component.scss | 16 ++++++ .../my-account-interface-settings.component.ts | 64 ++++++++++++++++++++++ .../my-account-settings.component.html | 5 +- .../my-account-settings.component.ts | 2 +- client/src/app/+my-account/my-account.module.ts | 14 ++--- client/src/app/core/plugins/plugin.service.ts | 2 +- client/src/app/core/server/server.service.ts | 8 ++- client/src/app/shared/users/user.model.ts | 4 ++ config/default.yaml | 3 + config/production.yaml.example | 3 + server.ts | 2 +- server/controllers/api/config.ts | 43 ++++++++++----- server/controllers/api/users/me.ts | 1 + server/helpers/custom-validators/plugins.ts | 6 ++ server/initializers/checker-before-init.ts | 3 +- server/initializers/config.ts | 3 + server/initializers/constants.ts | 5 +- server/initializers/migrations/0400-user-theme.ts | 25 +++++++++ server/lib/plugins/plugin-manager.ts | 34 ++++++++++-- server/lib/plugins/theme-utils.ts | 24 ++++++++ server/middlewares/validators/config.ts | 5 +- server/middlewares/validators/plugins.ts | 2 +- server/middlewares/validators/users.ts | 4 ++ server/models/account/user.ts | 11 +++- server/tests/api/check-params/config.ts | 3 + server/tests/api/server/config.ts | 3 + shared/extra-utils/server/config.ts | 3 + shared/models/server/custom-config.model.ts | 4 ++ shared/models/server/server-config.model.ts | 9 ++- shared/models/users/user-update-me.model.ts | 2 + 34 files changed, 312 insertions(+), 39 deletions(-) create mode 100644 client/src/app/+my-account/my-account-settings/my-account-interface/index.ts create mode 100644 client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.html create mode 100644 client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.scss create mode 100644 client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.ts create mode 100644 server/initializers/migrations/0400-user-theme.ts create mode 100644 server/lib/plugins/theme-utils.ts diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index d5b625d9c..fe9d856d0 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html @@ -85,6 +85,23 @@ +
Theme
+ + +
+ + +
+ +
+
+
+ +
Signup
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 055bae851..19a408425 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 @@ -73,6 +73,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { return this.configService.videoQuotaDailyOptions } + get availableThemes () { + return this.serverService.getConfig().theme.registered + } + getResolutionKey (resolution: string) { return 'transcoding.resolutions.' + resolution } @@ -92,6 +96,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { css: null } }, + theme: { + default: null + }, services: { twitter: { username: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME, diff --git a/client/src/app/+my-account/my-account-settings/my-account-interface/index.ts b/client/src/app/+my-account/my-account-settings/my-account-interface/index.ts new file mode 100644 index 000000000..62fce79a8 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-interface/index.ts @@ -0,0 +1 @@ +export * from './my-account-interface-settings.component' 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 new file mode 100644 index 000000000..f34e77f6a --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.html @@ -0,0 +1,13 @@ +
+
+ + +
+ +
+
+
diff --git a/client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.scss b/client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.scss new file mode 100644 index 000000000..629f01733 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.scss @@ -0,0 +1,16 @@ +@import '_variables'; +@import '_mixins'; + +input[type=submit] { + @include peertube-button; + @include orange-button; + + display: block; + margin-top: 15px; +} + +.peertube-select-container { + @include peertube-select-container(340px); + + margin-bottom: 30px; +} 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 new file mode 100644 index 000000000..f7055072f --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.ts @@ -0,0 +1,64 @@ +import { Component, Input, OnInit } from '@angular/core' +import { Notifier, ServerService } from '@app/core' +import { UserUpdateMe } from '../../../../../../shared' +import { AuthService } from '../../../core' +import { FormReactive, User, UserService } from '../../../shared' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' +import { Subject } from 'rxjs' + +@Component({ + selector: 'my-account-interface-settings', + templateUrl: './my-account-interface-settings.component.html', + styleUrls: [ './my-account-interface-settings.component.scss' ] +}) +export class MyAccountInterfaceSettingsComponent extends FormReactive implements OnInit { + @Input() user: User = null + @Input() userInformationLoaded: Subject + + constructor ( + protected formValidatorService: FormValidatorService, + private authService: AuthService, + private notifier: Notifier, + private userService: UserService, + private serverService: ServerService, + private i18n: I18n + ) { + super() + } + + get availableThemes () { + return this.serverService.getConfig().theme.registered + } + + ngOnInit () { + this.buildForm({ + theme: null + }) + + this.userInformationLoaded + .subscribe(() => { + this.form.patchValue({ + theme: this.user.theme + }) + }) + } + + updateInterfaceSettings () { + const theme = this.form.value['theme'] + + const details: UserUpdateMe = { + theme + } + + this.userService.updateMyProfile(details).subscribe( + () => { + this.notifier.success(this.i18n('Interface settings updated.')) + + window.location.reload() + }, + + err => this.notifier.error(err.message) + ) + } +} diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html index e51302f7c..eb9367d1f 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html @@ -10,9 +10,12 @@ - + + + + diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts index f4b954e54..95fd2a3db 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, ViewChild } from '@angular/core' +import { Component, OnInit } from '@angular/core' import { Notifier } from '@app/core' import { BytesPipe } from 'ngx-pipes' import { AuthService } from '../../core' diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index a1b198e3e..5be1b0d05 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts @@ -25,19 +25,14 @@ import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-b import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component' import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component' import { MyAccountNotificationPreferencesComponent } from '@app/+my-account/my-account-settings/my-account-notification-preferences' -import { - MyAccountVideoPlaylistCreateComponent -} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component' -import { - MyAccountVideoPlaylistUpdateComponent -} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component' +import { MyAccountVideoPlaylistCreateComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component' +import { MyAccountVideoPlaylistUpdateComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component' import { MyAccountVideoPlaylistsComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlists.component' -import { - MyAccountVideoPlaylistElementsComponent -} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component' +import { MyAccountVideoPlaylistElementsComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component' import { DragDropModule } from '@angular/cdk/drag-drop' import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-settings/my-account-change-email' import { MultiSelectModule } from 'primeng/primeng' +import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface' @NgModule({ imports: [ @@ -58,6 +53,7 @@ import { MultiSelectModule } from 'primeng/primeng' MyAccountVideoSettingsComponent, MyAccountProfileComponent, MyAccountChangeEmailComponent, + MyAccountInterfaceSettingsComponent, MyAccountVideosComponent, diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts index 6c567d3ca..7f751f479 100644 --- a/client/src/app/core/plugins/plugin.service.ts +++ b/client/src/app/core/plugins/plugin.service.ts @@ -33,7 +33,7 @@ export class PluginService { initializePlugins () { this.server.configLoaded .subscribe(() => { - this.plugins = this.server.getConfig().plugins + this.plugins = this.server.getConfig().plugin.registered this.buildScopeStruct() diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 80c52164d..7fb95fe4e 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts @@ -42,7 +42,13 @@ export class ServerService { css: '' } }, - plugins: [], + plugin: { + registered: [] + }, + theme: { + registered: [], + default: 'default' + }, email: { enabled: false }, diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts index 95a6ce9f9..53809f82c 100644 --- a/client/src/app/shared/users/user.model.ts +++ b/client/src/app/shared/users/user.model.ts @@ -26,6 +26,8 @@ export class User implements UserServerModel { videoChannels: VideoChannel[] createdAt: Date + theme: string + adminFlags?: UserAdminFlag blocked: boolean @@ -49,6 +51,8 @@ export class User implements UserServerModel { this.autoPlayVideo = hash.autoPlayVideo this.createdAt = hash.createdAt + this.theme = hash.theme + this.adminFlags = hash.adminFlags this.blocked = hash.blocked diff --git a/config/default.yaml b/config/default.yaml index ff3d6d54c..a1b2991cf 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -264,3 +264,6 @@ followers: enabled: true # Whether or not an administrator must manually validate a new follower manual_approval: false + +theme: + default: 'default' diff --git a/config/production.yaml.example b/config/production.yaml.example index 7158e076b..6c2eb4416 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -279,3 +279,6 @@ followers: enabled: true # Whether or not an administrator must manually validate a new follower manual_approval: false + +theme: + default: 'default' diff --git a/server.ts b/server.ts index ac373b041..d8e8f1e97 100644 --- a/server.ts +++ b/server.ts @@ -261,7 +261,7 @@ async function startApplication () { updateStreamingPlaylistsInfohashesIfNeeded() .catch(err => logger.error('Cannot update streaming playlist infohashes.', { err })) - await PluginManager.Instance.registerPlugins() + await PluginManager.Instance.registerPluginsAndThemes() // Make server listening server.listen(port, hostname, () => { diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 8563b7437..088234074 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts @@ -1,6 +1,6 @@ import * as express from 'express' import { snakeCase } from 'lodash' -import { ServerConfig, ServerConfigPlugin, UserRight } from '../../../shared' +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' @@ -16,7 +16,7 @@ import { isNumeric } from 'validator' import { objectConverter } from '../../helpers/core-utils' import { CONFIG, reloadConfig } from '../../initializers/config' import { PluginManager } from '../../lib/plugins/plugin-manager' -import { PluginType } from '../../../shared/models/plugins/plugin.type' +import { getThemeOrDefault } from '../../lib/plugins/theme-utils' const packageJSON = require('../../../../package.json') const configRouter = express.Router() @@ -56,19 +56,23 @@ async function getConfig (req: express.Request, res: express.Response) { .filter(key => CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.RESOLUTIONS[key] === true) .map(r => parseInt(r, 10)) - const plugins: ServerConfigPlugin[] = [] const registeredPlugins = PluginManager.Instance.getRegisteredPlugins() - for (const pluginName of Object.keys(registeredPlugins)) { - const plugin = registeredPlugins[ pluginName ] - if (plugin.type !== PluginType.PLUGIN) continue - - plugins.push({ - name: plugin.name, - version: plugin.version, - description: plugin.description, - clientScripts: plugin.clientScripts - }) - } + .map(p => ({ + name: p.name, + version: p.version, + description: p.description, + clientScripts: p.clientScripts + })) + + const registeredThemes = PluginManager.Instance.getRegisteredThemes() + .map(t => ({ + name: t.name, + version: t.version, + description: t.description, + clientScripts: t.clientScripts + })) + + const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT) const json: ServerConfig = { instance: { @@ -82,7 +86,13 @@ async function getConfig (req: express.Request, res: express.Response) { css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS } }, - plugins, + plugin: { + registered: registeredPlugins + }, + theme: { + registered: registeredThemes, + default: defaultTheme + }, email: { enabled: Emailer.isEnabled() }, @@ -240,6 +250,9 @@ function customConfig (): CustomConfig { javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT } }, + theme: { + default: CONFIG.THEME.DEFAULT + }, services: { twitter: { username: CONFIG.SERVICES.TWITTER.USERNAME, diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index a078334fe..e7ed3de64 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts @@ -183,6 +183,7 @@ async function updateMe (req: express.Request, res: express.Response) { if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages + if (body.theme !== undefined) user.theme = body.theme if (body.email !== undefined) { if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { diff --git a/server/helpers/custom-validators/plugins.ts b/server/helpers/custom-validators/plugins.ts index 2fcdc581f..4ab5f9ce8 100644 --- a/server/helpers/custom-validators/plugins.ts +++ b/server/helpers/custom-validators/plugins.ts @@ -4,6 +4,7 @@ import { PluginType } from '../../../shared/models/plugins/plugin.type' import { CONSTRAINTS_FIELDS } from '../../initializers/constants' import { PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model' import { isUrlValid } from './activitypub/misc' +import { isThemeRegistered } from '../../lib/plugins/theme-utils' const PLUGINS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.PLUGINS @@ -61,6 +62,10 @@ function isCSSPathsValid (css: any[]) { return isArray(css) && css.every(c => isSafePath(c)) } +function isThemeValid (name: string) { + return isPluginNameValid(name) && isThemeRegistered(name) +} + function isPackageJSONValid (packageJSON: PluginPackageJson, pluginType: PluginType) { return isNpmPluginNameValid(packageJSON.name) && isPluginDescriptionValid(packageJSON.description) && @@ -82,6 +87,7 @@ function isLibraryCodeValid (library: any) { export { isPluginTypeValid, isPackageJSONValid, + isThemeValid, isPluginVersionValid, isPluginNameValid, isPluginDescriptionValid, diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 1f5ec20df..c94bca2f8 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts @@ -29,7 +29,8 @@ function checkMissedConfig () { 'followers.instance.enabled', 'followers.instance.manual_approval', 'tracker.enabled', 'tracker.private', 'tracker.reject_too_many_announces', 'history.videos.max_age', 'views.videos.remote.max_age', - 'rates_limit.login.window', 'rates_limit.login.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max' + 'rates_limit.login.window', 'rates_limit.login.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max', + 'theme.default' ] const requiredAlternatives = [ [ // set diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 6737edcd6..dfc4bea21 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts @@ -224,6 +224,9 @@ const CONFIG = { get ENABLED () { return config.get('followers.instance.enabled') }, get MANUAL_APPROVAL () { return config.get('followers.instance.manual_approval') } } + }, + THEME: { + get DEFAULT () { return config.get('theme.default') } } } diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 8ceefbd0e..9d61ed537 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 395 +const LAST_MIGRATION_VERSION = 400 // --------------------------------------------------------------------------- @@ -585,6 +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' + // --------------------------------------------------------------------------- // Special constants for a test instance @@ -667,6 +669,7 @@ export { HLS_STREAMING_PLAYLIST_DIRECTORY, FEEDS, JOB_TTL, + DEFAULT_THEME, NSFW_POLICY_TYPES, STATIC_MAX_AGE, STATIC_PATHS, diff --git a/server/initializers/migrations/0400-user-theme.ts b/server/initializers/migrations/0400-user-theme.ts new file mode 100644 index 000000000..2c1763890 --- /dev/null +++ b/server/initializers/migrations/0400-user-theme.ts @@ -0,0 +1,25 @@ +import * as Sequelize from 'sequelize' + +async function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize, + db: any +}): Promise { + const data = { + type: Sequelize.STRING, + allowNull: false, + defaultValue: 'default' + } + + await utils.queryInterface.addColumn('user', 'theme', data) +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { + up, + down +} diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index 7cbfa8569..8496979f8 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts @@ -11,6 +11,7 @@ import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants' import { PluginType } from '../../../shared/models/plugins/plugin.type' import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' import { outputFile } from 'fs-extra' +import { ServerConfigPlugin } from '../../../shared/models/server' export interface RegisteredPlugin { name: string @@ -47,7 +48,7 @@ export class PluginManager { private constructor () { } - async registerPlugins () { + async registerPluginsAndThemes () { await this.resetCSSGlobalFile() const plugins = await PluginModel.listEnabledPluginsAndThemes() @@ -63,12 +64,20 @@ export class PluginManager { this.sortHooksByPriority() } + getRegisteredPluginOrTheme (name: string) { + return this.registeredPlugins[name] + } + getRegisteredPlugin (name: string) { - return this.registeredPlugins[ name ] + const registered = this.getRegisteredPluginOrTheme(name) + + if (!registered || registered.type !== PluginType.PLUGIN) return undefined + + return registered } getRegisteredTheme (name: string) { - const registered = this.getRegisteredPlugin(name) + const registered = this.getRegisteredPluginOrTheme(name) if (!registered || registered.type !== PluginType.THEME) return undefined @@ -76,7 +85,11 @@ export class PluginManager { } getRegisteredPlugins () { - return this.registeredPlugins + return this.getRegisteredPluginsOrThemes(PluginType.PLUGIN) + } + + getRegisteredThemes () { + return this.getRegisteredPluginsOrThemes(PluginType.THEME) } async runHook (hookName: string, param?: any) { @@ -309,6 +322,19 @@ export class PluginManager { } } + private getRegisteredPluginsOrThemes (type: PluginType) { + const plugins: RegisteredPlugin[] = [] + + for (const pluginName of Object.keys(this.registeredPlugins)) { + const plugin = this.registeredPlugins[ pluginName ] + if (plugin.type !== type) continue + + plugins.push(plugin) + } + + return plugins + } + static get Instance () { return this.instance || (this.instance = new this()) } diff --git a/server/lib/plugins/theme-utils.ts b/server/lib/plugins/theme-utils.ts new file mode 100644 index 000000000..066339e65 --- /dev/null +++ b/server/lib/plugins/theme-utils.ts @@ -0,0 +1,24 @@ +import { DEFAULT_THEME } from '../../initializers/constants' +import { PluginManager } from './plugin-manager' +import { CONFIG } from '../../initializers/config' + +function getThemeOrDefault (name: string) { + if (isThemeRegistered(name)) return name + + // Fallback to admin default theme + if (name !== CONFIG.THEME.DEFAULT) return getThemeOrDefault(CONFIG.THEME.DEFAULT) + + return DEFAULT_THEME +} + +function isThemeRegistered (name: string) { + if (name === DEFAULT_THEME) return true + + return !!PluginManager.Instance.getRegisteredThemes() + .find(r => r.name === name) +} + +export { + getThemeOrDefault, + isThemeRegistered +} diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index d015fa6fe..31b131914 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts @@ -1,10 +1,11 @@ import * as express from 'express' import { body } from 'express-validator/check' -import { isUserNSFWPolicyValid, isUserVideoQuotaValid, isUserVideoQuotaDailyValid } from '../../helpers/custom-validators/users' +import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' import { logger } from '../../helpers/logger' import { CustomConfig } from '../../../shared/models/server/custom-config.model' import { Emailer } from '../../lib/emailer' import { areValidationErrors } from './utils' +import { isThemeValid } from '../../helpers/custom-validators/plugins' const customConfigUpdateValidator = [ body('instance.name').exists().withMessage('Should have a valid instance name'), @@ -47,6 +48,8 @@ const customConfigUpdateValidator = [ body('followers.instance.enabled').isBoolean().withMessage('Should have a valid followers of instance boolean'), body('followers.instance.manualApproval').isBoolean().withMessage('Should have a valid manual approval boolean'), + body('theme.default').custom(isThemeValid).withMessage('Should have a valid theme'), + async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking customConfigUpdateValidator parameters', { parameters: req.body }) diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts index 672299ee1..fcb461624 100644 --- a/server/middlewares/validators/plugins.ts +++ b/server/middlewares/validators/plugins.ts @@ -16,7 +16,7 @@ const servePluginStaticDirectoryValidator = [ if (areValidationErrors(req, res)) return - const plugin = PluginManager.Instance.getRegisteredPlugin(req.params.pluginName) + const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(req.params.pluginName) if (!plugin || plugin.version !== req.params.pluginVersion) { return res.sendStatus(404) diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 947ed36c3..df7f77b84 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -28,6 +28,7 @@ import { ActorModel } from '../../models/activitypub/actor' import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor' import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels' import { UserRegister } from '../../../shared/models/users/user-register.model' +import { isThemeValid } from '../../helpers/custom-validators/plugins' const usersAddValidator = [ body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'), @@ -204,6 +205,9 @@ const usersUpdateMeValidator = [ body('videosHistoryEnabled') .optional() .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'), + body('theme') + .optional() + .custom(isThemeValid).withMessage('Should have a valid theme'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') }) diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 0f425bb82..b8ca1dd5c 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -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 { NSFW_POLICY_TYPES } from '../../initializers/constants' +import { DEFAULT_THEME, NSFW_POLICY_TYPES } from '../../initializers/constants' import { clearCacheByUserId } from '../../lib/oauth-model' import { UserNotificationSettingModel } from './user-notification-setting' import { VideoModel } from '../video/video' @@ -52,6 +52,8 @@ import { ActorModel } from '../activitypub/actor' import { ActorFollowModel } from '../activitypub/actor-follow' import { VideoImportModel } from '../video/video-import' import { UserAdminFlag } from '../../../shared/models/users/user-flag.model' +import { isThemeValid } from '../../helpers/custom-validators/plugins' +import { getThemeOrDefault } from '../../lib/plugins/theme-utils' enum ScopeNames { WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' @@ -187,6 +189,12 @@ export class UserModel extends Model { @Column(DataType.BIGINT) videoQuotaDaily: number + @AllowNull(false) + @Default(DEFAULT_THEME) + @Is('UserTheme', value => throwIfNotValid(value, isThemeValid, 'theme')) + @Column + theme: string + @CreatedAt createdAt: Date @@ -560,6 +568,7 @@ export class UserModel extends Model { autoPlayVideo: this.autoPlayVideo, videoLanguages: this.videoLanguages, role: this.role, + theme: getThemeOrDefault(this.theme), roleLabel: USER_ROLE_LABELS[ this.role ], videoQuota: this.videoQuota, videoQuotaDaily: this.videoQuotaDaily, diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index a0d9392dc..7773ae1e7 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts @@ -27,6 +27,9 @@ describe('Test config API validators', function () { css: 'body { background-color: red; }' } }, + theme: { + default: 'default' + }, services: { twitter: { username: '@MySuperUsername', diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index c39516dee..78fdc9cc0 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts @@ -190,6 +190,9 @@ describe('Test config', function () { css: 'body { background-color: red; }' } }, + theme: { + default: 'default' + }, services: { twitter: { username: '@Kuja', diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts index 2b7965bc2..8736f083f 100644 --- a/shared/extra-utils/server/config.ts +++ b/shared/extra-utils/server/config.ts @@ -59,6 +59,9 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) { css: 'body { background-color: red; }' } }, + theme: { + default: 'default' + }, services: { twitter: { username: '@MySuperUsername', diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts index 670553d16..a0541f5b6 100644 --- a/shared/models/server/custom-config.model.ts +++ b/shared/models/server/custom-config.model.ts @@ -15,6 +15,10 @@ export interface CustomConfig { } } + theme: { + default: string + } + services: { twitter: { username: string diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts index c259a849a..d6c660aac 100644 --- a/shared/models/server/server-config.model.ts +++ b/shared/models/server/server-config.model.ts @@ -24,7 +24,14 @@ export interface ServerConfig { } } - plugins: ServerConfigPlugin[] + plugin: { + registered: ServerConfigPlugin[] + } + + theme: { + registered: ServerConfigPlugin[] + default: string + } email: { enabled: boolean diff --git a/shared/models/users/user-update-me.model.ts b/shared/models/users/user-update-me.model.ts index 6e6cd7115..b6c0002e5 100644 --- a/shared/models/users/user-update-me.model.ts +++ b/shared/models/users/user-update-me.model.ts @@ -13,4 +13,6 @@ export interface UserUpdateMe { email?: string currentPassword?: string password?: string + + theme?: string } -- cgit v1.2.3