mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 08:47:49 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 198591dbd7 | |||
| f21d34506d | |||
| ab6ec98160 | |||
| f1e809781a | |||
| 789058b72f | |||
| 4a68b1430d | |||
| 66c4a7e16e | |||
| b929b2dddf | |||
| fb0b703438 | |||
| afb2c7c0ed | |||
| 52dded85ed | |||
| 2004bdebbf | |||
| c9bf8d357e | |||
| 09eee761a5 | |||
| 20f43b2fd4 | |||
| e6dd1c29d8 | |||
| 6e88c7c9ac | |||
| f015e00561 | |||
| e07827cdb5 | |||
| 103d43e7c9 | |||
| 23b7df0ce2 | |||
| a5671e19ce | |||
| f2fbd399fe |
@@ -118,7 +118,7 @@ jobs:
|
||||
run: |
|
||||
export VERSION=${{ steps.old_tag.outputs.tag }}
|
||||
export COMMIT=$(git log -1 --pretty=format:%h)
|
||||
echo ::set-output name=tag_name::$(sed -r 's/-pre(.*)?$//' <<< $VERSION)-$COMMIT-pre
|
||||
echo ::set-output name=tag_name::$(sed -r 's/(-.*)?-pre$//' <<< $VERSION)-$COMMIT-pre
|
||||
|
||||
- name: Push tag 📤
|
||||
uses: rickstaa/action-create-tag@v1.3.2
|
||||
|
||||
@@ -11,14 +11,14 @@ Keep an eye on the [Wiki](https://deckbrew.xyz) for more information about Plugi
|
||||
4. Under Miscellaneous, enable `CEF Remote Debugging`
|
||||
5. Click on the `STEAM` button and select `Power` -> `Switch to Desktop`
|
||||
6. Make sure you have a password set with the "passwd" command in terminal to install it ([YouTube Guide](https://www.youtube.com/watch?v=1vOMYGj22rQ)).
|
||||
7. Open a terminal and paste the following command into it:
|
||||
- For users:
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/legacy/dist/install_release.sh | sh`
|
||||
- For the latest pre-release,
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_prerelease.sh | sh`
|
||||
7. Open a terminal and paste the following command into it:
|
||||
- For the latest pre-release:
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/install_prerelease.sh | sh`
|
||||
- For testers/plugin developers:
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_prerelease.sh | sh`
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/install_prerelease.sh | sh`
|
||||
- [Wiki Link](https://deckbrew.xyz/en/loader-dev/development)
|
||||
- For the legacy version (unsupported):
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/legacy/dist/install_release.sh | sh`
|
||||
7. Done! Reboot back into Gaming mode and enjoy your plugins!
|
||||
|
||||
### Install/Uninstall Plugins
|
||||
@@ -28,7 +28,7 @@ Keep an eye on the [Wiki](https://deckbrew.xyz) for more information about Plugi
|
||||
|
||||
### Uninstall
|
||||
- Open a terminal and paste the following command into it:
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/uninstall.sh | sh`
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/uninstall.sh | sh`
|
||||
|
||||
## Features
|
||||
- Clean injecting and loading of one or more plugins
|
||||
@@ -46,6 +46,22 @@ Keep an eye on the [Wiki](https://deckbrew.xyz) for more information about Plugi
|
||||
- [Here's how to get the Steam Deck UI on your enviroment of choice.](https://youtu.be/1IAbZte8e7E?t=112)
|
||||
- (The video shows Windows usage but unless you're using Arch WSL/cygwin this script is unsupported on Windows.)
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. Clone the repository using the latest commit to main before starting your PR.
|
||||
2. In your clone of the repository run these commands:
|
||||
1. ``pnpm i``
|
||||
2. ``pnpm run build``
|
||||
3. If you are modifying the UI, these will need to be run before deploying the changes to your Deck.
|
||||
4. Use the vscode tasks or ``deck.sh`` script to deploy your changes to your Deck to test them.
|
||||
5. You will be testing your changes with the python script version, so you will need to build, deploy and reload each time.
|
||||
|
||||
Note: If you are recieveing build errors due to an out of date library, you should run this command inside of your repository:
|
||||
|
||||
```bash
|
||||
pnpm update decky-frontend-lib --latest
|
||||
```
|
||||
|
||||
Source control and deploying plugins are left to each respective contributor for the cloned repos in order to keep depedencies up to date.
|
||||
|
||||
## Credit
|
||||
|
||||
+17
-7
@@ -10,6 +10,7 @@ from asyncio import get_event_loop
|
||||
from time import time
|
||||
from hashlib import sha256
|
||||
from subprocess import Popen
|
||||
from injector import inject_to_tab
|
||||
|
||||
import json
|
||||
|
||||
@@ -23,9 +24,10 @@ class PluginInstallContext:
|
||||
self.hash = hash
|
||||
|
||||
class PluginBrowser:
|
||||
def __init__(self, plugin_path, server_instance) -> None:
|
||||
def __init__(self, plugin_path, server_instance, plugins) -> None:
|
||||
self.log = getLogger("browser")
|
||||
self.plugin_path = plugin_path
|
||||
self.plugins = plugins
|
||||
self.install_requests = {}
|
||||
|
||||
server_instance.add_routes([
|
||||
@@ -45,21 +47,28 @@ class PluginBrowser:
|
||||
|
||||
def find_plugin_folder(self, name):
|
||||
for folder in listdir(self.plugin_path):
|
||||
with open(path.join(self.plugin_path, folder, 'plugin.json'), 'r') as f:
|
||||
plugin = json.load(f)
|
||||
try:
|
||||
with open(path.join(self.plugin_path, folder, 'plugin.json'), 'r') as f:
|
||||
plugin = json.load(f)
|
||||
|
||||
if plugin['name'] == name:
|
||||
return path.join(self.plugin_path, folder)
|
||||
if plugin['name'] == name:
|
||||
return path.join(self.plugin_path, folder)
|
||||
except:
|
||||
self.log.debug(f"skipping {folder}")
|
||||
|
||||
async def uninstall_plugin(self, name):
|
||||
tab = await get_tab("SP")
|
||||
await tab.open_websocket()
|
||||
|
||||
try:
|
||||
if type(name) != str:
|
||||
data = await name.post()
|
||||
name = data.get("name")
|
||||
name = data.get("name", "undefined")
|
||||
self.log.info("uninstalling " + name)
|
||||
self.log.info(" at dir " + self.find_plugin_folder(name))
|
||||
await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
|
||||
if self.plugins[name]:
|
||||
self.plugins[name].stop()
|
||||
self.plugins.pop(name, None)
|
||||
rmtree(self.find_plugin_folder(name))
|
||||
except FileNotFoundError:
|
||||
self.log.warning(f"Plugin {name} not installed, skipping uninstallation")
|
||||
@@ -91,6 +100,7 @@ class PluginBrowser:
|
||||
)
|
||||
if ret:
|
||||
self.log.info(f"Installed {name} (Version: {version})")
|
||||
await inject_to_tab("SP", "window.syncDeckyPlugins()")
|
||||
else:
|
||||
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
||||
else:
|
||||
|
||||
+14
-1
@@ -1,7 +1,20 @@
|
||||
from aiohttp.web import middleware, Response
|
||||
import ssl
|
||||
import certifi
|
||||
import uuid
|
||||
|
||||
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
||||
|
||||
csrf_token = str(uuid.uuid4())
|
||||
|
||||
def get_ssl_context():
|
||||
return ssl_ctx
|
||||
return ssl_ctx
|
||||
|
||||
def get_csrf_token():
|
||||
return csrf_token
|
||||
|
||||
@middleware
|
||||
async def csrf_middleware(request, handler):
|
||||
if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/legacy/") or str(request.rel_url).startswith("/steam_resource/"):
|
||||
return await handler(request)
|
||||
return Response(text='Forbidden', status='403')
|
||||
+2
-2
@@ -33,7 +33,7 @@ class Tab:
|
||||
return (await self.websocket.receive_json()) if receive else None
|
||||
raise RuntimeError("Websocket not opened")
|
||||
|
||||
async def evaluate_js(self, js, run_async=False, manage_socket=True):
|
||||
async def evaluate_js(self, js, run_async=False, manage_socket=True, get_result=True):
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
@@ -45,7 +45,7 @@ class Tab:
|
||||
"userGesture": True,
|
||||
"awaitPromise": run_async
|
||||
}
|
||||
})
|
||||
}, get_result)
|
||||
|
||||
if manage_socket:
|
||||
await self.client.close()
|
||||
|
||||
@@ -8,10 +8,13 @@ window.addEventListener("message", function(evt) {
|
||||
}, false);
|
||||
|
||||
async function call_server_method(method_name, arg_object={}) {
|
||||
const token = await fetch("http://127.0.0.1:1337/auth/token").then(r => r.text());
|
||||
const response = await fetch(`http://127.0.0.1:1337/methods/${method_name}`, {
|
||||
method: 'POST',
|
||||
credentials: "include",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authentication: token
|
||||
},
|
||||
body: JSON.stringify(arg_object),
|
||||
});
|
||||
@@ -40,10 +43,13 @@ async function fetch_nocors(url, 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 token = await fetch("http://127.0.0.1:1337/auth/token").then(r => r.text());
|
||||
const response = await fetch(`http://127.0.0.1:1337/plugins/${plugin_name}/methods/${method_name}`, {
|
||||
method: 'POST',
|
||||
credentials: "include",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authentication: token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
args: arg_object,
|
||||
|
||||
+12
-5
@@ -20,12 +20,13 @@ from os import path
|
||||
from subprocess import call
|
||||
|
||||
import aiohttp_cors
|
||||
from aiohttp.web import Application, run_app, static
|
||||
from aiohttp.web import Application, run_app, static, get, Response
|
||||
from aiohttp_jinja2 import setup as jinja_setup
|
||||
|
||||
from browser import PluginBrowser
|
||||
from injector import inject_to_tab, tab_has_global_var
|
||||
from loader import Loader
|
||||
from helpers import csrf_middleware, get_csrf_token
|
||||
from utilities import Utilities
|
||||
from updater import Updater
|
||||
|
||||
@@ -41,12 +42,13 @@ class PluginManager:
|
||||
def __init__(self) -> None:
|
||||
self.loop = get_event_loop()
|
||||
self.web_app = Application()
|
||||
self.web_app.middlewares.append(csrf_middleware)
|
||||
self.cors = aiohttp_cors.setup(self.web_app, defaults={
|
||||
"https://steamloopback.host": aiohttp_cors.ResourceOptions(expose_headers="*",
|
||||
allow_headers="*")
|
||||
allow_headers="*", allow_credentials=True)
|
||||
})
|
||||
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)
|
||||
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.web_app, self.plugin_loader.plugins)
|
||||
self.utilities = Utilities(self)
|
||||
self.updater = Updater(self)
|
||||
|
||||
@@ -57,6 +59,8 @@ class PluginManager:
|
||||
self.loop.create_task(self.loader_reinjector())
|
||||
self.loop.create_task(self.load_plugins())
|
||||
self.loop.set_exception_handler(self.exception_handler)
|
||||
self.web_app.add_routes([get("/auth/token", self.get_auth_token)])
|
||||
|
||||
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'))])
|
||||
@@ -67,6 +71,9 @@ class PluginManager:
|
||||
return
|
||||
loop.default_exception_handler(context)
|
||||
|
||||
async def get_auth_token(self, request):
|
||||
return Response(text=get_csrf_token())
|
||||
|
||||
async def wait_for_server(self):
|
||||
async with ClientSession() as web:
|
||||
while True:
|
||||
@@ -83,8 +90,8 @@ class PluginManager:
|
||||
|
||||
async def loader_reinjector(self):
|
||||
while True:
|
||||
await sleep(1)
|
||||
if not await tab_has_global_var("SP", "DeckyPluginLoader"):
|
||||
await sleep(5)
|
||||
if not await tab_has_global_var("SP", "deckyHasLoaded"):
|
||||
logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
|
||||
await self.inject_javascript()
|
||||
|
||||
|
||||
+19
-12
@@ -8,7 +8,7 @@ from aiohttp import ClientSession, web
|
||||
|
||||
from injector import inject_to_tab, get_tab
|
||||
|
||||
from os import getcwd, path
|
||||
from os import getcwd, path, remove
|
||||
|
||||
from subprocess import call
|
||||
|
||||
@@ -22,7 +22,8 @@ class Updater:
|
||||
self.updater_methods = {
|
||||
"get_version": self.get_version,
|
||||
"do_update": self.do_update,
|
||||
"do_restart": self.do_restart
|
||||
"do_restart": self.do_restart,
|
||||
"check_for_updates": self.check_for_updates
|
||||
}
|
||||
self.remoteVer = None
|
||||
try:
|
||||
@@ -58,19 +59,24 @@ class Updater:
|
||||
return {
|
||||
"current": self.localVer,
|
||||
"remote": self.remoteVer,
|
||||
"updatable": self.remoteVer != None
|
||||
"updatable": self.localVer != None
|
||||
}
|
||||
else:
|
||||
return {"current": "unknown", "updatable": False}
|
||||
|
||||
async def check_for_updates(self):
|
||||
async with ClientSession() as web:
|
||||
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res:
|
||||
remoteVersions = await res.json()
|
||||
self.remoteVer = next(filter(lambda ver: ver["prerelease"] and ver["tag_name"].startswith("v") and ver["tag_name"].endswith("-pre"), remoteVersions), None)
|
||||
logger.info("Updated remote version information")
|
||||
return await self.get_version()
|
||||
|
||||
async def version_reloader(self):
|
||||
await sleep(30)
|
||||
while True:
|
||||
try:
|
||||
async with ClientSession() as web:
|
||||
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res:
|
||||
remoteVersions = await res.json()
|
||||
self.remoteVer = next(filter(lambda ver: ver["prerelease"] and ver["tag_name"].startswith("v") and ver["tag_name"].endswith("-pre"), remoteVersions), None)
|
||||
logger.info("Updated remote version information")
|
||||
await self.check_for_updates()
|
||||
except:
|
||||
pass
|
||||
await sleep(60 * 60) # 1 hour
|
||||
@@ -85,7 +91,10 @@ class Updater:
|
||||
async with ClientSession() as web:
|
||||
async with web.request("GET", download_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
|
||||
total = int(res.headers.get('content-length', 0))
|
||||
|
||||
try:
|
||||
remove(path.join(getcwd(), "PluginLoader"))
|
||||
except:
|
||||
pass
|
||||
with open(path.join(getcwd(), "PluginLoader"), "wb") as out:
|
||||
progress = 0
|
||||
raw = 0
|
||||
@@ -94,8 +103,7 @@ class Updater:
|
||||
raw += len(c)
|
||||
new_progress = round((raw / total) * 100)
|
||||
if progress != new_progress:
|
||||
if new_progress - progress>= 2:
|
||||
self.context.loop.create_task(tab.evaluate_js(f"window.DeckyUpdater.updateProgress({progress})", False, False))
|
||||
self.context.loop.create_task(tab.evaluate_js(f"window.DeckyUpdater.updateProgress({new_progress})", False, False, False))
|
||||
progress = new_progress
|
||||
|
||||
with open(path.join(getcwd(), ".loader.version"), "w") as out:
|
||||
@@ -110,4 +118,3 @@ class Updater:
|
||||
async def do_restart(self):
|
||||
call(["systemctl", "daemon-reload"])
|
||||
call(["systemctl", "restart", "plugin_loader"])
|
||||
exit(0)
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"decky-frontend-lib": "^1.2.1",
|
||||
"decky-frontend-lib": "^1.2.4",
|
||||
"react-icons": "^4.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -9,7 +9,7 @@ specifiers:
|
||||
'@types/react': 16.14.0
|
||||
'@types/react-router': 5.1.18
|
||||
'@types/webpack': ^5.28.0
|
||||
decky-frontend-lib: ^1.2.1
|
||||
decky-frontend-lib: ^1.2.4
|
||||
husky: ^8.0.1
|
||||
import-sort-style-module: ^6.0.0
|
||||
inquirer: ^8.2.4
|
||||
@@ -23,7 +23,7 @@ specifiers:
|
||||
typescript: ^4.7.4
|
||||
|
||||
dependencies:
|
||||
decky-frontend-lib: 1.2.1
|
||||
decky-frontend-lib: 1.2.4
|
||||
react-icons: 4.4.0_react@16.14.0
|
||||
|
||||
devDependencies:
|
||||
@@ -806,8 +806,8 @@ packages:
|
||||
ms: 2.1.2
|
||||
dev: true
|
||||
|
||||
/decky-frontend-lib/1.2.1:
|
||||
resolution: {integrity: sha512-aJmjOSMwQN9LTquYaMhSqW+FhmKLRgLb75JkcGRWKuIe8rjDfwwbAB/ckJseIC8UMzPKhspvcznfxyp+c72B5Q==}
|
||||
/decky-frontend-lib/1.2.4:
|
||||
resolution: {integrity: sha512-r3mLEey9KUkF68geJVSjNlOz/Fg4vpMKUzoutSceyd8o/J5l+QR+Vf0b3gwK3UN9Sp4Pj4XQ1eB82+/W0ApsFg==}
|
||||
dev: false
|
||||
|
||||
/deepmerge/4.2.2:
|
||||
|
||||
@@ -6,17 +6,21 @@ export interface RouterEntry {
|
||||
component: ComponentType;
|
||||
}
|
||||
|
||||
export type RoutePatch = (route: RouteProps) => RouteProps;
|
||||
|
||||
interface PublicDeckyRouterState {
|
||||
routes: Map<string, RouterEntry>;
|
||||
routePatches: Map<string, Set<RoutePatch>>;
|
||||
}
|
||||
|
||||
export class DeckyRouterState {
|
||||
private _routes = new Map<string, RouterEntry>();
|
||||
private _routePatches = new Map<string, Set<RoutePatch>>();
|
||||
|
||||
public eventBus = new EventTarget();
|
||||
|
||||
publicState(): PublicDeckyRouterState {
|
||||
return { routes: this._routes };
|
||||
return { routes: this._routes, routePatches: this._routePatches };
|
||||
}
|
||||
|
||||
addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props'] = {}) {
|
||||
@@ -24,6 +28,26 @@ export class DeckyRouterState {
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
addPatch(path: string, patch: RoutePatch) {
|
||||
let patchList = this._routePatches.get(path);
|
||||
if (!patchList) {
|
||||
patchList = new Set();
|
||||
this._routePatches.set(path, patchList);
|
||||
}
|
||||
patchList.add(patch);
|
||||
this.notifyUpdate();
|
||||
return patch;
|
||||
}
|
||||
|
||||
removePatch(path: string, patch: RoutePatch) {
|
||||
const patchList = this._routePatches.get(path);
|
||||
patchList?.delete(patch);
|
||||
if (patchList?.size == 0) {
|
||||
this._routePatches.delete(path);
|
||||
}
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
removeRoute(path: string) {
|
||||
this._routes.delete(path);
|
||||
this.notifyUpdate();
|
||||
@@ -36,6 +60,8 @@ export class DeckyRouterState {
|
||||
|
||||
interface DeckyRouterStateContext extends PublicDeckyRouterState {
|
||||
addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props']): void;
|
||||
addPatch(path: string, patch: RoutePatch): RoutePatch;
|
||||
removePatch(path: string, patch: RoutePatch): void;
|
||||
removeRoute(path: string): void;
|
||||
}
|
||||
|
||||
@@ -54,6 +80,7 @@ export const DeckyRouterStateContextProvider: FC<Props> = ({ children, deckyRout
|
||||
|
||||
useEffect(() => {
|
||||
function onUpdate() {
|
||||
console.log('test', deckyRouterState.publicState());
|
||||
setPublicDeckyRouterState({ ...deckyRouterState.publicState() });
|
||||
}
|
||||
|
||||
@@ -62,12 +89,15 @@ export const DeckyRouterStateContextProvider: FC<Props> = ({ children, deckyRout
|
||||
return () => deckyRouterState.eventBus.removeEventListener('update', onUpdate);
|
||||
}, []);
|
||||
|
||||
const addRoute = (path: string, component: RouterEntry['component'], props: RouterEntry['props'] = {}) =>
|
||||
deckyRouterState.addRoute(path, component, props);
|
||||
const removeRoute = (path: string) => deckyRouterState.removeRoute(path);
|
||||
const addRoute = deckyRouterState.addRoute.bind(deckyRouterState);
|
||||
const addPatch = deckyRouterState.addPatch.bind(deckyRouterState);
|
||||
const removePatch = deckyRouterState.removePatch.bind(deckyRouterState);
|
||||
const removeRoute = deckyRouterState.removeRoute.bind(deckyRouterState);
|
||||
|
||||
return (
|
||||
<DeckyRouterStateContext.Provider value={{ ...publicDeckyRouterState, addRoute, removeRoute }}>
|
||||
<DeckyRouterStateContext.Provider
|
||||
value={{ ...publicDeckyRouterState, addRoute, addPatch, removePatch, removeRoute }}
|
||||
>
|
||||
{children}
|
||||
</DeckyRouterStateContext.Provider>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { ModalRoot, QuickAccessTab, Router, Spinner, sleep, staticClasses } from 'decky-frontend-lib';
|
||||
import { FC, useState } from 'react';
|
||||
|
||||
interface PluginInstallModalProps {
|
||||
artifact: string;
|
||||
version: string;
|
||||
hash: string;
|
||||
// reinstall: boolean;
|
||||
onOK(): void;
|
||||
onCancel(): void;
|
||||
closeModal?(): void;
|
||||
}
|
||||
|
||||
const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, hash, onOK, onCancel, closeModal }) => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
return (
|
||||
<ModalRoot
|
||||
bOKDisabled={loading}
|
||||
closeModal={closeModal}
|
||||
onOK={async () => {
|
||||
setLoading(true);
|
||||
await onOK();
|
||||
Router.NavigateBackOrOpenMenu();
|
||||
await sleep(250);
|
||||
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
|
||||
}}
|
||||
onCancel={async () => {
|
||||
await onCancel();
|
||||
}}
|
||||
>
|
||||
<div className={staticClasses.Title} style={{ flexDirection: 'column' }}>
|
||||
{hash == 'False' ? <h3 style={{ color: 'red' }}>!!!!NO HASH PROVIDED!!!!</h3> : null}
|
||||
<div style={{ flexDirection: 'row' }}>
|
||||
{loading && <Spinner style={{ width: '20px' }} />} {loading ? 'Installing' : 'Install'} {artifact}
|
||||
{version ? ' version ' + version : null}
|
||||
{!loading && '?'}
|
||||
</div>
|
||||
</div>
|
||||
</ModalRoot>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginInstallModal;
|
||||
@@ -24,6 +24,7 @@ export default function UpdaterSettings() {
|
||||
const [versionInfo, setVersionInfo] = useState<VerInfo | null>(null);
|
||||
const [updateProgress, setUpdateProgress] = useState<number>(-1);
|
||||
const [reloading, setReloading] = useState<boolean>(false);
|
||||
const [checkingForUpdates, setCheckingForUpdates] = useState<boolean>(false);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = (await callUpdaterMethod('get_version')) as { result: VerInfo };
|
||||
@@ -51,32 +52,42 @@ export default function UpdaterSettings() {
|
||||
>
|
||||
{updateProgress == -1 ? (
|
||||
<DialogButton
|
||||
disabled={
|
||||
!versionInfo?.updatable || !versionInfo?.remote || versionInfo.remote.tag_name == versionInfo.current
|
||||
disabled={!versionInfo?.updatable || checkingForUpdates}
|
||||
onClick={
|
||||
!versionInfo?.remote || versionInfo?.remote?.tag_name == versionInfo?.current
|
||||
? async () => {
|
||||
setCheckingForUpdates(true);
|
||||
const res = (await callUpdaterMethod('check_for_updates')) as { result: VerInfo };
|
||||
setVersionInfo(res.result);
|
||||
setCheckingForUpdates(false);
|
||||
}
|
||||
: async () => {
|
||||
window.DeckyUpdater = {
|
||||
updateProgress: (i) => {
|
||||
setUpdateProgress(i);
|
||||
},
|
||||
finish: async () => {
|
||||
setUpdateProgress(0);
|
||||
setReloading(true);
|
||||
await finishUpdate();
|
||||
},
|
||||
};
|
||||
setUpdateProgress(0);
|
||||
callUpdaterMethod('do_update');
|
||||
}
|
||||
}
|
||||
onClick={async () => {
|
||||
window.DeckyUpdater = {
|
||||
updateProgress: (i) => {
|
||||
setUpdateProgress(i);
|
||||
},
|
||||
finish: async () => {
|
||||
setUpdateProgress(0);
|
||||
setReloading(true);
|
||||
await finishUpdate();
|
||||
},
|
||||
};
|
||||
setUpdateProgress(0);
|
||||
callUpdaterMethod('do_update');
|
||||
}}
|
||||
>
|
||||
Update
|
||||
{checkingForUpdates
|
||||
? 'Checking'
|
||||
: !versionInfo?.remote || versionInfo?.remote?.tag_name == versionInfo?.current
|
||||
? 'Check For Updates'
|
||||
: 'Install Update'}
|
||||
</DialogButton>
|
||||
) : (
|
||||
<ProgressBarWithInfo
|
||||
layout="inline"
|
||||
bottomSeparator={false}
|
||||
nProgress={updateProgress}
|
||||
nTransitionSec={0.01}
|
||||
indeterminate={reloading}
|
||||
sOperationText={reloading ? 'Reloading' : 'Updating'}
|
||||
/>
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function PluginList() {
|
||||
<div className={staticClasses.Title} style={{ marginLeft: 'auto', boxShadow: 'none' }}>
|
||||
<DialogButton
|
||||
style={{ height: '40px', width: '40px', padding: '10px 12px' }}
|
||||
onClick={() => window.DeckyPluginLoader.uninstall_plugin(name)}
|
||||
onClick={() => window.DeckyPluginLoader.uninstallPlugin(name)}
|
||||
>
|
||||
<FaTrash />
|
||||
</DialogButton>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SteamSpinner } from 'decky-frontend-lib';
|
||||
import { ModalRoot, SteamSpinner, showModal, staticClasses } from 'decky-frontend-lib';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
|
||||
import PluginCard from './PluginCard';
|
||||
@@ -35,19 +35,43 @@ export async function installFromURL(url: string) {
|
||||
await fetch('http://localhost:1337/browser/install_plugin', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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 function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVer: string) {
|
||||
showModal(
|
||||
<ModalRoot
|
||||
onOK={() => {
|
||||
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]);
|
||||
fetch('http://localhost:1337/browser/install_plugin', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onCancel={() => {
|
||||
// do nothing
|
||||
}}
|
||||
>
|
||||
<div className={staticClasses.Title} style={{ flexDirection: 'column', boxShadow: 'unset' }}>
|
||||
Using legacy plugins
|
||||
</div>
|
||||
You are currently installing a <b>legacy</b> plugin. Legacy plugins are no longer supported and may have issues.
|
||||
Legacy plugins do not support gamepad input. To interact with a legacy plugin, you will need to use the
|
||||
touchscreen.
|
||||
</ModalRoot>,
|
||||
);
|
||||
}
|
||||
|
||||
export async function requestPluginInstall(plugin: StorePlugin, selectedVer: StorePluginVersion) {
|
||||
@@ -59,6 +83,10 @@ export async function requestPluginInstall(plugin: StorePlugin, selectedVer: Sto
|
||||
await fetch('http://localhost:1337/browser/install_plugin', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,12 +96,16 @@ const StorePage: FC<{}> = () => {
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await fetch('https://beta.deckbrew.xyz/plugins', { method: 'GET' }).then((r) => r.json());
|
||||
const res = await fetch('https://beta.deckbrew.xyz/plugins', {
|
||||
method: 'GET',
|
||||
}).then((r) => r.json());
|
||||
console.log(res);
|
||||
setData(res.filter((x: StorePlugin) => x.name !== 'Example Plugin'));
|
||||
})();
|
||||
(async () => {
|
||||
const res = await fetch('https://plugins.deckbrew.xyz/get_plugins', { method: 'GET' }).then((r) => r.json());
|
||||
const res = await fetch('https://plugins.deckbrew.xyz/get_plugins', {
|
||||
method: 'GET',
|
||||
}).then((r) => r.json());
|
||||
console.log(res);
|
||||
setLegacyData(res);
|
||||
})();
|
||||
|
||||
+25
-13
@@ -7,22 +7,34 @@ declare global {
|
||||
DeckyUpdater?: DeckyUpdater;
|
||||
importDeckyPlugin: Function;
|
||||
syncDeckyPlugins: Function;
|
||||
deckyHasLoaded: boolean;
|
||||
deckyAuthToken: string;
|
||||
}
|
||||
}
|
||||
(async () => {
|
||||
window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text());
|
||||
|
||||
window.DeckyPluginLoader?.dismountAll();
|
||||
window.DeckyPluginLoader?.deinit();
|
||||
window.DeckyPluginLoader?.dismountAll();
|
||||
window.DeckyPluginLoader?.deinit();
|
||||
|
||||
window.DeckyPluginLoader = new PluginLoader();
|
||||
window.importDeckyPlugin = function (name: string) {
|
||||
window.DeckyPluginLoader?.importPlugin(name);
|
||||
};
|
||||
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);
|
||||
}
|
||||
};
|
||||
window.syncDeckyPlugins = async function () {
|
||||
const plugins = await (
|
||||
await fetch('http://127.0.0.1:1337/plugins', {
|
||||
credentials: 'include',
|
||||
headers: { Authentication: window.deckyAuthToken },
|
||||
})
|
||||
).json();
|
||||
for (const plugin of plugins) {
|
||||
if (!window.DeckyPluginLoader.hasPlugin(plugin)) window.DeckyPluginLoader?.importPlugin(plugin);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => window.syncDeckyPlugins(), 5000);
|
||||
setTimeout(() => window.syncDeckyPlugins(), 5000);
|
||||
|
||||
window.deckyHasLoaded = true;
|
||||
})();
|
||||
|
||||
+11
-1
@@ -8,6 +8,16 @@ export const log = (name: string, ...args: any[]) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const debug = (name: string, ...args: any[]) => {
|
||||
console.debug(
|
||||
`%c Decky %c ${name} %c`,
|
||||
'background: #16a085; color: black;',
|
||||
'background: #1abc9c; color: black;',
|
||||
'color: blue;',
|
||||
...args,
|
||||
);
|
||||
};
|
||||
|
||||
export const error = (name: string, ...args: any[]) => {
|
||||
console.log(
|
||||
`%c Decky %c ${name} %c`,
|
||||
@@ -28,7 +38,7 @@ class Logger {
|
||||
}
|
||||
|
||||
debug(...args: any[]) {
|
||||
log(this.name, ...args);
|
||||
debug(this.name, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ModalRoot, QuickAccessTab, Router, showModal, sleep, staticClasses } from 'decky-frontend-lib';
|
||||
import { ModalRoot, QuickAccessTab, showModal, staticClasses } from 'decky-frontend-lib';
|
||||
import { FaPlug } from 'react-icons/fa';
|
||||
|
||||
import { DeckyState, DeckyStateContextProvider } from './components/DeckyState';
|
||||
import LegacyPlugin from './components/LegacyPlugin';
|
||||
import PluginInstallModal from './components/modals/PluginInstallModal';
|
||||
import PluginView from './components/PluginView';
|
||||
import SettingsPage from './components/settings';
|
||||
import StorePage from './components/store/Store';
|
||||
@@ -55,27 +56,17 @@ class PluginLoader extends Logger {
|
||||
|
||||
public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) {
|
||||
showModal(
|
||||
<ModalRoot
|
||||
onOK={async () => {
|
||||
await this.callServerMethod('confirm_plugin_install', { request_id });
|
||||
Router.NavigateBackOrOpenMenu();
|
||||
await sleep(250);
|
||||
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
|
||||
}}
|
||||
onCancel={() => {
|
||||
this.callServerMethod('cancel_plugin_install', { request_id });
|
||||
}}
|
||||
>
|
||||
<div className={staticClasses.Title} style={{ flexDirection: 'column' }}>
|
||||
{hash == 'False' ? <h3 style={{ color: 'red' }}>!!!!NO HASH PROVIDED!!!!</h3> : null}
|
||||
Install {artifact}
|
||||
{version ? ' version ' + version : null}?
|
||||
</div>
|
||||
</ModalRoot>,
|
||||
<PluginInstallModal
|
||||
artifact={artifact}
|
||||
version={version}
|
||||
hash={hash}
|
||||
onOK={() => this.callServerMethod('confirm_plugin_install', { request_id })}
|
||||
onCancel={() => this.callServerMethod('cancel_plugin_install', { request_id })}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
public uninstall_plugin(name: string) {
|
||||
public uninstallPlugin(name: string) {
|
||||
showModal(
|
||||
<ModalRoot
|
||||
onOK={async () => {
|
||||
@@ -84,6 +75,10 @@ class PluginLoader extends Logger {
|
||||
await fetch('http://localhost:1337/browser/uninstall_plugin', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onCancel={() => {
|
||||
@@ -97,6 +92,10 @@ class PluginLoader extends Logger {
|
||||
);
|
||||
}
|
||||
|
||||
public hasPlugin(name: string) {
|
||||
return Boolean(this.plugins.find((plugin) => plugin.name == name));
|
||||
}
|
||||
|
||||
public dismountAll() {
|
||||
for (const plugin of this.plugins) {
|
||||
this.log(`Dismounting ${plugin.name}`);
|
||||
@@ -149,7 +148,12 @@ class PluginLoader extends Logger {
|
||||
}
|
||||
|
||||
private async importReactPlugin(name: string) {
|
||||
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`);
|
||||
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
});
|
||||
if (res.ok) {
|
||||
let plugin = await eval(await res.text())(this.createPluginAPI(name));
|
||||
this.plugins.push({
|
||||
@@ -171,8 +175,10 @@ class PluginLoader extends Logger {
|
||||
async callServerMethod(methodName: string, args = {}) {
|
||||
const response = await fetch(`http://127.0.0.1:1337/methods/${methodName}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
body: JSON.stringify(args),
|
||||
});
|
||||
@@ -187,8 +193,10 @@ class PluginLoader extends Logger {
|
||||
async callPluginMethod(methodName: string, args = {}) {
|
||||
const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
args,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { afterPatch, findModuleChild, unpatch } from 'decky-frontend-lib';
|
||||
import { ReactElement, createElement, memo } from 'react';
|
||||
import type { Route } from 'react-router';
|
||||
import type { Route, RouteProps } from 'react-router';
|
||||
|
||||
import {
|
||||
DeckyRouterState,
|
||||
DeckyRouterStateContextProvider,
|
||||
RoutePatch,
|
||||
RouterEntry,
|
||||
useDeckyRouterState,
|
||||
} from './components/DeckyRouterState';
|
||||
@@ -38,14 +39,16 @@ class RouterHook extends Logger {
|
||||
});
|
||||
|
||||
let Route: new () => Route;
|
||||
// Used to store the new replicated routes we create to allow routes to be unpatched.
|
||||
let toReplace = new Map<string, Route>();
|
||||
const DeckyWrapper = ({ children }: { children: ReactElement }) => {
|
||||
const { routes } = useDeckyRouterState();
|
||||
const { routes, routePatches } = useDeckyRouterState();
|
||||
|
||||
const routerIndex = children.props.children[0].props.children.length - 1;
|
||||
if (
|
||||
!children.props.children[0].props.children[routerIndex].length ||
|
||||
children.props.children[0].props.children !== routes.size
|
||||
) {
|
||||
const routeList = children.props.children[0].props.children;
|
||||
|
||||
let routerIndex = routeList.length;
|
||||
if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) {
|
||||
if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--;
|
||||
const newRouterArray: ReactElement[] = [];
|
||||
routes.forEach(({ component, props }, path) => {
|
||||
newRouterArray.push(
|
||||
@@ -54,8 +57,26 @@ class RouterHook extends Logger {
|
||||
</Route>,
|
||||
);
|
||||
});
|
||||
children.props.children[0].props.children[routerIndex] = newRouterArray;
|
||||
routeList[routerIndex] = newRouterArray;
|
||||
}
|
||||
routeList.forEach((route: Route, index: number) => {
|
||||
const replaced = toReplace.get(route?.props?.path as string);
|
||||
if (replaced) {
|
||||
routeList[index] = replaced;
|
||||
toReplace.delete(route?.props?.path as string);
|
||||
}
|
||||
if (route?.props?.path && routePatches.has(route.props.path as string)) {
|
||||
toReplace.set(
|
||||
route?.props?.path as string,
|
||||
// @ts-ignore
|
||||
createElement(routeList[index].type, routeList[index].props, routeList[index].props.children),
|
||||
);
|
||||
routePatches.get(route.props.path as string)?.forEach((patch) => {
|
||||
routeList[index].props = patch(routeList[index].props);
|
||||
});
|
||||
}
|
||||
});
|
||||
this.debug('Rerendered routes list');
|
||||
return children;
|
||||
};
|
||||
|
||||
@@ -92,6 +113,14 @@ class RouterHook extends Logger {
|
||||
this.routerState.addRoute(path, component, props);
|
||||
}
|
||||
|
||||
addPatch(path: string, patch: RoutePatch) {
|
||||
return this.routerState.addPatch(path, patch);
|
||||
}
|
||||
|
||||
removePatch(path: string, patch: RoutePatch) {
|
||||
this.routerState.removePatch(path, patch);
|
||||
}
|
||||
|
||||
removeRoute(path: string) {
|
||||
this.routerState.removeRoute(path);
|
||||
}
|
||||
|
||||
@@ -110,12 +110,12 @@ class TabsHook extends Logger {
|
||||
}
|
||||
|
||||
add(tab: Tab) {
|
||||
this.log('Adding tab', tab.id, 'to render array');
|
||||
this.debug('Adding tab', tab.id, 'to render array');
|
||||
this.tabs.push(tab);
|
||||
}
|
||||
|
||||
removeById(id: number) {
|
||||
this.log('Removing tab', id);
|
||||
this.debug('Removing tab', id);
|
||||
this.tabs = this.tabs.filter((tab) => tab.id !== id);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@ export interface DeckyUpdater {
|
||||
export async function callUpdaterMethod(methodName: string, args = {}) {
|
||||
const response = await fetch(`http://127.0.0.1:1337/updater/${methodName}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
body: JSON.stringify(args),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user