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">
<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>

View 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);
}

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

View 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') {

View File

@ -1,3 +1,4 @@
export * from './search-field.type';
export * from './search.component';
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 { 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() {

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;
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);

View File

@ -21,12 +21,12 @@
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>

View 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';

View 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);
}
}

View 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" />
@ -12,11 +12,15 @@
></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>

View File

@ -1,5 +1,5 @@
.video-miniature {
height: 200px;
margin-top: 30px;
display: inline-block;
position: relative;
@ -35,13 +35,34 @@
.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;
}
}
}
@ -49,7 +70,16 @@
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;
}
}
}
}

View 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';

View 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() {

View 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';

View 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",