</tabset>
</div>
-
-
<router-outlet></router-outlet>
}
},
video: {
+ image: {
+ size: { max: 0 },
+ extensions: []
+ },
file: {
extensions: []
}
MESSAGES: {}
}
+export const VIDEO_IMAGE = {
+ VALIDATORS: [ ],
+ MESSAGES: {}
+}
+
export const VIDEO_CHANNEL = {
VALIDATORS: [ Validators.required ],
MESSAGES: {
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>
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,
dateToHuman,
isInSmallView,
isInMobileView,
- immutableAssign
+ immutableAssign,
+ objectToFormData
}
BsDropdownModule.forRoot(),
ModalModule.forRoot(),
+ TabsModule.forRoot(),
PrimeSharedModule,
- NgPipesModule,
- TabsModule.forRoot()
+ NgPipesModule
],
declarations: [
BsDropdownModule,
ModalModule,
+ TabsModule,
PrimeSharedModule,
BytesPipe,
KeysPipe,
commentsEnabled: boolean
channel: number
privacy: VideoPrivacy
+ thumbnailfile?: any
+ previewfile?: any
+ thumbnailUrl: string
+ previewUrl: string
uuid?: string
id?: number
this.commentsEnabled = videoDetails.commentsEnabled
this.channel = videoDetails.channel.id
this.privacy = videoDetails.privacy
+ this.thumbnailUrl = videoDetails.thumbnailUrl
+ this.previewUrl = videoDetails.previewUrl
}
}
[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 }}
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 {
}
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,
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)
}
<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>
.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 {
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'
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
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))
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 () {
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'
],
declarations: [
- VideoEditComponent
+ VideoEditComponent,
+ VideoImageComponent
],
exports: [
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)
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
+ })
+ })
+ }
}
}
<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>
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 []
}
}
-.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;
}
}
}
}
},
video: {
+ image: {
+ extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME,
+ size: {
+ max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max
+ }
+ },
file: {
extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME
}
}
video: {
+ image: {
+ size: {
+ max: number
+ }
+ extensions: string[]
+ },
file: {
extensions: string[]
}
tags?: string[]
commentsEnabled?: boolean
nsfw?: boolean
+ thumbnailfile?: Blob
+ previewfile?: Blob
}