aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/.bootstraprc4
-rw-r--r--client/config/webpack.common.js8
-rw-r--r--client/package.json6
-rw-r--r--client/src/app/shared/search/search-field.type.ts2
-rw-r--r--client/src/app/shared/search/search.component.ts3
-rw-r--r--client/src/app/shared/users/auth.service.ts6
-rw-r--r--client/src/app/videos/video-add/video-add.component.html64
-rw-r--r--client/src/app/videos/video-add/video-add.component.scss27
-rw-r--r--client/src/app/videos/video-add/video-add.component.ts162
-rw-r--r--client/src/vendor.ts4
-rw-r--r--client/tsconfig.json2
-rw-r--r--client/typings.json2
-rw-r--r--server/initializers/constants.js4
-rw-r--r--server/middlewares/reqValidators/videos.js2
-rw-r--r--server/models/videos.js1
15 files changed, 201 insertions, 96 deletions
diff --git a/client/.bootstraprc b/client/.bootstraprc
index 76a0bdb7b..dd6c2128c 100644
--- a/client/.bootstraprc
+++ b/client/.bootstraprc
@@ -86,7 +86,7 @@ styles:
86 breadcrumbs: false 86 breadcrumbs: false
87 pagination: true 87 pagination: true
88 pager: false 88 pager: false
89 labels: false 89 labels: true
90 badges: false 90 badges: false
91 jumbotron: false 91 jumbotron: false
92 thumbnails: true 92 thumbnails: true
@@ -112,7 +112,7 @@ styles:
112### Bootstrap scripts 112### Bootstrap scripts
113scripts: 113scripts:
114 transition: false 114 transition: false
115 alert: true 115 alert: false
116 button: false 116 button: false
117 carousel: false 117 carousel: false
118 collapse: false 118 collapse: false
diff --git a/client/config/webpack.common.js b/client/config/webpack.common.js
index 7f1da74b9..9d05668e2 100644
--- a/client/config/webpack.common.js
+++ b/client/config/webpack.common.js
@@ -68,7 +68,7 @@ module.exports = {
68 root: helpers.root('src'), 68 root: helpers.root('src'),
69 69
70 // remove other default values 70 // remove other default values
71 modulesDirectories: [ 'node_modules', 'node_modules/blueimp-file-upload/js/vendor' ], 71 modulesDirectories: [ 'node_modules' ],
72 72
73 packageAlias: 'browser' 73 packageAlias: 'browser'
74 74
@@ -246,12 +246,6 @@ module.exports = {
246 chunksSortMode: 'dependency' 246 chunksSortMode: 'dependency'
247 }), 247 }),
248 248
249 new webpack.ProvidePlugin({
250 jQuery: 'jquery',
251 $: 'jquery',
252 jquery: 'jquery'
253 })
254
255 ], 249 ],
256 250
257 /* 251 /*
diff --git a/client/package.json b/client/package.json
index d2d039437..cd8afcc98 100644
--- a/client/package.json
+++ b/client/package.json
@@ -28,7 +28,6 @@
28 "@angular/router-deprecated": "2.0.0-rc.1", 28 "@angular/router-deprecated": "2.0.0-rc.1",
29 "angular-pipes": "^2.0.0", 29 "angular-pipes": "^2.0.0",
30 "awesome-typescript-loader": "^0.17.0", 30 "awesome-typescript-loader": "^0.17.0",
31 "blueimp-file-upload": "^9.12.1",
32 "bootstrap-loader": "^1.0.8", 31 "bootstrap-loader": "^1.0.8",
33 "bootstrap-sass": "^3.3.6", 32 "bootstrap-sass": "^3.3.6",
34 "compression-webpack-plugin": "^0.3.1", 33 "compression-webpack-plugin": "^0.3.1",
@@ -40,10 +39,9 @@
40 "es6-shim": "^0.35.0", 39 "es6-shim": "^0.35.0",
41 "file-loader": "^0.8.5", 40 "file-loader": "^0.8.5",
42 "html-webpack-plugin": "^2.19.0", 41 "html-webpack-plugin": "^2.19.0",
43 "jquery": "^2.2.3",
44 "jquery.ui.widget": "^1.10.3",
45 "json-loader": "^0.5.4", 42 "json-loader": "^0.5.4",
46 "ng2-bootstrap": "^1.0.16", 43 "ng2-bootstrap": "^1.0.16",
44 "ng2-file-upload": "^1.0.3",
47 "node-sass": "^3.7.0", 45 "node-sass": "^3.7.0",
48 "normalize.css": "^4.1.1", 46 "normalize.css": "^4.1.1",
49 "raw-loader": "^0.5.1", 47 "raw-loader": "^0.5.1",
@@ -75,4 +73,4 @@
75 "bundles/" 73 "bundles/"
76 ] 74 ]
77 } 75 }
78} \ No newline at end of file 76}
diff --git a/client/src/app/shared/search/search-field.type.ts b/client/src/app/shared/search/search-field.type.ts
index 846236290..5228ee68a 100644
--- a/client/src/app/shared/search/search-field.type.ts
+++ b/client/src/app/shared/search/search-field.type.ts
@@ -1 +1 @@
export type SearchField = "name" | "author" | "podUrl" | "magnetUri"; export type SearchField = "name" | "author" | "podUrl" | "magnetUri" | "tags";
diff --git a/client/src/app/shared/search/search.component.ts b/client/src/app/shared/search/search.component.ts
index 31f8b1535..c14c2d99c 100644
--- a/client/src/app/shared/search/search.component.ts
+++ b/client/src/app/shared/search/search.component.ts
@@ -18,7 +18,8 @@ export class SearchComponent {
18 name: 'Name', 18 name: 'Name',
19 author: 'Author', 19 author: 'Author',
20 podUrl: 'Pod Url', 20 podUrl: 'Pod Url',
21 magnetUri: 'Magnet Uri' 21 magnetUri: 'Magnet Uri',
22 tags: 'Tags'
22 }; 23 };
23 searchCriterias: Search = { 24 searchCriterias: Search = {
24 field: 'name', 25 field: 'name',
diff --git a/client/src/app/shared/users/auth.service.ts b/client/src/app/shared/users/auth.service.ts
index 720037563..1c822c1e1 100644
--- a/client/src/app/shared/users/auth.service.ts
+++ b/client/src/app/shared/users/auth.service.ts
@@ -43,7 +43,11 @@ export class AuthService {
43 } 43 }
44 44
45 getRequestHeader() { 45 getRequestHeader() {
46 return new Headers({ 'Authorization': `${this.getTokenType()} ${this.getToken()}` }); 46 return new Headers({ 'Authorization': this.getRequestHeaderValue() });
47 }
48
49 getRequestHeaderValue() {
50 return `${this.getTokenType()} ${this.getToken()}`;
47 } 51 }
48 52
49 getToken() { 53 getToken() {
diff --git a/client/src/app/videos/video-add/video-add.component.html b/client/src/app/videos/video-add/video-add.component.html
index cbe274e8a..6b2eb9377 100644
--- a/client/src/app/videos/video-add/video-add.component.html
+++ b/client/src/app/videos/video-add/video-add.component.html
@@ -2,42 +2,74 @@
2 2
3<div *ngIf="error" class="alert alert-danger">{{ error }}</div> 3<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
4 4
5<form (ngSubmit)="uploadFile()" #videoForm="ngForm"> 5<form novalidate (ngSubmit)="upload()" [ngFormModel]="videoForm">
6 <div class="form-group"> 6 <div class="form-group">
7 <label for="name">Video name</label> 7 <label for="name">Name</label>
8 <input 8 <input
9 type="text" class="form-control" name="name" id="name" required 9 type="text" class="form-control" name="name" id="name"
10 ngControl="name" #name="ngForm" 10 ngControl="name" #name="ngForm" [(ngModel)]="video.name"
11 > 11 >
12 <div [hidden]="name.valid || name.pristine" class="alert alert-danger"> 12 <div [hidden]="name.valid || name.pristine" class="alert alert-warning">
13 Name is required 13 A name is required and should be between 3 and 50 characters long
14 </div> 14 </div>
15 </div> 15 </div>
16 16
17 <div class="form-group"> 17 <div class="form-group">
18 <div class="btn btn-default btn-file"> 18 <label for="tags">Tags</label>
19 <input
20 type="text" class="form-control" name="tags" id="tags"
21 ngControl="tags" #tags="ngForm" [disabled]="isTagsInputDisabled" (keyup)="onTagKeyPress($event)" [(ngModel)]="currentTag"
22 >
23 <div [hidden]="tags.valid || tags.pristine" class="alert alert-warning">
24 A tag should be between 2 and 10 characters long
25 </div>
26 </div>
27
28 <div class="tags">
29 <div class="label label-info tag" *ngFor="let tag of video.tags">
30 {{ tag }}
31 <span class="remove" (click)="removeTag(tag)">x</span>
32 </div>
33 </div>
34
35 <div class="form-group">
36 <label for="videofile">File</label>
37 <div class="btn btn-default btn-file" [ngClass]="{ 'disabled': filename !== null }" >
19 <span>Select the video...</span> 38 <span>Select the video...</span>
20 <input type="file" name="videofile" id="videofile"> 39 <input
40 type="file" name="videofile" id="videofile"
41 ng2FileSelect [uploader]="uploader" [disabled]="filename !== null"
42 >
21 </div> 43 </div>
44 </div>
22 45
23 <span *ngIf="fileToUpload">{{ fileToUpload.name }}</span> 46 <div class="file-to-upload">
47 <div class="file" *ngIf="uploader.queue.length > 0">
48 <span class="filename">{{ filename }}</span>
49 <span class="glyphicon glyphicon-remove" (click)="removeFile()"></span>
50 </div>
24 </div> 51 </div>
25 52
26 <div class="form-group"> 53 <div class="form-group">
27 <label for="description">Description</label> 54 <label for="description">Description</label>
28 <textarea 55 <textarea
29 name="description" id="description" class="form-control" placeholder="Description..." required 56 name="description" id="description" class="form-control" placeholder="Description..."
30 ngControl="description" #description="ngForm" 57 ngControl="description" #description="ngForm" [(ngModel)]="video.description"
31 > 58 >
32 </textarea> 59 </textarea>
33 <div [hidden]="description.valid || description.pristine" class="alert alert-danger"> 60 <div [hidden]="description.valid || description.pristine" class="alert alert-warning">
34 A description is required 61 A description is required and should be between 3 and 250 characters long
35 </div> 62 </div>
36 </div> 63 </div>
37 64
38 <div id="progress" *ngIf="progressBar.max !== 0"> 65 <div class="progress">
39 <progressbar [value]="progressBar.value" [max]="progressBar.max">{{ progressBar.value | bytes }} / {{ progressBar.max | bytes }}</progressbar> 66 <progressbar [value]="uploader.progress" max="100"></progressbar>
40 </div> 67 </div>
41 68
42 <input type="submit" value="Upload" class="btn btn-default" [disabled]="!videoForm.form.valid || !fileToUpload"> 69 <div class="form-group">
70 <input
71 type="submit" value="Upload" class="btn btn-default form-control" [title]="getInvalidFieldsTitle()"
72 [disabled]="!videoForm.valid || video.tags.length === 0 || filename === null"
73 >
74 </div>
43</form> 75</form>
diff --git a/client/src/app/videos/video-add/video-add.component.scss b/client/src/app/videos/video-add/video-add.component.scss
index 01195f017..d66df2fd4 100644
--- a/client/src/app/videos/video-add/video-add.component.scss
+++ b/client/src/app/videos/video-add/video-add.component.scss
@@ -1,6 +1,7 @@
1.btn-file { 1.btn-file {
2 position: relative; 2 position: relative;
3 overflow: hidden; 3 overflow: hidden;
4 display: block;
4} 5}
5 6
6.btn-file input[type=file] { 7.btn-file input[type=file] {
@@ -28,6 +29,28 @@
28 margin-bottom: 10px; 29 margin-bottom: 10px;
29} 30}
30 31
31#progress { 32div.tags {
32 margin-bottom: 10px; 33 height: 40px;
34 font-size: 20px;
35 margin-top: 20px;
36
37 .tag {
38 margin-right: 10px;
39
40 .remove {
41 cursor: pointer;
42 }
43 }
44}
45
46div.file-to-upload {
47 height: 40px;
48
49 .glyphicon-remove {
50 cursor: pointer;
51 }
52}
53
54div.progress {
55 // height: 40px;
33} 56}
diff --git a/client/src/app/videos/video-add/video-add.component.ts b/client/src/app/videos/video-add/video-add.component.ts
index 144879a54..2b45ea125 100644
--- a/client/src/app/videos/video-add/video-add.component.ts
+++ b/client/src/app/videos/video-add/video-add.component.ts
@@ -1,29 +1,31 @@
1/// <reference path="../../../../typings/globals/jquery/index.d.ts" /> 1import { Control, ControlGroup, Validators } from '@angular/common';
2/// <reference path="../../../../typings/globals/jquery.fileupload/index.d.ts" />
3
4import { Component, ElementRef, OnInit } from '@angular/core'; 2import { Component, ElementRef, OnInit } from '@angular/core';
5import { Router } from '@angular/router-deprecated'; 3import { Router } from '@angular/router-deprecated';
6 4
7import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'; 5import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe';
8import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar'; 6import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar';
7import { FileSelectDirective, FileUploader } from 'ng2-file-upload/ng2-file-upload';
9 8
10import { AuthService, User } from '../../shared'; 9import { AuthService } from '../../shared';
11 10
12@Component({ 11@Component({
13 selector: 'my-videos-add', 12 selector: 'my-videos-add',
14 styles: [ require('./video-add.component.scss') ], 13 styles: [ require('./video-add.component.scss') ],
15 template: require('./video-add.component.html'), 14 template: require('./video-add.component.html'),
16 directives: [ PROGRESSBAR_DIRECTIVES ], 15 directives: [ FileSelectDirective, PROGRESSBAR_DIRECTIVES ],
17 pipes: [ BytesPipe ] 16 pipes: [ BytesPipe ]
18}) 17})
19 18
20export class VideoAddComponent implements OnInit { 19export class VideoAddComponent implements OnInit {
20 currentTag: string; // Tag the user is writing in the input
21 error: string = null; 21 error: string = null;
22 fileToUpload: any; 22 videoForm: ControlGroup;
23 progressBar: { value: number; max: number; } = { value: 0, max: 0 }; 23 uploader: FileUploader;
24 user: User; 24 video = {
25 25 name: '',
26 private form: any; 26 tags: [],
27 description: ''
28 };
27 29
28 constructor( 30 constructor(
29 private authService: AuthService, 31 private authService: AuthService,
@@ -31,52 +33,108 @@ export class VideoAddComponent implements OnInit {
31 private router: Router 33 private router: Router
32 ) {} 34 ) {}
33 35
36 get filename() {
37 if (this.uploader.queue.length === 0) {
38 return null;
39 }
40
41 return this.uploader.queue[0].file.name;
42 }
43
44 get isTagsInputDisabled () {
45 return this.video.tags.length >= 3;
46 }
47
48 getInvalidFieldsTitle() {
49 let title = '';
50 const nameControl = this.videoForm.controls['name'];
51 const descriptionControl = this.videoForm.controls['description'];
52
53 if (!nameControl.valid) {
54 title += 'A name is required\n';
55 }
56
57 if (this.video.tags.length === 0) {
58 title += 'At least one tag is required\n';
59 }
60
61 if (this.filename === null) {
62 title += 'A file is required\n';
63 }
64
65 if (!descriptionControl.valid) {
66 title += 'A description is required\n';
67 }
68
69 return title;
70 }
71
34 ngOnInit() { 72 ngOnInit() {
35 this.user = User.load(); 73 this.videoForm = new ControlGroup({
36 jQuery(this.elementRef.nativeElement).find('#videofile').fileupload({ 74 name: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(50) ])),
75 description: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(250) ])),
76 tags: new Control('', Validators.pattern('^[a-zA-Z0-9]{2,10}$'))
77 });
78
79
80 this.uploader = new FileUploader({
81 authToken: this.authService.getRequestHeaderValue(),
82 queueLimit: 1,
37 url: '/api/v1/videos', 83 url: '/api/v1/videos',
38 dataType: 'json', 84 removeAfterUpload: true
39 singleFileUploads: true,
40 multipart: true,
41 autoUpload: false,
42
43 add: (e, data) => {
44 this.form = data;
45 this.fileToUpload = data['files'][0];
46 },
47
48 progressall: (e, data) => {
49 this.progressBar.value = data.loaded;
50 // The server is a little bit slow to answer (has to seed the video)
51 // So we add more time to the progress bar (+10%)
52 this.progressBar.max = data.total + (0.1 * data.total);
53 },
54
55 done: (e, data) => {
56 this.progressBar.value = this.progressBar.max;
57 console.log('Video uploaded.');
58
59 // Print all the videos once it's finished
60 this.router.navigate(['VideosList']);
61 },
62
63 fail: (e, data) => {
64 const xhr = data.jqXHR;
65 if (xhr.status === 400) {
66 this.error = xhr.responseText;
67 } else {
68 this.error = 'Unknow error';
69 }
70
71 console.error(data);
72 }
73 }); 85 });
86
87 this.uploader.onBuildItemForm = (item, form) => {
88 form.append('name', this.video.name);
89 form.append('description', this.video.description);
90
91 for (let i = 0; i < this.video.tags.length; i++) {
92 form.append(`tags[${i}]`, this.video.tags[i]);
93 }
94 };
95 }
96
97 onTagKeyPress(event: KeyboardEvent) {
98 // Enter press
99 if (event.keyCode === 13) {
100 // Check if the tag is valid and does not already exist
101 if (
102 this.currentTag !== '' &&
103 this.videoForm.controls['tags'].valid &&
104 this.video.tags.indexOf(this.currentTag) === -1
105 ) {
106 this.video.tags.push(this.currentTag);
107 this.currentTag = '';
108 }
109 }
110 }
111
112 removeFile() {
113 this.uploader.clearQueue();
114 }
115
116 removeTag(tag: string) {
117 this.video.tags.splice(this.video.tags.indexOf(tag), 1);
74 } 118 }
75 119
76 uploadFile() { 120 upload() {
77 this.error = null; 121 const item = this.uploader.queue[0];
78 this.form.formData = jQuery(this.elementRef.nativeElement).find('form').serializeArray(); 122 // TODO: wait for https://github.com/valor-software/ng2-file-upload/pull/242
79 this.form.headers = this.authService.getRequestHeader().toJSON(); 123 item.alias = 'videofile';
80 this.form.submit(); 124
125 item.onSuccess = () => {
126 console.log('Video uploaded.');
127
128 // Print all the videos once it's finished
129 this.router.navigate(['VideosList']);
130 };
131
132 item.onError = (response: string, status: number) => {
133 this.error = (status === 400) ? response : 'Unknow error';
134 console.error(this.error);
135 };
136
137
138 this.uploader.uploadAll();
81 } 139 }
82} 140}
diff --git a/client/src/vendor.ts b/client/src/vendor.ts
index 496f44cf6..437d05822 100644
--- a/client/src/vendor.ts
+++ b/client/src/vendor.ts
@@ -18,7 +18,5 @@ import 'rxjs/add/operator/catch';
18import 'rxjs/add/operator/map'; 18import 'rxjs/add/operator/map';
19import 'rxjs/add/operator/mergeMap'; 19import 'rxjs/add/operator/mergeMap';
20 20
21import 'jquery';
22import 'bootstrap-loader'; 21import 'bootstrap-loader';
23import 'jquery.ui.widget/jquery.ui.widget'; 22import 'ng2-file-upload';
24import 'blueimp-file-upload';
diff --git a/client/tsconfig.json b/client/tsconfig.json
index 3b903f8c8..fdcf742ea 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -65,8 +65,6 @@
65 "src/vendor.ts", 65 "src/vendor.ts",
66 "typings/globals/es6-shim/index.d.ts", 66 "typings/globals/es6-shim/index.d.ts",
67 "typings/globals/jasmine/index.d.ts", 67 "typings/globals/jasmine/index.d.ts",
68 "typings/globals/jquery.fileupload/index.d.ts",
69 "typings/globals/jquery/index.d.ts",
70 "typings/globals/node/index.d.ts", 68 "typings/globals/node/index.d.ts",
71 "typings/index.d.ts" 69 "typings/index.d.ts"
72 ] 70 ]
diff --git a/client/typings.json b/client/typings.json
index ff8b56a48..9a8891f25 100644
--- a/client/typings.json
+++ b/client/typings.json
@@ -2,8 +2,6 @@
2 "globalDependencies": { 2 "globalDependencies": {
3 "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654", 3 "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654",
4 "jasmine": "registry:dt/jasmine#2.2.0+20160412134438", 4 "jasmine": "registry:dt/jasmine#2.2.0+20160412134438",
5 "jquery": "registry:dt/jquery#1.10.0+20160417213236",
6 "jquery.fileupload": "registry:dt/jquery.fileupload#5.40.1+20160316155526",
7 "node": "registry:dt/node#4.0.0+20160509154515" 5 "node": "registry:dt/node#4.0.0+20160509154515"
8 } 6 }
9} 7}
diff --git a/server/initializers/constants.js b/server/initializers/constants.js
index 6fa322010..22cbb1361 100644
--- a/server/initializers/constants.js
+++ b/server/initializers/constants.js
@@ -41,8 +41,8 @@ const THUMBNAILS_SIZE = '200x110'
41const THUMBNAILS_STATIC_PATH = '/static/thumbnails' 41const THUMBNAILS_STATIC_PATH = '/static/thumbnails'
42 42
43const VIDEOS_CONSTRAINTS_FIELDS = { 43const VIDEOS_CONSTRAINTS_FIELDS = {
44 NAME: { min: 1, max: 50 }, // Length 44 NAME: { min: 3, max: 50 }, // Length
45 DESCRIPTION: { min: 1, max: 250 }, // Length 45 DESCRIPTION: { min: 3, max: 250 }, // Length
46 MAGNET_URI: { min: 10 }, // Length 46 MAGNET_URI: { min: 10 }, // Length
47 DURATION: { min: 1, max: 7200 }, // Number 47 DURATION: { min: 1, max: 7200 }, // Number
48 AUTHOR: { min: 3, max: 20 }, // Length 48 AUTHOR: { min: 3, max: 20 }, // Length
diff --git a/server/middlewares/reqValidators/videos.js b/server/middlewares/reqValidators/videos.js
index 3618e4716..f31fd93a2 100644
--- a/server/middlewares/reqValidators/videos.js
+++ b/server/middlewares/reqValidators/videos.js
@@ -32,7 +32,7 @@ function videosAdd (req, res, next) {
32 } 32 }
33 33
34 if (!customValidators.isVideoDurationValid(duration)) { 34 if (!customValidators.isVideoDurationValid(duration)) {
35 return res.status(400).send('Duration of the video file is too big (max: ' + constants.MAXIMUM_VIDEO_DURATION + 's).') 35 return res.status(400).send('Duration of the video file is too big (max: ' + constants.VIDEOS_CONSTRAINTS_FIELDS.DURATION.max + 's).')
36 } 36 }
37 37
38 videoFile.duration = duration 38 videoFile.duration = duration
diff --git a/server/models/videos.js b/server/models/videos.js
index d6b743c7c..c177b414c 100644
--- a/server/models/videos.js
+++ b/server/models/videos.js
@@ -12,6 +12,7 @@ const port = config.get('webserver.port')
12 12
13// --------------------------------------------------------------------------- 13// ---------------------------------------------------------------------------
14 14
15// TODO: add indexes on searchable columns
15const videosSchema = mongoose.Schema({ 16const videosSchema = mongoose.Schema({
16 name: String, 17 name: String,
17 namePath: String, 18 namePath: String,