Add ability to update thumbnail and preview on client

pull/298/head
Chocobozzz 2018-02-16 16:35:32 +01:00
parent b6a4fd6b09
commit 6de3676898
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
20 changed files with 274 additions and 137 deletions

View File

@ -10,6 +10,4 @@
</tabset>
</div>
<router-outlet></router-outlet>

View File

@ -35,6 +35,10 @@ export class ServerService {
}
},
video: {
image: {
size: { max: 0 },
extensions: []
},
file: {
extensions: []
}

View File

@ -31,6 +31,11 @@ export const VIDEO_LANGUAGE = {
MESSAGES: {}
}
export const VIDEO_IMAGE = {
VALIDATORS: [ ],
MESSAGES: {}
}
export const VIDEO_CHANNEL = {
VALIDATORS: [ Validators.required ],
MESSAGES: {

View 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>

View 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
}

View 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,

View 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
}
}

View 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 }}

View 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)
}

View File

@ -1,109 +1,133 @@
<div class="video-edit row" [formGroup]="form">
<tabset class="root-tabset bootstrap">
<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>
<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 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 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 *ngIf="formErrors.description" class="form-error">
{{ formErrors.description }}
</div>
</div>
</div>
<div *ngIf="formErrors.category" class="form-error">
{{ formErrors.category }}
<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>
</div>
</tab>
<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>
<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.licence" class="form-error">
{{ formErrors.licence }}
</div>
</div>
</tabset>
<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>
</div>

View File

@ -47,6 +47,18 @@
.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 {

View 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 () {

View 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: [

View 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
})
})
}
}
}

View 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>

View 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 []

View File

@ -299,35 +299,46 @@ 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;
.tab-link {
display: flex !important;
align-items: center;
min-height: 30px !important;
padding: 0 15px;
}
&, & a {
color: #000 !important;
@include disable-default-a-behaviour;
}
&.active, &:hover {
background-color: #F0F0F0;
}
&.active {
font-weight: $font-semibold !important;
}
}
}
}
tabset.bootstrap {
.nav-item .nav-link {
&, & a {
color: #000 !important;
color: #000;
@include disable-default-a-behaviour;
}
&.active, &:hover {
background-color: #F0F0F0;
}
&.active {
font-weight: $font-semibold !important;
}
}
}

View 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
}

View File

@ -23,6 +23,12 @@ export interface ServerConfig {
}
video: {
image: {
size: {
max: number
}
extensions: string[]
},
file: {
extensions: string[]
}

View File

@ -11,4 +11,6 @@ export interface VideoUpdate {
tags?: string[]
commentsEnabled?: boolean
nsfw?: boolean
thumbnailfile?: Blob
previewfile?: Blob
}