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
+6 -3
View File
@@ -5,10 +5,13 @@
"name": "Debug",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/plugin_loader/main.py",
"preLaunchTask": "Stop Service",
"program": "${workspaceFolder}/backend/main.py",
"cwd": "${workspaceFolder}/backend",
"console": "integratedTerminal",
"justMyCode": true
"env": {
"PLUGIN_PATH": "/home/deck/homebrew/plugins"
},
"preLaunchTask": "Build frontend"
}
]
}
+6 -1
View File
@@ -5,6 +5,11 @@
"label": "Stop Service",
"type": "shell",
"command":"systemctl --user stop plugin_loader",
}
},
{
"label": "Build frontend",
"type": "shell",
"command":"cd ${workspaceFolder}/frontend; npm run build",
}
]
}
+6
View File
@@ -1,3 +1,9 @@
# TODO
- Fix button size/display
- Add plugin installation prompts for browser
- Fix components not updating unless tab opened first (with new tab hook)
- Clean up code
# Plugin Loader [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/ZU74G2NJzk)
![steamuserimages-a akamaihd](https://user-images.githubusercontent.com/10835354/161068262-ca723dc5-6795-417a-80f6-d8c1f9d03e93.jpg)
+1 -1
View File
@@ -26,7 +26,7 @@ class PluginBrowser:
server_instance.add_routes([
web.post("/browser/install_plugin", self.install_plugin),
web.get("/browser/iframe", self.redirect_to_store)
web.get("/browser/redirect", self.redirect_to_store)
])
def _unzip_to_plugin_dir(self, zip, name, hash):
+16
View File
@@ -48,6 +48,10 @@ class Tab:
await self.client.close()
return res
async def get_steam_resource(self, url):
res = await self.evaluate_js(f'(async function test() {{ return await (await fetch("{url}")).text() }})()', True)
return res["result"]["result"]["value"]
def __repr__(self):
return self.title
@@ -93,3 +97,15 @@ async def tab_has_global_var(tab_name, var_name):
return False
return res["result"]["result"]["value"]
async def tab_has_element(tab_name, element_name):
try:
tab = await get_tab(tab_name)
except ValueError:
return False
res = await tab.evaluate_js(f"document.getElementById('{element_name}') != null", False)
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
return False
return res["result"]["result"]["value"]
+56 -35
View File
@@ -1,17 +1,16 @@
from asyncio import Queue
from json.decoder import JSONDecodeError
from logging import getLogger
from os import listdir, path
from pathlib import Path
from time import time
from traceback import print_exc
from aiohttp import web
from aiohttp_jinja2 import template
from genericpath import exists
from watchdog.events import RegexMatchingEventHandler
from watchdog.observers.inotify import InotifyObserver as Observer
from injector import inject_to_tab
from injector import get_tab, inject_to_tab
from plugin import PluginWrapper
@@ -62,7 +61,6 @@ class Loader:
self.plugin_path = plugin_path
self.logger.info(f"plugin_path: {self.plugin_path}")
self.plugins = {}
self.import_plugins()
if live_reload:
self.reload_queue = Queue()
@@ -72,16 +70,20 @@ class Loader:
self.loop.create_task(self.handle_reloads())
server_instance.add_routes([
web.get("/plugins", self.handle_plugins),
web.get("/plugins", self.get_plugins),
web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle),
web.post("/plugins/{plugin_name}/methods/{method_name}", self.handle_plugin_method_call),
web.get("/plugins/{plugin_name}/assets/{path:.*}", self.handle_frontend_assets),
web.post("/methods/{method_name}", self.handle_server_method_call)
# The following is legacy plugin code.
web.get("/plugins/load_main/{name}", self.load_plugin_main_view),
web.get("/plugins/plugin_resource/{name}/{path:.+}", self.handle_sub_route),
web.get("/steam_resource/{path:.+}", self.get_steam_resource)
])
def handle_plugins(self, request):
plugins = list(map(lambda kv: dict([("name", kv[0])]), self.plugins.items()))
return web.json_response(plugins)
async def get_plugins(self, request):
plugins = list(self.plugins.values())
return web.json_response([str(i) if not i.legacy else "$LEGACY_"+str(i) for i in plugins])
def handle_frontend_assets(self, request):
plugin = self.plugins[request.match_info["plugin_name"]]
@@ -109,14 +111,13 @@ class Loader:
self.logger.info(f"Plugin {plugin.name} is passive")
self.plugins[plugin.name] = plugin.start()
self.logger.info(f"Loaded {plugin.name}")
if refresh:
self.loop.create_task(self.reload_frontend_plugin(plugin.name))
#self.loop.create_task(self.dispatch_plugin(plugin.name))
except Exception as e:
self.logger.error(f"Could not load {file}. {e}")
print_exc()
async def reload_frontend_plugin(self, name):
await inject_to_tab("SP", f"window.DeckyPluginLoader?.loadPlugin('{name}')")
async def dispatch_plugin(self, name):
await inject_to_tab("SP", f"window.importDeckyPlugin('{name}')")
def import_plugins(self):
self.logger.info(f"import plugins from {self.plugin_path}")
@@ -131,38 +132,58 @@ class Loader:
args = await self.reload_queue.get()
self.import_plugin(*args)
async def handle_server_method_call(self, request):
method_name = request.match_info["method_name"]
method_info = await request.json()
args = method_info["args"]
res = {}
try:
r = await self.utilities.util_methods[method_name](**args)
res["result"] = r
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return web.json_response(res)
async def handle_plugin_method_call(self, request):
res = {}
plugin = self.plugins[request.match_info["plugin_name"]]
method_name = request.match_info["method_name"]
method_info = await request.json()
args = method_info["args"]
try:
method_info = await request.json()
args = method_info["args"]
except JSONDecodeError:
args = {}
try:
if method_name.startswith("_"):
raise RuntimeError("Tried to call private method")
res["result"] = await plugin.execute_method(method_name, args)
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return web.json_response(res)
"""
The following methods are used to load legacy plugins, which are considered deprecated.
I made the choice to re-add them so that the first iteration/version of the react loader
can work as a drop-in replacement for the stable branch of the PluginLoader, so that we
can introduce it more smoothly and give people the chance to sample the new features even
without plugin support. They will be removed once legacy plugins are no longer relevant.
"""
async def load_plugin_main_view(self, request):
plugin = self.plugins[request.match_info["name"]]
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), 'r') as template:
template_data = template.read()
ret = f"""
<script src="/static/legacy-library.js"></script>
<script>const plugin_name = '{plugin.name}' </script>
<base href="http://127.0.0.1:1337/plugins/plugin_resource/{plugin.name}/">
{template_data}
"""
return web.Response(text=ret, content_type="text/html")
async def handle_sub_route(self, request):
plugin = self.plugins[request.match_info["name"]]
route_path = request.match_info["path"]
self.logger.info(path)
ret = ""
file_path = path.join(self.plugin_path, plugin.plugin_directory, route_path)
with open(file_path, 'r') as resource_data:
ret = resource_data.read()
return web.Response(text=ret)
async def get_steam_resource(self, request):
tab = await get_tab("QuickAccess")
try:
return web.Response(text=await tab.get_steam_resource(f"https://steamloopback.host/{request.match_info['path']}"), content_type="text/html")
except Exception as e:
return web.Response(text=str(e), status=400)
+20 -7
View File
@@ -1,6 +1,8 @@
from logging import DEBUG, INFO, basicConfig, getLogger
from os import getenv
from aiohttp import ClientSession
CONFIG = {
"plugin_path": getenv("PLUGIN_PATH", "/home/deck/homebrew/plugins"),
"chown_plugin_path": getenv("CHOWN_PLUGIN_PATH", "1") == "1",
@@ -8,7 +10,7 @@ CONFIG = {
"server_port": int(getenv("SERVER_PORT", "1337")),
"live_reload": getenv("LIVE_RELOAD", "1") == "1",
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING":30, "INFO": 20, "DEBUG": 10}[getenv("LOG_LEVEL", "INFO")],
"store_url": getenv("STORE_URL", "https://sdh.tzatzi.me/browse")
"store_url": getenv("STORE_URL", "https://beta.deckbrew.xyz")
}
basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s")
@@ -21,10 +23,9 @@ from subprocess import Popen
import aiohttp_cors
from aiohttp.web import Application, run_app, static
from aiohttp_jinja2 import setup as jinja_setup
from jinja2 import FileSystemLoader
from browser import PluginBrowser
from injector import get_tab, inject_to_tab, tab_has_global_var
from injector import inject_to_tab, tab_has_global_var
from loader import Loader
from utilities import Utilities
@@ -48,22 +49,34 @@ class PluginManager:
jinja_setup(self.web_app)
self.web_app.on_startup.append(self.inject_javascript)
if CONFIG["chown_plugin_path"] == True:
self.web_app.on_startup.append(chown_plugin_dir)
self.loop.create_task(self.loader_reinjector())
self.loop.create_task(self.load_plugins())
self.loop.set_exception_handler(self.exception_handler)
for route in list(self.web_app.router.routes()):
self.cors.add(route)
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
def exception_handler(self, loop, context):
if context["message"] == "Unclosed connection":
return
loop.default_exception_handler(context)
async def wait_for_server(self):
async with ClientSession() as web:
while True:
try:
await web.get(f"http://{CONFIG['server_host']}:{CONFIG['server_port']}")
return
except Exception as e:
await sleep(0.1)
async def load_plugins(self):
await self.wait_for_server()
self.plugin_loader.import_plugins()
#await inject_to_tab("SP", "window.syncDeckyPlugins();")
async def loader_reinjector(self):
while True:
await sleep(1)
+8 -2
View File
@@ -10,8 +10,6 @@ from signal import SIGINT, signal
from sys import exit
from time import time
from injector import inject_to_tab
class PluginWrapper:
def __init__(self, file, plugin_directory, plugin_path) -> None:
@@ -24,12 +22,20 @@ class PluginWrapper:
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r"))
self.legacy = False
self.main_view_html = json["main_view_html"] if "main_view_html" in json else ""
self.tile_view_html = json["tile_view_html"] if "tile_view_html" in json else ""
self.legacy = self.main_view_html or self.tile_view_html
self.name = json["name"]
self.author = json["author"]
self.flags = json["flags"]
self.passive = not path.isfile(self.file)
def __str__(self) -> str:
return self.name
def _init(self):
signal(SIGINT, lambda s, f: exit(0))
+39 -2
View File
@@ -1,5 +1,6 @@
from aiohttp import ClientSession
from aiohttp import ClientSession, web
from injector import inject_to_tab
from json.decoder import JSONDecodeError
import uuid
class Utilities:
@@ -11,9 +12,32 @@ class Utilities:
"confirm_plugin_install": self.confirm_plugin_install,
"execute_in_tab": self.execute_in_tab,
"inject_css_into_tab": self.inject_css_into_tab,
"remove_css_from_tab": self.remove_css_from_tab
"remove_css_from_tab": self.remove_css_from_tab,
"open_plugin_store": self.open_plugin_store
}
if context:
context.web_app.add_routes([
web.post("/methods/{method_name}", self._handle_server_method_call)
])
async def _handle_server_method_call(self, request):
method_name = request.match_info["method_name"]
try:
method_info = await request.json()
args = method_info["args"]
except JSONDecodeError:
args = {}
res = {}
try:
r = await self.util_methods[method_name](**args)
res["result"] = r
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return web.json_response(res)
async def confirm_plugin_install(self, request_id):
return await self.context.plugin_browser.confirm_plugin_install(request_id)
@@ -104,3 +128,16 @@ class Utilities:
"success": False,
"result": e
}
async def open_plugin_store(self):
await inject_to_tab("SP", """
(function() {
wpRequire = webpackJsonp.push([[], { get_require: (mod, _exports, wpRequire) => mod.exports = wpRequire }, [["get_require"]]]);
const all = () => Object.keys(wpRequire.c).map((x) => wpRequire.c[x].exports).filter((x) => x);
router = all().map(m => {
if (typeof m !== "object") return undefined;
for (let prop in m) { if (m[prop]?.Navigate) return m[prop]}
}).find(x => x)
router.NavigateToExternalWeb("http://127.0.0.1:1337/browser/redirect")
})();
""")
+3
View File
@@ -1 +1,4 @@
node_modules/
.yalc
yalc.lock
+1542 -15
View File
File diff suppressed because it is too large Load Diff
+13 -3
View File
@@ -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"
}
}
+22 -9
View File
@@ -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
});
+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);
}
+22
View File
@@ -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"]
}