summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--backend/injector.py196
-rw-r--r--backend/utilities.py73
-rw-r--r--frontend/src/components/Toast.tsx2
-rw-r--r--frontend/src/components/settings/index.tsx50
-rw-r--r--frontend/src/components/settings/pages/developer/index.tsx84
-rw-r--r--frontend/src/components/settings/pages/general/index.tsx24
-rw-r--r--frontend/src/developer.tsx88
-rw-r--r--frontend/src/index.tsx28
-rw-r--r--frontend/src/plugin-loader.tsx9
-rw-r--r--frontend/src/toaster.tsx5
-rw-r--r--frontend/src/utils/hooks/useSetting.ts24
-rw-r--r--frontend/src/utils/settings.ts24
12 files changed, 527 insertions, 80 deletions
diff --git a/backend/injector.py b/backend/injector.py
index 82436adf..de914d17 100644
--- a/backend/injector.py
+++ b/backend/injector.py
@@ -1,7 +1,7 @@
-#Injector code from https://github.com/SteamDeckHomebrew/steamdeck-ui-inject. More info on how it works there.
+# Injector code from https://github.com/SteamDeckHomebrew/steamdeck-ui-inject. More info on how it works there.
from asyncio import sleep
-from logging import debug, getLogger
+from logging import getLogger
from traceback import format_exc
from aiohttp import ClientSession
@@ -11,7 +11,10 @@ BASE_ADDRESS = "http://localhost:8080"
logger = getLogger("Injector")
+
class Tab:
+ cmd_id = 0
+
def __init__(self, res) -> None:
self.title = res["title"]
self.id = res["id"]
@@ -24,14 +27,24 @@ class Tab:
self.client = ClientSession()
self.websocket = await self.client.ws_connect(self.ws_url)
+ async def close_websocket(self):
+ await self.client.close()
+
async def listen_for_message(self):
async for message in self.websocket:
- yield message
+ data = message.json()
+ yield data
async def _send_devtools_cmd(self, dc, receive=True):
if self.websocket:
+ self.cmd_id += 1
+ dc["id"] = self.cmd_id
await self.websocket.send_json(dc)
- return (await self.websocket.receive_json()) if receive else None
+ if receive:
+ async for msg in self.listen_for_message():
+ if "id" in msg and msg["id"] == dc["id"]:
+ return msg
+ return None
raise RuntimeError("Websocket not opened")
async def evaluate_js(self, js, run_async=False, manage_socket=True, get_result=True):
@@ -39,7 +52,6 @@ class Tab:
await self.open_websocket()
res = await self._send_devtools_cmd({
- "id": 1,
"method": "Runtime.evaluate",
"params": {
"expression": js,
@@ -49,9 +61,172 @@ class Tab:
}, get_result)
if manage_socket:
- await self.client.close()
+ await self.close_websocket()
return res
+ async def enable(self):
+ """
+ Enables page domain notifications.
+ """
+ await self._send_devtools_cmd({
+ "method": "Page.enable",
+ }, False)
+
+ async def disable(self):
+ """
+ Disables page domain notifications.
+ """
+ await self._send_devtools_cmd({
+ "method": "Page.disable",
+ }, False)
+
+ async def reload_and_evaluate(self, js, manage_socket=True):
+ """
+ Reloads the current tab, with JS to run on load via debugger
+ """
+ if manage_socket:
+ await self.open_websocket()
+
+ await self._send_devtools_cmd({
+ "method": "Debugger.enable"
+ }, True)
+
+ await self._send_devtools_cmd({
+ "method": "Runtime.evaluate",
+ "params": {
+ "expression": "location.reload();",
+ "userGesture": True,
+ "awaitPromise": False
+ }
+ }, False)
+
+ breakpoint_res = await self._send_devtools_cmd({
+ "method": "Debugger.setInstrumentationBreakpoint",
+ "params": {
+ "instrumentation": "beforeScriptExecution"
+ }
+ }, True)
+
+ logger.info(breakpoint_res)
+
+ # Page finishes loading when breakpoint hits
+
+ for x in range(20):
+ # this works around 1/5 of the time, so just send it 8 times.
+ # the js accounts for being injected multiple times allowing only one instance to run at a time anyway
+ await self._send_devtools_cmd({
+ "method": "Runtime.evaluate",
+ "params": {
+ "expression": js,
+ "userGesture": True,
+ "awaitPromise": False
+ }
+ }, False)
+
+ await self._send_devtools_cmd({
+ "method": "Debugger.removeBreakpoint",
+ "params": {
+ "breakpointId": breakpoint_res["result"]["breakpointId"]
+ }
+ }, False)
+
+ for x in range(4):
+ await self._send_devtools_cmd({
+ "method": "Debugger.resume"
+ }, False)
+
+ await self._send_devtools_cmd({
+ "method": "Debugger.disable"
+ }, True)
+
+ if manage_socket:
+ await self.close_websocket()
+ return
+
+ async def add_script_to_evaluate_on_new_document(self, js, add_dom_wrapper=True, manage_socket=True, get_result=True):
+ """
+ How the underlying call functions is not particularly clear from the devtools docs, so stealing puppeteer's description:
+
+ Adds a function which would be invoked in one of the following scenarios:
+ * whenever the page is navigated
+ * whenever the child frame is attached or navigated. In this case, the
+ function is invoked in the context of the newly attached frame.
+
+ The function is invoked after the document was created but before any of
+ its scripts were run. This is useful to amend the JavaScript environment,
+ e.g. to seed `Math.random`.
+
+ Parameters
+ ----------
+ js : str
+ The script to evaluate on new document
+ add_dom_wrapper : bool
+ True to wrap the script in a wait for the 'DOMContentLoaded' event.
+ DOM will usually not exist when this execution happens,
+ so it is necessary to delay til DOM is loaded if you are modifying it
+ manage_socket : bool
+ True to have this function handle opening/closing the websocket for this tab
+ get_result : bool
+ True to wait for the result of this call
+
+ Returns
+ -------
+ int or None
+ The identifier of the script added, used to remove it later.
+ (see remove_script_to_evaluate_on_new_document below)
+ None is returned if `get_result` is False
+ """
+
+ wrappedjs = """
+ function scriptFunc() {
+ {js}
+ }
+ if (document.readyState === 'loading') {
+ addEventListener('DOMContentLoaded', () => {
+ scriptFunc();
+ });
+ } else {
+ scriptFunc();
+ }
+ """.format(js=js) if add_dom_wrapper else js
+
+ if manage_socket:
+ await self.open_websocket()
+
+ res = await self._send_devtools_cmd({
+ "method": "Page.addScriptToEvaluateOnNewDocument",
+ "params": {
+ "source": wrappedjs
+ }
+ }, get_result)
+
+ if manage_socket:
+ await self.close_websocket()
+ return res
+
+ async def remove_script_to_evaluate_on_new_document(self, script_id, manage_socket=True):
+ """
+ Removes a script from a page that was added with `add_script_to_evaluate_on_new_document`
+
+ Parameters
+ ----------
+ script_id : int
+ The identifier of the script to remove (returned from `add_script_to_evaluate_on_new_document`)
+ """
+
+ if manage_socket:
+ await self.open_websocket()
+
+ res = await self._send_devtools_cmd({
+ "method": "Page.removeScriptToEvaluateOnNewDocument",
+ "params": {
+ "identifier": script_id
+ }
+ }, False)
+
+ if manage_socket:
+ await self.close_websocket()
+
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"]
@@ -59,6 +234,7 @@ class Tab:
def __repr__(self):
return self.title
+
async def get_tabs():
async with ClientSession() as web:
res = {}
@@ -80,6 +256,7 @@ async def get_tabs():
else:
raise Exception(f"/json did not return 200. {await res.text()}")
+
async def get_tab(tab_name):
tabs = await get_tabs()
tab = next((i for i in tabs if i.title == tab_name), None)
@@ -87,11 +264,13 @@ async def get_tab(tab_name):
raise ValueError(f"Tab {tab_name} not found")
return tab
+
async def inject_to_tab(tab_name, js, run_async=False):
tab = await get_tab(tab_name)
return await tab.evaluate_js(js, run_async)
+
async def tab_has_global_var(tab_name, var_name):
try:
tab = await get_tab(tab_name)
@@ -104,14 +283,15 @@ async def tab_has_global_var(tab_name, var_name):
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"] \ No newline at end of file
+ return res["result"]["result"]["value"]
diff --git a/backend/utilities.py b/backend/utilities.py
index 853f60d2..8d899c0d 100644
--- a/backend/utilities.py
+++ b/backend/utilities.py
@@ -1,10 +1,13 @@
import uuid
import os
from json.decoder import JSONDecodeError
+from traceback import format_exc
+from asyncio import sleep, start_server, gather, open_connection
from aiohttp import ClientSession, web
-from injector import inject_to_tab
+from logging import getLogger
+from injector import inject_to_tab, get_tab
import helpers
import subprocess
@@ -26,9 +29,17 @@ class Utilities:
"disallow_remote_debugging": self.disallow_remote_debugging,
"set_setting": self.set_setting,
"get_setting": self.get_setting,
- "filepicker_ls": self.filepicker_ls
+ "filepicker_ls": self.filepicker_ls,
+ "disable_rdt": self.disable_rdt,
+ "enable_rdt": self.enable_rdt
}
+ self.logger = getLogger("Utilities")
+
+ self.rdt_proxy_server = None
+ self.rdt_script_id = None
+ self.rdt_proxy_task = None
+
if context:
context.web_app.add_routes([
web.post("/methods/{method_name}", self._handle_server_method_call)
@@ -194,3 +205,61 @@ class Utilities:
"realpath": os.path.realpath(path),
"files": files
}
+
+ # Based on https://stackoverflow.com/a/46422554/13174603
+ def start_rdt_proxy(self, ip, port):
+ async def pipe(reader, writer):
+ try:
+ while not reader.at_eof():
+ writer.write(await reader.read(2048))
+ finally:
+ writer.close()
+ async def handle_client(local_reader, local_writer):
+ try:
+ remote_reader, remote_writer = await open_connection(
+ ip, port)
+ pipe1 = pipe(local_reader, remote_writer)
+ pipe2 = pipe(remote_reader, local_writer)
+ await gather(pipe1, pipe2)
+ finally:
+ local_writer.close()
+
+ self.rdt_proxy_server = start_server(handle_client, "127.0.0.1", port)
+ self.rdt_proxy_task = self.context.loop.create_task(self.rdt_proxy_server)
+
+ def stop_rdt_proxy(self):
+ if self.rdt_proxy_server:
+ self.rdt_proxy_server.close()
+ self.rdt_proxy_task.cancel()
+
+ async def enable_rdt(self):
+ # TODO un-hardcode port
+ try:
+ self.stop_rdt_proxy()
+ ip = self.context.settings.getSetting("developer.rdt.ip", None)
+
+ if ip != None:
+ self.logger.info("Connecting to React DevTools at " + ip)
+ async with ClientSession() as web:
+ async with web.request("GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context()) as res:
+ if res.status != 200:
+ self.logger.error("Failed to connect to React DevTools at " + ip)
+ return False
+ self.start_rdt_proxy(ip, 8097)
+ script = "if(!window.deckyHasConnectedRDT){window.deckyHasConnectedRDT=true;\n" + await res.text() + "\n}"
+ self.logger.info("Connected to React DevTools, loading script")
+ tab = await get_tab("SP")
+ # RDT needs to load before React itself to work.
+ result = await tab.reload_and_evaluate(script)
+ self.logger.info(result)
+
+ except Exception:
+ self.logger.error("Failed to connect to React DevTools")
+ self.logger.error(format_exc())
+
+ async def disable_rdt(self):
+ self.logger.info("Disabling React DevTools")
+ tab = await get_tab("SP")
+ self.rdt_script_id = None
+ await tab.evaluate_js("location.reload();", False, True, False)
+ self.logger.info("React DevTools disabled")
diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx
index 559c37c6..01a436d7 100644
--- a/frontend/src/components/Toast.tsx
+++ b/frontend/src/components/Toast.tsx
@@ -32,7 +32,7 @@ const Toast: FunctionComponent<ToastProps> = ({ toast }) => {
return (
<div
style={{ '--toast-duration': `${toast.nToastDurationMS}ms` } as React.CSSProperties}
- className={joinClassNames(toastClasses.ToastPopup, toastClasses.toastEnter)}
+ className={toastClasses.toastEnter}
>
<div
onClick={toast.data.onClick}
diff --git a/frontend/src/components/settings/index.tsx b/frontend/src/components/settings/index.tsx
index eb3a8bbd..01f7d407 100644
--- a/frontend/src/components/settings/index.tsx
+++ b/frontend/src/components/settings/index.tsx
@@ -1,25 +1,39 @@
import { SidebarNavigation } from 'decky-frontend-lib';
+import { lazy } from 'react';
+import { useSetting } from '../../utils/hooks/useSetting';
+import WithSuspense from '../WithSuspense';
import GeneralSettings from './pages/general';
import PluginList from './pages/plugin_list';
+const DeveloperSettings = lazy(() => import('./pages/developer'));
+
export default function SettingsPage() {
- return (
- <SidebarNavigation
- title="Decky Settings"
- showTitle
- pages={[
- {
- title: 'General',
- content: <GeneralSettings />,
- route: '/decky/settings/general',
- },
- {
- title: 'Plugins',
- content: <PluginList />,
- route: '/decky/settings/plugins',
- },
- ]}
- />
- );
+ const [isDeveloper, setIsDeveloper] = useSetting<boolean>('developer.enabled', false);
+
+ const pages = [
+ {
+ title: 'General',
+ content: <GeneralSettings isDeveloper={isDeveloper} setIsDeveloper={setIsDeveloper} />,
+ route: '/decky/settings/general',
+ },
+ {
+ title: 'Plugins',
+ content: <PluginList />,
+ route: '/decky/settings/plugins',
+ },
+ ];
+
+ if (isDeveloper)
+ pages.push({
+ title: 'Developer',
+ content: (
+ <WithSuspense>
+ <DeveloperSettings />
+ </WithSuspense>
+ ),
+ route: '/decky/settings/developer',
+ });
+
+ return <SidebarNavigation title="Decky Settings" showTitle pages={pages} />;
}
diff --git a/frontend/src/components/settings/pages/developer/index.tsx b/frontend/src/components/settings/pages/developer/index.tsx
new file mode 100644
index 00000000..447c9606
--- /dev/null
+++ b/frontend/src/components/settings/pages/developer/index.tsx
@@ -0,0 +1,84 @@
+import { Field, Focusable, TextField, Toggle } from 'decky-frontend-lib';
+import { useRef } from 'react';
+import { FaReact, FaSteamSymbol } from 'react-icons/fa';
+
+import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer';
+import { useSetting } from '../../../../utils/hooks/useSetting';
+
+export default function DeveloperSettings() {
+ const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
+ const [reactDevtoolsEnabled, setReactDevtoolsEnabled] = useSetting<boolean>('developer.rdt.enabled', false);
+ const [reactDevtoolsIP, setReactDevtoolsIP] = useSetting<string>('developer.rdt.ip', '');
+ const textRef = useRef<HTMLDivElement>(null);
+
+ return (
+ <>
+ <Field
+ label="Enable Valve Internal"
+ description={
+ <span style={{ whiteSpace: 'pre-line' }}>
+ Enables the Valve internal developer menu.{' '}
+ <span style={{ color: 'red' }}>Do not touch anything in this menu unless you know what it does.</span>
+ </span>
+ }
+ icon={<FaSteamSymbol style={{ display: 'block' }} />}
+ >
+ <Toggle
+ value={enableValveInternal}
+ onChange={(toggleValue) => {
+ setEnableValveInternal(toggleValue);
+ setShowValveInternal(toggleValue);
+ }}
+ />
+ </Field>{' '}
+ <Focusable
+ onTouchEnd={
+ reactDevtoolsIP == ''
+ ? () => {
+ (textRef.current?.childNodes[0] as HTMLInputElement)?.focus();
+ }
+ : undefined
+ }
+ onClick={
+ reactDevtoolsIP == ''
+ ? () => {
+ (textRef.current?.childNodes[0] as HTMLInputElement)?.focus();
+ }
+ : undefined
+ }
+ onOKButton={
+ reactDevtoolsIP == ''
+ ? () => {
+ (textRef.current?.childNodes[0] as HTMLInputElement)?.focus();
+ }
+ : undefined
+ }
+ >
+ <Field
+ label="Enable React DevTools"
+ description={
+ <>
+ <span style={{ whiteSpace: 'pre-line' }}>
+ Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set
+ the IP address before enabling.
+ </span>
+ <div ref={textRef}>
+ <TextField label={'IP'} value={reactDevtoolsIP} onChange={(e) => setReactDevtoolsIP(e?.target.value)} />
+ </div>
+ </>
+ }
+ icon={<FaReact style={{ display: 'block' }} />}
+ >
+ <Toggle
+ value={reactDevtoolsEnabled}
+ disabled={reactDevtoolsIP == ''}
+ onChange={(toggleValue) => {
+ setReactDevtoolsEnabled(toggleValue);
+ setShouldConnectToReactDevTools(toggleValue);
+ }}
+ />
+ </Field>
+ </Focusable>
+ </>
+ );
+}
diff --git a/frontend/src/components/settings/pages/general/index.tsx b/frontend/src/components/settings/pages/general/index.tsx
index a37d8dab..768cfafb 100644
--- a/frontend/src/components/settings/pages/general/index.tsx
+++ b/frontend/src/components/settings/pages/general/index.tsx
@@ -1,13 +1,19 @@
-import { DialogButton, Field, TextField } from 'decky-frontend-lib';
+import { DialogButton, Field, TextField, Toggle } from 'decky-frontend-lib';
import { useState } from 'react';
-import { FaShapes } from 'react-icons/fa';
+import { FaShapes, FaTools } from 'react-icons/fa';
import { installFromURL } from '../../../../store';
import BranchSelect from './BranchSelect';
import RemoteDebuggingSettings from './RemoteDebugging';
import UpdaterSettings from './Updater';
-export default function GeneralSettings() {
+export default function GeneralSettings({
+ isDeveloper,
+ setIsDeveloper,
+}: {
+ isDeveloper: boolean;
+ setIsDeveloper: (val: boolean) => void;
+}) {
const [pluginURL, setPluginURL] = useState('');
// const [checked, setChecked] = useState(false); // store these in some kind of State instead
return (
@@ -25,6 +31,18 @@ export default function GeneralSettings() {
<BranchSelect />
<RemoteDebuggingSettings />
<Field
+ label="Developer mode"
+ description={<span style={{ whiteSpace: 'pre-line' }}>Enables Decky's developer settings.</span>}
+ icon={<FaTools style={{ display: 'block' }} />}
+ >
+ <Toggle
+ value={isDeveloper}
+ onChange={(toggleValue) => {
+ setIsDeveloper(toggleValue);
+ }}
+ />
+ </Field>
+ <Field
label="Manual plugin install"
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
icon={<FaShapes style={{ display: 'block' }} />}
diff --git a/frontend/src/developer.tsx b/frontend/src/developer.tsx
new file mode 100644
index 00000000..b1fe74d6
--- /dev/null
+++ b/frontend/src/developer.tsx
@@ -0,0 +1,88 @@
+import {
+ ReactRouter,
+ Router,
+ findModule,
+ findModuleChild,
+ gamepadDialogClasses,
+ gamepadSliderClasses,
+ playSectionClasses,
+ quickAccessControlsClasses,
+ quickAccessMenuClasses,
+ scrollClasses,
+ scrollPanelClasses,
+ sleep,
+ staticClasses,
+ updaterFieldClasses,
+} from 'decky-frontend-lib';
+import { FaReact } from 'react-icons/fa';
+
+import Logger from './logger';
+import { getSetting } from './utils/settings';
+
+const logger = new Logger('DeveloperMode');
+
+let removeSettingsObserver: () => void = () => {};
+
+export function setShowValveInternal(show: boolean) {
+ const settingsMod = findModuleChild((m) => {
+ if (typeof m !== 'object') return undefined;
+ for (let prop in m) {
+ if (typeof m[prop]?.settings?.bIsValveEmail !== 'undefined') return m[prop];
+ }
+ });
+
+ if (show) {
+ removeSettingsObserver = settingsMod[
+ Object.getOwnPropertySymbols(settingsMod).find((x) => x.toString() == 'Symbol(mobx administration)') as any
+ ].observe((e: any) => {
+ e.newValue.bIsValveEmail = true;
+ });
+ settingsMod.m_Settings.bIsValveEmail = true;
+ logger.log('Enabled Valve Internal menu');
+ } else {
+ removeSettingsObserver();
+ settingsMod.m_Settings.bIsValveEmail = false;
+ logger.log('Disabled Valve Internal menu');
+ }
+}
+
+export async function setShouldConnectToReactDevTools(enable: boolean) {
+ window.DeckyPluginLoader.toaster.toast({
+ title: (enable ? 'Enabling' : 'Disabling') + ' React DevTools',
+ body: 'Reloading in 5 seconds',
+ icon: <FaReact />,
+ });
+ await sleep(5000);
+ return enable
+ ? window.DeckyPluginLoader.callServerMethod('enable_rdt')
+ : window.DeckyPluginLoader.callServerMethod('disable_rdt');
+}
+
+export async function startup() {
+ const isValveInternalEnabled = await getSetting('developer.valve_internal', false);
+ const isRDTEnabled = await getSetting('developer.rdt.enabled', false);
+
+ if (isValveInternalEnabled) setShowValveInternal(isValveInternalEnabled);
+
+ if ((isRDTEnabled && !window.deckyHasConnectedRDT) || (!isRDTEnabled && window.deckyHasConnectedRDT))
+ setShouldConnectToReactDevTools(isRDTEnabled);
+
+ logger.log('Exposing decky-frontend-lib APIs as DFL');
+ window.DFL = {
+ findModuleChild,
+ findModule,
+ Router,
+ ReactRouter,
+ classes: {
+ scrollClasses,
+ staticClasses,
+ playSectionClasses,
+ scrollPanelClasses,
+ updaterFieldClasses,
+ gamepadDialogClasses,
+ gamepadSliderClasses,
+ quickAccessMenuClasses,
+ quickAccessControlsClasses,
+ },
+ };
+}
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index 08b37d15..03010e13 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -1,6 +1,3 @@
-import { ButtonItem, CommonUIModule, webpackCache } from 'decky-frontend-lib';
-import { forwardRef } from 'react';
-
import PluginLoader from './plugin-loader';
import { DeckyUpdater } from './updater';
@@ -11,32 +8,12 @@ declare global {
importDeckyPlugin: Function;
syncDeckyPlugins: Function;
deckyHasLoaded: boolean;
+ deckyHasConnectedRDT?: boolean;
deckyAuthToken: string;
- webpackJsonp: any;
+ DFL?: any;
}
}
-// HACK to fix plugins using webpack v4 push
-
-const v4Cache = {};
-for (let m of Object.keys(webpackCache)) {
- v4Cache[m] = { exports: webpackCache[m] };
-}
-
-if (!window.webpackJsonp || window.webpackJsonp.deckyShimmed) {
- window.webpackJsonp = {
- deckyShimmed: true,
- push: (mod: any): any => {
- if (mod[1].get_require) return { c: v4Cache };
- },
- };
- CommonUIModule.__deckyButtonItemShim = forwardRef((props: any, ref: any) => {
- // tricks the old filter into working
- const dummy = `childrenContainerWidth:"min"`;
- return <ButtonItem ref={ref} _shim={dummy} {...props} />;
- });
-}
-
(async () => {
window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text());
@@ -44,6 +21,7 @@ if (!window.webpackJsonp || window.webpackJsonp.deckyShimmed) {
window.DeckyPluginLoader?.deinit();
window.DeckyPluginLoader = new PluginLoader();
+ window.DeckyPluginLoader.init();
window.importDeckyPlugin = function (name: string, version: string) {
window.DeckyPluginLoader?.importPlugin(name, version);
};
diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx
index 6eee9bc0..400e7484 100644
--- a/frontend/src/plugin-loader.tsx
+++ b/frontend/src/plugin-loader.tsx
@@ -25,6 +25,7 @@ import { checkForUpdates } from './store';
import TabsHook from './tabs-hook';
import Toaster from './toaster';
import { VerInfo, callUpdaterMethod } from './updater';
+import { getSetting } from './utils/settings';
const StorePage = lazy(() => import('./components/store/Store'));
const SettingsPage = lazy(() => import('./components/settings'));
@@ -40,7 +41,7 @@ class PluginLoader extends Logger {
private tabsHook: TabsHook = new TabsHook();
// private windowHook: WindowHook = new WindowHook();
private routerHook: RouterHook = new RouterHook();
- private toaster: Toaster = new Toaster();
+ public toaster: Toaster = new Toaster();
private deckyState: DeckyState = new DeckyState();
private reloadLock: boolean = false;
@@ -172,6 +173,12 @@ class PluginLoader extends Logger {
}
}
+ public init() {
+ getSetting('developer.enabled', false).then((val) => {
+ if (val) import('./developer').then((developer) => developer.startup());
+ });
+ }
+
public deinit() {
this.routerHook.removeRoute('/decky/store');
this.routerHook.removeRoute('/decky/settings');
diff --git a/frontend/src/toaster.tsx b/frontend/src/toaster.tsx
index a55c87e6..55987056 100644
--- a/frontend/src/toaster.tsx
+++ b/frontend/src/toaster.tsx
@@ -38,7 +38,7 @@ class Toaster extends Logger {
await sleep(2000);
}
- this.node = instance.return.return;
+ this.node = instance.sibling.child;
let toast: any;
let renderedToast: ReactNode = null;
this.node.stateNode.render = (...args: any[]) => {
@@ -68,8 +68,7 @@ class Toaster extends Logger {
delete this.node.stateNode.render;
}
return ret;
- };
- this.node.stateNode.forceUpdate();
+ });
this.settingsModule = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
diff --git a/frontend/src/utils/hooks/useSetting.ts b/frontend/src/utils/hooks/useSetting.ts
index f950bf6a..afed5239 100644
--- a/frontend/src/utils/hooks/useSetting.ts
+++ b/frontend/src/utils/hooks/useSetting.ts
@@ -1,25 +1,14 @@
import { useEffect, useState } from 'react';
-interface GetSettingArgs<T> {
- key: string;
- default: T;
-}
-
-interface SetSettingArgs<T> {
- key: string;
- value: T;
-}
+import { getSetting, setSetting } from '../settings';
-export function useSetting<T>(key: string, def: T): [value: T | null, setValue: (value: T) => Promise<void>] {
+export function useSetting<T>(key: string, def: T): [value: T, setValue: (value: T) => Promise<void>] {
const [value, setValue] = useState(def);
useEffect(() => {
(async () => {
- const res = (await window.DeckyPluginLoader.callServerMethod('get_setting', {
- key,
- default: def,
- } as GetSettingArgs<T>)) as { result: T };
- setValue(res.result);
+ const res = await getSetting<T>(key, def);
+ setValue(res);
})();
}, []);
@@ -27,10 +16,7 @@ export function useSetting<T>(key: string, def: T): [value: T | null, setValue:
value,
async (val: T) => {
setValue(val);
- await window.DeckyPluginLoader.callServerMethod('set_setting', {
- key,
- value: val,
- } as SetSettingArgs<T>);
+ await setSetting(key, val);
},
];
}
diff --git a/frontend/src/utils/settings.ts b/frontend/src/utils/settings.ts
new file mode 100644
index 00000000..cadfe935
--- /dev/null
+++ b/frontend/src/utils/settings.ts
@@ -0,0 +1,24 @@
+interface GetSettingArgs<T> {
+ key: string;
+ default: T;
+}
+
+interface SetSettingArgs<T> {
+ key: string;
+ value: T;
+}
+
+export async function getSetting<T>(key: string, def: T): Promise<T> {
+ const res = (await window.DeckyPluginLoader.callServerMethod('get_setting', {
+ key,
+ default: def,
+ } as GetSettingArgs<T>)) as { result: T };
+ return res.result;
+}
+
+export async function setSetting<T>(key: string, value: T): Promise<void> {
+ await window.DeckyPluginLoader.callServerMethod('set_setting', {
+ key,
+ value,
+ } as SetSettingArgs<T>);
+}