diff --git a/client/src/app/+admin/config/config.routes.ts b/client/src/app/+admin/config/config.routes.ts index e161b3c52..d2ff90a3f 100644 --- a/client/src/app/+admin/config/config.routes.ts +++ b/client/src/app/+admin/config/config.routes.ts @@ -16,7 +16,7 @@ import { import { AdminConfigCustomizationComponent } from './pages/admin-config-customization.component' import { AdminConfigService } from '../../shared/shared-admin/admin-config.service' import { AdminConfigLogoComponent } from './pages/admin-config-logo.component' -import { InstanceLogoService } from './shared/instance-logo.service' +import { InstanceLogoService } from '../../shared/shared-instance/instance-logo.service' export const customConfigResolver: ResolveFn = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => { return inject(AdminConfigService).getCustomConfig() diff --git a/client/src/app/+admin/config/pages/admin-config-logo.component.ts b/client/src/app/+admin/config/pages/admin-config-logo.component.ts index 4f3938231..a6131ada5 100644 --- a/client/src/app/+admin/config/pages/admin-config-logo.component.ts +++ b/client/src/app/+admin/config/pages/admin-config-logo.component.ts @@ -15,7 +15,7 @@ import { of, Subscription, switchMap, tap } from 'rxjs' import { AdminConfigService } from '../../../shared/shared-admin/admin-config.service' import { PreviewUploadComponent } from '../../../shared/shared-forms/preview-upload.component' import { AdminSaveBarComponent } from '../shared/admin-save-bar.component' -import { InstanceLogoService } from '../shared/instance-logo.service' +import { InstanceLogoService } from '../../../shared/shared-instance/instance-logo.service' type Form = { hideInstanceName: FormControl diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.component.html b/client/src/app/+admin/overview/users/user-edit/user-edit.component.html index f111ddd0b..9bde28108 100644 --- a/client/src/app/+admin/overview/users/user-edit/user-edit.component.html +++ b/client/src/app/+admin/overview/users/user-edit/user-edit.component.html @@ -225,7 +225,7 @@
-
+
Send a link to reset the password by email to the user
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 index 1a38f7888..04333c816 100644 --- 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 @@ -7,7 +7,7 @@ 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 { AdminConfigWizardEditInfoComponent, FormEditInfo } 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' @@ -37,7 +37,7 @@ export class AdminConfigWizardModalComponent implements OnInit { readonly created = output() usageType: UsageType - instanceInfo: FormInfo + instanceInfo: FormEditInfo showWelcome: boolean dryRun: boolean 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 index 10d94edd1..aea6dc996 100644 --- 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 @@ -33,6 +33,13 @@
+
+ +
Used in various places such as on the website, mobile application and social media
+ + +
+
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 index 0e0928202..5ad59b236 100644 --- 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 @@ -11,6 +11,11 @@ form { height: 190px; } +.avatar-preview { + width: 128px; + height: 128px; +} + @media screen and (max-width: $small-view) { .mascot { display: none; 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 index af3b898e1..964aec602 100644 --- 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 @@ -11,22 +11,26 @@ import { FormReactiveMessagesTyped } from '@app/shared/form-validators/form-validator.model' import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service' +import { PreviewUploadComponent } from '@app/shared/shared-forms/preview-upload.component' import { ColorPickerModule } from 'primeng/colorpicker' import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component' +import { findAppropriateImage } from '@peertube/peertube-core-utils' +import { logger } from '@root-helpers/logger' type Form = { platformName: FormControl shortDescription: FormControl + avatar: FormControl primaryColor: FormControl } -export type FormInfo = FormDefaultTyped
+export type FormEditInfo = 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 ] + imports: [ CommonModule, FormsModule, ReactiveFormsModule, ColorPickerModule, CdkStepperModule, ButtonComponent, PreviewUploadComponent ] }) export class AdminConfigWizardEditInfoComponent implements OnInit { private server = inject(ServerService) @@ -38,7 +42,7 @@ export class AdminConfigWizardEditInfoComponent implements OnInit { readonly showBack = input.required({ transform: booleanAttribute }) readonly back = output() - readonly next = output() + readonly next = output() readonly hide = output() form: FormGroup @@ -47,12 +51,23 @@ export class AdminConfigWizardEditInfoComponent implements OnInit { ngOnInit () { this.buildForm() + + const avatar = findAppropriateImage(this.server.getHTMLConfig().instance.avatars, 128) + if (avatar) { + fetch(avatar.fileUrl) + .then(response => response.blob()) + .then(blob => this.form.patchValue({ avatar: blob })) + .catch(() => { + logger.error('Could not fetch instance avatar') + }) + } } private buildForm () { const obj: BuildFormArgumentTyped = { platformName: INSTANCE_NAME_VALIDATOR, shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR, + avatar: null, primaryColor: null } @@ -67,7 +82,7 @@ export class AdminConfigWizardEditInfoComponent implements OnInit { this.validationMessages = validationMessages } - private getDefaultValues (): FormDefaultTyped { + private getDefaultValues (): FormEditInfo { const config = this.server.getHTMLConfig() const primaryColorConfig = config.theme.customization.primaryColor @@ -78,6 +93,7 @@ export class AdminConfigWizardEditInfoComponent implements OnInit { return { platformName: config.instance.name, shortDescription: config.instance.shortDescription, + avatar: undefined as Blob, primaryColor } } 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 index 481deddc5..e087a0a89 100644 --- 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 @@ -11,8 +11,9 @@ 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 { FormEditInfo } from './admin-config-wizard-edit-info.component' import { UsageType } from './usage-type/usage-type.model' +import { InstanceLogoService } from '@app/shared/shared-instance/instance-logo.service' @Component({ selector: 'my-admin-config-wizard-preview', @@ -26,7 +27,7 @@ import { UsageType } from './usage-type/usage-type.model' CdkStepperModule, ButtonComponent ], - providers: [ AdminConfigService, PluginApiService ] + providers: [ AdminConfigService, PluginApiService, InstanceLogoService ] }) export class AdminConfigWizardPreviewComponent implements OnChanges { private adminConfig = inject(AdminConfigService) @@ -34,11 +35,12 @@ export class AdminConfigWizardPreviewComponent implements OnChanges { private notifier = inject(Notifier) private html = inject(HtmlRendererService) private server = inject(ServerService) + private instanceLogo = inject(InstanceLogoService) readonly currentStep = input.required({ transform: numberAttribute }) readonly totalSteps = input.required({ transform: numberAttribute }) readonly usageType = input.required() - readonly instanceInfo = input.required() + readonly instanceInfo = input.required() readonly dryRun = input.required({ transform: booleanAttribute }) readonly back = output() @@ -84,6 +86,12 @@ export class AdminConfigWizardPreviewComponent implements OnChanges { this.updating = true this.adminConfig.updateCustomConfig(this.config) + .pipe(() => { + const avatar = this.instanceInfo().avatar + if (avatar) return this.instanceLogo.updateAvatar(avatar) + + return this.instanceLogo.deleteAvatar() + }) .pipe( switchMap(() => this.server.resetConfig()), switchMap(() => { diff --git a/client/src/app/shared/form-validators/form-validator.model.ts b/client/src/app/shared/form-validators/form-validator.model.ts index d4fdcd460..bd38f972d 100644 --- a/client/src/app/shared/form-validators/form-validator.model.ts +++ b/client/src/app/shared/form-validators/form-validator.model.ts @@ -1,5 +1,4 @@ import { AsyncValidatorFn, FormArray, FormControl, FormGroup, ValidatorFn } from '@angular/forms' -import { PartialDeep } from 'type-fest' export type BuildFormValidator = { VALIDATORS: ValidatorFn[] @@ -19,7 +18,7 @@ export type BuildFormArgumentTyped = ReplaceForm export type FormDefault = { [name: string]: Blob | Date | boolean | number | number[] | string | string[] | FormDefault } -export type FormDefaultTyped = PartialDeep> +export type FormDefaultTyped = Partial> // --------------------------------------------------------------------------- @@ -42,8 +41,9 @@ export type UnwrapForm = { [K in keyof Form]: _UnwrapForm } -type _UnwrapForm = T extends FormGroup ? UnwrapForm : +type _UnwrapForm = T extends FormGroup ? Partial> : T extends FormArray ? _UnwrapForm[] : + T extends FormControl ? Blob : T extends FormControl ? U : never diff --git a/client/src/app/+admin/config/shared/instance-logo.service.ts b/client/src/app/shared/shared-instance/instance-logo.service.ts similarity index 100% rename from client/src/app/+admin/config/shared/instance-logo.service.ts rename to client/src/app/shared/shared-instance/instance-logo.service.ts diff --git a/packages/core-utils/src/common/image.ts b/packages/core-utils/src/common/image.ts index f229d46d8..ef833af27 100644 --- a/packages/core-utils/src/common/image.ts +++ b/packages/core-utils/src/common/image.ts @@ -1,4 +1,6 @@ export function findAppropriateImage (images: T[], wantedWidth: number) { + if (!images || images.length === 0) return undefined + const imagesSorted = images.sort((a, b) => a.width - b.width) for (const image of imagesSorted) {