]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - 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
CommitLineData
7ae71355 1import { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
df98563e 2import { ActivatedRoute, Router } from '@angular/router'
901637bb 3import { RedirectService } from '@app/core/routing/redirect.service'
07fa4c97 4import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component'
1f3e9fec
C
5import { MetaService } from '@ngx-meta/core'
6import { NotificationsService } from 'angular2-notifications'
df98563e 7import { Subscription } from 'rxjs/Subscription'
63c4db6d 8import * as videojs from 'video.js'
d7701449 9import 'videojs-hotkeys'
1f3e9fec 10import { UserVideoRateType, VideoRateType } from '../../../../../shared'
aa8b6df4 11import '../../../assets/player/peertube-videojs-plugin'
df98563e 12import { AuthService, ConfirmService } from '../../core'
1f3e9fec 13import { VideoBlacklistService } from '../../shared'
b1fa3eba 14import { Account } from '../../shared/account/account.model'
ff249f49 15import { VideoDetails } from '../../shared/video/video-details.model'
b1fa3eba 16import { Video } from '../../shared/video/video.model'
63c4db6d 17import { VideoService } from '../../shared/video/video.service'
202f6b6c 18import { MarkdownService } from '../shared'
4635f59d
C
19import { VideoDownloadComponent } from './modal/video-download.component'
20import { VideoReportComponent } from './modal/video-report.component'
21import { VideoShareComponent } from './modal/video-share.component'
dc8bc31b 22
dc8bc31b
C
23@Component({
24 selector: 'my-video-watch',
ec8d8440
C
25 templateUrl: './video-watch.component.html',
26 styleUrls: [ './video-watch.component.scss' ]
dc8bc31b 27})
0629423c 28export class VideoWatchComponent implements OnInit, OnDestroy {
22b59e80
C
29 private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern'
30
a96aed15 31 @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent
df98563e
C
32 @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
33 @ViewChild('videoReportModal') videoReportModal: VideoReportComponent
07fa4c97 34 @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent
df98563e 35
57a49263 36 otherVideosDisplayed: Video[] = []
b1fa3eba 37
df98563e 38 error = false
df98563e 39 player: videojs.Player
0826c92d 40 playerElement: HTMLVideoElement
154898b0 41 userRating: UserVideoRateType = null
404b54e1 42 video: VideoDetails = null
efee3505 43 videoPlayerLoaded = false
f1013131 44 videoNotFound = false
80958c78 45 descriptionLoading = false
2de96f4d
C
46
47 completeDescriptionShown = false
48 completeVideoDescription: string
49 shortVideoDescription: string
9d9597df 50 videoHTMLDescription = ''
e9189001 51 likesBarTooltipText = ''
df98563e 52
2b3b76ab 53 private hasAlreadyAcceptedPrivacyConcern = false
28832412 54 private otherVideos: Video[] = []
df98563e 55 private paramsSub: Subscription
df98563e
C
56
57 constructor (
4fd8aa32 58 private elementRef: ElementRef,
0629423c 59 private route: ActivatedRoute,
92fb909c 60 private router: Router,
d3ef341a 61 private videoService: VideoService,
35bf0c83 62 private videoBlacklistService: VideoBlacklistService,
92fb909c 63 private confirmService: ConfirmService,
3ec343a4 64 private metaService: MetaService,
7ddd02c9 65 private authService: AuthService,
9d9597df 66 private notificationsService: NotificationsService,
7ae71355 67 private markdownService: MarkdownService,
901637bb
C
68 private zone: NgZone,
69 private redirectService: RedirectService
d3ef341a 70 ) {}
dc8bc31b 71
b2731bff
C
72 get user () {
73 return this.authService.getUser()
74 }
75
df98563e 76 ngOnInit () {
2b3b76ab
C
77 if (localStorage.getItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY) === 'true') {
78 this.hasAlreadyAcceptedPrivacyConcern = true
79 }
80
b1fa3eba
C
81 this.videoService.getVideos({ currentPage: 1, itemsPerPage: 5 }, '-createdAt')
82 .subscribe(
649fb082
C
83 data => {
84 this.otherVideos = data.videos
85 this.updateOtherVideosDisplayed()
86 },
87
57a49263 88 err => console.error(err)
b1fa3eba
C
89 )
90
13fc89f4 91 this.paramsSub = this.route.params.subscribe(routeParams => {
ed9f9f5f
C
92 if (this.videoPlayerLoaded) {
93 this.player.pause()
94 }
95
1263fc4e
C
96 const uuid = routeParams['uuid']
97 // Video did not changed
98 if (this.video && this.video.uuid === uuid) return
99
0a6658fd 100 this.videoService.getVideo(uuid).subscribe(
92fb909c
C
101 video => this.onVideoFetched(video),
102
f1013131
C
103 error => {
104 this.videoNotFound = true
105 console.error(error)
106 }
df98563e
C
107 )
108 })
d1992b93
C
109 }
110
df98563e 111 ngOnDestroy () {
2ed6a0ae 112 // Remove player if it exists
efee3505 113 if (this.videoPlayerLoaded === true) {
2ed6a0ae
C
114 videojs(this.playerElement).dispose()
115 }
067e3f84 116
13fc89f4 117 // Unsubscribe subscriptions
df98563e 118 this.paramsSub.unsubscribe()
dc8bc31b 119 }
98b01bac 120
df98563e
C
121 setLike () {
122 if (this.isUserLoggedIn() === false) return
57a49263
BB
123 if (this.userRating === 'like') {
124 // Already liked this video
125 this.setRating('none')
126 } else {
127 this.setRating('like')
128 }
d38b8281
C
129 }
130
df98563e
C
131 setDislike () {
132 if (this.isUserLoggedIn() === false) return
57a49263
BB
133 if (this.userRating === 'dislike') {
134 // Already disliked this video
135 this.setRating('none')
136 } else {
137 this.setRating('dislike')
138 }
d38b8281
C
139 }
140
1f30a185 141 async blacklistVideo (event: Event) {
df98563e 142 event.preventDefault()
ab683a8e 143
1f30a185
C
144 const res = await this.confirmService.confirm('Do you really want to blacklist this video?', 'Blacklist')
145 if (res === false) return
198b205c 146
1f30a185
C
147 this.videoBlacklistService.blacklistVideo(this.video.id)
148 .subscribe(
149 status => {
150 this.notificationsService.success('Success', `Video ${this.video.name} had been blacklisted.`)
901637bb 151 this.redirectService.redirectToHomepage()
1f30a185 152 },
198b205c 153
1f30a185
C
154 error => this.notificationsService.error('Error', error.message)
155 )
198b205c
GS
156 }
157
2de96f4d 158 showMoreDescription () {
2de96f4d
C
159 if (this.completeVideoDescription === undefined) {
160 return this.loadCompleteDescription()
161 }
162
163 this.updateVideoDescription(this.completeVideoDescription)
80958c78 164 this.completeDescriptionShown = true
2de96f4d
C
165 }
166
167 showLessDescription () {
2de96f4d 168 this.updateVideoDescription(this.shortVideoDescription)
80958c78 169 this.completeDescriptionShown = false
2de96f4d
C
170 }
171
172 loadCompleteDescription () {
80958c78
C
173 this.descriptionLoading = true
174
2de96f4d
C
175 this.videoService.loadCompleteDescription(this.video.descriptionPath)
176 .subscribe(
177 description => {
80958c78
C
178 this.completeDescriptionShown = true
179 this.descriptionLoading = false
180
2de96f4d
C
181 this.shortVideoDescription = this.video.description
182 this.completeVideoDescription = description
183
184 this.updateVideoDescription(this.completeVideoDescription)
185 },
186
80958c78
C
187 error => {
188 this.descriptionLoading = false
c5911fd3 189 this.notificationsService.error('Error', error.message)
80958c78 190 }
2de96f4d
C
191 )
192 }
193
df98563e
C
194 showReportModal (event: Event) {
195 event.preventDefault()
196 this.videoReportModal.show()
4f8c0eb0
C
197 }
198
07fa4c97
C
199 showSupportModal () {
200 this.videoSupportModal.show()
201 }
202
df98563e
C
203 showShareModal () {
204 this.videoShareModal.show()
99cc4f49
C
205 }
206
a96aed15 207 showDownloadModal (event: Event) {
df98563e 208 event.preventDefault()
a96aed15 209 this.videoDownloadModal.show()
99cc4f49
C
210 }
211
df98563e
C
212 isUserLoggedIn () {
213 return this.authService.isLoggedIn()
4f8c0eb0
C
214 }
215
4635f59d
C
216 isVideoUpdatable () {
217 return this.video.isUpdatableBy(this.authService.getUser())
218 }
219
df98563e 220 isVideoBlacklistable () {
b2731bff 221 return this.video.isBlackistableBy(this.user)
198b205c
GS
222 }
223
b1fa3eba 224 getAvatarPath () {
c5911fd3 225 return Account.GET_ACCOUNT_AVATAR_URL(this.video.account)
b1fa3eba
C
226 }
227
6de36768
C
228 getVideoPoster () {
229 if (!this.video) return ''
230
231 return this.video.previewUrl
232 }
233
b1fa3eba
C
234 getVideoTags () {
235 if (!this.video || Array.isArray(this.video.tags) === false) return []
236
237 return this.video.tags.join(', ')
238 }
239
6725d05c
C
240 isVideoRemovable () {
241 return this.video.isRemovableBy(this.authService.getUser())
242 }
243
1f30a185 244 async removeVideo (event: Event) {
6725d05c
C
245 event.preventDefault()
246
1f30a185
C
247 const res = await this.confirmService.confirm('Do you really want to delete this video?', 'Delete')
248 if (res === false) return
6725d05c 249
1f30a185
C
250 this.videoService.removeVideo(this.video.id)
251 .subscribe(
252 status => {
253 this.notificationsService.success('Success', `Video ${this.video.name} deleted.`)
6725d05c 254
1f30a185 255 // Go back to the video-list.
901637bb 256 this.redirectService.redirectToHomepage()
1f30a185 257 },
6725d05c 258
1f30a185 259 error => this.notificationsService.error('Error', error.message)
e9189001 260 )
6725d05c
C
261 }
262
2de96f4d
C
263 private updateVideoDescription (description: string) {
264 this.video.description = description
265 this.setVideoDescriptionHTML()
266 }
267
268 private setVideoDescriptionHTML () {
cadb46d8
C
269 if (!this.video.description) {
270 this.videoHTMLDescription = ''
271 return
272 }
273
07fa4c97 274 this.videoHTMLDescription = this.markdownService.textMarkdownToHTML(this.video.description)
2de96f4d
C
275 }
276
e9189001
C
277 private setVideoLikesBarTooltipText () {
278 this.likesBarTooltipText = `${this.video.likes} likes / ${this.video.dislikes} dislikes`
279 }
280
0c31c33d
C
281 private handleError (err: any) {
282 const errorMessage: string = typeof err === 'string' ? err : err.message
bf5685f0
C
283 if (!errorMessage) return
284
0c31c33d
C
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
df98563e 296 private checkUserRating () {
d38b8281 297 // Unlogged users do not have ratings
df98563e 298 if (this.isUserLoggedIn() === false) return
d38b8281
C
299
300 this.videoService.getUserVideoRating(this.video.id)
301 .subscribe(
b632e904 302 ratingObject => {
d38b8281 303 if (ratingObject) {
df98563e 304 this.userRating = ratingObject.rating
d38b8281
C
305 }
306 },
307
bfb3a98f 308 err => this.notificationsService.error('Error', err.message)
df98563e 309 )
d38b8281
C
310 }
311
22b59e80 312 private async onVideoFetched (video: VideoDetails) {
df98563e 313 this.video = video
92fb909c 314
649fb082 315 this.updateOtherVideosDisplayed()
57a49263 316
b2731bff 317 if (this.video.isVideoNSFWForUser(this.user)) {
22b59e80 318 const res = await this.confirmService.confirm(
d6e32a2e
C
319 'This video contains mature or explicit content. Are you sure you want to watch it?',
320 'Mature or explicit content'
321 )
901637bb 322 if (res === false) return this.redirectService.redirectToHomepage()
92fb909c
C
323 }
324
22b59e80
C
325 // Player was already loaded
326 if (this.videoPlayerLoaded !== true) {
327 this.playerElement = this.elementRef.nativeElement.querySelector('#video-element')
ed9f9f5f 328
22b59e80
C
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 }
0826c92d 333
22b59e80
C
334 const videojsOptions = {
335 controls: true,
336 autoplay: this.isAutoplay(),
1198a08c 337 playbackRates: [ 0.5, 1, 1.5, 2 ],
22b59e80
C
338 plugins: {
339 peertube: {
340 videoFiles: this.video.files,
341 playerElement: this.playerElement,
22b59e80
C
342 videoViewUrl: this.videoService.getVideoViewUrl(this.video.uuid),
343 videoDuration: this.video.duration
344 },
345 hotkeys: {
346 enableVolumeScroll: false
aa8b6df4 347 }
3ec8dc09
C
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 ]
ed9f9f5f 371 }
22b59e80 372 }
aa8b6df4 373
22b59e80 374 this.videoPlayerLoaded = true
9d9597df 375
22b59e80
C
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()
92fb909c
C
393 }
394
57a49263
BB
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
154898b0 420 private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) {
df98563e
C
421 let likesToIncrement = 0
422 let dislikesToIncrement = 0
d38b8281
C
423
424 if (oldRating) {
df98563e
C
425 if (oldRating === 'like') likesToIncrement--
426 if (oldRating === 'dislike') dislikesToIncrement--
d38b8281
C
427 }
428
df98563e
C
429 if (newRating === 'like') likesToIncrement++
430 if (newRating === 'dislike') dislikesToIncrement++
d38b8281 431
df98563e
C
432 this.video.likes += likesToIncrement
433 this.video.dislikes += dislikesToIncrement
20b40b19 434
22b59e80 435 this.video.buildLikeAndDislikePercents()
20b40b19 436 this.setVideoLikesBarTooltipText()
d38b8281
C
437 }
438
649fb082 439 private updateOtherVideosDisplayed () {
f6dc2fff 440 if (this.video && this.otherVideos && this.otherVideos.length > 0) {
649fb082
C
441 this.otherVideosDisplayed = this.otherVideos.filter(v => v.uuid !== this.video.uuid)
442 }
443 }
444
df98563e
C
445 private setOpenGraphTags () {
446 this.metaService.setTitle(this.video.name)
758b996d 447
df98563e 448 this.metaService.setTag('og:type', 'video')
3ec343a4 449
df98563e
C
450 this.metaService.setTag('og:title', this.video.name)
451 this.metaService.setTag('name', this.video.name)
3ec343a4 452
df98563e
C
453 this.metaService.setTag('og:description', this.video.description)
454 this.metaService.setTag('description', this.video.description)
3ec343a4 455
d38309c3 456 this.metaService.setTag('og:image', this.video.previewPath)
3ec343a4 457
df98563e 458 this.metaService.setTag('og:duration', this.video.duration.toString())
3ec343a4 459
df98563e 460 this.metaService.setTag('og:site_name', 'PeerTube')
3ec343a4 461
df98563e
C
462 this.metaService.setTag('og:url', window.location.href)
463 this.metaService.setTag('url', window.location.href)
3ec343a4 464 }
1f3e9fec 465
d4c6a3b9
C
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 }
22b59e80 473
22b59e80
C
474 private acceptedPrivacyConcern () {
475 localStorage.setItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY, 'true')
2b3b76ab 476 this.hasAlreadyAcceptedPrivacyConcern = true
22b59e80 477 }
dc8bc31b 478}