Add simple subtitle edition from video captions tab
Introduce a new __Edit__ button on a subtitle. It opens a modal with simple textarea allowing the user to do quick corrections on a subtitle.
This commit is contained in:
parent
e66d0892b1
commit
57d74ec83d
11 changed files with 181 additions and 10 deletions
|
@ -77,7 +77,8 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
|
||||||
|
|
||||||
this.captionAdded.emit({
|
this.captionAdded.emit({
|
||||||
language: languageObject,
|
language: languageObject,
|
||||||
captionfile: this.form.value['captionfile']
|
captionfile: this.form.value['captionfile'],
|
||||||
|
action: 'CREATE'
|
||||||
})
|
})
|
||||||
|
|
||||||
this.hide()
|
this.hide()
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
<ng-template #modal>
|
||||||
|
<ng-container [formGroup]="form">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 i18n class="modal-title">Edit caption</h4>
|
||||||
|
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<label i18n for="captionFileContent">Caption</label>
|
||||||
|
<textarea
|
||||||
|
id="captionFileContent"
|
||||||
|
formControlName="captionFileContent"
|
||||||
|
class="form-control caption-textarea"
|
||||||
|
[ngClass]="{ 'input-error': formErrors['captionFileContent'] }"
|
||||||
|
>
|
||||||
|
</textarea>
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.captionFileContent" class="form-error">
|
||||||
|
{{ formErrors.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer inputs">
|
||||||
|
<input
|
||||||
|
type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
|
||||||
|
(click)="cancel()" (key.enter)="cancel()"
|
||||||
|
>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="submit" i18n-value value="Edit this caption" class="peertube-button orange-button"
|
||||||
|
[disabled]="!form.valid" (click)="updateCaption()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-template>
|
|
@ -0,0 +1,4 @@
|
||||||
|
.caption-textarea {
|
||||||
|
min-height: 600px;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
|
||||||
|
|
||||||
|
import { VIDEO_CAPTION_FILE_CONTENT_VALIDATOR } from '@app/shared/form-validators/video-captions-validators'
|
||||||
|
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
|
||||||
|
import { VideoCaptionEdit, VideoCaptionService, VideoCaptionWithPathEdit } from '@app/shared/shared-main'
|
||||||
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { HTMLServerConfig, VideoConstant } from '@shared/models'
|
||||||
|
import { ServerService } from '../../../../core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-video-caption-edit-modal',
|
||||||
|
styleUrls: [ './video-caption-edit-modal.component.scss' ],
|
||||||
|
templateUrl: './video-caption-edit-modal.component.html'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class VideoCaptionEditModalComponent extends FormReactive implements OnInit {
|
||||||
|
@Input() videoCaption: VideoCaptionWithPathEdit
|
||||||
|
@Input() serverConfig: HTMLServerConfig
|
||||||
|
|
||||||
|
@Output() captionEdited = new EventEmitter<VideoCaptionEdit>()
|
||||||
|
|
||||||
|
@ViewChild('modal', { static: true }) modal: ElementRef
|
||||||
|
|
||||||
|
videoCaptionLanguages: VideoConstant<string>[] = []
|
||||||
|
private openedModal: NgbModalRef
|
||||||
|
private closingModal = false
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
protected formValidatorService: FormValidatorService,
|
||||||
|
private modalService: NgbModal,
|
||||||
|
private videoCaptionService: VideoCaptionService,
|
||||||
|
private serverService: ServerService
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.serverService.getVideoLanguages().subscribe(languages => this.videoCaptionLanguages = languages)
|
||||||
|
|
||||||
|
this.buildForm({ captionFileContent: VIDEO_CAPTION_FILE_CONTENT_VALIDATOR })
|
||||||
|
|
||||||
|
this.loadCaptionContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
loadCaptionContent () {
|
||||||
|
const { captionPath } = this.videoCaption
|
||||||
|
if (captionPath) {
|
||||||
|
this.videoCaptionService.getCaptionContent({
|
||||||
|
captionPath
|
||||||
|
}).subscribe((res) => {
|
||||||
|
this.form.patchValue({
|
||||||
|
captionFileContent: res
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
show () {
|
||||||
|
this.closingModal = false
|
||||||
|
|
||||||
|
this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
hide () {
|
||||||
|
this.closingModal = true
|
||||||
|
this.openedModal.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel () {
|
||||||
|
this.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
isReplacingExistingCaption () {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCaption () {
|
||||||
|
const format = 'vtt'
|
||||||
|
const languageId = this.videoCaption.language.id
|
||||||
|
const languageObject = this.videoCaptionLanguages.find(l => l.id === languageId)
|
||||||
|
this.captionEdited.emit({
|
||||||
|
language: languageObject,
|
||||||
|
captionfile: new File([ this.form.value['captionFileContent'] ], `${languageId}.${format}`, {
|
||||||
|
type: 'text/vtt',
|
||||||
|
lastModified: Date.now()
|
||||||
|
}),
|
||||||
|
action: 'UPDATE'
|
||||||
|
})
|
||||||
|
|
||||||
|
this.hide()
|
||||||
|
}
|
||||||
|
}
|
|
@ -186,6 +186,7 @@
|
||||||
|
|
||||||
<div i18n class="caption-entry-state">Already uploaded ✔</div>
|
<div i18n class="caption-entry-state">Already uploaded ✔</div>
|
||||||
|
|
||||||
|
<span i18n class="caption-entry-edit" (click)="videoCaptionEditModal.show()">Edit</span>
|
||||||
<span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Delete</span>
|
<span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Delete</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -197,6 +198,14 @@
|
||||||
<span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel create</span>
|
<span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel create</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="videoCaption.action === 'UPDATE'">
|
||||||
|
<span class="caption-entry-label">{{ videoCaption.language.label }}</span>
|
||||||
|
|
||||||
|
<div i18n class="caption-entry-state caption-entry-state-create">Will be edited on update</div>
|
||||||
|
|
||||||
|
<span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel edition</span>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="videoCaption.action === 'REMOVE'">
|
<ng-container *ngIf="videoCaption.action === 'REMOVE'">
|
||||||
<span class="caption-entry-label">{{ videoCaption.language.label }}</span>
|
<span class="caption-entry-label">{{ videoCaption.language.label }}</span>
|
||||||
|
|
||||||
|
@ -204,6 +213,13 @@
|
||||||
|
|
||||||
<span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel deletion</span>
|
<span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel deletion</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<my-video-caption-edit-modal
|
||||||
|
#videoCaptionEditModal
|
||||||
|
[videoCaption]="videoCaption"
|
||||||
|
[serverConfig]="serverConfig"
|
||||||
|
(captionEdited)="onCaptionEdited($event)"
|
||||||
|
></my-video-caption-edit-modal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -373,5 +389,5 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<my-video-caption-add-modal
|
<my-video-caption-add-modal
|
||||||
#videoCaptionAddModal [existingCaptions]="getExistingCaptions()" [serverConfig]="serverConfig" (captionAdded)="onCaptionAdded($event)"
|
#videoCaptionAddModal [existingCaptions]="getExistingCaptions()" [serverConfig]="serverConfig" (captionAdded)="onCaptionEdited($event)"
|
||||||
></my-video-caption-add-modal>
|
></my-video-caption-add-modal>
|
||||||
|
|
|
@ -96,6 +96,10 @@ my-peertube-checkbox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.caption-entry-edit {
|
||||||
|
@include peertube-button;
|
||||||
|
}
|
||||||
|
|
||||||
.caption-entry-delete {
|
.caption-entry-delete {
|
||||||
@include peertube-button;
|
@include peertube-button;
|
||||||
@include grey-button;
|
@include grey-button;
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
} from '@app/shared/form-validators/video-validators'
|
} from '@app/shared/form-validators/video-validators'
|
||||||
import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms'
|
import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms'
|
||||||
import { InstanceService } from '@app/shared/shared-instance'
|
import { InstanceService } from '@app/shared/shared-instance'
|
||||||
import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
|
import { VideoCaptionEdit, VideoCaptionWithPathEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
|
||||||
import { PluginInfo } from '@root-helpers/plugins-manager'
|
import { PluginInfo } from '@root-helpers/plugins-manager'
|
||||||
import {
|
import {
|
||||||
HTMLServerConfig,
|
HTMLServerConfig,
|
||||||
|
@ -34,6 +34,7 @@ import {
|
||||||
} from '@shared/models'
|
} from '@shared/models'
|
||||||
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
||||||
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
||||||
|
import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component'
|
||||||
import { VideoEditType } from './video-edit.type'
|
import { VideoEditType } from './video-edit.type'
|
||||||
|
|
||||||
type VideoLanguages = VideoConstant<string> & { group?: string }
|
type VideoLanguages = VideoConstant<string> & { group?: string }
|
||||||
|
@ -58,13 +59,14 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
||||||
@Input() userVideoChannels: SelectChannelItem[] = []
|
@Input() userVideoChannels: SelectChannelItem[] = []
|
||||||
@Input() forbidScheduledPublication = true
|
@Input() forbidScheduledPublication = true
|
||||||
|
|
||||||
@Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = []
|
@Input() videoCaptions: (VideoCaptionWithPathEdit)[] = []
|
||||||
|
|
||||||
@Input() waitTranscodingEnabled = true
|
@Input() waitTranscodingEnabled = true
|
||||||
@Input() type: VideoEditType
|
@Input() type: VideoEditType
|
||||||
@Input() liveVideo: LiveVideo
|
@Input() liveVideo: LiveVideo
|
||||||
|
|
||||||
@ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent
|
@ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent
|
||||||
|
@ViewChild('videoCaptionEditModal', { static: true }) editCaptionModal: VideoCaptionEditModalComponent
|
||||||
|
|
||||||
@Output() formBuilt = new EventEmitter<void>()
|
@Output() formBuilt = new EventEmitter<void>()
|
||||||
@Output() pluginFieldsAdded = new EventEmitter<void>()
|
@Output() pluginFieldsAdded = new EventEmitter<void>()
|
||||||
|
@ -228,12 +230,12 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
||||||
.map(c => c.language.id)
|
.map(c => c.language.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
onCaptionAdded (caption: VideoCaptionEdit) {
|
onCaptionEdited (caption: VideoCaptionEdit) {
|
||||||
const existingCaption = this.videoCaptions.find(c => c.language.id === caption.language.id)
|
const existingCaption = this.videoCaptions.find(c => c.language.id === caption.language.id)
|
||||||
|
|
||||||
// Replace existing caption?
|
// Replace existing caption?
|
||||||
if (existingCaption) {
|
if (existingCaption) {
|
||||||
Object.assign(existingCaption, caption, { action: 'CREATE' as 'CREATE' })
|
Object.assign(existingCaption, caption)
|
||||||
} else {
|
} else {
|
||||||
this.videoCaptions.push(
|
this.videoCaptions.push(
|
||||||
Object.assign(caption, { action: 'CREATE' as 'CREATE' })
|
Object.assign(caption, { action: 'CREATE' as 'CREATE' })
|
||||||
|
@ -251,7 +253,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
// This caption is not on the server, just remove it from our array
|
// This caption is not on the server, just remove it from our array
|
||||||
if (caption.action === 'CREATE') {
|
if (caption.action === 'CREATE' || caption.action === 'UPDATE') {
|
||||||
removeElementFromArray(this.videoCaptions, caption)
|
removeElementFromArray(this.videoCaptions, caption)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { SharedMainModule } from '@app/shared/shared-main'
|
||||||
import { SharedVideoLiveModule } from '@app/shared/shared-video-live'
|
import { SharedVideoLiveModule } from '@app/shared/shared-video-live'
|
||||||
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
||||||
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
||||||
|
import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component'
|
||||||
import { VideoEditComponent } from './video-edit.component'
|
import { VideoEditComponent } from './video-edit.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -20,7 +21,8 @@ import { VideoEditComponent } from './video-edit.component'
|
||||||
|
|
||||||
declarations: [
|
declarations: [
|
||||||
VideoEditComponent,
|
VideoEditComponent,
|
||||||
VideoCaptionAddModalComponent
|
VideoCaptionAddModalComponent,
|
||||||
|
VideoCaptionEditModalComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
exports: [
|
exports: [
|
||||||
|
|
|
@ -14,3 +14,10 @@ export const VIDEO_CAPTION_FILE_VALIDATOR: BuildFormValidator = {
|
||||||
required: $localize`Video caption file is required.`
|
required: $localize`Video caption file is required.`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const VIDEO_CAPTION_FILE_CONTENT_VALIDATOR: BuildFormValidator = {
|
||||||
|
VALIDATORS: [ Validators.required ],
|
||||||
|
MESSAGES: {
|
||||||
|
required: $localize`Caption content is required.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ export interface VideoCaptionEdit {
|
||||||
label?: string
|
label?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
action?: 'CREATE' | 'REMOVE'
|
action?: 'CREATE' | 'REMOVE' | 'UPDATE'
|
||||||
captionfile?: any
|
captionfile?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type VideoCaptionWithPathEdit = VideoCaptionEdit & { captionPath?: string }
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { VideoService } from '@app/shared/shared-main/video'
|
||||||
import { peertubeTranslate } from '@shared/core-utils/i18n'
|
import { peertubeTranslate } from '@shared/core-utils/i18n'
|
||||||
import { ResultList, VideoCaption } from '@shared/models'
|
import { ResultList, VideoCaption } from '@shared/models'
|
||||||
import { VideoCaptionEdit } from './video-caption-edit.model'
|
import { VideoCaptionEdit } from './video-caption-edit.model'
|
||||||
|
import { environment } from '../../../../environments/environment'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VideoCaptionService {
|
export class VideoCaptionService {
|
||||||
|
@ -57,7 +58,7 @@ export class VideoCaptionService {
|
||||||
let obs: Observable<any> = of(undefined)
|
let obs: Observable<any> = of(undefined)
|
||||||
|
|
||||||
for (const videoCaption of videoCaptions) {
|
for (const videoCaption of videoCaptions) {
|
||||||
if (videoCaption.action === 'CREATE') {
|
if (videoCaption.action === 'CREATE' || videoCaption.action === 'UPDATE') {
|
||||||
obs = obs.pipe(switchMap(() => this.addCaption(videoId, videoCaption.language.id, videoCaption.captionfile)))
|
obs = obs.pipe(switchMap(() => this.addCaption(videoId, videoCaption.language.id, videoCaption.captionfile)))
|
||||||
} else if (videoCaption.action === 'REMOVE') {
|
} else if (videoCaption.action === 'REMOVE') {
|
||||||
obs = obs.pipe(switchMap(() => this.removeCaption(videoId, videoCaption.language.id)))
|
obs = obs.pipe(switchMap(() => this.removeCaption(videoId, videoCaption.language.id)))
|
||||||
|
@ -66,4 +67,8 @@ export class VideoCaptionService {
|
||||||
|
|
||||||
return obs
|
return obs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCaptionContent ({ captionPath }: Pick<VideoCaption, 'captionPath'>) {
|
||||||
|
return this.authHttp.get(`${environment.originServerUrl}${captionPath}`, { responseType: 'text' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue