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