From 40e87e9ecc54e3513fb586928330a7855eb192c6 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 12 Jul 2018 19:02:00 +0200 Subject: Implement captions/subtitles --- .../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 +++++++++---- 14 files changed, 333 insertions(+), 85 deletions(-) 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/videos/+video-edit') 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