feat(menu): reorganize native menu bar with discoverable actions

Move Check for Updates into the macOS app menu and add Settings (Cmd+,),
Save (Cmd+S), Find (Cmd+F), chapter navigation, and a Help > Report an
Issue link. Route new menu actions through menu-action events and trim
duplicate editor keybindings.
This commit is contained in:
pj committed 2026-07-15 09:31:57 +05:30
1 parent 866a971729
commit acf78dfe29
5 files changed
+122 -26

No files matched your search

+1 -1
View File
@@ -4713,7 +4713,7 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]] [[package]]
name = "margin-app" name = "margin-app"
version = "0.1.11" version = "0.1.12"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"fontdb", "fontdb",
+92 -16
View File
@@ -16,50 +16,116 @@ use tauri::{Emitter, Runtime};
#[cfg(desktop)] #[cfg(desktop)]
fn build_menu<R: Runtime>(handle: &tauri::AppHandle<R>) -> tauri::Result<Menu<R>> { fn build_menu<R: Runtime>(handle: &tauri::AppHandle<R>) -> tauri::Result<Menu<R>> {
let menu = Menu::default(handle)?; let menu = Menu::default(handle)?;
let new_book = MenuItemBuilder::with_id("new-book", "New Book") let new_book = MenuItemBuilder::with_id("new-book", "New Book")
.accelerator("CmdOrCtrl+N") .accelerator("CmdOrCtrl+N")
.build(handle)?; .build(handle)?;
let export_pdf = MenuItemBuilder::with_id("export-pdf", "Export as PDF…").build(handle)?; let save = MenuItemBuilder::with_id("save", "Save")
let export_epub = MenuItemBuilder::with_id("export-epub", "Export as EPUB…").build(handle)?; .accelerator("CmdOrCtrl+S")
.build(handle)?;
let export_pdf = MenuItemBuilder::with_id("export-pdf", "Export as PDF…")
.accelerator("CmdOrCtrl+Shift+P")
.build(handle)?;
let export_epub = MenuItemBuilder::with_id("export-epub", "Export as EPUB…")
.accelerator("CmdOrCtrl+Shift+E")
.build(handle)?;
let check_updates = let check_updates =
MenuItemBuilder::with_id("check-updates", "Check for Updates…").build(handle)?; MenuItemBuilder::with_id("check-updates", "Check for Updates…").build(handle)?;
let settings = MenuItemBuilder::with_id("settings", "Settings…")
.accelerator("CmdOrCtrl+,")
.build(handle)?;
let find = MenuItemBuilder::with_id("find", "Find…")
.accelerator("CmdOrCtrl+F")
.build(handle)?;
let report_issue =
MenuItemBuilder::with_id("report-issue", "Report an Issue…").build(handle)?;
let file_submenu = menu let submenus: Vec<_> = menu
.items()? .items()?
.into_iter() .into_iter()
.find_map(|item| match item { .filter_map(|item| match item {
MenuItemKind::Submenu(submenu) MenuItemKind::Submenu(submenu) => Some(submenu),
if submenu.text().map(|t| t == "File").unwrap_or(false) =>
{
Some(submenu)
}
_ => None, _ => None,
}); })
.collect();
match file_submenu { let find_submenu = |name: &str| {
submenus
.iter()
.find(|submenu| submenu.text().map(|t| t == name).unwrap_or(false))
.cloned()
};
match find_submenu("File") {
Some(submenu) => { Some(submenu) => {
submenu.prepend_items(&[ submenu.prepend_items(&[
&new_book, &new_book,
&PredefinedMenuItem::separator(handle)?, &PredefinedMenuItem::separator(handle)?,
&save,
&PredefinedMenuItem::separator(handle)?,
&export_pdf, &export_pdf,
&export_epub, &export_epub,
&PredefinedMenuItem::separator(handle)?, &PredefinedMenuItem::separator(handle)?,
&check_updates,
&PredefinedMenuItem::separator(handle)?,
])?; ])?;
} }
None => { None => {
let submenu = SubmenuBuilder::new(handle, "File") let submenu = SubmenuBuilder::new(handle, "File")
.item(&new_book) .item(&new_book)
.item(&PredefinedMenuItem::separator(handle)?) .item(&PredefinedMenuItem::separator(handle)?)
.item(&save)
.item(&PredefinedMenuItem::separator(handle)?)
.item(&export_pdf) .item(&export_pdf)
.item(&export_epub) .item(&export_epub)
.item(&PredefinedMenuItem::separator(handle)?)
.item(&check_updates)
.build()?; .build()?;
menu.insert(&submenu, 1)?; menu.insert(&submenu, 1)?;
} }
} }
if let Some(edit) = find_submenu("Edit") {
edit.append_items(&[&PredefinedMenuItem::separator(handle)?, &find])?;
}
if let Some(help) = find_submenu("Help") {
help.append_items(&[&report_issue])?;
}
#[cfg(target_os = "macos")]
{
if let Some(app_submenu) = submenus.first() {
app_submenu.insert(&check_updates, 1)?;
app_submenu.insert(&settings, 3)?;
app_submenu.insert(&PredefinedMenuItem::separator(handle)?, 4)?;
}
if let Some(view) = find_submenu("View") {
let toggle_chapters = MenuItemBuilder::with_id("toggle-chapters", "Toggle Chapters")
.accelerator("CmdOrCtrl+\\")
.build(handle)?;
let next_chapter = MenuItemBuilder::with_id("next-chapter", "Next Chapter")
.accelerator("CmdOrCtrl+Alt+Down")
.build(handle)?;
let prev_chapter = MenuItemBuilder::with_id("prev-chapter", "Previous Chapter")
.accelerator("CmdOrCtrl+Alt+Up")
.build(handle)?;
view.prepend_items(&[
&toggle_chapters,
&PredefinedMenuItem::separator(handle)?,
&next_chapter,
&prev_chapter,
&PredefinedMenuItem::separator(handle)?,
])?;
}
}
#[cfg(not(target_os = "macos"))]
{
if let Some(file) = find_submenu("File") {
file.append_items(&[&PredefinedMenuItem::separator(handle)?, &check_updates])?;
}
if let Some(edit) = find_submenu("Edit") {
edit.append_items(&[&settings])?;
}
}
Ok(menu) Ok(menu)
} }
@@ -95,7 +161,17 @@ pub fn run() {
.on_menu_event(|app, event| { .on_menu_event(|app, event| {
if matches!( if matches!(
event.id().0.as_str(), event.id().0.as_str(),
"new-book" | "export-pdf" | "export-epub" | "check-updates" "new-book"
| "save"
| "export-pdf"
| "export-epub"
| "check-updates"
| "settings"
| "find"
| "toggle-chapters"
| "next-chapter"
| "prev-chapter"
| "report-issue"
) { ) {
app.emit("menu-action", event.id().0.as_str()).ok(); app.emit("menu-action", event.id().0.as_str()).ok();
} }
+3
View File
@@ -1,5 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { openUrl } from "@tauri-apps/plugin-opener";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import { Library } from "./components/Library"; import { Library } from "./components/Library";
import { EditorView } from "./components/EditorView"; import { EditorView } from "./components/EditorView";
@@ -29,6 +30,8 @@ function App() {
} }
else if (event.payload === "export-epub") runExport("epub"); else if (event.payload === "export-epub") runExport("epub");
else if (event.payload === "check-updates") checkForUpdates(false); else if (event.payload === "check-updates") checkForUpdates(false);
else if (event.payload === "report-issue")
openUrl("https://github.com/priyanshujain/margin/issues").catch(() => {});
}); });
const unlistenWarn = listen<string>("pdf-warnings", (event) => { const unlistenWarn = listen<string>("pdf-warnings", (event) => {
useBook.getState().setNotice(`PDF exported with warnings:\n${event.payload}`); useBook.getState().setNotice(`PDF exported with warnings:\n${event.payload}`);
+25 -8
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { listen } from "@tauri-apps/api/event";
import type { Editor as TiptapEditor } from "@tiptap/react"; import type { Editor as TiptapEditor } from "@tiptap/react";
import { Sidebar } from "./Sidebar"; import { Sidebar } from "./Sidebar";
import { Dock } from "./Dock"; import { Dock } from "./Dock";
@@ -129,17 +130,14 @@ export function EditorView() {
useEffect(() => { useEffect(() => {
const onKey = (e: KeyboardEvent) => { const onKey = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "s") { if (e.metaKey && e.altKey && e.key.toLowerCase() === "f") {
e.preventDefault(); e.preventDefault();
saveNow(); openFind(true);
} else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "f") { } else if (e.metaKey && e.altKey && e.key === "ArrowRight") {
e.preventDefault();
openFind(e.altKey);
} else if (e.metaKey && e.altKey && (e.key === "ArrowDown" || e.key === "ArrowRight")) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
switchChapter(1); switchChapter(1);
} else if (e.metaKey && e.altKey && (e.key === "ArrowUp" || e.key === "ArrowLeft")) { } else if (e.metaKey && e.altKey && e.key === "ArrowLeft") {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
switchChapter(-1); switchChapter(-1);
@@ -151,7 +149,26 @@ export function EditorView() {
}; };
window.addEventListener("keydown", onKey, true); window.addEventListener("keydown", onKey, true);
return () => window.removeEventListener("keydown", onKey, true); return () => window.removeEventListener("keydown", onKey, true);
}, [saveNow, openFind, switchChapter]); }, [openFind, switchChapter]);
const menuCommands = useRef({ saveNow, openFind, setSettingsOpen, toggleSidebar, switchChapter });
menuCommands.current = { saveNow, openFind, setSettingsOpen, toggleSidebar, switchChapter };
useEffect(() => {
if (!isDesktop) return;
const unlisten = listen<string>("menu-action", (e) => {
const cmd = menuCommands.current;
if (e.payload === "save") cmd.saveNow();
else if (e.payload === "find") cmd.openFind(false);
else if (e.payload === "settings") cmd.setSettingsOpen(true);
else if (e.payload === "toggle-chapters") cmd.toggleSidebar();
else if (e.payload === "next-chapter") cmd.switchChapter(1);
else if (e.payload === "prev-chapter") cmd.switchChapter(-1);
});
return () => {
unlisten.then((stop) => stop());
};
}, []);
useEffect(() => { useEffect(() => {
if (!widthOpen && !exportOpen && !moreOpen) return; if (!widthOpen && !exportOpen && !moreOpen) return;
+1 -1
View File
@@ -20,7 +20,7 @@ const TRIM: Record<TrimSize, { w: string; h: string }> = {
a5: { w: "148mm", h: "210mm" }, a5: { w: "148mm", h: "210mm" },
}; };
const INLINE_SPECIAL = /[\\#$*_`<>@~[\]]/g; const INLINE_SPECIAL = /[\\#$*_`<>@~()[\]]/g;
const LINE_SEPARATORS = /[\n\r\v\f\u2028\u2029]+/g; const LINE_SEPARATORS = /[\n\r\v\f\u2028\u2029]+/g;
function esc(text: string): string { function esc(text: string): string {