mirror of
https://github.com/Hackdex-App/hackdex-website.git
synced 2026-08-23 09:04:24 -05:00
Refactor to have IndexedDB use id instead of name
This commit is contained in:
@@ -83,7 +83,7 @@ export default async function HackDetail({ params }: HackDetailProps) {
|
||||
title={hack.title}
|
||||
version={patchVersion || "Pre-release"}
|
||||
author={author}
|
||||
baseRom={baseRom?.name || ""}
|
||||
baseRomId={baseRom?.id || ""}
|
||||
platform={baseRom?.platform}
|
||||
patchUrl={signedPatchUrl}
|
||||
/>
|
||||
|
||||
@@ -11,7 +11,7 @@ interface HackActionsProps {
|
||||
title: string;
|
||||
version: string;
|
||||
author: string;
|
||||
baseRom: string;
|
||||
baseRomId: string;
|
||||
platform?: "GBA" | "GBC" | "GB" | "NDS";
|
||||
patchUrl: string;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ const HackActions: React.FC<HackActionsProps> = ({
|
||||
title,
|
||||
version,
|
||||
author,
|
||||
baseRom,
|
||||
baseRomId,
|
||||
platform,
|
||||
patchUrl,
|
||||
}) => {
|
||||
@@ -30,10 +30,10 @@ const HackActions: React.FC<HackActionsProps> = ({
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isLinked(baseRom) && (hasPermission(baseRom) || hasCached(baseRom))) {
|
||||
if (isLinked(baseRomId) && (hasPermission(baseRomId) || hasCached(baseRomId))) {
|
||||
setStatus("ready");
|
||||
}
|
||||
}, [baseRom, isLinked, hasPermission, hasCached]);
|
||||
}, [baseRomId, isLinked, hasPermission, hasCached]);
|
||||
|
||||
function onSelectFile(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const f = e.target.files?.[0] ?? null;
|
||||
@@ -47,12 +47,12 @@ const HackActions: React.FC<HackActionsProps> = ({
|
||||
setError(null);
|
||||
let baseFile = file;
|
||||
if (!baseFile) {
|
||||
if (!isLinked(baseRom) && !hasCached(baseRom)) return;
|
||||
if (!hasCached(baseRom)) {
|
||||
const perm = await ensurePermission(baseRom, true);
|
||||
if (!isLinked(baseRomId) && !hasCached(baseRomId)) return;
|
||||
if (!hasCached(baseRomId)) {
|
||||
const perm = await ensurePermission(baseRomId, true);
|
||||
if (perm !== "granted") return;
|
||||
}
|
||||
const linkedFile = await getFileBlob(baseRom);
|
||||
const linkedFile = await getFileBlob(baseRomId);
|
||||
if (!linkedFile) return;
|
||||
baseFile = linkedFile;
|
||||
}
|
||||
@@ -100,9 +100,9 @@ const HackActions: React.FC<HackActionsProps> = ({
|
||||
author={author}
|
||||
onPatch={onPatch}
|
||||
status={status}
|
||||
isLinked={isLinked(baseRom)}
|
||||
ready={hasPermission(baseRom) || hasCached(baseRom)}
|
||||
onClickLink={() => (isLinked(baseRom) ? ensurePermission(baseRom, true) : linkRom(baseRom))}
|
||||
isLinked={isLinked(baseRomId)}
|
||||
ready={hasPermission(baseRomId) || hasCached(baseRomId)}
|
||||
onClickLink={() => (isLinked(baseRomId) ? ensurePermission(baseRomId, true) : linkRom(baseRomId))}
|
||||
supported={supported}
|
||||
onUploadChange={onSelectFile}
|
||||
/>
|
||||
|
||||
@@ -39,9 +39,9 @@ export default function HackPatchForm(props: HackPatchFormProps) {
|
||||
const baseRomName = baseRomEntry?.name;
|
||||
|
||||
const { isLinked, hasPermission, hasCached, importUploadedBlob, ensurePermission, getFileBlob, supported } = useBaseRoms();
|
||||
const baseRomReady = !!baseRomName && (hasPermission(baseRomName) || hasCached(baseRomName));
|
||||
const baseRomNeedsPermission = !!baseRomName && isLinked(baseRomName) && !baseRomReady;
|
||||
const baseRomMissing = !!baseRomName && !isLinked(baseRomName) && !hasCached(baseRomName);
|
||||
const baseRomReady = !!baseRomId && (hasPermission(baseRomId) || hasCached(baseRomId));
|
||||
const baseRomNeedsPermission = !!baseRomId && isLinked(baseRomId) && !baseRomReady;
|
||||
const baseRomMissing = !!baseRomId && !isLinked(baseRomId) && !hasCached(baseRomId);
|
||||
|
||||
const isVersionTaken = version.trim() && existingVersions.includes(version.trim());
|
||||
const canSubmit = React.useMemo(() => {
|
||||
@@ -78,8 +78,8 @@ export default function HackPatchForm(props: HackPatchFormProps) {
|
||||
}, [patchMode]);
|
||||
|
||||
async function onGrantPermission() {
|
||||
if (!baseRomName) return;
|
||||
await ensurePermission(baseRomName, true);
|
||||
if (!baseRomId) return;
|
||||
await ensurePermission(baseRomId, true);
|
||||
}
|
||||
|
||||
async function onUploadBaseRom(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
@@ -92,7 +92,7 @@ export default function HackPatchForm(props: HackPatchFormProps) {
|
||||
setGenError("That ROM doesn't match any supported base ROM.");
|
||||
return;
|
||||
}
|
||||
if (matched !== baseRomName) {
|
||||
if (matched !== baseRomId) {
|
||||
setGenError(`This ROM matches "${matched}", but this hack requires "${baseRomName}".`);
|
||||
return;
|
||||
}
|
||||
@@ -106,11 +106,11 @@ export default function HackPatchForm(props: HackPatchFormProps) {
|
||||
setGenStatus("generating");
|
||||
setGenError("");
|
||||
const mod = e.target.files?.[0] || null;
|
||||
if (!mod || !baseRomName) {
|
||||
if (!mod || !baseRomId) {
|
||||
setGenStatus("idle");
|
||||
return;
|
||||
}
|
||||
let baseFile = await getFileBlob(baseRomName);
|
||||
let baseFile = await getFileBlob(baseRomId);
|
||||
if (!baseFile) {
|
||||
setGenStatus("idle");
|
||||
setGenError("Base ROM not available.");
|
||||
|
||||
@@ -97,9 +97,9 @@ export default function HackSubmitForm({ dummy = false }: HackSubmitFormProps) {
|
||||
const baseRomPlatform = baseRomEntry?.platform;
|
||||
const { isLinked, hasPermission, hasCached, importUploadedBlob, ensurePermission, getFileBlob, supported } = useBaseRoms();
|
||||
|
||||
const baseRomReady = baseRomName && (hasPermission(baseRomName) || hasCached(baseRomName));
|
||||
const baseRomNeedsPermission = baseRomName && isLinked(baseRomName) && !baseRomReady;
|
||||
const baseRomMissing = baseRomName && !isLinked(baseRomName) && !hasCached(baseRomName);
|
||||
const baseRomReady = baseRom && (hasPermission(baseRom) || hasCached(baseRom));
|
||||
const baseRomNeedsPermission = baseRom && isLinked(baseRom) && !baseRomReady;
|
||||
const baseRomMissing = baseRom && !isLinked(baseRom) && !hasCached(baseRom);
|
||||
|
||||
const coverPreviews = React.useMemo(() => {
|
||||
return newCoverFiles.map((f) => URL.createObjectURL(f));
|
||||
@@ -251,8 +251,8 @@ export default function HackSubmitForm({ dummy = false }: HackSubmitFormProps) {
|
||||
};
|
||||
|
||||
async function onGrantPermission() {
|
||||
if (!baseRomName) return;
|
||||
await ensurePermission(baseRomName, true);
|
||||
if (!baseRom) return;
|
||||
await ensurePermission(baseRom, true);
|
||||
}
|
||||
|
||||
async function onUploadBaseRom(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
@@ -260,13 +260,14 @@ export default function HackSubmitForm({ dummy = false }: HackSubmitFormProps) {
|
||||
setGenError("");
|
||||
const f = e.target.files?.[0];
|
||||
if (!f) return;
|
||||
const matchedName = await importUploadedBlob(f);
|
||||
if (!matchedName) {
|
||||
const matchedId = await importUploadedBlob(f);
|
||||
if (!matchedId) {
|
||||
setGenError("That ROM doesn't match any supported base ROM.");
|
||||
return;
|
||||
}
|
||||
if (matchedName !== baseRomName) {
|
||||
setGenError(`This ROM matches "${matchedName}", but the form requires "${baseRomName}".`);
|
||||
if (matchedId !== baseRom) {
|
||||
const matchedName = baseRoms.find(r => r.id === matchedId)?.name;
|
||||
setGenError(`This ROM matches "${matchedName ?? matchedId}", but the form requires "${baseRomName}".`);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
@@ -279,11 +280,11 @@ export default function HackSubmitForm({ dummy = false }: HackSubmitFormProps) {
|
||||
setGenStatus("generating");
|
||||
setGenError("");
|
||||
const mod = e.target.files?.[0] || null;
|
||||
if (!mod || !baseRomName) {
|
||||
if (!mod || !baseRom) {
|
||||
setGenStatus("idle");
|
||||
return;
|
||||
}
|
||||
let baseFile = await getFileBlob(baseRomName);
|
||||
let baseFile = await getFileBlob(baseRom);
|
||||
if (!baseFile) {
|
||||
setGenStatus("idle");
|
||||
setGenError("Base ROM not available.");
|
||||
|
||||
@@ -25,9 +25,9 @@ type CardHack = {
|
||||
export default function HackCard({ hack, clickable = true, className = "" }: { hack: CardHack; clickable?: boolean; className?: string }) {
|
||||
const { isLinked, hasPermission, hasCached } = useBaseRoms();
|
||||
const match = baseRoms.find((r) => r.id === hack.baseRomId);
|
||||
const baseName = match?.name ?? undefined;
|
||||
const linked = baseName ? isLinked(baseName) : false;
|
||||
const ready = baseName ? hasPermission(baseName) || hasCached(baseName) : false;
|
||||
const baseId = match?.id ?? undefined;
|
||||
const linked = baseId ? isLinked(baseId) : false;
|
||||
const ready = baseId ? hasPermission(baseId) || hasCached(baseId) : false;
|
||||
const images = (hack.covers && hack.covers.length > 0 ? hack.covers : []).filter(Boolean);
|
||||
const isCarousel = images.length > 1;
|
||||
const pathname = usePathname();
|
||||
@@ -156,7 +156,7 @@ export default function HackCard({ hack, clickable = true, className = "" }: { h
|
||||
return text.length > 120 ? text.slice(0, 120).trimEnd() + "…" : text;
|
||||
})()}
|
||||
</p>
|
||||
<div className="mt-3 text-xs text-foreground/60">Base: {baseName ?? "Unknown"}</div>
|
||||
<div className="mt-3 text-xs text-foreground/60">Base: {baseId ?? "Unknown"}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,14 +17,15 @@ export default function RomsInteractive() {
|
||||
async function onUpload(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const name = await importUploadedBlob(file);
|
||||
const id = await importUploadedBlob(file);
|
||||
const name = baseRoms.find(r => r.id === id)?.name;
|
||||
if (name) setUploadMsg(`Recognized and cached: ${name}`);
|
||||
else setUploadMsg("Unrecognized ROM. Not cached.");
|
||||
e.target.value = "";
|
||||
}
|
||||
|
||||
const linkedOrCached = baseRoms.filter(({ name }) => Boolean(cached[name]) || Boolean(linked[name]));
|
||||
const notLinked = baseRoms.filter(({ name }) => !cached[name] && !linked[name]);
|
||||
const linkedOrCached = baseRoms.filter(({ id }) => Boolean(cached[id]) || Boolean(linked[id]));
|
||||
const notLinked = baseRoms.filter(({ id }) => !cached[id] && !linked[id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -66,7 +67,8 @@ export default function RomsInteractive() {
|
||||
setUploadMsg(`Unrecognized file. Not cached. Accepted types: ${ALLOWED_TYPES.join(", ")}`);
|
||||
return;
|
||||
}
|
||||
const name = await importUploadedBlob(file);
|
||||
const id = await importUploadedBlob(file);
|
||||
const name = baseRoms.find(r => r.id === id)?.name;
|
||||
if (name) setUploadMsg(`Recognized and cached: ${name}`);
|
||||
else setUploadMsg("Unrecognized ROM. Not cached.");
|
||||
}}
|
||||
@@ -98,23 +100,24 @@ export default function RomsInteractive() {
|
||||
<div className="mt-6">
|
||||
<div className="mb-3 text-xs font-medium uppercase tracking-wide text-foreground/70">Linked or cached</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{linkedOrCached.map(({ name, platform, region }) => {
|
||||
const isLinked = Boolean(linked[name]);
|
||||
const status = statuses[name] ?? (isLinked ? "prompt" : "denied");
|
||||
const isCached = Boolean(cached[name]);
|
||||
{linkedOrCached.map(({ id, platform, region }) => {
|
||||
const name = baseRoms.find(r => r.id === id)?.name;
|
||||
const isLinked = Boolean(linked[id]);
|
||||
const status = statuses[id] ?? (isLinked ? "prompt" : "denied");
|
||||
const isCached = Boolean(cached[id]);
|
||||
return (
|
||||
<BaseRomCard
|
||||
key={name}
|
||||
name={name}
|
||||
key={id}
|
||||
name={name ?? id}
|
||||
platform={platform}
|
||||
region={region}
|
||||
isLinked={isLinked}
|
||||
status={status}
|
||||
isCached={isCached}
|
||||
onRemoveCache={() => removeFromCache(name)}
|
||||
onUnlink={() => unlinkRom(name)}
|
||||
onEnsurePermission={() => ensurePermission(name, true)}
|
||||
onImportCache={() => importToCache(name)}
|
||||
onRemoveCache={() => removeFromCache(id)}
|
||||
onUnlink={() => unlinkRom(id)}
|
||||
onEnsurePermission={() => ensurePermission(id, true)}
|
||||
onImportCache={() => importToCache(id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -125,9 +128,9 @@ export default function RomsInteractive() {
|
||||
<div className="mt-8">
|
||||
<div className="mb-3 text-xs font-medium uppercase tracking-wide text-foreground/70">Not linked</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{notLinked.map(({ name, platform, region }) => (
|
||||
{notLinked.map(({ id, name, platform, region }) => (
|
||||
<BaseRomCard
|
||||
key={name}
|
||||
key={id}
|
||||
name={name}
|
||||
platform={platform}
|
||||
region={region}
|
||||
|
||||
@@ -14,17 +14,17 @@ type ContextValue = {
|
||||
countGranted: number;
|
||||
countReady: number;
|
||||
totalCachedBytes: number;
|
||||
isLinked: (name: string) => boolean;
|
||||
hasPermission: (name: string) => boolean;
|
||||
hasCached: (name: string) => boolean;
|
||||
getHandle: (name: string) => any | null;
|
||||
linkRom: (name: string) => Promise<void>;
|
||||
unlinkRom: (name: string) => Promise<void>;
|
||||
ensurePermission: (name: string, request?: boolean) => Promise<"granted" | "prompt" | "denied" | "error">;
|
||||
getFileBlob: (name: string) => Promise<File | null>;
|
||||
importToCache: (name: string) => Promise<void>;
|
||||
removeFromCache: (name: string) => Promise<void>;
|
||||
importUploadedBlob: (file: File) => Promise<string | null>; // returns matched name
|
||||
isLinked: (id: string) => boolean;
|
||||
hasPermission: (id: string) => boolean;
|
||||
hasCached: (id: string) => boolean;
|
||||
getHandle: (id: string) => any | null;
|
||||
linkRom: (id: string) => Promise<void>;
|
||||
unlinkRom: (id: string) => Promise<void>;
|
||||
ensurePermission: (id: string, request?: boolean) => Promise<"granted" | "prompt" | "denied" | "error">;
|
||||
getFileBlob: (id: string) => Promise<File | null>;
|
||||
importToCache: (id: string) => Promise<void>;
|
||||
removeFromCache: (id: string) => Promise<void>;
|
||||
importUploadedBlob: (file: File) => Promise<string | null>; // returns matched id
|
||||
};
|
||||
|
||||
const BaseRomContext = React.createContext<ContextValue | null>(null);
|
||||
@@ -44,7 +44,7 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
|
||||
const st: Record<string, "granted" | "prompt" | "denied" | "error"> = {};
|
||||
const cacheState: Record<string, boolean> = {};
|
||||
for (const r of rows) {
|
||||
map[r.name] = r.handle;
|
||||
map[r.id] = r.handle;
|
||||
try {
|
||||
const perm = r.handle?.queryPermission?.({ mode: "read" });
|
||||
let state: any = "prompt";
|
||||
@@ -52,16 +52,16 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
|
||||
const result = await perm;
|
||||
state = result;
|
||||
}
|
||||
st[r.name] = state === "granted" ? "granted" : state === "denied" ? "denied" : "prompt";
|
||||
st[r.id] = state === "granted" ? "granted" : state === "denied" ? "denied" : "prompt";
|
||||
} catch {
|
||||
st[r.name] = "error";
|
||||
st[r.id] = "error";
|
||||
}
|
||||
}
|
||||
// Check existing blobs for all known bases (and linked ones)
|
||||
const blobRows = await getAllBlobEntries();
|
||||
let total = 0;
|
||||
for (const row of blobRows) {
|
||||
cacheState[row.name] = true;
|
||||
cacheState[row.id] = true;
|
||||
total += row.blob?.size ?? 0;
|
||||
}
|
||||
setLinked(map);
|
||||
@@ -74,23 +74,23 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
|
||||
})();
|
||||
}, []);
|
||||
|
||||
function isLinked(name: string) {
|
||||
return Boolean(linked[name]);
|
||||
function isLinked(id: string) {
|
||||
return Boolean(linked[id]);
|
||||
}
|
||||
|
||||
function getHandle(name: string) {
|
||||
return linked[name] ?? null;
|
||||
function getHandle(id: string) {
|
||||
return linked[id] ?? null;
|
||||
}
|
||||
|
||||
function hasPermission(name: string) {
|
||||
return statuses[name] === "granted";
|
||||
function hasPermission(id: string) {
|
||||
return statuses[id] === "granted";
|
||||
}
|
||||
|
||||
function hasCached(name: string) {
|
||||
return Boolean(cached[name]);
|
||||
function hasCached(id: string) {
|
||||
return Boolean(cached[id]);
|
||||
}
|
||||
|
||||
async function linkRom(name: string) {
|
||||
async function linkRom(id: string) {
|
||||
if (!supported) {
|
||||
// Fallback: use an <input type="file"> to allow selecting a ROM and cache it if recognized
|
||||
try {
|
||||
@@ -107,8 +107,8 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
|
||||
const hash = await sha1Hex(file);
|
||||
const match = baseRoms.find((r) => r.sha1 && r.sha1.toLowerCase() === hash.toLowerCase());
|
||||
if (match) {
|
||||
await setRomBlob(match.name, file);
|
||||
setCached((prev) => ({ ...prev, [match.name]: true }));
|
||||
await setRomBlob(match.id, file);
|
||||
setCached((prev) => ({ ...prev, [match.id]: true }));
|
||||
setTotalCachedBytes((n) => n + file.size);
|
||||
}
|
||||
} catch {}
|
||||
@@ -141,30 +141,30 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
|
||||
await handle.requestPermission({ mode: "read" });
|
||||
}
|
||||
}
|
||||
await setRomHandle(name, handle);
|
||||
setLinked((prev) => ({ ...prev, [name]: handle }));
|
||||
await setRomHandle(id, handle);
|
||||
setLinked((prev) => ({ ...prev, [id]: handle }));
|
||||
try {
|
||||
const q = await handle.queryPermission?.({ mode: "read" });
|
||||
setStatuses((prev) => ({ ...prev, [name]: q === "granted" ? "granted" : q === "denied" ? "denied" : "prompt" }));
|
||||
setStatuses((prev) => ({ ...prev, [id]: q === "granted" ? "granted" : q === "denied" ? "denied" : "prompt" }));
|
||||
} catch {
|
||||
setStatuses((prev) => ({ ...prev, [name]: "error" }));
|
||||
setStatuses((prev) => ({ ...prev, [id]: "error" }));
|
||||
}
|
||||
} catch (e) {
|
||||
// canceled or failed
|
||||
}
|
||||
}
|
||||
|
||||
async function unlinkRom(name: string) {
|
||||
async function unlinkRom(id: string) {
|
||||
try {
|
||||
await deleteRomHandle(name);
|
||||
await deleteRomHandle(id);
|
||||
setLinked((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[name];
|
||||
delete next[id];
|
||||
return next;
|
||||
});
|
||||
setStatuses((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[name];
|
||||
delete next[id];
|
||||
return next;
|
||||
});
|
||||
// Note: keep cached copy unless explicitly removed
|
||||
@@ -173,8 +173,8 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensurePermission(name: string, request = false) {
|
||||
const handle = linked[name];
|
||||
async function ensurePermission(id: string, request = false) {
|
||||
const handle = linked[id];
|
||||
if (!handle) return "error";
|
||||
try {
|
||||
let state = await handle.queryPermission?.({ mode: "read" });
|
||||
@@ -182,19 +182,19 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
|
||||
state = await handle.requestPermission({ mode: "read" });
|
||||
}
|
||||
const mapped = state === "granted" ? "granted" : state === "denied" ? "denied" : "prompt";
|
||||
setStatuses((prev) => ({ ...prev, [name]: mapped }));
|
||||
setStatuses((prev) => ({ ...prev, [id]: mapped }));
|
||||
return mapped;
|
||||
} catch {
|
||||
setStatuses((prev) => ({ ...prev, [name]: "error" }));
|
||||
setStatuses((prev) => ({ ...prev, [id]: "error" }));
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
||||
async function getFileBlob(name: string): Promise<File | null> {
|
||||
async function getFileBlob(id: string): Promise<File | null> {
|
||||
// Prefer cached
|
||||
const cachedBlob = await getRomBlob(name);
|
||||
if (cachedBlob) return new File([cachedBlob], name);
|
||||
const handle = linked[name];
|
||||
const cachedBlob = await getRomBlob(id);
|
||||
if (cachedBlob) return new File([cachedBlob], id);
|
||||
const handle = linked[id];
|
||||
if (!handle) return null;
|
||||
try {
|
||||
const file = await handle.getFile();
|
||||
@@ -205,25 +205,25 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function importToCache(name: string) {
|
||||
const handle = linked[name];
|
||||
async function importToCache(id: string) {
|
||||
const handle = linked[id];
|
||||
if (!handle) return;
|
||||
try {
|
||||
const file = await handle.getFile();
|
||||
await setRomBlob(name, file);
|
||||
setCached((prev) => ({ ...prev, [name]: true }));
|
||||
await setRomBlob(id, file);
|
||||
setCached((prev) => ({ ...prev, [id]: true }));
|
||||
setTotalCachedBytes((n) => n + file.size);
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
async function removeFromCache(name: string) {
|
||||
async function removeFromCache(id: string) {
|
||||
try {
|
||||
await deleteRomBlob(name);
|
||||
await deleteRomBlob(id);
|
||||
setCached((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[name];
|
||||
delete next[id];
|
||||
return next;
|
||||
});
|
||||
// We cannot easily know the blob size now; recalc total
|
||||
@@ -238,16 +238,16 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}
|
||||
|
||||
// Accept a user-uploaded base ROM, hash it, and if it matches a known base ROM, cache it under that name
|
||||
// Accept a user-uploaded base ROM, hash it, and if it matches a known base ROM, cache it under that id
|
||||
async function importUploadedBlob(file: File): Promise<string | null> {
|
||||
try {
|
||||
const hash = await sha1Hex(file);
|
||||
const match = baseRoms.find((r) => r.sha1 && r.sha1.toLowerCase() === hash.toLowerCase());
|
||||
if (!match) return null;
|
||||
await setRomBlob(match.name, file);
|
||||
setCached((prev) => ({ ...prev, [match.name]: true }));
|
||||
await setRomBlob(match.id, file);
|
||||
setCached((prev) => ({ ...prev, [match.id]: true }));
|
||||
setTotalCachedBytes((n) => n + file.size);
|
||||
return match.name;
|
||||
return match.id;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -262,9 +262,9 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
|
||||
const quota = estimate?.quota ?? Infinity;
|
||||
const usage = estimate?.usage ?? totalCachedBytes;
|
||||
const headroom = quota - usage;
|
||||
for (const name of names) {
|
||||
if (cached[name]) continue;
|
||||
const handle = linked[name];
|
||||
for (const id of names) {
|
||||
if (cached[id]) continue;
|
||||
const handle = linked[id];
|
||||
if (!handle) continue;
|
||||
try {
|
||||
const file = await handle.getFile();
|
||||
@@ -272,8 +272,8 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
|
||||
const smallEnough = size <= 128 * 1024 * 1024; // 128MB default
|
||||
const hasRoom = headroom > size * 1.2 && headroom > 64 * 1024 * 1024; // some buffer
|
||||
if (smallEnough && hasRoom) {
|
||||
await setRomBlob(name, file);
|
||||
setCached((prev) => ({ ...prev, [name]: true }));
|
||||
await setRomBlob(id, file);
|
||||
setCached((prev) => ({ ...prev, [id]: true }));
|
||||
setTotalCachedBytes((n) => n + size);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import type { Platform } from "@/data/baseRoms";
|
||||
import { PLATFORMS } from "@/data/baseRoms";
|
||||
|
||||
const DB_NAME = "romhaven";
|
||||
const DB_NAME = "hackdex";
|
||||
const DB_VERSION = 2;
|
||||
const STORE = "roms";
|
||||
const BLOB_STORE = "rom_blobs";
|
||||
const STORE = "base_roms";
|
||||
const BLOB_STORE = "base_rom_blobs";
|
||||
|
||||
function openDB(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -13,10 +13,10 @@ function openDB(): Promise<IDBDatabase> {
|
||||
req.onupgradeneeded = () => {
|
||||
const db = req.result;
|
||||
if (!db.objectStoreNames.contains(STORE)) {
|
||||
db.createObjectStore(STORE, { keyPath: "name" });
|
||||
db.createObjectStore(STORE, { keyPath: "id" });
|
||||
}
|
||||
if (!db.objectStoreNames.contains(BLOB_STORE)) {
|
||||
db.createObjectStore(BLOB_STORE, { keyPath: "name" });
|
||||
db.createObjectStore(BLOB_STORE, { keyPath: "id" });
|
||||
}
|
||||
};
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
@@ -24,29 +24,29 @@ function openDB(): Promise<IDBDatabase> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function setRomHandle(name: string, handle: any): Promise<void> {
|
||||
export async function setRomHandle(id: string, handle: any): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, "readwrite");
|
||||
const store = tx.objectStore(STORE);
|
||||
store.put({ name, handle, updatedAt: Date.now() });
|
||||
store.put({ id, handle, updatedAt: Date.now() });
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getRomHandle(name: string): Promise<any | null> {
|
||||
export async function getRomHandle(id: string): Promise<any | null> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, "readonly");
|
||||
const store = tx.objectStore(STORE);
|
||||
const req = store.get(name);
|
||||
const req = store.get(id);
|
||||
req.onsuccess = () => resolve(req.result?.handle ?? null);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAllRomEntries(): Promise<Array<{ name: string; handle: any }>> {
|
||||
export async function getAllRomEntries(): Promise<Array<{ id: string; handle: any }>> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, "readonly");
|
||||
@@ -54,57 +54,57 @@ export async function getAllRomEntries(): Promise<Array<{ name: string; handle:
|
||||
const req = store.getAll();
|
||||
req.onsuccess = () => {
|
||||
const rows = (req.result as any[]) || [];
|
||||
resolve(rows.map((r) => ({ name: r.name, handle: r.handle })));
|
||||
resolve(rows.map((r) => ({ id: r.id, handle: r.handle })));
|
||||
};
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteRomHandle(name: string): Promise<void> {
|
||||
export async function deleteRomHandle(id: string): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, "readwrite");
|
||||
const store = tx.objectStore(STORE);
|
||||
store.delete(name);
|
||||
store.delete(id);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function setRomBlob(name: string, blob: Blob): Promise<void> {
|
||||
export async function setRomBlob(id: string, blob: Blob): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(BLOB_STORE, "readwrite");
|
||||
const store = tx.objectStore(BLOB_STORE);
|
||||
store.put({ name, blob, updatedAt: Date.now() });
|
||||
store.put({ id, blob, updatedAt: Date.now() });
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getRomBlob(name: string): Promise<Blob | null> {
|
||||
export async function getRomBlob(id: string): Promise<Blob | null> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(BLOB_STORE, "readonly");
|
||||
const store = tx.objectStore(BLOB_STORE);
|
||||
const req = store.get(name);
|
||||
const req = store.get(id);
|
||||
req.onsuccess = () => resolve(req.result?.blob ?? null);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteRomBlob(name: string): Promise<void> {
|
||||
export async function deleteRomBlob(id: string): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(BLOB_STORE, "readwrite");
|
||||
const store = tx.objectStore(BLOB_STORE);
|
||||
store.delete(name);
|
||||
store.delete(id);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAllBlobEntries(): Promise<Array<{ name: string; blob: Blob; updatedAt?: number }>> {
|
||||
export async function getAllBlobEntries(): Promise<Array<{ id: string; blob: Blob; updatedAt?: number }>> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(BLOB_STORE, "readonly");
|
||||
|
||||
Reference in New Issue
Block a user