From cdeddff142fd20f8cb8bb346625909d61c596603 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 7 Apr 2021 17:01:29 +0200 Subject: 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 +- client/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-channels/my-video-channel-edit.ts | 2 + .../my-video-channel-update.component.ts | 38 ++++++++- .../+my-video-channels/my-video-channels.module.ts | 4 +- .../video-avatar-channel.component.html | 27 +++++++ .../video-avatar-channel.component.scss | 44 +++++++++++ .../+video-watch/video-avatar-channel.component.ts | 27 +++++++ .../+videos/+video-watch/video-watch.component.ts | 7 +- .../app/+videos/+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 | 73 +++++++++++++++++ .../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 ++++++++ client/src/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 ---------------------- .../account/actor-avatar-info.component.ts | 77 ------------------ .../app/shared/shared-main/account/actor.model.ts | 9 ++- client/src/app/shared/shared-main/account/index.ts | 2 - .../account/video-avatar-channel.component.html | 27 ------- .../account/video-avatar-channel.component.scss | 44 ----------- .../account/video-avatar-channel.component.ts | 27 ------- .../app/shared/shared-main/shared-main.module.ts | 14 +--- .../video-channel/video-channel.model.ts | 40 +++++++++- .../video-channel/video-channel.service.ts | 10 +-- .../app/shared/shared-moderation/moderation.scss | 2 +- .../report-modals/report.component.scss | 2 +- .../video-miniature.component.scss | 2 +- 39 files changed, 589 insertions(+), 356 deletions(-) create mode 100644 client/src/app/+videos/+video-watch/video-avatar-channel.component.html create mode 100644 client/src/app/+videos/+video-watch/video-avatar-channel.component.scss create mode 100644 client/src/app/+videos/+video-watch/video-avatar-channel.component.ts 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 create mode 100644 client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts 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 delete mode 100644 client/src/app/shared/shared-main/account/actor-avatar-info.component.ts delete mode 100644 client/src/app/shared/shared-main/account/video-avatar-channel.component.html delete mode 100644 client/src/app/shared/shared-main/account/video-avatar-channel.component.scss delete mode 100644 client/src/app/shared/shared-main/account/video-avatar-channel.component.ts (limited to 'client/src/app') 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/+videos/+video-watch/video-avatar-channel.component.html b/client/src/app/+videos/+video-watch/video-avatar-channel.component.html new file mode 100644 index 000000000..5058f05dd --- /dev/null +++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.html @@ -0,0 +1,27 @@ +
+ + + Channel avatar + + + + Account avatar + + + + + + Account avatar + + + + Channel avatar + + + + + + Account avatar + + +
diff --git a/client/src/app/+videos/+video-watch/video-avatar-channel.component.scss b/client/src/app/+videos/+video-watch/video-avatar-channel.component.scss new file mode 100644 index 000000000..4998e85fa --- /dev/null +++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.scss @@ -0,0 +1,44 @@ +@import '_mixins'; + +.wrapper { + $avatar-size: 35px; + + width: $avatar-size; + height: $avatar-size; + position: relative; + margin-right: 5px; + margin-bottom: 5px; + + &.avatar-sm { + width: 28px; + height: 28px; + margin-bottom: 3px; + } + + a { + @include disable-outline; + } + + a img { + height: 100%; + object-fit: cover; + position: absolute; + top:50%; + left:50%; + transform: translate(-50%,-50%); + border-radius: 5px; + + &:not(.channel-avatar) { + border-radius: 50%; + } + } + + a:nth-of-type(2) img { + height: 60%; + width: 60%; + border: 2px solid pvar(--mainBackgroundColor); + transform: translateX(15%); + position: relative; + background-color: pvar(--mainBackgroundColor); + } +} diff --git a/client/src/app/+videos/+video-watch/video-avatar-channel.component.ts b/client/src/app/+videos/+video-watch/video-avatar-channel.component.ts new file mode 100644 index 000000000..0b6e796df --- /dev/null +++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.ts @@ -0,0 +1,27 @@ +import { Component, Input, OnInit } from '@angular/core' +import { Video } from '@app/shared/shared-main/video' + +@Component({ + selector: 'my-video-avatar-channel', + templateUrl: './video-avatar-channel.component.html', + styleUrls: [ './video-avatar-channel.component.scss' ] +}) +export class VideoAvatarChannelComponent implements OnInit { + @Input() video: Video + @Input() byAccount: string + + @Input() size: 'md' | 'sm' = 'md' + @Input() genericChannel: boolean + + channelLinkTitle = '' + accountLinkTitle = '' + + ngOnInit () { + this.channelLinkTitle = $localize`${this.video.account.name} (channel page)` + this.accountLinkTitle = $localize`${this.video.byAccount} (account page)` + } + + isChannelAvatarNull () { + return this.video.channel.avatar === null + } +} 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-actor-image/actor-avatar-edit.component.ts b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts new file mode 100644 index 000000000..6f76172e9 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts @@ -0,0 +1,73 @@ +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' + +@Component({ + selector: 'my-actor-avatar-edit', + templateUrl: './actor-avatar-edit.component.html', + styleUrls: [ + './actor-image-edit.scss', + './actor-avatar-edit.component.scss' + ] +}) +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() + + avatarFormat = '' + maxAvatarSize = 0 + avatarExtensions = '' + + constructor ( + private serverService: ServerService, + private notifier: Notifier + ) { } + + ngOnInit (): void { + this.serverService.getConfig() + .subscribe(config => { + this.maxAvatarSize = config.avatar.file.size.max + this.avatarExtensions = config.avatar.file.extensions.join(', ') + + this.avatarFormat = `${$localize`max size`}: 192*192px, ` + + `${getBytes(this.maxAvatarSize)} ${$localize`extensions`}: ${this.avatarExtensions}` + }) + } + + onAvatarChange (input: HTMLInputElement) { + this.avatarfileInput = new ElementRef(input) + + const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ] + if (avatarfile.size > this.maxAvatarSize) { + this.notifier.error('Error', $localize`This image is too large.`) + return + } + + const formData = new FormData() + formData.append('avatarfile', avatarfile) + this.avatarPopover?.close() + this.avatarChange.emit(formData) + } + + deleteAvatar () { + this.avatarDelete.emit() + } + + hasAvatar () { + return !!this.actor.avatar + } + + isChannel () { + return !!(this.actor as VideoChannel).ownerAccount + } +} 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-avatar-info.component.ts b/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts deleted file mode 100644 index 87e9e917c..000000000 --- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core' -import { Notifier, ServerService } from '@app/core' -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' ] -}) -export class ActorAvatarInfoComponent implements OnInit, OnChanges { - @ViewChild('avatarfileInput') avatarfileInput: ElementRef - @ViewChild('avatarPopover') avatarPopover: NgbPopover - - @Input() actor: VideoChannel | Account - - @Output() avatarChange = new EventEmitter() - @Output() avatarDelete = new EventEmitter() - - avatarFormat = '' - maxAvatarSize = 0 - avatarExtensions = '' - - private avatarUrl: string - - constructor ( - private serverService: ServerService, - private notifier: Notifier - ) { } - - ngOnInit (): void { - this.serverService.getConfig() - .subscribe(config => { - this.maxAvatarSize = config.avatar.file.size.max - this.avatarExtensions = config.avatar.file.extensions.join(', ') - - this.avatarFormat = `${$localize`max size`}: 192*192px, ` + - `${getBytes(this.maxAvatarSize)} ${$localize`extensions`}: ${this.avatarExtensions}` - }) - } - - ngOnChanges (changes: SimpleChanges) { - if (changes['actor']) { - this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.actor) - } - } - - onAvatarChange (input: HTMLInputElement) { - this.avatarfileInput = new ElementRef(input) - - const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ] - if (avatarfile.size > this.maxAvatarSize) { - this.notifier.error('Error', $localize`This image is too large.`) - return - } - - const formData = new FormData() - formData.append('avatarfile', avatarfile) - this.avatarPopover?.close() - this.avatarChange.emit(formData) - } - - deleteAvatar () { - this.avatarDelete.emit() - } - - hasAvatar () { - return !!this.avatarUrl - } - - isChannel () { - return !!(this.actor as VideoChannel).ownerAccount - } -} 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/account/video-avatar-channel.component.html b/client/src/app/shared/shared-main/account/video-avatar-channel.component.html deleted file mode 100644 index 5058f05dd..000000000 --- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.html +++ /dev/null @@ -1,27 +0,0 @@ -
- - - Channel avatar - - - - Account avatar - - - - - - Account avatar - - - - Channel avatar - - - - - - Account avatar - - -
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.scss b/client/src/app/shared/shared-main/account/video-avatar-channel.component.scss deleted file mode 100644 index 4998e85fa..000000000 --- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.scss +++ /dev/null @@ -1,44 +0,0 @@ -@import '_mixins'; - -.wrapper { - $avatar-size: 35px; - - width: $avatar-size; - height: $avatar-size; - position: relative; - margin-right: 5px; - margin-bottom: 5px; - - &.avatar-sm { - width: 28px; - height: 28px; - margin-bottom: 3px; - } - - a { - @include disable-outline; - } - - a img { - height: 100%; - object-fit: cover; - position: absolute; - top:50%; - left:50%; - transform: translate(-50%,-50%); - border-radius: 5px; - - &:not(.channel-avatar) { - border-radius: 50%; - } - } - - a:nth-of-type(2) img { - height: 60%; - width: 60%; - border: 2px solid pvar(--mainBackgroundColor); - transform: translateX(15%); - position: relative; - background-color: pvar(--mainBackgroundColor); - } -} diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts b/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts deleted file mode 100644 index 440e2b522..000000000 --- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core' -import { Video } from '../video/video.model' - -@Component({ - selector: 'my-video-avatar-channel', - templateUrl: './video-avatar-channel.component.html', - styleUrls: [ './video-avatar-channel.component.scss' ] -}) -export class VideoAvatarChannelComponent implements OnInit { - @Input() video: Video - @Input() byAccount: string - - @Input() size: 'md' | 'sm' = 'md' - @Input() genericChannel: boolean - - channelLinkTitle = '' - accountLinkTitle = '' - - ngOnInit () { - this.channelLinkTitle = $localize`${this.video.account.name} (channel page)` - this.accountLinkTitle = $localize`${this.video.byAccount} (account page)` - } - - isChannelAvatarNull () { - return this.video.channel.avatar === null - } -} 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 { -- cgit v1.2.3