Return through local Framey panel routes

This commit is contained in:
SAPHID committed 2026-10-27 00:28:47 +00:00
1 parent efc55919b6
commit 39aa8d56fb
3 files changed
+62 -6

No files matched your search

+4 -1
View File
@@ -76,7 +76,7 @@ none of these frontend controls has passed a worn-headset check yet.
| `localStorage` | Synchronous, per-plugin cache backed by the Frame Control panel origin. Up to 64 keys, 64 KiB per value and 128 KiB total; `clear()` removes only this plugin's cache. Falls back to memory if panel storage is denied. | Parent/iframe round-trip and reopen in the local host test; real headset persistence remains unverified. |
| `SteamClient.Storage.GetJSON` / `SetObject` | Uses the same bounded per-plugin cache; a missing key rejects with `Not found`. It cannot read Steam account or global storage. | Host contract tests and local Chromium settings views; headset persistence after reopen remains unverified. |
| IndexedDB / localForage | IndexedDB is deliberately absent inside the opaque iframe so feature detection chooses the bounded localStorage driver. Plugins requiring IndexedDB directly remain unsupported. | HLTB official frontend in local Chromium: its previous `IDBFactory.open` SecurityError disappeared. No live HLTB data request was run. |
| `routerHook.addRoute` / `removeRoute`, `Navigation.Navigate` / `NavigateBack`, `useParams` | Local routes inside this plugin panel, with named segments such as `/plugin/game/:appid/:view?`; `useParams()` reads the current segment values | Host unit test; VR navigation unverified |
| `routerHook.addRoute` / `removeRoute`, `Navigation.Navigate` / `NavigateBack`, `useParams` | Local routes inside this plugin panel, with named segments such as `/plugin/game/:appid/:view?`; `useParams()` reads the current segment values. Back walks up to 32 local page transitions and skips routes removed since navigation. | Host unit test; VR navigation unverified |
| `routerHook.addPatch`, global components | No-op; cannot patch Steam routes | Not applicable |
| `findModule`, `findModuleChild`, `findModuleExport`, class lookup and module mapping | Searches a finite registry of Framey controls and CSS maps. Unknown Steam modules return `undefined` or an empty mapping. The optional UI-audio export cannot play sound. | Host contract tests; pinned TabMaster 2.15.1 QAM and documentation render in local Chromium. Steam library editing and worn VR remain unverified. |
| `Navigation.CloseSideMenus` | Returns to this plugin's main panel | Pinned HLTB for Deck 2.0.12 shallow settings view in local host test. Live HLTB search remains unverified. |
@@ -90,6 +90,9 @@ Frame Control store; only paths registered by that same plugin can navigate.
Optional route segments must be at the end. Framey accepts local path segments
only; query strings, external URLs, Steam library routes and duplicate parameter
names are rejected. Removing an active route returns to the plugin panel.
`Navigation.NavigateBack()` returns to the preceding local page, then the
plugin root. `Navigation.CloseSideMenus()` returns directly to the root and
clears that panel's history. Neither operation navigates Steam's own UI.
Toasts stay inside the panel, so keep essential errors in the plugin UI too.
Treat module lookup results as Framey-local exports. Do not depend on Steam
webpack IDs, function-source signatures, or Steam-only CSS class names; a
+36
View File
@@ -464,11 +464,47 @@ assert.equal(window.DFL.useParams().view,'art');
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123/art/extra'),false);
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123?admin=1'),false);
assert.equal(window.DFL.Navigation.Navigate('/plugin/game//art'),false);
assert.equal(window.DFL.Navigation.NavigateBack(),true);
assert.equal(window.DFL.useParams().appid,'123');
assert.equal(window.DFL.useParams().view,undefined,'Back returns to the previous local page');
assert.equal(window.DFL.Navigation.NavigateBack(),true);
assert.equal(window.DFL.useParams().appid,undefined,'A second Back returns to the plugin root');
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123'),true);
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123'),true);
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123/art'),true);
assert.equal(window.DFL.Navigation.NavigateBack(),true);
assert.equal(window.DFL.useParams().view,undefined,'Navigating to the same page did not add a history entry');
assert.equal(window.DFL.Navigation.NavigateBack(),true);
assert.equal(window.DFL.useParams().appid,undefined);
assert.throws(()=>api.routerHook.addRoute('/plugin/:id/:id',GameRoute));
assert.throws(()=>api.routerHook.addRoute('/plugin/:id?/art',GameRoute));
api.routerHook.addRoute('/plugin/other',LocalRoute);
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123'),true);
assert.equal(window.DFL.Navigation.Navigate('/plugin/other'),true);
removeGame();
assert.equal(window.DFL.Navigation.NavigateBack(),true);
assert.equal(nodes(window.__surface({plugin}),LocalRoute).length,0,
'Back skips a route that the plugin unregistered');
assert.equal(window.DFL.useParams().appid,undefined);
assert.equal(nodes(window.__surface({plugin}),GameRoute).length,0);
api.routerHook.removeRoute('/plugin/other');
api.routerHook.addRoute('/plugin/first',LocalRoute);
api.routerHook.addRoute('/plugin/second',LocalRoute);
for(let index=0;index<40;index++)
assert.equal(window.DFL.Navigation.Navigate(index%2 ? '/plugin/first' : '/plugin/second'),true);
for(let index=0;index<32;index++)window.DFL.Navigation.NavigateBack();
assert.equal(nodes(window.__surface({plugin}),LocalRoute).length,1);
window.DFL.Navigation.NavigateBack();
assert.equal(nodes(window.__surface({plugin}),LocalRoute).length,0,
'local route history is bounded and falls back to the plugin root');
window.DFL.Navigation.Navigate('/plugin/first');
window.DFL.Navigation.Navigate('/plugin/second');
window.DFL.Navigation.CloseSideMenus();
window.DFL.Navigation.NavigateBack();
assert.equal(nodes(window.__surface({plugin}),LocalRoute).length,0,
'CloseSideMenus clears the local route history');
api.routerHook.removeRoute('/plugin/first');
api.routerHook.removeRoute('/plugin/second');
// Event subscription is acknowledged before a method that immediately emits.
const onMessage=listeners.find(x=>x.type==='message').handler;
+22 -5
View File
@@ -56,6 +56,7 @@
const pending = new Map();
const routes = new Map();
const routeListeners = new Set();
const routeHistory = [];
let currentRoute = null;
let currentParams = {};
let nextId = 0;
@@ -368,15 +369,31 @@
}
return null;
}
function navigate(path) {
if (path !== null && !localPath(path)) return false;
const match = path === null ? null : activeRoute(path);
if (path !== null && !match) return false;
function showRoute(path, match) {
currentRoute = path;
currentParams = match?.params || {};
for (const listener of routeListeners) listener(path);
return true;
}
function navigate(path) {
if (path !== null && !localPath(path)) return false;
const match = path === null ? null : activeRoute(path);
if (path !== null && !match) return false;
if (path === null) routeHistory.length = 0;
else if (path !== currentRoute) {
routeHistory.push(currentRoute);
if (routeHistory.length > 32) routeHistory.shift();
}
return showRoute(path, match);
}
function navigateBack() {
while (routeHistory.length) {
const previous = routeHistory.pop();
const match = previous === null ? null : activeRoute(previous);
if (previous === null || match) return showRoute(previous, match);
}
return navigate(null);
}
const routerHook = {
addRoute(path, component) {
if (!routePattern(path) ||
@@ -491,7 +508,7 @@
joinClassNames:(...names)=>names.filter(Boolean).join(' '),
sleep:ms=>new Promise(resolve=>setTimeout(resolve,Math.max(0,Number(ms)||0))),
staticClasses: {Title:'framey-title'},
Navigation: {Navigate:navigate, NavigateBack:()=>navigate(null), CloseSideMenus:()=>navigate(null),
Navigation: {Navigate:navigate, NavigateBack:navigateBack, CloseSideMenus:()=>navigate(null),
NavigateToExternalWeb: url => { if (/^https:\/\//.test(url)) window.open(url, '_blank', 'noopener'); }},
showModal, showContextMenu:showModal,
// Steam app, library, and store patches have no target inside this panel.