From c7a17478a303acd375579cea57f33fd9460b258f Mon Sep 17 00:00:00 2001 From: Alex Southwell Date: Sun, 25 Oct 2026 07:16:05 +0000 Subject: [PATCH 1/3] Host Decky plugin panels in Frame Control VR (#223) * Host Decky plugin panels in Frame Control VR * Record Framey VR verification limits * Lock Frame before worn checks and clean private captures --------- Co-authored-by: SAPHID --- docs/current-support-matrix.md | 1 + docs/decky-frame.md | 3 + docs/evidence/framey-browser-2026-10-25.md | 36 +++ docs/evidence/framey-browser-2026-10-25.png | Bin 0 -> 63442 bytes docs/frame-plugin-author.md | 7 +- docs/framey-loader.md | 104 +++++++ docs/headset-app.md | 5 + scripts/framey-worn-check.py | 170 +++++++++++ tests/e2e/test_framey.py | 121 ++++++++ .../fakeframe/rootfs/usr/local/bin/systemctl | 11 +- .../usr/local/lib/fakeframe/cef_shim.js | 10 + tests/test_framey_panel.py | 104 +++++++ tests/test_framey_worn.py | 57 ++++ ui/frame_decky_store.py | 13 +- ui/framey-host.js | 107 +++++++ ui/framey_panel.py | 94 ++++++ ui/headset.html | 44 ++- ui/server.py | 58 +++- ui/vendor/REACT-DOM-LICENSE.txt | 21 ++ ui/vendor/REACT-LICENSE.txt | 21 ++ ui/vendor/README.md | 9 + ui/vendor/react-18.3.1.min.js | 31 ++ ui/vendor/react-dom-18.3.1.min.js | 267 ++++++++++++++++++ 23 files changed, 1283 insertions(+), 11 deletions(-) create mode 100644 docs/evidence/framey-browser-2026-10-25.md create mode 100644 docs/evidence/framey-browser-2026-10-25.png create mode 100644 docs/framey-loader.md create mode 100644 scripts/framey-worn-check.py create mode 100644 tests/e2e/test_framey.py create mode 100644 tests/test_framey_panel.py create mode 100644 tests/test_framey_worn.py create mode 100644 ui/framey-host.js create mode 100644 ui/framey_panel.py create mode 100644 ui/vendor/REACT-DOM-LICENSE.txt create mode 100644 ui/vendor/REACT-LICENSE.txt create mode 100644 ui/vendor/README.md create mode 100644 ui/vendor/react-18.3.1.min.js create mode 100644 ui/vendor/react-dom-18.3.1.min.js diff --git a/docs/current-support-matrix.md b/docs/current-support-matrix.md index e2ba476..5859efe 100644 --- a/docs/current-support-matrix.md +++ b/docs/current-support-matrix.md @@ -24,5 +24,6 @@ Current for Frame Control **v0.4.31**, 25 October 2026. “Real Frame + physical | Local Frame Control extensions | Example panel displayed live Frame status in a browser preview; the real Frame served a read-only example in the experimental headset panel | No | No wearer-visible extension view; third-party extensions and physical Windows/macOS extension folders untested | 25 Oct, v0.4.29: [PR #194](https://github.com/saphid/frame-control/pull/194), [PR #201](https://github.com/saphid/frame-control/pull/201), [extension guide](extensions.md) | | Experimental Frame Control SteamVR panel | Real Frame OS 0.5.6 installed its user service, registered an overlay, served authenticated live status and a read-only extension, saved a screenshot and short MP4, and removed owned resources | No | Headset was unworn in standby: overlay reported `not_visible`, stereo capture was dark; wearer-visible pixels and controller input are unverified. The new large read-only status screen has browser layout and packaging checks only | 25 Oct, v0.4.29 device check and v0.4.31 status screen: [PR #198](https://github.com/saphid/frame-control/pull/198), [PR #201](https://github.com/saphid/frame-control/pull/201), [PR #220](https://github.com/saphid/frame-control/pull/220), [panel guide](headset-app.md) | | Experimental Decky Loader manager | Real Frame OS 0.5.6 ARM64 user-service install, status, update and remove completed; seven official-store plugin backends loaded, and ProtonDB Badges completed managed store install/update/remove | No | Plugin pages, settings, routes, toasts and controller input in VR remain unverified; some plugins need root, x86_64 binaries or Deck-specific hardware | 25 Oct, v0.4.31: [PR #208](https://github.com/saphid/frame-control/pull/208), [compatibility table](decky-frame.md) | +| Experimental Framey VR plugin panels | No live real-Frame panel test | No | ProtonDB Badges settings rendered and sent a setting call in local Chromium; the fake Frame exercised the store and method bridge. Wearer-visible pixels, controller input and live settings persistence remain unverified | 25 Oct, [PR #223](https://github.com/saphid/frame-control/pull/223), [browser/fake Frame evidence](evidence/framey-browser-2026-10-25.md), [guide](framey-loader.md) | Release notes must preserve these limits until a newer recorded check changes them. When a feature is retested, add its date, exact app/Frame version, host type and proof here. The older [v0.3.1 matrix](support-matrix.html) is a historical snapshot and does not describe current coverage. diff --git a/docs/decky-frame.md b/docs/decky-frame.md index ba4392f..f1df978 100644 --- a/docs/decky-frame.md +++ b/docs/decky-frame.md @@ -142,6 +142,9 @@ on Steam Deck hardware may fail even if the loader runs. Frame Control lists plugins already present in its managed folder and the upstream `~/homebrew` folder. The opt-in store manager installs compatible archives only into its own folder. It cannot make Steam Deck QAM plugin pages visible in Frame VR. +The experimental [Framey panel host](framey-loader.md) can render compatible +plugin settings inside Frame Control's separate SteamVR dashboard panel. +The real headset still needs a worn-user visibility and controller check. The latest tested release was [Decky 3.3.0](https://github.com/SteamDeckHomebrew/decky-loader/releases/tag/v3.3.0), diff --git a/docs/evidence/framey-browser-2026-10-25.md b/docs/evidence/framey-browser-2026-10-25.md new file mode 100644 index 0000000..65dcc04 --- /dev/null +++ b/docs/evidence/framey-browser-2026-10-25.md @@ -0,0 +1,36 @@ +# Framey loader evidence, 25 October 2026 + +## Local browser and fake Frame + +The official Decky store returned 116 validated entries. ProtonDB Badges was +entry 14, release 1.3.4, SHA-256 +`90a3453ac02709cad64096e7539fd8141fa56f99ae3441fc24a4990dc9684358`. +Its published ZIP passed Frame Control's store manifest and path validator. The current +`dist/index.js` bundle was loaded in an opaque-origin iframe in local Chromium +with the Framey React/DFL shim. The [browser screenshot](framey-browser-2026-10-25.png) +shows the plugin's settings panel. A pointer click on **Enable Badge on +Library** issued `set_setting('settings', …)` through the iframe's request +bridge. The local test parent returned a stub backend reply; it did not use a +real headset or Decky backend. A separate fake Frame CEF fixture returned +`get_setting` and `set_setting` responses through Frame Control's CDP bridge; +that fixture does not model the real Decky process. + +The disposable fake Frame end-to-end harness passed the store catalogue, +acknowledgement and HTTP auth checks, hash-verified install, update, and +removal through the real Frame Control HTTP server and SSH transaction. The +full fake Frame suite passed 48 tests; the full unit suite passed 1,559 tests +(16 skipped). Unit tests covered unsafe archives, +unmanaged-folder preservation, asset isolation, backend-call scope, +and safe aborts of the worn-headset check. + +## Real headset status + +Read-only SSH on Frame OS 0.5.6 found battery at 98–99%. SteamVR activity was +initially 3 (standby), briefly 0, then 3 again. During the active probe the +shared `/tmp/frame-test.lock` belonged to `obs-aacapv3evh-20261025`. It later +became free after SteamVR returned to standby. Framey did not acquire, replace, +or remove it. The active probe found no installed Frame Control VR panel or +Framey plugin; the scripted worn check exited before acquiring a lock or +altering the headset. There is **no** wearer-visible VR +menu screenshot, controller-input proof, or live settings-persistence result +from this round. diff --git a/docs/evidence/framey-browser-2026-10-25.png b/docs/evidence/framey-browser-2026-10-25.png new file mode 100644 index 0000000000000000000000000000000000000000..c13f4fb6becf06bdc9fbe00cd560aebc2b4bd08f GIT binary patch literal 63442 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 Z22Q+4qLogNS@)&M(vk`iMPdeD{vTf;aXtV5 literal 0 HcmV?d00001 diff --git a/docs/frame-plugin-author.md b/docs/frame-plugin-author.md index 26f61d7..7383302 100644 --- a/docs/frame-plugin-author.md +++ b/docs/frame-plugin-author.md @@ -26,7 +26,12 @@ The current Frame SteamVR shell does not expose Decky's Steam Deck Quick Access Menu. A backend `Loaded` log only proves backend startup. Test any frontend page, setting, notification, route, and controller input while wearing a Frame before describing it as working in VR. Until a QAM bridge is verified, a Frame -Control extension is the practical route for a visible read-only VR panel. +Control extension is the simplest route for a self-contained read-only VR panel. + +Frame Control now has an experimental [Framey panel host](framey-loader.md) +for API v2 Decky frontend bundles. It renders common settings controls in the +separate SteamVR dashboard panel, but Steam library and store page patches +remain unavailable. Wearer-visible use is not yet verified. To test safely, use a separate Frame Control managed Decky install, verify the ZIP digest from the official store, install one plugin at a time, and inspect diff --git a/docs/framey-loader.md b/docs/framey-loader.md new file mode 100644 index 0000000..d573139 --- /dev/null +++ b/docs/framey-loader.md @@ -0,0 +1,104 @@ +# Framey loader on Steam Frame + +Framey is an experimental bridge for Decky store plugins inside Frame Control's +SteamVR dashboard panel. It is opt in. The panel and Decky backend are ordinary +user services under `~/.local/share/frame-control`; neither needs root or a +read-write SteamOS root filesystem. Decky's standard Quick Access menu still +does not appear in the separate SteamVR CEF target. + +## Use in the headset + +Install **Frame Control in VR** and **Decky Loader on Frame** from Frame Control +on your computer. Open the Frame Control tab in SteamVR, then choose **Decky +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. +Removing a plugin keeps its Decky settings. Removing the Frame Control headset +panel or Decky backend keeps plugin folders until each is removed separately. + +Frame Control's existing official-store manager downloads a published ZIP from +Decky's content-addressed CDN, verifies its store SHA-256 hash and manifest, +rejects unsafe ZIP members and root plugins, and swaps one managed plugin +folder with rollback on failure. It rejects non-ARM64 ELF and Windows binaries. +The Decky user service restarts after a file change. Framey's panel host accepts +only API v2 bundles with `dist/index.js`; other installed plugins remain +manageable but have no Framey panel. The store manager does not currently +screen a plugin's runtime network downloads. + +## What the compatibility host supplies + +The plugin bundle runs in an opaque-origin iframe in the Frame Control SteamVR +panel. It receives React 18.3.1, a small `DFL` panel-control shim, and the +Decky API v2 `call`/`callable` interface. Backend methods are sent by the +authenticated parent to Steam's existing `SharedJSContext` CDP client, then +through Decky's *existing* websocket connection. Framey never opens a second +Decky websocket, because Decky allows only one frontend connection. The iframe +does not receive Frame Control's UI key. React and ReactDOM are bundled for +offline use under their MIT licenses in `ui/vendor`. + +The host currently supplies sections, rows, buttons, toggles, dropdowns, +scroll containers, a modal, locale information, and backend calls for the +plugin that owns the panel. Steam library/store page patches, global Steam UI +components, Decky route hooks, 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 +the headset user when Decky loads it, so review third-party code before opting +in. The panel iframe can make HTTPS requests subject to ordinary browser CORS. + +ProtonDB Badges' current API v2 bundle rendered its settings in a local Chromium +browser with a stubbed backend reply on 25 October 2026. Its controls accepted +pointer input. This does **not** verify SteamVR visibility, controller input, +live Decky settings persistence, or badges on Steam game pages. The last item +has no supported injection target in the Frame Control panel. +The [browser and fake Frame evidence](evidence/framey-browser-2026-10-25.md) +records exactly what was exercised. + +## For plugin authors + +Publish a normal [Decky store](https://plugins.deckbrew.xyz/) API v2 release +with `plugin.json`, a single `dist/index.js` ESM frontend entry point, and a +Python backend if needed. Give the frontend a `content` React element returned +by `definePlugin`, and build settings as panel controls from `@decky/ui`. +Backend calls through `@decky/api` reach only that plugin's own backend. +For best VR compatibility, keep the panel self-contained and use `PanelSection`, +`PanelSectionRow`, `ButtonItem`, `ToggleField`, and `DropdownItem`. Handle a +missing Steam UI route, store overlay, or `SteamClient` object gracefully. +Avoid root, remote binary downloads, Steam library patches, and assumptions +about Steam Deck hardware. Declare settings defaults so a fresh user service +can render without pre-existing files. Test your ZIP with the store validator +and the fake Frame flow before asking users to install it. + +The [Frame Control extension API](extensions.md) is an alternative for a +Frame-specific panel. It needs only a local manifest, HTML, and the read-only +`status` bridge; it does not need Decky. + +## Removal + +Use **Remove** on each installed Framey plugin, then **Remove managed Decky** +and **Remove** under Frame Control in VR. These operations remove only +Frame Control-owned folders and services. Settings, the Chromium profile, and +other users' plugins are preserved. To remove one managed plugin manually, +first inspect its `.frame-control-store.json` receipt in +`~/.local/share/frame-control/decky/plugins/`, then delete only that +folder and run `systemctl --user try-restart frame-control-decky.service`. +The Decky and headset service manual removal commands are in +[Decky on Frame](decky-frame.md#opt-in-and-remove) and +[Frame Control in the headset](headset-app.md#remove). + +## Fast worn-headset check + +With an already installed Framey plugin and someone wearing the headset, run +`python3 scripts/framey-worn-check.py` from a paired computer. The script +atomically acquires `/tmp/frame-test.lock` before reading battery or SteamVR +activity. If another job owns the lock, it makes no sensor reads. It releases +its own lock without starting the interaction when the headset is in standby. +Within 35 seconds, use a controller +to open a managed plugin panel and touch one setting. The script records a +fresh trusted pointer event, the panel-ready event, SteamVR overlay visibility, +and a stereo screenshot with varied pixels. It deletes its own short-lived +challenge and releases only its own lock. It records the screenshot hash and +color count, then removes the private temporary image from both devices. +Pixel variety alone cannot prove the UI text was legible; a person must still +confirm the view in the headset. The script does not install or remove a plugin, +alter Steam settings, or wake the headset. diff --git a/docs/headset-app.md b/docs/headset-app.md index 32fab0a..6840cc3 100644 --- a/docs/headset-app.md +++ b/docs/headset-app.md @@ -33,6 +33,11 @@ It offers the version 1 read-only `capabilities` and `status` bridge from [the extension guide](extensions.md). Installing an extension still means copying its folder to the Frame; the panel does not download third-party code. +The opt-in [Framey loader](framey-loader.md) hosts compatible Decky plugin +panels and settings in a separate sandboxed iframe on this SteamVR page. The +existing official Decky store manager installs and removes their code. The +frontend host is browser-tested, but wearer-visible VR use remains unverified. + The panel also lists Decky plugins and can install, update, or remove compatible plugins from the official store when the opt-in managed loader is running. Each change asks for confirmation. The Frame VR shell does not show Decky's diff --git a/scripts/framey-worn-check.py b/scripts/framey-worn-check.py new file mode 100644 index 0000000..71b25d0 --- /dev/null +++ b/scripts/framey-worn-check.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Bounded, opt-in real Frame check. Run only while someone wears the headset. + +Requires an already installed Frame Control VR panel, Decky user service, and +one Framey-managed plugin. It never installs, restarts, or removes a service. +""" +import datetime +import hashlib +import json +from pathlib import Path +import re +import secrets +import shlex +import subprocess +import sys +import time + +ROOT = Path(__file__).resolve().parents[1] +SSH = ['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=5', 'frame'] +LOCK = '/tmp/frame-test.lock' + + +def remote(command, stdin=None, timeout=15): + run = subprocess.run([*SSH, command], input=stdin, capture_output=True, + text=True, timeout=timeout) + if run.returncode: + raise RuntimeError((run.stderr or run.stdout).strip() or f'SSH exited {run.returncode}') + return run.stdout + + +PROBE = r''' +import json, pathlib, subprocess +power=[] +for item in pathlib.Path('/sys/class/power_supply').glob('*/capacity'): + try: + if (item.parent/'type').read_text().strip() == 'Battery': + power.append({'name':item.parent.name,'percent':int(item.read_text().strip())}) + except (OSError,ValueError): pass +stats=subprocess.run(['/opt/steamvr/bin/linuxarm64/vrcmd','--stats'],capture_output=True,text=True,timeout=5) +try: activity=json.loads(stats.stdout)[0]['activity_level'] if stats.returncode == 0 else None +except (ValueError,KeyError,IndexError,TypeError): activity=None +panels=subprocess.run(['/opt/steamvr/bin/linuxarm64/vrcmd','--overlays'],capture_output=True,text=True,timeout=5) +panel=next((line.strip() for line in panels.stdout.splitlines() if 'valve.steam.desktopgame.2001947813' in line),None) +base=pathlib.Path.home()/'.local/share/frame-control/decky/plugins' +plugins=[] +if base.is_dir() and not base.is_symlink(): + for folder in base.iterdir(): + if folder.is_symlink(): continue + try: + p=json.loads((folder/'.frame-control-store.json').read_text()) + manifest=json.loads((folder/'plugin.json').read_text()) + if isinstance(p.get('storeId'),int): plugins.append({'id':p['storeId'],'name':manifest['name']}) + except (OSError,ValueError,KeyError): pass +service=subprocess.run(['systemctl','--user','is-active','frame-control-headset.service'],capture_output=True,text=True) +print(json.dumps({'battery':power,'activity':activity,'panel':panel, + 'panelService':service.returncode==0,'plugins':plugins})) +''' + +OVERLAY_PROBE = r''' +import ctypes as C, json +vr=C.CDLL('/opt/steamvr/bin/linuxarm64/libopenvr_api.so') +vr.VR_InitInternal2.argtypes=[C.POINTER(C.c_int),C.c_int,C.c_char_p] +vr.VR_GetGenericInterface.argtypes=[C.c_char_p,C.POINTER(C.c_int)] +vr.VR_GetGenericInterface.restype=C.c_void_p +error=C.c_int() +vr.VR_InitInternal2(C.byref(error),3,None) +result={'visible':None,'dashboardVisible':None,'error':error.value} +if not error.value: + try: + table=C.cast(vr.VR_GetGenericInterface(b'FnTable:IVROverlay_028',C.byref(error)),C.POINTER(C.c_void_p)) + if not error.value and table: + handle=C.c_uint64() + find=C.CFUNCTYPE(C.c_int,C.c_char_p,C.POINTER(C.c_uint64))(table[0]) + if find(b'valve.steam.desktopgame.2001947813',C.byref(handle))==0: + result['visible']=bool(C.CFUNCTYPE(C.c_bool,C.c_uint64)(table[45])(handle)) + result['dashboardVisible']=bool(C.CFUNCTYPE(C.c_bool)(table[68])()) + finally: vr.VR_ShutdownInternal() +print(json.dumps(result)) +''' + + +def probe(): + return json.loads(remote('python3 -', stdin=PROBE)) + + +def check_file(): + code = "import pathlib;print((pathlib.Path.home()/'.local/share/frame-control/headset/worn-check.json').read_text())" + return json.loads(remote('python3 -c ' + shlex.quote(code))) + + +def color_count(text): + return len(set(re.findall(r'#[0-9A-Fa-f]{6,8}', text))) + + +def main(): + result = {'time':datetime.datetime.now(datetime.timezone.utc).isoformat(), 'proof':'worn-headset check'} + token = 'framey-' + secrets.token_hex(12) + locked = False + challenge = False + try: + # Do not read even standby status until this task owns the device lock. + remote('mkdir ' + LOCK + ' && { printf %s ' + shlex.quote(token) + + ' > ' + LOCK + '/owner || { rm -f ' + LOCK + '/owner; rmdir ' + LOCK + '; exit 1; }; }') + locked = True + before = probe() + result['before'] = before + if not before['battery'] or min(x['percent'] for x in before['battery']) < 20: + raise RuntimeError('Battery capacity unavailable or below 20%; no device session started') + if before['activity'] is None or before['activity'] >= 3: + raise RuntimeError('SteamVR is in standby; no worn-headset check started') + if not before['panelService'] or not before['plugins']: + raise RuntimeError('Install and open the Frame Control panel and a Framey plugin before this check') + after_lock = probe() + if after_lock['activity'] >= 3 or min(x['percent'] for x in after_lock['battery']) < 20: + raise RuntimeError('Activity or battery changed after acquiring the lock') + state = json.dumps({'nonce':token,'started':time.time()}) + code = "import pathlib; p=pathlib.Path.home()/'.local/share/frame-control/headset/worn-check.json'; p.write_text(" + repr(state) + ")" + remote('python3 -c ' + shlex.quote(code)) + challenge = True + print('In the worn headset: open SteamVR dashboard → Frame Control → a managed Decky plugin → Open panel. Touch a setting with the controller. Waiting up to 35 seconds.', flush=True) + deadline = time.monotonic() + 35 + while time.monotonic() < deadline: + state = check_file() + if state.get('nonce') == token and state.get('panel') and state.get('input'): + break + time.sleep(1) + result['interaction'] = state + result['after'] = probe() + result['worn'] = isinstance(result['after']['activity'], int) and result['after']['activity'] < 3 + result['overlay'] = json.loads(remote('python3 -', stdin=OVERLAY_PROBE, timeout=15)) + result['panelVisible'] = result['overlay']['visible'] is True + installed_ids = {p['id'] for p in before['plugins']} + result['controllerInput'] = bool(state.get('input', {}).get('at', 0) > state.get('started', 0)) + result['pluginPanelReady'] = bool(state.get('panel', {}).get('at', 0) > state.get('started', 0) + and state['panel'].get('pluginId') in installed_ids) + if result['panelVisible'] and result['pluginPanelReady']: + shot = json.loads(remote('python3 -', stdin=(ROOT/'ui/frame_vrshot.py').read_text(), timeout=20))['path'] + local = Path('/tmp')/('framey-worn-' + token + '.png') + try: + with local.open('wb') as out: + copy = subprocess.run([*SSH, 'cat ' + shlex.quote(shot)], stdout=out, stderr=subprocess.PIPE, timeout=20) + if copy.returncode: raise RuntimeError('Could not copy the SteamVR screenshot') + result['screenshotSha256'] = hashlib.sha256(local.read_bytes()).hexdigest() + pixels = subprocess.run(['convert', str(local), '-resize', '64x64!', 'txt:-'], + capture_output=True, text=True, timeout=15) + result['screenshotColors'] = color_count(pixels.stdout) if pixels.returncode == 0 else None + finally: + local.unlink(missing_ok=True) + remote('rm -f ' + shlex.quote(shot)) + result['passed'] = (result['worn'] and result['panelVisible'] and result['controllerInput'] and + result['pluginPanelReady'] and (result.get('screenshotColors') or 0) > 8) + except Exception as exc: + result['error'] = str(exc) + finally: + if challenge: + code = "import json,pathlib; p=pathlib.Path.home()/'.local/share/frame-control/headset/worn-check.json'; d=json.loads(p.read_text()); p.unlink() if d.get('nonce')==" + repr(token) + " else None" + try: remote('python3 -c ' + shlex.quote(code)) + except Exception as exc: result['challengeCleanupError'] = str(exc) + if locked: + try: + remote('test "$(cat ' + LOCK + '/owner)" = ' + shlex.quote(token) + + ' && rm ' + LOCK + '/owner && rmdir ' + LOCK) + result['lockReleased'] = True + except Exception as exc: result['lockCleanupError'] = str(exc) + print(json.dumps(result, indent=2)) + return 0 if result.get('passed') else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tests/e2e/test_framey.py b/tests/e2e/test_framey.py new file mode 100644 index 0000000..a6b1042 --- /dev/null +++ b/tests/e2e/test_framey.py @@ -0,0 +1,121 @@ +"""Verified Decky-store-shaped install flow through the real HTTP server and fake Frame SSH.""" +import hashlib +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +import io +import json +import os +import shlex +import subprocess +import threading +import zipfile + +from harness import FrameTestCase, ROOT, Server, api, finished, ok, require, ssh + +require() # Skip the module outside the disposable fake Frame, like its peers. + + +def archive(version): + raw = io.BytesIO() + with zipfile.ZipFile(raw, 'w') as bundle: + bundle.writestr('sample-panel/plugin.json', json.dumps({'name':'Sample Panel','api_version':2,'flags':[]})) + bundle.writestr('sample-panel/dist/index.js', + f"export default () => ({{content: SP_REACT.createElement('p', null, 'Sample {version}')}});") + bundle.writestr('sample-panel/README.txt', ''.join(hashlib.sha256(f'{version}-{n}'.encode()).hexdigest() for n in range(35))) + return raw.getvalue() + + +ARTIFACTS = {hashlib.sha256(a).hexdigest(): a for a in (archive('1.0.0'), archive('1.1.0'))} +HASHES = list(ARTIFACTS) + + +class StoreFixture(BaseHTTPRequestHandler): + release = 0 + def log_message(self, *_): + pass + + def do_GET(self): + if self.path == '/plugins': + body = json.dumps([{'id':123,'name':'Sample Panel','author':'Fixture', + 'description':'A harmless panel','tags':[], + 'versions':[{'name':['1.0.0','1.1.0'][self.release], + 'hash':HASHES[self.release]}]}]).encode() + content_type = 'application/json' + elif self.path.startswith('/versions/') and self.path.endswith('.zip'): + body = ARTIFACTS.get(self.path.split('/')[-1][:-4]) + content_type = 'application/zip' + else: + body = None + self.send_response(200 if body is not None else 404) + self.send_header('Content-Type', content_type if body is not None else 'text/plain') + self.end_headers() + self.wfile.write(body or b'not found') + + +class FrameyFlow(FrameTestCase): + @classmethod + def setUpClass(cls): + require() + StoreFixture.release = 0 + cls.fixture = ThreadingHTTPServer(('127.0.0.1', 0), StoreFixture) + cls.thread = threading.Thread(target=cls.fixture.serve_forever, daemon=True) + cls.thread.start() + port = cls.fixture.server_port + cls.previous = {key:os.environ.get(key) for key in ('FRAMEY_STORE_URL','FRAMEY_CDN_URL')} + os.environ['FRAMEY_STORE_URL'] = f'http://127.0.0.1:{port}/plugins' + os.environ['FRAMEY_CDN_URL'] = f'http://127.0.0.1:{port}/versions/' + Server.stop() + super().setUpClass() + + @classmethod + def tearDownClass(cls): + Server.stop() + cls.fixture.shutdown() + for key, value in cls.previous.items(): + if value is None: os.environ.pop(key, None) + else: os.environ[key] = value + + def backend(self): + code = '''import json,pathlib +h=pathlib.Path.home(); b=h/'.local/share/frame-control/decky/.frame-control'; b.mkdir(parents=True,exist_ok=True) +(b/'receipt.json').write_text(json.dumps({'managedBy':'Frame Control','version':'v3.3.0'})) +(b.parent/'plugins').mkdir(exist_ok=True) +u=h/'.config/systemd/user/frame-control-decky.service'; u.parent.mkdir(parents=True,exist_ok=True); u.write_text('[Service]')''' + ssh('python3 -c ' + shlex.quote(code)) + ssh('mkdir -p ~/.local/share/frame-control/headset/bundle/ui') + source = (ROOT/'ui/frame_steam.py').read_bytes() + copy = subprocess.run(['ssh','frame','cat > ~/.local/share/frame-control/headset/bundle/ui/frame_steam.py'], + input=source, capture_output=True, timeout=20) + self.assertEqual(copy.returncode, 0, copy.stderr) + + def test_store_install_update_remove_and_guards(self): + self.backend() + status, _, _ = api('GET', '/api/decky/store', headers={'X-Frame-UI':''}) + self.assertEqual(status, 403) + catalogue = ok('GET', '/api/decky/store')['plugins'] + self.assertEqual((catalogue[0]['version'], catalogue[0]['sha256']), ('1.0.0', HASHES[0])) + status, _, _ = api('POST', '/api/decky/plugin', {'action':'install','id':123}) + self.assertEqual(status, 400) # Third-party code needs explicit acknowledgement. + job = finished(ok('POST', '/api/decky/plugin', + {'action':'install','id':123,'acknowledged':True})) + self.assertFalse(job.get('error'), job) + self.assertEqual(ok('GET', '/api/decky')['plugins'][0]['version'], '1.0.0') + self.assertIn('Sample 1.0.0', ssh('cat ~/.local/share/frame-control/decky/plugins/sample-panel/dist/index.js')) + status, _, _ = api('POST', '/api/framey/method', {'id':123,'method':'get_setting','args':[]}, + headers={'X-Frame-UI':''}) + self.assertEqual(status, 403) + status, _, _ = api('POST', '/api/framey/method', {'id':123,'method':'x;alert(1)','args':[]}) + self.assertEqual(status, 400) + self.assertEqual(ok('POST', '/api/framey/method', + {'id':123,'method':'get_setting','args':['theme','dark']})['result'], 'dark') + self.assertTrue(ok('POST', '/api/framey/method', + {'id':123,'method':'set_setting','args':['theme','light']})['result']) + StoreFixture.release = 1 + job = finished(ok('POST', '/api/decky/plugin', + {'action':'update','id':123,'acknowledged':True})) + self.assertFalse(job.get('error'), job) + self.assertEqual(ok('GET', '/api/decky')['plugins'][0]['version'], '1.1.0') + job = finished(ok('POST', '/api/decky/plugin', + {'action':'remove','id':123,'acknowledged':True})) + self.assertFalse(job.get('error'), job) + self.assertEqual(ok('GET', '/api/decky')['plugins'], []) + self.assertEqual(ssh('test -e ~/.local/share/frame-control/decky/plugins/sample-panel && echo yes || echo no').strip(), 'no') diff --git a/tests/fakeframe/rootfs/usr/local/bin/systemctl b/tests/fakeframe/rootfs/usr/local/bin/systemctl index 829d46e..ecda7d0 100755 --- a/tests/fakeframe/rootfs/usr/local/bin/systemctl +++ b/tests/fakeframe/rootfs/usr/local/bin/systemctl @@ -3,9 +3,11 @@ Stable NRestarts=0 and inactive keep-awake are harness assumptions, not observed device behavior. Real Frame query forms come from the historical doctor runbook -(2026-09-28, BUILD_ID 20260925.6191901). No service control is emulated here. +(2026-09-28, BUILD_ID 20260925.6191901). The Framey e2e fixture emulates only +the Decky test unit's active/start/stop answers; no real service is launched. """ import sys +from pathlib import Path args = sys.argv[1:] if args in (['--user', 'show', 'steam.service', '-p', 'NRestarts', '--value'], @@ -13,5 +15,12 @@ if args in (['--user', 'show', 'steam.service', '-p', 'NRestarts', '--value'], print('0') elif args == ['--user', 'show', 'fc-keep-awake.service', '-p', 'ActiveState', '--value']: print('inactive') +elif args in (['--user', 'is-active', 'frame-control-decky.service'], + ['--user', 'is-active', '--quiet', 'frame-control-decky.service'], + ['--user', 'stop', 'frame-control-decky.service'], + ['--user', 'start', 'frame-control-decky.service']) and \ + (Path.home()/'.config/systemd/user/frame-control-decky.service').is_file(): + if args[1] == 'is-active' and '--quiet' not in args: + print('active') else: sys.exit(1) diff --git a/tests/fakeframe/rootfs/usr/local/lib/fakeframe/cef_shim.js b/tests/fakeframe/rootfs/usr/local/lib/fakeframe/cef_shim.js index a65563a..a7d03ce 100644 --- a/tests/fakeframe/rootfs/usr/local/lib/fakeframe/cef_shim.js +++ b/tests/fakeframe/rootfs/usr/local/lib/fakeframe/cef_shim.js @@ -25,6 +25,15 @@ function newShortcutId(steam) { } function build(steam) { + // Synthetic Decky API for Framey's CDP bridge test. This does not model the + // real loader or prove frontend visibility on a headset. + const deckyBackend = {async call(route, plugin, method, ...args) { + if (route !== 'loader/call_plugin_method' || plugin !== 'Sample Panel') + throw Error('unexpected Decky plugin route'); + if (method === 'get_setting') return args[1]; + if (method === 'set_setting') return true; + throw Error('method unavailable in fake Decky backend'); + }}; const findShortcut = id => steam.shortcuts.find(s => s.appid === Number(id)); const gameOverview = a => ({ appid: a.appid, display_name: a.display_name, sort_as: a.display_name, app_type: a.app_type ?? 1, @@ -66,6 +75,7 @@ function build(steam) { value, }); return { + window: {DeckyBackend: deckyBackend}, DeckyBackend: deckyBackend, ...require('./canvas_stub').surface(), collectionStore: { GetUserCollectionsByName(name) { return steam.collections.filter(c => c.name === name).map(collection); }, diff --git a/tests/test_framey_panel.py b/tests/test_framey_panel.py new file mode 100644 index 0000000..1537794 --- /dev/null +++ b/tests/test_framey_panel.py @@ -0,0 +1,104 @@ +"""Framey hosts only store-owned API v2 frontends and scopes backend calls.""" +import sandbox # noqa: F401 +import gzip +import http.client +import io +import json +from pathlib import Path +import sys +import tempfile +import tarfile +import threading +import unittest +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1]/'ui')) +import framey_panel +import frame_headset +import server + + +class Panel(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.plugins = Path(tmp.name)/'plugins' + self.folder = self.plugins/'protondb-decky' + (self.folder/'dist').mkdir(parents=True) + (self.folder/'.frame-control-store.json').write_text(json.dumps({'storeId':14,'version':'1.3.4'})) + (self.folder/'plugin.json').write_text(json.dumps({'name':'ProtonDB Badges','api_version':2,'flags':[]})) + (self.folder/'dist/index.js').write_text("export default () => ({content: SP_REACT.createElement('p', null, 'OK')});") + home = patch.object(framey_panel, 'PLUGINS', self.plugins) + home.start() + self.addCleanup(home.stop) + + def test_owned_api_v2_bundle_is_embedded_without_ui_key(self): + page = framey_panel.panel(14).decode() + self.assertIn('framey-bundle', page) + self.assertIn('/framey-runtime/host.js', page) + self.assertNotIn('X-Frame-UI', page) + self.assertNotIn('SP_REACT.createElement', page) # bundled code is base64, not executable HTML. + with self.assertRaises(FileNotFoundError): + framey_panel.panel(15) + + def test_legacy_root_symlink_and_unmanaged_are_refused(self): + manifest = self.folder/'plugin.json' + manifest.write_text(json.dumps({'name':'ProtonDB Badges','api_version':1})) + with self.assertRaises(FileNotFoundError): framey_panel.panel(14) + manifest.write_text(json.dumps({'name':'ProtonDB Badges','api_version':2,'flags':['root']})) + with self.assertRaises(FileNotFoundError): framey_panel.panel(14) + manifest.write_text(json.dumps({'name':'ProtonDB Badges','api_version':2,'flags':[]})) + dist = self.folder/'dist' + dist.rename(self.folder/'saved-dist') + dist.symlink_to(self.folder/'saved-dist') + with self.assertRaises(FileNotFoundError): framey_panel.panel(14) + dist.unlink() + (self.folder/'.frame-control-store.json').unlink() + with self.assertRaises(FileNotFoundError): framey_panel.panel(14) + + def test_backend_method_is_scoped_to_store_receipt(self): + def fake_ssh(command, stdin=None, **_): + self.assertIn('python3 - ', command) + self.assertEqual(stdin, framey_panel.METHOD_SCRIPT) + return json.dumps({'result':{'theme':'dark'}}) + with patch.object(framey_panel.frame_decky, 'status', return_value={ + 'plugins':[{'storeId':14,'name':'ProtonDB Badges'}]}): + self.assertEqual(framey_panel.call_method(fake_ssh,14,'get_setting',['theme','dark']), + {'result':{'theme':'dark'}}) + with self.assertRaises(framey_panel.PanelError): framey_panel.call_method(fake_ssh,15,'get_setting',[]) + with self.assertRaises(framey_panel.PanelError): framey_panel.call_method(fake_ssh,14,'x;alert(1)',[]) + compile(framey_panel.METHOD_SCRIPT, '', 'exec') + + def test_headset_bundle_contains_runtime_and_licenses(self): + packed = frame_headset.bundle() + self.assertGreater(len(packed), 100_000) + self.assertLess(len(packed), frame_headset.MAX_ARCHIVE) + with tarfile.open(fileobj=io.BytesIO(gzip.decompress(packed))) as archive: + names = set(archive.getnames()) + self.assertTrue({'ui/framey_panel.py','ui/framey-host.js', + 'ui/vendor/react-18.3.1.min.js','ui/vendor/react-dom-18.3.1.min.js', + 'ui/vendor/REACT-LICENSE.txt','ui/vendor/REACT-DOM-LICENSE.txt'} <= names) + + def test_http_panel_is_sandboxable_and_missing_plugin_is_404(self): + httpd = server.ThreadingHTTPServer(('127.0.0.1', 0), server.Handler) + thread = threading.Thread(target=httpd.serve_forever, daemon=True) + thread.start() + self.addCleanup(httpd.server_close) + self.addCleanup(httpd.shutdown) + def get(path): + conn = http.client.HTTPConnection('127.0.0.1', httpd.server_port, timeout=5) + try: + conn.request('GET', path) + reply = conn.getresponse() + return reply.status, dict(reply.getheaders()), reply.read() + finally: + conn.close() + status, headers, body = get('/framey/panel/14') + self.assertEqual(status, 200) + self.assertIn('blob:', headers['Content-Security-Policy']) + self.assertIn(b'framey-bundle', body) + self.assertEqual(get('/framey/panel/15')[0], 404) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_framey_worn.py b/tests/test_framey_worn.py new file mode 100644 index 0000000..7d71e65 --- /dev/null +++ b/tests/test_framey_worn.py @@ -0,0 +1,57 @@ +"""The real-device check must abort before touching a standby or busy Frame.""" +import sandbox # noqa: F401 +import contextlib +import importlib.util +import io +from pathlib import Path +import unittest +from unittest.mock import patch + + +MODULE = Path(__file__).resolve().parents[1]/'scripts/framey-worn-check.py' +spec = importlib.util.spec_from_file_location('framey_worn_check', MODULE) +worn = importlib.util.module_from_spec(spec) +spec.loader.exec_module(worn) + + +class WornGate(unittest.TestCase): + def run_gate(self, state, remote_side_effect=None): + out = io.StringIO() + with patch.object(worn, 'probe', return_value=state) as probe, \ + patch.object(worn, 'remote', side_effect=remote_side_effect) as remote, \ + contextlib.redirect_stdout(out): + result = worn.main() + return result, out.getvalue(), remote, probe + + def test_standby_is_read_only_under_owned_lock_and_releases_it(self): + state = {'battery':[{'percent':99}], 'activity':3, 'panelService':True, + 'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None} + code, report, remote, probe = self.run_gate(state) + self.assertEqual(code, 1) + self.assertIn('standby', report) + probe.assert_called_once() + self.assertEqual(remote.call_count, 2) + self.assertIn('mkdir /tmp/frame-test.lock', remote.call_args_list[0].args[0]) + self.assertIn('rmdir /tmp/frame-test.lock', remote.call_args_list[1].args[0]) + + def test_low_battery_releases_owned_lock(self): + state = {'battery':[{'percent':19}], 'activity':0, 'panelService':True, + 'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None} + code, report, remote, probe = self.run_gate(state) + self.assertEqual(code, 1) + self.assertIn('below 20%', report) + probe.assert_called_once() + self.assertEqual(remote.call_count, 2) + + def test_foreign_lock_prevents_all_sensor_reads_and_is_never_removed(self): + state = {'battery':[{'percent':99}], 'activity':0, 'panelService':True, + 'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None} + code, report, remote, probe = self.run_gate(state, RuntimeError('File exists')) + self.assertEqual(code, 1) + self.assertIn('File exists', report) + probe.assert_not_called() + self.assertEqual(remote.call_count, 1) + self.assertIn('mkdir /tmp/frame-test.lock', remote.call_args.args[0]) + + def test_screenshot_palette_counts_distinct_colors(self): + self.assertEqual(worn.color_count('0 #ff00aa 1 #ff00aa 2 #00ff00'), 2) diff --git a/ui/frame_decky_store.py b/ui/frame_decky_store.py index 5025948..5ad45af 100644 --- a/ui/frame_decky_store.py +++ b/ui/frame_decky_store.py @@ -6,6 +6,7 @@ digest validates transport, not plugin trust; store plugins run as the user. import hashlib import io import json +import os import re import secrets import urllib.request @@ -24,8 +25,16 @@ SHA = re.compile(r'[0-9a-f]{64}\Z') VERSION = re.compile(r'[A-Za-z0-9][A-Za-z0-9._+\-]{0,39}\Z') +def _urls(): + # Deterministic fixture URLs exist only inside the disposable fake Frame host. + if os.environ.get('FRAME_E2E_CONTAINER') == '1': + return (os.environ.get('FRAMEY_STORE_URL', STORE_URL), + os.environ.get('FRAMEY_CDN_URL', CDN)) + return STORE_URL, CDN + + def catalog(): - request = urllib.request.Request(STORE_URL, headers={'User-Agent': 'Frame-Control'}) + request = urllib.request.Request(_urls()[0], headers={'User-Agent': 'Frame-Control'}) with urllib.request.urlopen(request, timeout=15) as response: raw = response.read(MAX_STORE + 1) if len(raw) > MAX_STORE: @@ -176,7 +185,7 @@ def change(ssh, action, plugin_id): raise ValueError('Only an installed Frame Control store plugin can be updated') data = None if action != 'remove': - url = CDN + plugin['sha256'] + '.zip' + url = _urls()[1] + plugin['sha256'] + '.zip' with urllib.request.urlopen(urllib.request.Request(url, headers={'User-Agent': 'Frame-Control'}), timeout=35) as response: data = response.read(MAX_ZIP + 1) folder = archive_info(data, plugin['sha256'], plugin['name']) diff --git a/ui/framey-host.js b/ui/framey-host.js new file mode 100644 index 0000000..0798c16 --- /dev/null +++ b/ui/framey-host.js @@ -0,0 +1,107 @@ +/* Framey panel compatibility layer. The iframe has an opaque origin and no UI key. */ +(() => { + 'use strict'; + const R = window.SP_REACT = window.React; + window.SP_REACTDOM = window.ReactDOM; + window.LocalizationManager = {m_rgLocalesToUse:[navigator.language || 'en']}; + const root = document.getElementById('framey-root'); + const error = document.getElementById('framey-error'); + const pending = new Map(); + let nextId = 0; + let plugin; + + function ask(method, args) { + if (!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(method)) return Promise.reject(Error('Invalid backend method')); + const id = ++nextId; + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { pending.delete(id); reject(Error('Decky backend timed out')); }, 30000); + pending.set(id, {resolve, reject, timer}); + parent.postMessage({type:'framey:call', id, method, args}, '*'); + }); + } + addEventListener('message', event => { + if (event.source !== parent || !event.data || event.data.type !== 'framey:reply') return; + const item = pending.get(event.data.id); + if (!item) return; + pending.delete(event.data.id); clearTimeout(item.timer); + if (event.data.error) item.reject(Error(event.data.error)); else item.resolve(event.data.result); + }); + addEventListener('unload', () => { try { plugin?.onDismount?.(); } catch (_) {} }); + addEventListener('pointerup', event => { + if (event.isTrusted) parent.postMessage({type:'framey:input'}, '*'); + }, {passive:true}); + + const passthrough = tag => ({children, ...props}) => R.createElement(tag, props, children); + const Field = ({label, description, children}) => R.createElement('label', null, + R.createElement('strong', null, label), description && R.createElement('small', null, description), children); + const ToggleField = ({label, description, checked, onChange}) => R.createElement(Field, {label, description}, + R.createElement('input', {type:'checkbox', checked:!!checked, onChange:e=>onChange?.(e.target.checked)})); + const DropdownItem = ({label, description, rgOptions=[], selectedOption, onChange}) => R.createElement(Field, {label, description}, + R.createElement('select', {value:String(selectedOption ?? ''), onChange:e=>onChange?.(rgOptions.find(x=>String(x.data)===e.target.value))}, + rgOptions.map(x=>R.createElement('option', {key:String(x.data), value:String(x.data)}, x.label)))); + const ButtonItem = ({label, children, onClick}) => R.createElement(Field, {label}, + R.createElement('button', {type:'button', onClick}, children || label)); + const PanelSection = ({title, children}) => R.createElement('section', null, + title && R.createElement('h2', null, title), children); + const ConfirmModal = ({strTitle, strOKButtonText, onOK, children}) => R.createElement('section', null, + R.createElement('h2', null, strTitle), children, + R.createElement('button', {type:'button', onClick:()=>{onOK?.(); closeModal();}}, strOKButtonText || 'Close')); + const modalRoot = window.ReactDOM.createRoot(document.getElementById('framey-modal')); + const closeModal = () => modalRoot.render(null); + const DFL = window.DFL = { + definePlugin: init => init, + PanelSection, PanelSectionRow: passthrough('div'), Field, ToggleField, DropdownItem, ButtonItem, + Focusable: passthrough('div'), ScrollPanel: passthrough('div'), ScrollPanelGroup: passthrough('div'), + DialogButton: passthrough('button'), ConfirmModal, + staticClasses: {Title:'framey-title'}, + Navigation: {NavigateToExternalWeb: url => { if (/^https:\/\//.test(url)) window.open(url, '_blank', 'noopener'); }}, + showModal: element => modalRoot.render(R.createElement('section', null, + R.createElement('button', {onClick:closeModal}, 'Close'), element)), + // Steam app, library, and store patches have no target inside this panel. + afterPatch: () => () => {}, createReactTreePatcher: () => () => {}, + findInReactTree: () => null, findModuleExport: () => null, + appDetailsClasses: {}, appDetailsHeaderClasses: {}, + useParams: () => ({}), + }; + DFL.DialogButton.render = () => ({type: DFL.DialogButton}); + const noRoute = {addPatch:()=>()=>{}, removePatch:()=>{}, addGlobalComponent:()=>{}, removeGlobalComponent:()=>{}}; + const toaster = {toast: ({title, body}) => { error.textContent = [title, body].filter(Boolean).join(': '); }}; + class PanelBoundary extends R.Component { + constructor(props) { super(props); this.state = {failure:null}; } + static getDerivedStateFromError(failure) { return {failure}; } + render() { return this.state.failure ? R.createElement('p', null, + `This Decky panel is not compatible with Framey: ${this.state.failure.message}`) : this.props.children; } + } + window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit = { + connect: (version, name) => ({ + _version: Math.min(version, 2), + call: (method, ...args) => ask(method, args), + callable: method => (...args) => ask(method, args), + addEventListener: () => {}, removeEventListener: () => {}, + routerHook:noRoute, toaster, + fetchNoCors: () => Promise.reject(Error('Decky proxy fetch is unavailable in Framey')), + executeInTab: () => Promise.reject(Error('Steam tab injection is unavailable in Framey')), + injectCssIntoTab: () => Promise.reject(Error('Steam tab injection is unavailable in Framey')), + removeCssFromTab: () => Promise.reject(Error('Steam tab injection is unavailable in Framey')), + getExternalResourceURL: () => null, + useQuickAccessVisible: () => true, + }), + }; + async function start() { + try { + const bytes = Uint8Array.from(atob(document.getElementById('framey-bundle').textContent), c=>c.charCodeAt(0)); + const url = URL.createObjectURL(new Blob([bytes], {type:'text/javascript'})); + let module; + try { module = await import(url); } finally { URL.revokeObjectURL(url); } + if (typeof module.default !== 'function') throw Error('The Decky frontend has no plugin entry point'); + plugin = await module.default(); + if (!plugin?.content) throw Error('The Decky plugin has no panel to display'); + window.ReactDOM.createRoot(root).render(R.createElement(PanelBoundary, null, plugin.content)); + parent.postMessage({type:'framey:ready'}, '*'); + } catch (failure) { + error.textContent = `This Decky panel is not compatible with Framey: ${failure.message}`; + parent.postMessage({type:'framey:error', message:failure.message}, '*'); + } + } + start(); +})(); diff --git a/ui/framey_panel.py b/ui/framey_panel.py new file mode 100644 index 0000000..b91d95b --- /dev/null +++ b/ui/framey_panel.py @@ -0,0 +1,94 @@ +"""Opaque VR frontend host for plugins owned by Frame Control's Decky store.""" +import base64 +import json +from pathlib import Path +import re +import shlex + +import frame_decky + + +METHOD = re.compile(r'[A-Za-z][A-Za-z0-9_]{0,63}\Z') +PLUGINS = Path.home()/'.local/share/frame-control/decky/plugins' + + +class PanelError(ValueError): + pass + + +def local_bundle(plugin_id): + """Read only an API v2 bundle with a store receipt; reject linked paths.""" + if type(plugin_id) is not int or plugin_id <= 0 or not PLUGINS.is_dir() or PLUGINS.is_symlink(): + raise FileNotFoundError('Framey plugin unavailable') + for folder in PLUGINS.iterdir(): + if folder.is_symlink() or not folder.is_dir(): + continue + receipt = folder/'.frame-control-store.json' + manifest = folder/'plugin.json' + dist = folder/'dist' + entry = dist/'index.js' + if any(p.is_symlink() for p in (receipt, manifest, dist, entry)): + continue + try: + if not receipt.is_file() or receipt.stat().st_size > 16_384: + continue + meta = json.loads(receipt.read_text()) + if not isinstance(meta, dict): + continue + if meta.get('storeId') != plugin_id: + continue + if not manifest.is_file() or manifest.stat().st_size > 16_384: + raise FileNotFoundError('Framey plugin manifest unavailable') + data = json.loads(manifest.read_text()) + if not isinstance(data, dict): + raise FileNotFoundError('Framey plugin manifest unavailable') + flags = data.get('flags', []) + if data.get('api_version') != 2 or not isinstance(flags, list) or any(x in flags for x in ('root', '_root')): + raise FileNotFoundError('This Decky frontend is not API v2 or needs root') + if not entry.is_file() or entry.stat().st_size > 8*1024*1024: + raise FileNotFoundError('Framey plugin frontend unavailable') + return entry.read_bytes() + except (OSError, ValueError): + raise FileNotFoundError('Framey plugin files are invalid') + raise FileNotFoundError('Plugin is not managed by the Frame Control Decky store') + + +def panel(plugin_id): + """The plugin receives no Frame UI key and is sandboxed to an opaque origin.""" + encoded = base64.b64encode(local_bundle(plugin_id)).decode('ascii') + return (''' +Framey plugin +

+ + +''').encode() + + +METHOD_SCRIPT = r''' +import json, pathlib, sys +sys.path.insert(0, str(pathlib.Path.home()/'.local/share/frame-control/headset/bundle/ui')) +from frame_steam import Page +payload = json.loads(sys.argv[1]) +expression = "(async()=>{if(!window.DeckyBackend)throw Error('Decky frontend is unavailable in Steam CEF');return await DeckyBackend.call('loader/call_plugin_method'," + json.dumps(payload['name']) + ',' + json.dumps(payload['method']) + ',' + ','.join(json.dumps(x) for x in payload['args']) + ')})()' +page = Page() +try: print(json.dumps({'result':page.eval(expression)})) +finally: page.sock.close() +''' + + +def call_method(ssh, plugin_id, method, args): + if type(plugin_id) is not int or plugin_id <= 0: + raise PanelError('Choose an installed Decky store plugin') + if not isinstance(method, str) or not METHOD.fullmatch(method): + raise PanelError('Invalid plugin method') + if not isinstance(args, list) or len(args) > 8 or len(json.dumps(args)) > 8192: + raise PanelError('Plugin method arguments are too large') + found = [p for p in frame_decky.status(ssh)['plugins'] if p.get('storeId') == plugin_id] + if len(found) != 1: + raise PanelError('The plugin is not managed by the Frame Control Decky store') + payload = {'name':found[0]['name'], 'method':method, 'args':args} + return json.loads(ssh('python3 - ' + shlex.quote(json.dumps(payload)), + stdin=METHOD_SCRIPT, timeout=25)) diff --git a/ui/headset.html b/ui/headset.html index 8bb919b..5dab058 100644 --- a/ui/headset.html +++ b/ui/headset.html @@ -43,10 +43,13 @@ iframe{display:block;width:100%;height:560px;background:#0d1925;border:1px solid

Running games

Loading…
-

Decky plugins

Manage official store plugins here. Their backend can run on Frame, but Decky's Steam Deck Quick Access Menu is absent from this VR shell. Plugin pages, settings, routes, and toasts are not yet available in this panel.

+

Decky plugins

Manage official store plugins here. Decky's Quick Access Menu is absent from this VR shell. Framey can show compatible plugin panels and settings below; Steam library and store page patches are unavailable.

+

Extensions

Local additions run only when opened in the isolated panel below. They can read status but cannot change headset settings through the extension API.

Loading…
@@ -94,13 +97,15 @@ async function refreshGames(){const list=byId('games');try{const data=await api( catch(error){byId('actionStatus').textContent=error.message;quit.disabled=false;}}; row.append(name,quit);list.append(row);}if(!data.running.length)list.textContent='No games are running.'; }catch(error){list.textContent=error.message;}} -let deckyState=null, deckyStore=[]; +let deckyState=null, deckyStore=[],activeFrameyId=null; async function refreshDecky(){try{deckyState=await api('/api/decky'); byId('deckyState').textContent=deckyState.running?'Decky loader is running. Backend loading is not a VR menu check.':'Decky is not running. Install it from Frame Control on your computer.'; const list=byId('deckyInstalled');list.replaceChildren(); for(const plugin of deckyState.plugins||[]){const row=document.createElement('div');row.className='block'; const name=document.createElement('strong');name.textContent=`${plugin.name} ${plugin.version||''}`;row.append(name); - if(plugin.storeId){const button=document.createElement('button');button.className='danger';button.textContent='Remove'; + if(plugin.storeId){const open=document.createElement('button');open.className='secondary';open.textContent='Open panel'; + open.onclick=()=>openFrameyPanel(plugin.storeId,plugin.name);row.append(open); + const button=document.createElement('button');button.className='danger';button.textContent='Remove'; button.onclick=()=>deckyChange('remove',{id:plugin.storeId,name:plugin.name});row.append(button);} list.append(row);}if(!deckyState.plugins?.length)list.textContent='No managed plugins installed.'; showDeckyStore();}catch(error){byId('deckyState').textContent=error.message;}} @@ -120,8 +125,37 @@ function showDeckyStore(){const list=byId('deckyStoreList');list.replaceChildren async function deckyChange(action,plugin){if(!confirm(`${action} ${plugin.name}? Store plugins run code as your Frame user.`))return; byId('deckyState').textContent=`${action} ${plugin.name}…`; try{const job=await api('/api/decky/plugin',{action,id:plugin.id,acknowledged:true}); - byId('deckyState').textContent=job.message||'Request queued. Refresh after it finishes.'; - setTimeout(refreshDecky,5000);}catch(error){byId('deckyState').textContent=error.message;}} + if(job.job){let completed=false;for(let n=0;n<180;n++){await new Promise(resolve=>setTimeout(resolve,1000)); + const progress=await api(`/api/job?id=${encodeURIComponent(job.job)}`); + byId('deckyState').textContent=progress.stage||progress.message||`${action} ${plugin.name}…`; + if(progress.done){if(progress.error)throw Error(progress.error);completed=true;break;}} + if(!completed)throw Error('The operation is still running. Refresh plugins to check its result.');} + await refreshDecky();}catch(error){byId('deckyState').textContent=error.message;}} +function openFrameyPanel(id,name){ + if(!(deckyState?.plugins||[]).some(p=>p.storeId===id))return; + activeFrameyId=id;byId('frameyTitle').textContent=name; + byId('frameyPanelStatus').textContent='Loading plugin panel…'; + byId('frameyFrame').src=`/framey/panel/${id}`; + byId('frameyPanel').hidden=false;byId('frameyPanel').scrollIntoView(); + setTimeout(()=>{if(activeFrameyId===id&&byId('frameyPanelStatus').textContent==='Loading plugin panel…') + byId('frameyPanelStatus').textContent='No compatible API v2 panel loaded. This plugin may need Steam UI features Framey cannot provide.';},12000); +} +byId('frameyClose').onclick=()=>{activeFrameyId=null;byId('frameyFrame').removeAttribute('src');byId('frameyPanel').hidden=true;}; +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:ready'){byId('frameyPanelStatus').textContent='Plugin panel ready'; + api('/api/framey/check',{kind:'panel',pluginId:activeFrameyId}).catch(()=>{});return;} + if(req.type==='framey:input'){api('/api/framey/check',{kind:'input'}).catch(()=>{});return;} + if(req.type==='framey:error'){byId('frameyPanelStatus').textContent=`Plugin panel unavailable: ${String(req.message).slice(0,200)}`;return;} + if(req.type!=='framey:call'||!Number.isSafeInteger(req.id)||req.id<1||typeof req.method!=='string'||!Array.isArray(req.args))return; + const pluginId=activeFrameyId,reply={type:'framey:reply',id:req.id}; + try{const data=await api('/api/framey/method',{id:pluginId,method:req.method,args:req.args});reply.result=data.result;} + catch(error){reply.error=error.message;} + if(pluginId===activeFrameyId&&event.source===byId('frameyFrame').contentWindow)event.source.postMessage(reply,'*'); +}); +window.addEventListener('pointerup',event=>{if(event.isTrusted) + api('/api/framey/check',{kind:'input'}).catch(()=>{});},{passive:true}); async function refreshRecord(){try{const data=await api('/api/record'); byId('record').textContent=['recording','starting'].includes(data.state)?'Stop recording':'Start recording'; byId('record').disabled=recordBusy||['stopping','saving'].includes(data.state); diff --git a/ui/server.py b/ui/server.py index 18c8cc8..f775bc0 100755 --- a/ui/server.py +++ b/ui/server.py @@ -81,6 +81,7 @@ import frame_compat_db # noqa: E402 import frame_extensions # noqa: E402 import frame_decky # noqa: E402 import frame_decky_store # noqa: E402 +import framey_panel # noqa: E402 import frame_headset # noqa: E402 frame_host.trust_bundled_cas() @@ -94,6 +95,7 @@ HEADSET_RUNTIME = LOCAL and os.environ.get("FRAME_HEADSET_RUNTIME") == "1" if LOCAL: os.environ["PATH"] = f"{HERE / 'local-bin'}{os.pathsep}{os.environ.get('PATH', '')}" UI_KEY = os.environ.get("FRAME_UI_KEY") or "1" +FRAMEY_CHECK_LOCK = threading.Lock() DEVICE = os.environ.get("FRAME_DEVICE") or "phone" # What the Frame's KDE Connect calls this device (keyboard and trackpad). INPUT_NAME = DEVICE if LOCAL else socket.gethostname().split(".")[0] @@ -573,6 +575,39 @@ def decky_plugin(body): lambda: frame_decky_store.change(ssh, action, plugin_id)) +def framey_method_action(body): + if not HEADSET_RUNTIME and os.environ.get('FRAME_E2E_CONTAINER') != '1': + raise Failure('Decky plugin panels run in the headset service', 400) + try: + return framey_panel.call_method(ssh, body.get('id'), body.get('method'), body.get('args')) + except framey_panel.PanelError as exc: + raise Failure(str(exc), 400) from exc + + +def framey_check_action(body): + """Record only a live, explicitly started worn-headset check.""" + if not HEADSET_RUNTIME or body.get('kind') not in ('input', 'panel'): + raise Failure('No headset interaction check is active', 400) + path = Path.home()/'.local/share/frame-control/headset/worn-check.json' + with FRAMEY_CHECK_LOCK: + try: + state = json.loads(path.read_text()) + except (OSError, ValueError): + raise Failure('No headset interaction check is active', 409) + started = state.get('started') + if (not isinstance(state.get('nonce'), str) or not isinstance(started, (int, float)) or + not 0 <= time.time() - started <= 90): + raise Failure('The headset interaction check expired', 409) + state[body['kind']] = {'at':time.time(), 'pluginId':body.get('pluginId') if body['kind']=='panel' else None} + next_path = path.with_name('worn-check-' + secrets.token_hex(4) + '.next') + try: + next_path.write_text(json.dumps(state)+'\n') + next_path.replace(path) + finally: + next_path.unlink(missing_ok=True) + return {'recorded':True} + + def headset_action(body): if LOCAL: raise Failure("Manage the headset panel from Frame Control on your computer", 400) @@ -3414,6 +3449,8 @@ POST = { "/api/android/backup": android_backup, "/api/backup": backup_action, "/api/decky": decky, "/api/decky/plugin": decky_plugin, + "/api/framey/method": framey_method_action, + "/api/framey/check": framey_check_action, "/api/headset": headset_action, "/api/headset/quick": headset_quick, "/api/vr": vr, @@ -3562,7 +3599,7 @@ class Handler(BaseHTTPRequestHandler): return False return True - def send_bytes(self, data, ctype, status=200, headers=(), extension=False): + def send_bytes(self, data, ctype, status=200, headers=(), extension=False, framey=False): self.send_response(status) self.send_header("Content-Type", ctype) for name, value in headers: @@ -3571,7 +3608,9 @@ class Handler(BaseHTTPRequestHandler): self.send_header("Cache-Control", "no-store") # Nobody may frame the UI (clickjacking). self.send_header("X-Content-Type-Options", "nosniff") - if extension: + if framey: + self.send_header("Content-Security-Policy", "default-src 'none'; script-src http://127.0.0.1:* http://localhost:* blob:; style-src 'unsafe-inline'; img-src https: data:; connect-src https:; frame-ancestors 'self'; base-uri 'none'; form-action 'none'") + elif extension: self.send_header("Content-Security-Policy", "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'self'; base-uri 'none'; form-action 'none'") else: self.send_header("X-Frame-Options", "DENY") @@ -3623,6 +3662,21 @@ class Handler(BaseHTTPRequestHandler): self.send_json(frame_decky.status(ssh)) elif path == "/api/decky/store": self.send_json({'plugins': frame_decky_store.catalog()}) + elif path.startswith('/framey/panel/'): + try: + plugin_id = int(path.rsplit('/', 1)[1]) + self.send_bytes(framey_panel.panel(plugin_id), 'text/html; charset=utf-8', framey=True) + except (ValueError, FileNotFoundError, OSError): + self.send_json({'error':'Framey panel unavailable'}, 404) + elif path.startswith('/framey-runtime/'): + files = {'react.js':'vendor/react-18.3.1.min.js', + 'react-dom.js':'vendor/react-dom-18.3.1.min.js', + 'host.js':'framey-host.js'} + name = files.get(path.rsplit('/',1)[-1]) + if not name: + self.send_json({'error':'not found'}, 404) + else: + self.send_bytes((HERE/name).read_bytes(), 'text/javascript; charset=utf-8', framey=True) elif path == "/api/headset": if LOCAL: raise Failure("Manage this panel from Frame Control on your computer", 400) diff --git a/ui/vendor/REACT-DOM-LICENSE.txt b/ui/vendor/REACT-DOM-LICENSE.txt new file mode 100644 index 0000000..b96dcb0 --- /dev/null +++ b/ui/vendor/REACT-DOM-LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ui/vendor/REACT-LICENSE.txt b/ui/vendor/REACT-LICENSE.txt new file mode 100644 index 0000000..b96dcb0 --- /dev/null +++ b/ui/vendor/REACT-LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ui/vendor/README.md b/ui/vendor/README.md new file mode 100644 index 0000000..d154715 --- /dev/null +++ b/ui/vendor/README.md @@ -0,0 +1,9 @@ +Framey bundles the production UMD files from the published npm packages +`react@18.3.1` and `react-dom@18.3.1` for offline headset use. The upstream +package tarballs were downloaded with `npm pack` on 25 October 2026. + +- `react-18.3.1.tgz`: npm SHA-1 `49ab892009c53933625bd16b2533fc754cab2891` +- `react-dom-18.3.1.tgz`: npm SHA-1 `c2265d79511b57d479b3dd3fdfa51536494c5cb4` + +Both packages use the MIT license. Their license texts are alongside the +bundled JavaScript files and are included in the headset app bundle. diff --git a/ui/vendor/react-18.3.1.min.js b/ui/vendor/react-18.3.1.min.js new file mode 100644 index 0000000..9f467ca --- /dev/null +++ b/ui/vendor/react-18.3.1.min.js @@ -0,0 +1,31 @@ +/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +(function(){'use strict';(function(c,x){"object"===typeof exports&&"undefined"!==typeof module?x(exports):"function"===typeof define&&define.amd?define(["exports"],x):(c=c||self,x(c.React={}))})(this,function(c){function x(a){if(null===a||"object"!==typeof a)return null;a=V&&a[V]||a["@@iterator"];return"function"===typeof a?a:null}function w(a,b,e){this.props=a;this.context=b;this.refs=W;this.updater=e||X}function Y(){}function K(a,b,e){this.props=a;this.context=b;this.refs=W;this.updater=e||X}function Z(a,b, +e){var m,d={},c=null,h=null;if(null!=b)for(m in void 0!==b.ref&&(h=b.ref),void 0!==b.key&&(c=""+b.key),b)aa.call(b,m)&&!ba.hasOwnProperty(m)&&(d[m]=b[m]);var l=arguments.length-2;if(1===l)d.children=e;else if(1>>1,d=a[c];if(0>>1;cD(l,e))fD(g,l)?(a[c]=g,a[f]=e,c=f):(a[c]=l,a[h]=e,c=h);else if(fD(g,e))a[c]=g,a[f]=e,c=f;else break a}}return b} +function D(a,b){var c=a.sortIndex-b.sortIndex;return 0!==c?c:a.id-b.id}function P(a){for(var b=p(r);null!==b;){if(null===b.callback)E(r);else if(b.startTime<=a)E(r),b.sortIndex=b.expirationTime,O(q,b);else break;b=p(r)}}function Q(a){z=!1;P(a);if(!u)if(null!==p(q))u=!0,R(S);else{var b=p(r);null!==b&&T(Q,b.startTime-a)}}function S(a,b){u=!1;z&&(z=!1,ea(A),A=-1);F=!0;var c=k;try{P(b);for(n=p(q);null!==n&&(!(n.expirationTime>b)||a&&!fa());){var m=n.callback;if("function"===typeof m){n.callback=null; +k=n.priorityLevel;var d=m(n.expirationTime<=b);b=v();"function"===typeof d?n.callback=d:n===p(q)&&E(q);P(b)}else E(q);n=p(q)}if(null!==n)var g=!0;else{var h=p(r);null!==h&&T(Q,h.startTime-b);g=!1}return g}finally{n=null,k=c,F=!1}}function fa(){return v()-hae?(a.sortIndex=c,O(r,a),null===p(q)&&a===p(r)&&(z?(ea(A),A=-1):z=!0,T(Q,c-e))):(a.sortIndex=d,O(q,a),u||F||(u=!0,R(S)));return a},unstable_cancelCallback:function(a){a.callback=null},unstable_wrapCallback:function(a){var b= +k;return function(){var c=k;k=b;try{return a.apply(this,arguments)}finally{k=c}}},unstable_getCurrentPriorityLevel:function(){return k},unstable_shouldYield:fa,unstable_requestPaint:function(){},unstable_continueExecution:function(){u||F||(u=!0,R(S))},unstable_pauseExecution:function(){},unstable_getFirstCallbackNode:function(){return p(q)},get unstable_now(){return v},unstable_forceFrameRate:function(a){0>a||125b}return!1}function Y(a,b,c,d,e,f,g){this.acceptsBooleans=2===b||3===b||4===b;this.attributeName=d;this.attributeNamespace=e;this.mustUseProperty=c;this.propertyName=a;this.type=b;this.sanitizeURL=f;this.removeEmptyString=g}function $d(a,b,c,d){var e=R.hasOwnProperty(b)?R[b]:null;if(null!==e?0!==e.type:d||!(2h||e[g]!==f[h]){var k="\n"+e[g].replace(" at new "," at ");a.displayName&&k.includes("")&&(k=k.replace("",a.displayName));return k}while(1<=g&&0<=h)}break}}}finally{ce=!1,Error.prepareStackTrace=c}return(a=a?a.displayName||a.name:"")?bc(a): +""}function fj(a){switch(a.tag){case 5:return bc(a.type);case 16:return bc("Lazy");case 13:return bc("Suspense");case 19:return bc("SuspenseList");case 0:case 2:case 15:return a=be(a.type,!1),a;case 11:return a=be(a.type.render,!1),a;case 1:return a=be(a.type,!0),a;default:return""}}function de(a){if(null==a)return null;if("function"===typeof a)return a.displayName||a.name||null;if("string"===typeof a)return a;switch(a){case Bb:return"Fragment";case Cb:return"Portal";case ee:return"Profiler";case fe:return"StrictMode"; +case ge:return"Suspense";case he:return"SuspenseList"}if("object"===typeof a)switch(a.$$typeof){case gg:return(a.displayName||"Context")+".Consumer";case hg:return(a._context.displayName||"Context")+".Provider";case ie:var b=a.render;a=a.displayName;a||(a=b.displayName||b.name||"",a=""!==a?"ForwardRef("+a+")":"ForwardRef");return a;case je:return b=a.displayName||null,null!==b?b:de(a.type)||"Memo";case Ta:b=a._payload;a=a._init;try{return de(a(b))}catch(c){}}return null}function gj(a){var b=a.type; +switch(a.tag){case 24:return"Cache";case 9:return(b.displayName||"Context")+".Consumer";case 10:return(b._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return a=b.render,a=a.displayName||a.name||"",b.displayName||(""!==a?"ForwardRef("+a+")":"ForwardRef");case 7:return"Fragment";case 5:return b;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return de(b);case 8:return b===fe?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler"; +case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if("function"===typeof b)return b.displayName||b.name||null;if("string"===typeof b)return b}return null}function Ua(a){switch(typeof a){case "boolean":case "number":case "string":case "undefined":return a;case "object":return a;default:return""}}function ig(a){var b=a.type;return(a=a.nodeName)&&"input"===a.toLowerCase()&&("checkbox"===b||"radio"=== +b)}function hj(a){var b=ig(a)?"checked":"value",c=Object.getOwnPropertyDescriptor(a.constructor.prototype,b),d=""+a[b];if(!a.hasOwnProperty(b)&&"undefined"!==typeof c&&"function"===typeof c.get&&"function"===typeof c.set){var e=c.get,f=c.set;Object.defineProperty(a,b,{configurable:!0,get:function(){return e.call(this)},set:function(a){d=""+a;f.call(this,a)}});Object.defineProperty(a,b,{enumerable:c.enumerable});return{getValue:function(){return d},setValue:function(a){d=""+a},stopTracking:function(){a._valueTracker= +null;delete a[b]}}}}function Pc(a){a._valueTracker||(a._valueTracker=hj(a))}function jg(a){if(!a)return!1;var b=a._valueTracker;if(!b)return!0;var c=b.getValue();var d="";a&&(d=ig(a)?a.checked?"true":"false":a.value);a=d;return a!==c?(b.setValue(a),!0):!1}function Qc(a){a=a||("undefined"!==typeof document?document:void 0);if("undefined"===typeof a)return null;try{return a.activeElement||a.body}catch(b){return a.body}}function ke(a,b){var c=b.checked;return E({},b,{defaultChecked:void 0,defaultValue:void 0, +value:void 0,checked:null!=c?c:a._wrapperState.initialChecked})}function kg(a,b){var c=null==b.defaultValue?"":b.defaultValue,d=null!=b.checked?b.checked:b.defaultChecked;c=Ua(null!=b.value?b.value:c);a._wrapperState={initialChecked:d,initialValue:c,controlled:"checkbox"===b.type||"radio"===b.type?null!=b.checked:null!=b.value}}function lg(a,b){b=b.checked;null!=b&&$d(a,"checked",b,!1)}function le(a,b){lg(a,b);var c=Ua(b.value),d=b.type;if(null!=c)if("number"===d){if(0===c&&""===a.value||a.value!= +c)a.value=""+c}else a.value!==""+c&&(a.value=""+c);else if("submit"===d||"reset"===d){a.removeAttribute("value");return}b.hasOwnProperty("value")?me(a,b.type,c):b.hasOwnProperty("defaultValue")&&me(a,b.type,Ua(b.defaultValue));null==b.checked&&null!=b.defaultChecked&&(a.defaultChecked=!!b.defaultChecked)}function mg(a,b,c){if(b.hasOwnProperty("value")||b.hasOwnProperty("defaultValue")){var d=b.type;if(!("submit"!==d&&"reset"!==d||void 0!==b.value&&null!==b.value))return;b=""+a._wrapperState.initialValue; +c||b===a.value||(a.value=b);a.defaultValue=b}c=a.name;""!==c&&(a.name="");a.defaultChecked=!!a._wrapperState.initialChecked;""!==c&&(a.name=c)}function me(a,b,c){if("number"!==b||Qc(a.ownerDocument)!==a)null==c?a.defaultValue=""+a._wrapperState.initialValue:a.defaultValue!==""+c&&(a.defaultValue=""+c)}function Db(a,b,c,d){a=a.options;if(b){b={};for(var e=0;e>>=0;return 0===a?32:31-(qj(a)/rj|0)|0}function hc(a){switch(a&-a){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return a& +4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return a&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return a}}function Vc(a,b){var c=a.pendingLanes;if(0===c)return 0;var d=0,e=a.suspendedLanes,f=a.pingedLanes,g=c&268435455;if(0!==g){var h=g&~e;0!==h?d=hc(h):(f&=g,0!==f&&(d=hc(f)))}else g=c&~e,0!==g?d=hc(g):0!==f&&(d=hc(f));if(0===d)return 0;if(0!==b&&b!==d&&0===(b&e)&& +(e=d&-d,f=b&-b,e>=f||16===e&&0!==(f&4194240)))return b;0!==(d&4)&&(d|=c&16);b=a.entangledLanes;if(0!==b)for(a=a.entanglements,b&=d;0c;c++)b.push(a); +return b}function ic(a,b,c){a.pendingLanes|=b;536870912!==b&&(a.suspendedLanes=0,a.pingedLanes=0);a=a.eventTimes;b=31-ta(b);a[b]=c}function uj(a,b){var c=a.pendingLanes&~b;a.pendingLanes=b;a.suspendedLanes=0;a.pingedLanes=0;a.expiredLanes&=b;a.mutableReadLanes&=b;a.entangledLanes&=b;b=a.entanglements;var d=a.eventTimes;for(a=a.expirationTimes;0=b)return{node:c,offset:b-a};a=d}a:{for(;c;){if(c.nextSibling){c=c.nextSibling;break a}c=c.parentNode}c=void 0}c=$g(c)}}function bh(a,b){return a&&b?a===b?!0:a&&3===a.nodeType?!1:b&&3===b.nodeType?bh(a,b.parentNode):"contains"in a?a.contains(b):a.compareDocumentPosition?!!(a.compareDocumentPosition(b)&16):!1:!1}function ch(){for(var a=window,b=Qc();b instanceof a.HTMLIFrameElement;){try{var c="string"===typeof b.contentWindow.location.href}catch(d){c=!1}if(c)a=b.contentWindow;else break; +b=Qc(a.document)}return b}function Ie(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return b&&("input"===b&&("text"===a.type||"search"===a.type||"tel"===a.type||"url"===a.type||"password"===a.type)||"textarea"===b||"true"===a.contentEditable)}function Tj(a){var b=ch(),c=a.focusedElem,d=a.selectionRange;if(b!==c&&c&&c.ownerDocument&&bh(c.ownerDocument.documentElement,c)){if(null!==d&&Ie(c))if(b=d.start,a=d.end,void 0===a&&(a=b),"selectionStart"in c)c.selectionStart=b,c.selectionEnd=Math.min(a,c.value.length); +else if(a=(b=c.ownerDocument||document)&&b.defaultView||window,a.getSelection){a=a.getSelection();var e=c.textContent.length,f=Math.min(d.start,e);d=void 0===d.end?f:Math.min(d.end,e);!a.extend&&f>d&&(e=d,d=f,f=e);e=ah(c,f);var g=ah(c,d);e&&g&&(1!==a.rangeCount||a.anchorNode!==e.node||a.anchorOffset!==e.offset||a.focusNode!==g.node||a.focusOffset!==g.offset)&&(b=b.createRange(),b.setStart(e.node,e.offset),a.removeAllRanges(),f>d?(a.addRange(b),a.extend(g.node,g.offset)):(b.setEnd(g.node,g.offset), +a.addRange(b)))}b=[];for(a=c;a=a.parentNode;)1===a.nodeType&&b.push({element:a,left:a.scrollLeft,top:a.scrollTop});"function"===typeof c.focus&&c.focus();for(c=0;cMb||(a.current=Se[Mb],Se[Mb]=null,Mb--)} +function y(a,b,c){Mb++;Se[Mb]=a.current;a.current=b}function Nb(a,b){var c=a.type.contextTypes;if(!c)return cb;var d=a.stateNode;if(d&&d.__reactInternalMemoizedUnmaskedChildContext===b)return d.__reactInternalMemoizedMaskedChildContext;var e={},f;for(f in c)e[f]=b[f];d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=b,a.__reactInternalMemoizedMaskedChildContext=e);return e}function ea(a){a=a.childContextTypes;return null!==a&&void 0!==a}function th(a,b,c){if(J.current!==cb)throw Error(m(168)); +y(J,b);y(S,c)}function uh(a,b,c){var d=a.stateNode;b=b.childContextTypes;if("function"!==typeof d.getChildContext)return c;d=d.getChildContext();for(var e in d)if(!(e in b))throw Error(m(108,gj(a)||"Unknown",e));return E({},c,d)}function ld(a){a=(a=a.stateNode)&&a.__reactInternalMemoizedMergedChildContext||cb;pb=J.current;y(J,a);y(S,S.current);return!0}function vh(a,b,c){var d=a.stateNode;if(!d)throw Error(m(169));c?(a=uh(a,b,pb),d.__reactInternalMemoizedMergedChildContext=a,v(S),v(J),y(J,a)):v(S); +y(S,c)}function wh(a){null===La?La=[a]:La.push(a)}function jk(a){md=!0;wh(a)}function db(){if(!Te&&null!==La){Te=!0;var a=0,b=z;try{var c=La;for(z=1;a>=g;e-=g;Ma=1<<32-ta(b)+e|c<t?(q=l,l=null):q=l.sibling;var A=r(e,l,h[t],k);if(null===A){null===l&&(l=q);break}a&&l&&null===A.alternate&&b(e,l);g=f(A,g,t);null===m?n=A:m.sibling=A;m=A;l=q}if(t===h.length)return c(e,l),D&&qb(e,t),n;if(null===l){for(;t< +h.length;t++)l=u(e,h[t],k),null!==l&&(g=f(l,g,t),null===m?n=l:m.sibling=l,m=l);D&&qb(e,t);return n}for(l=d(e,l);tt?(A=q,q=null):A=q.sibling;var x=r(e,q,w.value,k);if(null===x){null===q&&(q=A);break}a&&q&&null===x.alternate&&b(e,q);g=f(x,g,t);null===l?n=x:l.sibling=x;l=x;q=A}if(w.done)return c(e,q),D&&qb(e,t),n;if(null===q){for(;!w.done;t++,w=h.next())w=u(e,w.value,k),null!==w&&(g=f(w,g,t),null===l?n=w:l.sibling=w,l=w);D&&qb(e,t);return n}for(q=d(e,q);!w.done;t++,w=h.next())w=p(q,e,t,w.value,k),null!==w&&(a&&null!==w.alternate&&q.delete(null===w.key?t:w.key),g=f(w,g,t),null===l?n=w:l.sibling= +w,l=w);a&&q.forEach(function(a){return b(e,a)});D&&qb(e,t);return n}function v(a,d,f,h){"object"===typeof f&&null!==f&&f.type===Bb&&null===f.key&&(f=f.props.children);if("object"===typeof f&&null!==f){switch(f.$$typeof){case sd:a:{for(var k=f.key,n=d;null!==n;){if(n.key===k){k=f.type;if(k===Bb){if(7===n.tag){c(a,n.sibling);d=e(n,f.props.children);d.return=a;a=d;break a}}else if(n.elementType===k||"object"===typeof k&&null!==k&&k.$$typeof===Ta&&Ch(k)===n.type){c(a,n.sibling);d=e(n,f.props);d.ref=vc(a, +n,f);d.return=a;a=d;break a}c(a,n);break}else b(a,n);n=n.sibling}f.type===Bb?(d=sb(f.props.children,a.mode,h,f.key),d.return=a,a=d):(h=rd(f.type,f.key,f.props,null,a.mode,h),h.ref=vc(a,d,f),h.return=a,a=h)}return g(a);case Cb:a:{for(n=f.key;null!==d;){if(d.key===n)if(4===d.tag&&d.stateNode.containerInfo===f.containerInfo&&d.stateNode.implementation===f.implementation){c(a,d.sibling);d=e(d,f.children||[]);d.return=a;a=d;break a}else{c(a,d);break}else b(a,d);d=d.sibling}d=$e(f,a.mode,h);d.return=a; +a=d}return g(a);case Ta:return n=f._init,v(a,d,n(f._payload),h)}if(cc(f))return x(a,d,f,h);if(ac(f))return I(a,d,f,h);qd(a,f)}return"string"===typeof f&&""!==f||"number"===typeof f?(f=""+f,null!==d&&6===d.tag?(c(a,d.sibling),d=e(d,f),d.return=a,a=d):(c(a,d),d=Ze(f,a.mode,h),d.return=a,a=d),g(a)):c(a,d)}return v}function af(){bf=Rb=td=null}function cf(a,b){b=ud.current;v(ud);a._currentValue=b}function df(a,b,c){for(;null!==a;){var d=a.alternate;(a.childLanes&b)!==b?(a.childLanes|=b,null!==d&&(d.childLanes|= +b)):null!==d&&(d.childLanes&b)!==b&&(d.childLanes|=b);if(a===c)break;a=a.return}}function Sb(a,b){td=a;bf=Rb=null;a=a.dependencies;null!==a&&null!==a.firstContext&&(0!==(a.lanes&b)&&(ha=!0),a.firstContext=null)}function qa(a){var b=a._currentValue;if(bf!==a)if(a={context:a,memoizedValue:b,next:null},null===Rb){if(null===td)throw Error(m(308));Rb=a;td.dependencies={lanes:0,firstContext:a}}else Rb=Rb.next=a;return b}function ef(a){null===tb?tb=[a]:tb.push(a)}function Eh(a,b,c,d){var e=b.interleaved; +null===e?(c.next=c,ef(b)):(c.next=e.next,e.next=c);b.interleaved=c;return Oa(a,d)}function Oa(a,b){a.lanes|=b;var c=a.alternate;null!==c&&(c.lanes|=b);c=a;for(a=a.return;null!==a;)a.childLanes|=b,c=a.alternate,null!==c&&(c.childLanes|=b),c=a,a=a.return;return 3===c.tag?c.stateNode:null}function ff(a){a.updateQueue={baseState:a.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Fh(a,b){a=a.updateQueue;b.updateQueue===a&&(b.updateQueue= +{baseState:a.baseState,firstBaseUpdate:a.firstBaseUpdate,lastBaseUpdate:a.lastBaseUpdate,shared:a.shared,effects:a.effects})}function Pa(a,b){return{eventTime:a,lane:b,tag:0,payload:null,callback:null,next:null}}function fb(a,b,c){var d=a.updateQueue;if(null===d)return null;d=d.shared;if(0!==(p&2)){var e=d.pending;null===e?b.next=b:(b.next=e.next,e.next=b);d.pending=b;return kk(a,c)}e=d.interleaved;null===e?(b.next=b,ef(d)):(b.next=e.next,e.next=b);d.interleaved=b;return Oa(a,c)}function vd(a,b,c){b= +b.updateQueue;if(null!==b&&(b=b.shared,0!==(c&4194240))){var d=b.lanes;d&=a.pendingLanes;c|=d;b.lanes=c;xe(a,c)}}function Gh(a,b){var c=a.updateQueue,d=a.alternate;if(null!==d&&(d=d.updateQueue,c===d)){var e=null,f=null;c=c.firstBaseUpdate;if(null!==c){do{var g={eventTime:c.eventTime,lane:c.lane,tag:c.tag,payload:c.payload,callback:c.callback,next:null};null===f?e=f=g:f=f.next=g;c=c.next}while(null!==c);null===f?e=f=b:f=f.next=b}else e=f=b;c={baseState:d.baseState,firstBaseUpdate:e,lastBaseUpdate:f, +shared:d.shared,effects:d.effects};a.updateQueue=c;return}a=c.lastBaseUpdate;null===a?c.firstBaseUpdate=b:a.next=b;c.lastBaseUpdate=b}function wd(a,b,c,d){var e=a.updateQueue;gb=!1;var f=e.firstBaseUpdate,g=e.lastBaseUpdate,h=e.shared.pending;if(null!==h){e.shared.pending=null;var k=h,n=k.next;k.next=null;null===g?f=n:g.next=n;g=k;var l=a.alternate;null!==l&&(l=l.updateQueue,h=l.lastBaseUpdate,h!==g&&(null===h?l.firstBaseUpdate=n:h.next=n,l.lastBaseUpdate=k))}if(null!==f){var m=e.baseState;g=0;l= +n=k=null;h=f;do{var r=h.lane,p=h.eventTime;if((d&r)===r){null!==l&&(l=l.next={eventTime:p,lane:0,tag:h.tag,payload:h.payload,callback:h.callback,next:null});a:{var x=a,v=h;r=b;p=c;switch(v.tag){case 1:x=v.payload;if("function"===typeof x){m=x.call(p,m,r);break a}m=x;break a;case 3:x.flags=x.flags&-65537|128;case 0:x=v.payload;r="function"===typeof x?x.call(p,m,r):x;if(null===r||void 0===r)break a;m=E({},m,r);break a;case 2:gb=!0}}null!==h.callback&&0!==h.lane&&(a.flags|=64,r=e.effects,null===r?e.effects= +[h]:r.push(h))}else p={eventTime:p,lane:r,tag:h.tag,payload:h.payload,callback:h.callback,next:null},null===l?(n=l=p,k=m):l=l.next=p,g|=r;h=h.next;if(null===h)if(h=e.shared.pending,null===h)break;else r=h,h=r.next,r.next=null,e.lastBaseUpdate=r,e.shared.pending=null}while(1);null===l&&(k=m);e.baseState=k;e.firstBaseUpdate=n;e.lastBaseUpdate=l;b=e.shared.interleaved;if(null!==b){e=b;do g|=e.lane,e=e.next;while(e!==b)}else null===f&&(e.shared.lanes=0);ra|=g;a.lanes=g;a.memoizedState=m}}function Hh(a, +b,c){a=b.effects;b.effects=null;if(null!==a)for(b=0;bc?c:4;a(!0);var d=sf.transition;sf.transition= +{};try{a(!1),b()}finally{z=c,sf.transition=d}}function $h(){return sa().memoizedState}function qk(a,b,c){var d=hb(a);c={lane:d,action:c,hasEagerState:!1,eagerState:null,next:null};if(ai(a))bi(b,c);else if(c=Eh(a,b,c,d),null!==c){var e=Z();xa(c,a,d,e);ci(c,b,d)}}function ok(a,b,c){var d=hb(a),e={lane:d,action:c,hasEagerState:!1,eagerState:null,next:null};if(ai(a))bi(b,e);else{var f=a.alternate;if(0===a.lanes&&(null===f||0===f.lanes)&&(f=b.lastRenderedReducer,null!==f))try{var g=b.lastRenderedState, +h=f(g,c);e.hasEagerState=!0;e.eagerState=h;if(ua(h,g)){var k=b.interleaved;null===k?(e.next=e,ef(b)):(e.next=k.next,k.next=e);b.interleaved=e;return}}catch(n){}finally{}c=Eh(a,b,e,d);null!==c&&(e=Z(),xa(c,a,d,e),ci(c,b,d))}}function ai(a){var b=a.alternate;return a===C||null!==b&&b===C}function bi(a,b){zc=Ad=!0;var c=a.pending;null===c?b.next=b:(b.next=c.next,c.next=b);a.pending=b}function ci(a,b,c){if(0!==(c&4194240)){var d=b.lanes;d&=a.pendingLanes;c|=d;b.lanes=c;xe(a,c)}}function ya(a,b){if(a&& +a.defaultProps){b=E({},b);a=a.defaultProps;for(var c in a)void 0===b[c]&&(b[c]=a[c]);return b}return b}function tf(a,b,c,d){b=a.memoizedState;c=c(d,b);c=null===c||void 0===c?b:E({},b,c);a.memoizedState=c;0===a.lanes&&(a.updateQueue.baseState=c)}function di(a,b,c,d,e,f,g){a=a.stateNode;return"function"===typeof a.shouldComponentUpdate?a.shouldComponentUpdate(d,f,g):b.prototype&&b.prototype.isPureReactComponent?!qc(c,d)||!qc(e,f):!0}function ei(a,b,c){var d=!1,e=cb;var f=b.contextType;"object"===typeof f&& +null!==f?f=qa(f):(e=ea(b)?pb:J.current,d=b.contextTypes,f=(d=null!==d&&void 0!==d)?Nb(a,e):cb);b=new b(c,f);a.memoizedState=null!==b.state&&void 0!==b.state?b.state:null;b.updater=Dd;a.stateNode=b;b._reactInternals=a;d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=e,a.__reactInternalMemoizedMaskedChildContext=f);return b}function fi(a,b,c,d){a=b.state;"function"===typeof b.componentWillReceiveProps&&b.componentWillReceiveProps(c,d);"function"===typeof b.UNSAFE_componentWillReceiveProps&& +b.UNSAFE_componentWillReceiveProps(c,d);b.state!==a&&Dd.enqueueReplaceState(b,b.state,null)}function uf(a,b,c,d){var e=a.stateNode;e.props=c;e.state=a.memoizedState;e.refs={};ff(a);var f=b.contextType;"object"===typeof f&&null!==f?e.context=qa(f):(f=ea(b)?pb:J.current,e.context=Nb(a,f));e.state=a.memoizedState;f=b.getDerivedStateFromProps;"function"===typeof f&&(tf(a,b,f,c),e.state=a.memoizedState);"function"===typeof b.getDerivedStateFromProps||"function"===typeof e.getSnapshotBeforeUpdate||"function"!== +typeof e.UNSAFE_componentWillMount&&"function"!==typeof e.componentWillMount||(b=e.state,"function"===typeof e.componentWillMount&&e.componentWillMount(),"function"===typeof e.UNSAFE_componentWillMount&&e.UNSAFE_componentWillMount(),b!==e.state&&Dd.enqueueReplaceState(e,e.state,null),wd(a,c,e,d),e.state=a.memoizedState);"function"===typeof e.componentDidMount&&(a.flags|=4194308)}function Ub(a,b){try{var c="",d=b;do c+=fj(d),d=d.return;while(d);var e=c}catch(f){e="\nError generating stack: "+f.message+ +"\n"+f.stack}return{value:a,source:b,stack:e,digest:null}}function vf(a,b,c){return{value:a,source:null,stack:null!=c?c:null,digest:null!=b?b:null}}function wf(a,b){try{console.error(b.value)}catch(c){setTimeout(function(){throw c;})}}function gi(a,b,c){c=Pa(-1,c);c.tag=3;c.payload={element:null};var d=b.value;c.callback=function(){Ed||(Ed=!0,xf=d);wf(a,b)};return c}function hi(a,b,c){c=Pa(-1,c);c.tag=3;var d=a.type.getDerivedStateFromError;if("function"===typeof d){var e=b.value;c.payload=function(){return d(e)}; +c.callback=function(){wf(a,b)}}var f=a.stateNode;null!==f&&"function"===typeof f.componentDidCatch&&(c.callback=function(){wf(a,b);"function"!==typeof d&&(null===ib?ib=new Set([this]):ib.add(this));var c=b.stack;this.componentDidCatch(b.value,{componentStack:null!==c?c:""})});return c}function ii(a,b,c){var d=a.pingCache;if(null===d){d=a.pingCache=new rk;var e=new Set;d.set(b,e)}else e=d.get(b),void 0===e&&(e=new Set,d.set(b,e));e.has(c)||(e.add(c),a=sk.bind(null,a,b,c),b.then(a,a))}function ji(a){do{var b; +if(b=13===a.tag)b=a.memoizedState,b=null!==b?null!==b.dehydrated?!0:!1:!0;if(b)return a;a=a.return}while(null!==a);return null}function ki(a,b,c,d,e){if(0===(a.mode&1))return a===b?a.flags|=65536:(a.flags|=128,c.flags|=131072,c.flags&=-52805,1===c.tag&&(null===c.alternate?c.tag=17:(b=Pa(-1,1),b.tag=2,fb(c,b,1))),c.lanes|=1),a;a.flags|=65536;a.lanes=e;return a}function aa(a,b,c,d){b.child=null===a?li(b,null,c,d):Vb(b,a.child,c,d)}function mi(a,b,c,d,e){c=c.render;var f=b.ref;Sb(b,e);d=mf(a,b,c,d,f, +e);c=nf();if(null!==a&&!ha)return b.updateQueue=a.updateQueue,b.flags&=-2053,a.lanes&=~e,Qa(a,b,e);D&&c&&Ue(b);b.flags|=1;aa(a,b,d,e);return b.child}function ni(a,b,c,d,e){if(null===a){var f=c.type;if("function"===typeof f&&!yf(f)&&void 0===f.defaultProps&&null===c.compare&&void 0===c.defaultProps)return b.tag=15,b.type=f,oi(a,b,f,d,e);a=rd(c.type,null,d,b,b.mode,e);a.ref=b.ref;a.return=b;return b.child=a}f=a.child;if(0===(a.lanes&e)){var g=f.memoizedProps;c=c.compare;c=null!==c?c:qc;if(c(g,d)&&a.ref=== +b.ref)return Qa(a,b,e)}b.flags|=1;a=eb(f,d);a.ref=b.ref;a.return=b;return b.child=a}function oi(a,b,c,d,e){if(null!==a){var f=a.memoizedProps;if(qc(f,d)&&a.ref===b.ref)if(ha=!1,b.pendingProps=d=f,0!==(a.lanes&e))0!==(a.flags&131072)&&(ha=!0);else return b.lanes=a.lanes,Qa(a,b,e)}return zf(a,b,c,d,e)}function pi(a,b,c){var d=b.pendingProps,e=d.children,f=null!==a?a.memoizedState:null;if("hidden"===d.mode)if(0===(b.mode&1))b.memoizedState={baseLanes:0,cachePool:null,transitions:null},y(Ga,ba),ba|=c; +else{if(0===(c&1073741824))return a=null!==f?f.baseLanes|c:c,b.lanes=b.childLanes=1073741824,b.memoizedState={baseLanes:a,cachePool:null,transitions:null},b.updateQueue=null,y(Ga,ba),ba|=a,null;b.memoizedState={baseLanes:0,cachePool:null,transitions:null};d=null!==f?f.baseLanes:c;y(Ga,ba);ba|=d}else null!==f?(d=f.baseLanes|c,b.memoizedState=null):d=c,y(Ga,ba),ba|=d;aa(a,b,e,c);return b.child}function qi(a,b){var c=b.ref;if(null===a&&null!==c||null!==a&&a.ref!==c)b.flags|=512,b.flags|=2097152}function zf(a, +b,c,d,e){var f=ea(c)?pb:J.current;f=Nb(b,f);Sb(b,e);c=mf(a,b,c,d,f,e);d=nf();if(null!==a&&!ha)return b.updateQueue=a.updateQueue,b.flags&=-2053,a.lanes&=~e,Qa(a,b,e);D&&d&&Ue(b);b.flags|=1;aa(a,b,c,e);return b.child}function ri(a,b,c,d,e){if(ea(c)){var f=!0;ld(b)}else f=!1;Sb(b,e);if(null===b.stateNode)Fd(a,b),ei(b,c,d),uf(b,c,d,e),d=!0;else if(null===a){var g=b.stateNode,h=b.memoizedProps;g.props=h;var k=g.context,n=c.contextType;"object"===typeof n&&null!==n?n=qa(n):(n=ea(c)?pb:J.current,n=Nb(b, +n));var l=c.getDerivedStateFromProps,m="function"===typeof l||"function"===typeof g.getSnapshotBeforeUpdate;m||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==d||k!==n)&&fi(b,g,d,n);gb=!1;var r=b.memoizedState;g.state=r;wd(b,d,g,e);k=b.memoizedState;h!==d||r!==k||S.current||gb?("function"===typeof l&&(tf(b,c,l,d),k=b.memoizedState),(h=gb||di(b,c,h,d,r,k,n))?(m||"function"!==typeof g.UNSAFE_componentWillMount&&"function"!==typeof g.componentWillMount|| +("function"===typeof g.componentWillMount&&g.componentWillMount(),"function"===typeof g.UNSAFE_componentWillMount&&g.UNSAFE_componentWillMount()),"function"===typeof g.componentDidMount&&(b.flags|=4194308)):("function"===typeof g.componentDidMount&&(b.flags|=4194308),b.memoizedProps=d,b.memoizedState=k),g.props=d,g.state=k,g.context=n,d=h):("function"===typeof g.componentDidMount&&(b.flags|=4194308),d=!1)}else{g=b.stateNode;Fh(a,b);h=b.memoizedProps;n=b.type===b.elementType?h:ya(b.type,h);g.props= +n;m=b.pendingProps;r=g.context;k=c.contextType;"object"===typeof k&&null!==k?k=qa(k):(k=ea(c)?pb:J.current,k=Nb(b,k));var p=c.getDerivedStateFromProps;(l="function"===typeof p||"function"===typeof g.getSnapshotBeforeUpdate)||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==m||r!==k)&&fi(b,g,d,k);gb=!1;r=b.memoizedState;g.state=r;wd(b,d,g,e);var x=b.memoizedState;h!==m||r!==x||S.current||gb?("function"===typeof p&&(tf(b,c,p,d),x=b.memoizedState), +(n=gb||di(b,c,n,d,r,x,k)||!1)?(l||"function"!==typeof g.UNSAFE_componentWillUpdate&&"function"!==typeof g.componentWillUpdate||("function"===typeof g.componentWillUpdate&&g.componentWillUpdate(d,x,k),"function"===typeof g.UNSAFE_componentWillUpdate&&g.UNSAFE_componentWillUpdate(d,x,k)),"function"===typeof g.componentDidUpdate&&(b.flags|=4),"function"===typeof g.getSnapshotBeforeUpdate&&(b.flags|=1024)):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|= +4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=1024),b.memoizedProps=d,b.memoizedState=x),g.props=d,g.state=x,g.context=k,d=n):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=1024),d=!1)}return Af(a,b,c,d,f,e)}function Af(a,b,c,d,e,f){qi(a,b);var g=0!==(b.flags&128);if(!d&&!g)return e&&vh(b,c,!1), +Qa(a,b,f);d=b.stateNode;tk.current=b;var h=g&&"function"!==typeof c.getDerivedStateFromError?null:d.render();b.flags|=1;null!==a&&g?(b.child=Vb(b,a.child,null,f),b.child=Vb(b,null,h,f)):aa(a,b,h,f);b.memoizedState=d.state;e&&vh(b,c,!0);return b.child}function si(a){var b=a.stateNode;b.pendingContext?th(a,b.pendingContext,b.pendingContext!==b.context):b.context&&th(a,b.context,!1);gf(a,b.containerInfo)}function ti(a,b,c,d,e){Qb();Ye(e);b.flags|=256;aa(a,b,c,d);return b.child}function Bf(a){return{baseLanes:a, +cachePool:null,transitions:null}}function ui(a,b,c){var d=b.pendingProps,e=F.current,f=!1,g=0!==(b.flags&128),h;(h=g)||(h=null!==a&&null===a.memoizedState?!1:0!==(e&2));if(h)f=!0,b.flags&=-129;else if(null===a||null!==a.memoizedState)e|=1;y(F,e&1);if(null===a){Xe(b);a=b.memoizedState;if(null!==a&&(a=a.dehydrated,null!==a))return 0===(b.mode&1)?b.lanes=1:"$!"===a.data?b.lanes=8:b.lanes=1073741824,null;g=d.children;a=d.fallback;return f?(d=b.mode,f=b.child,g={mode:"hidden",children:g},0===(d&1)&&null!== +f?(f.childLanes=0,f.pendingProps=g):f=Gd(g,d,0,null),a=sb(a,d,c,null),f.return=b,a.return=b,f.sibling=a,b.child=f,b.child.memoizedState=Bf(c),b.memoizedState=Cf,a):Df(b,g)}e=a.memoizedState;if(null!==e&&(h=e.dehydrated,null!==h))return uk(a,b,g,d,h,e,c);if(f){f=d.fallback;g=b.mode;e=a.child;h=e.sibling;var k={mode:"hidden",children:d.children};0===(g&1)&&b.child!==e?(d=b.child,d.childLanes=0,d.pendingProps=k,b.deletions=null):(d=eb(e,k),d.subtreeFlags=e.subtreeFlags&14680064);null!==h?f=eb(h,f):(f= +sb(f,g,c,null),f.flags|=2);f.return=b;d.return=b;d.sibling=f;b.child=d;d=f;f=b.child;g=a.child.memoizedState;g=null===g?Bf(c):{baseLanes:g.baseLanes|c,cachePool:null,transitions:g.transitions};f.memoizedState=g;f.childLanes=a.childLanes&~c;b.memoizedState=Cf;return d}f=a.child;a=f.sibling;d=eb(f,{mode:"visible",children:d.children});0===(b.mode&1)&&(d.lanes=c);d.return=b;d.sibling=null;null!==a&&(c=b.deletions,null===c?(b.deletions=[a],b.flags|=16):c.push(a));b.child=d;b.memoizedState=null;return d} +function Df(a,b,c){b=Gd({mode:"visible",children:b},a.mode,0,null);b.return=a;return a.child=b}function Hd(a,b,c,d){null!==d&&Ye(d);Vb(b,a.child,null,c);a=Df(b,b.pendingProps.children);a.flags|=2;b.memoizedState=null;return a}function uk(a,b,c,d,e,f,g){if(c){if(b.flags&256)return b.flags&=-257,d=vf(Error(m(422))),Hd(a,b,g,d);if(null!==b.memoizedState)return b.child=a.child,b.flags|=128,null;f=d.fallback;e=b.mode;d=Gd({mode:"visible",children:d.children},e,0,null);f=sb(f,e,g,null);f.flags|=2;d.return= +b;f.return=b;d.sibling=f;b.child=d;0!==(b.mode&1)&&Vb(b,a.child,null,g);b.child.memoizedState=Bf(g);b.memoizedState=Cf;return f}if(0===(b.mode&1))return Hd(a,b,g,null);if("$!"===e.data){d=e.nextSibling&&e.nextSibling.dataset;if(d)var h=d.dgst;d=h;f=Error(m(419));d=vf(f,d,void 0);return Hd(a,b,g,d)}h=0!==(g&a.childLanes);if(ha||h){d=O;if(null!==d){switch(g&-g){case 4:e=2;break;case 16:e=8;break;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:e= +32;break;case 536870912:e=268435456;break;default:e=0}e=0!==(e&(d.suspendedLanes|g))?0:e;0!==e&&e!==f.retryLane&&(f.retryLane=e,Oa(a,e),xa(d,a,e,-1))}Ef();d=vf(Error(m(421)));return Hd(a,b,g,d)}if("$?"===e.data)return b.flags|=128,b.child=a.child,b=vk.bind(null,a),e._reactRetry=b,null;a=f.treeContext;fa=Ka(e.nextSibling);la=b;D=!0;wa=null;null!==a&&(na[oa++]=Ma,na[oa++]=Na,na[oa++]=rb,Ma=a.id,Na=a.overflow,rb=b);b=Df(b,d.children);b.flags|=4096;return b}function vi(a,b,c){a.lanes|=b;var d=a.alternate; +null!==d&&(d.lanes|=b);df(a.return,b,c)}function Ff(a,b,c,d,e){var f=a.memoizedState;null===f?a.memoizedState={isBackwards:b,rendering:null,renderingStartTime:0,last:d,tail:c,tailMode:e}:(f.isBackwards=b,f.rendering=null,f.renderingStartTime=0,f.last=d,f.tail=c,f.tailMode=e)}function wi(a,b,c){var d=b.pendingProps,e=d.revealOrder,f=d.tail;aa(a,b,d.children,c);d=F.current;if(0!==(d&2))d=d&1|2,b.flags|=128;else{if(null!==a&&0!==(a.flags&128))a:for(a=b.child;null!==a;){if(13===a.tag)null!==a.memoizedState&& +vi(a,c,b);else if(19===a.tag)vi(a,c,b);else if(null!==a.child){a.child.return=a;a=a.child;continue}if(a===b)break a;for(;null===a.sibling;){if(null===a.return||a.return===b)break a;a=a.return}a.sibling.return=a.return;a=a.sibling}d&=1}y(F,d);if(0===(b.mode&1))b.memoizedState=null;else switch(e){case "forwards":c=b.child;for(e=null;null!==c;)a=c.alternate,null!==a&&null===xd(a)&&(e=c),c=c.sibling;c=e;null===c?(e=b.child,b.child=null):(e=c.sibling,c.sibling=null);Ff(b,!1,e,c,f);break;case "backwards":c= +null;e=b.child;for(b.child=null;null!==e;){a=e.alternate;if(null!==a&&null===xd(a)){b.child=e;break}a=e.sibling;e.sibling=c;c=e;e=a}Ff(b,!0,c,null,f);break;case "together":Ff(b,!1,null,null,void 0);break;default:b.memoizedState=null}return b.child}function Fd(a,b){0===(b.mode&1)&&null!==a&&(a.alternate=null,b.alternate=null,b.flags|=2)}function Qa(a,b,c){null!==a&&(b.dependencies=a.dependencies);ra|=b.lanes;if(0===(c&b.childLanes))return null;if(null!==a&&b.child!==a.child)throw Error(m(153));if(null!== +b.child){a=b.child;c=eb(a,a.pendingProps);b.child=c;for(c.return=b;null!==a.sibling;)a=a.sibling,c=c.sibling=eb(a,a.pendingProps),c.return=b;c.sibling=null}return b.child}function wk(a,b,c){switch(b.tag){case 3:si(b);Qb();break;case 5:Ih(b);break;case 1:ea(b.type)&&ld(b);break;case 4:gf(b,b.stateNode.containerInfo);break;case 10:var d=b.type._context,e=b.memoizedProps.value;y(ud,d._currentValue);d._currentValue=e;break;case 13:d=b.memoizedState;if(null!==d){if(null!==d.dehydrated)return y(F,F.current& +1),b.flags|=128,null;if(0!==(c&b.child.childLanes))return ui(a,b,c);y(F,F.current&1);a=Qa(a,b,c);return null!==a?a.sibling:null}y(F,F.current&1);break;case 19:d=0!==(c&b.childLanes);if(0!==(a.flags&128)){if(d)return wi(a,b,c);b.flags|=128}e=b.memoizedState;null!==e&&(e.rendering=null,e.tail=null,e.lastEffect=null);y(F,F.current);if(d)break;else return null;case 22:case 23:return b.lanes=0,pi(a,b,c)}return Qa(a,b,c)}function Dc(a,b){if(!D)switch(a.tailMode){case "hidden":b=a.tail;for(var c=null;null!== +b;)null!==b.alternate&&(c=b),b=b.sibling;null===c?a.tail=null:c.sibling=null;break;case "collapsed":c=a.tail;for(var d=null;null!==c;)null!==c.alternate&&(d=c),c=c.sibling;null===d?b||null===a.tail?a.tail=null:a.tail.sibling=null:d.sibling=null}}function W(a){var b=null!==a.alternate&&a.alternate.child===a.child,c=0,d=0;if(b)for(var e=a.child;null!==e;)c|=e.lanes|e.childLanes,d|=e.subtreeFlags&14680064,d|=e.flags&14680064,e.return=a,e=e.sibling;else for(e=a.child;null!==e;)c|=e.lanes|e.childLanes, +d|=e.subtreeFlags,d|=e.flags,e.return=a,e=e.sibling;a.subtreeFlags|=d;a.childLanes=c;return b}function xk(a,b,c){var d=b.pendingProps;Ve(b);switch(b.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return W(b),null;case 1:return ea(b.type)&&(v(S),v(J)),W(b),null;case 3:d=b.stateNode;Tb();v(S);v(J);jf();d.pendingContext&&(d.context=d.pendingContext,d.pendingContext=null);if(null===a||null===a.child)pd(b)?b.flags|=4:null===a||a.memoizedState.isDehydrated&&0===(b.flags& +256)||(b.flags|=1024,null!==wa&&(Gf(wa),wa=null));xi(a,b);W(b);return null;case 5:hf(b);var e=ub(xc.current);c=b.type;if(null!==a&&null!=b.stateNode)yk(a,b,c,d,e),a.ref!==b.ref&&(b.flags|=512,b.flags|=2097152);else{if(!d){if(null===b.stateNode)throw Error(m(166));W(b);return null}a=ub(Ea.current);if(pd(b)){d=b.stateNode;c=b.type;var f=b.memoizedProps;d[Da]=b;d[uc]=f;a=0!==(b.mode&1);switch(c){case "dialog":B("cancel",d);B("close",d);break;case "iframe":case "object":case "embed":B("load",d);break; +case "video":case "audio":for(e=0;e\x3c/script>",a=a.removeChild(a.firstChild)):"string"===typeof d.is?a=g.createElement(c,{is:d.is}):(a=g.createElement(c),"select"===c&&(g=a,d.multiple?g.multiple=!0:d.size&&(g.size=d.size))):a=g.createElementNS(a,c);a[Da]=b;a[uc]=d;zk(a,b,!1,!1);b.stateNode=a;a:{g=qe(c,d);switch(c){case "dialog":B("cancel",a);B("close",a);e=d;break;case "iframe":case "object":case "embed":B("load",a);e=d;break; +case "video":case "audio":for(e=0;eHf&&(b.flags|=128,d=!0,Dc(f,!1),b.lanes=4194304)}else{if(!d)if(a=xd(g),null!==a){if(b.flags|=128,d=!0,c=a.updateQueue,null!==c&&(b.updateQueue=c,b.flags|=4),Dc(f,!0),null===f.tail&&"hidden"===f.tailMode&&!g.alternate&&!D)return W(b),null}else 2*P()-f.renderingStartTime>Hf&&1073741824!==c&&(b.flags|= +128,d=!0,Dc(f,!1),b.lanes=4194304);f.isBackwards?(g.sibling=b.child,b.child=g):(c=f.last,null!==c?c.sibling=g:b.child=g,f.last=g)}if(null!==f.tail)return b=f.tail,f.rendering=b,f.tail=b.sibling,f.renderingStartTime=P(),b.sibling=null,c=F.current,y(F,d?c&1|2:c&1),b;W(b);return null;case 22:case 23:return ba=Ga.current,v(Ga),d=null!==b.memoizedState,null!==a&&null!==a.memoizedState!==d&&(b.flags|=8192),d&&0!==(b.mode&1)?0!==(ba&1073741824)&&(W(b),b.subtreeFlags&6&&(b.flags|=8192)):W(b),null;case 24:return null; +case 25:return null}throw Error(m(156,b.tag));}function Bk(a,b,c){Ve(b);switch(b.tag){case 1:return ea(b.type)&&(v(S),v(J)),a=b.flags,a&65536?(b.flags=a&-65537|128,b):null;case 3:return Tb(),v(S),v(J),jf(),a=b.flags,0!==(a&65536)&&0===(a&128)?(b.flags=a&-65537|128,b):null;case 5:return hf(b),null;case 13:v(F);a=b.memoizedState;if(null!==a&&null!==a.dehydrated){if(null===b.alternate)throw Error(m(340));Qb()}a=b.flags;return a&65536?(b.flags=a&-65537|128,b):null;case 19:return v(F),null;case 4:return Tb(), +null;case 10:return cf(b.type._context),null;case 22:case 23:return ba=Ga.current,v(Ga),null;case 24:return null;default:return null}}function Wb(a,b){var c=a.ref;if(null!==c)if("function"===typeof c)try{c(null)}catch(d){G(a,b,d)}else c.current=null}function If(a,b,c){try{c()}catch(d){G(a,b,d)}}function Ck(a,b){Jf=Zc;a=ch();if(Ie(a)){if("selectionStart"in a)var c={start:a.selectionStart,end:a.selectionEnd};else a:{c=(c=a.ownerDocument)&&c.defaultView||window;var d=c.getSelection&&c.getSelection(); +if(d&&0!==d.rangeCount){c=d.anchorNode;var e=d.anchorOffset,f=d.focusNode;d=d.focusOffset;try{c.nodeType,f.nodeType}catch(M){c=null;break a}var g=0,h=-1,k=-1,n=0,q=0,u=a,r=null;b:for(;;){for(var p;;){u!==c||0!==e&&3!==u.nodeType||(h=g+e);u!==f||0!==d&&3!==u.nodeType||(k=g+d);3===u.nodeType&&(g+=u.nodeValue.length);if(null===(p=u.firstChild))break;r=u;u=p}for(;;){if(u===a)break b;r===c&&++n===e&&(h=g);r===f&&++q===d&&(k=g);if(null!==(p=u.nextSibling))break;u=r;r=u.parentNode}u=p}c=-1===h||-1===k?null: +{start:h,end:k}}else c=null}c=c||{start:0,end:0}}else c=null;Kf={focusedElem:a,selectionRange:c};Zc=!1;for(l=b;null!==l;)if(b=l,a=b.child,0!==(b.subtreeFlags&1028)&&null!==a)a.return=b,l=a;else for(;null!==l;){b=l;try{var x=b.alternate;if(0!==(b.flags&1024))switch(b.tag){case 0:case 11:case 15:break;case 1:if(null!==x){var v=x.memoizedProps,z=x.memoizedState,w=b.stateNode,A=w.getSnapshotBeforeUpdate(b.elementType===b.type?v:ya(b.type,v),z);w.__reactInternalSnapshotBeforeUpdate=A}break;case 3:var t= +b.stateNode.containerInfo;1===t.nodeType?t.textContent="":9===t.nodeType&&t.documentElement&&t.removeChild(t.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(m(163));}}catch(M){G(b,b.return,M)}a=b.sibling;if(null!==a){a.return=b.return;l=a;break}l=b.return}x=zi;zi=!1;return x}function Gc(a,b,c){var d=b.updateQueue;d=null!==d?d.lastEffect:null;if(null!==d){var e=d=d.next;do{if((e.tag&a)===a){var f=e.destroy;e.destroy=void 0;void 0!==f&&If(b,c,f)}e=e.next}while(e!==d)}} +function Id(a,b){b=b.updateQueue;b=null!==b?b.lastEffect:null;if(null!==b){var c=b=b.next;do{if((c.tag&a)===a){var d=c.create;c.destroy=d()}c=c.next}while(c!==b)}}function Lf(a){var b=a.ref;if(null!==b){var c=a.stateNode;switch(a.tag){case 5:a=c;break;default:a=c}"function"===typeof b?b(a):b.current=a}}function Ai(a){var b=a.alternate;null!==b&&(a.alternate=null,Ai(b));a.child=null;a.deletions=null;a.sibling=null;5===a.tag&&(b=a.stateNode,null!==b&&(delete b[Da],delete b[uc],delete b[Me],delete b[Dk], +delete b[Ek]));a.stateNode=null;a.return=null;a.dependencies=null;a.memoizedProps=null;a.memoizedState=null;a.pendingProps=null;a.stateNode=null;a.updateQueue=null}function Bi(a){return 5===a.tag||3===a.tag||4===a.tag}function Ci(a){a:for(;;){for(;null===a.sibling;){if(null===a.return||Bi(a.return))return null;a=a.return}a.sibling.return=a.return;for(a=a.sibling;5!==a.tag&&6!==a.tag&&18!==a.tag;){if(a.flags&2)continue a;if(null===a.child||4===a.tag)continue a;else a.child.return=a,a=a.child}if(!(a.flags& +2))return a.stateNode}}function Mf(a,b,c){var d=a.tag;if(5===d||6===d)a=a.stateNode,b?8===c.nodeType?c.parentNode.insertBefore(a,b):c.insertBefore(a,b):(8===c.nodeType?(b=c.parentNode,b.insertBefore(a,c)):(b=c,b.appendChild(a)),c=c._reactRootContainer,null!==c&&void 0!==c||null!==b.onclick||(b.onclick=kd));else if(4!==d&&(a=a.child,null!==a))for(Mf(a,b,c),a=a.sibling;null!==a;)Mf(a,b,c),a=a.sibling}function Nf(a,b,c){var d=a.tag;if(5===d||6===d)a=a.stateNode,b?c.insertBefore(a,b):c.appendChild(a); +else if(4!==d&&(a=a.child,null!==a))for(Nf(a,b,c),a=a.sibling;null!==a;)Nf(a,b,c),a=a.sibling}function jb(a,b,c){for(c=c.child;null!==c;)Di(a,b,c),c=c.sibling}function Di(a,b,c){if(Ca&&"function"===typeof Ca.onCommitFiberUnmount)try{Ca.onCommitFiberUnmount(Uc,c)}catch(h){}switch(c.tag){case 5:X||Wb(c,b);case 6:var d=T,e=za;T=null;jb(a,b,c);T=d;za=e;null!==T&&(za?(a=T,c=c.stateNode,8===a.nodeType?a.parentNode.removeChild(c):a.removeChild(c)):T.removeChild(c.stateNode));break;case 18:null!==T&&(za? +(a=T,c=c.stateNode,8===a.nodeType?Re(a.parentNode,c):1===a.nodeType&&Re(a,c),nc(a)):Re(T,c.stateNode));break;case 4:d=T;e=za;T=c.stateNode.containerInfo;za=!0;jb(a,b,c);T=d;za=e;break;case 0:case 11:case 14:case 15:if(!X&&(d=c.updateQueue,null!==d&&(d=d.lastEffect,null!==d))){e=d=d.next;do{var f=e,g=f.destroy;f=f.tag;void 0!==g&&(0!==(f&2)?If(c,b,g):0!==(f&4)&&If(c,b,g));e=e.next}while(e!==d)}jb(a,b,c);break;case 1:if(!X&&(Wb(c,b),d=c.stateNode,"function"===typeof d.componentWillUnmount))try{d.props= +c.memoizedProps,d.state=c.memoizedState,d.componentWillUnmount()}catch(h){G(c,b,h)}jb(a,b,c);break;case 21:jb(a,b,c);break;case 22:c.mode&1?(X=(d=X)||null!==c.memoizedState,jb(a,b,c),X=d):jb(a,b,c);break;default:jb(a,b,c)}}function Ei(a){var b=a.updateQueue;if(null!==b){a.updateQueue=null;var c=a.stateNode;null===c&&(c=a.stateNode=new Fk);b.forEach(function(b){var d=Gk.bind(null,a,b);c.has(b)||(c.add(b),b.then(d,d))})}}function Aa(a,b,c){c=b.deletions;if(null!==c)for(var d=0;de&&(e=g);d&=~f}d=e;d=P()-d;d=(120>d?120:480>d?480:1080>d?1080:1920>d?1920:3E3>d?3E3:4320>d?4320:1960*Mk(d/1960))-d;if(10a?16:a;if(null===lb)var d=!1;else{a=lb;lb=null;Qd=0;if(0!==(p&6))throw Error(m(331));var e=p;p|=4;for(l=a.current;null!==l;){var f=l,g=f.child;if(0!==(l.flags&16)){var h=f.deletions;if(null!==h){for(var k=0;kP()-Of?wb(a,0):Sf|=c);ia(a,b)}function Ti(a,b){0===b&&(0===(a.mode&1)?b=1:(b=Rd,Rd<<=1,0===(Rd&130023424)&&(Rd=4194304)));var c=Z();a=Oa(a,b);null!==a&&(ic(a,b,c),ia(a,c))}function vk(a){var b=a.memoizedState,c=0;null!==b&&(c=b.retryLane);Ti(a,c)}function Gk(a,b){var c=0;switch(a.tag){case 13:var d=a.stateNode;var e=a.memoizedState;null!==e&&(c=e.retryLane); +break;case 19:d=a.stateNode;break;default:throw Error(m(314));}null!==d&&d.delete(b);Ti(a,c)}function Mi(a,b){return xh(a,b)}function Tk(a,b,c,d){this.tag=a;this.key=c;this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null;this.index=0;this.ref=null;this.pendingProps=b;this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null;this.mode=d;this.subtreeFlags=this.flags=0;this.deletions=null;this.childLanes=this.lanes=0;this.alternate=null}function yf(a){a= +a.prototype;return!(!a||!a.isReactComponent)}function Uk(a){if("function"===typeof a)return yf(a)?1:0;if(void 0!==a&&null!==a){a=a.$$typeof;if(a===ie)return 11;if(a===je)return 14}return 2}function eb(a,b){var c=a.alternate;null===c?(c=pa(a.tag,b,a.key,a.mode),c.elementType=a.elementType,c.type=a.type,c.stateNode=a.stateNode,c.alternate=a,a.alternate=c):(c.pendingProps=b,c.type=a.type,c.flags=0,c.subtreeFlags=0,c.deletions=null);c.flags=a.flags&14680064;c.childLanes=a.childLanes;c.lanes=a.lanes;c.child= +a.child;c.memoizedProps=a.memoizedProps;c.memoizedState=a.memoizedState;c.updateQueue=a.updateQueue;b=a.dependencies;c.dependencies=null===b?null:{lanes:b.lanes,firstContext:b.firstContext};c.sibling=a.sibling;c.index=a.index;c.ref=a.ref;return c}function rd(a,b,c,d,e,f){var g=2;d=a;if("function"===typeof a)yf(a)&&(g=1);else if("string"===typeof a)g=5;else a:switch(a){case Bb:return sb(c.children,e,f,b);case fe:g=8;e|=8;break;case ee:return a=pa(12,c,b,e|2),a.elementType=ee,a.lanes=f,a;case ge:return a= +pa(13,c,b,e),a.elementType=ge,a.lanes=f,a;case he:return a=pa(19,c,b,e),a.elementType=he,a.lanes=f,a;case Ui:return Gd(c,e,f,b);default:if("object"===typeof a&&null!==a)switch(a.$$typeof){case hg:g=10;break a;case gg:g=9;break a;case ie:g=11;break a;case je:g=14;break a;case Ta:g=16;d=null;break a}throw Error(m(130,null==a?a:typeof a,""));}b=pa(g,c,b,e);b.elementType=a;b.type=d;b.lanes=f;return b}function sb(a,b,c,d){a=pa(7,a,d,b);a.lanes=c;return a}function Gd(a,b,c,d){a=pa(22,a,d,b);a.elementType= +Ui;a.lanes=c;a.stateNode={isHidden:!1};return a}function Ze(a,b,c){a=pa(6,a,null,b);a.lanes=c;return a}function $e(a,b,c){b=pa(4,null!==a.children?a.children:[],a.key,b);b.lanes=c;b.stateNode={containerInfo:a.containerInfo,pendingChildren:null,implementation:a.implementation};return b}function Vk(a,b,c,d,e){this.tag=b;this.containerInfo=a;this.finishedWork=this.pingCache=this.current=this.pendingChildren=null;this.timeoutHandle=-1;this.callbackNode=this.pendingContext=this.context=null;this.callbackPriority= +0;this.eventTimes=we(0);this.expirationTimes=we(-1);this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0;this.entanglements=we(0);this.identifierPrefix=d;this.onRecoverableError=e;this.mutableSourceEagerHydrationData=null}function Vf(a,b,c,d,e,f,g,h,k,l){a=new Vk(a,b,c,h,k);1===b?(b=1,!0===f&&(b|=8)):b=0;f=pa(3,null,null,b);a.current=f;f.stateNode=a;f.memoizedState={element:d,isDehydrated:c,cache:null,transitions:null, +pendingSuspenseBoundaries:null};ff(f);return a}function Wk(a,b,c){var d=3