video-edit.component.ts 12.4 KB
Newer Older
Chocobozzz's avatar
Chocobozzz committed
1
import { forkJoin } from 'rxjs'
Chocobozzz's avatar
Chocobozzz committed
2
import { map } from 'rxjs/operators'
Chocobozzz's avatar
Chocobozzz committed
3
import { SelectChannelItem } from 'src/types/select-options-item.model'
4
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
5
import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms'
6
import { HooksService, PluginService, ServerService } from '@app/core'
Chocobozzz's avatar
Chocobozzz committed
7
import { removeElementFromArray } from '@app/helpers'
8
9
10
11
12
13
14
15
16
17
18
19
20
import {
  VIDEO_CATEGORY_VALIDATOR,
  VIDEO_CHANNEL_VALIDATOR,
  VIDEO_DESCRIPTION_VALIDATOR,
  VIDEO_LANGUAGE_VALIDATOR,
  VIDEO_LICENCE_VALIDATOR,
  VIDEO_NAME_VALIDATOR,
  VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR,
  VIDEO_PRIVACY_VALIDATOR,
  VIDEO_SCHEDULE_PUBLICATION_AT_VALIDATOR,
  VIDEO_SUPPORT_VALIDATOR,
  VIDEO_TAGS_ARRAY_VALIDATOR
} from '@app/shared/form-validators/video-validators'
Chocobozzz's avatar
Chocobozzz committed
21
import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms'
Chocobozzz's avatar
Chocobozzz committed
22
import { InstanceService } from '@app/shared/shared-instance'
Chocobozzz's avatar
Chocobozzz committed
23
import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
Chocobozzz's avatar
Chocobozzz committed
24
import {
Chocobozzz's avatar
Chocobozzz committed
25
  HTMLServerConfig,
Chocobozzz's avatar
Chocobozzz committed
26
27
28
29
30
31
32
  LiveVideo,
  RegisterClientFormFieldOptions,
  RegisterClientVideoFieldOptions,
  VideoConstant,
  VideoDetails,
  VideoPrivacy
} from '@shared/models'
Chocobozzz's avatar
Chocobozzz committed
33
34
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
35
import { VideoEditType } from './video-edit.type'
36
37

type VideoLanguages = VideoConstant<string> & { group?: string }
38
39
40
41
type PluginField = {
  commonOptions: RegisterClientFormFieldOptions
  videoFormOptions: RegisterClientVideoFieldOptions
}
42
43
44
45
46
47

@Component({
  selector: 'my-video-edit',
  styleUrls: [ './video-edit.component.scss' ],
  templateUrl: './video-edit.component.html'
})
Chocobozzz's avatar
Chocobozzz committed
48
export class VideoEditComponent implements OnInit, OnDestroy {
49
50
  @Input() form: FormGroup
  @Input() formErrors: { [ id: string ]: string } = {}
Chocobozzz's avatar
Chocobozzz committed
51
  @Input() validationMessages: FormReactiveValidationMessages = {}
52
53
54

  @Input() videoToUpdate: VideoDetails

Chocobozzz's avatar
Chocobozzz committed
55
  @Input() userVideoChannels: SelectChannelItem[] = []
56
  @Input() schedulePublicationPossible = true
57

58
  @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = []
59

60
  @Input() waitTranscodingEnabled = true
61
  @Input() type: VideoEditType
62
  @Input() liveVideo: LiveVideo
Chocobozzz's avatar
Chocobozzz committed
63

Chocobozzz's avatar
Chocobozzz committed
64
  @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent
65

66
67
  @Output() pluginFieldsAdded = new EventEmitter<void>()

68
69
  // So that it can be accessed in the template
  readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
70

71
  videoPrivacies: VideoConstant<VideoPrivacy>[] = []
Chocobozzz's avatar
Chocobozzz committed
72
73
  videoCategories: VideoConstant<number>[] = []
  videoLicences: VideoConstant<number>[] = []
74
  videoLanguages: VideoLanguages[] = []
75

76
77
  pluginDataFormGroup: FormGroup

78
79
80
81
  schedulePublicationEnabled = false

  calendarLocale: any = {}
  minScheduledDate = new Date()
clementbrizard's avatar
clementbrizard committed
82
  myYearRange = '1880:' + (new Date()).getFullYear()
83
84
85

