Files
punktfunk/clients/decky/src/pair.tsx
T
enricobuehler fd699b3e2c feat(decky): plugin overhaul — on-Deck update check, exec-bit-free runner, About/host-detail UI, Punktfunk branding
Fixes from live debugging on the Deck:

- check_update() was dead on-device: Decky Loader's embedded (PyInstaller)
  Python has no usable default CA paths, so every HTTPS fetch failed with
  CERTIFICATE_VERIFY_FAILED. Build the SSL context explicitly: default paths
  first, then the known system bundles (SteamOS/Arch, Debian, Fedora/Bazzite,
  openSUSE), then certifi if importable. Verification stays on; the check
  stays offline-tolerant with its 30-min cache.
- "could not chmod runner" on every use: Decky extracts plugin zips without
  exec bits into a root-owned dir the unprivileged backend can't chmod. The
  Steam shortcut now launches the runner through /bin/sh with the script as a
  %command% argument — no exec bit needed, existing shortcuts migrate on
  reuse, the chmod attempt is gone.

UI/structure:

- index.tsx (660 lines) split into page/pair/settings/hooks/boundary modules;
  PluginErrorBoundary kept guarding every surface.
- New About section/tab: visible version + channel, explicit check-for-updates
  (forces past the cache, always toasts an outcome), setup-guide link, leave-
  chord help, and a Force-stop backstop for a wedged stream.
- Host rows open a details modal (address, protocol, pairing policy, paired
  state, fingerprint). Settings gain 1280×800 (Deck native), Xbox One and
  DualShock 4 pad types, and a host-compositor picker.
- Update flows note the Decky store contact can stall a couple of minutes on
  networks that blackhole plugins.deckbrew.xyz (observed live).
- "Punktfunk" in all user-facing strings; plugin id/paths/env unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:37:43 +00:00

92 lines
2.7 KiB
TypeScript

// PIN pairing modal — a gamepad-navigable digit grid (the OSK is unreliable in Gaming Mode).
// The host displays the PIN after the operator arms pairing; the user enters it here.
import { DialogButton, Focusable, ModalRoot, Spinner } from "@decky/ui";
import { toaster } from "@decky/api";
import { FC, useState } from "react";
import { Host, pair } from "./backend";
export const PairModal: FC<{
host: Host;
closeModal?: () => void;
onPaired: () => void;
}> = ({ host, closeModal, onPaired }) => {
const [pin, setPin] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const press = (d: string) => setPin((p) => (p.length >= 4 ? p : p + d));
const back = () => setPin((p) => p.slice(0, -1));
const submit = async () => {
setBusy(true);
setError(null);
try {
const res = await pair(host.host, host.port, pin, "Steam Deck");
if (res.ok) {
toaster.toast({ title: "Punktfunk", body: `Paired with ${host.name}` });
onPaired();
closeModal?.();
} else {
setError(res.error ?? "pairing failed");
setPin("");
}
} catch (e) {
setError(String(e));
} finally {
setBusy(false);
}
};
return (
<ModalRoot closeModal={closeModal}>
<div style={{ fontWeight: "bold", fontSize: "1.3em", marginBottom: "0.3em" }}>
Pair with {host.name}
</div>
<div style={{ opacity: 0.8, marginBottom: "1em" }}>
Arm pairing on the host (its console or web UI), then enter the 4-digit PIN it shows.
</div>
<div
style={{
fontSize: "2.2em",
letterSpacing: "0.4em",
textAlign: "center",
fontFamily: "monospace",
minHeight: "1.4em",
marginBottom: "0.6em",
}}
>
{pin.padEnd(4, "•")}
</div>
{error && (
<div style={{ color: "#ff6b6b", textAlign: "center", marginBottom: "0.6em" }}>
{error}
</div>
)}
<Focusable
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: "0.5em",
}}
>
{["1", "2", "3", "4", "5", "6", "7", "8", "9"].map((d) => (
<DialogButton key={d} disabled={busy} onClick={() => press(d)}>
{d}
</DialogButton>
))}
<DialogButton disabled={busy} onClick={back}>
</DialogButton>
<DialogButton disabled={busy} onClick={() => press("0")}>
0
</DialogButton>
<DialogButton disabled={busy || pin.length !== 4} onClick={submit}>
{busy ? <Spinner style={{ height: "1em" }} /> : "Pair"}
</DialogButton>
</Focusable>
</ModalRoot>
);
};