aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-04-07 17:01:29 +0200
committerChocobozzz <chocobozzz@cpy.re>2021-04-08 10:07:53 +0200
commitcdeddff142fd20f8cb8bb346625909d61c596603 (patch)
treee7b0ae302a002fb2eadc605300294a1f135c3744
parent282695e699a35b65441b548061ef0db5de9b3971 (diff)
downloadPeerTube-cdeddff142fd20f8cb8bb346625909d61c596603.tar.gz
PeerTube-cdeddff142fd20f8cb8bb346625909d61c596603.tar.zst
PeerTube-cdeddff142fd20f8cb8bb346625909d61c596603.zip
Add ability to update the banner
-rw-r--r--client/src/app/+admin/admin.module.ts2
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.html2
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.scss8
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-settings.component.html2
-rw-r--r--client/src/app/+my-account/my-account.module.ts6
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html11
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss7
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts2
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts38
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts4
-rw-r--r--client/src/app/+videos/+video-watch/video-avatar-channel.component.html (renamed from client/src/app/shared/shared-main/account/video-avatar-channel.component.html)0
-rw-r--r--client/src/app/+videos/+video-watch/video-avatar-channel.component.scss (renamed from client/src/app/shared/shared-main/account/video-avatar-channel.component.scss)0
-rw-r--r--client/src/app/+videos/+video-watch/video-avatar-channel.component.ts (renamed from client/src/app/shared/shared-main/account/video-avatar-channel.component.ts)2
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts7
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.module.ts3
-rw-r--r--client/src/app/core/server/server.service.ts6
-rw-r--r--client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html41
-rw-r--r--client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss54
-rw-r--r--client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts (renamed from client/src/app/shared/shared-main/account/actor-avatar-info.component.ts)30
-rw-r--r--client/src/app/shared/shared-actor-image/actor-banner-edit.component.html34
-rw-r--r--client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss27
-rw-r--r--client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts65
-rw-r--r--client/src/app/shared/shared-actor-image/actor-image-edit.scss35
-rw-r--r--client/src/app/shared/shared-actor-image/index.ts1
-rw-r--r--client/src/app/shared/shared-actor-image/shared-actor-image.module.ts29
-rw-r--r--client/src/app/shared/shared-main/account/actor-avatar-info.component.html42
-rw-r--r--client/src/app/shared/shared-main/account/actor-avatar-info.component.scss92
-rw-r--r--client/src/app/shared/shared-main/account/actor.model.ts9
-rw-r--r--client/src/app/shared/shared-main/account/index.ts2
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts14
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.model.ts40
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.service.ts10
-rw-r--r--client/src/app/shared/shared-moderation/moderation.scss2
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/report.component.scss2
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.scss2
-rw-r--r--client/src/sass/include/_mixins.scss10
-rw-r--r--client/src/sass/include/_variables.scss3
-rw-r--r--server/controllers/api/config.ts8
-rw-r--r--server/models/account/actor-image.ts6
-rw-r--r--server/models/account/user.ts16
-rw-r--r--server/tests/api/videos/video-channels.ts4
-rw-r--r--shared/models/server/server-config.model.ts9
42 files changed, 481 insertions, 206 deletions
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'
3import { TableModule } from 'primeng/table' 3import { TableModule } from 'primeng/table'
4import { NgModule } from '@angular/core' 4import { NgModule } from '@angular/core'
5import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' 5import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
6import { SharedActorImageModule } from '@app/shared/shared-actor-image'
6import { SharedFormModule } from '@app/shared/shared-forms' 7import { SharedFormModule } from '@app/shared/shared-forms'
7import { SharedGlobalIconModule } from '@app/shared/shared-icons' 8import { SharedGlobalIconModule } from '@app/shared/shared-icons'
8import { SharedMainModule } from '@app/shared/shared-main' 9import { SharedMainModule } from '@app/shared/shared-main'
@@ -49,6 +50,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
49 SharedGlobalIconModule, 50 SharedGlobalIconModule,
50 SharedAbuseListModule, 51 SharedAbuseListModule,
51 SharedVideoCommentModule, 52 SharedVideoCommentModule,
53 SharedActorImageModule,
52 54
53 TableModule, 55 TableModule,
54 SelectButtonModule, 56 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 @@
72 <div class="anchor" id="user"></div> <!-- user anchor --> 72 <div class="anchor" id="user"></div> <!-- user anchor -->
73 <div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div> 73 <div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div>
74 <div *ngIf="!isCreation() && user" class="account-title"> 74 <div *ngIf="!isCreation() && user" class="account-title">
75 <my-actor-avatar-info [actor]="user.account"></my-actor-avatar-info> 75 <my-actor-avatar-edit [actor]="user.account" [editable]="false" [displaySubscribers]="false" [displayUsername]="false"></my-actor-avatar-edit>
76 </div> 76 </div>
77 </div> 77 </div>
78 78
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 {
72 @include dashboard; 72 @include dashboard;
73 max-width: 900px; 73 max-width: 900px;
74} 74}
75
76my-actor-avatar-info ::ng-deep {
77 .actor-img-edit-container,
78 .actor-info-followers,
79 .actor-info-username {
80 display: none;
81 }
82}
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 @@
3 <div class="form-group col-12 col-lg-4 col-xl-3"></div> 3 <div class="form-group col-12 col-lg-4 col-xl-3"></div>
4 4
5 <div class="form-group col-12 col-lg-8 col-xl-9"> 5 <div class="form-group col-12 col-lg-8 col-xl-9">
6 <my-actor-avatar-info [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-info> 6 <my-actor-avatar-edit [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-edit>
7 </div> 7 </div>
8</div> 8</div>
9 9
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'
3import { DragDropModule } from '@angular/cdk/drag-drop' 3import { DragDropModule } from '@angular/cdk/drag-drop'
4import { NgModule } from '@angular/core' 4import { NgModule } from '@angular/core'
5import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' 5import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
6import { SharedActorImageModule } from '@app/shared/shared-actor-image'
6import { SharedFormModule } from '@app/shared/shared-forms' 7import { SharedFormModule } from '@app/shared/shared-forms'
7import { SharedGlobalIconModule } from '@app/shared/shared-icons' 8import { SharedGlobalIconModule } from '@app/shared/shared-icons'
8import { SharedMainModule } from '@app/shared/shared-main' 9import { SharedMainModule } from '@app/shared/shared-main'
@@ -10,6 +11,7 @@ import { SharedModerationModule } from '@app/shared/shared-moderation'
10import { SharedShareModal } from '@app/shared/shared-share-modal' 11import { SharedShareModal } from '@app/shared/shared-share-modal'
11import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' 12import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings'
12import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' 13import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
14import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
13import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' 15import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component'
14import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' 16import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component'
15import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' 17import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component'
@@ -20,7 +22,6 @@ import { MyAccountDangerZoneComponent } from './my-account-settings/my-account-d
20import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' 22import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences'
21import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' 23import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component'
22import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' 24import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
23import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
24import { MyAccountComponent } from './my-account.component' 25import { MyAccountComponent } from './my-account.component'
25 26
26@NgModule({ 27@NgModule({
@@ -37,7 +38,8 @@ import { MyAccountComponent } from './my-account.component'
37 SharedUserInterfaceSettingsModule, 38 SharedUserInterfaceSettingsModule,
38 SharedGlobalIconModule, 39 SharedGlobalIconModule,
39 SharedAbuseListModule, 40 SharedAbuseListModule,
40 SharedShareModal 41 SharedShareModal,
42 SharedActorImageModule
41 ], 43 ],
42 44
43 declarations: [ 45 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 @@
44 </div> 44 </div>
45 </div> 45 </div>
46 46
47 <my-actor-avatar-info 47 <h6 i18n>Banner image of your channel</h6>
48
49 <my-actor-banner-edit
50 *ngIf="!isCreation() && videoChannelToUpdate"
51 [actor]="videoChannelToUpdate" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
52 ></my-actor-banner-edit>
53
54 <my-actor-avatar-edit
48 *ngIf="!isCreation() && videoChannelToUpdate" 55 *ngIf="!isCreation() && videoChannelToUpdate"
49 [actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()" 56 [actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
50 ></my-actor-avatar-info> 57 ></my-actor-avatar-edit>
51 58
52 <div class="form-group"> 59 <div class="form-group">
53 <label i18n for="display-name">Display name</label> 60 <label i18n for="display-name">Display name</label>
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 {
10 @include settings-big-title; 10 @include settings-big-title;
11} 11}
12 12
13my-actor-avatar-info { 13my-actor-avatar-edit,
14my-actor-banner-edit {
14 display: block; 15 display: block;
15 margin-bottom: 20px; 16 margin-bottom: 20px;
16} 17}
17 18
19my-actor-banner-edit {
20 max-width: 500px;
21}
22
18.input-group { 23.input-group {
19 @include peertube-input-group(fit-content); 24 @include peertube-input-group(fit-content);
20} 25}
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 {
15 // We need this method so angular does not complain in child template that doesn't need this 15 // We need this method so angular does not complain in child template that doesn't need this
16 onAvatarChange (formData: FormData) { /* empty */ } 16 onAvatarChange (formData: FormData) { /* empty */ }
17 onAvatarDelete () { /* empty */ } 17 onAvatarDelete () { /* empty */ }
18 onBannerChange (formData: FormData) { /* empty */ }
19 onBannerDelete () { /* empty */ }
18 20
19 // Should be implemented by the child 21 // Should be implemented by the child
20 isBulkUpdateVideosDisplayed () { 22 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 @@
1import { Subscription } from 'rxjs' 1import { Subscription } from 'rxjs'
2import { HttpErrorResponse } from '@angular/common/http'
2import { Component, OnDestroy, OnInit } from '@angular/core' 3import { Component, OnDestroy, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, Notifier, ServerService } from '@app/core' 5import { AuthService, Notifier, ServerService } from '@app/core'
6import { uploadErrorHandler } from '@app/helpers'
5import { 7import {
6 VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, 8 VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
7 VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, 9 VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
@@ -11,8 +13,6 @@ import { FormValidatorService } from '@app/shared/shared-forms'
11import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' 13import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
12import { ServerConfig, VideoChannelUpdate } from '@shared/models' 14import { ServerConfig, VideoChannelUpdate } from '@shared/models'
13import { MyVideoChannelEdit } from './my-video-channel-edit' 15import { MyVideoChannelEdit } from './my-video-channel-edit'
14import { HttpErrorResponse } from '@angular/common/http'
15import { uploadErrorHandler } from '@app/helpers'
16 16
17@Component({ 17@Component({
18 selector: 'my-video-channel-update', 18 selector: 'my-video-channel-update',
@@ -101,7 +101,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
101 } 101 }
102 102
103 onAvatarChange (formData: FormData) { 103 onAvatarChange (formData: FormData) {
104 this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData) 104 this.videoChannelService.changeVideoChannelImage(this.videoChannelToUpdate.name, formData, 'avatar')
105 .subscribe( 105 .subscribe(
106 data => { 106 data => {
107 this.notifier.success($localize`Avatar changed.`) 107 this.notifier.success($localize`Avatar changed.`)
@@ -118,7 +118,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
118 } 118 }
119 119
120 onAvatarDelete () { 120 onAvatarDelete () {
121 this.videoChannelService.deleteVideoChannelAvatar(this.videoChannelToUpdate.name) 121 this.videoChannelService.deleteVideoChannelImage(this.videoChannelToUpdate.name, 'avatar')
122 .subscribe( 122 .subscribe(
123 data => { 123 data => {
124 this.notifier.success($localize`Avatar deleted.`) 124 this.notifier.success($localize`Avatar deleted.`)
@@ -130,6 +130,36 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
130 ) 130 )
131 } 131 }
132 132
133 onBannerChange (formData: FormData) {
134 this.videoChannelService.changeVideoChannelImage(this.videoChannelToUpdate.name, formData, 'banner')
135 .subscribe(
136 data => {
137 this.notifier.success($localize`Banner changed.`)
138
139 this.videoChannelToUpdate.updateBanner(data.banner)
140 },
141
142 (err: HttpErrorResponse) => uploadErrorHandler({
143 err,
144 name: $localize`banner`,
145 notifier: this.notifier
146 })
147 )
148 }
149
150 onBannerDelete () {
151 this.videoChannelService.deleteVideoChannelImage(this.videoChannelToUpdate.name, 'banner')
152 .subscribe(
153 data => {
154 this.notifier.success($localize`Banner deleted.`)
155
156 this.videoChannelToUpdate.resetBanner()
157 },
158
159 err => this.notifier.error(err.message)
160 )
161 }
162
133 get maxAvatarSize () { 163 get maxAvatarSize () {
134 return this.serverConfig.avatar.file.size.max 164 return this.serverConfig.avatar.file.size.max
135 } 165 }
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 @@
1import { ChartModule } from 'primeng/chart' 1import { ChartModule } from 'primeng/chart'
2import { NgModule } from '@angular/core' 2import { NgModule } from '@angular/core'
3import { SharedActorImageModule } from '@app/shared/shared-actor-image'
3import { SharedFormModule } from '@app/shared/shared-forms' 4import { SharedFormModule } from '@app/shared/shared-forms'
4import { SharedGlobalIconModule } from '@app/shared/shared-icons' 5import { SharedGlobalIconModule } from '@app/shared/shared-icons'
5import { SharedMainModule } from '@app/shared/shared-main' 6import { SharedMainModule } from '@app/shared/shared-main'
@@ -16,7 +17,8 @@ import { MyVideoChannelsComponent } from './my-video-channels.component'
16 17
17 SharedMainModule, 18 SharedMainModule,
18 SharedFormModule, 19 SharedFormModule,
19 SharedGlobalIconModule 20 SharedGlobalIconModule,
21 SharedActorImageModule
20 ], 22 ],
21 23
22 declarations: [ 24 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
index 5058f05dd..5058f05dd 100644
--- 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
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
index 4998e85fa..4998e85fa 100644
--- 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
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
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 @@
1import { Component, Input, OnInit } from '@angular/core' 1import { Component, Input, OnInit } from '@angular/core'
2import { Video } from '../video/video.model' 2import { Video } from '@app/shared/shared-main/video'
3 3
4@Component({ 4@Component({
5 selector: 'my-video-avatar-channel', 5 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'
29import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' 29import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
30import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 30import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
31import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models' 31import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models'
32import { cleanupVideoWatch, getStoredP2PEnabled, getStoredTheater, getStoredVideoWatchHistory } from '../../../assets/player/peertube-player-local-storage' 32import {
33 cleanupVideoWatch,
34 getStoredP2PEnabled,
35 getStoredTheater,
36 getStoredVideoWatchHistory
37} from '../../../assets/player/peertube-player-local-storage'
33import { 38import {
34 CustomizationOptions, 39 CustomizationOptions,
35 P2PMediaLoaderOptions, 40 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'
16import { VideoCommentsComponent } from './comment/video-comments.component' 16import { VideoCommentsComponent } from './comment/video-comments.component'
17import { RecommendationsModule } from './recommendations/recommendations.module' 17import { RecommendationsModule } from './recommendations/recommendations.module'
18import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive' 18import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive'
19import { VideoAvatarChannelComponent } from './video-avatar-channel.component'
19import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' 20import { VideoWatchPlaylistComponent } from './video-watch-playlist.component'
20import { VideoWatchRoutingModule } from './video-watch-routing.module' 21import { VideoWatchRoutingModule } from './video-watch-routing.module'
21import { VideoWatchComponent } from './video-watch.component' 22import { VideoWatchComponent } from './video-watch.component'
@@ -46,6 +47,8 @@ import { VideoWatchComponent } from './video-watch.component'
46 VideoCommentAddComponent, 47 VideoCommentAddComponent,
47 VideoCommentComponent, 48 VideoCommentComponent,
48 49
50 VideoAvatarChannelComponent,
51
49 TimestampRouteTransformerDirective, 52 TimestampRouteTransformerDirective,
50 TimestampRouteTransformerDirective 53 TimestampRouteTransformerDirective
51 ], 54 ],
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 {
98 extensions: [] 98 extensions: []
99 } 99 }
100 }, 100 },
101 banner: {
102 file: {
103 size: { max: 0 },
104 extensions: []
105 }
106 },
101 video: { 107 video: {
102 image: { 108 image: {
103 size: { max: 0 }, 109 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 @@
1<div class="actor" *ngIf="actor">
2 <div class="d-flex">
3 <img [ngClass]="{ channel: isChannel() }" [src]="actor.avatarUrl" alt="Avatar" />
4
5 <div class="actor-img-edit-container">
6
7 <div *ngIf="editable && !hasAvatar()" class="actor-img-edit-button" [ngbTooltip]="avatarFormat" placement="right" container="body">
8 <my-global-icon iconName="upload"></my-global-icon>
9 <label class="sr-only" for="avatarfile" i18n>Upload a new avatar</label>
10 <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
11 </div>
12
13 <div
14 *ngIf="editable && hasAvatar()" class="actor-img-edit-button"
15 #avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-image-info" autoClose="outside" placement="right"
16 >
17 <my-global-icon iconName="edit"></my-global-icon>
18 <label class="sr-only" for="avatarMenu" i18n>Change your avatar</label>
19 </div>
20
21 </div>
22 </div>
23
24 <div class="actor-info">
25 <div class="actor-info-display-name">{{ actor.displayName }}</div>
26 <div *ngIf="displayUsername" class="actor-info-username">{{ actor.name }}</div>
27 <div *ngIf="displaySubscribers" i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div>
28 </div>
29</div>
30
31<ng-template #avatarEditContent>
32 <div class="dropdown-item c-hand" [ngbTooltip]="avatarFormat" placement="right" container="body">
33 <my-global-icon iconName="upload"></my-global-icon>
34 <span for="avatarfile" i18n>Upload a new avatar</span>
35 <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
36 </div>
37 <div class="dropdown-item c-hand" (click)="deleteAvatar()" (key.enter)="deleteAvatar()">
38 <my-global-icon iconName="delete"></my-global-icon>
39 <span i18n>Remove avatar</span>
40 </div>
41</ng-template>
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 @@
1@import '_variables';
2@import '_mixins';
3
4.actor {
5 display: flex;
6
7 img {
8 margin-right: 15px;
9
10 &:not(.channel) {
11 @include avatar(100px);
12 }
13
14 &.channel {
15 @include channel-avatar(100px);
16 }
17 }
18
19 .actor-info {
20 display: inline-flex;
21 flex-direction: column;
22
23 .actor-info-display-name {
24 font-size: 20px;
25 font-weight: $font-bold;
26
27 @media screen and (max-width: $small-view) {
28 font-size: 16px;
29 }
30 }
31
32 .actor-info-username {
33 position: relative;
34 font-size: 14px;
35 color: pvar(--greyForegroundColor);
36 }
37
38 .actor-info-followers {
39 font-size: 15px;
40 padding-bottom: .5rem;
41 }
42 }
43}
44
45.actor-img-edit-container {
46 position: relative;
47 width: 0;
48}
49
50.actor-img-edit-button {
51 top: 55px;
52 right: 45px;
53 border-radius: 50%;
54}
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
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 @@
1import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier, ServerService } from '@app/core' 2import { Notifier, ServerService } from '@app/core'
3import { Account, VideoChannel } from '@app/shared/shared-main'
3import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' 4import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
4import { getBytes } from '@root-helpers/bytes' 5import { getBytes } from '@root-helpers/bytes'
5import { Account } from '../account/account.model'
6import { VideoChannel } from '../video-channel/video-channel.model'
7import { Actor } from './actor.model'
8 6
9@Component({ 7@Component({
10 selector: 'my-actor-avatar-info', 8 selector: 'my-actor-avatar-edit',
11 templateUrl: './actor-avatar-info.component.html', 9 templateUrl: './actor-avatar-edit.component.html',
12 styleUrls: [ './actor-avatar-info.component.scss' ] 10 styleUrls: [
11 './actor-image-edit.scss',
12 './actor-avatar-edit.component.scss'
13 ]
13}) 14})
14export class ActorAvatarInfoComponent implements OnInit, OnChanges { 15export class ActorAvatarEditComponent implements OnInit {
15 @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement> 16 @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement>
16 @ViewChild('avatarPopover') avatarPopover: NgbPopover 17 @ViewChild('avatarPopover') avatarPopover: NgbPopover
17 18
18 @Input() actor: VideoChannel | Account 19 @Input() actor: VideoChannel | Account
20 @Input() editable = true
21 @Input() displaySubscribers = true
22 @Input() displayUsername = true
19 23
20 @Output() avatarChange = new EventEmitter<FormData>() 24 @Output() avatarChange = new EventEmitter<FormData>()
21 @Output() avatarDelete = new EventEmitter<void>() 25 @Output() avatarDelete = new EventEmitter<void>()
@@ -24,8 +28,6 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
24 maxAvatarSize = 0 28 maxAvatarSize = 0
25 avatarExtensions = '' 29 avatarExtensions = ''
26 30
27 private avatarUrl: string
28
29 constructor ( 31 constructor (
30 private serverService: ServerService, 32 private serverService: ServerService,
31 private notifier: Notifier 33 private notifier: Notifier
@@ -42,12 +44,6 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
42 }) 44 })
43 } 45 }
44 46
45 ngOnChanges (changes: SimpleChanges) {
46 if (changes['actor']) {
47 this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.actor)
48 }
49 }
50
51 onAvatarChange (input: HTMLInputElement) { 47 onAvatarChange (input: HTMLInputElement) {
52 this.avatarfileInput = new ElementRef(input) 48 this.avatarfileInput = new ElementRef(input)
53 49
@@ -68,7 +64,7 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
68 } 64 }
69 65
70 hasAvatar () { 66 hasAvatar () {
71 return !!this.avatarUrl 67 return !!this.actor.avatar
72 } 68 }
73 69
74 isChannel () { 70 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 @@
1<div class="actor" *ngIf="actor">
2 <div class="actor-img-edit-container">
3 <div class="banner-placeholder">
4 <img *ngIf="hasBanner()" [src]="actor.bannerUrl" alt="Banner" />
5 </div>
6
7 <div *ngIf="!hasBanner()" class="actor-img-edit-button" [ngbTooltip]="bannerFormat" placement="right" container="body">
8 <my-global-icon iconName="upload"></my-global-icon>
9 <label for="bannerfile" i18n>Upload a new banner</label>
10 <input #bannerfileInput type="file" name="bannerfile" id="bannerfile" [accept]="bannerExtensions" (change)="onBannerChange(bannerfileInput)"/>
11 </div>
12
13 <div
14 *ngIf="hasBanner()" class="actor-img-edit-button"
15 #bannerPopover="ngbPopover" [ngbPopover]="bannerEditContent" popoverClass="popover-image-info" autoClose="outside" placement="right"
16 >
17 <my-global-icon iconName="edit"></my-global-icon>
18 <label for="bannerMenu" i18n>Change your banner</label>
19 </div>
20 </div>
21</div>
22
23<ng-template #bannerEditContent>
24 <div class="dropdown-item c-hand" [ngbTooltip]="bannerFormat" placement="right" container="body">
25 <my-global-icon iconName="upload"></my-global-icon>
26 <span for="bannerfile" i18n>Upload a new banner</span>
27 <input #bannerfileInput type="file" name="bannerfile" id="bannerfile" [accept]="bannerExtensions" (change)="onBannerChange(bannerfileInput)"/>
28 </div>
29
30 <div class="dropdown-item c-hand" (click)="deleteBanner()" (key.enter)="deleteBanner()">
31 <my-global-icon iconName="delete"></my-global-icon>
32 <span i18n>Remove banner</span>
33 </div>
34</ng-template>
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 @@
1@import '_variables';
2@import '_mixins';
3
4.banner-placeholder {
5 @include block-ratio('> div, > img', $banner-inverted-ratio);
6}
7
8.banner-placeholder {
9 background-color: pvar(--greyBackgroundColor);
10}
11
12.actor-img-edit-container {
13 position: relative;
14 display: flex;
15 justify-content: center;
16 align-items: center;
17}
18
19.actor-img-edit-button {
20 position: absolute;
21 width: auto;
22
23 label {
24 font-weight: $font-semibold;
25 margin-bottom: 0;
26 }
27}
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 @@
1import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'
2import { Notifier, ServerService } from '@app/core'
3import { VideoChannel } from '@app/shared/shared-main'
4import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
5import { getBytes } from '@root-helpers/bytes'
6
7@Component({
8 selector: 'my-actor-banner-edit',
9 templateUrl: './actor-banner-edit.component.html',
10 styleUrls: [
11 './actor-image-edit.scss',
12 './actor-banner-edit.component.scss'
13 ]
14})
15export class ActorBannerEditComponent implements OnInit {
16 @ViewChild('bannerfileInput') bannerfileInput: ElementRef<HTMLInputElement>
17 @ViewChild('bannerPopover') bannerPopover: NgbPopover
18
19 @Input() actor: VideoChannel
20
21 @Output() bannerChange = new EventEmitter<FormData>()
22 @Output() bannerDelete = new EventEmitter<void>()
23
24 bannerFormat = ''
25 maxBannerSize = 0
26 bannerExtensions = ''
27
28 constructor (
29 private serverService: ServerService,
30 private notifier: Notifier
31 ) { }
32
33 ngOnInit (): void {
34 this.serverService.getConfig()
35 .subscribe(config => {
36 this.maxBannerSize = config.banner.file.size.max
37 this.bannerExtensions = config.banner.file.extensions.join(', ')
38
39 this.bannerFormat = $localize`maxsize: ${getBytes(this.maxBannerSize)}, extensions: ${this.bannerExtensions}`
40 })
41 }
42
43 onBannerChange (input: HTMLInputElement) {
44 this.bannerfileInput = new ElementRef(input)
45
46 const bannerfile = this.bannerfileInput.nativeElement.files[ 0 ]
47 if (bannerfile.size > this.maxBannerSize) {
48 this.notifier.error('Error', $localize`This image is too large.`)
49 return
50 }
51
52 const formData = new FormData()
53 formData.append('bannerfile', bannerfile)
54 this.bannerPopover?.close()
55 this.bannerChange.emit(formData)
56 }
57
58 deleteBanner () {
59 this.bannerDelete.emit()
60 }
61
62 hasBanner () {
63 return !!this.actor.bannerUrl
64 }
65}
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 @@
1@import '_variables';
2@import '_mixins';
3
4.actor ::ng-deep .popover-image-info .popover-body {
5 padding: 0;
6
7 .dropdown-item {
8 padding: 6px 10px;
9 border-radius: 4px;
10
11 &:first-child {
12 @include peertube-file;
13 display: block;
14 }
15 }
16}
17
18.actor-img-edit-button {
19 @include peertube-button-file(21px);
20 @include button-with-icon(19px);
21 @include orange-button;
22
23 margin-top: 10px;
24 margin-bottom: 5px;
25 cursor: pointer;
26
27 input {
28 width: 30px;
29 height: 30px;
30 }
31
32 my-global-icon {
33 right: 7px;
34 }
35}
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 @@
1
2import { CommonModule } from '@angular/common'
3import { NgModule } from '@angular/core'
4import { SharedGlobalIconModule } from '../shared-icons'
5import { SharedMainModule } from '../shared-main'
6import { ActorAvatarEditComponent } from './actor-avatar-edit.component'
7import { ActorBannerEditComponent } from './actor-banner-edit.component'
8
9@NgModule({
10 imports: [
11 CommonModule,
12
13 SharedMainModule,
14 SharedGlobalIconModule
15 ],
16
17 declarations: [
18 ActorAvatarEditComponent,
19 ActorBannerEditComponent
20 ],
21
22 exports: [
23 ActorAvatarEditComponent,
24 ActorBannerEditComponent
25 ],
26
27 providers: [ ]
28})
29export 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 @@
1<ng-container *ngIf="actor">
2 <div class="actor">
3 <div class="d-flex">
4 <img [ngClass]="{ channel: isChannel() }" [src]="actor.avatarUrl" alt="Avatar" />
5
6 <div class="actor-img-edit-container">
7
8 <div *ngIf="!hasAvatar()" class="actor-img-edit-button" [ngbTooltip]="avatarFormat" placement="right" container="body">
9 <my-global-icon iconName="upload"></my-global-icon>
10 <label class="sr-only" for="avatarfile" i18n>Upload a new avatar</label>
11 <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
12 </div>
13
14 <div *ngIf="hasAvatar()" class="actor-img-edit-button" #avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-avatar-info" autoClose="outside" placement="right">
15 <my-global-icon iconName="edit"></my-global-icon>
16 <label class="sr-only" for="avatarMenu" i18n>Change your avatar</label>
17 </div>
18
19 </div>
20 </div>
21
22 <div class="actor-info">
23 <div class="actor-info-names">
24 <div class="actor-info-display-name">{{ actor.displayName }}</div>
25 <div class="actor-info-username">{{ actor.name }}</div>
26 </div>
27 <div i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div>
28 </div>
29 </div>
30</ng-container>
31
32<ng-template #avatarEditContent>
33 <div class="dropdown-item c-hand" [ngbTooltip]="avatarFormat" placement="right" container="body">
34 <my-global-icon iconName="upload"></my-global-icon>
35 <span for="avatarfile" i18n>Upload a new avatar</span>
36 <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
37 </div>
38 <div class="dropdown-item c-hand" (click)="deleteAvatar()" (key.enter)="deleteAvatar()">
39 <my-global-icon iconName="delete"></my-global-icon>
40 <span i18n>Remove avatar</span>
41 </div>
42</ng-template>
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 @@
1@import '_variables';
2@import '_mixins';
3
4.actor {
5 display: flex;
6
7 img {
8 margin-right: 15px;
9
10 &:not(.channel) {
11 @include avatar(100px);
12 }
13
14 &.channel {
15 @include channel-avatar(100px);
16 }
17 }
18
19 .actor-img-edit-container {
20 position: relative;
21 width: 0;
22
23 .actor-img-edit-button {
24 @include peertube-button-file(21px);
25 @include button-with-icon(19px);
26 @include orange-button;
27
28 margin-top: 10px;
29 margin-bottom: 5px;
30 border-radius: 50%;
31 top: 55px;
32 right: 45px;
33 cursor: pointer;
34
35 input {
36 width: 30px;
37 height: 30px;
38 }
39
40 my-global-icon {
41 right: 7px;
42 }
43 }
44 }
45
46 .actor-info {
47 justify-content: center;
48 display: inline-flex;
49 flex-direction: column;
50
51 .actor-info-names {
52 display: flex;
53 align-items: center;
54
55 .actor-info-display-name {
56 font-size: 20px;
57 font-weight: $font-bold;
58
59 @media screen and (max-width: $small-view) {
60 font-size: 16px;
61 }
62 }
63
64 .actor-info-username {
65 margin-left: 7px;
66 position: relative;
67 top: 2px;
68 font-size: 14px;
69 color: $grey-actor-name;
70 }
71 }
72
73 .actor-info-followers {
74 font-size: 15px;
75 padding-bottom: .5rem;
76 }
77 }
78}
79
80.actor-img-edit-container ::ng-deep .popover-avatar-info .popover-body {
81 padding: 0;
82
83 .dropdown-item {
84 padding: 6px 10px;
85 border-radius: 4px;
86
87 &:first-child {
88 @include peertube-file;
89 display: block;
90 }
91 }
92}
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'
3 3
4export abstract class Actor implements ActorServer { 4export abstract class Actor implements ActorServer {
5 id: number 5 id: number
6 url: string
7 name: string 6 name: string
7
8 host: string 8 host: string
9 url: string
10
9 followingCount: number 11 followingCount: number
10 followersCount: number 12 followersCount: number
13
11 createdAt: Date | string 14 createdAt: Date | string
12 updatedAt: Date | string 15 updatedAt: Date | string
13 avatar: ActorImage
14 16
17 avatar: ActorImage
15 avatarUrl: string 18 avatarUrl: string
16 19
17 isLocal: boolean 20 isLocal: boolean
@@ -24,6 +27,8 @@ export abstract class Actor implements ActorServer {
24 27
25 return absoluteAPIUrl + actor.avatar.path 28 return absoluteAPIUrl + actor.avatar.path
26 } 29 }
30
31 return ''
27 } 32 }
28 33
29 static CREATE_BY_STRING (accountName: string, host: string, forceHostname = false) { 34 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 @@
1export * from './account.model' 1export * from './account.model'
2export * from './account.service' 2export * from './account.service'
3export * from './actor-avatar-info.component'
4export * from './actor.model' 3export * from './actor.model'
5export * 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'
6import { FormsModule, ReactiveFormsModule } from '@angular/forms' 6import { FormsModule, ReactiveFormsModule } from '@angular/forms'
7import { RouterModule } from '@angular/router' 7import { RouterModule } from '@angular/router'
8import { 8import {
9 NgbButtonsModule,
9 NgbCollapseModule, 10 NgbCollapseModule,
10 NgbDropdownModule, 11 NgbDropdownModule,
11 NgbModalModule, 12 NgbModalModule,
12 NgbNavModule, 13 NgbNavModule,
13 NgbPopoverModule, 14 NgbPopoverModule,
14 NgbTooltipModule, 15 NgbTooltipModule
15 NgbButtonsModule
16} from '@ng-bootstrap/ng-bootstrap' 16} from '@ng-bootstrap/ng-bootstrap'
17import { LoadingBarModule } from '@ngx-loading-bar/core' 17import { LoadingBarModule } from '@ngx-loading-bar/core'
18import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' 18import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
19import { SharedGlobalIconModule } from '../shared-icons' 19import { SharedGlobalIconModule } from '../shared-icons'
20import { AccountService, ActorAvatarInfoComponent, VideoAvatarChannelComponent } from './account' 20import { AccountService } from './account'
21import { 21import {
22 AutofocusDirective, 22 AutofocusDirective,
23 BytesPipe, 23 BytesPipe,
@@ -32,7 +32,7 @@ import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditBu
32import { DateToggleComponent } from './date' 32import { DateToggleComponent } from './date'
33import { FeedComponent } from './feeds' 33import { FeedComponent } from './feeds'
34import { LoaderComponent, SmallLoaderComponent } from './loaders' 34import { LoaderComponent, SmallLoaderComponent } from './loaders'
35import { HelpComponent, ListOverflowComponent, TopMenuDropdownComponent, SimpleSearchInputComponent } from './misc' 35import { HelpComponent, ListOverflowComponent, SimpleSearchInputComponent, TopMenuDropdownComponent } from './misc'
36import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' 36import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users'
37import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' 37import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video'
38import { VideoCaptionService } from './video-caption' 38import { VideoCaptionService } from './video-caption'
@@ -65,9 +65,6 @@ import { VideoChannelService } from './video-channel'
65 ], 65 ],
66 66
67 declarations: [ 67 declarations: [
68 VideoAvatarChannelComponent,
69 ActorAvatarInfoComponent,
70
71 FromNowPipe, 68 FromNowPipe,
72 NumberFormatterPipe, 69 NumberFormatterPipe,
73 BytesPipe, 70 BytesPipe,
@@ -120,9 +117,6 @@ import { VideoChannelService } from './video-channel'
120 117
121 PrimeSharedModule, 118 PrimeSharedModule,
122 119
123 VideoAvatarChannelComponent,
124 ActorAvatarInfoComponent,
125
126 FromNowPipe, 120 FromNowPipe,
127 BytesPipe, 121 BytesPipe,
128 NumberFormatterPipe, 122 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 @@
1import { getAbsoluteAPIUrl } from '@app/helpers'
1import { Account as ServerAccount, ActorImage, VideoChannel as ServerVideoChannel, ViewsPerDate } from '@shared/models' 2import { Account as ServerAccount, ActorImage, VideoChannel as ServerVideoChannel, ViewsPerDate } from '@shared/models'
2import { Account } from '../account/account.model' 3import { Account } from '../account/account.model'
3import { Actor } from '../account/actor.model' 4import { Actor } from '../account/actor.model'
@@ -6,10 +7,15 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
6 displayName: string 7 displayName: string
7 description: string 8 description: string
8 support: string 9 support: string
10
9 isLocal: boolean 11 isLocal: boolean
12
10 nameWithHost: string 13 nameWithHost: string
11 nameWithHostForced: string 14 nameWithHostForced: string
12 15
16 banner: ActorImage
17 bannerUrl: string
18
13 ownerAccount?: ServerAccount 19 ownerAccount?: ServerAccount
14 ownerBy?: string 20 ownerBy?: string
15 ownerAvatarUrl?: string 21 ownerAvatarUrl?: string
@@ -22,6 +28,18 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
22 return Actor.GET_ACTOR_AVATAR_URL(actor) || this.GET_DEFAULT_AVATAR_URL() 28 return Actor.GET_ACTOR_AVATAR_URL(actor) || this.GET_DEFAULT_AVATAR_URL()
23 } 29 }
24 30
31 static GET_ACTOR_BANNER_URL (channel: ServerVideoChannel) {
32 if (channel?.banner?.url) return channel.banner.url
33
34 if (channel && channel.banner) {
35 const absoluteAPIUrl = getAbsoluteAPIUrl()
36
37 return absoluteAPIUrl + channel.banner.path
38 }
39
40 return ''
41 }
42
25 static GET_DEFAULT_AVATAR_URL () { 43 static GET_DEFAULT_AVATAR_URL () {
26 return `${window.location.origin}/client/assets/images/default-avatar-videochannel.png` 44 return `${window.location.origin}/client/assets/images/default-avatar-videochannel.png`
27 } 45 }
@@ -29,12 +47,14 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
29 constructor (hash: ServerVideoChannel) { 47 constructor (hash: ServerVideoChannel) {
30 super(hash) 48 super(hash)
31 49
32 this.updateComputedAttributes()
33
34 this.displayName = hash.displayName 50 this.displayName = hash.displayName
35 this.description = hash.description 51 this.description = hash.description
36 this.support = hash.support 52 this.support = hash.support
53
54 this.banner = hash.banner
55
37 this.isLocal = hash.isLocal 56 this.isLocal = hash.isLocal
57
38 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) 58 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host)
39 this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true) 59 this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true)
40 60
@@ -49,6 +69,8 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
49 this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host) 69 this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host)
50 this.ownerAvatarUrl = Account.GET_ACTOR_AVATAR_URL(this.ownerAccount) 70 this.ownerAvatarUrl = Account.GET_ACTOR_AVATAR_URL(this.ownerAccount)
51 } 71 }
72
73 this.updateComputedAttributes()
52 } 74 }
53 75
54 updateAvatar (newAvatar: ActorImage) { 76 updateAvatar (newAvatar: ActorImage) {
@@ -58,11 +80,21 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
58 } 80 }
59 81
60 resetAvatar () { 82 resetAvatar () {
61 this.avatar = null 83 this.updateAvatar(null)
62 this.avatarUrl = VideoChannel.GET_DEFAULT_AVATAR_URL() 84 }
85
86 updateBanner (newBanner: ActorImage) {
87 this.banner = newBanner
88
89 this.updateComputedAttributes()
90 }
91
92 resetBanner () {
93 this.updateBanner(null)
63 } 94 }
64 95
65 private updateComputedAttributes () { 96 private updateComputedAttributes () {
66 this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this) 97 this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this)
98 this.bannerUrl = VideoChannel.GET_ACTOR_BANNER_URL(this)
67 } 99 }
68} 100}
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 {
82 ) 82 )
83 } 83 }
84 84
85 changeVideoChannelAvatar (videoChannelName: string, avatarForm: FormData) { 85 changeVideoChannelImage (videoChannelName: string, avatarForm: FormData, type: 'avatar' | 'banner') {
86 const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar/pick' 86 const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type + '/pick'
87 87
88 return this.authHttp.post<{ avatar: ActorImage }>(url, avatarForm) 88 return this.authHttp.post<{ avatar?: ActorImage, banner?: ActorImage }>(url, avatarForm)
89 .pipe(catchError(err => this.restExtractor.handleError(err))) 89 .pipe(catchError(err => this.restExtractor.handleError(err)))
90 } 90 }
91 91
92 deleteVideoChannelAvatar (videoChannelName: string) { 92 deleteVideoChannelImage (videoChannelName: string, type: 'avatar' | 'banner') {
93 const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar' 93 const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type
94 94
95 return this.authHttp.delete(url) 95 return this.authHttp.delete(url)
96 .pipe( 96 .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 @@
32 color: pvar(--inputPlaceholderColor); 32 color: pvar(--inputPlaceholderColor);
33 } 33 }
34 34
35 @include large-screen-ratio($selector: 'div, ::ng-deep iframe') { 35 @include block-ratio($selector: 'div, ::ng-deep iframe') {
36 width: 100% !important; 36 width: 100% !important;
37 height: 100% !important; 37 height: 100% !important;
38 left: 0; 38 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 {
21} 21}
22 22
23.screenratio { 23.screenratio {
24 @include large-screen-ratio($selector: 'div, ::ng-deep iframe') { 24 @include block-ratio($selector: 'div, ::ng-deep iframe') {
25 left: 0; 25 left: 0;
26 }; 26 };
27} 27}
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;
97 width: 100%; 97 width: 100%;
98 98
99 my-video-thumbnail { 99 my-video-thumbnail {
100 @include large-screen-ratio($selector: '::ng-deep .video-thumbnail'); 100 @include block-ratio($selector: '::ng-deep .video-thumbnail');
101 } 101 }
102 102
103 .video-bottom { 103 .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 @@
886 } 886 }
887} 887}
888 888
889// applies 16:9 ratio to a child element (using $selector) only using 889// applies ratio (default to 16:9) to a child element (using $selector) only using
890// an immediate's parent size. This allows 16:9 ratio without explicit 890// an immediate's parent size. This allows to set a ratio without explicit
891// dimensions, as width/height cannot be computed from each other. 891// dimensions, as width/height cannot be computed from each other.
892@mixin large-screen-ratio ($selector: 'div') { 892@mixin block-ratio ($selector: 'div', $inverted-ratio: 9/16) {
893 $padding-percent: percentage($inverted-ratio);
894
893 position: relative; 895 position: relative;
894 height: 0; 896 height: 0;
895 width: 100%; 897 width: 100%;
896 padding-top: 56%; 898 padding-top: $padding-percent;
897 899
898 #{$selector} { 900 #{$selector} {
899 position: absolute; 901 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;
52$sub-menu-height: 81px; 52$sub-menu-height: 81px;
53 53
54$channel-background-color: #f6ede8; 54$channel-background-color: #f6ede8;
55
56$banner-inverted-ratio: 1/5;
57
55$max-channels-width: 1200px; 58$max-channels-width: 1200px;
56 59
57$footer-height: 30px; 60$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) {
163 extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME 163 extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
164 } 164 }
165 }, 165 },
166 banner: {
167 file: {
168 size: {
169 max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
170 },
171 extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
172 }
173 },
166 video: { 174 video: {
167 image: { 175 image: {
168 extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME, 176 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 {
72 } 72 }
73 73
74 getStaticPath () { 74 getStaticPath () {
75 return join(LAZY_STATIC_PATHS.AVATARS, this.filename) 75 if (this.type === ActorImageType.AVATAR) {
76 return join(LAZY_STATIC_PATHS.AVATARS, this.filename)
77 }
78
79 return join(LAZY_STATIC_PATHS.BANNERS, this.filename)
76 } 80 }
77 81
78 getPath () { 82 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'
71import { VideoPlaylistModel } from '../video/video-playlist' 71import { VideoPlaylistModel } from '../video/video-playlist'
72import { AccountModel } from './account' 72import { AccountModel } from './account'
73import { UserNotificationSettingModel } from './user-notification-setting' 73import { UserNotificationSettingModel } from './user-notification-setting'
74import { ActorImageModel } from './actor-image'
74 75
75enum ScopeNames { 76enum ScopeNames {
76 FOR_ME_API = 'FOR_ME_API', 77 FOR_ME_API = 'FOR_ME_API',
@@ -97,7 +98,20 @@ enum ScopeNames {
97 model: AccountModel, 98 model: AccountModel,
98 include: [ 99 include: [
99 { 100 {
100 model: VideoChannelModel 101 model: VideoChannelModel.unscoped(),
102 include: [
103 {
104 model: ActorModel,
105 required: true,
106 include: [
107 {
108 model: ActorImageModel,
109 as: 'Banner',
110 required: false
111 }
112 ]
113 }
114 ]
101 }, 115 },
102 { 116 {
103 attributes: [ 'id', 'name', 'type' ], 117 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 {
9 doubleFollow, 9 doubleFollow,
10 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
11 getVideo, 11 getVideo,
12 getVideoChannel,
12 getVideoChannelVideos, 13 getVideoChannelVideos,
13 testImage, 14 testImage,
14 updateVideo, 15 updateVideo,
@@ -306,7 +307,8 @@ describe('Test video channels', function () {
306 await waitJobs(servers) 307 await waitJobs(servers)
307 308
308 for (const server of servers) { 309 for (const server of servers) {
309 const videoChannel = await findChannel(server, secondVideoChannelId) 310 const res = await getVideoChannel(server.url, 'second_video_channel@' + servers[0].host)
311 const videoChannel = res.body
310 312
311 await testImage(server.url, 'banner-resized', videoChannel.banner.path) 313 await testImage(server.url, 'banner-resized', videoChannel.banner.path)
312 } 314 }
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 {
151 } 151 }
152 } 152 }
153 153
154 banner: {
155 file: {
156 size: {
157 max: number
158 }
159 extensions: string[]
160 }
161 }
162
154 video: { 163 video: {
155 image: { 164 image: {
156 size: { 165 size: {