]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - client/src/app/+videos/+video-edit/shared/video-edit.component.ts
Display latest uploaded date for captions
[github/Chocobozzz/PeerTube.git] / client / src / app / +videos / +video-edit / shared / video-edit.component.ts
index aebd53318aea63e9bba48da942ae8b351778e57a..99f8c9034915c33cb8dcdcc49b2623da55929216 100644 (file)
@@ -1,10 +1,11 @@
 import { forkJoin } from 'rxjs'
 import { map } from 'rxjs/operators'
-import { SelectChannelItem } from 'src/types/select-options-item.model'
-import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
-import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms'
+import { SelectChannelItem, SelectOptionsItem } from 'src/types/select-options-item.model'
+import { ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
+import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms'
 import { HooksService, PluginService, ServerService } from '@app/core'
 import { removeElementFromArray } from '@app/helpers'
+import { BuildFormValidator } from '@app/shared/form-validators'
 import {
   VIDEO_CATEGORY_VALIDATOR,
   VIDEO_CHANNEL_VALIDATOR,
@@ -20,10 +21,12 @@ import {
 } from '@app/shared/form-validators/video-validators'
 import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms'
 import { InstanceService } from '@app/shared/shared-instance'
-import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
+import { VideoCaptionEdit, VideoCaptionWithPathEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
+import { PluginInfo } from '@root-helpers/plugins-manager'
 import {
   HTMLServerConfig,
   LiveVideo,
+  LiveVideoLatencyMode,
   RegisterClientFormFieldOptions,
   RegisterClientVideoFieldOptions,
   VideoConstant,
@@ -32,10 +35,14 @@ import {
 } from '@shared/models'
 import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
 import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
+import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component'
 import { VideoEditType } from './video-edit.type'
+import { VideoSource } from '@shared/models/videos/video-source'
+import { logger } from '@root-helpers/logger'
 
 type VideoLanguages = VideoConstant<string> & { group?: string }
 type PluginField = {
+  pluginInfo: PluginInfo
   commonOptions: RegisterClientFormFieldOptions
   videoFormOptions: RegisterClientVideoFieldOptions
 }
@@ -53,16 +60,19 @@ export class VideoEditComponent implements OnInit, OnDestroy {
   @Input() videoToUpdate: VideoDetails
 
   @Input() userVideoChannels: SelectChannelItem[] = []
-  @Input() schedulePublicationPossible = true
+  @Input() forbidScheduledPublication = true
 
-  @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = []
+  @Input() videoCaptions: VideoCaptionWithPathEdit[] = []
+  @Input() videoSource: VideoSource
 
   @Input() waitTranscodingEnabled = true
   @Input() type: VideoEditType
   @Input() liveVideo: LiveVideo
 
   @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent
+  @ViewChild('videoCaptionEditModal', { static: true }) editCaptionModal: VideoCaptionEditModalComponent
 
+  @Output() formBuilt = new EventEmitter<void>()
   @Output() pluginFieldsAdded = new EventEmitter<void>()
 
   // So that it can be accessed in the template
@@ -72,6 +82,23 @@ export class VideoEditComponent implements OnInit, OnDestroy {
   videoCategories: VideoConstant<number>[] = []
   videoLicences: VideoConstant<number>[] = []
   videoLanguages: VideoLanguages[] = []
+  latencyModes: SelectOptionsItem[] = [
+    {
+      id: LiveVideoLatencyMode.SMALL_LATENCY,
+      label: $localize`Small latency`,
+      description: $localize`Reduce latency to ~15s disabling P2P`
+    },
+    {
+      id: LiveVideoLatencyMode.DEFAULT,
+      label: $localize`Default`,
+      description: $localize`Average latency of 30s`
+    },
+    {
+      id: LiveVideoLatencyMode.HIGH_LATENCY,
+      label: $localize`High latency`,
+      description: $localize`Average latency of 60s increasing P2P ratio`
+    }
+  ]
 
   pluginDataFormGroup: FormGroup
 
@@ -100,7 +127,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
     private instanceService: InstanceService,
     private i18nPrimengCalendarService: I18nPrimengCalendarService,
     private ngZone: NgZone,
-    private hooks: HooksService
+    private hooks: HooksService,
+    private cd: ChangeDetectorRef
   ) {
     this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
     this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat()
@@ -109,12 +137,13 @@ export class VideoEditComponent implements OnInit, OnDestroy {
   updateForm () {
     const defaultValues: any = {
       nsfw: 'false',
-      commentsEnabled: 'true',
-      downloadEnabled: 'true',
+      commentsEnabled: this.serverConfig.defaults.publish.commentsEnabled,
+      downloadEnabled: this.serverConfig.defaults.publish.downloadEnabled,
       waitTranscoding: 'true',
+      licence: this.serverConfig.defaults.publish.licence,
       tags: []
     }
-    const obj: any = {
+    const obj: { [ id: string ]: BuildFormValidator } = {
       name: VIDEO_NAME_VALIDATOR,
       privacy: VIDEO_PRIVACY_VALIDATOR,
       channelId: VIDEO_CHANNEL_VALIDATOR,
@@ -133,10 +162,11 @@ export class VideoEditComponent implements OnInit, OnDestroy {
       originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR,
       liveStreamKey: null,
       permanentLive: null,
+      latencyMode: null,
       saveReplay: null
     }
 
-    this.formValidatorService.updateForm(
+    this.formValidatorService.updateFormGroup(
       this.form,
       this.formErrors,
       this.validationMessages,
@@ -153,10 +183,13 @@ export class VideoEditComponent implements OnInit, OnDestroy {
 
     this.trackChannelChange()
     this.trackPrivacyChange()
-    this.trackLivePermanentFieldChange()
+
+    this.formBuilt.emit()
   }
 
   ngOnInit () {
+    this.serverConfig = this.serverService.getHTMLConfig()
+
     this.updateForm()
 
     this.pluginService.ensurePluginsAreLoaded('video-edit')
@@ -175,9 +208,11 @@ export class VideoEditComponent implements OnInit, OnDestroy {
       .subscribe(res => {
         this.videoLanguages = res.languages
           .map(l => {
+            if (l.id === 'zxx') return { ...l, group: $localize`Other`, groupOrder: 1 }
+
             return res.about.instance.languages.includes(l.id)
               ? { ...l, group: $localize`Instance languages`, groupOrder: 0 }
-              : { ...l, group: $localize`All languages`, groupOrder: 1 }
+              : { ...l, group: $localize`All languages`, groupOrder: 2 }
           })
           .sort((a, b) => a.groupOrder - b.groupOrder)
       })
@@ -185,16 +220,17 @@ export class VideoEditComponent implements OnInit, OnDestroy {
     this.serverService.getVideoPrivacies()
       .subscribe(privacies => {
         this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies).videoPrivacies
-        if (this.schedulePublicationPossible) {
-          this.videoPrivacies.push({
-            id: this.SPECIAL_SCHEDULED_PRIVACY,
-            label: $localize`Scheduled`,
-            description: $localize`Hide the video until a specific date`
-          })
-        }
-      })
 
-    this.serverConfig = this.serverService.getHTMLConfig()
+        // Can't schedule publication if private privacy is not available (could be deleted by a plugin)
+        const hasPrivatePrivacy = this.videoPrivacies.some(p => p.id === VideoPrivacy.PRIVATE)
+        if (this.forbidScheduledPublication || !hasPrivatePrivacy) return
+
+        this.videoPrivacies.push({
+          id: this.SPECIAL_SCHEDULED_PRIVACY,
+          label: $localize`Scheduled`,
+          description: $localize`Hide the video until a specific date`
+        })
+      })
 
     this.initialVideoCaptions = this.videoCaptions.map(c => c.language.id)
 
@@ -215,12 +251,12 @@ export class VideoEditComponent implements OnInit, OnDestroy {
                .map(c => c.language.id)
   }
 
-  onCaptionAdded (caption: VideoCaptionEdit) {
+  onCaptionEdited (caption: VideoCaptionEdit) {
     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' })
+      Object.assign(existingCaption, caption)
     } else {
       this.videoCaptions.push(
         Object.assign(caption, { action: 'CREATE' as 'CREATE' })
@@ -230,15 +266,15 @@ export class VideoEditComponent implements OnInit, OnDestroy {
     this.sortVideoCaptions()
   }
 
-  async deleteCaption (caption: VideoCaptionEdit) {
+  deleteCaption (caption: VideoCaptionEdit) {
     // Caption recovers his former state
-    if (caption.action && this.initialVideoCaptions.indexOf(caption.language.id) !== -1) {
+    if (caption.action && this.initialVideoCaptions.includes(caption.language.id)) {
       caption.action = undefined
       return
     }
 
     // This caption is not on the server, just remove it from our array
-    if (caption.action === 'CREATE') {
+    if (caption.action === 'CREATE' || caption.action === 'UPDATE') {
       removeElementFromArray(this.videoCaptions, caption)
       return
     }
@@ -258,6 +294,10 @@ export class VideoEditComponent implements OnInit, OnDestroy {
     return this.form.value['permanentLive'] === true
   }
 
+  isLatencyModeEnabled () {
+    return this.serverConfig.live.latencySetting.enabled
+  }
+
   isPluginFieldHidden (pluginField: PluginField) {
     if (typeof pluginField.commonOptions.hidden !== 'function') return false
 
@@ -268,6 +308,14 @@ export class VideoEditComponent implements OnInit, OnDestroy {
     })
   }
 
+  getPluginsFields (tab: 'main' | 'plugin-settings') {
+    return this.pluginFields.filter(p => {
+      const wanted = p.videoFormOptions.tab ?? 'plugin-settings'
+
+      return wanted === tab
+    })
+  }
+
   private sortVideoCaptions () {
     this.videoCaptions.sort((v1, v2) => {
       if (v1.language.label < v2.language.label) return -1
@@ -277,26 +325,61 @@ export class VideoEditComponent implements OnInit, OnDestroy {
     })
   }
 
-  private updatePluginFields () {
+  private async updatePluginFields () {
     this.pluginFields = this.pluginService.getRegisteredVideoFormFields(this.type)
 
     if (this.pluginFields.length === 0) return
 
-    const obj: any = {}
+    const pluginObj: { [ id: string ]: BuildFormValidator } = {}
+    const pluginValidationMessages: FormReactiveValidationMessages = {}
+    const pluginFormErrors: any = {}
+    const pluginDefaults: any = {}
 
     for (const setting of this.pluginFields) {
-      obj[setting.commonOptions.name] = new FormControl(setting.commonOptions.default)
+      await this.pluginService.translateSetting(setting.pluginInfo.plugin.npmName, setting.commonOptions)
+
+      const validator = async (control: AbstractControl) => {
+        if (!setting.commonOptions.error) return null
+
+        const error = await setting.commonOptions.error({ formValues: this.form.value, value: control.value })
+
+        return error?.error ? { [setting.commonOptions.name]: error.text } : null
+      }
+
+      const name = setting.commonOptions.name
+
+      pluginObj[name] = {
+        ASYNC_VALIDATORS: [ validator ],
+        VALIDATORS: [],
+        MESSAGES: {}
+      }
+
+      pluginDefaults[name] = setting.commonOptions.default
     }
 
-    this.pluginDataFormGroup = new FormGroup(obj)
+    this.pluginDataFormGroup = new FormGroup({})
+    this.formValidatorService.updateFormGroup(
+      this.pluginDataFormGroup,
+      pluginFormErrors,
+      pluginValidationMessages,
+      pluginObj,
+      pluginDefaults
+    )
+
     this.form.addControl('pluginData', this.pluginDataFormGroup)
+    this.formErrors['pluginData'] = pluginFormErrors
+    this.validationMessages['pluginData'] = pluginValidationMessages
 
+    this.cd.detectChanges()
     this.pluginFieldsAdded.emit()
+
+    // Plugins may need other control values to calculate potential errors
+    this.form.valueChanges.subscribe(() => this.formValidatorService.updateTreeValidity(this.pluginDataFormGroup))
   }
 
   private trackPrivacyChange () {
     // We will update the schedule input and the wait transcoding checkbox validators
-    this.form.controls[ 'privacy' ]
+    this.form.controls['privacy']
       .valueChanges
       .pipe(map(res => parseInt(res.toString(), 10)))
       .subscribe(
@@ -335,12 +418,12 @@ export class VideoEditComponent implements OnInit, OnDestroy {
 
   private trackChannelChange () {
     // We will update the "support" field depending on the channel
-    this.form.controls[ 'channelId' ]
+    this.form.controls['channelId']
       .valueChanges
       .pipe(map(res => parseInt(res.toString(), 10)))
       .subscribe(
         newChannelId => {
-          const oldChannelId = parseInt(this.form.value[ 'channelId' ], 10)
+          const oldChannelId = parseInt(this.form.value['channelId'], 10)
 
           // Not initialized yet
           if (isNaN(newChannelId)) return
@@ -349,7 +432,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
 
           // Wait support field update
           setTimeout(() => {
-            const currentSupport = this.form.value[ 'support' ]
+            const currentSupport = this.form.value['support']
 
             // First time we set the channel?
             if (isNaN(oldChannelId)) {
@@ -361,7 +444,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
 
             const oldChannel = this.userVideoChannels.find(c => c.id === oldChannelId)
             if (!newChannel || !oldChannel) {
-              console.error('Cannot find new or old channel.')
+              logger.error('Cannot find new or old channel.')
               return
             }
 
@@ -376,24 +459,6 @@ export class VideoEditComponent implements OnInit, OnDestroy {
       )
   }
 
-  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()
-          }
-        }
-      )
-  }
-
   private updateSupportField (support: string) {
     return this.form.patchValue({ support: support || '' })
   }