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