Compare commits
No commits in common. "9a1202f0fc5aca13e914235ba8b827f2397332d9" and "8745f88c2c7ca2390523d16dae0894ec1d28d519" have entirely different histories.
9a1202f0fc
...
8745f88c2c
6 changed files with 4 additions and 176 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue