diff options
Diffstat (limited to 'client/src')
-rw-r--r-- | client/src/app/shared/search/search-field.type.ts | 2 | ||||
-rw-r--r-- | client/src/app/shared/search/search.component.ts | 3 | ||||
-rw-r--r-- | client/src/app/shared/users/auth.service.ts | 6 | ||||
-rw-r--r-- | client/src/app/videos/video-add/video-add.component.html | 64 | ||||
-rw-r--r-- | client/src/app/videos/video-add/video-add.component.scss | 27 | ||||
-rw-r--r-- | client/src/app/videos/video-add/video-add.component.ts | 162 | ||||
-rw-r--r-- | client/src/vendor.ts | 4 |
7 files changed, 192 insertions, 76 deletions
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 { | 32 | div.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 | |||
46 | div.file-to-upload { | ||
47 | height: 40px; | ||
48 | |||
49 | .glyphicon-remove { | ||
50 | cursor: pointer; | ||
51 | } | ||
52 | } | ||
53 | |||
54 | div.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" /> | 1 | import { Control, ControlGroup, Validators } from '@angular/common'; |
2 | /// <reference path="../../../../typings/globals/jquery.fileupload/index.d.ts" /> | ||
3 | |||
4 | import { Component, ElementRef, OnInit } from '@angular/core'; | 2 | import { Component, ElementRef, OnInit } from '@angular/core'; |
5 | import { Router } from '@angular/router-deprecated'; | 3 | import { Router } from '@angular/router-deprecated'; |
6 | 4 | ||
7 | import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'; | 5 | import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'; |
8 | import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar'; | 6 | import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar'; |
7 | import { FileSelectDirective, FileUploader } from 'ng2-file-upload/ng2-file-upload'; | ||
9 | 8 | ||
10 | import { AuthService, User } from '../../shared'; | 9 | import { 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 | ||
20 | export class VideoAddComponent implements OnInit { | 19 | export 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'; | |||
18 | import 'rxjs/add/operator/map'; | 18 | import 'rxjs/add/operator/map'; |
19 | import 'rxjs/add/operator/mergeMap'; | 19 | import 'rxjs/add/operator/mergeMap'; |
20 | 20 | ||
21 | import 'jquery'; | ||
22 | import 'bootstrap-loader'; | 21 | import 'bootstrap-loader'; |
23 | import 'jquery.ui.widget/jquery.ui.widget'; | 22 | import 'ng2-file-upload'; |
24 | import 'blueimp-file-upload'; | ||