Add tags support to the video list

pull/10/head
Chocobozzz 2016-06-10 17:43:40 +02:00
parent e822fdaeee
commit 00a446454d
17 changed files with 140 additions and 64 deletions

View File

@ -14,40 +14,40 @@
<div class="row"> <div class="row">
<menu class="col-md-2 col-xs-3"> <menu class="col-md-2 col-xs-3">
<div class="panel_block"> <div class="panel-block">
<div id="panel_user_login" class="panel_button"> <div id="panel-user-login" class="panel-button">
<span class="glyphicon glyphicon-user"></span> <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> <a *ngIf="isLoggedIn" (click)="logout()">Logout</a>
</div> </div>
</div> </div>
<div class="panel_block"> <div class="panel-block">
<div id="panel_get_videos" class="panel_button"> <div id="panel-get-videos" class="panel-button">
<span class="glyphicon glyphicon-list"></span> <span class="glyphicon glyphicon-list"></span>
<a [routerLink]="['VideosList']">Get videos</a> <a [routerLink]="['/videos/list']">Get videos</a>
</div> </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> <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> </div>
<div class="panel_block" *ngIf="isLoggedIn"> <div class="panel-block" *ngIf="isLoggedIn">
<div id="panel_make_friends" class="panel_button"> <div id="panel-make-friends" class="panel-button">
<span class="glyphicon glyphicon-cloud"></span> <span class="glyphicon glyphicon-cloud"></span>
<a (click)='makeFriends()'>Make friends</a> <a (click)='makeFriends()'>Make friends</a>
</div> </div>
<div id="panel_quit_friends" class="panel_button"> <div id="panel-quit-friends" class="panel-button">
<span class="glyphicon glyphicon-plane"></span> <span class="glyphicon glyphicon-plane"></span>
<a (click)='quitFriends()'>Quit friends</a> <a (click)='quitFriends()'>Quit friends</a>
</div> </div>
</div> </div>
</menu> </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> <router-outlet></router-outlet>
</div> </div>

View File

@ -8,7 +8,7 @@ menu {
margin-right: 20px; margin-right: 20px;
border-right: 1px solid rgba(0, 0, 0, 0.2); border-right: 1px solid rgba(0, 0, 0, 0.2);
.panel_button { .panel-button {
margin: 8px; margin: 8px;
cursor: pointer; cursor: pointer;
transition: margin 0.2s; 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); border-bottom: 1px solid rgba(0, 0, 0, 0.1);
} }

View File

@ -1,6 +1,6 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { HTTP_PROVIDERS } from '@angular/http'; 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 { FriendService } from './friends';
import { LoginComponent } from './login'; import { LoginComponent } from './login';
@ -16,27 +16,23 @@ import {
VideoWatchComponent, VideoWatchComponent,
VideoService VideoService
} from './videos'; } from './videos';
import { SearchService } from './shared'; // Temporary
@RouteConfig([ @Routes([
{ {
path: '/users/login', path: '/users/login',
name: 'UserLogin',
component: LoginComponent component: LoginComponent
}, },
{ {
path: '/videos/list', path: '/videos/list',
name: 'VideosList', component: VideoListComponent
component: VideoListComponent,
useAsDefault: true
}, },
{ {
path: '/videos/watch/:id', path: '/videos/watch/:id',
name: 'VideosWatch',
component: VideoWatchComponent component: VideoWatchComponent
}, },
{ {
path: '/videos/add', path: '/videos/add',
name: 'VideosAdd',
component: VideoAddComponent component: VideoAddComponent
} }
]) ])
@ -46,7 +42,7 @@ import {
template: require('./app.component.html'), template: require('./app.component.html'),
styles: [ require('./app.component.scss') ], styles: [ require('./app.component.scss') ],
directives: [ ROUTER_DIRECTIVES, SearchComponent ], directives: [ ROUTER_DIRECTIVES, SearchComponent ],
providers: [ AuthService, FriendService, HTTP_PROVIDERS, ROUTER_PROVIDERS, VideoService ] providers: [ AuthService, FriendService, HTTP_PROVIDERS, ROUTER_PROVIDERS, VideoService, SearchService ]
}) })
export class AppComponent { export class AppComponent {
@ -75,12 +71,13 @@ export class AppComponent {
field: search.field, field: search.field,
search: search.value search: search.value
}; };
this.router.navigate(['VideosList', params]); this.router.navigate(['/videos/list', params]);
} else { } else {
this.router.navigate(['VideosList']); this.router.navigate(['/videos/list']);
} }
} }
// FIXME
logout() { logout() {
// this._authService.logout(); // this._authService.logout();
} }

View File

