]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Use ng2-file-upload instead of jquery and add tags support to the video
authorChocobozzz <florian.bigard@gmail.com>
Tue, 7 Jun 2016 20:34:02 +0000 (22:34 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Tue, 7 Jun 2016 20:34:02 +0000 (22:34 +0200)
upload form

15 files changed:
client/.bootstraprc
client/config/webpack.common.js
client/package.json
client/src/app/shared/search/search-field.type.ts
client/src/app/shared/search/search.component.ts
client/src/app/shared/users/auth.service.ts
client/src/app/videos/video-add/video-add.component.html
client/src/app/videos/video-add/video-add.component.scss
client/src/app/videos/video-add/video-add.component.ts
client/src/vendor.ts
client/tsconfig.json
client/typings.json
server/initializers/constants.js
server/middlewares/reqValidators/videos.js
server/models/videos.js

index 76a0bdb7b15d59ce90cf0df180b811d99d317388..dd6c2128cbebde65835b98a2839f66d01d8a159d 100644 (file)
@@ -86,7 +86,7 @@ styles:
   breadcrumbs: false
   pagination: true
   pager: false
-  labels: false
+  labels: true
   badges: false
   jumbotron: false
   thumbnails: true
@@ -112,7 +112,7 @@ styles:
 ### Bootstrap scripts
 scripts:
   transition: false
-  alert: true
+  alert: false
   button: false
   carousel: false
   collapse: false
index 7f1da74b9afbecaaa7d2267878af04a5d27e6e10..9d05668e2e6f9e0eafd91c1b616aedaa9fd9e28f 100644 (file)
@@ -68,7 +68,7 @@ module.exports = {
     root: helpers.root('src'),
 
     // remove other default values
-    modulesDirectories: [ 'node_modules', 'node_modules/blueimp-file-upload/js/vendor' ],
+    modulesDirectories: [ 'node_modules' ],
 
     packageAlias: 'browser'
 
@@ -246,12 +246,6 @@ module.exports = {
       chunksSortMode: 'dependency'
     }),
 
-    new webpack.ProvidePlugin({
-      jQuery: 'jquery',
-      $: 'jquery',
-      jquery: 'jquery'
-    })
-
   ],
 
   /*
index d2d039437abeb88484f35bf225673c27f1fbf8bc..cd8afcc981f0e82f5b30c14d6a2e69aa325ae417 100644 (file)
@@ -28,7 +28,6 @@
     "@angular/router-deprecated": "2.0.0-rc.1",
     "angular-pipes": "^2.0.0",
     "awesome-typescript-loader": "^0.17.0",
-    "blueimp-file-upload": "^9.12.1",
     "bootstrap-loader": "^1.0.8",
     "bootstrap-sass": "^3.3.6",
     "compression-webpack-plugin": "^0.3.1",
     "es6-shim": "^0.35.0",
     "file-loader": "^0.8.5",
     "html-webpack-plugin": "^2.19.0",
-    "jquery": "^2.2.3",
-    "jquery.ui.widget": "^1.10.3",
     "json-loader": "^0.5.4",
     "ng2-bootstrap": "^1.0.16",
+    "ng2-file-upload": "^1.0.3",
     "node-sass": "^3.7.0",
     "normalize.css": "^4.1.1",
     "raw-loader": "^0.5.1",
@@ -75,4 +73,4 @@
       "bundles/"
     ]
   }
-}
\ No newline at end of file
+}
index 846236290cb4ce21f42d78f444e351ce94856488..5228ee68a63b30d1abdb8788662b92f326c56370 100644 (file)
@@ -1 +1 @@
-export type SearchField = "name" | "author" | "podUrl" | "magnetUri";
+export type SearchField = "name" | "author" | "podUrl" | "magnetUri" | "tags";
index 31f8b1535d21f5f5301649f442dcd41d288ef2a2..c14c2d99c50473e728ee594db927c457c59cadc0 100644 (file)
@@ -18,7 +18,8 @@ export class SearchComponent {
     name: 'Name',
     author: 'Author',
     podUrl: 'Pod Url',
-    magnetUri: 'Magnet Uri'
+    magnetUri: 'Magnet Uri',
+    tags: 'Tags'
   };
   searchCriterias: Search = {
     field: 'name',
index 720037563beeada57d3c2c7d42942e627060046b..1c822c1e11650dd869d374d1783e4a3afd60344c 100644 (file)
@@ -43,7 +43,11 @@ export class AuthService {
   }
 
   getRequestHeader() {
-    return new Headers({ 'Authorization': `${this.getTokenType()} ${this.getToken()}` });
+    return new Headers({ 'Authorization': this.getRequestHeaderValue() });
+  }
+
+  getRequestHeaderValue() {
+    return `${this.getTokenType()} ${this.getToken()}`;
   }
 
   getToken() {
index cbe274e8a598a537545e522dd1a0e75c721e3e14..6b2eb9377df4e350cca02ebe15778afa62bca097 100644 (file)
@@ -2,42 +2,74 @@
 
 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
 
-<form (ngSubmit)="uploadFile()" #videoForm="ngForm">
+<form novalidate (ngSubmit)="upload()" [ngFormModel]="videoForm">
   <div class="form-group">
-    <label for="name">Video name</label>
+    <label for="name">Name</label>
     <input
-      type="text" class="form-control" name="name" id="name" required
-      ngControl="name"  #name="ngForm"
+      type="text" class="form-control" name="name" id="name"
+      ngControl="name" #name="ngForm" [(ngModel)]="video.name"
     >
-    <div [hidden]="name.valid || name.pristine" class="alert alert-danger">
-      Name is required
+    <div [hidden]="name.valid || name.pristine" class="alert alert-warning">
+      A name is required and should be between 3 and 50 characters long
     </div>
   </div>
 
   <div class="form-group">
-    <div class="btn btn-default btn-file">
+    <label for="tags">Tags</label>
+    <input
+      type="text" class="form-control" name="tags" id="tags"
+      ngControl="tags" #tags="ngForm" [disabled]="isTagsInputDisabled" (keyup)="onTagKeyPress($event)" [(ngModel)]="currentTag"
+    >
+    <div [hidden]="tags.valid || tags.pristine" class="alert alert-warning">
+      A tag should be between 2 and 10 characters long
+    </div>
+  </div>
+
+  <div class="tags">
+    <div class="label label-info tag" *ngFor="let tag of video.tags">
+      {{ tag }}
+      <span class="remove" (click)="removeTag(tag)">x</span>
+    </div>
+  </div>
+
+  <div class="form-group">
+    <label for="videofile">File</label>
+    <div class="btn btn-default btn-file" [ngClass]="{ 'disabled': filename !== null }" >
       <span>Select the video...</span>
-      <input type="file" name="videofile" id="videofile">
+      <input
+        type="file" name="videofile" id="videofile"
+        ng2FileSelect [uploader]="uploader" [disabled]="filename !== null"
+      >
     </div>
+  </div>
 
-    <span *ngIf="fileToUpload">{{ fileToUpload.name }}</span>
+  <div class="file-to-upload">
+    <div class="file" *ngIf="uploader.queue.length > 0">
+      <span class="filename">{{ filename }}</span>
+      <span class="glyphicon glyphicon-remove" (click)="removeFile()"></span>
+    </div>
   </div>
 
   <div class="form-group">
     <label for="description">Description</label>
     <textarea
-      name="description" id="description" class="form-control" placeholder="Description..." required
-      ngControl="description"  #description="ngForm"
+      name="description" id="description" class="form-control" placeholder="Description..."
+      ngControl="description"  #description="ngForm" [(ngModel)]="video.description"
     >
     </textarea>
-    <div [hidden]="description.valid || description.pristine" class="alert alert-danger">
-        A description is required
+    <div [hidden]="description.valid || description.pristine" class="alert alert-warning">
+        A description is required and should be between 3 and 250 characters long
     </div>
   </div>
 
-  <div id="progress" *ngIf="progressBar.max !== 0">
-    <progressbar [value]="progressBar.value" [max]="progressBar.max">{{ progressBar.value | bytes }} / {{ progressBar.max | bytes }}</progressbar>
+  <div class="progress">
+    <progressbar [value]="uploader.progress" max="100"></progressbar>
   </div>
 
-  <input type="submit" value="Upload" class="btn btn-default" [disabled]="!videoForm.form.valid || !fileToUpload">
+  <div class="form-group">
+    <input
+      type="submit" value="Upload" class="btn btn-default form-control" [title]="getInvalidFieldsTitle()"
+      [disabled]="!videoForm.valid || video.tags.length === 0 || filename === null"
+    >
+  </div>
 </form>
index 01195f0175b3e5c137aa243c53a92be705381bda..d66df2fd4859128dfb0c3d94dcca4f8b65c516a8 100644 (file)
@@ -1,6 +1,7 @@
 .btn-file {
   position: relative;
   overflow: hidden;
+  display: block;
 }
 
 .btn-file input[type=file] {
   margin-bottom: 10px;
 }
 
-#progress {
-  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;
+  }
+}
+
+div.progress {
+  // height: 40px;
 }
index 144879a54c9d2c15704388388bfc02c6585b22e4..2b45ea125f8b297d670f7dafe4d0e3098a5756f1 100644 (file)
@@ -1,29 +1,31 @@
-/// <reference path="../../../../typings/globals/jquery/index.d.ts" />
-/// <reference path="../../../../typings/globals/jquery.fileupload/index.d.ts" />
-
+import { Control, ControlGroup, Validators } from '@angular/common';
 import { Component, ElementRef, OnInit } from '@angular/core';
 import { Router } from '@angular/router-deprecated';
 
 import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
 import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar';
+import { FileSelectDirective, FileUploader } from 'ng2-file-upload/ng2-file-upload';
 
-import { AuthService, User } from '../../shared';
+import { AuthService } from '../../shared';
 
 @Component({
   selector: 'my-videos-add',
   styles: [ require('./video-add.component.scss') ],
   template: require('./video-add.component.html'),
-  directives: [ PROGRESSBAR_DIRECTIVES ],
+  directives: [ FileSelectDirective, PROGRESSBAR_DIRECTIVES ],
   pipes: [ BytesPipe ]
 })
 
 export class VideoAddComponent implements OnInit {
+  currentTag: string; // Tag the user is writing in the input
   error: string = null;
-  fileToUpload: any;
-  progressBar: { value: number; max: number; } = { value: 0, max: 0 };
-  user: User;
-
-  private form: any;
+  videoForm: ControlGroup;
+  uploader: FileUploader;
+  video = {
+    name: '',
+    tags: [],
+    description: ''
+  };
 
   constructor(
     private authService: AuthService,
@@ -31,52 +33,108 @@ export class VideoAddComponent implements OnInit {
     private router: Router
   ) {}
 
+  get filename() {
+    if (this.uploader.queue.length === 0) {
+      return null;
+    }
+
+    return this.uploader.queue[0].file.name;
+  }
+
+  get isTagsInputDisabled () {
+    return this.video.tags.length >= 3;
+  }
+
+  getInvalidFieldsTitle() {
+    let title = '';
+    const nameControl = this.videoForm.controls['name'];
+    const descriptionControl = this.videoForm.controls['description'];
+
+    if (!nameControl.valid) {
+      title += 'A name is required\n';
+    }
+
+    if (this.video.tags.length === 0) {
+      title += 'At least one tag is required\n';
+    }
+
+    if (this.filename === null) {
+      title += 'A file is required\n';
+    }
+
+    if (!descriptionControl.valid) {
+      title += 'A description is required\n';
+    }
+
+    return title;
+  }
+
   ngOnInit() {
-    this.user = User.load();
-    jQuery(this.elementRef.nativeElement).find('#videofile').fileupload({
+    this.videoForm = new ControlGroup({
+      name: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(50) ])),
+      description: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(250) ])),
+      tags: new Control('', Validators.pattern('^[a-zA-Z0-9]{2,10}$'))
+    });
+
+
+    this.uploader = new FileUploader({
+      authToken: this.authService.getRequestHeaderValue(),
+      queueLimit: 1,
       url: '/api/v1/videos',
-      dataType: 'json',
-      singleFileUploads: true,
-      multipart: true,
-      autoUpload: false,
-
-      add: (e, data) => {
-        this.form = data;
-        this.fileToUpload = data['files'][0];
-      },
-
-      progressall: (e, data) => {
-        this.progressBar.value = data.loaded;
-        // The server is a little bit slow to answer (has to seed the video)
-        // So we add more time to the progress bar (+10%)
-        this.progressBar.max = data.total + (0.1 * data.total);
-      },
-
-      done: (e, data) => {
-        this.progressBar.value = this.progressBar.max;
-        console.log('Video uploaded.');
-
-        // Print all the videos once it's finished
-        this.router.navigate(['VideosList']);
-      },
-
-      fail: (e, data) => {
-        const xhr = data.jqXHR;
-        if (xhr.status === 400) {
-          this.error = xhr.responseText;
-        } else {
-          this.error = 'Unknow error';
-        }
-
-        console.error(data);
-      }
+      removeAfterUpload: true
     });
+
+    this.uploader.onBuildItemForm = (item, form) => {
+      form.append('name', this.video.name);
+      form.append('description', this.video.description);
+
+      for (let i = 0; i < this.video.tags.length; i++) {
+        form.append(`tags[${i}]`, this.video.tags[i]);
+      }
+    };
+  }
+
+  onTagKeyPress(event: KeyboardEvent) {
+    // Enter press
+    if (event.keyCode === 13) {
+      // Check if the tag is valid and does not already exist
+      if (
+        this.currentTag !== '' &&
+        this.videoForm.controls['tags'].valid &&
+        this.video.tags.indexOf(this.currentTag) === -1
+      ) {
+        this.video.tags.push(this.currentTag);
+        this.currentTag = '';
+      }
+    }
+  }
+
+  removeFile() {
+    this.uploader.clearQueue();
+  }
+
+  removeTag(tag: string) {
+    this.video.tags.splice(this.video.tags.indexOf(tag), 1);
   }
 
-  uploadFile() {
-    this.error = null;
-    this.form.formData = jQuery(this.elementRef.nativeElement).find('form').serializeArray();
-    this.form.headers = this.authService.getRequestHeader().toJSON();
-    this.form.submit();
+  upload() {
+    const item = this.uploader.queue[0];
+    // TODO: wait for https://github.com/valor-software/ng2-file-upload/pull/242
+    item.alias = 'videofile';
+
+    item.onSuccess = () => {
+      console.log('Video uploaded.');
+
+      // Print all the videos once it's finished
+      this.router.navigate(['VideosList']);
+    };
+
+    item.onError = (response: string, status: number) => {
+      this.error = (status === 400) ? response : 'Unknow error';
+      console.error(this.error);
+    };
+
+
+    this.uploader.uploadAll();
   }
 }
index 496f44cf6cea941b7979dfcb405b545ab5f775a9..437d05822634bdc4201e29d36fc9b41f7e8d0109 100644 (file)
@@ -18,7 +18,5 @@ import 'rxjs/add/operator/catch';
 import 'rxjs/add/operator/map';
 import 'rxjs/add/operator/mergeMap';
 
-import 'jquery';
 import 'bootstrap-loader';
-import 'jquery.ui.widget/jquery.ui.widget';
-import 'blueimp-file-upload';
+import 'ng2-file-upload';
index 3b903f8c831dcb11ef5104050f237f7470c79b5b..fdcf742ea09cba4ca8541687c50ac505f0f927ba 100644 (file)
@@ -65,8 +65,6 @@
     "src/vendor.ts",
     "typings/globals/es6-shim/index.d.ts",
     "typings/globals/jasmine/index.d.ts",
-    "typings/globals/jquery.fileupload/index.d.ts",
-    "typings/globals/jquery/index.d.ts",
     "typings/globals/node/index.d.ts",
     "typings/index.d.ts"
   ]
index ff8b56a4841376ab8be138ed5bf819fa84ed40a9..9a8891f25f86fc1378759c0a99de59272edd6f3f 100644 (file)
@@ -2,8 +2,6 @@
   "globalDependencies": {
     "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654",
     "jasmine": "registry:dt/jasmine#2.2.0+20160412134438",
-    "jquery": "registry:dt/jquery#1.10.0+20160417213236",
-    "jquery.fileupload": "registry:dt/jquery.fileupload#5.40.1+20160316155526",
     "node": "registry:dt/node#4.0.0+20160509154515"
   }
 }
index 6fa322010330f1bee4f6d18074f0890d6430b782..22cbb13619a6671152744c671c1c2fa9af08307a 100644 (file)
@@ -41,8 +41,8 @@ const THUMBNAILS_SIZE = '200x110'
 const THUMBNAILS_STATIC_PATH = '/static/thumbnails'
 
 const VIDEOS_CONSTRAINTS_FIELDS = {
-  NAME: { min: 1, max: 50 }, // Length
-  DESCRIPTION: { min: 1, max: 250 }, // Length
+  NAME: { min: 3, max: 50 }, // Length
+  DESCRIPTION: { min: 3, max: 250 }, // Length
   MAGNET_URI: { min: 10 }, // Length
   DURATION: { min: 1, max: 7200 }, // Number
   AUTHOR: { min: 3, max: 20 }, // Length
index 3618e47160bf9f6397de4532aa2155f4ff04b56c..f31fd93a21c798c7a431f73526d4fbd6427a9ef1 100644 (file)
@@ -32,7 +32,7 @@ function videosAdd (req, res, next) {
       }
 
       if (!customValidators.isVideoDurationValid(duration)) {
-        return res.status(400).send('Duration of the video file is too big (max: ' + constants.MAXIMUM_VIDEO_DURATION + 's).')
+        return res.status(400).send('Duration of the video file is too big (max: ' + constants.VIDEOS_CONSTRAINTS_FIELDS.DURATION.max + 's).')
       }
 
       videoFile.duration = duration
index d6b743c7c46fe4db7ae5e0332f56d6adfe072289..c177b414ce467da93bc718fdb6a10de053957be0 100644 (file)
@@ -12,6 +12,7 @@ const port = config.get('webserver.port')
 
 // ---------------------------------------------------------------------------
 
+// TODO: add indexes on searchable columns
 const videosSchema = mongoose.Schema({
   name: String,
   namePath: String,