]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/app/videos/+video-watch/video-watch.component.ts
Merge branch 'develop' into 'develop'
[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 * 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 }