aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorlutangar <johan.dufour@gmail.com>2021-12-22 18:36:56 +0100
committerChocobozzz <chocobozzz@cpy.re>2022-02-28 14:29:01 +0100
commit57d74ec83d64daaf2efc6a3dad8adbcfe1f59415 (patch)
tree3d56b8c45e6f7c88149c34e9c93b994c94587107
parente66d0892b12c5b3b3e8a6b7a4129103a912486a9 (diff)
downloadPeerTube-57d74ec83d64daaf2efc6a3dad8adbcfe1f59415.tar.gz
PeerTube-57d74ec83d64daaf2efc6a3dad8adbcfe1f59415.tar.zst
PeerTube-57d74ec83d64daaf2efc6a3dad8adbcfe1f59415.zip
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.
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.ts3
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.html36
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.scss4
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.ts92
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.html18
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.scss4
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.ts12
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.module.ts4
-rw-r--r--client/src/app/shared/form-validators/video-captions-validators.ts7
-rw-r--r--client/src/app/shared/shared-main/video-caption/video-caption-edit.model.ts4
-rw-r--r--client/src/app/shared/shared-main/video-caption/video-caption.service.ts7
11 files changed, 181 insertions, 10 deletions
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
index 5c4152884..6deadfcbe 100644
--- 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
@@ -77,7 +77,8 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
77 77
78 this.captionAdded.emit({ 78 this.captionAdded.emit({
79 language: languageObject, 79 language: languageObject,
80 captionfile: this.form.value['captionfile'] 80 captionfile: this.form.value['captionfile'],
81 action: 'CREATE'
81 }) 82 })
82 83
83 this.hide() 84 this.hide()
diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.html b/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.html
new file mode 100644
index 000000000..4543b93d8
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.html
@@ -0,0 +1,36 @@
1<ng-template #modal>
2 <ng-container [formGroup]="form">
3
4 <div class="modal-header">
5 <h4 i18n class="modal-title">Edit caption</h4>
6 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
7 </div>
8
9 <div class="modal-body">
10 <label i18n for="captionFileContent">Caption</label>
11 <textarea
12 id="captionFileContent"
13 formControlName="captionFileContent"
14 class="form-control caption-textarea"
15 [ngClass]="{ 'input-error': formErrors['captionFileContent'] }"
16 >
17 </textarea>
18
19 <div *ngIf="formErrors.captionFileContent" class="form-error">
20 {{ formErrors.description }}
21 </div>
22 </div>
23
24 <div class="modal-footer inputs">
25 <input
26 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
27 (click)="cancel()" (key.enter)="cancel()"
28 >
29
30 <input
31 type="submit" i18n-value value="Edit this caption" class="peertube-button orange-button"
32 [disabled]="!form.valid" (click)="updateCaption()"
33 >
34 </div>
35 </ng-container>
36</ng-template>
diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.scss b/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.scss
new file mode 100644
index 000000000..bd96f2b7a
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.scss
@@ -0,0 +1,4 @@
1.caption-textarea {
2 min-height: 600px;
3}
4
diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.ts b/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.ts
new file mode 100644
index 000000000..d2232a38e
--- /dev/null
+++ b/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal/video-caption-edit-modal.component.ts
@@ -0,0 +1,92 @@
1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2
3import { VIDEO_CAPTION_FILE_CONTENT_VALIDATOR } from '@app/shared/form-validators/video-captions-validators'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5import { VideoCaptionEdit, VideoCaptionService, VideoCaptionWithPathEdit } from '@app/shared/shared-main'
6import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
7import { HTMLServerConfig, VideoConstant } from '@shared/models'
8import { ServerService } from '../../../../core'
9
10@Component({
11 selector: 'my-video-caption-edit-modal',
12 styleUrls: [ './video-caption-edit-modal.component.scss' ],
13 templateUrl: './video-caption-edit-modal.component.html'
14})
15
16export class VideoCaptionEditModalComponent extends FormReactive implements OnInit {
17 @Input() videoCaption: VideoCaptionWithPathEdit
18 @Input() serverConfig: HTMLServerConfig
19
20 @Output() captionEdited = new EventEmitter<VideoCaptionEdit>()
21
22 @ViewChild('modal', { static: true }) modal: ElementRef
23
24 videoCaptionLanguages: VideoConstant<string>[] = []
25 private openedModal: NgbModalRef
26 private closingModal = false
27
28 constructor (
29 protected formValidatorService: FormValidatorService,
30 private modalService: NgbModal,
31 private videoCaptionService: VideoCaptionService,
32 private serverService: ServerService
33 ) {
34 super()
35 }
36
37 ngOnInit () {
38 this.serverService.getVideoLanguages().subscribe(languages => this.videoCaptionLanguages = languages)
39
40 this.buildForm({ captionFileContent: VIDEO_CAPTION_FILE_CONTENT_VALIDATOR })
41
42 this.loadCaptionContent()
43 }
44
45 loadCaptionContent () {
46 const { captionPath } = this.videoCaption
47 if (captionPath) {
48 this.videoCaptionService.getCaptionContent({
49 captionPath
50 }).subscribe((res) => {
51 this.form.patchValue({
52 captionFileContent: res
53 })
54 })
55 }
56 }
57
58 show () {
59 this.closingModal = false
60
61 this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false })
62 }
63
64 hide () {
65 this.closingModal = true
66 this.openedModal.close()
67 }
68
69 cancel () {
70 this.hide()
71 }
72
73 isReplacingExistingCaption () {
74 return true
75 }
76
77 updateCaption () {
78 const format = 'vtt'
79 const languageId = this.videoCaption.language.id
80 const languageObject = this.videoCaptionLanguages.find(l => l.id === languageId)
81 this.captionEdited.emit({
82 language: languageObject,
83 captionfile: new File([ this.form.value['captionFileContent'] ], `${languageId}.${format}`, {
84 type: 'text/vtt',
85 lastModified: Date.now()
86 }),
87 action: 'UPDATE'
88 })
89
90 this.hide()
91 }
92}
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 9bb13ba88..2281f8631 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
@@ -186,6 +186,7 @@
186 186
187 <div i18n class="caption-entry-state">Already uploaded &#10004;</div> 187 <div i18n class="caption-entry-state">Already uploaded &#10004;</div>
188 188
189 <span i18n class="caption-entry-edit" (click)="videoCaptionEditModal.show()">Edit</span>
189 <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Delete</span> 190 <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Delete</span>
190 </ng-container> 191 </ng-container>
191 192
@@ -197,6 +198,14 @@
197 <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel create</span> 198 <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel create</span>
198 </ng-container> 199 </ng-container>
199 200
201 <ng-container *ngIf="videoCaption.action === 'UPDATE'">
202 <span class="caption-entry-label">{{ videoCaption.language.label }}</span>
203
204 <div i18n class="caption-entry-state caption-entry-state-create">Will be edited on update</div>
205
206 <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel edition</span>
207 </ng-container>
208
200 <ng-container *ngIf="videoCaption.action === 'REMOVE'"> 209 <ng-container *ngIf="videoCaption.action === 'REMOVE'">
201 <span class="caption-entry-label">{{ videoCaption.language.label }}</span> 210 <span class="caption-entry-label">{{ videoCaption.language.label }}</span>
202 211
@@ -204,6 +213,13 @@
204 213
205 <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel deletion</span> 214 <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Cancel deletion</span>
206 </ng-container> 215 </ng-container>
216
217 <my-video-caption-edit-modal
218 #videoCaptionEditModal
219 [videoCaption]="videoCaption"
220 [serverConfig]="serverConfig"
221 (captionEdited)="onCaptionEdited($event)"
222 ></my-video-caption-edit-modal>
207 </div> 223 </div>
208 </div> 224 </div>
209 225
@@ -373,5 +389,5 @@
373</div> 389</div>
374 390
375<my-video-caption-add-modal 391<my-video-caption-add-modal
376 #videoCaptionAddModal [existingCaptions]="getExistingCaptions()" [serverConfig]="serverConfig" (captionAdded)="onCaptionAdded($event)" 392 #videoCaptionAddModal [existingCaptions]="getExistingCaptions()" [serverConfig]="serverConfig" (captionAdded)="onCaptionEdited($event)"
377></my-video-caption-add-modal> 393></my-video-caption-add-modal>
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 4b1dec89a..5344e5431 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
@@ -96,6 +96,10 @@ my-peertube-checkbox {
96 } 96 }
97 } 97 }
98 98
99 .caption-entry-edit {
100 @include peertube-button;
101 }
102
99 .caption-entry-delete { 103 .caption-entry-delete {
100 @include peertube-button; 104 @include peertube-button;
101 @include grey-button; 105 @include grey-button;
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 31dbe43e6..0f4d0619b 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
@@ -21,7 +21,7 @@ import {
21} from '@app/shared/form-validators/video-validators' 21} from '@app/shared/form-validators/video-validators'
22import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms' 22import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms'
23import { InstanceService } from '@app/shared/shared-instance' 23import { InstanceService } from '@app/shared/shared-instance'
24import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' 24import { VideoCaptionEdit, VideoCaptionWithPathEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
25import { PluginInfo } from '@root-helpers/plugins-manager' 25import { PluginInfo } from '@root-helpers/plugins-manager'
26import { 26import {
27 HTMLServerConfig, 27 HTMLServerConfig,
@@ -34,6 +34,7 @@ import {
34} from '@shared/models' 34} from '@shared/models'
35import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' 35import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
36import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' 36import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
37import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component'
37import { VideoEditType } from './video-edit.type' 38import { VideoEditType } from './video-edit.type'
38 39
39type VideoLanguages = VideoConstant<string> & { group?: string } 40type VideoLanguages = VideoConstant<string> & { group?: string }
@@ -58,13 +59,14 @@ export class VideoEditComponent implements OnInit, OnDestroy {
58 @Input() userVideoChannels: SelectChannelItem[] = [] 59 @Input() userVideoChannels: SelectChannelItem[] = []
59 @Input() forbidScheduledPublication = true 60 @Input() forbidScheduledPublication = true
60 61
61 @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = [] 62 @Input() videoCaptions: (VideoCaptionWithPathEdit)[] = []
62 63
63 @Input() waitTranscodingEnabled = true 64 @Input() waitTranscodingEnabled = true
64 @Input() type: VideoEditType 65 @Input() type: VideoEditType
65 @Input() liveVideo: LiveVideo 66 @Input() liveVideo: LiveVideo
66 67
67 @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent 68 @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent
69 @ViewChild('videoCaptionEditModal', { static: true }) editCaptionModal: VideoCaptionEditModalComponent
68 70
69 @Output() formBuilt = new EventEmitter<void>() 71 @Output() formBuilt = new EventEmitter<void>()
70 @Output() pluginFieldsAdded = new EventEmitter<void>() 72 @Output() pluginFieldsAdded = new EventEmitter<void>()
@@ -228,12 +230,12 @@ export class VideoEditComponent implements OnInit, OnDestroy {
228 .map(c => c.language.id) 230 .map(c => c.language.id)
229 } 231 }
230 232
231 onCaptionAdded (caption: VideoCaptionEdit) { 233 onCaptionEdited (caption: VideoCaptionEdit) {
232 const existingCaption = this.videoCaptions.find(c => c.language.id === caption.language.id) 234 const existingCaption = this.videoCaptions.find(c => c.language.id === caption.language.id)
233 235
234 // Replace existing caption? 236 // Replace existing caption?
235 if (existingCaption) { 237 if (existingCaption) {
236 Object.assign(existingCaption, caption, { action: 'CREATE' as 'CREATE' }) 238 Object.assign(existingCaption, caption)
237 } else { 239 } else {
238 this.videoCaptions.push( 240 this.videoCaptions.push(
239 Object.assign(caption, { action: 'CREATE' as 'CREATE' }) 241 Object.assign(caption, { action: 'CREATE' as 'CREATE' })
@@ -251,7 +253,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
251 } 253 }
252 254
253 // This caption is not on the server, just remove it from our array 255 // This caption is not on the server, just remove it from our array
254 if (caption.action === 'CREATE') { 256 if (caption.action === 'CREATE' || caption.action === 'UPDATE') {
255 removeElementFromArray(this.videoCaptions, caption) 257 removeElementFromArray(this.videoCaptions, caption)
256 return 258 return
257 } 259 }
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 7a3854065..4e8767364 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
@@ -6,6 +6,7 @@ import { SharedMainModule } from '@app/shared/shared-main'
6import { SharedVideoLiveModule } from '@app/shared/shared-video-live' 6import { SharedVideoLiveModule } from '@app/shared/shared-video-live'
7import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' 7import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
8import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' 8import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
9import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component'
9import { VideoEditComponent } from './video-edit.component' 10import { VideoEditComponent } from './video-edit.component'
10 11
11@NgModule({ 12@NgModule({
@@ -20,7 +21,8 @@ import { VideoEditComponent } from './video-edit.component'
20 21
21 declarations: [ 22 declarations: [
22 VideoEditComponent, 23 VideoEditComponent,
23 VideoCaptionAddModalComponent 24 VideoCaptionAddModalComponent,
25 VideoCaptionEditModalComponent
24 ], 26 ],
25 27
26 exports: [ 28 exports: [
diff --git a/client/src/app/shared/form-validators/video-captions-validators.ts b/client/src/app/shared/form-validators/video-captions-validators.ts
index a16216422..e589fe934 100644
--- a/client/src/app/shared/form-validators/video-captions-validators.ts
+++ b/client/src/app/shared/form-validators/video-captions-validators.ts
@@ -14,3 +14,10 @@ export const VIDEO_CAPTION_FILE_VALIDATOR: BuildFormValidator = {
14 required: $localize`Video caption file is required.` 14 required: $localize`Video caption file is required.`
15 } 15 }
16} 16}
17
18export const VIDEO_CAPTION_FILE_CONTENT_VALIDATOR: BuildFormValidator = {
19 VALIDATORS: [ Validators.required ],
20 MESSAGES: {
21 required: $localize`Caption content is required.`
22 }
23}
diff --git a/client/src/app/shared/shared-main/video-caption/video-caption-edit.model.ts b/client/src/app/shared/shared-main/video-caption/video-caption-edit.model.ts
index 732f20158..129e80bc0 100644
--- a/client/src/app/shared/shared-main/video-caption/video-caption-edit.model.ts
+++ b/client/src/app/shared/shared-main/video-caption/video-caption-edit.model.ts
@@ -4,6 +4,8 @@ export interface VideoCaptionEdit {
4 label?: string 4 label?: string
5 } 5 }
6 6
7 action?: 'CREATE' | 'REMOVE' 7 action?: 'CREATE' | 'REMOVE' | 'UPDATE'
8 captionfile?: any 8 captionfile?: any
9} 9}
10
11export type VideoCaptionWithPathEdit = VideoCaptionEdit & { captionPath?: string }
diff --git a/client/src/app/shared/shared-main/video-caption/video-caption.service.ts b/client/src/app/shared/shared-main/video-caption/video-caption.service.ts
index 97b79d842..67eb09e4d 100644
--- a/client/src/app/shared/shared-main/video-caption/video-caption.service.ts
+++ b/client/src/app/shared/shared-main/video-caption/video-caption.service.ts
@@ -8,6 +8,7 @@ import { VideoService } from '@app/shared/shared-main/video'
8import { peertubeTranslate } from '@shared/core-utils/i18n' 8import { peertubeTranslate } from '@shared/core-utils/i18n'
9import { ResultList, VideoCaption } from '@shared/models' 9import { ResultList, VideoCaption } from '@shared/models'
10import { VideoCaptionEdit } from './video-caption-edit.model' 10import { VideoCaptionEdit } from './video-caption-edit.model'
11import { environment } from '../../../../environments/environment'
11 12
12@Injectable() 13@Injectable()
13export class VideoCaptionService { 14export class VideoCaptionService {
@@ -57,7 +58,7 @@ export class VideoCaptionService {
57 let obs: Observable<any> = of(undefined) 58 let obs: Observable<any> = of(undefined)
58 59
59 for (const videoCaption of videoCaptions) { 60 for (const videoCaption of videoCaptions) {
60 if (videoCaption.action === 'CREATE') { 61 if (videoCaption.action === 'CREATE' || videoCaption.action === 'UPDATE') {
61 obs = obs.pipe(switchMap(() => this.addCaption(videoId, videoCaption.language.id, videoCaption.captionfile))) 62 obs = obs.pipe(switchMap(() => this.addCaption(videoId, videoCaption.language.id, videoCaption.captionfile)))
62 } else if (videoCaption.action === 'REMOVE') { 63 } else if (videoCaption.action === 'REMOVE') {
63 obs = obs.pipe(switchMap(() => this.removeCaption(videoId, videoCaption.language.id))) 64 obs = obs.pipe(switchMap(() => this.removeCaption(videoId, videoCaption.language.id)))
@@ -66,4 +67,8 @@ export class VideoCaptionService {
66 67
67 return obs 68 return obs
68 } 69 }
70
71 getCaptionContent ({ captionPath }: Pick<VideoCaption, 'captionPath'>) {
72 return this.authHttp.get(`${environment.originServerUrl}${captionPath}`, { responseType: 'text' })
73 }
69} 74}