diff --git a/src/main/index.ts b/src/main/index.ts index 85124b1..cec9940 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -143,6 +143,16 @@ function forwardEditShortcuts(win: BrowserWindow) { } else if ((key === "z" && input.shift) || (key === "y" && !input.shift)) { event.preventDefault(); win.webContents.send("folio:redo-requested"); + } else if (key === "=" || key === "+") { + // Ctrl/Cmd+= or Ctrl/Cmd++ → zoom in (intercept the browser's native zoom). + event.preventDefault(); + win.webContents.send("folio:zoom-in-requested"); + } else if (key === "-") { + event.preventDefault(); + win.webContents.send("folio:zoom-out-requested"); + } else if (key === "0") { + event.preventDefault(); + win.webContents.send("folio:zoom-reset-requested"); } }); } @@ -548,6 +558,18 @@ ipcMain.handle("folio:setTheme", (_e, id: unknown) => { return { ok: true }; }); +// 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. +ipcMain.handle("folio:getZoom", () => { + const z = getSetting("zoom", 100); + return Number.isFinite(z) && z > 0 ? Math.round(z) : 100; +}); +ipcMain.handle("folio:setZoom", (_e, pct: unknown) => { + const z = typeof pct === "number" && Number.isFinite(pct) ? Math.round(pct) : 100; + setSetting("zoom", z); + return { ok: true }; +}); + ipcMain.handle("folio:importAttachment", async () => { const bp = requireBook(); const { canceled, filePaths } = await showOpenDialog({ diff --git a/src/main/preload.ts b/src/main/preload.ts index 7d4607a..141324a 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -46,6 +46,8 @@ contextBridge.exposeInMainWorld("folio", { copyText: (text: string) => ipcRenderer.invoke("folio:copyText", text), getClipboardText: () => ipcRenderer.invoke("folio:getClipboardText"), toggleFullscreen: () => ipcRenderer.invoke("folio:toggleFullscreen"), + getZoom: () => ipcRenderer.invoke("folio:getZoom"), + setZoom: (pct: number) => ipcRenderer.invoke("folio:setZoom", pct), // theme preference getTheme: () => ipcRenderer.invoke("folio:getTheme"), @@ -84,4 +86,10 @@ contextBridge.exposeInMainWorld("folio", { ipcRenderer.on("folio:selectAll-requested", () => cb()), onSaveRequested: (cb: () => void) => ipcRenderer.on("folio:save-requested", () => cb()), + onZoomInRequested: (cb: () => void) => + ipcRenderer.on("folio:zoom-in-requested", () => cb()), + onZoomOutRequested: (cb: () => void) => + ipcRenderer.on("folio:zoom-out-requested", () => cb()), + onZoomResetRequested: (cb: () => void) => + ipcRenderer.on("folio:zoom-reset-requested", () => cb()), }); diff --git a/src/renderer/book.ts b/src/renderer/book.ts index 075f8bf..5e4a78c 100644 --- a/src/renderer/book.ts +++ b/src/renderer/book.ts @@ -51,6 +51,8 @@ export type FolioAPI = { toggleFullscreen: () => Promise; getTheme: () => Promise; setTheme: (id: string) => Promise; + getZoom: () => Promise; + setZoom: (pct: number) => Promise; onBookOpened: (cb: (data: { path: string; title: string }) => void) => void; onBookClosed: (cb: () => void) => void; onChaptersChanged: (cb: () => void) => void; @@ -67,6 +69,9 @@ export type FolioAPI = { onRedoRequested: (cb: () => void) => void; onSelectAllRequested: (cb: () => void) => void; onSaveRequested: (cb: () => void) => void; + onZoomInRequested: (cb: () => void) => void; + onZoomOutRequested: (cb: () => void) => void; + onZoomResetRequested: (cb: () => void) => void; }; const $ = (id: string) => document.getElementById(id) as HTMLElement; @@ -1334,6 +1339,45 @@ export function initBook(api: FolioAPI) { }); api.onSaveRequested(() => void saveAndNotify()); + // ---- editor zoom (writing view only) ---- + // Zoom scales the document content area (the .editor-panes containers) so the + // sidebar/navigation keep their normal size. The chosen level is persisted + // app-wide and re-applied on launch. + let currentZoom = 100; + function applyZoom(pct: number, persist = true) { + const clamped = Math.max(50, Math.min(300, Math.round(pct))); + currentZoom = clamped; + document.querySelectorAll(".editor-panes").forEach((el) => { + el.style.zoom = String(clamped / 100); + }); + document.querySelectorAll(".zoom-level").forEach((el) => { + el.textContent = clamped + "%"; + }); + if (persist) void api.setZoom(clamped); + } + document.querySelectorAll(".zoom-control").forEach((ctrl) => { + ctrl + .querySelector('[data-zoom="out"]') + ?.addEventListener("click", () => applyZoom(currentZoom - 10)); + ctrl + .querySelector('[data-zoom="in"]') + ?.addEventListener("click", () => applyZoom(currentZoom + 10)); + ctrl + .querySelector('[data-zoom="reset"]') + ?.addEventListener("click", () => applyZoom(100)); + }); + api.onZoomInRequested(() => applyZoom(currentZoom + 10)); + api.onZoomOutRequested(() => applyZoom(currentZoom - 10)); + api.onZoomResetRequested(() => applyZoom(100)); + void (async () => { + try { + const z = await api.getZoom(); + if (typeof z === "number" && z > 0) applyZoom(z, false); + } catch { + /* keep default 100% */ + } + })(); + // Ctrl/Cmd+S saves the current chapter (in addition to the menu item). window.addEventListener("keydown", (e) => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") { diff --git a/src/renderer/index.css b/src/renderer/index.css index 2dddb61..315ee73 100644 --- a/src/renderer/index.css +++ b/src/renderer/index.css @@ -526,6 +526,32 @@ body.dragging-active * { border-color: var(--color-on-accent-border); } +/* Editor zoom controls (writing view only; sidebar/nav stay fixed size). */ +.zoom-control { + display: inline-flex; + align-items: center; + gap: 2px; +} +.zoom-control .icon-btn { + font-size: 1rem; + line-height: 1; +} +.zoom-control .zoom-level { + min-width: 50px; + border: 1px solid var(--color-border); + background: transparent; + color: var(--color-text); + border-radius: calc(var(--radius) - 4px); + font-size: 0.78rem; + padding: 2px 4px; + cursor: pointer; + text-align: center; +} +.zoom-control .zoom-level:hover { + background: var(--color-on-accent-soft); + border-color: var(--color-on-accent-border); +} + /* Small accent dot shown next to a chapter's title when it has a color. */ .row-color-dot { flex: 0 0 auto; diff --git a/src/renderer/index.html b/src/renderer/index.html index 0d76268..b2e2e1e 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -62,6 +62,11 @@ words: 0 +
+ + + +
@@ -230,6 +235,11 @@
+
+ + + +