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
|
||||
} from './pages'
|
||||
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) => {
|
||||
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>
|
||||
|
||||
<ng-container [formGroup]="form">
|
||||
<form [formGroup]="form">
|
||||
|
||||
<div class="pt-two-cols">
|
||||
|
||||
|
@ -109,4 +109,4 @@
|
|||
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</form>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { ActivatedRoute } from '@angular/router'
|
||||
import { CanComponentDeactivate } from '@app/core'
|
||||
|
@ -12,7 +12,8 @@ import {
|
|||
} from '@app/shared/form-validators/form-validator.model'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { CustomConfig } from '@peertube/peertube-models'
|
||||
import { AdminConfigService } from '../shared/admin-config.service'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { AdminConfigService } from '../../../shared/shared-admin/admin-config.service'
|
||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||
|
||||
type Form = {
|
||||
|
@ -44,7 +45,7 @@ type Form = {
|
|||
styleUrls: [ './admin-config-common.scss' ],
|
||||
imports: [ CommonModule, FormsModule, ReactiveFormsModule, AdminSaveBarComponent ]
|
||||
})
|
||||
export class AdminConfigAdvancedComponent implements OnInit, CanComponentDeactivate {
|
||||
export class AdminConfigAdvancedComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
private route = inject(ActivatedRoute)
|
||||
private formReactiveService = inject(FormReactiveService)
|
||||
private adminConfigService = inject(AdminConfigService)
|
||||
|
@ -54,11 +55,23 @@ export class AdminConfigAdvancedComponent implements OnInit, CanComponentDeactiv
|
|||
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||
|
||||
private customConfig: CustomConfig
|
||||
private customConfigSub: Subscription
|
||||
|
||||
ngOnInit () {
|
||||
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||
|
||||
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 () {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<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">
|
||||
<h2 i18n>APPEARANCE</h2>
|
||||
</div>
|
||||
|
@ -31,7 +32,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4" [formGroup]="form">
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>CUSTOMIZATION</h2>
|
||||
|
||||
|
@ -110,7 +111,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4" [formGroup]="form">
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<div class="anchor" id="customizations"></div>
|
||||
<!-- customizations anchor -->
|
||||
|
@ -176,3 +177,4 @@ color: red;
|
|||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { ActivatedRoute, RouterModule } from '@angular/router'
|
||||
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 { objectKeysTyped } from '@peertube/peertube-core-utils'
|
||||
import { CustomConfig } from '@peertube/peertube-models'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
import { capitalizeFirstLetter } from '@root-helpers/string'
|
||||
import { ColorPaletteThemeConfig, ThemeCustomizationKey } from '@root-helpers/theme-manager'
|
||||
import { formatHEX, parse } from 'color-bits'
|
||||
import debug from 'debug'
|
||||
import { ColorPickerModule } from 'primeng/colorpicker'
|
||||
import { debounceTime } from 'rxjs'
|
||||
import { debounceTime, Subscription } from 'rxjs'
|
||||
import { SelectOptionsItem } from 'src/types'
|
||||
import { AdminConfigService } from '../../../shared/shared-admin/admin-config.service'
|
||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||
import { AlertComponent } from '../../../shared/shared-main/common/alert.component'
|
||||
import { AdminConfigService } from '../shared/admin-config.service'
|
||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||
|
||||
const debugLogger = debug('peertube:config')
|
||||
|
@ -75,7 +73,7 @@ type Form = {
|
|||
PeertubeCheckboxComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigCustomizationComponent implements OnInit, CanComponentDeactivate {
|
||||
export class AdminConfigCustomizationComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
private formReactiveService = inject(FormReactiveService)
|
||||
private adminConfigService = inject(AdminConfigService)
|
||||
private serverService = inject(ServerService)
|
||||
|
@ -99,6 +97,8 @@ export class AdminConfigCustomizationComponent implements OnInit, CanComponentDe
|
|||
private customizationResetFields = new Set<ThemeCustomizationKey>()
|
||||
private customConfig: CustomConfig
|
||||
|
||||
private customConfigSub: Subscription
|
||||
|
||||
private readonly formFieldsObject: Record<ThemeCustomizationKey, { label: string, description?: string, type: 'color' | 'pixels' }> = {
|
||||
primaryColor: { label: $localize`Primary color`, type: 'color' },
|
||||
foregroundColor: { label: $localize`Foreground color`, type: 'color' },
|
||||
|
@ -127,6 +127,17 @@ export class AdminConfigCustomizationComponent implements OnInit, CanComponentDe
|
|||
|
||||
this.buildForm()
|
||||
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 () {
|
||||
|
@ -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 {
|
||||
form,
|
||||
formErrors,
|
||||
validationMessages
|
||||
} = this.formReactiveService.buildForm<Form>(obj, defaultValues)
|
||||
} = this.formReactiveService.buildForm<Form>(obj, this.getDefaultFormValues())
|
||||
|
||||
this.form = form
|
||||
this.formErrors = formErrors
|
||||
|
@ -281,6 +283,17 @@ export class AdminConfigCustomizationComponent implements OnInit, CanComponentDe
|
|||
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 () {
|
||||
const config = this.customConfig.theme.customization
|
||||
|
||||
|
@ -305,39 +318,16 @@ export class AdminConfigCustomizationComponent implements OnInit, CanComponentDe
|
|||
|
||||
private formatCustomizationFieldForForm (field: ThemeCustomizationKey, value: string) {
|
||||
if (this.formFieldsObject[field].type === 'pixels') {
|
||||
return this.formatPixelsForForm(value)
|
||||
return this.themeService.formatPixelsForForm(value)
|
||||
}
|
||||
|
||||
if (this.formFieldsObject[field].type === 'color') {
|
||||
return this.formatColorForForm(value)
|
||||
return this.themeService.formatColorForForm(value)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
private formatPixelsForForm (value: string) {
|
||||
if (typeof value === 'number') return value + ''
|
||||
if (typeof value !== 'string') return null
|
||||
|
||||
const result = parseInt(value.replace(/px$/, ''))
|
||||
|
||||
if (isNaN(result)) return null
|
||||
|
||||
return result + ''
|
||||
}
|
||||
|
||||
private formatColorForForm (value: string) {
|
||||
if (!value) return null
|
||||
|
||||
try {
|
||||
return formatHEX(parse(value))
|
||||
} catch (err) {
|
||||
logger.warn(`Error parsing color value "${value}"`, err)
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private buildNewCustomization (formValues: any) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<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="title-col">
|
||||
<h2 i18n>BEHAVIOR</h2>
|
||||
|
@ -666,4 +666,4 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</form>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { ActivatedRoute, RouterLink } from '@angular/router'
|
||||
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 { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { BroadcastMessageLevel, CustomConfig } from '@peertube/peertube-models'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { pairwise } from 'rxjs/operators'
|
||||
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 { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||
import { UserRealQuotaInfoComponent } from '../../shared/user-real-quota-info.component'
|
||||
import { AdminConfigService } from '../shared/admin-config.service'
|
||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||
|
||||
type Form = {
|
||||
|
@ -192,7 +193,7 @@ type Form = {
|
|||
AdminSaveBarComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigGeneralComponent implements OnInit, CanComponentDeactivate {
|
||||
export class AdminConfigGeneralComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
private server = inject(ServerService)
|
||||
private route = inject(ActivatedRoute)
|
||||
private formReactiveService = inject(FormReactiveService)
|
||||
|
@ -209,6 +210,7 @@ export class AdminConfigGeneralComponent implements OnInit, CanComponentDeactiva
|
|||
exportMaxUserVideoQuotaOptions: SelectOptionsItem[] = []
|
||||
|
||||
private customConfig: CustomConfig
|
||||
private customConfigSub: Subscription
|
||||
|
||||
ngOnInit () {
|
||||
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` }
|
||||
]
|
||||
|
||||
this.exportMaxUserVideoQuotaOptions = this.getVideoQuotaOptions().filter(o => (o.id as number) >= 1)
|
||||
this.exportMaxUserVideoQuotaOptions = this.getVideoQuotaOptions().filter(o => o.id >= 1)
|
||||
|
||||
this.buildForm()
|
||||
|
||||
this.subscribeToSignupChanges()
|
||||
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 () {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<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">
|
||||
<h2 i18n>HOMEPAGE</h2>
|
||||
</div>
|
||||
|
@ -25,4 +25,4 @@
|
|||
<div *ngIf="formErrors.homepageContent" class="form-error" role="alert">{{ formErrors.homepageContent }}</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>
|
||||
|
||||
<ng-container [formGroup]="form">
|
||||
<form [formGroup]="form">
|
||||
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
|
@ -341,4 +341,4 @@
|
|||
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
</form>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
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 { ActivatedRoute, RouterLink } from '@angular/router'
|
||||
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 { ActorAvatarEditComponent } from '../../../shared/shared-actor-image-edit/actor-avatar-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 { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||
import { SelectCheckboxComponent } from '../../../shared/shared-forms/select/select-checkbox.component'
|
||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
||||
import { AdminConfigService } from '../shared/admin-config.service'
|
||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||
import { Subscription } from 'rxjs'
|
||||
|
||||
type Form = {
|
||||
admin: FormGroup<{
|
||||
|
@ -97,7 +98,7 @@ type Form = {
|
|||
AdminSaveBarComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigInformationComponent implements OnInit, CanComponentDeactivate {
|
||||
export class AdminConfigInformationComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
private customMarkup = inject(CustomMarkupService)
|
||||
private notifier = inject(Notifier)
|
||||
private instanceService = inject(InstanceService)
|
||||
|
@ -137,6 +138,7 @@ export class AdminConfigInformationComponent implements OnInit, CanComponentDeac
|
|||
|
||||
private serverConfig: HTMLServerConfig
|
||||
private customConfig: CustomConfig
|
||||
private customConfigSub: Subscription
|
||||
|
||||
get instanceName () {
|
||||
return this.server.getHTMLConfig().instance.name
|
||||
|
@ -157,6 +159,17 @@ export class AdminConfigInformationComponent implements OnInit, CanComponentDeac
|
|||
|
||||
this.updateActorImages()
|
||||
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 () {
|
||||
|
|
|
@ -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>
|
||||
|
||||
<ng-container [formGroup]="form">
|
||||
<form [formGroup]="form">
|
||||
|
||||
<div class="pt-two-cols">
|
||||
<div class="title-col">
|
||||
|
@ -212,4 +212,4 @@
|
|||
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</form>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { ActivatedRoute, RouterLink } from '@angular/router'
|
||||
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 { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
||||
import { AdminConfigService, FormResolutions, ResolutionOption } from '../shared/admin-config.service'
|
||||
import { AdminConfigService, FormResolutions, ResolutionOption } from '../../../shared/shared-admin/admin-config.service'
|
||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||
import { Subscription } from 'rxjs'
|
||||
|
||||
type Form = {
|
||||
live: FormGroup<{
|
||||
|
@ -73,7 +74,7 @@ type Form = {
|
|||
AdminSaveBarComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigLiveComponent implements OnInit, CanComponentDeactivate {
|
||||
export class AdminConfigLiveComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
private configService = inject(AdminConfigService)
|
||||
private server = inject(ServerService)
|
||||
private route = inject(ActivatedRoute)
|
||||
|
@ -91,6 +92,7 @@ export class AdminConfigLiveComponent implements OnInit, CanComponentDeactivate
|
|||
liveResolutions: ResolutionOption[] = []
|
||||
|
||||
private customConfig: CustomConfig
|
||||
private customConfigSub: Subscription
|
||||
|
||||
ngOnInit () {
|
||||
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||
|
@ -111,6 +113,17 @@ export class AdminConfigLiveComponent implements OnInit, CanComponentDeactivate
|
|||
)
|
||||
|
||||
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 () {
|
||||
|
|
|
@ -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>
|
||||
|
||||
<ng-container [formGroup]="form">
|
||||
<form [formGroup]="form">
|
||||
|
||||
<div class="pt-two-cols">
|
||||
<div class="title-col">
|
||||
|
@ -281,4 +281,4 @@
|
|||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</form>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { ActivatedRoute, RouterLink } from '@angular/router'
|
||||
import { CanComponentDeactivate, Notifier, ServerService } from '@app/core'
|
||||
|
@ -16,12 +16,13 @@ import {
|
|||
} from '@app/shared/form-validators/form-validator.model'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { CustomConfig } from '@peertube/peertube-models'
|
||||
import { Subscription } from 'rxjs'
|
||||
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 { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
||||
import { AdminConfigService, FormResolutions, ResolutionOption } from '../shared/admin-config.service'
|
||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||
|
||||
type Form = {
|
||||
|
@ -84,7 +85,7 @@ type Form = {
|
|||
AdminSaveBarComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigVODComponent implements OnInit, CanComponentDeactivate {
|
||||
export class AdminConfigVODComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
private configService = inject(AdminConfigService)
|
||||
private notifier = inject(Notifier)
|
||||
private server = inject(ServerService)
|
||||
|
@ -103,6 +104,7 @@ export class AdminConfigVODComponent implements OnInit, CanComponentDeactivate {
|
|||
additionalVideoExtensions = ''
|
||||
|
||||
private customConfig: CustomConfig
|
||||
private customConfigSub: Subscription
|
||||
|
||||
ngOnInit () {
|
||||
const serverConfig = this.server.getHTMLConfig()
|
||||
|
@ -117,6 +119,17 @@ export class AdminConfigVODComponent implements OnInit, CanComponentDeactivate {
|
|||
this.buildForm()
|
||||
|
||||
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 () {
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
<div class="root-bar">
|
||||
<h2>{{ title() }}</h2>
|
||||
|
||||
<my-button
|
||||
theme="primary" class="save-button" icon="circle-tick"
|
||||
[disabled]="!canUpdate()" (click)="onSave($event)" i18n
|
||||
>Save</my-button>
|
||||
<div class="buttons">
|
||||
<my-button theme="secondary" class="pre-config" (click)="openConfigWizard()" i18n>Open config wizard</my-button>
|
||||
|
||||
<my-button theme="primary" class="save-button" icon="circle-tick" [disabled]="!canUpdate()" (click)="onSave($event)" i18n>Save</my-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!isUpdateAllowed()) {
|
||||
|
|
|
@ -25,10 +25,16 @@
|
|||
@include rfs(1.5rem, padding);
|
||||
}
|
||||
|
||||
.save-button {
|
||||
.buttons {
|
||||
@include margin-left(auto);
|
||||
}
|
||||
|
||||
.pre-config {
|
||||
display: inline-block;
|
||||
|
||||
@include margin-right(0.5rem);
|
||||
}
|
||||
|
||||
h2 {
|
||||
flex-shrink: 1;
|
||||
color: pvar(--fg-350);
|
||||
|
@ -48,7 +54,7 @@ h2 {
|
|||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.save-button,
|
||||
.buttons,
|
||||
h2 {
|
||||
@include margin-left(0);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { RouterModule } from '@angular/router'
|
|||
import { ScreenService, ServerService } from '@app/core'
|
||||
import { HeaderService } from '@app/header/header.service'
|
||||
import { FormReactiveErrors, FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { PeertubeModalService } from '@app/shared/shared-main/peertube-modal/peertube-modal.service'
|
||||
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.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 headerService = inject(HeaderService)
|
||||
private screenService = inject(ScreenService)
|
||||
private peertubeModal = inject(PeertubeModalService)
|
||||
|
||||
readonly title = input.required<string>()
|
||||
readonly form = input.required<FormGroup>()
|
||||
|
@ -59,6 +61,10 @@ export class AdminSaveBarComponent implements OnInit, OnDestroy {
|
|||
return this.formReactiveService.grabAllErrors(this.formErrors())
|
||||
}
|
||||
|
||||
openConfigWizard () {
|
||||
this.peertubeModal.openAdminConfigWizardSubject.next({ showWelcome: false })
|
||||
}
|
||||
|
||||
onSave (event: Event) {
|
||||
this.displayFormErrors = false
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { NgClass, NgFor, NgIf, NgTemplateOutlet } from '@angular/common'
|
|||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
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 {
|
||||
USER_CHANNEL_NAME_VALIDATOR,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { AuthService, ScreenService, ServerService, User } from '@app/core'
|
||||
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 { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
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 {
|
||||
USER_EMAIL_VALIDATOR,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { NgFor, NgIf } from '@angular/common'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
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 { PluginService } from '@app/core/plugins/plugin.service'
|
||||
import { compareSemVer } from '@peertube/peertube-core-utils'
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { NgFor, NgIf } from '@angular/common'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
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 { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
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 { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
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 { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgIf, NgFor } from '@angular/common'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, inject, input } from '@angular/core'
|
||||
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'
|
||||
|
||||
@Component({
|
||||
|
|
|
@ -21,11 +21,11 @@ import { WatchedWordsListService } from '@app/shared/standalone-watched-words/wa
|
|||
import { AdminModerationComponent } from './admin-moderation.component'
|
||||
import { AdminOverviewComponent } from './admin-overview.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 { AdminRegistrationService } from './moderation/registration-list'
|
||||
import { overviewRoutes, VideoAdminService } from './overview'
|
||||
import { PluginApiService } from './plugins/shared/plugin-api.service'
|
||||
import { PluginApiService } from '../shared/shared-admin/plugin-api.service'
|
||||
|
||||
const commonConfig = {
|
||||
path: '',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { SelectOptionsItem } from '../../../types/select-options-item.model'
|
||||
|
||||
export function getVideoQuotaOptions (): SelectOptionsItem[] {
|
||||
export function getVideoQuotaOptions (): SelectOptionsItem<number>[] {
|
||||
return [
|
||||
{ id: -1, label: $localize`Unlimited` },
|
||||
{ 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 [
|
||||
{ id: -1, label: $localize`Unlimited` },
|
||||
{ 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'
|
||||
|
||||
@Component({
|
||||
selector: 'my-custom-stepper',
|
||||
templateUrl: './custom-stepper.component.html',
|
||||
styleUrls: [ './custom-stepper.component.scss' ],
|
||||
providers: [ { provide: CdkStepper, useExisting: CustomStepperComponent } ],
|
||||
selector: 'my-register-stepper',
|
||||
templateUrl: './register-stepper.component.html',
|
||||
styleUrls: [ './register-stepper.component.scss' ],
|
||||
providers: [ { provide: CdkStepper, useExisting: RegisterStepperComponent } ],
|
||||
imports: [ NgIf, NgFor, NgClass, GlobalIconComponent, NgTemplateOutlet ]
|
||||
})
|
||||
export class CustomStepperComponent extends CdkStepper {
|
||||
|
||||
export class RegisterStepperComponent extends CdkStepper {
|
||||
onClick (index: number): void {
|
||||
this.selectedIndex = index
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<ng-container *ngIf="!signupDisabled">
|
||||
<div class="register-content">
|
||||
<my-custom-stepper linear>
|
||||
<my-register-stepper linear>
|
||||
|
||||
<cdk-step i18n-label label="About" [editable]="!signupSuccess">
|
||||
<my-signup-step-title mascotImageName="about">
|
||||
|
@ -119,7 +119,7 @@
|
|||
<button class="peertube-button-big secondary-button" cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button>
|
||||
</div>
|
||||
</cdk-step>
|
||||
</my-custom-stepper>
|
||||
</my-register-stepper>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import { SignupLabelComponent } from '../../shared/shared-main/users/signup-labe
|
|||
import { SignupStepTitleComponent } from '../shared/signup-step-title.component'
|
||||
import { SignupSuccessBeforeEmailComponent } from '../shared/signup-success-before-email.component'
|
||||
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 { RegisterStepChannelComponent } from './steps/register-step-channel.component'
|
||||
import { RegisterStepTermsComponent } from './steps/register-step-terms.component'
|
||||
|
@ -26,7 +26,7 @@ import { RegisterStepUserComponent } from './steps/register-step-user.component'
|
|||
imports: [
|
||||
NgIf,
|
||||
SignupLabelComponent,
|
||||
CustomStepperComponent,
|
||||
RegisterStepperComponent,
|
||||
CdkStep,
|
||||
SignupStepTitleComponent,
|
||||
RegisterStepAboutComponent,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_form-mixins' as *;
|
||||
@use "_variables" as *;
|
||||
@use "_css-variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
|
||||
$width-size: 275px;
|
||||
|
||||
|
@ -13,12 +14,12 @@ $width-size: 275px;
|
|||
}
|
||||
|
||||
.first-step-block {
|
||||
--input-bg: #{pvar(--bg-secondary-500)};
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
@include define-input-css-variables-in-modal;
|
||||
|
||||
.upload-icon {
|
||||
width: 90px;
|
||||
margin-bottom: 25px;
|
||||
|
@ -45,7 +46,7 @@ $width-size: 275px;
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
input[type="text"] {
|
||||
display: block;
|
||||
|
||||
@include peertube-input-text($width-size);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
|
||||
.caption-raw-textarea,
|
||||
.segments {
|
||||
|
@ -18,7 +18,7 @@
|
|||
|
||||
&.active,
|
||||
&:hover {
|
||||
background: pvar(--bg-secondary-300);
|
||||
background: pvar(--bg-secondary-400);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -55,8 +55,10 @@
|
|||
|
||||
@defer (when isUserLoggedIn()) {
|
||||
<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>
|
||||
}
|
||||
|
||||
|
|
|
@ -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 { 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 { Event, GuardsCheckStart, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouterOutlet } from '@angular/router'
|
||||
import { ActivatedRoute, Event, GuardsCheckStart, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouterOutlet } from '@angular/router'
|
||||
import {
|
||||
AuthService,
|
||||
Hotkey,
|
||||
|
@ -19,7 +17,7 @@ import {
|
|||
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||
import { PluginService } from '@app/core/plugins/plugin.service'
|
||||
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 { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
|
||||
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 { SharedModule } from 'primeng/api'
|
||||
import { ToastModule } from 'primeng/toast'
|
||||
import { forkJoin } from 'rxjs'
|
||||
import { filter, first, map } from 'rxjs/operators'
|
||||
import { MenuService } from './core/menu/menu.service'
|
||||
import { HeaderComponent } from './header/header.component'
|
||||
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 { ConfirmComponent } from './modal/confirm.component'
|
||||
import { GlobalIconComponent, GlobalIconName } from './shared/shared-icons/global-icon.component'
|
||||
|
||||
import { InstanceService } from './shared/shared-main/instance/instance.service'
|
||||
import { PeertubeModalService } from './shared/shared-main/peertube-modal/peertube-modal.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
|
@ -57,9 +57,9 @@ import { InstanceService } from './shared/shared-main/instance/instance.service'
|
|||
ToastModule,
|
||||
SharedModule,
|
||||
AccountSetupWarningModalComponent,
|
||||
AdminWelcomeModalComponent,
|
||||
InstanceConfigWarningModalComponent,
|
||||
CustomModalComponent
|
||||
CustomModalComponent,
|
||||
AdminConfigWizardModalComponent
|
||||
]
|
||||
})
|
||||
export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
@ -82,12 +82,15 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
private loadingBar = inject(LoadingBarService)
|
||||
private scrollService = inject(ScrollService)
|
||||
private userLocalStorage = inject(UserLocalStorageService)
|
||||
private peertubeModal = inject(PeertubeModalService)
|
||||
private route = inject(ActivatedRoute)
|
||||
|
||||
menu = inject(MenuService)
|
||||
|
||||
private static LS_BROADCAST_MESSAGE = 'app-broadcast-message-dismissed'
|
||||
|
||||
readonly accountSetupWarningModal = viewChild<AccountSetupWarningModalComponent>('accountSetupWarningModal')
|
||||
readonly adminWelcomeModal = viewChild<AdminWelcomeModalComponent>('adminWelcomeModal')
|
||||
readonly adminConfigWizardModal = viewChild<AdminConfigWizardModalComponent>('adminConfigWizardModal')
|
||||
readonly instanceConfigWarningModal = viewChild<InstanceConfigWarningModalComponent>('instanceConfigWarningModal')
|
||||
readonly customModal = viewChild<CustomModalComponent>('customModal')
|
||||
|
||||
|
@ -154,6 +157,13 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
this.peertubeModal.openAdminConfigWizardSubject.subscribe(({ showWelcome }) => {
|
||||
const adminWelcomeModal = this.adminConfigWizardModal()
|
||||
if (!adminWelcomeModal) return
|
||||
|
||||
adminWelcomeModal.show({ showWelcome })
|
||||
})
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
|
@ -171,6 +181,10 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
return this.authService.isLoggedIn()
|
||||
}
|
||||
|
||||
isUserAdmin () {
|
||||
return this.isUserLoggedIn() && this.authService.getUser().role.id === UserRole.ADMINISTRATOR
|
||||
}
|
||||
|
||||
hideBroadcastMessage () {
|
||||
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) {
|
||||
const adminWelcomeModal = this.adminWelcomeModal()
|
||||
const adminWelcomeModal = this.adminConfigWizardModal()
|
||||
if (!adminWelcomeModal) return
|
||||
|
||||
if (adminWelcomeModal.shouldOpen(user)) {
|
||||
return adminWelcomeModal.show()
|
||||
if (adminWelcomeModal.shouldAutoOpen(user)) {
|
||||
return adminWelcomeModal.show({ showWelcome: true })
|
||||
}
|
||||
|
||||
const instanceConfigWarningModal = this.instanceConfigWarningModal()
|
||||
if (!instanceConfigWarningModal) return
|
||||
if (!instanceConfigWarningModal.shouldOpenByUser(user)) return
|
||||
if (!instanceConfigWarningModal.canBeOpenByUser(user)) return
|
||||
|
||||
forkJoin([
|
||||
this.serverService.getConfig().pipe(first()),
|
||||
this.instanceService.getAbout().pipe(first())
|
||||
]).subscribe(([ config, about ]) => {
|
||||
const instanceConfigWarningModalValue = this.instanceConfigWarningModal()
|
||||
if (instanceConfigWarningModalValue.shouldOpen(config, about)) {
|
||||
if (instanceConfigWarningModalValue.shouldAutoOpen(config, about)) {
|
||||
instanceConfigWarningModalValue.show(about)
|
||||
}
|
||||
})
|
||||
|
@ -327,7 +341,7 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
const accountSetupWarningModal = this.accountSetupWarningModal()
|
||||
if (!accountSetupWarningModal) return
|
||||
|
||||
if (accountSetupWarningModal.shouldOpen(user)) {
|
||||
if (accountSetupWarningModal.shouldAutoOpen(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
|
||||
} = {}) {
|
||||
const { allowImages = false } = options
|
||||
|
@ -89,6 +93,7 @@ export class HtmlRendererService {
|
|||
const additionalTags = allowImages
|
||||
? [ 'img' ]
|
||||
: []
|
||||
|
||||
const additionalAttributes = allowImages
|
||||
? [ 'src', 'alt' ]
|
||||
: []
|
||||
|
|
|
@ -149,10 +149,10 @@ export class MarkdownService {
|
|||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -66,6 +66,7 @@ export class ServerService {
|
|||
|
||||
resetConfig () {
|
||||
this.configLoaded = false
|
||||
this.configObservable = undefined
|
||||
|
||||
// Notify config update
|
||||
return this.getConfig({ isReset: true })
|
||||
|
|
|
@ -195,13 +195,13 @@ export default {
|
|||
},
|
||||
list: {
|
||||
option: {
|
||||
focusBackground: 'var(--bg-secondary-500)',
|
||||
selectedBackground: '{highlight.background}',
|
||||
focusBackground: 'var(--bg-secondary-450)',
|
||||
selectedBackground: 'var(--bg-secondary-500)',
|
||||
selectedFocusBackground: 'var(--bg-secondary-500)',
|
||||
color: '{text.color}',
|
||||
focusColor: '{text.hover.color}',
|
||||
selectedColor: '{highlight.color}',
|
||||
selectedFocusColor: '{highlight.focus.color}',
|
||||
focusColor: '{text.color}',
|
||||
selectedColor: '{text.color}',
|
||||
selectedFocusColor: '{text.color}',
|
||||
icon: {
|
||||
color: '{surface.400}',
|
||||
focusColor: '{surface.500}'
|
||||
|
|
|
@ -10,6 +10,7 @@ import { PluginService } from '../plugins/plugin.service'
|
|||
import { ServerService } from '../server'
|
||||
import { UserService } from '../users/user.service'
|
||||
import { LocalStorageService } from '../wrappers/storage.service'
|
||||
import { formatHEX, parse } from 'color-bits'
|
||||
|
||||
@Injectable()
|
||||
export class ThemeService {
|
||||
|
@ -216,4 +217,31 @@ export class ThemeService {
|
|||
private getTheme (name: string) {
|
||||
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
|
||||
}
|
||||
|
||||
shouldOpen (user: User) {
|
||||
shouldAutoOpen (user: User) {
|
||||
if (this.modalService.hasOpenModals()) return false
|
||||
if (user.noAccountSetupWarningModal === 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.cancelButtonText = cancelButtonText || $localize`Cancel`
|
||||
|
||||
this.html.toSimpleSafeHtml(message)
|
||||
this.html.toSimpleSafeHtmlWithLinks(message)
|
||||
.then(html => {
|
||||
this.message = html
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ export class InstanceConfigWarningModalComponent implements OnInit {
|
|||
this.created.emit()
|
||||
}
|
||||
|
||||
shouldOpenByUser (user: User) {
|
||||
canBeOpenByUser (user: User) {
|
||||
if (this.modalService.hasOpenModals()) return false
|
||||
if (user.noInstanceConfigWarningModal === 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
|
||||
}
|
||||
|
||||
shouldOpen (serverConfig: ServerConfig, about: About) {
|
||||
shouldAutoOpen (serverConfig: ServerConfig, about: About) {
|
||||
if (!serverConfig.signup.allowed) return false
|
||||
|
||||
return serverConfig.instance.name.toLowerCase() === 'peertube' ||
|
||||
|
|
|
@ -8,8 +8,8 @@ import { CustomConfig } from '@peertube/peertube-models'
|
|||
import { DeepPartial } from '@peertube/peertube-typescript-utils'
|
||||
import merge from 'lodash-es/merge'
|
||||
import { catchError, map, switchMap } from 'rxjs/operators'
|
||||
import { environment } from '../../../../environments/environment'
|
||||
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
|
||||
import { environment } from '../../../environments/environment'
|
||||
import { SelectOptionsItem } from '../../../types/select-options-item.model'
|
||||
|
||||
export type FormResolutions = {
|
||||
'0p': FormControl<boolean>
|
||||
|
@ -111,6 +111,11 @@ export class AdminConfigService {
|
|||
)
|
||||
}
|
||||
|
||||
getCustomConfigReloadedObs () {
|
||||
return this.serverService.configReloaded
|
||||
.pipe(switchMap(() => this.getCustomConfig()))
|
||||
}
|
||||
|
||||
saveAndUpdateCurrent (options: {
|
||||
currentConfig: CustomConfig
|
||||
form: FormGroup
|
|
@ -13,7 +13,7 @@ import {
|
|||
RegisteredServerSettings,
|
||||
ResultList
|
||||
} from '@peertube/peertube-models'
|
||||
import { environment } from '../../../../environments/environment'
|
||||
import { environment } from '../../../environments/environment'
|
||||
|
||||
@Injectable()
|
||||
export class PluginApiService {
|
|
@ -1,7 +1,7 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_button-mixins' as *;
|
||||
@use '_form-mixins' as *;
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_button-mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
|
||||
input {
|
||||
@include peertube-input-text(auto);
|
||||
|
@ -9,11 +9,11 @@ input {
|
|||
|
||||
.btn,
|
||||
my-copy-button ::ng-deep .btn {
|
||||
background-color: pvar(--bg-secondary-400);
|
||||
background-color: pvar(--input-bg);
|
||||
border-left: 1px solid pvar(--bg);
|
||||
|
||||
&: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({
|
||||
selector: 'my-global-icon',
|
||||
template: '',
|
||||
styleUrls: [ './global-icon.component.scss' ],
|
||||
styleUrls: [ './common-icon.component.scss' ],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_button-mixins' as *;
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_button-mixins" as *;
|
||||
|
||||
@mixin reduced-padding {
|
||||
padding: pvar(--input-y-padding) calc(#{pvar(--input-x-padding)} / 2) !important;
|
||||
|
@ -25,7 +25,7 @@
|
|||
vertical-align: middle;
|
||||
margin-top: -1px;
|
||||
|
||||
@include margin-right(3px);
|
||||
@include margin-right(0.5rem);
|
||||
}
|
||||
|
||||
&:not(.rounded-icon-button) {
|
||||
|
|
|
@ -4,4 +4,5 @@ import { Subject } from 'rxjs'
|
|||
@Injectable({ providedIn: 'root' })
|
||||
export class PeertubeModalService {
|
||||
openQuickSettingsSubject = new Subject<void>()
|
||||
openAdminConfigWizardSubject = new Subject<{ showWelcome: boolean }>()
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use "_variables" as *;
|
||||
@use "_css-variables" as *;
|
||||
@use "_mixins" as *;
|
||||
|
||||
$filters-background: pvar(--bg-secondary-400);
|
||||
|
||||
|
@ -51,17 +52,15 @@ $filters-background: pvar(--bg-secondary-400);
|
|||
}
|
||||
|
||||
.filters {
|
||||
--input-bg: #{pvar(--input-bg-in-secondary)};
|
||||
--input-border-color: #{pvar(--input-bg-in-secondary)};
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background-color: $filters-background;
|
||||
border-radius: 14px;
|
||||
|
||||
@include define-input-css-variables-in-secondary;
|
||||
@include rfs(1.5rem, padding);
|
||||
|
||||
input[type=radio] + label {
|
||||
input[type="radio"] + label {
|
||||
font-weight: $font-regular;
|
||||
}
|
||||
|
||||
|
@ -78,7 +77,7 @@ $filters-background: pvar(--bg-secondary-400);
|
|||
|
||||
.active-filters {
|
||||
.active-filter:not(:last-child)::after {
|
||||
content: '•';
|
||||
content: "•";
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
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) {
|
||||
if (iteration > 100) {
|
||||
logger.error('Too many iteration when checking color palette injection. The theme may be missing the --is-dark CSS variable')
|
||||
|
||||
this.injectColorPalette()
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.canInjectCoreColorPalette()) {
|
||||
} else if (!this.canInjectCoreColorPalette()) {
|
||||
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 "_css-variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_button-mixins" as *;
|
||||
@import "./_bootstrap-variables";
|
||||
|
@ -117,8 +118,6 @@ body {
|
|||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 3px 15px;
|
||||
|
||||
&.active {
|
||||
color: pvar(--on-primary) !important;
|
||||
background-color: pvar(--primary);
|
||||
|
@ -180,8 +179,9 @@ body {
|
|||
}
|
||||
|
||||
.modal {
|
||||
@include define-input-css-variables-in-modal;
|
||||
|
||||
.modal-content {
|
||||
background-color: pvar(--bg);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
|
@ -251,7 +251,7 @@ body {
|
|||
@include disable-default-a-behaviour;
|
||||
|
||||
&.active {
|
||||
background-color: pvar(--bg) !important;
|
||||
background-color: transparent !important;
|
||||
border-bottom-color: pvar(--border-primary);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
|
||||
$modal-footer-border-width: 0;
|
||||
$modal-md: 600px;
|
||||
$modal-lg: 900px;
|
||||
$modal-content-border-radius: 14px;
|
||||
$modal-content-bg: pvar(--bg-secondary-350);
|
||||
|
||||
$grid-breakpoints: (
|
||||
// CLASSIC BREAKPOINTS GROUP
|
||||
|
|
|
@ -27,7 +27,8 @@
|
|||
--input-fg: var(--inputForegroundColor, #{pvar(--fg)});
|
||||
|
||||
--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-bg: #FEBBB2;
|
||||
|
@ -48,6 +49,8 @@
|
|||
--textarea-x-padding: 15px;
|
||||
--textarea-fg: var(--textareaForegroundColor, #{pvar(--input-fg)});
|
||||
--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-bg: var(--supportButtonBackgroundColor, transparent);
|
||||
|
@ -158,3 +161,22 @@
|
|||
--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-600: var(--input-bg-600),
|
||||
--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-bg: var(--input-danger-bg),
|
||||
--input-placeholder: var(--input-placeholder),
|
||||
|
@ -114,6 +115,8 @@ $variables: (
|
|||
--textarea-y-padding: var(--textarea-y-padding),
|
||||
--textarea-fg: var(--textarea-fg),
|
||||
--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-fg: var(--support-btn-fg),
|
||||
--support-btn-heart-bg: var(--support-btn-heart-bg),
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_css-variables' as *;
|
||||
@use '../player/build/peertube-player';
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_css-variables" as *;
|
||||
@use "../player/build/peertube-player";
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
:root {
|
||||
@include define-css-variables();
|
||||
}
|
||||
|
||||
& {
|
||||
body {
|
||||
font-family: $main-fonts;
|
||||
font-weight: $font-regular;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
video {
|
||||
width: 99%;
|
||||
|
@ -66,11 +66,11 @@ body {
|
|||
}
|
||||
|
||||
#error-details {
|
||||
margin-top: 30px
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
#error-details-content {
|
||||
margin-top: 10px
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#placeholder-preview {
|
||||
|
@ -141,4 +141,3 @@ body {
|
|||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ export class PeerTubeTheme {
|
|||
|
||||
this.themeManager.loadThemeStyle(themeName)
|
||||
|
||||
this.themeManager.injectCoreColorPalette()
|
||||
this.themeManager.injectColorPalette({ config: config.theme, currentTheme: themeName })
|
||||
}
|
||||
|
||||
loadThemePlugins (config: HTMLServerConfig) {
|
||||
|
|
|
@ -121,39 +121,6 @@ email:
|
|||
subject:
|
||||
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
|
||||
storage:
|
||||
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
|
||||
# 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:
|
||||
enabled: false
|
||||
|
||||
|
@ -880,6 +846,7 @@ import:
|
|||
# Max number of videos to import when the user asks for full sync
|
||||
full_sync_videos_limit: 1000
|
||||
|
||||
# Add ability for your users to import a PeerTube archive file to automatically create videos, channels, captions, etc
|
||||
users:
|
||||
# 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
|
||||
|
@ -1136,3 +1103,36 @@ client:
|
|||
storyboards:
|
||||
# Generate storyboards of local videos using ffmpeg so users can see the video preview in the player while scrubbing the video
|
||||
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:
|
||||
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
|
||||
storage:
|
||||
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
|
||||
# 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:
|
||||
enabled: false
|
||||
|
||||
|
@ -890,6 +856,7 @@ import:
|
|||
# Max number of videos to import when the user asks for full sync
|
||||
full_sync_videos_limit: 1000
|
||||
|
||||
# Add ability for your users to import a PeerTube archive file to automatically create videos, channels, captions, etc
|
||||
users:
|
||||
# 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
|
||||
|
@ -1050,6 +1017,25 @@ followings:
|
|||
theme:
|
||||
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:
|
||||
enabled: false
|
||||
message: '' # Support markdown
|
||||
|
@ -1084,6 +1070,7 @@ search:
|
|||
|
||||
# PeerTube client/interface configuration
|
||||
client:
|
||||
|
||||
videos:
|
||||
miniature:
|
||||
# By default PeerTube client displays author username
|
||||
|
@ -1126,3 +1113,36 @@ client:
|
|||
storyboards:
|
||||
# Generate storyboards of local videos using ffmpeg so users can see the video preview in the player while scrubbing the video
|
||||
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 { BroadcastMessageLevel } from './broadcast-message-level.type.js'
|
||||
|
||||
|
@ -320,4 +321,27 @@ export interface CustomConfig {
|
|||
storyboards: {
|
||||
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 */
|
||||
|
||||
import { ActorImageType, CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { ActorImageType, CustomConfig, HttpStatusCode, VideoCommentPolicy, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
|
@ -188,7 +188,19 @@ function buildNewCustomConfig (server: PeerTubeServer): CustomConfig {
|
|||
}
|
||||
},
|
||||
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: {
|
||||
twitter: {
|
||||
|
@ -410,6 +422,25 @@ function buildNewCustomConfig (server: PeerTubeServer): CustomConfig {
|
|||
exportExpiration: 43,
|
||||
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: {
|
||||
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
|
||||
manualApproval:
|
||||
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:
|
||||
properties:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue