diff --git a/client/src/app/+admin/config/config.routes.ts b/client/src/app/+admin/config/config.routes.ts index 755148280..32431338a 100644 --- a/client/src/app/+admin/config/config.routes.ts +++ b/client/src/app/+admin/config/config.routes.ts @@ -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 = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => { return inject(AdminConfigService).getCustomConfig() diff --git a/client/src/app/+admin/config/pages/admin-config-advanced.component.html b/client/src/app/+admin/config/pages/admin-config-advanced.component.html index 6cc09fd7a..d26e371fe 100644 --- a/client/src/app/+admin/config/pages/admin-config-advanced.component.html +++ b/client/src/app/+admin/config/pages/admin-config-advanced.component.html @@ -1,6 +1,6 @@ - +
@@ -109,4 +109,4 @@
- +
diff --git a/client/src/app/+admin/config/pages/admin-config-advanced.component.ts b/client/src/app/+admin/config/pages/admin-config-advanced.component.ts index d58996a93..877563c8e 100644 --- a/client/src/app/+admin/config/pages/admin-config-advanced.component.ts +++ b/client/src/app/+admin/config/pages/admin-config-advanced.component.ts @@ -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
= {} 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 () { diff --git a/client/src/app/+admin/config/pages/admin-config-customization.component.html b/client/src/app/+admin/config/pages/admin-config-customization.component.html index b12ab0cde..9747e6f60 100644 --- a/client/src/app/+admin/config/pages/admin-config-customization.component.html +++ b/client/src/app/+admin/config/pages/admin-config-customization.component.html @@ -1,178 +1,180 @@ -
-
-

APPEARANCE

-
+ +
+
+

APPEARANCE

+
-
- -
- +
+ +
+ - -
-
+ +
+ - - - -
- -
+ + + +
+ +
+
-
-
-
- -
-
-

CUSTOMIZATION

- -
- Use plugins & themes for more involved changes
-
- -
UI customization only applies if the user is using the default platform theme.
-
+
+
+

CUSTOMIZATION

- @if (getCurrentThemeName() !== getDefaultThemeName()) { - - +
+ Use plugins & themes for more involved changes +
+
-
You can't preview the changes because you aren't using your platform's default theme.
-
Current theme: {{ getCurrentThemeLabel() }}
-
Platform theme: {{ getDefaultThemeLabel() }}.
- - } @else { - - +
+ +
UI customization only applies if the user is using the default platform theme.
+
-
You can preview your UI customization but don't forget to save your changes once you are happy with the results.
- - } + @if (getCurrentThemeName() !== getDefaultThemeName()) { + + -
- - @for (field of customizationFormFields; track field.name) { -
- +
You can't preview the changes because you aren't using your platform's default theme.
+
Current theme: {{ getCurrentThemeLabel() }}
+
Platform theme: {{ getDefaultThemeLabel() }}.
+ + } @else { + + - +
You can preview your UI customization but don't forget to save your changes once you are happy with the results.
+
+ } -
{{ field.description }}
+
+ + @for (field of customizationFormFields; track field.name) { +
+ - @if (field.type === 'color') { - - } @else if (field.type === 'pixels' && isCustomizationFieldNumber(field.name)) { -
+ + +
{{ field.description }}
+ + @if (field.type === 'color') { + + } @else if (field.type === 'pixels' && isCustomizationFieldNumber(field.name)) { +
+ + pixels +
+ } @else { - pixels -
- } @else { - - } + > + } +
+ } +
+
+
+
+ +
+
+
+ +

Advanced

+
+ Advanced modifications to your PeerTube platform if creating a plugin or a theme is overkill. +
+
+ +
+ + +
+ + + +

Write JavaScript code directly. Example:

+
console.log('my instance is amazing');
+
+
+ + + +
- } + +
+ + + + +

Write CSS code directly. Example:

+
+  #custom-css {{ '{' }}
+  color: red;
+  {{ '}' }}
+  
+

Prepend with #custom-css to override styles. Example:

+
+  #custom-css .logged-in-email {{ '{' }}
+  color: red;
+  {{ '}' }}
+  
+
+
+ + + +
+
-
- -
-
-
- -

Advanced

-
- Advanced modifications to your PeerTube platform if creating a plugin or a theme is overkill. -
-
- -
- - -
- - - -

Write JavaScript code directly. Example:

-
console.log('my instance is amazing');
-
-
- - - - -
- -
- - - - -

Write CSS code directly. Example:

-
-#custom-css {{ '{' }}
-color: red;
-{{ '}' }}
-
-

Prepend with #custom-css to override styles. Example:

-
-#custom-css .logged-in-email {{ '{' }}
-color: red;
-{{ '}' }}
-
-
-
- - - -
-
-
-
-
+ diff --git a/client/src/app/+admin/config/pages/admin-config-customization.component.ts b/client/src/app/+admin/config/pages/admin-config-customization.component.ts index 30e0ca272..6e00a40f3 100644 --- a/client/src/app/+admin/config/pages/admin-config-customization.component.ts +++ b/client/src/app/+admin/config/pages/admin-config-customization.component.ts @@ -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() private customConfig: CustomConfig + private customConfigSub: Subscription + private readonly formFieldsObject: Record = { 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
= { - ...this.customConfig, - - theme: { - default: this.customConfig.theme.default, - customization: this.getDefaultCustomization() - } - } - const { form, formErrors, validationMessages - } = this.formReactiveService.buildForm(obj, defaultValues) + } = this.formReactiveService.buildForm(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 { + 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) { diff --git a/client/src/app/+admin/config/pages/admin-config-general.component.html b/client/src/app/+admin/config/pages/admin-config-general.component.html index ed32e1fd7..5d6103e5d 100644 --- a/client/src/app/+admin/config/pages/admin-config-general.component.html +++ b/client/src/app/+admin/config/pages/admin-config-general.component.html @@ -1,6 +1,6 @@ - +

BEHAVIOR

@@ -666,4 +666,4 @@
-
+
diff --git a/client/src/app/+admin/config/pages/admin-config-general.component.ts b/client/src/app/+admin/config/pages/admin-config-general.component.ts index 1d47a6343..78cfaf216 100644 --- a/client/src/app/+admin/config/pages/admin-config-general.component.ts +++ b/client/src/app/+admin/config/pages/admin-config-general.component.ts @@ -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 () { diff --git a/client/src/app/+admin/config/pages/admin-config-homepage.component.html b/client/src/app/+admin/config/pages/admin-config-homepage.component.html index 0b46f05e0..8a5b81f3f 100644 --- a/client/src/app/+admin/config/pages/admin-config-homepage.component.html +++ b/client/src/app/+admin/config/pages/admin-config-homepage.component.html @@ -1,6 +1,6 @@ -
+

HOMEPAGE

@@ -25,4 +25,4 @@
-
+ diff --git a/client/src/app/+admin/config/pages/admin-config-information.component.html b/client/src/app/+admin/config/pages/admin-config-information.component.html index ff39f56ad..de287e2f7 100644 --- a/client/src/app/+admin/config/pages/admin-config-information.component.html +++ b/client/src/app/+admin/config/pages/admin-config-information.component.html @@ -1,6 +1,6 @@ - +
@@ -341,4 +341,4 @@ - + diff --git a/client/src/app/+admin/config/pages/admin-config-information.component.ts b/client/src/app/+admin/config/pages/admin-config-information.component.ts index b0f35be94..daab24699 100644 --- a/client/src/app/+admin/config/pages/admin-config-information.component.ts +++ b/client/src/app/+admin/config/pages/admin-config-information.component.ts @@ -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 () { diff --git a/client/src/app/+admin/config/pages/admin-config-live.component.html b/client/src/app/+admin/config/pages/admin-config-live.component.html index 41c915328..2bd0b15af 100644 --- a/client/src/app/+admin/config/pages/admin-config-live.component.html +++ b/client/src/app/+admin/config/pages/admin-config-live.component.html @@ -1,6 +1,6 @@ - +
@@ -212,4 +212,4 @@
- +
diff --git a/client/src/app/+admin/config/pages/admin-config-live.component.ts b/client/src/app/+admin/config/pages/admin-config-live.component.ts index bdf3271fa..5bfc83115 100644 --- a/client/src/app/+admin/config/pages/admin-config-live.component.ts +++ b/client/src/app/+admin/config/pages/admin-config-live.component.ts @@ -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 () { diff --git a/client/src/app/+admin/config/pages/admin-config-vod.component.html b/client/src/app/+admin/config/pages/admin-config-vod.component.html index b6a54bbea..791b3da45 100644 --- a/client/src/app/+admin/config/pages/admin-config-vod.component.html +++ b/client/src/app/+admin/config/pages/admin-config-vod.component.html @@ -1,6 +1,6 @@ - +
@@ -281,4 +281,4 @@
- +
diff --git a/client/src/app/+admin/config/pages/admin-config-vod.component.ts b/client/src/app/+admin/config/pages/admin-config-vod.component.ts index 82d1a04bd..e817dad67 100644 --- a/client/src/app/+admin/config/pages/admin-config-vod.component.ts +++ b/client/src/app/+admin/config/pages/admin-config-vod.component.ts @@ -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 () { diff --git a/client/src/app/+admin/config/shared/admin-save-bar.component.html b/client/src/app/+admin/config/shared/admin-save-bar.component.html index f24cd8642..cdee01b1a 100644 --- a/client/src/app/+admin/config/shared/admin-save-bar.component.html +++ b/client/src/app/+admin/config/shared/admin-save-bar.component.html @@ -2,10 +2,11 @@

{{ title() }}

- Save +
+ Open config wizard + + Save +
@if (!isUpdateAllowed()) { diff --git a/client/src/app/+admin/config/shared/admin-save-bar.component.scss b/client/src/app/+admin/config/shared/admin-save-bar.component.scss index dc171e4d8..546c3cb77 100644 --- a/client/src/app/+admin/config/shared/admin-save-bar.component.scss +++ b/client/src/app/+admin/config/shared/admin-save-bar.component.scss @@ -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); } diff --git a/client/src/app/+admin/config/shared/admin-save-bar.component.ts b/client/src/app/+admin/config/shared/admin-save-bar.component.ts index 4c6d6341b..7d2e5754e 100644 --- a/client/src/app/+admin/config/shared/admin-save-bar.component.ts +++ b/client/src/app/+admin/config/shared/admin-save-bar.component.ts @@ -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() readonly form = input.required() @@ -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 diff --git a/client/src/app/+admin/overview/users/user-edit/user-create.component.ts b/client/src/app/+admin/overview/users/user-edit/user-create.component.ts index 8967f5657..247ad2ea7 100644 --- a/client/src/app/+admin/overview/users/user-edit/user-create.component.ts +++ b/client/src/app/+admin/overview/users/user-edit/user-create.component.ts @@ -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, diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.ts b/client/src/app/+admin/overview/users/user-edit/user-edit.ts index 5f506f396..08cc413f6 100644 --- a/client/src/app/+admin/overview/users/user-edit/user-edit.ts +++ b/client/src/app/+admin/overview/users/user-edit/user-edit.ts @@ -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' diff --git a/client/src/app/+admin/overview/users/user-edit/user-update.component.ts b/client/src/app/+admin/overview/users/user-edit/user-update.component.ts index bec1fa8ff..a34ed7334 100644 --- a/client/src/app/+admin/overview/users/user-edit/user-update.component.ts +++ b/client/src/app/+admin/overview/users/user-edit/user-update.component.ts @@ -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, diff --git a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts index 26b60de85..8cb0d84db 100644 --- a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts +++ b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts @@ -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' diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts index fddefc2d1..b21038a60 100644 --- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts +++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts @@ -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' diff --git a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts index ef2fa4dce..fb75b7c43 100644 --- a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts +++ b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts @@ -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' diff --git a/client/src/app/+admin/plugins/shared/plugin-card.component.ts b/client/src/app/+admin/plugins/shared/plugin-card.component.ts index 1041870c1..c27b2a256 100644 --- a/client/src/app/+admin/plugins/shared/plugin-card.component.ts +++ b/client/src/app/+admin/plugins/shared/plugin-card.component.ts @@ -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({ diff --git a/client/src/app/+admin/routes.ts b/client/src/app/+admin/routes.ts index a09f4eb45..9e3f43915 100644 --- a/client/src/app/+admin/routes.ts +++ b/client/src/app/+admin/routes.ts @@ -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: '', diff --git a/client/src/app/+admin/shared/user-quota-options.ts b/client/src/app/+admin/shared/user-quota-options.ts index 4f79c35f1..6a88e331d 100644 --- a/client/src/app/+admin/shared/user-quota-options.ts +++ b/client/src/app/+admin/shared/user-quota-options.ts @@ -1,6 +1,6 @@ import { SelectOptionsItem } from '../../../types/select-options-item.model' -export function getVideoQuotaOptions (): SelectOptionsItem[] { +export function getVideoQuotaOptions (): SelectOptionsItem[] { 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[] { return [ { id: -1, label: $localize`Unlimited` }, { id: 0, label: $localize`None - no upload possible` }, diff --git a/client/src/app/+signup/+register/custom-stepper.component.html b/client/src/app/+signup/+register/register-stepper.component.html similarity index 100% rename from client/src/app/+signup/+register/custom-stepper.component.html rename to client/src/app/+signup/+register/register-stepper.component.html diff --git a/client/src/app/+signup/+register/custom-stepper.component.scss b/client/src/app/+signup/+register/register-stepper.component.scss similarity index 100% rename from client/src/app/+signup/+register/custom-stepper.component.scss rename to client/src/app/+signup/+register/register-stepper.component.scss diff --git a/client/src/app/+signup/+register/custom-stepper.component.ts b/client/src/app/+signup/+register/register-stepper.component.ts similarity index 66% rename from client/src/app/+signup/+register/custom-stepper.component.ts rename to client/src/app/+signup/+register/register-stepper.component.ts index 6864da775..b2a0df5fe 100644 --- a/client/src/app/+signup/+register/custom-stepper.component.ts +++ b/client/src/app/+signup/+register/register-stepper.component.ts @@ -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 } diff --git a/client/src/app/+signup/+register/register.component.html b/client/src/app/+signup/+register/register.component.html index 468ea25eb..e410c2442 100644 --- a/client/src/app/+signup/+register/register.component.html +++ b/client/src/app/+signup/+register/register.component.html @@ -6,7 +6,7 @@
- + @@ -119,7 +119,7 @@
- +
diff --git a/client/src/app/+signup/+register/register.component.ts b/client/src/app/+signup/+register/register.component.ts index a6fe825eb..0ba04d86c 100644 --- a/client/src/app/+signup/+register/register.component.ts +++ b/client/src/app/+signup/+register/register.component.ts @@ -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, diff --git a/client/src/app/+videos-publish-manage/+video-publish/shared/common-publish.scss b/client/src/app/+videos-publish-manage/+video-publish/shared/common-publish.scss index 440e6be3e..42dc8a4e1 100644 --- a/client/src/app/+videos-publish-manage/+video-publish/shared/common-publish.scss +++ b/client/src/app/+videos-publish-manage/+video-publish/shared/common-publish.scss @@ -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); diff --git a/client/src/app/+videos-publish-manage/shared-manage/captions/video-caption-edit-modal.component.scss b/client/src/app/+videos-publish-manage/shared-manage/captions/video-caption-edit-modal.component.scss index 285ed76d1..1fd0f94a6 100644 --- a/client/src/app/+videos-publish-manage/shared-manage/captions/video-caption-edit-modal.component.scss +++ b/client/src/app/+videos-publish-manage/shared-manage/captions/video-caption-edit-modal.component.scss @@ -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); } } diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index 1b0745323..4803364b7 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html @@ -55,8 +55,10 @@ @defer (when isUserLoggedIn()) { +} - +@defer (when isUserAdmin()) { + } diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 8e35a5f6f..e8cb6dae9 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -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('accountSetupWarningModal') - readonly adminWelcomeModal = viewChild('adminWelcomeModal') + readonly adminConfigWizardModal = viewChild('adminConfigWizardModal') readonly instanceConfigWarningModal = viewChild('instanceConfigWarningModal') readonly customModal = viewChild('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) } } diff --git a/client/src/app/core/renderer/html-renderer.service.ts b/client/src/app/core/renderer/html-renderer.service.ts index 251dda403..fc9306c47 100644 --- a/client/src/app/core/renderer/html-renderer.service.ts +++ b/client/src/app/core/renderer/html-renderer.service.ts @@ -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' ] : [] diff --git a/client/src/app/core/renderer/markdown.service.ts b/client/src/app/core/renderer/markdown.service.ts index da9033c21..ab23613dc 100644 --- a/client/src/app/core/renderer/markdown.service.ts +++ b/client/src/app/core/renderer/markdown.service.ts @@ -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 diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index aec1568b9..ffbaee98c 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts @@ -66,6 +66,7 @@ export class ServerService { resetConfig () { this.configLoaded = false + this.configObservable = undefined // Notify config update return this.getConfig({ isReset: true }) diff --git a/client/src/app/core/theme/primeng/base.ts b/client/src/app/core/theme/primeng/base.ts index 97ec00455..57f20cd7d 100644 --- a/client/src/app/core/theme/primeng/base.ts +++ b/client/src/app/core/theme/primeng/base.ts @@ -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}' diff --git a/client/src/app/core/theme/theme.service.ts b/client/src/app/core/theme/theme.service.ts index 444de815e..fa57a592f 100644 --- a/client/src/app/core/theme/theme.service.ts +++ b/client/src/app/core/theme/theme.service.ts @@ -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 + '' + } } diff --git a/client/src/app/modal/account-setup-warning-modal.component.ts b/client/src/app/modal/account-setup-warning-modal.component.ts index 10edceed5..ae1203d31 100644 --- a/client/src/app/modal/account-setup-warning-modal.component.ts +++ b/client/src/app/modal/account-setup-warning-modal.component.ts @@ -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 diff --git a/client/src/app/modal/admin-config-wizard/admin-config-wizard-modal.component.html b/client/src/app/modal/admin-config-wizard/admin-config-wizard-modal.component.html new file mode 100644 index 000000000..39209cc54 --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/admin-config-wizard-modal.component.html @@ -0,0 +1,40 @@ + + + diff --git a/client/src/app/modal/admin-config-wizard/admin-config-wizard-modal.component.scss b/client/src/app/modal/admin-config-wizard/admin-config-wizard-modal.component.scss new file mode 100644 index 000000000..0d93c3706 --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/admin-config-wizard-modal.component.scss @@ -0,0 +1,6 @@ +@use "_variables" as *; +@use "_mixins" as *; + +.modal-body { + padding: 2rem 3rem; +} diff --git a/client/src/app/modal/admin-config-wizard/admin-config-wizard-modal.component.ts b/client/src/app/modal/admin-config-wizard/admin-config-wizard-modal.component.ts new file mode 100644 index 000000000..2250aff2a --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/admin-config-wizard-modal.component.ts @@ -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('modal') + readonly stepper = viewChild('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 + } +} diff --git a/client/src/app/modal/admin-config-wizard/admin-config-wizard-stepper.component.html b/client/src/app/modal/admin-config-wizard/admin-config-wizard-stepper.component.html new file mode 100644 index 000000000..8f207574d --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/admin-config-wizard-stepper.component.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/client/src/app/modal/admin-config-wizard/admin-config-wizard-stepper.component.ts b/client/src/app/modal/admin-config-wizard/admin-config-wizard-stepper.component.ts new file mode 100644 index 000000000..e8de4a8df --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/admin-config-wizard-stepper.component.ts @@ -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 { +} diff --git a/client/src/app/modal/admin-config-wizard/shared/admin-config-wizard-modal-common.scss b/client/src/app/modal/admin-config-wizard/shared/admin-config-wizard-modal-common.scss new file mode 100644 index 000000000..ceadbe896 --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/shared/admin-config-wizard-modal-common.scss @@ -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); +} diff --git a/client/src/app/modal/admin-config-wizard/shared/admin-config-wizard-modal-utils.ts b/client/src/app/modal/admin-config-wizard/shared/admin-config-wizard-modal-utils.ts new file mode 100644 index 000000000..446e4ea8e --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/shared/admin-config-wizard-modal-utils.ts @@ -0,0 +1,3 @@ +export function getNoWelcomeModalLocalStorageKey () { + return 'no_welcome_modal' +} diff --git a/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-documentation.component.html b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-documentation.component.html new file mode 100644 index 000000000..2908fff0e --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-documentation.component.html @@ -0,0 +1,70 @@ +
+
+
+ mascot +
+ +

+
Congratulations
+ +
Your platform has been configured!
+

+
+ +
+ + It's time to add information about your platform! + Setting up a description, specifying who you are, why you created your platform and + how long you plan to maintain it + is very important for visitors to understand on what type of website they are. + +
+ +
+
Useful links
+ + +
+ +
+
Documentation
+ +
    +
  • + Admin + + Managing users, following other platforms, dealing with spammers, configure object storage or remote transcoding... +
  • + +
  • + User + + Setup your account, managing video playlists, discover third-party applications... +
  • + +
  • + CLI + + Upload or import videos, parse logs, prune storage directories, reset user password... +
  • +
+
+ +
+ Close + + Fill platform information +
+
diff --git a/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-documentation.component.ts b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-documentation.component.ts new file mode 100644 index 000000000..3b3e1b177 --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-documentation.component.ts @@ -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() +} diff --git a/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-edit-info.component.html b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-edit-info.component.html new file mode 100644 index 000000000..10d94edd1 --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-edit-info.component.html @@ -0,0 +1,53 @@ +
+
+
+ mascot +
+ +
+
+
STEP {{ currentStep() }}/{{ totalSteps() }}
+
+ +

General information

+ +
You can edit this information later
+ +
+
+ + + + + +
+ +
+ + + + + +
+ +
+ + + +
+ +
+ @if (showBack()) { + Back + } + + Next step + +
+
+
+
+
diff --git a/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-edit-info.component.scss b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-edit-info.component.scss new file mode 100644 index 000000000..0e0928202 --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-edit-info.component.scss @@ -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; +} diff --git a/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-edit-info.component.ts b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-edit-info.component.ts new file mode 100644 index 000000000..af3b898e1 --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-edit-info.component.ts @@ -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 + shortDescription: FormControl + primaryColor: FormControl +} + +export type FormInfo = FormDefaultTyped
+ +@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() + readonly hide = output() + + form: FormGroup + formErrors: FormReactiveErrorsTyped = {} + validationMessages: FormReactiveMessagesTyped = {} + + ngOnInit () { + this.buildForm() + } + + private buildForm () { + const obj: BuildFormArgumentTyped = { + platformName: INSTANCE_NAME_VALIDATOR, + shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR, + primaryColor: null + } + + const { + form, + formErrors, + validationMessages + } = this.formReactiveService.buildForm(obj, this.getDefaultValues()) + + this.form = form + this.formErrors = formErrors + this.validationMessages = validationMessages + } + + private getDefaultValues (): FormDefaultTyped { + 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 + } + } +} diff --git a/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-form.component.html b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-form.component.html new file mode 100644 index 000000000..ed3fcb40a --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-form.component.html @@ -0,0 +1,90 @@ +
+
+
+ mascot +
+ +
+
+
STEP {{ currentStep() }}/{{ totalSteps() }}
+
+ +

Usage type

+ +
You can also edit your platform configuration at a later time
+
+
+ +
My platform is more like...
+ +
+
+
    +
  • + + + +
  • + +
  • + + + +
  • + +
  • + + + +
  • +
+
+ +
+ @if (platformType === 'community') { + + } @else if (platformType === 'institution') { + + } @else if (platformType === 'private') { + + } +
+ +
+ +
+ Back + Preview configuration + +
+
diff --git a/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-form.component.scss b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-form.component.scss new file mode 100644 index 000000000..db4619c7d --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-form.component.scss @@ -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; + } + } +} diff --git a/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-form.component.ts b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-form.component.ts new file mode 100644 index 000000000..678a9ed6e --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-form.component.ts @@ -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 + primaryColor: FormControl +} + +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() + readonly hide = output() + + iconKey = require('../../../../assets/images/feather/key.svg') + iconInstitution = require('../../../../assets/images/feather/institution.svg') + + form: FormGroup + formErrors: FormReactiveErrorsTyped = {} + validationMessages: FormReactiveMessagesTyped = {} + + 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 = { + platformName: INSTANCE_NAME_VALIDATOR, + primaryColor: null + } + + const { + form, + formErrors, + validationMessages + } = this.formReactiveService.buildForm(obj, this.getDefaultValues()) + + this.form = form + this.formErrors = formErrors + this.validationMessages = validationMessages + } + + private getDefaultValues (): FormDefaultTyped { + 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 + } + } +} diff --git a/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-preview.component.html b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-preview.component.html new file mode 100644 index 000000000..e2b3353a8 --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-preview.component.html @@ -0,0 +1,36 @@ +
+
+
+ mascot +
+ +
+
+
STEP {{ currentStep() }}/{{ totalSteps() }}
+
+ +

Configuration preview

+ +
+

If you confirm, PeerTube will:

+ +
    + @for (explanation of safeExplanations; track explanation) { +
  • + } +
+ +

+ If you want finer settings control, your platform configuration can be easily changed after the pre-configuration wizard! +

+ +
+
+
+ +
+ Back + Confirm this configuration + +
+
diff --git a/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-preview.component.ts b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-preview.component.ts new file mode 100644 index 000000000..d45a0d367 --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-preview.component.ts @@ -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() + readonly instanceInfo = input.required() + + readonly back = output() + readonly next = output() + readonly hide = output() + + safeExplanations: string[] = [] + plugins: string[] = [] + config: PartialDeep = {} + + 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, + 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 + } + }) + } +} diff --git a/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-welcome.component.html b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-welcome.component.html new file mode 100644 index 000000000..a3ecd641c --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-welcome.component.html @@ -0,0 +1,19 @@ +
+
+ mascot +
+ +

+
Welcome to PeerTube
+ +
dear administrator!
+

+ +
Sepia, the cuttlefish, has a few questions to help you quickly pre-configure your platform.
+ +
+ Let's go! + Remind me later + +
+
diff --git a/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-welcome.component.ts b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-welcome.component.ts new file mode 100644 index 000000000..14ed450a2 --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/admin-config-wizard-welcome.component.ts @@ -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) + }) + } +} diff --git a/client/src/app/modal/admin-config-wizard/steps/usage-type/community-based-config.component.html b/client/src/app/modal/admin-config-wizard/steps/usage-type/community-based-config.component.html new file mode 100644 index 000000000..2300135bd --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/usage-type/community-based-config.component.html @@ -0,0 +1,31 @@ + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ diff --git a/client/src/app/modal/admin-config-wizard/steps/usage-type/community-based-config.component.ts b/client/src/app/modal/admin-config-wizard/steps/usage-type/community-based-config.component.ts new file mode 100644 index 000000000..04d815cbf --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/usage-type/community-based-config.component.ts @@ -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 + videoQuota: FormControl + remoteImport: FormControl + live: FormControl + globalSearch: FormControl +} + +@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() + + form: FormGroup
+ formErrors: FormReactiveErrorsTyped = {} + validationMessages: FormReactiveMessagesTyped = {} + + registrationOptions: SelectOptionsItem[] = [ + { + 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[] = [ + { + 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[] = [ + { + 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[] = [ + { + 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[] = getVideoQuotaOptions() + + ngOnInit () { + this.buildForm() + + this.form.valueChanges.subscribe(value => { + this.usageType().patch(value) + }) + } + + private buildForm () { + const obj: BuildFormArgumentTyped = { + registration: null, + remoteImport: null, + videoQuota: null, + live: null, + globalSearch: null + } + + const { + form, + formErrors, + validationMessages + } = this.formReactiveService.buildForm(obj, this.usageType() as FormDefaultTyped) + + this.form = form + this.formErrors = formErrors + this.validationMessages = validationMessages + } +} diff --git a/client/src/app/modal/admin-config-wizard/steps/usage-type/institutional-config.component.html b/client/src/app/modal/admin-config-wizard/steps/usage-type/institutional-config.component.html new file mode 100644 index 000000000..a4b9b253b --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/usage-type/institutional-config.component.html @@ -0,0 +1,25 @@ + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ diff --git a/client/src/app/modal/admin-config-wizard/steps/usage-type/institutional-config.component.ts b/client/src/app/modal/admin-config-wizard/steps/usage-type/institutional-config.component.ts new file mode 100644 index 000000000..42f6997a0 --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/usage-type/institutional-config.component.ts @@ -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 + p2p: FormControl + transcription: FormControl + authType: FormControl +} + +@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() + + form: FormGroup
+ formErrors: FormReactiveErrorsTyped = {} + validationMessages: FormReactiveMessagesTyped = {} + + p2pOptions: SelectOptionsItem[] = [ + { + 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[] = [ + { + 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[] = [ + { + 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[] = [ + { + 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 = { + keepOriginalVideo: null, + p2p: null, + transcription: null, + authType: null + } + + const { + form, + formErrors, + validationMessages + } = this.formReactiveService.buildForm(obj, this.usageType() as FormDefaultTyped) + + this.form = form + this.formErrors = formErrors + this.validationMessages = validationMessages + } +} diff --git a/client/src/app/modal/admin-config-wizard/steps/usage-type/private-instance-config.component.html b/client/src/app/modal/admin-config-wizard/steps/usage-type/private-instance-config.component.html new file mode 100644 index 000000000..a12bbcfc2 --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/usage-type/private-instance-config.component.html @@ -0,0 +1,19 @@ + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ diff --git a/client/src/app/modal/admin-config-wizard/steps/usage-type/private-instance-config.component.ts b/client/src/app/modal/admin-config-wizard/steps/usage-type/private-instance-config.component.ts new file mode 100644 index 000000000..cde9eefbf --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/usage-type/private-instance-config.component.ts @@ -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 + live: FormControl + keepOriginalVideo: FormControl +} + +@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() + + form: FormGroup
+ formErrors: FormReactiveErrorsTyped = {} + validationMessages: FormReactiveMessagesTyped = {} + + importOptions: SelectOptionsItem[] = [ + { + 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[] = [ + { + id: 'enabled', + label: 'Yes' + }, + { + id: 'disabled', + label: 'No' + } + ] + + keepOriginalVideoOptions: SelectOptionsItem[] = [ + { + 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 = { + remoteImport: null, + live: null, + keepOriginalVideo: null + } + + const { + form, + formErrors, + validationMessages + } = this.formReactiveService.buildForm(obj, this.usageType() as FormDefaultTyped) + + this.form = form + this.formErrors = formErrors + this.validationMessages = validationMessages + } +} diff --git a/client/src/app/modal/admin-config-wizard/steps/usage-type/usage-type.model.ts b/client/src/app/modal/admin-config-wizard/steps/usage-type/usage-type.model.ts new file mode 100644 index 000000000..ba17f2ba1 --- /dev/null +++ b/client/src/app/modal/admin-config-wizard/steps/usage-type/usage-type.model.ts @@ -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 = {} + 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>) { + 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`Allow any user to register`) + + this.addConfig({ + signup: { + enabled: true, + requiresApproval: false + } + }) + } else if (this.registration === 'approval') { + this.addExplanation($localize`Allow users to apply for registration on your platform`) + + this.addConfig({ + signup: { + enabled: true, + requiresApproval: true + } + }) + } else if (this.registration === 'closed') { + this.addExplanation($localize`Disable user registration`) + + this.addConfig({ + signup: { + enabled: false + } + }) + } + + if (this.registration === 'approval' || this.registration === 'open') { + this.addExplanation($localize`Require moderator approval 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`Prevent new users from uploading videos (can be changed by moderators)` + ) + } else if (this.videoQuota === -1) { + this.addExplanation($localize`Will not limit the amount of videos new users can upload`) + } else { + this.addExplanation( + $localize`Set video quota to ${getBytes(this.videoQuota, 0)} 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`Allow your users to import and synchronize videos from remote platforms (YouTube, Vimeo...)` + ) + } else { + this.addExplanation($localize`Prevent your users from importing videos 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`Allow your users to stream lives and chat with their viewers using the Livechat plugin` + ) + } else { + this.addExplanation($localize`Prevent your users from running live streams`) + } + } + + private computeVideoPrivacy () { + if (!exists(this.defaultPrivacy)) return + + this.addConfig({ + defaults: { + publish: { + privacy: this.defaultPrivacy + } + } + }) + + if (this.defaultPrivacy === VideoPrivacy.INTERNAL) { + this.addExplanation($localize`Set the default video privacy to Internal`) + } else if (this.defaultPrivacy === VideoPrivacy.PUBLIC) { + this.addExplanation($localize`Set the default video privacy to Public`) + } + } + + 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`Enable P2P streaming by default for anonymous and new users`) + } else { + this.addExplanation($localize`Disable P2P streaming 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`Allow external platforms/users to subscribe to your content`) + } else { + this.addExplanation($localize`Prevent external platforms/users to subscribe to your content`) + } + } + + private computeKeepOriginalVideo () { + if (!exists(this.keepOriginalVideo)) return + + this.addConfig({ + transcoding: { + originalFile: { + keep: this.keepOriginalVideo === 'enabled' + } + } + }) + + if (this.keepOriginalVideo === 'enabled') { + this.addExplanation($localize`Will save a copy 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 allow your users to replace a video 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 SepiaSearch as default search engine` + ) + } + } + + private computeTranscription () { + if (!exists(this.transcription)) return + + this.addConfig({ + videoTranscription: { + enabled: this.transcription === 'enabled' + } + }) + + if (this.transcription === 'enabled') { + this.addExplanation( + $localize`Enable automatic transcription of videos to create subtitles and improve accessibility` + ) + } + } + + private computeAuth () { + if (!exists(this.authType)) return + + const configStr = $localize` The plugin must be configured after the pre-configuration wizard confirmation.` + + if (this.authType === 'ldap') { + this.addExplanation($localize`Install the LDAP authentication plugin.` + configStr) + + this.plugins.push('peertube-plugin-auth-ldap') + } else if (this.authType === 'saml') { + this.addExplanation($localize`Install the SAML 2.0 authentication plugin.` + configStr) + + this.plugins.push('peertube-plugin-auth-saml2') + } else if (this.authType === 'oidc') { + this.addExplanation($localize`Install the OpenID Connect authentication plugin.` + configStr) + + this.plugins.push('peertube-plugin-auth-openid-connect') + } + } + + private addConfig (newConfig: PartialDeep) { + return this.config = merge(this.config, newConfig) + } + + private addExplanation (explanation: string) { + this.unsafeExplanations.push(explanation) + } +} diff --git a/client/src/app/modal/admin-welcome-modal.component.html b/client/src/app/modal/admin-welcome-modal.component.html deleted file mode 100644 index 784a51f4d..000000000 --- a/client/src/app/modal/admin-welcome-modal.component.html +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - diff --git a/client/src/app/modal/admin-welcome-modal.component.scss b/client/src/app/modal/admin-welcome-modal.component.scss deleted file mode 100644 index afcd2406f..000000000 --- a/client/src/app/modal/admin-welcome-modal.component.scss +++ /dev/null @@ -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; - } -} diff --git a/client/src/app/modal/admin-welcome-modal.component.ts b/client/src/app/modal/admin-welcome-modal.component.ts deleted file mode 100644 index 1c10d31eb..000000000 --- a/client/src/app/modal/admin-welcome-modal.component.ts +++ /dev/null @@ -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('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) - }) - } -} diff --git a/client/src/app/modal/confirm.component.ts b/client/src/app/modal/confirm.component.ts index 74a73611a..29a290afa 100644 --- a/client/src/app/modal/confirm.component.ts +++ b/client/src/app/modal/confirm.component.ts @@ -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 diff --git a/client/src/app/modal/instance-config-warning-modal.component.ts b/client/src/app/modal/instance-config-warning-modal.component.ts index 6ecbd8d4b..b36fe7c2a 100644 --- a/client/src/app/modal/instance-config-warning-modal.component.ts +++ b/client/src/app/modal/instance-config-warning-modal.component.ts @@ -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' || diff --git a/client/src/app/+admin/config/shared/admin-config.service.ts b/client/src/app/shared/shared-admin/admin-config.service.ts similarity index 95% rename from client/src/app/+admin/config/shared/admin-config.service.ts rename to client/src/app/shared/shared-admin/admin-config.service.ts index 7f44fba04..eff9d0fba 100644 --- a/client/src/app/+admin/config/shared/admin-config.service.ts +++ b/client/src/app/shared/shared-admin/admin-config.service.ts @@ -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 @@ -111,6 +111,11 @@ export class AdminConfigService { ) } + getCustomConfigReloadedObs () { + return this.serverService.configReloaded + .pipe(switchMap(() => this.getCustomConfig())) + } + saveAndUpdateCurrent (options: { currentConfig: CustomConfig form: FormGroup diff --git a/client/src/app/+admin/plugins/shared/plugin-api.service.ts b/client/src/app/shared/shared-admin/plugin-api.service.ts similarity index 98% rename from client/src/app/+admin/plugins/shared/plugin-api.service.ts rename to client/src/app/shared/shared-admin/plugin-api.service.ts index d23ceb4d1..376cde619 100644 --- a/client/src/app/+admin/plugins/shared/plugin-api.service.ts +++ b/client/src/app/shared/shared-admin/plugin-api.service.ts @@ -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 { diff --git a/client/src/app/shared/shared-forms/input-text.component.scss b/client/src/app/shared/shared-forms/input-text.component.scss index ab0bccf2c..334465bd1 100644 --- a/client/src/app/shared/shared-forms/input-text.component.scss +++ b/client/src/app/shared/shared-forms/input-text.component.scss @@ -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; } } diff --git a/client/src/app/shared/shared-icons/global-icon.component.scss b/client/src/app/shared/shared-icons/common-icon.component.scss similarity index 100% rename from client/src/app/shared/shared-icons/global-icon.component.scss rename to client/src/app/shared/shared-icons/common-icon.component.scss diff --git a/client/src/app/shared/shared-icons/custom-icon.component.ts b/client/src/app/shared/shared-icons/custom-icon.component.ts new file mode 100644 index 000000000..03ed5c84e --- /dev/null +++ b/client/src/app/shared/shared-icons/custom-icon.component.ts @@ -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() + + ngOnInit () { + const nativeElement = this.el.nativeElement as HTMLElement + + nativeElement.innerHTML = this.html() + nativeElement.ariaHidden = 'true' + } +} diff --git a/client/src/app/shared/shared-icons/global-icon.component.ts b/client/src/app/shared/shared-icons/global-icon.component.ts index f30c3de20..595776468 100644 --- a/client/src/app/shared/shared-icons/global-icon.component.ts +++ b/client/src/app/shared/shared-icons/global-icon.component.ts @@ -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 }) diff --git a/client/src/app/shared/shared-main/buttons/button.component.scss b/client/src/app/shared/shared-main/buttons/button.component.scss index b2b325b3d..94e3e2195 100644 --- a/client/src/app/shared/shared-main/buttons/button.component.scss +++ b/client/src/app/shared/shared-main/buttons/button.component.scss @@ -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) { diff --git a/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts b/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts index 79da08a5c..14a509d1f 100644 --- a/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts +++ b/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts @@ -4,4 +4,5 @@ import { Subject } from 'rxjs' @Injectable({ providedIn: 'root' }) export class PeertubeModalService { openQuickSettingsSubject = new Subject() + openAdminConfigWizardSubject = new Subject<{ showWelcome: boolean }>() } diff --git a/client/src/app/shared/shared-video-miniature/video-filters-header.component.scss b/client/src/app/shared/shared-video-miniature/video-filters-header.component.scss index 981b38884..d27c883b0 100644 --- a/client/src/app/shared/shared-video-miniature/video-filters-header.component.scss +++ b/client/src/app/shared/shared-video-miniature/video-filters-header.component.scss @@ -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; diff --git a/client/src/assets/images/feather/institution.svg b/client/src/assets/images/feather/institution.svg new file mode 100644 index 000000000..77830eb6d --- /dev/null +++ b/client/src/assets/images/feather/institution.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/src/assets/images/feather/key.svg b/client/src/assets/images/feather/key.svg new file mode 100644 index 000000000..8a0200b3a --- /dev/null +++ b/client/src/assets/images/feather/key.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/src/root-helpers/theme-manager.ts b/client/src/root-helpers/theme-manager.ts index 284b6011f..b5b5e3694 100644 --- a/client/src/root-helpers/theme-manager.ts +++ b/client/src/root-helpers/theme-manager.ts @@ -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)) } diff --git a/client/src/sass/bootstrap.scss b/client/src/sass/bootstrap.scss index f53fa4105..6f172adf8 100644 --- a/client/src/sass/bootstrap.scss +++ b/client/src/sass/bootstrap.scss @@ -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); } diff --git a/client/src/sass/include/_bootstrap-variables.scss b/client/src/sass/include/_bootstrap-variables.scss index 0d6e3b543..51a50f69c 100644 --- a/client/src/sass/include/_bootstrap-variables.scss +++ b/client/src/sass/include/_bootstrap-variables.scss @@ -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 diff --git a/client/src/sass/include/_css-variables.scss b/client/src/sass/include/_css-variables.scss index 4db763764..abd7c4fdf 100644 --- a/client/src/sass/include/_css-variables.scss +++ b/client/src/sass/include/_css-variables.scss @@ -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}; +} diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss index 5cadd93de..e370d4806 100644 --- a/client/src/sass/include/_variables.scss +++ b/client/src/sass/include/_variables.scss @@ -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), diff --git a/client/src/standalone/videos/embed.scss b/client/src/standalone/videos/embed.scss index 8f25b7501..1c136fb31 100644 --- a/client/src/standalone/videos/embed.scss +++ b/client/src/standalone/videos/embed.scss @@ -1,20 +1,20 @@ -@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(); +} - & { - font-family: $main-fonts; - font-weight: $font-regular; - color: #000; - } +body { + font-family: $main-fonts; + font-weight: $font-regular; + color: #000; } video { @@ -66,11 +66,11 @@ body { } #error-details { - margin-top: 30px + margin-top: 30px; } #error-details-content { - margin-top: 10px + margin-top: 10px; } #placeholder-preview { @@ -105,7 +105,7 @@ body { margin: 1rem 0.5rem; border: 0; font-weight: 600; - border-radius: 3px!important; + border-radius: 3px !important; font-size: 18px; display: inline-block; } @@ -141,4 +141,3 @@ body { font-size: 14px; } } - diff --git a/client/src/standalone/videos/shared/peertube-theme.ts b/client/src/standalone/videos/shared/peertube-theme.ts index fbebff341..318f5e32f 100644 --- a/client/src/standalone/videos/shared/peertube-theme.ts +++ b/client/src/standalone/videos/shared/peertube-theme.ts @@ -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) { diff --git a/config/default.yaml b/config/default.yaml index 6396a8a8c..1c8a70953 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -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 diff --git a/config/production.yaml.example b/config/production.yaml.example index 8032bbcc4..c44019e15 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -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 diff --git a/packages/models/src/server/custom-config.model.ts b/packages/models/src/server/custom-config.model.ts index 2d765e33f..afdc6f164 100644 --- a/packages/models/src/server/custom-config.model.ts +++ b/packages/models/src/server/custom-config.model.ts @@ -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 + } + } } diff --git a/packages/tests/src/api/server/config.ts b/packages/tests/src/api/server/config.ts index 6e880d519..7dabcaaf6 100644 --- a/packages/tests/src/api/server/config.ts +++ b/packages/tests/src/api/server/config.ts @@ -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 + } } } } diff --git a/server/core/controllers/api/config.ts b/server/core/controllers/api/config.ts index bf9841864..96e714b35 100644 --- a/server/core/controllers/api/config.ts +++ b/server/core/controllers/api/config.ts @@ -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 + } } } } diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index ae11bbbfb..655fea0fe 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml @@ -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: