aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/core/plugins/plugin.service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/core/plugins/plugin.service.ts')
-rw-r--r--client/src/app/core/plugins/plugin.service.ts184
1 files changed, 45 insertions, 139 deletions
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts
index ccbfd3e4d..bfd5ba4cc 100644
--- a/client/src/app/core/plugins/plugin.service.ts
+++ b/client/src/app/core/plugins/plugin.service.ts
@@ -1,6 +1,5 @@
1import * as debug from 'debug' 1import { Observable, of } from 'rxjs'
2import { Observable, of, ReplaySubject } from 'rxjs' 2import { catchError, map, shareReplay } from 'rxjs/operators'
3import { catchError, first, map, shareReplay } from 'rxjs/operators'
4import { HttpClient } from '@angular/common/http' 3import { HttpClient } from '@angular/common/http'
5import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core' 4import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core'
6import { VideoEditType } from '@app/+videos/+video-edit/shared/video-edit.type' 5import { VideoEditType } from '@app/+videos/+video-edit/shared/video-edit.type'
@@ -11,7 +10,7 @@ import { RestExtractor } from '@app/core/rest'
11import { ServerService } from '@app/core/server/server.service' 10import { ServerService } from '@app/core/server/server.service'
12import { getDevLocale, isOnDevLocale } from '@app/helpers' 11import { getDevLocale, isOnDevLocale } from '@app/helpers'
13import { CustomModalComponent } from '@app/modal/custom-modal.component' 12import { CustomModalComponent } from '@app/modal/custom-modal.component'
14import { FormFields, Hooks, loadPlugin, PluginInfo, runHook } from '@root-helpers/plugins' 13import { PluginInfo, PluginsManager } from '@root-helpers/plugins-manager'
15import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n' 14import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n'
16import { 15import {
17 ClientHook, 16 ClientHook,
@@ -20,49 +19,39 @@ import {
20 PluginTranslation, 19 PluginTranslation,
21 PluginType, 20 PluginType,
22 PublicServerSetting, 21 PublicServerSetting,
22 RegisterClientFormFieldOptions,
23 RegisterClientSettingsScript, 23 RegisterClientSettingsScript,
24 RegisterClientVideoFieldOptions,
24 ServerConfigPlugin 25 ServerConfigPlugin
25} from '@shared/models' 26} from '@shared/models'
26import { environment } from '../../../environments/environment' 27import { environment } from '../../../environments/environment'
27import { RegisterClientHelpers } from '../../../types/register-client-option.model' 28import { RegisterClientHelpers } from '../../../types/register-client-option.model'
28 29
29const logger = debug('peertube:plugins') 30type FormFields = {
31 video: {
32 commonOptions: RegisterClientFormFieldOptions
33 videoFormOptions: RegisterClientVideoFieldOptions
34 }[]
35}
30 36
31@Injectable() 37@Injectable()
32export class PluginService implements ClientHook { 38export class PluginService implements ClientHook {
33 private static BASE_PLUGIN_API_URL = environment.apiUrl + '/api/v1/plugins' 39 private static BASE_PLUGIN_API_URL = environment.apiUrl + '/api/v1/plugins'
34 private static BASE_PLUGIN_URL = environment.apiUrl + '/plugins' 40 private static BASE_PLUGIN_URL = environment.apiUrl + '/plugins'
35 41
36 pluginsLoaded: { [ scope in PluginClientScope ]: ReplaySubject<boolean> } = {
37 common: new ReplaySubject<boolean>(1),
38 'admin-plugin': new ReplaySubject<boolean>(1),
39 search: new ReplaySubject<boolean>(1),
40 'video-watch': new ReplaySubject<boolean>(1),
41 signup: new ReplaySubject<boolean>(1),
42 login: new ReplaySubject<boolean>(1),
43 'video-edit': new ReplaySubject<boolean>(1),
44 embed: new ReplaySubject<boolean>(1)
45 }
46
47 translationsObservable: Observable<PluginTranslation> 42 translationsObservable: Observable<PluginTranslation>
48 43
49 customModal: CustomModalComponent 44 customModal: CustomModalComponent
50 45
51 private plugins: ServerConfigPlugin[] = []
52 private helpers: { [ npmName: string ]: RegisterClientHelpers } = {} 46 private helpers: { [ npmName: string ]: RegisterClientHelpers } = {}
53 47
54 private scopes: { [ scopeName: string ]: PluginInfo[] } = {}
55
56 private loadedScripts: { [ script: string ]: boolean } = {}
57 private loadedScopes: PluginClientScope[] = []
58 private loadingScopes: { [id in PluginClientScope]?: boolean } = {}
59
60 private hooks: Hooks = {}
61 private formFields: FormFields = { 48 private formFields: FormFields = {
62 video: [] 49 video: []
63 } 50 }
64 private settingsScripts: { [ npmName: string ]: RegisterClientSettingsScript } = {} 51 private settingsScripts: { [ npmName: string ]: RegisterClientSettingsScript } = {}
65 52
53 private pluginsManager: PluginsManager
54
66 constructor ( 55 constructor (
67 private authService: AuthService, 56 private authService: AuthService,
68 private notifier: Notifier, 57 private notifier: Notifier,
@@ -74,111 +63,48 @@ export class PluginService implements ClientHook {
74 @Inject(LOCALE_ID) private localeId: string 63 @Inject(LOCALE_ID) private localeId: string
75 ) { 64 ) {
76 this.loadTranslations() 65 this.loadTranslations()
66
67 this.pluginsManager = new PluginsManager({
68 peertubeHelpersFactory: this.buildPeerTubeHelpers.bind(this),
69 onFormFields: this.onFormFields.bind(this),
70 onSettingsScripts: this.onSettingsScripts.bind(this)
71 })
77 } 72 }
78 73
79 initializePlugins () { 74 initializePlugins () {
80 const config = this.server.getHTMLConfig() 75 this.pluginsManager.loadPluginsList(this.server.getHTMLConfig())
81 this.plugins = config.plugin.registered
82
83 this.buildScopeStruct()
84 76
85 this.ensurePluginsAreLoaded('common') 77 this.pluginsManager.ensurePluginsAreLoaded('common')
86 } 78 }
87 79
88 initializeCustomModal (customModal: CustomModalComponent) { 80 initializeCustomModal (customModal: CustomModalComponent) {
89 this.customModal = customModal 81 this.customModal = customModal
90 } 82 }
91 83
92 ensurePluginsAreLoaded (scope: PluginClientScope) { 84 runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> {
93 this.loadPluginsByScope(scope) 85 return this.zone.runOutsideAngular(() => {
94 86 return this.pluginsManager.runHook(hookName, result, params)
95 return this.pluginsLoaded[scope].asObservable() 87 })
96 .pipe(first(), shareReplay())
97 .toPromise()
98 } 88 }
99 89
100 addPlugin (plugin: ServerConfigPlugin, isTheme = false) { 90 ensurePluginsAreLoaded (scope: PluginClientScope) {
101 const pathPrefix = this.getPluginPathPrefix(isTheme) 91 return this.pluginsManager.ensurePluginsAreLoaded(scope)
102
103 for (const key of Object.keys(plugin.clientScripts)) {
104 const clientScript = plugin.clientScripts[key]
105
106 for (const scope of clientScript.scopes) {
107 if (!this.scopes[scope]) this.scopes[scope] = []
108
109 this.scopes[scope].push({
110 plugin,
111 clientScript: {
112 script: `${pathPrefix}/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`,
113 scopes: clientScript.scopes
114 },
115 pluginType: isTheme ? PluginType.THEME : PluginType.PLUGIN,
116 isTheme
117 })
118
119 this.loadedScripts[clientScript.script] = false
120 }
121 }
122 } 92 }
123 93
124 removePlugin (plugin: ServerConfigPlugin) { 94 reloadLoadedScopes () {
125 for (const key of Object.keys(this.scopes)) { 95 return this.pluginsManager.reloadLoadedScopes()
126 this.scopes[key] = this.scopes[key].filter(o => o.plugin.name !== plugin.name)
127 }
128 } 96 }
129 97
130 async reloadLoadedScopes () { 98 getPluginsManager () {
131 for (const scope of this.loadedScopes) { 99 return this.pluginsManager
132 await this.loadPluginsByScope(scope, true)
133 }
134 } 100 }
135 101
136 async loadPluginsByScope (scope: PluginClientScope, isReload = false) { 102 addPlugin (plugin: ServerConfigPlugin, isTheme = false) {
137 if (this.loadingScopes[scope]) return 103 return this.pluginsManager.addPlugin(plugin, isTheme)
138 if (!isReload && this.loadedScopes.includes(scope)) return
139
140 this.loadingScopes[scope] = true
141
142 logger('Loading scope %s', scope)
143
144 try {
145 if (!isReload) this.loadedScopes.push(scope)
146
147 const toLoad = this.scopes[ scope ]
148 if (!Array.isArray(toLoad)) {
149 this.loadingScopes[scope] = false
150 this.pluginsLoaded[scope].next(true)
151
152 logger('Nothing to load for scope %s', scope)
153 return
154 }
155
156 const promises: Promise<any>[] = []
157 for (const pluginInfo of toLoad) {
158 const clientScript = pluginInfo.clientScript
159
160 if (this.loadedScripts[ clientScript.script ]) continue
161
162 promises.push(this.loadPlugin(pluginInfo))
163
164 this.loadedScripts[ clientScript.script ] = true
165 }
166
167 await Promise.all(promises)
168
169 this.pluginsLoaded[scope].next(true)
170 this.loadingScopes[scope] = false
171
172 logger('Scope %s loaded', scope)
173 } catch (err) {
174 console.error('Cannot load plugins by scope %s.', scope, err)
175 }
176 } 104 }
177 105
178 runHook <T> (hookName: ClientHookName, result?: T, params?: any): Promise<T> { 106 removePlugin (plugin: ServerConfigPlugin) {
179 return this.zone.runOutsideAngular(() => { 107 return this.pluginsManager.removePlugin(plugin)
180 return runHook(this.hooks, hookName, result, params)
181 })
182 } 108 }
183 109
184 nameToNpmName (name: string, type: PluginType) { 110 nameToNpmName (name: string, type: PluginType) {
@@ -189,12 +115,6 @@ export class PluginService implements ClientHook {
189 return prefix + name 115 return prefix + name
190 } 116 }
191 117
192 pluginTypeFromNpmName (npmName: string) {
193 return npmName.startsWith('peertube-plugin-')
194 ? PluginType.PLUGIN
195 : PluginType.THEME
196 }
197
198 getRegisteredVideoFormFields (type: VideoEditType) { 118 getRegisteredVideoFormFields (type: VideoEditType) {
199 return this.formFields.video.filter(f => f.videoFormOptions.type === type) 119 return this.formFields.video.filter(f => f.videoFormOptions.type === type)
200 } 120 }
@@ -213,27 +133,17 @@ export class PluginService implements ClientHook {
213 return helpers.translate(toTranslate) 133 return helpers.translate(toTranslate)
214 } 134 }
215 135
216 private loadPlugin (pluginInfo: PluginInfo) { 136 private onFormFields (commonOptions: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) {
217 return this.zone.runOutsideAngular(() => { 137 this.formFields.video.push({
218 const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType) 138 commonOptions,
219 139 videoFormOptions
220 const helpers = this.buildPeerTubeHelpers(pluginInfo)
221 this.helpers[npmName] = helpers
222
223 return loadPlugin({
224 hooks: this.hooks,
225 formFields: this.formFields,
226 onSettingsScripts: options => this.settingsScripts[npmName] = options,
227 pluginInfo,
228 peertubeHelpersFactory: () => helpers
229 })
230 }) 140 })
231 } 141 }
232 142
233 private buildScopeStruct () { 143 private onSettingsScripts (pluginInfo: PluginInfo, options: RegisterClientSettingsScript) {
234 for (const plugin of this.plugins) { 144 const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType)
235 this.addPlugin(plugin) 145
236 } 146 this.settingsScripts[npmName] = options
237 } 147 }
238 148
239 private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers { 149 private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers {
@@ -242,12 +152,12 @@ export class PluginService implements ClientHook {
242 152
243 return { 153 return {
244 getBaseStaticRoute: () => { 154 getBaseStaticRoute: () => {
245 const pathPrefix = this.getPluginPathPrefix(pluginInfo.isTheme) 155 const pathPrefix = PluginsManager.getPluginPathPrefix(pluginInfo.isTheme)
246 return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/static` 156 return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/static`
247 }, 157 },
248 158
249 getBaseRouterRoute: () => { 159 getBaseRouterRoute: () => {
250 const pathPrefix = this.getPluginPathPrefix(pluginInfo.isTheme) 160 const pathPrefix = PluginsManager.getPluginPathPrefix(pluginInfo.isTheme)
251 return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/router` 161 return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/router`
252 }, 162 },
253 163
@@ -324,8 +234,4 @@ export class PluginService implements ClientHook {
324 .get<PluginTranslation>(PluginService.BASE_PLUGIN_URL + '/translations/' + completeLocale + '.json') 234 .get<PluginTranslation>(PluginService.BASE_PLUGIN_URL + '/translations/' + completeLocale + '.json')
325 .pipe(shareReplay()) 235 .pipe(shareReplay())
326 } 236 }
327
328 private getPluginPathPrefix (isTheme: boolean) {
329 return isTheme ? '/themes' : '/plugins'
330 }
331} 237}