aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/videos/+video-watch
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-10-09 14:28:44 +0200
committerChocobozzz <florian.bigard@gmail.com>2017-10-09 14:28:44 +0200
commita685e25ca05f08ad1b3f7fbaccc8744727bd8d27 (patch)
treee50fbc2f260a0017113c4668c3c0f3d2fd76ab87 /client/src/app/videos/+video-watch
parent2ed6a0aedc2d2f6b1ac2fd9a1ac137772831f713 (diff)
downloadPeerTube-a685e25ca05f08ad1b3f7fbaccc8744727bd8d27.tar.gz
PeerTube-a685e25ca05f08ad1b3f7fbaccc8744727bd8d27.tar.zst
PeerTube-a685e25ca05f08ad1b3f7fbaccc8744727bd8d27.zip
Try to optimize frontend
Diffstat (limited to 'client/src/app/videos/+video-watch')
-rw-r--r--client/src/app/videos/+video-watch/index.ts1
-rw-r--r--client/src/app/videos/+video-watch/video-magnet.component.html20
-rw-r--r--client/src/app/videos/+video-watch/video-magnet.component.ts27
-rw-r--r--client/src/app/videos/+video-watch/video-report.component.html38
-rw-r--r--client/src/app/videos/+video-watch/video-report.component.ts69
-rw-r--r--client/src/app/videos/+video-watch/video-share.component.html29
-rw-r--r--client/src/app/videos/+video-watch/video-share.component.ts42
-rw-r--r--client/src/app/videos/+video-watch/video-watch-routing.module.ts20
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html184
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss245
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts299
-rw-r--r--client/src/app/videos/+video-watch/video-watch.module.ts34
12 files changed, 1008 insertions, 0 deletions
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 @@
1<div bsModal #modal="bs-modal" class="modal" tabindex="-1">
2 <div class="modal-dialog">
3 <div class="modal-content modal-lg">
4
5 <div class="modal-header">
6 <button type="button" class="close" aria-label="Close" (click)="hide()">
7 <span aria-hidden="true">&times;</span>
8 </button>
9 <h4 class="modal-title">Magnet Uri</h4>
10 </div>
11
12 <div class="modal-body">
13 <div *ngFor="let file of video.files">
14 <label>{{ file.resolutionLabel }}</label>
15 <input #magnetUriInput (click)="magnetUriInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="file.magnetUri" />
16 </div>
17 </div>
18 </div>
19 </div>
20</div>
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 @@
1import { Component, Input, ViewChild } from '@angular/core'
2
3import { ModalDirective } from 'ngx-bootstrap/modal'
4
5import { Video } from '../shared'
6
7@Component({
8 selector: 'my-video-magnet',
9 templateUrl: './video-magnet.component.html'
10})
11export class VideoMagnetComponent {
12 @Input() video: Video = null
13
14 @ViewChild('modal') modal: ModalDirective
15
16 constructor () {
17 // empty
18 }
19
20 show () {
21 this.modal.show()
22 }
23
24 hide () {
25 this.modal.hide()
26 }
27}
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 @@
1<div bsModal #modal="bs-modal" class="modal" tabindex="-1">
2 <div class="modal-dialog">
3 <div class="modal-content modal-lg">
4
5 <div class="modal-header">
6 <button type="button" class="close" aria-label="Close" (click)="hide()">
7 <span aria-hidden="true">&times;</span>
8 </button>
9 <h4 class="modal-title">Report video</h4>
10 </div>
11
12 <div class="modal-body">
13
14 <form novalidate [formGroup]="form">
15 <div class="form-group">
16 <label for="description">Reason</label>
17 <textarea
18 id="reason" class="form-control" placeholder="Reason..."
19 formControlName="reason"
20 >
21 </textarea>
22 <div *ngIf="formErrors.reason" class="alert alert-danger">
23 {{ formErrors.reason }}
24 </div>
25 </div>
26
27 <div class="form-group">
28 <input
29 type="button" value="Report" class="btn btn-default form-control"
30 [disabled]="!form.valid" (click)="report()"
31 >
32 </div>
33 </form>
34
35 </div>
36 </div>
37 </div>
38</div>
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 @@
1import { Component, Input, OnInit, ViewChild } from '@angular/core'
2import { FormBuilder, FormGroup } from '@angular/forms'
3
4import { ModalDirective } from 'ngx-bootstrap/modal'
5import { NotificationsService } from 'angular2-notifications'
6
7import { FormReactive, VideoAbuseService, VIDEO_ABUSE_REASON } from '../../shared'
8import { Video, VideoService } from '../shared'
9
10@Component({
11 selector: 'my-video-report',
12 templateUrl: './video-report.component.html'
13})
14export class VideoReportComponent extends FormReactive implements OnInit {
15 @Input() video: Video = null
16
17 @ViewChild('modal') modal: ModalDirective
18
19 error: string = null
20 form: FormGroup
21 formErrors = {
22 reason: ''
23 }
24 validationMessages = {
25 reason: VIDEO_ABUSE_REASON.MESSAGES
26 }
27
28 constructor (
29 private formBuilder: FormBuilder,
30 private videoAbuseService: VideoAbuseService,
31 private notificationsService: NotificationsService
32 ) {
33 super()
34 }
35
36 ngOnInit () {
37 this.buildForm()
38 }
39
40 buildForm () {
41 this.form = this.formBuilder.group({
42 reason: [ '', VIDEO_ABUSE_REASON.VALIDATORS ]
43 })
44
45 this.form.valueChanges.subscribe(data => this.onValueChanged(data))
46 }
47
48 show () {
49 this.modal.show()
50 }
51
52 hide () {
53 this.modal.hide()
54 }
55
56 report () {
57 const reason = this.form.value['reason']
58
59 this.videoAbuseService.reportVideo(this.video.id, reason)
60 .subscribe(
61 () => {
62 this.notificationsService.success('Success', 'Video reported.')
63 this.hide()
64 },
65
66 err => this.notificationsService.error('Error', err.message)
67 )
68 }
69}
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 @@
1<div bsModal #modal="bs-modal" class="modal" tabindex="-1">
2 <div class="modal-dialog modal-lg">
3 <div class="modal-content">
4
5 <div class="modal-header">
6 <button type="button" class="close" aria-label="Close" (click)="hide()">
7 <span aria-hidden="true">&times;</span>
8 </button>
9 <h4 class="modal-title">Share</h4>
10 </div>
11
12 <div class="modal-body">
13 <div class="form-group">
14 <label>URL</label>
15 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getVideoUrl()" />
16 </div>
17
18 <div class="form-group">
19 <label>Embed</label>
20 <input #shareInput (click)="shareInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getVideoIframeCode()" />
21 </div>
22
23 <div *ngIf="notSecure()" class="alert alert-warning">
24 The url is not secured (no HTTPS), so the embed video won't work on HTTPS websites (web browsers block non secured HTTP requests on HTTPS websites).
25 </div>
26 </div>
27 </div>
28 </div>
29</div>
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 @@
1import { Component, Input, ViewChild } from '@angular/core'
2
3import { ModalDirective } from 'ngx-bootstrap/modal'
4
5import { Video } from '../shared'
6
7@Component({
8 selector: 'my-video-share',
9 templateUrl: './video-share.component.html'
10})
11export class VideoShareComponent {
12 @Input() video: Video = null
13
14 @ViewChild('modal') modal: ModalDirective
15
16 constructor () {
17 // empty
18 }
19
20 show () {
21 this.modal.show()
22 }
23
24 hide () {
25 this.modal.hide()
26 }
27
28 getVideoIframeCode () {
29 return '<iframe width="560" height="315" ' +
30 'src="' + window.location.origin + '/videos/embed/' + this.video.uuid + '" ' +
31 'frameborder="0" allowfullscreen>' +
32 '</iframe>'
33 }
34
35 getVideoUrl () {
36 return window.location.href
37 }
38
39 notSecure () {
40 return window.location.protocol === 'http:'
41 }
42}
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 @@
1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router'
3
4import { MetaGuard } from '@ngx-meta/core'
5
6import { VideoWatchComponent } from './video-watch.component'
7
8const videoWatchRoutes: Routes = [
9 {
10 path: '',
11 component: VideoWatchComponent,
12 canActivateChild: [ MetaGuard ]
13 }
14]
15
16@NgModule({
17 imports: [ RouterModule.forChild(videoWatchRoutes) ],
18 exports: [ RouterModule ]
19})
20export 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 @@
1<div *ngIf="error" class="row">
2 <div class="alert alert-danger">
3 The video load seems to be abnormally long.
4 <ul>
5 <li>Maybe the server {{ video.podHost }} is down :(</li>
6 <li>
7 If not, you can report an issue on
8 <a href="https://github.com/Chocobozzz/PeerTube/issues" title="Report an issue">
9 https://github.com/Chocobozzz/PeerTube/issues
10 </a>
11 </li>
12 </ul>
13 </div>
14</div>
15
16<div class="row">
17 <!-- We need the video container for videojs so we just hide it -->
18 <div [hidden]="videoNotFound" class="embed-responsive embed-responsive-19by9">
19 <video id="video-container" class="video-js vjs-sublime-skin"></video>
20 </div>
21
22 <div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div>
23</div>
24
25<!-- P2P informations -->
26<div id="torrent-info" class="row">
27 <div id="torrent-info-download" class="col-md-4 col-sm-4 col-xs-4">Download: {{ downloadSpeed | bytes }}/s</div>
28 <div id="torrent-info-upload" class="col-md-4 col-sm-4 col-xs-4">Upload: {{ uploadSpeed | bytes }}/s</div>
29 <div id="torrent-info-peers" class="col-md-4 col-sm-4 col-xs-4">Number of peers: {{ numPeers }}</div>
30</div>
31
32<!-- Video informations -->
33<div *ngIf="video !== null" id="video-info">
34 <div class="row video-name-views">
35 <div class="col-xs-8 col-md-8 video-name">
36 {{ video.name }}
37 </div>
38
39 <div class="col-xs-4 col-md-4 pull-right video-views">
40 {{ video.views}} views
41 </div>
42 </div>
43
44 <div class="row video-small-blocks">
45 <div class="col-xs-5 col-xs-3 col-md-3 video-small-block video-small-block-author">
46 <a class="option" title="Access to all videos of this user" [routerLink]="['/videos/list', { field: 'author', search: video.author }]">
47 <span class="glyphicon glyphicon-user"></span>
48 <span class="video-small-block-text">{{ video.by }}</span>
49 </a>
50 </div>
51
52 <div class="col-xs-2 col-md-3 video-small-block video-small-block-share">
53 <a class="option" (click)="showShareModal()" title="Share the video">
54 <span class="glyphicon glyphicon-share"></span>
55 <span class="video-small-block-text">Share</span>
56 </a>
57 </div>
58
59 <div class="col-xs-2 col-md-3 video-small-block video-small-block-more">
60 <div class="video-small-block-dropdown" dropdown dropup="true" placement="right">
61 <a class="option" title="Access to more options" dropdownToggle>
62 <span class="glyphicon glyphicon-option-horizontal"></span>
63 <span class="video-small-block-text">More</span>
64 </a>
65
66 <ul *dropdownMenu class="dropdown-menu" id="more-menu" role="menu" aria-labelledby="single-button">
67 <li *ngIf="canUserUpdateVideo()" role="menuitem">
68 <a class="dropdown-item" title="Update this video" href="#" [routerLink]="[ '/videos/edit', video.uuid ]">
69 <span class="glyphicon glyphicon-pencil"></span> Update
70 </a>
71 </li>
72
73 <li role="menuitem">
74 <a class="dropdown-item" title="Get magnet URI" href="#" (click)="showMagnetUriModal($event)">
75 <span class="glyphicon glyphicon-magnet"></span> Magnet
76 </a>
77 </li>
78
79 <li *ngIf="isUserLoggedIn()" role="menuitem">
80 <a class="dropdown-item" title="Report this video" href="#" (click)="showReportModal($event)">
81 <span class="glyphicon glyphicon-alert"></span> Report
82 </a>
83 </li>
84
85 <li *ngIf="isVideoRemovable()" role="menuitem">
86 <a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)">
87 <span class="glyphicon glyphicon-remove"></span> Delete
88 </a>
89 </li>
90
91 <li *ngIf="isVideoBlacklistable()" role="menuitem">
92 <a class="dropdown-item" title="Blacklist this video" href="#" (click)="blacklistVideo($event)">
93 <span class="glyphicon glyphicon-eye-close"></span> Blacklist
94 </a>
95 </li>
96 </ul>
97 </div>
98 </div>
99
100 <div class="col-xs-3 col-md-3 video-small-block video-small-block-rating">
101 <div class="video-small-block-like">
102 <span
103 class="glyphicon glyphicon-thumbs-up" title="Like this video"
104 [ngClass]="{ 'interactive': isUserLoggedIn(), 'activated': userRating === 'like' }" (click)="setLike()"
105 ></span>
106
107 <span class="video-small-block-text">
108 {{ video.likes }}
109 </span>
110 </div>
111
112 <div class="video-small-block-dislike">
113 <span
114 class="glyphicon glyphicon-thumbs-down" title="Dislike this video"
115 [ngClass]="{ 'interactive': isUserLoggedIn(), 'activated': userRating === 'dislike' }" (click)="setDislike()"
116 ></span>
117
118 <span class="video-small-block-text">
119 {{ video.dislikes }}
120 </span>
121 </div>
122 </div>
123 </div>
124
125 <div class="row video-details">
126 <div class="video-details-date-description col-xs-8 col-md-9">
127 <div class="video-details-date">
128 Published on {{ video.createdAt | date:'short' }}
129 </div>
130
131 <div class="video-details-description">
132 {{ video.description }}
133 </div>
134 </div>
135
136 <div class="video-details-attributes col-xs-4 col-md-3">
137 <div class="video-details-attribute">
138 <span class="video-details-attribute-label">
139 Category:
140 </span>
141 <span class="video-details-attribute-value">
142 {{ video.categoryLabel }}
143 </span>
144 </div>
145
146 <div class="video-details-attribute">
147 <span class="video-details-attribute-label">
148 Licence:
149 </span>
150 <span class="video-details-attribute-value">
151 {{ video.licenceLabel }}
152 </span>
153 </div>
154
155 <div class="video-details-attribute">
156 <span class="video-details-attribute-label">
157 Language:
158 </span>
159 <span class="video-details-attribute-value">
160 {{ video.languageLabel }}
161 </span>
162 </div>
163
164 <div class="video-details-attribute">
165 <span class="video-details-attribute-label">
166 Tags:
167 </span>
168
169 <div class="video-details-tags">
170 <a *ngFor="let tag of video.tags" [routerLink]="['/videos/list', { field: 'tags', search: tag }]" class="label label-primary">
171 {{ tag }}
172 </a>
173 </div>
174 </div>
175
176 </div>
177 </div>
178</div>
179
180<ng-template [ngIf]="video !== null">
181 <my-video-share #videoShareModal [video]="video"></my-video-share>
182 <my-video-magnet #videoMagnetModal [video]="video"></my-video-magnet>
183 <my-video-report #videoReportModal [video]="video"></my-video-report>
184</ng-template>
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 @@
1#video-container {
2 width: 100%;
3 height: 100%;
4}
5
6#video-not-found {
7 height: 300px;
8 line-height: 300px;
9 margin-top: 50px;
10 text-align: center;
11 font-weight: bold;
12}
13
14.embed-responsive {
15 height: 500px;
16
17 @media screen and (max-width: 600px) {
18 height: 300px;
19 }
20}
21
22#torrent-info {
23 font-size: 10px;
24 margin-top: 10px;
25 text-align: center;
26
27 div {
28 min-width: 60px;
29 }
30}
31
32#video-info {
33 .video-name-views {
34 font-weight: bold;
35 font-size: 18px;
36 height: $video-watch-title-height;
37 line-height: $video-watch-title-height;
38
39 .video-name {
40 padding-left: $video-watch-info-padding-left;
41 }
42
43 .video-views {
44 text-align: right;
45 // Keep a symmetry with the video name
46 padding-right: $video-watch-info-padding-left
47 }
48
49 }
50
51 .video-small-blocks {
52 height: $video-watch-info-height;
53 color: $video-watch-info-color;
54 border-color: $video-watch-border-color;
55 border-width: 1px 0px;
56 border-style: solid;
57
58 .video-small-block {
59 height: $video-watch-info-height;
60 display: flex;
61 flex-direction: column;
62 justify-content: center;
63 text-align: center;
64
65 a {
66 cursor: pointer;
67 transition: color 0.3s;
68 white-space: nowrap;
69 overflow: hidden;
70 text-overflow: ellipsis;
71
72 &, &:hover {
73 color: inherit;
74 text-decoration:none;
75 }
76
77 &:hover {
78 color: #000 !important;
79 }
80
81 &:hover > .glyphicon {
82 opacity: 1 !important;
83 }
84 }
85
86 .option .glyphicon {
87 font-size: 22px;
88 color: inherit;
89 opacity: 0.15;
90 margin-bottom: 10px;
91 transition: opacity 0.3s;
92 }
93
94 .video-small-block-text {
95 font-size: 15px;
96 font-weight: bold;
97 }
98 }
99
100 .video-small-block:not(:last-child) {
101 border-width: 0 1px 0 0;
102 border-color: $video-watch-border-color;
103 border-style: solid;
104 }
105
106 .video-small-block-author, .video-small-block-more {
107 a.option {
108 display: block;
109
110 .glyphicon {
111 display: block;
112 }
113 }
114 }
115
116 .video-small-block-share, .video-small-block-more {
117 a.option {
118 display: block;
119
120 .glyphicon {
121 display: block;
122 }
123 }
124 }
125
126 .video-small-block-more .video-small-block-dropdown {
127 position: relative;
128
129 .dropdown-item .glyphicon {
130 margin-right: 5px;
131 }
132 }
133
134 .video-small-block-rating {
135
136 .video-small-block-like {
137 margin-bottom: 10px;
138 }
139
140 .video-small-block-text {
141 vertical-align: top;
142 }
143
144 .glyphicon {
145 font-size: 18px;
146 margin: 0 10px 0 0;
147 opacity: 0.3;
148 }
149
150 .interactive {
151 cursor: pointer;
152 transition: opacity, color 0.3s;
153
154 &.activated, &:hover {
155 opacity: 1;
156 color: #000;
157 }
158 }
159 }
160 }
161
162 .video-details {
163 margin-top: 30px;
164
165 .video-details-date-description {
166 padding-left: $video-watch-info-padding-left;
167
168 .video-details-date {
169 font-weight: bold;
170 margin-bottom: 30px;
171 }
172 }
173
174 .video-details-attributes {
175 font-weight: bold;
176 font-size: 12px;
177
178 .video-details-attribute-label {
179 color: $video-watch-info-color;
180 display: inline-block;
181 width: 60px;
182 margin-right: 5px;
183 }
184 }
185
186 .video-details-tags {
187 display: inline-block;
188
189 a {
190 display: inline-block;
191 margin-right: 3px;
192 font-size: 11px;
193 }
194 }
195 }
196
197 @media screen and (max-width: 400px) {
198 .video-name-views {
199 font-size: 16px !important;
200 }
201 }
202
203 @media screen and (max-width: 800px) {
204 .video-name-views {
205 .video-name {
206 padding-left: 5px;
207 padding-right: 0px;
208 }
209
210 .video-views {
211 padding-left: 0px;
212 padding-right: 5px;
213 }
214 }
215
216 .video-small-blocks {
217 a, .video-small-block-text {
218 font-size: 13px !important;
219 }
220
221 .glyphicon {
222 font-size: 18px !important;
223 }
224
225 .video-small-block-author {
226 padding-left: 10px;
227 }
228 }
229
230 .video-details {
231 .video-details-date-description {
232 padding-left: 10px;
233 font-size: 13px !important;
234 }
235
236 .video-details-attributes {
237 font-size: 11px !important;
238
239 .video-details-attribute-label {
240 width: 50px;
241 }
242 }
243 }
244 }
245}
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 @@
1import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { Observable } from 'rxjs/Observable'
4import { Subscription } from 'rxjs/Subscription'
5
6import videojs from 'video.js'
7import '../../../assets/player/peertube-videojs-plugin'
8
9import { MetaService } from '@ngx-meta/core'
10import { NotificationsService } from 'angular2-notifications'
11
12import { AuthService, ConfirmService } from '../../core'
13import { VideoMagnetComponent } from './video-magnet.component'
14import { VideoShareComponent } from './video-share.component'
15import { VideoReportComponent } from './video-report.component'
16import { Video, VideoService } from '../shared'
17import { WebTorrentService } from './webtorrent.service'
18import { UserVideoRateType, VideoRateType } from '../../../../../shared'
19
20@Component({
21 selector: 'my-video-watch',
22 templateUrl: './video-watch.component.html',
23 styleUrls: [ './video-watch.component.scss' ]
24})
25export class VideoWatchComponent implements OnInit, OnDestroy {
26 @ViewChild('videoMagnetModal') videoMagnetModal: VideoMagnetComponent
27 @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
28 @ViewChild('videoReportModal') videoReportModal: VideoReportComponent
29
30 downloadSpeed: number
31 error = false
32 loading = false
33 numPeers: number
34 player: videojs.Player
35 playerElement: HTMLMediaElement
36 uploadSpeed: number
37 userRating: UserVideoRateType = null
38 video: Video = null
39 videoNotFound = false
40
41 private paramsSub: Subscription
42
43 constructor (
44 private elementRef: ElementRef,
45 private route: ActivatedRoute,
46 private router: Router,
47 private videoService: VideoService,
48 private confirmService: ConfirmService,
49 private metaService: MetaService,
50 private authService: AuthService,
51 private notificationsService: NotificationsService
52 ) {}
53
54 ngOnInit () {
55 this.paramsSub = this.route.params.subscribe(routeParams => {
56 let uuid = routeParams['uuid']
57 this.videoService.getVideo(uuid).subscribe(
58 video => this.onVideoFetched(video),
59
60 error => {
61 console.error(error)
62 this.videoNotFound = true
63 }
64 )
65 })
66 }
67
68 ngOnDestroy () {
69 // Remove player if it exists
70 if (this.videoNotFound === false) {
71 videojs(this.playerElement).dispose()
72 }
73
74 // Unsubscribe subscriptions
75 this.paramsSub.unsubscribe()
76 }
77
78 setLike () {
79 if (this.isUserLoggedIn() === false) return
80 // Already liked this video
81 if (this.userRating === 'like') return
82
83 this.videoService.setVideoLike(this.video.id)
84 .subscribe(
85 () => {
86 // Update the video like attribute
87 this.updateVideoRating(this.userRating, 'like')
88 this.userRating = 'like'
89 },
90
91 err => this.notificationsService.error('Error', err.message)
92 )
93 }
94
95 setDislike () {
96 if (this.isUserLoggedIn() === false) return
97 // Already disliked this video
98 if (this.userRating === 'dislike') return
99
100 this.videoService.setVideoDislike(this.video.id)
101 .subscribe(
102 () => {
103 // Update the video dislike attribute
104 this.updateVideoRating(this.userRating, 'dislike')
105 this.userRating = 'dislike'
106 },
107
108 err => this.notificationsService.error('Error', err.message)
109 )
110 }
111
112 removeVideo (event: Event) {
113 event.preventDefault()
114
115 this.confirmService.confirm('Do you really want to delete this video?', 'Delete').subscribe(
116 res => {
117 if (res === false) return
118
119 this.videoService.removeVideo(this.video.id)
120 .subscribe(
121 status => {
122 this.notificationsService.success('Success', `Video ${this.video.name} deleted.`)
123 // Go back to the video-list.
124 this.router.navigate(['/videos/list'])
125 },
126
127 error => this.notificationsService.error('Error', error.text)
128 )
129 }
130 )
131 }
132
133 blacklistVideo (event: Event) {
134 event.preventDefault()
135
136 this.confirmService.confirm('Do you really want to blacklist this video ?', 'Blacklist').subscribe(
137 res => {
138 if (res === false) return
139
140 this.videoService.blacklistVideo(this.video.id)
141 .subscribe(
142 status => {
143 this.notificationsService.success('Success', `Video ${this.video.name} had been blacklisted.`)
144 this.router.navigate(['/videos/list'])
145 },
146
147 error => this.notificationsService.error('Error', error.text)
148 )
149 }
150 )
151 }
152
153 showReportModal (event: Event) {
154 event.preventDefault()
155 this.videoReportModal.show()
156 }
157
158 showShareModal () {
159 this.videoShareModal.show()
160 }
161
162 showMagnetUriModal (event: Event) {
163 event.preventDefault()
164 this.videoMagnetModal.show()
165 }
166
167 isUserLoggedIn () {
168 return this.authService.isLoggedIn()
169 }
170
171 canUserUpdateVideo () {
172 return this.video.isUpdatableBy(this.authService.getUser())
173 }
174
175 isVideoRemovable () {
176 return this.video.isRemovableBy(this.authService.getUser())
177 }
178
179 isVideoBlacklistable () {
180 return this.video.isBlackistableBy(this.authService.getUser())
181 }
182
183 private handleError (err: any) {
184 const errorMessage: string = typeof err === 'string' ? err : err.message
185 let message = ''
186
187 if (errorMessage.indexOf('http error') !== -1) {
188 message = 'Cannot fetch video from server, maybe down.'
189 } else {
190 message = errorMessage
191 }
192
193 this.notificationsService.error('Error', message)
194 }
195
196 private checkUserRating () {
197 // Unlogged users do not have ratings
198 if (this.isUserLoggedIn() === false) return
199
200 this.videoService.getUserVideoRating(this.video.id)
201 .subscribe(
202 ratingObject => {
203 if (ratingObject) {
204 this.userRating = ratingObject.rating
205 }
206 },
207
208 err => this.notificationsService.error('Error', err.message)
209 )
210 }
211
212 private onVideoFetched (video: Video) {
213 this.video = video
214
215 let observable
216 if (this.video.isVideoNSFWForUser(this.authService.getUser())) {
217 observable = this.confirmService.confirm('This video is not safe for work. Are you sure you want to watch it?', 'NSFW')
218 } else {
219 observable = Observable.of(true)
220 }
221
222 observable.subscribe(
223 res => {
224 if (res === false) {
225 return this.router.navigate([ '/videos/list' ])
226 }
227
228 this.playerElement = this.elementRef.nativeElement.querySelector('#video-container')
229
230 const videojsOptions = {
231 controls: true,
232 autoplay: true,
233 plugins: {
234 peertube: {
235 videoFiles: this.video.files,
236 playerElement: this.playerElement,
237 autoplay: true,
238 peerTubeLink: false
239 }
240 }
241 }
242
243 const self = this
244 videojs(this.playerElement, videojsOptions, function () {
245 self.player = this
246 this.on('customError', (event, data) => {
247 self.handleError(data.err)
248 })
249
250 this.on('torrentInfo', (event, data) => {
251 self.downloadSpeed = data.downloadSpeed
252 self.numPeers = data.numPeers
253 self.uploadSpeed = data.uploadSpeed
254 })
255 })
256
257 this.setOpenGraphTags()
258 this.checkUserRating()
259 }
260 )
261 }
262
263 private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) {
264 let likesToIncrement = 0
265 let dislikesToIncrement = 0
266
267 if (oldRating) {
268 if (oldRating === 'like') likesToIncrement--
269 if (oldRating === 'dislike') dislikesToIncrement--
270 }
271
272 if (newRating === 'like') likesToIncrement++
273 if (newRating === 'dislike') dislikesToIncrement++
274
275 this.video.likes += likesToIncrement
276 this.video.dislikes += dislikesToIncrement
277 }
278
279 private setOpenGraphTags () {
280 this.metaService.setTitle(this.video.name)
281
282 this.metaService.setTag('og:type', 'video')
283
284 this.metaService.setTag('og:title', this.video.name)
285 this.metaService.setTag('name', this.video.name)
286
287 this.metaService.setTag('og:description', this.video.description)
288 this.metaService.setTag('description', this.video.description)
289
290 this.metaService.setTag('og:image', this.video.previewPath)
291
292 this.metaService.setTag('og:duration', this.video.duration.toString())
293
294 this.metaService.setTag('og:site_name', 'PeerTube')
295
296 this.metaService.setTag('og:url', window.location.href)
297 this.metaService.setTag('url', window.location.href)
298 }
299}
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 @@
1import { NgModule } from '@angular/core'
2
3import { VideoWatchRoutingModule } from './video-watch-routing.module'
4import { VideoService } from '../shared'
5import { SharedModule } from '../../shared'
6
7import { VideoWatchComponent } from './video-watch.component'
8import { VideoReportComponent } from './video-report.component'
9import { VideoShareComponent } from './video-share.component'
10import { VideoMagnetComponent } from './video-magnet.component'
11
12@NgModule({
13 imports: [
14 VideoWatchRoutingModule,
15 SharedModule
16 ],
17
18 declarations: [
19 VideoWatchComponent,
20
21 VideoMagnetComponent,
22 VideoShareComponent,
23 VideoReportComponent
24 ],
25
26 exports: [
27 VideoWatchComponent
28 ],
29
30 providers: [
31 VideoService
32 ]
33})
34export class VideoWatchModule { }