mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 09:49:20 +02:00
Redesign admin config and add theme customization
This commit is contained in:
parent
03425e10d3
commit
a6b89bde2b
98 changed files with 3928 additions and 2525 deletions
|
@ -214,7 +214,6 @@
|
||||||
"escape-string-regexp",
|
"escape-string-regexp",
|
||||||
"is-plain-object",
|
"is-plain-object",
|
||||||
"parse-srcset",
|
"parse-srcset",
|
||||||
"deepmerge",
|
|
||||||
"core-js/features/reflect",
|
"core-js/features/reflect",
|
||||||
"hammerjs",
|
"hammerjs",
|
||||||
"jschannel"
|
"jschannel"
|
||||||
|
|
|
@ -150,6 +150,7 @@ export default defineConfig([
|
||||||
'no-return-assign': 'off',
|
'no-return-assign': 'off',
|
||||||
'@typescript-eslint/unbound-method': 'off',
|
'@typescript-eslint/unbound-method': 'off',
|
||||||
'import/no-named-default': 'off',
|
'import/no-named-default': 'off',
|
||||||
|
'@typescript-eslint/prefer-reduce-type-parameter': 'off',
|
||||||
|
|
||||||
"@typescript-eslint/no-deprecated": [ 'error', {
|
"@typescript-eslint/no-deprecated": [ 'error', {
|
||||||
allow: [
|
allow: [
|
||||||
|
|
9
client/src/app/+admin/config/admin-config.component.html
Normal file
9
client/src/app/+admin/config/admin-config.component.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<div class="root">
|
||||||
|
<div>
|
||||||
|
<my-lateral-menu [config]="menuConfig"></my-lateral-menu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
||||||
|
</div>
|
14
client/src/app/+admin/config/admin-config.component.scss
Normal file
14
client/src/app/+admin/config/admin-config.component.scss
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
@use "_variables" as *;
|
||||||
|
@use "_mixins" as *;
|
||||||
|
@use "_form-mixins" as *;
|
||||||
|
@import "bootstrap/scss/mixins";
|
||||||
|
|
||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $medium-view) {
|
||||||
|
.root {
|
||||||
|
margin-bottom: 150px;
|
||||||
|
}
|
||||||
|
}
|
67
client/src/app/+admin/config/admin-config.component.ts
Normal file
67
client/src/app/+admin/config/admin-config.component.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, OnInit } from '@angular/core'
|
||||||
|
import { RouterModule } from '@angular/router'
|
||||||
|
import { LateralMenuComponent, LateralMenuConfig } from '../../shared/shared-main/menu/lateral-menu.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-admin-config',
|
||||||
|
styleUrls: [ './admin-config.component.scss' ],
|
||||||
|
templateUrl: './admin-config.component.html',
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
RouterModule,
|
||||||
|
LateralMenuComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminConfigComponent implements OnInit {
|
||||||
|
menuConfig: LateralMenuConfig
|
||||||
|
|
||||||
|
ngOnInit (): void {
|
||||||
|
this.menuConfig = {
|
||||||
|
title: $localize`Configuration`,
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
label: $localize`Information`,
|
||||||
|
routerLink: 'information'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
label: $localize`General`,
|
||||||
|
routerLink: 'general'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
label: $localize`Homepage`,
|
||||||
|
routerLink: 'homepage'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
label: $localize`Customization`,
|
||||||
|
routerLink: 'customization'
|
||||||
|
},
|
||||||
|
|
||||||
|
{ type: 'separator' },
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
label: $localize`VOD`,
|
||||||
|
routerLink: 'vod'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
label: $localize`Live`,
|
||||||
|
routerLink: 'live'
|
||||||
|
},
|
||||||
|
|
||||||
|
{ type: 'separator' },
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
label: $localize`Advanced`,
|
||||||
|
routerLink: 'advanced'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,37 @@
|
||||||
import { Routes } from '@angular/router'
|
import { inject } from '@angular/core'
|
||||||
import { EditCustomConfigComponent } from '@app/+admin/config/edit-custom-config'
|
import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot, Routes } from '@angular/router'
|
||||||
import { UserRightGuard } from '@app/core'
|
import { CanDeactivateGuard, ServerService, UserRightGuard } from '@app/core'
|
||||||
import { UserRight } from '@peertube/peertube-models'
|
import { CustomPageService } from '@app/shared/shared-main/custom-page/custom-page.service'
|
||||||
|
import { CustomConfig, UserRight, VideoConstant } from '@peertube/peertube-models'
|
||||||
|
import { map } from 'rxjs'
|
||||||
|
import { AdminConfigComponent } from './admin-config.component'
|
||||||
|
import {
|
||||||
|
AdminConfigAdvancedComponent,
|
||||||
|
AdminConfigGeneralComponent,
|
||||||
|
AdminConfigHomepageComponent,
|
||||||
|
AdminConfigInformationComponent,
|
||||||
|
AdminConfigLiveComponent,
|
||||||
|
AdminConfigVODComponent
|
||||||
|
} from './pages'
|
||||||
|
import { AdminConfigCustomizationComponent } from './pages/admin-config-customization.component'
|
||||||
|
import { AdminConfigService } from './shared/admin-config.service'
|
||||||
|
|
||||||
|
export const customConfigResolver: ResolveFn<CustomConfig> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||||
|
return inject(AdminConfigService).getCustomConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const homepageResolver: ResolveFn<string> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||||
|
return inject(CustomPageService).getInstanceHomepage()
|
||||||
|
.pipe(map(({ content }) => content))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const categoriesResolver: ResolveFn<VideoConstant<number>[]> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||||
|
return inject(ServerService).getVideoCategories()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const languagesResolver: ResolveFn<VideoConstant<string>[]> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||||
|
return inject(ServerService).getVideoLanguages()
|
||||||
|
}
|
||||||
|
|
||||||
export const configRoutes: Routes = [
|
export const configRoutes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -10,18 +40,96 @@ export const configRoutes: Routes = [
|
||||||
data: {
|
data: {
|
||||||
userRight: UserRight.MANAGE_CONFIGURATION
|
userRight: UserRight.MANAGE_CONFIGURATION
|
||||||
},
|
},
|
||||||
|
resolve: {
|
||||||
|
customConfig: customConfigResolver
|
||||||
|
},
|
||||||
|
component: AdminConfigComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
// Old path with PeerTube < 7.3
|
||||||
redirectTo: 'edit-custom',
|
path: 'edit-custom',
|
||||||
|
redirectTo: 'information',
|
||||||
pathMatch: 'full'
|
pathMatch: 'full'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'edit-custom',
|
path: '',
|
||||||
component: EditCustomConfigComponent,
|
redirectTo: 'information',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'homepage',
|
||||||
|
component: AdminConfigHomepageComponent,
|
||||||
|
canDeactivate: [ CanDeactivateGuard ],
|
||||||
|
resolve: {
|
||||||
|
homepageContent: homepageResolver
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
meta: {
|
meta: {
|
||||||
title: $localize`Edit custom configuration`
|
title: $localize`Edit your platform homepage`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'customization',
|
||||||
|
component: AdminConfigCustomizationComponent,
|
||||||
|
canDeactivate: [ CanDeactivateGuard ],
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
title: $localize`Platform customization`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'information',
|
||||||
|
component: AdminConfigInformationComponent,
|
||||||
|
canDeactivate: [ CanDeactivateGuard ],
|
||||||
|
resolve: {
|
||||||
|
categories: categoriesResolver,
|
||||||
|
languages: languagesResolver
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
title: $localize`Platform information`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'general',
|
||||||
|
component: AdminConfigGeneralComponent,
|
||||||
|
canDeactivate: [ CanDeactivateGuard ],
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
title: $localize`General configuration`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'vod',
|
||||||
|
component: AdminConfigVODComponent,
|
||||||
|
canDeactivate: [ CanDeactivateGuard ],
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
title: $localize`VOD configuration`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'live',
|
||||||
|
component: AdminConfigLiveComponent,
|
||||||
|
canDeactivate: [ CanDeactivateGuard ],
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
title: $localize`Live configuration`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'advanced',
|
||||||
|
component: AdminConfigAdvancedComponent,
|
||||||
|
canDeactivate: [ CanDeactivateGuard ],
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
title: $localize`Advanced configuration`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,135 +0,0 @@
|
||||||
<ng-container [formGroup]="form()">
|
|
||||||
|
|
||||||
<div class="pt-two-cols mt-5"> <!-- cache grid -->
|
|
||||||
|
|
||||||
<div class="title-col">
|
|
||||||
<h2 i18n>CACHE</h2>
|
|
||||||
<div i18n class="inner-form-description">
|
|
||||||
Some files are not federated, and fetched when necessary. Define their caching policies.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content-col">
|
|
||||||
<ng-container formGroupName="cache">
|
|
||||||
<div class="form-group" formGroupName="previews">
|
|
||||||
<label i18n for="cachePreviewsSize">Number of previews to keep in cache</label>
|
|
||||||
|
|
||||||
<div class="number-with-unit">
|
|
||||||
<input
|
|
||||||
type="number" min="0" id="cachePreviewsSize" class="form-control"
|
|
||||||
formControlName="size" [ngClass]="{ 'input-error': formErrors()['cache.previews.size'] }"
|
|
||||||
>
|
|
||||||
<span i18n>{getCacheSize('previews'), plural, =1 {cached image} other {cached images}}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf="formErrors().cache.previews.size" class="form-error" role="alert">{{ formErrors().cache.previews.size }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" formGroupName="captions">
|
|
||||||
<label i18n for="cacheCaptionsSize">Number of video captions to keep in cache</label>
|
|
||||||
|
|
||||||
<div class="number-with-unit">
|
|
||||||
<input
|
|
||||||
type="number" min="0" id="cacheCaptionsSize" class="form-control"
|
|
||||||
formControlName="size" [ngClass]="{ 'input-error': formErrors()['cache.captions.size'] }"
|
|
||||||
>
|
|
||||||
<span i18n>{getCacheSize('captions'), plural, =1 {cached caption} other {cached captions}}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf="formErrors().cache.captions.size" class="form-error" role="alert">{{ formErrors().cache.captions.size }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" formGroupName="torrents">
|
|
||||||
<label i18n for="cacheTorrentsSize">Number of video torrents to keep in cache</label>
|
|
||||||
|
|
||||||
<div class="number-with-unit">
|
|
||||||
<input
|
|
||||||
type="number" min="0" id="cacheTorrentsSize" class="form-control"
|
|
||||||
formControlName="size" [ngClass]="{ 'input-error': formErrors()['cache.torrents.size'] }"
|
|
||||||
>
|
|
||||||
<span i18n>{getCacheSize('torrents'), plural, =1 {cached torrent} other {cached torrents}}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf="formErrors().cache.torrents.size" class="form-error" role="alert">{{ formErrors().cache.torrents.size }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" formGroupName="torrents">
|
|
||||||
<label i18n for="cacheTorrentsSize">Number of video storyboard images to keep in cache</label>
|
|
||||||
|
|
||||||
<div class="number-with-unit">
|
|
||||||
<input
|
|
||||||
type="number" min="0" id="cacheStoryboardsSize" class="form-control"
|
|
||||||
formControlName="size" [ngClass]="{ 'input-error': formErrors()['cache.storyboards.size'] }"
|
|
||||||
>
|
|
||||||
<span i18n>{getCacheSize('storyboards'), plural, =1 {cached storyboard} other {cached storyboards}}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf="formErrors().cache.storyboards.size" class="form-error" role="alert">{{ formErrors().cache.storyboards.size }}</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4"> <!-- cache grid -->
|
|
||||||
<div class="title-col">
|
|
||||||
<div class="anchor" id="customizations"></div> <!-- customizations anchor -->
|
|
||||||
<h2 i18n>CUSTOMIZATIONS</h2>
|
|
||||||
<div i18n class="inner-form-description">
|
|
||||||
Slight modifications to your PeerTube instance for when creating a plugin or theme is overkill.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content-col">
|
|
||||||
<ng-container formGroupName="instance">
|
|
||||||
<ng-container formGroupName="customizations">
|
|
||||||
<div class="form-group">
|
|
||||||
<label i18n for="customizationJavascript">JavaScript</label>
|
|
||||||
<my-help>
|
|
||||||
<ng-container i18n>
|
|
||||||
<p class="mb-2">Write JavaScript code directly. Example:</p>
|
|
||||||
<pre>console.log('my instance is amazing');</pre>
|
|
||||||
</ng-container>
|
|
||||||
</my-help>
|
|
||||||
|
|
||||||
<textarea
|
|
||||||
id="customizationJavascript" formControlName="javascript" class="form-control" dir="ltr"
|
|
||||||
[ngClass]="{ 'input-error': formErrors()['instance.customizations.javascript'] }"
|
|
||||||
></textarea>
|
|
||||||
|
|
||||||
<div *ngIf="formErrors().instance.customizations.javascript" class="form-error" role="alert">{{ formErrors().instance.customizations.javascript }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="customizationCSS">CSS</label>
|
|
||||||
|
|
||||||
<my-help>
|
|
||||||
<ng-container i18n>
|
|
||||||
<p class="mb-2">Write CSS code directly. Example:</p>
|
|
||||||
<pre>
|
|
||||||
#custom-css {{ '{' }}
|
|
||||||
color: red;
|
|
||||||
{{ '}' }}
|
|
||||||
</pre>
|
|
||||||
<p class="mb-2">Prepend with <em>#custom-css</em> to override styles. Example:</p>
|
|
||||||
<pre>
|
|
||||||
#custom-css .logged-in-email {{ '{' }}
|
|
||||||
color: red;
|
|
||||||
{{ '}' }}
|
|
||||||
</pre>
|
|
||||||
</ng-container>
|
|
||||||
</my-help>
|
|
||||||
|
|
||||||
<textarea
|
|
||||||
id="customizationCSS" formControlName="css" class="form-control" dir="ltr"
|
|
||||||
[ngClass]="{ 'input-error': formErrors()['instance.customizations.css'] }"
|
|
||||||
></textarea>
|
|
||||||
<div *ngIf="formErrors().instance.customizations.css" class="form-error" role="alert">{{ formErrors().instance.customizations.css }}</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</ng-container>
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { NgClass, NgIf } from '@angular/common'
|
|
||||||
import { Component, input } from '@angular/core'
|
|
||||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'my-edit-advanced-configuration',
|
|
||||||
templateUrl: './edit-advanced-configuration.component.html',
|
|
||||||
styleUrls: [ './edit-custom-config.component.scss' ],
|
|
||||||
imports: [ FormsModule, ReactiveFormsModule, NgClass, NgIf, HelpComponent ]
|
|
||||||
})
|
|
||||||
export class EditAdvancedConfigurationComponent {
|
|
||||||
readonly form = input<FormGroup>(undefined)
|
|
||||||
readonly formErrors = input<any>(undefined)
|
|
||||||
|
|
||||||
getCacheSize (type: 'captions' | 'previews' | 'torrents' | 'storyboards') {
|
|
||||||
return this.form().value['cache'][type]['size']
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,215 +0,0 @@
|
||||||
import { NgClass, NgIf } from '@angular/common'
|
|
||||||
import { Component, OnChanges, OnInit, SimpleChanges, inject, input } from '@angular/core'
|
|
||||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { RouterLink } from '@angular/router'
|
|
||||||
import { ThemeService } from '@app/core'
|
|
||||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
|
||||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
|
||||||
import { pairwise } from 'rxjs/operators'
|
|
||||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
|
||||||
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
|
||||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
|
||||||
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
|
||||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
|
||||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
|
||||||
import { UserRealQuotaInfoComponent } from '../../shared/user-real-quota-info.component'
|
|
||||||
import { ConfigService } from '../shared/config.service'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'my-edit-basic-configuration',
|
|
||||||
templateUrl: './edit-basic-configuration.component.html',
|
|
||||||
styleUrls: [ './edit-custom-config.component.scss' ],
|
|
||||||
imports: [
|
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
RouterLink,
|
|
||||||
SelectCustomValueComponent,
|
|
||||||
NgIf,
|
|
||||||
PeertubeCheckboxComponent,
|
|
||||||
HelpComponent,
|
|
||||||
MarkdownTextareaComponent,
|
|
||||||
NgClass,
|
|
||||||
UserRealQuotaInfoComponent,
|
|
||||||
SelectOptionsComponent,
|
|
||||||
AlertComponent
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
|
||||||
private configService = inject(ConfigService)
|
|
||||||
private themeService = inject(ThemeService)
|
|
||||||
|
|
||||||
readonly form = input<FormGroup>(undefined)
|
|
||||||
readonly formErrors = input<any>(undefined)
|
|
||||||
|
|
||||||
readonly serverConfig = input<HTMLServerConfig>(undefined)
|
|
||||||
|
|
||||||
signupAlertMessage: string
|
|
||||||
defaultLandingPageOptions: SelectOptionsItem[] = []
|
|
||||||
availableThemes: SelectOptionsItem[]
|
|
||||||
|
|
||||||
exportExpirationOptions: SelectOptionsItem[] = []
|
|
||||||
exportMaxUserVideoQuotaOptions: SelectOptionsItem[] = []
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.buildLandingPageOptions()
|
|
||||||
this.checkSignupField()
|
|
||||||
this.checkImportSyncField()
|
|
||||||
|
|
||||||
this.availableThemes = [
|
|
||||||
this.themeService.getDefaultThemeItem(),
|
|
||||||
|
|
||||||
...this.themeService.buildAvailableThemes()
|
|
||||||
]
|
|
||||||
|
|
||||||
this.exportExpirationOptions = [
|
|
||||||
{ id: 1000 * 3600 * 24, label: $localize`1 day` },
|
|
||||||
{ id: 1000 * 3600 * 24 * 2, label: $localize`2 days` },
|
|
||||||
{ id: 1000 * 3600 * 24 * 7, label: $localize`7 days` },
|
|
||||||
{ id: 1000 * 3600 * 24 * 30, label: $localize`30 days` }
|
|
||||||
]
|
|
||||||
|
|
||||||
this.exportMaxUserVideoQuotaOptions = this.configService.videoQuotaOptions.filter(o => (o.id as number) >= 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnChanges (changes: SimpleChanges) {
|
|
||||||
if (changes['serverConfig']) {
|
|
||||||
this.buildLandingPageOptions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
countExternalAuth () {
|
|
||||||
return this.serverConfig().plugin.registeredExternalAuths.length
|
|
||||||
}
|
|
||||||
|
|
||||||
getVideoQuotaOptions () {
|
|
||||||
return this.configService.videoQuotaOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
getVideoQuotaDailyOptions () {
|
|
||||||
return this.configService.videoQuotaDailyOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
doesTrendingVideosAlgorithmsEnabledInclude (algorithm: string) {
|
|
||||||
const enabled = this.form().value['trending']['videos']['algorithms']['enabled']
|
|
||||||
if (!Array.isArray(enabled)) return false
|
|
||||||
|
|
||||||
return !!enabled.find((e: string) => e === algorithm)
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserVideoQuota () {
|
|
||||||
return this.form().value['user']['videoQuota']
|
|
||||||
}
|
|
||||||
|
|
||||||
isExportUsersEnabled () {
|
|
||||||
return this.form().value['export']['users']['enabled'] === true
|
|
||||||
}
|
|
||||||
|
|
||||||
getDisabledExportUsersClass () {
|
|
||||||
return { 'disabled-checkbox-extra': !this.isExportUsersEnabled() }
|
|
||||||
}
|
|
||||||
|
|
||||||
isSignupEnabled () {
|
|
||||||
return this.form().value['signup']['enabled'] === true
|
|
||||||
}
|
|
||||||
|
|
||||||
getDisabledSignupClass () {
|
|
||||||
return { 'disabled-checkbox-extra': !this.isSignupEnabled() }
|
|
||||||
}
|
|
||||||
|
|
||||||
isImportVideosHttpEnabled (): boolean {
|
|
||||||
return this.form().value['import']['videos']['http']['enabled'] === true
|
|
||||||
}
|
|
||||||
|
|
||||||
importSynchronizationChecked () {
|
|
||||||
return this.isImportVideosHttpEnabled() && this.form().value['import']['videoChannelSynchronization']['enabled']
|
|
||||||
}
|
|
||||||
|
|
||||||
hasUnlimitedSignup () {
|
|
||||||
return this.form().value['signup']['limit'] === -1
|
|
||||||
}
|
|
||||||
|
|
||||||
isSearchIndexEnabled () {
|
|
||||||
return this.form().value['search']['searchIndex']['enabled'] === true
|
|
||||||
}
|
|
||||||
|
|
||||||
getDisabledSearchIndexClass () {
|
|
||||||
return { 'disabled-checkbox-extra': !this.isSearchIndexEnabled() }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
isTranscriptionEnabled () {
|
|
||||||
return this.form().value['videoTranscription']['enabled'] === true
|
|
||||||
}
|
|
||||||
|
|
||||||
getTranscriptionRunnerDisabledClass () {
|
|
||||||
return { 'disabled-checkbox-extra': !this.isTranscriptionEnabled() }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
isAutoFollowIndexEnabled () {
|
|
||||||
return this.form().value['followings']['instance']['autoFollowIndex']['enabled'] === true
|
|
||||||
}
|
|
||||||
|
|
||||||
buildLandingPageOptions () {
|
|
||||||
let links: { label: string, path: string }[] = []
|
|
||||||
|
|
||||||
if (this.serverConfig().homepage.enabled) {
|
|
||||||
links.push({ label: $localize`Home`, path: '/home' })
|
|
||||||
}
|
|
||||||
|
|
||||||
links = links.concat([
|
|
||||||
{ label: $localize`Discover`, path: '/videos/overview' },
|
|
||||||
{ label: $localize`Browse all videos`, path: '/videos/browse' },
|
|
||||||
{ label: $localize`Browse local videos`, path: '/videos/browse?scope=local' }
|
|
||||||
])
|
|
||||||
|
|
||||||
this.defaultLandingPageOptions = links.map(o => ({
|
|
||||||
id: o.path,
|
|
||||||
label: o.label,
|
|
||||||
description: o.path
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkImportSyncField () {
|
|
||||||
const importSyncControl = this.form().get('import.videoChannelSynchronization.enabled')
|
|
||||||
const importVideosHttpControl = this.form().get('import.videos.http.enabled')
|
|
||||||
|
|
||||||
importVideosHttpControl.valueChanges
|
|
||||||
.subscribe(httpImportEnabled => {
|
|
||||||
importSyncControl.setValue(httpImportEnabled && importSyncControl.value)
|
|
||||||
if (httpImportEnabled) {
|
|
||||||
importSyncControl.enable()
|
|
||||||
} else {
|
|
||||||
importSyncControl.disable()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkSignupField () {
|
|
||||||
const signupControl = this.form().get('signup.enabled')
|
|
||||||
|
|
||||||
signupControl.valueChanges
|
|
||||||
.pipe(pairwise())
|
|
||||||
.subscribe(([ oldValue, newValue ]) => {
|
|
||||||
if (oldValue === false && newValue === true) {
|
|
||||||
this.signupAlertMessage =
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
$localize`You enabled signup: we automatically enabled the "Block new videos automatically" checkbox of the "Videos" section just below.`
|
|
||||||
|
|
||||||
this.form().patchValue({
|
|
||||||
autoBlacklist: {
|
|
||||||
videos: {
|
|
||||||
ofUsers: {
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
signupControl.updateValueAndValidity()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
import { Injectable } from '@angular/core'
|
|
||||||
import { FormGroup } from '@angular/forms'
|
|
||||||
import { formatICU } from '@app/helpers'
|
|
||||||
|
|
||||||
export type ResolutionOption = {
|
|
||||||
id: string
|
|
||||||
label: string
|
|
||||||
description?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class EditConfigurationService {
|
|
||||||
|
|
||||||
getTranscodingResolutions () {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: '0p',
|
|
||||||
label: $localize`Audio-only`,
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
description: $localize`"Split audio and video" must be enabled for the PeerTube player to propose an "Audio only" resolution to users`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '144p',
|
|
||||||
label: $localize`144p`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '240p',
|
|
||||||
label: $localize`240p`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '360p',
|
|
||||||
label: $localize`360p`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '480p',
|
|
||||||
label: $localize`480p`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '720p',
|
|
||||||
label: $localize`720p`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '1080p',
|
|
||||||
label: $localize`1080p`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '1440p',
|
|
||||||
label: $localize`1440p`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2160p',
|
|
||||||
label: $localize`2160p`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
isTranscodingEnabled (form: FormGroup) {
|
|
||||||
return form.value['transcoding']['enabled'] === true
|
|
||||||
}
|
|
||||||
|
|
||||||
isHLSEnabled (form: FormGroup) {
|
|
||||||
return form.value['transcoding']['hls']['enabled'] === true
|
|
||||||
}
|
|
||||||
|
|
||||||
isRemoteRunnerVODEnabled (form: FormGroup) {
|
|
||||||
return form.value['transcoding']['remoteRunners']['enabled'] === true
|
|
||||||
}
|
|
||||||
|
|
||||||
isRemoteRunnerLiveEnabled (form: FormGroup) {
|
|
||||||
return form.value['live']['transcoding']['remoteRunners']['enabled'] === true
|
|
||||||
}
|
|
||||||
|
|
||||||
isStudioEnabled (form: FormGroup) {
|
|
||||||
return form.value['videoStudio']['enabled'] === true
|
|
||||||
}
|
|
||||||
|
|
||||||
isLiveEnabled (form: FormGroup) {
|
|
||||||
return form.value['live']['enabled'] === true
|
|
||||||
}
|
|
||||||
|
|
||||||
isLiveTranscodingEnabled (form: FormGroup) {
|
|
||||||
return form.value['live']['transcoding']['enabled'] === true
|
|
||||||
}
|
|
||||||
|
|
||||||
getTotalTranscodingThreads (form: FormGroup) {
|
|
||||||
const transcodingEnabled = form.value['transcoding']['enabled']
|
|
||||||
const transcodingThreads = form.value['transcoding']['threads']
|
|
||||||
const liveTranscodingEnabled = form.value['live']['transcoding']['enabled']
|
|
||||||
const liveTranscodingThreads = form.value['live']['transcoding']['threads']
|
|
||||||
|
|
||||||
// checks whether all enabled method are on fixed values and not on auto (= 0)
|
|
||||||
let noneOnAuto = !transcodingEnabled || +transcodingThreads > 0
|
|
||||||
noneOnAuto &&= !liveTranscodingEnabled || +liveTranscodingThreads > 0
|
|
||||||
|
|
||||||
// count total of fixed value, repalcing auto by a single thread (knowing it will display "at least")
|
|
||||||
let value = 0
|
|
||||||
if (transcodingEnabled) value += +transcodingThreads || 1
|
|
||||||
if (liveTranscodingEnabled) value += +liveTranscodingThreads || 1
|
|
||||||
|
|
||||||
return {
|
|
||||||
value,
|
|
||||||
atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible
|
|
||||||
unit: formatICU($localize`{value, plural, =1 {thread} other {threads}}`, { value })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
<h1 class="visually-hidden" i18n>Configuration</h1>
|
|
||||||
|
|
||||||
<my-alert type="warning" *ngIf="!isUpdateAllowed()" i18n>
|
|
||||||
Updating instance configuration from the web interface is disabled by the system administrator.
|
|
||||||
</my-alert>
|
|
||||||
|
|
||||||
<form [formGroup]="form">
|
|
||||||
|
|
||||||
<div ngbNav #nav="ngbNav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" class="nav-tabs">
|
|
||||||
|
|
||||||
<ng-container ngbNavItem="instance-homepage">
|
|
||||||
<a ngbNavLink i18n>Homepage</a>
|
|
||||||
|
|
||||||
<ng-template ngbNavContent>
|
|
||||||
<my-edit-homepage [form]="form" [formErrors]="formErrors"></my-edit-homepage>
|
|
||||||
</ng-template>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container ngbNavItem="instance-information">
|
|
||||||
<a ngbNavLink i18n>Information</a>
|
|
||||||
|
|
||||||
<ng-template ngbNavContent>
|
|
||||||
<my-edit-instance-information [form]="form" [formErrors]="formErrors" [languageItems]="languageItems" [categoryItems]="categoryItems">
|
|
||||||
</my-edit-instance-information>
|
|
||||||
</ng-template>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container ngbNavItem="basic-configuration">
|
|
||||||
<a ngbNavLink i18n>Basic</a>
|
|
||||||
|
|
||||||
<ng-template ngbNavContent>
|
|
||||||
<my-edit-basic-configuration [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
|
|
||||||
</my-edit-basic-configuration>
|
|
||||||
</ng-template>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container ngbNavItem="transcoding">
|
|
||||||
<a ngbNavLink i18n>VOD Transcoding</a>
|
|
||||||
|
|
||||||
<ng-template ngbNavContent>
|
|
||||||
<my-edit-vod-transcoding [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
|
|
||||||
</my-edit-vod-transcoding>
|
|
||||||
</ng-template>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container ngbNavItem="live">
|
|
||||||
<a ngbNavLink i18n>Live streaming</a>
|
|
||||||
|
|
||||||
<ng-template ngbNavContent>
|
|
||||||
<my-edit-live-configuration [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
|
|
||||||
</my-edit-live-configuration>
|
|
||||||
</ng-template>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container ngbNavItem="advanced-configuration">
|
|
||||||
<a ngbNavLink i18n>Advanced</a>
|
|
||||||
|
|
||||||
<ng-template ngbNavContent>
|
|
||||||
<my-edit-advanced-configuration [form]="form" [formErrors]="formErrors">
|
|
||||||
</my-edit-advanced-configuration>
|
|
||||||
</ng-template>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div [ngbNavOutlet]="nav"></div>
|
|
||||||
|
|
||||||
<div class="row mt-4"> <!-- submit placement block -->
|
|
||||||
<div class="col-md-7 col-xl-5"></div>
|
|
||||||
<div class="col-md-5 col-xl-5">
|
|
||||||
|
|
||||||
<div role="alert" class="form-error submit-error" i18n *ngIf="!form.valid && isUpdateAllowed()">
|
|
||||||
There are errors in the form:
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li *ngFor="let error of grabAllErrors()">
|
|
||||||
{{ error }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span role="alert" class="form-error submit-error" i18n *ngIf="!hasLiveAllowReplayConsistentOptions()">
|
|
||||||
You cannot allow live replay if you don't enable transcoding.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span i18n *ngIf="!isUpdateAllowed()">
|
|
||||||
You cannot change the server configuration because it's managed externally.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<input
|
|
||||||
class="peertube-button primary-button"
|
|
||||||
(click)="formValidated()" type="submit" i18n-value value="Update configuration"
|
|
||||||
[disabled]="!form.valid || !hasConsistentOptions() || !isUpdateAllowed()"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
|
@ -1,144 +0,0 @@
|
||||||
@use '_variables' as *;
|
|
||||||
@use '_mixins' as *;
|
|
||||||
@use '_form-mixins' as *;
|
|
||||||
|
|
||||||
$form-base-input-width: 340px;
|
|
||||||
$form-max-width: 500px;
|
|
||||||
|
|
||||||
form {
|
|
||||||
padding-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
my-markdown-textarea {
|
|
||||||
display: block;
|
|
||||||
max-width: $form-max-width;
|
|
||||||
}
|
|
||||||
|
|
||||||
.homepage my-markdown-textarea {
|
|
||||||
display: block;
|
|
||||||
max-width: 90%;
|
|
||||||
|
|
||||||
::ng-deep textarea {
|
|
||||||
height: 300px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=text],
|
|
||||||
input[type=number] {
|
|
||||||
@include peertube-input-text($form-base-input-width);
|
|
||||||
}
|
|
||||||
|
|
||||||
.number-with-unit {
|
|
||||||
position: relative;
|
|
||||||
width: fit-content;
|
|
||||||
|
|
||||||
input[type=number] + span {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.4em;
|
|
||||||
right: 3em;
|
|
||||||
|
|
||||||
@media screen and (max-width: $mobile-view) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[disabled] {
|
|
||||||
opacity: 0.8;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=checkbox] {
|
|
||||||
@include peertube-checkbox;
|
|
||||||
}
|
|
||||||
|
|
||||||
.peertube-select-container {
|
|
||||||
@include peertube-select-container($form-base-input-width);
|
|
||||||
}
|
|
||||||
|
|
||||||
my-select-checkbox,
|
|
||||||
my-select-options,
|
|
||||||
my-select-custom-value {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
@include responsive-width($form-base-input-width);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=submit] {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
@include margin-left(auto);
|
|
||||||
|
|
||||||
+ .form-error {
|
|
||||||
display: inline;
|
|
||||||
|
|
||||||
@include margin-left(5px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.inner-form-description {
|
|
||||||
font-size: 15px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
max-width: 100%;
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
@include peertube-textarea(500px, 150px);
|
|
||||||
|
|
||||||
&.small {
|
|
||||||
height: 75px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-small-info {
|
|
||||||
font-style: italic;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.disabled-checkbox-extra {
|
|
||||||
&,
|
|
||||||
::ng-deep label {
|
|
||||||
opacity: .5;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[disabled] {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngb-tabset:not(.previews) ::ng-deep {
|
|
||||||
.nav-link {
|
|
||||||
font-size: 105%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-error {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-signup {
|
|
||||||
width: fit-content;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.callout-container {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
height: 0;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
my-actor-banner-edit {
|
|
||||||
max-width: $form-max-width;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
font-weight: $font-bold;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
|
@ -1,479 +0,0 @@
|
||||||
import { NgFor, NgIf } from '@angular/common'
|
|
||||||
import { Component, OnInit, inject } from '@angular/core'
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
|
||||||
import { ConfigService } from '@app/+admin/config/shared/config.service'
|
|
||||||
import { Notifier } from '@app/core'
|
|
||||||
import { ServerService } from '@app/core/server/server.service'
|
|
||||||
import { URL_VALIDATOR } from '@app/shared/form-validators/common-validators'
|
|
||||||
import {
|
|
||||||
ADMIN_EMAIL_VALIDATOR,
|
|
||||||
CACHE_SIZE_VALIDATOR,
|
|
||||||
CONCURRENCY_VALIDATOR,
|
|
||||||
EXPORT_EXPIRATION_VALIDATOR,
|
|
||||||
EXPORT_MAX_USER_VIDEO_QUOTA_VALIDATOR,
|
|
||||||
INSTANCE_NAME_VALIDATOR,
|
|
||||||
INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
|
|
||||||
MAX_INSTANCE_LIVES_VALIDATOR,
|
|
||||||
MAX_LIVE_DURATION_VALIDATOR,
|
|
||||||
MAX_SYNC_PER_USER,
|
|
||||||
MAX_USER_LIVES_VALIDATOR,
|
|
||||||
MAX_VIDEO_CHANNELS_PER_USER_VALIDATOR,
|
|
||||||
SERVICES_TWITTER_USERNAME_VALIDATOR,
|
|
||||||
SIGNUP_LIMIT_VALIDATOR,
|
|
||||||
SIGNUP_MINIMUM_AGE_VALIDATOR,
|
|
||||||
TRANSCODING_MAX_FPS_VALIDATOR,
|
|
||||||
TRANSCODING_THREADS_VALIDATOR
|
|
||||||
} from '@app/shared/form-validators/custom-config-validators'
|
|
||||||
import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
|
||||||
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
|
||||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
|
||||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
|
||||||
import { CustomPageService } from '@app/shared/shared-main/custom-page/custom-page.service'
|
|
||||||
import { NgbNav, NgbNavContent, NgbNavItem, NgbNavLink, NgbNavLinkBase, NgbNavOutlet } from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import { CustomConfig, CustomPage, HTMLServerConfig } from '@peertube/peertube-models'
|
|
||||||
import merge from 'lodash-es/merge'
|
|
||||||
import omit from 'lodash-es/omit'
|
|
||||||
import { forkJoin } from 'rxjs'
|
|
||||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
|
||||||
import { EditAdvancedConfigurationComponent } from './edit-advanced-configuration.component'
|
|
||||||
import { EditBasicConfigurationComponent } from './edit-basic-configuration.component'
|
|
||||||
import { EditConfigurationService } from './edit-configuration.service'
|
|
||||||
import { EditHomepageComponent } from './edit-homepage.component'
|
|
||||||
import { EditInstanceInformationComponent } from './edit-instance-information.component'
|
|
||||||
import { EditLiveConfigurationComponent } from './edit-live-configuration.component'
|
|
||||||
import { EditVODTranscodingComponent } from './edit-vod-transcoding.component'
|
|
||||||
|
|
||||||
type ComponentCustomConfig = CustomConfig & {
|
|
||||||
instanceCustomHomepage: CustomPage
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'my-edit-custom-config',
|
|
||||||
templateUrl: './edit-custom-config.component.html',
|
|
||||||
styleUrls: [ './edit-custom-config.component.scss' ],
|
|
||||||
imports: [
|
|
||||||
NgIf,
|
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
NgbNav,
|
|
||||||
NgbNavItem,
|
|
||||||
NgbNavLink,
|
|
||||||
NgbNavLinkBase,
|
|
||||||
NgbNavContent,
|
|
||||||
EditHomepageComponent,
|
|
||||||
EditInstanceInformationComponent,
|
|
||||||
EditBasicConfigurationComponent,
|
|
||||||
EditVODTranscodingComponent,
|
|
||||||
EditLiveConfigurationComponent,
|
|
||||||
EditAdvancedConfigurationComponent,
|
|
||||||
NgbNavOutlet,
|
|
||||||
NgFor,
|
|
||||||
AlertComponent
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|
||||||
protected formReactiveService = inject(FormReactiveService)
|
|
||||||
private router = inject(Router)
|
|
||||||
private route = inject(ActivatedRoute)
|
|
||||||
private notifier = inject(Notifier)
|
|
||||||
private configService = inject(ConfigService)
|
|
||||||
private customPage = inject(CustomPageService)
|
|
||||||
private serverService = inject(ServerService)
|
|
||||||
private editConfigurationService = inject(EditConfigurationService)
|
|
||||||
|
|
||||||
activeNav: string
|
|
||||||
|
|
||||||
customConfig: ComponentCustomConfig
|
|
||||||
serverConfig: HTMLServerConfig
|
|
||||||
|
|
||||||
homepage: CustomPage
|
|
||||||
|
|
||||||
languageItems: SelectOptionsItem[] = []
|
|
||||||
categoryItems: SelectOptionsItem[] = []
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.serverConfig = this.serverService.getHTMLConfig()
|
|
||||||
|
|
||||||
const formGroupData: { [key in keyof ComponentCustomConfig]: any } = {
|
|
||||||
instance: {
|
|
||||||
name: INSTANCE_NAME_VALIDATOR,
|
|
||||||
shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
|
|
||||||
description: null,
|
|
||||||
|
|
||||||
isNSFW: false,
|
|
||||||
defaultNSFWPolicy: null,
|
|
||||||
|
|
||||||
terms: null,
|
|
||||||
codeOfConduct: null,
|
|
||||||
|
|
||||||
creationReason: null,
|
|
||||||
moderationInformation: null,
|
|
||||||
administrator: null,
|
|
||||||
maintenanceLifetime: null,
|
|
||||||
businessModel: null,
|
|
||||||
|
|
||||||
hardwareInformation: null,
|
|
||||||
|
|
||||||
categories: null,
|
|
||||||
languages: null,
|
|
||||||
|
|
||||||
serverCountry: null,
|
|
||||||
support: {
|
|
||||||
text: null
|
|
||||||
},
|
|
||||||
social: {
|
|
||||||
externalLink: URL_VALIDATOR,
|
|
||||||
mastodonLink: URL_VALIDATOR,
|
|
||||||
blueskyLink: URL_VALIDATOR
|
|
||||||
},
|
|
||||||
|
|
||||||
defaultClientRoute: null,
|
|
||||||
|
|
||||||
customizations: {
|
|
||||||
javascript: null,
|
|
||||||
css: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
theme: {
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
services: {
|
|
||||||
twitter: {
|
|
||||||
username: SERVICES_TWITTER_USERNAME_VALIDATOR
|
|
||||||
}
|
|
||||||
},
|
|
||||||
client: {
|
|
||||||
videos: {
|
|
||||||
miniature: {
|
|
||||||
preferAuthorDisplayName: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
menu: {
|
|
||||||
login: {
|
|
||||||
redirectOnSingleExternalAuth: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cache: {
|
|
||||||
previews: {
|
|
||||||
size: CACHE_SIZE_VALIDATOR
|
|
||||||
},
|
|
||||||
captions: {
|
|
||||||
size: CACHE_SIZE_VALIDATOR
|
|
||||||
},
|
|
||||||
torrents: {
|
|
||||||
size: CACHE_SIZE_VALIDATOR
|
|
||||||
},
|
|
||||||
storyboards: {
|
|
||||||
size: CACHE_SIZE_VALIDATOR
|
|
||||||
}
|
|
||||||
},
|
|
||||||
signup: {
|
|
||||||
enabled: null,
|
|
||||||
limit: SIGNUP_LIMIT_VALIDATOR,
|
|
||||||
requiresApproval: null,
|
|
||||||
requiresEmailVerification: null,
|
|
||||||
minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR
|
|
||||||
},
|
|
||||||
import: {
|
|
||||||
videos: {
|
|
||||||
concurrency: CONCURRENCY_VALIDATOR,
|
|
||||||
http: {
|
|
||||||
enabled: null
|
|
||||||
},
|
|
||||||
torrent: {
|
|
||||||
enabled: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
videoChannelSynchronization: {
|
|
||||||
enabled: null,
|
|
||||||
maxPerUser: MAX_SYNC_PER_USER
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
enabled: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
export: {
|
|
||||||
users: {
|
|
||||||
enabled: null,
|
|
||||||
maxUserVideoQuota: EXPORT_MAX_USER_VIDEO_QUOTA_VALIDATOR,
|
|
||||||
exportExpiration: EXPORT_EXPIRATION_VALIDATOR
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trending: {
|
|
||||||
videos: {
|
|
||||||
algorithms: {
|
|
||||||
enabled: null,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
admin: {
|
|
||||||
email: ADMIN_EMAIL_VALIDATOR
|
|
||||||
},
|
|
||||||
contactForm: {
|
|
||||||
enabled: null
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
history: {
|
|
||||||
videos: {
|
|
||||||
enabled: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
videoQuota: USER_VIDEO_QUOTA_VALIDATOR,
|
|
||||||
videoQuotaDaily: USER_VIDEO_QUOTA_DAILY_VALIDATOR
|
|
||||||
},
|
|
||||||
videoChannels: {
|
|
||||||
maxPerUser: MAX_VIDEO_CHANNELS_PER_USER_VALIDATOR
|
|
||||||
},
|
|
||||||
transcoding: {
|
|
||||||
enabled: null,
|
|
||||||
threads: TRANSCODING_THREADS_VALIDATOR,
|
|
||||||
allowAdditionalExtensions: null,
|
|
||||||
allowAudioFiles: null,
|
|
||||||
profile: null,
|
|
||||||
concurrency: CONCURRENCY_VALIDATOR,
|
|
||||||
resolutions: {},
|
|
||||||
alwaysTranscodeOriginalResolution: null,
|
|
||||||
originalFile: {
|
|
||||||
keep: null
|
|
||||||
},
|
|
||||||
hls: {
|
|
||||||
enabled: null,
|
|
||||||
splitAudioAndVideo: null
|
|
||||||
},
|
|
||||||
webVideos: {
|
|
||||||
enabled: null
|
|
||||||
},
|
|
||||||
remoteRunners: {
|
|
||||||
enabled: null
|
|
||||||
},
|
|
||||||
fps: {
|
|
||||||
max: TRANSCODING_MAX_FPS_VALIDATOR
|
|
||||||
}
|
|
||||||
},
|
|
||||||
live: {
|
|
||||||
enabled: null,
|
|
||||||
|
|
||||||
maxDuration: MAX_LIVE_DURATION_VALIDATOR,
|
|
||||||
maxInstanceLives: MAX_INSTANCE_LIVES_VALIDATOR,
|
|
||||||
maxUserLives: MAX_USER_LIVES_VALIDATOR,
|
|
||||||
allowReplay: null,
|
|
||||||
latencySetting: {
|
|
||||||
enabled: null
|
|
||||||
},
|
|
||||||
|
|
||||||
transcoding: {
|
|
||||||
enabled: null,
|
|
||||||
threads: TRANSCODING_THREADS_VALIDATOR,
|
|
||||||
profile: null,
|
|
||||||
resolutions: {},
|
|
||||||
alwaysTranscodeOriginalResolution: null,
|
|
||||||
remoteRunners: {
|
|
||||||
enabled: null
|
|
||||||
},
|
|
||||||
fps: {
|
|
||||||
max: TRANSCODING_MAX_FPS_VALIDATOR
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
videoStudio: {
|
|
||||||
enabled: null,
|
|
||||||
remoteRunners: {
|
|
||||||
enabled: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
videoTranscription: {
|
|
||||||
enabled: null,
|
|
||||||
remoteRunners: {
|
|
||||||
enabled: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
videoFile: {
|
|
||||||
update: {
|
|
||||||
enabled: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
autoBlacklist: {
|
|
||||||
videos: {
|
|
||||||
ofUsers: {
|
|
||||||
enabled: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
followers: {
|
|
||||||
instance: {
|
|
||||||
enabled: null,
|
|
||||||
manualApproval: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
followings: {
|
|
||||||
instance: {
|
|
||||||
autoFollowBack: {
|
|
||||||
enabled: null
|
|
||||||
},
|
|
||||||
autoFollowIndex: {
|
|
||||||
enabled: null,
|
|
||||||
indexUrl: URL_VALIDATOR
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
broadcastMessage: {
|
|
||||||
enabled: null,
|
|
||||||
level: null,
|
|
||||||
dismissable: null,
|
|
||||||
message: null
|
|
||||||
},
|
|
||||||
search: {
|
|
||||||
remoteUri: {
|
|
||||||
users: null,
|
|
||||||
anonymous: null
|
|
||||||
},
|
|
||||||
searchIndex: {
|
|
||||||
enabled: null,
|
|
||||||
url: URL_VALIDATOR,
|
|
||||||
disableLocalSearch: null,
|
|
||||||
isDefaultSearch: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
instanceCustomHomepage: {
|
|
||||||
content: null
|
|
||||||
},
|
|
||||||
|
|
||||||
storyboards: {
|
|
||||||
enabled: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultValues = {
|
|
||||||
transcoding: {
|
|
||||||
resolutions: {} as { [id: string]: string }
|
|
||||||
},
|
|
||||||
live: {
|
|
||||||
transcoding: {
|
|
||||||
resolutions: {} as { [id: string]: string }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const resolution of this.editConfigurationService.getTranscodingResolutions()) {
|
|
||||||
defaultValues.transcoding.resolutions[resolution.id] = 'false'
|
|
||||||
formGroupData.transcoding.resolutions[resolution.id] = null
|
|
||||||
|
|
||||||
defaultValues.live.transcoding.resolutions[resolution.id] = 'false'
|
|
||||||
formGroupData.live.transcoding.resolutions[resolution.id] = null
|
|
||||||
}
|
|
||||||
|
|
||||||
this.buildForm(formGroupData)
|
|
||||||
|
|
||||||
if (this.route.snapshot.fragment) {
|
|
||||||
this.onNavChange(this.route.snapshot.fragment)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadConfigAndUpdateForm()
|
|
||||||
this.loadCategoriesAndLanguages()
|
|
||||||
|
|
||||||
if (!this.isUpdateAllowed()) {
|
|
||||||
this.form.disable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formValidated () {
|
|
||||||
this.forceCheck()
|
|
||||||
if (!this.form.valid) return
|
|
||||||
|
|
||||||
const value: ComponentCustomConfig = merge(this.customConfig, this.form.getRawValue())
|
|
||||||
|
|
||||||
forkJoin([
|
|
||||||
this.configService.updateCustomConfig(omit(value, 'instanceCustomHomepage')),
|
|
||||||
this.customPage.updateInstanceHomepage(value.instanceCustomHomepage.content)
|
|
||||||
])
|
|
||||||
.subscribe({
|
|
||||||
next: ([ resConfig ]) => {
|
|
||||||
const instanceCustomHomepage = { content: value.instanceCustomHomepage.content }
|
|
||||||
|
|
||||||
this.customConfig = { ...resConfig, instanceCustomHomepage }
|
|
||||||
|
|
||||||
// Reload general configuration
|
|
||||||
this.serverService.resetConfig()
|
|
||||||
.subscribe(config => {
|
|
||||||
this.serverConfig = config
|
|
||||||
})
|
|
||||||
|
|
||||||
this.updateForm()
|
|
||||||
|
|
||||||
this.notifier.success($localize`Configuration updated.`)
|
|
||||||
},
|
|
||||||
|
|
||||||
error: err => this.notifier.error(err.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
isUpdateAllowed () {
|
|
||||||
return this.serverConfig.webadmin.configuration.edition.allowed === true
|
|
||||||
}
|
|
||||||
|
|
||||||
hasConsistentOptions () {
|
|
||||||
if (this.hasLiveAllowReplayConsistentOptions()) return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
hasLiveAllowReplayConsistentOptions () {
|
|
||||||
if (
|
|
||||||
this.editConfigurationService.isTranscodingEnabled(this.form) === false &&
|
|
||||||
this.editConfigurationService.isLiveEnabled(this.form) &&
|
|
||||||
this.form.value['live']['allowReplay'] === true
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
onNavChange (newActiveNav: string) {
|
|
||||||
this.activeNav = newActiveNav
|
|
||||||
|
|
||||||
this.router.navigate([], { fragment: this.activeNav })
|
|
||||||
}
|
|
||||||
|
|
||||||
grabAllErrors () {
|
|
||||||
return this.formReactiveService.grabAllErrors(this.formErrors)
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateForm () {
|
|
||||||
this.form.patchValue(this.customConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadConfigAndUpdateForm () {
|
|
||||||
forkJoin([
|
|
||||||
this.configService.getCustomConfig(),
|
|
||||||
this.customPage.getInstanceHomepage()
|
|
||||||
]).subscribe({
|
|
||||||
next: ([ config, homepage ]) => {
|
|
||||||
this.customConfig = { ...config, instanceCustomHomepage: homepage }
|
|
||||||
|
|
||||||
this.updateForm()
|
|
||||||
this.markAllAsDirty()
|
|
||||||
},
|
|
||||||
|
|
||||||
error: err => this.notifier.error(err.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadCategoriesAndLanguages () {
|
|
||||||
forkJoin([
|
|
||||||
this.serverService.getVideoLanguages(),
|
|
||||||
this.serverService.getVideoCategories()
|
|
||||||
]).subscribe({
|
|
||||||
next: ([ languages, categories ]) => {
|
|
||||||
this.languageItems = languages.map(l => ({ label: l.label, id: l.id }))
|
|
||||||
this.categoryItems = categories.map(l => ({ label: l.label, id: l.id }))
|
|
||||||
},
|
|
||||||
|
|
||||||
error: err => this.notifier.error(err.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
<ng-container [formGroup]="form()">
|
|
||||||
|
|
||||||
<ng-container formGroupName="instanceCustomHomepage">
|
|
||||||
|
|
||||||
<div class="homepage pt-two-cols mt-5"> <!-- homepage grid -->
|
|
||||||
<div class="title-col">
|
|
||||||
<h2 i18n>INSTANCE HOMEPAGE</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content-col">
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label i18n for="instanceCustomHomepageContent">Homepage</label>
|
|
||||||
<div class="label-small-info">
|
|
||||||
<my-custom-markup-help></my-custom-markup-help>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<my-markdown-textarea
|
|
||||||
inputId="instanceCustomHomepageContent" formControlName="content"
|
|
||||||
[customMarkdownRenderer]="getCustomMarkdownRenderer()" [debounceTime]="500"
|
|
||||||
[formError]="formErrors()['instanceCustomHomepage.content']"
|
|
||||||
dir="ltr" monospace="true"
|
|
||||||
></my-markdown-textarea>
|
|
||||||
|
|
||||||
<div *ngIf="formErrors().instanceCustomHomepage.content" class="form-error" role="alert">{{ formErrors().instanceCustomHomepage.content }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
</ng-container>
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { Component, inject, input } from '@angular/core'
|
|
||||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { NgIf } from '@angular/common'
|
|
||||||
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
|
||||||
import { CustomMarkupHelpComponent } from '../../../shared/shared-custom-markup/custom-markup-help.component'
|
|
||||||
import { CustomMarkupService } from '@app/shared/shared-custom-markup/custom-markup.service'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'my-edit-homepage',
|
|
||||||
templateUrl: './edit-homepage.component.html',
|
|
||||||
styleUrls: [ './edit-custom-config.component.scss' ],
|
|
||||||
imports: [ FormsModule, ReactiveFormsModule, CustomMarkupHelpComponent, MarkdownTextareaComponent, NgIf ]
|
|
||||||
})
|
|
||||||
export class EditHomepageComponent {
|
|
||||||
private customMarkup = inject(CustomMarkupService)
|
|
||||||
|
|
||||||
readonly form = input<FormGroup>(undefined)
|
|
||||||
readonly formErrors = input<any>(undefined)
|
|
||||||
|
|
||||||
customMarkdownRenderer: (text: string) => Promise<HTMLElement>
|
|
||||||
|
|
||||||
getCustomMarkdownRenderer () {
|
|
||||||
return this.customMarkup.getCustomMarkdownRenderer()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,158 +0,0 @@
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { HttpErrorResponse } from '@angular/common/http'
|
|
||||||
import { Component, OnInit, inject, input } from '@angular/core'
|
|
||||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { RouterLink } from '@angular/router'
|
|
||||||
import { Notifier, ServerService } from '@app/core'
|
|
||||||
import { genericUploadErrorHandler } from '@app/helpers'
|
|
||||||
import { CustomMarkupService } from '@app/shared/shared-custom-markup/custom-markup.service'
|
|
||||||
import { SelectRadioComponent } from '@app/shared/shared-forms/select/select-radio.component'
|
|
||||||
import { InstanceService } from '@app/shared/shared-main/instance/instance.service'
|
|
||||||
import { maxBy } from '@peertube/peertube-core-utils'
|
|
||||||
import { ActorImage, HTMLServerConfig } from '@peertube/peertube-models'
|
|
||||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
|
||||||
import { ActorAvatarEditComponent } from '../../../shared/shared-actor-image-edit/actor-avatar-edit.component'
|
|
||||||
import { ActorBannerEditComponent } from '../../../shared/shared-actor-image-edit/actor-banner-edit.component'
|
|
||||||
import { CustomMarkupHelpComponent } from '../../../shared/shared-custom-markup/custom-markup-help.component'
|
|
||||||
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
|
||||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
|
||||||
import { SelectCheckboxComponent } from '../../../shared/shared-forms/select/select-checkbox.component'
|
|
||||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
|
||||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'my-edit-instance-information',
|
|
||||||
templateUrl: './edit-instance-information.component.html',
|
|
||||||
styleUrls: [ './edit-custom-config.component.scss' ],
|
|
||||||
imports: [
|
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
ActorAvatarEditComponent,
|
|
||||||
ActorBannerEditComponent,
|
|
||||||
SelectRadioComponent,
|
|
||||||
CommonModule,
|
|
||||||
CustomMarkupHelpComponent,
|
|
||||||
MarkdownTextareaComponent,
|
|
||||||
SelectCheckboxComponent,
|
|
||||||
RouterLink,
|
|
||||||
PeertubeCheckboxComponent,
|
|
||||||
PeerTubeTemplateDirective,
|
|
||||||
HelpComponent
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class EditInstanceInformationComponent implements OnInit {
|
|
||||||
private customMarkup = inject(CustomMarkupService)
|
|
||||||
private notifier = inject(Notifier)
|
|
||||||
private instanceService = inject(InstanceService)
|
|
||||||
private server = inject(ServerService)
|
|
||||||
|
|
||||||
readonly form = input<FormGroup>(undefined)
|
|
||||||
readonly formErrors = input<any>(undefined)
|
|
||||||
|
|
||||||
readonly languageItems = input<SelectOptionsItem[]>([])
|
|
||||||
readonly categoryItems = input<SelectOptionsItem[]>([])
|
|
||||||
|
|
||||||
instanceBannerUrl: string
|
|
||||||
instanceAvatars: ActorImage[] = []
|
|
||||||
|
|
||||||
nsfwItems: SelectOptionsItem[] = [
|
|
||||||
{
|
|
||||||
id: 'do_not_list',
|
|
||||||
label: $localize`Hide`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'warn',
|
|
||||||
label: $localize`Warn`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'blur',
|
|
||||||
label: $localize`Blur`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'display',
|
|
||||||
label: $localize`Display`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
private serverConfig: HTMLServerConfig
|
|
||||||
|
|
||||||
get instanceName () {
|
|
||||||
return this.server.getHTMLConfig().instance.name
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.serverConfig = this.server.getHTMLConfig()
|
|
||||||
|
|
||||||
this.updateActorImages()
|
|
||||||
}
|
|
||||||
|
|
||||||
getCustomMarkdownRenderer () {
|
|
||||||
return this.customMarkup.getCustomMarkdownRenderer()
|
|
||||||
}
|
|
||||||
|
|
||||||
onBannerChange (formData: FormData) {
|
|
||||||
this.instanceService.updateInstanceBanner(formData)
|
|
||||||
.subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.notifier.success($localize`Banner changed.`)
|
|
||||||
|
|
||||||
this.resetActorImages()
|
|
||||||
},
|
|
||||||
|
|
||||||
error: (err: HttpErrorResponse) => genericUploadErrorHandler({ err, name: $localize`banner`, notifier: this.notifier })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onBannerDelete () {
|
|
||||||
this.instanceService.deleteInstanceBanner()
|
|
||||||
.subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.notifier.success($localize`Banner deleted.`)
|
|
||||||
|
|
||||||
this.resetActorImages()
|
|
||||||
},
|
|
||||||
|
|
||||||
error: err => this.notifier.error(err.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onAvatarChange (formData: FormData) {
|
|
||||||
this.instanceService.updateInstanceAvatar(formData)
|
|
||||||
.subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.notifier.success($localize`Avatar changed.`)
|
|
||||||
|
|
||||||
this.resetActorImages()
|
|
||||||
},
|
|
||||||
|
|
||||||
error: (err: HttpErrorResponse) => genericUploadErrorHandler({ err, name: $localize`avatar`, notifier: this.notifier })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onAvatarDelete () {
|
|
||||||
this.instanceService.deleteInstanceAvatar()
|
|
||||||
.subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.notifier.success($localize`Avatar deleted.`)
|
|
||||||
|
|
||||||
this.resetActorImages()
|
|
||||||
},
|
|
||||||
|
|
||||||
error: err => this.notifier.error(err.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateActorImages () {
|
|
||||||
this.instanceBannerUrl = maxBy(this.serverConfig.instance.banners, 'width')?.path
|
|
||||||
this.instanceAvatars = this.serverConfig.instance.avatars
|
|
||||||
}
|
|
||||||
|
|
||||||
private resetActorImages () {
|
|
||||||
this.server.resetConfig()
|
|
||||||
.subscribe(config => {
|
|
||||||
this.serverConfig = config
|
|
||||||
|
|
||||||
this.updateActorImages()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
|
||||||
import { Component, OnChanges, OnInit, SimpleChanges, inject, input } from '@angular/core'
|
|
||||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
|
||||||
import { ConfigService } from '../shared/config.service'
|
|
||||||
import { EditConfigurationService, ResolutionOption } from './edit-configuration.service'
|
|
||||||
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
|
||||||
import { RouterLink } from '@angular/router'
|
|
||||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
|
||||||
import { NgClass, NgIf, NgFor } from '@angular/common'
|
|
||||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
|
||||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'my-edit-live-configuration',
|
|
||||||
templateUrl: './edit-live-configuration.component.html',
|
|
||||||
styleUrls: [ './edit-custom-config.component.scss' ],
|
|
||||||
imports: [
|
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
PeertubeCheckboxComponent,
|
|
||||||
PeerTubeTemplateDirective,
|
|
||||||
NgClass,
|
|
||||||
NgIf,
|
|
||||||
SelectOptionsComponent,
|
|
||||||
NgFor,
|
|
||||||
RouterLink,
|
|
||||||
SelectCustomValueComponent
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class EditLiveConfigurationComponent implements OnInit, OnChanges {
|
|
||||||
private configService = inject(ConfigService)
|
|
||||||
private editConfigurationService = inject(EditConfigurationService)
|
|
||||||
|
|
||||||
readonly form = input<FormGroup>(undefined)
|
|
||||||
readonly formErrors = input<any>(undefined)
|
|
||||||
readonly serverConfig = input<HTMLServerConfig>(undefined)
|
|
||||||
|
|
||||||
transcodingThreadOptions: SelectOptionsItem[] = []
|
|
||||||
transcodingProfiles: SelectOptionsItem[] = []
|
|
||||||
|
|
||||||
liveMaxDurationOptions: SelectOptionsItem[] = []
|
|
||||||
liveResolutions: ResolutionOption[] = []
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.transcodingThreadOptions = this.configService.transcodingThreadOptions
|
|
||||||
|
|
||||||
this.liveMaxDurationOptions = [
|
|
||||||
{ id: -1, label: $localize`No limit` },
|
|
||||||
{ id: 1000 * 3600, label: $localize`1 hour` },
|
|
||||||
{ id: 1000 * 3600 * 3, label: $localize`3 hours` },
|
|
||||||
{ id: 1000 * 3600 * 5, label: $localize`5 hours` },
|
|
||||||
{ id: 1000 * 3600 * 10, label: $localize`10 hours` }
|
|
||||||
]
|
|
||||||
|
|
||||||
this.liveResolutions = this.editConfigurationService.getTranscodingResolutions()
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnChanges (changes: SimpleChanges) {
|
|
||||||
if (changes['serverConfig']) {
|
|
||||||
this.transcodingProfiles = this.buildAvailableTranscodingProfile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildAvailableTranscodingProfile () {
|
|
||||||
const profiles = this.serverConfig().live.transcoding.availableProfiles
|
|
||||||
|
|
||||||
return profiles.map(p => {
|
|
||||||
if (p === 'default') {
|
|
||||||
return { id: p, label: $localize`Default`, description: $localize`x264, targeting maximum device compatibility` }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { id: p, label: p }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getResolutionKey (resolution: string) {
|
|
||||||
return 'live.transcoding.resolutions.' + resolution
|
|
||||||
}
|
|
||||||
|
|
||||||
getLiveRTMPPort () {
|
|
||||||
return this.serverConfig().live.rtmp.port
|
|
||||||
}
|
|
||||||
|
|
||||||
isLiveEnabled () {
|
|
||||||
return this.editConfigurationService.isLiveEnabled(this.form())
|
|
||||||
}
|
|
||||||
|
|
||||||
isRemoteRunnerLiveEnabled () {
|
|
||||||
return this.editConfigurationService.isRemoteRunnerLiveEnabled(this.form())
|
|
||||||
}
|
|
||||||
|
|
||||||
getDisabledLiveClass () {
|
|
||||||
return { 'disabled-checkbox-extra': !this.isLiveEnabled() }
|
|
||||||
}
|
|
||||||
|
|
||||||
getDisabledLiveTranscodingClass () {
|
|
||||||
return { 'disabled-checkbox-extra': !this.isLiveEnabled() || !this.isLiveTranscodingEnabled() }
|
|
||||||
}
|
|
||||||
|
|
||||||
getDisabledLiveLocalTranscodingClass () {
|
|
||||||
return { 'disabled-checkbox-extra': !this.isLiveEnabled() || !this.isLiveTranscodingEnabled() || this.isRemoteRunnerLiveEnabled() }
|
|
||||||
}
|
|
||||||
|
|
||||||
isLiveTranscodingEnabled () {
|
|
||||||
return this.editConfigurationService.isLiveTranscodingEnabled(this.form())
|
|
||||||
}
|
|
||||||
|
|
||||||
getTotalTranscodingThreads () {
|
|
||||||
return this.editConfigurationService.getTotalTranscodingThreads(this.form())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,159 +0,0 @@
|
||||||
import { NgClass, NgFor, NgIf } from '@angular/common'
|
|
||||||
import { Component, OnChanges, OnInit, SimpleChanges, inject, input } from '@angular/core'
|
|
||||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { RouterLink } from '@angular/router'
|
|
||||||
import { Notifier } from '@app/core'
|
|
||||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
|
||||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
|
||||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
|
||||||
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
|
||||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
|
||||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
|
||||||
import { ConfigService } from '../shared/config.service'
|
|
||||||
import { EditConfigurationService, ResolutionOption } from './edit-configuration.service'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'my-edit-vod-transcoding',
|
|
||||||
templateUrl: './edit-vod-transcoding.component.html',
|
|
||||||
styleUrls: [ './edit-custom-config.component.scss' ],
|
|
||||||
imports: [
|
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
PeertubeCheckboxComponent,
|
|
||||||
PeerTubeTemplateDirective,
|
|
||||||
NgClass,
|
|
||||||
NgFor,
|
|
||||||
NgIf,
|
|
||||||
RouterLink,
|
|
||||||
SelectCustomValueComponent,
|
|
||||||
SelectOptionsComponent
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class EditVODTranscodingComponent implements OnInit, OnChanges {
|
|
||||||
private configService = inject(ConfigService)
|
|
||||||
private editConfigurationService = inject(EditConfigurationService)
|
|
||||||
private notifier = inject(Notifier)
|
|
||||||
|
|
||||||
readonly form = input<FormGroup>(undefined)
|
|
||||||
readonly formErrors = input<any>(undefined)
|
|
||||||
readonly serverConfig = input<HTMLServerConfig>(undefined)
|
|
||||||
|
|
||||||
transcodingThreadOptions: SelectOptionsItem[] = []
|
|
||||||
transcodingProfiles: SelectOptionsItem[] = []
|
|
||||||
resolutions: ResolutionOption[] = []
|
|
||||||
|
|
||||||
additionalVideoExtensions = ''
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.transcodingThreadOptions = this.configService.transcodingThreadOptions
|
|
||||||
this.resolutions = this.editConfigurationService.getTranscodingResolutions()
|
|
||||||
|
|
||||||
this.checkTranscodingFields()
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnChanges (changes: SimpleChanges) {
|
|
||||||
if (changes['serverConfig']) {
|
|
||||||
this.transcodingProfiles = this.buildAvailableTranscodingProfile()
|
|
||||||
|
|
||||||
this.additionalVideoExtensions = this.serverConfig().video.file.extensions.join(' ')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildAvailableTranscodingProfile () {
|
|
||||||
const profiles = this.serverConfig().transcoding.availableProfiles
|
|
||||||
|
|
||||||
return profiles.map(p => {
|
|
||||||
if (p === 'default') {
|
|
||||||
return { id: p, label: $localize`Default`, description: $localize`x264, targeting maximum device compatibility` }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { id: p, label: p }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getResolutionKey (resolution: string) {
|
|
||||||
return 'transcoding.resolutions.' + resolution
|
|
||||||
}
|
|
||||||
|
|
||||||
isRemoteRunnerVODEnabled () {
|
|
||||||
return this.editConfigurationService.isRemoteRunnerVODEnabled(this.form())
|
|
||||||
}
|
|
||||||
|
|
||||||
isTranscodingEnabled () {
|
|
||||||
return this.editConfigurationService.isTranscodingEnabled(this.form())
|
|
||||||
}
|
|
||||||
|
|
||||||
isHLSEnabled () {
|
|
||||||
return this.editConfigurationService.isHLSEnabled(this.form())
|
|
||||||
}
|
|
||||||
|
|
||||||
isStudioEnabled () {
|
|
||||||
return this.editConfigurationService.isStudioEnabled(this.form())
|
|
||||||
}
|
|
||||||
|
|
||||||
getTranscodingDisabledClass () {
|
|
||||||
return { 'disabled-checkbox-extra': !this.isTranscodingEnabled() }
|
|
||||||
}
|
|
||||||
|
|
||||||
getHLSDisabledClass () {
|
|
||||||
return { 'disabled-checkbox-extra': !this.isHLSEnabled() }
|
|
||||||
}
|
|
||||||
|
|
||||||
getLocalTranscodingDisabledClass () {
|
|
||||||
return { 'disabled-checkbox-extra': !this.isTranscodingEnabled() || this.isRemoteRunnerVODEnabled() }
|
|
||||||
}
|
|
||||||
|
|
||||||
getStudioDisabledClass () {
|
|
||||||
return { 'disabled-checkbox-extra': !this.isStudioEnabled() }
|
|
||||||
}
|
|
||||||
|
|
||||||
getTotalTranscodingThreads () {
|
|
||||||
return this.editConfigurationService.getTotalTranscodingThreads(this.form())
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkTranscodingFields () {
|
|
||||||
const transcodingControl = this.form().get('transcoding.enabled')
|
|
||||||
const videoStudioControl = this.form().get('videoStudio.enabled')
|
|
||||||
const hlsControl = this.form().get('transcoding.hls.enabled')
|
|
||||||
const webVideosControl = this.form().get('transcoding.webVideos.enabled')
|
|
||||||
|
|
||||||
webVideosControl.valueChanges
|
|
||||||
.subscribe(newValue => {
|
|
||||||
if (newValue === false && hlsControl.value === false) {
|
|
||||||
hlsControl.setValue(true)
|
|
||||||
|
|
||||||
this.notifier.info(
|
|
||||||
$localize`Automatically enable HLS transcoding because at least 1 output format must be enabled when transcoding is enabled`,
|
|
||||||
'',
|
|
||||||
10000
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
hlsControl.valueChanges
|
|
||||||
.subscribe(newValue => {
|
|
||||||
if (newValue === false && webVideosControl.value === false) {
|
|
||||||
webVideosControl.setValue(true)
|
|
||||||
|
|
||||||
this.notifier.info(
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
$localize`Automatically enable Web Videos transcoding because at least 1 output format must be enabled when transcoding is enabled`,
|
|
||||||
'',
|
|
||||||
10000
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
transcodingControl.valueChanges
|
|
||||||
.subscribe(newValue => {
|
|
||||||
if (newValue === false) {
|
|
||||||
videoStudioControl.setValue(false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
transcodingControl.updateValueAndValidity()
|
|
||||||
webVideosControl.updateValueAndValidity()
|
|
||||||
videoStudioControl.updateValueAndValidity()
|
|
||||||
hlsControl.updateValueAndValidity()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
export * from './edit-advanced-configuration.component'
|
|
||||||
export * from './edit-basic-configuration.component'
|
|
||||||
export * from './edit-configuration.service'
|
|
||||||
export * from './edit-custom-config.component'
|
|
||||||
export * from './edit-homepage.component'
|
|
||||||
export * from './edit-instance-information.component'
|
|
||||||
export * from './edit-live-configuration.component'
|
|
||||||
export * from './edit-vod-transcoding.component'
|
|
|
@ -1,2 +1,2 @@
|
||||||
export * from './edit-custom-config'
|
export * from './pages'
|
||||||
export * from './config.routes'
|
export * from './config.routes'
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
<my-admin-save-bar i18n-title title="Advanced configuration" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||||
|
|
||||||
|
<ng-container [formGroup]="form">
|
||||||
|
|
||||||
|
<div class="pt-two-cols">
|
||||||
|
|
||||||
|
<div class="title-col">
|
||||||
|
<h2 i18n>CACHE</h2>
|
||||||
|
<div i18n class="inner-form-description">
|
||||||
|
Some files are not federated, and fetched when necessary. Define their caching policies.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-col">
|
||||||
|
<ng-container formGroupName="cache">
|
||||||
|
<div class="form-group" formGroupName="previews">
|
||||||
|
<label i18n for="cachePreviewsSize">Number of previews to keep in cache</label>
|
||||||
|
|
||||||
|
<div class="number-with-unit">
|
||||||
|
<input
|
||||||
|
type="number" min="0" id="cachePreviewsSize" class="form-control"
|
||||||
|
formControlName="size" [ngClass]="{ 'input-error': formErrors.cache.previews.size }"
|
||||||
|
>
|
||||||
|
<span i18n>{getCacheSize('previews'), plural, =1 {cached image} other {cached images}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.cache.previews.size" class="form-error" role="alert">{{ formErrors.cache.previews.size }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" formGroupName="captions">
|
||||||
|
<label i18n for="cacheCaptionsSize">Number of video captions to keep in cache</label>
|
||||||
|
|
||||||
|
<div class="number-with-unit">
|
||||||
|
<input
|
||||||
|
type="number" min="0" id="cacheCaptionsSize" class="form-control"
|
||||||
|
formControlName="size" [ngClass]="{ 'input-error': formErrors.cache.captions.size }"
|
||||||
|
>
|
||||||
|
<span i18n>{getCacheSize('captions'), plural, =1 {cached caption} other {cached captions}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.cache.captions.size" class="form-error" role="alert">{{ formErrors.cache.captions.size }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" formGroupName="torrents">
|
||||||
|
<label i18n for="cacheTorrentsSize">Number of video torrents to keep in cache</label>
|
||||||
|
|
||||||
|
<div class="number-with-unit">
|
||||||
|
<input
|
||||||
|
type="number" min="0" id="cacheTorrentsSize" class="form-control"
|
||||||
|
formControlName="size" [ngClass]="{ 'input-error': formErrors.cache.torrents.size }"
|
||||||
|
>
|
||||||
|
<span i18n>{getCacheSize('torrents'), plural, =1 {cached torrent} other {cached torrents}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.cache.torrents.size" class="form-error" role="alert">{{ formErrors.cache.torrents.size }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" formGroupName="storyboards">
|
||||||
|
<label i18n for="cacheTorrentsSize">Number of video storyboard images to keep in cache</label>
|
||||||
|
|
||||||
|
<div class="number-with-unit">
|
||||||
|
<input
|
||||||
|
type="number" min="0" id="cacheStoryboardsSize" class="form-control"
|
||||||
|
formControlName="size" [ngClass]="{ 'input-error': formErrors.cache.storyboards.size }"
|
||||||
|
>
|
||||||
|
<span i18n>{getCacheSize('storyboards'), plural, =1 {cached storyboard} other {cached storyboards}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.cache.storyboards.size" class="form-error" role="alert">{{ formErrors.cache.storyboards.size }}</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-two-cols mt-4">
|
||||||
|
<div class="title-col">
|
||||||
|
<h2 i18n>TWITTER/X</h2>
|
||||||
|
|
||||||
|
<div i18n class="inner-form-description">
|
||||||
|
Extra configuration required by Twitter/X. All other social media (Facebook, Mastodon, etc.) are supported out of the box.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-col">
|
||||||
|
|
||||||
|
<ng-container formGroupName="services">
|
||||||
|
<ng-container formGroupName="twitter">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="servicesTwitterUsername" i18n>Your Twitter/X username</label>
|
||||||
|
|
||||||
|
<div class="form-group-description">
|
||||||
|
<p i18n class="mb-0">Indicates the Twitter/X account for the website or platform where the content was published.</p>
|
||||||
|
|
||||||
|
<p i18n>This is just an extra information injected in PeerTube HTML that is required by Twitter/X. If you don't have a Twitter/X account, just leave the default value.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text" id="servicesTwitterUsername" class="form-control"
|
||||||
|
formControlName="username" [ngClass]="{ 'input-error': formErrors.services.twitter.username }"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.services.twitter.username" class="form-error" role="alert">{{ formErrors.services.twitter.username }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, inject, OnInit } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { ActivatedRoute } from '@angular/router'
|
||||||
|
import { CanComponentDeactivate } from '@app/core'
|
||||||
|
import { CACHE_SIZE_VALIDATOR, SERVICES_TWITTER_USERNAME_VALIDATOR } from '@app/shared/form-validators/custom-config-validators'
|
||||||
|
import {
|
||||||
|
BuildFormArgumentTyped,
|
||||||
|
FormDefaultTyped,
|
||||||
|
FormReactiveErrorsTyped,
|
||||||
|
FormReactiveMessagesTyped
|
||||||
|
} from '@app/shared/form-validators/form-validator.model'
|
||||||
|
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
|
import { CustomConfig } from '@peertube/peertube-models'
|
||||||
|
import { AdminConfigService } from '../shared/admin-config.service'
|
||||||
|
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||||
|
|
||||||
|
type Form = {
|
||||||
|
services: FormGroup<{
|
||||||
|
twitter: FormGroup<{
|
||||||
|
username: FormControl<string>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
|
||||||
|
cache: FormGroup<{
|
||||||
|
previews: FormGroup<{
|
||||||
|
size: FormControl<number>
|
||||||
|
}>
|
||||||
|
captions: FormGroup<{
|
||||||
|
size: FormControl<number>
|
||||||
|
}>
|
||||||
|
torrents: FormGroup<{
|
||||||
|
size: FormControl<number>
|
||||||
|
}>
|
||||||
|
storyboards: FormGroup<{
|
||||||
|
size: FormControl<number>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-admin-config-advanced',
|
||||||
|
templateUrl: './admin-config-advanced.component.html',
|
||||||
|
styleUrls: [ './admin-config-common.scss' ],
|
||||||
|
imports: [ CommonModule, FormsModule, ReactiveFormsModule, AdminSaveBarComponent ]
|
||||||
|
})
|
||||||
|
export class AdminConfigAdvancedComponent implements OnInit, CanComponentDeactivate {
|
||||||
|
private route = inject(ActivatedRoute)
|
||||||
|
private formReactiveService = inject(FormReactiveService)
|
||||||
|
private adminConfigService = inject(AdminConfigService)
|
||||||
|
|
||||||
|
form: FormGroup<Form>
|
||||||
|
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||||
|
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||||
|
|
||||||
|
private customConfig: CustomConfig
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||||
|
|
||||||
|
this.buildForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeactivate () {
|
||||||
|
return { canDeactivate: !this.form.dirty }
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildForm () {
|
||||||
|
const obj: BuildFormArgumentTyped<Form> = {
|
||||||
|
services: {
|
||||||
|
twitter: {
|
||||||
|
username: SERVICES_TWITTER_USERNAME_VALIDATOR
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cache: {
|
||||||
|
previews: {
|
||||||
|
size: CACHE_SIZE_VALIDATOR
|
||||||
|
},
|
||||||
|
captions: {
|
||||||
|
size: CACHE_SIZE_VALIDATOR
|
||||||
|
},
|
||||||
|
torrents: {
|
||||||
|
size: CACHE_SIZE_VALIDATOR
|
||||||
|
},
|
||||||
|
storyboards: {
|
||||||
|
size: CACHE_SIZE_VALIDATOR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValues: FormDefaultTyped<Form> = this.customConfig
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
formErrors,
|
||||||
|
validationMessages
|
||||||
|
} = this.formReactiveService.buildForm<Form>(obj, defaultValues)
|
||||||
|
|
||||||
|
this.form = form
|
||||||
|
this.formErrors = formErrors
|
||||||
|
this.validationMessages = validationMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
getCacheSize (type: 'captions' | 'previews' | 'torrents' | 'storyboards') {
|
||||||
|
return this.form.value.cache[type].size
|
||||||
|
}
|
||||||
|
|
||||||
|
save () {
|
||||||
|
this.adminConfigService.saveAndUpdateCurrent({
|
||||||
|
currentConfig: this.customConfig,
|
||||||
|
form: this.form,
|
||||||
|
formConfig: this.form.value,
|
||||||
|
success: $localize`Advanced configuration updated.`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
80
client/src/app/+admin/config/pages/admin-config-common.scss
Normal file
80
client/src/app/+admin/config/pages/admin-config-common.scss
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
@use "_variables" as *;
|
||||||
|
@use "_mixins" as *;
|
||||||
|
@use "_form-mixins" as *;
|
||||||
|
|
||||||
|
$form-base-input-width: 340px;
|
||||||
|
$form-max-width: 500px;
|
||||||
|
|
||||||
|
form {
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
my-markdown-textarea {
|
||||||
|
display: block;
|
||||||
|
max-width: $form-max-width;
|
||||||
|
}
|
||||||
|
|
||||||
|
.homepage my-markdown-textarea {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
::ng-deep textarea {
|
||||||
|
height: 300px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"] {
|
||||||
|
@include peertube-input-text($form-base-input-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
@include peertube-checkbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peertube-select-container {
|
||||||
|
@include peertube-select-container($form-base-input-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
my-select-checkbox,
|
||||||
|
my-select-options,
|
||||||
|
my-select-custom-value {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
@include responsive-width($form-base-input-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-form-description {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: pvar(--fg-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
max-width: 100%;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
@include peertube-textarea(500px, 150px);
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
height: 75px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-checkbox-extra {
|
||||||
|
&,
|
||||||
|
::ng-deep label {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my-actor-banner-edit {
|
||||||
|
max-width: $form-max-width;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-weight: $font-bold;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
|
@ -0,0 +1,178 @@
|
||||||
|
<my-admin-save-bar i18n-title title="Platform customization" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||||
|
|
||||||
|
<div class="pt-two-cols" [formGroup]="form">
|
||||||
|
<div class="title-col">
|
||||||
|
<h2 i18n>APPEARANCE</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-col">
|
||||||
|
<ng-container formGroupName="theme">
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="themeDefault">Theme</label>
|
||||||
|
|
||||||
|
<my-select-options formControlName="default" inputId="themeDefault" [items]="availableThemes"></my-select-options>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container formGroupName="client">
|
||||||
|
<ng-container formGroupName="videos">
|
||||||
|
<ng-container formGroupName="miniature">
|
||||||
|
<div class="form-group">
|
||||||
|
<my-peertube-checkbox
|
||||||
|
inputName="clientVideosMiniaturePreferAuthorDisplayName"
|
||||||
|
formControlName="preferAuthorDisplayName"
|
||||||
|
i18n-labelText
|
||||||
|
labelText="Prefer author display name in video miniature"
|
||||||
|
></my-peertube-checkbox>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-two-cols mt-4" [formGroup]="form">
|
||||||
|
<div class="title-col">
|
||||||
|
<h2 i18n>CUSTOMIZATION</h2>
|
||||||
|
|
||||||
|
<div i18n class="inner-form-description">
|
||||||
|
Use <a class="link-primary" routerLink="/admin/settings/plugins">plugins & themes</a> for more involved changes
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-col">
|
||||||
|
<ng-template #alertIntro>
|
||||||
|
<div i18n>UI customization only applies if the user is using the default platform theme.</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
@if (getCurrentThemeName() !== getDefaultThemeName()) {
|
||||||
|
<my-alert type="warning">
|
||||||
|
<ng-template *ngTemplateOutlet="alertIntro"></ng-template>
|
||||||
|
|
||||||
|
<div i18n>You can't preview the changes because you aren't using your platform's default theme.</div>
|
||||||
|
<div i18n>Current theme: <strong>{{ getCurrentThemeLabel() }}</strong></div>
|
||||||
|
<div i18n>Platform theme: <strong>{{ getDefaultThemeLabel() }}</strong>.</div>
|
||||||
|
</my-alert>
|
||||||
|
} @else {
|
||||||
|
<my-alert type="info">
|
||||||
|
<ng-template *ngTemplateOutlet="alertIntro"></ng-template>
|
||||||
|
|
||||||
|
<div i18n>You can preview your UI customization but <strong>don't forget to save your changes</strong> once you are happy with the results.</div>
|
||||||
|
</my-alert>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="form-group" formGroupName="theme">
|
||||||
|
<ng-container formGroupName="customization">
|
||||||
|
@for (field of customizationFormFields; track field.name) {
|
||||||
|
<div class="form-group">
|
||||||
|
<label [for]="field.inputId">{{ field.label }}</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
*ngIf="!hasDefaultCustomizationValue(field.name)"
|
||||||
|
type="button"
|
||||||
|
i18n
|
||||||
|
class="reset-button reset-button-small"
|
||||||
|
(click)="resetCustomizationField(field.name)"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div *ngIf="field.description" class="form-group-description">{{ field.description }}</div>
|
||||||
|
|
||||||
|
@if (field.type === 'color') {
|
||||||
|
<p-colorpicker class="d-block" [inputId]="field.inputId" [formControlName]="field.name" />
|
||||||
|
} @else if (field.type === 'pixels' && isCustomizationFieldNumber(field.name)) {
|
||||||
|
<div class="number-with-unit">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
[id]="field.inputId"
|
||||||
|
[name]="field.inputId"
|
||||||
|
class="form-control"
|
||||||
|
[formControlName]="field.name"
|
||||||
|
[ngClass]="{ 'input-error': formErrors.theme.customization[field.name]}"
|
||||||
|
/>
|
||||||
|
<span>pixels</span>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[id]="field.inputId"
|
||||||
|
[name]="field.inputId"
|
||||||
|
class="form-control"
|
||||||
|
[formControlName]="field.name"
|
||||||
|
[ngClass]="{ 'input-error': formErrors.theme.customization[field.name]}"
|
||||||
|
>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-two-cols mt-4" [formGroup]="form">
|
||||||
|
<div class="title-col">
|
||||||
|
<div class="anchor" id="customizations"></div>
|
||||||
|
<!-- customizations anchor -->
|
||||||
|
<h2 i18n>Advanced</h2>
|
||||||
|
<div i18n class="inner-form-description">
|
||||||
|
Advanced modifications to your PeerTube platform if creating a plugin or a theme is overkill.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-col">
|
||||||
|
<ng-container formGroupName="instance">
|
||||||
|
<ng-container formGroupName="customizations">
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="customizationJavascript">JavaScript</label>
|
||||||
|
<my-help>
|
||||||
|
<ng-container i18n>
|
||||||
|
<p class="mb-2">Write JavaScript code directly. Example:</p>
|
||||||
|
<pre>console.log('my instance is amazing');</pre>
|
||||||
|
</ng-container>
|
||||||
|
</my-help>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
id="customizationJavascript"
|
||||||
|
formControlName="javascript"
|
||||||
|
class="form-control"
|
||||||
|
dir="ltr"
|
||||||
|
[ngClass]="{ 'input-error': formErrors.instance.customizations.javascript }"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.instance.customizations.javascript" class="form-error" role="alert">{{ formErrors.instance.customizations.javascript }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="customizationCSS">CSS</label>
|
||||||
|
|
||||||
|
<my-help>
|
||||||
|
<ng-container i18n>
|
||||||
|
<p class="mb-2">Write CSS code directly. Example:</p>
|
||||||
|
<pre>
|
||||||
|
#custom-css {{ '{' }}
|
||||||
|
color: red;
|
||||||
|
{{ '}' }}
|
||||||
|
</pre>
|
||||||
|
<p class="mb-2">Prepend with <em>#custom-css</em> to override styles. Example:</p>
|
||||||
|
<pre>
|
||||||
|
#custom-css .logged-in-email {{ '{' }}
|
||||||
|
color: red;
|
||||||
|
{{ '}' }}
|
||||||
|
</pre>
|
||||||
|
</ng-container>
|
||||||
|
</my-help>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
id="customizationCSS"
|
||||||
|
formControlName="css"
|
||||||
|
class="form-control"
|
||||||
|
dir="ltr"
|
||||||
|
[ngClass]="{ 'input-error': formErrors.instance.customizations.css }"
|
||||||
|
></textarea>
|
||||||
|
<div *ngIf="formErrors.instance.customizations.css" class="form-error" role="alert">{{ formErrors.instance.customizations.css }}</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,363 @@
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, inject, OnInit } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, ValueChangeEvent } from '@angular/forms'
|
||||||
|
import { ActivatedRoute, RouterModule } from '@angular/router'
|
||||||
|
import { CanComponentDeactivate, ServerService, ThemeService } from '@app/core'
|
||||||
|
import { BuildFormArgumentTyped, FormDefaultTyped, FormReactiveMessagesTyped } from '@app/shared/form-validators/form-validator.model'
|
||||||
|
import { FormReactiveErrorsTyped, FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
|
import { PeertubeCheckboxComponent } from '@app/shared/shared-forms/peertube-checkbox.component'
|
||||||
|
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
|
||||||
|
import { objectKeysTyped } from '@peertube/peertube-core-utils'
|
||||||
|
import { CustomConfig } from '@peertube/peertube-models'
|
||||||
|
import { logger } from '@root-helpers/logger'
|
||||||
|
import { capitalizeFirstLetter } from '@root-helpers/string'
|
||||||
|
import { ColorPaletteThemeConfig, ThemeCustomizationKey } from '@root-helpers/theme-manager'
|
||||||
|
import { formatHEX, parse } from 'color-bits'
|
||||||
|
import debug from 'debug'
|
||||||
|
import { ColorPickerModule } from 'primeng/colorpicker'
|
||||||
|
import { debounceTime } from 'rxjs'
|
||||||
|
import { SelectOptionsItem } from 'src/types'
|
||||||
|
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||||
|
import { AlertComponent } from '../../../shared/shared-main/common/alert.component'
|
||||||
|
import { AdminConfigService } from '../shared/admin-config.service'
|
||||||
|
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||||
|
|
||||||
|
const debugLogger = debug('peertube:config')
|
||||||
|
|
||||||
|
type Form = {
|
||||||
|
instance: FormGroup<{
|
||||||
|
customizations: FormGroup<{
|
||||||
|
css: FormControl<string>
|
||||||
|
javascript: FormControl<string>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
|
||||||
|
client: FormGroup<{
|
||||||
|
videos: FormGroup<{
|
||||||
|
miniature: FormGroup<{
|
||||||
|
preferAuthorDisplayName: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
|
||||||
|
theme: FormGroup<{
|
||||||
|
default: FormControl<string>
|
||||||
|
|
||||||
|
customization: FormGroup<{
|
||||||
|
primaryColor: FormControl<string>
|
||||||
|
foregroundColor: FormControl<string>
|
||||||
|
backgroundColor: FormControl<string>
|
||||||
|
backgroundSecondaryColor: FormControl<string>
|
||||||
|
menuForegroundColor: FormControl<string>
|
||||||
|
menuBackgroundColor: FormControl<string>
|
||||||
|
menuBorderRadius: FormControl<string>
|
||||||
|
headerForegroundColor: FormControl<string>
|
||||||
|
headerBackgroundColor: FormControl<string>
|
||||||
|
inputBorderRadius: FormControl<string>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-admin-config-customization',
|
||||||
|
templateUrl: './admin-config-customization.component.html',
|
||||||
|
styleUrls: [ './admin-config-common.scss' ],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
RouterModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
AdminSaveBarComponent,
|
||||||
|
ColorPickerModule,
|
||||||
|
AlertComponent,
|
||||||
|
SelectOptionsComponent,
|
||||||
|
HelpComponent,
|
||||||
|
PeertubeCheckboxComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminConfigCustomizationComponent implements OnInit, CanComponentDeactivate {
|
||||||
|
private formReactiveService = inject(FormReactiveService)
|
||||||
|
private adminConfigService = inject(AdminConfigService)
|
||||||
|
private serverService = inject(ServerService)
|
||||||
|
private themeService = inject(ThemeService)
|
||||||
|
private route = inject(ActivatedRoute)
|
||||||
|
|
||||||
|
form: FormGroup<Form>
|
||||||
|
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||||
|
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||||
|
|
||||||
|
customizationFormFields: {
|
||||||
|
label: string
|
||||||
|
inputId: string
|
||||||
|
name: ThemeCustomizationKey
|
||||||
|
description?: string
|
||||||
|
type: 'color' | 'pixels'
|
||||||
|
}[] = []
|
||||||
|
|
||||||
|
availableThemes: SelectOptionsItem[]
|
||||||
|
|
||||||
|
private customizationResetFields = new Set<ThemeCustomizationKey>()
|
||||||
|
private customConfig: CustomConfig
|
||||||
|
|
||||||
|
private readonly formFieldsObject: Record<ThemeCustomizationKey, { label: string, description?: string, type: 'color' | 'pixels' }> = {
|
||||||
|
primaryColor: { label: $localize`Primary color`, type: 'color' },
|
||||||
|
foregroundColor: { label: $localize`Foreground color`, type: 'color' },
|
||||||
|
backgroundColor: { label: $localize`Background color`, type: 'color' },
|
||||||
|
backgroundSecondaryColor: {
|
||||||
|
label: $localize`Secondary background color`,
|
||||||
|
description: $localize`Used as a background for inputs, overlays...`,
|
||||||
|
type: 'color'
|
||||||
|
},
|
||||||
|
menuForegroundColor: { label: $localize`Menu foreground color`, type: 'color' },
|
||||||
|
menuBackgroundColor: { label: $localize`Menu background color`, type: 'color' },
|
||||||
|
menuBorderRadius: { label: $localize`Menu border radius`, type: 'pixels' },
|
||||||
|
headerForegroundColor: { label: $localize`Header foreground color`, type: 'color' },
|
||||||
|
headerBackgroundColor: { label: $localize`Header background color`, type: 'color' },
|
||||||
|
inputBorderRadius: { label: $localize`Input border radius`, type: 'pixels' }
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||||
|
|
||||||
|
this.availableThemes = [
|
||||||
|
this.themeService.getDefaultThemeItem(),
|
||||||
|
|
||||||
|
...this.themeService.buildAvailableThemes()
|
||||||
|
]
|
||||||
|
|
||||||
|
this.buildForm()
|
||||||
|
this.subscribeToCustomizationChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeactivate () {
|
||||||
|
return { canDeactivate: !this.form.dirty }
|
||||||
|
}
|
||||||
|
|
||||||
|
private subscribeToCustomizationChanges () {
|
||||||
|
let currentAnimationFrame: number
|
||||||
|
|
||||||
|
this.form.get('theme.customization').valueChanges.pipe(debounceTime(250)).subscribe(formValues => {
|
||||||
|
if (currentAnimationFrame) {
|
||||||
|
cancelAnimationFrame(currentAnimationFrame)
|
||||||
|
currentAnimationFrame = null
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAnimationFrame = requestAnimationFrame(() => {
|
||||||
|
this.themeService.updateColorPalette({
|
||||||
|
...this.customConfig.theme,
|
||||||
|
|
||||||
|
customization: this.buildNewCustomization(formValues)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const [ key, control ] of Object.entries((this.form.get('theme.customization') as FormGroup).controls)) {
|
||||||
|
control.events.subscribe(event => {
|
||||||
|
if (event instanceof ValueChangeEvent) {
|
||||||
|
debugLogger(`Deleting "${key}" from reset fields`)
|
||||||
|
|
||||||
|
this.customizationResetFields.delete(key as ThemeCustomizationKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildForm () {
|
||||||
|
for (const [ untypedName, info ] of Object.entries(this.formFieldsObject)) {
|
||||||
|
const name = untypedName as ThemeCustomizationKey
|
||||||
|
|
||||||
|
this.customizationFormFields.push({
|
||||||
|
label: info.label,
|
||||||
|
type: info.type,
|
||||||
|
inputId: `themeCustomization${capitalizeFirstLetter(name)}`,
|
||||||
|
name
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!this.customConfig.theme.customization[name]) {
|
||||||
|
this.customizationResetFields.add(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj: BuildFormArgumentTyped<Form> = {
|
||||||
|
client: {
|
||||||
|
videos: {
|
||||||
|
miniature: {
|
||||||
|
preferAuthorDisplayName: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
instance: {
|
||||||
|
customizations: {
|
||||||
|
css: null,
|
||||||
|
javascript: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
default: null,
|
||||||
|
customization: {
|
||||||
|
primaryColor: null,
|
||||||
|
foregroundColor: null,
|
||||||
|
backgroundColor: null,
|
||||||
|
backgroundSecondaryColor: null,
|
||||||
|
menuForegroundColor: null,
|
||||||
|
menuBackgroundColor: null,
|
||||||
|
menuBorderRadius: null,
|
||||||
|
headerForegroundColor: null,
|
||||||
|
headerBackgroundColor: null,
|
||||||
|
inputBorderRadius: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValues: FormDefaultTyped<Form> = {
|
||||||
|
...this.customConfig,
|
||||||
|
|
||||||
|
theme: {
|
||||||
|
default: this.customConfig.theme.default,
|
||||||
|
customization: this.getDefaultCustomization()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
formErrors,
|
||||||
|
validationMessages
|
||||||
|
} = this.formReactiveService.buildForm<Form>(obj, defaultValues)
|
||||||
|
|
||||||
|
this.form = form
|
||||||
|
this.formErrors = formErrors
|
||||||
|
this.validationMessages = validationMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentThemeName () {
|
||||||
|
return this.themeService.getCurrentThemeName()
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentThemeLabel () {
|
||||||
|
return this.availableThemes.find(t => t.id === this.themeService.getCurrentThemeName())?.label
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultThemeName () {
|
||||||
|
return this.serverService.getHTMLConfig().theme.default
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultThemeLabel () {
|
||||||
|
return this.availableThemes.find(t => t.id === this.getDefaultThemeName())?.label
|
||||||
|
}
|
||||||
|
|
||||||
|
hasDefaultCustomizationValue (field: ThemeCustomizationKey) {
|
||||||
|
return this.customizationResetFields.has(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
resetCustomizationField (field: ThemeCustomizationKey) {
|
||||||
|
this.customizationResetFields.add(field)
|
||||||
|
|
||||||
|
this.themeService.updateColorPalette({
|
||||||
|
...this.customConfig.theme,
|
||||||
|
|
||||||
|
customization: this.buildNewCustomization(this.form.get('theme.customization').value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const value = this.formatCustomizationFieldForForm(field, this.themeService.getCSSConfigValue(field))
|
||||||
|
const control = this.getCustomizationControl(field)
|
||||||
|
|
||||||
|
control.patchValue(value, { emitEvent: false })
|
||||||
|
control.markAsDirty()
|
||||||
|
}
|
||||||
|
|
||||||
|
save () {
|
||||||
|
const formValues = this.form.value
|
||||||
|
formValues.theme.customization = this.buildNewCustomization(formValues.theme.customization)
|
||||||
|
|
||||||
|
this.adminConfigService.saveAndUpdateCurrent({
|
||||||
|
currentConfig: this.customConfig,
|
||||||
|
form: this.form,
|
||||||
|
formConfig: this.form.value,
|
||||||
|
success: $localize`Platform customization updated.`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCustomizationControl (field: ThemeCustomizationKey) {
|
||||||
|
return this.form.get('theme.customization').get(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDefaultCustomization () {
|
||||||
|
const config = this.customConfig.theme.customization
|
||||||
|
|
||||||
|
return objectKeysTyped(this.formFieldsObject).reduce((acc, field) => {
|
||||||
|
acc[field] = config[field]
|
||||||
|
? this.formatCustomizationFieldForForm(field, config[field])
|
||||||
|
: this.formatCustomizationFieldForForm(field, this.themeService.getCSSConfigValue(field))
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {} as Record<ThemeCustomizationKey, string>)
|
||||||
|
}
|
||||||
|
|
||||||
|
isCustomizationFieldNumber (field: ThemeCustomizationKey) {
|
||||||
|
return this.isNumber(this.getCustomizationControl(field).value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private isNumber (value: string | number) {
|
||||||
|
return typeof value === 'number' || /^\d+$/.test(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private formatCustomizationFieldForForm (field: ThemeCustomizationKey, value: string) {
|
||||||
|
if (this.formFieldsObject[field].type === 'pixels') {
|
||||||
|
return this.formatPixelsForForm(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.formFieldsObject[field].type === 'color') {
|
||||||
|
return this.formatColorForForm(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatPixelsForForm (value: string) {
|
||||||
|
if (typeof value === 'number') return value + ''
|
||||||
|
if (typeof value !== 'string') return null
|
||||||
|
|
||||||
|
const result = parseInt(value.replace(/px$/, ''))
|
||||||
|
|
||||||
|
if (isNaN(result)) return null
|
||||||
|
|
||||||
|
return result + ''
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatColorForForm (value: string) {
|
||||||
|
if (!value) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
return formatHEX(parse(value))
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`Error parsing color value "${value}"`, err)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private buildNewCustomization (formValues: any) {
|
||||||
|
return objectKeysTyped(this.customConfig.theme.customization).reduce(
|
||||||
|
(acc: ColorPaletteThemeConfig['customization'], field) => {
|
||||||
|
acc[field] = this.formatCustomizationFieldForTheme(field, formValues[field])
|
||||||
|
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as ColorPaletteThemeConfig['customization']
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatCustomizationFieldForTheme (field: ThemeCustomizationKey, value: string) {
|
||||||
|
if (this.customizationResetFields.has(field)) return null
|
||||||
|
|
||||||
|
if (this.formFieldsObject[field].type === 'pixels' && this.isNumber(value)) {
|
||||||
|
value = value + 'px'
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +1,13 @@
|
||||||
<ng-container [formGroup]="form()">
|
<my-admin-save-bar i18n-title title="General configuration" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||||
<div class="pt-two-cols mt-5"> <!-- appearance grid -->
|
|
||||||
<div class="title-col">
|
|
||||||
<h2 i18n>APPEARANCE</h2>
|
|
||||||
|
|
||||||
<div i18n class="inner-form-description">
|
<ng-container [formGroup]="form">
|
||||||
Use <a class="link-primary" routerLink="/admin/settings/plugins">plugins & themes</a> for more involved changes, or add slight <a class="link-primary" routerLink="/admin/settings/config/edit-custom" fragment="advanced-configuration">customizations</a>.
|
<div class="pt-two-cols">
|
||||||
</div>
|
<div class="title-col">
|
||||||
|
<h2 i18n>BEHAVIOR</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-col">
|
<div class="content-col">
|
||||||
|
|
||||||
<ng-container formGroupName="theme">
|
|
||||||
<div class="form-group">
|
|
||||||
<label i18n for="themeDefault">Theme</label>
|
|
||||||
|
|
||||||
<my-select-options formControlName="default" inputId="themeDefault" [items]="availableThemes"></my-select-options>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<div class="form-group" formGroupName="instance">
|
<div class="form-group" formGroupName="instance">
|
||||||
<label i18n id="instanceDefaultClientRouteLabel" for="instanceDefaultClientRoute">Landing page</label>
|
<label i18n id="instanceDefaultClientRouteLabel" for="instanceDefaultClientRoute">Landing page</label>
|
||||||
|
|
||||||
|
@ -30,7 +20,7 @@
|
||||||
[clearable]="false"
|
[clearable]="false"
|
||||||
></my-select-custom-value>
|
></my-select-custom-value>
|
||||||
|
|
||||||
<div *ngIf="formErrors().instance.defaultClientRoute" class="form-error" role="alert">{{ formErrors().instance.defaultClientRoute }}</div>
|
<div *ngIf="formErrors.instance.defaultClientRoute" class="form-error" role="alert">{{ formErrors.instance.defaultClientRoute }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" formGroupName="trending">
|
<div class="form-group" formGroupName="trending">
|
||||||
|
@ -47,24 +37,13 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors().trending.videos.algorithms.default" class="form-error" role="alert">{{ formErrors().trending.videos.algorithms.default }}</div>
|
<div *ngIf="formErrors.trending.videos.algorithms.default" class="form-error" role="alert">{{ formErrors.trending.videos.algorithms.default }}</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container formGroupName="client">
|
<ng-container formGroupName="client">
|
||||||
|
|
||||||
<ng-container formGroupName="videos">
|
|
||||||
<ng-container formGroupName="miniature">
|
|
||||||
<div class="form-group">
|
|
||||||
<my-peertube-checkbox
|
|
||||||
inputName="clientVideosMiniaturePreferAuthorDisplayName" formControlName="preferAuthorDisplayName"
|
|
||||||
i18n-labelText labelText="Prefer author display name in video miniature"
|
|
||||||
></my-peertube-checkbox>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container formGroupName="menu">
|
<ng-container formGroupName="menu">
|
||||||
<ng-container formGroupName="login">
|
<ng-container formGroupName="login">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -73,8 +52,11 @@
|
||||||
i18n-labelText labelText="Redirect users on single external auth when users click on the login button in menu"
|
i18n-labelText labelText="Redirect users on single external auth when users click on the login button in menu"
|
||||||
>
|
>
|
||||||
<ng-container ngProjectAs="description">
|
<ng-container ngProjectAs="description">
|
||||||
<span *ngIf="countExternalAuth() === 0" i18n>⚠️ You don't have any external auth plugin enabled.</span>
|
@if (countExternalAuth() === 0) {
|
||||||
<span *ngIf="countExternalAuth() > 1" i18n>⚠️ You have multiple external auth plugins enabled.</span>
|
<span *ngIf="" i18n>⚠️ You don't have any external auth plugin enabled.</span>
|
||||||
|
} @else if (countExternalAuth() > 1) {
|
||||||
|
<span i18n>⚠️ You have multiple external auth plugins enabled.</span>
|
||||||
|
}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
@ -85,11 +67,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4"> <!-- broadcast grid -->
|
<div class="pt-two-cols mt-4">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>BROADCAST MESSAGE</h2>
|
<h2 i18n>BROADCAST MESSAGE</h2>
|
||||||
<div i18n class="inner-form-description">
|
<div i18n class="inner-form-description">
|
||||||
Display a message on your instance
|
Display a message on your platform
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -122,7 +104,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors().broadcastMessage.level" class="form-error" role="alert">{{ formErrors().broadcastMessage.level }}</div>
|
<div *ngIf="formErrors.broadcastMessage.level" class="form-error" role="alert">{{ formErrors.broadcastMessage.level }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -130,10 +112,10 @@
|
||||||
|
|
||||||
<my-markdown-textarea
|
<my-markdown-textarea
|
||||||
inputId="broadcastMessageMessage" formControlName="message"
|
inputId="broadcastMessageMessage" formControlName="message"
|
||||||
[formError]="formErrors()['broadcastMessage.message']" markdownType="to-unsafe-html"
|
[formError]="formErrors.broadcastMessage.message" markdownType="to-unsafe-html"
|
||||||
></my-markdown-textarea>
|
></my-markdown-textarea>
|
||||||
|
|
||||||
<div *ngIf="formErrors().broadcastMessage.message" class="form-error" role="alert">{{ formErrors().broadcastMessage.message }}</div>
|
<div *ngIf="formErrors.broadcastMessage.message" class="form-error" role="alert">{{ formErrors.broadcastMessage.message }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -144,9 +126,6 @@
|
||||||
<div class="pt-two-cols mt-4"> <!-- new users grid -->
|
<div class="pt-two-cols mt-4"> <!-- new users grid -->
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>NEW USERS</h2>
|
<h2 i18n>NEW USERS</h2>
|
||||||
<div i18n class="inner-form-description">
|
|
||||||
Manage <a class="link-primary" routerLink="/admin/overview/users">users</a> to set their quota individually.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-col">
|
<div class="content-col">
|
||||||
|
@ -160,7 +139,7 @@
|
||||||
<ng-container ngProjectAs="description">
|
<ng-container ngProjectAs="description">
|
||||||
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
|
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
|
||||||
|
|
||||||
<my-alert type="primary" class="alert-signup" *ngIf="signupAlertMessage">{{ signupAlertMessage }}</my-alert>
|
<my-alert type="primary" class="d-block mt-2" *ngIf="signupAlertMessage">{{ signupAlertMessage }}</my-alert>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container ngProjectAs="extra">
|
<ng-container ngProjectAs="extra">
|
||||||
|
@ -180,17 +159,17 @@
|
||||||
|
|
||||||
<div [ngClass]="getDisabledSignupClass()">
|
<div [ngClass]="getDisabledSignupClass()">
|
||||||
<label i18n for="signupLimit">Signup limit</label>
|
<label i18n for="signupLimit">Signup limit</label>
|
||||||
<span i18n class="small muted ms-1">When the total number of users in your instance reaches this limit, registrations are disabled. -1 == unlimited</span>
|
<span i18n class="small muted ms-1">When the total number of users in your platform reaches this limit, registrations are disabled. -1 == unlimited</span>
|
||||||
|
|
||||||
<div class="number-with-unit">
|
<div class="number-with-unit">
|
||||||
<input
|
<input
|
||||||
type="number" min="-1" id="signupLimit" class="form-control"
|
type="number" min="-1" id="signupLimit" class="form-control"
|
||||||
formControlName="limit" [ngClass]="{ 'input-error': formErrors()['signup.limit'] }"
|
formControlName="limit" [ngClass]="{ 'input-error': formErrors.signup.limit }"
|
||||||
>
|
>
|
||||||
<span i18n>{form().value['signup']['limit'], plural, =1 {user} other {users}}</span>
|
<span i18n>{form.value.signup.limit, plural, =1 {user} other {users}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors().signup.limit" class="form-error" role="alert">{{ formErrors().signup.limit }}</div>
|
<div *ngIf="formErrors.signup.limit" class="form-error" role="alert">{{ formErrors.signup.limit }}</div>
|
||||||
|
|
||||||
<small i18n *ngIf="hasUnlimitedSignup()" class="muted small">Signup won't be limited to a fixed number of users.</small>
|
<small i18n *ngIf="hasUnlimitedSignup()" class="muted small">Signup won't be limited to a fixed number of users.</small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -201,12 +180,12 @@
|
||||||
<div class="number-with-unit">
|
<div class="number-with-unit">
|
||||||
<input
|
<input
|
||||||
type="number" min="1" id="signupMinimumAge" class="form-control"
|
type="number" min="1" id="signupMinimumAge" class="form-control"
|
||||||
formControlName="minimumAge" [ngClass]="{ 'input-error': formErrors()['signup.minimumAge'] }"
|
formControlName="minimumAge" [ngClass]="{ 'input-error': formErrors.signup.minimumAge }"
|
||||||
>
|
>
|
||||||
<span i18n>{form().value['signup']['minimumAge'], plural, =1 {year old} other {years old}}</span>
|
<span i18n>{form.value.signup.minimumAge, plural, =1 {year old} other {years old}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors().signup.minimumAge" class="form-error" role="alert">{{ formErrors().signup.minimumAge }}</div>
|
<div *ngIf="formErrors.signup.minimumAge" class="form-error" role="alert">{{ formErrors.signup.minimumAge }}</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
|
@ -215,7 +194,7 @@
|
||||||
|
|
||||||
<ng-container formGroupName="user">
|
<ng-container formGroupName="user">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n id="userVideoQuotaLabel" for="userVideoQuota">Default video quota per user</label>
|
<label i18n id="userVideoQuotaLabel" for="userVideoQuota">Default video quota for a new user</label>
|
||||||
|
|
||||||
<my-select-custom-value
|
<my-select-custom-value
|
||||||
labelId="userVideoQuotaLabel"
|
labelId="userVideoQuotaLabel"
|
||||||
|
@ -228,11 +207,11 @@
|
||||||
|
|
||||||
<my-user-real-quota-info class="mt-2 d-block small muted" [videoQuota]="getUserVideoQuota()"></my-user-real-quota-info>
|
<my-user-real-quota-info class="mt-2 d-block small muted" [videoQuota]="getUserVideoQuota()"></my-user-real-quota-info>
|
||||||
|
|
||||||
<div *ngIf="formErrors().user.videoQuota" class="form-error" role="alert">{{ formErrors().user.videoQuota }}</div>
|
<div *ngIf="formErrors.user.videoQuota" class="form-error" role="alert">{{ formErrors.user.videoQuota }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n id="userVideoQuotaDaily" for="userVideoQuotaDaily">Default daily upload limit per user</label>
|
<label i18n id="userVideoQuotaDaily" for="userVideoQuotaDaily">Default daily upload limit for a new user</label>
|
||||||
|
|
||||||
<my-select-custom-value
|
<my-select-custom-value
|
||||||
labelId="userVideoQuotaDailyLabel"
|
labelId="userVideoQuotaDailyLabel"
|
||||||
|
@ -243,14 +222,14 @@
|
||||||
[clearable]="false"
|
[clearable]="false"
|
||||||
></my-select-custom-value>
|
></my-select-custom-value>
|
||||||
|
|
||||||
<div *ngIf="formErrors().user.videoQuotaDaily" class="form-error" role="alert">{{ formErrors().user.videoQuotaDaily }}</div>
|
<div *ngIf="formErrors.user.videoQuotaDaily" class="form-error" role="alert">{{ formErrors.user.videoQuotaDaily }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<ng-container formGroupName="history">
|
<ng-container formGroupName="history">
|
||||||
<ng-container formGroupName="videos">
|
<ng-container formGroupName="videos">
|
||||||
<my-peertube-checkbox
|
<my-peertube-checkbox
|
||||||
inputName="videosHistoryEnabled" formControlName="enabled"
|
inputName="videosHistoryEnabled" formControlName="enabled"
|
||||||
i18n-labelText labelText="Automatically enable video history for new users"
|
i18n-labelText labelText="Automatically enable video history for a new user"
|
||||||
>
|
>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -261,9 +240,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4"> <!-- videos grid -->
|
<div class="pt-two-cols mt-4">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>VIDEOS</h2>
|
<h2 i18n>VIDEO IMPORTS</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-col">
|
<div class="content-col">
|
||||||
|
@ -281,7 +260,7 @@
|
||||||
<span i18n>jobs in parallel</span>
|
<span i18n>jobs in parallel</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors().import.concurrency" class="form-error" role="alert">{{ formErrors().import.concurrency }}</div>
|
<div *ngIf="formErrors.import.videos.concurrency" class="form-error" role="alert">{{ formErrors.import.videos.concurrency }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" formGroupName="http">
|
<div class="form-group" formGroupName="http">
|
||||||
|
@ -328,16 +307,25 @@
|
||||||
<div class="number-with-unit">
|
<div class="number-with-unit">
|
||||||
<input
|
<input
|
||||||
type="number" min="1" id="videoChannelSynchronizationMaxPerUser" class="form-control"
|
type="number" min="1" id="videoChannelSynchronizationMaxPerUser" class="form-control"
|
||||||
formControlName="maxPerUser" [ngClass]="{ 'input-error': formErrors()['import']['videoChannelSynchronization']['maxPerUser'] }"
|
formControlName="maxPerUser" [ngClass]="{ 'input-error': formErrors['import']['videoChannelSynchronization']['maxPerUser'] }"
|
||||||
>
|
>
|
||||||
<span i18n>{form().value['import']['videoChannelSynchronization']['maxPerUser'], plural, =1 {sync} other {syncs}}</span>
|
<span i18n>{form.value.import.videoChannelSynchronization.maxPerUser, plural, =1 {sync} other {syncs}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors().import.videoChannelSynchronization.maxPerUser" class="form-error" role="alert">{{ formErrors().import.videoChannelSynchronization.maxPerUser }}</div>
|
<div *ngIf="formErrors.import.videoChannelSynchronization.maxPerUser" class="form-error" role="alert">{{ formErrors.import.videoChannelSynchronization.maxPerUser }}</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-two-cols mt-4">
|
||||||
|
<div class="title-col">
|
||||||
|
<h2 i18n>VIDEOS</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-col">
|
||||||
|
|
||||||
<ng-container formGroupName="autoBlacklist">
|
<ng-container formGroupName="autoBlacklist">
|
||||||
<ng-container formGroupName="videos">
|
<ng-container formGroupName="videos">
|
||||||
|
@ -414,7 +402,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4"> <!-- video channels grid -->
|
<div class="pt-two-cols mt-4">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>VIDEO CHANNELS</h2>
|
<h2 i18n>VIDEO CHANNELS</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -426,17 +414,17 @@
|
||||||
<div class="number-with-unit">
|
<div class="number-with-unit">
|
||||||
<input
|
<input
|
||||||
type="number" min="1" id="videoChannelsMaxPerUser" class="form-control"
|
type="number" min="1" id="videoChannelsMaxPerUser" class="form-control"
|
||||||
formControlName="maxPerUser" [ngClass]="{ 'input-error': formErrors()['videoChannels.maxPerUser'] }"
|
formControlName="maxPerUser" [ngClass]="{ 'input-error': formErrors.videoChannels.maxPerUser }"
|
||||||
>
|
>
|
||||||
<span i18n>{form().value['videoChannels']['maxPerUser'], plural, =1 {channel} other {channels}}</span>
|
<span i18n>{form.value.videoChannels.maxPerUser, plural, =1 {channel} other {channels}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors().videoChannels.maxPerUser" class="form-error" role="alert">{{ formErrors().videoChannels.maxPerUser }}</div>
|
<div *ngIf="formErrors.videoChannels.maxPerUser" class="form-error" role="alert">{{ formErrors.videoChannels.maxPerUser }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4"> <!-- search grid -->
|
<div class="pt-two-cols mt-4">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>SEARCH</h2>
|
<h2 i18n>SEARCH</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -452,7 +440,7 @@
|
||||||
i18n-labelText labelText="Allow users to do remote URI/handle search"
|
i18n-labelText labelText="Allow users to do remote URI/handle search"
|
||||||
>
|
>
|
||||||
<ng-container ngProjectAs="description">
|
<ng-container ngProjectAs="description">
|
||||||
<span i18n>Allow <strong>your users</strong> to look up remote videos/actors that may not be federated with your instance</span>
|
<span i18n>Allow <strong>your users</strong> to look up remote videos/actors that may not be federated with your platform</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
@ -463,7 +451,7 @@
|
||||||
i18n-labelText labelText="Allow anonymous to do remote URI/handle search"
|
i18n-labelText labelText="Allow anonymous to do remote URI/handle search"
|
||||||
>
|
>
|
||||||
<ng-container ngProjectAs="description">
|
<ng-container ngProjectAs="description">
|
||||||
<span i18n>Allow <strong>anonymous users</strong> to look up remote videos/actors that may not be federated with your instance</span>
|
<span i18n>Allow <strong>anonymous users</strong> to look up remote videos/actors that may not be federated with your platform</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
@ -477,23 +465,23 @@
|
||||||
i18n-labelText labelText="Enable global search"
|
i18n-labelText labelText="Enable global search"
|
||||||
>
|
>
|
||||||
<ng-container ngProjectAs="description">
|
<ng-container ngProjectAs="description">
|
||||||
<div i18n>⚠️ This functionality depends heavily on the moderation of instances followed by the search index you select.</div>
|
<div i18n>⚠️ This functionality depends heavily on the moderation of platforms followed by the search index you select.</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container ngProjectAs="extra">
|
<ng-container ngProjectAs="extra">
|
||||||
<div [ngClass]="getDisabledSearchIndexClass()">
|
<div [ngClass]="getDisabledSearchIndexClass()">
|
||||||
<label i18n for="searchIndexUrl">Search index URL</label>
|
<label i18n for="searchIndexUrl">Search index URL</label>
|
||||||
|
|
||||||
<div i18n class="label-small-info">
|
<div i18n class="form-group-description">
|
||||||
Use your <a class="link-primary" target="_blank" href="https://framagit.org/framasoft/peertube/search-index">your own search index</a> or choose the official one, <a class="link-primary" target="_blank" href="https://sepiasearch.org">https://sepiasearch.org</a>, that is not moderated.
|
Use your <a class="link-primary" target="_blank" href="https://framagit.org/framasoft/peertube/search-index">your own search index</a> or choose the official one, <a class="link-primary" target="_blank" href="https://sepiasearch.org">https://sepiasearch.org</a>, that is not moderated.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text" id="searchIndexUrl" class="form-control"
|
type="text" id="searchIndexUrl" class="form-control"
|
||||||
formControlName="url" [ngClass]="{ 'input-error': formErrors()['search.searchIndex.url'] }"
|
formControlName="url" [ngClass]="{ 'input-error': formErrors.search.searchIndex.url }"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div *ngIf="formErrors().search.searchIndex.url" class="form-error" role="alert">{{ formErrors().search.searchIndex.url }}</div>
|
<div *ngIf="formErrors.search.searchIndex.url" class="form-error" role="alert">{{ formErrors.search.searchIndex.url }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
|
@ -509,7 +497,7 @@
|
||||||
i18n-labelText labelText="Search bar uses the global search index by default"
|
i18n-labelText labelText="Search bar uses the global search index by default"
|
||||||
>
|
>
|
||||||
<ng-container ngProjectAs="description">
|
<ng-container ngProjectAs="description">
|
||||||
<span i18n>Otherwise the local search stays used by default</span>
|
<span i18n>Otherwise, the local search will be used by default</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
@ -525,7 +513,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4"> <!-- import/export grid -->
|
<div class="pt-two-cols mt-4">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>USER IMPORT/EXPORT</h2>
|
<h2 i18n>USER IMPORT/EXPORT</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -577,7 +565,7 @@
|
||||||
[clearable]="false"
|
[clearable]="false"
|
||||||
></my-select-custom-value>
|
></my-select-custom-value>
|
||||||
|
|
||||||
<div *ngIf="formErrors().export.users.maxUserVideoQuota" class="form-error" role="alert">{{ formErrors().export.users.maxUserVideoQuota }}</div>
|
<div *ngIf="formErrors.export.users.maxUserVideoQuota" class="form-error" role="alert">{{ formErrors.export.users.maxUserVideoQuota }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" [ngClass]="getDisabledExportUsersClass()">
|
<div class="form-group" [ngClass]="getDisabledExportUsersClass()">
|
||||||
|
@ -587,7 +575,7 @@
|
||||||
|
|
||||||
<div i18n class="mt-1 small muted">The archive file is deleted after this period.</div>
|
<div i18n class="mt-1 small muted">The archive file is deleted after this period.</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors().export.users.exportExpiration" class="form-error" role="alert">{{ formErrors().export.users.exportExpiration }}</div>
|
<div *ngIf="formErrors.export.users.exportExpiration" class="form-error" role="alert">{{ formErrors.export.users.exportExpiration }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -599,11 +587,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4"> <!-- federation grid -->
|
<div class="pt-two-cols mt-4">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>FEDERATION</h2>
|
<h2 i18n>FEDERATION</h2>
|
||||||
<div i18n class="inner-form-description">
|
<div i18n class="inner-form-description">
|
||||||
Manage <a class="link-primary" routerLink="/admin/settings/follows">relations</a> with other instances.
|
Manage <a class="link-primary" routerLink="/admin/settings/follows">relations</a> with other platforms.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -615,14 +603,14 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<my-peertube-checkbox
|
<my-peertube-checkbox
|
||||||
inputName="followersInstanceEnabled" formControlName="enabled"
|
inputName="followersInstanceEnabled" formControlName="enabled"
|
||||||
i18n-labelText labelText="Other instances can follow yours"
|
i18n-labelText labelText="Remote actors can follow your platform"
|
||||||
></my-peertube-checkbox>
|
></my-peertube-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<my-peertube-checkbox
|
<my-peertube-checkbox
|
||||||
inputName="followersInstanceManualApproval" formControlName="manualApproval"
|
inputName="followersInstanceManualApproval" formControlName="manualApproval"
|
||||||
i18n-labelText labelText="Manually approve new instance followers"
|
i18n-labelText labelText="Manually approve new followers that follow your platform"
|
||||||
></my-peertube-checkbox>
|
></my-peertube-checkbox>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -635,7 +623,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<my-peertube-checkbox
|
<my-peertube-checkbox
|
||||||
inputName="followingsInstanceAutoFollowBackEnabled" formControlName="enabled"
|
inputName="followingsInstanceAutoFollowBackEnabled" formControlName="enabled"
|
||||||
i18n-labelText labelText="Automatically follow back instances"
|
i18n-labelText labelText="Automatically follow back followers that follow your platform"
|
||||||
>
|
>
|
||||||
<ng-container ngProjectAs="description">
|
<ng-container ngProjectAs="description">
|
||||||
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
|
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
|
||||||
|
@ -648,7 +636,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<my-peertube-checkbox
|
<my-peertube-checkbox
|
||||||
inputName="followingsInstanceAutoFollowIndexEnabled" formControlName="enabled"
|
inputName="followingsInstanceAutoFollowIndexEnabled" formControlName="enabled"
|
||||||
i18n-labelText labelText="Automatically follow instances of a public index"
|
i18n-labelText labelText="Automatically follow platforms of a public index"
|
||||||
>
|
>
|
||||||
<ng-container ngProjectAs="description">
|
<ng-container ngProjectAs="description">
|
||||||
<div i18n>⚠️ This functionality requires a lot of attention and extra moderation.</div>
|
<div i18n>⚠️ This functionality requires a lot of attention and extra moderation.</div>
|
||||||
|
@ -663,9 +651,9 @@
|
||||||
<label i18n for="followingsInstanceAutoFollowIndexUrl">Index URL</label>
|
<label i18n for="followingsInstanceAutoFollowIndexUrl">Index URL</label>
|
||||||
<input
|
<input
|
||||||
type="text" id="followingsInstanceAutoFollowIndexUrl" class="form-control"
|
type="text" id="followingsInstanceAutoFollowIndexUrl" class="form-control"
|
||||||
formControlName="indexUrl" [ngClass]="{ 'input-error': formErrors()['followings.instance.autoFollowIndex.indexUrl'] }"
|
formControlName="indexUrl" [ngClass]="{ 'input-error': formErrors.followings.instance.autoFollowIndex.indexUrl }"
|
||||||
>
|
>
|
||||||
<div *ngIf="formErrors().followings.instance.autoFollowIndex.indexUrl" class="form-error" role="alert">{{ formErrors().followings.instance.autoFollowIndex.indexUrl }}</div>
|
<div *ngIf="formErrors.followings.instance.autoFollowIndex.indexUrl" class="form-error" role="alert">{{ formErrors.followings.instance.autoFollowIndex.indexUrl }}</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
|
@ -678,68 +666,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4"> <!-- administrators grid -->
|
|
||||||
<div class="title-col">
|
|
||||||
<h2 i18n>ADMINISTRATORS</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content-col">
|
|
||||||
|
|
||||||
<div class="form-group" formGroupName="admin">
|
|
||||||
<label i18n for="adminEmail">Admin email</label>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="text" id="adminEmail" class="form-control"
|
|
||||||
formControlName="email" [ngClass]="{ 'input-error': formErrors()['admin.email'] }"
|
|
||||||
>
|
|
||||||
|
|
||||||
<div *ngIf="formErrors().admin.email" class="form-error" role="alert">{{ formErrors().admin.email }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" formGroupName="contactForm">
|
|
||||||
<my-peertube-checkbox
|
|
||||||
inputName="enableContactForm" formControlName="enabled"
|
|
||||||
i18n-labelText labelText="Enable contact form"
|
|
||||||
></my-peertube-checkbox>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4"> <!-- Twitter grid -->
|
|
||||||
<div class="title-col">
|
|
||||||
<h2 i18n>TWITTER/X</h2>
|
|
||||||
|
|
||||||
<div i18n class="inner-form-description">
|
|
||||||
Extra configuration required by Twitter/X. All other social media (Facebook, Mastodon, etc.) are supported out of the box.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content-col">
|
|
||||||
|
|
||||||
<ng-container formGroupName="services">
|
|
||||||
<ng-container formGroupName="twitter">
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="servicesTwitterUsername" i18n>Your Twitter/X username</label>
|
|
||||||
|
|
||||||
<div class="label-small-info">
|
|
||||||
<p i18n class="mb-0">Indicates the Twitter/X account for the website or platform where the content was published.</p>
|
|
||||||
|
|
||||||
<p i18n>This is just an extra information injected in PeerTube HTML that is required by Twitter/X. If you don't have a Twitter/X account, just leave the default value.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="text" id="servicesTwitterUsername" class="form-control"
|
|
||||||
formControlName="username" [ngClass]="{ 'input-error': formErrors()['services.twitter.username'] }"
|
|
||||||
>
|
|
||||||
|
|
||||||
<div *ngIf="formErrors().services.twitter.username" class="form-error" role="alert">{{ formErrors().services.twitter.username }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
</ng-container>
|
|
@ -0,0 +1,517 @@
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, OnInit, inject } from '@angular/core'
|
||||||
|
import { FormArray, FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { ActivatedRoute, RouterLink } from '@angular/router'
|
||||||
|
import { getVideoQuotaDailyOptions, getVideoQuotaOptions } from '@app/+admin/shared/user-quota-options'
|
||||||
|
import { CanComponentDeactivate, ServerService } from '@app/core'
|
||||||
|
import { URL_VALIDATOR } from '@app/shared/form-validators/common-validators'
|
||||||
|
import {
|
||||||
|
CONCURRENCY_VALIDATOR,
|
||||||
|
EXPORT_EXPIRATION_VALIDATOR,
|
||||||
|
EXPORT_MAX_USER_VIDEO_QUOTA_VALIDATOR,
|
||||||
|
MAX_SYNC_PER_USER,
|
||||||
|
MAX_VIDEO_CHANNELS_PER_USER_VALIDATOR,
|
||||||
|
SIGNUP_LIMIT_VALIDATOR,
|
||||||
|
SIGNUP_MINIMUM_AGE_VALIDATOR
|
||||||
|
} from '@app/shared/form-validators/custom-config-validators'
|
||||||
|
import {
|
||||||
|
BuildFormArgumentTyped,
|
||||||
|
FormDefaultTyped,
|
||||||
|
FormReactiveErrorsTyped,
|
||||||
|
FormReactiveMessagesTyped
|
||||||
|
} from '@app/shared/form-validators/form-validator.model'
|
||||||
|
import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
||||||
|
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
|
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||||
|
import { BroadcastMessageLevel, CustomConfig } from '@peertube/peertube-models'
|
||||||
|
import { pairwise } from 'rxjs/operators'
|
||||||
|
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||||
|
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
||||||
|
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||||
|
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||||
|
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||||
|
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||||
|
import { UserRealQuotaInfoComponent } from '../../shared/user-real-quota-info.component'
|
||||||
|
import { AdminConfigService } from '../shared/admin-config.service'
|
||||||
|
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||||
|
|
||||||
|
type Form = {
|
||||||
|
instance: FormGroup<{
|
||||||
|
defaultClientRoute: FormControl<string>
|
||||||
|
}>
|
||||||
|
|
||||||
|
client: FormGroup<{
|
||||||
|
menu: FormGroup<{
|
||||||
|
login: FormGroup<{
|
||||||
|
redirectOnSingleExternalAuth: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
|
||||||
|
signup: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
limit: FormControl<number>
|
||||||
|
requiresApproval: FormControl<boolean>
|
||||||
|
requiresEmailVerification: FormControl<boolean>
|
||||||
|
minimumAge: FormControl<number>
|
||||||
|
}>
|
||||||
|
|
||||||
|
import: FormGroup<{
|
||||||
|
videos: FormGroup<{
|
||||||
|
concurrency: FormControl<number>
|
||||||
|
|
||||||
|
http: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
|
||||||
|
torrent: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
videoChannelSynchronization: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
maxPerUser: FormControl<number>
|
||||||
|
}>
|
||||||
|
users: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
|
||||||
|
export: FormGroup<{
|
||||||
|
users: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
maxUserVideoQuota: FormControl<number>
|
||||||
|
exportExpiration: FormControl<number>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
|
||||||
|
trending: FormGroup<{
|
||||||
|
videos: FormGroup<{
|
||||||
|
algorithms: FormGroup<{
|
||||||
|
enabled: FormArray<FormControl<string>>
|
||||||
|
default: FormControl<string>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
|
||||||
|
user: FormGroup<{
|
||||||
|
history: FormGroup<{
|
||||||
|
videos: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
videoQuota: FormControl<number>
|
||||||
|
videoQuotaDaily: FormControl<number>
|
||||||
|
}>
|
||||||
|
|
||||||
|
videoChannels: FormGroup<{
|
||||||
|
maxPerUser: FormControl<number>
|
||||||
|
}>
|
||||||
|
|
||||||
|
videoTranscription: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
remoteRunners: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
|
||||||
|
videoFile: FormGroup<{
|
||||||
|
update: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
|
||||||
|
autoBlacklist: FormGroup<{
|
||||||
|
videos: FormGroup<{
|
||||||
|
ofUsers: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
|
||||||
|
followers: FormGroup<{
|
||||||
|
instance: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
manualApproval: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
|
||||||
|
followings: FormGroup<{
|
||||||
|
instance: FormGroup<{
|
||||||
|
autoFollowBack: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
autoFollowIndex: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
indexUrl: FormControl<string>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
|
||||||
|
broadcastMessage: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
level: FormControl<BroadcastMessageLevel>
|
||||||
|
dismissable: FormControl<boolean>
|
||||||
|
message: FormControl<string>
|
||||||
|
}>
|
||||||
|
|
||||||
|
search: FormGroup<{
|
||||||
|
remoteUri: FormGroup<{
|
||||||
|
users: FormControl<boolean>
|
||||||
|
anonymous: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
searchIndex: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
url: FormControl<string>
|
||||||
|
disableLocalSearch: FormControl<boolean>
|
||||||
|
isDefaultSearch: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
|
||||||
|
storyboards: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-admin-config-general',
|
||||||
|
templateUrl: './admin-config-general.component.html',
|
||||||
|
styleUrls: [ './admin-config-common.scss' ],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterLink,
|
||||||
|
SelectCustomValueComponent,
|
||||||
|
PeertubeCheckboxComponent,
|
||||||
|
HelpComponent,
|
||||||
|
MarkdownTextareaComponent,
|
||||||
|
UserRealQuotaInfoComponent,
|
||||||
|
SelectOptionsComponent,
|
||||||
|
AlertComponent,
|
||||||
|
AdminSaveBarComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminConfigGeneralComponent implements OnInit, CanComponentDeactivate {
|
||||||
|
private server = inject(ServerService)
|
||||||
|
private route = inject(ActivatedRoute)
|
||||||
|
private formReactiveService = inject(FormReactiveService)
|
||||||
|
private adminConfigService = inject(AdminConfigService)
|
||||||
|
|
||||||
|
form: FormGroup<Form>
|
||||||
|
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||||
|
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||||
|
|
||||||
|
signupAlertMessage: string
|
||||||
|
defaultLandingPageOptions: SelectOptionsItem[] = []
|
||||||
|
|
||||||
|
exportExpirationOptions: SelectOptionsItem[] = []
|
||||||
|
exportMaxUserVideoQuotaOptions: SelectOptionsItem[] = []
|
||||||
|
|
||||||
|
private customConfig: CustomConfig
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||||
|
|
||||||
|
this.buildLandingPageOptions()
|
||||||
|
|
||||||
|
this.exportExpirationOptions = [
|
||||||
|
{ id: 1000 * 3600 * 24, label: $localize`1 day` },
|
||||||
|
{ id: 1000 * 3600 * 24 * 2, label: $localize`2 days` },
|
||||||
|
{ id: 1000 * 3600 * 24 * 7, label: $localize`7 days` },
|
||||||
|
{ id: 1000 * 3600 * 24 * 30, label: $localize`30 days` }
|
||||||
|
]
|
||||||
|
|
||||||
|
this.exportMaxUserVideoQuotaOptions = this.getVideoQuotaOptions().filter(o => (o.id as number) >= 1)
|
||||||
|
|
||||||
|
this.buildForm()
|
||||||
|
|
||||||
|
this.subscribeToSignupChanges()
|
||||||
|
this.subscribeToImportSyncChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildForm () {
|
||||||
|
const obj: BuildFormArgumentTyped<Form> = {
|
||||||
|
instance: {
|
||||||
|
defaultClientRoute: null
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
menu: {
|
||||||
|
login: {
|
||||||
|
redirectOnSingleExternalAuth: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
signup: {
|
||||||
|
enabled: null,
|
||||||
|
limit: SIGNUP_LIMIT_VALIDATOR,
|
||||||
|
requiresApproval: null,
|
||||||
|
requiresEmailVerification: null,
|
||||||
|
minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR
|
||||||
|
},
|
||||||
|
import: {
|
||||||
|
videos: {
|
||||||
|
concurrency: CONCURRENCY_VALIDATOR,
|
||||||
|
http: {
|
||||||
|
enabled: null
|
||||||
|
},
|
||||||
|
torrent: {
|
||||||
|
enabled: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
videoChannelSynchronization: {
|
||||||
|
enabled: null,
|
||||||
|
maxPerUser: MAX_SYNC_PER_USER
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
enabled: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
export: {
|
||||||
|
users: {
|
||||||
|
enabled: null,
|
||||||
|
maxUserVideoQuota: EXPORT_MAX_USER_VIDEO_QUOTA_VALIDATOR,
|
||||||
|
exportExpiration: EXPORT_EXPIRATION_VALIDATOR
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trending: {
|
||||||
|
videos: {
|
||||||
|
algorithms: {
|
||||||
|
enabled: null,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
history: {
|
||||||
|
videos: {
|
||||||
|
enabled: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
videoQuota: USER_VIDEO_QUOTA_VALIDATOR,
|
||||||
|
videoQuotaDaily: USER_VIDEO_QUOTA_DAILY_VALIDATOR
|
||||||
|
},
|
||||||
|
videoChannels: {
|
||||||
|
maxPerUser: MAX_VIDEO_CHANNELS_PER_USER_VALIDATOR
|
||||||
|
},
|
||||||
|
videoTranscription: {
|
||||||
|
enabled: null,
|
||||||
|
remoteRunners: {
|
||||||
|
enabled: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
videoFile: {
|
||||||
|
update: {
|
||||||
|
enabled: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
autoBlacklist: {
|
||||||
|
videos: {
|
||||||
|
ofUsers: {
|
||||||
|
enabled: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
followers: {
|
||||||
|
instance: {
|
||||||
|
enabled: null,
|
||||||
|
manualApproval: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
followings: {
|
||||||
|
instance: {
|
||||||
|
autoFollowBack: {
|
||||||
|
enabled: null
|
||||||
|
},
|
||||||
|
autoFollowIndex: {
|
||||||
|
enabled: null,
|
||||||
|
indexUrl: URL_VALIDATOR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
broadcastMessage: {
|
||||||
|
enabled: null,
|
||||||
|
level: null,
|
||||||
|
dismissable: null,
|
||||||
|
message: null
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
remoteUri: {
|
||||||
|
users: null,
|
||||||
|
anonymous: null
|
||||||
|
},
|
||||||
|
searchIndex: {
|
||||||
|
enabled: null,
|
||||||
|
url: URL_VALIDATOR,
|
||||||
|
disableLocalSearch: null,
|
||||||
|
isDefaultSearch: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
storyboards: {
|
||||||
|
enabled: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValues: FormDefaultTyped<Form> = this.customConfig
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
formErrors,
|
||||||
|
validationMessages
|
||||||
|
} = this.formReactiveService.buildForm<Form>(obj, defaultValues)
|
||||||
|
|
||||||
|
this.form = form
|
||||||
|
this.formErrors = formErrors
|
||||||
|
this.validationMessages = validationMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeactivate () {
|
||||||
|
return { canDeactivate: !this.form.dirty }
|
||||||
|
}
|
||||||
|
|
||||||
|
countExternalAuth () {
|
||||||
|
return this.server.getHTMLConfig().plugin.registeredExternalAuths.length
|
||||||
|
}
|
||||||
|
|
||||||
|
getVideoQuotaOptions () {
|
||||||
|
return getVideoQuotaOptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
getVideoQuotaDailyOptions () {
|
||||||
|
return getVideoQuotaDailyOptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
doesTrendingVideosAlgorithmsEnabledInclude (algorithm: string) {
|
||||||
|
const enabled = this.form.value.trending.videos.algorithms.enabled
|
||||||
|
if (!Array.isArray(enabled)) return false
|
||||||
|
|
||||||
|
return !!enabled.find((e: string) => e === algorithm)
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserVideoQuota () {
|
||||||
|
return this.form.value.user.videoQuota
|
||||||
|
}
|
||||||
|
|
||||||
|
isExportUsersEnabled () {
|
||||||
|
return this.form.value.export.users.enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisabledExportUsersClass () {
|
||||||
|
return { 'disabled-checkbox-extra': !this.isExportUsersEnabled() }
|
||||||
|
}
|
||||||
|
|
||||||
|
isSignupEnabled () {
|
||||||
|
return this.form.value.signup.enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisabledSignupClass () {
|
||||||
|
return { 'disabled-checkbox-extra': !this.isSignupEnabled() }
|
||||||
|
}
|
||||||
|
|
||||||
|
isImportVideosHttpEnabled (): boolean {
|
||||||
|
return this.form.value.import.videos.http.enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
importSynchronizationChecked () {
|
||||||
|
return this.isImportVideosHttpEnabled() && this.form.value.import.videoChannelSynchronization.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
hasUnlimitedSignup () {
|
||||||
|
return this.form.value.signup.limit === -1
|
||||||
|
}
|
||||||
|
|
||||||
|
isSearchIndexEnabled () {
|
||||||
|
return this.form.value.search.searchIndex.enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisabledSearchIndexClass () {
|
||||||
|
return { 'disabled-checkbox-extra': !this.isSearchIndexEnabled() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
isTranscriptionEnabled () {
|
||||||
|
return this.form.value.videoTranscription.enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
getTranscriptionRunnerDisabledClass () {
|
||||||
|
return { 'disabled-checkbox-extra': !this.isTranscriptionEnabled() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
isAutoFollowIndexEnabled () {
|
||||||
|
return this.form.value.followings.instance.autoFollowIndex.enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
buildLandingPageOptions () {
|
||||||
|
let links: { label: string, path: string }[] = []
|
||||||
|
|
||||||
|
if (this.server.getHTMLConfig().homepage.enabled) {
|
||||||
|
links.push({ label: $localize`Home`, path: '/home' })
|
||||||
|
}
|
||||||
|
|
||||||
|
links = links.concat([
|
||||||
|
{ label: $localize`Discover`, path: '/videos/overview' },
|
||||||
|
{ label: $localize`Browse all videos`, path: '/videos/browse' },
|
||||||
|
{ label: $localize`Browse local videos`, path: '/videos/browse?scope=local' }
|
||||||
|
])
|
||||||
|
|
||||||
|
this.defaultLandingPageOptions = links.map(o => ({
|
||||||
|
id: o.path,
|
||||||
|
label: o.label,
|
||||||
|
description: o.path
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
private subscribeToImportSyncChanges () {
|
||||||
|
const controls = this.form.controls
|
||||||
|
|
||||||
|
const importSyncControl = controls.import.controls.videoChannelSynchronization.controls.enabled
|
||||||
|
const importVideosHttpControl = controls.import.controls.videos.controls.http.controls.enabled
|
||||||
|
|
||||||
|
importVideosHttpControl.valueChanges
|
||||||
|
.subscribe(httpImportEnabled => {
|
||||||
|
importSyncControl.setValue(httpImportEnabled && importSyncControl.value)
|
||||||
|
|
||||||
|
if (httpImportEnabled) importSyncControl.enable()
|
||||||
|
else importSyncControl.disable()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private subscribeToSignupChanges () {
|
||||||
|
const signupControl = this.form.controls.signup.controls.enabled
|
||||||
|
|
||||||
|
signupControl.valueChanges
|
||||||
|
.pipe(pairwise())
|
||||||
|
.subscribe(([ oldValue, newValue ]) => {
|
||||||
|
if (oldValue === false && newValue === true) {
|
||||||
|
this.signupAlertMessage =
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
$localize`You enabled signup: we automatically enabled the "Block new videos automatically" checkbox of the "Videos" section just below.`
|
||||||
|
|
||||||
|
this.form.patchValue({
|
||||||
|
autoBlacklist: {
|
||||||
|
videos: {
|
||||||
|
ofUsers: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
signupControl.updateValueAndValidity()
|
||||||
|
}
|
||||||
|
|
||||||
|
save () {
|
||||||
|
this.adminConfigService.saveAndUpdateCurrent({
|
||||||
|
currentConfig: this.customConfig,
|
||||||
|
form: this.form,
|
||||||
|
formConfig: this.form.value,
|
||||||
|
success: $localize`Live configuration updated.`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
<my-admin-save-bar i18n-title title="Edit your homepage" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||||
|
|
||||||
|
<div class="homepage pt-two-cols" [formGroup]="form">
|
||||||
|
<div class="title-col">
|
||||||
|
<h2 i18n>HOMEPAGE</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-col">
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="homepageContent">Homepage content</label>
|
||||||
|
<div class="form-group-description">
|
||||||
|
<my-custom-markup-help></my-custom-markup-help>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<my-markdown-textarea
|
||||||
|
inputId="homepageContent"
|
||||||
|
formControlName="homepageContent"
|
||||||
|
[customMarkdownRenderer]="getCustomMarkdownRenderer()"
|
||||||
|
[debounceTime]="500"
|
||||||
|
[formError]="formErrors['homepageContent']"
|
||||||
|
dir="ltr"
|
||||||
|
monospace="true"
|
||||||
|
></my-markdown-textarea>
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.homepageContent" class="form-error" role="alert">{{ formErrors.homepageContent }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, inject, OnInit } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { ActivatedRoute } from '@angular/router'
|
||||||
|
import { CanComponentDeactivate, Notifier } from '@app/core'
|
||||||
|
import { BuildFormArgument } from '@app/shared/form-validators/form-validator.model'
|
||||||
|
import { CustomMarkupService } from '@app/shared/shared-custom-markup/custom-markup.service'
|
||||||
|
import { FormReactiveErrors, FormReactiveMessages, FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
|
import { CustomPageService } from '@app/shared/shared-main/custom-page/custom-page.service'
|
||||||
|
import { CustomMarkupHelpComponent } from '../../../shared/shared-custom-markup/custom-markup-help.component'
|
||||||
|
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
||||||
|
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||||
|
|
||||||
|
type Form = {
|
||||||
|
homepageContent: FormControl<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-admin-config-homepage',
|
||||||
|
templateUrl: './admin-config-homepage.component.html',
|
||||||
|
styleUrls: [ './admin-config-common.scss' ],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
CustomMarkupHelpComponent,
|
||||||
|
MarkdownTextareaComponent,
|
||||||
|
AdminSaveBarComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminConfigHomepageComponent implements OnInit, CanComponentDeactivate {
|
||||||
|
private formReactiveService = inject(FormReactiveService)
|
||||||
|
private notifier = inject(Notifier)
|
||||||
|
|
||||||
|
private route = inject(ActivatedRoute)
|
||||||
|
private customMarkup = inject(CustomMarkupService)
|
||||||
|
private customPage = inject(CustomPageService)
|
||||||
|
|
||||||
|
form: FormGroup<Form>
|
||||||
|
formErrors: FormReactiveErrors = {}
|
||||||
|
validationMessages: FormReactiveMessages = {}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.buildForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeactivate () {
|
||||||
|
return { canDeactivate: !this.form.dirty }
|
||||||
|
}
|
||||||
|
|
||||||
|
getCustomMarkdownRenderer () {
|
||||||
|
return this.customMarkup.getCustomMarkdownRenderer()
|
||||||
|
}
|
||||||
|
|
||||||
|
save () {
|
||||||
|
this.customPage.updateInstanceHomepage(this.form.value.homepageContent)
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.form.markAsPristine()
|
||||||
|
|
||||||
|
this.notifier.success($localize`Homepage updated.`)
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildForm () {
|
||||||
|
const obj: BuildFormArgument = {
|
||||||
|
homepageContent: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
formErrors,
|
||||||
|
validationMessages
|
||||||
|
} = this.formReactiveService.buildForm<Form>(obj, { homepageContent: this.route.snapshot.data['homepageContent'] })
|
||||||
|
|
||||||
|
this.form = form
|
||||||
|
this.formErrors = formErrors
|
||||||
|
this.validationMessages = validationMessages
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,47 @@
|
||||||
<ng-container [formGroup]="form()">
|
<my-admin-save-bar i18n-title title="Platform information" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||||
|
|
||||||
|
<ng-container [formGroup]="form">
|
||||||
|
|
||||||
|
<div class="pt-two-cols mt-4">
|
||||||
|
<div class="title-col">
|
||||||
|
<h2 i18n>ADMINISTRATORS</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-col">
|
||||||
|
|
||||||
|
<div class="form-group" formGroupName="admin">
|
||||||
|
<label i18n for="adminEmail">Admin email</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text" id="adminEmail" class="form-control"
|
||||||
|
formControlName="email" [ngClass]="{ 'input-error': formErrors.admin.email }"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.admin.email" class="form-error" role="alert">{{ formErrors.admin.email }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" formGroupName="contactForm">
|
||||||
|
<my-peertube-checkbox
|
||||||
|
inputName="enableContactForm" formControlName="enabled"
|
||||||
|
i18n-labelText labelText="Enable contact form"
|
||||||
|
></my-peertube-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ng-container formGroupName="instance">
|
<ng-container formGroupName="instance">
|
||||||
|
|
||||||
<div class="pt-two-cols mt-5"> <!-- instance grid -->
|
<div class="pt-two-cols">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>INSTANCE</h2>
|
<h2 i18n>PLATFORM</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-col">
|
<div class="content-col">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="avatarfile">Square icon</label>
|
<label i18n for="avatarfile">Square icon</label>
|
||||||
|
|
||||||
<div class="label-small-info">
|
<div class="form-group-description">
|
||||||
<p i18n class="mb-0">Square icon can be used on your custom homepage.</p>
|
<p i18n class="mb-0">Square icon can be used on your custom homepage.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -25,7 +55,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="bannerfile">Banner</label>
|
<label i18n for="bannerfile">Banner</label>
|
||||||
|
|
||||||
<div class="label-small-info">
|
<div class="form-group-description">
|
||||||
<p i18n class="mb-0">Banner is displayed in the about, login and registration pages and be used on your custom homepage.</p>
|
<p i18n class="mb-0">Banner is displayed in the about, login and registration pages and be used on your custom homepage.</p>
|
||||||
<p i18n>It can also be displayed on external websites to promote your instance, such as <a target="_blank" href="https://joinpeertube.org/instances">JoinPeerTube.org</a>.</p>
|
<p i18n>It can also be displayed on external websites to promote your instance, such as <a target="_blank" href="https://joinpeertube.org/instances">JoinPeerTube.org</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,10 +71,10 @@
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text" id="instanceName" class="form-control"
|
type="text" id="instanceName" class="form-control"
|
||||||
formControlName="name" [ngClass]="{ 'input-error': formErrors().instance.name }"
|
formControlName="name" [ngClass]="{ 'input-error': formErrors.instance.name }"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div *ngIf="formErrors().instance.name" class="form-error" role="alert">{{ formErrors().instance.name }}</div>
|
<div *ngIf="formErrors.instance.name" class="form-error" role="alert">{{ formErrors.instance.name }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -52,22 +82,22 @@
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
id="instanceShortDescription" formControlName="shortDescription" class="form-control small"
|
id="instanceShortDescription" formControlName="shortDescription" class="form-control small"
|
||||||
[ngClass]="{ 'input-error': formErrors()['instance.shortDescription'] }"
|
[ngClass]="{ 'input-error': formErrors.instance.shortDescription }"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<div *ngIf="formErrors().instance.shortDescription" class="form-error" role="alert">{{ formErrors().instance.shortDescription }}</div>
|
<div *ngIf="formErrors.instance.shortDescription" class="form-error" role="alert">{{ formErrors.instance.shortDescription }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="instanceDescription">Description</label>
|
<label i18n for="instanceDescription">Description</label>
|
||||||
<div class="label-small-info">
|
<div class="form-group-description">
|
||||||
<my-custom-markup-help supportRelMe="true"></my-custom-markup-help>
|
<my-custom-markup-help supportRelMe="true"></my-custom-markup-help>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<my-markdown-textarea
|
<my-markdown-textarea
|
||||||
inputId="instanceDescription" formControlName="description"
|
inputId="instanceDescription" formControlName="description"
|
||||||
[customMarkdownRenderer]="getCustomMarkdownRenderer()" [debounceTime]="500"
|
[customMarkdownRenderer]="getCustomMarkdownRenderer()" [debounceTime]="500"
|
||||||
[formError]="formErrors()['instance.description']"
|
[formError]="formErrors.instance.description"
|
||||||
></my-markdown-textarea>
|
></my-markdown-textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -77,7 +107,7 @@
|
||||||
<div>
|
<div>
|
||||||
<my-select-checkbox
|
<my-select-checkbox
|
||||||
inputId="instanceCategories"
|
inputId="instanceCategories"
|
||||||
formControlName="categories" [availableItems]="categoryItems()"
|
formControlName="categories" [availableItems]="categoryItems"
|
||||||
[selectableGroup]="false"
|
[selectableGroup]="false"
|
||||||
i18n-placeholder placeholder="Add a new category"
|
i18n-placeholder placeholder="Add a new category"
|
||||||
>
|
>
|
||||||
|
@ -91,7 +121,7 @@
|
||||||
<div>
|
<div>
|
||||||
<my-select-checkbox
|
<my-select-checkbox
|
||||||
inputId="instanceLanguages"
|
inputId="instanceLanguages"
|
||||||
formControlName="languages" [availableItems]="languageItems()"
|
formControlName="languages" [availableItems]="languageItems"
|
||||||
[selectableGroup]="false"
|
[selectableGroup]="false"
|
||||||
i18n-placeholder placeholder="Add a new language"
|
i18n-placeholder placeholder="Add a new language"
|
||||||
>
|
>
|
||||||
|
@ -101,20 +131,20 @@
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="instanceServerCountry">Server country</label>
|
<label i18n for="instanceServerCountry">Server country</label>
|
||||||
<div i18n class="label-small-info">PeerTube uses this setting to explain to your users which law they must follow in the "About" pages</div>
|
<div i18n class="form-group-description">PeerTube uses this setting to explain to your users which law they must follow in the "About" pages</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text" id="instanceServerCountry" class="form-control"
|
type="text" id="instanceServerCountry" class="form-control"
|
||||||
formControlName="serverCountry" [ngClass]="{ 'input-error': formErrors().instance.serverCountry }"
|
formControlName="serverCountry" [ngClass]="{ 'input-error': formErrors.instance.serverCountry }"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div *ngIf="formErrors().instance.serverCountry" class="form-error" role="alert">{{ formErrors().instance.serverCountry }}</div>
|
<div *ngIf="formErrors.instance.serverCountry" class="form-error" role="alert">{{ formErrors.instance.serverCountry }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4"> <!-- social grid -->
|
<div class="pt-two-cols mt-4">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>SOCIAL</h2>
|
<h2 i18n>SOCIAL</h2>
|
||||||
<div i18n class="inner-form-description">
|
<div i18n class="inner-form-description">
|
||||||
|
@ -126,25 +156,25 @@
|
||||||
|
|
||||||
<div class="form-group" formGroupName="support">
|
<div class="form-group" formGroupName="support">
|
||||||
<label i18n for="instanceSupportText">Support text</label><my-help helpType="markdownText"></my-help>
|
<label i18n for="instanceSupportText">Support text</label><my-help helpType="markdownText"></my-help>
|
||||||
<div i18n class="label-small-info">Explain to your users how to support your platform. If set, PeerTube will display a "Support" button in "About" instance pages</div>
|
<div i18n class="form-group-description">Explain to your users how to support your platform. If set, PeerTube will display a "Support" button in "About" instance pages</div>
|
||||||
|
|
||||||
<my-markdown-textarea
|
<my-markdown-textarea
|
||||||
inputId="instanceSupportText" formControlName="text" markdownType="enhanced"
|
inputId="instanceSupportText" formControlName="text" markdownType="enhanced"
|
||||||
[formError]="formErrors()['instance.support.text']"
|
[formError]="formErrors.instance.support.text"
|
||||||
></my-markdown-textarea>
|
></my-markdown-textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container formGroupName="social">
|
<ng-container formGroupName="social">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="instanceSocialExternalLink">External link</label>
|
<label i18n for="instanceSocialExternalLink">External link</label>
|
||||||
<div i18n class="label-small-info">Link to your main website</div>
|
<div i18n class="form-group-description">Link to your main website</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text" id="instanceSocialExternalLink" class="form-control"
|
type="text" id="instanceSocialExternalLink" class="form-control"
|
||||||
formControlName="externalLink" [ngClass]="{ 'input-error': formErrors().instance.social.externalLink }"
|
formControlName="externalLink" [ngClass]="{ 'input-error': formErrors.instance.social.externalLink }"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div *ngIf="formErrors().instance.social.externalLink" class="form-error" role="alert">{{ formErrors().instance.social.externalLink }}</div>
|
<div *ngIf="formErrors.instance.social.externalLink" class="form-error" role="alert">{{ formErrors.instance.social.externalLink }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -152,10 +182,10 @@
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text" id="instanceSocialMastodonLink" class="form-control"
|
type="text" id="instanceSocialMastodonLink" class="form-control"
|
||||||
formControlName="mastodonLink" [ngClass]="{ 'input-error': formErrors().instance.social.mastodonLink }"
|
formControlName="mastodonLink" [ngClass]="{ 'input-error': formErrors.instance.social.mastodonLink }"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div *ngIf="formErrors().instance.social.mastodonLink" class="form-error" role="alert">{{ formErrors().instance.social.mastodonLink }}</div>
|
<div *ngIf="formErrors.instance.social.mastodonLink" class="form-error" role="alert">{{ formErrors.instance.social.mastodonLink }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -163,10 +193,10 @@
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text" id="instanceSocialBlueskyLink" class="form-control"
|
type="text" id="instanceSocialBlueskyLink" class="form-control"
|
||||||
formControlName="blueskyLink" [ngClass]="{ 'input-error': formErrors().instance.social.blueskyLink }"
|
formControlName="blueskyLink" [ngClass]="{ 'input-error': formErrors.instance.social.blueskyLink }"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div *ngIf="formErrors().instance.social.blueskyLink" class="form-error" role="alert">{{ formErrors().instance.social.blueskyLink }}</div>
|
<div *ngIf="formErrors.instance.social.blueskyLink" class="form-error" role="alert">{{ formErrors.instance.social.blueskyLink }}</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -174,7 +204,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4"> <!-- moderation grid -->
|
<div class="pt-two-cols mt-4">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>MODERATION & SENSITIVE CONTENT</h2>
|
<h2 i18n>MODERATION & SENSITIVE CONTENT</h2>
|
||||||
<div i18n class="inner-form-description">
|
<div i18n class="inner-form-description">
|
||||||
|
@ -205,7 +235,7 @@
|
||||||
formControlName="defaultNSFWPolicy"
|
formControlName="defaultNSFWPolicy"
|
||||||
></my-select-radio>
|
></my-select-radio>
|
||||||
|
|
||||||
<div *ngIf="formErrors().instance.defaultNSFWPolicy" class="form-error" role="alert">{{ formErrors().instance.defaultNSFWPolicy }}</div>
|
<div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error" role="alert">{{ formErrors.instance.defaultNSFWPolicy }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -213,7 +243,7 @@
|
||||||
|
|
||||||
<my-markdown-textarea
|
<my-markdown-textarea
|
||||||
inputId="instanceTerms" formControlName="terms" markdownType="enhanced"
|
inputId="instanceTerms" formControlName="terms" markdownType="enhanced"
|
||||||
[formError]="formErrors()['instance.terms']"
|
[formError]="formErrors.instance.terms"
|
||||||
></my-markdown-textarea>
|
></my-markdown-textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -222,74 +252,74 @@
|
||||||
|
|
||||||
<my-markdown-textarea
|
<my-markdown-textarea
|
||||||
inputId="instanceCodeOfConduct" formControlName="codeOfConduct" markdownType="enhanced"
|
inputId="instanceCodeOfConduct" formControlName="codeOfConduct" markdownType="enhanced"
|
||||||
[formError]="formErrors()['instance.codeOfConduct']"
|
[formError]="formErrors.instance.codeOfConduct"
|
||||||
></my-markdown-textarea>
|
></my-markdown-textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="instanceModerationInformation">Moderation information</label><my-help helpType="markdownText"></my-help>
|
<label i18n for="instanceModerationInformation">Moderation information</label><my-help helpType="markdownText"></my-help>
|
||||||
<div i18n class="label-small-info">Who moderates the instance? What is the policy regarding sensitive content? Political videos? etc</div>
|
<div i18n class="form-group-description">Who moderates the instance? What is the policy regarding sensitive content? Political videos? etc</div>
|
||||||
|
|
||||||
<my-markdown-textarea
|
<my-markdown-textarea
|
||||||
inputId="instanceModerationInformation" formControlName="moderationInformation" markdownType="enhanced"
|
inputId="instanceModerationInformation" formControlName="moderationInformation" markdownType="enhanced"
|
||||||
[formError]="formErrors()['instance.moderationInformation']"
|
[formError]="formErrors.instance.moderationInformation"
|
||||||
></my-markdown-textarea>
|
></my-markdown-textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4"> <!-- you and your instance grid -->
|
<div class="pt-two-cols mt-4">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>YOU AND YOUR INSTANCE</h2>
|
<h2 i18n>YOU AND YOUR PLATFORM</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-col">
|
<div class="content-col">
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="instanceAdministrator">Who is behind the instance?</label><my-help helpType="markdownText"></my-help>
|
<label i18n for="instanceAdministrator">Who is behind the instance?</label><my-help helpType="markdownText"></my-help>
|
||||||
<div i18n class="label-small-info">A single person? A non-profit? A company?</div>
|
<div i18n class="form-group-description">A single person? A non-profit? A company?</div>
|
||||||
|
|
||||||
<my-markdown-textarea
|
<my-markdown-textarea
|
||||||
inputId="instanceAdministrator" formControlName="administrator" markdownType="enhanced"
|
inputId="instanceAdministrator" formControlName="administrator" markdownType="enhanced"
|
||||||
[formError]="formErrors()['instance.administrator']"
|
[formError]="formErrors.instance.administrator"
|
||||||
></my-markdown-textarea>
|
></my-markdown-textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="instanceCreationReason">Why did you create this instance?</label><my-help helpType="markdownText"></my-help>
|
<label i18n for="instanceCreationReason">Why did you create this instance?</label><my-help helpType="markdownText"></my-help>
|
||||||
<div i18n class="label-small-info">To share your personal videos? To open registrations and allow people to upload what they want?</div>
|
<div i18n class="form-group-description">To share your personal videos? To open registrations and allow people to upload what they want?</div>
|
||||||
|
|
||||||
<my-markdown-textarea
|
<my-markdown-textarea
|
||||||
inputId="instanceCreationReason" formControlName="creationReason" markdownType="enhanced"
|
inputId="instanceCreationReason" formControlName="creationReason" markdownType="enhanced"
|
||||||
[formError]="formErrors()['instance.creationReason']"
|
[formError]="formErrors.instance.creationReason"
|
||||||
></my-markdown-textarea>
|
></my-markdown-textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="instanceMaintenanceLifetime">How long do you plan to maintain this instance?</label><my-help helpType="markdownText"></my-help>
|
<label i18n for="instanceMaintenanceLifetime">How long do you plan to maintain this instance?</label><my-help helpType="markdownText"></my-help>
|
||||||
<div i18n class="label-small-info">It's important to know for users who want to register on your instance</div>
|
<div i18n class="form-group-description">It's important to know for users who want to register on your instance</div>
|
||||||
|
|
||||||
<my-markdown-textarea
|
<my-markdown-textarea
|
||||||
inputId="instanceMaintenanceLifetime" formControlName="maintenanceLifetime" markdownType="enhanced"
|
inputId="instanceMaintenanceLifetime" formControlName="maintenanceLifetime" markdownType="enhanced"
|
||||||
[formError]="formErrors()['instance.maintenanceLifetime']"
|
[formError]="formErrors.instance.maintenanceLifetime"
|
||||||
></my-markdown-textarea>
|
></my-markdown-textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="instanceBusinessModel">How will you finance the PeerTube server?</label><my-help helpType="markdownText"></my-help>
|
<label i18n for="instanceBusinessModel">How will you finance the PeerTube server?</label><my-help helpType="markdownText"></my-help>
|
||||||
<div i18n class="label-small-info">With your own funds? With user donations? Advertising?</div>
|
<div i18n class="form-group-description">With your own funds? With user donations? Advertising?</div>
|
||||||
|
|
||||||
<my-markdown-textarea
|
<my-markdown-textarea
|
||||||
inputId="instanceBusinessModel" formControlName="businessModel" markdownType="enhanced"
|
inputId="instanceBusinessModel" formControlName="businessModel" markdownType="enhanced"
|
||||||
[formError]="formErrors()['instance.businessModel']"
|
[formError]="formErrors.instance.businessModel"
|
||||||
></my-markdown-textarea>
|
></my-markdown-textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4"> <!-- other information grid -->
|
<div class="pt-two-cols mt-4">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>OTHER INFORMATION</h2>
|
<h2 i18n>OTHER INFORMATION</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -298,11 +328,11 @@
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="instanceHardwareInformation">What server/hardware does the instance run on?</label>
|
<label i18n for="instanceHardwareInformation">What server/hardware does the instance run on?</label>
|
||||||
<div i18n class="label-small-info">i.e. 2vCore 2GB RAM, a direct the link to the server you rent, etc.</div>
|
<div i18n class="form-group-description">i.e. 2vCore 2GB RAM, a direct the link to the server you rent, etc.</div>
|
||||||
|
|
||||||
<my-markdown-textarea
|
<my-markdown-textarea
|
||||||
inputId="instanceHardwareInformation" formControlName="hardwareInformation" markdownType="enhanced"
|
inputId="instanceHardwareInformation" formControlName="hardwareInformation" markdownType="enhanced"
|
||||||
[formError]="formErrors()['instance.hardwareInformation']"
|
[formError]="formErrors.instance.hardwareInformation"
|
||||||
></my-markdown-textarea>
|
></my-markdown-textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,299 @@
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http'
|
||||||
|
import { Component, OnInit, inject } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { ActivatedRoute, RouterLink } from '@angular/router'
|
||||||
|
import { CanComponentDeactivate, Notifier, ServerService } from '@app/core'
|
||||||
|
import { genericUploadErrorHandler } from '@app/helpers'
|
||||||
|
import { URL_VALIDATOR } from '@app/shared/form-validators/common-validators'
|
||||||
|
import {
|
||||||
|
ADMIN_EMAIL_VALIDATOR,
|
||||||
|
INSTANCE_NAME_VALIDATOR,
|
||||||
|
INSTANCE_SHORT_DESCRIPTION_VALIDATOR
|
||||||
|
} from '@app/shared/form-validators/custom-config-validators'
|
||||||
|
import {
|
||||||
|
BuildFormArgumentTyped,
|
||||||
|
FormDefaultTyped,
|
||||||
|
FormReactiveErrorsTyped,
|
||||||
|
FormReactiveMessagesTyped
|
||||||
|
} from '@app/shared/form-validators/form-validator.model'
|
||||||
|
import { CustomMarkupService } from '@app/shared/shared-custom-markup/custom-markup.service'
|
||||||
|
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
|
import { SelectRadioComponent } from '@app/shared/shared-forms/select/select-radio.component'
|
||||||
|
import { InstanceService } from '@app/shared/shared-main/instance/instance.service'
|
||||||
|
import { maxBy } from '@peertube/peertube-core-utils'
|
||||||
|
import { ActorImage, CustomConfig, HTMLServerConfig, NSFWPolicyType, VideoConstant } from '@peertube/peertube-models'
|
||||||
|
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||||
|
import { ActorAvatarEditComponent } from '../../../shared/shared-actor-image-edit/actor-avatar-edit.component'
|
||||||
|
import { ActorBannerEditComponent } from '../../../shared/shared-actor-image-edit/actor-banner-edit.component'
|
||||||
|
import { CustomMarkupHelpComponent } from '../../../shared/shared-custom-markup/custom-markup-help.component'
|
||||||
|
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
||||||
|
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||||
|
import { SelectCheckboxComponent } from '../../../shared/shared-forms/select/select-checkbox.component'
|
||||||
|
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||||
|
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
||||||
|
import { AdminConfigService } from '../shared/admin-config.service'
|
||||||
|
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||||
|
|
||||||
|
type Form = {
|
||||||
|
admin: FormGroup<{
|
||||||
|
email: FormControl<string>
|
||||||
|
}>
|
||||||
|
|
||||||
|
contactForm: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
|
||||||
|
instance: FormGroup<{
|
||||||
|
name: FormControl<string>
|
||||||
|
shortDescription: FormControl<string>
|
||||||
|
description: FormControl<string>
|
||||||
|
categories: FormControl<number[]>
|
||||||
|
languages: FormControl<string[]>
|
||||||
|
serverCountry: FormControl<string>
|
||||||
|
|
||||||
|
support: FormGroup<{
|
||||||
|
text: FormControl<string>
|
||||||
|
}>
|
||||||
|
|
||||||
|
social: FormGroup<{
|
||||||
|
externalLink: FormControl<string>
|
||||||
|
mastodonLink: FormControl<string>
|
||||||
|
blueskyLink: FormControl<string>
|
||||||
|
}>
|
||||||
|
|
||||||
|
isNSFW: FormControl<boolean>
|
||||||
|
defaultNSFWPolicy: FormControl<NSFWPolicyType>
|
||||||
|
|
||||||
|
terms: FormControl<string>
|
||||||
|
codeOfConduct: FormControl<string>
|
||||||
|
moderationInformation: FormControl<string>
|
||||||
|
administrator: FormControl<string>
|
||||||
|
creationReason: FormControl<string>
|
||||||
|
maintenanceLifetime: FormControl<string>
|
||||||
|
businessModel: FormControl<string>
|
||||||
|
hardwareInformation: FormControl<string>
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-admin-config-information',
|
||||||
|
templateUrl: './admin-config-information.component.html',
|
||||||
|
styleUrls: [ './admin-config-common.scss' ],
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
ActorAvatarEditComponent,
|
||||||
|
ActorBannerEditComponent,
|
||||||
|
SelectRadioComponent,
|
||||||
|
CommonModule,
|
||||||
|
CustomMarkupHelpComponent,
|
||||||
|
MarkdownTextareaComponent,
|
||||||
|
SelectCheckboxComponent,
|
||||||
|
RouterLink,
|
||||||
|
PeertubeCheckboxComponent,
|
||||||
|
PeerTubeTemplateDirective,
|
||||||
|
HelpComponent,
|
||||||
|
AdminSaveBarComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminConfigInformationComponent implements OnInit, CanComponentDeactivate {
|
||||||
|
private customMarkup = inject(CustomMarkupService)
|
||||||
|
private notifier = inject(Notifier)
|
||||||
|
private instanceService = inject(InstanceService)
|
||||||
|
private server = inject(ServerService)
|
||||||
|
private route = inject(ActivatedRoute)
|
||||||
|
private formReactiveService = inject(FormReactiveService)
|
||||||
|
private adminConfigService = inject(AdminConfigService)
|
||||||
|
|
||||||
|
form: FormGroup<Form>
|
||||||
|
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||||
|
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||||
|
|
||||||
|
languageItems: SelectOptionsItem[] = []
|
||||||
|
categoryItems: SelectOptionsItem[] = []
|
||||||
|
|
||||||
|
instanceBannerUrl: string
|
||||||
|
instanceAvatars: ActorImage[] = []
|
||||||
|
|
||||||
|
nsfwItems: SelectOptionsItem[] = [
|
||||||
|
{
|
||||||
|
id: 'do_not_list',
|
||||||
|
label: $localize`Hide`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'warn',
|
||||||
|
label: $localize`Warn`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'blur',
|
||||||
|
label: $localize`Blur`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'display',
|
||||||
|
label: $localize`Display`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
private serverConfig: HTMLServerConfig
|
||||||
|
private customConfig: CustomConfig
|
||||||
|
|
||||||
|
get instanceName () {
|
||||||
|
return this.server.getHTMLConfig().instance.name
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||||
|
|
||||||
|
const data = this.route.snapshot.data as {
|
||||||
|
languages: VideoConstant<string>[]
|
||||||
|
categories: VideoConstant<number>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.languageItems = data.languages.map(l => ({ label: l.label, id: l.id }))
|
||||||
|
this.categoryItems = data.categories.map(l => ({ label: l.label, id: l.id }))
|
||||||
|
|
||||||
|
this.serverConfig = this.server.getHTMLConfig()
|
||||||
|
|
||||||
|
this.updateActorImages()
|
||||||
|
this.buildForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildForm () {
|
||||||
|
const obj: BuildFormArgumentTyped<Form> = {
|
||||||
|
admin: {
|
||||||
|
email: ADMIN_EMAIL_VALIDATOR
|
||||||
|
},
|
||||||
|
contactForm: {
|
||||||
|
enabled: null
|
||||||
|
},
|
||||||
|
instance: {
|
||||||
|
name: INSTANCE_NAME_VALIDATOR,
|
||||||
|
shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
|
||||||
|
description: null,
|
||||||
|
|
||||||
|
isNSFW: null,
|
||||||
|
defaultNSFWPolicy: null,
|
||||||
|
|
||||||
|
terms: null,
|
||||||
|
codeOfConduct: null,
|
||||||
|
|
||||||
|
creationReason: null,
|
||||||
|
moderationInformation: null,
|
||||||
|
administrator: null,
|
||||||
|
maintenanceLifetime: null,
|
||||||
|
businessModel: null,
|
||||||
|
|
||||||
|
hardwareInformation: null,
|
||||||
|
|
||||||
|
categories: null,
|
||||||
|
languages: null,
|
||||||
|
|
||||||
|
serverCountry: null,
|
||||||
|
support: {
|
||||||
|
text: null
|
||||||
|
},
|
||||||
|
social: {
|
||||||
|
externalLink: URL_VALIDATOR,
|
||||||
|
mastodonLink: URL_VALIDATOR,
|
||||||
|
blueskyLink: URL_VALIDATOR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValues: FormDefaultTyped<Form> = this.customConfig
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
formErrors,
|
||||||
|
validationMessages
|
||||||
|
} = this.formReactiveService.buildForm<Form>(obj, defaultValues)
|
||||||
|
|
||||||
|
this.form = form
|
||||||
|
this.formErrors = formErrors
|
||||||
|
this.validationMessages = validationMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeactivate () {
|
||||||
|
return { canDeactivate: !this.form.dirty }
|
||||||
|
}
|
||||||
|
|
||||||
|
getCustomMarkdownRenderer () {
|
||||||
|
return this.customMarkup.getCustomMarkdownRenderer()
|
||||||
|
}
|
||||||
|
|
||||||
|
onBannerChange (formData: FormData) {
|
||||||
|
this.instanceService.updateInstanceBanner(formData)
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.notifier.success($localize`Banner changed.`)
|
||||||
|
|
||||||
|
this.resetActorImages()
|
||||||
|
},
|
||||||
|
|
||||||
|
error: (err: HttpErrorResponse) => genericUploadErrorHandler({ err, name: $localize`banner`, notifier: this.notifier })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onBannerDelete () {
|
||||||
|
this.instanceService.deleteInstanceBanner()
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.notifier.success($localize`Banner deleted.`)
|
||||||
|
|
||||||
|
this.resetActorImages()
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onAvatarChange (formData: FormData) {
|
||||||
|
this.instanceService.updateInstanceAvatar(formData)
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.notifier.success($localize`Avatar changed.`)
|
||||||
|
|
||||||
|
this.resetActorImages()
|
||||||
|
},
|
||||||
|
|
||||||
|
error: (err: HttpErrorResponse) => genericUploadErrorHandler({ err, name: $localize`avatar`, notifier: this.notifier })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onAvatarDelete () {
|
||||||
|
this.instanceService.deleteInstanceAvatar()
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.notifier.success($localize`Avatar deleted.`)
|
||||||
|
|
||||||
|
this.resetActorImages()
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateActorImages () {
|
||||||
|
this.instanceBannerUrl = maxBy(this.serverConfig.instance.banners, 'width')?.path
|
||||||
|
this.instanceAvatars = this.serverConfig.instance.avatars
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetActorImages () {
|
||||||
|
this.server.resetConfig()
|
||||||
|
.subscribe(config => {
|
||||||
|
this.serverConfig = config
|
||||||
|
|
||||||
|
this.updateActorImages()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
save () {
|
||||||
|
this.adminConfigService.saveAndUpdateCurrent({
|
||||||
|
currentConfig: this.customConfig,
|
||||||
|
form: this.form,
|
||||||
|
formConfig: this.form.value,
|
||||||
|
success: $localize`Information updated.`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,13 @@
|
||||||
<ng-container [formGroup]="form()">
|
<my-admin-save-bar i18n-title title="Live configuration" (save)="save()" [form]="form" [formErrors]="formErrors" [inconsistentOptions]="checkTranscodingConsistentOptions()"></my-admin-save-bar>
|
||||||
|
|
||||||
<div class="pt-two-cols mt-5">
|
<ng-container [formGroup]="form">
|
||||||
|
|
||||||
|
<div class="pt-two-cols">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>LIVE</h2>
|
<h2 i18n>LIVE</h2>
|
||||||
|
|
||||||
<div i18n class="inner-form-description">
|
<div i18n class="inner-form-description">
|
||||||
Enable users of your instance to stream live.
|
Enable users of your platform to stream a live.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -46,16 +48,16 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
||||||
<label i18n for="liveMaxInstanceLives">Max simultaneous lives created on your instance</label>
|
<label i18n for="liveMaxInstanceLives">Max simultaneous lives created on your platform</label>
|
||||||
|
|
||||||
<span i18n class="ms-2 small muted">(-1 for "unlimited")</span>
|
<span i18n class="ms-2 small muted">(-1 for "unlimited")</span>
|
||||||
|
|
||||||
<div class="number-with-unit">
|
<div class="number-with-unit">
|
||||||
<input type="number" id="liveMaxInstanceLives" formControlName="maxInstanceLives" />
|
<input type="number" id="liveMaxInstanceLives" formControlName="maxInstanceLives" />
|
||||||
<span i18n>{form().value['live']['maxInstanceLives'], plural, =1 {live} other {lives}}</span>
|
<span i18n>{form.value.live.maxInstanceLives, plural, =1 {live} other {lives}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors().live.maxInstanceLives" class="form-error" role="alert">{{ formErrors().live.maxInstanceLives }}</div>
|
<div *ngIf="formErrors.live.maxInstanceLives" class="form-error" role="alert">{{ formErrors.live.maxInstanceLives }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
||||||
|
@ -64,10 +66,10 @@
|
||||||
|
|
||||||
<div class="number-with-unit">
|
<div class="number-with-unit">
|
||||||
<input type="number" id="liveMaxUserLives" formControlName="maxUserLives" />
|
<input type="number" id="liveMaxUserLives" formControlName="maxUserLives" />
|
||||||
<span i18n>{form().value['live']['maxUserLives'], plural, =1 {live} other {lives}}</span>
|
<span i18n>{form.value.live.maxUserLives, plural, =1 {live} other {lives}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors().live.maxUserLives" class="form-error" role="alert">{{ formErrors().live.maxUserLives }}</div>
|
<div *ngIf="formErrors.live.maxUserLives" class="form-error" role="alert">{{ formErrors.live.maxUserLives }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
||||||
|
@ -75,7 +77,7 @@
|
||||||
|
|
||||||
<my-select-options inputId="liveMaxDuration" [items]="liveMaxDurationOptions" formControlName="maxDuration"></my-select-options>
|
<my-select-options inputId="liveMaxDuration" [items]="liveMaxDurationOptions" formControlName="maxDuration"></my-select-options>
|
||||||
|
|
||||||
<div *ngIf="formErrors().live.maxDuration" class="form-error" role="alert">{{ formErrors().live.maxDuration }}</div>
|
<div *ngIf="formErrors.live.maxDuration" class="form-error" role="alert">{{ formErrors.live.maxDuration }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -123,7 +125,7 @@
|
||||||
<span>FPS</span>
|
<span>FPS</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors().live.transcoding.fps.max" class="form-error" role="alert">{{ formErrors().live.transcoding.fps.max }}</div>
|
<div *ngIf="formErrors.live.transcoding.fps.max" class="form-error" role="alert">{{ formErrors.live.transcoding.fps.max }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ms-2 mt-3">
|
<div class="ms-2 mt-3">
|
||||||
|
@ -193,7 +195,7 @@
|
||||||
formControlName="threads"
|
formControlName="threads"
|
||||||
[clearable]="false"
|
[clearable]="false"
|
||||||
></my-select-custom-value>
|
></my-select-custom-value>
|
||||||
<div *ngIf="formErrors().live.transcoding.threads" class="form-error" role="alert">{{ formErrors().live.transcoding.threads }}</div>
|
<div *ngIf="formErrors.live.transcoding.threads" class="form-error" role="alert">{{ formErrors.live.transcoding.threads }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group mt-4" [ngClass]="getDisabledLiveLocalTranscodingClass()">
|
<div class="form-group mt-4" [ngClass]="getDisabledLiveLocalTranscodingClass()">
|
||||||
|
@ -202,7 +204,7 @@
|
||||||
|
|
||||||
<my-select-options inputId="liveTranscodingProfile" formControlName="profile" [items]="transcodingProfiles"></my-select-options>
|
<my-select-options inputId="liveTranscodingProfile" formControlName="profile" [items]="transcodingProfiles"></my-select-options>
|
||||||
|
|
||||||
<div *ngIf="formErrors().live.transcoding.profile" class="form-error" role="alert">{{ formErrors().live.transcoding.profile }}</div>
|
<div *ngIf="formErrors.live.transcoding.profile" class="form-error" role="alert">{{ formErrors.live.transcoding.profile }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
|
@ -0,0 +1,224 @@
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, OnInit, inject } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { ActivatedRoute, RouterLink } from '@angular/router'
|
||||||
|
import { CanComponentDeactivate, ServerService } from '@app/core'
|
||||||
|
import {
|
||||||
|
MAX_INSTANCE_LIVES_VALIDATOR,
|
||||||
|
MAX_LIVE_DURATION_VALIDATOR,
|
||||||
|
MAX_USER_LIVES_VALIDATOR,
|
||||||
|
TRANSCODING_MAX_FPS_VALIDATOR,
|
||||||
|
TRANSCODING_THREADS_VALIDATOR
|
||||||
|
} from '@app/shared/form-validators/custom-config-validators'
|
||||||
|
import {
|
||||||
|
BuildFormArgumentTyped,
|
||||||
|
FormDefaultTyped,
|
||||||
|
FormReactiveErrorsTyped,
|
||||||
|
FormReactiveMessagesTyped
|
||||||
|
} from '@app/shared/form-validators/form-validator.model'
|
||||||
|
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
|
import { CustomConfig } from '@peertube/peertube-models'
|
||||||
|
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||||
|
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||||
|
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||||
|
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||||
|
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
||||||
|
import { AdminConfigService, FormResolutions, ResolutionOption } from '../shared/admin-config.service'
|
||||||
|
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||||
|
|
||||||
|
type Form = {
|
||||||
|
live: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
allowReplay: FormControl<boolean>
|
||||||
|
latencySetting: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
maxInstanceLives: FormControl<number>
|
||||||
|
maxUserLives: FormControl<number>
|
||||||
|
maxDuration: FormControl<number>
|
||||||
|
|
||||||
|
transcoding: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
|
||||||
|
fps: FormGroup<{
|
||||||
|
max: FormControl<number>
|
||||||
|
}>
|
||||||
|
|
||||||
|
resolutions: FormGroup<FormResolutions>
|
||||||
|
alwaysTranscodeOriginalResolution: FormControl<boolean>
|
||||||
|
|
||||||
|
remoteRunners: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
|
||||||
|
threads: FormControl<number>
|
||||||
|
profile: FormControl<string>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-admin-config-live',
|
||||||
|
templateUrl: './admin-config-live.component.html',
|
||||||
|
styleUrls: [ './admin-config-common.scss' ],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
PeertubeCheckboxComponent,
|
||||||
|
PeerTubeTemplateDirective,
|
||||||
|
SelectOptionsComponent,
|
||||||
|
RouterLink,
|
||||||
|
SelectCustomValueComponent,
|
||||||
|
AdminSaveBarComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminConfigLiveComponent implements OnInit, CanComponentDeactivate {
|
||||||
|
private configService = inject(AdminConfigService)
|
||||||
|
private server = inject(ServerService)
|
||||||
|
private route = inject(ActivatedRoute)
|
||||||
|
private formReactiveService = inject(FormReactiveService)
|
||||||
|
private adminConfigService = inject(AdminConfigService)
|
||||||
|
|
||||||
|
form: FormGroup<Form>
|
||||||
|
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||||
|
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||||
|
|
||||||
|
transcodingThreadOptions: SelectOptionsItem[] = []
|
||||||
|
transcodingProfiles: SelectOptionsItem[] = []
|
||||||
|
|
||||||
|
liveMaxDurationOptions: SelectOptionsItem[] = []
|
||||||
|
liveResolutions: ResolutionOption[] = []
|
||||||
|
|
||||||
|
private customConfig: CustomConfig
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||||
|
|
||||||
|
this.transcodingThreadOptions = this.configService.transcodingThreadOptions
|
||||||
|
|
||||||
|
this.liveMaxDurationOptions = [
|
||||||
|
{ id: -1, label: $localize`No limit` },
|
||||||
|
{ id: 1000 * 3600, label: $localize`1 hour` },
|
||||||
|
{ id: 1000 * 3600 * 3, label: $localize`3 hours` },
|
||||||
|
{ id: 1000 * 3600 * 5, label: $localize`5 hours` },
|
||||||
|
{ id: 1000 * 3600 * 10, label: $localize`10 hours` }
|
||||||
|
]
|
||||||
|
|
||||||
|
this.liveResolutions = this.adminConfigService.transcodingResolutionOptions
|
||||||
|
this.transcodingProfiles = this.adminConfigService.buildTranscodingProfiles(
|
||||||
|
this.server.getHTMLConfig().live.transcoding.availableProfiles
|
||||||
|
)
|
||||||
|
|
||||||
|
this.buildForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildForm () {
|
||||||
|
const obj: BuildFormArgumentTyped<Form> = {
|
||||||
|
live: {
|
||||||
|
enabled: null,
|
||||||
|
allowReplay: null,
|
||||||
|
|
||||||
|
maxDuration: MAX_LIVE_DURATION_VALIDATOR,
|
||||||
|
maxInstanceLives: MAX_INSTANCE_LIVES_VALIDATOR,
|
||||||
|
maxUserLives: MAX_USER_LIVES_VALIDATOR,
|
||||||
|
latencySetting: {
|
||||||
|
enabled: null
|
||||||
|
},
|
||||||
|
|
||||||
|
transcoding: {
|
||||||
|
enabled: null,
|
||||||
|
threads: TRANSCODING_THREADS_VALIDATOR,
|
||||||
|
profile: null,
|
||||||
|
resolutions: this.adminConfigService.buildFormResolutions(),
|
||||||
|
alwaysTranscodeOriginalResolution: null,
|
||||||
|
remoteRunners: {
|
||||||
|
enabled: null
|
||||||
|
},
|
||||||
|
fps: {
|
||||||
|
max: TRANSCODING_MAX_FPS_VALIDATOR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValues: FormDefaultTyped<Form> = this.customConfig
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
formErrors,
|
||||||
|
validationMessages
|
||||||
|
} = this.formReactiveService.buildForm<Form>(obj, defaultValues)
|
||||||
|
|
||||||
|
this.form = form
|
||||||
|
this.formErrors = formErrors
|
||||||
|
this.validationMessages = validationMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeactivate () {
|
||||||
|
return { canDeactivate: !this.form.dirty }
|
||||||
|
}
|
||||||
|
|
||||||
|
getResolutionKey (resolution: string) {
|
||||||
|
return 'live.transcoding.resolutions.' + resolution
|
||||||
|
}
|
||||||
|
|
||||||
|
getLiveRTMPPort () {
|
||||||
|
return this.server.getHTMLConfig().live.rtmp.port
|
||||||
|
}
|
||||||
|
|
||||||
|
isLiveEnabled () {
|
||||||
|
return this.form.value.live.enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
isRemoteRunnerLiveEnabled () {
|
||||||
|
return this.form.value.live.transcoding.remoteRunners.enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisabledLiveClass () {
|
||||||
|
return { 'disabled-checkbox-extra': !this.isLiveEnabled() }
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisabledLiveTranscodingClass () {
|
||||||
|
return { 'disabled-checkbox-extra': !this.isLiveEnabled() || !this.isLiveTranscodingEnabled() }
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisabledLiveLocalTranscodingClass () {
|
||||||
|
return { 'disabled-checkbox-extra': !this.isLiveEnabled() || !this.isLiveTranscodingEnabled() || this.isRemoteRunnerLiveEnabled() }
|
||||||
|
}
|
||||||
|
|
||||||
|
isLiveTranscodingEnabled () {
|
||||||
|
return this.form.value.live.transcoding.enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
getTotalTranscodingThreads () {
|
||||||
|
return this.adminConfigService.getTotalTranscodingThreads({
|
||||||
|
transcoding: this.customConfig.transcoding,
|
||||||
|
live: {
|
||||||
|
transcoding: {
|
||||||
|
enabled: this.form.value.live.transcoding.enabled,
|
||||||
|
threads: this.form.value.live.transcoding.threads
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
save () {
|
||||||
|
this.adminConfigService.saveAndUpdateCurrent({
|
||||||
|
currentConfig: this.customConfig,
|
||||||
|
form: this.form,
|
||||||
|
formConfig: this.form.value,
|
||||||
|
success: $localize`Live configuration updated.`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
checkTranscodingConsistentOptions () {
|
||||||
|
return this.adminConfigService.checkTranscodingConsistentOptions({
|
||||||
|
transcoding: this.customConfig.transcoding,
|
||||||
|
live: {
|
||||||
|
enabled: this.form.value.live.enabled,
|
||||||
|
allowReplay: this.form.value.live.allowReplay
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,18 @@
|
||||||
<ng-container [formGroup]="form()">
|
<my-admin-save-bar i18n-title title="VOD configuration" (save)="save()" [form]="form" [formErrors]="formErrors" [inconsistentOptions]="checkTranscodingConsistentOptions()"></my-admin-save-bar>
|
||||||
|
|
||||||
|
<ng-container [formGroup]="form">
|
||||||
|
|
||||||
|
<div class="pt-two-cols">
|
||||||
|
<div class="title-col">
|
||||||
|
<h2 i18n>TRANSCODING</h2>
|
||||||
|
|
||||||
|
<div i18n class="inner-form-description">
|
||||||
|
Process uploaded videos so that they are streamable on any device. Although this is costly in terms of resources, it is a critical part of PeerTube, so proceed with caution.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4">
|
|
||||||
<div class="title-col"></div>
|
|
||||||
<div class="content-col">
|
<div class="content-col">
|
||||||
|
<div class="callout callout-primary mb-4">
|
||||||
<div class="callout callout-primary">
|
|
||||||
<span i18n>
|
<span i18n>
|
||||||
Estimating a server's capacity to transcode and stream videos isn't easy and we can't tune PeerTube automatically.
|
Estimating a server's capacity to transcode and stream videos isn't easy and we can't tune PeerTube automatically.
|
||||||
</span>
|
</span>
|
||||||
|
@ -15,20 +23,6 @@
|
||||||
However, you may want to read <a class="link-primary" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/admin/configuration#vod-transcoding">our guidelines</a> before tweaking the following values.
|
However, you may want to read <a class="link-primary" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/admin/configuration#vod-transcoding">our guidelines</a> before tweaking the following values.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4">
|
|
||||||
<div class="title-col">
|
|
||||||
<h2 i18n>TRANSCODING</h2>
|
|
||||||
|
|
||||||
<div i18n class="inner-form-description">
|
|
||||||
Process uploaded videos so that they are in a streamable form that any device can play. Though costly in
|
|
||||||
resources, this is a critical part of PeerTube, so tread carefully.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content-col">
|
|
||||||
|
|
||||||
<ng-container formGroupName="transcoding">
|
<ng-container formGroupName="transcoding">
|
||||||
|
|
||||||
|
@ -151,7 +145,7 @@
|
||||||
<span>FPS</span>
|
<span>FPS</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors().transcoding.fps.max" class="form-error" role="alert">{{ formErrors().transcoding.fps.max }}</div>
|
<div *ngIf="formErrors.transcoding.fps.max" class="form-error" role="alert">{{ formErrors.transcoding.fps.max }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" [ngClass]="getTranscodingDisabledClass()">
|
<div class="form-group" [ngClass]="getTranscodingDisabledClass()">
|
||||||
|
@ -220,7 +214,7 @@
|
||||||
[clearable]="false"
|
[clearable]="false"
|
||||||
></my-select-custom-value>
|
></my-select-custom-value>
|
||||||
|
|
||||||
<div *ngIf="formErrors().transcoding.threads" class="form-error" role="alert">{{ formErrors().transcoding.threads }}</div>
|
<div *ngIf="formErrors.transcoding.threads" class="form-error" role="alert">{{ formErrors.transcoding.threads }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" [ngClass]="getLocalTranscodingDisabledClass()">
|
<div class="form-group" [ngClass]="getLocalTranscodingDisabledClass()">
|
||||||
|
@ -232,7 +226,7 @@
|
||||||
<span i18n>jobs in parallel</span>
|
<span i18n>jobs in parallel</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formErrors().transcoding.concurrency" class="form-error" role="alert">{{ formErrors().transcoding.concurrency }}</div>
|
<div *ngIf="formErrors.transcoding.concurrency" class="form-error" role="alert">{{ formErrors.transcoding.concurrency }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" [ngClass]="getLocalTranscodingDisabledClass()">
|
<div class="form-group" [ngClass]="getLocalTranscodingDisabledClass()">
|
||||||
|
@ -241,7 +235,7 @@
|
||||||
|
|
||||||
<my-select-options inputId="transcodingProfile" formControlName="profile" [items]="transcodingProfiles"></my-select-options>
|
<my-select-options inputId="transcodingProfile" formControlName="profile" [items]="transcodingProfiles"></my-select-options>
|
||||||
|
|
||||||
<div *ngIf="formErrors().transcoding.profile" class="form-error" role="alert">{{ formErrors().transcoding.profile }}</div>
|
<div *ngIf="formErrors.transcoding.profile" class="form-error" role="alert">{{ formErrors.transcoding.profile }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
295
client/src/app/+admin/config/pages/admin-config-vod.component.ts
Normal file
295
client/src/app/+admin/config/pages/admin-config-vod.component.ts
Normal file
|
@ -0,0 +1,295 @@
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, OnInit, inject } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { ActivatedRoute, RouterLink } from '@angular/router'
|
||||||
|
import { CanComponentDeactivate, Notifier, ServerService } from '@app/core'
|
||||||
|
import {
|
||||||
|
CONCURRENCY_VALIDATOR,
|
||||||
|
TRANSCODING_MAX_FPS_VALIDATOR,
|
||||||
|
TRANSCODING_THREADS_VALIDATOR
|
||||||
|
} from '@app/shared/form-validators/custom-config-validators'
|
||||||
|
import {
|
||||||
|
BuildFormArgumentTyped,
|
||||||
|
FormDefaultTyped,
|
||||||
|
FormReactiveErrorsTyped,
|
||||||
|
FormReactiveMessagesTyped
|
||||||
|
} from '@app/shared/form-validators/form-validator.model'
|
||||||
|
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
|
import { CustomConfig } from '@peertube/peertube-models'
|
||||||
|
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||||
|
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||||
|
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||||
|
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||||
|
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
||||||
|
import { AdminConfigService, FormResolutions, ResolutionOption } from '../shared/admin-config.service'
|
||||||
|
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||||
|
|
||||||
|
type Form = {
|
||||||
|
transcoding: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
allowAdditionalExtensions: FormControl<boolean>
|
||||||
|
allowAudioFiles: FormControl<boolean>
|
||||||
|
|
||||||
|
originalFile: FormGroup<{
|
||||||
|
keep: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
|
||||||
|
webVideos: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
|
||||||
|
hls: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
splitAudioAndVideo: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
|
||||||
|
fps: FormGroup<{
|
||||||
|
max: FormControl<number>
|
||||||
|
}>
|
||||||
|
|
||||||
|
resolutions: FormGroup<FormResolutions>
|
||||||
|
alwaysTranscodeOriginalResolution: FormControl<boolean>
|
||||||
|
|
||||||
|
remoteRunners: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
|
||||||
|
threads: FormControl<number>
|
||||||
|
|
||||||
|
profile: FormControl<string>
|
||||||
|
concurrency: FormControl<number>
|
||||||
|
}>
|
||||||
|
|
||||||
|
videoStudio: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
remoteRunners: FormGroup<{
|
||||||
|
enabled: FormControl<boolean>
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-admin-config-vod',
|
||||||
|
templateUrl: './admin-config-vod.component.html',
|
||||||
|
styleUrls: [ './admin-config-common.scss' ],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
PeertubeCheckboxComponent,
|
||||||
|
PeerTubeTemplateDirective,
|
||||||
|
RouterLink,
|
||||||
|
SelectCustomValueComponent,
|
||||||
|
SelectOptionsComponent,
|
||||||
|
AdminSaveBarComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminConfigVODComponent implements OnInit, CanComponentDeactivate {
|
||||||
|
private configService = inject(AdminConfigService)
|
||||||
|
private notifier = inject(Notifier)
|
||||||
|
private server = inject(ServerService)
|
||||||
|
private route = inject(ActivatedRoute)
|
||||||
|
private formReactiveService = inject(FormReactiveService)
|
||||||
|
private adminConfigService = inject(AdminConfigService)
|
||||||
|
|
||||||
|
form: FormGroup<Form>
|
||||||
|
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||||
|
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||||
|
|
||||||
|
transcodingThreadOptions: SelectOptionsItem[] = []
|
||||||
|
transcodingProfiles: SelectOptionsItem[] = []
|
||||||
|
resolutions: ResolutionOption[] = []
|
||||||
|
|
||||||
|
additionalVideoExtensions = ''
|
||||||
|
|
||||||
|
private customConfig: CustomConfig
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
const serverConfig = this.server.getHTMLConfig()
|
||||||
|
|
||||||
|
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||||
|
|
||||||
|
this.transcodingThreadOptions = this.configService.transcodingThreadOptions
|
||||||
|
this.resolutions = this.adminConfigService.transcodingResolutionOptions
|
||||||
|
this.additionalVideoExtensions = serverConfig.video.file.extensions.join(' ')
|
||||||
|
this.transcodingProfiles = this.adminConfigService.buildTranscodingProfiles(serverConfig.transcoding.availableProfiles)
|
||||||
|
|
||||||
|
this.buildForm()
|
||||||
|
|
||||||
|
this.subscribeToTranscodingChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildForm () {
|
||||||
|
const obj: BuildFormArgumentTyped<Form> = {
|
||||||
|
transcoding: {
|
||||||
|
enabled: null,
|
||||||
|
allowAdditionalExtensions: null,
|
||||||
|
allowAudioFiles: null,
|
||||||
|
|
||||||
|
originalFile: {
|
||||||
|
keep: null
|
||||||
|
},
|
||||||
|
|
||||||
|
webVideos: {
|
||||||
|
enabled: null
|
||||||
|
},
|
||||||
|
|
||||||
|
hls: {
|
||||||
|
enabled: null,
|
||||||
|
splitAudioAndVideo: null
|
||||||
|
},
|
||||||
|
|
||||||
|
fps: {
|
||||||
|
max: TRANSCODING_MAX_FPS_VALIDATOR
|
||||||
|
},
|
||||||
|
|
||||||
|
resolutions: this.adminConfigService.buildFormResolutions(),
|
||||||
|
alwaysTranscodeOriginalResolution: null,
|
||||||
|
|
||||||
|
remoteRunners: {
|
||||||
|
enabled: null
|
||||||
|
},
|
||||||
|
|
||||||
|
threads: TRANSCODING_THREADS_VALIDATOR,
|
||||||
|
|
||||||
|
profile: null,
|
||||||
|
concurrency: CONCURRENCY_VALIDATOR
|
||||||
|
},
|
||||||
|
|
||||||
|
videoStudio: {
|
||||||
|
enabled: null,
|
||||||
|
remoteRunners: {
|
||||||
|
enabled: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValues: FormDefaultTyped<Form> = this.customConfig
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
formErrors,
|
||||||
|
validationMessages
|
||||||
|
} = this.formReactiveService.buildForm<Form>(obj, defaultValues)
|
||||||
|
|
||||||
|
this.form = form
|
||||||
|
this.formErrors = formErrors
|
||||||
|
this.validationMessages = validationMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeactivate () {
|
||||||
|
return { canDeactivate: !this.form.dirty }
|
||||||
|
}
|
||||||
|
|
||||||
|
getResolutionKey (resolution: string) {
|
||||||
|
return 'transcoding.resolutions.' + resolution
|
||||||
|
}
|
||||||
|
|
||||||
|
isRemoteRunnerVODEnabled () {
|
||||||
|
return this.form.value.transcoding.remoteRunners.enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
isTranscodingEnabled () {
|
||||||
|
return this.form.value.transcoding.enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
isHLSEnabled () {
|
||||||
|
return this.form.value.transcoding.hls.enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
isStudioEnabled () {
|
||||||
|
return this.form.value.videoStudio.enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
getTranscodingDisabledClass () {
|
||||||
|
return { 'disabled-checkbox-extra': !this.isTranscodingEnabled() }
|
||||||
|
}
|
||||||
|
|
||||||
|
getHLSDisabledClass () {
|
||||||
|
return { 'disabled-checkbox-extra': !this.isHLSEnabled() }
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocalTranscodingDisabledClass () {
|
||||||
|
return { 'disabled-checkbox-extra': !this.isTranscodingEnabled() || this.isRemoteRunnerVODEnabled() }
|
||||||
|
}
|
||||||
|
|
||||||
|
getStudioDisabledClass () {
|
||||||
|
return { 'disabled-checkbox-extra': !this.isStudioEnabled() }
|
||||||
|
}
|
||||||
|
|
||||||
|
getTotalTranscodingThreads () {
|
||||||
|
return this.adminConfigService.getTotalTranscodingThreads({
|
||||||
|
live: this.customConfig.live,
|
||||||
|
transcoding: {
|
||||||
|
enabled: this.form.value.transcoding.enabled,
|
||||||
|
threads: this.form.value.transcoding.threads
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private subscribeToTranscodingChanges () {
|
||||||
|
const controls = this.form.controls
|
||||||
|
|
||||||
|
const transcodingControl = controls.transcoding.controls.enabled
|
||||||
|
const videoStudioControl = controls.videoStudio.controls.enabled
|
||||||
|
const hlsControl = controls.transcoding.controls.hls.controls.enabled
|
||||||
|
const webVideosControl = controls.transcoding.controls.webVideos.controls.enabled
|
||||||
|
|
||||||
|
webVideosControl.valueChanges
|
||||||
|
.subscribe(newValue => {
|
||||||
|
if (newValue === false && hlsControl.value === false) {
|
||||||
|
hlsControl.setValue(true)
|
||||||
|
|
||||||
|
this.notifier.info(
|
||||||
|
$localize`Automatically enable HLS transcoding because at least 1 output format must be enabled when transcoding is enabled`,
|
||||||
|
'',
|
||||||
|
10000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
hlsControl.valueChanges
|
||||||
|
.subscribe(newValue => {
|
||||||
|
if (newValue === false && webVideosControl.value === false) {
|
||||||
|
webVideosControl.setValue(true)
|
||||||
|
|
||||||
|
this.notifier.info(
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
$localize`Automatically enable Web Videos transcoding because at least 1 output format must be enabled when transcoding is enabled`,
|
||||||
|
'',
|
||||||
|
10000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
transcodingControl.valueChanges
|
||||||
|
.subscribe(newValue => {
|
||||||
|
if (newValue === false) {
|
||||||
|
videoStudioControl.setValue(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
transcodingControl.updateValueAndValidity()
|
||||||
|
webVideosControl.updateValueAndValidity()
|
||||||
|
videoStudioControl.updateValueAndValidity()
|
||||||
|
hlsControl.updateValueAndValidity()
|
||||||
|
}
|
||||||
|
|
||||||
|
save () {
|
||||||
|
this.adminConfigService.saveAndUpdateCurrent({
|
||||||
|
currentConfig: this.customConfig,
|
||||||
|
form: this.form,
|
||||||
|
formConfig: this.form.value,
|
||||||
|
success: $localize`VOD configuration updated.`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
checkTranscodingConsistentOptions () {
|
||||||
|
return this.adminConfigService.checkTranscodingConsistentOptions({
|
||||||
|
transcoding: {
|
||||||
|
enabled: this.form.value.transcoding.enabled
|
||||||
|
},
|
||||||
|
live: this.customConfig.live
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
6
client/src/app/+admin/config/pages/index.ts
Normal file
6
client/src/app/+admin/config/pages/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export * from './admin-config-advanced.component'
|
||||||
|
export * from './admin-config-general.component'
|
||||||
|
export * from './admin-config-homepage.component'
|
||||||
|
export * from './admin-config-information.component'
|
||||||
|
export * from './admin-config-live.component'
|
||||||
|
export * from './admin-config-vod.component'
|
210
client/src/app/+admin/config/shared/admin-config.service.ts
Normal file
210
client/src/app/+admin/config/shared/admin-config.service.ts
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
import { HttpClient } from '@angular/common/http'
|
||||||
|
import { Injectable, inject } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
|
import { Notifier, RestExtractor, ServerService } from '@app/core'
|
||||||
|
import { formatICU } from '@app/helpers'
|
||||||
|
import { BuildFormValidator } from '@app/shared/form-validators/form-validator.model'
|
||||||
|
import { CustomConfig } from '@peertube/peertube-models'
|
||||||
|
import { DeepPartial } from '@peertube/peertube-typescript-utils'
|
||||||
|
import merge from 'lodash-es/merge'
|
||||||
|
import { catchError, map, switchMap } from 'rxjs/operators'
|
||||||
|
import { environment } from '../../../../environments/environment'
|
||||||
|
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
|
||||||
|
|
||||||
|
export type FormResolutions = {
|
||||||
|
'0p': FormControl<boolean>
|
||||||
|
'144p': FormControl<boolean>
|
||||||
|
'240p': FormControl<boolean>
|
||||||
|
'360p': FormControl<boolean>
|
||||||
|
'480p': FormControl<boolean>
|
||||||
|
'720p': FormControl<boolean>
|
||||||
|
'1080p': FormControl<boolean>
|
||||||
|
'1440p': FormControl<boolean>
|
||||||
|
'2160p': FormControl<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResolutionOption = { id: keyof FormResolutions, label: string, description?: string }
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminConfigService {
|
||||||
|
private authHttp = inject(HttpClient)
|
||||||
|
private restExtractor = inject(RestExtractor)
|
||||||
|
private notifier = inject(Notifier)
|
||||||
|
private serverService = inject(ServerService)
|
||||||
|
|
||||||
|
private static BASE_APPLICATION_URL = environment.apiUrl + '/api/v1/config'
|
||||||
|
|
||||||
|
transcodingThreadOptions: SelectOptionsItem[] = []
|
||||||
|
transcodingResolutionOptions: ResolutionOption[] = []
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
this.transcodingThreadOptions = [
|
||||||
|
{ id: 0, label: $localize`Auto (via ffmpeg)` },
|
||||||
|
{ id: 1, label: '1' },
|
||||||
|
{ id: 2, label: '2' },
|
||||||
|
{ id: 4, label: '4' },
|
||||||
|
{ id: 8, label: '8' },
|
||||||
|
{ id: 12, label: '12' },
|
||||||
|
{ id: 16, label: '16' },
|
||||||
|
{ id: 32, label: '32' }
|
||||||
|
]
|
||||||
|
|
||||||
|
this.transcodingResolutionOptions = [
|
||||||
|
{
|
||||||
|
id: '0p',
|
||||||
|
label: $localize`Audio-only`,
|
||||||
|
description:
|
||||||
|
$localize`"Split audio and video" must be enabled for the PeerTube player to propose an "Audio only" resolution to users`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '144p',
|
||||||
|
label: $localize`144p`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '240p',
|
||||||
|
label: $localize`240p`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '360p',
|
||||||
|
label: $localize`360p`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '480p',
|
||||||
|
label: $localize`480p`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '720p',
|
||||||
|
label: $localize`720p`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1080p',
|
||||||
|
label: $localize`1080p`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1440p',
|
||||||
|
label: $localize`1440p`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2160p',
|
||||||
|
label: $localize`2160p`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
getCustomConfig () {
|
||||||
|
return this.authHttp.get<CustomConfig>(AdminConfigService.BASE_APPLICATION_URL + '/custom')
|
||||||
|
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCustomConfig (partialConfig: DeepPartial<CustomConfig>) {
|
||||||
|
return this.getCustomConfig()
|
||||||
|
.pipe(
|
||||||
|
switchMap(customConfig => {
|
||||||
|
const newConfig = merge(customConfig, partialConfig)
|
||||||
|
|
||||||
|
return this.authHttp.put<CustomConfig>(AdminConfigService.BASE_APPLICATION_URL + '/custom', newConfig)
|
||||||
|
.pipe(map(() => newConfig))
|
||||||
|
}),
|
||||||
|
catchError(res => this.restExtractor.handleError(res))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveAndUpdateCurrent (options: {
|
||||||
|
currentConfig: CustomConfig
|
||||||
|
form: FormGroup
|
||||||
|
formConfig: DeepPartial<CustomConfig>
|
||||||
|
success: string
|
||||||
|
}) {
|
||||||
|
const { currentConfig, form, formConfig, success } = options
|
||||||
|
|
||||||
|
this.updateCustomConfig(formConfig)
|
||||||
|
.pipe(switchMap(() => this.serverService.resetConfig()))
|
||||||
|
.subscribe({
|
||||||
|
next: newConfig => {
|
||||||
|
Object.assign(currentConfig, newConfig)
|
||||||
|
|
||||||
|
form.markAsPristine()
|
||||||
|
|
||||||
|
this.notifier.success(success)
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
buildFormResolutions () {
|
||||||
|
const formResolutions = {} as Record<keyof FormResolutions, BuildFormValidator>
|
||||||
|
|
||||||
|
for (const resolution of this.transcodingResolutionOptions) {
|
||||||
|
formResolutions[resolution.id] = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return formResolutions
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTranscodingProfiles (profiles: string[]) {
|
||||||
|
return profiles.map(p => {
|
||||||
|
if (p === 'default') {
|
||||||
|
return { id: p, label: $localize`Default`, description: $localize`x264, targeting maximum device compatibility` }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { id: p, label: p }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getTotalTranscodingThreads (options: {
|
||||||
|
transcoding: {
|
||||||
|
enabled: boolean
|
||||||
|
threads: number
|
||||||
|
}
|
||||||
|
live: {
|
||||||
|
transcoding: {
|
||||||
|
enabled: boolean
|
||||||
|
threads: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
const transcodingEnabled = options.transcoding.enabled
|
||||||
|
const transcodingThreads = options.transcoding.threads
|
||||||
|
const liveTranscodingEnabled = options.live.transcoding.enabled
|
||||||
|
const liveTranscodingThreads = options.live.transcoding.threads
|
||||||
|
|
||||||
|
// checks whether all enabled method are on fixed values and not on auto (= 0)
|
||||||
|
let noneOnAuto = !transcodingEnabled || +transcodingThreads > 0
|
||||||
|
noneOnAuto &&= !liveTranscodingEnabled || +liveTranscodingThreads > 0
|
||||||
|
|
||||||
|
// count total of fixed value, repalcing auto by a single thread (knowing it will display "at least")
|
||||||
|
let value = 0
|
||||||
|
if (transcodingEnabled) value += +transcodingThreads || 1
|
||||||
|
if (liveTranscodingEnabled) value += +liveTranscodingThreads || 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible
|
||||||
|
unit: formatICU($localize`{value, plural, =1 {thread} other {threads}}`, { value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkTranscodingConsistentOptions (options: {
|
||||||
|
transcoding: {
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
live: {
|
||||||
|
enabled: boolean
|
||||||
|
allowReplay: boolean
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
if (
|
||||||
|
options.transcoding.enabled === false &&
|
||||||
|
options.live.enabled === true && options.live.allowReplay === true
|
||||||
|
) {
|
||||||
|
return $localize`You cannot allow live replay if you don't enable transcoding.`
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
<div class="root">
|
||||||
|
<div class="root-bar">
|
||||||
|
<h2>{{ title() }}</h2>
|
||||||
|
|
||||||
|
<my-button
|
||||||
|
theme="primary" class="save-button" icon="circle-tick"
|
||||||
|
[disabled]="!canUpdate()" (click)="onSave($event)" i18n
|
||||||
|
>Save</my-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!isUpdateAllowed()) {
|
||||||
|
<my-alert type="primary" i18n class="d-block mt-3">
|
||||||
|
Updating platform configuration from the web interface is disabled by the system administrator.
|
||||||
|
</my-alert>
|
||||||
|
} @else if (displayFormErrors && !form().valid) {
|
||||||
|
<my-alert type="danger" class="d-block mt-3">
|
||||||
|
<ng-container i18n>There are errors in the configuration:</ng-container>
|
||||||
|
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li *ngFor="let error of grabAllErrors()">{{ error }}</li>
|
||||||
|
</ul>
|
||||||
|
</my-alert>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (inconsistentOptions()) {
|
||||||
|
<my-alert type="danger" class="d-block mt-3">{{ inconsistentOptions() }}</my-alert>
|
||||||
|
}
|
||||||
|
</div>
|
|
@ -0,0 +1,55 @@
|
||||||
|
@use "_variables" as *;
|
||||||
|
@use "_mixins" as *;
|
||||||
|
@use "_form-mixins" as *;
|
||||||
|
@import "bootstrap/scss/mixins";
|
||||||
|
|
||||||
|
.root {
|
||||||
|
position: sticky;
|
||||||
|
top: pvar(--header-height);
|
||||||
|
z-index: 11;
|
||||||
|
background-color: pvar(--bg);
|
||||||
|
|
||||||
|
@include rfs(3rem, margin-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
border-radius: 14px;
|
||||||
|
background-color: pvar(--bg-secondary-350);
|
||||||
|
|
||||||
|
@include rfs(1.5rem, padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-button {
|
||||||
|
@include margin-left(auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
flex-shrink: 1;
|
||||||
|
color: pvar(--fg-350);
|
||||||
|
font-weight: $font-bold;
|
||||||
|
margin-bottom: 0;
|
||||||
|
line-height: normal;
|
||||||
|
|
||||||
|
@include margin-left(auto);
|
||||||
|
@include font-size(1.5rem);
|
||||||
|
@include ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $small-view) {
|
||||||
|
.root-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-button,
|
||||||
|
h2 {
|
||||||
|
@include margin-left(0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, inject, input, OnDestroy, OnInit, output } from '@angular/core'
|
||||||
|
import { FormGroup } from '@angular/forms'
|
||||||
|
import { RouterModule } from '@angular/router'
|
||||||
|
import { ScreenService, ServerService } from '@app/core'
|
||||||
|
import { HeaderService } from '@app/header/header.service'
|
||||||
|
import { FormReactiveErrors, FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
|
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
|
||||||
|
import { AlertComponent } from '../../../shared/shared-main/common/alert.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-admin-save-bar',
|
||||||
|
styleUrls: [ './admin-save-bar.component.scss' ],
|
||||||
|
templateUrl: './admin-save-bar.component.html',
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
RouterModule,
|
||||||
|
ButtonComponent,
|
||||||
|
AlertComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminSaveBarComponent implements OnInit, OnDestroy {
|
||||||
|
private formReactiveService = inject(FormReactiveService)
|
||||||
|
private server = inject(ServerService)
|
||||||
|
private headerService = inject(HeaderService)
|
||||||
|
private screenService = inject(ScreenService)
|
||||||
|
|
||||||
|
readonly title = input.required<string>()
|
||||||
|
readonly form = input.required<FormGroup>()
|
||||||
|
readonly formErrors = input.required<FormReactiveErrors>()
|
||||||
|
readonly inconsistentOptions = input<string>()
|
||||||
|
|
||||||
|
readonly save = output()
|
||||||
|
|
||||||
|
displayFormErrors = false
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
if (this.screenService.isInMobileView()) {
|
||||||
|
this.headerService.setSearchHidden(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy () {
|
||||||
|
this.headerService.setSearchHidden(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
isUpdateAllowed () {
|
||||||
|
return this.server.getHTMLConfig().webadmin.configuration.edition.allowed === true
|
||||||
|
}
|
||||||
|
|
||||||
|
canUpdate () {
|
||||||
|
if (!this.isUpdateAllowed()) return false
|
||||||
|
if (this.inconsistentOptions()) return false
|
||||||
|
|
||||||
|
return this.form().dirty
|
||||||
|
}
|
||||||
|
|
||||||
|
grabAllErrors () {
|
||||||
|
return this.formReactiveService.grabAllErrors(this.formErrors())
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave (event: Event) {
|
||||||
|
this.displayFormErrors = false
|
||||||
|
|
||||||
|
if (this.form().valid) {
|
||||||
|
this.save.emit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
this.displayFormErrors = true
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,70 +0,0 @@
|
||||||
import { catchError } from 'rxjs/operators'
|
|
||||||
import { HttpClient } from '@angular/common/http'
|
|
||||||
import { Injectable, inject } from '@angular/core'
|
|
||||||
import { RestExtractor } from '@app/core'
|
|
||||||
import { CustomConfig } from '@peertube/peertube-models'
|
|
||||||
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
|
|
||||||
import { environment } from '../../../../environments/environment'
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ConfigService {
|
|
||||||
private authHttp = inject(HttpClient)
|
|
||||||
private restExtractor = inject(RestExtractor)
|
|
||||||
|
|
||||||
private static BASE_APPLICATION_URL = environment.apiUrl + '/api/v1/config'
|
|
||||||
|
|
||||||
videoQuotaOptions: SelectOptionsItem[] = []
|
|
||||||
videoQuotaDailyOptions: SelectOptionsItem[] = []
|
|
||||||
transcodingThreadOptions: SelectOptionsItem[] = []
|
|
||||||
|
|
||||||
constructor () {
|
|
||||||
this.videoQuotaOptions = [
|
|
||||||
{ id: -1, label: $localize`Unlimited` },
|
|
||||||
{ id: 0, label: $localize`None - no upload possible` },
|
|
||||||
{ id: 100 * 1024 * 1024, label: $localize`100MB` },
|
|
||||||
{ id: 500 * 1024 * 1024, label: $localize`500MB` },
|
|
||||||
{ id: 1024 * 1024 * 1024, label: $localize`1GB` },
|
|
||||||
{ id: 5 * 1024 * 1024 * 1024, label: $localize`5GB` },
|
|
||||||
{ id: 20 * 1024 * 1024 * 1024, label: $localize`20GB` },
|
|
||||||
{ id: 50 * 1024 * 1024 * 1024, label: $localize`50GB` },
|
|
||||||
{ id: 100 * 1024 * 1024 * 1024, label: $localize`100GB` },
|
|
||||||
{ id: 200 * 1024 * 1024 * 1024, label: $localize`200GB` },
|
|
||||||
{ id: 500 * 1024 * 1024 * 1024, label: $localize`500GB` }
|
|
||||||
]
|
|
||||||
|
|
||||||
this.videoQuotaDailyOptions = [
|
|
||||||
{ id: -1, label: $localize`Unlimited` },
|
|
||||||
{ id: 0, label: $localize`None - no upload possible` },
|
|
||||||
{ id: 10 * 1024 * 1024, label: $localize`10MB` },
|
|
||||||
{ id: 50 * 1024 * 1024, label: $localize`50MB` },
|
|
||||||
{ id: 100 * 1024 * 1024, label: $localize`100MB` },
|
|
||||||
{ id: 500 * 1024 * 1024, label: $localize`500MB` },
|
|
||||||
{ id: 2 * 1024 * 1024 * 1024, label: $localize`2GB` },
|
|
||||||
{ id: 5 * 1024 * 1024 * 1024, label: $localize`5GB` },
|
|
||||||
{ id: 10 * 1024 * 1024 * 1024, label: $localize`10GB` },
|
|
||||||
{ id: 20 * 1024 * 1024 * 1024, label: $localize`20GB` },
|
|
||||||
{ id: 50 * 1024 * 1024 * 1024, label: $localize`50GB` }
|
|
||||||
]
|
|
||||||
|
|
||||||
this.transcodingThreadOptions = [
|
|
||||||
{ id: 0, label: $localize`Auto (via ffmpeg)` },
|
|
||||||
{ id: 1, label: '1' },
|
|
||||||
{ id: 2, label: '2' },
|
|
||||||
{ id: 4, label: '4' },
|
|
||||||
{ id: 8, label: '8' },
|
|
||||||
{ id: 12, label: '12' },
|
|
||||||
{ id: 16, label: '16' },
|
|
||||||
{ id: 32, label: '32' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
getCustomConfig () {
|
|
||||||
return this.authHttp.get<CustomConfig>(ConfigService.BASE_APPLICATION_URL + '/custom')
|
|
||||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCustomConfig (data: CustomConfig) {
|
|
||||||
return this.authHttp.put<CustomConfig>(ConfigService.BASE_APPLICATION_URL + '/custom', data)
|
|
||||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@ import { NgClass, NgFor, NgIf, NgTemplateOutlet } from '@angular/common'
|
||||||
import { Component, OnInit, inject } from '@angular/core'
|
import { Component, OnInit, inject } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { Router, RouterLink } from '@angular/router'
|
import { Router, RouterLink } from '@angular/router'
|
||||||
import { ConfigService } from '@app/+admin/config/shared/config.service'
|
import { AdminConfigService } from '@app/+admin/config/shared/admin-config.service'
|
||||||
import { AuthService, Notifier, ScreenService, ServerService } from '@app/core'
|
import { AuthService, Notifier, ScreenService, ServerService } from '@app/core'
|
||||||
import {
|
import {
|
||||||
USER_CHANNEL_NAME_VALIDATOR,
|
USER_CHANNEL_NAME_VALIDATOR,
|
||||||
|
@ -54,7 +54,7 @@ import { UserPasswordComponent } from './user-password.component'
|
||||||
export class UserCreateComponent extends UserEdit implements OnInit {
|
export class UserCreateComponent extends UserEdit implements OnInit {
|
||||||
protected serverService = inject(ServerService)
|
protected serverService = inject(ServerService)
|
||||||
protected formReactiveService = inject(FormReactiveService)
|
protected formReactiveService = inject(FormReactiveService)
|
||||||
protected configService = inject(ConfigService)
|
protected configService = inject(AdminConfigService)
|
||||||
protected screenService = inject(ScreenService)
|
protected screenService = inject(ScreenService)
|
||||||
protected auth = inject(AuthService)
|
protected auth = inject(AuthService)
|
||||||
private router = inject(Router)
|
private router = inject(Router)
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { Directive, OnInit } from '@angular/core'
|
import { Directive, OnInit } from '@angular/core'
|
||||||
import { ConfigService } from '@app/+admin/config/shared/config.service'
|
import { AdminConfigService } from '@app/+admin/config/shared/admin-config.service'
|
||||||
|
import { getVideoQuotaDailyOptions, getVideoQuotaOptions } from '@app/+admin/shared/user-quota-options'
|
||||||
import { AuthService, ScreenService, ServerService, User } from '@app/core'
|
import { AuthService, ScreenService, ServerService, User } from '@app/core'
|
||||||
|
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
||||||
import { peertubeTranslate, USER_ROLE_LABELS } from '@peertube/peertube-core-utils'
|
import { peertubeTranslate, USER_ROLE_LABELS } from '@peertube/peertube-core-utils'
|
||||||
import { HTMLServerConfig, UserAdminFlag, UserRole } from '@peertube/peertube-models'
|
import { HTMLServerConfig, UserAdminFlag, UserRole } from '@peertube/peertube-models'
|
||||||
import { SelectOptionsItem } from '../../../../../types/select-options-item.model'
|
import { SelectOptionsItem } from '../../../../../types/select-options-item.model'
|
||||||
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class UserEdit extends FormReactive implements OnInit {
|
export abstract class UserEdit extends FormReactive implements OnInit {
|
||||||
|
@ -18,7 +19,7 @@ export abstract class UserEdit extends FormReactive implements OnInit {
|
||||||
protected serverConfig: HTMLServerConfig
|
protected serverConfig: HTMLServerConfig
|
||||||
|
|
||||||
protected abstract serverService: ServerService
|
protected abstract serverService: ServerService
|
||||||
protected abstract configService: ConfigService
|
protected abstract configService: AdminConfigService
|
||||||
protected abstract screenService: ScreenService
|
protected abstract screenService: ScreenService
|
||||||
protected abstract auth: AuthService
|
protected abstract auth: AuthService
|
||||||
abstract isCreation (): boolean
|
abstract isCreation (): boolean
|
||||||
|
@ -88,7 +89,7 @@ export abstract class UserEdit extends FormReactive implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected buildQuotaOptions () {
|
protected buildQuotaOptions () {
|
||||||
this.videoQuotaOptions = this.configService.videoQuotaOptions
|
this.videoQuotaOptions = getVideoQuotaOptions()
|
||||||
this.videoQuotaDailyOptions = this.configService.videoQuotaDailyOptions
|
this.videoQuotaDailyOptions = getVideoQuotaDailyOptions()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { NgClass, NgFor, NgIf, NgTemplateOutlet } from '@angular/common'
|
||||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router'
|
import { ActivatedRoute, Router, RouterLink } from '@angular/router'
|
||||||
import { ConfigService } from '@app/+admin/config/shared/config.service'
|
import { AdminConfigService } from '@app/+admin/config/shared/admin-config.service'
|
||||||
import { AuthService, Notifier, ScreenService, ServerService, User, UserService } from '@app/core'
|
import { AuthService, Notifier, ScreenService, ServerService, User, UserService } from '@app/core'
|
||||||
import {
|
import {
|
||||||
USER_EMAIL_VALIDATOR,
|
USER_EMAIL_VALIDATOR,
|
||||||
|
@ -52,7 +52,7 @@ import { UserPasswordComponent } from './user-password.component'
|
||||||
export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
|
export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
|
||||||
protected formReactiveService = inject(FormReactiveService)
|
protected formReactiveService = inject(FormReactiveService)
|
||||||
protected serverService = inject(ServerService)
|
protected serverService = inject(ServerService)
|
||||||
protected configService = inject(ConfigService)
|
protected configService = inject(AdminConfigService)
|
||||||
protected screenService = inject(ScreenService)
|
protected screenService = inject(ScreenService)
|
||||||
protected auth = inject(AuthService)
|
protected auth = inject(AuthService)
|
||||||
private route = inject(ActivatedRoute)
|
private route = inject(ActivatedRoute)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Route, Routes, UrlSegment } from '@angular/router'
|
import { Route, Routes, UrlSegment } from '@angular/router'
|
||||||
import { configRoutes, EditConfigurationService } from '@app/+admin/config'
|
import { configRoutes } from '@app/+admin/config'
|
||||||
import { moderationRoutes } from '@app/+admin/moderation/moderation.routes'
|
import { moderationRoutes } from '@app/+admin/moderation/moderation.routes'
|
||||||
import { pluginsRoutes } from '@app/+admin/plugins/plugins.routes'
|
import { pluginsRoutes } from '@app/+admin/plugins/plugins.routes'
|
||||||
import { DebugService, JobService, LogsService, RunnerService, systemRoutes } from '@app/+admin/system'
|
import { DebugService, JobService, LogsService, RunnerService, systemRoutes } from '@app/+admin/system'
|
||||||
|
@ -21,7 +21,7 @@ import { WatchedWordsListService } from '@app/shared/standalone-watched-words/wa
|
||||||
import { AdminModerationComponent } from './admin-moderation.component'
|
import { AdminModerationComponent } from './admin-moderation.component'
|
||||||
import { AdminOverviewComponent } from './admin-overview.component'
|
import { AdminOverviewComponent } from './admin-overview.component'
|
||||||
import { AdminSettingsComponent } from './admin-settings.component'
|
import { AdminSettingsComponent } from './admin-settings.component'
|
||||||
import { ConfigService } from './config/shared/config.service'
|
import { AdminConfigService } from './config/shared/admin-config.service'
|
||||||
import { followsRoutes } from './follows'
|
import { followsRoutes } from './follows'
|
||||||
import { AdminRegistrationService } from './moderation/registration-list'
|
import { AdminRegistrationService } from './moderation/registration-list'
|
||||||
import { overviewRoutes, VideoAdminService } from './overview'
|
import { overviewRoutes, VideoAdminService } from './overview'
|
||||||
|
@ -37,7 +37,6 @@ const commonConfig = {
|
||||||
CustomMarkupService,
|
CustomMarkupService,
|
||||||
CustomPageService,
|
CustomPageService,
|
||||||
DebugService,
|
DebugService,
|
||||||
EditConfigurationService,
|
|
||||||
InstanceFollowService,
|
InstanceFollowService,
|
||||||
JobService,
|
JobService,
|
||||||
LogsService,
|
LogsService,
|
||||||
|
@ -48,7 +47,7 @@ const commonConfig = {
|
||||||
VideoAdminService,
|
VideoAdminService,
|
||||||
VideoBlockService,
|
VideoBlockService,
|
||||||
VideoCommentService,
|
VideoCommentService,
|
||||||
ConfigService,
|
AdminConfigService,
|
||||||
AbuseService,
|
AbuseService,
|
||||||
DynamicElementService,
|
DynamicElementService,
|
||||||
FindInBulkService,
|
FindInBulkService,
|
||||||
|
|
33
client/src/app/+admin/shared/user-quota-options.ts
Normal file
33
client/src/app/+admin/shared/user-quota-options.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { SelectOptionsItem } from '../../../types/select-options-item.model'
|
||||||
|
|
||||||
|
export function getVideoQuotaOptions (): SelectOptionsItem[] {
|
||||||
|
return [
|
||||||
|
{ id: -1, label: $localize`Unlimited` },
|
||||||
|
{ id: 0, label: $localize`None - no upload possible` },
|
||||||
|
{ id: 100 * 1024 * 1024, label: $localize`100MB` },
|
||||||
|
{ id: 500 * 1024 * 1024, label: $localize`500MB` },
|
||||||
|
{ id: 1024 * 1024 * 1024, label: $localize`1GB` },
|
||||||
|
{ id: 5 * 1024 * 1024 * 1024, label: $localize`5GB` },
|
||||||
|
{ id: 20 * 1024 * 1024 * 1024, label: $localize`20GB` },
|
||||||
|
{ id: 50 * 1024 * 1024 * 1024, label: $localize`50GB` },
|
||||||
|
{ id: 100 * 1024 * 1024 * 1024, label: $localize`100GB` },
|
||||||
|
{ id: 200 * 1024 * 1024 * 1024, label: $localize`200GB` },
|
||||||
|
{ id: 500 * 1024 * 1024 * 1024, label: $localize`500GB` }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVideoQuotaDailyOptions (): SelectOptionsItem[] {
|
||||||
|
return [
|
||||||
|
{ id: -1, label: $localize`Unlimited` },
|
||||||
|
{ id: 0, label: $localize`None - no upload possible` },
|
||||||
|
{ id: 10 * 1024 * 1024, label: $localize`10MB` },
|
||||||
|
{ id: 50 * 1024 * 1024, label: $localize`50MB` },
|
||||||
|
{ id: 100 * 1024 * 1024, label: $localize`100MB` },
|
||||||
|
{ id: 500 * 1024 * 1024, label: $localize`500MB` },
|
||||||
|
{ id: 2 * 1024 * 1024 * 1024, label: $localize`2GB` },
|
||||||
|
{ id: 5 * 1024 * 1024 * 1024, label: $localize`5GB` },
|
||||||
|
{ id: 10 * 1024 * 1024 * 1024, label: $localize`10GB` },
|
||||||
|
{ id: 20 * 1024 * 1024 * 1024, label: $localize`20GB` },
|
||||||
|
{ id: 50 * 1024 * 1024 * 1024, label: $localize`50GB` }
|
||||||
|
]
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ import { NgClass, NgFor, NgIf } from '@angular/common'
|
||||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||||
import { FormArray, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'
|
import { FormArray, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'
|
||||||
import { VIDEO_CHAPTERS_ARRAY_VALIDATOR, VIDEO_CHAPTER_TITLE_VALIDATOR } from '@app/shared/form-validators/video-chapter-validators'
|
import { VIDEO_CHAPTERS_ARRAY_VALIDATOR, VIDEO_CHAPTER_TITLE_VALIDATOR } from '@app/shared/form-validators/video-chapter-validators'
|
||||||
import { FormReactiveErrors, FormReactiveService, FormReactiveValidationMessages } from '@app/shared/shared-forms/form-reactive.service'
|
import { FormReactiveErrors, FormReactiveService, FormReactiveMessages } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
import { FormValidatorService } from '@app/shared/shared-forms/form-validator.service'
|
import { FormValidatorService } from '@app/shared/shared-forms/form-validator.service'
|
||||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||||
import debug from 'debug'
|
import debug from 'debug'
|
||||||
|
@ -52,7 +52,7 @@ export class VideoChaptersComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
form: FormGroup<Form>
|
form: FormGroup<Form>
|
||||||
formErrors: FormReactiveErrors & { chapters?: { title: string }[] } = {}
|
formErrors: FormReactiveErrors & { chapters?: { title: string }[] } = {}
|
||||||
validationMessages: FormReactiveValidationMessages = {}
|
validationMessages: FormReactiveMessages = {}
|
||||||
|
|
||||||
videoEdit: VideoEdit
|
videoEdit: VideoEdit
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angul
|
||||||
import { ServerService } from '@app/core'
|
import { ServerService } from '@app/core'
|
||||||
import { BuildFormArgument } from '@app/shared/form-validators/form-validator.model'
|
import { BuildFormArgument } from '@app/shared/form-validators/form-validator.model'
|
||||||
import { VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR } from '@app/shared/form-validators/video-validators'
|
import { VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR } from '@app/shared/form-validators/video-validators'
|
||||||
import { FormReactiveErrors, FormReactiveService, FormReactiveValidationMessages } from '@app/shared/shared-forms/form-reactive.service'
|
import { FormReactiveErrors, FormReactiveService, FormReactiveMessages } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
import { HTMLServerConfig } from '@peertube/peertube-models'
|
||||||
import debug from 'debug'
|
import debug from 'debug'
|
||||||
import { DatePickerModule } from 'primeng/datepicker'
|
import { DatePickerModule } from 'primeng/datepicker'
|
||||||
|
@ -44,7 +44,7 @@ export class VideoCustomizationComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
form: FormGroup<Form>
|
form: FormGroup<Form>
|
||||||
formErrors: FormReactiveErrors = {}
|
formErrors: FormReactiveErrors = {}
|
||||||
validationMessages: FormReactiveValidationMessages = {}
|
validationMessages: FormReactiveMessages = {}
|
||||||
|
|
||||||
videoEdit: VideoEdit
|
videoEdit: VideoEdit
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { ServerService } from '@app/core'
|
import { ServerService } from '@app/core'
|
||||||
import { BuildFormArgument } from '@app/shared/form-validators/form-validator.model'
|
import { BuildFormArgument } from '@app/shared/form-validators/form-validator.model'
|
||||||
import { FormReactiveErrors, FormReactiveService, FormReactiveValidationMessages } from '@app/shared/shared-forms/form-reactive.service'
|
import { FormReactiveErrors, FormReactiveService, FormReactiveMessages } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||||
import {
|
import {
|
||||||
|
@ -68,7 +68,7 @@ export class VideoLiveSettingsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
form: FormGroup<Form>
|
form: FormGroup<Form>
|
||||||
formErrors: FormReactiveErrors = {}
|
formErrors: FormReactiveErrors = {}
|
||||||
validationMessages: FormReactiveValidationMessages = {}
|
validationMessages: FormReactiveMessages = {}
|
||||||
|
|
||||||
videoEdit: VideoEdit
|
videoEdit: VideoEdit
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {
|
||||||
VIDEO_TAGS_ARRAY_VALIDATOR
|
VIDEO_TAGS_ARRAY_VALIDATOR
|
||||||
} from '@app/shared/form-validators/video-validators'
|
} from '@app/shared/form-validators/video-validators'
|
||||||
import { DynamicFormFieldComponent } from '@app/shared/shared-forms/dynamic-form-field.component'
|
import { DynamicFormFieldComponent } from '@app/shared/shared-forms/dynamic-form-field.component'
|
||||||
import { FormReactiveErrors, FormReactiveService, FormReactiveValidationMessages } from '@app/shared/shared-forms/form-reactive.service'
|
import { FormReactiveErrors, FormReactiveService, FormReactiveMessages } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
import { FormValidatorService } from '@app/shared/shared-forms/form-validator.service'
|
import { FormValidatorService } from '@app/shared/shared-forms/form-validator.service'
|
||||||
import { InputTextComponent } from '@app/shared/shared-forms/input-text.component'
|
import { InputTextComponent } from '@app/shared/shared-forms/input-text.component'
|
||||||
import { MarkdownTextareaComponent } from '@app/shared/shared-forms/markdown-textarea.component'
|
import { MarkdownTextareaComponent } from '@app/shared/shared-forms/markdown-textarea.component'
|
||||||
|
@ -120,7 +120,7 @@ export class VideoMainInfoComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
form: FormGroup<Form>
|
form: FormGroup<Form>
|
||||||
formErrors: FormReactiveErrors = {}
|
formErrors: FormReactiveErrors = {}
|
||||||
validationMessages: FormReactiveValidationMessages = {}
|
validationMessages: FormReactiveMessages = {}
|
||||||
|
|
||||||
forbidScheduledPublication: boolean
|
forbidScheduledPublication: boolean
|
||||||
hideWaitTranscoding: boolean
|
hideWaitTranscoding: boolean
|
||||||
|
@ -337,7 +337,7 @@ export class VideoMainInfoComponent implements OnInit, OnDestroy {
|
||||||
const { pluginData } = this.videoEdit.toCommonFormPatch()
|
const { pluginData } = this.videoEdit.toCommonFormPatch()
|
||||||
|
|
||||||
const pluginObj: { [id: string]: BuildFormValidator } = {}
|
const pluginObj: { [id: string]: BuildFormValidator } = {}
|
||||||
const pluginValidationMessages: FormReactiveValidationMessages = {}
|
const pluginValidationMessages: FormReactiveMessages = {}
|
||||||
const pluginFormErrors: FormReactiveErrors = {}
|
const pluginFormErrors: FormReactiveErrors = {}
|
||||||
const pluginDefaults: Record<string, string | boolean> = {}
|
const pluginDefaults: Record<string, string | boolean> = {}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { RouterLink } from '@angular/router'
|
||||||
import { ServerService } from '@app/core'
|
import { ServerService } from '@app/core'
|
||||||
import { BuildFormArgument } from '@app/shared/form-validators/form-validator.model'
|
import { BuildFormArgument } from '@app/shared/form-validators/form-validator.model'
|
||||||
import { VIDEO_NSFW_SUMMARY_VALIDATOR } from '@app/shared/form-validators/video-validators'
|
import { VIDEO_NSFW_SUMMARY_VALIDATOR } from '@app/shared/form-validators/video-validators'
|
||||||
import { FormReactiveErrors, FormReactiveService, FormReactiveValidationMessages } from '@app/shared/shared-forms/form-reactive.service'
|
import { FormReactiveErrors, FormReactiveService, FormReactiveMessages } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
import { HTMLServerConfig, VideoCommentPolicyType, VideoConstant } from '@peertube/peertube-models'
|
import { HTMLServerConfig, VideoCommentPolicyType, VideoConstant } from '@peertube/peertube-models'
|
||||||
import debug from 'debug'
|
import debug from 'debug'
|
||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
|
@ -51,7 +51,7 @@ export class VideoModerationComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
form: FormGroup<Form>
|
form: FormGroup<Form>
|
||||||
formErrors: FormReactiveErrors = {}
|
formErrors: FormReactiveErrors = {}
|
||||||
validationMessages: FormReactiveValidationMessages = {}
|
validationMessages: FormReactiveMessages = {}
|
||||||
|
|
||||||
commentPolicies: VideoConstant<VideoCommentPolicyType>[] = []
|
commentPolicies: VideoConstant<VideoCommentPolicyType>[] = []
|
||||||
serverConfig: HTMLServerConfig
|
serverConfig: HTMLServerConfig
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Component, inject, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
||||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { ServerService } from '@app/core'
|
import { ServerService } from '@app/core'
|
||||||
import { BuildFormArgument } from '@app/shared/form-validators/form-validator.model'
|
import { BuildFormArgument } from '@app/shared/form-validators/form-validator.model'
|
||||||
import { FormReactiveErrors, FormReactiveService, FormReactiveValidationMessages } from '@app/shared/shared-forms/form-reactive.service'
|
import { FormReactiveErrors, FormReactiveService, FormReactiveMessages } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
import debug from 'debug'
|
import debug from 'debug'
|
||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { ReactiveFileComponent } from '../../../shared/shared-forms/reactive-file.component'
|
import { ReactiveFileComponent } from '../../../shared/shared-forms/reactive-file.component'
|
||||||
|
@ -46,7 +46,7 @@ export class VideoReplaceFileComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
form: FormGroup<Form>
|
form: FormGroup<Form>
|
||||||
formErrors: FormReactiveErrors = {}
|
formErrors: FormReactiveErrors = {}
|
||||||
validationMessages: FormReactiveValidationMessages = {}
|
validationMessages: FormReactiveMessages = {}
|
||||||
|
|
||||||
videoEdit: VideoEdit
|
videoEdit: VideoEdit
|
||||||
|
|
||||||
|
|
|
@ -51,9 +51,9 @@ type Card = { label: string, value: string | number, moreInfo?: string, help?: s
|
||||||
|
|
||||||
const isBarGraph = (graphId: ActiveGraphId): graphId is BarGraphs => BAR_GRAPHS.some(graph => graph === graphId)
|
const isBarGraph = (graphId: ActiveGraphId): graphId is BarGraphs => BAR_GRAPHS.some(graph => graph === graphId)
|
||||||
|
|
||||||
ChartJSDefaults.backgroundColor = getComputedStyle(document.body).getPropertyValue('--bg')
|
ChartJSDefaults.backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--bg')
|
||||||
ChartJSDefaults.borderColor = getComputedStyle(document.body).getPropertyValue('--bg-secondary-500')
|
ChartJSDefaults.borderColor = getComputedStyle(document.documentElement).getPropertyValue('--bg-secondary-500')
|
||||||
ChartJSDefaults.color = getComputedStyle(document.body).getPropertyValue('--fg')
|
ChartJSDefaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg')
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: './video-stats.component.html',
|
templateUrl: './video-stats.component.html',
|
||||||
|
@ -654,7 +654,7 @@ export class VideoStatsComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildChartColor () {
|
private buildChartColor () {
|
||||||
return getComputedStyle(document.body).getPropertyValue('--border-primary')
|
return getComputedStyle(document.documentElement).getPropertyValue('--border-primary')
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatXTick (options: {
|
private formatXTick (options: {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { NgFor, NgIf } from '@angular/common'
|
||||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { ServerService } from '@app/core'
|
import { ServerService } from '@app/core'
|
||||||
import { FormReactiveErrors, FormReactiveService, FormReactiveValidationMessages } from '@app/shared/shared-forms/form-reactive.service'
|
import { FormReactiveErrors, FormReactiveService, FormReactiveMessages } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
import { ReactiveFileComponent } from '@app/shared/shared-forms/reactive-file.component'
|
import { ReactiveFileComponent } from '@app/shared/shared-forms/reactive-file.component'
|
||||||
import { TimestampInputComponent } from '@app/shared/shared-forms/timestamp-input.component'
|
import { TimestampInputComponent } from '@app/shared/shared-forms/timestamp-input.component'
|
||||||
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
|
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
|
||||||
|
@ -49,7 +49,7 @@ export class VideoStudioEditComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
form: FormGroup<Form>
|
form: FormGroup<Form>
|
||||||
formErrors: FormReactiveErrors = {}
|
formErrors: FormReactiveErrors = {}
|
||||||
validationMessages: FormReactiveValidationMessages = {}
|
validationMessages: FormReactiveMessages = {}
|
||||||
|
|
||||||
isRunningEdit = false
|
isRunningEdit = false
|
||||||
|
|
||||||
|
|
|
@ -1,141 +0,0 @@
|
||||||
<div class="menu">
|
|
||||||
<h1 i18n>MANAGE MY VIDEO</h1>
|
|
||||||
|
|
||||||
<ul class="ul-unstyle">
|
|
||||||
<li>
|
|
||||||
<a routerLink="." queryParamsHandling="merge" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">
|
|
||||||
<div class="global-icon-wrapper">
|
|
||||||
<my-global-icon iconName="film"></my-global-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span i18n>Main information</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li *ngIf="getVideo().isLive">
|
|
||||||
<a routerLink="live-settings" queryParamsHandling="merge" routerLinkActive="active">
|
|
||||||
<div class="global-icon-wrapper">
|
|
||||||
<my-global-icon iconName="live"></my-global-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span i18n>Live settings</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<div class="separator">
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a routerLink="customization" queryParamsHandling="merge" routerLinkActive="active">
|
|
||||||
<div class="global-icon-wrapper">
|
|
||||||
<my-global-icon iconName="cog"></my-global-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span i18n>Customization</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a routerLink="moderation" queryParamsHandling="merge" routerLinkActive="active">
|
|
||||||
<div class="global-icon-wrapper">
|
|
||||||
<my-global-icon iconName="moderation"></my-global-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span i18n>Moderation</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
@if (!getVideo().isLive) {
|
|
||||||
<li>
|
|
||||||
<a routerLink="captions" queryParamsHandling="merge" routerLinkActive="active">
|
|
||||||
<div class="global-icon-wrapper">
|
|
||||||
<my-global-icon iconName="captions"></my-global-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span i18n>Captions</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a routerLink="chapters" queryParamsHandling="merge" routerLinkActive="active">
|
|
||||||
<div class="global-icon-wrapper">
|
|
||||||
<my-global-icon iconName="chapters"></my-global-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span i18n>Chapters</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<div class="separator">
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<ng-template #iconStudio>
|
|
||||||
<div class="global-icon-wrapper">
|
|
||||||
<my-global-icon iconName="studio"></my-global-icon>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template #labelStudio>
|
|
||||||
<span i18n>Studio</span>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
@if (studioUnavailable()) {
|
|
||||||
<my-unavailable-menu-entry [help]="studioUnavailable()">
|
|
||||||
<span class="icon" *ngTemplateOutlet="iconStudio"></span>
|
|
||||||
<span class="label" *ngTemplateOutlet="labelStudio"></span>
|
|
||||||
</my-unavailable-menu-entry>
|
|
||||||
} @else {
|
|
||||||
<a routerLink="studio" queryParamsHandling="merge" routerLinkActive="active">
|
|
||||||
<ng-container *ngTemplateOutlet="iconStudio"></ng-container>
|
|
||||||
<ng-container *ngTemplateOutlet="labelStudio"></ng-container>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<ng-template #iconReplaceFile>
|
|
||||||
<div class="global-icon-wrapper">
|
|
||||||
<my-global-icon iconName="upload"></my-global-icon>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template #labelReplaceFile>
|
|
||||||
<span i18n>Replace file</span>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
@if (replaceFileUnavailable()) {
|
|
||||||
<my-unavailable-menu-entry [help]="replaceFileUnavailable()">
|
|
||||||
<span class="icon" *ngTemplateOutlet="iconReplaceFile"></span>
|
|
||||||
<span class="label" *ngTemplateOutlet="labelReplaceFile"></span>
|
|
||||||
</my-unavailable-menu-entry>
|
|
||||||
} @else {
|
|
||||||
<a routerLink="replace-file" queryParamsHandling="merge" routerLinkActive="active">
|
|
||||||
<ng-container *ngTemplateOutlet="iconReplaceFile"></ng-container>
|
|
||||||
<ng-container *ngTemplateOutlet="labelReplaceFile"></ng-container>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (canWatch()) {
|
|
||||||
<div class="separator">
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a routerLink="stats" queryParamsHandling="merge" routerLinkActive="active">
|
|
||||||
<div class="global-icon-wrapper">
|
|
||||||
<my-global-icon iconName="stats"></my-global-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span i18n>Statistics</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="menu-placeholder"></div>
|
|
|
@ -1,27 +1,17 @@
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { booleanAttribute, Component, inject, input, OnInit } from '@angular/core'
|
import { booleanAttribute, Component, inject, input, OnInit } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { RouterModule } from '@angular/router'
|
|
||||||
import { ServerService } from '@app/core'
|
import { ServerService } from '@app/core'
|
||||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
import { LateralMenuComponent, LateralMenuConfig } from '@app/shared/shared-main/menu/lateral-menu.component'
|
||||||
import { GlobalIconComponent } from '../../shared/shared-icons/global-icon.component'
|
|
||||||
import { getReplaceFileUnavailability, getStudioUnavailability } from './common/unavailable-features'
|
import { getReplaceFileUnavailability, getStudioUnavailability } from './common/unavailable-features'
|
||||||
import { VideoEdit } from './common/video-edit.model'
|
import { VideoEdit } from './common/video-edit.model'
|
||||||
import { UnavailableMenuEntryComponent } from './unavailable-menu-entry.component'
|
|
||||||
import { VideoManageController } from './video-manage-controller.service'
|
import { VideoManageController } from './video-manage-controller.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-manage-menu',
|
selector: 'my-video-manage-menu',
|
||||||
styleUrls: [ './video-manage-menu.component.scss' ],
|
template: '<my-lateral-menu [config]="menuConfig" />',
|
||||||
templateUrl: './video-manage-menu.component.html',
|
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
RouterModule,
|
LateralMenuComponent
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
NgbTooltipModule,
|
|
||||||
GlobalIconComponent,
|
|
||||||
UnavailableMenuEntryComponent
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class VideoManageMenuComponent implements OnInit {
|
export class VideoManageMenuComponent implements OnInit {
|
||||||
|
@ -30,6 +20,8 @@ export class VideoManageMenuComponent implements OnInit {
|
||||||
|
|
||||||
readonly canWatch = input.required<boolean, string | boolean>({ transform: booleanAttribute })
|
readonly canWatch = input.required<boolean, string | boolean>({ transform: booleanAttribute })
|
||||||
|
|
||||||
|
menuConfig: LateralMenuConfig
|
||||||
|
|
||||||
private videoEdit: VideoEdit
|
private videoEdit: VideoEdit
|
||||||
private replaceFileEnabled: boolean
|
private replaceFileEnabled: boolean
|
||||||
private studioEnabled: boolean
|
private studioEnabled: boolean
|
||||||
|
@ -43,6 +35,89 @@ export class VideoManageMenuComponent implements OnInit {
|
||||||
|
|
||||||
const { videoEdit } = this.manageController.getStore()
|
const { videoEdit } = this.manageController.getStore()
|
||||||
this.videoEdit = videoEdit
|
this.videoEdit = videoEdit
|
||||||
|
|
||||||
|
this.menuConfig = {
|
||||||
|
title: $localize``,
|
||||||
|
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
label: $localize`Main information`,
|
||||||
|
routerLinkActiveOptions: { exact: true },
|
||||||
|
icon: 'film',
|
||||||
|
routerLink: '.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
isDisplayed: () => this.getVideo().isLive,
|
||||||
|
label: $localize`Live settings`,
|
||||||
|
icon: 'live',
|
||||||
|
routerLink: 'live-settings'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
label: $localize`Customization`,
|
||||||
|
icon: 'cog',
|
||||||
|
routerLink: 'customization'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
label: $localize`Moderation`,
|
||||||
|
icon: 'moderation',
|
||||||
|
routerLink: 'moderation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
isDisplayed: () => !this.getVideo().isLive,
|
||||||
|
label: $localize`Captions`,
|
||||||
|
icon: 'captions',
|
||||||
|
routerLink: 'captions'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
isDisplayed: () => !this.getVideo().isLive,
|
||||||
|
label: $localize`Chapters`,
|
||||||
|
icon: 'chapters',
|
||||||
|
routerLink: 'chapters'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
label: $localize`Studio`,
|
||||||
|
icon: 'studio',
|
||||||
|
routerLink: 'studio',
|
||||||
|
unavailableText: () => this.studioUnavailable()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
label: $localize`Replace file`,
|
||||||
|
icon: 'upload',
|
||||||
|
routerLink: 'replace-file',
|
||||||
|
unavailableText: () => this.replaceFileUnavailable()
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
isDisplayed: () => this.canWatch(),
|
||||||
|
label: $localize`Statistics`,
|
||||||
|
icon: 'stats',
|
||||||
|
routerLink: 'stats'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideo () {
|
getVideo () {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { Observable, of, Subject } from 'rxjs'
|
|
||||||
import { first, map, share, shareReplay, switchMap, tap } from 'rxjs/operators'
|
|
||||||
import { HttpClient } from '@angular/common/http'
|
import { HttpClient } from '@angular/common/http'
|
||||||
import { Injectable, LOCALE_ID, inject } from '@angular/core'
|
import { inject, Injectable, LOCALE_ID } from '@angular/core'
|
||||||
import { getDevLocale, isOnDevLocale } from '@app/helpers'
|
import { getDevLocale, isOnDevLocale } from '@app/helpers'
|
||||||
import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@peertube/peertube-core-utils'
|
import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@peertube/peertube-core-utils'
|
||||||
import {
|
import {
|
||||||
|
@ -14,6 +12,8 @@ import {
|
||||||
VideoPrivacyType
|
VideoPrivacyType
|
||||||
} from '@peertube/peertube-models'
|
} from '@peertube/peertube-models'
|
||||||
import { logger } from '@root-helpers/logger'
|
import { logger } from '@root-helpers/logger'
|
||||||
|
import { Observable, of, Subject } from 'rxjs'
|
||||||
|
import { first, map, share, shareReplay, switchMap, tap } from 'rxjs/operators'
|
||||||
import { environment } from '../../../environments/environment'
|
import { environment } from '../../../environments/environment'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -37,8 +37,6 @@ export class ServerService {
|
||||||
private videoLanguagesObservable: Observable<VideoConstant<string>[]>
|
private videoLanguagesObservable: Observable<VideoConstant<string>[]>
|
||||||
private configObservable: Observable<ServerConfig>
|
private configObservable: Observable<ServerConfig>
|
||||||
|
|
||||||
private configReset = false
|
|
||||||
|
|
||||||
private configLoaded = false
|
private configLoaded = false
|
||||||
private config: ServerConfig
|
private config: ServerConfig
|
||||||
private htmlConfig: HTMLServerConfig
|
private htmlConfig: HTMLServerConfig
|
||||||
|
@ -68,13 +66,14 @@ export class ServerService {
|
||||||
|
|
||||||
resetConfig () {
|
resetConfig () {
|
||||||
this.configLoaded = false
|
this.configLoaded = false
|
||||||
this.configReset = true
|
|
||||||
|
|
||||||
// Notify config update
|
// Notify config update
|
||||||
return this.getConfig()
|
return this.getConfig({ isReset: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
getConfig () {
|
getConfig (options: {
|
||||||
|
isReset?: boolean
|
||||||
|
} = {}) {
|
||||||
if (this.configLoaded) return of(this.config)
|
if (this.configLoaded) return of(this.config)
|
||||||
|
|
||||||
if (!this.configObservable) {
|
if (!this.configObservable) {
|
||||||
|
@ -86,9 +85,8 @@ export class ServerService {
|
||||||
this.configLoaded = true
|
this.configLoaded = true
|
||||||
}),
|
}),
|
||||||
tap(config => {
|
tap(config => {
|
||||||
if (this.configReset) {
|
if (options.isReset) {
|
||||||
this.configReloaded.next(config)
|
this.configReloaded.next(config)
|
||||||
this.configReset = false
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
share()
|
share()
|
||||||
|
|
|
@ -179,7 +179,7 @@ export default {
|
||||||
overlay: {
|
overlay: {
|
||||||
select: {
|
select: {
|
||||||
background: 'var(--bg)',
|
background: 'var(--bg)',
|
||||||
borderColor: 'var---input-border-color)',
|
borderColor: 'var(--input-border-color)',
|
||||||
color: 'var(--fg)'
|
color: 'var(--fg)'
|
||||||
},
|
},
|
||||||
popover: {
|
popover: {
|
||||||
|
|
34
client/src/app/core/theme/primeng/components/colorpicker.ts
Normal file
34
client/src/app/core/theme/primeng/components/colorpicker.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { ColorPickerDesignTokens } from '@primeng/themes/types/colorpicker'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
root: {
|
||||||
|
transitionDuration: '{transition.duration}'
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
width: '100%',
|
||||||
|
height: '1.5rem',
|
||||||
|
borderRadius: '{form.field.border.radius}',
|
||||||
|
focusRing: {
|
||||||
|
width: '{focus.ring.width}',
|
||||||
|
style: '{focus.ring.style}',
|
||||||
|
color: '{focus.ring.color}',
|
||||||
|
offset: '{focus.ring.offset}',
|
||||||
|
shadow: '{focus.ring.shadow}'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
panel: {
|
||||||
|
shadow: '{overlay.popover.shadow}',
|
||||||
|
borderRadius: '{overlay.popover.borderRadius}'
|
||||||
|
},
|
||||||
|
colorScheme: {
|
||||||
|
light: {
|
||||||
|
panel: {
|
||||||
|
background: 'var(--bg-secondary-400)',
|
||||||
|
borderColor: 'var(--bg-secondary-450)'
|
||||||
|
},
|
||||||
|
handle: {
|
||||||
|
color: 'var(--fg)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as ColorPickerDesignTokens
|
|
@ -2,6 +2,7 @@ import base from './base'
|
||||||
import autocomplete from './components/autocomplete'
|
import autocomplete from './components/autocomplete'
|
||||||
import checkbox from './components/checkbox'
|
import checkbox from './components/checkbox'
|
||||||
import chip from './components/chip'
|
import chip from './components/chip'
|
||||||
|
import colorpicker from './components/colorpicker'
|
||||||
import datatable from './components/datatable'
|
import datatable from './components/datatable'
|
||||||
import datepicker from './components/datepicker'
|
import datepicker from './components/datepicker'
|
||||||
import inputchips from './components/inputchips'
|
import inputchips from './components/inputchips'
|
||||||
|
@ -18,6 +19,7 @@ export const PTPrimeTheme = {
|
||||||
select,
|
select,
|
||||||
inputchips,
|
inputchips,
|
||||||
chip,
|
chip,
|
||||||
|
colorpicker,
|
||||||
datepicker,
|
datepicker,
|
||||||
inputtext,
|
inputtext,
|
||||||
toast,
|
toast,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Injectable, inject } from '@angular/core'
|
||||||
import { HTMLServerConfig, ServerConfig, ServerConfigTheme } from '@peertube/peertube-models'
|
import { HTMLServerConfig, ServerConfig, ServerConfigTheme } from '@peertube/peertube-models'
|
||||||
import { logger } from '@root-helpers/logger'
|
import { logger } from '@root-helpers/logger'
|
||||||
import { capitalizeFirstLetter } from '@root-helpers/string'
|
import { capitalizeFirstLetter } from '@root-helpers/string'
|
||||||
import { ThemeManager } from '@root-helpers/theme-manager'
|
import { ColorPaletteThemeConfig, ThemeCustomizationKey, ThemeManager } from '@root-helpers/theme-manager'
|
||||||
import { UserLocalStorageKeys } from '@root-helpers/users'
|
import { UserLocalStorageKeys } from '@root-helpers/users'
|
||||||
import { environment } from '../../../environments/environment'
|
import { environment } from '../../../environments/environment'
|
||||||
import { AuthService } from '../auth'
|
import { AuthService } from '../auth'
|
||||||
|
@ -72,6 +72,14 @@ export class ThemeService {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateColorPalette (config: ColorPaletteThemeConfig = this.serverConfig.theme) {
|
||||||
|
this.themeManager.injectColorPalette({ currentTheme: this.getCurrentThemeName(), config })
|
||||||
|
}
|
||||||
|
|
||||||
|
getCSSConfigValue (configKey: ThemeCustomizationKey) {
|
||||||
|
return this.themeManager.getCSSConfigValue(configKey)
|
||||||
|
}
|
||||||
|
|
||||||
private injectThemes (themes: ServerConfigTheme[], fromLocalStorage = false) {
|
private injectThemes (themes: ServerConfigTheme[], fromLocalStorage = false) {
|
||||||
this.themes = themes
|
this.themes = themes
|
||||||
|
|
||||||
|
@ -89,7 +97,7 @@ export class ThemeService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCurrentThemeName () {
|
getCurrentThemeName () {
|
||||||
if (this.themeFromLocalStorage) return this.themeFromLocalStorage.name
|
if (this.themeFromLocalStorage) return this.themeFromLocalStorage.name
|
||||||
|
|
||||||
const theme = this.auth.isLoggedIn()
|
const theme = this.auth.isLoggedIn()
|
||||||
|
@ -137,7 +145,7 @@ export class ThemeService {
|
||||||
this.localStorageService.removeItem(UserLocalStorageKeys.LAST_ACTIVE_THEME, false)
|
this.localStorageService.removeItem(UserLocalStorageKeys.LAST_ACTIVE_THEME, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.themeManager.injectCoreColorPalette()
|
this.themeManager.injectColorPalette({ currentTheme: currentThemeName, config: this.serverConfig.theme })
|
||||||
|
|
||||||
this.oldThemeName = currentThemeName
|
this.oldThemeName = currentThemeName
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
@use 'sass:math';
|
@use "sass:math";
|
||||||
@use '_variables' as *;
|
@use "_variables" as *;
|
||||||
@use '_mixins' as *;
|
@use "_mixins" as *;
|
||||||
@use '_button-mixins' as *;
|
@use "_button-mixins" as *;
|
||||||
@use '_bootstrap-variables' as *;
|
@use "_bootstrap-variables" as *;
|
||||||
|
|
||||||
.mobile-msg {
|
.mobile-msg {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -29,7 +29,8 @@
|
||||||
--co-logo-size: 34px;
|
--co-logo-size: 34px;
|
||||||
--co-root-padding: 1.5rem;
|
--co-root-padding: 1.5rem;
|
||||||
|
|
||||||
background-color: pvar(--bg);
|
color: pvar(--header-fg);
|
||||||
|
background-color: pvar(--header-bg);
|
||||||
|
|
||||||
padding: var(--co-root-padding);
|
padding: var(--co-root-padding);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -96,7 +97,7 @@ my-search-typeahead {
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
z-index: #{z('header') + 1} !important;
|
z-index: #{z("header") + 1} !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
|
@ -119,7 +120,7 @@ my-search-typeahead {
|
||||||
|
|
||||||
.logged-in-container {
|
.logged-in-container {
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
transition: all .1s ease-in-out;
|
transition: all 0.1s ease-in-out;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -173,7 +174,7 @@ my-actor-avatar {
|
||||||
@include margin-right(0.5rem);
|
@include margin-right(0.5rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.margin-button[theme=tertiary] {
|
.margin-button[theme="tertiary"] {
|
||||||
@include margin-right(5px);
|
@include margin-right(5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,7 +239,7 @@ my-actor-avatar {
|
||||||
}
|
}
|
||||||
|
|
||||||
.peertube-title {
|
.peertube-title {
|
||||||
@include margin-right(5px)
|
@include margin-right(5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.instance-name {
|
.instance-name {
|
||||||
|
|
|
@ -132,8 +132,8 @@ export class HeaderComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
this.getSearchHiddenSub = this.headerService.getSearchHiddenObs()
|
this.getSearchHiddenSub = this.headerService.getSearchHiddenObs()
|
||||||
.subscribe(hidden => {
|
.subscribe(hidden => {
|
||||||
if (hidden) document.body.classList.add('global-search-hidden')
|
if (hidden) document.documentElement.classList.add('global-search-hidden')
|
||||||
else document.body.classList.remove('global-search-hidden')
|
else document.documentElement.classList.remove('global-search-hidden')
|
||||||
|
|
||||||
this.searchHidden = hidden
|
this.searchHidden = hidden
|
||||||
})
|
})
|
||||||
|
@ -167,7 +167,7 @@ export class HeaderComponent implements OnInit, OnDestroy {
|
||||||
if (!isAndroid() && !isIphone()) return
|
if (!isAndroid() && !isIphone()) return
|
||||||
|
|
||||||
this.mobileMsg = true
|
this.mobileMsg = true
|
||||||
document.body.classList.add('mobile-app-msg')
|
document.documentElement.classList.add('mobile-app-msg')
|
||||||
|
|
||||||
const host = window.location.host
|
const host = window.location.host
|
||||||
const intentConfig = this.htmlConfig.client.openInApp.android.intent
|
const intentConfig = this.htmlConfig.client.openInApp.android.intent
|
||||||
|
@ -228,7 +228,7 @@ export class HeaderComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
hideMobileMsg () {
|
hideMobileMsg () {
|
||||||
this.mobileMsg = false
|
this.mobileMsg = false
|
||||||
document.body.classList.remove('mobile-app-msg')
|
document.documentElement.classList.remove('mobile-app-msg')
|
||||||
|
|
||||||
peertubeLocalStorage.setItem(HeaderComponent.LS_HIDE_MOBILE_MSG, 'true')
|
peertubeLocalStorage.setItem(HeaderComponent.LS_HIDE_MOBILE_MSG, 'true')
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
@use 'sass:math';
|
@use "sass:math";
|
||||||
@use '_variables' as *;
|
@use "_variables" as *;
|
||||||
@use '_mixins' as *;
|
@use "_mixins" as *;
|
||||||
@use '_button-mixins' as *;
|
@use "_button-mixins" as *;
|
||||||
|
|
||||||
.menu-container {
|
.menu-container {
|
||||||
--co-menu-x-padding: 1.5rem;
|
--co-menu-x-padding: 1.5rem;
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-top: 1.5rem;
|
padding-top: 1.5rem;
|
||||||
padding-bottom: 1.5rem;
|
padding-bottom: 1.5rem;
|
||||||
border-radius: 14px;
|
border-radius: pvar(--menu-border-radius);
|
||||||
background-color: pvar(--menu-bg);
|
background-color: pvar(--menu-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@
|
||||||
.collapsed .toggle-menu-container,
|
.collapsed .toggle-menu-container,
|
||||||
.about-top {
|
.about-top {
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
margin: 1rem var(--co-menu-x-padding);
|
margin: 1rem var(--co-menu-x-padding);
|
||||||
|
@ -123,7 +123,7 @@
|
||||||
|
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
transition: background-color .1s ease-in-out;
|
transition: background-color 0.1s ease-in-out;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
|
@ -245,7 +245,7 @@
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
content: '';
|
content: "";
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: z(overlay);
|
z-index: z(overlay);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { AsyncValidatorFn, ValidatorFn } from '@angular/forms'
|
import { AsyncValidatorFn, FormArray, FormControl, FormGroup, ValidatorFn } from '@angular/forms'
|
||||||
|
import { PartialDeep } from 'type-fest'
|
||||||
|
|
||||||
export type BuildFormValidator = {
|
export type BuildFormValidator = {
|
||||||
VALIDATORS: ValidatorFn[]
|
VALIDATORS: ValidatorFn[]
|
||||||
|
@ -11,6 +12,48 @@ export type BuildFormArgument = {
|
||||||
[id: string]: BuildFormValidator | BuildFormArgument
|
[id: string]: BuildFormValidator | BuildFormArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BuildFormDefaultValues = {
|
export type BuildFormArgumentTyped<Form> = ReplaceForm<Form, BuildFormValidator>
|
||||||
[name: string]: Blob | Date | boolean | number | string | string[] | BuildFormDefaultValues
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type FormDefault = {
|
||||||
|
[name: string]: Blob | Date | boolean | number | number[] | string | string[] | FormDefault
|
||||||
}
|
}
|
||||||
|
export type FormDefaultTyped<Form> = PartialDeep<UnwrapForm<Form>>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type FormReactiveMessages = {
|
||||||
|
[id: string]: { [name: string]: string } | FormReactiveMessages | FormReactiveMessages[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormReactiveMessagesTyped<Form> = Partial<ReplaceForm<Form, string>>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type FormReactiveErrors = { [id: string]: string | FormReactiveErrors | FormReactiveErrors[] }
|
||||||
|
export type FormReactiveErrorsTyped<Form> = Partial<ReplaceForm<Form, string>>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type UnwrapForm<Form> = {
|
||||||
|
[K in keyof Form]: _UnwrapForm<Form[K]>
|
||||||
|
}
|
||||||
|
|
||||||
|
type _UnwrapForm<T> = T extends FormGroup<infer U> ? UnwrapForm<U> :
|
||||||
|
T extends FormArray<infer U> ? _UnwrapForm<U>[] :
|
||||||
|
T extends FormControl<infer U> ? U
|
||||||
|
: never
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type ReplaceForm<Form, By> = {
|
||||||
|
[K in keyof Form]: _ReplaceForm<Form[K], By>
|
||||||
|
}
|
||||||
|
|
||||||
|
type _ReplaceForm<T, By> = T extends FormGroup<infer U> ? ReplaceForm<U, By> :
|
||||||
|
T extends FormArray<infer U> ? _ReplaceForm<U, By> :
|
||||||
|
T extends FormControl ? By
|
||||||
|
: never
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
@use '_variables' as *;
|
@use "_variables" as *;
|
||||||
@use '_mixins' as *;
|
@use "_mixins" as *;
|
||||||
@use '_form-mixins' as *;
|
@use "_form-mixins" as *;
|
||||||
|
|
||||||
input:not([type=submit]) {
|
input:not([type="submit"]) {
|
||||||
max-width: 340px;
|
max-width: 340px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@ -21,10 +21,6 @@ textarea {
|
||||||
@include peertube-select-container(340px);
|
@include peertube-select-container(340px);
|
||||||
}
|
}
|
||||||
|
|
||||||
my-peertube-checkbox + .label-small-info {
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
my-markdown-textarea {
|
my-markdown-textarea {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
import { Injectable, inject } from '@angular/core'
|
import { Injectable, inject } from '@angular/core'
|
||||||
import { AbstractControl, FormGroup, StatusChangeEvent } from '@angular/forms'
|
import { AbstractControl, FormGroup, StatusChangeEvent } from '@angular/forms'
|
||||||
import { filter, firstValueFrom } from 'rxjs'
|
import { filter, firstValueFrom } from 'rxjs'
|
||||||
import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
|
import { BuildFormArgument, FormDefault, FormReactiveErrors, FormReactiveMessages } from '../form-validators/form-validator.model'
|
||||||
import { FormValidatorService } from './form-validator.service'
|
import { FormValidatorService } from './form-validator.service'
|
||||||
|
|
||||||
export type FormReactiveErrors = { [id: string]: string | FormReactiveErrors | FormReactiveErrors[] }
|
export * from '../form-validators/form-validator.model'
|
||||||
export type FormReactiveValidationMessages = {
|
|
||||||
[id: string]: { [name: string]: string } | FormReactiveValidationMessages | FormReactiveValidationMessages[]
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FormReactiveService {
|
export class FormReactiveService {
|
||||||
private formValidatorService = inject(FormValidatorService)
|
private formValidatorService = inject(FormValidatorService)
|
||||||
|
|
||||||
buildForm<T = any> (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) {
|
buildForm<T = any> (obj: BuildFormArgument, defaultValues: FormDefault = {}) {
|
||||||
const { formErrors, validationMessages, form } = this.formValidatorService.internalBuildForm<T>(obj, defaultValues)
|
const { formErrors, validationMessages, form } = this.formValidatorService.internalBuildForm<T>(obj, defaultValues)
|
||||||
|
|
||||||
form.events
|
form.events
|
||||||
|
@ -44,7 +41,7 @@ export class FormReactiveService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
forceCheck (form: FormGroup, formErrors: any, validationMessages: FormReactiveValidationMessages) {
|
forceCheck (form: FormGroup, formErrors: any, validationMessages: FormReactiveMessages) {
|
||||||
this.onStatusChanged({ form, formErrors, validationMessages, onlyDirty: false })
|
this.onStatusChanged({ form, formErrors, validationMessages, onlyDirty: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +73,7 @@ export class FormReactiveService {
|
||||||
private onStatusChanged (options: {
|
private onStatusChanged (options: {
|
||||||
form: FormGroup
|
form: FormGroup
|
||||||
formErrors: FormReactiveErrors
|
formErrors: FormReactiveErrors
|
||||||
validationMessages: FormReactiveValidationMessages
|
validationMessages: FormReactiveMessages
|
||||||
onlyDirty?: boolean // default true
|
onlyDirty?: boolean // default true
|
||||||
}) {
|
}) {
|
||||||
const { form, formErrors, validationMessages, onlyDirty = true } = options
|
const { form, formErrors, validationMessages, onlyDirty = true } = options
|
||||||
|
@ -86,7 +83,7 @@ export class FormReactiveService {
|
||||||
this.onStatusChanged({
|
this.onStatusChanged({
|
||||||
form: form.controls[field] as FormGroup,
|
form: form.controls[field] as FormGroup,
|
||||||
formErrors: formErrors[field] as FormReactiveErrors,
|
formErrors: formErrors[field] as FormReactiveErrors,
|
||||||
validationMessages: validationMessages[field] as FormReactiveValidationMessages,
|
validationMessages: validationMessages[field] as FormReactiveMessages,
|
||||||
onlyDirty
|
onlyDirty
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -99,7 +96,7 @@ export class FormReactiveService {
|
||||||
|
|
||||||
if (!control || (onlyDirty && !control.dirty) || !control.enabled || !control.errors) continue
|
if (!control || (onlyDirty && !control.dirty) || !control.enabled || !control.errors) continue
|
||||||
|
|
||||||
const staticMessages = validationMessages[field] as FormReactiveValidationMessages
|
const staticMessages = validationMessages[field] as FormReactiveMessages
|
||||||
for (const key of Object.keys(control.errors)) {
|
for (const key of Object.keys(control.errors)) {
|
||||||
const formErrorValue = control.errors[key]
|
const formErrorValue = control.errors[key]
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { FormGroup } from '@angular/forms'
|
import { FormGroup } from '@angular/forms'
|
||||||
import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
|
import { BuildFormArgument, FormDefault } from '../form-validators/form-validator.model'
|
||||||
import { FormReactiveService, FormReactiveValidationMessages } from './form-reactive.service'
|
import { FormReactiveService, FormReactiveMessages } from './form-reactive.service'
|
||||||
|
|
||||||
export abstract class FormReactive {
|
export abstract class FormReactive {
|
||||||
protected abstract formReactiveService: FormReactiveService
|
protected abstract formReactiveService: FormReactiveService
|
||||||
|
@ -8,9 +8,9 @@ export abstract class FormReactive {
|
||||||
|
|
||||||
form: FormGroup
|
form: FormGroup
|
||||||
formErrors: any // To avoid casting in template because of string | FormReactiveErrors
|
formErrors: any // To avoid casting in template because of string | FormReactiveErrors
|
||||||
validationMessages: FormReactiveValidationMessages
|
validationMessages: FormReactiveMessages
|
||||||
|
|
||||||
buildForm (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) {
|
buildForm (obj: BuildFormArgument, defaultValues: FormDefault = {}) {
|
||||||
const { formErrors, validationMessages, form } = this.formReactiveService.buildForm(obj, defaultValues)
|
const { formErrors, validationMessages, form } = this.formReactiveService.buildForm(obj, defaultValues)
|
||||||
|
|
||||||
this.form = form
|
this.form = form
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { Injectable, inject } from '@angular/core'
|
import { Injectable, inject } from '@angular/core'
|
||||||
import { AsyncValidatorFn, FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms'
|
import { AsyncValidatorFn, FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms'
|
||||||
import { objectKeysTyped } from '@peertube/peertube-core-utils'
|
import { objectKeysTyped } from '@peertube/peertube-core-utils'
|
||||||
import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
|
import { BuildFormArgument, FormDefault } from '../form-validators/form-validator.model'
|
||||||
import { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive.service'
|
import { FormReactiveErrors, FormReactiveMessages } from './form-reactive.service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FormValidatorService {
|
export class FormValidatorService {
|
||||||
private formBuilder = inject(FormBuilder)
|
private formBuilder = inject(FormBuilder)
|
||||||
|
|
||||||
internalBuildForm<T = any> (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) {
|
internalBuildForm<T = any> (obj: BuildFormArgument, defaultValues: FormDefault = {}) {
|
||||||
const formErrors: FormReactiveErrors = {}
|
const formErrors: FormReactiveErrors = {}
|
||||||
const validationMessages: FormReactiveValidationMessages = {}
|
const validationMessages: FormReactiveMessages = {}
|
||||||
const group: { [key: string]: any } = {}
|
const group: { [key: string]: any } = {}
|
||||||
|
|
||||||
for (const name of Object.keys(obj)) {
|
for (const name of Object.keys(obj)) {
|
||||||
|
@ -18,7 +18,7 @@ export class FormValidatorService {
|
||||||
|
|
||||||
const field = obj[name]
|
const field = obj[name]
|
||||||
if (this.isRecursiveField(field)) {
|
if (this.isRecursiveField(field)) {
|
||||||
const result = this.internalBuildForm(field as BuildFormArgument, defaultValues[name] as BuildFormDefaultValues)
|
const result = this.internalBuildForm(field as BuildFormArgument, defaultValues[name] as FormDefault)
|
||||||
group[name] = result.form
|
group[name] = result.form
|
||||||
formErrors[name] = result.formErrors
|
formErrors[name] = result.formErrors
|
||||||
validationMessages[name] = result.validationMessages
|
validationMessages[name] = result.validationMessages
|
||||||
|
@ -41,9 +41,9 @@ export class FormValidatorService {
|
||||||
updateFormGroup (
|
updateFormGroup (
|
||||||
form: FormGroup,
|
form: FormGroup,
|
||||||
formErrors: FormReactiveErrors,
|
formErrors: FormReactiveErrors,
|
||||||
validationMessages: FormReactiveValidationMessages,
|
validationMessages: FormReactiveMessages,
|
||||||
formToBuild: BuildFormArgument,
|
formToBuild: BuildFormArgument,
|
||||||
defaultValues: BuildFormDefaultValues = {}
|
defaultValues: FormDefault = {}
|
||||||
) {
|
) {
|
||||||
for (const name of objectKeysTyped(formToBuild)) {
|
for (const name of objectKeysTyped(formToBuild)) {
|
||||||
const field = formToBuild[name]
|
const field = formToBuild[name]
|
||||||
|
@ -55,9 +55,9 @@ export class FormValidatorService {
|
||||||
// FIXME: typings
|
// FIXME: typings
|
||||||
(form as any)[name],
|
(form as any)[name],
|
||||||
formErrors[name],
|
formErrors[name],
|
||||||
validationMessages[name] as FormReactiveValidationMessages,
|
validationMessages[name] as FormReactiveMessages,
|
||||||
formToBuild[name] as BuildFormArgument,
|
formToBuild[name] as BuildFormArgument,
|
||||||
defaultValues[name] as BuildFormDefaultValues
|
defaultValues[name] as FormDefault
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -77,11 +77,11 @@ export class FormValidatorService {
|
||||||
|
|
||||||
addControlInFormArray (options: {
|
addControlInFormArray (options: {
|
||||||
formErrors: FormReactiveErrors
|
formErrors: FormReactiveErrors
|
||||||
validationMessages: FormReactiveValidationMessages
|
validationMessages: FormReactiveMessages
|
||||||
formArray: FormArray
|
formArray: FormArray
|
||||||
controlName: string
|
controlName: string
|
||||||
formToBuild: BuildFormArgument
|
formToBuild: BuildFormArgument
|
||||||
defaultValues?: BuildFormDefaultValues
|
defaultValues?: FormDefault
|
||||||
}) {
|
}) {
|
||||||
const { formArray, formErrors, validationMessages, controlName, formToBuild, defaultValues = {} } = options
|
const { formArray, formErrors, validationMessages, controlName, formToBuild, defaultValues = {} } = options
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ export class FormValidatorService {
|
||||||
if (!validationMessages[controlName]) validationMessages[controlName] = []
|
if (!validationMessages[controlName]) validationMessages[controlName] = []
|
||||||
|
|
||||||
const formArrayErrors = formErrors[controlName] as FormReactiveErrors[]
|
const formArrayErrors = formErrors[controlName] as FormReactiveErrors[]
|
||||||
const formArrayValidationMessages = validationMessages[controlName] as FormReactiveValidationMessages[]
|
const formArrayValidationMessages = validationMessages[controlName] as FormReactiveMessages[]
|
||||||
|
|
||||||
const totalControls = formArray.controls.length
|
const totalControls = formArray.controls.length
|
||||||
formArrayErrors.push({})
|
formArrayErrors.push({})
|
||||||
|
@ -109,7 +109,7 @@ export class FormValidatorService {
|
||||||
|
|
||||||
removeControlFromFormArray (options: {
|
removeControlFromFormArray (options: {
|
||||||
formErrors: FormReactiveErrors
|
formErrors: FormReactiveErrors
|
||||||
validationMessages: FormReactiveValidationMessages
|
validationMessages: FormReactiveMessages
|
||||||
index: number
|
index: number
|
||||||
formArray: FormArray
|
formArray: FormArray
|
||||||
controlName: string
|
controlName: string
|
||||||
|
@ -117,7 +117,7 @@ export class FormValidatorService {
|
||||||
const { formArray, formErrors, validationMessages, index, controlName } = options
|
const { formArray, formErrors, validationMessages, index, controlName } = options
|
||||||
|
|
||||||
const formArrayErrors = formErrors[controlName] as FormReactiveErrors[]
|
const formArrayErrors = formErrors[controlName] as FormReactiveErrors[]
|
||||||
const formArrayValidationMessages = validationMessages[controlName] as FormReactiveValidationMessages[]
|
const formArrayValidationMessages = validationMessages[controlName] as FormReactiveMessages[]
|
||||||
|
|
||||||
formArrayErrors.splice(index, 1)
|
formArrayErrors.splice(index, 1)
|
||||||
formArrayValidationMessages.splice(index, 1)
|
formArrayValidationMessages.splice(index, 1)
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
<div *ngIf="recommended()" class="ms-2 pt-badge badge-secondary" i18n>Recommended</div>
|
<div *ngIf="recommended()" class="ms-2 pt-badge badge-secondary" i18n>Recommended</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex flex-column extra-container ms-4">
|
<div class="d-flex flex-column extra-container">
|
||||||
<div class="wrapper form-group-description" [id]="inputName() + '-description'">
|
<div class="wrapper form-group-description" [id]="inputName() + '-description'">
|
||||||
<ng-content select="description"></ng-content>
|
<ng-content select="description"></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
@use '_variables' as *;
|
@use "_variables" as *;
|
||||||
@use '_mixins' as *;
|
@use "_mixins" as *;
|
||||||
@use 'form-mixins' as *;
|
@use "form-mixins" as *;
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -35,3 +35,7 @@
|
||||||
.pt-badge {
|
.pt-badge {
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.extra-container {
|
||||||
|
@include margin-left(28px);
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { of } from 'rxjs'
|
|
||||||
import { catchError } from 'rxjs/operators'
|
|
||||||
import { HttpClient } from '@angular/common/http'
|
import { HttpClient } from '@angular/common/http'
|
||||||
import { Injectable, inject } from '@angular/core'
|
import { Injectable, inject } from '@angular/core'
|
||||||
import { RestExtractor } from '@app/core'
|
import { RestExtractor } from '@app/core'
|
||||||
import { CustomPage } from '@peertube/peertube-models'
|
import { CustomPage } from '@peertube/peertube-models'
|
||||||
|
import { Observable, of } from 'rxjs'
|
||||||
|
import { catchError } from 'rxjs/operators'
|
||||||
import { environment } from '../../../../environments/environment'
|
import { environment } from '../../../../environments/environment'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -13,7 +13,7 @@ export class CustomPageService {
|
||||||
|
|
||||||
static BASE_INSTANCE_HOMEPAGE_URL = environment.apiUrl + '/api/v1/custom-pages/homepage/instance'
|
static BASE_INSTANCE_HOMEPAGE_URL = environment.apiUrl + '/api/v1/custom-pages/homepage/instance'
|
||||||
|
|
||||||
getInstanceHomepage () {
|
getInstanceHomepage (): Observable<CustomPage> {
|
||||||
return this.authHttp.get<CustomPage>(CustomPageService.BASE_INSTANCE_HOMEPAGE_URL)
|
return this.authHttp.get<CustomPage>(CustomPageService.BASE_INSTANCE_HOMEPAGE_URL)
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError(err => {
|
catchError(err => {
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
<div class="menu">
|
||||||
|
<h1>{{ config().title }}</h1>
|
||||||
|
|
||||||
|
<ul class="ul-unstyle">
|
||||||
|
@for (entry of config().entries; track entry) {
|
||||||
|
@if (entry.type === 'separator') {
|
||||||
|
<div class="separator">
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
} @else if (entry.type === 'link' && isDisplayed(entry)) {
|
||||||
|
<ng-template #icon>
|
||||||
|
<div *ngIf="entry.icon" class="global-icon-wrapper">
|
||||||
|
<my-global-icon [iconName]="entry.icon"></my-global-icon>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #label>
|
||||||
|
<span>{{ entry.label }}</span>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
@if (isUnavailable(entry)) {
|
||||||
|
<my-unavailable-menu-entry [help]="entry.unavailableText()">
|
||||||
|
<span class="icon" *ngTemplateOutlet="icon"></span>
|
||||||
|
<span class="label" *ngTemplateOutlet="label"></span>
|
||||||
|
</my-unavailable-menu-entry>
|
||||||
|
} @else {
|
||||||
|
<a [routerLink]="entry.routerLink" queryParamsHandling="merge" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">
|
||||||
|
<ng-container *ngTemplateOutlet="icon"></ng-container>
|
||||||
|
<ng-container *ngTemplateOutlet="label"></ng-container>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="menu-placeholder"></div>
|
|
@ -1,7 +1,7 @@
|
||||||
@use '_variables' as *;
|
@use "_variables" as *;
|
||||||
@use '_mixins' as *;
|
@use "_mixins" as *;
|
||||||
@use '_form-mixins' as *;
|
@use "_form-mixins" as *;
|
||||||
@import 'bootstrap/scss/mixins';
|
@import "bootstrap/scss/mixins";
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
color: pvar(--fg-200);
|
color: pvar(--fg-200);
|
||||||
|
@ -105,6 +105,10 @@ a {
|
||||||
|
|
||||||
@include padding-right(1.5rem);
|
@include padding-right(1.5rem);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
height: 2px;
|
height: 2px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -132,7 +136,7 @@ a {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
top: unset;
|
top: unset;
|
||||||
width: calc(100vw - #{pvar(--menu-width)} - (#{pvar(--x-margin-content)} * 2));
|
width: calc(100vw - #{pvar(--menu-width)} - #{pvar(--x-margin-content)} * 2);
|
||||||
padding: 0.75rem 0.5rem;
|
padding: 0.75rem 0.5rem;
|
||||||
border: 1px solid pvar(--bg-secondary-450);
|
border: 1px solid pvar(--bg-secondary-450);
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, input } from '@angular/core'
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { RouterModule } from '@angular/router'
|
||||||
|
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { GlobalIconComponent, GlobalIconName } from '../../shared-icons/global-icon.component'
|
||||||
|
import { UnavailableMenuEntryComponent } from './unavailable-menu-entry.component'
|
||||||
|
|
||||||
|
type LateralMenuLinkEntry = {
|
||||||
|
type: 'link'
|
||||||
|
label: string
|
||||||
|
routerLink: string
|
||||||
|
routerLinkActiveOptions?: { exact: boolean }
|
||||||
|
|
||||||
|
icon?: GlobalIconName
|
||||||
|
|
||||||
|
isDisplayed?: () => boolean
|
||||||
|
unavailableText?: () => string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LateralMenuConfig = {
|
||||||
|
title: string
|
||||||
|
|
||||||
|
entries: ({ type: 'separator' } | LateralMenuLinkEntry)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-lateral-menu',
|
||||||
|
styleUrls: [ './lateral-menu.component.scss' ],
|
||||||
|
templateUrl: './lateral-menu.component.html',
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
RouterModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgbTooltipModule,
|
||||||
|
GlobalIconComponent,
|
||||||
|
UnavailableMenuEntryComponent,
|
||||||
|
GlobalIconComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class LateralMenuComponent {
|
||||||
|
config = input.required<LateralMenuConfig>()
|
||||||
|
|
||||||
|
isDisplayed (entry: LateralMenuLinkEntry) {
|
||||||
|
if (!entry.isDisplayed) return true
|
||||||
|
|
||||||
|
return entry.isDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
isUnavailable (entry: LateralMenuLinkEntry) {
|
||||||
|
if (!entry.unavailableText) return false
|
||||||
|
|
||||||
|
return !!entry.unavailableText()
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import { Component, input } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { HelpComponent } from '../../shared/shared-main/buttons/help.component'
|
import { HelpComponent } from '../buttons/help.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-unavailable-menu-entry',
|
selector: 'my-unavailable-menu-entry',
|
|
@ -2,7 +2,7 @@ import { NgIf } from '@angular/common'
|
||||||
import { Component, OnDestroy, OnInit, inject, input } from '@angular/core'
|
import { Component, OnDestroy, OnInit, inject, input } from '@angular/core'
|
||||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { AuthService, Notifier, ServerService, User, UserService } from '@app/core'
|
import { AuthService, Notifier, ServerService, User, UserService } from '@app/core'
|
||||||
import { FormReactiveErrors, FormReactiveService, FormReactiveValidationMessages } from '@app/shared/shared-forms/form-reactive.service'
|
import { FormReactiveErrors, FormReactiveService, FormReactiveMessages } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
import { NSFWFlag, NSFWFlagType, NSFWPolicyType, UserUpdateMe } from '@peertube/peertube-models'
|
import { NSFWFlag, NSFWFlagType, NSFWPolicyType, UserUpdateMe } from '@peertube/peertube-models'
|
||||||
import { pick } from 'lodash-es'
|
import { pick } from 'lodash-es'
|
||||||
import { Subject, Subscription } from 'rxjs'
|
import { Subject, Subscription } from 'rxjs'
|
||||||
|
@ -55,7 +55,7 @@ export class UserVideoSettingsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
form: FormGroup<Form>
|
form: FormGroup<Form>
|
||||||
formErrors: FormReactiveErrors = {}
|
formErrors: FormReactiveErrors = {}
|
||||||
validationMessages: FormReactiveValidationMessages = {}
|
validationMessages: FormReactiveMessages = {}
|
||||||
|
|
||||||
nsfwItems: SelectOptionsItem[] = [
|
nsfwItems: SelectOptionsItem[] = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,13 +1,87 @@
|
||||||
import { sortBy } from '@peertube/peertube-core-utils'
|
import { sortBy } from '@peertube/peertube-core-utils'
|
||||||
import { getLuminance, parse, toHSLA } from 'color-bits'
|
import { getLuminance, parse, toHSLA } from 'color-bits'
|
||||||
import { ServerConfigTheme } from '@peertube/peertube-models'
|
import { HTMLServerConfig, ServerConfig, ServerConfigTheme } from '@peertube/peertube-models'
|
||||||
import { logger } from './logger'
|
import { logger } from './logger'
|
||||||
import debug from 'debug'
|
import debug from 'debug'
|
||||||
|
|
||||||
const debugLogger = debug('peertube:theme')
|
const debugLogger = debug('peertube:theme')
|
||||||
|
|
||||||
|
type ConfigCSSVariableMap = Record<keyof ServerConfig['theme']['customization'], string>
|
||||||
|
|
||||||
|
export type ThemeCustomizationKey = keyof ConfigCSSVariableMap
|
||||||
|
export type ColorPaletteThemeConfig = Pick<HTMLServerConfig['theme'], 'default' | 'customization'>
|
||||||
|
|
||||||
export class ThemeManager {
|
export class ThemeManager {
|
||||||
private oldInjectedProperties: string[] = []
|
private configVariablesStyle: HTMLStyleElement
|
||||||
|
private colorPaletteStyle: HTMLStyleElement
|
||||||
|
private configuredCSSVariables = new Set<string>()
|
||||||
|
|
||||||
|
private readonly configCSSVariableMap: ConfigCSSVariableMap = {
|
||||||
|
primaryColor: '--primary',
|
||||||
|
foregroundColor: '--fg',
|
||||||
|
backgroundColor: '--bg',
|
||||||
|
backgroundSecondaryColor: '--bg-secondary',
|
||||||
|
menuForegroundColor: '--menu-fg',
|
||||||
|
menuBackgroundColor: '--menu-bg',
|
||||||
|
menuBorderRadius: '--menu-border-radius',
|
||||||
|
headerForegroundColor: '--header-fg',
|
||||||
|
headerBackgroundColor: '--header-bg',
|
||||||
|
inputBorderRadius: '--input-border-radius'
|
||||||
|
}
|
||||||
|
|
||||||
|
private defaultConfigValue: Record<keyof ConfigCSSVariableMap, string>
|
||||||
|
|
||||||
|
getCSSConfigValue (configKey: ThemeCustomizationKey) {
|
||||||
|
const cssVariable = this.configCSSVariableMap[configKey]
|
||||||
|
|
||||||
|
return getComputedStyle(document.documentElement).getPropertyValue(cssVariable)
|
||||||
|
}
|
||||||
|
|
||||||
|
injectConfigVariables (options: {
|
||||||
|
currentTheme: string
|
||||||
|
config: ColorPaletteThemeConfig
|
||||||
|
}) {
|
||||||
|
const { currentTheme, config } = options
|
||||||
|
|
||||||
|
if (!this.configVariablesStyle) {
|
||||||
|
this.configVariablesStyle = document.createElement('style')
|
||||||
|
this.configVariablesStyle.setAttribute('type', 'text/css')
|
||||||
|
this.configVariablesStyle.dataset.ptStyleId = 'config-variables'
|
||||||
|
document.head.appendChild(this.configVariablesStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.configuredCSSVariables.clear()
|
||||||
|
this.configVariablesStyle.textContent = ''
|
||||||
|
|
||||||
|
// Only inject config variables for the default theme
|
||||||
|
if (currentTheme !== config.default) return
|
||||||
|
|
||||||
|
const computedStyle = getComputedStyle(document.documentElement)
|
||||||
|
|
||||||
|
let configStyleContent = ''
|
||||||
|
|
||||||
|
this.defaultConfigValue = {} as any
|
||||||
|
|
||||||
|
for (const [ configKey, configValue ] of Object.entries(config.customization) as ([keyof ConfigCSSVariableMap, string][])) {
|
||||||
|
const cssVariable = this.configCSSVariableMap[configKey]
|
||||||
|
|
||||||
|
this.defaultConfigValue[configKey] = computedStyle.getPropertyValue(cssVariable)
|
||||||
|
|
||||||
|
if (!configValue) continue
|
||||||
|
|
||||||
|
if (!cssVariable) {
|
||||||
|
logger.error(`Unknown UI config variable "${configKey}" with value "${configValue}"`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
configStyleContent += ` ${cssVariable}: ${configValue};\n`
|
||||||
|
this.configuredCSSVariables.add(cssVariable)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configStyleContent) {
|
||||||
|
this.configVariablesStyle.textContent = `:root[data-pt-theme=${currentTheme}] {\n${configStyleContent} }`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
injectTheme (theme: ServerConfigTheme, apiUrl: string) {
|
injectTheme (theme: ServerConfigTheme, apiUrl: string) {
|
||||||
const head = this.getHeadElement()
|
const head = this.getHeadElement()
|
||||||
|
@ -42,7 +116,7 @@ export class ThemeManager {
|
||||||
link.disabled = link.getAttribute('title') !== name
|
link.disabled = link.getAttribute('title') !== name
|
||||||
|
|
||||||
if (!link.disabled) {
|
if (!link.disabled) {
|
||||||
link.onload = () => this.injectColorPalette()
|
link.onload = () => this._injectColorPalette()
|
||||||
} else {
|
} else {
|
||||||
link.onload = undefined
|
link.onload = undefined
|
||||||
}
|
}
|
||||||
|
@ -52,7 +126,10 @@ export class ThemeManager {
|
||||||
document.documentElement.dataset.ptTheme = name
|
document.documentElement.dataset.ptTheme = name
|
||||||
}
|
}
|
||||||
|
|
||||||
injectCoreColorPalette (iteration = 0) {
|
injectColorPalette (options: {
|
||||||
|
config: ColorPaletteThemeConfig
|
||||||
|
currentTheme: string
|
||||||
|
}, iteration = 0) {
|
||||||
if (iteration > 100) {
|
if (iteration > 100) {
|
||||||
logger.error('Too many iteration when checking color palette injection. The theme may be missing the --is-dark CSS variable')
|
logger.error('Too many iteration when checking color palette injection. The theme may be missing the --is-dark CSS variable')
|
||||||
|
|
||||||
|
@ -61,10 +138,14 @@ export class ThemeManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.canInjectCoreColorPalette()) {
|
if (!this.canInjectCoreColorPalette()) {
|
||||||
return setTimeout(() => this.injectCoreColorPalette(iteration + 1), Math.floor(iteration / 10))
|
return setTimeout(() => this.injectColorPalette(options, iteration + 1), Math.floor(iteration / 10))
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.injectColorPalette()
|
debugLogger(`Update color palette`, options.config)
|
||||||
|
|
||||||
|
this.injectConfigVariables(options)
|
||||||
|
|
||||||
|
return this._injectColorPalette()
|
||||||
}
|
}
|
||||||
|
|
||||||
removeThemeLink (linkEl: HTMLLinkElement) {
|
removeThemeLink (linkEl: HTMLLinkElement) {
|
||||||
|
@ -78,18 +159,19 @@ export class ThemeManager {
|
||||||
return isDark === '0' || isDark === '1'
|
return isDark === '0' || isDark === '1'
|
||||||
}
|
}
|
||||||
|
|
||||||
private injectColorPalette () {
|
private _injectColorPalette () {
|
||||||
console.log(`Injecting color palette`)
|
try {
|
||||||
|
if (!this.colorPaletteStyle) {
|
||||||
const rootStyle = document.documentElement.style
|
this.colorPaletteStyle = document.createElement('style')
|
||||||
const computedStyle = getComputedStyle(document.documentElement)
|
this.colorPaletteStyle.setAttribute('type', 'text/css')
|
||||||
|
this.colorPaletteStyle.dataset.ptStyleId = 'color-palette'
|
||||||
// FIXME: Remove previously injected properties
|
document.head.appendChild(this.colorPaletteStyle)
|
||||||
for (const property of this.oldInjectedProperties) {
|
|
||||||
rootStyle.removeProperty(property)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.oldInjectedProperties = []
|
let paletteStyleContent = ''
|
||||||
|
|
||||||
|
const computedStyle = getComputedStyle(document.documentElement)
|
||||||
|
this.colorPaletteStyle.textContent = ''
|
||||||
|
|
||||||
const isGlobalDarkTheme = () => {
|
const isGlobalDarkTheme = () => {
|
||||||
return this.isDarkTheme({
|
return this.isDarkTheme({
|
||||||
|
@ -133,7 +215,7 @@ export class ThemeManager {
|
||||||
|
|
||||||
// Use trim for some web browsers: https://github.com/Chocobozzz/PeerTube/issues/6952
|
// Use trim for some web browsers: https://github.com/Chocobozzz/PeerTube/issues/6952
|
||||||
const mainColorHSL = toHSLA(parse(mainColor.trim()))
|
const mainColorHSL = toHSLA(parse(mainColor.trim()))
|
||||||
debugLogger(`Theme main variable ${mainColor} -> ${this.toHSLStr(mainColorHSL)}`)
|
debugLogger(`Theme main variable --${prefix}: ${mainColor} -> ${this.toHSLStr(mainColorHSL)}`)
|
||||||
|
|
||||||
// Inject in alphabetical order for easy debug
|
// Inject in alphabetical order for easy debug
|
||||||
const toInject: { id: number, key: string, value: string }[] = [
|
const toInject: { id: number, key: string, value: string }[] = [
|
||||||
|
@ -147,7 +229,11 @@ export class ThemeManager {
|
||||||
const suffix = 500 + (50 * i * j)
|
const suffix = 500 + (50 * i * j)
|
||||||
const key = `--${prefix}-${suffix}`
|
const key = `--${prefix}-${suffix}`
|
||||||
|
|
||||||
const existingValue = computedStyle.getPropertyValue(key)
|
// Override all our variables if the CSS variable has been configured by the admin
|
||||||
|
const existingValue = this.configuredCSSVariables.has(`--${prefix}`)
|
||||||
|
? '0'
|
||||||
|
: computedStyle.getPropertyValue(key)
|
||||||
|
|
||||||
if (!existingValue || existingValue === '0') {
|
if (!existingValue || existingValue === '0') {
|
||||||
const newLuminance = this.buildNewLuminance(lastColorHSL, j * step, darkInverter)
|
const newLuminance = this.buildNewLuminance(lastColorHSL, j * step, darkInverter)
|
||||||
const newColorHSL = { ...lastColorHSL, l: newLuminance }
|
const newColorHSL = { ...lastColorHSL, l: newLuminance }
|
||||||
|
@ -170,14 +256,23 @@ export class ThemeManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const { key, value } of sortBy(toInject, 'id')) {
|
for (const { key, value } of sortBy(toInject, 'id')) {
|
||||||
rootStyle.setProperty(key, value)
|
paletteStyleContent += ` ${key}: ${value};\n`
|
||||||
this.oldInjectedProperties.push(key)
|
}
|
||||||
|
|
||||||
|
if (paletteStyleContent) {
|
||||||
|
// To override default variables
|
||||||
|
document.documentElement.className = 'color-palette'
|
||||||
|
|
||||||
|
this.colorPaletteStyle.textContent = `:root.color-palette {\n${paletteStyleContent} }`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.documentElement.dataset.bsTheme = isGlobalDarkTheme()
|
document.documentElement.dataset.bsTheme = isGlobalDarkTheme()
|
||||||
? 'dark'
|
? 'dark'
|
||||||
: ''
|
: ''
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Cannot inject color palette', err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildNewLuminance (base: { l: number }, factor: number, darkInverter: number) {
|
private buildNewLuminance (base: { l: number }, factor: number, darkInverter: number) {
|
||||||
|
|
|
@ -69,7 +69,7 @@ strong {
|
||||||
|
|
||||||
input[readonly] {
|
input[readonly] {
|
||||||
// Force blank on readonly inputs
|
// Force blank on readonly inputs
|
||||||
background-color: pvar(--input-bg) !important;
|
background-color: pvar(--input-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
|
|
92
client/src/sass/bootstrap.scss
vendored
92
client/src/sass/bootstrap.scss
vendored
|
@ -1,47 +1,47 @@
|
||||||
@use '_variables' as *;
|
@use "_variables" as *;
|
||||||
@use '_mixins' as *;
|
@use "_mixins" as *;
|
||||||
@use '_button-mixins' as *;
|
@use "_button-mixins" as *;
|
||||||
@import './_bootstrap-variables';
|
@import "./_bootstrap-variables";
|
||||||
@import 'bootstrap/scss/functions';
|
@import "bootstrap/scss/functions";
|
||||||
@import 'bootstrap/scss/variables';
|
@import "bootstrap/scss/variables";
|
||||||
@import 'bootstrap/scss/maps';
|
@import "bootstrap/scss/maps";
|
||||||
@import 'bootstrap/scss/mixins';
|
@import "bootstrap/scss/mixins";
|
||||||
@import 'bootstrap/scss/utilities';
|
@import "bootstrap/scss/utilities";
|
||||||
@import 'bootstrap/scss/root';
|
@import "bootstrap/scss/root";
|
||||||
@import 'bootstrap/scss/reboot';
|
@import "bootstrap/scss/reboot";
|
||||||
@import 'bootstrap/scss/type';
|
@import "bootstrap/scss/type";
|
||||||
@import 'bootstrap/scss/grid';
|
@import "bootstrap/scss/grid";
|
||||||
@import 'bootstrap/scss/forms';
|
@import "bootstrap/scss/forms";
|
||||||
@import 'bootstrap/scss/buttons';
|
@import "bootstrap/scss/buttons";
|
||||||
@import 'bootstrap/scss/transitions';
|
@import "bootstrap/scss/transitions";
|
||||||
@import 'bootstrap/scss/dropdown';
|
@import "bootstrap/scss/dropdown";
|
||||||
@import 'bootstrap/scss/button-group';
|
@import "bootstrap/scss/button-group";
|
||||||
@import 'bootstrap/scss/nav';
|
@import "bootstrap/scss/nav";
|
||||||
@import 'bootstrap/scss/card';
|
@import "bootstrap/scss/card";
|
||||||
@import 'bootstrap/scss/accordion';
|
@import "bootstrap/scss/accordion";
|
||||||
@import 'bootstrap/scss/alert';
|
@import "bootstrap/scss/alert";
|
||||||
@import 'bootstrap/scss/close';
|
@import "bootstrap/scss/close";
|
||||||
@import 'bootstrap/scss/modal';
|
@import "bootstrap/scss/modal";
|
||||||
@import 'bootstrap/scss/tooltip';
|
@import "bootstrap/scss/tooltip";
|
||||||
@import 'bootstrap/scss/popover';
|
@import "bootstrap/scss/popover";
|
||||||
@import 'bootstrap/scss/spinners';
|
@import "bootstrap/scss/spinners";
|
||||||
|
|
||||||
/* stylelint-disable-next-line at-rule-empty-line-before */
|
/* stylelint-disable-next-line at-rule-empty-line-before */
|
||||||
@import 'bootstrap/scss/helpers/clearfix';
|
@import "bootstrap/scss/helpers/clearfix";
|
||||||
@import 'bootstrap/scss/helpers/color-bg';
|
@import "bootstrap/scss/helpers/color-bg";
|
||||||
// @import 'bootstrap/scss/helpers/colored-links';
|
// @import 'bootstrap/scss/helpers/colored-links';
|
||||||
@import 'bootstrap/scss/helpers/focus-ring';
|
@import "bootstrap/scss/helpers/focus-ring";
|
||||||
@import 'bootstrap/scss/helpers/icon-link';
|
@import "bootstrap/scss/helpers/icon-link";
|
||||||
@import 'bootstrap/scss/helpers/ratio';
|
@import "bootstrap/scss/helpers/ratio";
|
||||||
@import 'bootstrap/scss/helpers/position';
|
@import "bootstrap/scss/helpers/position";
|
||||||
@import 'bootstrap/scss/helpers/stacks';
|
@import "bootstrap/scss/helpers/stacks";
|
||||||
@import 'bootstrap/scss/helpers/visually-hidden';
|
@import "bootstrap/scss/helpers/visually-hidden";
|
||||||
@import 'bootstrap/scss/helpers/stretched-link';
|
@import "bootstrap/scss/helpers/stretched-link";
|
||||||
@import 'bootstrap/scss/helpers/text-truncation';
|
@import "bootstrap/scss/helpers/text-truncation";
|
||||||
@import 'bootstrap/scss/helpers/vr';
|
@import "bootstrap/scss/helpers/vr";
|
||||||
|
|
||||||
/* stylelint-disable-next-line at-rule-empty-line-before */
|
/* stylelint-disable-next-line at-rule-empty-line-before */
|
||||||
@import 'bootstrap/scss/utilities/api';
|
@import "bootstrap/scss/utilities/api";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
--bs-border-color-translucent: #{pvar(--input-border-color)};
|
--bs-border-color-translucent: #{pvar(--input-border-color)};
|
||||||
|
@ -166,7 +166,7 @@ body {
|
||||||
@media screen and (min-width: #{breakpoint(md)}) {
|
@media screen and (min-width: #{breakpoint(md)}) {
|
||||||
.modal::before {
|
.modal::before {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
content: ' ';
|
content: " ";
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,7 +217,6 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// On desktop browsers, make the content and header horizontally sticked to right not move when modal open and close
|
// On desktop browsers, make the content and header horizontally sticked to right not move when modal open and close
|
||||||
.modal-open {
|
.modal-open {
|
||||||
overflow-y: scroll !important; // Make sure vertical scroll bar is always visible on desktop browsers to get disabled scrollbar effect
|
overflow-y: scroll !important; // Make sure vertical scroll bar is always visible on desktop browsers to get disabled scrollbar effect
|
||||||
|
@ -299,12 +298,6 @@ body {
|
||||||
font-size: $button-font-size;
|
font-size: $button-font-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control {
|
|
||||||
color: pvar(--fg);
|
|
||||||
background-color: pvar(--input-bg);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group {
|
.input-group {
|
||||||
> .btn,
|
> .btn,
|
||||||
> .input-group-text {
|
> .input-group-text {
|
||||||
|
@ -342,7 +335,7 @@ body {
|
||||||
|
|
||||||
.form-control-clear {
|
.form-control-clear {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: .5rem;
|
right: 0.5rem;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
|
@ -363,12 +356,11 @@ body {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// RTL compatibility
|
// RTL compatibility
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
:root[dir=rtl] .modal .modal-header .modal-title {
|
:root[dir="rtl"] .modal .modal-header .modal-title {
|
||||||
margin-inline-end: auto;
|
margin-inline-end: auto;
|
||||||
margin-right: unset;
|
margin-right: unset;
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,8 +82,29 @@ label,
|
||||||
}
|
}
|
||||||
|
|
||||||
label + .form-group-description,
|
label + .form-group-description,
|
||||||
|
label + my-help + .form-group-description,
|
||||||
.label + .form-group-description,
|
.label + .form-group-description,
|
||||||
.label-container + .form-group-description {
|
.label-container + .form-group-description {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
margin-top: -0.5rem;
|
margin-top: -0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-with-unit {
|
||||||
|
position: relative;
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
input[type="number"] + span {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.4em;
|
||||||
|
right: 3em;
|
||||||
|
|
||||||
|
@media screen and (max-width: $mobile-view) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[disabled] {
|
||||||
|
opacity: 0.8;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,10 +48,6 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.max-width-300px {
|
|
||||||
max-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.d-none-mw {
|
.d-none-mw {
|
||||||
@include on-mobile-main-col {
|
@include on-mobile-main-col {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@use '_variables' as *;
|
@use "_variables" as *;
|
||||||
@use '_mixins' as *;
|
@use "_mixins" as *;
|
||||||
|
|
||||||
.sub-menu-entry {
|
.sub-menu-entry {
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -50,14 +50,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-sub-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pt-breadcrumb {
|
.pt-breadcrumb {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -83,7 +75,7 @@
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
content: '/';
|
content: "/";
|
||||||
|
|
||||||
@include padding-right(0.5rem);
|
@include padding-right(0.5rem);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@use 'sass:map';
|
@use "sass:map";
|
||||||
@use '_variables' as *;
|
@use "_variables" as *;
|
||||||
|
|
||||||
$modal-footer-border-width: 0;
|
$modal-footer-border-width: 0;
|
||||||
$modal-md: 600px;
|
$modal-md: 600px;
|
||||||
|
@ -17,7 +17,6 @@ $grid-breakpoints: (
|
||||||
// Extra large screens / wide desktops
|
// Extra large screens / wide desktops
|
||||||
xl: 1200px,
|
xl: 1200px,
|
||||||
xxl: 1600px,
|
xxl: 1600px,
|
||||||
|
|
||||||
// SCREEN GROUP
|
// SCREEN GROUP
|
||||||
fhd: 1800px,
|
fhd: 1800px,
|
||||||
qhd: 2560px,
|
qhd: 2560px,
|
||||||
|
@ -43,6 +42,11 @@ $input-btn-focus-width: 0;
|
||||||
$input-btn-focus-color: inherit;
|
$input-btn-focus-color: inherit;
|
||||||
$input-focus-border-color: pvar(--input-border-color);
|
$input-focus-border-color: pvar(--input-border-color);
|
||||||
$input-focus-box-shadow: #{$focus-box-shadow-form};
|
$input-focus-box-shadow: #{$focus-box-shadow-form};
|
||||||
|
$input-padding-y: pvar(--input-y-padding);
|
||||||
|
$input-padding-x: pvar(--input-x-padding);
|
||||||
|
$input-border-radius: pvar(--input-border-radius);
|
||||||
|
$input-border-width: pvar(--input-border-width);
|
||||||
|
$input-border-color: pvar(--input-border-color);
|
||||||
|
|
||||||
$input-group-addon-color: pvar(--fg);
|
$input-group-addon-color: pvar(--fg);
|
||||||
$input-group-addon-bg: pvar(--bg-secondary-500);
|
$input-group-addon-bg: pvar(--bg-secondary-500);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@use '_variables' as *;
|
@use "_variables" as *;
|
||||||
@use '_mixins' as *;
|
@use "_mixins" as *;
|
||||||
|
|
||||||
@mixin define-css-variables() {
|
@mixin define-css-variables() {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
@ -10,6 +10,7 @@
|
||||||
--menu-fg: var(--menuForegroundColor);
|
--menu-fg: var(--menuForegroundColor);
|
||||||
--menu-margin-left: #{$menu-margin-left};
|
--menu-margin-left: #{$menu-margin-left};
|
||||||
--menu-width: #{$menu-width};
|
--menu-width: #{$menu-width};
|
||||||
|
--menu-border-radius: #{$menu-border-radius};
|
||||||
|
|
||||||
--fg: var(--mainForegroundColor, #000);
|
--fg: var(--mainForegroundColor, #000);
|
||||||
|
|
||||||
|
@ -33,6 +34,7 @@
|
||||||
|
|
||||||
--input-placeholder: var(--inputPlaceholderColor, #{pvar(--fg-50)});
|
--input-placeholder: var(--inputPlaceholderColor, #{pvar(--fg-50)});
|
||||||
--input-border-color: var(--inputBorderColor, #{pvar(--input-bg)});
|
--input-border-color: var(--inputBorderColor, #{pvar(--input-bg)});
|
||||||
|
--input-border-width: 1px;
|
||||||
|
|
||||||
--input-check-active-fg: #{pvar(--on-primary)};
|
--input-check-active-fg: #{pvar(--on-primary)};
|
||||||
--input-check-active-bg: #{pvar(--primary)};
|
--input-check-active-bg: #{pvar(--primary)};
|
||||||
|
@ -70,6 +72,9 @@
|
||||||
--menu-fg: #{pvar(--fg-400)};
|
--menu-fg: #{pvar(--fg-400)};
|
||||||
--menu-bg: #{pvar(--bg-secondary-400)};
|
--menu-bg: #{pvar(--bg-secondary-400)};
|
||||||
|
|
||||||
|
--header-fg: #{pvar(--fg)};
|
||||||
|
--header-bg: #{pvar(--bg)};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
--tmp-header-height: #{$header-height};
|
--tmp-header-height: #{$header-height};
|
||||||
|
@ -95,8 +100,8 @@
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Light theme
|
// Light theme
|
||||||
&[data-pt-theme=peertube-core-light-beige],
|
&[data-pt-theme="peertube-core-light-beige"],
|
||||||
&[data-pt-theme=default] {
|
&[data-pt-theme="default"] {
|
||||||
--is-dark: 0;
|
--is-dark: 0;
|
||||||
|
|
||||||
--primary: #FF8F37;
|
--primary: #FF8F37;
|
||||||
|
@ -128,7 +133,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Brown
|
// Brown
|
||||||
&[data-pt-theme=peertube-core-dark-brown] {
|
&[data-pt-theme="peertube-core-dark-brown"] {
|
||||||
--is-dark: 1;
|
--is-dark: 1;
|
||||||
|
|
||||||
--primary: #FD9C50;
|
--primary: #FD9C50;
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
max-width: $width;
|
max-width: $width;
|
||||||
color: pvar(--input-fg);
|
color: pvar(--input-fg);
|
||||||
background-color: pvar(--input-bg);
|
background-color: pvar(--input-bg);
|
||||||
border: 1px solid pvar(--input-border-color);
|
border: pvar(--input-border-width) solid pvar(--input-border-color);
|
||||||
border-radius: pvar(--input-border-radius);
|
border-radius: pvar(--input-border-radius);
|
||||||
|
|
||||||
@include rounded-line-height-1-5($font-size);
|
@include rounded-line-height-1-5($font-size);
|
||||||
|
@ -84,7 +84,7 @@
|
||||||
|
|
||||||
padding: pvar(--input-y-padding) calc(#{pvar(--input-x-padding)} + 23px) pvar(--input-y-padding) pvar(--input-x-padding);
|
padding: pvar(--input-y-padding) calc(#{pvar(--input-x-padding)} + 23px) pvar(--input-y-padding) pvar(--input-x-padding);
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 1px solid var(--input-border-color) !important;
|
border: pvar(--input-border-width) solid var(--input-border-color) !important;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
@use 'sass:math';
|
@use "sass:math";
|
||||||
@use 'sass:color';
|
@use "sass:color";
|
||||||
@use '_variables' as *;
|
@use "_variables" as *;
|
||||||
@import '_bootstrap-mixins';
|
@import "_bootstrap-mixins";
|
||||||
|
|
||||||
@mixin underline-primary {
|
@mixin underline-primary {
|
||||||
text-decoration: underline !important;
|
text-decoration: underline !important;
|
||||||
|
@ -67,7 +67,7 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: "";
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -134,7 +134,7 @@
|
||||||
@mixin responsive-width($width) {
|
@mixin responsive-width($width) {
|
||||||
width: $width;
|
width: $width;
|
||||||
|
|
||||||
@media screen and (max-width: #{$width - 30px}) {
|
@media screen and (max-width: #{$width + 30px}) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,7 +145,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
> *:not(:last-child)::after {
|
> *:not(:last-child)::after {
|
||||||
content: '•';
|
content: "•";
|
||||||
margin: 0 $separator-margin;
|
margin: 0 $separator-margin;
|
||||||
color: pvar(--primary);
|
color: pvar(--primary);
|
||||||
}
|
}
|
||||||
|
@ -179,7 +179,7 @@
|
||||||
|
|
||||||
my-global-icon {
|
my-global-icon {
|
||||||
width: 22px;
|
width: 22px;
|
||||||
opacity: .7;
|
opacity: 0.7;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -2px;
|
top: -2px;
|
||||||
|
|
||||||
|
@ -189,23 +189,23 @@
|
||||||
|
|
||||||
@mixin divider($color: pvar(--bg-secondary-400), $background: pvar(--bg)) {
|
@mixin divider($color: pvar(--bg-secondary-400), $background: pvar(--bg)) {
|
||||||
width: 95%;
|
width: 95%;
|
||||||
border-top: .05rem solid $color;
|
border-top: 0.05rem solid $color;
|
||||||
height: .05rem;
|
height: 0.05rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&[data-content] {
|
&[data-content] {
|
||||||
margin: .8rem 0;
|
margin: 0.8rem 0;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
background: $background;
|
background: $background;
|
||||||
color: $color;
|
color: $color;
|
||||||
content: attr(data-content);
|
content: attr(data-content);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: .7rem;
|
font-size: 0.7rem;
|
||||||
padding: 0 .4rem;
|
padding: 0 0.4rem;
|
||||||
transform: translateY(-.65rem);
|
transform: translateY(-0.65rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -213,7 +213,7 @@
|
||||||
// applies ratio (default to 16:9) to a child element (using $selector) only using
|
// applies ratio (default to 16:9) to a child element (using $selector) only using
|
||||||
// an immediate's parent size. This allows to set a ratio without explicit
|
// an immediate's parent size. This allows to set a ratio without explicit
|
||||||
// dimensions, as width/height cannot be computed from each other.
|
// dimensions, as width/height cannot be computed from each other.
|
||||||
@mixin block-ratio ($selector: 'div', $inverted-ratio: math.div(9, 16)) {
|
@mixin block-ratio($selector: "div", $inverted-ratio: math.div(9, 16)) {
|
||||||
$padding-percent: math.percentage($inverted-ratio);
|
$padding-percent: math.percentage($inverted-ratio);
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -318,7 +318,6 @@
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* inset-inline properties are not supported by iOS < 14.5
|
* inset-inline properties are not supported by iOS < 14.5
|
||||||
|
@ -335,7 +334,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@mixin left($value) {
|
@mixin left($value) {
|
||||||
@supports (inset-inline-start: $value) {
|
@supports (inset-inline-start: $value) {
|
||||||
inset-inline-start: $value;
|
inset-inline-start: $value;
|
||||||
|
@ -345,4 +343,3 @@
|
||||||
left: $value;
|
left: $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
@use 'sass:math';
|
@use "sass:math";
|
||||||
@use 'sass:color';
|
@use "sass:color";
|
||||||
@use 'sass:map';
|
@use "sass:map";
|
||||||
|
|
||||||
$medium-view: 1000px;
|
$medium-view: 1000px;
|
||||||
$small-view: 800px;
|
$small-view: 800px;
|
||||||
$mobile-view: 500px;
|
$mobile-view: 500px;
|
||||||
|
|
||||||
$main-fonts: 'Source Sans Pro', sans-serif;
|
$main-fonts: "Source Sans Pro", sans-serif;
|
||||||
$font-regular: 400;
|
$font-regular: 400;
|
||||||
$font-semibold: 600;
|
$font-semibold: 600;
|
||||||
$font-bold: 700;
|
$font-bold: 700;
|
||||||
|
@ -28,6 +28,7 @@ $header-height-mobile-view-without-search: 80px;
|
||||||
$header-mobile-msg-height: 48px;
|
$header-mobile-msg-height: 48px;
|
||||||
|
|
||||||
$menu-width: 248px;
|
$menu-width: 248px;
|
||||||
|
$menu-border-radius: 14px;
|
||||||
$menu-collapsed-width: 50px;
|
$menu-collapsed-width: 50px;
|
||||||
$menu-margin-left: 2rem;
|
$menu-margin-left: 2rem;
|
||||||
$menu-overlay-view: 1200px;
|
$menu-overlay-view: 1200px;
|
||||||
|
@ -74,7 +75,7 @@ $player-portrait-bottom-space: 50px;
|
||||||
$sub-menu-margin-bottom: 30px;
|
$sub-menu-margin-bottom: 30px;
|
||||||
$sub-menu-margin-bottom-small-view: 10px;
|
$sub-menu-margin-bottom-small-view: 10px;
|
||||||
|
|
||||||
$focus-box-shadow-dimensions: 0 0 0 .2rem;
|
$focus-box-shadow-dimensions: 0 0 0 0.2rem;
|
||||||
|
|
||||||
$form-input-font-size: 16px;
|
$form-input-font-size: 16px;
|
||||||
|
|
||||||
|
@ -88,46 +89,37 @@ $variables: (
|
||||||
--x-margin-content: var(--x-margin-content),
|
--x-margin-content: var(--x-margin-content),
|
||||||
--tmp-header-height: var(--tmp-header-height),
|
--tmp-header-height: var(--tmp-header-height),
|
||||||
--header-height: var(--header-height),
|
--header-height: var(--header-height),
|
||||||
|
--header-fg: var(--header-fg),
|
||||||
|
--header-bg: var(--header-bg),
|
||||||
--fg: var(--fg),
|
--fg: var(--fg),
|
||||||
--bg: var(--bg),
|
--bg: var(--bg),
|
||||||
|
|
||||||
--red: var(--red),
|
--red: var(--red),
|
||||||
--green: var(--green),
|
--green: var(--green),
|
||||||
|
|
||||||
--input-fg: var(--input-fg),
|
--input-fg: var(--input-fg),
|
||||||
|
|
||||||
--input-bg: var(--input-bg),
|
--input-bg: var(--input-bg),
|
||||||
--input-bg-550: var(--input-bg-550),
|
--input-bg-550: var(--input-bg-550),
|
||||||
--input-bg-600: var(--input-bg-600),
|
--input-bg-600: var(--input-bg-600),
|
||||||
--input-bg-in-secondary: var(--input-bg-in-secondary),
|
--input-bg-in-secondary: var(--input-bg-in-secondary),
|
||||||
|
|
||||||
--input-danger-fg: var(--input-danger-fg),
|
--input-danger-fg: var(--input-danger-fg),
|
||||||
--input-danger-bg: var(--input-danger-bg),
|
--input-danger-bg: var(--input-danger-bg),
|
||||||
|
|
||||||
--input-placeholder: var(--input-placeholder),
|
--input-placeholder: var(--input-placeholder),
|
||||||
--input-border-color: var(--input-border-color),
|
--input-border-color: var(--input-border-color),
|
||||||
--input-border-radius: var(--input-border-radius),
|
--input-border-radius: var(--input-border-radius),
|
||||||
|
--input-border-width: var(--input-border-width),
|
||||||
--input-check-active-fg: var(--input-check-active-fg),
|
--input-check-active-fg: var(--input-check-active-fg),
|
||||||
--input-check-active-bg: var(--input-check-active-bg),
|
--input-check-active-bg: var(--input-check-active-bg),
|
||||||
|
|
||||||
--input-x-padding: var(--input-x-padding),
|
--input-x-padding: var(--input-x-padding),
|
||||||
--input-y-padding: var(--input-y-padding),
|
--input-y-padding: var(--input-y-padding),
|
||||||
|
|
||||||
--textarea-x-padding: var(--textarea-x-padding),
|
--textarea-x-padding: var(--textarea-x-padding),
|
||||||
--textarea-y-padding: var(--textarea-y-padding),
|
--textarea-y-padding: var(--textarea-y-padding),
|
||||||
--textarea-fg: var(--textarea-fg),
|
--textarea-fg: var(--textarea-fg),
|
||||||
--textarea-bg: var(--textarea-bg),
|
--textarea-bg: var(--textarea-bg),
|
||||||
|
|
||||||
--support-btn-bg: var(--support-btn-bg),
|
--support-btn-bg: var(--support-btn-bg),
|
||||||
--support-btn-fg: var(--support-btn-fg),
|
--support-btn-fg: var(--support-btn-fg),
|
||||||
--support-btn-heart-bg: var(--support-btn-heart-bg),
|
--support-btn-heart-bg: var(--support-btn-heart-bg),
|
||||||
|
|
||||||
--secondary-icon-color: var(--secondary-icon-color),
|
--secondary-icon-color: var(--secondary-icon-color),
|
||||||
--active-icon-color: var(--active-icon-color),
|
--active-icon-color: var(--active-icon-color),
|
||||||
--active-icon-bg: var(--active-icon-bg),
|
--active-icon-bg: var(--active-icon-bg),
|
||||||
|
|
||||||
--fg-500: var(--fg-500),
|
--fg-500: var(--fg-500),
|
||||||
--fg-450: var(--fg-450),
|
--fg-450: var(--fg-450),
|
||||||
--fg-400: var(--fg-400),
|
--fg-400: var(--fg-400),
|
||||||
|
@ -138,7 +130,6 @@ $variables: (
|
||||||
--fg-150: var(--fg-150),
|
--fg-150: var(--fg-150),
|
||||||
--fg-100: var(--fg-100),
|
--fg-100: var(--fg-100),
|
||||||
--fg-50: var(--fg-50),
|
--fg-50: var(--fg-50),
|
||||||
|
|
||||||
--bg-secondary-600: var(--bg-secondary-600),
|
--bg-secondary-600: var(--bg-secondary-600),
|
||||||
--bg-secondary-550: var(--bg-secondary-550),
|
--bg-secondary-550: var(--bg-secondary-550),
|
||||||
--bg-secondary-500: var(--bg-secondary-500),
|
--bg-secondary-500: var(--bg-secondary-500),
|
||||||
|
@ -148,7 +139,6 @@ $variables: (
|
||||||
--bg-secondary-300: var(--bg-secondary-300),
|
--bg-secondary-300: var(--bg-secondary-300),
|
||||||
--bg-secondary-250: var(--bg-secondary-250),
|
--bg-secondary-250: var(--bg-secondary-250),
|
||||||
--bg-secondary-200: var(--bg-secondary-200),
|
--bg-secondary-200: var(--bg-secondary-200),
|
||||||
|
|
||||||
--menu-fg: var(--menu-fg),
|
--menu-fg: var(--menu-fg),
|
||||||
--menu-fg-600: var(--menu-fg-600),
|
--menu-fg-600: var(--menu-fg-600),
|
||||||
--menu-fg-550: var(--menu-fg-550),
|
--menu-fg-550: var(--menu-fg-550),
|
||||||
|
@ -162,7 +152,6 @@ $variables: (
|
||||||
--menu-fg-150: var(--menu-fg-150),
|
--menu-fg-150: var(--menu-fg-150),
|
||||||
--menu-fg-100: var(--menu-fg-100),
|
--menu-fg-100: var(--menu-fg-100),
|
||||||
--menu-fg-50: var(--menu-fg-50),
|
--menu-fg-50: var(--menu-fg-50),
|
||||||
|
|
||||||
--menu-bg: var(--menu-bg),
|
--menu-bg: var(--menu-bg),
|
||||||
--menu-bg-600: var(--menu-bg-600),
|
--menu-bg-600: var(--menu-bg-600),
|
||||||
--menu-bg-550: var(--menu-bg-550),
|
--menu-bg-550: var(--menu-bg-550),
|
||||||
|
@ -173,10 +162,9 @@ $variables: (
|
||||||
--menu-bg-300: var(--menu-bg-300),
|
--menu-bg-300: var(--menu-bg-300),
|
||||||
--menu-bg-250: var(--menu-bg-250),
|
--menu-bg-250: var(--menu-bg-250),
|
||||||
--menu-bg-200: var(--menu-bg-200),
|
--menu-bg-200: var(--menu-bg-200),
|
||||||
|
|
||||||
--menu-margin-left: var(--menu-margin-left),
|
--menu-margin-left: var(--menu-margin-left),
|
||||||
--menu-width: var(--menu-width),
|
--menu-width: var(--menu-width),
|
||||||
|
--menu-border-radius: var(--menu-border-radius),
|
||||||
--on-primary: var(--on-primary),
|
--on-primary: var(--on-primary),
|
||||||
--on-primary-700: var(--on-primary-700),
|
--on-primary-700: var(--on-primary-700),
|
||||||
--on-primary-650: var(--on-primary-650),
|
--on-primary-650: var(--on-primary-650),
|
||||||
|
@ -192,7 +180,6 @@ $variables: (
|
||||||
--on-primary-150: var(--on-primary-150),
|
--on-primary-150: var(--on-primary-150),
|
||||||
--on-primary-100: var(--on-primary-100),
|
--on-primary-100: var(--on-primary-100),
|
||||||
--on-primary-50: var(--on-primary-50),
|
--on-primary-50: var(--on-primary-50),
|
||||||
|
|
||||||
--primary: var(--primary),
|
--primary: var(--primary),
|
||||||
--primary-700: var(--primary-700),
|
--primary-700: var(--primary-700),
|
||||||
--primary-650: var(--primary-650),
|
--primary-650: var(--primary-650),
|
||||||
|
@ -208,16 +195,13 @@ $variables: (
|
||||||
--primary-150: var(--primary-150),
|
--primary-150: var(--primary-150),
|
||||||
--primary-100: var(--primary-100),
|
--primary-100: var(--primary-100),
|
||||||
--primary-50: var(--primary-50),
|
--primary-50: var(--primary-50),
|
||||||
|
|
||||||
--border-primary: var(--border-primary),
|
--border-primary: var(--border-primary),
|
||||||
--border-secondary: var(--border-secondary),
|
--border-secondary: var(--border-secondary),
|
||||||
|
|
||||||
--alert-primary-fg: var(--alert-primary-fg),
|
--alert-primary-fg: var(--alert-primary-fg),
|
||||||
--alert-primary-bg: var(--alert-primary-bg),
|
--alert-primary-bg: var(--alert-primary-bg),
|
||||||
--alert-primary-border-color: var(--alert-primary-border-color),
|
--alert-primary-border-color: var(--alert-primary-border-color),
|
||||||
|
|
||||||
--embed-fg: var(--embed-fg),
|
--embed-fg: var(--embed-fg),
|
||||||
--embed-big-play-bg: var(--embed-big-play-bg),
|
--embed-big-play-bg: var(--embed-big-play-bg)
|
||||||
);
|
);
|
||||||
|
|
||||||
// SASS type check our CSS variables
|
// SASS type check our CSS variables
|
||||||
|
@ -225,7 +209,7 @@ $variables: (
|
||||||
@if map.has-key($variables, $variable) {
|
@if map.has-key($variables, $variable) {
|
||||||
@return map.get($variables, $variable);
|
@return map.get($variables, $variable);
|
||||||
} @else {
|
} @else {
|
||||||
@error 'ERROR: Variable #{$variable} does not exist';
|
@error "ERROR: Variable #{$variable} does not exist";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,7 +217,7 @@ $variables: (
|
||||||
@if map.has-key($variables, $variable) and map.has-key($variables, $fallback) {
|
@if map.has-key($variables, $variable) and map.has-key($variables, $fallback) {
|
||||||
@return var($variable, map.get($variables, $fallback));
|
@return var($variable, map.get($variables, $fallback));
|
||||||
} @else {
|
} @else {
|
||||||
@error 'ERROR: Variable #{$variable} or #{$fallback} does not exist';
|
@error "ERROR: Variable #{$variable} or #{$fallback} does not exist";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -125,6 +125,14 @@ p-toast {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Colorpicker
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
p-colorpicker .p-colorpicker-preview {
|
||||||
|
border: 1px solid pvar(--fg-300);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Data table
|
// Data table
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -1040,6 +1040,25 @@ followings:
|
||||||
theme:
|
theme:
|
||||||
default: 'default'
|
default: 'default'
|
||||||
|
|
||||||
|
# Easily redefine the client UI when the user is using your default instance theme
|
||||||
|
# Use null to keep the default values
|
||||||
|
# If you need more advanced customizations, install or develop a dedicated theme: https://docs.joinpeertube.org/contribute/plugins
|
||||||
|
customization:
|
||||||
|
primary_color: null # Hex color. Example: '#FF8F37'
|
||||||
|
|
||||||
|
foreground_color: null # Hex color
|
||||||
|
background_color: null # Hex color
|
||||||
|
background_secondary_color: null # Hex color
|
||||||
|
|
||||||
|
menu_foreground_color: null # Hex color
|
||||||
|
menu_background_color: null # Hex color
|
||||||
|
menu_border_radius: null # Pixels. Example: '5px'
|
||||||
|
|
||||||
|
header_background_color: null # Hex color
|
||||||
|
header_foreground_color: null # Hex color
|
||||||
|
|
||||||
|
input_border_radius: null # Pixels
|
||||||
|
|
||||||
broadcast_message:
|
broadcast_message:
|
||||||
enabled: false
|
enabled: false
|
||||||
message: '' # Support markdown
|
message: '' # Support markdown
|
||||||
|
@ -1074,6 +1093,7 @@ search:
|
||||||
|
|
||||||
# PeerTube client/interface configuration
|
# PeerTube client/interface configuration
|
||||||
client:
|
client:
|
||||||
|
|
||||||
videos:
|
videos:
|
||||||
miniature:
|
miniature:
|
||||||
# By default PeerTube client displays author username
|
# By default PeerTube client displays author username
|
||||||
|
|
|
@ -56,6 +56,19 @@ export interface CustomConfig {
|
||||||
|
|
||||||
theme: {
|
theme: {
|
||||||
default: string
|
default: string
|
||||||
|
|
||||||
|
customization: {
|
||||||
|
primaryColor: string
|
||||||
|
foregroundColor: string
|
||||||
|
backgroundColor: string
|
||||||
|
backgroundSecondaryColor: string
|
||||||
|
menuForegroundColor: string
|
||||||
|
menuBackgroundColor: string
|
||||||
|
menuBorderRadius: string
|
||||||
|
headerForegroundColor: string
|
||||||
|
headerBackgroundColor: string
|
||||||
|
inputBorderRadius: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
services: {
|
services: {
|
||||||
|
|
|
@ -162,6 +162,19 @@ export interface ServerConfig {
|
||||||
builtIn: { name: 'peertube-core-light-beige' | 'peertube-core-dark-brown' }[]
|
builtIn: { name: 'peertube-core-light-beige' | 'peertube-core-dark-brown' }[]
|
||||||
|
|
||||||
default: string
|
default: string
|
||||||
|
|
||||||
|
customization: {
|
||||||
|
primaryColor: string
|
||||||
|
foregroundColor: string
|
||||||
|
backgroundColor: string
|
||||||
|
backgroundSecondaryColor: string
|
||||||
|
menuForegroundColor: string
|
||||||
|
menuBackgroundColor: string
|
||||||
|
menuBorderRadius: string
|
||||||
|
headerForegroundColor: string
|
||||||
|
headerBackgroundColor: string
|
||||||
|
inputBorderRadius: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
email: {
|
email: {
|
||||||
|
|
|
@ -66,7 +66,7 @@ export const ServerErrorCode = {
|
||||||
/**
|
/**
|
||||||
* oauthjs/oauth2-server error codes
|
* oauthjs/oauth2-server error codes
|
||||||
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
||||||
**/
|
*/
|
||||||
export const OAuth2ErrorCode = {
|
export const OAuth2ErrorCode = {
|
||||||
/**
|
/**
|
||||||
* The provided authorization grant (e.g., authorization code, resource owner
|
* The provided authorization grant (e.g., authorization code, resource owner
|
||||||
|
|
|
@ -272,7 +272,20 @@ function customConfig (): CustomConfig {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
default: CONFIG.THEME.DEFAULT
|
default: CONFIG.THEME.DEFAULT,
|
||||||
|
|
||||||
|
customization: {
|
||||||
|
primaryColor: CONFIG.THEME.CUSTOMIZATION.PRIMARY_COLOR,
|
||||||
|
foregroundColor: CONFIG.THEME.CUSTOMIZATION.FOREGROUND_COLOR,
|
||||||
|
backgroundColor: CONFIG.THEME.CUSTOMIZATION.BACKGROUND_COLOR,
|
||||||
|
backgroundSecondaryColor: CONFIG.THEME.CUSTOMIZATION.BACKGROUND_SECONDARY_COLOR,
|
||||||
|
menuForegroundColor: CONFIG.THEME.CUSTOMIZATION.MENU_FOREGROUND_COLOR,
|
||||||
|
menuBackgroundColor: CONFIG.THEME.CUSTOMIZATION.MENU_BACKGROUND_COLOR,
|
||||||
|
menuBorderRadius: CONFIG.THEME.CUSTOMIZATION.MENU_BORDER_RADIUS,
|
||||||
|
headerForegroundColor: CONFIG.THEME.CUSTOMIZATION.HEADER_FOREGROUND_COLOR,
|
||||||
|
headerBackgroundColor: CONFIG.THEME.CUSTOMIZATION.HEADER_BACKGROUND_COLOR,
|
||||||
|
inputBorderRadius: CONFIG.THEME.CUSTOMIZATION.INPUT_BORDER_RADIUS
|
||||||
|
}
|
||||||
},
|
},
|
||||||
services: {
|
services: {
|
||||||
twitter: {
|
twitter: {
|
||||||
|
|
|
@ -992,6 +992,39 @@ const CONFIG = {
|
||||||
THEME: {
|
THEME: {
|
||||||
get DEFAULT () {
|
get DEFAULT () {
|
||||||
return config.get<string>('theme.default')
|
return config.get<string>('theme.default')
|
||||||
|
},
|
||||||
|
|
||||||
|
CUSTOMIZATION: {
|
||||||
|
get PRIMARY_COLOR () {
|
||||||
|
return config.get<string>('theme.customization.primary_color')
|
||||||
|
},
|
||||||
|
get FOREGROUND_COLOR () {
|
||||||
|
return config.get<string>('theme.customization.foreground_color')
|
||||||
|
},
|
||||||
|
get BACKGROUND_COLOR () {
|
||||||
|
return config.get<string>('theme.customization.background_color')
|
||||||
|
},
|
||||||
|
get BACKGROUND_SECONDARY_COLOR () {
|
||||||
|
return config.get<string>('theme.customization.background_secondary_color')
|
||||||
|
},
|
||||||
|
get MENU_FOREGROUND_COLOR () {
|
||||||
|
return config.get<string>('theme.customization.menu_foreground_color')
|
||||||
|
},
|
||||||
|
get MENU_BACKGROUND_COLOR () {
|
||||||
|
return config.get<string>('theme.customization.menu_background_color')
|
||||||
|
},
|
||||||
|
get MENU_BORDER_RADIUS () {
|
||||||
|
return config.get<string>('theme.customization.menu_border_radius')
|
||||||
|
},
|
||||||
|
get HEADER_BACKGROUND_COLOR () {
|
||||||
|
return config.get<string>('theme.customization.header_background_color')
|
||||||
|
},
|
||||||
|
get HEADER_FOREGROUND_COLOR () {
|
||||||
|
return config.get<string>('theme.customization.header_foreground_color')
|
||||||
|
},
|
||||||
|
get INPUT_BORDER_RADIUS () {
|
||||||
|
return config.get<string>('theme.customization.input_border_radius')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
BROADCAST_MESSAGE: {
|
BROADCAST_MESSAGE: {
|
||||||
|
|
|
@ -156,7 +156,19 @@ class ServerConfigManager {
|
||||||
theme: {
|
theme: {
|
||||||
registered: this.getRegisteredThemes(),
|
registered: this.getRegisteredThemes(),
|
||||||
builtIn: this.getBuiltInThemes(),
|
builtIn: this.getBuiltInThemes(),
|
||||||
default: defaultTheme
|
default: defaultTheme,
|
||||||
|
customization: {
|
||||||
|
primaryColor: CONFIG.THEME.CUSTOMIZATION.PRIMARY_COLOR,
|
||||||
|
foregroundColor: CONFIG.THEME.CUSTOMIZATION.FOREGROUND_COLOR,
|
||||||
|
backgroundColor: CONFIG.THEME.CUSTOMIZATION.BACKGROUND_COLOR,
|
||||||
|
backgroundSecondaryColor: CONFIG.THEME.CUSTOMIZATION.BACKGROUND_SECONDARY_COLOR,
|
||||||
|
menuForegroundColor: CONFIG.THEME.CUSTOMIZATION.MENU_FOREGROUND_COLOR,
|
||||||
|
menuBackgroundColor: CONFIG.THEME.CUSTOMIZATION.MENU_BACKGROUND_COLOR,
|
||||||
|
menuBorderRadius: CONFIG.THEME.CUSTOMIZATION.MENU_BORDER_RADIUS,
|
||||||
|
headerForegroundColor: CONFIG.THEME.CUSTOMIZATION.HEADER_FOREGROUND_COLOR,
|
||||||
|
headerBackgroundColor: CONFIG.THEME.CUSTOMIZATION.HEADER_BACKGROUND_COLOR,
|
||||||
|
inputBorderRadius: CONFIG.THEME.CUSTOMIZATION.INPUT_BORDER_RADIUS
|
||||||
|
}
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
enabled: isEmailEnabled()
|
enabled: isEmailEnabled()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue