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