diff --git a/flake.nix b/flake.nix index 832ed58..5a8c0e8 100644 --- a/flake.nix +++ b/flake.nix @@ -71,7 +71,7 @@ inherit version; src = frontendSrc; fetcherVersion = 1; - hash = "sha256-nlhm3NYn4x+JlKcCgj1lAX43muB3QRKGDzaxfQNfJwc="; + hash = "sha256-t6Gj84hCE3CuDAJfbdXi0FuqgPCqlkMmAzETcKL4e3U="; }; buildPhase = "pnpm build"; diff --git a/package.json b/package.json index 8887fbc..3c11158 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@tauri-apps/plugin-http": "^2.5.8", "@tauri-apps/plugin-os": "^2.3.2", "@tauri-apps/plugin-shell": "^2.3.5", + "@tauri-apps/plugin-store": "~2.4.2", "clsx": "^2.1.1", "phosphor-svelte": "^3.1.0", "svelte-spa-router": "^4.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0e94a2..f6da067 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@tauri-apps/plugin-shell': specifier: ^2.3.5 version: 2.3.5 + '@tauri-apps/plugin-store': + specifier: ~2.4.2 + version: 2.4.2 clsx: specifier: ^2.1.1 version: 2.1.1 @@ -457,6 +460,9 @@ packages: '@tauri-apps/plugin-shell@2.3.5': resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==} + '@tauri-apps/plugin-store@2.4.2': + resolution: {integrity: sha512-0ClHS50Oq9HEvLPhNzTNFxbWVOqoAp3dRvtewQBeqfIQ0z5m3JRnOISIn2ZVPCrQC0MyGyhTS9DWhHjpigQE7A==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1071,6 +1077,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.10.1 + '@tauri-apps/plugin-store@2.4.2': + dependencies: + '@tauri-apps/api': 2.10.1 + '@types/estree@1.0.8': {} '@types/pug@2.0.10': {} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c7542b7..4ed22b9 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2107,6 +2107,7 @@ dependencies = [ "tauri-plugin-os", "tauri-plugin-process", "tauri-plugin-shell", + "tauri-plugin-store", "tokio", "urlencoding", "walkdir", @@ -4435,6 +4436,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "tauri-plugin-store" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca1a8ff83c269b115e98726ffc13f9e548a10161544a92ad121d6d0a96e16ea" +dependencies = [ + "dunce", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "tauri-runtime" version = "2.10.1" @@ -4896,9 +4913,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "tracing-core" version = "0.1.36" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 82ab9b1..45bc6b3 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ tauri-plugin-process = "2" tauri-plugin-http = "2" tauri-plugin-dialog = "2" tauri-plugin-os = "2.3.2" +tauri-plugin-store = "2" tauri-plugin-discord-rpc = { git = "https://github.com/Youwes09/tauri-plugin-discord-rpc" } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 82d481c..d860e1e 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,3 @@ fn main() { tauri_build::build() -} \ No newline at end of file +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 777dfe3..a1a1794 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -30,6 +30,7 @@ "process:allow-restart", "http:default", "http:allow-fetch", + "store:default", "discord-rpc:default", "discord-rpc:allow-connect", "discord-rpc:allow-disconnect", diff --git a/src-tauri/src/commands/backup.rs b/src-tauri/src/commands/backup.rs index 9a2c483..2f9932d 100644 --- a/src-tauri/src/commands/backup.rs +++ b/src-tauri/src/commands/backup.rs @@ -2,7 +2,8 @@ use std::path::PathBuf; use tauri::Manager; fn backup_dir(app: &tauri::AppHandle) -> PathBuf { - app.path().app_data_dir() + app.path() + .app_data_dir() .unwrap_or_else(|_| PathBuf::from(".")) .join("backups") } @@ -20,7 +21,8 @@ pub async fn export_app_data(app: tauri::AppHandle, json: String) -> Result Result Result { use tauri_plugin_dialog::DialogExt; - let path = app.dialog() + let path = app + .dialog() .file() .set_title("Open Moku app data backup") .blocking_pick_file() @@ -57,7 +60,11 @@ pub fn auto_backup_app_data(app: tauri::AppHandle, json: String) -> Result<(), S let mut entries: Vec<_> = std::fs::read_dir(&dir) .map_err(|e| e.to_string())? .filter_map(|e| e.ok()) - .filter(|e| e.file_name().to_string_lossy().starts_with("auto-moku-backup-")) + .filter(|e| { + e.file_name() + .to_string_lossy() + .starts_with("auto-moku-backup-") + }) .collect(); entries.sort_by_key(|e| e.file_name()); @@ -72,4 +79,4 @@ pub fn auto_backup_app_data(app: tauri::AppHandle, json: String) -> Result<(), S #[tauri::command] pub fn get_auto_backup_dir(app: tauri::AppHandle) -> String { backup_dir(&app).to_string_lossy().into_owned() -} \ No newline at end of file +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 6583021..ece0d93 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -2,4 +2,4 @@ pub mod backup; pub mod server; pub mod storage; pub mod system; -pub mod updater; \ No newline at end of file +pub mod updater; diff --git a/src-tauri/src/commands/server.rs b/src-tauri/src/commands/server.rs index 7c70cb1..0dd3341 100644 --- a/src-tauri/src/commands/server.rs +++ b/src-tauri/src/commands/server.rs @@ -1,6 +1,6 @@ -use tauri::Manager; use crate::server::{self, resolve::suwayomi_data_dir, SpawnError}; use crate::ServerState; +use tauri::Manager; #[tauri::command] pub fn spawn_server(binary: String, app: tauri::AppHandle) -> Result<(), SpawnError> { @@ -14,16 +14,24 @@ pub fn spawn_server(binary: String, app: tauri::AppHandle) -> Result<(), SpawnEr let data_dir = suwayomi_data_dir(); let log_path = data_dir.join("moku-spawn.log"); let _ = std::fs::create_dir_all(&data_dir); - let mut log = std::fs::OpenOptions::new().create(true).append(true).open(&log_path).ok(); + let mut log = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&log_path) + .ok(); - server::do_log(&mut log, &format!("[spawn_server] binary={:?} data_dir={:?}", binary, data_dir)); + server::do_log( + &mut log, + &format!("[spawn_server] binary={:?} data_dir={:?}", binary, data_dir), + ); server::conf::seed_server_conf(&data_dir); - let mut invocation = server::resolve::resolve_server_binary(&binary, &app, &mut log).map_err(|e| { - server::do_log(&mut log, &format!("[spawn_server] resolve failed: {:?}", e)); - e - })?; + let mut invocation = + server::resolve::resolve_server_binary(&binary, &app, &mut log).map_err(|e| { + server::do_log(&mut log, &format!("[spawn_server] resolve failed: {:?}", e)); + e + })?; if invocation.bin.ends_with("java") || invocation.bin.ends_with("java.exe") { let rootdir_flag = format!( @@ -33,12 +41,21 @@ pub fn spawn_server(binary: String, app: tauri::AppHandle) -> Result<(), SpawnEr invocation.args.insert(0, rootdir_flag); } - let working_dir = invocation.working_dir.unwrap_or_else(|| std::env::current_dir().unwrap_or_default()); + let working_dir = invocation + .working_dir + .unwrap_or_else(|| std::env::current_dir().unwrap_or_default()); - server::do_log(&mut log, &format!("[spawn_server] bin={:?} args={:?} cwd={:?}", invocation.bin, invocation.args, working_dir)); + server::do_log( + &mut log, + &format!( + "[spawn_server] bin={:?} args={:?} cwd={:?}", + invocation.bin, invocation.args, working_dir + ), + ); use tauri_plugin_shell::ShellExt; - let cmd = app.shell() + let cmd = app + .shell() .command(&invocation.bin) .env("JAVA_TOOL_OPTIONS", "-Djava.awt.headless=true") .args(&invocation.args) @@ -60,4 +77,4 @@ pub fn spawn_server(binary: String, app: tauri::AppHandle) -> Result<(), SpawnEr pub fn kill_server(app: tauri::AppHandle) -> Result<(), String> { server::kill_tachidesk(&app); Ok(()) -} \ No newline at end of file +} diff --git a/src-tauri/src/commands/storage.rs b/src-tauri/src/commands/storage.rs index 92eca4b..65289a7 100644 --- a/src-tauri/src/commands/storage.rs +++ b/src-tauri/src/commands/storage.rs @@ -1,5 +1,5 @@ -use std::path::PathBuf; use serde::Serialize; +use std::path::PathBuf; use sysinfo::Disks; use tauri::Emitter; use walkdir::WalkDir; @@ -10,8 +10,8 @@ use crate::server::resolve::suwayomi_data_dir; pub struct StorageInfo { pub manga_bytes: u64, pub total_bytes: u64, - pub free_bytes: u64, - pub path: String, + pub free_bytes: u64, + pub path: String, } fn resolve_downloads_path(downloads_path: &str) -> PathBuf { @@ -53,8 +53,8 @@ pub fn get_storage_info(downloads_path: String) -> Result { Ok(StorageInfo { manga_bytes, total_bytes: disk.total_space(), - free_bytes: disk.available_space(), - path: path.to_string_lossy().into_owned(), + free_bytes: disk.available_space(), + path: path.to_string_lossy().into_owned(), }) } @@ -74,7 +74,11 @@ pub fn create_directory(path: String) -> Result<(), String> { } #[tauri::command] -pub async fn migrate_downloads(app: tauri::AppHandle, src: String, dst: String) -> Result<(), String> { +pub async fn migrate_downloads( + app: tauri::AppHandle, + src: String, + dst: String, +) -> Result<(), String> { use std::fs; let src_path = PathBuf::from(src.trim()); @@ -90,12 +94,18 @@ pub async fn migrate_downloads(app: tauri::AppHandle, src: String, dst: String) .filter(|e| e.file_type().is_file()) .count() as u64; - let _ = app.emit("migrate_progress", serde_json::json!({ "done": 0u64, "total": total, "current": "" })); + let _ = app.emit( + "migrate_progress", + serde_json::json!({ "done": 0u64, "total": total, "current": "" }), + ); let mut done: u64 = 0; for entry in WalkDir::new(&src_path).into_iter().filter_map(|e| e.ok()) { - let rel = entry.path().strip_prefix(&src_path).map_err(|e| e.to_string())?; + let rel = entry + .path() + .strip_prefix(&src_path) + .map_err(|e| e.to_string())?; let target = dst_path.join(rel); if entry.file_type().is_dir() { @@ -106,12 +116,15 @@ pub async fn migrate_downloads(app: tauri::AppHandle, src: String, dst: String) } fs::copy(entry.path(), &target).map_err(|e| e.to_string())?; done += 1; - let _ = app.emit("migrate_progress", serde_json::json!({ - "done": done, "total": total, "current": rel.to_string_lossy() - })); + let _ = app.emit( + "migrate_progress", + serde_json::json!({ + "done": done, "total": total, "current": rel.to_string_lossy() + }), + ); } } fs::remove_dir_all(&src_path).map_err(|e| e.to_string())?; Ok(()) -} \ No newline at end of file +} diff --git a/src-tauri/src/commands/system.rs b/src-tauri/src/commands/system.rs index a9022b9..73041c3 100644 --- a/src-tauri/src/commands/system.rs +++ b/src-tauri/src/commands/system.rs @@ -1,6 +1,6 @@ -use tauri::Manager; #[cfg(target_os = "windows")] use crate::server::resolve::strip_unc; +use tauri::Manager; #[tauri::command] pub fn get_platform_ui_scale(window: tauri::Window) -> f64 { @@ -49,4 +49,4 @@ pub async fn pick_downloads_folder(app: tauri::AppHandle) -> Option { .set_title("Choose Downloads Folder") .blocking_pick_folder() .map(|p| p.to_string()) -} \ No newline at end of file +} diff --git a/src-tauri/src/commands/updater.rs b/src-tauri/src/commands/updater.rs index 38735e2..8b8258c 100644 --- a/src-tauri/src/commands/updater.rs +++ b/src-tauri/src/commands/updater.rs @@ -2,18 +2,18 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Clone)] pub struct ReleaseInfo { - pub tag_name: String, - pub name: String, - pub body: String, + pub tag_name: String, + pub name: String, + pub body: String, pub published_at: String, - pub html_url: String, + pub html_url: String, } #[derive(Clone, Serialize)] #[cfg_attr(not(target_os = "windows"), allow(dead_code))] struct UpdateProgress { downloaded: u64, - total: Option, + total: Option, } #[tauri::command] @@ -22,11 +22,11 @@ pub async fn list_releases() -> Result, String> { #[derive(Deserialize)] struct GhRelease { - tag_name: String, - name: Option, - body: Option, + tag_name: String, + name: Option, + body: Option, published_at: Option, - html_url: String, + html_url: String, } let client = reqwest::Client::builder() @@ -44,17 +44,20 @@ pub async fn list_releases() -> Result, String> { return Err(format!("GitHub API returned {}", resp.status())); } - let releases: Vec = serde_json::from_str( - &resp.text().await.map_err(|e| e.to_string())? - ).map_err(|e| e.to_string())?; + let releases: Vec = + serde_json::from_str(&resp.text().await.map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())?; - Ok(releases.into_iter().map(|r| ReleaseInfo { - tag_name: r.tag_name.clone(), - name: r.name.unwrap_or_else(|| r.tag_name.clone()), - body: r.body.unwrap_or_default(), - published_at: r.published_at.unwrap_or_default(), - html_url: r.html_url, - }).collect()) + Ok(releases + .into_iter() + .map(|r| ReleaseInfo { + tag_name: r.tag_name.clone(), + name: r.name.unwrap_or_else(|| r.tag_name.clone()), + body: r.body.unwrap_or_default(), + published_at: r.published_at.unwrap_or_default(), + html_url: r.html_url, + }) + .collect()) } #[tauri::command] @@ -70,9 +73,15 @@ pub async fn download_and_install_update(app: tauri::AppHandle, tag: String) -> use tauri_plugin_http::reqwest; #[derive(Deserialize)] - struct Asset { name: String, browser_download_url: String, size: u64 } + struct Asset { + name: String, + browser_download_url: String, + size: u64, + } #[derive(Deserialize)] - struct Release { assets: Vec } + struct Release { + assets: Vec, + } let client = reqwest::Client::builder() .user_agent("Moku") @@ -80,26 +89,41 @@ pub async fn download_and_install_update(app: tauri::AppHandle, tag: String) -> .map_err(|e| e.to_string())?; let resp = client - .get(format!("https://api.github.com/repos/moku-project/Moku/releases/tags/{}", tag)) + .get(format!( + "https://api.github.com/repos/moku-project/Moku/releases/tags/{}", + tag + )) .send() .await .map_err(|e| e.to_string())?; if !resp.status().is_success() { - return Err(format!("GitHub API returned {} for tag {}", resp.status(), tag)); + return Err(format!( + "GitHub API returned {} for tag {}", + resp.status(), + tag + )); } - let release: Release = serde_json::from_str( - &resp.text().await.map_err(|e| e.to_string())? - ).map_err(|e| e.to_string())?; + let release: Release = serde_json::from_str(&resp.text().await.map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())?; - let asset = release.assets + let asset = release + .assets .into_iter() .find(|a| a.name.ends_with("_x64-setup.exe")) .ok_or_else(|| format!("No x64-setup.exe asset found in release {}", tag))?; - let total = if asset.size > 0 { Some(asset.size) } else { None }; - let mut resp = client.get(&asset.browser_download_url).send().await.map_err(|e| e.to_string())?; + let total = if asset.size > 0 { + Some(asset.size) + } else { + None + }; + let mut resp = client + .get(&asset.browser_download_url) + .send() + .await + .map_err(|e| e.to_string())?; let tmp_path = std::env::temp_dir().join(&asset.name); let mut file = std::fs::File::create(&tmp_path).map_err(|e| e.to_string())?; @@ -123,4 +147,4 @@ pub async fn download_and_install_update(app: tauri::AppHandle, tag: String) -> Ok(()) } -} \ No newline at end of file +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 25118a9..db0ae22 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -10,6 +10,8 @@ pub struct ServerState(pub Mutex>); #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_store::Builder::new().build()) + .plugin(tauri_plugin_store::Builder::default().build()) .plugin(tauri_plugin_discord_rpc::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_os::init()) @@ -44,4 +46,4 @@ pub fn run() { }) .run(tauri::generate_context!()) .expect("error while running moku"); -} \ No newline at end of file +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 25c98f4..6d38072 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,4 +2,4 @@ fn main() { moku_lib::run(); -} \ No newline at end of file +} diff --git a/src-tauri/src/server/conf.rs b/src-tauri/src/server/conf.rs index d5302cc..25da9e6 100644 --- a/src-tauri/src/server/conf.rs +++ b/src-tauri/src/server/conf.rs @@ -31,14 +31,18 @@ pub fn seed_server_conf(data_dir: &PathBuf) { return; } - let Ok(contents) = std::fs::read_to_string(&conf_path) else { return }; + let Ok(contents) = std::fs::read_to_string(&conf_path) else { + return; + }; let patched = patch_conf_key( patch_conf_key( patch_conf_key(contents, "server.webUIEnabled", "false"), - "server.initialOpenInBrowserEnabled", "false", + "server.initialOpenInBrowserEnabled", + "false", ), - "server.systemTrayEnabled", "false", + "server.systemTrayEnabled", + "false", ); let _ = std::fs::write(&conf_path, patched); @@ -60,8 +64,10 @@ fn patch_conf_key(text: String, key: &str, value: &str) -> String { } let mut out = text; - if !out.ends_with('\n') { out.push('\n'); } + if !out.ends_with('\n') { + out.push('\n'); + } out.push_str(&replacement); out.push('\n'); out -} \ No newline at end of file +} diff --git a/src-tauri/src/server/mod.rs b/src-tauri/src/server/mod.rs index 944ec22..496966c 100644 --- a/src-tauri/src/server/mod.rs +++ b/src-tauri/src/server/mod.rs @@ -39,11 +39,15 @@ pub fn kill_tachidesk(app: &tauri::AppHandle) { .map(|o| String::from_utf8_lossy(&o.stdout).contains("java.exe")) .unwrap_or(false); - if !still_running { break; } + if !still_running { + break; + } std::thread::sleep(std::time::Duration::from_millis(100)); } } #[cfg(not(target_os = "windows"))] - let _ = std::process::Command::new("pkill").args(["-f", "tachidesk"]).status(); -} \ No newline at end of file + let _ = std::process::Command::new("pkill") + .args(["-f", "tachidesk"]) + .status(); +} diff --git a/src-tauri/src/server/resolve.rs b/src-tauri/src/server/resolve.rs index 77e7b5f..733dae4 100644 --- a/src-tauri/src/server/resolve.rs +++ b/src-tauri/src/server/resolve.rs @@ -1,7 +1,7 @@ -use std::path::PathBuf; -use serde::Serialize; -use tauri::Manager; use crate::server::do_log; +use serde::Serialize; +use std::path::PathBuf; +use tauri::Manager; #[derive(Serialize, Debug)] #[serde(tag = "kind", content = "message")] @@ -11,8 +11,8 @@ pub enum SpawnError { } pub struct ServerInvocation { - pub bin: String, - pub args: Vec, + pub bin: String, + pub args: Vec, pub working_dir: Option, } @@ -54,8 +54,15 @@ fn find_java_in_bundle(bundle_dir: &PathBuf, log: &mut Option) -> #[cfg(not(target_os = "windows"))] let java = bundle_dir.join("jre").join("bin").join("java"); - do_log(log, &format!("[find_java] path: {:?} exists: {}", java, java.exists())); - if java.exists() { Some(java) } else { None } + do_log( + log, + &format!("[find_java] path: {:?} exists: {}", java, java.exists()), + ); + if java.exists() { + Some(java) + } else { + None + } } pub fn resolve_server_binary( @@ -67,11 +74,14 @@ pub fn resolve_server_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() { return Ok(ServerInvocation { - bin: path.to_string_lossy().into_owned(), - args: vec![], + bin: path.to_string_lossy().into_owned(), + args: vec![], working_dir: path.parent().map(|p| p.to_path_buf()), }); } @@ -82,11 +92,14 @@ 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(), - args: vec![], + bin: p.to_string_lossy().into_owned(), + args: vec![], working_dir: Some(bin_dir.to_path_buf()), }); } @@ -105,16 +118,26 @@ pub fn resolve_server_binary( #[cfg(not(target_os = "macos"))] { let bundle_dir = resource_dir.join("binaries").join("suwayomi-bundle"); - let jar = bundle_dir.join("bin").join("Suwayomi-Server.jar"); + 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(), + bin: java.to_string_lossy().into_owned(), args: vec!["-jar".to_string(), jar.to_string_lossy().into_owned()], working_dir: Some(bundle_dir), }); @@ -122,31 +145,43 @@ pub fn resolve_server_binary( _ => 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(), - args: vec![], + bin: p.to_string_lossy().into_owned(), + args: vec![], working_dir: Some(resource_dir.clone()), }); } } 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()) - }); + 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)); + do_log( + log, + &format!("[resolve] generic JRE java={:?} jar={:?}", java, jar_path), + ); return Ok(ServerInvocation { - bin: java.to_string_lossy().into_owned(), + bin: java.to_string_lossy().into_owned(), args: vec!["-jar".to_string(), jar_path.to_string_lossy().into_owned()], working_dir: Some(resource_dir), }); @@ -159,7 +194,10 @@ pub fn resolve_server_binary( let resource_dir = app.path().resource_dir().unwrap_or_default(); let contents_dir = resource_dir.parent().unwrap_or(&resource_dir).to_path_buf(); - do_log(log, &format!("[resolve] macOS contents_dir = {:?}", contents_dir)); + do_log( + log, + &format!("[resolve] macOS contents_dir = {:?}", contents_dir), + ); const NATIVE_NAMES: &[&str] = &[ "suwayomi-server-aarch64-apple-darwin", @@ -171,7 +209,7 @@ pub fn resolve_server_binary( ]; let mut found_binary: Option = None; - let mut found_java: Option<(PathBuf, PathBuf)> = None; + let mut found_java: Option<(PathBuf, PathBuf)> = None; 'outer: for depth in 0u8..=8 { let entries: Vec = WalkDir::new(&contents_dir) @@ -184,15 +222,18 @@ pub fn resolve_server_binary( .collect(); for dir in &entries { - do_log(log, &format!("[resolve] scanning depth={} dir={:?}", depth, dir)); + 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: vec![], + bin: p.to_string_lossy().into_owned(), + args: vec![], working_dir: Some(dir.clone()), }); break 'outer; @@ -220,7 +261,10 @@ pub fn resolve_server_binary( 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)); + do_log( + log, + &format!("[resolve] found jar in bin/: {:?}", jar), + ); found_java = Some((java_exe.clone(), jar)); break 'jar; } @@ -228,7 +272,7 @@ pub fn resolve_server_binary( } match search.parent() { Some(p) => search = p, - None => break, + None => break, } } } @@ -243,7 +287,7 @@ pub fn resolve_server_binary( if let Some((java, jar)) = found_java { let working_dir = jar.parent().map(|p| p.to_path_buf()); return Ok(ServerInvocation { - bin: java.to_string_lossy().into_owned(), + bin: java.to_string_lossy().into_owned(), args: vec!["-jar".to_string(), jar.to_string_lossy().into_owned()], working_dir, }); @@ -254,16 +298,28 @@ pub fn resolve_server_binary( for name in &["suwayomi-server", "tachidesk-server"] { #[cfg(target_os = "windows")] - let found = std::process::Command::new("where").arg(name).output().map(|o| o.status.success()).unwrap_or(false); + let found = std::process::Command::new("where") + .arg(name) + .output() + .map(|o| o.status.success()) + .unwrap_or(false); #[cfg(not(target_os = "windows"))] - let found = std::process::Command::new("which").arg(name).output().map(|o| o.status.success()).unwrap_or(false); + let found = std::process::Command::new("which") + .arg(name) + .output() + .map(|o| o.status.success()) + .unwrap_or(false); if found { - return Ok(ServerInvocation { bin: name.to_string(), args: vec![], working_dir: None }); + return Ok(ServerInvocation { + bin: name.to_string(), + args: vec![], + working_dir: None, + }); } } Err(SpawnError::NotConfigured( "Server binary not found. Install Suwayomi-Server or set the path in Settings.".to_string(), )) -} \ No newline at end of file +} diff --git a/src/App.svelte b/src/App.svelte index 41cfc7b..9afb13a 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -6,7 +6,7 @@ import { platform } from "@tauri-apps/plugin-os"; import { store, setActiveDownloads } from "@store/state.svelte"; import { downloadStore } from "@features/downloads/store/downloadState.svelte"; - import { boot, startProbe, stopProbe, retryBoot, bypassBoot } from "@store/boot.svelte"; + import { boot, initStore, startProbe, stopProbe, retryBoot, bypassBoot } from "@store/boot.svelte"; import { initRpc, setIdle, clearReading, destroyRpc } from "@store/discord"; import { applyTheme } from "@core/theme"; import { applyZoom, mountZoomKey, mountIdleDetection } from "@core/ui"; @@ -100,6 +100,7 @@ }); } + await initStore(); startProbe(); const unlistenDownload = await listen<{ chapterId: number; mangaId: number; progress: number }[]>( diff --git a/src/core/persistence/index.ts b/src/core/persistence/index.ts new file mode 100644 index 0000000..97f6771 --- /dev/null +++ b/src/core/persistence/index.ts @@ -0,0 +1,2 @@ +export { loadAllStores, persistSettings, persistLibrary, persistUpdates } from "./persist"; +export type { PersistedData } from "./persist"; \ No newline at end of file diff --git a/src/core/persistence/persist.ts b/src/core/persistence/persist.ts new file mode 100644 index 0000000..6923c31 --- /dev/null +++ b/src/core/persistence/persist.ts @@ -0,0 +1,135 @@ +import { LazyStore } from "@tauri-apps/plugin-store"; + +const settingsStore = new LazyStore("settings.json", { autoSave: false }); +const libraryStore = new LazyStore("library.json", { autoSave: false }); +const updatesStore = new LazyStore("updates.json", { autoSave: false }); + +export interface PersistedData { + settings: any; + storeVersion: number | null; + history: any[]; + bookmarks: any[]; + markers: any[]; + readLog: any[]; + readingStats: any | null; + dailyReadCounts: Record; + libraryUpdates: any[]; + lastLibraryRefresh: number; + acknowledgedUpdateIds: number[]; +} + +export async function loadAllStores(): Promise { + const migrated = await migrateFromLocalStorage(); + if (migrated) return migrated; + + const [sv, s, hist, bk, mk, rl, rs, dc, lu, llr, au] = await Promise.all([ + settingsStore.get("storeVersion"), + settingsStore.get("settings"), + libraryStore.get("history"), + libraryStore.get("bookmarks"), + libraryStore.get("markers"), + libraryStore.get("readLog"), + libraryStore.get("readingStats"), + libraryStore.get>("dailyReadCounts"), + updatesStore.get("libraryUpdates"), + updatesStore.get("lastLibraryRefresh"), + updatesStore.get("acknowledgedUpdateIds"), + ]); + + return { + storeVersion: sv ?? null, + settings: s ?? null, + history: hist ?? [], + bookmarks: bk ?? [], + markers: mk ?? [], + readLog: rl ?? [], + readingStats: rs ?? null, + dailyReadCounts: dc ?? {}, + libraryUpdates: lu ?? [], + lastLibraryRefresh: llr ?? 0, + acknowledgedUpdateIds: au ?? [], + }; +} + +async function migrateFromLocalStorage(): Promise { + try { + const raw = localStorage.getItem("moku-store"); + if (!raw) return null; + const data = JSON.parse(raw); + + await Promise.all([ + persistSettings({ settings: data.settings ?? null, storeVersion: data.storeVersion ?? 1 }), + persistLibrary({ + history: data.history ?? [], + bookmarks: data.bookmarks ?? [], + markers: data.markers ?? [], + readLog: data.readLog ?? [], + readingStats: data.readingStats ?? null, + dailyReadCounts: data.dailyReadCounts ?? {}, + }), + persistUpdates({ + libraryUpdates: data.libraryUpdates ?? [], + lastLibraryRefresh: data.lastLibraryRefresh ?? 0, + acknowledgedUpdateIds: data.acknowledgedUpdateIds ?? [], + }), + ]); + + localStorage.removeItem("moku-store"); + + return { + storeVersion: data.storeVersion ?? null, + settings: data.settings ?? null, + history: data.history ?? [], + bookmarks: data.bookmarks ?? [], + markers: data.markers ?? [], + readLog: data.readLog ?? [], + readingStats: data.readingStats ?? null, + dailyReadCounts: data.dailyReadCounts ?? {}, + libraryUpdates: data.libraryUpdates ?? [], + lastLibraryRefresh: data.lastLibraryRefresh ?? 0, + acknowledgedUpdateIds: data.acknowledgedUpdateIds ?? [], + }; + } catch { + return null; + } +} + +export async function persistSettings(data: { settings: any; storeVersion: number }) { + await Promise.all([ + settingsStore.set("settings", data.settings), + settingsStore.set("storeVersion", data.storeVersion), + ]); + await settingsStore.save(); +} + +export async function persistLibrary(data: { + history: any[]; + bookmarks: any[]; + markers: any[]; + readLog: any[]; + readingStats: any; + dailyReadCounts: Record; +}) { + await Promise.all([ + libraryStore.set("history", data.history), + libraryStore.set("bookmarks", data.bookmarks), + libraryStore.set("markers", data.markers), + libraryStore.set("readLog", data.readLog), + libraryStore.set("readingStats", data.readingStats), + libraryStore.set("dailyReadCounts", data.dailyReadCounts), + ]); + await libraryStore.save(); +} + +export async function persistUpdates(data: { + libraryUpdates: any[]; + lastLibraryRefresh: number; + acknowledgedUpdateIds: number[]; +}) { + await Promise.all([ + updatesStore.set("libraryUpdates", data.libraryUpdates), + updatesStore.set("lastLibraryRefresh", data.lastLibraryRefresh), + updatesStore.set("acknowledgedUpdateIds", data.acknowledgedUpdateIds), + ]); + await updatesStore.save(); +} diff --git a/src/store/boot.svelte.ts b/src/store/boot.svelte.ts index 6ff81d8..dd58032 100644 --- a/src/store/boot.svelte.ts +++ b/src/store/boot.svelte.ts @@ -1,6 +1,7 @@ -import { store } from "@store/state.svelte"; -import { probeServer, loginBasic } from "@core/auth"; -import { trackingState } from "@features/tracking/store/trackingState.svelte"; +import { store } from "@store/state.svelte"; +import { probeServer, loginBasic } from "@core/auth"; +import { trackingState } from "@features/tracking/store/trackingState.svelte"; +import { loadAllStores } from "@core/persistence/persist"; const MAX_ATTEMPTS = 40; @@ -18,6 +19,11 @@ export const boot = $state({ let probeGeneration = 0; +export async function initStore() { + const saved = await loadAllStores(); + store.hydrate(saved); +} + export function startProbe() { const gen = ++probeGeneration; boot.failed = false; @@ -109,4 +115,4 @@ export function bypassBoot(onReady: () => void) { boot.loginRequired = false; boot.unsupportedMode = false; onReady(); -} \ No newline at end of file +} diff --git a/src/store/state.svelte.ts b/src/store/state.svelte.ts index 4f4cf1a..18d4d1b 100644 --- a/src/store/state.svelte.ts +++ b/src/store/state.svelte.ts @@ -8,6 +8,8 @@ import { DEFAULT_SETTINGS } from "../t import { DEFAULT_READING_STATS } from "../types/history"; import { notifications } from "./notifications.svelte"; import { app } from "./app.svelte"; +import { persistSettings, persistLibrary, persistUpdates } from "../core/persistence/persist"; +import type { PersistedData } from "../core/persistence/persist"; export type { NavPage } from "./app.svelte"; export type { Toast, ActiveDownload } from "./notifications.svelte"; @@ -27,29 +29,6 @@ const STORE_VERSION = 3; const AVG_MIN_PER_CHAPTER = 5; const RESET_ON_UPGRADE: (keyof Settings)[] = ["serverBinary", "readerZoom", "uiZoom"]; -function loadPersisted(): any { - try { const raw = localStorage.getItem("moku-store"); return raw ? JSON.parse(raw) : null; } - catch { return null; } -} - -function persist(patch: Record) { - try { localStorage.setItem("moku-store", JSON.stringify({ ...loadPersisted() ?? {}, ...patch })); } - catch {} -} - -const saved = (() => { - const data = loadPersisted(); - if (!data) return null; - if ((data.storeVersion ?? 1) < STORE_VERSION) { - const resetPatch: Partial = {}; - for (const key of RESET_ON_UPGRADE) (resetPatch as any)[key] = (DEFAULT_SETTINGS as any)[key]; - const migrated = { ...data, storeVersion: STORE_VERSION, settings: { ...data.settings, ...resetPatch } }; - try { localStorage.setItem("moku-store", JSON.stringify(migrated)); } catch {} - return migrated; - } - return data; -})(); - function mergeSettings(saved: any): Settings { return { ...DEFAULT_SETTINGS, ...saved?.settings, @@ -73,7 +52,7 @@ function mergeSettings(saved: any): Settings { } class Store { - settings: Settings = $state(mergeSettings(saved)); + settings: Settings = $state(mergeSettings(null)); activeManga: Manga | null = $state(null); previewManga: Manga | null = $state(null); activeChapter: Chapter | null = $state(null); @@ -84,19 +63,22 @@ class Store { categories: Category[] = $state([]); activeSource: Source | null = $state(null); libraryTagFilter: string[] = $state([]); - history: HistoryEntry[] = $state(saved?.history ?? []); - bookmarks: BookmarkEntry[]= $state(saved?.bookmarks ?? []); - markers: MarkerEntry[] = $state(saved?.markers ?? []); - readLog: ReadLogEntry[] = $state(saved?.readLog ?? []); - readingStats: ReadingStats = $state(saved?.readingStats ?? { ...DEFAULT_READING_STATS }); - dailyReadCounts: Record = $state(saved?.dailyReadCounts ?? {}); + history: HistoryEntry[] = $state([]); + bookmarks: BookmarkEntry[]= $state([]); + markers: MarkerEntry[] = $state([]); + readLog: ReadLogEntry[] = $state([]); + readingStats: ReadingStats = $state({ ...DEFAULT_READING_STATS }); + dailyReadCounts: Record = $state({}); searchCache: Map = $state(new Map()); searchLibraryIds: Set = $state(new Set()); searchSrcOffset: number = $state(0); readerSessionId: number = $state(0); - libraryUpdates: LibraryUpdateEntry[] = $state(saved?.libraryUpdates ?? []); - lastLibraryRefresh: number = $state(saved?.lastLibraryRefresh ?? 0); - acknowledgedUpdates: Set = $state(new Set(saved?.acknowledgedUpdateIds ?? [])); + libraryUpdates: LibraryUpdateEntry[] = $state([]); + lastLibraryRefresh: number = $state(0); + acknowledgedUpdates: Set = $state(new Set()); + isFullscreen: boolean = $state(false); + + #ready = false; get toasts() { return notifications.toasts; } get activeDownloads() { return notifications.activeDownloads; } @@ -109,19 +91,51 @@ class Store { get genreFilter() { return app.genreFilter; } set genreFilter(v) { app.setGenreFilter(v); } - constructor() { + hydrate(saved: PersistedData) { + if (this.#ready) return; + + if ((saved.storeVersion ?? 1) < STORE_VERSION && saved.settings) { + for (const key of RESET_ON_UPGRADE) + (saved.settings as any)[key] = (DEFAULT_SETTINGS as any)[key]; + } + + this.settings = mergeSettings(saved); + this.history = saved.history ?? []; + this.bookmarks = saved.bookmarks ?? []; + this.markers = saved.markers ?? []; + this.readLog = saved.readLog ?? []; + this.readingStats = saved.readingStats ?? { ...DEFAULT_READING_STATS }; + this.dailyReadCounts = saved.dailyReadCounts ?? {}; + this.libraryUpdates = saved.libraryUpdates ?? []; + this.lastLibraryRefresh = saved.lastLibraryRefresh ?? 0; + this.acknowledgedUpdates = new Set(saved.acknowledgedUpdateIds ?? []); + + this.#ready = true; + $effect.root(() => { $effect(() => { - persist({ - settings: this.settings, history: this.history, - bookmarks: this.bookmarks, markers: this.markers, - readLog: this.readLog, readingStats: this.readingStats, - dailyReadCounts: this.dailyReadCounts, - libraryUpdates: this.libraryUpdates, - lastLibraryRefresh: this.lastLibraryRefresh, - acknowledgedUpdateIds: [...this.acknowledgedUpdates], - storeVersion: STORE_VERSION, - }); + const s = this.settings; + if (!this.#ready) return; + persistSettings({ settings: s, storeVersion: STORE_VERSION }); + }); + + $effect(() => { + const h = this.history; + const bk = this.bookmarks; + const mk = this.markers; + const rl = this.readLog; + const rs = this.readingStats; + const dc = this.dailyReadCounts; + if (!this.#ready) return; + persistLibrary({ history: h, bookmarks: bk, markers: mk, readLog: rl, readingStats: rs, dailyReadCounts: dc }); + }); + + $effect(() => { + const lu = this.libraryUpdates; + const llr = this.lastLibraryRefresh; + const au = this.acknowledgedUpdates; + if (!this.#ready) return; + persistUpdates({ libraryUpdates: lu, lastLibraryRefresh: llr, acknowledgedUpdateIds: [...au] }); }); }); }