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
Inspirations from:
By [Olivier Massain](https://twitter.com/omassain)
* [Aurélien Salomon](https://dribbble.com/shots/1338727-Youtube-Redesign)
* [Wojciech Zieliński](https://dribbble.com/shots/3000315-youtube-concept)
Video.js theme:
* [zanechua](https://github.com/zanechua/videojs-sublime-inspired-skin)
Icons from [Robbie Pearce](https://robbiepearce.com/softies/)
# Fonts

View File

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

View File

@ -13,6 +13,7 @@ const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin')
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin')
const InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin')
const ngcWebpack = require('ngc-webpack')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const WebpackNotifierPlugin = require('webpack-notifier')
@ -146,14 +147,15 @@ module.exports = function (options) {
loader: 'sass-resources-loader',
options: {
resources: [
helpers.root('src/sass/_variables.scss')
helpers.root('src/sass/_variables.scss'),
helpers.root('src/sass/_mixins.scss')
]
}
}
]
},
{ test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: 'url-loader?limit=10000&minetype=application/font-woff' },
{ test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: 'file-loader' },
{ test: /\.(otf|ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: 'url-loader?limit=10000' },
/* Raw loader support for *.html
* Returns file content as string
@ -266,6 +268,17 @@ module.exports = function (options) {
inject: 'body'
}),
new CopyWebpackPlugin([
{
from: helpers.root('src/assets/images/favicon.png'),
to: 'assets/images/favicon.png'
},
{
from: helpers.root('src/assets/images/default-avatar.png'),
to: 'assets/images/default-avatar.png'
}
]),
/*
* Plugin: ScriptExtHtmlWebpackPlugin
* Description: Enhances html-webpack-plugin functionality
@ -289,6 +302,7 @@ module.exports = function (options) {
*/
new LoaderOptionsPlugin({
options: {
context: '',
sassLoader: {
precision: 10,
includePaths: [ helpers.root('src/sass') ]

View File

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

View File

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

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 { UserRight } from '../../../../shared'
import { AuthService } from '../core/auth/auth.service'
@Component({
template: '<router-outlet></router-outlet>'
templateUrl: './admin.component.html',
styleUrls: [ './admin.component.scss' ]
})
export class AdminComponent {
constructor (private auth: AuthService) {}
hasUsersRight () {
return this.auth.getUser().hasRight(UserRight.MANAGE_USERS)
}
hasServerFollowRight () {
return this.auth.getUser().hasRight(UserRight.MANAGE_SERVER_FOLLOW)
}
hasVideoAbusesRight () {
return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES)
}
hasVideoBlacklistRight () {
return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
}
hasJobsRight () {
return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS)
}
}

View File

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

View File

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

View File

@ -1,35 +1,22 @@
<div class="row">
<div class="content-padding">
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<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 class="form-group" *ngFor="let host of hosts; let id = index; trackBy:customTrackBy">
<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 *ngIf="canMakeFriends() === 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.
</div>
<input type="submit" value="Add following" class="btn btn-default" [disabled]="!isFormValid()">
</form>
<div *ngIf="hostsError" class="form-error">
{{ hostsError }}
</div>
</div>
</div>
<div *ngIf="httpEnabled() === false" class="alert alert-warning">
It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
</div>
<input type="submit" value="Add following" [disabled]="hostsError || !hostsString" class="btn btn-default">
</form>

View File

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

View File

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

View File

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

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>
<tab *ngFor="let link of links">
<ng-template tabHeading>
@ -8,4 +10,6 @@
</tabset>
</div>
<router-outlet></router-outlet>

View File

@ -1,21 +1,4 @@
.follows-menu {
margin-top: 20px;
}
tabset /deep/ {
.nav-link {
padding: 0;
}
.tab-link {
display: block;
text-align: center;
height: 40px;
width: 120px;
line-height: 40px;
&:hover, &:active, &:focus {
text-decoration: none !important;
}
}
.admin-sub-title {
flex-grow: 0;
margin-right: 30px;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,73 +1,68 @@
<div class="row">
<div class="content-padding">
<div class="admin-sub-title" *ngIf="isCreation() === true">Add user</div>
<div class="admin-sub-title" *ngIf="isCreation() === false">Edit user {{ username }}</div>
<h3 *ngIf="isCreation() === true">Add user</h3>
<h3 *ngIf="isCreation() === false">Edit user {{ username }}</h3>
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
<div class="form-group" *ngIf="isCreation()">
<label for="username">Username</label>
<input
type="text" class="form-control" id="username" placeholder="john"
formControlName="username"
>
<div *ngIf="formErrors.username" class="alert alert-danger">
{{ formErrors.username }}
</div>
</div>
<div class="form-group">
<label for="email">Email</label>
<input
type="text" class="form-control" id="email" placeholder="mail@example.com"
formControlName="email"
>
<div *ngIf="formErrors.email" class="alert alert-danger">
{{ formErrors.email }}
</div>
</div>
<div class="form-group" *ngIf="isCreation()">
<label for="password">Password</label>
<input
type="password" class="form-control" id="password"
formControlName="password"
>
<div *ngIf="formErrors.password" class="alert alert-danger">
{{ formErrors.password }}
</div>
</div>
<div class="form-group">
<label for="role">Role</label>
<select class="form-control" id="role" formControlName="role">
<option *ngFor="let role of roles" [value]="role.value">
{{ role.label }}
</option>
</select>
<div *ngIf="formErrors.role" class="alert alert-danger">
{{ formErrors.role }}
</div>
</div>
<div class="form-group">
<label for="videoQuota">Video quota</label>
<select class="form-control" id="videoQuota" formControlName="videoQuota">
<option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
{{ videoQuotaOption.label }}
</option>
</select>
<div class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
Transcoding is enabled on server. The video quota only take in account <strong>original</strong> video. <br />
In maximum, this user could use ~ {{ computeQuotaWithTranscoding() | bytes }}.
</div>
</div>
<input type="submit" value="{{ getFormButtonTitle() }}" class="btn btn-default" [disabled]="!form.valid">
</form>
<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
<div class="form-group" *ngIf="isCreation()">
<label for="username">Username</label>
<input
type="text" class="form-control" id="username" placeholder="john"
formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }"
>
<div *ngIf="formErrors.username" class="form-error">
{{ formErrors.username }}
</div>
</div>
</div>
<div class="form-group">
<label for="email">Email</label>
<input
type="text" class="form-control" id="email" placeholder="mail@example.com"
formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }"
>
<div *ngIf="formErrors.email" class="form-error">
{{ formErrors.email }}
</div>
</div>
<div class="form-group" *ngIf="isCreation()">
<label for="password">Password</label>
<input
type="password" class="form-control" id="password"
formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
>
<div *ngIf="formErrors.password" class="form-error">
{{ formErrors.password }}
</div>
</div>
<div class="form-group">
<label for="role">Role</label>
<select class="form-control" id="role" formControlName="role">
<option *ngFor="let role of roles" [value]="role.value">
{{ role.label }}
</option>
</select>
<div *ngIf="formErrors.role" class="form-error">
{{ formErrors.role }}
</div>
</div>
<div class="form-group">
<label for="videoQuota">Video quota</label>
<select class="form-control" id="videoQuota" formControlName="videoQuota">
<option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
{{ videoQuotaOption.label }}
</option>
</select>
<div class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
Transcoding is enabled on server. The video quota only take in account <strong>original</strong> video. <br />
In maximum, this user could use ~ {{ computeQuotaWithTranscoding() | bytes }}.
</div>
</div>
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
</form>

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,8 @@ import { VideoAbuse } from '../../../../../../shared'
@Component({
selector: 'my-video-abuse-list',
templateUrl: './video-abuse-list.component.html'
templateUrl: './video-abuse-list.component.html',
styleUrls: [ './video-abuse-list.component.scss']
})
export class VideoAbuseListComponent extends RestTable implements OnInit {
videoAbuses: VideoAbuse[] = []

View File

@ -18,7 +18,7 @@
<p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
<p-column header="Delete" styleClass="action-cell">
<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>
</p-column>
</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 { AccountComponent } from './account.component'
import { AccountSettingsComponent } from './account-settings/account-settings.component'
import { AccountVideosComponent } from './account-videos/account-videos.component'
const accountRoutes: Routes = [
{
path: 'account',
component: AccountComponent,
canActivate: [ MetaGuard, LoginGuard ],
data: {
meta: {
title: 'My account'
canActivateChild: [ MetaGuard, LoginGuard ],
children: [
{
path: 'settings',
component: AccountSettingsComponent,
data: {
meta: {
title: 'Account settings'
}
}
},
{
path: 'videos',
component: AccountVideosComponent,
data: {
meta: {
title: 'Account videos'
}
}
}
}
]
}
]

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 { FormBuilder, FormGroup } from '@angular/forms'
import { Router } from '@angular/router'
import { NotificationsService } from 'angular2-notifications'
import { FormReactive, UserService, USER_PASSWORD } from '../../shared'
import { FormReactive, USER_PASSWORD, UserService } from '../../../shared'
@Component({
selector: 'my-account-change-password',
templateUrl: './account-change-password.component.html'
templateUrl: './account-change-password.component.html',
styleUrls: [ './account-change-password.component.scss' ]
})
export class AccountChangePasswordComponent extends FormReactive implements OnInit {
error: string = null

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

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="content-padding">
<h3>Account</h3>
<div class="sub-menu">
<a routerLink="/account/settings" routerLinkActive="active" class="title-page">My account</a>
<div class="col-md-6 col-sm-12">
<div class="panel panel-default">
<div class="panel-heading">Change password</div>
<a routerLink="/account/videos" routerLinkActive="active" class="title-page">My videos</a>
</div>
<div class="panel-body">
<my-account-change-password></my-account-change-password>
</div>
</div>
</div>
<div class="col-md-6 col-sm-12">
<div class="panel panel-default">
<div class="panel-heading">Update my informations</div>
<div class="panel-body">
<my-account-details [user]="user"></my-account-details>
</div>
</div>
</div>
<div class="margin-content">
<router-outlet></router-outlet>
</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 { FormBuilder, FormGroup } from '@angular/forms'
import { Router } from '@angular/router'
import { NotificationsService } from 'angular2-notifications'
import { AuthService } from '../core'
import {
FormReactive,
User,
UserService,
USER_PASSWORD
} from '../shared'
import { Component } from '@angular/core'
@Component({
selector: 'my-account',
templateUrl: './account.component.html',
styleUrls: [ './account.component.scss' ]
})
export class AccountComponent implements OnInit {
user: User = null
constructor (private authService: AuthService) {}
ngOnInit () {
this.user = this.authService.getUser()
}
}
export class AccountComponent {}

View File

@ -1,11 +1,12 @@
import { NgModule } from '@angular/core'
import { AccountRoutingModule } from './account-routing.module'
import { AccountComponent } from './account.component'
import { AccountChangePasswordComponent } from './account-change-password'
import { AccountDetailsComponent } from './account-details'
import { AccountService } from './account.service'
import { SharedModule } from '../shared'
import { AccountRoutingModule } from './account-routing.module'
import { AccountChangePasswordComponent } from './account-settings/account-change-password/account-change-password.component'
import { AccountDetailsComponent } from './account-settings/account-details/account-details.component'
import { AccountSettingsComponent } from './account-settings/account-settings.component'
import { AccountComponent } from './account.component'
import { AccountService } from './account.service'
import { AccountVideosComponent } from './account-videos/account-videos.component'
@NgModule({
imports: [
@ -15,8 +16,10 @@ import { SharedModule } from '../shared'
declarations: [
AccountComponent,
AccountSettingsComponent,
AccountChangePasswordComponent,
AccountDetailsComponent
AccountDetailsComponent,
AccountVideosComponent
],
exports: [

View File

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

View File

@ -1,37 +1,26 @@
<div class="container-fluid">
<div class="row header">
<div>
<div class="header">
<div class="col-md-2 col-sm-3 col-xs-3 top-left-block" [ngClass]="{ 'border-bottom': isMenuDisplayed === false }">
<div class="hamburger-block" (click)="toggleMenu()">
<span class="glyphicon glyphicon-menu-hamburger"></span>
</div>
<div class="top-left-block" [ngClass]="{ 'border-bottom': isMenuDisplayed === false }">
<span class="icon icon-menu" (click)="toggleMenu()"></span>
<div id="peertube-title">
<a [routerLink]="['/videos/list']" title="Homepage"></a>
</div>
<a id="peertube-title" [routerLink]="['/videos/list']" title="Homepage">
<span class="icon icon-logo"></span>
PeerTube
</a>
</div>
<!-- Used for the fixed title -->
<div class="col-md-2 col-sm-3 col-xs-3 fake-title-block"></div>
<!-- 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 class="header-right">
<my-header></my-header>
</div>
</div>
<div class="row">
<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 class="sub-header-container">
<div *ngIf="isMenuDisplayed" class="title-menu-left">
<my-menu></my-menu>
</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-col container-fluid" [ngClass]="getMainColClasses()">
<div class="main-row">
<router-outlet></router-outlet>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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,34 +1,33 @@
<div class="row">
<div class="content-padding">
<h3>Login</h3>
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<form role="form" (ngSubmit)="login()" [formGroup]="form">
<div class="form-group">
<label for="username">Username</label>
<input
type="text" class="form-control" id="username" placeholder="Username" required
formControlName="username"
>
<div *ngIf="formErrors.username" class="alert alert-danger">
{{ formErrors.username }}
</div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password" class="form-control" name="password" id="password" placeholder="Password" required
formControlName="password"
>
<div *ngIf="formErrors.password" class="alert alert-danger">
{{ formErrors.password }}
</div>
</div>
<input type="submit" value="Login" class="btn btn-default" [disabled]="!form.valid">
</form>
<div class="margin-content">
<div class="title-page title-page-single">
Login
</div>
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<form role="form" (ngSubmit)="login()" [formGroup]="form">
<div class="form-group">
<label for="username">Username</label>
<input
type="text" id="username" placeholder="Username" required
formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }"
>
<div *ngIf="formErrors.username" class="form-error">
{{ formErrors.username }}
</div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password" name="password" id="password" placeholder="Password" required
formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
>
<div *ngIf="formErrors.password" class="form-error">
{{ formErrors.password }}
</div>
</div>
<input type="submit" value="Login" [disabled]="!form.valid">
</form>
</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({
selector: 'my-login',
templateUrl: './login.component.html'
templateUrl: './login.component.html',
styleUrls: [ './login.component.scss' ]
})
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 { Router } from '@angular/router'
import { AuthService, AuthStatus } from '../auth'
import { ServerService } from '../server'
import { UserRight } from '../../../../../shared/models/users/user-right.enum'
import { UserRight } from '../../../../shared/models/users/user-right.enum'
import { AuthService, AuthStatus, ServerService } from '../core'
import { User } from '../shared/users/user.model'
@Component({
selector: 'my-menu',
@ -11,6 +10,7 @@ import { UserRight } from '../../../../../shared/models/users/user-right.enum'
styleUrls: [ './menu.component.scss' ]
})
export class MenuComponent implements OnInit {
user: User
isLoggedIn: boolean
userHasAdminAccess = false
@ -29,16 +29,19 @@ export class MenuComponent implements OnInit {
ngOnInit () {
this.isLoggedIn = this.authService.isLoggedIn()
if (this.isLoggedIn === true) this.user = this.authService.getUser()
this.computeIsUserHasAdminAccess()
this.authService.loginChangedSource.subscribe(
status => {
if (status === AuthStatus.LoggedIn) {
this.isLoggedIn = true
this.user = this.authService.getUser()
this.computeIsUserHasAdminAccess()
console.log('Logged in.')
} else if (status === AuthStatus.LoggedOut) {
this.isLoggedIn = false
this.user = undefined
this.computeIsUserHasAdminAccess()
console.log('Logged out.')
} else {
@ -48,6 +51,10 @@ export class MenuComponent implements OnInit {
)
}
getUserAvatarPath () {
return this.user.getAvatarPath()
}
isRegistrationAllowed () {
return this.serverService.getConfig().signup.allowed
}
@ -78,7 +85,9 @@ export class MenuComponent implements OnInit {
return this.routesPerRight[right]
}
logout () {
logout (event: Event) {
event.preventDefault()
this.authService.logout()
// Redirect to home page
this.router.navigate(['/videos/list'])

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 (c: FormControl) {
export function validateHost (value: string) {
// Thanks to http://stackoverflow.com/a/106223
const HOST_REGEXP = new RegExp(
'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$'
)
return HOST_REGEXP.test(c.value) ? null : {
validateHost: {
valid: false
}
}
return HOST_REGEXP.test(value)
}

View File

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

View File

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

View File

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

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

View File

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

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