Use ng2-file-upload instead of jquery and add tags support to the video

upload form
pull/10/head
Chocobozzz 2016-06-07 22:34:02 +02:00
parent 8c255eb53c
commit e822fdaeee
15 changed files with 204 additions and 99 deletions

View File

@ -86,7 +86,7 @@ styles:
breadcrumbs: false breadcrumbs: false
pagination: true pagination: true
pager: false pager: false
labels: false labels: true
badges: false badges: false
jumbotron: false jumbotron: false
thumbnails: true thumbnails: true
@ -112,7 +112,7 @@ styles:
### Bootstrap scripts ### Bootstrap scripts
scripts: scripts:
transition: false transition: false
alert: true alert: false
button: false button: false
carousel: false carousel: false
collapse: false collapse: false

View File

@ -68,7 +68,7 @@ module.exports = {
root: helpers.root('src'), root: helpers.root('src'),
// remove other default values // remove other default values
modulesDirectories: [ 'node_modules', 'node_modules/blueimp-file-upload/js/vendor' ], modulesDirectories: [ 'node_modules' ],
packageAlias: 'browser' packageAlias: 'browser'
@ -246,12 +246,6 @@ module.exports = {
chunksSortMode: 'dependency' chunksSortMode: 'dependency'
}), }),
new webpack.ProvidePlugin({
jQuery: 'jquery',
$: 'jquery',
jquery: 'jquery'
})
], ],
/* /*

View File

@ -28,7 +28,6 @@
"@angular/router-deprecated": "2.0.0-rc.1", "@angular/router-deprecated": "2.0.0-rc.1",
"angular-pipes": "^2.0.0", "angular-pipes": "^2.0.0",
"awesome-typescript-loader": "^0.17.0", "awesome-typescript-loader": "^0.17.0",
"blueimp-file-upload": "^9.12.1",
"bootstrap-loader": "^1.0.8", "bootstrap-loader": "^1.0.8",
"bootstrap-sass": "^3.3.6", "bootstrap-sass": "^3.3.6",
"compression-webpack-plugin": "^0.3.1", "compression-webpack-plugin": "^0.3.1",
@ -40,10 +39,9 @@
"es6-shim": "^0.35.0", "es6-shim": "^0.35.0",
"file-loader": "^0.8.5", "file-loader": "^0.8.5",
"html-webpack-plugin": "^2.19.0", "html-webpack-plugin": "^2.19.0",
"jquery": "^2.2.3",
"jquery.ui.widget": "^1.10.3",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"ng2-bootstrap": "^1.0.16", "ng2-bootstrap": "^1.0.16",
"ng2-file-upload": "^1.0.3",
"node-sass": "^3.7.0", "node-sass": "^3.7.0",
"normalize.css": "^4.1.1", "normalize.css": "^4.1.1",
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
@ -75,4 +73,4 @@
"bundles/" "bundles/"
] ]
} }
} }

View File

@ -1 +1 @@
export type SearchField = "name" | "author" | "podUrl" | "magnetUri"; export type SearchField = "name" | "author" | "podUrl" | "magnetUri" | "tags";

View File

@ -18,7 +18,8 @@ export class SearchComponent {
name: 'Name', name: 'Name',
author: 'Author', author: 'Author',
podUrl: 'Pod Url', podUrl: 'Pod Url',
magnetUri: 'Magnet Uri' magnetUri: 'Magnet Uri',
tags: 'Tags'
}; };
searchCriterias: Search = { searchCriterias: Search = {
field: 'name', field: 'name',

View File

@ -43,7 +43,11 @@ export class AuthService {
} }
getRequestHeader() { getRequestHeader() {
return new Headers({ 'Authorization': `${this.getTokenType()} ${this.getToken()}` }); return new Headers({ 'Authorization': this.getRequestHeaderValue() });
}
getRequestHeaderValue() {
return `${this.getTokenType()} ${this.getToken()}`;
} }
getToken() { getToken() {

View File

@ -2,42 +2,74 @@
<div *ngIf="error" class="alert alert-danger">{{ error }}</div> <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<form (ngSubmit)="uploadFile()" #videoForm="ngForm"> <form novalidate (ngSubmit)="upload()" [ngFormModel]="videoForm">
<div class="form-group"> <div class="form-group">
<label for="name">Video name</label> <label for="name">Name</label>
<input <input
type="text" class="form-control" name="name" id="name" required type="text" class="form-control" name="name" id="name"
ngControl="name" #name="ngForm" ngControl="name" #name="ngForm" [(ngModel)]="video.name"
> >
<div [hidden]="name.valid || name.pristine" class="alert alert-danger"> <div [hidden]="name.valid || name.pristine" class="alert alert-warning">
Name is required A name is required and should be between 3 and 50 characters long
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="btn btn-default btn-file"> <label for="tags">Tags</label>
<span>Select the video...</span> <input
<input type="file" name="videofile" id="videofile"> 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>
<span *ngIf="fileToUpload">{{ fileToUpload.name }}</span> <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"
ng2FileSelect [uploader]="uploader" [disabled]="filename !== null"
>
</div>
</div>
<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>
<div class="form-group"> <div class="form-group">
<label for="description">Description</label> <label for="description">Description</label>
<textarea <textarea
name="description" id="description" class="form-control" placeholder="Description..." required name="description" id="description" class="form-control" placeholder="Description..."
ngControl="description" #description="ngForm" ngControl="description" #description="ngForm" [(ngModel)]="video.description"
> >
</textarea> </textarea>
<div [hidden]="description.valid || description.pristine" class="alert alert-danger"> <div [hidden]="description.valid || description.pristine" class="alert alert-warning">
A description is required A description is required and should be between 3 and 250 characters long
</div> </div>
</div> </div>
<div id="progress" *ngIf="progressBar.max !== 0"> <div class="progress">
<progressbar [value]="progressBar.value" [max]="progressBar.max">{{ progressBar.value | bytes }} / {{ progressBar.max | bytes }}</progressbar> <progressbar [value]="uploader.progress" max="100"></progressbar>
</div> </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> </form>

View File

@ -1,6 +1,7 @@
.btn-file { .btn-file {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
display: block;
} }
.btn-file input[type=file] { .btn-file input[type=file] {
@ -28,6 +29,28 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
#progress { div.tags {
margin-bottom: 10px; 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;
} }

View File

@ -1,29 +1,31 @@
/// <reference path="../../../../typings/globals/jquery/index.d.ts" /> import { Control, ControlGroup, Validators } from '@angular/common';
/// <reference path="../../../../typings/globals/jquery.fileupload/index.d.ts" />
import { Component, ElementRef, OnInit } from '@angular/core'; import { Component, ElementRef, OnInit } from '@angular/core';
import { Router } from '@angular/router-deprecated'; import { Router } from '@angular/router-deprecated';
import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'; import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar'; 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({ @Component({
selector: 'my-videos-add', selector: 'my-videos-add',
styles: [ require('./video-add.component.scss') ], styles: [ require('./video-add.component.scss') ],
template: require('./video-add.component.html'), template: require('./video-add.component.html'),
directives: [ PROGRESSBAR_DIRECTIVES ], directives: [ FileSelectDirective, PROGRESSBAR_DIRECTIVES ],
pipes: [ BytesPipe ] pipes: [ BytesPipe ]
}) })
export class VideoAddComponent implements OnInit { export class VideoAddComponent implements OnInit {
currentTag: string; // Tag the user is writing in the input
error: string = null; error: string = null;
fileToUpload: any; videoForm: ControlGroup;
progressBar: { value: number; max: number; } = { value: 0, max: 0 }; uploader: FileUploader;
user: User; video = {
name: '',
private form: any; tags: [],
description: ''
};
constructor( constructor(
private authService: AuthService, private authService: AuthService,
@ -31,52 +33,108 @@ export class VideoAddComponent implements OnInit {
private router: Router private router: Router
) {} ) {}
ngOnInit() { get filename() {
this.user = User.load(); if (this.uploader.queue.length === 0) {
jQuery(this.elementRef.nativeElement).find('#videofile').fileupload({ return null;
url: '/api/v1/videos', }
dataType: 'json',
singleFileUploads: true,
multipart: true,
autoUpload: false,
add: (e, data) => { return this.uploader.queue[0].file.name;
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);
}
});
} }
uploadFile() { get isTagsInputDisabled () {
this.error = null; return this.video.tags.length >= 3;
this.form.formData = jQuery(this.elementRef.nativeElement).find('form').serializeArray(); }
this.form.headers = this.authService.getRequestHeader().toJSON();
this.form.submit(); 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.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',
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);
}
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();
} }
} }

View File

@ -18,7 +18,5 @@ import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map'; import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/operator/mergeMap';
import 'jquery';
import 'bootstrap-loader'; import 'bootstrap-loader';
import 'jquery.ui.widget/jquery.ui.widget'; import 'ng2-file-upload';
import 'blueimp-file-upload';

View File

@ -65,8 +65,6 @@
"src/vendor.ts", "src/vendor.ts",
"typings/globals/es6-shim/index.d.ts", "typings/globals/es6-shim/index.d.ts",
"typings/globals/jasmine/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/globals/node/index.d.ts",
"typings/index.d.ts" "typings/index.d.ts"
] ]

View File

@ -2,8 +2,6 @@
"globalDependencies": { "globalDependencies": {
"es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654", "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654",
"jasmine": "registry:dt/jasmine#2.2.0+20160412134438", "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" "node": "registry:dt/node#4.0.0+20160509154515"
} }
} }

View File

@ -41,8 +41,8 @@ const THUMBNAILS_SIZE = '200x110'
const THUMBNAILS_STATIC_PATH = '/static/thumbnails' const THUMBNAILS_STATIC_PATH = '/static/thumbnails'
const VIDEOS_CONSTRAINTS_FIELDS = { const VIDEOS_CONSTRAINTS_FIELDS = {
NAME: { min: 1, max: 50 }, // Length NAME: { min: 3, max: 50 }, // Length
DESCRIPTION: { min: 1, max: 250 }, // Length DESCRIPTION: { min: 3, max: 250 }, // Length
MAGNET_URI: { min: 10 }, // Length MAGNET_URI: { min: 10 }, // Length
DURATION: { min: 1, max: 7200 }, // Number DURATION: { min: 1, max: 7200 }, // Number
AUTHOR: { min: 3, max: 20 }, // Length AUTHOR: { min: 3, max: 20 }, // Length

View File

@ -32,7 +32,7 @@ function videosAdd (req, res, next) {
} }
if (!customValidators.isVideoDurationValid(duration)) { 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 videoFile.duration = duration

View File

@ -12,6 +12,7 @@ const port = config.get('webserver.port')
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// TODO: add indexes on searchable columns
const videosSchema = mongoose.Schema({ const videosSchema = mongoose.Schema({
name: String, name: String,
namePath: String, namePath: String,