import { ButtonItem, Field, PanelSection, PanelSectionRow, Spinner, } from "@decky/ui"; import { callable, definePlugin, toaster, } from "@decky/api"; import { useEffect, useState } from "react"; import { FaTv, FaSyncAlt, FaStop, FaLock, FaLockOpen } from "react-icons/fa"; // ---- Backend bridge (see main.py) ---- interface Host { name: string; host: string; port: number; pair: string; // "required" | "optional" fp: string; } interface ConnectResult { ok: boolean; host: string | null; error?: string; } interface Status { connected: boolean; host: string | null; } const discover = callable<[], Host[]>("discover"); const connect = callable<[host: string, port: number], ConnectResult>("connect"); const disconnect = callable<[], { ok: boolean; host: string | null }>("disconnect"); const getStatus = callable<[], Status>("status"); function Content() { const [hosts, setHosts] = useState([]); const [scanning, setScanning] = useState(false); const [busyHost, setBusyHost] = useState(null); const [connectedHost, setConnectedHost] = useState(null); const refresh = async () => { setScanning(true); try { const found = await discover(); setHosts(found); toaster.toast({ title: "punktfunk", body: found.length === 0 ? "No hosts found on the LAN" : `Found ${found.length} host${found.length === 1 ? "" : "s"}`, }); } catch (e) { toaster.toast({ title: "punktfunk", body: `Discovery failed: ${e}` }); } finally { setScanning(false); } }; const onConnect = async (h: Host) => { const target = `${h.host}:${h.port}`; setBusyHost(target); try { const res = await connect(h.host, h.port); if (res.ok) { setConnectedHost(res.host); toaster.toast({ title: "punktfunk", body: `Connecting to ${h.name}` }); } else { toaster.toast({ title: "punktfunk", body: res.error === "client-not-found" ? "punktfunk-client is not installed" : `Connect failed: ${res.error ?? "unknown"}`, }); } } catch (e) { toaster.toast({ title: "punktfunk", body: `Connect failed: ${e}` }); } finally { setBusyHost(null); } }; const onDisconnect = async () => { try { await disconnect(); setConnectedHost(null); toaster.toast({ title: "punktfunk", body: "Disconnected" }); } catch (e) { toaster.toast({ title: "punktfunk", body: `Disconnect failed: ${e}` }); } }; // On panel open: sync the current connection status and do an initial scan. useEffect(() => { getStatus() .then((s) => setConnectedHost(s.connected ? s.host : null)) .catch(() => {}); void refresh(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <> {connectedHost ? `Connected — ${connectedHost}` : "Idle"} {connectedHost && ( Disconnect )} {scanning ? ( ) : ( )} {scanning ? "Scanning…" : "Refresh"} {hosts.length === 0 && !scanning && ( No hosts discovered yet. )} {hosts.map((h) => { const target = `${h.host}:${h.port}`; const isBusy = busyHost === target; const pairRequired = h.pair === "required"; return ( onConnect(h)} label={ {pairRequired ? ( ) : ( )} {h.name} } description={`${target}${pairRequired ? " · pairing required" : ""}`} > {isBusy ? "Connecting…" : "Connect"} ); })} ); } export default definePlugin(() => { return { name: "punktfunk", titleView:
punktfunk
, content: , icon: , onDismount() { // The backend tears the client down on _unload; nothing frontend-side to clean up. }, }; });