typings
-angular/**/*.js
-angular/**/*.map
-angular/**/*.css
+app/**/*.js
+app/**/*.map
+app/**/*.css
stylesheets/index.css
bundles
+main.js
+main.js.map
+++ /dev/null
-<div class="container">
-
- <header class="row">
- <div class="col-md-2">
- <h4>PeerTube</h4>
- </div>
-
- <div class="col-md-9">
- <my-search (search)="onSearch($event)"></my-search>
- </div>
- </header>
-
-
- <div class="row">
-
- <menu class="col-md-2 col-xs-3">
- <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" (click)="logout()">Logout</a>
- </div>
- </div>
-
- <div class="panel_block">
- <div id="panel_get_videos" class="panel_button">
- <span class="glyphicon glyphicon-list"></span>
- <a [routerLink]="['VideosList']">Get videos</a>
- </div>
-
- <div id="panel_upload_video" class="panel_button" *ngIf="isLoggedIn">
- <span class="glyphicon glyphicon-cloud-upload"></span>
- <a [routerLink]="['VideosAdd']">Upload a video</a>
- </div>
- </div>
-
- <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">
- <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">
- <router-outlet></router-outlet>
- </div>
-
- </div>
-
-
- <footer>
- PeerTube, CopyLeft 2015-2016
- </footer>
-</div>
+++ /dev/null
-header div {
- line-height: 25px;
- margin-bottom: 30px;
-}
-
-menu {
- min-height: 600px;
- margin-right: 20px;
- border-right: 1px solid rgba(0, 0, 0, 0.2);
-
- .panel_button {
- margin: 8px;
- cursor: pointer;
- transition: margin 0.2s;
-
- &:hover {
- margin-left: 15px;
- }
-
- a {
- color: #333333;
- }
- }
-
- .glyphicon {
- margin: 5px;
- }
-}
-
-.panel_block:not(:last-child) {
- border-bottom: 1px solid rgba(0, 0, 0, 0.1);
-}
+++ /dev/null
-import { Component } from '@angular/core';
-import { RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS, Router } from '@angular/router-deprecated';
-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/videos.service';
-import { FriendsService } from '../friends/services/friends.service';
-import { UserLoginComponent } from '../users/components/login/login.component';
-import { AuthService } from '../users/services/auth.service';
-import { AuthStatus } from '../users/models/authStatus';
-import { SearchComponent } from './search.component';
-import { Search } from './search';
-
-@RouteConfig([
- {
- path: '/users/login',
- name: 'UserLogin',
- component: UserLoginComponent
- },
- {
- path: '/videos/list',
- name: 'VideosList',
- component: VideosListComponent,
- useAsDefault: true
- },
- {
- path: '/videos/watch/:id',
- name: 'VideosWatch',
- component: VideosWatchComponent
- },
- {
- path: '/videos/add',
- name: 'VideosAdd',
- component: VideosAddComponent
- }
-])
-
-@Component({
- selector: 'my-app',
- templateUrl: 'app/angular/app/app.component.html',
- styleUrls: [ 'app/angular/app/app.component.css' ],
- directives: [ ROUTER_DIRECTIVES, SearchComponent ],
- providers: [ ROUTER_PROVIDERS, HTTP_PROVIDERS, VideosService, FriendsService, AuthService ]
-})
-
-export class AppComponent {
- isLoggedIn: boolean;
- search_field: string = name;
- choices = [ ];
-
- constructor(private _friendsService: FriendsService,
- private _authService: AuthService,
- private _router: Router
-
- ) {
- this.isLoggedIn = this._authService.isLoggedIn();
-
- this._authService.loginChanged$.subscribe(
- status => {
- if (status === AuthStatus.LoggedIn) {
- this.isLoggedIn = true;
- }
- }
- );
- }
-
- onSearch(search: Search) {
- if (search.value !== '') {
- const params = {
- search: search.value,
- field: search.field
- };
- this._router.navigate(['VideosList', params]);
- } else {
- this._router.navigate(['VideosList']);
- }
- }
-
- logout() {
- // this._authService.logout();
- }
-
- makeFriends() {
- this._friendsService.makeFriends().subscribe(
- status => {
- if (status === 409) {
- alert('Already made friends!');
- } else {
- alert('Made friends!');
- }
- },
- error => alert(error)
- );
- }
-
- quitFriends() {
- this._friendsService.quitFriends().subscribe(
- status => {
- alert('Quit friends!');
- },
- error => alert(error)
- );
- }
-}
+++ /dev/null
-<div class="input-group">
- <div class="input-group-btn" dropdown>
- <button id="simple-btn-keyboard-nav" type="button" class="btn btn-default" dropdownToggle>
- {{ getStringChoice(searchCriterias.field) }} <span class="caret"></span>
- </button>
- <ul class="dropdown-menu" role="menu" aria-labelledby="simple-btn-keyboard-nav">
- <li *ngFor="let choice of choiceKeys" class="dropdown-item">
- <a class="dropdown-item" href="#" (click)="choose($event, choice)">{{ getStringChoice(choice) }}</a>
- </li>
- </ul>
- </div>
-
- <input
- type="text" id="search-video" name="search-video" class="form-control" placeholder="Search a video..." class="form-control"
- [(ngModel)]="searchCriterias.value" (keyup.enter)="doSearch()"
- >
-</div>
+++ /dev/null
-import { Component, EventEmitter, Output } from '@angular/core';
-
-import { DROPDOWN_DIRECTIVES} from 'ng2-bootstrap/components/dropdown';
-
-import { Search, SearchField } from './search';
-
-@Component({
- selector: 'my-search',
- templateUrl: 'app/angular/app/search.component.html',
- directives: [ DROPDOWN_DIRECTIVES ]
-})
-
-export class SearchComponent {
- @Output() search: EventEmitter<Search> = new EventEmitter<Search>();
-
- searchCriterias: Search = {
- field: 'name',
- value: ''
- };
- fieldChoices = {
- name: 'Name',
- author: 'Author',
- podUrl: 'Pod Url',
- magnetUri: 'Magnet Uri'
- };
-
- get choiceKeys() {
- return Object.keys(this.fieldChoices);
- }
-
- getStringChoice(choiceKey: SearchField): string {
- return this.fieldChoices[choiceKey];
- }
-
- choose($event:MouseEvent, choice: SearchField) {
- $event.preventDefault();
- $event.stopPropagation();
-
- this.searchCriterias.field = choice;
- }
-
- doSearch(): void {
- this.search.emit(this.searchCriterias);
- }
-
-}
+++ /dev/null
-export type SearchField = "name" | "author" | "podUrl" | "magnetUri";
-
-export interface Search {
- field: SearchField;
- value: string;
-}
+++ /dev/null
-import { Injectable } from '@angular/core';
-import { Http, Response } from '@angular/http';
-import { Observable } from 'rxjs/Rx';
-
-@Injectable()
-export class FriendsService {
- private _baseFriendsUrl = '/api/v1/pods/';
-
- constructor (private http: Http) {}
-
- makeFriends() {
- return this.http.get(this._baseFriendsUrl + 'makefriends')
- .map(res => <number> res.status)
- .catch(this.handleError);
- }
-
- quitFriends() {
- return this.http.get(this._baseFriendsUrl + 'quitfriends')
- .map(res => <number> res.status)
- .catch(this.handleError);
- }
-
- private handleError (error: Response) {
- console.error(error);
- return Observable.throw(error.json().error || 'Server error');
- }
-}
+++ /dev/null
-import { bootstrap } from '@angular/platform-browser-dynamic';
-import { AppComponent } from './app/app.component';
-
-bootstrap(AppComponent);
+++ /dev/null
-<h3>Login</h3>
-<form role="form" (submit)="login(username.value, password.value)">
- <div class="form-group">
- <label for="username">Username</label>
- <input type="text" #username class="form-control" id="username" placeholder="Username">
- </div>
-
- <div class="form-group">
- <label for="password">Password</label>
- <input type="password" #password class="form-control" id="password" placeholder="Password">
- </div>
-
- <input type="submit" value="Login" class="btn btn-default">
-</form>
+++ /dev/null
-import { Component } from '@angular/core';
-import { Router } from '@angular/router-deprecated';
-
-import { AuthService } from '../../services/auth.service';
-import { AuthStatus } from '../../models/authStatus';
-import { User } from '../../models/user';
-
-@Component({
- selector: 'my-user-login',
- styleUrls: [ 'app/angular/users/components/login/login.component.css' ],
- templateUrl: 'app/angular/users/components/login/login.component.html'
-})
-
-export class UserLoginComponent {
- constructor(private _authService: AuthService, private _router: Router) {}
-
- login(username: string, password: string) {
- this._authService.login(username, password).subscribe(
- result => {
- const user = new User(username, result);
- user.save();
-
- this._authService.setStatus(AuthStatus.LoggedIn);
-
- this._router.navigate(['VideosList']);
- },
- error => {
- if (error.error === 'invalid_grant') {
- alert('Credentials are invalid.');
- } else {
- alert(`${error.error}: ${error.error_description}`);
- }
- }
- );
- }
-}
+++ /dev/null
-export enum AuthStatus {
- LoggedIn,
- LoggedOut
-}
+++ /dev/null
-export class Token {
- access_token: string;
- refresh_token: string;
- token_type: string;
-
- static load(): Token {
- return new Token({
- access_token: localStorage.getItem('access_token'),
- refresh_token: localStorage.getItem('refresh_token'),
- token_type: localStorage.getItem('token_type')
- });
- }
-
- constructor (hash?: any) {
- if (hash) {
- this.access_token = hash.access_token;
- this.refresh_token = hash.refresh_token;
- if (hash.token_type === 'bearer') {
- this.token_type = 'Bearer';
- } else {
- this.token_type = hash.token_type;
- }
- }
- }
-
- save():void {
- localStorage.setItem('access_token', this.access_token);
- localStorage.setItem('refresh_token', this.refresh_token);
- localStorage.setItem('token_type', this.token_type);
- }
-}
+++ /dev/null
-import { Token } from './token';
-
-export class User {
- username: string;
- token: Token;
-
- static load(): User {
- return new User(localStorage.getItem('username'), Token.load());
- }
-
- constructor (username: string, hash_token: any) {
- this.username = username;
- this.token = new Token(hash_token);
- }
-
- save(): void {
- localStorage.setItem('username', this.username);
- this.token.save();
- }
-}
+++ /dev/null
-import { Injectable } from '@angular/core';
-import { Http, Response, Headers, URLSearchParams, RequestOptions } from '@angular/http';
-import { Observable, Subject } from 'rxjs/Rx';
-
-import { AuthStatus } from '../models/authStatus';
-import { User } from '../models/user';
-
-@Injectable()
-export class AuthService {
- loginChanged$;
-
- private _loginChanged;
- private _baseLoginUrl = '/api/v1/users/token';
- private _baseClientUrl = '/api/v1/users/client';
- private _clientId = '';
- private _clientSecret = '';
-
- constructor (private http: Http) {
- this._loginChanged = new Subject<AuthStatus>();
- this.loginChanged$ = this._loginChanged.asObservable();
-
- // Fetch the client_id/client_secret
- // FIXME: save in local storage?
- this.http.get(this._baseClientUrl)
- .map(res => res.json())
- .catch(this.handleError)
- .subscribe(
- result => {
- this._clientId = result.client_id;
- this._clientSecret = result.client_secret;
- console.log('Client credentials loaded.');
- },
- error => {
- alert(error);
- }
- );
- }
-
- login(username: string, password: string) {
- let body = new URLSearchParams();
- body.set('client_id', this._clientId);
- body.set('client_secret', this._clientSecret);
- body.set('response_type', 'code');
- body.set('grant_type', 'password');
- body.set('scope', 'upload');
- body.set('username', username);
- body.set('password', password);
-
- let headers = new Headers();
- headers.append('Content-Type', 'application/x-www-form-urlencoded');
-
- let options = {
- headers: headers
- };
-
- return this.http.post(this._baseLoginUrl, body.toString(), options)
- .map(res => res.json())
- .catch(this.handleError);
- }
-
- logout() {
- // TODO make HTTP request
- }
-
- getRequestHeader(): Headers {
- return new Headers({ 'Authorization': `${this.getTokenType()} ${this.getToken()}` });
- }
-
- getAuthRequestOptions(): RequestOptions {
- return new RequestOptions({ headers: this.getRequestHeader() });
- }
-
- getToken(): string {
- return localStorage.getItem('access_token');
- }
-
- getTokenType(): string {
- return localStorage.getItem('token_type');
- }
-
- getUser(): User {
- if (this.isLoggedIn() === false) {
- return null;
- }
-
- const user = User.load();
-
- return user;
- }
-
- isLoggedIn(): boolean {
- if (this.getToken()) {
- return true;
- } else {
- return false;
- }
- }
-
- setStatus(status: AuthStatus) {
- this._loginChanged.next(status);
- }
-
- private handleError (error: Response) {
- console.error(error);
- return Observable.throw(error.json() || { error: 'Server error' });
- }
-}
+++ /dev/null
-<h3>Upload a video</h3>
-
-<form (ngSubmit)="uploadFile()" #videoForm="ngForm">
- <div class="form-group">
- <label for="name">Video name</label>
- <input
- type="text" class="form-control" name="name" id="name" required
- ngControl="name" #name="ngForm"
- >
- <div [hidden]="name.valid || name.pristine" class="alert alert-danger">
- Name is required
- </div>
- </div>
-
- <div class="form-group">
- <div class="btn btn-default btn-file">
- <span>Select the video...</span>
- <input type="file" name="videofile" id="videofile">
- </div>
-
- <span *ngIf="fileToUpload">{{ fileToUpload.name }}</span>
- </div>
-
- <div class="form-group">
- <label for="description">Description</label>
- <textarea
- name="description" id="description" class="form-control" placeholder="Description..." required
- ngControl="description" #description="ngForm"
- >
- </textarea>
- <div [hidden]="description.valid || description.pristine" class="alert alert-danger">
- A description is required
- </div>
- </div>
-
- <div id="progress" *ngIf="progressBar.max !== 0">
- <progressbar [value]="progressBar.value" [max]="progressBar.max">{{ progressBar.value | bytes }} / {{ progressBar.max | bytes }}</progressbar>
- </div>
-
- <input type="submit" value="Upload" class="btn btn-default" [disabled]="!videoForm.form.valid || !fileToUpload">
-</form>
+++ /dev/null
-.btn-file {
- position: relative;
- overflow: hidden;
-}
-
-.btn-file input[type=file] {
- position: absolute;
- top: 0;
- right: 0;
- min-width: 100%;
- min-height: 100%;
- font-size: 100px;
- text-align: right;
- filter: alpha(opacity=0);
- opacity: 0;
- outline: none;
- background: white;
- cursor: inherit;
- display: block;
-}
-
-.name_file {
- display: inline-block;
- margin-left: 10px;
-}
-
-.form-group {
- margin-bottom: 10px;
-}
-
-#progress {
- margin-bottom: 10px;
-}
+++ /dev/null
-import { Component, ElementRef, OnInit } from '@angular/core';
-import { Router } from '@angular/router-deprecated';
-
-import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar';
-import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
-
-import { AuthService } from '../../../users/services/auth.service';
-import { User } from '../../../users/models/user';
-
-// TODO: import it with systemjs
-declare var jQuery:any;
-
-@Component({
- selector: 'my-videos-add',
- styleUrls: [ 'app/angular/videos/components/add/videos-add.component.css' ],
- templateUrl: 'app/angular/videos/components/add/videos-add.component.html',
- directives: [ PROGRESSBAR_DIRECTIVES ],
- pipes: [ BytesPipe ]
-})
-
-export class VideosAddComponent implements OnInit {
- user: User;
- fileToUpload: any;
- progressBar: { value: number; max: number; } = { value: 0, max: 0 };
-
- private _form: any;
-
- constructor(
- private _router: Router, private _elementRef: ElementRef,
- private _authService: AuthService
- ) {}
-
- ngOnInit() {
- this.user = User.load();
- jQuery(this._elementRef.nativeElement).find('#videofile').fileupload({
- url: '/api/v1/videos',
- dataType: 'json',
- singleFileUploads: true,
- multipart: true,
- autoupload: false,
-
- add: (e, data) => {
- this._form = data;
- this.fileToUpload = data['files'][0];
- },
-
- progressall: (e, data) => {
- this.progressBar.value = data.loaded;
- // The server is a little bit slow to answer (has to seed the video)
- // So we add more time to the progress bar (+10%)
- this.progressBar.max = data.total + (0.1 * data.total);
- },
-
- done: (e, data) => {
- this.progressBar.value = this.progressBar.max;
- console.log('Video uploaded.');
-
- // Print all the videos once it's finished
- this._router.navigate(['VideosList']);
- }
- });
- }
-
- uploadFile() {
- this._form.headers = this._authService.getRequestHeader().toJSON();
- this._form.formData = jQuery(this._elementRef.nativeElement).find('form').serializeArray();
- this._form.submit();
- }
-}
+++ /dev/null
-export type SortField = "name" | "-name"
- | "duration" | "-duration"
- | "createdDate" | "-createdDate";
+++ /dev/null
-<div class="video-miniature col-md-4" (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>
+++ /dev/null
-.video-miniature {
- height: 200px;
- display: inline-block;
- position: relative;
-
- .video-miniature-thumbnail {
- display: block;
- position: relative;
-
- .video-miniature-duration {
- position: absolute;
- right: 60px;
- 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: 16px;
- 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);
- }
- }
-}
+++ /dev/null
-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)
- );
- }
- }
-}
+++ /dev/null
-<select class="form-control input-sm" [(ngModel)]="currentSort" (ngModelChange)="onSortChange()">
- <option *ngFor="let choice of choiceKeys" [value]="choice">
- {{ getStringChoice(choice) }}
- </option>
-</select>
+++ /dev/null
-import { Component, Input, Output, EventEmitter } from '@angular/core';
-
-import { SortField } from './sort';
-
-@Component({
- selector: 'my-video-sort',
- // styleUrls: [ 'app/angular/videos/components/list/video-sort.component.css' ],
- templateUrl: 'app/angular/videos/components/list/video-sort.component.html'
-})
-
-export class VideoSortComponent {
- @Output() sort = new EventEmitter<any>();
-
- @Input() currentSort: SortField;
-
- sortChoices = {
- 'name': 'Name - Asc',
- '-name': 'Name - Desc',
- 'duration': 'Duration - Asc',
- '-duration': 'Duration - Desc',
- 'createdDate': 'Created Date - Asc',
- '-createdDate': 'Created Date - Desc'
- };
-
- get choiceKeys() {
- return Object.keys(this.sortChoices);
- }
-
- getStringChoice(choiceKey: SortField): string {
- return this.sortChoices[choiceKey];
- }
-
- onSortChange() {
- this.sort.emit(this.currentSort);
- }
-}
+++ /dev/null
-<div class="row videos-info">
- <div class="col-md-9 videos-total-results"> {{ pagination.total }} videos</div>
- <my-video-sort class="col-md-3" [currentSort]="sort" (sort)="onSort($event)"></my-video-sort>
-</div>
-
-<div class="videos-miniatures">
- <my-loader [loading]="loading"></my-loader>
-
- <div class="col-md-12 no-video" *ngIf="!loading && videos.length === 0">There is no video.</div>
-
- <my-video-miniature *ngFor="let video of videos" [video]="video" [user]="user" (removed)="onRemoved(video)">
- </my-video-miniature>
-</div>
-
-<pagination
- [totalItems]="pagination.total" [itemsPerPage]="pagination.itemsPerPage" [(ngModel)]="pagination.currentPage"
- (ngModelChange)="getVideos()"
-></pagination>
+++ /dev/null
-.videos-info {
-
- padding-bottom: 20px;
- margin-bottom: 20px;
- border-bottom: 1px solid #f1f1f1;
- height: 40px;
- line-height: 40px;
- width: 765px;
- margin-left: 15px;
-
- my-video-sort {
- padding-right: 0;
- }
-
- .videos-total-results {
- font-size: 13px;
- padding-left: 0;
- }
-}
-
-.videos-miniatures {
- min-height: 600px;
-
- my-videos-miniature {
- display: inline-block;
- }
-
- .no-video {
- margin-top: 50px;
- text-align: center;
- }
-}
-
-pagination {
- display: block;
- text-align: center;
-}
+++ /dev/null
-import { Component, OnInit } from '@angular/core';
-import { ROUTER_DIRECTIVES, RouteParams, Router } from '@angular/router-deprecated';
-
-import { PAGINATION_DIRECTIVES } from 'ng2-bootstrap/components/pagination';
-
-import { AuthService } from '../../../users/services/auth.service';
-import { Pagination } from '../../pagination';
-import { User } from '../../../users/models/user';
-import { VideosService } from '../../videos.service';
-import { Video } from '../../video';
-import { VideoMiniatureComponent } from './video-miniature.component';
-import { Search, SearchField } from '../../../app/search';
-import { VideoSortComponent } from './video-sort.component';
-import { SortField } from './sort';
-import { LoaderComponent } from '../../loader.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, PAGINATION_DIRECTIVES, VideoMiniatureComponent, VideoSortComponent, LoaderComponent ]
-})
-
-export class VideosListComponent implements OnInit {
- user: User = null;
- videos: Video[] = [];
- pagination: Pagination = {
- currentPage: 1,
- itemsPerPage: 9,
- total: 0
- };
- sort: SortField;
- loading: boolean = false;
-
- private search: Search;
-
- constructor(
- private _authService: AuthService,
- private _videosService: VideosService,
- private _routeParams: RouteParams,
- private _router: Router
- ) {
- this.search = {
- value: this._routeParams.get('search'),
- field: <SearchField>this._routeParams.get('field')
- };
-
- this.sort = <SortField>this._routeParams.get('sort') || '-createdDate';
- }
-
- ngOnInit() {
- if (this._authService.isLoggedIn()) {
- this.user = User.load();
- }
-
- this.getVideos();
- }
-
- getVideos() {
- this.loading = true;
- this.videos = [];
-
- let observable = null;
-
- if (this.search.value !== null) {
- observable = this._videosService.searchVideos(this.search, this.pagination, this.sort);
- } else {
- observable = this._videosService.getVideos(this.pagination, this.sort);
- }
-
- observable.subscribe(
- ({ videos, totalVideos }) => {
- this.videos = videos;
- this.pagination.total = totalVideos;
- this.loading = false;
- },
- error => alert(error)
- );
- }
-
- onRemoved(video: Video): void {
- this.videos.splice(this.videos.indexOf(video), 1);
- }
-
- onSort(sort: SortField) {
- this.sort = sort;
-
- const params: any = {
- sort: this.sort
- };
-
- if (this.search.value) {
- params.search = this.search.value;
- params.field = this.search.field;
- }
-
- this._router.navigate(['VideosList', params]);
- this.getVideos();
- }
-}
+++ /dev/null
-<my-loader [loading]="loading"></my-loader>
-
-<div class="embed-responsive embed-responsive-19by9">
-</div>
-
-<div id="torrent-info">
- <div id="torrent-info-download">Download: {{ downloadSpeed | bytes }}/s</div>
- <div id="torrent-info-upload">Upload: {{ uploadSpeed | bytes }}/s</div>
- <div id="torrent-info-peers">Number of peers: {{ numPeers }}</div>
-<div>
+++ /dev/null
-.embed-responsive {
- height: 500px;
-}
-
-#torrent-info {
- font-size: 10px;
-
- div {
- display: inline-block;
- width: 33%;
- text-align: center;
- }
-}
+++ /dev/null
-import { Component, OnInit, ElementRef } from '@angular/core';
-import { RouteParams, CanDeactivate, ComponentInstruction } from '@angular/router-deprecated';
-
-import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
-
-import { LoaderComponent } from '../../loader.component';
-
-// TODO import it with systemjs
-declare var WebTorrent: any;
-
-import { Video } from '../../video';
-import { VideosService } from '../../videos.service';
-
-@Component({
- selector: 'my-video-watch',
- templateUrl: 'app/angular/videos/components/watch/videos-watch.component.html',
- styleUrls: [ 'app/angular/videos/components/watch/videos-watch.component.css' ],
- directives: [ LoaderComponent ],
- pipes: [ BytesPipe ]
-})
-
-export class VideosWatchComponent implements OnInit, CanDeactivate {
- video: Video;
- downloadSpeed: number;
- uploadSpeed: number;
- numPeers: number;
- loading: boolean = false;
-
- private _interval: NodeJS.Timer;
- private client: any;
-
- constructor(
- private _videosService: VideosService,
- private _routeParams: RouteParams,
- private _elementRef: ElementRef
- ) {
- // TODO: use a service
- this.client = new WebTorrent({ dht: false });
- }
-
- ngOnInit() {
- let id = this._routeParams.get('id');
- this._videosService.getVideo(id).subscribe(
- video => this.loadVideo(video),
- error => alert(error)
- );
- }
-
- loadVideo(video: Video) {
- this.loading = true;
- this.video = video;
- console.log('Adding ' + this.video.magnetUri + '.');
- this.client.add(this.video.magnetUri, (torrent) => {
- this.loading = false;
- console.log('Added ' + this.video.magnetUri + '.');
- torrent.files[0].appendTo(this._elementRef.nativeElement.querySelector('.embed-responsive'), (err) => {
- if (err) {
- alert('Cannot append the file.');
- console.error(err);
- }
- });
-
- // Refresh each second
- this._interval = setInterval(() => {
- this.downloadSpeed = torrent.downloadSpeed;
- this.uploadSpeed = torrent.uploadSpeed;
- this.numPeers = torrent.numPeers;
- }, 1000);
- });
- }
-
- routerCanDeactivate(next: ComponentInstruction, prev: ComponentInstruction) : any {
- console.log('Removing video from webtorrent.');
- clearInterval(this._interval);
- this.client.remove(this.video.magnetUri);
- return true;
- }
-}
+++ /dev/null
-<div id="video-loading" class="col-md-12 text-center" *ngIf="loading">
- <div class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></div>
-</div>
+++ /dev/null
-div {
- margin-top: 150px;
-}
-
-// Thanks https://gist.github.com/alexandrevicenzi/680147013e902a4eaa5d
-.glyphicon-refresh-animate {
- -animation: spin .7s infinite linear;
- -ms-animation: spin .7s infinite linear;
- -webkit-animation: spinw .7s infinite linear;
- -moz-animation: spinm .7s infinite linear;
-}
-
-@keyframes spin {
- from { transform: scale(1) rotate(0deg);}
- to { transform: scale(1) rotate(360deg);}
-}
-
-@-webkit-keyframes spinw {
- from { -webkit-transform: rotate(0deg);}
- to { -webkit-transform: rotate(360deg);}
-}
-
-@-moz-keyframes spinm {
- from { -moz-transform: rotate(0deg);}
- to { -moz-transform: rotate(360deg);}
-}
+++ /dev/null
-import { Component, Input } from '@angular/core';
-
-@Component({
- selector: 'my-loader',
- styleUrls: [ 'app/angular/videos/loader.component.css' ],
- templateUrl: 'app/angular/videos/loader.component.html'
-})
-
-export class LoaderComponent {
- @Input() loading: boolean;
-}
+++ /dev/null
-export interface Pagination {
- currentPage: number;
- itemsPerPage: number;
- total: number;
-}
+++ /dev/null
-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;
-
- 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;
- }
-
- 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;
- }
-}
+++ /dev/null
-import { Injectable } from '@angular/core';
-import { Http, Response, URLSearchParams } from '@angular/http';
-import { Observable } from 'rxjs/Rx';
-
-import { Pagination } from './pagination';
-import { Video } from './video';
-import { AuthService } from '../users/services/auth.service';
-import { Search } from '../app/search';
-import { SortField } from './components/list/sort';
-
-@Injectable()
-export class VideosService {
- private _baseVideoUrl = '/api/v1/videos/';
-
- constructor (private http: Http, private _authService: AuthService) {}
-
- getVideos(pagination: Pagination, sort: SortField) {
- const params = this.createPaginationParams(pagination);
-
- if (sort) params.set('sort', sort);
-
- return this.http.get(this._baseVideoUrl, { search: params })
- .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: Search, pagination: Pagination, sort: SortField) {
- const params = this.createPaginationParams(pagination);
-
- if (search.field) params.set('field', search.field);
- if (sort) params.set('sort', sort);
-
- return this.http.get(this._baseVideoUrl + 'search/' + encodeURIComponent(search.value), { search: params })
- .map(res => res.json())
- .map(this.extractVideos)
- .catch(this.handleError);
- }
-
- private extractVideos (body: any) {
- const videos_json = body.data;
- const totalVideos = body.total;
- const videos = [];
- for (const video_json of videos_json) {
- videos.push(new Video(video_json));
- }
-
- return { videos, totalVideos };
- }
-
- private handleError (error: Response) {
- console.error(error);
- return Observable.throw(error.json().error || 'Server error');
- }
-
- private createPaginationParams(pagination: Pagination) {
- const params = new URLSearchParams();
- const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage;
- const count: number = pagination.itemsPerPage;
-
- params.set('start', start.toString());
- params.set('count', count.toString());
-
- return params;
- }
-}
--- /dev/null
+<div class="container">
+
+ <header class="row">
+ <div class="col-md-2">
+ <h4>PeerTube</h4>
+ </div>
+
+ <div class="col-md-9">
+ <my-search (search)="onSearch($event)"></my-search>
+ </div>
+ </header>
+
+
+ <div class="row">
+
+ <menu class="col-md-2 col-xs-3">
+ <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" (click)="logout()">Logout</a>
+ </div>
+ </div>
+
+ <div class="panel_block">
+ <div id="panel_get_videos" class="panel_button">
+ <span class="glyphicon glyphicon-list"></span>
+ <a [routerLink]="['VideosList']">Get videos</a>
+ </div>
+
+ <div id="panel_upload_video" class="panel_button" *ngIf="isLoggedIn">
+ <span class="glyphicon glyphicon-cloud-upload"></span>
+ <a [routerLink]="['VideosAdd']">Upload a video</a>
+ </div>
+ </div>
+
+ <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">
+ <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">
+ <router-outlet></router-outlet>
+ </div>
+
+ </div>
+
+
+ <footer>
+ PeerTube, CopyLeft 2015-2016
+ </footer>
+</div>
--- /dev/null
+header div {
+ line-height: 25px;
+ margin-bottom: 30px;
+}
+
+menu {
+ min-height: 600px;
+ margin-right: 20px;
+ border-right: 1px solid rgba(0, 0, 0, 0.2);
+
+ .panel_button {
+ margin: 8px;
+ cursor: pointer;
+ transition: margin 0.2s;
+
+ &:hover {
+ margin-left: 15px;
+ }
+
+ a {
+ color: #333333;
+ }
+ }
+
+ .glyphicon {
+ margin: 5px;
+ }
+}
+
+.panel_block:not(:last-child) {
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+}
--- /dev/null
+import { Component } from '@angular/core';
+import { HTTP_PROVIDERS } from '@angular/http';
+import { RouteConfig, Router, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from '@angular/router-deprecated';
+
+import { FriendService } from './friends/index';
+import { Search, SearchComponent } from './shared/index';
+import {
+ UserLoginComponent,
+ AuthService,
+ AuthStatus
+} from './users/index';
+import {
+ VideoAddComponent,
+ VideoListComponent,
+ VideoWatchComponent,
+ VideoService
+} from './videos/index';
+
+@RouteConfig([
+ {
+ path: '/users/login',
+ name: 'UserLogin',
+ component: UserLoginComponent
+ },
+ {
+ path: '/videos/list',
+ name: 'VideosList',
+ component: VideoListComponent,
+ useAsDefault: true
+ },
+ {
+ path: '/videos/watch/:id',
+ name: 'VideosWatch',
+ component: VideoWatchComponent
+ },
+ {
+ path: '/videos/add',
+ name: 'VideosAdd',
+ component: VideoAddComponent
+ }
+])
+
+@Component({
+ selector: 'my-app',
+ templateUrl: 'client/app/app.component.html',
+ styleUrls: [ 'client/app/app.component.css' ],
+ directives: [ ROUTER_DIRECTIVES, SearchComponent ],
+ providers: [ ROUTER_PROVIDERS, HTTP_PROVIDERS, VideoService, FriendService, AuthService ]
+})
+
+export class AppComponent {
+ isLoggedIn: boolean;
+ search_field: string = name;
+ choices = [ ];
+
+ constructor(private _friendService: FriendService,
+ private _authService: AuthService,
+ private _router: Router
+
+ ) {
+ this.isLoggedIn = this._authService.isLoggedIn();
+
+ this._authService.loginChanged$.subscribe(
+ status => {
+ if (status === AuthStatus.LoggedIn) {
+ this.isLoggedIn = true;
+ }
+ }
+ );
+ }
+
+ onSearch(search: Search) {
+ if (search.value !== '') {
+ const params = {
+ search: search.value,
+ field: search.field
+ };
+ this._router.navigate(['VideosList', params]);
+ } else {
+ this._router.navigate(['VideosList']);
+ }
+ }
+
+ logout() {
+ // this._authService.logout();
+ }
+
+ makeFriends() {
+ this._friendService.makeFriends().subscribe(
+ status => {
+ if (status === 409) {
+ alert('Already made friends!');
+ } else {
+ alert('Made friends!');
+ }
+ },
+ error => alert(error)
+ );
+ }
+
+ quitFriends() {
+ this._friendService.quitFriends().subscribe(
+ status => {
+ alert('Quit friends!');
+ },
+ error => alert(error)
+ );
+ }
+}
--- /dev/null
+import { Injectable } from '@angular/core';
+import { Http, Response } from '@angular/http';
+import { Observable } from 'rxjs/Rx';
+
+@Injectable()
+export class FriendService {
+ private _baseFriendsUrl = '/api/v1/pods/';
+
+ constructor (private http: Http) {}
+
+ makeFriends() {
+ return this.http.get(this._baseFriendsUrl + 'makefriends')
+ .map(res => <number> res.status)
+ .catch(this.handleError);
+ }
+
+ quitFriends() {
+ return this.http.get(this._baseFriendsUrl + 'quitfriends')
+ .map(res => <number> res.status)
+ .catch(this.handleError);
+ }
+
+ private handleError (error: Response) {
+ console.error(error);
+ return Observable.throw(error.json().error || 'Server error');
+ }
+}
--- /dev/null
+export * from './friend.service';
--- /dev/null
+export * from './search-field.type';
+export * from './search.component';
+export * from './search.model';
--- /dev/null
+export type SearchField = "name" | "author" | "podUrl" | "magnetUri";
--- /dev/null
+<div class="input-group">
+ <div class="input-group-btn" dropdown>
+ <button id="simple-btn-keyboard-nav" type="button" class="btn btn-default" dropdownToggle>
+ {{ getStringChoice(searchCriterias.field) }} <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu" role="menu" aria-labelledby="simple-btn-keyboard-nav">
+ <li *ngFor="let choice of choiceKeys" class="dropdown-item">
+ <a class="dropdown-item" href="#" (click)="choose($event, choice)">{{ getStringChoice(choice) }}</a>
+ </li>
+ </ul>
+ </div>
+
+ <input
+ type="text" id="search-video" name="search-video" class="form-control" placeholder="Search a video..." class="form-control"
+ [(ngModel)]="searchCriterias.value" (keyup.enter)="doSearch()"
+ >
+</div>
--- /dev/null
+import { Component, EventEmitter, Output } from '@angular/core';
+
+import { DROPDOWN_DIRECTIVES} from 'ng2-bootstrap/components/dropdown';
+
+import { Search } from './search.model';
+import { SearchField } from './search-field.type';
+
+@Component({
+ selector: 'my-search',
+ templateUrl: 'client/app/shared/search.component.html',
+ directives: [ DROPDOWN_DIRECTIVES ]
+})
+
+export class SearchComponent {
+ @Output() search: EventEmitter<Search> = new EventEmitter<Search>();
+
+ searchCriterias: Search = {
+ field: 'name',
+ value: ''
+ };
+ fieldChoices = {
+ name: 'Name',
+ author: 'Author',
+ podUrl: 'Pod Url',
+ magnetUri: 'Magnet Uri'
+ };
+
+ get choiceKeys() {
+ return Object.keys(this.fieldChoices);
+ }
+
+ getStringChoice(choiceKey: SearchField): string {
+ return this.fieldChoices[choiceKey];
+ }
+
+ choose($event:MouseEvent, choice: SearchField) {
+ $event.preventDefault();
+ $event.stopPropagation();
+
+ this.searchCriterias.field = choice;
+ }
+
+ doSearch(): void {
+ this.search.emit(this.searchCriterias);
+ }
+
+}
--- /dev/null
+import { SearchField } from './search-field.type';
+
+export interface Search {
+ field: SearchField;
+ value: string;
+}
--- /dev/null
+export * from './login/index';
+export * from './shared/index';
--- /dev/null
+export * from './login.component';
--- /dev/null
+<h3>Login</h3>
+<form role="form" (submit)="login(username.value, password.value)">
+ <div class="form-group">
+ <label for="username">Username</label>
+ <input type="text" #username class="form-control" id="username" placeholder="Username">
+ </div>
+
+ <div class="form-group">
+ <label for="password">Password</label>
+ <input type="password" #password class="form-control" id="password" placeholder="Password">
+ </div>
+
+ <input type="submit" value="Login" class="btn btn-default">
+</form>
--- /dev/null
+import { Component } from '@angular/core';
+import { Router } from '@angular/router-deprecated';
+
+import { AuthService, AuthStatus, User } from '../shared/index';
+
+@Component({
+ selector: 'my-user-login',
+ styleUrls: [ 'client/app/users/login/login.component.css' ],
+ templateUrl: 'client/app/users/login/login.component.html'
+})
+
+export class UserLoginComponent {
+ constructor(private _authService: AuthService, private _router: Router) {}
+
+ login(username: string, password: string) {
+ this._authService.login(username, password).subscribe(
+ result => {
+ const user = new User(username, result);
+ user.save();
+
+ this._authService.setStatus(AuthStatus.LoggedIn);
+
+ this._router.navigate(['VideosList']);
+ },
+ error => {
+ if (error.error === 'invalid_grant') {
+ alert('Credentials are invalid.');
+ } else {
+ alert(`${error.error}: ${error.error_description}`);
+ }
+ }
+ );
+ }
+}
--- /dev/null
+export enum AuthStatus {
+ LoggedIn,
+ LoggedOut
+}
--- /dev/null
+import { Injectable } from '@angular/core';
+import { Headers, Http, RequestOptions, Response, URLSearchParams } from '@angular/http';
+import { Observable, Subject } from 'rxjs/Rx';
+
+import { AuthStatus } from './auth-status.model';
+import { User } from './user.model';
+
+@Injectable()
+export class AuthService {
+ loginChanged$;
+
+ private _loginChanged;
+ private _baseLoginUrl = '/api/v1/users/token';
+ private _baseClientUrl = '/api/v1/users/client';
+ private _clientId = '';
+ private _clientSecret = '';
+
+ constructor (private http: Http) {
+ this._loginChanged = new Subject<AuthStatus>();
+ this.loginChanged$ = this._loginChanged.asObservable();
+
+ // Fetch the client_id/client_secret
+ // FIXME: save in local storage?
+ this.http.get(this._baseClientUrl)
+ .map(res => res.json())
+ .catch(this.handleError)
+ .subscribe(
+ result => {
+ this._clientId = result.client_id;
+ this._clientSecret = result.client_secret;
+ console.log('Client credentials loaded.');
+ },
+ error => {
+ alert(error);
+ }
+ );
+ }
+
+ login(username: string, password: string) {
+ let body = new URLSearchParams();
+ body.set('client_id', this._clientId);
+ body.set('client_secret', this._clientSecret);
+ body.set('response_type', 'code');
+ body.set('grant_type', 'password');
+ body.set('scope', 'upload');
+ body.set('username', username);
+ body.set('password', password);
+
+ let headers = new Headers();
+ headers.append('Content-Type', 'application/x-www-form-urlencoded');
+
+ let options = {
+ headers: headers
+ };
+
+ return this.http.post(this._baseLoginUrl, body.toString(), options)
+ .map(res => res.json())
+ .catch(this.handleError);
+ }
+
+ logout() {
+ // TODO make HTTP request
+ }
+
+ getRequestHeader(): Headers {
+ return new Headers({ 'Authorization': `${this.getTokenType()} ${this.getToken()}` });
+ }
+
+ getAuthRequestOptions(): RequestOptions {
+ return new RequestOptions({ headers: this.getRequestHeader() });
+ }
+
+ getToken(): string {
+ return localStorage.getItem('access_token');
+ }
+
+ getTokenType(): string {
+ return localStorage.getItem('token_type');
+ }
+
+ getUser(): User {
+ if (this.isLoggedIn() === false) {
+ return null;
+ }
+
+ const user = User.load();
+
+ return user;
+ }
+
+ isLoggedIn(): boolean {
+ if (this.getToken()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ setStatus(status: AuthStatus) {
+ this._loginChanged.next(status);
+ }
+
+ private handleError (error: Response) {
+ console.error(error);
+ return Observable.throw(error.json() || { error: 'Server error' });
+ }
+}
--- /dev/null
+export * from './auth-status.model';
+export * from './auth.service';
+export * from './token.model';
+export * from './user.model';
--- /dev/null
+export class Token {
+ access_token: string;
+ refresh_token: string;
+ token_type: string;
+
+ static load(): Token {
+ return new Token({
+ access_token: localStorage.getItem('access_token'),
+ refresh_token: localStorage.getItem('refresh_token'),
+ token_type: localStorage.getItem('token_type')
+ });
+ }
+
+ constructor (hash?: any) {
+ if (hash) {
+ this.access_token = hash.access_token;
+ this.refresh_token = hash.refresh_token;
+ if (hash.token_type === 'bearer') {
+ this.token_type = 'Bearer';
+ } else {
+ this.token_type = hash.token_type;
+ }
+ }
+ }
+
+ save():void {
+ localStorage.setItem('access_token', this.access_token);
+ localStorage.setItem('refresh_token', this.refresh_token);
+ localStorage.setItem('token_type', this.token_type);
+ }
+}
--- /dev/null
+import { Token } from './token.model';
+
+export class User {
+ username: string;
+ token: Token;
+
+ static load(): User {
+ return new User(localStorage.getItem('username'), Token.load());
+ }
+
+ constructor (username: string, hash_token: any) {
+ this.username = username;
+ this.token = new Token(hash_token);
+ }
+
+ save(): void {
+ localStorage.setItem('username', this.username);
+ this.token.save();
+ }
+}
--- /dev/null
+export * from './shared/index';
+export * from './video-add/index';
+export * from './video-list/index';
+export * from './video-watch/index';
--- /dev/null
+export * from './loader/index';
+export * from './pagination.model';
+export * from './sort-field.type';
+export * from './video.model';
+export * from './video.service';
--- /dev/null
+export * from './loader.component';
--- /dev/null
+<div id="video-loading" class="col-md-12 text-center" *ngIf="loading">
+ <div class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></div>
+</div>
--- /dev/null
+div {
+ margin-top: 150px;
+}
+
+// Thanks https://gist.github.com/alexandrevicenzi/680147013e902a4eaa5d
+.glyphicon-refresh-animate {
+ -animation: spin .7s infinite linear;
+ -ms-animation: spin .7s infinite linear;
+ -webkit-animation: spinw .7s infinite linear;
+ -moz-animation: spinm .7s infinite linear;
+}
+
+@keyframes spin {
+ from { transform: scale(1) rotate(0deg);}
+ to { transform: scale(1) rotate(360deg);}
+}
+
+@-webkit-keyframes spinw {
+ from { -webkit-transform: rotate(0deg);}
+ to { -webkit-transform: rotate(360deg);}
+}
+
+@-moz-keyframes spinm {
+ from { -moz-transform: rotate(0deg);}
+ to { -moz-transform: rotate(360deg);}
+}
--- /dev/null
+import { Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'my-loader',
+ styleUrls: [ 'client/app/videos/shared/loader/loader.component.css' ],
+ templateUrl: 'client/app/videos/shared/loader/loader.component.html'
+})
+
+export class LoaderComponent {
+ @Input() loading: boolean;
+}
--- /dev/null
+export interface Pagination {
+ currentPage: number;
+ itemsPerPage: number;
+ total: number;
+}
--- /dev/null
+export type SortField = "name" | "-name"
+ | "duration" | "-duration"
+ | "createdDate" | "-createdDate";
--- /dev/null
+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;
+
+ 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;
+ }
+
+ 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;
+ }
+}
--- /dev/null
+import { Injectable } from '@angular/core';
+import { Http, Response, URLSearchParams } from '@angular/http';
+import { Observable } from 'rxjs/Rx';
+
+import { Pagination } from './pagination.model';
+import { Search } from '../../shared/index';
+import { SortField } from './sort-field.type';
+import { AuthService } from '../../users/index';
+import { Video } from './video.model';
+
+@Injectable()
+export class VideoService {
+ private _baseVideoUrl = '/api/v1/videos/';
+
+ constructor (private http: Http, private _authService: AuthService) {}
+
+ getVideos(pagination: Pagination, sort: SortField) {
+ const params = this.createPaginationParams(pagination);
+
+ if (sort) params.set('sort', sort);
+
+ return this.http.get(this._baseVideoUrl, { search: params })
+ .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: Search, pagination: Pagination, sort: SortField) {
+ const params = this.createPaginationParams(pagination);
+
+ if (search.field) params.set('field', search.field);
+ if (sort) params.set('sort', sort);
+
+ return this.http.get(this._baseVideoUrl + 'search/' + encodeURIComponent(search.value), { search: params })
+ .map(res => res.json())
+ .map(this.extractVideos)
+ .catch(this.handleError);
+ }
+
+ private extractVideos (body: any) {
+ const videos_json = body.data;
+ const totalVideos = body.total;
+ const videos = [];
+ for (const video_json of videos_json) {
+ videos.push(new Video(video_json));
+ }
+
+ return { videos, totalVideos };
+ }
+
+ private handleError (error: Response) {
+ console.error(error);
+ return Observable.throw(error.json().error || 'Server error');
+ }
+
+ private createPaginationParams(pagination: Pagination) {
+ const params = new URLSearchParams();
+ const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage;
+ const count: number = pagination.itemsPerPage;
+
+ params.set('start', start.toString());
+ params.set('count', count.toString());
+
+ return params;
+ }
+}
--- /dev/null
+export * from './video-add.component';
--- /dev/null
+<h3>Upload a video</h3>
+
+<form (ngSubmit)="uploadFile()" #videoForm="ngForm">
+ <div class="form-group">
+ <label for="name">Video name</label>
+ <input
+ type="text" class="form-control" name="name" id="name" required
+ ngControl="name" #name="ngForm"
+ >
+ <div [hidden]="name.valid || name.pristine" class="alert alert-danger">
+ Name is required
+ </div>
+ </div>
+
+ <div class="form-group">
+ <div class="btn btn-default btn-file">
+ <span>Select the video...</span>
+ <input type="file" name="videofile" id="videofile">
+ </div>
+
+ <span *ngIf="fileToUpload">{{ fileToUpload.name }}</span>
+ </div>
+
+ <div class="form-group">
+ <label for="description">Description</label>
+ <textarea
+ name="description" id="description" class="form-control" placeholder="Description..." required
+ ngControl="description" #description="ngForm"
+ >
+ </textarea>
+ <div [hidden]="description.valid || description.pristine" class="alert alert-danger">
+ A description is required
+ </div>
+ </div>
+
+ <div id="progress" *ngIf="progressBar.max !== 0">
+ <progressbar [value]="progressBar.value" [max]="progressBar.max">{{ progressBar.value | bytes }} / {{ progressBar.max | bytes }}</progressbar>
+ </div>
+
+ <input type="submit" value="Upload" class="btn btn-default" [disabled]="!videoForm.form.valid || !fileToUpload">
+</form>
--- /dev/null
+.btn-file {
+ position: relative;
+ overflow: hidden;
+}
+
+.btn-file input[type=file] {
+ position: absolute;
+ top: 0;
+ right: 0;
+ min-width: 100%;
+ min-height: 100%;
+ font-size: 100px;
+ text-align: right;
+ filter: alpha(opacity=0);
+ opacity: 0;
+ outline: none;
+ background: white;
+ cursor: inherit;
+ display: block;
+}
+
+.name_file {
+ display: inline-block;
+ margin-left: 10px;
+}
+
+.form-group {
+ margin-bottom: 10px;
+}
+
+#progress {
+ margin-bottom: 10px;
+}
--- /dev/null
+import { Component, ElementRef, OnInit } from '@angular/core';
+import { Router } from '@angular/router-deprecated';
+
+import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
+import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar';
+
+import { AuthService, User } from '../../users/index';
+
+// TODO: import it with systemjs
+declare var jQuery:any;
+
+@Component({
+ selector: 'my-videos-add',
+ styleUrls: [ 'client/app/videos/video-add/video-add.component.css' ],
+ templateUrl: 'client/app/videos/video-add/video-add.component.html',
+ directives: [ PROGRESSBAR_DIRECTIVES ],
+ pipes: [ BytesPipe ]
+})
+
+export class VideoAddComponent implements OnInit {
+ user: User;
+ fileToUpload: any;
+ progressBar: { value: number; max: number; } = { value: 0, max: 0 };
+
+ private _form: any;
+
+ constructor(
+ private _router: Router, private _elementRef: ElementRef,
+ private _authService: AuthService
+ ) {}
+
+ ngOnInit() {
+ this.user = User.load();
+ jQuery(this._elementRef.nativeElement).find('#videofile').fileupload({
+ url: '/api/v1/videos',
+ dataType: 'json',
+ singleFileUploads: true,
+ multipart: true,
+ autoupload: false,
+
+ add: (e, data) => {
+ this._form = data;
+ this.fileToUpload = data['files'][0];
+ },
+
+ progressall: (e, data) => {
+ this.progressBar.value = data.loaded;
+ // The server is a little bit slow to answer (has to seed the video)
+ // So we add more time to the progress bar (+10%)
+ this.progressBar.max = data.total + (0.1 * data.total);
+ },
+
+ done: (e, data) => {
+ this.progressBar.value = this.progressBar.max;
+ console.log('Video uploaded.');
+
+ // Print all the videos once it's finished
+ this._router.navigate(['VideosList']);
+ }
+ });
+ }
+
+ uploadFile() {
+ this._form.headers = this._authService.getRequestHeader().toJSON();
+ this._form.formData = jQuery(this._elementRef.nativeElement).find('form').serializeArray();
+ this._form.submit();
+ }
+}
--- /dev/null
+export * from './video-list.component';
+export * from './video-miniature.component';
+export * from './video-sort.component';
--- /dev/null
+<div class="row videos-info">
+ <div class="col-md-9 videos-total-results"> {{ pagination.total }} videos</div>
+ <my-video-sort class="col-md-3" [currentSort]="sort" (sort)="onSort($event)"></my-video-sort>
+</div>
+
+<div class="videos-miniatures">
+ <my-loader [loading]="loading"></my-loader>
+
+ <div class="col-md-12 no-video" *ngIf="!loading && videos.length === 0">There is no video.</div>
+
+ <my-video-miniature *ngFor="let video of videos" [video]="video" [user]="user" (removed)="onRemoved(video)">
+ </my-video-miniature>
+</div>
+
+<pagination
+ [totalItems]="pagination.total" [itemsPerPage]="pagination.itemsPerPage" [(ngModel)]="pagination.currentPage"
+ (ngModelChange)="getVideos()"
+></pagination>
--- /dev/null
+.videos-info {
+
+ padding-bottom: 20px;
+ margin-bottom: 20px;
+ border-bottom: 1px solid #f1f1f1;
+ height: 40px;
+ line-height: 40px;
+ width: 765px;
+ margin-left: 15px;
+
+ my-video-sort {
+ padding-right: 0;
+ }
+
+ .videos-total-results {
+ font-size: 13px;
+ padding-left: 0;
+ }
+}
+
+.videos-miniatures {
+ min-height: 600px;
+
+ my-videos-miniature {
+ display: inline-block;
+ }
+
+ .no-video {
+ margin-top: 50px;
+ text-align: center;
+ }
+}
+
+pagination {
+ display: block;
+ text-align: center;
+}
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { Router, ROUTER_DIRECTIVES, RouteParams } from '@angular/router-deprecated';
+
+import { PAGINATION_DIRECTIVES } from 'ng2-bootstrap/components/pagination';
+
+import {
+ LoaderComponent,
+ Pagination,
+ SortField,
+ Video,
+ VideoService
+} from '../shared/index';
+import { Search, SearchField } from '../../shared/index';
+import { AuthService, User } from '../../users/index';
+import { VideoMiniatureComponent } from './video-miniature.component';
+import { VideoSortComponent } from './video-sort.component';
+
+@Component({
+ selector: 'my-videos-list',
+ styleUrls: [ 'client/app/videos/video-list/video-list.component.css' ],
+ templateUrl: 'client/app/videos/video-list/video-list.component.html',
+ directives: [ ROUTER_DIRECTIVES, PAGINATION_DIRECTIVES, VideoMiniatureComponent, VideoSortComponent, LoaderComponent ]
+})
+
+export class VideoListComponent implements OnInit {
+ user: User = null;
+ videos: Video[] = [];
+ pagination: Pagination = {
+ currentPage: 1,
+ itemsPerPage: 9,
+ total: 0
+ };
+ sort: SortField;
+ loading: boolean = false;
+
+ private search: Search;
+
+ constructor(
+ private _authService: AuthService,
+ private _videoService: VideoService,
+ private _routeParams: RouteParams,
+ private _router: Router
+ ) {
+ this.search = {
+ value: this._routeParams.get('search'),
+ field: <SearchField>this._routeParams.get('field')
+ };
+
+ this.sort = <SortField>this._routeParams.get('sort') || '-createdDate';
+ }
+
+ ngOnInit() {
+ if (this._authService.isLoggedIn()) {
+ this.user = User.load();
+ }
+
+ this.getVideos();
+ }
+
+ getVideos() {
+ this.loading = true;
+ this.videos = [];
+
+ let observable = null;
+
+ if (this.search.value !== null) {
+ observable = this._videoService.searchVideos(this.search, this.pagination, this.sort);
+ } else {
+ observable = this._videoService.getVideos(this.pagination, this.sort);
+ }
+
+ observable.subscribe(
+ ({ videos, totalVideos }) => {
+ this.videos = videos;
+ this.pagination.total = totalVideos;
+ this.loading = false;
+ },
+ error => alert(error)
+ );
+ }
+
+ onRemoved(video: Video): void {
+ this.videos.splice(this.videos.indexOf(video), 1);
+ }
+
+ onSort(sort: SortField) {
+ this.sort = sort;
+
+ const params: any = {
+ sort: this.sort
+ };
+
+ if (this.search.value) {
+ params.search = this.search.value;
+ params.field = this.search.field;
+ }
+
+ this._router.navigate(['VideosList', params]);
+ this.getVideos();
+ }
+}
--- /dev/null
+<div class="video-miniature col-md-4" (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>
--- /dev/null
+.video-miniature {
+ height: 200px;
+ display: inline-block;
+ position: relative;
+
+ .video-miniature-thumbnail {
+ display: block;
+ position: relative;
+
+ .video-miniature-duration {
+ position: absolute;
+ right: 60px;
+ 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: 16px;
+ 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);
+ }
+ }
+}
--- /dev/null
+import { DatePipe } from '@angular/common';
+import { Component, Input, Output, EventEmitter } from '@angular/core';
+import { ROUTER_DIRECTIVES } from '@angular/router-deprecated';
+
+import { Video, VideoService } from '../shared/index';
+import { User } from '../../users/index';
+
+@Component({
+ selector: 'my-video-miniature',
+ styleUrls: [ 'client/app/videos/video-list/video-miniature.component.css' ],
+ templateUrl: 'client/app/videos/video-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 _videoService: VideoService) {}
+
+ 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._videoService.removeVideo(id).subscribe(
+ status => this.removed.emit(true),
+ error => alert(error)
+ );
+ }
+ }
+}
--- /dev/null
+<select class="form-control input-sm" [(ngModel)]="currentSort" (ngModelChange)="onSortChange()">
+ <option *ngFor="let choice of choiceKeys" [value]="choice">
+ {{ getStringChoice(choice) }}
+ </option>
+</select>
--- /dev/null
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+
+import { SortField } from '../shared/index';
+
+@Component({
+ selector: 'my-video-sort',
+ // styleUrls: [ 'app/angular/videos/components/list/video-sort.component.css' ],
+ templateUrl: 'client/app/videos/video-list/video-sort.component.html'
+})
+
+export class VideoSortComponent {
+ @Output() sort = new EventEmitter<any>();
+
+ @Input() currentSort: SortField;
+
+ sortChoices = {
+ 'name': 'Name - Asc',
+ '-name': 'Name - Desc',
+ 'duration': 'Duration - Asc',
+ '-duration': 'Duration - Desc',
+ 'createdDate': 'Created Date - Asc',
+ '-createdDate': 'Created Date - Desc'
+ };
+
+ get choiceKeys() {
+ return Object.keys(this.sortChoices);
+ }
+
+ getStringChoice(choiceKey: SortField): string {
+ return this.sortChoices[choiceKey];
+ }
+
+ onSortChange() {
+ this.sort.emit(this.currentSort);
+ }
+}
--- /dev/null
+export * from './video-watch.component';
--- /dev/null
+<my-loader [loading]="loading"></my-loader>
+
+<div class="embed-responsive embed-responsive-19by9">
+</div>
+
+<div id="torrent-info">
+ <div id="torrent-info-download">Download: {{ downloadSpeed | bytes }}/s</div>
+ <div id="torrent-info-upload">Upload: {{ uploadSpeed | bytes }}/s</div>
+ <div id="torrent-info-peers">Number of peers: {{ numPeers }}</div>
+<div>
--- /dev/null
+.embed-responsive {
+ height: 500px;
+}
+
+#torrent-info {
+ font-size: 10px;
+
+ div {
+ display: inline-block;
+ width: 33%;
+ text-align: center;
+ }
+}
--- /dev/null
+import { Component, ElementRef, OnInit } from '@angular/core';
+import { CanDeactivate, ComponentInstruction, RouteParams } from '@angular/router-deprecated';
+
+import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
+
+import { LoaderComponent, Video, VideoService } from '../shared/index';
+
+// TODO import it with systemjs
+declare var WebTorrent: any;
+
+@Component({
+ selector: 'my-video-watch',
+ templateUrl: 'client/app/videos/video-watch/video-watch.component.html',
+ styleUrls: [ 'client/app/videos/video-watch/video-watch.component.css' ],
+ directives: [ LoaderComponent ],
+ pipes: [ BytesPipe ]
+})
+
+export class VideoWatchComponent implements OnInit, CanDeactivate {
+ video: Video;
+ downloadSpeed: number;
+ uploadSpeed: number;
+ numPeers: number;
+ loading: boolean = false;
+
+ private _interval: NodeJS.Timer;
+ private client: any;
+
+ constructor(
+ private _videoService: VideoService,
+ private _routeParams: RouteParams,
+ private _elementRef: ElementRef
+ ) {
+ // TODO: use a service
+ this.client = new WebTorrent({ dht: false });
+ }
+
+ ngOnInit() {
+ let id = this._routeParams.get('id');
+ this._videoService.getVideo(id).subscribe(
+ video => this.loadVideo(video),
+ error => alert(error)
+ );
+ }
+
+ loadVideo(video: Video) {
+ this.loading = true;
+ this.video = video;
+ console.log('Adding ' + this.video.magnetUri + '.');
+ this.client.add(this.video.magnetUri, (torrent) => {
+ this.loading = false;
+ console.log('Added ' + this.video.magnetUri + '.');
+ torrent.files[0].appendTo(this._elementRef.nativeElement.querySelector('.embed-responsive'), (err) => {
+ if (err) {
+ alert('Cannot append the file.');
+ console.error(err);
+ }
+ });
+
+ // Refresh each second
+ this._interval = setInterval(() => {
+ this.downloadSpeed = torrent.downloadSpeed;
+ this.uploadSpeed = torrent.uploadSpeed;
+ this.numPeers = torrent.numPeers;
+ }, 1000);
+ });
+ }
+
+ routerCanDeactivate(next: ComponentInstruction, prev: ComponentInstruction) : any {
+ console.log('Removing video from webtorrent.');
+ clearInterval(this._interval);
+ this.client.remove(this.video.magnetUri);
+ return true;
+ }
+}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
- <link rel="stylesheet" href="/app/stylesheets/index.css">
+ <link rel="stylesheet" href="client/stylesheets/index.css">
<!-- 1. Load libraries -->
<!-- IE required polyfills, in this exact order -->
- <script src="/app/node_modules/es6-shim/es6-shim.min.js"></script>
- <script src="/app/node_modules/zone.js/dist/zone.js"></script>
- <script src="/app/node_modules/reflect-metadata/Reflect.js"></script>
- <script src="/app/node_modules/systemjs/dist/system.src.js"></script>
+ <script src="client/node_modules/es6-shim/es6-shim.min.js"></script>
+ <script src="client/node_modules/zone.js/dist/zone.js"></script>
+ <script src="client/node_modules/reflect-metadata/Reflect.js"></script>
+ <script src="client/node_modules/systemjs/dist/system.src.js"></script>
- <script src="/app/node_modules/jquery/dist/jquery.js"></script>
- <script src="/app/node_modules/jquery.ui.widget/jquery.ui.widget.js"></script>
- <script src="/app/node_modules/blueimp-file-upload/js/jquery.fileupload.js"></script>
+ <script src="client/node_modules/jquery/dist/jquery.js"></script>
+ <script src="client/node_modules/jquery.ui.widget/jquery.ui.widget.js"></script>
+ <script src="client/node_modules/blueimp-file-upload/js/jquery.fileupload.js"></script>
- <script src="/app/node_modules/webtorrent/webtorrent.min.js"></script>
+ <script src="client/node_modules/webtorrent/webtorrent.min.js"></script>
- <script src="/app/node_modules/ng2-bootstrap/bundles/ng2-bootstrap.min.js"></script>
+ <script src="client/node_modules/ng2-bootstrap/bundles/ng2-bootstrap.min.js"></script>
<!-- 2. Configure SystemJS -->
- <script src="/app/systemjs.config.js"></script>
+ <script src="client/systemjs.config.js"></script>
<script>
- System.import('app').catch(function(err){ console.error(err); });
+ System.import('client').catch(function(err){ console.error(err); });
</script>
</head>
--- /dev/null
+import { bootstrap } from '@angular/platform-browser-dynamic';
+
+import { AppComponent } from './app/app.component';
+
+bootstrap(AppComponent);
-$icon-font-path: "/app/node_modules/bootstrap-sass/assets/fonts/bootstrap/";
+$icon-font-path: "/client/node_modules/bootstrap-sass/assets/fonts/bootstrap/";
@import "bootstrap-variables";
@import "_bootstrap";
;(function (global) {
var map = {
- 'app': 'app/angular',
- 'angular-pipes': 'app/node_modules/angular-pipes',
- 'ng2-bootstrap': 'app/node_modules/ng2-bootstrap',
- 'angular-rxjs.bundle': 'app/bundles/angular-rxjs.bundle.js'
+ 'angular-pipes': 'client/node_modules/angular-pipes',
+ 'ng2-bootstrap': 'client/node_modules/ng2-bootstrap',
+ 'angular-rxjs.bundle': 'client/bundles/angular-rxjs.bundle.js'
}
var packages = {
- 'app': { main: 'main.js', defaultExtension: 'js' },
+ 'client': { main: 'main.js', defaultExtension: 'js' },
'ng2-bootstrap': { defaultExtension: 'js' },
'rxjs': { defaultExtension: 'js' }
}
],
"compileOnSave": false,
"files": [
- "angular/app/app.component.ts",
- "angular/app/search.component.ts",
- "angular/app/search.ts",
- "angular/friends/services/friends.service.ts",
- "angular/main.ts",
- "angular/users/components/login/login.component.ts",
- "angular/users/models/authStatus.ts",
- "angular/users/models/token.ts",
- "angular/users/models/user.ts",
- "angular/users/services/auth.service.ts",
- "angular/videos/components/add/videos-add.component.ts",
- "angular/videos/components/list/sort.ts",
- "angular/videos/components/list/video-miniature.component.ts",
- "angular/videos/components/list/video-sort.component.ts",
- "angular/videos/components/list/videos-list.component.ts",
- "angular/videos/components/watch/videos-watch.component.ts",
- "angular/videos/loader.component.ts",
- "angular/videos/pagination.ts",
- "angular/videos/video.ts",
- "angular/videos/videos.service.ts",
+ "app/app.component.ts",
+ "app/friends/friend.service.ts",
+ "app/friends/index.ts",
+ "app/shared/index.ts",
+ "app/shared/search-field.type.ts",
+ "app/shared/search.component.ts",
+ "app/shared/search.model.ts",
+ "app/users/index.ts",
+ "app/users/login/index.ts",
+ "app/users/login/login.component.ts",
+ "app/users/shared/auth-status.model.ts",
+ "app/users/shared/auth.service.ts",
+ "app/users/shared/index.ts",
+ "app/users/shared/token.model.ts",
+ "app/users/shared/user.model.ts",
+ "app/videos/index.ts",
+ "app/videos/shared/index.ts",
+ "app/videos/shared/loader/index.ts",
+ "app/videos/shared/loader/loader.component.ts",
+ "app/videos/shared/pagination.model.ts",
+ "app/videos/shared/sort-field.type.ts",
+ "app/videos/shared/video.model.ts",
+ "app/videos/shared/video.service.ts",
+ "app/videos/video-add/index.ts",
+ "app/videos/video-add/video-add.component.ts",
+ "app/videos/video-list/index.ts",
+ "app/videos/video-list/video-list.component.ts",
+ "app/videos/video-list/video-miniature.component.ts",
+ "app/videos/video-list/video-sort.component.ts",
+ "app/videos/video-watch/index.ts",
+ "app/videos/video-watch/video-watch.component.ts",
+ "main.ts",
"typings/globals/es6-shim/index.d.ts",
"typings/globals/jasmine/index.d.ts",
"typings/globals/node/index.d.ts",
# Compile index and angular files
concurrently \
"node-sass --include-path node_modules/bootstrap-sass/assets/stylesheets/ stylesheets/application.scss stylesheets/index.css" \
- "node-sass angular/ --output angular/"
+ "node-sass app/ --output app/"
cd client || exit -1
rm -f stylesheets/index.css
-find angular -regextype posix-egrep -regex ".*\.(css)$" -exec rm -f {} \;
+find app -regextype posix-egrep -regex ".*\.(css)$" -exec rm -f {} \;
#!/usr/bin/env sh
cd client || exit -1
-find angular -regextype posix-egrep -regex ".*\.(js|map)$" -exec rm -f {} \;
+find app -regextype posix-egrep -regex ".*\.(js|map)$" -exec rm -f {} \;
rm -rf ./bundles
+rm -f main.js main.js.map
#!/usr/bin/env sh
-livereload client/angular -e scss
+livereload client/app -e scss
concurrently \
"node-sass -w --include-path node_modules/bootstrap-sass/assets/stylesheets/ stylesheets/application.scss stylesheets/index.css" \
- "node-sass -w angular/ --output angular/"
+ "node-sass -w app/ --output app/"
app.use(apiRoute, routes.api)
// Static files
-app.use('/app', express.static(path.join(__dirname, '/client'), { maxAge: 0 }))
+app.use('/client', express.static(path.join(__dirname, '/client'), { maxAge: 0 }))
// 404 for static files not found
-app.use('/app/*', function (req, res, next) {
+app.use('/client/*', function (req, res, next) {
res.sendStatus(404)
})