mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 00:37:49 +00:00
Developer menu (#211)
* add settings utils to use settings outside of components * initial implementation of developer menu * ✨ Add support for addScriptToEvaluateOnNewDocument * React DevTools support * increase chance of RDT successfully injecting * Rewrite toaster hook to not re-create the window * remove friends focus workaround because it's fixed * Expose various DFL utilities as DFL in dev mode * try to fix text field focuss * move focusable to outside field * add onTouchEnd and onClick to focusable * Update pnpm-lock.yaml Co-authored-by: FinalDoom <7464170-FinalDoom@users.noreply.gitlab.com> Co-authored-by: TrainDoctor <traindoctor@protonmail.com>
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
@@ -24,6 +30,18 @@ export default function GeneralSettings() {
|
||||
<UpdaterSettings />
|
||||
<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)} />}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
+3
-25
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface GetSettingArgs<T> {
|
||||
key: string;
|
||||
default: T;
|
||||
}
|
||||
import { getSetting, setSetting } from '../settings';
|
||||
|
||||
interface SetSettingArgs<T> {
|
||||
key: string;
|
||||
value: T;
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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>);
|
||||
}
|
||||
Reference in New Issue
Block a user