mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2024-11-24 09:40:28 -06:00
Add ability to update the banner
This commit is contained in:
parent
282695e699
commit
cdeddff142
@ -3,6 +3,7 @@ import { SelectButtonModule } from 'primeng/selectbutton'
|
||||
import { TableModule } from 'primeng/table'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
|
||||
import { SharedActorImageModule } from '@app/shared/shared-actor-image'
|
||||
import { SharedFormModule } from '@app/shared/shared-forms'
|
||||
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
|
||||
import { SharedMainModule } from '@app/shared/shared-main'
|
||||
@ -49,6 +50,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
|
||||
SharedGlobalIconModule,
|
||||
SharedAbuseListModule,
|
||||
SharedVideoCommentModule,
|
||||
SharedActorImageModule,
|
||||
|
||||
TableModule,
|
||||
SelectButtonModule,
|
||||
|
@ -72,7 +72,7 @@
|
||||
<div class="anchor" id="user"></div> <!-- user anchor -->
|
||||
<div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div>
|
||||
<div *ngIf="!isCreation() && user" class="account-title">
|
||||
<my-actor-avatar-info [actor]="user.account"></my-actor-avatar-info>
|
||||
<my-actor-avatar-edit [actor]="user.account" [editable]="false" [displaySubscribers]="false" [displayUsername]="false"></my-actor-avatar-edit>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -72,11 +72,3 @@ input[type=submit], button {
|
||||
@include dashboard;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
my-actor-avatar-info ::ng-deep {
|
||||
.actor-img-edit-container,
|
||||
.actor-info-followers,
|
||||
.actor-info-username {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
<div class="form-group col-12 col-lg-4 col-xl-3"></div>
|
||||
|
||||
<div class="form-group col-12 col-lg-8 col-xl-9">
|
||||
<my-actor-avatar-info [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-info>
|
||||
<my-actor-avatar-edit [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-edit>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { TableModule } from 'primeng/table'
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
|
||||
import { SharedActorImageModule } from '@app/shared/shared-actor-image'
|
||||
import { SharedFormModule } from '@app/shared/shared-forms'
|
||||
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
|
||||
import { SharedMainModule } from '@app/shared/shared-main'
|
||||
@ -10,6 +11,7 @@ import { SharedModerationModule } from '@app/shared/shared-moderation'
|
||||
import { SharedShareModal } from '@app/shared/shared-share-modal'
|
||||
import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings'
|
||||
import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
|
||||
import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
|
||||
import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component'
|
||||
import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component'
|
||||
import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component'
|
||||
@ -20,7 +22,6 @@ import { MyAccountDangerZoneComponent } from './my-account-settings/my-account-d
|
||||
import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences'
|
||||
import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component'
|
||||
import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
|
||||
import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
|
||||
import { MyAccountComponent } from './my-account.component'
|
||||
|
||||
@NgModule({
|
||||
@ -37,7 +38,8 @@ import { MyAccountComponent } from './my-account.component'
|
||||
SharedUserInterfaceSettingsModule,
|
||||
SharedGlobalIconModule,
|
||||
SharedAbuseListModule,
|
||||
SharedShareModal
|
||||
SharedShareModal,
|
||||
SharedActorImageModule
|
||||
],
|
||||
|
||||
declarations: [
|
||||
|
@ -44,10 +44,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<my-actor-avatar-info
|
||||
<h6 i18n>Banner image of your channel</h6>
|
||||
|
||||
<my-actor-banner-edit
|
||||
*ngIf="!isCreation() && videoChannelToUpdate"
|
||||
[actor]="videoChannelToUpdate" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
|
||||
></my-actor-banner-edit>
|
||||
|
||||
<my-actor-avatar-edit
|
||||
*ngIf="!isCreation() && videoChannelToUpdate"
|
||||
[actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
|
||||
></my-actor-avatar-info>
|
||||
></my-actor-avatar-edit>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="display-name">Display name</label>
|
||||
|
@ -10,11 +10,16 @@ label {
|
||||
@include settings-big-title;
|
||||
}
|
||||
|
||||
my-actor-avatar-info {
|
||||
my-actor-avatar-edit,
|
||||
my-actor-banner-edit {
|
||||
display: block;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
my-actor-banner-edit {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
@include peertube-input-group(fit-content);
|
||||
}
|
||||
|
@ -15,6 +15,8 @@ export abstract class MyVideoChannelEdit extends FormReactive {
|
||||
// We need this method so angular does not complain in child template that doesn't need this
|
||||
onAvatarChange (formData: FormData) { /* empty */ }
|
||||
onAvatarDelete () { /* empty */ }
|
||||
onBannerChange (formData: FormData) { /* empty */ }
|
||||
onBannerDelete () { /* empty */ }
|
||||
|
||||
// Should be implemented by the child
|
||||
isBulkUpdateVideosDisplayed () {
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { Subscription } from 'rxjs'
|
||||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { AuthService, Notifier, ServerService } from '@app/core'
|
||||
import { uploadErrorHandler } from '@app/helpers'
|
||||
import {
|
||||
VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
|
||||
VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
|
||||
@ -11,8 +13,6 @@ import { FormValidatorService } from '@app/shared/shared-forms'
|
||||
import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
|
||||
import { ServerConfig, VideoChannelUpdate } from '@shared/models'
|
||||
import { MyVideoChannelEdit } from './my-video-channel-edit'
|
||||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
import { uploadErrorHandler } from '@app/helpers'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-channel-update',
|
||||
@ -101,7 +101,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
|
||||
}
|
||||
|
||||
onAvatarChange (formData: FormData) {
|
||||
this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData)
|
||||
this.videoChannelService.changeVideoChannelImage(this.videoChannelToUpdate.name, formData, 'avatar')
|
||||
.subscribe(
|
||||
data => {
|
||||
this.notifier.success($localize`Avatar changed.`)
|
||||
@ -118,7 +118,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
|
||||
}
|
||||
|
||||
onAvatarDelete () {
|
||||
this.videoChannelService.deleteVideoChannelAvatar(this.videoChannelToUpdate.name)
|
||||
this.videoChannelService.deleteVideoChannelImage(this.videoChannelToUpdate.name, 'avatar')
|
||||
.subscribe(
|
||||
data => {
|
||||
this.notifier.success($localize`Avatar deleted.`)
|
||||
@ -130,6 +130,36 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
|
||||
)
|
||||
}
|
||||
|
||||
onBannerChange (formData: FormData) {
|
||||
this.videoChannelService.changeVideoChannelImage(this.videoChannelToUpdate.name, formData, 'banner')
|
||||
.subscribe(
|
||||
data => {
|
||||
this.notifier.success($localize`Banner changed.`)
|
||||
|
||||
this.videoChannelToUpdate.updateBanner(data.banner)
|
||||
},
|
||||
|
||||
(err: HttpErrorResponse) => uploadErrorHandler({
|
||||
err,
|
||||
name: $localize`banner`,
|
||||
notifier: this.notifier
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
onBannerDelete () {
|
||||
this.videoChannelService.deleteVideoChannelImage(this.videoChannelToUpdate.name, 'banner')
|
||||
.subscribe(
|
||||
data => {
|
||||
this.notifier.success($localize`Banner deleted.`)
|
||||
|
||||
this.videoChannelToUpdate.resetBanner()
|
||||
},
|
||||
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
|
||||
get maxAvatarSize () {
|
||||
return this.serverConfig.avatar.file.size.max
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ChartModule } from 'primeng/chart'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { SharedActorImageModule } from '@app/shared/shared-actor-image'
|
||||
import { SharedFormModule } from '@app/shared/shared-forms'
|
||||
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
|
||||
import { SharedMainModule } from '@app/shared/shared-main'
|
||||
@ -16,7 +17,8 @@ import { MyVideoChannelsComponent } from './my-video-channels.component'
|
||||
|
||||
SharedMainModule,
|
||||
SharedFormModule,
|
||||
SharedGlobalIconModule
|
||||
SharedGlobalIconModule,
|
||||
SharedActorImageModule
|
||||
],
|
||||
|
||||
declarations: [
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { Video } from '../video/video.model'
|
||||
import { Video } from '@app/shared/shared-main/video'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-avatar-channel',
|
@ -29,7 +29,12 @@ import { MetaService } from '@ngx-meta/core'
|
||||
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
||||
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
|
||||
import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models'
|
||||
import { cleanupVideoWatch, getStoredP2PEnabled, getStoredTheater, getStoredVideoWatchHistory } from '../../../assets/player/peertube-player-local-storage'
|
||||
import {
|
||||
cleanupVideoWatch,
|
||||
getStoredP2PEnabled,
|
||||
getStoredTheater,
|
||||
getStoredVideoWatchHistory
|
||||
} from '../../../assets/player/peertube-player-local-storage'
|
||||
import {
|
||||
CustomizationOptions,
|
||||
P2PMediaLoaderOptions,
|
||||
|
@ -16,6 +16,7 @@ import { VideoCommentComponent } from './comment/video-comment.component'
|
||||
import { VideoCommentsComponent } from './comment/video-comments.component'
|
||||
import { RecommendationsModule } from './recommendations/recommendations.module'
|
||||
import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive'
|
||||
import { VideoAvatarChannelComponent } from './video-avatar-channel.component'
|
||||
import { VideoWatchPlaylistComponent } from './video-watch-playlist.component'
|
||||
import { VideoWatchRoutingModule } from './video-watch-routing.module'
|
||||
import { VideoWatchComponent } from './video-watch.component'
|
||||
@ -46,6 +47,8 @@ import { VideoWatchComponent } from './video-watch.component'
|
||||
VideoCommentAddComponent,
|
||||
VideoCommentComponent,
|
||||
|
||||
VideoAvatarChannelComponent,
|
||||
|
||||
TimestampRouteTransformerDirective,
|
||||
TimestampRouteTransformerDirective
|
||||
],
|
||||
|
@ -98,6 +98,12 @@ export class ServerService {
|
||||
extensions: []
|
||||
}
|
||||
},
|
||||
banner: {
|
||||
file: {
|
||||
size: { max: 0 },
|
||||
extensions: []
|
||||
}
|
||||
},
|
||||
video: {
|
||||
image: {
|
||||
size: { max: 0 },
|
||||
|
@ -0,0 +1,41 @@
|
||||
<div class="actor" *ngIf="actor">
|
||||
<div class="d-flex">
|
||||
<img [ngClass]="{ channel: isChannel() }" [src]="actor.avatarUrl" alt="Avatar" />
|
||||
|
||||
<div class="actor-img-edit-container">
|
||||
|
||||
<div *ngIf="editable && !hasAvatar()" class="actor-img-edit-button" [ngbTooltip]="avatarFormat" placement="right" container="body">
|
||||
<my-global-icon iconName="upload"></my-global-icon>
|
||||
<label class="sr-only" for="avatarfile" i18n>Upload a new avatar</label>
|
||||
<input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="editable && hasAvatar()" class="actor-img-edit-button"
|
||||
#avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-image-info" autoClose="outside" placement="right"
|
||||
>
|
||||
<my-global-icon iconName="edit"></my-global-icon>
|
||||
<label class="sr-only" for="avatarMenu" i18n>Change your avatar</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<ng-template #avatarEditContent>
|
||||
<div class="dropdown-item c-hand" [ngbTooltip]="avatarFormat" placement="right" container="body">
|
||||
<my-global-icon iconName="upload"></my-global-icon>
|
||||
<span for="avatarfile" i18n>Upload a new avatar</span>
|
||||
<input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
|
||||
</div>
|
||||
<div class="dropdown-item c-hand" (click)="deleteAvatar()" (key.enter)="deleteAvatar()">
|
||||
<my-global-icon iconName="delete"></my-global-icon>
|
||||
<span i18n>Remove avatar</span>
|
||||
</div>
|
||||
</ng-template>
|
@ -0,0 +1,54 @@
|
||||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.actor {
|
||||
display: flex;
|
||||
|
||||
img {
|
||||
margin-right: 15px;
|
||||
|
||||
&:not(.channel) {
|
||||
@include avatar(100px);
|
||||
}
|
||||
|
||||
&.channel {
|
||||
@include channel-avatar(100px);
|
||||
}
|
||||
}
|
||||
|
||||
.actor-info {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
|
||||
.actor-info-display-name {
|
||||
font-size: 20px;
|
||||
font-weight: $font-bold;
|
||||
|
||||
@media screen and (max-width: $small-view) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.actor-info-username {
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
color: pvar(--greyForegroundColor);
|
||||
}
|
||||
|
||||
.actor-info-followers {
|
||||
font-size: 15px;
|
||||
padding-bottom: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actor-img-edit-container {
|
||||
position: relative;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.actor-img-edit-button {
|
||||
top: 55px;
|
||||
right: 45px;
|
||||
border-radius: 50%;
|
||||
}
|
@ -1,21 +1,25 @@
|
||||
import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'
|
||||
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { Account, VideoChannel } from '@app/shared/shared-main'
|
||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { getBytes } from '@root-helpers/bytes'
|
||||
import { Account } from '../account/account.model'
|
||||
import { VideoChannel } from '../video-channel/video-channel.model'
|
||||
import { Actor } from './actor.model'
|
||||
|
||||
@Component({
|
||||
selector: 'my-actor-avatar-info',
|
||||
templateUrl: './actor-avatar-info.component.html',
|
||||
styleUrls: [ './actor-avatar-info.component.scss' ]
|
||||
selector: 'my-actor-avatar-edit',
|
||||
templateUrl: './actor-avatar-edit.component.html',
|
||||
styleUrls: [
|
||||
'./actor-image-edit.scss',
|
||||
'./actor-avatar-edit.component.scss'
|
||||
]
|
||||
})
|
||||
export class ActorAvatarInfoComponent implements OnInit, OnChanges {
|
||||
export class ActorAvatarEditComponent implements OnInit {
|
||||
@ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement>
|
||||
@ViewChild('avatarPopover') avatarPopover: NgbPopover
|
||||
|
||||
@Input() actor: VideoChannel | Account
|
||||
@Input() editable = true
|
||||
@Input() displaySubscribers = true
|
||||
@Input() displayUsername = true
|
||||
|
||||
@Output() avatarChange = new EventEmitter<FormData>()
|
||||
@Output() avatarDelete = new EventEmitter<void>()
|
||||
@ -24,8 +28,6 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
|
||||
maxAvatarSize = 0
|
||||
avatarExtensions = ''
|
||||
|
||||
private avatarUrl: string
|
||||
|
||||
constructor (
|
||||
private serverService: ServerService,
|
||||
private notifier: Notifier
|
||||
@ -42,12 +44,6 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
|
||||
})
|
||||
}
|
||||
|
||||
ngOnChanges (changes: SimpleChanges) {
|
||||
if (changes['actor']) {
|
||||
this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.actor)
|
||||
}
|
||||
}
|
||||
|
||||
onAvatarChange (input: HTMLInputElement) {
|
||||
this.avatarfileInput = new ElementRef(input)
|
||||
|
||||
@ -68,7 +64,7 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
hasAvatar () {
|
||||
return !!this.avatarUrl
|
||||
return !!this.actor.avatar
|
||||
}
|
||||
|
||||
isChannel () {
|
@ -0,0 +1,34 @@
|
||||
<div class="actor" *ngIf="actor">
|
||||
<div class="actor-img-edit-container">
|
||||
<div class="banner-placeholder">
|
||||
<img *ngIf="hasBanner()" [src]="actor.bannerUrl" alt="Banner" />
|
||||
</div>
|
||||
|
||||
<div *ngIf="!hasBanner()" class="actor-img-edit-button" [ngbTooltip]="bannerFormat" placement="right" container="body">
|
||||
<my-global-icon iconName="upload"></my-global-icon>
|
||||
<label for="bannerfile" i18n>Upload a new banner</label>
|
||||
<input #bannerfileInput type="file" name="bannerfile" id="bannerfile" [accept]="bannerExtensions" (change)="onBannerChange(bannerfileInput)"/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="hasBanner()" class="actor-img-edit-button"
|
||||
#bannerPopover="ngbPopover" [ngbPopover]="bannerEditContent" popoverClass="popover-image-info" autoClose="outside" placement="right"
|
||||
>
|
||||
<my-global-icon iconName="edit"></my-global-icon>
|
||||
<label for="bannerMenu" i18n>Change your banner</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #bannerEditContent>
|
||||
<div class="dropdown-item c-hand" [ngbTooltip]="bannerFormat" placement="right" container="body">
|
||||
<my-global-icon iconName="upload"></my-global-icon>
|
||||
<span for="bannerfile" i18n>Upload a new banner</span>
|
||||
<input #bannerfileInput type="file" name="bannerfile" id="bannerfile" [accept]="bannerExtensions" (change)="onBannerChange(bannerfileInput)"/>
|
||||
</div>
|
||||
|
||||
<div class="dropdown-item c-hand" (click)="deleteBanner()" (key.enter)="deleteBanner()">
|
||||
<my-global-icon iconName="delete"></my-global-icon>
|
||||
<span i18n>Remove banner</span>
|
||||
</div>
|
||||
</ng-template>
|
@ -0,0 +1,27 @@
|
||||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.banner-placeholder {
|
||||
@include block-ratio('> div, > img', $banner-inverted-ratio);
|
||||
}
|
||||
|
||||
.banner-placeholder {
|
||||
background-color: pvar(--greyBackgroundColor);
|
||||
}
|
||||
|
||||
.actor-img-edit-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actor-img-edit-button {
|
||||
position: absolute;
|
||||
width: auto;
|
||||
|
||||
label {
|
||||
font-weight: $font-semibold;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { VideoChannel } from '@app/shared/shared-main'
|
||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { getBytes } from '@root-helpers/bytes'
|
||||
|
||||
@Component({
|
||||
selector: 'my-actor-banner-edit',
|
||||
templateUrl: './actor-banner-edit.component.html',
|
||||
styleUrls: [
|
||||
'./actor-image-edit.scss',
|
||||
'./actor-banner-edit.component.scss'
|
||||
]
|
||||
})
|
||||
export class ActorBannerEditComponent implements OnInit {
|
||||
@ViewChild('bannerfileInput') bannerfileInput: ElementRef<HTMLInputElement>
|
||||
@ViewChild('bannerPopover') bannerPopover: NgbPopover
|
||||
|
||||
@Input() actor: VideoChannel
|
||||
|
||||
@Output() bannerChange = new EventEmitter<FormData>()
|
||||
@Output() bannerDelete = new EventEmitter<void>()
|
||||
|
||||
bannerFormat = ''
|
||||
maxBannerSize = 0
|
||||
bannerExtensions = ''
|
||||
|
||||
constructor (
|
||||
private serverService: ServerService,
|
||||
private notifier: Notifier
|
||||
) { }
|
||||
|
||||
ngOnInit (): void {
|
||||
this.serverService.getConfig()
|
||||
.subscribe(config => {
|
||||
this.maxBannerSize = config.banner.file.size.max
|
||||
this.bannerExtensions = config.banner.file.extensions.join(', ')
|
||||
|
||||
this.bannerFormat = $localize`maxsize: ${getBytes(this.maxBannerSize)}, extensions: ${this.bannerExtensions}`
|
||||
})
|
||||
}
|
||||
|
||||
onBannerChange (input: HTMLInputElement) {
|
||||
this.bannerfileInput = new ElementRef(input)
|
||||
|
||||
const bannerfile = this.bannerfileInput.nativeElement.files[ 0 ]
|
||||
if (bannerfile.size > this.maxBannerSize) {
|
||||
this.notifier.error('Error', $localize`This image is too large.`)
|
||||
return
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('bannerfile', bannerfile)
|
||||
this.bannerPopover?.close()
|
||||
this.bannerChange.emit(formData)
|
||||
}
|
||||
|
||||
deleteBanner () {
|
||||
this.bannerDelete.emit()
|
||||
}
|
||||
|
||||
hasBanner () {
|
||||
return !!this.actor.bannerUrl
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.actor ::ng-deep .popover-image-info .popover-body {
|
||||
padding: 0;
|
||||
|
||||
.dropdown-item {
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:first-child {
|
||||
@include peertube-file;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actor-img-edit-button {
|
||||
@include peertube-button-file(21px);
|
||||
@include button-with-icon(19px);
|
||||
@include orange-button;
|
||||
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
cursor: pointer;
|
||||
|
||||
input {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
my-global-icon {
|
||||
right: 7px;
|
||||
}
|
||||
}
|
1
client/src/app/shared/shared-actor-image/index.ts
Normal file
1
client/src/app/shared/shared-actor-image/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './shared-actor-image.module'
|
@ -0,0 +1,29 @@
|
||||
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { SharedGlobalIconModule } from '../shared-icons'
|
||||
import { SharedMainModule } from '../shared-main'
|
||||
import { ActorAvatarEditComponent } from './actor-avatar-edit.component'
|
||||
import { ActorBannerEditComponent } from './actor-banner-edit.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
SharedMainModule,
|
||||
SharedGlobalIconModule
|
||||
],
|
||||
|
||||
declarations: [
|
||||
ActorAvatarEditComponent,
|
||||
ActorBannerEditComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
ActorAvatarEditComponent,
|
||||
ActorBannerEditComponent
|
||||
],
|
||||
|
||||
providers: [ ]
|
||||
})
|
||||
export class SharedActorImageModule { }
|
@ -1,42 +0,0 @@
|
||||
<ng-container *ngIf="actor">
|
||||
<div class="actor">
|
||||
<div class="d-flex">
|
||||
<img [ngClass]="{ channel: isChannel() }" [src]="actor.avatarUrl" alt="Avatar" />
|
||||
|
||||
<div class="actor-img-edit-container">
|
||||
|
||||
<div *ngIf="!hasAvatar()" class="actor-img-edit-button" [ngbTooltip]="avatarFormat" placement="right" container="body">
|
||||
<my-global-icon iconName="upload"></my-global-icon>
|
||||
<label class="sr-only" for="avatarfile" i18n>Upload a new avatar</label>
|
||||
<input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hasAvatar()" class="actor-img-edit-button" #avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-avatar-info" autoClose="outside" placement="right">
|
||||
<my-global-icon iconName="edit"></my-global-icon>
|
||||
<label class="sr-only" for="avatarMenu" i18n>Change your avatar</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actor-info">
|
||||
<div class="actor-info-names">
|
||||
<div class="actor-info-display-name">{{ actor.displayName }}</div>
|
||||
<div class="actor-info-username">{{ actor.name }}</div>
|
||||
</div>
|
||||
<div i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #avatarEditContent>
|
||||
<div class="dropdown-item c-hand" [ngbTooltip]="avatarFormat" placement="right" container="body">
|
||||
<my-global-icon iconName="upload"></my-global-icon>
|
||||
<span for="avatarfile" i18n>Upload a new avatar</span>
|
||||
<input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
|
||||
</div>
|
||||
<div class="dropdown-item c-hand" (click)="deleteAvatar()" (key.enter)="deleteAvatar()">
|
||||
<my-global-icon iconName="delete"></my-global-icon>
|
||||
<span i18n>Remove avatar</span>
|
||||
</div>
|
||||
</ng-template>
|
@ -1,92 +0,0 @@
|
||||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.actor {
|
||||
display: flex;
|
||||
|
||||
img {
|
||||
margin-right: 15px;
|
||||
|
||||
&:not(.channel) {
|
||||
@include avatar(100px);
|
||||
}
|
||||
|
||||
&.channel {
|
||||
@include channel-avatar(100px);
|
||||
}
|
||||
}
|
||||
|
||||
.actor-img-edit-container {
|
||||
position: relative;
|
||||
width: 0;
|
||||
|
||||
.actor-img-edit-button {
|
||||
@include peertube-button-file(21px);
|
||||
@include button-with-icon(19px);
|
||||
@include orange-button;
|
||||
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 50%;
|
||||
top: 55px;
|
||||
right: 45px;
|
||||
cursor: pointer;
|
||||
|
||||
input {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
my-global-icon {
|
||||
right: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actor-info {
|
||||
justify-content: center;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
|
||||
.actor-info-names {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.actor-info-display-name {
|
||||
font-size: 20px;
|
||||
font-weight: $font-bold;
|
||||
|
||||
@media screen and (max-width: $small-view) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.actor-info-username {
|
||||
margin-left: 7px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
font-size: 14px;
|
||||
color: $grey-actor-name;
|
||||
}
|
||||
}
|
||||
|
||||
.actor-info-followers {
|
||||
font-size: 15px;
|
||||
padding-bottom: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actor-img-edit-container ::ng-deep .popover-avatar-info .popover-body {
|
||||
padding: 0;
|
||||
|
||||
.dropdown-item {
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:first-child {
|
||||
@include peertube-file;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,15 +3,18 @@ import { getAbsoluteAPIUrl } from '@app/helpers'
|
||||
|
||||
export abstract class Actor implements ActorServer {
|
||||
id: number
|
||||
url: string
|
||||
name: string
|
||||
|
||||
host: string
|
||||
url: string
|
||||
|
||||
followingCount: number
|
||||
followersCount: number
|
||||
|
||||
createdAt: Date | string
|
||||
updatedAt: Date | string
|
||||
avatar: ActorImage
|
||||
|
||||
avatar: ActorImage
|
||||
avatarUrl: string
|
||||
|
||||
isLocal: boolean
|
||||
@ -24,6 +27,8 @@ export abstract class Actor implements ActorServer {
|
||||
|
||||
return absoluteAPIUrl + actor.avatar.path
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
static CREATE_BY_STRING (accountName: string, host: string, forceHostname = false) {
|
||||
|
@ -1,5 +1,3 @@
|
||||
export * from './account.model'
|
||||
export * from './account.service'
|
||||
export * from './actor-avatar-info.component'
|
||||
export * from './actor.model'
|
||||
export * from './video-avatar-channel.component'
|
||||
|
@ -6,18 +6,18 @@ import { NgModule } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import {
|
||||
NgbButtonsModule,
|
||||
NgbCollapseModule,
|
||||
NgbDropdownModule,
|
||||
NgbModalModule,
|
||||
NgbNavModule,
|
||||
NgbPopoverModule,
|
||||
NgbTooltipModule,
|
||||
NgbButtonsModule
|
||||
NgbTooltipModule
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { LoadingBarModule } from '@ngx-loading-bar/core'
|
||||
import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
|
||||
import { SharedGlobalIconModule } from '../shared-icons'
|
||||
import { AccountService, ActorAvatarInfoComponent, VideoAvatarChannelComponent } from './account'
|
||||
import { AccountService } from './account'
|
||||
import {
|
||||
AutofocusDirective,
|
||||
BytesPipe,
|
||||
@ -32,7 +32,7 @@ import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditBu
|
||||
import { DateToggleComponent } from './date'
|
||||
import { FeedComponent } from './feeds'
|
||||
import { LoaderComponent, SmallLoaderComponent } from './loaders'
|
||||
import { HelpComponent, ListOverflowComponent, TopMenuDropdownComponent, SimpleSearchInputComponent } from './misc'
|
||||
import { HelpComponent, ListOverflowComponent, SimpleSearchInputComponent, TopMenuDropdownComponent } from './misc'
|
||||
import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users'
|
||||
import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video'
|
||||
import { VideoCaptionService } from './video-caption'
|
||||
@ -65,9 +65,6 @@ import { VideoChannelService } from './video-channel'
|
||||
],
|
||||
|
||||
declarations: [
|
||||
VideoAvatarChannelComponent,
|
||||
ActorAvatarInfoComponent,
|
||||
|
||||
FromNowPipe,
|
||||
NumberFormatterPipe,
|
||||
BytesPipe,
|
||||
@ -120,9 +117,6 @@ import { VideoChannelService } from './video-channel'
|
||||
|
||||
PrimeSharedModule,
|
||||
|
||||
VideoAvatarChannelComponent,
|
||||
ActorAvatarInfoComponent,
|
||||
|
||||
FromNowPipe,
|
||||
BytesPipe,
|
||||
NumberFormatterPipe,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { getAbsoluteAPIUrl } from '@app/helpers'
|
||||
import { Account as ServerAccount, ActorImage, VideoChannel as ServerVideoChannel, ViewsPerDate } from '@shared/models'
|
||||
import { Account } from '../account/account.model'
|
||||
import { Actor } from '../account/actor.model'
|
||||
@ -6,10 +7,15 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
|
||||
displayName: string
|
||||
description: string
|
||||
support: string
|
||||
|
||||
isLocal: boolean
|
||||
|
||||
nameWithHost: string
|
||||
nameWithHostForced: string
|
||||
|
||||
banner: ActorImage
|
||||
bannerUrl: string
|
||||
|
||||
ownerAccount?: ServerAccount
|
||||
ownerBy?: string
|
||||
ownerAvatarUrl?: string
|
||||
@ -22,6 +28,18 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
|
||||
return Actor.GET_ACTOR_AVATAR_URL(actor) || this.GET_DEFAULT_AVATAR_URL()
|
||||
}
|
||||
|
||||
static GET_ACTOR_BANNER_URL (channel: ServerVideoChannel) {
|
||||
if (channel?.banner?.url) return channel.banner.url
|
||||
|
||||
if (channel && channel.banner) {
|
||||
const absoluteAPIUrl = getAbsoluteAPIUrl()
|
||||
|
||||
return absoluteAPIUrl + channel.banner.path
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
static GET_DEFAULT_AVATAR_URL () {
|
||||
return `${window.location.origin}/client/assets/images/default-avatar-videochannel.png`
|
||||
}
|
||||
@ -29,12 +47,14 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
|
||||
constructor (hash: ServerVideoChannel) {
|
||||
super(hash)
|
||||
|
||||
this.updateComputedAttributes()
|
||||
|
||||
this.displayName = hash.displayName
|
||||
this.description = hash.description
|
||||
this.support = hash.support
|
||||
|
||||
this.banner = hash.banner
|
||||
|
||||
this.isLocal = hash.isLocal
|
||||
|
||||
this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host)
|
||||
this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true)
|
||||
|
||||
@ -49,6 +69,8 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
|
||||
this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host)
|
||||
this.ownerAvatarUrl = Account.GET_ACTOR_AVATAR_URL(this.ownerAccount)
|
||||
}
|
||||
|
||||
this.updateComputedAttributes()
|
||||
}
|
||||
|
||||
updateAvatar (newAvatar: ActorImage) {
|
||||
@ -58,11 +80,21 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
|
||||
}
|
||||
|
||||
resetAvatar () {
|
||||
this.avatar = null
|
||||
this.avatarUrl = VideoChannel.GET_DEFAULT_AVATAR_URL()
|
||||
this.updateAvatar(null)
|
||||
}
|
||||
|
||||
updateBanner (newBanner: ActorImage) {
|
||||
this.banner = newBanner
|
||||
|
||||
this.updateComputedAttributes()
|
||||
}
|
||||
|
||||
resetBanner () {
|
||||
this.updateBanner(null)
|
||||
}
|
||||
|
||||
private updateComputedAttributes () {
|
||||
this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this)
|
||||
this.bannerUrl = VideoChannel.GET_ACTOR_BANNER_URL(this)
|
||||
}
|
||||
}
|
||||
|
@ -82,15 +82,15 @@ export class VideoChannelService {
|
||||
)
|
||||
}
|
||||
|
||||
changeVideoChannelAvatar (videoChannelName: string, avatarForm: FormData) {
|
||||
const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar/pick'
|
||||
changeVideoChannelImage (videoChannelName: string, avatarForm: FormData, type: 'avatar' | 'banner') {
|
||||
const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type + '/pick'
|
||||
|
||||
return this.authHttp.post<{ avatar: ActorImage }>(url, avatarForm)
|
||||
return this.authHttp.post<{ avatar?: ActorImage, banner?: ActorImage }>(url, avatarForm)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
deleteVideoChannelAvatar (videoChannelName: string) {
|
||||
const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar'
|
||||
deleteVideoChannelImage (videoChannelName: string, type: 'avatar' | 'banner') {
|
||||
const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type
|
||||
|
||||
return this.authHttp.delete(url)
|
||||
.pipe(
|
||||
|
@ -32,7 +32,7 @@
|
||||
color: pvar(--inputPlaceholderColor);
|
||||
}
|
||||
|
||||
@include large-screen-ratio($selector: 'div, ::ng-deep iframe') {
|
||||
@include block-ratio($selector: 'div, ::ng-deep iframe') {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
left: 0;
|
||||
|
@ -21,7 +21,7 @@ textarea {
|
||||
}
|
||||
|
||||
.screenratio {
|
||||
@include large-screen-ratio($selector: 'div, ::ng-deep iframe') {
|
||||
@include block-ratio($selector: 'div, ::ng-deep iframe') {
|
||||
left: 0;
|
||||
};
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ $more-button-width: 40px;
|
||||
width: 100%;
|
||||
|
||||
my-video-thumbnail {
|
||||
@include large-screen-ratio($selector: '::ng-deep .video-thumbnail');
|
||||
@include block-ratio($selector: '::ng-deep .video-thumbnail');
|
||||
}
|
||||
|
||||
.video-bottom {
|
||||
|
@ -886,14 +886,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
// applies 16:9 ratio to a child element (using $selector) only using
|
||||
// an immediate's parent size. This allows 16:9 ratio without explicit
|
||||
// applies ratio (default to 16:9) to a child element (using $selector) only using
|
||||
// an immediate's parent size. This allows to set a ratio without explicit
|
||||
// dimensions, as width/height cannot be computed from each other.
|
||||
@mixin large-screen-ratio ($selector: 'div') {
|
||||
@mixin block-ratio ($selector: 'div', $inverted-ratio: 9/16) {
|
||||
$padding-percent: percentage($inverted-ratio);
|
||||
|
||||
position: relative;
|
||||
height: 0;
|
||||
width: 100%;
|
||||
padding-top: 56%;
|
||||
padding-top: $padding-percent;
|
||||
|
||||
#{$selector} {
|
||||
position: absolute;
|
||||
|
@ -52,6 +52,9 @@ $sub-menu-background-color: #F7F7F7;
|
||||
$sub-menu-height: 81px;
|
||||
|
||||
$channel-background-color: #f6ede8;
|
||||
|
||||
$banner-inverted-ratio: 1/5;
|
||||
|
||||
$max-channels-width: 1200px;
|
||||
|
||||
$footer-height: 30px;
|
||||
|
@ -163,6 +163,14 @@ async function getConfig (req: express.Request, res: express.Response) {
|
||||
extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
|
||||
}
|
||||
},
|
||||
banner: {
|
||||
file: {
|
||||
size: {
|
||||
max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
|
||||
},
|
||||
extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
|
||||
}
|
||||
},
|
||||
video: {
|
||||
image: {
|
||||
extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME,
|
||||
|
@ -72,7 +72,11 @@ export class ActorImageModel extends Model {
|
||||
}
|
||||
|
||||
getStaticPath () {
|
||||
return join(LAZY_STATIC_PATHS.AVATARS, this.filename)
|
||||
if (this.type === ActorImageType.AVATAR) {
|
||||
return join(LAZY_STATIC_PATHS.AVATARS, this.filename)
|
||||
}
|
||||
|
||||
return join(LAZY_STATIC_PATHS.BANNERS, this.filename)
|
||||
}
|
||||
|
||||
getPath () {
|
||||
|
@ -71,6 +71,7 @@ import { VideoLiveModel } from '../video/video-live'
|
||||
import { VideoPlaylistModel } from '../video/video-playlist'
|
||||
import { AccountModel } from './account'
|
||||
import { UserNotificationSettingModel } from './user-notification-setting'
|
||||
import { ActorImageModel } from './actor-image'
|
||||
|
||||
enum ScopeNames {
|
||||
FOR_ME_API = 'FOR_ME_API',
|
||||
@ -97,7 +98,20 @@ enum ScopeNames {
|
||||
model: AccountModel,
|
||||
include: [
|
||||
{
|
||||
model: VideoChannelModel
|
||||
model: VideoChannelModel.unscoped(),
|
||||
include: [
|
||||
{
|
||||
model: ActorModel,
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
model: ActorImageModel,
|
||||
as: 'Banner',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
attributes: [ 'id', 'name', 'type' ],
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
doubleFollow,
|
||||
flushAndRunMultipleServers,
|
||||
getVideo,
|
||||
getVideoChannel,
|
||||
getVideoChannelVideos,
|
||||
testImage,
|
||||
updateVideo,
|
||||
@ -306,7 +307,8 @@ describe('Test video channels', function () {
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const videoChannel = await findChannel(server, secondVideoChannelId)
|
||||
const res = await getVideoChannel(server.url, 'second_video_channel@' + servers[0].host)
|
||||
const videoChannel = res.body
|
||||
|
||||
await testImage(server.url, 'banner-resized', videoChannel.banner.path)
|
||||
}
|
||||
|
@ -151,6 +151,15 @@ export interface ServerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
banner: {
|
||||
file: {
|
||||
size: {
|
||||
max: number
|
||||
}
|
||||
extensions: string[]
|
||||
}
|
||||
}
|
||||
|
||||
video: {
|
||||
image: {
|
||||
size: {
|
||||
|
Loading…
Reference in New Issue
Block a user