import type { ExportBundle } from "@initiative/domain"; import { useCallback, useRef, useState } from "react"; import { useEncounterContext } from "../contexts/encounter-context.js"; import { usePlayerCharactersContext } from "../contexts/player-characters-context.js"; import { assembleExportBundle, bundleToJson, readImportFile, triggerDownload, validateImportBundle, } from "../persistence/export-import.js"; export function useEncounterExportImport() { const { encounter, undoRedoState, isEmpty: encounterIsEmpty, setEncounter, setUndoRedoState, } = useEncounterContext(); const { characters: playerCharacters, replacePlayerCharacters } = usePlayerCharactersContext(); const [importError, setImportError] = useState(null); const [showExportMethod, setShowExportMethod] = useState(false); const [showImportMethod, setShowImportMethod] = useState(false); const [showImportConfirm, setShowImportConfirm] = useState(false); const pendingBundleRef = useRef(null); const importFileRef = useRef(null); const handleExportDownload = useCallback( (includeHistory: boolean, filename: string) => { const bundle = assembleExportBundle( encounter, undoRedoState, playerCharacters, includeHistory, ); triggerDownload(bundle, filename); }, [encounter, undoRedoState, playerCharacters], ); const handleExportClipboard = useCallback( (includeHistory: boolean) => { const bundle = assembleExportBundle( encounter, undoRedoState, playerCharacters, includeHistory, ); void navigator.clipboard.writeText(bundleToJson(bundle)); }, [encounter, undoRedoState, playerCharacters], ); const applyImport = useCallback( (bundle: ExportBundle) => { setEncounter(bundle.encounter); setUndoRedoState({ undoStack: bundle.undoStack, redoStack: bundle.redoStack, }); replacePlayerCharacters([...bundle.playerCharacters]); }, [setEncounter, setUndoRedoState, replacePlayerCharacters], ); const handleValidatedBundle = useCallback( (result: ExportBundle | string) => { if (typeof result === "string") { setImportError(result); return; } if (encounterIsEmpty) { applyImport(result); } else { pendingBundleRef.current = result; setShowImportConfirm(true); } }, [encounterIsEmpty, applyImport], ); const handleImportFile = useCallback( async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; if (importFileRef.current) importFileRef.current.value = ""; setImportError(null); handleValidatedBundle(await readImportFile(file)); }, [handleValidatedBundle], ); const handleImportClipboard = useCallback( (text: string) => { setImportError(null); try { const parsed: unknown = JSON.parse(text); handleValidatedBundle(validateImportBundle(parsed)); } catch { setImportError("Invalid file format"); } }, [handleValidatedBundle], ); const handleImportConfirm = useCallback(() => { if (pendingBundleRef.current) { applyImport(pendingBundleRef.current); pendingBundleRef.current = null; } setShowImportConfirm(false); }, [applyImport]); const handleImportCancel = useCallback(() => { pendingBundleRef.current = null; setShowImportConfirm(false); }, []); return { importError, showExportMethod, showImportMethod, showImportConfirm, importFileRef, setImportError, setShowExportMethod, setShowImportMethod, handleExportDownload, handleExportClipboard, handleImportFile, handleImportClipboard, handleImportConfirm, handleImportCancel, } as const; }