diff --git a/src/App.tsx b/src/App.tsx index 8831cfc4..6f8e01ed 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,14 +16,15 @@ import { isPluginAvailable, } from "@/lib/capacitorBridge"; import { useDeepLinks } from "@/lib/deepLinks"; +import { DatabaseIcon, PlayIcon } from "@/lib/images"; import { ensurePurchasesUser, getPurchaseAccess, resetPurchasesUser, } from "@/lib/purchasesSetup"; +import { preloadZapLogo } from "@/lib/zapLogo"; import { routeTree } from "./routeTree.gen"; import { useStatusStore } from "./lib/store"; -import { DatabaseIcon, PlayIcon, preloadZapLogo } from "./lib/images"; import { ConnectionProvider } from "./components/ConnectionProvider"; import { ReconnectingIndicator } from "./components/ReconnectingIndicator"; import { MediaFinishedToast } from "./components/MediaFinishedToast.tsx"; diff --git a/src/__tests__/integration/create-mappings-edit.test.tsx b/src/__tests__/integration/create-mappings-edit.test.tsx index 185a2253..85cf2123 100644 --- a/src/__tests__/integration/create-mappings-edit.test.tsx +++ b/src/__tests__/integration/create-mappings-edit.test.tsx @@ -144,17 +144,9 @@ vi.mock("react-hot-toast", () => ({ }, })); -vi.mock("@/lib/writeNfcHook", () => ({ +vi.mock("@/lib/writeNfcHook", async (importOriginal) => ({ + ...(await importOriginal()), useNfcWriter: () => mockNfcWriter, - WriteMethod: { - Auto: "auto", - LocalNFC: "local", - RemoteReader: "remote", - }, - WriteAction: { - Write: "write", - Read: "read", - }, })); vi.mock("@capacitor-mlkit/barcode-scanning", () => ({ diff --git a/src/__tests__/integration/index-route.test.tsx b/src/__tests__/integration/index-route.test.tsx index 022e19b3..d9556bd0 100644 --- a/src/__tests__/integration/index-route.test.tsx +++ b/src/__tests__/integration/index-route.test.tsx @@ -116,20 +116,9 @@ vi.mock("@/hooks/useScanOperations", () => ({ })); // Mock useNfcWriter -vi.mock("@/lib/writeNfcHook", () => ({ +vi.mock("@/lib/writeNfcHook", async (importOriginal) => ({ + ...(await importOriginal()), useNfcWriter: vi.fn(() => mockNfcWriterState), - WriteMethod: { - Auto: "auto", - LocalNFC: "local", - RemoteReader: "remote", - }, - WriteAction: { - Write: "write", - Read: "read", - Format: "format", - Erase: "erase", - MakeReadOnly: "makeReadOnly", - }, })); // Use vi.hoisted for pro purchase mock diff --git a/src/__tests__/unit/lib/images.test.ts b/src/__tests__/unit/lib/images.test.ts index 60d718cb..c8fcad86 100644 --- a/src/__tests__/unit/lib/images.test.ts +++ b/src/__tests__/unit/lib/images.test.ts @@ -34,7 +34,7 @@ describe("Zap logo preload", () => { } vi.stubGlobal("Image", MockImage); - const { preloadZapLogo, ZAP_LOGO_URL } = await import("@/lib/images"); + const { preloadZapLogo, ZAP_LOGO_URL } = await import("@/lib/zapLogo"); const first = preloadZapLogo(); const second = preloadZapLogo(); @@ -84,7 +84,8 @@ describe("Zap logo preload", () => { vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue( context as unknown as CanvasRenderingContext2D, ); - const { preloadZapLogo, ZapLogo } = await import("@/lib/images"); + const { preloadZapLogo } = await import("@/lib/zapLogo"); + const { ZapLogo } = await import("@/lib/images"); await preloadZapLogo(); render(createElement(ZapLogo)); @@ -140,7 +141,7 @@ describe("Zap logo preload", () => { } vi.stubGlobal("Image", MockImage); - const { preloadZapLogo } = await import("@/lib/images"); + const { preloadZapLogo } = await import("@/lib/zapLogo"); await expect(preloadZapLogo()).resolves.toBeUndefined(); }); diff --git a/src/__tests__/unit/routes/create.custom.test.tsx b/src/__tests__/unit/routes/create.custom.test.tsx index 803c5a38..059855af 100644 --- a/src/__tests__/unit/routes/create.custom.test.tsx +++ b/src/__tests__/unit/routes/create.custom.test.tsx @@ -59,18 +59,10 @@ vi.mock("@/lib/preferencesStore", () => ({ }), })); -// Mock NFC writer -vi.mock("@/lib/writeNfcHook", () => ({ +// Mock NFC writer, keeping the real enums and isWriteModalOpen +vi.mock("@/lib/writeNfcHook", async (importOriginal) => ({ + ...(await importOriginal()), useNfcWriter: () => mockNfcWriter, - WriteMethod: { - Auto: "auto", - LocalNFC: "local", - RemoteReader: "remote", - }, - WriteAction: { - Write: "write", - Read: "read", - }, })); // Mock hooks @@ -102,10 +94,6 @@ vi.mock("@/components/ZapScriptInput.tsx", () => ({ // Mock WriteModal to simplify testing vi.mock("@/components/WriteModal", () => ({ - isWriteModalOpen: ( - writeIntent: boolean, - writer: { status: unknown; verifyError: unknown }, - ) => writeIntent && (writer.status === null || writer.verifyError !== null), WriteModal: ({ isOpen }: { isOpen: boolean }) => isOpen ?
Write Modal
: null, })); diff --git a/src/__tests__/unit/routes/create.index.test.tsx b/src/__tests__/unit/routes/create.index.test.tsx index d6396adf..dff79268 100644 --- a/src/__tests__/unit/routes/create.index.test.tsx +++ b/src/__tests__/unit/routes/create.index.test.tsx @@ -114,18 +114,10 @@ vi.mock("@/lib/toastUtils", () => ({ showRateLimitedErrorToast: mockShowRateLimitedErrorToast, })); -// Mock NFC writer -vi.mock("@/lib/writeNfcHook", () => ({ +// Mock NFC writer, keeping the real enums and isWriteModalOpen +vi.mock("@/lib/writeNfcHook", async (importOriginal) => ({ + ...(await importOriginal()), useNfcWriter: () => mockNfcWriter, - WriteMethod: { - Auto: "auto", - LocalNFC: "local", - RemoteReader: "remote", - }, - WriteAction: { - Write: "write", - Read: "read", - }, })); // Mock Capacitor @@ -143,10 +135,6 @@ vi.mock("@/hooks/usePageHeadingFocus", () => ({ // Mock WriteModal to simplify testing vi.mock("@/components/WriteModal", () => ({ - isWriteModalOpen: ( - writeIntent: boolean, - writer: { status: unknown; verifyError: unknown }, - ) => writeIntent && (writer.status === null || writer.verifyError !== null), WriteModal: ({ isOpen, close }: { isOpen: boolean; close: () => void }) => isOpen ? (
diff --git a/src/__tests__/unit/routes/create.nfc.test.tsx b/src/__tests__/unit/routes/create.nfc.test.tsx index de4cd90b..36659807 100644 --- a/src/__tests__/unit/routes/create.nfc.test.tsx +++ b/src/__tests__/unit/routes/create.nfc.test.tsx @@ -38,19 +38,9 @@ vi.mock("@tanstack/react-router", async (importOriginal) => { }); // Mock NFC writer -vi.mock("@/lib/writeNfcHook", () => ({ +vi.mock("@/lib/writeNfcHook", async (importOriginal) => ({ + ...(await importOriginal()), useNfcWriter: () => ({ ...mockNfcWriter }), - WriteMethod: { - Auto: "auto", - LocalNFC: "local", - RemoteReader: "remote", - }, - WriteAction: { - Write: "write", - Read: "read", - Format: "format", - Erase: "erase", - }, })); // Mock hooks diff --git a/src/components/WriteModal.tsx b/src/components/WriteModal.tsx index ff59b815..b1e419b6 100644 --- a/src/components/WriteModal.tsx +++ b/src/components/WriteModal.tsx @@ -4,18 +4,10 @@ import { useSmartSwipe } from "@/hooks/useSmartSwipe"; import { useBackButtonHandler } from "@/hooks/useBackButtonHandler"; import { useFocusTrap } from "@/hooks/useFocusTrap"; import { ScanResult } from "@/lib/models"; -import type { WriteNfcHook } from "@/lib/writeNfcHook"; import { ScanSpinner } from "./ScanSpinner"; import { Button } from "./wui/Button"; import { useAnnouncer } from "./A11yAnnouncer"; -export function isWriteModalOpen( - writeIntent: boolean, - writer: Pick, -): boolean { - return writeIntent && (writer.status === null || writer.verifyError !== null); -} - export function WriteModal(props: { isOpen: boolean; close: () => void; diff --git a/src/lib/images.tsx b/src/lib/images.tsx index 1e8b52fe..87d44bbe 100644 --- a/src/lib/images.tsx +++ b/src/lib/images.tsx @@ -1,49 +1,11 @@ import { useLayoutEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; - -export const ZAP_LOGO_URL = `${__APP_BASE_PATH__}lockup.webp`; -export const ZAP_LOGO_WIDTH = 160; -export const ZAP_LOGO_HEIGHT = 36; - -let zapLogoImage: HTMLImageElement | null = null; -let zapLogoReady: Promise | null = null; - -export function preloadZapLogo(): Promise { - if (typeof Image === "undefined") return Promise.resolve(); - - if (!zapLogoReady) { - zapLogoImage = new Image(ZAP_LOGO_WIDTH, ZAP_LOGO_HEIGHT); - zapLogoImage.decoding = "sync"; - zapLogoImage.fetchPriority = "high"; - zapLogoImage.src = ZAP_LOGO_URL; - zapLogoReady = - typeof zapLogoImage.decode === "function" - ? zapLogoImage.decode().catch(() => undefined) - : Promise.resolve(); - } - - return zapLogoReady; -} - -function drawPreloadedZapLogo( - canvas: HTMLCanvasElement, - pixelRatio = window.devicePixelRatio || 1, -): boolean { - if (!zapLogoImage?.complete || zapLogoImage.naturalWidth === 0) return false; - - const context = canvas.getContext("2d"); - if (!context) return false; - - const scale = Math.max(1, pixelRatio); - canvas.width = Math.round(ZAP_LOGO_WIDTH * scale); - canvas.height = Math.round(ZAP_LOGO_HEIGHT * scale); - context.setTransform(scale, 0, 0, scale, 0, 0); - context.imageSmoothingEnabled = true; - context.imageSmoothingQuality = "high"; - context.clearRect(0, 0, ZAP_LOGO_WIDTH, ZAP_LOGO_HEIGHT); - context.drawImage(zapLogoImage, 0, 0, ZAP_LOGO_WIDTH, ZAP_LOGO_HEIGHT); - return true; -} +import { + drawPreloadedZapLogo, + preloadZapLogo, + ZAP_LOGO_HEIGHT, + ZAP_LOGO_WIDTH, +} from "@/lib/zapLogo"; export function ZapLogo() { const { t } = useTranslation(); diff --git a/src/lib/writeNfcHook.tsx b/src/lib/writeNfcHook.tsx index aa8653d5..13458127 100644 --- a/src/lib/writeNfcHook.tsx +++ b/src/lib/writeNfcHook.tsx @@ -41,6 +41,14 @@ export interface WriteNfcHook { getVerifyError: () => NfcVerificationError | null; } +/** The write modal stays open until a write settles, or while a verify failed. */ +export function isWriteModalOpen( + writeIntent: boolean, + writer: Pick, +): boolean { + return writeIntent && (writer.status === null || writer.verifyError !== null); +} + export enum WriteMethod { Auto = "auto", LocalNFC = "local", diff --git a/src/lib/zapLogo.ts b/src/lib/zapLogo.ts new file mode 100644 index 00000000..81d0e3dc --- /dev/null +++ b/src/lib/zapLogo.ts @@ -0,0 +1,43 @@ +export const ZAP_LOGO_URL = `${__APP_BASE_PATH__}lockup.webp`; +export const ZAP_LOGO_WIDTH = 160; +export const ZAP_LOGO_HEIGHT = 36; + +let zapLogoImage: HTMLImageElement | null = null; +let zapLogoReady: Promise | null = null; + +export function preloadZapLogo(): Promise { + if (typeof Image === "undefined") return Promise.resolve(); + + if (!zapLogoReady) { + zapLogoImage = new Image(ZAP_LOGO_WIDTH, ZAP_LOGO_HEIGHT); + zapLogoImage.decoding = "sync"; + zapLogoImage.fetchPriority = "high"; + zapLogoImage.src = ZAP_LOGO_URL; + zapLogoReady = + typeof zapLogoImage.decode === "function" + ? zapLogoImage.decode().catch(() => undefined) + : Promise.resolve(); + } + + return zapLogoReady; +} + +export function drawPreloadedZapLogo( + canvas: HTMLCanvasElement, + pixelRatio = window.devicePixelRatio || 1, +): boolean { + if (!zapLogoImage?.complete || zapLogoImage.naturalWidth === 0) return false; + + const context = canvas.getContext("2d"); + if (!context) return false; + + const scale = Math.max(1, pixelRatio); + canvas.width = Math.round(ZAP_LOGO_WIDTH * scale); + canvas.height = Math.round(ZAP_LOGO_HEIGHT * scale); + context.setTransform(scale, 0, 0, scale, 0, 0); + context.imageSmoothingEnabled = true; + context.imageSmoothingQuality = "high"; + context.clearRect(0, 0, ZAP_LOGO_WIDTH, ZAP_LOGO_HEIGHT); + context.drawImage(zapLogoImage, 0, 0, ZAP_LOGO_WIDTH, ZAP_LOGO_HEIGHT); + return true; +} diff --git a/src/routes/-pages/Index.tsx b/src/routes/-pages/Index.tsx index 7f45a12c..751fd0a0 100644 --- a/src/routes/-pages/Index.tsx +++ b/src/routes/-pages/Index.tsx @@ -4,9 +4,13 @@ import { useTranslation } from "react-i18next"; import { Capacitor } from "@capacitor/core"; import { logger } from "@/lib/logger"; import { showRateLimitedErrorToast } from "@/lib/toastUtils"; -import { useNfcWriter, WriteMethod } from "@/lib/writeNfcHook.tsx"; +import { + isWriteModalOpen, + useNfcWriter, + WriteMethod, +} from "@/lib/writeNfcHook.tsx"; import { useProPurchase } from "@/components/ProPurchase.tsx"; -import { isWriteModalOpen, WriteModal } from "@/components/WriteModal.tsx"; +import { WriteModal } from "@/components/WriteModal.tsx"; import { useAnnouncer } from "@/components/A11yAnnouncer"; import { cancelSession } from "@/lib/nfc"; import { CoreAPI } from "@/lib/coreApi"; diff --git a/src/routes/-pages/MappingEditor.tsx b/src/routes/-pages/MappingEditor.tsx index f9ca6991..5f713a3e 100644 --- a/src/routes/-pages/MappingEditor.tsx +++ b/src/routes/-pages/MappingEditor.tsx @@ -22,9 +22,14 @@ import { import { Button } from "@/components/wui/Button"; import { PageFrame } from "@/components/PageFrame"; import { SlideModal } from "@/components/SlideModal"; -import { useNfcWriter, WriteAction, WriteMethod } from "@/lib/writeNfcHook"; +import { + isWriteModalOpen, + useNfcWriter, + WriteAction, + WriteMethod, +} from "@/lib/writeNfcHook"; import { usePreferencesStore } from "@/lib/preferencesStore"; -import { isWriteModalOpen, WriteModal } from "@/components/WriteModal"; +import { WriteModal } from "@/components/WriteModal"; import { useSmartSwipe } from "@/hooks/useSmartSwipe"; import { usePageHeadingFocus } from "@/hooks/usePageHeadingFocus"; import { appBackNavigationOptions } from "@/lib/tabSessionStore"; diff --git a/src/routes/create.custom.tsx b/src/routes/create.custom.tsx index 421067ee..94453038 100644 --- a/src/routes/create.custom.tsx +++ b/src/routes/create.custom.tsx @@ -7,8 +7,13 @@ import { BackIcon, CreateIcon } from "@/lib/images"; import { HeaderButton } from "@/components/wui/HeaderButton"; import { Button } from "@/components/wui/Button"; import { useSmartSwipe } from "@/hooks/useSmartSwipe"; -import { isWriteModalOpen, WriteModal } from "@/components/WriteModal"; -import { useNfcWriter, WriteAction, WriteMethod } from "@/lib/writeNfcHook"; +import { WriteModal } from "@/components/WriteModal"; +import { + isWriteModalOpen, + useNfcWriter, + WriteAction, + WriteMethod, +} from "@/lib/writeNfcHook"; import { logger } from "@/lib/logger"; import { PageFrame } from "@/components/PageFrame"; import { usePreferencesStore, selectCustomText } from "@/lib/preferencesStore"; diff --git a/src/routes/create.index.tsx b/src/routes/create.index.tsx index fdafac16..37ec1699 100644 --- a/src/routes/create.index.tsx +++ b/src/routes/create.index.tsx @@ -7,7 +7,12 @@ import { ListPlusIcon, NfcIcon } from "lucide-react"; import { usePageHeadingFocus } from "@/hooks/usePageHeadingFocus"; import { NextIcon, PlayIcon, SearchIcon, TextIcon } from "@/lib/images"; import { useStatusStore } from "@/lib/store"; -import { useNfcWriter, WriteAction, WriteMethod } from "@/lib/writeNfcHook"; +import { + isWriteModalOpen, + useNfcWriter, + WriteAction, + WriteMethod, +} from "@/lib/writeNfcHook"; import { CoreAPI } from "@/lib/coreApi"; import { isCoreFeatureAvailable } from "@/lib/featureGates"; import { logger } from "@/lib/logger"; @@ -15,7 +20,7 @@ import { showRateLimitedErrorToast } from "@/lib/toastUtils"; import type { PlayingResponse, SearchResultGame } from "@/lib/models"; import { MediaDetailsModal } from "@/components/MediaDetailsModal"; import { Card } from "@/components/wui/Card"; -import { isWriteModalOpen, WriteModal } from "@/components/WriteModal"; +import { WriteModal } from "@/components/WriteModal"; import { PageFrame } from "@/components/PageFrame"; import { usePreferencesStore } from "@/lib/preferencesStore"; diff --git a/src/routes/create.nfc.tsx b/src/routes/create.nfc.tsx index 3d4cba2d..6df7d978 100644 --- a/src/routes/create.nfc.tsx +++ b/src/routes/create.nfc.tsx @@ -10,8 +10,13 @@ import { logger } from "@/lib/logger"; // } from "lucide-react"; import { useSmartSwipe } from "@/hooks/useSmartSwipe"; import { useHaptics } from "@/hooks/useHaptics"; -import { isWriteModalOpen, WriteModal } from "@/components/WriteModal"; -import { useNfcWriter, WriteAction, WriteMethod } from "@/lib/writeNfcHook"; +import { WriteModal } from "@/components/WriteModal"; +import { + isWriteModalOpen, + useNfcWriter, + WriteAction, + WriteMethod, +} from "@/lib/writeNfcHook"; import { usePreferencesStore } from "@/lib/preferencesStore"; import { PageFrame } from "@/components/PageFrame"; import { HeaderButton } from "@/components/wui/HeaderButton"; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index d63ea907..fe1cd604 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,5 +1,5 @@ import { createFileRoute } from "@tanstack/react-router"; -import { preloadZapLogo } from "@/lib/images"; +import { preloadZapLogo } from "@/lib/zapLogo"; import { Index } from "./-pages/Index"; export const Route = createFileRoute("/")({