mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 19:17:03 +00:00
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:
1 parent
866a971729
commit
acf78dfe29
5 files changed
+122
-26
No files matched your search
Generated
+1
-1
@@ -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
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
Reference in new issue
Block a user