import { AlertTriangle, CheckCircle2, FolderSearch, Gamepad2, Library, Loader2, RefreshCw, Save, } from "lucide-react"; import { type ReactNode, useEffect, useState } from "react"; import { type Config, getConfig, putConfig, runSync, type Status, useStatusStream, } from "./api.js"; const relTime = (ms?: number): string => { if (!ms) return "never"; const s = Math.round((Date.now() - ms) / 1000); if (s < 60) return `${s}s ago`; if (s < 3600) return `${Math.round(s / 60)}m ago`; if (s < 86400) return `${Math.round(s / 3600)}h ago`; return `${Math.round(s / 86400)}d ago`; }; function Card({ title, icon, children, right, }: { title: string; icon: ReactNode; children: ReactNode; right?: ReactNode; }) { return (

{icon} {title}

{right}
{children}
); } function Toggle({ label, hint, checked, onChange, }: { label: string; hint?: string; checked: boolean; onChange: (v: boolean) => void; }) { return ( ); } const inputCls = "w-full rounded-lg border border-border bg-surface px-3 py-2 text-sm outline-none focus:border-brand"; export function App() { const status = useStatusStream(); const [config, setConfig] = useState(); const [saving, setSaving] = useState(false); const [syncing, setSyncing] = useState(false); const [msg, setMsg] = useState(); useEffect(() => { getConfig() .then(setConfig) .catch((e) => setMsg(String(e))); }, []); const patch = (p: Partial) => setConfig((c) => (c ? { ...c, ...p } : c)); const save = async () => { if (!config) return; setSaving(true); setMsg(undefined); try { setConfig(await putConfig(config)); setMsg("Saved — re-syncing with the new settings."); } catch (e) { setMsg(String(e)); } finally { setSaving(false); } }; const sync = async () => { setSyncing(true); setMsg(undefined); try { await runSync(); setMsg("Sync requested."); } catch (e) { setMsg(String(e)); } finally { setSyncing(false); } }; return (

Playnite

Sync your Playnite library into the Punktfunk game library.

{status ? (
{config && ( )}
) : (

Loading…

)} {msg && (

{msg}

)}
); } function Connection({ status }: { status: Status }) { const found = Boolean(status.location.file); return ( }> {found ? (

Exporter connected — updated{" "} {relTime(status.location.mtime ?? undefined)} .

{status.location.file}

) : (

No exporter output found. Install the{" "} Punktfunk Sync extension in Playnite (then restart Playnite).

{status.exportError && (

{status.exportError}

)}

Looked under:{" "} {status.location.dir ?? "no Playnite data dir found"}

)}
); } function LibrarySummary({ status, onSync, syncing, }: { status: Status; onSync: () => void; syncing: boolean; }) { const report = status.lastReport; const busy = syncing || status.syncing; const sources = report ? Object.entries(report.perSource).sort((a, b) => b[1] - a[1]) : []; return ( } right={ } >
{status.lastSync?.count ?? 0} title(s) synced · last {relTime(status.lastSync?.at)}
{sources.length > 0 && (
{sources.map(([src, n]) => ( {src} {n} ))}
)} {report && report.skipped.length > 0 && (

{report.skipped.length} skipped (e.g. {report.skipped[0]?.title}:{" "} {report.skipped[0]?.reason})

)} {report?.warnings.map((w) => (

{w}

))}
); } function Settings({ config, patch, onSave, saving, }: { config: Config; patch: (p: Partial) => void; onSave: () => void; saving: boolean; }) { const csv = (a: string[]) => a.join(", "); const parseCsv = (s: string) => s .split(",") .map((x) => x.trim()) .filter(Boolean); return ( } right={ } >
patch({ filter: { ...config.filter, installedOnly: v } }) } /> patch({ filter: { ...config.filter, includeHidden: v } }) } /> patch({ art: { ...config.art, mode: v ? "dataurl" : "off" } }) } /> patch({ art: { ...config.art, includeBackground: v } }) } />
); }