aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/shared')
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts3
-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)42
-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.ts76
-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-forms/input-toggle-hidden.component.html5
-rw-r--r--client/src/app/shared/shared-forms/markdown-textarea.component.scss2
-rw-r--r--client/src/app/shared/shared-forms/select/select-options.component.ts9
-rw-r--r--client/src/app/shared/shared-instance/instance-about-accordion.component.scss2
-rw-r--r--client/src/app/shared/shared-instance/instance-features-table.component.html2
-rw-r--r--client/src/app/shared/shared-instance/instance-features-table.component.ts10
-rw-r--r--client/src/app/shared/shared-main/account/account.model.ts4
-rw-r--r--client/src/app/shared/shared-main/account/actor-avatar-info.component.html43
-rw-r--r--client/src/app/shared/shared-main/account/actor-avatar-info.component.scss86
-rw-r--r--client/src/app/shared/shared-main/account/actor.model.ts19
-rw-r--r--client/src/app/shared/shared-main/account/index.ts2
-rw-r--r--client/src/app/shared/shared-main/account/video-avatar-channel.component.html26
-rw-r--r--client/src/app/shared/shared-main/account/video-avatar-channel.component.scss40
-rw-r--r--client/src/app/shared/shared-main/account/video-avatar-channel.component.ts27
-rw-r--r--client/src/app/shared/shared-main/angular/autofocus.directive.ts12
-rw-r--r--client/src/app/shared/shared-main/angular/index.ts1
-rw-r--r--client/src/app/shared/shared-main/auth/auth-interceptor.service.ts4
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.html13
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.scss36
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.ts52
-rw-r--r--client/src/app/shared/shared-main/peertube-modal/index.ts1
-rw-r--r--client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts7
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts17
-rw-r--r--client/src/app/shared/shared-main/users/user-notification.model.ts33
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.html48
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.ts9
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.model.ts53
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.service.ts12
-rw-r--r--client/src/app/shared/shared-main/video/video.model.ts6
-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-moderation/report-modals/video-report.component.ts3
-rw-r--r--client/src/app/shared/shared-share-modal/video-share.component.ts4
-rw-r--r--client/src/app/shared/shared-support-modal/index.ts3
-rw-r--r--client/src/app/shared/shared-support-modal/shared-support-modal.module.ts24
-rw-r--r--client/src/app/shared/shared-support-modal/support-modal.component.html15
-rw-r--r--client/src/app/shared/shared-support-modal/support-modal.component.scss3
-rw-r--r--client/src/app/shared/shared-support-modal/support-modal.component.ts40
-rw-r--r--client/src/app/shared/shared-video-miniature/abstract-video-list.html5
-rw-r--r--client/src/app/shared/shared-video-miniature/abstract-video-list.scss11
-rw-r--r--client/src/app/shared/shared-video-miniature/abstract-video-list.ts8
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.html149
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.scss37
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.ts38
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.html8
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.scss305
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.ts35
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-selection.component.html3
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-selection.component.scss42
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-selection.component.ts3
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss119
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts1
63 files changed, 1119 insertions, 666 deletions
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
index e34836a18..eeb9f128b 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
@@ -117,7 +117,8 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV
117 warningTitle: false, 117 warningTitle: false,
118 startTime: abuse.video.startAt, 118 startTime: abuse.video.startAt,
119 stopTime: abuse.video.endAt 119 stopTime: abuse.video.endAt
120 }) 120 }),
121 abuse.video.name
121 ) 122 )
122 } 123 }
123 124
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..0829263f4
--- /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]="preview || 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 b459c591f..d0d269489 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,27 @@
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 { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
2import { Notifier, ServerService } from '@app/core' 3import { Notifier, ServerService } from '@app/core'
4import { Account, VideoChannel } from '@app/shared/shared-main'
3import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' 5import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
4import { getBytes } from '@root-helpers/bytes' 6import { 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 7
9@Component({ 8@Component({
10 selector: 'my-actor-avatar-info', 9 selector: 'my-actor-avatar-edit',
11 templateUrl: './actor-avatar-info.component.html', 10 templateUrl: './actor-avatar-edit.component.html',
12 styleUrls: [ './actor-avatar-info.component.scss' ] 11 styleUrls: [
12 './actor-image-edit.scss',
13 './actor-avatar-edit.component.scss'
14 ]
13}) 15})
14export class ActorAvatarInfoComponent implements OnInit, OnChanges { 16export class ActorAvatarEditComponent implements OnInit {
15 @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement> 17 @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement>
16 @ViewChild('avatarPopover') avatarPopover: NgbPopover 18 @ViewChild('avatarPopover') avatarPopover: NgbPopover
17 19
18 @Input() actor: VideoChannel | Account 20 @Input() actor: VideoChannel | Account
21 @Input() editable = true
22 @Input() displaySubscribers = true
23 @Input() displayUsername = true
24 @Input() previewImage = false
19 25
20 @Output() avatarChange = new EventEmitter<FormData>() 26 @Output() avatarChange = new EventEmitter<FormData>()
21 @Output() avatarDelete = new EventEmitter<void>() 27 @Output() avatarDelete = new EventEmitter<void>()
@@ -24,9 +30,10 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
24 maxAvatarSize = 0 30 maxAvatarSize = 0
25 avatarExtensions = '' 31 avatarExtensions = ''
26 32
27 private avatarUrl: string 33 preview: SafeResourceUrl
28 34
29 constructor ( 35 constructor (
36 private sanitizer: DomSanitizer,
30 private serverService: ServerService, 37 private serverService: ServerService,
31 private notifier: Notifier 38 private notifier: Notifier
32 ) { } 39 ) { }
@@ -42,12 +49,6 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
42 }) 49 })
43 } 50 }
44 51
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) { 52 onAvatarChange (input: HTMLInputElement) {
52 this.avatarfileInput = new ElementRef(input) 53 this.avatarfileInput = new ElementRef(input)
53 54
@@ -61,13 +62,22 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
61 formData.append('avatarfile', avatarfile) 62 formData.append('avatarfile', avatarfile)
62 this.avatarPopover?.close() 63 this.avatarPopover?.close()
63 this.avatarChange.emit(formData) 64 this.avatarChange.emit(formData)
65
66 if (this.previewImage) {
67 this.preview = this.sanitizer.bypassSecurityTrustResourceUrl(URL.createObjectURL(avatarfile))
68 }
64 } 69 }
65 70
66 deleteAvatar () { 71 deleteAvatar () {
72 this.preview = undefined
67 this.avatarDelete.emit() 73 this.avatarDelete.emit()
68 } 74 }
69 75
70 hasAvatar () { 76 hasAvatar () {
71 return !!this.avatarUrl 77 return !!this.preview || !!this.actor.avatar
78 }
79
80 isChannel () {
81 return !!(this.actor as VideoChannel).ownerAccount
72 } 82 }
73} 83}
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..266fc26c5
--- /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]="preview || 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..8c12d3c4c
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts
@@ -0,0 +1,76 @@
1import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'
2import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
3import { Notifier, ServerService } from '@app/core'
4import { VideoChannel } from '@app/shared/shared-main'
5import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
6import { getBytes } from '@root-helpers/bytes'
7
8@Component({
9 selector: 'my-actor-banner-edit',
10 templateUrl: './actor-banner-edit.component.html',
11 styleUrls: [
12 './actor-image-edit.scss',
13 './actor-banner-edit.component.scss'
14 ]
15})
16export class ActorBannerEditComponent implements OnInit {
17 @ViewChild('bannerfileInput') bannerfileInput: ElementRef<HTMLInputElement>
18 @ViewChild('bannerPopover') bannerPopover: NgbPopover
19
20 @Input() actor: VideoChannel
21 @Input() previewImage = false
22
23 @Output() bannerChange = new EventEmitter<FormData>()
24 @Output() bannerDelete = new EventEmitter<void>()
25
26 bannerFormat = ''
27 maxBannerSize = 0
28 bannerExtensions = ''
29
30 preview: SafeResourceUrl
31
32 constructor (
33 private sanitizer: DomSanitizer,
34 private serverService: ServerService,
35 private notifier: Notifier
36 ) { }
37
38 ngOnInit (): void {
39 this.serverService.getConfig()
40 .subscribe(config => {
41 this.maxBannerSize = config.banner.file.size.max
42 this.bannerExtensions = config.banner.file.extensions.join(', ')
43
44 // tslint:disable:max-line-length
45 this.bannerFormat = $localize`ratio 6/1, recommended size: 1600x266, max size: ${getBytes(this.maxBannerSize)}, extensions: ${this.bannerExtensions}`
46 })
47 }
48
49 onBannerChange (input: HTMLInputElement) {
50 this.bannerfileInput = new ElementRef(input)
51
52 const bannerfile = this.bannerfileInput.nativeElement.files[ 0 ]
53 if (bannerfile.size > this.maxBannerSize) {
54 this.notifier.error('Error', $localize`This image is too large.`)
55 return
56 }
57
58 const formData = new FormData()
59 formData.append('bannerfile', bannerfile)
60 this.bannerPopover?.close()
61 this.bannerChange.emit(formData)
62
63 if (this.previewImage) {
64 this.preview = this.sanitizer.bypassSecurityTrustResourceUrl(URL.createObjectURL(bannerfile))
65 }
66 }
67
68 deleteBanner () {
69 this.preview = undefined
70 this.bannerDelete.emit()
71 }
72
73 hasBanner () {
74 return !!this.preview || !!this.actor.bannerUrl
75 }
76}
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-forms/input-toggle-hidden.component.html b/client/src/app/shared/shared-forms/input-toggle-hidden.component.html
index e7441e4c1..9f252f299 100644
--- a/client/src/app/shared/shared-forms/input-toggle-hidden.component.html
+++ b/client/src/app/shared/shared-forms/input-toggle-hidden.component.html
@@ -12,9 +12,10 @@
12 12
13 <button 13 <button
14 *ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button" 14 *ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button"
15 class="btn btn-outline-secondary" i18n-title title="Copy" 15 class="btn btn-outline-secondary text-uppercase" i18n-title title="Copy"
16 > 16 >
17 <span class="glyphicon glyphicon-copy"></span> 17 <span class="glyphicon glyphicon-duplicate"></span>
18 Copy
18 </button> 19 </button>
19 </div> 20 </div>
20</div> 21</div>
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.scss b/client/src/app/shared/shared-forms/markdown-textarea.component.scss
index fcddfea03..8203c7d1c 100644
--- a/client/src/app/shared/shared-forms/markdown-textarea.component.scss
+++ b/client/src/app/shared/shared-forms/markdown-textarea.component.scss
@@ -131,7 +131,7 @@ $input-border-radius: 3px;
131 border-right: none; 131 border-right: none;
132 132
133 :last-child { 133 :last-child {
134 margin-right: $not-expanded-horizontal-margins; 134 margin-right: pvar(--horizontalMarginContent);
135 } 135 }
136 } 136 }
137 137
diff --git a/client/src/app/shared/shared-forms/select/select-options.component.ts b/client/src/app/shared/shared-forms/select/select-options.component.ts
index 2890670e5..8482b9dea 100644
--- a/client/src/app/shared/shared-forms/select/select-options.component.ts
+++ b/client/src/app/shared/shared-forms/select/select-options.component.ts
@@ -1,4 +1,4 @@
1import { Component, forwardRef, Input } from '@angular/core' 1import { Component, forwardRef, HostListener, Input } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { SelectOptionsItem } from '../../../../types/select-options-item.model' 3import { SelectOptionsItem } from '../../../../types/select-options-item.model'
4 4
@@ -26,6 +26,13 @@ export class SelectOptionsComponent implements ControlValueAccessor {
26 26
27 propagateChange = (_: any) => { /* empty */ } 27 propagateChange = (_: any) => { /* empty */ }
28 28
29 // Allow plugins to update our value
30 @HostListener('change', [ '$event.target' ])
31 handleChange (event: any) {
32 this.writeValue(event.value)
33 this.onModelChange()
34 }
35
29 writeValue (id: number | string) { 36 writeValue (id: number | string) {
30 this.selectedId = id 37 this.selectedId = id
31 } 38 }
diff --git a/client/src/app/shared/shared-instance/instance-about-accordion.component.scss b/client/src/app/shared/shared-instance/instance-about-accordion.component.scss
index 275600d60..2f6b420e3 100644
--- a/client/src/app/shared/shared-instance/instance-about-accordion.component.scss
+++ b/client/src/app/shared/shared-instance/instance-about-accordion.component.scss
@@ -31,7 +31,7 @@ ngb-accordion ::ng-deep {
31 padding: 0; 31 padding: 0;
32 32
33 & + .collapse.show { 33 & + .collapse.show {
34 background-color: var(--submenuColor); 34 background-color: var(--submenuBackgroundColor);
35 } 35 }
36 } 36 }
37 } 37 }
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.html b/client/src/app/shared/shared-instance/instance-features-table.component.html
index ce2557147..d505b6739 100644
--- a/client/src/app/shared/shared-instance/instance-features-table.component.html
+++ b/client/src/app/shared/shared-instance/instance-features-table.component.html
@@ -11,7 +11,7 @@
11 <tr> 11 <tr>
12 <th i18n class="label" scope="row"> 12 <th i18n class="label" scope="row">
13 <div>Default NSFW/sensitive videos policy</div> 13 <div>Default NSFW/sensitive videos policy</div>
14 <div class="more-info">can be redefined by the users</div> 14 <div class="c-hand more-info" (click)="openQuickSettingsHighlight()">can be redefined by the users</div>
15 </th> 15 </th>
16 16
17 <td class="value">{{ buildNSFWLabel() }}</td> 17 <td class="value">{{ buildNSFWLabel() }}</td>
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.ts b/client/src/app/shared/shared-instance/instance-features-table.component.ts
index 0166157f9..c3b3dfdfd 100644
--- a/client/src/app/shared/shared-instance/instance-features-table.component.ts
+++ b/client/src/app/shared/shared-instance/instance-features-table.component.ts
@@ -1,6 +1,7 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ServerService } from '@app/core' 2import { ServerService } from '@app/core'
3import { ServerConfig } from '@shared/models' 3import { ServerConfig } from '@shared/models'
4import { PeertubeModalService } from '../shared-main/peertube-modal/peertube-modal.service'
4 5
5@Component({ 6@Component({
6 selector: 'my-instance-features-table', 7 selector: 'my-instance-features-table',
@@ -11,7 +12,10 @@ export class InstanceFeaturesTableComponent implements OnInit {
11 quotaHelpIndication = '' 12 quotaHelpIndication = ''
12 serverConfig: ServerConfig 13 serverConfig: ServerConfig
13 14
14 constructor (private serverService: ServerService) { } 15 constructor (
16 private serverService: ServerService,
17 private modalService: PeertubeModalService
18 ) { }
15 19
16 get initialUserVideoQuota () { 20 get initialUserVideoQuota () {
17 return this.serverConfig.user.videoQuota 21 return this.serverConfig.user.videoQuota
@@ -56,6 +60,10 @@ export class InstanceFeaturesTableComponent implements OnInit {
56 return this.serverService.getServerVersionAndCommit() 60 return this.serverService.getServerVersionAndCommit()
57 } 61 }
58 62
63 openQuickSettingsHighlight () {
64 this.modalService.openQuickSettingsSubject.next()
65 }
66
59 private getApproximateTime (seconds: number) { 67 private getApproximateTime (seconds: number) {
60 const hours = Math.floor(seconds / 3600) 68 const hours = Math.floor(seconds / 3600)
61 let pluralSuffix = '' 69 let pluralSuffix = ''
diff --git a/client/src/app/shared/shared-main/account/account.model.ts b/client/src/app/shared/shared-main/account/account.model.ts
index b71a893d1..17fddff09 100644
--- a/client/src/app/shared/shared-main/account/account.model.ts
+++ b/client/src/app/shared/shared-main/account/account.model.ts
@@ -1,4 +1,4 @@
1import { Account as ServerAccount, Avatar } from '@shared/models' 1import { Account as ServerAccount, ActorImage } from '@shared/models'
2import { Actor } from './actor.model' 2import { Actor } from './actor.model'
3 3
4export class Account extends Actor implements ServerAccount { 4export class Account extends Actor implements ServerAccount {
@@ -38,7 +38,7 @@ export class Account extends Actor implements ServerAccount {
38 this.mutedServerByInstance = false 38 this.mutedServerByInstance = false
39 } 39 }
40 40
41 updateAvatar (newAvatar: Avatar) { 41 updateAvatar (newAvatar: ActorImage) {
42 this.avatar = newAvatar 42 this.avatar = newAvatar
43 43
44 this.updateComputedAttributes() 44 this.updateComputedAttributes()
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 30584fd00..000000000
--- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.html
+++ /dev/null
@@ -1,43 +0,0 @@
1<ng-container *ngIf="actor">
2 <div class="actor">
3 <div class="d-flex">
4 <img [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
23 <div class="actor-info">
24 <div class="actor-info-names">
25 <div class="actor-info-display-name">{{ actor.displayName }}</div>
26 <div class="actor-info-username">{{ actor.name }}</div>
27 </div>
28 <div i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div>
29 </div>
30 </div>
31</ng-container>
32
33<ng-template #avatarEditContent>
34 <div class="dropdown-item c-hand" [ngbTooltip]="avatarFormat" placement="right" container="body">
35 <my-global-icon iconName="upload"></my-global-icon>
36 <span for="avatarfile" i18n>Upload a new avatar</span>
37 <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
38 </div>
39 <div class="dropdown-item c-hand" (click)="deleteAvatar()" (key.enter)="deleteAvatar()">
40 <my-global-icon iconName="delete"></my-global-icon>
41 <span i18n>Remove avatar</span>
42 </div>
43</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 57c298508..000000000
--- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss
+++ /dev/null
@@ -1,86 +0,0 @@
1@import '_variables';
2@import '_mixins';
3
4.actor {
5 display: flex;
6
7 img {
8 @include avatar(100px);
9
10 margin-right: 15px;
11 }
12
13 .actor-img-edit-container {
14 position: relative;
15 width: 0;
16
17 .actor-img-edit-button {
18 @include peertube-button-file(21px);
19 @include button-with-icon(19px);
20 @include orange-button;
21
22 margin-top: 10px;
23 margin-bottom: 5px;
24 border-radius: 50%;
25 top: 55px;
26 right: 45px;
27 cursor: pointer;
28
29 input {
30 width: 30px;
31 height: 30px;
32 }
33
34 my-global-icon {
35 right: 7px;
36 }
37 }
38 }
39
40 .actor-info {
41 justify-content: center;
42 display: inline-flex;
43 flex-direction: column;
44
45 .actor-info-names {
46 display: flex;
47 align-items: center;
48
49 .actor-info-display-name {
50 font-size: 20px;
51 font-weight: $font-bold;
52
53 @media screen and (max-width: $small-view) {
54 font-size: 16px;
55 }
56 }
57
58 .actor-info-username {
59 margin-left: 7px;
60 position: relative;
61 top: 2px;
62 font-size: 14px;
63 color: $grey-actor-name;
64 }
65 }
66
67 .actor-info-followers {
68 font-size: 15px;
69 padding-bottom: .5rem;
70 }
71 }
72}
73
74.actor-img-edit-container ::ng-deep .popover-avatar-info .popover-body {
75 padding: 0;
76
77 .dropdown-item {
78 padding: 6px 10px;
79 border-radius: 4px;
80
81 &:first-child {
82 @include peertube-file;
83 display: block;
84 }
85 }
86}
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 8222c9769..1ee0c297e 100644
--- a/client/src/app/shared/shared-main/account/actor.model.ts
+++ b/client/src/app/shared/shared-main/account/actor.model.ts
@@ -1,17 +1,20 @@
1import { Actor as ActorServer, Avatar } from '@shared/models' 1import { Actor as ActorServer, ActorImage } from '@shared/models'
2import { getAbsoluteAPIUrl } from '@app/helpers' 2import { 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: Avatar
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) {
@@ -42,11 +47,11 @@ export abstract class Actor implements ActorServer {
42 return host.trim() === thisHost 47 return host.trim() === thisHost
43 } 48 }
44 49
45 protected constructor (hash: ActorServer) { 50 protected constructor (hash: Partial<ActorServer>) {
46 this.id = hash.id 51 this.id = hash.id
47 this.url = hash.url 52 this.url = hash.url ?? ''
48 this.name = hash.name 53 this.name = hash.name ?? ''
49 this.host = hash.host 54 this.host = hash.host ?? ''
50 this.followingCount = hash.followingCount 55 this.followingCount = hash.followingCount
51 this.followersCount = hash.followersCount 56 this.followersCount = hash.followersCount
52 57
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/account/video-avatar-channel.component.html b/client/src/app/shared/shared-main/account/video-avatar-channel.component.html
deleted file mode 100644
index 310cc926f..000000000
--- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.html
+++ /dev/null
@@ -1,26 +0,0 @@
1<div class="wrapper" [ngClass]="'avatar-' + size">
2 <ng-container *ngIf="!isChannelAvatarNull() && !genericChannel">
3 <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle">
4 <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" />
5 </a>
6 <a [routerLink]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle">
7 <img [src]="video.accountAvatarUrl" i18n-alt alt="Account avatar" />
8 </a>
9 </ng-container>
10
11 <ng-container *ngIf="!isChannelAvatarNull() && genericChannel">
12 <a [routerLink]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle">
13 <img [src]="video.accountAvatarUrl" i18n-alt alt="Account avatar" />
14 </a>
15
16 <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle">
17 <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" />
18 </a>
19 </ng-container>
20
21 <ng-container *ngIf="isChannelAvatarNull()">
22 <a [routerLink]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle">
23 <img [src]="video.accountAvatarUrl" i18n-alt alt="Account avatar" />
24 </a>
25 </ng-container>
26</div>
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.scss b/client/src/app/shared/shared-main/account/video-avatar-channel.component.scss
deleted file mode 100644
index 37709fce6..000000000
--- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.scss
+++ /dev/null
@@ -1,40 +0,0 @@
1@import '_mixins';
2
3.wrapper {
4 $avatar-size: 35px;
5
6 width: $avatar-size;
7 height: $avatar-size;
8 position: relative;
9 margin-right: 5px;
10 margin-bottom: 5px;
11
12 &.avatar-sm {
13 width: 28px;
14 height: 28px;
15 margin-bottom: 3px;
16 }
17
18 a {
19 @include disable-outline;
20 }
21
22 a img {
23 height: 100%;
24 object-fit: cover;
25 position: absolute;
26 top:50%;
27 left:50%;
28 border-radius: 50%;
29 transform: translate(-50%,-50%)
30 }
31
32 a:nth-of-type(2) img {
33 height: 60%;
34 width: 60%;
35 border: 2px solid pvar(--mainBackgroundColor);
36 transform: translateX(15%);
37 position: relative;
38 background-color: pvar(--mainBackgroundColor);
39 }
40}
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts b/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts
deleted file mode 100644
index 440e2b522..000000000
--- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts
+++ /dev/null
@@ -1,27 +0,0 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { Video } from '../video/video.model'
3
4@Component({
5 selector: 'my-video-avatar-channel',
6 templateUrl: './video-avatar-channel.component.html',
7 styleUrls: [ './video-avatar-channel.component.scss' ]
8})
9export class VideoAvatarChannelComponent implements OnInit {
10 @Input() video: Video
11 @Input() byAccount: string
12
13 @Input() size: 'md' | 'sm' = 'md'
14 @Input() genericChannel: boolean
15
16 channelLinkTitle = ''
17 accountLinkTitle = ''
18
19 ngOnInit () {
20 this.channelLinkTitle = $localize`${this.video.account.name} (channel page)`
21 this.accountLinkTitle = $localize`${this.video.byAccount} (account page)`
22 }
23
24 isChannelAvatarNull () {
25 return this.video.channel.avatar === null
26 }
27}
diff --git a/client/src/app/shared/shared-main/angular/autofocus.directive.ts b/client/src/app/shared/shared-main/angular/autofocus.directive.ts
new file mode 100644
index 000000000..5f087d79d
--- /dev/null
+++ b/client/src/app/shared/shared-main/angular/autofocus.directive.ts
@@ -0,0 +1,12 @@
1import { AfterViewInit, Directive, ElementRef } from '@angular/core'
2
3@Directive({
4 selector: '[autofocus]'
5})
6export class AutofocusDirective implements AfterViewInit {
7 constructor (private host: ElementRef) { }
8
9 ngAfterViewInit () {
10 this.host.nativeElement.focus()
11 }
12}
diff --git a/client/src/app/shared/shared-main/angular/index.ts b/client/src/app/shared/shared-main/angular/index.ts
index 29f8b3650..8ea47bb33 100644
--- a/client/src/app/shared/shared-main/angular/index.ts
+++ b/client/src/app/shared/shared-main/angular/index.ts
@@ -1,3 +1,4 @@
1export * from './autofocus.directive'
1export * from './bytes.pipe' 2export * from './bytes.pipe'
2export * from './duration-formatter.pipe' 3export * from './duration-formatter.pipe'
3export * from './from-now.pipe' 4export * from './from-now.pipe'
diff --git a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
index 3ddaffbdf..4fe3b964d 100644
--- a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
+++ b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
@@ -27,7 +27,9 @@ export class AuthInterceptor implements HttpInterceptor {
27 catchError((err: HttpErrorResponse) => { 27 catchError((err: HttpErrorResponse) => {
28 if (err.status === HttpStatusCode.UNAUTHORIZED_401 && err.error && err.error.code === 'invalid_token') { 28 if (err.status === HttpStatusCode.UNAUTHORIZED_401 && err.error && err.error.code === 'invalid_token') {
29 return this.handleTokenExpired(req, next) 29 return this.handleTokenExpired(req, next)
30 } else if (err.status === HttpStatusCode.UNAUTHORIZED_401) { 30 }
31
32 if (err.status === HttpStatusCode.UNAUTHORIZED_401) {
31 return this.handleNotAuthenticated(err) 33 return this.handleNotAuthenticated(err)
32 } 34 }
33 35
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.html b/client/src/app/shared/shared-main/misc/simple-search-input.component.html
index fb0d97122..c20c02e23 100644
--- a/client/src/app/shared/shared-main/misc/simple-search-input.component.html
+++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.html
@@ -1,14 +1,15 @@
1<span> 1<div class="root">
2 <my-global-icon iconName="search" aria-label="Search" role="button" (click)="showInput()"></my-global-icon>
3
4 <input 2 <input
5 #ref 3 #ref
6 type="text" 4 type="text"
7 [(ngModel)]="value" 5 [(ngModel)]="value"
8 (focusout)="focusLost()"
9 (keyup.enter)="searchChange()" 6 (keyup.enter)="searchChange()"
10 [hidden]="!shown" 7 [hidden]="!inputShown"
11 [name]="name" 8 [name]="name"
12 [placeholder]="placeholder" 9 [placeholder]="placeholder"
13 > 10 >
14</span> 11
12 <my-global-icon iconName="search" aria-label="Search" role="button" (click)="onIconClick()" [title]="iconTitle"></my-global-icon>
13
14 <my-global-icon *ngIf="!alwaysShow && inputShown" i18n-title title="Close search" iconName="cross" (click)="hideInput()"></my-global-icon>
15</div>
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.scss b/client/src/app/shared/shared-main/misc/simple-search-input.component.scss
index 591b04fb2..5ae48f81b 100644
--- a/client/src/app/shared/shared-main/misc/simple-search-input.component.scss
+++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.scss
@@ -1,29 +1,29 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4span { 4.root {
5 opacity: .6; 5 display: flex;
6
7 &:focus-within {
8 opacity: 1;
9 }
10} 6}
11 7
12my-global-icon { 8my-global-icon {
13 height: 18px; 9 height: 28px;
14 position: relative; 10 width: 28px;
15 top: -2px; 11 margin-left: 10px;
16} 12 cursor: pointer;
17 13
18input { 14 &:hover {
19 @include peertube-input-text(150px); 15 color: pvar(--mainHoverColor);
16 }
20 17
21 height: 22px; // maximum height for the account/video-channels links 18 &[iconName=search] {
22 padding-left: 10px; 19 color: pvar(--mainForegroundColor);
23 background-color: transparent; 20 }
24 border: none;
25 21
26 &::placeholder { 22 &[iconName=cross] {
27 font-size: 15px; 23 color: pvar(--mainForegroundColor);
28 } 24 }
29} 25}
26
27input {
28 @include peertube-input-text(200px);
29}
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.ts b/client/src/app/shared/shared-main/misc/simple-search-input.component.ts
index 86ae9ab42..224d71134 100644
--- a/client/src/app/shared/shared-main/misc/simple-search-input.component.ts
+++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.ts
@@ -1,7 +1,7 @@
1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { Subject } from 'rxjs' 1import { Subject } from 'rxjs'
4import { debounceTime, distinctUntilChanged } from 'rxjs/operators' 2import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
3import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router'
5 5
6@Component({ 6@Component({
7 selector: 'simple-search-input', 7 selector: 'simple-search-input',
@@ -13,11 +13,14 @@ export class SimpleSearchInputComponent implements OnInit {
13 13
14 @Input() name = 'search' 14 @Input() name = 'search'
15 @Input() placeholder = $localize`Search` 15 @Input() placeholder = $localize`Search`
16 @Input() iconTitle = $localize`Search`
17 @Input() alwaysShow = true
16 18
17 @Output() searchChanged = new EventEmitter<string>() 19 @Output() searchChanged = new EventEmitter<string>()
20 @Output() inputDisplayChanged = new EventEmitter<boolean>()
18 21
19 value = '' 22 value = ''
20 shown: boolean 23 inputShown: boolean
21 24
22 private searchSubject = new Subject<string>() 25 private searchSubject = new Subject<string>()
23 26
@@ -35,20 +38,51 @@ export class SimpleSearchInputComponent implements OnInit {
35 .subscribe(value => this.searchChanged.emit(value)) 38 .subscribe(value => this.searchChanged.emit(value))
36 39
37 this.searchSubject.next(this.value) 40 this.searchSubject.next(this.value)
41
42 if (this.isInputShown()) this.showInput(false)
38 } 43 }
39 44
40 showInput () { 45 isInputShown () {
41 this.shown = true 46 if (this.alwaysShow) return true
42 setTimeout(() => this.input.nativeElement.focus()) 47
48 return this.inputShown
49 }
50
51 onIconClick () {
52 if (!this.isInputShown()) {
53 this.showInput()
54 return
55 }
56
57 this.searchChange()
58 }
59
60 showInput (focus = true) {
61 this.inputShown = true
62 this.inputDisplayChanged.emit(this.inputShown)
63
64 if (focus) {
65 setTimeout(() => this.input.nativeElement.focus())
66 }
67 }
68
69 hideInput () {
70 this.inputShown = false
71
72 if (this.isInputShown() === false) {
73 this.inputDisplayChanged.emit(this.inputShown)
74 }
43 } 75 }
44 76
45 focusLost () { 77 focusLost () {
46 if (this.value !== '') return 78 if (this.value) return
47 this.shown = false 79
80 this.hideInput()
48 } 81 }
49 82
50 searchChange () { 83 searchChange () {
51 this.router.navigate(['./search'], { relativeTo: this.route }) 84 this.router.navigate([ './search' ], { relativeTo: this.route })
85
52 this.searchSubject.next(this.value) 86 this.searchSubject.next(this.value)
53 } 87 }
54} 88}
diff --git a/client/src/app/shared/shared-main/peertube-modal/index.ts b/client/src/app/shared/shared-main/peertube-modal/index.ts
new file mode 100644
index 000000000..d631522e4
--- /dev/null
+++ b/client/src/app/shared/shared-main/peertube-modal/index.ts
@@ -0,0 +1 @@
export * from './peertube-modal.service'
diff --git a/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts b/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts
new file mode 100644
index 000000000..79da08a5c
--- /dev/null
+++ b/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts
@@ -0,0 +1,7 @@
1import { Injectable } from '@angular/core'
2import { Subject } from 'rxjs'
3
4@Injectable({ providedIn: 'root' })
5export class PeertubeModalService {
6 openQuickSettingsSubject = new Subject<void>()
7}
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 9d550996d..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,19 +6,20 @@ 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 BytesPipe, 23 BytesPipe,
23 DurationFormatterPipe, 24 DurationFormatterPipe,
24 FromNowPipe, 25 FromNowPipe,
@@ -31,7 +32,7 @@ import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditBu
31import { DateToggleComponent } from './date' 32import { DateToggleComponent } from './date'
32import { FeedComponent } from './feeds' 33import { FeedComponent } from './feeds'
33import { LoaderComponent, SmallLoaderComponent } from './loaders' 34import { LoaderComponent, SmallLoaderComponent } from './loaders'
34import { HelpComponent, ListOverflowComponent, TopMenuDropdownComponent, SimpleSearchInputComponent } from './misc' 35import { HelpComponent, ListOverflowComponent, SimpleSearchInputComponent, TopMenuDropdownComponent } from './misc'
35import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' 36import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users'
36import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' 37import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video'
37import { VideoCaptionService } from './video-caption' 38import { VideoCaptionService } from './video-caption'
@@ -64,13 +65,11 @@ import { VideoChannelService } from './video-channel'
64 ], 65 ],
65 66
66 declarations: [ 67 declarations: [
67 VideoAvatarChannelComponent,
68 ActorAvatarInfoComponent,
69
70 FromNowPipe, 68 FromNowPipe,
71 NumberFormatterPipe, 69 NumberFormatterPipe,
72 BytesPipe, 70 BytesPipe,
73 DurationFormatterPipe, 71 DurationFormatterPipe,
72 AutofocusDirective,
74 73
75 InfiniteScrollerDirective, 74 InfiniteScrollerDirective,
76 PeerTubeTemplateDirective, 75 PeerTubeTemplateDirective,
@@ -118,13 +117,11 @@ import { VideoChannelService } from './video-channel'
118 117
119 PrimeSharedModule, 118 PrimeSharedModule,
120 119
121 VideoAvatarChannelComponent,
122 ActorAvatarInfoComponent,
123
124 FromNowPipe, 120 FromNowPipe,
125 BytesPipe, 121 BytesPipe,
126 NumberFormatterPipe, 122 NumberFormatterPipe,
127 DurationFormatterPipe, 123 DurationFormatterPipe,
124 AutofocusDirective,
128 125
129 InfiniteScrollerDirective, 126 InfiniteScrollerDirective,
130 PeerTubeTemplateDirective, 127 PeerTubeTemplateDirective,
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts
index 1211995fd..88a4811da 100644
--- a/client/src/app/shared/shared-main/users/user-notification.model.ts
+++ b/client/src/app/shared/shared-main/users/user-notification.model.ts
@@ -6,6 +6,7 @@ import {
6 AbuseState, 6 AbuseState,
7 ActorInfo, 7 ActorInfo,
8 FollowState, 8 FollowState,
9 PluginType,
9 UserNotification as UserNotificationServer, 10 UserNotification as UserNotificationServer,
10 UserNotificationType, 11 UserNotificationType,
11 UserRight, 12 UserRight,
@@ -74,20 +75,40 @@ export class UserNotification implements UserNotificationServer {
74 } 75 }
75 } 76 }
76 77
78 plugin?: {
79 name: string
80 type: PluginType
81 latestVersion: string
82 }
83
84 peertube?: {
85 latestVersion: string
86 }
87
77 createdAt: string 88 createdAt: string
78 updatedAt: string 89 updatedAt: string
79 90
80 // Additional fields 91 // Additional fields
81 videoUrl?: string 92 videoUrl?: string
82 commentUrl?: any[] 93 commentUrl?: any[]
94
83 abuseUrl?: string 95 abuseUrl?: string
84 abuseQueryParams?: { [id: string]: string } = {} 96 abuseQueryParams?: { [id: string]: string } = {}
97
85 videoAutoBlacklistUrl?: string 98 videoAutoBlacklistUrl?: string
99
86 accountUrl?: string 100 accountUrl?: string
101
87 videoImportIdentifier?: string 102 videoImportIdentifier?: string
88 videoImportUrl?: string 103 videoImportUrl?: string
104
89 instanceFollowUrl?: string 105 instanceFollowUrl?: string
90 106
107 peertubeVersionLink?: string
108
109 pluginUrl?: string
110 pluginQueryParams?: { [id: string]: string } = {}
111
91 constructor (hash: UserNotificationServer, user: AuthUser) { 112 constructor (hash: UserNotificationServer, user: AuthUser) {
92 this.id = hash.id 113 this.id = hash.id
93 this.type = hash.type 114 this.type = hash.type
@@ -114,6 +135,9 @@ export class UserNotification implements UserNotificationServer {
114 this.actorFollow = hash.actorFollow 135 this.actorFollow = hash.actorFollow
115 if (this.actorFollow) this.setAccountAvatarUrl(this.actorFollow.follower) 136 if (this.actorFollow) this.setAccountAvatarUrl(this.actorFollow.follower)
116 137
138 this.plugin = hash.plugin
139 this.peertube = hash.peertube
140
117 this.createdAt = hash.createdAt 141 this.createdAt = hash.createdAt
118 this.updatedAt = hash.updatedAt 142 this.updatedAt = hash.updatedAt
119 143
@@ -197,6 +221,15 @@ export class UserNotification implements UserNotificationServer {
197 case UserNotificationType.AUTO_INSTANCE_FOLLOWING: 221 case UserNotificationType.AUTO_INSTANCE_FOLLOWING:
198 this.instanceFollowUrl = '/admin/follows/following-list' 222 this.instanceFollowUrl = '/admin/follows/following-list'
199 break 223 break
224
225 case UserNotificationType.NEW_PEERTUBE_VERSION:
226 this.peertubeVersionLink = 'https://joinpeertube.org/news'
227 break
228
229 case UserNotificationType.NEW_PLUGIN_VERSION:
230 this.pluginUrl = `/admin/plugins/list-installed`
231 this.pluginQueryParams.pluginType = this.plugin.type + ''
232 break
200 } 233 }
201 } catch (err) { 234 } catch (err) {
202 this.type = null 235 this.type = null
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html
index 265af8d55..325f0eaae 100644
--- a/client/src/app/shared/shared-main/users/user-notifications.component.html
+++ b/client/src/app/shared/shared-main/users/user-notifications.component.html
@@ -4,7 +4,7 @@
4 <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)"> 4 <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)">
5 5
6 <ng-container [ngSwitch]="notification.type"> 6 <ng-container [ngSwitch]="notification.type">
7 <ng-container *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION"> 7 <ng-container *ngSwitchCase="1"> <!-- UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION -->
8 <ng-container *ngIf="notification.video; then hasVideo; else noVideo"></ng-container> 8 <ng-container *ngIf="notification.video; then hasVideo; else noVideo"></ng-container>
9 9
10 <ng-template #hasVideo> 10 <ng-template #hasVideo>
@@ -26,7 +26,7 @@
26 </ng-template> 26 </ng-template>
27 </ng-container> 27 </ng-container>
28 28
29 <ng-container *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO"> 29 <ng-container *ngSwitchCase="5"> <!-- UserNotificationType.UNBLACKLIST_ON_MY_VIDEO -->
30 <my-global-icon iconName="undo" aria-hidden="true"></my-global-icon> 30 <my-global-icon iconName="undo" aria-hidden="true"></my-global-icon>
31 31
32 <div class="message" i18n> 32 <div class="message" i18n>
@@ -34,7 +34,7 @@
34 </div> 34 </div>
35 </ng-container> 35 </ng-container>
36 36
37 <ng-container *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO"> 37 <ng-container *ngSwitchCase="4"> <!-- UserNotificationType.BLACKLIST_ON_MY_VIDEO -->
38 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon> 38 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon>
39 39
40 <div class="message" i18n> 40 <div class="message" i18n>
@@ -42,7 +42,7 @@
42 </div> 42 </div>
43 </ng-container> 43 </ng-container>
44 44
45 <ng-container *ngSwitchCase="UserNotificationType.NEW_ABUSE_FOR_MODERATORS"> 45 <ng-container *ngSwitchCase="3"> <!-- UserNotificationType.NEW_ABUSE_FOR_MODERATORS -->
46 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> 46 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
47 47
48 <div class="message" *ngIf="notification.videoUrl" i18n> 48 <div class="message" *ngIf="notification.videoUrl" i18n>
@@ -63,7 +63,7 @@
63 </div> 63 </div>
64 </ng-container> 64 </ng-container>
65 65
66 <ng-container *ngSwitchCase="UserNotificationType.ABUSE_STATE_CHANGE"> 66 <ng-container *ngSwitchCase="15"> <!-- UserNotificationType.ABUSE_STATE_CHANGE -->
67 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> 67 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
68 68
69 <div class="message" i18n> 69 <div class="message" i18n>
@@ -73,7 +73,7 @@
73 </div> 73 </div>
74 </ng-container> 74 </ng-container>
75 75
76 <ng-container *ngSwitchCase="UserNotificationType.ABUSE_NEW_MESSAGE"> 76 <ng-container *ngSwitchCase="16"> <!-- UserNotificationType.ABUSE_NEW_MESSAGE -->
77 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> 77 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
78 78
79 <div class="message" i18n> 79 <div class="message" i18n>
@@ -81,7 +81,7 @@
81 </div> 81 </div>
82 </ng-container> 82 </ng-container>
83 83
84 <ng-container *ngSwitchCase="UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS"> 84 <ng-container *ngSwitchCase="12"> <!-- UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS -->
85 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon> 85 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon>
86 86
87 <div class="message" i18n> 87 <div class="message" i18n>
@@ -89,7 +89,7 @@
89 </div> 89 </div>
90 </ng-container> 90 </ng-container>
91 91
92 <ng-container *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO"> 92 <ng-container *ngSwitchCase="2">
93 <ng-container *ngIf="notification.comment"> 93 <ng-container *ngIf="notification.comment">
94 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> 94 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
95 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> 95 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
@@ -109,7 +109,7 @@
109 </ng-container> 109 </ng-container>
110 </ng-container> 110 </ng-container>
111 111
112 <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED"> 112 <ng-container *ngSwitchCase="6"> <!-- UserNotificationType.MY_VIDEO_PUBLISHED -->
113 <my-global-icon iconName="film" aria-hidden="true"></my-global-icon> 113 <my-global-icon iconName="film" aria-hidden="true"></my-global-icon>
114 114
115 <div class="message" i18n> 115 <div class="message" i18n>
@@ -117,7 +117,7 @@
117 </div> 117 </div>
118 </ng-container> 118 </ng-container>
119 119
120 <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS"> 120 <ng-container *ngSwitchCase="7"> <!-- UserNotificationType.MY_VIDEO_IMPORT_SUCCESS -->
121 <my-global-icon iconName="cloud-download" aria-hidden="true"></my-global-icon> 121 <my-global-icon iconName="cloud-download" aria-hidden="true"></my-global-icon>
122 122
123 <div class="message" i18n> 123 <div class="message" i18n>
@@ -125,7 +125,7 @@
125 </div> 125 </div>
126 </ng-container> 126 </ng-container>
127 127
128 <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR"> 128 <ng-container *ngSwitchCase="8"> <!-- UserNotificationType.MY_VIDEO_IMPORT_ERROR -->
129 <my-global-icon iconName="cloud-error" aria-hidden="true"></my-global-icon> 129 <my-global-icon iconName="cloud-error" aria-hidden="true"></my-global-icon>
130 130
131 <div class="message" i18n> 131 <div class="message" i18n>
@@ -133,7 +133,7 @@
133 </div> 133 </div>
134 </ng-container> 134 </ng-container>
135 135
136 <ng-container *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION"> 136 <ng-container *ngSwitchCase="9"> <!-- UserNotificationType.NEW_USER_REGISTRATION -->
137 <my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon> 137 <my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon>
138 138
139 <div class="message" i18n> 139 <div class="message" i18n>
@@ -141,7 +141,7 @@
141 </div> 141 </div>
142 </ng-container> 142 </ng-container>
143 143
144 <ng-container *ngSwitchCase="UserNotificationType.NEW_FOLLOW"> 144 <ng-container *ngSwitchCase="10"> <!-- UserNotificationType.NEW_FOLLOW -->
145 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> 145 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
146 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" /> 146 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" />
147 </a> 147 </a>
@@ -154,7 +154,7 @@
154 </div> 154 </div>
155 </ng-container> 155 </ng-container>
156 156
157 <ng-container *ngSwitchCase="UserNotificationType.COMMENT_MENTION"> 157 <ng-container *ngSwitchCase="11">
158 <ng-container *ngIf="notification.comment"> 158 <ng-container *ngIf="notification.comment">
159 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> 159 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
160 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> 160 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
@@ -174,7 +174,7 @@
174 </ng-container> 174 </ng-container>
175 </ng-container> 175 </ng-container>
176 176
177 <ng-container *ngSwitchCase="UserNotificationType.NEW_INSTANCE_FOLLOWER"> 177 <ng-container *ngSwitchCase="13"> <!-- UserNotificationType.NEW_INSTANCE_FOLLOWER -->
178 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon> 178 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon>
179 179
180 <div class="message" i18n> 180 <div class="message" i18n>
@@ -183,7 +183,7 @@
183 </div> 183 </div>
184 </ng-container> 184 </ng-container>
185 185
186 <ng-container *ngSwitchCase="UserNotificationType.AUTO_INSTANCE_FOLLOWING"> 186 <ng-container *ngSwitchCase="14"> <!-- UserNotificationType.AUTO_INSTANCE_FOLLOWING -->
187 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon> 187 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon>
188 188
189 <div class="message" i18n> 189 <div class="message" i18n>
@@ -191,6 +191,22 @@
191 </div> 191 </div>
192 </ng-container> 192 </ng-container>
193 193
194 <ng-container *ngSwitchCase="17"> <!-- UserNotificationType.NEW_PLUGIN_VERSION -->
195 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
196
197 <div class="message" i18n>
198 <a (click)="markAsRead(notification)" [routerLink]="notification.pluginUrl" [queryParams]="notification.pluginQueryParams">A new version of the plugin/theme {{ notification.plugin.name }}</a> is available: {{ notification.plugin.latestVersion }}
199 </div>
200 </ng-container>
201
202 <ng-container *ngSwitchCase="18"> <!-- UserNotificationType.NEW_PEERTUBE_VERSION -->
203 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
204
205 <div class="message" i18n>
206 <a (click)="markAsRead(notification)" [href]="notification.peertubeVersionLink" target="_blank" rel="noopener noreferer">A new version of PeerTube</a> is available: {{ notification.peertube.latestVersion }}
207 </div>
208 </ng-container>
209
194 <ng-container *ngSwitchDefault> 210 <ng-container *ngSwitchDefault>
195 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> 211 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
196 212
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.ts b/client/src/app/shared/shared-main/users/user-notifications.component.ts
index 387c49d94..d7c722355 100644
--- a/client/src/app/shared/shared-main/users/user-notifications.component.ts
+++ b/client/src/app/shared/shared-main/users/user-notifications.component.ts
@@ -21,9 +21,6 @@ export class UserNotificationsComponent implements OnInit {
21 notifications: UserNotification[] = [] 21 notifications: UserNotification[] = []
22 sortField = 'createdAt' 22 sortField = 'createdAt'
23 23
24 // So we can access it in the template
25 UserNotificationType = UserNotificationType
26
27 componentPagination: ComponentPagination 24 componentPagination: ComponentPagination
28 25
29 onDataSubject = new Subject<any[]>() 26 onDataSubject = new Subject<any[]>()
@@ -48,7 +45,7 @@ export class UserNotificationsComponent implements OnInit {
48 } 45 }
49 46
50 loadNotifications (reset?: boolean) { 47 loadNotifications (reset?: boolean) {
51 this.userNotificationService.listMyNotifications({ 48 const options = {
52 pagination: this.componentPagination, 49 pagination: this.componentPagination,
53 ignoreLoadingBar: this.ignoreLoadingBar, 50 ignoreLoadingBar: this.ignoreLoadingBar,
54 sort: { 51 sort: {
@@ -56,7 +53,9 @@ export class UserNotificationsComponent implements OnInit {
56 // if we order by creation date, we want DESC. all other fields are ASC (like unread). 53 // if we order by creation date, we want DESC. all other fields are ASC (like unread).
57 order: this.sortField === 'createdAt' ? -1 : 1 54 order: this.sortField === 'createdAt' ? -1 : 1
58 } 55 }
59 }) 56 }
57
58 this.userNotificationService.listMyNotifications(options)
60 .subscribe( 59 .subscribe(
61 result => { 60 result => {
62 this.notifications = reset ? result.data : this.notifications.concat(result.data) 61 this.notifications = reset ? result.data : this.notifications.concat(result.data)
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 c6a63fe6c..1ba3fcc0e 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,15 +1,22 @@
1import { VideoChannel as ServerVideoChannel, ViewsPerDate, Account, Avatar } from '@shared/models' 1import { getAbsoluteAPIUrl } from '@app/helpers'
2import { Account as ServerAccount, ActorImage, VideoChannel as ServerVideoChannel, ViewsPerDate } from '@shared/models'
3import { Account } from '../account/account.model'
2import { Actor } from '../account/actor.model' 4import { Actor } from '../account/actor.model'
3 5
4export class VideoChannel extends Actor implements ServerVideoChannel { 6export class VideoChannel extends Actor implements ServerVideoChannel {
5 displayName: string 7 displayName: string
6 description: string 8 description: string
7 support: string 9 support: string
10
8 isLocal: boolean 11 isLocal: boolean
12
9 nameWithHost: string 13 nameWithHost: string
10 nameWithHostForced: string 14 nameWithHostForced: string
11 15
12 ownerAccount?: Account 16 banner: ActorImage
17 bannerUrl: string
18
19 ownerAccount?: ServerAccount
13 ownerBy?: string 20 ownerBy?: string
14 ownerAvatarUrl?: string 21 ownerAvatarUrl?: string
15 22
@@ -21,19 +28,33 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
21 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()
22 } 29 }
23 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
24 static GET_DEFAULT_AVATAR_URL () { 43 static GET_DEFAULT_AVATAR_URL () {
25 return `${window.location.origin}/client/assets/images/default-avatar-videochannel.png` 44 return `${window.location.origin}/client/assets/images/default-avatar-videochannel.png`
26 } 45 }
27 46
28 constructor (hash: ServerVideoChannel) { 47 constructor (hash: Partial<ServerVideoChannel>) {
29 super(hash) 48 super(hash)
30 49
31 this.updateComputedAttributes()
32
33 this.displayName = hash.displayName 50 this.displayName = hash.displayName
34 this.description = hash.description 51 this.description = hash.description
35 this.support = hash.support 52 this.support = hash.support
53
54 this.banner = hash.banner
55
36 this.isLocal = hash.isLocal 56 this.isLocal = hash.isLocal
57
37 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) 58 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host)
38 this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true) 59 this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true)
39 60
@@ -46,22 +67,34 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
46 if (hash.ownerAccount) { 67 if (hash.ownerAccount) {
47 this.ownerAccount = hash.ownerAccount 68 this.ownerAccount = hash.ownerAccount
48 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)
49 this.ownerAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.ownerAccount) 70 this.ownerAvatarUrl = Account.GET_ACTOR_AVATAR_URL(this.ownerAccount)
50 } 71 }
72
73 this.updateComputedAttributes()
51 } 74 }
52 75
53 updateAvatar (newAvatar: Avatar) { 76 updateAvatar (newAvatar: ActorImage) {
54 this.avatar = newAvatar 77 this.avatar = newAvatar
55 78
56 this.updateComputedAttributes() 79 this.updateComputedAttributes()
57 } 80 }
58 81
59 resetAvatar () { 82 resetAvatar () {
60 this.avatar = null 83 this.updateAvatar(null)
61 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)
62 } 94 }
63 95
64 private updateComputedAttributes () { 96 updateComputedAttributes () {
65 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)
66 } 99 }
67} 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 eff3fad4d..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
@@ -3,7 +3,7 @@ import { catchError, map, tap } from 'rxjs/operators'
3import { HttpClient, HttpParams } from '@angular/common/http' 3import { HttpClient, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core' 4import { Injectable } from '@angular/core'
5import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' 5import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core'
6import { Avatar, ResultList, VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '@shared/models' 6import { ActorImage, ResultList, VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '@shared/models'
7import { environment } from '../../../../environments/environment' 7import { environment } from '../../../../environments/environment'
8import { Account } from '../account' 8import { Account } from '../account'
9import { AccountService } from '../account/account.service' 9import { AccountService } from '../account/account.service'
@@ -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: Avatar }>(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-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts
index adb6e884f..1c2c4a575 100644
--- a/client/src/app/shared/shared-main/video/video.model.ts
+++ b/client/src/app/shared/shared-main/video/video.model.ts
@@ -6,7 +6,7 @@ import { Actor } from '@app/shared/shared-main/account/actor.model'
6import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model' 6import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model'
7import { peertubeTranslate } from '@shared/core-utils/i18n' 7import { peertubeTranslate } from '@shared/core-utils/i18n'
8import { 8import {
9 Avatar, 9 ActorImage,
10 ServerConfig, 10 ServerConfig,
11 UserRight, 11 UserRight,
12 Video as VideoServerModel, 12 Video as VideoServerModel,
@@ -72,7 +72,7 @@ export class Video implements VideoServerModel {
72 displayName: string 72 displayName: string
73 url: string 73 url: string
74 host: string 74 host: string
75 avatar?: Avatar 75 avatar?: ActorImage
76 } 76 }
77 77
78 channel: { 78 channel: {
@@ -81,7 +81,7 @@ export class Video implements VideoServerModel {
81 displayName: string 81 displayName: string
82 url: string 82 url: string
83 host: string 83 host: string
84 avatar?: Avatar 84 avatar?: ActorImage
85 } 85 }
86 86
87 userHistory?: { 87 userHistory?: {
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-moderation/report-modals/video-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
index 5b06c0bc7..4ca6f52ad 100644
--- a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
+++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
@@ -61,7 +61,8 @@ export class VideoReportComponent extends FormReactive implements OnInit {
61 baseUrl: this.video.embedUrl, 61 baseUrl: this.video.embedUrl,
62 title: false, 62 title: false,
63 warningTitle: false 63 warningTitle: false
64 }) 64 }),
65 this.video.name
65 ) 66 )
66 ) 67 )
67 } 68 }
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.ts b/client/src/app/shared/shared-share-modal/video-share.component.ts
index b06ff3751..e8760bfcc 100644
--- a/client/src/app/shared/shared-share-modal/video-share.component.ts
+++ b/client/src/app/shared/shared-share-modal/video-share.component.ts
@@ -86,14 +86,14 @@ export class VideoShareComponent {
86 const options = this.getVideoOptions(this.video.embedUrl) 86 const options = this.getVideoOptions(this.video.embedUrl)
87 87
88 const embedUrl = buildVideoLink(options) 88 const embedUrl = buildVideoLink(options)
89 return buildVideoOrPlaylistEmbed(embedUrl) 89 return buildVideoOrPlaylistEmbed(embedUrl, this.video.name)
90 } 90 }
91 91
92 getPlaylistIframeCode () { 92 getPlaylistIframeCode () {
93 const options = this.getPlaylistOptions(this.playlist.embedUrl) 93 const options = this.getPlaylistOptions(this.playlist.embedUrl)
94 94
95 const embedUrl = buildPlaylistLink(options) 95 const embedUrl = buildPlaylistLink(options)
96 return buildVideoOrPlaylistEmbed(embedUrl) 96 return buildVideoOrPlaylistEmbed(embedUrl, this.playlist.displayName)
97 } 97 }
98 98
99 getVideoUrl () { 99 getVideoUrl () {
diff --git a/client/src/app/shared/shared-support-modal/index.ts b/client/src/app/shared/shared-support-modal/index.ts
new file mode 100644
index 000000000..f41bb4bc2
--- /dev/null
+++ b/client/src/app/shared/shared-support-modal/index.ts
@@ -0,0 +1,3 @@
1export * from './support-modal.component'
2
3export * from './shared-support-modal.module'
diff --git a/client/src/app/shared/shared-support-modal/shared-support-modal.module.ts b/client/src/app/shared/shared-support-modal/shared-support-modal.module.ts
new file mode 100644
index 000000000..1101d5535
--- /dev/null
+++ b/client/src/app/shared/shared-support-modal/shared-support-modal.module.ts
@@ -0,0 +1,24 @@
1import { NgModule } from '@angular/core'
2import { SharedFormModule } from '../shared-forms'
3import { SharedGlobalIconModule } from '../shared-icons'
4import { SharedMainModule } from '../shared-main/shared-main.module'
5import { SupportModalComponent } from './support-modal.component'
6
7@NgModule({
8 imports: [
9 SharedMainModule,
10 SharedFormModule,
11 SharedGlobalIconModule
12 ],
13
14 declarations: [
15 SupportModalComponent
16 ],
17
18 exports: [
19 SupportModalComponent
20 ],
21
22 providers: [ ]
23})
24export class SharedSupportModal { }
diff --git a/client/src/app/shared/shared-support-modal/support-modal.component.html b/client/src/app/shared/shared-support-modal/support-modal.component.html
new file mode 100644
index 000000000..4a967987f
--- /dev/null
+++ b/client/src/app/shared/shared-support-modal/support-modal.component.html
@@ -0,0 +1,15 @@
1<ng-template #modal let-hide="close">
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Support {{ displayName }}</h4>
4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div>
6
7 <div class="modal-body" [innerHTML]="htmlSupport"></div>
8
9 <div class="modal-footer inputs">
10 <input
11 type="button" role="button" i18n-value value="Maybe later" class="action-button action-button-cancel"
12 (click)="hide()" (key.enter)="hide()"
13 >
14 </div>
15</ng-template>
diff --git a/client/src/app/shared/shared-support-modal/support-modal.component.scss b/client/src/app/shared/shared-support-modal/support-modal.component.scss
new file mode 100644
index 000000000..184e09027
--- /dev/null
+++ b/client/src/app/shared/shared-support-modal/support-modal.component.scss
@@ -0,0 +1,3 @@
1.action-button-cancel {
2 margin-right: 0 !important;
3}
diff --git a/client/src/app/shared/shared-support-modal/support-modal.component.ts b/client/src/app/shared/shared-support-modal/support-modal.component.ts
new file mode 100644
index 000000000..ae603c7a8
--- /dev/null
+++ b/client/src/app/shared/shared-support-modal/support-modal.component.ts
@@ -0,0 +1,40 @@
1import { Component, Input, ViewChild } from '@angular/core'
2import { MarkdownService } from '@app/core'
3import { VideoDetails } from '@app/shared/shared-main'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5import { VideoChannel } from '@shared/models'
6
7@Component({
8 selector: 'my-support-modal',
9 templateUrl: './support-modal.component.html',
10 styleUrls: [ './support-modal.component.scss' ]
11})
12export class SupportModalComponent {
13 @Input() video: VideoDetails = null
14 @Input() videoChannel: VideoChannel = null
15
16 @ViewChild('modal', { static: true }) modal: NgbModal
17
18 htmlSupport = ''
19 displayName = ''
20
21 constructor (
22 private markdownService: MarkdownService,
23 private modalService: NgbModal
24 ) { }
25
26 show () {
27 const modalRef = this.modalService.open(this.modal, { centered: true })
28
29 const support = this.video?.support || this.videoChannel.support
30
31 this.markdownService.enhancedMarkdownToHTML(support)
32 .then(r => this.htmlSupport = r)
33
34 this.displayName = this.video
35 ? this.video.channel.displayName
36 : this.videoChannel.displayName
37
38 return modalRef
39 }
40}
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.html b/client/src/app/shared/shared-video-miniature/abstract-video-list.html
index 07f79cd6d..ee5df28be 100644
--- a/client/src/app/shared/shared-video-miniature/abstract-video-list.html
+++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.html
@@ -43,7 +43,7 @@
43 <div class="no-results" i18n *ngIf="hasDoneFirstQuery && videos.length === 0">No results.</div> 43 <div class="no-results" i18n *ngIf="hasDoneFirstQuery && videos.length === 0">No results.</div>
44 <div 44 <div
45 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()" 45 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()"
46 class="videos" 46 class="videos" [ngClass]="{ 'display-as-row': displayAsRow() }"
47 > 47 >
48 <ng-container *ngFor="let video of videos; trackBy: videoById;"> 48 <ng-container *ngFor="let video of videos; trackBy: videoById;">
49 <h2 class="date-title" *ngIf="getCurrentGroupedDateLabel(video)"> 49 <h2 class="date-title" *ngIf="getCurrentGroupedDateLabel(video)">
@@ -52,8 +52,7 @@
52 52
53 <div class="video-wrapper"> 53 <div class="video-wrapper">
54 <my-video-miniature 54 <my-video-miniature
55 [fitWidth]="true" 55 [video]="video" [user]="userMiniature" [displayAsRow]="displayAsRow()"
56 [video]="video" [user]="userMiniature" [ownerDisplayType]="ownerDisplayType"
57 [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions" 56 [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions"
58 (videoBlocked)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)" 57 (videoBlocked)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)"
59 > 58 >
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.scss b/client/src/app/shared/shared-video-miniature/abstract-video-list.scss
index 0a8aa8fa4..6570b63d0 100644
--- a/client/src/app/shared/shared-video-miniature/abstract-video-list.scss
+++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.scss
@@ -69,7 +69,16 @@ $iconSize: 16px;
69} 69}
70 70
71.margin-content { 71.margin-content {
72 @include fluid-videos-miniature-layout; 72 @include grid-videos-miniature-layout;
73}
74
75.display-as-row.videos {
76 margin-left: pvar(--horizontalMarginContent);
77 margin-right: pvar(--horizontalMarginContent);
78
79 .video-wrapper {
80 margin-bottom: 15px;
81 }
73} 82}
74 83
75@media screen and (max-width: $mobile-view) { 84@media screen and (max-width: $mobile-view) {
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts
index c13cb3748..f83380513 100644
--- a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts
+++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts
@@ -28,8 +28,8 @@ import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@sha
28import { ServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/models' 28import { ServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/models'
29import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type' 29import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
30import { Syndication, Video } from '../shared-main' 30import { Syndication, Video } from '../shared-main'
31import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component'
32import { GenericHeaderComponent, VideoListHeaderComponent } from './video-list-header.component' 31import { GenericHeaderComponent, VideoListHeaderComponent } from './video-list-header.component'
32import { MiniatureDisplayOptions } from './video-miniature.component'
33 33
34enum GroupDate { 34enum GroupDate {
35 UNKNOWN = 0, 35 UNKNOWN = 0,
@@ -65,7 +65,6 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, AfterConte
65 loadOnInit = true 65 loadOnInit = true
66 loadUserVideoPreferences = false 66 loadUserVideoPreferences = false
67 67
68 ownerDisplayType: OwnerDisplayType = 'account'
69 displayModerationBlock = false 68 displayModerationBlock = false
70 titleTooltip: string 69 titleTooltip: string
71 displayVideoActions = true 70 displayVideoActions = true
@@ -320,6 +319,11 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, AfterConte
320 viewContainerRef.createComponent(componentFactory, 0, injector) 319 viewContainerRef.createComponent(componentFactory, 0, injector)
321 } 320 }
322 321
322 // Can be redefined by child
323 displayAsRow () {
324 return false
325 }
326
323 // On videos hook for children that want to do something 327 // On videos hook for children that want to do something
324 protected onMoreVideos () { /* empty */ } 328 protected onMoreVideos () { /* empty */ }
325 329
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.html b/client/src/app/shared/shared-video-miniature/video-download.component.html
index 4608e93e7..8a9218343 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.html
@@ -17,85 +17,116 @@
17 </div> 17 </div>
18 18
19 <div class="modal-body"> 19 <div class="modal-body">
20 <div class="form-group"> 20 <div class="alert alert-warning" *ngIf="isConfidentialVideo()" i18n>
21 <div class="alert alert-warning" *ngIf="isConfidentialVideo()" i18n> 21 The following link contains a private token and should not be shared with anyone.
22 The following link contains a private token and should not be shared with anyone. 22 </div>
23 </div>
24 23
24 <ng-container *ngIf="type === 'subtitles'">
25 <div class="input-group input-group-sm"> 25 <div class="input-group input-group-sm">
26 <div class="input-group-prepend peertube-select-container">
27 <select *ngIf="type === 'video'" [(ngModel)]="resolutionId" (ngModelChange)="onResolutionIdChange()">
28 <option *ngFor="let file of getVideoFiles()" [value]="file.resolution.id">{{ file.resolution.label }}</option>
29 </select>
30
31 <select *ngIf="type === 'subtitles'" [(ngModel)]="subtitleLanguageId">
32 <option *ngFor="let caption of videoCaptions" [value]="caption.language.id">{{ caption.language.label }}</option>
33 </select>
34 </div>
35
36 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" /> 26 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" />
37 <div class="input-group-append" *ngIf="!isConfidentialVideo()"> 27 <div class="input-group-append" *ngIf="!isConfidentialVideo()">
38 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> 28 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary">
39 <span class="glyphicon glyphicon-copy"></span> 29 <span class="glyphicon glyphicon-duplicate"></span>
40 </button> 30 </button>
41 </div> 31 </div>
42 </div> 32 </div>
43 </div> 33 </ng-container>
44 34
45 <ng-container *ngIf="type === 'video' && videoFile?.metadata"> 35 <ng-container *ngIf="type === 'video'">
46 <div ngbNav #nav="ngbNav" class="nav-tabs"> 36 <div ngbNav #resolutionNav="ngbNav" class="nav-tabs" [activeId]="resolutionId" (activeIdChange)="onResolutionIdChange($event)">
47 37
48 <ng-container ngbNavItem> 38 <ng-container *ngFor="let file of getVideoFiles()" [ngbNavItem]="file.resolution.id">
49 <a ngbNavLink i18n>Format</a> 39 <a ngbNavLink i18n>{{ file.resolution.label }}</a>
40
50 <ng-template ngbNavContent> 41 <ng-template ngbNavContent>
51 <div class="file-metadata"> 42 <div class="nav-content">
52 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataFormat | keyvalue"> 43 <div class="input-group input-group-sm">
53 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> 44 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" />
54 <span class="metadata-attribute-value">{{ item.value.value }}</span> 45 <div class="input-group-append" *ngIf="!isConfidentialVideo()">
46 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary">
47 <span class="glyphicon glyphicon-duplicate"></span>
48 </button>
49 </div>
55 </div> 50 </div>
56 </div> 51 </div>
57 </ng-template> 52 </ng-template>
58 </ng-container> 53 </ng-container>
59 54 </div>
60 <ng-container ngbNavItem [disabled]="videoFileMetadataVideoStream === undefined"> 55 <div [ngbNavOutlet]="resolutionNav"></div>
61 <a ngbNavLink i18n>Video stream</a> 56
62 <ng-template ngbNavContent> 57 <div class="advanced-filters collapse-transition" [ngbCollapse]="isAdvancedCustomizationCollapsed">
63 <div class="file-metadata"> 58 <ng-container *ngIf="videoFile?.metadata">
64 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataVideoStream | keyvalue"> 59 <div ngbNav #nav="ngbNav" class="nav-tabs nav-metadata">
65 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> 60 <ng-container ngbNavItem>
66 <span class="metadata-attribute-value">{{ item.value.value }}</span> 61 <a ngbNavLink i18n>Format</a>
67 </div> 62 <ng-template ngbNavContent>
63 <div class="file-metadata">
64 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataFormat | keyvalue">
65 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
66 <span class="metadata-attribute-value">{{ item.value.value }}</span>
67 </div>
68 </div>
69 </ng-template>
70
71 <ng-container ngbNavItem [disabled]="videoFileMetadataVideoStream === undefined">
72 <a ngbNavLink i18n>Video stream</a>
73 <ng-template ngbNavContent>
74 <div class="file-metadata">
75 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataVideoStream | keyvalue">
76 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
77 <span class="metadata-attribute-value">{{ item.value.value }}</span>
78 </div>
79 </div>
80 </ng-template>
81 </ng-container>
82
83 <ng-container ngbNavItem [disabled]="videoFileMetadataAudioStream === undefined">
84 <a ngbNavLink i18n>Audio stream</a>
85 <ng-template ngbNavContent>
86 <div class="file-metadata">
87 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataAudioStream | keyvalue">
88 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
89 <span class="metadata-attribute-value">{{ item.value.value }}</span>
90 </div>
91 </div>
92 </ng-template>
93 </ng-container>
94
95 </ng-container>
96 </div>
97 <div [ngbNavOutlet]="nav"></div>
98 <div class="download-type">
99 <div class="peertube-radio-container">
100 <input type="radio" name="download" id="download-direct" [(ngModel)]="downloadType" value="direct">
101 <label i18n for="download-direct">Direct download</label>
68 </div> 102 </div>
69 </ng-template> 103 <div class="peertube-radio-container">
70 </ng-container> 104 <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent">
71 105 <label i18n for="download-torrent">Torrent (.torrent file)</label>
72 <ng-container ngbNavItem [disabled]="videoFileMetadataAudioStream === undefined">
73 <a ngbNavLink i18n>Audio stream</a>
74 <ng-template ngbNavContent>
75 <div class="file-metadata">
76 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataAudioStream | keyvalue">
77 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
78 <span class="metadata-attribute-value">{{ item.value.value }}</span>
79 </div>
80 </div> 106 </div>
81 </ng-template> 107 </div>
82 </ng-container> 108 </ng-container>
83 </div> 109 </div>
84 110
85 <div [ngbNavOutlet]="nav"></div> 111 <div (click)="isAdvancedCustomizationCollapsed = !isAdvancedCustomizationCollapsed" role="button" class="advanced-filters-button"
86 </ng-container> 112 [attr.aria-expanded]="!isAdvancedCustomizationCollapsed" aria-controls="collapseBasic">
87 113 <ng-container *ngIf="isAdvancedCustomizationCollapsed">
88 <div class="download-type" *ngIf="type === 'video'"> 114 <span class="glyphicon glyphicon-menu-down"></span>
89 <div class="peertube-radio-container"> 115
90 <input type="radio" name="download" id="download-direct" [(ngModel)]="downloadType" value="direct"> 116 <ng-container i18n>
91 <label i18n for="download-direct">Direct download</label> 117 Advanced
92 </div> 118 </ng-container>
93 119 </ng-container>
94 <div class="peertube-radio-container"> 120
95 <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent"> 121 <ng-container *ngIf="!isAdvancedCustomizationCollapsed">
96 <label i18n for="download-torrent">Torrent (.torrent file)</label> 122 <span class="glyphicon glyphicon-menu-up"></span>
123
124 <ng-container i18n>
125 Simple
126 </ng-container>
127 </ng-container>
97 </div> 128 </div>
98 </div> 129 </ng-container>
99 </div> 130 </div>
100 131
101 <div class="modal-footer inputs"> 132 <div class="modal-footer inputs">
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.scss b/client/src/app/shared/shared-video-miniature/video-download.component.scss
index d407e9531..199c3dac8 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.scss
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.scss
@@ -1,6 +1,28 @@
1@import 'variables'; 1@import 'variables';
2@import 'mixins'; 2@import 'mixins';
3 3
4.nav-content {
5 margin-top: 30px;
6}
7
8.advanced-filters-button {
9 display: flex;
10 justify-content: center;
11 align-items: center;
12 margin-top: 20px;
13 font-size: 16px;
14 font-weight: 600;
15 cursor: pointer;
16
17 .nav-tabs {
18 margin-top: 10x;
19 }
20
21 .glyphicon {
22 margin-right: 5px;
23 }
24}
25
4.peertube-select-container { 26.peertube-select-container {
5 @include peertube-select-container(85px); 27 @include peertube-select-container(85px);
6 28
@@ -15,12 +37,21 @@
15 } 37 }
16} 38}
17 39
40.action-button-cancel {
41 @include peertube-button-link;
42}
43
44.action-button-submit {
45 @include peertube-button-link;
46 @include orange-button;
47}
48
18#dropdownDownloadType { 49#dropdownDownloadType {
19 cursor: pointer; 50 cursor: pointer;
20} 51}
21 52
22.download-type { 53.download-type {
23 margin-top: 30px; 54 margin-top: 20px;
24 55
25 .peertube-radio-container { 56 .peertube-radio-container {
26 @include peertube-radio-container; 57 @include peertube-radio-container;
@@ -30,6 +61,10 @@
30 } 61 }
31} 62}
32 63
64.nav-metadata {
65 margin-top: 20px;
66}
67
33.file-metadata { 68.file-metadata {
34 padding: 1rem; 69 padding: 1rem;
35} 70}
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.ts b/client/src/app/shared/shared-video-miniature/video-download.component.ts
index 90f4daf7c..1e3745d94 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.ts
@@ -1,7 +1,9 @@
1import { mapValues, pick } from 'lodash-es' 1import { mapValues, pick } from 'lodash-es'
2import { pipe } from 'rxjs'
3import { tap } from 'rxjs/operators'
2import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' 4import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core'
3import { AuthService, Notifier } from '@app/core' 5import { AuthService, HooksService, Notifier } from '@app/core'
4import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
5import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' 7import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models'
6import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main' 8import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main'
7 9
@@ -16,7 +18,7 @@ type FileMetadata = { [key: string]: { label: string, value: string }}
16export class VideoDownloadComponent { 18export class VideoDownloadComponent {
17 @ViewChild('modal', { static: true }) modal: ElementRef 19 @ViewChild('modal', { static: true }) modal: ElementRef
18 20
19 downloadType: 'direct' | 'torrent' = 'torrent' 21 downloadType: 'direct' | 'torrent' = 'direct'
20 resolutionId: number | string = -1 22 resolutionId: number | string = -1
21 subtitleLanguageId: string 23 subtitleLanguageId: string
22 24
@@ -26,7 +28,9 @@ export class VideoDownloadComponent {
26 videoFileMetadataVideoStream: FileMetadata | undefined 28 videoFileMetadataVideoStream: FileMetadata | undefined
27 videoFileMetadataAudioStream: FileMetadata | undefined 29 videoFileMetadataAudioStream: FileMetadata | undefined
28 videoCaptions: VideoCaption[] 30 videoCaptions: VideoCaption[]
29 activeModal: NgbActiveModal 31 activeModal: NgbModalRef
32
33 isAdvancedCustomizationCollapsed = true
30 34
31 type: DownloadType = 'video' 35 type: DownloadType = 'video'
32 36
@@ -38,7 +42,8 @@ export class VideoDownloadComponent {
38 private notifier: Notifier, 42 private notifier: Notifier,
39 private modalService: NgbModal, 43 private modalService: NgbModal,
40 private videoService: VideoService, 44 private videoService: VideoService,
41 private auth: AuthService 45 private auth: AuthService,
46 private hooks: HooksService
42 ) { 47 ) {
43 this.bytesPipe = new BytesPipe() 48 this.bytesPipe = new BytesPipe()
44 this.numbersPipe = new NumberFormatterPipe(this.localeId) 49 this.numbersPipe = new NumberFormatterPipe(this.localeId)
@@ -62,9 +67,13 @@ export class VideoDownloadComponent {
62 67
63 this.activeModal = this.modalService.open(this.modal, { centered: true }) 68 this.activeModal = this.modalService.open(this.modal, { centered: true })
64 69
65 this.resolutionId = this.getVideoFiles()[0].resolution.id 70 this.onResolutionIdChange(this.getVideoFiles()[0].resolution.id)
66 this.onResolutionIdChange() 71
67 if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id 72 if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id
73
74 this.activeModal.shown.subscribe(() => {
75 this.hooks.runAction('action:modal.video-download.shown', 'common')
76 })
68 } 77 }
69 78
70 onClose () { 79 onClose () {
@@ -83,11 +92,15 @@ export class VideoDownloadComponent {
83 : this.getVideoFileLink() 92 : this.getVideoFileLink()
84 } 93 }
85 94
86 async onResolutionIdChange () { 95 async onResolutionIdChange (resolutionId: number) {
96 this.resolutionId = resolutionId
87 this.videoFile = this.getVideoFile() 97 this.videoFile = this.getVideoFile()
88 if (this.videoFile.metadata || !this.videoFile.metadataUrl) return
89 98
90 await this.hydrateMetadataFromMetadataUrl(this.videoFile) 99 if (!this.videoFile.metadata) {
100 if (!this.videoFile.metadataUrl) return
101
102 await this.hydrateMetadataFromMetadataUrl(this.videoFile)
103 }
91 104
92 this.videoFileMetadataFormat = this.videoFile 105 this.videoFileMetadataFormat = this.videoFile
93 ? this.getMetadataFormat(this.videoFile.metadata.format) 106 ? this.getMetadataFormat(this.videoFile.metadata.format)
@@ -101,9 +114,6 @@ export class VideoDownloadComponent {
101 } 114 }
102 115
103 getVideoFile () { 116 getVideoFile () {
104 // HTML select send us a string, so convert it to a number
105 this.resolutionId = parseInt(this.resolutionId.toString(), 10)
106
107 const file = this.getVideoFiles().find(f => f.resolution.id === this.resolutionId) 117 const file = this.getVideoFiles().find(f => f.resolution.id === this.resolutionId)
108 if (!file) { 118 if (!file) {
109 console.error('Could not find file with resolution %d.', this.resolutionId) 119 console.error('Could not find file with resolution %d.', this.resolutionId)
@@ -201,7 +211,7 @@ export class VideoDownloadComponent {
201 211
202 private hydrateMetadataFromMetadataUrl (file: VideoFile) { 212 private hydrateMetadataFromMetadataUrl (file: VideoFile) {
203 const observable = this.videoService.getVideoFileMetadata(file.metadataUrl) 213 const observable = this.videoService.getVideoFileMetadata(file.metadataUrl)
204 observable.subscribe(res => file.metadata = res) 214 .pipe(tap(res => file.metadata = res))
205 215
206 return observable.toPromise() 216 return observable.toPromise()
207 } 217 }
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
index 7a6df7b64..bac8bcc2d 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
@@ -1,4 +1,4 @@
1<div class="video-miniature" [ngClass]="{ 'display-as-row': displayAsRow, 'fit-width': fitWidth }" (mouseenter)="loadActions()"> 1<div class="video-miniature" [ngClass]="getClasses()" (mouseenter)="loadActions()">
2 <my-video-thumbnail 2 <my-video-thumbnail
3 [video]="video" [nsfw]="isVideoBlur" [videoRouterLink]="videoRouterLink" [videoHref]="videoHref" [videoTarget]="videoTarget" 3 [video]="video" [nsfw]="isVideoBlur" [videoRouterLink]="videoRouterLink" [videoHref]="videoHref" [videoTarget]="videoTarget"
4 [displayWatchLaterPlaylist]="isWatchLaterPlaylistDisplayed()" [inWatchLaterPlaylist]="inWatchLaterPlaylist" (watchLaterClick)="onWatchLaterClick($event)" 4 [displayWatchLaterPlaylist]="isWatchLaterPlaylistDisplayed()" [inWatchLaterPlaylist]="inWatchLaterPlaylist" (watchLaterClick)="onWatchLaterClick($event)"
@@ -9,9 +9,9 @@
9 9
10 <div class="video-bottom"> 10 <div class="video-bottom">
11 <div class="video-miniature-information"> 11 <div class="video-miniature-information">
12 <div class="d-inline-flex video-miniature-meta"> 12 <div class="d-flex video-miniature-meta">
13 <a *ngIf="displayOptions.avatar" class="avatar" [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> 13 <a *ngIf="displayOptions.avatar" class="avatar" [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle">
14 <img [src]="getAvatarUrl()" alt="" /> 14 <img [src]="getAvatarUrl()" alt="" [ngClass]="{ channel: displayOwnerVideoChannel() }" />
15 </a> 15 </a>
16 16
17 <div class="w-100 d-flex flex-column"> 17 <div class="w-100 d-flex flex-column">
@@ -33,7 +33,7 @@
33 </span> 33 </span>
34 </span> 34 </span>
35 35
36 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]"> 36 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/video-channels', video.byVideoChannel ]">
37 {{ video.byAccount }} 37 {{ video.byAccount }}
38 </a> 38 </a>
39 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> 39 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]">
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 38cac5b6e..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
@@ -3,198 +3,205 @@
3@import '_miniature'; 3@import '_miniature';
4 4
5$more-button-width: 40px; 5$more-button-width: 40px;
6$more-margin-right: 15px;
7 6
8.video-miniature { 7.video-miniature-name {
9 display: inline-flex; 8 @include miniature-name;
10 flex-direction: column; 9}
11 padding-bottom: $video-miniature-margin-bottom;
12 vertical-align: top;
13 10
14 .video-bottom { 11.video-miniature-information {
15 display: flex; 12 width: calc(100% - #{$more-button-width});
13}
16 14
17 .video-miniature-information { 15.avatar {
18 width: $video-miniature-width - $more-button-width - $more-margin-right; 16 margin: 10px 10px 0 0;
19 line-height: normal;
20 17
21 .avatar { 18 img:not(.channel) {
22 margin: 10px 10px 0 0; 19 @include avatar(40px);
20 }
23 21
24 img { 22 img.channel {
25 @include avatar(40px); 23 @include channel-avatar(40px);
26 } 24 }
27 } 25}
28 26
29 .video-miniature-name { 27.video-miniature-created-at-views {
30 @include miniature-name; 28 font-size: 13px;
31 width: calc(100% - #{$more-button-width}); 29}
32 }
33 30
34 .video-miniature-meta { 31.video-miniature-account,
35 width: calc(100% + #{$more-button-width}); 32.video-miniature-channel {
36 overflow: hidden; 33 @include disable-default-a-behaviour;
37 } 34 @include ellipsis;
38 35
39 .video-miniature-created-at-views { 36 display: block;
40 display: block; 37 font-size: 13px;
41 font-size: 13px; 38 color: pvar(--greyForegroundColor);
42 }
43 39
44 .video-miniature-account, 40 &:hover {
45 .video-miniature-channel { 41 color: $grey-foreground-hover-color;
46 @include disable-default-a-behaviour; 42 }
47 @include ellipsis; 43}
48 44
49 display: block; 45.video-info-privacy,
50 font-size: 13px; 46.video-info-blocked .blocked-label,
51 color: pvar(--greyForegroundColor); 47.video-info-nsfw {
48 font-weight: $font-semibold;
49}
52 50
53 &:hover { 51.video-info-blocked {
54 color: $grey-foreground-hover-color; 52 color: red;
55 }
56 }
57 53
58 .video-info-privacy, 54 .blocked-reason::before {
59 .video-info-blocked .blocked-label, 55 content: ' - ';
60 .video-info-nsfw { 56 }
61 font-weight: $font-semibold; 57}
62 }
63 58
64 .video-info-blocked { 59.video-info-nsfw {
65 color: red; 60 color: red;
61}
66 62
67 .blocked-reason::before { 63.video-actions {
68 content: ' - '; 64 width: $more-button-width;
69 } 65 height: 30px;
70 }
71 66
72 .video-info-nsfw { 67 ::ng-deep .dropdown-root:not(.show) {
73 color: red; 68 opacity: 0;
74 } 69 }
75 }
76 70
77 .video-actions { 71 ::ng-deep .playlist-dropdown.show + my-action-dropdown .dropdown-root {
78 margin-top: 3px; 72 opacity: 1;
79 width: $more-button-width; 73 }
80 height: 30px;
81 74
82 ::ng-deep .dropdown-root:not(.show) { 75 ::ng-deep .more-icon {
83 opacity: 0; 76 opacity: .6;
84 }
85 77
86 ::ng-deep .playlist-dropdown.show + my-action-dropdown .dropdown-root { 78 &:hover {
87 opacity: 1; 79 opacity: 1;
88 } 80 }
81 }
82}
89 83
90 ::ng-deep .more-icon { 84.video-miniature {
91 opacity: .6; 85 &:hover ::ng-deep .video-thumbnail-actions-overlay,
86 &:hover .video-actions ::ng-deep .dropdown-root {
87 opacity: 1 !important;
88 }
89}
92 90
93 &:hover { 91// Grid mode
94 opacity: 1; 92// Takes all the width on mobile
95 } 93.video-miniature:not(.display-as-row) {
96 } 94 display: flex;
97 } 95 flex-direction: column;
96 padding-bottom: $video-miniature-margin-bottom;
97 width: 100%;
98 98
99 @media screen and (max-width: $small-view) { 99 my-video-thumbnail {
100 .video-miniature-information { 100 @include block-ratio($selector: '::ng-deep .video-thumbnail');
101 margin: 0 10px; 101 }
102 }
103 102
104 .video-actions { 103 .video-bottom {
105 margin: 0; 104 display: flex;
106 top: -3px; 105 width: 100%;
106 }
107 107
108 ::ng-deep .dropdown-root { 108 .video-miniature-name {
109 opacity: 1 !important; 109 margin-top: 10px;
110 } 110 margin-bottom: 5px;
111 }
112 }
113 } 111 }
114 112
115 &:hover ::ng-deep .video-thumbnail .video-thumbnail-actions-overlay, 113 .video-miniature-created-at-views {
116 &:hover .video-bottom .video-actions ::ng-deep .dropdown-root { 114 display: block;
117 opacity: 1;
118 } 115 }
119 116
120 &.fit-width { 117 .video-actions {
118 margin-top: 3px;
119 }
120
121 @media screen and (max-width: $small-view) {
121 width: 100%; 122 width: 100%;
123 margin-bottom: 25px;
124
125 .video-miniature-information {
126 margin: 0 10px;
127
128 width: 100%;
129 text-align: left;
130 }
122 131
123 .video-bottom { 132 .video-actions {
124 width: 100% !important; 133 margin: 0;
134 top: -3px;
125 135
126 .video-miniature-information { 136 ::ng-deep .dropdown-root {
127 width: calc(100% - #{$more-button-width}) !important; 137 opacity: 1 !important;
128 } 138 }
129 } 139 }
130 140
131 my-video-thumbnail { 141 ::ng-deep .video-thumbnail {
132 @include large-screen-ratio($selector: '::ng-deep .video-thumbnail'); 142 border-radius: 0;
133 } 143 }
134 } 144 }
145}
146
147.video-miniature.display-as-row {
148 --rowThumbnailWidth: #{$video-thumbnail-width};
149 --rowThumbnailHeight: #{$video-thumbnail-height};
150
151 display: flex;
152 flex-direction: row;
135 153
136 &.display-as-row { 154 .video-bottom {
137 flex-direction: row;
138 padding-bottom: 0;
139 height: auto;
140 display: flex; 155 display: flex;
141 flex-grow: 1; 156 }
142 157
143 my-video-thumbnail { 158 // We don't display avatar in row mode
144 margin-right: 10px; 159 .avatar {
145 } 160 display: none;
161 }
146 162
147 .video-bottom { 163 my-video-thumbnail {
148 .video-miniature-information { 164 min-width: var(--rowThumbnailWidth);
149 @media screen and (min-width: $small-view) { 165 max-width: var(--rowThumbnailWidth);
150 width: auto; 166 height: var(--rowThumbnailHeight);
151 min-width: 500px; 167 margin-right: 10px;
152 } 168 }
153
154 .video-miniature-name {
155 @include ellipsis-multiline(1.3em, 2);
156
157 margin-top: 2px;
158 margin-bottom: 5px;
159 }
160
161 .video-miniature-created-at-views,
162 .video-miniature-account,
163 .video-miniature-channel {
164 font-size: 95%;
165 width: fit-content;
166 }
167
168 .video-miniature-created-at-views + .video-miniature-channel {
169 margin-top: 5px;
170 }
171
172 .video-info-privacy {
173 margin-top: 5px;
174 }
175
176 .video-info-blocked {
177 margin-top: 3px;
178 }
179 }
180 169
181 .video-actions { 170 .video-miniature-name {
182 margin: 0; 171 @include ellipsis-multiline($video-miniature-row-name-font-size, 2);
183 top: -3px; 172 }
184 } 173
185 } 174 .video-miniature-created-at-views,
175 .video-miniature-account,
176 .video-miniature-channel {
177 font-size: $video-miniature-row-info-font-size;
178 }
186 179
187 @media screen and (max-width: $small-view) { 180 .video-actions {
188 flex-direction: column; 181 margin-top: -3px;
189 height: auto; 182 }
183}
190 184
191 my-video-thumbnail { 185@include on-small-main-col {
192 margin-right: 0; 186 .video-miniature.display-as-row {
193 } 187 --rowThumbnailWidth: #{$video-thumbnail-medium-width};
188 --rowThumbnailHeight: #{$video-thumbnail-medium-height};
189 }
190}
194 191
195 .video-miniature-information { 192@include on-mobile-main-col {
196 min-width: initial; 193 .video-miniature.display-as-row {
197 } 194 --rowThumbnailWidth: #{$video-thumbnail-small-width};
195 --rowThumbnailHeight: #{$video-thumbnail-small-height};
196
197 .video-miniature-name {
198 font-size: $video-miniature-row-info-font-size;
199 }
200
201 .video-miniature-created-at-views,
202 .video-miniature-account,
203 .video-miniature-channel {
204 font-size: $video-miniature-row-mobile-info-font-size;
198 } 205 }
199 } 206 }
200} 207}
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
index cc5665ab1..48da92d6b 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
@@ -16,7 +16,6 @@ import { Video } from '../shared-main'
16import { VideoPlaylistService } from '../shared-video-playlist' 16import { VideoPlaylistService } from '../shared-video-playlist'
17import { VideoActionsDisplayType } from './video-actions-dropdown.component' 17import { VideoActionsDisplayType } from './video-actions-dropdown.component'
18 18
19export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto'
20export type MiniatureDisplayOptions = { 19export type MiniatureDisplayOptions = {
21 date?: boolean 20 date?: boolean
22 views?: boolean 21 views?: boolean
@@ -40,7 +39,6 @@ export class VideoMiniatureComponent implements OnInit {
40 @Input() user: User 39 @Input() user: User
41 @Input() video: Video 40 @Input() video: Video
42 41
43 @Input() ownerDisplayType: OwnerDisplayType = 'account'
44 @Input() displayOptions: MiniatureDisplayOptions = { 42 @Input() displayOptions: MiniatureDisplayOptions = {
45 date: true, 43 date: true,
46 views: true, 44 views: true,
@@ -51,9 +49,9 @@ export class VideoMiniatureComponent implements OnInit {
51 state: false, 49 state: false,
52 blacklistInfo: false 50 blacklistInfo: false
53 } 51 }
54 @Input() displayAsRow = false
55 @Input() displayVideoActions = true 52 @Input() displayVideoActions = true
56 @Input() fitWidth = false 53
54 @Input() displayAsRow = false
57 55
58 @Input() videoLinkType: VideoLinkType = 'internal' 56 @Input() videoLinkType: VideoLinkType = 'internal'
59 57
@@ -89,7 +87,7 @@ export class VideoMiniatureComponent implements OnInit {
89 videoHref: string 87 videoHref: string
90 videoTarget: string 88 videoTarget: string
91 89
92 private ownerDisplayTypeChosen: 'account' | 'videoChannel' 90 private ownerDisplayType: 'account' | 'videoChannel'
93 91
94 constructor ( 92 constructor (
95 private screenService: ScreenService, 93 private screenService: ScreenService,
@@ -140,11 +138,11 @@ export class VideoMiniatureComponent implements OnInit {
140 } 138 }
141 139
142 displayOwnerAccount () { 140 displayOwnerAccount () {
143 return this.ownerDisplayTypeChosen === 'account' 141 return this.ownerDisplayType === 'account'
144 } 142 }
145 143
146 displayOwnerVideoChannel () { 144 displayOwnerVideoChannel () {
147 return this.ownerDisplayTypeChosen === 'videoChannel' 145 return this.ownerDisplayType === 'videoChannel'
148 } 146 }
149 147
150 isUnlistedVideo () { 148 isUnlistedVideo () {
@@ -183,7 +181,7 @@ export class VideoMiniatureComponent implements OnInit {
183 } 181 }
184 182
185 getAvatarUrl () { 183 getAvatarUrl () {
186 if (this.ownerDisplayTypeChosen === 'account') { 184 if (this.displayOwnerAccount()) {
187 return this.video.accountAvatarUrl 185 return this.video.accountAvatarUrl
188 } 186 }
189 187
@@ -244,21 +242,26 @@ export class VideoMiniatureComponent implements OnInit {
244 return this.displayVideoActions && this.isUserLoggedIn() && this.inWatchLaterPlaylist !== undefined 242 return this.displayVideoActions && this.isUserLoggedIn() && this.inWatchLaterPlaylist !== undefined
245 } 243 }
246 244
247 private setUpBy () { 245 getClasses () {
248 if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') { 246 return {
249 this.ownerDisplayTypeChosen = this.ownerDisplayType 247 'display-as-row': this.displayAsRow
250 return
251 } 248 }
249 }
250
251 private setUpBy () {
252 const accountName = this.video.account.name
252 253
253 // If the video channel name an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12) 254 // If the video channel name is an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12)
255 // Or has not been customized (default created channel display name)
254 // -> Use the account name 256 // -> Use the account name
255 if ( 257 if (
256 this.video.channel.name === `${this.video.account.name}_channel` || 258 this.video.channel.displayName === `Default ${accountName} channel` ||
259 this.video.channel.displayName === `Main ${accountName} channel` ||
257 this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) 260 this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
258 ) { 261 ) {
259 this.ownerDisplayTypeChosen = 'account' 262 this.ownerDisplayType = 'account'
260 } else { 263 } else {
261 this.ownerDisplayTypeChosen = 'videoChannel' 264 this.ownerDisplayType = 'videoChannel'
262 } 265 }
263 } 266 }
264 267
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.html b/client/src/app/shared/shared-video-miniature/videos-selection.component.html
index 8caeaf092..dec9e99f3 100644
--- a/client/src/app/shared/shared-video-miniature/videos-selection.component.html
+++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.html
@@ -9,8 +9,7 @@
9 9
10 <my-video-miniature 10 <my-video-miniature
11 [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions" 11 [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions"
12 [displayVideoActions]="false" [ownerDisplayType]="ownerDisplayType" 12 [displayVideoActions]="false" [user]="user"
13 [user]="user"
14 ></my-video-miniature> 13 ></my-video-miniature>
15 14
16 <!-- Display only once --> 15 <!-- Display only once -->
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.scss b/client/src/app/shared/shared-video-miniature/videos-selection.component.scss
index c33e11889..a2939d521 100644
--- a/client/src/app/shared/shared-video-miniature/videos-selection.component.scss
+++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.scss
@@ -5,24 +5,24 @@
5 display: flex; 5 display: flex;
6 justify-content: flex-end; 6 justify-content: flex-end;
7 flex-grow: 1; 7 flex-grow: 1;
8}
8 9
9 .action-selection-mode-child { 10.action-selection-mode-child {
10 position: fixed; 11 position: fixed;
11
12 .action-button {
13 display: block;
14 margin-left: 55px;
15 }
16 12
17 .action-button-cancel-selection { 13 .action-button {
18 @include peertube-button; 14 display: block;
19 @include grey-button; 15 margin-left: 55px;
20 }
21 } 16 }
22} 17}
23 18
19.action-button-cancel-selection {
20 @include peertube-button;
21 @include grey-button;
22}
23
24.video { 24.video {
25 @include row-blocks; 25 @include row-blocks($column-responsive: false);
26 26
27 &:first-child { 27 &:first-child {
28 margin-top: 47px; 28 margin-top: 47px;
@@ -40,18 +40,16 @@
40 } 40 }
41} 41}
42 42
43@media screen and (max-width: $small-view) {
44 .video {
45 flex-direction: column;
46 height: auto;
47 43
48 .checkbox-container { 44@include on-small-main-col {
49 display: none; 45 .video {
50 } 46 flex-wrap: wrap;
47 }
48}
51 49
52 my-button { 50@include on-mobile-main-col {
53 margin-top: 10px; 51 .checkbox-container {
54 } 52 display: none;
55 } 53 }
56 54
57 .action-selection-mode { 55 .action-selection-mode {
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
index ca1cf2264..f8c3800d7 100644
--- a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
+++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
@@ -17,7 +17,7 @@ import { AuthService, ComponentPagination, LocalStorageService, Notifier, Screen
17import { ResultList, VideoSortField } from '@shared/models' 17import { ResultList, VideoSortField } from '@shared/models'
18import { PeerTubeTemplateDirective, Video } from '../shared-main' 18import { PeerTubeTemplateDirective, Video } from '../shared-main'
19import { AbstractVideoList } from './abstract-video-list' 19import { AbstractVideoList } from './abstract-video-list'
20import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component' 20import { MiniatureDisplayOptions } from './video-miniature.component'
21 21
22export type SelectionType = { [ id: number ]: boolean } 22export type SelectionType = { [ id: number ]: boolean }
23 23
@@ -31,7 +31,6 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni
31 @Input() pagination: ComponentPagination 31 @Input() pagination: ComponentPagination
32 @Input() titlePage: string 32 @Input() titlePage: string
33 @Input() miniatureDisplayOptions: MiniatureDisplayOptions 33 @Input() miniatureDisplayOptions: MiniatureDisplayOptions
34 @Input() ownerDisplayType: OwnerDisplayType
35 34
36 @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>> 35 @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>>
37 36
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
index 86f6664cb..f50f95003 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
@@ -1,4 +1,4 @@
1<div class="miniature" [ngClass]="{ 'no-videos': playlist.videosLength === 0, 'to-manage': toManage }"> 1<div class="miniature" [ngClass]="{ 'no-videos': playlist.videosLength === 0, 'to-manage': toManage, 'display-as-row': displayAsRow }">
2 <a 2 <a
3 [routerLink]="getPlaylistUrl()" [attr.title]="playlist.description" 3 [routerLink]="getPlaylistUrl()" [attr.title]="playlist.description"
4 class="miniature-thumbnail" 4 class="miniature-thumbnail"
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss
index 1b16dbb01..c5be5f292 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss
@@ -4,6 +4,7 @@
4 4
5.miniature { 5.miniature {
6 display: inline-block; 6 display: inline-block;
7 width: 100%;
7 8
8 &.no-videos:not(.to-manage){ 9 &.no-videos:not(.to-manage){
9 a { 10 a {
@@ -17,62 +18,92 @@
17 display: none; 18 display: none;
18 } 19 }
19 } 20 }
21}
20 22
21 .miniature-thumbnail { 23.miniature-thumbnail {
22 @include miniature-thumbnail; 24 @include miniature-thumbnail;
23 25
24 .miniature-playlist-info-overlay { 26 .miniature-playlist-info-overlay {
25 @include static-thumbnail-overlay; 27 @include static-thumbnail-overlay;
26 28
27 position: absolute; 29 position: absolute;
28 right: 0; 30 right: 0;
29 bottom: 0; 31 bottom: 0;
30 height: $video-thumbnail-height; 32 height: 100%;
31 padding: 0 10px; 33 padding: 0 10px;
32 display: flex; 34 display: flex;
33 align-items: center; 35 align-items: center;
34 font-size: 14px; 36 font-size: 14px;
35 font-weight: $font-semibold; 37 font-weight: $font-semibold;
36 }
37 } 38 }
39}
38 40
39 .miniature-info { 41.miniature-info {
40 width: 200px;
41 margin-top: 2px;
42 line-height: normal;
43
44 .miniature-name {
45 @include miniature-name;
46 42
47 @include ellipsis-multiline(1.3em, 2); 43 .miniature-name {
44 @include miniature-name;
45 @include ellipsis-multiline(1.3em, 2);
48 46
49 margin: 0; 47 margin: 0;
50 } 48 }
51 49
52 .by { 50 .by {
53 @include disable-default-a-behaviour; 51 @include disable-default-a-behaviour;
54 52
55 display: block; 53 display: block;
56 color: pvar(--greyForegroundColor); 54 color: pvar(--greyForegroundColor);
57 } 55 }
58 56
59 .privacy-date { 57 .privacy-date {
60 margin-top: 5px; 58 margin-top: 5px;
61 59
62 .video-info-privacy { 60 .video-info-privacy {
63 font-size: 14px; 61 font-size: 14px;
64 font-weight: $font-semibold; 62 font-weight: $font-semibold;
65 63
66 &::after { 64 &::after {
67 content: '-'; 65 content: '-';
68 margin: 0 3px; 66 margin: 0 3px;
69 }
70 } 67 }
71 } 68 }
69 }
72 70
73 .video-info-description { 71 .video-info-description {
74 margin-top: 10px; 72 margin-top: 10px;
75 color: pvar(--greyForegroundColor); 73 color: pvar(--greyForegroundColor);
76 } 74 }
75}
76
77.miniature:not(.display-as-row) {
78 .miniature-thumbnail {
79 margin-top: 10px;
80 margin-bottom: 5px;
81 }
82}
83
84.miniature.display-as-row {
85 --rowThumbnailWidth: #{$video-thumbnail-width};
86 --rowThumbnailHeight: #{$video-thumbnail-height};
87
88 display: flex;
89
90 .miniature-thumbnail {
91 width: var(--rowThumbnailWidth);
92 height: var(--rowThumbnailHeight);
93 margin-right: 10px;
94 }
95}
96
97@include on-small-main-col {
98 .miniature.display-as-row {
99 --rowThumbnailWidth: #{$video-thumbnail-medium-width};
100 --rowThumbnailHeight: #{$video-thumbnail-medium-height};
101 }
102}
103
104@include on-mobile-main-col {
105 .miniature.display-as-row {
106 --rowThumbnailWidth: #{$video-thumbnail-small-width};
107 --rowThumbnailHeight: #{$video-thumbnail-small-height};
77 } 108 }
78} 109}
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
index 251aa868a..6b0b1056f 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
@@ -12,6 +12,7 @@ export class VideoPlaylistMiniatureComponent {
12 @Input() displayChannel = false 12 @Input() displayChannel = false
13 @Input() displayDescription = false 13 @Input() displayDescription = false
14 @Input() displayPrivacy = false 14 @Input() displayPrivacy = false
15 @Input() displayAsRow = false
15 16
16 getPlaylistUrl () { 17 getPlaylistUrl () {
17 if (this.toManage) return [ '/my-library/video-playlists', this.playlist.uuid ] 18 if (this.toManage) return [ '/my-library/video-playlists', this.playlist.uuid ]