Add file associations to support system "open with Readest" function, closes #1

Support passing files as command line args to Readest
This commit is contained in:
chrox
2024-12-03 22:14:38 +01:00
parent 5ed74fe9c5
commit 1429f111bc
14 changed files with 371 additions and 18 deletions
+1
View File
@@ -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",
+110
View File
@@ -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"
+1 -3
View File
@@ -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"
@@ -57,6 +57,7 @@
"shell:default",
"updater:default",
"process:default",
"process:allow-restart"
"process:allow-restart",
"cli:default"
]
}
+75 -5
View File
@@ -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<PathBuf>) {
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::<Vec<_>>()
.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::<Vec<_>>();
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());
});
}
});
}
@@ -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"]
+1 -1
View File
@@ -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';
+52 -2
View File
@@ -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
}, []);
+40
View File
@@ -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;
};
+2 -2
View File
@@ -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();
}
}
}
};
+3 -3
View File
@@ -93,7 +93,7 @@ export abstract class BaseAppService implements AppService {
file: string | File,
books: Book[],
overwrite: boolean = false,
): Promise<Book[]> {
): Promise<Book | null> {
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<void> {
@@ -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<LibraryState>((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();
+1 -1
View File
@@ -27,7 +27,7 @@ export interface AppService {
loadSettings(): Promise<SystemSettings>;
saveSettings(settings: SystemSettings): Promise<void>;
importBook(file: string | File, books: Book[], overwrite?: boolean): Promise<Book[]>;
importBook(file: string | File, books: Book[], overwrite?: boolean): Promise<Book | null>;
deleteBook(book: Book): Promise<void>;
loadBookConfig(book: Book, settings: SystemSettings): Promise<BookConfig>;
saveBookConfig(book: Book, config: BookConfig, settings?: SystemSettings): Promise<void>;
+10
View File
@@ -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