diff --git a/CREDITS.md b/CREDITS.md index a7b2b5568..65017bbc1 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -8,14 +8,9 @@ # Design -Inspirations from: +By [Olivier Massain](https://twitter.com/omassain) - * [Aurélien Salomon](https://dribbble.com/shots/1338727-Youtube-Redesign) - * [Wojciech Zieliński](https://dribbble.com/shots/3000315-youtube-concept) - -Video.js theme: - - * [zanechua](https://github.com/zanechua/videojs-sublime-inspired-skin) +Icons from [Robbie Pearce](https://robbiepearce.com/softies/) # Fonts diff --git a/client/.bootstraprc b/client/.bootstraprc index 6ceef4fe9..cc6768d43 100644 --- a/client/.bootstraprc +++ b/client/.bootstraprc @@ -84,19 +84,19 @@ styles: navs: true navbar: false breadcrumbs: false - pagination: true + pagination: false pager: false - labels: true + labels: false badges: false jumbotron: false - thumbnails: true + thumbnails: false alerts: true - progress-bars: true + progress-bars: false media: true list-group: false panels: true wells: false - responsive-embed: true + responsive-embed: false close: true # 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') const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin') const InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin') const ngcWebpack = require('ngc-webpack') +const CopyWebpackPlugin = require('copy-webpack-plugin') const WebpackNotifierPlugin = require('webpack-notifier') @@ -146,14 +147,15 @@ module.exports = function (options) { loader: 'sass-resources-loader', options: { resources: [ - helpers.root('src/sass/_variables.scss') + helpers.root('src/sass/_variables.scss'), + helpers.root('src/sass/_mixins.scss') ] } } ] }, { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: 'url-loader?limit=10000&minetype=application/font-woff' }, - { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: 'file-loader' }, + { test: /\.(otf|ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: 'url-loader?limit=10000' }, /* Raw loader support for *.html * Returns file content as string @@ -266,6 +268,17 @@ module.exports = function (options) { inject: 'body' }), + new CopyWebpackPlugin([ + { + from: helpers.root('src/assets/images/favicon.png'), + to: 'assets/images/favicon.png' + }, + { + from: helpers.root('src/assets/images/default-avatar.png'), + to: 'assets/images/default-avatar.png' + } + ]), + /* * Plugin: ScriptExtHtmlWebpackPlugin * Description: Enhances html-webpack-plugin functionality @@ -289,6 +302,7 @@ module.exports = function (options) { */ new LoaderOptionsPlugin({ options: { + context: '', sassLoader: { precision: 10, 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) { loader: 'sass-resources-loader', options: { resources: [ - helpers.root('src/sass/_variables.scss') + helpers.root('src/sass/_variables.scss'), + helpers.root('src/sass/_mixins.scss') ] } } 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 @@ "@types/webpack": "^3.0.0", "@types/webtorrent": "^0.98.4", "add-asset-html-webpack-plugin": "^2.0.1", - "angular-pipes": "^6.0.0", "angular2-notifications": "^0.7.7", "angular2-template-loader": "^0.6.0", "assets-webpack-plugin": "^3.4.0", @@ -70,8 +69,10 @@ "markdown-it": "^8.4.0", "ng-router-loader": "^2.0.0", "ngc-webpack": "3.2.2", - "ngx-bootstrap": "1.9.3", + "ngx-bootstrap": "2.0.0-beta.9", "ngx-chips": "1.5.3", + "ngx-infinite-scroll": "^0.7.0", + "ngx-pipes": "^2.0.5", "node-sass": "^4.1.1", "normalize.css": "^7.0.0", "optimize-js-plugin": "0.0.4", @@ -86,6 +87,7 @@ "sass-resources-loader": "^1.2.1", "script-ext-html-webpack-plugin": "^1.3.2", "source-map-loader": "^0.2.1", + "source-sans-pro": "^2.0.10", "standard": "^10.0.0", "string-replace-loader": "^1.0.3", "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 @@ +
+ + +
+ +
+
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 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 @@ import { Component } from '@angular/core' +import { UserRight } from '../../../../shared' +import { AuthService } from '../core/auth/auth.service' @Component({ - template: '' + templateUrl: './admin.component.html', + styleUrls: [ './admin.component.scss' ] }) export class AdminComponent { + constructor (private auth: AuthService) {} + + hasUsersRight () { + return this.auth.getUser().hasRight(UserRight.MANAGE_USERS) + } + + hasServerFollowRight () { + return this.auth.getUser().hasRight(UserRight.MANAGE_SERVER_FOLLOW) + } + + hasVideoAbusesRight () { + return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES) + } + + hasVideoBlacklistRight () { + return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) + } + + hasJobsRight () { + return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS) + } } 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 @@ -
-
-

Followers list

- - - - - - - - -
-
+ + + + + + + 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 @@ -.btn { - margin-top: 10px; -} 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 @@ -
-
+
{{ error }}
-

Add following

+
+
+ -
{{ error }}
+ - -
- - -
- - - - - -
- -
- It should be a valid host. -
-
- -
- It seems that you are not on a HTTPS server. Your webserver need to have TLS activated in order to follow servers. -
- - - +
+ {{ hostsError }} +
-
+ +
+ It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers. +
+ + + 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 @@ -table { - margin-bottom: 40px; +textarea { + height: 250px; } -.input-group-btn button { - width: 35px; +input[type=submit] { + @include peertube-button; + @include orange-button; } + 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 @@ -import { Component, OnInit } from '@angular/core' -import { FormControl, FormGroup } from '@angular/forms' +import { Component } from '@angular/core' import { Router } from '@angular/router' - import { NotificationsService } from 'angular2-notifications' - import { ConfirmService } from '../../../core' import { validateHost } from '../../../shared' import { FollowService } from '../shared' @@ -13,9 +10,9 @@ import { FollowService } from '../shared' templateUrl: './following-add.component.html', styleUrls: [ './following-add.component.scss' ] }) -export class FollowingAddComponent implements OnInit { - form: FormGroup - hosts: string[] = [ ] +export class FollowingAddComponent { + hostsString = '' + hostsError: string = null error: string = null constructor ( @@ -25,76 +22,50 @@ export class FollowingAddComponent implements OnInit { private followService: FollowService ) {} - ngOnInit () { - this.form = new FormGroup({}) - this.addField() - } - - addField () { - this.form.addControl(`host-${this.hosts.length}`, new FormControl('', [ validateHost ])) - this.hosts.push('') - } - - canMakeFriends () { + httpEnabled () { return window.location.protocol === 'https:' } - customTrackBy (index: number, obj: any): any { - return index - } + onHostsChanged () { + this.hostsError = null - displayAddField (index: number) { - return index === (this.hosts.length - 1) - } + const newHostsErrors = [] + const hosts = this.getNotEmptyHosts() - displayRemoveField (index: number) { - return (index !== 0 || this.hosts.length > 1) && index !== (this.hosts.length - 1) - } - - isFormValid () { - // Do not check the last input - for (let i = 0; i < this.hosts.length - 1; i++) { - if (!this.form.controls[`host-${i}`].valid) return false + for (const host of hosts) { + if (validateHost(host) === false) { + newHostsErrors.push(`${host} is not valid`) + } } - const lastIndex = this.hosts.length - 1 - // If the last input (which is not the first) is empty, it's ok - if (this.hosts[lastIndex] === '' && lastIndex !== 0) { - return true - } else { - return this.form.controls[`host-${lastIndex}`].valid + if (newHostsErrors.length !== 0) { + this.hostsError = newHostsErrors.join('. ') } } - removeField (index: number) { - // Remove the last control - this.form.removeControl(`host-${this.hosts.length - 1}`) - this.hosts.splice(index, 1) - } - addFollowing () { this.error = '' - const notEmptyHosts = this.getNotEmptyHosts() - if (notEmptyHosts.length === 0) { - this.error = 'You need to specify at least 1 host.' - return + const hosts = this.getNotEmptyHosts() + if (hosts.length === 0) { + this.error = 'You need to specify hosts to follow.' } - if (!this.isHostsUnique(notEmptyHosts)) { + if (!this.isHostsUnique(hosts)) { this.error = 'Hosts need to be unique.' return } - const confirmMessage = 'Are you sure to make friends with:
- ' + notEmptyHosts.join('
- ') + const confirmMessage = 'If you confirm, you will send a follow request to:
- ' + hosts.join('
- ') this.confirmService.confirm(confirmMessage, 'Follow new server(s)').subscribe( res => { if (res === false) return - this.followService.follow(notEmptyHosts).subscribe( + this.followService.follow(hosts).subscribe( status => { this.notificationsService.success('Success', 'Follow request(s) sent!') - this.router.navigate([ '/admin/follows/following-list' ]) + + setTimeout(() => this.router.navigate([ '/admin/follows/following-list' ]), 500) }, err => this.notificationsService.error('Error', err.message) @@ -103,18 +74,15 @@ export class FollowingAddComponent implements OnInit { ) } - private getNotEmptyHosts () { - const notEmptyHosts = [] - - Object.keys(this.form.value).forEach((hostKey) => { - const host = this.form.value[hostKey] - if (host !== '') notEmptyHosts.push(host) - }) - - return notEmptyHosts - } - private isHostsUnique (hosts: string[]) { return hosts.every(host => hosts.indexOf(host) === hosts.lastIndexOf(host)) } + + private getNotEmptyHosts () { + const hosts = this.hostsString + .split('\n') + .filter(host => host && host.length !== 0) // Eject empty hosts + + return hosts + } } 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 @@ -
-
-

Following list

- - - - - - - - - - - - -
-
+ + + + + + + + + + + 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 @@ -
+
+
Manage follows
+ @@ -8,4 +10,6 @@
+ + 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 @@ -.follows-menu { - margin-top: 20px; -} - -tabset /deep/ { - .nav-link { - padding: 0; - } - - .tab-link { - display: block; - text-align: center; - height: 40px; - width: 120px; - line-height: 40px; - - &:hover, &:active, &:focus { - text-decoration: none !important; - } - } +.admin-sub-title { + flex-grow: 0; + margin-right: 30px; } 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 { for (let i = 0; i < this.links.length; i++) { const path = this.links[i].path - if (url.endsWith(path) === true) { + if (url.endsWith(path) === true && this.followsMenuTabs.tabs[i]) { this.followsMenuTabs.tabs[i].active = true return } 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 @@ -
-
-

Jobs list

- - - - - - - - - - -
+
+
Jobs list
+ + + + + + + +
{{ job.handlerInputData }}
+
+
+ + + +
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 @@ +pre { + font-size: 13px; +} 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 @@ -import { Component } from '@angular/core' +import { Component, OnInit } from '@angular/core' import { NotificationsService } from 'angular2-notifications' import { SortMeta } from 'primeng/primeng' import { Job } from '../../../../../../shared/index' import { RestPagination, RestTable } from '../../../shared' +import { viewportHeight } from '../../../shared/misc/utils' import { JobService } from '../shared' import { RestExtractor } from '../../../shared/rest/rest-extractor.service' @Component({ selector: 'my-jobs-list', templateUrl: './jobs-list.component.html', - styleUrls: [ ] + styleUrls: [ './jobs-list.component.scss' ] }) -export class JobsListComponent extends RestTable { +export class JobsListComponent extends RestTable implements OnInit { jobs: Job[] = [] totalRecords = 0 - rowsPerPage = 10 + rowsPerPage = 20 sort: SortMeta = { field: 'createdAt', order: 1 } pagination: RestPagination = { count: this.rowsPerPage, start: 0 } + scrollHeight = '' constructor ( private notificationsService: NotificationsService, @@ -26,10 +28,14 @@ export class JobsListComponent extends RestTable { super() } + ngOnInit () { + // 270 -> headers + footer... + this.scrollHeight = (viewportHeight() - 380) + 'px' + } + protected loadData () { this.jobsService .getJobs(this.pagination, this.sort) - .map(res => this.restExtractor.applyToResultListData(res, this.formatJob.bind(this))) .subscribe( resultList => { this.jobs = resultList.data @@ -39,12 +45,4 @@ export class JobsListComponent extends RestTable { err => this.notificationsService.error('Error', err.message) ) } - - private formatJob (job: Job) { - const handlerInputData = JSON.stringify(job.handlerInputData) - - return Object.assign(job, { - handlerInputData - }) - } } 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 { return this.authHttp.get>(JobService.BASE_JOB_URL, { params }) .map(res => this.restExtractor.convertResultListDateToHuman(res)) + .map(res => this.restExtractor.applyToResultListData(res, this.prettyPrintData)) .catch(err => this.restExtractor.handleError(err)) } + + private prettyPrintData (obj: Job) { + const handlerInputData = JSON.stringify(obj.handlerInputData, null, 2) + + return Object.assign(obj, { handlerInputData }) + } } 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 @@ -import { Injectable } from '@angular/core' import { HttpClient, HttpParams } from '@angular/common/http' -import { Observable } from 'rxjs/Observable' +import { Injectable } from '@angular/core' +import { BytesPipe } from 'ngx-pipes' +import { SortMeta } from 'primeng/components/common/sortmeta' import 'rxjs/add/operator/catch' import 'rxjs/add/operator/map' - -import { SortMeta } from 'primeng/components/common/sortmeta' -import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe' - -import { RestExtractor, User, RestPagination, RestService } from '../../../shared' -import { UserCreate, UserUpdate, ResultList } from '../../../../../../shared' +import { Observable } from 'rxjs/Observable' +import { ResultList, UserCreate, UserUpdate } from '../../../../../../shared' +import { RestExtractor, RestPagination, RestService, User } from '../../../shared' @Injectable() 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 @@ -
-
+
Add user
+
Edit user {{ username }}
-

Add user

-

Edit user {{ username }}

+
{{ error }}
-
{{ error }}
- -
-
- - -
- {{ formErrors.username }} -
-
- -
- - -
- {{ formErrors.email }} -
-
- -
- - -
- {{ formErrors.password }} -
-
- -
- - - -
- {{ formErrors.role }} -
-
- -
- - - -
- Transcoding is enabled on server. The video quota only take in account original video.
- In maximum, this user could use ~ {{ computeQuotaWithTranscoding() | bytes }}. -
-
- - -
+
+
+ + +
+ {{ formErrors.username }} +
-
+ +
+ + +
+ {{ formErrors.email }} +
+
+ +
+ + +
+ {{ formErrors.password }} +
+
+ +
+ + + +
+ {{ formErrors.role }} +
+
+ +
+ + + +
+ Transcoding is enabled on server. The video quota only take in account original video.
+ In maximum, this user could use ~ {{ computeQuotaWithTranscoding() | bytes }}. +
+
+ + + 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 @@ +.admin-sub-title { + margin-bottom: 30px; +} + +input:not([type=submit]) { + @include peertube-input-text(340px); + display: block; +} + +select { + @include peertube-select(340px); +} + +input[type=submit] { + @include peertube-button; + @include orange-button; +} + .transcoding-information { margin-top: 5px; 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 @@ -
-
+
+
Users list
-

Users list

- - - - - - - - - - - - - - - - - - - - - - - - - Add user - -
+ + + Add user +
+ + + + + + + + + + + + + + + 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 @@ -.add-user { - margin-top: 10px; -} + .add-button { + @include peertube-button-link; + @include orange-button; + + .icon.icon-add { + @include icon(22px); + + margin-right: 3px; + background-image: url('../../../../assets/images/admin/add.svg'); + } + } 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 @@ -
-
- -

Video abuses list

- - - - - - - - - - {{ videoAbuse.videoId }} - - - - - -
+
+
Video abuses list
+ + + + + + + + + + {{ videoAbuse.videoName }} + + + 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 @@ +/deep/ a { + + &, &:hover, &:active, &:focus { + color: #000; + } +} 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' @Component({ selector: 'my-video-abuse-list', - templateUrl: './video-abuse-list.component.html' + templateUrl: './video-abuse-list.component.html', + styleUrls: [ './video-abuse-list.component.scss'] }) export class VideoAbuseListComponent extends RestTable implements OnInit { 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 @@ - + 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 @@ -
{{ error }}
- -
-
- - -
- {{ formErrors['new-password'] }} -
-
- -
- - -
- - -
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 @@ -
{{ error }}
- -
-
- - -
- {{ formErrors['displayNSFW'] }} -
-
- - -
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' import { LoginGuard } from '../core' import { AccountComponent } from './account.component' +import { AccountSettingsComponent } from './account-settings/account-settings.component' +import { AccountVideosComponent } from './account-videos/account-videos.component' const accountRoutes: Routes = [ { path: 'account', component: AccountComponent, - canActivate: [ MetaGuard, LoginGuard ], - data: { - meta: { - title: 'My account' + canActivateChild: [ MetaGuard, LoginGuard ], + children: [ + { + path: 'settings', + component: AccountSettingsComponent, + data: { + meta: { + title: 'Account settings' + } + } + }, + { + path: 'videos', + component: AccountVideosComponent, + data: { + meta: { + title: 'Account videos' + } + } } - } + ] } ] 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 @@ +
{{ error }}
+ +
+ + + +
+ {{ formErrors['new-password'] }} +
+ + + + +
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 @@ +input[type=password] { + @include peertube-input-text(340px); + display: block; + + &#new-confirmed-password { + margin-top: 15px; + } +} + +input[type=submit] { + @include peertube-button; + @include orange-button; + + margin-top: 15px; +} + 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 similarity index 88% rename from client/src/app/account/account-change-password/account-change-password.component.ts rename to 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 @@ import { Component, OnInit } from '@angular/core' import { FormBuilder, FormGroup } from '@angular/forms' -import { Router } from '@angular/router' - import { NotificationsService } from 'angular2-notifications' - -import { FormReactive, UserService, USER_PASSWORD } from '../../shared' +import { FormReactive, USER_PASSWORD, UserService } from '../../../shared' @Component({ selector: 'my-account-change-password', - templateUrl: './account-change-password.component.html' + templateUrl: './account-change-password.component.html', + styleUrls: [ './account-change-password.component.scss' ] }) - export class AccountChangePasswordComponent extends FormReactive implements OnInit { error: string = null diff --git a/client/src/app/account/account-change-password/index.ts b/client/src/app/account/account-settings/account-change-password/index.ts similarity index 100% rename from client/src/app/account/account-change-password/index.ts rename to 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 @@ +
{{ error }}
+ +
+ + +
+ {{ formErrors['displayNSFW'] }} +
+ + +
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 @@ +label { + font-size: 15px; + font-weight: $font-regular; + margin-left: 5px; +} + +input[type=submit] { + @include peertube-button; + @include orange-button; + + display: block; + margin-top: 15px; +} 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 similarity index 78% rename from client/src/app/account/account-details/account-details.component.ts rename to 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 @@ -import { Component, OnInit, Input } from '@angular/core' +import { Component, Input, OnInit } from '@angular/core' import { FormBuilder, FormGroup } from '@angular/forms' -import { Router } from '@angular/router' - import { NotificationsService } from 'angular2-notifications' - -import { AuthService } from '../../core' -import { - FormReactive, - User, - UserService, - USER_PASSWORD -} from '../../shared' -import { UserUpdateMe } from '../../../../../shared' +import { UserUpdateMe } from '../../../../../../shared' +import { AuthService } from '../../../core' +import { FormReactive, User, UserService } from '../../../shared' @Component({ selector: 'my-account-details', - templateUrl: './account-details.component.html' + templateUrl: './account-details.component.html', + styleUrls: [ './account-details.component.scss' ] }) 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 similarity index 100% rename from client/src/app/account/account-details/index.ts rename to 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 @@ +
+ Avatar + + +
+ + + + + + + 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 @@ +.user { + display: flex; + + img { + @include avatar(50px); + margin-right: 15px; + } + + .user-info { + .user-info-username { + font-size: 20px; + font-weight: $font-bold; + } + + .user-info-followers { + font-size: 15px; + } + } +} + +.account-title { + text-transform: uppercase; + color: $orange-color; + font-weight: $font-bold; + font-size: 13px; + margin-top: 55px; + margin-bottom: 30px; +} 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 @@ +import { Component, OnInit } from '@angular/core' +import { User } from '../../shared' +import { AuthService } from '../../core' + +@Component({ + selector: 'my-account-settings', + templateUrl: './account-settings.component.html', + styleUrls: [ './account-settings.component.scss' ] +}) +export class AccountSettingsComponent implements OnInit { + user: User = null + + constructor (private authService: AuthService) {} + + ngOnInit () { + this.user = this.authService.getUser() + } + + getAvatarPath () { + return this.user.getAvatarPath() + } +} 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 @@ +
+
+ + + + +
+
{{ video.name }}
+ {{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views +
+ + +
+
+ + Cancel + + + + + Delete + +
+
+ +
+ + + +
+
+
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 @@ +.action-selection-mode { + width: 174px; + display: flex; + justify-content: flex-end; + + .action-selection-mode-child { + position: fixed; + + .action-button { + display: inline-block; + } + + .action-button-cancel-selection { + @include peertube-button; + @include grey-button; + + margin-right: 10px; + } + + .action-button-delete-selection { + @include peertube-button; + @include orange-button; + } + + .icon.icon-delete-white { + @include icon(21px); + + position: relative; + top: -2px; + background-image: url('../../../assets/images/global/delete-white.svg'); + } + } +} + +/deep/ .action-button { + &.action-button-delete { + margin-right: 10px; + } +} + +.video { + display: flex; + height: 130px; + padding-bottom: 20px; + + input[type=checkbox] { + margin-right: 20px; + outline: 0; + } + + &:first-child { + margin-top: 47px; + } + + &:not(:last-child) { + margin-bottom: 20px; + border-bottom: 1px solid #C6C6C6; + } + + my-video-thumbnail { + margin-right: 10px; + } + + .video-info { + flex-grow: 1; + + .video-info-name { + font-size: 16px; + font-weight: $font-semibold; + } + + .video-info-date-views { + font-size: 13px; + } + } +} + +@media screen and (max-width: 800px) { + .video { + flex-direction: column; + height: auto; + text-align: center; + + input[type=checkbox] { + display: none; + } + + my-video-thumbnail { + margin-right: 0; + } + + .video-buttons { + margin-top: 10px; + } + } +} 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 @@ +import { Component, OnInit } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { NotificationsService } from 'angular2-notifications' +import 'rxjs/add/observable/from' +import 'rxjs/add/operator/concatAll' +import { Observable } from 'rxjs/Observable' +import { ConfirmService } from '../../core/confirm' +import { AbstractVideoList } from '../../shared/video/abstract-video-list' +import { Video } from '../../shared/video/video.model' +import { VideoService } from '../../shared/video/video.service' + +@Component({ + selector: 'my-account-videos', + templateUrl: './account-videos.component.html', + styleUrls: [ './account-videos.component.scss' ] +}) +export class AccountVideosComponent extends AbstractVideoList implements OnInit { + titlePage = 'My videos' + currentRoute = '/account/videos' + checkedVideos: { [ id: number ]: boolean } = {} + + constructor (protected router: Router, + protected route: ActivatedRoute, + protected notificationsService: NotificationsService, + protected confirmService: ConfirmService, + private videoService: VideoService) { + super() + } + + ngOnInit () { + super.ngOnInit() + } + + abortSelectionMode () { + this.checkedVideos = {} + } + + isInSelectionMode () { + return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true) + } + + getVideosObservable () { + return this.videoService.getMyVideos(this.pagination, this.sort) + } + + deleteSelectedVideos () { + const toDeleteVideosIds = Object.keys(this.checkedVideos) + .filter(k => this.checkedVideos[k] === true) + .map(k => parseInt(k, 10)) + + this.confirmService.confirm(`Do you really want to delete ${toDeleteVideosIds.length} videos?`, 'Delete').subscribe( + res => { + if (res === false) return + + const observables: Observable[] = [] + for (const videoId of toDeleteVideosIds) { + const o = this.videoService + .removeVideo(videoId) + .do(() => this.spliceVideosById(videoId)) + + observables.push(o) + } + + Observable.from(observables) + .concatAll() + .subscribe( + res => this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`), + + err => this.notificationsService.error('Error', err.text) + ) + } + ) + } + + deleteVideo (video: Video) { + this.confirmService.confirm(`Do you really want to delete ${video.name}?`, 'Delete').subscribe( + res => { + if (res === false) return + + this.videoService.removeVideo(video.id) + .subscribe( + status => { + this.notificationsService.success('Success', `Video ${video.name} deleted.`) + this.spliceVideosById(video.id) + }, + + error => this.notificationsService.error('Error', error.text) + ) + } + ) + } + + private spliceVideosById (id: number) { + const index = this.videos.findIndex(v => v.id === id) + this.videos.splice(index, 1) + } +} 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 @@
-
-

Account

+ - -
-
-
Update my informations
- -
- -
-
-
+
+
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 @@ -.panel { - margin-top: 40px; -} 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 @@ -import { Component, OnInit } from '@angular/core' -import { FormBuilder, FormGroup } from '@angular/forms' -import { Router } from '@angular/router' - -import { NotificationsService } from 'angular2-notifications' - -import { AuthService } from '../core' -import { - FormReactive, - User, - UserService, - USER_PASSWORD -} from '../shared' +import { Component } from '@angular/core' @Component({ selector: 'my-account', templateUrl: './account.component.html', styleUrls: [ './account.component.scss' ] }) -export class AccountComponent implements OnInit { - user: User = null - - constructor (private authService: AuthService) {} - - ngOnInit () { - this.user = this.authService.getUser() - } -} +export class AccountComponent {} 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 @@ import { NgModule } from '@angular/core' - -import { AccountRoutingModule } from './account-routing.module' -import { AccountComponent } from './account.component' -import { AccountChangePasswordComponent } from './account-change-password' -import { AccountDetailsComponent } from './account-details' -import { AccountService } from './account.service' import { SharedModule } from '../shared' +import { AccountRoutingModule } from './account-routing.module' +import { AccountChangePasswordComponent } from './account-settings/account-change-password/account-change-password.component' +import { AccountDetailsComponent } from './account-settings/account-details/account-details.component' +import { AccountSettingsComponent } from './account-settings/account-settings.component' +import { AccountComponent } from './account.component' +import { AccountService } from './account.service' +import { AccountVideosComponent } from './account-videos/account-videos.component' @NgModule({ imports: [ @@ -15,8 +16,10 @@ import { SharedModule } from '../shared' declarations: [ AccountComponent, + AccountSettingsComponent, AccountChangePasswordComponent, - AccountDetailsComponent + AccountDetailsComponent, + AccountVideosComponent ], 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' const routes: Routes = [ { path: '', - redirectTo: '/videos/list', + redirectTo: '/videos/trending', pathMatch: 'full' }, { 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 @@ -
-
+
+
-
-
- -
+ - -
- - - +
+ +
-
-
- - +
+
+
- -
-
- -
+
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 @@ min-height: calc(100vh - #{$header-height} - #{$footer-height} - #{$footer-margin}); } +.sub-header-container { + margin-top: $header-height; +} + .title-menu-left { position: fixed; height: calc(100vh - #{$header-height}); padding: 0; + width: $menu-width; .title-menu-left-block.menu { height: 100%; @@ -14,125 +19,62 @@ .header { height: $header-height; - - .fake-title-block { - display: inline-block; - } + position: fixed; + top: 0; + width: 100%; + background-color: #fff; + z-index: 1000; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.16); + display: flex; .top-left-block { - z-index: 100; - background-color: #fff; - border-right: 1px solid $header-border-color; + width: $menu-width; + z-index: 1001; height: $header-height; - line-height: $header-height; - margin-top: 0; - margin-bottom: 0; display: flex; - position: fixed; - padding: 0; + align-items: center; - &.border-bottom { - border-bottom: 1px solid $header-border-color; - } + .icon { + @include icon(22px); - .hamburger-block { - margin-right: 15px; - margin-left: 15px; - - .glyphicon { - cursor: pointer; - position: relative; - top: 4px; + &.icon-menu { + background-image: url('../assets/images/header/menu.svg'); + margin: 0 18px 0 24px; } } #peertube-title { - a { - color: inherit !important; - display: block; - background: url('../assets/logo.png') no-repeat; - background-size: contain; - background-position: center; - height: 100%; - margin: auto; - width: 135px; + font-size: 20px; + font-weight: $font-bold; + color: inherit !important; + display: flex; + align-items: center; - &:hover { - color: inherit !important; - text-decoration: none !important; - } + @include disable-default-a-behaviour; + + .icon.icon-logo { + display: inline-block; + background: url('../assets/images/logo.svg') no-repeat; + width: 23px; + height: 24px; } } @media screen and (max-width: 500px) { + width: 70px; + #peertube-title { display: none; } - - .hamburger-block { - width: 100%; - text-align: center; - } - } - - @media screen and (min-width: 500px) and (max-width: 600px) { - #peertube-title a { - width: 80px; - } - } - - @media screen and (min-width: 600px) and (max-width: 700px) { - #peertube-title a { - width: 100px; - } - } - - @media screen and (min-width: 1000px) { - #peertube-title a { - width: 120px; - } - } - - @media screen and (min-width: 1000px) { - #peertube-title a { - width: 120px; - } - } - - @media screen and (min-width: 1200px) { - padding-left: 15px; - - .hamburger-block { - margin-right: 15px; - } - - #peertube-title a { - width: 135px; - } - } - - @media screen and (min-width: 1600px) { - .hamburger-block { - margin-right: 20px; - } - - #peertube-title a { - width: 180px; - } } } - my-search { - position: fixed; - z-index: 1000; - // Fix col-md-* padding - padding: 0; - } - - .search-col { - height: 100%; - margin-left: -15px; - padding: 0; + .header-right { + height: $header-height; + display: flex; + align-items: center; + flex-grow: 1; + justify-content: flex-end; } } 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 @@ import { Component, OnInit } from '@angular/core' import { Router } from '@angular/router' - import { AuthService, ServerService } from './core' -import { UserService } from './shared' @Component({ selector: 'my-app', @@ -62,20 +60,9 @@ export class AppComponent implements OnInit { } getMainColClasses () { - const colSizes = { - md: 10, - sm: 9, - xs: 9 - } - // Take all width is the menu is not displayed - if (this.isMenuDisplayed === false) { - Object.keys(colSizes).forEach(col => colSizes[col] = 12) - } + if (this.isMenuDisplayed === false) return [ 'expanded' ] - const classes = [] - Object.keys(colSizes).forEach(col => classes.push(`col-${col}-${colSizes[col]}`)) - - return classes + return [] } } 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' import { SignupModule } from './signup' import { SharedModule } from './shared' import { VideosModule } from './videos' +import { MenuComponent } from './menu' +import { HeaderComponent } from './header' export function metaFactory (): MetaLoader { return new MetaStaticLoader({ @@ -47,7 +49,10 @@ const APP_PROVIDERS = [ @NgModule({ bootstrap: [ AppComponent ], declarations: [ - AppComponent + AppComponent, + + MenuComponent, + HeaderComponent ], imports: [ 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,29 +1,24 @@ +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' import { Injectable } from '@angular/core' import { Router } from '@angular/router' -import { Observable } from 'rxjs/Observable' -import { Subject } from 'rxjs/Subject' -import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' -import { ReplaySubject } from 'rxjs/ReplaySubject' + +import { NotificationsService } from 'angular2-notifications' +import 'rxjs/add/observable/throw' import 'rxjs/add/operator/do' import 'rxjs/add/operator/map' import 'rxjs/add/operator/mergeMap' -import 'rxjs/add/observable/throw' - -import { NotificationsService } from 'angular2-notifications' +import { Observable } from 'rxjs/Observable' +import { ReplaySubject } from 'rxjs/ReplaySubject' +import { Subject } from 'rxjs/Subject' +import { OAuthClientLocal, User as UserServerModel, UserRefreshToken, UserRole, VideoChannel } from '../../../../../shared' +import { Account } from '../../../../../shared/models/accounts' +import { UserLogin } from '../../../../../shared/models/users/user-login.model' +// Do not use the barrel (dependency loop) +import { RestExtractor } from '../../shared/rest' +import { UserConstructorHash } from '../../shared/users/user.model' import { AuthStatus } from './auth-status.model' import { AuthUser } from './auth-user.model' -import { - OAuthClientLocal, - UserRole, - UserRefreshToken, - VideoChannel, - User as UserServerModel -} from '../../../../../shared' -// Do not use the barrel (dependency loop) -import { RestExtractor } from '../../shared/rest' -import { UserLogin } from '../../../../../shared/models/users/user-login.model' -import { UserConstructorHash } from '../../shared/users/user.model' interface UserLoginWithUsername extends UserLogin { access_token: string @@ -42,10 +37,7 @@ interface UserLoginWithUserInformation extends UserLogin { displayNSFW: boolean email: string videoQuota: number - account: { - id: number - uuid: string - } + account: Account videoChannels: VideoChannel[] } @@ -177,19 +169,15 @@ export class AuthService { return this.http.post(AuthService.BASE_TOKEN_URL, body, { headers }) .map(res => this.handleRefreshToken(res)) - .catch(res => { - // The refresh token is invalid? - if (res.status === 400 && res.error.error === 'invalid_grant') { - console.error('Cannot refresh token -> logout...') - this.logout() - this.router.navigate(['/login']) + .catch(err => { + console.error(err) + console.log('Cannot refresh token -> logout...') + this.logout() + this.router.navigate(['/login']) - return Observable.throw({ - error: 'You need to reconnect.' - }) - } - - return this.restExtractor.handleError(res) + return Observable.throw({ + error: 'You need to reconnect.' + }) }) } @@ -202,7 +190,6 @@ export class AuthService { } this.mergeUserInformation(obj) - .do(() => this.userInformationLoaded.next(true)) .subscribe( res => { this.user.displayNSFW = res.displayNSFW @@ -211,6 +198,8 @@ export class AuthService { this.user.account = res.account this.user.save() + + this.userInformationLoaded.next(true) } ) } 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 @@ - +

{{ title }}

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 { @Component({ selector: 'my-confirm', - templateUrl: './confirm.component.html' + templateUrl: './confirm.component.html', + styles: [ '.button { padding: 0 13px; }' ] }) export class ConfirmComponent implements OnInit { @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' ], declarations: [ - ConfirmComponent, - MenuComponent, - MenuAdminComponent + ConfirmComponent ], exports: [ SimpleNotificationsModule, - ConfirmComponent, - MenuComponent, - MenuAdminComponent + ConfirmComponent ], 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 @@ export * from './auth' export * from './server' export * from './confirm' -export * from './menu' export * from './routing' 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 @@ -export * from './menu.component' -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 @@ - - - - - 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 @@ -import { Component } from '@angular/core' - -import { AuthService } from '../auth/auth.service' -import { UserRight } from '../../../../../shared' - -@Component({ - selector: 'my-menu-admin', - templateUrl: './menu-admin.component.html', - styleUrls: [ './menu.component.scss' ] -}) -export class MenuAdminComponent { - constructor (private auth: AuthService) {} - - hasUsersRight () { - return this.auth.getUser().hasRight(UserRight.MANAGE_USERS) - } - - hasServerFollowRight () { - return this.auth.getUser().hasRight(UserRight.MANAGE_SERVER_FOLLOW) - } - - hasVideoAbusesRight () { - return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES) - } - - hasVideoBlacklistRight () { - return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) - } - - hasJobsRight () { - return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS) - } -} 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 @@ - - - - - - - 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 @@ -menu { - background-color: $black-background; - padding: 15px; - margin: 0; - height: 100%; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - z-index: 1000; - - @media screen and (max-width: 550px) { - font-size: 90%; - } - - @media screen and (min-width: 1200px) { - padding: 25px; - } - - .panel-block { - margin-bottom: 15px; - } - - .block-title { - text-transform: uppercase; - font-weight: bold; - color: $menu-color-block; - margin-bottom: 10px; - } - - a { - display: block; - margin-left: 5px; - height: 30px; - color: $menu-color-link; - cursor: pointer; - transition: color 0.3s; - - &:hover, &:focus { - text-decoration: none !important; - outline: none !important; - } - - .glyphicon { - margin-right: 15px; - } - - &:hover, &.active { - color: #fff; - } - } -} 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 @@ -import { Injectable } from '@angular/core' import { HttpClient } from '@angular/common/http' +import { Injectable } from '@angular/core' +import 'rxjs/add/operator/do' +import { ReplaySubject } from 'rxjs/ReplaySubject' import { ServerConfig } from '../../../../../shared' @@ -8,6 +10,11 @@ export class ServerService { private static BASE_CONFIG_URL = API_URL + '/api/v1/config/' private static BASE_VIDEO_URL = API_URL + '/api/v1/videos/' + videoPrivaciesLoaded = new ReplaySubject(1) + videoCategoriesLoaded = new ReplaySubject(1) + videoLicencesLoaded = new ReplaySubject(1) + videoLanguagesLoaded = new ReplaySubject(1) + private config: ServerConfig = { signup: { allowed: false @@ -29,19 +36,19 @@ export class ServerService { } loadVideoCategories () { - return this.loadVideoAttributeEnum('categories', this.videoCategories) + return this.loadVideoAttributeEnum('categories', this.videoCategories, this.videoCategoriesLoaded) } loadVideoLicences () { - return this.loadVideoAttributeEnum('licences', this.videoLicences) + return this.loadVideoAttributeEnum('licences', this.videoLicences, this.videoLicencesLoaded) } loadVideoLanguages () { - return this.loadVideoAttributeEnum('languages', this.videoLanguages) + return this.loadVideoAttributeEnum('languages', this.videoLanguages, this.videoLanguagesLoaded) } loadVideoPrivacies () { - return this.loadVideoAttributeEnum('privacies', this.videoPrivacies) + return this.loadVideoAttributeEnum('privacies', this.videoPrivacies, this.videoPrivaciesLoaded) } getConfig () { @@ -66,17 +73,20 @@ export class ServerService { private loadVideoAttributeEnum ( attributeName: 'categories' | 'licences' | 'languages' | 'privacies', - hashToPopulate: { id: number, label: string }[] + hashToPopulate: { id: number, label: string }[], + notifier: ReplaySubject ) { return this.http.get(ServerService.BASE_VIDEO_URL + attributeName) - .subscribe(data => { - Object.keys(data) - .forEach(dataKey => { - hashToPopulate.push({ - id: parseInt(dataKey, 10), - label: data[dataKey] - }) - }) + .subscribe(data => { + Object.keys(data) + .forEach(dataKey => { + hashToPopulate.push({ + id: parseInt(dataKey, 10), + label: data[dataKey] + }) }) + + notifier.next(true) + }) } } 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 @@ + + + + + + Upload + 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 @@ +#search-video { + @include peertube-input-text($search-input-width); + margin-right: 15px; + padding-right: 25px; // For the search icon + + &::placeholder { + color: #000; + } + + @media screen and (max-width: 600px) { + width: calc(100% - 150px); + } + + @media screen and (max-width: 400px) { + width: calc(100% - 70px); + } +} + +.icon.icon-search { + @include icon(25px); + height: 21px; + + background-image: url('../../assets/images/header/search.svg'); + + // yolo + position: absolute; + margin-left: -50px; + margin-top: 5px; +} + +.upload-button { + @include peertube-button-link; + @include orange-button; + + margin-right: 25px; + + .icon.icon-upload { + @include icon(22px); + + background-image: url('../../assets/images/header/upload.svg'); + height: 24px; + vertical-align: middle; + margin-right: 6px; + } + + @media screen and (max-width: 400px) { + margin-right: 10px; + padding: 0 10px; + + .icon.icon-upload { + margin-right: 0; + } + + .upload-button-label { + display: none; + } + } +} 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 @@ +import { Component, OnInit } from '@angular/core' +import { Router } from '@angular/router' +import { getParameterByName } from '../shared/misc/utils' + +@Component({ + selector: 'my-header', + templateUrl: './header.component.html', + styleUrls: [ './header.component.scss' ] +}) + +export class HeaderComponent implements OnInit { + searchValue = '' + + constructor (private router: Router) {} + + ngOnInit () { + const searchQuery = getParameterByName('search', window.location.href) + if (searchQuery) this.searchValue = searchQuery + } + + doSearch () { + if (!this.searchValue) return + + this.router.navigate([ '/videos', 'search' ], { + queryParams: { search: this.searchValue } + }) + } +} 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 @@ -
-
- -

Login

- -
{{ error }}
- -
-
- - -
- {{ formErrors.username }} -
-
- -
- - -
- {{ formErrors.password }} -
-
- - -
+
+
+ Login
+ +
{{ error }}
+ +
+
+ + +
+ {{ formErrors.username }} +
+
+ +
+ + +
+ {{ formErrors.password }} +
+
+ + +
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 @@ +input:not([type=submit]) { + @include peertube-input-text(340px); + display: block; +} + +input[type=submit] { + @include peertube-button; + @include orange-button; +} 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' @Component({ selector: 'my-login', - templateUrl: './login.component.html' + templateUrl: './login.component.html', + styleUrls: [ './login.component.scss' ] }) 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 @@ + +
+ Avatar + +
+ {{ user.username }} +
{{ user.email }}
+
+ +
+ + + +
+
+ + + + + + +
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 @@ +menu { + background-color: $black-background; + margin: 0; + padding: 0; + height: 100%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + z-index: 1000; + color: $menu-color; + + .logged-in-block { + height: 100px; + background-color: rgba(255, 255, 255, 0.15); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 35px; + + img { + margin-left: 20px; + margin-right: 10px; + + @include avatar(34px); + } + + .logged-in-info { + flex-grow: 1; + + .logged-in-username { + font-size: 16px; + font-weight: $font-semibold; + color: $menu-color; + cursor: pointer; + + @include disable-default-a-behaviour; + } + + .logged-in-email { + font-size: 13px; + color: #C6C6C6; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 140px; + } + } + + .logged-in-more { + margin-right: 20px; + + .glyphicon { + cursor: pointer; + font-size: 18px; + } + } + } + + .button-block { + margin: 30px 25px 35px 25px; + + .login-button, .create-account-button { + font-weight: $font-semibold; + font-size: 15px; + height: $button-height; + line-height: $button-height; + width: 100%; + border-radius: 3px; + text-align: center; + color: $menu-color; + display: block; + cursor: pointer; + margin-bottom: 15px; + + @include disable-default-a-behaviour; + + &.login-button { + background-color: $orange-color; + margin-bottom: 10px; + } + + &.create-account-button { + background-color: rgba(255, 255, 255, 0.25); + } + } + } + + .block-title { + text-transform: uppercase; + font-weight: $font-bold; // Bold + font-size: 13px; + margin-bottom: 25px; + } + + .panel-block { + margin-bottom: 45px; + margin-left: 26px; + + a { + display: flex; + color: $menu-color; + cursor: pointer; + height: 22px; + line-height: 22px; + font-size: 16px; + margin-bottom: 15px; + @include disable-default-a-behaviour; + + .icon { + @include icon(22px); + + margin-right: 18px; + + &.icon-videos-trending { + position: relative; + top: -2px; + background-image: url('../../assets/images/menu/trending.svg'); + } + + &.icon-videos-recently-added { + width: 23px; + height: 23px; + position: relative; + top: -1px; + background-image: url('../../assets/images/menu/recently-added.svg'); + } + + &.icon-administration { + width: 23px; + height: 23px; + + background-image: url('../../assets/images/menu/administration.svg'); + } + } + } + } +} + +@media screen and (max-width: 800px) { + menu { + .logged-in-block { + padding-left: 10px; + + img { + display: none; + } + + .logged-in-info { + .logged-in-username { + font-size: 14px; + } + + .logged-in-email { + font-size: 11px; + max-width: 120px; + } + } + + .logged-in-more { + margin-right: 5px; + + .login-button, .create-account-button { + font-weight: $font-semibold; + font-size: 15px; + height: $button-height; + line-height: $button-height; + width: 190px; + } + } + } + + .button-block { + margin: 20px 10px 25px 10px; + + .login-button, .create-account-button { + font-size: 13px; + } + } + + .panel-block { + margin-bottom: 30px; + margin-left: 10px; + + a { + font-size: 14px; + + .icon { + margin-right: 10px; + } + } + } + } +} diff --git a/client/src/app/core/menu/menu.component.ts b/client/src/app/menu/menu.component.ts similarity index 82% rename from client/src/app/core/menu/menu.component.ts rename to 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 @@ import { Component, OnInit } from '@angular/core' import { Router } from '@angular/router' - -import { AuthService, AuthStatus } from '../auth' -import { ServerService } from '../server' -import { UserRight } from '../../../../../shared/models/users/user-right.enum' +import { UserRight } from '../../../../shared/models/users/user-right.enum' +import { AuthService, AuthStatus, ServerService } from '../core' +import { User } from '../shared/users/user.model' @Component({ selector: 'my-menu', @@ -11,6 +10,7 @@ import { UserRight } from '../../../../../shared/models/users/user-right.enum' styleUrls: [ './menu.component.scss' ] }) export class MenuComponent implements OnInit { + user: User isLoggedIn: boolean userHasAdminAccess = false @@ -29,16 +29,19 @@ export class MenuComponent implements OnInit { ngOnInit () { this.isLoggedIn = this.authService.isLoggedIn() + if (this.isLoggedIn === true) this.user = this.authService.getUser() this.computeIsUserHasAdminAccess() this.authService.loginChangedSource.subscribe( status => { if (status === AuthStatus.LoggedIn) { this.isLoggedIn = true + this.user = this.authService.getUser() this.computeIsUserHasAdminAccess() console.log('Logged in.') } else if (status === AuthStatus.LoggedOut) { this.isLoggedIn = false + this.user = undefined this.computeIsUserHasAdminAccess() console.log('Logged out.') } else { @@ -48,6 +51,10 @@ export class MenuComponent implements OnInit { ) } + getUserAvatarPath () { + return this.user.getAvatarPath() + } + isRegistrationAllowed () { return this.serverService.getConfig().signup.allowed } @@ -78,7 +85,9 @@ export class MenuComponent implements OnInit { return this.routesPerRight[right] } - logout () { + logout (event: Event) { + event.preventDefault() + this.authService.logout() // Redirect to home page 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 @@ +import { Account as ServerAccount } from '../../../../../shared/models/accounts/account.model' +import { Avatar } from '../../../../../shared/models/avatars/avatar.model' + +export class Account implements ServerAccount { + id: number + uuid: string + name: string + host: string + followingCount: number + followersCount: number + createdAt: Date + updatedAt: Date + avatar: Avatar + + static GET_ACCOUNT_AVATAR_PATH (account: Account) { + if (account && account.avatar) return account.avatar.path + + return API_URL + '/client/assets/images/default-avatar.png' + } +} 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 @@ -import { FormControl } from '@angular/forms' - -export function validateHost (c: FormControl) { +export function validateHost (value: string) { // Thanks to http://stackoverflow.com/a/106223 const HOST_REGEXP = new RegExp( '^(([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])$' ) - return HOST_REGEXP.test(c.value) ? null : { - validateHost: { - valid: false - } - } + return HOST_REGEXP.test(value) } 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' export const VIDEO_ABUSE_REASON = { VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ], MESSAGES: { - 'required': 'Report reason name is required.', - 'minlength': 'Report reson must be at least 2 characters long.', - 'maxlength': 'Report reson cannot be more than 300 characters long.' + 'required': 'Report reason is required.', + 'minlength': 'Report reason must be at least 2 characters long.', + 'maxlength': 'Report reason cannot be more than 300 characters long.' } } 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 @@ import { Validators } from '@angular/forms' +export type ValidatorMessage = { + [ id: string ]: { + [ error: string ]: string + } +} + export const VIDEO_NAME = { VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(120) ], MESSAGES: { @@ -17,17 +23,13 @@ export const VIDEO_PRIVACY = { } export const VIDEO_CATEGORY = { - VALIDATORS: [ Validators.required ], - MESSAGES: { - 'required': 'Video category is required.' - } + VALIDATORS: [ ], + MESSAGES: {} } export const VIDEO_LICENCE = { - VALIDATORS: [ Validators.required ], - MESSAGES: { - 'required': 'Video licence is required.' - } + VALIDATORS: [ ], + MESSAGES: {} } export const VIDEO_LANGUAGE = { @@ -43,9 +45,8 @@ export const VIDEO_CHANNEL = { } export const VIDEO_DESCRIPTION = { - VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(3000) ], + VALIDATORS: [ Validators.minLength(3), Validators.maxLength(3000) ], MESSAGES: { - 'required': 'Video description is required.', 'minlength': 'Video description must be at least 3 characters long.', 'maxlength': 'Video description cannot be more than 3000 characters long.' } @@ -58,10 +59,3 @@ export const VIDEO_TAGS = { 'maxlength': 'A tag should be less than 30 characters long.' } } - -export const VIDEO_FILE = { - VALIDATORS: [ Validators.required ], - MESSAGES: { - 'required': 'Video file is required.' - } -} 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 @@ export * from './auth' export * from './forms' export * from './rest' -export * from './search' export * from './users' export * from './video-abuse' 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 @@ +.action-button { + @include peertube-button-link; + + font-size: 15px; + font-weight: $font-semibold; + color: #585858; + background-color: #E5E5E5; + + &:hover { + background-color: #EFEFEF; + } + + .icon { + @include icon(21px); + + position: relative; + top: -2px; + + &.icon-edit { + background-image: url('../../../assets/images/global/edit.svg'); + } + + &.icon-delete-grey { + background-image: url('../../../assets/images/global/delete-grey.svg'); + } + } +} 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 @@ + + + Delete + 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 @@ +import { Component } from '@angular/core' + +@Component({ + selector: 'my-delete-button', + styleUrls: [ './button.component.scss' ], + templateUrl: './delete-button.component.html' +}) + +export class DeleteButtonComponent { +} 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 @@ + + + Edit + 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 @@ +import { Component, Input } from '@angular/core' + +@Component({ + selector: 'my-edit-button', + styleUrls: [ './button.component.scss' ], + templateUrl: './edit-button.component.html' +}) + +export class EditButtonComponent { + @Input() routerLink = [] +} 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 @@ +import { Pipe, PipeTransform } from '@angular/core' + +// Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site +@Pipe({ name: 'myFromNow' }) +export class FromNowPipe implements PipeTransform { + + transform (value: number) { + const seconds = Math.floor((Date.now() - value) / 1000) + + let interval = Math.floor(seconds / 31536000) + if (interval > 1) { + return interval + ' years ago' + } + + interval = Math.floor(seconds / 2592000) + if (interval > 1) return interval + ' months ago' + if (interval === 1) return interval + ' month ago' + + interval = Math.floor(seconds / 604800) + if (interval > 1) return interval + ' weeks ago' + if (interval === 1) return interval + ' week ago' + + interval = Math.floor(seconds / 86400) + if (interval > 1) return interval + ' days ago' + if (interval === 1) return interval + ' day ago' + + interval = Math.floor(seconds / 3600) + if (interval > 1) return interval + ' hours ago' + if (interval === 1) return interval + ' hour ago' + + interval = Math.floor(seconds / 60) + if (interval >= 1) return interval + ' min ago' + + return Math.floor(seconds) + ' sec ago' + } +} 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 @@ +import { Pipe, PipeTransform } from '@angular/core' + +// Thanks: https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts + +@Pipe({ name: 'myNumberFormatter' }) +export class NumberFormatterPipe implements PipeTransform { + private dictionary: Array<{max: number, type: string}> = [ + { max: 1000, type: '' }, + { max: 1000000, type: 'K' }, + { max: 1000000000, type: 'M' } + ] + + transform (value: number) { + const format = this.dictionary.find(d => value < d.max) || this.dictionary[this.dictionary.length - 1] + const calc = Math.floor(value / (format.max / 1000)) + + return `${calc}${format.type}` + } +} 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 @@ +// Thanks: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript + +function getParameterByName (name: string, url: string) { + if (!url) url = window.location.href + name = name.replace(/[\[\]]/g, '\\$&') + + const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)') + const results = regex.exec(url) + + if (!results) return null + if (!results[2]) return '' + + return decodeURIComponent(results[2].replace(/\+/g, ' ')) +} + +function viewportHeight () { + return Math.max(document.documentElement.clientHeight, window.innerHeight || 0) +} + +export { + viewportHeight, + getParameterByName +} 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 @@ -export * from './search-field.type' -export * from './search.component' -export * from './search.model' -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 @@ -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 @@ -
- - - - - -
- - -
-
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 @@ -.icon-addon { - background-color: #fff; - border-radius: 0; - border-color: $header-border-color; - border-width: 0 0 1px 0; - text-align: right; - - .glyphicon-search { - width: 30px; - font-size: 20px; - } -} - -input, button, .input-group { - height: 100%; -} - -input, .input-group-btn { - border-radius: 0; - border-top: none; - border-left: none; -} - -input { - height: $header-height; - border-right: none; - font-weight: bold; - box-shadow: none; - - &, &:focus { - border-bottom: 1px solid $header-border-color !important; - outline: none !important; - box-shadow: none !important; - } -} - -button { - - &, &:hover, &:focus, &:active, &:visited { - background-color: #fff !important; - border-color: $header-border-color !important; - color: #858585 !important; - outline: none !important; - - height: $header-height; - border-width: 0 0 1px 0; - font-weight: bold; - text-decoration: none; - box-shadow: none; - } -} 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 @@ -import { Component, OnInit } from '@angular/core' -import { Router } from '@angular/router' - -import { Search } from './search.model' -import { SearchField } from './search-field.type' -import { SearchService } from './search.service' - -@Component({ - selector: 'my-search', - templateUrl: './search.component.html', - styleUrls: [ './search.component.scss' ] -}) - -export class SearchComponent implements OnInit { - fieldChoices = { - name: 'Name', - account: 'Account', - host: 'Host', - tags: 'Tags' - } - searchCriteria: Search = { - field: 'name', - value: '' - } - - constructor (private searchService: SearchService, private router: Router) {} - - ngOnInit () { - // Subscribe if the search changed - // Usually changed by videos list component - this.searchService.updateSearch.subscribe( - newSearchCriteria => { - // Put a field by default - if (!newSearchCriteria.field) { - newSearchCriteria.field = 'name' - } - - this.searchCriteria = newSearchCriteria - } - ) - } - - get choiceKeys () { - return Object.keys(this.fieldChoices) - } - - choose ($event: MouseEvent, choice: SearchField) { - $event.preventDefault() - $event.stopPropagation() - - this.searchCriteria.field = choice - - if (this.searchCriteria.value) { - this.doSearch() - } - } - - doSearch () { - if (this.router.url.indexOf('/videos/list') === -1) { - this.router.navigate([ '/videos/list' ]) - } - - this.searchService.searchUpdated.next(this.searchCriteria) - } - - getStringChoice (choiceKey: SearchField) { - return this.fieldChoices[choiceKey] - } -} 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 @@ -import { SearchField } from './search-field.type' - -export interface Search { - field: SearchField - value: string -} 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 @@ -import { Injectable } from '@angular/core' -import { Subject } from 'rxjs/Subject' -import { ReplaySubject } from 'rxjs/ReplaySubject' - -import { Search } from './search.model' - -// This class is needed to communicate between videos/ and search component -// Remove it when we'll be able to subscribe to router changes -@Injectable() -export class SearchService { - searchUpdated: Subject - updateSearch: Subject - - constructor () { - this.updateSearch = new Subject() - this.searchUpdated = new ReplaySubject(1) - } -} 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 @@ -import { NgModule } from '@angular/core' -import { HttpClientModule } from '@angular/common/http' import { CommonModule } from '@angular/common' +import { HttpClientModule } from '@angular/common/http' +import { NgModule } from '@angular/core' import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { RouterModule } from '@angular/router' -import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe' -import { KeysPipe } from 'angular-pipes/src/object/keys.pipe' import { BsDropdownModule } from 'ngx-bootstrap/dropdown' -import { ProgressbarModule } from 'ngx-bootstrap/progressbar' -import { PaginationModule } from 'ngx-bootstrap/pagination' import { ModalModule } from 'ngx-bootstrap/modal' -import { DataTableModule } from 'primeng/components/datatable/datatable' +import { InfiniteScrollModule } from 'ngx-infinite-scroll' +import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' +import { DataTableModule } from 'primeng/components/datatable/datatable' import { AUTH_INTERCEPTOR_PROVIDER } from './auth' +import { DeleteButtonComponent } from './misc/delete-button.component' +import { EditButtonComponent } from './misc/edit-button.component' +import { FromNowPipe } from './misc/from-now.pipe' +import { LoaderComponent } from './misc/loader.component' +import { NumberFormatterPipe } from './misc/number-formatter.pipe' import { RestExtractor, RestService } from './rest' -import { SearchComponent, SearchService } from './search' import { UserService } from './users' import { VideoAbuseService } from './video-abuse' import { VideoBlacklistService } from './video-blacklist' -import { LoaderComponent } from './misc/loader.component' +import { VideoMiniatureComponent } from './video/video-miniature.component' +import { VideoThumbnailComponent } from './video/video-thumbnail.component' +import { VideoService } from './video/video.service' @NgModule({ imports: [ @@ -31,18 +35,21 @@ import { LoaderComponent } from './misc/loader.component' BsDropdownModule.forRoot(), ModalModule.forRoot(), - PaginationModule.forRoot(), - ProgressbarModule.forRoot(), DataTableModule, - PrimeSharedModule + PrimeSharedModule, + InfiniteScrollModule, + NgPipesModule ], declarations: [ - BytesPipe, - KeysPipe, - SearchComponent, - LoaderComponent + LoaderComponent, + VideoThumbnailComponent, + VideoMiniatureComponent, + DeleteButtonComponent, + EditButtonComponent, + NumberFormatterPipe, + FromNowPipe ], exports: [ @@ -54,25 +61,30 @@ import { LoaderComponent } from './misc/loader.component' BsDropdownModule, ModalModule, - PaginationModule, - ProgressbarModule, DataTableModule, PrimeSharedModule, + InfiniteScrollModule, BytesPipe, KeysPipe, - SearchComponent, - LoaderComponent + LoaderComponent, + VideoThumbnailComponent, + VideoMiniatureComponent, + DeleteButtonComponent, + EditButtonComponent, + + NumberFormatterPipe, + FromNowPipe ], providers: [ AUTH_INTERCEPTOR_PROVIDER, RestExtractor, RestService, - SearchService, VideoAbuseService, VideoBlacklistService, - UserService + UserService, + VideoService ] }) 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 @@ -import { - User as UserServerModel, - UserRole, - VideoChannel, - UserRight, - hasUserRight -} from '../../../../../shared' +import { hasUserRight, User as UserServerModel, UserRight, UserRole, VideoChannel } from '../../../../../shared' +import { Account } from '../account/account.model' export type UserConstructorHash = { id: number, @@ -14,10 +9,7 @@ export type UserConstructorHash = { videoQuota?: number, displayNSFW?: boolean, createdAt?: Date, - account?: { - id: number - uuid: string - }, + account?: Account, videoChannels?: VideoChannel[] } export class User implements UserServerModel { @@ -27,10 +19,7 @@ export class User implements UserServerModel { role: UserRole displayNSFW: boolean videoQuota: number - account: { - id: number - uuid: string - } + account: Account videoChannels: VideoChannel[] createdAt: Date @@ -61,4 +50,8 @@ export class User implements UserServerModel { hasRight (right: UserRight) { return hasUserRight(this.role, right) } + + getAvatarPath () { + return Account.GET_ACCOUNT_AVATAR_PATH(this.account) + } } 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 @@ +
+
+ {{ titlePage }} +
+ +
+ + +
+
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 @@ +.videos { + text-align: center; + + my-video-miniature { + text-align: left; + } +} 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 @@ +import { OnInit } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { NotificationsService } from 'angular2-notifications' +import { Observable } from 'rxjs/Observable' +import { SortField } from './sort-field.type' +import { VideoPagination } from './video-pagination.model' +import { Video } from './video.model' + +export abstract class AbstractVideoList implements OnInit { + pagination: VideoPagination = { + currentPage: 1, + itemsPerPage: 25, + totalItems: null + } + sort: SortField = '-createdAt' + defaultSort: SortField = '-createdAt' + videos: Video[] = [] + loadOnInit = true + + protected notificationsService: NotificationsService + protected router: Router + protected route: ActivatedRoute + + protected abstract currentRoute: string + + abstract titlePage: string + private loadedPages: { [ id: number ]: boolean } = {} + + abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}> + + ngOnInit () { + // Subscribe to route changes + const routeParams = this.route.snapshot.params + this.loadRouteParams(routeParams) + + if (this.loadOnInit === true) this.loadMoreVideos('after') + } + + onNearOfTop () { + if (this.pagination.currentPage > 1) { + this.previousPage() + } + } + + onNearOfBottom () { + if (this.hasMoreVideos()) { + this.nextPage() + } + } + + reloadVideos () { + this.videos = [] + this.loadedPages = {} + this.loadMoreVideos('before') + } + + loadMoreVideos (where: 'before' | 'after') { + if (this.loadedPages[this.pagination.currentPage] === true) return + + const observable = this.getVideosObservable() + + observable.subscribe( + ({ videos, totalVideos }) => { + // Paging is too high, return to the first one + if (this.pagination.currentPage > 1 && totalVideos <= ((this.pagination.currentPage - 1) * this.pagination.itemsPerPage)) { + this.pagination.currentPage = 1 + this.setNewRouteParams() + return this.reloadVideos() + } + + this.loadedPages[this.pagination.currentPage] = true + this.pagination.totalItems = totalVideos + + if (where === 'before') { + this.videos = videos.concat(this.videos) + } else { + this.videos = this.videos.concat(videos) + } + }, + error => this.notificationsService.error('Error', error.text) + ) + } + + protected hasMoreVideos () { + // No results + if (this.pagination.totalItems === 0) return false + + // Not loaded yet + if (!this.pagination.totalItems) return true + + const maxPage = this.pagination.totalItems / this.pagination.itemsPerPage + return maxPage > this.pagination.currentPage + } + + protected previousPage () { + this.pagination.currentPage-- + + this.setNewRouteParams() + this.loadMoreVideos('before') + } + + protected nextPage () { + this.pagination.currentPage++ + + this.setNewRouteParams() + this.loadMoreVideos('after') + } + + protected buildRouteParams () { + // There is always a sort and a current page + const params = { + sort: this.sort, + page: this.pagination.currentPage + } + + return params + } + + protected loadRouteParams (routeParams: { [ key: string ]: any }) { + this.sort = routeParams['sort'] as SortField || this.defaultSort + + if (routeParams['page'] !== undefined) { + this.pagination.currentPage = parseInt(routeParams['page'], 10) + } else { + this.pagination.currentPage = 1 + } + } + + protected setNewRouteParams () { + const routeParams = this.buildRouteParams() + this.router.navigate([ this.currentRoute, routeParams ]) + } +} diff --git a/client/src/app/videos/shared/sort-field.type.ts b/client/src/app/shared/video/sort-field.type.ts similarity index 100% rename from client/src/app/videos/shared/sort-field.type.ts rename to 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 similarity index 79% rename from client/src/app/videos/shared/video-details.model.ts rename to 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 @@ -import { Video } from './video.model' +import { Account } from '../../../../../shared/models/accounts' +import { Video } from '../../shared/video/video.model' import { AuthUser } from '../../core' import { VideoDetails as VideoDetailsServerModel, @@ -10,7 +11,7 @@ import { } from '../../../../../shared' export class VideoDetails extends Video implements VideoDetailsServerModel { - account: string + accountName: string by: string createdAt: Date updatedAt: Date @@ -44,6 +45,9 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { channel: VideoChannel privacy: VideoPrivacy privacyLabel: string + account: Account + likesPercent: number + dislikesPercent: number constructor (hash: VideoDetailsServerModel) { super(hash) @@ -53,6 +57,10 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { this.descriptionPath = hash.descriptionPath this.files = hash.files this.channel = hash.channel + this.account = hash.account + + this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 + this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 } getAppropriateMagnetUri (actualDownloadSpeed = 0) { @@ -71,7 +79,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { } isRemovableBy (user: AuthUser) { - return user && this.isLocal === true && (this.account === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO)) + return user && this.isLocal === true && (this.accountName === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO)) } isBlackistableBy (user: AuthUser) { @@ -79,6 +87,6 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { } isUpdatableBy (user: AuthUser) { - return user && this.isLocal === true && user.username === this.account + return user && this.isLocal === true && user.username === this.accountName } } diff --git a/client/src/app/videos/shared/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts similarity index 59% rename from client/src/app/videos/shared/video-edit.model.ts rename to 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 { uuid?: string id?: number - constructor (videoDetails: VideoDetails) { - this.id = videoDetails.id - this.uuid = videoDetails.uuid - this.category = videoDetails.category - this.licence = videoDetails.licence - this.language = videoDetails.language - this.description = videoDetails.description - this.name = videoDetails.name - this.tags = videoDetails.tags - this.nsfw = videoDetails.nsfw - this.channel = videoDetails.channel.id - this.privacy = videoDetails.privacy + constructor (videoDetails?: VideoDetails) { + if (videoDetails) { + this.id = videoDetails.id + this.uuid = videoDetails.uuid + this.category = videoDetails.category + this.licence = videoDetails.licence + this.language = videoDetails.language + this.description = videoDetails.description + this.name = videoDetails.name + this.tags = videoDetails.tags + this.nsfw = videoDetails.nsfw + this.channel = videoDetails.channel.id + this.privacy = videoDetails.privacy + } } 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 @@ +
+ + +
+ + + {{ video.name }} + + + + {{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views + +
+
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 @@ +.video-miniature { + display: inline-block; + padding-right: 15px; + margin-bottom: 30px; + height: 175px; + vertical-align: top; + + .video-miniature-information { + width: 200px; + margin-top: 2px; + line-height: normal; + + .video-miniature-name { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: bold; + transition: color 0.2s; + font-size: 16px; + font-weight: $font-semibold; + color: #000; + + &:hover { + text-decoration: none; + } + + &.blur-filter { + filter: blur(3px); + padding-left: 4px; + } + } + + .video-miniature-created-at-views { + display: block; + font-size: 13px; + } + + .video-miniature-account { + font-size: 13px; + color: #585858; + } + } +} diff --git a/client/src/app/videos/video-list/shared/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts similarity index 75% rename from client/src/app/videos/video-list/shared/video-miniature.component.ts rename to 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 @@ import { Component, Input } from '@angular/core' - -import { SortField, Video } from '../../shared' -import { User } from '../../../shared' +import { User } from '../users' +import { Video } from './video.model' @Component({ selector: 'my-video-miniature', @@ -9,7 +8,6 @@ import { User } from '../../../shared' templateUrl: './video-miniature.component.html' }) export class VideoMiniatureComponent { - @Input() currentSort: SortField @Input() user: User @Input() video: Video diff --git a/client/src/app/videos/shared/video-pagination.model.ts b/client/src/app/shared/video/video-pagination.model.ts similarity index 78% rename from client/src/app/videos/shared/video-pagination.model.ts rename to 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 @@ export interface VideoPagination { currentPage: number itemsPerPage: number - totalItems: number + totalItems?: number } 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 @@ + +video thumbnail + +
+ {{ video.durationLabel }} +
+
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 @@ +.video-thumbnail { + display: inline-block; + position: relative; + border-radius: 4px; + overflow: hidden; + + &:hover { + text-decoration: none !important; + } + + img.blur-filter { + filter: blur(5px); + transform : scale(1.03); + } + + .video-thumbnail-overlay { + position: absolute; + right: 5px; + bottom: 5px; + display: inline-block; + background-color: rgba(0, 0, 0, 0.7); + color: #fff; + font-size: 12px; + font-weight: $font-bold; + border-radius: 3px; + padding: 0 5px; + } +} 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 @@ +import { Component, Input } from '@angular/core' +import { Video } from './video.model' + +@Component({ + selector: 'my-video-thumbnail', + styleUrls: [ './video-thumbnail.component.scss' ], + templateUrl: './video-thumbnail.component.html' +}) +export class VideoThumbnailComponent { + @Input() video: Video + @Input() nsfw = false +} diff --git a/client/src/app/videos/shared/video.model.ts b/client/src/app/shared/video/video.model.ts similarity index 91% rename from client/src/app/videos/shared/video.model.ts rename to 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 @@ import { Video as VideoServerModel } from '../../../../../shared' -import { User } from '../../shared' +import { User } from '../' +import { Account } from '../../../../../shared/models/accounts' export class Video implements VideoServerModel { - account: string + accountName: string by: string createdAt: Date updatedAt: Date @@ -31,6 +32,7 @@ export class Video implements VideoServerModel { likes: number dislikes: number nsfw: boolean + account: Account private static createByString (account: string, serverHost: string) { return account + '@' + serverHost @@ -52,7 +54,7 @@ export class Video implements VideoServerModel { absoluteAPIUrl = window.location.origin } - this.account = hash.account + this.accountName = hash.accountName this.createdAt = new Date(hash.createdAt.toString()) this.categoryLabel = hash.categoryLabel this.category = hash.category @@ -80,7 +82,7 @@ export class Video implements VideoServerModel { this.dislikes = hash.dislikes this.nsfw = hash.nsfw - this.by = Video.createByString(hash.account, hash.serverHost) + this.by = Video.createByString(hash.accountName, hash.serverHost) } isVideoNSFWForUser (user: User) { diff --git a/client/src/app/videos/shared/video.service.ts b/client/src/app/shared/video/video.service.ts similarity index 81% rename from client/src/app/videos/shared/video.service.ts rename to 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 @@ -import { Injectable } from '@angular/core' -import { Observable } from 'rxjs/Observable' import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' +import { Injectable } from '@angular/core' import 'rxjs/add/operator/catch' import 'rxjs/add/operator/map' - +import { Observable } from 'rxjs/Observable' +import { Video as VideoServerModel, VideoDetails as VideoDetailsServerModel } from '../../../../../shared' +import { ResultList } from '../../../../../shared/models/result-list.model' +import { UserVideoRateUpdate } from '../../../../../shared/models/videos/user-video-rate-update.model' +import { UserVideoRate } from '../../../../../shared/models/videos/user-video-rate.model' +import { VideoRateType } from '../../../../../shared/models/videos/video-rate.type' +import { VideoUpdate } from '../../../../../shared/models/videos/video-update.model' +import { RestExtractor } from '../rest/rest-extractor.service' +import { RestService } from '../rest/rest.service' +import { Search } from '../header/search.model' +import { UserService } from '../users/user.service' import { SortField } from './sort-field.type' -import { - RestExtractor, - RestService, - UserService, - Search -} from '../../shared' -import { Video } from './video.model' import { VideoDetails } from './video-details.model' import { VideoEdit } from './video-edit.model' import { VideoPagination } from './video-pagination.model' -import { - UserVideoRate, - VideoRateType, - VideoUpdate, - UserVideoRateUpdate, - Video as VideoServerModel, - VideoDetails as VideoDetailsServerModel, - ResultList -} from '../../../../../shared' +import { Video } from './video.model' @Injectable() export class VideoService { @@ -48,14 +42,17 @@ export class VideoService { } updateVideo (video: VideoEdit) { - const language = video.language ? video.language : null + const language = video.language || undefined + const licence = video.licence || undefined + const category = video.category || undefined + const description = video.description || undefined const body: VideoUpdate = { name: video.name, - category: video.category, - licence: video.licence, + category, + licence, language, - description: video.description, + description, privacy: video.privacy, tags: video.tags, nsfw: video.nsfw @@ -97,15 +94,14 @@ export class VideoService { .catch((res) => this.restExtractor.handleError(res)) } - searchVideos (search: Search, videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { - const url = VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value) + searchVideos (search: string, videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { + const url = VideoService.BASE_VIDEO_URL + 'search' const pagination = this.videoPaginationToRestPagination(videoPagination) let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination, sort) - - if (search.field) params.set('field', search.field) + params = params.append('search', search) return this.authHttp .get>(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 @@ -
-
+
-

Signup

+
+ Create an account +
{{ error }}
@@ -10,9 +11,9 @@ -
+
{{ formErrors.username }}
@@ -21,9 +22,9 @@ -
+
{{ formErrors.email }}
@@ -32,15 +33,14 @@ -
+
{{ formErrors.password }}
- + -
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 @@ +input:not([type=submit]) { + @include peertube-input-text(340px); + display: block; +} + +input[type=submit] { + @include peertube-button; + @include orange-button; +} 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' @Component({ selector: 'my-signup', - templateUrl: './signup.component.html' + templateUrl: './signup.component.html', + styleUrls: [ './signup.component.scss' ] }) export class SignupComponent extends FormReactive implements OnInit { 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 similarity index 64% rename from client/src/app/videos/shared/video-description.component.html rename to 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 @@ 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 @@ +textarea { + @include peertube-input-text(100%); + + padding: 5px 15px; + font-size: 15px; + height: 150px; + margin-bottom: 15px; +} + +/deep/ { + .nav-link { + display: flex !important; + align-items: center; + height: 30px !important; + padding: 0 15px !important; + } + + .tab-content { + min-height: 75px; + padding: 15px; + font-size: 15px; + } +} + diff --git a/client/src/app/videos/shared/video-description.component.ts b/client/src/app/videos/+video-edit/shared/video-description.component.ts similarity index 95% rename from client/src/app/videos/shared/video-description.component.ts rename to 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 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core' import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' -import { Subject } from 'rxjs/Subject' +import { truncate } from 'lodash' import 'rxjs/add/operator/debounceTime' import 'rxjs/add/operator/distinctUntilChanged' - -import { truncate } from 'lodash' - -import { MarkdownService } from './markdown.service' +import { Subject } from 'rxjs/Subject' +import { MarkdownService } from '../../shared' @Component({ selector: 'my-video-description', @@ -62,6 +60,8 @@ export class VideoDescriptionComponent implements ControlValueAccessor, OnInit { } private updateDescriptionPreviews () { + if (!this.description) return + this.truncatedDescriptionHTML = this.markdownService.markdownToHTML(truncate(this.description, { length: 250 })) this.descriptionHTML = this.markdownService.markdownToHTML(this.description) } 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 @@ +
+ +
+
+ + +
+ {{ formErrors.name }} +
+
+ +
+ (press enter to add the tag) + +
+ +
+ + + +
+ {{ formErrors.description }} +
+
+
+ +
+
+ + + +
+ {{ formErrors.category }} +
+
+ +
+ + + +
+ {{ formErrors.licence }} +
+
+ +
+ + + +
+ {{ formErrors.language }} +
+
+ +
+ + + +
+ {{ formErrors.privacy }} +
+
+ +
+ + +
+ +
+
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 @@ -.btn-file { - position: relative; - overflow: hidden; - display: block; -} +.video-edit { + height: 100%; -.btn-file input[type=file] { - position: absolute; - top: 0; - right: 0; - min-width: 100%; - min-height: 100%; - font-size: 100px; - text-align: right; - filter: alpha(opacity=0); - opacity: 0; - outline: none; - background: white; - cursor: inherit; - display: block; -} + .form-group { + margin-bottom: 25px; + } -.form-group { - margin-bottom: 10px; -} + input { + @include peertube-input-text(100%); + display: block; -div.tags { - height: 40px; - font-size: 20px; - margin-top: 20px; + &[type=checkbox] { + outline: 0; + } + } - .tag { - margin-right: 10px; + select { + @include peertube-select(100%); + } - .remove { - cursor: pointer; + input, select { + font-size: 15px + } + + .form-group-checkbox { + display: flex; + align-items: center; + + label { + font-weight: $font-regular; + margin: 0; + } + + input { + width: 10px; + margin-right: 10px; } } } -div.file-to-upload { - height: 40px; +.submit-container { + text-align: right; + position: relative; + bottom: $button-height; - .glyphicon-remove { - cursor: pointer; + .message-submit { + display: inline-block; + margin-right: 25px; + + color: #585858; + font-size: 15px; + } + + .submit-button { + @include peertube-button; + @include orange-button; + + display: inline-block; + + input { + cursor: inherit; + background-color: inherit; + border: none; + padding: 0; + outline: 0; + } + + .icon.icon-validate { + @include icon(20px); + + cursor: inherit; + position: relative; + top: -1px; + margin-right: 4px; + background-image: url('../../../../assets/images/global/validate.svg'); + } + } +} + +/deep/ { + .ng2-tag-input { + border: none !important; + } + + .ng2-tags-container { + display: flex; + align-items: center; + border: 1px solid #C6C6C6; + border-radius: 3px; + padding: 5px !important; + } + + tag { + background-color: #E5E5E5 !important; + border-radius: 3px !important; + font-size: 15px !important; + color: #000 !important; + height: 30px !important; + line-height: 30px !important; + margin: 0 5px 0 0 !important; + cursor: default !important; + padding: 0 8px 0 10px !important; + + div { + height: 100% !important; + } + } + + delete-icon { + cursor: pointer !important; + height: auto !important; + vertical-align: middle !important; + padding-left: 6px !important; + + svg { + height: auto !important; + vertical-align: middle !important; + fill: #585858 !important; + } + + &:hover { + transform: none !important; + } } } @@ -50,7 +128,3 @@ div.file-to-upload { font-size: 0.8em; font-style: italic; } - -.label-tags { - margin-bottom: 0; -} 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 @@ +import { Component, Input, OnInit } from '@angular/core' +import { FormBuilder, FormControl, FormGroup } from '@angular/forms' +import { ActivatedRoute, Router } from '@angular/router' +import { NotificationsService } from 'angular2-notifications' +import { ServerService } from 'app/core' +import { VideoEdit } from 'app/shared/video/video-edit.model' +import 'rxjs/add/observable/forkJoin' +import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' +import { + ValidatorMessage, + VIDEO_CATEGORY, + VIDEO_DESCRIPTION, + VIDEO_LANGUAGE, + VIDEO_LICENCE, + VIDEO_NAME, + VIDEO_PRIVACY, + VIDEO_TAGS +} from '../../../shared/forms/form-validators' + +@Component({ + selector: 'my-video-edit', + styleUrls: [ './video-edit.component.scss' ], + templateUrl: './video-edit.component.html' +}) + +export class VideoEditComponent implements OnInit { + @Input() form: FormGroup + @Input() formErrors: { [ id: string ]: string } = {} + @Input() validationMessages: ValidatorMessage = {} + @Input() videoPrivacies = [] + + tags: string[] = [] + videoCategories = [] + videoLicences = [] + videoLanguages = [] + video: VideoEdit + + tagValidators = VIDEO_TAGS.VALIDATORS + tagValidatorsMessages = VIDEO_TAGS.MESSAGES + + error: string = null + + constructor ( + private formBuilder: FormBuilder, + private route: ActivatedRoute, + private router: Router, + private notificationsService: NotificationsService, + private serverService: ServerService + ) { } + + updateForm () { + this.formErrors['name'] = '' + this.formErrors['privacy'] = '' + this.formErrors['category'] = '' + this.formErrors['licence'] = '' + this.formErrors['language'] = '' + this.formErrors['description'] = '' + + this.validationMessages['name'] = VIDEO_NAME.MESSAGES + this.validationMessages['privacy'] = VIDEO_PRIVACY.MESSAGES + this.validationMessages['category'] = VIDEO_CATEGORY.MESSAGES + this.validationMessages['licence'] = VIDEO_LICENCE.MESSAGES + this.validationMessages['language'] = VIDEO_LANGUAGE.MESSAGES + this.validationMessages['description'] = VIDEO_DESCRIPTION.MESSAGES + + this.form.addControl('name', new FormControl('', VIDEO_NAME.VALIDATORS)) + this.form.addControl('privacy', new FormControl('', VIDEO_PRIVACY.VALIDATORS)) + this.form.addControl('nsfw', new FormControl(false)) + this.form.addControl('category', new FormControl('', VIDEO_CATEGORY.VALIDATORS)) + this.form.addControl('licence', new FormControl('', VIDEO_LICENCE.VALIDATORS)) + this.form.addControl('language', new FormControl('', VIDEO_LANGUAGE.VALIDATORS)) + this.form.addControl('description', new FormControl('', VIDEO_DESCRIPTION.VALIDATORS)) + this.form.addControl('tags', new FormControl('')) + } + + ngOnInit () { + this.updateForm() + + this.videoCategories = this.serverService.getVideoCategories() + this.videoLicences = this.serverService.getVideoLicences() + this.videoLanguages = this.serverService.getVideoLanguages() + } +} 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' import { TagInputModule } from 'ngx-chips' import { TabsModule } from 'ngx-bootstrap/tabs' -import { VideoService, MarkdownService, VideoDescriptionComponent } from '../../shared' +import { MarkdownService } from '../../shared' import { SharedModule } from '../../../shared' +import { VideoDescriptionComponent } from './video-description.component' +import { VideoEditComponent } from './video-edit.component' @NgModule({ imports: [ @@ -15,18 +17,19 @@ import { SharedModule } from '../../../shared' ], declarations: [ - VideoDescriptionComponent + VideoDescriptionComponent, + VideoEditComponent ], exports: [ TagInputModule, TabsModule, - VideoDescriptionComponent + VideoDescriptionComponent, + VideoEditComponent ], providers: [ - VideoService, MarkdownService ] }) 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 @@ -
-
+
+
+ Upload your video +
-

Upload a video

+
{{ error }}
-
{{ error }}
+
+
+
-
-
- - -
- {{ formErrors.name }} -
+
+ Select the file to upload +
- - - -
- {{ formErrors.privacy }} -
- - -
- -
- - - -
- {{ formErrors.channelId }} -
- -
- - - -
- {{ formErrors.category }} -
-
- -
- - - -
- {{ formErrors.licence }} -
-
- -
- - - -
- {{ formErrors.language }} -
-
- -
- (press enter to add the tag) - -
- -
- -
- Select the video... - - -
-
- -
-
- {{ filename }} - -
-
- -
- {{ formErrors.videofile }} -
- -
- - - -
- {{ formErrors.description }} -
-
- -
- - - - Server is processing the video - - -
- -
- -
- +
+ + + + +
+ + + +
+
Publish will be available when upload is finished
+ +
+ + +
+
+
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 @@ +.upload-video-container { + border-radius: 3px; + background-color: #F7F7F7; + border: 3px solid #EAEAEA; + width: 100%; + height: 440px; + text-align: center; + margin-top: 40px; + display: flex; + justify-content: center; + align-items: center; + + .upload-video { + display: flex; + flex-direction: column; + align-items: center; + + .icon.icon-upload { + @include icon(90px); + margin-bottom: 25px; + cursor: default; + + background-image: url('../../../assets/images/video/upload.svg'); + } + + .button-file { + position: relative; + overflow: hidden; + display: inline-block; + margin-bottom: 70px; + + @include peertube-button; + @include orange-button; + + input[type=file] { + position: absolute; + top: 0; + right: 0; + min-width: 100%; + min-height: 100%; + font-size: 100px; + text-align: right; + filter: alpha(opacity=0); + opacity: 0; + outline: none; + background: white; + cursor: inherit; + display: block; + } + } + + select { + @include peertube-select(auto); + + display: inline-block; + font-size: 15px + } + } +} + +p-progressBar { + /deep/ .ui-progressbar { + margin-top: 25px !important; + margin-bottom: 40px !important; + font-size: 15px !important; + color: #fff !important; + height: 30px !important; + line-height: 30px !important; + border-radius: 3px !important; + background-color: rgba(11, 204, 41, 0.16) !important; + + .ui-progressbar-value { + background-color: #0BCC29 !important; + } + + .ui-progressbar-label { + text-align: left; + padding-left: 18px; + margin-top: 0 !important; + } + } + + &.processing { + /deep/ .ui-progressbar-label { + // Same color as background to hide "100%" + color: rgba(11, 204, 41, 0.16) !important; + + &::before { + content: 'Processing...'; + color: #fff; + } + } + } +} + + 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 @@ +import { HttpEventType, HttpResponse } from '@angular/common/http' import { Component, OnInit, ViewChild } from '@angular/core' import { FormBuilder, FormGroup } from '@angular/forms' import { Router } from '@angular/router' - import { NotificationsService } from 'angular2-notifications' - -import { - FormReactive, - VIDEO_NAME, - VIDEO_CATEGORY, - VIDEO_LICENCE, - VIDEO_LANGUAGE, - VIDEO_DESCRIPTION, - VIDEO_TAGS, - VIDEO_CHANNEL, - VIDEO_FILE, - VIDEO_PRIVACY -} from '../../shared' -import { AuthService, ServerService } from '../../core' -import { VideoService } from '../shared' +import { VideoService } from 'app/shared/video/video.service' import { VideoCreate } from '../../../../../shared' -import { HttpEventType, HttpResponse } from '@angular/common/http' +import { VideoPrivacy } from '../../../../../shared/models/videos' +import { AuthService, ServerService } from '../../core' +import { FormReactive } from '../../shared' +import { ValidatorMessage } from '../../shared/forms/form-validators' +import { VideoEdit } from '../../shared/video/video-edit.model' @Component({ selector: 'my-videos-add', - styleUrls: [ './shared/video-edit.component.scss' ], - templateUrl: './video-add.component.html' + templateUrl: './video-add.component.html', + styleUrls: [ + './shared/video-edit.component.scss', + './video-add.component.scss' + ] }) export class VideoAddComponent extends FormReactive implements OnInit { @ViewChild('videofileInput') videofileInput - progressPercent = 0 - tags: string[] = [] - videoCategories = [] - videoLicences = [] - videoLanguages = [] - videoPrivacies = [] - userVideoChannels = [] + isUploadingVideo = false + videoUploaded = false + videoUploadPercents = 0 + videoUploadedId = 0 - tagValidators = VIDEO_TAGS.VALIDATORS - tagValidatorsMessages = VIDEO_TAGS.MESSAGES - - error: string + error: string = null form: FormGroup - formErrors = { - name: '', - privacy: '', - category: '', - licence: '', - language: '', - channelId: '', - description: '', - videofile: '' - } - validationMessages = { - name: VIDEO_NAME.MESSAGES, - privacy: VIDEO_PRIVACY.MESSAGES, - category: VIDEO_CATEGORY.MESSAGES, - licence: VIDEO_LICENCE.MESSAGES, - language: VIDEO_LANGUAGE.MESSAGES, - channelId: VIDEO_CHANNEL.MESSAGES, - description: VIDEO_DESCRIPTION.MESSAGES, - videofile: VIDEO_FILE.MESSAGES - } + formErrors: { [ id: string ]: string } = {} + validationMessages: ValidatorMessage = {} + + userVideoChannels = [] + videoPrivacies = [] + firstStepPrivacyId = 0 + firstStepChannelId = 0 constructor ( private formBuilder: FormBuilder, @@ -75,35 +49,23 @@ export class VideoAddComponent extends FormReactive implements OnInit { super() } - get filename () { - return this.form.value['videofile'] - } - buildForm () { - this.form = this.formBuilder.group({ - name: [ '', VIDEO_NAME.VALIDATORS ], - nsfw: [ false ], - privacy: [ '', VIDEO_PRIVACY.VALIDATORS ], - category: [ '', VIDEO_CATEGORY.VALIDATORS ], - licence: [ '', VIDEO_LICENCE.VALIDATORS ], - language: [ '', VIDEO_LANGUAGE.VALIDATORS ], - channelId: [ '', VIDEO_CHANNEL.VALIDATORS ], - description: [ '', VIDEO_DESCRIPTION.VALIDATORS ], - videofile: [ '', VIDEO_FILE.VALIDATORS ], - tags: [ '' ] - }) - + this.form = this.formBuilder.group({}) this.form.valueChanges.subscribe(data => this.onValueChanged(data)) } ngOnInit () { - this.videoCategories = this.serverService.getVideoCategories() - this.videoLicences = this.serverService.getVideoLicences() - this.videoLanguages = this.serverService.getVideoLanguages() - this.videoPrivacies = this.serverService.getVideoPrivacies() - this.buildForm() + this.serverService.videoPrivaciesLoaded + .subscribe( + () => { + this.videoPrivacies = this.serverService.getVideoPrivacies() + + // Public by default + this.firstStepPrivacyId = VideoPrivacy.PUBLIC + }) + this.authService.userInformationLoaded .subscribe( () => { @@ -114,21 +76,13 @@ export class VideoAddComponent extends FormReactive implements OnInit { if (Array.isArray(videoChannels) === false) return this.userVideoChannels = videoChannels.map(v => ({ id: v.id, label: v.name })) - - this.form.patchValue({ channelId: this.userVideoChannels[0].id }) + this.firstStepChannelId = this.userVideoChannels[0].id } ) } - // The goal is to keep reactive form validation (required field) - // https://stackoverflow.com/a/44238894 - fileChange ($event) { - this.form.controls['videofile'].setValue($event.target.files[0].name) - } - - removeFile () { - this.videofileInput.nativeElement.value = '' - this.form.controls['videofile'].setValue('') + fileChange () { + this.uploadFirstStep() } checkForm () { @@ -137,62 +91,72 @@ export class VideoAddComponent extends FormReactive implements OnInit { return this.form.valid } - upload () { - if (this.checkForm() === false) { - return - } - - const formValue: VideoCreate = this.form.value - - const name = formValue.name - const privacy = formValue.privacy - const nsfw = formValue.nsfw - const category = formValue.category - const licence = formValue.licence - const language = formValue.language - const channelId = formValue.channelId - const description = formValue.description - const tags = formValue.tags + uploadFirstStep () { const videofile = this.videofileInput.nativeElement.files[0] + const name = videofile.name.replace(/\.[^/.]+$/, '') + const privacy = this.firstStepPrivacyId.toString() + const nsfw = false + const channelId = this.firstStepChannelId.toString() const formData = new FormData() formData.append('name', name) - formData.append('privacy', privacy.toString()) - formData.append('category', '' + category) + // Put the video "private" -> we wait he validates the second step + formData.append('privacy', VideoPrivacy.PRIVATE.toString()) formData.append('nsfw', '' + nsfw) - formData.append('licence', '' + licence) formData.append('channelId', '' + channelId) formData.append('videofile', videofile) - // Language is optional - if (language) { - formData.append('language', '' + language) - } - - formData.append('description', description) - - for (let i = 0; i < tags.length; i++) { - formData.append(`tags[${i}]`, tags[i]) - } + this.isUploadingVideo = true + this.form.patchValue({ + name, + privacy, + nsfw, + channelId + }) this.videoService.uploadVideo(formData).subscribe( event => { if (event.type === HttpEventType.UploadProgress) { - this.progressPercent = Math.round(100 * event.loaded / event.total) + this.videoUploadPercents = Math.round(100 * event.loaded / event.total) } else if (event instanceof HttpResponse) { console.log('Video uploaded.') - this.notificationsService.success('Success', 'Video uploaded.') - // Display all the videos once it's finished - this.router.navigate([ '/videos/list' ]) + this.videoUploaded = true + + this.videoUploadedId = event.body.video.id } }, err => { // Reset progress - this.progressPercent = 0 + this.videoUploadPercents = 0 this.error = err.message } ) } + + updateSecondStep () { + if (this.checkForm() === false) { + return + } + + const video = new VideoEdit() + video.patch(this.form.value) + video.channel = this.firstStepChannelId + video.id = this.videoUploadedId + + this.videoService.updateVideo(video) + .subscribe( + () => { + this.notificationsService.success('Success', 'Video published.') + this.router.navigate([ '/videos/watch', video.id ]) + }, + + err => { + this.error = 'Cannot update the video.' + console.error(err) + } + ) + + } } 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 @@ import { NgModule } from '@angular/core' +import { ProgressBarModule } from 'primeng/primeng' import { SharedModule } from '../../shared' import { VideoEditModule } from './shared/video-edit.module' import { VideoAddRoutingModule } from './video-add-routing.module' @@ -8,7 +9,8 @@ import { VideoAddComponent } from './video-add.component' imports: [ VideoAddRoutingModule, VideoEditModule, - SharedModule + SharedModule, + ProgressBarModule ], 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 @@ -
-
- -

Update {{ video?.name }}

- -
{{ error }}
+
+
+ Update {{ video?.name }} +
-
- - -
- {{ formErrors.name }} + + + +
+
+ +
- -
- - - -
- {{ formErrors.privacy }} -
-
- -
- - -
- -
- - - -
- {{ formErrors.category }} -
-
- -
- - - -
- {{ formErrors.licence }} -
-
- -
- - - -
- {{ formErrors.language }} -
-
- -
- (press enter to add the tag) - -
- -
- - - -
- {{ formErrors.description }} -
-
- -
- -
-
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 @@ import { Component, OnInit } from '@angular/core' import { FormBuilder, FormGroup } from '@angular/forms' import { ActivatedRoute, Router } from '@angular/router' -import 'rxjs/add/observable/forkJoin' - import { NotificationsService } from 'angular2-notifications' - -import { ServerService } from '../../core' -import { - FormReactive, - VIDEO_NAME, - VIDEO_CATEGORY, - VIDEO_LICENCE, - VIDEO_LANGUAGE, - VIDEO_DESCRIPTION, - VIDEO_TAGS, - VIDEO_PRIVACY -} from '../../shared' -import { VideoEdit, VideoService } from '../shared' +import 'rxjs/add/observable/forkJoin' import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' +import { ServerService } from '../../core' +import { FormReactive } from '../../shared' +import { ValidatorMessage } from '../../shared/forms/form-validators' +import { VideoEdit } from '../../shared/video/video-edit.model' +import { VideoService } from '../../shared/video/video.service' @Component({ selector: 'my-videos-update', @@ -26,34 +17,13 @@ import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy. }) export class VideoUpdateComponent extends FormReactive implements OnInit { - tags: string[] = [] - videoCategories = [] - videoLicences = [] - videoLanguages = [] - videoPrivacies = [] video: VideoEdit - tagValidators = VIDEO_TAGS.VALIDATORS - tagValidatorsMessages = VIDEO_TAGS.MESSAGES - error: string = null form: FormGroup - formErrors = { - name: '', - privacy: '', - category: '', - licence: '', - language: '', - description: '' - } - validationMessages = { - name: VIDEO_NAME.MESSAGES, - privacy: VIDEO_PRIVACY.MESSAGES, - category: VIDEO_CATEGORY.MESSAGES, - licence: VIDEO_LICENCE.MESSAGES, - language: VIDEO_LANGUAGE.MESSAGES, - description: VIDEO_DESCRIPTION.MESSAGES - } + formErrors: { [ id: string ]: string } = {} + validationMessages: ValidatorMessage = {} + videoPrivacies = [] fileError = '' @@ -69,30 +39,16 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { } buildForm () { - this.form = this.formBuilder.group({ - name: [ '', VIDEO_NAME.VALIDATORS ], - privacy: [ '', VIDEO_PRIVACY.VALIDATORS ], - nsfw: [ false ], - category: [ '', VIDEO_CATEGORY.VALIDATORS ], - licence: [ '', VIDEO_LICENCE.VALIDATORS ], - language: [ '', VIDEO_LANGUAGE.VALIDATORS ], - description: [ '', VIDEO_DESCRIPTION.VALIDATORS ], - tags: [ '' ] - }) - + this.form = this.formBuilder.group({}) this.form.valueChanges.subscribe(data => this.onValueChanged(data)) } ngOnInit () { this.buildForm() - this.videoCategories = this.serverService.getVideoCategories() - this.videoLicences = this.serverService.getVideoLicences() - this.videoLanguages = this.serverService.getVideoLanguages() this.videoPrivacies = this.serverService.getVideoPrivacies() const uuid: string = this.route.snapshot.params['uuid'] - this.videoService.getVideo(uuid) .switchMap(video => { return this.videoService @@ -104,7 +60,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { video => { this.video = new VideoEdit(video) - // We cannot set private a video that was not private anymore + // We cannot set private a video that was not private if (video.privacy !== VideoPrivacy.PRIVATE) { const newVideoPrivacies = [] 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 @@ - +

Download