WIP plugins: add theme support
This commit is contained in:
parent
8d76959e11
commit
7cd4d2ba10
|
@ -85,6 +85,23 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
|
<div i18n class="inner-form-title">Theme</div>
|
||||||
|
|
||||||
|
<ng-container formGroupName="theme">
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="themeDefault">Global theme</label>
|
||||||
|
|
||||||
|
<div class="peertube-select-container">
|
||||||
|
<select formControlName="default" id="themeDefault">
|
||||||
|
<option i18n value="default">default</option>
|
||||||
|
|
||||||
|
<option *ngFor="let theme of availableThemes" [value]="theme">{{ theme }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
<div i18n class="inner-form-title">Signup</div>
|
<div i18n class="inner-form-title">Signup</div>
|
||||||
|
|
||||||
<ng-container formGroupName="signup">
|
<ng-container formGroupName="signup">
|
||||||
|
|
|
@ -73,6 +73,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
return this.configService.videoQuotaDailyOptions
|
return this.configService.videoQuotaDailyOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get availableThemes () {
|
||||||
|
return this.serverService.getConfig().theme.registered
|
||||||
|
}
|
||||||
|
|
||||||
getResolutionKey (resolution: string) {
|
getResolutionKey (resolution: string) {
|
||||||
return 'transcoding.resolutions.' + resolution
|
return 'transcoding.resolutions.' + resolution
|
||||||
}
|
}
|
||||||
|
@ -92,6 +96,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
css: null
|
css: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
theme: {
|
||||||
|
default: null
|
||||||
|
},
|
||||||
services: {
|
services: {
|
||||||
twitter: {
|
twitter: {
|
||||||
username: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME,
|
username: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME,
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './my-account-interface-settings.component'
|
|
@ -0,0 +1,13 @@
|
||||||
|
<form role="form" (ngSubmit)="updateInterfaceSettings()" [formGroup]="form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="theme">Theme</label>
|
||||||
|
|
||||||
|
<div class="peertube-select-container">
|
||||||
|
<select formControlName="theme" id="theme">
|
||||||
|
<option i18n value="default">default</option>
|
||||||
|
|
||||||
|
<option *ngFor="let theme of availableThemes" [value]="theme">{{ theme }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
|
@ -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;
|
||||||
|
}
|
|
@ -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<any>
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,9 +10,12 @@
|
||||||
<div i18n class="account-title">Video settings</div>
|
<div i18n class="account-title">Video settings</div>
|
||||||
<my-account-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-video-settings>
|
<my-account-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-video-settings>
|
||||||
|
|
||||||
<div i18n class="account-title" id="notifications">Notifications</div>
|
<div i18n class="account-title">Notifications</div>
|
||||||
<my-account-notification-preferences [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-notification-preferences>
|
<my-account-notification-preferences [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-notification-preferences>
|
||||||
|
|
||||||
|
<div i18n class="account-title">Interface</div>
|
||||||
|
<my-account-interface-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-interface-settings>
|
||||||
|
|
||||||
<div i18n class="account-title">Password</div>
|
<div i18n class="account-title">Password</div>
|
||||||
<my-account-change-password></my-account-change-password>
|
<my-account-change-password></my-account-change-password>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
import { Component, OnInit } from '@angular/core'
|
||||||
import { Notifier } from '@app/core'
|
import { Notifier } from '@app/core'
|
||||||
import { BytesPipe } from 'ngx-pipes'
|
import { BytesPipe } from 'ngx-pipes'
|
||||||
import { AuthService } from '../../core'
|
import { AuthService } from '../../core'
|
||||||
|
|
|
@ -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 { 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 { 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 { MyAccountNotificationPreferencesComponent } from '@app/+my-account/my-account-settings/my-account-notification-preferences'
|
||||||
import {
|
import { MyAccountVideoPlaylistCreateComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component'
|
||||||
MyAccountVideoPlaylistCreateComponent
|
import { MyAccountVideoPlaylistUpdateComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component'
|
||||||
} 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 { MyAccountVideoPlaylistsComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlists.component'
|
||||||
import {
|
import { MyAccountVideoPlaylistElementsComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component'
|
||||||
MyAccountVideoPlaylistElementsComponent
|
|
||||||
} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component'
|
|
||||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||||
import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-settings/my-account-change-email'
|
import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-settings/my-account-change-email'
|
||||||
import { MultiSelectModule } from 'primeng/primeng'
|
import { MultiSelectModule } from 'primeng/primeng'
|
||||||
|
import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -58,6 +53,7 @@ import { MultiSelectModule } from 'primeng/primeng'
|
||||||
MyAccountVideoSettingsComponent,
|
MyAccountVideoSettingsComponent,
|
||||||
MyAccountProfileComponent,
|
MyAccountProfileComponent,
|
||||||
MyAccountChangeEmailComponent,
|
MyAccountChangeEmailComponent,
|
||||||
|
MyAccountInterfaceSettingsComponent,
|
||||||
|
|
||||||
MyAccountVideosComponent,
|
MyAccountVideosComponent,
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ export class PluginService {
|
||||||
initializePlugins () {
|
initializePlugins () {
|
||||||
this.server.configLoaded
|
this.server.configLoaded
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.plugins = this.server.getConfig().plugins
|
this.plugins = this.server.getConfig().plugin.registered
|
||||||
|
|
||||||
this.buildScopeStruct()
|
this.buildScopeStruct()
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,13 @@ export class ServerService {
|
||||||
css: ''
|
css: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugin: {
|
||||||
|
registered: []
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
registered: [],
|
||||||
|
default: 'default'
|
||||||
|
},
|
||||||
email: {
|
email: {
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,6 +26,8 @@ export class User implements UserServerModel {
|
||||||
videoChannels: VideoChannel[]
|
videoChannels: VideoChannel[]
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
|
|
||||||
|
theme: string
|
||||||
|
|
||||||
adminFlags?: UserAdminFlag
|
adminFlags?: UserAdminFlag
|
||||||
|
|
||||||
blocked: boolean
|
blocked: boolean
|
||||||
|
@ -49,6 +51,8 @@ export class User implements UserServerModel {
|
||||||
this.autoPlayVideo = hash.autoPlayVideo
|
this.autoPlayVideo = hash.autoPlayVideo
|
||||||
this.createdAt = hash.createdAt
|
this.createdAt = hash.createdAt
|
||||||
|
|
||||||
|
this.theme = hash.theme
|
||||||
|
|
||||||
this.adminFlags = hash.adminFlags
|
this.adminFlags = hash.adminFlags
|
||||||
|
|
||||||
this.blocked = hash.blocked
|
this.blocked = hash.blocked
|
||||||
|
|
|
@ -264,3 +264,6 @@ followers:
|
||||||
enabled: true
|
enabled: true
|
||||||
# Whether or not an administrator must manually validate a new follower
|
# Whether or not an administrator must manually validate a new follower
|
||||||
manual_approval: false
|
manual_approval: false
|
||||||
|
|
||||||
|
theme:
|
||||||
|
default: 'default'
|
||||||
|
|
|
@ -279,3 +279,6 @@ followers:
|
||||||
enabled: true
|
enabled: true
|
||||||
# Whether or not an administrator must manually validate a new follower
|
# Whether or not an administrator must manually validate a new follower
|
||||||
manual_approval: false
|
manual_approval: false
|
||||||
|
|
||||||
|
theme:
|
||||||
|
default: 'default'
|
||||||
|
|
|
@ -261,7 +261,7 @@ async function startApplication () {
|
||||||
updateStreamingPlaylistsInfohashesIfNeeded()
|
updateStreamingPlaylistsInfohashesIfNeeded()
|
||||||
.catch(err => logger.error('Cannot update streaming playlist infohashes.', { err }))
|
.catch(err => logger.error('Cannot update streaming playlist infohashes.', { err }))
|
||||||
|
|
||||||
await PluginManager.Instance.registerPlugins()
|
await PluginManager.Instance.registerPluginsAndThemes()
|
||||||
|
|
||||||
// Make server listening
|
// Make server listening
|
||||||
server.listen(port, hostname, () => {
|
server.listen(port, hostname, () => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { snakeCase } from 'lodash'
|
import { snakeCase } from 'lodash'
|
||||||
import { ServerConfig, ServerConfigPlugin, UserRight } from '../../../shared'
|
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'
|
||||||
|
@ -16,7 +16,7 @@ import { isNumeric } from 'validator'
|
||||||
import { objectConverter } from '../../helpers/core-utils'
|
import { objectConverter } from '../../helpers/core-utils'
|
||||||
import { CONFIG, reloadConfig } from '../../initializers/config'
|
import { CONFIG, reloadConfig } from '../../initializers/config'
|
||||||
import { PluginManager } from '../../lib/plugins/plugin-manager'
|
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 packageJSON = require('../../../../package.json')
|
||||||
const configRouter = express.Router()
|
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)
|
.filter(key => CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.RESOLUTIONS[key] === true)
|
||||||
.map(r => parseInt(r, 10))
|
.map(r => parseInt(r, 10))
|
||||||
|
|
||||||
const plugins: ServerConfigPlugin[] = []
|
|
||||||
const registeredPlugins = PluginManager.Instance.getRegisteredPlugins()
|
const registeredPlugins = PluginManager.Instance.getRegisteredPlugins()
|
||||||
for (const pluginName of Object.keys(registeredPlugins)) {
|
.map(p => ({
|
||||||
const plugin = registeredPlugins[ pluginName ]
|
name: p.name,
|
||||||
if (plugin.type !== PluginType.PLUGIN) continue
|
version: p.version,
|
||||||
|
description: p.description,
|
||||||
|
clientScripts: p.clientScripts
|
||||||
|
}))
|
||||||
|
|
||||||
plugins.push({
|
const registeredThemes = PluginManager.Instance.getRegisteredThemes()
|
||||||
name: plugin.name,
|
.map(t => ({
|
||||||
version: plugin.version,
|
name: t.name,
|
||||||
description: plugin.description,
|
version: t.version,
|
||||||
clientScripts: plugin.clientScripts
|
description: t.description,
|
||||||
})
|
clientScripts: t.clientScripts
|
||||||
}
|
}))
|
||||||
|
|
||||||
|
const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT)
|
||||||
|
|
||||||
const json: ServerConfig = {
|
const json: ServerConfig = {
|
||||||
instance: {
|
instance: {
|
||||||
|
@ -82,7 +86,13 @@ async function getConfig (req: express.Request, res: express.Response) {
|
||||||
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
|
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins,
|
plugin: {
|
||||||
|
registered: registeredPlugins
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
registered: registeredThemes,
|
||||||
|
default: defaultTheme
|
||||||
|
},
|
||||||
email: {
|
email: {
|
||||||
enabled: Emailer.isEnabled()
|
enabled: Emailer.isEnabled()
|
||||||
},
|
},
|
||||||
|
@ -240,6 +250,9 @@ function customConfig (): CustomConfig {
|
||||||
javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT
|
javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
theme: {
|
||||||
|
default: CONFIG.THEME.DEFAULT
|
||||||
|
},
|
||||||
services: {
|
services: {
|
||||||
twitter: {
|
twitter: {
|
||||||
username: CONFIG.SERVICES.TWITTER.USERNAME,
|
username: CONFIG.SERVICES.TWITTER.USERNAME,
|
||||||
|
|
|
@ -183,6 +183,7 @@ async function updateMe (req: express.Request, res: express.Response) {
|
||||||
if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
|
if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
|
||||||
if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
|
if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
|
||||||
if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages
|
if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages
|
||||||
|
if (body.theme !== undefined) user.theme = body.theme
|
||||||
|
|
||||||
if (body.email !== undefined) {
|
if (body.email !== undefined) {
|
||||||
if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
|
if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { PluginType } from '../../../shared/models/plugins/plugin.type'
|
||||||
import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
|
import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
|
||||||
import { PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model'
|
import { PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model'
|
||||||
import { isUrlValid } from './activitypub/misc'
|
import { isUrlValid } from './activitypub/misc'
|
||||||
|
import { isThemeRegistered } from '../../lib/plugins/theme-utils'
|
||||||
|
|
||||||
const PLUGINS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.PLUGINS
|
const PLUGINS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.PLUGINS
|
||||||
|
|
||||||
|
@ -61,6 +62,10 @@ function isCSSPathsValid (css: any[]) {
|
||||||
return isArray(css) && css.every(c => isSafePath(c))
|
return isArray(css) && css.every(c => isSafePath(c))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isThemeValid (name: string) {
|
||||||
|
return isPluginNameValid(name) && isThemeRegistered(name)
|
||||||
|
}
|
||||||
|
|
||||||
function isPackageJSONValid (packageJSON: PluginPackageJson, pluginType: PluginType) {
|
function isPackageJSONValid (packageJSON: PluginPackageJson, pluginType: PluginType) {
|
||||||
return isNpmPluginNameValid(packageJSON.name) &&
|
return isNpmPluginNameValid(packageJSON.name) &&
|
||||||
isPluginDescriptionValid(packageJSON.description) &&
|
isPluginDescriptionValid(packageJSON.description) &&
|
||||||
|
@ -82,6 +87,7 @@ function isLibraryCodeValid (library: any) {
|
||||||
export {
|
export {
|
||||||
isPluginTypeValid,
|
isPluginTypeValid,
|
||||||
isPackageJSONValid,
|
isPackageJSONValid,
|
||||||
|
isThemeValid,
|
||||||
isPluginVersionValid,
|
isPluginVersionValid,
|
||||||
isPluginNameValid,
|
isPluginNameValid,
|
||||||
isPluginDescriptionValid,
|
isPluginDescriptionValid,
|
||||||
|
|
|
@ -29,7 +29,8 @@ function checkMissedConfig () {
|
||||||
'followers.instance.enabled', 'followers.instance.manual_approval',
|
'followers.instance.enabled', 'followers.instance.manual_approval',
|
||||||
'tracker.enabled', 'tracker.private', 'tracker.reject_too_many_announces',
|
'tracker.enabled', 'tracker.private', 'tracker.reject_too_many_announces',
|
||||||
'history.videos.max_age', 'views.videos.remote.max_age',
|
'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 = [
|
const requiredAlternatives = [
|
||||||
[ // set
|
[ // set
|
||||||
|
|
|
@ -224,6 +224,9 @@ const CONFIG = {
|
||||||
get ENABLED () { return config.get<boolean>('followers.instance.enabled') },
|
get ENABLED () { return config.get<boolean>('followers.instance.enabled') },
|
||||||
get MANUAL_APPROVAL () { return config.get<boolean>('followers.instance.manual_approval') }
|
get MANUAL_APPROVAL () { return config.get<boolean>('followers.instance.manual_approval') }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
THEME: {
|
||||||
|
get DEFAULT () { return config.get<string>('theme.default') }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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_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'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Special constants for a test instance
|
// Special constants for a test instance
|
||||||
|
@ -667,6 +669,7 @@ export {
|
||||||
HLS_STREAMING_PLAYLIST_DIRECTORY,
|
HLS_STREAMING_PLAYLIST_DIRECTORY,
|
||||||
FEEDS,
|
FEEDS,
|
||||||
JOB_TTL,
|
JOB_TTL,
|
||||||
|
DEFAULT_THEME,
|
||||||
NSFW_POLICY_TYPES,
|
NSFW_POLICY_TYPES,
|
||||||
STATIC_MAX_AGE,
|
STATIC_MAX_AGE,
|
||||||
STATIC_PATHS,
|
STATIC_PATHS,
|
||||||
|
|
|
@ -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<void> {
|
||||||
|
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
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants'
|
||||||
import { PluginType } from '../../../shared/models/plugins/plugin.type'
|
import { PluginType } from '../../../shared/models/plugins/plugin.type'
|
||||||
import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
|
import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
|
||||||
import { outputFile } from 'fs-extra'
|
import { outputFile } from 'fs-extra'
|
||||||
|
import { ServerConfigPlugin } from '../../../shared/models/server'
|
||||||
|
|
||||||
export interface RegisteredPlugin {
|
export interface RegisteredPlugin {
|
||||||
name: string
|
name: string
|
||||||
|
@ -47,7 +48,7 @@ export class PluginManager {
|
||||||
private constructor () {
|
private constructor () {
|
||||||
}
|
}
|
||||||
|
|
||||||
async registerPlugins () {
|
async registerPluginsAndThemes () {
|
||||||
await this.resetCSSGlobalFile()
|
await this.resetCSSGlobalFile()
|
||||||
|
|
||||||
const plugins = await PluginModel.listEnabledPluginsAndThemes()
|
const plugins = await PluginModel.listEnabledPluginsAndThemes()
|
||||||
|
@ -63,12 +64,20 @@ export class PluginManager {
|
||||||
this.sortHooksByPriority()
|
this.sortHooksByPriority()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRegisteredPluginOrTheme (name: string) {
|
||||||
|
return this.registeredPlugins[name]
|
||||||
|
}
|
||||||
|
|
||||||
getRegisteredPlugin (name: string) {
|
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) {
|
getRegisteredTheme (name: string) {
|
||||||
const registered = this.getRegisteredPlugin(name)
|
const registered = this.getRegisteredPluginOrTheme(name)
|
||||||
|
|
||||||
if (!registered || registered.type !== PluginType.THEME) return undefined
|
if (!registered || registered.type !== PluginType.THEME) return undefined
|
||||||
|
|
||||||
|
@ -76,7 +85,11 @@ export class PluginManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
getRegisteredPlugins () {
|
getRegisteredPlugins () {
|
||||||
return this.registeredPlugins
|
return this.getRegisteredPluginsOrThemes(PluginType.PLUGIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
getRegisteredThemes () {
|
||||||
|
return this.getRegisteredPluginsOrThemes(PluginType.THEME)
|
||||||
}
|
}
|
||||||
|
|
||||||
async runHook (hookName: string, param?: any) {
|
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 () {
|
static get Instance () {
|
||||||
return this.instance || (this.instance = new this())
|
return this.instance || (this.instance = new this())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { body } from 'express-validator/check'
|
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 { logger } from '../../helpers/logger'
|
||||||
import { CustomConfig } from '../../../shared/models/server/custom-config.model'
|
import { CustomConfig } from '../../../shared/models/server/custom-config.model'
|
||||||
import { Emailer } from '../../lib/emailer'
|
import { Emailer } from '../../lib/emailer'
|
||||||
import { areValidationErrors } from './utils'
|
import { areValidationErrors } from './utils'
|
||||||
|
import { isThemeValid } from '../../helpers/custom-validators/plugins'
|
||||||
|
|
||||||
const customConfigUpdateValidator = [
|
const customConfigUpdateValidator = [
|
||||||
body('instance.name').exists().withMessage('Should have a valid instance name'),
|
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.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('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) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
logger.debug('Checking customConfigUpdateValidator parameters', { parameters: req.body })
|
logger.debug('Checking customConfigUpdateValidator parameters', { parameters: req.body })
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ const servePluginStaticDirectoryValidator = [
|
||||||
|
|
||||||
if (areValidationErrors(req, res)) return
|
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) {
|
if (!plugin || plugin.version !== req.params.pluginVersion) {
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { ActorModel } from '../../models/activitypub/actor'
|
||||||
import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor'
|
import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor'
|
||||||
import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels'
|
import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels'
|
||||||
import { UserRegister } from '../../../shared/models/users/user-register.model'
|
import { UserRegister } from '../../../shared/models/users/user-register.model'
|
||||||
|
import { isThemeValid } from '../../helpers/custom-validators/plugins'
|
||||||
|
|
||||||
const usersAddValidator = [
|
const usersAddValidator = [
|
||||||
body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
|
body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
|
||||||
|
@ -204,6 +205,9 @@ const usersUpdateMeValidator = [
|
||||||
body('videosHistoryEnabled')
|
body('videosHistoryEnabled')
|
||||||
.optional()
|
.optional()
|
||||||
.custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'),
|
.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) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
|
logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
|
||||||
|
|
|
@ -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 { NSFW_POLICY_TYPES } from '../../initializers/constants'
|
import { DEFAULT_THEME, 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'
|
||||||
|
@ -52,6 +52,8 @@ import { ActorModel } from '../activitypub/actor'
|
||||||
import { ActorFollowModel } from '../activitypub/actor-follow'
|
import { ActorFollowModel } from '../activitypub/actor-follow'
|
||||||
import { VideoImportModel } from '../video/video-import'
|
import { VideoImportModel } from '../video/video-import'
|
||||||
import { UserAdminFlag } from '../../../shared/models/users/user-flag.model'
|
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 {
|
enum ScopeNames {
|
||||||
WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
|
WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
|
||||||
|
@ -187,6 +189,12 @@ export class UserModel extends Model<UserModel> {
|
||||||
@Column(DataType.BIGINT)
|
@Column(DataType.BIGINT)
|
||||||
videoQuotaDaily: number
|
videoQuotaDaily: number
|
||||||
|
|
||||||
|
@AllowNull(false)
|
||||||
|
@Default(DEFAULT_THEME)
|
||||||
|
@Is('UserTheme', value => throwIfNotValid(value, isThemeValid, 'theme'))
|
||||||
|
@Column
|
||||||
|
theme: string
|
||||||
|
|
||||||
@CreatedAt
|
@CreatedAt
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
|
|
||||||
|
@ -560,6 +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),
|
||||||
roleLabel: USER_ROLE_LABELS[ this.role ],
|
roleLabel: USER_ROLE_LABELS[ this.role ],
|
||||||
videoQuota: this.videoQuota,
|
videoQuota: this.videoQuota,
|
||||||
videoQuotaDaily: this.videoQuotaDaily,
|
videoQuotaDaily: this.videoQuotaDaily,
|
||||||
|
|
|
@ -27,6 +27,9 @@ describe('Test config API validators', function () {
|
||||||
css: 'body { background-color: red; }'
|
css: 'body { background-color: red; }'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
theme: {
|
||||||
|
default: 'default'
|
||||||
|
},
|
||||||
services: {
|
services: {
|
||||||
twitter: {
|
twitter: {
|
||||||
username: '@MySuperUsername',
|
username: '@MySuperUsername',
|
||||||
|
|
|
@ -190,6 +190,9 @@ describe('Test config', function () {
|
||||||
css: 'body { background-color: red; }'
|
css: 'body { background-color: red; }'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
theme: {
|
||||||
|
default: 'default'
|
||||||
|
},
|
||||||
services: {
|
services: {
|
||||||
twitter: {
|
twitter: {
|
||||||
username: '@Kuja',
|
username: '@Kuja',
|
||||||
|
|
|
@ -59,6 +59,9 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
|
||||||
css: 'body { background-color: red; }'
|
css: 'body { background-color: red; }'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
theme: {
|
||||||
|
default: 'default'
|
||||||
|
},
|
||||||
services: {
|
services: {
|
||||||
twitter: {
|
twitter: {
|
||||||
username: '@MySuperUsername',
|
username: '@MySuperUsername',
|
||||||
|
|
|
@ -15,6 +15,10 @@ export interface CustomConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
theme: {
|
||||||
|
default: string
|
||||||
|
}
|
||||||
|
|
||||||
services: {
|
services: {
|
||||||
twitter: {
|
twitter: {
|
||||||
username: string
|
username: string
|
||||||
|
|
|
@ -24,7 +24,14 @@ export interface ServerConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins: ServerConfigPlugin[]
|
plugin: {
|
||||||
|
registered: ServerConfigPlugin[]
|
||||||
|
}
|
||||||
|
|
||||||
|
theme: {
|
||||||
|
registered: ServerConfigPlugin[]
|
||||||
|
default: string
|
||||||
|
}
|
||||||
|
|
||||||
email: {
|
email: {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
|
|
@ -13,4 +13,6 @@ export interface UserUpdateMe {
|
||||||
email?: string
|
email?: string
|
||||||
currentPassword?: string
|
currentPassword?: string
|
||||||
password?: string
|
password?: string
|
||||||
|
|
||||||
|
theme?: string
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue