Client: add basic support for updating a video
authorChocobozzz <florian.bigard@gmail.com>
Mon, 10 Apr 2017 19:15:28 +0000 (21:15 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Mon, 10 Apr 2017 19:16:36 +0000 (21:16 +0200)
17 files changed:
client/src/app/videos/index.ts
client/src/app/videos/shared/video.model.ts
client/src/app/videos/shared/video.service.ts
client/src/app/videos/video-add/index.ts [deleted file]
client/src/app/videos/video-add/video-add.component.html [deleted file]
client/src/app/videos/video-add/video-add.component.scss [deleted file]
client/src/app/videos/video-add/video-add.component.ts [deleted file]
client/src/app/videos/video-edit/index.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-edit.component.scss [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-watch/video-watch.component.html
client/src/app/videos/video-watch/video-watch.component.ts
client/src/app/videos/videos-routing.module.ts
client/src/app/videos/videos.module.ts

index ca386a51df51e63f80adbcd2ee233f1859466c57..5158a23f835032de6c81d8aa6fc542ea2479bfd6 100644 (file)
@@ -1,5 +1,5 @@
 export * from './shared';
-export * from './video-add';
+export * from './video-edit';
 export * from './video-list';
 export * from './video-watch';
 export * from './videos-routing.module';
index f135ca707e77f795214bb22f3ee9dd1a3275f882..404e3bf45fea496ac0ca3e29dac42190085a3e9a 100644 (file)
@@ -5,8 +5,11 @@ export class Video {
   by: string;
   createdAt: Date;
   categoryLabel: string;
+  category: string;
   licenceLabel: string;
+  licence: string;
   languageLabel: string;
+  language: string;
   description: string;
   duration: string;
   id: string;
@@ -38,8 +41,11 @@ export class Video {
     author: string,
     createdAt: string,
     categoryLabel: string,
+    category: string,
     licenceLabel: string,
+    licence: string,
     languageLabel: string;
+    language: string;
     description: string,
     duration: number;
     id: string,
@@ -57,8 +63,11 @@ export class Video {
     this.author  = hash.author;
     this.createdAt = new Date(hash.createdAt);
     this.categoryLabel = hash.categoryLabel;
+    this.category = hash.category;
     this.licenceLabel = hash.licenceLabel;
+    this.licence = hash.licence;
     this.languageLabel = hash.languageLabel;
+    this.language = hash.language;
     this.description = hash.description;
     this.duration = Video.createDurationString(hash.duration);
     this.id = hash.id;
@@ -84,4 +93,33 @@ export class Video {
     // If the video is NSFW and the user is not logged in, or the user does not want to display NSFW videos...
     return (this.nsfw && (!user || user.displayNSFW === false));
   }
+
+  patch(values: Object) {
+    Object.keys(values).forEach((key) => {
+      this[key] = values[key];
+    });
+  }
+
+  toJSON() {
+    return {
+      author: this.author,
+      createdAt: this.createdAt,
+      category: this.category,
+      licence: this.licence,
+      language: this.language,
+      description: this.description,
+      duration: this.duration,
+      id: this.id,
+      isLocal: this.isLocal,
+      magnetUri: this.magnetUri,
+      name: this.name,
+      podHost: this.podHost,
+      tags: this.tags,
+      thumbnailPath: this.thumbnailPath,
+      views: this.views,
+      likes: this.likes,
+      dislikes: this.dislikes,
+      nsfw: this.nsfw
+    };
+  }
 }
index 13d4ca246a11db03c8c2108fbb6308bcb1c7bbfc..ee67bc1ae7f3935b90c71f519f523e3d82edbef8 100644 (file)
@@ -1,5 +1,5 @@
 import { Injectable } from '@angular/core';
-import { Http } from '@angular/http';
+import { Http, Headers, RequestOptions } from '@angular/http';
 import { Observable } from 'rxjs/Observable';
 import 'rxjs/add/operator/catch';
 import 'rxjs/add/operator/map';
@@ -80,6 +80,23 @@ export class VideoService {
                     .catch((res) => this.restExtractor.handleError(res));
   }
 
+  updateVideo(video: Video) {
+    const body = {
+      name: video.name,
+      category: video.category,
+      licence: video.licence,
+      language: video.language,
+      description: video.description,
+      tags: video.tags
+    };
+    const headers = new Headers({ 'Content-Type': 'application/json' });
+    const options = new RequestOptions({ headers: headers });
+
+    return this.authHttp.put(`${VideoService.BASE_VIDEO_URL}/${video.id}`, body, options)
+                        .map(this.restExtractor.extractDataBool)
+                        .catch(this.restExtractor.handleError);
+  }
+
   getVideos(pagination: RestPagination, sort: SortField) {
     const params = this.restService.buildRestGetParams(pagination, sort);
 
diff --git a/client/src/app/videos/video-add/index.ts b/client/src/app/videos/video-add/index.ts
deleted file mode 100644 (file)
index 79488e8..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export * from './video-add.component';
diff --git a/client/src/app/videos/video-add/video-add.component.html b/client/src/app/videos/video-add/video-add.component.html
deleted file mode 100644 (file)
index 104747a..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-<h3>Upload a video</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">Tags</label> <span class="little-information">(press enter to add the tag)</span>
-    <input
-      type="text" class="form-control" id="currentTag"
-      formControlName="currentTag" (keyup)="onTagKeyPress($event)"
-    >
-    <div *ngIf="formErrors.currentTag" class="alert alert-danger">
-      {{ formErrors.currentTag }}
-    </div>
-  </div>
-
-  <div class="tags">
-    <div class="label label-primary tag" *ngFor="let tag of tags">
-      {{ tag }}
-      <span class="remove" (click)="removeTag(tag)">x</span>
-    </div>
-  </div>
-
-  <div *ngIf="tagsError" class="alert alert-danger">
-    {{ tagsError }}
-  </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"
-        (change)="fileChanged()"
-      >
-    </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 *ngIf="fileError" class="alert alert-danger">
-    {{ fileError }}
-  </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]="uploader.progress" max="100"></progressbar>
-  </div>
-
-  <div class="form-group">
-    <input
-      type="button" value="Upload" class="btn btn-default form-control"
-      (click)="upload()"
-    >
-  </div>
-</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
deleted file mode 100644 (file)
index 11ee329..0000000
+++ /dev/null
@@ -1,57 +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;
-}
-
-.name_file {
-  display: inline-block;
-  margin-left: 10px;
-}
-
-.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;
-}
diff --git a/client/src/app/videos/video-add/video-add.component.ts b/client/src/app/videos/video-add/video-add.component.ts
deleted file mode 100644 (file)
index da556f4..0000000
+++ /dev/null
@@ -1,230 +0,0 @@
-import { Component, ElementRef, OnInit } from '@angular/core';
-import { FormBuilder, FormGroup } from '@angular/forms';
-import { Router } from '@angular/router';
-
-import { FileUploader } from 'ng2-file-upload/ng2-file-upload';
-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 { VideoService } from '../shared';
-
-@Component({
-  selector: 'my-videos-add',
-  styleUrls: [ './video-add.component.scss' ],
-  templateUrl: './video-add.component.html'
-})
-
-export class VideoAddComponent extends FormReactive implements OnInit {
-  tags: string[] = [];
-  uploader: FileUploader;
-  videoCategories = [];
-  videoLicences = [];
-  videoLanguages = [];
-
-  error: string = null;
-  form: FormGroup;
-  formErrors = {
-    name: '',
-    category: '',
-    licence: '',
-    language: '',
-    description: '',
-    currentTag: ''
-  };
-  validationMessages = {
-    name: VIDEO_NAME.MESSAGES,
-    category: VIDEO_CATEGORY.MESSAGES,
-    licence: VIDEO_LICENCE.MESSAGES,
-    language: VIDEO_LANGUAGE.MESSAGES,
-    description: VIDEO_DESCRIPTION.MESSAGES,
-    currentTag: VIDEO_TAGS.MESSAGES
-  };
-
-  // Special error messages
-  tagsError = '';
-  fileError = '';
-
-  constructor(
-    private authService: AuthService,
-    private elementRef: ElementRef,
-    private formBuilder: FormBuilder,
-    private router: Router,
-    private notificationsService: NotificationsService,
-    private videoService: VideoService
-  ) {
-    super();
-  }
-
-  get filename() {
-    if (this.uploader.queue.length === 0) {
-      return null;
-    }
-
-    return this.uploader.queue[0].file.name;
-  }
-
-  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 ],
-      currentTag: [ '', VIDEO_TAGS.VALIDATORS ]
-    });
-
-    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.uploader = new FileUploader({
-      authToken: this.authService.getRequestHeaderValue(),
-      queueLimit: 1,
-      url: '/api/v1/videos',
-      removeAfterUpload: true
-    });
-
-    this.uploader.onBuildItemForm = (item, form) => {
-      const name = this.form.value['name'];
-      const nsfw = this.form.value['nsfw'];
-      const category = this.form.value['category'];
-      const licence = this.form.value['licence'];
-      const language = this.form.value['language'];
-      const description = this.form.value['description'];
-
-      form.append('name', name);
-      form.append('category', category);
-      form.append('nsfw', nsfw);
-      form.append('licence', licence);
-
-      // Language is optional
-      if (language) {
-        form.append('language', language);
-      }
-
-      form.append('description', description);
-
-      for (let i = 0; i < this.tags.length; i++) {
-        form.append(`tags[${i}]`, this.tags[i]);
-      }
-    };
-
-    this.buildForm();
-  }
-
-  checkForm() {
-    this.forceCheck();
-
-    if (this.filename === null) {
-      this.fileError = 'You did not add a file.';
-    }
-
-    return this.form.valid === true && this.tagsError === '' && this.fileError === '';
-  }
-
-  fileChanged() {
-    this.fileError = '';
-  }
-
-  onTagKeyPress(event: KeyboardEvent) {
-    // Enter press
-    if (event.keyCode === 13) {
-      this.addTagIfPossible();
-    }
-  }
-
-  removeFile() {
-    this.uploader.clearQueue();
-  }
-
-  removeTag(tag: string) {
-    this.tags.splice(this.tags.indexOf(tag), 1);
-    this.form.get('currentTag').enable();
-  }
-
-  upload() {
-    // Maybe the user forgot to press "enter" when he filled the field
-    this.addTagIfPossible();
-
-    if (this.checkForm() === false) {
-      return;
-    }
-
-    const item = this.uploader.queue[0];
-    // TODO: wait for https://github.com/valor-software/ng2-file-upload/pull/242
-    item.alias = 'videofile';
-
-    // FIXME: remove
-    // Run detection change for progress bar
-    const interval = setInterval(() => { ; }, 250);
-
-    item.onSuccess = () => {
-      clearInterval(interval);
-
-      console.log('Video uploaded.');
-      this.notificationsService.success('Success', 'Video uploaded.');
-
-
-      // Print all the videos once it's finished
-      this.router.navigate(['/videos/list']);
-    };
-
-    item.onError = (response: string, status: number) => {
-      clearInterval(interval);
-
-      // We need to handle manually these cases beceause we use the FileUpload component
-      if (status === 400) {
-        this.error = response;
-      } else if (status === 401) {
-        this.error = 'Access token was expired, refreshing token...';
-        this.authService.refreshAccessToken().subscribe(
-          () => {
-            // Update the uploader request header
-            this.uploader.authToken = this.authService.getRequestHeaderValue();
-            this.error += ' access token refreshed. Please retry your request.';
-          }
-        );
-      } else {
-        this.error = 'Unknow error';
-        console.error(this.error);
-      }
-    };
-
-    this.uploader.uploadAll();
-  }
-
-  private addTagIfPossible() {
-    const currentTag = this.form.value['currentTag'];
-    if (currentTag === undefined) return;
-
-    // Check if the tag is valid and does not already exist
-    if (
-      currentTag.length >= 2 &&
-      this.form.controls['currentTag'].valid &&
-      this.tags.indexOf(currentTag) === -1
-    ) {
-      this.tags.push(currentTag);
-      this.form.patchValue({ currentTag: '' });
-
-      if (this.tags.length >= 3) {
-        this.form.get('currentTag').disable();
-      }
-
-      this.tagsError = '';
-    }
-  }
-}
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..5ce4fb9
--- /dev/null
@@ -0,0 +1,2 @@
+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
new file mode 100644 (file)
index 0000000..104747a
--- /dev/null
@@ -0,0 +1,128 @@
+<h3>Upload a video</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">Tags</label> <span class="little-information">(press enter to add the tag)</span>
+    <input
+      type="text" class="form-control" id="currentTag"
+      formControlName="currentTag" (keyup)="onTagKeyPress($event)"
+    >
+    <div *ngIf="formErrors.currentTag" class="alert alert-danger">
+      {{ formErrors.currentTag }}
+    </div>
+  </div>
+
+  <div class="tags">
+    <div class="label label-primary tag" *ngFor="let tag of tags">
+      {{ tag }}
+      <span class="remove" (click)="removeTag(tag)">x</span>
+    </div>
+  </div>
+
+  <div *ngIf="tagsError" class="alert alert-danger">
+    {{ tagsError }}
+  </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"
+        (change)="fileChanged()"
+      >
+    </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 *ngIf="fileError" class="alert alert-danger">
+    {{ fileError }}
+  </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]="uploader.progress" max="100"></progressbar>
+  </div>
+
+  <div class="form-group">
+    <input
+      type="button" value="Upload" class="btn btn-default form-control"
+      (click)="upload()"
+    >
+  </div>
+</form>
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..e3cf0e9
--- /dev/null
@@ -0,0 +1,230 @@
+import { Component, ElementRef, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup } from '@angular/forms';
+import { Router } from '@angular/router';
+
+import { FileUploader } from 'ng2-file-upload/ng2-file-upload';
+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 { VideoService } from '../shared';
+
+@Component({
+  selector: 'my-videos-add',
+  styleUrls: [ './video-edit.component.scss' ],
+  templateUrl: './video-add.component.html'
+})
+
+export class VideoAddComponent extends FormReactive implements OnInit {
+  tags: string[] = [];
+  uploader: FileUploader;
+  videoCategories = [];
+  videoLicences = [];
+  videoLanguages = [];
+
+  error: string = null;
+  form: FormGroup;
+  formErrors = {
+    name: '',
+    category: '',
+    licence: '',
+    language: '',
+    description: '',
+    currentTag: ''
+  };
+  validationMessages = {
+    name: VIDEO_NAME.MESSAGES,
+    category: VIDEO_CATEGORY.MESSAGES,
+    licence: VIDEO_LICENCE.MESSAGES,
+    language: VIDEO_LANGUAGE.MESSAGES,
+    description: VIDEO_DESCRIPTION.MESSAGES,
+    currentTag: VIDEO_TAGS.MESSAGES
+  };
+
+  // Special error messages
+  tagsError = '';
+  fileError = '';
+
+  constructor(
+    private authService: AuthService,
+    private elementRef: ElementRef,
+    private formBuilder: FormBuilder,
+    private router: Router,
+    private notificationsService: NotificationsService,
+    private videoService: VideoService
+  ) {
+    super();
+  }
+
+  get filename() {
+    if (this.uploader.queue.length === 0) {
+      return null;
+    }
+
+    return this.uploader.queue[0].file.name;
+  }
+
+  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 ],
+      currentTag: [ '', VIDEO_TAGS.VALIDATORS ]
+    });
+
+    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.uploader = new FileUploader({
+      authToken: this.authService.getRequestHeaderValue(),
+      queueLimit: 1,
+      url: '/api/v1/videos',
+      removeAfterUpload: true
+    });
+
+    this.uploader.onBuildItemForm = (item, form) => {
+      const name = this.form.value['name'];
+      const nsfw = this.form.value['nsfw'];
+      const category = this.form.value['category'];
+      const licence = this.form.value['licence'];
+      const language = this.form.value['language'];
+      const description = this.form.value['description'];
+
+      form.append('name', name);
+      form.append('category', category);
+      form.append('nsfw', nsfw);
+      form.append('licence', licence);
+
+      // Language is optional
+      if (language) {
+        form.append('language', language);
+      }
+
+      form.append('description', description);
+
+      for (let i = 0; i < this.tags.length; i++) {
+        form.append(`tags[${i}]`, this.tags[i]);
+      }
+    };
+
+    this.buildForm();
+  }
+
+  checkForm() {
+    this.forceCheck();
+
+    if (this.filename === null) {
+      this.fileError = 'You did not add a file.';
+    }
+
+    return this.form.valid === true && this.tagsError === '' && this.fileError === '';
+  }
+
+  fileChanged() {
+    this.fileError = '';
+  }
+
+  onTagKeyPress(event: KeyboardEvent) {
+    // Enter press
+    if (event.keyCode === 13) {
+      this.addTagIfPossible();
+    }
+  }
+
+  removeFile() {
+    this.uploader.clearQueue();
+  }
+
+  removeTag(tag: string) {
+    this.tags.splice(this.tags.indexOf(tag), 1);
+    this.form.get('currentTag').enable();
+  }
+
+  upload() {
+    // Maybe the user forgot to press "enter" when he filled the field
+    this.addTagIfPossible();
+
+    if (this.checkForm() === false) {
+      return;
+    }
+
+    const item = this.uploader.queue[0];
+    // TODO: wait for https://github.com/valor-software/ng2-file-upload/pull/242
+    item.alias = 'videofile';
+
+    // FIXME: remove
+    // Run detection change for progress bar
+    const interval = setInterval(() => { ; }, 250);
+
+    item.onSuccess = () => {
+      clearInterval(interval);
+
+      console.log('Video uploaded.');
+      this.notificationsService.success('Success', 'Video uploaded.');
+
+
+      // Print all the videos once it's finished
+      this.router.navigate(['/videos/list']);
+    };
+
+    item.onError = (response: string, status: number) => {
+      clearInterval(interval);
+
+      // We need to handle manually these cases beceause we use the FileUpload component
+      if (status === 400) {
+        this.error = response;
+      } else if (status === 401) {
+        this.error = 'Access token was expired, refreshing token...';
+        this.authService.refreshAccessToken().subscribe(
+          () => {
+            // Update the uploader request header
+            this.uploader.authToken = this.authService.getRequestHeaderValue();
+            this.error += ' access token refreshed. Please retry your request.';
+          }
+        );
+      } else {
+        this.error = 'Unknow error';
+        console.error(this.error);
+      }
+    };
+
+    this.uploader.uploadAll();
+  }
+
+  private addTagIfPossible() {
+    const currentTag = this.form.value['currentTag'];
+    if (currentTag === undefined) return;
+
+    // Check if the tag is valid and does not already exist
+    if (
+      currentTag.length >= 2 &&
+      this.form.controls['currentTag'].valid &&
+      this.tags.indexOf(currentTag) === -1
+    ) {
+      this.tags.push(currentTag);
+      this.form.patchValue({ currentTag: '' });
+
+      if (this.tags.length >= 3) {
+        this.form.get('currentTag').disable();
+      }
+
+      this.tagsError = '';
+    }
+  }
+}
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..11ee329
--- /dev/null
@@ -0,0 +1,57 @@
+.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;
+}
+
+.name_file {
+  display: inline-block;
+  margin-left: 10px;
+}
+
+.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;
+}
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..665a952
--- /dev/null
@@ -0,0 +1,101 @@
+<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">Tags</label> <span class="little-information">(press enter to add the tag)</span>
+    <input
+      type="text" class="form-control" id="currentTag"
+      formControlName="currentTag" (keyup)="onTagKeyPress($event)"
+    >
+    <div *ngIf="formErrors.currentTag" class="alert alert-danger">
+      {{ formErrors.currentTag }}
+    </div>
+  </div>
+
+  <div class="tags">
+    <div class="label label-primary tag" *ngFor="let tag of tags">
+      {{ tag }}
+      <span class="remove" (click)="removeTag(tag)">x</span>
+    </div>
+  </div>
+
+  <div *ngIf="tagsError" class="alert alert-danger">
+    {{ tagsError }}
+  </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>
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..b45780a
--- /dev/null
@@ -0,0 +1,170 @@
+import { Component, ElementRef, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import { FileUploader } from 'ng2-file-upload/ng2-file-upload';
+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;
+
+  error: string = null;
+  form: FormGroup;
+  formErrors = {
+    name: '',
+    category: '',
+    licence: '',
+    language: '',
+    description: '',
+    currentTag: ''
+  };
+  validationMessages = {
+    name: VIDEO_NAME.MESSAGES,
+    category: VIDEO_CATEGORY.MESSAGES,
+    licence: VIDEO_LICENCE.MESSAGES,
+    language: VIDEO_LANGUAGE.MESSAGES,
+    description: VIDEO_DESCRIPTION.MESSAGES,
+    currentTag: VIDEO_TAGS.MESSAGES
+  };
+
+  // Special error messages
+  tagsError = '';
+  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 ],
+      currentTag: [ '', VIDEO_TAGS.VALIDATORS ]
+    });
+
+    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 id = this.route.snapshot.params['id'];
+    this.videoService.getVideo(id)
+                     .subscribe(
+                       video => {
+                         this.video = video;
+
+                         this.hydrateFormFromVideo();
+                       },
+
+                       err => this.error = 'Cannot fetch video.'
+                     );
+  }
+
+  checkForm() {
+    this.forceCheck();
+
+    return this.form.valid === true && this.tagsError === '' && this.fileError === '';
+  }
+
+
+  onTagKeyPress(event: KeyboardEvent) {
+    // Enter press
+    if (event.keyCode === 13) {
+      this.addTagIfPossible();
+    }
+  }
+
+  removeTag(tag: string) {
+    this.tags.splice(this.tags.indexOf(tag), 1);
+    this.form.get('currentTag').enable();
+  }
+
+  update() {
+    // Maybe the user forgot to press "enter" when he filled the field
+    this.addTagIfPossible();
+
+    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.id ]);
+                       },
+
+                       err => {
+                         this.error = 'Cannot update the video.';
+                         console.error(err);
+                       }
+                      );
+
+  }
+
+  private addTagIfPossible() {
+    const currentTag = this.form.value['currentTag'];
+    if (currentTag === undefined) return;
+
+    // Check if the tag is valid and does not already exist
+    if (
+      currentTag.length >= 2 &&
+      this.form.controls['currentTag'].valid &&
+      this.tags.indexOf(currentTag) === -1
+    ) {
+      this.tags.push(currentTag);
+      this.form.patchValue({ currentTag: '' });
+
+      if (this.tags.length >= 3) {
+        this.form.get('currentTag').disable();
+      }
+
+      this.tagsError = '';
+    }
+  }
+
+  private hydrateFormFromVideo() {
+    this.form.patchValue(this.video.toJSON());
+  }
+}
index a6ec7b20fda6158604a65d2039d8173931410f3d..2a6b15dc99f72c4a44826ff398759b8e948c116b 100644 (file)
         </button>
 
         <ul dropdownMenu 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.id ]">
