diff --git a/src-tauri/src/commands/server.rs b/src-tauri/src/commands/server.rs index f999322..c307025 100644 --- a/src-tauri/src/commands/server.rs +++ b/src-tauri/src/commands/server.rs @@ -3,7 +3,12 @@ use crate::ServerState; use tauri::Manager; #[tauri::command] -pub fn spawn_server(binary: String, web_ui_enabled: bool, app: tauri::AppHandle) -> Result<(), SpawnError> { +pub fn spawn_server( + binary: String, + binary_args: Option, + web_ui_enabled: bool, + app: tauri::AppHandle, +) -> Result<(), SpawnError> { { let state = app.state::(); if state.0.lock().unwrap().is_some() { @@ -20,9 +25,14 @@ pub fn spawn_server(binary: String, web_ui_enabled: bool, app: tauri::AppHandle) .open(&log_path) .ok(); + let binary_args = binary_args.unwrap_or_default(); + server::do_log( &mut log, - &format!("[spawn_server] binary={:?} web_ui_enabled={} data_dir={:?}", binary, web_ui_enabled, data_dir), + &format!( + "[spawn_server] binary={:?} binary_args={:?} web_ui_enabled={} data_dir={:?}", + binary, binary_args, web_ui_enabled, data_dir + ), ); server::conf::seed_server_conf(&data_dir, web_ui_enabled); @@ -33,6 +43,13 @@ pub fn spawn_server(binary: String, web_ui_enabled: bool, app: tauri::AppHandle) e })?; + if !binary_args.trim().is_empty() { + let extra: Vec = binary_args.split_whitespace().map(|s| s.to_string()).collect(); + let mut merged = extra; + merged.extend(invocation.args); + invocation.args = merged; + } + if invocation.bin.ends_with("java") || invocation.bin.ends_with("java.exe") { let rootdir_flag = format!( "-Dsuwayomi.tachidesk.config.server.rootDir={}", diff --git a/src-tauri/src/commands/system.rs b/src-tauri/src/commands/system.rs index e858648..fd66963 100644 --- a/src-tauri/src/commands/system.rs +++ b/src-tauri/src/commands/system.rs @@ -1,9 +1,7 @@ #[cfg(target_os = "windows")] use crate::server::resolve::strip_unc; -#[cfg(target_os = "windows")] use std::path::PathBuf; use tauri::Manager; -use std::path::PathBuf; #[tauri::command] pub fn get_platform_ui_scale(window: tauri::Window) -> f64 { @@ -54,6 +52,34 @@ pub async fn pick_downloads_folder(app: tauri::AppHandle) -> Option { .map(|p| p.to_string()) } +#[tauri::command] +pub async fn pick_server_binary(app: tauri::AppHandle) -> Option { + use tauri_plugin_dialog::DialogExt; + + #[cfg(target_os = "windows")] + let dialog = app + .dialog() + .file() + .set_title("Choose Server Binary") + .add_filter("Executable", &["exe", "jar", "bat", "cmd"]); + + #[cfg(target_os = "macos")] + let dialog = app + .dialog() + .file() + .set_title("Choose Server Binary") + .add_filter("Executable or JAR", &["jar", "command", "sh", "app"]); + + #[cfg(not(any(target_os = "windows", target_os = "macos")))] + let dialog = app + .dialog() + .file() + .set_title("Choose Server Binary") + .add_filter("Executable or JAR", &["jar", "sh"]); + + dialog.blocking_pick_file().map(|p| p.to_string()) +} + #[tauri::command] pub fn exit_app(app: tauri::AppHandle) { app.exit(0); @@ -100,11 +126,6 @@ pub async fn clear_moku_cache(app: tauri::AppHandle) -> Result<(), String> { let (tx, rx) = tokio::sync::oneshot::channel::>(); - // Note: We intentionally skip the WebView2 COM-level ClearBrowsingDataAll call here. - // The webview2_com crate pulls in a different version of windows_core than Tauri's - // own windows dependency, causing irreconcilable trait-impl conflicts at compile time. - // The filesystem cache removal below (app_cache_dir) is sufficient for our purposes; - // WebView2 will rebuild its cache on next launch from a clean directory. window .with_webview(move |_wv| { let _ = tx.send(Ok(())); @@ -168,4 +189,4 @@ pub fn reset_suwayomi_data(app: tauri::AppHandle) -> Result<(), String> { } } Ok(()) -} +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4a399bd..03c3b2f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -102,6 +102,7 @@ pub fn run() { commands::system::reset_suwayomi_data, commands::system::open_path, commands::system::pick_downloads_folder, + commands::system::pick_server_binary, commands::backup::export_app_data, commands::backup::import_app_data, commands::backup::auto_backup_app_data, @@ -159,5 +160,5 @@ pub fn run() { } }) .run(tauri::generate_context!()) - .expect("error while running moku"); + .expect("error while running moku") } \ No newline at end of file diff --git a/src-tauri/src/server/resolve.rs b/src-tauri/src/server/resolve.rs index a7e294d..8c0c5d6 100644 --- a/src-tauri/src/server/resolve.rs +++ b/src-tauri/src/server/resolve.rs @@ -1,7 +1,6 @@ use crate::server::do_log; use serde::Serialize; use std::path::PathBuf; -use walkdir::WalkDir; use tauri::Manager; #[derive(Serialize, Debug)] @@ -48,22 +47,14 @@ pub fn strip_unc(path: PathBuf) -> PathBuf { } } -#[cfg(not(target_os = "macos"))] -fn find_java_in_bundle(bundle_dir: &PathBuf, log: &mut Option) -> Option { - #[cfg(target_os = "windows")] - let java = strip_unc(bundle_dir.join("jre").join("bin").join("java.exe")); - #[cfg(not(target_os = "windows"))] - let java = bundle_dir.join("jre").join("bin").join("java"); +fn java_bin_name() -> &'static str { + if cfg!(target_os = "windows") { "java.exe" } else { "java" } +} - do_log( - log, - &format!("[find_java] path: {:?} exists: {}", java, java.exists()), - ); - if java.exists() { - Some(java) - } else { - None - } +fn find_java_in_bundle(bundle_dir: &PathBuf, log: &mut Option) -> Option { + let java = bundle_dir.join("jre").join("bin").join(java_bin_name()); + do_log(log, &format!("[find_java] {:?} exists={}", java, java.exists())); + if java.exists() { Some(java) } else { None } } fn data_root_args() -> Vec { @@ -74,24 +65,34 @@ fn jar_data_root_flag() -> String { format!("-Dsuwayomi.server.rootDir={}", suwayomi_data_dir().to_string_lossy()) } +fn jar_invocation(java: PathBuf, jar: PathBuf, working_dir: PathBuf) -> ServerInvocation { + ServerInvocation { + bin: java.to_string_lossy().into_owned(), + args: vec![ + jar_data_root_flag(), + "-jar".to_string(), + jar.to_string_lossy().into_owned(), + ], + working_dir: Some(working_dir), + } +} + pub fn resolve_server_binary( binary: &str, app: &tauri::AppHandle, log: &mut Option, ) -> Result { - do_log(log, &format!("[resolve] binary = {:?}", binary)); + do_log(log, &format!("[resolve] binary={:?}", binary)); if !binary.trim().is_empty() { let path = strip_unc(PathBuf::from(binary.trim())); - do_log( - log, - &format!("[resolve] user path: {:?} exists={}", path, path.exists()), - ); + do_log(log, &format!("[resolve] user path={:?} exists={}", path, path.exists())); if path.exists() { + let working_dir = path.parent().map(|p| p.to_path_buf()); return Ok(ServerInvocation { bin: path.to_string_lossy().into_owned(), args: data_root_args(), - working_dir: path.parent().map(|p| p.to_path_buf()), + working_dir, }); } do_log(log, "[resolve] user path not found, falling through"); @@ -101,10 +102,7 @@ pub fn resolve_server_binary( if let Some(bin_dir) = exe.parent() { for name in &["tachidesk-server", "suwayomi-launcher"] { let p = bin_dir.join(name); - do_log( - log, - &format!("[resolve] sibling: {:?} exists={}", p, p.exists()), - ); + do_log(log, &format!("[resolve] sibling={:?} exists={}", p, p.exists())); if p.exists() { return Ok(ServerInvocation { bin: p.to_string_lossy().into_owned(), @@ -116,54 +114,31 @@ pub fn resolve_server_binary( } } - #[cfg(not(target_os = "macos"))] - let resource_dir = { - let raw = app.path().resource_dir().unwrap_or_default(); - let stripped = strip_unc(raw); - do_log(log, &format!("[resolve] resource_dir = {:?}", stripped)); - stripped - }; - #[cfg(not(target_os = "macos"))] { + let resource_dir = { + let raw = app.path().resource_dir().unwrap_or_default(); + let stripped = strip_unc(raw); + do_log(log, &format!("[resolve] resource_dir={:?}", stripped)); + stripped + }; + let bundle_dir = resource_dir.join("binaries").join("suwayomi-bundle"); let jar = bundle_dir.join("bin").join("Suwayomi-Server.jar"); - do_log( - log, - &format!( - "[resolve] bundle_dir={:?} exists={}", - bundle_dir, - bundle_dir.exists() - ), - ); - do_log( - log, - &format!("[resolve] jar={:?} exists={}", jar, jar.exists()), - ); + do_log(log, &format!("[resolve] bundle_dir={:?} exists={}", bundle_dir, bundle_dir.exists())); + do_log(log, &format!("[resolve] jar={:?} exists={}", jar, jar.exists())); - match find_java_in_bundle(&bundle_dir, log) { - Some(java) if jar.exists() => { - do_log(log, "[resolve] using bundled JRE"); - return Ok(ServerInvocation { - bin: java.to_string_lossy().into_owned(), - args: vec![jar_data_root_flag(), "-jar".to_string(), jar.to_string_lossy().into_owned()], - working_dir: Some(bundle_dir), - }); + if let Some(java) = find_java_in_bundle(&bundle_dir, log) { + if jar.exists() { + do_log(log, "[resolve] using bundled JRE + jar"); + return Ok(jar_invocation(java, jar, bundle_dir)); } - _ => do_log(log, "[resolve] bundled JRE/jar not found, falling through"), } - for name in &[ - "suwayomi-launcher", - "suwayomi-launcher.sh", - "tachidesk-server", - ] { + for name in &["suwayomi-launcher", "suwayomi-launcher.sh", "tachidesk-server"] { let p = resource_dir.join(name); - do_log( - log, - &format!("[resolve] sidecar: {:?} exists={}", p, p.exists()), - ); + do_log(log, &format!("[resolve] sidecar={:?} exists={}", p, p.exists())); if p.exists() { return Ok(ServerInvocation { bin: p.to_string_lossy().into_owned(), @@ -174,26 +149,16 @@ pub fn resolve_server_binary( } if let Some(java) = find_java_in_bundle(&resource_dir, log) { - let jar = std::fs::read_dir(&resource_dir).ok().and_then(|mut rd| { - rd.find(|e| { - e.as_ref() - .map(|e| e.file_name().to_string_lossy().ends_with(".jar")) - .unwrap_or(false) - }) - .and_then(|e| e.ok()) - .map(|e| e.path()) - }); - - if let Some(jar_path) = jar { - do_log( - log, - &format!("[resolve] generic JRE java={:?} jar={:?}", java, jar_path), - ); - return Ok(ServerInvocation { - bin: java.to_string_lossy().into_owned(), - args: vec![jar_data_root_flag(), "-jar".to_string(), jar_path.to_string_lossy().into_owned()], - working_dir: Some(resource_dir), + let jar = std::fs::read_dir(&resource_dir) + .ok() + .and_then(|mut rd| { + rd.find(|e| e.as_ref().map(|e| e.file_name().to_string_lossy().ends_with(".jar")).unwrap_or(false)) + .and_then(|e| e.ok()) + .map(|e| e.path()) }); + if let Some(jar_path) = jar { + do_log(log, &format!("[resolve] generic JRE java={:?} jar={:?}", java, jar_path)); + return Ok(jar_invocation(java, jar_path, resource_dir)); } } } @@ -201,108 +166,43 @@ pub fn resolve_server_binary( #[cfg(target_os = "macos")] { let resource_dir = app.path().resource_dir().unwrap_or_default(); - let contents_dir = resource_dir.parent().unwrap_or(&resource_dir).to_path_buf(); + let bundle_dir = resource_dir.join("suwayomi-bundle"); - do_log( - log, - &format!("[resolve] macOS contents_dir = {:?}", contents_dir), - ); + do_log(log, &format!("[resolve] macOS resource_dir={:?}", resource_dir)); + do_log(log, &format!("[resolve] macOS bundle_dir={:?} exists={}", bundle_dir, bundle_dir.exists())); - const NATIVE_NAMES: &[&str] = &[ - "suwayomi-server-aarch64-apple-darwin", - "suwayomi-server-x86_64-apple-darwin", - "suwayomi-server", - "suwayomi-launcher", - "suwayomi-launcher.sh", - "tachidesk-server", - ]; + let java = bundle_dir.join("jre").join("bin").join("java"); + let jar = bundle_dir.join("bin").join("Suwayomi-Server.jar"); + let launcher_sh = bundle_dir.join("Suwayomi Launcher.command"); + let launcher_jar = bundle_dir.join("Suwayomi-Launcher.jar"); - let mut found_binary: Option = None; - let mut found_java: Option<(PathBuf, PathBuf)> = None; + do_log(log, &format!("[resolve] java={:?} exists={}", java, java.exists())); + do_log(log, &format!("[resolve] jar={:?} exists={}", jar, jar.exists())); + do_log(log, &format!("[resolve] launcher.command={:?} exists={}", launcher_sh, launcher_sh.exists())); + do_log(log, &format!("[resolve] launcher.jar={:?} exists={}", launcher_jar, launcher_jar.exists())); - 'outer: for depth in 0u8..=8 { - let entries: Vec = WalkDir::new(&contents_dir) - .min_depth(depth as usize) - .max_depth(depth as usize) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| e.file_type().is_dir()) - .map(|e| e.into_path()) - .collect(); - - for dir in &entries { - do_log( - log, - &format!("[resolve] scanning depth={} dir={:?}", depth, dir), - ); - - for name in NATIVE_NAMES { - let p = dir.join(name); - if p.exists() { - do_log(log, &format!("[resolve] found native binary: {:?}", p)); - found_binary = Some(ServerInvocation { - bin: p.to_string_lossy().into_owned(), - args: data_root_args(), - working_dir: Some(dir.clone()), - }); - break 'outer; - } - } - - if found_java.is_none() { - let java_exe = dir.join("bin").join("java"); - if java_exe.exists() { - do_log(log, &format!("[resolve] found java: {:?}", java_exe)); - let mut search = dir.as_path(); - 'jar: for _ in 0..5 { - if let Ok(rd) = std::fs::read_dir(search) { - for entry in rd.filter_map(|e| e.ok()) { - if entry.file_name().to_string_lossy().ends_with(".jar") { - let jar = entry.path(); - do_log(log, &format!("[resolve] found jar: {:?}", jar)); - found_java = Some((java_exe.clone(), jar)); - break 'jar; - } - } - } - let bin_sibling = search.join("bin"); - if let Ok(rd) = std::fs::read_dir(&bin_sibling) { - for entry in rd.filter_map(|e| e.ok()) { - if entry.file_name().to_string_lossy().ends_with(".jar") { - let jar = entry.path(); - do_log( - log, - &format!("[resolve] found jar in bin/: {:?}", jar), - ); - found_java = Some((java_exe.clone(), jar)); - break 'jar; - } - } - } - match search.parent() { - Some(p) => search = p, - None => break, - } - } - } - } - } + if java.exists() && jar.exists() { + do_log(log, "[resolve] macOS using bundled JRE + Suwayomi-Server.jar"); + return Ok(jar_invocation(java, jar, bundle_dir)); } - if let Some(inv) = found_binary { - return Ok(inv); - } - - if let Some((java, jar)) = found_java { - let working_dir = jar.parent().map(|p| p.to_path_buf()); + if launcher_sh.exists() { + use std::os::unix::fs::PermissionsExt; + let _ = std::fs::set_permissions(&launcher_sh, std::fs::Permissions::from_mode(0o755)); + do_log(log, "[resolve] macOS using Suwayomi Launcher.command"); return Ok(ServerInvocation { - bin: java.to_string_lossy().into_owned(), - args: vec![jar_data_root_flag(), "-jar".to_string(), jar.to_string_lossy().into_owned()], - working_dir, + bin: launcher_sh.to_string_lossy().into_owned(), + args: vec![], + working_dir: Some(bundle_dir), }); } - do_log(log, "[resolve] macOS scan found nothing in bundle"); + if java.exists() && launcher_jar.exists() { + do_log(log, "[resolve] macOS using bundled JRE + Suwayomi-Launcher.jar"); + return Ok(jar_invocation(java, launcher_jar, bundle_dir)); + } + + do_log(log, "[resolve] macOS bundle not found, falling through to PATH"); } for name in &["suwayomi-server", "tachidesk-server"] { @@ -314,6 +214,7 @@ pub fn resolve_server_binary( .filter(|o| o.status.success()) .and_then(|o| String::from_utf8(o.stdout).ok()) .and_then(|s| s.lines().next().map(|l| l.trim().to_string())); + #[cfg(not(target_os = "windows"))] let resolved = std::process::Command::new("which") .arg(name) diff --git a/src/features/settings/sections/GeneralSettings.svelte b/src/features/settings/sections/GeneralSettings.svelte index bc09eca..35df624 100644 --- a/src/features/settings/sections/GeneralSettings.svelte +++ b/src/features/settings/sections/GeneralSettings.svelte @@ -1,6 +1,7 @@
@@ -43,18 +50,70 @@

