display overhaul, compatibility with legacy plugins, fixes

This commit is contained in:
marios
2022-05-26 04:00:18 +03:00
parent 74438a3145
commit 4b923c1dc7
20 changed files with 2014 additions and 229 deletions
+13
View File
@@ -0,0 +1,13 @@
import React 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>
}
}
export default LegacyPlugin;
+40
View File
@@ -0,0 +1,40 @@
import { Button } from "decky-frontend-lib";
import React from "react"
class PluginView extends React.Component<{}, { runningPlugin: string, plugins: Array<any> }> {
constructor() {
super({});
this.state = {
plugins: [],
runningPlugin: ""
}
}
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 }) });
}
private openPlugin(name: string) {
const ev = new Event("pluginOpen");
ev.data = name;
window.__DeckyEvLoop.dispatchEvent(ev);
this.setState({ runningPlugin: name, plugins: this.state.plugins })
}
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;
}
}
}
export default PluginView;
+39
View File
@@ -0,0 +1,39 @@
import { Button, staticClasses } from "decky-frontend-lib";
import React from "react"
import { FaArrowCircleLeft, FaShoppingBag } from "react-icons/fa"
class TitleView extends React.Component<{}, { runningPlugin: string }> {
constructor() {
super({});
this.state = {
runningPlugin: ""
}
}
componentDidMount() {
window.__DeckyEvLoop.addEventListener("pluginOpen", (ev) => this.setState({ runningPlugin: ev.data }));
window.__DeckyEvLoop.addEventListener("pluginClose", (_) => this.setState({ runningPlugin: "" }));
}
private openPluginStore() {
fetch("http://127.0.0.1:1337/methods/open_plugin_store", {method: "POST"})
}
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>
}
}
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);
+21
View File
@@ -0,0 +1,21 @@
import PluginLoader from "./plugin-loader"
declare global {
interface Window {
DeckyPluginLoader: PluginLoader;
importDeckyPlugin: Function;
syncDeckyPlugins: Function;
}
}
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);
-131
View File
@@ -1,131 +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 lock = 0;
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) {
this.log('Loading Plugin:', name);
try {
while (this.lock === 1) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
this.lock = 1;
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.lock = 0;
}
}
dismountAll() {
for (const name of Object.keys(this.pluginInstances)) {
this.dismountPlugin(name);
}
}
static createPluginAPI(pluginName) {
return {
async callServerMethod(methodName, 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, 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, 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) {
return this.callServerMethod('execute_in_tab', {
tab,
run_async: runAsync,
code,
});
},
injectCssIntoTab(tab, style) {
return this.callServerMethod('inject_css_into_tab', {
tab,
style,
});
},
removeCssFromTab(tab, cssId) {
return this.callServerMethod('remove_css_from_tab', {
tab,
css_id: cssId,
});
},
};
}
}
export default PluginLoader;
+140
View File
@@ -0,0 +1,140 @@
import Logger from './logger';
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;
}
}
class PluginEventTarget extends EventTarget { }
window.__DeckyEvLoop = new PluginEventTarget();
class PluginLoader extends Logger {
private plugins: Plugin[] = [];
private tabsHook: TabsHook = new TabsHook();
constructor() {
super(PluginLoader.name);
this.log('Initialized');
this.tabsHook.add({
id: "main",
title: <TitleView />,
content: <PluginView />,
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 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);
this.log(`Loaded ${name}`);
const ev = new Event("setPlugins");
ev.data = this.plugins;
window.__DeckyEvLoop.dispatchEvent(ev);
}
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) {
return {
async callServerMethod(methodName, 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, 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, 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) {
return this.callServerMethod('execute_in_tab', {
tab,
run_async: runAsync,
code,
});
},
injectCssIntoTab(tab, style) {
return this.callServerMethod('inject_css_into_tab', {
tab,
style,
});
},
removeCssFromTab(tab, cssId) {
return this.callServerMethod('remove_css_from_tab', {
tab,
css_id: cssId,
});
},
};
}
}
export default PluginLoader;