diff --git a/apps/readest-app/package.json b/apps/readest-app/package.json index 731ca599..063a18b7 100644 --- a/apps/readest-app/package.json +++ b/apps/readest-app/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "@tauri-apps/api": "2.1.1", + "@tauri-apps/plugin-cli": "^2.0.0", "@tauri-apps/plugin-dialog": "^2.0.1", "@tauri-apps/plugin-fs": "^2.0.3", "@tauri-apps/plugin-http": "^2.0.1", diff --git a/apps/readest-app/src-tauri/Cargo.lock b/apps/readest-app/src-tauri/Cargo.lock index b528825f..184ff2d5 100644 --- a/apps/readest-app/src-tauri/Cargo.lock +++ b/apps/readest-app/src-tauri/Cargo.lock @@ -14,6 +14,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build 2.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tauri-plugin-cli", "tauri-plugin-devtools", "tauri-plugin-dialog", "tauri-plugin-fs", @@ -107,6 +108,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.93" @@ -595,6 +645,33 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "clap" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" + [[package]] name = "cocoa" version = "0.25.0" @@ -655,6 +732,12 @@ dependencies = [ "objc", ] +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "colored" version = "2.1.0" @@ -2250,6 +2333,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.12.1" @@ -4684,6 +4773,21 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-cli" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bccd4692b56822a60874542c7655546c8e7aed3c2e2926166e1498e595bd2a2" +dependencies = [ + "clap", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 1.0.69", +] + [[package]] name = "tauri-plugin-devtools" version = "2.0.0" @@ -5609,6 +5713,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.11.0" diff --git a/apps/readest-app/src-tauri/Cargo.toml b/apps/readest-app/src-tauri/Cargo.toml index 8009b79f..ae9f0a92 100644 --- a/apps/readest-app/src-tauri/Cargo.toml +++ b/apps/readest-app/src-tauri/Cargo.toml @@ -40,8 +40,6 @@ cocoa = "0.25" objc = "0.2.7" rand = "0.8" -[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] -tauri-plugin-updater = "2" - [target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies] +tauri-plugin-cli = "2" tauri-plugin-updater = "2" diff --git a/apps/readest-app/src-tauri/capabilities/default.json b/apps/readest-app/src-tauri/capabilities/default.json index 6809037f..00898912 100644 --- a/apps/readest-app/src-tauri/capabilities/default.json +++ b/apps/readest-app/src-tauri/capabilities/default.json @@ -57,6 +57,7 @@ "shell:default", "updater:default", "process:default", - "process:allow-restart" + "process:allow-restart", + "cli:default" ] } diff --git a/apps/readest-app/src-tauri/src/lib.rs b/apps/readest-app/src-tauri/src/lib.rs index 0323dd52..7b5d15d0 100644 --- a/apps/readest-app/src-tauri/src/lib.rs +++ b/apps/readest-app/src-tauri/src/lib.rs @@ -14,8 +14,34 @@ mod tauri_traffic_light_positioner_plugin; #[cfg(target_os = "macos")] use tauri::TitleBarStyle; +use std::path::PathBuf; +use tauri::{AppHandle, Emitter, Listener, Manager, Url}; use tauri::{WebviewUrl, WebviewWindowBuilder}; use tauri_plugin_dialog; +use tauri_plugin_fs::FsExt; + +fn handle_file_associations(app: AppHandle, files: Vec) { + let asset_protocol_scope = app.asset_protocol_scope(); + let fs_scope = app.fs_scope(); + for file in &files { + let _ = fs_scope.allow_file(file); + let _ = asset_protocol_scope.allow_file(file); + } + + let files = files + .into_iter() + .map(|f| { + let file = f.to_string_lossy().replace("\\", "\\\\"); + format!("\"{file}\"",) + }) + .collect::>() + .join(","); + let window = app.get_webview_window("main").unwrap(); + let script = format!("window.TAURI_CLI_ARGS = [{}];", files); + if let Err(e) = window.eval(&script) { + eprintln!("Failed to set open files variable: {}", e); + } +} #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { @@ -31,10 +57,37 @@ pub fn run() { let builder = builder.plugin(tauri_traffic_light_positioner_plugin::init()); builder - .setup(|app| { + .setup(|#[allow(unused_variables)] app| { + #[cfg(desktop)] + { + let mut files = Vec::new(); + // NOTICE: `args` may include URL protocol (`your-app-protocol://`) + // or arguments (`--`) if your app supports them. + // files may aslo be passed as `file://path/to/file` + for maybe_file in std::env::args().skip(1) { + // skip flags like -f or --flag + if maybe_file.starts_with("-") { + continue; + } + // handle `file://` path urls and skip other urls + if let Ok(url) = Url::parse(&maybe_file) { + if let Ok(path) = url.to_file_path() { + files.push(path); + } + } else { + files.push(PathBuf::from(maybe_file)) + } + } + let asset_protocol_scope = app.asset_protocol_scope(); + let fs_scope = app.fs_scope(); + for file in &files { + let _ = fs_scope.allow_file(file); + let _ = asset_protocol_scope.allow_file(file); + } + } + #[cfg(desktop)] + app.handle().plugin(tauri_plugin_cli::init())?; #[cfg(desktop)] - app.handle() - .plugin(tauri_plugin_updater::Builder::new().build())?; if cfg!(debug_assertions) { app.handle().plugin( tauri_plugin_log::Builder::default() @@ -63,8 +116,25 @@ pub fn run() { #[cfg(target_os = "macos")] menu::setup_macos_menu(&app.handle())?; + app.handle().emit("window-ready", {}).unwrap(); + Ok(()) }) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + .build(tauri::generate_context!()) + .expect("error while running tauri application") + .run(|app, event| { + #[cfg(any(target_os = "macos", target_os = "ios"))] + if let tauri::RunEvent::Opened { urls } = event { + let files = urls + .into_iter() + .filter_map(|url| url.to_file_path().ok()) + .collect::>(); + + let app_handle = app.clone(); + app.listen("window-ready", move |_| { + println!("Window is ready, proceeding to handle files."); + handle_file_associations(app_handle.clone(), files.clone()); + }); + } + }); } diff --git a/apps/readest-app/src-tauri/tauri.conf.json b/apps/readest-app/src-tauri/tauri.conf.json index c33809a3..bd2740cd 100644 --- a/apps/readest-app/src-tauri/tauri.conf.json +++ b/apps/readest-app/src-tauri/tauri.conf.json @@ -49,12 +49,81 @@ "macOS": { "minimumSystemVersion": "12.0" }, + "fileAssociations": [ + { + "name": "epub", + "ext": ["epub"], + "description": "EPUB file", + "mimeType": "application/epub+zip", + "role": "Viewer" + }, + { + "name": "mobi", + "ext": ["mobi"], + "description": "MOBI file", + "mimeType": "application/x-mobipocket-ebook", + "role": "Viewer" + }, + { + "name": "azw", + "ext": ["azw", "azw3"], + "description": "AZW file", + "mimeType": "application/vnd.amazon.ebook", + "role": "Viewer" + }, + { + "name": "fb2", + "ext": ["fb2"], + "description": "FB2 file", + "mimeType": "application/x-fictionbook+xml", + "role": "Viewer" + }, + { + "name": "cbz", + "ext": ["cbz"], + "description": "CBZ file", + "mimeType": "application/vnd.comicbook+zip", + "role": "Viewer" + }, + { + "name": "pdf", + "ext": ["pdf"], + "description": "PDF file", + "mimeType": "application/pdf", + "role": "Viewer" + } + ], "createUpdaterArtifacts": true }, "plugins": { "fs": { "requireLiteralLeadingDot": false }, + "cli": { + "description": "Readest CLI", + "args": [ + { + "name": "file1", + "index": 1, + "takesValue": true + }, + { + "name": "file2", + "index": 2, + "takesValue": true + }, + { + "name": "file3", + "index": 3, + "takesValue": true + }, + { + "name": "file4", + "index": 4, + "takesValue": true + } + ] + }, "updater": { "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU0OTAxMURGQkUzQjFENTQKUldSVUhUdSszeEdRNUExdmFkWnlvYWNYNG5wamkxMmUxRk5SejlMOTJVd28yNXlVTDh6Wld4OC8K", "endpoints": ["https://github.com/chrox/readest/releases/latest/download/latest.json"] diff --git a/apps/readest-app/src/app/layout.tsx b/apps/readest-app/src/app/layout.tsx index e474aefe..609cd571 100644 --- a/apps/readest-app/src/app/layout.tsx +++ b/apps/readest-app/src/app/layout.tsx @@ -6,7 +6,7 @@ import { AuthProvider } from '@/context/AuthContext'; import { EnvProvider } from '@/context/EnvContext'; import { CSPostHogProvider } from '@/context/PHContext'; import { useTheme } from '@/hooks/useTheme'; -import { checkForAppUpdates } from '../helpers/updater'; +import { checkForAppUpdates } from '@/helpers/updater'; import '../styles/globals.css'; import '../styles/fonts.css'; diff --git a/apps/readest-app/src/app/library/page.tsx b/apps/readest-app/src/app/library/page.tsx index 2b5a2b60..a33f9826 100644 --- a/apps/readest-app/src/app/library/page.tsx +++ b/apps/readest-app/src/app/library/page.tsx @@ -2,24 +2,55 @@ import * as React from 'react'; import { useState, useRef } from 'react'; +import { useRouter } from 'next/navigation'; import { useEnv } from '@/context/EnvContext'; import { useLibraryStore } from '@/store/libraryStore'; import { useSettingsStore } from '@/store/settingsStore'; +import { Book } from '@/types/book'; +import { parseOpenWithFiles } from '@/helpers/cli'; import Spinner from '@/components/Spinner'; import LibraryHeader from '@/app/library/components/LibraryHeader'; import Bookshelf from '@/app/library/components/Bookshelf'; +import { AppService } from '@/types/system'; const LibraryPage = () => { + const router = useRouter(); const { envConfig, appService } = useEnv(); - const { library: libraryBooks, setLibrary } = useLibraryStore(); + const { + library: libraryBooks, + setLibrary, + checkOpenWithBooks, + clearOpenWithBooks, + } = useLibraryStore(); const { setSettings } = useSettingsStore(); const [loading, setLoading] = useState(false); const isInitiating = useRef(false); const libraryLoaded = useRef(false); const [isSelectMode, setIsSelectMode] = useState(false); + const processOpenWithFiles = React.useCallback( + async (appService: AppService, openWithFiles: string[], libraryBooks: Book[]) => { + const bookIds: string[] = []; + for (const file of openWithFiles) { + const book = await appService.importBook(file, libraryBooks); + if (book) { + bookIds.push(book.hash); + } + } + setLibrary(libraryBooks); + appService.saveLibraryBooks(libraryBooks); + + console.log('Opening books:', bookIds); + if (bookIds.length > 0) { + router.push(`/reader?ids=${bookIds.join(',')}`); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); + React.useEffect(() => { if (isInitiating.current) return; isInitiating.current = true; @@ -29,13 +60,32 @@ const LibraryPage = () => { const appService = await envConfig.getAppService(); const settings = await appService.loadSettings(); setSettings(settings); - setLibrary(await appService.loadLibraryBooks()); + + const libraryBooks = await appService.loadLibraryBooks(); + if (checkOpenWithBooks) { + await handleOpenWithBooks(appService, libraryBooks); + } else { + setLibrary(libraryBooks); + } + libraryLoaded.current = true; if (loadingTimeout) clearTimeout(loadingTimeout); setLoading(false); }; + const handleOpenWithBooks = async (appService: AppService, libraryBooks: Book[]) => { + const openWithFiles = (await parseOpenWithFiles()) || []; + + if (openWithFiles.length > 0) { + await processOpenWithFiles(appService, openWithFiles, libraryBooks); + } else { + setLibrary(libraryBooks); + } + + clearOpenWithBooks(); + }; + initLibrary(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/apps/readest-app/src/helpers/cli.ts b/apps/readest-app/src/helpers/cli.ts new file mode 100644 index 00000000..4ac13fee --- /dev/null +++ b/apps/readest-app/src/helpers/cli.ts @@ -0,0 +1,40 @@ +import { getMatches } from '@tauri-apps/plugin-cli'; + +declare global { + interface Window { + TAURI_CLI_ARGS?: string[]; + } +} + +interface CliArgument { + value: string; + occurrences: number; +} + +const parseGlobalArgs = () => { + return window.TAURI_CLI_ARGS; +}; + +const parseCLIArgs = async () => { + const matches = await getMatches(); + const args = matches?.args; + const files: string[] = []; + if (args) { + for (const name of ['file1', 'file2', 'file3', 'file4']) { + const arg = args[name] as CliArgument; + if (arg && arg.occurrences > 0) { + files.push(arg.value); + } + } + } + + return files; +}; + +export const parseOpenWithFiles = async () => { + let files = parseGlobalArgs(); + if (!files) { + files = await parseCLIArgs(); + } + return files; +}; diff --git a/apps/readest-app/src/helpers/updater.ts b/apps/readest-app/src/helpers/updater.ts index 28e83406..d927d6ce 100644 --- a/apps/readest-app/src/helpers/updater.ts +++ b/apps/readest-app/src/helpers/updater.ts @@ -2,7 +2,7 @@ import { check } from '@tauri-apps/plugin-updater'; import { ask } from '@tauri-apps/plugin-dialog'; import { relaunch } from '@tauri-apps/plugin-process'; -export async function checkForAppUpdates() { +export const checkForAppUpdates = async () => { const update = await check(); if (update) { const yes = await ask( @@ -41,4 +41,4 @@ export async function checkForAppUpdates() { await relaunch(); } } -} +}; diff --git a/apps/readest-app/src/services/appService.ts b/apps/readest-app/src/services/appService.ts index f011858b..24d11777 100644 --- a/apps/readest-app/src/services/appService.ts +++ b/apps/readest-app/src/services/appService.ts @@ -93,7 +93,7 @@ export abstract class BaseAppService implements AppService { file: string | File, books: Book[], overwrite: boolean = false, - ): Promise { + ): Promise { try { let loadedBook: BookDoc; let format: BookFormat; @@ -156,11 +156,11 @@ export abstract class BaseAppService implements AppService { await this.saveBookConfig(book, INIT_BOOK_CONFIG); books.splice(0, 0, book); } + return book; } catch (error) { throw error; } - - return books; + return null; } async deleteBook(book: Book): Promise { diff --git a/apps/readest-app/src/store/libraryStore.ts b/apps/readest-app/src/store/libraryStore.ts index 5fda9c6b..9e2fa911 100644 --- a/apps/readest-app/src/store/libraryStore.ts +++ b/apps/readest-app/src/store/libraryStore.ts @@ -4,12 +4,16 @@ import { EnvConfigType } from '@/services/environment'; interface LibraryState { library: Book[]; + checkOpenWithBooks: boolean; + clearOpenWithBooks: () => void; setLibrary: (books: Book[]) => void; deleteBook: (envConfig: EnvConfigType, book: Book) => void; } export const useLibraryStore = create((set, get) => ({ library: [], + checkOpenWithBooks: true, + clearOpenWithBooks: () => set({ checkOpenWithBooks: false }), setLibrary: (books) => set({ library: books }), deleteBook: async (envConfig: EnvConfigType, book: Book) => { const appService = await envConfig.getAppService(); diff --git a/apps/readest-app/src/types/system.ts b/apps/readest-app/src/types/system.ts index dd689d5e..9e250a15 100644 --- a/apps/readest-app/src/types/system.ts +++ b/apps/readest-app/src/types/system.ts @@ -27,7 +27,7 @@ export interface AppService { loadSettings(): Promise; saveSettings(settings: SystemSettings): Promise; - importBook(file: string | File, books: Book[], overwrite?: boolean): Promise; + importBook(file: string | File, books: Book[], overwrite?: boolean): Promise; deleteBook(book: Book): Promise; loadBookConfig(book: Book, settings: SystemSettings): Promise; saveBookConfig(book: Book, config: BookConfig, settings?: SystemSettings): Promise; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57337e6e..ea5502ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: '@tauri-apps/api': specifier: 2.1.1 version: 2.1.1 + '@tauri-apps/plugin-cli': + specifier: ^2.0.0 + version: 2.0.0 '@tauri-apps/plugin-dialog': specifier: ^2.0.1 version: 2.0.1 @@ -705,6 +708,9 @@ packages: engines: {node: '>= 10'} hasBin: true + '@tauri-apps/plugin-cli@2.0.0': + resolution: {integrity: sha512-glQmlL1IiCGEa1FHYa/PTPSeYhfu56omLRgHXWlJECDt6DbJyRuJWVgtkQfUxtqnVdYnnU+DGIGeiInoEqtjLw==} + '@tauri-apps/plugin-dialog@2.0.1': resolution: {integrity: sha512-fnUrNr6EfvTqdls/ufusU7h6UbNFzLKvHk/zTuOiBq01R3dTODqwctZlzakdbfSp/7pNwTKvgKTAgl/NAP/Z0Q==} @@ -4098,6 +4104,10 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.1.0 '@tauri-apps/cli-win32-x64-msvc': 2.1.0 + '@tauri-apps/plugin-cli@2.0.0': + dependencies: + '@tauri-apps/api': 2.1.1 + '@tauri-apps/plugin-dialog@2.0.1': dependencies: '@tauri-apps/api': 2.1.1