]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/app/videos/+video-watch/video-watch.component.ts
Proxify local storage and handle if it is unavailable
[github/Chocobozzz/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 }