Client: Handle NSFW video

pull/61/head
Chocobozzz 2017-04-04 21:37:03 +02:00
parent 1d49e1e27d
commit 92fb909c9b
15 changed files with 157 additions and 22 deletions

View File

@ -1,7 +1,7 @@
import { Component, OnInit, ViewContainerRef } from '@angular/core'; import { Component, OnInit, ViewContainerRef } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AuthService } from './core'; import { AuthService, ConfigService } from './core';
import { VideoService } from './videos'; import { VideoService } from './videos';
import { UserService } from './shared'; import { UserService } from './shared';
@ -27,6 +27,7 @@ export class AppComponent implements OnInit {
constructor( constructor(
private router: Router, private router: Router,
private authService: AuthService, private authService: AuthService,
private configService: ConfigService,
private userService: UserService, private userService: UserService,
private videoService: VideoService, private videoService: VideoService,
viewContainerRef: ViewContainerRef viewContainerRef: ViewContainerRef
@ -38,6 +39,7 @@ export class AppComponent implements OnInit {
this.userService.checkTokenValidity(); this.userService.checkTokenValidity();
} }
this.configService.loadConfig();
this.videoService.loadVideoCategories(); this.videoService.loadVideoCategories();
this.videoService.loadVideoLicences(); this.videoService.loadVideoLicences();
} }

View File

@ -5,7 +5,8 @@ export class AuthUser extends User {
private static KEYS = { private static KEYS = {
ID: 'id', ID: 'id',
ROLE: 'role', ROLE: 'role',
USERNAME: 'username' USERNAME: 'username',
DISPLAY_NSFW: 'display_nsfw'
}; };
tokens: Tokens; tokens: Tokens;
@ -17,7 +18,8 @@ export class AuthUser extends User {
{ {
id: parseInt(localStorage.getItem(this.KEYS.ID)), id: parseInt(localStorage.getItem(this.KEYS.ID)),
username: localStorage.getItem(this.KEYS.USERNAME), username: localStorage.getItem(this.KEYS.USERNAME),
role: localStorage.getItem(this.KEYS.ROLE) role: localStorage.getItem(this.KEYS.ROLE),
displayNSFW: localStorage.getItem(this.KEYS.DISPLAY_NSFW) === 'true'
}, },
Tokens.load() Tokens.load()
); );
@ -30,10 +32,16 @@ export class AuthUser extends User {
localStorage.removeItem(this.KEYS.USERNAME); localStorage.removeItem(this.KEYS.USERNAME);
localStorage.removeItem(this.KEYS.ID); localStorage.removeItem(this.KEYS.ID);
localStorage.removeItem(this.KEYS.ROLE); localStorage.removeItem(this.KEYS.ROLE);
localStorage.removeItem(this.KEYS.DISPLAY_NSFW);
Tokens.flush(); Tokens.flush();
} }
constructor(userHash: { id: number, username: string, role: string }, hashTokens: any) { constructor(userHash: {
id: number,
username: string,
role: string,
displayNSFW: boolean
}, hashTokens: any) {
super(userHash); super(userHash);
this.tokens = new Tokens(hashTokens); this.tokens = new Tokens(hashTokens);
} }
@ -59,6 +67,7 @@ export class AuthUser extends User {
localStorage.setItem(AuthUser.KEYS.ID, this.id.toString()); localStorage.setItem(AuthUser.KEYS.ID, this.id.toString());
localStorage.setItem(AuthUser.KEYS.USERNAME, this.username); localStorage.setItem(AuthUser.KEYS.USERNAME, this.username);
localStorage.setItem(AuthUser.KEYS.ROLE, this.role); localStorage.setItem(AuthUser.KEYS.ROLE, this.role);
localStorage.setItem(AuthUser.KEYS.DISPLAY_NSFW, JSON.stringify(this.displayNSFW);
this.tokens.save(); this.tokens.save();
} }
} }

View File

@ -0,0 +1,36 @@
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { RestExtractor } from '../../shared/rest';
@Injectable()
export class ConfigService {
private static BASE_CONFIG_URL = '/api/v1/config/';
private config: {
signup: {
enabled: boolean
}
} = {
signup: {
enabled: false
}
};
constructor(
private http: Http,
private restExtractor: RestExtractor,
) {}
loadConfig() {
this.http.get(ConfigService.BASE_CONFIG_URL)
.map(this.restExtractor.extractDataGet)
.subscribe(data => {
this.config = data;
});
}
getConfig() {
return this.config;
}
}

View File

@ -0,0 +1 @@
export * from './config.service';

View File

@ -7,6 +7,7 @@ import { SimpleNotificationsModule } from 'angular2-notifications';
import { ModalModule } from 'ng2-bootstrap/modal'; import { ModalModule } from 'ng2-bootstrap/modal';
import { AuthService } from './auth'; import { AuthService } from './auth';
import { ConfigService } from './config';
import { ConfirmComponent, ConfirmService } from './confirm'; import { ConfirmComponent, ConfirmService } from './confirm';
import { MenuComponent, MenuAdminComponent } from './menu'; import { MenuComponent, MenuAdminComponent } from './menu';
import { throwIfAlreadyLoaded } from './module-import-guard'; import { throwIfAlreadyLoaded } from './module-import-guard';
@ -37,7 +38,8 @@ import { throwIfAlreadyLoaded } from './module-import-guard';
providers: [ providers: [
AuthService, AuthService,
ConfirmService ConfirmService,
ConfigService
] ]
}) })
export class CoreModule { export class CoreModule {

View File

@ -1,4 +1,5 @@
export * from './auth'; export * from './auth';
export * from './config';
export * from './confirm'; export * from './confirm';
export * from './menu'; export * from './menu';
export * from './core.module' export * from './core.module'

View File

@ -2,12 +2,20 @@ export class User {
id: number; id: number;
username: string; username: string;
role: string; role: string;
displayNSFW: boolean;
createdAt: Date; createdAt: Date;
constructor(hash: { id: number, username: string, role: string, createdAt?: Date }) { constructor(hash: {
id: number,
username: string,
role: string,
displayNSFW?: boolean,
createdAt?: Date,
}) {
this.id = hash.id; this.id = hash.id;
this.username = hash.username; this.username = hash.username;
this.role = hash.role; this.role = hash.role;
this.displayNSFW = hash.displayNSFW;
if (hash.createdAt) { if (hash.createdAt) {
this.createdAt = hash.createdAt; this.createdAt = hash.createdAt;

View File

@ -1,3 +1,5 @@
import { User } from '../../shared';
export class Video { export class Video {
author: string; author: string;
by: string; by: string;
@ -16,6 +18,7 @@ export class Video {
views: number; views: number;
likes: number; likes: number;
dislikes: number; dislikes: number;
nsfw: boolean;
private static createByString(author: string, podHost: string) { private static createByString(author: string, podHost: string) {
return author + '@' + podHost; return author + '@' + podHost;
@ -47,6 +50,7 @@ export class Video {
views: number, views: number,
likes: number, likes: number,
dislikes: number, dislikes: number,
nsfw: boolean
}) { }) {
this.author = hash.author; this.author = hash.author;
this.createdAt = new Date(hash.createdAt); this.createdAt = new Date(hash.createdAt);
@ -64,11 +68,17 @@ export class Video {
this.views = hash.views; this.views = hash.views;
this.likes = hash.likes; this.likes = hash.likes;
this.dislikes = hash.dislikes; this.dislikes = hash.dislikes;
this.nsfw = hash.nsfw;
this.by = Video.createByString(hash.author, hash.podHost); this.by = Video.createByString(hash.author, hash.podHost);
} }
isRemovableBy(user) { isRemovableBy(user: User) {
return this.isLocal === true && user && this.author === user.username; return this.isLocal === true && user && this.author === user.username;
} }
isVideoNSFWForUser(user: User) {
// If the video is NSFW and the user is not logged in, or the user does not want to display NSFW videos...
return (this.nsfw && (!user || user.displayNSFW === false));
}
} }

View File

@ -14,6 +14,14 @@
</div> </div>
</div> </div>
<div class="form-group">
<label for="nsfw">NSFW</label>
<input
type="checkbox" id="nsfw"
formControlName="nsfw"
>
</div>
<div class="form-group"> <div class="form-group">
<label for="category">Category</label> <label for="category">Category</label>
<select class="form-control" id="category" formControlName="category"> <select class="form-control" id="category" formControlName="category">

View File

@ -71,6 +71,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
buildForm() { buildForm() {
this.form = this.formBuilder.group({ this.form = this.formBuilder.group({
name: [ '', VIDEO_NAME.VALIDATORS ], name: [ '', VIDEO_NAME.VALIDATORS ],
nsfw: [ false ],
category: [ '', VIDEO_CATEGORY.VALIDATORS ], category: [ '', VIDEO_CATEGORY.VALIDATORS ],
licence: [ '', VIDEO_LICENCE.VALIDATORS ], licence: [ '', VIDEO_LICENCE.VALIDATORS ],
description: [ '', VIDEO_DESCRIPTION.VALIDATORS ], description: [ '', VIDEO_DESCRIPTION.VALIDATORS ],
@ -93,12 +94,14 @@ export class VideoAddComponent extends FormReactive implements OnInit {
this.uploader.onBuildItemForm = (item, form) => { this.uploader.onBuildItemForm = (item, form) => {
const name = this.form.value['name']; const name = this.form.value['name'];
const nsfw = this.form.value['nsfw'];
const category = this.form.value['category']; const category = this.form.value['category'];
const licence = this.form.value['licence']; const licence = this.form.value['licence'];
const description = this.form.value['description']; const description = this.form.value['description'];
form.append('name', name); form.append('name', name);
form.append('category', category); form.append('category', category);
form.append('nsfw', nsfw);
form.append('licence', licence); form.append('licence', licence);
form.append('description', description); form.append('description', description);

View File

@ -3,7 +3,11 @@
[routerLink]="['/videos/watch', video.id]" [attr.title]="video.description" [routerLink]="['/videos/watch', video.id]" [attr.title]="video.description"
class="video-miniature-thumbnail" class="video-miniature-thumbnail"
> >
<img [attr.src]="video.thumbnailPath" alt="video thumbnail" /> <img *ngIf="isVideoNSFWForThisUser() === false" [attr.src]="video.thumbnailPath" alt="video thumbnail" />
<div *ngIf="isVideoNSFWForThisUser()" class="thumbnail-nsfw">
NSFW
</div>
<span class="video-miniature-duration">{{ video.duration }}</span> <span class="video-miniature-duration">{{ video.duration }}</span>
</a> </a>
<span <span
@ -13,7 +17,7 @@
<div class="video-miniature-informations"> <div class="video-miniature-informations">
<span class="video-miniature-name-tags"> <span class="video-miniature-name-tags">
<a [routerLink]="['/videos/watch', video.id]" [attr.title]="video.name" class="video-miniature-name">{{ video.name }}</a> <a [routerLink]="['/videos/watch', video.id]" [attr.title]="getVideoName()" class="video-miniature-name">{{ getVideoName() }}</a>
<div class="video-miniature-tags"> <div class="video-miniature-tags">
<span *ngFor="let tag of video.tags" class="video-miniature-tag"> <span *ngFor="let tag of video.tags" class="video-miniature-tag">

View File

@ -15,6 +15,21 @@
display: inline-block; display: inline-block;
position: relative; position: relative;
&:hover {
text-decoration: none !important;
}
.thumbnail-nsfw {
background-color: #000;
color: #fff;
text-align: center;
font-size: 30px;
line-height: 110px;
width: 200px;
height: 110px;
}
.video-miniature-duration { .video-miniature-duration {
position: absolute; position: absolute;
right: 5px; right: 5px;

View File

@ -2,7 +2,7 @@ import { Component, Input, Output, EventEmitter } from '@angular/core';
import { NotificationsService } from 'angular2-notifications'; import { NotificationsService } from 'angular2-notifications';
import { ConfirmService } from '../../core'; import { ConfirmService, ConfigService } from '../../core';
import { SortField, Video, VideoService } from '../shared'; import { SortField, Video, VideoService } from '../shared';
import { User } from '../../shared'; import { User } from '../../shared';
@ -24,6 +24,7 @@ export class VideoMiniatureComponent {
constructor( constructor(
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private confirmService: ConfirmService, private confirmService: ConfirmService,
private configService: ConfigService,
private videoService: VideoService private videoService: VideoService
) {} ) {}
@ -31,6 +32,13 @@ export class VideoMiniatureComponent {
return this.hovering && this.video.isRemovableBy(this.user); return this.hovering && this.video.isRemovableBy(this.user);
} }
getVideoName() {
if (this.isVideoNSFWForThisUser())
return 'NSFW';
return this.video.name;
}
onBlur() { onBlur() {
this.hovering = false; this.hovering = false;
} }
@ -52,4 +60,8 @@ export class VideoMiniatureComponent {
} }
); );
} }
isVideoNSFWForThisUser() {
return this.video.isVideoNSFWForUser(this.user);
}
} }

View File

@ -1,12 +1,13 @@
import { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription'; import { Subscription } from 'rxjs/Subscription';
import * as videojs from 'video.js'; import * as videojs from 'video.js';
import { MetaService } from '@nglibs/meta'; import { MetaService } from '@nglibs/meta';
import { NotificationsService } from 'angular2-notifications'; import { NotificationsService } from 'angular2-notifications';
import { AuthService } from '../../core'; import { AuthService, ConfirmService } from '../../core';
import { VideoMagnetComponent } from './video-magnet.component'; import { VideoMagnetComponent } from './video-magnet.component';
import { VideoShareComponent } from './video-share.component'; import { VideoShareComponent } from './video-share.component';
import { VideoReportComponent } from './video-report.component'; import { VideoReportComponent } from './video-report.component';
@ -47,7 +48,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
private elementRef: ElementRef, private elementRef: ElementRef,
private ngZone: NgZone, private ngZone: NgZone,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router,
private videoService: VideoService, private videoService: VideoService,
private confirmService: ConfirmService,
private metaService: MetaService, private metaService: MetaService,
private webTorrentService: WebTorrentService, private webTorrentService: WebTorrentService,
private authService: AuthService, private authService: AuthService,
@ -58,15 +61,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.paramsSub = this.route.params.subscribe(routeParams => { this.paramsSub = this.route.params.subscribe(routeParams => {
let id = routeParams['id']; let id = routeParams['id'];
this.videoService.getVideo(id).subscribe( this.videoService.getVideo(id).subscribe(
video => { video => this.onVideoFetched(video),
this.video = video;
this.setOpenGraphTags(); error => this.videoNotFound = true
this.loadVideo();
this.checkUserRating();
},
error => {
this.videoNotFound = true;
}
); );
}); });
@ -92,7 +89,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
window.clearInterval(this.torrentInfosInterval); window.clearInterval(this.torrentInfosInterval);
window.clearTimeout(this.errorTimer); window.clearTimeout(this.errorTimer);
if (this.video !== null) { if (this.video !== null && this.webTorrentService.has(this.video.magnetUri)) {
this.webTorrentService.remove(this.video.magnetUri); this.webTorrentService.remove(this.video.magnetUri);
} }
@ -206,6 +203,29 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
); );
} }
private onVideoFetched(video: Video) {
this.video = video;
let observable;
if (this.video.isVideoNSFWForUser(this.authService.getUser())) {
observable = this.confirmService.confirm('This video is not safe for work. Are you sure you want to watch it?', 'NSFW');
} else {
observable = Observable.of(true);
}
observable.subscribe(
res => {
if (res === false) {
return this.router.navigate([ '/videos/list' ]);
}
this.setOpenGraphTags();
this.loadVideo();
this.checkUserRating();
}
);
}
private updateVideoRating(oldRating: RateType, newRating: RateType) { private updateVideoRating(oldRating: RateType, newRating: RateType) {
let likesToIncrement = 0; let likesToIncrement = 0;
let dislikesToIncrement = 0; let dislikesToIncrement = 0;

View File

@ -26,4 +26,8 @@ export class WebTorrentService {
remove(magnetUri: string) { remove(magnetUri: string) {
return this.client.remove(magnetUri); return this.client.remove(magnetUri);
} }
has(magnetUri: string) {
return this.client.get(magnetUri) !== null;
}
} }