diff options
author | Chocobozzz <florian.bigard@gmail.com> | 2017-12-11 11:06:32 +0100 |
---|---|---|
committer | Chocobozzz <florian.bigard@gmail.com> | 2017-12-11 11:06:32 +0100 |
commit | fada8d75550dc7365f7e18ee1569b9406251d660 (patch) | |
tree | db9dc01c18693824f83fce5020f4c1f3ae7c0865 | |
parent | 492fd28167f770d79a430fc57451b5a9e075d8e7 (diff) | |
parent | c2830fa8f84f61462098bf36add824f89436dfa9 (diff) | |
download | PeerTube-fada8d75550dc7365f7e18ee1569b9406251d660.tar.gz PeerTube-fada8d75550dc7365f7e18ee1569b9406251d660.tar.zst PeerTube-fada8d75550dc7365f7e18ee1569b9406251d660.zip |
Merge branch 'feature/design' into develop
244 files changed, 4713 insertions, 3244 deletions
diff --git a/CREDITS.md b/CREDITS.md index a7b2b5568..65017bbc1 100644 --- a/CREDITS.md +++ b/CREDITS.md | |||
@@ -8,14 +8,9 @@ | |||
8 | 8 | ||
9 | # Design | 9 | # Design |
10 | 10 | ||
11 | Inspirations from: | 11 | By [Olivier Massain](https://twitter.com/omassain) |
12 | 12 | ||
13 | * [Aurélien Salomon](https://dribbble.com/shots/1338727-Youtube-Redesign) | 13 | Icons from [Robbie Pearce](https://robbiepearce.com/softies/) |
14 | * [Wojciech Zieliński](https://dribbble.com/shots/3000315-youtube-concept) | ||
15 | |||
16 | Video.js theme: | ||
17 | |||
18 | * [zanechua](https://github.com/zanechua/videojs-sublime-inspired-skin) | ||
19 | 14 | ||
20 | # Fonts | 15 | # Fonts |
21 | 16 | ||
diff --git a/client/.bootstraprc b/client/.bootstraprc index 6ceef4fe9..cc6768d43 100644 --- a/client/.bootstraprc +++ b/client/.bootstraprc | |||
@@ -84,19 +84,19 @@ styles: | |||
84 | navs: true | 84 | navs: true |
85 | navbar: false | 85 | navbar: false |
86 | breadcrumbs: false | 86 | breadcrumbs: false |
87 | pagination: true | 87 | pagination: false |
88 | pager: false | 88 | pager: false |
89 | labels: true | 89 | labels: false |
90 | badges: false | 90 | badges: false |
91 | jumbotron: false | 91 | jumbotron: false |
92 | thumbnails: true | 92 | thumbnails: false |
93 | alerts: true | 93 | alerts: true |
94 | progress-bars: true | 94 | progress-bars: false |
95 | media: true | 95 | media: true |
96 | list-group: false | 96 | list-group: false |
97 | panels: true | 97 | panels: true |
98 | wells: false | 98 | wells: false |
99 | responsive-embed: true | 99 | responsive-embed: false |
100 | close: true | 100 | close: true |
101 | 101 | ||
102 | # Components w/ JavaScript | 102 | # Components w/ JavaScript |
diff --git a/client/config/webpack.common.js b/client/config/webpack.common.js index 9cd33d2ed..f387b44f9 100644 --- a/client/config/webpack.common.js +++ b/client/config/webpack.common.js | |||
@@ -13,6 +13,7 @@ const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin') | |||
13 | const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin') | 13 | const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin') |
14 | const InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin') | 14 | const InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin') |
15 | const ngcWebpack = require('ngc-webpack') | 15 | const ngcWebpack = require('ngc-webpack') |
16 | const CopyWebpackPlugin = require('copy-webpack-plugin') | ||
16 | 17 | ||
17 | const WebpackNotifierPlugin = require('webpack-notifier') | 18 | const WebpackNotifierPlugin = require('webpack-notifier') |
18 | 19 | ||
@@ -146,14 +147,15 @@ module.exports = function (options) { | |||
146 | loader: 'sass-resources-loader', | 147 | loader: 'sass-resources-loader', |
147 | options: { | 148 | options: { |
148 | resources: [ | 149 | resources: [ |
149 | helpers.root('src/sass/_variables.scss') | 150 | helpers.root('src/sass/_variables.scss'), |
151 | helpers.root('src/sass/_mixins.scss') | ||
150 | ] | 152 | ] |
151 | } | 153 | } |
152 | } | 154 | } |
153 | ] | 155 | ] |
154 | }, | 156 | }, |
155 | { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: 'url-loader?limit=10000&minetype=application/font-woff' }, | 157 | { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: 'url-loader?limit=10000&minetype=application/font-woff' }, |
156 | { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: 'file-loader' }, | 158 | { test: /\.(otf|ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: 'url-loader?limit=10000' }, |
157 | 159 | ||
158 | /* Raw loader support for *.html | 160 | /* Raw loader support for *.html |
159 | * Returns file content as string | 161 | * Returns file content as string |
@@ -266,6 +268,17 @@ module.exports = function (options) { | |||
266 | inject: 'body' | 268 | inject: 'body' |
267 | }), | 269 | }), |
268 | 270 | ||
271 | new CopyWebpackPlugin([ | ||
272 | { | ||
273 | from: helpers.root('src/assets/images/favicon.png'), | ||
274 | to: 'assets/images/favicon.png' | ||
275 | }, | ||
276 | { | ||
277 | from: helpers.root('src/assets/images/default-avatar.png'), | ||
278 | to: 'assets/images/default-avatar.png' | ||
279 | } | ||
280 | ]), | ||
281 | |||
269 | /* | 282 | /* |
270 | * Plugin: ScriptExtHtmlWebpackPlugin | 283 | * Plugin: ScriptExtHtmlWebpackPlugin |
271 | * Description: Enhances html-webpack-plugin functionality | 284 | * Description: Enhances html-webpack-plugin functionality |
@@ -289,6 +302,7 @@ module.exports = function (options) { | |||
289 | */ | 302 | */ |
290 | new LoaderOptionsPlugin({ | 303 | new LoaderOptionsPlugin({ |
291 | options: { | 304 | options: { |
305 | context: '', | ||
292 | sassLoader: { | 306 | sassLoader: { |
293 | precision: 10, | 307 | precision: 10, |
294 | includePaths: [ helpers.root('src/sass') ] | 308 | includePaths: [ helpers.root('src/sass') ] |
diff --git a/client/config/webpack.video-embed.js b/client/config/webpack.video-embed.js index fe40194cf..2b70b6681 100644 --- a/client/config/webpack.video-embed.js +++ b/client/config/webpack.video-embed.js | |||
@@ -74,7 +74,8 @@ module.exports = function (options) { | |||
74 | loader: 'sass-resources-loader', | 74 | loader: 'sass-resources-loader', |
75 | options: { | 75 | options: { |
76 | resources: [ | 76 | resources: [ |
77 | helpers.root('src/sass/_variables.scss') | 77 | helpers.root('src/sass/_variables.scss'), |
78 | helpers.root('src/sass/_mixins.scss') | ||
78 | ] | 79 | ] |
79 | } | 80 | } |
80 | } | 81 | } |
diff --git a/client/package.json b/client/package.json index 39b3185cc..45f555f29 100644 --- a/client/package.json +++ b/client/package.json | |||
@@ -43,7 +43,6 @@ | |||
43 | "@types/webpack": "^3.0.0", | 43 | "@types/webpack": "^3.0.0", |
44 | "@types/webtorrent": "^0.98.4", | 44 | "@types/webtorrent": "^0.98.4", |
45 | "add-asset-html-webpack-plugin": "^2.0.1", | 45 | "add-asset-html-webpack-plugin": "^2.0.1", |
46 | "angular-pipes": "^6.0.0", | ||
47 | "angular2-notifications": "^0.7.7", | 46 | "angular2-notifications": "^0.7.7", |
48 | "angular2-template-loader": "^0.6.0", | 47 | "angular2-template-loader": "^0.6.0", |
49 | "assets-webpack-plugin": "^3.4.0", | 48 | "assets-webpack-plugin": "^3.4.0", |
@@ -70,8 +69,10 @@ | |||
70 | "markdown-it": "^8.4.0", | 69 | "markdown-it": "^8.4.0", |
71 | "ng-router-loader": "^2.0.0", | 70 | "ng-router-loader": "^2.0.0", |
72 | "ngc-webpack": "3.2.2", | 71 | "ngc-webpack": "3.2.2", |
73 | "ngx-bootstrap": "1.9.3", | 72 | "ngx-bootstrap": "2.0.0-beta.9", |
74 | "ngx-chips": "1.5.3", | 73 | "ngx-chips": "1.5.3", |
74 | "ngx-infinite-scroll": "^0.7.0", | ||
75 | "ngx-pipes": "^2.0.5", | ||
75 | "node-sass": "^4.1.1", | 76 | "node-sass": "^4.1.1", |
76 | "normalize.css": "^7.0.0", | 77 | "normalize.css": "^7.0.0", |
77 | "optimize-js-plugin": "0.0.4", | 78 | "optimize-js-plugin": "0.0.4", |
@@ -86,6 +87,7 @@ | |||
86 | "sass-resources-loader": "^1.2.1", | 87 | "sass-resources-loader": "^1.2.1", |
87 | "script-ext-html-webpack-plugin": "^1.3.2", | 88 | "script-ext-html-webpack-plugin": "^1.3.2", |
88 | "source-map-loader": "^0.2.1", | 89 | "source-map-loader": "^0.2.1", |
90 | "source-sans-pro": "^2.0.10", | ||
89 | "standard": "^10.0.0", | 91 | "standard": "^10.0.0", |
90 | "string-replace-loader": "^1.0.3", | 92 | "string-replace-loader": "^1.0.3", |
91 | "style-loader": "^0.19.0", | 93 | "style-loader": "^0.19.0", |
diff --git a/client/src/app/+admin/admin.component.html b/client/src/app/+admin/admin.component.html new file mode 100644 index 000000000..0bf4c8aac --- /dev/null +++ b/client/src/app/+admin/admin.component.html | |||
@@ -0,0 +1,27 @@ | |||
1 | <div class="row"> | ||
2 | <div class="sub-menu"> | ||
3 | <a *ngIf="hasUsersRight()" routerLink="/admin/users" routerLinkActive="active" class="title-page"> | ||
4 | Users | ||
5 | </a> | ||
6 | |||
7 | <a *ngIf="hasServerFollowRight()" routerLink="/admin/follows" routerLinkActive="active" class="title-page"> | ||
8 | Manage follows | ||
9 | </a> | ||
10 | |||
11 | <a *ngIf="hasVideoAbusesRight()" routerLink="/admin/video-abuses" routerLinkActive="active" class="title-page"> | ||
12 | Video abuses | ||
13 | </a> | ||
14 | |||
15 | <a *ngIf="hasVideoBlacklistRight()" routerLink="/admin/video-blacklist" routerLinkActive="active" class="title-page"> | ||
16 | Video blacklist | ||
17 | </a> | ||
18 | |||
19 | <a *ngIf="hasJobsRight()" routerLink="/admin/jobs" routerLinkActive="active" class="title-page"> | ||
20 | Jobs | ||
21 | </a> | ||
22 | </div> | ||
23 | |||
24 | <div class="margin-content"> | ||
25 | <router-outlet></router-outlet> | ||
26 | </div> | ||
27 | </div> | ||
diff --git a/client/src/app/+admin/admin.component.scss b/client/src/app/+admin/admin.component.scss new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/client/src/app/+admin/admin.component.scss | |||
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts index ecd62ee61..75cd50cc7 100644 --- a/client/src/app/+admin/admin.component.ts +++ b/client/src/app/+admin/admin.component.ts | |||
@@ -1,7 +1,31 @@ | |||
1 | import { Component } from '@angular/core' | 1 | import { Component } from '@angular/core' |
2 | import { UserRight } from '../../../../shared' | ||
3 | import { AuthService } from '../core/auth/auth.service' | ||
2 | 4 | ||
3 | @Component({ | 5 | @Component({ |
4 | template: '<router-outlet></router-outlet>' | 6 | templateUrl: './admin.component.html', |
7 | styleUrls: [ './admin.component.scss' ] | ||
5 | }) | 8 | }) |
6 | export class AdminComponent { | 9 | export class AdminComponent { |
10 | constructor (private auth: AuthService) {} | ||
11 | |||
12 | hasUsersRight () { | ||
13 | return this.auth.getUser().hasRight(UserRight.MANAGE_USERS) | ||
14 | } | ||
15 | |||
16 | hasServerFollowRight () { | ||
17 | return this.auth.getUser().hasRight(UserRight.MANAGE_SERVER_FOLLOW) | ||
18 | } | ||
19 | |||
20 | hasVideoAbusesRight () { | ||
21 | return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES) | ||
22 | } | ||
23 | |||
24 | hasVideoBlacklistRight () { | ||
25 | return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) | ||
26 | } | ||
27 | |||
28 | hasJobsRight () { | ||
29 | return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS) | ||
30 | } | ||
7 | } | 31 | } |
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html index 473801822..a24039fc6 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.html +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html | |||
@@ -1,16 +1,10 @@ | |||
1 | <div class="row"> | 1 | <p-dataTable |
2 | <div class="content-padding"> | 2 | [value]="followers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" |
3 | <h3>Followers list</h3> | 3 | sortField="createdAt" (onLazyLoad)="loadLazy($event)" |
4 | 4 | > | |
5 | <p-dataTable | 5 | <p-column field="id" header="ID"></p-column> |
6 | [value]="followers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | 6 | <p-column field="follower.host" header="Host"></p-column> |
7 | sortField="createdAt" (onLazyLoad)="loadLazy($event)" | 7 | <p-column field="follower.score" header="Score"></p-column> |
8 | > | 8 | <p-column field="state" header="State"></p-column> |
9 | <p-column field="id" header="ID"></p-column> | 9 | <p-column field="createdAt" header="Created date" [sortable]="true"></p-column> |
10 | <p-column field="follower.host" header="Host"></p-column> | 10 | </p-dataTable> |
11 | <p-column field="follower.score" header="Score"></p-column> | ||
12 | <p-column field="state" header="State"></p-column> | ||
13 | <p-column field="createdAt" header="Created date" [sortable]="true"></p-column> | ||
14 | </p-dataTable> | ||
15 | </div> | ||
16 | </div> | ||
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.scss b/client/src/app/+admin/follows/followers-list/followers-list.component.scss index 0a0f621c6..e69de29bb 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.scss +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.scss | |||
@@ -1,3 +0,0 @@ | |||
1 | .btn { | ||
2 | margin-top: 10px; | ||
3 | } | ||
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.html b/client/src/app/+admin/follows/following-add/following-add.component.html index 8e7dddc11..25bab9d0d 100644 --- a/client/src/app/+admin/follows/following-add/following-add.component.html +++ b/client/src/app/+admin/follows/following-add/following-add.component.html | |||
@@ -1,35 +1,22 @@ | |||
1 | <div class="row"> | 1 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> |
2 | <div class="content-padding"> | ||
3 | 2 | ||
4 | <h3>Add following</h3> | 3 | <form (ngSubmit)="addFollowing()"> |
4 | <div class="form-group"> | ||
5 | <label for="hosts">1 host (without "http://") per line</label> | ||
5 | 6 | ||
6 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | 7 | <textarea |
8 | type="text" class="form-control" placeholder="example.com" id="hosts" name="hosts" | ||
9 | [(ngModel)]="hostsString" (ngModelChange)="onHostsChanged()" [ngClass]="{ 'input-error': hostsError }" | ||
10 | ></textarea> | ||
7 | 11 | ||
8 | <form (ngSubmit)="addFollowing()" [formGroup]="form"> | 12 | <div *ngIf="hostsError" class="form-error"> |
9 | <div class="form-group" *ngFor="let host of hosts; let id = index; trackBy:customTrackBy"> | 13 | {{ hostsError }} |
10 | <label [for]="'host-' + id">Host (so without "http://")</label> | 14 | </div> |
11 | 15 | </div> | |
12 | <div class="input-group"> | ||
13 | <input | ||
14 | type="text" class="form-control" placeholder="example.com" | ||
15 | [id]="'host-' + id" [formControlName]="'host-' + id" | ||
16 | /> | ||
17 | <span class="input-group-btn"> | ||
18 | <button *ngIf="displayAddField(id)" (click)="addField()" class="btn btn-default" type="button">+</button> | ||
19 | <button *ngIf="displayRemoveField(id)" (click)="removeField(id)" class="btn btn-default" type="button">-</button> | ||
20 | </span> | ||
21 | </div> | ||
22 | |||
23 | <div [hidden]="form.controls['host-' + id].valid || form.controls['host-' + id].pristine" class="alert alert-warning"> | ||
24 | It should be a valid host. | ||
25 | </div> | ||
26 | </div> | ||
27 | |||
28 | <div *ngIf="canMakeFriends() === false" class="alert alert-warning"> | ||
29 | It seems that you are not on a HTTPS server. Your webserver need to have TLS activated in order to follow servers. | ||
30 | </div> | ||
31 | 16 | ||
32 | <input type="submit" value="Add following" class="btn btn-default" [disabled]="!isFormValid()"> | 17 | <div *ngIf="httpEnabled() === false" class="alert alert-warning"> |
33 | </form> | 18 | It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers. |
34 | </div> | 19 | </div> |
35 | </div> | 20 | |
21 | <input type="submit" value="Add following" [disabled]="hostsError || !hostsString" class="btn btn-default"> | ||
22 | </form> | ||
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.scss b/client/src/app/+admin/follows/following-add/following-add.component.scss index 5fde51636..2cb3efe28 100644 --- a/client/src/app/+admin/follows/following-add/following-add.component.scss +++ b/client/src/app/+admin/follows/following-add/following-add.component.scss | |||
@@ -1,7 +1,9 @@ | |||
1 | table { | 1 | textarea { |
2 | margin-bottom: 40px; | 2 | height: 250px; |
3 | } | 3 | } |
4 | 4 | ||
5 | .input-group-btn button { | 5 | input[type=submit] { |
6 | width: 35px; | 6 | @include peertube-button; |
7 | @include orange-button; | ||
7 | } | 8 | } |
9 | |||
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.ts b/client/src/app/+admin/follows/following-add/following-add.component.ts index 814c6f1a1..bf842129d 100644 --- a/client/src/app/+admin/follows/following-add/following-add.component.ts +++ b/client/src/app/+admin/follows/following-add/following-add.component.ts | |||
@@ -1,9 +1,6 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component } from '@angular/core' |
2 | import { FormControl, FormGroup } from '@angular/forms' | ||
3 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
4 | |||
5 | import { NotificationsService } from 'angular2-notifications' | 3 | import { NotificationsService } from 'angular2-notifications' |
6 | |||
7 | import { ConfirmService } from '../../../core' | 4 | import { ConfirmService } from '../../../core' |
8 | import { validateHost } from '../../../shared' | 5 | import { validateHost } from '../../../shared' |
9 | import { FollowService } from '../shared' | 6 | import { FollowService } from '../shared' |
@@ -13,9 +10,9 @@ import { FollowService } from '../shared' | |||
13 | templateUrl: './following-add.component.html', | 10 | templateUrl: './following-add.component.html', |
14 | styleUrls: [ './following-add.component.scss' ] | 11 | styleUrls: [ './following-add.component.scss' ] |
15 | }) | 12 | }) |
16 | export class FollowingAddComponent implements OnInit { | 13 | export class FollowingAddComponent { |
17 | form: FormGroup | 14 | hostsString = '' |
18 | hosts: string[] = [ ] | 15 | hostsError: string = null |
19 | error: string = null | 16 | error: string = null |
20 | 17 | ||
21 | constructor ( | 18 | constructor ( |
@@ -25,76 +22,50 @@ export class FollowingAddComponent implements OnInit { | |||
25 | private followService: FollowService | 22 | private followService: FollowService |
26 | ) {} | 23 | ) {} |
27 | 24 | ||
28 | ngOnInit () { | 25 | httpEnabled () { |
29 | this.form = new FormGroup({}) | ||
30 | this.addField() | ||
31 | } | ||
32 | |||
33 | addField () { | ||
34 | this.form.addControl(`host-${this.hosts.length}`, new FormControl('', [ validateHost ])) | ||
35 | this.hosts.push('') | ||
36 | } | ||
37 | |||
38 | canMakeFriends () { | ||
39 | return window.location.protocol === 'https:' | 26 | return window.location.protocol === 'https:' |
40 | } | 27 | } |
41 | 28 | ||
42 | customTrackBy (index: number, obj: any): any { | 29 | onHostsChanged () { |
43 | return index | 30 | this.hostsError = null |
44 | } | ||
45 | |||
46 | displayAddField (index: number) { | ||
47 | return index === (this.hosts.length - 1) | ||
48 | } | ||
49 | 31 | ||
50 | displayRemoveField (index: number) { | 32 | const newHostsErrors = [] |
51 | return (index !== 0 || this.hosts.length > 1) && index !== (this.hosts.length - 1) | 33 | const hosts = this.getNotEmptyHosts() |
52 | } | ||
53 | 34 | ||
54 | isFormValid () { | 35 | for (const host of hosts) { |
55 | // Do not check the last input | 36 | if (validateHost(host) === false) { |
56 | for (let i = 0; i < this.hosts.length - 1; i++) { | 37 | newHostsErrors.push(`${host} is not valid`) |
57 | if (!this.form.controls[`host-${i}`].valid) return false | 38 | } |
58 | } | 39 | } |
59 | 40 | ||
60 | const lastIndex = this.hosts.length - 1 | 41 | if (newHostsErrors.length !== 0) { |
61 | // If the last input (which is not the first) is empty, it's ok | 42 | this.hostsError = newHostsErrors.join('. ') |
62 | if (this.hosts[lastIndex] === '' && lastIndex !== 0) { | ||
63 | return true | ||
64 | } else { | ||
65 | return this.form.controls[`host-${lastIndex}`].valid | ||
66 | } | 43 | } |
67 | } | 44 | } |
68 | 45 | ||
69 | removeField (index: number) { | ||
70 | // Remove the last control | ||
71 | this.form.removeControl(`host-${this.hosts.length - 1}`) | ||
72 | this.hosts.splice(index, 1) | ||
73 | } | ||
74 | |||
75 | addFollowing () { | 46 | addFollowing () { |
76 | this.error = '' | 47 | this.error = '' |
77 | 48 | ||
78 | const notEmptyHosts = this.getNotEmptyHosts() | 49 | const hosts = this.getNotEmptyHosts() |
79 | if (notEmptyHosts.length === 0) { | 50 | if (hosts.length === 0) { |
80 | this.error = 'You need to specify at least 1 host.' | 51 | this.error = 'You need to specify hosts to follow.' |
81 | return | ||
82 | } | 52 | } |
83 | 53 | ||
84 | if (!this.isHostsUnique(notEmptyHosts)) { | 54 | if (!this.isHostsUnique(hosts)) { |
85 | this.error = 'Hosts need to be unique.' | 55 | this.error = 'Hosts need to be unique.' |
86 | return | 56 | return |
87 | } | 57 | } |
88 | 58 | ||
89 | const confirmMessage = 'Are you sure to make friends with:<br /> - ' + notEmptyHosts.join('<br /> - ') | 59 | const confirmMessage = 'If you confirm, you will send a follow request to:<br /> - ' + hosts.join('<br /> - ') |
90 | this.confirmService.confirm(confirmMessage, 'Follow new server(s)').subscribe( | 60 | this.confirmService.confirm(confirmMessage, 'Follow new server(s)').subscribe( |
91 | res => { | 61 | res => { |
92 | if (res === false) return | 62 | if (res === false) return |
93 | 63 | ||
94 | this.followService.follow(notEmptyHosts).subscribe( | 64 | this.followService.follow(hosts).subscribe( |
95 | status => { | 65 | status => { |
96 | this.notificationsService.success('Success', 'Follow request(s) sent!') | 66 | this.notificationsService.success('Success', 'Follow request(s) sent!') |
97 | this.router.navigate([ '/admin/follows/following-list' ]) | 67 | |
68 | setTimeout(() => this.router.navigate([ '/admin/follows/following-list' ]), 500) | ||
98 | }, | 69 | }, |
99 | 70 | ||
100 | err => this.notificationsService.error('Error', err.message) | 71 | err => this.notificationsService.error('Error', err.message) |
@@ -103,18 +74,15 @@ export class FollowingAddComponent implements OnInit { | |||
103 | ) | 74 | ) |
104 | } | 75 | } |
105 | 76 | ||
106 | private getNotEmptyHosts () { | ||
107 | const notEmptyHosts = [] | ||
108 | |||
109 | Object.keys(this.form.value).forEach((hostKey) => { | ||
110 | const host = this.form.value[hostKey] | ||
111 | if (host !== '') notEmptyHosts.push(host) | ||
112 | }) | ||
113 | |||
114 | return notEmptyHosts | ||
115 | } | ||
116 | |||
117 | private isHostsUnique (hosts: string[]) { | 77 | private isHostsUnique (hosts: string[]) { |
118 | return hosts.every(host => hosts.indexOf(host) === hosts.lastIndexOf(host)) | 78 | return hosts.every(host => hosts.indexOf(host) === hosts.lastIndexOf(host)) |
119 | } | 79 | } |
80 | |||
81 | private getNotEmptyHosts () { | ||
82 | const hosts = this.hostsString | ||
83 | .split('\n') | ||
84 | .filter(host => host && host.length !== 0) // Eject empty hosts | ||
85 | |||
86 | return hosts | ||
87 | } | ||
120 | } | 88 | } |
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html index a73084312..2b6cc9113 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.html +++ b/client/src/app/+admin/follows/following-list/following-list.component.html | |||
@@ -1,20 +1,14 @@ | |||
1 | <div class="row"> | 1 | <p-dataTable |
2 | <div class="content-padding"> | 2 | [value]="following" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" |
3 | <h3>Following list</h3> | 3 | sortField="createdAt" (onLazyLoad)="loadLazy($event)" |
4 | 4 | > | |
5 | <p-dataTable | 5 | <p-column field="id" header="ID"></p-column> |
6 | [value]="following" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | 6 | <p-column field="following.host" header="Host"></p-column> |
7 | sortField="createdAt" (onLazyLoad)="loadLazy($event)" | 7 | <p-column field="state" header="State"></p-column> |
8 | > | 8 | <p-column field="createdAt" header="Created date" [sortable]="true"></p-column> |
9 | <p-column field="id" header="ID"></p-column> | 9 | <p-column styleClass="action-cell"> |
10 | <p-column field="following.host" header="Host"></p-column> | 10 | <ng-template pTemplate="body" let-following="rowData"> |
11 | <p-column field="state" header="State"></p-column> | 11 | <my-delete-button (click)="removeFollowing(following)"></my-delete-button> |
12 | <p-column field="createdAt" header="Created date" [sortable]="true"></p-column> | 12 | </ng-template> |
13 | <p-column header="Unfollow" styleClass="action-cell"> | 13 | </p-column> |
14 | <ng-template pTemplate="body" let-following="rowData"> | 14 | </p-dataTable> |
15 | <span (click)="removeFollowing(following)" class="glyphicon glyphicon-remove glyphicon-black" title="Unfollow"></span> | ||
16 | </ng-template> | ||
17 | </p-column> | ||
18 | </p-dataTable> | ||
19 | </div> | ||
20 | </div> | ||
diff --git a/client/src/app/+admin/follows/follows.component.html b/client/src/app/+admin/follows/follows.component.html index b67bc9736..1baba5a4d 100644 --- a/client/src/app/+admin/follows/follows.component.html +++ b/client/src/app/+admin/follows/follows.component.html | |||
@@ -1,4 +1,6 @@ | |||
1 | <div class="follows-menu"> | 1 | <div class="admin-sub-header"> |
2 | <div class="admin-sub-title">Manage follows</div> | ||
3 | |||
2 | <tabset #followsMenuTabs> | 4 | <tabset #followsMenuTabs> |
3 | <tab *ngFor="let link of links"> | 5 | <tab *ngFor="let link of links"> |
4 | <ng-template tabHeading> | 6 | <ng-template tabHeading> |
@@ -8,4 +10,6 @@ | |||
8 | </tabset> | 10 | </tabset> |
9 | </div> | 11 | </div> |
10 | 12 | ||
13 | |||
14 | |||
11 | <router-outlet></router-outlet> | 15 | <router-outlet></router-outlet> |
diff --git a/client/src/app/+admin/follows/follows.component.scss b/client/src/app/+admin/follows/follows.component.scss index d8ab41975..835fa3b78 100644 --- a/client/src/app/+admin/follows/follows.component.scss +++ b/client/src/app/+admin/follows/follows.component.scss | |||
@@ -1,21 +1,4 @@ | |||
1 | .follows-menu { | 1 | .admin-sub-title { |
2 | margin-top: 20px; | 2 | flex-grow: 0; |
3 | } | 3 | margin-right: 30px; |
4 | |||
5 | tabset /deep/ { | ||
6 | .nav-link { | ||
7 | padding: 0; | ||
8 | } | ||
9 | |||
10 | .tab-link { | ||
11 | display: block; | ||
12 | text-align: center; | ||
13 | height: 40px; | ||
14 | width: 120px; | ||
15 | line-height: 40px; | ||
16 | |||
17 | &:hover, &:active, &:focus { | ||
18 | text-decoration: none !important; | ||
19 | } | ||
20 | } | ||
21 | } | 4 | } |
diff --git a/client/src/app/+admin/follows/follows.component.ts b/client/src/app/+admin/follows/follows.component.ts index a1be82585..f29ad384f 100644 --- a/client/src/app/+admin/follows/follows.component.ts +++ b/client/src/app/+admin/follows/follows.component.ts | |||
@@ -47,7 +47,7 @@ export class FollowsComponent implements OnInit, AfterViewInit { | |||
47 | for (let i = 0; i < this.links.length; i++) { | 47 | for (let i = 0; i < this.links.length; i++) { |
48 | const path = this.links[i].path | 48 | const path = this.links[i].path |
49 | 49 | ||
50 | if (url.endsWith(path) === true) { | 50 | if (url.endsWith(path) === true && this.followsMenuTabs.tabs[i]) { |
51 | this.followsMenuTabs.tabs[i].active = true | 51 | this.followsMenuTabs.tabs[i].active = true |
52 | return | 52 | return |
53 | } | 53 | } |
diff --git a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.html b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.html index a90267172..7aa5f4254 100644 --- a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.html +++ b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.html | |||
@@ -1,18 +1,20 @@ | |||
1 | <div class="row"> | 1 | <div class="admin-sub-header"> |
2 | <div class="content-padding"> | 2 | <div class="admin-sub-title">Jobs list</div> |
3 | <h3>Jobs list</h3> | ||
4 | |||
5 | <p-dataTable | ||
6 | [value]="jobs" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | ||
7 | sortField="createdAt" (onLazyLoad)="loadLazy($event)" | ||
8 | > | ||
9 | <p-column field="id" header="ID"></p-column> | ||
10 | <p-column field="category" header="Category"></p-column> | ||
11 | <p-column field="handlerName" header="Handler name"></p-column> | ||
12 | <p-column field="handlerInputData" header="Input data"></p-column> | ||
13 | <p-column field="state" header="State"></p-column> | ||
14 | <p-column field="createdAt" header="Created date" [sortable]="true"></p-column> | ||
15 | <p-column field="updatedAt" header="Updated date"></p-column> | ||
16 | </p-dataTable> | ||
17 | </div> | ||
18 | </div> | 3 | </div> |
4 | |||
5 | <p-dataTable | ||
6 | [value]="jobs" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | ||
7 | sortField="createdAt" (onLazyLoad)="loadLazy($event)" [scrollable]="true" [virtualScroll]="true" [scrollHeight]="scrollHeight" | ||
8 | > | ||
9 | <p-column field="id" header="ID" [style]="{ width: '40px' }"></p-column> | ||
10 | <p-column field="category" header="Category" [style]="{ width: '100px' }"></p-column> | ||
11 | <p-column field="handlerName" header="Handler name" [style]="{ width: '200px' }"></p-column> | ||
12 | <p-column header="Input data"> | ||
13 | <ng-template pTemplate="body" let-job="rowData"> | ||
14 | <pre>{{ job.handlerInputData }}</pre> | ||
15 | </ng-template> | ||
16 | </p-column> | ||
17 | <p-column field="state" header="State" [style]="{ width: '100px' }"></p-column> | ||
18 | <p-column field="createdAt" header="Created date" [sortable]="true" [style]="{ width: '250px' }"></p-column> | ||
19 | <p-column field="updatedAt" header="Updated date" [style]="{ width: '250px' }"></p-column> | ||
20 | </p-dataTable> | ||
diff --git a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.scss b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.scss new file mode 100644 index 000000000..9dde13216 --- /dev/null +++ b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.scss | |||
@@ -0,0 +1,3 @@ | |||
1 | pre { | ||
2 | font-size: 13px; | ||
3 | } | ||
diff --git a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts index 88fe259fb..f93847f29 100644 --- a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts +++ b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts | |||
@@ -1,22 +1,24 @@ | |||
1 | import { Component } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { NotificationsService } from 'angular2-notifications' |
3 | import { SortMeta } from 'primeng/primeng' | 3 | import { SortMeta } from 'primeng/primeng' |
4 | import { Job } from '../../../../../../shared/index' | 4 | import { Job } from '../../../../../../shared/index' |
5 | import { RestPagination, RestTable } from '../../../shared' | 5 | import { RestPagination, RestTable } from '../../../shared' |
6 | import { viewportHeight } from '../../../shared/misc/utils' | ||
6 | import { JobService } from '../shared' | 7 | import { JobService } from '../shared' |
7 | import { RestExtractor } from '../../../shared/rest/rest-extractor.service' | 8 | import { RestExtractor } from '../../../shared/rest/rest-extractor.service' |
8 | 9 | ||
9 | @Component({ | 10 | @Component({ |
10 | selector: 'my-jobs-list', | 11 | selector: 'my-jobs-list', |
11 | templateUrl: './jobs-list.component.html', | 12 | templateUrl: './jobs-list.component.html', |
12 | styleUrls: [ ] | 13 | styleUrls: [ './jobs-list.component.scss' ] |
13 | }) | 14 | }) |
14 | export class JobsListComponent extends RestTable { | 15 | export class JobsListComponent extends RestTable implements OnInit { |
15 | jobs: Job[] = [] | 16 | jobs: Job[] = [] |
16 | totalRecords = 0 | 17 | totalRecords = 0 |
17 | rowsPerPage = 10 | 18 | rowsPerPage = 20 |
18 | sort: SortMeta = { field: 'createdAt', order: 1 } | 19 | sort: SortMeta = { field: 'createdAt', order: 1 } |
19 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | 20 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
21 | scrollHeight = '' | ||
20 | 22 | ||
21 | constructor ( | 23 | constructor ( |
22 | private notificationsService: NotificationsService, | 24 | private notificationsService: NotificationsService, |
@@ -26,10 +28,14 @@ export class JobsListComponent extends RestTable { | |||
26 | super() | 28 | super() |
27 | } | 29 | } |
28 | 30 | ||
31 | ngOnInit () { | ||
32 | // 270 -> headers + footer... | ||
33 | this.scrollHeight = (viewportHeight() - 380) + 'px' | ||
34 | } | ||
35 | |||
29 | protected loadData () { | 36 | protected loadData () { |
30 | this.jobsService | 37 | this.jobsService |
31 | .getJobs(this.pagination, this.sort) | 38 | .getJobs(this.pagination, this.sort) |
32 | .map(res => this.restExtractor.applyToResultListData(res, this.formatJob.bind(this))) | ||
33 | .subscribe( | 39 | .subscribe( |
34 | resultList => { | 40 | resultList => { |
35 | this.jobs = resultList.data | 41 | this.jobs = resultList.data |
@@ -39,12 +45,4 @@ export class JobsListComponent extends RestTable { | |||
39 | err => this.notificationsService.error('Error', err.message) | 45 | err => this.notificationsService.error('Error', err.message) |
40 | ) | 46 | ) |
41 | } | 47 | } |
42 | |||
43 | private formatJob (job: Job) { | ||
44 | const handlerInputData = JSON.stringify(job.handlerInputData) | ||
45 | |||
46 | return Object.assign(job, { | ||
47 | handlerInputData | ||
48 | }) | ||
49 | } | ||
50 | } | 48 | } |
diff --git a/client/src/app/+admin/jobs/shared/job.service.ts b/client/src/app/+admin/jobs/shared/job.service.ts index 49f1ab6f5..0cfbdbbea 100644 --- a/client/src/app/+admin/jobs/shared/job.service.ts +++ b/client/src/app/+admin/jobs/shared/job.service.ts | |||
@@ -25,6 +25,13 @@ export class JobService { | |||
25 | 25 | ||
26 | return this.authHttp.get<ResultList<Job>>(JobService.BASE_JOB_URL, { params }) | 26 | return this.authHttp.get<ResultList<Job>>(JobService.BASE_JOB_URL, { params }) |
27 | .map(res => this.restExtractor.convertResultListDateToHuman(res)) | 27 | .map(res => this.restExtractor.convertResultListDateToHuman(res)) |
28 | .map(res => this.restExtractor.applyToResultListData(res, this.prettyPrintData)) | ||
28 | .catch(err => this.restExtractor.handleError(err)) | 29 | .catch(err => this.restExtractor.handleError(err)) |
29 | } | 30 | } |
31 | |||
32 | private prettyPrintData (obj: Job) { | ||
33 | const handlerInputData = JSON.stringify(obj.handlerInputData, null, 2) | ||
34 | |||
35 | return Object.assign(obj, { handlerInputData }) | ||
36 | } | ||
30 | } | 37 | } |
diff --git a/client/src/app/+admin/users/shared/user.service.ts b/client/src/app/+admin/users/shared/user.service.ts index e4bd5df37..dc77cc1d8 100644 --- a/client/src/app/+admin/users/shared/user.service.ts +++ b/client/src/app/+admin/users/shared/user.service.ts | |||
@@ -1,14 +1,12 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { HttpClient, HttpParams } from '@angular/common/http' | 1 | import { HttpClient, HttpParams } from '@angular/common/http' |
3 | import { Observable } from 'rxjs/Observable' | 2 | import { Injectable } from '@angular/core' |
3 | import { BytesPipe } from 'ngx-pipes' | ||
4 | import { SortMeta } from 'primeng/components/common/sortmeta' | ||
4 | import 'rxjs/add/operator/catch' | 5 | import 'rxjs/add/operator/catch' |
5 | import 'rxjs/add/operator/map' | 6 | import 'rxjs/add/operator/map' |
6 | 7 | import { Observable } from 'rxjs/Observable' | |
7 | import { SortMeta } from 'primeng/components/common/sortmeta' | 8 | import { ResultList, UserCreate, UserUpdate } from '../../../../../../shared' |
8 | import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe' | 9 | import { RestExtractor, RestPagination, RestService, User } from '../../../shared' |
9 | |||
10 | import { RestExtractor, User, RestPagination, RestService } from '../../../shared' | ||
11 | import { UserCreate, UserUpdate, ResultList } from '../../../../../../shared' | ||
12 | 10 | ||
13 | @Injectable() | 11 | @Injectable() |
14 | export class UserService { | 12 | export class UserService { |
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html index 349be13c1..963e2f39a 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.html +++ b/client/src/app/+admin/users/user-edit/user-edit.component.html | |||
@@ -1,73 +1,68 @@ | |||
1 | <div class="row"> | 1 | <div class="admin-sub-title" *ngIf="isCreation() === true">Add user</div> |
2 | <div class="content-padding"> | 2 | <div class="admin-sub-title" *ngIf="isCreation() === false">Edit user {{ username }}</div> |
3 | 3 | ||
4 | <h3 *ngIf="isCreation() === true">Add user</h3> | 4 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> |
5 | <h3 *ngIf="isCreation() === false">Edit user {{ username }}</h3> | ||
6 | 5 | ||
7 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | 6 | <form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> |
8 | 7 | <div class="form-group" *ngIf="isCreation()"> | |
9 | <form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> | 8 | <label for="username">Username</label> |
10 | <div class="form-group" *ngIf="isCreation()"> | 9 | <input |
11 | <label for="username">Username</label> | 10 | type="text" class="form-control" id="username" placeholder="john" |
12 | <input | 11 | formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }" |
13 | type="text" class="form-control" id="username" placeholder="john" | 12 | > |
14 | formControlName="username" | 13 | <div *ngIf="formErrors.username" class="form-error"> |
15 | > | 14 | {{ formErrors.username }} |
16 | <div *ngIf="formErrors.username" class="alert alert-danger"> | 15 | </div> |
17 | {{ formErrors.username }} | 16 | </div> |
18 | </div> | ||
19 | </div> | ||
20 | |||
21 | <div class="form-group"> | ||
22 | <label for="email">Email</label> | ||
23 | <input | ||
24 | type="text" class="form-control" id="email" placeholder="mail@example.com" | ||
25 | formControlName="email" | ||
26 | > | ||
27 | <div *ngIf="formErrors.email" class="alert alert-danger"> | ||
28 | {{ formErrors.email }} | ||
29 | </div> | ||
30 | </div> | ||
31 | 17 | ||
32 | <div class="form-group" *ngIf="isCreation()"> | 18 | <div class="form-group"> |
33 | <label for="password">Password</label> | 19 | <label for="email">Email</label> |
34 | <input | 20 | <input |
35 | type="password" class="form-control" id="password" | 21 | type="text" class="form-control" id="email" placeholder="mail@example.com" |
36 | formControlName="password" | 22 | formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }" |
37 | > | 23 | > |
38 | <div *ngIf="formErrors.password" class="alert alert-danger"> | 24 | <div *ngIf="formErrors.email" class="form-error"> |
39 | {{ formErrors.password }} | 25 | {{ formErrors.email }} |
40 | </div> | 26 | </div> |
41 | </div> | 27 | </div> |
42 | 28 | ||
43 | <div class="form-group"> | 29 | <div class="form-group" *ngIf="isCreation()"> |
44 | <label for="role">Role</label> | 30 | <label for="password">Password</label> |
45 | <select class="form-control" id="role" formControlName="role"> | 31 | <input |
46 | <option *ngFor="let role of roles" [value]="role.value"> | 32 | type="password" class="form-control" id="password" |
47 | {{ role.label }} | 33 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" |
48 | </option> | 34 | > |
49 | </select> | 35 | <div *ngIf="formErrors.password" class="form-error"> |
36 | {{ formErrors.password }} | ||
37 | </div> | ||
38 | </div> | ||
50 | 39 | ||
51 | <div *ngIf="formErrors.role" class="alert alert-danger"> | 40 | <div class="form-group"> |
52 | {{ formErrors.role }} | 41 | <label for="role">Role</label> |
53 | </div> | 42 | <select class="form-control" id="role" formControlName="role"> |
54 | </div> | 43 | <option *ngFor="let role of roles" [value]="role.value"> |
44 | {{ role.label }} | ||
45 | </option> | ||
46 | </select> | ||
55 | 47 | ||
56 | <div class="form-group"> | 48 | <div *ngIf="formErrors.role" class="form-error"> |
57 | <label for="videoQuota">Video quota</label> | 49 | {{ formErrors.role }} |
58 | <select class="form-control" id="videoQuota" formControlName="videoQuota"> | 50 | </div> |
59 | <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value"> | 51 | </div> |
60 | {{ videoQuotaOption.label }} | ||
61 | </option> | ||
62 | </select> | ||
63 | 52 | ||
64 | <div class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()"> | 53 | <div class="form-group"> |
65 | Transcoding is enabled on server. The video quota only take in account <strong>original</strong> video. <br /> | 54 | <label for="videoQuota">Video quota</label> |
66 | In maximum, this user could use ~ {{ computeQuotaWithTranscoding() | bytes }}. | 55 | <select class="form-control" id="videoQuota" formControlName="videoQuota"> |
67 | </div> | 56 | <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value"> |
68 | </div> | 57 | {{ videoQuotaOption.label }} |
58 | </option> | ||
59 | </select> | ||
69 | 60 | ||
70 | <input type="submit" value="{{ getFormButtonTitle() }}" class="btn btn-default" [disabled]="!form.valid"> | 61 | <div class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()"> |
71 | </form> | 62 | Transcoding is enabled on server. The video quota only take in account <strong>original</strong> video. <br /> |
63 | In maximum, this user could use ~ {{ computeQuotaWithTranscoding() | bytes }}. | ||
64 | </div> | ||
72 | </div> | 65 | </div> |
73 | </div> | 66 | |
67 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> | ||
68 | </form> | ||
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.scss b/client/src/app/+admin/users/user-edit/user-edit.component.scss index 401caa0c6..68d270c19 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.scss +++ b/client/src/app/+admin/users/user-edit/user-edit.component.scss | |||
@@ -1,3 +1,21 @@ | |||
1 | .admin-sub-title { | ||
2 | margin-bottom: 30px; | ||
3 | } | ||
4 | |||
5 | input:not([type=submit]) { | ||
6 | @include peertube-input-text(340px); | ||
7 | display: block; | ||
8 | } | ||
9 | |||
10 | select { | ||
11 | @include peertube-select(340px); | ||
12 | } | ||
13 | |||
14 | input[type=submit] { | ||
15 | @include peertube-button; | ||
16 | @include orange-button; | ||
17 | } | ||
18 | |||
1 | .transcoding-information { | 19 | .transcoding-information { |
2 | margin-top: 5px; | 20 | margin-top: 5px; |
3 | font-size: 11px; | 21 | font-size: 11px; |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html index 16a8a8033..b3d90ba1e 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.html +++ b/client/src/app/+admin/users/user-list/user-list.component.html | |||
@@ -1,35 +1,26 @@ | |||
1 | <div class="row"> | 1 | <div class="admin-sub-header"> |
2 | <div class="content-padding"> | 2 | <div class="admin-sub-title">Users list</div> |
3 | 3 | ||
4 | <h3>Users list</h3> | 4 | <a class="add-button" routerLink="/admin/users/add"> |
5 | 5 | <span class="icon icon-add"></span> | |
6 | <p-dataTable | 6 | Add user |
7 | [value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | 7 | </a> |
8 | sortField="id" (onLazyLoad)="loadLazy($event)" | ||
9 | > | ||
10 | <p-column field="id" header="ID" [sortable]="true"></p-column> | ||
11 | <p-column field="username" header="Username" [sortable]="true"></p-column> | ||
12 | <p-column field="email" header="Email"></p-column> | ||
13 | <p-column field="videoQuota" header="Video quota"></p-column> | ||
14 | <p-column field="roleLabel" header="Role"></p-column> | ||
15 | <p-column field="createdAt" header="Created date" [sortable]="true"></p-column> | ||
16 | <p-column header="Edit" styleClass="action-cell"> | ||
17 | <ng-template pTemplate="body" let-user="rowData"> | ||
18 | <a [routerLink]="getRouterUserEditLink(user)" title="Edit this user"> | ||
19 | <span class="glyphicon glyphicon-pencil glyphicon-black"></span> | ||
20 | </a> | ||
21 | </ng-template> | ||
22 | </p-column> | ||
23 | <p-column header="Delete" styleClass="action-cell"> | ||
24 | <ng-template pTemplate="body" let-user="rowData"> | ||
25 | <span (click)="removeUser(user)" class="glyphicon glyphicon-remove glyphicon-black" title="Remove this user"></span> | ||
26 | </ng-template> | ||
27 | </p-column> | ||
28 | </p-dataTable> | ||
29 | |||
30 | <a class="add-user btn btn-success pull-right" [routerLink]="['/admin/users/add']"> | ||
31 | <span class="glyphicon glyphicon-plus"></span> | ||
32 | Add user | ||
33 | </a> | ||
34 | </div> | ||
35 | </div> | 8 | </div> |
9 | |||
10 | <p-dataTable | ||
11 | [value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | ||
12 | sortField="id" (onLazyLoad)="loadLazy($event)" | ||
13 | > | ||
14 | <p-column field="id" header="ID" [sortable]="true"></p-column> | ||
15 | <p-column field="username" header="Username" [sortable]="true"></p-column> | ||
16 | <p-column field="email" header="Email"></p-column> | ||
17 | <p-column field="videoQuota" header="Video quota"></p-column> | ||
18 | <p-column field="roleLabel" header="Role"></p-column> | ||
19 | <p-column field="createdAt" header="Created date" [sortable]="true"></p-column> | ||
20 | <p-column styleClass="action-cell"> | ||
21 | <ng-template pTemplate="body" let-user="rowData"> | ||
22 | <my-edit-button [routerLink]="getRouterUserEditLink(user)"></my-edit-button> | ||
23 | <my-delete-button (click)="removeUser(user)"></my-delete-button> | ||
24 | </ng-template> | ||
25 | </p-column> | ||
26 | </p-dataTable> | ||
diff --git a/client/src/app/+admin/users/user-list/user-list.component.scss b/client/src/app/+admin/users/user-list/user-list.component.scss index 71adef653..8b22f67ff 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.scss +++ b/client/src/app/+admin/users/user-list/user-list.component.scss | |||
@@ -1,3 +1,11 @@ | |||
1 | .add-user { | 1 | .add-button { |
2 | margin-top: 10px; | 2 | @include peertube-button-link; |
3 | } | 3 | @include orange-button; |
4 | |||
5 | .icon.icon-add { | ||
6 | @include icon(22px); | ||
7 | |||
8 | margin-right: 3px; | ||
9 | background-image: url('../../../../assets/images/admin/add.svg'); | ||
10 | } | ||
11 | } | ||
diff --git a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html index ab0a9d99f..d655a5e9b 100644 --- a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html +++ b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html | |||
@@ -1,24 +1,19 @@ | |||
1 | <div class="row"> | 1 | <div class="admin-sub-header"> |
2 | <div class="content-padding"> | 2 | <div class="admin-sub-title">Video abuses list</div> |
3 | |||
4 | <h3>Video abuses list</h3> | ||
5 | |||
6 | <p-dataTable | ||
7 | [value]="videoAbuses" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | ||
8 | sortField="id" (onLazyLoad)="loadLazy($event)" | ||
9 | > | ||
10 | <p-column field="id" header="ID" [sortable]="true"></p-column> | ||
11 | <p-column field="reason" header="Reason"></p-column> | ||
12 | <p-column field="reporterServerHost" header="Reporter server host"></p-column> | ||
13 | <p-column field="reporterUsername" header="Reporter username"></p-column> | ||
14 | <p-column field="videoName" header="Video name"></p-column> | ||
15 | <p-column header="Video" styleClass="action-cell"> | ||
16 | <ng-template pTemplate="body" let-videoAbuse="rowData"> | ||
17 | <a [routerLink]="getRouterVideoLink(videoAbuse.videoId)" title="Go to the video">{{ videoAbuse.videoId }}</a> | ||
18 | </ng-template> | ||
19 | </p-column> | ||
20 | <p-column field="createdAt" header="Created date" [sortable]="true"></p-column> | ||
21 | </p-dataTable> | ||
22 | |||
23 | </div> | ||
24 | </div> | 3 | </div> |
4 | |||
5 | <p-dataTable | ||
6 | [value]="videoAbuses" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | ||
7 | sortField="id" (onLazyLoad)="loadLazy($event)" | ||
8 | > | ||
9 | <p-column field="id" header="ID" [sortable]="true"></p-column> | ||
10 | <p-column field="reason" header="Reason"></p-column> | ||
11 | <p-column field="reporterServerHost" header="Reporter server host"></p-column> | ||
12 | <p-column field="reporterUsername" header="Reporter username"></p-column> | ||
13 | <p-column field="createdAt" header="Created date" [sortable]="true"></p-column> | ||
14 | <p-column header="Video"> | ||
15 | <ng-template pTemplate="body" let-videoAbuse="rowData"> | ||
16 | <a [routerLink]="getRouterVideoLink(videoAbuse.videoId)" title="Go to the video">{{ videoAbuse.videoName }}</a> | ||
17 | </ng-template> | ||
18 | </p-column> | ||
19 | </p-dataTable> | ||
diff --git a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.scss b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.scss new file mode 100644 index 000000000..6a4762650 --- /dev/null +++ b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.scss | |||
@@ -0,0 +1,6 @@ | |||
1 | /deep/ a { | ||
2 | |||
3 | &, &:hover, &:active, &:focus { | ||
4 | color: #000; | ||
5 | } | ||
6 | } | ||
diff --git a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts index 654603d01..b4d3bbd24 100644 --- a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts +++ b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts | |||
@@ -8,7 +8,8 @@ import { VideoAbuse } from '../../../../../../shared' | |||
8 | 8 | ||
9 | @Component({ | 9 | @Component({ |
10 | selector: 'my-video-abuse-list', | 10 | selector: 'my-video-abuse-list', |
11 | templateUrl: './video-abuse-list.component.html' | 11 | templateUrl: './video-abuse-list.component.html', |
12 | styleUrls: [ './video-abuse-list.component.scss'] | ||
12 | }) | 13 | }) |
13 | export class VideoAbuseListComponent extends RestTable implements OnInit { | 14 | export class VideoAbuseListComponent extends RestTable implements OnInit { |
14 | videoAbuses: VideoAbuse[] = [] | 15 | videoAbuses: VideoAbuse[] = [] |
diff --git a/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html b/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html index 05d116798..1d813fa07 100644 --- a/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html +++ b/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html | |||
@@ -18,7 +18,7 @@ | |||
18 | <p-column field="createdAt" header="Created date" [sortable]="true"></p-column> | 18 | <p-column field="createdAt" header="Created date" [sortable]="true"></p-column> |
19 | <p-column header="Delete" styleClass="action-cell"> | 19 | <p-column header="Delete" styleClass="action-cell"> |
20 | <ng-template pTemplate="body" let-entry="rowData"> | 20 | <ng-template pTemplate="body" let-entry="rowData"> |
21 | <span (click)="removeVideoFromBlacklist(entry)" class="glyphicon glyphicon-remove glyphicon-black" title="Remove this video from blacklist"></span> | 21 | <my-delete-button (click)="removeVideoFromBlacklist(entry)"></my-delete-button> |
22 | </ng-template> | 22 | </ng-template> |
23 | </p-column> | 23 | </p-column> |
24 | </p-dataTable> | 24 | </p-dataTable> |
diff --git a/client/src/app/account/account-change-password/account-change-password.component.html b/client/src/app/account/account-change-password/account-change-password.component.html deleted file mode 100644 index 92d9f900a..000000000 --- a/client/src/app/account/account-change-password/account-change-password.component.html +++ /dev/null | |||
@@ -1,24 +0,0 @@ | |||
1 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | ||
2 | |||
3 | <form role="form" (ngSubmit)="changePassword()" [formGroup]="form"> | ||
4 | <div class="form-group"> | ||
5 | <label for="new-password">New password</label> | ||
6 | <input | ||
7 | type="password" class="form-control" id="new-password" | ||
8 | formControlName="new-password" | ||
9 | > | ||
10 | <div *ngIf="formErrors['new-password']" class="alert alert-danger"> | ||
11 | {{ formErrors['new-password'] }} | ||
12 | </div> | ||
13 | </div> | ||
14 | |||
15 | <div class="form-group"> | ||
16 | <label for="name">Confirm new password</label> | ||
17 | <input | ||
18 | type="password" class="form-control" id="new-confirmed-password" | ||
19 | formControlName="new-confirmed-password" | ||
20 | > | ||
21 | </div> | ||
22 | |||
23 | <input type="submit" value="Change password" class="btn btn-default" [disabled]="!form.valid"> | ||
24 | </form> | ||
diff --git a/client/src/app/account/account-details/account-details.component.html b/client/src/app/account/account-details/account-details.component.html deleted file mode 100644 index 8f4f176af..000000000 --- a/client/src/app/account/account-details/account-details.component.html +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | ||
2 | |||
3 | <form role="form" (ngSubmit)="updateDetails()" [formGroup]="form"> | ||
4 | <div class="form-group"> | ||
5 | <input | ||
6 | type="checkbox" id="displayNSFW" | ||
7 | formControlName="displayNSFW" | ||
8 | > | ||
9 | <label for="displayNSFW">Display videos that contain mature or explicit content</label> | ||
10 | <div *ngIf="formErrors['displayNSFW']" class="alert alert-danger"> | ||
11 | {{ formErrors['displayNSFW'] }} | ||
12 | </div> | ||
13 | </div> | ||
14 | |||
15 | <input type="submit" value="Update" class="btn btn-default" [disabled]="!form.valid"> | ||
16 | </form> | ||
diff --git a/client/src/app/account/account-routing.module.ts b/client/src/app/account/account-routing.module.ts index 74d9aa03e..070b9b5c5 100644 --- a/client/src/app/account/account-routing.module.ts +++ b/client/src/app/account/account-routing.module.ts | |||
@@ -5,17 +5,34 @@ import { MetaGuard } from '@ngx-meta/core' | |||
5 | 5 | ||
6 | import { LoginGuard } from '../core' | 6 | import { LoginGuard } from '../core' |
7 | import { AccountComponent } from './account.component' | 7 | import { AccountComponent } from './account.component' |
8 | import { AccountSettingsComponent } from './account-settings/account-settings.component' | ||
9 | import { AccountVideosComponent } from './account-videos/account-videos.component' | ||
8 | 10 | ||
9 | const accountRoutes: Routes = [ | 11 | const accountRoutes: Routes = [ |
10 | { | 12 | { |
11 | path: 'account', | 13 | path: 'account', |
12 | component: AccountComponent, | 14 | component: AccountComponent, |
13 | canActivate: [ MetaGuard, LoginGuard ], | 15 | canActivateChild: [ MetaGuard, LoginGuard ], |
14 | data: { | 16 | children: [ |
15 | meta: { | 17 | { |
16 | title: 'My account' | 18 | path: 'settings', |
19 | component: AccountSettingsComponent, | ||
20 | data: { | ||
21 | meta: { | ||
22 | title: 'Account settings' | ||
23 | } | ||
24 | } | ||
25 | }, | ||
26 | { | ||
27 | path: 'videos', | ||
28 | component: AccountVideosComponent, | ||
29 | data: { | ||
30 | meta: { | ||
31 | title: 'Account videos' | ||
32 | } | ||
33 | } | ||
17 | } | 34 | } |
18 | } | 35 | ] |
19 | } | 36 | } |
20 | ] | 37 | ] |
21 | 38 | ||
diff --git a/client/src/app/account/account-settings/account-change-password/account-change-password.component.html b/client/src/app/account/account-settings/account-change-password/account-change-password.component.html new file mode 100644 index 000000000..b0e3cada4 --- /dev/null +++ b/client/src/app/account/account-settings/account-change-password/account-change-password.component.html | |||
@@ -0,0 +1,20 @@ | |||
1 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | ||
2 | |||
3 | <form role="form" (ngSubmit)="changePassword()" [formGroup]="form"> | ||
4 | |||
5 | <label for="new-password">Change password</label> | ||
6 | <input | ||
7 | type="password" id="new-password" placeholder="New password" | ||
8 | formControlName="new-password" [ngClass]="{ 'input-error': formErrors['new-password'] }" | ||
9 | > | ||
10 | <div *ngIf="formErrors['new-password']" class="form-error"> | ||
11 | {{ formErrors['new-password'] }} | ||
12 | </div> | ||
13 | |||
14 | <input | ||
15 | type="password" id="new-confirmed-password" placeholder="Confirm new password" | ||
16 | formControlName="new-confirmed-password" | ||
17 | > | ||
18 | |||
19 | <input type="submit" value="Change password" [disabled]="!form.valid"> | ||
20 | </form> | ||
diff --git a/client/src/app/account/account-settings/account-change-password/account-change-password.component.scss b/client/src/app/account/account-settings/account-change-password/account-change-password.component.scss new file mode 100644 index 000000000..7a4fdb34d --- /dev/null +++ b/client/src/app/account/account-settings/account-change-password/account-change-password.component.scss | |||
@@ -0,0 +1,16 @@ | |||
1 | input[type=password] { | ||
2 | @include peertube-input-text(340px); | ||
3 | display: block; | ||
4 | |||
5 | &#new-confirmed-password { | ||
6 | margin-top: 15px; | ||
7 | } | ||
8 | } | ||
9 | |||
10 | input[type=submit] { | ||
11 | @include peertube-button; | ||
12 | @include orange-button; | ||
13 | |||
14 | margin-top: 15px; | ||
15 | } | ||
16 | |||
diff --git a/client/src/app/account/account-change-password/account-change-password.component.ts b/client/src/app/account/account-settings/account-change-password/account-change-password.component.ts index 69edec54b..8979e1734 100644 --- a/client/src/app/account/account-change-password/account-change-password.component.ts +++ b/client/src/app/account/account-settings/account-change-password/account-change-password.component.ts | |||
@@ -1,16 +1,13 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { FormBuilder, FormGroup } from '@angular/forms' | 2 | import { FormBuilder, FormGroup } from '@angular/forms' |
3 | import { Router } from '@angular/router' | ||
4 | |||
5 | import { NotificationsService } from 'angular2-notifications' | 3 | import { NotificationsService } from 'angular2-notifications' |
6 | 4 | import { FormReactive, USER_PASSWORD, UserService } from '../../../shared' | |
7 | import { FormReactive, UserService, USER_PASSWORD } from '../../shared' | ||
8 | 5 | ||
9 | @Component({ | 6 | @Component({ |
10 | selector: 'my-account-change-password', | 7 | selector: 'my-account-change-password', |
11 | templateUrl: './account-change-password.component.html' | 8 | templateUrl: './account-change-password.component.html', |
9 | styleUrls: [ './account-change-password.component.scss' ] | ||
12 | }) | 10 | }) |
13 | |||
14 | export class AccountChangePasswordComponent extends FormReactive implements OnInit { | 11 | export class AccountChangePasswordComponent extends FormReactive implements OnInit { |
15 | error: string = null | 12 | error: string = null |
16 | 13 | ||
diff --git a/client/src/app/account/account-change-password/index.ts b/client/src/app/account/account-settings/account-change-password/index.ts index 44c330b66..44c330b66 100644 --- a/client/src/app/account/account-change-password/index.ts +++ b/client/src/app/account/account-settings/account-change-password/index.ts | |||
diff --git a/client/src/app/account/account-settings/account-details/account-details.component.html b/client/src/app/account/account-settings/account-details/account-details.component.html new file mode 100644 index 000000000..bc18b39b4 --- /dev/null +++ b/client/src/app/account/account-settings/account-details/account-details.component.html | |||
@@ -0,0 +1,14 @@ | |||
1 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | ||
2 | |||
3 | <form role="form" (ngSubmit)="updateDetails()" [formGroup]="form"> | ||
4 | <input | ||
5 | type="checkbox" id="displayNSFW" | ||
6 | formControlName="displayNSFW" | ||
7 | > | ||
8 | <label for="displayNSFW">Display videos that contain mature or explicit content</label> | ||
9 | <div *ngIf="formErrors['displayNSFW']" class="alert alert-danger"> | ||
10 | {{ formErrors['displayNSFW'] }} | ||
11 | </div> | ||
12 | |||
13 | <input type="submit" value="Save" [disabled]="!form.valid"> | ||
14 | </form> | ||
diff --git a/client/src/app/account/account-settings/account-details/account-details.component.scss b/client/src/app/account/account-settings/account-details/account-details.component.scss new file mode 100644 index 000000000..5c369f968 --- /dev/null +++ b/client/src/app/account/account-settings/account-details/account-details.component.scss | |||
@@ -0,0 +1,13 @@ | |||
1 | label { | ||
2 | font-size: 15px; | ||
3 | font-weight: $font-regular; | ||
4 | margin-left: 5px; | ||
5 | } | ||
6 | |||
7 | input[type=submit] { | ||
8 | @include peertube-button; | ||
9 | @include orange-button; | ||
10 | |||
11 | display: block; | ||
12 | margin-top: 15px; | ||
13 | } | ||
diff --git a/client/src/app/account/account-details/account-details.component.ts b/client/src/app/account/account-settings/account-details/account-details.component.ts index d7a6e6871..d835c53e5 100644 --- a/client/src/app/account/account-details/account-details.component.ts +++ b/client/src/app/account/account-settings/account-details/account-details.component.ts | |||
@@ -1,21 +1,14 @@ | |||
1 | import { Component, OnInit, Input } from '@angular/core' | 1 | import { Component, Input, OnInit } from '@angular/core' |
2 | import { FormBuilder, FormGroup } from '@angular/forms' | 2 | import { FormBuilder, FormGroup } from '@angular/forms' |
3 | import { Router } from '@angular/router' | ||
4 | |||
5 | import { NotificationsService } from 'angular2-notifications' | 3 | import { NotificationsService } from 'angular2-notifications' |
6 | 4 | import { UserUpdateMe } from '../../../../../../shared' | |
7 | import { AuthService } from '../../core' | 5 | import { AuthService } from '../../../core' |
8 | import { | 6 | import { FormReactive, User, UserService } from '../../../shared' |
9 | FormReactive, | ||
10 | User, | ||
11 | UserService, | ||
12 | USER_PASSWORD | ||
13 | } from '../../shared' | ||
14 | import { UserUpdateMe } from '../../../../../shared' | ||
15 | 7 | ||
16 | @Component({ | 8 | @Component({ |
17 | selector: 'my-account-details', | 9 | selector: 'my-account-details', |
18 | templateUrl: './account-details.component.html' | 10 | templateUrl: './account-details.component.html', |
11 | styleUrls: [ './account-details.component.scss' ] | ||
19 | }) | 12 | }) |
20 | 13 | ||
21 | export class AccountDetailsComponent extends FormReactive implements OnInit { | 14 | export class AccountDetailsComponent extends FormReactive implements OnInit { |
diff --git a/client/src/app/account/account-details/index.ts b/client/src/app/account/account-settings/account-details/index.ts index 4829f608a..4829f608a 100644 --- a/client/src/app/account/account-details/index.ts +++ b/client/src/app/account/account-settings/account-details/index.ts | |||
diff --git a/client/src/app/account/account-settings/account-settings.component.html b/client/src/app/account/account-settings/account-settings.component.html new file mode 100644 index 000000000..c0a74cc47 --- /dev/null +++ b/client/src/app/account/account-settings/account-settings.component.html | |||
@@ -0,0 +1,15 @@ | |||
1 | <div class="user"> | ||
2 | <img [src]="getAvatarPath()" alt="Avatar" /> | ||
3 | |||
4 | <div class="user-info"> | ||
5 | <div class="user-info-username">{{ user.username }}</div> | ||
6 | <div class="user-info-followers">{{ user.account?.followersCount }} subscribers</div> | ||
7 | </div> | ||
8 | </div> | ||
9 | |||
10 | |||
11 | <div class="account-title">Account settings</div> | ||
12 | <my-account-change-password></my-account-change-password> | ||
13 | |||
14 | <div class="account-title">Filtering</div> | ||
15 | <my-account-details [user]="user"></my-account-details> | ||
diff --git a/client/src/app/account/account-settings/account-settings.component.scss b/client/src/app/account/account-settings/account-settings.component.scss new file mode 100644 index 000000000..f514809b0 --- /dev/null +++ b/client/src/app/account/account-settings/account-settings.component.scss | |||
@@ -0,0 +1,28 @@ | |||
1 | .user { | ||
2 | display: flex; | ||
3 | |||
4 | img { | ||
5 | @include avatar(50px); | ||
6 | margin-right: 15px; | ||
7 | } | ||
8 | |||
9 | .user-info { | ||
10 | .user-info-username { | ||
11 | font-size: 20px; | ||
12 | font-weight: $font-bold; | ||
13 | } | ||
14 | |||
15 | .user-info-followers { | ||
16 | font-size: 15px; | ||
17 | } | ||
18 | } | ||
19 | } | ||
20 | |||
21 | .account-title { | ||
22 | text-transform: uppercase; | ||
23 | color: $orange-color; | ||
24 | font-weight: $font-bold; | ||
25 | font-size: 13px; | ||
26 | margin-top: 55px; | ||
27 | margin-bottom: 30px; | ||
28 | } | ||
diff --git a/client/src/app/account/account-settings/account-settings.component.ts b/client/src/app/account/account-settings/account-settings.component.ts new file mode 100644 index 000000000..cba251000 --- /dev/null +++ b/client/src/app/account/account-settings/account-settings.component.ts | |||
@@ -0,0 +1,22 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { User } from '../../shared' | ||
3 | import { AuthService } from '../../core' | ||
4 | |||
5 | @Component({ | ||
6 | selector: 'my-account-settings', | ||
7 | templateUrl: './account-settings.component.html', | ||
8 | styleUrls: [ './account-settings.component.scss' ] | ||
9 | }) | ||
10 | export class AccountSettingsComponent implements OnInit { | ||
11 | user: User = null | ||
12 | |||
13 | constructor (private authService: AuthService) {} | ||
14 | |||
15 | ngOnInit () { | ||
16 | this.user = this.authService.getUser() | ||
17 | } | ||
18 | |||
19 | getAvatarPath () { | ||
20 | return this.user.getAvatarPath() | ||
21 | } | ||
22 | } | ||
diff --git a/client/src/app/account/account-videos/account-videos.component.html b/client/src/app/account/account-videos/account-videos.component.html new file mode 100644 index 000000000..f69c0487d --- /dev/null +++ b/client/src/app/account/account-videos/account-videos.component.html | |||
@@ -0,0 +1,39 @@ | |||
1 | <div | ||
2 | class="videos" | ||
3 | infiniteScroll | ||
4 | [infiniteScrollDistance]="0.5" | ||
5 | [infiniteScrollUpDistance]="1.5" | ||
6 | (scrolled)="onNearOfBottom()" | ||
7 | (scrolledUp)="onNearOfTop()" | ||
8 | > | ||
9 | <div class="video" *ngFor="let video of videos; let i = index"> | ||
10 | <input type="checkbox" [(ngModel)]="checkedVideos[video.id]" /> | ||
11 | |||
12 | <my-video-thumbnail [video]="video"></my-video-thumbnail> | ||
13 | |||
14 | <div class="video-info"> | ||
15 | <div class="video-info-name">{{ video.name }}</div> | ||
16 | <span class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> | ||
17 | </div> | ||
18 | |||
19 | <!-- Display only once --> | ||
20 | <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0"> | ||
21 | <div class="action-selection-mode-child"> | ||
22 | <span class="action-button action-button-cancel-selection" (click)="abortSelectionMode()"> | ||
23 | Cancel | ||
24 | </span> | ||
25 | |||
26 | <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()"> | ||
27 | <span class="icon icon-delete-white"></span> | ||
28 | Delete | ||
29 | </span> | ||
30 | </div> | ||
31 | </div> | ||
32 | |||
33 | <div class="video-buttons" *ngIf="isInSelectionMode() === false"> | ||
34 | <my-delete-button (click)="deleteVideo(video)"></my-delete-button> | ||
35 | |||
36 | <my-edit-button [routerLink]="[ '/videos', 'edit', video.uuid ]"></my-edit-button> | ||
37 | </div> | ||
38 | </div> | ||
39 | </div> | ||
diff --git a/client/src/app/account/account-videos/account-videos.component.scss b/client/src/app/account/account-videos/account-videos.component.scss new file mode 100644 index 000000000..5459014a6 --- /dev/null +++ b/client/src/app/account/account-videos/account-videos.component.scss | |||
@@ -0,0 +1,96 @@ | |||
1 | .action-selection-mode { | ||
2 | width: 174px; | ||
3 | display: flex; | ||
4 | justify-content: flex-end; | ||
5 | |||
6 | .action-selection-mode-child { | ||
7 | position: fixed; | ||
8 | |||
9 | .action-button { | ||
10 | display: inline-block; | ||
11 | } | ||
12 | |||
13 | .action-button-cancel-selection { | ||
14 | @include peertube-button; | ||
15 | @include grey-button; | ||
16 | |||
17 | margin-right: 10px; | ||
18 | } | ||
19 | |||
20 | .action-button-delete-selection { | ||
21 | @include peertube-button; | ||
22 | @include orange-button; | ||
23 | } | ||
24 | |||
25 | .icon.icon-delete-white { | ||
26 | @include icon(21px); | ||
27 | |||
28 | position: relative; | ||
29 | top: -2px; | ||
30 | background-image: url('../../../assets/images/global/delete-white.svg'); | ||
31 | } | ||
32 | } | ||
33 | } | ||
34 | |||
35 | /deep/ .action-button { | ||
36 | &.action-button-delete { | ||
37 | margin-right: 10px; | ||
38 | } | ||
39 | } | ||
40 | |||
41 | .video { | ||
42 | display: flex; | ||
43 | height: 130px; | ||
44 | padding-bottom: 20px; | ||
45 | |||
46 | input[type=checkbox] { | ||
47 | margin-right: 20px; | ||
48 | outline: 0; | ||
49 | } | ||
50 | |||
51 | &:first-child { | ||
52 | margin-top: 47px; | ||
53 | } | ||
54 | |||
55 | &:not(:last-child) { | ||
56 | margin-bottom: 20px; | ||
57 | border-bottom: 1px solid #C6C6C6; | ||
58 | } | ||
59 | |||
60 | my-video-thumbnail { | ||
61 | margin-right: 10px; | ||
62 | } | ||
63 | |||
64 | .video-info { | ||
65 | flex-grow: 1; | ||
66 | |||
67 | .video-info-name { | ||
68 | font-size: 16px; | ||
69 | font-weight: $font-semibold; | ||
70 | } | ||
71 | |||
72 | .video-info-date-views { | ||
73 | font-size: 13px; | ||
74 | } | ||
75 | } | ||
76 | } | ||
77 | |||
78 | @media screen and (max-width: 800px) { | ||
79 | .video { | ||
80 | flex-direction: column; | ||
81 | height: auto; | ||
82 | text-align: center; | ||
83 | |||
84 | input[type=checkbox] { | ||
85 | display: none; | ||
86 | } | ||
87 | |||
88 | my-video-thumbnail { | ||
89 | margin-right: 0; | ||
90 | } | ||
91 | |||
92 | .video-buttons { | ||
93 | margin-top: 10px; | ||
94 | } | ||
95 | } | ||
96 | } | ||
diff --git a/client/src/app/account/account-videos/account-videos.component.ts b/client/src/app/account/account-videos/account-videos.component.ts new file mode 100644 index 000000000..5f12cfce0 --- /dev/null +++ b/client/src/app/account/account-videos/account-videos.component.ts | |||
@@ -0,0 +1,97 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { NotificationsService } from 'angular2-notifications' | ||
4 | import 'rxjs/add/observable/from' | ||
5 | import 'rxjs/add/operator/concatAll' | ||
6 | import { Observable } from 'rxjs/Observable' | ||
7 | import { ConfirmService } from '../../core/confirm' | ||
8 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | ||
9 | import { Video } from '../../shared/video/video.model' | ||
10 | import { VideoService } from '../../shared/video/video.service' | ||
11 | |||
12 | @Component({ | ||
13 | selector: 'my-account-videos', | ||
14 | templateUrl: './account-videos.component.html', | ||
15 | styleUrls: [ './account-videos.component.scss' ] | ||
16 | }) | ||
17 | export class AccountVideosComponent extends AbstractVideoList implements OnInit { | ||
18 | titlePage = 'My videos' | ||
19 | currentRoute = '/account/videos' | ||
20 | checkedVideos: { [ id: number ]: boolean } = {} | ||
21 | |||
22 | constructor (protected router: Router, | ||
23 | protected route: ActivatedRoute, | ||
24 | protected notificationsService: NotificationsService, | ||
25 | protected confirmService: ConfirmService, | ||
26 | private videoService: VideoService) { | ||
27 | super() | ||
28 | } | ||
29 | |||
30 | ngOnInit () { | ||
31 | super.ngOnInit() | ||
32 | } | ||
33 | |||
34 | abortSelectionMode () { | ||
35 | this.checkedVideos = {} | ||
36 | } | ||
37 | |||
38 | isInSelectionMode () { | ||
39 | return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true) | ||
40 | } | ||
41 | |||
42 | getVideosObservable () { | ||
43 | return this.videoService.getMyVideos(this.pagination, this.sort) | ||
44 | } | ||
45 | |||
46 | deleteSelectedVideos () { | ||
47 | const toDeleteVideosIds = Object.keys(this.checkedVideos) | ||
48 | .filter(k => this.checkedVideos[k] === true) | ||
49 | .map(k => parseInt(k, 10)) | ||
50 | |||
51 | this.confirmService.confirm(`Do you really want to delete ${toDeleteVideosIds.length} videos?`, 'Delete').subscribe( | ||
52 | res => { | ||
53 | if (res === false) return | ||
54 | |||
55 | const observables: Observable<any>[] = [] | ||
56 | for (const videoId of toDeleteVideosIds) { | ||
57 | const o = this.videoService | ||
58 | .removeVideo(videoId) | ||
59 | .do(() => this.spliceVideosById(videoId)) | ||
60 | |||
61 | observables.push(o) | ||
62 | } | ||
63 | |||
64 | Observable.from(observables) | ||
65 | .concatAll() | ||
66 | .subscribe( | ||
67 | res => this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`), | ||
68 | |||
69 | err => this.notificationsService.error('Error', err.text) | ||
70 | ) | ||
71 | } | ||
72 | ) | ||
73 | } | ||
74 | |||
75 | deleteVideo (video: Video) { | ||
76 | this.confirmService.confirm(`Do you really want to delete ${video.name}?`, 'Delete').subscribe( | ||
77 | res => { | ||
78 | if (res === false) return | ||
79 | |||
80 | this.videoService.removeVideo(video.id) | ||
81 | .subscribe( | ||
82 | status => { | ||
83 | this.notificationsService.success('Success', `Video ${video.name} deleted.`) | ||
84 | this.spliceVideosById(video.id) | ||
85 | }, | ||
86 | |||
87 | error => this.notificationsService.error('Error', error.text) | ||
88 | ) | ||
89 | } | ||
90 | ) | ||
91 | } | ||
92 | |||
93 | private spliceVideosById (id: number) { | ||
94 | const index = this.videos.findIndex(v => v.id === id) | ||
95 | this.videos.splice(index, 1) | ||
96 | } | ||
97 | } | ||
diff --git a/client/src/app/account/account.component.html b/client/src/app/account/account.component.html index 177e54999..d82a4ca4d 100644 --- a/client/src/app/account/account.component.html +++ b/client/src/app/account/account.component.html | |||
@@ -1,25 +1,11 @@ | |||
1 | <div class="row"> | 1 | <div class="row"> |
2 | <div class="content-padding"> | 2 | <div class="sub-menu"> |
3 | <h3>Account</h3> | 3 | <a routerLink="/account/settings" routerLinkActive="active" class="title-page">My account</a> |
4 | 4 | ||
5 | <div class="col-md-6 col-sm-12"> | 5 | <a routerLink="/account/videos" routerLinkActive="active" class="title-page">My videos</a> |
6 | <div class="panel panel-default"> | 6 | </div> |
7 | <div class="panel-heading">Change password</div> | ||
8 | |||
9 | <div class="panel-body"> | ||
10 | <my-account-change-password></my-account-change-password> | ||
11 | </div> | ||
12 | </div> | ||
13 | </div> | ||
14 | |||
15 | <div class="col-md-6 col-sm-12"> | ||
16 | <div class="panel panel-default"> | ||
17 | <div class="panel-heading">Update my informations</div> | ||
18 | 7 | ||
19 | <div class="panel-body"> | 8 | <div class="margin-content"> |
20 | <my-account-details [user]="user"></my-account-details> | 9 | <router-outlet></router-outlet> |
21 | </div> | ||
22 | </div> | ||
23 | </div> | ||
24 | </div> | 10 | </div> |
25 | </div> | 11 | </div> |
diff --git a/client/src/app/account/account.component.scss b/client/src/app/account/account.component.scss index 61b80d0a7..e69de29bb 100644 --- a/client/src/app/account/account.component.scss +++ b/client/src/app/account/account.component.scss | |||
@@ -1,3 +0,0 @@ | |||
1 | .panel { | ||
2 | margin-top: 40px; | ||
3 | } | ||
diff --git a/client/src/app/account/account.component.ts b/client/src/app/account/account.component.ts index 929934f67..3d3677ab0 100644 --- a/client/src/app/account/account.component.ts +++ b/client/src/app/account/account.component.ts | |||
@@ -1,28 +1,8 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component } from '@angular/core' |
2 | import { FormBuilder, FormGroup } from '@angular/forms' | ||
3 | import { Router } from '@angular/router' | ||
4 | |||
5 | import { NotificationsService } from 'angular2-notifications' | ||
6 | |||
7 | import { AuthService } from '../core' | ||
8 | import { | ||
9 | FormReactive, | ||
10 | User, | ||
11 | UserService, | ||
12 | USER_PASSWORD | ||
13 | } from '../shared' | ||
14 | 2 | ||
15 | @Component({ | 3 | @Component({ |
16 | selector: 'my-account', | 4 | selector: 'my-account', |
17 | templateUrl: './account.component.html', | 5 | templateUrl: './account.component.html', |
18 | styleUrls: [ './account.component.scss' ] | 6 | styleUrls: [ './account.component.scss' ] |
19 | }) | 7 | }) |
20 | export class AccountComponent implements OnInit { | 8 | export class AccountComponent {} |
21 | user: User = null | ||
22 | |||
23 | constructor (private authService: AuthService) {} | ||
24 | |||
25 | ngOnInit () { | ||
26 | this.user = this.authService.getUser() | ||
27 | } | ||
28 | } | ||
diff --git a/client/src/app/account/account.module.ts b/client/src/app/account/account.module.ts index 380e9d235..020199e23 100644 --- a/client/src/app/account/account.module.ts +++ b/client/src/app/account/account.module.ts | |||
@@ -1,11 +1,12 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | 2 | import { SharedModule } from '../shared' | |
3 | import { AccountRoutingModule } from './account-routing.module' | 3 | import { AccountRoutingModule } from './account-routing.module' |
4 | import { AccountChangePasswordComponent } from './account-settings/account-change-password/account-change-password.component' | ||
5 | import { AccountDetailsComponent } from './account-settings/account-details/account-details.component' | ||
6 | import { AccountSettingsComponent } from './account-settings/account-settings.component' | ||
4 | import { AccountComponent } from './account.component' | 7 | import { AccountComponent } from './account.component' |
5 | import { AccountChangePasswordComponent } from './account-change-password' | ||
6 | import { AccountDetailsComponent } from './account-details' | ||
7 | import { AccountService } from './account.service' | 8 | import { AccountService } from './account.service' |
8 | import { SharedModule } from '../shared' | 9 | import { AccountVideosComponent } from './account-videos/account-videos.component' |
9 | 10 | ||
10 | @NgModule({ | 11 | @NgModule({ |
11 | imports: [ | 12 | imports: [ |
@@ -15,8 +16,10 @@ import { SharedModule } from '../shared' | |||
15 | 16 | ||
16 | declarations: [ | 17 | declarations: [ |
17 | AccountComponent, | 18 | AccountComponent, |
19 | AccountSettingsComponent, | ||
18 | AccountChangePasswordComponent, | 20 | AccountChangePasswordComponent, |
19 | AccountDetailsComponent | 21 | AccountDetailsComponent, |
22 | AccountVideosComponent | ||
20 | ], | 23 | ], |
21 | 24 | ||
22 | exports: [ | 25 | exports: [ |
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index 0f9484344..fe72c9181 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts | |||
@@ -6,7 +6,7 @@ import { PreloadSelectedModulesList } from './core' | |||
6 | const routes: Routes = [ | 6 | const routes: Routes = [ |
7 | { | 7 | { |
8 | path: '', | 8 | path: '', |
9 | redirectTo: '/videos/list', | 9 | redirectTo: '/videos/trending', |
10 | pathMatch: 'full' | 10 | pathMatch: 'full' |
11 | }, | 11 | }, |
12 | { | 12 | { |
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index 8a826e783..da4273dda 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html | |||
@@ -1,37 +1,26 @@ | |||
1 | <div class="container-fluid"> | 1 | <div> |
2 | <div class="row header"> | 2 | <div class="header"> |
3 | 3 | ||
4 | <div class="col-md-2 col-sm-3 col-xs-3 top-left-block" [ngClass]="{ 'border-bottom': isMenuDisplayed === false }"> | 4 | <div class="top-left-block" [ngClass]="{ 'border-bottom': isMenuDisplayed === false }"> |
5 | <div class="hamburger-block" (click)="toggleMenu()"> | 5 | <span class="icon icon-menu" (click)="toggleMenu()"></span> |
6 | <span class="glyphicon glyphicon-menu-hamburger"></span> | ||
7 | </div> | ||
8 | 6 | ||
9 | <div id="peertube-title"> | 7 | <a id="peertube-title" [routerLink]="['/videos/list']" title="Homepage"> |
10 | <a [routerLink]="['/videos/list']" title="Homepage"></a> | 8 | <span class="icon icon-logo"></span> |
11 | </div> | 9 | PeerTube |
10 | </a> | ||
12 | </div> | 11 | </div> |
13 | 12 | ||
14 | <!-- Used for the fixed title --> | 13 | <div class="header-right"> |
15 | <div class="col-md-2 col-sm-3 col-xs-3 fake-title-block"></div> | 14 | <my-header></my-header> |
16 | |||
17 | <!-- We need to reset col-md-* because my-search is in fixed position --> | ||
18 | <my-search class="col-md-10 col-sm-9 col-xs-9"></my-search> | ||
19 | </div> | ||
20 | |||
21 | <div class="row"> | ||
22 | <div class="col-md-2 col-sm-3 col-xs-3 title-menu-left"> | ||
23 | |||
24 | <div class="title-menu-left-block menu"> | ||
25 | <my-menu *ngIf="isMenuDisplayed && isInAdmin() === false"></my-menu> | ||
26 | <my-menu-admin *ngIf="isMenuDisplayed && isInAdmin() === true"></my-menu-admin> | ||
27 | </div> | ||
28 | </div> | 15 | </div> |
16 | </div> | ||
29 | 17 | ||
30 | <!-- Used for the fixed menu --> | 18 | <div class="sub-header-container"> |
31 | <div class="fake-menu col-md-2 col-sm-3 col-xs-3"> | 19 | <div *ngIf="isMenuDisplayed" class="title-menu-left"> |
20 | <my-menu></my-menu> | ||
32 | </div> | 21 | </div> |
33 | 22 | ||
34 | <div class="main-col" [ngClass]="getMainColClasses()"> | 23 | <div class="main-col container-fluid" [ngClass]="getMainColClasses()"> |
35 | 24 | ||
36 | <div class="main-row"> | 25 | <div class="main-row"> |
37 | <router-outlet></router-outlet> | 26 | <router-outlet></router-outlet> |
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index a656d5c29..008c6d1f0 100644 --- a/client/src/app/app.component.scss +++ b/client/src/app/app.component.scss | |||
@@ -2,10 +2,15 @@ | |||
2 | min-height: calc(100vh - #{$header-height} - #{$footer-height} - #{$footer-margin}); | 2 | min-height: calc(100vh - #{$header-height} - #{$footer-height} - #{$footer-margin}); |
3 | } | 3 | } |
4 | 4 | ||
5 | .sub-header-container { | ||
6 | margin-top: $header-height; | ||
7 | } | ||
8 | |||
5 | .title-menu-left { | 9 | .title-menu-left { |
6 | position: fixed; | 10 | position: fixed; |
7 | height: calc(100vh - #{$header-height}); | 11 | height: calc(100vh - #{$header-height}); |
8 | padding: 0; | 12 | padding: 0; |
13 | width: $menu-width; | ||
9 | 14 | ||
10 | .title-menu-left-block.menu { | 15 | .title-menu-left-block.menu { |
11 | height: 100%; | 16 | height: 100%; |
@@ -14,125 +19,62 @@ | |||
14 | 19 | ||
15 | .header { | 20 | .header { |
16 | height: $header-height; | 21 | height: $header-height; |
17 | 22 | position: fixed; | |
18 | .fake-title-block { | 23 | top: 0; |
19 | display: inline-block; | 24 | width: 100%; |
20 | } | 25 | background-color: #fff; |
26 | z-index: 1000; | ||
27 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.16); | ||
28 | display: flex; | ||
21 | 29 | ||
22 | .top-left-block { | 30 | .top-left-block { |
23 | z-index: 100; | 31 | width: $menu-width; |
24 | background-color: #fff; | 32 | z-index: 1001; |
25 | border-right: 1px solid $header-border-color; | ||
26 | height: $header-height; | 33 | height: $header-height; |
27 | line-height: $header-height; | ||
28 | margin-top: 0; | ||
29 | margin-bottom: 0; | ||
30 | display: flex; | 34 | display: flex; |
31 | position: fixed; | 35 | align-items: center; |
32 | padding: 0; | ||
33 | 36 | ||
34 | &.border-bottom { | 37 | .icon { |
35 | border-bottom: 1px solid $header-border-color; | 38 | @include icon(22px); |
36 | } | ||
37 | |||
38 | .hamburger-block { | ||
39 | margin-right: 15px; | ||
40 | margin-left: 15px; | ||
41 | 39 | ||
42 | .glyphicon { | 40 | &.icon-menu { |
43 | cursor: pointer; | 41 | background-image: url('../assets/images/header/menu.svg'); |
44 | position: relative; | 42 | margin: 0 18px 0 24px; |
45 | top: 4px; | ||
46 | } | 43 | } |
47 | } | 44 | } |
48 | 45 | ||
49 | #peertube-title { | 46 | #peertube-title { |
50 | a { | 47 | font-size: 20px; |
51 | color: inherit !important; | 48 | font-weight: $font-bold; |
52 | display: block; | 49 | color: inherit !important; |
53 | background: url('../assets/logo.png') no-repeat; | 50 | display: flex; |
54 | background-size: contain; | 51 | align-items: center; |
55 | background-position: center; | 52 | |
56 | height: 100%; | 53 | @include disable-default-a-behaviour; |
57 | margin: auto; | 54 | |
58 | width: 135px; | 55 | .icon.icon-logo { |
59 | 56 | display: inline-block; | |
60 | &:hover { | 57 | background: url('../assets/images/logo.svg') no-repeat; |
61 | color: inherit !important; | 58 | width: 23px; |
62 | text-decoration: none !important; | 59 | height: 24px; |
63 | } | ||
64 | } | 60 | } |
65 | } | 61 | } |
66 | 62 | ||
67 | @media screen and (max-width: 500px) { | 63 | @media screen and (max-width: 500px) { |
64 | width: 70px; | ||
65 | |||
68 | #peertube-title { | 66 | #peertube-title { |
69 | display: none; | 67 | display: none; |
70 | } | 68 | } |
71 | |||
72 | .hamburger-block { | ||
73 | width: 100%; | ||
74 | text-align: center; | ||
75 | } | ||
76 | } | ||
77 | |||
78 | @media screen and (min-width: 500px) and (max-width: 600px) { | ||
79 | #peertube-title a { | ||
80 | width: 80px; | ||
81 | } | ||
82 | } | ||
83 | |||
84 | @media screen and (min-width: 600px) and (max-width: 700px) { | ||
85 | #peertube-title a { | ||
86 | width: 100px; | ||
87 | } | ||
88 | } | ||
89 | |||
90 | @media screen and (min-width: 1000px) { | ||
91 | #peertube-title a { | ||
92 | width: 120px; | ||
93 | } | ||
94 | } | ||
95 | |||
96 | @media screen and (min-width: 1000px) { | ||
97 | #peertube-title a { | ||
98 | width: 120px; | ||
99 | } | ||
100 | } | ||
101 | |||
102 | @media screen and (min-width: 1200px) { | ||
103 | padding-left: 15px; | ||
104 | |||
105 | .hamburger-block { | ||
106 | margin-right: 15px; | ||
107 | } | ||
108 | |||
109 | #peertube-title a { | ||
110 | width: 135px; | ||
111 | } | ||
112 | } | ||
113 | |||
114 | @media screen and (min-width: 1600px) { | ||
115 | .hamburger-block { | ||
116 | margin-right: 20px; | ||
117 | } | ||
118 | |||
119 | #peertube-title a { | ||
120 | width: 180px; | ||
121 | } | ||
122 | } | 69 | } |
123 | } | 70 | } |
124 | 71 | ||
125 | my-search { | 72 | .header-right { |
126 | position: fixed; | 73 | height: $header-height; |
127 | z-index: 1000; | 74 | display: flex; |
128 | // Fix col-md-* padding | 75 | align-items: center; |
129 | padding: 0; | 76 | flex-grow: 1; |
130 | } | 77 | justify-content: flex-end; |
131 | |||
132 | .search-col { | ||
133 | height: 100%; | ||
134 | margin-left: -15px; | ||
135 | padding: 0; | ||
136 | } | 78 | } |
137 | } | 79 | } |
138 | 80 | ||
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 9b699fafd..b1818c298 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts | |||
@@ -1,8 +1,6 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | |||
4 | import { AuthService, ServerService } from './core' | 3 | import { AuthService, ServerService } from './core' |
5 | import { UserService } from './shared' | ||
6 | 4 | ||
7 | @Component({ | 5 | @Component({ |
8 | selector: 'my-app', | 6 | selector: 'my-app', |
@@ -62,20 +60,9 @@ export class AppComponent implements OnInit { | |||
62 | } | 60 | } |
63 | 61 | ||
64 | getMainColClasses () { | 62 | getMainColClasses () { |
65 | const colSizes = { | ||
66 | md: 10, | ||
67 | sm: 9, | ||
68 | xs: 9 | ||
69 | } | ||
70 | |||
71 | // Take all width is the menu is not displayed | 63 | // Take all width is the menu is not displayed |
72 | if (this.isMenuDisplayed === false) { | 64 | if (this.isMenuDisplayed === false) return [ 'expanded' ] |
73 | Object.keys(colSizes).forEach(col => colSizes[col] = 12) | ||
74 | } | ||
75 | |||
76 | const classes = [] | ||
77 | Object.keys(colSizes).forEach(col => classes.push(`col-${col}-${colSizes[col]}`)) | ||
78 | 65 | ||
79 | return classes | 66 | return [] |
80 | } | 67 | } |
81 | } | 68 | } |
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index e71641e0d..1326e3411 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts | |||
@@ -20,6 +20,8 @@ import { LoginModule } from './login' | |||
20 | import { SignupModule } from './signup' | 20 | import { SignupModule } from './signup' |
21 | import { SharedModule } from './shared' | 21 | import { SharedModule } from './shared' |
22 | import { VideosModule } from './videos' | 22 | import { VideosModule } from './videos' |
23 | import { MenuComponent } from './menu' | ||
24 | import { HeaderComponent } from './header' | ||
23 | 25 | ||
24 | export function metaFactory (): MetaLoader { | 26 | export function metaFactory (): MetaLoader { |
25 | return new MetaStaticLoader({ | 27 | return new MetaStaticLoader({ |
@@ -47,7 +49,10 @@ const APP_PROVIDERS = [ | |||
47 | @NgModule({ | 49 | @NgModule({ |
48 | bootstrap: [ AppComponent ], | 50 | bootstrap: [ AppComponent ], |
49 | declarations: [ | 51 | declarations: [ |
50 | AppComponent | 52 | AppComponent, |
53 | |||
54 | MenuComponent, | ||
55 | HeaderComponent | ||
51 | ], | 56 | ], |
52 | imports: [ | 57 | imports: [ |
53 | BrowserModule, | 58 | BrowserModule, |
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 9e6c6b888..e887dde1f 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts | |||
@@ -1,30 +1,25 @@ | |||
1 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' | ||
1 | import { Injectable } from '@angular/core' | 2 | import { Injectable } from '@angular/core' |
2 | import { Router } from '@angular/router' | 3 | import { Router } from '@angular/router' |
3 | import { Observable } from 'rxjs/Observable' | 4 | |
4 | import { Subject } from 'rxjs/Subject' | 5 | import { NotificationsService } from 'angular2-notifications' |
5 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' | 6 | import 'rxjs/add/observable/throw' |
6 | import { ReplaySubject } from 'rxjs/ReplaySubject' | ||
7 | import 'rxjs/add/operator/do' | 7 | import 'rxjs/add/operator/do' |
8 | import 'rxjs/add/operator/map' | 8 | import 'rxjs/add/operator/map' |
9 | import 'rxjs/add/operator/mergeMap' | 9 | import 'rxjs/add/operator/mergeMap' |
10 | import 'rxjs/add/observable/throw' | 10 | import { Observable } from 'rxjs/Observable' |
11 | 11 | import { ReplaySubject } from 'rxjs/ReplaySubject' | |
12 | import { NotificationsService } from 'angular2-notifications' | 12 | import { Subject } from 'rxjs/Subject' |
13 | 13 | import { OAuthClientLocal, User as UserServerModel, UserRefreshToken, UserRole, VideoChannel } from '../../../../../shared' | |
14 | import { AuthStatus } from './auth-status.model' | 14 | import { Account } from '../../../../../shared/models/accounts' |
15 | import { AuthUser } from './auth-user.model' | 15 | import { UserLogin } from '../../../../../shared/models/users/user-login.model' |
16 | import { | ||
17 | OAuthClientLocal, | ||
18 | UserRole, | ||
19 | UserRefreshToken, | ||
20 | VideoChannel, | ||
21 | User as UserServerModel | ||
22 | } from '../../../../../shared' | ||
23 | // Do not use the barrel (dependency loop) | 16 | // Do not use the barrel (dependency loop) |
24 | import { RestExtractor } from '../../shared/rest' | 17 | import { RestExtractor } from '../../shared/rest' |
25 | import { UserLogin } from '../../../../../shared/models/users/user-login.model' | ||
26 | import { UserConstructorHash } from '../../shared/users/user.model' | 18 | import { UserConstructorHash } from '../../shared/users/user.model' |
27 | 19 | ||
20 | import { AuthStatus } from './auth-status.model' | ||
21 | import { AuthUser } from './auth-user.model' | ||
22 | |||
28 | interface UserLoginWithUsername extends UserLogin { | 23 | interface UserLoginWithUsername extends UserLogin { |
29 | access_token: string | 24 | access_token: string |
30 | refresh_token: string | 25 | refresh_token: string |
@@ -42,10 +37,7 @@ interface UserLoginWithUserInformation extends UserLogin { | |||
42 | displayNSFW: boolean | 37 | displayNSFW: boolean |
43 | email: string | 38 | email: string |
44 | videoQuota: number | 39 | videoQuota: number |
45 | account: { | 40 | account: Account |
46 | id: number | ||
47 | uuid: string | ||
48 | } | ||
49 | videoChannels: VideoChannel[] | 41 | videoChannels: VideoChannel[] |
50 | } | 42 | } |
51 | 43 | ||
@@ -177,19 +169,15 @@ export class AuthService { | |||
177 | 169 | ||
178 | return this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers }) | 170 | return this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers }) |
179 | .map(res => this.handleRefreshToken(res)) | 171 | .map(res => this.handleRefreshToken(res)) |
180 | .catch(res => { | 172 | .catch(err => { |
181 | // The refresh token is invalid? | 173 | console.error(err) |
182 | if (res.status === 400 && res.error.error === 'invalid_grant') { | 174 | console.log('Cannot refresh token -> logout...') |
183 | console.error('Cannot refresh token -> logout...') | 175 | this.logout() |
184 | this.logout() | 176 | this.router.navigate(['/login']) |
185 | this.router.navigate(['/login']) | 177 | |
186 | 178 | return Observable.throw({ | |
187 | return Observable.throw({ | 179 | error: 'You need to reconnect.' |
188 | error: 'You need to reconnect.' | 180 | }) |
189 | }) | ||
190 | } | ||
191 | |||
192 | return this.restExtractor.handleError(res) | ||
193 | }) | 181 | }) |
194 | } | 182 | } |
195 | 183 | ||
@@ -202,7 +190,6 @@ export class AuthService { | |||
202 | } | 190 | } |
203 | 191 | ||
204 | this.mergeUserInformation(obj) | 192 | this.mergeUserInformation(obj) |
205 | .do(() => this.userInformationLoaded.next(true)) | ||
206 | .subscribe( | 193 | .subscribe( |
207 | res => { | 194 | res => { |
208 | this.user.displayNSFW = res.displayNSFW | 195 | this.user.displayNSFW = res.displayNSFW |
@@ -211,6 +198,8 @@ export class AuthService { | |||
211 | this.user.account = res.account | 198 | this.user.account = res.account |
212 | 199 | ||
213 | this.user.save() | 200 | this.user.save() |
201 | |||
202 | this.userInformationLoaded.next(true) | ||
214 | } | 203 | } |
215 | ) | 204 | ) |
216 | } | 205 | } |
diff --git a/client/src/app/core/confirm/confirm.component.html b/client/src/app/core/confirm/confirm.component.html index 2726af6cc..31b735f97 100644 --- a/client/src/app/core/confirm/confirm.component.html +++ b/client/src/app/core/confirm/confirm.component.html | |||
@@ -6,14 +6,14 @@ | |||
6 | <button type="button" class="close" aria-label="Close" (click)="cancel()"> | 6 | <button type="button" class="close" aria-label="Close" (click)="cancel()"> |
7 | <span aria-hidden="true">×</span> | 7 | <span aria-hidden="true">×</span> |
8 | </button> | 8 | </button> |
9 | <h4 class="modal-title">{{ title }}</h4> | 9 | <h4 class="title-page title-page-single">{{ title }}</h4> |
10 | </div> | 10 | </div> |
11 | 11 | ||
12 | <div class="modal-body" [innerHtml]="message"></div> | 12 | <div class="modal-body" [innerHtml]="message"></div> |
13 | 13 | ||
14 | <div class="modal-footer"> | 14 | <div class="modal-footer"> |
15 | <button type="button" class="btn btn-default" data-dismiss="modal" (click)="cancel()">Cancel</button> | 15 | <button type="button" class="grey-button" data-dismiss="modal" (click)="cancel()">Cancel</button> |
16 | <button type="button" class="btn btn-primary" (click)="confirm()">Confirm</button> | 16 | <button type="button" class="orange-button" (click)="confirm()">Confirm</button> |
17 | </div> | 17 | </div> |
18 | </div> | 18 | </div> |
19 | </div> | 19 | </div> |
diff --git a/client/src/app/core/confirm/confirm.component.ts b/client/src/app/core/confirm/confirm.component.ts index c8e41e233..0515d969a 100644 --- a/client/src/app/core/confirm/confirm.component.ts +++ b/client/src/app/core/confirm/confirm.component.ts | |||
@@ -11,7 +11,8 @@ export interface ConfigChangedEvent { | |||
11 | 11 | ||
12 | @Component({ | 12 | @Component({ |
13 | selector: 'my-confirm', | 13 | selector: 'my-confirm', |
14 | templateUrl: './confirm.component.html' | 14 | templateUrl: './confirm.component.html', |
15 | styles: [ '.button { padding: 0 13px; }' ] | ||
15 | }) | 16 | }) |
16 | export class ConfirmComponent implements OnInit { | 17 | export class ConfirmComponent implements OnInit { |
17 | @ViewChild('confirmModal') confirmModal: ModalDirective | 18 | @ViewChild('confirmModal') confirmModal: ModalDirective |
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts index c4ce2b637..75262e6cf 100644 --- a/client/src/app/core/core.module.ts +++ b/client/src/app/core/core.module.ts | |||
@@ -26,17 +26,13 @@ import { throwIfAlreadyLoaded } from './module-import-guard' | |||
26 | ], | 26 | ], |
27 | 27 | ||
28 | declarations: [ | 28 | declarations: [ |
29 | ConfirmComponent, | 29 | ConfirmComponent |
30 | MenuComponent, | ||
31 | MenuAdminComponent | ||
32 | ], | 30 | ], |
33 | 31 | ||
34 | exports: [ | 32 | exports: [ |
35 | SimpleNotificationsModule, | 33 | SimpleNotificationsModule, |
36 | 34 | ||
37 | ConfirmComponent, | 35 | ConfirmComponent |
38 | MenuComponent, | ||
39 | MenuAdminComponent | ||
40 | ], | 36 | ], |
41 | 37 | ||
42 | providers: [ | 38 | providers: [ |
diff --git a/client/src/app/core/index.ts b/client/src/app/core/index.ts index 8358261ae..3c01e05aa 100644 --- a/client/src/app/core/index.ts +++ b/client/src/app/core/index.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | export * from './auth' | 1 | export * from './auth' |
2 | export * from './server' | 2 | export * from './server' |
3 | export * from './confirm' | 3 | export * from './confirm' |
4 | export * from './menu' | ||
5 | export * from './routing' | 4 | export * from './routing' |
6 | export * from './core.module' | 5 | export * from './core.module' |
diff --git a/client/src/app/core/menu/index.ts b/client/src/app/core/menu/index.ts deleted file mode 100644 index c905ed20a..000000000 --- a/client/src/app/core/menu/index.ts +++ /dev/null | |||
@@ -1,2 +0,0 @@ | |||
1 | export * from './menu.component' | ||
2 | export * from './menu-admin.component' | ||
diff --git a/client/src/app/core/menu/menu-admin.component.html b/client/src/app/core/menu/menu-admin.component.html deleted file mode 100644 index 9857b2e3e..000000000 --- a/client/src/app/core/menu/menu-admin.component.html +++ /dev/null | |||
@@ -1,35 +0,0 @@ | |||
1 | <menu> | ||
2 | <div class="panel-block"> | ||
3 | <a *ngIf="hasUsersRight()" routerLink="/admin/users" routerLinkActive="active"> | ||
4 | <span class="hidden-xs glyphicon glyphicon-user"></span> | ||
5 | List users | ||
6 | </a> | ||
7 | |||
8 | <a *ngIf="hasServerFollowRight()" routerLink="/admin/follows" routerLinkActive="active"> | ||
9 | <span class="hidden-xs glyphicon glyphicon-cloud"></span> | ||
10 | Manage follows | ||
11 | </a> | ||
12 | |||
13 | <a *ngIf="hasVideoAbusesRight()" routerLink="/admin/video-abuses" routerLinkActive="active"> | ||
14 | <span class="hidden-xs glyphicon glyphicon-alert"></span> | ||
15 | Video abuses | ||
16 | </a> | ||
17 | |||
18 | <a *ngIf="hasVideoBlacklistRight()" routerLink="/admin/video-blacklist" routerLinkActive="active"> | ||
19 | <span class="hidden-xs glyphicon glyphicon-eye-close"></span> | ||
20 | Video blacklist | ||
21 | </a> | ||
22 | |||
23 | <a *ngIf="hasJobsRight()" routerLink="/admin/jobs" routerLinkActive="active"> | ||
24 | <span class="hidden-xs glyphicon glyphicon-tasks"></span> | ||
25 | Jobs | ||
26 | </a> | ||
27 | </div> | ||
28 | |||
29 | <div class="panel-block"> | ||
30 | <a routerLink="/videos/list" routerLinkActive="active"> | ||
31 | <span class="hidden-xs glyphicon glyphicon-cog"></span> | ||
32 | Quit admin. | ||
33 | </a> | ||
34 | </div> | ||
35 | </menu> | ||
diff --git a/client/src/app/core/menu/menu-admin.component.ts b/client/src/app/core/menu/menu-admin.component.ts deleted file mode 100644 index ea8d5f57c..000000000 --- a/client/src/app/core/menu/menu-admin.component.ts +++ /dev/null | |||
@@ -1,33 +0,0 @@ | |||
1 | import { Component } from '@angular/core' | ||
2 | |||
3 | import { AuthService } from '../auth/auth.service' | ||
4 | import { UserRight } from '../../../../../shared' | ||
5 | |||
6 | @Component({ | ||
7 | selector: 'my-menu-admin', | ||
8 | templateUrl: './menu-admin.component.html', | ||
9 | styleUrls: [ './menu.component.scss' ] | ||
10 | }) | ||
11 | export class MenuAdminComponent { | ||
12 | constructor (private auth: AuthService) {} | ||
13 | |||
14 | hasUsersRight () { | ||
15 | return this.auth.getUser().hasRight(UserRight.MANAGE_USERS) | ||
16 | } | ||
17 | |||
18 | hasServerFollowRight () { | ||
19 | return this.auth.getUser().hasRight(UserRight.MANAGE_SERVER_FOLLOW) | ||
20 | } | ||
21 | |||
22 | hasVideoAbusesRight () { | ||
23 | return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES) | ||
24 | } | ||
25 | |||
26 | hasVideoBlacklistRight () { | ||
27 | return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) | ||
28 | } | ||
29 | |||
30 | hasJobsRight () { | ||
31 | return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS) | ||
32 | } | ||
33 | } | ||
diff --git a/client/src/app/core/menu/menu.component.html b/client/src/app/core/menu/menu.component.html deleted file mode 100644 index fcde23fdd..000000000 --- a/client/src/app/core/menu/menu.component.html +++ /dev/null | |||
@@ -1,55 +0,0 @@ | |||
1 | <menu> | ||
2 | <div class="panel-block"> | ||
3 | <div class="block-title">Account</div> | ||
4 | |||
5 | <div id="panel-user-login" class="panel-button"> | ||
6 | <a *ngIf="!isLoggedIn" routerLink="/login" routerLinkActive="active"> | ||
7 | <span class="hidden-xs glyphicon glyphicon-log-in"></span> | ||
8 | Login | ||
9 | </a> | ||
10 | |||
11 | <a *ngIf="isLoggedIn" (click)="logout()"> | ||
12 | <span class="hidden-xs glyphicon glyphicon-log-out"></span> | ||
13 | Logout | ||
14 | </a> | ||
15 | </div> | ||
16 | |||
17 | <a *ngIf="!isLoggedIn && isRegistrationAllowed()" routerLink="/signup" routerLinkActive="active"> | ||
18 | <span class="hidden-xs glyphicon glyphicon-user"></span> | ||
19 | Signup | ||
20 | </a> | ||
21 | |||
22 | <a *ngIf="isLoggedIn" routerLink="/account" routerLinkActive="active"> | ||
23 | <span class="hidden-xs glyphicon glyphicon-user"></span> | ||
24 | My account | ||
25 | </a> | ||
26 | |||
27 | <a *ngIf="isLoggedIn" routerLink="/videos/mine" routerLinkActive="active"> | ||
28 | <span class="hidden-xs glyphicon glyphicon-folder-open"></span> | ||
29 | My videos | ||
30 | </a> | ||
31 | </div> | ||
32 | |||
33 | <div class="panel-block"> | ||
34 | <div class="block-title">Videos</div> | ||
35 | |||
36 | <a routerLink="/videos/list" routerLinkActive="active"> | ||
37 | <span class="hidden-xs glyphicon glyphicon-list"></span> | ||
38 | See videos | ||
39 | </a> | ||
40 | |||
41 | <a *ngIf="isLoggedIn" routerLink="/videos/upload" routerLinkActive="active"> | ||
42 | <span class="hidden-xs glyphicon glyphicon-cloud-upload"></span> | ||
43 | Upload a video | ||
44 | </a> | ||
45 | </div> | ||
46 | |||
47 | <div *ngIf="userHasAdminAccess" class="panel-block"> | ||
48 | <div class="block-title">Other</div> | ||
49 | |||
50 | <a [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active"> | ||
51 | <span class="hidden-xs glyphicon glyphicon-cog"></span> | ||
52 | Administration | ||
53 | </a> | ||
54 | </div> | ||
55 | </menu> | ||
diff --git a/client/src/app/core/menu/menu.component.scss b/client/src/app/core/menu/menu.component.scss deleted file mode 100644 index 45679c310..000000000 --- a/client/src/app/core/menu/menu.component.scss +++ /dev/null | |||
@@ -1,51 +0,0 @@ | |||
1 | menu { | ||
2 | background-color: $black-background; | ||
3 | padding: 15px; | ||
4 | margin: 0; | ||
5 | height: 100%; | ||
6 | white-space: nowrap; | ||
7 | text-overflow: ellipsis; | ||
8 | overflow: hidden; | ||
9 | z-index: 1000; | ||
10 | |||
11 | @media screen and (max-width: 550px) { | ||
12 | font-size: 90%; | ||
13 | } | ||
14 | |||
15 | @media screen and (min-width: 1200px) { | ||
16 | padding: 25px; | ||
17 | } | ||
18 | |||
19 | .panel-block { | ||
20 | margin-bottom: 15px; | ||
21 | } | ||
22 | |||
23 | .block-title { | ||
24 | text-transform: uppercase; | ||
25 | font-weight: bold; | ||
26 | color: $menu-color-block; | ||
27 | margin-bottom: 10px; | ||
28 | } | ||
29 | |||
30 | a { | ||
31 | display: block; | ||
32 | margin-left: 5px; | ||
33 | height: 30px; | ||
34 | color: $menu-color-link; | ||
35 | cursor: pointer; | ||
36 | transition: color 0.3s; | ||
37 | |||
38 | &:hover, &:focus { | ||
39 | text-decoration: none !important; | ||
40 | outline: none !important; | ||
41 | } | ||
42 | |||
43 | .glyphicon { | ||
44 | margin-right: 15px; | ||
45 | } | ||
46 | |||
47 | &:hover, &.active { | ||
48 | color: #fff; | ||
49 | } | ||
50 | } | ||
51 | } | ||
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index cbc4074c9..16e0595b6 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { HttpClient } from '@angular/common/http' | 1 | import { HttpClient } from '@angular/common/http' |
2 | import { Injectable } from '@angular/core' | ||
3 | import 'rxjs/add/operator/do' | ||
4 | import { ReplaySubject } from 'rxjs/ReplaySubject' | ||
3 | 5 | ||
4 | import { ServerConfig } from '../../../../../shared' | 6 | import { ServerConfig } from '../../../../../shared' |
5 | 7 | ||
@@ -8,6 +10,11 @@ export class ServerService { | |||
8 | private static BASE_CONFIG_URL = API_URL + '/api/v1/config/' | 10 | private static BASE_CONFIG_URL = API_URL + '/api/v1/config/' |
9 | private static BASE_VIDEO_URL = API_URL + '/api/v1/videos/' | 11 | private static BASE_VIDEO_URL = API_URL + '/api/v1/videos/' |
10 | 12 | ||
13 | videoPrivaciesLoaded = new ReplaySubject<boolean>(1) | ||
14 | videoCategoriesLoaded = new ReplaySubject<boolean>(1) | ||
15 | videoLicencesLoaded = new ReplaySubject<boolean>(1) | ||
16 | videoLanguagesLoaded = new ReplaySubject<boolean>(1) | ||
17 | |||
11 | private config: ServerConfig = { | 18 | private config: ServerConfig = { |
12 | signup: { | 19 | signup: { |
13 | allowed: false | 20 | allowed: false |
@@ -29,19 +36,19 @@ export class ServerService { | |||
29 | } | 36 | } |
30 | 37 | ||
31 | loadVideoCategories () { | 38 | loadVideoCategories () { |
32 | return this.loadVideoAttributeEnum('categories', this.videoCategories) | 39 | return this.loadVideoAttributeEnum('categories', this.videoCategories, this.videoCategoriesLoaded) |
33 | } | 40 | } |
34 | 41 | ||
35 | loadVideoLicences () { | 42 | loadVideoLicences () { |
36 | return this.loadVideoAttributeEnum('licences', this.videoLicences) | 43 | return this.loadVideoAttributeEnum('licences', this.videoLicences, this.videoLicencesLoaded) |
37 | } | 44 | } |
38 | 45 | ||
39 | loadVideoLanguages () { | 46 | loadVideoLanguages () { |
40 | return this.loadVideoAttributeEnum('languages', this.videoLanguages) | 47 | return this.loadVideoAttributeEnum('languages', this.videoLanguages, this.videoLanguagesLoaded) |
41 | } | 48 | } |
42 | 49 | ||
43 | loadVideoPrivacies () { | 50 | loadVideoPrivacies () { |
44 | return this.loadVideoAttributeEnum('privacies', this.videoPrivacies) | 51 | return this.loadVideoAttributeEnum('privacies', this.videoPrivacies, this.videoPrivaciesLoaded) |
45 | } | 52 | } |
46 | 53 | ||
47 | getConfig () { | 54 | getConfig () { |
@@ -66,17 +73,20 @@ export class ServerService { | |||
66 | 73 | ||
67 | private loadVideoAttributeEnum ( | 74 | private loadVideoAttributeEnum ( |
68 | attributeName: 'categories' | 'licences' | 'languages' | 'privacies', | 75 | attributeName: 'categories' | 'licences' | 'languages' | 'privacies', |
69 | hashToPopulate: { id: number, label: string }[] | 76 | hashToPopulate: { id: number, label: string }[], |
77 | notifier: ReplaySubject<boolean> | ||
70 | ) { | 78 | ) { |
71 | return this.http.get(ServerService.BASE_VIDEO_URL + attributeName) | 79 | return this.http.get(ServerService.BASE_VIDEO_URL + attributeName) |
72 | .subscribe(data => { | 80 | .subscribe(data => { |
73 | Object.keys(data) | 81 | Object.keys(data) |
74 | .forEach(dataKey => { | 82 | .forEach(dataKey => { |
75 | hashToPopulate.push({ | 83 | hashToPopulate.push({ |
76 | id: parseInt(dataKey, 10), | 84 | id: parseInt(dataKey, 10), |
77 | label: data[dataKey] | 85 | label: data[dataKey] |
78 | }) | 86 | }) |
79 | }) | ||
80 | }) | 87 | }) |
88 | |||
89 | notifier.next(true) | ||
90 | }) | ||
81 | } | 91 | } |
82 | } | 92 | } |
diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html new file mode 100644 index 000000000..c853d2b1b --- /dev/null +++ b/client/src/app/header/header.component.html | |||
@@ -0,0 +1,10 @@ | |||
1 | <input | ||
2 | type="text" id="search-video" name="search-video" placeholder="Search..." | ||
3 | [(ngModel)]="searchValue" (keyup.enter)="doSearch()" | ||
4 | > | ||
5 | <span (click)="doSearch()" class="icon icon-search"></span> | ||
6 | |||
7 | <a class="upload-button" routerLink="/videos/upload"> | ||
8 | <span class="icon icon-upload"></span> | ||
9 | <span class="upload-button-label">Upload</span> | ||
10 | </a> | ||
diff --git a/client/src/app/header/header.component.scss b/client/src/app/header/header.component.scss new file mode 100644 index 000000000..fba70dd2f --- /dev/null +++ b/client/src/app/header/header.component.scss | |||
@@ -0,0 +1,58 @@ | |||
1 | #search-video { | ||
2 | @include peertube-input-text($search-input-width); | ||
3 | margin-right: 15px; | ||
4 | padding-right: 25px; // For the search icon | ||
5 | |||
6 | &::placeholder { | ||
7 | color: #000; | ||
8 | } | ||
9 | |||
10 | @media screen and (max-width: 600px) { | ||
11 | width: calc(100% - 150px); | ||
12 | } | ||
13 | |||
14 | @media screen and (max-width: 400px) { | ||
15 | width: calc(100% - 70px); | ||
16 | } | ||
17 | } | ||
18 | |||
19 | .icon.icon-search { | ||
20 | @include icon(25px); | ||
21 | height: 21px; | ||
22 | |||
23 | background-image: url('../../assets/images/header/search.svg'); | ||
24 | |||
25 | // yolo | ||
26 | position: absolute; | ||
27 | margin-left: -50px; | ||
28 | margin-top: 5px; | ||
29 | } | ||
30 | |||
31 | .upload-button { | ||
32 | @include peertube-button-link; | ||
33 | @include orange-button; | ||
34 | |||
35 | margin-right: 25px; | ||
36 | |||
37 | .icon.icon-upload { | ||
38 | @include icon(22px); | ||
39 | |||
40 | background-image: url('../../assets/images/header/upload.svg'); | ||
41 | height: 24px; | ||
42 | vertical-align: middle; | ||
43 | margin-right: 6px; | ||
44 | } | ||
45 | |||
46 | @media screen and (max-width: 400px) { | ||
47 | margin-right: 10px; | ||
48 | padding: 0 10px; | ||
49 | |||
50 | .icon.icon-upload { | ||
51 | margin-right: 0; | ||
52 | } | ||
53 | |||
54 | .upload-button-label { | ||
55 | display: none; | ||
56 | } | ||
57 | } | ||
58 | } | ||
diff --git a/client/src/app/header/header.component.ts b/client/src/app/header/header.component.ts new file mode 100644 index 000000000..a903048f2 --- /dev/null +++ b/client/src/app/header/header.component.ts | |||
@@ -0,0 +1,28 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { Router } from '@angular/router' | ||
3 | import { getParameterByName } from '../shared/misc/utils' | ||
4 | |||
5 | @Component({ | ||
6 | selector: 'my-header', | ||
7 | templateUrl: './header.component.html', | ||
8 | styleUrls: [ './header.component.scss' ] | ||
9 | }) | ||
10 | |||
11 | export class HeaderComponent implements OnInit { | ||
12 | searchValue = '' | ||
13 | |||
14 | constructor (private router: Router) {} | ||
15 | |||
16 | ngOnInit () { | ||
17 | const searchQuery = getParameterByName('search', window.location.href) | ||
18 | if (searchQuery) this.searchValue = searchQuery | ||
19 | } | ||
20 | |||
21 | doSearch () { | ||
22 | if (!this.searchValue) return | ||
23 | |||
24 | this.router.navigate([ '/videos', 'search' ], { | ||
25 | queryParams: { search: this.searchValue } | ||
26 | }) | ||
27 | } | ||
28 | } | ||
diff --git a/client/src/app/header/index.ts b/client/src/app/header/index.ts new file mode 100644 index 000000000..d98d2d00a --- /dev/null +++ b/client/src/app/header/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './header.component' | |||
diff --git a/client/src/app/login/login.component.html b/client/src/app/login/login.component.html index bcea0a27a..24807987c 100644 --- a/client/src/app/login/login.component.html +++ b/client/src/app/login/login.component.html | |||
@@ -1,34 +1,33 @@ | |||
1 | <div class="row"> | 1 | <div class="margin-content"> |
2 | <div class="content-padding"> | 2 | <div class="title-page title-page-single"> |
3 | 3 | Login | |
4 | <h3>Login</h3> | 4 | </div> |
5 | 5 | ||
6 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | 6 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> |
7 | 7 | ||
8 | <form role="form" (ngSubmit)="login()" [formGroup]="form"> | 8 | <form role="form" (ngSubmit)="login()" [formGroup]="form"> |
9 | <div class="form-group"> | 9 | <div class="form-group"> |
10 | <label for="username">Username</label> | 10 | <label for="username">Username</label> |
11 | <input | 11 | <input |
12 | type="text" class="form-control" id="username" placeholder="Username" required | 12 | type="text" id="username" placeholder="Username" required |
13 | formControlName="username" | 13 | formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }" |
14 | > | 14 | > |
15 | <div *ngIf="formErrors.username" class="alert alert-danger"> | 15 | <div *ngIf="formErrors.username" class="form-error"> |
16 | {{ formErrors.username }} | 16 | {{ formErrors.username }} |
17 | </div> | ||
18 | </div> | 17 | </div> |
18 | </div> | ||
19 | 19 | ||
20 | <div class="form-group"> | 20 | <div class="form-group"> |
21 | <label for="password">Password</label> | 21 | <label for="password">Password</label> |
22 | <input | 22 | <input |
23 | type="password" class="form-control" name="password" id="password" placeholder="Password" required | 23 | type="password" name="password" id="password" placeholder="Password" required |
24 | formControlName="password" | 24 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" |
25 | > | 25 | > |
26 | <div *ngIf="formErrors.password" class="alert alert-danger"> | 26 | <div *ngIf="formErrors.password" class="form-error"> |
27 | {{ formErrors.password }} | 27 | {{ formErrors.password }} |
28 | </div> | ||
29 | </div> | 28 | </div> |
29 | </div> | ||
30 | 30 | ||
31 | <input type="submit" value="Login" class="btn btn-default" [disabled]="!form.valid"> | 31 | <input type="submit" value="Login" [disabled]="!form.valid"> |
32 | </form> | 32 | </form> |
33 | </div> | ||
34 | </div> | 33 | </div> |
diff --git a/client/src/app/login/login.component.scss b/client/src/app/login/login.component.scss new file mode 100644 index 000000000..3b4326de4 --- /dev/null +++ b/client/src/app/login/login.component.scss | |||
@@ -0,0 +1,9 @@ | |||
1 | input:not([type=submit]) { | ||
2 | @include peertube-input-text(340px); | ||
3 | display: block; | ||
4 | } | ||
5 | |||
6 | input[type=submit] { | ||
7 | @include peertube-button; | ||
8 | @include orange-button; | ||
9 | } | ||
diff --git a/client/src/app/login/login.component.ts b/client/src/app/login/login.component.ts index 32dc9e36f..dfede5924 100644 --- a/client/src/app/login/login.component.ts +++ b/client/src/app/login/login.component.ts | |||
@@ -7,7 +7,8 @@ import { FormReactive } from '../shared' | |||
7 | 7 | ||
8 | @Component({ | 8 | @Component({ |
9 | selector: 'my-login', | 9 | selector: 'my-login', |
10 | templateUrl: './login.component.html' | 10 | templateUrl: './login.component.html', |
11 | styleUrls: [ './login.component.scss' ] | ||
11 | }) | 12 | }) |
12 | 13 | ||
13 | export class LoginComponent extends FormReactive implements OnInit { | 14 | export class LoginComponent extends FormReactive implements OnInit { |
diff --git a/client/src/app/menu/index.ts b/client/src/app/menu/index.ts new file mode 100644 index 000000000..421271c12 --- /dev/null +++ b/client/src/app/menu/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './menu.component' | |||
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html new file mode 100644 index 000000000..7a80fa4de --- /dev/null +++ b/client/src/app/menu/menu.component.html | |||
@@ -0,0 +1,50 @@ | |||
1 | <menu> | ||
2 | <div *ngIf="isLoggedIn" class="logged-in-block"> | ||
3 | <img [src]="getUserAvatarPath()" alt="Avatar" /> | ||
4 | |||
5 | <div class="logged-in-info"> | ||
6 | <a routerLink="/account/settings" class="logged-in-username">{{ user.username }}</a> | ||
7 | <div class="logged-in-email">{{ user.email }}</div> | ||
8 | </div> | ||
9 | |||
10 | <div class="logged-in-more" dropdown placement="right" container="body"> | ||
11 | <span class="glyphicon glyphicon-option-vertical" dropdownToggle></span> | ||
12 | |||
13 | <ul *dropdownMenu class="dropdown-menu"> | ||
14 | <li> | ||
15 | <a (click)="logout($event)" class="dropdown-item" title="Log out" href="#"> | ||
16 | Log out | ||
17 | </a> | ||
18 | </li> | ||
19 | </ul> | ||
20 | </div> | ||
21 | </div> | ||
22 | |||
23 | <div *ngIf="!isLoggedIn" class="button-block"> | ||
24 | <a routerLink="/login" class="login-button">Login</a> | ||
25 | <a *ngIf="isRegistrationAllowed()" routerLink="/signup" class="create-account-button">Create an account</a> | ||
26 | </div> | ||
27 | |||
28 | <div class="panel-block"> | ||
29 | <div class="block-title">Videos</div> | ||
30 | |||
31 | <a routerLink="/videos/trending" routerLinkActive="active"> | ||
32 | <span class="icon icon-videos-trending"></span> | ||
33 | Trending | ||
34 | </a> | ||
35 | |||
36 | <a routerLink="/videos/recently-added" routerLinkActive="active"> | ||
37 | <span class="icon icon-videos-recently-added"></span> | ||
38 | Recently added | ||
39 | </a> | ||
40 | </div> | ||
41 | |||
42 | <div *ngIf="userHasAdminAccess" class="panel-block"> | ||
43 | <div class="block-title">More</div> | ||
44 | |||
45 | <a [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active"> | ||
46 | <span class="icon icon-administration"></span> | ||
47 | Administration | ||
48 | </a> | ||
49 | </div> | ||
50 | </menu> | ||
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss new file mode 100644 index 000000000..97ceadde3 --- /dev/null +++ b/client/src/app/menu/menu.component.scss | |||
@@ -0,0 +1,193 @@ | |||
1 | menu { | ||
2 | background-color: $black-background; | ||
3 | margin: 0; | ||
4 | padding: 0; | ||
5 | height: 100%; | ||
6 | white-space: nowrap; | ||
7 | text-overflow: ellipsis; | ||
8 | overflow: hidden; | ||
9 | z-index: 1000; | ||
10 | color: $menu-color; | ||
11 | |||
12 | .logged-in-block { | ||
13 | height: 100px; | ||
14 | background-color: rgba(255, 255, 255, 0.15); | ||
15 | display: flex; | ||
16 | align-items: center; | ||
17 | justify-content: center; | ||
18 | margin-bottom: 35px; | ||
19 | |||
20 | img { | ||
21 | margin-left: 20px; | ||
22 | margin-right: 10px; | ||
23 | |||
24 | @include avatar(34px); | ||
25 | } | ||
26 | |||
27 | .logged-in-info { | ||
28 | flex-grow: 1; | ||
29 | |||
30 | .logged-in-username { | ||
31 | font-size: 16px; | ||
32 | font-weight: $font-semibold; | ||
33 | color: $menu-color; | ||
34 | cursor: pointer; | ||
35 | |||
36 | @include disable-default-a-behaviour; | ||
37 | } | ||
38 | |||
39 | .logged-in-email { | ||
40 | font-size: 13px; | ||
41 | color: #C6C6C6; | ||
42 | white-space: nowrap; | ||
43 | overflow: hidden; | ||
44 | text-overflow: ellipsis; | ||
45 | max-width: 140px; | ||
46 | } | ||
47 | } | ||
48 | |||
49 | .logged-in-more { | ||
50 | margin-right: 20px; | ||
51 | |||
52 | .glyphicon { | ||
53 | cursor: pointer; | ||
54 | font-size: 18px; | ||
55 | } | ||
56 | } | ||
57 | } | ||
58 | |||
59 | .button-block { | ||
60 | margin: 30px 25px 35px 25px; | ||
61 | |||
62 | .login-button, .create-account-button { | ||
63 | font-weight: $font-semibold; | ||
64 | font-size: 15px; | ||
65 | height: $button-height; | ||
66 | line-height: $button-height; | ||
67 | width: 100%; | ||
68 | border-radius: 3px; | ||
69 | text-align: center; | ||
70 | color: $menu-color; | ||
71 | display: block; | ||
72 | cursor: pointer; | ||
73 | margin-bottom: 15px; | ||
74 | |||
75 | @include disable-default-a-behaviour; | ||
76 | |||
77 | &.login-button { | ||
78 | background-color: $orange-color; | ||
79 | margin-bottom: 10px; | ||
80 | } | ||
81 | |||
82 | &.create-account-button { | ||
83 | background-color: rgba(255, 255, 255, 0.25); | ||
84 | } | ||
85 | } | ||
86 | } | ||
87 | |||
88 | .block-title { | ||
89 | text-transform: uppercase; | ||
90 | font-weight: $font-bold; // Bold | ||
91 | font-size: 13px; | ||
92 | margin-bottom: 25px; | ||
93 | } | ||
94 | |||
95 | .panel-block { | ||
96 | margin-bottom: 45px; | ||
97 | margin-left: 26px; | ||
98 | |||
99 | a { | ||
100 | display: flex; | ||
101 | color: $menu-color; | ||
102 | cursor: pointer; | ||
103 | height: 22px; | ||
104 | line-height: 22px; | ||
105 | font-size: 16px; | ||
106 | margin-bottom: 15px; | ||
107 | @include disable-default-a-behaviour; | ||
108 | |||
109 | .icon { | ||
110 | @include icon(22px); | ||
111 | |||
112 | margin-right: 18px; | ||
113 | |||
114 | &.icon-videos-trending { | ||
115 | position: relative; | ||
116 | top: -2px; | ||
117 | background-image: url('../../assets/images/menu/trending.svg'); | ||
118 | } | ||
119 | |||
120 | &.icon-videos-recently-added { | ||
121 | width: 23px; | ||
122 | height: 23px; | ||
123 | position: relative; | ||
124 | top: -1px; | ||
125 | background-image: url('../../assets/images/menu/recently-added.svg'); | ||
126 | } | ||
127 | |||
128 | &.icon-administration { | ||
129 | width: 23px; | ||
130 | height: 23px; | ||
131 | |||
132 | background-image: url('../../assets/images/menu/administration.svg'); | ||
133 | } | ||
134 | } | ||
135 | } | ||
136 | } | ||
137 | } | ||
138 | |||
139 | @media screen and (max-width: 800px) { | ||
140 | menu { | ||
141 | .logged-in-block { | ||
142 | padding-left: 10px; | ||
143 | |||
144 | img { | ||
145 | display: none; | ||
146 | } | ||
147 | |||
148 | .logged-in-info { | ||
149 | .logged-in-username { | ||
150 | font-size: 14px; | ||
151 | } | ||
152 | |||
153 | .logged-in-email { | ||
154 | font-size: 11px; | ||
155 | max-width: 120px; | ||
156 | } | ||
157 | } | ||
158 | |||
159 | .logged-in-more { | ||
160 | margin-right: 5px; | ||
161 | |||
162 | .login-button, .create-account-button { | ||
163 | font-weight: $font-semibold; | ||
164 | font-size: 15px; | ||
165 | height: $button-height; | ||
166 | line-height: $button-height; | ||
167 | width: 190px; | ||
168 | } | ||
169 | } | ||
170 | } | ||
171 | |||
172 | .button-block { | ||
173 | margin: 20px 10px 25px 10px; | ||
174 | |||
175 | .login-button, .create-account-button { | ||
176 | font-size: 13px; | ||
177 | } | ||
178 | } | ||
179 | |||
180 | .panel-block { | ||
181 | margin-bottom: 30px; | ||
182 | margin-left: 10px; | ||
183 | |||
184 | a { | ||
185 | font-size: 14px; | ||
186 | |||
187 | .icon { | ||
188 | margin-right: 10px; | ||
189 | } | ||
190 | } | ||
191 | } | ||
192 | } | ||
193 | } | ||
diff --git a/client/src/app/core/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index d2bd71534..8b8b714a8 100644 --- a/client/src/app/core/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts | |||
@@ -1,9 +1,8 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | 3 | import { UserRight } from '../../../../shared/models/users/user-right.enum' | |
4 | import { AuthService, AuthStatus } from '../auth' | 4 | import { AuthService, AuthStatus, ServerService } from '../core' |
5 | import { ServerService } from '../server' | 5 | import { User } from '../shared/users/user.model' |
6 | import { UserRight } from '../../../../../shared/models/users/user-right.enum' | ||
7 | 6 | ||
8 | @Component({ | 7 | @Component({ |
9 | selector: 'my-menu', | 8 | selector: 'my-menu', |
@@ -11,6 +10,7 @@ import { UserRight } from '../../../../../shared/models/users/user-right.enum' | |||
11 | styleUrls: [ './menu.component.scss' ] | 10 | styleUrls: [ './menu.component.scss' ] |
12 | }) | 11 | }) |
13 | export class MenuComponent implements OnInit { | 12 | export class MenuComponent implements OnInit { |
13 | user: User | ||
14 | isLoggedIn: boolean | 14 | isLoggedIn: boolean |
15 | userHasAdminAccess = false | 15 | userHasAdminAccess = false |
16 | 16 | ||
@@ -29,16 +29,19 @@ export class MenuComponent implements OnInit { | |||
29 | 29 | ||
30 | ngOnInit () { | 30 | ngOnInit () { |
31 | this.isLoggedIn = this.authService.isLoggedIn() | 31 | this.isLoggedIn = this.authService.isLoggedIn() |
32 | if (this.isLoggedIn === true) this.user = this.authService.getUser() | ||
32 | this.computeIsUserHasAdminAccess() | 33 | this.computeIsUserHasAdminAccess() |
33 | 34 | ||
34 | this.authService.loginChangedSource.subscribe( | 35 | this.authService.loginChangedSource.subscribe( |
35 | status => { | 36 | status => { |
36 | if (status === AuthStatus.LoggedIn) { | 37 | if (status === AuthStatus.LoggedIn) { |
37 | this.isLoggedIn = true | 38 | this.isLoggedIn = true |
39 | this.user = this.authService.getUser() | ||
38 | this.computeIsUserHasAdminAccess() | 40 | this.computeIsUserHasAdminAccess() |
39 | console.log('Logged in.') | 41 | console.log('Logged in.') |
40 | } else if (status === AuthStatus.LoggedOut) { | 42 | } else if (status === AuthStatus.LoggedOut) { |
41 | this.isLoggedIn = false | 43 | this.isLoggedIn = false |
44 | this.user = undefined | ||
42 | this.computeIsUserHasAdminAccess() | 45 | this.computeIsUserHasAdminAccess() |
43 | console.log('Logged out.') | 46 | console.log('Logged out.') |
44 | } else { | 47 | } else { |
@@ -48,6 +51,10 @@ export class MenuComponent implements OnInit { | |||
48 | ) | 51 | ) |
49 | } | 52 | } |
50 | 53 | ||
54 | getUserAvatarPath () { | ||
55 | return this.user.getAvatarPath() | ||
56 | } | ||
57 | |||
51 | isRegistrationAllowed () { | 58 | isRegistrationAllowed () { |
52 | return this.serverService.getConfig().signup.allowed | 59 | return this.serverService.getConfig().signup.allowed |
53 | } | 60 | } |
@@ -78,7 +85,9 @@ export class MenuComponent implements OnInit { | |||
78 | return this.routesPerRight[right] | 85 | return this.routesPerRight[right] |
79 | } | 86 | } |
80 | 87 | ||
81 | logout () { | 88 | logout (event: Event) { |
89 | event.preventDefault() | ||
90 | |||
82 | this.authService.logout() | 91 | this.authService.logout() |
83 | // Redirect to home page | 92 | // Redirect to home page |
84 | this.router.navigate(['/videos/list']) | 93 | this.router.navigate(['/videos/list']) |
diff --git a/client/src/app/shared/account/account.model.ts b/client/src/app/shared/account/account.model.ts new file mode 100644 index 000000000..0b008188a --- /dev/null +++ b/client/src/app/shared/account/account.model.ts | |||
@@ -0,0 +1,20 @@ | |||
1 | import { Account as ServerAccount } from '../../../../../shared/models/accounts/account.model' | ||
2 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' | ||
3 | |||
4 | export class Account implements ServerAccount { | ||
5 | id: number | ||
6 | uuid: string | ||
7 | name: string | ||
8 | host: string | ||
9 | followingCount: number | ||
10 | followersCount: number | ||
11 | createdAt: Date | ||
12 | updatedAt: Date | ||
13 | avatar: Avatar | ||
14 | |||
15 | static GET_ACCOUNT_AVATAR_PATH (account: Account) { | ||
16 | if (account && account.avatar) return account.avatar.path | ||
17 | |||
18 | return API_URL + '/client/assets/images/default-avatar.png' | ||
19 | } | ||
20 | } | ||
diff --git a/client/src/app/shared/forms/form-validators/host.validator.ts b/client/src/app/shared/forms/form-validators/host.validator.ts index 03e810fdb..c18a35f9b 100644 --- a/client/src/app/shared/forms/form-validators/host.validator.ts +++ b/client/src/app/shared/forms/form-validators/host.validator.ts | |||
@@ -1,14 +1,8 @@ | |||
1 | import { FormControl } from '@angular/forms' | 1 | export function validateHost (value: string) { |
2 | |||
3 | export function validateHost (c: FormControl) { | ||
4 | // Thanks to http://stackoverflow.com/a/106223 | 2 | // Thanks to http://stackoverflow.com/a/106223 |
5 | const HOST_REGEXP = new RegExp( | 3 | const HOST_REGEXP = new RegExp( |
6 | '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$' | 4 | '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$' |
7 | ) | 5 | ) |
8 | 6 | ||
9 | return HOST_REGEXP.test(c.value) ? null : { | 7 | return HOST_REGEXP.test(value) |
10 | validateHost: { | ||
11 | valid: false | ||
12 | } | ||
13 | } | ||
14 | } | 8 | } |
diff --git a/client/src/app/shared/forms/form-validators/video-abuse.ts b/client/src/app/shared/forms/form-validators/video-abuse.ts index 3c7f26205..4b2a2b789 100644 --- a/client/src/app/shared/forms/form-validators/video-abuse.ts +++ b/client/src/app/shared/forms/form-validators/video-abuse.ts | |||
@@ -3,8 +3,8 @@ import { Validators } from '@angular/forms' | |||
3 | export const VIDEO_ABUSE_REASON = { | 3 | export const VIDEO_ABUSE_REASON = { |
4 | VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ], | 4 | VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ], |
5 | MESSAGES: { | 5 | MESSAGES: { |
6 | 'required': 'Report reason name is required.', | 6 | 'required': 'Report reason is required.', |
7 | 'minlength': 'Report reson must be at least 2 characters long.', | 7 | 'minlength': 'Report reason must be at least 2 characters long.', |
8 | 'maxlength': 'Report reson cannot be more than 300 characters long.' | 8 | 'maxlength': 'Report reason cannot be more than 300 characters long.' |
9 | } | 9 | } |
10 | } | 10 | } |
diff --git a/client/src/app/shared/forms/form-validators/video.ts b/client/src/app/shared/forms/form-validators/video.ts index 65f11f5da..45da7df4a 100644 --- a/client/src/app/shared/forms/form-validators/video.ts +++ b/client/src/app/shared/forms/form-validators/video.ts | |||
@@ -1,5 +1,11 @@ | |||
1 | import { Validators } from '@angular/forms' | 1 | import { Validators } from '@angular/forms' |
2 | 2 | ||
3 | export type ValidatorMessage = { | ||
4 | [ id: string ]: { | ||
5 | [ error: string ]: string | ||
6 | } | ||
7 | } | ||
8 | |||
3 | export const VIDEO_NAME = { | 9 | export const VIDEO_NAME = { |
4 | VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(120) ], | 10 | VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(120) ], |
5 | MESSAGES: { | 11 | MESSAGES: { |
@@ -17,17 +23,13 @@ export const VIDEO_PRIVACY = { | |||
17 | } | 23 | } |
18 | 24 | ||
19 | export const VIDEO_CATEGORY = { | 25 | export const VIDEO_CATEGORY = { |
20 | VALIDATORS: [ Validators.required ], | 26 | VALIDATORS: [ ], |
21 | MESSAGES: { | 27 | MESSAGES: {} |
22 | 'required': 'Video category is required.' | ||
23 | } | ||
24 | } | 28 | } |
25 | 29 | ||
26 | export const VIDEO_LICENCE = { | 30 | export const VIDEO_LICENCE = { |
27 | VALIDATORS: [ Validators.required ], | 31 | VALIDATORS: [ ], |
28 | MESSAGES: { | 32 | MESSAGES: {} |
29 | 'required': 'Video licence is required.' | ||
30 | } | ||
31 | } | 33 | } |
32 | 34 | ||
33 | export const VIDEO_LANGUAGE = { | 35 | export const VIDEO_LANGUAGE = { |
@@ -43,9 +45,8 @@ export const VIDEO_CHANNEL = { | |||
43 | } | 45 | } |
44 | 46 | ||
45 | export const VIDEO_DESCRIPTION = { | 47 | export const VIDEO_DESCRIPTION = { |
46 | VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(3000) ], | 48 | VALIDATORS: [ Validators.minLength(3), Validators.maxLength(3000) ], |
47 | MESSAGES: { | 49 | MESSAGES: { |
48 | 'required': 'Video description is required.', | ||
49 | 'minlength': 'Video description must be at least 3 characters long.', | 50 | 'minlength': 'Video description must be at least 3 characters long.', |
50 | 'maxlength': 'Video description cannot be more than 3000 characters long.' | 51 | 'maxlength': 'Video description cannot be more than 3000 characters long.' |
51 | } | 52 | } |
@@ -58,10 +59,3 @@ export const VIDEO_TAGS = { | |||
58 | 'maxlength': 'A tag should be less than 30 characters long.' | 59 | 'maxlength': 'A tag should be less than 30 characters long.' |
59 | } | 60 | } |
60 | } | 61 | } |
61 | |||
62 | export const VIDEO_FILE = { | ||
63 | VALIDATORS: [ Validators.required ], | ||
64 | MESSAGES: { | ||
65 | 'required': 'Video file is required.' | ||
66 | } | ||
67 | } | ||
diff --git a/client/src/app/shared/index.ts b/client/src/app/shared/index.ts index 79bf5ef43..413dda16a 100644 --- a/client/src/app/shared/index.ts +++ b/client/src/app/shared/index.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | export * from './auth' | 1 | export * from './auth' |
2 | export * from './forms' | 2 | export * from './forms' |
3 | export * from './rest' | 3 | export * from './rest' |
4 | export * from './search' | ||
5 | export * from './users' | 4 | export * from './users' |
6 | export * from './video-abuse' | 5 | export * from './video-abuse' |
7 | export * from './video-blacklist' | 6 | export * from './video-blacklist' |
diff --git a/client/src/app/shared/misc/button.component.scss b/client/src/app/shared/misc/button.component.scss new file mode 100644 index 000000000..5fcae4f10 --- /dev/null +++ b/client/src/app/shared/misc/button.component.scss | |||
@@ -0,0 +1,27 @@ | |||
1 | .action-button { | ||
2 | @include peertube-button-link; | ||
3 | |||
4 | font-size: 15px; | ||
5 | font-weight: $font-semibold; | ||
6 | color: #585858; | ||
7 | background-color: #E5E5E5; | ||
8 | |||
9 | &:hover { | ||
10 | background-color: #EFEFEF; | ||
11 | } | ||
12 | |||
13 | .icon { | ||
14 | @include icon(21px); | ||
15 | |||
16 | position: relative; | ||
17 | top: -2px; | ||
18 | |||
19 | &.icon-edit { | ||
20 | background-image: url('../../../assets/images/global/edit.svg'); | ||
21 | } | ||
22 | |||
23 | &.icon-delete-grey { | ||
24 | background-image: url('../../../assets/images/global/delete-grey.svg'); | ||
25 | } | ||
26 | } | ||
27 | } | ||
diff --git a/client/src/app/shared/misc/delete-button.component.html b/client/src/app/shared/misc/delete-button.component.html new file mode 100644 index 000000000..3db483882 --- /dev/null +++ b/client/src/app/shared/misc/delete-button.component.html | |||
@@ -0,0 +1,4 @@ | |||
1 | <span class="action-button action-button-delete" > | ||
2 | <span class="icon icon-delete-grey"></span> | ||
3 | Delete | ||
4 | </span> | ||
diff --git a/client/src/app/shared/misc/delete-button.component.ts b/client/src/app/shared/misc/delete-button.component.ts new file mode 100644 index 000000000..e04039f69 --- /dev/null +++ b/client/src/app/shared/misc/delete-button.component.ts | |||
@@ -0,0 +1,10 @@ | |||
1 | import { Component } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-delete-button', | ||
5 | styleUrls: [ './button.component.scss' ], | ||
6 | templateUrl: './delete-button.component.html' | ||
7 | }) | ||
8 | |||
9 | export class DeleteButtonComponent { | ||
10 | } | ||
diff --git a/client/src/app/shared/misc/edit-button.component.html b/client/src/app/shared/misc/edit-button.component.html new file mode 100644 index 000000000..6e9564bd7 --- /dev/null +++ b/client/src/app/shared/misc/edit-button.component.html | |||
@@ -0,0 +1,4 @@ | |||
1 | <a class="action-button" [routerLink]="routerLink"> | ||
2 | <span class="icon icon-edit"></span> | ||
3 | Edit | ||
4 | </a> | ||
diff --git a/client/src/app/shared/misc/edit-button.component.ts b/client/src/app/shared/misc/edit-button.component.ts new file mode 100644 index 000000000..201a618ec --- /dev/null +++ b/client/src/app/shared/misc/edit-button.component.ts | |||
@@ -0,0 +1,11 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-edit-button', | ||
5 | styleUrls: [ './button.component.scss' ], | ||
6 | templateUrl: './edit-button.component.html' | ||
7 | }) | ||
8 | |||
9 | export class EditButtonComponent { | ||
10 | @Input() routerLink = [] | ||
11 | } | ||
diff --git a/client/src/app/shared/misc/from-now.pipe.ts b/client/src/app/shared/misc/from-now.pipe.ts new file mode 100644 index 000000000..fac02af0b --- /dev/null +++ b/client/src/app/shared/misc/from-now.pipe.ts | |||
@@ -0,0 +1,36 @@ | |||
1 | import { Pipe, PipeTransform } from '@angular/core' | ||
2 | |||
3 | // Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site | ||
4 | @Pipe({ name: 'myFromNow' }) | ||
5 | export class FromNowPipe implements PipeTransform { | ||
6 | |||
7 | transform (value: number) { | ||
8 | const seconds = Math.floor((Date.now() - value) / 1000) | ||
9 | |||
10 | let interval = Math.floor(seconds / 31536000) | ||
11 | if (interval > 1) { | ||
12 | return interval + ' years ago' | ||
13 | } | ||
14 | |||
15 | interval = Math.floor(seconds / 2592000) | ||
16 | if (interval > 1) return interval + ' months ago' | ||
17 | if (interval === 1) return interval + ' month ago' | ||
18 | |||
19 | interval = Math.floor(seconds / 604800) | ||
20 | if (interval > 1) return interval + ' weeks ago' | ||
21 | if (interval === 1) return interval + ' week ago' | ||
22 | |||
23 | interval = Math.floor(seconds / 86400) | ||
24 | if (interval > 1) return interval + ' days ago' | ||
25 | if (interval === 1) return interval + ' day ago' | ||
26 | |||
27 | interval = Math.floor(seconds / 3600) | ||
28 | if (interval > 1) return interval + ' hours ago' | ||
29 | if (interval === 1) return interval + ' hour ago' | ||
30 | |||
31 | interval = Math.floor(seconds / 60) | ||
32 | if (interval >= 1) return interval + ' min ago' | ||
33 | |||
34 | return Math.floor(seconds) + ' sec ago' | ||
35 | } | ||
36 | } | ||
diff --git a/client/src/app/shared/misc/number-formatter.pipe.ts b/client/src/app/shared/misc/number-formatter.pipe.ts new file mode 100644 index 000000000..8a0756a36 --- /dev/null +++ b/client/src/app/shared/misc/number-formatter.pipe.ts | |||
@@ -0,0 +1,19 @@ | |||
1 | import { Pipe, PipeTransform } from '@angular/core' | ||
2 | |||
3 | // Thanks: https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts | ||
4 | |||
5 | @Pipe({ name: 'myNumberFormatter' }) | ||
6 | export class NumberFormatterPipe implements PipeTransform { | ||
7 | private dictionary: Array<{max: number, type: string}> = [ | ||
8 | { max: 1000, type: '' }, | ||
9 | { max: 1000000, type: 'K' }, | ||
10 | { max: 1000000000, type: 'M' } | ||
11 | ] | ||
12 | |||
13 | transform (value: number) { | ||
14 | const format = this.dictionary.find(d => value < d.max) || this.dictionary[this.dictionary.length - 1] | ||
15 | const calc = Math.floor(value / (format.max / 1000)) | ||
16 | |||
17 | return `${calc}${format.type}` | ||
18 | } | ||
19 | } | ||
diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts new file mode 100644 index 000000000..df9e0381a --- /dev/null +++ b/client/src/app/shared/misc/utils.ts | |||
@@ -0,0 +1,23 @@ | |||
1 | // Thanks: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript | ||
2 | |||
3 | function getParameterByName (name: string, url: string) { | ||
4 | if (!url) url = window.location.href | ||
5 | name = name.replace(/[\[\]]/g, '\\$&') | ||
6 | |||
7 | const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)') | ||
8 | const results = regex.exec(url) | ||
9 | |||
10 | if (!results) return null | ||
11 | if (!results[2]) return '' | ||
12 | |||
13 | return decodeURIComponent(results[2].replace(/\+/g, ' ')) | ||
14 | } | ||
15 | |||
16 | function viewportHeight () { | ||
17 | return Math.max(document.documentElement.clientHeight, window.innerHeight || 0) | ||
18 | } | ||
19 | |||
20 | export { | ||
21 | viewportHeight, | ||
22 | getParameterByName | ||
23 | } | ||
diff --git a/client/src/app/shared/search/index.ts b/client/src/app/shared/search/index.ts deleted file mode 100644 index d4016cf89..000000000 --- a/client/src/app/shared/search/index.ts +++ /dev/null | |||
@@ -1,4 +0,0 @@ | |||
1 | export * from './search-field.type' | ||
2 | export * from './search.component' | ||
3 | export * from './search.model' | ||
4 | export * from './search.service' | ||
diff --git a/client/src/app/shared/search/search-field.type.ts b/client/src/app/shared/search/search-field.type.ts deleted file mode 100644 index 7323d6cc3..000000000 --- a/client/src/app/shared/search/search-field.type.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export type SearchField = 'name' | 'account' | 'host' | 'tags' | ||
diff --git a/client/src/app/shared/search/search.component.html b/client/src/app/shared/search/search.component.html deleted file mode 100644 index 75e9dfa59..000000000 --- a/client/src/app/shared/search/search.component.html +++ /dev/null | |||
@@ -1,22 +0,0 @@ | |||
1 | <div class="input-group"> | ||
2 | |||
3 | <span class="hidden-xs input-group-addon icon-addon"> | ||
4 | <span class="glyphicon glyphicon-search"></span> | ||
5 | </span> | ||
6 | |||
7 | <input | ||
8 | type="text" id="search-video" name="search-video" class="form-control" placeholder="Search" class="form-control" | ||
9 | [(ngModel)]="searchCriteria.value" (keyup.enter)="doSearch()" | ||
10 | > | ||
11 | |||
12 | <div class="input-group-btn" dropdown placement="bottom right"> | ||
13 | <button id="simple-btn-keyboard-nav" type="button" class="btn btn-default" dropdownToggle> | ||
14 | {{ getStringChoice(searchCriteria.field) }} <span class="caret"></span> | ||
15 | </button> | ||
16 | <ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="simple-btn-keyboard-nav" *dropdownMenu> | ||
17 | <li *ngFor="let choice of choiceKeys" class="dropdown-item" role="menu-item"> | ||
18 | <a class="dropdown-item" href="#" (click)="choose($event, choice)">{{ getStringChoice(choice) }}</a> | ||
19 | </li> | ||
20 | </ul> | ||
21 | </div> | ||
22 | </div> | ||
diff --git a/client/src/app/shared/search/search.component.scss b/client/src/app/shared/search/search.component.scss deleted file mode 100644 index 583f9586f..000000000 --- a/client/src/app/shared/search/search.component.scss +++ /dev/null | |||
@@ -1,51 +0,0 @@ | |||
1 | .icon-addon { | ||
2 | background-color: #fff; | ||
3 | border-radius: 0; | ||
4 | border-color: $header-border-color; | ||
5 | border-width: 0 0 1px 0; | ||
6 | text-align: right; | ||
7 | |||
8 | .glyphicon-search { | ||
9 | width: 30px; | ||
10 | font-size: 20px; | ||
11 | } | ||
12 | } | ||
13 | |||
14 | input, button, .input-group { | ||
15 | height: 100%; | ||
16 | } | ||
17 | |||
18 | input, .input-group-btn { | ||
19 | border-radius: 0; | ||
20 | border-top: none; | ||
21 | border-left: none; | ||
22 | } | ||
23 | |||
24 | input { | ||
25 | height: $header-height; | ||
26 | border-right: none; | ||
27 | font-weight: bold; | ||
28 | box-shadow: none; | ||
29 | |||
30 | &, &:focus { | ||
31 | border-bottom: 1px solid $header-border-color !important; | ||
32 | outline: none !important; | ||
33 | box-shadow: none !important; | ||
34 | } | ||
35 | } | ||
36 | |||
37 | button { | ||
38 | |||
39 | &, &:hover, &:focus, &:active, &:visited { | ||
40 | background-color: #fff !important; | ||
41 | border-color: $header-border-color !important; | ||
42 | color: #858585 !important; | ||
43 | outline: none !important; | ||
44 | |||
45 | height: $header-height; | ||
46 | border-width: 0 0 1px 0; | ||
47 | font-weight: bold; | ||
48 | text-decoration: none; | ||
49 | box-shadow: none; | ||
50 | } | ||
51 | } | ||
diff --git a/client/src/app/shared/search/search.component.ts b/client/src/app/shared/search/search.component.ts deleted file mode 100644 index 6ef19c97a..000000000 --- a/client/src/app/shared/search/search.component.ts +++ /dev/null | |||
@@ -1,69 +0,0 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { Router } from '@angular/router' | ||
3 | |||
4 | import { Search } from './search.model' | ||
5 | import { SearchField } from './search-field.type' | ||
6 | import { SearchService } from './search.service' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-search', | ||
10 | templateUrl: './search.component.html', | ||
11 | styleUrls: [ './search.component.scss' ] | ||
12 | }) | ||
13 | |||
14 | export class SearchComponent implements OnInit { | ||
15 | fieldChoices = { | ||
16 | name: 'Name', | ||
17 | account: 'Account', | ||
18 | host: 'Host', | ||
19 | tags: 'Tags' | ||
20 | } | ||
21 | searchCriteria: Search = { | ||
22 | field: 'name', | ||
23 | value: '' | ||
24 | } | ||
25 | |||
26 | constructor (private searchService: SearchService, private router: Router) {} | ||
27 | |||
28 | ngOnInit () { | ||
29 | // Subscribe if the search changed | ||
30 | // Usually changed by videos list component | ||
31 | this.searchService.updateSearch.subscribe( | ||
32 | newSearchCriteria => { | ||
33 | // Put a field by default | ||
34 | if (!newSearchCriteria.field) { | ||
35 | newSearchCriteria.field = 'name' | ||
36 | } | ||
37 | |||
38 | this.searchCriteria = newSearchCriteria | ||
39 | } | ||
40 | ) | ||
41 | } | ||
42 | |||
43 | get choiceKeys () { | ||
44 | return Object.keys(this.fieldChoices) | ||
45 | } | ||
46 | |||
47 | choose ($event: MouseEvent, choice: SearchField) { | ||
48 | $event.preventDefault() | ||
49 | $event.stopPropagation() | ||
50 | |||
51 | this.searchCriteria.field = choice | ||
52 | |||
53 | if (this.searchCriteria.value) { | ||
54 | this.doSearch() | ||
55 | } | ||
56 | } | ||
57 | |||
58 | doSearch () { | ||
59 | if (this.router.url.indexOf('/videos/list') === -1) { | ||
60 | this.router.navigate([ '/videos/list' ]) | ||
61 | } | ||
62 | |||
63 | this.searchService.searchUpdated.next(this.searchCriteria) | ||
64 | } | ||
65 | |||
66 | getStringChoice (choiceKey: SearchField) { | ||
67 | return this.fieldChoices[choiceKey] | ||
68 | } | ||
69 | } | ||
diff --git a/client/src/app/shared/search/search.model.ts b/client/src/app/shared/search/search.model.ts deleted file mode 100644 index 174adf2c6..000000000 --- a/client/src/app/shared/search/search.model.ts +++ /dev/null | |||
@@ -1,6 +0,0 @@ | |||
1 | import { SearchField } from './search-field.type' | ||
2 | |||
3 | export interface Search { | ||
4 | field: SearchField | ||
5 | value: string | ||
6 | } | ||
diff --git a/client/src/app/shared/search/search.service.ts b/client/src/app/shared/search/search.service.ts deleted file mode 100644 index 0480b46bd..000000000 --- a/client/src/app/shared/search/search.service.ts +++ /dev/null | |||
@@ -1,18 +0,0 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { Subject } from 'rxjs/Subject' | ||
3 | import { ReplaySubject } from 'rxjs/ReplaySubject' | ||
4 | |||
5 | import { Search } from './search.model' | ||
6 | |||
7 | // This class is needed to communicate between videos/ and search component | ||
8 | // Remove it when we'll be able to subscribe to router changes | ||
9 | @Injectable() | ||
10 | export class SearchService { | ||
11 | searchUpdated: Subject<Search> | ||
12 | updateSearch: Subject<Search> | ||
13 | |||
14 | constructor () { | ||
15 | this.updateSearch = new Subject<Search>() | ||
16 | this.searchUpdated = new ReplaySubject<Search>(1) | ||
17 | } | ||
18 | } | ||
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 456ce851e..d0e163f69 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts | |||
@@ -1,25 +1,29 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { HttpClientModule } from '@angular/common/http' | ||
3 | import { CommonModule } from '@angular/common' | 1 | import { CommonModule } from '@angular/common' |
2 | import { HttpClientModule } from '@angular/common/http' | ||
3 | import { NgModule } from '@angular/core' | ||
4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms' | 4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms' |
5 | import { RouterModule } from '@angular/router' | 5 | import { RouterModule } from '@angular/router' |
6 | 6 | ||
7 | import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe' | ||
8 | import { KeysPipe } from 'angular-pipes/src/object/keys.pipe' | ||
9 | import { BsDropdownModule } from 'ngx-bootstrap/dropdown' | 7 | import { BsDropdownModule } from 'ngx-bootstrap/dropdown' |
10 | import { ProgressbarModule } from 'ngx-bootstrap/progressbar' | ||
11 | import { PaginationModule } from 'ngx-bootstrap/pagination' | ||
12 | import { ModalModule } from 'ngx-bootstrap/modal' | 8 | import { ModalModule } from 'ngx-bootstrap/modal' |
13 | import { DataTableModule } from 'primeng/components/datatable/datatable' | 9 | import { InfiniteScrollModule } from 'ngx-infinite-scroll' |
10 | import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' | ||
14 | import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' | 11 | import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' |
12 | import { DataTableModule } from 'primeng/components/datatable/datatable' | ||
15 | 13 | ||
16 | import { AUTH_INTERCEPTOR_PROVIDER } from './auth' | 14 | import { AUTH_INTERCEPTOR_PROVIDER } from './auth' |
15 | import { DeleteButtonComponent } from './misc/delete-button.component' | ||
16 | import { EditButtonComponent } from './misc/edit-button.component' | ||
17 | import { FromNowPipe } from './misc/from-now.pipe' | ||
18 | import { LoaderComponent } from './misc/loader.component' | ||
19 | import { NumberFormatterPipe } from './misc/number-formatter.pipe' | ||
17 | import { RestExtractor, RestService } from './rest' | 20 | import { RestExtractor, RestService } from './rest' |
18 | import { SearchComponent, SearchService } from './search' | ||
19 | import { UserService } from './users' | 21 | import { UserService } from './users' |
20 | import { VideoAbuseService } from './video-abuse' | 22 | import { VideoAbuseService } from './video-abuse' |
21 | import { VideoBlacklistService } from './video-blacklist' | 23 | import { VideoBlacklistService } from './video-blacklist' |
22 | import { LoaderComponent } from './misc/loader.component' | 24 | import { VideoMiniatureComponent } from './video/video-miniature.component' |
25 | import { VideoThumbnailComponent } from './video/video-thumbnail.component' | ||
26 | import { VideoService } from './video/video.service' | ||
23 | 27 | ||
24 | @NgModule({ | 28 | @NgModule({ |
25 | imports: [ | 29 | imports: [ |
@@ -31,18 +35,21 @@ import { LoaderComponent } from './misc/loader.component' | |||
31 | 35 | ||
32 | BsDropdownModule.forRoot(), | 36 | BsDropdownModule.forRoot(), |
33 | ModalModule.forRoot(), | 37 | ModalModule.forRoot(), |
34 | PaginationModule.forRoot(), | ||
35 | ProgressbarModule.forRoot(), | ||
36 | 38 | ||
37 | DataTableModule, | 39 | DataTableModule, |
38 | PrimeSharedModule | 40 | PrimeSharedModule, |
41 | InfiniteScrollModule, | ||
42 | NgPipesModule | ||
39 | ], | 43 | ], |
40 | 44 | ||
41 | declarations: [ | 45 | declarations: [ |
42 | BytesPipe, | 46 | LoaderComponent, |
43 | KeysPipe, | 47 | VideoThumbnailComponent, |
44 | SearchComponent, | 48 | VideoMiniatureComponent, |
45 | LoaderComponent | 49 | DeleteButtonComponent, |
50 | EditButtonComponent, | ||
51 | NumberFormatterPipe, | ||
52 | FromNowPipe | ||
46 | ], | 53 | ], |
47 | 54 | ||
48 | exports: [ | 55 | exports: [ |
@@ -54,25 +61,30 @@ import { LoaderComponent } from './misc/loader.component' | |||
54 | 61 | ||
55 | BsDropdownModule, | 62 | BsDropdownModule, |
56 | ModalModule, | 63 | ModalModule, |
57 | PaginationModule, | ||
58 | ProgressbarModule, | ||
59 | DataTableModule, | 64 | DataTableModule, |
60 | PrimeSharedModule, | 65 | PrimeSharedModule, |
66 | InfiniteScrollModule, | ||
61 | BytesPipe, | 67 | BytesPipe, |
62 | KeysPipe, | 68 | KeysPipe, |
63 | 69 | ||
64 | SearchComponent, | 70 | LoaderComponent, |
65 | LoaderComponent | 71 | VideoThumbnailComponent, |
72 | VideoMiniatureComponent, | ||
73 | DeleteButtonComponent, | ||
74 | EditButtonComponent, | ||
75 | |||
76 | NumberFormatterPipe, | ||
77 | FromNowPipe | ||
66 | ], | 78 | ], |
67 | 79 | ||
68 | providers: [ | 80 | providers: [ |
69 | AUTH_INTERCEPTOR_PROVIDER, | 81 | AUTH_INTERCEPTOR_PROVIDER, |
70 | RestExtractor, | 82 | RestExtractor, |
71 | RestService, | 83 | RestService, |
72 | SearchService, | ||
73 | VideoAbuseService, | 84 | VideoAbuseService, |
74 | VideoBlacklistService, | 85 | VideoBlacklistService, |
75 | UserService | 86 | UserService, |
87 | VideoService | ||
76 | ] | 88 | ] |
77 | }) | 89 | }) |
78 | export class SharedModule { } | 90 | export class SharedModule { } |
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts index b075ab717..b4d13f37c 100644 --- a/client/src/app/shared/users/user.model.ts +++ b/client/src/app/shared/users/user.model.ts | |||
@@ -1,10 +1,5 @@ | |||
1 | import { | 1 | import { hasUserRight, User as UserServerModel, UserRight, UserRole, VideoChannel } from '../../../../../shared' |
2 | User as UserServerModel, | 2 | import { Account } from '../account/account.model' |
3 | UserRole, | ||
4 | VideoChannel, | ||
5 | UserRight, | ||
6 | hasUserRight | ||
7 | } from '../../../../../shared' | ||
8 | 3 | ||
9 | export type UserConstructorHash = { | 4 | export type UserConstructorHash = { |
10 | id: number, | 5 | id: number, |
@@ -14,10 +9,7 @@ export type UserConstructorHash = { | |||
14 | videoQuota?: number, | 9 | videoQuota?: number, |
15 | displayNSFW?: boolean, | 10 | displayNSFW?: boolean, |
16 | createdAt?: Date, | 11 | createdAt?: Date, |
17 | account?: { | 12 | account?: Account, |
18 | id: number | ||
19 | uuid: string | ||
20 | }, | ||
21 | videoChannels?: VideoChannel[] | 13 | videoChannels?: VideoChannel[] |
22 | } | 14 | } |
23 | export class User implements UserServerModel { | 15 | export class User implements UserServerModel { |
@@ -27,10 +19,7 @@ export class User implements UserServerModel { | |||
27 | role: UserRole | 19 | role: UserRole |
28 | displayNSFW: boolean | 20 | displayNSFW: boolean |
29 | videoQuota: number | 21 | videoQuota: number |
30 | account: { | 22 | account: Account |
31 | id: number | ||
32 | uuid: string | ||
33 | } | ||
34 | videoChannels: VideoChannel[] | 23 | videoChannels: VideoChannel[] |
35 | createdAt: Date | 24 | createdAt: Date |
36 | 25 | ||
@@ -61,4 +50,8 @@ export class User implements UserServerModel { | |||
61 | hasRight (right: UserRight) { | 50 | hasRight (right: UserRight) { |
62 | return hasUserRight(this.role, right) | 51 | return hasUserRight(this.role, right) |
63 | } | 52 | } |
53 | |||
54 | getAvatarPath () { | ||
55 | return Account.GET_ACCOUNT_AVATAR_PATH(this.account) | ||
56 | } | ||
64 | } | 57 | } |
diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html new file mode 100644 index 000000000..5761f2c81 --- /dev/null +++ b/client/src/app/shared/video/abstract-video-list.html | |||
@@ -0,0 +1,20 @@ | |||
1 | <div class="margin-content"> | ||
2 | <div class="title-page title-page-single"> | ||
3 | {{ titlePage }} | ||
4 | </div> | ||
5 | |||
6 | <div | ||
7 | class="videos" | ||
8 | infiniteScroll | ||
9 | [infiniteScrollUpDistance]="1.5" | ||
10 | [infiniteScrollDistance]="0.5" | ||
11 | (scrolled)="onNearOfBottom()" | ||
12 | (scrolledUp)="onNearOfTop()" | ||
13 | > | ||
14 | <my-video-miniature | ||
15 | class="ng-animate" | ||
16 | *ngFor="let video of videos" [video]="video" [user]="user" | ||
17 | > | ||
18 | </my-video-miniature> | ||
19 | </div> | ||
20 | </div> | ||
diff --git a/client/src/app/shared/video/abstract-video-list.scss b/client/src/app/shared/video/abstract-video-list.scss new file mode 100644 index 000000000..52797bc6c --- /dev/null +++ b/client/src/app/shared/video/abstract-video-list.scss | |||
@@ -0,0 +1,7 @@ | |||
1 | .videos { | ||
2 | text-align: center; | ||
3 | |||
4 | my-video-miniature { | ||
5 | text-align: left; | ||
6 | } | ||
7 | } | ||
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts new file mode 100644 index 000000000..ba1635a18 --- /dev/null +++ b/client/src/app/shared/video/abstract-video-list.ts | |||
@@ -0,0 +1,133 @@ | |||
1 | import { OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { NotificationsService } from 'angular2-notifications' | ||
4 | import { Observable } from 'rxjs/Observable' | ||
5 | import { SortField } from './sort-field.type' | ||
6 | import { VideoPagination } from './video-pagination.model' | ||
7 | import { Video } from './video.model' | ||
8 | |||
9 | export abstract class AbstractVideoList implements OnInit { | ||
10 | pagination: VideoPagination = { | ||
11 | currentPage: 1, | ||
12 | itemsPerPage: 25, | ||
13 | totalItems: null | ||
14 | } | ||
15 | sort: SortField = '-createdAt' | ||
16 | defaultSort: SortField = '-createdAt' | ||
17 | videos: Video[] = [] | ||
18 | loadOnInit = true | ||
19 | |||
20 | protected notificationsService: NotificationsService | ||
21 | protected router: Router | ||
22 | protected route: ActivatedRoute | ||
23 | |||
24 | protected abstract currentRoute: string | ||
25 | |||
26 | abstract titlePage: string | ||
27 | private loadedPages: { [ id: number ]: boolean } = {} | ||
28 | |||
29 | abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}> | ||
30 | |||
31 | ngOnInit () { | ||
32 | // Subscribe to route changes | ||
33 | const routeParams = this.route.snapshot.params | ||
34 | this.loadRouteParams(routeParams) | ||
35 | |||
36 | if (this.loadOnInit === true) this.loadMoreVideos('after') | ||
37 | } | ||
38 | |||
39 | onNearOfTop () { | ||
40 | if (this.pagination.currentPage > 1) { | ||
41 | this.previousPage() | ||
42 | } | ||
43 | } | ||
44 | |||
45 | onNearOfBottom () { | ||
46 | if (this.hasMoreVideos()) { | ||
47 | this.nextPage() | ||
48 | } | ||
49 | } | ||
50 | |||
51 | reloadVideos () { | ||
52 | this.videos = [] | ||
53 | this.loadedPages = {} | ||
54 | this.loadMoreVideos('before') | ||
55 | } | ||
56 | |||
57 | loadMoreVideos (where: 'before' | 'after') { | ||
58 | if (this.loadedPages[this.pagination.currentPage] === true) return | ||
59 | |||
60 | const observable = this.getVideosObservable() | ||
61 | |||
62 | observable.subscribe( | ||
63 | ({ videos, totalVideos }) => { | ||
64 | // Paging is too high, return to the first one | ||
65 | if (this.pagination.currentPage > 1 && totalVideos <= ((this.pagination.currentPage - 1) * this.pagination.itemsPerPage)) { | ||
66 | this.pagination.currentPage = 1 | ||
67 | this.setNewRouteParams() | ||
68 | return this.reloadVideos() | ||
69 | } | ||
70 | |||
71 | this.loadedPages[this.pagination.currentPage] = true | ||
72 | this.pagination.totalItems = totalVideos | ||
73 | |||
74 | if (where === 'before') { | ||
75 | this.videos = videos.concat(this.videos) | ||
76 | } else { | ||
77 | this.videos = this.videos.concat(videos) | ||
78 | } | ||
79 | }, | ||
80 | error => this.notificationsService.error('Error', error.text) | ||
81 | ) | ||
82 | } | ||
83 | |||
84 | protected hasMoreVideos () { | ||
85 | // No results | ||
86 | if (this.pagination.totalItems === 0) return false | ||
87 | |||
88 | // Not loaded yet | ||
89 | if (!this.pagination.totalItems) return true | ||
90 | |||
91 | const maxPage = this.pagination.totalItems / this.pagination.itemsPerPage | ||
92 | return maxPage > this.pagination.currentPage | ||
93 | } | ||
94 | |||
95 | protected previousPage () { | ||
96 | this.pagination.currentPage-- | ||
97 | |||
98 | this.setNewRouteParams() | ||
99 | this.loadMoreVideos('before') | ||
100 | } | ||
101 | |||
102 | protected nextPage () { | ||
103 | this.pagination.currentPage++ | ||
104 | |||
105 | this.setNewRouteParams() | ||
106 | this.loadMoreVideos('after') | ||
107 | } | ||
108 | |||
109 | protected buildRouteParams () { | ||
110 | // There is always a sort and a current page | ||
111 | const params = { | ||
112 | sort: this.sort, | ||
113 | page: this.pagination.currentPage | ||
114 | } | ||
115 | |||
116 | return params | ||
117 | } | ||
118 | |||
119 | protected loadRouteParams (routeParams: { [ key: string ]: any }) { | ||
120 | this.sort = routeParams['sort'] as SortField || this.defaultSort | ||
121 | |||
122 | if (routeParams['page'] !== undefined) { | ||
123 | this.pagination.currentPage = parseInt(routeParams['page'], 10) | ||
124 | } else { | ||
125 | this.pagination.currentPage = 1 | ||
126 | } | ||
127 | } | ||
128 | |||
129 | protected setNewRouteParams () { | ||
130 | const routeParams = this.buildRouteParams() | ||
131 | this.router.navigate([ this.currentRoute, routeParams ]) | ||
132 | } | ||
133 | } | ||
diff --git a/client/src/app/videos/shared/sort-field.type.ts b/client/src/app/shared/video/sort-field.type.ts index 776f360f8..776f360f8 100644 --- a/client/src/app/videos/shared/sort-field.type.ts +++ b/client/src/app/shared/video/sort-field.type.ts | |||
diff --git a/client/src/app/videos/shared/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index 64cb4f847..b96f8f6c8 100644 --- a/client/src/app/videos/shared/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { Video } from './video.model' | 1 | import { Account } from '../../../../../shared/models/accounts' |
2 | import { Video } from '../../shared/video/video.model' | ||
2 | import { AuthUser } from '../../core' | 3 | import { AuthUser } from '../../core' |
3 | import { | 4 | import { |
4 | VideoDetails as VideoDetailsServerModel, | 5 | VideoDetails as VideoDetailsServerModel, |
@@ -10,7 +11,7 @@ import { | |||
10 | } from '../../../../../shared' | 11 | } from '../../../../../shared' |
11 | 12 | ||
12 | export class VideoDetails extends Video implements VideoDetailsServerModel { | 13 | export class VideoDetails extends Video implements VideoDetailsServerModel { |
13 | account: string | 14 | accountName: string |
14 | by: string | 15 | by: string |
15 | createdAt: Date | 16 | createdAt: Date |
16 | updatedAt: Date | 17 | updatedAt: Date |
@@ -44,6 +45,9 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
44 | channel: VideoChannel | 45 | channel: VideoChannel |
45 | privacy: VideoPrivacy | 46 | privacy: VideoPrivacy |
46 | privacyLabel: string | 47 | privacyLabel: string |
48 | account: Account | ||
49 | likesPercent: number | ||
50 | dislikesPercent: number | ||
47 | 51 | ||
48 | constructor (hash: VideoDetailsServerModel) { | 52 | constructor (hash: VideoDetailsServerModel) { |
49 | super(hash) | 53 | super(hash) |
@@ -53,6 +57,10 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
53 | this.descriptionPath = hash.descriptionPath | 57 | this.descriptionPath = hash.descriptionPath |
54 | this.files = hash.files | 58 | this.files = hash.files |
55 | this.channel = hash.channel | 59 | this.channel = hash.channel |
60 | this.account = hash.account | ||
61 | |||
62 | this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 | ||
63 | this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 | ||
56 | } | 64 | } |
57 | 65 | ||
58 | getAppropriateMagnetUri (actualDownloadSpeed = 0) { | 66 | getAppropriateMagnetUri (actualDownloadSpeed = 0) { |
@@ -71,7 +79,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
71 | } | 79 | } |
72 | 80 | ||
73 | isRemovableBy (user: AuthUser) { | 81 | isRemovableBy (user: AuthUser) { |
74 | return user && this.isLocal === true && (this.account === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO)) | 82 | return user && this.isLocal === true && (this.accountName === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO)) |
75 | } | 83 | } |
76 | 84 | ||
77 | isBlackistableBy (user: AuthUser) { | 85 | isBlackistableBy (user: AuthUser) { |
@@ -79,6 +87,6 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
79 | } | 87 | } |
80 | 88 | ||
81 | isUpdatableBy (user: AuthUser) { | 89 | isUpdatableBy (user: AuthUser) { |
82 | return user && this.isLocal === true && user.username === this.account | 90 | return user && this.isLocal === true && user.username === this.accountName |
83 | } | 91 | } |
84 | } | 92 | } |
diff --git a/client/src/app/videos/shared/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts index 88d23a59f..955255bfa 100644 --- a/client/src/app/videos/shared/video-edit.model.ts +++ b/client/src/app/shared/video/video-edit.model.ts | |||
@@ -14,18 +14,20 @@ export class VideoEdit { | |||
14 | uuid?: string | 14 | uuid?: string |
15 | id?: number | 15 | id?: number |
16 | 16 | ||
17 | constructor (videoDetails: VideoDetails) { | 17 | constructor (videoDetails?: VideoDetails) { |
18 | this.id = videoDetails.id | 18 | if (videoDetails) { |
19 | this.uuid = videoDetails.uuid | 19 | this.id = videoDetails.id |
20 | this.category = videoDetails.category | 20 | this.uuid = videoDetails.uuid |
21 | this.licence = videoDetails.licence | 21 | this.category = videoDetails.category |
22 | this.language = videoDetails.language | 22 | this.licence = videoDetails.licence |
23 | this.description = videoDetails.description | 23 | this.language = videoDetails.language |
24 | this.name = videoDetails.name | 24 | this.description = videoDetails.description |
25 | this.tags = videoDetails.tags | 25 | this.name = videoDetails.name |
26 | this.nsfw = videoDetails.nsfw | 26 | this.tags = videoDetails.tags |
27 | this.channel = videoDetails.channel.id | 27 | this.nsfw = videoDetails.nsfw |
28 | this.privacy = videoDetails.privacy | 28 | this.channel = videoDetails.channel.id |
29 | this.privacy = videoDetails.privacy | ||
30 | } | ||
29 | } | 31 | } |
30 | 32 | ||
31 | patch (values: Object) { | 33 | patch (values: Object) { |
diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html new file mode 100644 index 000000000..7ac017235 --- /dev/null +++ b/client/src/app/shared/video/video-miniature.component.html | |||
@@ -0,0 +1,17 @@ | |||
1 | <div class="video-miniature"> | ||
2 | <my-video-thumbnail [video]="video" [nsfw]="isVideoNSFWForThisUser()"></my-video-thumbnail> | ||
3 | |||
4 | <div class="video-miniature-information"> | ||
5 | <span class="video-miniature-name"> | ||
6 | <a | ||
7 | class="video-miniature-name" | ||
8 | [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }" | ||
9 | > | ||
10 | {{ video.name }} | ||
11 | </a> | ||
12 | </span> | ||
13 | |||
14 | <span class="video-miniature-created-at-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> | ||
15 | <span class="video-miniature-account">{{ video.by }}</span> | ||
16 | </div> | ||
17 | </div> | ||
diff --git a/client/src/app/shared/video/video-miniature.component.scss b/client/src/app/shared/video/video-miniature.component.scss new file mode 100644 index 000000000..37e84897b --- /dev/null +++ b/client/src/app/shared/video/video-miniature.component.scss | |||
@@ -0,0 +1,44 @@ | |||
1 | .video-miniature { | ||
2 | display: inline-block; | ||
3 | padding-right: 15px; | ||
4 | margin-bottom: 30px; | ||
5 | height: 175px; | ||
6 | vertical-align: top; | ||
7 | |||
8 | .video-miniature-information { | ||
9 | width: 200px; | ||
10 | margin-top: 2px; | ||
11 | line-height: normal; | ||
12 | |||
13 | .video-miniature-name { | ||
14 | display: block; | ||
15 | overflow: hidden; | ||
16 | text-overflow: ellipsis; | ||
17 | white-space: nowrap; | ||
18 | font-weight: bold; | ||
19 | transition: color 0.2s; | ||
20 | font-size: 16px; | ||
21 | font-weight: $font-semibold; | ||
22 | color: #000; | ||
23 | |||
24 | &:hover { | ||
25 | text-decoration: none; | ||
26 | } | ||
27 | |||
28 | &.blur-filter { | ||
29 | filter: blur(3px); | ||
30 | padding-left: 4px; | ||
31 | } | ||
32 | } | ||
33 | |||
34 | .video-miniature-created-at-views { | ||
35 | display: block; | ||
36 | font-size: 13px; | ||
37 | } | ||
38 | |||
39 | .video-miniature-account { | ||
40 | font-size: 13px; | ||
41 | color: #585858; | ||
42 | } | ||
43 | } | ||
44 | } | ||
diff --git a/client/src/app/videos/video-list/shared/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts index e5a87907b..4d79a74bb 100644 --- a/client/src/app/videos/video-list/shared/video-miniature.component.ts +++ b/client/src/app/shared/video/video-miniature.component.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | 2 | import { User } from '../users' | |
3 | import { SortField, Video } from '../../shared' | 3 | import { Video } from './video.model' |
4 | import { User } from '../../../shared' | ||
5 | 4 | ||
6 | @Component({ | 5 | @Component({ |
7 | selector: 'my-video-miniature', | 6 | selector: 'my-video-miniature', |
@@ -9,7 +8,6 @@ import { User } from '../../../shared' | |||
9 | templateUrl: './video-miniature.component.html' | 8 | templateUrl: './video-miniature.component.html' |
10 | }) | 9 | }) |
11 | export class VideoMiniatureComponent { | 10 | export class VideoMiniatureComponent { |
12 | @Input() currentSort: SortField | ||
13 | @Input() user: User | 11 | @Input() user: User |
14 | @Input() video: Video | 12 | @Input() video: Video |
15 | 13 | ||
diff --git a/client/src/app/videos/shared/video-pagination.model.ts b/client/src/app/shared/video/video-pagination.model.ts index 9e71769cb..e9db61596 100644 --- a/client/src/app/videos/shared/video-pagination.model.ts +++ b/client/src/app/shared/video/video-pagination.model.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | export interface VideoPagination { | 1 | export interface VideoPagination { |
2 | currentPage: number | 2 | currentPage: number |
3 | itemsPerPage: number | 3 | itemsPerPage: number |
4 | totalItems: number | 4 | totalItems?: number |
5 | } | 5 | } |
diff --git a/client/src/app/shared/video/video-thumbnail.component.html b/client/src/app/shared/video/video-thumbnail.component.html new file mode 100644 index 000000000..5c698e8f6 --- /dev/null +++ b/client/src/app/shared/video/video-thumbnail.component.html | |||
@@ -0,0 +1,10 @@ | |||
1 | <a | ||
2 | [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name" | ||
3 | class="video-thumbnail" | ||
4 | > | ||
5 | <img [attr.src]="video.thumbnailUrl" alt="video thumbnail" [ngClass]="{ 'blur-filter': nsfw }" /> | ||
6 | |||
7 | <div class="video-thumbnail-overlay"> | ||
8 | {{ video.durationLabel }} | ||
9 | </div> | ||
10 | </a> | ||
diff --git a/client/src/app/shared/video/video-thumbnail.component.scss b/client/src/app/shared/video/video-thumbnail.component.scss new file mode 100644 index 000000000..ab4f9bcb1 --- /dev/null +++ b/client/src/app/shared/video/video-thumbnail.component.scss | |||
@@ -0,0 +1,28 @@ | |||
1 | .video-thumbnail { | ||
2 | display: inline-block; | ||
3 | position: relative; | ||
4 | border-radius: 4px; | ||
5 | overflow: hidden; | ||
6 | |||
7 | &:hover { | ||
8 | text-decoration: none !important; | ||
9 | } | ||
10 | |||
11 | img.blur-filter { | ||
12 | filter: blur(5px); | ||
13 | transform : scale(1.03); | ||
14 | } | ||
15 | |||
16 | .video-thumbnail-overlay { | ||
17 | position: absolute; | ||
18 | right: 5px; | ||
19 | bottom: 5px; | ||
20 | display: inline-block; | ||
21 | background-color: rgba(0, 0, 0, 0.7); | ||
22 | color: #fff; | ||
23 | font-size: 12px; | ||
24 | font-weight: $font-bold; | ||
25 | border-radius: 3px; | ||
26 | padding: 0 5px; | ||
27 | } | ||
28 | } | ||
diff --git a/client/src/app/shared/video/video-thumbnail.component.ts b/client/src/app/shared/video/video-thumbnail.component.ts new file mode 100644 index 000000000..e543e9903 --- /dev/null +++ b/client/src/app/shared/video/video-thumbnail.component.ts | |||
@@ -0,0 +1,12 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { Video } from './video.model' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-video-thumbnail', | ||
6 | styleUrls: [ './video-thumbnail.component.scss' ], | ||
7 | templateUrl: './video-thumbnail.component.html' | ||
8 | }) | ||
9 | export class VideoThumbnailComponent { | ||
10 | @Input() video: Video | ||
11 | @Input() nsfw = false | ||
12 | } | ||
diff --git a/client/src/app/videos/shared/video.model.ts b/client/src/app/shared/video/video.model.ts index 0dd41d71b..d86ef8f92 100644 --- a/client/src/app/videos/shared/video.model.ts +++ b/client/src/app/shared/video/video.model.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { Video as VideoServerModel } from '../../../../../shared' | 1 | import { Video as VideoServerModel } from '../../../../../shared' |
2 | import { User } from '../../shared' | 2 | import { User } from '../' |
3 | import { Account } from '../../../../../shared/models/accounts' | ||
3 | 4 | ||
4 | export class Video implements VideoServerModel { | 5 | export class Video implements VideoServerModel { |
5 | account: string | 6 | accountName: string |
6 | by: string | 7 | by: string |
7 | createdAt: Date | 8 | createdAt: Date |
8 | updatedAt: Date | 9 | updatedAt: Date |
@@ -31,6 +32,7 @@ export class Video implements VideoServerModel { | |||
31 | likes: number | 32 | likes: number |
32 | dislikes: number | 33 | dislikes: number |
33 | nsfw: boolean | 34 | nsfw: boolean |
35 | account: Account | ||
34 | 36 | ||
35 | private static createByString (account: string, serverHost: string) { | 37 | private static createByString (account: string, serverHost: string) { |
36 | return account + '@' + serverHost | 38 | return account + '@' + serverHost |
@@ -52,7 +54,7 @@ export class Video implements VideoServerModel { | |||
52 | absoluteAPIUrl = window.location.origin | 54 | absoluteAPIUrl = window.location.origin |
53 | } | 55 | } |
54 | 56 | ||
55 | this.account = hash.account | 57 | this.accountName = hash.accountName |
56 | this.createdAt = new Date(hash.createdAt.toString()) | 58 | this.createdAt = new Date(hash.createdAt.toString()) |
57 | this.categoryLabel = hash.categoryLabel | 59 | this.categoryLabel = hash.categoryLabel |
58 | this.category = hash.category | 60 | this.category = hash.category |
@@ -80,7 +82,7 @@ export class Video implements VideoServerModel { | |||
80 | this.dislikes = hash.dislikes | 82 | this.dislikes = hash.dislikes |
81 | this.nsfw = hash.nsfw | 83 | this.nsfw = hash.nsfw |
82 | 84 | ||
83 | this.by = Video.createByString(hash.account, hash.serverHost) | 85 | this.by = Video.createByString(hash.accountName, hash.serverHost) |
84 | } | 86 | } |
85 | 87 | ||
86 | isVideoNSFWForUser (user: User) { | 88 | isVideoNSFWForUser (user: User) { |
diff --git a/client/src/app/videos/shared/video.service.ts b/client/src/app/shared/video/video.service.ts index 5d25a26d4..1a0644c3d 100644 --- a/client/src/app/videos/shared/video.service.ts +++ b/client/src/app/shared/video/video.service.ts | |||
@@ -1,29 +1,23 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { Observable } from 'rxjs/Observable' | ||
3 | import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' | 1 | import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' |
2 | import { Injectable } from '@angular/core' | ||
4 | import 'rxjs/add/operator/catch' | 3 | import 'rxjs/add/operator/catch' |
5 | import 'rxjs/add/operator/map' | 4 | import 'rxjs/add/operator/map' |
6 | 5 | import { Observable } from 'rxjs/Observable' | |
6 | import { Video as VideoServerModel, VideoDetails as VideoDetailsServerModel } from '../../../../../shared' | ||
7 | import { ResultList } from '../../../../../shared/models/result-list.model' | ||
8 | import { UserVideoRateUpdate } from '../../../../../shared/models/videos/user-video-rate-update.model' | ||
9 | import { UserVideoRate } from '../../../../../shared/models/videos/user-video-rate.model' | ||
10 | import { VideoRateType } from '../../../../../shared/models/videos/video-rate.type' | ||
11 | import { VideoUpdate } from '../../../../../shared/models/videos/video-update.model' | ||
12 | import { RestExtractor } from '../rest/rest-extractor.service' | ||
13 | import { RestService } from '../rest/rest.service' | ||
14 | import { Search } from '../header/search.model' | ||
15 | import { UserService } from '../users/user.service' | ||
7 | import { SortField } from './sort-field.type' | 16 | import { SortField } from './sort-field.type' |
8 | import { | ||
9 | RestExtractor, | ||
10 | RestService, | ||
11 | UserService, | ||
12 | Search | ||
13 | } from '../../shared' | ||
14 | import { Video } from './video.model' | ||
15 | import { VideoDetails } from './video-details.model' | 17 | import { VideoDetails } from './video-details.model' |
16 | import { VideoEdit } from './video-edit.model' | 18 | import { VideoEdit } from './video-edit.model' |
17 | import { VideoPagination } from './video-pagination.model' | 19 | import { VideoPagination } from './video-pagination.model' |
18 | import { | 20 | import { Video } from './video.model' |
19 | UserVideoRate, | ||
20 | VideoRateType, | ||
21 | VideoUpdate, | ||
22 | UserVideoRateUpdate, | ||
23 | Video as VideoServerModel, | ||
24 | VideoDetails as VideoDetailsServerModel, | ||
25 | ResultList | ||
26 | } from '../../../../../shared' | ||
27 | 21 | ||
28 | @Injectable() | 22 | @Injectable() |
29 | export class VideoService { | 23 | export class VideoService { |
@@ -48,14 +42,17 @@ export class VideoService { | |||
48 | } | 42 | } |
49 | 43 | ||
50 | updateVideo (video: VideoEdit) { | 44 | updateVideo (video: VideoEdit) { |
51 | const language = video.language ? video.language : null | 45 | const language = video.language || undefined |
46 | const licence = video.licence || undefined | ||
47 | const category = video.category || undefined | ||
48 | const description = video.description || undefined | ||
52 | 49 | ||
53 | const body: VideoUpdate = { | 50 | const body: VideoUpdate = { |
54 | name: video.name, | 51 | name: video.name, |
55 | category: video.category, | 52 | category, |
56 | licence: video.licence, | 53 | licence, |
57 | language, | 54 | language, |
58 | description: video.description, | 55 | description, |
59 | privacy: video.privacy, | 56 | privacy: video.privacy, |
60 | tags: video.tags, | 57 | tags: video.tags, |
61 | nsfw: video.nsfw | 58 | nsfw: video.nsfw |
@@ -97,15 +94,14 @@ export class VideoService { | |||
97 | .catch((res) => this.restExtractor.handleError(res)) | 94 | .catch((res) => this.restExtractor.handleError(res)) |
98 | } | 95 | } |
99 | 96 | ||
100 | searchVideos (search: Search, videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { | 97 | searchVideos (search: string, videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { |
101 | const url = VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value) | 98 | const url = VideoService.BASE_VIDEO_URL + 'search' |
102 | 99 | ||
103 | const pagination = this.videoPaginationToRestPagination(videoPagination) | 100 | const pagination = this.videoPaginationToRestPagination(videoPagination) |
104 | 101 | ||
105 | let params = new HttpParams() | 102 | let params = new HttpParams() |
106 | params = this.restService.addRestGetParams(params, pagination, sort) | 103 | params = this.restService.addRestGetParams(params, pagination, sort) |
107 | 104 | params = params.append('search', search) | |
108 | if (search.field) params.set('field', search.field) | ||
109 | 105 | ||
110 | return this.authHttp | 106 | return this.authHttp |
111 | .get<ResultList<VideoServerModel>>(url, { params }) | 107 | .get<ResultList<VideoServerModel>>(url, { params }) |
diff --git a/client/src/app/signup/signup.component.html b/client/src/app/signup/signup.component.html index b8b7826eb..eb36b29f6 100644 --- a/client/src/app/signup/signup.component.html +++ b/client/src/app/signup/signup.component.html | |||
@@ -1,7 +1,8 @@ | |||
1 | <div class="row"> | 1 | <div class="margin-content"> |
2 | <div class="content-padding"> | ||
3 | 2 | ||
4 | <h3>Signup</h3> | 3 | <div class="title-page title-page-single"> |
4 | Create an account | ||
5 | </div> | ||
5 | 6 | ||
6 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | 7 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> |
7 | 8 | ||
@@ -10,9 +11,9 @@ | |||
10 | <label for="username">Username</label> | 11 | <label for="username">Username</label> |
11 | <input | 12 | <input |
12 | type="text" class="form-control" id="username" placeholder="Username" | 13 | type="text" class="form-control" id="username" placeholder="Username" |
13 | formControlName="username" | 14 | formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }" |
14 | > | 15 | > |
15 | <div *ngIf="formErrors.username" class="alert alert-danger"> | 16 | <div *ngIf="formErrors.username" class="form-error"> |
16 | {{ formErrors.username }} | 17 | {{ formErrors.username }} |
17 | </div> | 18 | </div> |
18 | </div> | 19 | </div> |
@@ -21,9 +22,9 @@ | |||
21 | <label for="email">Email</label> | 22 | <label for="email">Email</label> |
22 | <input | 23 | <input |
23 | type="text" class="form-control" id="email" placeholder="Email" | 24 | type="text" class="form-control" id="email" placeholder="Email" |
24 | formControlName="email" | 25 | formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }" |
25 | > | 26 | > |
26 | <div *ngIf="formErrors.email" class="alert alert-danger"> | 27 | <div *ngIf="formErrors.email" class="form-error"> |
27 | {{ formErrors.email }} | 28 | {{ formErrors.email }} |
28 | </div> | 29 | </div> |
29 | </div> | 30 | </div> |
@@ -32,15 +33,14 @@ | |||
32 | <label for="password">Password</label> | 33 | <label for="password">Password</label> |
33 | <input | 34 | <input |
34 | type="password" class="form-control" id="password" placeholder="Password" | 35 | type="password" class="form-control" id="password" placeholder="Password" |
35 | formControlName="password" | 36 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" |
36 | > | 37 | > |
37 | <div *ngIf="formErrors.password" class="alert alert-danger"> | 38 | <div *ngIf="formErrors.password" class="form-error"> |
38 | {{ formErrors.password }} | 39 | {{ formErrors.password }} |
39 | </div> | 40 | </div> |
40 | </div> | 41 | </div> |
41 | 42 | ||
42 | <input type="submit" value="Signup" class="btn btn-default" [disabled]="!form.valid"> | 43 | <input type="submit" value="Signup" [disabled]="!form.valid"> |
43 | </form> | 44 | </form> |
44 | 45 | ||
45 | </div> | ||
46 | </div> | 46 | </div> |
diff --git a/client/src/app/signup/signup.component.scss b/client/src/app/signup/signup.component.scss new file mode 100644 index 000000000..3b4326de4 --- /dev/null +++ b/client/src/app/signup/signup.component.scss | |||
@@ -0,0 +1,9 @@ | |||
1 | input:not([type=submit]) { | ||
2 | @include peertube-input-text(340px); | ||
3 | display: block; | ||
4 | } | ||
5 | |||
6 | input[type=submit] { | ||
7 | @include peertube-button; | ||
8 | @include orange-button; | ||
9 | } | ||
diff --git a/client/src/app/signup/signup.component.ts b/client/src/app/signup/signup.component.ts index 28e1ed0a8..13390a32a 100644 --- a/client/src/app/signup/signup.component.ts +++ b/client/src/app/signup/signup.component.ts | |||
@@ -16,7 +16,8 @@ import { UserCreate } from '../../../../shared' | |||
16 | 16 | ||
17 | @Component({ | 17 | @Component({ |
18 | selector: 'my-signup', | 18 | selector: 'my-signup', |
19 | templateUrl: './signup.component.html' | 19 | templateUrl: './signup.component.html', |
20 | styleUrls: [ './signup.component.scss' ] | ||
20 | }) | 21 | }) |
21 | export class SignupComponent extends FormReactive implements OnInit { | 22 | export class SignupComponent extends FormReactive implements OnInit { |
22 | error: string = null | 23 | error: string = null |
diff --git a/client/src/app/videos/shared/video-description.component.html b/client/src/app/videos/+video-edit/shared/video-description.component.html index 7a228857c..5d05467be 100644 --- a/client/src/app/videos/shared/video-description.component.html +++ b/client/src/app/videos/+video-edit/shared/video-description.component.html | |||
@@ -1,6 +1,6 @@ | |||
1 | <textarea | 1 | <textarea |
2 | [(ngModel)]="description" (ngModelChange)="onModelChange()" | 2 | [(ngModel)]="description" (ngModelChange)="onModelChange()" |
3 | id="description" class="form-control" placeholder="My super video"> | 3 | id="description" name="description"> |
4 | </textarea> | 4 | </textarea> |
5 | 5 | ||
6 | <tabset #staticTabs class="previews"> | 6 | <tabset #staticTabs class="previews"> |
diff --git a/client/src/app/videos/+video-edit/shared/video-description.component.scss b/client/src/app/videos/+video-edit/shared/video-description.component.scss new file mode 100644 index 000000000..2a4c8d189 --- /dev/null +++ b/client/src/app/videos/+video-edit/shared/video-description.component.scss | |||
@@ -0,0 +1,24 @@ | |||
1 | textarea { | ||
2 | @include peertube-input-text(100%); | ||
3 | |||
4 | padding: 5px 15px; | ||
5 | font-size: 15px; | ||
6 | height: 150px; | ||
7 | margin-bottom: 15px; | ||
8 | } | ||
9 | |||
10 | /deep/ { | ||
11 | .nav-link { | ||
12 | display: flex !important; | ||
13 | align-items: center; | ||
14 | height: 30px !important; | ||
15 | padding: 0 15px !important; | ||
16 | } | ||
17 | |||
18 | .tab-content { | ||
19 | min-height: 75px; | ||
20 | padding: 15px; | ||
21 | font-size: 15px; | ||
22 | } | ||
23 | } | ||
24 | |||
diff --git a/client/src/app/videos/shared/video-description.component.ts b/client/src/app/videos/+video-edit/shared/video-description.component.ts index d9ffb7800..9b77a27e6 100644 --- a/client/src/app/videos/shared/video-description.component.ts +++ b/client/src/app/videos/+video-edit/shared/video-description.component.ts | |||
@@ -1,12 +1,10 @@ | |||
1 | import { Component, forwardRef, Input, OnInit } from '@angular/core' | 1 | import { Component, forwardRef, Input, OnInit } from '@angular/core' |
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { Subject } from 'rxjs/Subject' | 3 | import { truncate } from 'lodash' |
4 | import 'rxjs/add/operator/debounceTime' | 4 | import 'rxjs/add/operator/debounceTime' |
5 | import 'rxjs/add/operator/distinctUntilChanged' | 5 | import 'rxjs/add/operator/distinctUntilChanged' |
6 | 6 | import { Subject } from 'rxjs/Subject' | |
7 | import { truncate } from 'lodash' | 7 | import { MarkdownService } from '../../shared' |
8 | |||
9 | import { MarkdownService } from './markdown.service' | ||
10 | 8 | ||
11 | @Component({ | 9 | @Component({ |
12 | selector: 'my-video-description', | 10 | selector: 'my-video-description', |
@@ -62,6 +60,8 @@ export class VideoDescriptionComponent implements ControlValueAccessor, OnInit { | |||
62 | } | 60 | } |
63 | 61 | ||
64 | private updateDescriptionPreviews () { | 62 | private updateDescriptionPreviews () { |
63 | if (!this.description) return | ||
64 | |||
65 | this.truncatedDescriptionHTML = this.markdownService.markdownToHTML(truncate(this.description, { length: 250 })) | 65 | this.truncatedDescriptionHTML = this.markdownService.markdownToHTML(truncate(this.description, { length: 250 })) |
66 | this.descriptionHTML = this.markdownService.markdownToHTML(this.description) | 66 | this.descriptionHTML = this.markdownService.markdownToHTML(this.description) |
67 | } | 67 | } |
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html new file mode 100644 index 000000000..8c071ce12 --- /dev/null +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html | |||
@@ -0,0 +1,86 @@ | |||
1 | <div class="video-edit row" [formGroup]="form"> | ||
2 | |||
3 | <div class="col-md-8"> | ||
4 | <div class="form-group"> | ||
5 | <label for="name">Title</label> | ||
6 | <input type="text" id="name" formControlName="name" /> | ||
7 | <div *ngIf="formErrors.name" class="form-error"> | ||
8 | {{ formErrors.name }} | ||
9 | </div> | ||
10 | </div> | ||
11 | |||
12 | <div class="form-group"> | ||
13 | <label class="label-tags">Tags</label> <span class="little-information">(press enter to add the tag)</span> | ||
14 | <tag-input | ||
15 | [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" | ||
16 | formControlName="tags" maxItems="5" modelAsStrings="true" | ||
17 | ></tag-input> | ||
18 | </div> | ||
19 | |||
20 | <div class="form-group"> | ||
21 | <label for="description">Description</label> | ||
22 | <my-video-description formControlName="description"></my-video-description> | ||
23 | |||
24 | <div *ngIf="formErrors.description" class="form-error"> | ||
25 | {{ formErrors.description }} | ||
26 | </div> | ||
27 | </div> | ||
28 | </div> | ||
29 | |||
30 | <div class="col-md-4"> | ||
31 | <div class="form-group"> | ||
32 | <label for="category">Category</label> | ||
33 | <select id="category" formControlName="category"> | ||
34 | <option></option> | ||
35 | <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option> | ||
36 | </select> | ||
37 | |||
38 | <div *ngIf="formErrors.category" class="form-error"> | ||
39 | {{ formErrors.category }} | ||
40 | </div> | ||
41 | </div> | ||
42 | |||
43 | <div class="form-group"> | ||
44 | <label for="licence">Licence</label> | ||
45 | <select id="licence" formControlName="licence"> | ||
46 | <option></option> | ||
47 | <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option> | ||
48 | </select> | ||
49 | |||
50 | <div *ngIf="formErrors.licence" class="form-error"> | ||
51 | {{ formErrors.licence }} | ||
52 | </div> | ||
53 | </div> | ||
54 | |||
55 | <div class="form-group"> | ||
56 | <label for="language">Language</label> | ||
57 | <select id="language" formControlName="language"> | ||
58 | <option></option> | ||
59 | <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option> | ||
60 | </select> | ||
61 | |||
62 | <div *ngIf="formErrors.language" class="form-error"> | ||
63 | {{ formErrors.language }} | ||
64 | </div> | ||
65 | </div> | ||
66 | |||
67 | <div class="form-group"> | ||
68 | <label for="privacy">Privacy</label> | ||
69 | <select id="privacy" formControlName="privacy"> | ||
70 | |||
71 | <option></option> | ||
72 | <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> | ||
73 | </select> | ||
74 | |||
75 | <div *ngIf="formErrors.privacy" class="form-error"> | ||
76 | {{ formErrors.privacy }} | ||
77 | </div> | ||
78 | </div> | ||
79 | |||
80 | <div class="form-group form-group-checkbox"> | ||
81 | <input type="checkbox" id="nsfw" formControlName="nsfw" /> | ||
82 | <label for="nsfw">This video contains mature or explicit content</label> | ||
83 | </div> | ||
84 | |||
85 | </div> | ||
86 | </div> | ||
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.scss b/client/src/app/videos/+video-edit/shared/video-edit.component.scss index 9ee0c520c..d363499ce 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.scss +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.scss | |||
@@ -1,48 +1,126 @@ | |||
1 | .btn-file { | 1 | .video-edit { |
2 | position: relative; | 2 | height: 100%; |
3 | overflow: hidden; | 3 | |
4 | display: block; | 4 | .form-group { |
5 | margin-bottom: 25px; | ||
6 | } | ||
7 | |||
8 | input { | ||
9 | @include peertube-input-text(100%); | ||
10 | display: block; | ||
11 | |||
12 | &[type=checkbox] { | ||
13 | outline: 0; | ||
14 | } | ||
15 | } | ||
16 | |||
17 | select { | ||
18 | @include peertube-select(100%); | ||
19 | } | ||
20 | |||
21 | input, select { | ||
22 | font-size: 15px | ||
23 | } | ||
24 | |||
25 | .form-group-checkbox { | ||
26 | display: flex; | ||
27 | align-items: center; | ||
28 | |||
29 | label { | ||
30 | font-weight: $font-regular; | ||
31 | margin: 0; | ||
32 | } | ||
33 | |||
34 | input { | ||
35 | width: 10px; | ||
36 | margin-right: 10px; | ||
37 | } | ||
38 | } | ||
5 | } | 39 | } |
6 | 40 | ||
7 | .btn-file input[type=file] { | 41 | .submit-container { |
8 | position: absolute; | ||
9 | top: 0; | ||
10 | right: 0; | ||
11 | min-width: 100%; | ||
12 | min-height: 100%; | ||
13 | font-size: 100px; | ||
14 | text-align: right; | 42 | text-align: right; |
15 | filter: alpha(opacity=0); | 43 | position: relative; |
16 | opacity: 0; | 44 | bottom: $button-height; |
17 | outline: none; | ||
18 | background: white; | ||
19 | cursor: inherit; | ||
20 | display: block; | ||
21 | } | ||
22 | 45 | ||
23 | .form-group { | 46 | .message-submit { |
24 | margin-bottom: 10px; | 47 | display: inline-block; |
25 | } | 48 | margin-right: 25px; |
49 | |||
50 | color: #585858; | ||
51 | font-size: 15px; | ||
52 | } | ||
53 | |||
54 | .submit-button { | ||
55 | @include peertube-button; | ||
56 | @include orange-button; | ||
57 | |||
58 | display: inline-block; | ||
26 | 59 | ||
27 | div.tags { | 60 | input { |
28 | height: 40px; | 61 | cursor: inherit; |
29 | font-size: 20px; | 62 | background-color: inherit; |
30 | margin-top: 20px; | 63 | border: none; |
64 | padding: 0; | ||
65 | outline: 0; | ||
66 | } | ||
31 | 67 | ||
32 | .tag { | 68 | .icon.icon-validate { |
33 | margin-right: 10px; | 69 | @include icon(20px); |
34 | 70 | ||
35 | .remove { | 71 | cursor: inherit; |
36 | cursor: pointer; | 72 | position: relative; |
73 | top: -1px; | ||
74 | margin-right: 4px; | ||
75 | background-image: url('../../../../assets/images/global/validate.svg'); | ||
37 | } | 76 | } |
38 | } | 77 | } |
39 | } | 78 | } |
40 | 79 | ||
41 | div.file-to-upload { | 80 | /deep/ { |
42 | height: 40px; | 81 | .ng2-tag-input { |
82 | border: none !important; | ||
83 | } | ||
43 | 84 | ||
44 | .glyphicon-remove { | 85 | .ng2-tags-container { |
45 | cursor: pointer; | 86 | display: flex; |
87 | align-items: center; | ||
88 | border: 1px solid #C6C6C6; | ||
89 | border-radius: 3px; | ||
90 | padding: 5px !important; | ||
91 | } | ||
92 | |||
93 | tag { | ||
94 | background-color: #E5E5E5 !important; | ||
95 | border-radius: 3px !important; | ||
96 | font-size: 15px !important; | ||
97 | color: #000 !important; | ||
98 | height: 30px !important; | ||
99 | line-height: 30px !important; | ||
100 | margin: 0 5px 0 0 !important; | ||
101 | cursor: default !important; | ||
102 | padding: 0 8px 0 10px !important; | ||
103 | |||
104 | div { | ||
105 | height: 100% !important; | ||
106 | } | ||
107 | } | ||
108 | |||
109 | delete-icon { | ||
110 | cursor: pointer !important; | ||
111 | height: auto !important; | ||
112 | vertical-align: middle !important; | ||
113 | padding-left: 6px !important; | ||
114 | |||
115 | svg { | ||
116 | height: auto !important; | ||
117 | vertical-align: middle !important; | ||
118 | fill: #585858 !important; | ||
119 | } | ||
120 | |||
121 | &:hover { | ||
122 | transform: none !important; | ||
123 | } | ||
46 | } | 124 | } |
47 | } | 125 | } |
48 | 126 | ||
@@ -50,7 +128,3 @@ div.file-to-upload { | |||
50 | font-size: 0.8em; | 128 | font-size: 0.8em; |
51 | font-style: italic; | 129 | font-style: italic; |
52 | } | 130 | } |
53 | |||
54 | .label-tags { | ||
55 | margin-bottom: 0; | ||
56 | } | ||
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.ts b/client/src/app/videos/+video-edit/shared/video-edit.component.ts new file mode 100644 index 000000000..5b1cc3f9c --- /dev/null +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts | |||
@@ -0,0 +1,83 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { FormBuilder, FormControl, FormGroup } from '@angular/forms' | ||
3 | import { ActivatedRoute, Router } from '@angular/router' | ||
4 | import { NotificationsService } from 'angular2-notifications' | ||
5 | import { ServerService } from 'app/core' | ||
6 | import { VideoEdit } from 'app/shared/video/video-edit.model' | ||
7 | import 'rxjs/add/observable/forkJoin' | ||
8 | import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' | ||
9 | import { | ||
10 | ValidatorMessage, | ||
11 | VIDEO_CATEGORY, | ||
12 | VIDEO_DESCRIPTION, | ||
13 | VIDEO_LANGUAGE, | ||
14 | VIDEO_LICENCE, | ||
15 | VIDEO_NAME, | ||
16 | VIDEO_PRIVACY, | ||
17 | VIDEO_TAGS | ||
18 | } from '../../../shared/forms/form-validators' | ||
19 | |||
20 | @Component({ | ||
21 | selector: 'my-video-edit', | ||
22 | styleUrls: [ './video-edit.component.scss' ], | ||
23 | templateUrl: './video-edit.component.html' | ||
24 | }) | ||
25 | |||
26 | export class VideoEditComponent implements OnInit { | ||
27 | @Input() form: FormGroup | ||
28 | @Input() formErrors: { [ id: string ]: string } = {} | ||
29 | @Input() validationMessages: ValidatorMessage = {} | ||
30 | @Input() videoPrivacies = [] | ||
31 | |||
32 | tags: string[] = [] | ||
33 | videoCategories = [] | ||
34 | videoLicences = [] | ||
35 | videoLanguages = [] | ||
36 | video: VideoEdit | ||
37 | |||
38 | tagValidators = VIDEO_TAGS.VALIDATORS | ||
39 | tagValidatorsMessages = VIDEO_TAGS.MESSAGES | ||
40 | |||
41 | error: string = null | ||
42 | |||
43 | constructor ( | ||
44 | private formBuilder: FormBuilder, | ||
45 | private route: ActivatedRoute, | ||
46 | private router: Router, | ||
47 | private notificationsService: NotificationsService, | ||
48 | private serverService: ServerService | ||
49 | ) { } | ||
50 | |||
51 | updateForm () { | ||
52 | this.formErrors['name'] = '' | ||
53 | this.formErrors['privacy'] = '' | ||
54 | this.formErrors['category'] = '' | ||
55 | this.formErrors['licence'] = '' | ||
56 | this.formErrors['language'] = '' | ||
57 | this.formErrors['description'] = '' | ||
58 | |||
59 | this.validationMessages['name'] = VIDEO_NAME.MESSAGES | ||
60 | this.validationMessages['privacy'] = VIDEO_PRIVACY.MESSAGES | ||
61 | this.validationMessages['category'] = VIDEO_CATEGORY.MESSAGES | ||
62 | this.validationMessages['licence'] = VIDEO_LICENCE.MESSAGES | ||
63 | this.validationMessages['language'] = VIDEO_LANGUAGE.MESSAGES | ||
64 | this.validationMessages['description'] = VIDEO_DESCRIPTION.MESSAGES | ||
65 | |||
66 | this.form.addControl('name', new FormControl('', VIDEO_NAME.VALIDATORS)) | ||
67 | this.form.addControl('privacy', new FormControl('', VIDEO_PRIVACY.VALIDATORS)) | ||
68 | this.form.addControl('nsfw', new FormControl(false)) | ||
69 | this.form.addControl('category', new FormControl('', VIDEO_CATEGORY.VALIDATORS)) | ||
70 | this.form.addControl('licence', new FormControl('', VIDEO_LICENCE.VALIDATORS)) | ||
71 | this.form.addControl('language', new FormControl('', VIDEO_LANGUAGE.VALIDATORS)) | ||
72 | this.form.addControl('description', new FormControl('', VIDEO_DESCRIPTION.VALIDATORS)) | ||
73 | this.form.addControl('tags', new FormControl('')) | ||
74 | } | ||
75 | |||
76 | ngOnInit () { | ||
77 | this.updateForm() | ||
78 | |||
79 | this.videoCategories = this.serverService.getVideoCategories() | ||
80 | this.videoLicences = this.serverService.getVideoLicences() | ||
81 | this.videoLanguages = this.serverService.getVideoLanguages() | ||
82 | } | ||
83 | } | ||
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.module.ts b/client/src/app/videos/+video-edit/shared/video-edit.module.ts index c64cea920..ce106d82f 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.module.ts +++ b/client/src/app/videos/+video-edit/shared/video-edit.module.ts | |||
@@ -3,8 +3,10 @@ import { NgModule } from '@angular/core' | |||
3 | import { TagInputModule } from 'ngx-chips' | 3 | import { TagInputModule } from 'ngx-chips' |
4 | import { TabsModule } from 'ngx-bootstrap/tabs' | 4 | import { TabsModule } from 'ngx-bootstrap/tabs' |
5 | 5 | ||
6 | import { VideoService, MarkdownService, VideoDescriptionComponent } from '../../shared' | 6 | import { MarkdownService } from '../../shared' |
7 | import { SharedModule } from '../../../shared' | 7 | import { SharedModule } from '../../../shared' |
8 | import { VideoDescriptionComponent } from './video-description.component' | ||
9 | import { VideoEditComponent } from './video-edit.component' | ||
8 | 10 | ||
9 | @NgModule({ | 11 | @NgModule({ |
10 | imports: [ | 12 | imports: [ |
@@ -15,18 +17,19 @@ import { SharedModule } from '../../../shared' | |||
15 | ], | 17 | ], |
16 | 18 | ||
17 | declarations: [ | 19 | declarations: [ |
18 | VideoDescriptionComponent | 20 | VideoDescriptionComponent, |
21 | VideoEditComponent | ||
19 | ], | 22 | ], |
20 | 23 | ||
21 | exports: [ | 24 | exports: [ |
22 | TagInputModule, | 25 | TagInputModule, |
23 | TabsModule, | 26 | TabsModule, |
24 | 27 | ||
25 | VideoDescriptionComponent | 28 | VideoDescriptionComponent, |
29 | VideoEditComponent | ||
26 | ], | 30 | ], |
27 | 31 | ||
28 | providers: [ | 32 | providers: [ |
29 | VideoService, | ||
30 | MarkdownService | 33 | MarkdownService |
31 | ] | 34 | ] |
32 | }) | 35 | }) |
diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html index b4e0f9f7c..a6f2bf6f2 100644 --- a/client/src/app/videos/+video-edit/video-add.component.html +++ b/client/src/app/videos/+video-edit/video-add.component.html | |||
@@ -1,141 +1,53 @@ | |||
1 | <div class="row"> | 1 | <div class="margin-content"> |
2 | <div class="content-padding"> | 2 | <div class="title-page title-page-single"> |
3 | Upload your video | ||
4 | </div> | ||
3 | 5 | ||
4 | <h3>Upload a video</h3> | 6 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> |
5 | 7 | ||
6 | <div *ngIf="error !== undefined" class="alert alert-danger">{{ error }}</div> | 8 | <div *ngIf="!isUploadingVideo" class="upload-video-container"> |
9 | <div class="upload-video"> | ||
10 | <div class="icon icon-upload"></div> | ||
7 | 11 | ||
8 | <form novalidate [formGroup]="form"> | 12 | <div class="button-file"> |
9 | <div class="form-group"> | 13 | <span>Select the file to upload</span> |
10 | <label for="name">Name</label> | 14 | <input #videofileInput type="file" name="videofile" id="videofile" (change)="fileChange()" /> |
11 | <input | ||
12 | type="text" class="form-control" id="name" | ||
13 | formControlName="name" | ||
14 | > | ||
15 | <div *ngIf="formErrors.name" class="alert alert-danger"> | ||
16 | {{ formErrors.name }} | ||
17 | </div> | ||
18 | </div> | 15 | </div> |
19 | 16 | ||
20 | <div class="form-group"> | 17 | <div class="form-group"> |
21 | <label for="privacy">Privacy</label> | 18 | <select [(ngModel)]="firstStepPrivacyId"> |
22 | <select class="form-control" id="privacy" formControlName="privacy"> | ||
23 | <option></option> | ||
24 | <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> | 19 | <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> |
25 | </select> | 20 | </select> |
26 | |||
27 | <div *ngIf="formErrors.privacy" class="alert alert-danger"> | ||
28 | {{ formErrors.privacy }} | ||
29 | </div> | ||
30 | </div> | ||
31 | |||
32 | <div class="form-group"> | ||
33 | <input | ||
34 | type="checkbox" id="nsfw" | ||
35 | formControlName="nsfw" | ||
36 | > | ||
37 | <label for="nsfw">This video contains mature or explicit content</label> | ||
38 | </div> | 21 | </div> |
39 | 22 | ||
40 | <div class="form-group"> | 23 | <div class="form-group"> |
41 | <label for="category">Channel</label> | 24 | <select [(ngModel)]="firstStepChannelId"> |
42 | <select class="form-control" id="channelId" formControlName="channelId"> | ||
43 | <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> | 25 | <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> |
44 | </select> | 26 | </select> |
45 | |||
46 | <div *ngIf="formErrors.channelId" class="alert alert-danger"> | ||
47 | {{ formErrors.channelId }} | ||
48 | </div> | ||
49 | </div> | ||
50 | |||
51 | <div class="form-group"> | ||
52 | <label for="category">Category</label> | ||
53 | <select class="form-control" id="category" formControlName="category"> | ||
54 | <option></option> | ||
55 | <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option> | ||
56 | </select> | ||
57 | |||
58 | <div *ngIf="formErrors.category" class="alert alert-danger"> | ||
59 | {{ formErrors.category }} | ||
60 | </div> | ||
61 | </div> | ||
62 | |||
63 | <div class="form-group"> | ||
64 | <label for="licence">Licence</label> | ||
65 | <select class="form-control" id="licence" formControlName="licence"> | ||
66 | <option></option> | ||
67 | <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option> | ||
68 | </select> | ||
69 | |||
70 | <div *ngIf="formErrors.licence" class="alert alert-danger"> | ||
71 | {{ formErrors.licence }} | ||
72 | </div> | ||
73 | </div> | ||
74 | |||
75 | <div class="form-group"> | ||
76 | <label for="language">Language</label> | ||
77 | <select class="form-control" id="language" formControlName="language"> | ||
78 | <option></option> | ||
79 | <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option> | ||
80 | </select> | ||
81 | |||
82 | <div *ngIf="formErrors.language" class="alert alert-danger"> | ||
83 | {{ formErrors.language }} | ||
84 | </div> | ||
85 | </div> | ||
86 | |||
87 | <div class="form-group"> | ||
88 | <label class="label-tags">Tags</label> <span class="little-information">(press enter to add the tag)</span> | ||
89 | <tag-input | ||
90 | [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" | ||
91 | formControlName="tags" maxItems="5" modelAsStrings="true" | ||
92 | ></tag-input> | ||
93 | </div> | ||
94 | |||
95 | <div class="form-group"> | ||
96 | <label for="videofile">File</label> | ||
97 | <div class="btn btn-default btn-file"> | ||
98 | <span>Select the video...</span> | ||
99 | <input #videofileInput type="file" name="videofile" id="videofile" (change)="fileChange($event)" /> | ||
100 | <input type="hidden" name="videofileHidden" formControlName="videofile"/> | ||
101 | </div> | ||
102 | </div> | 27 | </div> |
28 | </div> | ||
29 | </div> | ||
103 | 30 | ||
104 | <div class="file-to-upload"> | 31 | <p-progressBar |
105 | <div class="file" *ngIf="filename"> | 32 | *ngIf="isUploadingVideo" [value]="videoUploadPercents" |
106 | <span class="filename">{{ filename }}</span> | 33 | [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }" |
107 | <span class="glyphicon glyphicon-remove" (click)="removeFile()"></span> | 34 | ></p-progressBar> |
108 | </div> | ||
109 | </div> | ||
110 | 35 | ||
111 | <div *ngIf="formErrors.videofile" class="alert alert-danger"> | 36 | <!-- Hidden because we need to load the component --> |
112 | {{ formErrors.videofile }} | 37 | <form [hidden]="!isUploadingVideo" novalidate [formGroup]="form"> |
113 | </div> | 38 | <my-video-edit |
39 | [form]="form" [formErrors]="formErrors" | ||
40 | [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" | ||
41 | ></my-video-edit> | ||
114 | 42 | ||
115 | <div class="form-group"> | ||
116 | <label for="description">Description</label> | ||
117 | <my-video-description formControlName="description"></my-video-description> | ||
118 | 43 | ||
119 | <div *ngIf="formErrors.description" class="alert alert-danger"> | 44 | <div class="submit-container"> |
120 | {{ formErrors.description }} | 45 | <div *ngIf="videoUploaded === false" class="message-submit">Publish will be available when upload is finished</div> |
121 | </div> | ||
122 | </div> | ||
123 | 46 | ||
124 | <div class="progress"> | 47 | <div class="submit-button" (click)="updateSecondStep()" [ngClass]="{ disabled: !form.valid || videoUploaded !== true }"> |
125 | <progressbar [value]="progressPercent" max="100"> | 48 | <span class="icon icon-validate"></span> |
126 | <ng-template [ngIf]="progressPercent === 100"> | 49 | <input type="button" value="Publish" /> |
127 | <span class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></span> | ||
128 | Server is processing the video | ||
129 | </ng-template> | ||
130 | </progressbar> | ||
131 | </div> | 50 | </div> |
132 | 51 | </div> | |
133 | <div class="form-group"> | 52 | </form> |
134 | <input | ||
135 | type="button" value="Upload" class="btn btn-default form-control" | ||
136 | (click)="upload()" | ||
137 | > | ||
138 | </div> | ||
139 | </form> | ||
140 | </div> | ||
141 | </div> | 53 | </div> |
diff --git a/client/src/app/videos/+video-edit/video-add.component.scss b/client/src/app/videos/+video-edit/video-add.component.scss new file mode 100644 index 000000000..39673b4b7 --- /dev/null +++ b/client/src/app/videos/+video-edit/video-add.component.scss | |||
@@ -0,0 +1,96 @@ | |||
1 | .upload-video-container { | ||
2 | border-radius: 3px; | ||
3 | background-color: #F7F7F7; | ||
4 | border: 3px solid #EAEAEA; | ||
5 | width: 100%; | ||
6 | height: 440px; | ||
7 | text-align: center; | ||
8 | margin-top: 40px; | ||
9 | display: flex; | ||
10 | justify-content: center; | ||
11 | align-items: center; | ||
12 | |||
13 | .upload-video { | ||
14 | display: flex; | ||
15 | flex-direction: column; | ||
16 | align-items: center; | ||
17 | |||
18 | .icon.icon-upload { | ||
19 | @include icon(90px); | ||
20 | margin-bottom: 25px; | ||
21 | cursor: default; | ||
22 | |||
23 | background-image: url('../../../assets/images/video/upload.svg'); | ||
24 | } | ||
25 | |||
26 | .button-file { | ||
27 | position: relative; | ||
28 | overflow: hidden; | ||
29 | display: inline-block; | ||
30 | margin-bottom: 70px; | ||
31 | |||
32 | @include peertube-button; | ||
33 | @include orange-button; | ||
34 | |||
35 | input[type=file] { | ||
36 | position: absolute; | ||
37 | top: 0; | ||
38 | right: 0; | ||
39 | min-width: 100%; | ||
40 | min-height: 100%; | ||
41 | font-size: 100px; | ||
42 | text-align: right; | ||
43 | filter: alpha(opacity=0); | ||
44 | opacity: 0; | ||
45 | outline: none; | ||
46 | background: white; | ||
47 | cursor: inherit; | ||
48 | display: block; | ||
49 | } | ||
50 | } | ||
51 | |||
52 | select { | ||
53 | @include peertube-select(auto); | ||
54 | |||
55 | display: inline-block; | ||
56 | font-size: 15px | ||
57 | } | ||
58 | } | ||
59 | } | ||
60 | |||
61 | p-progressBar { | ||
62 | /deep/ .ui-progressbar { | ||
63 | margin-top: 25px !important; | ||
64 | margin-bottom: 40px !important; | ||
65 | font-size: 15px !important; | ||
66 | color: #fff !important; | ||
67 | height: 30px !important; | ||
68 | line-height: 30px !important; | ||
69 | border-radius: 3px !important; | ||
70 | background-color: rgba(11, 204, 41, 0.16) !important; | ||
71 | |||
72 | .ui-progressbar-value { | ||
73 | background-color: #0BCC29 !important; | ||
74 | } | ||
75 | |||
76 | .ui-progressbar-label { | ||
77 | text-align: left; | ||
78 | padding-left: 18px; | ||
79 | margin-top: 0 !important; | ||
80 | } | ||
81 | } | ||
82 | |||
83 | &.processing { | ||
84 | /deep/ .ui-progressbar-label { | ||
85 | // Same color as background to hide "100%" | ||
86 | color: rgba(11, 204, 41, 0.16) !important; | ||
87 | |||
88 | &::before { | ||
89 | content: 'Processing...'; | ||
90 | color: #fff; | ||
91 | } | ||
92 | } | ||
93 | } | ||
94 | } | ||
95 | |||
96 | |||
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts index 1704cf486..2bbc3de17 100644 --- a/client/src/app/videos/+video-edit/video-add.component.ts +++ b/client/src/app/videos/+video-edit/video-add.component.ts | |||
@@ -1,68 +1,42 @@ | |||
1 | import { HttpEventType, HttpResponse } from '@angular/common/http' | ||
1 | import { Component, OnInit, ViewChild } from '@angular/core' | 2 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { FormBuilder, FormGroup } from '@angular/forms' | 3 | import { FormBuilder, FormGroup } from '@angular/forms' |
3 | import { Router } from '@angular/router' | 4 | import { Router } from '@angular/router' |
4 | |||
5 | import { NotificationsService } from 'angular2-notifications' | 5 | import { NotificationsService } from 'angular2-notifications' |
6 | 6 | import { VideoService } from 'app/shared/video/video.service' | |
7 | import { | ||
8 | FormReactive, | ||
9 | VIDEO_NAME, | ||
10 | VIDEO_CATEGORY, | ||
11 | VIDEO_LICENCE, | ||
12 | VIDEO_LANGUAGE, | ||
13 | VIDEO_DESCRIPTION, | ||
14 | VIDEO_TAGS, | ||
15 | VIDEO_CHANNEL, | ||
16 | VIDEO_FILE, | ||
17 | VIDEO_PRIVACY | ||
18 | } from '../../shared' | ||
19 | import { AuthService, ServerService } from '../../core' | ||
20 | import { VideoService } from '../shared' | ||
21 | import { VideoCreate } from '../../../../../shared' | 7 | import { VideoCreate } from '../../../../../shared' |
22 | import { HttpEventType, HttpResponse } from '@angular/common/http' | 8 | import { VideoPrivacy } from '../../../../../shared/models/videos' |
9 | import { AuthService, ServerService } from '../../core' | ||
10 | import { FormReactive } from '../../shared' | ||
11 | import { ValidatorMessage } from '../../shared/forms/form-validators' | ||
12 | import { VideoEdit } from '../../shared/video/video-edit.model' | ||
23 | 13 | ||
24 | @Component({ | 14 | @Component({ |
25 | selector: 'my-videos-add', | 15 | selector: 'my-videos-add', |
26 | styleUrls: [ './shared/video-edit.component.scss' ], | 16 | templateUrl: './video-add.component.html', |
27 | templateUrl: './video-add.component.html' | 17 | styleUrls: [ |
18 | './shared/video-edit.component.scss', | ||
19 | './video-add.component.scss' | ||
20 | ] | ||
28 | }) | 21 | }) |
29 | 22 | ||
30 | export class VideoAddComponent extends FormReactive implements OnInit { | 23 | export class VideoAddComponent extends FormReactive implements OnInit { |
31 | @ViewChild('videofileInput') videofileInput | 24 | @ViewChild('videofileInput') videofileInput |
32 | 25 | ||
33 | progressPercent = 0 | 26 | isUploadingVideo = false |
34 | tags: string[] = [] | 27 | videoUploaded = false |
35 | videoCategories = [] | 28 | videoUploadPercents = 0 |
36 | videoLicences = [] | 29 | videoUploadedId = 0 |
37 | videoLanguages = [] | ||
38 | videoPrivacies = [] | ||
39 | userVideoChannels = [] | ||
40 | |||
41 | tagValidators = VIDEO_TAGS.VALIDATORS | ||
42 | tagValidatorsMessages = VIDEO_TAGS.MESSAGES | ||
43 | 30 | ||
44 | error: string | 31 | error: string = null |
45 | form: FormGroup | 32 | form: FormGroup |
46 | formErrors = { | 33 | formErrors: { [ id: string ]: string } = {} |
47 | name: '', | 34 | validationMessages: ValidatorMessage = {} |
48 | privacy: '', | 35 | |
49 | category: '', | 36 | userVideoChannels = [] |
50 | licence: '', | 37 | videoPrivacies = [] |
51 | language: '', | 38 | firstStepPrivacyId = 0 |
52 | channelId: '', | 39 | firstStepChannelId = 0 |
53 | description: '', | ||
54 | videofile: '' | ||
55 | } | ||
56 | validationMessages = { | ||
57 | name: VIDEO_NAME.MESSAGES, | ||
58 | privacy: VIDEO_PRIVACY.MESSAGES, | ||
59 | category: VIDEO_CATEGORY.MESSAGES, | ||
60 | licence: VIDEO_LICENCE.MESSAGES, | ||
61 | language: VIDEO_LANGUAGE.MESSAGES, | ||
62 | channelId: VIDEO_CHANNEL.MESSAGES, | ||
63 | description: VIDEO_DESCRIPTION.MESSAGES, | ||
64 | videofile: VIDEO_FILE.MESSAGES | ||
65 | } | ||
66 | 40 | ||
67 | constructor ( | 41 | constructor ( |
68 | private formBuilder: FormBuilder, | 42 | private formBuilder: FormBuilder, |
@@ -75,35 +49,23 @@ export class VideoAddComponent extends FormReactive implements OnInit { | |||
75 | super() | 49 | super() |
76 | } | 50 | } |
77 | 51 | ||
78 | get filename () { | ||
79 | return this.form.value['videofile'] | ||
80 | } | ||
81 | |||
82 | buildForm () { | 52 | buildForm () { |
83 | this.form = this.formBuilder.group({ | 53 | this.form = this.formBuilder.group({}) |
84 | name: [ '', VIDEO_NAME.VALIDATORS ], | ||
85 | nsfw: [ false ], | ||
86 | privacy: [ '', VIDEO_PRIVACY.VALIDATORS ], | ||
87 | category: [ '', VIDEO_CATEGORY.VALIDATORS ], | ||
88 | licence: [ '', VIDEO_LICENCE.VALIDATORS ], | ||
89 | language: [ '', VIDEO_LANGUAGE.VALIDATORS ], | ||
90 | channelId: [ '', VIDEO_CHANNEL.VALIDATORS ], | ||
91 | description: [ '', VIDEO_DESCRIPTION.VALIDATORS ], | ||
92 | videofile: [ '', VIDEO_FILE.VALIDATORS ], | ||
93 | tags: [ '' ] | ||
94 | }) | ||
95 | |||
96 | this.form.valueChanges.subscribe(data => this.onValueChanged(data)) | 54 | this.form.valueChanges.subscribe(data => this.onValueChanged(data)) |
97 | } | 55 | } |
98 | 56 | ||
99 | ngOnInit () { | 57 | ngOnInit () { |
100 | this.videoCategories = this.serverService.getVideoCategories() | ||
101 | this.videoLicences = this.serverService.getVideoLicences() | ||
102 | this.videoLanguages = this.serverService.getVideoLanguages() | ||
103 | this.videoPrivacies = this.serverService.getVideoPrivacies() | ||
104 | |||
105 | this.buildForm() | 58 | this.buildForm() |
106 | 59 | ||
60 | this.serverService.videoPrivaciesLoaded | ||
61 | .subscribe( | ||
62 | () => { | ||
63 | this.videoPrivacies = this.serverService.getVideoPrivacies() | ||
64 | |||
65 | // Public by default | ||
66 | this.firstStepPrivacyId = VideoPrivacy.PUBLIC | ||
67 | }) | ||
68 | |||
107 | this.authService.userInformationLoaded | 69 | this.authService.userInformationLoaded |
108 | .subscribe( | 70 | .subscribe( |
109 | () => { | 71 | () => { |
@@ -114,21 +76,13 @@ export class VideoAddComponent extends FormReactive implements OnInit { | |||
114 | if (Array.isArray(videoChannels) === false) return | 76 | if (Array.isArray(videoChannels) === false) return |
115 | 77 | ||
116 | this.userVideoChannels = videoChannels.map(v => ({ id: v.id, label: v.name })) | 78 | this.userVideoChannels = videoChannels.map(v => ({ id: v.id, label: v.name })) |
117 | 79 | this.firstStepChannelId = this.userVideoChannels[0].id | |
118 | this.form.patchValue({ channelId: this.userVideoChannels[0].id }) | ||
119 | } | 80 | } |
120 | ) | 81 | ) |
121 | } | 82 | } |
122 | 83 | ||
123 | // The goal is to keep reactive form validation (required field) | 84 | fileChange () { |
124 | // https://stackoverflow.com/a/44238894 | 85 | this.uploadFirstStep() |
125 | fileChange ($event) { | ||
126 | this.form.controls['videofile'].setValue($event.target.files[0].name) | ||
127 | } | ||
128 | |||
129 | removeFile () { | ||
130 | this.videofileInput.nativeElement.value = '' | ||
131 | this.form.controls['videofile'].setValue('') | ||
132 | } | 86 | } |
133 | 87 | ||
134 | checkForm () { | 88 | checkForm () { |
@@ -137,62 +91,72 @@ export class VideoAddComponent extends FormReactive implements OnInit { | |||
137 | return this.form.valid | 91 | return this.form.valid |
138 | } | 92 | } |
139 | 93 | ||
140 | upload () { | 94 | uploadFirstStep () { |
141 | if (this.checkForm() === false) { | ||
142 | return | ||
143 | } | ||
144 | |||
145 | const formValue: VideoCreate = this.form.value | ||
146 | |||
147 | const name = formValue.name | ||
148 | const privacy = formValue.privacy | ||
149 | const nsfw = formValue.nsfw | ||
150 | const category = formValue.category | ||
151 | const licence = formValue.licence | ||
152 | const language = formValue.language | ||
153 | const channelId = formValue.channelId | ||
154 | const description = formValue.description | ||
155 | const tags = formValue.tags | ||
156 | const videofile = this.videofileInput.nativeElement.files[0] | 95 | const videofile = this.videofileInput.nativeElement.files[0] |
96 | const name = videofile.name.replace(/\.[^/.]+$/, '') | ||
97 | const privacy = this.firstStepPrivacyId.toString() | ||
98 | const nsfw = false | ||
99 | const channelId = this.firstStepChannelId.toString() | ||
157 | 100 | ||
158 | const formData = new FormData() | 101 | const formData = new FormData() |
159 | formData.append('name', name) | 102 | formData.append('name', name) |
160 | formData.append('privacy', privacy.toString()) | 103 | // Put the video "private" -> we wait he validates the second step |
161 | formData.append('category', '' + category) | 104 | formData.append('privacy', VideoPrivacy.PRIVATE.toString()) |
162 | formData.append('nsfw', '' + nsfw) | 105 | formData.append('nsfw', '' + nsfw) |
163 | formData.append('licence', '' + licence) | ||
164 | formData.append('channelId', '' + channelId) | 106 | formData.append('channelId', '' + channelId) |
165 | formData.append('videofile', videofile) | 107 | formData.append('videofile', videofile) |
166 | 108 | ||
167 | // Language is optional | 109 | this.isUploadingVideo = true |
168 | if (language) { | 110 | this.form.patchValue({ |
169 | formData.append('language', '' + language) | 111 | name, |
170 | } | 112 | privacy, |
171 | 113 | nsfw, | |
172 | formData.append('description', description) | 114 | channelId |
173 | 115 | }) | |
174 | for (let i = 0; i < tags.length; i++) { | ||
175 | formData.append(`tags[${i}]`, tags[i]) | ||
176 | } | ||
177 | 116 | ||
178 | this.videoService.uploadVideo(formData).subscribe( | 117 | this.videoService.uploadVideo(formData).subscribe( |
179 | event => { | 118 | event => { |
180 | if (event.type === HttpEventType.UploadProgress) { | 119 | if (event.type === HttpEventType.UploadProgress) { |
181 | this.progressPercent = Math.round(100 * event.loaded / event.total) | 120 | this.videoUploadPercents = Math.round(100 * event.loaded / event.total) |
182 | } else if (event instanceof HttpResponse) { | 121 | } else if (event instanceof HttpResponse) { |
183 | console.log('Video uploaded.') | 122 | console.log('Video uploaded.') |
184 | this.notificationsService.success('Success', 'Video uploaded.') | ||
185 | 123 | ||
186 | // Display all the videos once it's finished | 124 | this.videoUploaded = true |
187 | this.router.navigate([ '/videos/list' ]) | 125 | |
126 | this.videoUploadedId = event.body.video.id | ||
188 | } | 127 | } |
189 | }, | 128 | }, |
190 | 129 | ||
191 | err => { | 130 | err => { |
192 | // Reset progress | 131 | // Reset progress |
193 | this.progressPercent = 0 | 132 | this.videoUploadPercents = 0 |
194 | this.error = err.message | 133 | this.error = err.message |
195 | } | 134 | } |
196 | ) | 135 | ) |
197 | } | 136 | } |
137 | |||
138 | updateSecondStep () { | ||
139 | if (this.checkForm() === false) { | ||
140 | return | ||
141 | } | ||
142 | |||
143 | const video = new VideoEdit() | ||
144 | video.patch(this.form.value) | ||
145 | video.channel = this.firstStepChannelId | ||
146 | video.id = this.videoUploadedId | ||
147 | |||
148 | this.videoService.updateVideo(video) | ||
149 | .subscribe( | ||
150 | () => { | ||
151 | this.notificationsService.success('Success', 'Video published.') | ||
152 | this.router.navigate([ '/videos/watch', video.id ]) | ||
153 | }, | ||
154 | |||
155 | err => { | ||
156 | this.error = 'Cannot update the video.' | ||
157 | console.error(err) | ||
158 | } | ||
159 | ) | ||
160 | |||
161 | } | ||
198 | } | 162 | } |
diff --git a/client/src/app/videos/+video-edit/video-add.module.ts b/client/src/app/videos/+video-edit/video-add.module.ts index f58d12dac..1efecdf4d 100644 --- a/client/src/app/videos/+video-edit/video-add.module.ts +++ b/client/src/app/videos/+video-edit/video-add.module.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { ProgressBarModule } from 'primeng/primeng' | ||
2 | import { SharedModule } from '../../shared' | 3 | import { SharedModule } from '../../shared' |
3 | import { VideoEditModule } from './shared/video-edit.module' | 4 | import { VideoEditModule } from './shared/video-edit.module' |
4 | import { VideoAddRoutingModule } from './video-add-routing.module' | 5 | import { VideoAddRoutingModule } from './video-add-routing.module' |
@@ -8,7 +9,8 @@ import { VideoAddComponent } from './video-add.component' | |||
8 | imports: [ | 9 | imports: [ |
9 | VideoAddRoutingModule, | 10 | VideoAddRoutingModule, |
10 | VideoEditModule, | 11 | VideoEditModule, |
11 | SharedModule | 12 | SharedModule, |
13 | ProgressBarModule | ||
12 | ], | 14 | ], |
13 | 15 | ||
14 | declarations: [ | 16 | declarations: [ |
diff --git a/client/src/app/videos/+video-edit/video-update.component.html b/client/src/app/videos/+video-edit/video-update.component.html index b9c6139b2..261b8a130 100644 --- a/client/src/app/videos/+video-edit/video-update.component.html +++ b/client/src/app/videos/+video-edit/video-update.component.html | |||
@@ -1,101 +1,20 @@ | |||
1 | <div class="row"> | 1 | <div class="margin-content"> |
2 | <div class="content-padding"> | 2 | <div class="title-page title-page-single"> |
3 | 3 | Update {{ video?.name }} | |
4 | <h3>Update {{ video?.name }}</h3> | 4 | </div> |
5 | |||
6 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | ||
7 | 5 | ||
8 | <form novalidate [formGroup]="form"> | 6 | <form novalidate [formGroup]="form"> |
9 | <div class="form-group"> | ||
10 | <label for="name">Name</label> | ||
11 | <input | ||
12 | type="text" class="form-control" id="name" | ||
13 | formControlName="name" | ||
14 | > | ||
15 | <div *ngIf="formErrors.name" class="alert alert-danger"> | ||
16 | {{ formErrors.name }} | ||
17 | </div> | ||
18 | </div> | ||
19 | |||
20 | <div class="form-group"> | ||
21 | <label for="privacy">Privacy</label> | ||
22 | <select class="form-control" id="privacy" formControlName="privacy"> | ||
23 | <option></option> | ||
24 | <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> | ||
25 | </select> | ||
26 | |||
27 | <div *ngIf="formErrors.privacy" class="alert alert-danger"> | ||
28 | {{ formErrors.privacy }} | ||
29 | </div> | ||
30 | </div> | ||
31 | |||
32 | <div class="form-group"> | ||
33 | <input | ||
34 | type="checkbox" id="nsfw" | ||
35 | formControlName="nsfw" | ||
36 | > | ||
37 | <label for="nsfw">This video contains mature or explicit content</label> | ||
38 | </div> | ||
39 | |||
40 | <div class="form-group"> | ||
41 | <label for="category">Category</label> | ||
42 | <select class="form-control" id="category" formControlName="category"> | ||
43 | <option></option> | ||
44 | <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option> | ||
45 | </select> | ||
46 | |||
47 | <div *ngIf="formErrors.category" class="alert alert-danger"> | ||
48 | {{ formErrors.category }} | ||
49 | </div> | ||
50 | </div> | ||
51 | |||
52 | <div class="form-group"> | ||
53 | <label for="licence">Licence</label> | ||
54 | <select class="form-control" id="licence" formControlName="licence"> | ||
55 | <option></option> | ||
56 | <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option> | ||
57 | </select> | ||
58 | |||
59 | <div *ngIf="formErrors.licence" class="alert alert-danger"> | ||
60 | {{ formErrors.licence }} | ||
61 | </div> | ||
62 | </div> | ||
63 | |||
64 | <div class="form-group"> | ||
65 | <label for="language">Language</label> | ||
66 | <select class="form-control" id="language" formControlName="language"> | ||
67 | <option></option> | ||
68 | <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option> | ||
69 | </select> | ||
70 | |||
71 | <div *ngIf="formErrors.language" class="alert alert-danger"> | ||
72 | {{ formErrors.language }} | ||
73 | </div> | ||
74 | </div> | ||
75 | |||
76 | <div class="form-group"> | ||
77 | <label class="label-tags">Tags</label> <span class="little-information">(press enter to add the tag)</span> | ||
78 | <tag-input | ||
79 | [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" | ||
80 | formControlName="tags" maxItems="5" modelAsStrings="true" | ||
81 | ></tag-input> | ||
82 | </div> | ||
83 | 7 | ||
84 | <div class="form-group"> | 8 | <my-video-edit |
85 | <label for="description">Description</label> | 9 | [form]="form" [formErrors]="formErrors" |
86 | <my-video-description formControlName="description"></my-video-description> | 10 | [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" |
11 | ></my-video-edit> | ||
87 | 12 | ||
88 | <div *ngIf="formErrors.description" class="alert alert-danger"> | 13 | <div class="submit-container"> |
89 | {{ formErrors.description }} | 14 | <div class="submit-button" (click)="update()" [ngClass]="{ disabled: !form.valid }"> |
15 | <span class="icon icon-validate"></span> | ||
16 | <input type="button" value="Update" /> | ||
90 | </div> | 17 | </div> |
91 | </div> | 18 | </div> |
92 | |||
93 | <div class="form-group"> | ||
94 | <input | ||
95 | type="button" value="Update" class="btn btn-default form-control" | ||
96 | (click)="update()" | ||
97 | > | ||
98 | </div> | ||
99 | </form> | 19 | </form> |
100 | </div> | ||
101 | </div> | 20 | </div> |
diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts index 0e966cb50..d1da8b6d8 100644 --- a/client/src/app/videos/+video-edit/video-update.component.ts +++ b/client/src/app/videos/+video-edit/video-update.component.ts | |||
@@ -1,23 +1,14 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { FormBuilder, FormGroup } from '@angular/forms' | 2 | import { FormBuilder, FormGroup } from '@angular/forms' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import 'rxjs/add/observable/forkJoin' | ||
5 | |||
6 | import { NotificationsService } from 'angular2-notifications' | 4 | import { NotificationsService } from 'angular2-notifications' |
7 | 5 | import 'rxjs/add/observable/forkJoin' | |
8 | import { ServerService } from '../../core' | ||
9 | import { | ||
10 | FormReactive, | ||
11 | VIDEO_NAME, | ||
12 | VIDEO_CATEGORY, | ||
13 | VIDEO_LICENCE, | ||
14 | VIDEO_LANGUAGE, | ||
15 | VIDEO_DESCRIPTION, | ||
16 | VIDEO_TAGS, | ||
17 | VIDEO_PRIVACY | ||
18 | } from '../../shared' | ||
19 | import { VideoEdit, VideoService } from '../shared' | ||
20 | import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' | 6 | import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' |
7 | import { ServerService } from '../../core' | ||
8 | import { FormReactive } from '../../shared' | ||
9 | import { ValidatorMessage } from '../../shared/forms/form-validators' | ||
10 | import { VideoEdit } from '../../shared/video/video-edit.model' | ||
11 | import { VideoService } from '../../shared/video/video.service' | ||
21 | 12 | ||
22 | @Component({ | 13 | @Component({ |
23 | selector: 'my-videos-update', | 14 | selector: 'my-videos-update', |
@@ -26,34 +17,13 @@ import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy. | |||
26 | }) | 17 | }) |
27 | 18 | ||
28 | export class VideoUpdateComponent extends FormReactive implements OnInit { | 19 | export class VideoUpdateComponent extends FormReactive implements OnInit { |
29 | tags: string[] = [] | ||
30 | videoCategories = [] | ||
31 | videoLicences = [] | ||
32 | videoLanguages = [] | ||
33 | videoPrivacies = [] | ||
34 | video: VideoEdit | 20 | video: VideoEdit |
35 | 21 | ||
36 | tagValidators = VIDEO_TAGS.VALIDATORS | ||
37 | tagValidatorsMessages = VIDEO_TAGS.MESSAGES | ||
38 | |||
39 | error: string = null | 22 | error: string = null |
40 | form: FormGroup | 23 | form: FormGroup |
41 | formErrors = { | 24 | formErrors: { [ id: string ]: string } = {} |
42 | name: '', | 25 | validationMessages: ValidatorMessage = {} |
43 | privacy: '', | 26 | videoPrivacies = [] |
44 | category: '', | ||
45 | licence: '', | ||
46 | language: '', | ||
47 | description: '' | ||
48 | } | ||
49 | validationMessages = { | ||
50 | name: VIDEO_NAME.MESSAGES, | ||
51 | privacy: VIDEO_PRIVACY.MESSAGES, | ||
52 | category: VIDEO_CATEGORY.MESSAGES, | ||
53 | licence: VIDEO_LICENCE.MESSAGES, | ||
54 | language: VIDEO_LANGUAGE.MESSAGES, | ||
55 | description: VIDEO_DESCRIPTION.MESSAGES | ||
56 | } | ||
57 | 27 | ||
58 | fileError = '' | 28 | fileError = '' |
59 | 29 | ||
@@ -69,30 +39,16 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
69 | } | 39 | } |
70 | 40 | ||
71 | buildForm () { | 41 | buildForm () { |
72 | this.form = this.formBuilder.group({ | 42 | this.form = this.formBuilder.group({}) |
73 | name: [ '', VIDEO_NAME.VALIDATORS ], | ||
74 | privacy: [ '', VIDEO_PRIVACY.VALIDATORS ], | ||
75 | nsfw: [ false ], | ||
76 | category: [ '', VIDEO_CATEGORY.VALIDATORS ], | ||
77 | licence: [ '', VIDEO_LICENCE.VALIDATORS ], | ||
78 | language: [ '', VIDEO_LANGUAGE.VALIDATORS ], | ||
79 | description: [ '', VIDEO_DESCRIPTION.VALIDATORS ], | ||
80 | tags: [ '' ] | ||
81 | }) | ||
82 | |||
83 | this.form.valueChanges.subscribe(data => this.onValueChanged(data)) | 43 | this.form.valueChanges.subscribe(data => this.onValueChanged(data)) |
84 | } | 44 | } |
85 | 45 | ||
86 | ngOnInit () { | 46 | ngOnInit () { |
87 | this.buildForm() | 47 | this.buildForm() |
88 | 48 | ||
89 | this.videoCategories = this.serverService.getVideoCategories() | ||
90 | this.videoLicences = this.serverService.getVideoLicences() | ||
91 | this.videoLanguages = this.serverService.getVideoLanguages() | ||
92 | this.videoPrivacies = this.serverService.getVideoPrivacies() | 49 | this.videoPrivacies = this.serverService.getVideoPrivacies() |
93 | 50 | ||
94 | const uuid: string = this.route.snapshot.params['uuid'] | 51 | const uuid: string = this.route.snapshot.params['uuid'] |
95 | |||
96 | this.videoService.getVideo(uuid) | 52 | this.videoService.getVideo(uuid) |
97 | .switchMap(video => { | 53 | .switchMap(video => { |
98 | return this.videoService | 54 | return this.videoService |
@@ -104,7 +60,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
104 | video => { | 60 | video => { |
105 | this.video = new VideoEdit(video) | 61 | this.video = new VideoEdit(video) |
106 | 62 | ||
107 | // We cannot set private a video that was not private anymore | 63 | // We cannot set private a video that was not private |
108 | if (video.privacy !== VideoPrivacy.PRIVATE) { | 64 | if (video.privacy !== VideoPrivacy.PRIVATE) { |
109 | const newVideoPrivacies = [] | 65 | const newVideoPrivacies = [] |
110 | for (const p of this.videoPrivacies) { | 66 | for (const p of this.videoPrivacies) { |
diff --git a/client/src/app/videos/+video-watch/video-download.component.html b/client/src/app/videos/+video-watch/video-download.component.html index ddc57e999..7efc79e93 100644 --- a/client/src/app/videos/+video-watch/video-download.component.html +++ b/client/src/app/videos/+video-watch/video-download.component.html | |||
@@ -6,18 +6,19 @@ | |||
6 | <button type="button" class="close" aria-label="Close" (click)="hide()"> | 6 | <button type="button" class="close" aria-label="Close" (click)="hide()"> |
7 | <span aria-hidden="true">×</span> | 7 | <span aria-hidden="true">×</span> |
8 | </button> | 8 | </button> |
9 | <h4 class="modal-title">Download</h4> | 9 | <h4 class="title-page title-page-single">Download</h4> |
10 | </div> | 10 | </div> |
11 | 11 | ||
12 | <div class="modal-body"> | 12 | <div class="modal-body"> |
13 | <div *ngFor="let file of video.files" class="resolution-block"> | 13 | <div *ngFor="let file of video.files" class="resolution-block"> |
14 | <label>{{ file.resolutionLabel }}</label> | 14 | <label>{{ file.resolutionLabel }}</label> |
15 | <a class="btn btn-default " target="_blank" [href]="file.torrentUrl"> | 15 | |
16 | <span class="glyphicon glyphicon-download"></span> | 16 | <a class="orange-button-link " target="_blank" [href]="file.torrentUrl"> |
17 | <span class="icon icon-download"></span> | ||
17 | Torrent file | 18 | Torrent file |
18 | </a> | 19 | </a> |
19 | <a class="btn btn-default" target="_blank" [href]="file.fileUrl"> | 20 | <a class="orange-button-link" target="_blank" [href]="file.fileUrl"> |
20 | <span class="glyphicon glyphicon-download"></span> | 21 | <span class="icon icon-download"></span> |
21 | Download | 22 | Download |
22 | </a> | 23 | </a> |
23 | 24 | ||
diff --git a/client/src/app/videos/+video-watch/video-download.component.scss b/client/src/app/videos/+video-watch/video-download.component.scss new file mode 100644 index 000000000..c9d5af9c1 --- /dev/null +++ b/client/src/app/videos/+video-watch/video-download.component.scss | |||
@@ -0,0 +1,23 @@ | |||
1 | .resolution-block:not(:first-child) { | ||
2 | margin-top: 30px; | ||
3 | } | ||
4 | |||
5 | .orange-button-link { | ||
6 | margin-right: 10px; | ||
7 | } | ||
8 | |||
9 | label { | ||
10 | display: block; | ||
11 | } | ||
12 | |||
13 | .icon { | ||
14 | @include icon(21px); | ||
15 | |||
16 | margin-right: 5px; | ||
17 | position: relative; | ||
18 | top: -1px; | ||
19 | |||
20 | &.icon-download { | ||
21 | background-image: url('../../../assets/images/video/download-white.svg'); | ||
22 | } | ||
23 | } | ||
diff --git a/client/src/app/videos/+video-watch/video-download.component.ts b/client/src/app/videos/+video-watch/video-download.component.ts index c32f8d586..095df1698 100644 --- a/client/src/app/videos/+video-watch/video-download.component.ts +++ b/client/src/app/videos/+video-watch/video-download.component.ts | |||
@@ -1,13 +1,11 @@ | |||
1 | import { Component, Input, ViewChild } from '@angular/core' | 1 | import { Component, Input, ViewChild } from '@angular/core' |
2 | |||
3 | import { ModalDirective } from 'ngx-bootstrap/modal' | 2 | import { ModalDirective } from 'ngx-bootstrap/modal' |
4 | 3 | import { VideoDetails } from '../../shared/video/video-details.model' | |
5 | import { VideoDetails } from '../shared' | ||
6 | 4 | ||
7 | @Component({ | 5 | @Component({ |
8 | selector: 'my-video-download', | 6 | selector: 'my-video-download', |
9 | templateUrl: './video-download.component.html', | 7 | templateUrl: './video-download.component.html', |
10 | styles: [ '.resolution-block { margin-top: 20px; }' ] | 8 | styleUrls: [ './video-download.component.scss' ] |
11 | }) | 9 | }) |
12 | export class VideoDownloadComponent { | 10 | export class VideoDownloadComponent { |
13 | @Input() video: VideoDetails = null | 11 | @Input() video: VideoDetails = null |
diff --git a/client/src/app/videos/+video-watch/video-report.component.html b/client/src/app/videos/+video-watch/video-report.component.html index ceb7cf50a..20474bab4 100644 --- a/client/src/app/videos/+video-watch/video-report.component.html +++ b/client/src/app/videos/+video-watch/video-report.component.html | |||
@@ -6,28 +6,28 @@ | |||
6 | <button type="button" class="close" aria-label="Close" (click)="hide()"> | 6 | <button type="button" class="close" aria-label="Close" (click)="hide()"> |
7 | <span aria-hidden="true">×</span> | 7 | <span aria-hidden="true">×</span> |
8 | </button> | 8 | </button> |
9 | <h4 class="modal-title">Report video</h4> | 9 | <h4 class="title-page title-page-single">Report video</h4> |
10 | </div> | 10 | </div> |
11 | 11 | ||
12 | <div class="modal-body"> | 12 | <div class="modal-body"> |
13 | 13 | ||
14 | <form novalidate [formGroup]="form"> | 14 | <form novalidate [formGroup]="form" (ngSubmit)="report()"> |
15 | <div class="form-group"> | 15 | <div class="form-group"> |
16 | <label for="reason">Reason</label> | 16 | <label for="reason">Reason</label> |
17 | <textarea | 17 | <textarea |
18 | id="reason" class="form-control" placeholder="Reason..." | 18 | id="reason" class="form-control" placeholder="Reason..." |
19 | formControlName="reason" | 19 | formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }" |
20 | > | 20 | > |
21 | </textarea> | 21 | </textarea> |
22 | <div *ngIf="formErrors.reason" class="alert alert-danger"> | 22 | <div *ngIf="formErrors.reason" class="form-error"> |
23 | {{ formErrors.reason }} | 23 | {{ formErrors.reason }} |
24 | </div> | 24 | </div> |
25 | </div> | 25 | </div> |
26 | 26 | ||
27 | <div class="form-group"> | 27 | <div class="form-group"> |
28 | <input | 28 | <input |
29 | type="button" value="Report" class="btn btn-default form-control" | 29 | type="submit" value="Report" class="orange-button" |
30 | [disabled]="!form.valid" (click)="report()" | 30 | [disabled]="!form.valid" |
31 | > | 31 | > |
32 | </div> | 32 | </div> |
33 | </form> | 33 | </form> |
diff --git a/client/src/app/videos/+video-watch/video-report.component.ts b/client/src/app/videos/+video-watch/video-report.component.ts index fc9b5a9d4..b94e4144e 100644 --- a/client/src/app/videos/+video-watch/video-report.component.ts +++ b/client/src/app/videos/+video-watch/video-report.component.ts | |||
@@ -1,11 +1,9 @@ | |||
1 | import { Component, Input, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, Input, OnInit, ViewChild } from '@angular/core' |
2 | import { FormBuilder, FormGroup } from '@angular/forms' | 2 | import { FormBuilder, FormGroup } from '@angular/forms' |
3 | |||
4 | import { ModalDirective } from 'ngx-bootstrap/modal' | ||
5 | import { NotificationsService } from 'angular2-notifications' | 3 | import { NotificationsService } from 'angular2-notifications' |
6 | 4 | import { ModalDirective } from 'ngx-bootstrap/modal' | |
7 | import { FormReactive, VideoAbuseService, VIDEO_ABUSE_REASON } from '../../shared' | 5 | import { FormReactive, VIDEO_ABUSE_REASON, VideoAbuseService } from '../../shared' |
8 | import { VideoDetails, VideoService } from '../shared' | 6 | import { VideoDetails } from '../../shared/video/video-details.model' |
9 | 7 | ||
10 | @Component({ | 8 | @Component({ |
11 | selector: 'my-video-report', | 9 | selector: 'my-video-report', |
diff --git a/client/src/app/videos/+video-watch/video-share.component.html b/client/src/app/videos/+video-watch/video-share.component.html index 88f59c063..36ec38d88 100644 --- a/client/src/app/videos/+video-watch/video-share.component.html +++ b/client/src/app/videos/+video-watch/video-share.component.html | |||
@@ -6,7 +6,7 @@ | |||
6 | <button type="button" class="close" aria-label="Close" (click)="hide()"> | 6 | <button type="button" class="close" aria-label="Close" (click)="hide()"> |
7 | <span aria-hidden="true">×</span> | 7 | <span aria-hidden="true">×</span> |
8 | </button> | 8 | </button> |
9 | <h4 class="modal-title">Share</h4> | 9 | <h4 class="title-page title-page-single">Share</h4> |
10 | </div> | 10 | </div> |
11 | 11 | ||
12 | <div class="modal-body"> | 12 | <div class="modal-body"> |
diff --git a/client/src/app/videos/+video-watch/video-share.component.ts b/client/src/app/videos/+video-watch/video-share.component.ts index aeef65ecf..4df9adf29 100644 --- a/client/src/app/videos/+video-watch/video-share.component.ts +++ b/client/src/app/videos/+video-watch/video-share.component.ts | |||
@@ -1,8 +1,6 @@ | |||
1 | import { Component, Input, ViewChild } from '@angular/core' | 1 | import { Component, Input, ViewChild } from '@angular/core' |
2 | |||
3 | import { ModalDirective } from 'ngx-bootstrap/modal' | 2 | import { ModalDirective } from 'ngx-bootstrap/modal' |
4 | 3 | import { VideoDetails } from '../../shared/video/video-details.model' | |
5 | import { VideoDetails } from '../shared' | ||
6 | 4 | ||
7 | @Component({ | 5 | @Component({ |
8 | selector: 'my-video-share', | 6 | selector: 'my-video-share', |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html index f528d73c3..f99e84caf 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.html +++ b/client/src/app/videos/+video-watch/video-watch.component.html | |||
@@ -1,197 +1,153 @@ | |||
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.serverHost }} 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"> | 1 | <div class="row"> |
17 | <!-- We need the video container for videojs so we just hide it --> | 2 | <!-- We need the video container for videojs so we just hide it --> |
18 | <div [hidden]="videoNotFound" class="embed-responsive embed-responsive-19by9"> | 3 | <div [hidden]="videoNotFound" id="video-container"> |
19 | <video id="video-container" class="video-js vjs-sublime-skin"></video> | 4 | <video id="video-element" class="video-js vjs-peertube-skin vjs-fluid"></video> |
20 | </div> | 5 | </div> |
21 | 6 | ||
22 | <div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div> | 7 | <div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div> |
23 | </div> | ||
24 | |||
25 | <!-- P2P information --> | ||
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 information --> | ||
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-account"> | ||
46 | <a class="option" title="Access to all videos of this user" [routerLink]="['/videos/list', { field: 'account', search: video.account }]"> | ||
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="hidden-xs video-small-block-text">Share</span> | ||
56 | </a> | ||
57 | </div> | ||
58 | 8 | ||
59 | <div class="col-xs-2 col-md-3 video-small-block video-small-block-more"> | 9 | <!-- Video information --> |
60 | <div class="video-small-block-dropdown" dropdown dropup="true" placement="right"> | 10 | <div *ngIf="video" class="margin-content video-bottom"> |
61 | <a class="option" title="Access to more options" dropdownToggle> | 11 | <div class="video-info"> |
62 | <span class="glyphicon glyphicon-option-horizontal"></span> | 12 | <div class="video-info-name-actions"> |
63 | <span class="hidden-xs video-small-block-text">More</span> | 13 | <div class="video-info-name">{{ video.name }}</div> |
64 | </a> | 14 | |
65 | 15 | <div class="video-info-actions"> | |
66 | <ul *dropdownMenu class="dropdown-menu" id="more-menu" role="menu" aria-labelledby="single-button"> | 16 | <div *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" class="action-button"> |
67 | <li *ngIf="canUserUpdateVideo()" role="menuitem"> | 17 | <span class="icon icon-like" title="Like this video" (click)="setLike()"></span> |
68 | <a class="dropdown-item" title="Update this video" href="#" [routerLink]="[ '/videos/edit', video.uuid ]"> | 18 | </div> |
69 | <span class="glyphicon glyphicon-pencil"></span> Update | 19 | |
70 | </a> | 20 | <div *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" class="action-button"> |
71 | </li> | 21 | <span class="icon icon-dislike" title="Dislike this video" (click)="setDislike()"></span> |
72 | 22 | </div> | |
73 | <li role="menuitem"> | 23 | |
74 | <a class="dropdown-item" title="Download the video" href="#" (click)="showDownloadModal($event)"> | 24 | <div (click)="showShareModal()" class="action-button"> |
75 | <span class="glyphicon glyphicon-download-alt"></span> Download | 25 | <span class="icon icon-share"></span> |
76 | </a> | 26 | Share |
77 | </li> | 27 | </div> |
78 | 28 | ||
79 | <li *ngIf="isUserLoggedIn()" role="menuitem"> | 29 | <div class="action-more" dropdown dropup="true" placement="right"> |
80 | <a class="dropdown-item" title="Report this video" href="#" (click)="showReportModal($event)"> | 30 | <div class="action-button" dropdownToggle> |
81 | <span class="glyphicon glyphicon-alert"></span> Report | 31 | <span class="icon icon-more"></span> |
82 | </a> | 32 | </div> |
83 | </li> | 33 | |
84 | 34 | <ul *dropdownMenu class="dropdown-menu" id="more-menu" role="menu" aria-labelledby="single-button"> | |
85 | <li *ngIf="isVideoRemovable()" role="menuitem"> | 35 | <li role="menuitem"> |
86 | <a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)"> | 36 | <a class="dropdown-item" title="Download the video" href="#" (click)="showDownloadModal($event)"> |
87 | <span class="glyphicon glyphicon-remove"></span> Delete | 37 | <span class="icon icon-download"></span> Download |
88 | </a> | 38 | </a> |
89 | </li> | 39 | </li> |
90 | 40 | ||
91 | <li *ngIf="isVideoBlacklistable()" role="menuitem"> | 41 | <li *ngIf="isUserLoggedIn()" role="menuitem"> |
92 | <a class="dropdown-item" title="Blacklist this video" href="#" (click)="blacklistVideo($event)"> | 42 | <a class="dropdown-item" title="Report this video" href="#" (click)="showReportModal($event)"> |
93 | <span class="glyphicon glyphicon-eye-close"></span> Blacklist | 43 | <span class="icon icon-alert"></span> Report |
94 | </a> | 44 | </a> |
95 | </li> | 45 | </li> |
96 | </ul> | 46 | |
47 | <li *ngIf="isVideoBlacklistable()" role="menuitem"> | ||
48 | <a class="dropdown-item" title="Blacklist this video" href="#" (click)="blacklistVideo($event)"> | ||
49 | <span class="icon icon-blacklist"></span> Blacklist | ||
50 | </a> | ||
51 | </li> | ||
52 | </ul> | ||
53 | </div> | ||
54 | </div> | ||
97 | </div> | 55 | </div> |
98 | </div> | ||
99 | 56 | ||
100 | <div class="col-xs-3 col-md-3 video-small-block video-small-block-rating"> | 57 | <div class="video-info-date-views-bar"> |
101 | <div class="video-small-block-like"> | 58 | <div class="video-info-date-views"> |
102 | <span | 59 | {{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views |
103 | class="glyphicon glyphicon-thumbs-up" title="Like this video" | 60 | </div> |
104 | [ngClass]="{ 'interactive': isUserLoggedIn(), 'activated': userRating === 'like' }" (click)="setLike()" | ||
105 | ></span> | ||
106 | 61 | ||
107 | <span class="video-small-block-text"> | 62 | <div *ngIf="video.likes !== 0 || video.dislikes !== 0" class="video-info-likes-dislikes-bar"> |
108 | {{ video.likes }} | 63 | <div class="likes-bar" [ngStyle]="{ 'width.%': video.likesPercent }"></div> |
109 | </span> | 64 | </div> |
110 | </div> | 65 | </div> |
111 | 66 | ||
112 | <div class="video-small-block-dislike"> | 67 | <div class="video-info-channel"> |
113 | <span | 68 | {{ video.channel.name }} |
114 | class="glyphicon glyphicon-thumbs-down" title="Dislike this video" | 69 | <!-- Here will be the subscribe button --> |
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> | 70 | </div> |
122 | </div> | ||
123 | </div> | ||
124 | 71 | ||
125 | <div class="row video-details"> | 72 | <div class="video-info-by"> |
126 | <div class="video-details-date-description col-xs-8 col-md-9"> | 73 | By {{ video.by }} |
127 | <div class="video-details-date"> | 74 | <img [src]="getAvatarPath()" alt="Account avatar" /> |
128 | Published on {{ video.createdAt | date:'short' }} | ||
129 | </div> | 75 | </div> |
130 | 76 | ||
131 | <div class="video-details-description" [innerHTML]="videoHTMLDescription"></div> | 77 | <div class="video-info-description"> |
78 | <div class="video-info-description-html" [innerHTML]="videoHTMLDescription"></div> | ||
132 | 79 | ||
133 | <div class="video-details-description-more" *ngIf="completeDescriptionShown === false && video.description.length === 250" (click)="showMoreDescription()"> | 80 | <div class="video-info-description-more" *ngIf="completeDescriptionShown === false && video.description?.length === 250" (click)="showMoreDescription()"> |
134 | Show more | 81 | Show more |
135 | <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-down"></span> | 82 | <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-down"></span> |
136 | <my-loader class="description-loading" [loading]="descriptionLoading"></my-loader> | 83 | <my-loader class="description-loading" [loading]="descriptionLoading"></my-loader> |
137 | </div> | 84 | </div> |
138 | 85 | ||
139 | <div *ngIf="completeDescriptionShown === true" (click)="showLessDescription()" class="video-details-description-more"> | 86 | <div *ngIf="completeDescriptionShown === true" (click)="showLessDescription()" class="video-info-description-more"> |
140 | Show less | 87 | Show less |
141 | <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-up"></span> | 88 | <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-up"></span> |
89 | </div> | ||
142 | </div> | 90 | </div> |
143 | </div> | ||
144 | 91 | ||
145 | <div class="video-details-attributes col-xs-4 col-md-3"> | 92 | <div class="video-attributes"> |
146 | <div class="video-details-attribute"> | 93 | <div class="video-attribute"> |
147 | <span class="video-details-attribute-label"> | 94 | <span class="video-attribute-label"> |
148 | Privacy: | 95 | Privacy |
149 | </span> | 96 | </span> |
150 | <span class="video-details-attribute-value"> | 97 | <span class="video-attribute-value"> |
151 | {{ video.privacyLabel }} | 98 | {{ video.privacyLabel }} |
152 | </span> | 99 | </span> |
153 | </div> | 100 | </div> |
154 | 101 | ||
155 | <div class="video-details-attribute"> | 102 | <div class="video-attribute"> |
156 | <span class="video-details-attribute-label"> | 103 | <span class="video-attribute-label"> |
157 | Category: | 104 | Category |
158 | </span> | 105 | </span> |
159 | <span class="video-details-attribute-value"> | 106 | <span class="video-attribute-value"> |
160 | {{ video.categoryLabel }} | 107 | {{ video.categoryLabel }} |
161 | </span> | 108 | </span> |
162 | </div> | 109 | </div> |
163 | 110 | ||
164 | <div class="video-details-attribute"> | 111 | <div class="video-attribute"> |
165 | <span class="video-details-attribute-label"> | 112 | <span class="video-attribute-label"> |
166 | Licence: | 113 | Licence |
167 | </span> | 114 | </span> |
168 | <span class="video-details-attribute-value"> | 115 | <span class="video-attribute-value"> |
169 | {{ video.licenceLabel }} | 116 | {{ video.licenceLabel }} |
170 | </span> | 117 | </span> |
171 | </div> | 118 | </div> |
172 | 119 | ||
173 | <div class="video-details-attribute"> | 120 | <div class="video-attribute"> |
174 | <span class="video-details-attribute-label"> | 121 | <span class="video-attribute-label"> |
175 | Language: | 122 | Language |
176 | </span> | 123 | </span> |
177 | <span class="video-details-attribute-value"> | 124 | <span class="video-attribute-value"> |
178 | {{ video.languageLabel }} | 125 | {{ video.languageLabel }} |
179 | </span> | 126 | </span> |
180 | </div> | 127 | </div> |
181 | 128 | ||
182 | <div class="video-details-attribute"> | 129 | <div class="video-attribute"> |
183 | <span class="video-details-attribute-label"> | 130 | <span class="video-attribute-label"> |
184 | Tags: | 131 | Tags |
185 | </span> | 132 | </span> |
186 | 133 | ||
187 | <div class="video-details-tags"> | 134 | <span class="video-attribute-value"> |
188 | <a *ngFor="let tag of video.tags" [routerLink]="['/videos/list', { field: 'tags', search: tag }]" class="label label-primary"> | 135 | {{ getVideoTags() }} |
189 | {{ tag }} | 136 | </span> |
190 | </a> | ||
191 | </div> | 137 | </div> |
192 | </div> | 138 | </div> |
193 | 139 | ||
194 | </div> | 140 | </div> |
141 | |||
142 | <div class="other-videos"> | ||
143 | <div class="title-page title-page-single"> | ||
144 | Other videos | ||
145 | </div> | ||
146 | |||
147 | <div *ngFor="let video of otherVideos"> | ||
148 | <my-video-miniature [video]="video" [user]="user"></my-video-miniature> | ||
149 | </div> | ||
150 | </div> | ||
195 | </div> | 151 | </div> |
196 | </div> | 152 | </div> |
197 | 153 | ||
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss index cad21dd18..9daa757b4 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.scss +++ b/client/src/app/videos/+video-watch/video-watch.component.scss | |||
@@ -1,6 +1,22 @@ | |||
1 | #video-container { | 1 | #video-container { |
2 | width: 100%; | 2 | background-color: #000; |
3 | height: 100%; | 3 | display: flex; |
4 | justify-content: center; | ||
5 | |||
6 | #video-element { | ||
7 | width: 888px; | ||
8 | height: 500px; | ||
9 | |||
10 | @media screen and (max-width: 800px) { | ||
11 | height: auto; | ||
12 | } | ||
13 | |||
14 | // VideoJS create an inner video player | ||
15 | video { | ||
16 | outline: 0; | ||
17 | position: relative !important; | ||
18 | } | ||
19 | } | ||
4 | } | 20 | } |
5 | 21 | ||
6 | #video-not-found { | 22 | #video-not-found { |
@@ -11,175 +27,153 @@ | |||
11 | font-weight: bold; | 27 | font-weight: bold; |
12 | } | 28 | } |
13 | 29 | ||
14 | .embed-responsive { | 30 | .video-bottom { |
15 | height: 500px; | 31 | margin-top: 40px; |
32 | display: flex; | ||
16 | 33 | ||
17 | @media screen and (max-width: 600px) { | 34 | .video-info { |
18 | height: 300px; | 35 | flex-grow: 1; |
19 | } | 36 | margin-right: 28px; |
20 | } | ||
21 | 37 | ||
22 | #torrent-info { | 38 | .video-info-name-actions { |
23 | font-size: 10px; | 39 | display: flex; |
24 | margin-top: 10px; | 40 | align-items: center; |
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 | min-height: $video-watch-title-height; | ||
37 | display: flex; | ||
38 | align-items: center; | ||
39 | |||
40 | .video-name { | ||
41 | padding-left: $video-watch-info-padding-left; | ||
42 | } | ||
43 | 41 | ||
44 | .video-views { | 42 | .video-info-name { |
45 | text-align: right; | 43 | font-size: 27px; |
46 | // Keep a symmetry with the video name | 44 | font-weight: $font-semibold; |
47 | padding-right: $video-watch-info-padding-left | 45 | flex-grow: 1; |
48 | } | 46 | } |
49 | 47 | ||
50 | } | 48 | .video-info-actions { |
49 | .action-button { | ||
50 | @include peertube-button; | ||
51 | @include grey-button; | ||
51 | 52 | ||
52 | .video-small-blocks { | 53 | font-size: 15px; |
53 | height: $video-watch-info-height; | 54 | font-weight: $font-semibold; |
54 | color: $video-watch-info-color; | 55 | display: inline-block; |
55 | border-color: $video-watch-border-color; | 56 | padding: 0 10px 0 10px; |
56 | border-width: 1px 0px; | ||
57 | border-style: solid; | ||
58 | 57 | ||
59 | .video-small-block { | 58 | .icon { |
60 | height: $video-watch-info-height; | 59 | @include icon(21px); |
61 | display: flex; | ||
62 | flex-direction: column; | ||
63 | justify-content: center; | ||
64 | text-align: center; | ||
65 | 60 | ||
66 | a { | 61 | position: relative; |
67 | cursor: pointer; | 62 | top: -2px; |
68 | transition: color 0.3s; | ||
69 | white-space: nowrap; | ||
70 | overflow: hidden; | ||
71 | text-overflow: ellipsis; | ||
72 | |||
73 | &, &:hover { | ||
74 | color: inherit; | ||
75 | text-decoration:none; | ||
76 | } | ||
77 | 63 | ||
78 | &:hover { | 64 | &.icon-like { |
79 | color: #000 !important; | 65 | background-image: url('../../../assets/images/video/like-grey.svg'); |
80 | } | 66 | } |
81 | 67 | ||
82 | &:hover > .glyphicon { | 68 | &.icon-dislike { |
83 | opacity: 1 !important; | 69 | background-image: url('../../../assets/images/video/dislike-grey.svg'); |
84 | } | 70 | } |
85 | } | ||
86 | 71 | ||
87 | .option .glyphicon { | 72 | &.icon-share { |
88 | font-size: 22px; | 73 | background-image: url('../../../assets/images/video/share.svg'); |
89 | color: inherit; | 74 | } |
90 | opacity: 0.15; | ||
91 | margin-bottom: 10px; | ||
92 | transition: opacity 0.3s; | ||
93 | } | ||
94 | 75 | ||
95 | .video-small-block-text { | 76 | &.icon-more { |
96 | font-size: 15px; | 77 | background-image: url('../../../assets/images/video/more.svg'); |
97 | font-weight: bold; | 78 | top: -1px; |
98 | } | 79 | } |
99 | } | 80 | } |
100 | 81 | ||
101 | .video-small-block:not(:last-child) { | 82 | &.activated { |
102 | border-width: 0 1px 0 0; | 83 | @include orange-button; |
103 | border-color: $video-watch-border-color; | ||
104 | border-style: solid; | ||
105 | } | ||
106 | 84 | ||
107 | .video-small-block-account, .video-small-block-more { | 85 | .icon-like { |
108 | a.option { | 86 | background-image: url('../../../assets/images/video/like-white.svg'); |
109 | display: block; | 87 | } |
110 | 88 | ||
111 | .glyphicon { | 89 | .icon-dislike { |
112 | display: block; | 90 | background-image: url('../../../assets/images/video/dislike-white.svg'); |
91 | } | ||
92 | } | ||
113 | } | 93 | } |
114 | } | ||
115 | } | ||
116 | 94 | ||
117 | .video-small-block-share, .video-small-block-more { | 95 | .action-more { |
118 | a.option { | 96 | display: inline-block; |
119 | display: block; | 97 | |
120 | 98 | .dropdown-menu .icon { | |
121 | .glyphicon { | 99 | display: inline-block; |
122 | display: block; | 100 | background-repeat: no-repeat; |
101 | background-size: contain; | ||
102 | width: 21px; | ||
103 | height: 21px; | ||
104 | vertical-align: middle; | ||
105 | margin-right: 5px; | ||
106 | position: relative; | ||
107 | top: -1px; | ||
108 | |||
109 | &.icon-download { | ||
110 | background-image: url('../../../assets/images/video/download-grey.svg'); | ||
111 | } | ||
112 | |||
113 | &.icon-alert { | ||
114 | background-image: url('../../../assets/images/video/alert.svg'); | ||
115 | } | ||
116 | |||
117 | &.icon-blacklist { | ||
118 | background-image: url('../../../assets/images/video/eye-closed.svg'); | ||
119 | } | ||
120 | } | ||
123 | } | 121 | } |
124 | } | 122 | } |
125 | } | 123 | } |
126 | 124 | ||
127 | .video-small-block-more .video-small-block-dropdown { | 125 | .video-info-date-views-bar { |
128 | position: relative; | 126 | display: flex; |
129 | |||
130 | .dropdown-item .glyphicon { | ||
131 | margin-right: 5px; | ||
132 | } | ||
133 | } | ||
134 | |||
135 | .video-small-block-rating { | ||
136 | 127 | ||
137 | .video-small-block-like { | 128 | .video-info-date-views { |
129 | font-size: 16px; | ||
138 | margin-bottom: 10px; | 130 | margin-bottom: 10px; |
131 | flex-grow: 1; | ||
139 | } | 132 | } |
140 | 133 | ||
141 | .video-small-block-text { | 134 | .video-info-likes-dislikes-bar { |
142 | vertical-align: top; | 135 | height: 5px; |
143 | } | 136 | width: 186px; |
137 | background-color: #E5E5E5; | ||
138 | margin-top: 25px; | ||
144 | 139 | ||
145 | .glyphicon { | 140 | .likes-bar { |
146 | font-size: 18px; | 141 | height: 100%; |
147 | margin: 0 10px 0 0; | 142 | background-color: #39CC0B; |
148 | opacity: 0.3; | 143 | } |
149 | } | 144 | } |
145 | } | ||
150 | 146 | ||
151 | .interactive { | 147 | .video-info-channel { |
152 | cursor: pointer; | 148 | font-weight: $font-semibold; |
153 | transition: opacity, color 0.3s; | 149 | font-size: 15px; |
150 | } | ||
154 | 151 | ||
155 | &.activated, &:hover { | 152 | .video-info-by { |
156 | opacity: 1; | 153 | display: flex; |
157 | color: #000; | 154 | align-items: center; |
158 | } | 155 | font-size: 13px; |
156 | |||
157 | img { | ||
158 | width: 16px; | ||
159 | height: 16px; | ||
160 | margin-left: 3px; | ||
159 | } | 161 | } |
160 | } | 162 | } |
161 | } | ||
162 | 163 | ||
163 | .video-details { | 164 | .video-info-description { |
164 | margin-top: 30px; | 165 | margin: 20px 0; |
165 | 166 | font-size: 15px; | |
166 | .video-details-date-description { | ||
167 | padding-left: $video-watch-info-padding-left; | ||
168 | 167 | ||
169 | .description-loading { | 168 | .description-loading { |
170 | display: inline-block; | 169 | display: inline-block; |
171 | } | 170 | } |
172 | 171 | ||
173 | .video-details-date { | 172 | .video-info-description-more { |
174 | font-weight: bold; | ||
175 | margin-bottom: 30px; | ||
176 | } | ||
177 | |||
178 | .video-details-description-more { | ||
179 | cursor: pointer; | 173 | cursor: pointer; |
180 | margin-top: 15px; | 174 | font-weight: $font-semibold; |
181 | font-weight: bold; | 175 | color: #585858; |
182 | color: #acaeb7; | 176 | font-size: 14px; |
183 | 177 | ||
184 | .glyphicon { | 178 | .glyphicon { |
185 | position: relative; | 179 | position: relative; |
@@ -188,109 +182,68 @@ | |||
188 | } | 182 | } |
189 | } | 183 | } |
190 | 184 | ||
191 | .video-details-attributes { | 185 | .video-attributes { |
192 | font-weight: bold; | 186 | .video-attribute { |
193 | font-size: 12px; | 187 | font-size: 13px; |
194 | 188 | display: block; | |
195 | .video-details-attribute { | 189 | margin-bottom: 12px; |
196 | display: flex; | ||
197 | 190 | ||
198 | .video-details-attribute-label { | 191 | .video-attribute-label { |
199 | color: $video-watch-info-color; | 192 | width: 86px; |
200 | flex-basis: 60px; | 193 | display: inline-block; |
201 | flex-grow: 0; | 194 | color: #585858; |
202 | flex-shrink: 0; | 195 | font-weight: $font-bold; |
203 | margin-right: 5px; | ||
204 | } | 196 | } |
205 | } | 197 | } |
206 | } | 198 | } |
207 | |||
208 | .video-details-tags { | ||
209 | display: flex; | ||
210 | flex-wrap: wrap; | ||
211 | |||
212 | a { | ||
213 | margin: 0 3px 3px 0; | ||
214 | font-size: 11px; | ||
215 | } | ||
216 | } | ||
217 | } | 199 | } |
218 | 200 | ||
219 | @media screen and (max-width: 800px) { | 201 | .other-videos { |
220 | .video-name-views { | 202 | .title-page { |
221 | .video-name { | 203 | margin-top: 0; |
222 | padding-left: 5px; | ||
223 | padding-right: 0px; | ||
224 | } | ||
225 | |||
226 | .video-views { | ||
227 | padding-left: 0px; | ||
228 | padding-right: 5px; | ||
229 | } | ||
230 | } | 204 | } |
231 | 205 | ||
232 | .video-small-blocks { | 206 | /deep/ .video-miniature { |
233 | a, .video-small-block-text { | 207 | display: flex; |
234 | font-size: 13px !important; | 208 | height: 100%; |
235 | } | 209 | margin-bottom: 20px; |
236 | |||
237 | .glyphicon { | ||
238 | font-size: 18px !important; | ||
239 | } | ||
240 | 210 | ||
241 | .video-small-block-account { | 211 | .video-miniature-information { |
242 | padding-left: 10px; | 212 | margin-left: 10px; |
243 | padding-right: 10px; | ||
244 | } | 213 | } |
245 | } | 214 | } |
215 | } | ||
216 | } | ||
246 | 217 | ||
247 | .video-details { | ||
248 | .video-details-date-description { | ||
249 | padding-left: 10px; | ||
250 | font-size: 13px !important; | ||
251 | } | ||
252 | |||
253 | .video-details-attributes { | ||
254 | font-size: 11px !important; | ||
255 | 218 | ||
256 | .video-details-attribute-label { | 219 | @media screen and (max-width: 1000px) { |
257 | width: 50px; | 220 | .other-videos { |
258 | } | 221 | display: none; |
259 | } | ||
260 | } | ||
261 | } | 222 | } |
223 | } | ||
262 | 224 | ||
263 | @media screen and (max-width: 500px) { | 225 | @media screen and (max-width: 800px) { |
264 | .video-name-views { | 226 | .video-bottom { |
265 | font-size: 16px !important; | 227 | margin: 20px 0 0 0; |
266 | } | ||
267 | 228 | ||
268 | // Keep the same hierarchy than max-width: 800px | 229 | .video-info { |
269 | .video-small-blocks { | 230 | margin-right: 0; |
270 | a, .video-small-block-text { | ||
271 | font-size: 10px !important; | ||
272 | } | ||
273 | 231 | ||
274 | .video-small-block-account { | 232 | .video-info-name-actions { |
275 | padding-left: 5px; | 233 | align-items: left; |
276 | padding-right: 5px; | 234 | flex-direction: column; |
235 | margin-bottom: 30px; | ||
277 | } | 236 | } |
278 | } | ||
279 | 237 | ||
280 | .video-details { | 238 | .video-info-date-views-bar { |
281 | .video-details-date-description { | 239 | align-items: left; |
240 | flex-direction: column; | ||
282 | margin-bottom: 30px; | 241 | margin-bottom: 30px; |
283 | width: 100%; | ||
284 | 242 | ||
285 | .video-details-date { | 243 | .video-info-likes-dislikes-bar { |
286 | margin-bottom: 15px; | 244 | margin-top: 0; |
287 | } | 245 | } |
288 | } | 246 | } |
289 | |||
290 | .video-details-attributes { | ||
291 | padding-left: 10px; | ||
292 | padding-right: 10px; | ||
293 | } | ||
294 | } | 247 | } |
295 | } | 248 | } |
296 | } | 249 | } |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index b26f3092f..d4e3ec014 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -2,6 +2,7 @@ import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/co | |||
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { MetaService } from '@ngx-meta/core' | 3 | import { MetaService } from '@ngx-meta/core' |
4 | import { NotificationsService } from 'angular2-notifications' | 4 | import { NotificationsService } from 'angular2-notifications' |
5 | import { VideoService } from 'app/shared/video/video.service' | ||
5 | import { Observable } from 'rxjs/Observable' | 6 | import { Observable } from 'rxjs/Observable' |
6 | import { Subscription } from 'rxjs/Subscription' | 7 | import { Subscription } from 'rxjs/Subscription' |
7 | import videojs from 'video.js' | 8 | import videojs from 'video.js' |
@@ -9,7 +10,10 @@ import { UserVideoRateType, VideoRateType } from '../../../../../shared' | |||
9 | import '../../../assets/player/peertube-videojs-plugin' | 10 | import '../../../assets/player/peertube-videojs-plugin' |
10 | import { AuthService, ConfirmService } from '../../core' | 11 | import { AuthService, ConfirmService } from '../../core' |
11 | import { VideoBlacklistService } from '../../shared' | 12 | import { VideoBlacklistService } from '../../shared' |
12 | import { MarkdownService, VideoDetails, VideoService } from '../shared' | 13 | import { Account } from '../../shared/account/account.model' |
14 | import { VideoDetails } from '../../shared/video/video-details.model' | ||
15 | import { Video } from '../../shared/video/video.model' | ||
16 | import { MarkdownService } from '../shared' | ||
13 | import { VideoDownloadComponent } from './video-download.component' | 17 | import { VideoDownloadComponent } from './video-download.component' |
14 | import { VideoReportComponent } from './video-report.component' | 18 | import { VideoReportComponent } from './video-report.component' |
15 | import { VideoShareComponent } from './video-share.component' | 19 | import { VideoShareComponent } from './video-share.component' |
@@ -24,13 +28,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
24 | @ViewChild('videoShareModal') videoShareModal: VideoShareComponent | 28 | @ViewChild('videoShareModal') videoShareModal: VideoShareComponent |
25 | @ViewChild('videoReportModal') videoReportModal: VideoReportComponent | 29 | @ViewChild('videoReportModal') videoReportModal: VideoReportComponent |
26 | 30 | ||
27 | downloadSpeed: number | 31 | otherVideos: Video[] = [] |
32 | |||
28 | error = false | 33 | error = false |
29 | loading = false | 34 | loading = false |
30 | numPeers: number | ||
31 | player: videojs.Player | 35 | player: videojs.Player |
32 | playerElement: HTMLMediaElement | 36 | playerElement: HTMLMediaElement |
33 | uploadSpeed: number | ||
34 | userRating: UserVideoRateType = null | 37 | userRating: UserVideoRateType = null |
35 | video: VideoDetails = null | 38 | video: VideoDetails = null |
36 | videoPlayerLoaded = false | 39 | videoPlayerLoaded = false |
@@ -58,6 +61,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
58 | ) {} | 61 | ) {} |
59 | 62 | ||
60 | ngOnInit () { | 63 | ngOnInit () { |
64 | this.videoService.getVideos({ currentPage: 1, itemsPerPage: 5 }, '-createdAt') | ||
65 | .subscribe( | ||
66 | data => this.otherVideos = data.videos, | ||
67 | |||
68 | err => console.error(err) | ||
69 | ) | ||
70 | |||
61 | this.paramsSub = this.route.params.subscribe(routeParams => { | 71 | this.paramsSub = this.route.params.subscribe(routeParams => { |
62 | let uuid = routeParams['uuid'] | 72 | let uuid = routeParams['uuid'] |
63 | this.videoService.getVideo(uuid).subscribe( | 73 | this.videoService.getVideo(uuid).subscribe( |
@@ -115,27 +125,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
115 | ) | 125 | ) |
116 | } | 126 | } |
117 | 127 | ||
118 | removeVideo (event: Event) { | ||
119 | event.preventDefault() | ||
120 | |||
121 | this.confirmService.confirm('Do you really want to delete this video?', 'Delete').subscribe( | ||
122 | res => { | ||
123 | if (res === false) return | ||
124 | |||
125 | this.videoService.removeVideo(this.video.id) | ||
126 | .subscribe( | ||
127 | status => { | ||
128 | this.notificationsService.success('Success', `Video ${this.video.name} deleted.`) | ||
129 | // Go back to the video-list. | ||
130 | this.router.navigate(['/videos/list']) | ||
131 | }, | ||
132 | |||
133 | error => this.notificationsService.error('Error', error.text) | ||
134 | ) | ||
135 | } | ||
136 | ) | ||
137 | } | ||
138 | |||
139 | blacklistVideo (event: Event) { | 128 | blacklistVideo (event: Event) { |
140 | event.preventDefault() | 129 | event.preventDefault() |
141 | 130 | ||
@@ -166,7 +155,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
166 | } | 155 | } |
167 | 156 | ||
168 | showLessDescription () { | 157 | showLessDescription () { |
169 | |||
170 | this.updateVideoDescription(this.shortVideoDescription) | 158 | this.updateVideoDescription(this.shortVideoDescription) |
171 | this.completeDescriptionShown = false | 159 | this.completeDescriptionShown = false |
172 | } | 160 | } |
@@ -211,16 +199,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
211 | return this.authService.isLoggedIn() | 199 | return this.authService.isLoggedIn() |
212 | } | 200 | } |
213 | 201 | ||
214 | canUserUpdateVideo () { | 202 | isVideoBlacklistable () { |
215 | return this.video.isUpdatableBy(this.authService.getUser()) | 203 | return this.video.isBlackistableBy(this.authService.getUser()) |
216 | } | 204 | } |
217 | 205 | ||
218 | isVideoRemovable () { | 206 | getAvatarPath () { |
219 | return this.video.isRemovableBy(this.authService.getUser()) | 207 | return Account.GET_ACCOUNT_AVATAR_PATH(this.video.account) |
220 | } | 208 | } |
221 | 209 | ||
222 | isVideoBlacklistable () { | 210 | getVideoTags () { |
223 | return this.video.isBlackistableBy(this.authService.getUser()) | 211 | if (!this.video || Array.isArray(this.video.tags) === false) return [] |
212 | |||
213 | return this.video.tags.join(', ') | ||
224 | } | 214 | } |
225 | 215 | ||
226 | private updateVideoDescription (description: string) { | 216 | private updateVideoDescription (description: string) { |
@@ -229,6 +219,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
229 | } | 219 | } |
230 | 220 | ||
231 | private setVideoDescriptionHTML () { | 221 | private setVideoDescriptionHTML () { |
222 | if (!this.video.description) { | ||
223 | this.videoHTMLDescription = '' | ||
224 | return | ||
225 | } | ||
226 | |||
232 | this.videoHTMLDescription = this.markdownService.markdownToHTML(this.video.description) | 227 | this.videoHTMLDescription = this.markdownService.markdownToHTML(this.video.description) |
233 | } | 228 | } |
234 | 229 | ||
@@ -281,7 +276,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
281 | return this.router.navigate([ '/videos/list' ]) | 276 | return this.router.navigate([ '/videos/list' ]) |
282 | } | 277 | } |
283 | 278 | ||
284 | this.playerElement = this.elementRef.nativeElement.querySelector('#video-container') | 279 | this.playerElement = this.elementRef.nativeElement.querySelector('#video-element') |
285 | 280 | ||
286 | const videojsOptions = { | 281 | const videojsOptions = { |
287 | controls: true, | 282 | controls: true, |
@@ -304,12 +299,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
304 | this.on('customError', (event, data) => { | 299 | this.on('customError', (event, data) => { |
305 | self.handleError(data.err) | 300 | self.handleError(data.err) |
306 | }) | 301 | }) |
307 | |||
308 | this.on('torrentInfo', (event, data) => { | ||
309 | self.downloadSpeed = data.downloadSpeed | ||
310 | self.numPeers = data.numPeers | ||
311 | self.uploadSpeed = data.uploadSpeed | ||
312 | }) | ||
313 | }) | 302 | }) |
314 | 303 | ||
315 | this.setVideoDescriptionHTML() | 304 | this.setVideoDescriptionHTML() |
diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts index 1b983200d..0b1dd5c15 100644 --- a/client/src/app/videos/+video-watch/video-watch.module.ts +++ b/client/src/app/videos/+video-watch/video-watch.module.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | 2 | ||
3 | import { VideoWatchRoutingModule } from './video-watch-routing.module' | 3 | import { VideoWatchRoutingModule } from './video-watch-routing.module' |
4 | import { VideoService, MarkdownService } from '../shared' | 4 | import { MarkdownService } from '../shared' |
5 | import { SharedModule } from '../../shared' | 5 | import { SharedModule } from '../../shared' |
6 | 6 | ||
7 | import { VideoWatchComponent } from './video-watch.component' | 7 | import { VideoWatchComponent } from './video-watch.component' |
@@ -28,8 +28,7 @@ import { VideoDownloadComponent } from './video-download.component' | |||
28 | ], | 28 | ], |
29 | 29 | ||
30 | providers: [ | 30 | providers: [ |
31 | MarkdownService, | 31 | MarkdownService |
32 | VideoService | ||
33 | ] | 32 | ] |
34 | }) | 33 | }) |
35 | export class VideoWatchModule { } | 34 | export class VideoWatchModule { } |
diff --git a/client/src/app/videos/shared/index.ts b/client/src/app/videos/shared/index.ts index 3f1458088..7a66944b9 100644 --- a/client/src/app/videos/shared/index.ts +++ b/client/src/app/videos/shared/index.ts | |||
@@ -1,8 +1 @@ | |||
1 | export * from './sort-field.type' | ||
2 | export * from './markdown.service' | export * from './markdown.service' | |
3 | export * from './video.model' | ||
4 | export * from './video-details.model' | ||
5 | export * from './video-edit.model' | ||
6 | export * from './video.service' | ||
7 | export * from './video-description.component' | ||
8 | export * from './video-pagination.model' | ||
diff --git a/client/src/app/videos/shared/video-description.component.scss b/client/src/app/videos/shared/video-description.component.scss deleted file mode 100644 index d8d73e846..000000000 --- a/client/src/app/videos/shared/video-description.component.scss +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | textarea { | ||
2 | height: 150px; | ||
3 | } | ||
4 | |||
5 | .previews /deep/ { | ||
6 | .nav { | ||
7 | margin-top: 10px; | ||
8 | font-size: 0.9em; | ||
9 | } | ||
10 | |||
11 | .tab-content { | ||
12 | min-height: 75px; | ||
13 | padding: 5px; | ||
14 | } | ||
15 | } | ||
diff --git a/client/src/app/videos/video-list/index.ts b/client/src/app/videos/video-list/index.ts index ed2bb1657..5e7c7886c 100644 --- a/client/src/app/videos/video-list/index.ts +++ b/client/src/app/videos/video-list/index.ts | |||
@@ -1,3 +1,3 @@ | |||
1 | export * from './my-videos.component' | 1 | export * from './video-recently-added.component' |
2 | export * from './video-list.component' | 2 | export * from './video-trending.component' |
3 | export * from './shared' | 3 | export * from './video-search.component' |
diff --git a/client/src/app/videos/video-list/my-videos.component.ts b/client/src/app/videos/video-list/my-videos.component.ts deleted file mode 100644 index 648741a40..000000000 --- a/client/src/app/videos/video-list/my-videos.component.ts +++ /dev/null | |||
@@ -1,36 +0,0 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | |||
4 | import { NotificationsService } from 'angular2-notifications' | ||
5 | |||
6 | import { AbstractVideoList } from './shared' | ||
7 | import { VideoService } from '../shared' | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-videos', | ||
11 | styleUrls: [ './shared/abstract-video-list.scss' ], | ||
12 | templateUrl: './shared/abstract-video-list.html' | ||
13 | }) | ||
14 | export class MyVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { | ||
15 | |||
16 | constructor ( | ||
17 | protected router: Router, | ||
18 | protected route: ActivatedRoute, | ||
19 | protected notificationsService: NotificationsService, | ||
20 | private videoService: VideoService | ||
21 | ) { | ||
22 | super() | ||
23 | } | ||
24 | |||
25 | ngOnInit () { | ||
26 | super.ngOnInit() | ||
27 | } | ||
28 | |||
29 | ngOnDestroy () { | ||
30 | this.subActivatedRoute.unsubscribe() | ||
31 | } | ||
32 | |||
33 | getVideosObservable () { | ||
34 | return this.videoService.getMyVideos(this.pagination, this.sort) | ||
35 | } | ||
36 | } | ||
diff --git a/client/src/app/videos/video-list/shared/abstract-video-list.html b/client/src/app/videos/video-list/shared/abstract-video-list.html deleted file mode 100644 index 680fba3f5..000000000 --- a/client/src/app/videos/video-list/shared/abstract-video-list.html +++ /dev/null | |||
@@ -1,28 +0,0 @@ | |||
1 | <div class="row"> | ||
2 | <div class="content-padding"> | ||
3 | <div class="videos-info"> | ||
4 | <div class="col-md-9 col-xs-5 videos-total-results"> | ||
5 | <span *ngIf="pagination.totalItems !== null">{{ pagination.totalItems }} videos</span> | ||
6 | |||
7 | <my-loader [loading]="loading | async"></my-loader> | ||
8 | </div> | ||
9 | |||
10 | <my-video-sort class="col-md-3 col-xs-7" [currentSort]="sort" (sort)="onSort($event)"></my-video-sort> | ||
11 | </div> | ||
12 | </div> | ||
13 | </div> | ||
14 | |||
15 | <div class="content-padding videos-miniatures"> | ||
16 | <div class="no-video" *ngIf="isThereNoVideo()">There is no video.</div> | ||
17 | |||
18 | <my-video-miniature | ||
19 | class="ng-animate" | ||
20 | *ngFor="let video of videos" [video]="video" [user]="user" [currentSort]="sort" | ||
21 | > | ||
22 | </my-video-miniature> | ||
23 | </div> | ||
24 | |||
25 | <pagination *ngIf="pagination.totalItems !== null && pagination.totalItems !== 0" | ||
26 | [totalItems]="pagination.totalItems" [itemsPerPage]="pagination.itemsPerPage" [maxSize]="6" [boundaryLinks]="true" [rotate]="false" | ||
27 | [(ngModel)]="pagination.currentPage" (pageChanged)="onPageChanged($event)" | ||
28 | ></pagination> | ||
diff --git a/client/src/app/videos/video-list/shared/abstract-video-list.scss b/client/src/app/videos/video-list/shared/abstract-video-list.scss deleted file mode 100644 index 4b4409602..000000000 --- a/client/src/app/videos/video-list/shared/abstract-video-list.scss +++ /dev/null | |||
@@ -1,37 +0,0 @@ | |||
1 | .videos-info { | ||
2 | @media screen and (max-width: 400px) { | ||
3 | margin-left: 0; | ||
4 | } | ||
5 | |||
6 | border-bottom: 1px solid #f1f1f1; | ||
7 | height: 40px; | ||
8 | line-height: 40px; | ||
9 | |||
10 | .videos-total-results { | ||
11 | font-size: 13px; | ||
12 | } | ||
13 | |||
14 | my-loader { | ||
15 | display: inline-block; | ||
16 | margin-left: 5px; | ||
17 | } | ||
18 | } | ||
19 | |||
20 | .videos-miniatures { | ||
21 | text-align: center; | ||
22 | padding-top: 0; | ||
23 | |||
24 | my-video-miniature { | ||
25 | text-align: left; | ||
26 | } | ||
27 | |||
28 | .no-video { | ||
29 | margin-top: 50px; | ||
30 | text-align: center; | ||
31 | } | ||
32 | } | ||
33 | |||
34 | pagination { | ||
35 | display: block; | ||
36 | text-align: center; | ||
37 | } | ||
diff --git a/client/src/app/videos/video-list/shared/abstract-video-list.ts b/client/src/app/videos/video-list/shared/abstract-video-list.ts deleted file mode 100644 index 87d5bc48a..000000000 --- a/client/src/app/videos/video-list/shared/abstract-video-list.ts +++ /dev/null | |||
@@ -1,104 +0,0 @@ | |||
1 | import { OnDestroy, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { Subscription } from 'rxjs/Subscription' | ||
4 | import { BehaviorSubject } from 'rxjs/BehaviorSubject' | ||
5 | import { Observable } from 'rxjs/Observable' | ||
6 | |||
7 | import { NotificationsService } from 'angular2-notifications' | ||
8 | |||
9 | import { | ||
10 | SortField, | ||
11 | Video, | ||
12 | VideoPagination | ||
13 | } from '../../shared' | ||
14 | |||
15 | export abstract class AbstractVideoList implements OnInit, OnDestroy { | ||
16 | loading: BehaviorSubject<boolean> = new BehaviorSubject(false) | ||
17 | pagination: VideoPagination = { | ||
18 | currentPage: 1, | ||
19 | itemsPerPage: 25, | ||
20 | totalItems: null | ||
21 | } | ||
22 | sort: SortField | ||
23 | videos: Video[] = [] | ||
24 | |||
25 | protected notificationsService: NotificationsService | ||
26 | protected router: Router | ||
27 | protected route: ActivatedRoute | ||
28 | |||
29 | protected subActivatedRoute: Subscription | ||
30 | |||
31 | abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}> | ||
32 | |||
33 | ngOnInit () { | ||
34 | // Subscribe to route changes | ||
35 | this.subActivatedRoute = this.route.params.subscribe(routeParams => { | ||
36 | this.loadRouteParams(routeParams) | ||
37 | |||
38 | this.getVideos() | ||
39 | }) | ||
40 | } | ||
41 | |||
42 | ngOnDestroy () { | ||
43 | this.subActivatedRoute.unsubscribe() | ||
44 | } | ||
45 | |||
46 | getVideos () { | ||
47 | this.loading.next(true) | ||
48 | this.videos = [] | ||
49 | |||
50 | const observable = this.getVideosObservable() | ||
51 | |||
52 | observable.subscribe( | ||
53 | ({ videos, totalVideos }) => { | ||
54 | this.videos = videos | ||
55 | this.pagination.totalItems = totalVideos | ||
56 | |||
57 | this.loading.next(false) | ||
58 | }, | ||
59 | error => this.notificationsService.error('Error', error.text) | ||
60 | ) | ||
61 | } | ||
62 | |||
63 | isThereNoVideo () { | ||
64 | return !this.loading.getValue() && this.videos.length === 0 | ||
65 | } | ||
66 | |||
67 | onPageChanged (event: { page: number }) { | ||
68 | // Be sure the current page is set | ||
69 | this.pagination.currentPage = event.page | ||
70 | |||
71 | this.navigateToNewParams() | ||
72 | } | ||
73 | |||
74 | onSort (sort: SortField) { | ||
75 | this.sort = sort | ||
76 | |||
77 | this.navigateToNewParams() | ||
78 | } | ||
79 | |||
80 | protected buildRouteParams () { | ||
81 | // There is always a sort and a current page | ||
82 | const params = { | ||
83 | sort: this.sort, | ||
84 | page: this.pagination.currentPage | ||
85 | } | ||
86 | |||
87 | return params | ||
88 | } | ||
89 | |||
90 | protected loadRouteParams (routeParams: { [ key: string ]: any }) { | ||
91 | this.sort = routeParams['sort'] as SortField || '-createdAt' | ||
92 | |||
93 | if (routeParams['page'] !== undefined) { | ||
94 | this.pagination.currentPage = parseInt(routeParams['page'], 10) | ||
95 | } else { | ||
96 | this.pagination.currentPage = 1 | ||
97 | } | ||
98 | } | ||
99 | |||
100 | protected navigateToNewParams () { | ||
101 | const routeParams = this.buildRouteParams() | ||
102 | this.router.navigate([ '/videos/list', routeParams ]) | ||
103 | } | ||
104 | } | ||
diff --git a/client/src/app/videos/video-list/shared/index.ts b/client/src/app/videos/video-list/shared/index.ts deleted file mode 100644 index d8f73bcda..000000000 --- a/client/src/app/videos/video-list/shared/index.ts +++ /dev/null | |||
@@ -1,3 +0,0 @@ | |||
1 | export * from './abstract-video-list' | ||
2 | export * from './video-miniature.component' | ||
3 | export * from './video-sort.component' | ||
diff --git a/client/src/app/videos/video-list/shared/video-miniature.component.html b/client/src/app/videos/video-list/shared/video-miniature.component.html deleted file mode 100644 index 6bbd29666..000000000 --- a/client/src/app/videos/video-list/shared/video-miniature.component.html +++ /dev/null | |||
@@ -1,33 +0,0 @@ | |||
1 | <div class="video-miniature"> | ||
2 | <a | ||
3 | [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.description" | ||
4 | class="video-miniature-thumbnail" | ||
5 | > | ||
6 | <img [attr.src]="video.thumbnailUrl" alt="video thumbnail" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }" /> | ||
7 | |||
8 | <div class="video-miniature-thumbnail-overlay"> | ||
9 | <span class="video-miniature-thumbnail-overlay-views">{{ video.views }} views</span> | ||
10 | <span class="video-miniature-thumbnail-overlay-duration">{{ video.durationLabel }}</span> | ||
11 | </div> | ||
12 | </a> | ||
13 | |||
14 | <div class="video-miniature-information"> | ||
15 | <span class="video-miniature-name"> | ||
16 | <a | ||
17 | class="video-miniature-name" | ||
18 | [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }" | ||
19 | > | ||
20 | {{ video.name }} | ||
21 | </a> | ||
22 | </span> | ||
23 | |||
24 | <div class="video-miniature-tags"> | ||
25 | <span *ngFor="let tag of video.tags" class="video-miniature-tag"> | ||
26 | <a [routerLink]="['/videos/list', { field: 'tags', search: tag, sort: currentSort }]" class="label label-primary">{{ tag }}</a> | ||
27 | </span> | ||
28 | </div> | ||
29 | |||
30 | <a [routerLink]="['/videos/list', { field: 'account', search: video.account, sort: currentSort }]" class="video-miniature-account">{{ video.by }}</a> | ||
31 | <span class="video-miniature-created-at">{{ video.createdAt | date:'short' }}</span> | ||
32 | </div> | ||
33 | </div> | ||
diff --git a/client/src/app/videos/video-list/shared/video-miniature.component.scss b/client/src/app/videos/video-list/shared/video-miniature.component.scss deleted file mode 100644 index 507ace098..000000000 --- a/client/src/app/videos/video-list/shared/video-miniature.component.scss +++ /dev/null | |||
@@ -1,101 +0,0 @@ | |||
1 | .video-miniature { | ||
2 | margin: 15px 10px; | ||
3 | display: inline-block; | ||
4 | position: relative; | ||
5 | height: 190px; | ||
6 | vertical-align: top; | ||
7 | |||
8 | .video-miniature-thumbnail { | ||
9 | display: inline-block; | ||
10 | position: relative; | ||
11 | border-radius: 3px; | ||
12 | overflow: hidden; | ||
13 | |||
14 | &:hover { | ||
15 | text-decoration: none !important; | ||
16 | } | ||
17 | |||
18 | img.blur-filter { | ||
19 | filter: blur(5px); | ||
20 | transform : scale(1.03); | ||
21 | } | ||
22 | |||
23 | .video-miniature-thumbnail-overlay { | ||
24 | position: absolute; | ||
25 | right: 0px; | ||
26 | bottom: 0px; | ||
27 | display: inline-block; | ||
28 | background-color: rgba(0, 0, 0, 0.7); | ||
29 | color: #fff; | ||
30 | padding: 3px 5px; | ||
31 | font-size: 11px; | ||
32 | font-weight: bold; | ||
33 | width: 100%; | ||
34 | |||
35 | .video-miniature-thumbnail-overlay-views { | ||
36 | |||
37 | } | ||
38 | |||
39 | .video-miniature-thumbnail-overlay-duration { | ||
40 | float: right; | ||
41 | } | ||
42 | } | ||
43 | } | ||
44 | |||
45 | .video-miniature-information { | ||
46 | width: 200px; | ||
47 | |||
48 | .video-miniature-name { | ||
49 | height: 23px; | ||
50 | display: block; | ||
51 | overflow: hidden; | ||
52 | text-overflow: ellipsis; | ||
53 | white-space: nowrap; | ||
54 | font-weight: bold; | ||
55 | transition: color 0.2s; | ||
56 | font-size: 15px; | ||
57 | |||
58 | &:hover { | ||
59 | text-decoration: none; | ||
60 | } | ||
61 | |||
62 | &.blur-filter { | ||
63 | filter: blur(3px); | ||
64 | padding-left: 4px; | ||
65 | } | ||
66 | |||
67 | .video-miniature-tags { | ||
68 | // Fix for chrome when tags are long | ||
69 | width: 201px; | ||
70 | |||
71 | .video-miniature-tag { | ||
72 | font-size: 13px; | ||
73 | cursor: pointer; | ||
74 | position: relative; | ||
75 | top: -2px; | ||
76 | |||
77 | .label { | ||
78 | transition: background-color 0.2s; | ||
79 | } | ||
80 | } | ||
81 | } | ||
82 | } | ||
83 | |||
84 | .video-miniature-account, .video-miniature-created-at { | ||
85 | display: block; | ||
86 | margin-left: 1px; | ||
87 | font-size: 11px; | ||
88 | color: $video-miniature-other-infos; | ||
89 | opacity: 0.9; | ||
90 | } | ||
91 | |||
92 | .video-miniature-account { | ||
93 | transition: color 0.2s; | ||
94 | |||
95 | &:hover { | ||
96 | color: #23527c; | ||
97 | text-decoration: none; | ||
98 | } | ||
99 | } | ||
100 | } | ||
101 | } | ||
diff --git a/client/src/app/videos/video-list/shared/video-sort.component.html b/client/src/app/videos/video-list/shared/video-sort.component.html deleted file mode 100644 index 3bece0b22..000000000 --- a/client/src/app/videos/video-list/shared/video-sort.component.html +++ /dev/null | |||
@@ -1,5 +0,0 @@ | |||
1 | <select class="form-control input-sm" [(ngModel)]="currentSort" (ngModelChange)="onSortChange()"> | ||
2 | <option *ngFor="let choice of choiceKeys" [value]="choice"> | ||
3 | {{ getStringChoice(choice) }} | ||
4 | </option> | ||
5 | </select> | ||
diff --git a/client/src/app/videos/video-list/shared/video-sort.component.ts b/client/src/app/videos/video-list/shared/video-sort.component.ts deleted file mode 100644 index 8aa89d32b..000000000 --- a/client/src/app/videos/video-list/shared/video-sort.component.ts +++ /dev/null | |||
@@ -1,39 +0,0 @@ | |||
1 | import { Component, EventEmitter, Input, Output } from '@angular/core' | ||
2 | |||
3 | import { SortField } from '../../shared' | ||
4 | |||
5 | @Component({ | ||
6 | selector: 'my-video-sort', | ||
7 | templateUrl: './video-sort.component.html' | ||
8 | }) | ||
9 | |||
10 | export class VideoSortComponent { | ||
11 | @Output() sort = new EventEmitter<any>() | ||
12 | |||
13 | @Input() currentSort: SortField | ||
14 | |||
15 | sortChoices: { [ P in SortField ]: string } = { | ||
16 | 'name': 'Name - Asc', | ||
17 | '-name': 'Name - Desc', | ||
18 | 'duration': 'Duration - Asc', | ||
19 | '-duration': 'Duration - Desc', | ||
20 | 'createdAt': 'Created Date - Asc', | ||
21 | '-createdAt': 'Created Date - Desc', | ||
22 | 'views': 'Views - Asc', | ||
23 | '-views': 'Views - Desc', | ||
24 | 'likes': 'Likes - Asc', | ||
25 | '-likes': 'Likes - Desc' | ||
26 | } | ||
27 | |||
28 | get choiceKeys () { | ||
29 | return Object.keys(this.sortChoices) | ||
30 | } | ||
31 | |||
32 | getStringChoice (choiceKey: SortField) { | ||
33 | return this.sortChoices[choiceKey] | ||
34 | } | ||
35 | |||
36 | onSortChange () { | ||
37 | this.sort.emit(this.currentSort) | ||
38 | } | ||
39 | } | ||
diff --git a/client/src/app/videos/video-list/video-list.component.ts b/client/src/app/videos/video-list/video-list.component.ts deleted file mode 100644 index 784162679..000000000 --- a/client/src/app/videos/video-list/video-list.component.ts +++ /dev/null | |||
@@ -1,94 +0,0 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { Subscription } from 'rxjs/Subscription' | ||
4 | |||
5 | import { NotificationsService } from 'angular2-notifications' | ||
6 | |||
7 | import { VideoService } from '../shared' | ||
8 | import { Search, SearchField, SearchService } from '../../shared' | ||
9 | import { AbstractVideoList } from './shared' | ||
10 | |||
11 | @Component({ | ||
12 | selector: 'my-videos-list', | ||
13 | styleUrls: [ './shared/abstract-video-list.scss' ], | ||
14 | templateUrl: './shared/abstract-video-list.html' | ||
15 | }) | ||
16 | export class VideoListComponent extends AbstractVideoList implements OnInit, OnDestroy { | ||
17 | private search: Search | ||
18 | private subSearch: Subscription | ||
19 | |||
20 | constructor ( | ||
21 | protected router: Router, | ||
22 | protected route: ActivatedRoute, | ||
23 | protected notificationsService: NotificationsService, | ||
24 | private videoService: VideoService, | ||
25 | private searchService: SearchService | ||
26 | ) { | ||
27 | super() | ||
28 | } | ||
29 | |||
30 | ngOnInit () { | ||
31 | // Subscribe to route changes | ||
32 | this.subActivatedRoute = this.route.params.subscribe(routeParams => { | ||
33 | this.loadRouteParams(routeParams) | ||
34 | |||
35 | // Update the search service component | ||
36 | this.searchService.updateSearch.next(this.search) | ||
37 | this.getVideos() | ||
38 | }) | ||
39 | |||
40 | // Subscribe to search changes | ||
41 | this.subSearch = this.searchService.searchUpdated.subscribe(search => { | ||
42 | this.search = search | ||
43 | // Reset pagination | ||
44 | this.pagination.currentPage = 1 | ||
45 | |||
46 | this.navigateToNewParams() | ||
47 | }) | ||
48 | } | ||
49 | |||
50 | ngOnDestroy () { | ||
51 | super.ngOnDestroy() | ||
52 | |||
53 | this.subSearch.unsubscribe() | ||
54 | } | ||
55 | |||
56 | getVideosObservable () { | ||
57 | let observable = null | ||
58 | if (this.search.value) { | ||
59 | observable = this.videoService.searchVideos(this.search, this.pagination, this.sort) | ||
60 | } else { | ||
61 | observable = this.videoService.getVideos(this.pagination, this.sort) | ||
62 | } | ||
63 | |||
64 | return observable | ||
65 | } | ||
66 | |||
67 | protected buildRouteParams () { | ||
68 | const params = super.buildRouteParams() | ||
69 | |||
70 | // Maybe there is a search | ||
71 | if (this.search.value) { | ||
72 | params['field'] = this.search.field | ||
73 | params['search'] = this.search.value | ||
74 | } | ||
75 | |||
76 | return params | ||
77 | } | ||
78 | |||
79 | protected loadRouteParams (routeParams: { [ key: string ]: any }) { | ||
80 | super.loadRouteParams(routeParams) | ||
81 | |||
82 | if (routeParams['search'] !== undefined) { | ||
83 | this.search = { | ||
84 | value: routeParams['search'], | ||
85 | field: routeParams['field'] as SearchField | ||
86 | } | ||
87 | } else { | ||
88 | this.search = { | ||
89 | value: '', | ||
90 | field: 'name' | ||
91 | } | ||
92 | } | ||
93 | } | ||
94 | } | ||
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts new file mode 100644 index 000000000..6168fac95 --- /dev/null +++ b/client/src/app/videos/video-list/video-recently-added.component.ts | |||
@@ -0,0 +1,32 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { NotificationsService } from 'angular2-notifications' | ||
4 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | ||
5 | import { SortField } from '../../shared/video/sort-field.type' | ||
6 | import { VideoService } from '../../shared/video/video.service' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-videos-recently-added', | ||
10 | styleUrls: [ '../../shared/video/abstract-video-list.scss' ], | ||
11 | templateUrl: '../../shared/video/abstract-video-list.html' | ||
12 | }) | ||
13 | export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit { | ||
14 | titlePage = 'Recently added' | ||
15 | currentRoute = '/videos/recently-added' | ||
16 | sort: SortField = '-createdAt' | ||
17 | |||
18 | constructor (protected router: Router, | ||
19 | protected route: ActivatedRoute, | ||
20 | protected notificationsService: NotificationsService, | ||
21 | private videoService: VideoService) { | ||
22 | super() | ||
23 | } | ||
24 | |||
25 | ngOnInit () { | ||
26 | super.ngOnInit() | ||
27 | } | ||
28 | |||
29 | getVideosObservable () { | ||
30 | return this.videoService.getVideos(this.pagination, this.sort) | ||
31 | } | ||
32 | } | ||
diff --git a/client/src/app/videos/video-list/video-search.component.ts b/client/src/app/videos/video-list/video-search.component.ts new file mode 100644 index 000000000..ba851d27e --- /dev/null +++ b/client/src/app/videos/video-list/video-search.component.ts | |||
@@ -0,0 +1,51 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { NotificationsService } from 'angular2-notifications' | ||
4 | import { AbstractVideoList } from 'app/shared/video/abstract-video-list' | ||
5 | import { Subscription } from 'rxjs/Subscription' | ||
6 | import { SortField } from '../../shared/video/sort-field.type' | ||
7 | import { VideoService } from '../../shared/video/video.service' | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-videos-search', | ||
11 | styleUrls: [ '../../shared/video/abstract-video-list.scss' ], | ||
12 | templateUrl: '../../shared/video/abstract-video-list.html' | ||
13 | }) | ||
14 | export class VideoSearchComponent extends AbstractVideoList implements OnInit, OnDestroy { | ||
15 | titlePage = 'Search' | ||
16 | currentRoute = '/videos/search' | ||
17 | loadOnInit = false | ||
18 | |||
19 | private search = '' | ||
20 | private subActivatedRoute: Subscription | ||
21 | |||
22 | constructor (protected router: Router, | ||
23 | protected route: ActivatedRoute, | ||
24 | protected notificationsService: NotificationsService, | ||
25 | private videoService: VideoService) { | ||
26 | super() | ||
27 | } | ||
28 | |||
29 | ngOnInit () { | ||
30 | super.ngOnInit() | ||
31 | |||
32 | this.subActivatedRoute = this.route.queryParams.subscribe( | ||
33 | queryParams => { | ||
34 | this.search = queryParams['search'] | ||
35 | this.reloadVideos() | ||
36 | }, | ||
37 | |||
38 | err => this.notificationsService.error('Error', err.text) | ||
39 | ) | ||
40 | } | ||
41 | |||
42 | ngOnDestroy () { | ||
43 | if (this.subActivatedRoute) { | ||
44 | this.subActivatedRoute.unsubscribe() | ||
45 | } | ||
46 | } | ||
47 | |||
48 | getVideosObservable () { | ||
49 | return this.videoService.searchVideos(this.search, this.pagination, this.sort) | ||
50 | } | ||
51 | } | ||
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts new file mode 100644 index 000000000..e80fd7f2c --- /dev/null +++ b/client/src/app/videos/video-list/video-trending.component.ts | |||
@@ -0,0 +1,32 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { NotificationsService } from 'angular2-notifications' | ||
4 | import { AbstractVideoList } from 'app/shared/video/abstract-video-list' | ||
5 | import { SortField } from '../../shared/video/sort-field.type' | ||
6 | import { VideoService } from '../../shared/video/video.service' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-videos-trending', | ||
10 | styleUrls: [ '../../shared/video/abstract-video-list.scss' ], | ||
11 | templateUrl: '../../shared/video/abstract-video-list.html' | ||
12 | }) | ||
13 | export class VideoTrendingComponent extends AbstractVideoList implements OnInit { | ||
14 | titlePage = 'Trending' | ||
15 | currentRoute = '/videos/trending' | ||
16 | defaultSort: SortField = '-views' | ||
17 | |||
18 | constructor (protected router: Router, | ||
19 | protected route: ActivatedRoute, | ||
20 | protected notificationsService: NotificationsService, | ||
21 | private videoService: VideoService) { | ||
22 | super() | ||
23 | } | ||
24 | |||
25 | ngOnInit () { | ||
26 | super.ngOnInit() | ||
27 | } | ||
28 | |||
29 | getVideosObservable () { | ||
30 | return this.videoService.getVideos(this.pagination, this.sort) | ||
31 | } | ||
32 | } | ||
diff --git a/client/src/app/videos/videos-routing.module.ts b/client/src/app/videos/videos-routing.module.ts index 3ca3e5486..6910421b7 100644 --- a/client/src/app/videos/videos-routing.module.ts +++ b/client/src/app/videos/videos-routing.module.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | |||
4 | import { MetaGuard } from '@ngx-meta/core' | 3 | import { MetaGuard } from '@ngx-meta/core' |
5 | 4 | import { VideoSearchComponent } from './video-list' | |
6 | import { VideoListComponent, MyVideosComponent } from './video-list' | 5 | import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component' |
6 | import { VideoTrendingComponent } from './video-list/video-trending.component' | ||
7 | import { VideosComponent } from './videos.component' | 7 | import { VideosComponent } from './videos.component' |
8 | 8 | ||
9 | const videosRoutes: Routes = [ | 9 | const videosRoutes: Routes = [ |
@@ -13,20 +13,34 @@ const videosRoutes: Routes = [ | |||
13 | canActivateChild: [ MetaGuard ], | 13 | canActivateChild: [ MetaGuard ], |
14 | children: [ | 14 | children: [ |
15 | { | 15 | { |
16 | path: 'mine', | 16 | path: 'list', |
17 | component: MyVideosComponent, | 17 | pathMatch: 'full', |
18 | redirectTo: 'recently-added' | ||
19 | }, | ||
20 | { | ||
21 | path: 'trending', | ||
22 | component: VideoTrendingComponent, | ||
18 | data: { | 23 | data: { |
19 | meta: { | 24 | meta: { |
20 | title: 'My videos' | 25 | title: 'Trending videos' |
21 | } | 26 | } |
22 | } | 27 | } |
23 | }, | 28 | }, |
24 | { | 29 | { |
25 | path: 'list', | 30 | path: 'recently-added', |
26 | component: VideoListComponent, | 31 | component: VideoRecentlyAddedComponent, |
32 | data: { | ||
33 | meta: { | ||
34 | title: 'Recently added videos' | ||
35 | } | ||
36 | } | ||
37 | }, | ||
38 | { | ||
39 | path: 'search', | ||
40 | component: VideoSearchComponent, | ||
27 | data: { | 41 | data: { |
28 | meta: { | 42 | meta: { |
29 | title: 'Videos list' | 43 | title: 'Search videos' |
30 | } | 44 | } |
31 | } | 45 | } |
32 | }, | 46 | }, |
@@ -50,6 +64,7 @@ const videosRoutes: Routes = [ | |||
50 | }, | 64 | }, |
51 | { | 65 | { |
52 | path: ':uuid', | 66 | path: ':uuid', |
67 | pathMatch: 'full', | ||
53 | redirectTo: 'watch/:uuid' | 68 | redirectTo: 'watch/:uuid' |
54 | }, | 69 | }, |
55 | { | 70 | { |
diff --git a/client/src/app/videos/videos.module.ts b/client/src/app/videos/videos.module.ts index 4f3054c3a..4b14d1da8 100644 --- a/client/src/app/videos/videos.module.ts +++ b/client/src/app/videos/videos.module.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { SharedModule } from '../shared' | 2 | import { SharedModule } from '../shared' |
3 | import { VideoService } from './shared' | 3 | import { VideoSearchComponent } from './video-list' |
4 | import { MyVideosComponent, VideoListComponent, VideoMiniatureComponent, VideoSortComponent } from './video-list' | 4 | import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component' |
5 | import { VideoTrendingComponent } from './video-list/video-trending.component' | ||
5 | import { VideosRoutingModule } from './videos-routing.module' | 6 | import { VideosRoutingModule } from './videos-routing.module' |
6 | import { VideosComponent } from './videos.component' | 7 | import { VideosComponent } from './videos.component' |
7 | 8 | ||
@@ -14,18 +15,15 @@ import { VideosComponent } from './videos.component' | |||
14 | declarations: [ | 15 | declarations: [ |
15 | VideosComponent, | 16 | VideosComponent, |
16 | 17 | ||
17 | VideoListComponent, | 18 | VideoTrendingComponent, |
18 | MyVideosComponent, | 19 | VideoRecentlyAddedComponent, |
19 | VideoMiniatureComponent, | 20 | VideoSearchComponent |
20 | VideoSortComponent | ||
21 | ], | 21 | ], |
22 | 22 | ||
23 | exports: [ | 23 | exports: [ |
24 | VideosComponent | 24 | VideosComponent |
25 | ], | 25 | ], |
26 | 26 | ||
27 | providers: [ | 27 | providers: [] |
28 | VideoService | ||
29 | ] | ||
30 | }) | 28 | }) |
31 | export class VideosModule { } | 29 | export class VideosModule { } |
diff --git a/client/src/assets/favicon.png b/client/src/assets/favicon.png deleted file mode 100644 index bb57ee6b0..000000000 --- a/client/src/assets/favicon.png +++ /dev/null | |||
Binary files differ | |||
diff --git a/client/src/assets/images/admin/add.svg b/client/src/assets/images/admin/add.svg new file mode 100644 index 000000000..42b269c43 --- /dev/null +++ b/client/src/assets/images/admin/add.svg | |||
@@ -0,0 +1,13 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
5 | <g id="Artboard-4" transform="translate(-92.000000, -115.000000)"> | ||
6 | <g id="2" transform="translate(92.000000, 115.000000)"> | ||
7 | <circle id="Oval-1" stroke="#ffffff" stroke-width="2" cx="12" cy="12" r="10"></circle> | ||
8 | <rect id="Rectangle-1" fill="#ffffff" x="11" y="7" width="2" height="10" rx="1"></rect> | ||
9 | <rect id="Rectangle-1" fill="#ffffff" x="7" y="11" width="10" height="2" rx="1"></rect> | ||
10 | </g> | ||
11 | </g> | ||
12 | </g> | ||
13 | </svg> | ||
diff --git a/client/src/assets/images/default-avatar.png b/client/src/assets/images/default-avatar.png new file mode 100644 index 000000000..4b7fd2c0a --- /dev/null +++ b/client/src/assets/images/default-avatar.png | |||
Binary files differ | |||
diff --git a/client/src/assets/images/favicon.png b/client/src/assets/images/favicon.png new file mode 100644 index 000000000..2e589cf6c --- /dev/null +++ b/client/src/assets/images/favicon.png | |||
Binary files differ | |||
diff --git a/client/src/assets/images/global/delete-grey.svg b/client/src/assets/images/global/delete-grey.svg new file mode 100644 index 000000000..67e9e2ce7 --- /dev/null +++ b/client/src/assets/images/global/delete-grey.svg | |||
@@ -0,0 +1,14 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
5 | <g id="Artboard-4" transform="translate(-224.000000, -159.000000)"> | ||
6 | <g id="25" transform="translate(224.000000, 159.000000)"> | ||
7 | <path d="M5,7 L5,20.0081158 C5,21.1082031 5.89706013,22 7.00585866,22 L16.9941413,22 C18.1019465,22 19,21.1066027 19,20.0081158 L19,7" id="Path-296" stroke="#585858" stroke-width="2"></path> | ||
8 | <rect id="Rectangle-424" fill="#585858" x="2" y="4" width="20" height="2" rx="1"></rect> | ||
9 | <path d="M9,10.9970301 C9,10.4463856 9.44386482,10 10,10 C10.5522847,10 11,10.4530363 11,10.9970301 L11,17.0029699 C11,17.5536144 10.5561352,18 10,18 C9.44771525,18 9,17.5469637 9,17.0029699 L9,10.9970301 Z M13,10.9970301 C13,10.4463856 13.4438648,10 14,10 C14.5522847,10 15,10.4530363 15,10.9970301 L15,17.0029699 C15,17.5536144 14.5561352,18 14,18 C13.4477153,18 13,17.5469637 13,17.0029699 L13,10.9970301 Z" id="Combined-Shape" fill="#585858"></path> | ||
10 | <path d="M9,5 L9,2.99895656 C9,2.44724809 9.45097518,2 9.99077797,2 L14.009222,2 C14.5564136,2 15,2.44266033 15,2.99895656 L15,5" id="Path-33" stroke="#585858" stroke-width="2" stroke-linejoin="round"></path> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> | ||
diff --git a/client/src/assets/images/global/delete-white.svg b/client/src/assets/images/global/delete-white.svg new file mode 100644 index 000000000..9c52de557 --- /dev/null +++ b/client/src/assets/images/global/delete-white.svg | |||
@@ -0,0 +1,14 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
5 | <g id="Artboard-4" transform="translate(-224.000000, -159.000000)"> | ||
6 | <g id="25" transform="translate(224.000000, 159.000000)"> | ||
7 | <path d="M5,7 L5,20.0081158 C5,21.1082031 5.89706013,22 7.00585866,22 L16.9941413,22 C18.1019465,22 19,21.1066027 19,20.0081158 L19,7" id="Path-296" stroke="#ffffff" stroke-width="2"></path> | ||
8 | <rect id="Rectangle-424" fill="#ffffff" x="2" y="4" width="20" height="2" rx="1"></rect> | ||
9 | <path d="M9,10.9970301 C9,10.4463856 9.44386482,10 10,10 C10.5522847,10 11,10.4530363 11,10.9970301 L11,17.0029699 C11,17.5536144 10.5561352,18 10,18 C9.44771525,18 9,17.5469637 9,17.0029699 L9,10.9970301 Z M13,10.9970301 C13,10.4463856 13.4438648,10 14,10 C14.5522847,10 15,10.4530363 15,10.9970301 L15,17.0029699 C15,17.5536144 14.5561352,18 14,18 C13.4477153,18 13,17.5469637 13,17.0029699 L13,10.9970301 Z" id="Combined-Shape" fill="#ffffff"></path> | ||
10 | <path d="M9,5 L9,2.99895656 C9,2.44724809 9.45097518,2 9.99077797,2 L14.009222,2 C14.5564136,2 15,2.44266033 15,2.99895656 L15,5" id="Path-33" stroke="#ffffff" stroke-width="2" stroke-linejoin="round"></path> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> | ||
diff --git a/client/src/assets/images/global/edit.svg b/client/src/assets/images/global/edit.svg new file mode 100644 index 000000000..23ece68f1 --- /dev/null +++ b/client/src/assets/images/global/edit.svg | |||
@@ -0,0 +1,15 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>edit</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-48.000000, -203.000000)" stroke="#585858" stroke-width="2"> | ||
9 | <g id="41" transform="translate(48.000000, 203.000000)"> | ||
10 | <path d="M3,21.0000003 L3,17 L15.8898356,4.11016442 C17.0598483,2.9401517 18.9638992,2.94723715 20.1306896,4.11402752 L19.9181432,3.90148112 C21.0902894,5.07362738 21.0882407,6.97202708 19.9174652,8.1377941 L7,21.0000003 L3,21.0000003 Z" id="Path-74" stroke-linecap="round" stroke-linejoin="round"></path> | ||
11 | <path d="M14.5,5.5 L18.5,9.5" id="Path-75"></path> | ||
12 | </g> | ||
13 | </g> | ||
14 | </g> | ||
15 | </svg> | ||
diff --git a/client/src/assets/images/global/validate.svg b/client/src/assets/images/global/validate.svg new file mode 100644 index 000000000..5c7ee9d14 --- /dev/null +++ b/client/src/assets/images/global/validate.svg | |||
@@ -0,0 +1,14 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
5 | <g id="Artboard-4" transform="translate(-400.000000, -1134.000000)" stroke="#ffffff" stroke-width="2"> | ||
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | ||
7 | <g id="yes" transform="translate(352.000000, 88.000000)"> | ||
8 | <circle id="Oval-1" cx="12" cy="12" r="10"></circle> | ||
9 | <polyline id="Path-288" stroke-linecap="round" stroke-linejoin="round" points="8.5 12.5 10.5 14.5 15.5 9.5"></polyline> | ||
10 | </g> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> | ||
diff --git a/client/src/assets/images/header/menu.svg b/client/src/assets/images/header/menu.svg new file mode 100644 index 000000000..7101bf73b --- /dev/null +++ b/client/src/assets/images/header/menu.svg | |||
@@ -0,0 +1,14 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>menu</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-180.000000, -203.000000)" stroke="#333333"> | ||
9 | <g id="44" transform="translate(180.000000, 203.000000)"> | ||
10 | <path d="M3.5,7 C3.5,6.72319836 3.72175357,6.5 3.99339768,6.5 L20.0066023,6.5 C20.2799786,6.5 20.5,6.72089465 20.5,7 C20.5,7.27680164 20.2782464,7.5 20.0066023,7.5 L3.99339768,7.5 C3.72002141,7.5 3.5,7.27910535 3.5,7 Z M3.5,12 C3.5,11.7231984 3.72175357,11.5 3.99339768,11.5 L20.0066023,11.5 C20.2799786,11.5 20.5,11.7208946 20.5,12 C20.5,12.2768016 20.2782464,12.5 20.0066023,12.5 L3.99339768,12.5 C3.72002141,12.5 3.5,12.2791054 3.5,12 Z M3.5,17 C3.5,16.7231984 3.72175357,16.5 3.99339768,16.5 L20.0066023,16.5 C20.2799786,16.5 20.5,16.7208946 20.5,17 C20.5,17.2768016 20.2782464,17.5 20.0066023,17.5 L3.99339768,17.5 C3.72002141,17.5 3.5,17.2791054 3.5,17 Z" id="Combined-Shape"></path> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> \ No newline at end of file | ||
diff --git a/client/src/assets/images/header/search.svg b/client/src/assets/images/header/search.svg new file mode 100644 index 000000000..489b59e9b --- /dev/null +++ b/client/src/assets/images/header/search.svg | |||
@@ -0,0 +1,12 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
5 | <g id="Artboard-4" transform="translate(-136.000000, -115.000000)" stroke="#000" stroke-width="2"> | ||
6 | <g id="3" transform="translate(136.000000, 115.000000)"> | ||
7 | <circle id="Oval-3" cx="10" cy="10" r="7"></circle> | ||
8 | <path d="M15,15 L21,21" id="Path-3" stroke-linecap="round" stroke-linejoin="round"></path> | ||
9 | </g> | ||
10 | </g> | ||
11 | </g> | ||
12 | </svg> | ||
diff --git a/client/src/assets/images/header/upload.svg b/client/src/assets/images/header/upload.svg new file mode 100644 index 000000000..2b07caf76 --- /dev/null +++ b/client/src/assets/images/header/upload.svg | |||
@@ -0,0 +1,16 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>cloud-upload</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-312.000000, -775.000000)" stroke="#fff" stroke-width="2"> | ||
9 | <g id="307" transform="translate(312.000000, 775.000000)"> | ||
10 | <path d="M8,18 L5,18 L5,18 C2.790861,18 1,16.209139 1,14 C1,11.790861 2.790861,10 5,10 C5.35840468,10 5.70579988,10.0471371 6.03632437,10.1355501 C6.01233106,9.92702603 6,9.71495305 6,9.5 C6,6.46243388 8.46243388,4 11.5,4 C14.0673313,4 16.2238156,5.7590449 16.8299648,8.1376465 C17.2052921,8.04765874 17.5970804,8 18,8 C20.7614237,8 23,10.2385763 23,13 C23,15.7614237 20.7614237,18 18,18 L16,18" id="Combined-Shape" stroke-linejoin="round"></path> | ||
11 | <path d="M12,13 L12,21" id="Path-58"></path> | ||
12 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 12.500000) scale(1, -1) translate(-12.000000, -12.500000) " points="15 11 12 14 9 11"></polyline> | ||
13 | </g> | ||
14 | </g> | ||
15 | </g> | ||
16 | </svg> | ||
diff --git a/client/src/assets/images/logo.svg b/client/src/assets/images/logo.svg new file mode 100644 index 000000000..8777acd5b --- /dev/null +++ b/client/src/assets/images/logo.svg | |||
@@ -0,0 +1,118 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||
2 | <svg | ||
3 | xmlns:dc="http://purl.org/dc/elements/1.1/" | ||
4 | xmlns:cc="http://creativecommons.org/ns#" | ||
5 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | ||
6 | xmlns:svg="http://www.w3.org/2000/svg" | ||
7 | xmlns="http://www.w3.org/2000/svg" | ||
8 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | ||
9 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | ||
10 | viewBox="2799 -911 16 22" | ||
11 | version="1.1" | ||
12 | id="svg13" | ||
13 | sodipodi:docname="logo.svg" | ||
14 | width="16" | ||
15 | height="22" | ||
16 | inkscape:version="0.92.2 5c3e80d, 2017-08-06"> | ||
17 | <metadata | ||
18 | id="metadata17"> | ||
19 | <rdf:RDF> | ||
20 | <cc:Work | ||
21 | rdf:about=""> | ||
22 | <dc:format>image/svg+xml</dc:format> | ||
23 | <dc:type | ||
24 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | ||
25 | <dc:title></dc:title> | ||
26 | </cc:Work> | ||
27 | </rdf:RDF> | ||
28 | </metadata> | ||
29 | <sodipodi:namedview | ||
30 | pagecolor="#ffffff" | ||
31 | bordercolor="#666666" | ||
32 | borderopacity="1" | ||
33 | objecttolerance="10" | ||
34 | gridtolerance="10" | ||
35 | guidetolerance="10" | ||
36 | inkscape:pageopacity="0" | ||
37 | inkscape:pageshadow="2" | ||
38 | inkscape:window-width="1916" | ||
39 | inkscape:window-height="1040" | ||
40 | id="namedview15" | ||
41 | showgrid="false" | ||
42 | inkscape:zoom="29.790476" | ||
43 | inkscape:cx="-1.1827326" | ||
44 | inkscape:cy="12.088" | ||
45 | inkscape:window-x="0" | ||
46 | inkscape:window-y="18" | ||
47 | inkscape:window-maximized="0" | ||
48 | inkscape:current-layer="svg13" /> | ||
49 | <defs | ||
50 | id="defs4"> | ||
51 | <style | ||
52 | id="style2"> | ||
53 | .cls-3 { | ||
54 | fill: #211f20; | ||
55 | } | ||
56 | |||
57 | .cls-4 { | ||
58 | fill: #737373; | ||
59 | } | ||
60 | |||
61 | .cls-5 { | ||
62 | fill: #f1680d; | ||
63 | } | ||
64 | |||
65 | .cls-6 { | ||
66 | fill: #fff; | ||
67 | } | ||
68 | </style> | ||
69 | </defs> | ||
70 | <g | ||
71 | id="Artboard_1" | ||
72 | data-name="Artboard – 1" | ||
73 | class="cls-1" | ||
74 | transform="translate(0.03356777,-1.9929667)"> | ||
75 | <g | ||
76 | id="Symbol_3_1" | ||
77 | data-name="Symbol 3 – 1" | ||
78 | transform="translate(2759,-975)"> | ||
79 | <g | ||
80 | id="Group_44" | ||
81 | data-name="Group 44" | ||
82 | transform="translate(0,2.333)"> | ||
83 | <path | ||
84 | id="Path_4" | ||
85 | data-name="Path 4" | ||
86 | class="cls-3" | ||
87 | d="m -949,-500 v 10.667 l 8,-5.333" | ||
88 | transform="translate(989,564)" | ||
89 | inkscape:connector-curvature="0" | ||
90 | style="fill:#211f20" /> | ||
91 | <path | ||
92 | id="Path_5" | ||
93 | data-name="Path 5" | ||
94 | class="cls-4" | ||
95 | d="m -949,-500 v 10.667 l 8,-5.333" | ||
96 | transform="translate(989,574.667)" | ||
97 | inkscape:connector-curvature="0" | ||
98 | style="fill:#737373" /> | ||
99 | <path | ||
100 | id="Path_6" | ||
101 | data-name="Path 6" | ||
102 | class="cls-5" | ||
103 | d="m -949,-500 v 10.667 l 8,-5.333" | ||
104 | transform="translate(997,569.333)" | ||
105 | inkscape:connector-curvature="0" | ||
106 | style="fill:#f1680d" /> | ||
107 | <path | ||
108 | id="Path_7" | ||
109 | data-name="Path 7" | ||
110 | class="cls-6" | ||
111 | d="M 0,0 V 10.667 L 8,5.333 Z" | ||
112 | transform="rotate(180,24,40)" | ||
113 | inkscape:connector-curvature="0" | ||
114 | style="fill:#ffffff" /> | ||
115 | </g> | ||
116 | </g> | ||
117 | </g> | ||
118 | </svg> | ||
diff --git a/client/src/assets/images/menu/administration.svg b/client/src/assets/images/menu/administration.svg new file mode 100644 index 000000000..b6da837d2 --- /dev/null +++ b/client/src/assets/images/menu/administration.svg | |||
@@ -0,0 +1,14 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>filter</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-444.000000, -247.000000)" fill="#808080"> | ||
9 | <g id="70" transform="translate(444.000000, 247.000000)"> | ||
10 | <path d="M8.82929429,17 L20.0066023,17 C20.5552407,17 21,17.4438648 21,18 C21,18.5522847 20.5550537,19 20.0066023,19 L8.82929429,19 C8.41745788,20.1651924 7.30621883,21 6,21 C4.34314575,21 3,19.6568542 3,18 C3,16.3431458 4.34314575,15 6,15 C7.30621883,15 8.41745788,15.8348076 8.82929429,17 Z M9.17070571,13 L3.99339768,13 C3.44475929,13 3,12.5561352 3,12 C3,11.4477153 3.44494629,11 3.99339768,11 L9.17070571,11 C9.58254212,9.83480763 10.6937812,9 12,9 C13.3062188,9 14.4174579,9.83480763 14.8292943,11 L20.0066023,11 C20.5552407,11 21,11.4438648 21,12 C21,12.5522847 20.5550537,13 20.0066023,13 L14.8292943,13 C14.4174579,14.1651924 13.3062188,15 12,15 C10.6937812,15 9.58254212,14.1651924 9.17070571,13 Z M15.1659641,6.98648118 C15.1124525,6.99537358 15.05751,7 15.0014977,7 L3.99850233,7 C3.44704472,7 3,6.55613518 3,6 C3,5.44771525 3.44748943,5 3.99850233,5 L15.0014977,5 C15.0575314,5 15.1124871,5.00458274 15.1660053,5.01340035 C15.5740343,3.84121344 16.6887792,3 18,3 C19.6568542,3 21,4.34314575 21,6 C21,7.65685425 19.6568542,9 18,9 C16.688735,9 15.5739592,8.15872988 15.1659641,6.98648118 Z M18,7 C18.5522847,7 19,6.55228475 19,6 C19,5.44771525 18.5522847,5 18,5 C17.4477153,5 17,5.44771525 17,6 C17,6.55228475 17.4477153,7 18,7 Z M12,13 C12.5522847,13 13,12.5522847 13,12 C13,11.4477153 12.5522847,11 12,11 C11.4477153,11 11,11.4477153 11,12 C11,12.5522847 11.4477153,13 12,13 Z M6,19 C6.55228475,19 7,18.5522847 7,18 C7,17.4477153 6.55228475,17 6,17 C5.44771525,17 5,17.4477153 5,18 C5,18.5522847 5.44771525,19 6,19 Z" id="Combined-Shape"></path> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> | ||
diff --git a/client/src/assets/images/menu/recently-added.svg b/client/src/assets/images/menu/recently-added.svg new file mode 100644 index 000000000..6473837f8 --- /dev/null +++ b/client/src/assets/images/menu/recently-added.svg | |||
@@ -0,0 +1,13 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
5 | <g id="Artboard-4" transform="translate(-92.000000, -115.000000)"> | ||
6 | <g id="2" transform="translate(92.000000, 115.000000)"> | ||
7 | <circle id="Oval-1" stroke="#808080" stroke-width="2" cx="12" cy="12" r="10"></circle> | ||
8 | <rect id="Rectangle-1" fill="#808080" x="11" y="7" width="2" height="10" rx="1"></rect> | ||
9 | <rect id="Rectangle-1" fill="#808080" x="7" y="11" width="10" height="2" rx="1"></rect> | ||
10 | </g> | ||
11 | </g> | ||
12 | </g> | ||
13 | </svg> | ||
diff --git a/client/src/assets/images/menu/trending.svg b/client/src/assets/images/menu/trending.svg new file mode 100644 index 000000000..ffc65cc04 --- /dev/null +++ b/client/src/assets/images/menu/trending.svg | |||
@@ -0,0 +1,16 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>graph</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"> | ||
8 | <g id="Artboard-4" transform="translate(-444.000000, -203.000000)" stroke-width="2" stroke="#808080"> | ||
9 | <g id="50" transform="translate(444.000000, 203.000000)"> | ||
10 | <polyline id="Path-96" points="3 3 3 21.006249 21.0246733 21.006249"></polyline> | ||
11 | <polyline id="Path-101" points="6 18 11 12 14 13 19 7"></polyline> | ||
12 | <polygon id="Path-102" points="20 9 20 6 17 6"></polygon> | ||
13 | </g> | ||
14 | </g> | ||
15 | </g> | ||
16 | </svg> | ||
diff --git a/client/src/assets/images/video/alert.svg b/client/src/assets/images/video/alert.svg new file mode 100644 index 000000000..6d3af029f --- /dev/null +++ b/client/src/assets/images/video/alert.svg | |||
@@ -0,0 +1,16 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>alert</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-48.000000, -467.000000)"> | ||
9 | <g id="161" transform="translate(48.000000, 467.000000)"> | ||
10 | <path d="M12.8715755,3.50973876 L12,1.96027114 L11.1284245,3.50973876 L2.12842446,19.5097388 L1.29015252,21 L3,21 L21,21 L22.7098475,21 L21.8715755,19.5097388 L12.8715755,3.50973876 Z" id="Triangle-2" stroke="#585858" stroke-width="2" stroke-linejoin="round"></path> | ||
11 | <path d="M12,17.75 C12.6903559,17.75 13.25,17.1903559 13.25,16.5 C13.25,15.8096441 12.6903559,15.25 12,15.25 C11.3096441,15.25 10.75,15.8096441 10.75,16.5 C10.75,17.1903559 11.3096441,17.75 12,17.75 Z" id="Oval-8" fill="#585858"></path> | ||
12 | <rect id="Rectangle-3" fill="#585858" x="11" y="9" width="2" height="5" rx="1"></rect> | ||
13 | </g> | ||
14 | </g> | ||
15 | </g> | ||
16 | </svg> | ||
diff --git a/client/src/assets/images/video/dislike-grey.svg b/client/src/assets/images/video/dislike-grey.svg new file mode 100644 index 000000000..56a7908fb --- /dev/null +++ b/client/src/assets/images/video/dislike-grey.svg | |||
@@ -0,0 +1,14 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
5 | <g id="Artboard-4" transform="translate(-752.000000, -1090.000000)" stroke="#585858" stroke-width="2"> | ||
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | ||
7 | <g id="thumbs-down" transform="translate(704.000000, 44.000000)"> | ||
8 | <path d="M6,16 C6,18.5 6.5,21 8,21 L16.9938335,21 C17.5495239,21 18.1819788,20.5956028 18.4072817,20.0949295 L20.8562951,14.6526776 C21.7640882,12.6353595 20.7154925,11 18.5092545,11 L15.5,11 C15.5,11 18.5,5 15,5 C12.5,5 11.5,11 8,11 C6.5,11 6,13.5 6,16 Z" id="Path-188" stroke-linejoin="round" transform="translate(13.591488, 13.000000) scale(1, -1) translate(-13.591488, -13.000000) "></path> | ||
9 | <path d="M4,4.5 C4,4.5 3,7 3,10 C3,13 4,15.5 4,15.5" id="Path-189" transform="translate(3.500000, 10.000000) scale(1, -1) translate(-3.500000, -10.000000) "></path> | ||
10 | </g> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> | ||
diff --git a/client/src/assets/images/video/dislike-white.svg b/client/src/assets/images/video/dislike-white.svg new file mode 100644 index 000000000..cfc6eaa1f --- /dev/null +++ b/client/src/assets/images/video/dislike-white.svg | |||
@@ -0,0 +1,14 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
5 | <g id="Artboard-4" transform="translate(-752.000000, -1090.000000)" stroke="#ffffff" stroke-width="2"> | ||
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | ||
7 | <g id="thumbs-down" transform="translate(704.000000, 44.000000)"> | ||
8 | <path d="M6,16 C6,18.5 6.5,21 8,21 L16.9938335,21 C17.5495239,21 18.1819788,20.5956028 18.4072817,20.0949295 L20.8562951,14.6526776 C21.7640882,12.6353595 20.7154925,11 18.5092545,11 L15.5,11 C15.5,11 18.5,5 15,5 C12.5,5 11.5,11 8,11 C6.5,11 6,13.5 6,16 Z" id="Path-188" stroke-linejoin="round" transform="translate(13.591488, 13.000000) scale(1, -1) translate(-13.591488, -13.000000) "></path> | ||
9 | <path d="M4,4.5 C4,4.5 3,7 3,10 C3,13 4,15.5 4,15.5" id="Path-189" transform="translate(3.500000, 10.000000) scale(1, -1) translate(-3.500000, -10.000000) "></path> | ||
10 | </g> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> | ||
diff --git a/client/src/assets/images/video/download-grey.svg b/client/src/assets/images/video/download-grey.svg new file mode 100644 index 000000000..5b0cca5ef --- /dev/null +++ b/client/src/assets/images/video/download-grey.svg | |||
@@ -0,0 +1,16 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>download</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-180.000000, -291.000000)" stroke="#585858" stroke-width="2"> | ||
9 | <g id="84" transform="translate(180.000000, 291.000000)"> | ||
10 | <path d="M12,3 L12,15" id="Path-58"></path> | ||
11 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 14.000000) rotate(-270.000000) translate(-12.000000, -14.000000) " points="9 8 15 14 9 20"></polyline> | ||
12 | <path d="M3,18 L3,20.0590859 C3,20.6127331 3.44494889,21.0615528 3.99340349,21.0615528 L20.0067018,21.0615528 C20.5553434,21.0615528 21.0001052,20.6098102 21.0001051,20.0590859 L21.0001049,18" id="Path-12" stroke-linejoin="round"></path> | ||
13 | </g> | ||
14 | </g> | ||
15 | </g> | ||
16 | </svg> | ||
diff --git a/client/src/assets/images/video/download-white.svg b/client/src/assets/images/video/download-white.svg new file mode 100644 index 000000000..0e66e06e8 --- /dev/null +++ b/client/src/assets/images/video/download-white.svg | |||
@@ -0,0 +1,16 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>download</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-180.000000, -291.000000)" stroke="#ffffff" stroke-width="2"> | ||
9 | <g id="84" transform="translate(180.000000, 291.000000)"> | ||
10 | <path d="M12,3 L12,15" id="Path-58"></path> | ||
11 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 14.000000) rotate(-270.000000) translate(-12.000000, -14.000000) " points="9 8 15 14 9 20"></polyline> | ||
12 | <path d="M3,18 L3,20.0590859 C3,20.6127331 3.44494889,21.0615528 3.99340349,21.0615528 L20.0067018,21.0615528 C20.5553434,21.0615528 21.0001052,20.6098102 21.0001051,20.0590859 L21.0001049,18" id="Path-12" stroke-linejoin="round"></path> | ||
13 | </g> | ||
14 | </g> | ||
15 | </g> | ||
16 | </svg> | ||
diff --git a/client/src/assets/images/video/eye-closed.svg b/client/src/assets/images/video/eye-closed.svg new file mode 100644 index 000000000..c5b739659 --- /dev/null +++ b/client/src/assets/images/video/eye-closed.svg | |||
@@ -0,0 +1,18 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
5 | <g id="Artboard-4" transform="translate(-796.000000, -1046.000000)" stroke="#585858" stroke-width="2"> | ||
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | ||
7 | <g id="eye-closed" transform="translate(760.000000, 12.000000) scale(1, -1) translate(-760.000000, -12.000000) translate(748.000000, 0.000000)"> | ||
8 | <path d="M2,14 C2,14 5,7 12,7 C19,7 22,14 22,14" id="Path-80" stroke-linejoin="round"></path> | ||
9 | <path d="M12,7 L12,5" id="Path-81"></path> | ||
10 | <path d="M18,8.5 L19,7" id="Path-81"></path> | ||
11 | <path d="M21,12 L22.5,11" id="Path-81"></path> | ||
12 | <path d="M1.5,12 L3,11" id="Path-81" transform="translate(2.250000, 11.500000) scale(1, -1) translate(-2.250000, -11.500000) "></path> | ||
13 | <path d="M5,8.5 L6,7" id="Path-81" transform="translate(5.500000, 7.750000) scale(-1, 1) translate(-5.500000, -7.750000) "></path> | ||
14 | </g> | ||
15 | </g> | ||
16 | </g> | ||
17 | </g> | ||
18 | </svg> | ||
diff --git a/client/src/assets/images/video/like-grey.svg b/client/src/assets/images/video/like-grey.svg new file mode 100644 index 000000000..5ef6c7b31 --- /dev/null +++ b/client/src/assets/images/video/like-grey.svg | |||
@@ -0,0 +1,15 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>thumbs-up</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-708.000000, -643.000000)" stroke="#585858" stroke-width="2"> | ||
9 | <g id="256" transform="translate(708.000000, 643.000000)"> | ||
10 | <path d="M6,14 C6,16.5 6.5,19 8,19 L16.9938335,19 C17.5495239,19 18.1819788,18.5956028 18.4072817,18.0949295 L20.8562951,12.6526776 C21.7640882,10.6353595 20.7154925,9 18.5092545,9 L15.5,9 C15.5,9 18.5,3 15,3 C12.5,3 11.5,9 8,9 C6.5,9 6,11.5 6,14 Z" id="Path-188" stroke-linejoin="round"></path> | ||
11 | <path d="M4,8.5 C4,8.5 3,11 3,14 C3,17 4,19.5 4,19.5" id="Path-189"></path> | ||
12 | </g> | ||
13 | </g> | ||
14 | </g> | ||
15 | </svg> | ||
diff --git a/client/src/assets/images/video/like-white.svg b/client/src/assets/images/video/like-white.svg new file mode 100644 index 000000000..88e5f6a9a --- /dev/null +++ b/client/src/assets/images/video/like-white.svg | |||
@@ -0,0 +1,15 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>thumbs-up</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-708.000000, -643.000000)" stroke="#ffffff" stroke-width="2"> | ||
9 | <g id="256" transform="translate(708.000000, 643.000000)"> | ||
10 | <path d="M6,14 C6,16.5 6.5,19 8,19 L16.9938335,19 C17.5495239,19 18.1819788,18.5956028 18.4072817,18.0949295 L20.8562951,12.6526776 C21.7640882,10.6353595 20.7154925,9 18.5092545,9 L15.5,9 C15.5,9 18.5,3 15,3 C12.5,3 11.5,9 8,9 C6.5,9 6,11.5 6,14 Z" id="Path-188" stroke-linejoin="round"></path> | ||
11 | <path d="M4,8.5 C4,8.5 3,11 3,14 C3,17 4,19.5 4,19.5" id="Path-189"></path> | ||
12 | </g> | ||
13 | </g> | ||
14 | </g> | ||
15 | </svg> | ||
diff --git a/client/src/assets/images/video/more.svg b/client/src/assets/images/video/more.svg new file mode 100644 index 000000000..dea392136 --- /dev/null +++ b/client/src/assets/images/video/more.svg | |||
@@ -0,0 +1,11 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
5 | <g id="Artboard-4" transform="translate(-444.000000, -115.000000)" fill="#585858"> | ||
6 | <g id="10" transform="translate(444.000000, 115.000000)"> | ||
7 | <path d="M10,12 C10,10.8954305 10.8877296,10 12,10 C13.1045695,10 14,10.8877296 14,12 C14,13.1045695 13.1122704,14 12,14 C10.8954305,14 10,13.1122704 10,12 Z M17,12 C17,10.8954305 17.8877296,10 19,10 C20.1045695,10 21,10.8877296 21,12 C21,13.1045695 20.1122704,14 19,14 C17.8954305,14 17,13.1122704 17,12 Z M3,12 C3,10.8954305 3.88772964,10 5,10 C6.1045695,10 7,10.8877296 7,12 C7,13.1045695 6.11227036,14 5,14 C3.8954305,14 3,13.1122704 3,12 Z" id="Combined-Shape"></path> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> | ||
diff --git a/client/src/assets/images/video/share.svg b/client/src/assets/images/video/share.svg new file mode 100644 index 000000000..da0f43e81 --- /dev/null +++ b/client/src/assets/images/video/share.svg | |||
@@ -0,0 +1,16 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>share</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-312.000000, -203.000000)" stroke="#585858" stroke-width="2"> | ||
9 | <g id="47" transform="translate(312.000000, 203.000000)"> | ||
10 | <path d="M20,15 L20,18.0026083 C20,19.1057373 19.1073772,20 18.0049107,20 L5.99508929,20 C4.8932319,20 4,19.1073772 4,18.0049107 L4,5.99508929 C4,4.8932319 4.89585781,4 5.9973917,4 L9,4" id="Rectangle-460"></path> | ||
11 | <polyline id="Path-93" stroke-linejoin="round" points="13 4 20.0207973 4 20.0207973 11.0191059"></polyline> | ||
12 | <path d="M19,5 L12,12" id="Path-94" stroke-linejoin="round"></path> | ||
13 | </g> | ||
14 | </g> | ||
15 | </g> | ||
16 | </svg> | ||
diff --git a/client/src/assets/images/video/upload.svg b/client/src/assets/images/video/upload.svg new file mode 100644 index 000000000..c5b7cb443 --- /dev/null +++ b/client/src/assets/images/video/upload.svg | |||
@@ -0,0 +1,16 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>cloud-upload</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-312.000000, -775.000000)" stroke="#C6C6C6" stroke-width="2"> | ||
9 | <g id="307" transform="translate(312.000000, 775.000000)"> | ||
10 | <path d="M8,18 L5,18 L5,18 C2.790861,18 1,16.209139 1,14 C1,11.790861 2.790861,10 5,10 C5.35840468,10 5.70579988,10.0471371 6.03632437,10.1355501 C6.01233106,9.92702603 6,9.71495305 6,9.5 C6,6.46243388 8.46243388,4 11.5,4 C14.0673313,4 16.2238156,5.7590449 16.8299648,8.1376465 C17.2052921,8.04765874 17.5970804,8 18,8 C20.7614237,8 23,10.2385763 23,13 C23,15.7614237 20.7614237,18 18,18 L16,18" id="Combined-Shape" stroke-linejoin="round"></path> | ||
11 | <path d="M12,13 L12,21" id="Path-58"></path> | ||
12 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 12.500000) scale(1, -1) translate(-12.000000, -12.500000) " points="15 11 12 14 9 11"></polyline> | ||
13 | </g> | ||
14 | </g> | ||
15 | </g> | ||
16 | </svg> | ||
diff --git a/client/src/assets/logo.png b/client/src/assets/logo.png deleted file mode 100644 index c1d77a24c..000000000 --- a/client/src/assets/logo.png +++ /dev/null | |||
Binary files differ | |||
diff --git a/client/src/assets/player/images/arrow-down.svg b/client/src/assets/player/images/arrow-down.svg new file mode 100644 index 000000000..3377cdab2 --- /dev/null +++ b/client/src/assets/player/images/arrow-down.svg | |||
@@ -0,0 +1,14 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
5 | <g id="Artboard-4" transform="translate(-532.000000, -1046.000000)" stroke="#fff" stroke-width="2"> | ||
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | ||
7 | <g id="down" transform="translate(484.000000, 0.000000)"> | ||
8 | <path d="M12,3 L12,20" id="Path-58"></path> | ||
9 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 17.000000) scale(-1, -1) translate(-12.000000, -17.000000) " points="4 21 12 13 20 21"></polyline> | ||
10 | </g> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> | ||
diff --git a/client/src/assets/player/images/arrow-up.svg b/client/src/assets/player/images/arrow-up.svg new file mode 100644 index 000000000..b1a7890a8 --- /dev/null +++ b/client/src/assets/player/images/arrow-up.svg | |||
@@ -0,0 +1,14 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
5 | <g id="Artboard-4" transform="translate(-488.000000, -1046.000000)" stroke="#fff" stroke-width="2"> | ||
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | ||
7 | <g id="up" transform="translate(440.000000, 0.000000)"> | ||
8 | <path d="M12,4 L12,21" id="Path-58"></path> | ||
9 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 7.000000) scale(-1, 1) translate(-12.000000, -7.000000) " points="4 11 12 3 20 11"></polyline> | ||
10 | </g> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> | ||
diff --git a/client/src/assets/player/images/fullscreen.svg b/client/src/assets/player/images/fullscreen.svg new file mode 100644 index 000000000..44e0041a4 --- /dev/null +++ b/client/src/assets/player/images/fullscreen.svg | |||
@@ -0,0 +1,18 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>fullscreen</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-576.000000, -159.000000)" stroke="#fff" stroke-width="2"> | ||
9 | <g id="33" transform="translate(576.000000, 159.000000)"> | ||
10 | <rect id="Rectangle-433" x="1" y="4" width="22" height="16" rx="1"></rect> | ||
11 | <polyline id="Path-42" stroke-linecap="round" stroke-linejoin="round" points="20 10 20 7 17 7"></polyline> | ||
12 | <polyline id="Path-42" stroke-linecap="round" stroke-linejoin="round" points="7 17 4 17 4 14"></polyline> | ||
13 | <polyline id="Path-42" stroke-linecap="round" stroke-linejoin="round" transform="translate(18.500000, 15.500000) scale(1, -1) translate(-18.500000, -15.500000) " points="20 17 20 14 17 14"></polyline> | ||
14 | <polyline id="Path-42" stroke-linecap="round" stroke-linejoin="round" transform="translate(5.500000, 8.500000) scale(1, -1) translate(-5.500000, -8.500000) " points="7 10 4 10 4 7"></polyline> | ||
15 | </g> | ||
16 | </g> | ||
17 | </g> | ||
18 | </svg> | ||
diff --git a/client/src/assets/player/images/volume-mute.svg b/client/src/assets/player/images/volume-mute.svg new file mode 100644 index 000000000..0c7c296bc --- /dev/null +++ b/client/src/assets/player/images/volume-mute.svg | |||
@@ -0,0 +1,16 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>volume-mute</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-92.000000, -159.000000)" stroke="#fff" stroke-width="2"> | ||
9 | <g id="22" transform="translate(92.000000, 159.000000)"> | ||
10 | <path d="M2,8.99703014 C2,8.4463856 2.44335318,8 3.0093689,8 L6,8 L12,4 L12,20 L6,16 L3.0093689,16 C2.45190985,16 2,15.5469637 2,15.0029699 L2,8.99703014 Z" id="Rectangle-415" stroke-linejoin="round"></path> | ||
11 | <path d="M16,15 L22,9" id="Path-28"></path> | ||
12 | <path d="M16.0000002,15 L22.0249378,9" id="Path-28" transform="translate(19.012469, 12.000000) scale(-1, 1) translate(-19.012469, -12.000000) "></path> | ||
13 | </g> | ||
14 | </g> | ||
15 | </g> | ||
16 | </svg> | ||
diff --git a/client/src/assets/player/images/volume.svg b/client/src/assets/player/images/volume.svg new file mode 100644 index 000000000..590913add --- /dev/null +++ b/client/src/assets/player/images/volume.svg | |||
@@ -0,0 +1,13 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
5 | <g id="Artboard-4" transform="translate(-884.000000, -115.000000)" stroke="#fff" stroke-width="2"> | ||
6 | <g id="20" transform="translate(884.000000, 115.000000)"> | ||
7 | <path d="M2,8.99703014 C2,8.4463856 2.44335318,8 3.0093689,8 L6,8 L12,4 L12,20 L6,16 L3.0093689,16 C2.45190985,16 2,15.5469637 2,15.0029699 L2,8.99703014 Z" id="Rectangle-415" stroke-linejoin="round"></path> | ||
8 | <path d="M16,8 C16,8 18,9.5 18,12 C18,14.5 16,16 16,16" id="Path-26"></path> | ||
9 | <path d="M16.0734116,20 C19.3093571,18.9698098 22.0000001,15.5773201 22.0000001,12 C22.0000001,8.43619491 19.2903975,5.04132966 16.0734116,4" id="Oval-33"></path> | ||
10 | </g> | ||
11 | </g> | ||
12 | </g> | ||
13 | </svg> | ||
diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/peertube-videojs-plugin.ts index c54d8b5ea..4ba37b7d9 100644 --- a/client/src/assets/player/peertube-videojs-plugin.ts +++ b/client/src/assets/player/peertube-videojs-plugin.ts | |||
@@ -2,9 +2,24 @@ | |||
2 | 2 | ||
3 | import videojs, { Player } from 'video.js' | 3 | import videojs, { Player } from 'video.js' |
4 | import * as WebTorrent from 'webtorrent' | 4 | import * as WebTorrent from 'webtorrent' |
5 | import { VideoFile } from '../../../../shared' | ||
5 | 6 | ||
6 | import { renderVideo } from './video-renderer' | 7 | import { renderVideo } from './video-renderer' |
7 | import { VideoFile } from '../../../../shared' | 8 | |
9 | // https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts | ||
10 | // Don't import all Angular stuff, just copy the code with shame | ||
11 | const dictionaryBytes: Array<{max: number, type: string}> = [ | ||
12 | { max: 1024, type: 'B' }, | ||
13 | { max: 1048576, type: 'KB' }, | ||
14 | { max: 1073741824, type: 'MB' }, | ||
15 | { max: 1.0995116e12, type: 'GB' } | ||
16 | ] | ||
17 | function bytes (value) { | ||
18 | const format = dictionaryBytes.find(d => value < d.max) || dictionaryBytes[dictionaryBytes.length - 1] | ||
19 | const calc = Math.floor(value / (format.max / 1024)).toString() | ||
20 | |||
21 | return [ calc, format.type ] | ||
22 | } | ||
8 | 23 | ||
9 | // videojs typings don't have some method we need | 24 | // videojs typings don't have some method we need |
10 | const videojsUntyped = videojs as any | 25 | const videojsUntyped = videojs as any |
@@ -62,6 +77,7 @@ const ResolutionMenuButton = videojsUntyped.extend(MenuButton, { | |||
62 | 77 | ||
63 | update: function () { | 78 | update: function () { |
64 | this.label.innerHTML = this.player_.getCurrentResolutionLabel() | 79 | this.label.innerHTML = this.player_.getCurrentResolutionLabel() |
80 | this.hide() | ||
65 | return MenuButton.prototype.update.call(this) | 81 | return MenuButton.prototype.update.call(this) |
66 | }, | 82 | }, |
67 | 83 | ||
@@ -74,8 +90,7 @@ MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton) | |||
74 | const Button = videojsUntyped.getComponent('Button') | 90 | const Button = videojsUntyped.getComponent('Button') |
75 | const PeertubeLinkButton = videojsUntyped.extend(Button, { | 91 | const PeertubeLinkButton = videojsUntyped.extend(Button, { |
76 | constructor: function (player) { | 92 | constructor: function (player) { |
77 | Button.apply(this, arguments) | 93 | Button.call(this, player) |
78 | this.player = player | ||
79 | }, | 94 | }, |
80 | 95 | ||
81 | createEl: function () { | 96 | createEl: function () { |
@@ -90,11 +105,80 @@ const PeertubeLinkButton = videojsUntyped.extend(Button, { | |||
90 | }, | 105 | }, |
91 | 106 | ||
92 | handleClick: function () { | 107 | handleClick: function () { |
93 | this.player.pause() | 108 | this.player_.pause() |
94 | } | 109 | } |
95 | }) | 110 | }) |
96 | Button.registerComponent('PeerTubeLinkButton', PeertubeLinkButton) | 111 | Button.registerComponent('PeerTubeLinkButton', PeertubeLinkButton) |
97 | 112 | ||
113 | const WebTorrentButton = videojsUntyped.extend(Button, { | ||
114 | constructor: function (player) { | ||
115 | Button.call(this, player) | ||
116 | }, | ||
117 | |||
118 | createEl: function () { | ||
119 | const div = document.createElement('div') | ||
120 | const subDiv = document.createElement('div') | ||
121 | div.appendChild(subDiv) | ||
122 | |||
123 | const downloadIcon = document.createElement('span') | ||
124 | downloadIcon.classList.add('icon', 'icon-download') | ||
125 | subDiv.appendChild(downloadIcon) | ||
126 | |||
127 | const downloadSpeedText = document.createElement('span') | ||
128 | downloadSpeedText.classList.add('download-speed-text') | ||
129 | const downloadSpeedNumber = document.createElement('span') | ||
130 | downloadSpeedNumber.classList.add('download-speed-number') | ||
131 | const downloadSpeedUnit = document.createElement('span') | ||
132 | downloadSpeedText.appendChild(downloadSpeedNumber) | ||
133 | downloadSpeedText.appendChild(downloadSpeedUnit) | ||
134 | subDiv.appendChild(downloadSpeedText) | ||
135 | |||
136 | const uploadIcon = document.createElement('span') | ||
137 | uploadIcon.classList.add('icon', 'icon-upload') | ||
138 | subDiv.appendChild(uploadIcon) | ||
139 | |||
140 | const uploadSpeedText = document.createElement('span') | ||
141 | uploadSpeedText.classList.add('upload-speed-text') | ||
142 | const uploadSpeedNumber = document.createElement('span') | ||
143 | uploadSpeedNumber.classList.add('upload-speed-number') | ||
144 | const uploadSpeedUnit = document.createElement('span') | ||
145 | uploadSpeedText.appendChild(uploadSpeedNumber) | ||
146 | uploadSpeedText.appendChild(uploadSpeedUnit) | ||
147 | subDiv.appendChild(uploadSpeedText) | ||
148 | |||
149 | const peersText = document.createElement('span') | ||
150 | peersText.textContent = ' peers' | ||
151 | peersText.classList.add('peers-text') | ||
152 | const peersNumber = document.createElement('span') | ||
153 | peersNumber.classList.add('peers-number') | ||
154 | subDiv.appendChild(peersNumber) | ||
155 | subDiv.appendChild(peersText) | ||
156 | |||
157 | div.className = 'vjs-webtorrent' | ||
158 | // Hide the stats before we get the info | ||
159 | subDiv.className = 'vjs-webtorrent-hidden' | ||
160 | |||
161 | this.player_.on('torrentInfo', (event, data) => { | ||
162 | const downloadSpeed = bytes(data.downloadSpeed) | ||
163 | const uploadSpeed = bytes(data.uploadSpeed) | ||
164 | const numPeers = data.numPeers | ||
165 | |||
166 | downloadSpeedNumber.textContent = downloadSpeed[0] | ||
167 | downloadSpeedUnit.textContent = ' ' + downloadSpeed[1] | ||
168 | |||
169 | uploadSpeedNumber.textContent = uploadSpeed[0] | ||
170 | uploadSpeedUnit.textContent = ' ' + uploadSpeed[1] | ||
171 | |||
172 | peersNumber.textContent = numPeers | ||
173 | |||
174 | subDiv.className = 'vjs-webtorrent-displayed' | ||
175 | }) | ||
176 | |||
177 | return div | ||
178 | } | ||
179 | }) | ||
180 | Button.registerComponent('WebTorrentButton', WebTorrentButton) | ||
181 | |||
98 | type PeertubePluginOptions = { | 182 | type PeertubePluginOptions = { |
99 | videoFiles: VideoFile[] | 183 | videoFiles: VideoFile[] |
100 | playerElement: HTMLVideoElement | 184 | playerElement: HTMLVideoElement |
@@ -223,6 +307,12 @@ const peertubePlugin = function (options: PeertubePluginOptions) { | |||
223 | } | 307 | } |
224 | } | 308 | } |
225 | 309 | ||
310 | const webTorrentButton = new WebTorrentButton(player) | ||
311 | controlBar.webTorrent = controlBar.el().insertBefore(webTorrentButton.el(), controlBar.progressControl.el()) | ||
312 | controlBar.webTorrent.dispose = function () { | ||
313 | this.parentNode.removeChild(this) | ||
314 | } | ||
315 | |||
226 | if (options.autoplay === true) { | 316 | if (options.autoplay === true) { |
227 | player.updateVideoFile() | 317 | player.updateVideoFile() |
228 | } else { | 318 | } else { |
@@ -245,7 +335,7 @@ const peertubePlugin = function (options: PeertubePluginOptions) { | |||
245 | }, 1000) | 335 | }, 1000) |
246 | }) | 336 | }) |
247 | 337 | ||
248 | function handleError (err: Error|string) { | 338 | function handleError (err: Error | string) { |
249 | return player.trigger('customError', { err }) | 339 | return player.trigger('customError', { err }) |
250 | } | 340 | } |
251 | } | 341 | } |
diff --git a/client/src/index.html b/client/src/index.html index 8e94b903d..4af6b12f6 100644 --- a/client/src/index.html +++ b/client/src/index.html | |||
@@ -11,7 +11,7 @@ | |||
11 | <!-- open graph and oembed tags --> | 11 | <!-- open graph and oembed tags --> |
12 | <!-- Do not remove it! --> | 12 | <!-- Do not remove it! --> |
13 | 13 | ||
14 | <link rel="icon" type="image/png" href="/client/assets/favicon.png" /> | 14 | <link rel="icon" type="image/png" href="/client/assets/images/favicon.png" /> |
15 | 15 | ||
16 | <!-- base url --> | 16 | <!-- base url --> |
17 | <base href="<%= htmlWebpackPlugin.options.metadata.baseUrl %>"> | 17 | <base href="<%= htmlWebpackPlugin.options.metadata.baseUrl %>"> |
diff --git a/client/src/sass/_mixins.scss b/client/src/sass/_mixins.scss new file mode 100644 index 000000000..2a7192fb2 --- /dev/null +++ b/client/src/sass/_mixins.scss | |||
@@ -0,0 +1,95 @@ | |||
1 | @mixin disable-default-a-behaviour { | ||
2 | &:hover, &:focus, &:active { | ||
3 | text-decoration: none !important; | ||
4 | outline: none !important; | ||
5 | } | ||
6 | } | ||
7 | |||
8 | @mixin peertube-input-text($width) { | ||
9 | display: inline-block; | ||
10 | height: $button-height; | ||
11 | width: $width; | ||
12 | background: #fff; | ||
13 | border: 1px solid #C6C6C6; | ||
14 | border-radius: 3px; | ||
15 | padding-left: 15px; | ||
16 | |||
17 | &::placeholder { | ||
18 | color: #585858; | ||
19 | } | ||
20 | } | ||
21 | |||
22 | @mixin orange-button { | ||
23 | color: #fff; | ||
24 | background-color: $orange-color; | ||
25 | |||
26 | &:hover, &:active, &:focus { | ||
27 | color: #fff; | ||
28 | background-color: $orange-hoover-color; | ||
29 | } | ||
30 | |||
31 | &[disabled], &.disabled { | ||
32 | cursor: default; | ||
33 | color: #fff; | ||
34 | background-color: #C6C6C6; | ||
35 | } | ||
36 | } | ||
37 | |||
38 | @mixin grey-button { | ||
39 | background-color: $grey-color; | ||
40 | color: #585858; | ||
41 | |||
42 | &:hover, &:active, &:focus, &[disabled], &.disabled { | ||
43 | color: #585858; | ||
44 | background-color: $grey-hoover-color; | ||
45 | } | ||
46 | |||
47 | &[disabled], &.disabled { | ||
48 | cursor: default; | ||
49 | } | ||
50 | } | ||
51 | |||
52 | @mixin peertube-button { | ||
53 | border: none; | ||
54 | font-weight: $font-semibold; | ||
55 | font-size: 15px; | ||
56 | height: $button-height; | ||
57 | line-height: $button-height; | ||
58 | border-radius: 3px; | ||
59 | text-align: center; | ||
60 | padding: 0 17px 0 13px; | ||
61 | cursor: pointer; | ||
62 | outline: 0; | ||
63 | } | ||
64 | |||
65 | @mixin peertube-button-link { | ||
66 | display: inline-block; | ||
67 | |||
68 | @include disable-default-a-behaviour; | ||
69 | @include peertube-button; | ||
70 | } | ||
71 | |||
72 | @mixin avatar ($size) { | ||
73 | width: $size; | ||
74 | height: $size; | ||
75 | } | ||
76 | |||
77 | @mixin icon ($size) { | ||
78 | display: inline-block; | ||
79 | background-repeat: no-repeat; | ||
80 | background-size: contain; | ||
81 | width: $size; | ||
82 | height: $size; | ||
83 | vertical-align: middle; | ||
84 | cursor: pointer; | ||
85 | } | ||
86 | |||
87 | |||
88 | @mixin peertube-select ($width) { | ||
89 | background-color: #fff; | ||
90 | border: 1px solid #C6C6C6; | ||
91 | height: $button-height; | ||
92 | width: $width; | ||
93 | border-radius: 3px; | ||
94 | padding-left: 15px; | ||
95 | } | ||
diff --git a/client/src/sass/_variables.scss b/client/src/sass/_variables.scss index f0ffb43ba..0d310409b 100644 --- a/client/src/sass/_variables.scss +++ b/client/src/sass/_variables.scss | |||
@@ -1,23 +1,29 @@ | |||
1 | $grey-color: #555; | 1 | $font-regular: 400; |
2 | $font-semibold: 600; | ||
3 | $font-bold: 700; | ||
2 | 4 | ||
3 | $black-background: #1d2125; | 5 | $grey-color: #E5E5E5; |
6 | $grey-hoover-color: #EFEFEF;; | ||
7 | $orange-color: #F1680D; | ||
8 | $orange-hoover-color: #F97D46; | ||
9 | |||
10 | $black-background: #000; | ||
4 | $grey-background: #f6f2f2; | 11 | $grey-background: #f6f2f2; |
12 | $red-error: #FF0000; | ||
13 | |||
14 | $expanded-horizontal-margins: 150px; | ||
15 | $not-expanded-horizontal-margins: 30px; | ||
5 | 16 | ||
6 | $menu-color-link: #9cabb8; | 17 | $button-height: 30px; |
7 | $menu-color-block: #686f77; | ||
8 | 18 | ||
9 | $header-height: 65px; | 19 | $header-height: 50px; |
10 | $header-border-color: #e9eff6; | 20 | $header-border-color: #e9eff6; |
21 | $search-input-width: 375px; | ||
22 | |||
23 | $menu-color: #fff; | ||
24 | $menu-width: 240px; | ||
11 | 25 | ||
12 | $footer-height: 30px; | 26 | $footer-height: 30px; |
13 | $footer-margin: 30px; | 27 | $footer-margin: 30px; |
14 | 28 | ||
15 | $footer-border-color: $header-border-color; | 29 | $footer-border-color: $header-border-color; |
16 | |||
17 | $video-miniature-other-infos: #686767; | ||
18 | |||
19 | $video-watch-border-color: #eceef4; | ||
20 | $video-watch-title-height: 90px; | ||
21 | $video-watch-info-color: #9da0ae; | ||
22 | $video-watch-info-height: 120px; | ||
23 | $video-watch-info-padding-left: 40px; | ||
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss index 47e1b6df0..9d347d566 100644 --- a/client/src/sass/application.scss +++ b/client/src/sass/application.scss | |||
@@ -1,3 +1,5 @@ | |||
1 | $FontPathSourceSansPro: "../fonts/source-sans-pro"; | ||
2 | @import '~source-sans-pro/source-sans-pro'; | ||
1 | @import '~primeng/resources/themes/bootstrap/theme.css'; | 3 | @import '~primeng/resources/themes/bootstrap/theme.css'; |
2 | @import '~primeng/resources/primeng.css'; | 4 | @import '~primeng/resources/primeng.css'; |
3 | @import '~video.js/dist/video-js.css'; | 5 | @import '~video.js/dist/video-js.css'; |
@@ -7,17 +9,30 @@ | |||
7 | display: none !important; | 9 | display: none !important; |
8 | } | 10 | } |
9 | 11 | ||
12 | body { | ||
13 | font-family: 'Source Sans Pro'; | ||
14 | font-weight: $font-regular; | ||
15 | color: #000; | ||
16 | } | ||
17 | |||
10 | input.readonly { | 18 | input.readonly { |
11 | /* Force blank on readonly inputs */ | 19 | /* Force blank on readonly inputs */ |
12 | background-color: #fff !important; | 20 | background-color: #fff !important; |
13 | } | 21 | } |
14 | 22 | ||
15 | .form-control, .btn { | 23 | label { |
16 | border-radius: 0; | 24 | font-weight: $font-bold; |
25 | font-size: 15px; | ||
17 | } | 26 | } |
18 | 27 | ||
19 | .dropdown-menu { | 28 | .form-error { |
20 | border-radius: 0; | 29 | display: block; |
30 | color: $red-error; | ||
31 | margin-top: 5px; | ||
32 | } | ||
33 | |||
34 | .input-error { | ||
35 | border-color: $red-error !important; | ||
21 | } | 36 | } |
22 | 37 | ||
23 | .glyphicon-black { | 38 | .glyphicon-black { |
@@ -25,44 +40,73 @@ input.readonly { | |||
25 | } | 40 | } |
26 | 41 | ||
27 | .main-col { | 42 | .main-col { |
28 | .content-padding { | 43 | margin-left: $menu-width; |
29 | padding: 15px 30px; | ||
30 | 44 | ||
31 | @media screen and (max-width: 800px) { | 45 | .margin-content { |
32 | padding: 15px 10px; | 46 | margin-left: $not-expanded-horizontal-margins; |
33 | } | 47 | margin-right: $not-expanded-horizontal-margins; |
48 | } | ||
34 | 49 | ||
35 | @media screen and (min-width: 1400px) { | 50 | .sub-menu { |
36 | padding: 15px 40px; | 51 | background-color: #F7F7F7; |
37 | } | 52 | width: 100%; |
53 | height: 81px; | ||
54 | margin-bottom: 30px; | ||
55 | display: flex; | ||
56 | align-items: center; | ||
57 | padding-left: $not-expanded-horizontal-margins; | ||
58 | } | ||
38 | 59 | ||
39 | @media screen and (min-width: 1600px) { | 60 | // Override some properties if the main content is expanded (no menu on the left) |
40 | padding: 15px 50px; | 61 | &.expanded { |
62 | margin-left: 0; | ||
63 | |||
64 | .margin-content { | ||
65 | margin-left: $expanded-horizontal-margins; | ||
66 | margin-right: $expanded-horizontal-margins; | ||
41 | } | 67 | } |
42 | 68 | ||
43 | @media screen and (min-width: 1800px) { | 69 | .sub-menu { |
44 | padding: 15px 60px; | 70 | padding-left: $expanded-horizontal-margins; |
45 | } | 71 | } |
46 | } | 72 | } |
47 | } | 73 | } |
48 | 74 | ||
49 | // On small screen, menu is absolute and displayed over the page | 75 | .title-page { |
50 | @media screen and (max-width: 500px) { | 76 | color: #000; |
51 | .title-menu-left { | 77 | font-size: 16px; |
52 | width: 120px; | 78 | display: inline-block; |
53 | position: absolute !important; | 79 | margin-right: 55px; |
54 | z-index: 10000; | 80 | font-weight: $font-semibold; |
81 | @include disable-default-a-behaviour; | ||
82 | |||
83 | &.active, &.title-page-single { | ||
84 | border-bottom: 2px solid $orange-color; | ||
85 | font-weight: $font-bold; | ||
86 | margin-top: 30px; | ||
87 | margin-bottom: 25px; | ||
55 | } | 88 | } |
56 | 89 | ||
57 | .main-col { | 90 | &:hover, &:active, &:focus { |
58 | width: 100% !important; | 91 | color: #000; |
59 | } | 92 | } |
93 | } | ||
94 | |||
95 | .admin-sub-header { | ||
96 | display: flex; | ||
97 | align-items: center; | ||
98 | margin-bottom: 30px; | ||
60 | 99 | ||
61 | .fake-menu { | 100 | .admin-sub-title { |
62 | display: none; | 101 | flex-grow: 1; |
63 | } | 102 | } |
64 | } | 103 | } |
65 | 104 | ||
105 | .admin-sub-title { | ||
106 | font-size: 20px; | ||
107 | font-weight: bold; | ||
108 | } | ||
109 | |||
66 | // Thanks https://gist.github.com/alexandrevicenzi/680147013e902a4eaa5d | 110 | // Thanks https://gist.github.com/alexandrevicenzi/680147013e902a4eaa5d |
67 | .glyphicon-refresh-animate { | 111 | .glyphicon-refresh-animate { |
68 | -animation: spin .7s infinite linear; | 112 | -animation: spin .7s infinite linear; |
@@ -86,13 +130,209 @@ input.readonly { | |||
86 | to { -moz-transform: rotate(360deg);} | 130 | to { -moz-transform: rotate(360deg);} |
87 | } | 131 | } |
88 | 132 | ||
89 | /* ngprime data table customizations */ | 133 | // ngprime data table customizations |
90 | p-datatable { | 134 | p-datatable { |
135 | font-size: 15px !important; | ||
136 | |||
137 | .ui-datatable-scrollable-header { | ||
138 | background-color: #fff !important; | ||
139 | } | ||
140 | |||
141 | .ui-widget-content { | ||
142 | border: none !important; | ||
143 | } | ||
144 | |||
145 | .ui-datatable-virtual-table { | ||
146 | border-top: none !important; | ||
147 | } | ||
148 | |||
149 | td { | ||
150 | border: 1px solid #E5E5E5 !important; | ||
151 | padding-left: 15px !important; | ||
152 | } | ||
153 | |||
154 | tr { | ||
155 | background-color: #fff !important; | ||
156 | height: 46px; | ||
157 | |||
158 | &:hover { | ||
159 | background-color: #f0f0f0 !important; | ||
160 | } | ||
161 | |||
162 | &:not(:hover) { | ||
163 | .action-cell * { | ||
164 | display: none !important; | ||
165 | } | ||
166 | } | ||
167 | |||
168 | &:first-child td { | ||
169 | border-top: none !important; | ||
170 | } | ||
171 | |||
172 | &:last-child td { | ||
173 | border-bottom: none !important; | ||
174 | } | ||
175 | } | ||
176 | |||
177 | th { | ||
178 | border: none !important; | ||
179 | border-bottom: 1px solid #f0f0f0 !important; | ||
180 | text-align: left !important; | ||
181 | padding: 5px 0 5px 15px !important; | ||
182 | font-weight: $font-semibold !important; | ||
183 | color: #000 !important; | ||
184 | |||
185 | &.ui-sortable-column:hover:not(.ui-state-active) { | ||
186 | background-color: #f0f0f0 !important; | ||
187 | border: 1px solid #f0f0f0 !important; | ||
188 | border-width: 0 1px !important; | ||
189 | } | ||
190 | |||
191 | &.ui-state-active { | ||
192 | color: #fff !important; | ||
193 | background-color: $orange-color !important; | ||
194 | border: 1px solid $orange-color !important; | ||
195 | border-width: 0 1px !important; | ||
196 | } | ||
197 | } | ||
198 | |||
91 | .action-cell { | 199 | .action-cell { |
200 | width: 250px !important; | ||
201 | padding: 0 !important; | ||
92 | text-align: center; | 202 | text-align: center; |
203 | } | ||
204 | |||
205 | p-paginator { | ||
206 | .ui-paginator-bottom { | ||
207 | position: relative; | ||
208 | border: none !important; | ||
209 | border: 1px solid #f0f0f0 !important; | ||
210 | height: 40px; | ||
211 | display: flex; | ||
212 | justify-content: center; | ||
213 | align-items: center; | ||
214 | |||
215 | a { | ||
216 | color: #000 !important; | ||
217 | font-weight: $font-semibold !important; | ||
218 | margin-right: 20px !important; | ||
219 | outline: 0 !important; | ||
220 | border-radius: 3px !important; | ||
221 | padding: 5px 2px !important; | ||
222 | |||
223 | &.ui-state-active { | ||
224 | &, &:hover, &:active, &:focus { | ||
225 | color: #fff !important; | ||
226 | background-color: $orange-color !important; | ||
227 | } | ||
228 | } | ||
229 | } | ||
230 | } | ||
231 | } | ||
232 | } | ||
233 | |||
234 | // Bootstrap customizations | ||
235 | .dropdown-menu { | ||
236 | border-radius: 3px; | ||
237 | box-shadow: 0 3px 6px; | ||
238 | font-size: 15px; | ||
239 | |||
240 | .dropdown-item { | ||
241 | padding: 3px 15px; | ||
242 | } | ||
243 | |||
244 | a { | ||
245 | color: #000 !important; | ||
246 | } | ||
247 | } | ||
248 | |||
249 | .modal { | ||
250 | .modal-header { | ||
251 | border-bottom: none; | ||
252 | |||
253 | .title-page-single { | ||
254 | margin: 0; | ||
255 | } | ||
256 | } | ||
257 | } | ||
258 | |||
259 | .nav { | ||
260 | font-size: 16px !important; | ||
261 | border: none !important; | ||
262 | |||
263 | .nav-item .nav-link { | ||
264 | margin-right: 30px; | ||
265 | padding: 0; | ||
266 | border-radius: 3px; | ||
267 | border: none !important; | ||
268 | |||
269 | .tab-link { | ||
270 | display: flex !important; | ||
271 | align-items: center; | ||
272 | height: 30px !important; | ||
273 | padding: 0 15px; | ||
274 | } | ||
275 | |||
276 | &, & a { | ||
277 | color: #000 !important; | ||
278 | @include disable-default-a-behaviour; | ||
279 | } | ||
280 | |||
281 | &.active, &:hover { | ||
282 | background-color: #F0F0F0; | ||
283 | } | ||
284 | |||
285 | &.active { | ||
286 | font-weight: $font-semibold !important; | ||
287 | } | ||
288 | } | ||
289 | } | ||
290 | |||
291 | .orange-button { | ||
292 | @include peertube-button; | ||
293 | @include orange-button; | ||
294 | } | ||
295 | |||
296 | .orange-button-link { | ||
297 | @include peertube-button-link; | ||
298 | @include orange-button; | ||
299 | } | ||
300 | |||
301 | .grey-button { | ||
302 | @include peertube-button; | ||
303 | @include grey-button; | ||
304 | } | ||
305 | |||
306 | .grey-button-link { | ||
307 | @include peertube-button-link; | ||
308 | @include grey-button; | ||
309 | } | ||
310 | |||
311 | // On small screen, menu is absolute | ||
312 | @media screen and (max-width: 800px) { | ||
313 | .title-menu-left { | ||
314 | width: 150px !important; | ||
315 | position: absolute !important; | ||
316 | z-index: 10000; | ||
317 | } | ||
318 | |||
319 | .main-col { | ||
320 | margin-left: 0; | ||
321 | |||
322 | &, &.expanded { | ||
323 | .margin-content { | ||
324 | margin-left: 10px; | ||
325 | margin-right: 10px; | ||
326 | } | ||
327 | |||
328 | .sub-menu { | ||
329 | padding-left: 10px; | ||
330 | margin-bottom: 10px; | ||
331 | } | ||
93 | 332 | ||
94 | .glyphicon { | 333 | input[type=text], input[type=password] { |
95 | cursor: pointer; | 334 | width: 100% !important; |
335 | } | ||
96 | } | 336 | } |
97 | } | 337 | } |
98 | } | 338 | } |
diff --git a/client/src/sass/pre-customizations.scss b/client/src/sass/pre-customizations.scss index 693489828..52eef50f2 100644 --- a/client/src/sass/pre-customizations.scss +++ b/client/src/sass/pre-customizations.scss | |||
@@ -1,4 +1,5 @@ | |||
1 | @import '_variables.scss'; | 1 | @import '_variables.scss'; |
2 | @import '_mixins.scss'; | ||
2 | 3 | ||
3 | $bootstrap-sass-asset-helper: false !default; | 4 | $bootstrap-sass-asset-helper: false !default; |
4 | // | 5 | // |
diff --git a/client/src/sass/video-js-custom.scss b/client/src/sass/video-js-custom.scss index 34a958764..1c5701bea 100644 --- a/client/src/sass/video-js-custom.scss +++ b/client/src/sass/video-js-custom.scss | |||
@@ -1,346 +1,322 @@ | |||
1 | // Thanks: https://github.com/kmoskwiak/videojs-resolution-switcher/pull/92/files | 1 | // Thanks: https://github.com/zanechua/videojs-sublime-inspired-skin |
2 | .vjs-resolution-button-label { | 2 | $primary-foreground-color: #fff; |
3 | font-size: 1em; | 3 | $primary-background-color: #000; |
4 | line-height: 3em; | 4 | $font-size: 13px; |
5 | position: absolute; | 5 | $control-bar-height: 34px; |
6 | top: 0; | ||
7 | left: -1px; | ||
8 | width: 100%; | ||
9 | height: 100%; | ||
10 | text-align: center; | ||
11 | box-sizing: inherit; | ||
12 | } | ||
13 | |||
14 | .vjs-resolution-button { | ||
15 | outline: 0 !important; | ||
16 | 6 | ||
17 | .vjs-menu { | 7 | .video-js.vjs-peertube-skin { |
18 | .vjs-menu-content { | 8 | font-size: $font-size; |
19 | width: 4em; | 9 | color: $primary-foreground-color; |
20 | left: 50%; /* Center the menu, in it's parent */ | ||
21 | margin-left: -2em; /* half of width, to center */ | ||
22 | } | ||
23 | 10 | ||
24 | li { | 11 | .vjs-button > .vjs-icon-placeholder::before { |
25 | text-transform: none; | 12 | line-height: $control-bar-height; |
26 | font-size: 1em; | ||
27 | } | ||
28 | } | 13 | } |
29 | } | ||
30 | 14 | ||
31 | // Thanks: https://github.com/zanechua/videojs-sublime-inspired-skin | 15 | .vjs-mouse-display:before, |
32 | 16 | .vjs-play-progress:before, | |
33 | // Video JS Sublime Skin | 17 | .vjs-volume-level:before { |
34 | // The following are SCSS variables to automate some of the values. | 18 | content: ''; /* Remove Circle From Progress Bar */ |
35 | // But don't feel limited by them. Change/replace whatever you want. | 19 | } |
36 | |||
37 | // The color of icons, text, and the big play button border. | ||
38 | // Try changing to #0f0 | ||
39 | $primary-foreground-color: #fff; // #fff default | ||
40 | 20 | ||
41 | // The default color of control backgrounds is mostly black but with a little | 21 | .vjs-audio-button { |
42 | // bit of blue so it can still be seen on all-black video frames, which are common. | 22 | display: none; |
43 | // Try changing to #900 | 23 | } |
44 | $primary-background-color: #2B333F; // #2B333F default | ||
45 | 24 | ||
46 | // Try changing to true | 25 | .vjs-big-play-button { |
47 | $center-big-play-button: true; // true default | 26 | outline: 0; |
27 | font-size: 8em; | ||
48 | 28 | ||
49 | .video-js { | 29 | $big-play-width: 3em; |
50 | /* The base font size controls the size of everything, not just text. | 30 | $big-play-height: 1.5em; |
51 | All dimensions use em-based sizes so that the scale along with the font size. | ||
52 | Try increasing it to 15px and see what happens. */ | ||
53 | font-size: 10px; | ||
54 | 31 | ||
55 | /* The main font color changes the ICON COLORS as well as the text */ | 32 | border: 0; |
56 | color: $primary-foreground-color; | 33 | border-radius: 0.3em; |
57 | } | ||
58 | 34 | ||
59 | /* The "Big Play Button" is the play button that shows before the video plays. | ||
60 | To center it set the align values to center and middle. The typical location | ||
61 | of the button is the center, but there is trend towards moving it to a corner | ||
62 | where it gets out of the way of valuable content in the poster image.*/ | ||
63 | .vjs-sublime-skin .vjs-big-play-button { | ||
64 | /* The font size is what makes the big play button...big. | ||
65 | All width/height values use ems, which are a multiple of the font size. | ||
66 | If the .video-js font-size is 10px, then 3em equals 30px.*/ | ||
67 | font-size: 8em; | ||
68 | |||
69 | /* We're using SCSS vars here because the values are used in multiple places. | ||
70 | Now that font size is set, the following em values will be a multiple of the | ||
71 | new font size. If the font-size is 3em (30px), then setting any of | ||
72 | the following values to 3em would equal 30px. 3 * font-size. */ | ||
73 | $big-play-width: 3em; | ||
74 | /* 1.5em = 45px default */ | ||
75 | $big-play-height: 1.5em; | ||
76 | |||
77 | line-height: $big-play-height; | ||
78 | height: $big-play-height; | ||
79 | width: $big-play-width; | ||
80 | |||
81 | /* 0.06666em = 2px default */ | ||
82 | border: 0; | ||
83 | /* 0.3em = 9px default */ | ||
84 | border-radius: 0.3em; | ||
85 | |||
86 | @if $center-big-play-button { | ||
87 | /* Align center */ | ||
88 | left: 50%; | 35 | left: 50%; |
89 | top: 50%; | 36 | top: 50%; |
90 | margin-left: -($big-play-width / 2); | 37 | margin-left: -($big-play-width / 2); |
91 | margin-top: -($big-play-height / 2); | 38 | margin-top: -($big-play-height / 2); |
92 | } @else { | 39 | background-color: transparent !important; |
93 | /* Align top left. 0.5em = 15px default */ | ||
94 | left: 0.5em; | ||
95 | top: 0.5em; | ||
96 | } | 40 | } |
97 | } | ||
98 | 41 | ||
99 | /* The default color of control backgrounds is mostly black but with a little | 42 | .vjs-control-bar, |
100 | bit of blue so it can still be seen on all-black video frames, which are common. */ | 43 | .vjs-big-play-button, |
101 | .video-js .vjs-control-bar, | 44 | .vjs-menu-button .vjs-menu-content { |
102 | .video-js .vjs-big-play-button, | 45 | background-color: rgba($primary-background-color, 0.5); |
103 | .video-js .vjs-menu-button .vjs-menu-content { | 46 | } |
104 | /* IE8 - has no alpha support */ | ||
105 | background-color: $primary-background-color; | ||
106 | /* Opacity: 1.0 = 100%, 0.0 = 0% */ | ||
107 | background-color: rgba($primary-background-color, 0.7); | ||
108 | background-color: transparent; | ||
109 | } | ||
110 | 47 | ||
111 | // Make a slightly lighter version of the main background | 48 | $slider-bg-color: lighten($primary-background-color, 33%); |
112 | // for the slider background. | ||
113 | $slider-bg-color: lighten($primary-background-color, 33%); | ||
114 | |||
115 | /* Slider - used for Volume bar and Progress bar */ | ||
116 | .video-js .vjs-slider { | ||
117 | background-color: $slider-bg-color; | ||
118 | background-color: rgba($slider-bg-color, 0.5); | ||
119 | background-color: rgba(255,255,255,.3); | ||
120 | border-radius: 2px; | ||
121 | height: 6.5px; | ||
122 | } | ||
123 | 49 | ||
124 | /* The slider bar color is used for the progress bar and the volume bar | 50 | .vjs-slider { |
125 | (the first two can be removed after a fix that's coming) */ | 51 | background-color: rgba(255, 255, 255, .3); |
126 | .video-js .vjs-volume-level, | 52 | border-radius: 2px; |
127 | .video-js .vjs-play-progress, | 53 | height: 5px; |
128 | .video-js .vjs-slider-bar { | 54 | } |
129 | background: $primary-foreground-color; | ||
130 | } | ||
131 | 55 | ||
132 | /* Enlarged Slider to enable easier tracking. Adjust all the height:6.5px to preferred height for the slider if necessary. */ | 56 | /* The slider bar color is used for the progress bar and the volume bar |
133 | .video-js .vjs-progress-holder .vjs-load-progress, | 57 | (the first two can be removed after a fix that's coming) */ |
134 | .video-js .vjs-progress-holder .vjs-load-progress div, | 58 | .vjs-volume-level, |
135 | .video-js .vjs-progress-holder .vjs-play-progress, | 59 | .vjs-play-progress, |
136 | .video-js .vjs-progress-holder .vjs-tooltip-progress-bar { | 60 | .vjs-slider-bar { |
137 | height: 6.5px; | 61 | background: $primary-foreground-color; |
138 | } | 62 | } |
139 | 63 | ||
140 | /* The main progress bar also has a bar that shows how much has been loaded. */ | 64 | .vjs-load-progress { |
141 | .video-js .vjs-load-progress { | 65 | background: rgba($slider-bg-color, 0.5); |
142 | /* For IE8 we'll lighten the color */ | 66 | } |
143 | background: ligthen($slider-bg-color, 25%); | ||
144 | /* Otherwise we'll rely on stacked opacities */ | ||
145 | background: rgba($slider-bg-color, 0.5); | ||
146 | } | ||
147 | 67 | ||
148 | /* The load progress bar also has internal divs that represent | 68 | .vjs-load-progress div { |
149 | smaller disconnected loaded time ranges */ | 69 | background: rgba($slider-bg-color, 0.75); |
150 | .video-js .vjs-load-progress div { | 70 | } |
151 | /* For IE8 we'll lighten the color */ | ||
152 | background: ligthen($slider-bg-color, 50%); | ||
153 | /* Otherwise we'll rely on stacked opacities */ | ||
154 | background: rgba($slider-bg-color, 0.75); | ||
155 | } | ||
156 | 71 | ||
157 | //Skin Style Starts | 72 | .vjs-poster { |
158 | .vjs-sublime-skin .vjs-poster { | ||
159 | outline: none; /* Remove Blue Outline on Click*/ | 73 | outline: none; /* Remove Blue Outline on Click*/ |
160 | outline: 0; | 74 | outline: 0; |
161 | } | 75 | } |
162 | |||
163 | .vjs-sublime-skin:hover .vjs-big-play-button { | ||
164 | background-color: transparent; | ||
165 | } | ||
166 | |||
167 | .vjs-sublime-skin .vjs-fullscreen-control:before, | ||
168 | .vjs-sublime-skin.vjs-fullscreen .vjs-fullscreen-control:before { | ||
169 | content: ''; /* Remove Fullscreen Exit Icon */ | ||
170 | } | ||
171 | |||
172 | .vjs-sublime-skin.vjs-fullscreen .vjs-fullscreen-control { | ||
173 | background: #fff; | ||
174 | } | ||
175 | |||
176 | .vjs-sublime-skin .vjs-fullscreen-control { | ||
177 | border: 3px solid #fff; | ||
178 | box-sizing: border-box; | ||
179 | cursor: pointer; | ||
180 | margin-top: -7px; | ||
181 | top: 50%; | ||
182 | height: 14px; | ||
183 | width: 22px; | ||
184 | margin-right: 10px; | ||
185 | } | ||
186 | |||
187 | .vjs-sublime-skin.vjs-fullscreen .vjs-fullscreen-control:after { | ||
188 | background: #000; | ||
189 | content: ""; | ||
190 | display: block; | ||
191 | position: absolute; | ||
192 | bottom: 0; | ||
193 | left: 0; | ||
194 | height: 5px; | ||
195 | width: 5px; | ||
196 | } | ||
197 | |||
198 | .vjs-sublime-skin .vjs-progress-holder { | ||
199 | margin: 0; | ||
200 | } | ||
201 | |||
202 | .vjs-sublime-skin .vjs-progress-control .vjs-progress-holder:after { | ||
203 | border-radius: 2px; | ||
204 | display: block; | ||
205 | height: 6.5px; | ||
206 | } | ||
207 | |||
208 | .vjs-sublime-skin .vjs-progress-control .vjs-load-progres, | ||
209 | .vjs-sublime-skin .vjs-progress-control .vjs-play-progress { | ||
210 | border-radius: 2px; | ||
211 | height: 6.5px; | ||
212 | } | ||
213 | |||
214 | .vjs-sublime-skin .vjs-playback-rate { | ||
215 | display: none; /* Remove Playback Rate */ | ||
216 | } | ||
217 | |||
218 | .vjs-sublime-skin .vjs-progress-control { | ||
219 | margin-right: 50px; | ||
220 | } | ||
221 | |||
222 | .vjs-sublime-skin .vjs-time-control { | ||
223 | right: 55px; | ||
224 | } | ||
225 | 76 | ||
226 | .vjs-sublime-skin .vjs-volume-menu-button:before { | 77 | .vjs-control-bar { |
227 | width: 1.2em; | 78 | height: $control-bar-height; |
228 | z-index: 1; | ||
229 | } | ||
230 | 79 | ||
231 | .vjs-sublime-skin .vjs-volume-menu-button .vjs-menu, | 80 | .vjs-progress-control { |
232 | .vjs-sublime-skin .vjs-volume-menu-button:focus .vjs-menu, | 81 | bottom: 34px; |
233 | .vjs-sublime-skin .vjs-volume-menu-button.vjs-slider-active .vjs-menu { | 82 | width: 100%; |
234 | display: block; | 83 | position: absolute; |
235 | opacity: 1; | 84 | height: 5px; |
236 | } | ||
237 | 85 | ||
238 | .vjs-sublime-skin .vjs-volume-menu-button, | 86 | .vjs-progress-holder { |
239 | .vjs-sublime-skin .vjs-volume-panel { | 87 | margin: 0; |
240 | width: 6em; | 88 | border-radius: 0; |
241 | position: absolute; | 89 | } |
242 | right: 0; | 90 | } |
243 | margin-right: 65px; | ||
244 | } | ||
245 | 91 | ||
246 | .vjs-sublime-skin .vjs-volume-menu-button .vjs-menu-content, | 92 | .vjs-play-control { |
247 | .vjs-sublime-skin .vjs-volume-menu-button:hover, | 93 | font-size: $font-size; |
248 | .vjs-sublime-skin .vjs-volume-menu-button:focus, | 94 | padding: 0 17px; |
249 | .vjs-sublime-skin .vjs-volume-menu-button.vjs-slider-active, | 95 | margin-right: 5px; |
250 | .vjs-sublime-skin .vjs-volume-panel .vjs-volume-control, | 96 | } |
251 | .vjs-sublime-skin .vjs-volume-panel:hover, | ||
252 | .vjs-sublime-skin .vjs-volume-panel:focus, | ||
253 | .vjs-sublime-skin .vjs-volume-panel.vjs-slider-active { | ||
254 | width: 6em; | ||
255 | } | ||
256 | 97 | ||
257 | .vjs-sublime-skin .vjs-volume-menu-button .vjs-menu { | 98 | .vjs-time-control { |
258 | left: 23px; | 99 | &.vjs-current-time { |
259 | } | 100 | font-size: $font-size; |
101 | display: inline-block; | ||
102 | padding: 0; | ||
103 | |||
104 | .vjs-current-time-display { | ||
105 | line-height: $control-bar-height; | ||
106 | |||
107 | &::after { | ||
108 | content: "/"; | ||
109 | margin: 0 1px 0 2px; | ||
110 | } | ||
111 | } | ||
112 | } | ||
113 | |||
114 | &.vjs-duration { | ||
115 | font-size: $font-size; | ||
116 | display: inline-block; | ||
117 | padding: 0; | ||
118 | |||
119 | .vjs-duration-display { | ||
120 | line-height: $control-bar-height; | ||
121 | } | ||
122 | } | ||
123 | |||
124 | &.vjs-remaining-time { | ||
125 | display: none; | ||
126 | } | ||
127 | } | ||
260 | 128 | ||
261 | .vjs-sublime-skin .vjs-mouse-display:before, | 129 | .vjs-webtorrent { |
262 | .vjs-sublime-skin .vjs-play-progress:before, | 130 | width: 100%; |
263 | .vjs-sublime-skin .vjs-volume-level:before { | 131 | line-height: $control-bar-height; |
264 | content: ''; /* Remove Circle From Progress Bar */ | 132 | text-align: right; |
265 | } | 133 | padding-right: 60px; |
134 | |||
135 | .vjs-webtorrent-displayed { | ||
136 | display: block; | ||
137 | } | ||
138 | |||
139 | .vjs-webtorrent-hidden { | ||
140 | display: none; | ||
141 | } | ||
142 | |||
143 | .download-speed-number, .upload-speed-number, .peers-number { | ||
144 | font-weight: $font-semibold; | ||
145 | } | ||
146 | |||
147 | .download-speed-text, .upload-speed-text, .peers-text { | ||
148 | margin-right: 15px; | ||
149 | } | ||
150 | |||
151 | .icon { | ||
152 | display: inline-block; | ||
153 | width: 15px; | ||
154 | height: 15px; | ||
155 | background-size: contain; | ||
156 | vertical-align: middle; | ||
157 | background-repeat: no-repeat; | ||
158 | margin-right: 6px; | ||
159 | position: relative; | ||
160 | top: -1px; | ||
161 | |||
162 | &.icon-download { | ||
163 | background-image: url('../assets/player/images/arrow-down.svg'); | ||
164 | } | ||
165 | |||
166 | &.icon-upload { | ||
167 | background-image: url('../assets/player/images/arrow-up.svg'); | ||
168 | } | ||
169 | } | ||
170 | } | ||
266 | 171 | ||
267 | .vjs-sublime-skin .vjs-mouse-display:after, | 172 | .vjs-mute-control { |
268 | .vjs-sublime-skin .vjs-play-progress:after, | 173 | .vjs-icon-placeholder { |
269 | .vjs-sublime-skin .vjs-time-tooltip { | 174 | display: inline-block; |
270 | width: 5.5em; | 175 | width: 22px; |
271 | } | 176 | height: 22px; |
177 | vertical-align: middle; | ||
178 | background: url('../assets/player/images/volume.svg') no-repeat; | ||
179 | background-size: contain; | ||
180 | |||
181 | &::before { | ||
182 | content: ''; | ||
183 | } | ||
184 | } | ||
185 | |||
186 | &.vjs-vol-0 .vjs-icon-placeholder { | ||
187 | background: url('../assets/player/images/volume-mute.svg') no-repeat; | ||
188 | background-size: contain; | ||
189 | } | ||
190 | } | ||
272 | 191 | ||
273 | .vjs-sublime-skin .vjs-audio-button { | 192 | .vjs-volume-menu-button, |
274 | display: none; | 193 | .vjs-volume-panel { |
275 | } | 194 | width: 6em; |
195 | position: absolute; | ||
196 | right: 0; | ||
197 | margin-right: 65px; | ||
198 | } | ||
276 | 199 | ||
277 | .vjs-sublime-skin .vjs-volume-bar { | 200 | .vjs-volume-bar { |
278 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAcCAQAAACw95UnAAAAMElEQVRIx2NgoBL4n4YKGUYNHkEG4zJg1OCRYDCpBowaPJwMppbLRg0eNXjUYBLEAXWNUA6QNm1lAAAAAElFTkSuQmCC); | 201 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAcCAQAAACw95UnAAAAMElEQVRIx2NgoBL4n4YKGUYNHkEG4zJg1OCRYDCpBowaPJwMppbLRg0eNXjUYBLEAXWNUA6QNm1lAAAAAElFTkSuQmCC) no-repeat; |
279 | background-size: 22px 14px; | 202 | background-size: 22px 14px; |
280 | background-repeat: no-repeat; | 203 | height: 100%; |
281 | height: 100%; | 204 | width: 100%; |
282 | width: 100%; | 205 | max-width: 22px; |
283 | max-width: 22px; | 206 | max-height: 14px; |
284 | max-height: 14px; | 207 | margin: 7px 4px; |
285 | margin: 7px 4px; | 208 | border-radius: 0; |
286 | border-radius: 0; | 209 | top: 3px; |
287 | } | 210 | |
211 | .vjs-volume-level { | ||
212 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAcAQAAAAAyhWABAAAAAnRSTlMAAHaTzTgAAAAZSURBVHgBYwAB/g9EUv+JokCqiaT+U4MCAPKPS7WUUOc1AAAAAElFTkSuQmCC) no-repeat; | ||
213 | background-size: 22px 14px; | ||
214 | max-width: 22px; | ||
215 | max-height: 14px; | ||
216 | height: 100%; | ||
217 | } | ||
218 | } | ||
288 | 219 | ||
289 | .vjs-sublime-skin .vjs-volume-level { | 220 | .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active, |
290 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAcAQAAAAAyhWABAAAAAnRSTlMAAHaTzTgAAAAZSURBVHgBYwAB/g9EUv+JokCqiaT+U4MCAPKPS7WUUOc1AAAAAElFTkSuQmCC); | 221 | .vjs-volume-panel.vjs-volume-panel-horizontal:active, |
291 | background-size: 22px 14px; | 222 | .vjs-volume-panel.vjs-volume-panel-horizontal:focus, |
292 | background-repeat: no-repeat; | 223 | .vjs-volume-panel.vjs-volume-panel-horizontal:hover { |
293 | max-width: 22px; | 224 | width: 6em; |
294 | max-height: 14px; | 225 | transition-property: none; |
295 | height: 100%; | 226 | } |
296 | } | ||
297 | 227 | ||
298 | /* New for VideoJS v6 */ | 228 | .vjs-volume-panel .vjs-mute-control:hover ~ .vjs-volume-control.vjs-volume-horizontal { |
299 | .vjs-sublime-skin .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active, | 229 | width: 3em; |
300 | .vjs-sublime-skin .vjs-volume-panel.vjs-volume-panel-horizontal:active, | 230 | height: auto; |
301 | .vjs-sublime-skin .vjs-volume-panel.vjs-volume-panel-horizontal:focus, | 231 | } |
302 | .vjs-sublime-skin .vjs-volume-panel.vjs-volume-panel-horizontal:hover { | ||
303 | width: 6em; | ||
304 | transition-property: none; | ||
305 | } | ||
306 | 232 | ||
307 | .vjs-sublime-skin .vjs-volume-panel .vjs-mute-control:hover ~ .vjs-volume-control.vjs-volume-horizontal { | 233 | .vjs-volume-panel .vjs-mute-control:hover ~ .vjs-volume-control { |
308 | width: 3em; | 234 | transition-property: none; |
309 | height: auto; | 235 | } |
310 | } | ||
311 | 236 | ||
312 | .vjs-sublime-skin .vjs-volume-panel .vjs-mute-control:hover ~ .vjs-volume-control { | 237 | .vjs-volume-panel { |
313 | transition-property: none; | 238 | .vjs-mute-control { |
314 | } | 239 | width: 2em; |
240 | z-index: 1; | ||
241 | padding: 0; | ||
242 | } | ||
243 | |||
244 | .vjs-volume-control { | ||
245 | display: inline-block; | ||
246 | position: relative; | ||
247 | left: 5px; | ||
248 | opacity: 1; | ||
249 | width: 3em; | ||
250 | height: auto; | ||
251 | } | ||
252 | } | ||
315 | 253 | ||
316 | .vjs-sublime-skin .vjs-fullscreen-control .vjs-icon-placeholder { | 254 | .vjs-fullscreen-control { |
317 | display: none; /* Remove Duplicate Fullscreen Icon */ | 255 | width: 37px; |
318 | } | 256 | |
257 | .vjs-icon-placeholder { | ||
258 | display: inline-block; | ||
259 | width: 22px; | ||
260 | height: 22px; | ||
261 | vertical-align: middle; | ||
262 | background: url('../assets/player/images/fullscreen.svg') no-repeat; | ||
263 | background-size: contain; | ||
264 | |||
265 | &::before { | ||
266 | content: ''; | ||
267 | } | ||
268 | } | ||
269 | } | ||
319 | 270 | ||
320 | .vjs-sublime-skin .vjs-volume-panel .vjs-mute-control { | 271 | .vjs-menu-button-popup { |
321 | width: 2em; | 272 | font-size: 13px; |
322 | z-index: 1; | 273 | font-weight: $font-semibold; |
323 | padding: 0; | 274 | width: 42px; |
324 | } | 275 | |
276 | // Thanks: https://github.com/kmoskwiak/videojs-resolution-switcher/pull/92/files | ||
277 | .vjs-resolution-button-label { | ||
278 | line-height: $control-bar-height; | ||
279 | position: absolute; | ||
280 | top: 0; | ||
281 | left: -1px; | ||
282 | width: 100%; | ||
283 | height: 100%; | ||
284 | text-align: center; | ||
285 | box-sizing: inherit; | ||
286 | } | ||
287 | |||
288 | .vjs-resolution-button { | ||
289 | outline: 0 !important; | ||
290 | } | ||
291 | |||
292 | .vjs-menu { | ||
293 | top: 20px; | ||
294 | |||
295 | .vjs-menu-content { | ||
296 | width: 4em; | ||
297 | left: 50%; /* Center the menu, in it's parent */ | ||
298 | margin-left: -2em; /* half of width, to center */ | ||
299 | } | ||
300 | |||
301 | li { | ||
302 | text-transform: none; | ||
303 | font-size: 13px; | ||
304 | } | ||
305 | } | ||
306 | } | ||
307 | } | ||
325 | 308 | ||
326 | .vjs-sublime-skin .vjs-volume-panel .vjs-volume-control { | 309 | @media screen and (max-width: 450px) { |
327 | display: inline-block; | 310 | .vjs-webtorrent-displayed { |
328 | position: relative; | 311 | display: none !important; |
329 | left: 5px; | 312 | } |
330 | opacity: 1; | 313 | } |
331 | width: 3em; | ||
332 | height: auto; | ||
333 | } | 314 | } |
334 | 315 | ||
335 | // Thanks: https://projects.lukehaas.me/css-loaders/ | 316 | // Thanks: https://projects.lukehaas.me/css-loaders/ |
336 | .vjs-loading-spinner { | 317 | .vjs-loading-spinner { |
337 | margin: 0 !important; | ||
338 | position: absolute; | ||
339 | // 15px is the nav bar height | ||
340 | top: calc(50% - 15px); | ||
341 | left: 50%; | 318 | left: 50%; |
342 | font-size: 10px; | 319 | font-size: 10px; |
343 | position: relative; | ||
344 | text-indent: -9999em; | 320 | text-indent: -9999em; |
345 | border: 0.7em solid rgba(255, 255, 255, 0.2); | 321 | border: 0.7em solid rgba(255, 255, 255, 0.2); |
346 | border-left-color: #ffffff; | 322 | border-left-color: #ffffff; |
@@ -367,3 +343,4 @@ $slider-bg-color: lighten($primary-background-color, 33%); | |||
367 | } | 343 | } |
368 | } | 344 | } |
369 | } | 345 | } |
346 | |||
diff --git a/client/src/standalone/videos/embed.html b/client/src/standalone/videos/embed.html index 0a35bc362..fa4d0bdba 100644 --- a/client/src/standalone/videos/embed.html +++ b/client/src/standalone/videos/embed.html | |||
@@ -11,7 +11,7 @@ | |||
11 | 11 | ||
12 | <body> | 12 | <body> |
13 | 13 | ||
14 | <video id="video-container" class="video-js vjs-sublime-skin vjs-big-play-centered"> | 14 | <video id="video-container" class="video-js vjs-peertube-skin"> |
15 | </video> | 15 | </video> |
16 | 16 | ||
17 | </body> | 17 | </body> |
diff --git a/client/src/standalone/videos/embed.scss b/client/src/standalone/videos/embed.scss index b76f09677..9140cd37c 100644 --- a/client/src/standalone/videos/embed.scss +++ b/client/src/standalone/videos/embed.scss | |||
@@ -23,17 +23,13 @@ html, body { | |||
23 | } | 23 | } |
24 | 24 | ||
25 | .vjs-peertube-link { | 25 | .vjs-peertube-link { |
26 | color: white; | 26 | color: #fff; |
27 | text-decoration: none; | 27 | text-decoration: none; |
28 | font-size: 1.3em; | 28 | font-size: $font-size; |
29 | line-height: 2.20; | 29 | line-height: $control-bar-height; |
30 | transition: all .4s; | 30 | transition: all .4s; |
31 | position: relative; | 31 | font-weight: $font-semibold; |
32 | right: 8px; | 32 | margin-right: 3px; |
33 | } | ||
34 | |||
35 | .vjs-resolution-button-label { | ||
36 | left: -7px; | ||
37 | } | 33 | } |
38 | 34 | ||
39 | .vjs-peertube-link:hover { | 35 | .vjs-peertube-link:hover { |
@@ -42,5 +38,21 @@ html, body { | |||
42 | 38 | ||
43 | // Fix volume panel because we added a new component (PeerTube link) | 39 | // Fix volume panel because we added a new component (PeerTube link) |
44 | .vjs-volume-panel { | 40 | .vjs-volume-panel { |
45 | margin-right: 130px !important; | 41 | margin-right: 121px !important; |
42 | } | ||
43 | |||
44 | @media screen and (max-width: 350px) { | ||
45 | .vjs-play-control { | ||
46 | padding: 0 5px !important; | ||
47 | width: 25px !important; | ||
48 | } | ||
49 | |||
50 | .vjs-volume-control { | ||
51 | display: none !important; | ||
52 | } | ||
53 | |||
54 | .vjs-volume-panel { | ||
55 | width: 26px !important; | ||
56 | margin-right: 140px !important; | ||
57 | } | ||
46 | } | 58 | } |
diff --git a/client/yarn.lock b/client/yarn.lock index c5a47bb89..bd6870061 100644 --- a/client/yarn.lock +++ b/client/yarn.lock | |||
@@ -264,10 +264,6 @@ amdefine@>=0.0.4: | |||
264 | version "1.0.1" | 264 | version "1.0.1" |
265 | resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" | 265 | resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" |
266 | 266 | ||
267 | angular-pipes@^6.0.0: | ||
268 | version "6.5.3" | ||
269 | resolved "https://registry.yarnpkg.com/angular-pipes/-/angular-pipes-6.5.3.tgz#6bed37c51ebc2adaf3412663bfe25179d0489b02" | ||
270 | |||
271 | angular2-notifications@^0.7.7: | 267 | angular2-notifications@^0.7.7: |
272 | version "0.7.8" | 268 | version "0.7.8" |
273 | resolved "https://registry.yarnpkg.com/angular2-notifications/-/angular2-notifications-0.7.8.tgz#ecbcb95a8d2d402af94a9a080d6664c70d33a029" | 269 | resolved "https://registry.yarnpkg.com/angular2-notifications/-/angular2-notifications-0.7.8.tgz#ecbcb95a8d2d402af94a9a080d6664c70d33a029" |
@@ -4708,9 +4704,9 @@ ngc-webpack@3.2.2: | |||
4708 | source-map "^0.5.6" | 4704 | source-map "^0.5.6" |
4709 | ts-node "^3.2.0" | 4705 | ts-node "^3.2.0" |
4710 | 4706 | ||
4711 | ngx-bootstrap@1.9.3: | 4707 | ngx-bootstrap@2.0.0-beta.9: |
4712 | version "1.9.3" | 4708 | version "2.0.0-beta.9" |
4713 | resolved "https://registry.yarnpkg.com/ngx-bootstrap/-/ngx-bootstrap-1.9.3.tgz#28e75d14fb1beaee609383d7694de4eb3ba03b26" | 4709 | resolved "https://registry.yarnpkg.com/ngx-bootstrap/-/ngx-bootstrap-2.0.0-beta.9.tgz#9aa7c88269534e7a5440481f31b137549f749796" |
4714 | 4710 | ||
4715 | ngx-chips@1.5.3: | 4711 | ngx-chips@1.5.3: |
4716 | version "1.5.3" | 4712 | version "1.5.3" |
@@ -4718,6 +4714,14 @@ ngx-chips@1.5.3: | |||
4718 | dependencies: | 4714 | dependencies: |
4719 | ng2-material-dropdown "0.7.10" | 4715 | ng2-material-dropdown "0.7.10" |
4720 | 4716 | ||
4717 | ngx-infinite-scroll@^0.7.0: | ||
4718 | version "0.7.0" | ||
4719 | resolved "https://registry.yarnpkg.com/ngx-infinite-scroll/-/ngx-infinite-scroll-0.7.0.tgz#a390c61c6a05ac14485e1c5bc8b4e6f6bd62fd6a" | ||
4720 | |||
4721 | ngx-pipes@^2.0.5: | ||
4722 | version "2.0.5" | ||
4723 | resolved "https://registry.yarnpkg.com/ngx-pipes/-/ngx-pipes-2.0.5.tgz#743b827e350b1e66f5bdae49e90a02fa631d4c54" | ||
4724 | |||
4721 | no-case@^2.2.0: | 4725 | no-case@^2.2.0: |
4722 | version "2.3.2" | 4726 | version "2.3.2" |
4723 | resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" | 4727 | resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" |
@@ -6602,6 +6606,10 @@ source-map@^0.6.1, source-map@~0.6.1: | |||
6602 | version "0.6.1" | 6606 | version "0.6.1" |
6603 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" | 6607 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" |
6604 | 6608 | ||
6609 | source-sans-pro@^2.0.10: | ||
6610 | version "2.0.10" | ||
6611 | resolved "https://registry.yarnpkg.com/source-sans-pro/-/source-sans-pro-2.0.10.tgz#c1ca859cf164a088944c5e83745085e87cd533a9" | ||
6612 | |||
6605 | spdx-correct@~1.0.0: | 6613 | spdx-correct@~1.0.0: |
6606 | version "1.0.2" | 6614 | version "1.0.2" |
6607 | resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" | 6615 | resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" |
diff --git a/config/default.yaml b/config/default.yaml index b53fa0d5b..2c1043067 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -16,6 +16,7 @@ database: | |||
16 | 16 | ||
17 | # From the project root directory | 17 | # From the project root directory |
18 | storage: | 18 | storage: |
19 | avatars: 'avatars/' | ||
19 | certs: 'certs/' | 20 | certs: 'certs/' |
20 | videos: 'videos/' | 21 | videos: 'videos/' |
21 | logs: 'logs/' | 22 | logs: 'logs/' |
diff --git a/config/production.yaml.example b/config/production.yaml.example index 1af20a9e4..404d35c16 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example | |||
@@ -17,6 +17,7 @@ database: | |||
17 | 17 | ||
18 | # From the project root directory | 18 | # From the project root directory |
19 | storage: | 19 | storage: |
20 | avatars: 'avatars/' | ||
20 | certs: 'certs/' | 21 | certs: 'certs/' |
21 | videos: 'videos/' | 22 | videos: 'videos/' |
22 | logs: 'logs/' | 23 | logs: 'logs/' |
diff --git a/config/test-1.yaml b/config/test-1.yaml index d9b4d2b1a..49fbebf04 100644 --- a/config/test-1.yaml +++ b/config/test-1.yaml | |||
@@ -10,6 +10,7 @@ database: | |||
10 | 10 | ||
11 | # From the project root directory | 11 | # From the project root directory |
12 | storage: | 12 | storage: |
13 | avatars: 'test1/avatars/' | ||
13 | certs: 'test1/certs/' | 14 | certs: 'test1/certs/' |
14 | videos: 'test1/videos/' | 15 | videos: 'test1/videos/' |
15 | logs: 'test1/logs/' | 16 | logs: 'test1/logs/' |
diff --git a/config/test-2.yaml b/config/test-2.yaml index 236dcb10d..ff0df5962 100644 --- a/config/test-2.yaml +++ b/config/test-2.yaml | |||
@@ -10,6 +10,7 @@ database: | |||
10 | 10 | ||
11 | # From the project root directory | 11 | # From the project root directory |
12 | storage: | 12 | storage: |
13 | avatars: 'test2/avatars/' | ||
13 | certs: 'test2/certs/' | 14 | certs: 'test2/certs/' |
14 | videos: 'test2/videos/' | 15 | videos: 'test2/videos/' |
15 | logs: 'test2/logs/' | 16 | logs: 'test2/logs/' |
diff --git a/config/test-3.yaml b/config/test-3.yaml index 291b43edc..4fbb00050 100644 --- a/config/test-3.yaml +++ b/config/test-3.yaml | |||
@@ -10,6 +10,7 @@ database: | |||
10 | 10 | ||
11 | # From the project root directory | 11 | # From the project root directory |
12 | storage: | 12 | storage: |
13 | avatars: 'test3/avatars/' | ||
13 | certs: 'test3/certs/' | 14 | certs: 'test3/certs/' |
14 | videos: 'test3/videos/' | 15 | videos: 'test3/videos/' |
15 | logs: 'test3/logs/' | 16 | logs: 'test3/logs/' |
diff --git a/config/test-4.yaml b/config/test-4.yaml index 6f80939fc..e4f0f2691 100644 --- a/config/test-4.yaml +++ b/config/test-4.yaml | |||
@@ -10,6 +10,7 @@ database: | |||
10 | 10 | ||
11 | # From the project root directory | 11 | # From the project root directory |
12 | storage: | 12 | storage: |
13 | avatars: 'test4/avatars/' | ||
13 | certs: 'test4/certs/' | 14 | certs: 'test4/certs/' |
14 | videos: 'test4/videos/' | 15 | videos: 'test4/videos/' |
15 | logs: 'test4/logs/' | 16 | logs: 'test4/logs/' |
diff --git a/config/test-5.yaml b/config/test-5.yaml index 0b5eab72e..610f523c8 100644 --- a/config/test-5.yaml +++ b/config/test-5.yaml | |||
@@ -10,6 +10,7 @@ database: | |||
10 | 10 | ||
11 | # From the project root directory | 11 | # From the project root directory |
12 | storage: | 12 | storage: |
13 | avatars: 'test5/avatars/' | ||
13 | certs: 'test5/certs/' | 14 | certs: 'test5/certs/' |
14 | videos: 'test5/videos/' | 15 | videos: 'test5/videos/' |
15 | logs: 'test5/logs/' | 16 | logs: 'test5/logs/' |
diff --git a/config/test-6.yaml b/config/test-6.yaml index 5d33e45b9..088b55c17 100644 --- a/config/test-6.yaml +++ b/config/test-6.yaml | |||
@@ -10,6 +10,7 @@ database: | |||
10 | 10 | ||
11 | # From the project root directory | 11 | # From the project root directory |
12 | storage: | 12 | storage: |
13 | avatars: 'test6/avatars/' | ||
13 | certs: 'test6/certs/' | 14 | certs: 'test6/certs/' |
14 | videos: 'test6/videos/' | 15 | videos: 'test6/videos/' |
15 | logs: 'test6/logs/' | 16 | logs: 'test6/logs/' |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index e2798830e..63de662a7 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -15,6 +15,7 @@ import { getServerAccount } from '../../../helpers/utils' | |||
15 | import { CONFIG, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT, VIDEO_PRIVACIES } from '../../../initializers' | 15 | import { CONFIG, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT, VIDEO_PRIVACIES } from '../../../initializers' |
16 | import { database as db } from '../../../initializers/database' | 16 | import { database as db } from '../../../initializers/database' |
17 | import { sendAddVideo } from '../../../lib/activitypub/send/send-add' | 17 | import { sendAddVideo } from '../../../lib/activitypub/send/send-add' |
18 | import { sendCreateViewToOrigin } from '../../../lib/activitypub/send/send-create' | ||
18 | import { sendUpdateVideo } from '../../../lib/activitypub/send/send-update' | 19 | import { sendUpdateVideo } from '../../../lib/activitypub/send/send-update' |
19 | import { shareVideoByServer } from '../../../lib/activitypub/share' | 20 | import { shareVideoByServer } from '../../../lib/activitypub/share' |
20 | import { getVideoActivityPubUrl } from '../../../lib/activitypub/url' | 21 | import { getVideoActivityPubUrl } from '../../../lib/activitypub/url' |
@@ -26,7 +27,6 @@ import { | |||
26 | authenticate, | 27 | authenticate, |
27 | paginationValidator, | 28 | paginationValidator, |
28 | setPagination, | 29 | setPagination, |
29 | setVideosSearch, | ||
30 | setVideosSort, | 30 | setVideosSort, |
31 | videosAddValidator, | 31 | videosAddValidator, |
32 | videosGetValidator, | 32 | videosGetValidator, |
@@ -40,7 +40,6 @@ import { abuseVideoRouter } from './abuse' | |||
40 | import { blacklistRouter } from './blacklist' | 40 | import { blacklistRouter } from './blacklist' |
41 | import { videoChannelRouter } from './channel' | 41 | import { videoChannelRouter } from './channel' |
42 | import { rateVideoRouter } from './rate' | 42 | import { rateVideoRouter } from './rate' |
43 | import { sendCreateViewToOrigin } from '../../../lib/activitypub/send/send-create' | ||
44 | 43 | ||
45 | const videosRouter = express.Router() | 44 | const videosRouter = express.Router() |
46 | 45 | ||
@@ -84,6 +83,14 @@ videosRouter.get('/', | |||
84 | setPagination, | 83 | setPagination, |
85 | asyncMiddleware(listVideos) | 84 | asyncMiddleware(listVideos) |
86 | ) | 85 | ) |
86 | videosRouter.get('/search', | ||
87 | videosSearchValidator, | ||
88 | paginationValidator, | ||
89 | videosSortValidator, | ||
90 | setVideosSort, | ||
91 | setPagination, | ||
92 | asyncMiddleware(searchVideos) | ||
93 | ) | ||
87 | videosRouter.put('/:id', | 94 | videosRouter.put('/:id', |
88 | authenticate, | 95 | authenticate, |
89 | asyncMiddleware(videosUpdateValidator), | 96 | asyncMiddleware(videosUpdateValidator), |
@@ -115,16 +122,6 @@ videosRouter.delete('/:id', | |||
115 | asyncMiddleware(removeVideoRetryWrapper) | 122 | asyncMiddleware(removeVideoRetryWrapper) |
116 | ) | 123 | ) |
117 | 124 | ||
118 | videosRouter.get('/search/:value', | ||
119 | videosSearchValidator, | ||
120 | paginationValidator, | ||
121 | videosSortValidator, | ||
122 | setVideosSort, | ||
123 | setPagination, | ||
124 | setVideosSearch, | ||
125 | asyncMiddleware(searchVideos) | ||
126 | ) | ||
127 | |||
128 | // --------------------------------------------------------------------------- | 125 | // --------------------------------------------------------------------------- |
129 | 126 | ||
130 | export { | 127 | export { |
@@ -157,59 +154,64 @@ async function addVideoRetryWrapper (req: express.Request, res: express.Response | |||
157 | errorMessage: 'Cannot insert the video with many retries.' | 154 | errorMessage: 'Cannot insert the video with many retries.' |
158 | } | 155 | } |
159 | 156 | ||
160 | await retryTransactionWrapper(addVideo, options) | 157 | const video = await retryTransactionWrapper(addVideo, options) |
161 | 158 | ||
162 | // TODO : include Location of the new video -> 201 | 159 | res.json({ |
163 | res.type('json').status(204).end() | 160 | video: { |
161 | id: video.id, | ||
162 | uuid: video.uuid | ||
163 | } | ||
164 | }).end() | ||
164 | } | 165 | } |
165 | 166 | ||
166 | async function addVideo (req: express.Request, res: express.Response, videoPhysicalFile: Express.Multer.File) { | 167 | async function addVideo (req: express.Request, res: express.Response, videoPhysicalFile: Express.Multer.File) { |
167 | const videoInfo: VideoCreate = req.body | 168 | const videoInfo: VideoCreate = req.body |
168 | let videoUUID = '' | ||
169 | 169 | ||
170 | await db.sequelize.transaction(async t => { | 170 | // Prepare data so we don't block the transaction |
171 | const sequelizeOptions = { transaction: t } | 171 | const videoData = { |
172 | name: videoInfo.name, | ||
173 | remote: false, | ||
174 | extname: extname(videoPhysicalFile.filename), | ||
175 | category: videoInfo.category, | ||
176 | licence: videoInfo.licence, | ||
177 | language: videoInfo.language, | ||
178 | nsfw: videoInfo.nsfw, | ||
179 | description: videoInfo.description, | ||
180 | privacy: videoInfo.privacy, | ||
181 | duration: videoPhysicalFile['duration'], // duration was added by a previous middleware | ||
182 | channelId: res.locals.videoChannel.id | ||
183 | } | ||
184 | const video = db.Video.build(videoData) | ||
185 | video.url = getVideoActivityPubUrl(video) | ||
172 | 186 | ||
173 | const videoData = { | 187 | const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename) |
174 | name: videoInfo.name, | 188 | const videoFileHeight = await getVideoFileHeight(videoFilePath) |
175 | remote: false, | ||
176 | extname: extname(videoPhysicalFile.filename), | ||
177 | category: videoInfo.category, | ||
178 | licence: videoInfo.licence, | ||
179 | language: videoInfo.language, | ||
180 | nsfw: videoInfo.nsfw, | ||
181 | description: videoInfo.description, | ||
182 | privacy: videoInfo.privacy, | ||
183 | duration: videoPhysicalFile['duration'], // duration was added by a previous middleware | ||
184 | channelId: res.locals.videoChannel.id | ||
185 | } | ||
186 | const video = db.Video.build(videoData) | ||
187 | video.url = getVideoActivityPubUrl(video) | ||
188 | 189 | ||
189 | const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename) | 190 | const videoFileData = { |
190 | const videoFileHeight = await getVideoFileHeight(videoFilePath) | 191 | extname: extname(videoPhysicalFile.filename), |
192 | resolution: videoFileHeight, | ||
193 | size: videoPhysicalFile.size | ||
194 | } | ||
195 | const videoFile = db.VideoFile.build(videoFileData) | ||
196 | const videoDir = CONFIG.STORAGE.VIDEOS_DIR | ||
197 | const source = join(videoDir, videoPhysicalFile.filename) | ||
198 | const destination = join(videoDir, video.getVideoFilename(videoFile)) | ||
191 | 199 | ||
192 | const videoFileData = { | 200 | await renamePromise(source, destination) |
193 | extname: extname(videoPhysicalFile.filename), | 201 | // This is important in case if there is another attempt in the retry process |
194 | resolution: videoFileHeight, | 202 | videoPhysicalFile.filename = video.getVideoFilename(videoFile) |
195 | size: videoPhysicalFile.size | ||
196 | } | ||
197 | const videoFile = db.VideoFile.build(videoFileData) | ||
198 | const videoDir = CONFIG.STORAGE.VIDEOS_DIR | ||
199 | const source = join(videoDir, videoPhysicalFile.filename) | ||
200 | const destination = join(videoDir, video.getVideoFilename(videoFile)) | ||
201 | 203 | ||
202 | await renamePromise(source, destination) | 204 | const tasks = [] |
203 | // This is important in case if there is another attempt in the retry process | ||
204 | videoPhysicalFile.filename = video.getVideoFilename(videoFile) | ||
205 | 205 | ||
206 | const tasks = [] | 206 | tasks.push( |
207 | video.createTorrentAndSetInfoHash(videoFile), | ||
208 | video.createThumbnail(videoFile), | ||
209 | video.createPreview(videoFile) | ||
210 | ) | ||
211 | await Promise.all(tasks) | ||
207 | 212 | ||
208 | tasks.push( | 213 | return db.sequelize.transaction(async t => { |
209 | video.createTorrentAndSetInfoHash(videoFile), | 214 | const sequelizeOptions = { transaction: t } |
210 | video.createThumbnail(videoFile), | ||
211 | video.createPreview(videoFile) | ||
212 | ) | ||
213 | 215 | ||
214 | if (CONFIG.TRANSCODING.ENABLED === true) { | 216 | if (CONFIG.TRANSCODING.ENABLED === true) { |
215 | // Put uuid because we don't have id auto incremented for now | 217 | // Put uuid because we don't have id auto incremented for now |
@@ -217,21 +219,17 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi | |||
217 | videoUUID: video.uuid | 219 | videoUUID: video.uuid |
218 | } | 220 | } |
219 | 221 | ||
220 | tasks.push( | 222 | await transcodingJobScheduler.createJob(t, 'videoFileOptimizer', dataInput) |
221 | transcodingJobScheduler.createJob(t, 'videoFileOptimizer', dataInput) | ||
222 | ) | ||
223 | } | 223 | } |
224 | await Promise.all(tasks) | ||
225 | 224 | ||
226 | const videoCreated = await video.save(sequelizeOptions) | 225 | const videoCreated = await video.save(sequelizeOptions) |
227 | // Do not forget to add video channel information to the created video | 226 | // Do not forget to add video channel information to the created video |
228 | videoCreated.VideoChannel = res.locals.videoChannel | 227 | videoCreated.VideoChannel = res.locals.videoChannel |
229 | videoUUID = videoCreated.uuid | ||
230 | 228 | ||
231 | videoFile.videoId = video.id | 229 | videoFile.videoId = video.id |
232 | |||
233 | await videoFile.save(sequelizeOptions) | 230 | await videoFile.save(sequelizeOptions) |
234 | video.VideoFiles = [videoFile] | 231 | |
232 | video.VideoFiles = [ videoFile ] | ||
235 | 233 | ||
236 | if (videoInfo.tags) { | 234 | if (videoInfo.tags) { |
237 | const tagInstances = await db.Tag.findOrCreateTags(videoInfo.tags, t) | 235 | const tagInstances = await db.Tag.findOrCreateTags(videoInfo.tags, t) |
@@ -241,15 +239,17 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi | |||
241 | } | 239 | } |
242 | 240 | ||
243 | // Let transcoding job send the video to friends because the video file extension might change | 241 | // Let transcoding job send the video to friends because the video file extension might change |
244 | if (CONFIG.TRANSCODING.ENABLED === true) return undefined | 242 | if (CONFIG.TRANSCODING.ENABLED === true) return videoCreated |
245 | // Don't send video to remote servers, it is private | 243 | // Don't send video to remote servers, it is private |
246 | if (video.privacy === VideoPrivacy.PRIVATE) return undefined | 244 | if (video.privacy === VideoPrivacy.PRIVATE) return videoCreated |
247 | 245 | ||
248 | await sendAddVideo(video, t) | 246 | await sendAddVideo(video, t) |
249 | await shareVideoByServer(video, t) | 247 | await shareVideoByServer(video, t) |
250 | }) | ||
251 | 248 | ||
252 | logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoUUID) | 249 | logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid) |
250 | |||
251 | return videoCreated | ||
252 | }) | ||
253 | } | 253 | } |
254 | 254 | ||
255 | async function updateVideoRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { | 255 | async function updateVideoRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { |
@@ -280,7 +280,7 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
280 | if (videoInfoToUpdate.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence) | 280 | if (videoInfoToUpdate.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence) |
281 | if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language) | 281 | if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language) |
282 | if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw) | 282 | if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw) |
283 | if (videoInfoToUpdate.privacy !== undefined) videoInstance.set('privacy', videoInfoToUpdate.privacy) | 283 | if (videoInfoToUpdate.privacy !== undefined) videoInstance.set('privacy', parseInt(videoInfoToUpdate.privacy.toString(), 10)) |
284 | if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description) | 284 | if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description) |
285 | 285 | ||
286 | const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) | 286 | const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) |
@@ -298,9 +298,9 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
298 | } | 298 | } |
299 | 299 | ||
300 | // Video is not private anymore, send a create action to remote servers | 300 | // Video is not private anymore, send a create action to remote servers |
301 | if (wasPrivateVideo === true && videoInstance.privacy !== VideoPrivacy.PRIVATE) { | 301 | if (wasPrivateVideo === true && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE) { |
302 | await sendAddVideo(videoInstance, t) | 302 | await sendAddVideo(videoInstanceUpdated, t) |
303 | await shareVideoByServer(videoInstance, t) | 303 | await shareVideoByServer(videoInstanceUpdated, t) |
304 | } | 304 | } |
305 | }) | 305 | }) |
306 | 306 | ||
@@ -378,8 +378,7 @@ async function removeVideo (req: express.Request, res: express.Response) { | |||
378 | 378 | ||
379 | async function searchVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 379 | async function searchVideos (req: express.Request, res: express.Response, next: express.NextFunction) { |
380 | const resultList = await db.Video.searchAndPopulateAccountAndServerAndTags( | 380 | const resultList = await db.Video.searchAndPopulateAccountAndServerAndTags( |
381 | req.params.value, | 381 | req.query.search, |
382 | req.query.field, | ||
383 | req.query.start, | 382 | req.query.start, |
384 | req.query.count, | 383 | req.query.count, |
385 | req.query.sort | 384 | req.query.sort |
diff --git a/server/controllers/client.ts b/server/controllers/client.ts index 64e5829ca..f474c4282 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts | |||
@@ -18,6 +18,7 @@ import { VideoInstance } from '../models' | |||
18 | const clientsRouter = express.Router() | 18 | const clientsRouter = express.Router() |
19 | 19 | ||
20 | const distPath = join(root(), 'client', 'dist') | 20 | const distPath = join(root(), 'client', 'dist') |
21 | const assetsImagesPath = join(root(), 'client', 'dist', 'assets', 'images') | ||
21 | const embedPath = join(distPath, 'standalone', 'videos', 'embed.html') | 22 | const embedPath = join(distPath, 'standalone', 'videos', 'embed.html') |
22 | const indexPath = join(distPath, 'index.html') | 23 | const indexPath = join(distPath, 'index.html') |
23 | 24 | ||
@@ -33,6 +34,7 @@ clientsRouter.use('/videos/embed', (req: express.Request, res: express.Response, | |||
33 | 34 | ||
34 | // Static HTML/CSS/JS client files | 35 | // Static HTML/CSS/JS client files |
35 | clientsRouter.use('/client', express.static(distPath, { maxAge: STATIC_MAX_AGE })) | 36 | clientsRouter.use('/client', express.static(distPath, { maxAge: STATIC_MAX_AGE })) |
37 | clientsRouter.use('/client/assets/images', express.static(assetsImagesPath, { maxAge: STATIC_MAX_AGE })) | ||
36 | 38 | ||
37 | // 404 for static files not found | 39 | // 404 for static files not found |
38 | clientsRouter.use('/client/*', (req: express.Request, res: express.Response, next: express.NextFunction) => { | 40 | clientsRouter.use('/client/*', (req: express.Request, res: express.Response, next: express.NextFunction) => { |
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index 12c672fd2..2ed2988f5 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts | |||
@@ -49,14 +49,14 @@ function isVideoTorrentObjectValid (video: any) { | |||
49 | isActivityPubVideoDurationValid(video.duration) && | 49 | isActivityPubVideoDurationValid(video.duration) && |
50 | isUUIDValid(video.uuid) && | 50 | isUUIDValid(video.uuid) && |
51 | setValidRemoteTags(video) && | 51 | setValidRemoteTags(video) && |
52 | isRemoteIdentifierValid(video.category) && | 52 | (!video.category || isRemoteIdentifierValid(video.category)) && |
53 | isRemoteIdentifierValid(video.licence) && | 53 | (!video.licence || isRemoteIdentifierValid(video.licence)) && |
54 | (!video.language || isRemoteIdentifierValid(video.language)) && | 54 | (!video.language || isRemoteIdentifierValid(video.language)) && |
55 | isVideoViewsValid(video.views) && | 55 | isVideoViewsValid(video.views) && |
56 | isVideoNSFWValid(video.nsfw) && | 56 | isVideoNSFWValid(video.nsfw) && |
57 | isDateValid(video.published) && | 57 | isDateValid(video.published) && |
58 | isDateValid(video.updated) && | 58 | isDateValid(video.updated) && |
59 | isRemoteVideoContentValid(video.mediaType, video.content) && | 59 | (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) && |
60 | isRemoteVideoIconValid(video.icon) && | 60 | isRemoteVideoIconValid(video.icon) && |
61 | setValidRemoteVideoUrls(video) && | 61 | setValidRemoteVideoUrls(video) && |
62 | video.url.length !== 0 | 62 | video.url.length !== 0 |
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index f13178c54..37fa8b08a 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -14,11 +14,11 @@ const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS | |||
14 | const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES | 14 | const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES |
15 | 15 | ||
16 | function isVideoCategoryValid (value: number) { | 16 | function isVideoCategoryValid (value: number) { |
17 | return VIDEO_CATEGORIES[value] !== undefined | 17 | return value === null || VIDEO_CATEGORIES[value] !== undefined |
18 | } | 18 | } |
19 | 19 | ||
20 | function isVideoLicenceValid (value: number) { | 20 | function isVideoLicenceValid (value: number) { |
21 | return VIDEO_LICENCES[value] !== undefined | 21 | return value === null || VIDEO_LICENCES[value] !== undefined |
22 | } | 22 | } |
23 | 23 | ||
24 | function isVideoLanguageValid (value: number) { | 24 | function isVideoLanguageValid (value: number) { |
@@ -38,7 +38,7 @@ function isVideoTruncatedDescriptionValid (value: string) { | |||
38 | } | 38 | } |
39 | 39 | ||
40 | function isVideoDescriptionValid (value: string) { | 40 | function isVideoDescriptionValid (value: string) { |
41 | return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.DESCRIPTION) | 41 | return value === null || (exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.DESCRIPTION)) |
42 | } | 42 | } |
43 | 43 | ||
44 | function isVideoNameValid (value: string) { | 44 | function isVideoNameValid (value: string) { |
@@ -84,7 +84,7 @@ function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | | |||
84 | } | 84 | } |
85 | 85 | ||
86 | function isVideoPrivacyValid (value: string) { | 86 | function isVideoPrivacyValid (value: string) { |
87 | return VIDEO_PRIVACIES[value] !== undefined | 87 | return validator.isInt(value + '') && VIDEO_PRIVACIES[value] !== undefined |
88 | } | 88 | } |
89 | 89 | ||
90 | function isVideoFileInfoHashValid (value: string) { | 90 | function isVideoFileInfoHashValid (value: string) { |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index e3d779456..7be7a5f95 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -14,7 +14,7 @@ import { FollowState } from '../../shared/models/accounts/follow.model' | |||
14 | 14 | ||
15 | // --------------------------------------------------------------------------- | 15 | // --------------------------------------------------------------------------- |
16 | 16 | ||
17 | const LAST_MIGRATION_VERSION = 110 | 17 | const LAST_MIGRATION_VERSION = 120 |
18 | 18 | ||
19 | // --------------------------------------------------------------------------- | 19 | // --------------------------------------------------------------------------- |
20 | 20 | ||
@@ -25,11 +25,6 @@ const API_VERSION = 'v1' | |||
25 | const PAGINATION_COUNT_DEFAULT = 15 | 25 | const PAGINATION_COUNT_DEFAULT = 15 |
26 | 26 | ||
27 | // Sortable columns per schema | 27 | // Sortable columns per schema |
28 | const SEARCHABLE_COLUMNS = { | ||
29 | VIDEOS: [ 'name', 'magnetUri', 'host', 'account', 'tags' ] | ||
30 | } | ||
31 | |||
32 | // Sortable columns per schema | ||
33 | const SORTABLE_COLUMNS = { | 28 | const SORTABLE_COLUMNS = { |
34 | USERS: [ 'id', 'username', 'createdAt' ], | 29 | USERS: [ 'id', 'username', 'createdAt' ], |
35 | JOBS: [ 'id', 'createdAt' ], | 30 | JOBS: [ 'id', 'createdAt' ], |
@@ -60,6 +55,7 @@ const CONFIG = { | |||
60 | PASSWORD: config.get<string>('database.password') | 55 | PASSWORD: config.get<string>('database.password') |
61 | }, | 56 | }, |
62 | STORAGE: { | 57 | STORAGE: { |
58 | AVATARS_DIR: join(root(), config.get<string>('storage.avatars')), | ||
63 | LOG_DIR: join(root(), config.get<string>('storage.logs')), | 59 | LOG_DIR: join(root(), config.get<string>('storage.logs')), |
64 | VIDEOS_DIR: join(root(), config.get<string>('storage.videos')), | 60 | VIDEOS_DIR: join(root(), config.get<string>('storage.videos')), |
65 | THUMBNAILS_DIR: join(root(), config.get<string>('storage.thumbnails')), | 61 | THUMBNAILS_DIR: join(root(), config.get<string>('storage.thumbnails')), |
@@ -105,6 +101,9 @@ const CONFIG = { | |||
105 | CONFIG.WEBSERVER.URL = CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT | 101 | CONFIG.WEBSERVER.URL = CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT |
106 | CONFIG.WEBSERVER.HOST = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT | 102 | CONFIG.WEBSERVER.HOST = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT |
107 | 103 | ||
104 | const AVATARS_DIR = { | ||
105 | ACCOUNT: join(CONFIG.STORAGE.AVATARS_DIR, 'account') | ||
106 | } | ||
108 | // --------------------------------------------------------------------------- | 107 | // --------------------------------------------------------------------------- |
109 | 108 | ||
110 | const CONSTRAINTS_FIELDS = { | 109 | const CONSTRAINTS_FIELDS = { |
@@ -356,7 +355,7 @@ export { | |||
356 | PREVIEWS_SIZE, | 355 | PREVIEWS_SIZE, |
357 | REMOTE_SCHEME, | 356 | REMOTE_SCHEME, |
358 | FOLLOW_STATES, | 357 | FOLLOW_STATES, |
359 | SEARCHABLE_COLUMNS, | 358 | AVATARS_DIR, |
360 | SERVER_ACCOUNT_NAME, | 359 | SERVER_ACCOUNT_NAME, |
361 | PRIVATE_RSA_KEY_SIZE, | 360 | PRIVATE_RSA_KEY_SIZE, |
362 | SORTABLE_COLUMNS, | 361 | SORTABLE_COLUMNS, |
diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 90dbba5b9..bb95992e1 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -2,6 +2,7 @@ import { join } from 'path' | |||
2 | import { flattenDepth } from 'lodash' | 2 | import { flattenDepth } from 'lodash' |
3 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 3 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string |
4 | import * as Sequelize from 'sequelize' | 4 | import * as Sequelize from 'sequelize' |
5 | import { AvatarModel } from '../models/avatar' | ||
5 | 6 | ||
6 | import { CONFIG } from './constants' | 7 | import { CONFIG } from './constants' |
7 | // Do not use barrel, we need to load database first | 8 | // Do not use barrel, we need to load database first |
@@ -36,6 +37,7 @@ export type PeerTubeDatabase = { | |||
36 | init?: (silent: boolean) => Promise<void>, | 37 | init?: (silent: boolean) => Promise<void>, |
37 | 38 | ||
38 | Application?: ApplicationModel, | 39 | Application?: ApplicationModel, |
40 | Avatar?: AvatarModel, | ||
39 | Account?: AccountModel, | 41 | Account?: AccountModel, |
40 | Job?: JobModel, | 42 | Job?: JobModel, |
41 | OAuthClient?: OAuthClientModel, | 43 | OAuthClient?: OAuthClientModel, |
diff --git a/server/initializers/migrations/0115-account-avatar.ts b/server/initializers/migrations/0115-account-avatar.ts new file mode 100644 index 000000000..2b947ceda --- /dev/null +++ b/server/initializers/migrations/0115-account-avatar.ts | |||
@@ -0,0 +1,31 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import { PeerTubeDatabase } from '../database' | ||
3 | |||
4 | async function up (utils: { | ||
5 | transaction: Sequelize.Transaction, | ||
6 | queryInterface: Sequelize.QueryInterface, | ||
7 | sequelize: Sequelize.Sequelize, | ||
8 | db: PeerTubeDatabase | ||
9 | }): Promise<void> { | ||
10 | await utils.db.Avatar.sync() | ||
11 | |||
12 | const data = { | ||
13 | type: Sequelize.INTEGER, | ||
14 | allowNull: true, | ||
15 | references: { | ||
16 | model: 'Avatars', | ||
17 | key: 'id' | ||
18 | }, | ||
19 | onDelete: 'CASCADE' | ||
20 | } | ||
21 | await utils.queryInterface.addColumn('Accounts', 'avatarId', data) | ||
22 | } | ||
23 | |||
24 | function down (options) { | ||
25 | throw new Error('Not implemented.') | ||
26 | } | ||
27 | |||
28 | export { | ||
29 | up, | ||
30 | down | ||
31 | } | ||
diff --git a/server/initializers/migrations/0120-video-null.ts b/server/initializers/migrations/0120-video-null.ts new file mode 100644 index 000000000..9130d10ee --- /dev/null +++ b/server/initializers/migrations/0120-video-null.ts | |||
@@ -0,0 +1,47 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import { CONSTRAINTS_FIELDS } from '../constants' | ||
3 | import { PeerTubeDatabase } from '../database' | ||
4 | |||
5 | async function up (utils: { | ||
6 | transaction: Sequelize.Transaction, | ||
7 | queryInterface: Sequelize.QueryInterface, | ||
8 | sequelize: Sequelize.Sequelize, | ||
9 | db: PeerTubeDatabase | ||
10 | }): Promise<void> { | ||
11 | |||
12 | { | ||
13 | const data = { | ||
14 | type: Sequelize.INTEGER, | ||
15 | allowNull: true, | ||
16 | defaultValue: null | ||
17 | } | ||
18 | await utils.queryInterface.changeColumn('Videos', 'licence', data) | ||
19 | } | ||
20 | |||
21 | { | ||
22 | const data = { | ||
23 | type: Sequelize.INTEGER, | ||
24 | allowNull: true, | ||
25 | defaultValue: null | ||
26 | } | ||
27 | await utils.queryInterface.changeColumn('Videos', 'category', data) | ||
28 | } | ||
29 | |||
30 | { | ||
31 | const data = { | ||
32 | type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max), | ||
33 | allowNull: true, | ||
34 | defaultValue: null | ||
35 | } | ||
36 | await utils.queryInterface.changeColumn('Videos', 'description', data) | ||
37 | } | ||
38 | } | ||
39 | |||
40 | function down (options) { | ||
41 | throw new Error('Not implemented.') | ||
42 | } | ||
43 | |||
44 | export { | ||
45 | up, | ||
46 | down | ||
47 | } | ||
diff --git a/server/lib/activitypub/process/misc.ts b/server/lib/activitypub/process/misc.ts index f20e588ab..0baa22c26 100644 --- a/server/lib/activitypub/process/misc.ts +++ b/server/lib/activitypub/process/misc.ts | |||
@@ -41,15 +41,30 @@ async function videoActivityObjectToDBAttributes ( | |||
41 | language = parseInt(videoObject.language.identifier, 10) | 41 | language = parseInt(videoObject.language.identifier, 10) |
42 | } | 42 | } |
43 | 43 | ||
44 | let category = null | ||
45 | if (videoObject.category) { | ||
46 | category = parseInt(videoObject.category.identifier, 10) | ||
47 | } | ||
48 | |||
49 | let licence = null | ||
50 | if (videoObject.licence) { | ||
51 | licence = parseInt(videoObject.licence.identifier, 10) | ||
52 | } | ||
53 | |||
54 | let description = null | ||
55 | if (videoObject.content) { | ||
56 | description = videoObject.content | ||
57 | } | ||
58 | |||
44 | const videoData: VideoAttributes = { | 59 | const videoData: VideoAttributes = { |
45 | name: videoObject.name, | 60 | name: videoObject.name, |
46 | uuid: videoObject.uuid, | 61 | uuid: videoObject.uuid, |
47 | url: videoObject.id, | 62 | url: videoObject.id, |
48 | category: parseInt(videoObject.category.identifier, 10), | 63 | category, |
49 | licence: parseInt(videoObject.licence.identifier, 10), | 64 | licence, |
50 | language, | 65 | language, |
66 | description, | ||
51 | nsfw: videoObject.nsfw, | 67 | nsfw: videoObject.nsfw, |
52 | description: videoObject.content, | ||
53 | channelId: videoChannel.id, | 68 | channelId: videoChannel.id, |
54 | duration: parseInt(duration, 10), | 69 | duration: parseInt(duration, 10), |
55 | createdAt: new Date(videoObject.published), | 70 | createdAt: new Date(videoObject.published), |
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index aafcad2d9..0cef26953 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts | |||
@@ -4,6 +4,5 @@ export * from './async' | |||
4 | export * from './oauth' | 4 | export * from './oauth' |
5 | export * from './pagination' | 5 | export * from './pagination' |
6 | export * from './servers' | 6 | export * from './servers' |
7 | export * from './search' | ||
8 | export * from './sort' | 7 | export * from './sort' |
9 | export * from './user-right' | 8 | export * from './user-right' |
diff --git a/server/middlewares/search.ts b/server/middlewares/search.ts deleted file mode 100644 index 6fe83d25b..000000000 --- a/server/middlewares/search.ts +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | import 'express-validator' | ||
2 | import * as express from 'express' | ||
3 | |||
4 | function setVideosSearch (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
5 | if (!req.query.field) req.query.field = 'name' | ||
6 | |||
7 | return next() | ||
8 | } | ||
9 | |||
10 | // --------------------------------------------------------------------------- | ||
11 | |||
12 | export { | ||
13 | setVideosSearch | ||
14 | } | ||
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts index f21680aa0..10625e41d 100644 --- a/server/middlewares/validators/videos.ts +++ b/server/middlewares/validators/videos.ts | |||
@@ -18,7 +18,7 @@ import { | |||
18 | } from '../../helpers/custom-validators/videos' | 18 | } from '../../helpers/custom-validators/videos' |
19 | import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils' | 19 | import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils' |
20 | import { logger } from '../../helpers/logger' | 20 | import { logger } from '../../helpers/logger' |
21 | import { CONSTRAINTS_FIELDS, SEARCHABLE_COLUMNS } from '../../initializers' | 21 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
22 | import { database as db } from '../../initializers/database' | 22 | import { database as db } from '../../initializers/database' |
23 | import { UserInstance } from '../../models/account/user-interface' | 23 | import { UserInstance } from '../../models/account/user-interface' |
24 | import { VideoInstance } from '../../models/video/video-interface' | 24 | import { VideoInstance } from '../../models/video/video-interface' |
@@ -31,11 +31,11 @@ const videosAddValidator = [ | |||
31 | + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') | 31 | + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') |
32 | ), | 32 | ), |
33 | body('name').custom(isVideoNameValid).withMessage('Should have a valid name'), | 33 | body('name').custom(isVideoNameValid).withMessage('Should have a valid name'), |
34 | body('category').custom(isVideoCategoryValid).withMessage('Should have a valid category'), | 34 | body('category').optional().custom(isVideoCategoryValid).withMessage('Should have a valid category'), |
35 | body('licence').custom(isVideoLicenceValid).withMessage('Should have a valid licence'), | 35 | body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'), |
36 | body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'), | 36 | body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'), |
37 | body('nsfw').custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'), | 37 | body('nsfw').custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'), |
38 | body('description').custom(isVideoDescriptionValid).withMessage('Should have a valid description'), | 38 | body('description').optional().custom(isVideoDescriptionValid).withMessage('Should have a valid description'), |
39 | body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'), | 39 | body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'), |
40 | body('privacy').custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'), | 40 | body('privacy').custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'), |
41 | body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'), | 41 | body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'), |
@@ -172,8 +172,7 @@ const videosRemoveValidator = [ | |||
172 | ] | 172 | ] |
173 | 173 | ||
174 | const videosSearchValidator = [ | 174 | const videosSearchValidator = [ |
175 | param('value').not().isEmpty().withMessage('Should have a valid search'), | 175 | query('search').not().isEmpty().withMessage('Should have a valid search'), |
176 | query('field').optional().isIn(SEARCHABLE_COLUMNS.VIDEOS).withMessage('Should have correct searchable column'), | ||
177 | 176 | ||
178 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 177 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
179 | logger.debug('Checking videosSearch parameters', { parameters: req.params }) | 178 | logger.debug('Checking videosSearch parameters', { parameters: req.params }) |
diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts index b369766dc..46fe068e3 100644 --- a/server/models/account/account-interface.ts +++ b/server/models/account/account-interface.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import * as Sequelize from 'sequelize' | 2 | import * as Sequelize from 'sequelize' |
3 | import { Account as FormattedAccount, ActivityPubActor } from '../../../shared' | 3 | import { Account as FormattedAccount, ActivityPubActor } from '../../../shared' |
4 | import { AvatarInstance } from '../avatar' | ||
4 | import { ServerInstance } from '../server/server-interface' | 5 | import { ServerInstance } from '../server/server-interface' |
5 | import { VideoChannelInstance } from '../video/video-channel-interface' | 6 | import { VideoChannelInstance } from '../video/video-channel-interface' |
6 | 7 | ||
@@ -51,6 +52,7 @@ export interface AccountAttributes { | |||
51 | serverId?: number | 52 | serverId?: number |
52 | userId?: number | 53 | userId?: number |
53 | applicationId?: number | 54 | applicationId?: number |
55 | avatarId?: number | ||
54 | } | 56 | } |
55 | 57 | ||
56 | export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> { | 58 | export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> { |
@@ -68,6 +70,7 @@ export interface AccountInstance extends AccountClass, AccountAttributes, Sequel | |||
68 | 70 | ||
69 | Server: ServerInstance | 71 | Server: ServerInstance |
70 | VideoChannels: VideoChannelInstance[] | 72 | VideoChannels: VideoChannelInstance[] |
73 | Avatar: AvatarInstance | ||
71 | } | 74 | } |
72 | 75 | ||
73 | export interface AccountModel extends AccountClass, Sequelize.Model<AccountInstance, AccountAttributes> {} | 76 | export interface AccountModel extends AccountClass, Sequelize.Model<AccountInstance, AccountAttributes> {} |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 61a88524c..8b0819f39 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import { join } from 'path' | ||
1 | import * as Sequelize from 'sequelize' | 2 | import * as Sequelize from 'sequelize' |
3 | import { Avatar } from '../../../shared/models/avatars/avatar.model' | ||
2 | import { | 4 | import { |
3 | activityPubContextify, | 5 | activityPubContextify, |
4 | isAccountFollowersCountValid, | 6 | isAccountFollowersCountValid, |
@@ -8,6 +10,7 @@ import { | |||
8 | isUserUsernameValid | 10 | isUserUsernameValid |
9 | } from '../../helpers' | 11 | } from '../../helpers' |
10 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 12 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
13 | import { AVATARS_DIR } from '../../initializers' | ||
11 | import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers/constants' | 14 | import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers/constants' |
12 | import { sendDeleteAccount } from '../../lib/activitypub/send/send-delete' | 15 | import { sendDeleteAccount } from '../../lib/activitypub/send/send-delete' |
13 | import { addMethodsToModel } from '../utils' | 16 | import { addMethodsToModel } from '../utils' |
@@ -252,6 +255,14 @@ function associate (models) { | |||
252 | as: 'followers', | 255 | as: 'followers', |
253 | onDelete: 'cascade' | 256 | onDelete: 'cascade' |
254 | }) | 257 | }) |
258 | |||
259 | Account.hasOne(models.Avatar, { | ||
260 | foreignKey: { | ||
261 | name: 'avatarId', | ||
262 | allowNull: true | ||
263 | }, | ||
264 | onDelete: 'cascade' | ||
265 | }) | ||
255 | } | 266 | } |
256 | 267 | ||
257 | function afterDestroy (account: AccountInstance) { | 268 | function afterDestroy (account: AccountInstance) { |
@@ -265,6 +276,15 @@ function afterDestroy (account: AccountInstance) { | |||
265 | toFormattedJSON = function (this: AccountInstance) { | 276 | toFormattedJSON = function (this: AccountInstance) { |
266 | let host = CONFIG.WEBSERVER.HOST | 277 | let host = CONFIG.WEBSERVER.HOST |
267 | let score: number | 278 | let score: number |
279 | let avatar: Avatar = null | ||
280 | |||
281 | if (this.Avatar) { | ||
282 | avatar = { | ||
283 | path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename), | ||
284 | createdAt: this.Avatar.createdAt, | ||
285 | updatedAt: this.Avatar.updatedAt | ||
286 | } | ||
287 | } | ||
268 | 288 | ||
269 | if (this.Server) { | 289 | if (this.Server) { |
270 | host = this.Server.host | 290 | host = this.Server.host |
@@ -273,11 +293,15 @@ toFormattedJSON = function (this: AccountInstance) { | |||
273 | 293 | ||
274 | const json = { | 294 | const json = { |
275 | id: this.id, | 295 | id: this.id, |
296 | uuid: this.uuid, | ||
276 | host, | 297 | host, |
277 | score, | 298 | score, |
278 | name: this.name, | 299 | name: this.name, |
300 | followingCount: this.followingCount, | ||
301 | followersCount: this.followersCount, | ||
279 | createdAt: this.createdAt, | 302 | createdAt: this.createdAt, |
280 | updatedAt: this.updatedAt | 303 | updatedAt: this.updatedAt, |
304 | avatar | ||
281 | } | 305 | } |
282 | 306 | ||
283 | return json | 307 | return json |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 8f7c9b013..3705947c0 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -157,10 +157,7 @@ toFormattedJSON = function (this: UserInstance) { | |||
157 | roleLabel: USER_ROLE_LABELS[this.role], | 157 | roleLabel: USER_ROLE_LABELS[this.role], |
158 | videoQuota: this.videoQuota, | 158 | videoQuota: this.videoQuota, |
159 | createdAt: this.createdAt, | 159 | createdAt: this.createdAt, |
160 | account: { | 160 | account: this.Account.toFormattedJSON() |
161 | id: this.Account.id, | ||
162 | uuid: this.Account.uuid | ||
163 | } | ||
164 | } | 161 | } |
165 | 162 | ||
166 | if (Array.isArray(this.Account.VideoChannels) === true) { | 163 | if (Array.isArray(this.Account.VideoChannels) === true) { |
diff --git a/server/models/avatar/avatar-interface.ts b/server/models/avatar/avatar-interface.ts new file mode 100644 index 000000000..4af2b87b7 --- /dev/null +++ b/server/models/avatar/avatar-interface.ts | |||
@@ -0,0 +1,16 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | export namespace AvatarMethods {} | ||
4 | |||
5 | export interface AvatarClass {} | ||
6 | |||
7 | export interface AvatarAttributes { | ||
8 | filename: string | ||
9 | } | ||
10 | |||
11 | export interface AvatarInstance extends AvatarClass, AvatarAttributes, Sequelize.Instance<AvatarAttributes> { | ||
12 | createdAt: Date | ||
13 | updatedAt: Date | ||
14 | } | ||
15 | |||
16 | export interface AvatarModel extends AvatarClass, Sequelize.Model<AvatarInstance, AvatarAttributes> {} | ||
diff --git a/server/models/avatar/avatar.ts b/server/models/avatar/avatar.ts new file mode 100644 index 000000000..96308fd5f --- /dev/null +++ b/server/models/avatar/avatar.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import { addMethodsToModel } from '../utils' | ||
3 | import { AvatarAttributes, AvatarInstance } from './avatar-interface' | ||
4 | |||
5 | let Avatar: Sequelize.Model<AvatarInstance, AvatarAttributes> | ||
6 | |||
7 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | ||
8 | Avatar = sequelize.define<AvatarInstance, AvatarAttributes>('Avatar', | ||
9 | { | ||
10 | filename: { | ||
11 | type: DataTypes.STRING, | ||
12 | allowNull: false | ||
13 | } | ||
14 | }, | ||
15 | {} | ||
16 | ) | ||
17 | |||
18 | const classMethods = [] | ||
19 | addMethodsToModel(Avatar, classMethods) | ||
20 | |||
21 | return Avatar | ||
22 | } | ||
23 | |||
24 | // ------------------------------ Statics ------------------------------ | ||
diff --git a/server/models/avatar/index.ts b/server/models/avatar/index.ts new file mode 100644 index 000000000..877aed1ce --- /dev/null +++ b/server/models/avatar/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './avatar-interface' | |||
diff --git a/server/models/index.ts b/server/models/index.ts index 65faa5294..fedd97dd1 100644 --- a/server/models/index.ts +++ b/server/models/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export * from './application' | 1 | export * from './application' |
2 | export * from './avatar' | ||
2 | export * from './job' | 3 | export * from './job' |
3 | export * from './oauth' | 4 | export * from './oauth' |
4 | export * from './server' | 5 | export * from './server' |
diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts index be140de86..2a63350af 100644 --- a/server/models/video/video-interface.ts +++ b/server/models/video/video-interface.ts | |||
@@ -50,7 +50,6 @@ export namespace VideoMethods { | |||
50 | export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> > | 50 | export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> > |
51 | export type SearchAndPopulateAccountAndServerAndTags = ( | 51 | export type SearchAndPopulateAccountAndServerAndTags = ( |
52 | value: string, | 52 | value: string, |
53 | field: string, | ||
54 | start: number, | 53 | start: number, |
55 | count: number, | 54 | count: number, |
56 | sort: string | 55 | sort: string |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index f3469c1de..d46fdeebe 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -104,7 +104,8 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da | |||
104 | }, | 104 | }, |
105 | category: { | 105 | category: { |
106 | type: DataTypes.INTEGER, | 106 | type: DataTypes.INTEGER, |
107 | allowNull: false, | 107 | allowNull: true, |
108 | defaultValue: null, | ||
108 | validate: { | 109 | validate: { |
109 | categoryValid: value => { | 110 | categoryValid: value => { |
110 | const res = isVideoCategoryValid(value) | 111 | const res = isVideoCategoryValid(value) |
@@ -114,7 +115,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da | |||
114 | }, | 115 | }, |
115 | licence: { | 116 | licence: { |
116 | type: DataTypes.INTEGER, | 117 | type: DataTypes.INTEGER, |
117 | allowNull: false, | 118 | allowNull: true, |
118 | defaultValue: null, | 119 | defaultValue: null, |
119 | validate: { | 120 | validate: { |
120 | licenceValid: value => { | 121 | licenceValid: value => { |
@@ -126,6 +127,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da | |||
126 | language: { | 127 | language: { |
127 | type: DataTypes.INTEGER, | 128 | type: DataTypes.INTEGER, |
128 | allowNull: true, | 129 | allowNull: true, |
130 | defaultValue: null, | ||
129 | validate: { | 131 | validate: { |
130 | languageValid: value => { | 132 | languageValid: value => { |
131 | const res = isVideoLanguageValid(value) | 133 | const res = isVideoLanguageValid(value) |
@@ -155,7 +157,8 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da | |||
155 | }, | 157 | }, |
156 | description: { | 158 | description: { |
157 | type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max), | 159 | type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max), |
158 | allowNull: false, | 160 | allowNull: true, |
161 | defaultValue: null, | ||
159 | validate: { | 162 | validate: { |
160 | descriptionValid: value => { | 163 | descriptionValid: value => { |
161 | const res = isVideoDescriptionValid(value) | 164 | const res = isVideoDescriptionValid(value) |
@@ -486,7 +489,7 @@ toFormattedJSON = function (this: VideoInstance) { | |||
486 | description: this.getTruncatedDescription(), | 489 | description: this.getTruncatedDescription(), |
487 | serverHost, | 490 | serverHost, |
488 | isLocal: this.isOwned(), | 491 | isLocal: this.isOwned(), |
489 | account: this.VideoChannel.Account.name, | 492 | accountName: this.VideoChannel.Account.name, |
490 | duration: this.duration, | 493 | duration: this.duration, |
491 | views: this.views, | 494 | views: this.views, |
492 | likes: this.likes, | 495 | likes: this.likes, |
@@ -514,6 +517,7 @@ toFormattedDetailsJSON = function (this: VideoInstance) { | |||
514 | privacy: this.privacy, | 517 | privacy: this.privacy, |
515 | descriptionPath: this.getDescriptionPath(), | 518 | descriptionPath: this.getDescriptionPath(), |
516 | channel: this.VideoChannel.toFormattedJSON(), | 519 | channel: this.VideoChannel.toFormattedJSON(), |
520 | account: this.VideoChannel.Account.toFormattedJSON(), | ||
517 | files: [] | 521 | files: [] |
518 | } | 522 | } |
519 | 523 | ||
@@ -560,6 +564,22 @@ toActivityPubObject = function (this: VideoInstance) { | |||
560 | } | 564 | } |
561 | } | 565 | } |
562 | 566 | ||
567 | let category | ||
568 | if (this.category) { | ||
569 | category = { | ||
570 | identifier: this.category + '', | ||
571 | name: this.getCategoryLabel() | ||
572 | } | ||
573 | } | ||
574 | |||
575 | let licence | ||
576 | if (this.licence) { | ||
577 | licence = { | ||
578 | identifier: this.licence + '', | ||
579 | name: this.getLicenceLabel() | ||
580 | } | ||
581 | } | ||
582 | |||
563 | let likesObject | 583 | let likesObject |
564 | let dislikesObject | 584 | let dislikesObject |
565 | 585 | ||
@@ -631,14 +651,8 @@ toActivityPubObject = function (this: VideoInstance) { | |||
631 | duration: 'PT' + this.duration + 'S', | 651 | duration: 'PT' + this.duration + 'S', |
632 | uuid: this.uuid, | 652 | uuid: this.uuid, |
633 | tag, | 653 | tag, |
634 | category: { | 654 | category, |
635 | identifier: this.category + '', | 655 | licence, |
636 | name: this.getCategoryLabel() | ||
637 | }, | ||
638 | licence: { | ||
639 | identifier: this.licence + '', | ||
640 | name: this.getLicenceLabel() | ||
641 | }, | ||
642 | language, | 656 | language, |
643 | views: this.views, | 657 | views: this.views, |
644 | nsfw: this.nsfw, | 658 | nsfw: this.nsfw, |
@@ -663,6 +677,8 @@ toActivityPubObject = function (this: VideoInstance) { | |||
663 | } | 677 | } |
664 | 678 | ||
665 | getTruncatedDescription = function (this: VideoInstance) { | 679 | getTruncatedDescription = function (this: VideoInstance) { |
680 | if (!this.description) return null | ||
681 | |||
666 | const options = { | 682 | const options = { |
667 | length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max | 683 | length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max |
668 | } | 684 | } |
@@ -753,8 +769,6 @@ getDescriptionPath = function (this: VideoInstance) { | |||
753 | 769 | ||
754 | getCategoryLabel = function (this: VideoInstance) { | 770 | getCategoryLabel = function (this: VideoInstance) { |
755 | let categoryLabel = VIDEO_CATEGORIES[this.category] | 771 | let categoryLabel = VIDEO_CATEGORIES[this.category] |
756 | |||
757 | // Maybe our server is not up to date and there are new categories since our version | ||
758 | if (!categoryLabel) categoryLabel = 'Misc' | 772 | if (!categoryLabel) categoryLabel = 'Misc' |
759 | 773 | ||
760 | return categoryLabel | 774 | return categoryLabel |
@@ -762,15 +776,12 @@ getCategoryLabel = function (this: VideoInstance) { | |||
762 | 776 | ||
763 | getLicenceLabel = function (this: VideoInstance) { | 777 | getLicenceLabel = function (this: VideoInstance) { |
764 | let licenceLabel = VIDEO_LICENCES[this.licence] | 778 | let licenceLabel = VIDEO_LICENCES[this.licence] |
765 | |||
766 | // Maybe our server is not up to date and there are new licences since our version | ||
767 | if (!licenceLabel) licenceLabel = 'Unknown' | 779 | if (!licenceLabel) licenceLabel = 'Unknown' |
768 | 780 | ||
769 | return licenceLabel | 781 | return licenceLabel |
770 | } | 782 | } |
771 | 783 | ||
772 | getLanguageLabel = function (this: VideoInstance) { | 784 | getLanguageLabel = function (this: VideoInstance) { |
773 | // Language is an optional attribute | ||
774 | let languageLabel = VIDEO_LANGUAGES[this.language] | 785 | let languageLabel = VIDEO_LANGUAGES[this.language] |
775 | if (!languageLabel) languageLabel = 'Unknown' | 786 | if (!languageLabel) languageLabel = 'Unknown' |
776 | 787 | ||
@@ -1070,7 +1081,7 @@ loadByUUIDAndPopulateAccountAndServerAndTags = function (uuid: string) { | |||
1070 | return Video.findOne(options) | 1081 | return Video.findOne(options) |
1071 | } | 1082 | } |
1072 | 1083 | ||
1073 | searchAndPopulateAccountAndServerAndTags = function (value: string, field: string, start: number, count: number, sort: string) { | 1084 | searchAndPopulateAccountAndServerAndTags = function (value: string, start: number, count: number, sort: string) { |
1074 | const serverInclude: Sequelize.IncludeOptions = { | 1085 | const serverInclude: Sequelize.IncludeOptions = { |
1075 | model: Video['sequelize'].models.Server, | 1086 | model: Video['sequelize'].models.Server, |
1076 | required: false | 1087 | required: false |
@@ -1099,33 +1110,24 @@ searchAndPopulateAccountAndServerAndTags = function (value: string, field: strin | |||
1099 | order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ] | 1110 | order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ] |
1100 | } | 1111 | } |
1101 | 1112 | ||
1102 | if (field === 'tags') { | 1113 | // TODO: search on tags too |
1103 | const escapedValue = Video['sequelize'].escape('%' + value + '%') | 1114 | // const escapedValue = Video['sequelize'].escape('%' + value + '%') |
1104 | query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal( | 1115 | // query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal( |
1105 | `(SELECT "VideoTags"."videoId" | 1116 | // `(SELECT "VideoTags"."videoId" |
1106 | FROM "Tags" | 1117 | // FROM "Tags" |
1107 | INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" | 1118 | // INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" |
1108 | WHERE name ILIKE ${escapedValue} | 1119 | // WHERE name ILIKE ${escapedValue} |
1109 | )` | 1120 | // )` |
1110 | ) | 1121 | // ) |
1111 | } else if (field === 'host') { | 1122 | |
1112 | // FIXME: Include our server? (not stored in the database) | 1123 | // TODO: search on account too |
1113 | serverInclude.where = { | 1124 | // accountInclude.where = { |
1114 | host: { | 1125 | // name: { |
1115 | [Sequelize.Op.iLike]: '%' + value + '%' | 1126 | // [Sequelize.Op.iLike]: '%' + value + '%' |
1116 | } | 1127 | // } |
1117 | } | 1128 | // } |
1118 | serverInclude.required = true | 1129 | query.where['name'] = { |
1119 | } else if (field === 'account') { | 1130 | [Sequelize.Op.iLike]: '%' + value + '%' |
1120 | accountInclude.where = { | ||
1121 | name: { | ||
1122 | [Sequelize.Op.iLike]: '%' + value + '%' | ||
1123 | } | ||
1124 | } | ||
1125 | } else { | ||
1126 | query.where[field] = { | ||
1127 | [Sequelize.Op.iLike]: '%' + value + '%' | ||
1128 | } | ||
1129 | } | 1131 | } |
1130 | 1132 | ||
1131 | query.include = [ | 1133 | query.include = [ |
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts index 2962f5640..0aaa6e7c9 100644 --- a/server/tests/api/check-params/videos.ts +++ b/server/tests/api/check-params/videos.ts | |||
@@ -189,14 +189,6 @@ describe('Test videos API validator', function () { | |||
189 | await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 189 | await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) |
190 | }) | 190 | }) |
191 | 191 | ||
192 | it('Should fail without a category', async function () { | ||
193 | const fields = getCompleteVideoUploadAttributes() | ||
194 | delete fields.category | ||
195 | |||
196 | const attaches = getVideoUploadAttaches | ||
197 | await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | ||
198 | }) | ||
199 | |||
200 | it('Should fail with a bad category', async function () { | 192 | it('Should fail with a bad category', async function () { |
201 | const fields = getCompleteVideoUploadAttributes() | 193 | const fields = getCompleteVideoUploadAttributes() |
202 | fields.category = 125 | 194 | fields.category = 125 |
@@ -205,14 +197,6 @@ describe('Test videos API validator', function () { | |||
205 | await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 197 | await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) |
206 | }) | 198 | }) |
207 | 199 | ||
208 | it('Should fail without a licence', async function () { | ||
209 | const fields = getCompleteVideoUploadAttributes() | ||
210 | delete fields.licence | ||
211 | |||
212 | const attaches = getVideoUploadAttaches() | ||
213 | await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | ||
214 | }) | ||
215 | |||
216 | it('Should fail with a bad licence', async function () { | 200 | it('Should fail with a bad licence', async function () { |
217 | const fields = getCompleteVideoUploadAttributes() | 201 | const fields = getCompleteVideoUploadAttributes() |
218 | fields.licence = 125 | 202 | fields.licence = 125 |
@@ -245,14 +229,6 @@ describe('Test videos API validator', function () { | |||
245 | await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 229 | await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) |
246 | }) | 230 | }) |
247 | 231 | ||
248 | it('Should fail without description', async function () { | ||
249 | const fields = getCompleteVideoUploadAttributes() | ||
250 | delete fields.description | ||
251 | |||
252 | const attaches = getVideoUploadAttaches() | ||
253 | await makePostUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | ||
254 | }) | ||
255 | |||
256 | it('Should fail with a long description', async function () { | 232 | it('Should fail with a long description', async function () { |
257 | const fields = getCompleteVideoUploadAttributes() | 233 | const fields = getCompleteVideoUploadAttributes() |
258 | fields.description = 'my super description which is very very very very very very very very very very very very long'.repeat(35) | 234 | fields.description = 'my super description which is very very very very very very very very very very very very long'.repeat(35) |
@@ -345,7 +321,7 @@ describe('Test videos API validator', function () { | |||
345 | token: server.accessToken, | 321 | token: server.accessToken, |
346 | fields, | 322 | fields, |
347 | attaches, | 323 | attaches, |
348 | statusCodeExpected: 204 | 324 | statusCodeExpected: 200 |
349 | }) | 325 | }) |
350 | 326 | ||
351 | attaches.videofile = join(__dirname, '..', 'fixtures', 'video_short.mp4') | 327 | attaches.videofile = join(__dirname, '..', 'fixtures', 'video_short.mp4') |
@@ -355,7 +331,7 @@ describe('Test videos API validator', function () { | |||
355 | token: server.accessToken, | 331 | token: server.accessToken, |
356 | fields, | 332 | fields, |
357 | attaches, | 333 | attaches, |
358 | statusCodeExpected: 204 | 334 | statusCodeExpected: 200 |
359 | }) | 335 | }) |
360 | 336 | ||
361 | attaches.videofile = join(__dirname, '..', 'fixtures', 'video_short.ogv') | 337 | attaches.videofile = join(__dirname, '..', 'fixtures', 'video_short.ogv') |
@@ -365,7 +341,7 @@ describe('Test videos API validator', function () { | |||
365 | token: server.accessToken, | 341 | token: server.accessToken, |
366 | fields, | 342 | fields, |
367 | attaches, | 343 | attaches, |
368 | statusCodeExpected: 204 | 344 | statusCodeExpected: 200 |
369 | }) | 345 | }) |
370 | }) | 346 | }) |
371 | }) | 347 | }) |
diff --git a/server/tests/api/follows.ts b/server/tests/api/follows.ts index aadae3cce..dcb4c8bd9 100644 --- a/server/tests/api/follows.ts +++ b/server/tests/api/follows.ts | |||
@@ -227,7 +227,7 @@ describe('Test follows', function () { | |||
227 | expect(videoDetails.nsfw).to.be.ok | 227 | expect(videoDetails.nsfw).to.be.ok |
228 | expect(videoDetails.description).to.equal('my super description') | 228 | expect(videoDetails.description).to.equal('my super description') |
229 | expect(videoDetails.serverHost).to.equal('localhost:9003') | 229 | expect(videoDetails.serverHost).to.equal('localhost:9003') |
230 | expect(videoDetails.account).to.equal('root') | 230 | expect(videoDetails.accountName).to.equal('root') |
231 | expect(videoDetails.likes).to.equal(1) | 231 | expect(videoDetails.likes).to.equal(1) |
232 | expect(videoDetails.dislikes).to.equal(1) | 232 | expect(videoDetails.dislikes).to.equal(1) |
233 | expect(videoDetails.isLocal).to.be.false | 233 | expect(videoDetails.isLocal).to.be.false |
diff --git a/server/tests/api/multiple-servers.ts b/server/tests/api/multiple-servers.ts index c80ded862..2f17f017a 100644 --- a/server/tests/api/multiple-servers.ts +++ b/server/tests/api/multiple-servers.ts | |||
@@ -2,6 +2,8 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { join } from 'path' | ||
6 | import * as request from 'supertest' | ||
5 | 7 | ||
6 | import { | 8 | import { |
7 | dateIsValid, | 9 | dateIsValid, |
@@ -111,13 +113,14 @@ describe('Test multiple servers', function () { | |||
111 | expect(video.tags).to.deep.equal([ 'tag1p1', 'tag2p1' ]) | 113 | expect(video.tags).to.deep.equal([ 'tag1p1', 'tag2p1' ]) |
112 | expect(dateIsValid(video.createdAt)).to.be.true | 114 | expect(dateIsValid(video.createdAt)).to.be.true |
113 | expect(dateIsValid(video.updatedAt)).to.be.true | 115 | expect(dateIsValid(video.updatedAt)).to.be.true |
114 | expect(video.account).to.equal('root') | 116 | expect(video.accountName).to.equal('root') |
115 | 117 | ||
116 | const res2 = await getVideo(server.url, video.uuid) | 118 | const res2 = await getVideo(server.url, video.uuid) |
117 | const videoDetails = res2.body | 119 | const videoDetails = res2.body |
118 | 120 | ||
119 | expect(videoDetails.channel.name).to.equal('my channel') | 121 | expect(videoDetails.channel.name).to.equal('my channel') |
120 | expect(videoDetails.channel.description).to.equal('super channel') | 122 | expect(videoDetails.channel.description).to.equal('super channel') |
123 | expect(videoDetails.account.name).to.equal('root') | ||
121 | expect(dateIsValid(videoDetails.channel.createdAt)).to.be.true | 124 | expect(dateIsValid(videoDetails.channel.createdAt)).to.be.true |
122 | expect(dateIsValid(videoDetails.channel.updatedAt)).to.be.true | 125 | expect(dateIsValid(videoDetails.channel.updatedAt)).to.be.true |
123 | expect(videoDetails.files).to.have.lengthOf(1) | 126 | expect(videoDetails.files).to.have.lengthOf(1) |
@@ -201,7 +204,7 @@ describe('Test multiple servers', function () { | |||
201 | expect(video.tags).to.deep.equal([ 'tag1p2', 'tag2p2', 'tag3p2' ]) | 204 | expect(video.tags).to.deep.equal([ 'tag1p2', 'tag2p2', 'tag3p2' ]) |
202 | expect(dateIsValid(video.createdAt)).to.be.true | 205 | expect(dateIsValid(video.createdAt)).to.be.true |
203 | expect(dateIsValid(video.updatedAt)).to.be.true | 206 | expect(dateIsValid(video.updatedAt)).to.be.true |
204 | expect(video.account).to.equal('user1') | 207 | expect(video.accountName).to.equal('user1') |
205 | 208 | ||
206 | if (server.url !== 'http://localhost:9002') { | 209 | if (server.url !== 'http://localhost:9002') { |
207 | expect(video.isLocal).to.be.false | 210 | expect(video.isLocal).to.be.false |
@@ -316,7 +319,7 @@ describe('Test multiple servers', function () { | |||
316 | expect(video1.serverHost).to.equal('localhost:9003') | 319 | expect(video1.serverHost).to.equal('localhost:9003') |
317 | expect(video1.duration).to.equal(5) | 320 | expect(video1.duration).to.equal(5) |
318 | expect(video1.tags).to.deep.equal([ 'tag1p3' ]) | 321 | expect(video1.tags).to.deep.equal([ 'tag1p3' ]) |
319 | expect(video1.account).to.equal('root') | 322 | expect(video1.accountName).to.equal('root') |
320 | expect(dateIsValid(video1.createdAt)).to.be.true | 323 | expect(dateIsValid(video1.createdAt)).to.be.true |
321 | expect(dateIsValid(video1.updatedAt)).to.be.true | 324 | expect(dateIsValid(video1.updatedAt)).to.be.true |
322 | 325 | ||
@@ -342,7 +345,7 @@ describe('Test multiple servers', function () { | |||
342 | expect(video2.serverHost).to.equal('localhost:9003') | 345 | expect(video2.serverHost).to.equal('localhost:9003') |
343 | expect(video2.duration).to.equal(5) | 346 | expect(video2.duration).to.equal(5) |
344 | expect(video2.tags).to.deep.equal([ 'tag2p3', 'tag3p3', 'tag4p3' ]) | 347 | expect(video2.tags).to.deep.equal([ 'tag2p3', 'tag3p3', 'tag4p3' ]) |
345 | expect(video2.account).to.equal('root') | 348 | expect(video2.accountName).to.equal('root') |
346 | expect(dateIsValid(video2.createdAt)).to.be.true | 349 | expect(dateIsValid(video2.createdAt)).to.be.true |
347 | expect(dateIsValid(video2.updatedAt)).to.be.true | 350 | expect(dateIsValid(video2.updatedAt)).to.be.true |
348 | 351 | ||
@@ -690,7 +693,7 @@ describe('Test multiple servers', function () { | |||
690 | expect(baseVideo.licence).to.equal(video.licence) | 693 | expect(baseVideo.licence).to.equal(video.licence) |
691 | expect(baseVideo.category).to.equal(video.category) | 694 | expect(baseVideo.category).to.equal(video.category) |
692 | expect(baseVideo.nsfw).to.equal(video.nsfw) | 695 | expect(baseVideo.nsfw).to.equal(video.nsfw) |
693 | expect(baseVideo.account).to.equal(video.account) | 696 | expect(baseVideo.accountName).to.equal(video.accountName) |
694 | expect(baseVideo.tags).to.deep.equal(video.tags) | 697 | expect(baseVideo.tags).to.deep.equal(video.tags) |
695 | } | 698 | } |
696 | }) | 699 | }) |
@@ -706,6 +709,50 @@ describe('Test multiple servers', function () { | |||
706 | }) | 709 | }) |
707 | }) | 710 | }) |
708 | 711 | ||
712 | describe('With minimum parameters', function () { | ||
713 | it('Should upload and propagate the video', async function () { | ||
714 | this.timeout(50000) | ||
715 | |||
716 | const path = '/api/v1/videos/upload' | ||
717 | |||
718 | const req = request(servers[1].url) | ||
719 | .post(path) | ||
720 | .set('Accept', 'application/json') | ||
721 | .set('Authorization', 'Bearer ' + servers[1].accessToken) | ||
722 | .field('name', 'minimum parameters') | ||
723 | .field('privacy', '1') | ||
724 | .field('nsfw', 'false') | ||
725 | .field('channelId', '1') | ||
726 | |||
727 | const filePath = join(__dirname, '..', 'api', 'fixtures', 'video_short.webm') | ||
728 | |||
729 | await req.attach('videofile', filePath) | ||
730 | .expect(200) | ||
731 | |||
732 | await wait(25000) | ||
733 | |||
734 | for (const server of servers) { | ||
735 | const res = await getVideosList(server.url) | ||
736 | const video = res.body.data.find(v => v.name === 'minimum parameters') | ||
737 | |||
738 | expect(video.name).to.equal('minimum parameters') | ||
739 | expect(video.category).to.equal(null) | ||
740 | expect(video.categoryLabel).to.equal('Misc') | ||
741 | expect(video.licence).to.equal(null) | ||
742 | expect(video.licenceLabel).to.equal('Unknown') | ||
743 | expect(video.language).to.equal(null) | ||
744 | expect(video.languageLabel).to.equal('Unknown') | ||
745 | expect(video.nsfw).to.not.be.ok | ||
746 | expect(video.description).to.equal(null) | ||
747 | expect(video.serverHost).to.equal('localhost:9002') | ||
748 | expect(video.accountName).to.equal('root') | ||
749 | expect(video.tags).to.deep.equal([ ]) | ||
750 | expect(dateIsValid(video.createdAt)).to.be.true | ||
751 | expect(dateIsValid(video.updatedAt)).to.be.true | ||
752 | } | ||
753 | }) | ||
754 | }) | ||
755 | |||
709 | after(async function () { | 756 | after(async function () { |
710 | killallServers(servers) | 757 | killallServers(servers) |
711 | 758 | ||
diff --git a/server/tests/api/services.ts b/server/tests/api/services.ts index 8d96ccc5e..4d480c305 100644 --- a/server/tests/api/services.ts +++ b/server/tests/api/services.ts | |||
@@ -46,7 +46,7 @@ describe('Test services', function () { | |||
46 | 46 | ||
47 | expect(res.body.html).to.equal(expectedHtml) | 47 | expect(res.body.html).to.equal(expectedHtml) |
48 | expect(res.body.title).to.equal(server.video.name) | 48 | expect(res.body.title).to.equal(server.video.name) |
49 | expect(res.body.author_name).to.equal(server.video.account) | 49 | expect(res.body.author_name).to.equal(server.video.accountName) |
50 | expect(res.body.width).to.equal(560) | 50 | expect(res.body.width).to.equal(560) |
51 | expect(res.body.height).to.equal(315) | 51 | expect(res.body.height).to.equal(315) |
52 | expect(res.body.thumbnail_url).to.equal(expectedThumbnailUrl) | 52 | expect(res.body.thumbnail_url).to.equal(expectedThumbnailUrl) |
@@ -66,7 +66,7 @@ describe('Test services', function () { | |||
66 | 66 | ||
67 | expect(res.body.html).to.equal(expectedHtml) | 67 | expect(res.body.html).to.equal(expectedHtml) |
68 | expect(res.body.title).to.equal(server.video.name) | 68 | expect(res.body.title).to.equal(server.video.name) |
69 | expect(res.body.author_name).to.equal(server.video.account) | 69 | expect(res.body.author_name).to.equal(server.video.accountName) |
70 | expect(res.body.height).to.equal(50) | 70 | expect(res.body.height).to.equal(50) |
71 | expect(res.body.width).to.equal(50) | 71 | expect(res.body.width).to.equal(50) |
72 | expect(res.body).to.not.have.property('thumbnail_url') | 72 | expect(res.body).to.not.have.property('thumbnail_url') |
diff --git a/server/tests/api/single-server.ts b/server/tests/api/single-server.ts index 041d13225..174fb480d 100644 --- a/server/tests/api/single-server.ts +++ b/server/tests/api/single-server.ts | |||
@@ -1,40 +1,40 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | 1 | /* tslint:disable:no-unused-expression */ |
2 | 2 | ||
3 | import * as chai from 'chai' | ||
3 | import { keyBy } from 'lodash' | 4 | import { keyBy } from 'lodash' |
4 | import { join } from 'path' | ||
5 | import 'mocha' | 5 | import 'mocha' |
6 | import * as chai from 'chai' | 6 | import { join } from 'path' |
7 | const expect = chai.expect | ||
8 | |||
9 | import { | 7 | import { |
10 | ServerInfo, | ||
11 | flushTests, | ||
12 | runServer, | ||
13 | uploadVideo, | ||
14 | getVideosList, | ||
15 | rateVideo, | ||
16 | removeVideo, | ||
17 | wait, | ||
18 | setAccessTokensToServers, | ||
19 | searchVideo, | ||
20 | killallServers, | ||
21 | dateIsValid, | 8 | dateIsValid, |
9 | flushTests, | ||
10 | getVideo, | ||
22 | getVideoCategories, | 11 | getVideoCategories, |
23 | getVideoLicences, | ||
24 | getVideoLanguages, | 12 | getVideoLanguages, |
13 | getVideoLicences, | ||
25 | getVideoPrivacies, | 14 | getVideoPrivacies, |
26 | testVideoImage, | 15 | getVideosList, |
27 | webtorrentAdd, | ||
28 | getVideo, | ||
29 | readdirPromise, | ||
30 | getVideosListPagination, | 16 | getVideosListPagination, |
31 | searchVideoWithPagination, | ||
32 | getVideosListSort, | 17 | getVideosListSort, |
18 | killallServers, | ||
19 | rateVideo, | ||
20 | readdirPromise, | ||
21 | removeVideo, | ||
22 | runServer, | ||
23 | searchVideo, | ||
24 | searchVideoWithPagination, | ||
33 | searchVideoWithSort, | 25 | searchVideoWithSort, |
34 | updateVideo | 26 | ServerInfo, |
27 | setAccessTokensToServers, | ||
28 | testVideoImage, | ||
29 | updateVideo, | ||
30 | uploadVideo, | ||
31 | wait, | ||
32 | webtorrentAdd | ||
35 | } from '../utils' | 33 | } from '../utils' |
36 | import { viewVideo } from '../utils/videos' | 34 | import { viewVideo } from '../utils/videos' |
37 | 35 | ||
36 | const expect = chai.expect | ||
37 | |||
38 | describe('Test a single server', function () { | 38 | describe('Test a single server', function () { |
39 | let server: ServerInfo = null | 39 | let server: ServerInfo = null |
40 | let videoId = -1 | 40 | let videoId = -1 |
@@ -103,7 +103,10 @@ describe('Test a single server', function () { | |||
103 | licence: 6, | 103 | licence: 6, |
104 | tags: [ 'tag1', 'tag2', 'tag3' ] | 104 | tags: [ 'tag1', 'tag2', 'tag3' ] |
105 | } | 105 | } |
106 | await uploadVideo(server.url, server.accessToken, videoAttributes) | 106 | const res = await uploadVideo(server.url, server.accessToken, videoAttributes) |
107 | expect(res.body.video).to.not.be.undefined | ||
108 | expect(res.body.video.id).to.equal(1) | ||
109 | expect(res.body.video.uuid).to.have.length.above(5) | ||
107 | }) | 110 | }) |
108 | 111 | ||
109 | it('Should seed the uploaded video', async function () { | 112 | it('Should seed the uploaded video', async function () { |
@@ -127,7 +130,7 @@ describe('Test a single server', function () { | |||
127 | expect(video.nsfw).to.be.ok | 130 | expect(video.nsfw).to.be.ok |
128 | expect(video.description).to.equal('my super description') | 131 | expect(video.description).to.equal('my super description') |
129 | expect(video.serverHost).to.equal('localhost:9001') | 132 | expect(video.serverHost).to.equal('localhost:9001') |
130 | expect(video.account).to.equal('root') | 133 | expect(video.accountName).to.equal('root') |
131 | expect(video.isLocal).to.be.true | 134 | expect(video.isLocal).to.be.true |
132 | expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) | 135 | expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) |
133 | expect(dateIsValid(video.createdAt)).to.be.true | 136 | expect(dateIsValid(video.createdAt)).to.be.true |
@@ -176,7 +179,7 @@ describe('Test a single server', function () { | |||
176 | expect(video.nsfw).to.be.ok | 179 | expect(video.nsfw).to.be.ok |
177 | expect(video.description).to.equal('my super description') | 180 | expect(video.description).to.equal('my super description') |
178 | expect(video.serverHost).to.equal('localhost:9001') | 181 | expect(video.serverHost).to.equal('localhost:9001') |
179 | expect(video.account).to.equal('root') | 182 | expect(video.accountName).to.equal('root') |
180 | expect(video.isLocal).to.be.true | 183 | expect(video.isLocal).to.be.true |
181 | expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) | 184 | expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) |
182 | expect(dateIsValid(video.createdAt)).to.be.true | 185 | expect(dateIsValid(video.createdAt)).to.be.true |
@@ -225,7 +228,7 @@ describe('Test a single server', function () { | |||
225 | expect(video.views).to.equal(3) | 228 | expect(video.views).to.equal(3) |
226 | }) | 229 | }) |
227 | 230 | ||
228 | it('Should search the video by name by default', async function () { | 231 | it('Should search the video by name', async function () { |
229 | const res = await searchVideo(server.url, 'my') | 232 | const res = await searchVideo(server.url, 'my') |
230 | 233 | ||
231 | expect(res.body.total).to.equal(1) | 234 | expect(res.body.total).to.equal(1) |
@@ -243,7 +246,7 @@ describe('Test a single server', function () { | |||
243 | expect(video.nsfw).to.be.ok | 246 | expect(video.nsfw).to.be.ok |
244 | expect(video.description).to.equal('my super description') | 247 | expect(video.description).to.equal('my super description') |
245 | expect(video.serverHost).to.equal('localhost:9001') | 248 | expect(video.serverHost).to.equal('localhost:9001') |
246 | expect(video.account).to.equal('root') | 249 | expect(video.accountName).to.equal('root') |
247 | expect(video.isLocal).to.be.true | 250 | expect(video.isLocal).to.be.true |
248 | expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) | 251 | expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) |
249 | expect(dateIsValid(video.createdAt)).to.be.true | 252 | expect(dateIsValid(video.createdAt)).to.be.true |
@@ -279,35 +282,36 @@ describe('Test a single server', function () { | |||
279 | // }) | 282 | // }) |
280 | // }) | 283 | // }) |
281 | 284 | ||
282 | it('Should search the video by tag', async function () { | 285 | // Not implemented yet |
283 | const res = await searchVideo(server.url, 'tag1', 'tags') | 286 | // it('Should search the video by tag', async function () { |
284 | 287 | // const res = await searchVideo(server.url, 'tag1') | |
285 | expect(res.body.total).to.equal(1) | 288 | // |
286 | expect(res.body.data).to.be.an('array') | 289 | // expect(res.body.total).to.equal(1) |
287 | expect(res.body.data.length).to.equal(1) | 290 | // expect(res.body.data).to.be.an('array') |
288 | 291 | // expect(res.body.data.length).to.equal(1) | |
289 | const video = res.body.data[0] | 292 | // |
290 | expect(video.name).to.equal('my super name') | 293 | // const video = res.body.data[0] |
291 | expect(video.category).to.equal(2) | 294 | // expect(video.name).to.equal('my super name') |
292 | expect(video.categoryLabel).to.equal('Films') | 295 | // expect(video.category).to.equal(2) |
293 | expect(video.licence).to.equal(6) | 296 | // expect(video.categoryLabel).to.equal('Films') |
294 | expect(video.licenceLabel).to.equal('Attribution - Non Commercial - No Derivatives') | 297 | // expect(video.licence).to.equal(6) |
295 | expect(video.language).to.equal(3) | 298 | // expect(video.licenceLabel).to.equal('Attribution - Non Commercial - No Derivatives') |
296 | expect(video.languageLabel).to.equal('Mandarin') | 299 | // expect(video.language).to.equal(3) |
297 | expect(video.nsfw).to.be.ok | 300 | // expect(video.languageLabel).to.equal('Mandarin') |
298 | expect(video.description).to.equal('my super description') | 301 | // expect(video.nsfw).to.be.ok |
299 | expect(video.serverHost).to.equal('localhost:9001') | 302 | // expect(video.description).to.equal('my super description') |
300 | expect(video.account).to.equal('root') | 303 | // expect(video.serverHost).to.equal('localhost:9001') |
301 | expect(video.isLocal).to.be.true | 304 | // expect(video.accountName).to.equal('root') |
302 | expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) | 305 | // expect(video.isLocal).to.be.true |
303 | expect(dateIsValid(video.createdAt)).to.be.true | 306 | // expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) |
304 | expect(dateIsValid(video.updatedAt)).to.be.true | 307 | // expect(dateIsValid(video.createdAt)).to.be.true |
305 | 308 | // expect(dateIsValid(video.updatedAt)).to.be.true | |
306 | const test = await testVideoImage(server.url, 'video_short.webm', video.thumbnailPath) | 309 | // |
307 | expect(test).to.equal(true) | 310 | // const test = await testVideoImage(server.url, 'video_short.webm', video.thumbnailPath) |
308 | }) | 311 | // expect(test).to.equal(true) |
312 | // }) | ||
309 | 313 | ||
310 | it('Should not find a search by name by default', async function () { | 314 | it('Should not find a search by name', async function () { |
311 | const res = await searchVideo(server.url, 'hello') | 315 | const res = await searchVideo(server.url, 'hello') |
312 | 316 | ||
313 | expect(res.body.total).to.equal(0) | 317 | expect(res.body.total).to.equal(0) |
@@ -315,21 +319,23 @@ describe('Test a single server', function () { | |||
315 | expect(res.body.data.length).to.equal(0) | 319 | expect(res.body.data.length).to.equal(0) |
316 | }) | 320 | }) |
317 | 321 | ||
318 | it('Should not find a search by author', async function () { | 322 | // Not implemented yet |
319 | const res = await searchVideo(server.url, 'hello', 'account') | 323 | // it('Should not find a search by author', async function () { |
320 | 324 | // const res = await searchVideo(server.url, 'hello') | |
321 | expect(res.body.total).to.equal(0) | 325 | // |
322 | expect(res.body.data).to.be.an('array') | 326 | // expect(res.body.total).to.equal(0) |
323 | expect(res.body.data.length).to.equal(0) | 327 | // expect(res.body.data).to.be.an('array') |
324 | }) | 328 | // expect(res.body.data.length).to.equal(0) |
325 | 329 | // }) | |
326 | it('Should not find a search by tag', async function () { | 330 | // |
327 | const res = await searchVideo(server.url, 'hello', 'tags') | 331 | // Not implemented yet |
328 | 332 | // it('Should not find a search by tag', async function () { | |
329 | expect(res.body.total).to.equal(0) | 333 | // const res = await searchVideo(server.url, 'hello') |
330 | expect(res.body.data).to.be.an('array') | 334 | // |
331 | expect(res.body.data.length).to.equal(0) | 335 | // expect(res.body.total).to.equal(0) |
332 | }) | 336 | // expect(res.body.data).to.be.an('array') |
337 | // expect(res.body.data.length).to.equal(0) | ||
338 | // }) | ||
333 | 339 | ||
334 | it('Should remove the video', async function () { | 340 | it('Should remove the video', async function () { |
335 | await removeVideo(server.url, server.accessToken, videoId) | 341 | await removeVideo(server.url, server.accessToken, videoId) |
@@ -357,7 +363,7 @@ describe('Test a single server', function () { | |||
357 | 'video_short1.webm', 'video_short2.webm', 'video_short3.webm' | 363 | 'video_short1.webm', 'video_short2.webm', 'video_short3.webm' |
358 | ] | 364 | ] |
359 | 365 | ||
360 | // const tasks: Promise<any>[] = [] | 366 | const tasks: Promise<any>[] = [] |
361 | for (const video of videos) { | 367 | for (const video of videos) { |
362 | const videoAttributes = { | 368 | const videoAttributes = { |
363 | name: video + ' name', | 369 | name: video + ' name', |
@@ -371,13 +377,10 @@ describe('Test a single server', function () { | |||
371 | } | 377 | } |
372 | 378 | ||
373 | const p = uploadVideo(server.url, server.accessToken, videoAttributes) | 379 | const p = uploadVideo(server.url, server.accessToken, videoAttributes) |
374 | await p | 380 | tasks.push(p) |
375 | } | 381 | } |
376 | // FIXME: concurrent uploads does not work :( | 382 | |
377 | // tasks.push(p) | 383 | await Promise.all(tasks) |
378 | // } | ||
379 | // | ||
380 | // await Promise.all(tasks) | ||
381 | }) | 384 | }) |
382 | 385 | ||
383 | it('Should have the correct durations', async function () { | 386 | it('Should have the correct durations', async function () { |
@@ -443,7 +446,7 @@ describe('Test a single server', function () { | |||
443 | }) | 446 | }) |
444 | 447 | ||
445 | it('Should search the first video', async function () { | 448 | it('Should search the first video', async function () { |
446 | const res = await searchVideoWithPagination(server.url, 'webm', 'name', 0, 1, 'name') | 449 | const res = await searchVideoWithPagination(server.url, 'webm', 0, 1, 'name') |
447 | 450 | ||
448 | const videos = res.body.data | 451 | const videos = res.body.data |
449 | expect(res.body.total).to.equal(4) | 452 | expect(res.body.total).to.equal(4) |
@@ -452,7 +455,7 @@ describe('Test a single server', function () { | |||
452 | }) | 455 | }) |
453 | 456 | ||
454 | it('Should search the last two videos', async function () { | 457 | it('Should search the last two videos', async function () { |
455 | const res = await searchVideoWithPagination(server.url, 'webm', 'name', 2, 2, 'name') | 458 | const res = await searchVideoWithPagination(server.url, 'webm', 2, 2, 'name') |
456 | 459 | ||
457 | const videos = res.body.data | 460 | const videos = res.body.data |
458 | expect(res.body.total).to.equal(4) | 461 | expect(res.body.total).to.equal(4) |
@@ -462,20 +465,21 @@ describe('Test a single server', function () { | |||
462 | }) | 465 | }) |
463 | 466 | ||
464 | it('Should search all the webm videos', async function () { | 467 | it('Should search all the webm videos', async function () { |
465 | const res = await searchVideoWithPagination(server.url, 'webm', 'name', 0, 15) | 468 | const res = await searchVideoWithPagination(server.url, 'webm', 0, 15) |
466 | 469 | ||
467 | const videos = res.body.data | 470 | const videos = res.body.data |
468 | expect(res.body.total).to.equal(4) | 471 | expect(res.body.total).to.equal(4) |
469 | expect(videos.length).to.equal(4) | 472 | expect(videos.length).to.equal(4) |
470 | }) | 473 | }) |
471 | 474 | ||
472 | it('Should search all the root author videos', async function () { | 475 | // Not implemented yet |
473 | const res = await searchVideoWithPagination(server.url, 'root', 'account', 0, 15) | 476 | // it('Should search all the root author videos', async function () { |
474 | 477 | // const res = await searchVideoWithPagination(server.url, 'root', 0, 15) | |
475 | const videos = res.body.data | 478 | // |
476 | expect(res.body.total).to.equal(6) | 479 | // const videos = res.body.data |
477 | expect(videos.length).to.equal(6) | 480 | // expect(res.body.total).to.equal(6) |
478 | }) | 481 | // expect(videos.length).to.equal(6) |
482 | // }) | ||
479 | 483 | ||
480 | // Not implemented yet | 484 | // Not implemented yet |
481 | // it('Should search all the 9001 port videos', async function () { | 485 | // it('Should search all the 9001 port videos', async function () { |
@@ -559,7 +563,8 @@ describe('Test a single server', function () { | |||
559 | expect(video.nsfw).to.be.ok | 563 | expect(video.nsfw).to.be.ok |
560 | expect(video.description).to.equal('my super description updated') | 564 | expect(video.description).to.equal('my super description updated') |
561 | expect(video.serverHost).to.equal('localhost:9001') | 565 | expect(video.serverHost).to.equal('localhost:9001') |
562 | expect(video.account).to.equal('root') | 566 | expect(video.accountName).to.equal('root') |
567 | expect(video.account.name).to.equal('root') | ||
563 | expect(video.isLocal).to.be.true | 568 | expect(video.isLocal).to.be.true |
564 | expect(video.tags).to.deep.equal([ 'tagup1', 'tagup2' ]) | 569 | expect(video.tags).to.deep.equal([ 'tagup1', 'tagup2' ]) |
565 | expect(dateIsValid(video.createdAt)).to.be.true | 570 | expect(dateIsValid(video.createdAt)).to.be.true |
@@ -608,7 +613,7 @@ describe('Test a single server', function () { | |||
608 | expect(video.nsfw).to.be.ok | 613 | expect(video.nsfw).to.be.ok |
609 | expect(video.description).to.equal('my super description updated') | 614 | expect(video.description).to.equal('my super description updated') |
610 | expect(video.serverHost).to.equal('localhost:9001') | 615 | expect(video.serverHost).to.equal('localhost:9001') |
611 | expect(video.account).to.equal('root') | 616 | expect(video.accountName).to.equal('root') |
612 | expect(video.isLocal).to.be.true | 617 | expect(video.isLocal).to.be.true |
613 | expect(video.tags).to.deep.equal([ 'supertag', 'tag1', 'tag2' ]) | 618 | expect(video.tags).to.deep.equal([ 'supertag', 'tag1', 'tag2' ]) |
614 | expect(dateIsValid(video.createdAt)).to.be.true | 619 | expect(dateIsValid(video.createdAt)).to.be.true |
@@ -648,7 +653,7 @@ describe('Test a single server', function () { | |||
648 | expect(video.nsfw).to.be.ok | 653 | expect(video.nsfw).to.be.ok |
649 | expect(video.description).to.equal('hello everybody') | 654 | expect(video.description).to.equal('hello everybody') |
650 | expect(video.serverHost).to.equal('localhost:9001') | 655 | expect(video.serverHost).to.equal('localhost:9001') |
651 | expect(video.account).to.equal('root') | 656 | expect(video.accountName).to.equal('root') |
652 | expect(video.isLocal).to.be.true | 657 | expect(video.isLocal).to.be.true |
653 | expect(video.tags).to.deep.equal([ 'supertag', 'tag1', 'tag2' ]) | 658 | expect(video.tags).to.deep.equal([ 'supertag', 'tag1', 'tag2' ]) |
654 | expect(dateIsValid(video.createdAt)).to.be.true | 659 | expect(dateIsValid(video.createdAt)).to.be.true |
diff --git a/server/tests/api/users.ts b/server/tests/api/users.ts index 33646e84f..b3163b1e1 100644 --- a/server/tests/api/users.ts +++ b/server/tests/api/users.ts | |||
@@ -113,11 +113,11 @@ describe('Test users', function () { | |||
113 | 113 | ||
114 | it('Should upload the video with the correct token', async function () { | 114 | it('Should upload the video with the correct token', async function () { |
115 | const videoAttributes = {} | 115 | const videoAttributes = {} |
116 | await uploadVideo(server.url, accessToken, videoAttributes, 204) | 116 | await uploadVideo(server.url, accessToken, videoAttributes) |
117 | const res = await getVideosList(server.url) | 117 | const res = await getVideosList(server.url) |
118 | const video = res.body.data[ 0 ] | 118 | const video = res.body.data[ 0 ] |
119 | 119 | ||
120 | expect(video.account) | 120 | expect(video.accountName) |
121 | .to | 121 | .to |
122 | .equal('root') | 122 | .equal('root') |
123 | videoId = video.id | 123 | videoId = video.id |
@@ -125,7 +125,7 @@ describe('Test users', function () { | |||
125 | 125 | ||
126 | it('Should upload the video again with the correct token', async function () { | 126 | it('Should upload the video again with the correct token', async function () { |
127 | const videoAttributes = {} | 127 | const videoAttributes = {} |
128 | await uploadVideo(server.url, accessToken, videoAttributes, 204) | 128 | await uploadVideo(server.url, accessToken, videoAttributes) |
129 | }) | 129 | }) |
130 | 130 | ||
131 | it('Should retrieve a video rating', async function () { | 131 | it('Should retrieve a video rating', async function () { |
@@ -487,7 +487,7 @@ describe('Test users', function () { | |||
487 | .equal(1) | 487 | .equal(1) |
488 | 488 | ||
489 | const video = res.body.data[ 0 ] | 489 | const video = res.body.data[ 0 ] |
490 | expect(video.account) | 490 | expect(video.accountName) |
491 | .to | 491 | .to |
492 | .equal('root') | 492 | .equal('root') |
493 | }) | 493 | }) |
diff --git a/server/tests/utils/servers.ts b/server/tests/utils/servers.ts index faa2f19ff..8340fbc18 100644 --- a/server/tests/utils/servers.ts +++ b/server/tests/utils/servers.ts | |||
@@ -24,7 +24,7 @@ interface ServerInfo { | |||
24 | id: number | 24 | id: number |
25 | uuid: string | 25 | uuid: string |
26 | name: string | 26 | name: string |
27 | account: string | 27 | accountName: string |
28 | } | 28 | } |
29 | 29 | ||
30 | remoteVideo?: { | 30 | remoteVideo?: { |
diff --git a/server/tests/utils/videos.ts b/server/tests/utils/videos.ts index 73a9f1a0a..fb758cf29 100644 --- a/server/tests/utils/videos.ts +++ b/server/tests/utils/videos.ts | |||
@@ -145,26 +145,25 @@ function removeVideo (url: string, token: string, id: number, expectedStatus = 2 | |||
145 | .expect(expectedStatus) | 145 | .expect(expectedStatus) |
146 | } | 146 | } |
147 | 147 | ||
148 | function searchVideo (url: string, search: string, field?: string) { | 148 | function searchVideo (url: string, search: string) { |
149 | const path = '/api/v1/videos' | 149 | const path = '/api/v1/videos' |
150 | const req = request(url) | 150 | const req = request(url) |
151 | .get(path + '/search/' + search) | 151 | .get(path + '/search') |
152 | .set('Accept', 'application/json') | 152 | .query({ search }) |
153 | 153 | .set('Accept', 'application/json') | |
154 | if (field) req.query({ field }) | ||
155 | 154 | ||
156 | return req.expect(200) | 155 | return req.expect(200) |
157 | .expect('Content-Type', /json/) | 156 | .expect('Content-Type', /json/) |
158 | } | 157 | } |
159 | 158 | ||
160 | function searchVideoWithPagination (url: string, search: string, field: string, start: number, count: number, sort?: string) { | 159 | function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) { |
161 | const path = '/api/v1/videos' | 160 | const path = '/api/v1/videos' |
162 | 161 | ||
163 | const req = request(url) | 162 | const req = request(url) |
164 | .get(path + '/search/' + search) | 163 | .get(path + '/search') |
165 | .query({ start }) | 164 | .query({ start }) |
165 | .query({ search }) | ||
166 | .query({ count }) | 166 | .query({ count }) |
167 | .query({ field }) | ||
168 | 167 | ||
169 | if (sort) req.query({ sort }) | 168 | if (sort) req.query({ sort }) |
170 | 169 | ||
@@ -177,7 +176,8 @@ function searchVideoWithSort (url: string, search: string, sort: string) { | |||
177 | const path = '/api/v1/videos' | 176 | const path = '/api/v1/videos' |
178 | 177 | ||
179 | return request(url) | 178 | return request(url) |
180 | .get(path + '/search/' + search) | 179 | .get(path + '/search') |
180 | .query({ search }) | ||
181 | .query({ sort }) | 181 | .query({ sort }) |
182 | .set('Accept', 'application/json') | 182 | .set('Accept', 'application/json') |
183 | .expect(200) | 183 | .expect(200) |
@@ -201,7 +201,7 @@ async function testVideoImage (url: string, imageName: string, imagePath: string | |||
201 | } | 201 | } |
202 | } | 202 | } |
203 | 203 | ||
204 | async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 204) { | 204 | async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 200) { |
205 | const path = '/api/v1/videos/upload' | 205 | const path = '/api/v1/videos/upload' |
206 | let defaultChannelId = '1' | 206 | let defaultChannelId = '1' |
207 | 207 | ||
diff --git a/shared/models/accounts/account.model.ts b/shared/models/accounts/account.model.ts index 338426dc7..d14701317 100644 --- a/shared/models/accounts/account.model.ts +++ b/shared/models/accounts/account.model.ts | |||
@@ -1,5 +1,13 @@ | |||
1 | import { Avatar } from '../avatars/avatar.model' | ||
2 | |||
1 | export interface Account { | 3 | export interface Account { |
2 | id: number | 4 | id: number |
5 | uuid: string | ||
3 | name: string | 6 | name: string |
4 | host: string | 7 | host: string |
8 | followingCount: number | ||
9 | followersCount: number | ||
10 | createdAt: Date | ||
11 | updatedAt: Date | ||
12 | avatar: Avatar | ||
5 | } | 13 | } |
diff --git a/shared/models/avatars/avatar.model.ts b/shared/models/avatars/avatar.model.ts new file mode 100644 index 000000000..301d00929 --- /dev/null +++ b/shared/models/avatars/avatar.model.ts | |||
@@ -0,0 +1,5 @@ | |||
1 | export interface Avatar { | ||
2 | path: string | ||
3 | createdAt: Date | string | ||
4 | updatedAt: Date | string | ||
5 | } | ||
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index a8012734c..4b17881e5 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { Account } from '../accounts' | ||
1 | import { VideoChannel } from '../videos/video-channel.model' | 2 | import { VideoChannel } from '../videos/video-channel.model' |
2 | import { UserRole } from './user-role' | 3 | import { UserRole } from './user-role' |
3 | 4 | ||
@@ -8,10 +9,7 @@ export interface User { | |||
8 | displayNSFW: boolean | 9 | displayNSFW: boolean |
9 | role: UserRole | 10 | role: UserRole |
10 | videoQuota: number | 11 | videoQuota: number |
11 | createdAt: Date, | 12 | createdAt: Date |
12 | account: { | 13 | account: Account |
13 | id: number | ||
14 | uuid: string | ||
15 | } | ||
16 | videoChannels?: VideoChannel[] | 14 | videoChannels?: VideoChannel[] |
17 | } | 15 | } |
diff --git a/shared/models/videos/video-create.model.ts b/shared/models/videos/video-create.model.ts index e537c38a8..8bc6a6639 100644 --- a/shared/models/videos/video-create.model.ts +++ b/shared/models/videos/video-create.model.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import { VideoPrivacy } from './video-privacy.enum' | 1 | import { VideoPrivacy } from './video-privacy.enum' |
2 | 2 | ||
3 | export interface VideoCreate { | 3 | export interface VideoCreate { |
4 | category: number | 4 | category?: number |
5 | licence: number | 5 | licence?: number |
6 | language: number | 6 | language?: number |
7 | description: string | 7 | description?: string |
8 | channelId: number | 8 | channelId: number |
9 | nsfw: boolean | 9 | nsfw: boolean |
10 | name: string | 10 | name: string |
11 | tags: string[] | 11 | tags?: string[] |
12 | privacy: VideoPrivacy | 12 | privacy: VideoPrivacy |
13 | } | 13 | } |
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index 08b29425c..dc12a05d9 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { Account } from '../accounts' | ||
1 | import { VideoChannel } from './video-channel.model' | 2 | import { VideoChannel } from './video-channel.model' |
2 | import { VideoPrivacy } from './video-privacy.enum' | 3 | import { VideoPrivacy } from './video-privacy.enum' |
3 | 4 | ||
@@ -13,7 +14,7 @@ export interface VideoFile { | |||
13 | export interface Video { | 14 | export interface Video { |
14 | id: number | 15 | id: number |
15 | uuid: string | 16 | uuid: string |
16 | account: string | 17 | accountName: string |
17 | createdAt: Date | string | 18 | createdAt: Date | string |
18 | updatedAt: Date | string | 19 | updatedAt: Date | string |
19 | categoryLabel: string | 20 | categoryLabel: string |
@@ -43,4 +44,5 @@ export interface VideoDetails extends Video { | |||
43 | descriptionPath: string | 44 | descriptionPath: string |
44 | channel: VideoChannel | 45 | channel: VideoChannel |
45 | files: VideoFile[] | 46 | files: VideoFile[] |
47 | account: Account | ||
46 | } | 48 | } |