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
This commit is contained in:
parent
1a1d69936c
commit
04c6540e4c
5 changed files with 34 additions and 0 deletions
|
|
@ -550,6 +550,22 @@ handleIpc("folio:saveChapter", (_e, id: string, content: string) => {
|
||||||
return { ok: true };
|
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
|
// 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
|
// native Folio document format. Never automatic — the UI triggers this. The
|
||||||
// migration pipeline makes a recovery backup and keeps the original .md files.
|
// migration pipeline makes a recovery backup and keeps the original .md files.
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ contextBridge.exposeInMainWorld("folio", {
|
||||||
ipcRenderer.invoke("folio:loadChapterDoc", id),
|
ipcRenderer.invoke("folio:loadChapterDoc", id),
|
||||||
saveChapterDoc: (id: string, doc: unknown) =>
|
saveChapterDoc: (id: string, doc: unknown) =>
|
||||||
ipcRenderer.invoke("folio:saveChapterDoc", id, doc),
|
ipcRenderer.invoke("folio:saveChapterDoc", id, doc),
|
||||||
|
saveChapterAs: (id: string) =>
|
||||||
|
ipcRenderer.invoke("folio:saveChapterAs", id),
|
||||||
importImageAsset: () => ipcRenderer.invoke("folio:importImageAsset"),
|
importImageAsset: () => ipcRenderer.invoke("folio:importImageAsset"),
|
||||||
exportMarkdown: () => ipcRenderer.invoke("folio:exportMarkdown"),
|
exportMarkdown: () => ipcRenderer.invoke("folio:exportMarkdown"),
|
||||||
exportZip: () => ipcRenderer.invoke("folio:exportZip"),
|
exportZip: () => ipcRenderer.invoke("folio:exportZip"),
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import type { BookCtx } from "./types.js";
|
||||||
import { alertMessage } from "../dialog.js";
|
import { alertMessage } from "../dialog.js";
|
||||||
import {
|
import {
|
||||||
$,
|
$,
|
||||||
|
basename,
|
||||||
closeEl,
|
closeEl,
|
||||||
countWords,
|
countWords,
|
||||||
formatWordCount,
|
formatWordCount,
|
||||||
|
|
@ -230,6 +231,19 @@ export function createEditorFeature(ctx: BookCtx): EditorFeature {
|
||||||
const saveBtn = $("saveBtn");
|
const saveBtn = $("saveBtn");
|
||||||
saveBtn?.addEventListener("click", () => void saveAndNotify());
|
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
|
// 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
|
// last stretch of typing is persisted even if the app closes within the
|
||||||
// debounce window.
|
// debounce window.
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ export type FolioAPI = {
|
||||||
migrateBook: () => Promise<any>;
|
migrateBook: () => Promise<any>;
|
||||||
loadChapterDoc: (id: string) => Promise<any>;
|
loadChapterDoc: (id: string) => Promise<any>;
|
||||||
saveChapterDoc: (id: string, doc: unknown) => Promise<any>;
|
saveChapterDoc: (id: string, doc: unknown) => Promise<any>;
|
||||||
|
saveChapterAs: (id: string) => Promise<any>;
|
||||||
importImageAsset: () => Promise<any>;
|
importImageAsset: () => Promise<any>;
|
||||||
exportMarkdown: () => Promise<any>;
|
exportMarkdown: () => Promise<any>;
|
||||||
exportZip: () => Promise<any>;
|
exportZip: () => Promise<any>;
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@
|
||||||
<div class="editor-actions">
|
<div class="editor-actions">
|
||||||
<span class="save-state" id="saveState">Saved</span>
|
<span class="save-state" id="saveState">Saved</span>
|
||||||
<button class="button button-ghost" id="saveBtn" title="Save chapter (Ctrl+S)">Save</button>
|
<button class="button button-ghost" id="saveBtn" title="Save chapter (Ctrl+S)">Save</button>
|
||||||
|
<button class="button button-ghost" id="saveAsBtn" title="Save chapter as Markdown file">Save As</button>
|
||||||
<span class="word-count" id="wordCount">words: 0</span>
|
<span class="word-count" id="wordCount">words: 0</span>
|
||||||
<button class="icon-button" id="statsBtn" title="Writing statistics">📊</button>
|
<button class="icon-button" id="statsBtn" title="Writing statistics">📊</button>
|
||||||
<button class="icon-button" id="commentsBtn" title="Comments">💬</button>
|
<button class="icon-button" id="commentsBtn" title="Comments">💬</button>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue