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'
12 VideoPlaylistElementCreate,
13 VideoPlaylistElementUpdate,
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'
19 const logger = debug('peertube:playlists:VideoAddToPlaylistComponent')
21 type PlaylistElement = {
23 playlistElementId?: number
24 startTimestamp?: number
25 stopTimestamp?: number
28 type PlaylistSummary = {
31 optionalRowDisplayed: boolean
33 elements: PlaylistElement[]
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
42 export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, OnChanges, OnDestroy, DisableForReuseHook {
44 @Input() currentVideoTimestamp: number
45 @Input() lazyLoad = false
47 isNewPlaylistBlockOpened = false
49 videoPlaylistSearch: string
50 videoPlaylistSearchChanged = new Subject<void>()
52 videoPlaylists: PlaylistSummary[] = []
54 private disabled = false
56 private listenToPlaylistChangeSub: Subscription
57 private playlistsData: CachedPlaylist[] = []
60 protected formValidatorService: FormValidatorService,
61 private authService: AuthService,
62 private notifier: Notifier,
63 private videoPlaylistService: VideoPlaylistService,
64 private cd: ChangeDetectorRef
70 return this.authService.getUser()
75 displayName: VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR
78 this.videoPlaylistService.listenToMyAccountPlaylistsChange()
79 .subscribe(result => {
80 this.playlistsData = result.data
82 this.videoPlaylistService.runPlaylistCheck(this.video.id)
85 this.videoPlaylistSearchChanged
86 .pipe(debounceTime(500))
87 .subscribe(() => this.load())
89 if (this.lazyLoad === false) this.load()
92 ngOnChanges (simpleChanges: SimpleChanges) {
93 if (simpleChanges['video']) {
99 this.unsubscribePlaylistChanges()
107 this.disabled = false
111 logger('Reloading component')
113 this.videoPlaylists = []
114 this.videoPlaylistSearch = undefined
118 this.cd.markForCheck()
122 logger('Loading component')
124 this.listenToVideoPlaylistChange()
126 this.videoPlaylistService.listMyPlaylistWithCache(this.user, this.videoPlaylistSearch)
127 .subscribe(playlistsResult => {
128 this.playlistsData = playlistsResult.data
130 this.videoPlaylistService.runPlaylistCheck(this.video.id)
134 openChange (opened: boolean) {
135 if (opened === false) {
136 this.isNewPlaylistBlockOpened = false
140 openCreateBlock (event: Event) {
141 event.preventDefault()
143 this.isNewPlaylistBlockOpened = true
146 toggleMainPlaylist (e: Event, playlist: PlaylistSummary) {
149 if (this.isPresentMultipleTimes(playlist) || playlist.optionalRowDisplayed) return
151 if (playlist.elements.length === 0) {
152 const element: PlaylistElement = {
154 playlistElementId: undefined,
156 stopTimestamp: this.video.duration
159 this.addVideoInPlaylist(playlist, element)
161 this.removeVideoFromPlaylist(playlist, playlist.elements[0].playlistElementId)
162 playlist.elements = []
165 this.cd.markForCheck()
168 toggleOptionalPlaylist (e: Event, playlist: PlaylistSummary, element: PlaylistElement, startTimestamp: number, stopTimestamp: number) {
171 if (element.enabled) {
172 this.removeVideoFromPlaylist(playlist, element.playlistElementId)
173 element.enabled = false
175 // Hide optional rows pane when the user unchecked all the playlists
176 if (this.isPrimaryCheckboxChecked(playlist) === false) {
177 playlist.optionalRowDisplayed = false
180 const element: PlaylistElement = {
182 playlistElementId: undefined,
187 this.addVideoInPlaylist(playlist, element)
190 this.cd.markForCheck()
194 const displayName = this.form.value['displayName']
196 const videoPlaylistCreate: VideoPlaylistCreate = {
198 privacy: VideoPlaylistPrivacy.PRIVATE
201 this.videoPlaylistService.createVideoPlaylist(videoPlaylistCreate)
204 this.isNewPlaylistBlockOpened = false
206 this.cd.markForCheck()
209 error: err => this.notifier.error(err.message)
213 onVideoPlaylistSearchChanged () {
214 this.videoPlaylistSearchChanged.next()
217 isPrimaryCheckboxChecked (playlist: PlaylistSummary) {
218 return playlist.elements.filter(e => e.enabled)
222 toggleOptionalRow (playlist: PlaylistSummary) {
223 playlist.optionalRowDisplayed = !playlist.optionalRowDisplayed
225 this.cd.markForCheck()
228 getPrimaryInputName (playlist: PlaylistSummary) {
229 return 'in-playlist-primary-' + playlist.id
232 getOptionalInputName (playlist: PlaylistSummary, element?: PlaylistElement) {
233 const suffix = element
234 ? '-' + element.playlistElementId
237 return 'in-playlist-optional-' + playlist.id + suffix
240 buildOptionalRowElements (playlist: PlaylistSummary) {
241 const elements = playlist.elements
243 const lastElement = elements.length === 0
245 : elements[elements.length - 1]
247 // Build an empty last element
248 if (!lastElement || lastElement.enabled === true) {
252 stopTimestamp: this.video.duration
259 isPresentMultipleTimes (playlist: PlaylistSummary) {
260 return playlist.elements.filter(e => e.enabled === true).length > 1
263 onElementTimestampUpdate (playlist: PlaylistSummary, element: PlaylistElement) {
264 if (!element.playlistElementId || element.enabled === false) return
266 const body: VideoPlaylistElementUpdate = {
267 startTimestamp: element.startTimestamp,
268 stopTimestamp: element.stopTimestamp
271 this.videoPlaylistService.updateVideoOfPlaylist(playlist.id, element.playlistElementId, body, this.video.id)
274 this.notifier.success($localize`Timestamps updated`)
277 error: err => this.notifier.error(err.message),
279 complete: () => this.cd.markForCheck()
283 private isOptionalRowDisplayed (playlist: PlaylistSummary) {
284 const elements = playlist.elements.filter(e => e.enabled)
286 if (elements.length > 1) return true
288 if (elements.length === 1) {
289 const element = elements[0]
292 (element.startTimestamp && element.startTimestamp !== 0) ||
293 (element.stopTimestamp && element.stopTimestamp !== this.video.duration)
302 private removeVideoFromPlaylist (playlist: PlaylistSummary, elementId: number) {
303 this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, elementId, this.video.id)
306 this.notifier.success($localize`Video removed from ${playlist.displayName}`)
309 error: err => this.notifier.error(err.message),
311 complete: () => this.cd.markForCheck()
315 private listenToVideoPlaylistChange () {
316 this.unsubscribePlaylistChanges()
318 this.listenToPlaylistChangeSub = this.videoPlaylistService.listenToVideoPlaylistChange(this.video.id)
319 .pipe(filter(() => this.disabled === false))
320 .subscribe(existResult => this.rebuildPlaylists(existResult))
323 private unsubscribePlaylistChanges () {
324 if (this.listenToPlaylistChangeSub) {
325 this.listenToPlaylistChangeSub.unsubscribe()
326 this.listenToPlaylistChangeSub = undefined
330 private rebuildPlaylists (existResult: VideoExistInPlaylist[]) {
331 logger('Got existing results for %d.', this.video.id, existResult)
333 const oldPlaylists = this.videoPlaylists
335 this.videoPlaylists = []
336 for (const playlist of this.playlistsData) {
337 const existingPlaylists = existResult.filter(p => p.playlistId === playlist.id)
339 const playlistSummary = {
341 optionalRowDisplayed: false,
342 displayName: playlist.displayName,
343 elements: existingPlaylists.map(e => ({
345 playlistElementId: e.playlistElementId,
346 startTimestamp: e.startTimestamp || 0,
347 stopTimestamp: e.stopTimestamp || this.video.duration
351 const oldPlaylist = oldPlaylists.find(p => p.id === playlist.id)
352 playlistSummary.optionalRowDisplayed = oldPlaylist
353 ? oldPlaylist.optionalRowDisplayed
354 : this.isOptionalRowDisplayed(playlistSummary)
356 this.videoPlaylists.push(playlistSummary)
359 logger('Rebuilt playlist state for video %d.', this.video.id, this.videoPlaylists)
361 this.cd.markForCheck()
364 private addVideoInPlaylist (playlist: PlaylistSummary, element: PlaylistElement) {
365 const body: VideoPlaylistElementCreate = { videoId: this.video.id }
367 if (element.startTimestamp) body.startTimestamp = element.startTimestamp
368 if (element.stopTimestamp && element.stopTimestamp !== this.video.duration) body.stopTimestamp = element.stopTimestamp
370 this.videoPlaylistService.addVideoInPlaylist(playlist.id, body)
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}`
377 this.notifier.success(message)
379 if (element) element.playlistElementId = res.videoPlaylistElement.id
382 error: err => this.notifier.error(err.message),
384 complete: () => this.cd.markForCheck()
388 private formatTimestamp (element: PlaylistElement) {
389 const start = element.startTimestamp ? secondsToTime(element.startTimestamp) : ''
390 const stop = element.stopTimestamp ? secondsToTime(element.stopTimestamp) : ''
392 return `(${start}-${stop})`