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