Playlist reorder support
[oweals/peertube.git] / client / src / app / +my-account / my-account-video-playlists / my-account-video-playlist-elements.component.ts
1 import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
2 import { Notifier, ServerService } from '@app/core'
3 import { AuthService } from '../../core/auth'
4 import { ConfirmService } from '../../core/confirm'
5 import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
6 import { Video } from '@app/shared/video/video.model'
7 import { Subject, Subscription } from 'rxjs'
8 import { ActivatedRoute } from '@angular/router'
9 import { VideoService } from '@app/shared/video/video.service'
10 import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
11 import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
12 import { I18n } from '@ngx-translate/i18n-polyfill'
13 import { secondsToTime } from '../../../assets/player/utils'
14 import { VideoPlaylistElementUpdate } from '@shared/models'
15 import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
16 import { CdkDragDrop, CdkDragMove } from '@angular/cdk/drag-drop'
17 import { throttleTime } from 'rxjs/operators'
18
19 @Component({
20   selector: 'my-account-video-playlist-elements',
21   templateUrl: './my-account-video-playlist-elements.component.html',
22   styleUrls: [ './my-account-video-playlist-elements.component.scss' ]
23 })
24 export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestroy {
25   @ViewChild('moreDropdown') moreDropdown: NgbDropdown
26
27   videos: Video[] = []
28   playlist: VideoPlaylist
29
30   pagination: ComponentPagination = {
31     currentPage: 1,
32     itemsPerPage: 10,
33     totalItems: null
34   }
35
36   displayTimestampOptions = false
37
38   timestampOptions: {
39     startTimestampEnabled: boolean
40     startTimestamp: number
41     stopTimestampEnabled: boolean
42     stopTimestamp: number
43   } = {} as any
44
45   private videoPlaylistId: string | number
46   private paramsSub: Subscription
47   private dragMoveSubject = new Subject<number>()
48
49   constructor (
50     private authService: AuthService,
51     private serverService: ServerService,
52     private notifier: Notifier,
53     private confirmService: ConfirmService,
54     private route: ActivatedRoute,
55     private i18n: I18n,
56     private videoService: VideoService,
57     private videoPlaylistService: VideoPlaylistService
58   ) {}
59
60   ngOnInit () {
61     this.paramsSub = this.route.params.subscribe(routeParams => {
62       this.videoPlaylistId = routeParams[ 'videoPlaylistId' ]
63       this.loadElements()
64
65       this.loadPlaylistInfo()
66     })
67
68     this.dragMoveSubject.asObservable()
69       .pipe(throttleTime(200))
70       .subscribe(y => this.checkScroll(y))
71   }
72
73   ngOnDestroy () {
74     if (this.paramsSub) this.paramsSub.unsubscribe()
75   }
76
77   drop (event: CdkDragDrop<any>) {
78     const previousIndex = event.previousIndex
79     const newIndex = event.currentIndex
80
81     if (previousIndex === newIndex) return
82
83     const oldPosition = this.videos[previousIndex].playlistElement.position
84     const insertAfter = newIndex === 0 ? 0 : this.videos[newIndex].playlistElement.position
85
86     this.videoPlaylistService.reorderPlaylist(this.playlist.id, oldPosition, insertAfter)
87       .subscribe(
88         () => { /* nothing to do */ },
89
90         err => this.notifier.error(err.message)
91       )
92
93     const video = this.videos[previousIndex]
94
95     this.videos.splice(previousIndex, 1)
96     this.videos.splice(newIndex, 0, video)
97
98     this.reorderClientPositions()
99   }
100
101   onDragMove (event: CdkDragMove<any>) {
102     this.dragMoveSubject.next(event.pointerPosition.y)
103   }
104
105   checkScroll (pointerY: number) {
106     // FIXME: Uncomment when https://github.com/angular/material2/issues/14098 is fixed
107     // FIXME: Remove when https://github.com/angular/material2/issues/13588 is implemented
108     // if (pointerY < 150) {
109     //   window.scrollBy({
110     //     left: 0,
111     //     top: -20,
112     //     behavior: 'smooth'
113     //   })
114     //
115     //   return
116     // }
117     //
118     // if (window.innerHeight - pointerY <= 50) {
119     //   window.scrollBy({
120     //     left: 0,
121     //     top: 20,
122     //     behavior: 'smooth'
123     //   })
124     // }
125   }
126
127   isVideoBlur (video: Video) {
128     return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig())
129   }
130
131   removeFromPlaylist (video: Video) {
132     this.videoPlaylistService.removeVideoFromPlaylist(this.playlist.id, video.id)
133         .subscribe(
134           () => {
135             this.notifier.success(this.i18n('Video removed from {{name}}', { name: this.playlist.displayName }))
136
137             this.videos = this.videos.filter(v => v.id !== video.id)
138             this.reorderClientPositions()
139           },
140
141           err => this.notifier.error(err.message)
142         )
143
144     this.moreDropdown.close()
145   }
146
147   updateTimestamps (video: Video) {
148     const body: VideoPlaylistElementUpdate = {}
149
150     body.startTimestamp = this.timestampOptions.startTimestampEnabled ? this.timestampOptions.startTimestamp : null
151     body.stopTimestamp = this.timestampOptions.stopTimestampEnabled ? this.timestampOptions.stopTimestamp : null
152
153     this.videoPlaylistService.updateVideoOfPlaylist(this.playlist.id, video.id, body)
154         .subscribe(
155           () => {
156             this.notifier.success(this.i18n('Timestamps updated'))
157
158             video.playlistElement.startTimestamp = body.startTimestamp
159             video.playlistElement.stopTimestamp = body.stopTimestamp
160           },
161
162           err => this.notifier.error(err.message)
163         )
164
165     this.moreDropdown.close()
166   }
167
168   onNearOfBottom () {
169     // Last page
170     if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
171
172     this.pagination.currentPage += 1
173     this.loadElements()
174   }
175
176   formatTimestamp (video: Video) {
177     const start = video.playlistElement.startTimestamp
178     const stop = video.playlistElement.stopTimestamp
179
180     const startFormatted = secondsToTime(start, true, ':')
181     const stopFormatted = secondsToTime(stop, true, ':')
182
183     if (start === null && stop === null) return ''
184
185     if (start !== null && stop === null) return this.i18n('Starts at ') + startFormatted
186     if (start === null && stop !== null) return this.i18n('Stops at ') + stopFormatted
187
188     return this.i18n('Starts at ') + startFormatted + this.i18n(' and stops at ') + stopFormatted
189   }
190
191   onDropdownOpenChange () {
192     this.displayTimestampOptions = false
193   }
194
195   toggleDisplayTimestampsOptions (event: Event, video: Video) {
196     event.preventDefault()
197
198     this.displayTimestampOptions = !this.displayTimestampOptions
199
200     if (this.displayTimestampOptions === true) {
201       this.timestampOptions = {
202         startTimestampEnabled: false,
203         stopTimestampEnabled: false,
204         startTimestamp: 0,
205         stopTimestamp: video.duration
206       }
207
208       if (video.playlistElement.startTimestamp) {
209         this.timestampOptions.startTimestampEnabled = true
210         this.timestampOptions.startTimestamp = video.playlistElement.startTimestamp
211       }
212
213       if (video.playlistElement.stopTimestamp) {
214         this.timestampOptions.stopTimestampEnabled = true
215         this.timestampOptions.stopTimestamp = video.playlistElement.stopTimestamp
216       }
217     }
218   }
219
220   private loadElements () {
221     this.videoService.getPlaylistVideos(this.videoPlaylistId, this.pagination)
222         .subscribe(({ totalVideos, videos }) => {
223           this.videos = this.videos.concat(videos)
224           this.pagination.totalItems = totalVideos
225         })
226   }
227
228   private loadPlaylistInfo () {
229     this.videoPlaylistService.getVideoPlaylist(this.videoPlaylistId)
230       .subscribe(playlist => {
231         this.playlist = playlist
232       })
233   }
234
235   private reorderClientPositions () {
236     let i = 1
237
238     for (const video of this.videos) {
239       video.playlistElement.position = i
240       i++
241     }
242   }
243 }