From f7fa653004f6e295b808974cb447f19941a4f599 Mon Sep 17 00:00:00 2001 From: Paficent Date: Sat, 25 Apr 2026 22:08:44 -0400 Subject: [PATCH] better --- package-lock.json | 10 + package.json | 1 + src-tauri/Cargo.lock | 2 + src-tauri/Cargo.toml | 2 + src-tauri/src/commands/browse.rs | 2 +- src-tauri/src/commands/logs.rs | 9 +- src-tauri/src/commands/mods.rs | 8 +- src-tauri/src/commands/onboarding.rs | 5 +- src-tauri/src/errors.rs | 107 +++-- src-tauri/src/services/gamebanana.rs | 6 +- src-tauri/src/services/installer.rs | 375 ++++++++---------- src-tauri/src/services/jeode.rs | 6 +- .../{log_watcher.rs => jeodewatcher.rs} | 11 +- src-tauri/src/services/mod.rs | 2 +- src-tauri/src/services/mods.rs | 161 +------- src-tauri/src/services/settings.rs | 17 +- src-tauri/src/services/watcher.rs | 6 +- src.zip | Bin 0 -> 105130 bytes src/lib/components/browse/BrowseCard.svelte | 31 +- 19 files changed, 302 insertions(+), 459 deletions(-) rename src-tauri/src/services/{log_watcher.rs => jeodewatcher.rs} (88%) create mode 100644 src.zip diff --git a/package-lock.json b/package-lock.json index 26c21f4..75114d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@tauri-apps/api": "^2.10.1", + "@tauri-apps/plugin-opener": "^2.5.3", "lucide-svelte": "^0.474.0" }, "devDependencies": { @@ -1552,6 +1553,15 @@ "node": ">= 10" } }, + "node_modules/@tauri-apps/plugin-opener": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz", + "integrity": "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, "node_modules/@tsconfig/svelte": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.8.tgz", diff --git a/package.json b/package.json index 776e158..e750f1e 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@tauri-apps/api": "^2.10.1", + "@tauri-apps/plugin-opener": "^2.5.3", "lucide-svelte": "^0.474.0" }, "devDependencies": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f3d1f21..4ad1fb2 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -19,9 +19,11 @@ dependencies = [ "tauri-plugin-log", "tauri-plugin-opener", "tempfile", + "thiserror 1.0.69", "tokio", "unrar", "urlencoding", + "walkdir", "winreg", "zip", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f29cd28..ddb8eb7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -33,6 +33,8 @@ zip = "2" sevenz-rust = "0.6" unrar = "0.5.8" urlencoding = "2" +walkdir = "2.5.0" +thiserror = "1" [dev-dependencies] tempfile = "3" diff --git a/src-tauri/src/commands/browse.rs b/src-tauri/src/commands/browse.rs index 9be67b5..ca2ff6a 100644 --- a/src-tauri/src/commands/browse.rs +++ b/src-tauri/src/commands/browse.rs @@ -55,7 +55,7 @@ pub async fn browse_install_mod( author: mod_author, }; - let result = crate::services::installer::install_with_metadata(&archive_path, &dir, &metadata); + let result = crate::services::installer::install(&archive_path, &dir, Some(&metadata)); let _ = std::fs::remove_file(&archive_path); result } diff --git a/src-tauri/src/commands/logs.rs b/src-tauri/src/commands/logs.rs index 64ee527..3d1b9db 100644 --- a/src-tauri/src/commands/logs.rs +++ b/src-tauri/src/commands/logs.rs @@ -1,6 +1,6 @@ use tauri::State; -use crate::errors::AppError; +use crate::errors::{AppError, Context}; use crate::state::AppState; #[tauri::command] @@ -17,10 +17,7 @@ pub async fn read_log_file(state: State<'_, AppState>) -> Result Ok(String::from_utf8_lossy(&bytes).into_owned()), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(String::new()), - Err(e) => Err(AppError::from(format!( - "Failed to read {}: {e}", - log_path.display() - ))), + Err(e) => Err(e).with_context(|| format!("failed to read {}", log_path.display())), } } @@ -37,5 +34,5 @@ pub async fn watch_log_file( .ok_or_else(|| AppError::from("Game directory not set"))?; let jeode_dir = dir.join("jeode"); - crate::services::log_watcher::start(app, jeode_dir) + crate::services::jeodewatcher::start(app, jeode_dir) } diff --git a/src-tauri/src/commands/mods.rs b/src-tauri/src/commands/mods.rs index 3bea1ec..01a0b29 100644 --- a/src-tauri/src/commands/mods.rs +++ b/src-tauri/src/commands/mods.rs @@ -1,7 +1,7 @@ use tauri::State; use tauri_plugin_dialog::DialogExt; -use crate::errors::AppError; +use crate::errors::{AppError, Context}; use crate::services::mods::{self, ModInfo}; use crate::state::AppState; @@ -36,8 +36,8 @@ pub async fn remove_mod(id: String, state: State<'_, AppState>) -> Result<(), Ap pub async fn open_mod_folder(id: String, state: State<'_, AppState>) -> Result<(), AppError> { let dir = game_dir(&state)?; let mod_path = mods::mod_folder_path(&dir, &id)?; - tauri_plugin_opener::reveal_item_in_dir(&mod_path) - .map_err(|e| AppError::from(format!("Failed to open mod folder: {e}"))) + tauri_plugin_opener::reveal_item_in_dir(&mod_path).context("failed to open mod folder")?; + Ok(()) } #[tauri::command] @@ -71,7 +71,7 @@ pub async fn install_mod( let path = file_path .into_path() .map_err(|_| AppError::from("Invalid file path selected"))?; - let result = crate::services::installer::install(&path, &dir)?; + let result = crate::services::installer::install(&path, &dir, None)?; Ok(Some(result)) } None => Ok(None), diff --git a/src-tauri/src/commands/onboarding.rs b/src-tauri/src/commands/onboarding.rs index 452ef9b..a9e5963 100644 --- a/src-tauri/src/commands/onboarding.rs +++ b/src-tauri/src/commands/onboarding.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use tauri::State; use tauri_plugin_dialog::DialogExt; -use crate::errors::AppError; +use crate::errors::{AppError, Context}; use crate::services::{game, jeode}; use crate::state::AppState; @@ -85,5 +85,6 @@ const STEAM_LAUNCH_URL: &str = "steam://run/1419170"; #[tauri::command] pub async fn launch_game() -> Result<(), AppError> { tauri_plugin_opener::open_url(STEAM_LAUNCH_URL, None::<&str>) - .map_err(|e| AppError::from(format!("Failed to launch game: {e}"))) + .context("failed to launch game")?; + Ok(()) } diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs index f60d165..111afdc 100644 --- a/src-tauri/src/errors.rs +++ b/src-tauri/src/errors.rs @@ -1,54 +1,101 @@ -use serde::Serialize; +use serde::{ser::SerializeStruct, Serialize, Serializer}; +use thiserror::Error; -#[derive(Debug, Serialize, Clone)] -pub struct AppError { - pub message: String, -} +#[derive(Debug, Error)] +pub enum AppError { + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), -impl std::fmt::Display for AppError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.message) - } -} + #[error("network error: {0}")] + Network(#[from] reqwest::Error), -impl std::error::Error for AppError {} + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), -impl From for AppError { - fn from(err: std::io::Error) -> Self { - Self { - message: err.to_string(), - } - } + #[error("{message}")] + Other { + message: String, + #[source] + source: Option>, + }, } -impl From for AppError { - fn from(err: reqwest::Error) -> Self { - Self { - message: err.to_string(), +pub type AppResult = Result; + +impl AppError { + pub fn msg(message: impl Into) -> Self { + Self::Other { + message: message.into(), + source: None, } } -} -impl From for AppError { - fn from(err: serde_json::Error) -> Self { - Self { - message: err.to_string(), + fn kind(&self) -> &'static str { + match self { + Self::Io(_) => "io", + Self::Network(_) => "network", + Self::Json(_) => "json", + Self::Other { .. } => "other", } } } impl From for AppError { fn from(message: String) -> Self { - Self { message } + Self::msg(message) } } impl From<&str> for AppError { fn from(message: &str) -> Self { - Self { - message: message.to_string(), + Self::msg(message) + } +} + +impl Serialize for AppError { + fn serialize(&self, serializer: S) -> Result { + let mut message = self.to_string(); + let mut src = std::error::Error::source(self); + while let Some(e) = src { + message.push_str(": "); + message.push_str(&e.to_string()); + src = e.source(); } + + let mut s = serializer.serialize_struct("AppError", 2)?; + s.serialize_field("kind", self.kind())?; + s.serialize_field("message", &message)?; + s.end() } } -pub type AppResult = Result; +pub trait Context { + fn context(self, msg: impl Into) -> AppResult; + fn with_context(self, f: F) -> AppResult + where + F: FnOnce() -> S, + S: Into; +} + +impl Context for Result +where + E: std::error::Error + Send + Sync + 'static, +{ + fn context(self, msg: impl Into) -> AppResult { + self.map_err(|e| AppError::Other { + message: msg.into(), + source: Some(Box::new(e)), + }) + } + + fn with_context(self, f: F) -> AppResult + where + F: FnOnce() -> S, + S: Into, + { + self.map_err(|e| AppError::Other { + message: f().into(), + source: Some(Box::new(e)), + }) + } +} diff --git a/src-tauri/src/services/gamebanana.rs b/src-tauri/src/services/gamebanana.rs index a41f33b..603094c 100644 --- a/src-tauri/src/services/gamebanana.rs +++ b/src-tauri/src/services/gamebanana.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::errors::{AppError, AppResult}; +use crate::errors::{AppError, AppResult, Context}; const API_BASE: &str = "https://gamebanana.com/apiv11"; const MSM_GAME_ID: u32 = 9640; @@ -323,14 +323,14 @@ pub async fn download_to_temp(file_id: u64, file_name: &str) -> AppResult, } -enum ArchiveKind { +enum Archive { Zip, SevenZ, Rar, } -impl ArchiveKind { +impl Archive { fn from_path(path: &Path) -> AppResult { match path .extension() @@ -40,73 +41,68 @@ impl ArchiveKind { None => Err("No file extension ???".into()), } } -} - -fn staging_dir() -> AppResult { - let base = std::env::temp_dir().join("cantus/staging"); - std::fs::create_dir_all(&base)?; - Ok(base) -} - -fn unique_staging_path() -> AppResult { - let ts = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis()) - .unwrap_or(0); - - let path = staging_dir()?.join(format!("mod_{ts}")); - std::fs::create_dir_all(&path)?; - Ok(path) -} -fn extract(archive: &Path, dest: &Path) -> AppResult<()> { - match ArchiveKind::from_path(archive)? { - ArchiveKind::Zip => extract_zip(archive, dest), - ArchiveKind::SevenZ => extract_7z(archive, dest), - ArchiveKind::Rar => extract_rar(archive, dest), + fn extract(self, archive: &Path, dest: &Path) -> AppResult<()> { + match self { + Self::Zip => { + let file = std::fs::File::open(archive)?; + let mut ar = zip::ZipArchive::new(file).context("failed to open zip")?; + ar.extract(dest).context("failed to extract zip")?; + } + Self::SevenZ => { + sevenz_rust::decompress_file(archive, dest).context("7z extraction failed")?; + } + Self::Rar => { + let mut ar = unrar::Archive::new(archive) + .open_for_processing() + .context("failed to open rar")?; + while let Some(header) = ar.read_header().context("corrupt rar header")? { + ar = header + .extract_with_base(dest) + .context("rar extraction failed")?; + } + } + } + Ok(()) } } -fn extract_zip(path: &Path, dest: &Path) -> AppResult<()> { - let file = std::fs::File::open(path)?; - let mut ar = zip::ZipArchive::new(file) - .map_err(|e| AppError::from(format!("Failed to open zip: {e}")))?; - ar.extract(dest) - .map_err(|e| AppError::from(format!("Failed to extract zip: {e}")))?; - Ok(()) -} +struct StagingDir(PathBuf); + +impl StagingDir { + fn new() -> AppResult { + let base = std::env::temp_dir().join("cantus/staging"); + let ts = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0); + let path = base.join(format!("mod_{ts}")); + std::fs::create_dir_all(&path)?; + Ok(Self(path)) + } -fn extract_7z(path: &Path, dest: &Path) -> AppResult<()> { - sevenz_rust::decompress_file(path, dest) - .map_err(|e| AppError::from(format!("7z extraction failed: {e}")))?; - Ok(()) + fn path(&self) -> &Path { + &self.0 + } } -fn extract_rar(path: &Path, dest: &Path) -> AppResult<()> { - let mut ar = unrar::Archive::new(path) - .open_for_processing() - .map_err(|e| AppError::from(format!("Failed to open rar: {e}")))?; - - while let Some(header) = ar - .read_header() - .map_err(|e| AppError::from(format!("Corrupt rar header: {e}")))? - { - ar = header - .extract_with_base(dest) - .map_err(|e| AppError::from(format!("Rar extraction failed: {e}")))?; +impl Drop for StagingDir { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); } - Ok(()) } fn copy_dir_recursive(src: &Path, dest: &Path) -> AppResult<()> { - std::fs::create_dir_all(dest)?; - for entry in std::fs::read_dir(src)?.flatten() { - let target = dest.join(entry.file_name()); - if entry.path().is_dir() { - copy_dir_recursive(&entry.path(), &target)?; - continue; + for entry in WalkDir::new(src) { + let entry = entry.context("walk failed")?; + let rel = entry.path().strip_prefix(src).unwrap(); + let target = dest.join(rel); + + if entry.file_type().is_dir() { + std::fs::create_dir_all(&target)?; + } else { + std::fs::copy(entry.path(), &target)?; } - std::fs::copy(entry.path(), target)?; } Ok(()) } @@ -120,24 +116,15 @@ fn copy_file_creating_parents(src: &Path, dest: &Path) -> AppResult<()> { } fn collect_files(dir: &Path) -> AppResult> { - let mut out = Vec::new(); - collect_files_walk(dir, &mut out)?; - Ok(out) -} - -fn collect_files_walk(dir: &Path, out: &mut Vec) -> AppResult<()> { if !dir.is_dir() { - return Ok(()); + return Ok(Vec::new()); } - for entry in std::fs::read_dir(dir)?.flatten() { - let p = entry.path(); - if p.is_dir() { - collect_files_walk(&p, out)?; - continue; - } - out.push(p); - } - Ok(()) + Ok(WalkDir::new(dir) + .into_iter() + .filter_map(Result::ok) + .filter(|e| e.file_type().is_file()) + .map(|e| e.into_path()) + .collect()) } fn build_file_index(data_dir: &Path) -> AppResult>> { @@ -155,65 +142,46 @@ fn build_file_index(data_dir: &Path) -> AppResult>> Ok(index) } -fn find_mod_roots(extracted: &Path) -> Vec { - let subdirs: Vec<_> = std::fs::read_dir(extracted) - .ok() +fn find_mod_roots(extracted: &Path) -> AppResult> { + let direct: Vec = WalkDir::new(extracted) + .min_depth(1) + .max_depth(1) .into_iter() - .flatten() - .flatten() - .filter(|e| e.path().is_dir()) - .collect(); - - let roots: Vec = subdirs - .iter() - .filter(|e| e.path().join("manifest.json").exists()) - .map(|e| e.path()) + .filter_map(Result::ok) + .filter(|e| e.file_type().is_dir() && e.path().join("manifest.json").exists()) + .map(|e| e.into_path()) .collect(); - if roots.len() > 1 { - return roots; + if direct.len() > 1 { + return Ok(direct); } - vec![find_mod_root(extracted)] -} - -fn find_mod_root(extracted: &Path) -> PathBuf { if let Some(manifest) = find_manifest(extracted) { - return manifest.parent().unwrap_or(extracted).to_path_buf(); + return Ok(vec![manifest.parent().unwrap_or(extracted).to_path_buf()]); } - let entries: Vec<_> = std::fs::read_dir(extracted) - .ok() + let top: Vec = WalkDir::new(extracted) + .min_depth(1) + .max_depth(1) .into_iter() - .flatten() - .flatten() + .filter_map(Result::ok) + .filter(|e| e.file_type().is_dir()) + .map(|e| e.into_path()) .collect(); - match entries.as_slice() { - [only] if only.path().is_dir() => only.path(), + Ok(vec![match top.as_slice() { + [only] => only.clone(), _ => extracted.to_path_buf(), - } + }]) } fn find_manifest(dir: &Path) -> Option { - let candidate = dir.join("manifest.json"); - if candidate.exists() { - return Some(candidate); - } - let mut children: Vec<_> = std::fs::read_dir(dir).ok()?.flatten().collect(); - children.sort_by_key(|e| e.file_name()); - - children - .iter() - .filter_map(|e| { - let p = e.path(); - if p.is_dir() { - find_manifest(&p) - } else { - None - } - }) - .next() + WalkDir::new(dir) + .sort_by_file_name() + .into_iter() + .filter_map(Result::ok) + .map(|e| e.into_path()) + .find(|p| p.file_name().and_then(|n| n.to_str()) == Some("manifest.json")) } fn mod_name_from_archive(path: &Path) -> String { @@ -223,23 +191,6 @@ fn mod_name_from_archive(path: &Path) -> String { .to_string() } -// native as in already a jeode mod not DLL mods -fn install_native(root: &Path, mods_dir: &Path) -> AppResult { - let raw = std::fs::read_to_string(root.join("manifest.json"))?; - let manifest: mods::Manifest = serde_json::from_str(&raw) - .map_err(|e| AppError::from(format!("Invalid manifest.json: {e}")))?; - - let id = mods::sanitize_id(&manifest.id); - let dest = mods_dir.join(&id); - - if dest.exists() { - std::fs::remove_dir_all(&dest)?; - } - copy_dir_recursive(root, &dest)?; - - Ok(manifest.name) -} - // no data directory but has gfx etc fn looks_like_bare_data(mod_root: &Path, game_dir: &Path) -> bool { let game_data = game_dir.join("data"); @@ -247,12 +198,12 @@ fn looks_like_bare_data(mod_root: &Path, game_dir: &Path) -> bool { return false; } - let subdirs: Vec<_> = std::fs::read_dir(mod_root) - .ok() + let subdirs: Vec<_> = WalkDir::new(mod_root) + .min_depth(1) + .max_depth(1) .into_iter() - .flatten() - .flatten() - .filter(|e| e.path().is_dir()) + .filter_map(Result::ok) + .filter(|e| e.file_type().is_dir()) .collect(); if subdirs.is_empty() { @@ -267,52 +218,6 @@ fn looks_like_bare_data(mod_root: &Path, game_dir: &Path) -> bool { hits > 0 && hits * 2 >= subdirs.len() } -fn install_rebuilt( - archive_path: &Path, - root: &Path, - game_dir: &Path, - mods_dir: &Path, - metadata: Option<&ModMetadata>, -) -> AppResult { - let name = metadata - .map(|m| m.name.clone()) - .unwrap_or_else(|| mod_name_from_archive(archive_path)); - let id = metadata - .map(|m| mods::sanitize_id(&m.id)) - .unwrap_or_else(|| mods::sanitize_id(&name)); - let author = metadata - .map(|m| m.author.clone()) - .unwrap_or_else(|| "Unknown".into()); - let mod_dir = mods_dir.join(&id); - - if mod_dir.exists() { - std::fs::remove_dir_all(&mod_dir)?; - } - std::fs::create_dir_all(&mod_dir)?; - - if root.join("data").is_dir() { - copy_dir_recursive(root, &mod_dir)?; - } else if looks_like_bare_data(root, game_dir) { - copy_dir_recursive(root, &mod_dir.join("data"))?; - } else { - place_files_by_name(root, game_dir, &mod_dir)?; - } - - let manifest = mods::Manifest { - id, - name: name.clone(), - author, - assets: mods::ManifestAssets { - auto_override: true, - ..Default::default() - }, - ..mods::default_manifest() - }; - mods::write_manifest(&mod_dir.join("manifest.json"), &manifest)?; - - Ok(name) -} - fn place_files_by_name(root: &Path, game_dir: &Path, mod_dir: &Path) -> AppResult<()> { let game_data = game_dir.join("data"); let index = build_file_index(&game_data)?; @@ -359,68 +264,100 @@ fn place_files_by_name(root: &Path, game_dir: &Path, mod_dir: &Path) -> AppResul Ok(()) } -fn install_single( +fn install_rebuilt( archive_path: &Path, root: &Path, game_dir: &Path, mods_dir: &Path, metadata: Option<&ModMetadata>, ) -> AppResult { - if root.join("manifest.json").exists() { - return install_native(root, mods_dir); + let name = metadata + .map(|m| m.name.clone()) + .unwrap_or_else(|| mod_name_from_archive(archive_path)); + let id = metadata + .map(|m| mods::sanitize_id(&m.id)) + .unwrap_or_else(|| mods::sanitize_id(&name)); + let author = metadata + .map(|m| m.author.clone()) + .unwrap_or_else(|| "Unknown".into()); + let mod_dir = mods_dir.join(&id); + + if mod_dir.exists() { + std::fs::remove_dir_all(&mod_dir)?; + } + std::fs::create_dir_all(&mod_dir)?; + + if root.join("data").is_dir() { + copy_dir_recursive(root, &mod_dir)?; + } else if looks_like_bare_data(root, game_dir) { + copy_dir_recursive(root, &mod_dir.join("data"))?; + } else { + place_files_by_name(root, game_dir, &mod_dir)?; + } + + let manifest = mods::Manifest { + id, + name: name.clone(), + author, + assets: mods::ManifestAssets { + auto_override: true, + ..Default::default() + }, + ..mods::default_manifest() + }; + mods::write_manifest(&mod_dir.join("manifest.json"), &manifest)?; + + Ok(name) +} + +// native as in already a jeode mod not DLL mods +fn install_native(root: &Path, mods_dir: &Path) -> AppResult { + let raw = std::fs::read_to_string(root.join("manifest.json"))?; + let manifest: mods::Manifest = serde_json::from_str(&raw).context("invalid manifest.json")?; + + let id = mods::sanitize_id(&manifest.id); + let dest = mods_dir.join(&id); + + if dest.exists() { + std::fs::remove_dir_all(&dest)?; } - install_rebuilt(archive_path, root, game_dir, mods_dir, metadata) + copy_dir_recursive(root, &dest)?; + + Ok(manifest.name) } -fn do_install( +pub fn install( archive_path: &Path, game_dir: &Path, - staging: &Path, metadata: Option<&ModMetadata>, ) -> AppResult { - extract(archive_path, staging)?; + let staging = StagingDir::new()?; + Archive::from_path(archive_path)?.extract(archive_path, staging.path())?; + let mods_dir = game_dir.join("mods"); std::fs::create_dir_all(&mods_dir)?; - let roots = find_mod_roots(staging); + let roots = find_mod_roots(staging.path())?; let total = roots.len(); let mut installed = Vec::new(); let mut errors = Vec::new(); for root in &roots { - match install_single(archive_path, root, game_dir, &mods_dir, metadata) { + let result = if root.join("manifest.json").exists() { + install_native(root, &mods_dir) + } else { + install_rebuilt(archive_path, root, game_dir, &mods_dir, metadata) + }; + match result { Ok(name) => installed.push(name), Err(e) => errors.push(e.to_string()), } } - let error = if errors.is_empty() { - None - } else { - Some(errors.join("; ")) - }; - + let error = (!errors.is_empty()).then(|| errors.join("; ")); Ok(InstallResult { installed, total, error, }) } - -pub fn install(archive_path: &Path, game_dir: &Path) -> AppResult { - let staging = unique_staging_path()?; - let result = do_install(archive_path, game_dir, &staging, None); - let _ = std::fs::remove_dir_all(&staging); - result -} - -pub fn install_with_metadata( - archive_path: &Path, - game_dir: &Path, - metadata: &ModMetadata, -) -> AppResult { - let staging = unique_staging_path()?; - let result = do_install(archive_path, game_dir, &staging, Some(metadata)); - let _ = std::fs::remove_dir_all(&staging); - result -} diff --git a/src-tauri/src/services/jeode.rs b/src-tauri/src/services/jeode.rs index 83046ba..a8fdc16 100644 --- a/src-tauri/src/services/jeode.rs +++ b/src-tauri/src/services/jeode.rs @@ -2,7 +2,7 @@ use std::path::Path; use sha2::{Digest, Sha256}; -use crate::errors::{AppError, AppResult}; +use crate::errors::{AppError, AppResult, Context}; const GITHUB_LATEST: &str = "https://api.github.com/repos/Paficent/jeode/releases/latest"; const USER_AGENT: &str = "cantus/0.1.0"; @@ -48,7 +48,7 @@ pub async fn install(game_dir: &Path) -> AppResult<()> { .send() .await? .error_for_status() - .map_err(|e| AppError::from(format!("GitHub API request failed: {e}")))? + .context("GitHub API request failed")? .json() .await?; @@ -64,7 +64,7 @@ pub async fn install(game_dir: &Path) -> AppResult<()> { .send() .await? .error_for_status() - .map_err(|e| AppError::from(format!("Download failed for {}: {e}", target.asset_name)))? + .with_context(|| format!("download failed for {}", target.asset_name))? .bytes() .await?; diff --git a/src-tauri/src/services/log_watcher.rs b/src-tauri/src/services/jeodewatcher.rs similarity index 88% rename from src-tauri/src/services/log_watcher.rs rename to src-tauri/src/services/jeodewatcher.rs index 5e3c6c4..8157aed 100644 --- a/src-tauri/src/services/log_watcher.rs +++ b/src-tauri/src/services/jeodewatcher.rs @@ -1,5 +1,3 @@ -//TODO: rename this to just jeode watcher - use std::ffi::OsStr; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; @@ -54,10 +52,13 @@ pub fn start(app: tauri::AppHandle, jeode_dir: PathBuf) -> AppResult<()> { let now = Instant::now(); let touches_log = event.paths.iter().any(|p| p.file_name() == Some(log_name)); - let touches_settings = - event.paths.iter().any(|p| p.file_name() == Some(settings_name)); + let touches_settings = event + .paths + .iter() + .any(|p| p.file_name() == Some(settings_name)); - if touches_log && now.duration_since(last_log_emit) >= Duration::from_millis(DEBOUNCE_MS) + if touches_log + && now.duration_since(last_log_emit) >= Duration::from_millis(DEBOUNCE_MS) { last_log_emit = now; let _ = app.emit("log-changed", ()); diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index a994497..cab5e03 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -2,7 +2,7 @@ pub mod game; pub mod gamebanana; pub mod installer; pub mod jeode; -pub mod log_watcher; +pub mod jeodewatcher; pub mod mods; pub mod settings; pub mod watcher; diff --git a/src-tauri/src/services/mods.rs b/src-tauri/src/services/mods.rs index c198d16..dd7bc20 100644 --- a/src-tauri/src/services/mods.rs +++ b/src-tauri/src/services/mods.rs @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; -use crate::errors::{AppError, AppResult}; +use crate::errors::{AppError, AppResult, Context}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Manifest { @@ -111,7 +111,7 @@ fn read_manifest(mod_path: &Path, dir_name: &str) -> AppResult { } let content = std::fs::read_to_string(&manifest_path)?; let mut manifest: Manifest = serde_json::from_str(&content) - .map_err(|e| AppError::from(format!("Failed to parse manifest in {dir_name}: {e}")))?; + .with_context(|| format!("failed to parse manifest in {}", dir_name))?; manifest.id = sanitize_id(&manifest.id); Ok(manifest) } @@ -227,160 +227,3 @@ pub fn write_manifest(path: &Path, manifest: &Manifest) -> AppResult<()> { std::fs::write(path, content + "\n")?; Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - - fn setup_mod( - tmp: &Path, - dir_name: &str, - manifest_json: Option<&str>, - create_entry: bool, - ) -> PathBuf { - let mods_dir = tmp.join("mods"); - let mod_dir = mods_dir.join(dir_name); - fs::create_dir_all(&mod_dir).unwrap(); - if let Some(json) = manifest_json { - fs::write(mod_dir.join("manifest.json"), json).unwrap(); - } - if create_entry { - fs::write(mod_dir.join("init.lua"), "-- entry").unwrap(); - } - mod_dir - } - - #[test] - fn sanitize_id_strips_invalid_chars() { - assert_eq!(sanitize_id("My Cool Mod!"), "mycoolmod"); - assert_eq!(sanitize_id("test-mod_123"), "test-mod_123"); - assert_eq!(sanitize_id("!!!"), "unknown"); - assert_eq!(sanitize_id(""), "unknown"); - } - - #[test] - fn detect_type_native_when_native_entry_set() { - let tmp = tempfile::tempdir().unwrap(); - let mod_path = tmp.path().join("test-mod"); - fs::create_dir_all(&mod_path).unwrap(); - let manifest = Manifest { - native_entry: "mod.dll".into(), - ..default_manifest() - }; - assert_eq!(detect_type(&manifest, &mod_path), "native"); - } - - #[test] - fn detect_type_lua_when_entry_exists() { - let tmp = tempfile::tempdir().unwrap(); - let mod_path = tmp.path().join("test-mod"); - fs::create_dir_all(&mod_path).unwrap(); - fs::write(mod_path.join("init.lua"), "").unwrap(); - let manifest = Manifest { - native_entry: String::new(), - entry: "init.lua".into(), - ..default_manifest() - }; - assert_eq!(detect_type(&manifest, &mod_path), "lua"); - } - - #[test] - fn detect_type_asset_when_no_entry() { - let tmp = tempfile::tempdir().unwrap(); - let mod_path = tmp.path().join("test-mod"); - fs::create_dir_all(&mod_path).unwrap(); - let manifest = Manifest { - native_entry: String::new(), - entry: "init.lua".into(), - ..default_manifest() - }; - assert_eq!(detect_type(&manifest, &mod_path), "asset"); - } - - #[test] - fn list_reads_mods_from_directory() { - let tmp = tempfile::tempdir().unwrap(); - setup_mod( - tmp.path(), - "my-mod", - Some( - r#"{"id":"my-mod","name":"My Mod","author":"Tester","version":"1.0.0","enabled":true}"#, - ), - true, - ); - setup_mod( - tmp.path(), - "asset-pack", - Some( - r#"{"id":"asset-pack","name":"Asset Pack","author":"Artist","version":"2.0.0","enabled":false}"#, - ), - false, - ); - - let mods = list(tmp.path()).unwrap(); - assert_eq!(mods.len(), 2); - - let asset = mods.iter().find(|m| m.id == "asset-pack").unwrap(); - assert_eq!(asset.mod_type, "asset"); - assert!(!asset.enabled); - - let cool = mods.iter().find(|m| m.id == "my-mod").unwrap(); - assert_eq!(cool.mod_type, "lua"); - assert!(cool.enabled); - } - - #[test] - fn list_returns_empty_when_no_mods_dir() { - let tmp = tempfile::tempdir().unwrap(); - let mods = list(tmp.path()).unwrap(); - assert!(mods.is_empty()); - } - - #[test] - fn list_generates_manifest_for_dir_without_one() { - let tmp = tempfile::tempdir().unwrap(); - setup_mod(tmp.path(), "bare-mod", None, false); - - let mods = list(tmp.path()).unwrap(); - assert_eq!(mods.len(), 1); - assert_eq!(mods[0].id, "bare-mod"); - assert_eq!(mods[0].name, "bare-mod"); - assert_eq!(mods[0].mod_type, "asset"); - } - - #[test] - fn toggle_flips_enabled_in_manifest() { - let tmp = tempfile::tempdir().unwrap(); - setup_mod( - tmp.path(), - "toggle-test", - Some(r#"{"id":"toggle-test","name":"Toggle Test","enabled":true}"#), - false, - ); - - let new_state = toggle(tmp.path(), "toggle-test").unwrap(); - assert!(!new_state); - - let mods = list(tmp.path()).unwrap(); - assert!(!mods[0].enabled); - - let new_state = toggle(tmp.path(), "toggle-test").unwrap(); - assert!(new_state); - } - - #[test] - fn remove_deletes_mod_directory() { - let tmp = tempfile::tempdir().unwrap(); - setup_mod( - tmp.path(), - "remove-me", - Some(r#"{"id":"remove-me","name":"test"}"#), - true, - ); - - assert!(tmp.path().join("mods/remove-me").exists()); - remove(tmp.path(), "remove-me").unwrap(); - assert!(!tmp.path().join("mods/remove-me").exists()); - } -} diff --git a/src-tauri/src/services/settings.rs b/src-tauri/src/services/settings.rs index 895ab27..d08f0f6 100644 --- a/src-tauri/src/services/settings.rs +++ b/src-tauri/src/services/settings.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use tauri::Manager; -use crate::errors::{AppError, AppResult}; +use crate::errors::{AppResult, Context}; const SETTINGS_FILE: &str = "config.json"; @@ -41,7 +41,7 @@ fn settings_path(app: &tauri::AppHandle) -> AppResult { let dir = app .path() .app_config_dir() - .map_err(|e| AppError::from(format!("Failed to resolve config directory: {e}")))?; + .context("failed to resolve config directory")?; Ok(dir.join(SETTINGS_FILE)) } @@ -52,11 +52,8 @@ pub fn load(app: &tauri::AppHandle) -> AppResult { return Ok(Settings::default()); } - let content = std::fs::read_to_string(&path) - .map_err(|e| AppError::from(format!("Failed to read settings: {e}")))?; - - serde_json::from_str(&content) - .map_err(|e| AppError::from(format!("Failed to parse settings: {e}"))) + let content = std::fs::read_to_string(&path).context("failed to read settings")?; + serde_json::from_str(&content).context("failed to parse settings") } pub fn save(app: &tauri::AppHandle, settings: &Settings) -> AppResult<()> { @@ -125,11 +122,9 @@ pub fn load_jeode(game_dir: &Path) -> AppResult { return Ok(JeodeSettings::default()); } - let content = std::fs::read_to_string(&path) - .map_err(|e| AppError::from(format!("Failed to read jeode settings: {e}")))?; + let content = std::fs::read_to_string(&path).context("failed to read jeode settings")?; - serde_json::from_str(&content) - .map_err(|e| AppError::from(format!("Failed to parse jeode settings: {e}"))) + serde_json::from_str(&content).context("failed to parse jeode settings") } pub fn save_jeode(game_dir: &Path, settings: &JeodeSettings) -> AppResult<()> { diff --git a/src-tauri/src/services/watcher.rs b/src-tauri/src/services/watcher.rs index e60e605..d427823 100644 --- a/src-tauri/src/services/watcher.rs +++ b/src-tauri/src/services/watcher.rs @@ -5,7 +5,7 @@ use std::time::Duration; use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher}; use tauri::Emitter; -use crate::errors::AppResult; +use crate::errors::{AppResult, Context}; static WATCHING: AtomicBool = AtomicBool::new(false); @@ -22,11 +22,11 @@ pub fn start(app: tauri::AppHandle, mods_dir: PathBuf) -> AppResult<()> { tx, notify::Config::default().with_poll_interval(Duration::from_secs(2)), ) - .map_err(|e| crate::errors::AppError::from(format!("Failed to create watcher: {e}")))?; + .context("failed to create watcher")?; watcher .watch(&mods_dir, RecursiveMode::Recursive) - .map_err(|e| crate::errors::AppError::from(format!("Failed to watch mods folder: {e}")))?; + .context("failed to watch mods folder")?; std::thread::spawn(move || { let _watcher = watcher; diff --git a/src.zip b/src.zip new file mode 100644 index 0000000000000000000000000000000000000000..09d758d4815b7c7f89874656c89e726c9e6ff97e GIT binary patch literal 105130 zcmdRWWmKL^vNi7R!GgQHYY6V{?(Po3Ex{dvySux)2Z9E72@u>qI5TrkCXG-&jXa z=c}ZI(pPw;V3~J7kr=?F2v~rp9(Z7A)Q<4)CIp_fZWW>QCZia`B{hnc%d>+5Q9yH|N0|fn7fGl-Qt!NzV zfA(&|9c!kd&*SAOB+U=iMTgL`gK91XDdJX7Ms|tkat=y7Z&qO`P(>Yj?Od<4Z|P)g zWIXFkPXuHd%JEj;K0f#Bxz%ezCUOVl!OxqB>Rr4~zBDl0opi+4=_U43ZDx7Ak+8)j)Xt1%3&e^&@33_zNKMdW)6~&tO13 zd%+(Ag7*D-zqk&K_MgD!*XwSJkKlJ5_9_@C93P)whD4+v25KxKp8cnmQrE_YM&JGqm-1y9 zU&gYsr1b_1LhFt)0@zfopGcL-pwwZQHTO|2s9Qd9pHVivUrs7>=)(!|h2aM#ISq$J zpjMF0krY-zyZg8EVc$~4C5@6pHU$lAb7m~ADy&Ce!wtU~VsduFb=0$sW#8{Y9&IIA zI?BAXcFj}N8VKe@CjsRoTH>_Jtl{TA%of<=D&6dle#yrq?%%SRbh+fKs%Rp_9hV7R$)@{K7Uwtwk#| zj&a+b@(sPWD#@nW6&zi>KWU#+J?Sdu2bL5qOg*+j&yl7n8FEU6R47*wYZp~{7vG%> zGgdkY+mf$1u~w9Vq@5Q&d{a6NXQ3eyBP z8SBLVY6RF3Jcy<@2TUsPiP108-fN6jgJr@kj2~e=6C6y85dLf-tC$+BvjH3zp}-~9 z*fyw@d6kX`cpJFWG}ASVhESq&`;4SfZ~K|)`K z2=*8hBuJyLKEBWi0I$k^GOcn6pdMwu@^s+>=-wzc_{OL9+XY&inPK;e!zw?p`^R}k z=>Un%`x1R~j6(pEj}VC>T+6Ruq1rHOvH+Kj_ZL>}g-A(iH|o{oEiP z0{DO+AQ40$@O9|=Y6$2?>_MEC2M?n*}NknJx`IIYF`t(Nv<4)~2x z)}@jZ=NXJi!5idLMB~G@ z-B&LeEfBmWgcJyy&ThExaMq6A;4F)k@H;bwx(m65 z(^)@6XC=2VUX|i1c`FVl)6yoEW1R~Q8*tVuvkmwq-5r{_s;V3b|@YTY!cz54Zh{ydOK-&-^ZB_=MGBFt@%T=f)QdGV6XrO z%F+WdaZ?K<<-LK7e(*~+I9Zyxm?|_^OX4%+))g4&+cL$GDnN#%ijn4&Kz;x<6CgIL zuJL~?mM%!zdAtP|QMeH&#dUS|hyo;)KPFnud zsPzE(!l?q3czXOG000=z;rCzS)IUK|{|Qe0Q9ryy|Cb=Z|IDfOPKFi^hChptFDiob zuWBMLwlh4H4kqw~OGxaBjuJIX%gw|`(0#cmXdWG+XCwy-G8DGCc(TP^5X|Tuj)El( zS@_dJ!Zp-X2BSq?@B>r6S)sh1!X@=*lbAD`~!Q~9{+JBm9)Wv<$ zc!^Z*P5wy+;%ADrOiE0MzF1?+otRDU`bzEwwZ(+Zf}C%6AFw2lR~uRN31gGwW6Db; z+e)JtM=At9<>yC-hAO~QjoYKKdP%LHcd`4w+A$7p1<(|?OHc~LZ|-jn{{kMu#*^

w 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 KQV3)VF#aF)AIuK` literal 0 HcmV?d00001 diff --git a/src/lib/components/browse/BrowseCard.svelte b/src/lib/components/browse/BrowseCard.svelte index e60d365..6c9c38a 100644 --- a/src/lib/components/browse/BrowseCard.svelte +++ b/src/lib/components/browse/BrowseCard.svelte @@ -11,7 +11,7 @@ RefreshCw, } from "lucide-svelte"; import type { BrowseMod } from "$lib/types/browse"; - import { open } from "@tauri-apps/api/shell"; + import { openUrl } from "@tauri-apps/plugin-opener"; interface Props { mod: BrowseMod; @@ -26,8 +26,8 @@ return n.toString(); } - function openMod() { - open(`https://gamebanana.com/mods/${mod.id}`); + async function openMod() { + await openUrl(`https://gamebanana.com/mods/${mod.id}`); } function timeAgo(ts: number): string { @@ -48,14 +48,19 @@ -

- {mod.name} +
+
- {mod.name} + openMod()} + >{mod.name} {mod.author}