1
0
Fork 0

Add ability to update thumbnail and preview on client

This commit is contained in:
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> </tabset>
</div> </div>
<router-outlet></router-outlet> <router-outlet></router-outlet>

View file

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

View file

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

View file

@ -5,7 +5,7 @@
id="description" name="description"> id="description" name="description">
</textarea> </textarea>
<tabset *ngIf="arePreviewsDisplayed()" #staticTabs class="previews"> <tabset *ngIf="arePreviewsDisplayed()" class="previews">
<tab *ngIf="truncate !== undefined" heading="Truncated description preview" [innerHTML]="truncatedDescriptionHTML"></tab> <tab *ngIf="truncate !== undefined" heading="Truncated description preview" [innerHTML]="truncatedDescriptionHTML"></tab>
<tab heading="Complete description preview" [innerHTML]="descriptionHTML"></tab> <tab heading="Complete description preview" [innerHTML]="descriptionHTML"></tab>
</tabset> </tabset>

View file

@ -67,6 +67,27 @@ function isInMobileView () {
return window.innerWidth < 500 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 { export {
viewportHeight, viewportHeight,
getParameterByName, getParameterByName,
@ -75,5 +96,6 @@ export {
dateToHuman, dateToHuman,
isInSmallView, isInSmallView,
isInMobileView, isInMobileView,
immutableAssign immutableAssign,
objectToFormData
} }

View file

@ -40,10 +40,10 @@ import { VideoService } from './video/video.service'
BsDropdownModule.forRoot(), BsDropdownModule.forRoot(),
ModalModule.forRoot(), ModalModule.forRoot(),
TabsModule.forRoot(),
PrimeSharedModule, PrimeSharedModule,
NgPipesModule, NgPipesModule
TabsModule.forRoot()
], ],
declarations: [ declarations: [
@ -69,6 +69,7 @@ import { VideoService } from './video/video.service'
BsDropdownModule, BsDropdownModule,
ModalModule, ModalModule,
TabsModule,
PrimeSharedModule, PrimeSharedModule,
BytesPipe, BytesPipe,
KeysPipe, KeysPipe,

View file

@ -12,6 +12,10 @@ export class VideoEdit {
commentsEnabled: boolean commentsEnabled: boolean
channel: number channel: number
privacy: VideoPrivacy privacy: VideoPrivacy
thumbnailfile?: any
previewfile?: any
thumbnailUrl: string
previewUrl: string
uuid?: string uuid?: string
id?: number id?: number
@ -29,6 +33,8 @@ export class VideoEdit {
this.commentsEnabled = videoDetails.commentsEnabled this.commentsEnabled = videoDetails.commentsEnabled
this.channel = videoDetails.channel.id this.channel = videoDetails.channel.id
this.privacy = videoDetails.privacy 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" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name"
class="video-thumbnail" 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"> <div class="video-thumbnail-overlay">
{{ video.durationLabel }} {{ video.durationLabel }}

View file

@ -18,6 +18,7 @@ import { SortField } from './sort-field.type'
import { VideoDetails } from './video-details.model' import { VideoDetails } from './video-details.model'
import { VideoEdit } from './video-edit.model' import { VideoEdit } from './video-edit.model'
import { Video } from './video.model' import { Video } from './video.model'
import { objectToFormData } from '@app/shared/misc/utils'
@Injectable() @Injectable()
export class VideoService { export class VideoService {
@ -46,10 +47,10 @@ export class VideoService {
} }
updateVideo (video: VideoEdit) { updateVideo (video: VideoEdit) {
const language = video.language || null const language = video.language || undefined
const licence = video.licence || null const licence = video.licence || undefined
const category = video.category || null const category = video.category || undefined
const description = video.description || null const description = video.description || undefined
const body: VideoUpdate = { const body: VideoUpdate = {
name: video.name, name: video.name,
@ -60,10 +61,14 @@ export class VideoService {
privacy: video.privacy, privacy: video.privacy,
tags: video.tags, tags: video.tags,
nsfw: video.nsfw, 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) .map(this.restExtractor.extractDataBool)
.catch(this.restExtractor.handleError) .catch(this.restExtractor.handleError)
} }

View file

@ -1,109 +1,133 @@
<div class="video-edit row" [formGroup]="form"> <div class="video-edit row" [formGroup]="form">
<tabset class="root-tabset bootstrap">
<div class="col-md-8"> <tab heading="Basic info">
<div class="form-group"> <div class="col-md-8">
<label for="name">Title</label> <div class="form-group">
<input type="text" id="name" formControlName="name" /> <label for="name">Title</label>
<div *ngIf="formErrors.name" class="form-error"> <input type="text" id="name" formControlName="name" />
{{ formErrors.name }} <div *ngIf="formErrors.name" class="form-error">
</div> {{ formErrors.name }}
</div> </div>
</div>
<div class="form-group"> <div class="form-group">
<label class="label-tags">Tags</label> <span>(press Enter to add)</span> <label class="label-tags">Tags</label> <span>(press Enter to add)</span>
<tag-input <tag-input
[ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" [ngModel]="tags" [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
formControlName="tags" maxItems="5" modelAsStrings="true" formControlName="tags" maxItems="5" modelAsStrings="true"
></tag-input> ></tag-input>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="description">Description</label> <label for="description">Description</label>
<my-markdown-textarea truncate="250" formControlName="description"></my-markdown-textarea> <my-markdown-textarea truncate="250" formControlName="description"></my-markdown-textarea>
<div *ngIf="formErrors.description" class="form-error"> <div *ngIf="formErrors.description" class="form-error">
{{ formErrors.description }} {{ formErrors.description }}
</div> </div>
</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>
<div *ngIf="formErrors.category" class="form-error"> <div class="col-md-4">
{{ formErrors.category }} <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>
</div> </tab>
<div class="form-group"> <tab heading="Advanced settings">
<label for="licence">Licence</label> <div class="col-md-12">
<div class="peertube-select-container"> <div class="form-group">
<select id="licence" formControlName="licence"> <my-video-image
<option></option> inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile"
<option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option> previewWidth="200px" previewHeight="110px"
</select> ></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> </div>
</tab>
<div *ngIf="formErrors.licence" class="form-error"> </tabset>
{{ 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> </div>

View file

@ -47,6 +47,18 @@
.label-tags + span { .label-tags + span {
font-size: 15px; 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 { .submit-container {

View file

@ -1,6 +1,7 @@
import { Component, Input, OnInit } from '@angular/core' import { Component, Input, OnInit } from '@angular/core'
import { FormBuilder, FormControl, FormGroup } from '@angular/forms' import { FormBuilder, FormControl, FormGroup } from '@angular/forms'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { VIDEO_IMAGE } from '@app/shared'
import { NotificationsService } from 'angular2-notifications' import { NotificationsService } from 'angular2-notifications'
import 'rxjs/add/observable/forkJoin' import 'rxjs/add/observable/forkJoin'
import { ServerService } from '../../../core/server' import { ServerService } from '../../../core/server'
@ -57,6 +58,8 @@ export class VideoEditComponent implements OnInit {
this.formErrors['licence'] = '' this.formErrors['licence'] = ''
this.formErrors['language'] = '' this.formErrors['language'] = ''
this.formErrors['description'] = '' this.formErrors['description'] = ''
this.formErrors['thumbnailfile'] = ''
this.formErrors['previewfile'] = ''
this.validationMessages['name'] = VIDEO_NAME.MESSAGES this.validationMessages['name'] = VIDEO_NAME.MESSAGES
this.validationMessages['privacy'] = VIDEO_PRIVACY.MESSAGES this.validationMessages['privacy'] = VIDEO_PRIVACY.MESSAGES
@ -65,6 +68,8 @@ export class VideoEditComponent implements OnInit {
this.validationMessages['licence'] = VIDEO_LICENCE.MESSAGES this.validationMessages['licence'] = VIDEO_LICENCE.MESSAGES
this.validationMessages['language'] = VIDEO_LANGUAGE.MESSAGES this.validationMessages['language'] = VIDEO_LANGUAGE.MESSAGES
this.validationMessages['description'] = VIDEO_DESCRIPTION.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('name', new FormControl('', VIDEO_NAME.VALIDATORS))
this.form.addControl('privacy', new FormControl('', VIDEO_PRIVACY.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('language', new FormControl('', VIDEO_LANGUAGE.VALIDATORS))
this.form.addControl('description', new FormControl('', VIDEO_DESCRIPTION.VALIDATORS)) this.form.addControl('description', new FormControl('', VIDEO_DESCRIPTION.VALIDATORS))
this.form.addControl('tags', new FormControl('')) this.form.addControl('tags', new FormControl(''))
this.form.addControl('thumbnailfile', new FormControl(''))
this.form.addControl('previewfile', new FormControl(''))
} }
ngOnInit () { ngOnInit () {

View file

@ -1,4 +1,5 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { VideoImageComponent } from '@app/videos/+video-edit/shared/video-image.component'
import { TabsModule } from 'ngx-bootstrap/tabs' import { TabsModule } from 'ngx-bootstrap/tabs'
import { TagInputModule } from 'ngx-chips' import { TagInputModule } from 'ngx-chips'
import { SharedModule } from '../../../shared' import { SharedModule } from '../../../shared'
@ -12,7 +13,8 @@ import { VideoEditComponent } from './video-edit.component'
], ],
declarations: [ declarations: [
VideoEditComponent VideoEditComponent,
VideoImageComponent
], ],
exports: [ exports: [

View file

@ -48,11 +48,10 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
this.buildForm() this.buildForm()
this.serverService.videoPrivaciesLoaded this.serverService.videoPrivaciesLoaded
.subscribe( .subscribe(() => this.videoPrivacies = this.serverService.getVideoPrivacies())
() => this.videoPrivacies = this.serverService.getVideoPrivacies()
)
populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
.catch(err => console.error('Cannot populate async user video channels.', err))
const uuid: string = this.route.snapshot.params['uuid'] const uuid: string = this.route.snapshot.params['uuid']
this.videoService.getVideo(uuid) this.videoService.getVideo(uuid)
@ -116,5 +115,26 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
private hydrateFormFromVideo () { private hydrateFormFromVideo () {
this.form.patchValue(this.video.toJSON()) 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"> <div class="row">
<!-- We need the video container for videojs so we just hide it --> <!-- We need the video container for videojs so we just hide it -->
<div [hidden]="videoNotFound" id="video-container"> <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>
<div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</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) return Account.GET_ACCOUNT_AVATAR_URL(this.video.account)
} }
getVideoPoster () {
if (!this.video) return ''
return this.video.previewUrl
}
getVideoTags () { getVideoTags () {
if (!this.video || Array.isArray(this.video.tags) === false) return [] if (!this.video || Array.isArray(this.video.tags) === false) return []

View file

@ -299,35 +299,46 @@ p-datatable {
} }
} }
.nav { tabset:not(.bootstrap) {
font-size: 16px !important; .nav {
border: none !important; font-size: 16px !important;
.nav-item .nav-link {
margin-right: 30px;
padding: 0;
border-radius: 3px;
border: none !important; border: none !important;
.tab-link { .nav-item .nav-link {
display: flex !important; margin-right: 30px;
align-items: center; padding: 0;
min-height: 30px !important; border-radius: 3px;
padding: 0 15px; 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 { &, & a {
color: #000 !important; color: #000;
@include disable-default-a-behaviour; @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: { video: {
image: {
extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME,
size: {
max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max
}
},
file: { file: {
extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME
} }

View file

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

View file

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