diff --git a/apps/readest-app/src-tauri/src/lib.rs b/apps/readest-app/src-tauri/src/lib.rs index cf63d775..afa80c57 100644 --- a/apps/readest-app/src-tauri/src/lib.rs +++ b/apps/readest-app/src-tauri/src/lib.rs @@ -136,7 +136,7 @@ fn get_executable_dir() -> String { #[derive(Clone, serde::Serialize)] #[allow(dead_code)] -struct Payload { +struct SingleInstancePayload { args: Vec, cwd: String, } @@ -179,7 +179,7 @@ pub fn run() { if !files.is_empty() { allow_file_in_scopes(app, files.clone()); } - app.emit("single-instance", Payload { args: argv, cwd }) + app.emit("single-instance", SingleInstancePayload { args: argv, cwd }) .unwrap(); })); diff --git a/apps/readest-app/src-tauri/src/macos/menu.rs b/apps/readest-app/src-tauri/src/macos/menu.rs index 66bbca83..b7fdb555 100644 --- a/apps/readest-app/src-tauri/src/macos/menu.rs +++ b/apps/readest-app/src-tauri/src/macos/menu.rs @@ -1,8 +1,17 @@ +use crate::allow_file_in_scopes; +use std::path::PathBuf; use tauri::menu::MenuEvent; -use tauri::menu::{SubmenuBuilder, HELP_SUBMENU_ID}; +use tauri::menu::{MenuItemBuilder, SubmenuBuilder, HELP_SUBMENU_ID}; use tauri::AppHandle; +use tauri::Emitter; use tauri_plugin_opener::OpenerExt; +#[derive(Clone, serde::Serialize)] +#[allow(dead_code)] +struct OpenFilesPayload { + files: Vec, +} + pub fn setup_macos_menu(app: &AppHandle) -> tauri::Result<()> { let global_menu = app.menu().unwrap(); @@ -10,6 +19,23 @@ pub fn setup_macos_menu(app: &AppHandle) -> tauri::Result<()> { global_menu.remove(&item)?; } + let open_item = MenuItemBuilder::new("Open...") + .id("open_file") + .accelerator("Cmd+O") + .build(app)?; + + if let Some(file_menu) = global_menu.items()?.iter().find(|item| { + if let Some(submenu) = item.as_submenu() { + submenu.text().ok().as_deref() == Some("File") + } else { + false + } + }) { + if let Some(file_submenu) = file_menu.as_submenu() { + file_submenu.insert(&open_item, 0)?; + } + } + global_menu.append( &SubmenuBuilder::new(app, "Help") .text("privacy_policy", "Privacy Policy") @@ -28,7 +54,9 @@ pub fn setup_macos_menu(app: &AppHandle) -> tauri::Result<()> { pub fn handle_menu_event(app: &AppHandle, event: &MenuEvent) { let opener = app.opener(); - if event.id() == "privacy_policy" { + if event.id() == "open_file" { + handle_open_file(app); + } else if event.id() == "privacy_policy" { let _ = opener.open_url("https://readest.com/privacy-policy", None::<&str>); } else if event.id() == "report_issue" { let _ = opener.open_url("https://github.com/readest/readest/issues", None::<&str>); @@ -36,3 +64,25 @@ pub fn handle_menu_event(app: &AppHandle, event: &MenuEvent) { let _ = opener.open_url("https://readest.com/support", None::<&str>); } } + +fn handle_open_file(app: &AppHandle) { + use tauri_plugin_dialog::DialogExt; + + let app_handle = app.clone(); + + app.dialog() + .file() + .add_filter( + "Files", + &["epub", "pdf", "mobi", "azw", "azw3", "fb2", "cbz", "txt"], + ) + .pick_file(move |file_path| { + if let Some(path) = file_path { + let payload = OpenFilesPayload { + files: vec![path.to_string()], + }; + allow_file_in_scopes(&app_handle, vec![PathBuf::from(path.to_string())]); + let _ = app_handle.emit("open-files", payload); + } + }); +} diff --git a/apps/readest-app/src/helpers/shortcuts.ts b/apps/readest-app/src/helpers/shortcuts.ts index ac6a2d3b..2b547df7 100644 --- a/apps/readest-app/src/helpers/shortcuts.ts +++ b/apps/readest-app/src/helpers/shortcuts.ts @@ -17,7 +17,7 @@ const DEFAULT_SHORTCUTS = { onWikipediaSelection: ['ctrl+w', 'cmd+w'], onReadAloudSelection: ['ctrl+r', 'cmd+r'], onOpenFontLayoutSettings: ['shift+f', 'ctrl+,', 'cmd+,'], - onOpenBooks: ['ctrl+o', 'cmd+o'], + onOpenBooks: ['ctrl+o'], onReloadPage: ['shift+r'], onToggleFullscreen: ['F11'], onCloseWindow: ['ctrl+w', 'cmd+w'], diff --git a/apps/readest-app/src/hooks/useOpenWithBooks.ts b/apps/readest-app/src/hooks/useOpenWithBooks.ts index 055e82a9..f7aa9c41 100644 --- a/apps/readest-app/src/hooks/useOpenWithBooks.ts +++ b/apps/readest-app/src/hooks/useOpenWithBooks.ts @@ -14,6 +14,10 @@ interface SingleInstancePayload { cwd: string; } +interface OpenFilesPayload { + files: string[]; +} + interface SharedIntentPayload { urls: string[]; } @@ -80,8 +84,8 @@ export function useOpenWithBooks() { // For Windows/Linux deep link and macOS open-file event const unlistenDeeplink = getCurrentWindow().listen( 'single-instance', - ({ event, payload }) => { - console.log('Received deep link:', event, payload); + ({ payload }) => { + console.log('Received deep link:', payload); const { args } = payload; if (args?.[1]) { handleOpenWithFileUrl([args[1]]); @@ -89,6 +93,18 @@ export function useOpenWithBooks() { }, ); + // macOS in-app open-files event + const unlistenOpenFiles = getCurrentWindow().listen( + 'open-files', + ({ payload }) => { + console.log('Received open files:', payload); + const { files } = payload; + if (files && files.length > 0) { + handleOpenWithFileUrl(files); + } + }, + ); + // For Android "Share to Readest" intent let unlistenSharedIntent: Promise | null = null; // FIXME: register/unregister plugin listeniner on iOS might cause app freeze for unknown reason @@ -106,6 +122,7 @@ export function useOpenWithBooks() { const unlistenOpenUrl = listenOpenWithFiles(); return () => { unlistenDeeplink.then((f) => f()); + unlistenOpenFiles.then((f) => f()); unlistenOpenUrl.then((f) => f()); unlistenSharedIntent?.then((f) => f.unregister()); };