aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared
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 /client/src/app/shared
parent693263e936763a851e3c8c020e3739def8bd4eca (diff)
downloadPeerTube-3a0fb65c61f80b510bce979a45d59d17948745e8.tar.gz
PeerTube-3a0fb65c61f80b510bce979a45d59d17948745e8.tar.zst
PeerTube-3a0fb65c61f80b510bce979a45d59d17948745e8.zip
Add video miniature dropdown
Diffstat (limited to 'client/src/app/shared')
-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.html38
-rw-r--r--client/src/app/shared/video/modals/video-blacklist.component.scss6
-rw-r--r--client/src/app/shared/video/modals/video-blacklist.component.ts76
-rw-r--r--client/src/app/shared/video/modals/video-download.component.html52
-rw-r--r--client/src/app/shared/video/modals/video-download.component.scss25
-rw-r--r--client/src/app/shared/video/modals/video-download.component.ts69
-rw-r--r--client/src/app/shared/video/modals/video-report.component.html36
-rw-r--r--client/src/app/shared/video/modals/video-report.component.scss10
-rw-r--r--client/src/app/shared/video/modals/video-report.component.ts81
-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
28 files changed, 978 insertions, 147 deletions
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/shared/video/modals/video-blacklist.component.html b/client/src/app/shared/video/modals/video-blacklist.component.html
new file mode 100644
index 000000000..1a87bdcd4
--- /dev/null
+++ b/client/src/app/shared/video/modals/video-blacklist.component.html
@@ -0,0 +1,38 @@
1<ng-template #modal>
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Blacklist video</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">
8
9 <form novalidate [formGroup]="form" (ngSubmit)="blacklist()">
10 <div class="form-group">
11 <textarea i18n-placeholder placeholder="Reason..." formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }">
12 </textarea>
13 <div *ngIf="formErrors.reason" class="form-error">
14 {{ formErrors.reason }}
15 </div>
16 </div>
17
18 <div class="form-group" *ngIf="video.isLocal">
19 <my-peertube-checkbox
20 inputName="unfederate" formControlName="unfederate"
21 i18n-labelText labelText="Unfederate the video (ask for its deletion from the remote instances)"
22 ></my-peertube-checkbox>
23 </div>
24
25 <div class="form-group inputs">
26 <span i18n class="action-button action-button-cancel" (click)="hide()">
27 Cancel
28 </span>
29
30 <input
31 type="submit" i18n-value value="Submit" class="action-button-submit"
32 [disabled]="!form.valid"
33 >
34 </div>
35 </form>
36
37 </div>
38</ng-template>
diff --git a/client/src/app/shared/video/modals/video-blacklist.component.scss b/client/src/app/shared/video/modals/video-blacklist.component.scss
new file mode 100644
index 000000000..afcdb9a16
--- /dev/null
+++ b/client/src/app/shared/video/modals/video-blacklist.component.scss
@@ -0,0 +1,6 @@
1@import 'variables';
2@import 'mixins';
3
4textarea {
5 @include peertube-textarea(100%, 100px);
6}
diff --git a/client/src/app/shared/video/modals/video-blacklist.component.ts b/client/src/app/shared/video/modals/video-blacklist.component.ts
new file mode 100644
index 000000000..4e4e8dc50
--- /dev/null
+++ b/client/src/app/shared/video/modals/video-blacklist.component.ts
@@ -0,0 +1,76 @@
1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier, RedirectService } from '@app/core'
3import { VideoBlacklistService } from '../../../shared/video-blacklist'
4import { VideoDetails } from '../../../shared/video/video-details.model'
5import { I18n } from '@ngx-translate/i18n-polyfill'
6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
9import { FormReactive, VideoBlacklistValidatorsService } from '@app/shared/forms'
10
11@Component({
12 selector: 'my-video-blacklist',
13 templateUrl: './video-blacklist.component.html',
14 styleUrls: [ './video-blacklist.component.scss' ]
15})
16export class VideoBlacklistComponent extends FormReactive implements OnInit {
17 @Input() video: VideoDetails = null
18
19 @ViewChild('modal') modal: NgbModal
20
21 @Output() videoBlacklisted = new EventEmitter()
22
23 error: string = null
24
25 private openedModal: NgbModalRef
26
27 constructor (
28 protected formValidatorService: FormValidatorService,
29 private modalService: NgbModal,
30 private videoBlacklistValidatorsService: VideoBlacklistValidatorsService,
31 private videoBlacklistService: VideoBlacklistService,
32 private notifier: Notifier,
33 private redirectService: RedirectService,
34 private i18n: I18n
35 ) {
36 super()
37 }
38
39 ngOnInit () {
40 const defaultValues = { unfederate: 'true' }
41
42 this.buildForm({
43 reason: this.videoBlacklistValidatorsService.VIDEO_BLACKLIST_REASON,
44 unfederate: null
45 }, defaultValues)
46 }
47
48 show () {
49 this.openedModal = this.modalService.open(this.modal, { keyboard: false })
50 }
51
52 hide () {
53 this.openedModal.close()
54 this.openedModal = null
55 }
56
57 blacklist () {
58 const reason = this.form.value[ 'reason' ] || undefined
59 const unfederate = this.video.isLocal ? this.form.value[ 'unfederate' ] : undefined
60
61 this.videoBlacklistService.blacklistVideo(this.video.id, reason, unfederate)
62 .subscribe(
63 () => {
64 this.notifier.success(this.i18n('Video blacklisted.'))
65 this.hide()
66
67 this.video.blacklisted = true
68 this.video.blacklistedReason = reason
69
70 this.videoBlacklisted.emit()
71 },
72
73 err => this.notifier.error(err.message)
74 )
75 }
76}
diff --git a/client/src/app/shared/video/modals/video-download.component.html b/client/src/app/shared/video/modals/video-download.component.html
new file mode 100644
index 000000000..2bb5d6d37
--- /dev/null
+++ b/client/src/app/shared/video/modals/video-download.component.html
@@ -0,0 +1,52 @@
1<ng-template #modal let-hide="close">
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Download video</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">
8 <div class="form-group">
9 <div class="input-group input-group-sm">
10 <div class="input-group-prepend peertube-select-container">
11 <select [(ngModel)]="resolutionId">
12 <option *ngFor="let file of video.files" [value]="file.resolution.id">{{ file.resolution.label }}</option>
13 </select>
14 </div>
15 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" />
16 <div class="input-group-append">
17 <button [ngxClipboard]="urlInput" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary">
18 <span class="glyphicon glyphicon-copy"></span>
19 </button>
20 </div>
21 </div>
22 </div>
23
24 <div class="download-type">
25 <div class="peertube-radio-container">
26 <input type="radio" name="download" id="download-direct" [(ngModel)]="downloadType" value="direct">
27 <label i18n for="download-direct">Direct download</label>
28 </div>
29
30 <div class="peertube-radio-container">
31 <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent">
32 <label i18n for="download-torrent">Torrent (.torrent file)</label>
33 </div>
34
35 <div class="peertube-radio-container">
36 <input type="radio" name="download" id="download-magnet" [(ngModel)]="downloadType" value="magnet">
37 <label i18n for="download-magnet">Torrent (magnet link)</label>
38 </div>
39 </div>
40 </div>
41
42 <div class="modal-footer inputs">
43 <span i18n class="action-button action-button-cancel" (click)="hide()">
44 Cancel
45 </span>
46
47 <input
48 type="submit" i18n-value value="Download" class="action-button-submit"
49 (click)="download()"
50 >
51 </div>
52</ng-template>
diff --git a/client/src/app/shared/video/modals/video-download.component.scss b/client/src/app/shared/video/modals/video-download.component.scss
new file mode 100644
index 000000000..3e826c3b6
--- /dev/null
+++ b/client/src/app/shared/video/modals/video-download.component.scss
@@ -0,0 +1,25 @@
1@import 'variables';
2@import 'mixins';
3
4.peertube-select-container {
5 @include peertube-select-container(100px);
6
7 border-top-right-radius: 0;
8 border-bottom-right-radius: 0;
9 border-right: none;
10
11 select {
12 height: inherit;
13 }
14}
15
16.download-type {
17 margin-top: 30px;
18
19 .peertube-radio-container {
20 @include peertube-radio-container;
21
22 display: inline-block;
23 margin-right: 30px;
24 }
25}
diff --git a/client/src/app/shared/video/modals/video-download.component.ts b/client/src/app/shared/video/modals/video-download.component.ts
new file mode 100644
index 000000000..64aaeb3c8
--- /dev/null
+++ b/client/src/app/shared/video/modals/video-download.component.ts
@@ -0,0 +1,69 @@
1import { Component, ElementRef, ViewChild } from '@angular/core'
2import { VideoDetails } from '../../../shared/video/video-details.model'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { Notifier } from '@app/core'
6
7@Component({
8 selector: 'my-video-download',
9 templateUrl: './video-download.component.html',
10 styleUrls: [ './video-download.component.scss' ]
11})
12export class VideoDownloadComponent {
13 @ViewChild('modal') modal: ElementRef
14
15 downloadType: 'direct' | 'torrent' | 'magnet' = 'torrent'
16 resolutionId: number | string = -1
17
18 private video: VideoDetails
19
20 constructor (
21 private notifier: Notifier,
22 private modalService: NgbModal,
23 private i18n: I18n
24 ) { }
25
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
33 this.resolutionId = this.video.files[0].resolution.id
34 }
35
36 onClose () {
37 this.video = undefined
38 }
39
40 download () {
41 window.location.assign(this.getLink())
42 }
43
44 getLink () {
45 // HTML select send us a string, so convert it to a number
46 this.resolutionId = parseInt(this.resolutionId.toString(), 10)
47
48 const file = this.video.files.find(f => f.resolution.id === this.resolutionId)
49 if (!file) {
50 console.error('Could not find file with resolution %d.', this.resolutionId)
51 return
52 }
53
54 switch (this.downloadType) {
55 case 'direct':
56 return file.fileDownloadUrl
57
58 case 'torrent':
59 return file.torrentDownloadUrl
60
61 case 'magnet':
62 return file.magnetUri
63 }
64 }
65
66 activateCopiedMessage () {
67 this.notifier.success(this.i18n('Copied'))
68 }
69}
diff --git a/client/src/app/shared/video/modals/video-report.component.html b/client/src/app/shared/video/modals/video-report.component.html
new file mode 100644
index 000000000..b9434da26
--- /dev/null
+++ b/client/src/app/shared/video/modals/video-report.component.html
@@ -0,0 +1,36 @@
1<ng-template #modal>
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Report video</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">
8
9 <div i18n class="information">
10 Your report will be sent to moderators of {{ currentHost }}.
11 <ng-container *ngIf="isRemoteVideo()"> It will be forwarded to origin instance {{ originHost }} too.</ng-container>
12 </div>
13
14 <form novalidate [formGroup]="form" (ngSubmit)="report()">
15 <div class="form-group">
16 <textarea i18n-placeholder placeholder="Reason..." formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }">
17 </textarea>
18 <div *ngIf="formErrors.reason" class="form-error">
19 {{ formErrors.reason }}
20 </div>
21 </div>
22
23 <div class="form-group inputs">
24 <span i18n class="action-button action-button-cancel" (click)="hide()">
25 Cancel
26 </span>
27
28 <input
29 type="submit" i18n-value value="Submit" class="action-button-submit"
30 [disabled]="!form.valid"
31 >
32 </div>
33 </form>
34
35 </div>
36</ng-template>
diff --git a/client/src/app/shared/video/modals/video-report.component.scss b/client/src/app/shared/video/modals/video-report.component.scss
new file mode 100644
index 000000000..4713660a2
--- /dev/null
+++ b/client/src/app/shared/video/modals/video-report.component.scss
@@ -0,0 +1,10 @@
1@import 'variables';
2@import 'mixins';
3
4.information {
5 margin-bottom: 20px;
6}
7
8textarea {
9 @include peertube-textarea(100%, 100px);
10}
diff --git a/client/src/app/shared/video/modals/video-report.component.ts b/client/src/app/shared/video/modals/video-report.component.ts
new file mode 100644
index 000000000..725dd020f
--- /dev/null
+++ b/client/src/app/shared/video/modals/video-report.component.ts
@@ -0,0 +1,81 @@
1import { Component, Input, OnInit, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core'
3import { FormReactive } from '../../../shared/forms'
4import { VideoDetails } from '../../../shared/video/video-details.model'
5import { I18n } from '@ngx-translate/i18n-polyfill'
6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
7import { VideoAbuseValidatorsService } from '@app/shared/forms/form-validators/video-abuse-validators.service'
8import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
9import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
10import { VideoAbuseService } from '@app/shared/video-abuse'
11
12@Component({
13 selector: 'my-video-report',
14 templateUrl: './video-report.component.html',
15 styleUrls: [ './video-report.component.scss' ]
16})
17export class VideoReportComponent extends FormReactive implements OnInit {
18 @Input() video: VideoDetails = null
19
20 @ViewChild('modal') modal: NgbModal
21
22 error: string = null
23
24 private openedModal: NgbModalRef
25
26 constructor (
27 protected formValidatorService: FormValidatorService,
28 private modalService: NgbModal,
29 private videoAbuseValidatorsService: VideoAbuseValidatorsService,
30 private videoAbuseService: VideoAbuseService,
31 private notifier: Notifier,
32 private i18n: I18n
33 ) {
34 super()
35 }
36
37 get currentHost () {
38 return window.location.host
39 }
40
41 get originHost () {
42 if (this.isRemoteVideo()) {
43 return this.video.account.host
44 }
45
46 return ''
47 }
48
49 ngOnInit () {
50 this.buildForm({
51 reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON
52 })
53 }
54
55 show () {
56 this.openedModal = this.modalService.open(this.modal, { keyboard: false })
57 }
58
59 hide () {
60 this.openedModal.close()
61 this.openedModal = null
62 }
63
64 report () {
65 const reason = this.form.value['reason']
66
67 this.videoAbuseService.reportVideo(this.video.id, reason)
68 .subscribe(
69 () => {
70 this.notifier.success(this.i18n('Video reported.'))
71 this.hide()
72 },
73
74 err => this.notifier.error(err.message)
75 )
76 }
77
78 isRemoteVideo () {
79 return !this.video.isLocal
80 }
81}
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">