Try to optimize frontend
authorChocobozzz <florian.bigard@gmail.com>
Mon, 9 Oct 2017 12:28:44 +0000 (14:28 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Mon, 9 Oct 2017 12:28:44 +0000 (14:28 +0200)
52 files changed:
client/.bootstraprc
client/config/webpack.common.js
client/config/webpack.prod.js
client/package.json
client/src/app/app-routing.module.ts
client/src/app/app.component.ts
client/src/app/core/index.ts
client/src/app/core/routing/index.ts [new file with mode: 0644]
client/src/app/core/routing/preload-selected-modules-list.ts [new file with mode: 0644]
client/src/app/videos/+video-edit/index.ts [new file with mode: 0644]
client/src/app/videos/+video-edit/video-add-routing.module.ts [new file with mode: 0644]
client/src/app/videos/+video-edit/video-add.component.html [new file with mode: 0644]
client/src/app/videos/+video-edit/video-add.component.ts [new file with mode: 0644]
client/src/app/videos/+video-edit/video-add.module.ts [new file with mode: 0644]
client/src/app/videos/+video-edit/video-edit.component.scss [new file with mode: 0644]
client/src/app/videos/+video-edit/video-update-routing.module.ts [new file with mode: 0644]
client/src/app/videos/+video-edit/video-update.component.html [new file with mode: 0644]
client/src/app/videos/+video-edit/video-update.component.ts [new file with mode: 0644]
client/src/app/videos/+video-edit/video-update.module.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-magnet.component.html [new file with mode: 0644]
client/src/app/videos/+video-watch/video-magnet.component.ts [new file with mode: 0644]
client/src/app/videos/+video-watch/video-report.component.html [new file with mode: 0644]
client/src/app/videos/+video-watch/video-report.component.ts [new file with mode: 0644]
client/src/app/videos/+video-watch/video-share.component.html [new file with mode: 0644]
client/src/app/videos/+video-watch/video-share.component.ts [new file with mode: 0644]
client/src/app/videos/+video-watch/video-watch-routing.module.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/video-watch.module.ts [new file with mode: 0644]
client/src/app/videos/index.ts
client/src/app/videos/video-edit/index.ts [deleted file]
client/src/app/videos/video-edit/video-add.component.html [deleted file]
client/src/app/videos/video-edit/video-add.component.ts [deleted file]
client/src/app/videos/video-edit/video-edit.component.scss [deleted file]
client/src/app/videos/video-edit/video-update.component.html [deleted file]
client/src/app/videos/video-edit/video-update.component.ts [deleted file]
client/src/app/videos/video-watch/index.ts [deleted file]
client/src/app/videos/video-watch/video-magnet.component.html [deleted file]
client/src/app/videos/video-watch/video-magnet.component.ts [deleted file]
client/src/app/videos/video-watch/video-report.component.html [deleted file]
client/src/app/videos/video-watch/video-report.component.ts [deleted file]
client/src/app/videos/video-watch/video-share.component.html [deleted file]
client/src/app/videos/video-watch/video-share.component.ts [deleted file]
client/src/app/videos/video-watch/video-watch.component.html [deleted file]
client/src/app/videos/video-watch/video-watch.component.scss [deleted file]
client/src/app/videos/video-watch/video-watch.component.ts [deleted file]
client/src/app/videos/videos-routing.module.ts
client/src/app/videos/videos.component.ts
client/src/app/videos/videos.module.ts
client/yarn.lock

index d266656c14f65fed44103f3a03f68f4d2f552e1e..e560cb5fb0f0f98f2fe459862cb50272ce2da989 100644 (file)
@@ -19,12 +19,12 @@ styleLoaders:
 # 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
+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
index 000699aa5396d4e157a34459170f846404e6c396..83bcbc2408fe45410a1693bf678eff89ed633156 100644 (file)
@@ -11,6 +11,7 @@ const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin
 const HtmlWebpackPlugin = require('html-webpack-plugin')
 const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin')
 const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin')
+const InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin')
 const ngcWebpack = require('ngc-webpack')
 
 const WebpackNotifierPlugin = require('webpack-notifier')
@@ -216,7 +217,9 @@ module.exports = function (options) {
       new CommonsChunkPlugin({
         name: 'vendor',
         chunks: ['main'],
-        minChunks: module => /node_modules\//.test(module.resource)
+        minChunks: module => {
+          return /node_modules\//.test(module.resource)
+        }
       }),
 
       // Specify the correct order the scripts will be injected in
@@ -244,20 +247,6 @@ module.exports = function (options) {
         }
       ),
 
-      /*
-       * Plugin: ScriptExtHtmlWebpackPlugin
-       * Description: Enhances html-webpack-plugin functionality
-       * with different deployment options for your scripts including:
-       *
-       * See: https://github.com/numical/script-ext-html-webpack-plugin
-       */
-      new ScriptExtHtmlWebpackPlugin({
-        sync: [ /polyfill|vendor/ ],
-        defaultAttribute: 'async',
-        preload: [/polyfill|vendor|main/],
-        prefetch: [/chunk/]
-      }),
-
       /*
        * Plugin: HtmlWebpackPlugin
        * Description: Simplifies creation of HTML files to serve your webpack bundles.
@@ -277,6 +266,20 @@ module.exports = function (options) {
         inject: 'body'
       }),
 
+      /*
+       * Plugin: ScriptExtHtmlWebpackPlugin
+       * Description: Enhances html-webpack-plugin functionality
+       * with different deployment options for your scripts including:
+       *
+       * See: https://github.com/numical/script-ext-html-webpack-plugin
+       */
+      new ScriptExtHtmlWebpackPlugin({
+        sync: [ /polyfill|vendor/ ],
+        defaultAttribute: 'async',
+        preload: [/polyfill|vendor|main/],
+        prefetch: [/chunk/]
+      }),
+
       new WebpackNotifierPlugin({ alwaysNotify: true }),
 
       /**
@@ -296,7 +299,9 @@ module.exports = function (options) {
       new ngcWebpack.NgcWebpackPlugin({
         disabled: !AOT,
         tsConfig: helpers.root('tsconfig.webpack.json')
-      })
+      }),
+
+      new InlineManifestWebpackPlugin(),
     ],
 
     /*
index 777c816e85db389f6ba5f2f07ff9a94cabd23617..ecd7914c793b2d3cbfb61f4c11c285a1e856a398 100644 (file)
@@ -17,6 +17,8 @@ const NormalModuleReplacementPlugin = require('webpack/lib/NormalModuleReplaceme
 const OptimizeJsPlugin = require('optimize-js-plugin')
 const HashedModuleIdsPlugin = require('webpack/lib/HashedModuleIdsPlugin')
 const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
+const ExtractTextPlugin = require('extract-text-webpack-plugin')
+
 /**
  * Webpack Constants
  */
@@ -120,15 +122,10 @@ module.exports = function (env) {
           sourceMap: false
         }),
 
-        /**
-         * Plugin: DedupePlugin
-         * Description: Prevents the inclusion of duplicate code into your bundle
-         * and instead applies a copy of the function at runtime.
-         *
-         * See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
-         * See: https://github.com/webpack/docs/wiki/optimization#deduplication
-         */
-        // new DedupePlugin(),
+        new ExtractTextPlugin({
+          filename: '[name].[contenthash].css',
+          allChunks: true
+        }),
 
         /**
          * Plugin: DefinePlugin
@@ -158,7 +155,6 @@ module.exports = function (env) {
         *
         * See: https://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin
         */
-        // NOTE: To debug prod builds uncomment //debug lines and comment //prod lines
         new UglifyJsPlugin({
           parallel: true,
           uglifyOptions: {
index 6fb1da425d8f0511e5915b27a2eee0b481de1bd9..eaa31c22f2ebdcea8aa48cd1c157b08c1b7496e0 100644 (file)
   },
   "license": "GPLv3",
   "dependencies": {
-    "@angular/animations": "~4.3.0",
-    "@angular/common": "~4.3.0",
-    "@angular/compiler": "~4.3.0",
-    "@angular/compiler-cli": "~4.3.0",
-    "@angular/core": "~4.3.0",
-    "@angular/forms": "~4.3.0",
-    "@angular/http": "~4.3.0",
-    "@angular/platform-browser": "~4.3.0",
-    "@angular/platform-browser-dynamic": "~4.3.0",
-    "@angular/router": "~4.3.0",
+    "@angular/animations": "~4.4.0",
+    "@angular/common": "~4.4.0",
+    "@angular/compiler": "~4.4.0",
+    "@angular/compiler-cli": "~4.4.0",
+    "@angular/core": "~4.4.0",
+    "@angular/forms": "~4.4.0",
+    "@angular/http": "~4.4.0",
+    "@angular/platform-browser": "~4.4.0",
+    "@angular/platform-browser-dynamic": "~4.4.0",
+    "@angular/router": "~4.4.0",
     "@angularclass/hmr": "^2.1.0",
     "@angularclass/hmr-loader": "^3.0.2",
     "@ngx-meta/core": "^0.4.0-rc.2",
@@ -55,7 +55,7 @@
     "css-loader": "^0.28.4",
     "css-to-string-loader": "^0.1.3",
     "es6-shim": "^0.35.0",
-    "file-loader": "^0.11.2",
+    "file-loader": "^1.1.5",
     "html-webpack-plugin": "^2.19.0",
     "ie-shim": "^0.1.0",
     "intl": "^1.2.4",
     "script-ext-html-webpack-plugin": "^1.3.2",
     "source-map-loader": "^0.2.1",
     "string-replace-loader": "^1.0.3",
-    "style-loader": "^0.18.2",
+    "style-loader": "^0.19.0",
     "tslib": "^1.5.0",
     "tslint": "^5.7.0",
     "tslint-loader": "^3.3.0",
     "typescript": "^2.5.2",
-    "url-loader": "^0.5.7",
+    "url-loader": "^0.6.2",
     "video.js": "^6.2.0",
     "videojs-dock": "^2.0.2",
     "webpack": "^3.3.0",
@@ -95,6 +95,7 @@
     "add-asset-html-webpack-plugin": "^2.0.1",
     "codelyzer": "^3.0.0-beta.4",
     "extract-text-webpack-plugin": "^3.0.0",
+    "inline-manifest-webpack-plugin": "^3.0.1",
     "primeng": "^4.2.0",
     "purify-css": "^1.2.5",
     "purifycss-webpack": "^0.7.0",
index 191ae6974b230baa80eedfcae54fd1fd54c6edc6..0f948434410ad488b80a77a6c90ce68134c6f947 100644 (file)
@@ -1,5 +1,7 @@
 import { NgModule } from '@angular/core'
-import { Routes, RouterModule, PreloadAllModules } from '@angular/router'
+import { Routes, RouterModule } from '@angular/router'
+
+import { PreloadSelectedModulesList } from './core'
 
 const routes: Routes = [
   {
@@ -17,9 +19,10 @@ const routes: Routes = [
   imports: [
     RouterModule.forRoot(routes, {
       useHash: Boolean(history.pushState) === false,
-      preloadingStrategy: PreloadAllModules
+      preloadingStrategy: PreloadSelectedModulesList
     })
   ],
+  providers: [ PreloadSelectedModulesList ],
   exports: [ RouterModule ]
 })
 export class AppRoutingModule {}
index ae86bc96f035bf2baea5ee669bef5504aed8d74c..82e647c98d5e7f386bab71f98c4bd12931a59144 100644 (file)
@@ -2,7 +2,6 @@ import { Component, OnInit, ViewContainerRef } from '@angular/core'
 import { Router } from '@angular/router'
 
 import { AuthService, ConfigService } from './core'
-import { VideoService } from './videos'
 import { UserService } from './shared'
 
 @Component({
@@ -30,8 +29,7 @@ export class AppComponent implements OnInit {
     private router: Router,
     private authService: AuthService,
     private configService: ConfigService,
-    private userService: UserService,
-    private videoService: VideoService
+    private userService: UserService
   ) {}
 
   ngOnInit () {
@@ -43,9 +41,6 @@ export class AppComponent implements OnInit {
     }
 
     this.configService.loadConfig()
-    this.videoService.loadVideoCategories()
-    this.videoService.loadVideoLicences()
-    this.videoService.loadVideoLanguages()
 
     // Do not display menu on small screens
     if (window.innerWidth < 600) {
index 01b12ce7e48153a943ef071d9eb8ce17614df648..31322138f7932eb9bb5d75e0a755be0df687c827 100644 (file)
@@ -2,4 +2,5 @@ export * from './auth'
 export * from './config'
 export * from './confirm'
 export * from './menu'
+export * from './routing'
 export * from './core.module'
diff --git a/client/src/app/core/routing/index.ts b/client/src/app/core/routing/index.ts
new file mode 100644 (file)
index 0000000..17f3ee8
--- /dev/null
@@ -0,0 +1 @@
+export * from './preload-selected-modules-list'
diff --git a/client/src/app/core/routing/preload-selected-modules-list.ts b/client/src/app/core/routing/preload-selected-modules-list.ts
new file mode 100644 (file)
index 0000000..dd5be6a
--- /dev/null
@@ -0,0 +1,16 @@
+import { Route, PreloadingStrategy } from '@angular/router';
+import { Observable } from 'rxjs/Observable';
+import 'rxjs/add/observable/timer';
+import 'rxjs/add/operator/switchMap';
+
+export class PreloadSelectedModulesList implements PreloadingStrategy {
+  preload(route: Route, load: Function): Observable<any> {
+    if (!route.data || !route.data.preload) return Observable.of(null);
+
+    if (typeof route.data.preload === 'number') {
+      return Observable.timer(route.data.preload).switchMap(() => load());
+    }
+
+    return load();
+  }
+}
diff --git a/client/src/app/videos/+video-edit/index.ts b/client/src/app/videos/+video-edit/index.ts
new file mode 100644 (file)
index 0000000..63e0414
--- /dev/null
@@ -0,0 +1,2 @@
+export * from './video-add.module'
+export * from './video-update.module'
diff --git a/client/src/app/videos/+video-edit/video-add-routing.module.ts b/client/src/app/videos/+video-edit/video-add-routing.module.ts
new file mode 100644 (file)
index 0000000..9e8fa4a
--- /dev/null
@@ -0,0 +1,20 @@
+import { NgModule } from '@angular/core'
+import { RouterModule, Routes } from '@angular/router'
+
+import { MetaGuard } from '@ngx-meta/core'
+
+import { VideoAddComponent } from './video-add.component'
+
+const videoAddRoutes: Routes = [
+  {
+    path: '',
+    component: VideoAddComponent,
+    canActivateChild: [ MetaGuard ]
+  }
+]
+
+@NgModule({
+  imports: [ RouterModule.forChild(videoAddRoutes) ],
+  exports: [ RouterModule ]
+})
+export class VideoAddRoutingModule {}
diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html
new file mode 100644 (file)
index 0000000..698152f
--- /dev/null
@@ -0,0 +1,121 @@
+<div class="row">
+  <div class="content-padding">
+
+    <h3>Upload a video</h3>
+
+    <div *ngIf="error !== undefined" class="alert alert-danger">{{ error }}</div>
+
+    <form novalidate [formGroup]="form">
+      <div class="form-group">
+        <label for="name">Name</label>
+        <input
+          type="text" class="form-control" id="name"
+          formControlName="name"
+        >
+        <div *ngIf="formErrors.name" class="alert alert-danger">
+          {{ formErrors.name }}
+        </div>
+      </div>
+
+      <div class="form-group">
+        <label for="nsfw">NSFW</label>
+        <input
+          type="checkbox" id="nsfw"
+          formControlName="nsfw"
+        >
+      </div>
+
+      <div class="form-group">
+        <label for="category">Category</label>
+        <select class="form-control" id="category" formControlName="category">
+          <option></option>
+          <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option>
+        </select>
+
+        <div *ngIf="formErrors.category" class="alert alert-danger">
+          {{ formErrors.category }}
+        </div>
+      </div>
+
+      <div class="form-group">
+        <label for="licence">Licence</label>
+        <select class="form-control" id="licence" formControlName="licence">
+          <option></option>
+          <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option>
+        </select>
+
+        <div *ngIf="formErrors.licence" class="alert alert-danger">
+          {{ formErrors.licence }}
+        </div>
+      </div>
+
+      <div class="form-group">
+        <label for="language">Language</label>
+        <select class="form-control" id="language" formControlName="language">
+          <option></option>
+          <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option>
+        </select>
+
+        <div *ngIf="formErrors.language" class="alert alert-danger">
+          {{ formErrors.language }}
+        </div>
+      </div>
+
+      <div class="form-group">
+        <label class="label-tags">Tags</label> <span class="little-information">(press enter to add the tag)</span>
+        <tag-input
+          [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
+          formControlName="tags" maxItems="3" modelAsStrings="true"
+        ></tag-input>
+      </div>
+
+      <div class="form-group">
+        <label for="videofile">File</label>
+        <div class="btn btn-default btn-file">
+          <span>Select the video...</span>
+          <input #videofileInput type="file" name="videofile" id="videofile" (change)="fileChange($event)" />
+          <input type="hidden" name="videofileHidden" formControlName="videofile"/>
+        </div>
+      </div>
+
+      <div class="file-to-upload">
+        <div class="file" *ngIf="filename">
+          <span class="filename">{{ filename }}</span>
+          <span class="glyphicon glyphicon-remove" (click)="removeFile()"></span>
+        </div>
+      </div>
+
+      <div *ngIf="formErrors.videofile" class="alert alert-danger">
+        {{ formErrors.videofile }}
+      </div>
+
+      <div class="form-group">
+        <label for="description">Description</label>
+        <textarea
+          id="description" class="form-control" placeholder="Description..."
+          formControlName="description"
+        >
+        </textarea>
+        <div *ngIf="formErrors.description" class="alert alert-danger">
+          {{ formErrors.description }}
+        </div>
+      </div>
+
+      <div class="progress">
+        <progressbar [value]="progressPercent" max="100">
+          <ng-template [ngIf]="progressPercent === 100">
+            <span class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></span>
+            Server is processing the video
+          </ng-template>
+        </progressbar>
+      </div>
+
+      <div class="form-group">
+        <input
+          type="button" value="Upload" class="btn btn-default form-control"
+          (click)="upload()"
+        >
+      </div>
+    </form>
+  </div>
+</div>
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts
new file mode 100644 (file)
index 0000000..21311b1
--- /dev/null
@@ -0,0 +1,165 @@
+import { Component, OnInit, ViewChild } from '@angular/core'
+import { FormBuilder, FormGroup } from '@angular/forms'
+import { Router } from '@angular/router'
+
+import { NotificationsService } from 'angular2-notifications'
+
+import {
+  FormReactive,
+  VIDEO_NAME,
+  VIDEO_CATEGORY,
+  VIDEO_LICENCE,
+  VIDEO_LANGUAGE,
+  VIDEO_DESCRIPTION,
+  VIDEO_TAGS
+} from '../../shared'
+import { VideoService } from '../shared'
+import { VideoCreate } from '../../../../../shared'
+import { HttpEventType, HttpResponse } from '@angular/common/http'
+import { VIDEO_FILE } from '../../shared/forms/form-validators/video'
+
+@Component({
+  selector: 'my-videos-add',
+  styleUrls: [ './video-edit.component.scss' ],
+  templateUrl: './video-add.component.html'
+})
+
+export class VideoAddComponent extends FormReactive implements OnInit {
+  @ViewChild('videofileInput') videofileInput
+
+  progressPercent = 0
+  tags: string[] = []
+  videoCategories = []
+  videoLicences = []
+  videoLanguages = []
+
+  tagValidators = VIDEO_TAGS.VALIDATORS
+  tagValidatorsMessages = VIDEO_TAGS.MESSAGES
+
+  error: string
+  form: FormGroup
+  formErrors = {
+    name: '',
+    category: '',
+    licence: '',
+    language: '',
+    description: '',
+    videofile: ''
+  }
+  validationMessages = {
+    name: VIDEO_NAME.MESSAGES,
+    category: VIDEO_CATEGORY.MESSAGES,
+    licence: VIDEO_LICENCE.MESSAGES,
+    language: VIDEO_LANGUAGE.MESSAGES,
+    description: VIDEO_DESCRIPTION.MESSAGES,
+    videofile: VIDEO_FILE.MESSAGES
+  }
+
+  constructor (
+    private formBuilder: FormBuilder,
+    private router: Router,
+    private notificationsService: NotificationsService,
+    private videoService: VideoService
+  ) {
+    super()
+  }
+
+  get filename () {
+    return this.form.value['videofile']
+  }
+
+  buildForm () {
+    this.form = this.formBuilder.group({
+      name: [ '', VIDEO_NAME.VALIDATORS ],
+      nsfw: [ false ],
+      category: [ '', VIDEO_CATEGORY.VALIDATORS ],
+      licence: [ '', VIDEO_LICENCE.VALIDATORS ],
+      language: [ '', VIDEO_LANGUAGE.VALIDATORS ],
+      description: [ '', VIDEO_DESCRIPTION.VALIDATORS ],
+      videofile: [ '', VIDEO_FILE.VALIDATORS ],
+      tags: [ '' ]
+    })
+
+    this.form.valueChanges.subscribe(data => this.onValueChanged(data))
+  }
+
+  ngOnInit () {
+    this.videoCategories = this.videoService.videoCategories
+    this.videoLicences = this.videoService.videoLicences
+    this.videoLanguages = this.videoService.videoLanguages
+
+    this.buildForm()
+  }
+
+  // The goal is to keep reactive form validation (required field)
+  // https://stackoverflow.com/a/44238894
+  fileChange ($event) {
+    this.form.controls['videofile'].setValue($event.target.files[0].name)
+  }
+
+  removeFile () {
+    this.videofileInput.nativeElement.value = ''
+    this.form.controls['videofile'].setValue('')
+  }
+
+  checkForm () {
+    this.forceCheck()
+
+    return this.form.valid
+  }
+
+  upload () {
+    if (this.checkForm() === false) {
+      return
+    }
+
+    const formValue: VideoCreate = this.form.value
+
+    const name = formValue.name
+    const nsfw = formValue.nsfw
+    const category = formValue.category
+    const licence = formValue.licence
+    const language = formValue.language
+    const description = formValue.description
+    const tags = formValue.tags
+    const videofile = this.videofileInput.nativeElement.files[0]
+
+    const formData = new FormData()
+    formData.append('name', name)
+    formData.append('category', '' + category)
+    formData.append('nsfw', '' + nsfw)
+    formData.append('licence', '' + licence)
+    formData.append('videofile', videofile)
+
+    // Language is optional
+    if (language) {
+      formData.append('language', '' + language)
+    }
+
+    formData.append('description', description)
+
+    for (let i = 0; i < tags.length; i++) {
+      formData.append(`tags[${i}]`, tags[i])
+    }
+
+    this.videoService.uploadVideo(formData).subscribe(
+      event => {
+        if (event.type === HttpEventType.UploadProgress) {
+          this.progressPercent = Math.round(100 * event.loaded / event.total)
+        } else if (event instanceof HttpResponse) {
+          console.log('Video uploaded.')
+          this.notificationsService.success('Success', 'Video uploaded.')
+
+          // Display all the videos once it's finished
+          this.router.navigate([ '/videos/list' ])
+        }
+      },
+
+      err => {
+        // Reset progress
+        this.progressPercent = 0
+        this.error = err.message
+      }
+    )
+  }
+}
diff --git a/client/src/app/videos/+video-edit/video-add.module.ts b/client/src/app/videos/+video-edit/video-add.module.ts
new file mode 100644 (file)
index 0000000..141d33a
--- /dev/null
@@ -0,0 +1,30 @@
+import { NgModule } from '@angular/core'
+
+import { TagInputModule } from 'ngx-chips'
+
+import { VideoAddRoutingModule } from './video-add-routing.module'
+import { VideoAddComponent } from './video-add.component'
+import { VideoService } from '../shared'
+import { SharedModule } from '../../shared'
+
+@NgModule({
+  imports: [
+    TagInputModule,
+
+    VideoAddRoutingModule,
+    SharedModule
+  ],
+
+  declarations: [
+    VideoAddComponent
+  ],
+
+  exports: [
+    VideoAddComponent
+  ],
+
+  providers: [
+    VideoService
+  ]
+})
+export class VideoAddModule { }
diff --git a/client/src/app/videos/+video-edit/video-edit.component.scss b/client/src/app/videos/+video-edit/video-edit.component.scss
new file mode 100644 (file)
index 0000000..9ee0c52
--- /dev/null
@@ -0,0 +1,56 @@
+.btn-file {
+  position: relative;
+  overflow: hidden;
+  display: block;
+}
+
+.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;
+}
+
+.form-group {
+  margin-bottom: 10px;
+}
+
+div.tags {
+  height: 40px;
+  font-size: 20px;
+  margin-top: 20px;
+
+  .tag {
+    margin-right: 10px;
+
+    .remove {
+      cursor: pointer;
+    }
+  }
+}
+
+div.file-to-upload {
+  height: 40px;
+
+  .glyphicon-remove {
+    cursor: pointer;
+  }
+}
+
+.little-information {
+  font-size: 0.8em;
+  font-style: italic;
+}
+
+.label-tags {
+  margin-bottom: 0;
+}
diff --git a/client/src/app/videos/+video-edit/video-update-routing.module.ts b/client/src/app/videos/+video-edit/video-update-routing.module.ts
new file mode 100644 (file)
index 0000000..1d06a7a
--- /dev/null
@@ -0,0 +1,20 @@
+import { NgModule } from '@angular/core'
+import { RouterModule, Routes } from '@angular/router'
+
+import { MetaGuard } from '@ngx-meta/core'
+
+import { VideoUpdateComponent } from './video-update.component'
+
+const videoUpdateRoutes: Routes = [
+  {
+    path: '',
+    component: VideoUpdateComponent,
+    canActivateChild: [ MetaGuard ]
+  }
+]
+
+@NgModule({
+  imports: [ RouterModule.forChild(videoUpdateRoutes) ],
+  exports: [ RouterModule ]
+})
+export class VideoUpdateRoutingModule {}
diff --git a/client/src/app/videos/+video-edit/video-update.component.html b/client/src/app/videos/+video-edit/video-update.component.html
new file mode 100644 (file)
index 0000000..7f4faf2
--- /dev/null
@@ -0,0 +1,92 @@
+<div class="row">
+  <div class="content-padding">
+
+  <h3>Update {{ video?.name }}</h3>
+
+  <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
+
+  <form novalidate [formGroup]="form">
+    <div class="form-group">
+      <label for="name">Name</label>
+      <input
+        type="text" class="form-control" id="name"
+        formControlName="name"
+      >
+      <div *ngIf="formErrors.name" class="alert alert-danger">
+        {{ formErrors.name }}
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="nsfw">NSFW</label>
+      <input
+        type="checkbox" id="nsfw"
+        formControlName="nsfw"
+      >
+    </div>
+
+    <div class="form-group">
+      <label for="category">Category</label>
+      <select class="form-control" id="category" formControlName="category">
+        <option></option>
+        <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option>
+      </select>
+
+      <div *ngIf="formErrors.category" class="alert alert-danger">
+        {{ formErrors.category }}
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="licence">Licence</label>
+      <select class="form-control" id="licence" formControlName="licence">
+        <option></option>
+        <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option>
+      </select>
+
+      <div *ngIf="formErrors.licence" class="alert alert-danger">
+        {{ formErrors.licence }}
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="language">Language</label>
+      <select class="form-control" id="language" formControlName="language">
+        <option></option>
+        <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option>
+      </select>
+
+      <div *ngIf="formErrors.language" class="alert alert-danger">
+        {{ formErrors.language }}
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="tags" class="label-tags">Tags</label> <span class="little-information">(press enter to add the tag)</span>
+      <tag-input
+        [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
+        formControlName="tags" maxItems="3" modelAsStrings="true"
+      ></tag-input>
+    </div>
+
+    <div class="form-group">
+      <label for="description">Description</label>
+      <textarea
+        id="description" class="form-control" placeholder="Description..."
+        formControlName="description"
+      >
+      </textarea>
+      <div *ngIf="formErrors.description" class="alert alert-danger">
+        {{ formErrors.description }}
+      </div>
+    </div>
+
+    <div class="form-group">
+      <input
+        type="button" value="Update" class="btn btn-default form-control"
+        (click)="update()"
+      >
+    </div>
+  </form>
+  </div>
+</div>
diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts
new file mode 100644 (file)
index 0000000..141ed35
--- /dev/null
@@ -0,0 +1,134 @@
+import { Component, ElementRef, OnInit } from '@angular/core'
+import { FormBuilder, FormGroup } from '@angular/forms'
+import { ActivatedRoute, Router } from '@angular/router'
+
+import { NotificationsService } from 'angular2-notifications'
+
+import { AuthService } from '../../core'
+import {
+  FormReactive,
+  VIDEO_NAME,
+  VIDEO_CATEGORY,
+  VIDEO_LICENCE,
+  VIDEO_LANGUAGE,
+  VIDEO_DESCRIPTION,
+  VIDEO_TAGS
+} from '../../shared'
+import { Video, VideoService } from '../shared'
+
+@Component({
+  selector: 'my-videos-update',
+  styleUrls: [ './video-edit.component.scss' ],
+  templateUrl: './video-update.component.html'
+})
+
+export class VideoUpdateComponent extends FormReactive implements OnInit {
+  tags: string[] = []
+  videoCategories = []
+  videoLicences = []
+  videoLanguages = []
+  video: Video
+
+  tagValidators = VIDEO_TAGS.VALIDATORS
+  tagValidatorsMessages = VIDEO_TAGS.MESSAGES
+
+  error: string = null
+  form: FormGroup
+  formErrors = {
+    name: '',
+    category: '',
+    licence: '',
+    language: '',
+    description: ''
+  }
+  validationMessages = {
+    name: VIDEO_NAME.MESSAGES,
+    category: VIDEO_CATEGORY.MESSAGES,
+    licence: VIDEO_LICENCE.MESSAGES,
+    language: VIDEO_LANGUAGE.MESSAGES,
+    description: VIDEO_DESCRIPTION.MESSAGES
+  }
+
+  fileError = ''
+
+  constructor (
+    private authService: AuthService,
+    private elementRef: ElementRef,
+    private formBuilder: FormBuilder,
+    private route: ActivatedRoute,
+    private router: Router,
+    private notificationsService: NotificationsService,
+    private videoService: VideoService
+  ) {
+    super()
+  }
+
+  buildForm () {
+    this.form = this.formBuilder.group({
+      name: [ '', VIDEO_NAME.VALIDATORS ],
+      nsfw: [ false ],
+      category: [ '', VIDEO_CATEGORY.VALIDATORS ],
+      licence: [ '', VIDEO_LICENCE.VALIDATORS ],
+      language: [ '', VIDEO_LANGUAGE.VALIDATORS ],
+      description: [ '', VIDEO_DESCRIPTION.VALIDATORS ],
+      tags: [ '' ]
+    })
+
+    this.form.valueChanges.subscribe(data => this.onValueChanged(data))
+  }
+
+  ngOnInit () {
+    this.buildForm()
+
+    this.videoCategories = this.videoService.videoCategories
+    this.videoLicences = this.videoService.videoLicences
+    this.videoLanguages = this.videoService.videoLanguages
+
+    const uuid: string = this.route.snapshot.params['uuid']
+    this.videoService.getVideo(uuid)
+                     .subscribe(
+                       video => {
+                         this.video = video
+
+                         this.hydrateFormFromVideo()
+                       },
+
+                       err => {
+                         console.error(err)
+                         this.error = 'Cannot fetch video.'
+                       }
+                     )
+  }
+
+  checkForm () {
+    this.forceCheck()
+
+    return this.form.valid
+  }
+
+  update () {
+    if (this.checkForm() === false) {
+      return
+    }
+
+    this.video.patch(this.form.value)
+
+    this.videoService.updateVideo(this.video)
+                     .subscribe(
+                       () => {
+                         this.notificationsService.success('Success', 'Video updated.')
+                         this.router.navigate([ '/videos/watch', this.video.uuid ])
+                       },
+
+                       err => {
+                         this.error = 'Cannot update the video.'
+                         console.error(err)
+                       }
+                      )
+
+  }
+
+  private hydrateFormFromVideo () {
+    this.form.patchValue(this.video.toJSON())
+  }
+}
diff --git a/client/src/app/videos/+video-edit/video-update.module.ts b/client/src/app/videos/+video-edit/video-update.module.ts
new file mode 100644 (file)
index 0000000..eeb2e35
--- /dev/null
@@ -0,0 +1,30 @@
+import { NgModule } from '@angular/core'
+
+import { TagInputModule } from 'ngx-chips'
+
+import { VideoUpdateRoutingModule } from './video-update-routing.module'
+import { VideoUpdateComponent } from './video-update.component'
+import { VideoService } from '../shared'
+import { SharedModule } from '../../shared'
+
+@NgModule({
+  imports: [
+    TagInputModule,
+
+    VideoUpdateRoutingModule,
+    SharedModule
+  ],
+
+  declarations: [
+    VideoUpdateComponent
+  ],
+
+  exports: [
+    VideoUpdateComponent
+  ],
+
+  providers: [
+    VideoService
+  ]
+})
+export class VideoUpdateModule { }
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..b19bfdb
--- /dev/null
@@ -0,0 +1 @@
+export * from './video-watch.module'
diff --git a/client/src/app/videos/+video-watch/video-magnet.component.html b/client/src/app/videos/+video-watch/video-magnet.component.html
new file mode 100644 (file)
index 0000000..484280c
--- /dev/null
@@ -0,0 +1,20 @@
+<div bsModal #modal="bs-modal" class="modal" tabindex="-1">
+  <div class="modal-dialog">
+    <div class="modal-content modal-lg">
+
+      <div class="modal-header">
+        <button type="button" class="close" aria-label="Close" (click)="hide()">
+          <span aria-hidden="true">&times;</span>
+        </button>
+        <h4 class="modal-title">Magnet Uri</h4>
+      </div>
+
+      <div class="modal-body">
+        <div *ngFor="let file of video.files">
+          <label>{{ file.resolutionLabel }}</label>
+          <input #magnetUriInput (click)="magnetUriInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="file.magnetUri" />
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
diff --git a/client/src/app/videos/+video-watch/video-magnet.component.ts b/client/src/app/videos/+video-watch/video-magnet.component.ts
new file mode 100644 (file)
index 0000000..f9432e9
--- /dev/null
@@ -0,0 +1,27 @@
+import { Component, Input, ViewChild } from '@angular/core'
+
+import { ModalDirective } from 'ngx-bootstrap/modal'
+
+import { Video } from '../shared'
+
+@Component({
+  selector: 'my-video-magnet',
+  templateUrl: './video-magnet.component.html'
+})
+export class VideoMagnetComponent {
+  @Input() video: Video = null
+
+  @ViewChild('modal') modal: ModalDirective
+
+  constructor () {
+    // empty
+  }
+
+  show () {
+    this.modal.show()
+  }
+
+  hide () {
+    this.modal.hide()
+  }
+}
diff --git a/client/src/app/videos/+video-watch/video-report.component.html b/client/src/app/videos/+video-watch/video-report.component.html
new file mode 100644 (file)
index 0000000..741080e
--- /dev/null
@@ -0,0 +1,38 @@
+<div bsModal #modal="bs-modal" class="modal" tabindex="-1">
+  <div class="modal-dialog">
+    <div class="modal-content modal-lg">
+
+      <div class="modal-header">
+        <button type="button" class="close" aria-label="Close" (click)="hide()">
+          <span aria-hidden="true">&times;</span>
+        </button>
+        <h4 class="modal-title">Report video</h4>
+      </div>
+
+      <div class="modal-body">
+
+        <form novalidate [formGroup]="form">
+          <div class="form-group">
+            <label for="description">Reason</label>
+            <textarea
+              id="reason" class="form-control" placeholder="Reason..."
+              formControlName="reason"
+            >
+            </textarea>
+            <div *ngIf="formErrors.reason" class="alert alert-danger">
+              {{ formErrors.reason }}
+            </div>
+          </div>
+
+          <div class="form-group">
+            <input
+              type="button" value="Report" class="btn btn-default form-control"
+              [disabled]="!form.valid" (click)="report()"
+            >
+          </div>
+        </form>
+
+      </div>
+    </div>
+  </div>
+</div>
diff --git a/client/src/app/videos/+video-watch/video-report.component.ts b/client/src/app/videos/+video-watch/video-report.component.ts
new file mode 100644 (file)
index 0000000..d9c83a6
--- /dev/null
@@ -0,0 +1,69 @@
+import { Component, Input, OnInit, ViewChild } from '@angular/core'
+import { FormBuilder, FormGroup } from '@angular/forms'
+
+import { ModalDirective } from 'ngx-bootstrap/modal'
+import { NotificationsService } from 'angular2-notifications'
+
+import { FormReactive, VideoAbuseService, VIDEO_ABUSE_REASON } from '../../shared'
+import { Video, VideoService } from '../shared'
+
+@Component({
+  selector: 'my-video-report',
+  templateUrl: './video-report.component.html'
+})
+export class VideoReportComponent extends FormReactive implements OnInit {
+  @Input() video: Video = null
+
+  @ViewChild('modal') modal: ModalDirective
+
+  error: string = null
+  form: FormGroup
+  formErrors = {
+    reason: ''
+  }
+  validationMessages = {
+    reason: VIDEO_ABUSE_REASON.MESSAGES
+  }
+
+  constructor (
+    private formBuilder: FormBuilder,
+    private videoAbuseService: VideoAbuseService,
+    private notificationsService: NotificationsService
+   ) {
+    super()
+  }
+
+  ngOnInit () {
+    this.buildForm()
+  }
+
+  buildForm () {
+    this.form = this.formBuilder.group({
+      reason: [ '', VIDEO_ABUSE_REASON.VALIDATORS ]
+    })
+
+    this.form.valueChanges.subscribe(data => this.onValueChanged(data))
+  }
+
+  show () {
+    this.modal.show()
+  }
+
+  hide () {
+    this.modal.hide()
+  }
+
+  report () {
+    const reason = this.form.value['reason']
+
+    this.videoAbuseService.reportVideo(this.video.id, reason)
+                          .subscribe(
+                            () => {
+                              this.notificationsService.success('Success', 'Video reported.')
+                              this.hide()
+                            },
+
+                            err => this.notificationsService.error('Error', err.message)
+                           )
+  }
+}
diff --git a/client/src/app/videos/+video-watch/video-share.component.html b/client/src/app/videos/+video-watch/video-share.component.html
new file mode 100644 (file)
index 0000000..88f59c0
--- /dev/null
@@ -0,0 +1,29 @@
+<div bsModal #modal="bs-modal" class="modal" tabindex="-1">
+  <div class="modal-dialog modal-lg">
+    <div class="modal-content">
+
+      <div class="modal-header">
+        <button type="button" class="close" aria-label="Close" (click)="hide()">
+          <span aria-hidden="true">&times;</span>
+        </button>
+        <h4 class="modal-title">Share</h4>
+      </div>
+
+      <div class="modal-body">
+        <div class="form-group">
+          <label>URL</label>
+          <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getVideoUrl()" />
+        </div>
+
+        <div class="form-group">
+          <label>Embed</label>
+          <input #shareInput (click)="shareInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getVideoIframeCode()" />
+        </div>
+
+        <div *ngIf="notSecure()" class="alert alert-warning">
+          The url is not secured (no HTTPS), so the embed video won't work on HTTPS websites (web browsers block non secured HTTP requests on HTTPS websites).
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
diff --git a/client/src/app/videos/+video-watch/video-share.component.ts b/client/src/app/videos/+video-watch/video-share.component.ts
new file mode 100644 (file)
index 0000000..133f934
--- /dev/null
@@ -0,0 +1,42 @@
+import { Component, Input, ViewChild } from '@angular/core'
+
+import { ModalDirective } from 'ngx-bootstrap/modal'
+
+import { Video } from '../shared'
+
+@Component({
+  selector: 'my-video-share',
+  templateUrl: './video-share.component.html'
+})
+export class VideoShareComponent {
+  @Input() video: Video = null
+
+  @ViewChild('modal') modal: ModalDirective
+
+  constructor () {
+    // empty
+  }
+
+  show () {
+    this.modal.show()
+  }
+
+  hide () {
+    this.modal.hide()
+  }
+
+  getVideoIframeCode () {
+    return '<iframe width="560" height="315" ' +
+           'src="' + window.location.origin + '/videos/embed/' + this.video.uuid + '" ' +
+           'frameborder="0" allowfullscreen>' +
+           '</iframe>'
+  }
+
+  getVideoUrl () {
+    return window.location.href
+  }
+
+  notSecure () {
+    return window.location.protocol === 'http:'
+  }
+}
diff --git a/client/src/app/videos/+video-watch/video-watch-routing.module.ts b/client/src/app/videos/+video-watch/video-watch-routing.module.ts
new file mode 100644 (file)
index 0000000..97fa5c7
--- /dev/null
@@ -0,0 +1,20 @@
+import { NgModule } from '@angular/core'
+import { RouterModule, Routes } from '@angular/router'
+
+import { MetaGuard } from '@ngx-meta/core'
+
+import { VideoWatchComponent } from './video-watch.component'
+
+const videoWatchRoutes: Routes = [
+  {
+    path: '',
+    component: VideoWatchComponent,
+    canActivateChild: [ MetaGuard ]
+  }
+]
+
+@NgModule({
+  imports: [ RouterModule.forChild(videoWatchRoutes) ],
+  exports: [ RouterModule ]
+})
+export class VideoWatchRoutingModule {}
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..8886313
--- /dev/null
@@ -0,0 +1,184 @@
+<div *ngIf="error" class="row">
+  <div class="alert alert-danger">
+    The video load seems to be abnormally long.
+    <ul>
+      <li>Maybe the server {{ video.podHost }} is down :(</li>
+      <li>
+        If not, you can report an issue on
+        <a href="https://github.com/Chocobozzz/PeerTube/issues" title="Report an issue">
+          https://github.com/Chocobozzz/PeerTube/issues
+        </a>
+      </li>
+    </ul>
+  </div>
+</div>
+
+<div class="row">
+  <!-- We need the video container for videojs so we just hide it -->
+  <div [hidden]="videoNotFound" class="embed-responsive embed-responsive-19by9">
+     <video id="video-container" class="video-js vjs-sublime-skin"></video>
+  </div>
+
+  <div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div>
+</div>
+
+<!-- P2P informations -->
+<div id="torrent-info" class="row">
+  <div id="torrent-info-download" class="col-md-4 col-sm-4 col-xs-4">Download: {{ downloadSpeed | bytes }}/s</div>
+  <div id="torrent-info-upload" class="col-md-4 col-sm-4 col-xs-4">Upload: {{ uploadSpeed | bytes }}/s</div>
+  <div id="torrent-info-peers" class="col-md-4 col-sm-4 col-xs-4">Number of peers: {{ numPeers }}</div>
+</div>
+
+<!-- Video informations -->
+<div *ngIf="video !== null" id="video-info">
+  <div class="row video-name-views">
+    <div class="col-xs-8 col-md-8 video-name">
+      {{ video.name }}
+    </div>
+
+    <div class="col-xs-4 col-md-4 pull-right video-views">
+      {{ video.views}} views
+    </div>
+  </div>
+
+  <div class="row video-small-blocks">
+    <div class="col-xs-5 col-xs-3 col-md-3 video-small-block video-small-block-author">
+      <a class="option" title="Access to all videos of this user" [routerLink]="['/videos/list', { field: 'author', search: video.author }]">
+        <span class="glyphicon glyphicon-user"></span>
+        <span class="video-small-block-text">{{ video.by }}</span>
+      </a>
+    </div>
+
+    <div class="col-xs-2 col-md-3 video-small-block video-small-block-share">
+      <a class="option" (click)="showShareModal()" title="Share the video">
+        <span class="glyphicon glyphicon-share"></span>
+        <span class="video-small-block-text">Share</span>
+      </a>
+    </div>
+
+    <div class="col-xs-2 col-md-3 video-small-block video-small-block-more">
+      <div class="video-small-block-dropdown" dropdown dropup="true" placement="right">
+        <a class="option" title="Access to more options" dropdownToggle>
+          <span class="glyphicon glyphicon-option-horizontal"></span>
+          <span class="video-small-block-text">More</span>
+        </a>
+
+        <ul *dropdownMenu class="dropdown-menu" id="more-menu" role="menu" aria-labelledby="single-button">
+          <li *ngIf="canUserUpdateVideo()" role="menuitem">
+            <a class="dropdown-item" title="Update this video" href="#" [routerLink]="[ '/videos/edit', video.uuid ]">
+              <span class="glyphicon glyphicon-pencil"></span> Update
+            </a>
+          </li>
+
+          <li role="menuitem">
+            <a class="dropdown-item" title="Get magnet URI" href="#" (click)="showMagnetUriModal($event)">
+              <span class="glyphicon glyphicon-magnet"></span> Magnet
+            </a>
+          </li>
+
+          <li *ngIf="isUserLoggedIn()" role="menuitem">
+            <a class="dropdown-item" title="Report this video" href="#" (click)="showReportModal($event)">
+              <span class="glyphicon glyphicon-alert"></span> Report
+            </a>
+          </li>
+
+          <li *ngIf="isVideoRemovable()" role="menuitem">
+            <a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)">
+              <span class="glyphicon glyphicon-remove"></span> Delete
+            </a>
+          </li>
+
+          <li *ngIf="isVideoBlacklistable()" role="menuitem">
+            <a class="dropdown-item" title="Blacklist this video" href="#" (click)="blacklistVideo($event)">
+              <span class="glyphicon glyphicon-eye-close"></span> Blacklist
+            </a>
+          </li>
+        </ul>
+      </div>
+    </div>
+
+    <div class="col-xs-3 col-md-3 video-small-block video-small-block-rating">
+      <div class="video-small-block-like">
+        <span
+          class="glyphicon glyphicon-thumbs-up" title="Like this video"
+          [ngClass]="{ 'interactive': isUserLoggedIn(), 'activated': userRating === 'like' }" (click)="setLike()"
+        ></span>
+
+        <span class="video-small-block-text">
+          {{ video.likes }}
+        </span>
+      </div>
+
+      <div class="video-small-block-dislike">
+        <span
+          class="glyphicon glyphicon-thumbs-down" title="Dislike this video"
+          [ngClass]="{ 'interactive': isUserLoggedIn(), 'activated': userRating === 'dislike' }" (click)="setDislike()"
+        ></span>
+
+        <span class="video-small-block-text">
+          {{ video.dislikes }}
+        </span>
+      </div>
+    </div>
+  </div>
+
+  <div class="row video-details">
+    <div class="video-details-date-description col-xs-8 col-md-9">
+      <div class="video-details-date">
+        Published on {{ video.createdAt | date:'short' }}
+      </div>
+
+      <div class="video-details-description">
+        {{ video.description }}
+      </div>
+    </div>
+
+    <div class="video-details-attributes col-xs-4 col-md-3">
+      <div class="video-details-attribute">
+        <span class="video-details-attribute-label">
+          Category:
+        </span>
+        <span class="video-details-attribute-value">
+          {{ video.categoryLabel }}
+        </span>
+      </div>
+
+      <div class="video-details-attribute">
+        <span class="video-details-attribute-label">
+          Licence:
+        </span>
+        <span class="video-details-attribute-value">
+          {{ video.licenceLabel }}
+        </span>
+      </div>
+
+      <div class="video-details-attribute">
+        <span class="video-details-attribute-label">
+          Language:
+        </span>
+        <span class="video-details-attribute-value">
+          {{ video.languageLabel }}
+        </span>
+      </div>
+
+      <div class="video-details-attribute">
+        <span class="video-details-attribute-label">
+          Tags:
+        </span>
+
+        <div class="video-details-tags">
+          <a *ngFor="let tag of video.tags" [routerLink]="['/videos/list', { field: 'tags', search: tag }]" class="label label-primary">
+            {{ tag }}
+          </a>
+        </div>
+      </div>
+
+    </div>
+  </div>
+</div>
+
+<ng-template [ngIf]="video !== null">
+  <my-video-share #videoShareModal [video]="video"></my-video-share>
+  <my-video-magnet #videoMagnetModal [video]="video"></my-video-magnet>
+  <my-video-report #videoReportModal [video]="video"></my-video-report>
+</ng-template>
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..6966174
--- /dev/null
@@ -0,0 +1,245 @@
+#video-container {
+  width: 100%;
+  height: 100%;
+}
+
+#video-not-found {
+  height: 300px;
+  line-height: 300px;
+  margin-top: 50px;
+  text-align: center;
+  font-weight: bold;
+}
+
+.embed-responsive {
+  height: 500px;
+
+  @media screen and (max-width: 600px) {
+    height: 300px;
+  }
+}
+
+#torrent-info {
+  font-size: 10px;
+  margin-top: 10px;
+  text-align: center;
+
+  div {
+    min-width: 60px;
+  }
+}
+
+#video-info {
+  .video-name-views {
+    font-weight: bold;
+    font-size: 18px;
+    height: $video-watch-title-height;
+    line-height: $video-watch-title-height;
+
+    .video-name {
+      padding-left: $video-watch-info-padding-left;
+    }
+
+    .video-views {
+      text-align: right;
+      // Keep a symmetry with the video name
+      padding-right: $video-watch-info-padding-left
+    }
+
+  }
+
+  .video-small-blocks {
+    height: $video-watch-info-height;
+    color: $video-watch-info-color;
+    border-color: $video-watch-border-color;
+    border-width: 1px 0px;
+    border-style: solid;
+
+    .video-small-block {
+      height: $video-watch-info-height;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      text-align: center;
+
+      a {
+        cursor: pointer;
+        transition: color 0.3s;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+
+        &, &:hover {
+          color: inherit;
+          text-decoration:none;
+        }
+
+        &:hover {
+          color: #000 !important;
+        }
+
+        &:hover > .glyphicon {
+          opacity: 1 !important;
+        }
+      }
+
+      .option .glyphicon {
+        font-size: 22px;
+        color: inherit;
+        opacity: 0.15;
+        margin-bottom: 10px;
+        transition: opacity 0.3s;
+      }
+
+      .video-small-block-text {
+        font-size: 15px;
+        font-weight: bold;
+      }
+    }
+
+    .video-small-block:not(:last-child) {
+      border-width: 0 1px 0 0;
+      border-color: $video-watch-border-color;
+      border-style: solid;
+    }
+
+    .video-small-block-author, .video-small-block-more {
+      a.option {
+        display: block;
+
+        .glyphicon {
+          display: block;
+        }
+      }
+    }
+
+    .video-small-block-share, .video-small-block-more {
+      a.option {
+        display: block;
+
+        .glyphicon {
+          display: block;
+        }
+      }
+    }
+
+    .video-small-block-more .video-small-block-dropdown {
+      position: relative;
+
+      .dropdown-item .glyphicon {
+        margin-right: 5px;
+      }
+    }
+
+    .video-small-block-rating {
+
+      .video-small-block-like {
+        margin-bottom: 10px;
+      }
+
+      .video-small-block-text {
+        vertical-align: top;
+      }
+
+      .glyphicon {
+        font-size: 18px;
+        margin: 0 10px 0 0;
+        opacity: 0.3;
+      }
+
+      .interactive {
+        cursor: pointer;
+        transition: opacity, color 0.3s;
+
+        &.activated, &:hover {
+          opacity: 1;
+          color: #000;
+        }
+      }
+    }
+  }
+
+  .video-details {
+    margin-top: 30px;
+
+    .video-details-date-description {
+      padding-left: $video-watch-info-padding-left;
+
+      .video-details-date {
+        font-weight: bold;
+        margin-bottom: 30px;
+      }
+    }
+
+    .video-details-attributes {
+      font-weight: bold;
+      font-size: 12px;
+
+      .video-details-attribute-label {
+        color: $video-watch-info-color;
+        display: inline-block;
+        width: 60px;
+        margin-right: 5px;
+      }
+    }
+
+    .video-details-tags {
+      display: inline-block;
+
+      a {
+        display: inline-block;
+        margin-right: 3px;
+        font-size: 11px;
+      }
+    }
+  }
+
+  @media screen and (max-width: 400px) {
+    .video-name-views {
+      font-size: 16px !important;
+    }
+  }
+
+  @media screen and (max-width: 800px) {
+    .video-name-views {
+      .video-name {
+        padding-left: 5px;
+        padding-right: 0px;
+      }
+
+      .video-views {
+        padding-left: 0px;
+        padding-right: 5px;
+      }
+    }
+
+    .video-small-blocks {
+      a, .video-small-block-text {
+        font-size: 13px !important;
+      }
+
+      .glyphicon {
+        font-size: 18px !important;
+      }
+
+      .video-small-block-author {
+        padding-left: 10px;
+      }
+    }
+
+    .video-details {
+      .video-details-date-description {
+        padding-left: 10px;
+        font-size: 13px !important;
+      }
+
+      .video-details-attributes {
+        font-size: 11px !important;
+
+        .video-details-attribute-label {
+          width: 50px;
+        }
+      }
+    }
+  }
+}
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..874dd59
--- /dev/null
@@ -0,0 +1,299 @@
+import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
+import { Observable } from 'rxjs/Observable'
+import { Subscription } from 'rxjs/Subscription'
+
+import videojs from 'video.js'
+import '../../../assets/player/peertube-videojs-plugin'
+
+import { MetaService } from '@ngx-meta/core'
+import { NotificationsService } from 'angular2-notifications'
+
+import { AuthService, ConfirmService } from '../../core'
+import { VideoMagnetComponent } from './video-magnet.component'
+import { VideoShareComponent } from './video-share.component'
+import { VideoReportComponent } from './video-report.component'
+import { Video, VideoService } from '../shared'
+import { WebTorrentService } from './webtorrent.service'
+import { UserVideoRateType, VideoRateType } from '../../../../../shared'
+
+@Component({
+  selector: 'my-video-watch',
+  templateUrl: './video-watch.component.html',
+  styleUrls: [ './video-watch.component.scss' ]
+})
+export class VideoWatchComponent implements OnInit, OnDestroy {
+  @ViewChild('videoMagnetModal') videoMagnetModal: VideoMagnetComponent
+  @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
+  @ViewChild('videoReportModal') videoReportModal: VideoReportComponent
+
+  downloadSpeed: number
+  error = false
+  loading = false
+  numPeers: number
+  player: videojs.Player
+  playerElement: HTMLMediaElement
+  uploadSpeed: number
+  userRating: UserVideoRateType = null
+  video: Video = null
+  videoNotFound = false
+
+  private paramsSub: Subscription
+
+  constructor (
+    private elementRef: ElementRef,
+    private route: ActivatedRoute,
+    private router: Router,
+    private videoService: VideoService,
+    private confirmService: ConfirmService,
+    private metaService: MetaService,
+    private authService: AuthService,
+    private notificationsService: NotificationsService
+  ) {}
+
+  ngOnInit () {
+    this.paramsSub = this.route.params.subscribe(routeParams => {
+      let uuid = routeParams['uuid']
+      this.videoService.getVideo(uuid).subscribe(
+        video => this.onVideoFetched(video),
+
+        error => {
+          console.error(error)
+          this.videoNotFound = true
+        }
+      )
+    })
+  }
+
+  ngOnDestroy () {
+    // Remove player if it exists
+    if (this.videoNotFound === false) {
+      videojs(this.playerElement).dispose()
+    }
+
+    // Unsubscribe subscriptions
+    this.paramsSub.unsubscribe()
+  }
+
+  setLike () {
+    if (this.isUserLoggedIn() === false) return
+    // Already liked this video
+    if (this.userRating === 'like') return
+
+    this.videoService.setVideoLike(this.video.id)
+                     .subscribe(
+                      () => {
+                        // Update the video like attribute
+                        this.updateVideoRating(this.userRating, 'like')
+                        this.userRating = 'like'
+                      },
+
+                      err => this.notificationsService.error('Error', err.message)
+                     )
+  }
+
+  setDislike () {
+    if (this.isUserLoggedIn() === false) return
+    // Already disliked this video
+    if (this.userRating === 'dislike') return
+
+    this.videoService.setVideoDislike(this.video.id)
+                     .subscribe(
+                      () => {
+                        // Update the video dislike attribute
+                        this.updateVideoRating(this.userRating, 'dislike')
+                        this.userRating = 'dislike'
+                      },
+
+                      err => this.notificationsService.error('Error', err.message)
+                     )
+  }
+
+  removeVideo (event: Event) {
+    event.preventDefault()
+
+    this.confirmService.confirm('Do you really want to delete this video?', 'Delete').subscribe(
+      res => {
+        if (res === false) return
+
+        this.videoService.removeVideo(this.video.id)
+                         .subscribe(
+                           status => {
+                             this.notificationsService.success('Success', `Video ${this.video.name} deleted.`)
+                             // Go back to the video-list.
+                             this.router.navigate(['/videos/list'])
+                           },
+
+                           error => this.notificationsService.error('Error', error.text)
+                          )
+      }
+    )
+  }
+
+  blacklistVideo (event: Event) {
+    event.preventDefault()
+
+    this.confirmService.confirm('Do you really want to blacklist this video ?', 'Blacklist').subscribe(
+      res => {
+        if (res === false) return
+
+        this.videoService.blacklistVideo(this.video.id)
+                         .subscribe(
+                           status => {
+                             this.notificationsService.success('Success', `Video ${this.video.name} had been blacklisted.`)
+                             this.router.navigate(['/videos/list'])
+                           },
+
+                           error => this.notificationsService.error('Error', error.text)
+                         )
+      }
+    )
+  }
+
+  showReportModal (event: Event) {
+    event.preventDefault()
+    this.videoReportModal.show()
+  }
+
+  showShareModal () {
+    this.videoShareModal.show()
+  }
+
+  showMagnetUriModal (event: Event) {
+    event.preventDefault()
+    this.videoMagnetModal.show()
+  }
+
+  isUserLoggedIn () {
+    return this.authService.isLoggedIn()
+  }
+
+  canUserUpdateVideo () {
+    return this.video.isUpdatableBy(this.authService.getUser())
+  }
+
+  isVideoRemovable () {
+    return this.video.isRemovableBy(this.authService.getUser())
+  }
+
+  isVideoBlacklistable () {
+    return this.video.isBlackistableBy(this.authService.getUser())
+  }
+
+  private handleError (err: any) {
+    const errorMessage: string = typeof err === 'string' ? err : err.message
+    let message = ''
+
+    if (errorMessage.indexOf('http error') !== -1) {
+      message = 'Cannot fetch video from server, maybe down.'
+    } else {
+      message = errorMessage
+    }
+
+    this.notificationsService.error('Error', message)
+  }
+
+  private checkUserRating () {
+    // Unlogged users do not have ratings
+    if (this.isUserLoggedIn() === false) return
+
+    this.videoService.getUserVideoRating(this.video.id)
+                     .subscribe(
+                       ratingObject => {
+                         if (ratingObject) {
+                           this.userRating = ratingObject.rating
+                         }
+                       },
+
+                       err => this.notificationsService.error('Error', err.message)
+                      )
+  }
+
+  private onVideoFetched (video: Video) {
+    this.video = video
+
+    let observable
+    if (this.video.isVideoNSFWForUser(this.authService.getUser())) {
+      observable = this.confirmService.confirm('This video is not safe for work. Are you sure you want to watch it?', 'NSFW')
+    } else {
+      observable = Observable.of(true)
+    }
+
+    observable.subscribe(
+      res => {
+        if (res === false) {
+          return this.router.navigate([ '/videos/list' ])
+        }
+
+        this.playerElement = this.elementRef.nativeElement.querySelector('#video-container')
+
+        const videojsOptions = {
+          controls: true,
+          autoplay: true,
+          plugins: {
+            peertube: {
+              videoFiles: this.video.files,
+              playerElement: this.playerElement,
+              autoplay: true,
+              peerTubeLink: false
+            }
+          }
+        }
+
+        const self = this
+        videojs(this.playerElement, videojsOptions, function () {
+          self.player = this
+          this.on('customError', (event, data) => {
+            self.handleError(data.err)
+          })
+
+          this.on('torrentInfo', (event, data) => {
+            self.downloadSpeed = data.downloadSpeed
+            self.numPeers = data.numPeers
+            self.uploadSpeed = data.uploadSpeed
+          })
+        })
+
+        this.setOpenGraphTags()
+        this.checkUserRating()
+      }
+    )
+  }
+
+  private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) {
+    let likesToIncrement = 0
+    let dislikesToIncrement = 0
+
+    if (oldRating) {
+      if (oldRating === 'like') likesToIncrement--
+      if (oldRating === 'dislike') dislikesToIncrement--
+    }
+
+    if (newRating === 'like') likesToIncrement++
+    if (newRating === 'dislike') dislikesToIncrement++
+
+    this.video.likes += likesToIncrement
+    this.video.dislikes += dislikesToIncrement
+  }
+
+  private setOpenGraphTags () {
+    this.metaService.setTitle(this.video.name)
+
+    this.metaService.setTag('og:type', 'video')
+
+    this.metaService.setTag('og:title', this.video.name)
+    this.metaService.setTag('name', this.video.name)
+
+    this.metaService.setTag('og:description', this.video.description)
+    this.metaService.setTag('description', this.video.description)
+
+    this.metaService.setTag('og:image', this.video.previewPath)
+
+    this.metaService.setTag('og:duration', this.video.duration.toString())
+
+    this.metaService.setTag('og:site_name', 'PeerTube')
+
+    this.metaService.setTag('og:url', window.location.href)
+    this.metaService.setTag('url', window.location.href)
+  }
+}
diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts
new file mode 100644 (file)
index 0000000..5f20b17
--- /dev/null
@@ -0,0 +1,34 @@
+import { NgModule } from '@angular/core'
+
+import { VideoWatchRoutingModule } from './video-watch-routing.module'
+import { VideoService } from '../shared'
+import { SharedModule } from '../../shared'
+
+import { VideoWatchComponent } from './video-watch.component'
+import { VideoReportComponent } from './video-report.component'
+import { VideoShareComponent } from './video-share.component'
+import { VideoMagnetComponent } from './video-magnet.component'
+
+@NgModule({
+  imports: [
+    VideoWatchRoutingModule,
+    SharedModule
+  ],
+
+  declarations: [
+    VideoWatchComponent,
+
+    VideoMagnetComponent,
+    VideoShareComponent,
+    VideoReportComponent
+  ],
+
+  exports: [
+    VideoWatchComponent
+  ],
+
+  providers: [
+    VideoService
+  ]
+})
+export class VideoWatchModule { }
index 83edcc75868e6e7ff8e3370eb9eff146b275f192..028a5854b09374bf3e24b69cdd7f4c5d215d9b17 100644 (file)
@@ -1,7 +1 @@
-export * from './shared'
-export * from './video-edit'
-export * from './video-list'
-export * from './video-watch'
-export * from './videos-routing.module'
-export * from './videos.component'
 export * from './videos.module'
diff --git a/client/src/app/videos/video-edit/index.ts b/client/src/app/videos/video-edit/index.ts
deleted file mode 100644 (file)
index 3b4a9cb..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './video-add.component'
-export * from './video-update.component'
diff --git a/client/src/app/videos/video-edit/video-add.component.html b/client/src/app/videos/video-edit/video-add.component.html
deleted file mode 100644 (file)
index 698152f..0000000
+++ /dev/null
@@ -1,121 +0,0 @@
-<div class="row">
-  <div class="content-padding">
-
-    <h3>Upload a video</h3>
-
-    <div *ngIf="error !== undefined" class="alert alert-danger">{{ error }}</div>
-
-    <form novalidate [formGroup]="form">
-      <div class="form-group">
-        <label for="name">Name</label>
-        <input
-          type="text" class="form-control" id="name"
-          formControlName="name"
-        >
-        <div *ngIf="formErrors.name" class="alert alert-danger">
-          {{ formErrors.name }}
-        </div>
-      </div>
-
-      <div class="form-group">
-        <label for="nsfw">NSFW</label>
-        <input
-          type="checkbox" id="nsfw"
-          formControlName="nsfw"
-        >
-      </div>
-
-      <div class="form-group">
-        <label for="category">Category</label>
-        <select class="form-control" id="category" formControlName="category">
-          <option></option>
-          <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option>
-        </select>
-
-        <div *ngIf="formErrors.category" class="alert alert-danger">
-          {{ formErrors.category }}
-        </div>
-      </div>
-
-      <div class="form-group">
-        <label for="licence">Licence</label>
-        <select class="form-control" id="licence" formControlName="licence">
-          <option></option>
-          <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option>
-        </select>
-
-        <div *ngIf="formErrors.licence" class="alert alert-danger">
-          {{ formErrors.licence }}
-        </div>
-      </div>
-
-      <div class="form-group">
-        <label for="language">Language</label>
-        <select class="form-control" id="language" formControlName="language">
-          <option></option>
-          <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option>
-        </select>
-
-        <div *ngIf="formErrors.language" class="alert alert-danger">
-          {{ formErrors.language }}
-        </div>
-      </div>
-
-      <div class="form-group">
-        <label class="label-tags">Tags</label> <span class="little-information">(press enter to add the tag)</span>
-        <tag-input
-          [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
-          formControlName="tags" maxItems="3" modelAsStrings="true"
-        ></tag-input>
-      </div>
-
-      <div class="form-group">
-        <label for="videofile">File</label>
-        <div class="btn btn-default btn-file">
-          <span>Select the video...</span>
-          <input #videofileInput type="file" name="videofile" id="videofile" (change)="fileChange($event)" />
-          <input type="hidden" name="videofileHidden" formControlName="videofile"/>
-        </div>
-      </div>
-
-      <div class="file-to-upload">
-        <div class="file" *ngIf="filename">
-          <span class="filename">{{ filename }}</span>
-          <span class="glyphicon glyphicon-remove" (click)="removeFile()"></span>
-        </div>
-      </div>
-
-      <div *ngIf="formErrors.videofile" class="alert alert-danger">
-        {{ formErrors.videofile }}
-      </div>
-
-      <div class="form-group">
-        <label for="description">Description</label>
-        <textarea
-          id="description" class="form-control" placeholder="Description..."
-          formControlName="description"
-        >
-        </textarea>
-        <div *ngIf="formErrors.description" class="alert alert-danger">
-          {{ formErrors.description }}
-        </div>
-      </div>
-
-      <div class="progress">
-        <progressbar [value]="progressPercent" max="100">
-          <ng-template [ngIf]="progressPercent === 100">
-            <span class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></span>
-            Server is processing the video
-          </ng-template>
-        </progressbar>
-      </div>
-
-      <div class="form-group">
-        <input
-          type="button" value="Upload" class="btn btn-default form-control"
-          (click)="upload()"
-        >
-      </div>
-    </form>
-  </div>
-</div>
diff --git a/client/src/app/videos/video-edit/video-add.component.ts b/client/src/app/videos/video-edit/video-add.component.ts
deleted file mode 100644 (file)
index 21311b1..0000000
+++ /dev/null
@@ -1,165 +0,0 @@
-import { Component, OnInit, ViewChild } from '@angular/core'
-import { FormBuilder, FormGroup } from '@angular/forms'
-import { Router } from '@angular/router'
-
-import { NotificationsService } from 'angular2-notifications'
-
-import {
-  FormReactive,
-  VIDEO_NAME,
-  VIDEO_CATEGORY,
-  VIDEO_LICENCE,
-  VIDEO_LANGUAGE,
-  VIDEO_DESCRIPTION,
-  VIDEO_TAGS
-} from '../../shared'
-import { VideoService } from '../shared'
-import { VideoCreate } from '../../../../../shared'
-import { HttpEventType, HttpResponse } from '@angular/common/http'
-import { VIDEO_FILE } from '../../shared/forms/form-validators/video'
-
-@Component({
-  selector: 'my-videos-add',
-  styleUrls: [ './video-edit.component.scss' ],
-  templateUrl: './video-add.component.html'
-})
-
-export class VideoAddComponent extends FormReactive implements OnInit {
-  @ViewChild('videofileInput') videofileInput
-
-  progressPercent = 0
-  tags: string[] = []
-  videoCategories = []
-  videoLicences = []
-  videoLanguages = []
-
-  tagValidators = VIDEO_TAGS.VALIDATORS
-  tagValidatorsMessages = VIDEO_TAGS.MESSAGES
-
-  error: string
-  form: FormGroup
-  formErrors = {
-    name: '',
-    category: '',
-    licence: '',
-    language: '',
-    description: '',
-    videofile: ''
-  }
-  validationMessages = {
-    name: VIDEO_NAME.MESSAGES,
-    category: VIDEO_CATEGORY.MESSAGES,
-    licence: VIDEO_LICENCE.MESSAGES,
-    language: VIDEO_LANGUAGE.MESSAGES,
-    description: VIDEO_DESCRIPTION.MESSAGES,
-    videofile: VIDEO_FILE.MESSAGES
-  }
-
-  constructor (
-    private formBuilder: FormBuilder,
-    private router: Router,
-    private notificationsService: NotificationsService,
-    private videoService: VideoService
-  ) {
-    super()
-  }
-
-  get filename () {
-    return this.form.value['videofile']
-  }
-
-  buildForm () {
-    this.form = this.formBuilder.group({
-      name: [ '', VIDEO_NAME.VALIDATORS ],
-      nsfw: [ false ],
-      category: [ '', VIDEO_CATEGORY.VALIDATORS ],
-      licence: [ '', VIDEO_LICENCE.VALIDATORS ],
-      language: [ '', VIDEO_LANGUAGE.VALIDATORS ],
-      description: [ '', VIDEO_DESCRIPTION.VALIDATORS ],
-      videofile: [ '', VIDEO_FILE.VALIDATORS ],
-      tags: [ '' ]
-    })
-
-    this.form.valueChanges.subscribe(data => this.onValueChanged(data))
-  }
-
-  ngOnInit () {
-    this.videoCategories = this.videoService.videoCategories
-    this.videoLicences = this.videoService.videoLicences
-    this.videoLanguages = this.videoService.videoLanguages
-
-    this.buildForm()
-  }
-
-  // The goal is to keep reactive form validation (required field)
-  // https://stackoverflow.com/a/44238894
-  fileChange ($event) {
-    this.form.controls['videofile'].setValue($event.target.files[0].name)
-  }
-
-  removeFile () {
-    this.videofileInput.nativeElement.value = ''
-    this.form.controls['videofile'].setValue('')
-  }
-
-  checkForm () {
-    this.forceCheck()
-
-    return this.form.valid
-  }
-
-  upload () {
-    if (this.checkForm() === false) {
-      return
-    }
-
-    const formValue: VideoCreate = this.form.value
-
-    const name = formValue.name
-    const nsfw = formValue.nsfw
-    const category = formValue.category
-    const licence = formValue.licence
-    const language = formValue.language
-    const description = formValue.description
-    const tags = formValue.tags
-    const videofile = this.videofileInput.nativeElement.files[0]
-
-    const formData = new FormData()
-    formData.append('name', name)
-    formData.append('category', '' + category)
-    formData.append('nsfw', '' + nsfw)
-    formData.append('licence', '' + licence)
-    formData.append('videofile', videofile)
-
-    // Language is optional
-    if (language) {
-      formData.append('language', '' + language)
-    }
-
-    formData.append('description', description)
-
-    for (let i = 0; i < tags.length; i++) {
-      formData.append(`tags[${i}]`, tags[i])
-    }
-
-    this.videoService.uploadVideo(formData).subscribe(
-      event => {
-        if (event.type === HttpEventType.UploadProgress) {
-          this.progressPercent = Math.round(100 * event.loaded / event.total)
-        } else if (event instanceof HttpResponse) {
-          console.log('Video uploaded.')
-          this.notificationsService.success('Success', 'Video uploaded.')
-
-          // Display all the videos once it's finished
-          this.router.navigate([ '/videos/list' ])
-        }
-      },
-
-      err => {
-        // Reset progress
-        this.progressPercent = 0
-        this.error = err.message
-      }
-    )
-  }
-}
diff --git a/client/src/app/videos/video-edit/video-edit.component.scss b/client/src/app/videos/video-edit/video-edit.component.scss
deleted file mode 100644 (file)
index 9ee0c52..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-.btn-file {
-  position: relative;
-  overflow: hidden;
-  display: block;
-}
-
-.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;
-}
-
-.form-group {
-  margin-bottom: 10px;
-}
-
-div.tags {
-  height: 40px;
-  font-size: 20px;
-  margin-top: 20px;
-
-  .tag {
-    margin-right: 10px;
-
-    .remove {
-      cursor: pointer;
-    }
-  }
-}
-
-div.file-to-upload {
-  height: 40px;
-
-  .glyphicon-remove {
-    cursor: pointer;
-  }
-}
-
-.little-information {
-  font-size: 0.8em;
-  font-style: italic;
-}
-
-.label-tags {
-  margin-bottom: 0;
-}
diff --git a/client/src/app/videos/video-edit/video-update.component.html b/client/src/app/videos/video-edit/video-update.component.html
deleted file mode 100644 (file)
index 7f4faf2..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-<div class="row">
-  <div class="content-padding">
-
-  <h3>Update {{ video?.name }}</h3>
-
-  <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
-
-  <form novalidate [formGroup]="form">
-    <div class="form-group">
-      <label for="name">Name</label>
-      <input
-        type="text" class="form-control" id="name"
-        formControlName="name"
-      >
-      <div *ngIf="formErrors.name" class="alert alert-danger">
-        {{ formErrors.name }}
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="nsfw">NSFW</label>
-      <input
-        type="checkbox" id="nsfw"
-        formControlName="nsfw"
-      >
-    </div>
-
-    <div class="form-group">
-      <label for="category">Category</label>
-      <select class="form-control" id="category" formControlName="category">
-        <option></option>
-        <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option>
-      </select>
-
-      <div *ngIf="formErrors.category" class="alert alert-danger">
-        {{ formErrors.category }}
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="licence">Licence</label>
-      <select class="form-control" id="licence" formControlName="licence">
-        <option></option>
-        <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option>
-      </select>
-
-      <div *ngIf="formErrors.licence" class="alert alert-danger">
-        {{ formErrors.licence }}
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="language">Language</label>
-      <select class="form-control" id="language" formControlName="language">
-        <option></option>
-        <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option>
-      </select>
-
-      <div *ngIf="formErrors.language" class="alert alert-danger">
-        {{ formErrors.language }}
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="tags" class="label-tags">Tags</label> <span class="little-information">(press enter to add the tag)</span>
-      <tag-input
-        [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
-        formControlName="tags" maxItems="3" modelAsStrings="true"
-      ></tag-input>
-    </div>
-
-    <div class="form-group">
-      <label for="description">Description</label>
-      <textarea
-        id="description" class="form-control" placeholder="Description..."
-        formControlName="description"
-      >
-      </textarea>
-      <div *ngIf="formErrors.description" class="alert alert-danger">
-        {{ formErrors.description }}
-      </div>
-    </div>
-
-    <div class="form-group">
-      <input
-        type="button" value="Update" class="btn btn-default form-control"
-        (click)="update()"
-      >
-    </div>
-  </form>
-  </div>
-</div>
diff --git a/client/src/app/videos/video-edit/video-update.component.ts b/client/src/app/videos/video-edit/video-update.component.ts
deleted file mode 100644 (file)
index 141ed35..0000000
+++ /dev/null
@@ -1,134 +0,0 @@
-import { Component, ElementRef, OnInit } from '@angular/core'
-import { FormBuilder, FormGroup } from '@angular/forms'
-import { ActivatedRoute, Router } from '@angular/router'
-
-import { NotificationsService } from 'angular2-notifications'
-
-import { AuthService } from '../../core'
-import {
-  FormReactive,
-  VIDEO_NAME,
-  VIDEO_CATEGORY,
-  VIDEO_LICENCE,
-  VIDEO_LANGUAGE,
-  VIDEO_DESCRIPTION,
-  VIDEO_TAGS
-} from '../../shared'
-import { Video, VideoService } from '../shared'
-
-@Component({
-  selector: 'my-videos-update',
-  styleUrls: [ './video-edit.component.scss' ],
-  templateUrl: './video-update.component.html'
-})
-
-export class VideoUpdateComponent extends FormReactive implements OnInit {
-  tags: string[] = []
-  videoCategories = []
-  videoLicences = []
-  videoLanguages = []
-  video: Video
-
-  tagValidators = VIDEO_TAGS.VALIDATORS
-  tagValidatorsMessages = VIDEO_TAGS.MESSAGES
-
-  error: string = null
-  form: FormGroup
-  formErrors = {
-    name: '',
-    category: '',
-    licence: '',
-    language: '',
-    description: ''
-  }
-  validationMessages = {
-    name: VIDEO_NAME.MESSAGES,
-    category: VIDEO_CATEGORY.MESSAGES,
-    licence: VIDEO_LICENCE.MESSAGES,
-    language: VIDEO_LANGUAGE.MESSAGES,
-    description: VIDEO_DESCRIPTION.MESSAGES
-  }
-
-  fileError = ''
-
-  constructor (
-    private authService: AuthService,
-    private elementRef: ElementRef,
-    private formBuilder: FormBuilder,
-    private route: ActivatedRoute,
-    private router: Router,
-    private notificationsService: NotificationsService,
-    private videoService: VideoService
-  ) {
-    super()
-  }
-
-  buildForm () {
-    this.form = this.formBuilder.group({
-      name: [ '', VIDEO_NAME.VALIDATORS ],
-      nsfw: [ false ],
-      category: [ '', VIDEO_CATEGORY.VALIDATORS ],
-      licence: [ '', VIDEO_LICENCE.VALIDATORS ],
-      language: [ '', VIDEO_LANGUAGE.VALIDATORS ],
-      description: [ '', VIDEO_DESCRIPTION.VALIDATORS ],
-      tags: [ '' ]
-    })
-
-    this.form.valueChanges.subscribe(data => this.onValueChanged(data))
-  }
-
-  ngOnInit () {
-    this.buildForm()
-
-    this.videoCategories = this.videoService.videoCategories
-    this.videoLicences = this.videoService.videoLicences
-    this.videoLanguages = this.videoService.videoLanguages
-
-    const uuid: string = this.route.snapshot.params['uuid']
-    this.videoService.getVideo(uuid)
-                     .subscribe(
-                       video => {
-                         this.video = video
-
-                         this.hydrateFormFromVideo()
-                       },
-
-                       err => {
-                         console.error(err)
-                         this.error = 'Cannot fetch video.'
-                       }
-                     )
-  }
-
-  checkForm () {
-    this.forceCheck()
-
-    return this.form.valid
-  }
-
-  update () {
-    if (this.checkForm() === false) {
-      return
-    }
-
-    this.video.patch(this.form.value)
-
-    this.videoService.updateVideo(this.video)
-                     .subscribe(
-                       () => {
-                         this.notificationsService.success('Success', 'Video updated.')
-                         this.router.navigate([ '/videos/watch', this.video.uuid ])
-                       },
-
-                       err => {
-                         this.error = 'Cannot update the video.'
-                         console.error(err)
-                       }
-                      )
-
-  }
-
-  private hydrateFormFromVideo () {
-    this.form.patchValue(this.video.toJSON())
-  }
-}
diff --git a/client/src/app/videos/video-watch/index.ts b/client/src/app/videos/video-watch/index.ts
deleted file mode 100644 (file)
index 1058724..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from './video-magnet.component'
-export * from './video-share.component'
-export * from './video-report.component'
-export * from './video-watch.component'
diff --git a/client/src/app/videos/video-watch/video-magnet.component.html b/client/src/app/videos/video-watch/video-magnet.component.html
deleted file mode 100644 (file)
index 484280c..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-<div bsModal #modal="bs-modal" class="modal" tabindex="-1">
-  <div class="modal-dialog">
-    <div class="modal-content modal-lg">
-
-      <div class="modal-header">
-        <button type="button" class="close" aria-label="Close" (click)="hide()">
-          <span aria-hidden="true">&times;</span>
-        </button>
-        <h4 class="modal-title">Magnet Uri</h4>
-      </div>
-
-      <div class="modal-body">
-        <div *ngFor="let file of video.files">
-          <label>{{ file.resolutionLabel }}</label>
-          <input #magnetUriInput (click)="magnetUriInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="file.magnetUri" />
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
diff --git a/client/src/app/videos/video-watch/video-magnet.component.ts b/client/src/app/videos/video-watch/video-magnet.component.ts
deleted file mode 100644 (file)
index f9432e9..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Component, Input, ViewChild } from '@angular/core'
-
-import { ModalDirective } from 'ngx-bootstrap/modal'
-
-import { Video } from '../shared'
-
-@Component({
-  selector: 'my-video-magnet',
-  templateUrl: './video-magnet.component.html'
-})
-export class VideoMagnetComponent {
-  @Input() video: Video = null
-
-  @ViewChild('modal') modal: ModalDirective
-
-  constructor () {
-    // empty
-  }
-
-  show () {
-    this.modal.show()
-  }
-
-  hide () {
-    this.modal.hide()
-  }
-}
diff --git a/client/src/app/videos/video-watch/video-report.component.html b/client/src/app/videos/video-watch/video-report.component.html
deleted file mode 100644 (file)
index 741080e..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-<div bsModal #modal="bs-modal" class="modal" tabindex="-1">
-  <div class="modal-dialog">
-    <div class="modal-content modal-lg">
-
-      <div class="modal-header">
-        <button type="button" class="close" aria-label="Close" (click)="hide()">
-          <span aria-hidden="true">&times;</span>
-        </button>
-        <h4 class="modal-title">Report video</h4>
-      </div>
-
-      <div class="modal-body">
-
-        <form novalidate [formGroup]="form">
-          <div class="form-group">
-            <label for="description">Reason</label>
-            <textarea
-              id="reason" class="form-control" placeholder="Reason..."
-              formControlName="reason"
-            >
-            </textarea>
-            <div *ngIf="formErrors.reason" class="alert alert-danger">
-              {{ formErrors.reason }}
-            </div>
-          </div>
-
-          <div class="form-group">
-            <input
-              type="button" value="Report" class="btn btn-default form-control"
-              [disabled]="!form.valid" (click)="report()"
-            >
-          </div>
-        </form>
-
-      </div>
-    </div>
-  </div>
-</div>
diff --git a/client/src/app/videos/video-watch/video-report.component.ts b/client/src/app/videos/video-watch/video-report.component.ts
deleted file mode 100644 (file)
index d9c83a6..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-import { Component, Input, OnInit, ViewChild } from '@angular/core'
-import { FormBuilder, FormGroup } from '@angular/forms'
-
-import { ModalDirective } from 'ngx-bootstrap/modal'
-import { NotificationsService } from 'angular2-notifications'
-
-import { FormReactive, VideoAbuseService, VIDEO_ABUSE_REASON } from '../../shared'
-import { Video, VideoService } from '../shared'
-
-@Component({
-  selector: 'my-video-report',
-  templateUrl: './video-report.component.html'
-})
-export class VideoReportComponent extends FormReactive implements OnInit {
-  @Input() video: Video = null
-
-  @ViewChild('modal') modal: ModalDirective
-
-  error: string = null
-  form: FormGroup
-  formErrors = {
-    reason: ''
-  }
-  validationMessages = {
-    reason: VIDEO_ABUSE_REASON.MESSAGES
-  }
-
-  constructor (
-    private formBuilder: FormBuilder,
-    private videoAbuseService: VideoAbuseService,
-    private notificationsService: NotificationsService
-   ) {
-    super()
-  }
-
-  ngOnInit () {
-    this.buildForm()
-  }
-
-  buildForm () {
-    this.form = this.formBuilder.group({
-      reason: [ '', VIDEO_ABUSE_REASON.VALIDATORS ]
-    })
-
-    this.form.valueChanges.subscribe(data => this.onValueChanged(data))
-  }
-
-  show () {
-    this.modal.show()
-  }
-
-  hide () {
-    this.modal.hide()
-  }
-
-  report () {
-    const reason = this.form.value['reason']
-
-    this.videoAbuseService.reportVideo(this.video.id, reason)
-                          .subscribe(
-                            () => {
-                              this.notificationsService.success('Success', 'Video reported.')
-                              this.hide()
-                            },
-
-                            err => this.notificationsService.error('Error', err.message)
-                           )
-  }
-}
diff --git a/client/src/app/videos/video-watch/video-share.component.html b/client/src/app/videos/video-watch/video-share.component.html
deleted file mode 100644 (file)
index 88f59c0..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-<div bsModal #modal="bs-modal" class="modal" tabindex="-1">
-  <div class="modal-dialog modal-lg">
-    <div class="modal-content">
-
-      <div class="modal-header">
-        <button type="button" class="close" aria-label="Close" (click)="hide()">
-          <span aria-hidden="true">&times;</span>
-        </button>
-        <h4 class="modal-title">Share</h4>
-      </div>
-
-      <div class="modal-body">
-        <div class="form-group">
-          <label>URL</label>
-          <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getVideoUrl()" />
-        </div>
-
-        <div class="form-group">
-          <label>Embed</label>
-          <input #shareInput (click)="shareInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getVideoIframeCode()" />
-        </div>
-
-        <div *ngIf="notSecure()" class="alert alert-warning">
-          The url is not secured (no HTTPS), so the embed video won't work on HTTPS websites (web browsers block non secured HTTP requests on HTTPS websites).
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
diff --git a/client/src/app/videos/video-watch/video-share.component.ts b/client/src/app/videos/video-watch/video-share.component.ts
deleted file mode 100644 (file)
index 133f934..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-import { Component, Input, ViewChild } from '@angular/core'
-
-import { ModalDirective } from 'ngx-bootstrap/modal'
-
-import { Video } from '../shared'
-
-@Component({
-  selector: 'my-video-share',
-  templateUrl: './video-share.component.html'
-})
-export class VideoShareComponent {
-  @Input() video: Video = null
-
-  @ViewChild('modal') modal: ModalDirective
-
-  constructor () {
-    // empty
-  }
-
-  show () {
-    this.modal.show()
-  }
-
-  hide () {
-    this.modal.hide()
-  }
-
-  getVideoIframeCode () {
-    return '<iframe width="560" height="315" ' +
-           'src="' + window.location.origin + '/videos/embed/' + this.video.uuid + '" ' +
-           'frameborder="0" allowfullscreen>' +
-           '</iframe>'
-  }
-
-  getVideoUrl () {
-    return window.location.href
-  }
-
-  notSecure () {
-    return window.location.protocol === 'http:'
-  }
-}
diff --git a/client/src/app/videos/video-watch/video-watch.component.html b/client/src/app/videos/video-watch/video-watch.component.html
deleted file mode 100644 (file)
index 8886313..0000000
+++ /dev/null
@@ -1,184 +0,0 @@
-<div *ngIf="error" class="row">
-  <div class="alert alert-danger">
-    The video load seems to be abnormally long.
-    <ul>
-      <li>Maybe the server {{ video.podHost }} is down :(</li>
-      <li>
-        If not, you can report an issue on
-        <a href="https://github.com/Chocobozzz/PeerTube/issues" title="Report an issue">
-          https://github.com/Chocobozzz/PeerTube/issues
-        </a>
-      </li>
-    </ul>
-  </div>
-</div>
-
-<div class="row">
-  <!-- We need the video container for videojs so we just hide it -->
-  <div [hidden]="videoNotFound" class="embed-responsive embed-responsive-19by9">
-     <video id="video-container" class="video-js vjs-sublime-skin"></video>
-  </div>
-
-  <div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div>
-</div>
-
-<!-- P2P informations -->
-<div id="torrent-info" class="row">
-  <div id="torrent-info-download" class="col-md-4 col-sm-4 col-xs-4">Download: {{ downloadSpeed | bytes }}/s</div>
-  <div id="torrent-info-upload" class="col-md-4 col-sm-4 col-xs-4">Upload: {{ uploadSpeed | bytes }}/s</div>
-  <div id="torrent-info-peers" class="col-md-4 col-sm-4 col-xs-4">Number of peers: {{ numPeers }}</div>
-</div>
-
-<!-- Video informations -->
-<div *ngIf="video !== null" id="video-info">
-  <div class="row video-name-views">
-    <div class="col-xs-8 col-md-8 video-name">
-      {{ video.name }}
-    </div>
-
-    <div class="col-xs-4 col-md-4 pull-right video-views">
-      {{ video.views}} views
-    </div>
-  </div>
-
-  <div class="row video-small-blocks">
-    <div class="col-xs-5 col-xs-3 col-md-3 video-small-block video-small-block-author">
-      <a class="option" title="Access to all videos of this user" [routerLink]="['/videos/list', { field: 'author', search: video.author }]">
-        <span class="glyphicon glyphicon-user"></span>
-        <span class="video-small-block-text">{{ video.by }}</span>
-      </a>
-    </div>
-
-    <div class="col-xs-2 col-md-3 video-small-block video-small-block-share">
-      <a class="option" (click)="showShareModal()" title="Share the video">
-        <span class="glyphicon glyphicon-share"></span>
-        <span class="video-small-block-text">Share</span>
-      </a>
-    </div>
-
-    <div class="col-xs-2 col-md-3 video-small-block video-small-block-more">
-      <div class="video-small-block-dropdown" dropdown dropup="true" placement="right">
-        <a class="option" title="Access to more options" dropdownToggle>
-          <span class="glyphicon glyphicon-option-horizontal"></span>
-          <span class="video-small-block-text">More</span>
-        </a>
-
-        <ul *dropdownMenu class="dropdown-menu" id="more-menu" role="menu" aria-labelledby="single-button">
-          <li *ngIf="canUserUpdateVideo()" role="menuitem">
-            <a class="dropdown-item" title="Update this video" href="#" [routerLink]="[ '/videos/edit', video.uuid ]">
-              <span class="glyphicon glyphicon-pencil"></span> Update
-            </a>
-          </li>
-
-          <li role="menuitem">
-            <a class="dropdown-item" title="Get magnet URI" href="#" (click)="showMagnetUriModal($event)">
-              <span class="glyphicon glyphicon-magnet"></span> Magnet
-            </a>
-          </li>
-
-          <li *ngIf="isUserLoggedIn()" role="menuitem">
-            <a class="dropdown-item" title="Report this video" href="#" (click)="showReportModal($event)">
-              <span class="glyphicon glyphicon-alert"></span> Report
-            </a>
-          </li>
-
-          <li *ngIf="isVideoRemovable()" role="menuitem">
-            <a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)">
-              <span class="glyphicon glyphicon-remove"></span> Delete
-            </a>
-          </li>
-
-          <li *ngIf="isVideoBlacklistable()" role="menuitem">
-            <a class="dropdown-item" title="Blacklist this video" href="#" (click)="blacklistVideo($event)">
-              <span class="glyphicon glyphicon-eye-close"></span> Blacklist
-            </a>
-          </li>
-        </ul>
-      </div>
-    </div>
-
-    <div class="col-xs-3 col-md-3 video-small-block video-small-block-rating">
-      <div class="video-small-block-like">
-        <span
-          class="glyphicon glyphicon-thumbs-up" title="Like this video"
-          [ngClass]="{ 'interactive': isUserLoggedIn(), 'activated': userRating === 'like' }" (click)="setLike()"
-        ></span>
-
-        <span class="video-small-block-text">
-          {{ video.likes }}
-        </span>
-      </div>
-
-      <div class="video-small-block-dislike">
-        <span
-          class="glyphicon glyphicon-thumbs-down" title="Dislike this video"
-          [ngClass]="{ 'interactive': isUserLoggedIn(), 'activated': userRating === 'dislike' }" (click)="setDislike()"
-        ></span>
-
-        <span class="video-small-block-text">
-          {{ video.dislikes }}
-        </span>
-      </div>
-    </div>
-  </div>
-
-  <div class="row video-details">
-    <div class="video-details-date-description col-xs-8 col-md-9">
-      <div class="video-details-date">
-        Published on {{ video.createdAt | date:'short' }}
-      </div>
-
-      <div class="video-details-description">
-        {{ video.description }}
-      </div>
-    </div>
-
-    <div class="video-details-attributes col-xs-4 col-md-3">
-      <div class="video-details-attribute">
-        <span class="video-details-attribute-label">
-          Category:
-        </span>
-        <span class="video-details-attribute-value">
-          {{ video.categoryLabel }}
-        </span>
-      </div>
-
-      <div class="video-details-attribute">
-        <span class="video-details-attribute-label">
-          Licence:
-        </span>
-        <span class="video-details-attribute-value">
-          {{ video.licenceLabel }}
-        </span>
-      </div>
-
-      <div class="video-details-attribute">
-        <span class="video-details-attribute-label">
-          Language:
-        </span>
-        <span class="video-details-attribute-value">
-          {{ video.languageLabel }}
-        </span>
-      </div>
-
-      <div class="video-details-attribute">
-        <span class="video-details-attribute-label">
-          Tags:
-        </span>
-
-        <div class="video-details-tags">
-          <a *ngFor="let tag of video.tags" [routerLink]="['/videos/list', { field: 'tags', search: tag }]" class="label label-primary">
-            {{ tag }}
-          </a>
-        </div>
-      </div>
-
-    </div>
-  </div>
-</div>
-
-<ng-template [ngIf]="video !== null">
-  <my-video-share #videoShareModal [video]="video"></my-video-share>
-  <my-video-magnet #videoMagnetModal [video]="video"></my-video-magnet>
-  <my-video-report #videoReportModal [video]="video"></my-video-report>
-</ng-template>
diff --git a/client/src/app/videos/video-watch/video-watch.component.scss b/client/src/app/videos/video-watch/video-watch.component.scss
deleted file mode 100644 (file)
index 6966174..0000000
+++ /dev/null
@@ -1,245 +0,0 @@
-#video-container {
-  width: 100%;
-  height: 100%;
-}
-
-#video-not-found {
-  height: 300px;
-  line-height: 300px;
-  margin-top: 50px;
-  text-align: center;
-  font-weight: bold;
-}
-
-.embed-responsive {
-  height: 500px;
-
-  @media screen and (max-width: 600px) {
-    height: 300px;
-  }
-}
-
-#torrent-info {
-  font-size: 10px;
-  margin-top: 10px;
-  text-align: center;
-
-  div {
-    min-width: 60px;
-  }
-}
-
-#video-info {
-  .video-name-views {
-    font-weight: bold;
-    font-size: 18px;
-    height: $video-watch-title-height;
-    line-height: $video-watch-title-height;
-
-    .video-name {
-      padding-left: $video-watch-info-padding-left;
-    }
-
-    .video-views {
-      text-align: right;
-      // Keep a symmetry with the video name
-      padding-right: $video-watch-info-padding-left
-    }
-
-  }
-
-  .video-small-blocks {
-    height: $video-watch-info-height;
-    color: $video-watch-info-color;
-    border-color: $video-watch-border-color;
-    border-width: 1px 0px;
-    border-style: solid;
-
-    .video-small-block {
-      height: $video-watch-info-height;
-      display: flex;
-      flex-direction: column;
-      justify-content: center;
-      text-align: center;
-
-      a {
-        cursor: pointer;
-        transition: color 0.3s;
-        white-space: nowrap;
-        overflow: hidden;
-        text-overflow: ellipsis;
-
-        &, &:hover {
-          color: inherit;
-          text-decoration:none;
-        }
-
-        &:hover {
-          color: #000 !important;
-        }
-
-        &:hover > .glyphicon {
-          opacity: 1 !important;
-        }
-      }
-
-      .option .glyphicon {
-        font-size: 22px;
-        color: inherit;
-        opacity: 0.15;
-        margin-bottom: 10px;
-        transition: opacity 0.3s;
-      }
-
-      .video-small-block-text {
-        font-size: 15px;
-        font-weight: bold;
-      }
-    }
-
-    .video-small-block:not(:last-child) {
-      border-width: 0 1px 0 0;
-      border-color: $video-watch-border-color;
-      border-style: solid;
-    }
-
-    .video-small-block-author, .video-small-block-more {
-      a.option {
-        display: block;
-
-        .glyphicon {
-          display: block;
-        }
-      }
-    }
-
-    .video-small-block-share, .video-small-block-more {
-      a.option {
-        display: block;
-
-        .glyphicon {
-          display: block;
-        }
-      }
-    }
-
-    .video-small-block-more .video-small-block-dropdown {
-      position: relative;
-
-      .dropdown-item .glyphicon {
-        margin-right: 5px;
-      }
-    }
-
-    .video-small-block-rating {
-
-      .video-small-block-like {
-        margin-bottom: 10px;
-      }
-
-      .video-small-block-text {
-        vertical-align: top;
-      }
-
-      .glyphicon {
-        font-size: 18px;
-        margin: 0 10px 0 0;
-        opacity: 0.3;
-      }
-
-      .interactive {
-        cursor: pointer;
-        transition: opacity, color 0.3s;
-
-        &.activated, &:hover {
-          opacity: 1;
-          color: #000;
-        }
-      }
-    }
-  }
-
-  .video-details {
-    margin-top: 30px;
-
-    .video-details-date-description {
-      padding-left: $video-watch-info-padding-left;
-
-      .video-details-date {
-        font-weight: bold;
-        margin-bottom: 30px;
-      }
-    }
-
-    .video-details-attributes {
-      font-weight: bold;
-      font-size: 12px;
-
-      .video-details-attribute-label {
-        color: $video-watch-info-color;
-        display: inline-block;
-        width: 60px;
-        margin-right: 5px;
-      }
-    }
-
-    .video-details-tags {
-      display: inline-block;
-
-      a {
-        display: inline-block;
-        margin-right: 3px;
-        font-size: 11px;
-      }
-    }
-  }
-
-  @media screen and (max-width: 400px) {
-    .video-name-views {
-      font-size: 16px !important;
-    }
-  }
-
-  @media screen and (max-width: 800px) {
-    .video-name-views {
-      .video-name {
-        padding-left: 5px;
-        padding-right: 0px;
-      }
-
-      .video-views {
-        padding-left: 0px;
-        padding-right: 5px;
-      }
-    }
-
-    .video-small-blocks {
-      a, .video-small-block-text {
-        font-size: 13px !important;
-      }
-
-      .glyphicon {
-        font-size: 18px !important;
-      }
-
-      .video-small-block-author {
-        padding-left: 10px;
-      }
-    }
-
-    .video-details {
-      .video-details-date-description {
-        padding-left: 10px;
-        font-size: 13px !important;
-      }
-
-      .video-details-attributes {
-        font-size: 11px !important;
-
-        .video-details-attribute-label {
-          width: 50px;
-        }
-      }
-    }
-  }
-}
diff --git a/client/src/app/videos/video-watch/video-watch.component.ts b/client/src/app/videos/video-watch/video-watch.component.ts
deleted file mode 100644 (file)
index db3e1cd..0000000
+++ /dev/null
@@ -1,299 +0,0 @@
-import { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
-import { ActivatedRoute, Router } from '@angular/router'
-import { Observable } from 'rxjs/Observable'
-import { Subscription } from 'rxjs/Subscription'
-
-import videojs from 'video.js'
-import '../../../assets/player/peertube-videojs-plugin'
-
-import { MetaService } from '@ngx-meta/core'
-import { NotificationsService } from 'angular2-notifications'
-
-import { AuthService, ConfirmService } from '../../core'
-import { VideoMagnetComponent } from './video-magnet.component'
-import { VideoShareComponent } from './video-share.component'
-import { VideoReportComponent } from './video-report.component'
-import { Video, VideoService } from '../shared'
-import { WebTorrentService } from './webtorrent.service'
-import { UserVideoRateType, VideoRateType } from '../../../../../shared'
-
-@Component({
-  selector: 'my-video-watch',
-  templateUrl: './video-watch.component.html',
-  styleUrls: [ './video-watch.component.scss' ]
-})
-export class VideoWatchComponent implements OnInit, OnDestroy {
-  @ViewChild('videoMagnetModal') videoMagnetModal: VideoMagnetComponent
-  @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
-  @ViewChild('videoReportModal') videoReportModal: VideoReportComponent
-
-  downloadSpeed: number
-  error = false
-  loading = false
-  numPeers: number
-  player: videojs.Player
-  playerElement: HTMLMediaElement
-  uploadSpeed: number
-  userRating: UserVideoRateType = null
-  video: Video = null
-  videoNotFound = false
-
-  private paramsSub: Subscription
-
-  constructor (
-    private elementRef: ElementRef,
-    private route: ActivatedRoute,
-    private router: Router,
-    private videoService: VideoService,
-    private confirmService: ConfirmService,
-    private metaService: MetaService,
-    private authService: AuthService,
-    private notificationsService: NotificationsService
-  ) {}
-
-  ngOnInit () {
-    this.paramsSub = this.route.params.subscribe(routeParams => {
-      let uuid = routeParams['uuid']
-      this.videoService.getVideo(uuid).subscribe(
-        video => this.onVideoFetched(video),
-
-        error => {
-          console.error(error)
-          this.videoNotFound = true
-        }
-      )
-    })
-  }
-
-  ngOnDestroy () {
-    // Remove player if it exists
-    if (this.videoNotFound === false) {
-      videojs(this.playerElement).dispose()
-    }
-
-    // Unsubscribe subscriptions
-    this.paramsSub.unsubscribe()
-  }
-
-  setLike () {
-    if (this.isUserLoggedIn() === false) return
-    // Already liked this video
-    if (this.userRating === 'like') return
-
-    this.videoService.setVideoLike(this.video.id)
-                     .subscribe(
-                      () => {
-                        // Update the video like attribute
-                        this.updateVideoRating(this.userRating, 'like')
-                        this.userRating = 'like'
-                      },
-
-                      err => this.notificationsService.error('Error', err.message)
-                     )
-  }
-
-  setDislike () {
-    if (this.isUserLoggedIn() === false) return
-    // Already disliked this video
-    if (this.userRating === 'dislike') return
-
-    this.videoService.setVideoDislike(this.video.id)
-                     .subscribe(
-                      () => {
-                        // Update the video dislike attribute
-                        this.updateVideoRating(this.userRating, 'dislike')
-                        this.userRating = 'dislike'
-                      },
-
-                      err => this.notificationsService.error('Error', err.message)
-                     )
-  }
-
-  removeVideo (event: Event) {
-    event.preventDefault()
-
-    this.confirmService.confirm('Do you really want to delete this video?', 'Delete').subscribe(
-      res => {
-        if (res === false) return
-
-        this.videoService.removeVideo(this.video.id)
-                         .subscribe(
-                           status => {
-                             this.notificationsService.success('Success', `Video ${this.video.name} deleted.`)
-                             // Go back to the video-list.
-                             this.router.navigate(['/videos/list'])
-                           },
-
-                           error => this.notificationsService.error('Error', error.text)
-                          )
-      }
-    )
-  }
-
-  blacklistVideo (event: Event) {
-    event.preventDefault()
-
-    this.confirmService.confirm('Do you really want to blacklist this video ?', 'Blacklist').subscribe(
-      res => {
-        if (res === false) return
-
-        this.videoService.blacklistVideo(this.video.id)
-                         .subscribe(
-                           status => {
-                             this.notificationsService.success('Success', `Video ${this.video.name} had been blacklisted.`)
-                             this.router.navigate(['/videos/list'])
-                           },
-
-                           error => this.notificationsService.error('Error', error.text)
-                         )
-      }
-    )
-  }
-
-  showReportModal (event: Event) {
-    event.preventDefault()
-    this.videoReportModal.show()
-  }
-
-  showShareModal () {
-    this.videoShareModal.show()
-  }
-
-  showMagnetUriModal (event: Event) {
-    event.preventDefault()
-    this.videoMagnetModal.show()
-  }
-
-  isUserLoggedIn () {
-    return this.authService.isLoggedIn()
-  }
-
-  canUserUpdateVideo () {
-    return this.video.isUpdatableBy(this.authService.getUser())
-  }
-
-  isVideoRemovable () {
-    return this.video.isRemovableBy(this.authService.getUser())
-  }
-
-  isVideoBlacklistable () {
-    return this.video.isBlackistableBy(this.authService.getUser())
-  }
-
-  private handleError (err: any) {
-    const errorMessage: string = typeof err === 'string' ? err : err.message
-    let message = ''
-
-    if (errorMessage.indexOf('http error') !== -1) {
-      message = 'Cannot fetch video from server, maybe down.'
-    } else {
-      message = errorMessage
-    }
-
-    this.notificationsService.error('Error', message)
-  }
-
-  private checkUserRating () {
-    // Unlogged users do not have ratings
-    if (this.isUserLoggedIn() === false) return
-
-    this.videoService.getUserVideoRating(this.video.id)
-                     .subscribe(
-                       ratingObject => {
-                         if (ratingObject) {
-                           this.userRating = ratingObject.rating
-                         }
-                       },
-
-                       err => this.notificationsService.error('Error', err.message)
-                      )
-  }
-
-  private onVideoFetched (video: Video) {
-    this.video = video
-
-    let observable
-    if (this.video.isVideoNSFWForUser(this.authService.getUser())) {
-      observable = this.confirmService.confirm('This video is not safe for work. Are you sure you want to watch it?', 'NSFW')
-    } else {
-      observable = Observable.of(true)
-    }
-
-    observable.subscribe(
-      res => {
-        if (res === false) {
-          return this.router.navigate([ '/videos/list' ])
-        }
-
-        this.playerElement = this.elementRef.nativeElement.querySelector('#video-container')
-
-        const videojsOptions = {
-          controls: true,
-          autoplay: true,
-          plugins: {
-            peertube: {
-              videoFiles: this.video.files,
-              playerElement: this.playerElement,
-              autoplay: true,
-              peerTubeLink: false
-            }
-          }
-        }
-
-        const self = this
-        videojs(this.playerElement, videojsOptions, function () {
-          self.player = this
-          this.on('customError', (event, data) => {
-            self.handleError(data.err)
-          })
-
-          this.on('torrentInfo', (event, data) => {
-            self.downloadSpeed = data.downloadSpeed
-            self.numPeers = data.numPeers
-            self.uploadSpeed = data.uploadSpeed
-          })
-        })
-
-        this.setOpenGraphTags()
-        this.checkUserRating()
-      }
-    )
-  }
-
-  private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) {
-    let likesToIncrement = 0
-    let dislikesToIncrement = 0
-
-    if (oldRating) {
-      if (oldRating === 'like') likesToIncrement--
-      if (oldRating === 'dislike') dislikesToIncrement--
-    }
-
-    if (newRating === 'like') likesToIncrement++
-    if (newRating === 'dislike') dislikesToIncrement++
-
-    this.video.likes += likesToIncrement
-    this.video.dislikes += dislikesToIncrement
-  }
-
-  private setOpenGraphTags () {
-    this.metaService.setTitle(this.video.name)
-
-    this.metaService.setTag('og:type', 'video')
-
-    this.metaService.setTag('og:title', this.video.name)
-    this.metaService.setTag('name', this.video.name)
-
-    this.metaService.setTag('og:description', this.video.description)
-    this.metaService.setTag('description', this.video.description)
-
-    this.metaService.setTag('og:image', this.video.previewPath)
-
-    this.metaService.setTag('og:duration', this.video.duration.toString())
-
-    this.metaService.setTag('og:site_name', 'PeerTube')
-
-    this.metaService.setTag('og:url', window.location.href)
-    this.metaService.setTag('url', window.location.href)
-  }
-}
index 715671ba734be670dc402dd5956e9f49672f9849..225b6b018abc02bfb50dbf627b2d9f7069846cfe 100644 (file)
@@ -3,10 +3,8 @@ import { RouterModule, Routes } from '@angular/router'
 
 import { MetaGuard } from '@ngx-meta/core'
 
