Thumbnail, author and duration support in client
authorChocobozzz <florian.bigard@gmail.com>
Sat, 21 May 2016 16:03:34 +0000 (18:03 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Sat, 21 May 2016 16:08:23 +0000 (18:08 +0200)
13 files changed:
client/angular/app/app.component.ts
client/angular/videos/components/list/video-miniature.component.html [new file with mode: 0644]
client/angular/videos/components/list/video-miniature.component.scss [new file with mode: 0644]
client/angular/videos/components/list/video-miniature.component.ts [new file with mode: 0644]
client/angular/videos/components/list/videos-list.component.html
client/angular/videos/components/list/videos-list.component.scss
client/angular/videos/components/list/videos-list.component.ts
client/angular/videos/components/watch/videos-watch.component.ts
client/angular/videos/models/video.ts [deleted file]
client/angular/videos/services/videos.service.ts [deleted file]
client/angular/videos/video.ts [new file with mode: 0644]
client/angular/videos/videos.service.ts [new file with mode: 0644]
server/middlewares/reqValidators/videos.js

index 359d7128ee47ca935be17122b5602e8a349ee4e1..a105ed26a9ec881b66eeb2a2fda432c3fb56f3cd 100644 (file)
@@ -5,7 +5,7 @@ import { HTTP_PROVIDERS } from '@angular/http';
 import { VideosAddComponent } from '../videos/components/add/videos-add.component';
 import { VideosListComponent } from '../videos/components/list/videos-list.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 { UserLoginComponent } from '../users/components/login/login.component';
 import { AuthService } from '../users/services/auth.service';
diff --git a/client/angular/videos/components/list/video-miniature.component.html b/client/angular/videos/components/list/video-miniature.component.html
new file mode 100644 (file)
index 0000000..b88a19d
--- /dev/null
@@ -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>
diff --git a/client/angular/videos/components/list/video-miniature.component.scss b/client/angular/videos/components/list/video-miniature.component.scss
new file mode 100644 (file)
index 0000000..dbcd658
--- /dev/null
@@ -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);
+    }
+  }
+}
diff --git a/client/angular/videos/components/list/video-miniature.component.ts b/client/angular/videos/components/list/video-miniature.component.ts
new file mode 100644 (file)
index 0000000..383c2c6
--- /dev/null
@@ -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)
+      );
+    }
+  }
+}
index 4eeacbc77ce983ce246a6b6af1adc598c81da7a7..776339d10be21550d8b35707b7c81ffe79fec58f 100644 (file)
@@ -1,12 +1,3 @@
 <div *ngIf="videos.length === 0">There is no video.</div>
-<div *ngFor="let video of videos" class="video">
-  <div>
-    <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>
+<my-video-miniature *ngFor="let video of videos" [video]="video" [user]="user" (removed)="onRemoved(video)">
+</my-video-miniature>
index 82ddd80e57f4076c86dce4cce7ad052449727979..ac930978cf6a6131d6b89dc5f4e15cb4c9a3e087 100644 (file)
@@ -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 {
   display: inline-block;
   margin-top: 100px;
 }
+
+my-videos-miniature {
+  display: inline-block;
+}
index 6ff0b2afb42fc16455344c185443cebc9560e016..6fc0c1f04b290f2f79ec271bace64ca04c59c082 100644 (file)
@@ -3,14 +3,15 @@ import { ROUTER_DIRECTIVES, RouteParams } from '@angular/router-deprecated';
 
 import { AuthService } from '../../../users/services/auth.service';
 import { User } from '../../../users/models/user';
-import { VideosService } from '../../services/videos.service';
-import { Video } from '../../models/video';
+import { VideosService } from '../../videos.service';
+import { Video } from '../../video';
+import { VideoMiniatureComponent } from './video-miniature.component';
 
 @Component({
   selector: 'my-videos-list',
   styleUrls: [ 'app/angular/videos/components/list/videos-list.component.css' ],
   templateUrl: 'app/angular/videos/components/list/videos-list.component.html',
-  directives: [ ROUTER_DIRECTIVES ]
+  directives: [ ROUTER_DIRECTIVES, VideoMiniatureComponent ]
 })
 
 export class VideosListComponent implements OnInit {
@@ -50,11 +51,8 @@ export class VideosListComponent implements OnInit {
     );
   }
 
-  removeVideo(id: string) {
-    this._videosService.removeVideo(id).subscribe(
-      status => this.getVideos(),
-      error => alert(error)
-    );
+  onRemoved(video: Video): void {
+    this.videos.splice(this.videos.indexOf(video), 1);
   }
 
 }
index 3d1829b99047fb057cad6ce60412aaaa59d7846c..3eb005d07b4debd23ca197fa8a646729703fe10f 100644 (file)
@@ -5,8 +5,8 @@ import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
 // TODO import it with systemjs
 declare var WebTorrent: any;
 
-import { Video } from '../../models/video';
-import { VideosService } from '../../services/videos.service';
+import { Video } from '../../video';
+import { VideosService } from '../../videos.service';
 
 @Component({
   selector: 'my-video-watch',
diff --git a/client/angular/videos/models/video.ts b/client/angular/videos/models/video.ts
deleted file mode 100644 (file)
index e52c6d8..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-export interface Video {
-  id: string;
-  name: string;
-  description: string;
-  magnetUri: string;
-  podUrl: string;
-  isLocal: boolean;
-}
diff --git a/client/angular/videos/services/videos.service.ts b/client/angular/videos/services/videos.service.ts
deleted file mode 100644 (file)
index d085483..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-import { Injectable } from '@angular/core';
-import { Http, Response } from '@angular/http';
-import { Observable } from 'rxjs/Rx';
-
-import { Video } from '../models/video';
-import { AuthService } from '../../users/services/auth.service';
-
-@Injectable()
-export class VideosService {
-  private _baseVideoUrl = '/api/v1/videos/';
-
-  constructor (private http: Http, private _authService: AuthService) {}
-
-  getVideos() {
-    return this.http.get(this._baseVideoUrl)
-                    .map(res => <Video[]> res.json())
-                    .catch(this.handleError);
-  }
-
-  getVideo(id: string) {
-    return this.http.get(this._baseVideoUrl + id)
-                    .map(res => <Video> res.json())
-                    .catch(this.handleError);
-  }
-
-  removeVideo(id: string) {
-    if (confirm('Are you sure?')) {
-      const options = this._authService.getAuthRequestOptions();
-      return this.http.delete(this._baseVideoUrl + id, options)
-                      .map(res => <number> res.status)
-                      .catch(this.handleError);
-    }
-  }
-
-  searchVideos(search: string) {
-    return this.http.get(this._baseVideoUrl + 'search/' + search)
-                    .map(res => <Video> res.json())
-                    .catch(this.handleError);
-  }
-
-  private handleError (error: Response) {
-    console.error(error);
-    return Observable.throw(error.json().error || 'Server error');
-  }
-}
diff --git a/client/angular/videos/video.ts b/client/angular/videos/video.ts
new file mode 100644 (file)
index 0000000..32ff64e
--- /dev/null
@@ -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;
+  }
+}
diff --git a/client/angular/videos/videos.service.ts b/client/angular/videos/videos.service.ts
new file mode 100644 (file)
index 0000000..f4790b5
--- /dev/null
@@ -0,0 +1,54 @@
+import { Injectable } from '@angular/core';
+import { Http, Response } from '@angular/http';
+import { Observable } from 'rxjs/Rx';
+
+import { Video } from './video';
+import { AuthService } from '../users/services/auth.service';
+
+@Injectable()
+export class VideosService {
+  private _baseVideoUrl = '/api/v1/videos/';
+
+  constructor (private http: Http, private _authService: AuthService) {}
+
+  getVideos() {
+    return this.http.get(this._baseVideoUrl)
+                    .map(res => res.json())
+                    .map(this.extractVideos)
+                    .catch(this.handleError);
+  }
+
+  getVideo(id: string) {
+    return this.http.get(this._baseVideoUrl + id)
+                    .map(res => <Video> res.json())
+                    .catch(this.handleError);
+  }
+
+  removeVideo(id: string) {
+    const options = this._authService.getAuthRequestOptions();
+    return this.http.delete(this._baseVideoUrl + id, options)
+                    .map(res => <number> res.status)
+                    .catch(this.handleError);
+  }
+
+  searchVideos(search: string) {
+    return this.http.get(this._baseVideoUrl + 'search/' + search)
+                    .map(res => res.json())
+                    .map(this.extractVideos)
+                    .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) {
+    console.error(error);
+    return Observable.throw(error.json().error || 'Server error');
+  }
+}
index 6e6e75fb319ae42ade94f9e858df5d671cc9f850..d4dec1a59ef8b0e081d027294ee143fe1d820534 100644 (file)
@@ -30,7 +30,7 @@ function videosAdd (req, res, next) {
       }
 
       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