diff options
Diffstat (limited to 'frontend/src')
| -rw-r--r-- | frontend/src/components/DeckyState.tsx | 74 | ||||
| -rw-r--r-- | frontend/src/components/LegacyPlugin.tsx | 26 | ||||
| -rw-r--r-- | frontend/src/components/PluginView.tsx | 69 | ||||
| -rw-r--r-- | frontend/src/components/TitleView.tsx | 49 | ||||
| -rw-r--r-- | frontend/src/index.tsx | 18 | ||||
| -rw-r--r-- | frontend/src/plugin-loader.tsx | 98 | ||||
| -rw-r--r-- | frontend/src/plugin.ts | 6 | ||||
| -rw-r--r-- | frontend/src/tabs-hook.ts | 4 |
8 files changed, 206 insertions, 138 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 index 86abf2c8..40f9e85f 100644 --- a/frontend/src/components/LegacyPlugin.tsx +++ b/frontend/src/components/LegacyPlugin.tsx @@ -1,13 +1,21 @@ -import React from "react" +import { VFC } from 'react'; -class LegacyPlugin extends React.Component { - constructor(props: object) { - super(props); - } +// 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> - } +// render() { +// return <iframe style={{ border: 'none', width: '100%', height: '100%' }} src={this.props.url}></iframe> +// } +// } + +interface Props { + url: string; } -export default LegacyPlugin;
\ No newline at end of file +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 index 27cb386a..b3640395 100644 --- a/frontend/src/components/PluginView.tsx +++ b/frontend/src/components/PluginView.tsx @@ -1,40 +1,39 @@ -import { Button } from "decky-frontend-lib"; -import React from "react" +import { ButtonItem, DialogButton, PanelSection, PanelSectionRow } from 'decky-frontend-lib'; +import { VFC } from 'react'; +import { FaArrowLeft } from 'react-icons/fa'; -class PluginView extends React.Component<{}, { runningPlugin: string, plugins: Array<any> }> { - constructor() { - super({}); - this.state = { - plugins: [], - runningPlugin: "" - } - } +import { useDeckyState } from './DeckyState'; - componentDidMount() { - window.__DeckyEvLoop.addEventListener("pluginClose", (_) => { this.setState({ runningPlugin: "", plugins: this.state.plugins }) }); - window.__DeckyEvLoop.addEventListener("setPlugins", (ev) => { console.log(ev); this.setState({ plugins: ev.data, runningPlugin: this.state.runningPlugin }) }); - } +const PluginView: VFC = () => { + const { plugins, activePlugin, setActivePlugin, closeActivePlugin } = useDeckyState(); - private openPlugin(name: string) { - const ev = new Event("pluginOpen"); - ev.data = name; - window.__DeckyEvLoop.dispatchEvent(ev); - this.setState({ runningPlugin: name, plugins: this.state.plugins }) - } + 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> + ); + } - render() { - if (this.state.runningPlugin) { - return this.state.plugins.find(x => x.name == this.state.runningPlugin).content; - } - else { - let buttons = []; - for (const plugin of this.state.plugins) { - buttons.push(<Button layout="below" onClick={(_) => this.openPlugin(plugin.name)}>{plugin.icon}{plugin.name}</Button>) - } - if (buttons.length == 0) return <div className='staticClasses.Text'>No plugins...</div>; - return buttons; - } - } -} + 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;
\ No newline at end of file +export default PluginView; diff --git a/frontend/src/components/TitleView.tsx b/frontend/src/components/TitleView.tsx index e0a8552f..4b4a6825 100644 --- a/frontend/src/components/TitleView.tsx +++ b/frontend/src/components/TitleView.tsx @@ -1,39 +1,20 @@ -import { Button, staticClasses } from "decky-frontend-lib"; -import React from "react" -import { FaArrowCircleLeft, FaShoppingBag } from "react-icons/fa" +import { staticClasses } from 'decky-frontend-lib'; +import { VFC } from 'react'; -class TitleView extends React.Component<{}, { runningPlugin: string }> { - constructor() { - super({}); - this.state = { - runningPlugin: "" - } - } +import { useDeckyState } from './DeckyState'; - componentDidMount() { - window.__DeckyEvLoop.addEventListener("pluginOpen", (ev) => this.setState({ runningPlugin: ev.data })); - window.__DeckyEvLoop.addEventListener("pluginClose", (_) => this.setState({ runningPlugin: "" })); - } +const TitleView: VFC = () => { + const { activePlugin } = useDeckyState(); - private openPluginStore() { - fetch("http://127.0.0.1:1337/methods/open_plugin_store", {method: "POST"}) - } + if (activePlugin === null) { + return <div className={staticClasses.Title}>Decky</div>; + } - render() { - if (this.state.runningPlugin) - return <div className={staticClasses.Title}> - <Button bottomSeparator={false} onClick={(_) => { - window.__DeckyEvLoop.dispatchEvent(new Event("pluginClose")); - this.setState({ runningPlugin: "" }); - }}><FaArrowCircleLeft /></Button> - {this.state.runningPlugin} - </div> - else - return <div className={staticClasses.Title}> - Plugins - <Button bottomSeparator={false} onClick={ (_) => this.openPluginStore() }><FaShoppingBag /></Button> - </div> - } -} + return ( + <div className={staticClasses.Title} style={{ paddingLeft: '60px' }}> + {activePlugin.name} + </div> + ); +}; -export default TitleView;
\ No newline at end of file +export default TitleView; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 13118ca3..89194777 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,4 +1,4 @@ -import PluginLoader from "./plugin-loader" +import PluginLoader from './plugin-loader'; declare global { interface Window { @@ -7,15 +7,19 @@ declare global { syncDeckyPlugins: Function; } } + +window.DeckyPluginLoader?.dismountAll(); + window.DeckyPluginLoader = new PluginLoader(); -window.importDeckyPlugin = function(name: string) { +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(); +}; + +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);
\ No newline at end of file +setTimeout(() => window.syncDeckyPlugins(), 5000); diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index cf15e099..ddb92542 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -1,64 +1,61 @@ +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'; -import { FaPlug } from "react-icons/fa"; - -import PluginView from "./components/PluginView"; -import TitleView from "./components/TitleView"; -import LegacyPlugin from "./components/LegacyPlugin" - -interface Plugin { - name: any; - content: any; - icon: any; - onDismount?(): void; -} declare global { - interface Window { - __DeckyEvLoop: PluginEventTarget; - __DeckyRunningPlugin: string; - } + interface Window {} } -class PluginEventTarget extends EventTarget { } -window.__DeckyEvLoop = new PluginEventTarget(); 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: <TitleView />, - content: <PluginView />, - icon: <FaPlug /> + id: 'main', + title: ( + <DeckyStateContextProvider deckyState={this.deckyState}> + <TitleView /> + </DeckyStateContextProvider> + ), + content: ( + <DeckyStateContextProvider deckyState={this.deckyState}> + <PluginView /> + </DeckyStateContextProvider> + ), + icon: <FaPlug />, }); - SteamClient.Input.RegisterForControllerInputMessages(this.handleBack); - window.__DeckyEvLoop.addEventListener("pluginOpen", (x) => window.__DeckyRunningPlugin = x.data); - window.__DeckyEvLoop.addEventListener("pluginClose", (_) => window.__DeckyRunningPlugin = ""); } - private handleBack(ev) { - const e = ev[0]; - if (e.strActionName == "B" && window.__DeckyRunningPlugin) - window.__DeckyEvLoop.dispatchEvent(new Event("pluginClose")); + 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_")) - this.importLegacyPlugin(name.replace("$LEGACY_", "")); - else - this.importReactPlugin(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}`); - const ev = new Event("setPlugins"); - ev.data = this.plugins; - window.__DeckyEvLoop.dispatchEvent(ev); + + this.deckyState.setPlugins(this.plugins); } private async importReactPlugin(name: string) { @@ -68,10 +65,9 @@ class PluginLoader extends Logger { this.plugins.push({ name: name, icon: content.icon, - content: content.content + content: content.content, }); - } - else throw new Error(`${name} frontend_bundle not OK`); + } else throw new Error(`${name} frontend_bundle not OK`); } private async importLegacyPlugin(name: string) { @@ -79,13 +75,13 @@ class PluginLoader extends Logger { this.plugins.push({ name: name, icon: <FaPlug />, - content: <LegacyPlugin url={ url } /> + content: <LegacyPlugin url={url} />, }); } - static createPluginAPI(pluginName) { + static createPluginAPI(pluginName: string) { return { - async callServerMethod(methodName, args = {}) { + async callServerMethod(methodName: string, args = {}) { const response = await fetch(`http://127.0.0.1:1337/methods/${methodName}`, { method: 'POST', headers: { @@ -96,7 +92,7 @@ class PluginLoader extends Logger { return response.json(); }, - async callPluginMethod(methodName, args = {}) { + async callPluginMethod(methodName: string, args = {}) { const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, { method: 'POST', headers: { @@ -109,25 +105,25 @@ class PluginLoader extends Logger { return response.json(); }, - fetchNoCors(url, request: any = {}) { + 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, runAsync, code) { + executeInTab(tab: string, runAsync: boolean, code: string) { return this.callServerMethod('execute_in_tab', { tab, run_async: runAsync, code, }); }, - injectCssIntoTab(tab, style) { + injectCssIntoTab(tab: string, style: string) { return this.callServerMethod('inject_css_into_tab', { tab, style, }); }, - removeCssFromTab(tab, cssId) { + removeCssFromTab(tab: string, cssId: any) { return this.callServerMethod('remove_css_from_tab', { tab, css_id: cssId, @@ -137,4 +133,4 @@ class PluginLoader extends Logger { } } -export default PluginLoader;
\ No newline at end of file +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); } |
