]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add ability to set custom field to video form
authorChocobozzz <me@florianbigard.com>
Thu, 20 Aug 2020 14:18:16 +0000 (16:18 +0200)
committerChocobozzz <chocobozzz@cpy.re>
Fri, 21 Aug 2020 13:39:51 +0000 (15:39 +0200)
30 files changed:
client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html
client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss
client/src/app/+videos/+video-edit/shared/video-edit.component.html
client/src/app/+videos/+video-edit/shared/video-edit.component.scss
client/src/app/+videos/+video-edit/shared/video-edit.component.ts
client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html
client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html
client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
client/src/app/+videos/+video-edit/video-update.component.html
client/src/app/+videos/+video-edit/video-update.component.ts
client/src/app/core/plugins/plugin.service.ts
client/src/app/shared/shared-forms/dynamic-form-field.component.html [new file with mode: 0644]
client/src/app/shared/shared-forms/dynamic-form-field.component.scss [new file with mode: 0644]
client/src/app/shared/shared-forms/dynamic-form-field.component.ts [new file with mode: 0644]
client/src/app/shared/shared-forms/shared-form.module.ts
client/src/app/shared/shared-main/video/video-edit.model.ts
client/src/app/shared/shared-main/video/video.model.ts
client/src/app/shared/shared-main/video/video.service.ts
client/src/root-helpers/plugins.ts
client/src/standalone/videos/embed.ts
client/src/types/register-client-option.model.ts
server/controllers/api/videos/index.ts
server/models/video/video-format-utils.ts
shared/models/plugins/client-hook.model.ts
shared/models/plugins/index.ts
shared/models/plugins/plugin-client-scope.type.ts
shared/models/plugins/register-client-form-field.model.ts [new file with mode: 0644]
shared/models/plugins/register-server-setting.model.ts
shared/models/videos/video-update.model.ts
shared/models/videos/video.model.ts

index f3fc429ff65aff0d67a1da022c240c2e48b8919e..cb2894568d11129840e71ca351c8b2f304ef1e49 100644 (file)
@@ -7,38 +7,7 @@
 
   <form *ngIf="hasRegisteredSettings()" role="form" (ngSubmit)="formValidated()" [formGroup]="form">
     <div class="form-group" *ngFor="let setting of registeredSettings">
-      <label *ngIf="setting.type !== 'input-checkbox'" [attr.for]="setting.name" [innerHTML]="setting.label"></label>
-
-      <input *ngIf="setting.type === 'input'" type="text" [id]="setting.name" [formControlName]="setting.name" />
-
-      <textarea *ngIf="setting.type === 'input-textarea'" type="text" [id]="setting.name" [formControlName]="setting.name"></textarea>
-
-      <my-help *ngIf="setting.type === 'markdown-text'" helpType="markdownText"></my-help>
-
-      <my-help *ngIf="setting.type === 'markdown-enhanced'" helpType="markdownEnhanced"></my-help>
-
-      <my-markdown-textarea
-        *ngIf="setting.type === 'markdown-text'"
-        markdownType="text" [id]="setting.name" [formControlName]="setting.name" textareaWidth="500px"
-        [classes]="{ 'input-error': formErrors['settings.name'] }"
-      ></my-markdown-textarea>
-
-      <my-markdown-textarea
-        *ngIf="setting.type === 'markdown-enhanced'"
-        markdownType="enhanced" [id]="setting.name" [formControlName]="setting.name" textareaWidth="500px"
-        [classes]="{ 'input-error': formErrors['settings.name'] }"
-      ></my-markdown-textarea>
-
-      <my-peertube-checkbox
-        *ngIf="setting.type === 'input-checkbox'"
-        [id]="setting.name"
-        [formControlName]="setting.name"
-        [labelInnerHTML]="setting.label"
-      ></my-peertube-checkbox>
-
-      <div *ngIf="formErrors[setting.name]" class="form-error">
-        {{ formErrors[setting.name] }}
-      </div>
+      <my-dynamic-form-field [form]="form" [setting]="setting" [formErrors]="formErrors"></my-dynamic-form-field>
     </div>
 
     <input type="submit" i18n value="Update plugin settings" [disabled]="!form.valid">
index cc35aec57d97bcd2580d1ebf32cceda0e06ff13e..5ab6e5f1b4ce18a07ad67a11f60722d4572d0712 100644 (file)
@@ -5,22 +5,6 @@ h2 {
   margin-bottom: 20px;
 }
 
-input:not([type=submit]) {
-  @include peertube-input-text(340px);
-
-  display: block;
-}
-
-textarea {
-  @include peertube-textarea(340px, 200px);
-
-  display: block;
-}
-
-.peertube-select-container {
-  @include peertube-select-container(340px);
-}
-
 input[type=submit], button {
   @include peertube-button;
   @include orange-button;
index ae3413e79b8223b9ad03ea6ac11c20a651a5e097..842997b209869b623a6cbcf5a878e4514e8d46d3 100644 (file)
       </ng-template>
     </ng-container>
 
+    <ng-container ngbNavItem *ngIf="pluginFields.length !== 0">
+      <a ngbNavLink i18n>Plugin settings</a>
+
+      <ng-template ngbNavContent>
+        <div class="row plugin-settings">
+
+          <div class="col-md-12 col-xl-8">
+            <div *ngFor="let pluginSetting of pluginFields" class="form-group">
+              <my-dynamic-form-field [form]="pluginDataFormGroup" [formErrors]="formErrors" [setting]="pluginSetting.commonOptions"></my-dynamic-form-field>
+            </div>
+          </div>
+
+        </div>
+      </ng-template>
+    </ng-container>
   </div>
 
   <div [ngbNavOutlet]="nav"></div>
index 9caf009c5c28bfe441818a3dd06f9bdffd0a5341..3082a4f72bfdbfb37788b62db9e2b26ed6612680 100644 (file)
@@ -7,7 +7,8 @@
 @import 'variables';
 @import 'mixins';
 
-label {
+label,
+my-dynamic-form-field ::ng-deep label {
   font-weight: $font-regular;
   font-size: 100%;
 }
index 92d06aa12414c58f44948a0e03d2cd3d9bb70c27..f04111e69488f7c7a18d8f3866eb4e80ebbc6ba8 100644 (file)
@@ -1,8 +1,8 @@
 import { forkJoin } from 'rxjs'
 import { map } from 'rxjs/operators'
-import { Component, Input, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
+import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
 import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'
-import { ServerService } from '@app/core'
+import { HooksService, PluginService, ServerService } from '@app/core'
 import { removeElementFromArray } from '@app/helpers'
 import {
   VIDEO_CATEGORY_VALIDATOR,
@@ -21,6 +21,7 @@ import { FormReactiveValidationMessages, FormValidatorService, SelectChannelItem
 import { InstanceService } from '@app/shared/shared-instance'
 import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
 import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models'
+import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
 import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
 import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
 
@@ -39,9 +40,12 @@ export class VideoEditComponent implements OnInit, OnDestroy {
   @Input() schedulePublicationPossible = true
   @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = []
   @Input() waitTranscodingEnabled = true
+  @Input() type: 'import-url' | 'import-torrent' | 'upload' | 'update'
 
   @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent
 
+  @Output() pluginFieldsAdded = new EventEmitter<void>()
+
   // So that it can be accessed in the template
   readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
 
@@ -53,6 +57,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
   tagValidators: ValidatorFn[]
   tagValidatorsMessages: { [ name: string ]: string }
 
+  pluginDataFormGroup: FormGroup
+
   schedulePublicationEnabled = false
 
   calendarLocale: any = {}
@@ -64,6 +70,11 @@ export class VideoEditComponent implements OnInit, OnDestroy {
 
   serverConfig: ServerConfig
 
+  pluginFields: {
+    commonOptions: RegisterClientFormFieldOptions
+    videoFormOptions: RegisterClientVideoFieldOptions
+  }[] = []
+
   private schedulerInterval: any
   private firstPatchDone = false
   private initialVideoCaptions: string[] = []
@@ -72,9 +83,11 @@ export class VideoEditComponent implements OnInit, OnDestroy {
     private formValidatorService: FormValidatorService,
     private videoService: VideoService,
     private serverService: ServerService,
+    private pluginService: PluginService,
     private instanceService: InstanceService,
     private i18nPrimengCalendarService: I18nPrimengCalendarService,
-    private ngZone: NgZone
+    private ngZone: NgZone,
+    private hooks: HooksService
   ) {
     this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale()
     this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
@@ -136,19 +149,26 @@ export class VideoEditComponent implements OnInit, OnDestroy {
   ngOnInit () {
     this.updateForm()
 
+    this.pluginService.ensurePluginsAreLoaded('video-edit')
+      .then(() => this.updatePluginFields())
+
     this.serverService.getVideoCategories()
         .subscribe(res => this.videoCategories = res)
+
     this.serverService.getVideoLicences()
         .subscribe(res => this.videoLicences = res)
+
     forkJoin([
       this.instanceService.getAbout(),
       this.serverService.getVideoLanguages()
     ]).pipe(map(([ about, languages ]) => ({ about, languages })))
       .subscribe(res => {
         this.videoLanguages = res.languages
-          .map(l => res.about.instance.languages.includes(l.id)
-            ? { ...l, group: $localize`Instance languages`, groupOrder: 0 }
-            : { ...l, group: $localize`All languages`, groupOrder: 1 })
+          .map(l => {
+            return res.about.instance.languages.includes(l.id)
+              ? { ...l, group: $localize`Instance languages`, groupOrder: 0 }
+              : { ...l, group: $localize`All languages`, groupOrder: 1 }
+          })
           .sort((a, b) => a.groupOrder - b.groupOrder)
       })
 
@@ -173,6 +193,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
     this.ngZone.runOutsideAngular(() => {
       this.schedulerInterval = setInterval(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute
     })
+
+    this.hooks.runAction('action:video-edit.init', 'video-edit', { type: this.type })
   }
 
   ngOnDestroy () {
@@ -223,6 +245,23 @@ export class VideoEditComponent implements OnInit, OnDestroy {
     })
   }
 
+  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()
+  }
+
   private trackPrivacyChange () {
     // We will update the schedule input and the wait transcoding checkbox validators
     this.form.controls[ 'privacy' ]
index 825cb6df4b576f79c250865f4d9c706067eda000..785514c76505bd53c7420cc281347ebbff8f4aab 100644 (file)
@@ -58,6 +58,7 @@
   <my-video-edit
     [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false"
     [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
+    type="import-torrent"
   ></my-video-edit>
 
   <div class="submit-container">
index 2107dc9d0c6abfbe60720a588165aaa6927d2da2..3e4eb5fbc2fe2fd61995b1a8720ca0ca215b116a 100644 (file)
@@ -54,6 +54,7 @@
   <my-video-edit
     [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false"
     [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
+    type="import-url"
   ></my-video-edit>
 
   <div class="submit-container">
index ed697c25bd7a1f6fd896c13f5f8885e046eeeffb..677fa119780cb99fb1f48952bd85d5c729b8060c 100644 (file)
@@ -69,6 +69,7 @@
     [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
     [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
     [waitTranscodingEnabled]="waitTranscodingEnabled"
+    type="upload"
   ></my-video-edit>
 
   <div class="submit-container">
index 6c12393955a5ef6cd4f3e901aac815cd92dd47d1..b375963990b28f19ff0509904b9aae9c3a831508 100644 (file)
       [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible"
       [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
       [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled"
+      type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()"
     ></my-video-edit>
 
     <div class="submit-container">
       <my-button className="orange-button" i18n-label label="Update" icon="circle-tick"
-                 (click)="update()" (keydown.enter)="update()" 
+                 (click)="update()" (keydown.enter)="update()"
                  [disabled]="!form.valid || isUpdatingVideo === true"
       ></my-button>
     </div>
index 2e1d0f89d59f8d8ea644e45f289c08463f966581..20438a2d3566a549ea9e0ddf500b76e6e1d2ea73 100644 (file)
@@ -126,6 +126,14 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
         )
   }
 
+  hydratePluginFieldsFromVideo () {
+    if (!this.video.pluginData) return
+
+    this.form.patchValue({
+      pluginData: this.video.pluginData
+    })
+  }
+
   private hydrateFormFromVideo () {
     this.form.patchValue(this.video.toFormPatch())
 
index 871613b89f6712f86a04f4e84eba6c9902106f74..4e44a18658329aad439aecf4b90500ca6ef642b7 100644 (file)
@@ -9,7 +9,7 @@ import { RestExtractor } from '@app/core/rest'
 import { ServerService } from '@app/core/server/server.service'
 import { getDevLocale, isOnDevLocale } from '@app/helpers'
 import { CustomModalComponent } from '@app/modal/custom-modal.component'
-import { Hooks, loadPlugin, PluginInfo, runHook } from '@root-helpers/plugins'
+import { FormFields, Hooks, loadPlugin, PluginInfo, runHook } from '@root-helpers/plugins'
 import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n'
 import {
   ClientHook,
@@ -36,6 +36,7 @@ export class PluginService implements ClientHook {
     'video-watch': new ReplaySubject<boolean>(1),
     signup: new ReplaySubject<boolean>(1),
     login: new ReplaySubject<boolean>(1),
+    'video-edit': new ReplaySubject<boolean>(1),
     embed: new ReplaySubject<boolean>(1)
   }
 
@@ -50,6 +51,9 @@ export class PluginService implements ClientHook {
   private loadingScopes: { [id in PluginClientScope]?: boolean } = {}
 
   private hooks: Hooks = {}
+  private formFields: FormFields = {
+    video: []
+  }
 
   constructor (
     private authService: AuthService,
@@ -188,9 +192,18 @@ export class PluginService implements ClientHook {
       : PluginType.THEME
   }
 
+  getRegisteredVideoFormFields (type: 'import-url' | 'import-torrent' | 'upload' | 'update') {
+    return this.formFields.video.filter(f => f.videoFormOptions.type === type)
+  }
+
   private loadPlugin (pluginInfo: PluginInfo) {
     return this.zone.runOutsideAngular(() => {
-      return loadPlugin(this.hooks, pluginInfo, pluginInfo => this.buildPeerTubeHelpers(pluginInfo))
+      return loadPlugin({
+        hooks: this.hooks,
+        formFields: this.formFields,
+        pluginInfo,
+        peertubeHelpersFactory: pluginInfo => this.buildPeerTubeHelpers(pluginInfo)
+      })
     })
   }
 
diff --git a/client/src/app/shared/shared-forms/dynamic-form-field.component.html b/client/src/app/shared/shared-forms/dynamic-form-field.component.html
new file mode 100644 (file)
index 0000000..c111ea7
--- /dev/null
@@ -0,0 +1,35 @@
+<div [formGroup]="form">
+  <label *ngIf="setting.type !== 'input-checkbox'" [attr.for]="setting.name" [innerHTML]="setting.label"></label>
+
+  <input *ngIf="setting.type === 'input'" type="text" [id]="setting.name" [formControlName]="setting.name" />
+
+  <textarea *ngIf="setting.type === 'input-textarea'" type="text" [id]="setting.name" [formControlName]="setting.name"></textarea>
+
+  <my-help *ngIf="setting.type === 'markdown-text'" helpType="markdownText"></my-help>
+
+  <my-help *ngIf="setting.type === 'markdown-enhanced'" helpType="markdownEnhanced"></my-help>
+
+  <my-markdown-textarea
+    *ngIf="setting.type === 'markdown-text'"
+    markdownType="text" [id]="setting.name" [formControlName]="setting.name" textareaWidth="500px"
+    [classes]="{ 'input-error': formErrors['settings.name'] }"
+  ></my-markdown-textarea>
+
+  <my-markdown-textarea
+    *ngIf="setting.type === 'markdown-enhanced'"
+    markdownType="enhanced" [id]="setting.name" [formControlName]="setting.name" textareaWidth="500px"
+    [classes]="{ 'input-error': formErrors['settings.name'] }"
+  ></my-markdown-textarea>
+
+  <my-peertube-checkbox
+    *ngIf="setting.type === 'input-checkbox'"
+    [id]="setting.name"
+    [formControlName]="setting.name"
+    [labelInnerHTML]="setting.label"
+  ></my-peertube-checkbox>
+
+  <div *ngIf="formErrors[setting.name]" class="form-error">
+    {{ formErrors[setting.name] }}
+  </div>
+
+</div>
diff --git a/client/src/app/shared/shared-forms/dynamic-form-field.component.scss b/client/src/app/shared/shared-forms/dynamic-form-field.component.scss
new file mode 100644 (file)
index 0000000..70b3cf6
--- /dev/null
@@ -0,0 +1,18 @@
+@import '_variables';
+@import '_mixins';
+
+input:not([type=submit]) {
+  @include peertube-input-text(340px);
+
+  display: block;
+}
+
+textarea {
+  @include peertube-textarea(340px, 200px);
+
+  display: block;
+}
+
+.peertube-select-container {
+  @include peertube-select-container(340px);
+}
diff --git a/client/src/app/shared/shared-forms/dynamic-form-field.component.ts b/client/src/app/shared/shared-forms/dynamic-form-field.component.ts
new file mode 100644 (file)
index 0000000..b638907
--- /dev/null
@@ -0,0 +1,15 @@
+import { Component, Input } from '@angular/core'
+import { FormGroup } from '@angular/forms'
+import { RegisterClientFormFieldOptions } from '@shared/models'
+
+@Component({
+  selector: 'my-dynamic-form-field',
+  templateUrl: './dynamic-form-field.component.html',
+  styleUrls: [ './dynamic-form-field.component.scss' ]
+})
+
+export class DynamicFormFieldComponent {
+  @Input() form: FormGroup
+  @Input() formErrors: any
+  @Input() setting: RegisterClientFormFieldOptions
+}
index 1946ac21f078aabe3547e7b66fd8a5a87ad2c1f9..a28988f87b41f34753675994dac46c961fd21f6c 100644 (file)
@@ -15,6 +15,7 @@ import { ReactiveFileComponent } from './reactive-file.component'
 import { SelectChannelComponent, SelectCheckboxComponent, SelectOptionsComponent, SelectTagsComponent } from './select'
 import { TextareaAutoResizeDirective } from './textarea-autoresize.directive'
 import { TimestampInputComponent } from './timestamp-input.component'
+import { DynamicFormFieldComponent } from './dynamic-form-field.component'
 
 @NgModule({
   imports: [
@@ -41,7 +42,9 @@ import { TimestampInputComponent } from './timestamp-input.component'
     SelectChannelComponent,
     SelectOptionsComponent,
     SelectTagsComponent,
-    SelectCheckboxComponent
+    SelectCheckboxComponent,
+
+    DynamicFormFieldComponent
   ],
 
   exports: [
@@ -63,7 +66,9 @@ import { TimestampInputComponent } from './timestamp-input.component'
     SelectChannelComponent,
     SelectOptionsComponent,
     SelectTagsComponent,
-    SelectCheckboxComponent
+    SelectCheckboxComponent,
+
+    DynamicFormFieldComponent
   ],
 
   providers: [
index 6a529e0523e934e7a9328c65998b13e7fee89816..757b686c01149d31326839cd11f73cfa3bc84769 100644 (file)
@@ -25,6 +25,8 @@ export class VideoEdit implements VideoUpdate {
   scheduleUpdate?: VideoScheduleUpdate
   originallyPublishedAt?: Date | string
 
+  pluginData?: any
+
   constructor (
     video?: Video & {
       tags: string[],
@@ -55,10 +57,12 @@ export class VideoEdit implements VideoUpdate {
 
       this.scheduleUpdate = video.scheduledUpdate
       this.originallyPublishedAt = video.originallyPublishedAt ? new Date(video.originallyPublishedAt) : null
+
+      this.pluginData = video.pluginData
     }
   }
 
-  patch (values: { [ id: string ]: string }) {
+  patch (values: { [ id: string ]: any }) {
     Object.keys(values).forEach((key) => {
       this[ key ] = values[ key ]
     })
index 73f0198e20e8c7c9eecfa97c9db1db3e40522377..0dca3da0d5544c9d4244045214772c4cb9479fac 100644 (file)
@@ -84,6 +84,8 @@ export class Video implements VideoServerModel {
     currentTime: number
   }
 
+  pluginData?: any
+
   static buildClientUrl (videoUUID: string) {
     return '/videos/watch/' + videoUUID
   }
@@ -152,6 +154,8 @@ export class Video implements VideoServerModel {
 
     this.originInstanceHost = this.account.host
     this.originInstanceUrl = 'https://' + this.originInstanceHost
+
+    this.pluginData = hash.pluginData
   }
 
   isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
index 48aff82b4886434e370eaf483a21805d781c154f..8a688c8edcdc43e73ebe9596524c70616faeafe9 100644 (file)
@@ -96,6 +96,7 @@ export class VideoService implements VideosProvider {
       downloadEnabled: video.downloadEnabled,
       thumbnailfile: video.thumbnailfile,
       previewfile: video.previewfile,
+      pluginData: video.pluginData,
       scheduleUpdate,
       originallyPublishedAt
     }
index 011721761682ee2a388c87a15ed74903967adcd8..4bc2c8eb212fedb210b7737701f4fcc4088ba1ac 100644 (file)
@@ -1,6 +1,14 @@
-import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
-import { ClientHookName, ClientScript, RegisterClientHookOptions, ServerConfigPlugin, PluginType, clientHookObject } from '../../../shared/models'
 import { RegisterClientHelpers } from 'src/types/register-client-option.model'
+import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
+import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
+import {
+  ClientHookName,
+  clientHookObject,
+  ClientScript,
+  PluginType,
+  RegisterClientHookOptions,
+  ServerConfigPlugin
+} from '../../../shared/models'
 import { ClientScript as ClientScriptModule } from '../types/client-script.model'
 import { importModule } from './utils'
 
@@ -18,6 +26,13 @@ type PluginInfo = {
   isTheme: boolean
 }
 
+type FormFields = {
+  video: {
+    commonOptions: RegisterClientFormFieldOptions
+    videoFormOptions: RegisterClientVideoFieldOptions
+  }[]
+}
+
 async function runHook<T> (hooks: Hooks, hookName: ClientHookName, result?: T, params?: any) {
   if (!hooks[hookName]) return result
 
@@ -34,7 +49,13 @@ async function runHook<T> (hooks: Hooks, hookName: ClientHookName, result?: T, p
   return result
 }
 
-function loadPlugin (hooks: Hooks, pluginInfo: PluginInfo, peertubeHelpersFactory: (pluginInfo: PluginInfo) => RegisterClientHelpers) {
+function loadPlugin (options: {
+  hooks: Hooks
+  pluginInfo: PluginInfo
+  peertubeHelpersFactory: (pluginInfo: PluginInfo) => RegisterClientHelpers
+  formFields?: FormFields
+}) {
+  const { hooks, pluginInfo, peertubeHelpersFactory, formFields } = options
   const { plugin, clientScript } = pluginInfo
 
   const registerHook = (options: RegisterClientHookOptions) => {
@@ -54,12 +75,23 @@ function loadPlugin (hooks: Hooks, pluginInfo: PluginInfo, peertubeHelpersFactor
     })
   }
 
+  const registerVideoField = (commonOptions: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) => {
+    if (!formFields) {
+      throw new Error('Video field registration is not supported')
+    }
+
+    formFields.video.push({
+      commonOptions,
+      videoFormOptions
+    })
+  }
+
   const peertubeHelpers = peertubeHelpersFactory(pluginInfo)
 
   console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name)
 
   return importModule(clientScript.script)
-    .then((script: ClientScriptModule) => script.register({ registerHook, peertubeHelpers }))
+    .then((script: ClientScriptModule) => script.register({ registerHook, registerVideoField, peertubeHelpers }))
     .then(() => sortHooksByPriority(hooks))
     .catch(err => console.error('Cannot import or register plugin %s.', pluginInfo.plugin.name, err))
 }
@@ -68,6 +100,7 @@ export {
   HookStructValue,
   Hooks,
   PluginInfo,
+  FormFields,
   loadPlugin,
   runHook
 }
index fe65794f71f7d24e10f021c5c4c378819a144dcb..c79471005f7e3cda4862f561f5c7a0319ffe37c7 100644 (file)
@@ -750,7 +750,11 @@ export class PeerTubeEmbed {
           isTheme: false
         }
 
-        await loadPlugin(this.peertubeHooks, pluginInfo, _ => this.buildPeerTubeHelpers(translations))
+        await loadPlugin({
+          hooks: this.peertubeHooks,
+          pluginInfo,
+          peertubeHelpersFactory: _ => this.buildPeerTubeHelpers(translations)
+        })
       }
     }
   }
index dff00e9dd93d924c4e6d98f32667dbc4bca5be79..e3c6d803d476d165f3d8464a00f6f9efa307c471 100644 (file)
@@ -1,8 +1,11 @@
+import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
 import { RegisterClientHookOptions } from '@shared/models/plugins/register-client-hook.model'
 
 export type RegisterClientOptions = {
   registerHook: (options: RegisterClientHookOptions) => void
 
+  registerVideoField: (commonOptions: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) => void
+
   peertubeHelpers: RegisterClientHelpers
 }
 
index c05acfd2fccaca2cb4c9d997924a4e49b4d8b1cf..15b6f214f043a40c84196c819c1bb85d04940261 100644 (file)
@@ -414,7 +414,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
       Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated)
     }
 
-    Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated })
+    Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body })
   } catch (err) {
     // Force fields we want to update
     // If the transaction is retried, sequelize will think the object has not changed
index 9b6509dfd6540b6df68078adcf01cf38b5ea7da4..7a17c839f5f88082ffb7bfe05429b730b5d31175 100644 (file)
@@ -78,7 +78,10 @@ function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFor
 
     userHistory: userHistory ? {
       currentTime: userHistory.currentTime
-    } : undefined
+    } : undefined,
+
+    // Can be added by external plugins
+    pluginData: (video as any).pluginData
   }
 
   if (options) {
index 193a3f6469fdd1da07ea2f4bda2634f9acbfc48e..7b7144676f2097a685372774d62efbed4392be95 100644 (file)
@@ -70,6 +70,9 @@ export const clientActionHookObject = {
   // Fired when a user click on 'View x replies' and they're loaded
   'action:video-watch.video-thread-replies.loaded': true,
 
+  // Fired when the video edit page (upload, URL/torrent import, update) is being initialized
+  'action:video-edit.init': true,
+
   // Fired when the login page is being initialized
   'action:login.init': true,
 
index 209fca7918e6b93755478487e78dbe9f4c621d68..83ed6f583be5c4a78e1014d6d19e68689a165075 100644 (file)
@@ -19,6 +19,7 @@ export * from './plugin-video-privacy-manager.model'
 export * from './plugin.type'
 export * from './public-server.setting'
 export * from './register-client-hook.model'
+export * from './register-client-form-field.model'
 export * from './register-server-hook.model'
 export * from './register-server-setting.model'
 export * from './server-hook.model'
index a3c669fe792f4ff86355d92454d8950e7ad53942..e188ce1003bd3cd77bddc68816e38c519af2ef58 100644 (file)
@@ -1 +1 @@
-export type PluginClientScope = 'common' | 'video-watch' | 'search' | 'signup' | 'login' | 'embed'
+export type PluginClientScope = 'common' | 'video-watch' | 'search' | 'signup' | 'login' | 'embed' | 'video-edit'
diff --git a/shared/models/plugins/register-client-form-field.model.ts b/shared/models/plugins/register-client-form-field.model.ts
new file mode 100644 (file)
index 0000000..df24339
--- /dev/null
@@ -0,0 +1,12 @@
+export interface RegisterClientFormFieldOptions {
+  name: string
+  label: string
+  type: 'input' | 'input-checkbox' | 'input-textarea' | 'markdown-text' | 'markdown-enhanced'
+
+  // Default setting value
+  default?: string | boolean
+}
+
+export interface RegisterClientVideoFieldOptions {
+  type: 'import-url' | 'import-torrent' | 'update' | 'upload'
+}
index 920c3480fc712e054ec3029e4750bcf89286ee9d..6872dc53e371a776d712218c2482f1deaf9229ef 100644 (file)
@@ -1,15 +1,10 @@
-export interface RegisterServerSettingOptions {
-  name: string
-  label: string
-  type: 'input' | 'input-checkbox' | 'input-textarea' | 'markdown-text' | 'markdown-enhanced'
+import { RegisterClientFormFieldOptions } from './register-client-form-field.model'
 
+export interface RegisterServerSettingOptions extends RegisterClientFormFieldOptions {
   // If the setting is not private, anyone can view its value (client code included)
   // If the setting is private, only server-side hooks can access it
   // Mainly used by the PeerTube client to get admin config
   private: boolean
-
-  // Default setting value
-  default?: string | boolean
 }
 
 export interface RegisteredServerSettings {
index 4ef9041560edc68b1615f41d005e839e5125edfd..86653b959a73b257e4fa2db8b2d1d6a7ff06eda9 100644 (file)
@@ -19,4 +19,6 @@ export interface VideoUpdate {
   previewfile?: Blob
   scheduleUpdate?: VideoScheduleUpdate
   originallyPublishedAt?: Date | string
+
+  pluginData?: any
 }
index 557e66e096ee93f83f826a8111b5eee708380798..158ee8f05f408904128f6c37d9a38d91e52451cd 100644 (file)
@@ -53,6 +53,8 @@ export interface Video {
   userHistory?: {
     currentTime: number
   }
+
+  pluginData?: any
 }
 
 export interface VideoDetails extends Video {