// Plugin entry: the Quick Access Menu panel + route registration. The fullscreen page lives // in page.tsx; shared hooks/actions in hooks.ts; the Steam-shortcut launch in steam.ts. import { ButtonItem, Field, Navigation, PanelSection, PanelSectionRow, Spinner, showModal, staticClasses, } from "@decky/ui"; import { definePlugin, routerHook } from "@decky/api"; import { FC } from "react"; import { FaDownload, FaLock, FaLockOpen, FaPlay, FaSyncAlt, FaTv } from "react-icons/fa"; import { PluginErrorBoundary } from "./boundary"; import { applyUpdate, checkForUpdatesNow, hasUpdate, resolvePinHost, startStream, useHosts, usePins, useUpdate, } from "./hooks"; import { streamPin } from "./library"; import { PunktfunkRoute, ROUTE } from "./page"; import { PairModal } from "./pair"; // ---------------------------------------------------------------------------------------- // QAM panel — quick status + entry into the full page + one-tap stream for known hosts // and pinned games. // ---------------------------------------------------------------------------------------- const QamPanel: FC = () => { const { hosts, scanning, refresh } = useHosts(); const { info: update, checking, check } = useUpdate(); const pins = usePins(); return ( <> {hasUpdate(update) && ( applyUpdate(update!, check)} label={ update!.update_available ? `Plugin v${update!.current} → v${update!.latest}${ update!.client_update_available ? " + client" : "" }` : "New client version" } description="Installing can take a couple of minutes" > Update Punktfunk )} { Navigation.Navigate(ROUTE); Navigation.CloseSideMenus(); }} > Open Punktfunk {/* Pinned games — the "jump straight into Playnite" rows. Pin games from a host's picker (fullscreen page → host row → games button). */} {pins.pins.length > 0 && ( {pins.pins.map((pin) => { const { online } = resolvePinHost(pin, hosts); return ( streamPin(pin, hosts, pins)} label={pin.title} description={`${pin.host_name}${online ? "" : " · offline?"}${ pin.paired ? "" : " · pairing required" }`} > Stream ); })} )} {scanning ? ( ) : ( )} {scanning ? "Scanning…" : "Refresh"} {hosts.length === 0 && scanning && ( )} {hosts.length === 0 && !scanning && ( )} {hosts.map((h) => { const needsPair = h.pair === "required" && !h.paired; return ( needsPair ? showModal( startStream(h)} />) : startStream(h) } label={ {needsPair ? : } {h.name} } description={`${h.host}:${h.port}${h.paired ? " · paired" : ""}`} > {needsPair ? "Pair & Stream" : "Stream"} ); })} void checkForUpdatesNow(check)} > {checking ? "Checking…" : "Check for updates"} ); }; export default definePlugin(() => { routerHook.addRoute(ROUTE, PunktfunkRoute, { exact: true }); return { // `name` is the plugin's INTERNAL id — it must stay in sync with plugin.json (the loader // keys plugins by it), so it stays lowercase; user-facing strings say "Punktfunk". name: "punktfunk", // `staticClasses?.Title` is guarded so a future client that drops the export can't throw // at plugin-load time (an error boundary only catches render-time, not load-time, errors). titleView:
Punktfunk
, content: ( ), icon: , onDismount() { routerHook.removeRoute(ROUTE); }, }; });