]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add ability to update thumbnail and preview on client
authorChocobozzz <me@florianbigard.com>
Fri, 16 Feb 2018 15:35:32 +0000 (16:35 +0100)
committerChocobozzz <me@florianbigard.com>
Fri, 16 Feb 2018 15:35:32 +0000 (16:35 +0100)
20 files changed:
client/src/app/+admin/follows/follows.component.html
client/src/app/core/server/server.service.ts
client/src/app/shared/forms/form-validators/video.ts
client/src/app/shared/forms/markdown-textarea.component.html
client/src/app/shared/misc/utils.ts
client/src/app/shared/shared.module.ts
client/src/app/shared/video/video-edit.model.ts
client/src/app/shared/video/video-thumbnail.component.html
client/src/app/shared/video/video.service.ts
client/src/app/videos/+video-edit/shared/video-edit.component.html
client/src/app/videos/+video-edit/shared/video-edit.component.scss
client/src/app/videos/+video-edit/shared/video-edit.component.ts
client/src/app/videos/+video-edit/shared/video-edit.module.ts
client/src/app/videos/+video-edit/video-update.component.ts
client/src/app/videos/+video-watch/video-watch.component.html
client/src/app/videos/+video-watch/video-watch.component.ts
client/src/sass/application.scss
server/controllers/api/config.ts
shared/models/config/server-config.model.ts
shared/models/videos/video-update.model.ts

index 1baba5a4d821b65fd963c14877a4200e96403172..d3d7486229f4eca5cd05d315e38aecabb474a938 100644 (file)
@@ -10,6 +10,4 @@
   </tabset>
 </div>
 
-
-
 <router-outlet></router-outlet>
