// Settings — subscribes the config draft (raw edits, resolved display values) + // liveStatusAtom (the Files paths); mutates saveConfig via the sticky SaveBar. import { useAtomRefresh, useAtomValue } from "@effect/atom-react"; import type { ArtMode, RawConfig } from "@playnite/contract"; import { PathListEditor } from "@unom/app-ui/path-list-editor"; import { SettingsGroup, SettingsRow } from "@unom/app-ui/settings"; import { CodeBlock } from "@unom/ui/code-block"; import { InputNumber } from "@unom/ui/form/input-number"; import { InputText } from "@unom/ui/form/input-text"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@unom/ui/form/select"; import { Switch } from "@unom/ui/form/switch"; import { Skeleton } from "@unom/ui/skeleton"; import { Gate } from "@/components/gate"; import { SaveBar } from "@/components/save-bar"; import { configAtom, liveStatusAtom, statusAtom } from "@/data/atoms"; import { useConfigDraft } from "@/data/drafts"; import type { PageProps } from "@/routes"; type ArtRaw = NonNullable; type SyncRaw = NonNullable; type FilterRaw = NonNullable; const PageSkeleton = () => (
); const ART_MODES: ReadonlyArray<{ readonly value: ArtMode; readonly label: string; readonly hint: string; }> = [ { value: "host", label: "Served by the host", hint: "Send each cover's local path and let the host serve the bytes. Scales to any library size — the recommended mode.", }, { value: "dataurl", label: "Inlined in the payload", hint: "Embed each cover in the library payload. Self-contained, but only fit for small libraries.", }, { value: "off", label: "No cover art", hint: "Sync titles with no art." }, ]; /** A string list editor over one of the source filters (`sources` / `excludeSources`). */ const SourceList = ({ values, onChange, placeholder, addLabel, }: { values: ReadonlyArray; onChange: (next: ReadonlyArray) => void; placeholder: string; addLabel: string; }) => ( items={values} onChange={onChange} path={{ get: (s) => s, set: (_, value) => value, placeholder }} makeItem={(s) => s} addLabel={addLabel} className="w-full" /> ); export const SettingsPage = (_: PageProps) => { const config = useAtomValue(configAtom); const retry = useAtomRefresh(configAtom); const status = useAtomValue(liveStatusAtom); const retryStatus = useAtomRefresh(statusAtom); const draft = useConfigDraft(); const { resolved } = draft; const patchArt = (patch: Partial) => draft.patch({ art: { ...(draft.draft.art ?? {}), ...patch } }); const patchSync = (patch: Partial) => draft.patch({ sync: { ...(draft.draft.sync ?? {}), ...patch } }); const patchFilter = (patch: Partial) => draft.patch({ filter: { ...(draft.draft.filter ?? {}), ...patch } }); const artMode = resolved?.art.mode ?? "host"; return ( } retry={retry}> {() => (
patchFilter({ installedOnly: checked === true }) } /> patchFilter({ includeHidden: checked === true }) } /> patchFilter({ sources: [...sources] })} placeholder="Steam" addLabel="Add source" /> patchFilter({ excludeSources: [...excludeSources] }) } placeholder="Xbox" addLabel="Exclude source" /> m.value === artMode)?.hint ?? "" } > {artMode !== "off" ? ( patchArt({ includeBackground: checked === true }) } /> ) : null} {artMode === "dataurl" ? ( patchArt({ maxBytes })} /> ) : null} patchSync({ watch: checked === true }) } /> patchSync({ pollMinutes })} /> patchSync({ debounceMs })} /> { const dir = event.target.value; // `playniteDir` is an optionalKey: undefined is dropped by // JSON.stringify, so the PUT body omits it entirely. draft.patch({ playniteDir: dir.length > 0 ? dir : undefined, }); }} /> } retry={retryStatus} > {(s) => ( {`config: ${s.paths.config}\ncache: ${s.paths.cache}\ninbox: ${s.paths.ingest}\nexport: ${s.location.file ?? "(not written yet)"}`} )}
)}
); };