Cleanup after merge

This commit is contained in:
Jonas Dellinger
2022-05-26 13:30:14 +02:00
24 changed files with 2083 additions and 228 deletions
+74
View File
@@ -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>
);
};
+21
View File
@@ -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;
+39
View File
@@ -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;
+20
View File
@@ -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;
-16
View File
@@ -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);
+25
View File
@@ -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);
-132
View File
@@ -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;
+136
View File
@@ -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;
+6
View File
@@ -0,0 +1,6 @@
export interface Plugin {
name: any;
content: any;
icon: any;
onDismount?(): void;
}
+2 -2
View File
@@ -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);
}