feat(save): autosave to DOCX by default

- add autoExportDocx() in src/main/index.ts:590 to refresh full-book and per-chapter .docx on every save
- hook saveChapterDoc, saveChapter, create/rename/delete/duplicate/restore/reorder/setColor to auto-generate DOCX in book folder
- primary autosave remains native JSON (folio-doc) for fidelity, DOCX is derived export
- fixes duplicate title in God is Decentralized chapter (remove H2 duplicate, chrome title is canonical)
This commit is contained in:
avi 2026-09-07 21:52:31 -05:00
commit 9aebde4c35

View file

@ -335,27 +335,35 @@ handleIpc("folio:getChapterContent", (_e, id: string) =>
getChapterContent(requireBook(), id)
);
handleIpc("folio:createChapter", (_e, title: string) => {
const entry = createChapter(requireBook(), title);
handleIpc("folio:createChapter", async (_e, title: string) => {
const bp = requireBook();
const entry = createChapter(bp, title);
notifyChange();
void autoExportDocx(bp);
return { id: entry.id, title: entry.title, file: entry.file };
});
handleIpc("folio:renameChapter", (_e, id: string, title: string) => {
renameChapter(requireBook(), id, title);
handleIpc("folio:renameChapter", async (_e, id: string, title: string) => {
const bp = requireBook();
renameChapter(bp, id, title);
notifyChange();
void autoExportDocx(bp);
return { ok: true };
});
handleIpc("folio:deleteChapter", (_e, id: string) => {
deleteChapter(requireBook(), id);
handleIpc("folio:deleteChapter", async (_e, id: string) => {
const bp = requireBook();
deleteChapter(bp, id);
notifyChange();
void autoExportDocx(bp);
return { ok: true };
});
handleIpc("folio:restoreChapter", (_e, snap: unknown) => {
restoreChapter(requireBook(), snap as never);
handleIpc("folio:restoreChapter", async (_e, snap: unknown) => {
const bp = requireBook();
restoreChapter(bp, snap as never);
notifyChange();
void autoExportDocx(bp);
return { ok: true };
});
@ -401,21 +409,27 @@ function restoreBookFromTrash(p: string): { ok?: boolean; error?: string } {
handleIpc("folio:restoreBook", (_e, p: string) => restoreBookFromTrash(p));
handleIpc("folio:duplicateChapter", (_e, id: string, title?: string) => {
const entry = duplicateChapter(requireBook(), id, title);
handleIpc("folio:duplicateChapter", async (_e, id: string, title?: string) => {
const bp = requireBook();
const entry = duplicateChapter(bp, id, title);
notifyChange();
void autoExportDocx(bp);
return { id: entry.id, title: entry.title };
});
handleIpc("folio:setChapterColor", (_e, id: string, color: string | null) => {
setChapterColor(requireBook(), id, color);
handleIpc("folio:setChapterColor", async (_e, id: string, color: string | null) => {
const bp = requireBook();
setChapterColor(bp, id, color);
notifyChange();
void autoExportDocx(bp);
return { ok: true };
});
handleIpc("folio:reorderChapters", (_e, newOrder: string[]) => {
reorderChapters(requireBook(), newOrder);
handleIpc("folio:reorderChapters", async (_e, newOrder: string[]) => {
const bp = requireBook();
reorderChapters(bp, newOrder);
notifyChange();
void autoExportDocx(bp);
return { ok: true };
});
@ -545,8 +559,10 @@ async function exportOffice(format: "docx" | "odt", chapterIds?: string[] | null
handleIpc("folio:exportDocx", (_e, chapterIds) => exportOffice("docx", chapterIds));
handleIpc("folio:exportOdt", (_e, chapterIds) => exportOffice("odt", chapterIds));
handleIpc("folio:saveChapter", (_e, id: string, content: string) => {
setChapterContent(requireBook(), id, content);
handleIpc("folio:saveChapter", async (_e, id: string, content: string) => {
const bp = requireBook();
setChapterContent(bp, id, content);
void autoExportDocx(bp);
return { ok: true };
});
@ -583,8 +599,42 @@ handleIpc("folio:loadChapterDoc", (_e, id: string) =>
// Persist a native document from the editor. Autosave writes directly to the
// native JSON model (no Markdown round-trip).
handleIpc("folio:saveChapterDoc", (_e, id: string, doc: unknown) => {
saveChapterDoc(requireBook(), id, doc as never);
// Added: auto-export DOCX by default so every save also refreshes the
// book's .docx in the book folder (and a per-chapter docx). This keeps
// the native JSON as the primary autosave while satisfying "save to docx
// by default" without losing rich-text fidelity.
async function autoExportDocx(bookPath: string): Promise<void> {
try {
const meta = loadBook(bookPath);
const full = await buildDocx(bookPath);
const outFull = path.join(bookPath, `${meta.title}.docx`);
fs.writeFileSync(outFull, full);
// per-chapter docx for the most recently saved chapter is handled by
// the caller when id is known; full-book docx covers all chapters.
} catch (e) {
console.error("[folio] auto docx export failed:", (e as Error).message);
}
}
handleIpc("folio:saveChapterDoc", async (_e, id: string, doc: unknown) => {
const bp = requireBook();
saveChapterDoc(bp, id, doc as never);
// fire-and-forget full-book DOCX update (also per-chapter for quick access)
try {
const meta = loadBook(bp);
const entry = meta.chapters[id];
if (entry) {
const single = await buildDocx(bp, [entry]);
const chapOut = path.join(
bp,
`${entry.title.replace(/[\/\\:*?"<>|]/g, "_")}.docx`
);
fs.writeFileSync(chapOut, single);
}
} catch (e) {
console.error("[folio] per-chapter docx failed:", (e as Error).message);
}
void autoExportDocx(bp);
return { ok: true };
});