From 04c6540e4c4d88e8a6e8ddcc1557d7cb53f7cddf Mon Sep 17 00:00:00 2001 From: avi Date: Sat, 5 Sep 2026 12:44:07 -0500 Subject: [PATCH] feat(renderer): add Save As button to chapter header - Add 'Save As' button next to Save in the chapter writing view - Exports current chapter as Markdown to a user-chosen file location - New IPC handler folio:saveChapterAs shows OS save dialog --- src/main/index.ts | 16 ++++++++++++++++ src/main/preload.ts | 2 ++ src/renderer/book/editor.ts | 14 ++++++++++++++ src/renderer/book/types.ts | 1 + src/renderer/index.html | 1 + 5 files changed, 34 insertions(+) diff --git a/src/main/index.ts b/src/main/index.ts index 862e566..3051a2f 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -550,6 +550,22 @@ handleIpc("folio:saveChapter", (_e, id: string, content: string) => { return { ok: true }; }); +handleIpc("folio:saveChapterAs", async (_e, id: string) => { + const bp = requireBook(); + const meta = loadBook(bp); + const entry = meta.chapters[id]; + if (!entry) return { error: "Chapter not found" }; + const { canceled, filePath } = await showSaveDialog({ + title: "Save Chapter As", + defaultPath: `${entry.title}.md`, + filters: [{ name: "Markdown", extensions: ["md"] }], + }); + if (canceled || !filePath) return { canceled: true } as const; + const content = getChapterContent(bp, id); + fs.writeFileSync(filePath, content, "utf-8"); + return { ok: true, filePath }; +}); + // Explicit, opt-in migration of a single chapter (or the whole book) to the // native Folio document format. Never automatic — the UI triggers this. The // migration pipeline makes a recovery backup and keeps the original .md files. diff --git a/src/main/preload.ts b/src/main/preload.ts index cdf81a3..d1f1748 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -42,6 +42,8 @@ contextBridge.exposeInMainWorld("folio", { ipcRenderer.invoke("folio:loadChapterDoc", id), saveChapterDoc: (id: string, doc: unknown) => ipcRenderer.invoke("folio:saveChapterDoc", id, doc), + saveChapterAs: (id: string) => + ipcRenderer.invoke("folio:saveChapterAs", id), importImageAsset: () => ipcRenderer.invoke("folio:importImageAsset"), exportMarkdown: () => ipcRenderer.invoke("folio:exportMarkdown"), exportZip: () => ipcRenderer.invoke("folio:exportZip"), diff --git a/src/renderer/book/editor.ts b/src/renderer/book/editor.ts index 07f5ef6..38e8a4c 100644 --- a/src/renderer/book/editor.ts +++ b/src/renderer/book/editor.ts @@ -11,6 +11,7 @@ import type { BookCtx } from "./types.js"; import { alertMessage } from "../dialog.js"; import { $, + basename, closeEl, countWords, formatWordCount, @@ -230,6 +231,19 @@ export function createEditorFeature(ctx: BookCtx): EditorFeature { const saveBtn = $("saveBtn"); saveBtn?.addEventListener("click", () => void saveAndNotify()); + // Save As button — exports the current chapter as a Markdown file to a + // user-chosen location via the OS save dialog. + const saveAsBtn = $("saveAsBtn"); + saveAsBtn?.addEventListener("click", async () => { + if (!state.selectedId) return; + const res = await api.saveChapterAs(state.selectedId); + if (res && "error" in res) { + showToast("Save As failed"); + } else if (res && "filePath" in res) { + showToast(`Saved to ${basename(res.filePath as string)}`); + } + }); + // Flush any pending auto-save when the window is hidden or loses focus, so the // last stretch of typing is persisted even if the app closes within the // debounce window. diff --git a/src/renderer/book/types.ts b/src/renderer/book/types.ts index 13ae945..1cdc15c 100644 --- a/src/renderer/book/types.ts +++ b/src/renderer/book/types.ts @@ -56,6 +56,7 @@ export type FolioAPI = { migrateBook: () => Promise; loadChapterDoc: (id: string) => Promise; saveChapterDoc: (id: string, doc: unknown) => Promise; + saveChapterAs: (id: string) => Promise; importImageAsset: () => Promise; exportMarkdown: () => Promise; exportZip: () => Promise; diff --git a/src/renderer/index.html b/src/renderer/index.html index c6b9e3f..fb1f3d6 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -70,6 +70,7 @@
Saved + words: 0