import { useEffect, useMemo, useState } from "react"; import { Modal, Button, toast } from "@pubscript/ui"; import { exportProjectData, type ExportFormat } from "@pubscript/exporters"; import { useProjectStore } from "../../store/project-store.js"; import { useSettings } from "../../store/settings.js"; import { api } from "../../api.js"; import { DirectoryBrowserModal } from "../../components/DirectoryBrowserModal.js"; import { buildExportProject } from "./buildExportProject.js"; import { downloadFile } from "./download.js"; import { EXPORT_FORMATS, type FormatInfo } from "./formats.js"; export interface ExportDialogProps { open: boolean; onClose: () => void; } type Scope = "project" | "chapter"; /** Collapse a string into a filesystem-safe slug. */ function slug(value: string): string { return ( value .trim() .toLowerCase() .replace(/[^\w-]+/g, "-") .replace(/-+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 60) || "manuscript" ); } export function ExportDialog({ open, onClose }: ExportDialogProps) { const adapter = useProjectStore((s) => s.adapter); const project = useProjectStore((s) => s.project); const projectId = project?.id; const chapters = useProjectStore((s) => s.chapters); const projectDirectory = useSettings((s) => s.projectDirectory); const serverPort = useSettings((s) => s.serverPort); const [format, setFormat] = useState("epub"); const [scope, setScope] = useState("project"); const [chapterId, setChapterId] = useState(""); const [fileName, setFileName] = useState(""); const [saveDir, setSaveDir] = useState(projectDirectory || "~/Documents/PubScript"); const [alsoDownloadBrowser, setAlsoDownloadBrowser] = useState(false); const [browseOpen, setBrowseOpen] = useState(false); const [books, setBooks] = useState<{ id: string; title: string }[]>([]); const [exporting, setExporting] = useState(false); const [error, setError] = useState(null); const formatInfo: FormatInfo | undefined = useMemo( () => EXPORT_FORMATS.find((f) => f.id === format), [format], ); // Initialize default fileName and saveDir when dialog opens useEffect(() => { if (!open) return; const ext = formatInfo?.extension ?? "epub"; const base = slug(project?.title || "manuscript"); setFileName(`${base}.${ext}`); setSaveDir(projectDirectory || "~/Documents/PubScript"); setError(null); }, [open, project?.title, projectDirectory]); // Update file extension when conversion format changes const handleFormatChange = (nextFormat: ExportFormat) => { setFormat(nextFormat); const nextExt = EXPORT_FORMATS.find((f) => f.id === nextFormat)?.extension ?? "bin"; setFileName((prev) => { const trimmed = prev.trim(); if (!trimmed) { return `${slug(project?.title || "manuscript")}.${nextExt}`; } const withoutExt = trimmed.replace(/\.[a-z0-9]+$/i, ""); return `${withoutExt}.${nextExt}`; }); }; // Populate the book/chapter selectors when the dialog opens. useEffect(() => { if (!open || !adapter || !projectId) return; let active = true; void (async () => { const allBooks = await adapter.listBooks(projectId); if (!active) return; setBooks(allBooks); let firstChapterId = ""; for (const book of allBooks) { const cs = await adapter.listChapters(book.id); const first = cs[0]; if (first) { firstChapterId = first.id; break; } } if (!active) return; setChapterId(firstChapterId); })(); return () => { active = false; }; }, [open, adapter, projectId]); const chapterOptions = useMemo(() => { const byBook = new Map(); for (const book of books) { byBook.set( book.id, chapters.filter((c) => c.bookId === book.id), ); } return byBook; }, [books, chapters]); const handleExport = async () => { if (!adapter || !projectId) return; setExporting(true); setError(null); try { const exportProj = await buildExportProject(adapter, projectId); const options: Parameters[2] = { scope: scope === "chapter" ? "chapter" : "project", }; if (scope === "chapter") options.scopeId = chapterId; const data = await exportProjectData(exportProj, format, options); const ext = formatInfo?.extension ?? "bin"; const rawName = fileName.trim() || `${slug(project?.title || "manuscript")}.${ext}`; const finalFileName = rawName.includes(".") ? rawName : `${rawName}.${ext}`; const saved = await api.saveExportToDirectory( { directory: saveDir.trim() || projectDirectory || "~/Documents/PubScript", fileName: finalFileName, data, }, serverPort, ); if (saved.savedOnServer && saved.savedPath) { if (alsoDownloadBrowser) { downloadFile( finalFileName, data, formatInfo?.mime ?? "application/octet-stream", ); } toast.success(`Saved on host machine: ${saved.savedPath}`); } else { downloadFile( finalFileName, data, formatInfo?.mime ?? "application/octet-stream", ); toast.success(`Downloaded ${finalFileName}`); } onClose(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setExporting(false); } }; return ( <> } >
{scope === "chapter" && ( )}
Save Directory on Host Machine
setSaveDir(e.target.value)} placeholder="~/Documents/PubScript" disabled={exporting} />
{error && (

{error}

)}
setSaveDir(dir)} onClose={() => setBrowseOpen(false)} /> ); }