aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-04-05 10:52:27 +0200
committerChocobozzz <me@florianbigard.com>2019-04-05 10:53:09 +0200
commit3a0fb65c61f80b510bce979a45d59d17948745e8 (patch)
treec1be0b2158a008fea826835c8d2fd4e8d648bae9
parent693263e936763a851e3c8c020e3739def8bd4eca (diff)
downloadPeerTube-3a0fb65c61f80b510bce979a45d59d17948745e8.tar.gz
PeerTube-3a0fb65c61f80b510bce979a45d59d17948745e8.tar.zst
PeerTube-3a0fb65c61f80b510bce979a45d59d17948745e8.zip
Add video miniature dropdown
-rw-r--r--client/src/app/+my-account/my-account-history/my-account-history.component.html4
-rw-r--r--client/src/app/search/search.component.html5
-rw-r--r--client/src/app/search/search.component.ts6
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.html21
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.scss25
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.ts25
-rw-r--r--client/src/app/shared/misc/screen.service.ts2
-rw-r--r--client/src/app/shared/shared.module.ts19
-rw-r--r--client/src/app/shared/video-playlist/video-add-to-playlist.component.html112
-rw-r--r--client/src/app/shared/video-playlist/video-add-to-playlist.component.scss5
-rw-r--r--client/src/app/shared/video-playlist/video-add-to-playlist.component.ts5
-rw-r--r--client/src/app/shared/video/abstract-video-list.html10
-rw-r--r--client/src/app/shared/video/abstract-video-list.ts6
-rw-r--r--client/src/app/shared/video/modals/video-blacklist.component.html (renamed from client/src/app/videos/+video-watch/modal/video-blacklist.component.html)0
-rw-r--r--client/src/app/shared/video/modals/video-blacklist.component.scss (renamed from client/src/app/videos/+video-watch/modal/video-blacklist.component.scss)0
-rw-r--r--client/src/app/shared/video/modals/video-blacklist.component.ts (renamed from client/src/app/videos/+video-watch/modal/video-blacklist.component.ts)13
-rw-r--r--client/src/app/shared/video/modals/video-download.component.html (renamed from client/src/app/videos/+video-watch/modal/video-download.component.html)0
-rw-r--r--client/src/app/shared/video/modals/video-download.component.scss (renamed from client/src/app/videos/+video-watch/modal/video-download.component.scss)0
-rw-r--r--client/src/app/shared/video/modals/video-download.component.ts (renamed from client/src/app/videos/+video-watch/modal/video-download.component.ts)43
-rw-r--r--client/src/app/shared/video/modals/video-report.component.html (renamed from client/src/app/videos/+video-watch/modal/video-report.component.html)0
-rw-r--r--client/src/app/shared/video/modals/video-report.component.scss (renamed from client/src/app/videos/+video-watch/modal/video-report.component.scss)0
-rw-r--r--client/src/app/shared/video/modals/video-report.component.ts (renamed from client/src/app/videos/+video-watch/modal/video-report.component.ts)3
-rw-r--r--client/src/app/shared/video/video-actions-dropdown.component.html21
-rw-r--r--client/src/app/shared/video/video-actions-dropdown.component.scss12
-rw-r--r--client/src/app/shared/video/video-actions-dropdown.component.ts237
-rw-r--r--client/src/app/shared/video/video-details.model.ts16
-rw-r--r--client/src/app/shared/video/video-miniature.component.html89
-rw-r--r--client/src/app/shared/video/video-miniature.component.scss36
-rw-r--r--client/src/app/shared/video/video-miniature.component.ts70
-rw-r--r--client/src/app/shared/video/video.model.ts19
-rw-r--r--client/src/app/shared/video/videos-selection.component.html2
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html37
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss12
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts75
-rw-r--r--client/src/app/videos/+video-watch/video-watch.module.ts8
-rw-r--r--client/src/app/videos/video-list/video-overview.component.html6
36 files changed, 643 insertions, 301 deletions
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.html b/client/src/app/+my-account/my-account-history/my-account-history.component.html
index 4b94490a0..6e274f689 100644
--- a/client/src/app/+my-account/my-account-history/my-account-history.component.html
+++ b/client/src/app/+my-account/my-account-history/my-account-history.component.html
@@ -15,6 +15,8 @@
15 15
16<div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" class="videos"> 16<div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" class="videos">
17 <div class="video" *ngFor="let video of videos"> 17 <div class="video" *ngFor="let video of videos">
18 <my-video-miniature [video]="video" [displayAsRow]="true"></my-video-miniature> 18 <my-video-miniature
19 [video]="video" [displayAsRow]="true"
20 (videoRemoved)="removeVideoFromArray(video)" (videoBlacklisted)="removeVideoFromArray(video)"></my-video-miniature>
19 </div> 21 </div>
20</div> 22</div>
diff --git a/client/src/app/search/search.component.html b/client/src/app/search/search.component.html
index da2ace54d..0a9f78cb2 100644
--- a/client/src/app/search/search.component.html
+++ b/client/src/app/search/search.component.html
@@ -48,7 +48,10 @@
48 </div> 48 </div>
49 49
50 <div *ngIf="isVideo(result)" class="entry video"> 50 <div *ngIf="isVideo(result)" class="entry video">
51 <my-video-miniature [video]="result" [user]="user" [displayAsRow]="true"></my-video-miniature> 51 <my-video-miniature
52 [video]="result" [user]="user" [displayAsRow]="true"
53 (videoBlacklisted)="removeVideoFromArray(result)" (videoRemoved)="removeVideoFromArray(result)"
54 ></my-video-miniature>
52 </div> 55 </div>
53 </ng-container> 56 </ng-container>
54 57
diff --git a/client/src/app/search/search.component.ts b/client/src/app/search/search.component.ts
index a3383ed8a..a7ddbe1f8 100644
--- a/client/src/app/search/search.component.ts
+++ b/client/src/app/search/search.component.ts
@@ -1,6 +1,6 @@
1import { Component, OnDestroy, OnInit } from '@angular/core' 1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { AuthService, Notifier, ServerService } from '@app/core' 3import { AuthService, Notifier } from '@app/core'
4import { forkJoin, Subscription } from 'rxjs' 4import { forkJoin, Subscription } from 'rxjs'
5import { SearchService } from '@app/search/search.service' 5import { SearchService } from '@app/search/search.service'
6import { ComponentPagination } from '@app/shared/rest/component-pagination.model' 6import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
@@ -138,6 +138,10 @@ export class SearchComponent implements OnInit, OnDestroy {
138 return this.advancedSearch.size() 138 return this.advancedSearch.size()
139 } 139 }
140 140
141 removeVideoFromArray (video: Video) {
142 this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id)
143 }
144
141 private resetPagination () { 145 private resetPagination () {
142 this.pagination.currentPage = 1 146 this.pagination.currentPage = 1
143 this.pagination.totalItems = null 147 this.pagination.totalItems = null
diff --git a/client/src/app/shared/buttons/action-dropdown.component.html b/client/src/app/shared/buttons/action-dropdown.component.html
index 6999474d6..cc244dc76 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.html
+++ b/client/src/app/shared/buttons/action-dropdown.component.html
@@ -1,9 +1,11 @@
1<div class="dropdown-root" ngbDropdown [placement]="placement"> 1<div class="dropdown-root" ngbDropdown [placement]="placement">
2 <div 2 <div
3 class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange' }" 3 class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange', 'button-styled': buttonStyled }"
4 ngbDropdownToggle role="button" 4 ngbDropdownToggle role="button"
5 > 5 >
6 <my-global-icon *ngIf="!label" class="more-icon" iconName="more-horizontal"></my-global-icon> 6 <my-global-icon *ngIf="!label && buttonDirection === 'horizontal'" class="more-icon" iconName="more-horizontal"></my-global-icon>
7 <my-global-icon *ngIf="!label && buttonDirection === 'vertical'" class="more-icon" iconName="more-vertical"></my-global-icon>
8
7 <span *ngIf="label" class="dropdown-toggle">{{ label }}</span> 9 <span *ngIf="label" class="dropdown-toggle">{{ label }}</span>
8 </div> 10 </div>
9 11
@@ -12,15 +14,24 @@
12 14
13 <ng-container *ngFor="let action of actions"> 15 <ng-container *ngFor="let action of actions">
14 <ng-container *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true"> 16 <ng-container *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true">
15 <a *ngIf="action.linkBuilder" class="dropdown-item" [routerLink]="action.linkBuilder(entry)">{{ action.label }}</a>
16 17
17 <span *ngIf="!action.linkBuilder" class="custom-action dropdown-item" (click)="action.handler(entry)" role="button"> 18 <a *ngIf="action.linkBuilder" [ngClass]="{ 'with-icon': !!action.iconName }" class="dropdown-item" [routerLink]="action.linkBuilder(entry)">
19 <my-global-icon *ngIf="action.iconName" [iconName]="action.iconName" [ngClass]="'icon-' + action.iconName"></my-global-icon>
20 {{ action.label }}
21 </a>
22
23 <span
24 *ngIf="!action.linkBuilder" [ngClass]="{ 'with-icon': !!action.iconName }" (click)="action.handler(entry)"
25 class="custom-action dropdown-item" role="button"
26 >
27 <my-global-icon *ngIf="action.iconName" [iconName]="action.iconName" [ngClass]="'icon-' + action.iconName"></my-global-icon>
18 {{ action.label }} 28 {{ action.label }}
19 </span> 29 </span>
30
20 </ng-container> 31 </ng-container>
21 </ng-container> 32 </ng-container>
22 33
23 <div class="dropdown-divider"></div> 34 <div *ngIf="areActionsDisplayed(actions, entry)" class="dropdown-divider"></div>
24 35
25 </ng-container> 36 </ng-container>
26 </div> 37 </div>
diff --git a/client/src/app/shared/buttons/action-dropdown.component.scss b/client/src/app/shared/buttons/action-dropdown.component.scss
index 985b2ca88..5073190b0 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.scss
+++ b/client/src/app/shared/buttons/action-dropdown.component.scss
@@ -8,12 +8,19 @@
8.action-button { 8.action-button {
9 @include peertube-button; 9 @include peertube-button;
10 10
11 &.grey { 11 &.button-styled {
12 @include grey-button; 12
13 } 13 &.grey {
14 @include grey-button;
15 }
16
17 &.orange {
18 @include orange-button;
19 }
14 20
15 &.orange { 21 &:hover, &:active, &:focus {
16 @include orange-button; 22 background-color: $grey-background-color;
23 }
17 } 24 }
18 25
19 display: inline-block; 26 display: inline-block;
@@ -23,10 +30,6 @@
23 display: none; 30 display: none;
24 } 31 }
25 32
26 &:hover, &:active, &:focus {
27 background-color: $grey-background-color;
28 }
29
30 .more-icon { 33 .more-icon {
31 width: 21px; 34 width: 21px;
32 } 35 }
@@ -48,6 +51,10 @@
48 cursor: pointer; 51 cursor: pointer;
49 color: #000 !important; 52 color: #000 !important;
50 53
54 &.with-icon {
55 @include dropdown-with-icon-item;
56 }
57
51 a, span { 58 a, span {
52 display: block; 59 display: block;
53 width: 100%; 60 width: 100%;
diff --git a/client/src/app/shared/buttons/action-dropdown.component.ts b/client/src/app/shared/buttons/action-dropdown.component.ts
index 275e2b51e..f5345831b 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.ts
+++ b/client/src/app/shared/buttons/action-dropdown.component.ts
@@ -1,12 +1,18 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { GlobalIconName } from '@app/shared/images/global-icon.component'
2 3
3export type DropdownAction<T> = { 4export type DropdownAction<T> = {
4 label?: string 5 label?: string
6 iconName?: GlobalIconName
5 handler?: (a: T) => any 7 handler?: (a: T) => any
6 linkBuilder?: (a: T) => (string | number)[] 8 linkBuilder?: (a: T) => (string | number)[]
7 isDisplayed?: (a: T) => boolean 9 isDisplayed?: (a: T) => boolean
8} 10}
9 11
12export type DropdownButtonSize = 'normal' | 'small'
13export type DropdownTheme = 'orange' | 'grey'
14export type DropdownDirection = 'horizontal' | 'vertical'
15
10@Component({ 16@Component({
11 selector: 'my-action-dropdown', 17 selector: 'my-action-dropdown',
12 styleUrls: [ './action-dropdown.component.scss' ], 18 styleUrls: [ './action-dropdown.component.scss' ],
@@ -16,14 +22,29 @@ export type DropdownAction<T> = {
16export class ActionDropdownComponent<T> { 22export class ActionDropdownComponent<T> {
17 @Input() actions: DropdownAction<T>[] | DropdownAction<T>[][] = [] 23 @Input() actions: DropdownAction<T>[] | DropdownAction<T>[][] = []
18 @Input() entry: T 24 @Input() entry: T
25
19 @Input() placement = 'bottom-left' 26 @Input() placement = 'bottom-left'
20 @Input() buttonSize: 'normal' | 'small' = 'normal' 27
28 @Input() buttonSize: DropdownButtonSize = 'normal'
29 @Input() buttonDirection: DropdownDirection = 'horizontal'
30 @Input() buttonStyled = true
31
21 @Input() label: string 32 @Input() label: string
22 @Input() theme: 'orange' | 'grey' = 'grey' 33 @Input() theme: DropdownTheme = 'grey'
23 34
24 getActions () { 35 getActions () {
25 if (this.actions.length !== 0 && Array.isArray(this.actions[0])) return this.actions 36 if (this.actions.length !== 0 && Array.isArray(this.actions[0])) return this.actions
26 37
27 return [ this.actions ] 38 return [ this.actions ]
28 } 39 }
40
41 areActionsDisplayed (actions: DropdownAction<T>[], entry: T) {
42 return actions.some(a => a.isDisplayed === undefined || a.isDisplayed(entry))
43 }
44
45 handleClick (event: Event, action: DropdownAction<T>) {
46 event.preventDefault()
47
48 // action.handler(entry)
49 }
29} 50}
diff --git a/client/src/app/shared/misc/screen.service.ts b/client/src/app/shared/misc/screen.service.ts
index 1cbc96b14..db481204e 100644
--- a/client/src/app/shared/misc/screen.service.ts
+++ b/client/src/app/shared/misc/screen.service.ts
@@ -32,6 +32,8 @@ export class ScreenService {
32 } 32 }
33 33
34 private cacheWindowInnerWidthExpired () { 34 private cacheWindowInnerWidthExpired () {
35 if (!this.lastFunctionCallTime) return true
36
35 return new Date().getTime() > (this.lastFunctionCallTime + this.cacheForMs) 37 return new Date().getTime() > (this.lastFunctionCallTime + this.cacheForMs)
36 } 38 }
37} 39}
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index 68225b457..ded65653f 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -80,6 +80,11 @@ import { NumberFormatterPipe } from '@app/shared/angular/number-formatter.pipe'
80import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe' 80import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe'
81import { FromNowPipe } from '@app/shared/angular/from-now.pipe' 81import { FromNowPipe } from '@app/shared/angular/from-now.pipe'
82import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive' 82import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive'
83import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component'
84import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component'
85import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component'
86import { VideoReportComponent } from '@app/shared/video/modals/video-report.component'
87import { ClipboardModule } from 'ngx-clipboard'
83 88
84@NgModule({ 89@NgModule({
85 imports: [ 90 imports: [
@@ -95,6 +100,8 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template
95 NgbTabsetModule, 100 NgbTabsetModule,
96 NgbTooltipModule, 101 NgbTooltipModule,
97 102
103 ClipboardModule,
104
98 PrimeSharedModule, 105 PrimeSharedModule,
99 InputMaskModule, 106 InputMaskModule,
100 NgPipesModule 107 NgPipesModule
@@ -110,6 +117,11 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template
110 VideoAddToPlaylistComponent, 117 VideoAddToPlaylistComponent,
111 VideoPlaylistElementMiniatureComponent, 118 VideoPlaylistElementMiniatureComponent,
112 VideosSelectionComponent, 119 VideosSelectionComponent,
120 VideoActionsDropdownComponent,
121
122 VideoDownloadComponent,
123 VideoReportComponent,
124 VideoBlacklistComponent,
113 125
114 FeedComponent, 126 FeedComponent,
115 127
@@ -158,6 +170,8 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template
158 NgbTabsetModule, 170 NgbTabsetModule,
159 NgbTooltipModule, 171 NgbTooltipModule,
160 172
173 ClipboardModule,
174
161 PrimeSharedModule, 175 PrimeSharedModule,
162 InputMaskModule, 176 InputMaskModule,
163 BytesPipe, 177 BytesPipe,
@@ -172,6 +186,11 @@ import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template
172 VideoAddToPlaylistComponent, 186 VideoAddToPlaylistComponent,
173 VideoPlaylistElementMiniatureComponent, 187 VideoPlaylistElementMiniatureComponent,
174 VideosSelectionComponent, 188 VideosSelectionComponent,
189 VideoActionsDropdownComponent,
190
191 VideoDownloadComponent,
192 VideoReportComponent,
193 VideoBlacklistComponent,
175 194
176 FeedComponent, 195 FeedComponent,
177 196
diff --git a/client/src/app/shared/video-playlist/video-add-to-playlist.component.html b/client/src/app/shared/video-playlist/video-add-to-playlist.component.html
index 19b326206..6029b3648 100644
--- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.html
+++ b/client/src/app/shared/video-playlist/video-add-to-playlist.component.html
@@ -1,74 +1,76 @@
1<div class="header"> 1<div class="root">
2 <div class="first-row"> 2 <div class="header">
3 <div i18n class="title">Save to</div> 3 <div class="first-row">
4 <div i18n class="title">Save to</div>
4 5
5 <div class="options" (click)="displayOptions = !displayOptions"> 6 <div class="options" (click)="displayOptions = !displayOptions">
6 <my-global-icon iconName="cog"></my-global-icon> 7 <my-global-icon iconName="cog"></my-global-icon>
7 8
8 <span i18n>Options</span> 9 <span i18n>Options</span>
10 </div>
9 </div> 11 </div>
10 </div>
11 12
12 <div class="options-row" *ngIf="displayOptions"> 13 <div class="options-row" *ngIf="displayOptions">
13 <div> 14 <div>
14 <my-peertube-checkbox 15 <my-peertube-checkbox
15 inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled" 16 inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled"
16 i18n-labelText labelText="Start at" 17 i18n-labelText labelText="Start at"
17 ></my-peertube-checkbox> 18 ></my-peertube-checkbox>
18 19
19 <my-timestamp-input 20 <my-timestamp-input
20 [timestamp]="timestampOptions.startTimestamp" 21 [timestamp]="timestampOptions.startTimestamp"
21 [maxTimestamp]="video.duration" 22 [maxTimestamp]="video.duration"
22 [disabled]="!timestampOptions.startTimestampEnabled" 23 [disabled]="!timestampOptions.startTimestampEnabled"
23 [(ngModel)]="timestampOptions.startTimestamp" 24 [(ngModel)]="timestampOptions.startTimestamp"
24 ></my-timestamp-input> 25 ></my-timestamp-input>
25 </div> 26 </div>
26 27
27 <div> 28 <div>
28 <my-peertube-checkbox 29 <my-peertube-checkbox
29 inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled" 30 inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled"
30 i18n-labelText labelText="Stop at" 31 i18n-labelText labelText="Stop at"
31 ></my-peertube-checkbox> 32 ></my-peertube-checkbox>
32 33
33 <my-timestamp-input 34 <my-timestamp-input
34 [timestamp]="timestampOptions.stopTimestamp" 35 [timestamp]="timestampOptions.stopTimestamp"
35 [maxTimestamp]="video.duration" 36 [maxTimestamp]="video.duration"
36 [disabled]="!timestampOptions.stopTimestampEnabled" 37 [disabled]="!timestampOptions.stopTimestampEnabled"
37 [(ngModel)]="timestampOptions.stopTimestamp" 38 [(ngModel)]="timestampOptions.stopTimestamp"
38 ></my-timestamp-input> 39 ></my-timestamp-input>
40 </div>
39 </div> 41 </div>
40 </div> 42 </div>
41</div>
42 43
43<div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)"> 44 <div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)">
44 <my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist"></my-peertube-checkbox> 45 <my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist"></my-peertube-checkbox>
45 46
46 <div class="display-name"> 47 <div class="display-name">
47 {{ playlist.displayName }} 48 {{ playlist.displayName }}
48 49
49 <div *ngIf="playlist.inPlaylist && (playlist.startTimestamp || playlist.stopTimestamp)" class="timestamp-info"> 50 <div *ngIf="playlist.inPlaylist && (playlist.startTimestamp || playlist.stopTimestamp)" class="timestamp-info">
50 {{ formatTimestamp(playlist) }} 51 {{ formatTimestamp(playlist) }}
52 </div>
51 </div> 53 </div>
52 </div> 54 </div>
53</div>
54 55
55<div class="new-playlist-button dropdown-item" (click)="openCreateBlock($event)" [hidden]="isNewPlaylistBlockOpened"> 56 <div class="new-playlist-button dropdown-item" (click)="openCreateBlock($event)" [hidden]="isNewPlaylistBlockOpened">
56 <my-global-icon iconName="add"></my-global-icon> 57 <my-global-icon iconName="add"></my-global-icon>
57 58
58 Create a new playlist 59 Create a new playlist
59</div> 60 </div>
60 61
61<form class="new-playlist-block dropdown-item" *ngIf="isNewPlaylistBlockOpened" (ngSubmit)="createPlaylist()" [formGroup]="form"> 62 <form class="new-playlist-block dropdown-item" *ngIf="isNewPlaylistBlockOpened" (ngSubmit)="createPlaylist()" [formGroup]="form">
62 <div class="form-group"> 63 <div class="form-group">
63 <label i18n for="displayName">Display name</label> 64 <label i18n for="displayName">Display name</label>
64 <input 65 <input
65 type="text" id="displayName" 66 type="text" id="displayName"
66 formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }" 67 formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
67 > 68 >
68 <div *ngIf="formErrors['displayName']" class="form-error"> 69 <div *ngIf="formErrors['displayName']" class="form-error">
69 {{ formErrors['displayName'] }} 70 {{ formErrors['displayName'] }}
71 </div>
70 </div> 72 </div>
71 </div>
72 73
73 <input type="submit" i18n-value value="Create" [disabled]="!form.valid"> 74 <input type="submit" i18n-value value="Create" [disabled]="!form.valid">
74</form> 75 </form>
76</div>
diff --git a/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss b/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss
index bc0d55912..0424e2ee9 100644
--- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss
+++ b/client/src/app/shared/video-playlist/video-add-to-playlist.component.scss
@@ -1,6 +1,11 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4.root {
5 max-height: 300px;
6 overflow-y: auto;
7}
8
4.header { 9.header {
5 min-width: 240px; 10 min-width: 240px;
6 padding: 6px 24px 10px 24px; 11 padding: 6px 24px 10px 24px;
diff --git a/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts b/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts
index 705f62404..152f20c85 100644
--- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts
+++ b/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts
@@ -24,6 +24,7 @@ type PlaylistSummary = {
24export class VideoAddToPlaylistComponent extends FormReactive implements OnInit { 24export class VideoAddToPlaylistComponent extends FormReactive implements OnInit {
25 @Input() video: Video 25 @Input() video: Video
26 @Input() currentVideoTimestamp: number 26 @Input() currentVideoTimestamp: number
27 @Input() lazyLoad = false
27 28
28 isNewPlaylistBlockOpened = false 29 isNewPlaylistBlockOpened = false
29 videoPlaylists: PlaylistSummary[] = [] 30 videoPlaylists: PlaylistSummary[] = []
@@ -57,6 +58,10 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit
57 displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME 58 displayName: this.videoPlaylistValidatorsService.VIDEO_PLAYLIST_DISPLAY_NAME
58 }) 59 })
59 60
61 if (this.lazyLoad !== true) this.load()
62 }
63
64 load () {
60 forkJoin([ 65 forkJoin([
61 this.videoPlaylistService.listAccountPlaylists(this.user.account, '-updatedAt'), 66 this.videoPlaylistService.listAccountPlaylists(this.user.account, '-updatedAt'),
62 this.videoPlaylistService.doesVideoExistInPlaylist(this.video.id) 67 this.videoPlaylistService.doesVideoExistInPlaylist(this.video.id)
diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html
index e134654a3..d1b761674 100644
--- a/client/src/app/shared/video/abstract-video-list.html
+++ b/client/src/app/shared/video/abstract-video-list.html
@@ -1,4 +1,4 @@
1<div [ngClass]="{ 'margin-content': marginContent }"> 1<div class="margin-content">
2 <div class="videos-header"> 2 <div class="videos-header">
3 <div *ngIf="titlePage" class="title-page title-page-single"> 3 <div *ngIf="titlePage" class="title-page title-page-single">
4 <div placement="bottom" [ngbTooltip]="titleTooltip" container="body"> 4 <div placement="bottom" [ngbTooltip]="titleTooltip" container="body">
@@ -11,7 +11,7 @@
11 <div class="moderation-block" *ngIf="displayModerationBlock"> 11 <div class="moderation-block" *ngIf="displayModerationBlock">
12 <my-peertube-checkbox 12 <my-peertube-checkbox
13 (change)="toggleModerationDisplay()" 13 (change)="toggleModerationDisplay()"
14 inputName="display-unlisted-private" i18n-labelText labelText="Display unlisted and private videos" 14 inputName="display-unlisted-private" i18n-labelText labelText="Display unlisted and private videos"
15 > 15 >
16 </my-peertube-checkbox> 16 </my-peertube-checkbox>
17 </div> 17 </div>
@@ -22,7 +22,11 @@
22 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" 22 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true"
23 class="videos" 23 class="videos"
24 > 24 >
25 <my-video-miniature *ngFor="let video of videos; trackBy: videoById" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType"> 25 <my-video-miniature
26 *ngFor="let video of videos; trackBy: videoById" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType"
27 [displayVideoActions]="displayVideoActions"
28 (videoBlacklisted)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)"
29 >
26 </my-video-miniature> 30 </my-video-miniature>
27 </div> 31 </div>
28</div> 32</div>
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts
index 099650129..cf43d429d 100644
--- a/client/src/app/shared/video/abstract-video-list.ts
+++ b/client/src/app/shared/video/abstract-video-list.ts
@@ -26,11 +26,11 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
26 syndicationItems: Syndication[] = [] 26 syndicationItems: Syndication[] = []
27 27
28 loadOnInit = true 28 loadOnInit = true
29 marginContent = true
30 videos: Video[] = [] 29 videos: Video[] = []
31 ownerDisplayType: OwnerDisplayType = 'account' 30 ownerDisplayType: OwnerDisplayType = 'account'
32 displayModerationBlock = false 31 displayModerationBlock = false
33 titleTooltip: string 32 titleTooltip: string
33 displayVideoActions = true
34 34
35 disabled = false 35 disabled = false
36 36
@@ -120,6 +120,10 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
120 throw new Error('toggleModerationDisplay is not implemented') 120 throw new Error('toggleModerationDisplay is not implemented')
121 } 121 }
122 122
123 removeVideoFromArray (video: Video) {
124 this.videos = this.videos.filter(v => v.id !== video.id)
125 }
126
123 // On videos hook for children that want to do something 127 // On videos hook for children that want to do something
124 protected onMoreVideos () { /* empty */ } 128 protected onMoreVideos () { /* empty */ }
125 129
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.html b/client/src/app/shared/video/modals/video-blacklist.component.html
index 1a87bdcd4..1a87bdcd4 100644
--- a/client/src/app/videos/+video-watch/modal/video-blacklist.component.html
+++ b/client/src/app/shared/video/modals/video-blacklist.component.html
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.scss b/client/src/app/shared/video/modals/video-blacklist.component.scss
index afcdb9a16..afcdb9a16 100644
--- a/client/src/app/videos/+video-watch/modal/video-blacklist.component.scss
+++ b/client/src/app/shared/video/modals/video-blacklist.component.scss
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts b/client/src/app/shared/video/modals/video-blacklist.component.ts
index 50a7cadd1..4e4e8dc50 100644
--- a/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts
+++ b/client/src/app/shared/video/modals/video-blacklist.component.ts
@@ -1,11 +1,12 @@
1import { Component, Input, OnInit, ViewChild } from '@angular/core' 1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier, RedirectService } from '@app/core' 2import { Notifier, RedirectService } from '@app/core'
3import { FormReactive, VideoBlacklistService, VideoBlacklistValidatorsService } from '../../../shared/index' 3import { VideoBlacklistService } from '../../../shared/video-blacklist'
4import { VideoDetails } from '../../../shared/video/video-details.model' 4import { VideoDetails } from '../../../shared/video/video-details.model'
5import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
9import { FormReactive, VideoBlacklistValidatorsService } from '@app/shared/forms'
9 10
10@Component({ 11@Component({
11 selector: 'my-video-blacklist', 12 selector: 'my-video-blacklist',
@@ -17,6 +18,8 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit {
17 18
18 @ViewChild('modal') modal: NgbModal 19 @ViewChild('modal') modal: NgbModal
19 20
21 @Output() videoBlacklisted = new EventEmitter()
22
20 error: string = null 23 error: string = null
21 24
22 private openedModal: NgbModalRef 25 private openedModal: NgbModalRef
@@ -60,7 +63,11 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit {
60 () => { 63 () => {
61 this.notifier.success(this.i18n('Video blacklisted.')) 64 this.notifier.success(this.i18n('Video blacklisted.'))
62 this.hide() 65 this.hide()
63 this.redirectService.redirectToHomepage() 66
67 this.video.blacklisted = true
68 this.video.blacklistedReason = reason
69
70 this.videoBlacklisted.emit()
64 }, 71 },
65 72
66 err => this.notifier.error(err.message) 73 err => this.notifier.error(err.message)
diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.html b/client/src/app/shared/video/modals/video-download.component.html
index 2bb5d6d37..2bb5d6d37 100644
--- a/client/src/app/videos/+video-watch/modal/video-download.component.html
+++ b/client/src/app/shared/video/modals/video-download.component.html
diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.scss b/client/src/app/shared/video/modals/video-download.component.scss
index 3e826c3b6..3e826c3b6 100644
--- a/client/src/app/videos/+video-watch/modal/video-download.component.scss
+++ b/client/src/app/shared/video/modals/video-download.component.scss
diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.ts b/client/src/app/shared/video/modals/video-download.component.ts
index 834385771..64aaeb3c8 100644
--- a/client/src/app/videos/+video-watch/modal/video-download.component.ts
+++ b/client/src/app/shared/video/modals/video-download.component.ts
@@ -1,4 +1,4 @@
1import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core' 1import { Component, ElementRef, ViewChild } from '@angular/core'
2import { VideoDetails } from '../../../shared/video/video-details.model' 2import { VideoDetails } from '../../../shared/video/video-details.model'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -9,26 +9,32 @@ import { Notifier } from '@app/core'
9 templateUrl: './video-download.component.html', 9 templateUrl: './video-download.component.html',
10 styleUrls: [ './video-download.component.scss' ] 10 styleUrls: [ './video-download.component.scss' ]
11}) 11})
12export class VideoDownloadComponent implements OnInit { 12export class VideoDownloadComponent {
13 @Input() video: VideoDetails = null
14
15 @ViewChild('modal') modal: ElementRef 13 @ViewChild('modal') modal: ElementRef
16 14
17 downloadType: 'direct' | 'torrent' | 'magnet' = 'torrent' 15 downloadType: 'direct' | 'torrent' | 'magnet' = 'torrent'
18 resolutionId: number | string = -1 16 resolutionId: number | string = -1
19 17
18 private video: VideoDetails
19
20 constructor ( 20 constructor (
21 private notifier: Notifier, 21 private notifier: Notifier,
22 private modalService: NgbModal, 22 private modalService: NgbModal,
23 private i18n: I18n 23 private i18n: I18n
24 ) { } 24 ) { }
25 25
26 ngOnInit () { 26 show (video: VideoDetails) {
27 this.video = video
28
29 const m = this.modalService.open(this.modal)
30 m.result.then(() => this.onClose())
31 .catch(() => this.onClose())
32
27 this.resolutionId = this.video.files[0].resolution.id 33 this.resolutionId = this.video.files[0].resolution.id
28 } 34 }
29 35
30 show () { 36 onClose () {
31 this.modalService.open(this.modal) 37 this.video = undefined
32 } 38 }
33 39
34 download () { 40 download () {
@@ -45,21 +51,16 @@ export class VideoDownloadComponent implements OnInit {
45 return 51 return
46 } 52 }
47 53
48 const link = (() => { 54 switch (this.downloadType) {
49 switch (this.downloadType) { 55 case 'direct':
50 case 'direct': { 56 return file.fileDownloadUrl
51 return file.fileDownloadUrl 57
52 } 58 case 'torrent':
53 case 'torrent': { 59 return file.torrentDownloadUrl
54 return file.torrentDownloadUrl
55 }
56 case 'magnet': {
57 return file.magnetUri
58 }
59 }
60 })()
61 60
62 return link 61 case 'magnet':
62 return file.magnetUri
63 }
63 } 64 }
64 65
65 activateCopiedMessage () { 66 activateCopiedMessage () {
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.html b/client/src/app/shared/video/modals/video-report.component.html
index b9434da26..b9434da26 100644
--- a/client/src/app/videos/+video-watch/modal/video-report.component.html
+++ b/client/src/app/shared/video/modals/video-report.component.html
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.scss b/client/src/app/shared/video/modals/video-report.component.scss
index 4713660a2..4713660a2 100644
--- a/client/src/app/videos/+video-watch/modal/video-report.component.scss
+++ b/client/src/app/shared/video/modals/video-report.component.scss
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.ts b/client/src/app/shared/video/modals/video-report.component.ts
index 911f3b447..725dd020f 100644
--- a/client/src/app/videos/+video-watch/modal/video-report.component.ts
+++ b/client/src/app/shared/video/modals/video-report.component.ts
@@ -1,12 +1,13 @@
1import { Component, Input, OnInit, ViewChild } from '@angular/core' 1import { Component, Input, OnInit, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { FormReactive, VideoAbuseService } from '../../../shared/index' 3import { FormReactive } from '../../../shared/forms'
4import { VideoDetails } from '../../../shared/video/video-details.model' 4import { VideoDetails } from '../../../shared/video/video-details.model'
5import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
7import { VideoAbuseValidatorsService } from '@app/shared/forms/form-validators/video-abuse-validators.service' 7import { VideoAbuseValidatorsService } from '@app/shared/forms/form-validators/video-abuse-validators.service'
8import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 8import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
9import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 9import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
10import { VideoAbuseService } from '@app/shared/video-abuse'
10 11
11@Component({ 12@Component({
12 selector: 'my-video-report', 13 selector: 'my-video-report',
diff --git a/client/src/app/shared/video/video-actions-dropdown.component.html b/client/src/app/shared/video/video-actions-dropdown.component.html
new file mode 100644
index 000000000..300fe318a
--- /dev/null
+++ b/client/src/app/shared/video/video-actions-dropdown.component.html
@@ -0,0 +1,21 @@
1<ng-container *ngIf="videoActions.length !== 0">
2
3 <div class="playlist-dropdown" ngbDropdown #playlistDropdown="ngbDropdown" role="button" autoClose="outside" [placement]="getPlaylistDropdownPlacement()"
4 *ngIf="isUserLoggedIn() && displayOptions.playlist" (openChange)="playlistAdd.openChange($event)"
5 >
6 <span class="anchor" ngbDropdownAnchor></span>
7
8 <div ngbDropdownMenu>
9 <my-video-add-to-playlist #playlistAdd [video]="video" [lazyLoad]="true"></my-video-add-to-playlist>
10 </div>
11 </div>
12
13 <my-action-dropdown
14 [actions]="videoActions" [label]="label" [entry]="{ video: video }" (mouseenter)="loadDropdownInformation()"
15 [buttonSize]="buttonSize" [placement]="placement" [buttonDirection]="buttonDirection" [buttonStyled]="buttonStyled"
16 ></my-action-dropdown>
17
18 <my-video-download #videoDownloadModal></my-video-download>
19 <my-video-report #videoReportModal [video]="video"></my-video-report>
20 <my-video-blacklist #videoBlacklistModal [video]="video" (videoBlacklisted)="onVideoBlacklisted()"></my-video-blacklist>
21</ng-container>
diff --git a/client/src/app/shared/video/video-actions-dropdown.component.scss b/client/src/app/shared/video/video-actions-dropdown.component.scss
new file mode 100644
index 000000000..7ffdce822
--- /dev/null
+++ b/client/src/app/shared/video/video-actions-dropdown.component.scss
@@ -0,0 +1,12 @@
1.playlist-dropdown {
2 position: absolute;
3
4 .anchor {
5 display: block;
6 opacity: 0;
7 }
8}
9
10/deep/ .icon-playlist-add {
11 left: 2px;
12}
diff --git a/client/src/app/shared/video/video-actions-dropdown.component.ts b/client/src/app/shared/video/video-actions-dropdown.component.ts
new file mode 100644
index 000000000..90bdf7df8
--- /dev/null
+++ b/client/src/app/shared/video/video-actions-dropdown.component.ts
@@ -0,0 +1,237 @@
1import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core'
2import { I18n } from '@ngx-translate/i18n-polyfill'
3import { DropdownAction, DropdownButtonSize, DropdownDirection } from '@app/shared/buttons/action-dropdown.component'
4import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core'
5import { BlocklistService } from '@app/shared/blocklist'
6import { Video } from '@app/shared/video/video.model'
7import { VideoService } from '@app/shared/video/video.service'
8import { VideoDetails } from '@app/shared/video/video-details.model'
9import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
10import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component'
11import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component'
12import { VideoReportComponent } from '@app/shared/video/modals/video-report.component'
13import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component'
14import { VideoBlacklistService } from '@app/shared/video-blacklist'
15import { ScreenService } from '@app/shared/misc/screen.service'
16
17export type VideoActionsDisplayType = {
18 playlist?: boolean
19 download?: boolean
20 update?: boolean
21 blacklist?: boolean
22 delete?: boolean
23 report?: boolean
24}
25
26@Component({
27 selector: 'my-video-actions-dropdown',
28 templateUrl: './video-actions-dropdown.component.html',
29 styleUrls: [ './video-actions-dropdown.component.scss' ]
30})
31export class VideoActionsDropdownComponent implements OnChanges {
32 @ViewChild('playlistDropdown') playlistDropdown: NgbDropdown
33 @ViewChild('playlistAdd') playlistAdd: VideoAddToPlaylistComponent
34
35 @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent
36 @ViewChild('videoReportModal') videoReportModal: VideoReportComponent
37 @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent
38
39 @Input() video: Video | VideoDetails
40
41 @Input() displayOptions: VideoActionsDisplayType = {
42 playlist: false,
43 download: true,
44 update: true,
45 blacklist: true,
46 delete: true,
47 report: true
48 }
49 @Input() placement: string = 'left'
50
51 @Input() label: string
52
53 @Input() buttonStyled = false
54 @Input() buttonSize: DropdownButtonSize = 'normal'
55 @Input() buttonDirection: DropdownDirection = 'vertical'
56
57 @Output() videoRemoved = new EventEmitter()
58 @Output() videoUnblacklisted = new EventEmitter()
59 @Output() videoBlacklisted = new EventEmitter()
60
61 videoActions: DropdownAction<{ video: Video }>[][] = []
62
63 private loaded = false
64
65 constructor (
66 private authService: AuthService,
67 private notifier: Notifier,
68 private confirmService: ConfirmService,
69 private videoBlacklistService: VideoBlacklistService,
70 private serverService: ServerService,
71 private screenService: ScreenService,
72 private videoService: VideoService,
73 private blocklistService: BlocklistService,
74 private i18n: I18n
75 ) { }
76
77 get user () {
78 return this.authService.getUser()
79 }
80
81 ngOnChanges () {
82 this.buildActions()
83 }
84
85 isUserLoggedIn () {
86 return this.authService.isLoggedIn()
87 }
88
89 loadDropdownInformation () {
90 if (!this.isUserLoggedIn() || this.loaded === true) return
91
92 this.loaded = true
93
94 if (this.displayOptions.playlist) this.playlistAdd.load()
95 }
96
97 /* Show modals */
98
99 showDownloadModal () {
100 this.videoDownloadModal.show(this.video as VideoDetails)
101 }
102
103 showReportModal () {
104 this.videoReportModal.show()
105 }
106
107 showBlacklistModal () {
108 this.videoBlacklistModal.show()
109 }
110
111 /* Actions checker */
112
113 isVideoUpdatable () {
114 return this.video.isUpdatableBy(this.user)
115 }
116
117 isVideoRemovable () {
118 return this.video.isRemovableBy(this.user)
119 }
120
121 isVideoBlacklistable () {
122 return this.video.isBlackistableBy(this.user)
123 }
124
125 isVideoUnblacklistable () {
126 return this.video.isUnblacklistableBy(this.user)
127 }
128
129 /* Action handlers */
130
131 async unblacklistVideo () {
132 const confirmMessage = this.i18n(
133 'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
134 )
135
136 const res = await this.confirmService.confirm(confirmMessage, this.i18n('Unblacklist'))
137 if (res === false) return
138
139 this.videoBlacklistService.removeVideoFromBlacklist(this.video.id).subscribe(
140 () => {
141 this.notifier.success(this.i18n('Video {{name}} removed from the blacklist.', { name: this.video.name }))
142
143 this.video.blacklisted = false
144 this.video.blacklistedReason = null
145
146 this.videoUnblacklisted.emit()
147 },
148
149 err => this.notifier.error(err.message)
150 )
151 }
152
153 async removeVideo () {
154 const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this video?'), this.i18n('Delete'))
155 if (res === false) return
156
157 this.videoService.removeVideo(this.video.id)
158 .subscribe(
159 () => {
160 this.notifier.success(this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name }))
161
162 this.videoRemoved.emit()
163 },
164
165 error => this.notifier.error(error.message)
166 )
167 }
168
169 onVideoBlacklisted () {
170 this.videoBlacklisted.emit()
171 }
172
173 getPlaylistDropdownPlacement () {
174 if (this.screenService.isInSmallView()) {
175 return 'bottom-right'
176 }
177
178 return 'bottom-left bottom-right'
179 }
180
181 private buildActions () {
182 this.videoActions = []
183
184 if (this.authService.isLoggedIn()) {
185 this.videoActions.push([
186 {
187 label: this.i18n('Save to playlist'),
188 handler: () => this.playlistDropdown.toggle(),
189 isDisplayed: () => this.displayOptions.playlist,
190 iconName: 'playlist-add'
191 }
192 ])
193
194 this.videoActions.push([
195 {
196 label: this.i18n('Download'),
197 handler: () => this.showDownloadModal(),
198 isDisplayed: () => this.displayOptions.download,
199 iconName: 'download'
200 },
201 {
202 label: this.i18n('Update'),
203 linkBuilder: ({ video }) => [ '/videos/update', video.uuid ],
204 iconName: 'edit',
205 isDisplayed: () => this.displayOptions.update && this.isVideoUpdatable()
206 },
207 {
208 label: this.i18n('Blacklist'),
209 handler: () => this.showBlacklistModal(),
210 iconName: 'no',
211 isDisplayed: () => this.displayOptions.blacklist && this.isVideoBlacklistable()
212 },
213 {
214 label: this.i18n('Unblacklist'),
215 handler: () => this.unblacklistVideo(),
216 iconName: 'undo',
217 isDisplayed: () => this.displayOptions.blacklist && this.isVideoUnblacklistable()
218 },
219 {
220 label: this.i18n('Delete'),
221 handler: () => this.removeVideo(),
222 isDisplayed: () => this.displayOptions.delete && this.isVideoRemovable(),
223 iconName: 'delete'
224 }
225 ])
226
227 this.videoActions.push([
228 {
229 label: this.i18n('Report'),
230 handler: () => this.showReportModal(),
231 isDisplayed: () => this.displayOptions.report,
232 iconName: 'alert'
233 }
234 ])
235 }
236 }
237}
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts
index 388357343..8463e15d7 100644
--- a/client/src/app/shared/video/video-details.model.ts
+++ b/client/src/app/shared/video/video-details.model.ts
@@ -44,22 +44,6 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
44 this.buildLikeAndDislikePercents() 44 this.buildLikeAndDislikePercents()
45 } 45 }
46 46
47 isRemovableBy (user: AuthUser) {
48 return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO))
49 }
50
51 isBlackistableBy (user: AuthUser) {
52 return this.blacklisted !== true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true
53 }
54
55 isUnblacklistableBy (user: AuthUser) {
56 return this.blacklisted === true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true
57 }
58
59 isUpdatableBy (user: AuthUser) {
60 return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO))
61 }
62
63 buildLikeAndDislikePercents () { 47 buildLikeAndDislikePercents () {
64 this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 48 this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100
65 this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 49 this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100
diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html
index f4ae0b0dd..7af0f1113 100644
--- a/client/src/app/shared/video/video-miniature.component.html
+++ b/client/src/app/shared/video/video-miniature.component.html
@@ -1,47 +1,56 @@
1<div class="video-miniature" [ngClass]="{ 'display-as-row': displayAsRow }"> 1<div class="video-miniature" [ngClass]="{ 'display-as-row': displayAsRow }" (mouseenter)="loadActions()">
2 <my-video-thumbnail [video]="video" [nsfw]="isVideoBlur"></my-video-thumbnail> 2 <my-video-thumbnail [video]="video" [nsfw]="isVideoBlur"></my-video-thumbnail>
3 3
4 <div class="video-miniature-information"> 4 <div class="video-bottom">
5 <a 5 <div class="video-miniature-information">
6 tabindex="-1" 6 <a
7 class="video-miniature-name" 7 tabindex="-1"
8 [routerLink]="[ '/videos/watch', video.uuid ]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }" 8 class="video-miniature-name"
9 > 9 [routerLink]="[ '/videos/watch', video.uuid ]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }"
10 <ng-container *ngIf="displayOptions.privacyLabel"> 10 >
11 <span *ngIf="isUnlistedVideo()" class="badge badge-warning" i18n>Unlisted</span> 11 <ng-container *ngIf="displayOptions.privacyLabel">
12 <span *ngIf="isPrivateVideo()" class="badge badge-danger" i18n>Private</span> 12 <span *ngIf="isUnlistedVideo()" class="badge badge-warning" i18n>Unlisted</span>
13 </ng-container> 13 <span *ngIf="isPrivateVideo()" class="badge badge-danger" i18n>Private</span>
14 14 </ng-container>
15 {{ video.name }} 15
16 </a> 16 {{ video.name }}
17 17 </a>
18 <span class="video-miniature-created-at-views"> 18
19 <ng-container *ngIf="displayOptions.date">{{ video.publishedAt | myFromNow }}</ng-container> 19 <span class="video-miniature-created-at-views">
20 <ng-container *ngIf="displayOptions.date && displayOptions.views"> - </ng-container> 20 <ng-container *ngIf="displayOptions.date">{{ video.publishedAt | myFromNow }}</ng-container>
21 <ng-container i18n *ngIf="displayOptions.views">{{ video.views | myNumberFormatter }} views</ng-container> 21 <ng-container *ngIf="displayOptions.date && displayOptions.views"> - </ng-container>
22 </span> 22 <ng-container i18n *ngIf="displayOptions.views">{{ video.views | myNumberFormatter }} views</ng-container>
23 23 </span>
24 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]"> 24
25 {{ video.byAccount }} 25 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]">
26 </a> 26 {{ video.byAccount }}
27 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> 27 </a>
28 {{ video.byVideoChannel }} 28 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]">
29 </a> 29 {{ video.byVideoChannel }}
30 30 </a>
31 <div class="video-info-privacy"> 31
32 <ng-container *ngIf="displayOptions.privacyText">{{ video.privacy.label }}</ng-container> 32 <div class="video-info-privacy">
33 <ng-container *ngIf="displayOptions.privacyText && displayOptions.state"> - </ng-container> 33 <ng-container *ngIf="displayOptions.privacyText">{{ video.privacy.label }}</ng-container>
34 <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container> 34 <ng-container *ngIf="displayOptions.privacyText && displayOptions.state"> - </ng-container>
35 <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container>
36 </div>
37
38 <div *ngIf="displayOptions.blacklistInfo && video.blacklisted" class="video-info-blacklisted">
39 <span class="blacklisted-label" i18n>Blacklisted</span>
40 <span class="blacklisted-reason" *ngIf="video.blacklistedReason">{{ video.blacklistedReason }}</span>
41 </div>
42
43 <div i18n *ngIf="displayOptions.nsfw && video.nsfw" class="video-info-nsfw">
44 Sensitive
45 </div>
35 </div> 46 </div>
36 47
37 <div *ngIf="displayOptions.blacklistInfo && video.blacklisted" class="video-info-blacklisted"> 48 <div class="video-actions">
38 <span class="blacklisted-label" i18n>Blacklisted</span> 49 <!-- FIXME: remove bottom placement when overflow is fixed in bootstrap dropdown -->
39 <span class="blacklisted-reason" *ngIf="video.blacklistedReason">{{ video.blacklistedReason }}</span> 50 <my-video-actions-dropdown
51 *ngIf="showActions" [video]="video" [displayOptions]="videoActionsDisplayOptions" placement="bottom-left bottom-right left"
52 (videoRemoved)="onVideoRemoved()" (videoBlacklisted)="onVideoBlacklisted()" (videoUnblacklisted)="onVideoUnblacklisted()"
53 ></my-video-actions-dropdown>
40 </div> 54 </div>
41
42 <div i18n *ngIf="displayOptions.nsfw && video.nsfw" class="video-info-nsfw">
43 Sensitive
44 </div>
45
46 </div> 55 </div>
47</div> 56</div>
diff --git a/client/src/app/shared/video/video-miniature.component.scss b/client/src/app/shared/video/video-miniature.component.scss
index fdc3dc033..0d4e59c2a 100644
--- a/client/src/app/shared/video/video-miniature.component.scss
+++ b/client/src/app/shared/video/video-miniature.component.scss
@@ -56,6 +56,37 @@
56 } 56 }
57 } 57 }
58 58
59 .video-bottom {
60 display: flex;
61
62 .video-actions {
63 margin-top: 3px;
64 margin-right: 10px;
65 }
66
67 /deep/ .dropdown-root:not(.show) {
68 display: none;
69 }
70
71 &:hover /deep/ .dropdown-root {
72 display: block;
73 }
74
75 /deep/ .playlist-dropdown.show + my-action-dropdown .dropdown-root {
76 display: block;
77 }
78
79 @media screen and (max-width: $small-view) {
80 .video-actions {
81 margin-right: 0;
82 }
83
84 /deep/ .dropdown-root {
85 display: block !important;
86 }
87 }
88 }
89
59 &.display-as-row { 90 &.display-as-row {
60 flex-direction: row; 91 flex-direction: row;
61 margin-bottom: 0; 92 margin-bottom: 0;
@@ -91,6 +122,11 @@
91 } 122 }
92 } 123 }
93 124
125 .video-bottom .video-actions {
126 margin: 0;
127 top: -3px;
128 }
129
94 @media screen and (max-width: $small-view) { 130 @media screen and (max-width: $small-view) {
95 flex-direction: column; 131 flex-direction: column;
96 height: auto; 132 height: auto;
diff --git a/client/src/app/shared/video/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts
index 800417a79..e3552abba 100644
--- a/client/src/app/shared/video/video-miniature.component.ts
+++ b/client/src/app/shared/video/video-miniature.component.ts
@@ -1,9 +1,11 @@
1import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core' 1import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, LOCALE_ID, OnInit, Output } from '@angular/core'
2import { User } from '../users' 2import { User } from '../users'
3import { Video } from './video.model' 3import { Video } from './video.model'
4import { ServerService } from '@app/core' 4import { ServerService } from '@app/core'
5import { VideoPrivacy, VideoState } from '../../../../../shared' 5import { VideoPrivacy, VideoState } from '../../../../../shared'
6import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { VideoActionsDisplayType } from '@app/shared/video/video-actions-dropdown.component'
8import { ScreenService } from '@app/shared/misc/screen.service'
7 9
8export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto' 10export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto'
9export type MiniatureDisplayOptions = { 11export type MiniatureDisplayOptions = {
@@ -38,10 +40,26 @@ export class VideoMiniatureComponent implements OnInit {
38 blacklistInfo: false 40 blacklistInfo: false
39 } 41 }
40 @Input() displayAsRow = false 42 @Input() displayAsRow = false
43 @Input() displayVideoActions = true
44
45 @Output() videoBlacklisted = new EventEmitter()
46 @Output() videoUnblacklisted = new EventEmitter()
47 @Output() videoRemoved = new EventEmitter()
48
49 videoActionsDisplayOptions: VideoActionsDisplayType = {
50 playlist: true,
51 download: false,
52 update: true,
53 blacklist: true,
54 delete: true,
55 report: true
56 }
57 showActions = false
41 58
42 private ownerDisplayTypeChosen: 'account' | 'videoChannel' 59 private ownerDisplayTypeChosen: 'account' | 'videoChannel'
43 60
44 constructor ( 61 constructor (
62 private screenService: ScreenService,
45 private serverService: ServerService, 63 private serverService: ServerService,
46 private i18n: I18n, 64 private i18n: I18n,
47 @Inject(LOCALE_ID) private localeId: string 65 @Inject(LOCALE_ID) private localeId: string
@@ -52,20 +70,10 @@ export class VideoMiniatureComponent implements OnInit {
52 } 70 }
53 71
54 ngOnInit () { 72 ngOnInit () {
55 if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') { 73 this.setUpBy()
56 this.ownerDisplayTypeChosen = this.ownerDisplayType
57 return
58 }
59 74
60 // If the video channel name an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12) 75 if (this.screenService.isInSmallView()) {
61 // -> Use the account name 76 this.showActions = true
62 if (
63 this.video.channel.name === `${this.video.account.name}_channel` ||
64 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}$/)
65 ) {
66 this.ownerDisplayTypeChosen = 'account'
67 } else {
68 this.ownerDisplayTypeChosen = 'videoChannel'
69 } 77 }
70 } 78 }
71 79
@@ -109,4 +117,38 @@ export class VideoMiniatureComponent implements OnInit {
109 117
110 return '' 118 return ''
111 } 119 }
120
121 loadActions () {
122 if (this.displayVideoActions) this.showActions = true
123 }
124
125 onVideoBlacklisted () {
126 this.videoBlacklisted.emit()
127 }
128
129 onVideoUnblacklisted () {
130 this.videoUnblacklisted.emit()
131 }
132
133 onVideoRemoved () {
134 this.videoRemoved.emit()
135 }
136
137 private setUpBy () {
138 if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') {
139 this.ownerDisplayTypeChosen = this.ownerDisplayType
140 return
141 }
142
143 // If the video channel name an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12)
144 // -> Use the account name
145 if (
146 this.video.channel.name === `${this.video.account.name}_channel` ||
147 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}$/)
148 ) {
149 this.ownerDisplayTypeChosen = 'account'
150 } else {
151 this.ownerDisplayTypeChosen = 'videoChannel'
152 }
153 }
112} 154}
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts
index 95b5e3671..0cef3eb8f 100644
--- a/client/src/app/shared/video/video.model.ts
+++ b/client/src/app/shared/video/video.model.ts
@@ -1,11 +1,12 @@
1import { User } from '../' 1import { User } from '../'
2import { PlaylistElement, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared' 2import { PlaylistElement, UserRight, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared'
3import { Avatar } from '../../../../../shared/models/avatars/avatar.model' 3import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
4import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model' 4import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model'
5import { durationToString, getAbsoluteAPIUrl } from '../misc/utils' 5import { durationToString, getAbsoluteAPIUrl } from '../misc/utils'
6import { peertubeTranslate, ServerConfig } from '../../../../../shared/models' 6import { peertubeTranslate, ServerConfig } from '../../../../../shared/models'
7import { Actor } from '@app/shared/actor/actor.model' 7import { Actor } from '@app/shared/actor/actor.model'
8import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model' 8import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model'
9import { AuthUser } from '@app/core'
9 10
10export class Video implements VideoServerModel { 11export class Video implements VideoServerModel {
11 byVideoChannel: string 12 byVideoChannel: string
@@ -141,4 +142,20 @@ export class Video implements VideoServerModel {
141 // Return default instance config 142 // Return default instance config
142 return serverConfig.instance.defaultNSFWPolicy !== 'display' 143 return serverConfig.instance.defaultNSFWPolicy !== 'display'
143 } 144 }
145
146 isRemovableBy (user: AuthUser) {
147 return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO))
148 }
149
150 isBlackistableBy (user: AuthUser) {
151 return this.blacklisted !== true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true
152 }
153
154 isUnblacklistableBy (user: AuthUser) {
155 return this.blacklisted === true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true
156 }
157
158 isUpdatableBy (user: AuthUser) {
159 return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO))
160 }
144} 161}
diff --git a/client/src/app/shared/video/videos-selection.component.html b/client/src/app/shared/video/videos-selection.component.html
index 6f3401b4b..53809b6fd 100644
--- a/client/src/app/shared/video/videos-selection.component.html
+++ b/client/src/app/shared/video/videos-selection.component.html
@@ -6,7 +6,7 @@
6 <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="_selection[video.id]"></my-peertube-checkbox> 6 <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="_selection[video.id]"></my-peertube-checkbox>
7 </div> 7 </div>
8 8
9 <my-video-miniature [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions"></my-video-miniature> 9 <my-video-miniature [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions" [displayVideoActions]="false"></my-video-miniature>
10 10
11 <!-- Display only once --> 11 <!-- Display only once -->
12 <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0"> 12 <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0">
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html
index ad1d04b70..7755a729a 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -120,37 +120,9 @@
120 </div> 120 </div>
121 </div> 121 </div>
122 122
123 <div class="action-dropdown" ngbDropdown placement="top" role="button"> 123 <my-video-actions-dropdown
124 <div class="action-button" ngbDropdownToggle role="button"> 124 placement="top" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" (videoRemoved)="onVideoRemoved()"
125 <my-global-icon class="more-icon" iconName="more-horizontal"></my-global-icon> 125 ></my-video-actions-dropdown>
126 </div>
127
128 <div ngbDropdownMenu>
129 <a *ngIf="isVideoDownloadable()" class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)">
130 <my-global-icon iconName="download"></my-global-icon> <ng-container i18n>Download</ng-container>
131 </a>
132
133 <a *ngIf="isUserLoggedIn()" class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)">
134 <my-global-icon iconName="alert"></my-global-icon> <ng-container i18n>Report</ng-container>
135 </a>
136
137 <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]">
138 <my-global-icon iconName="edit"></my-global-icon> <ng-container i18n>Update</ng-container>
139 </a>
140
141 <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)">
142 <my-global-icon iconName="no"></my-global-icon> <ng-container i18n>Blacklist</ng-container>
143 </a>
144
145 <a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)">
146 <my-global-icon iconName="undo"></my-global-icon> <ng-container i18n>Unblacklist</ng-container>
147 </a>
148
149 <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)">
150 <my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete</ng-container>
151 </a>
152 </div>
153 </div>
154 </div> 126 </div>
155 127
156 <div 128 <div
@@ -270,7 +242,4 @@
270<ng-template [ngIf]="video !== null"> 242<ng-template [ngIf]="video !== null">
271 <my-video-support #videoSupportModal [video]="video"></my-video-support> 243 <my-video-support #videoSupportModal [video]="video"></my-video-support>
272 <my-video-share #videoShareModal [video]="video"></my-video-share> 244 <my-video-share #videoShareModal [video]="video"></my-video-share>
273 <my-video-download #videoDownloadModal [video]="video"></my-video-download>
274 <my-video-report #videoReportModal [video]="video"></my-video-report>
275 <my-video-blacklist #videoBlacklistModal [video]="video"></my-video-blacklist>
276</ng-template> 245</ng-template>
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss
index 2874847cd..c1eaf9b2b 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch.component.scss
@@ -257,7 +257,9 @@ $player-factor: 1.7; // 16/9
257 display: flex; 257 display: flex;
258 align-items: center; 258 align-items: center;
259 259
260 .action-button:not(:first-child), .action-dropdown { 260 .action-button:not(:first-child),
261 .action-dropdown,
262 my-video-actions-dropdown {
261 margin-left: 10px; 263 margin-left: 10px;
262 } 264 }
263 265
@@ -304,14 +306,6 @@ $player-factor: 1.7; // 16/9
304 margin-left: 3px; 306 margin-left: 3px;
305 } 307 }
306 } 308 }
307
308 .action-dropdown {
309 display: inline-block;
310
311 .dropdown-menu .dropdown-item {
312 @include dropdown-with-icon-item;
313 }
314 }
315 } 309 }
316 310
317 .video-info-likes-dislikes-bar { 311 .video-info-likes-dislikes-bar {
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index cedbbf985..53673d9d9 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -13,10 +13,7 @@ import { AuthService, ConfirmService } from '../../core'
13import { RestExtractor, VideoBlacklistService } from '../../shared' 13import { RestExtractor, VideoBlacklistService } from '../../shared'
14import { VideoDetails } from '../../shared/video/video-details.model' 14import { VideoDetails } from '../../shared/video/video-details.model'
15import { VideoService } from '../../shared/video/video.service' 15import { VideoService } from '../../shared/video/video.service'
16import { VideoDownloadComponent } from './modal/video-download.component'
17import { VideoReportComponent } from './modal/video-report.component'
18import { VideoShareComponent } from './modal/video-share.component' 16import { VideoShareComponent } from './modal/video-share.component'
19import { VideoBlacklistComponent } from './modal/video-blacklist.component'
20import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' 17import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component'
21import { I18n } from '@ngx-translate/i18n-polyfill' 18import { I18n } from '@ngx-translate/i18n-polyfill'
22import { environment } from '../../../environments/environment' 19import { environment } from '../../../environments/environment'
@@ -32,6 +29,7 @@ import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
32import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' 29import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
33import { ComponentPagination } from '@app/shared/rest/component-pagination.model' 30import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
34import { Video } from '@app/shared/video/video.model' 31import { Video } from '@app/shared/video/video.model'
32import { VideoActionsDisplayType } from '@app/shared/video/video-actions-dropdown.component'
35 33
36@Component({ 34@Component({
37 selector: 'my-video-watch', 35 selector: 'my-video-watch',
@@ -41,11 +39,8 @@ import { Video } from '@app/shared/video/video.model'
41export class VideoWatchComponent implements OnInit, OnDestroy { 39export class VideoWatchComponent implements OnInit, OnDestroy {
42 private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern' 40 private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern'
43 41
44 @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent
45 @ViewChild('videoShareModal') videoShareModal: VideoShareComponent 42 @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
46 @ViewChild('videoReportModal') videoReportModal: VideoReportComponent
47 @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent 43 @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent
48 @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent
49 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent 44 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
50 45
51 player: any 46 player: any
@@ -212,11 +207,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
212 ) 207 )
213 } 208 }
214 209
215 showReportModal (event: Event) {
216 event.preventDefault()
217 this.videoReportModal.show()
218 }
219
220 showSupportModal () { 210 showSupportModal () {
221 this.videoSupportModal.show() 211 this.videoSupportModal.show()
222 } 212 }
@@ -225,54 +215,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
225 this.videoShareModal.show(this.currentTime) 215 this.videoShareModal.show(this.currentTime)
226 } 216 }
227 217
228 showDownloadModal (event: Event) {
229 event.preventDefault()
230 this.videoDownloadModal.show()
231 }
232
233 showBlacklistModal (event: Event) {
234 event.preventDefault()
235 this.videoBlacklistModal.show()
236 }
237
238 async unblacklistVideo (event: Event) {
239 event.preventDefault()
240
241 const confirmMessage = this.i18n(
242 'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
243 )
244
245 const res = await this.confirmService.confirm(confirmMessage, this.i18n('Unblacklist'))
246 if (res === false) return
247
248 this.videoBlacklistService.removeVideoFromBlacklist(this.video.id).subscribe(
249 () => {
250 this.notifier.success(this.i18n('Video {{name}} removed from the blacklist.', { name: this.video.name }))
251
252 this.video.blacklisted = false
253 this.video.blacklistedReason = null
254 },
255
256 err => this.notifier.error(err.message)
257 )
258 }
259
260 isUserLoggedIn () { 218 isUserLoggedIn () {
261 return this.authService.isLoggedIn() 219 return this.authService.isLoggedIn()
262 } 220 }
263 221
264 isVideoUpdatable () {
265 return this.video.isUpdatableBy(this.authService.getUser())
266 }
267
268 isVideoBlacklistable () {
269 return this.video.isBlackistableBy(this.user)
270 }
271
272 isVideoUnblacklistable () {
273 return this.video.isUnblacklistableBy(this.user)
274 }
275
276 getVideoTags () { 222 getVideoTags () {
277 if (!this.video || Array.isArray(this.video.tags) === false) return [] 223 if (!this.video || Array.isArray(this.video.tags) === false) return []
278 224
@@ -283,23 +229,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
283 return this.video.isRemovableBy(this.authService.getUser()) 229 return this.video.isRemovableBy(this.authService.getUser())
284 } 230 }
285 231
286 async removeVideo (event: Event) { 232 onVideoRemoved () {
287 event.preventDefault() 233 this.redirectService.redirectToHomepage()
288
289 const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this video?'), this.i18n('Delete'))
290 if (res === false) return
291
292 this.videoService.removeVideo(this.video.id)
293 .subscribe(
294 () => {
295 this.notifier.success(this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name }))
296
297 // Go back to the video-list.
298 this.redirectService.redirectToHomepage()
299 },
300
301 error => this.notifier.error(error.message)
302 )
303 } 234 }
304 235
305 acceptedPrivacyConcern () { 236 acceptedPrivacyConcern () {
diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts
index 2f448db78..983350f52 100644
--- a/client/src/app/videos/+video-watch/video-watch.module.ts
+++ b/client/src/app/videos/+video-watch/video-watch.module.ts
@@ -1,26 +1,21 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' 2import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component'
3import { ClipboardModule } from 'ngx-clipboard'
4import { SharedModule } from '../../shared' 3import { SharedModule } from '../../shared'
5import { VideoCommentAddComponent } from './comment/video-comment-add.component' 4import { VideoCommentAddComponent } from './comment/video-comment-add.component'
6import { VideoCommentComponent } from './comment/video-comment.component' 5import { VideoCommentComponent } from './comment/video-comment.component'
7import { VideoCommentService } from './comment/video-comment.service' 6import { VideoCommentService } from './comment/video-comment.service'
8import { VideoCommentsComponent } from './comment/video-comments.component' 7import { VideoCommentsComponent } from './comment/video-comments.component'
9import { VideoDownloadComponent } from './modal/video-download.component'
10import { VideoReportComponent } from './modal/video-report.component'
11import { VideoShareComponent } from './modal/video-share.component' 8import { VideoShareComponent } from './modal/video-share.component'
12import { VideoWatchRoutingModule } from './video-watch-routing.module' 9import { VideoWatchRoutingModule } from './video-watch-routing.module'
13import { VideoWatchComponent } from './video-watch.component' 10import { VideoWatchComponent } from './video-watch.component'
14import { NgxQRCodeModule } from 'ngx-qrcode2' 11import { NgxQRCodeModule } from 'ngx-qrcode2'
15import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' 12import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
16import { VideoBlacklistComponent } from '@app/videos/+video-watch/modal/video-blacklist.component'
17import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module' 13import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module'
18 14
19@NgModule({ 15@NgModule({
20 imports: [ 16 imports: [
21 VideoWatchRoutingModule, 17 VideoWatchRoutingModule,
22 SharedModule, 18 SharedModule,
23 ClipboardModule,
24 NgbTooltipModule, 19 NgbTooltipModule,
25 NgxQRCodeModule, 20 NgxQRCodeModule,
26 RecommendationsModule 21 RecommendationsModule
@@ -29,10 +24,7 @@ import { RecommendationsModule } from '@app/videos/recommendations/recommendatio
29 declarations: [ 24 declarations: [
30 VideoWatchComponent, 25 VideoWatchComponent,
31 26
32 VideoDownloadComponent,
33 VideoShareComponent, 27 VideoShareComponent,
34 VideoReportComponent,
35 VideoBlacklistComponent,
36 VideoSupportComponent, 28 VideoSupportComponent,
37 VideoCommentsComponent, 29 VideoCommentsComponent,
38 VideoCommentAddComponent, 30 VideoCommentAddComponent,
diff --git a/client/src/app/videos/video-list/video-overview.component.html b/client/src/app/videos/video-list/video-overview.component.html
index cb26592e3..b644dd798 100644
--- a/client/src/app/videos/video-list/video-overview.component.html
+++ b/client/src/app/videos/video-list/video-overview.component.html
@@ -7,7 +7,7 @@
7 <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a> 7 <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a>
8 </div> 8 </div>
9 9
10 <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user"></my-video-miniature> 10 <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user" [displayVideoActions]="false"></my-video-miniature>
11 </div> 11 </div>
12 12
13 <div class="section" *ngFor="let object of overview.tags"> 13 <div class="section" *ngFor="let object of overview.tags">
@@ -15,7 +15,7 @@
15 <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a> 15 <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a>
16 </div> 16 </div>
17 17
18 <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user"></my-video-miniature> 18 <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user" [displayVideoActions]="false"></my-video-miniature>
19 </div> 19 </div>
20 20
21 <div class="section channel" *ngFor="let object of overview.channels"> 21 <div class="section channel" *ngFor="let object of overview.channels">
@@ -27,7 +27,7 @@
27 </a> 27 </a>
28 </div> 28 </div>
29 29
30 <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user"></my-video-miniature> 30 <my-video-miniature *ngFor="let video of object.videos" [video]="video" [user]="user" [displayVideoActions]="false"></my-video-miniature>
31 </div> 31 </div>
32 32
33</div> 33</div>