]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
Add more when deleting a 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 { secondsToTime } from '@shared/core-utils'
8 import {
9 Video,
10 VideoExistInPlaylist,
11 VideoPlaylistCreate,
12 VideoPlaylistElementCreate,
13 VideoPlaylistElementUpdate,
14 VideoPlaylistPrivacy
15 } from '@shared/models'
16 import { VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR } from '../form-validators/video-playlist-validators'
17 import { CachedPlaylist, VideoPlaylistService } from './video-playlist.service'
18
19 const logger = debug('peertube:playlists:VideoAddToPlaylistComponent')
20
21 type PlaylistElement = {
22 enabled: boolean
23 playlistElementId?: number
24 startTimestamp?: number
25 stopTimestamp?: number
26 }
27
28 type PlaylistSummary = {
29 id: number
30 displayName: string
31 optionalRowDisplayed: boolean
32
33 elements: PlaylistElement[]
34 }
35
36 @Component({
37 selector: 'my-video-add-to-playlist',
38 styleUrls: [ './video-add-to-playlist.component.scss' ],
39 templateUrl: './video-add-to-playlist.component.html',
40 changeDetection: ChangeDetectionStrategy.OnPush
41 })
42 export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, OnChanges, OnDestroy, DisableForReuseHook {
43 @Input() video: Video
44 @Input() currentVideoTimestamp: number
45 @Input() lazyLoad = false
46
47 isNewPlaylistBlockOpened = false
48
49 videoPlaylistSearch: string
50 videoPlaylistSearchChanged = new Subject<void>()
51
52 videoPlaylists: PlaylistSummary[] = []
53
54 private disabled = false
55
56 private listenToPlaylistChangeSub: Subscription
57 private playlistsData: CachedPlaylist[] = []
58
59 constructor (
60 protected formValidatorService: FormValidatorService,
61 private authService: AuthService,
62 private notifier: Notifier,
63 private videoPlaylistService: VideoPlaylistService,
64 private cd: ChangeDetectorRef
65 ) {
66 super()
67 }
68
69 get user () {
70 return this.authService.getUser()
71 }
72
73 ngOnInit () {
74 this.buildForm({
75 displayName: VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR
76 })
77
78 this.videoPlaylistService.listenToMyAccountPlaylistsChange()
79 .subscribe(result => {
80 this.playlistsData = result.data
81
82 this.videoPlaylistService.runPlaylistCheck(this.video.id)
83 })
84
85 this.videoPlaylistSearchChanged
86 .pipe(debounceTime(500))
87 .subscribe(() => this.load())
88
89 if (this.lazyLoad === false) this.load()
90 }
91
92 ngOnChanges (simpleChanges: SimpleChanges) {
93 if (simpleChanges['video']) {
94 this.reload()
95 }
96 }
97
98 ngOnDestroy () {
99 this.unsubscribePlaylistChanges()
100 }
101
102 disableForReuse () {
103 this.disabled = true
104 }
105
106 enabledForReuse () {
107 this.disabled = false
108 }
109
110 reload () {
111 logger('Reloading component')
112
113 this.videoPlaylists = []
114 this.videoPlaylistSearch = undefined
115
116 this.load()
117
118 this.cd.markForCheck()
119 }
120
121 load () {
122 logger('Loading component')
123
124 this.listenToVideoPlaylistChange()
125
126 this.videoPlaylistService.listMyPlaylistWithCache(this.user, this.videoPlaylistSearch)
127 .subscribe(playlistsResult => {
128 this.playlistsData = playlistsResult.data
129
130 this.videoPlaylistService.runPlaylistCheck(this.video.id)
131 })
132 }
133
134 openChange (opened: boolean) {
135 if (opened === false) {
136 this.isNewPlaylistBlockOpened = false
137 }
138 }
139
140 openCreateBlock (event: Event) {
141 event.preventDefault()
142
143 this.isNewPlaylistBlockOpened = true
144 }
145
146 toggleMainPlaylist (e: Event, playlist: PlaylistSummary) {
147 e.preventDefault()
148
149 if (this.isPresentMultipleTimes(playlist) || playlist.optionalRowDisplayed) return
150
151 if (playlist.elements.length === 0) {
152 const element: PlaylistElement = {
153 enabled: true,
154 playlistElementId: undefined,
155 startTimestamp: 0,
156 stopTimestamp: this.video.duration
157 }
158
159 this.addVideoInPlaylist(playlist, element)
160 } else {
161 this.removeVideoFromPlaylist(playlist, playlist.elements[0].playlistElementId)
162 playlist.elements = []
163 }
164
165 this.cd.markForCheck()
166 }
167
168 toggleOptionalPlaylist (e: Event, playlist: PlaylistSummary, element: PlaylistElement, startTimestamp: number, stopTimestamp: number) {
169 e.preventDefault()
170
171 if (element.enabled) {
172 this.removeVideoFromPlaylist(playlist, element.playlistElementId)
173 element.enabled = false
174
175 // Hide optional rows pane when the user unchecked all the playlists
176 if (this.isPrimaryCheckboxChecked(playlist) === false) {
177 playlist.optionalRowDisplayed = false
178 }
179 } else {
180 const element: PlaylistElement = {
181 enabled: true,
182 playlistElementId: undefined,
183 startTimestamp,
184 stopTimestamp
185 }
186
187 this.addVideoInPlaylist(playlist, element)
188 }
189
190 this.cd.markForCheck()
191 }
192
193 createPlaylist () {
194 const displayName = this.form.value['displayName']
195
196 const videoPlaylistCreate: VideoPlaylistCreate = {
197 displayName,
198 privacy: VideoPlaylistPrivacy.PRIVATE
199 }
200
201 this.videoPlaylistService.createVideoPlaylist(videoPlaylistCreate)
202 .subscribe({
203 next: () => {
204 this.isNewPlaylistBlockOpened = false
205
206 this.cd.markForCheck()
207 },
208
209 error: err => this.notifier.error(err.message)
210 })
211 }
212
213 onVideoPlaylistSearchChanged () {
214 this.videoPlaylistSearchChanged.next()
215 }
216
217 isPrimaryCheckboxChecked (playlist: PlaylistSummary) {
218 return playlist.elements.filter(e => e.enabled)
219 .length !== 0
220 }
221
222 toggleOptionalRow (playlist: PlaylistSummary) {
223 playlist.optionalRowDisplayed = !playlist.optionalRowDisplayed
224
225 this.cd.markForCheck()
226 }
227
228 getPrimaryInputName (playlist: PlaylistSummary) {
229 return 'in-playlist-primary-' + playlist.id
230 }
231
232 getOptionalInputName (playlist: PlaylistSummary, element?: PlaylistElement) {
233 const suffix = element
234 ? '-' + element.playlistElementId
235 : ''
236
237 return 'in-playlist-optional-' + playlist.id + suffix
238 }
239
240 buildOptionalRowElements (playlist: PlaylistSummary) {
241 const elements = playlist.elements
242
243 const lastElement = elements.length === 0
244 ? undefined
245 : elements[elements.length - 1]
246
247 // Build an empty last element
248 if (!lastElement || lastElement.enabled === true) {
249 elements.push({
250 enabled: false,
251 startTimestamp: 0,
252 stopTimestamp: this.video.duration
253 })
254 }
255
256 return elements
257 }
258
259 isPresentMultipleTimes (playlist: PlaylistSummary) {
260 return playlist.elements.filter(e => e.enabled === true).length > 1
261 }
262
263 onElementTimestampUpdate (playlist: PlaylistSummary, element: PlaylistElement) {
264 if (!element.playlistElementId || element.enabled === false) return
265
266 const body: VideoPlaylistElementUpdate = {
267 startTimestamp: element.startTimestamp,
268 stopTimestamp: element.stopTimestamp
269 }
270
271 this.videoPlaylistService.updateVideoOfPlaylist(playlist.id, element.playlistElementId, body, this.video.id)
272 .subscribe({
273 next: () => {
274 this.notifier.success($localize`Timestamps updated`)
275 },
276
277 error: err => this.notifier.error(err.message),
278
279 complete: () => this.cd.markForCheck()
280 })
281 }
282
283 private isOptionalRowDisplayed (playlist: PlaylistSummary) {
284 const elements = playlist.elements.filter(e => e.enabled)
285
286 if (elements.length > 1) return true
287
288 if (elements.length === 1) {
289 const element = elements[0]
290
291 if (
292 (element.startTimestamp && element.startTimestamp !== 0) ||
293 (element.stopTimestamp && element.stopTimestamp !== this.video.duration)
294 ) {
295 return true
296 }
297 }
298
299 return false
300 }
301
302 private removeVideoFromPlaylist (playlist: PlaylistSummary, elementId: number) {
303 this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, elementId, this.video.id)
304 .subscribe({
305 next: () => {
306 this.notifier.success($localize`Video removed from ${playlist.displayName}`)
307 },
308
309 error: err => this.notifier.error(err.message),
310
311 complete: () => this.cd.markForCheck()
312 })
313 }
314
315 private listenToVideoPlaylistChange () {
316 this.unsubscribePlaylistChanges()
317
318 this.listenToPlaylistChangeSub = this.videoPlaylistService.listenToVideoPlaylistChange(this.video.id)
319 .pipe(filter(() => this.disabled === false))
320 .subscribe(existResult => this.rebuildPlaylists(existResult))
321 }
322
323 private unsubscribePlaylistChanges () {
324 if (this.listenToPlaylistChangeSub) {
325 this.listenToPlaylistChangeSub.unsubscribe()
326 this.listenToPlaylistChangeSub = undefined
327 }
328 }
329
330 private rebuildPlaylists (existResult: VideoExistInPlaylist[]) {
331 logger('Got existing results for %d.', this.video.id, existResult)
332
333 const oldPlaylists = this.videoPlaylists
334
335 this.videoPlaylists = []
336 for (const playlist of this.playlistsData) {
337 const existingPlaylists = existResult.filter(p => p.playlistId === playlist.id)
338
339 const playlistSummary = {
340 id: playlist.id,
341 optionalRowDisplayed: false,
342 displayName: playlist.displayName,
343 elements: existingPlaylists.map(e => ({
344 enabled: true,
345 playlistElementId: e.playlistElementId,
346 startTimestamp: e.startTimestamp || 0,
347 stopTimestamp: e.stopTimestamp || this.video.duration
348 }))
349 }
350
351 const oldPlaylist = oldPlaylists.find(p => p.id === playlist.id)
352 playlistSummary.optionalRowDisplayed = oldPlaylist
353 ? oldPlaylist.optionalRowDisplayed
354 : this.isOptionalRowDisplayed(playlistSummary)
355
356 this.videoPlaylists.push(playlistSummary)
357 }
358
359 logger('Rebuilt playlist state for video %d.', this.video.id, this.videoPlaylists)
360
361 this.cd.markForCheck()
362 }
363
364 private addVideoInPlaylist (playlist: PlaylistSummary, element: PlaylistElement) {
365 const body: VideoPlaylistElementCreate = { videoId: this.video.id }
366
367 if (element.startTimestamp) body.startTimestamp = element.startTimestamp
368 if (element.stopTimestamp && element.stopTimestamp !== this.video.duration) body.stopTimestamp = element.stopTimestamp
369
370 this.videoPlaylistService.addVideoInPlaylist(playlist.id, body)
371 .subscribe({
372 next: res => {
373 const message = body.startTimestamp || body.stopTimestamp
374 ? $localize`Video added in ${playlist.displayName} at timestamps ${this.formatTimestamp(element)}`
375 : $localize`Video added in ${playlist.displayName}`
376
377 this.notifier.success(message)
378
379 if (element) element.playlistElementId = res.videoPlaylistElement.id
380 },
381
382 error: err => this.notifier.error(err.message),
383
384 complete: () => this.cd.markForCheck()
385 })
386 }
387
388 private formatTimestamp (element: PlaylistElement) {
389 const start = element.startTimestamp ? secondsToTime(element.startTimestamp) : ''
390 const stop = element.stopTimestamp ? secondsToTime(element.stopTimestamp) : ''
391
392 return `(${start}-${stop})`
393 }
394 }