diff --git a/examples/framey-settings/README.md b/examples/framey-settings/README.md index 55c9228..52e1233 100644 --- a/examples/framey-settings/README.md +++ b/examples/framey-settings/README.md @@ -13,6 +13,14 @@ browser cache. A missing, damaged, or old non-boolean value reads as the caller's default; the next save atomically replaces the sample's settings file. +The **About this setting** button opens a local detail page at +`/framey-sample/details/saved-setting`. It demonstrates `routerHook.addRoute`, +`Navigation.Navigate`, `useParams`, `NavigateBack`, and removal of the route in +`onDismount`. The `:topic?` segment is optional. Framey keeps this navigation +inside the plugin panel; it does not open a Steam library page. Put optional +segments at the end of the path and do not use query strings. This detail page +has not been checked with headset controllers. + ## Build From this directory, run: @@ -35,7 +43,7 @@ plugin directory does not enable its VR panel. Run `python3 -m unittest discover -s tests -p test_framey_sample.py` from the repository root. It checks backend persistence and evaluates the built bundle against Framey's actual host script, including the quick action's scoped -backend call. The fake Frame store flow is exercised by +backend call and local route registration/navigation. The fake Frame store flow is exercised by the repository's e2e suite. No worn-headset visibility or controller-input test has passed for this sample yet. diff --git a/examples/framey-settings/dist/index.js b/examples/framey-settings/dist/index.js index 714534c..c33f008 100644 --- a/examples/framey-settings/dist/index.js +++ b/examples/framey-settings/dist/index.js @@ -16,6 +16,7 @@ if (api._version != API_VERSION) { console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version ${api._version}. Some features may not work.`); } const callable = api.callable; +const routerHook = api.routerHook; const toaster = api.toaster; const definePlugin = (fn) => { return (...args) => { @@ -59,24 +60,32 @@ function Content() { setBusy(false); } } - return SP_JSX.jsxs(DFL.PanelSection, { title: "Framey settings sample", children: [SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ToggleField, { label: "Example setting", description: "Stored by the Decky Python backend", checked: enabled, disabled: busy, onChange: (value) => { void change(value); } }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { disabled: busy, onClick: () => { void refresh(); }, children: "Reload saved setting" }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx("p", { role: "status", children: message }) })] }); + return SP_JSX.jsxs(DFL.PanelSection, { title: "Framey settings sample", children: [SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ToggleField, { label: "Example setting", description: "Stored by the Decky Python backend", checked: enabled, disabled: busy, onChange: (value) => { void change(value); } }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { disabled: busy, onClick: () => { void refresh(); }, children: "Reload saved setting" }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { onClick: () => DFL.Navigation.Navigate('/framey-sample/details/saved-setting'), children: "About this setting" }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx("p", { role: "status", children: message }) })] }); } -var index = definePlugin(() => Object.assign({ - name: 'Framey Settings Sample', - icon: SP_JSX.jsx("span", { "aria-hidden": "true", children: "\u2699" }), - content: SP_JSX.jsx(Content, {}), -}, { - // Framey additions are ignored by Decky's own menu. Keep the main content - // usable there too, while showing authors the extra VR settings view. - settings: SP_JSX.jsx(Content, {}), - frameyQuickActions: [{ - label: 'Read saved setting', - onClick: async () => { - const enabled = await getSetting('enabled', false); - return `Saved setting is ${enabled ? 'on' : 'off'}`; - }, - }], -})); +function Details() { + const { topic } = DFL.useParams(); + return SP_JSX.jsxs(DFL.PanelSection, { title: "Sample detail page", children: [SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsxs("p", { children: ["Topic: ", topic || 'overview'] }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx("p", { children: "The toggle is stored by this plugin's Decky backend in the user's settings directory." }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { onClick: () => DFL.Navigation.NavigateBack(), children: "Back to setting" }) })] }); +} +var index = definePlugin(() => { + routerHook.addRoute('/framey-sample/details/:topic?', Details); + return Object.assign({ + name: 'Framey Settings Sample', + icon: SP_JSX.jsx("span", { "aria-hidden": "true", children: "\u2699" }), + content: SP_JSX.jsx(Content, {}), + onDismount: () => routerHook.removeRoute('/framey-sample/details/:topic?'), + }, { + // Framey additions are ignored by Decky's own menu. Keep the main content + // usable there too, while showing authors the extra VR settings view. + settings: SP_JSX.jsx(Content, {}), + frameyQuickActions: [{ + label: 'Read saved setting', + onClick: async () => { + const enabled = await getSetting('enabled', false); + return `Saved setting is ${enabled ? 'on' : 'off'}`; + }, + }], + }); +}); export { index as default }; //# sourceMappingURL=index.js.map diff --git a/examples/framey-settings/dist/index.js.map b/examples/framey-settings/dist/index.js.map index cf55c3c..8a59a17 100644 --- a/examples/framey-settings/dist/index.js.map +++ b/examples/framey-settings/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sources":["decky://decky/plugin/Framey%20Settings%20Sample/node_modules/.pnpm/@decky+api@1.1.3/node_modules/@decky/api/dist/index.js","decky://decky/plugin/Framey%20Settings%20Sample/src/index.tsx"],"sourcesContent":["import _manifest from '@decky/manifest';\nexport * from \"./types\";\nconst manifest = _manifest;\nconst API_VERSION = 2;\nif (!manifest?.name) {\n throw new Error('[@decky/api]: Failed to find plugin manifest.');\n}\nconst internalAPIConnection = window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit;\nif (!internalAPIConnection) {\n throw new Error('[@decky/api]: Failed to connect to the loader as as the loader API was not initialized. This is likely a bug in Decky Loader.');\n}\nlet api;\ntry {\n api = internalAPIConnection.connect(API_VERSION, manifest.name);\n}\ncatch {\n api = internalAPIConnection.connect(1, manifest.name);\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version 1. Some features may not work.`);\n}\nif (api._version != API_VERSION) {\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version ${api._version}. Some features may not work.`);\n}\nexport const call = api.call;\nexport const callable = api.callable;\nexport const addEventListener = api.addEventListener;\nexport const removeEventListener = api.removeEventListener;\nexport const routerHook = api.routerHook;\nexport const toaster = api.toaster;\nexport const openFilePicker = api.openFilePicker;\nexport const executeInTab = api.executeInTab;\nexport const injectCssIntoTab = api.injectCssIntoTab;\nexport const removeCssFromTab = api.removeCssFromTab;\nexport const fetchNoCors = api.fetchNoCors;\nexport const getExternalResourceURL = api.getExternalResourceURL;\nexport const useQuickAccessVisible = api.useQuickAccessVisible;\nexport const definePlugin = (fn) => {\n return (...args) => {\n return fn(...args);\n };\n};\n","import {callable, definePlugin, toaster} from '@decky/api';\nimport {ButtonItem, PanelSection, PanelSectionRow, ToggleField} from '@decky/ui';\nimport {useEffect, useState} from 'react';\n\nconst getSetting = callable<[key: string, fallback: boolean], boolean>('get_setting');\nconst setSetting = callable<[key: string, value: boolean], boolean>('set_setting');\n\nfunction Content() {\n const [enabled, setEnabled] = useState(false);\n const [busy, setBusy] = useState(true);\n const [message, setMessage] = useState('Loading setting…');\n\n async function refresh() {\n setBusy(true);\n try {\n const value = await getSetting('enabled', false);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n } catch (error) {\n setMessage(`Could not read setting: ${String(error)}`);\n } finally {\n setBusy(false);\n }\n }\n\n useEffect(() => {void refresh();}, []);\n\n async function change(value: boolean) {\n setBusy(true);\n try {\n await setSetting('enabled', value);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n toaster.toast({title: 'Framey sample', body: 'Setting saved'});\n } catch (error) {\n setMessage(`Could not save setting: ${String(error)}`);\n } finally {\n setBusy(false);\n }\n }\n\n return \n \n {void change(value);}} />\n \n {void refresh();}}>Reload saved setting\n

