mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-26 13:00:33 +01:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37e25e62ac | ||
|
|
d1e526cb4b | ||
|
|
8230fdb19d | ||
|
|
2937a12864 | ||
|
|
71fe4a3ebb | ||
|
|
42f3b2e1e7 | ||
|
|
ca4c370b33 | ||
|
|
c50f96510f | ||
|
|
43e801091a | ||
|
|
b4e632b218 | ||
|
|
83b8d13846 | ||
|
|
340ca88359 | ||
|
|
e62589417b | ||
|
|
93da4e466f |
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"version": "0.4.42",
|
||||
"version": "0.4.44",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frame-control",
|
||||
"version": "0.4.42",
|
||||
"version": "0.4.44",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "^44.4.5",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"productName": "Frame Control",
|
||||
"version": "0.4.42",
|
||||
"version": "0.4.44",
|
||||
"description": "Desktop app for managing a Valve Steam Frame over SSH",
|
||||
"private": true,
|
||||
"main": "main.js",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# v0.4.43 candidate Frame gate, 26 October 2026
|
||||
|
||||
The combined code head `232b253e7a9400aa9829f9fea6a5d2011dd2a8f3` (PR #309 after merging main with PR #308) ran `scripts/frame-smoke.sh` under its own `/tmp/frame-test.lock` owner at 07:57 UTC. Its locked preflight found SteamVR activity 3 (standby), proximity 6.25 and battery 99%. On Frame OS 0.5.6 build `20261022.6153909`, it recorded **12 passed / 1 skipped / 0 failed**. The ARM64 and Proton executables launched; x86_64 launch was skipped because Steam Linux Runtime 4 is absent. It removed its test titles, exited 0 and released its lock. The private detailed result is `/tmp/frame-smoke-pr309/2026-10-26T075752Z-20261022.6153909.json` on the test host. A fresh, separately owned read-only lock then found activity 3 and battery 99%, and was released. PR #310 subsequently changed only the Windows wireless CI smoke test; the release branch merged that test change and bumped the app version, with no further headset runtime code change after this Frame smoke.
|
||||
|
||||
PR #308's panel code commit `d5e62f643d462b7070f27c2c47b11b4aa9982094` previously passed the temporary Framey sample `scripts/framey-vr-check.py --exercise` machine check, with cleanup and power restoration; see [Web Browser follow-up](framey-web-browser-2026-10-26.md). That test used synthetic OpenVR input and a repository sample, not the official Web Browser plugin or a person wearing the headset. No wake or VR input exercise ran on this combined code head because wearer confirmation was absent. The current smoke made no worn visual, physical-controller, audible-output or official-plugin VR claim.
|
||||
|
||||
This is candidate evidence, not a published-release result. Publication still needs all exact-head PR and release checks, ten tagged installers with matching `SHA256SUMS`, package validation, the publisher dry run, and public updater readback. The exact-version official Decky catalog in this candidate is **4 works / 49 partial / 52 broken / 11 N/A**. Volume Boost's works claim covers tested muted sink controls; Junk-Store remains partial, and Web Browser remains broken.
|
||||
@@ -0,0 +1,79 @@
|
||||
# v0.4.43 tagged release, 26 October 2026
|
||||
|
||||
The annotated `v0.4.43` tag dereferences to main merge commit
|
||||
`b4e632b218eed0761db488061e3fe6c503b06a0a` from PR #311. Both the app
|
||||
and lockfile report 0.4.43. The [tagged release workflow](https://github.com/saphid/frame-control/actions/runs/40292211650)
|
||||
passed its draft, Linux, Windows, Apple Silicon Mac, Intel Mac and checksum jobs.
|
||||
The [tagged main checks](https://github.com/saphid/frame-control/actions/runs/40292128084)
|
||||
passed all seven jobs, including iOS and fake-Frame end-to-end tests. The Linux
|
||||
builder launched its AppImage under isolated Xvfb; that does not prove a physical
|
||||
display, FUSE mount or headset connection.
|
||||
|
||||
## Frame and package gates
|
||||
|
||||
The [combined-head candidate](v0443-candidate-2026-10-26.md) passed the real
|
||||
Frame smoke before tagging: 12 passed, one optional x86 launch skipped, zero
|
||||
failed. The only later changes before the tag were the Windows wireless CI
|
||||
test and version metadata. Its locked preflight found SteamVR activity 3
|
||||
(standby), proximity 6.25 and battery 99%.
|
||||
|
||||
The draft had ten installers plus `SHA256SUMS`. Each installer had a GitHub
|
||||
SHA-256 digest. A fresh downloaded checksum list contained exactly those ten
|
||||
names and digests; its own downloaded bytes matched GitHub's digest
|
||||
`57f211d8bbf76db8aa96cc4a5ae31410ed7e3075ce2e8c0c17394feb9426d01c`.
|
||||
`scripts/publish-release.sh --dry-run v0.4.43` passed the exact remote tag,
|
||||
installer and checksum gates after the notes were attached.
|
||||
|
||||
Freshly downloaded packages passed these independent checks:
|
||||
|
||||
| Package | Result |
|
||||
| --- | --- |
|
||||
| Linux x86_64 AppImage | SHA-256 `d5e6c2272a14a2aa77aa932db43f3ce887e09eb78c87bf2de410832b5df4827d`; launched under isolated Xvfb, bundled server returned HTTP 200 with `FrameControl/1`, and stopped on exit. |
|
||||
| Mac arm64 ZIP | SHA-256 `c6c4a9c74644c3479d1af112ec60d4c2770201fc366f21bca23a4e2fcc86194a`; ZIP CRC passed, bundle version 0.4.43, main Mach-O CPU arm64. |
|
||||
| Mac x64 ZIP | SHA-256 `58dfc88bb71f1733add7477f9cd888417e4827e608fe910017837ba466a72a40`; ZIP CRC passed, bundle version 0.4.43, main Mach-O CPU x86_64. |
|
||||
| Windows x64 Setup | SHA-256 `8c17522b0e96d54f53949f4df56a3e1ec1fe2da8df595482a358a3d014b6f1a9`; PE/Nullsoft installer format. The tagged Windows CI also installed and checked its packaged files and Python libraries. |
|
||||
| Linux arm64 DEB | SHA-256 `c00f141b6b63344c629bbd659a366a74705c52061d83572ae235a75ac6618df0`; package version 0.4.43 and architecture arm64. |
|
||||
|
||||
Tagged Mac and Windows packages were not launched on physical machines in this
|
||||
release gate. Earlier Intel Mac acceptance covers an older version, not these
|
||||
tagged bytes.
|
||||
|
||||
## Publication and later main
|
||||
|
||||
`scripts/publish-release.sh v0.4.43` published
|
||||
[v0.4.43](https://github.com/saphid/frame-control/releases/tag/v0.4.43) at
|
||||
2026-10-26 08:39:09 UTC. GitHub's latest-release API returned `draft=false`,
|
||||
`prerelease=false`, the expected tag, ten installers, `SHA256SUMS` and
|
||||
`update.json`. A fresh public latest-download of `update.json` parsed as
|
||||
version 0.4.43 with the stable release page, ten hashed assets and the worn-VR
|
||||
limits in its notes. The release catalog is **4 works / 49 partial / 52 broken /
|
||||
11 N/A**. Volume Boost's works label covers tested muted sink controls only.
|
||||
|
||||
PRs #312 and #313 merged **after** the tag and are absent from v0.4.43. On their
|
||||
combined main commit `c50f96510fdb42a2aee227436a04c0cfe8817919`, a second
|
||||
locked `scripts/frame-smoke.sh` run at 08:37 UTC found activity 3, proximity 6.25
|
||||
and battery 99%, then passed 12 checks with the same optional x86 launch skip
|
||||
and zero failures. Test title and process cleanup passed; the command exited 0
|
||||
and released only its owned lock. The private result is
|
||||
`tests/smoke/results/2026-10-26T083719Z-20261022.6153909.json` in the test
|
||||
checkout. This post-tag smoke does not test the new Framey scroll buttons or
|
||||
Junk-Store's deployed headset panel.
|
||||
|
||||
The guarded `scripts/framey-vr-check.py --exercise` then ran on the same code
|
||||
commit, using the existing direct `frame` SSH alias and its own shared-lock owner.
|
||||
It began at 08:45 UTC in SteamVR standby (activity 3, proximity 6.3125,
|
||||
battery 99%). The temporary sample panel passed setting save/reopen, quick
|
||||
action, slider and text checks. Synthetic native OpenVR clicks on the visible
|
||||
**Scroll plugin down/up** buttons moved the 160-pixel test region 0 → 120 → 0;
|
||||
the compositor screenshot hash changed. Native synthetic wheel attempts still
|
||||
produced no wheel event or movement. The private report is
|
||||
`/tmp/framey-scroll-postmerge-direct-20261026/result.json` on the test host.
|
||||
The command exited 0, removed its test services and power watchdog, read back
|
||||
the original `false` / `5` power settings, found battery 99%, and released its
|
||||
owned lock with no cleanup errors. It did not exercise an official plugin or a
|
||||
physical controller.
|
||||
|
||||
No wearer confirmation was available, so no worn check, wake or input exercise
|
||||
ran. Physical controller use, optical readability, official-plugin VR panels,
|
||||
audible output and VR keyboard remain unverified. The hourly worn-check schedule
|
||||
stays enabled.
|
||||
@@ -0,0 +1,56 @@
|
||||
# v0.4.44 candidate gate, 26 October 2026
|
||||
|
||||
This candidate starts from main commit
|
||||
`c50f96510fdb42a2aee227436a04c0cfe8817919`, which contains PR #312's
|
||||
Framey scroll buttons and PR #313's read-only installed-app metadata projection.
|
||||
It also carries the documentation-only v0.4.43 release evidence from PR #314.
|
||||
The app and lockfile are bumped to 0.4.44. No compatibility label changes:
|
||||
the exact-version official Decky catalog remains **4 works / 49 partial /
|
||||
52 broken / 11 N/A**. Junk-Store's About and Dependencies pages now render in
|
||||
the local Chromium test with its actual ARM64 backend; the plugin remains partial.
|
||||
|
||||
## Real Frame checks on the combined code
|
||||
|
||||
At 08:37 UTC, `scripts/frame-smoke.sh` ran on that main commit under its own
|
||||
`/tmp/frame-test.lock` owner. Locked preflight found SteamVR activity 3
|
||||
(standby), proximity 6.25 and battery 99%. On Frame OS 0.5.6 build
|
||||
`20261022.6153909`, ARM64 and Proton executables launched; the optional x86_64
|
||||
launch was skipped because Steam Linux Runtime 4 is absent. The result was
|
||||
**12 passed / 1 skipped / 0 failed**. It removed its test titles, exited 0 and
|
||||
released its owned lock. The private result is
|
||||
`tests/smoke/results/2026-10-26T083719Z-20261022.6153909.json` on the test
|
||||
host.
|
||||
|
||||
At 08:45 UTC, `scripts/framey-vr-check.py --exercise` ran the same code under
|
||||
a separate unique shared-lock owner, beginning in standby (activity 3,
|
||||
proximity 6.3125, battery 99%). Synthetic native OpenVR clicks on the new
|
||||
**Scroll plugin down/up** buttons moved a temporary sample region 0 → 120 → 0.
|
||||
The sample setting save/reopen, quick action, slider, text and compositor-change
|
||||
checks passed. Native synthetic wheel attempts still produced no event or
|
||||
movement. Cleanup removed test services and its power watchdog, restored the
|
||||
original `false` / `5` power values, read battery 99%, and released its lock
|
||||
with no errors. The private report is
|
||||
`/tmp/framey-scroll-postmerge-direct-20261026/result.json` on the test host.
|
||||
This is an unworn sample panel check. It does not prove physical controller
|
||||
scrolling, optical readability or an official-plugin panel in VR.
|
||||
|
||||
## Publication boundary
|
||||
|
||||
The version-bumped release branch also ran `scripts/frame-smoke.sh` after its
|
||||
normal merge of PR #314's documentation. At 08:59 UTC, its locked preflight
|
||||
found activity 3, proximity 6.3125 and battery 97%. The result was again
|
||||
**12 passed / 1 skipped / 0 failed** on the same Frame OS build; ARM64 and
|
||||
Proton launches passed, while the optional x86 launch was skipped for the
|
||||
missing Steam Linux Runtime 4. Owned test title/process cleanup passed, the
|
||||
command exited 0, and its lock was released. The private result is
|
||||
`tests/smoke/results/2026-10-26T085951Z-20261022.6153909.json` on the test
|
||||
host. This evidence-only paragraph is the sole change after that smoke.
|
||||
|
||||
PR #313's own actual ARM64 backend/library test is documented in
|
||||
[installed-context evidence](framey-installed-context-2026-10-26.md). Its
|
||||
dependency installation, login, game operations, RSS and optional websocket
|
||||
remain unverified. The candidate still needs exact-head CI, tagged builds,
|
||||
ten installer digests with `SHA256SUMS`, downloaded package checks, publisher
|
||||
dry run and public updater readback before publication. A worn check requires
|
||||
activity, proximity and reliable wearer confirmation; none was available, so
|
||||
the hourly schedule stays enabled.
|
||||
@@ -63,6 +63,7 @@ none of these frontend controls has passed a worn-headset check yet.
|
||||
| Official `@decky/rollup` JSX (`SP_JSX.jsx`, `jsxs`, `Fragment`) | Creates React elements in the isolated panel | Sample bundle smoke test; host unit test |
|
||||
| `PanelSection`, rows, buttons, toggles, dropdowns | Local touch-sized controls; disabled controls stay inert during a backend call | Local browser and host unit test |
|
||||
| `TextField`, `SliderField`, grouped `Dropdown` options | Native text, range and select controls in the panel; disabled state, numeric slider callback and basic input types | Host unit test; worn VR use unverified |
|
||||
| Scrollable panel content | Visible Scroll plugin up/down buttons move the last region reached by a trusted pointer, or the document if that region disappears. Plugins may still use ordinary CSS overflow containers. | Parent/iframe message tests; physical controller scroll remains unverified |
|
||||
| `Tabs`, `SidebarNavigation`, spinners, dialog/menu wrappers, progress bars | Local tab navigation and ordinary HTML controls; no Steam navigation or controller focus tree | Host tests and catalog frontend smoke checks in local Chromium |
|
||||
| `SteamUIStore` / `DFL.Router.RunningApps`, `MainRunningApp` | Bounded detached Steam CEF reads while the panel is open; refresh about every two seconds; no MobX observation. Failed reads clear stale context. | Locked real Frame empty-list reads; nonempty local Chromium fixtures. Real running-game selection unverified. |
|
||||
| `GameSessions.RegisterForAppLifetimeNotifications` | Actual Steam callbacks, bounded queue and disposable `.unregister()` handle; no events inferred from snapshot changes; lease cleanup after lost panels | Real Frame registration/poll/close; launch/exit, retry, expiry and duplicate tests use explicit fixtures. Real game transitions unverified. |
|
||||
|
||||
@@ -16,6 +16,10 @@ plugins → Open official store**. Search the catalogue, confirm an install, and
|
||||
choose **Open panel** from the installed plugin list. The panel contains the
|
||||
plugin's own controls and settings. Update and Remove affect only plugin
|
||||
folders that Frame Control's store manager installed.
|
||||
Use **Scroll plugin up** and **Scroll plugin down** above an open panel when
|
||||
wheel scrolling is unavailable. Point at a scrollable part of the plugin first
|
||||
to move that section; otherwise the buttons move the panel document. Each
|
||||
press moves about three quarters of the visible section and stops at its edge.
|
||||
Removing a plugin keeps its Decky settings. Removing the Frame Control headset
|
||||
panel or Decky backend keeps plugin folders until each is removed separately.
|
||||
|
||||
@@ -82,7 +86,11 @@ record the resulting TabMaster documentation support and remaining blockers.
|
||||
The host currently supplies sections, rows, buttons, toggles, text fields,
|
||||
sliders, grouped dropdowns,
|
||||
scroll containers, a modal, locale information, local panel routes and toasts,
|
||||
and backend calls for the plugin that owns the panel. Steam library/store page
|
||||
and backend calls for the plugin that owns the panel. The headset's scroll
|
||||
buttons send only a direction to the isolated iframe. They target the last
|
||||
scrollable region reached by a trusted pointer move and fall back to the iframe
|
||||
document when that region disappears. A result message reports whether content
|
||||
moved; this bridge has no backend or Steam privilege. Steam library/store page
|
||||
patches, global Steam UI components, file pickers, and privileged root methods
|
||||
have no VR target. A plugin that depends on them can render only part of its UI
|
||||
or show an incompatibility message. A Decky plugin's Python backend still runs as
|
||||
|
||||
@@ -59,6 +59,10 @@ pointer over it, then attempts OpenVR smooth and discrete scroll events. It
|
||||
reports region geometry, trusted pointer events, wheel events and `scrollTop`.
|
||||
No wheel event or offset change means this synthetic injection path remains
|
||||
unverified; it does not establish whether a physical controller can scroll.
|
||||
The exercise then clicks the visible **Scroll plugin down/up** buttons through
|
||||
the same native OpenVR pointer transport and requires the test region to move
|
||||
down and return to the top. This proves the button path only; it does not
|
||||
identify a physical controller or repair native wheel delivery.
|
||||
Temporary controls exercise the shipped
|
||||
`DFL.SliderField` and `DFL.TextField`; their values remain UI-only. Text uses
|
||||
CDP `Input.insertText` after OpenVR pointer focus. This is synthetic input,
|
||||
|
||||
@@ -71,10 +71,26 @@ def exercise(profile, plugin_id, folder):
|
||||
if not hit:
|
||||
raise RuntimeError('Native click hit another element: ' + str(panel.eval('({tag:__vrLastTarget?.tagName,text:__vrLastTarget?.textContent?.slice(0,100)})')))
|
||||
|
||||
def click_parent(selector):
|
||||
parent.eval(f'document.querySelector({json.dumps(selector)}).scrollIntoView({{block:"center"}})')
|
||||
time.sleep(.3)
|
||||
pos = parent.eval(f'''(()=>{{let r=document.querySelector({json.dumps(selector)}).getBoundingClientRect();
|
||||
return {{x:r.x+r.width/2,y:r.y+r.height/2,w:innerWidth,h:innerHeight}}}})()''')
|
||||
sx, sy = vr.snapshot()['input']['scale']
|
||||
x, y = pos['x'] * sx / pos['w'], sy - pos['y'] * sy / pos['h']
|
||||
count = parent.eval('window.__vrParentClickCount')
|
||||
vr.post_mouse(300, x, y)
|
||||
vr.post_mouse(301, x, y, 1)
|
||||
vr.post_mouse(302, x, y, 1)
|
||||
wait(lambda:parent.eval('window.__vrParentClickCount'), lambda n:n > count, seconds=3)
|
||||
if not parent.eval(f'!!window.__vrParentLastTarget?.closest({json.dumps(selector)})'):
|
||||
raise RuntimeError('Native click missed parent control ' + selector)
|
||||
|
||||
try:
|
||||
parent.eval(f'(async()=>{{await refreshDecky();openFrameyPanel({plugin_id},"Framey VR Check");return true}})()')
|
||||
wait(lambda:parent.eval('byId("frameyPanelStatus").innerText'), lambda s:s == 'Plugin panel ready')
|
||||
panel = connect_panel()
|
||||
parent.eval('window.__vrParentClickCount=0;addEventListener("pointerup",e=>{window.__vrParentClickCount++;window.__vrParentLastTarget=e.target;});true')
|
||||
initial = wait(text, lambda s:'Saved setting: off' in s or 'Could not read setting:' in s)
|
||||
if 'Could not read setting:' in initial: raise RuntimeError(initial)
|
||||
result['initialText'] = initial
|
||||
@@ -149,6 +165,13 @@ def exercise(profile, plugin_id, folder):
|
||||
result['scrollAfter'] = result['scrollAttempts'][-1]['offset']
|
||||
result['scrollEvents'] = panel.eval('__vrWheelEvents')
|
||||
result['nativeScrollMoved'] = result['scrollAfter'] != result['scrollBefore']
|
||||
click_parent('#frameyScrollDown')
|
||||
result['buttonScrollAfterDown'] = wait(
|
||||
lambda:panel.eval('document.getElementById("vr-scroll-probe").scrollTop'), lambda n:n > 0)
|
||||
click_parent('#frameyScrollUp')
|
||||
result['buttonScrollAfterUp'] = wait(
|
||||
lambda:panel.eval('document.getElementById("vr-scroll-probe").scrollTop'), lambda n:n == 0)
|
||||
result['buttonScrollMoved'] = result['buttonScrollAfterDown'] > 0 and result['buttonScrollAfterUp'] == 0
|
||||
panel.eval('document.getElementById("vr-scroll-probe").remove()')
|
||||
panel.eval('document.getElementById("vr-controls").remove()')
|
||||
# Scroll saved status into the visible iframe for compositor evidence.
|
||||
@@ -156,7 +179,7 @@ def exercise(profile, plugin_id, folder):
|
||||
result['finalOverlay'] = vr.snapshot()
|
||||
result['passed'] = (result['trustedOverlayPointer'] and result['diskSetting'] == {'enabled':True}
|
||||
and result['reopenedChecked'] and result['sliderValue'] != '10'
|
||||
and result['textValue'] == 'framey-ci')
|
||||
and result['textValue'] == 'framey-ci' and result['buttonScrollMoved'])
|
||||
return result
|
||||
except Exception as exc:
|
||||
result['error'] = str(exc)
|
||||
|
||||
@@ -21,7 +21,8 @@ const window={React,ReactDOM:{createRoot:node=>{
|
||||
const listeners=[];const messages=[];const parent={postMessage:message=>messages.push(message)};
|
||||
const context=vm.createContext({window,document:{getElementById:id=>{
|
||||
if(!elements.has(id))elements.set(id,new Element());return elements.get(id);},createElement:()=>new Element()},
|
||||
navigator:{language:'en'}, parent,DOMException,addEventListener:(type,handler)=>listeners.push({type,handler}),
|
||||
navigator:{language:'en'}, parent,DOMException,getComputedStyle:node=>({overflowY:node.overflowY||'visible'}),
|
||||
addEventListener:(type,handler)=>listeners.push({type,handler}),
|
||||
setTimeout:()=>1,clearTimeout(){}});
|
||||
vm.runInContext(testSource,context);
|
||||
// No Steam React tree exists inside the isolated Framey iframe.
|
||||
@@ -387,6 +388,35 @@ assert.equal(nodes(window.__surface({plugin}),GameRoute).length,0);
|
||||
|
||||
// Event subscription is acknowledged before a method that immediately emits.
|
||||
const onMessage=listeners.find(x=>x.type==='message').handler;
|
||||
// The headset's two scroll buttons target the last trusted pointer's nested
|
||||
// scroll region, with a document fallback after that region is removed.
|
||||
const documentScroll={scrollTop:0,clientHeight:500,scrollHeight:900,isConnected:true,overflowY:'auto'};
|
||||
const nestedScroll={scrollTop:0,clientHeight:160,scrollHeight:716,isConnected:true,
|
||||
overflowY:'scroll',parentElement:documentScroll};
|
||||
const child={scrollTop:0,clientHeight:0,scrollHeight:0,parentElement:nestedScroll};
|
||||
context.document.scrollingElement=documentScroll;
|
||||
const pointerMove=listeners.find(x=>x.type==='pointermove').handler;
|
||||
pointerMove({isTrusted:true,target:child});
|
||||
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'down'}});
|
||||
assert.equal(nestedScroll.scrollTop,120);
|
||||
assert.equal(messages.at(-1).moved,true);
|
||||
pointerMove({isTrusted:false,target:documentScroll});
|
||||
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'up'}});
|
||||
assert.equal(nestedScroll.scrollTop,0,'Untrusted movement cannot retarget scroll');
|
||||
nestedScroll.scrollTop=550;
|
||||
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'down'}});
|
||||
assert.equal(nestedScroll.scrollTop,556,'Scroll stops at the content end');
|
||||
assert.equal(messages.at(-1).moved,true);
|
||||
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'down'}});
|
||||
assert.equal(messages.at(-1).moved,false);
|
||||
const scrollReplies=messages.length;
|
||||
onMessage({source:parent,data:{type:'framey:scroll',session:'stale',direction:'up'}});
|
||||
onMessage({source:{},data:{type:'framey:scroll',session:'',direction:'up'}});
|
||||
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'sideways'}});
|
||||
assert.equal(messages.length,scrollReplies);
|
||||
nestedScroll.isConnected=false;
|
||||
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'down'}});
|
||||
assert.equal(documentScroll.scrollTop,375);
|
||||
const eventValues=[];
|
||||
const callback=(...args)=>eventValues.push(args);
|
||||
assert.equal(api.addEventListener('timer:tick',callback),callback);
|
||||
|
||||
@@ -69,7 +69,6 @@ vm.runInContext('openFrameyPanel(96,"Timer")',context);
|
||||
const priorReplies=replies.length;
|
||||
requests.at(-1).resolve({ok:true,json:async()=>({result:'AU'})});
|
||||
await staleRead;assert.equal(replies.length,priorReplies);
|
||||
|
||||
// Running reads are tied to the current iframe and session, with no method path.
|
||||
session=vm.runInContext('frameySession',context);
|
||||
const running={type:'framey:running',id:20,session,args:[0]};
|
||||
@@ -96,3 +95,19 @@ await finalRunning;
|
||||
await send({type:'framey:running:close',session});
|
||||
assert.equal(requests.at(-1).body.receipt,'a'.repeat(64));
|
||||
assert.equal(requests.at(-1).body.active,false);
|
||||
|
||||
const scrollSession=vm.runInContext('frameySession',context);
|
||||
el('frameyScrollDown').onclick();
|
||||
assert.equal(replies.at(-1).type,'framey:scroll');
|
||||
assert.equal(replies.at(-1).session,scrollSession);
|
||||
assert.equal(replies.at(-1).direction,'down');
|
||||
await send({type:'framey:scroll:result',session:'old',direction:'down',moved:true});
|
||||
assert.equal(el('frameyPanelStatus').textContent,'Loading plugin panel…');
|
||||
await send({type:'framey:scroll:result',session:scrollSession,direction:'down',moved:true});
|
||||
assert.equal(el('frameyPanelStatus').textContent,'Scrolled plugin down');
|
||||
await send({type:'framey:scroll:result',session:scrollSession,direction:'down',moved:false});
|
||||
assert.equal(el('frameyPanelStatus').textContent,'No more plugin content in that direction');
|
||||
el('frameyClose').onclick();
|
||||
const replyCount=replies.length;
|
||||
el('frameyScrollUp').onclick();
|
||||
assert.equal(replies.length,replyCount);
|
||||
@@ -34,6 +34,25 @@
|
||||
const root = document.getElementById('framey-root');
|
||||
const error = document.getElementById('framey-error');
|
||||
const toastRoot = document.getElementById('framey-toasts');
|
||||
let lastScrollTarget = null;
|
||||
function scrollTarget(element) {
|
||||
for (let node = element; node && node !== document; node = node.parentElement) {
|
||||
if (node.scrollHeight <= node.clientHeight + 1) continue;
|
||||
if (node === document.scrollingElement || /^(auto|scroll|overlay)$/.test(getComputedStyle(node).overflowY))
|
||||
return node;
|
||||
}
|
||||
return document.scrollingElement;
|
||||
}
|
||||
function scrollPlugin(direction) {
|
||||
if (direction !== 'up' && direction !== 'down') return false;
|
||||
const target = lastScrollTarget?.isConnected ? lastScrollTarget : document.scrollingElement;
|
||||
if (!target) return false;
|
||||
const before = target.scrollTop;
|
||||
const maximum = Math.max(0, target.scrollHeight - target.clientHeight);
|
||||
const step = Math.max(120, Math.round(target.clientHeight * 0.75));
|
||||
target.scrollTop = Math.max(0, Math.min(maximum, before + (direction === 'down' ? step : -step)));
|
||||
return target.scrollTop !== before;
|
||||
}
|
||||
const pending = new Map();
|
||||
const routes = new Map();
|
||||
const routeListeners = new Set();
|
||||
@@ -139,6 +158,14 @@
|
||||
return request(method, args, legacy);
|
||||
}
|
||||
addEventListener('message', event => {
|
||||
if (event.source === parent && event.data?.type === 'framey:scroll' &&
|
||||
event.data.session === cacheSession) {
|
||||
if (event.data.direction !== 'up' && event.data.direction !== 'down') return;
|
||||
const moved = scrollPlugin(event.data.direction);
|
||||
parent.postMessage({type:'framey:scroll:result', session:cacheSession,
|
||||
direction:event.data.direction, moved}, '*');
|
||||
return;
|
||||
}
|
||||
if (event.source === parent && event.data?.type === 'framey:cache:seed' &&
|
||||
event.data.session === cacheSession && acceptSeed) {
|
||||
const entries = event.data.entries;
|
||||
@@ -165,6 +192,9 @@
|
||||
addEventListener('pointerup', event => {
|
||||
if (event.isTrusted) parent.postMessage({type:'framey:input'}, '*');
|
||||
}, {passive:true});
|
||||
addEventListener('pointermove', event => {
|
||||
if (event.isTrusted) lastScrollTarget = scrollTarget(event.target);
|
||||
}, {passive:true});
|
||||
|
||||
const passthrough = tag => ({children, ...props}) => R.createElement(tag, props, children);
|
||||
const Field = ({label, description, children}) => R.createElement('label', null,
|
||||
|
||||
+14
-1
@@ -53,7 +53,8 @@ iframe{display:block;width:100%;height:560px;background:#0d1925;border:1px solid
|
||||
<label>Show <select id="deckyStoreFilter"><option value="all">All plugins</option><option value="compatible">No known install blocker</option><option value="installed">Installed plugins</option></select></label></div>
|
||||
<p id="deckyStoreSummary" role="status" aria-live="polite"></p><div id="deckyStoreList" class="stack"></div>
|
||||
<div class="row" style="margin-top:14px"><button id="deckyStorePrevious" class="secondary">Previous</button><span id="deckyStorePage"></span><button id="deckyStoreNext" class="secondary">Next</button></div></div>
|
||||
<div id="frameyPanel" hidden><div class="row" style="margin-top:16px"><strong id="frameyTitle"></strong><button id="frameyClose" class="secondary">Close plugin panel</button></div>
|
||||
<div id="frameyPanel" hidden><div class="row" style="margin-top:16px"><strong id="frameyTitle"></strong><button id="frameyClose" class="secondary">Close plugin panel</button>
|
||||
<button id="frameyScrollUp" class="secondary">Scroll plugin up</button><button id="frameyScrollDown" class="secondary">Scroll plugin down</button></div>
|
||||
<p id="frameyCompatibility"></p>
|
||||
<p id="frameyPanelStatus" role="status">Loading plugin panel…</p>
|
||||
<iframe id="frameyFrame" title="Framey Decky plugin panel" sandbox="allow-scripts allow-popups" referrerpolicy="no-referrer"></iframe></div>
|
||||
@@ -251,6 +252,12 @@ function closeFrameyEvents(){
|
||||
function closeFrameyPanel(){closeFrameyEvents();activeFrameyId=null;activeFrameyReceipt=null;frameySession=null;
|
||||
byId('frameyFrame').removeAttribute('src');byId('frameyPanel').hidden=true;}
|
||||
byId('frameyClose').onclick=closeFrameyPanel;
|
||||
function scrollFramey(direction){
|
||||
if(activeFrameyId===null||!frameySession||!['up','down'].includes(direction))return;
|
||||
byId('frameyFrame').contentWindow?.postMessage({type:'framey:scroll',session:frameySession,direction},'*');
|
||||
}
|
||||
byId('frameyScrollUp').onclick=()=>scrollFramey('up');
|
||||
byId('frameyScrollDown').onclick=()=>scrollFramey('down');
|
||||
function frameyCacheKey(id){return `framey-plugin-cache-v1:${id}`;}
|
||||
function frameyCacheRead(id){
|
||||
try{const data=JSON.parse(localStorage.getItem(frameyCacheKey(id))||'{}');
|
||||
@@ -271,6 +278,12 @@ function frameyCacheSave(id,entries){
|
||||
window.addEventListener('message',async event=>{
|
||||
if(activeFrameyId===null||event.source!==byId('frameyFrame').contentWindow)return;
|
||||
const req=event.data;if(!req||typeof req!=='object')return;
|
||||
if(req.type==='framey:scroll:result'&&req.session===frameySession){
|
||||
if(['up','down'].includes(req.direction)&&typeof req.moved==='boolean')
|
||||
byId('frameyPanelStatus').textContent=req.moved?'Scrolled plugin '+req.direction:
|
||||
'No more plugin content in that direction';
|
||||
return;
|
||||
}
|
||||
if(req.type==='framey:cache:init'&&req.session===frameySession){
|
||||
event.source.postMessage({type:'framey:cache:seed',session:frameySession,
|
||||
entries:frameyCacheRead(activeFrameyId)},'*');return;
|
||||
|
||||
Reference in new issue
Block a user