  calendarTimezone: string
  calendarDateFormat: string
86

Chocobozzz's avatar
Chocobozzz committed
87
  serverConfig: HTMLServerConfig
Chocobozzz's avatar
Chocobozzz committed
88

89
  pluginFields: PluginField[] = []
90

BO41's avatar
BO41 committed
91
  private schedulerInterval: any
Chocobozzz's avatar
Chocobozzz committed
92
  private firstPatchDone = false
93
  private initialVideoCaptions: string[] = []
Chocobozzz's avatar
Chocobozzz committed
94

95
  constructor (
Chocobozzz's avatar
Chocobozzz committed
96
    private formValidatorService: FormValidatorService,
97
    private videoService: VideoService,
98
    private serverService: ServerService,
99
    private pluginService: PluginService,
100
    private instanceService: InstanceService,
Chocobozzz's avatar
Chocobozzz committed
101
    private i18nPrimengCalendarService: I18nPrimengCalendarService,
102
103
    private ngZone: NgZone,
    private hooks: HooksService
Chocobozzz's avatar
Chocobozzz committed
104
  ) {
105
106
    this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
    this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat()
Chocobozzz's avatar
Chocobozzz committed
107
  }
108
109

  updateForm () {
BO41's avatar
BO41 committed
110
    const defaultValues: any = {
Chocobozzz's avatar
Chocobozzz committed
111
112
      nsfw: 'false',
      commentsEnabled: 'true',
113
      downloadEnabled: 'true',
114
      waitTranscoding: 'true',
Chocobozzz's avatar
Chocobozzz committed
115
116
      tags: []
    }
BO41's avatar
BO41 committed
117
    const obj: any = {
118
119
120
      name: VIDEO_NAME_VALIDATOR,
      privacy: VIDEO_PRIVACY_VALIDATOR,
      channelId: VIDEO_CHANNEL_VALIDATOR,
Chocobozzz's avatar
Chocobozzz committed
121
122
      nsfw: null,
      commentsEnabled: null,
123
      downloadEnabled: null,
124
      waitTranscoding: null,
125
126
127
128
129
      category: VIDEO_CATEGORY_VALIDATOR,
      licence: VIDEO_LICENCE_VALIDATOR,
      language: VIDEO_LANGUAGE_VALIDATOR,
      description: VIDEO_DESCRIPTION_VALIDATOR,
      tags: VIDEO_TAGS_ARRAY_VALIDATOR,
Chocobozzz's avatar
Chocobozzz committed
130
      previewfile: null,
131
132
      support: VIDEO_SUPPORT_VALIDATOR,
      schedulePublicationAt: VIDEO_SCHEDULE_PUBLICATION_AT_VALIDATOR,
133
      originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR,
Chocobozzz's avatar
Chocobozzz committed
134
      liveStreamKey: null,
Chocobozzz's avatar
Chocobozzz committed
135
      permanentLive: null,
Chocobozzz's avatar
Chocobozzz committed
136
      saveReplay: null
Chocobozzz's avatar
Chocobozzz committed
137
    }
138

Chocobozzz's avatar
Chocobozzz committed
139
140
141
142
143
144
145
    this.formValidatorService.updateForm(
      this.form,
      this.formErrors,
      this.validationMessages,
      obj,
      defaultValues
    )
146

Chocobozzz's avatar
Chocobozzz committed
147
148
149
150
151
152
153
    this.form.addControl('captions', new FormArray([
      new FormGroup({
        language: new FormControl(),
        captionfile: new FormControl()
      })
    ]))

154
155
    this.trackChannelChange()
    this.trackPrivacyChange()
Chocobozzz's avatar
Chocobozzz committed
156
    this.trackLivePermanentFieldChange()
157
158
159
160
161
  }