index 65714fd053e542b4541851e83c5298c638de615b..553ad8af6f8550cdf7dff72f643ffaf161b614ad 100644 (file)
@@ -35,6 +35,10 @@ export class ServerService {
       }
     },
     video: {
+      image: {
+        size: { max: 0 },
+        extensions: []
+      },
       file: {
         extensions: []
       }
index 500b5bc5f1bcfccc4c6ae67df3fb10e63cc57e76..34a237a120116ff770f815fd1bf092884a3804d6 100644 (file)
@@ -31,6 +31,11 @@ export const VIDEO_LANGUAGE = {
   MESSAGES: {}
 }
 
+export const VIDEO_IMAGE = {
+  VALIDATORS: [ ],
+  MESSAGES: {}
+}
+
 export const VIDEO_CHANNEL = {
   VALIDATORS: [ Validators.required ],
   MESSAGES: {
index d2d4cf95cb3580f8b5522ffa9863a1560d261dae..e8c5ded5b0c64d87da97e389bad971d79f48d480 100644 (file)
@@ -5,7 +5,7 @@
     id="description" name="description">
   </textarea>
 
-  <tabset *ngIf="arePreviewsDisplayed()" #staticTabs class="previews">
+  <tabset *ngIf="arePreviewsDisplayed()" class="previews">
     <tab *ngIf="truncate !== undefined" heading="Truncated description preview" [innerHTML]="truncatedDescriptionHTML"></tab>
     <tab heading="Complete description preview" [innerHTML]="descriptionHTML"></tab>
   </tabset>
index e6a6970983a0a8a45f9603c49a21d9ef925bc8e2..e2e4c5b36bb3ea2ccb591c7c7fa8b5d036e6d9f1 100644 (file)
@@ -67,6 +67,27 @@ function isInMobileView () {
   return window.innerWidth < 500
 }
 
+// Thanks: https://gist.github.com/ghinda/8442a57f22099bdb2e34
+function objectToFormData (obj: any, form?: FormData, namespace?: string) {
+  let fd = form || new FormData()
+  let formKey
+
+  for (let key of Object.keys(obj)) {
+    if (namespace) formKey = `${namespace}[${key}]`
+    else formKey = key
+
+    if (obj[key] === undefined) continue
+
+    if (typeof obj[ key ] === 'object' && !(obj[ key ] instanceof File)) {
+      objectToFormData(obj[ key ], fd, key)
+    } else {
+      fd.append(formKey, obj[ key ])
+    }
+  }
+
+  return fd
+}
+
 export {
   viewportHeight,
   getParameterByName,
@@ -75,5 +96,6 @@ export {
   dateToHuman,
   isInSmallView,
   isInMobileView,
-  immutableAssign
+  immutableAssign,
+  objectToFormData
 }
index 330a0ba84a4d3c5240900b0840a0cfe94e629777..2a942647947b72b3ed5d184fbfbae5f7e4ba3525 100644 (file)
@@ -40,10 +40,10 @@ import { VideoService } from './video/video.service'
 
     BsDropdownModule.forRoot(),
     ModalModule.forRoot(),
+    TabsModule.forRoot(),
 
     PrimeSharedModule,
-    NgPipesModule,
-    TabsModule.forRoot()
+    NgPipesModule
   ],
 
   declarations: [
@@ -69,6 +69,7 @@ import { VideoService } from './video/video.service'
 
     BsDropdownModule,
     ModalModule,
+    TabsModule,
     PrimeSharedModule,
     BytesPipe,
     KeysPipe,
index b1c77221783b11609b19891b32120769855e4069..c39252f46fffa527b81381ef206aa273fb62bcdd 100644 (file)
@@ -12,6 +12,10 @@ export class VideoEdit {
   commentsEnabled: boolean
   channel: number
   privacy: VideoPrivacy
+  thumbnailfile?: any
+  previewfile?: any
+  thumbnailUrl: string
+  previewUrl: string
   uuid?: string
   id?: number
 
@@ -29,6 +33,8 @@ export class VideoEdit {
       this.commentsEnabled = videoDetails.commentsEnabled
       this.channel = videoDetails.channel.id
       this.privacy = videoDetails.privacy
+      this.thumbnailUrl = videoDetails.thumbnailUrl
+      this.previewUrl = videoDetails.previewUrl
     }
   }
 
index 8acfb3c41b3723266f0b163e1fa1fb642658f6b7..4604d10e250f4b6490facd14a2dce13c3880a043 100644 (file)
@@ -2,7 +2,7 @@
   [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name"
 class="video-thumbnail"
 >
-<img [attr.src]="getImageUrl()" alt="video thumbnail" [ngClass]="{ 'blur-filter': nsfw }" />
+<img [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" />
 
 <div class="video-thumbnail-overlay">
   {{ video.durationLabel }}
index 01d32176b0d61b77314aa6b3896837d9d23580f4..2e7138cd1fe187a6b16ad9249ff74aa554b600de 100644 (file)
@@ -18,6 +18,7 @@ import { SortField } from './sort-field.type'
 import { VideoDetails } from './video-details.model'
 import { VideoEdit } from './video-edit.model'
 import { Video } from './video.model'
+import { objectToFormData } from '@app/shared/misc/utils'
 
 @Injectable()
 export class VideoService {
@@ -46,10 +47,10 @@ export class VideoService {
   }
 
   updateVideo (video: VideoEdit) {
-    const language = video.language || null
-    const licence = video.licence || null
-    const category = video.category || null
-    const description = video.description || null
+    const language = video.language || undefined
+    const licence = video.licence || undefined
+    const category = video.category || undefined
+    const description = video.description || undefined
 
     const body: VideoUpdate = {
       name: video.name,
@@ -60,10 +61,14 @@ export class VideoService {
       privacy: video.privacy,
       tags: video.tags,
       nsfw: video.nsfw,
-      commentsEnabled: video.commentsEnabled
+      commentsEnabled: video.commentsEnabled,
+      thumbnailfile: video.thumbnailfile,
+      previewfile: video.previewfile
     }
 
-    return this.authHttp.put(VideoService.BASE_VIDEO_URL + video.id, body)
+    const data = objectToFormData(body)
+
+    return this.authHttp.put(VideoService.BASE_VIDEO_URL + video.id, data)
                         .map(this.restExtractor.extractDataBool)
                         .catch(this.restExtractor.handleError)
   }
index d031825bdb9ad391d0dfe601c3ec3b3447dc998b..899249778f4be37b17f8abbda397619e7f10b619 100644 (file)
 <div class="video-edit row" [formGroup]="form">
-
-  <div class="col-md-8">
-    <div class="form-group">
-      <label for="name">Title</label>
-      <input type="text" id="name" formControlName="name" />
-      <div *ngIf="formErrors.name" class="form-error">
-        {{ formErrors.name }}
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label class="label-tags">Tags</label> <span>(press Enter to add)</span>
-      <tag-input
-        [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
-        formControlName="tags" maxItems="5" modelAsStrings="true"
-      ></tag-input>
-    </div>
-
-    <div class="form-group">
-      <label for="description">Description</label>
-      <my-markdown-textarea truncate="250" formControlName="description"></my-markdown-textarea>
-
-      <div *ngIf="formErrors.description" class="form-error">
-        {{ formErrors.description }}
-      </div>
-    </div>
-  </div>
-
-  <div class="col-md-4">
-    <div class="form-group">
-      <label>Channel</label>
-      <div class="peertube-select-disabled-container">
-        <select formControlName="channelId">
-          <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
-        </select>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="category">Category</label>
-      <div class="peertube-select-container">
-        <select id="category" formControlName="category">
-          <option></option>
-          <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option>
-        </select>
-      </div>
-
-      <div *ngIf="formErrors.category" class="form-error">
-        {{ formErrors.category }}
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="licence">Licence</label>
-      <div class="peertube-select-container">
-        <select id="licence" formControlName="licence">
-          <option></option>
-          <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option>
-        </select>
+  <tabset class="root-tabset bootstrap">
+
+    <tab heading="Basic info">
+      <div class="col-md-8">
+        <div class="form-group">
+          <label for="name">Title</label>
+          <input type="text" id="name" formControlName="name" />
+          <div *ngIf="formErrors.name" class="form-error">
+            {{ formErrors.name }}
+          </div>
+        </div>
+
+        <div class="form-group">
+          <label class="label-tags">Tags</label> <span>(press Enter to add)</span>
+          <tag-input
+            [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
+            formControlName="tags" maxItems="5" modelAsStrings="true"
+          ></tag-input>
+        </div>
+
+        <div class="form-group">
+          <label for="description">Description</label>
+          <my-markdown-textarea truncate="250" formControlName="description"></my-markdown-textarea>
+
+          <div *ngIf="formErrors.description" class="form-error">
+            {{ formErrors.description }}
+          </div>
+        </div>
       </div>
 
-      <div *ngIf="formErrors.licence" class="form-error">
-        {{ formErrors.licence }}
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="language">Language</label>
-      <div class="peertube-select-container">
-        <select id="language" formControlName="language">
-          <option></option>
-          <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option>
-        </select>
-      </div>
+      <div class="col-md-4">
+        <div class="form-group">
+          <label>Channel</label>
+          <div class="peertube-select-disabled-container">
+            <select formControlName="channelId">
+              <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
+            </select>
+          </div>
+        </div>
+
+        <div class="form-group">
+          <label for="category">Category</label>
+          <div class="peertube-select-container">
+            <select id="category" formControlName="category">
+              <option></option>
+              <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option>
+            </select>
+          </div>
+
+          <div *ngIf="formErrors.category" class="form-error">
+            {{ formErrors.category }}
+          </div>
+        </div>
+
+        <div class="form-group">
+          <label for="licence">Licence</label>
+          <div class="peertube-select-container">
+            <select id="licence" formControlName="licence">
+              <option></option>
+              <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option>
+            </select>
+          </div>
+
+          <div *ngIf="formErrors.licence" class="form-error">
+            {{ formErrors.licence }}
+          </div>
+        </div>
+
+        <div class="form-group">
+          <label for="language">Language</label>
+          <div class="peertube-select-container">
+            <select id="language" formControlName="language">
+              <option></option>
+              <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option>
+            </select>
+          </div>
+
+          <div *ngIf="formErrors.language" class="form-error">
+            {{ formErrors.language }}
+          </div>
+        </div>
+
+        <div class="form-group">
+          <label for="privacy">Privacy</label>
+          <div class="peertube-select-container">
+            <select id="privacy" formControlName="privacy">
+              <option></option>
+              <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
+            </select>
+          </div>
+
+          <div *ngIf="formErrors.privacy" class="form-error">
+            {{ formErrors.privacy }}
+          </div>
+        </div>
+
+        <div class="form-group form-group-checkbox">
+          <input type="checkbox" id="nsfw" formControlName="nsfw" />
+          <label for="nsfw"></label>
+          <label for="nsfw">This video contains mature or explicit content</label>
+        </div>
+
+        <div class="form-group form-group-checkbox">
+          <input type="checkbox" id="commentsEnabled" formControlName="commentsEnabled" />
+          <label for="commentsEnabled"></label>
+          <label for="commentsEnabled">Enable video comments</label>
+        </div>
 
-      <div *ngIf="formErrors.language" class="form-error">
-        {{ formErrors.language }}
       </div>
-    </div>
-
-    <div class="form-group">
-      <label for="privacy">Privacy</label>
-      <div class="peertube-select-container">
-        <select id="privacy" formControlName="privacy">
-          <option></option>
-          <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
-        </select>
+    </tab>
+
+    <tab heading="Advanced settings">
+      <div class="col-md-12">
+        <div class="form-group">
+          <my-video-image
+            inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile"
+            previewWidth="200px" previewHeight="110px"
+          ></my-video-image>
+        </div>
+
+        <div class="form-group">
+          <my-video-image
+            inputLabel="Upload preview" inputName="previewfile" formControlName="previewfile"
+            previewWidth="360px" previewHeight="200px"
+          ></my-video-image>
+        </div>
       </div>
+    </tab>
 
-      <div *ngIf="formErrors.privacy" class="form-error">
-        {{ formErrors.privacy }}
-      </div>
-    </div>
-
-    <div class="form-group form-group-checkbox">
-      <input type="checkbox" id="nsfw" formControlName="nsfw" />
-      <label for="nsfw"></label>
-      <label for="nsfw">This video contains mature or explicit content</label>
-    </div>
-
-    <div class="form-group form-group-checkbox">
-      <input type="checkbox" id="commentsEnabled" formControlName="commentsEnabled" />
-      <label for="commentsEnabled"></label>
-      <label for="commentsEnabled">Enable video comments</label>
-    </div>
+  </tabset>
 
-  </div>
 </div>
index 1df9d400610286ef8eac4ffb146748361272d8fd..f78336fa8f226784861968d1b7af7c2d084fd695 100644 (file)
   .label-tags + span {
     font-size: 15px;
   }
+
+  .root-tabset /deep/ > .nav {
+    margin-left: 15px;
+    margin-bottom: 15px;
+
+    .nav-link {
+      display: flex !important;
+      align-items: center;
+      height: 30px !important;
+      padding: 0 15px !important;
+    }
+  }
 }
 
 .submit-container {
index 2b307d5fafd081a408468fac173ad24effd5d240..85e5cc3f596125be76242af72f6049fa197a6903 100644 (file)
@@ -1,6 +1,7 @@
 import { Component, Input, OnInit } from '@angular/core'
 import { FormBuilder, FormControl, FormGroup } from '@angular/forms'
 import { ActivatedRoute, Router } from '@angular/router'
+import { VIDEO_IMAGE } from '@app/shared'
 import { NotificationsService } from 'angular2-notifications'
 import 'rxjs/add/observable/forkJoin'
 import { ServerService } from '../../../core/server'
@@ -57,6 +58,8 @@ export class VideoEditComponent implements OnInit {
     this.formErrors['licence'] = ''
     this.formErrors['language'] = ''
     this.formErrors['description'] = ''
+    this.formErrors['thumbnailfile'] = ''
+    this.formErrors['previewfile'] = ''
 
     this.validationMessages['name'] = VIDEO_NAME.MESSAGES
     this.validationMessages['privacy'] = VIDEO_PRIVACY.MESSAGES
@@ -65,6 +68,8 @@ export class VideoEditComponent implements OnInit {
     this.validationMessages['licence'] = VIDEO_LICENCE.MESSAGES
     this.validationMessages['language'] = VIDEO_LANGUAGE.MESSAGES
     this.validationMessages['description'] = VIDEO_DESCRIPTION.MESSAGES
+    this.validationMessages['thumbnailfile'] = VIDEO_IMAGE.MESSAGES
+    this.validationMessages['previewfile'] = VIDEO_IMAGE.MESSAGES
 
     this.form.addControl('name', new FormControl('', VIDEO_NAME.VALIDATORS))
     this.form.addControl('privacy', new FormControl('', VIDEO_PRIVACY.VALIDATORS))
@@ -76,6 +81,8 @@ export class VideoEditComponent implements OnInit {
     this.form.addControl('language', new FormControl('', VIDEO_LANGUAGE.VALIDATORS))
     this.form.addControl('description', new FormControl('', VIDEO_DESCRIPTION.VALIDATORS))
     this.form.addControl('tags', new FormControl(''))
+    this.form.addControl('thumbnailfile', new FormControl(''))
+    this.form.addControl('previewfile', new FormControl(''))
   }
 
   ngOnInit () {
index 098a71ae6d93e7180cbb58d78023a91c8dd9ba65..1b82281bfcd06ab6546e166a724692700aba4ed9 100644 (file)
@@ -1,4 +1,5 @@
 import { NgModule } from '@angular/core'
+import { VideoImageComponent } from '@app/videos/+video-edit/shared/video-image.component'
 import { TabsModule } from 'ngx-bootstrap/tabs'
 import { TagInputModule } from 'ngx-chips'
 import { SharedModule } from '../../../shared'
@@ -12,7 +13,8 @@ import { VideoEditComponent } from './video-edit.component'
   ],
 
   declarations: [
-    VideoEditComponent
+    VideoEditComponent,
+    VideoImageComponent
   ],
 
   exports: [
index 7f41b56d8ab8771718e5b372025774bdb8825f0c..ad6452835f4d522242a05e4e86f134943c36137f 100644 (file)
@@ -48,11 +48,10 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
     this.buildForm()
 
     this.serverService.videoPrivaciesLoaded
-      .subscribe(
-        () => this.videoPrivacies = this.serverService.getVideoPrivacies()
-      )
+      .subscribe(() => this.videoPrivacies = this.serverService.getVideoPrivacies())
 
     populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
+      .catch(err => console.error('Cannot populate async user video channels.', err))
 
     const uuid: string = this.route.snapshot.params['uuid']
     this.videoService.getVideo(uuid)
@@ -116,5 +115,26 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
 
   private hydrateFormFromVideo () {
     this.form.patchValue(this.video.toJSON())
+
+    const objects = [
+      {
+        url: 'thumbnailUrl',
+        name: 'thumbnailfile'
+      },
+      {
+        url: 'previewUrl',
+        name: 'previewfile'
+      }
+    ]
+
+    for (const obj of objects) {
+      fetch(this.video[obj.url])
+        .then(response => response.blob())
+        .then(data => {
+          this.form.patchValue({
+            [ obj.name ]: data
+          })
+        })
+    }
   }
 }
index af90e22a19eb33f851eb4019c7f3a2d51593354f..8c173d6b35fcdd0a6608b1da3bbd2869f404472a 100644 (file)
@@ -1,7 +1,7 @@
 <div class="row">
   <!-- We need the video container for videojs so we just hide it -->
   <div [hidden]="videoNotFound" id="video-container">
-     <video id="video-element" class="video-js vjs-peertube-skin"></video>
+     <video [poster]="getVideoPoster()" id="video-element" class="video-js vjs-peertube-skin"></video>
   </div>
 
   <div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div>
index 7a64406e6fc424f087ed4d87187f9c7a7559ef1f..7c97f0964af48d9196c1469a1e05209583bf25c4 100644 (file)
@@ -211,6 +211,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     return Account.GET_ACCOUNT_AVATAR_URL(this.video.account)
   }
 
+  getVideoPoster () {
+    if (!this.video) return ''
+
+    return this.video.previewUrl
+  }
+
   getVideoTags () {
     if (!this.video || Array.isArray(this.video.tags) === false) return []
 
index 34022100200f8de3b3d3f04dc95a6bb6ee305ed7..80dd3408f223c3dc7d444d643df101099e5dd75e 100644 (file)
@@ -299,34 +299,45 @@ p-datatable {
   }
 }
 
-.nav {
-  font-size: 16px !important;
-  border: none !important;
-
-  .nav-item .nav-link {
-    margin-right: 30px;
-    padding: 0;
-    border-radius: 3px;
+tabset:not(.bootstrap) {
+  .nav {
+    font-size: 16px !important;
     border: none !important;
 
-    .tab-link {
-      display: flex !important;
-      align-items: center;
-      min-height: 30px !important;
-      padding: 0 15px;
-    }
+    .nav-item .nav-link {
+      margin-right: 30px;
+      padding: 0;
+      border-radius: 3px;
+      border: none !important;
 
-    &, & a {
-      color: #000 !important;
-      @include disable-default-a-behaviour;
-    }
+      .tab-link {
+        display: flex !important;
+        align-items: center;
+        min-height: 30px !important;
+        padding: 0 15px;
+      }
 
-    &.active, &:hover {
-      background-color: #F0F0F0;
+      &, & a {
+        color: #000 !important;
+        @include disable-default-a-behaviour;
+      }
+
+      &.active, &:hover {
+        background-color: #F0F0F0;
+      }
+
+      &.active {
+        font-weight: $font-semibold !important;
+      }
     }
+  }
+}
 
-    &.active {
-      font-weight: $font-semibold !important;
+tabset.bootstrap {
+  .nav-item .nav-link {
+    &, & a {
+      color: #000;
+      @include disable-default-a-behaviour;
     }
   }
 }
index 89163edb3f99fba5ec26148d5c4efcaaf993fa40..532afb8c0832f0119e327e65938aa5a13e64c910 100644 (file)
@@ -61,6 +61,12 @@ async function getConfig (req: express.Request, res: express.Response, next: exp
       }
     },
     video: {
+      image: {
+        extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME,
+        size: {
+          max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max
+        }
+      },
       file: {
         extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME
       }
index fdc36bcc17f57b7c6ebdccf42bc51ff72017911f..988dd71e3635ed1056df6f7159a87aa8887adcdb 100644 (file)
@@ -23,6 +23,12 @@ export interface ServerConfig {
   }
 
   video: {
+    image: {
+      size: {
+        max: number
+      }
+      extensions: string[]
+    },
     file: {
       extensions: string[]
     }
index 0b26484d7aa8da870f77a703a3d2ab8ac6923bc5..b281ace9af0c9edc23df43ae8a54cafd090178f5 100644 (file)
@@ -11,4 +11,6 @@ export interface VideoUpdate {
   tags?: string[]
   commentsEnabled?: boolean
   nsfw?: boolean
+  thumbnailfile?: Blob
+  previewfile?: Blob
 }