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
+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"]
+57 -36
View File
@@ -2,16 +2,15 @@ from asyncio import Queue
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 json.decoder import JSONDecodeError
from watchdog.events import FileSystemEventHandler
from watchdog.observers.polling import PollingObserver as Observer
from injector import inject_to_tab
from injector import inject_to_tab, get_tab
from plugin import PluginWrapper
@@ -63,7 +62,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()
@@ -73,17 +71,21 @@ 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.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_bundle(self, request):
async def handle_frontend_bundle(self, request):
plugin = self.plugins[request.match_info["plugin_name"]]
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.frontend_bundle), 'r') as bundle:
@@ -103,14 +105,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}")
@@ -125,38 +126,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)
+11 -4
View File
@@ -10,9 +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:
self.file = file
@@ -24,13 +21,23 @@ class PluginWrapper:
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r"))
if "frontend_bundle" in json:
self.frontend_bundle = json["frontend_bundle"]
self.legacy = False
else:
self.main_view_html = json["main_view_html"]
self.tile_view_html = json["tile_view_html"] if "tile_view_html" in json else ""
self.legacy = True
self.name = json["name"]
self.author = json["author"]
self.frontend_bundle = json["frontend_bundle"]
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")
})();
""")
+1546 -15
View File
File diff suppressed because it is too large Load Diff
+14 -4
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-typescript": "^8.3.2",
"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"
"@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",
"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.2",
"react-icons": "^4.3.1"
}
}
+17 -9
View File
@@ -1,16 +1,24 @@
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'),
}),
],
output: {
file: '../backend/static/plugin-loader.iife.js',
format: 'iife',
},
plugins: [commonjs(), resolve(), typescript()]
}
export default options
});
+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;
+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"]
}