mirror of https://github.com/Chocobozzz/PeerTube
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.pull/4826/head
parent
e66d0892b1
commit
57d74ec83d
|
@ -77,7 +77,8 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
|
|||
|
||||
this.captionAdded.emit({
|
||||
language: languageObject,
|
||||
captionfile: this.form.value['captionfile']
|
||||
captionfile: this.form.value['captionfile'],
|
||||
action: 'CREATE'
|
||||
})
|
||||
|
||||
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>
|
||||
|
||||
<span i18n class="caption-entry-edit" (click)="videoCaptionEditModal.show()">Edit</span>
|
||||
<span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Delete</span>
|
||||
</ng-container>
|
||||
|
||||
|
@ -197,6 +198,14 @@
|
|||
<span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel create</span>
|
||||
</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'">
|
||||
<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>
|
||||
</ng-container>
|
||||
|
||||
<my-video-caption-edit-modal
|
||||
#videoCaptionEditModal
|
||||
[videoCaption]="videoCaption"
|
||||
[serverConfig]="serverConfig"
|
||||
(captionEdited)="onCaptionEdited($event)"
|
||||
></my-video-caption-edit-modal>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -373,5 +389,5 @@
|
|||
</div>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -96,6 +96,10 @@ my-peertube-checkbox {
|
|||
}
|
||||
}
|
||||
|
||||
.caption-entry-edit {
|
||||
@include peertube-button;
|
||||
}
|
||||
|
||||
.caption-entry-delete {
|
||||
@include peertube-button;
|
||||
@include grey-button;
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
} from '@app/shared/form-validators/video-validators'
|
||||
import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms'
|
||||
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 {
|
||||
HTMLServerConfig,
|
||||
|
@ -34,6 +34,7 @@ import {
|
|||
} from '@shared/models'
|
||||
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
||||
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'
|
||||
|
||||
type VideoLanguages = VideoConstant<string> & { group?: string }
|
||||
|
@ -58,13 +59,14 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
|||
@Input() userVideoChannels: SelectChannelItem[] = []
|
||||
@Input() forbidScheduledPublication = true
|
||||
|
||||
@Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = []
|
||||
@Input() videoCaptions: (VideoCaptionWithPathEdit)[] = []
|
||||
|
||||
@Input() waitTranscodingEnabled = true
|
||||
@Input() type: VideoEditType
|
||||
@Input() liveVideo: LiveVideo
|
||||
|
||||
@ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent
|
||||
@ViewChild('videoCaptionEditModal', { static: true }) editCaptionModal: VideoCaptionEditModalComponent
|
||||
|
||||
@Output() formBuilt = new EventEmitter<void>()
|
||||
@Output() pluginFieldsAdded = new EventEmitter<void>()
|
||||
|
@ -228,12 +230,12 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
|||
.map(c => c.language.id)
|
||||
}
|
||||
|
||||
onCaptionAdded (caption: VideoCaptionEdit) {
|
||||
onCaptionEdited (caption: VideoCaptionEdit) {
|
||||
const existingCaption = this.videoCaptions.find(c => c.language.id === caption.language.id)
|
||||
|
||||
// Replace existing caption?
|
||||
if (existingCaption) {
|
||||
Object.assign(existingCaption, caption, { action: 'CREATE' as 'CREATE' })
|
||||
Object.assign(existingCaption, caption)
|
||||
} else {
|
||||
this.videoCaptions.push(
|
||||
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
|
||||
if (caption.action === 'CREATE') {
|
||||
if (caption.action === 'CREATE' || caption.action === 'UPDATE') {
|
||||
removeElementFromArray(this.videoCaptions, caption)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { SharedMainModule } from '@app/shared/shared-main'
|
|||
import { SharedVideoLiveModule } from '@app/shared/shared-video-live'
|
||||
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
||||
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'
|
||||
|
||||
@NgModule({
|
||||
|
@ -20,7 +21,8 @@ import { VideoEditComponent } from './video-edit.component'
|
|||
|
||||
declarations: [
|
||||
VideoEditComponent,
|
||||
VideoCaptionAddModalComponent
|
||||
VideoCaptionAddModalComponent,
|
||||
VideoCaptionEditModalComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
|
|
|
@ -14,3 +14,10 @@ export const VIDEO_CAPTION_FILE_VALIDATOR: BuildFormValidator = {
|
|||
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
|
||||
}
|
||||
|
||||
action?: 'CREATE' | 'REMOVE'
|
||||
action?: 'CREATE' | 'REMOVE' | 'UPDATE'
|
||||
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 { ResultList, VideoCaption } from '@shared/models'
|
||||
import { VideoCaptionEdit } from './video-caption-edit.model'
|
||||
import { environment } from '../../../../environments/environment'
|
||||
|
||||
@Injectable()
|
||||
export class VideoCaptionService {
|
||||
|
@ -57,7 +58,7 @@ export class VideoCaptionService {
|
|||
let obs: Observable<any> = of(undefined)
|
||||
|
||||
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)))
|
||||
} else if (videoCaption.action === 'REMOVE') {
|
||||
obs = obs.pipe(switchMap(() => this.removeCaption(videoId, videoCaption.language.id)))
|
||||
|
@ -66,4 +67,8 @@ export class VideoCaptionService {
|
|||
|
||||
return obs
|
||||
}
|
||||
|
||||
getCaptionContent ({ captionPath }: Pick<VideoCaption, 'captionPath'>) {
|
||||
return this.authHttp.get(`${environment.originServerUrl}${captionPath}`, { responseType: 'text' })
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue