Files
punktfunk/clients/decky/src/index.tsx
T
enricobuehler 8f90563ffd
android / android (push) Has been cancelled
apple / screenshots (push) Has been cancelled
apple / swift (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / web (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
ci / rust (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
docs: dedicated Arch Linux host+client guide
Every other distro has a full Host Setup page; Arch only had table rows. Add
docs/arch.md (signed pacman binary repo: key import + repo + install, GPU
prereqs, service/linger, web console, client, PKGBUILD appendix), slot it into
the nav after fedora-kde, and point the install/client tables at it. Update the
client-install rows from 'from the PKGBUILD' to the binary repo now that it exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 17:37:01 +00:00

202 lines
6.7 KiB
TypeScript

// 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) && (
<PanelSection title="Update available">
<PanelSectionRow>
<ButtonItem
layout="below"
onClick={() => 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"
>
<FaDownload style={{ marginRight: "0.5em" }} />
Update Punktfunk
</ButtonItem>
</PanelSectionRow>
</PanelSection>
)}
<PanelSection title="Punktfunk">
<PanelSectionRow>
<ButtonItem
layout="below"
description="Host details, stream settings, and help"
onClick={() => {
Navigation.Navigate(ROUTE);
Navigation.CloseSideMenus();
}}
>
<FaTv style={{ marginRight: "0.5em" }} />
Open Punktfunk
</ButtonItem>
</PanelSectionRow>
</PanelSection>
{/* 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 && (
<PanelSection title="Pinned Games">
{pins.pins.map((pin) => {
const { online } = resolvePinHost(pin, hosts);
return (
<PanelSectionRow key={`${pin.host_fp}:${pin.game_id}`}>
<ButtonItem
layout="below"
onClick={() => streamPin(pin, hosts, pins)}
label={pin.title}
description={`${pin.host_name}${online ? "" : " · offline?"}${
pin.paired ? "" : " · pairing required"
}`}
>
<FaPlay style={{ marginRight: "0.5em" }} />
Stream
</ButtonItem>
</PanelSectionRow>
);
})}
</PanelSection>
)}
<PanelSection title="Hosts">
<PanelSectionRow>
<ButtonItem layout="below" onClick={refresh} disabled={scanning}>
{scanning ? (
<Spinner style={{ height: "1em", marginRight: "0.5em" }} />
) : (
<FaSyncAlt style={{ marginRight: "0.5em" }} />
)}
{scanning ? "Scanning…" : "Refresh"}
</ButtonItem>
</PanelSectionRow>
{hosts.length === 0 && scanning && (
<PanelSectionRow>
<Field focusable={false} description="Scanning your network…" />
</PanelSectionRow>
)}
{hosts.length === 0 && !scanning && (
<PanelSectionRow>
<Field
focusable={false}
label="No hosts found"
description="Start a Punktfunk host on this network, then refresh."
/>
</PanelSectionRow>
)}
{hosts.map((h) => {
const needsPair = h.pair === "required" && !h.paired;
return (
<PanelSectionRow key={h.fp || `${h.host}:${h.port}`}>
<ButtonItem
layout="below"
onClick={() =>
needsPair
? showModal(<PairModal host={h} onPaired={() => startStream(h)} />)
: startStream(h)
}
label={
<span style={{ display: "inline-flex", alignItems: "center", gap: "0.4em" }}>
{needsPair ? <FaLock /> : <FaLockOpen />}
{h.name}
</span>
}
description={`${h.host}:${h.port}${h.paired ? " · paired" : ""}`}
>
{needsPair ? "Pair & Stream" : "Stream"}
</ButtonItem>
</PanelSectionRow>
);
})}
</PanelSection>
<PanelSection title="About">
<PanelSectionRow>
<Field
focusable={false}
label="Version"
description={
update
? `v${update.current}${update.channel ? ` · ${update.channel}` : " · dev build"}`
: "…"
}
/>
</PanelSectionRow>
<PanelSectionRow>
<ButtonItem
layout="below"
disabled={checking}
onClick={() => void checkForUpdatesNow(check)}
>
{checking ? "Checking…" : "Check for updates"}
</ButtonItem>
</PanelSectionRow>
</PanelSection>
</>
);
};
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: <div className={staticClasses?.Title}>Punktfunk</div>,
content: (
<PluginErrorBoundary>
<QamPanel />
</PluginErrorBoundary>
),
icon: <FaTv />,
onDismount() {
routerHook.removeRoute(ROUTE);
},
};
});