From a685e25ca05f08ad1b3f7fbaccc8744727bd8d27 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 9 Oct 2017 14:28:44 +0200 Subject: Try to optimize frontend --- client/src/app/videos/+video-watch/index.ts | 1 + .../+video-watch/video-magnet.component.html | 20 ++ .../videos/+video-watch/video-magnet.component.ts | 27 ++ .../+video-watch/video-report.component.html | 38 +++ .../videos/+video-watch/video-report.component.ts | 69 +++++ .../videos/+video-watch/video-share.component.html | 29 ++ .../videos/+video-watch/video-share.component.ts | 42 +++ .../+video-watch/video-watch-routing.module.ts | 20 ++ .../videos/+video-watch/video-watch.component.html | 184 +++++++++++++ .../videos/+video-watch/video-watch.component.scss | 245 +++++++++++++++++ .../videos/+video-watch/video-watch.component.ts | 299 +++++++++++++++++++++ .../app/videos/+video-watch/video-watch.module.ts | 34 +++ 12 files changed, 1008 insertions(+) create mode 100644 client/src/app/videos/+video-watch/index.ts create mode 100644 client/src/app/videos/+video-watch/video-magnet.component.html create mode 100644 client/src/app/videos/+video-watch/video-magnet.component.ts create mode 100644 client/src/app/videos/+video-watch/video-report.component.html create mode 100644 client/src/app/videos/+video-watch/video-report.component.ts create mode 100644 client/src/app/videos/+video-watch/video-share.component.html create mode 100644 client/src/app/videos/+video-watch/video-share.component.ts create mode 100644 client/src/app/videos/+video-watch/video-watch-routing.module.ts create mode 100644 client/src/app/videos/+video-watch/video-watch.component.html create mode 100644 client/src/app/videos/+video-watch/video-watch.component.scss create mode 100644 client/src/app/videos/+video-watch/video-watch.component.ts create mode 100644 client/src/app/videos/+video-watch/video-watch.module.ts (limited to 'client/src/app/videos/+video-watch') diff --git a/client/src/app/videos/+video-watch/index.ts b/client/src/app/videos/+video-watch/index.ts new file mode 100644 index 000000000..b19bfdb1e --- /dev/null +++ b/client/src/app/videos/+video-watch/index.ts @@ -0,0 +1 @@ +export * from './video-watch.module' diff --git a/client/src/app/videos/+video-watch/video-magnet.component.html b/client/src/app/videos/+video-watch/video-magnet.component.html new file mode 100644 index 000000000..484280c45 --- /dev/null +++ b/client/src/app/videos/+video-watch/video-magnet.component.html @@ -0,0 +1,20 @@ + diff --git a/client/src/app/videos/+video-watch/video-magnet.component.ts b/client/src/app/videos/+video-watch/video-magnet.component.ts new file mode 100644 index 000000000..f9432e92c --- /dev/null +++ b/client/src/app/videos/+video-watch/video-magnet.component.ts @@ -0,0 +1,27 @@ +import { Component, Input, ViewChild } from '@angular/core' + +import { ModalDirective } from 'ngx-bootstrap/modal' + +import { Video } from '../shared' + +@Component({ + selector: 'my-video-magnet', + templateUrl: './video-magnet.component.html' +}) +export class VideoMagnetComponent { + @Input() video: Video = null + + @ViewChild('modal') modal: ModalDirective + + constructor () { + // empty + } + + show () { + this.modal.show() + } + + hide () { + this.modal.hide() + } +} diff --git a/client/src/app/videos/+video-watch/video-report.component.html b/client/src/app/videos/+video-watch/video-report.component.html new file mode 100644 index 000000000..741080ead --- /dev/null +++ b/client/src/app/videos/+video-watch/video-report.component.html @@ -0,0 +1,38 @@ + diff --git a/client/src/app/videos/+video-watch/video-report.component.ts b/client/src/app/videos/+video-watch/video-report.component.ts new file mode 100644 index 000000000..d9c83a640 --- /dev/null +++ b/client/src/app/videos/+video-watch/video-report.component.ts @@ -0,0 +1,69 @@ +import { Component, Input, OnInit, ViewChild } from '@angular/core' +import { FormBuilder, FormGroup } from '@angular/forms' + +import { ModalDirective } from 'ngx-bootstrap/modal' +import { NotificationsService } from 'angular2-notifications' + +import { FormReactive, VideoAbuseService, VIDEO_ABUSE_REASON } from '../../shared' +import { Video, VideoService } from '../shared' + +@Component({ + selector: 'my-video-report', + templateUrl: './video-report.component.html' +}) +export class VideoReportComponent extends FormReactive implements OnInit { + @Input() video: Video = null + + @ViewChild('modal') modal: ModalDirective + + error: string = null + form: FormGroup + formErrors = { + reason: '' + } + validationMessages = { + reason: VIDEO_ABUSE_REASON.MESSAGES + } + + constructor ( + private formBuilder: FormBuilder, + private videoAbuseService: VideoAbuseService, + private notificationsService: NotificationsService + ) { + super() + } + + ngOnInit () { + this.buildForm() + } + + buildForm () { + this.form = this.formBuilder.group({ + reason: [ '', VIDEO_ABUSE_REASON.VALIDATORS ] + }) + + this.form.valueChanges.subscribe(data => this.onValueChanged(data)) + } + + show () { + this.modal.show() + } + + hide () { + this.modal.hide() + } + + report () { + const reason = this.form.value['reason'] + + this.videoAbuseService.reportVideo(this.video.id, reason) + .subscribe( + () => { + this.notificationsService.success('Success', 'Video reported.') + this.hide() + }, + + err => this.notificationsService.error('Error', err.message) + ) + } +} diff --git a/client/src/app/videos/+video-watch/video-share.component.html b/client/src/app/videos/+video-watch/video-share.component.html new file mode 100644 index 000000000..88f59c063 --- /dev/null +++ b/client/src/app/videos/+video-watch/video-share.component.html @@ -0,0 +1,29 @@ + diff --git a/client/src/app/videos/+video-watch/video-share.component.ts b/client/src/app/videos/+video-watch/video-share.component.ts new file mode 100644 index 000000000..133f93498 --- /dev/null +++ b/client/src/app/videos/+video-watch/video-share.component.ts @@ -0,0 +1,42 @@ +import { Component, Input, ViewChild } from '@angular/core' + +import { ModalDirective } from 'ngx-bootstrap/modal' + +import { Video } from '../shared' + +@Component({ + selector: 'my-video-share', + templateUrl: './video-share.component.html' +}) +export class VideoShareComponent { + @Input() video: Video = null + + @ViewChild('modal') modal: ModalDirective + + constructor () { + // empty + } + + show () { + this.modal.show() + } + + hide () { + this.modal.hide() + } + + getVideoIframeCode () { + return '' + } + + getVideoUrl () { + return window.location.href + } + + notSecure () { + return window.location.protocol === 'http:' + } +} diff --git a/client/src/app/videos/+video-watch/video-watch-routing.module.ts b/client/src/app/videos/+video-watch/video-watch-routing.module.ts new file mode 100644 index 000000000..97fa5c725 --- /dev/null +++ b/client/src/app/videos/+video-watch/video-watch-routing.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core' +import { RouterModule, Routes } from '@angular/router' + +import { MetaGuard } from '@ngx-meta/core' + +import { VideoWatchComponent } from './video-watch.component' + +const videoWatchRoutes: Routes = [ + { + path: '', + component: VideoWatchComponent, + canActivateChild: [ MetaGuard ] + } +] + +@NgModule({ + imports: [ RouterModule.forChild(videoWatchRoutes) ], + exports: [ RouterModule ] +}) +export class VideoWatchRoutingModule {} diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html new file mode 100644 index 000000000..88863131a --- /dev/null +++ b/client/src/app/videos/+video-watch/video-watch.component.html @@ -0,0 +1,184 @@ +
+
+ The video load seems to be abnormally long. + +
+
+ +
+ +
+ +
+ +
Video not found :'(
+
+ + +
+
Download: {{ downloadSpeed | bytes }}/s
+
Upload: {{ uploadSpeed | bytes }}/s
+
Number of peers: {{ numPeers }}
+
+ + +
+
+
+ {{ video.name }} +
+ +
+ {{ video.views}} views +
+
+ +
+ + + + + + +
+
+ + + + {{ video.likes }} + +
+ +
+ + + + {{ video.dislikes }} + +
+
+
+ +
+
+
+ Published on {{ video.createdAt | date:'short' }} +
+ +
+ {{ video.description }} +
+
+ +
+
+ + Category: + + + {{ video.categoryLabel }} + +
+ +
+ + Licence: + + + {{ video.licenceLabel }} + +
+ +
+ + Language: + + + {{ video.languageLabel }} + +
+ +
+ + Tags: + + + +
+ +
+
+
+ + + + + + diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss new file mode 100644 index 000000000..69661747c --- /dev/null +++ b/client/src/app/videos/+video-watch/video-watch.component.scss @@ -0,0 +1,245 @@ +#video-container { + width: 100%; + height: 100%; +} + +#video-not-found { + height: 300px; + line-height: 300px; + margin-top: 50px; + text-align: center; + font-weight: bold; +} + +.embed-responsive { + height: 500px; + + @media screen and (max-width: 600px) { + height: 300px; + } +} + +#torrent-info { + font-size: 10px; + margin-top: 10px; + text-align: center; + + div { + min-width: 60px; + } +} + +#video-info { + .video-name-views { + font-weight: bold; + font-size: 18px; + height: $video-watch-title-height; + line-height: $video-watch-title-height; + + .video-name { + padding-left: $video-watch-info-padding-left; + } + + .video-views { + text-align: right; + // Keep a symmetry with the video name + padding-right: $video-watch-info-padding-left + } + + } + + .video-small-blocks { + height: $video-watch-info-height; + color: $video-watch-info-color; + border-color: $video-watch-border-color; + border-width: 1px 0px; + border-style: solid; + + .video-small-block { + height: $video-watch-info-height; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; + + a { + cursor: pointer; + transition: color 0.3s; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &, &:hover { + color: inherit; + text-decoration:none; + } + + &:hover { + color: #000 !important; + } + + &:hover > .glyphicon { + opacity: 1 !important; + } + } + + .option .glyphicon { + font-size: 22px; + color: inherit; + opacity: 0.15; + margin-bottom: 10px; + transition: opacity 0.3s; + } + + .video-small-block-text { + font-size: 15px; + font-weight: bold; + } + } + + .video-small-block:not(:last-child) { + border-width: 0 1px 0 0; + border-color: $video-watch-border-color; + border-style: solid; + } + + .video-small-block-author, .video-small-block-more { + a.option { + display: block; + + .glyphicon { + display: block; + } + } + } + + .video-small-block-share, .video-small-block-more { + a.option { + display: block; + + .glyphicon { + display: block; + } + } + } + + .video-small-block-more .video-small-block-dropdown { + position: relative; + + .dropdown-item .glyphicon { + margin-right: 5px; + } + } + + .video-small-block-rating { + + .video-small-block-like { + margin-bottom: 10px; + } + + .video-small-block-text { + vertical-align: top; + } + + .glyphicon { + font-size: 18px; + margin: 0 10px 0 0; + opacity: 0.3; + } + + .interactive { + cursor: pointer; + transition: opacity, color 0.3s; + + &.activated, &:hover { + opacity: 1; + color: #000; + } + } + } + } + + .video-details { + margin-top: 30px; + + .video-details-date-description { + padding-left: $video-watch-info-padding-left; + + .video-details-date { + font-weight: bold; + margin-bottom: 30px; + } + } + + .video-details-attributes { + font-weight: bold; + font-size: 12px; + + .video-details-attribute-label { + color: $video-watch-info-color; + display: inline-block; + width: 60px; + margin-right: 5px; + } + } + + .video-details-tags { + display: inline-block; + + a { + display: inline-block; + margin-right: 3px; + font-size: 11px; + } + } + } + + @media screen and (max-width: 400px) { + .video-name-views { + font-size: 16px !important; + } + } + + @media screen and (max-width: 800px) { + .video-name-views { + .video-name { + padding-left: 5px; + padding-right: 0px; + } + + .video-views { + padding-left: 0px; + padding-right: 5px; + } + } + + .video-small-blocks { + a, .video-small-block-text { + font-size: 13px !important; + } + + .glyphicon { + font-size: 18px !important; + } + + .video-small-block-author { + padding-left: 10px; + } + } + + .video-details { + .video-details-date-description { + padding-left: 10px; + font-size: 13px !important; + } + + .video-details-attributes { + font-size: 11px !important; + + .video-details-attribute-label { + width: 50px; + } + } + } + } +} diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts new file mode 100644 index 000000000..874dd5997 --- /dev/null +++ b/client/src/app/videos/+video-watch/video-watch.component.ts @@ -0,0 +1,299 @@ +import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { Observable } from 'rxjs/Observable' +import { Subscription } from 'rxjs/Subscription' + +import videojs from 'video.js' +import '../../../assets/player/peertube-videojs-plugin' + +import { MetaService } from '@ngx-meta/core' +import { NotificationsService } from 'angular2-notifications' + +import { AuthService, ConfirmService } from '../../core' +import { VideoMagnetComponent } from './video-magnet.component' +import { VideoShareComponent } from './video-share.component' +import { VideoReportComponent } from './video-report.component' +import { Video, VideoService } from '../shared' +import { WebTorrentService } from './webtorrent.service' +import { UserVideoRateType, VideoRateType } from '../../../../../shared' + +@Component({ + selector: 'my-video-watch', + templateUrl: './video-watch.component.html', + styleUrls: [ './video-watch.component.scss' ] +}) +export class VideoWatchComponent implements OnInit, OnDestroy { + @ViewChild('videoMagnetModal') videoMagnetModal: VideoMagnetComponent + @ViewChild('videoShareModal') videoShareModal: VideoShareComponent + @ViewChild('videoReportModal') videoReportModal: VideoReportComponent + + downloadSpeed: number + error = false + loading = false + numPeers: number + player: videojs.Player + playerElement: HTMLMediaElement + uploadSpeed: number + userRating: UserVideoRateType = null + video: Video = null + videoNotFound = false + + private paramsSub: Subscription + + constructor ( + private elementRef: ElementRef, + private route: ActivatedRoute, + private router: Router, + private videoService: VideoService, + private confirmService: ConfirmService, + private metaService: MetaService, + private authService: AuthService, + private notificationsService: NotificationsService + ) {} + + ngOnInit () { + this.paramsSub = this.route.params.subscribe(routeParams => { + let uuid = routeParams['uuid'] + this.videoService.getVideo(uuid).subscribe( + video => this.onVideoFetched(video), + + error => { + console.error(error) + this.videoNotFound = true + } + ) + }) + } + + ngOnDestroy () { + // Remove player if it exists + if (this.videoNotFound === false) { + videojs(this.playerElement).dispose() + } + + // Unsubscribe subscriptions + this.paramsSub.unsubscribe() + } + + setLike () { + if (this.isUserLoggedIn() === false) return + // Already liked this video + if (this.userRating === 'like') return + + this.videoService.setVideoLike(this.video.id) + .subscribe( + () => { + // Update the video like attribute + this.updateVideoRating(this.userRating, 'like') + this.userRating = 'like' + }, + + err => this.notificationsService.error('Error', err.message) + ) + } + + setDislike () { + if (this.isUserLoggedIn() === false) return + // Already disliked this video + if (this.userRating === 'dislike') return + + this.videoService.setVideoDislike(this.video.id) + .subscribe( + () => { + // Update the video dislike attribute + this.updateVideoRating(this.userRating, 'dislike') + this.userRating = 'dislike' + }, + + err => this.notificationsService.error('Error', err.message) + ) + } + + removeVideo (event: Event) { + event.preventDefault() + + this.confirmService.confirm('Do you really want to delete this video?', 'Delete').subscribe( + res => { + if (res === false) return + + this.videoService.removeVideo(this.video.id) + .subscribe( + status => { + this.notificationsService.success('Success', `Video ${this.video.name} deleted.`) + // Go back to the video-list. + this.router.navigate(['/videos/list']) + }, + + error => this.notificationsService.error('Error', error.text) + ) + } + ) + } + + blacklistVideo (event: Event) { + event.preventDefault() + + this.confirmService.confirm('Do you really want to blacklist this video ?', 'Blacklist').subscribe( + res => { + if (res === false) return + + this.videoService.blacklistVideo(this.video.id) + .subscribe( + status => { + this.notificationsService.success('Success', `Video ${this.video.name} had been blacklisted.`) + this.router.navigate(['/videos/list']) + }, + + error => this.notificationsService.error('Error', error.text) + ) + } + ) + } + + showReportModal (event: Event) { + event.preventDefault() + this.videoReportModal.show() + } + + showShareModal () { + this.videoShareModal.show() + } + + showMagnetUriModal (event: Event) { + event.preventDefault() + this.videoMagnetModal.show() + } + + isUserLoggedIn () { + return this.authService.isLoggedIn() + } + + canUserUpdateVideo () { + return this.video.isUpdatableBy(this.authService.getUser()) + } + + isVideoRemovable () { + return this.video.isRemovableBy(this.authService.getUser()) + } + + isVideoBlacklistable () { + return this.video.isBlackistableBy(this.authService.getUser()) + } + + private handleError (err: any) { + const errorMessage: string = typeof err === 'string' ? err : err.message + let message = '' + + if (errorMessage.indexOf('http error') !== -1) { + message = 'Cannot fetch video from server, maybe down.' + } else { + message = errorMessage + } + + this.notificationsService.error('Error', message) + } + + private checkUserRating () { + // Unlogged users do not have ratings + if (this.isUserLoggedIn() === false) return + + this.videoService.getUserVideoRating(this.video.id) + .subscribe( + ratingObject => { + if (ratingObject) { + this.userRating = ratingObject.rating + } + }, + + err => this.notificationsService.error('Error', err.message) + ) + } + + private onVideoFetched (video: Video) { + this.video = video + + let observable + if (this.video.isVideoNSFWForUser(this.authService.getUser())) { + observable = this.confirmService.confirm('This video is not safe for work. Are you sure you want to watch it?', 'NSFW') + } else { + observable = Observable.of(true) + } + + observable.subscribe( + res => { + if (res === false) { + return this.router.navigate([ '/videos/list' ]) + } + + this.playerElement = this.elementRef.nativeElement.querySelector('#video-container') + + const videojsOptions = { + controls: true, + autoplay: true, + plugins: { + peertube: { + videoFiles: this.video.files, + playerElement: this.playerElement, + autoplay: true, + peerTubeLink: false + } + } + } + + const self = this + videojs(this.playerElement, videojsOptions, function () { + self.player = this + this.on('customError', (event, data) => { + self.handleError(data.err) + }) + + this.on('torrentInfo', (event, data) => { + self.downloadSpeed = data.downloadSpeed + self.numPeers = data.numPeers + self.uploadSpeed = data.uploadSpeed + }) + }) + + this.setOpenGraphTags() + this.checkUserRating() + } + ) + } + + private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) { + let likesToIncrement = 0 + let dislikesToIncrement = 0 + + if (oldRating) { + if (oldRating === 'like') likesToIncrement-- + if (oldRating === 'dislike') dislikesToIncrement-- + } + + if (newRating === 'like') likesToIncrement++ + if (newRating === 'dislike') dislikesToIncrement++ + + this.video.likes += likesToIncrement + this.video.dislikes += dislikesToIncrement + } + + private setOpenGraphTags () { + this.metaService.setTitle(this.video.name) + + this.metaService.setTag('og:type', 'video') + + this.metaService.setTag('og:title', this.video.name) + this.metaService.setTag('name', this.video.name) + + this.metaService.setTag('og:description', this.video.description) + this.metaService.setTag('description', this.video.description) + + this.metaService.setTag('og:image', this.video.previewPath) + + this.metaService.setTag('og:duration', this.video.duration.toString()) + + this.metaService.setTag('og:site_name', 'PeerTube') + + this.metaService.setTag('og:url', window.location.href) + this.metaService.setTag('url', window.location.href) + } +} diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts new file mode 100644 index 000000000..5f20b171e --- /dev/null +++ b/client/src/app/videos/+video-watch/video-watch.module.ts @@ -0,0 +1,34 @@ +import { NgModule } from '@angular/core' + +import { VideoWatchRoutingModule } from './video-watch-routing.module' +import { VideoService } from '../shared' +import { SharedModule } from '../../shared' + +import { VideoWatchComponent } from './video-watch.component' +import { VideoReportComponent } from './video-report.component' +import { VideoShareComponent } from './video-share.component' +import { VideoMagnetComponent } from './video-magnet.component' + +@NgModule({ + imports: [ + VideoWatchRoutingModule, + SharedModule + ], + + declarations: [ + VideoWatchComponent, + + VideoMagnetComponent, + VideoShareComponent, + VideoReportComponent + ], + + exports: [ + VideoWatchComponent + ], + + providers: [ + VideoService + ] +}) +export class VideoWatchModule { } -- cgit v1.2.3