  ngOnInit () {
    this.updateForm()

162
163
164
    this.pluginService.ensurePluginsAreLoaded('video-edit')
      .then(() => this.updatePluginFields())

Chocobozzz's avatar
Chocobozzz committed
165
166
    this.serverService.getVideoCategories()
        .subscribe(res => this.videoCategories = res)
167

Chocobozzz's avatar
Chocobozzz committed
168
169
    this.serverService.getVideoLicences()
        .subscribe(res => this.videoLicences = res)
170

171
172
173
174
175
176
    forkJoin([
      this.instanceService.getAbout(),
      this.serverService.getVideoLanguages()
    ]).pipe(map(([ about, languages ]) => ({ about, languages })))
      .subscribe(res => {
        this.videoLanguages = res.languages
177
178
179
180
181
          .map(l => {
            return res.about.instance.languages.includes(l.id)
              ? { ...l, group: $localize`Instance languages`, groupOrder: 0 }
              : { ...l, group: $localize`All languages`, groupOrder: 1 }
          })
182
183
          .sort((a, b) => a.groupOrder - b.groupOrder)
      })
Chocobozzz's avatar
Chocobozzz committed
184
185

    this.serverService.getVideoPrivacies()
186
      .subscribe(privacies => {
187
        this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies).videoPrivacies
188
189
190
        if (this.schedulePublicationPossible) {
          this.videoPrivacies.push({
            id: this.SPECIAL_SCHEDULED_PRIVACY,
Chocobozzz's avatar
Chocobozzz committed
191
192
            label: $localize`Scheduled`,
            description: $localize`Hide the video until a specific date`
193
194
195
          })
        }
      })
196

Chocobozzz's avatar
Chocobozzz committed
197
    this.serverConfig = this.serverService.getHTMLConfig()
198

199
    this.initialVideoCaptions = this.videoCaptions.map(c => c.language.id)
Chocobozzz's avatar
Chocobozzz committed
200
201
202
203

    this.ngZone.runOutsideAngular(() => {
      this.schedulerInterval = setInterval(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute
    })
204
205

    this.hooks.runAction('action:video-edit.init', 'video-edit', { type: this.type })
Chocobozzz's avatar
Chocobozzz committed
206
207
208
209
210
211
  }

  ngOnDestroy () {
    if (this.schedulerInterval) clearInterval(this.schedulerInterval)
  }

212
213
214
215
216
217
  getExistingCaptions () {
    return this.videoCaptions
               .filter(c => c.action !== 'REMOVE')
               .map(c => c.language.id)
  }

Chocobozzz's avatar
Chocobozzz committed
218
  onCaptionAdded (caption: VideoCaptionEdit) {
Chocobozzz's avatar
Chocobozzz committed
219
220
221
222
223
    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' })
Chocobozzz's avatar
Chocobozzz committed
224
225
226
227
    } else {
      this.videoCaptions.push(
        Object.assign(caption, { action: 'CREATE' as 'CREATE' })
      )
Chocobozzz's avatar
Chocobozzz committed
228
229
    }

Chocobozzz's avatar
Chocobozzz committed
230
    this.sortVideoCaptions()
Chocobozzz's avatar
Chocobozzz committed
231
232
  }

233
234
235
236
237
238
239
  async deleteCaption (caption: VideoCaptionEdit) {
    // Caption recovers his former state
    if (caption.action && this.initialVideoCaptions.indexOf(caption.language.id) !== -1) {
      caption.action = undefined
      return
    }

Chocobozzz's avatar
Chocobozzz committed
240
241
242
243
244
245
246
247
248
249
250
    // This caption is not on the server, just remove it from our array
    if (caption.action === 'CREATE') {
      removeElementFromArray(this.videoCaptions, caption)
      return
    }

    caption.action = 'REMOVE' as 'REMOVE'
  }

  openAddCaptionModal () {
    this.videoCaptionAddModal.show()
251
252
  }

Chocobozzz's avatar
Chocobozzz committed
253
254
255
256
  isSaveReplayEnabled () {
    return this.serverConfig.live.allowReplay
  }

Chocobozzz's avatar
Chocobozzz committed
257
258
259
260
  isPermanentLiveEnabled () {
    return this.form.value['permanentLive'] === true
  }

261
262
263
264
265
266
267
268
269
270
  isPluginFieldHidden (pluginField: PluginField) {
    if (typeof pluginField.commonOptions.hidden !== 'function') return false

    return pluginField.commonOptions.hidden({
      formValues: this.form.value,
      videoToUpdate: this.videoToUpdate,
      liveVideo: this.liveVideo
    })
  }

Chocobozzz's avatar
Chocobozzz committed
271
272
273
274
275
276
277
278
279
  private sortVideoCaptions () {
    this.videoCaptions.sort((v1, v2) => {
      if (v1.language.label < v2.language.label) return -1
      if (v1.language.label === v2.language.label) return 0

      return 1
    })
  }