Server

+
-
Server URLBase URL of your Suwayomi instance
- updateSettings({ serverUrl: e.currentTarget.value })} placeholder="http://localhost:4567" spellcheck="false" /> +
+ Server URL + Base URL of your Suwayomi instance +
+
+ updateSettings({ serverUrl: e.currentTarget.value })} + placeholder="http://localhost:4567" spellcheck="false" /> + +
+ + + + {#if serverAdvancedOpen} +
+
+
+ Server binary + Path to server executable — leave blank to use bundled +
+
+ updateSettings({ serverBinary: e.currentTarget.value })} + placeholder="auto-detect" spellcheck="false" /> + +
+
+
+ {/if} +
@@ -87,7 +146,7 @@
Close button behaviorWhat happens when you click the X button
{#each [["ask","Ask"],["tray","Tray"],["quit","Quit"]] as [v, l]} - + {/each}
@@ -137,4 +196,70 @@ .s-seg-btn:not(:last-child) { border-right: 1px solid var(--border-strong); } .s-seg-btn.active { background: var(--accent-muted); color: var(--accent-fg); } .s-seg-btn:not(.active):hover { background: var(--bg-raised); color: var(--text-secondary); } + + .srv-url-group { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; + } + + .srv-adv-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + flex-shrink: 0; + border-radius: var(--radius-md); + border: 1px solid var(--border-dim); + background: var(--bg-surface); + color: var(--text-faint); + cursor: pointer; + transition: background var(--t-base), color var(--t-base), border-color var(--t-base); + } + .srv-adv-btn:hover { background: var(--bg-overlay); color: var(--text-muted); border-color: var(--border-strong); } + .srv-adv-btn.open { background: var(--bg-overlay); color: var(--text-secondary); border-color: var(--border-strong); } + .srv-adv-btn svg { transition: transform var(--t-base); } + .srv-adv-btn.open svg { transform: rotate(180deg); } + + .srv-adv-panel { + border-top: 1px solid var(--border-dim); + background: var(--bg-base); + } + + .srv-adv-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px var(--sp-4); + gap: var(--sp-4); + } + + .srv-file-group { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; + } + + .srv-path-input { + width: 160px; + } + + .srv-file-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + flex-shrink: 0; + border-radius: var(--radius-md); + border: 1px solid var(--border-dim); + background: var(--bg-surface); + color: var(--text-faint); + cursor: pointer; + transition: background var(--t-base), color var(--t-base), border-color var(--t-base); + } + .srv-file-btn:hover { background: var(--bg-overlay); color: var(--text-muted); border-color: var(--border-strong); } \ No newline at end of file diff --git a/src/types/settings.ts b/src/types/settings.ts index 3dc7f2b..2d991aa 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -92,7 +92,7 @@ export interface Settings { discordRpc: boolean; chapterSortDir: ChapterSortDir; chapterSortMode: ChapterSortMode; chapterPageSize: number; uiZoom: number; compactSidebar: boolean; gpuAcceleration: boolean; - serverUrl: string; serverBinary: string; autoStartServer: boolean; suwayomiWebUI: boolean; + serverUrl: string; serverBinary: string; serverBinaryArgs: string; autoStartServer: boolean; suwayomiWebUI: boolean; preferredExtensionLang: string; keybinds: Keybinds; idleTimeoutMin?: number; splashCards?: boolean; storageLimitGb: number | null; markReadOnNext: boolean; readerDebounceMs: number; @@ -143,7 +143,7 @@ export const DEFAULT_SETTINGS: Settings = { discordRpc: false, chapterSortDir: "desc", chapterSortMode: "source", chapterPageSize: 25, uiZoom: 1.0, compactSidebar: false, gpuAcceleration: true, - serverUrl: "http://localhost:4567", serverBinary: "", autoStartServer: true, suwayomiWebUI: false, + serverUrl: "http://localhost:4567", serverBinary: "", serverBinaryArgs: "", autoStartServer: true, suwayomiWebUI: false, preferredExtensionLang: "en", keybinds: DEFAULT_KEYBINDS, idleTimeoutMin: 5, splashCards: true, storageLimitGb: null, markReadOnNext: true, readerDebounceMs: 120, autoBookmark: true,