--- /dev/null
+---
+# Output debugging info
+# loglevel: debug
+
+# Major version of Bootstrap: 3 or 4
+bootstrapVersion: 3
+
+# If Bootstrap version 3 is used - turn on/off custom icon font path
+useCustomIconFontPath: true
+
+# Webpack loaders, order matters
+styleLoaders:
+ - style
+ - css
+ - sass
+
+# Extract styles to stand-alone css file
+# Different settings for different environments can be used,
+# It depends on value of NODE_ENV environment variable
+# This param can also be set in webpack config:
+# entry: 'bootstrap-loader/extractStyles'
+extractStyles: false
+# env:
+# development:
+# extractStyles: false
+# production:
+# extractStyles: true
+
+# Customize Bootstrap variables that get imported before the original Bootstrap variables.
+# Thus original Bootstrap variables can depend on values from here. All the bootstrap
+# variables are configured with !default, and thus, if you define the variable here, then
+# that value is used, rather than the default. However, many bootstrap variables are derived
+# from other bootstrap variables, and thus, you want to set this up before we load the
+# official bootstrap versions.
+# For example, _variables.scss contains:
+# $input-color: $gray !default;
+# This means you can define $input-color before we load _variables.scss
+preBootstrapCustomizations: ./src/sass/pre-customizations.scss
+
+# This gets loaded after bootstrap/variables is loaded and before bootstrap is loaded.
+# A good example of this is when you want to override a bootstrap variable to be based
+# on the default value of bootstrap. This is pretty specialized case. Thus, you normally
+# just override bootrap variables in preBootstrapCustomizations so that derived
+# variables will use your definition.
+#
+# For example, in _variables.scss:
+# $input-height: (($font-size-base * $line-height) + ($input-padding-y * 2) + ($border-width * 2)) !default;
+# This means that you could define this yourself in preBootstrapCustomizations. Or you can do
+# this in bootstrapCustomizations to make the input height 10% bigger than the default calculation.
+# Thus you can leverage the default calculations.
+# $input-height: $input-height * 1.10;
+# bootstrapCustomizations: ./app/styles/bootstrap/customizations.scss
+
+# Import your custom styles here. You have access to all the bootstrap variables. If you require
+# your sass files separately, you will not have access to the bootstrap variables, mixins, clases, etc.
+# Usually this endpoint-file contains list of @imports of your application styles.
+appStyles: ./src/sass/application.scss
+
+### Bootstrap styles
+styles:
+
+ # Mixins
+ mixins: true
+
+ # Reset and dependencies
+ normalize: true
+ print: true
+ glyphicons: true
+
+ # Core CSS
+ scaffolding: true
+ type: true
+ code: false
+ grid: true
+ tables: true
+ forms: true
+ buttons: true
+
+ # Components
+ component-animations: false
+ dropdowns: true
+ button-groups: true
+ input-groups: true
+ navs: false
+ navbar: false
+ breadcrumbs: false
+ pagination: true
+ pager: false
+ labels: false
+ badges: false
+ jumbotron: false
+ thumbnails: true
+ alerts: false
+ progress-bars: true
+ media: true
+ list-group: false
+ panels: false
+ wells: false
+ responsive-embed: false
+ close: false
+
+ # Components w/ JavaScript
+ modals: false
+ tooltip: false
+ popovers: false
+ carousel: false
+
+ # Utility classes
+ utilities: true
+ responsive-utilities: true
+
+### Bootstrap scripts
+scripts:
+ transition: false
+ alert: false
+ button: false
+ carousel: false
+ collapse: false
+ dropdown: false
+ modal: false
+ tooltip: false
+ popover: false
+ scrollspy: false
+ tab: false
+ affix: false
-typings
-app/**/*.js
-app/**/*.map
-app/**/*.css
-stylesheets/index.css
-bundles
-main.js
-main.js.map
+dist/
+typings/
+++ /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 { LoginComponent } from './login/index';
-import {
- AuthService,
- AuthStatus,
- Search,
- SearchComponent
-} from './shared/index';
-import {
- VideoAddComponent,
- VideoListComponent,
- VideoWatchComponent,
- VideoService
-} from './videos/index';
-
-@RouteConfig([
- {
- path: '/users/login',
- name: 'UserLogin',
- component: LoginComponent
- },
- {
- 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: [ AuthService, FriendService, HTTP_PROVIDERS, ROUTER_PROVIDERS, VideoService ]
-})
-
-export class AppComponent {
- choices = [];
- isLoggedIn: boolean;
-
- constructor(
- private authService: AuthService,
- private friendService: FriendService,
- private router: Router
- ) {
- this.isLoggedIn = this.authService.isLoggedIn();
-
- this.authService.loginChangedSource.subscribe(
- status => {
- if (status === AuthStatus.LoggedIn) {
- this.isLoggedIn = true;
- }
- }
- );
- }
-
- onSearch(search: Search) {
- if (search.value !== '') {
- const params = {
- field: search.field,
- search: search.value
- };
- 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';
-
-import { AuthService } from '../shared/index';
-
-@Injectable()
-export class FriendService {
- private static BASE_FRIEND_URL: string = '/api/v1/pods/';
-
- constructor (private http: Http, private authService: AuthService) {}
-
- makeFriends() {
- const headers = this.authService.getRequestHeader();
- return this.http.get(FriendService.BASE_FRIEND_URL + 'makefriends', { headers })
- .map(res => res.status)
- .catch(this.handleError);
- }
-
- quitFriends() {
- const headers = this.authService.getRequestHeader();
- return this.http.get(FriendService.BASE_FRIEND_URL + 'quitfriends', { headers })
- .map(res => res.status)
- .catch(this.handleError);
- }
-
- private handleError (error: Response): Observable<number> {
- console.error(error);
- return Observable.throw(error.json().error || 'Server error');
- }
-}
+++ /dev/null
-export * from './friend.service';
+++ /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-login',
- templateUrl: 'client/app/login/login.component.html'
-})
-
-export class LoginComponent {
- 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 * from './search/index';
-export * from './users/index'
+++ /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/search.component.html',
- directives: [ DROPDOWN_DIRECTIVES ]
-})
-
-export class SearchComponent {
- @Output() search = new EventEmitter<Search>();
-
- fieldChoices = {
- name: 'Name',
- author: 'Author',
- podUrl: 'Pod Url',
- magnetUri: 'Magnet Uri'
- };
- searchCriterias: Search = {
- field: 'name',
- value: ''
- };
-
- get choiceKeys() {
- return Object.keys(this.fieldChoices);
- }
-
- choose($event: MouseEvent, choice: SearchField) {
- $event.preventDefault();
- $event.stopPropagation();
-
- this.searchCriterias.field = choice;
- }
-
- doSearch() {
- this.search.emit(this.searchCriterias);
- }
-
- getStringChoice(choiceKey: SearchField) {
- return this.fieldChoices[choiceKey];
- }
-}
+++ /dev/null
-import { SearchField } from './search-field.type';
-
-export interface Search {
- field: SearchField;
- value: string;
-}
+++ /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 {
- private static BASE_CLIENT_URL = '/api/v1/users/client';
- private static BASE_LOGIN_URL = '/api/v1/users/token';
-
- loginChangedSource: Observable<AuthStatus>;
-
- private clientId: string;
- private clientSecret: string;
- private loginChanged: Subject<AuthStatus>;
-
- constructor(private http: Http) {
- this.loginChanged = new Subject<AuthStatus>();
- this.loginChangedSource = this.loginChanged.asObservable();
-
- // Fetch the client_id/client_secret
- // FIXME: save in local storage?
- this.http.get(AuthService.BASE_CLIENT_URL)
- .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);
- }
- );
- }
-
- getAuthRequestOptions(): RequestOptions {
- return new RequestOptions({ headers: this.getRequestHeader() });
- }
-
- getRequestHeader() {
- return new Headers({ 'Authorization': `${this.getTokenType()} ${this.getToken()}` });
- }
-
- getToken() {
- return localStorage.getItem('access_token');
- }
-
- getTokenType() {
- return localStorage.getItem('token_type');
- }
-
- getUser(): User {
- if (this.isLoggedIn() === false) {
- return null;
- }
-
- const user = User.load();
-
- return user;
- }
-
- isLoggedIn() {
- if (this.getToken()) {
- return true;
- } else {
- return false;
- }
- }
-
- 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(AuthService.BASE_LOGIN_URL, body.toString(), options)
- .map(res => res.json())
- .catch(this.handleError);
- }
-
- logout() {
- // TODO make HTTP request
- }
-
- 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() {
- 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() {
- 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() {
- return new User(localStorage.getItem('username'), Token.load());
- }
-
- constructor(username: string, hash_token: any) {
- this.username = username;
- this.token = new Token(hash_token);
- }
-
- save() {
- 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 {
- author: string;
- by: string;
- createdDate: Date;
- description: string;
- duration: string;
- id: string;
- isLocal: boolean;
- magnetUri: string;
- name: string;
- podUrl: string;
- thumbnailPath: string;
-
- private static createByString(author: string, podUrl: string) {
- let [ host, port ] = podUrl.replace(/^https?:\/\//, '').split(':');
-
- if (port === '80' || port === '443') {
- port = '';
- } else {
- port = ':' + port;
- }
-
- return author + '@' + host + port;
- }
-
- private static createDurationString(duration: number) {
- 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();
- }
-
- constructor(hash: {
- author: string,
- createdDate: string,
- description: string,
- duration: number;
- id: string,
- isLocal: boolean,
- magnetUri: string,
- name: string,
- podUrl: string,
- thumbnailPath: string
- }) {
- this.author = hash.author;
- this.createdDate = new Date(hash.createdDate);
- this.description = hash.description;
- this.duration = Video.createDurationString(hash.duration);
- this.id = hash.id;
- this.isLocal = hash.isLocal;
- this.magnetUri = hash.magnetUri;
- this.name = hash.name;
- this.podUrl = hash.podUrl;
- this.thumbnailPath = hash.thumbnailPath;
-
- this.by = Video.createByString(hash.author, hash.podUrl);
- }
-
- isRemovableBy(user) {
- 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 '../../shared/index';
-import { Video } from './video.model';
-
-@Injectable()
-export class VideoService {
- private static BASE_VIDEO_URL = '/api/v1/videos/';
-
- constructor(
- private authService: AuthService,
- private http: Http
- ) {}
-
- getVideo(id: string) {
- return this.http.get(VideoService.BASE_VIDEO_URL + id)
- .map(res => <Video> res.json())
- .catch(this.handleError);
- }
-
- getVideos(pagination: Pagination, sort: SortField) {
- const params = this.createPaginationParams(pagination);
-
- if (sort) params.set('sort', sort);
-
- return this.http.get(VideoService.BASE_VIDEO_URL, { search: params })
- .map(res => res.json())
- .map(this.extractVideos)
- .catch(this.handleError);
- }
-
- removeVideo(id: string) {
- const options = this.authService.getAuthRequestOptions();
- return this.http.delete(VideoService.BASE_VIDEO_URL + 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(VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value), { search: params })
- .map(res => res.json())
- .map(this.extractVideos)
- .catch(this.handleError);
- }
-
- 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;
- }
-
- 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');
- }
-}
+++ /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
-/// <reference path="../../../typings/globals/jquery/index.d.ts" />
-/// <reference path="../../../typings/globals/jquery.fileupload/index.d.ts" />
-
-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 '../../shared/index';
-
-@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 {
- fileToUpload: any;
- progressBar: { value: number; max: number; } = { value: 0, max: 0 };
- user: User;
-
- private form: any;
-
- constructor(
- private authService: AuthService,
- private elementRef: ElementRef,
- private router: Router
- ) {}
-
- 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.formData = jQuery(this.elementRef.nativeElement).find('form').serializeArray();
- this.form.headers = this.authService.getRequestHeader().toJSON();
- 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="noVideo()">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 { AuthService, Search, SearchField, User } from '../../shared/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: [ LoaderComponent, PAGINATION_DIRECTIVES, ROUTER_DIRECTIVES, VideoMiniatureComponent, VideoSortComponent ]
-})
-
-export class VideoListComponent implements OnInit {
- loading = false;
- pagination: Pagination = {
- currentPage: 1,
- itemsPerPage: 9,
- total: 0
- };
- sort: SortField;
- user: User = null;
- videos: Video[] = [];
-
- private search: Search;
-
- 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';
- }
-
- 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)
- );
- }
-
- noVideo() {
- return !this.loading && this.videos.length === 0;
- }
-
- onRemoved(video: Video) {
- 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.field = this.search.field;
- params.search = this.search.value;
- }
-
- 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 '../../shared/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() user: User;
- @Input() video: Video;
-
- hovering = false;
-
- constructor(private videoService: VideoService) {}
-
- displayRemoveIcon() {
- return this.hovering && this.video.isRemovableBy(this.user);
- }
-
- onBlur() {
- this.hovering = false;
- }
-
- onHover() {
- this.hovering = true;
- }
-
- 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',
- 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) {
- return this.sortChoices[choiceKey];
- }
-
- onSortChange() {
- this.sort.emit(this.currentSort);
- }
-}
+++ /dev/null
-export * from './video-watch.component';
-export * from './webtorrent.service';
+++ /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';
-import { WebTorrentService } from './webtorrent.service';
-
-@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' ],
- providers: [ WebTorrentService ],
- directives: [ LoaderComponent ],
- pipes: [ BytesPipe ]
-})
-
-export class VideoWatchComponent implements OnInit, CanDeactivate {
- downloadSpeed: number;
- loading: boolean = false;
- numPeers: number;
- uploadSpeed: number;
- video: Video;
-
- private interval: NodeJS.Timer;
-
- constructor(
- private elementRef: ElementRef,
- private routeParams: RouteParams,
- private videoService: VideoService,
- private webTorrentService: WebTorrentService
- ) {}
-
- loadVideo(video: Video) {
- this.loading = true;
- this.video = video;
- console.log('Adding ' + this.video.magnetUri + '.');
-
- this.webTorrentService.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.numPeers = torrent.numPeers;
- this.uploadSpeed = torrent.uploadSpeed;
- }, 1000);
- });
- }
-
- ngOnInit() {
- let id = this.routeParams.get('id');
- this.videoService.getVideo(id).subscribe(
- video => this.loadVideo(video),
- error => alert(error)
- );
- }
-
- routerCanDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
- console.log('Removing video from webtorrent.');
- clearInterval(this.interval);
- this.webTorrentService.remove(this.video.magnetUri);
- return true;
- }
-}
+++ /dev/null
-// Don't use webtorrent typings for now
-// It misses some little things I'll fix later
-// <reference path="../../../typings/globals/webtorrent/index.d.ts" />
-
-import { Injectable } from '@angular/core';
-
-// import WebTorrent = require('webtorrent');
-declare var WebTorrent: any;
-
-@Injectable()
-export class WebTorrentService {
- // private client: WebTorrent.Client;
- private client: any;
-
- constructor() {
- this.client = new WebTorrent({ dht: false });
- }
-
- add(magnetUri: string, callback: Function) {
- return this.client.add(magnetUri, callback);
- }
-
- remove(magnetUri: string) {
- return this.client.remove(magnetUri);
- }
-}
--- /dev/null
+const path = require('path')
+
+const ROOT = path.resolve(__dirname, '..')
+
+console.log('root directory:', root() + '\n')
+
+function hasProcessFlag (flag) {
+ return process.argv.join('').indexOf(flag) > -1
+}
+
+function root (args) {
+ args = Array.prototype.slice.call(arguments, 0)
+ return path.join.apply(path, [ROOT].concat(args))
+}
+
+exports.hasProcessFlag = hasProcessFlag
+exports.root = root
--- /dev/null
+const webpack = require('webpack')
+const helpers = require('./helpers')
+
+/*
+ * Webpack Plugins
+ */
+
+var CopyWebpackPlugin = (CopyWebpackPlugin = require('copy-webpack-plugin'), CopyWebpackPlugin.default || CopyWebpackPlugin)
+const HtmlWebpackPlugin = require('html-webpack-plugin')
+const ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin
+
+/*
+ * Webpack Constants
+ */
+const METADATA = {
+ title: 'PeerTube',
+ baseUrl: '/'
+}
+
+/*
+ * Webpack configuration
+ *
+ * See: http://webpack.github.io/docs/configuration.html#cli
+ */
+module.exports = {
+ /*
+ * Static metadata for index.html
+ *
+ * See: (custom attribute)
+ */
+ metadata: METADATA,
+
+ /*
+ * Cache generated modules and chunks to improve performance for multiple incremental builds.
+ * This is enabled by default in watch mode.
+ * You can pass false to disable it.
+ *
+ * See: http://webpack.github.io/docs/configuration.html#cache
+ */
+ // cache: false,
+
+ /*
+ * The entry point for the bundle
+ * Our Angular.js app
+ *
+ * See: http://webpack.github.io/docs/configuration.html#entry
+ */
+ entry: {
+ 'polyfills': './src/polyfills.ts',
+ 'vendor': './src/vendor.ts',
+ 'main': './src/main.ts'
+ },
+
+ /*
+ * Options affecting the resolving of modules.
+ *
+ * See: http://webpack.github.io/docs/configuration.html#resolve
+ */
+ resolve: {
+ /*
+ * An array of extensions that should be used to resolve modules.
+ *
+ * See: http://webpack.github.io/docs/configuration.html#resolve-extensions
+ */
+ extensions: [ '', '.ts', '.js', '.scss' ],
+
+ // Make sure root is src
+ root: helpers.root('src'),
+
+ // remove other default values
+ modulesDirectories: [ 'node_modules' ]
+
+ },
+
+ /*
+ * Options affecting the normal modules.
+ *
+ * See: http://webpack.github.io/docs/configuration.html#module
+ */
+ module: {
+ /*
+ * An array of applied pre and post loaders.
+ *
+ * See: http://webpack.github.io/docs/configuration.html#module-preloaders-module-postloaders
+ */
+ preLoaders: [
+
+ /*
+ * Tslint loader support for *.ts files
+ *
+ * See: https://github.com/wbuchwalter/tslint-loader
+ */
+ // { test: /\.ts$/, loader: 'tslint-loader', exclude: [ helpers.root('node_modules') ] },
+
+ /*
+ * Source map loader support for *.js files
+ * Extracts SourceMaps for source files that as added as sourceMappingURL comment.
+ *
+ * See: https://github.com/webpack/source-map-loader
+ */
+ {
+ test: /\.js$/,
+ loader: 'source-map-loader',
+ exclude: [
+ // these packages have problems with their sourcemaps
+ helpers.root('node_modules/rxjs'),
+ helpers.root('node_modules/@angular')
+ ]
+ }
+
+ ],
+
+ /*
+ * An array of automatically applied loaders.
+ *
+ * IMPORTANT: The loaders here are resolved relative to the resource which they are applied to.
+ * This means they are not resolved relative to the configuration file.
+ *
+ * See: http://webpack.github.io/docs/configuration.html#module-loaders
+ */
+ loaders: [
+
+ /*
+ * Typescript loader support for .ts and Angular 2 async routes via .async.ts
+ *
+ * See: https://github.com/s-panferov/awesome-typescript-loader
+ */
+ {
+ test: /\.ts$/,
+ loader: 'awesome-typescript-loader',
+ exclude: [/\.(spec|e2e)\.ts$/]
+ },
+
+ /*
+ * Json loader support for *.json files.
+ *
+ * See: https://github.com/webpack/json-loader
+ */
+ {
+ test: /\.json$/,
+ loader: 'json-loader'
+ },
+
+ /*
+ * Raw loader support for *.css files
+ * Returns file content as string
+ *
+ * See: https://github.com/webpack/raw-loader
+ */
+ {
+ test: /\.scss$/,
+ exclude: /node_modules/,
+ loaders: [ 'raw-loader', 'sass-loader' ]
+ },
+
+ {
+ test: /\.(woff2?|ttf|eot|svg)$/,
+ loader: 'url?limit=10000&name=assets/fonts/[hash].[ext]'
+ },
+
+ /* Raw loader support for *.html
+ * Returns file content as string
+ *
+ * See: https://github.com/webpack/raw-loader
+ */
+ {
+ test: /\.html$/,
+ loader: 'raw-loader',
+ exclude: [ helpers.root('src/index.html') ]
+ }
+
+ ]
+
+ },
+
+ /*
+ * Add additional plugins to the compiler.
+ *
+ * See: http://webpack.github.io/docs/configuration.html#plugins
+ */
+ plugins: [
+
+ /*
+ * Plugin: ForkCheckerPlugin
+ * Description: Do type checking in a separate process, so webpack don't need to wait.
+ *
+ * See: https://github.com/s-panferov/awesome-typescript-loader#forkchecker-boolean-defaultfalse
+ */
+ new ForkCheckerPlugin(),
+
+ /*
+ * Plugin: OccurenceOrderPlugin
+ * Description: Varies the distribution of the ids to get the smallest id length
+ * for often used ids.
+ *
+ * See: https://webpack.github.io/docs/list-of-plugins.html#occurrenceorderplugin
+ * See: https://github.com/webpack/docs/wiki/optimization#minimize
+ */
+ new webpack.optimize.OccurenceOrderPlugin(true),
+
+ /*
+ * Plugin: CommonsChunkPlugin
+ * Description: Shares common code between the pages.
+ * It identifies common modules and put them into a commons chunk.
+ *
+ * See: https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin
+ * See: https://github.com/webpack/docs/wiki/optimization#multi-page-app
+ */
+ new webpack.optimize.CommonsChunkPlugin({
+ name: [ 'polyfills', 'vendor' ].reverse()
+ }),
+
+ /*
+ * Plugin: CopyWebpackPlugin
+ * Description: Copy files and directories in webpack.
+ *
+ * Copies project static assets.
+ *
+ * See: https://www.npmjs.com/package/copy-webpack-plugin
+ */
+ new CopyWebpackPlugin([{
+ from: 'src/assets',
+ to: 'assets'
+ }]),
+
+ /*
+ * Plugin: HtmlWebpackPlugin
+ * Description: Simplifies creation of HTML files to serve your webpack bundles.
+ * This is especially useful for webpack bundles that include a hash in the filename
+ * which changes every compilation.
+ *
+ * See: https://github.com/ampedandwired/html-webpack-plugin
+ */
+ new HtmlWebpackPlugin({
+ template: 'src/index.html',
+ chunksSortMode: 'dependency'
+ }),
+
+ new webpack.ProvidePlugin({
+ jQuery: 'jquery',
+ $: 'jquery',
+ jquery: 'jquery'
+ })
+
+ ],
+
+ /*
+ * Include polyfills or mocks for various node stuff
+ * Description: Node configuration
+ *
+ * See: https://webpack.github.io/docs/configuration.html#node
+ */
+ node: {
+ global: 'window',
+ crypto: 'empty',
+ module: false,
+ clearImmediate: false,
+ setImmediate: false
+ }
+
+}
--- /dev/null
+const helpers = require('./helpers')
+const webpackMerge = require('webpack-merge') // used to merge webpack configs
+const commonConfig = require('./webpack.common.js') // the settings that are common to prod and dev
+
+/**
+ * Webpack Plugins
+ */
+const DefinePlugin = require('webpack/lib/DefinePlugin')
+
+/**
+ * Webpack Constants
+ */
+const ENV = process.env.ENV = process.env.NODE_ENV = 'development'
+const HMR = helpers.hasProcessFlag('hot')
+const METADATA = webpackMerge(commonConfig.metadata, {
+ host: 'localhost',
+ port: 3000,
+ ENV: ENV,
+ HMR: HMR
+})
+
+/**
+ * Webpack configuration
+ *
+ * See: http://webpack.github.io/docs/configuration.html#cli
+ */
+module.exports = webpackMerge(commonConfig, {
+ /**
+ * Merged metadata from webpack.common.js for index.html
+ *
+ * See: (custom attribute)
+ */
+ metadata: METADATA,
+
+ /**
+ * Switch loaders to debug mode.
+ *
+ * See: http://webpack.github.io/docs/configuration.html#debug
+ */
+ debug: true,
+
+ /**
+ * Developer tool to enhance debugging
+ *
+ * See: http://webpack.github.io/docs/configuration.html#devtool
+ * See: https://github.com/webpack/docs/wiki/build-performance#sourcemaps
+ */
+ devtool: 'cheap-module-source-map',
+
+ /**
+ * Options affecting the output of the compilation.
+ *
+ * See: http://webpack.github.io/docs/configuration.html#output
+ */
+ output: {
+ /**
+ * The output directory as absolute path (required).
+ *
+ * See: http://webpack.github.io/docs/configuration.html#output-path
+ */
+ path: helpers.root('dist'),
+
+ /**
+ * Specifies the name of each output file on disk.
+ * IMPORTANT: You must not specify an absolute path here!
+ *
+ * See: http://webpack.github.io/docs/configuration.html#output-filename
+ */
+ filename: '[name].bundle.js',
+
+ /**
+ * The filename of the SourceMaps for the JavaScript files.
+ * They are inside the output.path directory.
+ *
+ * See: http://webpack.github.io/docs/configuration.html#output-sourcemapfilename
+ */
+ sourceMapFilename: '[name].map',
+
+ /** The filename of non-entry chunks as relative path
+ * inside the output.path directory.
+ *
+ * See: http://webpack.github.io/docs/configuration.html#output-chunkfilename
+ */
+ chunkFilename: '[id].chunk.js',
+
+ publicPath: '/client/'
+
+ },
+
+ plugins: [
+
+ /**
+ * Plugin: DefinePlugin
+ * Description: Define free variables.
+ * Useful for having development builds with debug logging or adding global constants.
+ *
+ * Environment helpers
+ *
+ * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
+ */
+ // NOTE: when adding more properties, make sure you include them in custom-typings.d.ts
+ new DefinePlugin({
+ 'ENV': JSON.stringify(METADATA.ENV),
+ 'HMR': METADATA.HMR,
+ 'process.env': {
+ 'ENV': JSON.stringify(METADATA.ENV),
+ 'NODE_ENV': JSON.stringify(METADATA.ENV),
+ 'HMR': METADATA.HMR
+ }
+ })
+ ],
+
+ /**
+ * Static analysis linter for TypeScript advanced options configuration
+ * Description: An extensible linter for the TypeScript language.
+ *
+ * See: https://github.com/wbuchwalter/tslint-loader
+ */
+ tslint: {
+ emitErrors: false,
+ failOnHint: false,
+ resourcePath: 'src'
+ },
+
+ /**
+ * Webpack Development Server configuration
+ * Description: The webpack-dev-server is a little node.js Express server.
+ * The server emits information about the compilation state to the client,
+ * which reacts to those events.
+ *
+ * See: https://webpack.github.io/docs/webpack-dev-server.html
+ */
+ devServer: {
+ port: METADATA.port,
+ host: METADATA.host,
+ historyApiFallback: true,
+ watchOptions: {
+ aggregateTimeout: 300,
+ poll: 1000
+ },
+ outputPath: helpers.root('dist')
+ },
+
+ /*
+ * Include polyfills or mocks for various node stuff
+ * Description: Node configuration
+ *
+ * See: https://webpack.github.io/docs/configuration.html#node
+ */
+ node: {
+ global: 'window',
+ crypto: 'empty',
+ process: true,
+ module: false,
+ clearImmediate: false,
+ setImmediate: false
+ }
+
+})
+++ /dev/null
-<html>
- <head>
- <base href="/">
-
- <title>PeerTube</title>
-
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
-
- <link rel="stylesheet" href="client/stylesheets/index.css">
-
- <!-- 1. Load libraries -->
- <!-- IE required polyfills, in this exact order -->
- <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="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="client/node_modules/webtorrent/webtorrent.min.js"></script>
-
- <!-- 2. Configure SystemJS -->
- <script src="client/systemjs.config.js"></script>
- <script>
- System.import('client').catch(function(err){ console.error(err); });
- </script>
- </head>
-
- <!-- 3. Display the application -->
- <body>
- <my-app>Loading...</my-app>
- </body>
-</html>
+++ /dev/null
-import { bootstrap } from '@angular/platform-browser-dynamic';
-
-import { AppComponent } from './app/app.component';
-
-bootstrap(AppComponent);
"url": "git://github.com/Chocobozzz/PeerTube.git"
},
"scripts": {
- "tsc": "tsc",
- "tsc:w": "tsc -w",
"typings": "typings",
"postinstall": "typings install",
- "test": "standard && tslint -c ./tslint.json angular/**/*.ts"
+ "test": "standard && tslint -c ./tslint.json angular/**/*.ts",
+ "build": "webpack --config config/webpack.dev.js --progress --profile --colors --display-error-details --display-cached",
+ "watch": "npm run build -- --watch"
},
"license": "GPLv3",
"dependencies": {
"@angular/router-deprecated": "2.0.0-rc.1",
"angular-pipes": "^2.0.0",
"blueimp-file-upload": "^9.12.1",
+ "bootstrap-loader": "^1.0.8",
"bootstrap-sass": "^3.3.6",
+ "core-js": "^2.4.0",
"es6-promise": "^3.0.2",
"es6-shim": "^0.35.0",
"jquery": "^2.2.3",
"jquery.ui.widget": "^1.10.3",
"ng2-bootstrap": "^1.0.16",
+ "normalize.css": "^4.1.1",
"reflect-metadata": "0.1.3",
"rxjs": "5.0.0-beta.6",
"systemjs": "0.19.27",
"zone.js": "0.6.12"
},
"devDependencies": {
+ "awesome-typescript-loader": "^0.17.0",
"codelyzer": "0.0.19",
+ "compression-webpack-plugin": "^0.3.1",
+ "copy-webpack-plugin": "^3.0.1",
+ "css-loader": "^0.23.1",
+ "es6-promise-loader": "^1.0.1",
+ "exports-loader": "^0.6.3",
+ "expose-loader": "^0.7.1",
+ "file-loader": "^0.8.5",
+ "html-webpack-plugin": "^2.19.0",
+ "imports-loader": "^0.6.5",
+ "json-loader": "^0.5.4",
+ "node-sass": "^3.7.0",
+ "raw-loader": "^0.5.1",
+ "resolve-url-loader": "^1.4.3",
+ "sass-loader": "^3.2.0",
+ "source-map-loader": "^0.1.5",
"standard": "^7.0.1",
+ "style-loader": "^0.13.1",
"systemjs-builder": "^0.15.16",
+ "ts-helpers": "^1.1.1",
+ "ts-node": "^0.7.3",
"tslint": "^3.7.4",
+ "tslint-loader": "^2.1.4",
"typescript": "^1.8.10",
- "typings": "^1.0.4"
+ "typings": "^1.0.4",
+ "url-loader": "^0.5.7",
+ "webpack": "^1.13.1",
+ "webpack-dev-server": "^1.14.1",
+ "webpack-md5-hash": "0.0.5",
+ "webpack-merge": "^0.13.0"
},
"standard": {
"ignore": [
--- /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';
+import { LoginComponent } from './login';
+import {
+ AuthService,
+ AuthStatus,
+ Search,
+ SearchComponent
+} from './shared';
+import {
+ VideoAddComponent,
+ VideoListComponent,
+ VideoWatchComponent,
+ VideoService
+} from './videos';
+
+@RouteConfig([
+ {
+ path: '/users/login',
+ name: 'UserLogin',
+ component: LoginComponent
+ },
+ {
+ 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',
+ template: require('./app.component.html'),
+ styles: [ require('./app.component.scss') ],
+ directives: [ ROUTER_DIRECTIVES, SearchComponent ],
+ providers: [ AuthService, FriendService, HTTP_PROVIDERS, ROUTER_PROVIDERS, VideoService ]
+})
+
+export class AppComponent {
+ choices = [];
+ isLoggedIn: boolean;
+
+ constructor(
+ private authService: AuthService,
+ private friendService: FriendService,
+ private router: Router
+ ) {
+ this.isLoggedIn = this.authService.isLoggedIn();
+
+ this.authService.loginChangedSource.subscribe(
+ status => {
+ if (status === AuthStatus.LoggedIn) {
+ this.isLoggedIn = true;
+ }
+ }
+ );
+ }
+
+ onSearch(search: Search) {
+ if (search.value !== '') {
+ const params = {
+ field: search.field,
+ search: search.value
+ };
+ 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';
+
+import { AuthService } from '../shared';
+
+@Injectable()
+export class FriendService {
+ private static BASE_FRIEND_URL: string = '/api/v1/pods/';
+
+ constructor (private http: Http, private authService: AuthService) {}
+
+ makeFriends() {
+ const headers = this.authService.getRequestHeader();
+ return this.http.get(FriendService.BASE_FRIEND_URL + 'makefriends', { headers })
+ .map(res => res.status)
+ .catch(this.handleError);
+ }
+
+ quitFriends() {
+ const headers = this.authService.getRequestHeader();
+ return this.http.get(FriendService.BASE_FRIEND_URL + 'quitfriends', { headers })
+ .map(res => res.status)
+ .catch(this.handleError);
+ }
+
+ private handleError (error: Response): Observable<number> {
+ console.error(error);
+ return Observable.throw(error.json().error || 'Server error');
+ }
+}
--- /dev/null
+export * from './friend.service';
--- /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';
+
+@Component({
+ selector: 'my-login',
+ template: require('./login.component.html')
+})
+
+export class LoginComponent {
+ 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 * from './search';
+export * from './users'
--- /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',
+ template: require('./search.component.html'),
+ directives: [ DROPDOWN_DIRECTIVES ]
+})
+
+export class SearchComponent {
+ @Output() search = new EventEmitter<Search>();
+
+ fieldChoices = {
+ name: 'Name',
+ author: 'Author',
+ podUrl: 'Pod Url',
+ magnetUri: 'Magnet Uri'
+ };
+ searchCriterias: Search = {
+ field: 'name',
+ value: ''
+ };
+
+ get choiceKeys() {
+ return Object.keys(this.fieldChoices);
+ }
+
+ choose($event: MouseEvent, choice: SearchField) {
+ $event.preventDefault();
+ $event.stopPropagation();
+
+ this.searchCriterias.field = choice;
+ }
+
+ doSearch() {
+ this.search.emit(this.searchCriterias);
+ }
+
+ getStringChoice(choiceKey: SearchField) {
+ return this.fieldChoices[choiceKey];
+ }
+}
--- /dev/null
+import { SearchField } from './search-field.type';
+
+export interface Search {
+ field: SearchField;
+ value: string;
+}
--- /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 {
+ private static BASE_CLIENT_URL = '/api/v1/users/client';
+ private static BASE_LOGIN_URL = '/api/v1/users/token';
+
+ loginChangedSource: Observable<AuthStatus>;
+
+ private clientId: string;
+ private clientSecret: string;
+ private loginChanged: Subject<AuthStatus>;
+
+ constructor(private http: Http) {
+ this.loginChanged = new Subject<AuthStatus>();
+ this.loginChangedSource = this.loginChanged.asObservable();
+
+ // Fetch the client_id/client_secret
+ // FIXME: save in local storage?
+ this.http.get(AuthService.BASE_CLIENT_URL)
+ .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);
+ }
+ );
+ }
+
+ getAuthRequestOptions(): RequestOptions {
+ return new RequestOptions({ headers: this.getRequestHeader() });
+ }
+
+ getRequestHeader() {
+ return new Headers({ 'Authorization': `${this.getTokenType()} ${this.getToken()}` });
+ }
+
+ getToken() {
+ return localStorage.getItem('access_token');
+ }
+
+ getTokenType() {
+ return localStorage.getItem('token_type');
+ }
+
+ getUser(): User {
+ if (this.isLoggedIn() === false) {
+ return null;
+ }
+
+ const user = User.load();
+
+ return user;
+ }
+
+ isLoggedIn() {
+ if (this.getToken()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ 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(AuthService.BASE_LOGIN_URL, body.toString(), options)
+ .map(res => res.json())
+ .catch(this.handleError);
+ }
+
+ logout() {
+ // TODO make HTTP request
+ }
+
+ 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() {
+ 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() {
+ 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() {
+ return new User(localStorage.getItem('username'), Token.load());
+ }
+
+ constructor(username: string, hash_token: any) {
+ this.username = username;
+ this.token = new Token(hash_token);
+ }
+
+ save() {
+ localStorage.setItem('username', this.username);
+ this.token.save();
+ }
+}
--- /dev/null
+export * from './shared';
+export * from './video-add';
+export * from './video-list';
+export * from './video-watch';
--- /dev/null
+export * from './loader';
+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',
+ styles: [ require('./loader.component.scss') ],
+ template: require('./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 {
+ author: string;
+ by: string;
+ createdDate: Date;
+ description: string;
+ duration: string;
+ id: string;
+ isLocal: boolean;
+ magnetUri: string;
+ name: string;
+ podUrl: string;
+ thumbnailPath: string;
+
+ private static createByString(author: string, podUrl: string) {
+ let [ host, port ] = podUrl.replace(/^https?:\/\//, '').split(':');
+
+ if (port === '80' || port === '443') {
+ port = '';
+ } else {
+ port = ':' + port;
+ }
+
+ return author + '@' + host + port;
+ }
+
+ private static createDurationString(duration: number) {
+ 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();
+ }
+
+ constructor(hash: {
+ author: string,
+ createdDate: string,
+ description: string,
+ duration: number;
+ id: string,
+ isLocal: boolean,
+ magnetUri: string,
+ name: string,
+ podUrl: string,
+ thumbnailPath: string
+ }) {
+ this.author = hash.author;
+ this.createdDate = new Date(hash.createdDate);
+ this.description = hash.description;
+ this.duration = Video.createDurationString(hash.duration);
+ this.id = hash.id;
+ this.isLocal = hash.isLocal;
+ this.magnetUri = hash.magnetUri;
+ this.name = hash.name;
+ this.podUrl = hash.podUrl;
+ this.thumbnailPath = hash.thumbnailPath;
+
+ this.by = Video.createByString(hash.author, hash.podUrl);
+ }
+
+ isRemovableBy(user) {
+ 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';
+import { SortField } from './sort-field.type';
+import { AuthService } from '../../shared';
+import { Video } from './video.model';
+
+@Injectable()
+export class VideoService {
+ private static BASE_VIDEO_URL = '/api/v1/videos/';
+
+ constructor(
+ private authService: AuthService,
+ private http: Http
+ ) {}
+
+ getVideo(id: string) {
+ return this.http.get(VideoService.BASE_VIDEO_URL + id)
+ .map(res => <Video> res.json())
+ .catch(this.handleError);
+ }
+
+ getVideos(pagination: Pagination, sort: SortField) {
+ const params = this.createPaginationParams(pagination);
+
+ if (sort) params.set('sort', sort);
+
+ return this.http.get(VideoService.BASE_VIDEO_URL, { search: params })
+ .map(res => res.json())
+ .map(this.extractVideos)
+ .catch(this.handleError);
+ }
+
+ removeVideo(id: string) {
+ const options = this.authService.getAuthRequestOptions();
+ return this.http.delete(VideoService.BASE_VIDEO_URL + 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(VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value), { search: params })
+ .map(res => res.json())
+ .map(this.extractVideos)
+ .catch(this.handleError);
+ }
+
+ 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;
+ }
+
+ 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');
+ }
+}
--- /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
+/// <reference path="../../../../typings/globals/jquery/index.d.ts" />
+/// <reference path="../../../../typings/globals/jquery.fileupload/index.d.ts" />
+
+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 '../../shared';
+
+@Component({
+ selector: 'my-videos-add',
+ styles: [ require('./video-add.component.scss') ],
+ template: require('./video-add.component.html'),
+ directives: [ PROGRESSBAR_DIRECTIVES ],
+ pipes: [ BytesPipe ]
+})
+
+export class VideoAddComponent implements OnInit {
+ fileToUpload: any;
+ progressBar: { value: number; max: number; } = { value: 0, max: 0 };
+ user: User;
+
+ private form: any;
+
+ constructor(
+ private authService: AuthService,
+ private elementRef: ElementRef,
+ private router: Router
+ ) {}
+
+ 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.formData = jQuery(this.elementRef.nativeElement).find('form').serializeArray();
+ this.form.headers = this.authService.getRequestHeader().toJSON();
+ 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="noVideo()">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';
+import { AuthService, Search, SearchField, User } from '../../shared';
+import { VideoMiniatureComponent } from './video-miniature.component';
+import { VideoSortComponent } from './video-sort.component';
+
+@Component({
+ selector: 'my-videos-list',
+ styles: [ require('./video-list.component.scss') ],
+ template: require('./video-list.component.html'),
+ directives: [ LoaderComponent, PAGINATION_DIRECTIVES, ROUTER_DIRECTIVES, VideoMiniatureComponent, VideoSortComponent ]
+})
+
+export class VideoListComponent implements OnInit {
+ loading = false;
+ pagination: Pagination = {
+ currentPage: 1,
+ itemsPerPage: 9,
+ total: 0
+ };
+ sort: SortField;
+ user: User = null;
+ videos: Video[] = [];
+
+ private search: Search;
+
+ 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';
+ }
+
+ 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)
+ );
+ }
+
+ noVideo() {
+ return !this.loading && this.videos.length === 0;
+ }
+
+ onRemoved(video: Video) {
+ 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.field = this.search.field;
+ params.search = this.search.value;
+ }
+
+ 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';
+import { User } from '../../shared';
+
+@Component({
+ selector: 'my-video-miniature',
+ styles: [ require('./video-miniature.component.scss') ],
+ template: require('./video-miniature.component.html'),
+ directives: [ ROUTER_DIRECTIVES ],
+ pipes: [ DatePipe ]
+})
+
+export class VideoMiniatureComponent {
+ @Output() removed = new EventEmitter<any>();
+
+ @Input() user: User;
+ @Input() video: Video;
+
+ hovering = false;
+
+ constructor(private videoService: VideoService) {}
+
+ displayRemoveIcon() {
+ return this.hovering && this.video.isRemovableBy(this.user);
+ }
+
+ onBlur() {
+ this.hovering = false;
+ }
+
+ onHover() {
+ this.hovering = true;
+ }
+
+ 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';
+
+@Component({
+ selector: 'my-video-sort',
+ template: require('./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) {
+ return this.sortChoices[choiceKey];
+ }
+
+ onSortChange() {
+ this.sort.emit(this.currentSort);
+ }
+}
--- /dev/null
+export * from './video-watch.component';
+export * from './webtorrent.service';
--- /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';
+import { WebTorrentService } from './webtorrent.service';
+
+@Component({
+ selector: 'my-video-watch',
+ template: require('./video-watch.component.html'),
+ styles: [ require('./video-watch.component.scss') ],
+ providers: [ WebTorrentService ],
+ directives: [ LoaderComponent ],
+ pipes: [ BytesPipe ]
+})
+
+export class VideoWatchComponent implements OnInit, CanDeactivate {
+ downloadSpeed: number;
+ loading: boolean = false;
+ numPeers: number;
+ uploadSpeed: number;
+ video: Video;
+
+ private interval: NodeJS.Timer;
+
+ constructor(
+ private elementRef: ElementRef,
+ private routeParams: RouteParams,
+ private videoService: VideoService,
+ private webTorrentService: WebTorrentService
+ ) {}
+
+ loadVideo(video: Video) {
+ this.loading = true;
+ this.video = video;
+ console.log('Adding ' + this.video.magnetUri + '.');
+
+ this.webTorrentService.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.numPeers = torrent.numPeers;
+ this.uploadSpeed = torrent.uploadSpeed;
+ }, 1000);
+ });
+ }
+
+ ngOnInit() {
+ let id = this.routeParams.get('id');
+ this.videoService.getVideo(id).subscribe(
+ video => this.loadVideo(video),
+ error => alert(error)
+ );
+ }
+
+ routerCanDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
+ console.log('Removing video from webtorrent.');
+ clearInterval(this.interval);
+ this.webTorrentService.remove(this.video.magnetUri);
+ return true;
+ }
+}
--- /dev/null
+// Don't use webtorrent typings for now
+// It misses some little things I'll fix later
+// <reference path="../../../../typings/globals/webtorrent/index.d.ts" />
+
+import { Injectable } from '@angular/core';
+
+// import WebTorrent = require('webtorrent');
+declare var WebTorrent: any;
+
+@Injectable()
+export class WebTorrentService {
+ // private client: WebTorrent.Client;
+ private client: any;
+
+ constructor() {
+ this.client = new WebTorrent({ dht: false });
+ }
+
+ add(magnetUri: string, callback: Function) {
+ return this.client.add(magnetUri, callback);
+ }
+
+ remove(magnetUri: string) {
+ return this.client.remove(magnetUri);
+ }
+}
--- /dev/null
+/*
+ * Custom Type Definitions
+ * When including 3rd party modules you also need to include the type definition for the module
+ * if they don't provide one within the module. You can try to install it with typings
+
+typings install node --save
+
+ * If you can't find the type definition in the registry we can make an ambient definition in
+ * this file for now. For example
+
+declare module "my-module" {
+ export function doesSomething(value: string): string;
+}
+
+ *
+ * If you're prototying and you will fix the types later you can also declare it as type any
+ *
+
+declare var assert: any;
+
+ *
+ * If you're importing a module that uses Node.js modules which are CommonJS you need to import as
+ *
+
+import * as _ from 'lodash'
+
+ * You can include your type definitions in this file until you create one for the typings registry
+ * see https://github.com/typings/registry
+ *
+ */
+
+
+// Extra variables that live on Global that will be replaced by webpack DefinePlugin
+declare var ENV: string;
+declare var HMR: boolean;
+interface GlobalEnvironment {
+ ENV;
+ HMR;
+}
+
+interface WebpackModule {
+ hot: {
+ data?: any,
+ idle: any,
+ accept(dependencies?: string | string[], callback?: (updatedDependencies?: any) => void): void;
+ decline(dependencies?: string | string[]): void;
+ dispose(callback?: (data?: any) => void): void;
+ addDisposeHandler(callback?: (data?: any) => void): void;
+ removeDisposeHandler(callback?: (data?: any) => void): void;
+ check(autoApply?: any, callback?: (err?: Error, outdatedModules?: any[]) => void): void;
+ apply(options?: any, callback?: (err?: Error, outdatedModules?: any[]) => void): void;
+ status(callback?: (status?: string) => void): void | string;
+ removeStatusHandler(callback?: (status?: string) => void): void;
+ };
+}
+
+interface WebpackRequire {
+ context(file: string, flag?: boolean, exp?: RegExp): any;
+}
+
+
+interface ErrorStackTraceLimit {
+ stackTraceLimit: number;
+}
+
+
+
+// Extend typings
+interface NodeRequire extends WebpackRequire {}
+interface ErrorConstructor extends ErrorStackTraceLimit {}
+interface NodeModule extends WebpackModule {}
+interface Global extends GlobalEnvironment {}
+
+
+declare namespace Reflect {
+ function decorate(decorators: ClassDecorator[], target: Function): Function;
+ function decorate(
+ decorators: (PropertyDecorator | MethodDecorator)[],
+ target: Object,
+ targetKey: string | symbol,
+ descriptor?: PropertyDescriptor): PropertyDescriptor;
+
+ function metadata(metadataKey: any, metadataValue: any): {
+ (target: Function): void;
+ (target: Object, propertyKey: string | symbol): void;
+ };
+ function defineMetadata(metadataKey: any, metadataValue: any, target: Object): void;
+ function defineMetadata(
+ metadataKey: any,
+ metadataValue: any,
+ target: Object,
+ targetKey: string | symbol): void;
+ function hasMetadata(metadataKey: any, target: Object): boolean;
+ function hasMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
+ function hasOwnMetadata(metadataKey: any, target: Object): boolean;
+ function hasOwnMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
+ function getMetadata(metadataKey: any, target: Object): any;
+ function getMetadata(metadataKey: any, target: Object, targetKey: string | symbol): any;
+ function getOwnMetadata(metadataKey: any, target: Object): any;
+ function getOwnMetadata(metadataKey: any, target: Object, targetKey: string | symbol): any;
+ function getMetadataKeys(target: Object): any[];
+ function getMetadataKeys(target: Object, targetKey: string | symbol): any[];
+ function getOwnMetadataKeys(target: Object): any[];
+ function getOwnMetadataKeys(target: Object, targetKey: string | symbol): any[];
+ function deleteMetadata(metadataKey: any, target: Object): boolean;
+ function deleteMetadata(metadataKey: any, target: Object, targetKey: string | symbol): boolean;
+}
+
+
+// We need this here since there is a problem with Zone.js typings
+interface Thenable<T> {
+ then<U>(
+ onFulfilled?: (value: T) => U | Thenable<U>,
+ onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
+ then<U>(
+ onFulfilled?: (value: T) => U | Thenable<U>,
+ onRejected?: (error: any) => void): Thenable<U>;
+ catch<U>(onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
+}
--- /dev/null
+<html>
+ <head>
+ <base href="/">
+
+ <title>PeerTube</title>
+
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+
+ <link rel="icon" href="/client/assets/favicon.ico" />
+ </head>
+
+ <!-- 3. Display the application -->
+ <body>
+ <my-app>Loading...</my-app>
+ </body>
+</html>
--- /dev/null
+import { enableProdMode } from '@angular/core';
+import { bootstrap } from '@angular/platform-browser-dynamic';
+
+import { AppComponent } from './app/app.component';
+
+if (process.env.ENV === 'production') {
+ enableProdMode();
+}
+
+bootstrap(AppComponent);
--- /dev/null
+// Polyfills
+// (these modules are what are in 'angular2/bundles/angular2-polyfills' so don't use that here)
+
+// import 'ie-shim'; // Internet Explorer
+// import 'es6-shim';
+// import 'es6-promise';
+// import 'es7-reflect-metadata';
+
+// Prefer CoreJS over the polyfills above
+import 'core-js/es6';
+import 'core-js/es7/reflect';
+require('zone.js/dist/zone');
+
+// Typescript emit helpers polyfill
+import 'ts-helpers';
+
+if ('production' === ENV) {
+ // Production
+
+
+} else {
+ // Development
+
+ Error.stackTraceLimit = Infinity;
+
+ require('zone.js/dist/long-stack-trace-zone');
+
+}
--- /dev/null
+body {
+ padding: 20px;
+}
+
+footer {
+ border-top: 1px solid rgba(0, 0, 0, 0.2);
+ padding-top: 10px;
+ text-align: center;
+ font-size: small;
+ margin-top: 30px;
+}
--- /dev/null
+// $icon-font-path: "/assets/fonts/";
--- /dev/null
+// For vendors for example jQuery, Lodash, angular2-jwt just import them here unless you plan on
+// chunking vendors files for async loading. You would need to import the async loaded vendors
+// at the entry point of the async loaded file. Also see custom-typings.d.ts as you also need to
+// run `typings install x` where `x` is your module
+
+// Angular 2
+import '@angular/platform-browser';
+import '@angular/platform-browser-dynamic';
+import '@angular/core';
+import '@angular/common';
+import '@angular/http';
+import '@angular/router-deprecated';
+
+// RxJS
+import 'rxjs/add/operator/map';
+import 'rxjs/add/operator/mergeMap';
+
+import 'jquery';
+import 'bootstrap-loader';
+
+if ('production' === ENV) {
+ // Production
+
+
+} else {
+ // Development
+
+}
+++ /dev/null
-$icon-font-path: "/client/node_modules/bootstrap-sass/assets/fonts/bootstrap/";
-
-@import "bootstrap-variables";
-@import "_bootstrap";
-@import "base.scss";
+++ /dev/null
-body {
- padding: 20px;
-}
-
-footer {
- border-top: 1px solid rgba(0, 0, 0, 0.2);
- padding-top: 10px;
- text-align: center;
- font-size: small;
- margin-top: 30px;
-}
+++ /dev/null
-// Override Bootstrap variables here (defaults from bootstrap-sass v3.3.6):
-
-//
-// Variables
-// --------------------------------------------------
-
-
-//== Colors
-//
-//## Gray and brand colors for use across Bootstrap.
-
-// $gray-base: #000
-// $gray-darker: lighten($gray-base, 13.5%) // #222
-// $gray-dark: lighten($gray-base, 20%) // #333
-// $gray: lighten($gray-base, 33.5%) // #555
-// $gray-light: lighten($gray-base, 46.7%) // #777
-// $gray-lighter: lighten($gray-base, 93.5%) // #eee
-
-// $brand-primary: darken(#428bca, 6.5%) // #337ab7
-// $brand-success: #5cb85c
-// $brand-info: #5bc0de
-// $brand-warning: #f0ad4e
-// $brand-danger: #d9534f
-
-
-//== Scaffolding
-//
-//## Settings for some of the most global styles.
-
-//** Background color for `<body>`.
-// $body-bg: #fff
-//** Global text color on `<body>`.
-// $text-color: $gray-dark
-
-//** Global textual link color.
-// $link-color: $brand-primary
-//** Link hover color set via `darken()` function.
-// $link-hover-color: darken($link-color, 15%)
-//** Link hover decoration.
-// $link-hover-decoration: underline
-
-
-//== Typography
-//
-//## Font, line-height, and color for body text, headings, and more.
-
-// $font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif
-// $font-family-serif: Georgia, "Times New Roman", Times, serif
-//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
-// $font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace
-// $font-family-base: $font-family-sans-serif
-
-// $font-size-base: 14px
-// $font-size-large: ceil(($font-size-base * 1.25)) // ~18px
-// $font-size-small: ceil(($font-size-base * 0.85)) // ~12px
-
-// $font-size-h1: floor(($font-size-base * 2.6)) // ~36px
-// $font-size-h2: floor(($font-size-base * 2.15)) // ~30px
-// $font-size-h3: ceil(($font-size-base * 1.7)) // ~24px
-// $font-size-h4: ceil(($font-size-base * 1.25)) // ~18px
-// $font-size-h5: $font-size-base
-// $font-size-h6: ceil(($font-size-base * 0.85)) // ~12px
-
-//** Unit-less `line-height` for use in components like buttons.
-// $line-height-base: 1.428571429 // 20/14
-//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
-// $line-height-computed: floor(($font-size-base * $line-height-base)) // ~20px
-
-//** By default, this inherits from the `<body>`.
-// $headings-font-family: inherit
-// $headings-font-weight: 500
-// $headings-line-height: 1.1
-// $headings-color: inherit
-
-
-//== Iconography
-//
-//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
-
-//** Load fonts from this directory.
-
-// [converter] If $bootstrap-sass-asset-helper if used, provide path relative to the assets load path.
-// [converter] This is because some asset helpers, such as Sprockets, do not work with file-relative paths.
-// $icon-font-path: if($bootstrap-sass-asset-helper, "bootstrap/", "../fonts/bootstrap/")
-
-//** File name for all font files.
-// $icon-font-name: "glyphicons-halflings-regular"
-//** Element ID within SVG icon file.
-// $icon-font-svg-id: "glyphicons_halflingsregular"
-
-
-//== Components
-//
-//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
-
-// $padding-base-vertical: 6px
-// $padding-base-horizontal: 12px
-
-// $padding-large-vertical: 10px
-// $padding-large-horizontal: 16px
-
-// $padding-small-vertical: 5px
-// $padding-small-horizontal: 10px
-
-// $padding-xs-vertical: 1px
-// $padding-xs-horizontal: 5px
-
-// $line-height-large: 1.3333333 // extra decimals for Win 8.1 Chrome
-// $line-height-small: 1.5
-
-// $border-radius-base: 0;
-// $border-radius-large: 0;
-// $border-radius-small: 0;
-
-//** Global color for active items (e.g., navs or dropdowns).
-// $component-active-color: #fff
-//** Global background color for active items (e.g., navs or dropdowns).
-// $component-active-bg: $brand-primary
-
-//** Width of the `border` for generating carets that indicator dropdowns.
-// $caret-width-base: 4px
-//** Carets increase slightly in size for larger components.
-// $caret-width-large: 5px
-
-
-//== Tables
-//
-//## Customizes the `.table` component with basic values, each used across all table variations.
-
-//** Padding for `<th>`s and `<td>`s.
-// $table-cell-padding: 8px
-//** Padding for cells in `.table-condensed`.
-// $table-condensed-cell-padding: 5px
-
-//** Default background color used for all tables.
-// $table-bg: transparent
-//** Background color used for `.table-striped`.
-// $table-bg-accent: #f9f9f9
-//** Background color used for `.table-hover`.
-// $table-bg-hover: #f5f5f5
-// $table-bg-active: $table-bg-hover
-
-//** Border color for table and cell borders.
-// $table-border-color: #ddd
-
-
-//== Buttons
-//
-//## For each of Bootstrap's buttons, define text, background and border color.
-
-// $btn-font-weight: normal
-
-// $btn-default-color: #333
-// $btn-default-bg: #fff
-// $btn-default-border: #ccc
-
-// $btn-primary-color: #fff
-// $btn-primary-bg: $brand-primary
-// $btn-primary-border: darken($btn-primary-bg, 5%)
-
-// $btn-success-color: #fff
-// $btn-success-bg: $brand-success
-// $btn-success-border: darken($btn-success-bg, 5%)
-
-// $btn-info-color: #fff
-// $btn-info-bg: $brand-info
-// $btn-info-border: darken($btn-info-bg, 5%)
-
-// $btn-warning-color: #fff
-// $btn-warning-bg: $brand-warning
-// $btn-warning-border: darken($btn-warning-bg, 5%)
-
-// $btn-danger-color: #fff
-// $btn-danger-bg: $brand-danger
-// $btn-danger-border: darken($btn-danger-bg, 5%)
-
-// $btn-link-disabled-color: $gray-light
-
-// Allows for customizing button radius independently from global border radius
-// $btn-border-radius-base: $border-radius-base
-// $btn-border-radius-large: $border-radius-large
-// $btn-border-radius-small: $border-radius-small
-
-
-//== Forms
-//
-//##
-
-//** `<input>` background color
-// $input-bg: #fff
-//** `<input disabled>` background color
-// $input-bg-disabled: $gray-lighter
-
-//** Text color for `<input>`s
-// $input-color: $gray
-//** `<input>` border color
-// $input-border: #ccc
-
-// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4
-//** Default `.form-control` border radius
-// This has no effect on `<select>`s in some browsers, due to the limited stylability of `<select>`s in CSS.
-// $input-border-radius: $border-radius-base
-//** Large `.form-control` border radius
-// $input-border-radius-large: $border-radius-large
-//** Small `.form-control` border radius
-// $input-border-radius-small: $border-radius-small
-
-//** Border color for inputs on focus
-// $input-border-focus: #66afe9
-
-//** Placeholder text color
-// $input-color-placeholder: #999
-
-//** Default `.form-control` height
-// $input-height-base: ($line-height-computed + ($padding-base-vertical * 2) + 2)
-//** Large `.form-control` height
-// $input-height-large: (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2)
-//** Small `.form-control` height
-// $input-height-small: (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2)
-
-//** `.form-group` margin
-// $form-group-margin-bottom: 15px
-
-// $legend-color: $gray-dark
-// $legend-border-color: #e5e5e5
-
-//** Background color for textual input addons
-// $input-group-addon-bg: $gray-lighter
-//** Border color for textual input addons
-// $input-group-addon-border-color: $input-border
-
-//** Disabled cursor for form controls and buttons.
-// $cursor-disabled: not-allowed
-
-
-//== Dropdowns
-//
-//## Dropdown menu container and contents.
-
-//** Background for the dropdown menu.
-// $dropdown-bg: #fff
-//** Dropdown menu `border-color`.
-// $dropdown-border: rgba(0,0,0,.15)
-//** Dropdown menu `border-color` **for IE8**.
-// $dropdown-fallback-border: #ccc
-//** Divider color for between dropdown items.
-// $dropdown-divider-bg: #e5e5e5
-
-//** Dropdown link text color.
-// $dropdown-link-color: $gray-dark
-//** Hover color for dropdown links.
-// $dropdown-link-hover-color: darken($gray-dark, 5%)
-//** Hover background for dropdown links.
-// $dropdown-link-hover-bg: #f5f5f5
-
-//** Active dropdown menu item text color.
-// $dropdown-link-active-color: $component-active-color
-//** Active dropdown menu item background color.
-// $dropdown-link-active-bg: $component-active-bg
-
-//** Disabled dropdown menu item background color.
-// $dropdown-link-disabled-color: $gray-light
-
-//** Text color for headers within dropdown menus.
-// $dropdown-header-color: $gray-light
-
-//** Deprecated `$dropdown-caret-color` as of v3.1.0
-// $dropdown-caret-color: #000
-
-
-//-- Z-index master list
-//
-// Warning: Avoid customizing these values. They're used for a bird's eye view
-// of components dependent on the z-axis and are designed to all work together.
-//
-// Note: These variables are not generated into the Customizer.
-
-// $zindex-navbar: 1000
-// $zindex-dropdown: 1000
-// $zindex-popover: 1060
-// $zindex-tooltip: 1070
-// $zindex-navbar-fixed: 1030
-// $zindex-modal-background: 1040
-// $zindex-modal: 1050
-
-
-//== Media queries breakpoints
-//
-//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
-
-// Extra small screen / phone
-//** Deprecated `$screen-xs` as of v3.0.1
-// $screen-xs: 480px
-//** Deprecated `$screen-xs-min` as of v3.2.0
-// $screen-xs-min: $screen-xs
-//** Deprecated `$screen-phone` as of v3.0.1
-// $screen-phone: $screen-xs-min
-
-// Small screen / tablet
-//** Deprecated `$screen-sm` as of v3.0.1
-// $screen-sm: 768px
-// $screen-sm-min: $screen-sm
-//** Deprecated `$screen-tablet` as of v3.0.1
-// $screen-tablet: $screen-sm-min
-
-// Medium screen / desktop
-//** Deprecated `$screen-md` as of v3.0.1
-// $screen-md: 992px
-// $screen-md-min: $screen-md
-//** Deprecated `$screen-desktop` as of v3.0.1
-// $screen-desktop: $screen-md-min
-
-// Large screen / wide desktop
-//** Deprecated `$screen-lg` as of v3.0.1
-// $screen-lg: 1200px
-// $screen-lg-min: $screen-lg
-//** Deprecated `$screen-lg-desktop` as of v3.0.1
-// $screen-lg-desktop: $screen-lg-min
-
-// So media queries don't overlap when required, provide a maximum
-// $screen-xs-max: ($screen-sm-min - 1)
-// $screen-sm-max: ($screen-md-min - 1)
-// $screen-md-max: ($screen-lg-min - 1)
-
-
-//== Grid system
-//
-//## Define your custom responsive grid.
-
-//** Number of columns in the grid.
-// $grid-columns: 12
-//** Padding between columns. Gets divided in half for the left and right.
-// $grid-gutter-width: 30px
-// Navbar collapse
-//** Point at which the navbar becomes uncollapsed.
-// $grid-float-breakpoint: $screen-sm-min
-//** Point at which the navbar begins collapsing.
-// $grid-float-breakpoint-max: ($grid-float-breakpoint - 1)
-
-
-//== Container sizes
-//
-//## Define the maximum width of `.container` for different screen sizes.
-
-// Small screen / tablet
-// $container-tablet: (720px + $grid-gutter-width)
-//** For `$screen-sm-min` and up.
-// $container-sm: $container-tablet
-
-// Medium screen / desktop
-// $container-desktop: (940px + $grid-gutter-width)
-//** For `$screen-md-min` and up.
-// $container-md: $container-desktop
-
-// Large screen / wide desktop
-// $container-large-desktop: (1140px + $grid-gutter-width)
-//** For `$screen-lg-min` and up.
-// $container-lg: $container-large-desktop
-
-
-//== Navbar
-//
-//##
-
-// Basics of a navbar
-// $navbar-height: 50px
-// $navbar-margin-bottom: $line-height-computed
-// $navbar-border-radius: $border-radius-base
-// $navbar-padding-horizontal: floor(($grid-gutter-width / 2))
-// $navbar-padding-vertical: (($navbar-height - $line-height-computed) / 2)
-// $navbar-collapse-max-height: 340px
-
-// $navbar-default-color: #777
-// $navbar-default-bg: #f8f8f8
-// $navbar-default-border: darken($navbar-default-bg, 6.5%)
-
-// Navbar links
-// $navbar-default-link-color: #777
-// $navbar-default-link-hover-color: #333
-// $navbar-default-link-hover-bg: transparent
-// $navbar-default-link-active-color: #555
-// $navbar-default-link-active-bg: darken($navbar-default-bg, 6.5%)
-// $navbar-default-link-disabled-color: #ccc
-// $navbar-default-link-disabled-bg: transparent
-
-// Navbar brand label
-// $navbar-default-brand-color: $navbar-default-link-color
-// $navbar-default-brand-hover-color: darken($navbar-default-brand-color, 10%)
-// $navbar-default-brand-hover-bg: transparent
-
-// Navbar toggle
-// $navbar-default-toggle-hover-bg: #ddd
-// $navbar-default-toggle-icon-bar-bg: #888
-// $navbar-default-toggle-border-color: #ddd
-
-
-//=== Inverted navbar
-// Reset inverted navbar basics
-// $navbar-inverse-color: lighten($gray-light, 15%)
-// $navbar-inverse-bg: #222
-// $navbar-inverse-border: darken($navbar-inverse-bg, 10%)
-
-// Inverted navbar links
-// $navbar-inverse-link-color: lighten($gray-light, 15%)
-// $navbar-inverse-link-hover-color: #fff
-// $navbar-inverse-link-hover-bg: transparent
-// $navbar-inverse-link-active-color: $navbar-inverse-link-hover-color
-// $navbar-inverse-link-active-bg: darken($navbar-inverse-bg, 10%)
-// $navbar-inverse-link-disabled-color: #444
-// $navbar-inverse-link-disabled-bg: transparent
-
-// Inverted navbar brand label
-// $navbar-inverse-brand-color: $navbar-inverse-link-color
-// $navbar-inverse-brand-hover-color: #fff
-// $navbar-inverse-brand-hover-bg: transparent
-
-// Inverted navbar toggle
-// $navbar-inverse-toggle-hover-bg: #333
-// $navbar-inverse-toggle-icon-bar-bg: #fff
-// $navbar-inverse-toggle-border-color: #333
-
-
-//== Navs
-//
-//##
-
-//=== Shared nav styles
-// $nav-link-padding: 10px 15px
-// $nav-link-hover-bg: $gray-lighter
-
-// $nav-disabled-link-color: $gray-light
-// $nav-disabled-link-hover-color: $gray-light
-
-//== Tabs
-// $nav-tabs-border-color: #ddd
-
-// $nav-tabs-link-hover-border-color: $gray-lighter
-
-// $nav-tabs-active-link-hover-bg: $body-bg
-// $nav-tabs-active-link-hover-color: $gray
-// $nav-tabs-active-link-hover-border-color: #ddd
-
-// $nav-tabs-justified-link-border-color: #ddd
-// $nav-tabs-justified-active-link-border-color: $body-bg
-
-//== Pills
-// $nav-pills-border-radius: $border-radius-base
-// $nav-pills-active-link-hover-bg: $component-active-bg
-// $nav-pills-active-link-hover-color: $component-active-color
-
-
-//== Pagination
-//
-//##
-
-// $pagination-color: $link-color
-// $pagination-bg: #fff
-// $pagination-border: #ddd
-
-// $pagination-hover-color: $link-hover-color
-// $pagination-hover-bg: $gray-lighter
-// $pagination-hover-border: #ddd
-
-// $pagination-active-color: #fff
-// $pagination-active-bg: $brand-primary
-// $pagination-active-border: $brand-primary
-
-// $pagination-disabled-color: $gray-light
-// $pagination-disabled-bg: #fff
-// $pagination-disabled-border: #ddd
-
-
-//== Pager
-//
-//##
-
-// $pager-bg: $pagination-bg
-// $pager-border: $pagination-border
-// $pager-border-radius: 15px
-
-// $pager-hover-bg: $pagination-hover-bg
-
-// $pager-active-bg: $pagination-active-bg
-// $pager-active-color: $pagination-active-color
-
-// $pager-disabled-color: $pagination-disabled-color
-
-
-//== Jumbotron
-//
-//##
-
-// $jumbotron-padding: 30px
-// $jumbotron-color: inherit
-// $jumbotron-bg: $gray-lighter
-// $jumbotron-heading-color: inherit
-// $jumbotron-font-size: ceil(($font-size-base * 1.5))
-// $jumbotron-heading-font-size: ceil(($font-size-base * 4.5))
-
-
-//== Form states and alerts
-//
-//## Define colors for form feedback states and, by default, alerts.
-
-// $state-success-text: #3c763d
-// $state-success-bg: #dff0d8
-// $state-success-border: darken(adjust-hue($state-success-bg, -10), 5%)
-
-// $state-info-text: #31708f
-// $state-info-bg: #d9edf7
-// $state-info-border: darken(adjust-hue($state-info-bg, -10), 7%)
-
-// $state-warning-text: #8a6d3b
-// $state-warning-bg: #fcf8e3
-// $state-warning-border: darken(adjust-hue($state-warning-bg, -10), 5%)
-
-// $state-danger-text: #a94442
-// $state-danger-bg: #f2dede
-// $state-danger-border: darken(adjust-hue($state-danger-bg, -10), 5%)
-
-
-//== Tooltips
-//
-//##
-
-//** Tooltip max width
-// $tooltip-max-width: 200px
-//** Tooltip text color
-// $tooltip-color: #fff
-//** Tooltip background color
-// $tooltip-bg: #000
-// $tooltip-opacity: .9
-
-//** Tooltip arrow width
-// $tooltip-arrow-width: 5px
-//** Tooltip arrow color
-// $tooltip-arrow-color: $tooltip-bg
-
-
-//== Popovers
-//
-//##
-
-//** Popover body background color
-// $popover-bg: #fff
-//** Popover maximum width
-// $popover-max-width: 276px
-//** Popover border color
-// $popover-border-color: rgba(0,0,0,.2)
-//** Popover fallback border color
-// $popover-fallback-border-color: #ccc
-
-//** Popover title background color
-// $popover-title-bg: darken($popover-bg, 3%)
-
-//** Popover arrow width
-// $popover-arrow-width: 10px
-//** Popover arrow color
-// $popover-arrow-color: $popover-bg
-
-//** Popover outer arrow width
-// $popover-arrow-outer-width: ($popover-arrow-width + 1)
-//** Popover outer arrow color
-// $popover-arrow-outer-color: fade_in($popover-border-color, 0.05)
-//** Popover outer arrow fallback color
-// $popover-arrow-outer-fallback-color: darken($popover-fallback-border-color, 20%)
-
-
-//== Labels
-//
-//##
-
-//** Default label background color
-// $label-default-bg: $gray-light
-//** Primary label background color
-// $label-primary-bg: $brand-primary
-//** Success label background color
-// $label-success-bg: $brand-success
-//** Info label background color
-// $label-info-bg: $brand-info
-//** Warning label background color
-// $label-warning-bg: $brand-warning
-//** Danger label background color
-// $label-danger-bg: $brand-danger
-
-//** Default label text color
-// $label-color: #fff
-//** Default text color of a linked label
-// $label-link-hover-color: #fff
-
-
-//== Modals
-//
-//##
-
-//** Padding applied to the modal body
-// $modal-inner-padding: 15px
-
-//** Padding applied to the modal title
-// $modal-title-padding: 15px
-//** Modal title line-height
-// $modal-title-line-height: $line-height-base
-
-//** Background color of modal content area
-// $modal-content-bg: #fff
-//** Modal content border color
-// $modal-content-border-color: rgba(0,0,0,.2)
-//** Modal content border color **for IE8**
-// $modal-content-fallback-border-color: #999
-
-//** Modal backdrop background color
-// $modal-backdrop-bg: #000
-//** Modal backdrop opacity
-// $modal-backdrop-opacity: .5
-//** Modal header border color
-// $modal-header-border-color: #e5e5e5
-//** Modal footer border color
-// $modal-footer-border-color: $modal-header-border-color
-
-// $modal-lg: 900px
-// $modal-md: 600px
-// $modal-sm: 300px
-
-
-//== Alerts
-//
-//## Define alert colors, border radius, and padding.
-
-// $alert-padding: 15px
-// $alert-border-radius: $border-radius-base
-// $alert-link-font-weight: bold
-
-// $alert-success-bg: $state-success-bg
-// $alert-success-text: $state-success-text
-// $alert-success-border: $state-success-border
-
-// $alert-info-bg: $state-info-bg
-// $alert-info-text: $state-info-text
-// $alert-info-border: $state-info-border
-
-// $alert-warning-bg: $state-warning-bg
-// $alert-warning-text: $state-warning-text
-// $alert-warning-border: $state-warning-border
-
-// $alert-danger-bg: $state-danger-bg
-// $alert-danger-text: $state-danger-text
-// $alert-danger-border: $state-danger-border
-
-
-//== Progress bars
-//
-//##
-
-//** Background color of the whole progress component
-// $progress-bg: #f5f5f5
-//** Progress bar text color
-// $progress-bar-color: #fff
-//** Variable for setting rounded corners on progress bar.
-// $progress-border-radius: $border-radius-base
-
-//** Default progress bar color
-// $progress-bar-bg: $brand-primary
-//** Success progress bar color
-// $progress-bar-success-bg: $brand-success
-//** Warning progress bar color
-// $progress-bar-warning-bg: $brand-warning
-//** Danger progress bar color
-// $progress-bar-danger-bg: $brand-danger
-//** Info progress bar color
-// $progress-bar-info-bg: $brand-info
-
-
-//== List group
-//
-//##
-
-//** Background color on `.list-group-item`
-// $list-group-bg: #fff
-//** `.list-group-item` border color
-// $list-group-border: #ddd
-//** List group border radius
-// $list-group-border-radius: $border-radius-base
-
-//** Background color of single list items on hover
-// $list-group-hover-bg: #f5f5f5
-//** Text color of active list items
-// $list-group-active-color: $component-active-color
-//** Background color of active list items
-// $list-group-active-bg: $component-active-bg
-//** Border color of active list elements
-// $list-group-active-border: $list-group-active-bg
-//** Text color for content within active list items
-// $list-group-active-text-color: lighten($list-group-active-bg, 40%)
-
-//** Text color of disabled list items
-// $list-group-disabled-color: $gray-light
-//** Background color of disabled list items
-// $list-group-disabled-bg: $gray-lighter
-//** Text color for content within disabled list items
-// $list-group-disabled-text-color: $list-group-disabled-color
-
-// $list-group-link-color: #555
-// $list-group-link-hover-color: $list-group-link-color
-// $list-group-link-heading-color: #333
-
-
-//== Panels
-//
-//##
-
-// $panel-bg: #fff
-// $panel-body-padding: 15px
-// $panel-heading-padding: 10px 15px
-// $panel-footer-padding: $panel-heading-padding
-// $panel-border-radius: $border-radius-base
-
-//** Border color for elements within panels
-// $panel-inner-border: #ddd
-// $panel-footer-bg: #f5f5f5
-
-// $panel-default-text: $gray-dark
-// $panel-default-border: #ddd
-// $panel-default-heading-bg: #f5f5f5
-
-// $panel-primary-text: #fff
-// $panel-primary-border: $brand-primary
-// $panel-primary-heading-bg: $brand-primary
-
-// $panel-success-text: $state-success-text
-// $panel-success-border: $state-success-border
-// $panel-success-heading-bg: $state-success-bg
-
-// $panel-info-text: $state-info-text
-// $panel-info-border: $state-info-border
-// $panel-info-heading-bg: $state-info-bg
-
-// $panel-warning-text: $state-warning-text
-// $panel-warning-border: $state-warning-border
-// $panel-warning-heading-bg: $state-warning-bg
-
-// $panel-danger-text: $state-danger-text
-// $panel-danger-border: $state-danger-border
-// $panel-danger-heading-bg: $state-danger-bg
-
-
-//== Thumbnails
-//
-//##
-
-//** Padding around the thumbnail image
-// $thumbnail-padding: 4px
-//** Thumbnail background color
-// $thumbnail-bg: $body-bg
-//** Thumbnail border color
-// $thumbnail-border: #ddd
-//** Thumbnail border radius
-// $thumbnail-border-radius: $border-radius-base
-
-//** Custom text color for thumbnail captions
-// $thumbnail-caption-color: $text-color
-//** Padding around the thumbnail caption
-// $thumbnail-caption-padding: 9px
-
-
-//== Wells
-//
-//##
-
-// $well-bg: #f5f5f5
-// $well-border: darken($well-bg, 7%)
-
-
-//== Badges
-//
-//##
-
-// $badge-color: #fff
-//** Linked badge text color on hover
-// $badge-link-hover-color: #fff
-// $badge-bg: $gray-light
-
-//** Badge text color in active nav link
-// $badge-active-color: $link-color
-//** Badge background color in active nav link
-// $badge-active-bg: #fff
-
-// $badge-font-weight: bold
-// $badge-line-height: 1
-// $badge-border-radius: 10px
-
-
-//== Breadcrumbs
-//
-//##
-
-// $breadcrumb-padding-vertical: 8px
-// $breadcrumb-padding-horizontal: 15px
-//** Breadcrumb background color
-// $breadcrumb-bg: #f5f5f5
-//** Breadcrumb text color
-// $breadcrumb-color: #ccc
-//** Text color of current page in the breadcrumb
-// $breadcrumb-active-color: $gray-light
-//** Textual separator for between breadcrumb elements
-// $breadcrumb-separator: "/"
-
-
-//== Carousel
-//
-//##
-
-// $carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6)
-
-// $carousel-control-color: #fff
-// $carousel-control-width: 15%
-// $carousel-control-opacity: .5
-// $carousel-control-font-size: 20px
-
-// $carousel-indicator-active-bg: #fff
-// $carousel-indicator-border-color: #fff
-
-// $carousel-caption-color: #fff
-
-
-//== Close
-//
-//##
-
-// $close-font-weight: bold
-// $close-color: #000
-// $close-text-shadow: 0 1px 0 #fff
-
-
-//== Code
-//
-//##
-
-// $code-color: #c7254e
-// $code-bg: #f9f2f4
-
-// $kbd-color: #fff
-// $kbd-bg: #333
-
-// $pre-bg: #f5f5f5
-// $pre-color: $gray-dark
-// $pre-border-color: #ccc
-// $pre-scrollable-max-height: 340px
-
-
-//== Type
-//
-//##
-
-//** Horizontal offset for forms and lists.
-// $component-offset-horizontal: 180px
-//** Text muted color
-// $text-muted: $gray-light
-//** Abbreviations and acronyms border color
-// $abbr-border-color: $gray-light
-//** Headings small color
-// $headings-small-color: $gray-light
-//** Blockquote small color
-// $blockquote-small-color: $gray-light
-//** Blockquote font size
-// $blockquote-font-size: ($font-size-base * 1.25)
-//** Blockquote border color
-// $blockquote-border-color: $gray-lighter
-//** Page header border color
-// $page-header-border-color: $gray-lighter
-//** Width of horizontal description list titles
-// $dl-horizontal-offset: $component-offset-horizontal
-//** Point at which .dl-horizontal becomes horizontal
-// $dl-horizontal-breakpoint: $grid-float-breakpoint
-//** Horizontal line color.
-// $hr-border: $gray-lighter
+++ /dev/null
-var SystemBuilder = require('systemjs-builder')
-var builder = new SystemBuilder('node_modules', 'systemjs.config.js')
-
-var toBundle = [
- 'rxjs/Rx',
- '@angular/common',
- '@angular/compiler',
- '@angular/core',
- '@angular/http',
- '@angular/platform-browser',
- '@angular/platform-browser-dynamic',
- '@angular/router-deprecated'
-]
-
-builder.bundle(toBundle.join(' + '), 'bundles/angular-rxjs.bundle.js')
+++ /dev/null
-;(function (global) {
- var map = {
- '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 = {
- 'client': { main: 'main.js', defaultExtension: 'js' },
- 'ng2-bootstrap': { defaultExtension: 'js' },
- 'rxjs': { defaultExtension: 'js' }
- }
- var packageNames = [
- '@angular/common',
- '@angular/compiler',
- '@angular/core',
- '@angular/http',
- '@angular/platform-browser',
- '@angular/platform-browser-dynamic',
- '@angular/router-deprecated',
- 'angular-pipes'
- ]
-
- packageNames.forEach(function (pkgName) {
- packages[pkgName] = { main: 'index.js', defaultExtension: 'js' }
- })
-
- var config = {
- map: map,
- packages: packages,
- bundles: {
- 'angular-rxjs.bundle': [
- 'rxjs/Rx.js',
- '@angular/common/index.js',
- '@angular/compiler/index.js',
- '@angular/core/index.js',
- '@angular/http/index.js',
- '@angular/platform-browser/index.js',
- '@angular/platform-browser-dynamic/index.js',
- '@angular/router-deprecated/index.js'
- ]
- }
- }
-
- // filterSystemConfig - index.html's chance to modify config before we register it.
- if (global.filterSystemConfig) global.filterSystemConfig(config)
- System.config(config)
-})(this)
],
"compileOnSave": false,
"files": [
- "app/app.component.ts",
- "app/friends/friend.service.ts",
- "app/friends/index.ts",
- "app/login/index.ts",
- "app/login/login.component.ts",
- "app/shared/index.ts",
- "app/shared/search/index.ts",
- "app/shared/search/search-field.type.ts",
- "app/shared/search/search.component.ts",
- "app/shared/search/search.model.ts",
- "app/shared/users/auth-status.model.ts",
- "app/shared/users/auth.service.ts",
- "app/shared/users/index.ts",
- "app/shared/users/token.model.ts",
- "app/shared/users/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",
- "app/videos/video-watch/webtorrent.service.ts",
- "main.ts",
+ "src/app/app.component.ts",
+ "src/app/friends/friend.service.ts",
+ "src/app/friends/index.ts",
+ "src/app/login/index.ts",
+ "src/app/login/login.component.ts",
+ "src/app/shared/index.ts",
+ "src/app/shared/search/index.ts",
+ "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/users/auth-status.model.ts",
+ "src/app/shared/users/auth.service.ts",
+ "src/app/shared/users/index.ts",
+ "src/app/shared/users/token.model.ts",
+ "src/app/shared/users/user.model.ts",
+ "src/app/videos/index.ts",
+ "src/app/videos/shared/index.ts",
+ "src/app/videos/shared/loader/index.ts",
+ "src/app/videos/shared/loader/loader.component.ts",
+ "src/app/videos/shared/pagination.model.ts",
+ "src/app/videos/shared/sort-field.type.ts",
+ "src/app/videos/shared/video.model.ts",
+ "src/app/videos/shared/video.service.ts",
+ "src/app/videos/video-add/index.ts",
+ "src/app/videos/video-add/video-add.component.ts",
+ "src/app/videos/video-list/index.ts",
+ "src/app/videos/video-list/video-list.component.ts",
+ "src/app/videos/video-list/video-miniature.component.ts",
+ "src/app/videos/video-list/video-sort.component.ts",
+ "src/app/videos/video-watch/index.ts",
+ "src/app/videos/video-watch/video-watch.component.ts",
+ "src/app/videos/video-watch/webtorrent.service.ts",
+ "src/custom-typings.d.ts",
+ "src/main.ts",
+ "src/polyfills.ts",
+ "src/vendor.ts",
"typings/globals/es6-shim/index.d.ts",
"typings/globals/jasmine/index.d.ts",
"typings/globals/jquery.fileupload/index.d.ts",
--- /dev/null
+switch (process.env.NODE_ENV) {
+ case 'prod':
+ case 'production':
+ module.exports = require('./config/webpack.prod')
+ break
+
+ case 'test':
+ case 'testing':
+ module.exports = require('./config/webpack.test')
+ break
+
+ case 'dev':
+ case 'development':
+ default:
+ module.exports = require('./config/webpack.dev')
+}
app.use(apiRoute, routes.api)
// Static files
-app.use('/client', express.static(path.join(__dirname, '/client'), { maxAge: 0 }))
+app.use('/client', express.static(path.join(__dirname, '/client/dist'), { maxAge: 0 }))
// 404 for static files not found
app.use('/client/*', function (req, res, next) {
res.sendStatus(404)
// Client application
app.use('/*', function (req, res, next) {
- res.sendFile(path.join(__dirname, 'client/index.html'))
+ res.sendFile(path.join(__dirname, 'client/dist/index.html'))
})
// ----------- Tracker -----------