mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 00:37:49 +00:00
Cleanup after merge
This commit is contained in:
@@ -1 +1,4 @@
|
||||
node_modules/
|
||||
|
||||
.yalc
|
||||
yalc.lock
|
||||
|
||||
Generated
+1542
-15
File diff suppressed because it is too large
Load Diff
+13
-3
@@ -10,18 +10,28 @@
|
||||
"format": "prettier -c src -w"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^22.0.0",
|
||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||
"@rollup/plugin-commonjs": "^21.1.0",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^13.2.1",
|
||||
"@rollup/plugin-replace": "^4.0.0",
|
||||
"@rollup/plugin-typescript": "^8.3.2",
|
||||
"@types/react": "16.14.0",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"husky": "^8.0.1",
|
||||
"import-sort-style-module": "^6.0.0",
|
||||
"prettier": "^2.6.2",
|
||||
"prettier-plugin-import-sort": "^0.0.7",
|
||||
"rollup": "^2.71.1"
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"rollup": "^2.70.2"
|
||||
},
|
||||
"importSort": {
|
||||
".js, .jsx, .ts, .tsx": {
|
||||
"style": "module"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"decky-frontend-lib": "^0.0.3",
|
||||
"react-icons": "^4.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import json from '@rollup/plugin-json';
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import { defineConfig } from 'rollup';
|
||||
|
||||
|
||||
/** @type {import('rollup').RollupOptions} */
|
||||
const options = {
|
||||
input: 'src/index.ts',
|
||||
export default defineConfig({
|
||||
input: 'src/index.tsx',
|
||||
plugins: [
|
||||
commonjs(),
|
||||
nodeResolve(),
|
||||
typescript(),
|
||||
json(),
|
||||
replace({
|
||||
preventAssignment: false,
|
||||
'process.env.NODE_ENV': JSON.stringify('production'),
|
||||
}),
|
||||
],
|
||||
external: ["react", "react-dom"],
|
||||
output: {
|
||||
file: '../backend/static/plugin-loader.iife.js',
|
||||
globals: {
|
||||
react: 'SP_REACT',
|
||||
'react-dom': 'SP_REACTDOM',
|
||||
},
|
||||
format: 'iife',
|
||||
},
|
||||
plugins: [commonjs(), resolve(), typescript()]
|
||||
}
|
||||
|
||||
export default options
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface Plugin {
|
||||
name: any;
|
||||
content: any;
|
||||
icon: any;
|
||||
onDismount?(): void;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"module": "ESNext",
|
||||
"target": "ES2020",
|
||||
"jsx": "react-jsx",
|
||||
"declaration": false,
|
||||
"moduleResolution": "node",
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"esModuleInterop": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitAny": true,
|
||||
"strict": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user