Support audio upload in client

This commit is contained in:
Chocobozzz 2019-05-17 10:45:53 +02:00
parent 536598cfaf
commit 7b992a86b1
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
19 changed files with 192 additions and 107 deletions

View File

@ -57,10 +57,12 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<my-image-upload <label i18n>Playlist thumbnail</label>
i18n-inputLabel inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile"
previewWidth="200px" previewHeight="110px" <my-preview-upload
></my-image-upload> i18n-inputLabel inputLabel="Edit" inputName="thumbnailfile" formControlName="thumbnailfile"
previewWidth="223px" previewHeight="122px"
></my-preview-upload>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,6 +1,4 @@
import { FormReactive } from '@app/shared' import { FormReactive } from '@app/shared'
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
import { ServerService } from '@app/core'
import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model' import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model'
export abstract class MyAccountVideoPlaylistEdit extends FormReactive { export abstract class MyAccountVideoPlaylistEdit extends FormReactive {

View File

@ -4,18 +4,6 @@
.action-button { .action-button {
@include peertube-button-link; @include peertube-button-link;
@include button-with-icon(21px, 0, -2px); @include button-with-icon(21px, 0, -2px);
font-weight: $font-semibold;
color: $grey-foreground-color;
background-color: $grey-background-color;
&:hover {
background-color: $grey-background-hover-color;
}
my-global-icon {
@include apply-svg-color($grey-foreground-color);
}
} }
// In a table, try to minimize the space taken by this button // In a table, try to minimize the space taken by this button

View File

@ -9,7 +9,7 @@ import { GlobalIconName } from '@app/shared/images/global-icon.component'
export class ButtonComponent { export class ButtonComponent {
@Input() label = '' @Input() label = ''
@Input() className: string = undefined @Input() className: 'orange-button' | 'grey-button' = 'grey-button'
@Input() icon: GlobalIconName = undefined @Input() icon: GlobalIconName = undefined
@Input() title: string = undefined @Input() title: string = undefined

View File

@ -1,6 +1,9 @@
<div class="root"> <div class="root">
<div class="button-file"> <div class="button-file" [ngClass]="{ 'with-icon': !!icon }">
<my-global-icon *ngIf="icon" [iconName]="icon"></my-global-icon>
<span>{{ inputLabel }}</span> <span>{{ inputLabel }}</span>
<input <input
type="file" type="file"
[name]="inputName" [id]="inputName" [accept]="extensions" [name]="inputName" [id]="inputName" [accept]="extensions"
@ -8,7 +11,5 @@
/> />
</div> </div>
<div i18n class="file-constraints">(extensions: {{ allowedExtensionsMessage }}, max size: {{ maxFileSize | bytes }})</div>
<div class="filename" *ngIf="displayFilename === true && filename">{{ filename }}</div> <div class="filename" *ngIf="displayFilename === true && filename">{{ filename }}</div>
</div> </div>

View File

@ -8,13 +8,11 @@
.button-file { .button-file {
@include peertube-button-file(auto); @include peertube-button-file(auto);
@include grey-button;
min-width: 190px; &.with-icon {
@include button-with-icon;
} }
.file-constraints {
margin-left: 5px;
font-size: 13px;
} }
.filename { .filename {

View File

@ -2,6 +2,7 @@ import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@ang
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
import { Notifier } from '@app/core' import { Notifier } from '@app/core'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { GlobalIconName } from '@app/shared/images/global-icon.component'
@Component({ @Component({
selector: 'my-reactive-file', selector: 'my-reactive-file',
@ -21,6 +22,7 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor {
@Input() extensions: string[] = [] @Input() extensions: string[] = []
@Input() maxFileSize: number @Input() maxFileSize: number
@Input() displayFilename = false @Input() displayFilename = false
@Input() icon: GlobalIconName
@Output() fileChanged = new EventEmitter<Blob>() @Output() fileChanged = new EventEmitter<Blob>()

View File

@ -1,9 +0,0 @@
<div class="root">
<my-reactive-file
[inputName]="inputName" [inputLabel]="inputLabel" [extensions]="videoImageExtensions" [maxFileSize]="maxVideoImageSize"
(fileChanged)="onFileChanged($event)"
></my-reactive-file>
<img *ngIf="imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" [src]="imageSrc" class="preview" />
<div *ngIf="!imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" class="preview no-image"></div>
</div>

View File

@ -1,18 +0,0 @@
@import '_variables';
@import '_mixins';
.root {
height: auto;
display: flex;
align-items: center;
.preview {
border: 2px solid grey;
border-radius: 4px;
margin-left: 50px;
&.no-image {
background-color: #ececec;
}
}
}

View File

@ -0,0 +1,13 @@
<div class="root">
<div class="preview-container">
<my-reactive-file
[inputName]="inputName" [inputLabel]="inputLabel" [extensions]="videoImageExtensions" [maxFileSize]="maxVideoImageSize"
icon="edit" (fileChanged)="onFileChanged($event)"
></my-reactive-file>
<img *ngIf="imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" [src]="imageSrc" class="preview" />
<div *ngIf="!imageSrc" [ngStyle]="{ width: previewWidth, height: previewHeight }" class="preview no-image"></div>
</div>
<div i18n class="file-constraints">(extensions: {{ allowedExtensionsMessage }}, max size: {{ maxVideoImageSize | bytes }})</div>
</div>

View File

@ -0,0 +1,27 @@
@import '_variables';
@import '_mixins';
.root {
height: auto;
display: flex;
flex-direction: column;
.preview-container {
position: relative;
my-reactive-file {
position: absolute;
bottom: 10px;
left: 10px;
}
.preview {
border: 2px solid grey;
border-radius: 4px;
&.no-image {
background-color: #ececec;
}
}
}
}

View File

@ -1,27 +1,28 @@
import { Component, forwardRef, Input } from '@angular/core' import { Component, forwardRef, Input, OnInit } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
import { ServerService } from '@app/core' import { ServerService } from '@app/core'
@Component({ @Component({
selector: 'my-image-upload', selector: 'my-preview-upload',
styleUrls: [ './image-upload.component.scss' ], styleUrls: [ './preview-upload.component.scss' ],
templateUrl: './image-upload.component.html', templateUrl: './preview-upload.component.html',
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ImageUploadComponent), useExisting: forwardRef(() => PreviewUploadComponent),
multi: true multi: true
} }
] ]
}) })
export class ImageUploadComponent implements ControlValueAccessor { export class PreviewUploadComponent implements OnInit, ControlValueAccessor {
@Input() inputLabel: string @Input() inputLabel: string
@Input() inputName: string @Input() inputName: string
@Input() previewWidth: string @Input() previewWidth: string
@Input() previewHeight: string @Input() previewHeight: string
imageSrc: SafeResourceUrl imageSrc: SafeResourceUrl
allowedExtensionsMessage = ''
private file: File private file: File
@ -38,6 +39,10 @@ export class ImageUploadComponent implements ControlValueAccessor {
return this.serverService.getConfig().video.image.size.max return this.serverService.getConfig().video.image.size.max
} }
ngOnInit () {
this.allowedExtensionsMessage = this.videoImageExtensions.join(', ')
}
onFileChanged (file: File) { onFileChanged (file: File) {
this.file = file this.file = file

View File

@ -69,7 +69,7 @@ import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/sha
import { ConfirmComponent } from '@app/shared/confirm/confirm.component' import { ConfirmComponent } from '@app/shared/confirm/confirm.component'
import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component' import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
import { ImageUploadComponent } from '@app/shared/images/image-upload.component' import { PreviewUploadComponent } from '@app/shared/images/preview-upload.component'
import { GlobalIconComponent } from '@app/shared/images/global-icon.component' import { GlobalIconComponent } from '@app/shared/images/global-icon.component'
import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component' import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component'
import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component' import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component'
@ -154,7 +154,7 @@ import { ClipboardModule } from 'ngx-clipboard'
ConfirmComponent, ConfirmComponent,
GlobalIconComponent, GlobalIconComponent,
ImageUploadComponent PreviewUploadComponent
], ],
exports: [ exports: [
@ -218,7 +218,7 @@ import { ClipboardModule } from 'ngx-clipboard'
ConfirmComponent, ConfirmComponent,
GlobalIconComponent, GlobalIconComponent,
ImageUploadComponent, PreviewUploadComponent,
NumberFormatterPipe, NumberFormatterPipe,
ObjectLengthPipe, ObjectLengthPipe,

View File

@ -85,6 +85,11 @@ export class VideoEdit implements VideoUpdate {
const originallyPublishedAt = new Date(values['originallyPublishedAt']) const originallyPublishedAt = new Date(values['originallyPublishedAt'])
this.originallyPublishedAt = originallyPublishedAt.toISOString() this.originallyPublishedAt = originallyPublishedAt.toISOString()
} }
// Use the same file than the preview for the thumbnail
if (this.previewfile) {
this.thumbnailfile = this.previewfile
}
} }
toFormPatch () { toFormPatch () {

View File

@ -187,18 +187,14 @@
<ng-template ngbTabContent> <ng-template ngbTabContent>
<div class="row advanced-settings"> <div class="row advanced-settings">
<div class="col-md-12 col-xl-8"> <div class="col-md-12 col-xl-8">
<div class="form-group">
<my-image-upload
i18n-inputLabel inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile"
previewWidth="200px" previewHeight="110px"
></my-image-upload>
</div>
<div class="form-group"> <div class="form-group">
<my-image-upload <label i18n for="previewfile">Video preview</label>
i18n-inputLabel inputLabel="Upload preview" inputName="previewfile" formControlName="previewfile"
<my-preview-upload
i18n-inputLabel inputLabel="Edit" inputName="previewfile" formControlName="previewfile"
previewWidth="360px" previewHeight="200px" previewWidth="360px" previewHeight="200px"
></my-image-upload> ></my-preview-upload>
</div> </div>
<div class="form-group"> <div class="form-group">

View File

@ -100,7 +100,6 @@ export class VideoEditComponent implements OnInit, OnDestroy {
language: this.videoValidatorsService.VIDEO_LANGUAGE, language: this.videoValidatorsService.VIDEO_LANGUAGE,
description: this.videoValidatorsService.VIDEO_DESCRIPTION, description: this.videoValidatorsService.VIDEO_DESCRIPTION,
tags: null, tags: null,
thumbnailfile: null,
previewfile: null, previewfile: null,
support: this.videoValidatorsService.VIDEO_SUPPORT, support: this.videoValidatorsService.VIDEO_SUPPORT,
schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT, schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT,

View File

@ -26,6 +26,27 @@
</select> </select>
</div> </div>
</div> </div>
<ng-container *ngIf="isUploadingAudioFile">
<div class="form-group audio-preview">
<label i18n for="previewfileUpload">Video background image</label>
<div i18n class="audio-image-info">
Image that will be merged with your audio file.
<br />
The chosen image will be definitive and cannot be modified.
</div>
<my-preview-upload
i18n-inputLabel inputLabel="Edit" inputName="previewfileUpload" [(ngModel)]="previewfileUpload"
previewWidth="360px" previewHeight="200px"
></my-preview-upload>
</div>
<div class="form-group upload-audio-button">
<my-button className="orange-button" i18n-label [label]="getAudioUploadLabel()" icon="upload" (click)="uploadFirstStep(true)"></my-button>
</div>
</ng-container>
</div> </div>
</div> </div>

View File

@ -1,11 +1,22 @@
@import 'variables'; @import 'variables';
@import 'mixins'; @import 'mixins';
.first-step-block .form-group-channel { .first-step-block {
.form-group-channel {
margin-bottom: 20px; margin-bottom: 20px;
margin-top: 35px; margin-top: 35px;
} }
.audio-image-info {
margin-bottom: 10px;
}
.audio-preview {
margin: 30px 0;
}
}
.upload-progress-cancel { .upload-progress-cancel {
display: flex; display: flex;
margin-top: 25px; margin-top: 25px;

View File

@ -35,8 +35,10 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
userVideoQuotaUsed = 0 userVideoQuotaUsed = 0
userVideoQuotaUsedDaily = 0 userVideoQuotaUsedDaily = 0
isUploadingAudioFile = false
isUploadingVideo = false isUploadingVideo = false
isUpdatingVideo = false isUpdatingVideo = false
videoUploaded = false videoUploaded = false
videoUploadObservable: Subscription = null videoUploadObservable: Subscription = null
videoUploadPercents = 0 videoUploadPercents = 0
@ -44,7 +46,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
id: 0, id: 0,
uuid: '' uuid: ''
} }
waitTranscodingEnabled = true waitTranscodingEnabled = true
previewfileUpload: File
error: string error: string
@ -100,6 +104,17 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
} }
} }
getVideoFile () {
return this.videofileInput.nativeElement.files[0]
}
getAudioUploadLabel () {
const videofile = this.getVideoFile()
if (!videofile) return this.i18n('Upload')
return this.i18n('Upload {{videofileName}}', { videofileName: videofile.name })
}
fileChange () { fileChange () {
this.uploadFirstStep() this.uploadFirstStep()
} }
@ -114,38 +129,15 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
} }
} }
uploadFirstStep () { uploadFirstStep (clickedOnButton = false) {
const videofile = this.videofileInput.nativeElement.files[0] const videofile = this.getVideoFile()
if (!videofile) return if (!videofile) return
// Check global user quota if (!this.checkGlobalUserQuota(videofile)) return
const bytePipes = new BytesPipe() if (!this.checkDailyUserQuota(videofile)) return
const videoQuota = this.authService.getUser().videoQuota
if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
const msg = this.i18n(
'Your video quota is exceeded with this video (video size: {{videoSize}}, used: {{videoQuotaUsed}}, quota: {{videoQuota}})',
{
videoSize: bytePipes.transform(videofile.size, 0),
videoQuotaUsed: bytePipes.transform(this.userVideoQuotaUsed, 0),
videoQuota: bytePipes.transform(videoQuota, 0)
}
)
this.notifier.error(msg)
return
}
// Check daily user quota if (clickedOnButton === false && this.isAudioFile(videofile.name)) {
const videoQuotaDaily = this.authService.getUser().videoQuotaDaily this.isUploadingAudioFile = true
if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) {
const msg = this.i18n(
'Your daily video quota is exceeded with this video (video size: {{videoSize}}, used: {{quotaUsedDaily}}, quota: {{quotaDaily}})',
{
videoSize: bytePipes.transform(videofile.size, 0),
quotaUsedDaily: bytePipes.transform(this.userVideoQuotaUsedDaily, 0),
quotaDaily: bytePipes.transform(videoQuotaDaily, 0)
}
)
this.notifier.error(msg)
return return
} }
@ -180,6 +172,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
formData.append('channelId', '' + channelId) formData.append('channelId', '' + channelId)
formData.append('videofile', videofile) formData.append('videofile', videofile)
if (this.previewfileUpload) {
formData.append('previewfile', this.previewfileUpload)
formData.append('thumbnailfile', this.previewfileUpload)
}
this.isUploadingVideo = true this.isUploadingVideo = true
this.firstStepDone.emit(name) this.firstStepDone.emit(name)
@ -187,7 +184,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
name, name,
privacy, privacy,
nsfw, nsfw,
channelId channelId,
previewfile: this.previewfileUpload
}) })
this.explainedVideoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies) this.explainedVideoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies)
@ -251,4 +249,52 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
} }
) )
} }
private checkGlobalUserQuota (videofile: File) {
const bytePipes = new BytesPipe()
// Check global user quota
const videoQuota = this.authService.getUser().videoQuota
if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
const msg = this.i18n(
'Your video quota is exceeded with this video (video size: {{videoSize}}, used: {{videoQuotaUsed}}, quota: {{videoQuota}})',
{
videoSize: bytePipes.transform(videofile.size, 0),
videoQuotaUsed: bytePipes.transform(this.userVideoQuotaUsed, 0),
videoQuota: bytePipes.transform(videoQuota, 0)
}
)
this.notifier.error(msg)
return false
}
return true
}
private checkDailyUserQuota (videofile: File) {
const bytePipes = new BytesPipe()
// Check daily user quota
const videoQuotaDaily = this.authService.getUser().videoQuotaDaily
if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) {
const msg = this.i18n(
'Your daily video quota is exceeded with this video (video size: {{videoSize}}, used: {{quotaUsedDaily}}, quota: {{quotaDaily}})',
{
videoSize: bytePipes.transform(videofile.size, 0),
quotaUsedDaily: bytePipes.transform(this.userVideoQuotaUsedDaily, 0),
quotaDaily: bytePipes.transform(videoQuotaDaily, 0)
}
)
this.notifier.error(msg)
return false
}
return true
}
private isAudioFile (filename: string) {
return filename.endsWith('.mp3') || filename.endsWith('.flac') || filename.endsWith('.ogg')
}
} }