forked from phenom/healer-man
101 lines
5.3 KiB
TypeScript
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 />}
|
|
/>
|
|
);
|
|
}
|