Support plugin hooks in embed
This commit is contained in:
parent
a9f6802e7d
commit
f95628636b
8 changed files with 217 additions and 114 deletions
|
@ -7,38 +7,22 @@ import { Notifier } from '@app/core/notification'
|
|||
import { MarkdownService } from '@app/core/renderer'
|
||||
import { RestExtractor } from '@app/core/rest'
|
||||
import { ServerService } from '@app/core/server/server.service'
|
||||
import { getDevLocale, importModule, isOnDevLocale } from '@app/helpers'
|
||||
import { getDevLocale, isOnDevLocale } from '@app/helpers'
|
||||
import { CustomModalComponent } from '@app/modal/custom-modal.component'
|
||||
import { Hooks, loadPlugin, PluginInfo, runHook } from '@root-helpers/plugins'
|
||||
import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n'
|
||||
import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
|
||||
import {
|
||||
ClientHook,
|
||||
ClientHookName,
|
||||
clientHookObject,
|
||||
ClientScript,
|
||||
PluginClientScope,
|
||||
PluginTranslation,
|
||||
PluginType,
|
||||
PublicServerSetting,
|
||||
RegisterClientHookOptions,
|
||||
ServerConfigPlugin
|
||||
} from '@shared/models'
|
||||
import { environment } from '../../../environments/environment'
|
||||
import { ClientScript as ClientScriptModule } from '../../../types/client-script.model'
|
||||
import { RegisterClientHelpers } from '../../../types/register-client-option.model'
|
||||
|
||||
interface HookStructValue extends RegisterClientHookOptions {
|
||||
plugin: ServerConfigPlugin
|
||||
clientScript: ClientScript
|
||||
}
|
||||
|
||||
type PluginInfo = {
|
||||
plugin: ServerConfigPlugin
|
||||
clientScript: ClientScript
|
||||
pluginType: PluginType
|
||||
isTheme: boolean
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PluginService implements ClientHook {
|
||||
private static BASE_PLUGIN_API_URL = environment.apiUrl + '/api/v1/plugins'
|
||||
|
@ -51,7 +35,8 @@ export class PluginService implements ClientHook {
|
|||
search: new ReplaySubject<boolean>(1),
|
||||
'video-watch': new ReplaySubject<boolean>(1),
|
||||
signup: new ReplaySubject<boolean>(1),
|
||||
login: new ReplaySubject<boolean>(1)
|
||||
login: new ReplaySubject<boolean>(1),
|
||||
embed: new ReplaySubject<boolean>(1)
|
||||
}
|
||||
|
||||
translationsObservable: Observable<PluginTranslation>
|
||||
|
@ -64,7 +49,7 @@ export class PluginService implements ClientHook {
|
|||
private loadedScopes: PluginClientScope[] = []
|
||||
private loadingScopes: { [id in PluginClientScope]?: boolean } = {}
|
||||
|
||||
private hooks: { [ name: string ]: HookStructValue[] } = {}
|
||||
private hooks: Hooks = {}
|
||||
|
||||
constructor (
|
||||
private authService: AuthService,
|
||||
|
@ -120,7 +105,7 @@ export class PluginService implements ClientHook {
|
|||
this.scopes[scope].push({
|
||||
plugin,
|
||||
clientScript: {
|
||||
script: environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`,
|
||||
script: `${pathPrefix}/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`,
|
||||
scopes: clientScript.scopes
|
||||
},
|
||||
pluginType: isTheme ? PluginType.THEME : PluginType.PLUGIN,
|
||||
|
@ -184,20 +169,8 @@ export class PluginService implements ClientHook {
|
|||
}
|
||||
|
||||
runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> {
|
||||
return this.zone.runOutsideAngular(async () => {
|
||||
if (!this.hooks[ hookName ]) return result
|
||||
|
||||
const hookType = getHookType(hookName)
|
||||
|
||||
for (const hook of this.hooks[ hookName ]) {
|
||||
console.log('Running hook %s of plugin %s.', hookName, hook.plugin.name)
|
||||
|
||||
result = await internalRunHook(hook.handler, hookType, result, params, err => {
|
||||
console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.clientScript.script, hook.plugin.name, err)
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
return this.zone.runOutsideAngular(() => {
|
||||
return runHook(this.hooks, hookName, result, params)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -216,34 +189,8 @@ export class PluginService implements ClientHook {
|
|||
}
|
||||
|
||||
private loadPlugin (pluginInfo: PluginInfo) {
|
||||
const { plugin, clientScript } = pluginInfo
|
||||
|
||||
const registerHook = (options: RegisterClientHookOptions) => {
|
||||
if (clientHookObject[options.target] !== true) {
|
||||
console.error('Unknown hook %s of plugin %s. Skipping.', options.target, plugin.name)
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.hooks[options.target]) this.hooks[options.target] = []
|
||||
|
||||
this.hooks[options.target].push({
|
||||
plugin,
|
||||
clientScript,
|
||||
target: options.target,
|
||||
handler: options.handler,
|
||||
priority: options.priority || 0
|
||||
})
|
||||
}
|
||||
|
||||
const peertubeHelpers = this.buildPeerTubeHelpers(pluginInfo)
|
||||
|
||||
console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name)
|
||||
|
||||
return this.zone.runOutsideAngular(() => {
|
||||
return importModule(clientScript.script)
|
||||
.then((script: ClientScriptModule) => script.register({ registerHook, peertubeHelpers }))
|
||||
.then(() => this.sortHooksByPriority())
|
||||
.catch(err => console.error('Cannot import or register plugin %s.', pluginInfo.plugin.name, err))
|
||||
return loadPlugin(this.hooks, pluginInfo, pluginInfo => this.buildPeerTubeHelpers(pluginInfo))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -253,14 +200,6 @@ export class PluginService implements ClientHook {
|
|||
}
|
||||
}
|
||||
|
||||
private sortHooksByPriority () {
|
||||
for (const hookName of Object.keys(this.hooks)) {
|
||||
this.hooks[hookName].sort((a, b) => {
|
||||
return b.priority - a.priority
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers {
|
||||
const { plugin } = pluginInfo
|
||||
const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType)
|
||||
|
|
|
@ -148,41 +148,6 @@ function scrollToTop () {
|
|||
window.scroll(0, 0)
|
||||
}
|
||||
|
||||
// Thanks: https://github.com/uupaa/dynamic-import-polyfill
|
||||
function importModule (path: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const vector = '$importModule$' + Math.random().toString(32).slice(2)
|
||||
const script = document.createElement('script')
|
||||
|
||||
const destructor = () => {
|
||||
delete window[ vector ]
|
||||
script.onerror = null
|
||||
script.onload = null
|
||||
script.remove()
|
||||
URL.revokeObjectURL(script.src)
|
||||
script.src = ''
|
||||
}
|
||||
|
||||
script.defer = true
|
||||
script.type = 'module'
|
||||
|
||||
script.onerror = () => {
|
||||
reject(new Error(`Failed to import: ${path}`))
|
||||
destructor()
|
||||
}
|
||||
script.onload = () => {
|
||||
resolve(window[ vector ])
|
||||
destructor()
|
||||
}
|
||||
const absURL = (environment.apiUrl || window.location.origin) + path
|
||||
const loader = `import * as m from "${absURL}"; window.${vector} = m;` // export Module
|
||||
const blob = new Blob([ loader ], { type: 'text/javascript' })
|
||||
script.src = URL.createObjectURL(blob)
|
||||
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
function isInViewport (el: HTMLElement) {
|
||||
const bounding = el.getBoundingClientRect()
|
||||
return (
|
||||
|
@ -216,7 +181,6 @@ export {
|
|||
getAbsoluteEmbedUrl,
|
||||
objectLineFeedToHtml,
|
||||
removeElementFromArray,
|
||||
importModule,
|
||||
scrollToTop,
|
||||
isInViewport,
|
||||
isXPercentInViewport
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
import 'core-js/features/reflect'
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
production: true,
|
||||
hmr: false,
|
||||
apiUrl: 'http://localhost:9000',
|
||||
embedUrl: 'http://localhost:9000'
|
||||
apiUrl: '',
|
||||
embedUrl: ''
|
||||
}
|
||||
|
|
81
client/src/root-helpers/plugins.ts
Normal file
81
client/src/root-helpers/plugins.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
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 { ClientScript as ClientScriptModule } from '../types/client-script.model'
|
||||
import { importModule } from './utils'
|
||||
|
||||
interface HookStructValue extends RegisterClientHookOptions {
|
||||
plugin: ServerConfigPlugin
|
||||
clientScript: ClientScript
|
||||
}
|
||||
|
||||
type Hooks = { [ name: string ]: HookStructValue[] }
|
||||
|
||||
type PluginInfo = {
|
||||
plugin: ServerConfigPlugin
|
||||
clientScript: ClientScript
|
||||
pluginType: PluginType
|
||||
isTheme: boolean
|
||||
}
|
||||
|
||||
async function runHook<T> (hooks: Hooks, hookName: ClientHookName, result?: T, params?: any) {
|
||||
if (!hooks[hookName]) return result
|
||||
|
||||
const hookType = getHookType(hookName)
|
||||
|
||||
for (const hook of hooks[hookName]) {
|
||||
console.log('Running hook %s of plugin %s.', hookName, hook.plugin.name)
|
||||
|
||||
result = await internalRunHook(hook.handler, hookType, result, params, err => {
|
||||
console.error('Cannot run hook %s of script %s of plugin %s.', hookName, hook.clientScript.script, hook.plugin.name, err)
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function loadPlugin (hooks: Hooks, pluginInfo: PluginInfo, peertubeHelpersFactory: (pluginInfo: PluginInfo) => RegisterClientHelpers) {
|
||||
const { plugin, clientScript } = pluginInfo
|
||||
|
||||
const registerHook = (options: RegisterClientHookOptions) => {
|
||||
if (clientHookObject[options.target] !== true) {
|
||||
console.error('Unknown hook %s of plugin %s. Skipping.', options.target, plugin.name)
|
||||
return
|
||||
}
|
||||
|
||||
if (!hooks[options.target]) hooks[options.target] = []
|
||||
|
||||
hooks[options.target].push({
|
||||
plugin,
|
||||
clientScript,
|
||||
target: options.target,
|
||||
handler: options.handler,
|
||||
priority: options.priority || 0
|
||||
})
|
||||
}
|
||||
|
||||
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(() => sortHooksByPriority(hooks))
|
||||
.catch(err => console.error('Cannot import or register plugin %s.', pluginInfo.plugin.name, err))
|
||||
}
|
||||
|
||||
export {
|
||||
HookStructValue,
|
||||
Hooks,
|
||||
PluginInfo,
|
||||
loadPlugin,
|
||||
runHook
|
||||
}
|
||||
|
||||
function sortHooksByPriority (hooks: Hooks) {
|
||||
for (const hookName of Object.keys(hooks)) {
|
||||
hooks[hookName].sort((a, b) => {
|
||||
return b.priority - a.priority
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import { environment } from '../environments/environment'
|
||||
|
||||
function objectToUrlEncoded (obj: any) {
|
||||
const str: string[] = []
|
||||
for (const key of Object.keys(obj)) {
|
||||
|
@ -7,6 +9,42 @@ function objectToUrlEncoded (obj: any) {
|
|||
return str.join('&')
|
||||
}
|
||||
|
||||
// Thanks: https://github.com/uupaa/dynamic-import-polyfill
|
||||
function importModule (path: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const vector = '$importModule$' + Math.random().toString(32).slice(2)
|
||||
const script = document.createElement('script')
|
||||
|
||||
const destructor = () => {
|
||||
delete window[ vector ]
|
||||
script.onerror = null
|
||||
script.onload = null
|
||||
script.remove()
|
||||
URL.revokeObjectURL(script.src)
|
||||
script.src = ''
|
||||
}
|
||||
|
||||
script.defer = true
|
||||
script.type = 'module'
|
||||
|
||||
script.onerror = () => {
|
||||
reject(new Error(`Failed to import: ${path}`))
|
||||
destructor()
|
||||
}
|
||||
script.onload = () => {
|
||||
resolve(window[ vector ])
|
||||
destructor()
|
||||
}
|
||||
const absURL = (environment.apiUrl || window.location.origin) + path
|
||||
const loader = `import * as m from "${absURL}"; window.${vector} = m;` // export Module
|
||||
const blob = new Blob([ loader ], { type: 'text/javascript' })
|
||||
script.src = URL.createObjectURL(blob)
|
||||
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
importModule,
|
||||
objectToUrlEncoded
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import './embed.scss'
|
||||
import videojs from 'video.js'
|
||||
import { objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index'
|
||||
import { Tokens } from '@root-helpers/users'
|
||||
import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
|
||||
import {
|
||||
ResultList,
|
||||
|
@ -11,12 +9,19 @@ import {
|
|||
VideoDetails,
|
||||
VideoPlaylist,
|
||||
VideoPlaylistElement,
|
||||
VideoStreamingPlaylistType
|
||||
VideoStreamingPlaylistType,
|
||||
PluginType,
|
||||
ClientHookName
|
||||
} from '../../../../shared/models'
|
||||
import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager'
|
||||
import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
|
||||
import { TranslationsManager } from '../../assets/player/translations-manager'
|
||||
import { Hooks, loadPlugin, runHook } from '../../root-helpers/plugins'
|
||||
import { Tokens } from '../../root-helpers/users'
|
||||
import { peertubeLocalStorage } from '../../root-helpers/peertube-web-storage'
|
||||
import { objectToUrlEncoded } from '../../root-helpers/utils'
|
||||
import { PeerTubeEmbedApi } from './embed-api'
|
||||
import { RegisterClientHelpers } from '../../types/register-client-option.model'
|
||||
|
||||
type Translations = { [ id: string ]: string }
|
||||
|
||||
|
@ -60,6 +65,9 @@ export class PeerTubeEmbed {
|
|||
|
||||
private wrapperElement: HTMLElement
|
||||
|
||||
private peertubeHooks: Hooks = {}
|
||||
private loadedScripts = new Set<string>()
|
||||
|
||||
static async main () {
|
||||
const videoContainerId = 'video-wrapper'
|
||||
const embed = new PeerTubeEmbed(videoContainerId)
|
||||
|
@ -473,6 +481,8 @@ export class PeerTubeEmbed {
|
|||
this.PeertubePlayerManagerModulePromise
|
||||
])
|
||||
|
||||
await this.ensurePluginsAreLoaded(config, serverTranslations)
|
||||
|
||||
const videoInfo: VideoDetails = videoInfoTmp
|
||||
|
||||
const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
|
||||
|
@ -577,6 +587,8 @@ export class PeerTubeEmbed {
|
|||
this.playNextVideo()
|
||||
})
|
||||
}
|
||||
|
||||
this.runHook('action:embed.player.loaded', undefined, { player: this.player })
|
||||
}
|
||||
|
||||
private async initCore () {
|
||||
|
@ -714,6 +726,69 @@ export class PeerTubeEmbed {
|
|||
private isPlaylistEmbed () {
|
||||
return window.location.pathname.split('/')[1] === 'video-playlists'
|
||||
}
|
||||
|
||||
private async ensurePluginsAreLoaded (config: ServerConfig, translations?: { [ id: string ]: string }) {
|
||||
if (config.plugin.registered.length === 0) return
|
||||
|
||||
for (const plugin of config.plugin.registered) {
|
||||
for (const key of Object.keys(plugin.clientScripts)) {
|
||||
const clientScript = plugin.clientScripts[key]
|
||||
|
||||
if (clientScript.scopes.includes('embed') === false) continue
|
||||
|
||||
const script = `/plugins/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`
|
||||
|
||||
if (this.loadedScripts.has(script)) continue
|
||||
|
||||
const pluginInfo = {
|
||||
plugin,
|
||||
clientScript: {
|
||||
script,
|
||||
scopes: clientScript.scopes
|
||||
},
|
||||
pluginType: PluginType.PLUGIN,
|
||||
isTheme: false
|
||||
}
|
||||
|
||||
await loadPlugin(this.peertubeHooks, pluginInfo, _ => this.buildPeerTubeHelpers(translations))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private buildPeerTubeHelpers (translations?: { [ id: string ]: string }): RegisterClientHelpers {
|
||||
function unimplemented (): any {
|
||||
throw new Error('This helper is not implemented in embed.')
|
||||
}
|
||||
|
||||
return {
|
||||
getBaseStaticRoute: unimplemented,
|
||||
|
||||
getSettings: unimplemented,
|
||||
|
||||
isLoggedIn: unimplemented,
|
||||
|
||||
notifier: {
|
||||
info: unimplemented,
|
||||
error: unimplemented,
|
||||
success: unimplemented
|
||||
},
|
||||
|
||||
showModal: unimplemented,
|
||||
|
||||
markdownRenderer: {
|
||||
textMarkdownToHTML: unimplemented,
|
||||
enhancedMarkdownToHTML: unimplemented
|
||||
},
|
||||
|
||||
translate: (value: string) => {
|
||||
return Promise.resolve(peertubeTranslate(value, translations))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> {
|
||||
return runHook(this.peertubeHooks, hookName, result, params)
|
||||
}
|
||||
}
|
||||
|
||||
PeerTubeEmbed.main()
|
||||
|
|
|
@ -80,7 +80,13 @@ export const clientActionHookObject = {
|
|||
'action:router.navigation-end': true,
|
||||
|
||||
// Fired when the registration page is being initialized
|
||||
'action:signup.register.init': true
|
||||
'action:signup.register.init': true,
|
||||
|
||||
// ####### Embed hooks #######
|
||||
// In embed scope, peertube helpers are not available
|
||||
|
||||
// Fired when the embed loaded the player
|
||||
'action:embed.player.loaded': true
|
||||
}
|
||||
|
||||
export type ClientActionHookName = keyof typeof clientActionHookObject
|
||||
|
|
|
@ -1 +1 @@
|
|||
export type PluginClientScope = 'common' | 'video-watch' | 'search' | 'signup' | 'login'
|
||||
export type PluginClientScope = 'common' | 'video-watch' | 'search' | 'signup' | 'login' | 'embed'
|
||||
|
|
Loading…
Reference in a new issue