aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared/video
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/video
parent693263e936763a851e3c8c020e3739def8bd4eca (diff)
downloadPeerTube-3a0fb65c61f80b510bce979a45d59d17948745e8.tar.gz
PeerTube-3a0fb65c61f80b510bce979a45d59d17948745e8.tar.zst
PeerTube-3a0fb65c61f80b510bce979a45d59d17948745e8.zip
Add video miniature dropdown
Diffstat (limited to 'client/src/app/shared/video')
-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
20 files changed, 835 insertions, 76 deletions
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">