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 @@
Video settings
-Notifications
+Notifications
+Interface
+
+
Password
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
+ .map(p => ({
+ name: p.name,
+ version: p.version,
+ description: p.description,
+ clientScripts: p.clientScripts
+ }))
- plugins.push({
- name: plugin.name,
- version: plugin.version,
- description: plugin.description,
- clientScripts: plugin.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
}