From 6e3c05072cb507e2a376b7019836bea7bf663cb0 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Sat, 15 Oct 2022 23:46:42 -0400 Subject: Developer menu (#211) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- frontend/src/components/Toast.tsx | 2 +- frontend/src/components/settings/index.tsx | 50 +++++++----- .../components/settings/pages/developer/index.tsx | 84 +++++++++++++++++++++ .../components/settings/pages/general/index.tsx | 24 +++++- frontend/src/developer.tsx | 88 ++++++++++++++++++++++ frontend/src/index.tsx | 28 +------ frontend/src/plugin-loader.tsx | 9 ++- frontend/src/toaster.tsx | 5 +- frontend/src/utils/hooks/useSetting.ts | 24 ++---- frontend/src/utils/settings.ts | 24 ++++++ 10 files changed, 268 insertions(+), 70 deletions(-) create mode 100644 frontend/src/components/settings/pages/developer/index.tsx create mode 100644 frontend/src/developer.tsx create mode 100644 frontend/src/utils/settings.ts (limited to 'frontend/src') 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 = ({ toast }) => { return (
import('./pages/developer')); + export default function SettingsPage() { - return ( - , - route: '/decky/settings/general', - }, - { - title: 'Plugins', - content: , - route: '/decky/settings/plugins', - }, - ]} - /> - ); + const [isDeveloper, setIsDeveloper] = useSetting('developer.enabled', false); + + const pages = [ + { + title: 'General', + content: , + route: '/decky/settings/general', + }, + { + title: 'Plugins', + content: , + route: '/decky/settings/plugins', + }, + ]; + + if (isDeveloper) + pages.push({ + title: 'Developer', + content: ( + + + + ), + route: '/decky/settings/developer', + }); + + return ; } 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('developer.valve_internal', false); + const [reactDevtoolsEnabled, setReactDevtoolsEnabled] = useSetting('developer.rdt.enabled', false); + const [reactDevtoolsIP, setReactDevtoolsIP] = useSetting('developer.rdt.ip', ''); + const textRef = useRef(null); + + return ( + <> + + Enables the Valve internal developer menu.{' '} + Do not touch anything in this menu unless you know what it does. + + } + icon={} + > + { + setEnableValveInternal(toggleValue); + setShowValveInternal(toggleValue); + }} + /> + {' '} + { + (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 + } + > + + + Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set + the IP address before enabling. + +
+ setReactDevtoolsIP(e?.target.value)} /> +
+ + } + icon={} + > + { + setReactDevtoolsEnabled(toggleValue); + setShouldConnectToReactDevTools(toggleValue); + }} + /> +
+
+ + ); +} 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 ( @@ -24,6 +30,18 @@ export default function GeneralSettings() { + Enables Decky's developer settings.} + icon={} + > + { + setIsDeveloper(toggleValue); + }} + /> + setPluginURL(e?.target.value)} />} 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: , + }); + 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 ; - }); -} - (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 { - key: string; - default: T; -} - -interface SetSettingArgs { - key: string; - value: T; -} +import { getSetting, setSetting } from '../settings'; -export function useSetting(key: string, def: T): [value: T | null, setValue: (value: T) => Promise] { +export function useSetting(key: string, def: T): [value: T, setValue: (value: T) => Promise] { const [value, setValue] = useState(def); useEffect(() => { (async () => { - const res = (await window.DeckyPluginLoader.callServerMethod('get_setting', { - key, - default: def, - } as GetSettingArgs)) as { result: T }; - setValue(res.result); + const res = await getSetting(key, def); + setValue(res); })(); }, []); @@ -27,10 +16,7 @@ export function useSetting(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); + 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 { + key: string; + default: T; +} + +interface SetSettingArgs { + key: string; + value: T; +} + +export async function getSetting(key: string, def: T): Promise { + const res = (await window.DeckyPluginLoader.callServerMethod('get_setting', { + key, + default: def, + } as GetSettingArgs)) as { result: T }; + return res.result; +} + +export async function setSetting(key: string, value: T): Promise { + await window.DeckyPluginLoader.callServerMethod('set_setting', { + key, + value, + } as SetSettingArgs); +} -- cgit v1.2.3