From 40e87e9ecc54e3513fb586928330a7855eb192c6 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 12 Jul 2018 19:02:00 +0200 Subject: Implement captions/subtitles --- .../edit-custom-config.component.html | 21 ++++-- .../edit-custom-config.component.ts | 4 ++ client/src/app/core/server/server.service.ts | 6 ++ .../custom-config-validators.service.ts | 10 +++ .../src/app/shared/forms/form-validators/index.ts | 1 + .../video-captions-validators.service.ts | 27 ++++++++ client/src/app/shared/forms/index.ts | 1 + .../app/shared/forms/reactive-file.component.html | 14 ++++ .../app/shared/forms/reactive-file.component.scss | 24 +++++++ .../app/shared/forms/reactive-file.component.ts | 75 ++++++++++++++++++++ client/src/app/shared/misc/utils.ts | 10 ++- client/src/app/shared/shared.module.ts | 10 ++- client/src/app/shared/video-caption/index.ts | 1 + .../video-caption/video-caption-edit.model.ts | 9 +++ .../shared/video-caption/video-caption.service.ts | 61 +++++++++++++++++ client/src/app/shared/video/video.model.ts | 2 +- client/src/app/shared/video/video.service.ts | 4 +- .../shared/video-caption-add-modal.component.html | 47 +++++++++++++ .../shared/video-caption-add-modal.component.scss | 15 ++++ .../shared/video-caption-add-modal.component.ts | 80 ++++++++++++++++++++++ .../+video-edit/shared/video-edit.component.html | 32 ++++++++- .../+video-edit/shared/video-edit.component.scss | 35 ++++++++++ .../+video-edit/shared/video-edit.component.ts | 53 ++++++++++++-- .../videos/+video-edit/shared/video-edit.module.ts | 4 +- .../+video-edit/shared/video-image.component.html | 15 ++-- .../+video-edit/shared/video-image.component.scss | 10 --- .../+video-edit/shared/video-image.component.ts | 26 ++----- .../videos/+video-edit/video-add.component.html | 2 +- .../app/videos/+video-edit/video-add.component.ts | 50 +++++++------- .../videos/+video-edit/video-update.component.html | 1 + .../videos/+video-edit/video-update.component.ts | 48 +++++++++---- 31 files changed, 602 insertions(+), 96 deletions(-) create mode 100644 client/src/app/shared/forms/form-validators/video-captions-validators.service.ts create mode 100644 client/src/app/shared/forms/reactive-file.component.html create mode 100644 client/src/app/shared/forms/reactive-file.component.scss create mode 100644 client/src/app/shared/forms/reactive-file.component.ts create mode 100644 client/src/app/shared/video-caption/index.ts create mode 100644 client/src/app/shared/video-caption/video-caption-edit.model.ts create mode 100644 client/src/app/shared/video-caption/video-caption.service.ts create mode 100644 client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html create mode 100644 client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.scss create mode 100644 client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts (limited to 'client/src/app') diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index 1e5308531..97900e523 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html @@ -206,15 +206,17 @@ Check this checkbox, save the configuration and test with a video URL of your in -
Cache
+
+ Cache -
- +
+
+
+
+ + +
+ {{ formErrors.cacheCaptionsSize }} +
+
+
Customizations
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index 7b3e72803..8d476393f 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts @@ -67,6 +67,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { servicesTwitterUsername: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME, servicesTwitterWhitelisted: null, cachePreviewsSize: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE, + cacheCaptionsSize: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE, signupEnabled: null, signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT, adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL, @@ -156,6 +157,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { cache: { previews: { size: this.form.value['cachePreviewsSize'] + }, + captions: { + size: this.form.value['cacheCaptionsSize'] } }, signup: { diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 74363e6a1..3baefb6a7 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts @@ -59,6 +59,12 @@ export class ServerService { extensions: [] } }, + videoCaption: { + file: { + size: { max: 0 }, + extensions: [] + } + }, user: { videoQuota: -1 } diff --git a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts index 1b36bbc6b..0c2489a9d 100644 --- a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts +++ b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts @@ -9,6 +9,7 @@ export class CustomConfigValidatorsService { readonly INSTANCE_SHORT_DESCRIPTION: BuildFormValidator readonly SERVICES_TWITTER_USERNAME: BuildFormValidator readonly CACHE_PREVIEWS_SIZE: BuildFormValidator + readonly CACHE_CAPTIONS_SIZE: BuildFormValidator readonly SIGNUP_LIMIT: BuildFormValidator readonly ADMIN_EMAIL: BuildFormValidator readonly TRANSCODING_THREADS: BuildFormValidator @@ -44,6 +45,15 @@ export class CustomConfigValidatorsService { } } + this.CACHE_CAPTIONS_SIZE = { + VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ], + MESSAGES: { + 'required': this.i18n('Captions cache size is required.'), + 'min': this.i18n('Captions cache size must be greater than 1.'), + 'pattern': this.i18n('Captions cache size must be a number.') + } + } + this.SIGNUP_LIMIT = { VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ], MESSAGES: { diff --git a/client/src/app/shared/forms/form-validators/index.ts b/client/src/app/shared/forms/form-validators/index.ts index 487683088..60d735ef7 100644 --- a/client/src/app/shared/forms/form-validators/index.ts +++ b/client/src/app/shared/forms/form-validators/index.ts @@ -8,3 +8,4 @@ export * from './video-abuse-validators.service' export * from './video-channel-validators.service' export * from './video-comment-validators.service' export * from './video-validators.service' +export * from './video-captions-validators.service' diff --git a/client/src/app/shared/forms/form-validators/video-captions-validators.service.ts b/client/src/app/shared/forms/form-validators/video-captions-validators.service.ts new file mode 100644 index 000000000..d1b4667bb --- /dev/null +++ b/client/src/app/shared/forms/form-validators/video-captions-validators.service.ts @@ -0,0 +1,27 @@ +import { I18n } from '@ngx-translate/i18n-polyfill' +import { Validators } from '@angular/forms' +import { Injectable } from '@angular/core' +import { BuildFormValidator } from '@app/shared' + +@Injectable() +export class VideoCaptionsValidatorsService { + readonly VIDEO_CAPTION_LANGUAGE: BuildFormValidator + readonly VIDEO_CAPTION_FILE: BuildFormValidator + + constructor (private i18n: I18n) { + + this.VIDEO_CAPTION_LANGUAGE = { + VALIDATORS: [ Validators.required ], + MESSAGES: { + 'required': this.i18n('Video caption language is required.') + } + } + + this.VIDEO_CAPTION_FILE = { + VALIDATORS: [ Validators.required ], + MESSAGES: { + 'required': this.i18n('Video caption file is required.') + } + } + } +} diff --git a/client/src/app/shared/forms/index.ts b/client/src/app/shared/forms/index.ts index 7464bb022..41c321c4c 100644 --- a/client/src/app/shared/forms/index.ts +++ b/client/src/app/shared/forms/index.ts @@ -1,2 +1,3 @@ export * from './form-validators' export * from './form-reactive' +export * from './reactive-file.component' diff --git a/client/src/app/shared/forms/reactive-file.component.html b/client/src/app/shared/forms/reactive-file.component.html new file mode 100644 index 000000000..9fb1c9e3e --- /dev/null +++ b/client/src/app/shared/forms/reactive-file.component.html @@ -0,0 +1,14 @@ +
+
+ {{ inputLabel }} + +
+ +
(extensions: {{ allowedExtensionsMessage }}, max size: {{ maxFileSize | bytes }})
+ +
{{ filename }}
+
diff --git a/client/src/app/shared/forms/reactive-file.component.scss b/client/src/app/shared/forms/reactive-file.component.scss new file mode 100644 index 000000000..d89844264 --- /dev/null +++ b/client/src/app/shared/forms/reactive-file.component.scss @@ -0,0 +1,24 @@ +@import '_variables'; +@import '_mixins'; + +.root { + height: auto; + display: flex; + align-items: center; + + .button-file { + @include peertube-button-file(auto); + + min-width: 190px; + } + + .file-constraints { + margin-left: 5px; + font-size: 13px; + } + + .filename { + font-weight: $font-semibold; + margin-left: 5px; + } +} diff --git a/client/src/app/shared/forms/reactive-file.component.ts b/client/src/app/shared/forms/reactive-file.component.ts new file mode 100644 index 000000000..f5758b643 --- /dev/null +++ b/client/src/app/shared/forms/reactive-file.component.ts @@ -0,0 +1,75 @@ +import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core' +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' +import { NotificationsService } from 'angular2-notifications' +import { I18n } from '@ngx-translate/i18n-polyfill' + +@Component({ + selector: 'my-reactive-file', + styleUrls: [ './reactive-file.component.scss' ], + templateUrl: './reactive-file.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ReactiveFileComponent), + multi: true + } + ] +}) +export class ReactiveFileComponent implements OnInit, ControlValueAccessor { + @Input() inputLabel: string + @Input() inputName: string + @Input() extensions: string[] = [] + @Input() maxFileSize: number + @Input() displayFilename = false + + @Output() fileChanged = new EventEmitter() + + allowedExtensionsMessage = '' + + private file: File + + constructor ( + private notificationsService: NotificationsService, + private i18n: I18n + ) {} + + get filename () { + if (!this.file) return '' + + return this.file.name + } + + ngOnInit () { + this.allowedExtensionsMessage = this.extensions.join(', ') + } + + fileChange (event: any) { + if (event.target.files && event.target.files.length) { + const [ file ] = event.target.files + + if (file.size > this.maxFileSize) { + this.notificationsService.error(this.i18n('Error'), this.i18n('This file is too large.')) + return + } + + this.file = file + + this.propagateChange(this.file) + this.fileChanged.emit(this.file) + } + } + + propagateChange = (_: any) => { /* empty */ } + + writeValue (file: any) { + this.file = file + } + + registerOnChange (fn: (_: any) => void) { + this.propagateChange = fn + } + + registerOnTouched () { + // Unused + } +} diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts index 53aff1b24..8381745f5 100644 --- a/client/src/app/shared/misc/utils.ts +++ b/client/src/app/shared/misc/utils.ts @@ -81,7 +81,7 @@ function objectToFormData (obj: any, form?: FormData, namespace?: string) { } if (obj[key] !== null && typeof obj[ key ] === 'object' && !(obj[ key ] instanceof File)) { - objectToFormData(obj[ key ], fd, key) + objectToFormData(obj[ key ], fd, formKey) } else { fd.append(formKey, obj[ key ]) } @@ -96,6 +96,11 @@ function lineFeedToHtml (obj: object, keyToNormalize: string) { }) } +function removeElementFromArray (arr: T[], elem: T) { + const index = arr.indexOf(elem) + if (index !== -1) arr.splice(index, 1) +} + export { objectToUrlEncoded, getParameterByName, @@ -104,5 +109,6 @@ export { dateToHuman, immutableAssign, objectToFormData, - lineFeedToHtml + lineFeedToHtml, + removeElementFromArray } diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 97e49e7ab..c3f4bf88b 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -37,12 +37,14 @@ import { I18n } from '@ngx-translate/i18n-polyfill' import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' import { CustomConfigValidatorsService, - LoginValidatorsService, + LoginValidatorsService, ReactiveFileComponent, ResetPasswordValidatorsService, UserValidatorsService, VideoAbuseValidatorsService, VideoChannelValidatorsService, VideoCommentValidatorsService, VideoValidatorsService } from '@app/shared/forms' import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar' import { ScreenService } from '@app/shared/misc/screen.service' +import { VideoCaptionsValidatorsService } from '@app/shared/forms/form-validators/video-captions-validators.service' +import { VideoCaptionService } from '@app/shared/video-caption' @NgModule({ imports: [ @@ -74,7 +76,8 @@ import { ScreenService } from '@app/shared/misc/screen.service' FromNowPipe, MarkdownTextareaComponent, InfiniteScrollerDirective, - HelpComponent + HelpComponent, + ReactiveFileComponent ], exports: [ @@ -102,6 +105,7 @@ import { ScreenService } from '@app/shared/misc/screen.service' MarkdownTextareaComponent, InfiniteScrollerDirective, HelpComponent, + ReactiveFileComponent, NumberFormatterPipe, ObjectLengthPipe, @@ -119,6 +123,7 @@ import { ScreenService } from '@app/shared/misc/screen.service' AccountService, MarkdownService, VideoChannelService, + VideoCaptionService, FormValidatorService, CustomConfigValidatorsService, @@ -129,6 +134,7 @@ import { ScreenService } from '@app/shared/misc/screen.service' VideoChannelValidatorsService, VideoCommentValidatorsService, VideoValidatorsService, + VideoCaptionsValidatorsService, I18nPrimengCalendarService, ScreenService, diff --git a/client/src/app/shared/video-caption/index.ts b/client/src/app/shared/video-caption/index.ts new file mode 100644 index 000000000..c48a70558 --- /dev/null +++ b/client/src/app/shared/video-caption/index.ts @@ -0,0 +1 @@ +export * from './video-caption.service' diff --git a/client/src/app/shared/video-caption/video-caption-edit.model.ts b/client/src/app/shared/video-caption/video-caption-edit.model.ts new file mode 100644 index 000000000..732f20158 --- /dev/null +++ b/client/src/app/shared/video-caption/video-caption-edit.model.ts @@ -0,0 +1,9 @@ +export interface VideoCaptionEdit { + language: { + id: string + label?: string + } + + action?: 'CREATE' | 'REMOVE' + captionfile?: any +} diff --git a/client/src/app/shared/video-caption/video-caption.service.ts b/client/src/app/shared/video-caption/video-caption.service.ts new file mode 100644 index 000000000..4ae8ebd0a --- /dev/null +++ b/client/src/app/shared/video-caption/video-caption.service.ts @@ -0,0 +1,61 @@ +import { catchError, map } from 'rxjs/operators' +import { HttpClient } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { forkJoin, Observable } from 'rxjs' +import { ResultList } from '../../../../../shared' +import { RestExtractor, RestService } from '../rest' +import { VideoCaption } from '../../../../../shared/models/videos/video-caption.model' +import { VideoService } from '@app/shared/video/video.service' +import { objectToFormData } from '@app/shared/misc/utils' +import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' + +@Injectable() +export class VideoCaptionService { + constructor ( + private authHttp: HttpClient, + private restService: RestService, + private restExtractor: RestExtractor + ) {} + + listCaptions (videoId: number | string): Observable> { + return this.authHttp.get>(VideoService.BASE_VIDEO_URL + videoId + '/captions') + .pipe(catchError(res => this.restExtractor.handleError(res))) + } + + removeCaption (videoId: number | string, language: string) { + return this.authHttp.delete(VideoService.BASE_VIDEO_URL + videoId + '/captions/' + language) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(res => this.restExtractor.handleError(res)) + ) + } + + addCaption (videoId: number | string, language: string, captionfile: File) { + const body = { captionfile } + const data = objectToFormData(body) + + return this.authHttp.put(VideoService.BASE_VIDEO_URL + videoId + '/captions/' + language, data) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(res => this.restExtractor.handleError(res)) + ) + } + + updateCaptions (videoId: number | string, videoCaptions: VideoCaptionEdit[]) { + const observables: Observable[] = [] + + for (const videoCaption of videoCaptions) { + if (videoCaption.action === 'CREATE') { + observables.push( + this.addCaption(videoId, videoCaption.language.id, videoCaption.captionfile) + ) + } else if (videoCaption.action === 'REMOVE') { + observables.push( + this.removeCaption(videoId, videoCaption.language.id) + ) + } + } + + return forkJoin(observables) + } +} diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index 5c820a227..6b1a299ea 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts @@ -1,7 +1,7 @@ import { User } from '../' import { Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared' import { Avatar } from '../../../../../shared/models/avatars/avatar.model' -import { VideoConstant } from '../../../../../shared/models/videos/video.model' +import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model' import { getAbsoluteAPIUrl } from '../misc/utils' import { ServerConfig } from '../../../../../shared/models' import { Actor } from '@app/shared/actor/actor.model' diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index 9498a06fe..b4c1f10f9 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts @@ -28,8 +28,8 @@ import { ServerService } from '@app/core' @Injectable() export class VideoService { - private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' - private static BASE_FEEDS_URL = environment.apiUrl + '/feeds/videos.' + static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' + static BASE_FEEDS_URL = environment.apiUrl + '/feeds/videos.' constructor ( private authHttp: HttpClient, diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html new file mode 100644 index 000000000..9cd303b29 --- /dev/null +++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html @@ -0,0 +1,47 @@ + diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.scss b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.scss new file mode 100644 index 000000000..c6da1877e --- /dev/null +++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.scss @@ -0,0 +1,15 @@ +@import '_variables'; +@import '_mixins'; + +.peertube-select-container { + @include peertube-select-container(auto); +} + +.caption-file { + margin-top: 20px; +} + +.warning-replace-caption { + color: red; + margin-top: 10px; +} \ No newline at end of file diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts new file mode 100644 index 000000000..45b8c71f8 --- /dev/null +++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts @@ -0,0 +1,80 @@ +import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' +import { ModalDirective } from 'ngx-bootstrap/modal' +import { FormReactive } from '@app/shared' +import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' +import { VideoCaptionsValidatorsService } from '@app/shared/forms/form-validators/video-captions-validators.service' +import { ServerService } from '@app/core' +import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' + +@Component({ + selector: 'my-video-caption-add-modal', + styleUrls: [ './video-caption-add-modal.component.scss' ], + templateUrl: './video-caption-add-modal.component.html' +}) + +export class VideoCaptionAddModalComponent extends FormReactive implements OnInit { + @Input() existingCaptions: string[] + + @Output() captionAdded = new EventEmitter() + + @ViewChild('modal') modal: ModalDirective + + videoCaptionLanguages = [] + + private closingModal = false + + constructor ( + protected formValidatorService: FormValidatorService, + private serverService: ServerService, + private videoCaptionsValidatorsService: VideoCaptionsValidatorsService + ) { + super() + } + + get videoCaptionExtensions () { + return this.serverService.getConfig().videoCaption.file.extensions + } + + get videoCaptionMaxSize () { + return this.serverService.getConfig().videoCaption.file.size.max + } + + ngOnInit () { + this.videoCaptionLanguages = this.serverService.getVideoLanguages() + + this.buildForm({ + language: this.videoCaptionsValidatorsService.VIDEO_CAPTION_LANGUAGE, + captionfile: this.videoCaptionsValidatorsService.VIDEO_CAPTION_FILE + }) + } + + show () { + this.modal.show() + } + + hide () { + this.modal.hide() + } + + isReplacingExistingCaption () { + if (this.closingModal === true) return false + + const languageId = this.form.value[ 'language' ] + + return languageId && this.existingCaptions.indexOf(languageId) !== -1 + } + + async addCaption () { + this.closingModal = true + + const languageId = this.form.value[ 'language' ] + const languageObject = this.videoCaptionLanguages.find(l => l.id === languageId) + + this.captionAdded.emit({ + language: languageObject, + captionfile: this.form.value['captionfile'] + }) + + this.hide() + } +} diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html index 447c5ab9b..14d5f3614 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html @@ -132,13 +132,39 @@
+ +
+ + + +
+ +
+ + + Delete +
+
+ +
+ No captions for now. +
+ +
+
+
@@ -172,3 +198,7 @@
+ + \ No newline at end of file diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.scss b/client/src/app/videos/+video-edit/shared/video-edit.component.scss index 061eca4a7..03b8359de 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.scss +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.scss @@ -7,6 +7,7 @@ .video-edit { height: 100%; + min-height: 300px; .form-group { margin-bottom: 25px; @@ -49,6 +50,40 @@ } } +.captions { + + .captions-header { + text-align: right; + + .create-caption { + @include create-button('../../../../assets/images/global/add.svg'); + } + } + + .caption-entry { + display: flex; + height: 40px; + align-items: center; + + .caption-entry-label { + font-size: 15px; + font-weight: bold; + + margin-right: 20px; + } + + .caption-entry-delete { + @include peertube-button; + @include grey-button; + } + } + + .no-caption { + text-align: center; + font-size: 15px; + } +} + .submit-container { text-align: right; diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.ts b/client/src/app/videos/+video-edit/shared/video-edit.component.ts index 66eb6611a..9394d7dab 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.ts +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts @@ -1,5 +1,5 @@ -import { Component, Input, OnInit } from '@angular/core' -import { FormGroup, ValidatorFn, Validators } from '@angular/forms' +import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms' import { ActivatedRoute, Router } from '@angular/router' import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared' import { NotificationsService } from 'angular2-notifications' @@ -8,6 +8,10 @@ import { VideoEdit } from '../../../shared/video/video-edit.model' import { map } from 'rxjs/operators' import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar' +import { VideoCaptionService } from '@app/shared/video-caption' +import { VideoCaptionAddModalComponent } from '@app/videos/+video-edit/shared/video-caption-add-modal.component' +import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' +import { removeElementFromArray } from '@app/shared/misc/utils' @Component({ selector: 'my-video-edit', @@ -15,13 +19,16 @@ import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calend templateUrl: './video-edit.component.html' }) -export class VideoEditComponent implements OnInit { +export class VideoEditComponent implements OnInit, OnDestroy { @Input() form: FormGroup @Input() formErrors: { [ id: string ]: string } = {} @Input() validationMessages: FormReactiveValidationMessages = {} @Input() videoPrivacies = [] @Input() userVideoChannels: { id: number, label: string, support: string }[] = [] @Input() schedulePublicationPossible = true + @Input() videoCaptions: VideoCaptionEdit[] = [] + + @ViewChild('videoCaptionAddModal') videoCaptionAddModal: VideoCaptionAddModalComponent // So that it can be accessed in the template readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY @@ -41,9 +48,12 @@ export class VideoEditComponent implements OnInit { calendarTimezone: string calendarDateFormat: string + private schedulerInterval + constructor ( private formValidatorService: FormValidatorService, private videoValidatorsService: VideoValidatorsService, + private videoCaptionService: VideoCaptionService, private route: ActivatedRoute, private router: Router, private notificationsService: NotificationsService, @@ -91,6 +101,13 @@ export class VideoEditComponent implements OnInit { defaultValues ) + this.form.addControl('captions', new FormArray([ + new FormGroup({ + language: new FormControl(), + captionfile: new FormControl() + }) + ])) + this.trackChannelChange() this.trackPrivacyChange() } @@ -102,7 +119,35 @@ export class VideoEditComponent implements OnInit { this.videoLicences = this.serverService.getVideoLicences() this.videoLanguages = this.serverService.getVideoLanguages() - setTimeout(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute + this.schedulerInterval = setInterval(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute + } + + ngOnDestroy () { + if (this.schedulerInterval) clearInterval(this.schedulerInterval) + } + + getExistingCaptions () { + return this.videoCaptions.map(c => c.language.id) + } + + onCaptionAdded (caption: VideoCaptionEdit) { + this.videoCaptions.push( + Object.assign(caption, { action: 'CREATE' as 'CREATE' }) + ) + } + + deleteCaption (caption: VideoCaptionEdit) { + // This caption is not on the server, just remove it from our array + if (caption.action === 'CREATE') { + removeElementFromArray(this.videoCaptions, caption) + return + } + + caption.action = 'REMOVE' as 'REMOVE' + } + + openAddCaptionModal () { + this.videoCaptionAddModal.show() } private trackPrivacyChange () { diff --git a/client/src/app/videos/+video-edit/shared/video-edit.module.ts b/client/src/app/videos/+video-edit/shared/video-edit.module.ts index 6bf3e34b1..f6bd65fdc 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.module.ts +++ b/client/src/app/videos/+video-edit/shared/video-edit.module.ts @@ -5,6 +5,7 @@ import { SharedModule } from '../../../shared/' import { VideoEditComponent } from './video-edit.component' import { VideoImageComponent } from './video-image.component' import { CalendarModule } from 'primeng/components/calendar/calendar' +import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' @NgModule({ imports: [ @@ -16,7 +17,8 @@ import { CalendarModule } from 'primeng/components/calendar/calendar' declarations: [ VideoEditComponent, - VideoImageComponent + VideoImageComponent, + VideoCaptionAddModalComponent ], exports: [ diff --git a/client/src/app/videos/+video-edit/shared/video-image.component.html b/client/src/app/videos/+video-edit/shared/video-image.component.html index e319d7ee7..c09c862c4 100644 --- a/client/src/app/videos/+video-edit/shared/video-image.component.html +++ b/client/src/app/videos/+video-edit/shared/video-image.component.html @@ -1,15 +1,8 @@
-
-
- {{ inputLabel }} - -
-
(extensions: {{ videoImageExtensions }}, max size: {{ maxVideoImageSize | bytes }})
-
+
diff --git a/client/src/app/videos/+video-edit/shared/video-image.component.scss b/client/src/app/videos/+video-edit/shared/video-image.component.scss index d4901e7ab..b63963bca 100644 --- a/client/src/app/videos/+video-edit/shared/video-image.component.scss +++ b/client/src/app/videos/+video-edit/shared/video-image.component.scss @@ -6,16 +6,6 @@ display: flex; align-items: center; - .button-file { - @include peertube-button-file(auto); - - min-width: 190px; - } - - .image-constraints { - font-size: 13px; - } - .preview { border: 2px solid grey; border-radius: 4px; diff --git a/client/src/app/videos/+video-edit/shared/video-image.component.ts b/client/src/app/videos/+video-edit/shared/video-image.component.ts index 25955baaa..a604cde90 100644 --- a/client/src/app/videos/+video-edit/shared/video-image.component.ts +++ b/client/src/app/videos/+video-edit/shared/video-image.component.ts @@ -2,8 +2,6 @@ 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' -import { NotificationsService } from 'angular2-notifications' -import { I18n } from '@ngx-translate/i18n-polyfill' @Component({ selector: 'my-video-image', @@ -25,36 +23,26 @@ export class VideoImageComponent implements ControlValueAccessor { imageSrc: SafeResourceUrl - private file: Blob + private file: File constructor ( private sanitizer: DomSanitizer, - private serverService: ServerService, - private notificationsService: NotificationsService, - private i18n: I18n + private serverService: ServerService ) {} get videoImageExtensions () { - return this.serverService.getConfig().video.image.extensions.join(',') + return this.serverService.getConfig().video.image.extensions } get maxVideoImageSize () { return this.serverService.getConfig().video.image.size.max } - fileChange (event: any) { - if (event.target.files && event.target.files.length) { - const [ file ] = event.target.files - - if (file.size > this.maxVideoImageSize) { - this.notificationsService.error(this.i18n('Error'), this.i18n('This image is too large.')) - return - } + onFileChanged (file: File) { + this.file = file - this.file = file - this.propagateChange(this.file) - this.updatePreview() - } + this.propagateChange(this.file) + this.updatePreview() } propagateChange = (_: any) => { /* empty */ } diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html index 7d9443209..9c2c01c65 100644 --- a/client/src/app/videos/+video-edit/video-add.component.html +++ b/client/src/app/videos/+video-edit/video-add.component.html @@ -46,7 +46,7 @@
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts index 7c4b6260b..8c30cedfb 100644 --- a/client/src/app/videos/+video-edit/video-add.component.ts +++ b/client/src/app/videos/+video-edit/video-add.component.ts @@ -15,6 +15,8 @@ import { VideoEdit } from '../../shared/video/video-edit.model' import { VideoService } from '../../shared/video/video.service' import { I18n } from '@ngx-translate/i18n-polyfill' import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' +import { switchMap } from 'rxjs/operators' +import { VideoCaptionService } from '@app/shared/video-caption' @Component({ selector: 'my-videos-add', @@ -46,6 +48,7 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy videoPrivacies = [] firstStepPrivacyId = 0 firstStepChannelId = 0 + videoCaptions = [] constructor ( protected formValidatorService: FormValidatorService, @@ -56,7 +59,8 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy private serverService: ServerService, private videoService: VideoService, private loadingBar: LoadingBarService, - private i18n: I18n + private i18n: I18n, + private videoCaptionService: VideoCaptionService ) { super() } @@ -159,11 +163,8 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy let name: string // If the name of the file is very small, keep the extension - if (nameWithoutExtension.length < 3) { - name = videofile.name - } else { - name = nameWithoutExtension - } + if (nameWithoutExtension.length < 3) name = videofile.name + else name = nameWithoutExtension const privacy = this.firstStepPrivacyId.toString() const nsfw = false @@ -225,22 +226,25 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy this.isUpdatingVideo = true this.loadingBar.start() this.videoService.updateVideo(video) - .subscribe( - () => { - this.isUpdatingVideo = false - this.isUploadingVideo = false - this.loadingBar.complete() - - this.notificationsService.success(this.i18n('Success'), this.i18n('Video published.')) - this.router.navigate([ '/videos/watch', video.uuid ]) - }, - - err => { - this.isUpdatingVideo = false - this.notificationsService.error(this.i18n('Error'), err.message) - console.error(err) - } - ) - + .pipe( + // Then update captions + switchMap(() => this.videoCaptionService.updateCaptions(video.id, this.videoCaptions)) + ) + .subscribe( + () => { + this.isUpdatingVideo = false + this.isUploadingVideo = false + this.loadingBar.complete() + + this.notificationsService.success(this.i18n('Success'), this.i18n('Video published.')) + this.router.navigate([ '/videos/watch', video.uuid ]) + }, + + err => { + this.isUpdatingVideo = false + this.notificationsService.error(this.i18n('Error'), err.message) + console.error(err) + } + ) } } diff --git a/client/src/app/videos/+video-edit/video-update.component.html b/client/src/app/videos/+video-edit/video-update.component.html index 5cb16c8ab..9242c30a0 100644 --- a/client/src/app/videos/+video-edit/video-update.component.html +++ b/client/src/app/videos/+video-edit/video-update.component.html @@ -8,6 +8,7 @@
diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts index c4e6f44de..b67874401 100644 --- a/client/src/app/videos/+video-edit/video-update.component.ts +++ b/client/src/app/videos/+video-edit/video-update.component.ts @@ -12,6 +12,7 @@ import { VideoService } from '../../shared/video/video.service' import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' import { I18n } from '@ngx-translate/i18n-polyfill' import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' +import { VideoCaptionService } from '@app/shared/video-caption' @Component({ selector: 'my-videos-update', @@ -25,6 +26,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { videoPrivacies = [] userVideoChannels = [] schedulePublicationPossible = false + videoCaptions = [] constructor ( protected formValidatorService: FormValidatorService, @@ -36,6 +38,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { private authService: AuthService, private loadingBar: LoadingBarService, private videoChannelService: VideoChannelService, + private videoCaptionService: VideoCaptionService, private i18n: I18n ) { super() @@ -63,12 +66,21 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { map(videoChannels => videoChannels.map(c => ({ id: c.id, label: c.displayName, support: c.support }))), map(videoChannels => ({ video, videoChannels })) ) + }), + switchMap(({ video, videoChannels }) => { + return this.videoCaptionService + .listCaptions(video.id) + .pipe( + map(result => result.data), + map(videoCaptions => ({ video, videoChannels, videoCaptions })) + ) }) ) .subscribe( - ({ video, videoChannels }) => { + ({ video, videoChannels, videoCaptions }) => { this.video = new VideoEdit(video) this.userVideoChannels = videoChannels + this.videoCaptions = videoCaptions // We cannot set private a video that was not private if (this.video.privacy !== VideoPrivacy.PRIVATE) { @@ -102,21 +114,27 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { this.loadingBar.start() this.isUpdatingVideo = true + + // Update the video this.videoService.updateVideo(this.video) - .subscribe( - () => { - this.isUpdatingVideo = false - this.loadingBar.complete() - this.notificationsService.success(this.i18n('Success'), this.i18n('Video updated.')) - this.router.navigate([ '/videos/watch', this.video.uuid ]) - }, - - err => { - this.isUpdatingVideo = false - this.notificationsService.error(this.i18n('Error'), err.message) - console.error(err) - } - ) + .pipe( + // Then update captions + switchMap(() => this.videoCaptionService.updateCaptions(this.video.id, this.videoCaptions)) + ) + .subscribe( + () => { + this.isUpdatingVideo = false + this.loadingBar.complete() + this.notificationsService.success(this.i18n('Success'), this.i18n('Video updated.')) + this.router.navigate([ '/videos/watch', this.video.uuid ]) + }, + + err => { + this.isUpdatingVideo = false + this.notificationsService.error(this.i18n('Error'), err.message) + console.error(err) + } + ) } -- cgit v1.2.3