]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add tags support to the video list
authorChocobozzz <florian.bigard@gmail.com>
Fri, 10 Jun 2016 15:43:40 +0000 (17:43 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Fri, 10 Jun 2016 15:46:29 +0000 (17:46 +0200)
17 files changed:
client/src/app/app.component.html
client/src/app/app.component.scss
client/src/app/app.component.ts
client/src/app/login/login.component.ts
client/src/app/shared/search/index.ts
client/src/app/shared/search/search.component.ts
client/src/app/shared/search/search.service.ts [new file with mode: 0644]
client/src/app/videos/shared/video.model.ts
client/src/app/videos/video-add/video-add.component.html
client/src/app/videos/video-add/video-add.component.ts
client/src/app/videos/video-list/video-list.component.ts
client/src/app/videos/video-list/video-miniature.component.html
client/src/app/videos/video-list/video-miniature.component.scss
client/src/app/videos/video-list/video-miniature.component.ts
client/src/app/videos/video-watch/video-watch.component.ts
client/src/vendor.ts
client/tsconfig.json

index 48e97d5239952ce3f5a6bca44db3790f89034505..7e2a0c5f69e4f1428e0a5ea7523ccdf7b09264da 100644 (file)
   <div class="row">
 
     <menu class="col-md-2 col-xs-3">
-      <div class="panel_block">
-        <div id="panel_user_login" class="panel_button">
+      <div class="panel-block">
+        <div id="panel-user-login" class="panel-button">
           <span class="glyphicon glyphicon-user"></span>
-          <a *ngIf="!isLoggedIn" [routerLink]="['UserLogin']">Login</a>
+          <a *ngIf="!isLoggedIn" [routerLink]="['/users/login']">Login</a>
           <a *ngIf="isLoggedIn" (click)="logout()">Logout</a>
         </div>
       </div>
 
-      <div class="panel_block">
-        <div id="panel_get_videos" class="panel_button">
+      <div class="panel-block">
+        <div id="panel-get-videos" class="panel-button">
           <span class="glyphicon glyphicon-list"></span>
-          <a [routerLink]="['VideosList']">Get videos</a>
+          <a [routerLink]="['/videos/list']">Get videos</a>
         </div>
 
-        <div id="panel_upload_video" class="panel_button" *ngIf="isLoggedIn">
+        <div id="panel-upload-video" class="panel-button" *ngIf="isLoggedIn">
           <span class="glyphicon glyphicon-cloud-upload"></span>
-          <a [routerLink]="['VideosAdd']">Upload a video</a>
+          <a [routerLink]="['/videos/add']">Upload a video</a>
         </div>
       </div>
 
-      <div class="panel_block" *ngIf="isLoggedIn">
-        <div id="panel_make_friends" class="panel_button">
+      <div class="panel-block" *ngIf="isLoggedIn">
+        <div id="panel-make-friends" class="panel-button">
           <span class="glyphicon glyphicon-cloud"></span>
           <a (click)='makeFriends()'>Make friends</a>
         </div>
 
-        <div id="panel_quit_friends" class="panel_button">
+        <div id="panel-quit-friends" class="panel-button">
           <span class="glyphicon glyphicon-plane"></span>
           <a (click)='quitFriends()'>Quit friends</a>
         </div>
       </div>
     </menu>
 
-    <div class="col-md-9 col-xs-8 router_outler_container">
+    <div class="col-md-9 col-xs-8 router-outler-container">
       <router-outlet></router-outlet>
     </div>
 
index e02c2d5b0718b0a925223cc7bb06c7559595ce3f..1a9a196ffa22c89e99057891b3de397a323f81c7 100644 (file)
@@ -8,7 +8,7 @@ menu {
   margin-right: 20px;
   border-right: 1px solid rgba(0, 0, 0, 0.2);
 
-  .panel_button {
+  .panel-button {
     margin: 8px;
     cursor: pointer;
     transition: margin 0.2s;
@@ -27,6 +27,6 @@ menu {
   }
 }
 
-.panel_block:not(:last-child) {
+.panel-block:not(:last-child) {
   border-bottom: 1px solid rgba(0, 0, 0, 0.1);
 }
index 81b700a215c3f3b1de9c67bd8e32d13e21842cd1..2a1486fb24a05aefc751d4e7f9bcd37867135f4b 100644 (file)
@@ -1,6 +1,6 @@
 import { Component } from '@angular/core';
 import { HTTP_PROVIDERS } from '@angular/http';
-import { RouteConfig, Router, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from '@angular/router-deprecated';
+import { Router, ROUTER_DIRECTIVES, ROUTER_PROVIDERS, Routes } from '@angular/router';
 
 import { FriendService } from './friends';
 import { LoginComponent } from './login';
@@ -16,27 +16,23 @@ import {
   VideoWatchComponent,
   VideoService
 } from './videos';
+import { SearchService } from './shared'; // Temporary
 
-@RouteConfig([
+@Routes([
   {
     path: '/users/login',
-    name: 'UserLogin',
     component: LoginComponent
   },
   {
     path: '/videos/list',
-    name: 'VideosList',
-    component: VideoListComponent,
-    useAsDefault: true
+    component: VideoListComponent
   },
   {
     path: '/videos/watch/:id',
-    name: 'VideosWatch',
     component: VideoWatchComponent
   },
   {
     path: '/videos/add',
-    name: 'VideosAdd',
     component: VideoAddComponent
   }
 ])
@@ -46,7 +42,7 @@ import {
     template: require('./app.component.html'),
     styles: [ require('./app.component.scss') ],
     directives: [ ROUTER_DIRECTIVES, SearchComponent ],
-    providers: [ AuthService, FriendService, HTTP_PROVIDERS, ROUTER_PROVIDERS, VideoService ]
+    providers: [ AuthService, FriendService, HTTP_PROVIDERS, ROUTER_PROVIDERS, VideoService, SearchService ]
 })
 
 export class AppComponent {
@@ -75,12 +71,13 @@ export class AppComponent {
         field: search.field,
         search: search.value
       };
-      this.router.navigate(['VideosList', params]);
+      this.router.navigate(['/videos/list', params]);
     } else {
-      this.router.navigate(['VideosList']);
+      this.router.navigate(['/videos/list']);
     }
   }
 
+  // FIXME
   logout() {
     // this._authService.logout();
   }
index 768594ac42a817d2952c785acfebfd4d1c87deee..bcfa021fab5a2e9fac85d2dfc8966a580cb9a5f7 100644 (file)
@@ -1,5 +1,5 @@
 import { Component } from '@angular/core';
-import { Router } from '@angular/router-deprecated';
+import { Router } from '@angular/router';
 
 import { AuthService, AuthStatus, User } from '../shared';
 
@@ -26,7 +26,7 @@ export class LoginComponent {
 
         this.authService.setStatus(AuthStatus.LoggedIn);
 
-        this.router.navigate(['VideosList']);
+        this.router.navigate(['/videos/list']);
       },
       error => {
         if (error.error === 'invalid_grant') {
index a49a4f1a95e9ee9caa82a07d3ac19d830e709bae..a897ed09959a6e38f52efac4b0453190559f1315 100644 (file)
@@ -1,3 +1,4 @@
 export * from './search-field.type';
 export * from './search.component';
 export * from './search.model';
+export * from './search.service';
index c14c2d99c50473e728ee594db927c457c59cadc0..ed1ce807ac0145f917aa04e396df9a35d2d42d58 100644 (file)
@@ -1,9 +1,10 @@
-import { Component, EventEmitter, Output } from '@angular/core';
+import { Component, EventEmitter, Output, OnInit } from '@angular/core';
 
 import { DROPDOWN_DIRECTIVES} from  'ng2-bootstrap/components/dropdown';
 
 import { Search } from './search.model';
 import { SearchField } from './search-field.type';
+import { SearchService } from './search.service'; // Temporary
 
 @Component({
     selector: 'my-search',
@@ -11,7 +12,7 @@ import { SearchField } from './search-field.type';
     directives: [ DROPDOWN_DIRECTIVES ]
 })
 
-export class SearchComponent {
+export class SearchComponent implements OnInit {
   @Output() search = new EventEmitter<Search>();
 
   fieldChoices = {
@@ -26,6 +27,21 @@ export class SearchComponent {
     value: ''
   };
 
+  constructor(private searchService: SearchService) {}
+
+  ngOnInit() {
+    this.searchService.searchChanged.subscribe(
+      newSearchCriterias => {
+        // Put a field by default
+        if (!newSearchCriterias.field) {
+          newSearchCriterias.field = 'name';
+        }
+
+        this.searchCriterias = newSearchCriterias;
+      }
+    );
+  }
+
   get choiceKeys() {
     return Object.keys(this.fieldChoices);
   }
@@ -35,6 +51,7 @@ export class SearchComponent {
     $event.stopPropagation();
 
     this.searchCriterias.field = choice;
+    this.doSearch();
   }
 
   doSearch() {
diff --git a/client/src/app/shared/search/search.service.ts b/client/src/app/shared/search/search.service.ts
new file mode 100644 (file)
index 0000000..787c02d
--- /dev/null
@@ -0,0 +1,15 @@
+import { Injectable } from '@angular/core';
+import { Subject } from 'rxjs/Subject';
+
+import { Search } from './search.model';
+
+// This class is needed to communicate between videos/list and search component
+// Remove it when we'll be able to subscribe to router changes
+@Injectable()
+export class SearchService {
+  searchChanged: Subject<Search>;
+
+  constructor() {
+    this.searchChanged = new Subject<Search>();
+  }
+}
index 614403d799394c24d0387ea0151724514dd3ebde..65417f751e6c3b67d2246ca31b1ed275f437b451 100644 (file)
@@ -9,6 +9,7 @@ export class Video {
   magnetUri: string;
   name: string;
   podUrl: string;
+  tags: string[];
   thumbnailPath: string;
 
   private static createByString(author: string, podUrl: string) {
@@ -42,6 +43,7 @@ export class Video {
     magnetUri: string,
     name: string,
     podUrl: string,
+    tags: string[],
     thumbnailPath: string
   }) {
     this.author  = hash.author;
@@ -53,6 +55,7 @@ export class Video {
     this.magnetUri = hash.magnetUri;
     this.name = hash.name;
     this.podUrl = hash.podUrl;
+    this.tags = hash.tags;
     this.thumbnailPath = hash.thumbnailPath;
 
     this.by = Video.createByString(hash.author, hash.podUrl);
index 6b2eb9377df4e350cca02ebe15778afa62bca097..bcd78c7cb5dc96a1e47adddd3a1281e335267cb8 100644 (file)
       ngControl="tags" #tags="ngForm" [disabled]="isTagsInputDisabled" (keyup)="onTagKeyPress($event)" [(ngModel)]="currentTag"
     >
     <div [hidden]="tags.valid || tags.pristine" class="alert alert-warning">
-      A tag should be between 2 and 10 characters long
+      A tag should be between 2 and 10 characters (alphanumeric) long
     </div>
   </div>
 
   <div class="tags">
-    <div class="label label-info tag" *ngFor="let tag of video.tags">
+    <div class="label label-primary tag" *ngFor="let tag of video.tags">
       {{ tag }}
       <span class="remove" (click)="removeTag(tag)">x</span>
     </div>
index 2b45ea125f8b297d670f7dafe4d0e3098a5756f1..7d8fbdc29ac57c1e4aa1e36816ed0b522927a8e2 100644 (file)
@@ -1,6 +1,6 @@
 import { Control, ControlGroup, Validators } from '@angular/common';
 import { Component, ElementRef, OnInit } from '@angular/core';
-import { Router } from '@angular/router-deprecated';
+import { Router } from '@angular/router';
 
 import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
 import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar';
index 059317383fe8e1dbaacb219156dadd31ce7b0e3f..46263eb659a96d9e217f8d69a32fc9db58a7b708 100644 (file)
@@ -1,5 +1,5 @@
 import { Component, OnInit } from '@angular/core';
-import { Router, ROUTER_DIRECTIVES, RouteParams } from '@angular/router-deprecated';
+import { Router, ROUTER_DIRECTIVES, RouteSegment } from '@angular/router';
 
 import { PAGINATION_DIRECTIVES } from 'ng2-bootstrap/components/pagination';
 
@@ -13,6 +13,7 @@ import {
 import { AuthService, Search, SearchField, User } from '../../shared';
 import { VideoMiniatureComponent } from './video-miniature.component';
 import { VideoSortComponent } from './video-sort.component';
+import { SearchService } from '../../shared';
 
 @Component({
   selector: 'my-videos-list',
@@ -37,22 +38,26 @@ export class VideoListComponent implements OnInit {
   constructor(
     private authService: AuthService,
     private router: Router,
-    private routeParams: RouteParams,
-    private videoService: VideoService
-  ) {
-    this.search = {
-      value: this.routeParams.get('search'),
-      field: <SearchField>this.routeParams.get('field')
-    };
-
-    this.sort = <SortField>this.routeParams.get('sort') || '-createdDate';
-  }
+    private routeSegment: RouteSegment,
+    private videoService: VideoService,
+    private searchService: SearchService // Temporary
+  ) {}
 
   ngOnInit() {
     if (this.authService.isLoggedIn()) {
       this.user = User.load();
     }
 
+    this.search = {
+      value: this.routeSegment.getParam('search'),
+      field: <SearchField>this.routeSegment.getParam('field')
+    };
+
+    // Temporary
+    this.searchChanged(this.search);
+
+    this.sort = <SortField>this.routeSegment.getParam('sort') || '-createdDate';
+
     this.getVideos();
   }
 
@@ -62,7 +67,7 @@ export class VideoListComponent implements OnInit {
 
     let observable = null;
 
-    if (this.search.value !== null) {
+    if (this.search.value) {
       observable = this.videoService.searchVideos(this.search, this.pagination, this.sort);
     } else {
       observable = this.videoService.getVideos(this.pagination, this.sort);
@@ -99,7 +104,10 @@ export class VideoListComponent implements OnInit {
       params.search = this.search.value;
     }
 
-    this.router.navigate(['VideosList', params]);
-    this.getVideos();
+    this.router.navigate(['/videos/list', params]);
+  }
+
+  searchChanged(search: Search) {
+    this.searchService.searchChanged.next(search);
   }
 }
index 244254b5a8b81aeed7df693ff577d87087ac8a77..92e19bb8b1a9cf8c5bfc2e4ff5283bbe2c7e3804 100644 (file)
@@ -1,6 +1,6 @@
 <div class="video-miniature col-md-4" (mouseenter)="onHover()" (mouseleave)="onBlur()">
   <a
-    [routerLink]="['VideosWatch', { id: video.id }]" [attr.title]="video.description"
+    [routerLink]="['/videos/watch', video.id]" [attr.title]="video.description"
     class="video-miniature-thumbnail"
   >
     <img [attr.src]="video.thumbnailPath" alt="video thumbnail" />
   ></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-name-tags">
+      <a [routerLink]="['/videos/watch', video.id]" class="video-miniature-name">{{ video.name }}</a>
 
-    <span class="video-miniature-author">by {{ video.by }}</span>
+      <span *ngFor="let tag of video.tags" class="video-miniature-tag">
+        <a [routerLink]="['/videos/list', { field: 'tags', search: tag }]" class="label label-primary">{{ tag }}</a>
+      </span>
+    </span>
+
+    <a [routerLink]="['/videos/list', { field: 'author', search: video.author }]" class="video-miniature-author">by {{ video.by }}</a>
     <span class="video-miniature-created-date">on {{ video.createdDate | date:'short' }}</span>
   </div>
 </div>
index 3aa0ca63b57f74e14de3b9e3faa91cbacf3c70e2..40d37b83f09ac55a2a85db3e14bd56e936b0be86 100644 (file)
@@ -1,5 +1,5 @@
 .video-miniature {
-  height: 200px;
+  margin-top: 30px;
   display: inline-block;
   position: relative;
 
 
   .video-miniature-informations {
     margin-left: 3px;
+    width: 200px;
 
-    .video-miniature-name {
+    .video-miniature-name-tags {
       display: block;
-      font-weight: bold;
 
-      &:hover {
-        text-decoration: none;
+      .video-miniature-name {
+        font-weight: bold;
+
+        &:hover {
+          text-decoration: none;
+        }
+
+        &::after {
+          content: '\002022';
+          margin-left: 3px;
+        }
+      }
+
+      .video-miniature-tag {
+        font-size: 12px;
+        cursor: pointer;
+        transition: opacity 0.5s;
+        position: relative;
+        top: -2px;
+
+        &:hover {
+          opacity: 0.9;
+        }
       }
     }
 
       display: block;
       margin-left: 1px;
       font-size: 11px;
-      color: rgba(0, 0, 0, 0.5);
+      color: rgb(54, 118, 173);
+    }
+
+    .video-miniature-author {
+      transition: opacity 0.5s;
+
+      &:hover {
+        text-decoration: none;
+        opacity: 0.9;
+      }
     }
   }
 }
index 639339b445fd6802fc557896c8ec5c92cffefea4..90645d67ff36c79cc3cff257a2f64a7b59e00e22 100644 (file)
@@ -1,6 +1,6 @@
 import { DatePipe } from '@angular/common';
 import { Component, Input, Output, EventEmitter } from '@angular/core';
-import { ROUTER_DIRECTIVES } from '@angular/router-deprecated';
+import { ROUTER_DIRECTIVES } from '@angular/router';
 
 import { Video, VideoService } from '../shared';
 import { User } from '../../shared';
index 05e844f60e5be0569ade01a93de61400c85e2a5a..99188bfb376a15a553e6566ac0326732fc6e34d2 100644 (file)
@@ -1,5 +1,5 @@
 import { Component, ElementRef, OnInit } from '@angular/core';
-import { CanDeactivate, ComponentInstruction, RouteParams } from '@angular/router-deprecated';
+import { CanDeactivate, RouteSegment } from '@angular/router';
 
 import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
 
@@ -30,7 +30,7 @@ export class VideoWatchComponent implements OnInit, CanDeactivate {
 
   constructor(
     private elementRef: ElementRef,
-    private routeParams: RouteParams,
+    private routeSegment: RouteSegment,
     private videoService: VideoService,
     private webTorrentService: WebTorrentService
   ) {}
@@ -74,7 +74,7 @@ export class VideoWatchComponent implements OnInit, CanDeactivate {
   }
 
   ngOnInit() {
-    let id = this.routeParams.get('id');
+    let id = this.routeSegment.getParam('id');
     this.videoService.getVideo(id).subscribe(
       video => {
         this.video = video;
@@ -84,11 +84,11 @@ export class VideoWatchComponent implements OnInit, CanDeactivate {
     );
   }
 
-  routerCanDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
+  routerCanDeactivate() {
     console.log('Removing video from webtorrent.');
     clearInterval(this.torrentInfosInterval);
     this.webTorrentService.remove(this.video.magnetUri);
-    return true;
+    return Promise.resolve(true);
   }
 
   private loadTooLong() {
index 437d05822634bdc4201e29d36fc9b41f7e8d0109..cf152457801d68615ed2466ca161586910b5aed5 100644 (file)
@@ -9,7 +9,7 @@ import '@angular/platform-browser-dynamic';
 import '@angular/core';
 import '@angular/common';
 import '@angular/http';
-import '@angular/router-deprecated';
+import '@angular/router';
 
 // RxJS
 import 'rxjs/Observable';
index fdcf742ea09cba4ca8541687c50ac505f0f927ba..54b931ba6ae6350b32bf2bf0b156d861e223431f 100644 (file)
@@ -37,6 +37,7 @@
     "src/app/shared/search/search-field.type.ts",
     "src/app/shared/search/search.component.ts",
     "src/app/shared/search/search.model.ts",
+    "src/app/shared/search/search.service.ts",
     "src/app/shared/users/auth-status.model.ts",
     "src/app/shared/users/auth.service.ts",
     "src/app/shared/users/index.ts",