Add ability to set avatar to instance
This commit is contained in:
parent
db06d13c67
commit
bb7cb0d2fd
29 changed files with 693 additions and 348 deletions
|
@ -8,11 +8,25 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-lg-8 col-xl-9">
|
<div class="col-12 col-lg-8 col-xl-9">
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="avatarfile">Square icon</label>
|
||||||
|
|
||||||
|
<div class="label-small-info">
|
||||||
|
<p i18n class="mb-0">Square icon can be used on your custom homepage.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<my-actor-avatar-edit
|
||||||
|
class="d-block mb-4"
|
||||||
|
actorType="account" previewImage="false" [username]="instanceName" displayUsername="false"
|
||||||
|
[avatars]="instanceAvatars" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
|
||||||
|
></my-actor-avatar-edit>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="bannerfile">Banner</label>
|
<label i18n for="bannerfile">Banner</label>
|
||||||
|
|
||||||
<div class="label-small-info">
|
<div class="label-small-info">
|
||||||
<p i18n class="mb-0">Banner is displayed in the about, login and registration pages.</p>
|
<p i18n class="mb-0">Banner is displayed in the about, login and registration pages and be used on your custom homepage.</p>
|
||||||
<p i18n>It can also be displayed on external websites to promote your instance, such as <a target="_blank" href="https://joinpeertube.org/instances">JoinPeerTube.org</a>.</p>
|
<p i18n>It can also be displayed on external websites to promote your instance, such as <a target="_blank" href="https://joinpeertube.org/instances">JoinPeerTube.org</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,11 @@ import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||||
import { Component, Input, OnInit } from '@angular/core'
|
import { Component, Input, OnInit } from '@angular/core'
|
||||||
import { FormGroup } from '@angular/forms'
|
import { FormGroup } from '@angular/forms'
|
||||||
import { CustomMarkupService } from '@app/shared/shared-custom-markup'
|
import { CustomMarkupService } from '@app/shared/shared-custom-markup'
|
||||||
import { Notifier } from '@app/core'
|
import { Notifier, ServerService } from '@app/core'
|
||||||
import { HttpErrorResponse } from '@angular/common/http'
|
import { HttpErrorResponse } from '@angular/common/http'
|
||||||
import { genericUploadErrorHandler } from '@app/helpers'
|
import { genericUploadErrorHandler } from '@app/helpers'
|
||||||
import { InstanceService } from '@app/shared/shared-instance'
|
import { InstanceService } from '@app/shared/shared-instance'
|
||||||
|
import { ActorImage, HTMLServerConfig } from '@peertube/peertube-models'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-edit-instance-information',
|
selector: 'my-edit-instance-information',
|
||||||
|
@ -20,17 +21,27 @@ export class EditInstanceInformationComponent implements OnInit {
|
||||||
@Input() categoryItems: SelectOptionsItem[] = []
|
@Input() categoryItems: SelectOptionsItem[] = []
|
||||||
|
|
||||||
instanceBannerUrl: string
|
instanceBannerUrl: string
|
||||||
|
instanceAvatars: ActorImage[] = []
|
||||||
|
|
||||||
|
private serverConfig: HTMLServerConfig
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private customMarkup: CustomMarkupService,
|
private customMarkup: CustomMarkupService,
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private instanceService: InstanceService
|
private instanceService: InstanceService,
|
||||||
|
private server: ServerService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get instanceName () {
|
||||||
|
return this.server.getHTMLConfig().instance.name
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.resetBannerUrl()
|
this.serverConfig = this.server.getHTMLConfig()
|
||||||
|
|
||||||
|
this.updateActorImages()
|
||||||
}
|
}
|
||||||
|
|
||||||
getCustomMarkdownRenderer () {
|
getCustomMarkdownRenderer () {
|
||||||
|
@ -43,7 +54,7 @@ export class EditInstanceInformationComponent implements OnInit {
|
||||||
next: () => {
|
next: () => {
|
||||||
this.notifier.success($localize`Banner changed.`)
|
this.notifier.success($localize`Banner changed.`)
|
||||||
|
|
||||||
this.resetBannerUrl()
|
this.resetActorImages()
|
||||||
},
|
},
|
||||||
|
|
||||||
error: (err: HttpErrorResponse) => genericUploadErrorHandler({ err, name: $localize`banner`, notifier: this.notifier })
|
error: (err: HttpErrorResponse) => genericUploadErrorHandler({ err, name: $localize`banner`, notifier: this.notifier })
|
||||||
|
@ -56,17 +67,51 @@ export class EditInstanceInformationComponent implements OnInit {
|
||||||
next: () => {
|
next: () => {
|
||||||
this.notifier.success($localize`Banner deleted.`)
|
this.notifier.success($localize`Banner deleted.`)
|
||||||
|
|
||||||
this.resetBannerUrl()
|
this.resetActorImages()
|
||||||
},
|
},
|
||||||
|
|
||||||
error: err => this.notifier.error(err.message)
|
error: err => this.notifier.error(err.message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetBannerUrl () {
|
onAvatarChange (formData: FormData) {
|
||||||
this.instanceService.getInstanceBannerUrl()
|
this.instanceService.updateInstanceAvatar(formData)
|
||||||
.subscribe(instanceBannerUrl => {
|
.subscribe({
|
||||||
this.instanceBannerUrl = instanceBannerUrl
|
next: () => {
|
||||||
|
this.notifier.success($localize`Avatar changed.`)
|
||||||
|
|
||||||
|
this.resetActorImages()
|
||||||
|
},
|
||||||
|
|
||||||
|
error: (err: HttpErrorResponse) => genericUploadErrorHandler({ err, name: $localize`avatar`, notifier: this.notifier })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onAvatarDelete () {
|
||||||
|
this.instanceService.deleteInstanceAvatar()
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.notifier.success($localize`Avatar deleted.`)
|
||||||
|
|
||||||
|
this.resetActorImages()
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateActorImages () {
|
||||||
|
this.instanceBannerUrl = this.serverConfig.instance.banners?.[0]?.path
|
||||||
|
this.instanceAvatars = this.serverConfig.instance.avatars
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetActorImages () {
|
||||||
|
this.server.resetConfig()
|
||||||
|
.subscribe(config => {
|
||||||
|
this.serverConfig = config
|
||||||
|
|
||||||
|
this.updateActorImages()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,10 +70,18 @@
|
||||||
<div class="row mt-4"> <!-- user grid -->
|
<div class="row mt-4"> <!-- user grid -->
|
||||||
<div class="col-12 col-lg-4 col-xl-3">
|
<div class="col-12 col-lg-4 col-xl-3">
|
||||||
<div class="anchor" id="user"></div> <!-- user anchor -->
|
<div class="anchor" id="user"></div> <!-- user anchor -->
|
||||||
<div *ngIf="isCreation()" class="section-left-column-title" i18n>NEW USER</div>
|
|
||||||
<div *ngIf="!isCreation() && user" class="section-left-column-title">
|
@if (isCreation()) {
|
||||||
<my-actor-avatar-edit [actor]="user.account" [editable]="false" [displaySubscribers]="false" [displayUsername]="false"></my-actor-avatar-edit>
|
<div class="section-left-column-title" i18n>NEW USER</div>
|
||||||
|
} @else if (user) {
|
||||||
|
<div class="section-left-column-title">
|
||||||
|
<my-actor-avatar-edit
|
||||||
|
actorType="account" [displayName]="user.account.displayName" [avatars]="user.account.avatars"
|
||||||
|
editable="false" [username]="user.username" displayUsername="false"
|
||||||
|
></my-actor-avatar-edit>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-lg-8 col-xl-9">
|
<div class="col-12 col-lg-8 col-xl-9">
|
||||||
|
|
|
@ -16,9 +16,10 @@
|
||||||
></my-actor-banner-edit>
|
></my-actor-banner-edit>
|
||||||
|
|
||||||
<my-actor-avatar-edit
|
<my-actor-avatar-edit
|
||||||
*ngIf="videoChannel" [previewImage]="isCreation()" class="d-block mb-4"
|
*ngIf="videoChannel" class="d-block mb-4" actorType="channel"
|
||||||
[actor]="videoChannel" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
|
[displayName]="videoChannel.displayName" [previewImage]="isCreation()" [avatars]="videoChannel.avatars"
|
||||||
[displayUsername]="!isCreation()" [displaySubscribers]="!isCreation()"
|
[username]="!isCreation() && videoChannel.displayName" [subscribers]="!isCreation() && videoChannel.followersCount"
|
||||||
|
(avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
|
||||||
></my-actor-avatar-edit>
|
></my-actor-avatar-edit>
|
||||||
|
|
||||||
<div class="form-group" *ngIf="isCreation()">
|
<div class="form-group" *ngIf="isCreation()">
|
||||||
|
|
|
@ -4,7 +4,11 @@
|
||||||
<div class="col-12 col-lg-4 col-xl-3"></div>
|
<div class="col-12 col-lg-4 col-xl-3"></div>
|
||||||
|
|
||||||
<div class="col-12 col-lg-8 col-xl-9">
|
<div class="col-12 col-lg-8 col-xl-9">
|
||||||
<my-actor-avatar-edit [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-edit>
|
<my-actor-avatar-edit
|
||||||
|
actorType="account" [avatars]="user.account.avatars"
|
||||||
|
[displayName]="user.account.displayName" [username]="user.username" [subscribers]="user.account.followersCount"
|
||||||
|
(avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
|
||||||
|
></my-actor-avatar-edit>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="actor" *ngIf="actor">
|
<div class="actor" *ngIf="actor">
|
||||||
<div class="position-relative me-3">
|
<div class="position-relative me-3">
|
||||||
<my-actor-avatar [actor]="actor" [actorType]="getActorType()" [previewImage]="preview" size="100"></my-actor-avatar>
|
<my-actor-avatar [actor]="actor" [actorType]="actorType" [previewImage]="preview" size="100"></my-actor-avatar>
|
||||||
|
|
||||||
<div *ngIf="editable && !hasAvatar()" class="actor-img-edit-button button-focus-within" [ngbTooltip]="avatarFormat" placement="right" container="body">
|
<div *ngIf="editable && !hasAvatar()" class="actor-img-edit-button button-focus-within" [ngbTooltip]="avatarFormat" placement="right" container="body">
|
||||||
<my-global-icon iconName="upload"></my-global-icon>
|
<my-global-icon iconName="upload"></my-global-icon>
|
||||||
|
@ -31,8 +31,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actor-info">
|
<div class="actor-info">
|
||||||
<div class="actor-info-display-name">{{ actor.displayName }}</div>
|
<div *ngIf="displayName" class="actor-info-display-name">{{ displayName }}</div>
|
||||||
<div *ngIf="displayUsername" class="actor-info-username">{{ actor.name }}</div>
|
<div *ngIf="displayUsername && username" class="actor-info-username">{{ username }}</div>
|
||||||
<div *ngIf="displaySubscribers" i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div>
|
<div *ngIf="subscribers" i18n class="actor-info-followers">{{ subscribers }} subscribers</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
|
import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild, booleanAttribute } from '@angular/core'
|
||||||
import { Notifier, ServerService } from '@app/core'
|
import { Notifier, ServerService } from '@app/core'
|
||||||
import { Account, VideoChannel } from '@app/shared/shared-main'
|
|
||||||
import { getBytes } from '@root-helpers/bytes'
|
import { getBytes } from '@root-helpers/bytes'
|
||||||
import { imageToDataURL } from '@root-helpers/images'
|
import { imageToDataURL } from '@root-helpers/images'
|
||||||
|
import { ActorAvatarInput } from '../shared-actor-image/actor-avatar.component'
|
||||||
|
import { ActorImage } from '@peertube/peertube-models'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-actor-avatar-edit',
|
selector: 'my-actor-avatar-edit',
|
||||||
|
@ -12,14 +13,19 @@ import { imageToDataURL } from '@root-helpers/images'
|
||||||
'./actor-avatar-edit.component.scss'
|
'./actor-avatar-edit.component.scss'
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ActorAvatarEditComponent implements OnInit {
|
export class ActorAvatarEditComponent implements OnInit, OnChanges {
|
||||||
@ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement>
|
@ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement>
|
||||||
|
|
||||||
@Input() actor: VideoChannel | Account
|
@Input({ required: true }) actorType: 'channel' | 'account'
|
||||||
@Input() editable = true
|
@Input({ required: true }) avatars: ActorImage[]
|
||||||
@Input() displaySubscribers = true
|
@Input({ required: true }) username: string
|
||||||
@Input() displayUsername = true
|
|
||||||
@Input() previewImage = false
|
@Input() displayName: string
|
||||||
|
@Input() subscribers: number
|
||||||
|
|
||||||
|
@Input({ transform: booleanAttribute }) displayUsername = true
|
||||||
|
@Input({ transform: booleanAttribute }) editable = true
|
||||||
|
@Input({ transform: booleanAttribute }) previewImage = false
|
||||||
|
|
||||||
@Output() avatarChange = new EventEmitter<FormData>()
|
@Output() avatarChange = new EventEmitter<FormData>()
|
||||||
@Output() avatarDelete = new EventEmitter<void>()
|
@Output() avatarDelete = new EventEmitter<void>()
|
||||||
|
@ -30,6 +36,8 @@ export class ActorAvatarEditComponent implements OnInit {
|
||||||
|
|
||||||
preview: string
|
preview: string
|
||||||
|
|
||||||
|
actor: ActorAvatarInput
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private serverService: ServerService,
|
private serverService: ServerService,
|
||||||
private notifier: Notifier
|
private notifier: Notifier
|
||||||
|
@ -41,8 +49,14 @@ export class ActorAvatarEditComponent implements OnInit {
|
||||||
this.maxAvatarSize = config.avatar.file.size.max
|
this.maxAvatarSize = config.avatar.file.size.max
|
||||||
this.avatarExtensions = config.avatar.file.extensions.join(', ')
|
this.avatarExtensions = config.avatar.file.extensions.join(', ')
|
||||||
|
|
||||||
this.avatarFormat = `${$localize`max size`}: 192*192px, ` +
|
this.avatarFormat = $localize`max size: 192*192px, ${getBytes(this.maxAvatarSize)} extensions: ${this.avatarExtensions}`
|
||||||
`${getBytes(this.maxAvatarSize)} ${$localize`extensions`}: ${this.avatarExtensions}`
|
}
|
||||||
|
|
||||||
|
ngOnChanges () {
|
||||||
|
this.actor = {
|
||||||
|
avatars: this.avatars,
|
||||||
|
name: this.username
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onAvatarChange (input: HTMLInputElement) {
|
onAvatarChange (input: HTMLInputElement) {
|
||||||
|
@ -69,12 +83,6 @@ export class ActorAvatarEditComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
hasAvatar () {
|
hasAvatar () {
|
||||||
return !!this.preview || this.actor.avatars.length !== 0
|
return !!this.preview || this.avatars.length !== 0
|
||||||
}
|
|
||||||
|
|
||||||
getActorType () {
|
|
||||||
if ((this.actor as VideoChannel).ownerAccount) return 'channel'
|
|
||||||
|
|
||||||
return 'account'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<ng-template #img>
|
<ng-template #img>
|
||||||
<img *ngIf="displayImage()" [class]="classes" [src]="previewImage || avatarUrl || defaultAvatarUrl" alt="" />
|
<img #avatarEl *ngIf="displayImage()" [ngClass]="classes" [src]="previewImage || avatarUrl || defaultAvatarUrl" alt="" />
|
||||||
|
|
||||||
<div *ngIf="displayActorInitial()" [ngClass]="classes">
|
<div #avatarEl *ngIf="displayActorInitial()" [ngClass]="classes">
|
||||||
<span>{{ getActorInitial() }}</span>
|
<span>{{ getActorInitial() }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="displayPlaceholder()" [ngClass]="classes"></div>
|
<div #avatarEl *ngIf="displayPlaceholder()" [ngClass]="classes"></div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<a *ngIf="actor && href" [href]="href" target="_blank" rel="noopener noreferrer" [title]="title">
|
<a *ngIf="actor && href" [href]="href" target="_blank" rel="noopener noreferrer" [title]="title">
|
||||||
|
|
|
@ -2,9 +2,7 @@
|
||||||
@use '_mixins' as *;
|
@use '_mixins' as *;
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
--avatarSize: 100%;
|
// Defined in component
|
||||||
--initialFontSize: 22px;
|
|
||||||
|
|
||||||
width: var(--avatarSize);
|
width: var(--avatarSize);
|
||||||
height: var(--avatarSize);
|
height: var(--avatarSize);
|
||||||
min-width: var(--avatarSize);
|
min-width: var(--avatarSize);
|
||||||
|
@ -20,26 +18,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$sizes: '18', '25', '28', '32', '34', '35', '36', '40', '48', '75', '80', '100', '120';
|
|
||||||
|
|
||||||
@each $size in $sizes {
|
|
||||||
.avatar-#{$size} {
|
|
||||||
--avatarSize: #{$size}px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-18 {
|
|
||||||
--initialFontSize: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-100 {
|
|
||||||
--initialFontSize: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-120 {
|
|
||||||
--initialFontSize: 46px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,35 @@
|
||||||
import { Component, Input, OnChanges, OnInit } from '@angular/core'
|
import { Component, ElementRef, Input, OnChanges, OnInit, ViewChild, numberAttribute } from '@angular/core'
|
||||||
import { VideoChannel } from '../shared-main'
|
import { VideoChannel } from '../shared-main'
|
||||||
import { Account } from '../shared-main/account/account.model'
|
import { Account } from '../shared-main/account/account.model'
|
||||||
import { objectKeysTyped } from '@peertube/peertube-core-utils'
|
import { objectKeysTyped } from '@peertube/peertube-core-utils'
|
||||||
|
|
||||||
type ActorInput = {
|
export type ActorAvatarInput = {
|
||||||
name: string
|
name: string
|
||||||
avatars: { width: number, url?: string, path: string }[]
|
avatars: { width: number, url?: string, path: string }[]
|
||||||
url: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ActorAvatarSize = '18' | '25' | '28' | '32' | '34' | '35' | '36' | '40' | '48' | '75' | '80' | '100' | '120'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-actor-avatar',
|
selector: 'my-actor-avatar',
|
||||||
styleUrls: [ './actor-avatar.component.scss' ],
|
styleUrls: [ './actor-avatar.component.scss' ],
|
||||||
templateUrl: './actor-avatar.component.html'
|
templateUrl: './actor-avatar.component.html'
|
||||||
})
|
})
|
||||||
export class ActorAvatarComponent implements OnInit, OnChanges {
|
export class ActorAvatarComponent implements OnInit, OnChanges {
|
||||||
private _title: string
|
@ViewChild('avatarEl') avatarEl: ElementRef
|
||||||
|
|
||||||
@Input() actor: ActorInput
|
@Input() actor: ActorAvatarInput
|
||||||
@Input() actorType: 'channel' | 'account' | 'unlogged'
|
@Input() actorType: 'channel' | 'account' | 'unlogged'
|
||||||
|
|
||||||
@Input() previewImage: string
|
@Input() previewImage: string
|
||||||
|
|
||||||
@Input() size: ActorAvatarSize
|
@Input({ transform: numberAttribute }) size: number
|
||||||
|
|
||||||
// Use an external link
|
// Use an external link
|
||||||
@Input() href: string
|
@Input() href: string
|
||||||
// Use routerLink
|
// Use routerLink
|
||||||
@Input() internalHref: string | any[]
|
@Input() internalHref: string | any[]
|
||||||
|
|
||||||
|
private _title: string
|
||||||
|
|
||||||
@Input() set title (value) {
|
@Input() set title (value) {
|
||||||
this._title = value
|
this._title = value
|
||||||
}
|
}
|
||||||
|
@ -47,6 +46,10 @@ export class ActorAvatarComponent implements OnInit, OnChanges {
|
||||||
defaultAvatarUrl: string
|
defaultAvatarUrl: string
|
||||||
avatarUrl: string
|
avatarUrl: string
|
||||||
|
|
||||||
|
constructor (private el: ElementRef) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.buildDefaultAvatarUrl()
|
this.buildDefaultAvatarUrl()
|
||||||
|
|
||||||
|
@ -60,10 +63,21 @@ export class ActorAvatarComponent implements OnInit, OnChanges {
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildClasses () {
|
private buildClasses () {
|
||||||
|
let avatarSize = '100%'
|
||||||
|
let initialFontSize = '22px'
|
||||||
|
|
||||||
this.classes = [ 'avatar' ]
|
this.classes = [ 'avatar' ]
|
||||||
|
|
||||||
if (this.size) {
|
if (this.size) {
|
||||||
this.classes.push(`avatar-${this.size}`)
|
avatarSize = `${this.size}px`
|
||||||
|
|
||||||
|
if (this.size <= 18) {
|
||||||
|
initialFontSize = '13px'
|
||||||
|
} else if (this.size >= 100) {
|
||||||
|
initialFontSize = '40px'
|
||||||
|
} else if (this.size >= 120) {
|
||||||
|
initialFontSize = '46px'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isChannel()) {
|
if (this.isChannel()) {
|
||||||
|
@ -77,6 +91,10 @@ export class ActorAvatarComponent implements OnInit, OnChanges {
|
||||||
this.classes.push('initial')
|
this.classes.push('initial')
|
||||||
this.classes.push(this.getColorTheme())
|
this.classes.push(this.getColorTheme())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const elStyle = (this.el.nativeElement as HTMLElement).style
|
||||||
|
elStyle.setProperty('--avatarSize', avatarSize)
|
||||||
|
elStyle.setProperty('--initialFontSize', initialFontSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildDefaultAvatarUrl () {
|
private buildDefaultAvatarUrl () {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
ChannelMiniatureMarkupData,
|
ChannelMiniatureMarkupData,
|
||||||
ContainerMarkupData,
|
ContainerMarkupData,
|
||||||
EmbedMarkupData,
|
EmbedMarkupData,
|
||||||
|
InstanceAvatarMarkupData,
|
||||||
InstanceBannerMarkupData,
|
InstanceBannerMarkupData,
|
||||||
PlaylistMiniatureMarkupData,
|
PlaylistMiniatureMarkupData,
|
||||||
VideoMiniatureMarkupData,
|
VideoMiniatureMarkupData,
|
||||||
|
@ -16,6 +17,7 @@ import {
|
||||||
ButtonMarkupComponent,
|
ButtonMarkupComponent,
|
||||||
ChannelMiniatureMarkupComponent,
|
ChannelMiniatureMarkupComponent,
|
||||||
EmbedMarkupComponent,
|
EmbedMarkupComponent,
|
||||||
|
InstanceAvatarMarkupComponent,
|
||||||
InstanceBannerMarkupComponent,
|
InstanceBannerMarkupComponent,
|
||||||
PlaylistMiniatureMarkupComponent,
|
PlaylistMiniatureMarkupComponent,
|
||||||
VideoMiniatureMarkupComponent,
|
VideoMiniatureMarkupComponent,
|
||||||
|
@ -30,6 +32,7 @@ type HTMLBuilderFunction = (el: HTMLElement) => HTMLElement
|
||||||
export class CustomMarkupService {
|
export class CustomMarkupService {
|
||||||
private angularBuilders: { [ selector: string ]: AngularBuilderFunction } = {
|
private angularBuilders: { [ selector: string ]: AngularBuilderFunction } = {
|
||||||
'peertube-instance-banner': el => this.instanceBannerBuilder(el),
|
'peertube-instance-banner': el => this.instanceBannerBuilder(el),
|
||||||
|
'peertube-instance-avatar': el => this.instanceAvatarBuilder(el),
|
||||||
'peertube-button': el => this.buttonBuilder(el),
|
'peertube-button': el => this.buttonBuilder(el),
|
||||||
'peertube-video-embed': el => this.embedBuilder(el, 'video'),
|
'peertube-video-embed': el => this.embedBuilder(el, 'video'),
|
||||||
'peertube-playlist-embed': el => this.embedBuilder(el, 'playlist'),
|
'peertube-playlist-embed': el => this.embedBuilder(el, 'playlist'),
|
||||||
|
@ -171,6 +174,19 @@ export class CustomMarkupService {
|
||||||
return { component, loadedPromise }
|
return { component, loadedPromise }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private instanceAvatarBuilder (el: HTMLElement) {
|
||||||
|
const data = el.dataset as InstanceAvatarMarkupData
|
||||||
|
const { component, loadedPromise } = this.dynamicElementService.createElement(InstanceAvatarMarkupComponent)
|
||||||
|
|
||||||
|
const model = {
|
||||||
|
size: this.buildNumber(data.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dynamicElementService.setModel(component, model)
|
||||||
|
|
||||||
|
return { component, loadedPromise }
|
||||||
|
}
|
||||||
|
|
||||||
private videoMiniatureBuilder (el: HTMLElement) {
|
private videoMiniatureBuilder (el: HTMLElement) {
|
||||||
const data = el.dataset as VideoMiniatureMarkupData
|
const data = el.dataset as VideoMiniatureMarkupData
|
||||||
const { component, loadedPromise } = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent)
|
const { component, loadedPromise } = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export * from './button-markup.component'
|
export * from './button-markup.component'
|
||||||
export * from './channel-miniature-markup.component'
|
export * from './channel-miniature-markup.component'
|
||||||
export * from './embed-markup.component'
|
export * from './embed-markup.component'
|
||||||
|
export * from './instance-avatar-markup.component'
|
||||||
export * from './instance-banner-markup.component'
|
export * from './instance-banner-markup.component'
|
||||||
export * from './playlist-miniature-markup.component'
|
export * from './playlist-miniature-markup.component'
|
||||||
export * from './video-miniature-markup.component'
|
export * from './video-miniature-markup.component'
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<my-actor-avatar *ngIf="actor" [actor]="actor" actorType="account" [size]="size"></my-actor-avatar>
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'
|
||||||
|
import { CustomMarkupComponent } from './shared'
|
||||||
|
import { ActorAvatarInput } from '@app/shared/shared-actor-image/actor-avatar.component'
|
||||||
|
import { ServerService } from '@app/core'
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Markup component that creates the img HTML element containing the instance avatar
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-instance-avatar-markup',
|
||||||
|
templateUrl: 'instance-avatar-markup.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class InstanceAvatarMarkupComponent implements OnInit, CustomMarkupComponent {
|
||||||
|
@Input() size: number
|
||||||
|
|
||||||
|
actor: ActorAvatarInput
|
||||||
|
loaded: undefined
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
private server: ServerService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
const { instance } = this.server.getHTMLConfig()
|
||||||
|
|
||||||
|
this.actor = {
|
||||||
|
avatars: instance.avatars,
|
||||||
|
name: this.server.getHTMLConfig().instance.name
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cd.markForCheck()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'
|
||||||
import { CustomMarkupComponent } from './shared'
|
import { CustomMarkupComponent } from './shared'
|
||||||
import { InstanceService } from '@app/shared/shared-instance'
|
import { ServerService } from '@app/core'
|
||||||
import { finalize } from 'rxjs'
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Markup component that creates the img HTML element containing the instance banner
|
* Markup component that creates the img HTML element containing the instance banner
|
||||||
|
@ -15,21 +14,18 @@ import { finalize } from 'rxjs'
|
||||||
export class InstanceBannerMarkupComponent implements OnInit, CustomMarkupComponent {
|
export class InstanceBannerMarkupComponent implements OnInit, CustomMarkupComponent {
|
||||||
@Input() revertHomePaddingTop: boolean
|
@Input() revertHomePaddingTop: boolean
|
||||||
|
|
||||||
@Output() loaded = new EventEmitter<boolean>()
|
|
||||||
|
|
||||||
instanceBannerUrl: string
|
instanceBannerUrl: string
|
||||||
|
loaded: undefined
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
private instance: InstanceService
|
private server: ServerService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.instance.getInstanceBannerUrl()
|
const { instance } = this.server.getHTMLConfig()
|
||||||
.pipe(finalize(() => this.loaded.emit(true)))
|
|
||||||
.subscribe(instanceBannerUrl => {
|
this.instanceBannerUrl = instance.banners?.[0]?.path
|
||||||
this.instanceBannerUrl = instanceBannerUrl
|
|
||||||
this.cd.markForCheck()
|
this.cd.markForCheck()
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
ButtonMarkupComponent,
|
ButtonMarkupComponent,
|
||||||
ChannelMiniatureMarkupComponent,
|
ChannelMiniatureMarkupComponent,
|
||||||
EmbedMarkupComponent,
|
EmbedMarkupComponent,
|
||||||
|
InstanceAvatarMarkupComponent,
|
||||||
InstanceBannerMarkupComponent,
|
InstanceBannerMarkupComponent,
|
||||||
PlaylistMiniatureMarkupComponent,
|
PlaylistMiniatureMarkupComponent,
|
||||||
VideoMiniatureMarkupComponent,
|
VideoMiniatureMarkupComponent,
|
||||||
|
@ -41,7 +42,8 @@ import {
|
||||||
ButtonMarkupComponent,
|
ButtonMarkupComponent,
|
||||||
CustomMarkupHelpComponent,
|
CustomMarkupHelpComponent,
|
||||||
CustomMarkupContainerComponent,
|
CustomMarkupContainerComponent,
|
||||||
InstanceBannerMarkupComponent
|
InstanceBannerMarkupComponent,
|
||||||
|
InstanceAvatarMarkupComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
exports: [
|
exports: [
|
||||||
|
@ -53,7 +55,8 @@ import {
|
||||||
ButtonMarkupComponent,
|
ButtonMarkupComponent,
|
||||||
CustomMarkupHelpComponent,
|
CustomMarkupHelpComponent,
|
||||||
CustomMarkupContainerComponent,
|
CustomMarkupContainerComponent,
|
||||||
InstanceBannerMarkupComponent
|
InstanceBannerMarkupComponent,
|
||||||
|
InstanceAvatarMarkupComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
providers: [
|
providers: [
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Component, Input, OnInit, booleanAttribute } from '@angular/core'
|
import { Component, Input, OnInit, booleanAttribute } from '@angular/core'
|
||||||
import { InstanceService } from './instance.service'
|
import { ServerService } from '@app/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-instance-banner',
|
selector: 'my-instance-banner',
|
||||||
|
@ -10,12 +10,13 @@ export class InstanceBannerComponent implements OnInit {
|
||||||
|
|
||||||
instanceBannerUrl: string
|
instanceBannerUrl: string
|
||||||
|
|
||||||
constructor (private instanceService: InstanceService) {
|
constructor (private server: ServerService) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.instanceService.getInstanceBannerUrl()
|
const { instance } = this.server.getHTMLConfig()
|
||||||
.subscribe(instanceBannerUrl => this.instanceBannerUrl = instanceBannerUrl)
|
|
||||||
|
this.instanceBannerUrl = instance.banners?.[0]?.path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { forkJoin, of } from 'rxjs'
|
import { forkJoin } from 'rxjs'
|
||||||
import { catchError, map, tap } from 'rxjs/operators'
|
import { catchError, map } from 'rxjs/operators'
|
||||||
import { HttpClient } from '@angular/common/http'
|
import { HttpClient } from '@angular/common/http'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { MarkdownService, RestExtractor, ServerService } from '@app/core'
|
import { MarkdownService, RestExtractor, ServerService } from '@app/core'
|
||||||
|
@ -18,8 +18,6 @@ export class InstanceService {
|
||||||
private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config'
|
private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config'
|
||||||
private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server'
|
private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server'
|
||||||
|
|
||||||
private instanceBannerUrl: string
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private authHttp: HttpClient,
|
private authHttp: HttpClient,
|
||||||
private restExtractor: RestExtractor,
|
private restExtractor: RestExtractor,
|
||||||
|
@ -30,29 +28,12 @@ export class InstanceService {
|
||||||
|
|
||||||
getAbout () {
|
getAbout () {
|
||||||
return this.authHttp.get<About>(InstanceService.BASE_CONFIG_URL + '/about')
|
return this.authHttp.get<About>(InstanceService.BASE_CONFIG_URL + '/about')
|
||||||
.pipe(
|
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||||
tap(about => {
|
|
||||||
const banners = about.instance.banners
|
|
||||||
if (banners.length !== 0) this.instanceBannerUrl = banners[0].path
|
|
||||||
}),
|
|
||||||
catchError(res => this.restExtractor.handleError(res))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
getInstanceBannerUrl () {
|
|
||||||
if (this.instanceBannerUrl || this.instanceBannerUrl === null) {
|
|
||||||
return of(this.instanceBannerUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.getAbout()
|
|
||||||
.pipe(map(() => this.instanceBannerUrl))
|
|
||||||
}
|
|
||||||
|
|
||||||
updateInstanceBanner (formData: FormData) {
|
updateInstanceBanner (formData: FormData) {
|
||||||
this.instanceBannerUrl = undefined
|
|
||||||
|
|
||||||
const url = InstanceService.BASE_CONFIG_URL + '/instance-banner/pick'
|
const url = InstanceService.BASE_CONFIG_URL + '/instance-banner/pick'
|
||||||
|
|
||||||
return this.authHttp.post(url, formData)
|
return this.authHttp.post(url, formData)
|
||||||
|
@ -60,8 +41,6 @@ export class InstanceService {
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteInstanceBanner () {
|
deleteInstanceBanner () {
|
||||||
this.instanceBannerUrl = null
|
|
||||||
|
|
||||||
const url = InstanceService.BASE_CONFIG_URL + '/instance-banner'
|
const url = InstanceService.BASE_CONFIG_URL + '/instance-banner'
|
||||||
|
|
||||||
return this.authHttp.delete(url)
|
return this.authHttp.delete(url)
|
||||||
|
@ -70,6 +49,22 @@ export class InstanceService {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
updateInstanceAvatar (formData: FormData) {
|
||||||
|
const url = InstanceService.BASE_CONFIG_URL + '/instance-avatar/pick'
|
||||||
|
|
||||||
|
return this.authHttp.post(url, formData)
|
||||||
|
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteInstanceAvatar () {
|
||||||
|
const url = InstanceService.BASE_CONFIG_URL + '/instance-avatar'
|
||||||
|
|
||||||
|
return this.authHttp.delete(url)
|
||||||
|
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
contactAdministrator (fromEmail: string, fromName: string, subject: string, message: string) {
|
contactAdministrator (fromEmail: string, fromName: string, subject: string, message: string) {
|
||||||
const body = {
|
const body = {
|
||||||
fromEmail,
|
fromEmail,
|
||||||
|
|
|
@ -8,12 +8,12 @@ import {
|
||||||
Input,
|
Input,
|
||||||
LOCALE_ID,
|
LOCALE_ID,
|
||||||
OnInit,
|
OnInit,
|
||||||
Output
|
Output,
|
||||||
|
numberAttribute
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { AuthService, ScreenService, ServerService, User } from '@app/core'
|
import { AuthService, ScreenService, ServerService, User } from '@app/core'
|
||||||
import { HTMLServerConfig, VideoExistInPlaylist, VideoPlaylistType, VideoPrivacy, VideoState } from '@peertube/peertube-models'
|
import { HTMLServerConfig, VideoExistInPlaylist, VideoPlaylistType, VideoPrivacy, VideoState } from '@peertube/peertube-models'
|
||||||
import { LinkType } from '../../../types/link.type'
|
import { LinkType } from '../../../types/link.type'
|
||||||
import { ActorAvatarSize } from '../shared-actor-image/actor-avatar.component'
|
|
||||||
import { Video, VideoService } from '../shared-main'
|
import { Video, VideoService } from '../shared-main'
|
||||||
import { VideoPlaylistService } from '../shared-video-playlist'
|
import { VideoPlaylistService } from '../shared-video-playlist'
|
||||||
import { VideoActionsDisplayType } from './video-actions-dropdown.component'
|
import { VideoActionsDisplayType } from './video-actions-dropdown.component'
|
||||||
|
@ -68,7 +68,7 @@ export class VideoMiniatureComponent implements OnInit {
|
||||||
stats: false
|
stats: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input() actorImageSize: ActorAvatarSize = '40'
|
@Input({ transform: numberAttribute }) actorImageSize = 40
|
||||||
|
|
||||||
@Input() displayAsRow = false
|
@Input() displayAsRow = false
|
||||||
|
|
||||||
|
|
|
@ -62,3 +62,7 @@ export type ContainerMarkupData = {
|
||||||
export type InstanceBannerMarkupData = {
|
export type InstanceBannerMarkupData = {
|
||||||
revertHomePaddingTop?: StringBoolean // default to 'true'
|
revertHomePaddingTop?: StringBoolean // default to 'true'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InstanceAvatarMarkupData = {
|
||||||
|
size: string // size in pixels
|
||||||
|
}
|
||||||
|
|
|
@ -20,5 +20,6 @@ export interface About {
|
||||||
categories: number[]
|
categories: number[]
|
||||||
|
|
||||||
banners: ActorImage[]
|
banners: ActorImage[]
|
||||||
|
avatars: ActorImage[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { ActorImage } from '../index.js'
|
||||||
import { ClientScriptJSON } from '../plugins/plugin-package-json.model.js'
|
import { ClientScriptJSON } from '../plugins/plugin-package-json.model.js'
|
||||||
import { NSFWPolicyType } from '../videos/nsfw-policy.type.js'
|
import { NSFWPolicyType } from '../videos/nsfw-policy.type.js'
|
||||||
import { VideoPrivacyType } from '../videos/video-privacy.enum.js'
|
import { VideoPrivacyType } from '../videos/video-privacy.enum.js'
|
||||||
|
@ -90,6 +91,9 @@ export interface ServerConfig {
|
||||||
javascript: string
|
javascript: string
|
||||||
css: string
|
css: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
avatars: ActorImage[]
|
||||||
|
banners: ActorImage[]
|
||||||
}
|
}
|
||||||
|
|
||||||
search: {
|
search: {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import merge from 'lodash-es/merge.js'
|
import merge from 'lodash-es/merge.js'
|
||||||
import { About, CustomConfig, HttpStatusCode, ServerConfig } from '@peertube/peertube-models'
|
import { About, ActorImageType, ActorImageType_Type, CustomConfig, HttpStatusCode, ServerConfig } from '@peertube/peertube-models'
|
||||||
import { DeepPartial } from '@peertube/peertube-typescript-utils'
|
import { DeepPartial } from '@peertube/peertube-typescript-utils'
|
||||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/abstract-command.js'
|
import { AbstractCommand, OverrideCommandOptions } from '../shared/abstract-command.js'
|
||||||
|
|
||||||
|
@ -365,27 +365,38 @@ export class ConfigCommand extends AbstractCommand {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
updateInstanceBanner (options: OverrideCommandOptions & {
|
updateInstanceImage (options: OverrideCommandOptions & {
|
||||||
fixture: string
|
fixture: string
|
||||||
|
type: ActorImageType_Type
|
||||||
}) {
|
}) {
|
||||||
const { fixture } = options
|
const { fixture, type } = options
|
||||||
|
|
||||||
const path = `/api/v1/config/instance-banner/pick`
|
const path = type === ActorImageType.BANNER
|
||||||
|
? `/api/v1/config/instance-banner/pick`
|
||||||
|
: `/api/v1/config/instance-avatar/pick`
|
||||||
|
|
||||||
return this.updateImageRequest({
|
return this.updateImageRequest({
|
||||||
...options,
|
...options,
|
||||||
|
|
||||||
path,
|
path,
|
||||||
fixture,
|
fixture,
|
||||||
fieldname: 'bannerfile',
|
fieldname: type === ActorImageType.BANNER
|
||||||
|
? 'bannerfile'
|
||||||
|
: 'avatarfile',
|
||||||
|
|
||||||
implicitToken: true,
|
implicitToken: true,
|
||||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteInstanceBanner (options: OverrideCommandOptions = {}) {
|
deleteInstanceImage (options: OverrideCommandOptions & {
|
||||||
const path = `/api/v1/config/instance-banner`
|
type: ActorImageType_Type
|
||||||
|
}) {
|
||||||
|
const suffix = options.type === ActorImageType.BANNER
|
||||||
|
? 'instance-banner'
|
||||||
|
: 'instance-avatar'
|
||||||
|
|
||||||
|
const path = `/api/v1/config/${suffix}`
|
||||||
|
|
||||||
return this.deleteRequest({
|
return this.deleteRequest({
|
||||||
...options,
|
...options,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
import merge from 'lodash-es/merge.js'
|
import merge from 'lodash-es/merge.js'
|
||||||
import { omit } from '@peertube/peertube-core-utils'
|
import { omit } from '@peertube/peertube-core-utils'
|
||||||
import { CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
|
import { ActorImageType, CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
createSingleServer,
|
createSingleServer,
|
||||||
|
@ -421,6 +421,7 @@ describe('Test config API validators', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('When deleting the configuration', function () {
|
describe('When deleting the configuration', function () {
|
||||||
|
|
||||||
it('Should fail without token', async function () {
|
it('Should fail without token', async function () {
|
||||||
await makeDeleteRequest({
|
await makeDeleteRequest({
|
||||||
url: server.url,
|
url: server.url,
|
||||||
|
@ -439,17 +440,23 @@ describe('Test config API validators', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Updating instance banner', function () {
|
describe('Updating instance image', function () {
|
||||||
const path = '/api/v1/config/instance-banner/pick'
|
const toTest = [
|
||||||
|
{ path: '/api/v1/config/instance-banner/pick', attachName: 'bannerfile' },
|
||||||
|
{ path: '/api/v1/config/instance-avatar/pick', attachName: 'avatarfile' }
|
||||||
|
]
|
||||||
|
|
||||||
it('Should fail with an incorrect input file', async function () {
|
it('Should fail with an incorrect input file', async function () {
|
||||||
const attaches = { bannerfile: buildAbsoluteFixturePath('video_short.mp4') }
|
for (const { attachName, path } of toTest) {
|
||||||
|
const attaches = { [attachName]: buildAbsoluteFixturePath('video_short.mp4') }
|
||||||
|
|
||||||
await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields: {}, attaches })
|
await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields: {}, attaches })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with a big file', async function () {
|
it('Should fail with a big file', async function () {
|
||||||
const attaches = { bannerfile: buildAbsoluteFixturePath('avatar-big.png') }
|
for (const { attachName, path } of toTest) {
|
||||||
|
const attaches = { [attachName]: buildAbsoluteFixturePath('avatar-big.png') }
|
||||||
|
|
||||||
await makeUploadRequest({
|
await makeUploadRequest({
|
||||||
url: server.url,
|
url: server.url,
|
||||||
|
@ -459,10 +466,12 @@ describe('Test config API validators', function () {
|
||||||
attaches,
|
attaches,
|
||||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail without token', async function () {
|
it('Should fail without token', async function () {
|
||||||
const attaches = { bannerfile: buildAbsoluteFixturePath('avatar.png') }
|
for (const { attachName, path } of toTest) {
|
||||||
|
const attaches = { [attachName]: buildAbsoluteFixturePath('avatar.png') }
|
||||||
|
|
||||||
await makeUploadRequest({
|
await makeUploadRequest({
|
||||||
url: server.url,
|
url: server.url,
|
||||||
|
@ -471,10 +480,12 @@ describe('Test config API validators', function () {
|
||||||
attaches,
|
attaches,
|
||||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail without the appropriate rights', async function () {
|
it('Should fail without the appropriate rights', async function () {
|
||||||
const attaches = { bannerfile: buildAbsoluteFixturePath('avatar.png') }
|
for (const { attachName, path } of toTest) {
|
||||||
|
const attaches = { [attachName]: buildAbsoluteFixturePath('avatar.png') }
|
||||||
|
|
||||||
await makeUploadRequest({
|
await makeUploadRequest({
|
||||||
url: server.url,
|
url: server.url,
|
||||||
|
@ -484,10 +495,12 @@ describe('Test config API validators', function () {
|
||||||
attaches,
|
attaches,
|
||||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should succeed with the correct params', async function () {
|
it('Should succeed with the correct params', async function () {
|
||||||
const attaches = { bannerfile: buildAbsoluteFixturePath('avatar.png') }
|
for (const { attachName, path } of toTest) {
|
||||||
|
const attaches = { [attachName]: buildAbsoluteFixturePath('avatar.png') }
|
||||||
|
|
||||||
await makeUploadRequest({
|
await makeUploadRequest({
|
||||||
url: server.url,
|
url: server.url,
|
||||||
|
@ -497,21 +510,29 @@ describe('Test config API validators', function () {
|
||||||
attaches,
|
attaches,
|
||||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Deleting instance banner', function () {
|
describe('Deleting instance image', function () {
|
||||||
|
const types = [ ActorImageType.BANNER, ActorImageType.AVATAR ]
|
||||||
|
|
||||||
it('Should fail without token', async function () {
|
it('Should fail without token', async function () {
|
||||||
await server.config.deleteInstanceBanner({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
for (const type of types) {
|
||||||
|
await server.config.deleteInstanceImage({ type, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail without the appropriate rights', async function () {
|
it('Should fail without the appropriate rights', async function () {
|
||||||
await server.config.deleteInstanceBanner({ token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
for (const type of types) {
|
||||||
|
await server.config.deleteInstanceImage({ type, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should succeed with the correct params', async function () {
|
it('Should succeed with the correct params', async function () {
|
||||||
await server.config.deleteInstanceBanner()
|
for (const type of types) {
|
||||||
|
await server.config.deleteInstanceImage({ type })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { parallelTests } from '@peertube/peertube-node-utils'
|
import { parallelTests } from '@peertube/peertube-node-utils'
|
||||||
import { CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
|
import { ActorImageType, CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
createSingleServer,
|
createSingleServer,
|
||||||
|
@ -11,7 +11,7 @@ import {
|
||||||
PeerTubeServer,
|
PeerTubeServer,
|
||||||
setAccessTokensToServers
|
setAccessTokensToServers
|
||||||
} from '@peertube/peertube-server-commands'
|
} from '@peertube/peertube-server-commands'
|
||||||
import { testFileExistsOrNot, testImage } from '@tests/shared/checks.js'
|
import { testFileExistsOrNot, testImage, testImageSize } from '@tests/shared/checks.js'
|
||||||
import { basename } from 'path'
|
import { basename } from 'path'
|
||||||
|
|
||||||
function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
|
function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
|
||||||
|
@ -521,7 +521,6 @@ describe('Test static config', function () {
|
||||||
|
|
||||||
describe('Test config', function () {
|
describe('Test config', function () {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
let bannerPath: string
|
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(30000)
|
this.timeout(30000)
|
||||||
|
@ -530,6 +529,8 @@ describe('Test config', function () {
|
||||||
await setAccessTokensToServers([ server ])
|
await setAccessTokensToServers([ server ])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Config keys', function () {
|
||||||
|
|
||||||
it('Should have the correct default config', async function () {
|
it('Should have the correct default config', async function () {
|
||||||
const data = await server.config.getConfig()
|
const data = await server.config.getConfig()
|
||||||
|
|
||||||
|
@ -641,32 +642,6 @@ describe('Test config', function () {
|
||||||
expect(instance.banners).to.have.lengthOf(0)
|
expect(instance.banners).to.have.lengthOf(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should update instance banner', async function () {
|
|
||||||
await server.config.updateInstanceBanner({ fixture: 'banner.jpg' })
|
|
||||||
|
|
||||||
const { instance } = await server.config.getAbout()
|
|
||||||
|
|
||||||
expect(instance.banners).to.have.lengthOf(1)
|
|
||||||
|
|
||||||
bannerPath = instance.banners[0].path
|
|
||||||
await testImage(server.url, 'banner-resized', bannerPath)
|
|
||||||
await testFileExistsOrNot(server, 'avatars', basename(bannerPath), true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should re-update an existing instance banner', async function () {
|
|
||||||
await server.config.updateInstanceBanner({ fixture: 'banner.jpg' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should remove instance banner', async function () {
|
|
||||||
await server.config.deleteInstanceBanner()
|
|
||||||
|
|
||||||
const { instance } = await server.config.getAbout()
|
|
||||||
|
|
||||||
expect(instance.banners).to.have.lengthOf(0)
|
|
||||||
|
|
||||||
await testFileExistsOrNot(server, 'avatars', basename(bannerPath), false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should remove the custom configuration', async function () {
|
it('Should remove the custom configuration', async function () {
|
||||||
await server.config.deleteCustomConfig()
|
await server.config.deleteCustomConfig()
|
||||||
|
|
||||||
|
@ -709,6 +684,83 @@ describe('Test config', function () {
|
||||||
expect(res.headers['x-powered-by']).to.not.exist
|
expect(res.headers['x-powered-by']).to.not.exist
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Image files', function () {
|
||||||
|
|
||||||
|
async function checkAndGetServerImages () {
|
||||||
|
const { instance } = await server.config.getAbout()
|
||||||
|
const htmlConfig = await server.config.getIndexHTMLConfig()
|
||||||
|
const serverConfig = await server.config.getIndexHTMLConfig()
|
||||||
|
|
||||||
|
expect(instance.avatars).to.deep.equal(htmlConfig.instance.avatars)
|
||||||
|
expect(serverConfig.instance.avatars).to.deep.equal(htmlConfig.instance.avatars)
|
||||||
|
|
||||||
|
expect(instance.banners).to.deep.equal(htmlConfig.instance.banners)
|
||||||
|
expect(serverConfig.instance.banners).to.deep.equal(htmlConfig.instance.banners)
|
||||||
|
|
||||||
|
return htmlConfig.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Banner', function () {
|
||||||
|
let bannerPath: string
|
||||||
|
|
||||||
|
it('Should update instance banner', async function () {
|
||||||
|
await server.config.updateInstanceImage({ type: ActorImageType.BANNER, fixture: 'banner.jpg' })
|
||||||
|
|
||||||
|
const { banners } = await checkAndGetServerImages()
|
||||||
|
|
||||||
|
expect(banners).to.have.lengthOf(1)
|
||||||
|
|
||||||
|
bannerPath = banners[0].path
|
||||||
|
await testImage(server.url, 'banner-resized', bannerPath)
|
||||||
|
await testFileExistsOrNot(server, 'avatars', basename(bannerPath), true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should re-update an existing instance banner', async function () {
|
||||||
|
await server.config.updateInstanceImage({ type: ActorImageType.BANNER, fixture: 'banner.jpg' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should remove instance banner', async function () {
|
||||||
|
await server.config.deleteInstanceImage({ type: ActorImageType.BANNER })
|
||||||
|
|
||||||
|
const { banners } = await checkAndGetServerImages()
|
||||||
|
expect(banners).to.have.lengthOf(0)
|
||||||
|
|
||||||
|
await testFileExistsOrNot(server, 'avatars', basename(bannerPath), false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Avatar', function () {
|
||||||
|
let avatarPath: string
|
||||||
|
|
||||||
|
it('Should update instance avatar', async function () {
|
||||||
|
for (const extension of [ '.png', '.gif' ]) {
|
||||||
|
const fixture = 'avatar' + extension
|
||||||
|
|
||||||
|
await server.config.updateInstanceImage({ type: ActorImageType.AVATAR, fixture })
|
||||||
|
|
||||||
|
const { avatars } = await checkAndGetServerImages()
|
||||||
|
|
||||||
|
for (const avatar of avatars) {
|
||||||
|
await testImageSize(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatar.path, extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
avatarPath = avatars[0].path
|
||||||
|
await testFileExistsOrNot(server, 'avatars', basename(avatarPath), true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should remove instance banner', async function () {
|
||||||
|
await server.config.deleteInstanceImage({ type: ActorImageType.AVATAR })
|
||||||
|
|
||||||
|
const { avatars } = await checkAndGetServerImages()
|
||||||
|
expect(avatars).to.have.lengthOf(0)
|
||||||
|
|
||||||
|
await testFileExistsOrNot(server, 'avatars', basename(avatarPath), false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
await cleanupTests([ server ])
|
await cleanupTests([ server ])
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { remove, writeJSON } from 'fs-extra/esm'
|
||||||
import snakeCase from 'lodash-es/snakeCase.js'
|
import snakeCase from 'lodash-es/snakeCase.js'
|
||||||
import validator from 'validator'
|
import validator from 'validator'
|
||||||
import { ServerConfigManager } from '@server/lib/server-config-manager.js'
|
import { ServerConfigManager } from '@server/lib/server-config-manager.js'
|
||||||
import { About, ActorImageType, CustomConfig, HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
import { About, ActorImageType, ActorImageType_Type, CustomConfig, HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||||
import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger.js'
|
import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger.js'
|
||||||
import { objectConverter } from '../../helpers/core-utils.js'
|
import { objectConverter } from '../../helpers/core-utils.js'
|
||||||
import { CONFIG, reloadConfig } from '../../initializers/config.js'
|
import { CONFIG, reloadConfig } from '../../initializers/config.js'
|
||||||
|
@ -14,6 +14,7 @@ import {
|
||||||
authenticate,
|
authenticate,
|
||||||
ensureUserHasRight,
|
ensureUserHasRight,
|
||||||
openapiOperationDoc,
|
openapiOperationDoc,
|
||||||
|
updateAvatarValidator,
|
||||||
updateBannerValidator
|
updateBannerValidator
|
||||||
} from '../../middlewares/index.js'
|
} from '../../middlewares/index.js'
|
||||||
import { customConfigUpdateValidator, ensureConfigIsEditable } from '../../middlewares/validators/config.js'
|
import { customConfigUpdateValidator, ensureConfigIsEditable } from '../../middlewares/validators/config.js'
|
||||||
|
@ -63,18 +64,36 @@ configRouter.delete('/custom',
|
||||||
asyncMiddleware(deleteCustomConfig)
|
asyncMiddleware(deleteCustomConfig)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
configRouter.post('/instance-banner/pick',
|
configRouter.post('/instance-banner/pick',
|
||||||
authenticate,
|
authenticate,
|
||||||
createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT),
|
createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT),
|
||||||
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
|
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
|
||||||
updateBannerValidator,
|
updateBannerValidator,
|
||||||
asyncMiddleware(updateInstanceBanner)
|
asyncMiddleware(updateInstanceImageFactory(ActorImageType.BANNER))
|
||||||
)
|
)
|
||||||
|
|
||||||
configRouter.delete('/instance-banner',
|
configRouter.delete('/instance-banner',
|
||||||
authenticate,
|
authenticate,
|
||||||
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
|
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
|
||||||
asyncMiddleware(deleteInstanceBanner)
|
asyncMiddleware(deleteInstanceImageFactory(ActorImageType.BANNER))
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
configRouter.post('/instance-avatar/pick',
|
||||||
|
authenticate,
|
||||||
|
createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT),
|
||||||
|
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
|
||||||
|
updateAvatarValidator,
|
||||||
|
asyncMiddleware(updateInstanceImageFactory(ActorImageType.AVATAR))
|
||||||
|
)
|
||||||
|
|
||||||
|
configRouter.delete('/instance-avatar',
|
||||||
|
authenticate,
|
||||||
|
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
|
||||||
|
asyncMiddleware(deleteInstanceImageFactory(ActorImageType.AVATAR))
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
@ -86,7 +105,7 @@ async function getConfig (req: express.Request, res: express.Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAbout (req: express.Request, res: express.Response) {
|
async function getAbout (req: express.Request, res: express.Response) {
|
||||||
const banners = await ActorImageModel.listByActor(await getServerActor(), ActorImageType.BANNER)
|
const { avatars, banners } = await ActorImageModel.listServerActorImages()
|
||||||
|
|
||||||
const about: About = {
|
const about: About = {
|
||||||
instance: {
|
instance: {
|
||||||
|
@ -107,7 +126,8 @@ async function getAbout (req: express.Request, res: express.Response) {
|
||||||
languages: CONFIG.INSTANCE.LANGUAGES,
|
languages: CONFIG.INSTANCE.LANGUAGES,
|
||||||
categories: CONFIG.INSTANCE.CATEGORIES,
|
categories: CONFIG.INSTANCE.CATEGORIES,
|
||||||
|
|
||||||
banners: banners.map(b => b.toFormattedJSON())
|
banners: banners.map(b => b.toFormattedJSON()),
|
||||||
|
avatars: avatars.map(a => a.toFormattedJSON())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,29 +175,47 @@ async function updateCustomConfig (req: express.Request, res: express.Response)
|
||||||
return res.json(data)
|
return res.json(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateInstanceBanner (req: express.Request, res: express.Response) {
|
// ---------------------------------------------------------------------------
|
||||||
const bannerPhysicalFile = req.files['bannerfile'][0]
|
|
||||||
|
|
||||||
const serverActor = await getServerActor()
|
function updateInstanceImageFactory (imageType: ActorImageType_Type) {
|
||||||
serverActor.Banners = await ActorImageModel.listByActor(serverActor, ActorImageType.BANNER) // Reload banners from DB
|
return async (req: express.Request, res: express.Response) => {
|
||||||
|
const field = imageType === ActorImageType.BANNER
|
||||||
|
? 'bannerfile'
|
||||||
|
: 'avatarfile'
|
||||||
|
|
||||||
|
const imagePhysicalFile = req.files[field][0]
|
||||||
|
|
||||||
await updateLocalActorImageFiles({
|
await updateLocalActorImageFiles({
|
||||||
accountOrChannel: serverActor.Account,
|
accountOrChannel: (await getServerActorWithUpdatedImages(imageType)).Account,
|
||||||
imagePhysicalFile: bannerPhysicalFile,
|
imagePhysicalFile,
|
||||||
type: ActorImageType.BANNER,
|
type: imageType,
|
||||||
sendActorUpdate: false
|
sendActorUpdate: false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ClientHtml.invalidateCache()
|
||||||
|
|
||||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteInstanceBanner (req: express.Request, res: express.Response) {
|
function deleteInstanceImageFactory (imageType: ActorImageType_Type) {
|
||||||
const serverActor = await getServerActor()
|
return async (req: express.Request, res: express.Response) => {
|
||||||
serverActor.Banners = await ActorImageModel.listByActor(serverActor, ActorImageType.BANNER) // Reload banners from DB
|
await deleteLocalActorImageFile((await getServerActorWithUpdatedImages(imageType)).Account, imageType)
|
||||||
|
|
||||||
await deleteLocalActorImageFile(serverActor.Account, ActorImageType.BANNER)
|
ClientHtml.invalidateCache()
|
||||||
|
|
||||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getServerActorWithUpdatedImages (imageType: ActorImageType_Type) {
|
||||||
|
const serverActor = await getServerActor()
|
||||||
|
const updatedImages = await ActorImageModel.listByActor(serverActor, imageType) // Reload images from DB
|
||||||
|
|
||||||
|
if (imageType === ActorImageType.BANNER) serverActor.Banners = updatedImages
|
||||||
|
else serverActor.Avatars = updatedImages
|
||||||
|
|
||||||
|
return serverActor
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { Hooks } from './plugins/hooks.js'
|
||||||
import { PluginManager } from './plugins/plugin-manager.js'
|
import { PluginManager } from './plugins/plugin-manager.js'
|
||||||
import { getThemeOrDefault } from './plugins/theme-utils.js'
|
import { getThemeOrDefault } from './plugins/theme-utils.js'
|
||||||
import { VideoTranscodingProfilesManager } from './transcoding/default-transcoding-profiles.js'
|
import { VideoTranscodingProfilesManager } from './transcoding/default-transcoding-profiles.js'
|
||||||
|
import { ActorImageModel } from '@server/models/actor/actor-image.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -48,6 +49,8 @@ class ServerConfigManager {
|
||||||
|
|
||||||
const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
|
const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
|
||||||
|
|
||||||
|
const { avatars, banners } = await ActorImageModel.listServerActorImages()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
client: {
|
client: {
|
||||||
videos: {
|
videos: {
|
||||||
|
@ -100,7 +103,9 @@ class ServerConfigManager {
|
||||||
customizations: {
|
customizations: {
|
||||||
javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
|
javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
|
||||||
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
|
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
|
||||||
}
|
},
|
||||||
|
avatars: avatars.map(a => a.toFormattedJSON()),
|
||||||
|
banners: banners.map(b => b.toFormattedJSON())
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
remoteUri: {
|
remoteUri: {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { CONFIG } from '../../initializers/config.js'
|
||||||
import { LAZY_STATIC_PATHS, MIMETYPES, WEBSERVER } from '../../initializers/constants.js'
|
import { LAZY_STATIC_PATHS, MIMETYPES, WEBSERVER } from '../../initializers/constants.js'
|
||||||
import { SequelizeModel, buildSQLAttributes, throwIfNotValid } from '../shared/index.js'
|
import { SequelizeModel, buildSQLAttributes, throwIfNotValid } from '../shared/index.js'
|
||||||
import { ActorModel } from './actor.js'
|
import { ActorModel } from './actor.js'
|
||||||
|
import { getServerActor } from '../application/application.js'
|
||||||
|
|
||||||
@Table({
|
@Table({
|
||||||
tableName: 'actorImage',
|
tableName: 'actorImage',
|
||||||
|
@ -123,6 +124,15 @@ export class ActorImageModel extends SequelizeModel<ActorImageModel> {
|
||||||
return ActorImageModel.findAll(query)
|
return ActorImageModel.findAll(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async listServerActorImages () {
|
||||||
|
const serverActor = await getServerActor()
|
||||||
|
const promises = [ ActorImageType.AVATAR, ActorImageType.BANNER ].map(type => ActorImageModel.listByActor(serverActor, type))
|
||||||
|
|
||||||
|
const [ avatars, banners ] = await Promise.all(promises)
|
||||||
|
|
||||||
|
return { avatars, banners }
|
||||||
|
}
|
||||||
|
|
||||||
static getImageUrl (image: MActorImage) {
|
static getImageUrl (image: MActorImage) {
|
||||||
if (!image) return undefined
|
if (!image) return undefined
|
||||||
|
|
||||||
|
|
|
@ -953,17 +953,8 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- Config
|
- Config
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'204':
|
||||||
description: successful operation
|
description: successful operation
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
banners:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/ActorImage'
|
|
||||||
'413':
|
'413':
|
||||||
description: image file too large
|
description: image file too large
|
||||||
headers:
|
headers:
|
||||||
|
@ -998,6 +989,51 @@ paths:
|
||||||
'204':
|
'204':
|
||||||
description: successful operation
|
description: successful operation
|
||||||
|
|
||||||
|
/api/v1/config/instance-avatar/pick:
|
||||||
|
post:
|
||||||
|
summary: Update instance avatar
|
||||||
|
security:
|
||||||
|
- OAuth2:
|
||||||
|
- admin
|
||||||
|
tags:
|
||||||
|
- Config
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: successful operation
|
||||||
|
'413':
|
||||||
|
description: image file too large
|
||||||
|
headers:
|
||||||
|
X-File-Maximum-Size:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: Nginx size
|
||||||
|
description: Maximum file size for the avatar
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
avatarfile:
|
||||||
|
description: The file to upload.
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
encoding:
|
||||||
|
avatarfile:
|
||||||
|
contentType: image/png, image/jpeg
|
||||||
|
|
||||||
|
'/api/v1/config/instance-avatar':
|
||||||
|
delete:
|
||||||
|
summary: Delete instance avatar
|
||||||
|
security:
|
||||||
|
- OAuth2:
|
||||||
|
- admin
|
||||||
|
tags:
|
||||||
|
- Config
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: successful operation
|
||||||
|
|
||||||
/api/v1/custom-pages/homepage/instance:
|
/api/v1/custom-pages/homepage/instance:
|
||||||
get:
|
get:
|
||||||
summary: Get instance custom homepage
|
summary: Get instance custom homepage
|
||||||
|
@ -8251,6 +8287,14 @@ components:
|
||||||
type: string
|
type: string
|
||||||
css:
|
css:
|
||||||
type: string
|
type: string
|
||||||
|
avatars:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ActorImage'
|
||||||
|
banners:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ActorImage'
|
||||||
search:
|
search:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -8613,6 +8657,36 @@ components:
|
||||||
type: string
|
type: string
|
||||||
terms:
|
terms:
|
||||||
type: string
|
type: string
|
||||||
|
codeOfConduct:
|
||||||
|
type: string
|
||||||
|
hardwareInformation:
|
||||||
|
type: string
|
||||||
|
creationReason:
|
||||||
|
type: string
|
||||||
|
moderationInformation:
|
||||||
|
type: string
|
||||||
|
administrator:
|
||||||
|
type: string
|
||||||
|
maintenanceLifetime:
|
||||||
|
type: string
|
||||||
|
businessModel:
|
||||||
|
type: string
|
||||||
|
languages:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
categories:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
avatars:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ActorImage'
|
||||||
|
banners:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ActorImage'
|
||||||
ServerConfigCustom:
|
ServerConfigCustom:
|
||||||
properties:
|
properties:
|
||||||
instance:
|
instance:
|
||||||
|
|
Loading…
Reference in a new issue