mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 01:39:37 +02:00
Add admin config wizard
This commit is contained in:
parent
a6b89bde2b
commit
eb11e5793f
96 changed files with 2609 additions and 616 deletions
|
@ -14,7 +14,7 @@ import {
|
||||||
AdminConfigVODComponent
|
AdminConfigVODComponent
|
||||||
} from './pages'
|
} from './pages'
|
||||||
import { AdminConfigCustomizationComponent } from './pages/admin-config-customization.component'
|
import { AdminConfigCustomizationComponent } from './pages/admin-config-customization.component'
|
||||||
import { AdminConfigService } from './shared/admin-config.service'
|
import { AdminConfigService } from '../../shared/shared-admin/admin-config.service'
|
||||||
|
|
||||||
export const customConfigResolver: ResolveFn<CustomConfig> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
export const customConfigResolver: ResolveFn<CustomConfig> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||||
return inject(AdminConfigService).getCustomConfig()
|
return inject(AdminConfigService).getCustomConfig()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<my-admin-save-bar i18n-title title="Advanced configuration" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
<my-admin-save-bar i18n-title title="Advanced configuration" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||||
|
|
||||||
<ng-container [formGroup]="form">
|
<form [formGroup]="form">
|
||||||
|
|
||||||
<div class="pt-two-cols">
|
<div class="pt-two-cols">
|
||||||
|
|
||||||
|
@ -109,4 +109,4 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</form>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { Component, inject, OnInit } from '@angular/core'
|
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { CanComponentDeactivate } from '@app/core'
|
import { CanComponentDeactivate } from '@app/core'
|
||||||
|
@ -12,7 +12,8 @@ import {
|
||||||
} from '@app/shared/form-validators/form-validator.model'
|
} from '@app/shared/form-validators/form-validator.model'
|
||||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
import { CustomConfig } from '@peertube/peertube-models'
|
import { CustomConfig } from '@peertube/peertube-models'
|
||||||
import { AdminConfigService } from '../shared/admin-config.service'
|
import { Subscription } from 'rxjs'
|
||||||
|
import { AdminConfigService } from '../../../shared/shared-admin/admin-config.service'
|
||||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||||
|
|
||||||
type Form = {
|
type Form = {
|
||||||
|
@ -44,7 +45,7 @@ type Form = {
|
||||||
styleUrls: [ './admin-config-common.scss' ],
|
styleUrls: [ './admin-config-common.scss' ],
|
||||||
imports: [ CommonModule, FormsModule, ReactiveFormsModule, AdminSaveBarComponent ]
|
imports: [ CommonModule, FormsModule, ReactiveFormsModule, AdminSaveBarComponent ]
|
||||||
})
|
})
|
||||||
export class AdminConfigAdvancedComponent implements OnInit, CanComponentDeactivate {
|
export class AdminConfigAdvancedComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||||
private route = inject(ActivatedRoute)
|
private route = inject(ActivatedRoute)
|
||||||
private formReactiveService = inject(FormReactiveService)
|
private formReactiveService = inject(FormReactiveService)
|
||||||
private adminConfigService = inject(AdminConfigService)
|
private adminConfigService = inject(AdminConfigService)
|
||||||
|
@ -54,11 +55,23 @@ export class AdminConfigAdvancedComponent implements OnInit, CanComponentDeactiv
|
||||||
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||||
|
|
||||||
private customConfig: CustomConfig
|
private customConfig: CustomConfig
|
||||||
|
private customConfigSub: Subscription
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||||
|
|
||||||
this.buildForm()
|
this.buildForm()
|
||||||
|
|
||||||
|
this.customConfigSub = this.adminConfigService.getCustomConfigReloadedObs()
|
||||||
|
.subscribe(customConfig => {
|
||||||
|
this.customConfig = customConfig
|
||||||
|
|
||||||
|
this.form.patchValue(this.customConfig)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy () {
|
||||||
|
if (this.customConfigSub) this.customConfigSub.unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
canDeactivate () {
|
canDeactivate () {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<my-admin-save-bar i18n-title title="Platform customization" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
<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">
|
<form [formGroup]="form">
|
||||||
|
<div class="pt-two-cols">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>APPEARANCE</h2>
|
<h2 i18n>APPEARANCE</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,7 +32,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4" [formGroup]="form">
|
<div class="pt-two-cols mt-4">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>CUSTOMIZATION</h2>
|
<h2 i18n>CUSTOMIZATION</h2>
|
||||||
|
|
||||||
|
@ -110,7 +111,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4" [formGroup]="form">
|
<div class="pt-two-cols mt-4">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<div class="anchor" id="customizations"></div>
|
<div class="anchor" id="customizations"></div>
|
||||||
<!-- customizations anchor -->
|
<!-- customizations anchor -->
|
||||||
|
@ -176,3 +177,4 @@ color: red;
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { Component, inject, OnInit } from '@angular/core'
|
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, ValueChangeEvent } from '@angular/forms'
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, ValueChangeEvent } from '@angular/forms'
|
||||||
import { ActivatedRoute, RouterModule } from '@angular/router'
|
import { ActivatedRoute, RouterModule } from '@angular/router'
|
||||||
import { CanComponentDeactivate, ServerService, ThemeService } from '@app/core'
|
import { CanComponentDeactivate, ServerService, ThemeService } from '@app/core'
|
||||||
|
@ -9,17 +9,15 @@ import { PeertubeCheckboxComponent } from '@app/shared/shared-forms/peertube-che
|
||||||
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
|
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
|
||||||
import { objectKeysTyped } from '@peertube/peertube-core-utils'
|
import { objectKeysTyped } from '@peertube/peertube-core-utils'
|
||||||
import { CustomConfig } from '@peertube/peertube-models'
|
import { CustomConfig } from '@peertube/peertube-models'
|
||||||
import { logger } from '@root-helpers/logger'
|
|
||||||
import { capitalizeFirstLetter } from '@root-helpers/string'
|
import { capitalizeFirstLetter } from '@root-helpers/string'
|
||||||
import { ColorPaletteThemeConfig, ThemeCustomizationKey } from '@root-helpers/theme-manager'
|
import { ColorPaletteThemeConfig, ThemeCustomizationKey } from '@root-helpers/theme-manager'
|
||||||
import { formatHEX, parse } from 'color-bits'
|
|
||||||
import debug from 'debug'
|
import debug from 'debug'
|
||||||
import { ColorPickerModule } from 'primeng/colorpicker'
|
import { ColorPickerModule } from 'primeng/colorpicker'
|
||||||
import { debounceTime } from 'rxjs'
|
import { debounceTime, Subscription } from 'rxjs'
|
||||||
import { SelectOptionsItem } from 'src/types'
|
import { SelectOptionsItem } from 'src/types'
|
||||||
|
import { AdminConfigService } from '../../../shared/shared-admin/admin-config.service'
|
||||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||||
import { AlertComponent } from '../../../shared/shared-main/common/alert.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'
|
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||||
|
|
||||||
const debugLogger = debug('peertube:config')
|
const debugLogger = debug('peertube:config')
|
||||||
|
@ -75,7 +73,7 @@ type Form = {
|
||||||
PeertubeCheckboxComponent
|
PeertubeCheckboxComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AdminConfigCustomizationComponent implements OnInit, CanComponentDeactivate {
|
export class AdminConfigCustomizationComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||||
private formReactiveService = inject(FormReactiveService)
|
private formReactiveService = inject(FormReactiveService)
|
||||||
private adminConfigService = inject(AdminConfigService)
|
private adminConfigService = inject(AdminConfigService)
|
||||||
private serverService = inject(ServerService)
|
private serverService = inject(ServerService)
|
||||||
|
@ -99,6 +97,8 @@ export class AdminConfigCustomizationComponent implements OnInit, CanComponentDe
|
||||||
private customizationResetFields = new Set<ThemeCustomizationKey>()
|
private customizationResetFields = new Set<ThemeCustomizationKey>()
|
||||||
private customConfig: CustomConfig
|
private customConfig: CustomConfig
|
||||||
|
|
||||||
|
private customConfigSub: Subscription
|
||||||
|
|
||||||
private readonly formFieldsObject: Record<ThemeCustomizationKey, { label: string, description?: string, type: 'color' | 'pixels' }> = {
|
private readonly formFieldsObject: Record<ThemeCustomizationKey, { label: string, description?: string, type: 'color' | 'pixels' }> = {
|
||||||
primaryColor: { label: $localize`Primary color`, type: 'color' },
|
primaryColor: { label: $localize`Primary color`, type: 'color' },
|
||||||
foregroundColor: { label: $localize`Foreground color`, type: 'color' },
|
foregroundColor: { label: $localize`Foreground color`, type: 'color' },
|
||||||
|
@ -127,6 +127,17 @@ export class AdminConfigCustomizationComponent implements OnInit, CanComponentDe
|
||||||
|
|
||||||
this.buildForm()
|
this.buildForm()
|
||||||
this.subscribeToCustomizationChanges()
|
this.subscribeToCustomizationChanges()
|
||||||
|
|
||||||
|
this.customConfigSub = this.adminConfigService.getCustomConfigReloadedObs()
|
||||||
|
.subscribe(customConfig => {
|
||||||
|
this.customConfig = customConfig
|
||||||
|
|
||||||
|
this.form.patchValue(this.getDefaultFormValues(), { emitEvent: false })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy () {
|
||||||
|
if (this.customConfigSub) this.customConfigSub.unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
canDeactivate () {
|
canDeactivate () {
|
||||||
|
@ -209,20 +220,11 @@ export class AdminConfigCustomizationComponent implements OnInit, CanComponentDe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultValues: FormDefaultTyped<Form> = {
|
|
||||||
...this.customConfig,
|
|
||||||
|
|
||||||
theme: {
|
|
||||||
default: this.customConfig.theme.default,
|
|
||||||
customization: this.getDefaultCustomization()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
form,
|
form,
|
||||||
formErrors,
|
formErrors,
|
||||||
validationMessages
|
validationMessages
|
||||||
} = this.formReactiveService.buildForm<Form>(obj, defaultValues)
|
} = this.formReactiveService.buildForm<Form>(obj, this.getDefaultFormValues())
|
||||||
|
|
||||||
this.form = form
|
this.form = form
|
||||||
this.formErrors = formErrors
|
this.formErrors = formErrors
|
||||||
|
@ -281,6 +283,17 @@ export class AdminConfigCustomizationComponent implements OnInit, CanComponentDe
|
||||||
return this.form.get('theme.customization').get(field)
|
return this.form.get('theme.customization').get(field)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDefaultFormValues (): FormDefaultTyped<Form> {
|
||||||
|
return {
|
||||||
|
...this.customConfig,
|
||||||
|
|
||||||
|
theme: {
|
||||||
|
default: this.customConfig.theme.default,
|
||||||
|
customization: this.getDefaultCustomization()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getDefaultCustomization () {
|
private getDefaultCustomization () {
|
||||||
const config = this.customConfig.theme.customization
|
const config = this.customConfig.theme.customization
|
||||||
|
|
||||||
|
@ -305,39 +318,16 @@ export class AdminConfigCustomizationComponent implements OnInit, CanComponentDe
|
||||||
|
|
||||||
private formatCustomizationFieldForForm (field: ThemeCustomizationKey, value: string) {
|
private formatCustomizationFieldForForm (field: ThemeCustomizationKey, value: string) {
|
||||||
if (this.formFieldsObject[field].type === 'pixels') {
|
if (this.formFieldsObject[field].type === 'pixels') {
|
||||||
return this.formatPixelsForForm(value)
|
return this.themeService.formatPixelsForForm(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.formFieldsObject[field].type === 'color') {
|
if (this.formFieldsObject[field].type === 'color') {
|
||||||
return this.formatColorForForm(value)
|
return this.themeService.formatColorForForm(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return 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) {
|
private buildNewCustomization (formValues: any) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<my-admin-save-bar i18n-title title="General configuration" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
<my-admin-save-bar i18n-title title="General configuration" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||||
|
|
||||||
<ng-container [formGroup]="form">
|
<form [formGroup]="form">
|
||||||
<div class="pt-two-cols">
|
<div class="pt-two-cols">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>BEHAVIOR</h2>
|
<h2 i18n>BEHAVIOR</h2>
|
||||||
|
@ -666,4 +666,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-container>
|
</form>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { Component, OnInit, inject } from '@angular/core'
|
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||||
import { FormArray, FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormArray, FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { ActivatedRoute, RouterLink } from '@angular/router'
|
import { ActivatedRoute, RouterLink } from '@angular/router'
|
||||||
import { getVideoQuotaDailyOptions, getVideoQuotaOptions } from '@app/+admin/shared/user-quota-options'
|
import { getVideoQuotaDailyOptions, getVideoQuotaOptions } from '@app/+admin/shared/user-quota-options'
|
||||||
|
@ -24,15 +24,16 @@ import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@a
|
||||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
import { FormReactiveService } 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 { BroadcastMessageLevel, CustomConfig } from '@peertube/peertube-models'
|
import { BroadcastMessageLevel, CustomConfig } from '@peertube/peertube-models'
|
||||||
|
import { Subscription } from 'rxjs'
|
||||||
import { pairwise } from 'rxjs/operators'
|
import { pairwise } from 'rxjs/operators'
|
||||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||||
|
import { AdminConfigService } from '../../../shared/shared-admin/admin-config.service'
|
||||||
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
||||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||||
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||||
import { UserRealQuotaInfoComponent } from '../../shared/user-real-quota-info.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'
|
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||||
|
|
||||||
type Form = {
|
type Form = {
|
||||||
|
@ -192,7 +193,7 @@ type Form = {
|
||||||
AdminSaveBarComponent
|
AdminSaveBarComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AdminConfigGeneralComponent implements OnInit, CanComponentDeactivate {
|
export class AdminConfigGeneralComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||||
private server = inject(ServerService)
|
private server = inject(ServerService)
|
||||||
private route = inject(ActivatedRoute)
|
private route = inject(ActivatedRoute)
|
||||||
private formReactiveService = inject(FormReactiveService)
|
private formReactiveService = inject(FormReactiveService)
|
||||||
|
@ -209,6 +210,7 @@ export class AdminConfigGeneralComponent implements OnInit, CanComponentDeactiva
|
||||||
exportMaxUserVideoQuotaOptions: SelectOptionsItem[] = []
|
exportMaxUserVideoQuotaOptions: SelectOptionsItem[] = []
|
||||||
|
|
||||||
private customConfig: CustomConfig
|
private customConfig: CustomConfig
|
||||||
|
private customConfigSub: Subscription
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||||
|
@ -222,12 +224,23 @@ export class AdminConfigGeneralComponent implements OnInit, CanComponentDeactiva
|
||||||
{ id: 1000 * 3600 * 24 * 30, label: $localize`30 days` }
|
{ id: 1000 * 3600 * 24 * 30, label: $localize`30 days` }
|
||||||
]
|
]
|
||||||
|
|
||||||
this.exportMaxUserVideoQuotaOptions = this.getVideoQuotaOptions().filter(o => (o.id as number) >= 1)
|
this.exportMaxUserVideoQuotaOptions = this.getVideoQuotaOptions().filter(o => o.id >= 1)
|
||||||
|
|
||||||
this.buildForm()
|
this.buildForm()
|
||||||
|
|
||||||
this.subscribeToSignupChanges()
|
this.subscribeToSignupChanges()
|
||||||
this.subscribeToImportSyncChanges()
|
this.subscribeToImportSyncChanges()
|
||||||
|
|
||||||
|
this.customConfigSub = this.adminConfigService.getCustomConfigReloadedObs()
|
||||||
|
.subscribe(customConfig => {
|
||||||
|
this.customConfig = customConfig
|
||||||
|
|
||||||
|
this.form.patchValue(this.customConfig)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy () {
|
||||||
|
if (this.customConfigSub) this.customConfigSub.unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildForm () {
|
private buildForm () {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<my-admin-save-bar i18n-title title="Edit your homepage" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
<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">
|
<form class="homepage pt-two-cols" [formGroup]="form">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>HOMEPAGE</h2>
|
<h2 i18n>HOMEPAGE</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,4 +25,4 @@
|
||||||
<div *ngIf="formErrors.homepageContent" class="form-error" role="alert">{{ formErrors.homepageContent }}</div>
|
<div *ngIf="formErrors.homepageContent" class="form-error" role="alert">{{ formErrors.homepageContent }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<my-admin-save-bar i18n-title title="Platform information" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
<my-admin-save-bar i18n-title title="Platform information" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||||
|
|
||||||
<ng-container [formGroup]="form">
|
<form [formGroup]="form">
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4">
|
<div class="pt-two-cols mt-4">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
|
@ -341,4 +341,4 @@
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
</ng-container>
|
</form>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { HttpErrorResponse } from '@angular/common/http'
|
import { HttpErrorResponse } from '@angular/common/http'
|
||||||
import { Component, OnInit, inject } from '@angular/core'
|
import { Component, OnInit, OnDestroy, inject } from '@angular/core'
|
||||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { ActivatedRoute, RouterLink } from '@angular/router'
|
import { ActivatedRoute, RouterLink } from '@angular/router'
|
||||||
import { CanComponentDeactivate, Notifier, ServerService } from '@app/core'
|
import { CanComponentDeactivate, Notifier, ServerService } from '@app/core'
|
||||||
|
@ -26,14 +26,15 @@ import { ActorImage, CustomConfig, HTMLServerConfig, NSFWPolicyType, VideoConsta
|
||||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||||
import { ActorAvatarEditComponent } from '../../../shared/shared-actor-image-edit/actor-avatar-edit.component'
|
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 { ActorBannerEditComponent } from '../../../shared/shared-actor-image-edit/actor-banner-edit.component'
|
||||||
|
import { AdminConfigService } from '../../../shared/shared-admin/admin-config.service'
|
||||||
import { CustomMarkupHelpComponent } from '../../../shared/shared-custom-markup/custom-markup-help.component'
|
import { CustomMarkupHelpComponent } from '../../../shared/shared-custom-markup/custom-markup-help.component'
|
||||||
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
||||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||||
import { SelectCheckboxComponent } from '../../../shared/shared-forms/select/select-checkbox.component'
|
import { SelectCheckboxComponent } from '../../../shared/shared-forms/select/select-checkbox.component'
|
||||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
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'
|
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||||
|
import { Subscription } from 'rxjs'
|
||||||
|
|
||||||
type Form = {
|
type Form = {
|
||||||
admin: FormGroup<{
|
admin: FormGroup<{
|
||||||
|
@ -97,7 +98,7 @@ type Form = {
|
||||||
AdminSaveBarComponent
|
AdminSaveBarComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AdminConfigInformationComponent implements OnInit, CanComponentDeactivate {
|
export class AdminConfigInformationComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||||
private customMarkup = inject(CustomMarkupService)
|
private customMarkup = inject(CustomMarkupService)
|
||||||
private notifier = inject(Notifier)
|
private notifier = inject(Notifier)
|
||||||
private instanceService = inject(InstanceService)
|
private instanceService = inject(InstanceService)
|
||||||
|
@ -137,6 +138,7 @@ export class AdminConfigInformationComponent implements OnInit, CanComponentDeac
|
||||||
|
|
||||||
private serverConfig: HTMLServerConfig
|
private serverConfig: HTMLServerConfig
|
||||||
private customConfig: CustomConfig
|
private customConfig: CustomConfig
|
||||||
|
private customConfigSub: Subscription
|
||||||
|
|
||||||
get instanceName () {
|
get instanceName () {
|
||||||
return this.server.getHTMLConfig().instance.name
|
return this.server.getHTMLConfig().instance.name
|
||||||
|
@ -157,6 +159,17 @@ export class AdminConfigInformationComponent implements OnInit, CanComponentDeac
|
||||||
|
|
||||||
this.updateActorImages()
|
this.updateActorImages()
|
||||||
this.buildForm()
|
this.buildForm()
|
||||||
|
|
||||||
|
this.customConfigSub = this.adminConfigService.getCustomConfigReloadedObs()
|
||||||
|
.subscribe(customConfig => {
|
||||||
|
this.customConfig = customConfig
|
||||||
|
|
||||||
|
this.form.patchValue(this.customConfig)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy () {
|
||||||
|
if (this.customConfigSub) this.customConfigSub.unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildForm () {
|
private buildForm () {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<my-admin-save-bar i18n-title title="Live configuration" (save)="save()" [form]="form" [formErrors]="formErrors" [inconsistentOptions]="checkTranscodingConsistentOptions()"></my-admin-save-bar>
|
<my-admin-save-bar i18n-title title="Live configuration" (save)="save()" [form]="form" [formErrors]="formErrors" [inconsistentOptions]="checkTranscodingConsistentOptions()"></my-admin-save-bar>
|
||||||
|
|
||||||
<ng-container [formGroup]="form">
|
<form [formGroup]="form">
|
||||||
|
|
||||||
<div class="pt-two-cols">
|
<div class="pt-two-cols">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
|
@ -212,4 +212,4 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</form>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { Component, OnInit, inject } from '@angular/core'
|
import { Component, OnInit, OnDestroy, inject } from '@angular/core'
|
||||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { ActivatedRoute, RouterLink } from '@angular/router'
|
import { ActivatedRoute, RouterLink } from '@angular/router'
|
||||||
import { CanComponentDeactivate, ServerService } from '@app/core'
|
import { CanComponentDeactivate, ServerService } from '@app/core'
|
||||||
|
@ -23,8 +23,9 @@ import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube
|
||||||
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
||||||
import { AdminConfigService, FormResolutions, ResolutionOption } from '../shared/admin-config.service'
|
import { AdminConfigService, FormResolutions, ResolutionOption } from '../../../shared/shared-admin/admin-config.service'
|
||||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||||
|
import { Subscription } from 'rxjs'
|
||||||
|
|
||||||
type Form = {
|
type Form = {
|
||||||
live: FormGroup<{
|
live: FormGroup<{
|
||||||
|
@ -73,7 +74,7 @@ type Form = {
|
||||||
AdminSaveBarComponent
|
AdminSaveBarComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AdminConfigLiveComponent implements OnInit, CanComponentDeactivate {
|
export class AdminConfigLiveComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||||
private configService = inject(AdminConfigService)
|
private configService = inject(AdminConfigService)
|
||||||
private server = inject(ServerService)
|
private server = inject(ServerService)
|
||||||
private route = inject(ActivatedRoute)
|
private route = inject(ActivatedRoute)
|
||||||
|
@ -91,6 +92,7 @@ export class AdminConfigLiveComponent implements OnInit, CanComponentDeactivate
|
||||||
liveResolutions: ResolutionOption[] = []
|
liveResolutions: ResolutionOption[] = []
|
||||||
|
|
||||||
private customConfig: CustomConfig
|
private customConfig: CustomConfig
|
||||||
|
private customConfigSub: Subscription
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||||
|
@ -111,6 +113,17 @@ export class AdminConfigLiveComponent implements OnInit, CanComponentDeactivate
|
||||||
)
|
)
|
||||||
|
|
||||||
this.buildForm()
|
this.buildForm()
|
||||||
|
|
||||||
|
this.customConfigSub = this.adminConfigService.getCustomConfigReloadedObs()
|
||||||
|
.subscribe(customConfig => {
|
||||||
|
this.customConfig = customConfig
|
||||||
|
|
||||||
|
this.form.patchValue(this.customConfig)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy () {
|
||||||
|
if (this.customConfigSub) this.customConfigSub.unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildForm () {
|
private buildForm () {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<my-admin-save-bar i18n-title title="VOD configuration" (save)="save()" [form]="form" [formErrors]="formErrors" [inconsistentOptions]="checkTranscodingConsistentOptions()"></my-admin-save-bar>
|
<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">
|
<form [formGroup]="form">
|
||||||
|
|
||||||
<div class="pt-two-cols">
|
<div class="pt-two-cols">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
|
@ -281,4 +281,4 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</form>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { Component, OnInit, inject } from '@angular/core'
|
import { Component, OnInit, OnDestroy, inject } from '@angular/core'
|
||||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { ActivatedRoute, RouterLink } from '@angular/router'
|
import { ActivatedRoute, RouterLink } from '@angular/router'
|
||||||
import { CanComponentDeactivate, Notifier, ServerService } from '@app/core'
|
import { CanComponentDeactivate, Notifier, ServerService } from '@app/core'
|
||||||
|
@ -16,12 +16,13 @@ import {
|
||||||
} from '@app/shared/form-validators/form-validator.model'
|
} from '@app/shared/form-validators/form-validator.model'
|
||||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
import { CustomConfig } from '@peertube/peertube-models'
|
import { CustomConfig } from '@peertube/peertube-models'
|
||||||
|
import { Subscription } from 'rxjs'
|
||||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||||
|
import { AdminConfigService, FormResolutions, ResolutionOption } from '../../../shared/shared-admin/admin-config.service'
|
||||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||||
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
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'
|
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||||
|
|
||||||
type Form = {
|
type Form = {
|
||||||
|
@ -84,7 +85,7 @@ type Form = {
|
||||||
AdminSaveBarComponent
|
AdminSaveBarComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AdminConfigVODComponent implements OnInit, CanComponentDeactivate {
|
export class AdminConfigVODComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||||
private configService = inject(AdminConfigService)
|
private configService = inject(AdminConfigService)
|
||||||
private notifier = inject(Notifier)
|
private notifier = inject(Notifier)
|
||||||
private server = inject(ServerService)
|
private server = inject(ServerService)
|
||||||
|
@ -103,6 +104,7 @@ export class AdminConfigVODComponent implements OnInit, CanComponentDeactivate {
|
||||||
additionalVideoExtensions = ''
|
additionalVideoExtensions = ''
|
||||||
|
|
||||||
private customConfig: CustomConfig
|
private customConfig: CustomConfig
|
||||||
|
private customConfigSub: Subscription
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
const serverConfig = this.server.getHTMLConfig()
|
const serverConfig = this.server.getHTMLConfig()
|
||||||
|
@ -117,6 +119,17 @@ export class AdminConfigVODComponent implements OnInit, CanComponentDeactivate {
|
||||||
this.buildForm()
|
this.buildForm()
|
||||||
|
|
||||||
this.subscribeToTranscodingChanges()
|
this.subscribeToTranscodingChanges()
|
||||||
|
|
||||||
|
this.customConfigSub = this.adminConfigService.getCustomConfigReloadedObs()
|
||||||
|
.subscribe(customConfig => {
|
||||||
|
this.customConfig = customConfig
|
||||||
|
|
||||||
|
this.form.patchValue(this.customConfig)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy () {
|
||||||
|
if (this.customConfigSub) this.customConfigSub.unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildForm () {
|
private buildForm () {
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
<div class="root-bar">
|
<div class="root-bar">
|
||||||
<h2>{{ title() }}</h2>
|
<h2>{{ title() }}</h2>
|
||||||
|
|
||||||
<my-button
|
<div class="buttons">
|
||||||
theme="primary" class="save-button" icon="circle-tick"
|
<my-button theme="secondary" class="pre-config" (click)="openConfigWizard()" i18n>Open config wizard</my-button>
|
||||||
[disabled]="!canUpdate()" (click)="onSave($event)" i18n
|
|
||||||
>Save</my-button>
|
<my-button theme="primary" class="save-button" icon="circle-tick" [disabled]="!canUpdate()" (click)="onSave($event)" i18n>Save</my-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!isUpdateAllowed()) {
|
@if (!isUpdateAllowed()) {
|
||||||
|
|
|
@ -25,10 +25,16 @@
|
||||||
@include rfs(1.5rem, padding);
|
@include rfs(1.5rem, padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-button {
|
.buttons {
|
||||||
@include margin-left(auto);
|
@include margin-left(auto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pre-config {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
@include margin-right(0.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
color: pvar(--fg-350);
|
color: pvar(--fg-350);
|
||||||
|
@ -48,7 +54,7 @@ h2 {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-button,
|
.buttons,
|
||||||
h2 {
|
h2 {
|
||||||
@include margin-left(0);
|
@include margin-left(0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { RouterModule } from '@angular/router'
|
||||||
import { ScreenService, ServerService } from '@app/core'
|
import { ScreenService, ServerService } from '@app/core'
|
||||||
import { HeaderService } from '@app/header/header.service'
|
import { HeaderService } from '@app/header/header.service'
|
||||||
import { FormReactiveErrors, FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
import { FormReactiveErrors, FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
|
import { PeertubeModalService } from '@app/shared/shared-main/peertube-modal/peertube-modal.service'
|
||||||
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
|
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
|
||||||
import { AlertComponent } from '../../../shared/shared-main/common/alert.component'
|
import { AlertComponent } from '../../../shared/shared-main/common/alert.component'
|
||||||
|
|
||||||
|
@ -24,6 +25,7 @@ export class AdminSaveBarComponent implements OnInit, OnDestroy {
|
||||||
private server = inject(ServerService)
|
private server = inject(ServerService)
|
||||||
private headerService = inject(HeaderService)
|
private headerService = inject(HeaderService)
|
||||||
private screenService = inject(ScreenService)
|
private screenService = inject(ScreenService)
|
||||||
|
private peertubeModal = inject(PeertubeModalService)
|
||||||
|
|
||||||
readonly title = input.required<string>()
|
readonly title = input.required<string>()
|
||||||
readonly form = input.required<FormGroup>()
|
readonly form = input.required<FormGroup>()
|
||||||
|
@ -59,6 +61,10 @@ export class AdminSaveBarComponent implements OnInit, OnDestroy {
|
||||||
return this.formReactiveService.grabAllErrors(this.formErrors())
|
return this.formReactiveService.grabAllErrors(this.formErrors())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openConfigWizard () {
|
||||||
|
this.peertubeModal.openAdminConfigWizardSubject.next({ showWelcome: false })
|
||||||
|
}
|
||||||
|
|
||||||
onSave (event: Event) {
|
onSave (event: Event) {
|
||||||
this.displayFormErrors = false
|
this.displayFormErrors = false
|
||||||
|
|
||||||
|
|
|
@ -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 { AdminConfigService } from '@app/+admin/config/shared/admin-config.service'
|
import { AdminConfigService } from '@app/shared/shared-admin/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,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Directive, OnInit } from '@angular/core'
|
import { Directive, OnInit } from '@angular/core'
|
||||||
import { AdminConfigService } from '@app/+admin/config/shared/admin-config.service'
|
import { AdminConfigService } from '@app/shared/shared-admin/admin-config.service'
|
||||||
import { getVideoQuotaDailyOptions, getVideoQuotaOptions } from '@app/+admin/shared/user-quota-options'
|
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 { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
||||||
|
|
|
@ -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 { AdminConfigService } from '@app/+admin/config/shared/admin-config.service'
|
import { AdminConfigService } from '@app/shared/shared-admin/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,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { NgFor, NgIf } from '@angular/common'
|
import { NgFor, NgIf } from '@angular/common'
|
||||||
import { Component, OnInit, inject } from '@angular/core'
|
import { Component, OnInit, inject } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
|
import { PluginApiService } from '@app/shared/shared-admin/plugin-api.service'
|
||||||
import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, resetCurrentPage, updatePaginationOnDelete } from '@app/core'
|
import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, resetCurrentPage, updatePaginationOnDelete } from '@app/core'
|
||||||
import { PluginService } from '@app/core/plugins/plugin.service'
|
import { PluginService } from '@app/core/plugins/plugin.service'
|
||||||
import { compareSemVer } from '@peertube/peertube-core-utils'
|
import { compareSemVer } from '@peertube/peertube-core-utils'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { NgFor, NgIf } from '@angular/common'
|
import { NgFor, NgIf } from '@angular/common'
|
||||||
import { Component, OnInit, inject } from '@angular/core'
|
import { Component, OnInit, inject } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
|
import { PluginApiService } from '@app/shared/shared-admin/plugin-api.service'
|
||||||
import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService, resetCurrentPage } from '@app/core'
|
import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService, resetCurrentPage } from '@app/core'
|
||||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||||
import { PeerTubePluginIndex, PluginType, PluginType_Type } from '@peertube/peertube-models'
|
import { PeerTubePluginIndex, PluginType, PluginType_Type } from '@peertube/peertube-models'
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { HooksService, Notifier, PluginService } from '@app/core'
|
||||||
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
||||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
import { PeerTubePlugin, RegisterServerSettingOptions } from '@peertube/peertube-models'
|
import { PeerTubePlugin, RegisterServerSettingOptions } from '@peertube/peertube-models'
|
||||||
import { PluginApiService } from '../shared/plugin-api.service'
|
import { PluginApiService } from '../../../shared/shared-admin/plugin-api.service'
|
||||||
import { DynamicFormFieldComponent } from '../../../shared/shared-forms/dynamic-form-field.component'
|
import { DynamicFormFieldComponent } from '../../../shared/shared-forms/dynamic-form-field.component'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NgIf, NgFor } from '@angular/common'
|
import { NgIf, NgFor } from '@angular/common'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Component, inject, input } from '@angular/core'
|
import { Component, inject, input } from '@angular/core'
|
||||||
import { PeerTubePlugin, PeerTubePluginIndex, PluginType_Type } from '@peertube/peertube-models'
|
import { PeerTubePlugin, PeerTubePluginIndex, PluginType_Type } from '@peertube/peertube-models'
|
||||||
import { PluginApiService } from './plugin-api.service'
|
import { PluginApiService } from '../../../shared/shared-admin/plugin-api.service'
|
||||||
import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component'
|
import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|
|
@ -21,11 +21,11 @@ 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 { AdminConfigService } from './config/shared/admin-config.service'
|
import { AdminConfigService } from '../shared/shared-admin/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'
|
||||||
import { PluginApiService } from './plugins/shared/plugin-api.service'
|
import { PluginApiService } from '../shared/shared-admin/plugin-api.service'
|
||||||
|
|
||||||
const commonConfig = {
|
const commonConfig = {
|
||||||
path: '',
|
path: '',
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { SelectOptionsItem } from '../../../types/select-options-item.model'
|
import { SelectOptionsItem } from '../../../types/select-options-item.model'
|
||||||
|
|
||||||
export function getVideoQuotaOptions (): SelectOptionsItem[] {
|
export function getVideoQuotaOptions (): SelectOptionsItem<number>[] {
|
||||||
return [
|
return [
|
||||||
{ id: -1, label: $localize`Unlimited` },
|
{ id: -1, label: $localize`Unlimited` },
|
||||||
{ id: 0, label: $localize`None - no upload possible` },
|
{ id: 0, label: $localize`None - no upload possible` },
|
||||||
|
@ -16,7 +16,7 @@ export function getVideoQuotaOptions (): SelectOptionsItem[] {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getVideoQuotaDailyOptions (): SelectOptionsItem[] {
|
export function getVideoQuotaDailyOptions (): SelectOptionsItem<number>[] {
|
||||||
return [
|
return [
|
||||||
{ id: -1, label: $localize`Unlimited` },
|
{ id: -1, label: $localize`Unlimited` },
|
||||||
{ id: 0, label: $localize`None - no upload possible` },
|
{ id: 0, label: $localize`None - no upload possible` },
|
||||||
|
|
|
@ -4,14 +4,13 @@ import { GlobalIconComponent } from '../../shared/shared-icons/global-icon.compo
|
||||||
import { NgIf, NgFor, NgClass, NgTemplateOutlet } from '@angular/common'
|
import { NgIf, NgFor, NgClass, NgTemplateOutlet } from '@angular/common'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-custom-stepper',
|
selector: 'my-register-stepper',
|
||||||
templateUrl: './custom-stepper.component.html',
|
templateUrl: './register-stepper.component.html',
|
||||||
styleUrls: [ './custom-stepper.component.scss' ],
|
styleUrls: [ './register-stepper.component.scss' ],
|
||||||
providers: [ { provide: CdkStepper, useExisting: CustomStepperComponent } ],
|
providers: [ { provide: CdkStepper, useExisting: RegisterStepperComponent } ],
|
||||||
imports: [ NgIf, NgFor, NgClass, GlobalIconComponent, NgTemplateOutlet ]
|
imports: [ NgIf, NgFor, NgClass, GlobalIconComponent, NgTemplateOutlet ]
|
||||||
})
|
})
|
||||||
export class CustomStepperComponent extends CdkStepper {
|
export class RegisterStepperComponent extends CdkStepper {
|
||||||
|
|
||||||
onClick (index: number): void {
|
onClick (index: number): void {
|
||||||
this.selectedIndex = index
|
this.selectedIndex = index
|
||||||
}
|
}
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<ng-container *ngIf="!signupDisabled">
|
<ng-container *ngIf="!signupDisabled">
|
||||||
<div class="register-content">
|
<div class="register-content">
|
||||||
<my-custom-stepper linear>
|
<my-register-stepper linear>
|
||||||
|
|
||||||
<cdk-step i18n-label label="About" [editable]="!signupSuccess">
|
<cdk-step i18n-label label="About" [editable]="!signupSuccess">
|
||||||
<my-signup-step-title mascotImageName="about">
|
<my-signup-step-title mascotImageName="about">
|
||||||
|
@ -119,7 +119,7 @@
|
||||||
<button class="peertube-button-big secondary-button" cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button>
|
<button class="peertube-button-big secondary-button" cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button>
|
||||||
</div>
|
</div>
|
||||||
</cdk-step>
|
</cdk-step>
|
||||||
</my-custom-stepper>
|
</my-register-stepper>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { SignupLabelComponent } from '../../shared/shared-main/users/signup-labe
|
||||||
import { SignupStepTitleComponent } from '../shared/signup-step-title.component'
|
import { SignupStepTitleComponent } from '../shared/signup-step-title.component'
|
||||||
import { SignupSuccessBeforeEmailComponent } from '../shared/signup-success-before-email.component'
|
import { SignupSuccessBeforeEmailComponent } from '../shared/signup-success-before-email.component'
|
||||||
import { SignupService } from '../shared/signup.service'
|
import { SignupService } from '../shared/signup.service'
|
||||||
import { CustomStepperComponent } from './custom-stepper.component'
|
import { RegisterStepperComponent } from './register-stepper.component'
|
||||||
import { RegisterStepAboutComponent } from './steps/register-step-about.component'
|
import { RegisterStepAboutComponent } from './steps/register-step-about.component'
|
||||||
import { RegisterStepChannelComponent } from './steps/register-step-channel.component'
|
import { RegisterStepChannelComponent } from './steps/register-step-channel.component'
|
||||||
import { RegisterStepTermsComponent } from './steps/register-step-terms.component'
|
import { RegisterStepTermsComponent } from './steps/register-step-terms.component'
|
||||||
|
@ -26,7 +26,7 @@ import { RegisterStepUserComponent } from './steps/register-step-user.component'
|
||||||
imports: [
|
imports: [
|
||||||
NgIf,
|
NgIf,
|
||||||
SignupLabelComponent,
|
SignupLabelComponent,
|
||||||
CustomStepperComponent,
|
RegisterStepperComponent,
|
||||||
CdkStep,
|
CdkStep,
|
||||||
SignupStepTitleComponent,
|
SignupStepTitleComponent,
|
||||||
RegisterStepAboutComponent,
|
RegisterStepAboutComponent,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
@use '_variables' as *;
|
@use "_variables" as *;
|
||||||
@use '_mixins' as *;
|
@use "_css-variables" as *;
|
||||||
@use '_form-mixins' as *;
|
@use "_mixins" as *;
|
||||||
|
@use "_form-mixins" as *;
|
||||||
|
|
||||||
$width-size: 275px;
|
$width-size: 275px;
|
||||||
|
|
||||||
|
@ -13,12 +14,12 @@ $width-size: 275px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.first-step-block {
|
.first-step-block {
|
||||||
--input-bg: #{pvar(--bg-secondary-500)};
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
@include define-input-css-variables-in-modal;
|
||||||
|
|
||||||
.upload-icon {
|
.upload-icon {
|
||||||
width: 90px;
|
width: 90px;
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
|
@ -45,7 +46,7 @@ $width-size: 275px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=text] {
|
input[type="text"] {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
@include peertube-input-text($width-size);
|
@include peertube-input-text($width-size);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@use '_variables' as *;
|
@use "_variables" as *;
|
||||||
@use '_mixins' as *;
|
@use "_mixins" as *;
|
||||||
|
|
||||||
.caption-raw-textarea,
|
.caption-raw-textarea,
|
||||||
.segments {
|
.segments {
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
&.active,
|
&.active,
|
||||||
&:hover {
|
&:hover {
|
||||||
background: pvar(--bg-secondary-300);
|
background: pvar(--bg-secondary-400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,8 +55,10 @@
|
||||||
|
|
||||||
@defer (when isUserLoggedIn()) {
|
@defer (when isUserLoggedIn()) {
|
||||||
<my-account-setup-warning-modal #accountSetupWarningModal (created)="onModalCreated()"></my-account-setup-warning-modal>
|
<my-account-setup-warning-modal #accountSetupWarningModal (created)="onModalCreated()"></my-account-setup-warning-modal>
|
||||||
|
}
|
||||||
|
|
||||||
<my-admin-welcome-modal #adminWelcomeModal (created)="onModalCreated()"></my-admin-welcome-modal>
|
@defer (when isUserAdmin()) {
|
||||||
|
<my-admin-config-wizard-modal #adminConfigWizardModal (created)="onModalCreated()"></my-admin-config-wizard-modal>
|
||||||
<my-instance-config-warning-modal #instanceConfigWarningModal (created)="onModalCreated()"></my-instance-config-warning-modal>
|
<my-instance-config-warning-modal #instanceConfigWarningModal (created)="onModalCreated()"></my-instance-config-warning-modal>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { forkJoin } from 'rxjs'
|
|
||||||
import { filter, first, map } from 'rxjs/operators'
|
|
||||||
import { DOCUMENT, getLocaleDirection, NgClass, NgIf, PlatformLocation } from '@angular/common'
|
import { DOCUMENT, getLocaleDirection, NgClass, NgIf, PlatformLocation } from '@angular/common'
|
||||||
import { AfterViewInit, Component, LOCALE_ID, OnDestroy, OnInit, inject, viewChild } from '@angular/core'
|
import { AfterViewInit, Component, inject, LOCALE_ID, OnDestroy, OnInit, viewChild } from '@angular/core'
|
||||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
|
||||||
import { Event, GuardsCheckStart, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouterOutlet } from '@angular/router'
|
import { ActivatedRoute, Event, GuardsCheckStart, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouterOutlet } from '@angular/router'
|
||||||
import {
|
import {
|
||||||
AuthService,
|
AuthService,
|
||||||
Hotkey,
|
Hotkey,
|
||||||
|
@ -19,7 +17,7 @@ import {
|
||||||
import { HooksService } from '@app/core/plugins/hooks.service'
|
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||||
import { PluginService } from '@app/core/plugins/plugin.service'
|
import { PluginService } from '@app/core/plugins/plugin.service'
|
||||||
import { AccountSetupWarningModalComponent } from '@app/modal/account-setup-warning-modal.component'
|
import { AccountSetupWarningModalComponent } from '@app/modal/account-setup-warning-modal.component'
|
||||||
import { AdminWelcomeModalComponent } from '@app/modal/admin-welcome-modal.component'
|
import { AdminConfigWizardModalComponent } from '@app/modal/admin-config-wizard/admin-config-wizard-modal.component'
|
||||||
import { CustomModalComponent } from '@app/modal/custom-modal.component'
|
import { CustomModalComponent } from '@app/modal/custom-modal.component'
|
||||||
import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
|
import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
|
||||||
import { NgbConfig, NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbConfig, NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
@ -30,6 +28,8 @@ import { logger } from '@root-helpers/logger'
|
||||||
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
||||||
import { SharedModule } from 'primeng/api'
|
import { SharedModule } from 'primeng/api'
|
||||||
import { ToastModule } from 'primeng/toast'
|
import { ToastModule } from 'primeng/toast'
|
||||||
|
import { forkJoin } from 'rxjs'
|
||||||
|
import { filter, first, map } from 'rxjs/operators'
|
||||||
import { MenuService } from './core/menu/menu.service'
|
import { MenuService } from './core/menu/menu.service'
|
||||||
import { HeaderComponent } from './header/header.component'
|
import { HeaderComponent } from './header/header.component'
|
||||||
import { POP_STATE_MODAL_DISMISS } from './helpers'
|
import { POP_STATE_MODAL_DISMISS } from './helpers'
|
||||||
|
@ -37,8 +37,8 @@ import { HotkeysCheatSheetComponent } from './hotkeys/hotkeys-cheat-sheet.compon
|
||||||
import { MenuComponent } from './menu/menu.component'
|
import { MenuComponent } from './menu/menu.component'
|
||||||
import { ConfirmComponent } from './modal/confirm.component'
|
import { ConfirmComponent } from './modal/confirm.component'
|
||||||
import { GlobalIconComponent, GlobalIconName } from './shared/shared-icons/global-icon.component'
|
import { GlobalIconComponent, GlobalIconName } from './shared/shared-icons/global-icon.component'
|
||||||
|
|
||||||
import { InstanceService } from './shared/shared-main/instance/instance.service'
|
import { InstanceService } from './shared/shared-main/instance/instance.service'
|
||||||
|
import { PeertubeModalService } from './shared/shared-main/peertube-modal/peertube-modal.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-app',
|
selector: 'my-app',
|
||||||
|
@ -57,9 +57,9 @@ import { InstanceService } from './shared/shared-main/instance/instance.service'
|
||||||
ToastModule,
|
ToastModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
AccountSetupWarningModalComponent,
|
AccountSetupWarningModalComponent,
|
||||||
AdminWelcomeModalComponent,
|
|
||||||
InstanceConfigWarningModalComponent,
|
InstanceConfigWarningModalComponent,
|
||||||
CustomModalComponent
|
CustomModalComponent,
|
||||||
|
AdminConfigWizardModalComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
@ -82,12 +82,15 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
private loadingBar = inject(LoadingBarService)
|
private loadingBar = inject(LoadingBarService)
|
||||||
private scrollService = inject(ScrollService)
|
private scrollService = inject(ScrollService)
|
||||||
private userLocalStorage = inject(UserLocalStorageService)
|
private userLocalStorage = inject(UserLocalStorageService)
|
||||||
|
private peertubeModal = inject(PeertubeModalService)
|
||||||
|
private route = inject(ActivatedRoute)
|
||||||
|
|
||||||
menu = inject(MenuService)
|
menu = inject(MenuService)
|
||||||
|
|
||||||
private static LS_BROADCAST_MESSAGE = 'app-broadcast-message-dismissed'
|
private static LS_BROADCAST_MESSAGE = 'app-broadcast-message-dismissed'
|
||||||
|
|
||||||
readonly accountSetupWarningModal = viewChild<AccountSetupWarningModalComponent>('accountSetupWarningModal')
|
readonly accountSetupWarningModal = viewChild<AccountSetupWarningModalComponent>('accountSetupWarningModal')
|
||||||
readonly adminWelcomeModal = viewChild<AdminWelcomeModalComponent>('adminWelcomeModal')
|
readonly adminConfigWizardModal = viewChild<AdminConfigWizardModalComponent>('adminConfigWizardModal')
|
||||||
readonly instanceConfigWarningModal = viewChild<InstanceConfigWarningModalComponent>('instanceConfigWarningModal')
|
readonly instanceConfigWarningModal = viewChild<InstanceConfigWarningModalComponent>('instanceConfigWarningModal')
|
||||||
readonly customModal = viewChild<CustomModalComponent>('customModal')
|
readonly customModal = viewChild<CustomModalComponent>('customModal')
|
||||||
|
|
||||||
|
@ -154,6 +157,13 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.peertubeModal.openAdminConfigWizardSubject.subscribe(({ showWelcome }) => {
|
||||||
|
const adminWelcomeModal = this.adminConfigWizardModal()
|
||||||
|
if (!adminWelcomeModal) return
|
||||||
|
|
||||||
|
adminWelcomeModal.show({ showWelcome })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit () {
|
ngAfterViewInit () {
|
||||||
|
@ -171,6 +181,10 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return this.authService.isLoggedIn()
|
return this.authService.isLoggedIn()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isUserAdmin () {
|
||||||
|
return this.isUserLoggedIn() && this.authService.getUser().role.id === UserRole.ADMINISTRATOR
|
||||||
|
}
|
||||||
|
|
||||||
hideBroadcastMessage () {
|
hideBroadcastMessage () {
|
||||||
peertubeLocalStorage.setItem(AppComponent.LS_BROADCAST_MESSAGE, this.serverConfig.broadcastMessage.message)
|
peertubeLocalStorage.setItem(AppComponent.LS_BROADCAST_MESSAGE, this.serverConfig.broadcastMessage.message)
|
||||||
|
|
||||||
|
@ -301,23 +315,23 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private openAdminModalsIfNeeded (user: User) {
|
private openAdminModalsIfNeeded (user: User) {
|
||||||
const adminWelcomeModal = this.adminWelcomeModal()
|
const adminWelcomeModal = this.adminConfigWizardModal()
|
||||||
if (!adminWelcomeModal) return
|
if (!adminWelcomeModal) return
|
||||||
|
|
||||||
if (adminWelcomeModal.shouldOpen(user)) {
|
if (adminWelcomeModal.shouldAutoOpen(user)) {
|
||||||
return adminWelcomeModal.show()
|
return adminWelcomeModal.show({ showWelcome: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const instanceConfigWarningModal = this.instanceConfigWarningModal()
|
const instanceConfigWarningModal = this.instanceConfigWarningModal()
|
||||||
if (!instanceConfigWarningModal) return
|
if (!instanceConfigWarningModal) return
|
||||||
if (!instanceConfigWarningModal.shouldOpenByUser(user)) return
|
if (!instanceConfigWarningModal.canBeOpenByUser(user)) return
|
||||||
|
|
||||||
forkJoin([
|
forkJoin([
|
||||||
this.serverService.getConfig().pipe(first()),
|
this.serverService.getConfig().pipe(first()),
|
||||||
this.instanceService.getAbout().pipe(first())
|
this.instanceService.getAbout().pipe(first())
|
||||||
]).subscribe(([ config, about ]) => {
|
]).subscribe(([ config, about ]) => {
|
||||||
const instanceConfigWarningModalValue = this.instanceConfigWarningModal()
|
const instanceConfigWarningModalValue = this.instanceConfigWarningModal()
|
||||||
if (instanceConfigWarningModalValue.shouldOpen(config, about)) {
|
if (instanceConfigWarningModalValue.shouldAutoOpen(config, about)) {
|
||||||
instanceConfigWarningModalValue.show(about)
|
instanceConfigWarningModalValue.show(about)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -327,7 +341,7 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
const accountSetupWarningModal = this.accountSetupWarningModal()
|
const accountSetupWarningModal = this.accountSetupWarningModal()
|
||||||
if (!accountSetupWarningModal) return
|
if (!accountSetupWarningModal) return
|
||||||
|
|
||||||
if (accountSetupWarningModal.shouldOpen(user)) {
|
if (accountSetupWarningModal.shouldAutoOpen(user)) {
|
||||||
accountSetupWarningModal.show(user)
|
accountSetupWarningModal.show(user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,11 @@ export class HtmlRendererService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async toSimpleSafeHtml (text: string, options: {
|
toSimpleSafeHtml (text: string) {
|
||||||
|
return this.sanitize(this.simpleDomPurify, this.removeClassAttributes(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
async toSimpleSafeHtmlWithLinks (text: string, options: {
|
||||||
allowImages?: boolean
|
allowImages?: boolean
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const { allowImages = false } = options
|
const { allowImages = false } = options
|
||||||
|
@ -89,6 +93,7 @@ export class HtmlRendererService {
|
||||||
const additionalTags = allowImages
|
const additionalTags = allowImages
|
||||||
? [ 'img' ]
|
? [ 'img' ]
|
||||||
: []
|
: []
|
||||||
|
|
||||||
const additionalAttributes = allowImages
|
const additionalAttributes = allowImages
|
||||||
? [ 'src', 'alt' ]
|
? [ 'src', 'alt' ]
|
||||||
: []
|
: []
|
||||||
|
|
|
@ -149,10 +149,10 @@ export class MarkdownService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'enhancedMarkdownIt' || name === 'enhancedWithHTMLMarkdownIt') {
|
if (name === 'enhancedMarkdownIt' || name === 'enhancedWithHTMLMarkdownIt') {
|
||||||
return this.htmlRenderer.toSimpleSafeHtml(html, { allowImages: true })
|
return this.htmlRenderer.toSimpleSafeHtmlWithLinks(html, { allowImages: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.htmlRenderer.toSimpleSafeHtml(html)
|
return this.htmlRenderer.toSimpleSafeHtmlWithLinks(html)
|
||||||
}
|
}
|
||||||
|
|
||||||
return html
|
return html
|
||||||
|
|
|
@ -66,6 +66,7 @@ export class ServerService {
|
||||||
|
|
||||||
resetConfig () {
|
resetConfig () {
|
||||||
this.configLoaded = false
|
this.configLoaded = false
|
||||||
|
this.configObservable = undefined
|
||||||
|
|
||||||
// Notify config update
|
// Notify config update
|
||||||
return this.getConfig({ isReset: true })
|
return this.getConfig({ isReset: true })
|
||||||
|
|
|
@ -195,13 +195,13 @@ export default {
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
option: {
|
option: {
|
||||||
focusBackground: 'var(--bg-secondary-500)',
|
focusBackground: 'var(--bg-secondary-450)',
|
||||||
selectedBackground: '{highlight.background}',
|
selectedBackground: 'var(--bg-secondary-500)',
|
||||||
selectedFocusBackground: 'var(--bg-secondary-500)',
|
selectedFocusBackground: 'var(--bg-secondary-500)',
|
||||||
color: '{text.color}',
|
color: '{text.color}',
|
||||||
focusColor: '{text.hover.color}',
|
focusColor: '{text.color}',
|
||||||
selectedColor: '{highlight.color}',
|
selectedColor: '{text.color}',
|
||||||
selectedFocusColor: '{highlight.focus.color}',
|
selectedFocusColor: '{text.color}',
|
||||||
icon: {
|
icon: {
|
||||||
color: '{surface.400}',
|
color: '{surface.400}',
|
||||||
focusColor: '{surface.500}'
|
focusColor: '{surface.500}'
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { PluginService } from '../plugins/plugin.service'
|
||||||
import { ServerService } from '../server'
|
import { ServerService } from '../server'
|
||||||
import { UserService } from '../users/user.service'
|
import { UserService } from '../users/user.service'
|
||||||
import { LocalStorageService } from '../wrappers/storage.service'
|
import { LocalStorageService } from '../wrappers/storage.service'
|
||||||
|
import { formatHEX, parse } from 'color-bits'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ThemeService {
|
export class ThemeService {
|
||||||
|
@ -216,4 +217,31 @@ export class ThemeService {
|
||||||
private getTheme (name: string) {
|
private getTheme (name: string) {
|
||||||
return this.themes.find(t => t.name === name)
|
return this.themes.find(t => t.name === name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Utils
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
formatColorForForm (value: string) {
|
||||||
|
if (!value) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
return formatHEX(parse(value))
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`Error parsing color value "${value}"`, err)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 + ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ export class AccountSetupWarningModalComponent implements OnInit {
|
||||||
return !!user.account.description
|
return !!user.account.description
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldOpen (user: User) {
|
shouldAutoOpen (user: User) {
|
||||||
if (this.modalService.hasOpenModals()) return false
|
if (this.modalService.hasOpenModals()) return false
|
||||||
if (user.noAccountSetupWarningModal === true) return false
|
if (user.noAccountSetupWarningModal === true) return false
|
||||||
if (peertubeLocalStorage.getItem(this.LS_KEYS.NO_ACCOUNT_SETUP_WARNING_MODAL) === 'true') return false
|
if (peertubeLocalStorage.getItem(this.LS_KEYS.NO_ACCOUNT_SETUP_WARNING_MODAL) === 'true') return false
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
<ng-template #modal let-hide="close">
|
||||||
|
<div class="modal-body">
|
||||||
|
<my-admin-config-wizard-stepper #stepper>
|
||||||
|
@if (showWelcome) {
|
||||||
|
<cdk-step i18n-label label="Welcome introduction">
|
||||||
|
<my-admin-config-wizard-welcome (back)="stepper.previous()" (next)="stepper.next()" (hide)="hide()"></my-admin-config-wizard-welcome>
|
||||||
|
</cdk-step>
|
||||||
|
}
|
||||||
|
|
||||||
|
<cdk-step i18n-label label="Edit general information">
|
||||||
|
<my-admin-config-wizard-edit-info
|
||||||
|
[currentStep]="currentStep()" totalSteps="3"
|
||||||
|
(back)="stepper.previous()" (next)="instanceInfo = $event; stepper.next()" (hide)="hide()"
|
||||||
|
[showBack]="showWelcome"
|
||||||
|
></my-admin-config-wizard-edit-info>
|
||||||
|
</cdk-step>
|
||||||
|
|
||||||
|
<cdk-step i18n-label label="Usage type">
|
||||||
|
<my-admin-config-wizard-form
|
||||||
|
[currentStep]="currentStep()" totalSteps="3"
|
||||||
|
(back)="stepper.previous()" (next)="usageType = $event; stepper.next()" (hide)="hide()"
|
||||||
|
></my-admin-config-wizard-form>
|
||||||
|
</cdk-step>
|
||||||
|
|
||||||
|
<cdk-step i18n-label label="Configuration preview">
|
||||||
|
<my-admin-config-wizard-preview
|
||||||
|
[usageType]="usageType" [instanceInfo]="instanceInfo"
|
||||||
|
[currentStep]="currentStep()" totalSteps="3"
|
||||||
|
(back)="stepper.previous()" (next)="showWelcome ? stepper.next() : hide()" (hide)="hide()"
|
||||||
|
></my-admin-config-wizard-preview>
|
||||||
|
</cdk-step>
|
||||||
|
|
||||||
|
@if (showWelcome) {
|
||||||
|
<cdk-step i18n-label label="Post configuration documentation">
|
||||||
|
<my-admin-config-wizard-documentation (hide)="hide()"></my-admin-config-wizard-documentation>
|
||||||
|
</cdk-step>
|
||||||
|
}
|
||||||
|
</my-admin-config-wizard-stepper>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
|
@ -0,0 +1,6 @@
|
||||||
|
@use "_variables" as *;
|
||||||
|
@use "_mixins" as *;
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 2rem 3rem;
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { CdkStepperModule } from '@angular/cdk/stepper'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, ElementRef, OnInit, inject, output, viewChild } from '@angular/core'
|
||||||
|
import { ActivatedRoute } from '@angular/router'
|
||||||
|
import { User } from '@app/core'
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
||||||
|
import { AdminConfigWizardStepperComponent } from './admin-config-wizard-stepper.component'
|
||||||
|
import { getNoWelcomeModalLocalStorageKey } from './shared/admin-config-wizard-modal-utils'
|
||||||
|
import { AdminConfigWizardDocumentationComponent } from './steps/admin-config-wizard-documentation.component'
|
||||||
|
import { AdminConfigWizardEditInfoComponent, FormInfo } from './steps/admin-config-wizard-edit-info.component'
|
||||||
|
import { AdminConfigWizardFormComponent } from './steps/admin-config-wizard-form.component'
|
||||||
|
import { AdminConfigWizardPreviewComponent } from './steps/admin-config-wizard-preview.component'
|
||||||
|
import { AdminConfigWizardWelcomeComponent } from './steps/admin-config-wizard-welcome.component'
|
||||||
|
import { UsageType } from './steps/usage-type/usage-type.model'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-admin-config-wizard-modal',
|
||||||
|
templateUrl: './admin-config-wizard-modal.component.html',
|
||||||
|
styleUrls: [ './admin-config-wizard-modal.component.scss' ],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
CdkStepperModule,
|
||||||
|
AdminConfigWizardStepperComponent,
|
||||||
|
AdminConfigWizardWelcomeComponent,
|
||||||
|
AdminConfigWizardEditInfoComponent,
|
||||||
|
AdminConfigWizardFormComponent,
|
||||||
|
AdminConfigWizardPreviewComponent,
|
||||||
|
AdminConfigWizardDocumentationComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminConfigWizardModalComponent implements OnInit {
|
||||||
|
private modalService = inject(NgbModal)
|
||||||
|
private route = inject(ActivatedRoute)
|
||||||
|
|
||||||
|
readonly modal = viewChild<ElementRef>('modal')
|
||||||
|
readonly stepper = viewChild<AdminConfigWizardStepperComponent>('stepper')
|
||||||
|
|
||||||
|
readonly created = output()
|
||||||
|
|
||||||
|
usageType: UsageType
|
||||||
|
showWelcome: boolean
|
||||||
|
instanceInfo: FormInfo
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.created.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldAutoOpen (user: User) {
|
||||||
|
if (this.modalService.hasOpenModals()) return false
|
||||||
|
if (this.route.snapshot.fragment === 'admin-welcome-wizard') return true
|
||||||
|
if (user.noWelcomeModal === true) return false
|
||||||
|
if (peertubeLocalStorage.getItem(getNoWelcomeModalLocalStorageKey()) === 'true') return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
show ({ showWelcome }: { showWelcome: boolean }) {
|
||||||
|
this.showWelcome = showWelcome
|
||||||
|
|
||||||
|
this.modalService.open(this.modal(), {
|
||||||
|
centered: true,
|
||||||
|
backdrop: 'static',
|
||||||
|
keyboard: false,
|
||||||
|
size: 'lg'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStep () {
|
||||||
|
if (!this.stepper()) return 0
|
||||||
|
|
||||||
|
const currentStep = this.stepper().selectedIndex
|
||||||
|
|
||||||
|
// The welcome step is not counted in the total steps
|
||||||
|
if (this.showWelcome) return currentStep
|
||||||
|
|
||||||
|
return currentStep + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSteps () {
|
||||||
|
if (!this.stepper()) return 0
|
||||||
|
|
||||||
|
const totalSteps = this.stepper().steps.length
|
||||||
|
|
||||||
|
// The welcome step is not counted in the total steps
|
||||||
|
if (this.showWelcome) return totalSteps - 1
|
||||||
|
|
||||||
|
return totalSteps
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div>
|
||||||
|
<div [ngTemplateOutlet]="selected ? selected.content : null"></div>
|
||||||
|
</div>
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { CdkStepper } from '@angular/cdk/stepper'
|
||||||
|
import { CommonModule, NgTemplateOutlet } from '@angular/common'
|
||||||
|
import { Component } from '@angular/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-admin-config-wizard-stepper',
|
||||||
|
templateUrl: './admin-config-wizard-stepper.component.html',
|
||||||
|
providers: [ { provide: CdkStepper, useExisting: AdminConfigWizardStepperComponent } ],
|
||||||
|
imports: [ CommonModule, NgTemplateOutlet ]
|
||||||
|
})
|
||||||
|
export class AdminConfigWizardStepperComponent extends CdkStepper {
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
@use "_variables" as *;
|
||||||
|
@use "_mixins" as *;
|
||||||
|
|
||||||
|
.steps {
|
||||||
|
color: pvar(--fg-200);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: $font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: pvar(--fg-350);
|
||||||
|
font-weight: $font-bold;
|
||||||
|
|
||||||
|
@include font-size(38px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-title {
|
||||||
|
color: pvar(--fg-300);
|
||||||
|
font-weight: normal;
|
||||||
|
|
||||||
|
@include font-size(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-content {
|
||||||
|
color: pvar(--fg-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
@include rfs(2.5rem, margin-top);
|
||||||
|
@include rfs(1.5rem, gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
max-width: 512px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-columns {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
&.width-50 {
|
||||||
|
> div {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.one-column {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.mascot-container {
|
||||||
|
position: relative;
|
||||||
|
height: 110px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mascot-container,
|
||||||
|
.mascot {
|
||||||
|
width: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mascot {
|
||||||
|
position: absolute;
|
||||||
|
top: -100px;
|
||||||
|
left: 0;
|
||||||
|
height: 190px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: $font-bold;
|
||||||
|
color: var(--fg-300);
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function getNoWelcomeModalLocalStorageKey () {
|
||||||
|
return 'no_welcome_modal'
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
<div class="root">
|
||||||
|
<div class="one-column">
|
||||||
|
<div class="mascot-container">
|
||||||
|
<img class="mascot" src="/client/assets/images/mascot/happy.svg" alt="mascot" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>
|
||||||
|
<div i18n class="title">Congratulations</div>
|
||||||
|
|
||||||
|
<div i18n class="sub-title">Your platform has been configured!</div>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<span class="text-content" i18n>
|
||||||
|
It's time to add information about your platform!
|
||||||
|
<strong>Setting up a description</strong>, specifying <strong>who you are</strong>, why <strong>you created your platform</strong> and
|
||||||
|
<strong>how long</strong> you plan to <strong>maintain it</strong>
|
||||||
|
is very important for visitors to understand on what type of website they are.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<h5 i18n>Useful links</h5>
|
||||||
|
|
||||||
|
<ul class="text-content">
|
||||||
|
<li>
|
||||||
|
<a class="link-primary me-1" href="https://joinpeertube.org" target="_blank" i18n>Official PeerTube website</a>
|
||||||
|
|
||||||
|
<span i18n>Blog post, get help or discover PeerTube</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a class="link-primary me-1" href="https://instances.joinpeertube.org/instances" target="_blank" i18n>Public PeerTube index</a>
|
||||||
|
|
||||||
|
<span i18n>Put your platform on the official PeerTube public index</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<h5 i18n>Documentation</h5>
|
||||||
|
|
||||||
|
<ul class="text-content">
|
||||||
|
<li>
|
||||||
|
<a class="link-primary me-1" href="https://docs.joinpeertube.org/admin/following-instances" target="_blank" i18n>Admin</a>
|
||||||
|
|
||||||
|
<span i18n>Managing users, following other platforms, dealing with spammers, configure object storage or remote transcoding...</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a class="link-primary me-1" href="https://docs.joinpeertube.org/use/setup-account" target="_blank">User</a>
|
||||||
|
|
||||||
|
<span i18n>Setup your account, managing video playlists, discover third-party applications...</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a class="link-primary me-1" href="https://docs.joinpeertube.org/maintain/tools" target="_blank" i18n>CLI</a>
|
||||||
|
|
||||||
|
<span i18n>Upload or import videos, parse logs, prune storage directories, reset user password...</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<my-button i18n (click)="hide.emit()" theme="secondary">Close</my-button>
|
||||||
|
|
||||||
|
<my-button i18n theme="primary" ptRouterLink="/admin/settings/config" (click)="hide.emit()">Fill platform information</my-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, output } from '@angular/core'
|
||||||
|
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-admin-config-wizard-documentation',
|
||||||
|
templateUrl: './admin-config-wizard-documentation.component.html',
|
||||||
|
styleUrls: [ '../shared/admin-config-wizard-modal-common.scss' ],
|
||||||
|
imports: [ CommonModule, ButtonComponent ]
|
||||||
|
})
|
||||||
|
export class AdminConfigWizardDocumentationComponent {
|
||||||
|
readonly hide = output()
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
<div class="root">
|
||||||
|
<div class="two-columns">
|
||||||
|
<div>
|
||||||
|
<img class="mascot" src="/client/assets/images/mascot/pointing.svg" alt="mascot">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="steps">
|
||||||
|
<div class="header-steps" i18n>STEP {{ currentStep() }}/{{ totalSteps() }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 i18n class="title">General information</h4>
|
||||||
|
|
||||||
|
<div class="text-content">You can edit this information later</div>
|
||||||
|
|
||||||
|
<form [formGroup]="form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="platformName">Platform name</label>
|
||||||
|
|
||||||
|
<input type="text" id="platformName" class="form-control" formControlName="platformName" [ngClass]="{ 'input-error': formErrors.platformName }">
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.platformName" class="form-error" role="alert">{{ formErrors.platformName }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="shortDescription">Short description</label>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
id="shortDescription" formControlName="shortDescription" class="form-control small"
|
||||||
|
[ngClass]="{ 'input-error': formErrors.shortDescription }"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.shortDescription" class="form-error" role="alert">{{ formErrors.shortDescription }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="primaryColor">Primary color</label>
|
||||||
|
|
||||||
|
<p-colorpicker class="d-block" inputId="primaryColor" formControlName="primaryColor" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
@if (showBack()) {
|
||||||
|
<my-button i18n icon="arrow-left" (click)="back.emit()" theme="secondary">Back</my-button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<my-button i18n theme="primary" (click)="next.emit(form.value)" [disabled]="!form.valid">Next step</my-button>
|
||||||
|
<button i18n (click)="hide.emit()" class="button-as-link">Ignore for now</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,26 @@
|
||||||
|
@use "_variables" as *;
|
||||||
|
@use "_mixins" as *;
|
||||||
|
|
||||||
|
form {
|
||||||
|
@include rfs(2rem, margin-top);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mascot {
|
||||||
|
margin-top: -90px;
|
||||||
|
width: 170px;
|
||||||
|
height: 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $small-view) {
|
||||||
|
.mascot {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-columns {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { CdkStepperModule } from '@angular/cdk/stepper'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { booleanAttribute, Component, inject, input, numberAttribute, OnInit, output } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { ServerService, ThemeService } from '@app/core'
|
||||||
|
import { 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 { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
|
import { ColorPickerModule } from 'primeng/colorpicker'
|
||||||
|
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
|
||||||
|
|
||||||
|
type Form = {
|
||||||
|
platformName: FormControl<string>
|
||||||
|
shortDescription: FormControl<string>
|
||||||
|
primaryColor: FormControl<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormInfo = FormDefaultTyped<Form>
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-admin-config-wizard-edit-info',
|
||||||
|
templateUrl: './admin-config-wizard-edit-info.component.html',
|
||||||
|
styleUrls: [ './admin-config-wizard-edit-info.component.scss', '../shared/admin-config-wizard-modal-common.scss' ],
|
||||||
|
imports: [ CommonModule, FormsModule, ReactiveFormsModule, ColorPickerModule, CdkStepperModule, ButtonComponent ]
|
||||||
|
})
|
||||||
|
export class AdminConfigWizardEditInfoComponent implements OnInit {
|
||||||
|
private server = inject(ServerService)
|
||||||
|
private formReactiveService = inject(FormReactiveService)
|
||||||
|
private themeService = inject(ThemeService)
|
||||||
|
|
||||||
|
readonly currentStep = input.required({ transform: numberAttribute })
|
||||||
|
readonly totalSteps = input.required({ transform: numberAttribute })
|
||||||
|
readonly showBack = input.required({ transform: booleanAttribute })
|
||||||
|
|
||||||
|
readonly back = output()
|
||||||
|
readonly next = output<FormInfo>()
|
||||||
|
readonly hide = output()
|
||||||
|
|
||||||
|
form: FormGroup<Form>
|
||||||
|
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||||
|
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.buildForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildForm () {
|
||||||
|
const obj: BuildFormArgumentTyped<Form> = {
|
||||||
|
platformName: INSTANCE_NAME_VALIDATOR,
|
||||||
|
shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
|
||||||
|
primaryColor: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
formErrors,
|
||||||
|
validationMessages
|
||||||
|
} = this.formReactiveService.buildForm<Form>(obj, this.getDefaultValues())
|
||||||
|
|
||||||
|
this.form = form
|
||||||
|
this.formErrors = formErrors
|
||||||
|
this.validationMessages = validationMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDefaultValues (): FormDefaultTyped<Form> {
|
||||||
|
const config = this.server.getHTMLConfig()
|
||||||
|
const primaryColorConfig = config.theme.customization.primaryColor
|
||||||
|
|
||||||
|
const primaryColor = primaryColorConfig
|
||||||
|
? this.themeService.formatColorForForm(primaryColorConfig)
|
||||||
|
: this.themeService.formatColorForForm(this.themeService.getCSSConfigValue('primaryColor'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
platformName: config.instance.name,
|
||||||
|
shortDescription: config.instance.shortDescription,
|
||||||
|
primaryColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
<div class="root">
|
||||||
|
<div class="two-columns mb-4">
|
||||||
|
<div>
|
||||||
|
<img class="mascot" src="/client/assets/images/mascot/default.svg" alt="mascot">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="steps">
|
||||||
|
<div class="header-steps" i18n>STEP {{ currentStep() }}/{{ totalSteps() }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 i18n class="title">Usage type</h4>
|
||||||
|
|
||||||
|
<div class="text-content">You can also edit your platform configuration at a later time</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="label" i18n>My platform is more like...</div>
|
||||||
|
|
||||||
|
<div class="two-columns width-50">
|
||||||
|
<div class="platform-types" [ngClass]="{ 'platform-type-selected': platformType }">
|
||||||
|
<ul class="ul-unstyle">
|
||||||
|
<li class="platform-type">
|
||||||
|
<input type="radio" name="platformType" id="platformTypeCommunity" value="community" [(ngModel)]="platformType">
|
||||||
|
|
||||||
|
<label for="platformTypeCommunity">
|
||||||
|
<div>
|
||||||
|
<my-global-icon iconName="users"></my-global-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="type-label" i18n>Community-based</div>
|
||||||
|
|
||||||
|
<div class="type-description" i18n>Enable a community to publish content and interact together.</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="platform-type">
|
||||||
|
<input type="radio" name="platformType" id="platformTypeInstitution" value="institution" [(ngModel)]="platformType">
|
||||||
|
|
||||||
|
<label for="platformTypeInstitution">
|
||||||
|
<div>
|
||||||
|
<my-custom-icon [html]="iconInstitution"></my-custom-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="type-label" i18n>Institutional</div>
|
||||||
|
|
||||||
|
<div class="type-description" i18n>To broadcast your videos. Recommended for public institutions, association and companies.</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="platform-type">
|
||||||
|
<input type="radio" name="platformType" id="platformTypePrivate" value="private" [(ngModel)]="platformType">
|
||||||
|
|
||||||
|
<label for="platformTypePrivate">
|
||||||
|
<div>
|
||||||
|
<my-custom-icon [html]="iconKey"></my-custom-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="type-label" i18n>Private</div>
|
||||||
|
|
||||||
|
<div class="type-description" i18n>Only certain people can access content. Recommended for families and closed communities.</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="platform-config">
|
||||||
|
@if (platformType === 'community') {
|
||||||
|
<my-community-based-config [usageType]="usageType[platformType]"></my-community-based-config>
|
||||||
|
} @else if (platformType === 'institution') {
|
||||||
|
<my-institutional-config [usageType]="usageType[platformType]"></my-institutional-config>
|
||||||
|
} @else if (platformType === 'private') {
|
||||||
|
<my-private-instance-config [usageType]="usageType[platformType]"></my-private-instance-config>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<my-button i18n icon="arrow-left" (click)="back.emit()" theme="secondary">Back</my-button>
|
||||||
|
<my-button i18n theme="primary" (click)="next.emit(usageType[platformType])" [disabled]="!platformType">Preview configuration</my-button>
|
||||||
|
<button i18n (click)="hide.emit()" class="button-as-link">Ignore for now</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,116 @@
|
||||||
|
@use "_variables" as *;
|
||||||
|
@use "_mixins" as *;
|
||||||
|
|
||||||
|
.mascot {
|
||||||
|
margin-top: -90px;
|
||||||
|
width: 170px;
|
||||||
|
height: 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-columns.width-50 {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-types {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
@media screen and (min-width: $small-view) {
|
||||||
|
&.platform-type-selected + div {
|
||||||
|
border-inline-start: 1px solid pvar(--bg-secondary-450);
|
||||||
|
}
|
||||||
|
|
||||||
|
+ div {
|
||||||
|
@include padding-left(2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include margin-right(2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $small-view) {
|
||||||
|
&.platform-type-selected + div {
|
||||||
|
border-block-start: 1px solid pvar(--bg-secondary-450);
|
||||||
|
}
|
||||||
|
|
||||||
|
+ div {
|
||||||
|
@include padding-top(2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include rfs(2rem, margin-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-type {
|
||||||
|
label {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
gap: 1rem;
|
||||||
|
color: pvar(--fg-300);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid pvar(--bg-secondary-450);
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
my-global-icon,
|
||||||
|
my-custom-icon {
|
||||||
|
color: pvar(--secondary-icon-color);
|
||||||
|
|
||||||
|
@include global-icon-size(34px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: pvar(--bg-secondary-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-label {
|
||||||
|
color: pvar(--fg-400);
|
||||||
|
font-weight: $font-bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-description {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:checked + label {
|
||||||
|
border: 2px solid pvar(--border-primary);
|
||||||
|
margin: -1px;
|
||||||
|
background-color: pvar(--bg);
|
||||||
|
|
||||||
|
my-global-icon,
|
||||||
|
my-custom-icon {
|
||||||
|
color: pvar(--border-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $small-view) {
|
||||||
|
.mascot {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-columns {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-columns.width-50 {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
import { CdkStepperModule } from '@angular/cdk/stepper'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, inject, input, numberAttribute, OnInit, output } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { ServerService, ThemeService } from '@app/core'
|
||||||
|
import { INSTANCE_NAME_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 { ColorPickerModule } from 'primeng/colorpicker'
|
||||||
|
import { debounceTime } from 'rxjs'
|
||||||
|
import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component'
|
||||||
|
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
|
||||||
|
import { CommunityBasedConfigComponent } from './usage-type/community-based-config.component'
|
||||||
|
import { InstitutionalConfigComponent } from './usage-type/institutional-config.component'
|
||||||
|
import { PrivateInstanceConfigComponent } from './usage-type/private-instance-config.component'
|
||||||
|
import { UsageType } from './usage-type/usage-type.model'
|
||||||
|
import { CustomIconComponent } from '../../../shared/shared-icons/custom-icon.component'
|
||||||
|
|
||||||
|
type Form = {
|
||||||
|
platformName: FormControl<string>
|
||||||
|
primaryColor: FormControl<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlatformType = 'community' | 'institution' | 'private'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-admin-config-wizard-form',
|
||||||
|
templateUrl: './admin-config-wizard-form.component.html',
|
||||||
|
styleUrls: [ './admin-config-wizard-form.component.scss', '../shared/admin-config-wizard-modal-common.scss' ],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
ColorPickerModule,
|
||||||
|
CdkStepperModule,
|
||||||
|
ButtonComponent,
|
||||||
|
GlobalIconComponent,
|
||||||
|
CommunityBasedConfigComponent,
|
||||||
|
PrivateInstanceConfigComponent,
|
||||||
|
InstitutionalConfigComponent,
|
||||||
|
CustomIconComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AdminConfigWizardFormComponent implements OnInit {
|
||||||
|
private server = inject(ServerService)
|
||||||
|
private formReactiveService = inject(FormReactiveService)
|
||||||
|
private themeService = inject(ThemeService)
|
||||||
|
|
||||||
|
readonly currentStep = input.required({ transform: numberAttribute })
|
||||||
|
readonly totalSteps = input.required({ transform: numberAttribute })
|
||||||
|
|
||||||
|
readonly back = output()
|
||||||
|
readonly next = output<UsageType>()
|
||||||
|
readonly hide = output()
|
||||||
|
|
||||||
|
iconKey = require('../../../../assets/images/feather/key.svg')
|
||||||
|
iconInstitution = require('../../../../assets/images/feather/institution.svg')
|
||||||
|
|
||||||
|
form: FormGroup<Form>
|
||||||
|
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||||
|
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||||
|
|
||||||
|
platformType: PlatformType
|
||||||
|
usageType: { [id in PlatformType]: UsageType } = {
|
||||||
|
community: UsageType.initForCommunity(),
|
||||||
|
institution: UsageType.initForInstitution(),
|
||||||
|
private: UsageType.initForPrivateInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.buildForm()
|
||||||
|
|
||||||
|
this.subscribeToColorChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
private subscribeToColorChanges () {
|
||||||
|
let currentAnimationFrame: number
|
||||||
|
|
||||||
|
this.form.get('primaryColor').valueChanges.pipe(debounceTime(250)).subscribe(value => {
|
||||||
|
if (currentAnimationFrame) {
|
||||||
|
cancelAnimationFrame(currentAnimationFrame)
|
||||||
|
currentAnimationFrame = null
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAnimationFrame = requestAnimationFrame(() => {
|
||||||
|
const config = this.server.getHTMLConfig()
|
||||||
|
|
||||||
|
this.themeService.updateColorPalette({
|
||||||
|
...config.theme,
|
||||||
|
|
||||||
|
customization: {
|
||||||
|
...config.theme.customization,
|
||||||
|
|
||||||
|
primaryColor: this.themeService.formatColorForForm(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildForm () {
|
||||||
|
const obj: BuildFormArgumentTyped<Form> = {
|
||||||
|
platformName: INSTANCE_NAME_VALIDATOR,
|
||||||
|
primaryColor: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
formErrors,
|
||||||
|
validationMessages
|
||||||
|
} = this.formReactiveService.buildForm<Form>(obj, this.getDefaultValues())
|
||||||
|
|
||||||
|
this.form = form
|
||||||
|
this.formErrors = formErrors
|
||||||
|
this.validationMessages = validationMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDefaultValues (): FormDefaultTyped<Form> {
|
||||||
|
const config = this.server.getHTMLConfig()
|
||||||
|
const primaryColorConfig = config.theme.customization.primaryColor
|
||||||
|
|
||||||
|
const primaryColor = primaryColorConfig
|
||||||
|
? this.themeService.formatColorForForm(primaryColorConfig)
|
||||||
|
: this.themeService.formatColorForForm(this.themeService.getCSSConfigValue('primaryColor'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
platformName: config.instance.name,
|
||||||
|
primaryColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
<div class="root">
|
||||||
|
<div class="two-columns">
|
||||||
|
<div>
|
||||||
|
<img class="mascot" src="/client/assets/images/mascot/default.svg" alt="mascot">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="steps">
|
||||||
|
<div class="header-steps" i18n>STEP {{ currentStep() }}/{{ totalSteps() }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 i18n class="title">Configuration preview</h4>
|
||||||
|
|
||||||
|
<div class="text-content mt-3">
|
||||||
|
<p i18n>If you confirm, PeerTube will:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
@for (explanation of safeExplanations; track explanation) {
|
||||||
|
<li [innerHTML]="explanation"></li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p i18n>
|
||||||
|
If you want finer settings control, your platform configuration can be easily changed after the pre-configuration wizard!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<my-button i18n icon="arrow-left" (click)="back.emit()" theme="secondary">Back</my-button>
|
||||||
|
<my-button i18n theme="primary" [disabled]="updating" [loading]="updating" (click)="confirm()">Confirm this configuration</my-button>
|
||||||
|
<button i18n (click)="hide.emit()" class="button-as-link">Ignore for now</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { CdkStepperModule } from '@angular/cdk/stepper'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, inject, input, numberAttribute, OnChanges, output } from '@angular/core'
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { HtmlRendererService, Notifier, ServerService } from '@app/core'
|
||||||
|
import { AdminConfigService } from '@app/shared/shared-admin/admin-config.service'
|
||||||
|
import { PluginApiService } from '@app/shared/shared-admin/plugin-api.service'
|
||||||
|
import { CustomConfig } from '@peertube/peertube-models'
|
||||||
|
import merge from 'lodash-es/merge'
|
||||||
|
import { ColorPickerModule } from 'primeng/colorpicker'
|
||||||
|
import { concatMap, from, switchMap, toArray } from 'rxjs'
|
||||||
|
import { PartialDeep } from 'type-fest'
|
||||||
|
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
|
||||||
|
import { FormInfo } from './admin-config-wizard-edit-info.component'
|
||||||
|
import { UsageType } from './usage-type/usage-type.model'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-admin-config-wizard-preview',
|
||||||
|
templateUrl: './admin-config-wizard-preview.component.html',
|
||||||
|
styleUrls: [ '../shared/admin-config-wizard-modal-common.scss' ],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
ColorPickerModule,
|
||||||
|
CdkStepperModule,
|
||||||
|
ButtonComponent
|
||||||
|
],
|
||||||
|
providers: [ AdminConfigService, PluginApiService ]
|
||||||
|
})
|
||||||
|
export class AdminConfigWizardPreviewComponent implements OnChanges {
|
||||||
|
private adminConfig = inject(AdminConfigService)
|
||||||
|
private pluginAPI = inject(PluginApiService)
|
||||||
|
private notifier = inject(Notifier)
|
||||||
|
private html = inject(HtmlRendererService)
|
||||||
|
private server = inject(ServerService)
|
||||||
|
|
||||||
|
readonly currentStep = input.required({ transform: numberAttribute })
|
||||||
|
readonly totalSteps = input.required({ transform: numberAttribute })
|
||||||
|
readonly usageType = input.required<UsageType>()
|
||||||
|
readonly instanceInfo = input.required<FormInfo>()
|
||||||
|
|
||||||
|
readonly back = output()
|
||||||
|
readonly next = output()
|
||||||
|
readonly hide = output()
|
||||||
|
|
||||||
|
safeExplanations: string[] = []
|
||||||
|
plugins: string[] = []
|
||||||
|
config: PartialDeep<CustomConfig> = {}
|
||||||
|
|
||||||
|
updating = false
|
||||||
|
|
||||||
|
ngOnChanges () {
|
||||||
|
if (this.usageType()) {
|
||||||
|
this.safeExplanations = this.usageType()
|
||||||
|
.getUnsafeExplanations()
|
||||||
|
.map(e => this.html.toSimpleSafeHtml(e))
|
||||||
|
|
||||||
|
this.config = merge(
|
||||||
|
{
|
||||||
|
instance: {
|
||||||
|
name: this.instanceInfo().platformName,
|
||||||
|
shortDescription: this.instanceInfo().shortDescription
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
customization: {
|
||||||
|
primaryColor: this.instanceInfo().primaryColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} satisfies PartialDeep<CustomConfig>,
|
||||||
|
this.usageType().getConfig()
|
||||||
|
)
|
||||||
|
|
||||||
|
this.plugins = this.usageType().getPlugins()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm () {
|
||||||
|
this.updating = true
|
||||||
|
|
||||||
|
this.adminConfig.updateCustomConfig(this.config)
|
||||||
|
.pipe(
|
||||||
|
switchMap(() => this.server.resetConfig()),
|
||||||
|
switchMap(() => {
|
||||||
|
return from(this.plugins)
|
||||||
|
.pipe(
|
||||||
|
concatMap(plugin => this.pluginAPI.install(plugin)),
|
||||||
|
toArray()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.updating = false
|
||||||
|
|
||||||
|
this.next.emit()
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => {
|
||||||
|
this.notifier.error(err.message)
|
||||||
|
this.updating = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
<div class="root one-column">
|
||||||
|
<div class="mascot-container">
|
||||||
|
<img class="mascot" src="/client/assets/images/mascot/happy.svg" alt="mascot">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>
|
||||||
|
<div i18n class="title">Welcome to PeerTube</div>
|
||||||
|
|
||||||
|
<div i18n class="sub-title">dear administrator!</div>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div class="text-content mt-3" i18n>Sepia, the cuttlefish, has a few questions to help you <strong>quickly pre-configure your platform</strong>.</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<my-button i18n theme="primary" (click)="next.emit()">Let's go!</my-button>
|
||||||
|
<my-button i18n (click)="hide.emit()" theme="secondary">Remind me later</my-button>
|
||||||
|
<button i18n (click)="doNotOpenAgain(); hide.emit()" class="button-as-link">Do not display again</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { CdkStepperModule } from '@angular/cdk/stepper'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, inject, output } from '@angular/core'
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { Notifier, UserService } from '@app/core'
|
||||||
|
import { ButtonComponent } from '@app/shared/shared-main/buttons/button.component'
|
||||||
|
import { logger } from '@root-helpers/logger'
|
||||||
|
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
||||||
|
import { getNoWelcomeModalLocalStorageKey } from '../shared/admin-config-wizard-modal-utils'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-admin-config-wizard-welcome',
|
||||||
|
templateUrl: './admin-config-wizard-welcome.component.html',
|
||||||
|
styleUrls: [ '../shared/admin-config-wizard-modal-common.scss' ],
|
||||||
|
imports: [ CommonModule, FormsModule, ReactiveFormsModule, CdkStepperModule, ButtonComponent ]
|
||||||
|
})
|
||||||
|
export class AdminConfigWizardWelcomeComponent {
|
||||||
|
private userService = inject(UserService)
|
||||||
|
private notifier = inject(Notifier)
|
||||||
|
|
||||||
|
readonly back = output()
|
||||||
|
readonly next = output()
|
||||||
|
readonly hide = output()
|
||||||
|
|
||||||
|
doNotOpenAgain () {
|
||||||
|
peertubeLocalStorage.setItem(getNoWelcomeModalLocalStorageKey(), 'true')
|
||||||
|
|
||||||
|
this.userService.updateMyProfile({ noWelcomeModal: true })
|
||||||
|
.subscribe({
|
||||||
|
next: () => logger.info('We will not open the welcome modal again.'),
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
<form [formGroup]="form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="">Registration policy</label>
|
||||||
|
|
||||||
|
<my-select-options inputId="registration" [items]="registrationOptions" formControlName="registration"></my-select-options>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="">Video quota for new users</label>
|
||||||
|
|
||||||
|
<my-select-options inputId="videoQuota" [items]="videoQuotaOptions" formControlName="videoQuota"></my-select-options>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="remoteImport">Video import and synchronization</label>
|
||||||
|
|
||||||
|
<my-select-options inputId="remoteImport" [items]="importOptions" formControlName="remoteImport"></my-select-options>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="">My community can stream lives</label>
|
||||||
|
|
||||||
|
<my-select-options inputId="live" [items]="liveOptions" formControlName="live"></my-select-options>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="">Search</label>
|
||||||
|
|
||||||
|
<my-select-options inputId="globalSearch" [items]="globalSearchOptions" formControlName="globalSearch"></my-select-options>
|
||||||
|
</div>
|
||||||
|
</form>
|
|
@ -0,0 +1,135 @@
|
||||||
|
import { CdkStepperModule } from '@angular/cdk/stepper'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, inject, model, OnInit } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { getVideoQuotaOptions } from '@app/+admin/shared/user-quota-options'
|
||||||
|
import {
|
||||||
|
BuildFormArgumentTyped,
|
||||||
|
FormDefaultTyped,
|
||||||
|
FormReactiveErrorsTyped,
|
||||||
|
FormReactiveMessagesTyped
|
||||||
|
} from '@app/shared/form-validators/form-validator.model'
|
||||||
|
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
|
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
|
||||||
|
import { ColorPickerModule } from 'primeng/colorpicker'
|
||||||
|
import { SelectOptionsItem } from 'src/types'
|
||||||
|
import { EnabledDisabled, RegistrationType, UsageType } from './usage-type.model'
|
||||||
|
|
||||||
|
type Form = {
|
||||||
|
registration: FormControl<RegistrationType>
|
||||||
|
videoQuota: FormControl<number>
|
||||||
|
remoteImport: FormControl<EnabledDisabled>
|
||||||
|
live: FormControl<EnabledDisabled>
|
||||||
|
globalSearch: FormControl<EnabledDisabled>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-community-based-config',
|
||||||
|
templateUrl: './community-based-config.component.html',
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
ColorPickerModule,
|
||||||
|
CdkStepperModule,
|
||||||
|
SelectOptionsComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class CommunityBasedConfigComponent implements OnInit {
|
||||||
|
private formReactiveService = inject(FormReactiveService)
|
||||||
|
|
||||||
|
usageType = model.required<UsageType>()
|
||||||
|
|
||||||
|
form: FormGroup<Form>
|
||||||
|
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||||
|
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||||
|
|
||||||
|
registrationOptions: SelectOptionsItem<RegistrationType>[] = [
|
||||||
|
{
|
||||||
|
id: 'open',
|
||||||
|
label: 'Open',
|
||||||
|
description: 'Anyone can register and use the platform'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'approval',
|
||||||
|
label: 'Requires approval',
|
||||||
|
description: 'Anyone can register, but a moderator must approve their account before they can use the platform'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'closed',
|
||||||
|
label: 'Closed',
|
||||||
|
description: 'Only an administrator can create users on the platform'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
importOptions: SelectOptionsItem<EnabledDisabled>[] = [
|
||||||
|
{
|
||||||
|
id: 'enabled',
|
||||||
|
label: 'Enabled',
|
||||||
|
description:
|
||||||
|
'Your community can import videos from remote platforms (YouTube, Vimeo...) and automatically synchronize remote channels'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'disabled',
|
||||||
|
label: 'Disabled',
|
||||||
|
description: 'Your community cannot import or synchronize content from remote platforms'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
liveOptions: SelectOptionsItem<EnabledDisabled>[] = [
|
||||||
|
{
|
||||||
|
id: 'enabled',
|
||||||
|
label: 'Yes',
|
||||||
|
description: 'Your community can live stream on the platform (this requires extra moderation work)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'disabled',
|
||||||
|
label: 'No',
|
||||||
|
description: 'Your community is not permitted to run live streams on the platform'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
globalSearchOptions: SelectOptionsItem<string>[] = [
|
||||||
|
{
|
||||||
|
id: 'enabled',
|
||||||
|
label: 'Enable global search',
|
||||||
|
description: 'Use https://sepiasearch.org as default search engine to search for content across all known peertube platforms'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'disabled',
|
||||||
|
label: 'Disable global search',
|
||||||
|
description: 'Use your platform search engine which only displays local content'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
videoQuotaOptions: SelectOptionsItem<number>[] = getVideoQuotaOptions()
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.buildForm()
|
||||||
|
|
||||||
|
this.form.valueChanges.subscribe(value => {
|
||||||
|
this.usageType().patch(value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildForm () {
|
||||||
|
const obj: BuildFormArgumentTyped<Form> = {
|
||||||
|
registration: null,
|
||||||
|
remoteImport: null,
|
||||||
|
videoQuota: null,
|
||||||
|
live: null,
|
||||||
|
globalSearch: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
formErrors,
|
||||||
|
validationMessages
|
||||||
|
} = this.formReactiveService.buildForm<Form>(obj, this.usageType() as FormDefaultTyped<Form>)
|
||||||
|
|
||||||
|
this.form = form
|
||||||
|
this.formErrors = formErrors
|
||||||
|
this.validationMessages = validationMessages
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
<form [formGroup]="form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="keepOriginalVideo">Save a copy of the uploaded video</label>
|
||||||
|
|
||||||
|
<my-select-options inputId="keepOriginalVideo" [items]="keepOriginalVideoOptions" formControlName="keepOriginalVideo"></my-select-options>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="p2p">P2P</label>
|
||||||
|
|
||||||
|
<my-select-options inputId="p2p" [items]="p2pOptions" formControlName="p2p"></my-select-options>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="transcription">Video transcription</label>
|
||||||
|
|
||||||
|
<my-select-options inputId="transcription" [items]="transcriptionOptions" formControlName="transcription"></my-select-options>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="authType">Remote authentication</label>
|
||||||
|
|
||||||
|
<my-select-options inputId="authType" [items]="authenticationOptions" formControlName="authType"></my-select-options>
|
||||||
|
</div>
|
||||||
|
</form>
|
|
@ -0,0 +1,133 @@
|
||||||
|
import { CdkStepperModule } from '@angular/cdk/stepper'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, inject, model, OnInit } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import {
|
||||||
|
BuildFormArgumentTyped,
|
||||||
|
FormDefaultTyped,
|
||||||
|
FormReactiveErrorsTyped,
|
||||||
|
FormReactiveMessagesTyped
|
||||||
|
} from '@app/shared/form-validators/form-validator.model'
|
||||||
|
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
|
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
|
||||||
|
import { ColorPickerModule } from 'primeng/colorpicker'
|
||||||
|
import { SelectOptionsItem } from 'src/types'
|
||||||
|
import { AuthType, EnabledDisabled, UsageType } from './usage-type.model'
|
||||||
|
|
||||||
|
type Form = {
|
||||||
|
keepOriginalVideo: FormControl<EnabledDisabled>
|
||||||
|
p2p: FormControl<EnabledDisabled>
|
||||||
|
transcription: FormControl<EnabledDisabled>
|
||||||
|
authType: FormControl<AuthType>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-institutional-config',
|
||||||
|
templateUrl: './institutional-config.component.html',
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
ColorPickerModule,
|
||||||
|
CdkStepperModule,
|
||||||
|
SelectOptionsComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class InstitutionalConfigComponent implements OnInit {
|
||||||
|
private formReactiveService = inject(FormReactiveService)
|
||||||
|
|
||||||
|
usageType = model.required<UsageType>()
|
||||||
|
|
||||||
|
form: FormGroup<Form>
|
||||||
|
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||||
|
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||||
|
|
||||||
|
p2pOptions: SelectOptionsItem<EnabledDisabled>[] = [
|
||||||
|
{
|
||||||
|
id: 'enabled',
|
||||||
|
label: 'Enabled',
|
||||||
|
description: 'Enable P2P streaming by default for anonymous and new users'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'disabled',
|
||||||
|
label: 'Disabled',
|
||||||
|
description: 'Disable P2P streaming'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
transcriptionOptions: SelectOptionsItem<EnabledDisabled>[] = [
|
||||||
|
{
|
||||||
|
id: 'enabled',
|
||||||
|
label: 'Enabled',
|
||||||
|
description: 'Enable automatic transcription of videos to automatically generate subtitles'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'disabled',
|
||||||
|
label: 'Disabled',
|
||||||
|
description: 'Disable automatic transcription of videos'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
keepOriginalVideoOptions: SelectOptionsItem<EnabledDisabled>[] = [
|
||||||
|
{
|
||||||
|
id: 'enabled',
|
||||||
|
label: 'Yes',
|
||||||
|
description: 'Keep the original video file on the server'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'disabled',
|
||||||
|
label: 'No',
|
||||||
|
description: 'Delete the original video file after processing'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
authenticationOptions: SelectOptionsItem<AuthType>[] = [
|
||||||
|
{
|
||||||
|
id: 'local',
|
||||||
|
label: 'Disabled',
|
||||||
|
description: 'Your platform will manage user registration and login internally'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ldap',
|
||||||
|
label: 'LDAP',
|
||||||
|
description: 'Use LDAP for user authentication'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'oidc',
|
||||||
|
label: 'OIDC',
|
||||||
|
description: 'Use OpenID Connect for user authentication'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'saml',
|
||||||
|
label: 'SAML',
|
||||||
|
description: 'Use SAML 2.0 for user authentication'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.buildForm()
|
||||||
|
|
||||||
|
this.form.valueChanges.subscribe(value => {
|
||||||
|
this.usageType().patch(value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildForm () {
|
||||||
|
const obj: BuildFormArgumentTyped<Form> = {
|
||||||
|
keepOriginalVideo: null,
|
||||||
|
p2p: null,
|
||||||
|
transcription: null,
|
||||||
|
authType: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
formErrors,
|
||||||
|
validationMessages
|
||||||
|
} = this.formReactiveService.buildForm<Form>(obj, this.usageType() as FormDefaultTyped<Form>)
|
||||||
|
|
||||||
|
this.form = form
|
||||||
|
this.formErrors = formErrors
|
||||||
|
this.validationMessages = validationMessages
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
<form [formGroup]="form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="remoteImport">Video import and synchronization</label>
|
||||||
|
|
||||||
|
<my-select-options inputId="remoteImport" [items]="importOptions" formControlName="remoteImport"></my-select-options>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="live">Users can stream lives</label>
|
||||||
|
|
||||||
|
<my-select-options inputId="live" [items]="liveOptions" formControlName="live"></my-select-options>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="keepOriginalVideo">Save a copy of the uploaded video</label>
|
||||||
|
|
||||||
|
<my-select-options inputId="keepOriginalVideo" [items]="keepOriginalVideoOptions" formControlName="keepOriginalVideo"></my-select-options>
|
||||||
|
</div>
|
||||||
|
</form>
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { CdkStepperModule } from '@angular/cdk/stepper'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, inject, model, OnInit } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import {
|
||||||
|
BuildFormArgumentTyped,
|
||||||
|
FormDefaultTyped,
|
||||||
|
FormReactiveErrorsTyped,
|
||||||
|
FormReactiveMessagesTyped
|
||||||
|
} from '@app/shared/form-validators/form-validator.model'
|
||||||
|
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
|
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
|
||||||
|
import { ColorPickerModule } from 'primeng/colorpicker'
|
||||||
|
import { SelectOptionsItem } from 'src/types'
|
||||||
|
import { EnabledDisabled, UsageType } from './usage-type.model'
|
||||||
|
|
||||||
|
type Form = {
|
||||||
|
remoteImport: FormControl<EnabledDisabled>
|
||||||
|
live: FormControl<EnabledDisabled>
|
||||||
|
keepOriginalVideo: FormControl<EnabledDisabled>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-private-instance-config',
|
||||||
|
templateUrl: './private-instance-config.component.html',
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
ColorPickerModule,
|
||||||
|
CdkStepperModule,
|
||||||
|
SelectOptionsComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class PrivateInstanceConfigComponent implements OnInit {
|
||||||
|
private formReactiveService = inject(FormReactiveService)
|
||||||
|
|
||||||
|
usageType = model.required<UsageType>()
|
||||||
|
|
||||||
|
form: FormGroup<Form>
|
||||||
|
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||||
|
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||||
|
|
||||||
|
importOptions: SelectOptionsItem<EnabledDisabled>[] = [
|
||||||
|
{
|
||||||
|
id: 'enabled',
|
||||||
|
label: 'Enabled',
|
||||||
|
description: 'Users can import videos from remote platforms (YouTube, Vimeo...) and automatically synchronize remote channels'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'disabled',
|
||||||
|
label: 'Disabled',
|
||||||
|
description: 'Disable video import and channel synchronization'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
liveOptions: SelectOptionsItem<EnabledDisabled>[] = [
|
||||||
|
{
|
||||||
|
id: 'enabled',
|
||||||
|
label: 'Yes'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'disabled',
|
||||||
|
label: 'No'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
keepOriginalVideoOptions: SelectOptionsItem<EnabledDisabled>[] = [
|
||||||
|
{
|
||||||
|
id: 'enabled',
|
||||||
|
label: 'Yes',
|
||||||
|
description: 'Keep the original video file on the server'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'disabled',
|
||||||
|
label: 'No',
|
||||||
|
description: 'Delete the original video file after processing'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.buildForm()
|
||||||
|
|
||||||
|
this.form.valueChanges.subscribe(value => {
|
||||||
|
this.usageType().patch(value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildForm () {
|
||||||
|
const obj: BuildFormArgumentTyped<Form> = {
|
||||||
|
remoteImport: null,
|
||||||
|
live: null,
|
||||||
|
keepOriginalVideo: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
formErrors,
|
||||||
|
validationMessages
|
||||||
|
} = this.formReactiveService.buildForm<Form>(obj, this.usageType() as FormDefaultTyped<Form>)
|
||||||
|
|
||||||
|
this.form = form
|
||||||
|
this.formErrors = formErrors
|
||||||
|
this.validationMessages = validationMessages
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,429 @@
|
||||||
|
import { exists } from '@peertube/peertube-core-utils'
|
||||||
|
import { CustomConfig, VideoPrivacy } from '@peertube/peertube-models'
|
||||||
|
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
|
||||||
|
import { getBytes } from '@root-helpers/bytes'
|
||||||
|
import merge from 'lodash-es/merge'
|
||||||
|
import { PartialDeep } from 'type-fest'
|
||||||
|
|
||||||
|
export type RegistrationType = 'open' | 'closed' | 'approval'
|
||||||
|
export type EnabledDisabled = 'disabled' | 'enabled'
|
||||||
|
export type AuthType = 'local' | 'ldap' | 'saml' | 'oidc'
|
||||||
|
|
||||||
|
export class UsageType {
|
||||||
|
registration: RegistrationType
|
||||||
|
videoQuota: number
|
||||||
|
remoteImport: EnabledDisabled
|
||||||
|
live: EnabledDisabled
|
||||||
|
globalSearch: EnabledDisabled
|
||||||
|
defaultPrivacy: typeof VideoPrivacy.INTERNAL | typeof VideoPrivacy.PUBLIC
|
||||||
|
p2p: EnabledDisabled
|
||||||
|
federation: EnabledDisabled
|
||||||
|
keepOriginalVideo: EnabledDisabled
|
||||||
|
allowReplaceFile: EnabledDisabled
|
||||||
|
preferDisplayName: EnabledDisabled
|
||||||
|
transcription: EnabledDisabled
|
||||||
|
authType: AuthType
|
||||||
|
|
||||||
|
private unsafeExplanations: string[] = []
|
||||||
|
private config: PartialDeep<CustomConfig> = {}
|
||||||
|
private plugins: string[] = []
|
||||||
|
|
||||||
|
private constructor () {
|
||||||
|
}
|
||||||
|
|
||||||
|
static initForCommunity () {
|
||||||
|
const usageType = new UsageType()
|
||||||
|
|
||||||
|
usageType.registration = 'approval'
|
||||||
|
usageType.remoteImport = 'disabled'
|
||||||
|
usageType.live = 'enabled'
|
||||||
|
usageType.videoQuota = 5 * 1024 * 1024 * 1024 // Default to 5GB
|
||||||
|
usageType.globalSearch = 'enabled'
|
||||||
|
|
||||||
|
usageType.defaultPrivacy = VideoPrivacy.PUBLIC
|
||||||
|
usageType.p2p = 'enabled'
|
||||||
|
usageType.federation = 'enabled'
|
||||||
|
usageType.keepOriginalVideo = 'disabled'
|
||||||
|
usageType.allowReplaceFile = 'disabled'
|
||||||
|
|
||||||
|
// Use current config for: authType, preferDisplayName and transcription
|
||||||
|
|
||||||
|
usageType.compute()
|
||||||
|
|
||||||
|
return usageType
|
||||||
|
}
|
||||||
|
|
||||||
|
static initForPrivateInstance () {
|
||||||
|
const usageType = new UsageType()
|
||||||
|
|
||||||
|
usageType.registration = 'closed'
|
||||||
|
usageType.remoteImport = 'enabled'
|
||||||
|
usageType.live = 'enabled'
|
||||||
|
usageType.videoQuota = -1
|
||||||
|
usageType.globalSearch = 'disabled'
|
||||||
|
|
||||||
|
usageType.defaultPrivacy = VideoPrivacy.INTERNAL
|
||||||
|
usageType.p2p = 'disabled'
|
||||||
|
usageType.federation = 'disabled'
|
||||||
|
usageType.keepOriginalVideo = 'enabled'
|
||||||
|
usageType.allowReplaceFile = 'enabled'
|
||||||
|
usageType.preferDisplayName = 'enabled'
|
||||||
|
|
||||||
|
// Use current config for: authType and transcription
|
||||||
|
|
||||||
|
usageType.compute()
|
||||||
|
|
||||||
|
return usageType
|
||||||
|
}
|
||||||
|
|
||||||
|
static initForInstitution () {
|
||||||
|
const usageType = new UsageType()
|
||||||
|
|
||||||
|
usageType.registration = 'closed'
|
||||||
|
usageType.remoteImport = 'enabled'
|
||||||
|
usageType.live = 'enabled'
|
||||||
|
usageType.videoQuota = -1
|
||||||
|
usageType.globalSearch = 'disabled'
|
||||||
|
|
||||||
|
usageType.defaultPrivacy = VideoPrivacy.PUBLIC
|
||||||
|
usageType.p2p = 'disabled'
|
||||||
|
usageType.keepOriginalVideo = 'enabled'
|
||||||
|
usageType.allowReplaceFile = 'enabled'
|
||||||
|
usageType.preferDisplayName = 'enabled'
|
||||||
|
|
||||||
|
usageType.authType = 'local'
|
||||||
|
usageType.transcription = 'enabled'
|
||||||
|
|
||||||
|
// Use current config for: federation
|
||||||
|
|
||||||
|
usageType.compute()
|
||||||
|
|
||||||
|
return usageType
|
||||||
|
}
|
||||||
|
|
||||||
|
private compute () {
|
||||||
|
this.unsafeExplanations = []
|
||||||
|
this.plugins = []
|
||||||
|
this.config = {}
|
||||||
|
|
||||||
|
this.computeRegistration()
|
||||||
|
this.computeVideoPrivacy()
|
||||||
|
this.computeVideoQuota()
|
||||||
|
this.computeKeepOriginalVideo()
|
||||||
|
this.computeReplaceVideoFile()
|
||||||
|
this.computeVideoImport()
|
||||||
|
this.computeStreamLives()
|
||||||
|
this.computeP2P()
|
||||||
|
this.computeGlobalSearch()
|
||||||
|
this.computeFederation()
|
||||||
|
this.computeMiniatureSettings()
|
||||||
|
this.computeTranscription()
|
||||||
|
this.computeAuth()
|
||||||
|
}
|
||||||
|
|
||||||
|
getUnsafeExplanations () {
|
||||||
|
return [ ...this.unsafeExplanations ]
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfig () {
|
||||||
|
return { ...this.config }
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlugins () {
|
||||||
|
return [ ...this.plugins ]
|
||||||
|
}
|
||||||
|
|
||||||
|
patch (obj: Partial<AttributesOnly<UsageType>>) {
|
||||||
|
for (const [ key, value ] of Object.entries(obj)) {
|
||||||
|
;(this as any)[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
this.compute()
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeRegistration () {
|
||||||
|
if (!exists(this.registration)) return
|
||||||
|
|
||||||
|
if (this.registration === 'open') {
|
||||||
|
this.addExplanation($localize`<strong>Allow</strong> any user <strong>to register</strong>`)
|
||||||
|
|
||||||
|
this.addConfig({
|
||||||
|
signup: {
|
||||||
|
enabled: true,
|
||||||
|
requiresApproval: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (this.registration === 'approval') {
|
||||||
|
this.addExplanation($localize`Allow users to <strong>apply for registration</strong> on your platform`)
|
||||||
|
|
||||||
|
this.addConfig({
|
||||||
|
signup: {
|
||||||
|
enabled: true,
|
||||||
|
requiresApproval: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (this.registration === 'closed') {
|
||||||
|
this.addExplanation($localize`<strong>Disable</strong> user <strong>registration</strong>`)
|
||||||
|
|
||||||
|
this.addConfig({
|
||||||
|
signup: {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.registration === 'approval' || this.registration === 'open') {
|
||||||
|
this.addExplanation($localize`Require <strong>moderator approval</strong> for videos published by your community`)
|
||||||
|
|
||||||
|
this.addConfig({
|
||||||
|
autoBlacklist: {
|
||||||
|
videos: {
|
||||||
|
ofUsers: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeVideoQuota () {
|
||||||
|
if (!exists(this.videoQuota)) return
|
||||||
|
|
||||||
|
this.addConfig({
|
||||||
|
user: {
|
||||||
|
videoQuota: this.videoQuota
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.videoQuota === 0) {
|
||||||
|
this.addExplanation(
|
||||||
|
$localize`<strong>Prevent</strong> new users <strong>from uploading videos</strong> (can be changed by moderators)`
|
||||||
|
)
|
||||||
|
} else if (this.videoQuota === -1) {
|
||||||
|
this.addExplanation($localize`Will <strong>not limit the amount of videos</strong> new users can upload`)
|
||||||
|
} else {
|
||||||
|
this.addExplanation(
|
||||||
|
$localize`Set <strong>video quota to ${getBytes(this.videoQuota, 0)}</strong> for new users (can be changed by moderators)`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeVideoImport () {
|
||||||
|
if (!exists(this.remoteImport)) return
|
||||||
|
|
||||||
|
this.addConfig({
|
||||||
|
import: {
|
||||||
|
videos: {
|
||||||
|
http: {
|
||||||
|
enabled: this.remoteImport === 'enabled'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
videoChannelSynchronization: {
|
||||||
|
enabled: this.remoteImport === 'enabled'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.remoteImport === 'enabled') {
|
||||||
|
this.addExplanation(
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
$localize`<strong>Allow</strong> your users <strong>to import and synchronize</strong> videos from remote platforms (YouTube, Vimeo...)`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.addExplanation($localize`<strong>Prevent</strong> your users <strong>from importing videos</strong> from remote platforms`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeStreamLives () {
|
||||||
|
if (!exists(this.live)) return
|
||||||
|
|
||||||
|
this.addConfig({
|
||||||
|
live: {
|
||||||
|
enabled: this.live === 'enabled'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.live === 'enabled') {
|
||||||
|
this.plugins.push('peertube-plugin-livechat')
|
||||||
|
|
||||||
|
this.addExplanation(
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
$localize`<strong>Allow</strong> your users <strong>to stream lives</strong> and chat with their viewers using the <strong>Livechat</strong> plugin`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.addExplanation($localize`<strong>Prevent</strong> your users from running <strong>live streams</strong>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeVideoPrivacy () {
|
||||||
|
if (!exists(this.defaultPrivacy)) return
|
||||||
|
|
||||||
|
this.addConfig({
|
||||||
|
defaults: {
|
||||||
|
publish: {
|
||||||
|
privacy: this.defaultPrivacy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.defaultPrivacy === VideoPrivacy.INTERNAL) {
|
||||||
|
this.addExplanation($localize`Set the <strong>default video privacy</strong> to <strong>Internal</strong>`)
|
||||||
|
} else if (this.defaultPrivacy === VideoPrivacy.PUBLIC) {
|
||||||
|
this.addExplanation($localize`Set the <strong>default video privacy</strong> to <strong>Public</strong>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeP2P () {
|
||||||
|
if (!exists(this.p2p)) return
|
||||||
|
|
||||||
|
this.addConfig({
|
||||||
|
defaults: {
|
||||||
|
p2p: {
|
||||||
|
embed: {
|
||||||
|
enabled: this.p2p === 'enabled'
|
||||||
|
},
|
||||||
|
webapp: {
|
||||||
|
enabled: this.p2p === 'enabled'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.p2p === 'enabled') {
|
||||||
|
this.addExplanation($localize`<strong>Enable P2P streaming</strong> by default for anonymous and new users`)
|
||||||
|
} else {
|
||||||
|
this.addExplanation($localize`<strong>Disable P2P streaming</strong> by default for anonymous and new users`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeFederation () {
|
||||||
|
if (!exists(this.federation)) return
|
||||||
|
|
||||||
|
this.addConfig({
|
||||||
|
followers: {
|
||||||
|
instance: {
|
||||||
|
enabled: this.federation === 'enabled'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.federation === 'enabled') {
|
||||||
|
this.addExplanation($localize`<strong>Allow</strong> external platforms/users to <strong>subscribe</strong> to your content`)
|
||||||
|
} else {
|
||||||
|
this.addExplanation($localize`<strong>Prevent</strong> external platforms/users to <strong>subscribe to your content</strong>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeKeepOriginalVideo () {
|
||||||
|
if (!exists(this.keepOriginalVideo)) return
|
||||||
|
|
||||||
|
this.addConfig({
|
||||||
|
transcoding: {
|
||||||
|
originalFile: {
|
||||||
|
keep: this.keepOriginalVideo === 'enabled'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.keepOriginalVideo === 'enabled') {
|
||||||
|
this.addExplanation($localize`Will <strong>save a copy</strong> of the uploaded video file`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeReplaceVideoFile () {
|
||||||
|
if (!exists(this.allowReplaceFile)) return
|
||||||
|
|
||||||
|
this.addConfig({
|
||||||
|
videoFile: {
|
||||||
|
update: {
|
||||||
|
enabled: this.allowReplaceFile === 'enabled'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.allowReplaceFile === 'enabled') {
|
||||||
|
this.addExplanation(
|
||||||
|
$localize`Will <strong>allow</strong> your users <strong>to replace a video</strong> that has already been published`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeMiniatureSettings () {
|
||||||
|
if (!exists(this.preferDisplayName)) return
|
||||||
|
|
||||||
|
this.addConfig({
|
||||||
|
client: {
|
||||||
|
videos: {
|
||||||
|
miniature: {
|
||||||
|
preferAuthorDisplayName: this.preferDisplayName === 'enabled'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeGlobalSearch () {
|
||||||
|
if (!exists(this.globalSearch)) return
|
||||||
|
|
||||||
|
this.addConfig({
|
||||||
|
search: {
|
||||||
|
searchIndex: {
|
||||||
|
enabled: this.globalSearch === 'enabled',
|
||||||
|
isDefaultSearch: this.globalSearch === 'enabled',
|
||||||
|
url: 'https://sepiasearch.org'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.globalSearch === 'enabled') {
|
||||||
|
this.addExplanation(
|
||||||
|
$localize`Set <a href="https://sepiasearch.org" target="_blank">SepiaSearch</a> as <strong>default search engine</strong>`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeTranscription () {
|
||||||
|
if (!exists(this.transcription)) return
|
||||||
|
|
||||||
|
this.addConfig({
|
||||||
|
videoTranscription: {
|
||||||
|
enabled: this.transcription === 'enabled'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.transcription === 'enabled') {
|
||||||
|
this.addExplanation(
|
||||||
|
$localize`<strong>Enable automatic transcription</strong> of videos to create subtitles and improve accessibility`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeAuth () {
|
||||||
|
if (!exists(this.authType)) return
|
||||||
|
|
||||||
|
const configStr = $localize` The plugin <strong>must be configured</strong> after the pre-configuration wizard confirmation.`
|
||||||
|
|
||||||
|
if (this.authType === 'ldap') {
|
||||||
|
this.addExplanation($localize`Install the <strong>LDAP</strong> authentication plugin.` + configStr)
|
||||||
|
|
||||||
|
this.plugins.push('peertube-plugin-auth-ldap')
|
||||||
|
} else if (this.authType === 'saml') {
|
||||||
|
this.addExplanation($localize`Install the <strong>SAML 2.0</strong> authentication plugin.` + configStr)
|
||||||
|
|
||||||
|
this.plugins.push('peertube-plugin-auth-saml2')
|
||||||
|
} else if (this.authType === 'oidc') {
|
||||||
|
this.addExplanation($localize`Install the <strong>OpenID Connect</strong> authentication plugin.` + configStr)
|
||||||
|
|
||||||
|
this.plugins.push('peertube-plugin-auth-openid-connect')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addConfig (newConfig: PartialDeep<CustomConfig>) {
|
||||||
|
return this.config = merge(this.config, newConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private addExplanation (explanation: string) {
|
||||||
|
this.unsafeExplanations.push(explanation)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,86 +0,0 @@
|
||||||
<ng-template #modal let-hide="close">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h4 i18n class="modal-title">Welcome to PeerTube, dear administrator!</h4>
|
|
||||||
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
|
|
||||||
<my-global-icon iconName="cross"></my-global-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
|
|
||||||
<div class="block-documentation">
|
|
||||||
<div class="columns">
|
|
||||||
<a class="link-block" href="https://docs.joinpeertube.org/maintain/tools" target="_blank" i18n-title title="Go to the CLI documentation">
|
|
||||||
<h5 i18n class="link-primary">CLI documentation</h5>
|
|
||||||
|
|
||||||
<div i18n>Upload or import videos, parse logs, prune storage directories, reset user password...</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a class="link-block" href="https://docs.joinpeertube.org/admin/following-instances" target="_blank" i18n-title title="Go to the admin documentation">
|
|
||||||
<h5 i18n class="link-primary">Admin documentation</h5>
|
|
||||||
|
|
||||||
<div i18n>Managing users, following other instances, dealing with spammers...</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a class="link-block" href="https://docs.joinpeertube.org/use/setup-account" target="_blank" i18n-title title="Go to the user documentation">
|
|
||||||
<h5 i18n class="link-primary">User documentation</h5>
|
|
||||||
|
|
||||||
<div i18n>Setup your account, managing video playlists, discover third-party applications...</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="two-columns">
|
|
||||||
|
|
||||||
<img class="mascot mascot-fw" src="/client/assets/images/mascot/pointing.svg" alt="mascot">
|
|
||||||
|
|
||||||
<div class="block-links">
|
|
||||||
<div i18n class="subtitle">Useful links</div>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li i18n>
|
|
||||||
Official PeerTube website (news, support, contribute...): <a href="https://joinpeertube.org" target="_blank" rel="noopener noreferrer">https://joinpeertube.org</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li i18n>
|
|
||||||
Put your instance on the public PeerTube index: <a href="https://instances.joinpeertube.org/instances">https://instances.joinpeertube.org/instances</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="two-columns">
|
|
||||||
<img class="mascot" src="/client/assets/images/mascot/happy.svg" alt="mascot">
|
|
||||||
|
|
||||||
<div class="block-configuration">
|
|
||||||
<div i18n class="subtitle">It's time to configure your instance!</div>
|
|
||||||
|
|
||||||
<p i18n>
|
|
||||||
Choosing your <strong>instance name</strong>, <strong>setting up a description</strong>, specifying <strong>who you are</strong>,
|
|
||||||
why <strong>you created your instance</strong> and <strong>how long</strong> you plan to <strong>maintain it</strong>
|
|
||||||
is very important for visitors to understand on what type of instance they are.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p i18n>
|
|
||||||
If you want to open registrations, please decide what <strong>your moderation rules</strong> and <strong>instance
|
|
||||||
terms of service</strong> are, as well as specify the categories and languages and your moderators speak.
|
|
||||||
This way, you will help users to register on <strong>the appropriate</strong> PeerTube instance.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer inputs">
|
|
||||||
<input
|
|
||||||
type="button" role="button" i18n-value value="Remind me later" class="peertube-button secondary-button"
|
|
||||||
(click)="hide()" (key.enter)="hide()"
|
|
||||||
>
|
|
||||||
|
|
||||||
<a i18n (click)="doNotOpenAgain(); hide()" (key.enter)="doNotOpenAgain(); hide()"
|
|
||||||
class="peertube-button-link primary-button" href="/admin/settings/config/edit-custom" target="_blank"
|
|
||||||
rel="noopener noreferrer" ngbAutofocus>
|
|
||||||
Configure my instance
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</ng-template>
|
|
|
@ -1,70 +0,0 @@
|
||||||
@use '_variables' as *;
|
|
||||||
@use '_mixins' as *;
|
|
||||||
|
|
||||||
.two-columns {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 50px;
|
|
||||||
|
|
||||||
@include on-small-main-col {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mascot-fw {
|
|
||||||
width: 170px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mascot {
|
|
||||||
display: block;
|
|
||||||
min-width: 170px;
|
|
||||||
|
|
||||||
@include margin-right(2rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-weight: $font-semibold;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-documentation {
|
|
||||||
.subtitle {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.configure-instance {
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 18px;
|
|
||||||
margin: 20px 0 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.columns {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-block {
|
|
||||||
color: pvar(--fg);
|
|
||||||
padding: 10px;
|
|
||||||
transition: background-color 0.2s ease-in;
|
|
||||||
flex-basis: 33%;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
@include disable-default-a-behaviour;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
> h5 {
|
|
||||||
font-weight: $font-semibold;
|
|
||||||
font-size: 1rem;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
import { Component, ElementRef, OnInit, inject, output, viewChild } from '@angular/core'
|
|
||||||
import { Notifier, User, UserService } from '@app/core'
|
|
||||||
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
|
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import { logger } from '@root-helpers/logger'
|
|
||||||
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'my-admin-welcome-modal',
|
|
||||||
templateUrl: './admin-welcome-modal.component.html',
|
|
||||||
styleUrls: [ './admin-welcome-modal.component.scss' ],
|
|
||||||
imports: [ GlobalIconComponent ]
|
|
||||||
})
|
|
||||||
export class AdminWelcomeModalComponent implements OnInit {
|
|
||||||
private userService = inject(UserService)
|
|
||||||
private modalService = inject(NgbModal)
|
|
||||||
private notifier = inject(Notifier)
|
|
||||||
|
|
||||||
readonly modal = viewChild<ElementRef>('modal')
|
|
||||||
|
|
||||||
readonly created = output()
|
|
||||||
|
|
||||||
private LS_KEYS = {
|
|
||||||
NO_WELCOME_MODAL: 'no_welcome_modal'
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.created.emit()
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldOpen (user: User) {
|
|
||||||
if (this.modalService.hasOpenModals()) return false
|
|
||||||
if (user.noWelcomeModal === true) return false
|
|
||||||
if (peertubeLocalStorage.getItem(this.LS_KEYS.NO_WELCOME_MODAL) === 'true') return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
show () {
|
|
||||||
this.modalService.open(this.modal(), {
|
|
||||||
centered: true,
|
|
||||||
backdrop: 'static',
|
|
||||||
keyboard: false,
|
|
||||||
size: 'lg'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
doNotOpenAgain () {
|
|
||||||
peertubeLocalStorage.setItem(this.LS_KEYS.NO_WELCOME_MODAL, 'true')
|
|
||||||
|
|
||||||
this.userService.updateMyProfile({ noWelcomeModal: true })
|
|
||||||
.subscribe({
|
|
||||||
next: () => logger.info('We will not open the welcome modal again.'),
|
|
||||||
|
|
||||||
error: err => this.notifier.error(err.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -71,7 +71,7 @@ export class ConfirmComponent implements OnInit {
|
||||||
this.confirmButtonText = confirmButtonText || $localize`Confirm`
|
this.confirmButtonText = confirmButtonText || $localize`Confirm`
|
||||||
this.cancelButtonText = cancelButtonText || $localize`Cancel`
|
this.cancelButtonText = cancelButtonText || $localize`Cancel`
|
||||||
|
|
||||||
this.html.toSimpleSafeHtml(message)
|
this.html.toSimpleSafeHtmlWithLinks(message)
|
||||||
.then(html => {
|
.then(html => {
|
||||||
this.message = html
|
this.message = html
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ export class InstanceConfigWarningModalComponent implements OnInit {
|
||||||
this.created.emit()
|
this.created.emit()
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldOpenByUser (user: User) {
|
canBeOpenByUser (user: User) {
|
||||||
if (this.modalService.hasOpenModals()) return false
|
if (this.modalService.hasOpenModals()) return false
|
||||||
if (user.noInstanceConfigWarningModal === true) return false
|
if (user.noInstanceConfigWarningModal === true) return false
|
||||||
if (peertubeLocalStorage.getItem(this.LS_KEYS.NO_INSTANCE_CONFIG_WARNING_MODAL) === 'true') return false
|
if (peertubeLocalStorage.getItem(this.LS_KEYS.NO_INSTANCE_CONFIG_WARNING_MODAL) === 'true') return false
|
||||||
|
@ -44,7 +44,7 @@ export class InstanceConfigWarningModalComponent implements OnInit {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldOpen (serverConfig: ServerConfig, about: About) {
|
shouldAutoOpen (serverConfig: ServerConfig, about: About) {
|
||||||
if (!serverConfig.signup.allowed) return false
|
if (!serverConfig.signup.allowed) return false
|
||||||
|
|
||||||
return serverConfig.instance.name.toLowerCase() === 'peertube' ||
|
return serverConfig.instance.name.toLowerCase() === 'peertube' ||
|
||||||
|
|
|
@ -8,8 +8,8 @@ import { CustomConfig } from '@peertube/peertube-models'
|
||||||
import { DeepPartial } from '@peertube/peertube-typescript-utils'
|
import { DeepPartial } from '@peertube/peertube-typescript-utils'
|
||||||
import merge from 'lodash-es/merge'
|
import merge from 'lodash-es/merge'
|
||||||
import { catchError, map, switchMap } from 'rxjs/operators'
|
import { catchError, map, switchMap } from 'rxjs/operators'
|
||||||
import { environment } from '../../../../environments/environment'
|
import { environment } from '../../../environments/environment'
|
||||||
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
|
import { SelectOptionsItem } from '../../../types/select-options-item.model'
|
||||||
|
|
||||||
export type FormResolutions = {
|
export type FormResolutions = {
|
||||||
'0p': FormControl<boolean>
|
'0p': FormControl<boolean>
|
||||||
|
@ -111,6 +111,11 @@ export class AdminConfigService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCustomConfigReloadedObs () {
|
||||||
|
return this.serverService.configReloaded
|
||||||
|
.pipe(switchMap(() => this.getCustomConfig()))
|
||||||
|
}
|
||||||
|
|
||||||
saveAndUpdateCurrent (options: {
|
saveAndUpdateCurrent (options: {
|
||||||
currentConfig: CustomConfig
|
currentConfig: CustomConfig
|
||||||
form: FormGroup
|
form: FormGroup
|
|
@ -13,7 +13,7 @@ import {
|
||||||
RegisteredServerSettings,
|
RegisteredServerSettings,
|
||||||
ResultList
|
ResultList
|
||||||
} from '@peertube/peertube-models'
|
} from '@peertube/peertube-models'
|
||||||
import { environment } from '../../../../environments/environment'
|
import { environment } from '../../../environments/environment'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PluginApiService {
|
export class PluginApiService {
|
|
@ -1,7 +1,7 @@
|
||||||
@use '_variables' as *;
|
@use "_variables" as *;
|
||||||
@use '_mixins' as *;
|
@use "_mixins" as *;
|
||||||
@use '_button-mixins' as *;
|
@use "_button-mixins" as *;
|
||||||
@use '_form-mixins' as *;
|
@use "_form-mixins" as *;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
@include peertube-input-text(auto);
|
@include peertube-input-text(auto);
|
||||||
|
@ -9,11 +9,11 @@ input {
|
||||||
|
|
||||||
.btn,
|
.btn,
|
||||||
my-copy-button ::ng-deep .btn {
|
my-copy-button ::ng-deep .btn {
|
||||||
background-color: pvar(--bg-secondary-400);
|
background-color: pvar(--input-bg);
|
||||||
border-left: 1px solid pvar(--bg);
|
border-left: 1px solid pvar(--bg);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: pvar(--bg-secondary-450);
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
21
client/src/app/shared/shared-icons/custom-icon.component.ts
Normal file
21
client/src/app/shared/shared-icons/custom-icon.component.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { ChangeDetectionStrategy, Component, ElementRef, OnInit, inject, input } from '@angular/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-custom-icon',
|
||||||
|
template: '',
|
||||||
|
styleUrls: [ './common-icon.component.scss' ],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
standalone: true
|
||||||
|
})
|
||||||
|
export class CustomIconComponent implements OnInit {
|
||||||
|
private el = inject(ElementRef)
|
||||||
|
|
||||||
|
readonly html = input.required<string>()
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
const nativeElement = this.el.nativeElement as HTMLElement
|
||||||
|
|
||||||
|
nativeElement.innerHTML = this.html()
|
||||||
|
nativeElement.ariaHidden = 'true'
|
||||||
|
}
|
||||||
|
}
|
|
@ -105,7 +105,7 @@ export type GlobalIconName = keyof typeof icons
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-global-icon',
|
selector: 'my-global-icon',
|
||||||
template: '',
|
template: '',
|
||||||
styleUrls: [ './global-icon.component.scss' ],
|
styleUrls: [ './common-icon.component.scss' ],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true
|
standalone: true
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
@use '_variables' as *;
|
@use "_variables" as *;
|
||||||
@use '_mixins' as *;
|
@use "_mixins" as *;
|
||||||
@use '_button-mixins' as *;
|
@use "_button-mixins" as *;
|
||||||
|
|
||||||
@mixin reduced-padding {
|
@mixin reduced-padding {
|
||||||
padding: pvar(--input-y-padding) calc(#{pvar(--input-x-padding)} / 2) !important;
|
padding: pvar(--input-y-padding) calc(#{pvar(--input-x-padding)} / 2) !important;
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
|
|
||||||
@include margin-right(3px);
|
@include margin-right(0.5rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.rounded-icon-button) {
|
&:not(.rounded-icon-button) {
|
||||||
|
|
|
@ -4,4 +4,5 @@ import { Subject } from 'rxjs'
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class PeertubeModalService {
|
export class PeertubeModalService {
|
||||||
openQuickSettingsSubject = new Subject<void>()
|
openQuickSettingsSubject = new Subject<void>()
|
||||||
|
openAdminConfigWizardSubject = new Subject<{ showWelcome: boolean }>()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
@use '_variables' as *;
|
@use "_variables" as *;
|
||||||
@use '_mixins' as *;
|
@use "_css-variables" as *;
|
||||||
|
@use "_mixins" as *;
|
||||||
|
|
||||||
$filters-background: pvar(--bg-secondary-400);
|
$filters-background: pvar(--bg-secondary-400);
|
||||||
|
|
||||||
|
@ -51,17 +52,15 @@ $filters-background: pvar(--bg-secondary-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters {
|
.filters {
|
||||||
--input-bg: #{pvar(--input-bg-in-secondary)};
|
|
||||||
--input-border-color: #{pvar(--input-bg-in-secondary)};
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
background-color: $filters-background;
|
background-color: $filters-background;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
|
|
||||||
|
@include define-input-css-variables-in-secondary;
|
||||||
@include rfs(1.5rem, padding);
|
@include rfs(1.5rem, padding);
|
||||||
|
|
||||||
input[type=radio] + label {
|
input[type="radio"] + label {
|
||||||
font-weight: $font-regular;
|
font-weight: $font-regular;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +77,7 @@ $filters-background: pvar(--bg-secondary-400);
|
||||||
|
|
||||||
.active-filters {
|
.active-filters {
|
||||||
.active-filter:not(:last-child)::after {
|
.active-filter:not(:last-child)::after {
|
||||||
content: '•';
|
content: "•";
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
|
|
6
client/src/assets/images/feather/institution.svg
Normal file
6
client/src/assets/images/feather/institution.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-briefcase-business-icon lucide-briefcase-business">
|
||||||
|
<path d="M12 12h.01" />
|
||||||
|
<path d="M16 6V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2" />
|
||||||
|
<path d="M22 13a18.15 18.15 0 0 1-20 0" />
|
||||||
|
<rect width="20" height="14" x="2" y="6" rx="2" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 441 B |
4
client/src/assets/images/feather/key.svg
Normal file
4
client/src/assets/images/feather/key.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-key-round-icon lucide-key-round">
|
||||||
|
<path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" />
|
||||||
|
<circle cx="16.5" cy="7.5" r=".5" fill="currentColor" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 488 B |
|
@ -132,12 +132,7 @@ export class ThemeManager {
|
||||||
}, iteration = 0) {
|
}, 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')
|
||||||
|
} else if (!this.canInjectCoreColorPalette()) {
|
||||||
this.injectColorPalette()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.canInjectCoreColorPalette()) {
|
|
||||||
return setTimeout(() => this.injectColorPalette(options, iteration + 1), Math.floor(iteration / 10))
|
return setTimeout(() => this.injectColorPalette(options, iteration + 1), Math.floor(iteration / 10))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
8
client/src/sass/bootstrap.scss
vendored
8
client/src/sass/bootstrap.scss
vendored
|
@ -1,4 +1,5 @@
|
||||||
@use "_variables" as *;
|
@use "_variables" as *;
|
||||||
|
@use "_css-variables" as *;
|
||||||
@use "_mixins" as *;
|
@use "_mixins" as *;
|
||||||
@use "_button-mixins" as *;
|
@use "_button-mixins" as *;
|
||||||
@import "./_bootstrap-variables";
|
@import "./_bootstrap-variables";
|
||||||
|
@ -117,8 +118,6 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
padding: 3px 15px;
|
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: pvar(--on-primary) !important;
|
color: pvar(--on-primary) !important;
|
||||||
background-color: pvar(--primary);
|
background-color: pvar(--primary);
|
||||||
|
@ -180,8 +179,9 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
|
@include define-input-css-variables-in-modal;
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background-color: pvar(--bg);
|
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,7 +251,7 @@ body {
|
||||||
@include disable-default-a-behaviour;
|
@include disable-default-a-behaviour;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: pvar(--bg) !important;
|
background-color: transparent !important;
|
||||||
border-bottom-color: pvar(--border-primary);
|
border-bottom-color: pvar(--border-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
|
|
||||||
$modal-footer-border-width: 0;
|
$modal-footer-border-width: 0;
|
||||||
$modal-md: 600px;
|
$modal-md: 600px;
|
||||||
|
$modal-lg: 900px;
|
||||||
|
$modal-content-border-radius: 14px;
|
||||||
|
$modal-content-bg: pvar(--bg-secondary-350);
|
||||||
|
|
||||||
$grid-breakpoints: (
|
$grid-breakpoints: (
|
||||||
// CLASSIC BREAKPOINTS GROUP
|
// CLASSIC BREAKPOINTS GROUP
|
||||||
|
|
|
@ -27,7 +27,8 @@
|
||||||
--input-fg: var(--inputForegroundColor, #{pvar(--fg)});
|
--input-fg: var(--inputForegroundColor, #{pvar(--fg)});
|
||||||
|
|
||||||
--input-bg: var(--inputBackgroundColor, #{pvar(--bg-secondary-400)});
|
--input-bg: var(--inputBackgroundColor, #{pvar(--bg-secondary-400)});
|
||||||
--input-bg-in-secondary: #{pvar(--input-bg-550)};
|
--input-bg-in-modal: #{pvar(--input-bg-550)};
|
||||||
|
--input-bg-in-secondary: #{pvar(--input-bg-600)};
|
||||||
|
|
||||||
--input-danger-fg: #9C221C;
|
--input-danger-fg: #9C221C;
|
||||||
--input-danger-bg: #FEBBB2;
|
--input-danger-bg: #FEBBB2;
|
||||||
|
@ -48,6 +49,8 @@
|
||||||
--textarea-x-padding: 15px;
|
--textarea-x-padding: 15px;
|
||||||
--textarea-fg: var(--textareaForegroundColor, #{pvar(--input-fg)});
|
--textarea-fg: var(--textareaForegroundColor, #{pvar(--input-fg)});
|
||||||
--textarea-bg: var(--textareaBackgroundColor, #{pvar(--input-bg)});
|
--textarea-bg: var(--textareaBackgroundColor, #{pvar(--input-bg)});
|
||||||
|
--textarea-bg-in-modal: #{pvar(--input-bg-in-modal)};
|
||||||
|
--textarea-bg-in-secondary: #{pvar(--input-bg-in-secondary)};
|
||||||
|
|
||||||
--support-btn-fg: var(--supportButtonColor, #{pvar(--fg-300)});
|
--support-btn-fg: var(--supportButtonColor, #{pvar(--fg-300)});
|
||||||
--support-btn-bg: var(--supportButtonBackgroundColor, transparent);
|
--support-btn-bg: var(--supportButtonBackgroundColor, transparent);
|
||||||
|
@ -158,3 +161,22 @@
|
||||||
--active-icon-bg: #{pvar(--bg-secondary-600)};
|
--active-icon-bg: #{pvar(--bg-secondary-600)};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin define-input-css-variables-in-secondary() {
|
||||||
|
--textarea-bg: #{pvar(--textarea-bg-in-secondary)};
|
||||||
|
|
||||||
|
@include define-input-css-variables(pvar(--input-bg-in-secondary));
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin define-input-css-variables-in-modal() {
|
||||||
|
--textarea-bg: #{pvar(--textarea-bg-in-modal)};
|
||||||
|
|
||||||
|
@include define-input-css-variables(pvar(--input-bg-in-modal));
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin define-input-css-variables($value) {
|
||||||
|
--input-bg: #{$value};
|
||||||
|
--input-border-color: #{$value};
|
||||||
|
--p-multiselect-background: #{$value};
|
||||||
|
--p-select-background: #{$value};
|
||||||
|
}
|
||||||
|
|
|
@ -100,6 +100,7 @@ $variables: (
|
||||||
--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-bg-in-modal: var(--input-bg-in-modal),
|
||||||
--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),
|
||||||
|
@ -114,6 +115,8 @@ $variables: (
|
||||||
--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),
|
||||||
|
--textarea-bg-in-secondary: var(--textarea-bg-in-secondary),
|
||||||
|
--textarea-bg-in-modal: var(--textarea-bg-in-modal),
|
||||||
--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),
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
@use '_variables' as *;
|
@use "_variables" as *;
|
||||||
@use '_mixins' as *;
|
@use "_mixins" as *;
|
||||||
@use '_css-variables' as *;
|
@use "_css-variables" as *;
|
||||||
@use '../player/build/peertube-player';
|
@use "../player/build/peertube-player";
|
||||||
|
|
||||||
[hidden] {
|
[hidden] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
:root {
|
||||||
@include define-css-variables();
|
@include define-css-variables();
|
||||||
|
}
|
||||||
|
|
||||||
& {
|
body {
|
||||||
font-family: $main-fonts;
|
font-family: $main-fonts;
|
||||||
font-weight: $font-regular;
|
font-weight: $font-regular;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
video {
|
video {
|
||||||
width: 99%;
|
width: 99%;
|
||||||
|
@ -66,11 +66,11 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
#error-details {
|
#error-details {
|
||||||
margin-top: 30px
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#error-details-content {
|
#error-details-content {
|
||||||
margin-top: 10px
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#placeholder-preview {
|
#placeholder-preview {
|
||||||
|
@ -141,4 +141,3 @@ body {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ export class PeerTubeTheme {
|
||||||
|
|
||||||
this.themeManager.loadThemeStyle(themeName)
|
this.themeManager.loadThemeStyle(themeName)
|
||||||
|
|
||||||
this.themeManager.injectCoreColorPalette()
|
this.themeManager.injectColorPalette({ config: config.theme, currentTheme: themeName })
|
||||||
}
|
}
|
||||||
|
|
||||||
loadThemePlugins (config: HTMLServerConfig) {
|
loadThemePlugins (config: HTMLServerConfig) {
|
||||||
|
|
|
@ -121,39 +121,6 @@ email:
|
||||||
subject:
|
subject:
|
||||||
prefix: '[PeerTube]'
|
prefix: '[PeerTube]'
|
||||||
|
|
||||||
# Update default PeerTube values
|
|
||||||
# Set by API when the field is not provided and put as default value in client
|
|
||||||
defaults:
|
|
||||||
# Change default values when publishing a video (upload/import/go Live)
|
|
||||||
publish:
|
|
||||||
download_enabled: true
|
|
||||||
|
|
||||||
# enabled = 1, disabled = 2, requires_approval = 3
|
|
||||||
comments_policy: 1
|
|
||||||
|
|
||||||
# public = 1, unlisted = 2, private = 3, internal = 4
|
|
||||||
privacy: 1
|
|
||||||
|
|
||||||
# CC-BY = 1, CC-SA = 2, CC-ND = 3, CC-NC = 4, CC-NC-SA = 5, CC-NC-ND = 6, Public Domain = 7
|
|
||||||
# You can also choose a custom licence value added by a plugin
|
|
||||||
# No licence by default
|
|
||||||
licence: null
|
|
||||||
|
|
||||||
p2p:
|
|
||||||
# Enable P2P by default in PeerTube client
|
|
||||||
# Can be enabled/disabled by anonymous users and logged in users
|
|
||||||
webapp:
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
# Enable P2P by default in PeerTube embed
|
|
||||||
# Can be enabled/disabled by URL option
|
|
||||||
embed:
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
player:
|
|
||||||
# By default, playback starts automatically when opening a video
|
|
||||||
auto_play: true
|
|
||||||
|
|
||||||
# From the project root directory
|
# From the project root directory
|
||||||
storage:
|
storage:
|
||||||
tmp: 'storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing...
|
tmp: 'storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing...
|
||||||
|
@ -654,7 +621,6 @@ transcoding:
|
||||||
|
|
||||||
# Generate videos in a web compatible format
|
# Generate videos in a web compatible format
|
||||||
# If you also enabled the hls format, it will multiply videos storage by 2
|
# If you also enabled the hls format, it will multiply videos storage by 2
|
||||||
# If disabled, breaks federation with PeerTube instances < 2.1
|
|
||||||
web_videos:
|
web_videos:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
@ -880,6 +846,7 @@ import:
|
||||||
# Max number of videos to import when the user asks for full sync
|
# Max number of videos to import when the user asks for full sync
|
||||||
full_sync_videos_limit: 1000
|
full_sync_videos_limit: 1000
|
||||||
|
|
||||||
|
# Add ability for your users to import a PeerTube archive file to automatically create videos, channels, captions, etc
|
||||||
users:
|
users:
|
||||||
# Video quota is checked on import so the user doesn't upload a too big archive file
|
# Video quota is checked on import so the user doesn't upload a too big archive file
|
||||||
# Video quota (daily quota is not taken into account) is also checked for each video when PeerTube is processing the import
|
# Video quota (daily quota is not taken into account) is also checked for each video when PeerTube is processing the import
|
||||||
|
@ -1136,3 +1103,36 @@ client:
|
||||||
storyboards:
|
storyboards:
|
||||||
# Generate storyboards of local videos using ffmpeg so users can see the video preview in the player while scrubbing the video
|
# Generate storyboards of local videos using ffmpeg so users can see the video preview in the player while scrubbing the video
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
# Update default PeerTube values
|
||||||
|
# Set by API when the field is not provided and put as default value in client
|
||||||
|
defaults:
|
||||||
|
# Change default values when publishing a video (upload/import/go Live)
|
||||||
|
publish:
|
||||||
|
download_enabled: true
|
||||||
|
|
||||||
|
# enabled = 1, disabled = 2, requires_approval = 3
|
||||||
|
comments_policy: 1
|
||||||
|
|
||||||
|
# public = 1, unlisted = 2, private = 3, internal = 4
|
||||||
|
privacy: 1
|
||||||
|
|
||||||
|
# CC-BY = 1, CC-SA = 2, CC-ND = 3, CC-NC = 4, CC-NC-SA = 5, CC-NC-ND = 6, Public Domain = 7
|
||||||
|
# You can also choose a custom licence value added by a plugin
|
||||||
|
# No licence by default
|
||||||
|
licence: null
|
||||||
|
|
||||||
|
p2p:
|
||||||
|
# Enable P2P by default in PeerTube client
|
||||||
|
# Can be enabled/disabled by anonymous users and logged in users
|
||||||
|
webapp:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Enable P2P by default in PeerTube embed
|
||||||
|
# Can be enabled/disabled by URL option
|
||||||
|
embed:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
player:
|
||||||
|
# By default, playback starts automatically when opening a video
|
||||||
|
auto_play: true
|
||||||
|
|
|
@ -119,39 +119,6 @@ email:
|
||||||
subject:
|
subject:
|
||||||
prefix: '[PeerTube]'
|
prefix: '[PeerTube]'
|
||||||
|
|
||||||
# Update default PeerTube values
|
|
||||||
# Set by API when the field is not provided and put as default value in client
|
|
||||||
defaults:
|
|
||||||
# Change default values when publishing a video (upload/import/go Live)
|
|
||||||
publish:
|
|
||||||
download_enabled: true
|
|
||||||
|
|
||||||
# enabled = 1, disabled = 2, requires_approval = 3
|
|
||||||
comments_policy: 1
|
|
||||||
|
|
||||||
# public = 1, unlisted = 2, private = 3, internal = 4
|
|
||||||
privacy: 1
|
|
||||||
|
|
||||||
# CC-BY = 1, CC-SA = 2, CC-ND = 3, CC-NC = 4, CC-NC-SA = 5, CC-NC-ND = 6, Public Domain = 7
|
|
||||||
# You can also choose a custom licence value added by a plugin
|
|
||||||
# No licence by default
|
|
||||||
licence: null
|
|
||||||
|
|
||||||
p2p:
|
|
||||||
# Enable P2P by default in PeerTube client
|
|
||||||
# Can be enabled/disabled by anonymous users and logged in users
|
|
||||||
webapp:
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
# Enable P2P by default in PeerTube embed
|
|
||||||
# Can be enabled/disabled by URL option
|
|
||||||
embed:
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
player:
|
|
||||||
# By default, playback starts automatically when opening a video
|
|
||||||
auto_play: true
|
|
||||||
|
|
||||||
# From the project root directory
|
# From the project root directory
|
||||||
storage:
|
storage:
|
||||||
tmp: '/var/www/peertube/storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing...
|
tmp: '/var/www/peertube/storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing...
|
||||||
|
@ -664,7 +631,6 @@ transcoding:
|
||||||
|
|
||||||
# Generate videos in a web compatible format
|
# Generate videos in a web compatible format
|
||||||
# If you also enabled the hls format, it will multiply videos storage by 2
|
# If you also enabled the hls format, it will multiply videos storage by 2
|
||||||
# If disabled, breaks federation with PeerTube instances < 2.1
|
|
||||||
web_videos:
|
web_videos:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
@ -890,6 +856,7 @@ import:
|
||||||
# Max number of videos to import when the user asks for full sync
|
# Max number of videos to import when the user asks for full sync
|
||||||
full_sync_videos_limit: 1000
|
full_sync_videos_limit: 1000
|
||||||
|
|
||||||
|
# Add ability for your users to import a PeerTube archive file to automatically create videos, channels, captions, etc
|
||||||
users:
|
users:
|
||||||
# Video quota is checked on import so the user doesn't upload a too big archive file
|
# Video quota is checked on import so the user doesn't upload a too big archive file
|
||||||
# Video quota (daily quota is not taken into account) is also checked for each video when PeerTube is processing the import
|
# Video quota (daily quota is not taken into account) is also checked for each video when PeerTube is processing the import
|
||||||
|
@ -1050,6 +1017,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
|
||||||
|
@ -1084,6 +1070,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
|
||||||
|
@ -1126,3 +1113,36 @@ client:
|
||||||
storyboards:
|
storyboards:
|
||||||
# Generate storyboards of local videos using ffmpeg so users can see the video preview in the player while scrubbing the video
|
# Generate storyboards of local videos using ffmpeg so users can see the video preview in the player while scrubbing the video
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
# Update default PeerTube values
|
||||||
|
# Set by API when the field is not provided and put as default value in client
|
||||||
|
defaults:
|
||||||
|
# Change default values when publishing a video (upload/import/go Live)
|
||||||
|
publish:
|
||||||
|
download_enabled: true
|
||||||
|
|
||||||
|
# enabled = 1, disabled = 2, requires_approval = 3
|
||||||
|
comments_policy: 1
|
||||||
|
|
||||||
|
# public = 1, unlisted = 2, private = 3, internal = 4
|
||||||
|
privacy: 1
|
||||||
|
|
||||||
|
# CC-BY = 1, CC-SA = 2, CC-ND = 3, CC-NC = 4, CC-NC-SA = 5, CC-NC-ND = 6, Public Domain = 7
|
||||||
|
# You can also choose a custom licence value added by a plugin
|
||||||
|
# No licence by default
|
||||||
|
licence: null
|
||||||
|
|
||||||
|
p2p:
|
||||||
|
# Enable P2P by default in PeerTube client
|
||||||
|
# Can be enabled/disabled by anonymous users and logged in users
|
||||||
|
webapp:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Enable P2P by default in PeerTube embed
|
||||||
|
# Can be enabled/disabled by URL option
|
||||||
|
embed:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
player:
|
||||||
|
# By default, playback starts automatically when opening a video
|
||||||
|
auto_play: true
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { VideoCommentPolicyType, VideoPrivacyType } from '../videos/index.js'
|
||||||
import { NSFWPolicyType } from '../videos/nsfw-policy.type.js'
|
import { NSFWPolicyType } from '../videos/nsfw-policy.type.js'
|
||||||
import { BroadcastMessageLevel } from './broadcast-message-level.type.js'
|
import { BroadcastMessageLevel } from './broadcast-message-level.type.js'
|
||||||
|
|
||||||
|
@ -320,4 +321,27 @@ export interface CustomConfig {
|
||||||
storyboards: {
|
storyboards: {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaults: {
|
||||||
|
publish: {
|
||||||
|
downloadEnabled: boolean
|
||||||
|
commentsPolicy: VideoCommentPolicyType
|
||||||
|
privacy: VideoPrivacyType
|
||||||
|
licence: number
|
||||||
|
}
|
||||||
|
|
||||||
|
p2p: {
|
||||||
|
webapp: {
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
embed: {
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
player: {
|
||||||
|
autoPlay: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
import { ActorImageType, CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
|
import { ActorImageType, CustomConfig, HttpStatusCode, VideoCommentPolicy, VideoPrivacy } from '@peertube/peertube-models'
|
||||||
import {
|
import {
|
||||||
PeerTubeServer,
|
PeerTubeServer,
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
|
@ -188,7 +188,19 @@ function buildNewCustomConfig (server: PeerTubeServer): CustomConfig {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
default: 'default'
|
default: 'default',
|
||||||
|
customization: {
|
||||||
|
primaryColor: '#001',
|
||||||
|
foregroundColor: '#002',
|
||||||
|
backgroundColor: '#003',
|
||||||
|
backgroundSecondaryColor: '#004',
|
||||||
|
menuForegroundColor: '#005',
|
||||||
|
menuBackgroundColor: '#006',
|
||||||
|
menuBorderRadius: '1px',
|
||||||
|
headerForegroundColor: '#008',
|
||||||
|
headerBackgroundColor: '#009',
|
||||||
|
inputBorderRadius: '2px'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
services: {
|
services: {
|
||||||
twitter: {
|
twitter: {
|
||||||
|
@ -410,6 +422,25 @@ function buildNewCustomConfig (server: PeerTubeServer): CustomConfig {
|
||||||
exportExpiration: 43,
|
exportExpiration: 43,
|
||||||
maxUserVideoQuota: 42
|
maxUserVideoQuota: 42
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
publish: {
|
||||||
|
commentsPolicy: VideoCommentPolicy.REQUIRES_APPROVAL,
|
||||||
|
downloadEnabled: false,
|
||||||
|
licence: 2,
|
||||||
|
privacy: VideoPrivacy.INTERNAL
|
||||||
|
},
|
||||||
|
p2p: {
|
||||||
|
embed: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
webapp: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
player: {
|
||||||
|
autoPlay: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -508,6 +508,26 @@ function customConfig (): CustomConfig {
|
||||||
},
|
},
|
||||||
storyboards: {
|
storyboards: {
|
||||||
enabled: CONFIG.STORYBOARDS.ENABLED
|
enabled: CONFIG.STORYBOARDS.ENABLED
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
publish: {
|
||||||
|
downloadEnabled: CONFIG.DEFAULTS.PUBLISH.DOWNLOAD_ENABLED,
|
||||||
|
commentsPolicy: CONFIG.DEFAULTS.PUBLISH.COMMENTS_POLICY,
|
||||||
|
privacy: CONFIG.DEFAULTS.PUBLISH.PRIVACY,
|
||||||
|
licence: CONFIG.DEFAULTS.PUBLISH.LICENCE
|
||||||
|
},
|
||||||
|
p2p: {
|
||||||
|
webapp: {
|
||||||
|
enabled: CONFIG.DEFAULTS.P2P.WEBAPP.ENABLED
|
||||||
|
},
|
||||||
|
|
||||||
|
embed: {
|
||||||
|
enabled: CONFIG.DEFAULTS.P2P.EMBED.ENABLED
|
||||||
|
}
|
||||||
|
},
|
||||||
|
player: {
|
||||||
|
autoPlay: CONFIG.DEFAULTS.PLAYER.AUTO_PLAY
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10043,6 +10043,43 @@ components:
|
||||||
type: boolean
|
type: boolean
|
||||||
manualApproval:
|
manualApproval:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
storyboard:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
defaults:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
publish:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
downloadEnabled:
|
||||||
|
type: boolean
|
||||||
|
commentsPolicy:
|
||||||
|
$ref: '#/components/schemas/VideoCommentsPolicySet'
|
||||||
|
privacy:
|
||||||
|
$ref: '#/components/schemas/VideoPrivacySet'
|
||||||
|
licence:
|
||||||
|
$ref: '#/components/schemas/VideoLicenceSet'
|
||||||
|
p2p:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
webapp:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
embed:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
player:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
autoPlay:
|
||||||
|
type: boolean
|
||||||
|
|
||||||
CustomHomepage:
|
CustomHomepage:
|
||||||
properties:
|
properties:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue