Thumbnail, author and duration support in client
This commit is contained in:
parent
295ba044af
commit
501bc6c2b1
12 changed files with 221 additions and 71 deletions
|
@ -5,7 +5,7 @@ import { HTTP_PROVIDERS } from '@angular/http';
|
||||||
import { VideosAddComponent } from '../videos/components/add/videos-add.component';
|
import { VideosAddComponent } from '../videos/components/add/videos-add.component';
|
||||||
import { VideosListComponent } from '../videos/components/list/videos-list.component';
|
import { VideosListComponent } from '../videos/components/list/videos-list.component';
|
||||||
import { VideosWatchComponent } from '../videos/components/watch/videos-watch.component';
|
import { VideosWatchComponent } from '../videos/components/watch/videos-watch.component';
|
||||||
import { VideosService } from '../videos/services/videos.service';
|
import { VideosService } from '../videos/videos.service';
|
||||||
import { FriendsService } from '../friends/services/friends.service';
|
import { FriendsService } from '../friends/services/friends.service';
|
||||||
import { UserLoginComponent } from '../users/components/login/login.component';
|
import { UserLoginComponent } from '../users/components/login/login.component';
|
||||||
import { AuthService } from '../users/services/auth.service';
|
import { AuthService } from '../users/services/auth.service';
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
<div class="video-miniature" (mouseenter)="onHover()" (mouseleave)="onBlur()">
|
||||||
|
<a
|
||||||
|
[routerLink]="['VideosWatch', { id: video.id }]" [attr.title]="video.description"
|
||||||
|
class="video-miniature-thumbnail"
|
||||||
|
>
|
||||||
|
<img [attr.src]="video.thumbnailPath" alt="video thumbnail" />
|
||||||
|
<span class="video-miniature-duration">{{ video.duration }}</span>
|
||||||
|
</a>
|
||||||
|
<span
|
||||||
|
*ngIf="displayRemoveIcon()" (click)="removeVideo(video.id)"
|
||||||
|
class="video-miniature-remove glyphicon glyphicon-remove"
|
||||||
|
></span>
|
||||||
|
|
||||||
|
<div class="video-miniature-informations">
|
||||||
|
<a [routerLink]="['VideosWatch', { id: video.id }]" class="video-miniature-name">
|
||||||
|
<span>{{ video.name }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<span class="video-miniature-author">by {{ video.by }}</span>
|
||||||
|
<span class="video-miniature-created-date">on {{ video.createdDate | date:'short' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,57 @@
|
||||||
|
.video-miniature {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 40px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.video-miniature-thumbnail {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.video-miniature-duration {
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
padding: 2px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-miniature-remove {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
left: 2px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
padding: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-miniature-informations {
|
||||||
|
margin-left: 3px;
|
||||||
|
|
||||||
|
.video-miniature-name {
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-miniature-author, .video-miniature-created-date {
|
||||||
|
display: block;
|
||||||
|
margin-left: 1px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||||
|
import { DatePipe } from '@angular/common';
|
||||||
|
import { ROUTER_DIRECTIVES } from '@angular/router-deprecated';
|
||||||
|
|
||||||
|
import { Video } from '../../video';
|
||||||
|
import { VideosService } from '../../videos.service';
|
||||||
|
import { User } from '../../../users/models/user';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-video-miniature',
|
||||||
|
styleUrls: [ 'app/angular/videos/components/list/video-miniature.component.css' ],
|
||||||
|
templateUrl: 'app/angular/videos/components/list/video-miniature.component.html',
|
||||||
|
directives: [ ROUTER_DIRECTIVES ],
|
||||||
|
pipes: [ DatePipe ]
|
||||||
|
})
|
||||||
|
|
||||||
|
export class VideoMiniatureComponent {
|
||||||
|
@Output() removed = new EventEmitter<any>();
|
||||||
|
|
||||||
|
@Input() video: Video;
|
||||||
|
@Input() user: User;
|
||||||
|
|
||||||
|
hovering: boolean = false;
|
||||||
|
|
||||||
|
constructor(private _videosService: VideosService) {}
|
||||||
|
|
||||||
|
onHover() {
|
||||||
|
this.hovering = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlur() {
|
||||||
|
this.hovering = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayRemoveIcon(): boolean {
|
||||||
|
return this.hovering && this.video.isRemovableBy(this.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeVideo(id: string) {
|
||||||
|
if (confirm('Do you really want to remove this video?')) {
|
||||||
|
this._videosService.removeVideo(id).subscribe(
|
||||||
|
status => this.removed.emit(true),
|
||||||
|
error => alert(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,3 @@
|
||||||
<div *ngIf="videos.length === 0">There is no video.</div>
|
<div *ngIf="videos.length === 0">There is no video.</div>
|
||||||
<div *ngFor="let video of videos" class="video">
|
<my-video-miniature *ngFor="let video of videos" [video]="video" [user]="user" (removed)="onRemoved(video)">
|
||||||
<div>
|
</my-video-miniature>
|
||||||
<a [routerLink]="['VideosWatch', { id: video.id }]" class="video_name">{{ video.name }}</a>
|
|
||||||
<span class="video_pod_url">{{ video.podUrl }}</span>
|
|
||||||
<span *ngIf="video.isLocal === true && user && video.author === user.username" (click)="removeVideo(video.id)" class="video_remove glyphicon glyphicon-remove"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="video_description">
|
|
||||||
{{ video.description }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -1,34 +1,8 @@
|
||||||
.video {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
transition: margin 0.5s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.video_name {
|
|
||||||
color: #333333;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video_pod_url {
|
|
||||||
font-size: small;
|
|
||||||
color: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.video_description {
|
|
||||||
font-size: small;
|
|
||||||
font-style: italic;
|
|
||||||
margin-left: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video_remove {
|
|
||||||
margin: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 100px;
|
margin-top: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
my-videos-miniature {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
|
@ -3,14 +3,15 @@ import { ROUTER_DIRECTIVES, RouteParams } from '@angular/router-deprecated';
|
||||||
|
|
||||||
import { AuthService } from '../../../users/services/auth.service';
|
import { AuthService } from '../../../users/services/auth.service';
|
||||||
import { User } from '../../../users/models/user';
|
import { User } from '../../../users/models/user';
|
||||||
import { VideosService } from '../../services/videos.service';
|
import { VideosService } from '../../videos.service';
|
||||||
import { Video } from '../../models/video';
|
import { Video } from '../../video';
|
||||||
|
import { VideoMiniatureComponent } from './video-miniature.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-videos-list',
|
selector: 'my-videos-list',
|
||||||
styleUrls: [ 'app/angular/videos/components/list/videos-list.component.css' ],
|
styleUrls: [ 'app/angular/videos/components/list/videos-list.component.css' ],
|
||||||
templateUrl: 'app/angular/videos/components/list/videos-list.component.html',
|
templateUrl: 'app/angular/videos/components/list/videos-list.component.html',
|
||||||
directives: [ ROUTER_DIRECTIVES ]
|
directives: [ ROUTER_DIRECTIVES, VideoMiniatureComponent ]
|
||||||
})
|
})
|
||||||
|
|
||||||
export class VideosListComponent implements OnInit {
|
export class VideosListComponent implements OnInit {
|
||||||
|
@ -50,11 +51,8 @@ export class VideosListComponent implements OnInit {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeVideo(id: string) {
|
onRemoved(video: Video): void {
|
||||||
this._videosService.removeVideo(id).subscribe(
|
this.videos.splice(this.videos.indexOf(video), 1);
|
||||||
status => this.getVideos(),
|
|
||||||
error => alert(error)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
|
||||||
// TODO import it with systemjs
|
// TODO import it with systemjs
|
||||||
declare var WebTorrent: any;
|
declare var WebTorrent: any;
|
||||||
|
|
||||||
import { Video } from '../../models/video';
|
import { Video } from '../../video';
|
||||||
import { VideosService } from '../../services/videos.service';
|
import { VideosService } from '../../videos.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-watch',
|
selector: 'my-video-watch',
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
export interface Video {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
magnetUri: string;
|
|
||||||
podUrl: string;
|
|
||||||
isLocal: boolean;
|
|
||||||
}
|
|
60
client/angular/videos/video.ts
Normal file
60
client/angular/videos/video.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
export class Video {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
magnetUri: string;
|
||||||
|
podUrl: string;
|
||||||
|
isLocal: boolean;
|
||||||
|
thumbnailPath: string;
|
||||||
|
author: string;
|
||||||
|
createdDate: Date;
|
||||||
|
by: string;
|
||||||
|
duration: string;
|
||||||
|
|
||||||
|
constructor(hash: {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
magnetUri: string,
|
||||||
|
podUrl: string,
|
||||||
|
isLocal: boolean,
|
||||||
|
thumbnailPath: string,
|
||||||
|
author: string,
|
||||||
|
createdDate: string,
|
||||||
|
duration: number;
|
||||||
|
}) {
|
||||||
|
this.id = hash.id;
|
||||||
|
this.name = hash.name;
|
||||||
|
this.description = hash.description;
|
||||||
|
this.magnetUri = hash.magnetUri;
|
||||||
|
this.podUrl = hash.podUrl;
|
||||||
|
this.isLocal = hash.isLocal;
|
||||||
|
this.thumbnailPath = hash.thumbnailPath;
|
||||||
|
this.author = hash.author;
|
||||||
|
this.createdDate = new Date(hash.createdDate);
|
||||||
|
this.duration = Video.createDurationString(hash.duration);
|
||||||
|
this.by = Video.createByString(hash.author, hash.podUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
isRemovableBy(user): boolean {
|
||||||
|
return this.isLocal === true && user && this.author === user.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static createDurationString(duration: number): string {
|
||||||
|
const minutes = Math.floor(duration / 60);
|
||||||
|
const seconds = duration % 60;
|
||||||
|
const minutes_padding = minutes >= 10 ? '' : '0';
|
||||||
|
const seconds_padding = seconds >= 10 ? '' : '0'
|
||||||
|
|
||||||
|
return minutes_padding + minutes.toString() + ':' + seconds_padding + seconds.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static createByString(author: string, podUrl: string): string {
|
||||||
|
let [ host, port ] = podUrl.replace(/^https?:\/\//, '').split(':');
|
||||||
|
|
||||||
|
if (port === '80' || port === '443') port = '';
|
||||||
|
else port = ':' + port;
|
||||||
|
|
||||||
|
return author + '@' + host + port;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,8 @@ import { Injectable } from '@angular/core';
|
||||||
import { Http, Response } from '@angular/http';
|
import { Http, Response } from '@angular/http';
|
||||||
import { Observable } from 'rxjs/Rx';
|
import { Observable } from 'rxjs/Rx';
|
||||||
|
|
||||||
import { Video } from '../models/video';
|
import { Video } from './video';
|
||||||
import { AuthService } from '../../users/services/auth.service';
|
import { AuthService } from '../users/services/auth.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VideosService {
|
export class VideosService {
|
||||||
|
@ -13,7 +13,8 @@ export class VideosService {
|
||||||
|
|
||||||
getVideos() {
|
getVideos() {
|
||||||
return this.http.get(this._baseVideoUrl)
|
return this.http.get(this._baseVideoUrl)
|
||||||
.map(res => <Video[]> res.json())
|
.map(res => res.json())
|
||||||
|
.map(this.extractVideos)
|
||||||
.catch(this.handleError);
|
.catch(this.handleError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,20 +25,28 @@ export class VideosService {
|
||||||
}
|
}
|
||||||
|
|
||||||
removeVideo(id: string) {
|
removeVideo(id: string) {
|
||||||
if (confirm('Are you sure?')) {
|
const options = this._authService.getAuthRequestOptions();
|
||||||
const options = this._authService.getAuthRequestOptions();
|
return this.http.delete(this._baseVideoUrl + id, options)
|
||||||
return this.http.delete(this._baseVideoUrl + id, options)
|
.map(res => <number> res.status)
|
||||||
.map(res => <number> res.status)
|
.catch(this.handleError);
|
||||||
.catch(this.handleError);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
searchVideos(search: string) {
|
searchVideos(search: string) {
|
||||||
return this.http.get(this._baseVideoUrl + 'search/' + search)
|
return this.http.get(this._baseVideoUrl + 'search/' + search)
|
||||||
.map(res => <Video> res.json())
|
.map(res => res.json())
|
||||||
|
.map(this.extractVideos)
|
||||||
.catch(this.handleError);
|
.catch(this.handleError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extractVideos (body: any[]) {
|
||||||
|
const videos = [];
|
||||||
|
for (const video_json of body) {
|
||||||
|
videos.push(new Video(video_json));
|
||||||
|
}
|
||||||
|
|
||||||
|
return videos;
|
||||||
|
}
|
||||||
|
|
||||||
private handleError (error: Response) {
|
private handleError (error: Response) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return Observable.throw(error.json().error || 'Server error');
|
return Observable.throw(error.json().error || 'Server error');
|
|
@ -30,7 +30,7 @@ function videosAdd (req, res, next) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (duration > constants.MAXIMUM_VIDEO_DURATION) {
|
if (duration > constants.MAXIMUM_VIDEO_DURATION) {
|
||||||
return res.status(400).send('Duration of the video file is too big.')
|
return res.status(400).send('Duration of the video file is too big (' + constants.MAXIMUM_VIDEO_DURATION + ').')
|
||||||
}
|
}
|
||||||
|
|
||||||
videoFile.duration = duration
|
videoFile.duration = duration
|
||||||
|
|
Loading…
Reference in a new issue