diff options
Diffstat (limited to 'frontend/src/plugin-loader.tsx')
| -rw-r--r-- | frontend/src/plugin-loader.tsx | 147 |
1 files changed, 108 insertions, 39 deletions
diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index 4d3415c8..e7fc7031 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -1,13 +1,27 @@ -import { ModalRoot, QuickAccessTab, Router, SteamSpinner, showModal, sleep, staticClasses } from 'decky-frontend-lib'; -import { Suspense, lazy } from 'react'; +import { + ConfirmModal, + ModalRoot, + Patch, + QuickAccessTab, + Router, + callOriginal, + findModuleChild, + replacePatch, + showModal, + sleep, + staticClasses, +} from 'decky-frontend-lib'; +import { lazy } from 'react'; import { FaPlug } from 'react-icons/fa'; import { DeckyState, DeckyStateContextProvider, useDeckyState } from './components/DeckyState'; import LegacyPlugin from './components/LegacyPlugin'; +import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches'; import PluginInstallModal from './components/modals/PluginInstallModal'; import NotificationBadge from './components/NotificationBadge'; import PluginView from './components/PluginView'; import TitleView from './components/TitleView'; +import WithSuspense from './components/WithSuspense'; import Logger from './logger'; import { Plugin } from './plugin'; import RouterHook from './router-hook'; @@ -16,6 +30,11 @@ import TabsHook from './tabs-hook'; import Toaster from './toaster'; import { VerInfo, callUpdaterMethod } from './updater'; +const StorePage = lazy(() => import('./components/store/Store')); +const SettingsPage = lazy(() => import('./components/settings')); + +const FilePicker = lazy(() => import('./components/modals/filepicker')); + declare global { interface Window {} } @@ -32,11 +51,13 @@ class PluginLoader extends Logger { // stores a list of plugin names which requested to be reloaded private pluginReloadQueue: { name: string; version?: string }[] = []; + private focusWorkaroundPatch?: Patch; + constructor() { super(PluginLoader.name); this.log('Initialized'); - const TabIcon = () => { + const TabBadge = () => { const { updates, hasLoaderUpdate } = useDeckyState(); return <NotificationBadge show={(updates && updates.size > 0) || hasLoaderUpdate} />; }; @@ -53,57 +74,72 @@ class PluginLoader extends Logger { icon: ( <DeckyStateContextProvider deckyState={this.deckyState}> <FaPlug /> - <TabIcon /> + <TabBadge /> </DeckyStateContextProvider> ), }); - const StorePage = lazy(() => import('./components/store/Store')); - const SettingsPage = lazy(() => import('./components/settings')); - this.routerHook.addRoute('/decky/store', () => ( - <Suspense - fallback={ - <div - style={{ - marginTop: '40px', - height: 'calc( 100% - 40px )', - overflowY: 'scroll', - }} - > - <SteamSpinner /> - </div> - } - > + <WithSuspense route={true}> <StorePage /> - </Suspense> + </WithSuspense> )); this.routerHook.addRoute('/decky/settings', () => { return ( <DeckyStateContextProvider deckyState={this.deckyState}> - <Suspense - fallback={ - <div - style={{ - marginTop: '40px', - height: 'calc( 100% - 40px )', - overflowY: 'scroll', - }} - > - <SteamSpinner /> - </div> - } - > + <WithSuspense route={true}> <SettingsPage /> - </Suspense> + </WithSuspense> </DeckyStateContextProvider> ); }); + + initFilepickerPatches(); + + this.updateVersion(); + + const self = this; + + try { + // TODO remove all of this once Valve fixes the bug + const focusManager = findModuleChild((m) => { + if (typeof m !== 'object') return false; + for (let prop in m) { + if (m[prop]?.prototype?.TakeFocus) return m[prop]; + } + return false; + }); + + this.focusWorkaroundPatch = replacePatch(focusManager.prototype, 'TakeFocus', function () { + // @ts-ignore + const classList = this.m_node?.m_element.classList; + if ( + // @ts-ignore + (this.m_node?.m_element && classList.contains(staticClasses.TabGroupPanel)) || + classList.contains('FriendsListTab') || + classList.contains('FriendsTabList') || + classList.contains('FriendsListAndChatsSteamDeck') + ) { + self.debug('Intercepted friends re-focus'); + return true; + } + + return callOriginal; + }); + } catch (e) { + this.error('Friends focus patch failed', e); + } } - public async notifyUpdates() { + public async updateVersion() { const versionInfo = (await callUpdaterMethod('get_version')).result as VerInfo; this.deckyState.setVersionInfo(versionInfo); + + return versionInfo; + } + + public async notifyUpdates() { + const versionInfo = await this.updateVersion(); if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) { this.toaster.toast({ title: 'Decky', @@ -147,7 +183,7 @@ class PluginLoader extends Logger { public uninstallPlugin(name: string) { showModal( - <ModalRoot + <ConfirmModal onOK={async () => { await this.callServerMethod('uninstall_plugin', { name }); }} @@ -158,7 +194,7 @@ class PluginLoader extends Logger { <div className={staticClasses.Title} style={{ flexDirection: 'column' }}> Uninstall {name}? </div> - </ModalRoot>, + </ConfirmModal>, ); } @@ -176,6 +212,8 @@ class PluginLoader extends Logger { public deinit() { this.routerHook.removeRoute('/decky/store'); this.routerHook.removeRoute('/decky/settings'); + deinitFilepickerPatches(); + this.focusWorkaroundPatch?.unpatch(); } public unloadPlugin(name: string) { @@ -225,7 +263,8 @@ class PluginLoader extends Logger { }, }); if (res.ok) { - let plugin = await eval(await res.text())(this.createPluginAPI(name)); + let plugin_export = await eval(await res.text()); + let plugin = plugin_export(this.createPluginAPI(name)); this.plugins.push({ ...plugin, name: name, @@ -257,11 +296,41 @@ class PluginLoader extends Logger { return response.json(); } + openFilePicker( + startPath: string, + includeFiles?: boolean, + regex?: RegExp, + ): Promise<{ path: string; realpath: string }> { + return new Promise((resolve, reject) => { + const Content = ({ closeModal }: { closeModal?: () => void }) => ( + // Purposely outside of the FilePicker component as lazy-loaded ModalRoots don't focus correctly + <ModalRoot + onCancel={() => { + reject('User canceled'); + closeModal?.(); + }} + > + <WithSuspense> + <FilePicker + startPath={startPath} + includeFiles={includeFiles} + regex={regex} + onSubmit={resolve} + closeModal={closeModal} + /> + </WithSuspense> + </ModalRoot> + ); + showModal(<Content />); + }); + } + createPluginAPI(pluginName: string) { return { routerHook: this.routerHook, toaster: this.toaster, callServerMethod: this.callServerMethod, + openFilePicker: this.openFilePicker, async callPluginMethod(methodName: string, args = {}) { const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, { method: 'POST', |
