refactor(ipc): unify 41 handlers into handleIpc wrapper; improve error logging and non-Error handling
This commit is contained in:
parent
3afb43d4ed
commit
b5db582538
1 changed files with 242 additions and 336 deletions
|
|
@ -138,6 +138,23 @@ function notifyChange() {
|
||||||
getWindow()?.webContents.send("folio:chapters-changed");
|
getWindow()?.webContents.send("folio:chapters-changed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Standard IPC registration. Every handler gets the same crash safety net:
|
||||||
|
// a thrown error is logged (with its channel, for diagnosability) and
|
||||||
|
// returned to the renderer as `{ error }` instead of surfacing as an
|
||||||
|
// unhandled promise rejection that the UI would silently swallow.
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
function handleIpc(channel: string, fn: (...args: any[]) => unknown): void {
|
||||||
|
ipcMain.handle(channel, async (_e, ...args: any[]) => {
|
||||||
|
try {
|
||||||
|
return await fn(...args);
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error(`[folio] ${channel} failed: ${msg}`);
|
||||||
|
return { error: msg };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Route undo/redo/select-all shortcuts to the renderer so the active editor's
|
// Route undo/redo/select-all shortcuts to the renderer so the active editor's
|
||||||
// own history is used when it has focus, instead of Electron's native
|
// own history is used when it has focus, instead of Electron's native
|
||||||
// (menu-role) undo/redo which does not understand the editor's history.
|
// (menu-role) undo/redo which does not understand the editor's history.
|
||||||
|
|
@ -191,10 +208,9 @@ async function openBookAt(p: string): Promise<BookResult | ErrResult> {
|
||||||
// Create a new book as a dedicated folder <name>/ inside the user's Documents
|
// Create a new book as a dedicated folder <name>/ inside the user's Documents
|
||||||
// directory, then open it. The folder name is the sanitized book title; if a
|
// directory, then open it. The folder name is the sanitized book title; if a
|
||||||
// folder with that name already exists we append a numeric suffix.
|
// folder with that name already exists we append a numeric suffix.
|
||||||
ipcMain.handle("folio:newBookNamed", async (_e, name: string) => {
|
handleIpc("folio:newBookNamed", async (_e, name: string) => {
|
||||||
const title = (name || "").trim();
|
const title = (name || "").trim();
|
||||||
if (!title) return { error: "Please provide a book name." };
|
if (!title) return { error: "Please provide a book name." };
|
||||||
try {
|
|
||||||
const base = app.getPath("documents");
|
const base = app.getPath("documents");
|
||||||
const folder = sanitizeBookFolderName(title) || "Untitled";
|
const folder = sanitizeBookFolderName(title) || "Untitled";
|
||||||
let target = path.join(base, folder);
|
let target = path.join(base, folder);
|
||||||
|
|
@ -212,13 +228,9 @@ ipcMain.handle("folio:newBookNamed", async (_e, name: string) => {
|
||||||
refreshMenu();
|
refreshMenu();
|
||||||
getWindow()?.webContents.send("folio:bookOpened", { path: target, title: meta.title });
|
getWindow()?.webContents.send("folio:bookOpened", { path: target, title: meta.title });
|
||||||
return { path: target, title: meta.title };
|
return { path: target, title: meta.title };
|
||||||
} catch (err) {
|
|
||||||
return { error: err instanceof Error ? err.message : String(err) };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("folio:deleteBook", async (_e, p: string) => {
|
handleIpc("folio:deleteBook", async (_e, p: string) => {
|
||||||
try {
|
|
||||||
if (!hasBook(p)) {
|
if (!hasBook(p)) {
|
||||||
// Stale recent entry: nothing to trash, just clear it from the list.
|
// Stale recent entry: nothing to trash, just clear it from the list.
|
||||||
removeRecent(p);
|
removeRecent(p);
|
||||||
|
|
@ -247,12 +259,9 @@ ipcMain.handle("folio:deleteBook", async (_e, p: string) => {
|
||||||
// In-app Undo reads the Linux home trash layout; on other platforms the
|
// In-app Undo reads the Linux home trash layout; on other platforms the
|
||||||
// OS trash is still recoverable by hand, so no false promise is made.
|
// OS trash is still recoverable by hand, so no false promise is made.
|
||||||
return { ok: true, trashed, canUndoRestore: process.platform === "linux" };
|
return { ok: true, trashed, canUndoRestore: process.platform === "linux" };
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("folio:openBook", async () => {
|
handleIpc("folio:openBook", async () => {
|
||||||
const { canceled, filePaths } = await showOpenDialog({
|
const { canceled, filePaths } = await showOpenDialog({
|
||||||
properties: ["openDirectory"],
|
properties: ["openDirectory"],
|
||||||
title: "Open a Folio book",
|
title: "Open a Folio book",
|
||||||
|
|
@ -265,9 +274,9 @@ ipcMain.handle("folio:openBook", async () => {
|
||||||
return openBookAt(p);
|
return openBookAt(p);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("folio:openPath", async (_e, p: string) => openBookAt(p));
|
handleIpc("folio:openPath", async (_e, p: string) => openBookAt(p));
|
||||||
|
|
||||||
ipcMain.handle("folio:reveal", async (_e, p: string) => {
|
handleIpc("folio:reveal", async (_e, p: string) => {
|
||||||
if (!p) return false;
|
if (!p) return false;
|
||||||
// True "reveal": show the book's folio.json selected in the file manager.
|
// True "reveal": show the book's folio.json selected in the file manager.
|
||||||
// Fall back to opening the folder itself if the meta file is missing.
|
// Fall back to opening the folder itself if the meta file is missing.
|
||||||
|
|
@ -280,7 +289,7 @@ ipcMain.handle("folio:reveal", async (_e, p: string) => {
|
||||||
return !err;
|
return !err;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("folio:pickBackupFolder", async () => {
|
handleIpc("folio:pickBackupFolder", async () => {
|
||||||
const { canceled, filePaths } = await showOpenDialog({
|
const { canceled, filePaths } = await showOpenDialog({
|
||||||
properties: ["openDirectory"],
|
properties: ["openDirectory"],
|
||||||
title: "Choose recovery backup folder",
|
title: "Choose recovery backup folder",
|
||||||
|
|
@ -288,9 +297,9 @@ ipcMain.handle("folio:pickBackupFolder", async () => {
|
||||||
return { canceled, filePaths };
|
return { canceled, filePaths };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("folio:getRecents", (): Recent[] => listRecents());
|
handleIpc("folio:getRecents", (): Recent[] => listRecents());
|
||||||
|
|
||||||
ipcMain.handle("folio:closeBook", () => {
|
handleIpc("folio:closeBook", () => {
|
||||||
bookPath = null;
|
bookPath = null;
|
||||||
mainWindow?.setTitle("Folio");
|
mainWindow?.setTitle("Folio");
|
||||||
// Notify the renderer BEFORE rebuilding the menu: a refreshMenu failure
|
// Notify the renderer BEFORE rebuilding the menu: a refreshMenu failure
|
||||||
|
|
@ -309,71 +318,41 @@ function requireBook(): string {
|
||||||
return bookPath;
|
return bookPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.handle("folio:getBookMeta", () => {
|
handleIpc("folio:getBookMeta", () => loadBook(requireBook()));
|
||||||
try {
|
|
||||||
return loadBook(requireBook());
|
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle("folio:setComments", (_e, comments: Record<string, unknown>) => {
|
handleIpc("folio:setComments", (_e, comments: Record<string, unknown>) => {
|
||||||
try {
|
|
||||||
const meta = loadBook(requireBook());
|
const meta = loadBook(requireBook());
|
||||||
meta.comments = comments as never;
|
meta.comments = comments as never;
|
||||||
saveMeta(requireBook(), meta);
|
saveMeta(requireBook(), meta);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("folio:getChapterContent", (_e, id: string) => {
|
handleIpc("folio:getChapterContent", (_e, id: string) =>
|
||||||
try {
|
getChapterContent(requireBook(), id)
|
||||||
return getChapterContent(requireBook(), id);
|
);
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle("folio:createChapter", (_e, title: string) => {
|
handleIpc("folio:createChapter", (_e, title: string) => {
|
||||||
try {
|
|
||||||
const entry = createChapter(requireBook(), title);
|
const entry = createChapter(requireBook(), title);
|
||||||
notifyChange();
|
notifyChange();
|
||||||
return { id: entry.id, title: entry.title, file: entry.file };
|
return { id: entry.id, title: entry.title, file: entry.file };
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("folio:renameChapter", (_e, id: string, title: string) => {
|
handleIpc("folio:renameChapter", (_e, id: string, title: string) => {
|
||||||
try {
|
|
||||||
renameChapter(requireBook(), id, title);
|
renameChapter(requireBook(), id, title);
|
||||||
notifyChange();
|
notifyChange();
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("folio:deleteChapter", (_e, id: string) => {
|
handleIpc("folio:deleteChapter", (_e, id: string) => {
|
||||||
try {
|
|
||||||
deleteChapter(requireBook(), id);
|
deleteChapter(requireBook(), id);
|
||||||
notifyChange();
|
notifyChange();
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("folio:restoreChapter", (_e, snap: unknown) => {
|
handleIpc("folio:restoreChapter", (_e, snap: unknown) => {
|
||||||
try {
|
|
||||||
restoreChapter(requireBook(), snap as never);
|
restoreChapter(requireBook(), snap as never);
|
||||||
notifyChange();
|
notifyChange();
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Restore a book the user just trashed (Undo for book deletion). The desktop
|
// Restore a book the user just trashed (Undo for book deletion). The desktop
|
||||||
|
|
@ -416,47 +395,28 @@ function restoreBookFromTrash(p: string): { ok?: boolean; error?: string } {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.handle("folio:restoreBook", (_e, p: string) => restoreBookFromTrash(p));
|
handleIpc("folio:restoreBook", (_e, p: string) => restoreBookFromTrash(p));
|
||||||
|
|
||||||
ipcMain.handle("folio:duplicateChapter", (_e, id: string, title?: string) => {
|
handleIpc("folio:duplicateChapter", (_e, id: string, title?: string) => {
|
||||||
try {
|
|
||||||
const entry = duplicateChapter(requireBook(), id, title);
|
const entry = duplicateChapter(requireBook(), id, title);
|
||||||
notifyChange();
|
notifyChange();
|
||||||
return { id: entry.id, title: entry.title };
|
return { id: entry.id, title: entry.title };
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("folio:setChapterColor", (_e, id: string, color: string | null) => {
|
handleIpc("folio:setChapterColor", (_e, id: string, color: string | null) => {
|
||||||
try {
|
|
||||||
setChapterColor(requireBook(), id, color);
|
setChapterColor(requireBook(), id, color);
|
||||||
notifyChange();
|
notifyChange();
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("folio:reorderChapters", (_e, newOrder: string[]) => {
|
handleIpc("folio:reorderChapters", (_e, newOrder: string[]) => {
|
||||||
try {
|
|
||||||
reorderChapters(requireBook(), newOrder);
|
reorderChapters(requireBook(), newOrder);
|
||||||
notifyChange();
|
notifyChange();
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// expose ordered chapter list for the renderer
|
// expose ordered chapter list for the renderer
|
||||||
ipcMain.handle("folio:getChapterList", () => {
|
handleIpc("folio:getChapterList", () => listChaptersInOrder(loadBook(requireBook())));
|
||||||
try {
|
|
||||||
const meta = loadBook(requireBook());
|
|
||||||
return listChaptersInOrder(meta);
|
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function currentBookTitle(): string {
|
function currentBookTitle(): string {
|
||||||
try {
|
try {
|
||||||
|
|
@ -466,8 +426,7 @@ function currentBookTitle(): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.handle("folio:exportMarkdown", async () => {
|
handleIpc("folio:exportMarkdown", async () => {
|
||||||
try {
|
|
||||||
const bp = requireBook();
|
const bp = requireBook();
|
||||||
const { canceled, filePath } = await showSaveDialog({
|
const { canceled, filePath } = await showSaveDialog({
|
||||||
title: "Export combined Markdown",
|
title: "Export combined Markdown",
|
||||||
|
|
@ -477,13 +436,9 @@ ipcMain.handle("folio:exportMarkdown", async () => {
|
||||||
if (canceled || !filePath) return { canceled: true } as const;
|
if (canceled || !filePath) return { canceled: true } as const;
|
||||||
fs.writeFileSync(filePath, combineMarkdown(bp), "utf-8");
|
fs.writeFileSync(filePath, combineMarkdown(bp), "utf-8");
|
||||||
return { ok: true, filePath };
|
return { ok: true, filePath };
|
||||||
} catch (err) {
|
|
||||||
return { error: err instanceof Error ? err.message : String(err) };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("folio:exportZip", async () => {
|
handleIpc("folio:exportZip", async () => {
|
||||||
try {
|
|
||||||
const bp = requireBook();
|
const bp = requireBook();
|
||||||
const { canceled, filePath } = await showSaveDialog({
|
const { canceled, filePath } = await showSaveDialog({
|
||||||
title: "Export book bundle (ZIP)",
|
title: "Export book bundle (ZIP)",
|
||||||
|
|
@ -494,9 +449,6 @@ ipcMain.handle("folio:exportZip", async () => {
|
||||||
const buf = await buildZip(bp);
|
const buf = await buildZip(bp);
|
||||||
fs.writeFileSync(filePath, buf);
|
fs.writeFileSync(filePath, buf);
|
||||||
return { ok: true, filePath };
|
return { ok: true, filePath };
|
||||||
} catch (err) {
|
|
||||||
return { error: err instanceof Error ? err.message : String(err) };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Export the Full Book view as a PDF. The renderer shows only the assembled
|
// Export the Full Book view as a PDF. The renderer shows only the assembled
|
||||||
|
|
@ -504,9 +456,8 @@ ipcMain.handle("folio:exportZip", async () => {
|
||||||
// chrome, so printing the window produces a clean, book-only PDF with an
|
// chrome, so printing the window produces a clean, book-only PDF with an
|
||||||
// automatic page break before each chapter after the first. Chapter files and
|
// automatic page break before each chapter after the first. Chapter files and
|
||||||
// the assembled Markdown are never touched.
|
// the assembled Markdown are never touched.
|
||||||
ipcMain.handle("folio:exportPdf", async (_e, chapterIds?: string[] | null) => {
|
handleIpc("folio:exportPdf", async (_e, chapterIds?: string[] | null) => {
|
||||||
try {
|
requireBook();
|
||||||
requireBook();
|
|
||||||
// An explicitly empty selection is an error; undefined/null means the
|
// An explicitly empty selection is an error; undefined/null means the
|
||||||
// whole book. The old guard checked length>0 before comparing to zero,
|
// whole book. The old guard checked length>0 before comparing to zero,
|
||||||
// so it could never fire and empty arrays fell through to a full export.
|
// so it could never fire and empty arrays fell through to a full export.
|
||||||
|
|
@ -538,9 +489,6 @@ ipcMain.handle("folio:exportPdf", async (_e, chapterIds?: string[] | null) => {
|
||||||
fs.writeFileSync(filePath, buf);
|
fs.writeFileSync(filePath, buf);
|
||||||
const pdfOk = buf.subarray(0, 4).toString("utf-8") === "%PDF";
|
const pdfOk = buf.subarray(0, 4).toString("utf-8") === "%PDF";
|
||||||
return { ok: true, filePath, bytes: buf.length, pdfOk };
|
return { ok: true, filePath, bytes: buf.length, pdfOk };
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Export the manuscript as a DOCX or ODT. Both formats share one pipeline: the
|
// Export the manuscript as a DOCX or ODT. Both formats share one pipeline: the
|
||||||
|
|
@ -590,62 +538,39 @@ async function exportOffice(format: "docx" | "odt", chapterIds?: string[] | null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.handle("folio:exportDocx", (_e, chapterIds) => exportOffice("docx", chapterIds));
|
handleIpc("folio:exportDocx", (_e, chapterIds) => exportOffice("docx", chapterIds));
|
||||||
ipcMain.handle("folio:exportOdt", (_e, chapterIds) => exportOffice("odt", chapterIds));
|
handleIpc("folio:exportOdt", (_e, chapterIds) => exportOffice("odt", chapterIds));
|
||||||
|
|
||||||
ipcMain.handle("folio:saveChapter", (_e, id: string, content: string) => {
|
handleIpc("folio:saveChapter", (_e, id: string, content: string) => {
|
||||||
try {
|
|
||||||
setChapterContent(requireBook(), id, content);
|
setChapterContent(requireBook(), id, content);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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.
|
||||||
ipcMain.handle("folio:migrateChapter", (_e, id: string) => {
|
handleIpc("folio:migrateChapter", (_e, id: string) =>
|
||||||
try {
|
migrateChapterToNative(requireBook(), id)
|
||||||
return migrateChapterToNative(requireBook(), id);
|
);
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle("folio:migrateBook", () => {
|
handleIpc("folio:migrateBook", () => migrateBookToNative(requireBook()));
|
||||||
try {
|
|
||||||
return migrateBookToNative(requireBook());
|
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load a chapter as a native document for the Tiptap editor. Legacy Markdown
|
// Load a chapter as a native document for the Tiptap editor. Legacy Markdown
|
||||||
// chapters are converted on the fly (see chapters.loadChapterDoc).
|
// chapters are converted on the fly (see chapters.loadChapterDoc).
|
||||||
ipcMain.handle("folio:loadChapterDoc", (_e, id: string) => {
|
handleIpc("folio:loadChapterDoc", (_e, id: string) =>
|
||||||
try {
|
loadChapterDoc(requireBook(), id)
|
||||||
return loadChapterDoc(requireBook(), id);
|
);
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Persist a native document from the editor. Autosave writes directly to the
|
// Persist a native document from the editor. Autosave writes directly to the
|
||||||
// native JSON model (no Markdown round-trip).
|
// native JSON model (no Markdown round-trip).
|
||||||
ipcMain.handle("folio:saveChapterDoc", (_e, id: string, doc: unknown) => {
|
handleIpc("folio:saveChapterDoc", (_e, id: string, doc: unknown) => {
|
||||||
try {
|
|
||||||
saveChapterDoc(requireBook(), id, doc as never);
|
saveChapterDoc(requireBook(), id, doc as never);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (e) {
|
|
||||||
return { error: (e as Error).message };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy a user-chosen image into the book's assets/ directory and return a path
|
// Copy a user-chosen image into the book's assets/ directory and return a path
|
||||||
// relative to the book root (e.g. "assets/photo.png") for embedding.
|
// relative to the book root (e.g. "assets/photo.png") for embedding.
|
||||||
ipcMain.handle("folio:importImageAsset", async () => {
|
handleIpc("folio:importImageAsset", async () => {
|
||||||
try {
|
|
||||||
const bp = requireBook();
|
const bp = requireBook();
|
||||||
const { canceled, filePaths } = await showOpenDialog({
|
const { canceled, filePaths } = await showOpenDialog({
|
||||||
title: "Insert image",
|
title: "Insert image",
|
||||||
|
|
@ -670,23 +595,20 @@ ipcMain.handle("folio:importImageAsset", async () => {
|
||||||
fs.copyFileSync(src, path.join(dir, name));
|
fs.copyFileSync(src, path.join(dir, name));
|
||||||
const relPath = `assets/${name}`;
|
const relPath = `assets/${name}`;
|
||||||
return { ok: true, name, relPath };
|
return { ok: true, name, relPath };
|
||||||
} catch (err) {
|
|
||||||
return { error: err instanceof Error ? err.message : String(err) };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("folio:copyText", (_e, text: string) => {
|
handleIpc("folio:copyText", (_e, text: string) => {
|
||||||
clipboard.writeText(typeof text === "string" ? text : "");
|
clipboard.writeText(typeof text === "string" ? text : "");
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("folio:getClipboardText", () => {
|
handleIpc("folio:getClipboardText", () => {
|
||||||
return clipboard.readText();
|
return clipboard.readText();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Full screen is driven by the Electron window (not the DOM Fullscreen API,
|
// Full screen is driven by the Electron window (not the DOM Fullscreen API,
|
||||||
// which is unreliable inside Electron and fails silently here).
|
// which is unreliable inside Electron and fails silently here).
|
||||||
ipcMain.handle("folio:toggleFullscreen", () => {
|
handleIpc("folio:toggleFullscreen", () => {
|
||||||
const win = getWindow();
|
const win = getWindow();
|
||||||
if (!win) return false;
|
if (!win) return false;
|
||||||
win.setFullScreen(!win.isFullScreen());
|
win.setFullScreen(!win.isFullScreen());
|
||||||
|
|
@ -711,50 +633,37 @@ ipcMain.on("folio:context-menu", (event, x: number, y: number) => {
|
||||||
// Theme preference. Stored as a plain string so future themes can be added
|
// Theme preference. Stored as a plain string so future themes can be added
|
||||||
// without touching the main process. "default" from older builds maps to
|
// without touching the main process. "default" from older builds maps to
|
||||||
// "light" in the renderer, so it is normalized here too.
|
// "light" in the renderer, so it is normalized here too.
|
||||||
ipcMain.handle("folio:getTheme", () => getSetting("theme", "light"));
|
handleIpc("folio:getTheme", () => getSetting("theme", "light"));
|
||||||
ipcMain.handle("folio:setTheme", (_e, id: unknown) => {
|
handleIpc("folio:setTheme", (_e, id: unknown) => {
|
||||||
try {
|
|
||||||
const theme =
|
const theme =
|
||||||
typeof id === "string" && id && id !== "default" ? id : "light";
|
typeof id === "string" && id && id !== "default" ? id : "light";
|
||||||
setSetting("theme", theme);
|
setSetting("theme", theme);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (err) {
|
|
||||||
return { error: err instanceof Error ? err.message : String(err) };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Editor zoom preference (percentage). Persisted app-wide, not per book, so the
|
// Editor zoom preference (percentage). Persisted app-wide, not per book, so the
|
||||||
// writing view stays at the user's chosen size across books and restarts.
|
// writing view stays at the user's chosen size across books and restarts.
|
||||||
ipcMain.handle("folio:getZoom", () => {
|
handleIpc("folio:getZoom", () => {
|
||||||
const z = getSetting<number>("zoom", 100);
|
const z = getSetting<number>("zoom", 100);
|
||||||
return Number.isFinite(z) && z > 0 ? Math.round(z) : 100;
|
return Number.isFinite(z) && z > 0 ? Math.round(z) : 100;
|
||||||
});
|
});
|
||||||
ipcMain.handle("folio:setZoom", (_e, pct: unknown) => {
|
handleIpc("folio:setZoom", (_e, pct: unknown) => {
|
||||||
try {
|
|
||||||
const z = typeof pct === "number" && Number.isFinite(pct) ? Math.round(pct) : 100;
|
const z = typeof pct === "number" && Number.isFinite(pct) ? Math.round(pct) : 100;
|
||||||
setSetting("zoom", z);
|
setSetting("zoom", z);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (err) {
|
|
||||||
return { error: err instanceof Error ? err.message : String(err) };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Backup location preference. Backup reads/writes settings through the
|
// Backup location preference. Backup reads/writes settings through the
|
||||||
// injected store so the module itself stays Electron-free for tests.
|
// injected store so the module itself stays Electron-free for tests.
|
||||||
bindBackupSettings({ get: getSetting, set: setSetting });
|
bindBackupSettings({ get: getSetting, set: setSetting });
|
||||||
ipcMain.handle("folio:getBackupLocation", () => getRecoveryRoot());
|
handleIpc("folio:getBackupLocation", () => getRecoveryRoot());
|
||||||
ipcMain.handle("folio:setBackupLocation", (_e, loc: unknown) => {
|
handleIpc("folio:setBackupLocation", (_e, loc: unknown) => {
|
||||||
try {
|
|
||||||
const s = typeof loc === "string" ? loc.trim() : "";
|
const s = typeof loc === "string" ? loc.trim() : "";
|
||||||
setRecoveryRoot(s);
|
setRecoveryRoot(s);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (err) {
|
|
||||||
return { error: err instanceof Error ? err.message : String(err) };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("folio:importAttachment", async () => {
|
handleIpc("folio:importAttachment", async () => {
|
||||||
try {
|
|
||||||
const bp = requireBook();
|
const bp = requireBook();
|
||||||
const { canceled, filePaths } = await showOpenDialog({
|
const { canceled, filePaths } = await showOpenDialog({
|
||||||
title: "Attach a file",
|
title: "Attach a file",
|
||||||
|
|
@ -776,7 +685,4 @@ ipcMain.handle("folio:importAttachment", async () => {
|
||||||
fs.copyFileSync(src, path.join(dir, name));
|
fs.copyFileSync(src, path.join(dir, name));
|
||||||
const relPath = path.relative(bp, path.join(dir, name)).split(path.sep).join("/");
|
const relPath = path.relative(bp, path.join(dir, name)).split(path.sep).join("/");
|
||||||
return { ok: true, name, relPath };
|
return { ok: true, name, relPath };
|
||||||
} catch (err) {
|
|
||||||
return { error: err instanceof Error ? err.message : String(err) };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue