Merge branch 'feature/design' into develop

This commit is contained in:
Chocobozzz 2017-12-11 11:06:32 +01:00
commit fada8d7555
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
244 changed files with 4777 additions and 3308 deletions

View File

@ -8,14 +8,9 @@
# Design # Design
Inspirations from: By [Olivier Massain](https://twitter.com/omassain)
* [Aurélien Salomon](https://dribbble.com/shots/1338727-Youtube-Redesign) Icons from [Robbie Pearce](https://robbiepearce.com/softies/)
* [Wojciech Zieliński](https://dribbble.com/shots/3000315-youtube-concept)
Video.js theme:
* [zanechua](https://github.com/zanechua/videojs-sublime-inspired-skin)
# Fonts # Fonts

View File

@ -84,19 +84,19 @@ styles:
navs: true navs: true
navbar: false navbar: false
breadcrumbs: false breadcrumbs: false
pagination: true pagination: false
pager: false pager: false
labels: true labels: false
badges: false badges: false
jumbotron: false jumbotron: false
thumbnails: true thumbnails: false
alerts: true alerts: true
progress-bars: true progress-bars: false
media: true media: true
list-group: false list-group: false
panels: true panels: true
wells: false wells: false
responsive-embed: true responsive-embed: false
close: true close: true
# Components w/ JavaScript # Components w/ JavaScript

View File

@ -13,6 +13,7 @@ const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin')
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin') const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin')
const InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin') const InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin')
const ngcWebpack = require('ngc-webpack') const ngcWebpack = require('ngc-webpack')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const WebpackNotifierPlugin = require('webpack-notifier') const WebpackNotifierPlugin = require('webpack-notifier')
@ -146,14 +147,15 @@ module.exports = function (options) {
loader: 'sass-resources-loader', loader: 'sass-resources-loader',
options: { options: {
resources: [ 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: /\.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 /* Raw loader support for *.html
* Returns file content as string * Returns file content as string
@ -266,6 +268,17 @@ module.exports = function (options) {
inject: 'body' 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 * Plugin: ScriptExtHtmlWebpackPlugin
* Description: Enhances html-webpack-plugin functionality * Description: Enhances html-webpack-plugin functionality
@ -289,6 +302,7 @@ module.exports = function (options) {
*/ */
new LoaderOptionsPlugin({ new LoaderOptionsPlugin({
options: { options: {
context: '',
sassLoader: { sassLoader: {
precision: 10, precision: 10,
includePaths: [ helpers.root('src/sass') ] includePaths: [ helpers.root('src/sass') ]

View File

@ -74,7 +74,8 @@ module.exports = function (options) {
loader: 'sass-resources-loader', loader: 'sass-resources-loader',
options: { options: {
resources: [ resources: [
helpers.root('src/sass/_variables.scss') helpers.root('src/sass/_variables.scss'),
helpers.root('src/sass/_mixins.scss')
] ]
} }
} }

View File

@ -43,7 +43,6 @@
"@types/webpack": "^3.0.0", "@types/webpack": "^3.0.0",
"@types/webtorrent": "^0.98.4", "@types/webtorrent": "^0.98.4",
"add-asset-html-webpack-plugin": "^2.0.1", "add-asset-html-webpack-plugin": "^2.0.1",
"angular-pipes": "^6.0.0",
"angular2-notifications": "^0.7.7", "angular2-notifications": "^0.7.7",
"angular2-template-loader": "^0.6.0", "angular2-template-loader": "^0.6.0",
"assets-webpack-plugin": "^3.4.0", "assets-webpack-plugin": "^3.4.0",
@ -70,8 +69,10 @@
"markdown-it": "^8.4.0", "markdown-it": "^8.4.0",
"ng-router-loader": "^2.0.0", "ng-router-loader": "^2.0.0",
"ngc-webpack": "3.2.2", "ngc-webpack": "3.2.2",
"ngx-bootstrap": "1.9.3", "ngx-bootstrap": "2.0.0-beta.9",
"ngx-chips": "1.5.3", "ngx-chips": "1.5.3",
"ngx-infinite-scroll": "^0.7.0",
"ngx-pipes": "^2.0.5",
"node-sass": "^4.1.1", "node-sass": "^4.1.1",
"normalize.css": "^7.0.0", "normalize.css": "^7.0.0",
"optimize-js-plugin": "0.0.4", "optimize-js-plugin": "0.0.4",
@ -86,6 +87,7 @@
"sass-resources-loader": "^1.2.1", "sass-resources-loader": "^1.2.1",
"script-ext-html-webpack-plugin": "^1.3.2", "script-ext-html-webpack-plugin": "^1.3.2",
"source-map-loader": "^0.2.1", "source-map-loader": "^0.2.1",
"source-sans-pro": "^2.0.10",
"standard": "^10.0.0", "standard": "^10.0.0",
"string-replace-loader": "^1.0.3", "string-replace-loader": "^1.0.3",
"style-loader": "^0.19.0", "style-loader": "^0.19.0",

View File

@ -0,0 +1,27 @@
<div class="row">
<div class="sub-menu">
<a *ngIf="hasUsersRight()" routerLink="/admin/users" routerLinkActive="active" class="title-page">
Users
</a>
<a *ngIf="hasServerFollowRight()" routerLink="/admin/follows" routerLinkActive="active" class="title-page">
Manage follows
</a>
<a *ngIf="hasVideoAbusesRight()" routerLink="/admin/video-abuses" routerLinkActive="active" class="title-page">
Video abuses
</a>
<a *ngIf="hasVideoBlacklistRight()" routerLink="/admin/video-blacklist" routerLinkActive="active" class="title-page">
Video blacklist
</a>
<a *ngIf="hasJobsRight()" routerLink="/admin/jobs" routerLinkActive="active" class="title-page">
Jobs
</a>
</div>
<div class="margin-content">
<router-outlet></router-outlet>
</div>
</div>

View File

@ -1,7 +1,31 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { UserRight } from '../../../../shared'
import { AuthService } from '../core/auth/auth.service'
@Component({ @Component({
template: '<router-outlet></router-outlet>' templateUrl: './admin.component.html',
styleUrls: [ './admin.component.scss' ]
}) })
export class AdminComponent { 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)
}
} }

View File

@ -1,16 +1,10 @@
<div class="row"> <p-dataTable
<div class="content-padding">
<h3>Followers list</h3>
<p-dataTable
[value]="followers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" [value]="followers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
sortField="createdAt" (onLazyLoad)="loadLazy($event)" sortField="createdAt" (onLazyLoad)="loadLazy($event)"
> >
<p-column field="id" header="ID"></p-column> <p-column field="id" header="ID"></p-column>
<p-column field="follower.host" header="Host"></p-column> <p-column field="follower.host" header="Host"></p-column>
<p-column field="follower.score" header="Score"></p-column> <p-column field="follower.score" header="Score"></p-column>
<p-column field="state" header="State"></p-column> <p-column field="state" header="State"></p-column>
<p-column field="createdAt" header="Created date" [sortable]="true"></p-column> <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
</p-dataTable> </p-dataTable>
</div>
</div>

View File

@ -1,3 +0,0 @@
.btn {
margin-top: 10px;
}

View File

@ -1,35 +1,22 @@
<div class="row"> <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<div class="content-padding">
<h3>Add following</h3> <form (ngSubmit)="addFollowing()">
<div class="form-group">
<label for="hosts">1 host (without "http://") per line</label>
<div *ngIf="error" class="alert alert-danger">{{ error }}</div> <textarea
type="text" class="form-control" placeholder="example.com" id="hosts" name="hosts"
[(ngModel)]="hostsString" (ngModelChange)="onHostsChanged()" [ngClass]="{ 'input-error': hostsError }"
></textarea>
<form (ngSubmit)="addFollowing()" [formGroup]="form"> <div *ngIf="hostsError" class="form-error">
<div class="form-group" *ngFor="let host of hosts; let id = index; trackBy:customTrackBy"> {{ hostsError }}
<label [for]="'host-' + id">Host (so without "http://")</label>
<div class="input-group">
<input
type="text" class="form-control" placeholder="example.com"
[id]="'host-' + id" [formControlName]="'host-' + id"
/>
<span class="input-group-btn">
<button *ngIf="displayAddField(id)" (click)="addField()" class="btn btn-default" type="button">+</button>
<button *ngIf="displayRemoveField(id)" (click)="removeField(id)" class="btn btn-default" type="button">-</button>
</span>
</div>
<div [hidden]="form.controls['host-' + id].valid || form.controls['host-' + id].pristine" class="alert alert-warning">
It should be a valid host.
</div> </div>
</div> </div>
<div *ngIf="canMakeFriends() === false" class="alert alert-warning"> <div *ngIf="httpEnabled() === false" class="alert alert-warning">
It seems that you are not on a HTTPS server. Your webserver need to have TLS activated in order to follow servers. It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
</div> </div>
<input type="submit" value="Add following" class="btn btn-default" [disabled]="!isFormValid()"> <input type="submit" value="Add following" [disabled]="hostsError || !hostsString" class="btn btn-default">
</form> </form>
</div>
</div>

View File

@ -1,7 +1,9 @@
table { textarea {
margin-bottom: 40px; height: 250px;
} }
.input-group-btn button { input[type=submit] {
width: 35px; @include peertube-button;
@include orange-button;
} }

View File

@ -1,9 +1,6 @@
import { Component, OnInit } from '@angular/core' import { Component } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { NotificationsService } from 'angular2-notifications' import { NotificationsService } from 'angular2-notifications'
import { ConfirmService } from '../../../core' import { ConfirmService } from '../../../core'
import { validateHost } from '../../../shared' import { validateHost } from '../../../shared'
import { FollowService } from '../shared' import { FollowService } from '../shared'
@ -13,9 +10,9 @@ import { FollowService } from '../shared'
templateUrl: './following-add.component.html', templateUrl: './following-add.component.html',
styleUrls: [ './following-add.component.scss' ] styleUrls: [ './following-add.component.scss' ]
}) })
export class FollowingAddComponent implements OnInit { export class FollowingAddComponent {
form: FormGroup hostsString = ''
hosts: string[] = [ ] hostsError: string = null
error: string = null error: string = null
constructor ( constructor (
@ -25,76 +22,50 @@ export class FollowingAddComponent implements OnInit {
private followService: FollowService private followService: FollowService
) {} ) {}
ngOnInit () { httpEnabled () {
this.form = new FormGroup({})
this.addField()
}
addField () {
this.form.addControl(`host-${this.hosts.length}`, new FormControl('', [ validateHost ]))
this.hosts.push('')
}
canMakeFriends () {
return window.location.protocol === 'https:' return window.location.protocol === 'https:'
} }
customTrackBy (index: number, obj: any): any { onHostsChanged () {
return index this.hostsError = null
}
displayAddField (index: number) { const newHostsErrors = []
return index === (this.hosts.length - 1) const hosts = this.getNotEmptyHosts()
}
displayRemoveField (index: number) { for (const host of hosts) {
return (index !== 0 || this.hosts.length > 1) && index !== (this.hosts.length - 1) if (validateHost(host) === false) {
} newHostsErrors.push(`${host} is not valid`)
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
}
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
} }
} }
removeField (index: number) { if (newHostsErrors.length !== 0) {
// Remove the last control this.hostsError = newHostsErrors.join('. ')
this.form.removeControl(`host-${this.hosts.length - 1}`) }
this.hosts.splice(index, 1)
} }
addFollowing () { addFollowing () {
this.error = '' this.error = ''
const notEmptyHosts = this.getNotEmptyHosts() const hosts = this.getNotEmptyHosts()
if (notEmptyHosts.length === 0) { if (hosts.length === 0) {
this.error = 'You need to specify at least 1 host.' this.error = 'You need to specify hosts to follow.'
return
} }
if (!this.isHostsUnique(notEmptyHosts)) { if (!this.isHostsUnique(hosts)) {
this.error = 'Hosts need to be unique.' this.error = 'Hosts need to be unique.'
return return
} }
const confirmMessage = 'Are you sure to make friends with:<br /> - ' + notEmptyHosts.join('<br /> - ') const confirmMessage = 'If you confirm, you will send a follow request to:<br /> - ' + hosts.join('<br /> - ')
this.confirmService.confirm(confirmMessage, 'Follow new server(s)').subscribe( this.confirmService.confirm(confirmMessage, 'Follow new server(s)').subscribe(
res => { res => {
if (res === false) return if (res === false) return
this.followService.follow(notEmptyHosts).subscribe( this.followService.follow(hosts).subscribe(
status => { status => {
this.notificationsService.success('Success', 'Follow request(s) sent!') 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) 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[]) { private isHostsUnique (hosts: string[]) {
return hosts.every(host => hosts.indexOf(host) === hosts.lastIndexOf(host)) 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
}
} }

View File

@ -1,20 +1,14 @@
<div class="row"> <p-dataTable
<div class="content-padding">
<h3>Following list</h3>
<p-dataTable
[value]="following" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" [value]="following" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
sortField="createdAt" (onLazyLoad)="loadLazy($event)" sortField="createdAt" (onLazyLoad)="loadLazy($event)"
> >
<p-column field="id" header="ID"></p-column> <p-column field="id" header="ID"></p-column>
<p-column field="following.host" header="Host"></p-column> <p-column field="following.host" header="Host"></p-column>
<p-column field="state" header="State"></p-column> <p-column field="state" header="State"></p-column>
<p-column field="createdAt" header="Created date" [sortable]="true"></p-column> <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
<p-column header="Unfollow" styleClass="action-cell"> <p-column styleClass="action-cell">
<ng-template pTemplate="body" let-following="rowData"> <ng-template pTemplate="body" let-following="rowData">
<span (click)="removeFollowing(following)" class="glyphicon glyphicon-remove glyphicon-black" title="Unfollow"></span> <my-delete-button (click)="removeFollowing(following)"></my-delete-button>
</ng-template> </ng-template>
</p-column> </p-column>
</p-dataTable> </p-dataTable>
</div>
</div>

View File

@ -1,4 +1,6 @@
<div class="follows-menu"> <div class="admin-sub-header">
<div class="admin-sub-title">Manage follows</div>
<tabset #followsMenuTabs> <tabset #followsMenuTabs>
<tab *ngFor="let link of links"> <tab *ngFor="let link of links">
<ng-template tabHeading> <ng-template tabHeading>
@ -8,4 +10,6 @@
</tabset> </tabset>
</div> </div>
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@ -1,21 +1,4 @@
.follows-menu { .admin-sub-title {
margin-top: 20px; flex-grow: 0;
} margin-right: 30px;
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;
}
}
} }

View File

@ -47,7 +47,7 @@ export class FollowsComponent implements OnInit, AfterViewInit {
for (let i = 0; i < this.links.length; i++) { for (let i = 0; i < this.links.length; i++) {
const path = this.links[i].path 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 this.followsMenuTabs.tabs[i].active = true
return return
} }

View File

@ -1,18 +1,20 @@
<div class="row"> <div class="admin-sub-header">
<div class="content-padding"> <div class="admin-sub-title">Jobs list</div>
<h3>Jobs list</h3>
<p-dataTable
[value]="jobs" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
sortField="createdAt" (onLazyLoad)="loadLazy($event)"
>
<p-column field="id" header="ID"></p-column>
<p-column field="category" header="Category"></p-column>
<p-column field="handlerName" header="Handler name"></p-column>
<p-column field="handlerInputData" header="Input data"></p-column>
<p-column field="state" header="State"></p-column>
<p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
<p-column field="updatedAt" header="Updated date"></p-column>
</p-dataTable>
</div>
</div> </div>
<p-dataTable
[value]="jobs" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
sortField="createdAt" (onLazyLoad)="loadLazy($event)" [scrollable]="true" [virtualScroll]="true" [scrollHeight]="scrollHeight"
>
<p-column field="id" header="ID" [style]="{ width: '40px' }"></p-column>
<p-column field="category" header="Category" [style]="{ width: '100px' }"></p-column>
<p-column field="handlerName" header="Handler name" [style]="{ width: '200px' }"></p-column>
<p-column header="Input data">
<ng-template pTemplate="body" let-job="rowData">
<pre>{{ job.handlerInputData }}</pre>
</ng-template>
</p-column>
<p-column field="state" header="State" [style]="{ width: '100px' }"></p-column>
<p-column field="createdAt" header="Created date" [sortable]="true" [style]="{ width: '250px' }"></p-column>
<p-column field="updatedAt" header="Updated date" [style]="{ width: '250px' }"></p-column>
</p-dataTable>

View File

@ -0,0 +1,3 @@
pre {
font-size: 13px;
}

View File

@ -1,22 +1,24 @@
import { Component } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { NotificationsService } from 'angular2-notifications' import { NotificationsService } from 'angular2-notifications'
import { SortMeta } from 'primeng/primeng' import { SortMeta } from 'primeng/primeng'
import { Job } from '../../../../../../shared/index' import { Job } from '../../../../../../shared/index'
import { RestPagination, RestTable } from '../../../shared' import { RestPagination, RestTable } from '../../../shared'
import { viewportHeight } from '../../../shared/misc/utils'
import { JobService } from '../shared' import { JobService } from '../shared'
import { RestExtractor } from '../../../shared/rest/rest-extractor.service' import { RestExtractor } from '../../../shared/rest/rest-extractor.service'
@Component({ @Component({
selector: 'my-jobs-list', selector: 'my-jobs-list',
templateUrl: './jobs-list.component.html', 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[] = [] jobs: Job[] = []
totalRecords = 0 totalRecords = 0
rowsPerPage = 10 rowsPerPage = 20
sort: SortMeta = { field: 'createdAt', order: 1 } sort: SortMeta = { field: 'createdAt', order: 1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 } pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
scrollHeight = ''
constructor ( constructor (
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
@ -26,10 +28,14 @@ export class JobsListComponent extends RestTable {
super() super()
} }
ngOnInit () {
// 270 -> headers + footer...
this.scrollHeight = (viewportHeight() - 380) + 'px'
}
protected loadData () { protected loadData () {
this.jobsService this.jobsService
.getJobs(this.pagination, this.sort) .getJobs(this.pagination, this.sort)
.map(res => this.restExtractor.applyToResultListData(res, this.formatJob.bind(this)))
.subscribe( .subscribe(
resultList => { resultList => {
this.jobs = resultList.data this.jobs = resultList.data
@ -39,12 +45,4 @@ export class JobsListComponent extends RestTable {
err => this.notificationsService.error('Error', err.message) err => this.notificationsService.error('Error', err.message)
) )
} }
private formatJob (job: Job) {
const handlerInputData = JSON.stringify(job.handlerInputData)
return Object.assign(job, {
handlerInputData
})
}
} }

View File

@ -25,6 +25,13 @@ export class JobService {
return this.authHttp.get<ResultList<Job>>(JobService.BASE_JOB_URL, { params }) return this.authHttp.get<ResultList<Job>>(JobService.BASE_JOB_URL, { params })
.map(res => this.restExtractor.convertResultListDateToHuman(res)) .map(res => this.restExtractor.convertResultListDateToHuman(res))
.map(res => this.restExtractor.applyToResultListData(res, this.prettyPrintData))
.catch(err => this.restExtractor.handleError(err)) .catch(err => this.restExtractor.handleError(err))
} }
private prettyPrintData (obj: Job) {
const handlerInputData = JSON.stringify(obj.handlerInputData, null, 2)
return Object.assign(obj, { handlerInputData })
}
} }

View File

@ -1,14 +1,12 @@
import { Injectable } from '@angular/core'
import { HttpClient, HttpParams } from '@angular/common/http' 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/catch'
import 'rxjs/add/operator/map' import 'rxjs/add/operator/map'
import { Observable } from 'rxjs/Observable'
import { SortMeta } from 'primeng/components/common/sortmeta' import { ResultList, UserCreate, UserUpdate } from '../../../../../../shared'
import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe' import { RestExtractor, RestPagination, RestService, User } from '../../../shared'
import { RestExtractor, User, RestPagination, RestService } from '../../../shared'
import { UserCreate, UserUpdate, ResultList } from '../../../../../../shared'
@Injectable() @Injectable()
export class UserService { export class UserService {

View File

@ -1,19 +1,16 @@
<div class="row"> <div class="admin-sub-title" *ngIf="isCreation() === true">Add user</div>
<div class="content-padding"> <div class="admin-sub-title" *ngIf="isCreation() === false">Edit user {{ username }}</div>
<h3 *ngIf="isCreation() === true">Add user</h3> <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<h3 *ngIf="isCreation() === false">Edit user {{ username }}</h3>
<div *ngIf="error" class="alert alert-danger">{{ error }}</div> <form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
<div class="form-group" *ngIf="isCreation()"> <div class="form-group" *ngIf="isCreation()">
<label for="username">Username</label> <label for="username">Username</label>
<input <input
type="text" class="form-control" id="username" placeholder="john" type="text" class="form-control" id="username" placeholder="john"
formControlName="username" formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }"
> >
<div *ngIf="formErrors.username" class="alert alert-danger"> <div *ngIf="formErrors.username" class="form-error">
{{ formErrors.username }} {{ formErrors.username }}
</div> </div>
</div> </div>
@ -22,9 +19,9 @@
<label for="email">Email</label> <label for="email">Email</label>
<input <input
type="text" class="form-control" id="email" placeholder="mail@example.com" type="text" class="form-control" id="email" placeholder="mail@example.com"
formControlName="email" formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }"
> >
<div *ngIf="formErrors.email" class="alert alert-danger"> <div *ngIf="formErrors.email" class="form-error">
{{ formErrors.email }} {{ formErrors.email }}
</div> </div>
</div> </div>
@ -33,9 +30,9 @@
<label for="password">Password</label> <label for="password">Password</label>
<input <input
type="password" class="form-control" id="password" type="password" class="form-control" id="password"
formControlName="password" formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
> >
<div *ngIf="formErrors.password" class="alert alert-danger"> <div *ngIf="formErrors.password" class="form-error">
{{ formErrors.password }} {{ formErrors.password }}
</div> </div>
</div> </div>
@ -48,7 +45,7 @@
</option> </option>
</select> </select>
<div *ngIf="formErrors.role" class="alert alert-danger"> <div *ngIf="formErrors.role" class="form-error">
{{ formErrors.role }} {{ formErrors.role }}
</div> </div>
</div> </div>
@ -67,7 +64,5 @@
</div> </div>
</div> </div>
<input type="submit" value="{{ getFormButtonTitle() }}" class="btn btn-default" [disabled]="!form.valid"> <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
</form> </form>
</div>
</div>

View File

@ -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 { .transcoding-information {
margin-top: 5px; margin-top: 5px;
font-size: 11px; font-size: 11px;

View File

@ -1,35 +1,26 @@
<div class="row"> <div class="admin-sub-header">
<div class="content-padding"> <div class="admin-sub-title">Users list</div>
<h3>Users list</h3> <a class="add-button" routerLink="/admin/users/add">
<span class="icon icon-add"></span>
Add user
</a>
</div>
<p-dataTable <p-dataTable
[value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" [value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
sortField="id" (onLazyLoad)="loadLazy($event)" sortField="id" (onLazyLoad)="loadLazy($event)"
> >
<p-column field="id" header="ID" [sortable]="true"></p-column> <p-column field="id" header="ID" [sortable]="true"></p-column>
<p-column field="username" header="Username" [sortable]="true"></p-column> <p-column field="username" header="Username" [sortable]="true"></p-column>
<p-column field="email" header="Email"></p-column> <p-column field="email" header="Email"></p-column>
<p-column field="videoQuota" header="Video quota"></p-column> <p-column field="videoQuota" header="Video quota"></p-column>
<p-column field="roleLabel" header="Role"></p-column> <p-column field="roleLabel" header="Role"></p-column>
<p-column field="createdAt" header="Created date" [sortable]="true"></p-column> <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
<p-column header="Edit" styleClass="action-cell"> <p-column styleClass="action-cell">
<ng-template pTemplate="body" let-user="rowData"> <ng-template pTemplate="body" let-user="rowData">
<a [routerLink]="getRouterUserEditLink(user)" title="Edit this user"> <my-edit-button [routerLink]="getRouterUserEditLink(user)"></my-edit-button>
<span class="glyphicon glyphicon-pencil glyphicon-black"></span> <my-delete-button (click)="removeUser(user)"></my-delete-button>
</a>
</ng-template> </ng-template>
</p-column> </p-column>
<p-column header="Delete" styleClass="action-cell"> </p-dataTable>
<ng-template pTemplate="body" let-user="rowData">
<span (click)="removeUser(user)" class="glyphicon glyphicon-remove glyphicon-black" title="Remove this user"></span>
</ng-template>
</p-column>
</p-dataTable>
<a class="add-user btn btn-success pull-right" [routerLink]="['/admin/users/add']">
<span class="glyphicon glyphicon-plus"></span>
Add user
</a>
</div>
</div>

View File

@ -1,3 +1,11 @@
.add-user { .add-button {
margin-top: 10px; @include peertube-button-link;
} @include orange-button;
.icon.icon-add {
@include icon(22px);
margin-right: 3px;
background-image: url('../../../../assets/images/admin/add.svg');
}
}

View File

@ -1,24 +1,19 @@
<div class="row"> <div class="admin-sub-header">
<div class="content-padding"> <div class="admin-sub-title">Video abuses list</div>
</div>
<h3>Video abuses list</h3> <p-dataTable
<p-dataTable
[value]="videoAbuses" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" [value]="videoAbuses" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
sortField="id" (onLazyLoad)="loadLazy($event)" sortField="id" (onLazyLoad)="loadLazy($event)"
> >
<p-column field="id" header="ID" [sortable]="true"></p-column> <p-column field="id" header="ID" [sortable]="true"></p-column>
<p-column field="reason" header="Reason"></p-column> <p-column field="reason" header="Reason"></p-column>
<p-column field="reporterServerHost" header="Reporter server host"></p-column> <p-column field="reporterServerHost" header="Reporter server host"></p-column>
<p-column field="reporterUsername" header="Reporter username"></p-column> <p-column field="reporterUsername" header="Reporter username"></p-column>
<p-column field="videoName" header="Video name"></p-column> <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
<p-column header="Video" styleClass="action-cell"> <p-column header="Video">
<ng-template pTemplate="body" let-videoAbuse="rowData"> <ng-template pTemplate="body" let-videoAbuse="rowData">
<a [routerLink]="getRouterVideoLink(videoAbuse.videoId)" title="Go to the video">{{ videoAbuse.videoId }}</a> <a [routerLink]="getRouterVideoLink(videoAbuse.videoId)" title="Go to the video">{{ videoAbuse.videoName }}</a>
</ng-template> </ng-template>
</p-column> </p-column>
<p-column field="createdAt" header="Created date" [sortable]="true"></p-column> </p-dataTable>
</p-dataTable>
</div>
</div>

View File

@ -0,0 +1,6 @@
/deep/ a {
&, &:hover, &:active, &:focus {
color: #000;
}
}

View File

@ -8,7 +8,8 @@ import { VideoAbuse } from '../../../../../../shared'
@Component({ @Component({
selector: 'my-video-abuse-list', 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 { export class VideoAbuseListComponent extends RestTable implements OnInit {
videoAbuses: VideoAbuse[] = [] videoAbuses: VideoAbuse[] = []

View File

@ -18,7 +18,7 @@
<p-column field="createdAt" header="Created date" [sortable]="true"></p-column> <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
<p-column header="Delete" styleClass="action-cell"> <p-column header="Delete" styleClass="action-cell">
<ng-template pTemplate="body" let-entry="rowData"> <ng-template pTemplate="body" let-entry="rowData">
<span (click)="removeVideoFromBlacklist(entry)" class="glyphicon glyphicon-remove glyphicon-black" title="Remove this video from blacklist"></span> <my-delete-button (click)="removeVideoFromBlacklist(entry)"></my-delete-button>
</ng-template> </ng-template>
</p-column> </p-column>
</p-dataTable> </p-dataTable>

View File

@ -1,24 +0,0 @@
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<form role="form" (ngSubmit)="changePassword()" [formGroup]="form">
<div class="form-group">
<label for="new-password">New password</label>
<input
type="password" class="form-control" id="new-password"
formControlName="new-password"
>
<div *ngIf="formErrors['new-password']" class="alert alert-danger">
{{ formErrors['new-password'] }}
</div>
</div>
<div class="form-group">
<label for="name">Confirm new password</label>
<input
type="password" class="form-control" id="new-confirmed-password"
formControlName="new-confirmed-password"
>
</div>
<input type="submit" value="Change password" class="btn btn-default" [disabled]="!form.valid">
</form>

View File

@ -1,16 +0,0 @@
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form">
<div class="form-group">
<input
type="checkbox" id="displayNSFW"
formControlName="displayNSFW"
>
<label for="displayNSFW">Display videos that contain mature or explicit content</label>
<div *ngIf="formErrors['displayNSFW']" class="alert alert-danger">
{{ formErrors['displayNSFW'] }}
</div>
</div>
<input type="submit" value="Update" class="btn btn-default" [disabled]="!form.valid">
</form>

View File

@ -5,17 +5,34 @@ import { MetaGuard } from '@ngx-meta/core'
import { LoginGuard } from '../core' import { LoginGuard } from '../core'
import { AccountComponent } from './account.component' import { AccountComponent } from './account.component'
import { AccountSettingsComponent } from './account-settings/account-settings.component'
import { AccountVideosComponent } from './account-videos/account-videos.component'
const accountRoutes: Routes = [ const accountRoutes: Routes = [
{ {
path: 'account', path: 'account',
component: AccountComponent, component: AccountComponent,
canActivate: [ MetaGuard, LoginGuard ], canActivateChild: [ MetaGuard, LoginGuard ],
children: [
{
path: 'settings',
component: AccountSettingsComponent,
data: { data: {
meta: { meta: {
title: 'My account' title: 'Account settings'
} }
} }
},
{
path: 'videos',
component: AccountVideosComponent,
data: {
meta: {
title: 'Account videos'
}
}
}
]
} }
] ]

View File

@ -0,0 +1,20 @@
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<form role="form" (ngSubmit)="changePassword()" [formGroup]="form">
<label for="new-password">Change password</label>
<input
type="password" id="new-password" placeholder="New password"
formControlName="new-password" [ngClass]="{ 'input-error': formErrors['new-password'] }"
>
<div *ngIf="formErrors['new-password']" class="form-error">
{{ formErrors['new-password'] }}
</div>
<input
type="password" id="new-confirmed-password" placeholder="Confirm new password"
formControlName="new-confirmed-password"
>
<input type="submit" value="Change password" [disabled]="!form.valid">
</form>

View File

@ -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;
}

View File

@ -1,16 +1,13 @@
import { Component, OnInit } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { FormBuilder, FormGroup } from '@angular/forms' import { FormBuilder, FormGroup } from '@angular/forms'
import { Router } from '@angular/router'
import { NotificationsService } from 'angular2-notifications' import { NotificationsService } from 'angular2-notifications'
import { FormReactive, USER_PASSWORD, UserService } from '../../../shared'
import { FormReactive, UserService, USER_PASSWORD } from '../../shared'
@Component({ @Component({
selector: 'my-account-change-password', 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 { export class AccountChangePasswordComponent extends FormReactive implements OnInit {
error: string = null error: string = null

View File

@ -0,0 +1,14 @@
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form">
<input
type="checkbox" id="displayNSFW"
formControlName="displayNSFW"
>
<label for="displayNSFW">Display videos that contain mature or explicit content</label>
<div *ngIf="formErrors['displayNSFW']" class="alert alert-danger">
{{ formErrors['displayNSFW'] }}
</div>
<input type="submit" value="Save" [disabled]="!form.valid">
</form>

View File

@ -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;
}

View File

@ -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 { FormBuilder, FormGroup } from '@angular/forms'
import { Router } from '@angular/router'
import { NotificationsService } from 'angular2-notifications' import { NotificationsService } from 'angular2-notifications'
import { UserUpdateMe } from '../../../../../../shared'
import { AuthService } from '../../core' import { AuthService } from '../../../core'
import { import { FormReactive, User, UserService } from '../../../shared'
FormReactive,
User,
UserService,
USER_PASSWORD
} from '../../shared'
import { UserUpdateMe } from '../../../../../shared'
@Component({ @Component({
selector: 'my-account-details', 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 { export class AccountDetailsComponent extends FormReactive implements OnInit {

View File

@ -0,0 +1,15 @@
<div class="user">
<img [src]="getAvatarPath()" alt="Avatar" />
<div class="user-info">
<div class="user-info-username">{{ user.username }}</div>
<div class="user-info-followers">{{ user.account?.followersCount }} subscribers</div>
</div>
</div>
<div class="account-title">Account settings</div>
<my-account-change-password></my-account-change-password>
<div class="account-title">Filtering</div>
<my-account-details [user]="user"></my-account-details>

View File

@ -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;
}

View File

@ -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()
}
}

View File

@ -0,0 +1,39 @@
<div
class="videos"
infiniteScroll
[infiniteScrollDistance]="0.5"
[infiniteScrollUpDistance]="1.5"
(scrolled)="onNearOfBottom()"
(scrolledUp)="onNearOfTop()"
>
<div class="video" *ngFor="let video of videos; let i = index">
<input type="checkbox" [(ngModel)]="checkedVideos[video.id]" />
<my-video-thumbnail [video]="video"></my-video-thumbnail>
<div class="video-info">
<div class="video-info-name">{{ video.name }}</div>
<span class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
</div>
<!-- Display only once -->
<div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0">
<div class="action-selection-mode-child">
<span class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
Cancel
</span>
<span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
<span class="icon icon-delete-white"></span>
Delete
</span>
</div>
</div>
<div class="video-buttons" *ngIf="isInSelectionMode() === false">
<my-delete-button (click)="deleteVideo(video)"></my-delete-button>
<my-edit-button [routerLink]="[ '/videos', 'edit', video.uuid ]"></my-edit-button>
</div>
</div>
</div>

View File

@ -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;
}
}
}

View File

@ -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<any>[] = []
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)
}
}

View File

@ -1,25 +1,11 @@
<div class="row"> <div class="row">
<div class="content-padding"> <div class="sub-menu">
<h3>Account</h3> <a routerLink="/account/settings" routerLinkActive="active" class="title-page">My account</a>
<div class="col-md-6 col-sm-12"> <a routerLink="/account/videos" routerLinkActive="active" class="title-page">My videos</a>
<div class="panel panel-default">
<div class="panel-heading">Change password</div>
<div class="panel-body">
<my-account-change-password></my-account-change-password>
</div>
</div>
</div> </div>
<div class="col-md-6 col-sm-12"> <div class="margin-content">
<div class="panel panel-default"> <router-outlet></router-outlet>
<div class="panel-heading">Update my informations</div>
<div class="panel-body">
<my-account-details [user]="user"></my-account-details>
</div>
</div>
</div>
</div> </div>
</div> </div>

View File

@ -1,3 +0,0 @@
.panel {
margin-top: 40px;
}

View File

@ -1,28 +1,8 @@
import { Component, OnInit } from '@angular/core' import { Component } 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'
@Component({ @Component({
selector: 'my-account', selector: 'my-account',
templateUrl: './account.component.html', templateUrl: './account.component.html',
styleUrls: [ './account.component.scss' ] styleUrls: [ './account.component.scss' ]
}) })
export class AccountComponent implements OnInit { export class AccountComponent {}
user: User = null
constructor (private authService: AuthService) {}
ngOnInit () {
this.user = this.authService.getUser()
}
}

View File

@ -1,11 +1,12 @@
import { NgModule } from '@angular/core' 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 { 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({ @NgModule({
imports: [ imports: [
@ -15,8 +16,10 @@ import { SharedModule } from '../shared'
declarations: [ declarations: [
AccountComponent, AccountComponent,
AccountSettingsComponent,
AccountChangePasswordComponent, AccountChangePasswordComponent,
AccountDetailsComponent AccountDetailsComponent,
AccountVideosComponent
], ],
exports: [ exports: [

View File

@ -6,7 +6,7 @@ import { PreloadSelectedModulesList } from './core'
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
redirectTo: '/videos/list', redirectTo: '/videos/trending',
pathMatch: 'full' pathMatch: 'full'
}, },
{ {

View File

@ -1,37 +1,26 @@
<div class="container-fluid"> <div>
<div class="row header"> <div class="header">
<div class="col-md-2 col-sm-3 col-xs-3 top-left-block" [ngClass]="{ 'border-bottom': isMenuDisplayed === false }"> <div class="top-left-block" [ngClass]="{ 'border-bottom': isMenuDisplayed === false }">
<div class="hamburger-block" (click)="toggleMenu()"> <span class="icon icon-menu" (click)="toggleMenu()"></span>
<span class="glyphicon glyphicon-menu-hamburger"></span>
<a id="peertube-title" [routerLink]="['/videos/list']" title="Homepage">
<span class="icon icon-logo"></span>
PeerTube
</a>
</div> </div>
<div id="peertube-title"> <div class="header-right">
<a [routerLink]="['/videos/list']" title="Homepage"></a> <my-header></my-header>
</div> </div>
</div> </div>
<!-- Used for the fixed title --> <div class="sub-header-container">
<div class="col-md-2 col-sm-3 col-xs-3 fake-title-block"></div> <div *ngIf="isMenuDisplayed" class="title-menu-left">
<my-menu></my-menu>
<!-- We need to reset col-md-* because my-search is in fixed position -->
<my-search class="col-md-10 col-sm-9 col-xs-9"></my-search>
</div> </div>
<div class="row"> <div class="main-col container-fluid" [ngClass]="getMainColClasses()">
<div class="col-md-2 col-sm-3 col-xs-3 title-menu-left">
<div class="title-menu-left-block menu">
<my-menu *ngIf="isMenuDisplayed && isInAdmin() === false"></my-menu>
<my-menu-admin *ngIf="isMenuDisplayed && isInAdmin() === true"></my-menu-admin>
</div>
</div>
<!-- Used for the fixed menu -->
<div class="fake-menu col-md-2 col-sm-3 col-xs-3">
</div>
<div class="main-col" [ngClass]="getMainColClasses()">
<div class="main-row"> <div class="main-row">
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@ -2,10 +2,15 @@
min-height: calc(100vh - #{$header-height} - #{$footer-height} - #{$footer-margin}); min-height: calc(100vh - #{$header-height} - #{$footer-height} - #{$footer-margin});
} }
.sub-header-container {
margin-top: $header-height;
}
.title-menu-left { .title-menu-left {
position: fixed; position: fixed;
height: calc(100vh - #{$header-height}); height: calc(100vh - #{$header-height});
padding: 0; padding: 0;
width: $menu-width;
.title-menu-left-block.menu { .title-menu-left-block.menu {
height: 100%; height: 100%;
@ -14,125 +19,62 @@
.header { .header {
height: $header-height; height: $header-height;
position: fixed;
.fake-title-block { top: 0;
display: inline-block; width: 100%;
} background-color: #fff;
z-index: 1000;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.16);
display: flex;
.top-left-block { .top-left-block {
z-index: 100; width: $menu-width;
background-color: #fff; z-index: 1001;
border-right: 1px solid $header-border-color;
height: $header-height; height: $header-height;
line-height: $header-height;
margin-top: 0;
margin-bottom: 0;
display: flex; display: flex;
position: fixed; align-items: center;
padding: 0;
&.border-bottom { .icon {
border-bottom: 1px solid $header-border-color; @include icon(22px);
}
.hamburger-block { &.icon-menu {
margin-right: 15px; background-image: url('../assets/images/header/menu.svg');
margin-left: 15px; margin: 0 18px 0 24px;
.glyphicon {
cursor: pointer;
position: relative;
top: 4px;
} }
} }
#peertube-title { #peertube-title {
a { font-size: 20px;
font-weight: $font-bold;
color: inherit !important; color: inherit !important;
display: block; display: flex;
background: url('../assets/logo.png') no-repeat; align-items: center;
background-size: contain;
background-position: center;
height: 100%;
margin: auto;
width: 135px;
&:hover { @include disable-default-a-behaviour;
color: inherit !important;
text-decoration: none !important; .icon.icon-logo {
} display: inline-block;
background: url('../assets/images/logo.svg') no-repeat;
width: 23px;
height: 24px;
} }
} }
@media screen and (max-width: 500px) { @media screen and (max-width: 500px) {
width: 70px;
#peertube-title { #peertube-title {
display: none; display: none;
} }
.hamburger-block {
width: 100%;
text-align: center;
} }
} }
@media screen and (min-width: 500px) and (max-width: 600px) { .header-right {
#peertube-title a { height: $header-height;
width: 80px; display: flex;
} align-items: center;
} flex-grow: 1;
justify-content: flex-end;
@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;
} }
} }

View File

@ -1,8 +1,6 @@
import { Component, OnInit } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { AuthService, ServerService } from './core' import { AuthService, ServerService } from './core'
import { UserService } from './shared'
@Component({ @Component({
selector: 'my-app', selector: 'my-app',
@ -62,20 +60,9 @@ export class AppComponent implements OnInit {
} }
getMainColClasses () { getMainColClasses () {
const colSizes = {
md: 10,
sm: 9,
xs: 9
}
// Take all width is the menu is not displayed // Take all width is the menu is not displayed
if (this.isMenuDisplayed === false) { if (this.isMenuDisplayed === false) return [ 'expanded' ]
Object.keys(colSizes).forEach(col => colSizes[col] = 12)
}
const classes = [] return []
Object.keys(colSizes).forEach(col => classes.push(`col-${col}-${colSizes[col]}`))
return classes
} }
} }

View File

@ -20,6 +20,8 @@ import { LoginModule } from './login'
import { SignupModule } from './signup' import { SignupModule } from './signup'
import { SharedModule } from './shared' import { SharedModule } from './shared'
import { VideosModule } from './videos' import { VideosModule } from './videos'
import { MenuComponent } from './menu'
import { HeaderComponent } from './header'
export function metaFactory (): MetaLoader { export function metaFactory (): MetaLoader {
return new MetaStaticLoader({ return new MetaStaticLoader({
@ -47,7 +49,10 @@ const APP_PROVIDERS = [
@NgModule({ @NgModule({
bootstrap: [ AppComponent ], bootstrap: [ AppComponent ],
declarations: [ declarations: [
AppComponent AppComponent,
MenuComponent,
HeaderComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -1,29 +1,24 @@
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { Observable } from 'rxjs/Observable'
import { Subject } from 'rxjs/Subject' import { NotificationsService } from 'angular2-notifications'
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' import 'rxjs/add/observable/throw'
import { ReplaySubject } from 'rxjs/ReplaySubject'
import 'rxjs/add/operator/do' import 'rxjs/add/operator/do'
import 'rxjs/add/operator/map' import 'rxjs/add/operator/map'
import 'rxjs/add/operator/mergeMap' import 'rxjs/add/operator/mergeMap'
import 'rxjs/add/observable/throw' import { Observable } from 'rxjs/Observable'
import { ReplaySubject } from 'rxjs/ReplaySubject'
import { NotificationsService } from 'angular2-notifications' 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 { AuthStatus } from './auth-status.model'
import { AuthUser } from './auth-user.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 { interface UserLoginWithUsername extends UserLogin {
access_token: string access_token: string
@ -42,10 +37,7 @@ interface UserLoginWithUserInformation extends UserLogin {
displayNSFW: boolean displayNSFW: boolean
email: string email: string
videoQuota: number videoQuota: number
account: { account: Account
id: number
uuid: string
}
videoChannels: VideoChannel[] videoChannels: VideoChannel[]
} }
@ -177,19 +169,15 @@ export class AuthService {
return this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers }) return this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers })
.map(res => this.handleRefreshToken(res)) .map(res => this.handleRefreshToken(res))
.catch(res => { .catch(err => {
// The refresh token is invalid? console.error(err)
if (res.status === 400 && res.error.error === 'invalid_grant') { console.log('Cannot refresh token -> logout...')
console.error('Cannot refresh token -> logout...')
this.logout() this.logout()
this.router.navigate(['/login']) this.router.navigate(['/login'])
return Observable.throw({ return Observable.throw({
error: 'You need to reconnect.' error: 'You need to reconnect.'
}) })
}
return this.restExtractor.handleError(res)
}) })
} }
@ -202,7 +190,6 @@ export class AuthService {
} }
this.mergeUserInformation(obj) this.mergeUserInformation(obj)
.do(() => this.userInformationLoaded.next(true))
.subscribe( .subscribe(
res => { res => {
this.user.displayNSFW = res.displayNSFW this.user.displayNSFW = res.displayNSFW
@ -211,6 +198,8 @@ export class AuthService {
this.user.account = res.account this.user.account = res.account
this.user.save() this.user.save()
this.userInformationLoaded.next(true)
} }
) )
} }

View File

@ -6,14 +6,14 @@
<button type="button" class="close" aria-label="Close" (click)="cancel()"> <button type="button" class="close" aria-label="Close" (click)="cancel()">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
<h4 class="modal-title">{{ title }}</h4> <h4 class="title-page title-page-single">{{ title }}</h4>
</div> </div>
<div class="modal-body" [innerHtml]="message"></div> <div class="modal-body" [innerHtml]="message"></div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal" (click)="cancel()">Cancel</button> <button type="button" class="grey-button" data-dismiss="modal" (click)="cancel()">Cancel</button>
<button type="button" class="btn btn-primary" (click)="confirm()">Confirm</button> <button type="button" class="orange-button" (click)="confirm()">Confirm</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -11,7 +11,8 @@ export interface ConfigChangedEvent {
@Component({ @Component({
selector: 'my-confirm', selector: 'my-confirm',
templateUrl: './confirm.component.html' templateUrl: './confirm.component.html',
styles: [ '.button { padding: 0 13px; }' ]
}) })
export class ConfirmComponent implements OnInit { export class ConfirmComponent implements OnInit {
@ViewChild('confirmModal') confirmModal: ModalDirective @ViewChild('confirmModal') confirmModal: ModalDirective

View File

@ -26,17 +26,13 @@ import { throwIfAlreadyLoaded } from './module-import-guard'
], ],
declarations: [ declarations: [
ConfirmComponent, ConfirmComponent
MenuComponent,
MenuAdminComponent
], ],
exports: [ exports: [
SimpleNotificationsModule, SimpleNotificationsModule,
ConfirmComponent, ConfirmComponent
MenuComponent,
MenuAdminComponent
], ],
providers: [ providers: [

View File

@ -1,6 +1,5 @@
export * from './auth' export * from './auth'
export * from './server' export * from './server'
export * from './confirm' export * from './confirm'
export * from './menu'
export * from './routing' export * from './routing'
export * from './core.module' export * from './core.module'

View File

@ -1,2 +0,0 @@
export * from './menu.component'
export * from './menu-admin.component'

View File

@ -1,35 +0,0 @@
<menu>
<div class="panel-block">
<a *ngIf="hasUsersRight()" routerLink="/admin/users" routerLinkActive="active">
<span class="hidden-xs glyphicon glyphicon-user"></span>
List users
</a>
<a *ngIf="hasServerFollowRight()" routerLink="/admin/follows" routerLinkActive="active">
<span class="hidden-xs glyphicon glyphicon-cloud"></span>
Manage follows
</a>
<a *ngIf="hasVideoAbusesRight()" routerLink="/admin/video-abuses" routerLinkActive="active">
<span class="hidden-xs glyphicon glyphicon-alert"></span>
Video abuses
</a>
<a *ngIf="hasVideoBlacklistRight()" routerLink="/admin/video-blacklist" routerLinkActive="active">
<span class="hidden-xs glyphicon glyphicon-eye-close"></span>
Video blacklist
</a>
<a *ngIf="hasJobsRight()" routerLink="/admin/jobs" routerLinkActive="active">
<span class="hidden-xs glyphicon glyphicon-tasks"></span>
Jobs
</a>
</div>
<div class="panel-block">
<a routerLink="/videos/list" routerLinkActive="active">
<span class="hidden-xs glyphicon glyphicon-cog"></span>
Quit admin.
</a>
</div>
</menu>

View File

@ -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)
}
}

View File

@ -1,55 +0,0 @@
<menu>
<div class="panel-block">
<div class="block-title">Account</div>
<div id="panel-user-login" class="panel-button">
<a *ngIf="!isLoggedIn" routerLink="/login" routerLinkActive="active">
<span class="hidden-xs glyphicon glyphicon-log-in"></span>
Login
</a>
<a *ngIf="isLoggedIn" (click)="logout()">
<span class="hidden-xs glyphicon glyphicon-log-out"></span>
Logout
</a>
</div>
<a *ngIf="!isLoggedIn && isRegistrationAllowed()" routerLink="/signup" routerLinkActive="active">
<span class="hidden-xs glyphicon glyphicon-user"></span>
Signup
</a>
<a *ngIf="isLoggedIn" routerLink="/account" routerLinkActive="active">
<span class="hidden-xs glyphicon glyphicon-user"></span>
My account
</a>
<a *ngIf="isLoggedIn" routerLink="/videos/mine" routerLinkActive="active">
<span class="hidden-xs glyphicon glyphicon-folder-open"></span>
My videos
</a>
</div>
<div class="panel-block">
<div class="block-title">Videos</div>
<a routerLink="/videos/list" routerLinkActive="active">
<span class="hidden-xs glyphicon glyphicon-list"></span>
See videos
</a>
<a *ngIf="isLoggedIn" routerLink="/videos/upload" routerLinkActive="active">
<span class="hidden-xs glyphicon glyphicon-cloud-upload"></span>
Upload a video
</a>
</div>
<div *ngIf="userHasAdminAccess" class="panel-block">
<div class="block-title">Other</div>
<a [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active">
<span class="hidden-xs glyphicon glyphicon-cog"></span>
Administration
</a>
</div>
</menu>

View File

@ -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;
}
}
}

View File

@ -1,5 +1,7 @@
import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http' 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' import { ServerConfig } from '../../../../../shared'
@ -8,6 +10,11 @@ export class ServerService {
private static BASE_CONFIG_URL = API_URL + '/api/v1/config/' private static BASE_CONFIG_URL = API_URL + '/api/v1/config/'
private static BASE_VIDEO_URL = API_URL + '/api/v1/videos/' private static BASE_VIDEO_URL = API_URL + '/api/v1/videos/'
videoPrivaciesLoaded = new ReplaySubject<boolean>(1)
videoCategoriesLoaded = new ReplaySubject<boolean>(1)
videoLicencesLoaded = new ReplaySubject<boolean>(1)
videoLanguagesLoaded = new ReplaySubject<boolean>(1)
private config: ServerConfig = { private config: ServerConfig = {
signup: { signup: {
allowed: false allowed: false
@ -29,19 +36,19 @@ export class ServerService {
} }
loadVideoCategories () { loadVideoCategories () {
return this.loadVideoAttributeEnum('categories', this.videoCategories) return this.loadVideoAttributeEnum('categories', this.videoCategories, this.videoCategoriesLoaded)
} }
loadVideoLicences () { loadVideoLicences () {
return this.loadVideoAttributeEnum('licences', this.videoLicences) return this.loadVideoAttributeEnum('licences', this.videoLicences, this.videoLicencesLoaded)
} }
loadVideoLanguages () { loadVideoLanguages () {
return this.loadVideoAttributeEnum('languages', this.videoLanguages) return this.loadVideoAttributeEnum('languages', this.videoLanguages, this.videoLanguagesLoaded)
} }
loadVideoPrivacies () { loadVideoPrivacies () {
return this.loadVideoAttributeEnum('privacies', this.videoPrivacies) return this.loadVideoAttributeEnum('privacies', this.videoPrivacies, this.videoPrivaciesLoaded)
} }
getConfig () { getConfig () {
@ -66,7 +73,8 @@ export class ServerService {
private loadVideoAttributeEnum ( private loadVideoAttributeEnum (
attributeName: 'categories' | 'licences' | 'languages' | 'privacies', attributeName: 'categories' | 'licences' | 'languages' | 'privacies',
hashToPopulate: { id: number, label: string }[] hashToPopulate: { id: number, label: string }[],
notifier: ReplaySubject<boolean>
) { ) {
return this.http.get(ServerService.BASE_VIDEO_URL + attributeName) return this.http.get(ServerService.BASE_VIDEO_URL + attributeName)
.subscribe(data => { .subscribe(data => {
@ -77,6 +85,8 @@ export class ServerService {
label: data[dataKey] label: data[dataKey]
}) })
}) })
notifier.next(true)
}) })
} }
} }

View File

@ -0,0 +1,10 @@
<input
type="text" id="search-video" name="search-video" placeholder="Search..."
[(ngModel)]="searchValue" (keyup.enter)="doSearch()"
>
<span (click)="doSearch()" class="icon icon-search"></span>
<a class="upload-button" routerLink="/videos/upload">
<span class="icon icon-upload"></span>
<span class="upload-button-label">Upload</span>
</a>

View File

@ -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;
}
}
}

View File

@ -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 }
})
}
}

View File

@ -0,0 +1 @@
export * from './header.component'

View File

@ -1,7 +1,7 @@
<div class="row"> <div class="margin-content">
<div class="content-padding"> <div class="title-page title-page-single">
Login
<h3>Login</h3> </div>
<div *ngIf="error" class="alert alert-danger">{{ error }}</div> <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
@ -9,10 +9,10 @@
<div class="form-group"> <div class="form-group">
<label for="username">Username</label> <label for="username">Username</label>
<input <input
type="text" class="form-control" id="username" placeholder="Username" required type="text" id="username" placeholder="Username" required
formControlName="username" formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }"
> >
<div *ngIf="formErrors.username" class="alert alert-danger"> <div *ngIf="formErrors.username" class="form-error">
{{ formErrors.username }} {{ formErrors.username }}
</div> </div>
</div> </div>
@ -20,15 +20,14 @@
<div class="form-group"> <div class="form-group">
<label for="password">Password</label> <label for="password">Password</label>
<input <input
type="password" class="form-control" name="password" id="password" placeholder="Password" required type="password" name="password" id="password" placeholder="Password" required
formControlName="password" formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
> >
<div *ngIf="formErrors.password" class="alert alert-danger"> <div *ngIf="formErrors.password" class="form-error">
{{ formErrors.password }} {{ formErrors.password }}
</div> </div>
</div> </div>
<input type="submit" value="Login" class="btn btn-default" [disabled]="!form.valid"> <input type="submit" value="Login" [disabled]="!form.valid">
</form> </form>
</div>
</div> </div>

View File

@ -0,0 +1,9 @@
input:not([type=submit]) {
@include peertube-input-text(340px);
display: block;
}
input[type=submit] {
@include peertube-button;
@include orange-button;
}

View File

@ -7,7 +7,8 @@ import { FormReactive } from '../shared'
@Component({ @Component({
selector: 'my-login', selector: 'my-login',
templateUrl: './login.component.html' templateUrl: './login.component.html',
styleUrls: [ './login.component.scss' ]
}) })
export class LoginComponent extends FormReactive implements OnInit { export class LoginComponent extends FormReactive implements OnInit {

View File

@ -0,0 +1 @@
export * from './menu.component'

View File

@ -0,0 +1,50 @@
<menu>
<div *ngIf="isLoggedIn" class="logged-in-block">
<img [src]="getUserAvatarPath()" alt="Avatar" />
<div class="logged-in-info">
<a routerLink="/account/settings" class="logged-in-username">{{ user.username }}</a>
<div class="logged-in-email">{{ user.email }}</div>
</div>
<div class="logged-in-more" dropdown placement="right" container="body">
<span class="glyphicon glyphicon-option-vertical" dropdownToggle></span>
<ul *dropdownMenu class="dropdown-menu">
<li>
<a (click)="logout($event)" class="dropdown-item" title="Log out" href="#">
Log out
</a>
</li>
</ul>
</div>
</div>
<div *ngIf="!isLoggedIn" class="button-block">
<a routerLink="/login" class="login-button">Login</a>
<a *ngIf="isRegistrationAllowed()" routerLink="/signup" class="create-account-button">Create an account</a>
</div>
<div class="panel-block">
<div class="block-title">Videos</div>
<a routerLink="/videos/trending" routerLinkActive="active">
<span class="icon icon-videos-trending"></span>
Trending
</a>
<a routerLink="/videos/recently-added" routerLinkActive="active">
<span class="icon icon-videos-recently-added"></span>
Recently added
</a>
</div>
<div *ngIf="userHasAdminAccess" class="panel-block">
<div class="block-title">More</div>
<a [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active">
<span class="icon icon-administration"></span>
Administration
</a>
</div>
</menu>

View File

@ -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;
}
}
}
}
}

View File

@ -1,9 +1,8 @@
import { Component, OnInit } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { UserRight } from '../../../../shared/models/users/user-right.enum'
import { AuthService, AuthStatus } from '../auth' import { AuthService, AuthStatus, ServerService } from '../core'
import { ServerService } from '../server' import { User } from '../shared/users/user.model'
import { UserRight } from '../../../../../shared/models/users/user-right.enum'
@Component({ @Component({
selector: 'my-menu', selector: 'my-menu',
@ -11,6 +10,7 @@ import { UserRight } from '../../../../../shared/models/users/user-right.enum'
styleUrls: [ './menu.component.scss' ] styleUrls: [ './menu.component.scss' ]
}) })
export class MenuComponent implements OnInit { export class MenuComponent implements OnInit {
user: User
isLoggedIn: boolean isLoggedIn: boolean
userHasAdminAccess = false userHasAdminAccess = false
@ -29,16 +29,19 @@ export class MenuComponent implements OnInit {
ngOnInit () { ngOnInit () {
this.isLoggedIn = this.authService.isLoggedIn() this.isLoggedIn = this.authService.isLoggedIn()
if (this.isLoggedIn === true) this.user = this.authService.getUser()
this.computeIsUserHasAdminAccess() this.computeIsUserHasAdminAccess()
this.authService.loginChangedSource.subscribe( this.authService.loginChangedSource.subscribe(
status => { status => {
if (status === AuthStatus.LoggedIn) { if (status === AuthStatus.LoggedIn) {
this.isLoggedIn = true this.isLoggedIn = true
this.user = this.authService.getUser()
this.computeIsUserHasAdminAccess() this.computeIsUserHasAdminAccess()
console.log('Logged in.') console.log('Logged in.')
} else if (status === AuthStatus.LoggedOut) { } else if (status === AuthStatus.LoggedOut) {
this.isLoggedIn = false this.isLoggedIn = false
this.user = undefined
this.computeIsUserHasAdminAccess() this.computeIsUserHasAdminAccess()
console.log('Logged out.') console.log('Logged out.')
} else { } else {
@ -48,6 +51,10 @@ export class MenuComponent implements OnInit {
) )
} }
getUserAvatarPath () {
return this.user.getAvatarPath()
}
isRegistrationAllowed () { isRegistrationAllowed () {
return this.serverService.getConfig().signup.allowed return this.serverService.getConfig().signup.allowed
} }
@ -78,7 +85,9 @@ export class MenuComponent implements OnInit {
return this.routesPerRight[right] return this.routesPerRight[right]
} }
logout () { logout (event: Event) {
event.preventDefault()
this.authService.logout() this.authService.logout()
// Redirect to home page // Redirect to home page
this.router.navigate(['/videos/list']) this.router.navigate(['/videos/list'])

View File

@ -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'
}
}

View File

@ -1,14 +1,8 @@
import { FormControl } from '@angular/forms' export function validateHost (value: string) {
export function validateHost (c: FormControl) {
// Thanks to http://stackoverflow.com/a/106223 // Thanks to http://stackoverflow.com/a/106223
const HOST_REGEXP = new RegExp( 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])$' '^(([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 : { return HOST_REGEXP.test(value)
validateHost: {
valid: false
}
}
} }

View File

@ -3,8 +3,8 @@ import { Validators } from '@angular/forms'
export const VIDEO_ABUSE_REASON = { export const VIDEO_ABUSE_REASON = {
VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ], VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ],
MESSAGES: { MESSAGES: {
'required': 'Report reason name is required.', 'required': 'Report reason is required.',
'minlength': 'Report reson must be at least 2 characters long.', 'minlength': 'Report reason must be at least 2 characters long.',
'maxlength': 'Report reson cannot be more than 300 characters long.' 'maxlength': 'Report reason cannot be more than 300 characters long.'
} }
} }

View File

@ -1,5 +1,11 @@
import { Validators } from '@angular/forms' import { Validators } from '@angular/forms'
export type ValidatorMessage = {
[ id: string ]: {
[ error: string ]: string
}
}
export const VIDEO_NAME = { export const VIDEO_NAME = {
VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(120) ], VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(120) ],
MESSAGES: { MESSAGES: {
@ -17,17 +23,13 @@ export const VIDEO_PRIVACY = {
} }
export const VIDEO_CATEGORY = { export const VIDEO_CATEGORY = {
VALIDATORS: [ Validators.required ], VALIDATORS: [ ],
MESSAGES: { MESSAGES: {}
'required': 'Video category is required.'
}
} }
export const VIDEO_LICENCE = { export const VIDEO_LICENCE = {
VALIDATORS: [ Validators.required ], VALIDATORS: [ ],
MESSAGES: { MESSAGES: {}
'required': 'Video licence is required.'
}
} }
export const VIDEO_LANGUAGE = { export const VIDEO_LANGUAGE = {
@ -43,9 +45,8 @@ export const VIDEO_CHANNEL = {
} }
export const VIDEO_DESCRIPTION = { export const VIDEO_DESCRIPTION = {
VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(3000) ], VALIDATORS: [ Validators.minLength(3), Validators.maxLength(3000) ],
MESSAGES: { MESSAGES: {
'required': 'Video description is required.',
'minlength': 'Video description must be at least 3 characters long.', 'minlength': 'Video description must be at least 3 characters long.',
'maxlength': 'Video description cannot be more than 3000 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.' 'maxlength': 'A tag should be less than 30 characters long.'
} }
} }
export const VIDEO_FILE = {
VALIDATORS: [ Validators.required ],
MESSAGES: {
'required': 'Video file is required.'
}
}

View File

@ -1,7 +1,6 @@
export * from './auth' export * from './auth'
export * from './forms' export * from './forms'
export * from './rest' export * from './rest'
export * from './search'
export * from './users' export * from './users'
export * from './video-abuse' export * from './video-abuse'
export * from './video-blacklist' export * from './video-blacklist'

View File

@ -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');
}
}
}

View File

@ -0,0 +1,4 @@
<span class="action-button action-button-delete" >
<span class="icon icon-delete-grey"></span>
Delete
</span>

View File

@ -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 {
}

View File

@ -0,0 +1,4 @@
<a class="action-button" [routerLink]="routerLink">
<span class="icon icon-edit"></span>
Edit
</a>

View File

@ -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 = []
}

View File

@ -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'
}
}

View File

@ -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}`
}
}

View File

@ -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
}

View File

@ -1,4 +0,0 @@
export * from './search-field.type'
export * from './search.component'
export * from './search.model'
export * from './search.service'

View File

@ -1 +0,0 @@
export type SearchField = 'name' | 'account' | 'host' | 'tags'

View File

@ -1,22 +0,0 @@
<div class="input-group">
<span class="hidden-xs input-group-addon icon-addon">
<span class="glyphicon glyphicon-search"></span>
</span>
<input
type="text" id="search-video" name="search-video" class="form-control" placeholder="Search" class="form-control"
[(ngModel)]="searchCriteria.value" (keyup.enter)="doSearch()"
>
<div class="input-group-btn" dropdown placement="bottom right">
<button id="simple-btn-keyboard-nav" type="button" class="btn btn-default" dropdownToggle>
{{ getStringChoice(searchCriteria.field) }} <span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="simple-btn-keyboard-nav" *dropdownMenu>
<li *ngFor="let choice of choiceKeys" class="dropdown-item" role="menu-item">
<a class="dropdown-item" href="#" (click)="choose($event, choice)">{{ getStringChoice(choice) }}</a>
</li>
</ul>
</div>
</div>

View File

@ -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;
}
}

View File

@ -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]
}
}

View File

@ -1,6 +0,0 @@
import { SearchField } from './search-field.type'
export interface Search {
field: SearchField
value: string
}

View File

@ -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<Search>
updateSearch: Subject<Search>
constructor () {
this.updateSearch = new Subject<Search>()
this.searchUpdated = new ReplaySubject<Search>(1)
}
}

View File

@ -1,25 +1,29 @@
import { NgModule } from '@angular/core'
import { HttpClientModule } from '@angular/common/http'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { HttpClientModule } from '@angular/common/http'
import { NgModule } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterModule } from '@angular/router' 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 { BsDropdownModule } from 'ngx-bootstrap/dropdown'
import { ProgressbarModule } from 'ngx-bootstrap/progressbar'
import { PaginationModule } from 'ngx-bootstrap/pagination'
import { ModalModule } from 'ngx-bootstrap/modal' 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 { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
import { DataTableModule } from 'primeng/components/datatable/datatable'
import { AUTH_INTERCEPTOR_PROVIDER } from './auth' 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 { RestExtractor, RestService } from './rest'
import { SearchComponent, SearchService } from './search'
import { UserService } from './users' import { UserService } from './users'
import { VideoAbuseService } from './video-abuse' import { VideoAbuseService } from './video-abuse'
import { VideoBlacklistService } from './video-blacklist' 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({ @NgModule({
imports: [ imports: [
@ -31,18 +35,21 @@ import { LoaderComponent } from './misc/loader.component'
BsDropdownModule.forRoot(), BsDropdownModule.forRoot(),
ModalModule.forRoot(), ModalModule.forRoot(),
PaginationModule.forRoot(),
ProgressbarModule.forRoot(),
DataTableModule, DataTableModule,
PrimeSharedModule PrimeSharedModule,
InfiniteScrollModule,
NgPipesModule
], ],
declarations: [ declarations: [
BytesPipe, LoaderComponent,
KeysPipe, VideoThumbnailComponent,
SearchComponent, VideoMiniatureComponent,
LoaderComponent DeleteButtonComponent,
EditButtonComponent,
NumberFormatterPipe,
FromNowPipe
], ],
exports: [ exports: [
@ -54,25 +61,30 @@ import { LoaderComponent } from './misc/loader.component'
BsDropdownModule, BsDropdownModule,
ModalModule, ModalModule,
PaginationModule,
ProgressbarModule,
DataTableModule, DataTableModule,
PrimeSharedModule, PrimeSharedModule,
InfiniteScrollModule,
BytesPipe, BytesPipe,
KeysPipe, KeysPipe,
SearchComponent, LoaderComponent,
LoaderComponent VideoThumbnailComponent,
VideoMiniatureComponent,
DeleteButtonComponent,
EditButtonComponent,
NumberFormatterPipe,
FromNowPipe
], ],
providers: [ providers: [
AUTH_INTERCEPTOR_PROVIDER, AUTH_INTERCEPTOR_PROVIDER,
RestExtractor, RestExtractor,
RestService, RestService,
SearchService,
VideoAbuseService, VideoAbuseService,
VideoBlacklistService, VideoBlacklistService,
UserService UserService,
VideoService
] ]
}) })
export class SharedModule { } export class SharedModule { }

View File

@ -1,10 +1,5 @@
import { import { hasUserRight, User as UserServerModel, UserRight, UserRole, VideoChannel } from '../../../../../shared'
User as UserServerModel, import { Account } from '../account/account.model'
UserRole,
VideoChannel,
UserRight,
hasUserRight
} from '../../../../../shared'
export type UserConstructorHash = { export type UserConstructorHash = {
id: number, id: number,
@ -14,10 +9,7 @@ export type UserConstructorHash = {
videoQuota?: number, videoQuota?: number,
displayNSFW?: boolean, displayNSFW?: boolean,
createdAt?: Date, createdAt?: Date,
account?: { account?: Account,
id: number
uuid: string
},
videoChannels?: VideoChannel[] videoChannels?: VideoChannel[]
} }
export class User implements UserServerModel { export class User implements UserServerModel {
@ -27,10 +19,7 @@ export class User implements UserServerModel {
role: UserRole role: UserRole
displayNSFW: boolean displayNSFW: boolean
videoQuota: number videoQuota: number
account: { account: Account
id: number
uuid: string
}
videoChannels: VideoChannel[] videoChannels: VideoChannel[]
createdAt: Date createdAt: Date
@ -61,4 +50,8 @@ export class User implements UserServerModel {
hasRight (right: UserRight) { hasRight (right: UserRight) {
return hasUserRight(this.role, right) return hasUserRight(this.role, right)
} }
getAvatarPath () {
return Account.GET_ACCOUNT_AVATAR_PATH(this.account)
}
} }

Some files were not shown because too many files have changed in this diff Show More