Add hotkeys in video player
[oweals/peertube.git] / client / src / app / videos / +video-watch / video-watch.component.ts
1 import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'
2 import { ActivatedRoute, Router } from '@angular/router'
3 import { MetaService } from '@ngx-meta/core'
4 import { NotificationsService } from 'angular2-notifications'
5 import { Observable } from 'rxjs/Observable'
6 import { Subscription } from 'rxjs/Subscription'
7 import * as videojs from 'video.js'
8 import 'videojs-hotkeys'
9 import { UserVideoRateType, VideoRateType } from '../../../../../shared'
10 import '../../../assets/player/peertube-videojs-plugin'
11 import { AuthService, ConfirmService } from '../../core'
12 import { VideoBlacklistService } from '../../shared'
13 import { Account } from '../../shared/account/account.model'
14 import { VideoDetails } from '../../shared/video/video-details.model'
15 import { Video } from '../../shared/video/video.model'
16 import { VideoService } from '../../shared/video/video.service'
17 import { MarkdownService } from '../shared'
18 import { VideoDownloadComponent } from './modal/video-download.component'
19 import { VideoReportComponent } from './modal/video-report.component'
20 import { VideoShareComponent } from './modal/video-share.component'
21
22 @Component({
23   selector: 'my-video-watch',
24   templateUrl: './video-watch.component.html',
25   styleUrls: [ './video-watch.component.scss' ]
26 })
27 export class VideoWatchComponent implements OnInit, OnDestroy {
28   @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent
29   @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
30   @ViewChild('videoReportModal') videoReportModal: VideoReportComponent
31
32   otherVideosDisplayed: Video[] = []
33
34   error = false
35   loading = false
36   player: videojs.Player
37   playerElement: HTMLVideoElement
38   userRating: UserVideoRateType = null
39   video: VideoDetails = null
40   videoPlayerLoaded = false
41   videoNotFound = false
42   descriptionLoading = false
43
44   completeDescriptionShown = false
45   completeVideoDescription: string
46   shortVideoDescription: string
47   videoHTMLDescription = ''
48   likesBarTooltipText = ''
49
50   private otherVideos: Video[] = []
51   private paramsSub: Subscription
52
53   constructor (
54     private elementRef: ElementRef,
55     private route: ActivatedRoute,
56     private router: Router,
57     private videoService: VideoService,
58     private videoBlacklistService: VideoBlacklistService,
59     private confirmService: ConfirmService,
60     private metaService: MetaService,
61     private authService: AuthService,
62     private notificationsService: NotificationsService,
63     private markdownService: MarkdownService
64   ) {}
65
66   get user () {
67     return this.authService.getUser()
68   }
69
70   ngOnInit () {
71     this.videoService.getVideos({ currentPage: 1, itemsPerPage: 5 }, '-createdAt')
72       .subscribe(
73         data => this.otherVideos = data.videos,
74         err => console.error(err)
75       )
76
77     this.paramsSub = this.route.params.subscribe(routeParams => {
78       if (this.videoPlayerLoaded) {
79         this.player.pause()
80       }
81
82       let uuid = routeParams['uuid']
83       this.videoService.getVideo(uuid).subscribe(
84         video => this.onVideoFetched(video),
85
86         error => {
87           this.videoNotFound = true
88           console.error(error)
89         }
90       )
91     })
92   }
93
94   ngOnDestroy () {
95     // Remove player if it exists
96     if (this.videoPlayerLoaded === true) {
97       videojs(this.playerElement).dispose()
98     }
99
100     // Unsubscribe subscriptions
101     this.paramsSub.unsubscribe()
102   }
103
104   setLike () {
105     if (this.isUserLoggedIn() === false) return
106     if (this.userRating === 'like') {
107       // Already liked this video
108       this.setRating('none')
109     } else {
110       this.setRating('like')
111     }
112   }
113
114   setDislike () {
115     if (this.isUserLoggedIn() === false) return
116     if (this.userRating === 'dislike') {
117       // Already disliked this video
118       this.setRating('none')
119     } else {
120       this.setRating('dislike')
121     }
122   }
123
124   blacklistVideo (event: Event) {
125     event.preventDefault()
126
127     this.confirmService.confirm('Do you really want to blacklist this video?', 'Blacklist').subscribe(
128       res => {
129         if (res === false) return
130
131         this.videoBlacklistService.blacklistVideo(this.video.id)
132                                   .subscribe(
133                                     status => {
134                                       this.notificationsService.success('Success', `Video ${this.video.name} had been blacklisted.`)
135                                       this.router.navigate(['/videos/list'])
136                                     },
137
138                                     error => this.notificationsService.error('Error', error.message)
139                                   )
140       }
141     )
142   }
143
144   showMoreDescription () {
145     if (this.completeVideoDescription === undefined) {
146       return this.loadCompleteDescription()
147     }
148
149     this.updateVideoDescription(this.completeVideoDescription)
150     this.completeDescriptionShown = true
151   }
152
153   showLessDescription () {
154     this.updateVideoDescription(this.shortVideoDescription)
155     this.completeDescriptionShown = false
156   }
157
158   loadCompleteDescription () {
159     this.descriptionLoading = true
160
161     this.videoService.loadCompleteDescription(this.video.descriptionPath)
162       .subscribe(
163         description => {
164           this.completeDescriptionShown = true
165           this.descriptionLoading = false
166
167           this.shortVideoDescription = this.video.description
168           this.completeVideoDescription = description
169
170           this.updateVideoDescription(this.completeVideoDescription)
171         },
172
173         error => {
174           this.descriptionLoading = false
175           this.notificationsService.error('Error', error.message)
176         }
177       )
178   }
179
180   showReportModal (event: Event) {
181     event.preventDefault()
182     this.videoReportModal.show()
183   }
184
185   showShareModal () {
186     this.videoShareModal.show()
187   }
188
189   showDownloadModal (event: Event) {
190     event.preventDefault()
191     this.videoDownloadModal.show()
192   }
193
194   isUserLoggedIn () {
195     return this.authService.isLoggedIn()
196   }
197
198   isVideoUpdatable () {
199     return this.video.isUpdatableBy(this.authService.getUser())
200   }
201
202   isVideoBlacklistable () {
203     return this.video.isBlackistableBy(this.user)
204   }
205
206   getAvatarPath () {
207     return Account.GET_ACCOUNT_AVATAR_URL(this.video.account)
208   }
209
210   getVideoTags () {
211     if (!this.video || Array.isArray(this.video.tags) === false) return []
212
213     return this.video.tags.join(', ')
214   }
215
216   isVideoRemovable () {
217     return this.video.isRemovableBy(this.authService.getUser())
218   }
219
220   removeVideo (event: Event) {
221     event.preventDefault()
222
223     this.confirmService.confirm('Do you really want to delete this video?', 'Delete')
224       .subscribe(
225         res => {
226           if (res === false) return
227
228           this.videoService.removeVideo(this.video.id)
229             .subscribe(
230               status => {
231                 this.notificationsService.success('Success', `Video ${this.video.name} deleted.`)
232
233                 // Go back to the video-list.
234                 this.router.navigate([ '/videos/list' ])
235               },
236
237               error => this.notificationsService.error('Error', error.message)
238             )
239         }
240       )
241   }
242
243   private updateVideoDescription (description: string) {
244     this.video.description = description
245     this.setVideoDescriptionHTML()
246   }
247
248   private setVideoDescriptionHTML () {
249     if (!this.video.description) {
250       this.videoHTMLDescription = ''
251       return
252     }
253
254     this.videoHTMLDescription = this.markdownService.markdownToHTML(this.video.description)
255   }
256
257   private setVideoLikesBarTooltipText () {
258     this.likesBarTooltipText = `${this.video.likes} likes / ${this.video.dislikes} dislikes`
259   }
260
261   private handleError (err: any) {
262     const errorMessage: string = typeof err === 'string' ? err : err.message
263     let message = ''
264
265     if (errorMessage.indexOf('http error') !== -1) {
266       message = 'Cannot fetch video from server, maybe down.'
267     } else {
268       message = errorMessage
269     }
270
271     this.notificationsService.error('Error', message)
272   }
273
274   private checkUserRating () {
275     // Unlogged users do not have ratings
276     if (this.isUserLoggedIn() === false) return
277
278     this.videoService.getUserVideoRating(this.video.id)
279                      .subscribe(
280                        ratingObject => {
281                          if (ratingObject) {
282                            this.userRating = ratingObject.rating
283                          }
284                        },
285
286                        err => this.notificationsService.error('Error', err.message)
287                       )
288   }
289
290   private onVideoFetched (video: VideoDetails) {
291     this.video = video
292
293     if (this.otherVideos.length > 0) {
294       this.otherVideosDisplayed = this.otherVideos.filter(v => v.uuid !== this.video.uuid)
295     }
296
297     let observable
298     if (this.video.isVideoNSFWForUser(this.user)) {
299       observable = this.confirmService.confirm(
300         'This video contains mature or explicit content. Are you sure you want to watch it?',
301         'Mature or explicit content'
302       )
303     } else {
304       observable = Observable.of(true)
305     }
306
307     observable.subscribe(
308       res => {
309         if (res === false) {
310
311           return this.router.navigate([ '/videos/list' ])
312         }
313
314         // Player was already loaded
315         if (this.videoPlayerLoaded !== true) {
316           this.playerElement = this.elementRef.nativeElement.querySelector('#video-element')
317
318           // If autoplay is true, we don't really need a poster
319           if (this.isAutoplay() === false) {
320             this.playerElement.poster = this.video.previewUrl
321           }
322
323           const videojsOptions = {
324             controls: true,
325             autoplay: this.isAutoplay(),
326             plugins: {
327               peertube: {
328                 videoFiles: this.video.files,
329                 playerElement: this.playerElement,
330                 peerTubeLink: false
331               },
332               hotkeys: {}
333             }
334           }
335
336           this.videoPlayerLoaded = true
337
338           const self = this
339           videojs(this.playerElement, videojsOptions, function () {
340             self.player = this
341             this.on('customError', (event, data) => {
342               self.handleError(data.err)
343             })
344           })
345         } else {
346           (this.player as any).setVideoFiles(this.video.files)
347         }
348
349         this.setVideoDescriptionHTML()
350         this.setVideoLikesBarTooltipText()
351
352         this.setOpenGraphTags()
353         this.checkUserRating()
354
355         this.prepareViewAdd()
356       }
357     )
358   }
359
360   private setRating (nextRating) {
361     let method
362     switch (nextRating) {
363       case 'like':
364         method = this.videoService.setVideoLike
365         break
366       case 'dislike':
367         method = this.videoService.setVideoDislike
368         break
369       case 'none':
370         method = this.videoService.unsetVideoLike
371         break
372     }
373
374     method.call(this.videoService, this.video.id)
375      .subscribe(
376       () => {
377         // Update the video like attribute
378         this.updateVideoRating(this.userRating, nextRating)
379         this.userRating = nextRating
380       },
381       err => this.notificationsService.error('Error', err.message)
382      )
383   }
384
385   private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) {
386     let likesToIncrement = 0
387     let dislikesToIncrement = 0
388
389     if (oldRating) {
390       if (oldRating === 'like') likesToIncrement--
391       if (oldRating === 'dislike') dislikesToIncrement--
392     }
393
394     if (newRating === 'like') likesToIncrement++
395     if (newRating === 'dislike') dislikesToIncrement++
396
397     this.video.likes += likesToIncrement
398     this.video.dislikes += dislikesToIncrement
399   }
400
401   private setOpenGraphTags () {
402     this.metaService.setTitle(this.video.name)
403
404     this.metaService.setTag('og:type', 'video')
405
406     this.metaService.setTag('og:title', this.video.name)
407     this.metaService.setTag('name', this.video.name)
408
409     this.metaService.setTag('og:description', this.video.description)
410     this.metaService.setTag('description', this.video.description)
411
412     this.metaService.setTag('og:image', this.video.previewPath)
413
414     this.metaService.setTag('og:duration', this.video.duration.toString())
415
416     this.metaService.setTag('og:site_name', 'PeerTube')
417
418     this.metaService.setTag('og:url', window.location.href)
419     this.metaService.setTag('url', window.location.href)
420   }
421
422   private prepareViewAdd () {
423     // After 30 seconds (or 3/4 of the video), increment add a view
424     let viewTimeoutSeconds = 30
425     if (this.video.duration < viewTimeoutSeconds) viewTimeoutSeconds = (this.video.duration * 3) / 4
426
427     setTimeout(() => {
428       this.videoService
429         .viewVideo(this.video.uuid)
430         .subscribe()
431
432     }, viewTimeoutSeconds * 1000)
433   }
434
435   private isAutoplay () {
436     // True by default
437     if (!this.user) return true
438
439     // Be sure the autoPlay is set to false
440     return this.user.autoPlayVideo !== false
441   }
442 }