]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
Update playlist add component to accept multiple times the same video
[github/Chocobozzz/PeerTube.git] / client / src / app / shared / shared-video-playlist / video-add-to-playlist.component.ts
1 import * as debug from 'debug'
2 import { Subject, Subscription } from 'rxjs'
3 import { debounceTime, filter } from 'rxjs/operators'
4 import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'
5 import { AuthService, DisableForReuseHook, Notifier } from '@app/core'
6 import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
7 import { Video, VideoExistInPlaylist, VideoPlaylistCreate, VideoPlaylistElementCreate, VideoPlaylistPrivacy, VideoPlaylistElementUpdate } from '@shared/models'
8 import { secondsToTime } from '../../../assets/player/utils'
9 import { VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR } from '../form-validators/video-playlist-validators'
10 import { CachedPlaylist, VideoPlaylistService } from './video-playlist.service'
11 import { invoke, last } from 'lodash'
12
13 const logger = debug('peertube:playlists:VideoAddToPlaylistComponent')
14
15 type PlaylistElement = {
16 enabled: boolean
17 playlistElementId?: number
18 startTimestamp?: number
19 stopTimestamp?: number
20 }
21
22 type PlaylistSummary = {
23 id: number
24 displayName: string
25 optionalRowDisplayed: boolean
26
27 elements: PlaylistElement[]
28 }
29
30 @Component({
31 selector: 'my-video-add-to-playlist',
32 styleUrls: [ './video-add-to-playlist.component.scss' ],
33 templateUrl: './video-add-to-playlist.component.html',
34 changeDetection: ChangeDetectionStrategy.OnPush
35 })
36 export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, OnChanges, OnDestroy, DisableForReuseHook {
37 @Input() video: Video
38 @Input() currentVideoTimestamp: number
39 @Input() lazyLoad = false
40
41 isNewPlaylistBlockOpened = false
42
43 videoPlaylistSearch: string
44 videoPlaylistSearchChanged = new Subject<string>()
45
46 videoPlaylists: PlaylistSummary[] = []
47
48 private disabled = false
49
50 private listenToPlaylistChangeSub: Subscription
51 private playlistsData: CachedPlaylist[] = []
52
53 constructor (
54 protected formValidatorService: FormValidatorService,
55 private authService: AuthService,
56 private notifier: Notifier,
57 private videoPlaylistService: VideoPlaylistService,
58 private cd: ChangeDetectorRef
59 ) {
60 super()
61 }
62
63 get user () {
64 return this.authService.getUser()
65 }
66
67 ngOnInit () {
68 this.buildForm({
69 displayName: VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR
70 })
71
72 this.videoPlaylistService.listenToMyAccountPlaylistsChange()
73 .subscribe(result => {
74 this.playlistsData = result.data
75
76 this.videoPlaylistService.runPlaylistCheck(this.video.id)
77 })
78
79 this.videoPlaylistSearchChanged
80 .pipe(debounceTime(500))
81 .subscribe(() => this.load())
82
83 if (this.lazyLoad === false) this.load()
84 }
85
86 ngOnChanges (simpleChanges: SimpleChanges) {
87 if (simpleChanges['video']) {
88 this.reload()
89 }
90 }
91
92 ngOnDestroy () {
93 this.unsubscribePlaylistChanges()
94 }
95
96 disableForReuse () {
97 this.disabled = true
98 }
99
100 enabledForReuse () {
101 this.disabled = false
102 }
103
104 reload () {
105 logger('Reloading component')
106
107 this.videoPlaylists = []
108 this.videoPlaylistSearch = undefined
109
110 this.load()
111
112 this.cd.markForCheck()
113 }
114
115 load () {
116 logger('Loading component')
117
118 this.listenToVideoPlaylistChange()
119
120 this.videoPlaylistService.listMyPlaylistWithCache(this.user, this.videoPlaylistSearch)
121 .subscribe(playlistsResult => {
122 this.playlistsData = playlistsResult.data
123
124 this.videoPlaylistService.runPlaylistCheck(this.video.id)
125 })
126 }
127
128 openChange (opened: boolean) {
129 if (opened === false) {
130 this.isNewPlaylistBlockOpened = false
131 }
132 }
133
134 openCreateBlock (event: Event) {
135 event.preventDefault()
136
137 this.isNewPlaylistBlockOpened = true
138 }
139
140 toggleMainPlaylist (e: Event, playlist: PlaylistSummary) {
141 e.preventDefault()
142
143 if (this.isPresentMultipleTimes(playlist) || playlist.optionalRowDisplayed) return
144
145 if (playlist.elements.length === 0) {
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)
154 } else {
155 this.removeVideoFromPlaylist(playlist, playlist.elements[0].playlistElementId)
156 playlist.elements = []
157 }
158
159 this.cd.markForCheck()
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 }
183
184 this.cd.markForCheck()
185 }
186
187 createPlaylist () {
188 const displayName = this.form.value[ 'displayName' ]
189
190 const videoPlaylistCreate: VideoPlaylistCreate = {
191 displayName,
192 privacy: VideoPlaylistPrivacy.PRIVATE
193 }
194
195 this.videoPlaylistService.createVideoPlaylist(videoPlaylistCreate).subscribe(
196 () => {
197 this.isNewPlaylistBlockOpened = false
198
199 this.cd.markForCheck()
200 },
201
202 err => this.notifier.error(err.message)
203 )
204 }
205
206 onVideoPlaylistSearchChanged () {
207 this.videoPlaylistSearchChanged.next()
208 }
209
210 isPrimaryCheckboxChecked (playlist: PlaylistSummary) {
211 return playlist.elements.filter(e => e.enabled)
212 .length !== 0
213 }
214
215 toggleOptionalRow (playlist: PlaylistSummary) {
216 playlist.optionalRowDisplayed = !playlist.optionalRowDisplayed
217
218 this.cd.markForCheck()
219 }
220
221 getPrimaryInputName (playlist: PlaylistSummary) {
222 return 'in-playlist-primary-' + playlist.id
223 }
224
225 getOptionalInputName (playlist: PlaylistSummary, element?: PlaylistElement) {
226 const suffix = element
227 ? '-' + element.playlistElementId
228 : ''
229
230 return 'in-playlist-optional-' + playlist.id + suffix
231 }
232
233 buildOptionalRowElements (playlist: PlaylistSummary) {
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 )
276 }
277
278 private isOptionalRowDisplayed (playlist: PlaylistSummary) {
279 const elements = playlist.elements.filter(e => e.enabled)
280
281 if (elements.length > 1) return true
282
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)
299 .subscribe(
300 () => {
301 this.notifier.success($localize`Video removed from ${playlist.displayName}`)
302 },
303
304 err => {
305 this.notifier.error(err.message)
306 },
307
308 () => this.cd.markForCheck()
309 )
310 }
311
312 private listenToVideoPlaylistChange () {
313 this.unsubscribePlaylistChanges()
314
315 this.listenToPlaylistChangeSub = this.videoPlaylistService.listenToVideoPlaylistChange(this.video.id)
316 .pipe(filter(() => this.disabled === false))
317 .subscribe(existResult => this.rebuildPlaylists(existResult))
318 }
319
320 private unsubscribePlaylistChanges () {
321 if (this.listenToPlaylistChangeSub) {
322 this.listenToPlaylistChangeSub.unsubscribe()
323 this.listenToPlaylistChangeSub = undefined
324 }
325 }
326
327 private rebuildPlaylists (existResult: VideoExistInPlaylist[]) {
328 logger('Got existing results for %d.', this.video.id, existResult)
329
330 const oldPlaylists = this.videoPlaylists
331
332 this.videoPlaylists = []
333 for (const playlist of this.playlistsData) {
334 const existingPlaylists = existResult.filter(p => p.playlistId === playlist.id)
335
336 const playlistSummary = {
337 id: playlist.id,
338 optionalRowDisplayed: false,
339 displayName: playlist.displayName,
340 elements: existingPlaylists.map(e => ({
341 enabled: true,
342 playlistElementId: e.playlistElementId,
343 startTimestamp: e.startTimestamp || 0,
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)
354 }
355
356 logger('Rebuilt playlist state for video %d.', this.video.id, this.videoPlaylists)
357
358 this.cd.markForCheck()
359 }
360
361 private addVideoInPlaylist (playlist: PlaylistSummary, element: PlaylistElement) {
362 const body: VideoPlaylistElementCreate = { videoId: this.video.id }
363
364 if (element.startTimestamp) body.startTimestamp = element.startTimestamp
365 if (element.stopTimestamp && element.stopTimestamp !== this.video.duration) body.stopTimestamp = element.stopTimestamp
366
367 this.videoPlaylistService.addVideoInPlaylist(playlist.id, body)
368 .subscribe(
369 res => {
370 const message = body.startTimestamp || body.stopTimestamp
371 ? $localize`Video added in ${playlist.displayName} at timestamps ${this.formatTimestamp(element)}`
372 : $localize`Video added in ${playlist.displayName}`
373
374 this.notifier.success(message)
375
376 if (element) element.playlistElementId = res.videoPlaylistElement.id
377 },
378
379 err => {
380 this.notifier.error(err.message)
381 },
382
383 () => this.cd.markForCheck()
384 )
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 }
393 }