-import { VideoAddComponent, VideoUpdateComponent } from './video-edit'
 import { VideoListComponent } from './video-list'
 import { VideosComponent } from './videos.component'
-import { VideoWatchComponent } from './video-watch'
 
 const videosRoutes: Routes = [
   {
@@ -25,7 +23,7 @@ const videosRoutes: Routes = [
       },
       {
         path: 'add',
-        component: VideoAddComponent,
+        loadChildren: 'app/videos/+video-edit#VideoAddModule',
         data: {
           meta: {
             title: 'Add a video'
@@ -34,7 +32,7 @@ const videosRoutes: Routes = [
       },
       {
         path: 'edit/:uuid',
-        component: VideoUpdateComponent,
+        loadChildren: 'app/videos/+video-edit#VideoUpdateModule',
         data: {
           meta: {
             title: 'Edit a video'
@@ -47,7 +45,10 @@ const videosRoutes: Routes = [
       },
       {
         path: 'watch/:uuid',
-        component: VideoWatchComponent
+        loadChildren: 'app/videos/+video-watch#VideoWatchModule',
+        data: {
+          preload: 3000
+        }
       }
     ]
   }
index 972c2221f5c70dbaf42527015ec18e28fcb28f68..26d9d28d403ff6df99eff7fa4228719300921117 100644 (file)
@@ -1,8 +1,16 @@
-import { Component } from '@angular/core'
+import { Component, OnInit } from '@angular/core'
+
+import { VideoService } from './shared'
 
 @Component({
   template: '<router-outlet></router-outlet>'
 })
+export class VideosComponent implements OnInit {
+  constructor(private videoService: VideoService) {}
 
-export class VideosComponent {
+  ngOnInit () {
+    this.videoService.loadVideoCategories()
+    this.videoService.loadVideoLicences()
+    this.videoService.loadVideoLanguages()
+  }
 }
index bc86118cc56ce9d5bbe58f1e92a6b1f30238b504..3a0c3feac122241f4387a152bf91db7e8aaee59d 100644 (file)
@@ -1,24 +1,13 @@
 import { NgModule } from '@angular/core'
 
-import { TagInputModule } from 'ngx-chips'
-
 import { VideosRoutingModule } from './videos-routing.module'
 import { VideosComponent } from './videos.component'
-import { VideoAddComponent, VideoUpdateComponent } from './video-edit'
 import { LoaderComponent, VideoListComponent, VideoMiniatureComponent, VideoSortComponent } from './video-list'
-import {
-  VideoWatchComponent,
-  VideoMagnetComponent,
-  VideoReportComponent,
-  VideoShareComponent
-} from './video-watch'
 import { VideoService } from './shared'
 import { SharedModule } from '../shared'
 
 @NgModule({
   imports: [
-    TagInputModule,
-
     VideosRoutingModule,
     SharedModule
   ],
@@ -26,18 +15,10 @@ import { SharedModule } from '../shared'
   declarations: [
     VideosComponent,
 
-    VideoAddComponent,
-    VideoUpdateComponent,
-
     VideoListComponent,
     VideoMiniatureComponent,
     VideoSortComponent,
 
-    VideoWatchComponent,
-    VideoMagnetComponent,
-    VideoShareComponent,
-    VideoReportComponent,
-
     LoaderComponent
   ],
 
index 011ecce681e6f5f1234e7900f5fb5f116e6198ab..f2ee714471bf84e7edc6f529b15aad552f2f6447 100644 (file)
@@ -2,71 +2,71 @@
 # yarn lockfile v1
 
 
-"@angular/animations@~4.3.0":
-  version "4.3.6"
-  resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-4.3.6.tgz#bf9283ec7c8c98b32f569d84dcda10890fdc0262"
+"@angular/animations@~4.4.0":
+  version "4.4.4"
+  resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-4.4.4.tgz#a2f9353604347abe15df98292058842f52f08bc2"
   dependencies:
     tslib "^1.7.1"
 
-"@angular/common@~4.3.0":
-  version "4.3.6"
-  resolved "https://registry.yarnpkg.com/@angular/common/-/common-4.3.6.tgz#ed37e9307c7506dd834797c1a6cf675e52b5b6ee"
+"@angular/common@~4.4.0":
+  version "4.4.4"
+  resolved "https://registry.yarnpkg.com/@angular/common/-/common-4.4.4.tgz#ae0a818aaa0c6a3f0901e7b80bd94e1c22eb9365"
   dependencies:
     tslib "^1.7.1"
 
-"@angular/compiler-cli@~4.3.0":
-  version "4.3.6"
-  resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-4.3.6.tgz#6afa6aef68dd681e61b398be4d6270e5c8680b12"
+"@angular/compiler-cli@~4.4.0":
+  version "4.4.4"
+  resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-4.4.4.tgz#063080a497d9175396825050222c717da184f6cf"
   dependencies:
-    "@angular/tsc-wrapped" "4.3.6"
+    "@angular/tsc-wrapped" "4.4.4"
     minimist "^1.2.0"
     reflect-metadata "^0.1.2"
 
-"@angular/compiler@~4.3.0":
-  version "4.3.6"
-  resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-4.3.6.tgz#be170df098b71e835ccedf168d5fb7b23e5045b8"
+"@angular/compiler@~4.4.0":
+  version "4.4.4"
+  resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-4.4.4.tgz#326eb0029d9a3541aaca124def9adc51c36f2b41"
   dependencies:
     tslib "^1.7.1"
 
-"@angular/core@~4.3.0":
-  version "4.3.6"
-  resolved "https://registry.yarnpkg.com/@angular/core/-/core-4.3.6.tgz#bbac63d68d0f7bcb389d12b34208652be3287e96"
+"@angular/core@~4.4.0":
+  version "4.4.4"
+  resolved "https://registry.yarnpkg.com/@angular/core/-/core-4.4.4.tgz#bd37ecf54158f97489996c9386bd222f80a32f5c"
   dependencies:
     tslib "^1.7.1"
 
-"@angular/forms@~4.3.0":
-  version "4.3.6"
-  resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-4.3.6.tgz#0f20c4597c16a152745d7cd95559855a0a5c6687"
+"@angular/forms@~4.4.0":
+  version "4.4.4"
+  resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-4.4.4.tgz#4db3790509b6b10f1db8a7c1b7f52187cf64cfd4"
   dependencies:
     tslib "^1.7.1"
 
-"@angular/http@~4.3.0":
-  version "4.3.6"
-  resolved "https://registry.yarnpkg.com/@angular/http/-/http-4.3.6.tgz#563827d1a7d5e89e3b7d86b77fbbd367b2c08591"
+"@angular/http@~4.4.0":
+  version "4.4.4"
+  resolved "https://registry.yarnpkg.com/@angular/http/-/http-4.4.4.tgz#667faf616bb624168eafae6ee92e5eba23a9d1f2"
   dependencies:
     tslib "^1.7.1"
 
-"@angular/platform-browser-dynamic@~4.3.0":
-  version "4.3.6"
-  resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-4.3.6.tgz#9eabf826f119c98f85c2a96edcb18ab00b4efb1c"
+"@angular/platform-browser-dynamic@~4.4.0":
+  version "4.4.4"
+  resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-4.4.4.tgz#c3c9eb854a528556a07054127932e527fa932e14"
   dependencies:
     tslib "^1.7.1"
 
-"@angular/platform-browser@~4.3.0":
-  version "4.3.6"
-  resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-4.3.6.tgz#6152b1f3b78d0246fc5e150e2f7b9ed4337e3ba6"
+"@angular/platform-browser@~4.4.0":
+  version "4.4.4"
+  resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-4.4.4.tgz#a3898e2e7ba9d84ffa0d47144c6971179c75aee6"
   dependencies:
     tslib "^1.7.1"
 
-"@angular/router@~4.3.0":
-  version "4.3.6"
-  resolved "https://registry.yarnpkg.com/@angular/router/-/router-4.3.6.tgz#64033edb4fcda08a323e7533b4a1820c0f28d130"
+"@angular/router@~4.4.0":
+  version "4.4.4"
+  resolved "https://registry.yarnpkg.com/@angular/router/-/router-4.4.4.tgz#7be391096e843cb3e04f9f05d1d65a88df9bc7cf"
   dependencies:
     tslib "^1.7.1"
 
-"@angular/tsc-wrapped@4.3.6":
-  version "4.3.6"
-  resolved "https://registry.yarnpkg.com/@angular/tsc-wrapped/-/tsc-wrapped-4.3.6.tgz#1aa66e0ab2c4799a4ad14b675e13953aa5fcd436"
+"@angular/tsc-wrapped@4.4.4":
+  version "4.4.4"
+  resolved "https://registry.yarnpkg.com/@angular/tsc-wrapped/-/tsc-wrapped-4.4.4.tgz#9841821e55616b826ca160250fe85e15fc74ffc3"
   dependencies:
     tsickle "^0.21.0"
 
   resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.25.tgz#66ecaf4df93f5281b48427ee96fbcdfc4f0cdce1"
 
 "@types/node@^6.0.38":
-  version "6.0.88"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.88.tgz#f618f11a944f6a18d92b5c472028728a3e3d4b66"
+  version "6.0.89"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.89.tgz#154be0e6a823760cd6083aa8c48f952e2e63e0b0"
 
 "@types/parse-torrent-file@*":
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-5.16.0.tgz#b35085050b41b81b9425faa3616f925239685f88"
 
 "@types/webpack@^3.0.0":
-  version "3.0.10"
-  resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-3.0.10.tgz#1d27db07df32109f8c882535b547aae4252fd53e"
+  version "3.0.13"
+  resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-3.0.13.tgz#5a49ae51e784e73bc46830a6a20656e85b8af0e6"
   dependencies:
     "@types/node" "*"
     "@types/tapable" "*"
@@ -205,12 +205,12 @@ acorn@^5.0.0, acorn@^5.1.1:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75"
 
 add-asset-html-webpack-plugin@^2.0.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/add-asset-html-webpack-plugin/-/add-asset-html-webpack-plugin-2.1.1.tgz#bbcdfbfad657847d1b9424e9ed4476650975180c"
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/add-asset-html-webpack-plugin/-/add-asset-html-webpack-plugin-2.1.2.tgz#b3e60192602cdc53f03f2b19b7de36b5c4a6c7fe"
   dependencies:
-    bluebird "^3.4.6"
+    bluebird "^3.5.0"
     globby "^6.1.0"
-    minimatch "^3.0.3"
+    minimatch "^3.0.4"
 
 addr-to-ip-port@^1.0.1, addr-to-ip-port@^1.4.2:
   version "1.4.2"
@@ -273,8 +273,8 @@ angular-pipes@^6.0.0:
   resolved "https://registry.yarnpkg.com/angular-pipes/-/angular-pipes-6.5.3.tgz#6bed37c51ebc2adaf3412663bfe25179d0489b02"
 
 angular2-notifications@^0.7.7:
-  version "0.7.7"
-  resolved "https://registry.yarnpkg.com/angular2-notifications/-/angular2-notifications-0.7.7.tgz#50471d7325483cd0679eeb670c6d3a4618647a2f"
+  version "0.7.8"
+  resolved "https://registry.yarnpkg.com/angular2-notifications/-/angular2-notifications-0.7.8.tgz#ecbcb95a8d2d402af94a9a080d6664c70d33a029"
 
 angular2-template-loader@^0.6.0:
   version "0.6.2"
@@ -1126,14 +1126,14 @@ block-stream@*:
   dependencies:
     inherits "~2.0.0"
 
-bluebird@^2.10.2:
-  version "2.11.0"
-  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1"
-
-bluebird@^3.4.6, bluebird@^3.4.7:
+bluebird@^3.4.7:
   version "3.5.0"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
 
+bluebird@^3.5.0, bluebird@^3.5.1:
+  version "3.5.1"
+  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
+
 bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
   version "4.11.8"
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
@@ -1567,8 +1567,8 @@ code-point-at@^1.0.0:
   resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
 
 codelyzer@^3.0.0-beta.4:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/codelyzer/-/codelyzer-3.2.0.tgz#68eb0a67771ea73006b517053c3035c1838abf14"
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/codelyzer/-/codelyzer-3.2.1.tgz#5b1ac75f7e0eb04647842ee29a322bf2167e7229"
   dependencies:
     app-root-path "^2.0.1"
     css-selector-tokenizer "^0.7.0"
@@ -1730,16 +1730,16 @@ copy-descriptor@^0.1.0:
   resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
 
 copy-webpack-plugin@^4.0.0:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.0.1.tgz#9728e383b94316050d0c7463958f2b85c0aa8200"
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.1.1.tgz#53ae69e04955ebfa9fda411f54cbb968531d71fd"
   dependencies:
-    bluebird "^2.10.2"
-    fs-extra "^0.26.4"
-    glob "^6.0.4"
-    is-glob "^3.1.0"
+    bluebird "^3.5.1"
+    fs-extra "^4.0.2"
+    glob "^7.1.2"
+    is-glob "^4.0.0"
     loader-utils "^0.2.15"
     lodash "^4.3.0"
-    minimatch "^3.0.0"
+    minimatch "^3.0.4"
     node-dir "^0.1.10"
 
 core-js@^2.4.0, core-js@^2.5.0:
@@ -2003,13 +2003,6 @@ deep-is@~0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
 
-default-gateway@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-2.0.2.tgz#e365db05c50a4643cc1990c6178228c540a0b910"
-  dependencies:
-    execa "^0.7.0"
-    ip-regex "^2.1.0"
-
 define-properties@^1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94"
@@ -2683,8 +2676,8 @@ extglob@^1.1.0:
     to-regex "^2.1.0"
 
 extract-text-webpack-plugin@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.0.tgz#90caa7907bc449f335005e3ac7532b41b00de612"
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.1.tgz#605a8893faca1dd49bb0d2ca87493f33fd43d102"
   dependencies:
     async "^2.4.1"
     loader-utils "^1.1.0"
@@ -2733,11 +2726,12 @@ file-entry-cache@^2.0.0:
     flat-cache "^1.2.1"
     object-assign "^4.0.1"
 
-file-loader@^0.11.2:
-  version "0.11.2"
-  resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.11.2.tgz#4ff1df28af38719a6098093b88c82c71d1794a34"
+file-loader@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.5.tgz#91c25b6b6fbe56dae99f10a425fd64933b5c9daa"
   dependencies:
     loader-utils "^1.0.2"
+    schema-utils "^0.3.0"
 
 filename-regex@^2.0.0:
   version "2.0.1"
@@ -2892,15 +2886,13 @@ fs-chunk-store@^1.6.2:
     run-parallel "^1.1.2"
     thunky "^1.0.1"
 
-fs-extra@^0.26.4:
-  version "0.26.7"
-  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.26.7.tgz#9ae1fdd94897798edab76d0918cf42d0c3184fa9"
+fs-extra@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.2.tgz#f91704c53d1b461f893452b0c307d9997647ab6b"
   dependencies:
     graceful-fs "^4.1.2"
-    jsonfile "^2.1.0"
-    klaw "^1.0.0"
-    path-is-absolute "^1.0.0"
-    rimraf "^2.2.8"
+    jsonfile "^4.0.0"
+    universalify "^0.1.0"
 
 fs.realpath@^1.0.0:
   version "1.0.0"
@@ -3010,17 +3002,7 @@ glob-parent@^2.0.0:
   dependencies:
     is-glob "^2.0.0"
 
-glob@^6.0.4:
-  version "6.0.4"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
-  dependencies:
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "2 || 3"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
-glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@~7.1.1:
+glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1:
   version "7.1.2"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
   dependencies:
@@ -3078,7 +3060,7 @@ globule@^1.0.0:
     lodash "~4.17.4"
     minimatch "~3.0.2"
 
-graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
+graceful-fs@^4.1.2, graceful-fs@^4.1.6:
   version "4.1.11"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
 
@@ -3361,6 +3343,12 @@ ini@~1.3.0:
   version "1.3.4"
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
 
+inline-manifest-webpack-plugin@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/inline-manifest-webpack-plugin/-/inline-manifest-webpack-plugin-3.0.1.tgz#ca2151063115298e2fd94b669ab76c7dd63e44ad"
+  dependencies:
+    source-map-url "0.4.0"
+
 inquirer@^0.12.0:
   version "0.12.0"
   resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
@@ -3379,12 +3367,11 @@ inquirer@^0.12.0:
     strip-ansi "^3.0.0"
     through "^2.3.6"
 
-internal-ip@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-2.0.2.tgz#bed2b35491e8b42aee087de7614e870908ee80f2"
+internal-ip@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c"
   dependencies:
-    default-gateway "^2.0.2"
-    ipaddr.js "^1.5.1"
+    meow "^3.3.0"
 
 interpret@^1.0.0:
   version "1.0.3"
@@ -3404,10 +3391,6 @@ invert-kv@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
 
-ip-regex@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
-
 ip-set@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/ip-set/-/ip-set-1.0.1.tgz#633b66d0bd6c8d0de968d053263c9120d3b6727e"
@@ -3422,7 +3405,7 @@ ipaddr.js@1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0"
 
-"ipaddr.js@>= 0.1.5", ipaddr.js@^1.0.1, ipaddr.js@^1.5.1:
+"ipaddr.js@>= 0.1.5", ipaddr.js@^1.0.1:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0"
 
@@ -3548,6 +3531,12 @@ is-glob@^3.1.0:
   dependencies:
     is-extglob "^2.1.0"
 
+is-glob@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0"
+  dependencies:
+    is-extglob "^2.1.1"
+
 is-my-json-valid@^2.10.0:
   version "2.16.1"
   resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11"
@@ -3741,12 +3730,18 @@ json5@^0.5.0, json5@^0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
 
-jsonfile@^2.1.0, jsonfile@^2.4.0:
+jsonfile@^2.4.0:
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"
   optionalDependencies:
     graceful-fs "^4.1.6"
 
+jsonfile@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
 jsonify@~0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
@@ -3818,12 +3813,6 @@ kind-of@^5.0.0:
   version "5.0.2"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.0.2.tgz#f57bec933d9a2209ffa96c5c08343607b7035fda"
 
-klaw@^1.0.0:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
-  optionalDependencies:
-    graceful-fs "^4.1.9"
-
 lazy-cache@^0.2.3:
   version "0.2.7"
   resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65"
@@ -4232,7 +4221,7 @@ memory-fs@^0.4.0, memory-fs@~0.4.1:
     errno "^0.1.3"
     readable-stream "^2.0.1"
 
-meow@^3.7.0:
+meow@^3.3.0, meow@^3.7.0:
   version "3.7.0"
   resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
   dependencies:
@@ -4312,10 +4301,14 @@ mime@1.3.4:
   version "1.3.4"
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
 
-mime@1.3.x, mime@^1.3.4:
+mime@^1.3.4:
   version "1.3.6"
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0"
 
+mime@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
+
 mimic-fn@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
@@ -4338,7 +4331,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
 
-"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2:
+minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
   dependencies:
@@ -5352,8 +5345,8 @@ pretty-error@^2.0.2:
     utila "~0.4"
 
 primeng@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/primeng/-/primeng-4.2.0.tgz#49c8c99de26d254f41d3fbb8759227fe1d269772"
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/primeng/-/primeng-4.2.2.tgz#b76116c24505ddcad7f52a4bba76b584d8e7c527"
 
 private@^0.1.6, private@^0.1.7, private@~0.1.5:
   version "0.1.7"
@@ -5920,8 +5913,8 @@ sass-loader@^6.0.3:
     pify "^3.0.0"
 
 sass-resources-loader@^1.2.1:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/sass-resources-loader/-/sass-resources-loader-1.3.0.tgz#f061f8df7cc8ecda9ee7fd9b602ca8ee2ea73612"
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/sass-resources-loader/-/sass-resources-loader-1.3.1.tgz#6784abca83818e4f7c96a4da86af187e8615357e"
   dependencies:
     async "^2.1.4"
     chalk "^1.1.3"
@@ -6207,8 +6200,8 @@ source-list-map@~0.1.7:
   resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106"
 
 source-map-loader@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-0.2.1.tgz#48126be9230bd47fad05e46a8c3c2e3d2dabe507"
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-0.2.2.tgz#1249348ff6a66ea64a2957fc98f74cb6bba67505"
   dependencies:
     async "^0.9.0"
     loader-utils "~0.2.2"
@@ -6238,7 +6231,7 @@ source-map-support@^0.4.0, source-map-support@^0.4.15, source-map-support@^0.4.2
   dependencies:
     source-map "^0.5.6"
 
-source-map-url@^0.4.0:
+source-map-url@0.4.0, source-map-url@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
 
@@ -6495,9 +6488,9 @@ strip-json-comments@^2.0.0, strip-json-comments@^2.0.1, strip-json-comments@~2.0
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
 
-style-loader@^0.18.2:
-  version "0.18.2"
-  resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.18.2.tgz#cc31459afbcd6d80b7220ee54b291a9fd66ff5eb"
+style-loader@^0.19.0:
+  version "0.19.0"
+  resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.19.0.tgz#7258e788f0fee6a42d710eaf7d6c2412a4c50759"
   dependencies:
     loader-utils "^1.0.2"
     schema-utils "^0.3.0"
@@ -6822,8 +6815,8 @@ typedarray@^0.0.6:
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
 
 typescript@^2.5.2:
-  version "2.5.2"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.2.tgz#038a95f7d9bbb420b1bf35ba31d4c5c1dd3ffe34"
+  version "2.5.3"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.3.tgz#df3dcdc38f3beb800d4bc322646b04a3f6ca7f0d"
 
 uglify-js@3.0.x, uglify-js@^3.0.6:
   version "3.0.28"
@@ -6888,6 +6881,10 @@ uniqs@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
 
+universalify@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7"
+
 unordered-array-remove@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/unordered-array-remove/-/unordered-array-remove-1.0.2.tgz#c546e8f88e317a0cf2644c97ecb57dba66d250ef"
@@ -6915,12 +6912,13 @@ urix@^0.1.0, urix@~0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
 
-url-loader@^0.5.7:
-  version "0.5.9"
-  resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.5.9.tgz#cc8fea82c7b906e7777019250869e569e995c295"
+url-loader@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.6.2.tgz#a007a7109620e9d988d14bce677a1decb9a993f7"
   dependencies:
     loader-utils "^1.0.2"
-    mime "1.3.x"
+    mime "^1.4.1"
+    schema-utils "^0.3.0"
 
 url-parse@1.0.x:
   version "1.0.5"
@@ -7055,8 +7053,8 @@ video.js@^5.19.2:
     xhr "2.2.2"
 
 video.js@^6.2.0:
-  version "6.2.8"
-  resolved "https://registry.yarnpkg.com/video.js/-/video.js-6.2.8.tgz#e449710bf8513f607456293ae1da97559a94fb97"
+  version "6.3.2"
+  resolved "https://registry.yarnpkg.com/video.js/-/video.js-6.3.2.tgz#53f7cd08e4219157e4053b795673c3a9fb3d3072"
   dependencies:
     babel-runtime "^6.9.2"
     global "4.3.2"
@@ -7158,8 +7156,8 @@ webpack-dev-middleware@^1.11.0:
     time-stamp "^2.0.0"
 
 webpack-dev-server@^2.4.5:
-  version "2.8.2"
-  resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.8.2.tgz#abd61f410778cc4c843d7cebbf41465b1ab7734c"
+  version "2.9.1"
+  resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.9.1.tgz#7ac9320b61b00eb65b2109f15c82747fc5b93585"
   dependencies:
     ansi-html "0.0.7"
     array-includes "^3.0.3"
@@ -7171,7 +7169,7 @@ webpack-dev-server@^2.4.5:
     express "^4.13.3"
     html-entities "^1.2.0"
     http-proxy-middleware "~0.17.4"
-    internal-ip "^2.0.2"
+    internal-ip "1.2.0"
     ip "^1.1.5"
     loglevel "^1.4.1"
     opn "^5.1.0"
@@ -7222,8 +7220,8 @@ webpack-sources@^1.0.1:
     source-map "~0.5.3"
 
 webpack@^3.3.0:
-  version "3.5.6"
-  resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.5.6.tgz#a492fb6c1ed7f573816f90e00c8fbb5a20cc5c36"
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.6.0.tgz#a89a929fbee205d35a4fa2cc487be9cbec8898bc"
   dependencies:
     acorn "^5.0.0"
     acorn-dynamic-import "^2.0.0"
@@ -7526,5 +7524,5 @@ zero-fill@^2.2.3:
   resolved "https://registry.yarnpkg.com/zero-fill/-/zero-fill-2.2.3.tgz#a3def06ba5e39ae644850bb4ca2ad4112b4855e9"
 
 zone.js@~0.8.5:
-  version "0.8.17"
-  resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.17.tgz#4c5e5185a857da8da793daf3919371c5a36b2a0b"
+  version "0.8.18"
+  resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.18.tgz#8cecb3977fcd1b3090562ff4570e2847e752b48d"