From 4f857cd7d19c4919b5a3f94a36d1bd0c6d388405 Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Thu, 23 Oct 2025 12:14:13 -1000 Subject: [PATCH] Fixes and improvements to hack action bar --- src/components/Hack/HackActions.tsx | 39 ++++++++++-- src/components/Hack/StickyActionBar.tsx | 84 ++++++++++++++++++++----- src/utils/idb.ts | 4 +- 3 files changed, 107 insertions(+), 20 deletions(-) diff --git a/src/components/Hack/HackActions.tsx b/src/components/Hack/HackActions.tsx index cc87659..5350143 100644 --- a/src/components/Hack/HackActions.tsx +++ b/src/components/Hack/HackActions.tsx @@ -29,15 +29,28 @@ const HackActions: React.FC = ({ const [status, setStatus] = React.useState<"idle" | "ready" | "patching" | "done" | "downloading">("idle"); const [error, setError] = React.useState(null); const [patchBlob, setPatchBlob] = React.useState(null); + const baseRomName = React.useMemo(() => baseRoms.find(r => r.id === baseRomId)?.name || null, [baseRomId]); React.useEffect(() => { - if (isLinked(baseRomId) && (hasPermission(baseRomId) || hasCached(baseRomId))) { + if ((isLinked(baseRomId) && hasPermission(baseRomId)) || hasCached(baseRomId)) { if (status !== "downloading" && status !== "patching" && status !== "done") { setStatus("ready"); } } }, [baseRomId, isLinked, hasPermission, hasCached, status]); + React.useEffect(() => { + let timeoutId: NodeJS.Timeout | undefined; + if (error) { + timeoutId = setTimeout(() => { + setError(null); + }, 3000); + } + return () => { + if (timeoutId) clearTimeout(timeoutId); + } + }, [error]); + // Pre-download patch on mount (or when patchUrl changes) and cache as Blob React.useEffect(() => { let aborted = false; @@ -76,11 +89,25 @@ const HackActions: React.FC = ({ }; }, [patchUrl]); - function onSelectFile(e: React.ChangeEvent) { + async function onSelectFile(e: React.ChangeEvent) { const f = e.target.files?.[0] ?? null; setFile(f); - setStatus(f ? "ready" : "idle"); - if (f) importUploadedBlob(f); + if (f) { + const id = await importUploadedBlob(f); + if (!id) { + setError("That ROM doesn't match any supported base ROM."); + setStatus("idle"); + e.target.value = ""; + return; + } + if (id !== baseRomId) { + setError(`This ROM matches "${id}", but this hack requires "${baseRomName}".`); + setStatus("idle"); + e.target.value = ""; + return; + } + setStatus("ready"); + } } async function onPatch() { @@ -145,10 +172,12 @@ const HackActions: React.FC = ({ title={title} version={version} author={author} + baseRomPlatform={platform} onPatch={onPatch} status={status} + error={error} isLinked={isLinked(baseRomId)} - ready={hasPermission(baseRomId) || hasCached(baseRomId)} + romReady={hasPermission(baseRomId) || hasCached(baseRomId)} onClickLink={() => (isLinked(baseRomId) ? ensurePermission(baseRomId, true) : linkRom(baseRomId))} supported={supported} onUploadChange={onSelectFile} diff --git a/src/components/Hack/StickyActionBar.tsx b/src/components/Hack/StickyActionBar.tsx index fd6e1fc..c6a2641 100644 --- a/src/components/Hack/StickyActionBar.tsx +++ b/src/components/Hack/StickyActionBar.tsx @@ -1,24 +1,61 @@ "use client"; import React from "react"; +import { platformAccept } from "@/utils/idb"; +import type { Platform } from "@/data/baseRoms"; -export default function StickyActionBar({ title, version, author, onPatch, status, isLinked, ready, onClickLink, supported, onUploadChange }: { +export default function StickyActionBar({ title, version, author, baseRomPlatform, onPatch, status, error, isLinked, romReady, onClickLink, supported, onUploadChange }: { title: string; version?: string; author: string; + baseRomPlatform?: Platform; onPatch: () => void; status: "idle" | "ready" | "patching" | "done" | "downloading"; + error: string | null; isLinked: boolean; - ready: boolean; + romReady: boolean; onClickLink: () => void; supported: boolean; onUploadChange: (e: React.ChangeEvent) => void; }) { const [mounted, setMounted] = React.useState(false); React.useEffect(() => setMounted(true), []); - const isDisabled = status === "patching" || (mounted && !ready && !isLinked && !supported); + const uploadInputRef = React.useRef(null); + const [errorMessage, setErrorMessage] = React.useState(null); + const [showError, setShowError] = React.useState(false); + const [patchAgainReady, setPatchAgainReady] = React.useState(true); + + // Keep error mounted to allow fade-out when error becomes null + React.useEffect(() => { + let timeoutId: number | undefined; + if (error) { + setErrorMessage(error); + // next frame to ensure transition runs + requestAnimationFrame(() => setShowError(true)); + } else if (errorMessage !== null) { + setShowError(false); + timeoutId = window.setTimeout(() => setErrorMessage(null), 300); + } else { + setShowError(false); + } + return () => { + if (timeoutId) window.clearTimeout(timeoutId); + }; + }, [error, errorMessage]); + + React.useEffect(() => { + if (status === "done") { + setPatchAgainReady(false); + setTimeout(() => { + setPatchAgainReady(true); + }, 3000); + } else { + setPatchAgainReady(true); + } + }, [status]); + return ( -
+
@@ -33,39 +70,58 @@ export default function StickyActionBar({ title, version, author, onPatch, statu - {status === "downloading" ? "Downloading..." : ready ? "Ready" : isLinked ? "Permission needed" : "Base ROM needed"} + {status === "downloading" ? "Downloading..." : romReady ? "Ready" : isLinked ? "Permission needed" : "Base ROM needed"} - {!ready && !isLinked && ( + {!romReady && !isLinked && ( )} - {!ready && isLinked && ( + {!romReady && isLinked && ( )}
+ {errorMessage !== null && ( +
+ {errorMessage} +
+ )}
); } diff --git a/src/utils/idb.ts b/src/utils/idb.ts index 9a09af6..e21a3a5 100644 --- a/src/utils/idb.ts +++ b/src/utils/idb.ts @@ -115,7 +115,9 @@ export async function getAllBlobEntries(): Promise();