@ -1,5 +1,5 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Router } from '@angular/router-deprecated'; import { Router } from '@angular/router';
import { AuthService, AuthStatus, User } from '../shared'; import { AuthService, AuthStatus, User } from '../shared';
@ -26,7 +26,7 @@ export class LoginComponent {
this.authService.setStatus(AuthStatus.LoggedIn); this.authService.setStatus(AuthStatus.LoggedIn);
this.router.navigate(['VideosList']); this.router.navigate(['/videos/list']);
}, },
error => { error => {
if (error.error === 'invalid_grant') { if (error.error === 'invalid_grant') {

View File

@ -1,3 +1,4 @@
export * from './search-field.type'; export * from './search-field.type';
export * from './search.component'; export * from './search.component';
export * from './search.model'; export * from './search.model';
export * from './search.service';

View 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 { DROPDOWN_DIRECTIVES} from 'ng2-bootstrap/components/dropdown';
import { Search } from './search.model'; import { Search } from './search.model';
import { SearchField } from './search-field.type'; import { SearchField } from './search-field.type';
import { SearchService } from './search.service'; // Temporary
@Component({ @Component({
selector: 'my-search', selector: 'my-search',
@ -11,7 +12,7 @@ import { SearchField } from './search-field.type';
directives: [ DROPDOWN_DIRECTIVES ] directives: [ DROPDOWN_DIRECTIVES ]
}) })
export class SearchComponent { export class SearchComponent implements OnInit {
@Output() search = new EventEmitter<Search>(); @Output() search = new EventEmitter<Search>();
fieldChoices = { fieldChoices = {
@ -26,6 +27,21 @@ export class SearchComponent {
value: '' 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() { get choiceKeys() {
return Object.keys(this.fieldChoices); return Object.keys(this.fieldChoices);
} }
@ -35,6 +51,7 @@ export class SearchComponent {
$event.stopPropagation(); $event.stopPropagation();
this.searchCriterias.field = choice; this.searchCriterias.field = choice;
this.doSearch();
} }
doSearch() { doSearch() {

View File

@ -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>();
}
}

View File

@ -9,6 +9,7 @@ export class Video {
magnetUri: string; magnetUri: string;
name: string; name: string;
podUrl: string; podUrl: string;
tags: string[];
thumbnailPath: string; thumbnailPath: string;
private static createByString(author: string, podUrl: string) { private static createByString(author: string, podUrl: string) {
@ -42,6 +43,7 @@ export class Video {
magnetUri: string, magnetUri: string,
name: string, name: string,
podUrl: string, podUrl: string,
tags: string[],
thumbnailPath: string thumbnailPath: string
}) { }) {
this.author = hash.author; this.author = hash.author;
@ -53,6 +55,7 @@ export class Video {
this.magnetUri = hash.magnetUri; this.magnetUri = hash.magnetUri;
this.name = hash.name; this.name = hash.name;
this.podUrl = hash.podUrl; this.podUrl = hash.podUrl;
this.tags = hash.tags;
this.thumbnailPath = hash.thumbnailPath; this.thumbnailPath = hash.thumbnailPath;
this.by = Video.createByString(hash.author, hash.podUrl); this.by = Video.createByString(hash.author, hash.podUrl);

View File

@ -21,12 +21,12 @@
ngControl="tags" #tags="ngForm" [disabled]="isTagsInputDisabled" (keyup)="onTagKeyPress($event)" [(ngModel)]="currentTag" ngControl="tags" #tags="ngForm" [disabled]="isTagsInputDisabled" (keyup)="onTagKeyPress($event)" [(ngModel)]="currentTag"
> >
<div [hidden]="tags.valid || tags.pristine" class="alert alert-warning"> <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> </div>
<div class="tags"> <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 }} {{ tag }}
<span class="remove" (click)="removeTag(tag)">x</span> <span class="remove" (click)="removeTag(tag)">x</span>
</div> </div>

View File

@ -1,6 +1,6 @@
import { Control, ControlGroup, Validators } from '@angular/common'; import { Control, ControlGroup, Validators } from '@angular/common';
import { Component, ElementRef, OnInit } from '@angular/core'; 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 { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar'; import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar';

View File

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core'; 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'; import { PAGINATION_DIRECTIVES } from 'ng2-bootstrap/components/pagination';
@ -13,6 +13,7 @@ import {
import { AuthService, Search, SearchField, User } from '../../shared'; import { AuthService, Search, SearchField, User } from '../../shared';
import { VideoMiniatureComponent } from './video-miniature.component'; import { VideoMiniatureComponent } from './video-miniature.component';
import { VideoSortComponent } from './video-sort.component'; import { VideoSortComponent } from './video-sort.component';
import { SearchService } from '../../shared';
@Component({ @Component({
selector: 'my-videos-list', selector: 'my-videos-list',
@ -37,22 +38,26 @@ export class VideoListComponent implements OnInit {
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private router: Router, private router: Router,
private routeParams: RouteParams, private routeSegment: RouteSegment,
private videoService: VideoService private videoService: VideoService,
) { private searchService: SearchService // Temporary
this.search = { ) {}
value: this.routeParams.get('search'),
field: <SearchField>this.routeParams.get('field')
};
this.sort = <SortField>this.routeParams.get('sort') || '-createdDate';
}
ngOnInit() { ngOnInit() {
if (this.authService.isLoggedIn()) { if (this.authService.isLoggedIn()) {
this.user = User.load(); 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(); this.getVideos();
} }
@ -62,7 +67,7 @@ export class VideoListComponent implements OnInit {
let observable = null; let observable = null;
if (this.search.value !== null) { if (this.search.value) {
observable = this.videoService.searchVideos(this.search, this.pagination, this.sort); observable = this.videoService.searchVideos(this.search, this.pagination, this.sort);
} else { } else {
observable = this.videoService.getVideos(this.pagination, this.sort); observable = this.videoService.getVideos(this.pagination, this.sort);
@ -99,7 +104,10 @@ export class VideoListComponent implements OnInit {
params.search = this.search.value; params.search = this.search.value;
} }
this.router.navigate(['VideosList', params]); this.router.navigate(['/videos/list', params]);
this.getVideos(); }
searchChanged(search: Search) {
this.searchService.searchChanged.next(search);
} }
} }

View File

@ -1,6 +1,6 @@
<div class="video-miniature col-md-4" (mouseenter)="onHover()" (mouseleave)="onBlur()"> <div class="video-miniature col-md-4" (mouseenter)="onHover()" (mouseleave)="onBlur()">
<a <a
[routerLink]="['VideosWatch', { id: 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 [attr.src]="video.thumbnailPath" alt="video thumbnail" />
@ -12,11 +12,15 @@
></span> ></span>
<div class="video-miniature-informations"> <div class="video-miniature-informations">
<a [routerLink]="['VideosWatch', { id: video.id }]" class="video-miniature-name"> <span class="video-miniature-name-tags">
<span>{{ video.name }}</span> <a [routerLink]="['/videos/watch', video.id]" class="video-miniature-name">{{ video.name }}</a>
</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> <span class="video-miniature-created-date">on {{ video.createdDate | date:'short' }}</span>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
.video-miniature { .video-miniature {
height: 200px; margin-top: 30px;
display: inline-block; display: inline-block;
position: relative; position: relative;
@ -35,21 +35,51 @@
.video-miniature-informations { .video-miniature-informations {
margin-left: 3px; margin-left: 3px;
width: 200px;
.video-miniature-name-tags {
display: block;
.video-miniature-name { .video-miniature-name {
display: block;
font-weight: bold; font-weight: bold;
&:hover { &:hover {
text-decoration: none; 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;
}
}
} }
.video-miniature-author, .video-miniature-created-date { .video-miniature-author, .video-miniature-created-date {
display: block; display: block;
margin-left: 1px; margin-left: 1px;
font-size: 11px; 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;
}
} }
} }
} }

View File

@ -1,6 +1,6 @@
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { Component, Input, Output, EventEmitter } from '@angular/core'; 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 { Video, VideoService } from '../shared';
import { User } from '../../shared'; import { User } from '../../shared';

View File

@ -1,5 +1,5 @@
import { Component, ElementRef, OnInit } from '@angular/core'; 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'; import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
@ -30,7 +30,7 @@ export class VideoWatchComponent implements OnInit, CanDeactivate {
constructor( constructor(
private elementRef: ElementRef, private elementRef: ElementRef,
private routeParams: RouteParams, private routeSegment: RouteSegment,
private videoService: VideoService, private videoService: VideoService,
private webTorrentService: WebTorrentService private webTorrentService: WebTorrentService
) {} ) {}
@ -74,7 +74,7 @@ export class VideoWatchComponent implements OnInit, CanDeactivate {
} }
ngOnInit() { ngOnInit() {
let id = this.routeParams.get('id'); let id = this.routeSegment.getParam('id');
this.videoService.getVideo(id).subscribe( this.videoService.getVideo(id).subscribe(
video => { video => {
this.video = 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.'); console.log('Removing video from webtorrent.');
clearInterval(this.torrentInfosInterval); clearInterval(this.torrentInfosInterval);
this.webTorrentService.remove(this.video.magnetUri); this.webTorrentService.remove(this.video.magnetUri);
return true; return Promise.resolve(true);
} }
private loadTooLong() { private loadTooLong() {

View File

@ -9,7 +9,7 @@ import '@angular/platform-browser-dynamic';
import '@angular/core'; import '@angular/core';
import '@angular/common'; import '@angular/common';
import '@angular/http'; import '@angular/http';
import '@angular/router-deprecated'; import '@angular/router';
// RxJS // RxJS
import 'rxjs/Observable'; import 'rxjs/Observable';

View File

@ -37,6 +37,7 @@
"src/app/shared/search/search-field.type.ts", "src/app/shared/search/search-field.type.ts",
"src/app/shared/search/search.component.ts", "src/app/shared/search/search.component.ts",
"src/app/shared/search/search.model.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-status.model.ts",
"src/app/shared/users/auth.service.ts", "src/app/shared/users/auth.service.ts",
"src/app/shared/users/index.ts", "src/app/shared/users/index.ts",