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