aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-08-20 16:18:16 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-08-21 15:39:51 +0200
commit7294aab0c879ef96c0fde15c389a2c4c1463d3c7 (patch)
treebad1176720ee04266eba5470e9956e3a7871e349
parentf95628636b6ccdf3eae2449ca718e075b072f678 (diff)
downloadPeerTube-7294aab0c879ef96c0fde15c389a2c4c1463d3c7.tar.gz
PeerTube-7294aab0c879ef96c0fde15c389a2c4c1463d3c7.tar.zst
PeerTube-7294aab0c879ef96c0fde15c389a2c4c1463d3c7.zip
Add ability to set custom field to video form
-rw-r--r--client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html33
-rw-r--r--client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss16
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.html15
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.scss3
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.ts51
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html1
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html1
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html1
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.html3
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.ts8
-rw-r--r--client/src/app/core/plugins/plugin.service.ts17
-rw-r--r--client/src/app/shared/shared-forms/dynamic-form-field.component.html35
-rw-r--r--client/src/app/shared/shared-forms/dynamic-form-field.component.scss18
-rw-r--r--client/src/app/shared/shared-forms/dynamic-form-field.component.ts15
-rw-r--r--client/src/app/shared/shared-forms/shared-form.module.ts9
-rw-r--r--client/src/app/shared/shared-main/video/video-edit.model.ts6
-rw-r--r--client/src/app/shared/shared-main/video/video.model.ts4
-rw-r--r--client/src/app/shared/shared-main/video/video.service.ts1
-rw-r--r--client/src/root-helpers/plugins.ts41
-rw-r--r--client/src/standalone/videos/embed.ts6
-rw-r--r--client/src/types/register-client-option.model.ts3
-rw-r--r--server/controllers/api/videos/index.ts2
-rw-r--r--server/models/video/video-format-utils.ts5
-rw-r--r--shared/models/plugins/client-hook.model.ts3
-rw-r--r--shared/models/plugins/index.ts1
-rw-r--r--shared/models/plugins/plugin-client-scope.type.ts2
-rw-r--r--shared/models/plugins/register-client-form-field.model.ts12
-rw-r--r--shared/models/plugins/register-server-setting.model.ts9
-rw-r--r--shared/models/videos/video-update.model.ts2
-rw-r--r--shared/models/videos/video.model.ts2
30 files changed, 249 insertions, 76 deletions
diff --git a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html
index f3fc429ff..cb2894568 100644
--- a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html
+++ b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html
@@ -7,38 +7,7 @@
7 7
8 <form *ngIf="hasRegisteredSettings()" role="form" (ngSubmit)="formValidated()" [formGroup]="form"> 8 <form *ngIf="hasRegisteredSettings()" role="form" (ngSubmit)="formValidated()" [formGroup]="form">
9 <div class="form-group" *ngFor="let setting of registeredSettings"> 9 <div class="form-group" *ngFor="let setting of registeredSettings">
10 <label *ngIf="setting.type !== 'input-checkbox'" [attr.for]="setting.name" [innerHTML]="setting.label"></label> 10 <my-dynamic-form-field [form]="form" [setting]="setting" [formErrors]="formErrors"></my-dynamic-form-field>
11
12 <input *ngIf="setting.type === 'input'" type="text" [id]="setting.name" [formControlName]="setting.name" />
13
14 <textarea *ngIf="setting.type === 'input-textarea'" type="text" [id]="setting.name" [formControlName]="setting.name"></textarea>
15
16 <my-help *ngIf="setting.type === 'markdown-text'" helpType="markdownText"></my-help>
17
18 <my-help *ngIf="setting.type === 'markdown-enhanced'" helpType="markdownEnhanced"></my-help>
19
20 <my-markdown-textarea
21 *ngIf="setting.type === 'markdown-text'"
22 markdownType="text" [id]="setting.name" [formControlName]="setting.name" textareaWidth="500px"
23 [classes]="{ 'input-error': formErrors['settings.name'] }"
24 ></my-markdown-textarea>
25
26 <my-markdown-textarea
27 *ngIf="setting.type === 'markdown-enhanced'"
28 markdownType="enhanced" [id]="setting.name" [formControlName]="setting.name" textareaWidth="500px"
29 [classes]="{ 'input-error': formErrors['settings.name'] }"
30 ></my-markdown-textarea>
31
32 <my-peertube-checkbox
33 *ngIf="setting.type === 'input-checkbox'"
34 [id]="setting.name"
35 [formControlName]="setting.name"
36 [labelInnerHTML]="setting.label"
37 ></my-peertube-checkbox>
38
39 <div *ngIf="formErrors[setting.name]" class="form-error">
40 {{ formErrors[setting.name] }}
41 </div>
42 </div> 11 </div>
43 12
44 <input type="submit" i18n value="Update plugin settings" [disabled]="!form.valid"> 13 <input type="submit" i18n value="Update plugin settings" [disabled]="!form.valid">
diff --git a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss
index cc35aec57..5ab6e5f1b 100644
--- a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss
+++ b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss
@@ -5,22 +5,6 @@ h2 {
5 margin-bottom: 20px; 5 margin-bottom: 20px;
6} 6}
7 7
8input:not([type=submit]) {
9 @include peertube-input-text(340px);
10
11 display: block;
12}
13
14textarea {
15 @include peertube-textarea(340px, 200px);
16
17 display: block;
18}
19
20.peertube-select-container {
21 @include peertube-select-container(340px);
22}
23
24input[type=submit], button { 8input[type=submit], button {
25 @include peertube-button; 9 @include peertube-button;
26 @include orange-button; 10 @include orange-button;
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
index ae3413e79..842997b20 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
@@ -265,6 +265,21 @@
265 </ng-template> 265 </ng-template>
266 </ng-container> 266 </ng-container>
267 267
268 <ng-container ngbNavItem *ngIf="pluginFields.length !== 0">
269 <a ngbNavLink i18n>Plugin settings</a>
270
271 <ng-template ngbNavContent>
272 <div class="row plugin-settings">
273
274 <div class="col-md-12 col-xl-8">
275 <div *ngFor="let pluginSetting of pluginFields" class="form-group">
276 <my-dynamic-form-field [form]="pluginDataFormGroup" [formErrors]="formErrors" [setting]="pluginSetting.commonOptions"></my-dynamic-form-field>
277 </div>
278 </div>
279
280 </div>
281 </ng-template>
282 </ng-container>
268 </div> 283 </div>
269 284
270 <div [ngbNavOutlet]="nav"></div> 285 <div [ngbNavOutlet]="nav"></div>
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss
index 9caf009c5..3082a4f72 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss
@@ -7,7 +7,8 @@
7@import 'variables'; 7@import 'variables';
8@import 'mixins'; 8@import 'mixins';
9 9
10label { 10label,
11my-dynamic-form-field ::ng-deep label {
11 font-weight: $font-regular; 12 font-weight: $font-regular;
12 font-size: 100%; 13 font-size: 100%;
13} 14}
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
index 92d06aa12..f04111e69 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
@@ -1,8 +1,8 @@
1import { forkJoin } from 'rxjs' 1import { forkJoin } from 'rxjs'
2import { map } from 'rxjs/operators' 2import { map } from 'rxjs/operators'
3import { Component, Input, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' 3import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
4import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms' 4import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'
5import { ServerService } from '@app/core' 5import { HooksService, PluginService, ServerService } from '@app/core'
6import { removeElementFromArray } from '@app/helpers' 6import { removeElementFromArray } from '@app/helpers'
7import { 7import {
8 VIDEO_CATEGORY_VALIDATOR, 8 VIDEO_CATEGORY_VALIDATOR,
@@ -21,6 +21,7 @@ import { FormReactiveValidationMessages, FormValidatorService, SelectChannelItem
21import { InstanceService } from '@app/shared/shared-instance' 21import { InstanceService } from '@app/shared/shared-instance'
22import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' 22import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
23import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models' 23import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models'
24import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
24import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' 25import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
25import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' 26import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
26 27
@@ -39,9 +40,12 @@ export class VideoEditComponent implements OnInit, OnDestroy {
39 @Input() schedulePublicationPossible = true 40 @Input() schedulePublicationPossible = true
40 @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = [] 41 @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = []
41 @Input() waitTranscodingEnabled = true 42 @Input() waitTranscodingEnabled = true
43 @Input() type: 'import-url' | 'import-torrent' | 'upload' | 'update'
42 44
43 @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent 45 @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent
44 46
47 @Output() pluginFieldsAdded = new EventEmitter<void>()
48
45 // So that it can be accessed in the template 49 // So that it can be accessed in the template
46 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY 50 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
47 51
@@ -53,6 +57,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
53 tagValidators: ValidatorFn[] 57 tagValidators: ValidatorFn[]
54 tagValidatorsMessages: { [ name: string ]: string } 58 tagValidatorsMessages: { [ name: string ]: string }
55 59
60 pluginDataFormGroup: FormGroup
61
56 schedulePublicationEnabled = false 62 schedulePublicationEnabled = false
57 63
58 calendarLocale: any = {} 64 calendarLocale: any = {}
@@ -64,6 +70,11 @@ export class VideoEditComponent implements OnInit, OnDestroy {
64 70
65 serverConfig: ServerConfig 71 serverConfig: ServerConfig
66 72
73 pluginFields: {
74 commonOptions: RegisterClientFormFieldOptions
75 videoFormOptions: RegisterClientVideoFieldOptions
76 }[] = []
77
67 private schedulerInterval: any 78 private schedulerInterval: any
68 private firstPatchDone = false 79 private firstPatchDone = false
69 private initialVideoCaptions: string[] = [] 80 private initialVideoCaptions: string[] = []
@@ -72,9 +83,11 @@ export class VideoEditComponent implements OnInit, OnDestroy {
72 private formValidatorService: FormValidatorService, 83 private formValidatorService: FormValidatorService,
73 private videoService: VideoService, 84 private videoService: VideoService,
74 private serverService: ServerService, 85 private serverService: ServerService,
86 private pluginService: PluginService,
75 private instanceService: InstanceService, 87 private instanceService: InstanceService,
76 private i18nPrimengCalendarService: I18nPrimengCalendarService, 88 private i18nPrimengCalendarService: I18nPrimengCalendarService,
77 private ngZone: NgZone 89 private ngZone: NgZone,
90 private hooks: HooksService
78 ) { 91 ) {
79 this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale() 92 this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale()
80 this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone() 93 this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
@@ -136,19 +149,26 @@ export class VideoEditComponent implements OnInit, OnDestroy {
136 ngOnInit () { 149 ngOnInit () {
137 this.updateForm() 150 this.updateForm()
138 151
152 this.pluginService.ensurePluginsAreLoaded('video-edit')
153 .then(() => this.updatePluginFields())
154
139 this.serverService.getVideoCategories() 155 this.serverService.getVideoCategories()
140 .subscribe(res => this.videoCategories = res) 156 .subscribe(res => this.videoCategories = res)
157
141 this.serverService.getVideoLicences() 158 this.serverService.getVideoLicences()
142 .subscribe(res => this.videoLicences = res) 159 .subscribe(res => this.videoLicences = res)
160
143 forkJoin([ 161 forkJoin([
144 this.instanceService.getAbout(), 162 this.instanceService.getAbout(),
145 this.serverService.getVideoLanguages() 163 this.serverService.getVideoLanguages()
146 ]).pipe(map(([ about, languages ]) => ({ about, languages }))) 164 ]).pipe(map(([ about, languages ]) => ({ about, languages })))
147 .subscribe(res => { 165 .subscribe(res => {
148 this.videoLanguages = res.languages 166 this.videoLanguages = res.languages
149 .map(l => res.about.instance.languages.includes(l.id) 167 .map(l => {
150 ? { ...l, group: $localize`Instance languages`, groupOrder: 0 } 168 return res.about.instance.languages.includes(l.id)
151 : { ...l, group: $localize`All languages`, groupOrder: 1 }) 169 ? { ...l, group: $localize`Instance languages`, groupOrder: 0 }
170 : { ...l, group: $localize`All languages`, groupOrder: 1 }
171 })
152 .sort((a, b) => a.groupOrder - b.groupOrder) 172 .sort((a, b) => a.groupOrder - b.groupOrder)
153 }) 173 })
154 174
@@ -173,6 +193,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
173 this.ngZone.runOutsideAngular(() => { 193 this.ngZone.runOutsideAngular(() => {
174 this.schedulerInterval = setInterval(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute 194 this.schedulerInterval = setInterval(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute
175 }) 195 })
196
197 this.hooks.runAction('action:video-edit.init', 'video-edit', { type: this.type })
176 } 198 }
177 199
178 ngOnDestroy () { 200 ngOnDestroy () {
@@ -223,6 +245,23 @@ export class VideoEditComponent implements OnInit, OnDestroy {
223 }) 245 })
224 } 246 }
225 247
248 private updatePluginFields () {
249 this.pluginFields = this.pluginService.getRegisteredVideoFormFields(this.type)
250
251 if (this.pluginFields.length === 0) return
252
253 const obj: any = {}
254
255 for (const setting of this.pluginFields) {
256 obj[setting.commonOptions.name] = new FormControl(setting.commonOptions.default)
257 }
258
259 this.pluginDataFormGroup = new FormGroup(obj)
260 this.form.addControl('pluginData', this.pluginDataFormGroup)
261
262 this.pluginFieldsAdded.emit()
263 }
264
226 private trackPrivacyChange () { 265 private trackPrivacyChange () {
227 // We will update the schedule input and the wait transcoding checkbox validators 266 // We will update the schedule input and the wait transcoding checkbox validators
228 this.form.controls[ 'privacy' ] 267 this.form.controls[ 'privacy' ]
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html
index 825cb6df4..785514c76 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html
@@ -58,6 +58,7 @@
58 <my-video-edit 58 <my-video-edit
59 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false" 59 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false"
60 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" 60 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
61 type="import-torrent"
61 ></my-video-edit> 62 ></my-video-edit>
62 63
63 <div class="submit-container"> 64 <div class="submit-container">
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html
index 2107dc9d0..3e4eb5fbc 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html
@@ -54,6 +54,7 @@
54 <my-video-edit 54 <my-video-edit
55 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false" 55 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false"
56 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" 56 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
57 type="import-url"
57 ></my-video-edit> 58 ></my-video-edit>
58 59
59 <div class="submit-container"> 60 <div class="submit-container">
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
index ed697c25b..677fa1197 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
@@ -69,6 +69,7 @@
69 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" 69 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
70 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" 70 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
71 [waitTranscodingEnabled]="waitTranscodingEnabled" 71 [waitTranscodingEnabled]="waitTranscodingEnabled"
72 type="upload"
72 ></my-video-edit> 73 ></my-video-edit>
73 74
74 <div class="submit-container"> 75 <div class="submit-container">
diff --git a/client/src/app/+videos/+video-edit/video-update.component.html b/client/src/app/+videos/+video-edit/video-update.component.html
index 6c1239395..b37596399 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.html
+++ b/client/src/app/+videos/+video-edit/video-update.component.html
@@ -10,11 +10,12 @@
10 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible" 10 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible"
11 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" 11 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
12 [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled" 12 [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled"
13 type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()"
13 ></my-video-edit> 14 ></my-video-edit>
14 15
15 <div class="submit-container"> 16 <div class="submit-container">
16 <my-button className="orange-button" i18n-label label="Update" icon="circle-tick" 17 <my-button className="orange-button" i18n-label label="Update" icon="circle-tick"
17 (click)="update()" (keydown.enter)="update()" 18 (click)="update()" (keydown.enter)="update()"
18 [disabled]="!form.valid || isUpdatingVideo === true" 19 [disabled]="!form.valid || isUpdatingVideo === true"
19 ></my-button> 20 ></my-button>
20 </div> 21 </div>
diff --git a/client/src/app/+videos/+video-edit/video-update.component.ts b/client/src/app/+videos/+video-edit/video-update.component.ts
index 2e1d0f89d..20438a2d3 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.ts
+++ b/client/src/app/+videos/+video-edit/video-update.component.ts
@@ -126,6 +126,14 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
126 ) 126 )
127 } 127 }
128 128
129 hydratePluginFieldsFromVideo () {
130 if (!this.video.pluginData) return
131
132 this.form.patchValue({
133 pluginData: this.video.pluginData
134 })
135 }
136
129 private hydrateFormFromVideo () { 137 private hydrateFormFromVideo () {
130 this.form.patchValue(this.video.toFormPatch()) 138 this.form.patchValue(this.video.toFormPatch())
131 139
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts
index 871613b89..4e44a1865 100644
--- a/client/src/app/core/plugins/plugin.service.ts
+++ b/client/src/app/core/plugins/plugin.service.ts
@@ -9,7 +9,7 @@ import { RestExtractor } from '@app/core/rest'
9import { ServerService } from '@app/core/server/server.service' 9import { ServerService } from '@app/core/server/server.service'
10import { getDevLocale, isOnDevLocale } from '@app/helpers' 10import { getDevLocale, isOnDevLocale } from '@app/helpers'
11import { CustomModalComponent } from '@app/modal/custom-modal.component' 11import { CustomModalComponent } from '@app/modal/custom-modal.component'
12import { Hooks, loadPlugin, PluginInfo, runHook } from '@root-helpers/plugins' 12import { FormFields, Hooks, loadPlugin, PluginInfo, runHook } from '@root-helpers/plugins'
13import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n' 13import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n'
14import { 14import {
15 ClientHook, 15 ClientHook,
@@ -36,6 +36,7 @@ export class PluginService implements ClientHook {
36 'video-watch': new ReplaySubject<boolean>(1), 36 'video-watch': new ReplaySubject<boolean>(1),
37 signup: new ReplaySubject<boolean>(1), 37 signup: new ReplaySubject<boolean>(1),
38 login: new ReplaySubject<boolean>(1), 38 login: new ReplaySubject<boolean>(1),
39 'video-edit': new ReplaySubject<boolean>(1),
39 embed: new ReplaySubject<boolean>(1) 40 embed: new ReplaySubject<boolean>(1)
40 } 41 }
41 42
@@ -50,6 +51,9 @@ export class PluginService implements ClientHook {
50 private loadingScopes: { [id in PluginClientScope]?: boolean } = {} 51 private loadingScopes: { [id in PluginClientScope]?: boolean } = {}
51 52
52 private hooks: Hooks = {} 53 private hooks: Hooks = {}
54 private formFields: FormFields = {
55 video: []
56 }
53 57
54 constructor ( 58 constructor (
55 private authService: AuthService, 59 private authService: AuthService,
@@ -188,9 +192,18 @@ export class PluginService implements ClientHook {
188 : PluginType.THEME 192 : PluginType.THEME
189 } 193 }
190 194
195 getRegisteredVideoFormFields (type: 'import-url' | 'import-torrent' | 'upload' | 'update') {
196 return this.formFields.video.filter(f => f.videoFormOptions.type === type)
197 }
198
191 private loadPlugin (pluginInfo: PluginInfo) { 199 private loadPlugin (pluginInfo: PluginInfo) {
192 return this.zone.runOutsideAngular(() => { 200 return this.zone.runOutsideAngular(() => {
193 return loadPlugin(this.hooks, pluginInfo, pluginInfo => this.buildPeerTubeHelpers(pluginInfo)) 201 return loadPlugin({
202 hooks: this.hooks,
203 formFields: this.formFields,
204 pluginInfo,
205 peertubeHelpersFactory: pluginInfo => this.buildPeerTubeHelpers(pluginInfo)
206 })
194 }) 207 })
195 } 208 }
196 209
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
index 000000000..c111ea7df
--- /dev/null
+++ b/client/src/app/shared/shared-forms/dynamic-form-field.component.html
@@ -0,0 +1,35 @@
1<div [formGroup]="form">
2 <label *ngIf="setting.type !== 'input-checkbox'" [attr.for]="setting.name" [innerHTML]="setting.label"></label>
3
4 <input *ngIf="setting.type === 'input'" type="text" [id]="setting.name" [formControlName]="setting.name" />
5
6 <textarea *ngIf="setting.type === 'input-textarea'" type="text" [id]="setting.name" [formControlName]="setting.name"></textarea>
7
8 <my-help *ngIf="setting.type === 'markdown-text'" helpType="markdownText"></my-help>
9
10 <my-help *ngIf="setting.type === 'markdown-enhanced'" helpType="markdownEnhanced"></my-help>
11
12 <my-markdown-textarea
13 *ngIf="setting.type === 'markdown-text'"
14 markdownType="text" [id]="setting.name" [formControlName]="setting.name" textareaWidth="500px"
15 [classes]="{ 'input-error': formErrors['settings.name'] }"
16 ></my-markdown-textarea>
17
18 <my-markdown-textarea
19 *ngIf="setting.type === 'markdown-enhanced'"
20 markdownType="enhanced" [id]="setting.name" [formControlName]="setting.name" textareaWidth="500px"
21 [classes]="{ 'input-error': formErrors['settings.name'] }"
22 ></my-markdown-textarea>
23
24 <my-peertube-checkbox
25 *ngIf="setting.type === 'input-checkbox'"
26 [id]="setting.name"
27 [formControlName]="setting.name"
28 [labelInnerHTML]="setting.label"
29 ></my-peertube-checkbox>
30
31 <div *ngIf="formErrors[setting.name]" class="form-error">
32 {{ formErrors[setting.name] }}
33 </div>
34
35</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
index 000000000..70b3cf6c3
--- /dev/null
+++ b/client/src/app/shared/shared-forms/dynamic-form-field.component.scss
@@ -0,0 +1,18 @@
1@import '_variables';
2@import '_mixins';
3
4input:not([type=submit]) {
5 @include peertube-input-text(340px);
6
7 display: block;
8}
9
10textarea {
11 @include peertube-textarea(340px, 200px);
12
13 display: block;
14}
15
16.peertube-select-container {
17 @include peertube-select-container(340px);
18}
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
index 000000000..b63890797
--- /dev/null
+++ b/client/src/app/shared/shared-forms/dynamic-form-field.component.ts
@@ -0,0 +1,15 @@
1import { Component, Input } from '@angular/core'
2import { FormGroup } from '@angular/forms'
3import { RegisterClientFormFieldOptions } from '@shared/models'
4
5@Component({
6 selector: 'my-dynamic-form-field',
7 templateUrl: './dynamic-form-field.component.html',
8 styleUrls: [ './dynamic-form-field.component.scss' ]
9})
10
11export class DynamicFormFieldComponent {
12 @Input() form: FormGroup
13 @Input() formErrors: any
14 @Input() setting: RegisterClientFormFieldOptions
15}
diff --git a/client/src/app/shared/shared-forms/shared-form.module.ts b/client/src/app/shared/shared-forms/shared-form.module.ts
index 1946ac21f..a28988f87 100644
--- a/client/src/app/shared/shared-forms/shared-form.module.ts
+++ b/client/src/app/shared/shared-forms/shared-form.module.ts
@@ -15,6 +15,7 @@ import { ReactiveFileComponent } from './reactive-file.component'
15import { SelectChannelComponent, SelectCheckboxComponent, SelectOptionsComponent, SelectTagsComponent } from './select' 15import { SelectChannelComponent, SelectCheckboxComponent, SelectOptionsComponent, SelectTagsComponent } from './select'
16import { TextareaAutoResizeDirective } from './textarea-autoresize.directive' 16import { TextareaAutoResizeDirective } from './textarea-autoresize.directive'
17import { TimestampInputComponent } from './timestamp-input.component' 17import { TimestampInputComponent } from './timestamp-input.component'
18import { DynamicFormFieldComponent } from './dynamic-form-field.component'
18 19
19@NgModule({ 20@NgModule({
20 imports: [ 21 imports: [
@@ -41,7 +42,9 @@ import { TimestampInputComponent } from './timestamp-input.component'
41 SelectChannelComponent, 42 SelectChannelComponent,
42 SelectOptionsComponent, 43 SelectOptionsComponent,
43 SelectTagsComponent, 44 SelectTagsComponent,
44 SelectCheckboxComponent 45 SelectCheckboxComponent,
46
47 DynamicFormFieldComponent
45 ], 48 ],
46 49
47 exports: [ 50 exports: [
@@ -63,7 +66,9 @@ import { TimestampInputComponent } from './timestamp-input.component'
63 SelectChannelComponent, 66 SelectChannelComponent,
64 SelectOptionsComponent, 67 SelectOptionsComponent,
65 SelectTagsComponent, 68 SelectTagsComponent,
66 SelectCheckboxComponent 69 SelectCheckboxComponent,
70
71 DynamicFormFieldComponent
67 ], 72 ],
68 73
69 providers: [ 74 providers: [
diff --git a/client/src/app/shared/shared-main/video/video-edit.model.ts b/client/src/app/shared/shared-main/video/video-edit.model.ts
index 6a529e052..757b686c0 100644
--- a/client/src/app/shared/shared-main/video/video-edit.model.ts
+++ b/client/src/app/shared/shared-main/video/video-edit.model.ts
@@ -25,6 +25,8 @@ export class VideoEdit implements VideoUpdate {
25 scheduleUpdate?: VideoScheduleUpdate 25 scheduleUpdate?: VideoScheduleUpdate
26 originallyPublishedAt?: Date | string 26 originallyPublishedAt?: Date | string
27 27
28 pluginData?: any
29
28 constructor ( 30 constructor (
29 video?: Video & { 31 video?: Video & {
30 tags: string[], 32 tags: string[],
@@ -55,10 +57,12 @@ export class VideoEdit implements VideoUpdate {
55 57
56 this.scheduleUpdate = video.scheduledUpdate 58 this.scheduleUpdate = video.scheduledUpdate
57 this.originallyPublishedAt = video.originallyPublishedAt ? new Date(video.originallyPublishedAt) : null 59 this.originallyPublishedAt = video.originallyPublishedAt ? new Date(video.originallyPublishedAt) : null
60
61 this.pluginData = video.pluginData
58 } 62 }
59 } 63 }
60 64
61 patch (values: { [ id: string ]: string }) { 65 patch (values: { [ id: string ]: any }) {
62 Object.keys(values).forEach((key) => { 66 Object.keys(values).forEach((key) => {
63 this[ key ] = values[ key ] 67 this[ key ] = values[ key ]
64 }) 68 })
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts
index 73f0198e2..0dca3da0d 100644
--- a/client/src/app/shared/shared-main/video/video.model.ts
+++ b/client/src/app/shared/shared-main/video/video.model.ts
@@ -84,6 +84,8 @@ export class Video implements VideoServerModel {
84 currentTime: number 84 currentTime: number
85 } 85 }
86 86
87 pluginData?: any
88
87 static buildClientUrl (videoUUID: string) { 89 static buildClientUrl (videoUUID: string) {
88 return '/videos/watch/' + videoUUID 90 return '/videos/watch/' + videoUUID
89 } 91 }
@@ -152,6 +154,8 @@ export class Video implements VideoServerModel {
152 154
153 this.originInstanceHost = this.account.host 155 this.originInstanceHost = this.account.host
154 this.originInstanceUrl = 'https://' + this.originInstanceHost 156 this.originInstanceUrl = 'https://' + this.originInstanceHost
157
158 this.pluginData = hash.pluginData
155 } 159 }
156 160
157 isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { 161 isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
index 48aff82b4..8a688c8ed 100644
--- a/client/src/app/shared/shared-main/video/video.service.ts
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -96,6 +96,7 @@ export class VideoService implements VideosProvider {
96 downloadEnabled: video.downloadEnabled, 96 downloadEnabled: video.downloadEnabled,
97 thumbnailfile: video.thumbnailfile, 97 thumbnailfile: video.thumbnailfile,
98 previewfile: video.previewfile, 98 previewfile: video.previewfile,
99 pluginData: video.pluginData,
99 scheduleUpdate, 100 scheduleUpdate,
100 originallyPublishedAt 101 originallyPublishedAt
101 } 102 }
diff --git a/client/src/root-helpers/plugins.ts b/client/src/root-helpers/plugins.ts
index 011721761..4bc2c8eb2 100644
--- a/client/src/root-helpers/plugins.ts
+++ b/client/src/root-helpers/plugins.ts
@@ -1,6 +1,14 @@
1import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
2import { ClientHookName, ClientScript, RegisterClientHookOptions, ServerConfigPlugin, PluginType, clientHookObject } from '../../../shared/models'
3import { RegisterClientHelpers } from 'src/types/register-client-option.model' 1import { RegisterClientHelpers } from 'src/types/register-client-option.model'
2import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
3import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
4import {
5 ClientHookName,
6 clientHookObject,
7 ClientScript,
8 PluginType,
9 RegisterClientHookOptions,
10 ServerConfigPlugin
11} from '../../../shared/models'
4import { ClientScript as ClientScriptModule } from '../types/client-script.model' 12import { ClientScript as ClientScriptModule } from '../types/client-script.model'
5import { importModule } from './utils' 13import { importModule } from './utils'
6 14
@@ -18,6 +26,13 @@ type PluginInfo = {
18 isTheme: boolean 26 isTheme: boolean
19} 27}
20 28
29type FormFields = {
30 video: {
31 commonOptions: RegisterClientFormFieldOptions
32 videoFormOptions: RegisterClientVideoFieldOptions
33 }[]
34}
35
21async function runHook<T> (hooks: Hooks, hookName: ClientHookName, result?: T, params?: any) { 36async function runHook<T> (hooks: Hooks, hookName: ClientHookName, result?: T, params?: any) {
22 if (!hooks[hookName]) return result 37 if (!hooks[hookName]) return result
23 38
@@ -34,7 +49,13 @@ async function runHook<T> (hooks: Hooks, hookName: ClientHookName, result?: T, p
34 return result 49 return result
35} 50}
36 51
37function loadPlugin (hooks: Hooks, pluginInfo: PluginInfo, peertubeHelpersFactory: (pluginInfo: PluginInfo) => RegisterClientHelpers) { 52function loadPlugin (options: {
53 hooks: Hooks
54 pluginInfo: PluginInfo
55 peertubeHelpersFactory: (pluginInfo: PluginInfo) => RegisterClientHelpers
56 formFields?: FormFields
57}) {
58 const { hooks, pluginInfo, peertubeHelpersFactory, formFields } = options
38 const { plugin, clientScript } = pluginInfo 59 const { plugin, clientScript } = pluginInfo
39 60
40 const registerHook = (options: RegisterClientHookOptions) => { 61 const registerHook = (options: RegisterClientHookOptions) => {
@@ -54,12 +75,23 @@ function loadPlugin (hooks: Hooks, pluginInfo: PluginInfo, peertubeHelpersFactor
54 }) 75 })
55 } 76 }
56 77
78 const registerVideoField = (commonOptions: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) => {
79 if (!formFields) {
80 throw new Error('Video field registration is not supported')
81 }
82
83 formFields.video.push({
84 commonOptions,
85 videoFormOptions
86 })
87 }
88
57 const peertubeHelpers = peertubeHelpersFactory(pluginInfo) 89 const peertubeHelpers = peertubeHelpersFactory(pluginInfo)
58 90
59 console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name) 91 console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name)
60 92
61 return importModule(clientScript.script) 93 return importModule(clientScript.script)
62 .then((script: ClientScriptModule) => script.register({ registerHook, peertubeHelpers })) 94 .then((script: ClientScriptModule) => script.register({ registerHook, registerVideoField, peertubeHelpers }))
63 .then(() => sortHooksByPriority(hooks)) 95 .then(() => sortHooksByPriority(hooks))
64 .catch(err => console.error('Cannot import or register plugin %s.', pluginInfo.plugin.name, err)) 96 .catch(err => console.error('Cannot import or register plugin %s.', pluginInfo.plugin.name, err))
65} 97}
@@ -68,6 +100,7 @@ export {
68 HookStructValue, 100 HookStructValue,
69 Hooks, 101 Hooks,
70 PluginInfo, 102 PluginInfo,
103 FormFields,
71 loadPlugin, 104 loadPlugin,
72 runHook 105 runHook
73} 106}
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index fe65794f7..c79471005 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -750,7 +750,11 @@ export class PeerTubeEmbed {
750 isTheme: false 750 isTheme: false
751 } 751 }
752 752
753 await loadPlugin(this.peertubeHooks, pluginInfo, _ => this.buildPeerTubeHelpers(translations)) 753 await loadPlugin({
754 hooks: this.peertubeHooks,
755 pluginInfo,
756 peertubeHelpersFactory: _ => this.buildPeerTubeHelpers(translations)
757 })
754 } 758 }
755 } 759 }
756 } 760 }
diff --git a/client/src/types/register-client-option.model.ts b/client/src/types/register-client-option.model.ts
index dff00e9dd..e3c6d803d 100644
--- a/client/src/types/register-client-option.model.ts
+++ b/client/src/types/register-client-option.model.ts
@@ -1,8 +1,11 @@
1import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
1import { RegisterClientHookOptions } from '@shared/models/plugins/register-client-hook.model' 2import { RegisterClientHookOptions } from '@shared/models/plugins/register-client-hook.model'
2 3
3export type RegisterClientOptions = { 4export type RegisterClientOptions = {
4 registerHook: (options: RegisterClientHookOptions) => void 5 registerHook: (options: RegisterClientHookOptions) => void
5 6
7 registerVideoField: (commonOptions: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) => void
8
6 peertubeHelpers: RegisterClientHelpers 9 peertubeHelpers: RegisterClientHelpers
7} 10}
8 11
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index c05acfd2f..15b6f214f 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -414,7 +414,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
414 Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated) 414 Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated)
415 } 415 }
416 416
417 Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated }) 417 Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body })
418 } catch (err) { 418 } catch (err) {
419 // Force fields we want to update 419 // Force fields we want to update
420 // If the transaction is retried, sequelize will think the object has not changed 420 // If the transaction is retried, sequelize will think the object has not changed
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index 9b6509dfd..7a17c839f 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -78,7 +78,10 @@ function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFor
78 78
79 userHistory: userHistory ? { 79 userHistory: userHistory ? {
80 currentTime: userHistory.currentTime 80 currentTime: userHistory.currentTime
81 } : undefined 81 } : undefined,
82
83 // Can be added by external plugins
84 pluginData: (video as any).pluginData
82 } 85 }
83 86
84 if (options) { 87 if (options) {
diff --git a/shared/models/plugins/client-hook.model.ts b/shared/models/plugins/client-hook.model.ts
index 193a3f646..7b7144676 100644
--- a/shared/models/plugins/client-hook.model.ts
+++ b/shared/models/plugins/client-hook.model.ts
@@ -70,6 +70,9 @@ export const clientActionHookObject = {
70 // Fired when a user click on 'View x replies' and they're loaded 70 // Fired when a user click on 'View x replies' and they're loaded
71 'action:video-watch.video-thread-replies.loaded': true, 71 'action:video-watch.video-thread-replies.loaded': true,
72 72
73 // Fired when the video edit page (upload, URL/torrent import, update) is being initialized
74 'action:video-edit.init': true,
75
73 // Fired when the login page is being initialized 76 // Fired when the login page is being initialized
74 'action:login.init': true, 77 'action:login.init': true,
75 78
diff --git a/shared/models/plugins/index.ts b/shared/models/plugins/index.ts
index 209fca791..83ed6f583 100644
--- a/shared/models/plugins/index.ts
+++ b/shared/models/plugins/index.ts
@@ -19,6 +19,7 @@ export * from './plugin-video-privacy-manager.model'
19export * from './plugin.type' 19export * from './plugin.type'
20export * from './public-server.setting' 20export * from './public-server.setting'
21export * from './register-client-hook.model' 21export * from './register-client-hook.model'
22export * from './register-client-form-field.model'
22export * from './register-server-hook.model' 23export * from './register-server-hook.model'
23export * from './register-server-setting.model' 24export * from './register-server-setting.model'
24export * from './server-hook.model' 25export * from './server-hook.model'
diff --git a/shared/models/plugins/plugin-client-scope.type.ts b/shared/models/plugins/plugin-client-scope.type.ts
index a3c669fe7..e188ce100 100644
--- a/shared/models/plugins/plugin-client-scope.type.ts
+++ b/shared/models/plugins/plugin-client-scope.type.ts
@@ -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
index 000000000..df24339c6
--- /dev/null
+++ b/shared/models/plugins/register-client-form-field.model.ts
@@ -0,0 +1,12 @@
1export interface RegisterClientFormFieldOptions {
2 name: string
3 label: string
4 type: 'input' | 'input-checkbox' | 'input-textarea' | 'markdown-text' | 'markdown-enhanced'
5
6 // Default setting value
7 default?: string | boolean
8}
9
10export interface RegisterClientVideoFieldOptions {
11 type: 'import-url' | 'import-torrent' | 'update' | 'upload'
12}
diff --git a/shared/models/plugins/register-server-setting.model.ts b/shared/models/plugins/register-server-setting.model.ts
index 920c3480f..6872dc53e 100644
--- a/shared/models/plugins/register-server-setting.model.ts
+++ b/shared/models/plugins/register-server-setting.model.ts
@@ -1,15 +1,10 @@
1export interface RegisterServerSettingOptions { 1import { RegisterClientFormFieldOptions } from './register-client-form-field.model'
2 name: string
3 label: string
4 type: 'input' | 'input-checkbox' | 'input-textarea' | 'markdown-text' | 'markdown-enhanced'
5 2
3export interface RegisterServerSettingOptions extends RegisterClientFormFieldOptions {
6 // If the setting is not private, anyone can view its value (client code included) 4 // If the setting is not private, anyone can view its value (client code included)
7 // If the setting is private, only server-side hooks can access it 5 // If the setting is private, only server-side hooks can access it
8 // Mainly used by the PeerTube client to get admin config 6 // Mainly used by the PeerTube client to get admin config
9 private: boolean 7 private: boolean
10
11 // Default setting value
12 default?: string | boolean
13} 8}
14 9
15export interface RegisteredServerSettings { 10export interface RegisteredServerSettings {
diff --git a/shared/models/videos/video-update.model.ts b/shared/models/videos/video-update.model.ts
index 4ef904156..86653b959 100644
--- a/shared/models/videos/video-update.model.ts
+++ b/shared/models/videos/video-update.model.ts
@@ -19,4 +19,6 @@ export interface VideoUpdate {
19 previewfile?: Blob 19 previewfile?: Blob
20 scheduleUpdate?: VideoScheduleUpdate 20 scheduleUpdate?: VideoScheduleUpdate
21 originallyPublishedAt?: Date | string 21 originallyPublishedAt?: Date | string
22
23 pluginData?: any
22} 24}
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts
index 557e66e09..158ee8f05 100644
--- a/shared/models/videos/video.model.ts
+++ b/shared/models/videos/video.model.ts
@@ -53,6 +53,8 @@ export interface Video {
53 userHistory?: { 53 userHistory?: {
54 currentTime: number 54 currentTime: number
55 } 55 }
56
57 pluginData?: any
56} 58}
57 59
58export interface VideoDetails extends Video { 60export interface VideoDetails extends Video {