diff options
| author | Jonas Dellinger <jonas@dellinger.dev> | 2022-05-26 13:30:14 +0200 |
|---|---|---|
| committer | Jonas Dellinger <jonas@dellinger.dev> | 2022-05-26 13:30:14 +0200 |
| commit | 71dd0ea449469ed38e784b9c73b673eece680446 (patch) | |
| tree | 15914a2b7979296b8c04cac0e75191eb9f955919 /frontend/src | |
| parent | a06efc08bc01a4a014d916ff1e219a0f17d0c480 (diff) | |
| parent | 4b923c1dc70eaa4a3ca58d9e9f3218785b2fe919 (diff) | |
| download | decky-loader-71dd0ea449469ed38e784b9c73b673eece680446.tar.gz decky-loader-71dd0ea449469ed38e784b9c73b673eece680446.zip | |
Cleanup after merge
Diffstat (limited to 'frontend/src')
| -rw-r--r-- | frontend/src/components/DeckyState.tsx | 74 | ||||
| -rw-r--r-- | frontend/src/components/LegacyPlugin.tsx | 21 | ||||
| -rw-r--r-- | frontend/src/components/PluginView.tsx | 39 | ||||
| -rw-r--r-- | frontend/src/components/TitleView.tsx | 20 | ||||
| -rw-r--r-- | frontend/src/index.ts | 16 | ||||
| -rw-r--r-- | frontend/src/index.tsx | 25 | ||||
| -rw-r--r-- | frontend/src/plugin-loader.ts | 132 | ||||
| -rw-r--r-- | frontend/src/plugin-loader.tsx | 136 | ||||
| -rw-r--r-- | frontend/src/plugin.ts | 6 | ||||
| -rw-r--r-- | frontend/src/tabs-hook.ts | 4 |
10 files changed, 323 insertions, 150 deletions
diff --git a/frontend/src/components/DeckyState.tsx b/frontend/src/components/DeckyState.tsx new file mode 100644 index 00000000..cbeeb5b4 --- /dev/null +++ b/frontend/src/components/DeckyState.tsx @@ -0,0 +1,74 @@ +import { FC, createContext, useContext, useEffect, useState } from 'react'; + +import { Plugin } from '../plugin'; + +interface PublicDeckyState { + plugins: Plugin[]; + activePlugin: Plugin | null; +} + +export class DeckyState { + private _plugins: Plugin[] = []; + private _activePlugin: Plugin | null = null; + + public eventBus = new EventTarget(); + + publicState(): PublicDeckyState { + return { plugins: this._plugins, activePlugin: this._activePlugin }; + } + + setPlugins(plugins: Plugin[]) { + this._plugins = plugins; + this.notifyUpdate(); + } + + setActivePlugin(name: string) { + this._activePlugin = this._plugins.find((plugin) => plugin.name === name) ?? null; + this.notifyUpdate(); + } + + closeActivePlugin() { + this._activePlugin = null; + this.notifyUpdate(); + } + + private notifyUpdate() { + this.eventBus.dispatchEvent(new Event('update')); + } +} + +interface DeckyStateContext extends PublicDeckyState { + setActivePlugin(name: string): void; + closeActivePlugin(): void; +} + +const DeckyStateContext = createContext<DeckyStateContext>(null as any); + +export const useDeckyState = () => useContext(DeckyStateContext); + +interface Props { + deckyState: DeckyState; +} + +export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) => { + const [publicDeckyState, setPublicDeckyState] = useState<PublicDeckyState>({ ...deckyState.publicState() }); + + useEffect(() => { + function onUpdate() { + setPublicDeckyState({ ...deckyState.publicState() }); + } + + deckyState.eventBus.addEventListener('update', onUpdate); + + return () => deckyState.eventBus.removeEventListener('update', onUpdate); + }, []); + + const setActivePlugin = (name: string) => deckyState.setActivePlugin(name); + const closeActivePlugin = () => deckyState.closeActivePlugin(); + + return ( + <DeckyStateContext.Provider value={{ ...publicDeckyState, setActivePlugin, closeActivePlugin }}> + {children} + </DeckyStateContext.Provider> + ); +}; diff --git a/frontend/src/components/LegacyPlugin.tsx b/frontend/src/components/LegacyPlugin.tsx new file mode 100644 index 00000000..40f9e85f --- /dev/null +++ b/frontend/src/components/LegacyPlugin.tsx @@ -0,0 +1,21 @@ +import { VFC } from 'react'; + +// class LegacyPlugin extends React.Component { +// constructor(props: object) { +// super(props); +// } + +// render() { +// return <iframe style={{ border: 'none', width: '100%', height: '100%' }} src={this.props.url}></iframe> +// } +// } + +interface Props { + url: string; +} + +const LegacyPlugin: VFC<Props> = () => { + return <div>LegacyPlugin Hello World</div>; +}; + +export default LegacyPlugin; diff --git a/frontend/src/components/PluginView.tsx b/frontend/src/components/PluginView.tsx new file mode 100644 index 00000000..b3640395 --- /dev/null +++ b/frontend/src/components/PluginView.tsx @@ -0,0 +1,39 @@ +import { ButtonItem, DialogButton, PanelSection, PanelSectionRow } from 'decky-frontend-lib'; +import { VFC } from 'react'; +import { FaArrowLeft } from 'react-icons/fa'; + +import { useDeckyState } from './DeckyState'; + +const PluginView: VFC = () => { + const { plugins, activePlugin, setActivePlugin, closeActivePlugin } = useDeckyState(); + + if (activePlugin) { + return ( + <div> + <div style={{ position: 'absolute', top: '3px', left: '16px', zIndex: 20 }}> + <DialogButton style={{ minWidth: 0, padding: '10px 12px' }} onClick={closeActivePlugin}> + <FaArrowLeft style={{ display: 'block' }} /> + </DialogButton> + </div> + {activePlugin.content} + </div> + ); + } + + return ( + <PanelSection> + {plugins.map(({ name, icon }) => ( + <PanelSectionRow key={name}> + <ButtonItem layout="below" onClick={() => setActivePlugin(name)}> + <div style={{ display: 'flex', justifyContent: 'space-between' }}> + <div>{icon}</div> + <div>{name}</div> + </div> + </ButtonItem> + </PanelSectionRow> + ))} + </PanelSection> + ); +}; + +export default PluginView; diff --git a/frontend/src/components/TitleView.tsx b/frontend/src/components/TitleView.tsx new file mode 100644 index 00000000..4b4a6825 --- /dev/null +++ b/frontend/src/components/TitleView.tsx @@ -0,0 +1,20 @@ +import { staticClasses } from 'decky-frontend-lib'; +import { VFC } from 'react'; + +import { useDeckyState } from './DeckyState'; + +const TitleView: VFC = () => { + const { activePlugin } = useDeckyState(); + + if (activePlugin === null) { + return <div className={staticClasses.Title}>Decky</div>; + } + + return ( + <div className={staticClasses.Title} style={{ paddingLeft: '60px' }}> + {activePlugin.name} + </div> + ); +}; + +export default TitleView; diff --git a/frontend/src/index.ts b/frontend/src/index.ts deleted file mode 100644 index 390b83c9..00000000 --- a/frontend/src/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import PluginLoader from './plugin-loader'; - -declare global { - interface Window { - DeckyPluginLoader?: PluginLoader; - } -} - -if (window.DeckyPluginLoader) { - window.DeckyPluginLoader?.dismountAll(); -} - -window.DeckyPluginLoader = new PluginLoader(); -setTimeout(async () => { - window.DeckyPluginLoader?.loadAllPlugins(); -}, 5000); diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx new file mode 100644 index 00000000..89194777 --- /dev/null +++ b/frontend/src/index.tsx @@ -0,0 +1,25 @@ +import PluginLoader from './plugin-loader'; + +declare global { + interface Window { + DeckyPluginLoader: PluginLoader; + importDeckyPlugin: Function; + syncDeckyPlugins: Function; + } +} + +window.DeckyPluginLoader?.dismountAll(); + +window.DeckyPluginLoader = new PluginLoader(); +window.importDeckyPlugin = function (name: string) { + window.DeckyPluginLoader?.importPlugin(name); +}; + +window.syncDeckyPlugins = async function () { + const plugins = await (await fetch('http://127.0.0.1:1337/plugins')).json(); + for (const plugin of plugins) { + window.DeckyPluginLoader?.importPlugin(plugin); + } +}; + +setTimeout(() => window.syncDeckyPlugins(), 5000); diff --git a/frontend/src/plugin-loader.ts b/frontend/src/plugin-loader.ts deleted file mode 100644 index de4dd138..00000000 --- a/frontend/src/plugin-loader.ts +++ /dev/null @@ -1,132 +0,0 @@ -import Logger from './logger'; -import TabsHook from './tabs-hook'; - -interface Plugin { - title: any; - content: any; - icon: any; - onDismount?(): void; -} - -class PluginLoader extends Logger { - private pluginInstances: Record<string, Plugin> = {}; - private tabsHook: TabsHook; - private reloadSet = new Set(); - - constructor() { - super(PluginLoader.name); - - this.log('Initialized'); - this.tabsHook = new TabsHook(); - } - - dismountPlugin(name: string) { - this.log(`Dismounting ${name}`); - this.pluginInstances[name]?.onDismount?.(); - delete this.pluginInstances[name]; - this.tabsHook.removeById(name); - } - - async loadAllPlugins() { - this.log('Loading all plugins'); - const plugins = await (await fetch(`http://127.0.0.1:1337/plugins`)).json(); - this.log('Received:', plugins); - - return Promise.all(plugins.map((plugin) => this.loadPlugin(plugin.name))); - } - - async loadPlugin(name: string) { - this.log('Loading Plugin:', name); - - try { - if (this.reloadSet.has(name)) { - this.log('Skipping loading of', name, "since it's already loading..."); - return; - } - this.reloadSet.add(name); - - if (this.pluginInstances[name]) { - this.dismountPlugin(name); - } - - const response = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`); - const code = await response.text(); - - const pluginAPI = PluginLoader.createPluginAPI(name); - this.pluginInstances[name] = await eval(code)(pluginAPI); - - const { title, icon, content } = this.pluginInstances[name]; - this.tabsHook.add({ - id: name, - title, - icon, - content, - }); - } catch (e) { - console.error(e); - } finally { - this.reloadSet.delete(name); - } - } - - dismountAll() { - for (const name of Object.keys(this.pluginInstances)) { - this.dismountPlugin(name); - } - } - - static createPluginAPI(pluginName: string) { - return { - async callServerMethod(methodName: string, args = {}) { - const response = await fetch(`http://127.0.0.1:1337/methods/${methodName}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(args), - }); - - return response.json(); - }, - async callPluginMethod(methodName: string, args = {}) { - const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - args, - }), - }); - - return response.json(); - }, - fetchNoCors(url: string, request: any = {}) { - let args = { method: 'POST', headers: {}, body: '' }; - const req = { ...args, ...request, url, data: request.body }; - return this.callServerMethod('http_request', req); - }, - executeInTab(tab: string, runAsync: boolean, code: string) { - return this.callServerMethod('execute_in_tab', { - tab, - run_async: runAsync, - code, - }); - }, - injectCssIntoTab(tab: string, style: string) { - return this.callServerMethod('inject_css_into_tab', { - tab, - style, - }); - }, - removeCssFromTab(tab: string, cssId: any) { - return this.callServerMethod('remove_css_from_tab', { - tab, - css_id: cssId, - }); - }, - }; - } -} - -export default PluginLoader; diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx new file mode 100644 index 00000000..ddb92542 --- /dev/null +++ b/frontend/src/plugin-loader.tsx @@ -0,0 +1,136 @@ +import { FaPlug } from 'react-icons/fa'; + +import { DeckyState, DeckyStateContextProvider } from './components/DeckyState'; +import LegacyPlugin from './components/LegacyPlugin'; +import PluginView from './components/PluginView'; +import TitleView from './components/TitleView'; +import Logger from './logger'; +import { Plugin } from './plugin'; +import TabsHook from './tabs-hook'; + +declare global { + interface Window {} +} + +class PluginLoader extends Logger { + private plugins: Plugin[] = []; + private tabsHook: TabsHook = new TabsHook(); + private deckyState: DeckyState = new DeckyState(); + + constructor() { + super(PluginLoader.name); + this.log('Initialized'); + + this.tabsHook.add({ + id: 'main', + title: ( + <DeckyStateContextProvider deckyState={this.deckyState}> + <TitleView /> + </DeckyStateContextProvider> + ), + content: ( + <DeckyStateContextProvider deckyState={this.deckyState}> + <PluginView /> + </DeckyStateContextProvider> + ), + icon: <FaPlug />, + }); + } + + public dismountAll() { + for (const plugin of this.plugins) { + this.log(`Dismounting ${plugin.name}`); + plugin.onDismount?.(); + } + } + + public async importPlugin(name: string) { + this.log(`Trying to load ${name}`); + let find = this.plugins.find((x) => x.name == name); + if (find) this.plugins.splice(this.plugins.indexOf(find), 1); + if (name.startsWith('$LEGACY_')) { + await this.importLegacyPlugin(name.replace('$LEGACY_', '')); + } else { + await this.importReactPlugin(name); + } + this.log(`Loaded ${name}`); + + this.deckyState.setPlugins(this.plugins); + } + + private async importReactPlugin(name: string) { + let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`); + if (res.ok) { + let content = await eval(await res.text())(PluginLoader.createPluginAPI(name)); + this.plugins.push({ + name: name, + icon: content.icon, + content: content.content, + }); + } else throw new Error(`${name} frontend_bundle not OK`); + } + + private async importLegacyPlugin(name: string) { + const url = `http://127.0.0.1:1337/plugins/load_main/${name}`; + this.plugins.push({ + name: name, + icon: <FaPlug />, + content: <LegacyPlugin url={url} />, + }); + } + + static createPluginAPI(pluginName: string) { + return { + async callServerMethod(methodName: string, args = {}) { + const response = await fetch(`http://127.0.0.1:1337/methods/${methodName}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(args), + }); + + return response.json(); + }, + async callPluginMethod(methodName: string, args = {}) { + const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + args, + }), + }); + + return response.json(); + }, + fetchNoCors(url: string, request: any = {}) { + let args = { method: 'POST', headers: {}, body: '' }; + const req = { ...args, ...request, url, data: request.body }; + return this.callServerMethod('http_request', req); + }, + executeInTab(tab: string, runAsync: boolean, code: string) { + return this.callServerMethod('execute_in_tab', { + tab, + run_async: runAsync, + code, + }); + }, + injectCssIntoTab(tab: string, style: string) { + return this.callServerMethod('inject_css_into_tab', { + tab, + style, + }); + }, + removeCssFromTab(tab: string, cssId: any) { + return this.callServerMethod('remove_css_from_tab', { + tab, + css_id: cssId, + }); + }, + }; + } +} + +export default PluginLoader; diff --git a/frontend/src/plugin.ts b/frontend/src/plugin.ts new file mode 100644 index 00000000..2780d679 --- /dev/null +++ b/frontend/src/plugin.ts @@ -0,0 +1,6 @@ +export interface Plugin { + name: any; + content: any; + icon: any; + onDismount?(): void; +} diff --git a/frontend/src/tabs-hook.ts b/frontend/src/tabs-hook.ts index 17f41d91..dd013844 100644 --- a/frontend/src/tabs-hook.ts +++ b/frontend/src/tabs-hook.ts @@ -9,7 +9,7 @@ declare global { } } -const isTabsArray = (tabs) => { +const isTabsArray = (tabs: any) => { const length = tabs.length; return length === 7 && tabs[length - 1]?.key === 6 && tabs[length - 1]?.tab; }; @@ -35,7 +35,7 @@ class TabsHook extends Logger { const filter = Array.prototype.__filter ?? Array.prototype.filter; Array.prototype.__filter = filter; - Array.prototype.filter = function (...args) { + Array.prototype.filter = function (...args: any[]) { if (isTabsArray(this)) { self.render(this); } |
