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 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">
|
||||
<label i18n for="bannerfile">Banner</label>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -2,10 +2,11 @@ import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
|||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
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 { genericUploadErrorHandler } from '@app/helpers'
|
||||
import { InstanceService } from '@app/shared/shared-instance'
|
||||
import { ActorImage, HTMLServerConfig } from '@peertube/peertube-models'
|
||||
|
||||
@Component({
|
||||
selector: 'my-edit-instance-information',
|
||||
|
@ -20,17 +21,27 @@ export class EditInstanceInformationComponent implements OnInit {
|
|||
@Input() categoryItems: SelectOptionsItem[] = []
|
||||
|
||||
instanceBannerUrl: string
|
||||
instanceAvatars: ActorImage[] = []
|
||||
|
||||
private serverConfig: HTMLServerConfig
|
||||
|
||||
constructor (
|
||||
private customMarkup: CustomMarkupService,
|
||||
private notifier: Notifier,
|
||||
private instanceService: InstanceService
|
||||
private instanceService: InstanceService,
|
||||
private server: ServerService
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
get instanceName () {
|
||||
return this.server.getHTMLConfig().instance.name
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.resetBannerUrl()
|
||||
this.serverConfig = this.server.getHTMLConfig()
|
||||
|
||||
this.updateActorImages()
|
||||
}
|
||||
|
||||
getCustomMarkdownRenderer () {
|
||||
|
@ -39,15 +50,15 @@ export class EditInstanceInformationComponent implements OnInit {
|
|||
|
||||
onBannerChange (formData: FormData) {
|
||||
this.instanceService.updateInstanceBanner(formData)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Banner changed.`)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
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 })
|
||||
})
|
||||
}
|
||||
|
||||
onBannerDelete () {
|
||||
|
@ -56,17 +67,51 @@ export class EditInstanceInformationComponent implements OnInit {
|
|||
next: () => {
|
||||
this.notifier.success($localize`Banner deleted.`)
|
||||
|
||||
this.resetBannerUrl()
|
||||
this.resetActorImages()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
private resetBannerUrl () {
|
||||
this.instanceService.getInstanceBannerUrl()
|
||||
.subscribe(instanceBannerUrl => {
|
||||
this.instanceBannerUrl = instanceBannerUrl
|
||||
onAvatarChange (formData: FormData) {
|
||||
this.instanceService.updateInstanceAvatar(formData)
|
||||
.subscribe({
|
||||
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="col-12 col-lg-4 col-xl-3">
|
||||
<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">
|
||||
<my-actor-avatar-edit [actor]="user.account" [editable]="false" [displaySubscribers]="false" [displayUsername]="false"></my-actor-avatar-edit>
|
||||
</div>
|
||||
|
||||
@if (isCreation()) {
|
||||
<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 class="col-12 col-lg-8 col-xl-9">
|
||||
|
|
|
@ -16,9 +16,10 @@
|
|||
></my-actor-banner-edit>
|
||||
|
||||
<my-actor-avatar-edit
|
||||
*ngIf="videoChannel" [previewImage]="isCreation()" class="d-block mb-4"
|
||||
[actor]="videoChannel" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
|
||||
[displayUsername]="!isCreation()" [displaySubscribers]="!isCreation()"
|
||||
*ngIf="videoChannel" class="d-block mb-4" actorType="channel"
|
||||
[displayName]="videoChannel.displayName" [previewImage]="isCreation()" [avatars]="videoChannel.avatars"
|
||||
[username]="!isCreation() && videoChannel.displayName" [subscribers]="!isCreation() && videoChannel.followersCount"
|
||||
(avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
|
||||
></my-actor-avatar-edit>
|
||||
|
||||
<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-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>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="actor" *ngIf="actor">
|
||||
<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">
|
||||
<my-global-icon iconName="upload"></my-global-icon>
|
||||
|
@ -31,8 +31,8 @@
|
|||
</div>
|
||||
|
||||
<div class="actor-info">
|
||||
<div class="actor-info-display-name">{{ actor.displayName }}</div>
|
||||
<div *ngIf="displayUsername" class="actor-info-username">{{ actor.name }}</div>
|
||||
<div *ngIf="displaySubscribers" i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div>
|
||||
<div *ngIf="displayName" class="actor-info-display-name">{{ displayName }}</div>
|
||||
<div *ngIf="displayUsername && username" class="actor-info-username">{{ username }}</div>
|
||||
<div *ngIf="subscribers" i18n class="actor-info-followers">{{ subscribers }} subscribers</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 { Account, VideoChannel } from '@app/shared/shared-main'
|
||||
import { getBytes } from '@root-helpers/bytes'
|
||||
import { imageToDataURL } from '@root-helpers/images'
|
||||
import { ActorAvatarInput } from '../shared-actor-image/actor-avatar.component'
|
||||
import { ActorImage } from '@peertube/peertube-models'
|
||||
|
||||
@Component({
|
||||
selector: 'my-actor-avatar-edit',
|
||||
|
@ -12,14 +13,19 @@ import { imageToDataURL } from '@root-helpers/images'
|
|||
'./actor-avatar-edit.component.scss'
|
||||
]
|
||||
})
|
||||
export class ActorAvatarEditComponent implements OnInit {
|
||||
export class ActorAvatarEditComponent implements OnInit, OnChanges {
|
||||
@ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement>
|
||||
|
||||
@Input() actor: VideoChannel | Account
|
||||
@Input() editable = true
|
||||
@Input() displaySubscribers = true
|
||||
@Input() displayUsername = true
|
||||
@Input() previewImage = false
|
||||
@Input({ required: true }) actorType: 'channel' | 'account'
|
||||
@Input({ required: true }) avatars: ActorImage[]
|
||||
@Input({ required: true }) username: string
|
||||
|
||||
@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() avatarDelete = new EventEmitter<void>()
|
||||
|
@ -30,6 +36,8 @@ export class ActorAvatarEditComponent implements OnInit {
|
|||
|
||||
preview: string
|
||||
|
||||
actor: ActorAvatarInput
|
||||
|
||||
constructor (
|
||||
private serverService: ServerService,
|
||||
private notifier: Notifier
|
||||
|
@ -41,8 +49,14 @@ export class ActorAvatarEditComponent implements OnInit {
|
|||
this.maxAvatarSize = config.avatar.file.size.max
|
||||
this.avatarExtensions = config.avatar.file.extensions.join(', ')
|
||||
|
||||
this.avatarFormat = `${$localize`max size`}: 192*192px, ` +
|
||||
`${getBytes(this.maxAvatarSize)} ${$localize`extensions`}: ${this.avatarExtensions}`
|
||||
this.avatarFormat = $localize`max size: 192*192px, ${getBytes(this.maxAvatarSize)} extensions: ${this.avatarExtensions}`
|
||||
}
|
||||
|
||||
ngOnChanges () {
|
||||
this.actor = {
|
||||
avatars: this.avatars,
|
||||
name: this.username
|
||||
}
|
||||
}
|
||||
|
||||
onAvatarChange (input: HTMLInputElement) {
|
||||
|
@ -69,12 +83,6 @@ export class ActorAvatarEditComponent implements OnInit {
|
|||
}
|
||||
|
||||
hasAvatar () {
|
||||
return !!this.preview || this.actor.avatars.length !== 0
|
||||
}
|
||||
|
||||
getActorType () {
|
||||
if ((this.actor as VideoChannel).ownerAccount) return 'channel'
|
||||
|
||||
return 'account'
|
||||
return !!this.preview || this.avatars.length !== 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<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>
|
||||
</div>
|
||||
|
||||
<div *ngIf="displayPlaceholder()" [ngClass]="classes"></div>
|
||||
<div #avatarEl *ngIf="displayPlaceholder()" [ngClass]="classes"></div>
|
||||
</ng-template>
|
||||
|
||||
<a *ngIf="actor && href" [href]="href" target="_blank" rel="noopener noreferrer" [title]="title">
|
||||
|
|
|
@ -2,9 +2,7 @@
|
|||
@use '_mixins' as *;
|
||||
|
||||
.avatar {
|
||||
--avatarSize: 100%;
|
||||
--initialFontSize: 22px;
|
||||
|
||||
// Defined in component
|
||||
width: var(--avatarSize);
|
||||
height: 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 {
|
||||
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 { Account } from '../shared-main/account/account.model'
|
||||
import { objectKeysTyped } from '@peertube/peertube-core-utils'
|
||||
|
||||
type ActorInput = {
|
||||
export type ActorAvatarInput = {
|
||||
name: 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({
|
||||
selector: 'my-actor-avatar',
|
||||
styleUrls: [ './actor-avatar.component.scss' ],
|
||||
templateUrl: './actor-avatar.component.html'
|
||||
})
|
||||
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() previewImage: string
|
||||
|
||||
@Input() size: ActorAvatarSize
|
||||
@Input({ transform: numberAttribute }) size: number
|
||||
|
||||
// Use an external link
|
||||
@Input() href: string
|
||||
// Use routerLink
|
||||
@Input() internalHref: string | any[]
|
||||
|
||||
private _title: string
|
||||
|
||||
@Input() set title (value) {
|
||||
this._title = value
|
||||
}
|
||||
|
@ -47,6 +46,10 @@ export class ActorAvatarComponent implements OnInit, OnChanges {
|
|||
defaultAvatarUrl: string
|
||||
avatarUrl: string
|
||||
|
||||
constructor (private el: ElementRef) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.buildDefaultAvatarUrl()
|
||||
|
||||
|
@ -60,10 +63,21 @@ export class ActorAvatarComponent implements OnInit, OnChanges {
|
|||
}
|
||||
|
||||
private buildClasses () {
|
||||
let avatarSize = '100%'
|
||||
let initialFontSize = '22px'
|
||||
|
||||
this.classes = [ 'avatar' ]
|
||||
|
||||
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()) {
|
||||
|
@ -77,6 +91,10 @@ export class ActorAvatarComponent implements OnInit, OnChanges {
|
|||
this.classes.push('initial')
|
||||
this.classes.push(this.getColorTheme())
|
||||
}
|
||||
|
||||
const elStyle = (this.el.nativeElement as HTMLElement).style
|
||||
elStyle.setProperty('--avatarSize', avatarSize)
|
||||
elStyle.setProperty('--initialFontSize', initialFontSize)
|
||||
}
|
||||
|
||||
private buildDefaultAvatarUrl () {
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
ChannelMiniatureMarkupData,
|
||||
ContainerMarkupData,
|
||||
EmbedMarkupData,
|
||||
InstanceAvatarMarkupData,
|
||||
InstanceBannerMarkupData,
|
||||
PlaylistMiniatureMarkupData,
|
||||
VideoMiniatureMarkupData,
|
||||
|
@ -16,6 +17,7 @@ import {
|
|||
ButtonMarkupComponent,
|
||||
ChannelMiniatureMarkupComponent,
|
||||
EmbedMarkupComponent,
|
||||
InstanceAvatarMarkupComponent,
|
||||
InstanceBannerMarkupComponent,
|
||||
PlaylistMiniatureMarkupComponent,
|
||||
VideoMiniatureMarkupComponent,
|
||||
|
@ -30,6 +32,7 @@ type HTMLBuilderFunction = (el: HTMLElement) => HTMLElement
|
|||
export class CustomMarkupService {
|
||||
private angularBuilders: { [ selector: string ]: AngularBuilderFunction } = {
|
||||
'peertube-instance-banner': el => this.instanceBannerBuilder(el),
|
||||
'peertube-instance-avatar': el => this.instanceAvatarBuilder(el),
|
||||
'peertube-button': el => this.buttonBuilder(el),
|
||||
'peertube-video-embed': el => this.embedBuilder(el, 'video'),
|
||||
'peertube-playlist-embed': el => this.embedBuilder(el, 'playlist'),
|
||||
|
@ -171,6 +174,19 @@ export class CustomMarkupService {
|
|||
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) {
|
||||
const data = el.dataset as VideoMiniatureMarkupData
|
||||
const { component, loadedPromise } = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export * from './button-markup.component'
|
||||
export * from './channel-miniature-markup.component'
|
||||
export * from './embed-markup.component'
|
||||
export * from './instance-avatar-markup.component'
|
||||
export * from './instance-banner-markup.component'
|
||||
export * from './playlist-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 { InstanceService } from '@app/shared/shared-instance'
|
||||
import { finalize } from 'rxjs'
|
||||
import { ServerService } from '@app/core'
|
||||
|
||||
/*
|
||||
* 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 {
|
||||
@Input() revertHomePaddingTop: boolean
|
||||
|
||||
@Output() loaded = new EventEmitter<boolean>()
|
||||
|
||||
instanceBannerUrl: string
|
||||
loaded: undefined
|
||||
|
||||
constructor (
|
||||
private cd: ChangeDetectorRef,
|
||||
private instance: InstanceService
|
||||
private server: ServerService
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
this.instance.getInstanceBannerUrl()
|
||||
.pipe(finalize(() => this.loaded.emit(true)))
|
||||
.subscribe(instanceBannerUrl => {
|
||||
this.instanceBannerUrl = instanceBannerUrl
|
||||
this.cd.markForCheck()
|
||||
})
|
||||
const { instance } = this.server.getHTMLConfig()
|
||||
|
||||
this.instanceBannerUrl = instance.banners?.[0]?.path
|
||||
this.cd.markForCheck()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
ButtonMarkupComponent,
|
||||
ChannelMiniatureMarkupComponent,
|
||||
EmbedMarkupComponent,
|
||||
InstanceAvatarMarkupComponent,
|
||||
InstanceBannerMarkupComponent,
|
||||
PlaylistMiniatureMarkupComponent,
|
||||
VideoMiniatureMarkupComponent,
|
||||
|
@ -41,7 +42,8 @@ import {
|
|||
ButtonMarkupComponent,
|
||||
CustomMarkupHelpComponent,
|
||||
CustomMarkupContainerComponent,
|
||||
InstanceBannerMarkupComponent
|
||||
InstanceBannerMarkupComponent,
|
||||
InstanceAvatarMarkupComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
|
@ -53,7 +55,8 @@ import {
|
|||
ButtonMarkupComponent,
|
||||
CustomMarkupHelpComponent,
|
||||
CustomMarkupContainerComponent,
|
||||
InstanceBannerMarkupComponent
|
||||
InstanceBannerMarkupComponent,
|
||||
InstanceAvatarMarkupComponent
|
||||
],
|
||||
|
||||
providers: [
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, Input, OnInit, booleanAttribute } from '@angular/core'
|
||||
import { InstanceService } from './instance.service'
|
||||
import { ServerService } from '@app/core'
|
||||
|
||||
@Component({
|
||||
selector: 'my-instance-banner',
|
||||
|
@ -10,12 +10,13 @@ export class InstanceBannerComponent implements OnInit {
|
|||
|
||||
instanceBannerUrl: string
|
||||
|
||||
constructor (private instanceService: InstanceService) {
|
||||
constructor (private server: ServerService) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.instanceService.getInstanceBannerUrl()
|
||||
.subscribe(instanceBannerUrl => this.instanceBannerUrl = instanceBannerUrl)
|
||||
const { instance } = this.server.getHTMLConfig()
|
||||
|
||||
this.instanceBannerUrl = instance.banners?.[0]?.path
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { forkJoin, of } from 'rxjs'
|
||||
import { catchError, map, tap } from 'rxjs/operators'
|
||||
import { forkJoin } from 'rxjs'
|
||||
import { catchError, map } from 'rxjs/operators'
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/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_SERVER_URL = environment.apiUrl + '/api/v1/server'
|
||||
|
||||
private instanceBannerUrl: string
|
||||
|
||||
constructor (
|
||||
private authHttp: HttpClient,
|
||||
private restExtractor: RestExtractor,
|
||||
|
@ -30,29 +28,12 @@ export class InstanceService {
|
|||
|
||||
getAbout () {
|
||||
return this.authHttp.get<About>(InstanceService.BASE_CONFIG_URL + '/about')
|
||||
.pipe(
|
||||
tap(about => {
|
||||
const banners = about.instance.banners
|
||||
if (banners.length !== 0) this.instanceBannerUrl = banners[0].path
|
||||
}),
|
||||
catchError(res => this.restExtractor.handleError(res))
|
||||
)
|
||||
.pipe(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) {
|
||||
this.instanceBannerUrl = undefined
|
||||
|
||||
const url = InstanceService.BASE_CONFIG_URL + '/instance-banner/pick'
|
||||
|
||||
return this.authHttp.post(url, formData)
|
||||
|
@ -60,8 +41,6 @@ export class InstanceService {
|
|||
}
|
||||
|
||||
deleteInstanceBanner () {
|
||||
this.instanceBannerUrl = null
|
||||
|
||||
const url = InstanceService.BASE_CONFIG_URL + '/instance-banner'
|
||||
|
||||
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) {
|
||||
const body = {
|
||||
fromEmail,
|
||||
|
|
|
@ -8,12 +8,12 @@ import {
|
|||
Input,
|
||||
LOCALE_ID,
|
||||
OnInit,
|
||||
Output
|
||||
Output,
|
||||
numberAttribute
|
||||
} from '@angular/core'
|
||||
import { AuthService, ScreenService, ServerService, User } from '@app/core'
|
||||
import { HTMLServerConfig, VideoExistInPlaylist, VideoPlaylistType, VideoPrivacy, VideoState } from '@peertube/peertube-models'
|
||||
import { LinkType } from '../../../types/link.type'
|
||||
import { ActorAvatarSize } from '../shared-actor-image/actor-avatar.component'
|
||||
import { Video, VideoService } from '../shared-main'
|
||||
import { VideoPlaylistService } from '../shared-video-playlist'
|
||||
import { VideoActionsDisplayType } from './video-actions-dropdown.component'
|
||||
|
@ -68,7 +68,7 @@ export class VideoMiniatureComponent implements OnInit {
|
|||
stats: false
|
||||
}
|
||||
|
||||
@Input() actorImageSize: ActorAvatarSize = '40'
|
||||
@Input({ transform: numberAttribute }) actorImageSize = 40
|
||||
|
||||
@Input() displayAsRow = false
|
||||
|
||||
|
|
|
@ -62,3 +62,7 @@ export type ContainerMarkupData = {
|
|||
export type InstanceBannerMarkupData = {
|
||||
revertHomePaddingTop?: StringBoolean // default to 'true'
|
||||
}
|
||||
|
||||
export type InstanceAvatarMarkupData = {
|
||||
size: string // size in pixels
|
||||
}
|
||||
|
|
|
@ -20,5 +20,6 @@ export interface About {
|
|||
categories: number[]
|
||||
|
||||
banners: ActorImage[]
|
||||
avatars: ActorImage[]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { ActorImage } from '../index.js'
|
||||
import { ClientScriptJSON } from '../plugins/plugin-package-json.model.js'
|
||||
import { NSFWPolicyType } from '../videos/nsfw-policy.type.js'
|
||||
import { VideoPrivacyType } from '../videos/video-privacy.enum.js'
|
||||
|
@ -90,6 +91,9 @@ export interface ServerConfig {
|
|||
javascript: string
|
||||
css: string
|
||||
}
|
||||
|
||||
avatars: ActorImage[]
|
||||
banners: ActorImage[]
|
||||
}
|
||||
|
||||
search: {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { AbstractCommand, OverrideCommandOptions } from '../shared/abstract-command.js'
|
||||
|
||||
|
@ -365,27 +365,38 @@ export class ConfigCommand extends AbstractCommand {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
updateInstanceBanner (options: OverrideCommandOptions & {
|
||||
updateInstanceImage (options: OverrideCommandOptions & {
|
||||
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({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fixture,
|
||||
fieldname: 'bannerfile',
|
||||
fieldname: type === ActorImageType.BANNER
|
||||
? 'bannerfile'
|
||||
: 'avatarfile',
|
||||
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
deleteInstanceBanner (options: OverrideCommandOptions = {}) {
|
||||
const path = `/api/v1/config/instance-banner`
|
||||
deleteInstanceImage (options: OverrideCommandOptions & {
|
||||
type: ActorImageType_Type
|
||||
}) {
|
||||
const suffix = options.type === ActorImageType.BANNER
|
||||
? 'instance-banner'
|
||||
: 'instance-avatar'
|
||||
|
||||
const path = `/api/v1/config/${suffix}`
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
import merge from 'lodash-es/merge.js'
|
||||
import { omit } from '@peertube/peertube-core-utils'
|
||||
import { CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { ActorImageType, CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
|
@ -421,6 +421,7 @@ describe('Test config API validators', function () {
|
|||
})
|
||||
|
||||
describe('When deleting the configuration', function () {
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
|
@ -439,79 +440,99 @@ describe('Test config API validators', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Updating instance banner', function () {
|
||||
const path = '/api/v1/config/instance-banner/pick'
|
||||
describe('Updating instance image', function () {
|
||||
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 () {
|
||||
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 () {
|
||||
const attaches = { bannerfile: buildAbsoluteFixturePath('avatar-big.png') }
|
||||
for (const { attachName, path } of toTest) {
|
||||
const attaches = { [attachName]: buildAbsoluteFixturePath('avatar-big.png') }
|
||||
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: {},
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: {},
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
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({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: {},
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: {},
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
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({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userAccessToken,
|
||||
fields: {},
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userAccessToken,
|
||||
fields: {},
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
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({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: {},
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: {},
|
||||
attaches,
|
||||
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 () {
|
||||
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 () {
|
||||
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 () {
|
||||
await server.config.deleteInstanceBanner()
|
||||
for (const type of types) {
|
||||
await server.config.deleteInstanceImage({ type })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { expect } from 'chai'
|
||||
import { parallelTests } from '@peertube/peertube-node-utils'
|
||||
import { CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { ActorImageType, CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
|
@ -11,7 +11,7 @@ import {
|
|||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} 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'
|
||||
|
||||
function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
|
||||
|
@ -521,7 +521,6 @@ describe('Test static config', function () {
|
|||
|
||||
describe('Test config', function () {
|
||||
let server: PeerTubeServer
|
||||
let bannerPath: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
@ -530,184 +529,237 @@ describe('Test config', function () {
|
|||
await setAccessTokensToServers([ server ])
|
||||
})
|
||||
|
||||
it('Should have the correct default config', async function () {
|
||||
const data = await server.config.getConfig()
|
||||
describe('Config keys', function () {
|
||||
|
||||
expect(data.openTelemetry.metrics.enabled).to.be.false
|
||||
expect(data.openTelemetry.metrics.playbackStatsInterval).to.equal(15000)
|
||||
it('Should have the correct default config', async function () {
|
||||
const data = await server.config.getConfig()
|
||||
|
||||
expect(data.views.videos.watchingInterval.anonymous).to.equal(5000)
|
||||
expect(data.views.videos.watchingInterval.users).to.equal(5000)
|
||||
})
|
||||
expect(data.openTelemetry.metrics.enabled).to.be.false
|
||||
expect(data.openTelemetry.metrics.playbackStatsInterval).to.equal(15000)
|
||||
|
||||
it('Should have a correct config on a server with registration enabled', async function () {
|
||||
const data = await server.config.getConfig()
|
||||
expect(data.views.videos.watchingInterval.anonymous).to.equal(5000)
|
||||
expect(data.views.videos.watchingInterval.users).to.equal(5000)
|
||||
})
|
||||
|
||||
expect(data.signup.allowed).to.be.true
|
||||
})
|
||||
it('Should have a correct config on a server with registration enabled', async function () {
|
||||
const data = await server.config.getConfig()
|
||||
|
||||
it('Should have a correct config on a server with registration enabled and a users limit', async function () {
|
||||
this.timeout(5000)
|
||||
expect(data.signup.allowed).to.be.true
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
server.registrations.register({ username: 'user1' }),
|
||||
server.registrations.register({ username: 'user2' }),
|
||||
server.registrations.register({ username: 'user3' })
|
||||
])
|
||||
it('Should have a correct config on a server with registration enabled and a users limit', async function () {
|
||||
this.timeout(5000)
|
||||
|
||||
const data = await server.config.getConfig()
|
||||
await Promise.all([
|
||||
server.registrations.register({ username: 'user1' }),
|
||||
server.registrations.register({ username: 'user2' }),
|
||||
server.registrations.register({ username: 'user3' })
|
||||
])
|
||||
|
||||
expect(data.signup.allowed).to.be.false
|
||||
})
|
||||
const data = await server.config.getConfig()
|
||||
|
||||
it('Should have the correct video allowed extensions', async function () {
|
||||
const data = await server.config.getConfig()
|
||||
expect(data.signup.allowed).to.be.false
|
||||
})
|
||||
|
||||
expect(data.video.file.extensions).to.have.lengthOf(3)
|
||||
expect(data.video.file.extensions).to.contain('.mp4')
|
||||
expect(data.video.file.extensions).to.contain('.webm')
|
||||
expect(data.video.file.extensions).to.contain('.ogv')
|
||||
it('Should have the correct video allowed extensions', async function () {
|
||||
const data = await server.config.getConfig()
|
||||
|
||||
await server.videos.upload({ attributes: { fixture: 'video_short.mkv' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
|
||||
await server.videos.upload({ attributes: { fixture: 'sample.ogg' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
|
||||
expect(data.video.file.extensions).to.have.lengthOf(3)
|
||||
expect(data.video.file.extensions).to.contain('.mp4')
|
||||
expect(data.video.file.extensions).to.contain('.webm')
|
||||
expect(data.video.file.extensions).to.contain('.ogv')
|
||||
|
||||
expect(data.contactForm.enabled).to.be.true
|
||||
})
|
||||
await server.videos.upload({ attributes: { fixture: 'video_short.mkv' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
|
||||
await server.videos.upload({ attributes: { fixture: 'sample.ogg' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
|
||||
|
||||
it('Should get the customized configuration', async function () {
|
||||
const data = await server.config.getCustomConfig()
|
||||
expect(data.contactForm.enabled).to.be.true
|
||||
})
|
||||
|
||||
checkInitialConfig(server, data)
|
||||
})
|
||||
it('Should get the customized configuration', async function () {
|
||||
const data = await server.config.getCustomConfig()
|
||||
|
||||
it('Should update the customized configuration', async function () {
|
||||
await server.config.updateCustomConfig({ newCustomConfig })
|
||||
checkInitialConfig(server, data)
|
||||
})
|
||||
|
||||
const data = await server.config.getCustomConfig()
|
||||
checkUpdatedConfig(data)
|
||||
})
|
||||
it('Should update the customized configuration', async function () {
|
||||
await server.config.updateCustomConfig({ newCustomConfig })
|
||||
|
||||
it('Should have the correct updated video allowed extensions', async function () {
|
||||
this.timeout(30000)
|
||||
const data = await server.config.getCustomConfig()
|
||||
checkUpdatedConfig(data)
|
||||
})
|
||||
|
||||
const data = await server.config.getConfig()
|
||||
it('Should have the correct updated video allowed extensions', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
expect(data.video.file.extensions).to.have.length.above(4)
|
||||
expect(data.video.file.extensions).to.contain('.mp4')
|
||||
expect(data.video.file.extensions).to.contain('.webm')
|
||||
expect(data.video.file.extensions).to.contain('.ogv')
|
||||
expect(data.video.file.extensions).to.contain('.flv')
|
||||
expect(data.video.file.extensions).to.contain('.wmv')
|
||||
expect(data.video.file.extensions).to.contain('.mkv')
|
||||
expect(data.video.file.extensions).to.contain('.mp3')
|
||||
expect(data.video.file.extensions).to.contain('.ogg')
|
||||
expect(data.video.file.extensions).to.contain('.flac')
|
||||
const data = await server.config.getConfig()
|
||||
|
||||
await server.videos.upload({ attributes: { fixture: 'video_short.mkv' }, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await server.videos.upload({ attributes: { fixture: 'sample.ogg' }, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
expect(data.video.file.extensions).to.have.length.above(4)
|
||||
expect(data.video.file.extensions).to.contain('.mp4')
|
||||
expect(data.video.file.extensions).to.contain('.webm')
|
||||
expect(data.video.file.extensions).to.contain('.ogv')
|
||||
expect(data.video.file.extensions).to.contain('.flv')
|
||||
expect(data.video.file.extensions).to.contain('.wmv')
|
||||
expect(data.video.file.extensions).to.contain('.mkv')
|
||||
expect(data.video.file.extensions).to.contain('.mp3')
|
||||
expect(data.video.file.extensions).to.contain('.ogg')
|
||||
expect(data.video.file.extensions).to.contain('.flac')
|
||||
|
||||
it('Should have the configuration updated after a restart', async function () {
|
||||
this.timeout(30000)
|
||||
await server.videos.upload({ attributes: { fixture: 'video_short.mkv' }, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await server.videos.upload({ attributes: { fixture: 'sample.ogg' }, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
|
||||
await killallServers([ server ])
|
||||
it('Should have the configuration updated after a restart', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
await server.run()
|
||||
await killallServers([ server ])
|
||||
|
||||
const data = await server.config.getCustomConfig()
|
||||
await server.run()
|
||||
|
||||
checkUpdatedConfig(data)
|
||||
})
|
||||
const data = await server.config.getCustomConfig()
|
||||
|
||||
it('Should fetch the about information', async function () {
|
||||
const { instance } = await server.config.getAbout()
|
||||
checkUpdatedConfig(data)
|
||||
})
|
||||
|
||||
expect(instance.name).to.equal('PeerTube updated')
|
||||
expect(instance.shortDescription).to.equal('my short description')
|
||||
expect(instance.description).to.equal('my super description')
|
||||
expect(instance.terms).to.equal('my super terms')
|
||||
expect(instance.codeOfConduct).to.equal('my super coc')
|
||||
it('Should fetch the about information', async function () {
|
||||
const { instance } = await server.config.getAbout()
|
||||
|
||||
expect(instance.creationReason).to.equal('my super creation reason')
|
||||
expect(instance.moderationInformation).to.equal('my super moderation information')
|
||||
expect(instance.administrator).to.equal('Kuja')
|
||||
expect(instance.maintenanceLifetime).to.equal('forever')
|
||||
expect(instance.businessModel).to.equal('my super business model')
|
||||
expect(instance.hardwareInformation).to.equal('2vCore 3GB RAM')
|
||||
expect(instance.name).to.equal('PeerTube updated')
|
||||
expect(instance.shortDescription).to.equal('my short description')
|
||||
expect(instance.description).to.equal('my super description')
|
||||
expect(instance.terms).to.equal('my super terms')
|
||||
expect(instance.codeOfConduct).to.equal('my super coc')
|
||||
|
||||
expect(instance.languages).to.deep.equal([ 'en', 'es' ])
|
||||
expect(instance.categories).to.deep.equal([ 1, 2 ])
|
||||
expect(instance.creationReason).to.equal('my super creation reason')
|
||||
expect(instance.moderationInformation).to.equal('my super moderation information')
|
||||
expect(instance.administrator).to.equal('Kuja')
|
||||
expect(instance.maintenanceLifetime).to.equal('forever')
|
||||
expect(instance.businessModel).to.equal('my super business model')
|
||||
expect(instance.hardwareInformation).to.equal('2vCore 3GB RAM')
|
||||
|
||||
expect(instance.banners).to.have.lengthOf(0)
|
||||
})
|
||||
expect(instance.languages).to.deep.equal([ 'en', 'es' ])
|
||||
expect(instance.categories).to.deep.equal([ 1, 2 ])
|
||||
|
||||
it('Should update instance banner', async function () {
|
||||
await server.config.updateInstanceBanner({ fixture: 'banner.jpg' })
|
||||
expect(instance.banners).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
const { instance } = await server.config.getAbout()
|
||||
it('Should remove the custom configuration', async function () {
|
||||
await server.config.deleteCustomConfig()
|
||||
|
||||
expect(instance.banners).to.have.lengthOf(1)
|
||||
const data = await server.config.getCustomConfig()
|
||||
checkInitialConfig(server, data)
|
||||
})
|
||||
|
||||
bannerPath = instance.banners[0].path
|
||||
await testImage(server.url, 'banner-resized', bannerPath)
|
||||
await testFileExistsOrNot(server, 'avatars', basename(bannerPath), true)
|
||||
})
|
||||
it('Should enable/disable security headers', async function () {
|
||||
this.timeout(25000)
|
||||
|
||||
it('Should re-update an existing instance banner', async function () {
|
||||
await server.config.updateInstanceBanner({ fixture: 'banner.jpg' })
|
||||
})
|
||||
{
|
||||
const res = await makeGetRequest({
|
||||
url: server.url,
|
||||
path: '/api/v1/config',
|
||||
expectedStatus: 200
|
||||
})
|
||||
|
||||
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 () {
|
||||
await server.config.deleteCustomConfig()
|
||||
|
||||
const data = await server.config.getCustomConfig()
|
||||
checkInitialConfig(server, data)
|
||||
})
|
||||
|
||||
it('Should enable/disable security headers', async function () {
|
||||
this.timeout(25000)
|
||||
|
||||
{
|
||||
const res = await makeGetRequest({
|
||||
url: server.url,
|
||||
path: '/api/v1/config',
|
||||
expectedStatus: 200
|
||||
})
|
||||
|
||||
expect(res.headers['x-frame-options']).to.exist
|
||||
expect(res.headers['x-powered-by']).to.equal('PeerTube')
|
||||
}
|
||||
|
||||
await killallServers([ server ])
|
||||
|
||||
const config = {
|
||||
security: {
|
||||
frameguard: { enabled: false },
|
||||
powered_by_header: { enabled: false }
|
||||
expect(res.headers['x-frame-options']).to.exist
|
||||
expect(res.headers['x-powered-by']).to.equal('PeerTube')
|
||||
}
|
||||
}
|
||||
await server.run(config)
|
||||
|
||||
{
|
||||
const res = await makeGetRequest({
|
||||
url: server.url,
|
||||
path: '/api/v1/config',
|
||||
expectedStatus: 200
|
||||
await killallServers([ server ])
|
||||
|
||||
const config = {
|
||||
security: {
|
||||
frameguard: { enabled: false },
|
||||
powered_by_header: { enabled: false }
|
||||
}
|
||||
}
|
||||
await server.run(config)
|
||||
|
||||
{
|
||||
const res = await makeGetRequest({
|
||||
url: server.url,
|
||||
path: '/api/v1/config',
|
||||
expectedStatus: 200
|
||||
})
|
||||
|
||||
expect(res.headers['x-frame-options']).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)
|
||||
})
|
||||
|
||||
expect(res.headers['x-frame-options']).to.not.exist
|
||||
expect(res.headers['x-powered-by']).to.not.exist
|
||||
}
|
||||
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 () {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { remove, writeJSON } from 'fs-extra/esm'
|
|||
import snakeCase from 'lodash-es/snakeCase.js'
|
||||
import validator from 'validator'
|
||||
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 { objectConverter } from '../../helpers/core-utils.js'
|
||||
import { CONFIG, reloadConfig } from '../../initializers/config.js'
|
||||
|
@ -14,6 +14,7 @@ import {
|
|||
authenticate,
|
||||
ensureUserHasRight,
|
||||
openapiOperationDoc,
|
||||
updateAvatarValidator,
|
||||
updateBannerValidator
|
||||
} from '../../middlewares/index.js'
|
||||
import { customConfigUpdateValidator, ensureConfigIsEditable } from '../../middlewares/validators/config.js'
|
||||
|
@ -63,18 +64,36 @@ configRouter.delete('/custom',
|
|||
asyncMiddleware(deleteCustomConfig)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
configRouter.post('/instance-banner/pick',
|
||||
authenticate,
|
||||
createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT),
|
||||
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
|
||||
updateBannerValidator,
|
||||
asyncMiddleware(updateInstanceBanner)
|
||||
asyncMiddleware(updateInstanceImageFactory(ActorImageType.BANNER))
|
||||
)
|
||||
|
||||
configRouter.delete('/instance-banner',
|
||||
authenticate,
|
||||
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) {
|
||||
const banners = await ActorImageModel.listByActor(await getServerActor(), ActorImageType.BANNER)
|
||||
const { avatars, banners } = await ActorImageModel.listServerActorImages()
|
||||
|
||||
const about: About = {
|
||||
instance: {
|
||||
|
@ -107,7 +126,8 @@ async function getAbout (req: express.Request, res: express.Response) {
|
|||
languages: CONFIG.INSTANCE.LANGUAGES,
|
||||
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)
|
||||
}
|
||||
|
||||
async function updateInstanceBanner (req: express.Request, res: express.Response) {
|
||||
const bannerPhysicalFile = req.files['bannerfile'][0]
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const serverActor = await getServerActor()
|
||||
serverActor.Banners = await ActorImageModel.listByActor(serverActor, ActorImageType.BANNER) // Reload banners from DB
|
||||
function updateInstanceImageFactory (imageType: ActorImageType_Type) {
|
||||
return async (req: express.Request, res: express.Response) => {
|
||||
const field = imageType === ActorImageType.BANNER
|
||||
? 'bannerfile'
|
||||
: 'avatarfile'
|
||||
|
||||
await updateLocalActorImageFiles({
|
||||
accountOrChannel: serverActor.Account,
|
||||
imagePhysicalFile: bannerPhysicalFile,
|
||||
type: ActorImageType.BANNER,
|
||||
sendActorUpdate: false
|
||||
})
|
||||
const imagePhysicalFile = req.files[field][0]
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
await updateLocalActorImageFiles({
|
||||
accountOrChannel: (await getServerActorWithUpdatedImages(imageType)).Account,
|
||||
imagePhysicalFile,
|
||||
type: imageType,
|
||||
sendActorUpdate: false
|
||||
})
|
||||
|
||||
ClientHtml.invalidateCache()
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteInstanceBanner (req: express.Request, res: express.Response) {
|
||||
function deleteInstanceImageFactory (imageType: ActorImageType_Type) {
|
||||
return async (req: express.Request, res: express.Response) => {
|
||||
await deleteLocalActorImageFile((await getServerActorWithUpdatedImages(imageType)).Account, imageType)
|
||||
|
||||
ClientHtml.invalidateCache()
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
}
|
||||
|
||||
async function getServerActorWithUpdatedImages (imageType: ActorImageType_Type) {
|
||||
const serverActor = await getServerActor()
|
||||
serverActor.Banners = await ActorImageModel.listByActor(serverActor, ActorImageType.BANNER) // Reload banners from DB
|
||||
const updatedImages = await ActorImageModel.listByActor(serverActor, imageType) // Reload images from DB
|
||||
|
||||
await deleteLocalActorImageFile(serverActor.Account, ActorImageType.BANNER)
|
||||
if (imageType === ActorImageType.BANNER) serverActor.Banners = updatedImages
|
||||
else serverActor.Avatars = updatedImages
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
return serverActor
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -15,6 +15,7 @@ import { Hooks } from './plugins/hooks.js'
|
|||
import { PluginManager } from './plugins/plugin-manager.js'
|
||||
import { getThemeOrDefault } from './plugins/theme-utils.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 { avatars, banners } = await ActorImageModel.listServerActorImages()
|
||||
|
||||
return {
|
||||
client: {
|
||||
videos: {
|
||||
|
@ -100,7 +103,9 @@ class ServerConfigManager {
|
|||
customizations: {
|
||||
javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
|
||||
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
|
||||
}
|
||||
},
|
||||
avatars: avatars.map(a => a.toFormattedJSON()),
|
||||
banners: banners.map(b => b.toFormattedJSON())
|
||||
},
|
||||
search: {
|
||||
remoteUri: {
|
||||
|
|
|
@ -20,6 +20,7 @@ import { CONFIG } from '../../initializers/config.js'
|
|||
import { LAZY_STATIC_PATHS, MIMETYPES, WEBSERVER } from '../../initializers/constants.js'
|
||||
import { SequelizeModel, buildSQLAttributes, throwIfNotValid } from '../shared/index.js'
|
||||
import { ActorModel } from './actor.js'
|
||||
import { getServerActor } from '../application/application.js'
|
||||
|
||||
@Table({
|
||||
tableName: 'actorImage',
|
||||
|
@ -123,6 +124,15 @@ export class ActorImageModel extends SequelizeModel<ActorImageModel> {
|
|||
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) {
|
||||
if (!image) return undefined
|
||||
|
||||
|
|
|
@ -953,17 +953,8 @@ paths:
|
|||
tags:
|
||||
- Config
|
||||
responses:
|
||||
'200':
|
||||
'204':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
banners:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ActorImage'
|
||||
'413':
|
||||
description: image file too large
|
||||
headers:
|
||||
|
@ -998,6 +989,51 @@ paths:
|
|||
'204':
|
||||
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:
|
||||
get:
|
||||
summary: Get instance custom homepage
|
||||
|
@ -8251,6 +8287,14 @@ components:
|
|||
type: string
|
||||
css:
|
||||
type: string
|
||||
avatars:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ActorImage'
|
||||
banners:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ActorImage'
|
||||
search:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -8613,6 +8657,36 @@ components:
|
|||
type: string
|
||||
terms:
|
||||
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:
|
||||
properties:
|
||||
instance:
|
||||
|
|
Loading…
Reference in a new issue