Improve frontend accessibility
[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 import { VideoConstant } from '../../../../../../shared'
16
17 @Component({
18   selector: 'my-video-edit',
19   styleUrls: [ './video-edit.component.scss' ],
20   templateUrl: './video-edit.component.html'
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: { id: number, label: string }[] = []
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: VideoConstant<string>[] = []
37   videoLicences: VideoConstant<string>[] = []
38   videoLanguages: VideoConstant<string>[] = []
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   private firstPatchDone = false
53
54   constructor (
55     private formValidatorService: FormValidatorService,
56     private videoValidatorsService: VideoValidatorsService,
57     private videoCaptionService: VideoCaptionService,
58     private route: ActivatedRoute,
59     private router: Router,
60     private notificationsService: NotificationsService,
61     private serverService: ServerService,
62     private i18nPrimengCalendarService: I18nPrimengCalendarService
63   ) {
64     this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS
65     this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES
66
67     this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale()
68     this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
69     this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat()
70   }
71
72   get existingCaptions () {
73     return this.videoCaptions
74                .filter(c => c.action !== 'REMOVE')
75                .map(c => c.language.id)
76   }
77
78   updateForm () {
79     const defaultValues = {
80       nsfw: 'false',
81       commentsEnabled: 'true',
82       waitTranscoding: 'true',
83       tags: []
84     }
85     const obj = {
86       name: this.videoValidatorsService.VIDEO_NAME,
87       privacy: this.videoValidatorsService.VIDEO_PRIVACY,
88       channelId: this.videoValidatorsService.VIDEO_CHANNEL,
89       nsfw: null,
90       commentsEnabled: null,
91       waitTranscoding: null,
92       category: this.videoValidatorsService.VIDEO_CATEGORY,
93       licence: this.videoValidatorsService.VIDEO_LICENCE,
94       language: this.videoValidatorsService.VIDEO_LANGUAGE,
95       description: this.videoValidatorsService.VIDEO_DESCRIPTION,
96       tags: null,
97       thumbnailfile: null,
98       previewfile: null,
99       support: this.videoValidatorsService.VIDEO_SUPPORT,
100       schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT
101     }
102
103     this.formValidatorService.updateForm(
104       this.form,
105       this.formErrors,
106       this.validationMessages,
107       obj,
108       defaultValues
109     )
110
111     this.form.addControl('captions', new FormArray([
112       new FormGroup({
113         language: new FormControl(),
114         captionfile: new FormControl()
115       })
116     ]))
117
118     this.trackChannelChange()
119     this.trackPrivacyChange()
120   }
121
122   ngOnInit () {
123     this.updateForm()
124
125     this.videoCategories = this.serverService.getVideoCategories()
126     this.videoLicences = this.serverService.getVideoLicences()
127     this.videoLanguages = this.serverService.getVideoLanguages()
128
129     this.schedulerInterval = setInterval(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute
130   }
131
132   ngOnDestroy () {
133     if (this.schedulerInterval) clearInterval(this.schedulerInterval)
134   }
135
136   onCaptionAdded (caption: VideoCaptionEdit) {
137     const existingCaption = this.videoCaptions.find(c => c.language.id === caption.language.id)
138
139     // Replace existing caption?
140     if (existingCaption) {
141       Object.assign(existingCaption, caption, { action: 'CREATE' as 'CREATE' })
142       return
143     }
144
145     this.videoCaptions.push(
146       Object.assign(caption, { action: 'CREATE' as 'CREATE' })
147     )
148   }
149
150   deleteCaption (caption: VideoCaptionEdit) {
151     // This caption is not on the server, just remove it from our array
152     if (caption.action === 'CREATE') {
153       removeElementFromArray(this.videoCaptions, caption)
154       return
155     }
156
157     caption.action = 'REMOVE' as 'REMOVE'
158   }
159
160   openAddCaptionModal () {
161     this.videoCaptionAddModal.show()
162   }
163
164   private trackPrivacyChange () {
165     // We will update the "support" field depending on the channel
166     this.form.controls[ 'privacy' ]
167       .valueChanges
168       .pipe(map(res => parseInt(res.toString(), 10)))
169       .subscribe(
170         newPrivacyId => {
171
172           this.schedulePublicationEnabled = newPrivacyId === this.SPECIAL_SCHEDULED_PRIVACY
173
174           // Value changed
175           const scheduleControl = this.form.get('schedulePublicationAt')
176           const waitTranscodingControl = this.form.get('waitTranscoding')
177
178           if (this.schedulePublicationEnabled) {
179             scheduleControl.setValidators([ Validators.required ])
180
181             waitTranscodingControl.disable()
182             waitTranscodingControl.setValue(false)
183           } else {
184             scheduleControl.clearValidators()
185
186             waitTranscodingControl.enable()
187
188             // Do not update the control value on first patch (values come from the server)
189             if (this.firstPatchDone === true) {
190               waitTranscodingControl.setValue(true)
191             }
192           }
193
194           scheduleControl.updateValueAndValidity()
195           waitTranscodingControl.updateValueAndValidity()
196
197           this.firstPatchDone = true
198
199         }
200       )
201   }
202
203   private trackChannelChange () {
204     // We will update the "support" field depending on the channel
205     this.form.controls[ 'channelId' ]
206       .valueChanges
207       .pipe(map(res => parseInt(res.toString(), 10)))
208       .subscribe(
209         newChannelId => {
210           const oldChannelId = parseInt(this.form.value[ 'channelId' ], 10)
211           const currentSupport = this.form.value[ 'support' ]
212
213           // Not initialized yet
214           if (isNaN(newChannelId)) return
215           const newChannel = this.userVideoChannels.find(c => c.id === newChannelId)
216           if (!newChannel) return
217
218           // First time we set the channel?
219           if (isNaN(oldChannelId)) return this.updateSupportField(newChannel.support)
220           const oldChannel = this.userVideoChannels.find(c => c.id === oldChannelId)
221
222           if (!newChannel || !oldChannel) {
223             console.error('Cannot find new or old channel.')
224             return
225           }
226
227           // If the current support text is not the same than the old channel, the user updated it.
228           // We don't want the user to lose his text, so stop here
229           if (currentSupport && currentSupport !== oldChannel.support) return
230
231           // Update the support text with our new channel
232           this.updateSupportField(newChannel.support)
233         }
234       )
235   }
236
237   private updateSupportField (support: string) {
238     return this.form.patchValue({ support: support || '' })
239   }
240 }