]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/app/videos/+video-watch/video-watch.component.ts
Use banner instead of modal for privacy concerns
[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
53 private hasAlreadyAcceptedPrivacyConcern = false
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 private updateVideoDescription (description: string) {
264 this.video.description = description
265 this.setVideoDescriptionHTML()
266 }
267
268 private setVideoDescriptionHTML () {
269 if (!this.video.description) {
270 this.videoHTMLDescription = ''
271 return
272 }
273
274 this.videoHTMLDescription = this.markdownService.textMarkdownToHTML(this.video.description)
275 }
276
277 private setVideoLikesBarTooltipText () {
278 this.likesBarTooltipText = `${this.video.likes} likes / ${this.video.dislikes} dislikes`
279 }
280
281 private handleError (err: any) {
282 const errorMessage: string = typeof err === 'string' ? err : err.message
283 if (!errorMessage) return
284
285 let message = ''
286
287 if (errorMessage.indexOf('http error') !== -1) {
288 message = 'Cannot fetch video from server, maybe down.'
289 } else {
290 message = errorMessage
291 }
292
293 this.notificationsService.error('Error', message)
294 }
295
296 private checkUserRating () {
297 // Unlogged users do not have ratings
298 if (this.isUserLoggedIn() === false) return
299
300 this.videoService.getUserVideoRating(this.video.id)
301 .subscribe(
302 ratingObject => {
303 if (ratingObject) {
304 this.userRating = ratingObject.rating
305 }
306 },
307
308 err => this.notificationsService.error('Error', err.message)
309 )
310 }
311
312 private async onVideoFetched (video: VideoDetails) {
313 this.video = video
314
315 this.updateOtherVideosDisplayed()
316
317 if (this.video.isVideoNSFWForUser(this.user)) {
318 const res = await this.confirmService.confirm(
319 'This video contains mature or explicit content. Are you sure you want to watch it?',
320 'Mature or explicit content'
321 )
322 if (res === false) return this.redirectService.redirectToHomepage()
323 }
324
325 // Player was already loaded
326 if (this.videoPlayerLoaded !== true) {
327 this.playerElement = this.elementRef.nativeElement.querySelector('#video-element')
328
329 // If autoplay is true, we don't really need a poster
330 if (this.isAutoplay() === false) {
331 this.playerElement.poster = this.video.previewUrl
332 }
333
334 const videojsOptions = {
335 controls: true,
336 autoplay: this.isAutoplay(),
337 playbackRates: [ 0.5, 1, 1.5, 2 ],
338 plugins: {
339 peertube: {
340 videoFiles: this.video.files,
341 playerElement: this.playerElement,
342 videoViewUrl: this.videoService.getVideoViewUrl(this.video.uuid),
343 videoDuration: this.video.duration
344 },
345 hotkeys: {
346 enableVolumeScroll: false
347 }
348 },
349 controlBar: {
350 children: [
351 'playToggle',
352 'currentTimeDisplay',
353 'timeDivider',
354 'durationDisplay',
355 'liveDisplay',
356
357 'flexibleWidthSpacer',
358 'progressControl',
359
360 'webTorrentButton',
361
362 'playbackRateMenuButton',
363
364 'muteToggle',
365 'volumeControl',
366
367 'resolutionMenuButton',
368
369 'fullscreenToggle'
370 ]
371 }
372 }
373
374 this.videoPlayerLoaded = true
375
376 const self = this
377 this.zone.runOutsideAngular(() => {
378 videojs(this.playerElement, videojsOptions, function () {
379 self.player = this
380 this.on('customError', (event, data) => self.handleError(data.err))
381 })
382 })
383 } else {
384 const videoViewUrl = this.videoService.getVideoViewUrl(this.video.uuid)
385 this.player.peertube().setVideoFiles(this.video.files, videoViewUrl, this.video.duration)
386 }
387
388 this.setVideoDescriptionHTML()
389 this.setVideoLikesBarTooltipText()
390
391 this.setOpenGraphTags()
392 this.checkUserRating()
393 }
394
395 private setRating (nextRating) {
396 let method
397 switch (nextRating) {
398 case 'like':
399 method = this.videoService.setVideoLike
400 break
401 case 'dislike':
402 method = this.videoService.setVideoDislike
403 break
404 case 'none':
405 method = this.videoService.unsetVideoLike
406 break
407 }
408
409 method.call(this.videoService, this.video.id)
410 .subscribe(
411 () => {
412 // Update the video like attribute
413 this.updateVideoRating(this.userRating, nextRating)
414 this.userRating = nextRating
415 },
416 err => this.notificationsService.error('Error', err.message)
417 )
418 }
419
420 private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) {
421 let likesToIncrement = 0
422 let dislikesToIncrement = 0
423
424 if (oldRating) {
425 if (oldRating === 'like') likesToIncrement--
426 if (oldRating === 'dislike') dislikesToIncrement--
427 }
428
429 if (newRating === 'like') likesToIncrement++
430 if (newRating === 'dislike') dislikesToIncrement++
431
432 this.video.likes += likesToIncrement
433 this.video.dislikes += dislikesToIncrement
434
435 this.video.buildLikeAndDislikePercents()
436 this.setVideoLikesBarTooltipText()
437 }
438
439 private updateOtherVideosDisplayed () {
440 if (this.video && this.otherVideos && this.otherVideos.length > 0) {
441 this.otherVideosDisplayed = this.otherVideos.filter(v => v.uuid !== this.video.uuid)
442 }
443 }
444
445 private setOpenGraphTags () {
446 this.metaService.setTitle(this.video.name)
447
448 this.metaService.setTag('og:type', 'video')
449
450 this.metaService.setTag('og:title', this.video.name)
451 this.metaService.setTag('name', this.video.name)
452
453 this.metaService.setTag('og:description', this.video.description)
454 this.metaService.setTag('description', this.video.description)
455
456 this.metaService.setTag('og:image', this.video.previewPath)
457
458 this.metaService.setTag('og:duration', this.video.duration.toString())
459
460 this.metaService.setTag('og:site_name', 'PeerTube')
461
462 this.metaService.setTag('og:url', window.location.href)
463 this.metaService.setTag('url', window.location.href)
464 }
465
466 private isAutoplay () {
467 // True by default
468 if (!this.user) return true
469
470 // Be sure the autoPlay is set to false
471 return this.user.autoPlayVideo !== false
472 }
473
474 private acceptedPrivacyConcern () {
475 localStorage.setItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY, 'true')
476 this.hasAlreadyAcceptedPrivacyConcern = true
477 }
478 }