<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">
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;
</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>
@import 'variables';
@import 'mixins';
-label {
+label,
+my-dynamic-form-field ::ng-deep label {
font-weight: $font-regular;
font-size: 100%;
}
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,
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'
@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
tagValidators: ValidatorFn[]
tagValidatorsMessages: { [ name: string ]: string }
+ pluginDataFormGroup: FormGroup
+
schedulePublicationEnabled = false
calendarLocale: any = {}
serverConfig: ServerConfig
+ pluginFields: {
+ commonOptions: RegisterClientFormFieldOptions
+ videoFormOptions: RegisterClientVideoFieldOptions
+ }[] = []
+
private schedulerInterval: any
private firstPatchDone = false
private initialVideoCaptions: string[] = []
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()
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)
})
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 () {
})
}
+ 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' ]
<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">
<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">
[form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
[validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
[waitTranscodingEnabled]="waitTranscodingEnabled"
+ type="upload"
></my-video-edit>
<div class="submit-container">
[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>
)
}
+ hydratePluginFieldsFromVideo () {
+ if (!this.video.pluginData) return
+
+ this.form.patchValue({
+ pluginData: this.video.pluginData
+ })
+ }
+
private hydrateFormFromVideo () {
this.form.patchValue(this.video.toFormPatch())
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,
'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)
}
private loadingScopes: { [id in PluginClientScope]?: boolean } = {}
private hooks: Hooks = {}
+ private formFields: FormFields = {
+ video: []
+ }
constructor (
private authService: AuthService,
: 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)
+ })
})
}
--- /dev/null
+<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>
--- /dev/null
+@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);
+}
--- /dev/null
+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
+}
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: [
SelectChannelComponent,
SelectOptionsComponent,
SelectTagsComponent,
- SelectCheckboxComponent
+ SelectCheckboxComponent,
+
+ DynamicFormFieldComponent
],
exports: [
SelectChannelComponent,
SelectOptionsComponent,
SelectTagsComponent,
- SelectCheckboxComponent
+ SelectCheckboxComponent,
+
+ DynamicFormFieldComponent
],
providers: [
scheduleUpdate?: VideoScheduleUpdate
originallyPublishedAt?: Date | string
+ pluginData?: any
+
constructor (
video?: Video & {
tags: string[],
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 ]
})
currentTime: number
}
+ pluginData?: any
+
static buildClientUrl (videoUUID: string) {
return '/videos/watch/' + videoUUID
}
this.originInstanceHost = this.account.host
this.originInstanceUrl = 'https://' + this.originInstanceHost
+
+ this.pluginData = hash.pluginData
}
isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
downloadEnabled: video.downloadEnabled,
thumbnailfile: video.thumbnailfile,
previewfile: video.previewfile,
+ pluginData: video.pluginData,
scheduleUpdate,
originallyPublishedAt
}
-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'
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
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) => {
})
}
+ 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))
}
HookStructValue,
Hooks,
PluginInfo,
+ FormFields,
loadPlugin,
runHook
}
isTheme: false
}
- await loadPlugin(this.peertubeHooks, pluginInfo, _ => this.buildPeerTubeHelpers(translations))
+ await loadPlugin({
+ hooks: this.peertubeHooks,
+ pluginInfo,
+ peertubeHelpersFactory: _ => this.buildPeerTubeHelpers(translations)
+ })
}
}
}
+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
}
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
userHistory: userHistory ? {
currentTime: userHistory.currentTime
- } : undefined
+ } : undefined,
+
+ // Can be added by external plugins
+ pluginData: (video as any).pluginData
}
if (options) {
// 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,
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'
-export type PluginClientScope = 'common' | 'video-watch' | 'search' | 'signup' | 'login' | 'embed'
+export type PluginClientScope = 'common' | 'video-watch' | 'search' | 'signup' | 'login' | 'embed' | 'video-edit'
--- /dev/null
+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'
+}
-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 {
previewfile?: Blob
scheduleUpdate?: VideoScheduleUpdate
originallyPublishedAt?: Date | string
+
+ pluginData?: any
}
userHistory?: {
currentTime: number
}
+
+ pluginData?: any
}
export interface VideoDetails extends Video {