280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
  private updatePluginFields () {
    this.pluginFields = this.pluginService.getRegisteredVideoFormFields(this.type)

    if (this.pluginFields.length === 0) return

    const obj: any = {}

    for (const setting of this.pluginFields) {
      obj[setting.commonOptions.name] = new FormControl(setting.commonOptions.default)
    }

    this.pluginDataFormGroup = new FormGroup(obj)
    this.form.addControl('pluginData', this.pluginDataFormGroup)

    this.pluginFieldsAdded.emit()
  }

297
  private trackPrivacyChange () {
298
    // We will update the schedule input and the wait transcoding checkbox validators
299
300
301
302
303
    this.form.controls[ 'privacy' ]
      .valueChanges
      .pipe(map(res => parseInt(res.toString(), 10)))
      .subscribe(
        newPrivacyId => {
Chocobozzz's avatar
Chocobozzz committed
304

305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
          this.schedulePublicationEnabled = newPrivacyId === this.SPECIAL_SCHEDULED_PRIVACY

          // Value changed
          const scheduleControl = this.form.get('schedulePublicationAt')
          const waitTranscodingControl = this.form.get('waitTranscoding')

          if (this.schedulePublicationEnabled) {
            scheduleControl.setValidators([ Validators.required ])

            waitTranscodingControl.disable()
            waitTranscodingControl.setValue(false)
          } else {
            scheduleControl.clearValidators()

            waitTranscodingControl.enable()
Chocobozzz's avatar
Chocobozzz committed
320
321
322
323
324

            // Do not update the control value on first patch (values come from the server)
            if (this.firstPatchDone === true) {
              waitTranscodingControl.setValue(true)
            }
325
326
327
328
          }

          scheduleControl.updateValueAndValidity()
          waitTranscodingControl.updateValueAndValidity()
Chocobozzz's avatar
Chocobozzz committed
329
330
331

          this.firstPatchDone = true

332
333
334
335
336
        }
      )
  }

  private trackChannelChange () {
337
    // We will update the "support" field depending on the channel
338
    this.form.controls[ 'channelId' ]
339
340
341
342
      .valueChanges
      .pipe(map(res => parseInt(res.toString(), 10)))
      .subscribe(
        newChannelId => {
343
          const oldChannelId = parseInt(this.form.value[ 'channelId' ], 10)
344
345
346
347

          // Not initialized yet
          if (isNaN(newChannelId)) return
          const newChannel = this.userVideoChannels.find(c => c.id === newChannelId)
Chocobozzz's avatar
Chocobozzz committed
348
          if (!newChannel) return
349

Chocobozzz's avatar
Chocobozzz committed
350
351
352
          // Wait support field update
          setTimeout(() => {
            const currentSupport = this.form.value[ 'support' ]
353

Chocobozzz's avatar
Chocobozzz committed
354
            // First time we set the channel?
355
356
357
358
359
360
            if (isNaN(oldChannelId)) {
              // Fill support if it's empty
              if (!currentSupport) this.updateSupportField(newChannel.support)

              return
            }
Chocobozzz's avatar
Chocobozzz committed
361
362
363
364
365
366

            const oldChannel = this.userVideoChannels.find(c => c.id === oldChannelId)
            if (!newChannel || !oldChannel) {
              console.error('Cannot find new or old channel.')
              return
            }
367

Chocobozzz's avatar
Chocobozzz committed
368
369
370
            // If the current support text is not the same than the old channel, the user updated it.
            // We don't want the user to lose his text, so stop here
            if (currentSupport && currentSupport !== oldChannel.support) return
371

Chocobozzz's avatar
Chocobozzz committed
372
373
374
            // Update the support text with our new channel
            this.updateSupportField(newChannel.support)
          })
375
376
        }
      )
377
378
  }

Chocobozzz's avatar
Chocobozzz committed
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
  private trackLivePermanentFieldChange () {
    // We will update the "support" field depending on the channel
    this.form.controls['permanentLive']
      .valueChanges
      .subscribe(
        permanentLive => {
          const saveReplayControl = this.form.controls['saveReplay']

          if (permanentLive === true) {
            saveReplayControl.setValue(false)
            saveReplayControl.disable()
          } else {
            saveReplayControl.enable()
          }
        }
      )
  }

397
398
399
  private updateSupportField (support: string) {
    return this.form.patchValue({ support: support || '' })
  }
400
}