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 @@
[form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
[validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
[waitTranscodingEnabled]="waitTranscodingEnabled"
+ type="upload"
>
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 @@
[form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible"
[validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
[videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled"
+ type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()"
>
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 {
)
}
+ hydratePluginFieldsFromVideo () {
+ if (!this.video.pluginData) return
+
+ this.form.patchValue({
+ pluginData: this.video.pluginData
+ })
+ }
+
private hydrateFormFromVideo () {
this.form.patchValue(this.video.toFormPatch())
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'
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
(1),
signup: new ReplaySubject(1),
login: new ReplaySubject(1),
+ 'video-edit': new ReplaySubject(1),
embed: new ReplaySubject(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
index 000000000..c111ea7df
--- /dev/null
+++ b/client/src/app/shared/shared-forms/dynamic-form-field.component.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formErrors[setting.name] }}
+
+
+
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 @@
+@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
index 000000000..b63890797
--- /dev/null
+++ b/client/src/app/shared/shared-forms/dynamic-form-field.component.ts
@@ -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
+}
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'
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: [
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 {
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 ]
})
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 {
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) {
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 {
downloadEnabled: video.downloadEnabled,
thumbnailfile: video.thumbnailfile,
previewfile: video.previewfile,
+ pluginData: video.pluginData,
scheduleUpdate,
originallyPublishedAt
}
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 @@
-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 (hooks: Hooks, hookName: ClientHookName, result?: T, params?: any) {
if (!hooks[hookName]) return result
@@ -34,7 +49,13 @@ async function runHook (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
}
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 {
isTheme: false
}
- await loadPlugin(this.peertubeHooks, pluginInfo, _ => this.buildPeerTubeHelpers(translations))
+ await loadPlugin({
+ hooks: this.peertubeHooks,
+ pluginInfo,
+ peertubeHelpersFactory: _ => this.buildPeerTubeHelpers(translations)
+ })
}
}
}
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 @@
+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
}
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) {
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
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
userHistory: userHistory ? {
currentTime: userHistory.currentTime
- } : undefined
+ } : undefined,
+
+ // Can be added by external plugins
+ pluginData: (video as any).pluginData
}
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 = {
// 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,
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'
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'
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 @@
+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'
+}
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 @@
-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 {
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 {
previewfile?: Blob
scheduleUpdate?: VideoScheduleUpdate
originallyPublishedAt?: Date | string
+
+ pluginData?: any
}
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 {
userHistory?: {
currentTime: number
}
+
+ pluginData?: any
}
export interface VideoDetails extends Video {