Add document/editor zoom controls
- Persist an app-wide zoom percentage (not per book) and apply it only to the writing view (.editor-panes) so the sidebar/navigation stay fixed size - Add a [-] [100%] [+] control to the chapter and Full Book headers, with a click-to-reset level button - Route Ctrl/Cmd+=, Ctrl/Cmd+-, and Ctrl/Cmd+0 to zoom in/out/reset, intercepting the browser's native zoom
This commit is contained in:
parent
946a2967db
commit
7bdddf32b2
5 changed files with 110 additions and 0 deletions
|
|
@ -143,6 +143,16 @@ function forwardEditShortcuts(win: BrowserWindow) {
|
||||||
} else if ((key === "z" && input.shift) || (key === "y" && !input.shift)) {
|
} else if ((key === "z" && input.shift) || (key === "y" && !input.shift)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
win.webContents.send("folio:redo-requested");
|
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 };
|
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<number>("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 () => {
|
ipcMain.handle("folio:importAttachment", async () => {
|
||||||
const bp = requireBook();
|
const bp = requireBook();
|
||||||
const { canceled, filePaths } = await showOpenDialog({
|
const { canceled, filePaths } = await showOpenDialog({
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ contextBridge.exposeInMainWorld("folio", {
|
||||||
copyText: (text: string) => ipcRenderer.invoke("folio:copyText", text),
|
copyText: (text: string) => ipcRenderer.invoke("folio:copyText", text),
|
||||||
getClipboardText: () => ipcRenderer.invoke("folio:getClipboardText"),
|
getClipboardText: () => ipcRenderer.invoke("folio:getClipboardText"),
|
||||||
toggleFullscreen: () => ipcRenderer.invoke("folio:toggleFullscreen"),
|
toggleFullscreen: () => ipcRenderer.invoke("folio:toggleFullscreen"),
|
||||||
|
getZoom: () => ipcRenderer.invoke("folio:getZoom"),
|
||||||
|
setZoom: (pct: number) => ipcRenderer.invoke("folio:setZoom", pct),
|
||||||
|
|
||||||
// theme preference
|
// theme preference
|
||||||
getTheme: () => ipcRenderer.invoke("folio:getTheme"),
|
getTheme: () => ipcRenderer.invoke("folio:getTheme"),
|
||||||
|
|
@ -84,4 +86,10 @@ contextBridge.exposeInMainWorld("folio", {
|
||||||
ipcRenderer.on("folio:selectAll-requested", () => cb()),
|
ipcRenderer.on("folio:selectAll-requested", () => cb()),
|
||||||
onSaveRequested: (cb: () => void) =>
|
onSaveRequested: (cb: () => void) =>
|
||||||
ipcRenderer.on("folio:save-requested", () => cb()),
|
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()),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,8 @@ export type FolioAPI = {
|
||||||
toggleFullscreen: () => Promise<boolean>;
|
toggleFullscreen: () => Promise<boolean>;
|
||||||
getTheme: () => Promise<string>;
|
getTheme: () => Promise<string>;
|
||||||
setTheme: (id: string) => Promise<any>;
|
setTheme: (id: string) => Promise<any>;
|
||||||
|
getZoom: () => Promise<number>;
|
||||||
|
setZoom: (pct: number) => Promise<any>;
|
||||||
onBookOpened: (cb: (data: { path: string; title: string }) => void) => void;
|
onBookOpened: (cb: (data: { path: string; title: string }) => void) => void;
|
||||||
onBookClosed: (cb: () => void) => void;
|
onBookClosed: (cb: () => void) => void;
|
||||||
onChaptersChanged: (cb: () => void) => void;
|
onChaptersChanged: (cb: () => void) => void;
|
||||||
|
|
@ -67,6 +69,9 @@ export type FolioAPI = {
|
||||||
onRedoRequested: (cb: () => void) => void;
|
onRedoRequested: (cb: () => void) => void;
|
||||||
onSelectAllRequested: (cb: () => void) => void;
|
onSelectAllRequested: (cb: () => void) => void;
|
||||||
onSaveRequested: (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;
|
const $ = (id: string) => document.getElementById(id) as HTMLElement;
|
||||||
|
|
@ -1334,6 +1339,45 @@ export function initBook(api: FolioAPI) {
|
||||||
});
|
});
|
||||||
api.onSaveRequested(() => void saveAndNotify());
|
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<HTMLElement>(".editor-panes").forEach((el) => {
|
||||||
|
el.style.zoom = String(clamped / 100);
|
||||||
|
});
|
||||||
|
document.querySelectorAll<HTMLElement>(".zoom-level").forEach((el) => {
|
||||||
|
el.textContent = clamped + "%";
|
||||||
|
});
|
||||||
|
if (persist) void api.setZoom(clamped);
|
||||||
|
}
|
||||||
|
document.querySelectorAll<HTMLElement>(".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).
|
// Ctrl/Cmd+S saves the current chapter (in addition to the menu item).
|
||||||
window.addEventListener("keydown", (e) => {
|
window.addEventListener("keydown", (e) => {
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
|
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
|
||||||
|
|
|
||||||
|
|
@ -526,6 +526,32 @@ body.dragging-active * {
|
||||||
border-color: var(--color-on-accent-border);
|
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. */
|
/* Small accent dot shown next to a chapter's title when it has a color. */
|
||||||
.row-color-dot {
|
.row-color-dot {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,11 @@
|
||||||
<span class="word-count" id="wordCount">words: 0</span>
|
<span class="word-count" id="wordCount">words: 0</span>
|
||||||
<button class="icon-button" id="exportBtn" title="Export book (Markdown)">⬇</button>
|
<button class="icon-button" id="exportBtn" title="Export book (Markdown)">⬇</button>
|
||||||
<button class="icon-button" id="exportZipBtn" title="Export bundle (ZIP)">📦</button>
|
<button class="icon-button" id="exportZipBtn" title="Export bundle (ZIP)">📦</button>
|
||||||
|
<div class="zoom-control" role="group" aria-label="Zoom">
|
||||||
|
<button class="icon-btn" type="button" data-zoom="out" title="Zoom out (Ctrl+-)">−</button>
|
||||||
|
<button class="zoom-level" type="button" data-zoom="reset" title="Reset zoom to 100% (Ctrl+0)">100%</button>
|
||||||
|
<button class="icon-btn" type="button" data-zoom="in" title="Zoom in (Ctrl+=)">+</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="editor-toolbar" id="editorToolbar">
|
<div class="editor-toolbar" id="editorToolbar">
|
||||||
|
|
@ -230,6 +235,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="button button-primary" id="copyAllBtn" title="Copy the entire assembled manuscript as Markdown">Copy All</button>
|
<button class="button button-primary" id="copyAllBtn" title="Copy the entire assembled manuscript as Markdown">Copy All</button>
|
||||||
|
<div class="zoom-control" role="group" aria-label="Zoom">
|
||||||
|
<button class="icon-btn" type="button" data-zoom="out" title="Zoom out (Ctrl+-)">−</button>
|
||||||
|
<button class="zoom-level" type="button" data-zoom="reset" title="Reset zoom to 100% (Ctrl+0)">100%</button>
|
||||||
|
<button class="icon-btn" type="button" data-zoom="in" title="Zoom in (Ctrl+=)">+</button>
|
||||||
|
</div>
|
||||||
<span class="save-state" id="fullBookStatus"></span>
|
<span class="save-state" id="fullBookStatus"></span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue