From cdeddff142fd20f8cb8bb346625909d61c596603 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 7 Apr 2021 17:01:29 +0200 Subject: [PATCH] Add ability to update the banner --- client/src/app/+admin/admin.module.ts | 2 + .../users/user-edit/user-edit.component.html | 2 +- .../users/user-edit/user-edit.component.scss | 8 -- .../my-account-settings.component.html | 2 +- .../src/app/+my-account/my-account.module.ts | 6 +- .../my-video-channel-edit.component.html | 11 ++- .../my-video-channel-edit.component.scss | 7 +- .../my-video-channel-edit.ts | 2 + .../my-video-channel-update.component.ts | 38 +++++++- .../my-video-channels.module.ts | 4 +- .../video-avatar-channel.component.html | 0 .../video-avatar-channel.component.scss | 0 .../video-avatar-channel.component.ts | 2 +- .../+video-watch/video-watch.component.ts | 7 +- .../+video-watch/video-watch.module.ts | 3 + client/src/app/core/server/server.service.ts | 6 ++ .../actor-avatar-edit.component.html | 41 +++++++++ .../actor-avatar-edit.component.scss | 54 +++++++++++ .../actor-avatar-edit.component.ts} | 30 +++--- .../actor-banner-edit.component.html | 34 +++++++ .../actor-banner-edit.component.scss | 27 ++++++ .../actor-banner-edit.component.ts | 65 +++++++++++++ .../shared-actor-image/actor-image-edit.scss | 35 +++++++ .../app/shared/shared-actor-image/index.ts | 1 + .../shared-actor-image.module.ts | 29 ++++++ .../account/actor-avatar-info.component.html | 42 --------- .../account/actor-avatar-info.component.scss | 92 ------------------- .../shared/shared-main/account/actor.model.ts | 9 +- .../app/shared/shared-main/account/index.ts | 2 - .../shared/shared-main/shared-main.module.ts | 14 +-- .../video-channel/video-channel.model.ts | 40 +++++++- .../video-channel/video-channel.service.ts | 10 +- .../shared/shared-moderation/moderation.scss | 2 +- .../report-modals/report.component.scss | 2 +- .../video-miniature.component.scss | 2 +- client/src/sass/include/_mixins.scss | 10 +- client/src/sass/include/_variables.scss | 3 + server/controllers/api/config.ts | 8 ++ server/models/account/actor-image.ts | 6 +- server/models/account/user.ts | 16 +++- server/tests/api/videos/video-channels.ts | 4 +- shared/models/server/server-config.model.ts | 9 ++ 42 files changed, 481 insertions(+), 206 deletions(-) rename client/src/app/{shared/shared-main/account => +videos/+video-watch}/video-avatar-channel.component.html (100%) rename client/src/app/{shared/shared-main/account => +videos/+video-watch}/video-avatar-channel.component.scss (100%) rename client/src/app/{shared/shared-main/account => +videos/+video-watch}/video-avatar-channel.component.ts (93%) create mode 100644 client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html create mode 100644 client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss rename client/src/app/shared/{shared-main/account/actor-avatar-info.component.ts => shared-actor-image/actor-avatar-edit.component.ts} (70%) create mode 100644 client/src/app/shared/shared-actor-image/actor-banner-edit.component.html create mode 100644 client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss create mode 100644 client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts create mode 100644 client/src/app/shared/shared-actor-image/actor-image-edit.scss create mode 100644 client/src/app/shared/shared-actor-image/index.ts create mode 100644 client/src/app/shared/shared-actor-image/shared-actor-image.module.ts delete mode 100644 client/src/app/shared/shared-main/account/actor-avatar-info.component.html delete mode 100644 client/src/app/shared/shared-main/account/actor-avatar-info.component.scss diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index fd648a425..bac65c88e 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts @@ -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, diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html index 243c6556a..5e92c0f36 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.html +++ b/client/src/app/+admin/users/user-edit/user-edit.component.html @@ -72,7 +72,7 @@
NEW USER
- +
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.scss b/client/src/app/+admin/users/user-edit/user-edit.component.scss index aa87b8d6d..8b0ac8783 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.scss +++ b/client/src/app/+admin/users/user-edit/user-edit.component.scss @@ -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; - } -} diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html index b0d2ec58d..48d06280b 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html @@ -3,7 +3,7 @@
- +
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index 076864563..3df48d0aa 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts @@ -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: [ diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html index 735f9e3ba..7b8928907 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html @@ -44,10 +44,17 @@ - Banner image of your channel + + + + + >
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss index 8f8af655c..22de103d1 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss @@ -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); } diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts index 3e20a27ee..0cdf2fe34 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts @@ -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 () { diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts index 6cd1ff503..22935a87a 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts @@ -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 } diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts index 92b56db49..53557ca02 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts @@ -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: [ diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.html b/client/src/app/+videos/+video-watch/video-avatar-channel.component.html similarity index 100% rename from client/src/app/shared/shared-main/account/video-avatar-channel.component.html rename to client/src/app/+videos/+video-watch/video-avatar-channel.component.html diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.scss b/client/src/app/+videos/+video-watch/video-avatar-channel.component.scss similarity index 100% rename from client/src/app/shared/shared-main/account/video-avatar-channel.component.scss rename to client/src/app/+videos/+video-watch/video-avatar-channel.component.scss diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts b/client/src/app/+videos/+video-watch/video-avatar-channel.component.ts similarity index 93% rename from client/src/app/shared/shared-main/account/video-avatar-channel.component.ts rename to client/src/app/+videos/+video-watch/video-avatar-channel.component.ts index 440e2b522..0b6e796df 100644 --- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts +++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.ts @@ -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', diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index 9656f08e9..7f3ceeebc 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts @@ -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, diff --git a/client/src/app/+videos/+video-watch/video-watch.module.ts b/client/src/app/+videos/+video-watch/video-watch.module.ts index d65cf8d68..3e9f3822e 100644 --- a/client/src/app/+videos/+video-watch/video-watch.module.ts +++ b/client/src/app/+videos/+video-watch/video-watch.module.ts @@ -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 ], diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 11288fc54..906191ae1 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts @@ -98,6 +98,12 @@ export class ServerService { extensions: [] } }, + banner: { + file: { + size: { max: 0 }, + extensions: [] + } + }, video: { image: { size: { max: 0 }, diff --git a/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html new file mode 100644 index 000000000..10f2ef262 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html @@ -0,0 +1,41 @@ +
+
+ Avatar + +
+ +
+ + + +
+ +
+ + +
+ +
+
+ +
+
{{ actor.displayName }}
+
{{ actor.name }}
+
{{ actor.followersCount }} subscribers
+
+
+ + + + + diff --git a/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss new file mode 100644 index 000000000..8b0172315 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss @@ -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%; +} diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts similarity index 70% rename from client/src/app/shared/shared-main/account/actor-avatar-info.component.ts rename to client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts index 87e9e917c..6f76172e9 100644 --- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts +++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts @@ -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 @ViewChild('avatarPopover') avatarPopover: NgbPopover @Input() actor: VideoChannel | Account + @Input() editable = true + @Input() displaySubscribers = true + @Input() displayUsername = true @Output() avatarChange = new EventEmitter() @Output() avatarDelete = new EventEmitter() @@ -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 () { diff --git a/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html new file mode 100644 index 000000000..eb1b66422 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html @@ -0,0 +1,34 @@ +
+
+ + +
+ + + +
+ +
+ + +
+
+
+ + + + + + diff --git a/client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss new file mode 100644 index 000000000..23606f871 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss @@ -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; + } +} diff --git a/client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts new file mode 100644 index 000000000..b92ecef4b --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts @@ -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 + @ViewChild('bannerPopover') bannerPopover: NgbPopover + + @Input() actor: VideoChannel + + @Output() bannerChange = new EventEmitter() + @Output() bannerDelete = new EventEmitter() + + 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 + } +} diff --git a/client/src/app/shared/shared-actor-image/actor-image-edit.scss b/client/src/app/shared/shared-actor-image/actor-image-edit.scss new file mode 100644 index 000000000..918955a89 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-image-edit.scss @@ -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; + } +} diff --git a/client/src/app/shared/shared-actor-image/index.ts b/client/src/app/shared/shared-actor-image/index.ts new file mode 100644 index 000000000..18a9038eb --- /dev/null +++ b/client/src/app/shared/shared-actor-image/index.ts @@ -0,0 +1 @@ +export * from './shared-actor-image.module' diff --git a/client/src/app/shared/shared-actor-image/shared-actor-image.module.ts b/client/src/app/shared/shared-actor-image/shared-actor-image.module.ts new file mode 100644 index 000000000..6044f9925 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/shared-actor-image.module.ts @@ -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 { } diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.html b/client/src/app/shared/shared-main/account/actor-avatar-info.component.html deleted file mode 100644 index f3db55310..000000000 --- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.html +++ /dev/null @@ -1,42 +0,0 @@ - -
-
- Avatar - -
- -
- - - -
- -
- - -
- -
-
- -
-
-
{{ actor.displayName }}
-
{{ actor.name }}
-
-
{{ actor.followersCount }} subscribers
-
-
-
- - - - - diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss b/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss deleted file mode 100644 index 40ba4269b..000000000 --- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss +++ /dev/null @@ -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; - } - } -} diff --git a/client/src/app/shared/shared-main/account/actor.model.ts b/client/src/app/shared/shared-main/account/actor.model.ts index c105a88ac..670823060 100644 --- a/client/src/app/shared/shared-main/account/actor.model.ts +++ b/client/src/app/shared/shared-main/account/actor.model.ts @@ -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) { diff --git a/client/src/app/shared/shared-main/account/index.ts b/client/src/app/shared/shared-main/account/index.ts index 61c800e56..b80ddb9f5 100644 --- a/client/src/app/shared/shared-main/account/index.ts +++ b/client/src/app/shared/shared-main/account/index.ts @@ -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' diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts index 3e21d491a..16d230f46 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts @@ -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, diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts index b4c3365a9..d8be42eef 100644 --- a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts +++ b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts @@ -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) } } diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts index 3f9ef74fa..e65261763 100644 --- a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts +++ b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts @@ -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( diff --git a/client/src/app/shared/shared-moderation/moderation.scss b/client/src/app/shared/shared-moderation/moderation.scss index 4a4e05535..cdcc12fe0 100644 --- a/client/src/app/shared/shared-moderation/moderation.scss +++ b/client/src/app/shared/shared-moderation/moderation.scss @@ -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; diff --git a/client/src/app/shared/shared-moderation/report-modals/report.component.scss b/client/src/app/shared/shared-moderation/report-modals/report.component.scss index b2606cbd8..0567330f5 100644 --- a/client/src/app/shared/shared-moderation/report-modals/report.component.scss +++ b/client/src/app/shared/shared-moderation/report-modals/report.component.scss @@ -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; }; } diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss index 1b50f3290..621951919 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss @@ -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 { diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index e37b89c62..bf844ac5d 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss @@ -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; diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss index c451febdc..3501b305f 100644 --- a/client/src/sass/include/_variables.scss +++ b/client/src/sass/include/_variables.scss @@ -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; diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 313513cea..e28f7502d 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts @@ -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, diff --git a/server/models/account/actor-image.ts b/server/models/account/actor-image.ts index c532bd08d..b779e3cf6 100644 --- a/server/models/account/actor-image.ts +++ b/server/models/account/actor-image.ts @@ -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 () { diff --git a/server/models/account/user.ts b/server/models/account/user.ts index a7a65c489..00c6d73aa 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -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' ], diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts index 8033b9ba5..e50582218 100644 --- a/server/tests/api/videos/video-channels.ts +++ b/server/tests/api/videos/video-channels.ts @@ -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) } diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts index efde4ad9d..85d84af44 100644 --- a/shared/models/server/server-config.model.ts +++ b/shared/models/server/server-config.model.ts @@ -151,6 +151,15 @@ export interface ServerConfig { } } + banner: { + file: { + size: { + max: number + } + extensions: string[] + } + } + video: { image: { size: {