Compare commits
2 commits
8745f88c2c
...
9a1202f0fc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a1202f0fc | ||
|
|
14cad71688 |
6 changed files with 176 additions and 4 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "folio",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "folio",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tiptap/core": "^2.27.2",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "folio",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"description": "A simple, local, open-source desktop writing app.",
|
||||
"productName": "Folio",
|
||||
"author": "Folio Contributors",
|
||||
|
|
|
|||
|
|
@ -340,6 +340,70 @@ handleIpc("folio:pickBackupFolder", async () => {
|
|||
|
||||
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
|
||||
// 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
|
||||
|
|
@ -795,6 +859,12 @@ function wireContextMenu(win: BrowserWindow) {
|
|||
win.webContents.on("context-menu", (_e, params) => {
|
||||
const target = getWindow();
|
||||
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[] = [];
|
||||
if (params.misspelledWord) {
|
||||
const sugs = (params.dictionarySuggestions || []).slice(0, 5);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,20 @@ contextBridge.exposeInMainWorld("folio", {
|
|||
renameBook: (p: string, title: string) => ipcRenderer.invoke("folio:renameBook", p, title),
|
||||
hideRecent: (p: string) => ipcRenderer.invoke("folio:hideRecent", 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),
|
||||
openBook: () => ipcRenderer.invoke("folio:openBook"),
|
||||
openBookPath: (p: string) => ipcRenderer.invoke("folio:openPath", p),
|
||||
|
|
|
|||
|
|
@ -43,6 +43,17 @@ export type FolioAPI = {
|
|||
renameBook: (p: string, title: string) => Promise<any>;
|
||||
hideRecent: (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>;
|
||||
getSpellcheck: () => Promise<boolean>;
|
||||
setSpellcheck: (on: boolean) => Promise<any>;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import type { BookCtx } from "./types.js";
|
||||
import type { ChapterEntry, Recent } from "./types.js";
|
||||
import type { BookMeta } from "../../main/project.js";
|
||||
import { alertMessage, promptInput } from "../dialog.js";
|
||||
import { alertMessage, confirmMessage, promptInput } from "../dialog.js";
|
||||
import { $, showUndoToast, showView } from "./ui.js";
|
||||
|
||||
export interface WelcomeFeature {
|
||||
|
|
@ -221,6 +221,28 @@ export function createWelcomeFeature(ctx: BookCtx): WelcomeFeature {
|
|||
// double-click or Enter opens the book. Replaces the old always-on
|
||||
// "×" button, which deleted on a mis-click.
|
||||
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("keydown", (e) => {
|
||||
if (e.key === "Enter") void openPath(r.path);
|
||||
|
|
@ -363,6 +385,61 @@ export function createWelcomeFeature(ctx: BookCtx): WelcomeFeature {
|
|||
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 {
|
||||
showWelcome,
|
||||
showPlaceholder,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue