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