diff options
| -rw-r--r-- | backend/browser.py | 28 | ||||
| -rw-r--r-- | backend/legacy/library.js | 78 | ||||
| -rw-r--r-- | backend/loader.py | 6 | ||||
| -rw-r--r-- | backend/main.py | 6 | ||||
| -rw-r--r-- | frontend/src/components/store/PluginCard.tsx | 73 | ||||
| -rw-r--r-- | frontend/src/components/store/Store.tsx | 41 |
6 files changed, 194 insertions, 38 deletions
diff --git a/backend/browser.py b/backend/browser.py index f2325aff..37099e19 100644 --- a/backend/browser.py +++ b/backend/browser.py @@ -12,21 +12,20 @@ from hashlib import sha256 from subprocess import Popen class PluginInstallContext: - def __init__(self, gh_url, version, hash) -> None: - self.gh_url = gh_url + def __init__(self, artifact, name, version, hash) -> None: + self.artifact = artifact + self.name = name self.version = version self.hash = hash class PluginBrowser: - def __init__(self, plugin_path, server_instance, store_url) -> None: + def __init__(self, plugin_path, server_instance) -> None: self.log = getLogger("browser") self.plugin_path = plugin_path - self.store_url = store_url self.install_requests = {} server_instance.add_routes([ - web.post("/browser/install_plugin", self.install_plugin), - web.get("/browser/redirect", self.redirect_to_store) + web.post("/browser/install_plugin", self.install_plugin) ]) def _unzip_to_plugin_dir(self, zip, name, hash): @@ -40,12 +39,12 @@ class PluginBrowser: Popen(["chmod", "-R", "555", self.plugin_path]) return True - async def _install(self, name, version, hash): + async def _install(self, artifact, name, version, hash): rmtree(path.join(self.plugin_path, name), ignore_errors=True) self.log.info(f"Installing {name} (Version: {version})") async with ClientSession() as client: - self.log.debug(f"Fetching {name}") - res = await client.get(name) + self.log.debug(f"Fetching {artifact}") + res = await client.get(artifact) if res.status == 200: self.log.debug("Got 200. Reading...") data = await res.read() @@ -67,24 +66,21 @@ class PluginBrowser: else: self.log.fatal(f"Could not fetch from URL. {await res.text()}") - async def redirect_to_store(self, request): - return web.Response(status=302, headers={"Location": self.store_url}) - async def install_plugin(self, request): data = await request.post() - get_event_loop().create_task(self.request_plugin_install(data.get("name", "No name"), data.get("version", "dev"), data.get("hash", False))) + get_event_loop().create_task(self.request_plugin_install(data.get("artifact", ""), data.get("name", "No name"), data.get("version", "dev"), data.get("hash", False))) return web.Response(text="Requested plugin install") - async def request_plugin_install(self, name, version, hash): + async def request_plugin_install(self, artifact, name, version, hash): request_id = str(time()) - self.install_requests[request_id] = PluginInstallContext(name, version, hash) + self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash) tab = await get_tab("SP") await tab.open_websocket() await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}')") async def confirm_plugin_install(self, request_id): request = self.install_requests.pop(request_id) - await self._install(request.gh_url, request.version, request.hash) + await self._install(request.artifact, request.name, request.version, request.hash) def cancel_plugin_install(self, request_id): self.install_requests.pop(request_id)
\ No newline at end of file diff --git a/backend/legacy/library.js b/backend/legacy/library.js new file mode 100644 index 00000000..f9dfe699 --- /dev/null +++ b/backend/legacy/library.js @@ -0,0 +1,78 @@ +class PluginEventTarget extends EventTarget { } +method_call_ev_target = new PluginEventTarget(); + +window.addEventListener("message", function(evt) { + let ev = new Event(evt.data.call_id); + ev.data = evt.data.result; + method_call_ev_target.dispatchEvent(ev); +}, false); + +async function call_server_method(method_name, arg_object={}) { + const response = await fetch(`http://127.0.0.1:1337/methods/${method_name}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(arg_object), + }); + + const dta = await response.json(); + if (!dta.success) throw dta.result; + return dta.result; +} + +// Source: https://stackoverflow.com/a/2117523 Thanks! +function uuidv4() { + return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); +} + +async function fetch_nocors(url, request={}) { + let args = { method: "POST", headers: {}, body: "" }; + request = {...args, ...request}; + request.url = url; + request.data = request.body; + delete request.body; //maintain api-compatibility with fetch + return await call_server_method("http_request", request); +} + +async function call_plugin_method(method_name, arg_object={}) { + if (plugin_name == undefined) + throw new Error("Plugin methods can only be called from inside plugins (duh)"); + const response = await fetch(`http://127.0.0.1:1337/plugins/${plugin_name}/methods/${method_name}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + args: arg_object, + }), + }); + + const dta = await response.json(); + if (!dta.success) throw dta.result; + return dta.result; +} + +async function execute_in_tab(tab, run_async, code) { + return await call_server_method("execute_in_tab", { + 'tab': tab, + 'run_async': run_async, + 'code': code + }); +} + +async function inject_css_into_tab(tab, style) { + return await call_server_method("inject_css_into_tab", { + 'tab': tab, + 'style': style + }); +} + +async function remove_css_from_tab(tab, css_id) { + return await call_server_method("remove_css_from_tab", { + 'tab': tab, + 'css_id': css_id + }); +}
\ No newline at end of file diff --git a/backend/loader.py b/backend/loader.py index c420fafe..3f5d1ab8 100644 --- a/backend/loader.py +++ b/backend/loader.py @@ -116,7 +116,7 @@ class Loader: self.logger.info(f"Plugin {plugin.name} is passive") self.plugins[plugin.name] = plugin.start() self.logger.info(f"Loaded {plugin.name}") - self.loop.create_task(self.dispatch_plugin(plugin.name)) + self.loop.create_task(self.dispatch_plugin(plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name)) except Exception as e: self.logger.error(f"Could not load {file}. {e}") print_exc() @@ -168,8 +168,8 @@ class Loader: 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> + <script src="/legacy/library.js"></script> + <script>window.plugin_name = '{plugin.name}' </script> <base href="http://127.0.0.1:1337/plugins/plugin_resource/{plugin.name}/"> {template_data} """ diff --git a/backend/main.py b/backend/main.py index f942ee29..afd3cc21 100644 --- a/backend/main.py +++ b/backend/main.py @@ -9,8 +9,7 @@ CONFIG = { "server_host": getenv("SERVER_HOST", "127.0.0.1"), "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://beta.deckbrew.xyz") + "log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING":30, "INFO": 20, "DEBUG": 10}[getenv("LOG_LEVEL", "INFO")] } basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s") @@ -44,7 +43,7 @@ class PluginManager: allow_headers="*") }) self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"]) - self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.web_app, CONFIG["store_url"]) + self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.web_app) self.utilities = Utilities(self) jinja_setup(self.web_app) @@ -57,6 +56,7 @@ class PluginManager: 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'))]) + self.web_app.add_routes([static("/legacy", path.join(path.dirname(__file__), 'legacy'))]) def exception_handler(self, loop, context): if context["message"] == "Unclosed connection": diff --git a/frontend/src/components/store/PluginCard.tsx b/frontend/src/components/store/PluginCard.tsx index 20ffead6..e99d95d0 100644 --- a/frontend/src/components/store/PluginCard.tsx +++ b/frontend/src/components/store/PluginCard.tsx @@ -10,16 +10,26 @@ import { } from 'decky-frontend-lib'; import { FC, useRef, useState } from 'react'; -import { StorePlugin, StorePluginVersion, requestPluginInstall } from './Store'; +import { + LegacyStorePlugin, + StorePlugin, + StorePluginVersion, + requestLegacyPluginInstall, + requestPluginInstall, +} from './Store'; interface PluginCardProps { - plugin: StorePlugin; + plugin: StorePlugin | LegacyStorePlugin; } const classNames = (...classes: string[]) => { return classes.join(' '); }; +function isLegacyPlugin(plugin: LegacyStorePlugin | StorePlugin): plugin is LegacyStorePlugin { + return 'artifact' in plugin; +} + const PluginCard: FC<PluginCardProps> = ({ plugin }) => { const [selectedOption, setSelectedOption] = useState<number>(0); const buttonRef = useRef<HTMLDivElement>(null); @@ -36,10 +46,10 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => { <Focusable // className="Panel Focusable" ref={containerRef} - onActivate={(e: CustomEvent) => { + onActivate={(_: CustomEvent) => { buttonRef.current!.focus(); }} - onCancel={(e: CustomEvent) => { + onCancel={(_: CustomEvent) => { if (containerRef.current!.querySelectorAll('* :focus').length === 0) { Router.NavigateBackOrOpenMenu(); setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000); @@ -64,7 +74,14 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => { className={classNames(staticClasses.Text)} // onClick={() => Router.NavigateToExternalWeb('https://github.com/' + plugin.artifact)} > - {plugin.name} + {isLegacyPlugin(plugin) ? ( + <div> + <span style={{ color: 'grey' }}>{plugin.artifact.split('/')[0]}/</span> + {plugin.artifact.split('/')[1]} + </div> + ) : ( + plugin.name + )} </a> </div> <div @@ -79,10 +96,17 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => { width: 'auto', height: '160px', }} - src={`https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/${plugin.name.replace( - '/', - '_', - )}.png`} + src={ + isLegacyPlugin(plugin) + ? `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/${plugin.artifact.replace( + '/', + '_', + )}.png` + : `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/${plugin.name.replace( + '/', + '_', + )}.png` + } /> <div style={{ @@ -107,6 +131,18 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => { {tag == 'root' ? 'Requires root' : tag} </span> ))} + {isLegacyPlugin(plugin) && ( + <span + style={{ + padding: '5px', + marginRight: '10px', + borderRadius: '5px', + background: '#ACB2C947', + }} + > + legacy + </span> + )} </p> </div> </div> @@ -132,7 +168,11 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => { > <DialogButton ref={buttonRef} - onClick={() => requestPluginInstall(plugin, plugin.versions[selectedOption])} + onClick={() => + isLegacyPlugin(plugin) + ? requestLegacyPluginInstall(plugin, Object.keys(plugin.versions)[selectedOption]) + : requestPluginInstall(plugin, plugin.versions[selectedOption]) + } > Install </DialogButton> @@ -144,10 +184,15 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => { > <Dropdown rgOptions={ - plugin.versions.map((version: StorePluginVersion, index) => ({ - data: index, - label: version.name, - })) as SingleDropdownOption[] + (isLegacyPlugin(plugin) + ? Object.keys(plugin.versions).map((v, k) => ({ + data: k, + label: v, + })) + : plugin.versions.map((version: StorePluginVersion, index) => ({ + data: index, + label: version.name, + }))) as SingleDropdownOption[] } strDefaultLabel={'Select a version'} selectedOption={selectedOption} diff --git a/frontend/src/components/store/Store.tsx b/frontend/src/components/store/Store.tsx index e317096f..f28434dd 100644 --- a/frontend/src/components/store/Store.tsx +++ b/frontend/src/components/store/Store.tsx @@ -17,6 +17,16 @@ export interface StorePlugin { tags: string[]; } +export interface LegacyStorePlugin { + artifact: string; + versions: { + [version: string]: string; + }; + author: string; + description: string; + tags: string[]; +} + export async function installFromURL(url: string) { const formData = new FormData(); const splitURL = url.split('/'); @@ -28,6 +38,18 @@ export async function installFromURL(url: string) { }); } +export async function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVer: string) { + const formData = new FormData(); + formData.append('name', plugin.artifact); + formData.append('artifact', `https://github.com/${plugin.artifact}/archive/refs/tags/${selectedVer}.zip`); + formData.append('version', selectedVer); + formData.append('hash', plugin.versions[selectedVer]); + await fetch('http://localhost:1337/browser/install_plugin', { + method: 'POST', + body: formData, + }); +} + export async function requestPluginInstall(plugin: StorePlugin, selectedVer: StorePluginVersion) { const formData = new FormData(); formData.append('name', plugin.name); @@ -42,6 +64,7 @@ export async function requestPluginInstall(plugin: StorePlugin, selectedVer: Sto const StorePage: FC<{}> = () => { const [data, setData] = useState<StorePlugin[] | null>(null); + const [legacyData, setLegacyData] = useState<LegacyStorePlugin[] | null>(null); useEffect(() => { (async () => { @@ -49,6 +72,11 @@ const StorePage: FC<{}> = () => { console.log(res); setData(res); })(); + (async () => { + const res = await fetch('https://plugins.deckbrew.xyz/get_plugins', { method: 'GET' }).then((r) => r.json()); + console.log(res); + setLegacyData(res); + })(); }, []); return ( @@ -67,12 +95,21 @@ const StorePage: FC<{}> = () => { height: '100%', }} > - {data === null ? ( + {!data ? ( <div style={{ height: '100%' }}> <SteamSpinner /> </div> ) : ( - data.map((plugin: StorePlugin) => <PluginCard plugin={plugin} />) + <div> + {data.map((plugin: StorePlugin) => ( + <PluginCard plugin={plugin} /> + ))} + {!legacyData ? ( + <SteamSpinner /> + ) : ( + legacyData.map((plugin: LegacyStorePlugin) => <PluginCard plugin={plugin} />) + )} + </div> )} </div> </div> |