{message}

\n
;\n}\n\nexport default definePlugin(() => Object.assign({\n name: 'Framey Settings Sample',\n icon: ⚙,\n content: ,\n}, {\n // Framey additions are ignored by Decky's own menu. Keep the main content\n // usable there too, while showing authors the extra VR settings view.\n settings: ,\n frameyQuickActions: [{\n label: 'Read saved setting',\n onClick: async () => {\n const enabled = await getSetting('enabled', false);\n return `Saved setting is ${enabled ? 'on' : 'off'}`;\n },\n }],\n}));\n"],"names":[],"mappings":"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","x_google_ignoreList":[0]} \ No newline at end of file +{"version":3,"file":"index.js","sources":["decky://decky/plugin/Framey%20Settings%20Sample/node_modules/.pnpm/@decky+api@1.1.3/node_modules/@decky/api/dist/index.js","decky://decky/plugin/Framey%20Settings%20Sample/src/index.tsx"],"sourcesContent":["import _manifest from '@decky/manifest';\nexport * from \"./types\";\nconst manifest = _manifest;\nconst API_VERSION = 2;\nif (!manifest?.name) {\n throw new Error('[@decky/api]: Failed to find plugin manifest.');\n}\nconst internalAPIConnection = window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit;\nif (!internalAPIConnection) {\n throw new Error('[@decky/api]: Failed to connect to the loader as as the loader API was not initialized. This is likely a bug in Decky Loader.');\n}\nlet api;\ntry {\n api = internalAPIConnection.connect(API_VERSION, manifest.name);\n}\ncatch {\n api = internalAPIConnection.connect(1, manifest.name);\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version 1. Some features may not work.`);\n}\nif (api._version != API_VERSION) {\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version ${api._version}. Some features may not work.`);\n}\nexport const call = api.call;\nexport const callable = api.callable;\nexport const addEventListener = api.addEventListener;\nexport const removeEventListener = api.removeEventListener;\nexport const routerHook = api.routerHook;\nexport const toaster = api.toaster;\nexport const openFilePicker = api.openFilePicker;\nexport const executeInTab = api.executeInTab;\nexport const injectCssIntoTab = api.injectCssIntoTab;\nexport const removeCssFromTab = api.removeCssFromTab;\nexport const fetchNoCors = api.fetchNoCors;\nexport const getExternalResourceURL = api.getExternalResourceURL;\nexport const useQuickAccessVisible = api.useQuickAccessVisible;\nexport const definePlugin = (fn) => {\n return (...args) => {\n return fn(...args);\n };\n};\n","import {callable, definePlugin, routerHook, toaster} from '@decky/api';\nimport {ButtonItem, Navigation, PanelSection, PanelSectionRow, ToggleField, useParams} from '@decky/ui';\nimport {useEffect, useState} from 'react';\n\nconst getSetting = callable<[key: string, fallback: boolean], boolean>('get_setting');\nconst setSetting = callable<[key: string, value: boolean], boolean>('set_setting');\n\nfunction Content() {\n const [enabled, setEnabled] = useState(false);\n const [busy, setBusy] = useState(true);\n const [message, setMessage] = useState('Loading setting…');\n\n async function refresh() {\n setBusy(true);\n try {\n const value = await getSetting('enabled', false);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n } catch (error) {\n setMessage(`Could not read setting: ${String(error)}`);\n } finally {\n setBusy(false);\n }\n }\n\n useEffect(() => {void refresh();}, []);\n\n async function change(value: boolean) {\n setBusy(true);\n try {\n await setSetting('enabled', value);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n toaster.toast({title: 'Framey sample', body: 'Setting saved'});\n } catch (error) {\n setMessage(`Could not save setting: ${String(error)}`);\n } finally {\n setBusy(false);\n }\n }\n\n return \n \n {void change(value);}} />\n \n {void refresh();}}>Reload saved setting\n Navigation.Navigate('/framey-sample/details/saved-setting')}>About this setting\n

{message}

\n
;\n}\n\nfunction Details() {\n const {topic} = useParams<{topic?: string}>();\n return \n

Topic: {topic || 'overview'}

\n

The toggle is stored by this plugin's Decky backend in the user's settings directory.

\n Navigation.NavigateBack()}>Back to setting\n
;\n}\n\nexport default definePlugin(() => {\n routerHook.addRoute('/framey-sample/details/:topic?', Details);\n return Object.assign({\n name: 'Framey Settings Sample',\n icon: ⚙,\n content: ,\n onDismount: () => routerHook.removeRoute('/framey-sample/details/:topic?'),\n}, {\n // Framey additions are ignored by Decky's own menu. Keep the main content\n // usable there too, while showing authors the extra VR settings view.\n settings: ,\n frameyQuickActions: [{\n label: 'Read saved setting',\n onClick: async () => {\n const enabled = await getSetting('enabled', false);\n return `Saved setting is ${enabled ? 'on' : 'off'}`;\n },\n }],\n });\n});\n"],"names":[],"mappings":"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","x_google_ignoreList":[0]} \ No newline at end of file diff --git a/examples/framey-settings/src/index.tsx b/examples/framey-settings/src/index.tsx index d7e5c5d..f7f4505 100644 --- a/examples/framey-settings/src/index.tsx +++ b/examples/framey-settings/src/index.tsx @@ -1,5 +1,5 @@ -import {callable, definePlugin, toaster} from '@decky/api'; -import {ButtonItem, PanelSection, PanelSectionRow, ToggleField} from '@decky/ui'; +import {callable, definePlugin, routerHook, toaster} from '@decky/api'; +import {ButtonItem, Navigation, PanelSection, PanelSectionRow, ToggleField, useParams} from '@decky/ui'; import {useEffect, useState} from 'react'; const getSetting = callable<[key: string, fallback: boolean], boolean>('get_setting'); @@ -45,14 +45,27 @@ function Content() { checked={enabled} disabled={busy} onChange={(value: boolean) => {void change(value);}} /> {void refresh();}}>Reload saved setting + Navigation.Navigate('/framey-sample/details/saved-setting')}>About this setting

{message}

; } -export default definePlugin(() => Object.assign({ +function Details() { + const {topic} = useParams<{topic?: string}>(); + return +

Topic: {topic || 'overview'}

+

The toggle is stored by this plugin's Decky backend in the user's settings directory.

+ Navigation.NavigateBack()}>Back to setting +
; +} + +export default definePlugin(() => { + routerHook.addRoute('/framey-sample/details/:topic?', Details); + return Object.assign({ name: 'Framey Settings Sample', icon: , content: , + onDismount: () => routerHook.removeRoute('/framey-sample/details/:topic?'), }, { // Framey additions are ignored by Decky's own menu. Keep the main content // usable there too, while showing authors the extra VR settings view. @@ -64,4 +77,5 @@ export default definePlugin(() => Object.assign({ return `Saved setting is ${enabled ? 'on' : 'off'}`; }, }], -})); + }); +}); diff --git a/tests/page/framey_sample.mjs b/tests/page/framey_sample.mjs index d6104ba..4a03864 100644 --- a/tests/page/framey_sample.mjs +++ b/tests/page/framey_sample.mjs @@ -36,3 +36,9 @@ assert.equal(plugin.frameyQuickActions[0].label,'Read saved setting'); assert.equal(await plugin.frameyQuickActions[0].onClick(),'Saved setting is on'); assert.equal(calls[0].method,'get_setting'); assert.deepEqual(Array.from(calls[0].args),['enabled',false]); +assert.equal(window.DFL.Navigation.Navigate('/framey-sample/details/saved-setting'),true); +assert.equal(window.DFL.useParams().topic,'saved-setting'); +assert.equal(window.DFL.Navigation.Navigate('/library/home'),false); +plugin.onDismount(); +assert.equal(window.DFL.useParams().topic,undefined); +assert.equal(window.DFL.Navigation.Navigate('/framey-sample/details/saved-setting'),false);