</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>
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 {
.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
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
<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"
/>
</div>
- <div i18n class="file-constraints">(extensions: {{ allowedExtensionsMessage }}, max size: {{ maxFileSize | bytes }})</div>
-
<div class="filename" *ngIf="displayFilename === true && filename">{{ filename }}</div>
</div>
.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 {
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',
@Input() extensions: string[] = []
@Input() maxFileSize: number
@Input() displayFilename = false
+ @Input() icon: GlobalIconName
@Output() fileChanged = new EventEmitter<Blob>()
+++ /dev/null
-<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>
+++ /dev/null
-@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;
- }
- }
-}
+++ /dev/null
-import { Component, forwardRef, Input } 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',
- providers: [
- {
- provide: NG_VALUE_ACCESSOR,
- useExisting: forwardRef(() => ImageUploadComponent),
- multi: true
- }
- ]
-})
-export class ImageUploadComponent implements ControlValueAccessor {
- @Input() inputLabel: string
- @Input() inputName: string
- @Input() previewWidth: string
- @Input() previewHeight: string
-
- imageSrc: SafeResourceUrl
-
- private file: File
-
- constructor (
- private sanitizer: DomSanitizer,
- private serverService: ServerService
- ) {}
-
- get videoImageExtensions () {
- return this.serverService.getConfig().video.image.extensions
- }
-
- get maxVideoImageSize () {
- return this.serverService.getConfig().video.image.size.max
- }
-
- onFileChanged (file: File) {
- this.file = file
-
- this.propagateChange(this.file)
- this.updatePreview()
- }
-
- propagateChange = (_: any) => { /* empty */ }
-
- writeValue (file: any) {
- this.file = file
- this.updatePreview()
- }
-
- registerOnChange (fn: (_: any) => void) {
- this.propagateChange = fn
- }
-
- registerOnTouched () {
- // Unused
- }
-
- private updatePreview () {
- if (this.file) {
- const url = URL.createObjectURL(this.file)
- this.imageSrc = this.sanitizer.bypassSecurityTrustResourceUrl(url)
- }
- }
-}
--- /dev/null
+<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>
--- /dev/null
+@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;
+ }
+ }
+ }
+}
--- /dev/null
+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-preview-upload',
+ styleUrls: [ './preview-upload.component.scss' ],
+ templateUrl: './preview-upload.component.html',
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => PreviewUploadComponent),
+ multi: true
+ }
+ ]
+})
+export class PreviewUploadComponent implements OnInit, ControlValueAccessor {
+ @Input() inputLabel: string
+ @Input() inputName: string
+ @Input() previewWidth: string
+ @Input() previewHeight: string
+
+ imageSrc: SafeResourceUrl
+ allowedExtensionsMessage = ''
+
+ private file: File
+
+ constructor (
+ private sanitizer: DomSanitizer,
+ private serverService: ServerService
+ ) {}
+
+ get videoImageExtensions () {
+ return this.serverService.getConfig().video.image.extensions
+ }
+
+ get maxVideoImageSize () {
+ return this.serverService.getConfig().video.image.size.max
+ }
+
+ ngOnInit () {
+ this.allowedExtensionsMessage = this.videoImageExtensions.join(', ')
+ }
+
+ onFileChanged (file: File) {
+ this.file = file
+
+ this.propagateChange(this.file)
+ this.updatePreview()
+ }
+
+ propagateChange = (_: any) => { /* empty */ }
+
+ writeValue (file: any) {
+ this.file = file
+ this.updatePreview()
+ }
+
+ registerOnChange (fn: (_: any) => void) {
+ this.propagateChange = fn
+ }
+
+ registerOnTouched () {
+ // Unused
+ }
+
+ private updatePreview () {
+ if (this.file) {
+ const url = URL.createObjectURL(this.file)
+ this.imageSrc = this.sanitizer.bypassSecurityTrustResourceUrl(url)
+ }
+ }
+}
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'
ConfirmComponent,
GlobalIconComponent,
- ImageUploadComponent
+ PreviewUploadComponent
],
exports: [
ConfirmComponent,
GlobalIconComponent,
- ImageUploadComponent,
+ PreviewUploadComponent,
NumberFormatterPipe,
ObjectLengthPipe,
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 () {
<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">
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,
</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>
@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 {
userVideoQuotaUsed = 0
userVideoQuotaUsedDaily = 0
+ isUploadingAudioFile = false
isUploadingVideo = false
isUpdatingVideo = false
+
videoUploaded = false
videoUploadObservable: Subscription = null
videoUploadPercents = 0
id: 0,
uuid: ''
}
+
waitTranscodingEnabled = true
+ previewfileUpload: File
error: string
}
}
+ 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()
}
}
}
- 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
}
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)
name,
privacy,
nsfw,
- channelId
+ channelId,
+ previewfile: this.previewfileUpload
})
this.explainedVideoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies)
}
)
}
+
+ 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')
+ }
}