Implement captions/subtitles
[oweals/peertube.git] / client / src / app / videos / +video-edit / shared / video-edit.component.ts
1 import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'
2 import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'
3 import { ActivatedRoute, Router } from '@angular/router'
4 import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared'
5 import { NotificationsService } from 'angular2-notifications'
6 import { ServerService } from '../../../core/server'
7 import { VideoEdit } from '../../../shared/video/video-edit.model'
8 import { map } from 'rxjs/operators'
9 import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
10 import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
11 import { VideoCaptionService } from '@app/shared/video-caption'
12 import { VideoCaptionAddModalComponent } from '@app/videos/+video-edit/shared/video-caption-add-modal.component'
13 import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
14 import { removeElementFromArray } from '@app/shared/misc/utils'
15
16 @Component({
17   selector: 'my-video-edit',
18   styleUrls: [ './video-edit.component.scss' ],
19   templateUrl: './video-edit.component.html'
20 })
21
22 export class VideoEditComponent implements OnInit, OnDestroy {
23   @Input() form: FormGroup
24   @Input() formErrors: { [ id: string ]: string } = {}
25   @Input() validationMessages: FormReactiveValidationMessages = {}
26   @Input() videoPrivacies = []
27   @Input() userVideoChannels: { id: number, label: string, support: string }[] = []
28   @Input() schedulePublicationPossible = true
29   @Input() videoCaptions: VideoCaptionEdit[] = []
30
31   @ViewChild('videoCaptionAddModal') videoCaptionAddModal: VideoCaptionAddModalComponent
32
33   // So that it can be accessed in the template
34   readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
35
36   videoCategories = []
37   videoLicences = []
38   videoLanguages = []
39
40   tagValidators: ValidatorFn[]
41   tagValidatorsMessages: { [ name: string ]: string }
42
43   schedulePublicationEnabled = false
44
45   calendarLocale: any = {}
46   minScheduledDate = new Date()
47
48   calendarTimezone: string
49   calendarDateFormat: string
50
51   private schedulerInterval
52
53   constructor (
54     private formValidatorService: FormValidatorService,
55     private videoValidatorsService: VideoValidatorsService,
56     private videoCaptionService: VideoCaptionService,
57     private route: ActivatedRoute,
58     private router: Router,
59     private notificationsService: NotificationsService,
60     private serverService: ServerService,
61     private i18nPrimengCalendarService: I18nPrimengCalendarService
62   ) {
63     this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS
64     this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES
65
66     this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale()
67     this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
68     this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat()
69   }
70
71   updateForm () {
72     const defaultValues = {
73       nsfw: 'false',
74       commentsEnabled: 'true',
75       waitTranscoding: 'true',
76       tags: []
77     }
78     const obj = {
79       name: this.videoValidatorsService.VIDEO_NAME,
80       privacy: this.videoValidatorsService.VIDEO_PRIVACY,
81       channelId: this.videoValidatorsService.VIDEO_CHANNEL,
82       nsfw: null,
83       commentsEnabled: null,
84       waitTranscoding: null,
85       category: this.videoValidatorsService.VIDEO_CATEGORY,
86       licence: this.videoValidatorsService.VIDEO_LICENCE,
87       language: this.videoValidatorsService.VIDEO_LANGUAGE,
88       description: this.videoValidatorsService.VIDEO_DESCRIPTION,
89       tags: null,
90       thumbnailfile: null,
91       previewfile: null,
92       support: this.videoValidatorsService.VIDEO_SUPPORT,
93       schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT
94     }
95
96     this.formValidatorService.updateForm(
97       this.form,
98       this.formErrors,
99       this.validationMessages,
100       obj,
101       defaultValues
102     )
103
104     this.form.addControl('captions', new FormArray([
105       new FormGroup({
106         language: new FormControl(),
107         captionfile: new FormControl()
108       })
109     ]))
110
111     this.trackChannelChange()
112     this.trackPrivacyChange()
113   }
114
115   ngOnInit () {
116     this.updateForm()
117
118     this.videoCategories = this.serverService.getVideoCategories()
119     this.videoLicences = this.serverService.getVideoLicences()
120     this.videoLanguages = this.serverService.getVideoLanguages()
121
122     this.schedulerInterval = setInterval(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute
123   }
124
125   ngOnDestroy () {
126     if (this.schedulerInterval) clearInterval(this.schedulerInterval)
127   }
128
129   getExistingCaptions () {
130     return this.videoCaptions.map(c => c.language.id)
131   }
132
133   onCaptionAdded (caption: VideoCaptionEdit) {
134     this.videoCaptions.push(
135       Object.assign(caption, { action: 'CREATE' as 'CREATE' })
136     )
137   }
138
139   deleteCaption (caption: VideoCaptionEdit) {
140     // This caption is not on the server, just remove it from our array
141     if (caption.action === 'CREATE') {
142       removeElementFromArray(this.videoCaptions, caption)
143       return
144     }
145
146     caption.action = 'REMOVE' as 'REMOVE'
147   }
148
149   openAddCaptionModal () {
150     this.videoCaptionAddModal.show()
151   }
152
153   private trackPrivacyChange () {
154     // We will update the "support" field depending on the channel
155     this.form.controls[ 'privacy' ]
156       .valueChanges
157       .pipe(map(res => parseInt(res.toString(), 10)))
158       .subscribe(
159         newPrivacyId => {
160           this.schedulePublicationEnabled = newPrivacyId === this.SPECIAL_SCHEDULED_PRIVACY
161
162           // Value changed
163           const scheduleControl = this.form.get('schedulePublicationAt')
164           const waitTranscodingControl = this.form.get('waitTranscoding')
165
166           if (this.schedulePublicationEnabled) {
167             scheduleControl.setValidators([ Validators.required ])
168
169             waitTranscodingControl.disable()
170             waitTranscodingControl.setValue(false)
171           } else {
172             scheduleControl.clearValidators()
173
174             waitTranscodingControl.enable()
175             waitTranscodingControl.setValue(true)
176           }
177
178           scheduleControl.updateValueAndValidity()
179           waitTranscodingControl.updateValueAndValidity()
180         }
181       )
182   }
183
184   private trackChannelChange () {
185     // We will update the "support" field depending on the channel
186     this.form.controls[ 'channelId' ]
187       .valueChanges
188       .pipe(map(res => parseInt(res.toString(), 10)))
189       .subscribe(
190         newChannelId => {
191           const oldChannelId = parseInt(this.form.value[ 'channelId' ], 10)
192           const currentSupport = this.form.value[ 'support' ]
193
194           // Not initialized yet
195           if (isNaN(newChannelId)) return
196           const newChannel = this.userVideoChannels.find(c => c.id === newChannelId)
197           if (!newChannel) return
198
199           // First time we set the channel?
200           if (isNaN(oldChannelId)) return this.updateSupportField(newChannel.support)
201           const oldChannel = this.userVideoChannels.find(c => c.id === oldChannelId)
202
203           if (!newChannel || !oldChannel) {
204             console.error('Cannot find new or old channel.')
205             return
206           }
207
208           // If the current support text is not the same than the old channel, the user updated it.
209           // We don't want the user to lose his text, so stop here
210           if (currentSupport && currentSupport !== oldChannel.support) return
211
212           // Update the support text with our new channel
213           this.updateSupportField(newChannel.support)
214         }
215       )
216   }
217
218   private updateSupportField (support: string) {
219     return this.form.patchValue({ support: support || '' })
220   }
221 }