First draft to use webpack instead of systemjs
authorChocobozzz <florian.bigard@gmail.com>
Fri, 3 Jun 2016 20:08:03 +0000 (22:08 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Fri, 3 Jun 2016 20:08:03 +0000 (22:08 +0200)
121 files changed:
client/.bootstraprc [new file with mode: 0644]
client/.gitignore
client/app/app.component.html [deleted file]
client/app/app.component.scss [deleted file]
client/app/app.component.ts [deleted file]
client/app/friends/friend.service.ts [deleted file]
client/app/friends/index.ts [deleted file]
client/app/login/index.ts [deleted file]
client/app/login/login.component.html [deleted file]
client/app/login/login.component.ts [deleted file]
client/app/shared/index.ts [deleted file]
client/app/shared/search/index.ts [deleted file]
client/app/shared/search/search-field.type.ts [deleted file]
client/app/shared/search/search.component.html [deleted file]
client/app/shared/search/search.component.ts [deleted file]
client/app/shared/search/search.model.ts [deleted file]
client/app/shared/users/auth-status.model.ts [deleted file]
client/app/shared/users/auth.service.ts [deleted file]
client/app/shared/users/index.ts [deleted file]
client/app/shared/users/token.model.ts [deleted file]
client/app/shared/users/user.model.ts [deleted file]
client/app/videos/index.ts [deleted file]
client/app/videos/shared/index.ts [deleted file]
client/app/videos/shared/loader/index.ts [deleted file]
client/app/videos/shared/loader/loader.component.html [deleted file]
client/app/videos/shared/loader/loader.component.scss [deleted file]
client/app/videos/shared/loader/loader.component.ts [deleted file]
client/app/videos/shared/pagination.model.ts [deleted file]
client/app/videos/shared/sort-field.type.ts [deleted file]
client/app/videos/shared/video.model.ts [deleted file]
client/app/videos/shared/video.service.ts [deleted file]
client/app/videos/video-add/index.ts [deleted file]
client/app/videos/video-add/video-add.component.html [deleted file]
client/app/videos/video-add/video-add.component.scss [deleted file]
client/app/videos/video-add/video-add.component.ts [deleted file]
client/app/videos/video-list/index.ts [deleted file]
client/app/videos/video-list/video-list.component.html [deleted file]
client/app/videos/video-list/video-list.component.scss [deleted file]
client/app/videos/video-list/video-list.component.ts [deleted file]
client/app/videos/video-list/video-miniature.component.html [deleted file]
client/app/videos/video-list/video-miniature.component.scss [deleted file]
client/app/videos/video-list/video-miniature.component.ts [deleted file]
client/app/videos/video-list/video-sort.component.html [deleted file]
client/app/videos/video-list/video-sort.component.ts [deleted file]
client/app/videos/video-watch/index.ts [deleted file]
client/app/videos/video-watch/video-watch.component.html [deleted file]
client/app/videos/video-watch/video-watch.component.scss [deleted file]
client/app/videos/video-watch/video-watch.component.ts [deleted file]
client/app/videos/video-watch/webtorrent.service.ts [deleted file]
client/config/helpers.js [new file with mode: 0644]
client/config/webpack.common.js [new file with mode: 0644]
client/config/webpack.dev.js [new file with mode: 0644]
client/config/webpack.prod.js [new file with mode: 0644]
client/images/favicon.png [deleted file]
client/images/loading.gif [deleted file]
client/index.html [deleted file]
client/main.ts [deleted file]
client/package.json
client/src/app/app.component.html [new file with mode: 0644]
client/src/app/app.component.scss [new file with mode: 0644]
client/src/app/app.component.ts [new file with mode: 0644]
client/src/app/friends/friend.service.ts [new file with mode: 0644]
client/src/app/friends/index.ts [new file with mode: 0644]
client/src/app/login/index.ts [new file with mode: 0644]
client/src/app/login/login.component.html [new file with mode: 0644]
client/src/app/login/login.component.ts [new file with mode: 0644]
client/src/app/shared/index.ts [new file with mode: 0644]
client/src/app/shared/search/index.ts [new file with mode: 0644]
client/src/app/shared/search/search-field.type.ts [new file with mode: 0644]
client/src/app/shared/search/search.component.html [new file with mode: 0644]
client/src/app/shared/search/search.component.ts [new file with mode: 0644]
client/src/app/shared/search/search.model.ts [new file with mode: 0644]
client/src/app/shared/users/auth-status.model.ts [new file with mode: 0644]
client/src/app/shared/users/auth.service.ts [new file with mode: 0644]
client/src/app/shared/users/index.ts [new file with mode: 0644]
client/src/app/shared/users/token.model.ts [new file with mode: 0644]
client/src/app/shared/users/user.model.ts [new file with mode: 0644]
client/src/app/videos/index.ts [new file with mode: 0644]
client/src/app/videos/shared/index.ts [new file with mode: 0644]
client/src/app/videos/shared/loader/index.ts [new file with mode: 0644]
client/src/app/videos/shared/loader/loader.component.html [new file with mode: 0644]
client/src/app/videos/shared/loader/loader.component.scss [new file with mode: 0644]
client/src/app/videos/shared/loader/loader.component.ts [new file with mode: 0644]
client/src/app/videos/shared/pagination.model.ts [new file with mode: 0644]
client/src/app/videos/shared/sort-field.type.ts [new file with mode: 0644]
client/src/app/videos/shared/video.model.ts [new file with mode: 0644]
client/src/app/videos/shared/video.service.ts [new file with mode: 0644]
client/src/app/videos/video-add/index.ts [new file with mode: 0644]
client/src/app/videos/video-add/video-add.component.html [new file with mode: 0644]
client/src/app/videos/video-add/video-add.component.scss [new file with mode: 0644]
client/src/app/videos/video-add/video-add.component.ts [new file with mode: 0644]
client/src/app/videos/video-list/index.ts [new file with mode: 0644]
client/src/app/videos/video-list/video-list.component.html [new file with mode: 0644]
client/src/app/videos/video-list/video-list.component.scss [new file with mode: 0644]
client/src/app/videos/video-list/video-list.component.ts [new file with mode: 0644]
client/src/app/videos/video-list/video-miniature.component.html [new file with mode: 0644]
client/src/app/videos/video-list/video-miniature.component.scss [new file with mode: 0644]
client/src/app/videos/video-list/video-miniature.component.ts [new file with mode: 0644]
client/src/app/videos/video-list/video-sort.component.html [new file with mode: 0644]
client/src/app/videos/video-list/video-sort.component.ts [new file with mode: 0644]
client/src/app/videos/video-watch/index.ts [new file with mode: 0644]
client/src/app/videos/video-watch/video-watch.component.html [new file with mode: 0644]
client/src/app/videos/video-watch/video-watch.component.scss [new file with mode: 0644]
client/src/app/videos/video-watch/video-watch.component.ts [new file with mode: 0644]
client/src/app/videos/video-watch/webtorrent.service.ts [new file with mode: 0644]
client/src/assets/favicon.png [new file with mode: 0644]
client/src/custom-typings.d.ts [new file with mode: 0644]
client/src/index.html [new file with mode: 0644]
client/src/main.ts [new file with mode: 0644]
client/src/polyfills.ts [new file with mode: 0644]
client/src/sass/application.scss [new file with mode: 0644]
client/src/sass/pre-customizations.scss [new file with mode: 0644]
client/src/vendor.ts [new file with mode: 0644]
client/stylesheets/application.scss [deleted file]
client/stylesheets/base.scss [deleted file]
client/stylesheets/bootstrap-variables.scss [deleted file]
client/systemjs.bundle.js [deleted file]
client/systemjs.config.js [deleted file]
client/tsconfig.json
client/webpack.config.js [new file with mode: 0644]
server.js

diff --git a/client/.bootstraprc b/client/.bootstraprc
new file mode 100644 (file)
index 0000000..a4d4fe6
--- /dev/null
@@ -0,0 +1,125 @@
+---
+# 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
index 81e4a1cf7defb1b796eec84483f384f045254171..b20541002501a0c1259869024dbd0de88278467d 100644 (file)
@@ -1,8 +1,2 @@
-typings
-app/**/*.js
-app/**/*.map
-app/**/*.css
-stylesheets/index.css
-bundles
-main.js
-main.js.map
+dist/
+typings/
diff --git a/client/app/app.component.html b/client/app/app.component.html
deleted file mode 100644 (file)
index 48e97d5..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-<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>
diff --git a/client/app/app.component.scss b/client/app/app.component.scss
deleted file mode 100644 (file)
index e02c2d5..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-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);
-}
diff --git a/client/app/app.component.ts b/client/app/app.component.ts
deleted file mode 100644 (file)
index 94924a4..0000000
+++ /dev/null
@@ -1,109 +0,0 @@
-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)
-    );
-  }
-}
diff --git a/client/app/friends/friend.service.ts b/client/app/friends/friend.service.ts
deleted file mode 100644 (file)
index d3684f0..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-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');
-  }
-}
diff --git a/client/app/friends/index.ts b/client/app/friends/index.ts
deleted file mode 100644 (file)
index 0adc256..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export * from './friend.service';
diff --git a/client/app/login/index.ts b/client/app/login/index.ts
deleted file mode 100644 (file)
index 69c1644..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export * from './login.component';
diff --git a/client/app/login/login.component.html b/client/app/login/login.component.html
deleted file mode 100644 (file)
index 9406945..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<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>
diff --git a/client/app/login/login.component.ts b/client/app/login/login.component.ts
deleted file mode 100644 (file)
index 50f598d..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-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}`);
-        }
-      }
-    );
-  }
-}
diff --git a/client/app/shared/index.ts b/client/app/shared/index.ts
deleted file mode 100644 (file)
index ad3ee00..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './search/index';
-export * from './users/index'
diff --git a/client/app/shared/search/index.ts b/client/app/shared/search/index.ts
deleted file mode 100644 (file)
index a49a4f1..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-export * from './search-field.type';
-export * from './search.component';
-export * from './search.model';
diff --git a/client/app/shared/search/search-field.type.ts b/client/app/shared/search/search-field.type.ts
deleted file mode 100644 (file)
index 8462362..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export type SearchField = "name" | "author" | "podUrl" | "magnetUri";
diff --git a/client/app/shared/search/search.component.html b/client/app/shared/search/search.component.html
deleted file mode 100644 (file)
index fb13ac7..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-<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>
diff --git a/client/app/shared/search/search.component.ts b/client/app/shared/search/search.component.ts
deleted file mode 100644 (file)
index d541cd0..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-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];
-  }
-}
diff --git a/client/app/shared/search/search.model.ts b/client/app/shared/search/search.model.ts
deleted file mode 100644 (file)
index 932a656..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-import { SearchField } from './search-field.type';
-
-export interface Search {
-  field: SearchField;
-  value: string;
-}
diff --git a/client/app/shared/users/auth-status.model.ts b/client/app/shared/users/auth-status.model.ts
deleted file mode 100644 (file)
index f646bd4..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-export enum AuthStatus {
-  LoggedIn,
-  LoggedOut
-}
diff --git a/client/app/shared/users/auth.service.ts b/client/app/shared/users/auth.service.ts
deleted file mode 100644 (file)
index d63fe38..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-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' });
-  }
-}
diff --git a/client/app/shared/users/index.ts b/client/app/shared/users/index.ts
deleted file mode 100644 (file)
index c6816b3..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from './auth-status.model';
-export * from './auth.service';
-export * from './token.model';
-export * from './user.model';
diff --git a/client/app/shared/users/token.model.ts b/client/app/shared/users/token.model.ts
deleted file mode 100644 (file)
index 021c83f..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-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);
-  }
-}
diff --git a/client/app/shared/users/user.model.ts b/client/app/shared/users/user.model.ts
deleted file mode 100644 (file)
index ca0a5f2..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-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();
-  }
-}
diff --git a/client/app/videos/index.ts b/client/app/videos/index.ts
deleted file mode 100644 (file)
index 1c80ac5..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from './shared/index';
-export * from './video-add/index';
-export * from './video-list/index';
-export * from './video-watch/index';
diff --git a/client/app/videos/shared/index.ts b/client/app/videos/shared/index.ts
deleted file mode 100644 (file)
index c535c46..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-export * from './loader/index';
-export * from './pagination.model';
-export * from './sort-field.type';
-export * from './video.model';
-export * from './video.service';
diff --git a/client/app/videos/shared/loader/index.ts b/client/app/videos/shared/loader/index.ts
deleted file mode 100644 (file)
index ab22584..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export * from './loader.component';
diff --git a/client/app/videos/shared/loader/loader.component.html b/client/app/videos/shared/loader/loader.component.html
deleted file mode 100644 (file)
index d02296a..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-<div id="video-loading" class="col-md-12 text-center" *ngIf="loading">
-  <div class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></div>
-</div>
diff --git a/client/app/videos/shared/loader/loader.component.scss b/client/app/videos/shared/loader/loader.component.scss
deleted file mode 100644 (file)
index 4541958..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-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);}
-}
diff --git a/client/app/videos/shared/loader/loader.component.ts b/client/app/videos/shared/loader/loader.component.ts
deleted file mode 100644 (file)
index 666d43b..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-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;
-}
diff --git a/client/app/videos/shared/pagination.model.ts b/client/app/videos/shared/pagination.model.ts
deleted file mode 100644 (file)
index 06f7a78..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-export interface Pagination {
-  currentPage: number;
-  itemsPerPage: number;
-  total: number;
-}
diff --git a/client/app/videos/shared/sort-field.type.ts b/client/app/videos/shared/sort-field.type.ts
deleted file mode 100644 (file)
index 6e8cc79..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-export type SortField = "name" | "-name"
-                      | "duration" | "-duration"
-                      | "createdDate" | "-createdDate";
diff --git a/client/app/videos/shared/video.model.ts b/client/app/videos/shared/video.model.ts
deleted file mode 100644 (file)
index 614403d..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-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;
-  }
-}
diff --git a/client/app/videos/shared/video.service.ts b/client/app/videos/shared/video.service.ts
deleted file mode 100644 (file)
index a786b2a..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-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');
-  }
-}
diff --git a/client/app/videos/video-add/index.ts b/client/app/videos/video-add/index.ts
deleted file mode 100644 (file)
index 79488e8..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export * from './video-add.component';
diff --git a/client/app/videos/video-add/video-add.component.html b/client/app/videos/video-add/video-add.component.html
deleted file mode 100644 (file)
index 80d229c..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-<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>
diff --git a/client/app/videos/video-add/video-add.component.scss b/client/app/videos/video-add/video-add.component.scss
deleted file mode 100644 (file)
index 01195f0..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-.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;
-}
diff --git a/client/app/videos/video-add/video-add.component.ts b/client/app/videos/video-add/video-add.component.ts
deleted file mode 100644 (file)
index e17b1b0..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/// <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();
-  }
-}
diff --git a/client/app/videos/video-list/index.ts b/client/app/videos/video-list/index.ts
deleted file mode 100644 (file)
index 1f6d6a4..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-export * from './video-list.component';
-export * from './video-miniature.component';
-export * from './video-sort.component';
diff --git a/client/app/videos/video-list/video-list.component.html b/client/app/videos/video-list/video-list.component.html
deleted file mode 100644 (file)
index 80b1e7b..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-<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>
diff --git a/client/app/videos/video-list/video-list.component.scss b/client/app/videos/video-list/video-list.component.scss
deleted file mode 100644 (file)
index 9441d80..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-.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;
-}
diff --git a/client/app/videos/video-list/video-list.component.ts b/client/app/videos/video-list/video-list.component.ts
deleted file mode 100644 (file)
index baca00d..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-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();
-  }
-}
diff --git a/client/app/videos/video-list/video-miniature.component.html b/client/app/videos/video-list/video-miniature.component.html
deleted file mode 100644 (file)
index 244254b..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-<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>
diff --git a/client/app/videos/video-list/video-miniature.component.scss b/client/app/videos/video-list/video-miniature.component.scss
deleted file mode 100644 (file)
index 4488abe..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-.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);
-    }
-  }
-}
diff --git a/client/app/videos/video-list/video-miniature.component.ts b/client/app/videos/video-list/video-miniature.component.ts
deleted file mode 100644 (file)
index 11b828c..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-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)
-      );
-    }
-  }
-}
diff --git a/client/app/videos/video-list/video-sort.component.html b/client/app/videos/video-list/video-sort.component.html
deleted file mode 100644 (file)
index 3bece0b..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-<select class="form-control input-sm" [(ngModel)]="currentSort" (ngModelChange)="onSortChange()">
-  <option *ngFor="let choice of choiceKeys" [value]="choice">
-    {{ getStringChoice(choice) }}
-  </option>
-</select>
diff --git a/client/app/videos/video-list/video-sort.component.ts b/client/app/videos/video-list/video-sort.component.ts
deleted file mode 100644 (file)
index 2cb810f..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-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);
-  }
-}
diff --git a/client/app/videos/video-watch/index.ts b/client/app/videos/video-watch/index.ts
deleted file mode 100644 (file)
index b17aaac..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './video-watch.component';
-export * from './webtorrent.service';
diff --git a/client/app/videos/video-watch/video-watch.component.html b/client/app/videos/video-watch/video-watch.component.html
deleted file mode 100644 (file)
index 6c36b27..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-<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>
diff --git a/client/app/videos/video-watch/video-watch.component.scss b/client/app/videos/video-watch/video-watch.component.scss
deleted file mode 100644 (file)
index 1228d42..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-.embed-responsive {
-  height: 500px;
-}
-
-#torrent-info {
-  font-size: 10px;
-
-  div {
-    display: inline-block;
-    width: 33%;
-    text-align: center;
-  }
-}
diff --git a/client/app/videos/video-watch/video-watch.component.ts b/client/app/videos/video-watch/video-watch.component.ts
deleted file mode 100644 (file)
index 71fb4f6..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-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;
-  }
-}
diff --git a/client/app/videos/video-watch/webtorrent.service.ts b/client/app/videos/video-watch/webtorrent.service.ts
deleted file mode 100644 (file)
index 0c73ab4..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-// 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);
-  }
-}
diff --git a/client/config/helpers.js b/client/config/helpers.js
new file mode 100644 (file)
index 0000000..24d7dae
--- /dev/null
@@ -0,0 +1,17 @@
+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
diff --git a/client/config/webpack.common.js b/client/config/webpack.common.js
new file mode 100644 (file)
index 0000000..5f3f44b
--- /dev/null
@@ -0,0 +1,261 @@
+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
+  }
+
+}
diff --git a/client/config/webpack.dev.js b/client/config/webpack.dev.js
new file mode 100644 (file)
index 0000000..19768d8
--- /dev/null
@@ -0,0 +1,159 @@
+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
+  }
+
+})
diff --git a/client/config/webpack.prod.js b/client/config/webpack.prod.js
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/client/images/favicon.png b/client/images/favicon.png
deleted file mode 100644 (file)
index bb57ee6..0000000
Binary files a/client/images/favicon.png and /dev/null differ
diff --git a/client/images/loading.gif b/client/images/loading.gif
deleted file mode 100644 (file)
index f2a1bc0..0000000
Binary files a/client/images/loading.gif and /dev/null differ
diff --git a/client/index.html b/client/index.html
deleted file mode 100644 (file)
index 6fbcd1f..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-<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>
diff --git a/client/main.ts b/client/main.ts
deleted file mode 100644 (file)
index 5e2ea0d..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-import { bootstrap }    from '@angular/platform-browser-dynamic';
-
-import { AppComponent } from './app/app.component';
-
-bootstrap(AppComponent);
index dea673357856de7198480ba58a583cc6a319fd39..bfdfe557496208e3c80374ebb91364a2e753206d 100644 (file)
     "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": [
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html
new file mode 100644 (file)
index 0000000..48e97d5
--- /dev/null
@@ -0,0 +1,60 @@
+<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>
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss
new file mode 100644 (file)
index 0000000..e02c2d5
--- /dev/null
@@ -0,0 +1,32 @@
+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);
+}
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
new file mode 100644 (file)
index 0000000..81b700a
--- /dev/null
@@ -0,0 +1,109 @@
+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)
+    );
+  }
+}
diff --git a/client/src/app/friends/friend.service.ts b/client/src/app/friends/friend.service.ts
new file mode 100644 (file)
index 0000000..a8b1a1b
--- /dev/null
@@ -0,0 +1,31 @@
+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');
+  }
+}
diff --git a/client/src/app/friends/index.ts b/client/src/app/friends/index.ts
new file mode 100644 (file)
index 0000000..0adc256
--- /dev/null
@@ -0,0 +1 @@
+export * from './friend.service';
diff --git a/client/src/app/login/index.ts b/client/src/app/login/index.ts
new file mode 100644 (file)
index 0000000..69c1644
--- /dev/null
@@ -0,0 +1 @@
+export * from './login.component';
diff --git a/client/src/app/login/login.component.html b/client/src/app/login/login.component.html
new file mode 100644 (file)
index 0000000..9406945
--- /dev/null
@@ -0,0 +1,14 @@
+<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>
diff --git a/client/src/app/login/login.component.ts b/client/src/app/login/login.component.ts
new file mode 100644 (file)
index 0000000..9d88536
--- /dev/null
@@ -0,0 +1,36 @@
+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}`);
+        }
+      }
+    );
+  }
+}
diff --git a/client/src/app/shared/index.ts b/client/src/app/shared/index.ts
new file mode 100644 (file)
index 0000000..0cab7da
--- /dev/null
@@ -0,0 +1,2 @@
+export * from './search';
+export * from './users'
diff --git a/client/src/app/shared/search/index.ts b/client/src/app/shared/search/index.ts
new file mode 100644 (file)
index 0000000..a49a4f1
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './search-field.type';
+export * from './search.component';
+export * from './search.model';
diff --git a/client/src/app/shared/search/search-field.type.ts b/client/src/app/shared/search/search-field.type.ts
new file mode 100644 (file)
index 0000000..8462362
--- /dev/null
@@ -0,0 +1 @@
+export type SearchField = "name" | "author" | "podUrl" | "magnetUri";
diff --git a/client/src/app/shared/search/search.component.html b/client/src/app/shared/search/search.component.html
new file mode 100644 (file)
index 0000000..fb13ac7
--- /dev/null
@@ -0,0 +1,17 @@
+<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>
diff --git a/client/src/app/shared/search/search.component.ts b/client/src/app/shared/search/search.component.ts
new file mode 100644 (file)
index 0000000..31f8b15
--- /dev/null
@@ -0,0 +1,46 @@
+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];
+  }
+}
diff --git a/client/src/app/shared/search/search.model.ts b/client/src/app/shared/search/search.model.ts
new file mode 100644 (file)
index 0000000..932a656
--- /dev/null
@@ -0,0 +1,6 @@
+import { SearchField } from './search-field.type';
+
+export interface Search {
+  field: SearchField;
+  value: string;
+}
diff --git a/client/src/app/shared/users/auth-status.model.ts b/client/src/app/shared/users/auth-status.model.ts
new file mode 100644 (file)
index 0000000..f646bd4
--- /dev/null
@@ -0,0 +1,4 @@
+export enum AuthStatus {
+  LoggedIn,
+  LoggedOut
+}
diff --git a/client/src/app/shared/users/auth.service.ts b/client/src/app/shared/users/auth.service.ts
new file mode 100644 (file)
index 0000000..d63fe38
--- /dev/null
@@ -0,0 +1,108 @@
+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' });
+  }
+}
diff --git a/client/src/app/shared/users/index.ts b/client/src/app/shared/users/index.ts
new file mode 100644 (file)
index 0000000..c6816b3
--- /dev/null
@@ -0,0 +1,4 @@
+export * from './auth-status.model';
+export * from './auth.service';
+export * from './token.model';
+export * from './user.model';
diff --git a/client/src/app/shared/users/token.model.ts b/client/src/app/shared/users/token.model.ts
new file mode 100644 (file)
index 0000000..021c83f
--- /dev/null
@@ -0,0 +1,32 @@
+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);
+  }
+}
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts
new file mode 100644 (file)
index 0000000..ca0a5f2
--- /dev/null
@@ -0,0 +1,20 @@
+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();
+  }
+}
diff --git a/client/src/app/videos/index.ts b/client/src/app/videos/index.ts
new file mode 100644 (file)
index 0000000..9a92fa5
--- /dev/null
@@ -0,0 +1,4 @@
+export * from './shared';
+export * from './video-add';
+export * from './video-list';
+export * from './video-watch';
diff --git a/client/src/app/videos/shared/index.ts b/client/src/app/videos/shared/index.ts
new file mode 100644 (file)
index 0000000..a54120f
--- /dev/null
@@ -0,0 +1,5 @@
+export * from './loader';
+export * from './pagination.model';
+export * from './sort-field.type';
+export * from './video.model';
+export * from './video.service';
diff --git a/client/src/app/videos/shared/loader/index.ts b/client/src/app/videos/shared/loader/index.ts
new file mode 100644 (file)
index 0000000..ab22584
--- /dev/null
@@ -0,0 +1 @@
+export * from './loader.component';
diff --git a/client/src/app/videos/shared/loader/loader.component.html b/client/src/app/videos/shared/loader/loader.component.html
new file mode 100644 (file)
index 0000000..d02296a
--- /dev/null
@@ -0,0 +1,3 @@
+<div id="video-loading" class="col-md-12 text-center" *ngIf="loading">
+  <div class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></div>
+</div>
diff --git a/client/src/app/videos/shared/loader/loader.component.scss b/client/src/app/videos/shared/loader/loader.component.scss
new file mode 100644 (file)
index 0000000..4541958
--- /dev/null
@@ -0,0 +1,26 @@
+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);}
+}
diff --git a/client/src/app/videos/shared/loader/loader.component.ts b/client/src/app/videos/shared/loader/loader.component.ts
new file mode 100644 (file)
index 0000000..cdd07d1
--- /dev/null
@@ -0,0 +1,11 @@
+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;
+}
diff --git a/client/src/app/videos/shared/pagination.model.ts b/client/src/app/videos/shared/pagination.model.ts
new file mode 100644 (file)
index 0000000..06f7a78
--- /dev/null
@@ -0,0 +1,5 @@
+export interface Pagination {
+  currentPage: number;
+  itemsPerPage: number;
+  total: number;
+}
diff --git a/client/src/app/videos/shared/sort-field.type.ts b/client/src/app/videos/shared/sort-field.type.ts
new file mode 100644 (file)
index 0000000..6e8cc79
--- /dev/null
@@ -0,0 +1,3 @@
+export type SortField = "name" | "-name"
+                      | "duration" | "-duration"
+                      | "createdDate" | "-createdDate";
diff --git a/client/src/app/videos/shared/video.model.ts b/client/src/app/videos/shared/video.model.ts
new file mode 100644 (file)
index 0000000..614403d
--- /dev/null
@@ -0,0 +1,64 @@
+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;
+  }
+}
diff --git a/client/src/app/videos/shared/video.service.ts b/client/src/app/videos/shared/video.service.ts
new file mode 100644 (file)
index 0000000..76d46cb
--- /dev/null
@@ -0,0 +1,82 @@
+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');
+  }
+}
diff --git a/client/src/app/videos/video-add/index.ts b/client/src/app/videos/video-add/index.ts
new file mode 100644 (file)
index 0000000..79488e8
--- /dev/null
@@ -0,0 +1 @@
+export * from './video-add.component';
diff --git a/client/src/app/videos/video-add/video-add.component.html b/client/src/app/videos/video-add/video-add.component.html
new file mode 100644 (file)
index 0000000..80d229c
--- /dev/null
@@ -0,0 +1,41 @@
+<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>
diff --git a/client/src/app/videos/video-add/video-add.component.scss b/client/src/app/videos/video-add/video-add.component.scss
new file mode 100644 (file)
index 0000000..01195f0
--- /dev/null
@@ -0,0 +1,33 @@
+.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;
+}
diff --git a/client/src/app/videos/video-add/video-add.component.ts b/client/src/app/videos/video-add/video-add.component.ts
new file mode 100644 (file)
index 0000000..8df4f95
--- /dev/null
@@ -0,0 +1,69 @@
+/// <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();
+  }
+}
diff --git a/client/src/app/videos/video-list/index.ts b/client/src/app/videos/video-list/index.ts
new file mode 100644 (file)
index 0000000..1f6d6a4
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './video-list.component';
+export * from './video-miniature.component';
+export * from './video-sort.component';
diff --git a/client/src/app/videos/video-list/video-list.component.html b/client/src/app/videos/video-list/video-list.component.html
new file mode 100644 (file)
index 0000000..80b1e7b
--- /dev/null
@@ -0,0 +1,18 @@
+<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>
diff --git a/client/src/app/videos/video-list/video-list.component.scss b/client/src/app/videos/video-list/video-list.component.scss
new file mode 100644 (file)
index 0000000..9441d80
--- /dev/null
@@ -0,0 +1,37 @@
+.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;
+}
diff --git a/client/src/app/videos/video-list/video-list.component.ts b/client/src/app/videos/video-list/video-list.component.ts
new file mode 100644 (file)
index 0000000..b1ce558
--- /dev/null
@@ -0,0 +1,105 @@
+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();
+  }
+}
diff --git a/client/src/app/videos/video-list/video-miniature.component.html b/client/src/app/videos/video-list/video-miniature.component.html
new file mode 100644 (file)
index 0000000..244254b
--- /dev/null
@@ -0,0 +1,22 @@
+<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>
diff --git a/client/src/app/videos/video-list/video-miniature.component.scss b/client/src/app/videos/video-list/video-miniature.component.scss
new file mode 100644 (file)
index 0000000..4488abe
--- /dev/null
@@ -0,0 +1,55 @@
+.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);
+    }
+  }
+}
diff --git a/client/src/app/videos/video-list/video-miniature.component.ts b/client/src/app/videos/video-list/video-miniature.component.ts
new file mode 100644 (file)
index 0000000..639339b
--- /dev/null
@@ -0,0 +1,46 @@
+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)
+      );
+    }
+  }
+}
diff --git a/client/src/app/videos/video-list/video-sort.component.html b/client/src/app/videos/video-list/video-sort.component.html
new file mode 100644 (file)
index 0000000..3bece0b
--- /dev/null
@@ -0,0 +1,5 @@
+<select class="form-control input-sm" [(ngModel)]="currentSort" (ngModelChange)="onSortChange()">
+  <option *ngFor="let choice of choiceKeys" [value]="choice">
+    {{ getStringChoice(choice) }}
+  </option>
+</select>
diff --git a/client/src/app/videos/video-list/video-sort.component.ts b/client/src/app/videos/video-list/video-sort.component.ts
new file mode 100644 (file)
index 0000000..0d76b54
--- /dev/null
@@ -0,0 +1,35 @@
+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);
+  }
+}
diff --git a/client/src/app/videos/video-watch/index.ts b/client/src/app/videos/video-watch/index.ts
new file mode 100644 (file)
index 0000000..b17aaac
--- /dev/null
@@ -0,0 +1,2 @@
+export * from './video-watch.component';
+export * from './webtorrent.service';
diff --git a/client/src/app/videos/video-watch/video-watch.component.html b/client/src/app/videos/video-watch/video-watch.component.html
new file mode 100644 (file)
index 0000000..6c36b27
--- /dev/null
@@ -0,0 +1,10 @@
+<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>
diff --git a/client/src/app/videos/video-watch/video-watch.component.scss b/client/src/app/videos/video-watch/video-watch.component.scss
new file mode 100644 (file)
index 0000000..1228d42
--- /dev/null
@@ -0,0 +1,13 @@
+.embed-responsive {
+  height: 500px;
+}
+
+#torrent-info {
+  font-size: 10px;
+
+  div {
+    display: inline-block;
+    width: 33%;
+    text-align: center;
+  }
+}
diff --git a/client/src/app/videos/video-watch/video-watch.component.ts b/client/src/app/videos/video-watch/video-watch.component.ts
new file mode 100644 (file)
index 0000000..db82283
--- /dev/null
@@ -0,0 +1,72 @@
+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;
+  }
+}
diff --git a/client/src/app/videos/video-watch/webtorrent.service.ts b/client/src/app/videos/video-watch/webtorrent.service.ts
new file mode 100644 (file)
index 0000000..bf38b5a
--- /dev/null
@@ -0,0 +1,26 @@
+// 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);
+  }
+}
diff --git a/client/src/assets/favicon.png b/client/src/assets/favicon.png
new file mode 100644 (file)
index 0000000..bb57ee6
Binary files /dev/null and b/client/src/assets/favicon.png differ
diff --git a/client/src/custom-typings.d.ts b/client/src/custom-typings.d.ts
new file mode 100644 (file)
index 0000000..14c7d8a
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * 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>;
+}
diff --git a/client/src/index.html b/client/src/index.html
new file mode 100644 (file)
index 0000000..83f4cc8
--- /dev/null
@@ -0,0 +1,17 @@
+<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>
diff --git a/client/src/main.ts b/client/src/main.ts
new file mode 100644 (file)
index 0000000..76b3f49
--- /dev/null
@@ -0,0 +1,10 @@
+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);
diff --git a/client/src/polyfills.ts b/client/src/polyfills.ts
new file mode 100644 (file)
index 0000000..3395eed
--- /dev/null
@@ -0,0 +1,28 @@
+// 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');
+
+}
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss
new file mode 100644 (file)
index 0000000..5c56e95
--- /dev/null
@@ -0,0 +1,11 @@
+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;
+}
diff --git a/client/src/sass/pre-customizations.scss b/client/src/sass/pre-customizations.scss
new file mode 100644 (file)
index 0000000..e920041
--- /dev/null
@@ -0,0 +1 @@
+// $icon-font-path: "/assets/fonts/";
diff --git a/client/src/vendor.ts b/client/src/vendor.ts
new file mode 100644 (file)
index 0000000..0f82c59
--- /dev/null
@@ -0,0 +1,28 @@
+// 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
+
+}
diff --git a/client/stylesheets/application.scss b/client/stylesheets/application.scss
deleted file mode 100644 (file)
index 98c1e37..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-$icon-font-path: "/client/node_modules/bootstrap-sass/assets/fonts/bootstrap/";
-
-@import "bootstrap-variables";
-@import "_bootstrap";
-@import "base.scss";
diff --git a/client/stylesheets/base.scss b/client/stylesheets/base.scss
deleted file mode 100644 (file)
index 5c56e95..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-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;
-}
diff --git a/client/stylesheets/bootstrap-variables.scss b/client/stylesheets/bootstrap-variables.scss
deleted file mode 100644 (file)
index a2472f3..0000000
+++ /dev/null
@@ -1,875 +0,0 @@
-// 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
diff --git a/client/systemjs.bundle.js b/client/systemjs.bundle.js
deleted file mode 100644 (file)
index 2fd4515..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-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')
diff --git a/client/systemjs.config.js b/client/systemjs.config.js
deleted file mode 100644 (file)
index d04bc41..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-;(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)
index a8b8269a49ee66ea71d8008a59d2df512ce2d2a1..1832d7b7e308149117668b706892605154f536df 100644 (file)
   ],
   "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",
diff --git a/client/webpack.config.js b/client/webpack.config.js
new file mode 100644 (file)
index 0000000..8f54d88
--- /dev/null
@@ -0,0 +1,16 @@
+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')
+}
index 02c0d53cd7ef949fe0592aac77ce3bffbcc9c984..4c8e8cfd39d03b18614e57fb56a1c1d143582871 100644 (file)
--- a/server.js
+++ b/server.js
@@ -64,7 +64,7 @@ const apiRoute = '/api/' + constants.API_VERSION
 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)
@@ -76,7 +76,7 @@ app.use(constants.THUMBNAILS_STATIC_PATH, express.static(thumbnailsPhysicalPath,
 
 // 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 -----------