Compare commits

..

No commits in common. "9a1202f0fc5aca13e914235ba8b827f2397332d9" and "8745f88c2c7ca2390523d16dae0894ec1d28d519" have entirely different histories.

6 changed files with 4 additions and 176 deletions

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "folio", "name": "folio",
"version": "0.1.6", "version": "0.1.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "folio", "name": "folio",
"version": "0.1.6", "version": "0.1.5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tiptap/core": "^2.27.2", "@tiptap/core": "^2.27.2",

View file

@ -1,6 +1,6 @@
{ {
"name": "folio", "name": "folio",
"version": "0.1.6", "version": "0.1.5",
"description": "A simple, local, open-source desktop writing app.", "description": "A simple, local, open-source desktop writing app.",
"productName": "Folio", "productName": "Folio",
"author": "Folio Contributors", "author": "Folio Contributors",

View file

@ -340,70 +340,6 @@ handleIpc("folio:pickBackupFolder", async () => {
handleIpc("folio:getRecents", (): Recent[] => listRecents()); handleIpc("folio:getRecents", (): Recent[] => listRecents());
// ---- recents right-click menu ----
// The renderer claims a right-click on a recents row synchronously
// (ipcRenderer.sendSync) before the main-process context-menu event fires,
// so the generic spelling/edit popup is skipped for that target and a
// dedicated book menu is shown instead. Menu actions are sent back to the
// renderer (folio:recents-menu) and executed by the same handlers the row
// buttons use — one code path, consistent behavior.
interface RecentsMenuEntry {
path: string;
title: string;
hidden?: boolean;
missing?: boolean;
}
let recentsMenuClaimedAt = 0;
ipcMain.on(
"folio:recents-contextmenu-sync",
(event, entry: RecentsMenuEntry, vx: number, vy: number) => {
if (!entry || typeof entry.path !== "string") {
event.returnValue = false;
return;
}
const win = getWindow();
if (!win) {
event.returnValue = false;
return;
}
recentsMenuClaimedAt = Date.now();
// Convert viewport coords to screen coords for the popup.
const [ox, oy] = win.getContentBounds
? [win.getContentBounds().x, win.getContentBounds().y]
: [0, 0];
const x = Number.isFinite(vx) ? ox + Math.round(vx) : undefined;
const y = Number.isFinite(vy) ? oy + Math.round(vy) : undefined;
const exists = hasBook(entry.path);
const send = (action: string) =>
win.webContents.send("folio:recents-menu", { action, entry });
const items: Electron.MenuItemConstructorOptions[] = [];
if (exists) {
items.push({ label: "Open", click: () => send("open") });
items.push({ label: "Rename…", click: () => send("rename") });
items.push({ label: "Show in Folder", click: () => send("reveal") });
items.push({ type: "separator" });
}
items.push(
entry.hidden
? { label: "Unhide in Recents", click: () => send("unhide") }
: {
label: "Hide from Recents",
toolTip:
"Removes the entry from the list only — the book on disk is untouched",
click: () => send("hide"),
}
);
items.push({ type: "separator" });
items.push({
label: exists ? "Delete Book (to Trash)…" : "Remove from List",
click: () => send(exists ? "delete" : "remove"),
});
Menu.buildFromTemplate(items).popup({ window: win, x, y });
event.returnValue = true;
}
);
// Rename a book's display title (folio.json + matching recents entry). The // Rename a book's display title (folio.json + matching recents entry). The
// folder on disk keeps its name: renaming the directory would invalidate // folder on disk keeps its name: renaming the directory would invalidate
// every other recents/menu path that points at it, so the title is the only // every other recents/menu path that points at it, so the title is the only
@ -859,12 +795,6 @@ function wireContextMenu(win: BrowserWindow) {
win.webContents.on("context-menu", (_e, params) => { win.webContents.on("context-menu", (_e, params) => {
const target = getWindow(); const target = getWindow();
if (!target) return; if (!target) return;
// A recents row claimed this right-click (see the
// folio:recents-contextmenu-sync handler): its dedicated book menu was
// already popped up, so skip the generic spelling/edit popup. The claim
// is only honored within a short window so it can never suppress later
// right-clicks elsewhere in the app.
if (Date.now() - recentsMenuClaimedAt < 250) return;
const items: Electron.MenuItemConstructorOptions[] = []; const items: Electron.MenuItemConstructorOptions[] = [];
if (params.misspelledWord) { if (params.misspelledWord) {
const sugs = (params.dictionarySuggestions || []).slice(0, 5); const sugs = (params.dictionarySuggestions || []).slice(0, 5);

View file

@ -7,20 +7,6 @@ contextBridge.exposeInMainWorld("folio", {
renameBook: (p: string, title: string) => ipcRenderer.invoke("folio:renameBook", p, title), renameBook: (p: string, title: string) => ipcRenderer.invoke("folio:renameBook", p, title),
hideRecent: (p: string) => ipcRenderer.invoke("folio:hideRecent", p), hideRecent: (p: string) => ipcRenderer.invoke("folio:hideRecent", p),
unhideRecent: (p: string) => ipcRenderer.invoke("folio:unhideRecent", p), unhideRecent: (p: string) => ipcRenderer.invoke("folio:unhideRecent", p),
// Synchronously claim a right-click on a recents row so the main process
// pops the dedicated book menu instead of the generic edit menu.
claimRecentsContext: (
p: string,
title: string,
hidden?: boolean,
missing?: boolean,
x?: number,
y?: number
): boolean =>
ipcRenderer.sendSync("folio:recents-contextmenu-sync", { path: p, title, hidden, missing }, x, y) === true,
onRecentsMenu: (
cb: (msg: { action: string; entry: { path: string; title: string; hidden?: boolean; missing?: boolean } }) => void
) => ipcRenderer.on("folio:recents-menu", (_e, msg) => cb(msg)),
restoreBook: (p: string) => ipcRenderer.invoke("folio:restoreBook", p), restoreBook: (p: string) => ipcRenderer.invoke("folio:restoreBook", p),
openBook: () => ipcRenderer.invoke("folio:openBook"), openBook: () => ipcRenderer.invoke("folio:openBook"),
openBookPath: (p: string) => ipcRenderer.invoke("folio:openPath", p), openBookPath: (p: string) => ipcRenderer.invoke("folio:openPath", p),

View file

@ -43,17 +43,6 @@ export type FolioAPI = {
renameBook: (p: string, title: string) => Promise<any>; renameBook: (p: string, title: string) => Promise<any>;
hideRecent: (p: string) => Promise<any>; hideRecent: (p: string) => Promise<any>;
unhideRecent: (p: string) => Promise<any>; unhideRecent: (p: string) => Promise<any>;
claimRecentsContext: (
p: string,
title: string,
hidden?: boolean,
missing?: boolean,
x?: number,
y?: number
) => boolean;
onRecentsMenu: (
cb: (msg: { action: string; entry: { path: string; title: string; hidden?: boolean; missing?: boolean } }) => void
) => void;
closeBook: () => Promise<boolean>; closeBook: () => Promise<boolean>;
getSpellcheck: () => Promise<boolean>; getSpellcheck: () => Promise<boolean>;
setSpellcheck: (on: boolean) => Promise<any>; setSpellcheck: (on: boolean) => Promise<any>;

View file

@ -9,7 +9,7 @@
import type { BookCtx } from "./types.js"; import type { BookCtx } from "./types.js";
import type { ChapterEntry, Recent } from "./types.js"; import type { ChapterEntry, Recent } from "./types.js";
import type { BookMeta } from "../../main/project.js"; import type { BookMeta } from "../../main/project.js";
import { alertMessage, confirmMessage, promptInput } from "../dialog.js"; import { alertMessage, promptInput } from "../dialog.js";
import { $, showUndoToast, showView } from "./ui.js"; import { $, showUndoToast, showView } from "./ui.js";
export interface WelcomeFeature { export interface WelcomeFeature {
@ -221,28 +221,6 @@ export function createWelcomeFeature(ctx: BookCtx): WelcomeFeature {
// double-click or Enter opens the book. Replaces the old always-on // double-click or Enter opens the book. Replaces the old always-on
// "×" button, which deleted on a mis-click. // "×" button, which deleted on a mis-click.
label.addEventListener("click", () => selectRecent(li)); label.addEventListener("click", () => selectRecent(li));
// Right-click (two-finger tap) on a recents row opens a dedicated
// context menu (Open / Rename / Hide / Delete / Show in Folder). The
// renderer claims the event synchronously (sendSync) so the main
// process skips the generic spelling/edit menu for this target; if the
// claim ever fails, fall back to selecting the row inline.
li.addEventListener("contextmenu", (e) => {
e.preventDefault();
try {
const claimed = api.claimRecentsContext(
r.path,
r.title || r.path,
!!r.hidden,
!!r.missing,
e.clientX,
e.clientY
);
if (claimed) return;
} catch {
/* fall through to inline selection */
}
selectRecent(li);
});
li.addEventListener("dblclick", () => void openPath(r.path)); li.addEventListener("dblclick", () => void openPath(r.path));
li.addEventListener("keydown", (e) => { li.addEventListener("keydown", (e) => {
if (e.key === "Enter") void openPath(r.path); if (e.key === "Enter") void openPath(r.path);
@ -385,61 +363,6 @@ export function createWelcomeFeature(ctx: BookCtx): WelcomeFeature {
if (state.currentBook) void api.revealInFolder(state.currentBook.path); if (state.currentBook) void api.revealInFolder(state.currentBook.path);
}); });
// Right-click menu actions (popped by the main process; see
// folio:recents-contextmenu-sync). Runs the same operations as the row
// buttons so behavior is identical from either surface.
api.onRecentsMenu(async ({ action, entry }) => {
switch (action) {
case "open":
await openPath(entry.path);
return;
case "rename": {
const name = await promptInput("Rename book", entry.title || "");
if (name === null) return;
const trimmed = name.trim();
if (!trimmed || trimmed === entry.title) return;
const res = await api.renameBook(entry.path, trimmed);
if (res && "error" in res) {
await alertMessage("Could not rename", (res as { error: string }).error);
return;
}
await renderRecents();
return;
}
case "reveal":
await api.revealInFolder(entry.path);
return;
case "hide":
await api.hideRecent(entry.path);
await renderRecents();
return;
case "unhide":
await api.unhideRecent(entry.path);
await renderRecents();
return;
case "remove":
// Stale entry (book gone): no confirmation needed, nothing to lose.
await api.deleteBook(entry.path);
await renderRecents();
return;
case "delete": {
const yes = await confirmMessage(
`Delete "${entry.title}"?`,
"The whole book folder moves to the system trash. Hide from Recents removes it from the list without touching the disk.",
"Move to Trash"
);
if (!yes) return;
const res = await api.deleteBook(entry.path);
if (res && "error" in res) {
await alertMessage("Could not delete", (res as { error: string }).error);
return;
}
await renderRecents();
return;
}
}
});
return { return {
showWelcome, showWelcome,
showPlaceholder, showPlaceholder,