From 33fdd9d2968614a8cf1196523dfbdbc7ee05dc1e Mon Sep 17 00:00:00 2001 From: SAPHID Date: Sun, 25 Oct 2026 21:23:09 +0000 Subject: [PATCH] Show Framey settings and quick action in author sample --- examples/framey-settings/README.md | 10 ++++++++-- examples/framey-settings/dist/index.js | 13 ++++++++++++- examples/framey-settings/dist/index.js.map | 2 +- examples/framey-settings/src/index.tsx | 13 ++++++++++++- tests/page/framey_sample.mjs | 15 ++++++++++++++- 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/examples/framey-settings/README.md b/examples/framey-settings/README.md index 6bcf845..55c9228 100644 --- a/examples/framey-settings/README.md +++ b/examples/framey-settings/README.md @@ -5,7 +5,12 @@ This is a small Decky API v2 plugin for authors. Its panel uses the official backend method, stores one boolean under `decky.decky_SETTINGS_DIR`, and reads it back when the panel opens. The backend rejects other keys and values. The frontend also shows a local toast after a successful save. A -missing, damaged, or old non-boolean value reads as the caller's default; +Framey settings tab exposes the same toggle, and a read-only quick action +fetches the saved value without changing it. These two fields are Framey +additions; Decky's own menu still uses the main panel content. The quick +action calls the backend, so it reads the persisted setting rather than a +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. ## Build @@ -29,7 +34,8 @@ 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. The fake Frame store flow is exercised by +against Framey's actual host script, including the quick action's scoped +backend call. 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 565ec66..714534c 100644 --- a/examples/framey-settings/dist/index.js +++ b/examples/framey-settings/dist/index.js @@ -61,10 +61,21 @@ function Content() { } 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 }) })] }); } -var index = definePlugin(() => ({ +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'}`; + }, + }], })); export { index as default }; diff --git a/examples/framey-settings/dist/index.js.map b/examples/framey-settings/dist/index.js.map index 469e504..cf55c3c 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(() => ({\n name: 'Framey Settings Sample',\n icon: ⚙,\n content: ,\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, 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 diff --git a/examples/framey-settings/src/index.tsx b/examples/framey-settings/src/index.tsx index b93a1f0..d7e5c5d 100644 --- a/examples/framey-settings/src/index.tsx +++ b/examples/framey-settings/src/index.tsx @@ -49,8 +49,19 @@ function Content() { ; } -export default definePlugin(() => ({ +export default definePlugin(() => Object.assign({ name: 'Framey Settings Sample', icon: , 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: , + frameyQuickActions: [{ + label: 'Read saved setting', + onClick: async () => { + const enabled = await getSetting('enabled', false); + return `Saved setting is ${enabled ? 'on' : 'off'}`; + }, + }], })); diff --git a/tests/page/framey_sample.mjs b/tests/page/framey_sample.mjs index e54e2c3..d6104ba 100644 --- a/tests/page/framey_sample.mjs +++ b/tests/page/framey_sample.mjs @@ -10,8 +10,16 @@ const React={Fragment:Symbol('fragment'),Component:class {}, createElement:(type,props,...children)=>({type,props:props||{},children}), useState:value=>[value,()=>{}],useEffect(){},isValidElement:value=>!!value?.type}; const window={React,ReactDOM:{createRoot:()=>({render(){}})},addEventListener(){}}; +const listeners=[]; +const calls=[]; +const parent={postMessage(message){ + if(message.type!=='framey:call')return; + calls.push(message); + listeners.find(x=>x.type==='message').handler({source:parent, + data:{type:'framey:reply',id:message.id,result:true}}); +}}; const context=vm.createContext({window,document:{getElementById:()=>({})}, - navigator:{language:'en'},parent:{postMessage(){}},addEventListener(){}, + navigator:{language:'en'},parent,addEventListener:(type,handler)=>listeners.push({type,handler}), setTimeout:()=>1,clearTimeout(){}}); vm.runInContext(host,context); Object.assign(context,{SP_REACT:window.SP_REACT,SP_JSX:window.SP_JSX,DFL:window.DFL}); @@ -23,3 +31,8 @@ const content=plugin.content.type(); assert.equal(content.type,window.DFL.PanelSection); assert.equal(content.props.title,'Framey settings sample'); assert.equal(content.children[0][0].type,window.DFL.PanelSectionRow); +assert.equal(plugin.settings.type,plugin.content.type); +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]);