Compare commits

...

8 Commits

Author SHA1 Message Date
AAGaming ab6ec98160 API for patching existing routes, lower power use 2022-08-02 18:54:55 -04:00
Beebles f1e809781a forgot one update (#125) 2022-07-30 19:42:16 -07:00
Beebles 789058b72f Readme.md references incorrect github repo (#124) 2022-07-30 18:40:44 -07:00
TrainDoctor 4a68b1430d Update README.md 2022-07-28 13:50:03 -07:00
TrainDoctor 66c4a7e16e Update README.md 2022-07-25 17:29:56 -07:00
TrainDoctor b929b2dddf Update README.md 2022-07-25 17:08:10 -07:00
TrainDoctor fb0b703438 Fix unintended question mark in "Installing" modal 2022-07-25 16:07:16 -07:00
AAGaming afb2c7c0ed Better install process UX, fix reinstalling 2022-07-25 17:13:50 -04:00
12 changed files with 183 additions and 56 deletions
+23 -7
View File
@@ -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
+7 -1
View File
@@ -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([
@@ -64,6 +66,9 @@ class PluginBrowser:
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")
@@ -95,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:
+3 -3
View File
@@ -46,7 +46,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)
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.web_app, self.plugin_loader.plugins)
self.utilities = Utilities(self)
self.updater = Updater(self)
@@ -83,8 +83,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()
+1 -1
View File
@@ -37,7 +37,7 @@
}
},
"dependencies": {
"decky-frontend-lib": "^1.2.1",
"decky-frontend-lib": "^1.2.4",
"react-icons": "^4.4.0"
}
}
+4 -4
View File
@@ -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:
+35 -5
View File
@@ -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;
+4 -1
View File
@@ -7,6 +7,7 @@ declare global {
DeckyUpdater?: DeckyUpdater;
importDeckyPlugin: Function;
syncDeckyPlugins: Function;
deckyHasLoaded: boolean;
}
}
@@ -21,8 +22,10 @@ window.importDeckyPlugin = function (name: string) {
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);
if (!window.DeckyPluginLoader.hasPlugin(plugin)) window.DeckyPluginLoader?.importPlugin(plugin);
}
};
setTimeout(() => window.syncDeckyPlugins(), 5000);
window.deckyHasLoaded = true;
+11 -1
View File
@@ -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);
}
}
+13 -18
View File
@@ -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,23 +56,13 @@ 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 })}
/>,
);
}
@@ -97,6 +88,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}`);
+37 -13
View File
@@ -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,19 +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();
let routerIndex = children.props.children[0].props.children.length;
if (
!children.props.children[0].props.children[routerIndex - 1]?.length ||
children.props.children[0].props.children[routerIndex - 1]?.length !== routes.size
) {
if (
children.props.children[0].props.children[routerIndex - 1]?.length &&
children.props.children[0].props.children[routerIndex - 1].length !== routes.size
)
routerIndex--;
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(
@@ -59,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;
};
@@ -97,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);
}
+2 -2
View File
@@ -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);
}