diff options
Diffstat (limited to 'client/src/app')
-rw-r--r-- | client/src/app/+videos/+video-watch/video-watch.component.ts | 6 | ||||
-rw-r--r-- | client/src/app/core/plugins/plugin.service.ts | 184 | ||||
-rw-r--r-- | client/src/app/core/theme/theme.service.ts | 1 |
3 files changed, 51 insertions, 140 deletions
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index 3fdbc0184..a444dc51f 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts | |||
@@ -12,6 +12,7 @@ import { | |||
12 | MetaService, | 12 | MetaService, |
13 | Notifier, | 13 | Notifier, |
14 | PeerTubeSocket, | 14 | PeerTubeSocket, |
15 | PluginService, | ||
15 | RestExtractor, | 16 | RestExtractor, |
16 | ScreenService, | 17 | ScreenService, |
17 | ServerService, | 18 | ServerService, |
@@ -146,6 +147,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
146 | private videoCaptionService: VideoCaptionService, | 147 | private videoCaptionService: VideoCaptionService, |
147 | private hotkeysService: HotkeysService, | 148 | private hotkeysService: HotkeysService, |
148 | private hooks: HooksService, | 149 | private hooks: HooksService, |
150 | private pluginService: PluginService, | ||
149 | private peertubeSocket: PeerTubeSocket, | 151 | private peertubeSocket: PeerTubeSocket, |
150 | private screenService: ScreenService, | 152 | private screenService: ScreenService, |
151 | private location: PlatformLocation, | 153 | private location: PlatformLocation, |
@@ -859,7 +861,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
859 | 861 | ||
860 | webtorrent: { | 862 | webtorrent: { |
861 | videoFiles: video.files | 863 | videoFiles: video.files |
862 | } | 864 | }, |
865 | |||
866 | pluginsManager: this.pluginService.getPluginsManager() | ||
863 | } | 867 | } |
864 | 868 | ||
865 | // Only set this if we're in a playlist | 869 | // Only set this if we're in a playlist |
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 @@ | |||
1 | import * as debug from 'debug' | 1 | import { Observable, of } from 'rxjs' |
2 | import { Observable, of, ReplaySubject } from 'rxjs' | 2 | import { catchError, map, shareReplay } from 'rxjs/operators' |
3 | import { catchError, first, map, shareReplay } from 'rxjs/operators' | ||
4 | import { HttpClient } from '@angular/common/http' | 3 | import { HttpClient } from '@angular/common/http' |
5 | import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core' | 4 | import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core' |
6 | import { VideoEditType } from '@app/+videos/+video-edit/shared/video-edit.type' | 5 | import { VideoEditType } from '@app/+videos/+video-edit/shared/video-edit.type' |
@@ -11,7 +10,7 @@ import { RestExtractor } from '@app/core/rest' | |||
11 | import { ServerService } from '@app/core/server/server.service' | 10 | import { ServerService } from '@app/core/server/server.service' |
12 | import { getDevLocale, isOnDevLocale } from '@app/helpers' | 11 | import { getDevLocale, isOnDevLocale } from '@app/helpers' |
13 | import { CustomModalComponent } from '@app/modal/custom-modal.component' | 12 | import { CustomModalComponent } from '@app/modal/custom-modal.component' |
14 | import { FormFields, Hooks, loadPlugin, PluginInfo, runHook } from '@root-helpers/plugins' | 13 | import { PluginInfo, PluginsManager } from '@root-helpers/plugins-manager' |
15 | import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n' | 14 | import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n' |
16 | import { | 15 | import { |
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' |
26 | import { environment } from '../../../environments/environment' | 27 | import { environment } from '../../../environments/environment' |
27 | import { RegisterClientHelpers } from '../../../types/register-client-option.model' | 28 | import { RegisterClientHelpers } from '../../../types/register-client-option.model' |
28 | 29 | ||
29 | const logger = debug('peertube:plugins') | 30 | type FormFields = { |
31 | video: { | ||
32 | commonOptions: RegisterClientFormFieldOptions | ||
33 | videoFormOptions: RegisterClientVideoFieldOptions | ||
34 | }[] | ||
35 | } | ||
30 | 36 | ||
31 | @Injectable() | 37 | @Injectable() |
32 | export class PluginService implements ClientHook { | 38 | export 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 | } |
diff --git a/client/src/app/core/theme/theme.service.ts b/client/src/app/core/theme/theme.service.ts index 0c7dec0a1..c35548798 100644 --- a/client/src/app/core/theme/theme.service.ts +++ b/client/src/app/core/theme/theme.service.ts | |||
@@ -114,6 +114,7 @@ export class ThemeService { | |||
114 | const theme = this.getTheme(currentTheme) | 114 | const theme = this.getTheme(currentTheme) |
115 | if (theme) { | 115 | if (theme) { |
116 | console.log('Adding scripts of theme %s.', currentTheme) | 116 | console.log('Adding scripts of theme %s.', currentTheme) |
117 | |||
117 | this.pluginService.addPlugin(theme, true) | 118 | this.pluginService.addPlugin(theme, true) |
118 | 119 | ||
119 | this.pluginService.reloadLoadedScopes() | 120 | this.pluginService.reloadLoadedScopes() |