Support audio upload in client
This commit is contained in:
parent
536598cfaf
commit
7b992a86b1
|
@ -57,10 +57,12 @@
|
|||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-image-upload
|
||||
i18n-inputLabel inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile"
|
||||
previewWidth="200px" previewHeight="110px"
|
||||
></my-image-upload>
|
||||
<label i18n>Playlist thumbnail</label>
|
||||
|
||||
<my-preview-upload
|
||||
i18n-inputLabel inputLabel="Edit" inputName="thumbnailfile" formControlName="thumbnailfile"
|
||||
previewWidth="223px" previewHeight="122px"
|
||||
></my-preview-upload>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
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'
|
||||
|
||||
export abstract class MyAccountVideoPlaylistEdit extends FormReactive {
|
||||
|
|
|
@ -4,18 +4,6 @@
|
|||
.action-button {
|
||||
@include peertube-button-link;
|
||||
@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
|
||||
|
|
|
@ -9,7 +9,7 @@ import { GlobalIconName } from '@app/shared/images/global-icon.component'
|
|||
|
||||
export class ButtonComponent {
|
||||
@Input() label = ''
|
||||
@Input() className: string = undefined
|
||||
@Input() className: 'orange-button' | 'grey-button' = 'grey-button'
|
||||
@Input() icon: GlobalIconName = undefined
|
||||
@Input() title: string = undefined
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
<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>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
[name]="inputName" [id]="inputName" [accept]="extensions"
|
||||
|
@ -8,7 +11,5 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div i18n class="file-constraints">(extensions: {{ allowedExtensionsMessage }}, max size: {{ maxFileSize | bytes }})</div>
|
||||
|
||||
<div class="filename" *ngIf="displayFilename === true && filename">{{ filename }}</div>
|
||||
</div>
|
||||
|
|
|
@ -8,13 +8,11 @@
|
|||
|
||||
.button-file {
|
||||
@include peertube-button-file(auto);
|
||||
@include grey-button;
|
||||
|
||||
min-width: 190px;
|
||||
}
|
||||
|
||||
.file-constraints {
|
||||
margin-left: 5px;
|
||||
font-size: 13px;
|
||||
&.with-icon {
|
||||
@include button-with-icon;
|
||||
}
|
||||
}
|
||||
|
||||
.filename {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@ang
|
|||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { Notifier } from '@app/core'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { GlobalIconName } from '@app/shared/images/global-icon.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-reactive-file',
|
||||
|
@ -21,6 +22,7 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor {
|
|||
@Input() extensions: string[] = []
|
||||
@Input() maxFileSize: number
|
||||
@Input() displayFilename = false
|
||||
@Input() icon: GlobalIconName
|
||||
|
||||
@Output() fileChanged = new EventEmitter<Blob>()
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
|
||||
import { ServerService } from '@app/core'
|
||||
|
||||
@Component({
|
||||
selector: 'my-image-upload',
|
||||
styleUrls: [ './image-upload.component.scss' ],
|
||||
templateUrl: './image-upload.component.html',
|
||||
selector: 'my-preview-upload',
|
||||
styleUrls: [ './preview-upload.component.scss' ],
|
||||
templateUrl: './preview-upload.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => ImageUploadComponent),
|
||||
useExisting: forwardRef(() => PreviewUploadComponent),
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class ImageUploadComponent implements ControlValueAccessor {
|
||||
export class PreviewUploadComponent implements OnInit, ControlValueAccessor {
|
||||
@Input() inputLabel: string
|
||||
@Input() inputName: string
|
||||
@Input() previewWidth: string
|
||||
@Input() previewHeight: string
|
||||
|
||||
imageSrc: SafeResourceUrl
|
||||
allowedExtensionsMessage = ''
|
||||
|
||||
private file: File
|
||||
|
||||
|
@ -38,6 +39,10 @@ export class ImageUploadComponent implements ControlValueAccessor {
|
|||
return this.serverService.getConfig().video.image.size.max
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.allowedExtensionsMessage = this.videoImageExtensions.join(', ')
|
||||
}
|
||||
|
||||
onFileChanged (file: File) {
|
||||
this.file = file
|
||||
|
|
@ -69,7 +69,7 @@ import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/sha
|
|||
import { ConfirmComponent } from '@app/shared/confirm/confirm.component'
|
||||
import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
|
||||
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 { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component'
|
||||
import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component'
|
||||
|
@ -154,7 +154,7 @@ import { ClipboardModule } from 'ngx-clipboard'
|
|||
ConfirmComponent,
|
||||
|
||||
GlobalIconComponent,
|
||||
ImageUploadComponent
|
||||
PreviewUploadComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
|
@ -218,7 +218,7 @@ import { ClipboardModule } from 'ngx-clipboard'
|
|||
ConfirmComponent,
|
||||
|
||||
GlobalIconComponent,
|
||||
ImageUploadComponent,
|
||||
PreviewUploadComponent,
|
||||
|
||||
NumberFormatterPipe,
|
||||
ObjectLengthPipe,
|
||||
|
|
|
@ -85,6 +85,11 @@ export class VideoEdit implements VideoUpdate {
|
|||
const originallyPublishedAt = new Date(values['originallyPublishedAt'])
|
||||
this.originallyPublishedAt = originallyPublishedAt.toISOString()
|
||||
}
|
||||
|
||||
// Use the same file than the preview for the thumbnail
|
||||
if (this.previewfile) {
|
||||
this.thumbnailfile = this.previewfile
|
||||
}
|
||||
}
|
||||
|
||||
toFormPatch () {
|
||||
|
|
|
@ -187,18 +187,14 @@
|
|||
<ng-template ngbTabContent>
|
||||
<div class="row advanced-settings">
|
||||
<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">
|
||||
<my-image-upload
|
||||
i18n-inputLabel inputLabel="Upload preview" inputName="previewfile" formControlName="previewfile"
|
||||
<label i18n for="previewfile">Video preview</label>
|
||||
|
||||
<my-preview-upload
|
||||
i18n-inputLabel inputLabel="Edit" inputName="previewfile" formControlName="previewfile"
|
||||
previewWidth="360px" previewHeight="200px"
|
||||
></my-image-upload>
|
||||
></my-preview-upload>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
|
|
@ -100,7 +100,6 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
|||
language: this.videoValidatorsService.VIDEO_LANGUAGE,
|
||||
description: this.videoValidatorsService.VIDEO_DESCRIPTION,
|
||||
tags: null,
|
||||
thumbnailfile: null,
|
||||
previewfile: null,
|
||||
support: this.videoValidatorsService.VIDEO_SUPPORT,
|
||||
schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT,
|
||||
|
|
|
@ -26,6 +26,27 @@
|
|||
</select>
|
||||
</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>
|
||||
|
||||
|
|
|
@ -1,9 +1,20 @@
|
|||
@import 'variables';
|
||||
@import 'mixins';
|
||||
|
||||
.first-step-block .form-group-channel {
|
||||
margin-bottom: 20px;
|
||||
margin-top: 35px;
|
||||
.first-step-block {
|
||||
|
||||
.form-group-channel {
|
||||
margin-bottom: 20px;
|
||||
margin-top: 35px;
|
||||
}
|
||||
|
||||
.audio-image-info {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.audio-preview {
|
||||
margin: 30px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-progress-cancel {
|
||||
|
|
|
@ -35,8 +35,10 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
userVideoQuotaUsed = 0
|
||||
userVideoQuotaUsedDaily = 0
|
||||
|
||||
isUploadingAudioFile = false
|
||||
isUploadingVideo = false
|
||||
isUpdatingVideo = false
|
||||
|
||||
videoUploaded = false
|
||||
videoUploadObservable: Subscription = null
|
||||
videoUploadPercents = 0
|
||||
|
@ -44,7 +46,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
id: 0,
|
||||
uuid: ''
|
||||
}
|
||||
|
||||
waitTranscodingEnabled = true
|
||||
previewfileUpload: File
|
||||
|
||||
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 () {
|
||||
this.uploadFirstStep()
|
||||
}
|
||||
|
@ -114,38 +129,15 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
}
|
||||
}
|
||||
|
||||
uploadFirstStep () {
|
||||
const videofile = this.videofileInput.nativeElement.files[0]
|
||||
uploadFirstStep (clickedOnButton = false) {
|
||||
const videofile = this.getVideoFile()
|
||||
if (!videofile) return
|
||||
|
||||
// Check global user quota
|
||||
const bytePipes = new BytesPipe()
|
||||
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
|
||||
}
|
||||
if (!this.checkGlobalUserQuota(videofile)) return
|
||||
if (!this.checkDailyUserQuota(videofile)) return
|
||||
|
||||
// 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)
|
||||
if (clickedOnButton === false && this.isAudioFile(videofile.name)) {
|
||||
this.isUploadingAudioFile = true
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -180,6 +172,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
formData.append('channelId', '' + channelId)
|
||||
formData.append('videofile', videofile)
|
||||
|
||||
if (this.previewfileUpload) {
|
||||
formData.append('previewfile', this.previewfileUpload)
|
||||
formData.append('thumbnailfile', this.previewfileUpload)
|
||||
}
|
||||
|
||||
this.isUploadingVideo = true
|
||||
this.firstStepDone.emit(name)
|
||||
|
||||
|
@ -187,7 +184,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
name,
|
||||
privacy,
|
||||
nsfw,
|
||||
channelId
|
||||
channelId,
|
||||
previewfile: this.previewfileUpload
|
||||
})
|
||||
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue