Files
healer-man/src/ui/ContentManagerScreen.tsx
T
2026-09-11 14:26:15 -04:00

101 lines
5.3 KiB
TypeScript

import { Capacitor } from "@capacitor/core";
import { useMemo } from "react";
import {
checkForContentUpdates,
contentPackHasUpdate,
contentPackIsBundled,
contentPackIsInstalled,
installContentPack,
removeContentPack,
} from "../content/contentManager";
import { useContentManagerSnapshot } from "../content/useContentManager";
import { DualDisplayFrame } from "../components/DualDisplayFrame";
import { LazyLocalCompanionPanel } from "./LazyLocalCompanionPanel";
import { BrandMark, FrontSurface } from "./FrontSurface";
function formatBytes(bytes: number): string {
if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
if (bytes >= 1024 ** 2) return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${bytes} B`;
}
export function ContentManagerScreen({ onClose }: { readonly onClose: () => void }) {
const state = useContentManagerSnapshot();
const native = Capacitor.isNativePlatform();
const packs = useMemo(() => state.manifest?.packs ?? state.bundledManifest?.packs ?? [], [state.bundledManifest, state.manifest]);
const progress = state.totalBytes > 0 ? Math.min(1, state.downloadedBytes / state.totalBytes) : 0;
return (
<DualDisplayFrame
contextLabel="Downloaded content"
top={
<FrontSurface className="content-manager-screen" ariaLabel="Healer Man content manager">
<header className="content-manager-header">
<BrandMark compact />
<div><span>Main menu / Downloads</span><strong>Offline content</strong></div>
<button type="button" className="front-button" onClick={onClose}>Back</button>
</header>
<main className="content-manager-main">
<section className="content-manager-summary">
<p className="front-kicker">Local-first installation</p>
<h1>Choose what stays on this device</h1>
<p>
Installed content packs work without a server connection. Updates download to a temporary staging area,
verify their SHA-256, and only then replace the active version.
</p>
{!native && <p className="front-notice">Basic content is included. Optional packs can be downloaded for offline play and removed when you want the storage back.</p>}
<div className="content-manager-summary__actions">
<button type="button" className="front-button" disabled={state.checking} onClick={() => void checkForContentUpdates()}>
{state.checking ? "Checking..." : "Check for updates"}
</button>
<small>{state.manifest ? `Catalog ${state.manifest.version}` : "Offline catalog unavailable"}</small>
</div>
{state.installingPackId && (
<div className="content-manager-progress" role="progressbar" aria-valuenow={Math.round(progress * 100)} aria-valuemin={0} aria-valuemax={100}>
<span style={{ width: `${progress * 100}%` }} />
<small>{formatBytes(state.downloadedBytes)} / {formatBytes(state.totalBytes)}</small>
</div>
)}
{state.error && <p className="front-notice" role="alert">{state.error}</p>}
</section>
<section className="content-manager-packs" aria-label="Content packs">
{packs.length === 0 && <p className="content-manager-empty">Connect once to discover optional downloads. Bundled starter content remains playable offline.</p>}
{packs.map((pack) => {
const installed = contentPackIsInstalled(pack.id);
const update = contentPackHasUpdate(pack.id);
const bundled = contentPackIsBundled(pack.id);
const busy = state.installingPackId !== null;
return (
<article key={pack.id} className="content-pack-row">
<div>
<strong>{pack.label}</strong>
<small>{formatBytes(pack.downloadBytes ?? pack.bytes)} download / {pack.fileCount} files</small>
</div>
<span className={installed ? "is-installed" : ""}>
{update ? "Update available" : bundled ? "Included — always available" : installed ? "Installed — offline ready" : "Not installed"}
</span>
<div className="content-pack-row__actions">
{(!installed || update) && (
<button type="button" className="front-button front-button--primary" disabled={busy} onClick={() => void installContentPack(pack.id).catch(() => undefined)}>
{state.installingPackId === pack.id ? "Downloading..." : update ? "Update" : "Download"}
</button>
)}
{installed && !bundled && (
<button type="button" className="front-button" disabled={busy} onClick={() => void removeContentPack(pack.id).catch(() => undefined)}>
Remove
</button>
)}
</div>
</article>
);
})}
</section>
</main>
</FrontSurface>
}
bottom={<LazyLocalCompanionPanel />}
/>
);
}