aboutsummaryrefslogtreecommitdiffhomepage
path: root/client
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-08-18 15:51:51 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-08-19 11:30:21 +0200
commite79df4eefbeba3e7ab74e17cae1369c80c94b848 (patch)
tree917e22bf85195394e0829ebd962ac5f861009bdf /client
parentcbb513e737bfca3ad3dbd43ad9614968e6e207cf (diff)
downloadPeerTube-e79df4eefbeba3e7ab74e17cae1369c80c94b848.tar.gz
PeerTube-e79df4eefbeba3e7ab74e17cae1369c80c94b848.tar.zst
PeerTube-e79df4eefbeba3e7ab74e17cae1369c80c94b848.zip
Update playlist add component to accept multiple times the same video
Diffstat (limited to 'client')
-rw-r--r--client/src/app/shared/shared-forms/timestamp-input.component.ts6
-rw-r--r--client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html84
-rw-r--r--client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss96
-rw-r--r--client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts223
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist.service.ts3
5 files changed, 285 insertions, 127 deletions
diff --git a/client/src/app/shared/shared-forms/timestamp-input.component.ts b/client/src/app/shared/shared-forms/timestamp-input.component.ts
index 8d67a96ac..0ffd03d02 100644
--- a/client/src/app/shared/shared-forms/timestamp-input.component.ts
+++ b/client/src/app/shared/shared-forms/timestamp-input.component.ts
@@ -1,4 +1,4 @@
1import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core' 1import { ChangeDetectorRef, Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { secondsToTime, timeToInt } from '../../../assets/player/utils' 3import { secondsToTime, timeToInt } from '../../../assets/player/utils'
4 4
@@ -19,6 +19,8 @@ export class TimestampInputComponent implements ControlValueAccessor, OnInit {
19 @Input() timestamp: number 19 @Input() timestamp: number
20 @Input() disabled = false 20 @Input() disabled = false
21 21
22 @Output() inputBlur = new EventEmitter()
23
22 timestampString: string 24 timestampString: string
23 25
24 constructor (private changeDetector: ChangeDetectorRef) {} 26 constructor (private changeDetector: ChangeDetectorRef) {}
@@ -57,5 +59,7 @@ export class TimestampInputComponent implements ControlValueAccessor, OnInit {
57 59
58 this.propagateChange(this.timestamp) 60 this.propagateChange(this.timestamp)
59 } 61 }
62
63 this.inputBlur.emit()
60 } 64 }
61} 65}
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html
index a40e0699e..37d5017cf 100644
--- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html
+++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.html
@@ -2,58 +2,60 @@
2 <div class="header"> 2 <div class="header">
3 <div class="first-row"> 3 <div class="first-row">
4 <div i18n class="title">Save to</div> 4 <div i18n class="title">Save to</div>
5
6 <div class="options" (click)="displayOptions = !displayOptions">
7 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
8
9 <span i18n>Options</span>
10 </div>
11 </div> 5 </div>
6 </div>
12 7
13 <div class="options-row" *ngIf="displayOptions"> 8 <div class="input-container">
14 <div> 9 <input type="text" placeholder="Search playlists" i18n-placeholder [(ngModel)]="videoPlaylistSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" />
15 <my-peertube-checkbox 10 </div>
16 inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled"
17 i18n-labelText labelText="Start at"
18 ></my-peertube-checkbox>
19
20 <my-timestamp-input
21 [timestamp]="timestampOptions.startTimestamp"
22 [maxTimestamp]="video.duration"
23 [disabled]="!timestampOptions.startTimestampEnabled"
24 [(ngModel)]="timestampOptions.startTimestamp"
25 ></my-timestamp-input>
26 </div>
27 11
28 <div> 12 <div class="playlists">
13 <div
14 *ngFor="let playlist of videoPlaylists"
15 class="playlist dropdown-item" [ngClass]="{ 'has-optional-row': playlist.optionalRowDisplayed }"
16 >
17 <div class="primary-row">
29 <my-peertube-checkbox 18 <my-peertube-checkbox
30 inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled" 19 [disabled]="isPresentMultipleTimes(playlist) || playlist.optionalRowDisplayed" [inputName]="getPrimaryInputName(playlist)"
31 i18n-labelText labelText="Stop at" 20 [ngModel]="isPrimaryCheckboxChecked(playlist)" [onPushWorkaround]="true"
21 (click)="toggleMainPlaylist($event, playlist)"
32 ></my-peertube-checkbox> 22 ></my-peertube-checkbox>
33 23
34 <my-timestamp-input 24 <label class="display-name" (click)="toggleMainPlaylist($event, playlist)">
35 [timestamp]="timestampOptions.stopTimestamp" 25 {{ playlist.displayName }}
36 [maxTimestamp]="video.duration" 26 </label>
37 [disabled]="!timestampOptions.stopTimestampEnabled" 27
38 [(ngModel)]="timestampOptions.stopTimestamp" 28 <div class="optional-row-icon" *ngIf="isPrimaryCheckboxChecked(playlist)" (click)="toggleOptionalRow(playlist)">
39 ></my-timestamp-input> 29 <my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
30 </div>
40 </div> 31 </div>
41 </div>
42 </div>
43 32
44 <div class="input-container"> 33 <div class="optional-rows" *ngIf="playlist.optionalRowDisplayed">
45 <input type="text" placeholder="Search playlists" i18n-placeholder [(ngModel)]="videoPlaylistSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" /> 34 <div class="labels">
46 </div> 35 <div i18n>Start at</div>
36 <div i18n>Stop at</div>
37 </div>
47 38
48 <div class="playlists"> 39 <div *ngFor="let element of buildOptionalRowElements(playlist)">
49 <div class="playlist dropdown-item" *ngFor="let playlist of videoPlaylists" (click)="togglePlaylist($event, playlist)"> 40 <my-peertube-checkbox
50 <my-peertube-checkbox [inputName]="'in-playlist-' + playlist.id" [(ngModel)]="playlist.inPlaylist" [onPushWorkaround]="true"></my-peertube-checkbox> 41 [inputName]="getOptionalInputName(playlist, element)"
42 [ngModel]="element.enabled" [onPushWorkaround]="true"
43 (click)="toggleOptionalPlaylist($event, playlist, element, startAt.timestamp, stopAt.timestamp)"
44 ></my-peertube-checkbox>
51 45
52 <div class="display-name"> 46 <my-timestamp-input
53 {{ playlist.displayName }} 47 [maxTimestamp]="video.duration"
48 [(ngModel)]="element.startTimestamp"
49 (inputBlur)="onElementTimestampUpdate(playlist, element)"
50 #startAt
51 ></my-timestamp-input>
54 52
55 <div *ngIf="playlist.inPlaylist && (playlist.startTimestamp || playlist.stopTimestamp)" class="timestamp-info"> 53 <my-timestamp-input
56 {{ formatTimestamp(playlist) }} 54 [maxTimestamp]="video.duration"
55 [(ngModel)]="element.stopTimestamp"
56 (inputBlur)="onElementTimestampUpdate(playlist, element)"
57 #stopAt
58 ></my-timestamp-input>
57 </div> 59 </div>
58 </div> 60 </div>
59 </div> 61 </div>
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss
index cb9ab9a17..d2c8804e3 100644
--- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss
+++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.scss
@@ -1,6 +1,10 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4$optional-rows-checkbox-width: 34px;
5$timestamp-width: 50px;
6$timestamp-margin-right: 10px;
7
4.header, 8.header,
5.dropdown-item, 9.dropdown-item,
6.input-container { 10.input-container {
@@ -24,31 +28,6 @@
24 font-size: 18px; 28 font-size: 18px;
25 flex-grow: 1; 29 flex-grow: 1;
26 } 30 }
27
28 .options {
29 display: flex;
30 align-items: center;
31 font-size: 14px;
32 cursor: pointer;
33
34 my-global-icon {
35 @include apply-svg-color(#333);
36
37 width: 16px;
38 height: 23px;
39 margin-right: 3px;
40 }
41 }
42 }
43
44 .options-row {
45 margin-top: 10px;
46 padding-left: 10px;
47
48 > div {
49 display: flex;
50 align-items: center;
51 }
52 } 31 }
53} 32}
54 33
@@ -58,8 +37,16 @@
58} 37}
59 38
60.playlist { 39.playlist {
61 display: inline-flex; 40 padding: 8px 10px 8px 24px;
62 cursor: pointer; 41
42 &.has-optional-row:hover {
43 background-color: inherit;
44 }
45}
46
47.primary-row,
48.optional-rows > div {
49 display: flex;
63 50
64 my-peertube-checkbox { 51 my-peertube-checkbox {
65 margin-right: 10px; 52 margin-right: 10px;
@@ -69,11 +56,58 @@
69 .display-name { 56 .display-name {
70 display: flex; 57 display: flex;
71 align-items: flex-end; 58 align-items: flex-end;
59 flex-grow: 1;
60 margin: 0;
61 font-weight: $font-regular;
62 cursor: pointer;
63 }
64
65 .optional-row-icon {
66 display: flex;
67 align-items: center;
68 font-size: 14px;
69 cursor: pointer;
70
71 my-global-icon {
72 @include apply-svg-color(#333);
73
74 width: 19px;
75 height: 19px;
76 margin-right: 0;
77 }
78 }
79
80 my-timestamp-input {
81 margin-right: $timestamp-margin-right;
82
83 ::ng-deep .ui-inputtext {
84 padding: 0;
85 width: $timestamp-width;
86 }
87 }
88}
89
90.optional-rows {
91 > div {
92 padding: 8px 5px 5px 10px;
93 }
94
95 my-peertube-checkbox {
96 display: block;
97 width: $optional-rows-checkbox-width;
98 margin-right: 0 !important;
99 }
100
101 .labels {
102 margin-left: $optional-rows-checkbox-width;
103 font-size: 13px;
104 color: pvar(--greyForegroundColor);
105 padding-top: 5px;
106 padding-bottom: 0;
72 107
73 .timestamp-info { 108 div {
74 font-size: 0.9em; 109 margin-right: $timestamp-margin-right;
75 color: pvar(--greyForegroundColor); 110 width: $timestamp-width;
76 margin-left: 5px;
77 } 111 }
78 } 112 }
79} 113}
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
index 41f16e0bf..b6a3408c7 100644
--- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
+++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
@@ -4,21 +4,27 @@ import { debounceTime, filter } from 'rxjs/operators'
4import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core' 4import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'
5import { AuthService, DisableForReuseHook, Notifier } from '@app/core' 5import { AuthService, DisableForReuseHook, Notifier } from '@app/core'
6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
7import { Video, VideoExistInPlaylist, VideoPlaylistCreate, VideoPlaylistElementCreate, VideoPlaylistPrivacy } from '@shared/models' 7import { Video, VideoExistInPlaylist, VideoPlaylistCreate, VideoPlaylistElementCreate, VideoPlaylistPrivacy, VideoPlaylistElementUpdate } from '@shared/models'
8import { secondsToTime } from '../../../assets/player/utils' 8import { secondsToTime } from '../../../assets/player/utils'
9import { VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR } from '../form-validators/video-playlist-validators' 9import { VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR } from '../form-validators/video-playlist-validators'
10import { CachedPlaylist, VideoPlaylistService } from './video-playlist.service' 10import { CachedPlaylist, VideoPlaylistService } from './video-playlist.service'
11import { invoke, last } from 'lodash'
11 12
12const logger = debug('peertube:playlists:VideoAddToPlaylistComponent') 13const logger = debug('peertube:playlists:VideoAddToPlaylistComponent')
13 14
15type PlaylistElement = {
16 enabled: boolean
17 playlistElementId?: number
18 startTimestamp?: number
19 stopTimestamp?: number
20}
21
14type PlaylistSummary = { 22type PlaylistSummary = {
15 id: number 23 id: number
16 inPlaylist: boolean
17 displayName: string 24 displayName: string
25 optionalRowDisplayed: boolean
18 26
19 playlistElementId?: number 27 elements: PlaylistElement[]
20 startTimestamp?: number
21 stopTimestamp?: number
22} 28}
23 29
24@Component({ 30@Component({
@@ -33,16 +39,11 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
33 @Input() lazyLoad = false 39 @Input() lazyLoad = false
34 40
35 isNewPlaylistBlockOpened = false 41 isNewPlaylistBlockOpened = false
42
36 videoPlaylistSearch: string 43 videoPlaylistSearch: string
37 videoPlaylistSearchChanged = new Subject<string>() 44 videoPlaylistSearchChanged = new Subject<string>()
45
38 videoPlaylists: PlaylistSummary[] = [] 46 videoPlaylists: PlaylistSummary[] = []
39 timestampOptions: {
40 startTimestampEnabled: boolean
41 startTimestamp: number
42 stopTimestampEnabled: boolean
43 stopTimestamp: number
44 }
45 displayOptions = false
46 47
47 private disabled = false 48 private disabled = false
48 49
@@ -106,7 +107,6 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
106 this.videoPlaylists = [] 107 this.videoPlaylists = []
107 this.videoPlaylistSearch = undefined 108 this.videoPlaylistSearch = undefined
108 109
109 this.resetOptions(true)
110 this.load() 110 this.load()
111 111
112 this.cd.markForCheck() 112 this.cd.markForCheck()
@@ -115,7 +115,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
115 load () { 115 load () {
116 logger('Loading component') 116 logger('Loading component')
117 117
118 this.listenToPlaylistChanges() 118 this.listenToVideoPlaylistChange()
119 119
120 this.videoPlaylistService.listMyPlaylistWithCache(this.user, this.videoPlaylistSearch) 120 this.videoPlaylistService.listMyPlaylistWithCache(this.user, this.videoPlaylistSearch)
121 .subscribe(playlistsResult => { 121 .subscribe(playlistsResult => {
@@ -128,7 +128,6 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
128 openChange (opened: boolean) { 128 openChange (opened: boolean) {
129 if (opened === false) { 129 if (opened === false) {
130 this.isNewPlaylistBlockOpened = false 130 this.isNewPlaylistBlockOpened = false
131 this.displayOptions = false
132 } 131 }
133 } 132 }
134 133
@@ -138,17 +137,49 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
138 this.isNewPlaylistBlockOpened = true 137 this.isNewPlaylistBlockOpened = true
139 } 138 }
140 139
141 togglePlaylist (event: Event, playlist: PlaylistSummary) { 140 toggleMainPlaylist (e: Event, playlist: PlaylistSummary) {
142 event.preventDefault() 141 e.preventDefault()
142
143 if (this.isPresentMultipleTimes(playlist) || playlist.optionalRowDisplayed) return
143 144
144 if (playlist.inPlaylist === true) { 145 if (playlist.elements.length === 0) {
145 this.removeVideoFromPlaylist(playlist) 146 const element: PlaylistElement = {
147 enabled: true,
148 playlistElementId: undefined,
149 startTimestamp: 0,
150 stopTimestamp: this.video.duration
151 }
152
153 this.addVideoInPlaylist(playlist, element)
146 } else { 154 } else {
147 this.addVideoInPlaylist(playlist) 155 this.removeVideoFromPlaylist(playlist, playlist.elements[0].playlistElementId)
156 playlist.elements = []
148 } 157 }
149 158
150 playlist.inPlaylist = !playlist.inPlaylist 159 this.cd.markForCheck()
151 this.resetOptions() 160 }
161
162 toggleOptionalPlaylist (e: Event, playlist: PlaylistSummary, element: PlaylistElement, startTimestamp: number, stopTimestamp: number) {
163 e.preventDefault()
164
165 if (element.enabled) {
166 this.removeVideoFromPlaylist(playlist, element.playlistElementId)
167 element.enabled = false
168
169 // Hide optional rows pane when the user unchecked all the playlists
170 if (this.isPrimaryCheckboxChecked(playlist) === false) {
171 playlist.optionalRowDisplayed = false
172 }
173 } else {
174 const element: PlaylistElement = {
175 enabled: true,
176 playlistElementId: undefined,
177 startTimestamp,
178 stopTimestamp
179 }
180
181 this.addVideoInPlaylist(playlist, element)
182 }
152 183
153 this.cd.markForCheck() 184 this.cd.markForCheck()
154 } 185 }
@@ -172,34 +203,99 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
172 ) 203 )
173 } 204 }
174 205
175 resetOptions (resetTimestamp = false) { 206 onVideoPlaylistSearchChanged () {
176 this.displayOptions = false 207 this.videoPlaylistSearchChanged.next()
208 }
177 209
178 this.timestampOptions = {} as any 210 isPrimaryCheckboxChecked (playlist: PlaylistSummary) {
179 this.timestampOptions.startTimestampEnabled = false 211 return playlist.elements.filter(e => e.enabled)
180 this.timestampOptions.stopTimestampEnabled = false 212 .length !== 0
213 }
181 214
182 if (resetTimestamp) { 215 toggleOptionalRow (playlist: PlaylistSummary) {
183 this.timestampOptions.startTimestamp = 0 216 playlist.optionalRowDisplayed = !playlist.optionalRowDisplayed
184 this.timestampOptions.stopTimestamp = this.video.duration 217
185 } 218 this.cd.markForCheck()
186 } 219 }
187 220
188 formatTimestamp (playlist: PlaylistSummary) { 221 getPrimaryInputName (playlist: PlaylistSummary) {
189 const start = playlist.startTimestamp ? secondsToTime(playlist.startTimestamp) : '' 222 return 'in-playlist-primary-' + playlist.id
190 const stop = playlist.stopTimestamp ? secondsToTime(playlist.stopTimestamp) : '' 223 }
191 224
192 return `(${start}-${stop})` 225 getOptionalInputName (playlist: PlaylistSummary, element?: PlaylistElement) {
226 const suffix = element
227 ? '-' + element.playlistElementId
228 : ''
229
230 return 'in-playlist-optional-' + playlist.id + suffix
193 } 231 }
194 232
195 onVideoPlaylistSearchChanged () { 233 buildOptionalRowElements (playlist: PlaylistSummary) {
196 this.videoPlaylistSearchChanged.next() 234 const elements = playlist.elements
235
236 const lastElement = elements.length === 0
237 ? undefined
238 : elements[elements.length - 1]
239
240 // Build an empty last element
241 if (!lastElement || lastElement.enabled === true) {
242 elements.push({
243 enabled: false,
244 startTimestamp: 0,
245 stopTimestamp: this.video.duration
246 })
247 }
248
249 return elements
250 }
251
252 isPresentMultipleTimes (playlist: PlaylistSummary) {
253 return playlist.elements.filter(e => e.enabled === true).length > 1
254 }
255
256 onElementTimestampUpdate (playlist: PlaylistSummary, element: PlaylistElement) {
257 if (!element.playlistElementId || element.enabled === false) return
258
259 const body: VideoPlaylistElementUpdate = {
260 startTimestamp: element.startTimestamp,
261 stopTimestamp: element.stopTimestamp
262 }
263
264 this.videoPlaylistService.updateVideoOfPlaylist(playlist.id, element.playlistElementId, body, this.video.id)
265 .subscribe(
266 () => {
267 this.notifier.success($localize`Timestamps updated`)
268 },
269
270 err => {
271 this.notifier.error(err.message)
272 },
273
274 () => this.cd.markForCheck()
275 )
197 } 276 }
198 277
199 private removeVideoFromPlaylist (playlist: PlaylistSummary) { 278 private isOptionalRowDisplayed (playlist: PlaylistSummary) {
200 if (!playlist.playlistElementId) return 279 const elements = playlist.elements.filter(e => e.enabled)
280
281 if (elements.length > 1) return true
201 282
202 this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, playlist.playlistElementId, this.video.id) 283 if (elements.length === 1) {
284 const element = elements[0]
285
286 if (
287 (element.startTimestamp && element.startTimestamp !== 0) ||
288 (element.stopTimestamp && element.stopTimestamp !== this.video.duration)
289 ) {
290 return true
291 }
292 }
293
294 return false
295 }
296
297 private removeVideoFromPlaylist (playlist: PlaylistSummary, elementId: number) {
298 this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, elementId, this.video.id)
203 .subscribe( 299 .subscribe(
204 () => { 300 () => {
205 this.notifier.success($localize`Video removed from ${playlist.displayName}`) 301 this.notifier.success($localize`Video removed from ${playlist.displayName}`)
@@ -213,7 +309,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
213 ) 309 )
214 } 310 }
215 311
216 private listenToPlaylistChanges () { 312 private listenToVideoPlaylistChange () {
217 this.unsubscribePlaylistChanges() 313 this.unsubscribePlaylistChanges()
218 314
219 this.listenToPlaylistChangeSub = this.videoPlaylistService.listenToVideoPlaylistChange(this.video.id) 315 this.listenToPlaylistChangeSub = this.videoPlaylistService.listenToVideoPlaylistChange(this.video.id)
@@ -231,18 +327,30 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
231 private rebuildPlaylists (existResult: VideoExistInPlaylist[]) { 327 private rebuildPlaylists (existResult: VideoExistInPlaylist[]) {
232 logger('Got existing results for %d.', this.video.id, existResult) 328 logger('Got existing results for %d.', this.video.id, existResult)
233 329
330 const oldPlaylists = this.videoPlaylists
331
234 this.videoPlaylists = [] 332 this.videoPlaylists = []
235 for (const playlist of this.playlistsData) { 333 for (const playlist of this.playlistsData) {
236 const existingPlaylist = existResult.find(p => p.playlistId === playlist.id) 334 const existingPlaylists = existResult.filter(p => p.playlistId === playlist.id)
237 335
238 this.videoPlaylists.push({ 336 const playlistSummary = {
239 id: playlist.id, 337 id: playlist.id,
338 optionalRowDisplayed: false,
240 displayName: playlist.displayName, 339 displayName: playlist.displayName,
241 inPlaylist: !!existingPlaylist, 340 elements: existingPlaylists.map(e => ({
242 playlistElementId: existingPlaylist ? existingPlaylist.playlistElementId : undefined, 341 enabled: true,
243 startTimestamp: existingPlaylist ? existingPlaylist.startTimestamp : undefined, 342 playlistElementId: e.playlistElementId,
244 stopTimestamp: existingPlaylist ? existingPlaylist.stopTimestamp : undefined 343 startTimestamp: e.startTimestamp || 0,
245 }) 344 stopTimestamp: e.stopTimestamp || this.video.duration
345 }))
346 }
347
348 const oldPlaylist = oldPlaylists.find(p => p.id === playlist.id)
349 playlistSummary.optionalRowDisplayed = oldPlaylist
350 ? oldPlaylist.optionalRowDisplayed
351 : this.isOptionalRowDisplayed(playlistSummary)
352
353 this.videoPlaylists.push(playlistSummary)
246 } 354 }
247 355
248 logger('Rebuilt playlist state for video %d.', this.video.id, this.videoPlaylists) 356 logger('Rebuilt playlist state for video %d.', this.video.id, this.videoPlaylists)
@@ -250,20 +358,22 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
250 this.cd.markForCheck() 358 this.cd.markForCheck()
251 } 359 }
252 360
253 private addVideoInPlaylist (playlist: PlaylistSummary) { 361 private addVideoInPlaylist (playlist: PlaylistSummary, element: PlaylistElement) {
254 const body: VideoPlaylistElementCreate = { videoId: this.video.id } 362 const body: VideoPlaylistElementCreate = { videoId: this.video.id }
255 363
256 if (this.timestampOptions.startTimestampEnabled) body.startTimestamp = this.timestampOptions.startTimestamp 364 if (element.startTimestamp) body.startTimestamp = element.startTimestamp
257 if (this.timestampOptions.stopTimestampEnabled) body.stopTimestamp = this.timestampOptions.stopTimestamp 365 if (element.stopTimestamp && element.stopTimestamp !== this.video.duration) body.stopTimestamp = element.stopTimestamp
258 366
259 this.videoPlaylistService.addVideoInPlaylist(playlist.id, body) 367 this.videoPlaylistService.addVideoInPlaylist(playlist.id, body)
260 .subscribe( 368 .subscribe(
261 () => { 369 res => {
262 const message = body.startTimestamp || body.stopTimestamp 370 const message = body.startTimestamp || body.stopTimestamp
263 ? $localize`Video added in ${playlist.displayName} at timestamps ${this.formatTimestamp(playlist)}` 371 ? $localize`Video added in ${playlist.displayName} at timestamps ${this.formatTimestamp(element)}`
264 : $localize`Video added in ${playlist.displayName}` 372 : $localize`Video added in ${playlist.displayName}`
265 373
266 this.notifier.success(message) 374 this.notifier.success(message)
375
376 if (element) element.playlistElementId = res.videoPlaylistElement.id
267 }, 377 },
268 378
269 err => { 379 err => {
@@ -273,4 +383,11 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
273 () => this.cd.markForCheck() 383 () => this.cd.markForCheck()
274 ) 384 )
275 } 385 }
386
387 private formatTimestamp (element: PlaylistElement) {
388 const start = element.startTimestamp ? secondsToTime(element.startTimestamp) : ''
389 const stop = element.stopTimestamp ? secondsToTime(element.stopTimestamp) : ''
390
391 return `(${start}-${stop})`
392 }
276} 393}
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
index dc1b56129..1b87e0b2a 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
@@ -1,7 +1,7 @@
1import * as debug from 'debug' 1import * as debug from 'debug'
2import { uniq } from 'lodash-es' 2import { uniq } from 'lodash-es'
3import { asyncScheduler, merge, Observable, of, ReplaySubject, Subject } from 'rxjs' 3import { asyncScheduler, merge, Observable, of, ReplaySubject, Subject } from 'rxjs'
4import { bufferTime, catchError, filter, map, observeOn, share, switchMap, tap } from 'rxjs/operators' 4import { bufferTime, catchError, filter, map, observeOn, share, switchMap, tap, distinctUntilChanged } from 'rxjs/operators'
5import { HttpClient, HttpParams } from '@angular/common/http' 5import { HttpClient, HttpParams } from '@angular/common/http'
6import { Injectable, NgZone } from '@angular/core' 6import { Injectable, NgZone } from '@angular/core'
7import { AuthUser, ComponentPaginationLight, RestExtractor, RestService, ServerService } from '@app/core' 7import { AuthUser, ComponentPaginationLight, RestExtractor, RestService, ServerService } from '@app/core'
@@ -53,6 +53,7 @@ export class VideoPlaylistService {
53 ) { 53 ) {
54 this.videoExistsInPlaylistObservable = merge( 54 this.videoExistsInPlaylistObservable = merge(
55 this.videoExistsInPlaylistNotifier.pipe( 55 this.videoExistsInPlaylistNotifier.pipe(
56 distinctUntilChanged(),
56 // We leave Angular zone so Protractor does not get stuck 57 // We leave Angular zone so Protractor does not get stuck
57 bufferTime(500, leaveZone(this.ngZone, asyncScheduler)), 58 bufferTime(500, leaveZone(this.ngZone, asyncScheduler)),
58 filter(videoIds => videoIds.length !== 0), 59 filter(videoIds => videoIds.length !== 0),