summaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/components/DeckyState.tsx74
-rw-r--r--frontend/src/components/LegacyPlugin.tsx21
-rw-r--r--frontend/src/components/PluginView.tsx39
-rw-r--r--frontend/src/components/TitleView.tsx20
-rw-r--r--frontend/src/index.ts16
-rw-r--r--frontend/src/index.tsx25
-rw-r--r--frontend/src/plugin-loader.ts132
-rw-r--r--frontend/src/plugin-loader.tsx136
-rw-r--r--frontend/src/plugin.ts6
-rw-r--r--frontend/src/tabs-hook.ts4
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);
}