+              <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
index 37ed70a99f3f62017cf0a9c2b75a9ec1522ec52d..e04626a677f9c70225c20bc6b898c5cd06f70f08 100644 (file)
@@ -187,6 +187,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     return this.authService.isLoggedIn();
   }
 
+  canUserUpdateVideo() {
+    return this.authService.getUser() !== null &&
+           this.authService.getUser().username === this.video.author;
+  }
+
   private checkUserRating() {
     // Unlogged users do not have ratings
     if (this.isUserLoggedIn() === false) return;
index 005e9def6548bf39e41c50935dc67f3c81e17d3c..70968b4d1d90681142ce34838c23ab40c8cfa91e 100644 (file)
@@ -1,7 +1,7 @@
 import { NgModule } from '@angular/core';
 import { RouterModule, Routes } from '@angular/router';
 
-import { VideoAddComponent } from './video-add';
+import { VideoAddComponent, VideoUpdateComponent } from './video-edit';
 import { VideoListComponent } from './video-list';
 import { VideosComponent } from './videos.component';
 import { VideoWatchComponent } from './video-watch';
@@ -29,6 +29,15 @@ const videosRoutes: Routes = [
           }
         }
       },
+      {
+        path: 'edit/:id',
+        component: VideoUpdateComponent,
+        data: {
+          meta: {
+            title: 'Edit a video'
+          }
+        }
+      },
       {
         path: ':id',
         redirectTo: 'watch/:id'
index 03dea17b5b433e27cda9d6362e6fc6e7fb03e58e..fa37ad966b318b643368c21dffc3b60eb2a4bde7 100644 (file)
@@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
 
 import { VideosRoutingModule } from './videos-routing.module';
 import { VideosComponent } from './videos.component';
-import { VideoAddComponent } from './video-add';
+import { VideoAddComponent, VideoUpdateComponent } from './video-edit';
 import { VideoListComponent, VideoMiniatureComponent, VideoSortComponent } from './video-list';
 import {
   VideoWatchComponent,
@@ -24,6 +24,7 @@ import { SharedModule } from '../shared';
     VideosComponent,
 
     VideoAddComponent,
+    VideoUpdateComponent,
 
     VideoListComponent,
     VideoMiniatureComponent,