Merge branch 'feature/design' into develop
This commit is contained in:
commit
fada8d7555
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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') ]
|
||||
|
|
|
@ -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')
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
.btn {
|
||||
margin-top: 10px;
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
pre {
|
||||
font-size: 13px;
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
/deep/ a {
|
||||
|
||||
&, &:hover, &:active, &:focus {
|
||||
color: #000;
|
||||
}
|
||||
}
|
|
@ -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[] = []
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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 {
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
.panel {
|
||||
margin-top: 40px;
|
||||
}
|
|
@ -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 {}
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -6,7 +6,7 @@ import { PreloadSelectedModulesList } from './core'
|
|||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: '/videos/list',
|
||||
redirectTo: '/videos/trending',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
<button type="button" class="close" aria-label="Close" (click)="cancel()">
|
||||
<span aria-hidden="true">×</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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -26,17 +26,13 @@ import { throwIfAlreadyLoaded } from './module-import-guard'
|
|||
],
|
||||
|
||||
declarations: [
|
||||
ConfirmComponent,
|
||||
MenuComponent,
|
||||
MenuAdminComponent
|
||||
ConfirmComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
SimpleNotificationsModule,
|
||||
|
||||
ConfirmComponent,
|
||||
MenuComponent,
|
||||
MenuAdminComponent
|
||||
ConfirmComponent
|
||||
],
|
||||
|
||||
providers: [
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
export * from './auth'
|
||||
export * from './server'
|
||||
export * from './confirm'
|
||||
export * from './menu'
|
||||
export * from './routing'
|
||||
export * from './core.module'
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
export * from './menu.component'
|
||||
export * from './menu-admin.component'
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './header.component'
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
input:not([type=submit]) {
|
||||
@include peertube-input-text(340px);
|
||||
display: block;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
@include peertube-button;
|
||||
@include orange-button;
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './menu.component'
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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'])
|
|
@ -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'
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<span class="action-button action-button-delete" >
|
||||
<span class="icon icon-delete-grey"></span>
|
||||
Delete
|
||||
</span>
|
|
@ -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 {
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<a class="action-button" [routerLink]="routerLink">
|
||||
<span class="icon icon-edit"></span>
|
||||
Edit
|
||||
</a>
|
|
@ -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 = []
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
}
|
|
@ -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}`
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export * from './search-field.type'
|
||||
export * from './search.component'
|
||||
export * from './search.model'
|
||||
export * from './search.service'
|
|
@ -1 +0,0 @@
|
|||
export type SearchField = 'name' | 'account' | 'host' | 'tags'
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import { SearchField } from './search-field.type'
|
||||
|
||||
export interface Search {
|
||||
field: SearchField
|
||||
value: string
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 { }
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue