]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - client/src/app/videos/+video-watch/video-watch.component.ts
Handle resizes on videos list
[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 = ''
73e09f27 52 hasAlreadyAcceptedPrivacyConcern = false
df98563e 53
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
73e09f27
C
263 acceptedPrivacyConcern () {
264 localStorage.setItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY, 'true')
265 this.hasAlreadyAcceptedPrivacyConcern = true
266 }
267
2de96f4d
C
268 private updateVideoDescription (description: string) {
269 this.video.description = description
270 this.setVideoDescriptionHTML()
271 }
272
273 private setVideoDescriptionHTML () {
cadb46d8
C
274 if (!this.video.description) {
275 this.videoHTMLDescription = ''
276 return
277 }
278
07fa4c97 279 this.videoHTMLDescription = this.markdownService.textMarkdownToHTML(this.video.description)
2de96f4d
C
280 }
281
e9189001
C
282 private setVideoLikesBarTooltipText () {
283 this.likesBarTooltipText = `${this.video.likes} likes / ${this.video.dislikes} dislikes`
284 }
285
0c31c33d
C
286 private handleError (err: any) {
287 const errorMessage: string = typeof err === 'string' ? err : err.message
bf5685f0
C
288 if (!errorMessage) return
289
0c31c33d
C
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
df98563e 301 private checkUserRating () {
d38b8281 302 // Unlogged users do not have ratings
df98563e 303 if (this.isUserLoggedIn() === false) return
d38b8281
C
304
305 this.videoService.getUserVideoRating(this.video.id)
306 .subscribe(
b632e904 307 ratingObject => {
d38b8281 308 if (ratingObject) {
df98563e 309 this.userRating = ratingObject.rating
d38b8281
C
310 }
311 },
312
bfb3a98f 313 err => this.notificationsService.error('Error', err.message)
df98563e 314 )
d38b8281
C
315 }
316
22b59e80 317 private async onVideoFetched (video: VideoDetails) {
df98563e 318 this.video = video
92fb909c 319
649fb082 320 this.updateOtherVideosDisplayed()
57a49263 321
b2731bff 322 if (this.video.isVideoNSFWForUser(this.user)) {
22b59e80 323 const res = await this.confirmService.confirm(
d6e32a2e
C
324 'This video contains mature or explicit content. Are you sure you want to watch it?',
325 'Mature or explicit content'
326 )
901637bb 327 if (res === false) return this.redirectService.redirectToHomepage()
92fb909c
C
328 }
329
22b59e80
C
330 // Player was already loaded
331 if (this.videoPlayerLoaded !== true) {
332 this.playerElement = this.elementRef.nativeElement.querySelector('#video-element')
ed9f9f5f 333
22b59e80
C
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 }
0826c92d 338
22b59e80
C
339 const videojsOptions = {
340 controls: true,
341 autoplay: this.isAutoplay(),
1198a08c 342 playbackRates: [ 0.5, 1, 1.5, 2 ],
22b59e80
C
343 plugins: {
344 peertube: {
345 videoFiles: this.video.files,
346 playerElement: this.playerElement,
22b59e80
C
347 videoViewUrl: this.videoService.getVideoViewUrl(this.video.uuid),
348 videoDuration: this.video.duration
349 },
350 hotkeys: {
351 enableVolumeScroll: false
aa8b6df4 352 }
3ec8dc09
C
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 ]
ed9f9f5f 376 }
22b59e80 377 }
aa8b6df4 378
22b59e80 379 this.videoPlayerLoaded = true
9d9597df 380
22b59e80
C
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()
92fb909c
C
398 }
399
57a49263
BB
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
154898b0 425 private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) {
df98563e
C
426 let likesToIncrement = 0
427 let dislikesToIncrement = 0
d38b8281
C
428
429 if (oldRating) {
df98563e
C
430 if (oldRating === 'like') likesToIncrement--
431 if (oldRating === 'dislike') dislikesToIncrement--
d38b8281
C
432 }
433
df98563e
C
434 if (newRating === 'like') likesToIncrement++
435 if (newRating === 'dislike') dislikesToIncrement++
d38b8281 436
df98563e
C
437 this.video.likes += likesToIncrement
438 this.video.dislikes += dislikesToIncrement
20b40b19 439
22b59e80 440 this.video.buildLikeAndDislikePercents()
20b40b19 441 this.setVideoLikesBarTooltipText()
d38b8281
C
442 }
443
649fb082 444 private updateOtherVideosDisplayed () {
f6dc2fff 445 if (this.video && this.otherVideos && this.otherVideos.length > 0) {
649fb082
C
446 this.otherVideosDisplayed = this.otherVideos.filter(v => v.uuid !== this.video.uuid)
447 }
448 }
449
df98563e
C
450 private setOpenGraphTags () {
451 this.metaService.setTitle(this.video.name)
758b996d 452
df98563e 453 this.metaService.setTag('og:type', 'video')
3ec343a4 454
df98563e
C
455 this.metaService.setTag('og:title', this.video.name)
456 this.metaService.setTag('name', this.video.name)
3ec343a4 457
df98563e
C
458 this.metaService.setTag('og:description', this.video.description)
459 this.metaService.setTag('description', this.video.description)
3ec343a4 460
d38309c3 461 this.metaService.setTag('og:image', this.video.previewPath)
3ec343a4 462
df98563e 463 this.metaService.setTag('og:duration', this.video.duration.toString())
3ec343a4 464
df98563e 465 this.metaService.setTag('og:site_name', 'PeerTube')
3ec343a4 466
df98563e
C
467 this.metaService.setTag('og:url', window.location.href)
468 this.metaService.setTag('url', window.location.href)
3ec343a4 469 }
1f3e9fec 470
d4c6a3b9
C
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 }
dc8bc31b 478}