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 @@
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss
index 8f8af655c..22de103d1 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss
@@ -10,11 +10,16 @@ label {
@include settings-big-title;
}
-my-actor-avatar-info {
+my-actor-avatar-edit,
+my-actor-banner-edit {
display: block;
margin-bottom: 20px;
}
+my-actor-banner-edit {
+ max-width: 500px;
+}
+
.input-group {
@include peertube-input-group(fit-content);
}
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts
index 3e20a27ee..0cdf2fe34 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts
@@ -15,6 +15,8 @@ export abstract class MyVideoChannelEdit extends FormReactive {
// We need this method so angular does not complain in child template that doesn't need this
onAvatarChange (formData: FormData) { /* empty */ }
onAvatarDelete () { /* empty */ }
+ onBannerChange (formData: FormData) { /* empty */ }
+ onBannerDelete () { /* empty */ }
// Should be implemented by the child
isBulkUpdateVideosDisplayed () {
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts
index 6cd1ff503..22935a87a 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts
@@ -1,7 +1,9 @@
import { Subscription } from 'rxjs'
+import { HttpErrorResponse } from '@angular/common/http'
import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, Notifier, ServerService } from '@app/core'
+import { uploadErrorHandler } from '@app/helpers'
import {
VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
@@ -11,8 +13,6 @@ import { FormValidatorService } from '@app/shared/shared-forms'
import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
import { ServerConfig, VideoChannelUpdate } from '@shared/models'
import { MyVideoChannelEdit } from './my-video-channel-edit'
-import { HttpErrorResponse } from '@angular/common/http'
-import { uploadErrorHandler } from '@app/helpers'
@Component({
selector: 'my-video-channel-update',
@@ -101,7 +101,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
}
onAvatarChange (formData: FormData) {
- this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData)
+ this.videoChannelService.changeVideoChannelImage(this.videoChannelToUpdate.name, formData, 'avatar')
.subscribe(
data => {
this.notifier.success($localize`Avatar changed.`)
@@ -118,7 +118,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
}
onAvatarDelete () {
- this.videoChannelService.deleteVideoChannelAvatar(this.videoChannelToUpdate.name)
+ this.videoChannelService.deleteVideoChannelImage(this.videoChannelToUpdate.name, 'avatar')
.subscribe(
data => {
this.notifier.success($localize`Avatar deleted.`)
@@ -130,6 +130,36 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
)
}
+ onBannerChange (formData: FormData) {
+ this.videoChannelService.changeVideoChannelImage(this.videoChannelToUpdate.name, formData, 'banner')
+ .subscribe(
+ data => {
+ this.notifier.success($localize`Banner changed.`)
+
+ this.videoChannelToUpdate.updateBanner(data.banner)
+ },
+
+ (err: HttpErrorResponse) => uploadErrorHandler({
+ err,
+ name: $localize`banner`,
+ notifier: this.notifier
+ })
+ )
+ }
+
+ onBannerDelete () {
+ this.videoChannelService.deleteVideoChannelImage(this.videoChannelToUpdate.name, 'banner')
+ .subscribe(
+ data => {
+ this.notifier.success($localize`Banner deleted.`)
+
+ this.videoChannelToUpdate.resetBanner()
+ },
+
+ err => this.notifier.error(err.message)
+ )
+ }
+
get maxAvatarSize () {
return this.serverConfig.avatar.file.size.max
}
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts
index 92b56db49..53557ca02 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts
@@ -1,5 +1,6 @@
import { ChartModule } from 'primeng/chart'
import { NgModule } from '@angular/core'
+import { SharedActorImageModule } from '@app/shared/shared-actor-image'
import { SharedFormModule } from '@app/shared/shared-forms'
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
import { SharedMainModule } from '@app/shared/shared-main'
@@ -16,7 +17,8 @@ import { MyVideoChannelsComponent } from './my-video-channels.component'
SharedMainModule,
SharedFormModule,
- SharedGlobalIconModule
+ SharedGlobalIconModule,
+ SharedActorImageModule
],
declarations: [
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.html b/client/src/app/+videos/+video-watch/video-avatar-channel.component.html
similarity index 100%
rename from client/src/app/shared/shared-main/account/video-avatar-channel.component.html
rename to client/src/app/+videos/+video-watch/video-avatar-channel.component.html
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.scss b/client/src/app/+videos/+video-watch/video-avatar-channel.component.scss
similarity index 100%
rename from client/src/app/shared/shared-main/account/video-avatar-channel.component.scss
rename to client/src/app/+videos/+video-watch/video-avatar-channel.component.scss
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts b/client/src/app/+videos/+video-watch/video-avatar-channel.component.ts
similarity index 93%
rename from client/src/app/shared/shared-main/account/video-avatar-channel.component.ts
rename to client/src/app/+videos/+video-watch/video-avatar-channel.component.ts
index 440e2b522..0b6e796df 100644
--- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts
+++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.ts
@@ -1,5 +1,5 @@
import { Component, Input, OnInit } from '@angular/core'
-import { Video } from '../video/video.model'
+import { Video } from '@app/shared/shared-main/video'
@Component({
selector: 'my-video-avatar-channel',
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts
index 9656f08e9..7f3ceeebc 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -29,7 +29,12 @@ import { MetaService } from '@ngx-meta/core'
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models'
-import { cleanupVideoWatch, getStoredP2PEnabled, getStoredTheater, getStoredVideoWatchHistory } from '../../../assets/player/peertube-player-local-storage'
+import {
+ cleanupVideoWatch,
+ getStoredP2PEnabled,
+ getStoredTheater,
+ getStoredVideoWatchHistory
+} from '../../../assets/player/peertube-player-local-storage'
import {
CustomizationOptions,
P2PMediaLoaderOptions,
diff --git a/client/src/app/+videos/+video-watch/video-watch.module.ts b/client/src/app/+videos/+video-watch/video-watch.module.ts
index d65cf8d68..3e9f3822e 100644
--- a/client/src/app/+videos/+video-watch/video-watch.module.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.module.ts
@@ -16,6 +16,7 @@ import { VideoCommentComponent } from './comment/video-comment.component'
import { VideoCommentsComponent } from './comment/video-comments.component'
import { RecommendationsModule } from './recommendations/recommendations.module'
import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive'
+import { VideoAvatarChannelComponent } from './video-avatar-channel.component'
import { VideoWatchPlaylistComponent } from './video-watch-playlist.component'
import { VideoWatchRoutingModule } from './video-watch-routing.module'
import { VideoWatchComponent } from './video-watch.component'
@@ -46,6 +47,8 @@ import { VideoWatchComponent } from './video-watch.component'
VideoCommentAddComponent,
VideoCommentComponent,
+ VideoAvatarChannelComponent,
+
TimestampRouteTransformerDirective,
TimestampRouteTransformerDirective
],
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 11288fc54..906191ae1 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -98,6 +98,12 @@ export class ServerService {
extensions: []
}
},
+ banner: {
+ file: {
+ size: { max: 0 },
+ extensions: []
+ }
+ },
video: {
image: {
size: { max: 0 },
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html
new file mode 100644
index 000000000..10f2ef262
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html
@@ -0,0 +1,41 @@
+
+
+
![Avatar]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ actor.displayName }}
+
{{ actor.name }}
+
{{ actor.followersCount }} subscribers
+
+
+
+
+
+
+ Upload a new avatar
+
+
+
+
+ Remove avatar
+
+
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss
new file mode 100644
index 000000000..8b0172315
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss
@@ -0,0 +1,54 @@
+@import '_variables';
+@import '_mixins';
+
+.actor {
+ display: flex;
+
+ img {
+ margin-right: 15px;
+
+ &:not(.channel) {
+ @include avatar(100px);
+ }
+
+ &.channel {
+ @include channel-avatar(100px);
+ }
+ }
+
+ .actor-info {
+ display: inline-flex;
+ flex-direction: column;
+
+ .actor-info-display-name {
+ font-size: 20px;
+ font-weight: $font-bold;
+
+ @media screen and (max-width: $small-view) {
+ font-size: 16px;
+ }
+ }
+
+ .actor-info-username {
+ position: relative;
+ font-size: 14px;
+ color: pvar(--greyForegroundColor);
+ }
+
+ .actor-info-followers {
+ font-size: 15px;
+ padding-bottom: .5rem;
+ }
+ }
+}
+
+.actor-img-edit-container {
+ position: relative;
+ width: 0;
+}
+
+.actor-img-edit-button {
+ top: 55px;
+ right: 45px;
+ border-radius: 50%;
+}
diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts
similarity index 70%
rename from client/src/app/shared/shared-main/account/actor-avatar-info.component.ts
rename to client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts
index 87e9e917c..6f76172e9 100644
--- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts
+++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts
@@ -1,21 +1,25 @@
-import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'
+import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
import { Notifier, ServerService } from '@app/core'
+import { Account, VideoChannel } from '@app/shared/shared-main'
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
import { getBytes } from '@root-helpers/bytes'
-import { Account } from '../account/account.model'
-import { VideoChannel } from '../video-channel/video-channel.model'
-import { Actor } from './actor.model'
@Component({
- selector: 'my-actor-avatar-info',
- templateUrl: './actor-avatar-info.component.html',
- styleUrls: [ './actor-avatar-info.component.scss' ]
+ selector: 'my-actor-avatar-edit',
+ templateUrl: './actor-avatar-edit.component.html',
+ styleUrls: [
+ './actor-image-edit.scss',
+ './actor-avatar-edit.component.scss'
+ ]
})
-export class ActorAvatarInfoComponent implements OnInit, OnChanges {
+export class ActorAvatarEditComponent implements OnInit {
@ViewChild('avatarfileInput') avatarfileInput: ElementRef
@ViewChild('avatarPopover') avatarPopover: NgbPopover
@Input() actor: VideoChannel | Account
+ @Input() editable = true
+ @Input() displaySubscribers = true
+ @Input() displayUsername = true
@Output() avatarChange = new EventEmitter()
@Output() avatarDelete = new EventEmitter()
@@ -24,8 +28,6 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
maxAvatarSize = 0
avatarExtensions = ''
- private avatarUrl: string
-
constructor (
private serverService: ServerService,
private notifier: Notifier
@@ -42,12 +44,6 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
})
}
- ngOnChanges (changes: SimpleChanges) {
- if (changes['actor']) {
- this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.actor)
- }
- }
-
onAvatarChange (input: HTMLInputElement) {
this.avatarfileInput = new ElementRef(input)
@@ -68,7 +64,7 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
}
hasAvatar () {
- return !!this.avatarUrl
+ return !!this.actor.avatar
}
isChannel () {
diff --git a/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html
new file mode 100644
index 000000000..eb1b66422
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html
@@ -0,0 +1,34 @@
+
+
+
+
![Banner]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Upload a new banner
+
+
+
+
+
+ Remove banner
+
+
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
-
-
-
-
-
-
-
- Upload a new avatar
-
-
-
-
- Remove avatar
-
-
diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss b/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss
deleted file mode 100644
index 40ba4269b..000000000
--- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss
+++ /dev/null
@@ -1,92 +0,0 @@
-@import '_variables';
-@import '_mixins';
-
-.actor {
- display: flex;
-
- img {
- margin-right: 15px;
-
- &:not(.channel) {
- @include avatar(100px);
- }
-
- &.channel {
- @include channel-avatar(100px);
- }
- }
-
- .actor-img-edit-container {
- position: relative;
- width: 0;
-
- .actor-img-edit-button {
- @include peertube-button-file(21px);
- @include button-with-icon(19px);
- @include orange-button;
-
- margin-top: 10px;
- margin-bottom: 5px;
- border-radius: 50%;
- top: 55px;
- right: 45px;
- cursor: pointer;
-
- input {
- width: 30px;
- height: 30px;
- }
-
- my-global-icon {
- right: 7px;
- }
- }
- }
-
- .actor-info {
- justify-content: center;
- display: inline-flex;
- flex-direction: column;
-
- .actor-info-names {
- display: flex;
- align-items: center;
-
- .actor-info-display-name {
- font-size: 20px;
- font-weight: $font-bold;
-
- @media screen and (max-width: $small-view) {
- font-size: 16px;
- }
- }
-
- .actor-info-username {
- margin-left: 7px;
- position: relative;
- top: 2px;
- font-size: 14px;
- color: $grey-actor-name;
- }
- }
-
- .actor-info-followers {
- font-size: 15px;
- padding-bottom: .5rem;
- }
- }
-}
-
-.actor-img-edit-container ::ng-deep .popover-avatar-info .popover-body {
- padding: 0;
-
- .dropdown-item {
- padding: 6px 10px;
- border-radius: 4px;
-
- &:first-child {
- @include peertube-file;
- display: block;
- }
- }
-}
diff --git a/client/src/app/shared/shared-main/account/actor.model.ts b/client/src/app/shared/shared-main/account/actor.model.ts
index c105a88ac..670823060 100644
--- a/client/src/app/shared/shared-main/account/actor.model.ts
+++ b/client/src/app/shared/shared-main/account/actor.model.ts
@@ -3,15 +3,18 @@ import { getAbsoluteAPIUrl } from '@app/helpers'
export abstract class Actor implements ActorServer {
id: number
- url: string
name: string
+
host: string
+ url: string
+
followingCount: number
followersCount: number
+
createdAt: Date | string
updatedAt: Date | string
- avatar: ActorImage
+ avatar: ActorImage
avatarUrl: string
isLocal: boolean
@@ -24,6 +27,8 @@ export abstract class Actor implements ActorServer {
return absoluteAPIUrl + actor.avatar.path
}
+
+ return ''
}
static CREATE_BY_STRING (accountName: string, host: string, forceHostname = false) {
diff --git a/client/src/app/shared/shared-main/account/index.ts b/client/src/app/shared/shared-main/account/index.ts
index 61c800e56..b80ddb9f5 100644
--- a/client/src/app/shared/shared-main/account/index.ts
+++ b/client/src/app/shared/shared-main/account/index.ts
@@ -1,5 +1,3 @@
export * from './account.model'
export * from './account.service'
-export * from './actor-avatar-info.component'
export * from './actor.model'
-export * from './video-avatar-channel.component'
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts
index 3e21d491a..16d230f46 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -6,18 +6,18 @@ import { NgModule } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterModule } from '@angular/router'
import {
+ NgbButtonsModule,
NgbCollapseModule,
NgbDropdownModule,
NgbModalModule,
NgbNavModule,
NgbPopoverModule,
- NgbTooltipModule,
- NgbButtonsModule
+ NgbTooltipModule
} from '@ng-bootstrap/ng-bootstrap'
import { LoadingBarModule } from '@ngx-loading-bar/core'
import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
import { SharedGlobalIconModule } from '../shared-icons'
-import { AccountService, ActorAvatarInfoComponent, VideoAvatarChannelComponent } from './account'
+import { AccountService } from './account'
import {
AutofocusDirective,
BytesPipe,
@@ -32,7 +32,7 @@ import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditBu
import { DateToggleComponent } from './date'
import { FeedComponent } from './feeds'
import { LoaderComponent, SmallLoaderComponent } from './loaders'
-import { HelpComponent, ListOverflowComponent, TopMenuDropdownComponent, SimpleSearchInputComponent } from './misc'
+import { HelpComponent, ListOverflowComponent, SimpleSearchInputComponent, TopMenuDropdownComponent } from './misc'
import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users'
import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video'
import { VideoCaptionService } from './video-caption'
@@ -65,9 +65,6 @@ import { VideoChannelService } from './video-channel'
],
declarations: [
- VideoAvatarChannelComponent,
- ActorAvatarInfoComponent,
-
FromNowPipe,
NumberFormatterPipe,
BytesPipe,
@@ -120,9 +117,6 @@ import { VideoChannelService } from './video-channel'
PrimeSharedModule,
- VideoAvatarChannelComponent,
- ActorAvatarInfoComponent,
-
FromNowPipe,
BytesPipe,
NumberFormatterPipe,
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
index b4c3365a9..d8be42eef 100644
--- a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
+++ b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
@@ -1,3 +1,4 @@
+import { getAbsoluteAPIUrl } from '@app/helpers'
import { Account as ServerAccount, ActorImage, VideoChannel as ServerVideoChannel, ViewsPerDate } from '@shared/models'
import { Account } from '../account/account.model'
import { Actor } from '../account/actor.model'
@@ -6,10 +7,15 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
displayName: string
description: string
support: string
+
isLocal: boolean
+
nameWithHost: string
nameWithHostForced: string
+ banner: ActorImage
+ bannerUrl: string
+
ownerAccount?: ServerAccount
ownerBy?: string
ownerAvatarUrl?: string
@@ -22,6 +28,18 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
return Actor.GET_ACTOR_AVATAR_URL(actor) || this.GET_DEFAULT_AVATAR_URL()
}
+ static GET_ACTOR_BANNER_URL (channel: ServerVideoChannel) {
+ if (channel?.banner?.url) return channel.banner.url
+
+ if (channel && channel.banner) {
+ const absoluteAPIUrl = getAbsoluteAPIUrl()
+
+ return absoluteAPIUrl + channel.banner.path
+ }
+
+ return ''
+ }
+
static GET_DEFAULT_AVATAR_URL () {
return `${window.location.origin}/client/assets/images/default-avatar-videochannel.png`
}
@@ -29,12 +47,14 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
constructor (hash: ServerVideoChannel) {
super(hash)
- this.updateComputedAttributes()
-
this.displayName = hash.displayName
this.description = hash.description
this.support = hash.support
+
+ this.banner = hash.banner
+
this.isLocal = hash.isLocal
+
this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host)
this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true)
@@ -49,6 +69,8 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host)
this.ownerAvatarUrl = Account.GET_ACTOR_AVATAR_URL(this.ownerAccount)
}
+
+ this.updateComputedAttributes()
}
updateAvatar (newAvatar: ActorImage) {
@@ -58,11 +80,21 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
}
resetAvatar () {
- this.avatar = null
- this.avatarUrl = VideoChannel.GET_DEFAULT_AVATAR_URL()
+ this.updateAvatar(null)
+ }
+
+ updateBanner (newBanner: ActorImage) {
+ this.banner = newBanner
+
+ this.updateComputedAttributes()
+ }
+
+ resetBanner () {
+ this.updateBanner(null)
}
private updateComputedAttributes () {
this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this)
+ this.bannerUrl = VideoChannel.GET_ACTOR_BANNER_URL(this)
}
}
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
index 3f9ef74fa..e65261763 100644
--- a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
+++ b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
@@ -82,15 +82,15 @@ export class VideoChannelService {
)
}
- changeVideoChannelAvatar (videoChannelName: string, avatarForm: FormData) {
- const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar/pick'
+ changeVideoChannelImage (videoChannelName: string, avatarForm: FormData, type: 'avatar' | 'banner') {
+ const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type + '/pick'
- return this.authHttp.post<{ avatar: ActorImage }>(url, avatarForm)
+ return this.authHttp.post<{ avatar?: ActorImage, banner?: ActorImage }>(url, avatarForm)
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
- deleteVideoChannelAvatar (videoChannelName: string) {
- const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar'
+ deleteVideoChannelImage (videoChannelName: string, type: 'avatar' | 'banner') {
+ const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type
return this.authHttp.delete(url)
.pipe(
diff --git a/client/src/app/shared/shared-moderation/moderation.scss b/client/src/app/shared/shared-moderation/moderation.scss
index 4a4e05535..cdcc12fe0 100644
--- a/client/src/app/shared/shared-moderation/moderation.scss
+++ b/client/src/app/shared/shared-moderation/moderation.scss
@@ -32,7 +32,7 @@
color: pvar(--inputPlaceholderColor);
}
- @include large-screen-ratio($selector: 'div, ::ng-deep iframe') {
+ @include block-ratio($selector: 'div, ::ng-deep iframe') {
width: 100% !important;
height: 100% !important;
left: 0;
diff --git a/client/src/app/shared/shared-moderation/report-modals/report.component.scss b/client/src/app/shared/shared-moderation/report-modals/report.component.scss
index b2606cbd8..0567330f5 100644
--- a/client/src/app/shared/shared-moderation/report-modals/report.component.scss
+++ b/client/src/app/shared/shared-moderation/report-modals/report.component.scss
@@ -21,7 +21,7 @@ textarea {
}
.screenratio {
- @include large-screen-ratio($selector: 'div, ::ng-deep iframe') {
+ @include block-ratio($selector: 'div, ::ng-deep iframe') {
left: 0;
};
}
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
index 1b50f3290..621951919 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
@@ -97,7 +97,7 @@ $more-button-width: 40px;
width: 100%;
my-video-thumbnail {
- @include large-screen-ratio($selector: '::ng-deep .video-thumbnail');
+ @include block-ratio($selector: '::ng-deep .video-thumbnail');
}
.video-bottom {
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index e37b89c62..bf844ac5d 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -886,14 +886,16 @@
}
}
-// applies 16:9 ratio to a child element (using $selector) only using
-// an immediate's parent size. This allows 16:9 ratio without explicit
+// applies ratio (default to 16:9) to a child element (using $selector) only using
+// an immediate's parent size. This allows to set a ratio without explicit
// dimensions, as width/height cannot be computed from each other.
-@mixin large-screen-ratio ($selector: 'div') {
+@mixin block-ratio ($selector: 'div', $inverted-ratio: 9/16) {
+ $padding-percent: percentage($inverted-ratio);
+
position: relative;
height: 0;
width: 100%;
- padding-top: 56%;
+ padding-top: $padding-percent;
#{$selector} {
position: absolute;
diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss
index c451febdc..3501b305f 100644
--- a/client/src/sass/include/_variables.scss
+++ b/client/src/sass/include/_variables.scss
@@ -52,6 +52,9 @@ $sub-menu-background-color: #F7F7F7;
$sub-menu-height: 81px;
$channel-background-color: #f6ede8;
+
+$banner-inverted-ratio: 1/5;
+
$max-channels-width: 1200px;
$footer-height: 30px;
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 313513cea..e28f7502d 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -163,6 +163,14 @@ async function getConfig (req: express.Request, res: express.Response) {
extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
}
},
+ banner: {
+ file: {
+ size: {
+ max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
+ },
+ extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
+ }
+ },
video: {
image: {
extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME,
diff --git a/server/models/account/actor-image.ts b/server/models/account/actor-image.ts
index c532bd08d..b779e3cf6 100644
--- a/server/models/account/actor-image.ts
+++ b/server/models/account/actor-image.ts
@@ -72,7 +72,11 @@ export class ActorImageModel extends Model {
}
getStaticPath () {
- return join(LAZY_STATIC_PATHS.AVATARS, this.filename)
+ if (this.type === ActorImageType.AVATAR) {
+ return join(LAZY_STATIC_PATHS.AVATARS, this.filename)
+ }
+
+ return join(LAZY_STATIC_PATHS.BANNERS, this.filename)
}
getPath () {
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index a7a65c489..00c6d73aa 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -71,6 +71,7 @@ import { VideoLiveModel } from '../video/video-live'
import { VideoPlaylistModel } from '../video/video-playlist'
import { AccountModel } from './account'
import { UserNotificationSettingModel } from './user-notification-setting'
+import { ActorImageModel } from './actor-image'
enum ScopeNames {
FOR_ME_API = 'FOR_ME_API',
@@ -97,7 +98,20 @@ enum ScopeNames {
model: AccountModel,
include: [
{
- model: VideoChannelModel
+ model: VideoChannelModel.unscoped(),
+ include: [
+ {
+ model: ActorModel,
+ required: true,
+ include: [
+ {
+ model: ActorImageModel,
+ as: 'Banner',
+ required: false
+ }
+ ]
+ }
+ ]
},
{
attributes: [ 'id', 'name', 'type' ],
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts
index 8033b9ba5..e50582218 100644
--- a/server/tests/api/videos/video-channels.ts
+++ b/server/tests/api/videos/video-channels.ts
@@ -9,6 +9,7 @@ import {
doubleFollow,
flushAndRunMultipleServers,
getVideo,
+ getVideoChannel,
getVideoChannelVideos,
testImage,
updateVideo,
@@ -306,7 +307,8 @@ describe('Test video channels', function () {
await waitJobs(servers)
for (const server of servers) {
- const videoChannel = await findChannel(server, secondVideoChannelId)
+ const res = await getVideoChannel(server.url, 'second_video_channel@' + servers[0].host)
+ const videoChannel = res.body
await testImage(server.url, 'banner-resized', videoChannel.banner.path)
}
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts
index efde4ad9d..85d84af44 100644
--- a/shared/models/server/server-config.model.ts
+++ b/shared/models/server/server-config.model.ts
@@ -151,6 +151,15 @@ export interface ServerConfig {
}
}
+ banner: {
+ file: {
+ size: {
+ max: number
+ }
+ extensions: string[]
+ }
+ }
+
video: {
image: {
size: {