fd699b3e2c
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>
92 lines
2.7 KiB
TypeScript
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>
|
|
);
|
|
};
|