Commit cdeddff1 authored by Chocobozzz's avatar Chocobozzz Committed by Chocobozzz

Add ability to update the banner

parent 282695e6
......@@ -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 () {
......
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
}
......
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: [
......
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 },
......
<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>
@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%;
}
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 () {
......
<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>
@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;
}
}
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 () {