diff options
| author | jbofill <74568881+jessebofill@users.noreply.github.com> | 2025-12-30 12:29:08 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-30 13:29:08 -0600 |
| commit | 9f586a1b97cf9069fbfbeee17e3909baf9e95f66 (patch) | |
| tree | c3477abcd8edec5fdf61251748a4f3bbef165e83 /frontend/src/plugin-loader.tsx | |
| parent | 789851579b8eaff70c2fb9da999e86d86a2d95bd (diff) | |
| download | decky-loader-9f586a1b97cf9069fbfbeee17e3909baf9e95f66.tar.gz decky-loader-9f586a1b97cf9069fbfbeee17e3909baf9e95f66.zip | |
* implement base frontend changes necessary for plugin disabling
* implement frontend diisable functions/ modal
* plugin disable boilerplate / untested
* Feat disable plugins (#810)
* implement base frontend changes necessary for plugin disabling
* implement frontend diisable functions/ modal
---------
Co-authored-by: Jesse Bofill <jesse_bofill@yahoo.com>
* fix mistakes
* add frontend
* working plugin disable, not tested extensively
* fix uninstalled hidden plugins remaining in list
* hide plugin irrelevant plugin setting menu option when disabled
* fix hidden plugin issues
* reset disabled plugin on uninstall
* fix plugin load on reenable
* move disable settings uninstall cleanup
* add engilsh tranlsations for enable/ disable elements
* fix bug where wrong loadType can get passed to importPlugin
* show correct number of hidden plugins if plugin is both hidden and disabled
* fix: get fresh list of plugin updates when changed in settings plugin list
* fix: fix invalid semver plugin version from preventing latest updates
* retain x position when changing focus in list items that have multiple horizontal focusables
* correction to pluging version checking validation
* make sure disabled plugins get checked for updates
* show number of disabled plugins at bottom of plugin view
* add notice to update modals that disabled plugins will be enabled upon installation
* run formatter
* Update backend/decky_loader/locales/en-US.json
Co-authored-by: EMERALD <hudson.samuels@gmail.com>
* chore: correct filename typo
* chore: change disabled icon
* chore: revert accidental defsettings changes
* format
* add timeout to frontend importPlugin
if a request hangs this prevent it from blocking other plugin loads.
backend diaptch_plugin which calls this for individual plugin load (as opposed to batch) is set to 15s.
other callers of importPlugin are not using timeout, same as before.
* fix plugin update checking loop
---------
Co-authored-by: marios <marios8543@gmail.com>
Co-authored-by: EMERALD <hudson.samuels@gmail.com>
Diffstat (limited to 'frontend/src/plugin-loader.tsx')
| -rw-r--r-- | frontend/src/plugin-loader.tsx | 104 |
1 files changed, 89 insertions, 15 deletions
diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index 2bdfcec1..fd4dc1c0 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -19,6 +19,7 @@ import { DeckyState, DeckyStateContextProvider, UserInfo, useDeckyState } from ' import { File, FileSelectionType } from './components/modals/filepicker'; import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches'; import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal'; +import PluginDisableModal from './components/modals/PluginDisableModal'; import PluginInstallModal from './components/modals/PluginInstallModal'; import PluginUninstallModal from './components/modals/PluginUninstallModal'; import NotificationBadge from './components/NotificationBadge'; @@ -30,7 +31,7 @@ import { FrozenPluginService } from './frozen-plugins-service'; import { HiddenPluginsService } from './hidden-plugins-service'; import Logger from './logger'; import { NotificationService } from './notification-service'; -import { InstallType, Plugin, PluginLoadType } from './plugin'; +import { DisabledPlugin, InstallType, Plugin, PluginLoadType } from './plugin'; import RouterHook from './router-hook'; import { deinitSteamFixes, initSteamFixes } from './steamfixes'; import { checkForPluginUpdates } from './store'; @@ -91,6 +92,7 @@ class PluginLoader extends Logger { DeckyBackend.addEventListener('loader/notify_updates', this.notifyUpdates.bind(this)); DeckyBackend.addEventListener('loader/import_plugin', this.importPlugin.bind(this)); DeckyBackend.addEventListener('loader/unload_plugin', this.unloadPlugin.bind(this)); + DeckyBackend.addEventListener('loader/disable_plugin', this.doDisablePlugin.bind(this)); DeckyBackend.addEventListener('loader/add_plugin_install_prompt', this.addPluginInstallPrompt.bind(this)); DeckyBackend.addEventListener( 'loader/add_multiple_plugins_install_prompt', @@ -175,7 +177,7 @@ class PluginLoader extends Logger { private getPluginsFromBackend = DeckyBackend.callable< [], - { name: string; version: string; load_type: PluginLoadType }[] + { name: string; version: string; load_type: PluginLoadType; disabled: boolean }[] >('loader/get_plugins'); private restartWebhelper = DeckyBackend.callable<[], void>('utilities/restart_webhelper'); @@ -198,10 +200,16 @@ class PluginLoader extends Logger { this.runCrashChecker(); const plugins = await this.getPluginsFromBackend(); const pluginLoadPromises = []; + const disabledPlugins: DisabledPlugin[] = []; const loadStart = performance.now(); for (const plugin of plugins) { - if (!this.hasPlugin(plugin.name)) - pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, plugin.load_type, false)); + if (plugin.disabled) { + disabledPlugins.push({ name: plugin.name, version: plugin.version }); + this.deckyState.setDisabledPlugins(disabledPlugins); + } else { + if (!this.hasPlugin(plugin.name)) + pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, plugin.load_type, false)); + } } await Promise.all(pluginLoadPromises); const loadEnd = performance.now(); @@ -252,7 +260,9 @@ class PluginLoader extends Logger { public async checkPluginUpdates() { const frozenPlugins = this.deckyState.publicState().frozenPlugins; - const updates = await checkForPluginUpdates(this.plugins.filter((p) => !frozenPlugins.includes(p.name))); + const updates = await checkForPluginUpdates( + this.deckyState.publicState().installedPlugins.filter((p) => !frozenPlugins.includes(p.name)), + ); this.deckyState.setUpdates(updates); return updates; } @@ -290,6 +300,7 @@ class PluginLoader extends Logger { version={version} hash={hash} installType={install_type} + disabled={this.deckyState.publicState().disabledPlugins.some((p) => p.name === artifact)} onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)} onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)} />, @@ -303,6 +314,7 @@ class PluginLoader extends Logger { showModal( <MultiplePluginsInstallModal requests={requests} + disabledPlugins={this.deckyState.publicState().disabledPlugins} onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)} onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)} />, @@ -310,7 +322,19 @@ class PluginLoader extends Logger { } public uninstallPlugin(name: string, title: string, buttonText: string, description: string) { - showModal(<PluginUninstallModal name={name} title={title} buttonText={buttonText} description={description} />); + showModal( + <PluginUninstallModal + name={name} + title={title} + buttonText={buttonText} + description={description} + deckyState={this.deckyState} + />, + ); + } + + public disablePlugin(name: string, title: string, buttonText: string, description: string) { + showModal(<PluginDisableModal name={name} title={title} buttonText={buttonText} description={description} />); } public hasPlugin(name: string) { @@ -351,6 +375,19 @@ class PluginLoader extends Logger { this.errorBoundaryHook.deinit(); } + public doDisablePlugin(name: string) { + const plugin = this.plugins.find((plugin) => plugin.name === name); + if (plugin == undefined) return; + + plugin?.onDismount?.(); + this.plugins = this.plugins.filter((p) => p !== plugin); + this.deckyState.setDisabledPlugins([ + ...this.deckyState.publicState().disabledPlugins, + { name: plugin.name, version: plugin.version }, + ]); + this.deckyState.setPlugins(this.plugins); + } + public unloadPlugin(name: string, skipStateUpdate: boolean = false) { const plugin = this.plugins.find((plugin) => plugin.name === name); plugin?.onDismount?.(); @@ -363,6 +400,7 @@ class PluginLoader extends Logger { version?: string | undefined, loadType: PluginLoadType = PluginLoadType.ESMODULE_V1, useQueue: boolean = true, + timeoutMS?: number, ) { if (useQueue && this.reloadLock) { this.log('Reload currently in progress, adding to queue', name); @@ -376,9 +414,11 @@ class PluginLoader extends Logger { this.unloadPlugin(name, true); const startTime = performance.now(); - await this.importReactPlugin(name, version, loadType); + + await this.importReactPlugin(name, version, loadType, timeoutMS); const endTime = performance.now(); + this.deckyState.setDisabledPlugins(this.deckyState.publicState().disabledPlugins.filter((d) => d.name !== name)); this.deckyState.setPlugins(this.plugins); this.log(`Loaded ${name} in ${endTime - startTime}ms`); } catch (e) { @@ -388,7 +428,7 @@ class PluginLoader extends Logger { this.reloadLock = false; const nextPlugin = this.pluginReloadQueue.shift(); if (nextPlugin) { - this.importPlugin(nextPlugin.name, nextPlugin.version, loadType); + this.importPlugin(nextPlugin.name, nextPlugin.version, nextPlugin.loadType, true, timeoutMS); } } } @@ -398,12 +438,28 @@ class PluginLoader extends Logger { name: string, version?: string, loadType: PluginLoadType = PluginLoadType.ESMODULE_V1, + timeoutMS?: number, ) { let spExists = this.checkForSP(); + const timeoutException = new Error( + `${name} failed to load within ${timeoutMS ? `${timeoutMS / 1000} second` : ''} time limit`, + ); + let timeout: number | undefined; + try { switch (loadType) { case PluginLoadType.ESMODULE_V1: - const plugin_exports = await import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js?t=${Date.now()}`); + const importJS = () => import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js?t=${Date.now()}`); + + const promise = + timeoutMS === undefined + ? importJS() + : Promise.race([ + importJS(), + new Promise((_, reject) => (timeout = setTimeout(() => reject(timeoutException), timeoutMS))), + ]); + + const plugin_exports = await promise; let plugin = plugin_exports.default(); this.plugins.push({ @@ -415,12 +471,26 @@ class PluginLoader extends Logger { break; case PluginLoadType.LEGACY_EVAL_IIFE: - let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, { - credentials: 'include', - headers: { - 'X-Decky-Auth': deckyAuthToken, - }, - }); + const fetchJS = async () => { + const controller = new AbortController(); + const { signal } = controller; + + if (timeoutMS !== undefined) timeout = setTimeout(() => controller.abort(), timeoutMS); + + try { + return await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, { + credentials: 'include', + headers: { + 'X-Decky-Auth': deckyAuthToken, + }, + signal, + }); + } catch (e: any) { + throw 'name' in e && e.name === 'AbortError' ? timeoutException : e; + } + }; + + let res = await fetchJS(); if (res.ok) { let plugin_export: (serverAPI: any) => Plugin = await eval( (await res.text()) + `\n//# sourceURL=decky://decky/legacy_plugin/${encodeURIComponent(name)}/index.js`, @@ -439,6 +509,8 @@ class PluginLoader extends Logger { throw new Error(`${name} has no defined loadType.`); } } catch (e) { + if (e === timeoutException) throw timeoutException; + this.error('Error loading plugin ' + name, e); const TheError: FC<{}> = () => ( <PanelSection> @@ -481,6 +553,8 @@ class PluginLoader extends Logger { body: '' + e, icon: <FaExclamationCircle />, }); + } finally { + if (timeout !== undefined) clearTimeout(timeout); } if (spExists && !this.checkForSP()) { |
