diff --git a/src-tauri/src/commands/assets_command.rs b/src-tauri/src/commands/assets_command.rs index a8b3250e..0a2008f1 100644 --- a/src-tauri/src/commands/assets_command.rs +++ b/src-tauri/src/commands/assets_command.rs @@ -22,7 +22,7 @@ type Result = std::result::Result; /// * `Result` - The local file path as a string, or an error if the download fails. #[command] pub async fn get_or_download_asset_model(url: &str) -> Result { - info!("get_or_download_asset_model called with URL: {}", url); + debug!("get_or_download_asset_model called with URL: {}", url); // Parse the URL to extract the path components let parsed_url = Url::parse(url).map_err(|e| { @@ -106,7 +106,7 @@ pub async fn get_or_download_asset_model(url: &str) -> Result { } // File doesn't exist, download it synchronously (blocking) - info!("Asset model not found locally, downloading from: {}", url); + debug!("Asset model not found locally, downloading from: {}", url); // Ensure parent directories exist if let Some(parent) = local_file_path.parent() { diff --git a/src-tauri/src/commands/nrc_commands.rs b/src-tauri/src/commands/nrc_commands.rs index c94518fe..88c5a86e 100644 --- a/src-tauri/src/commands/nrc_commands.rs +++ b/src-tauri/src/commands/nrc_commands.rs @@ -1,4 +1,5 @@ use crate::error::{AppError, CommandError}; +use crate::logging; use crate::minecraft::api::norisk_api::{AdventCalendarDay, CrashlogDto, NoRiskApi, ReferralInfo, Reward, UserNotification}; use crate::minecraft::api::wordpress_api::{BlogPost, WordPressApi}; use crate::minecraft::auth::minecraft_auth::Credentials; @@ -17,10 +18,23 @@ use crate::utils::updater_utils; /// * `Result, CommandError>` - A vector of blog posts or an error. #[tauri::command] pub async fn get_news_and_changelogs_command() -> Result, CommandError> { - info!("Executing get_news_and_changelogs_command"); + debug!("Executing get_news_and_changelogs_command"); Ok(WordPressApi::get_news_and_changelogs().await?) } +#[tauri::command] +pub async fn dump_debug_logs_command(reason: Option) -> Result { + let dump_reason = reason + .filter(|s| !s.trim().is_empty()) + .unwrap_or_else(|| "manual_hotkey".to_string()); + + let path = logging::dump_debug_buffer_to_file(&format!("manual: {}", dump_reason)) + .map_err(|e| AppError::Other(format!("Failed to dump debug logs: {}", e)))?; + + info!("Debug ring buffer dumped to: {}", path.display()); + Ok(path.to_string_lossy().to_string()) +} + #[tauri::command] pub async fn discord_auth_link(app: AppHandle) -> Result<(), CommandError> { debug!("Executing discord_auth_link command"); @@ -402,10 +416,10 @@ pub async fn submit_crash_log_command(payload: CrashlogDto) -> Result<(), Comman pub async fn log_message_command(level: String, message: String) -> Result<(), CommandError> { match level.to_lowercase().as_str() { "debug" => debug!("[Frontend] {}", message), - "info" => info!("[Frontend] {}", message), + "info" => debug!("[Frontend] {}", message), "warn" => log::warn!("[Frontend] {}", message), "error" => error!("[Frontend] {}", message), - _ => info!("[Frontend] {}", message), + _ => debug!("[Frontend] {}", message), } Ok(()) } diff --git a/src-tauri/src/commands/profile_command.rs b/src-tauri/src/commands/profile_command.rs index d702af65..a71d2759 100644 --- a/src-tauri/src/commands/profile_command.rs +++ b/src-tauri/src/commands/profile_command.rs @@ -29,7 +29,7 @@ use crate::utils::{ shaderpack_utils, }; use chrono::Utc; -use log::{error, info, trace, warn}; +use log::{error, info, trace, warn, debug}; use sanitize_filename::sanitize; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; @@ -2258,7 +2258,7 @@ pub struct AllProfilesAndLastPlayed { #[tauri::command] pub async fn get_all_profiles_and_last_played() -> Result { - info!("Executing get_all_profiles_and_last_played command"); + debug!("Executing get_all_profiles_and_last_played command"); let state = State::get().await?; // Fetch User Profiles (includes editable copies of standard profiles) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a1939048..26f2b4ef 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3,6 +3,7 @@ pub mod config; pub mod error; pub mod friends; pub mod integrations; +pub mod logging; pub mod minecraft; pub mod state; pub mod utils; diff --git a/src-tauri/src/logging.rs b/src-tauri/src/logging.rs index 95e3db7a..eeec5242 100644 --- a/src-tauri/src/logging.rs +++ b/src-tauri/src/logging.rs @@ -1,20 +1,176 @@ use crate::config::{ProjectDirsExt, LAUNCHER_DIRECTORY}; +use chrono::Utc; use log::LevelFilter; use log4rs::append::console::{ConsoleAppender, Target}; use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller; use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger; use log4rs::append::rolling_file::policy::compound::CompoundPolicy; use log4rs::append::rolling_file::RollingFileAppender; -use log4rs::config::{Appender, Config, Root}; +use log4rs::config::{Appender, Config, Logger, Root}; +use log4rs::encode::writer::simple::SimpleWriter; +use log4rs::encode::Encode; use log4rs::encode::pattern::PatternEncoder; +use log4rs::filter::threshold::ThresholdFilter; +use once_cell::sync::Lazy; +use std::collections::VecDeque; +use std::fs::OpenOptions; +use std::io::Write; +use std::sync::Mutex; use tokio::fs; const LOG_DIR_NAME: &str = "logs"; const LOG_FILE_NAME: &str = "launcher.log"; +const DEBUG_DUMP_FILE_PREFIX: &str = "launcher-debug"; const LOG_PATTERN: &str = "{d(%Y-%m-%d %H:%M:%S%.3f)} | {({l}):5.5} | {m}{n}"; const CONSOLE_LOG_PATTERN: &str = "{d(%H:%M:%S)} | {h({l}):5.5} | {m}{n}"; // Slightly simpler pattern for console const LOG_FILE_SIZE_LIMIT_BYTES: u64 = 4_800_000; // ~4.8MB to fit Discord's 8MB upload limit const LOG_FILE_BACKUP_COUNT: u32 = 10; +const DEBUG_RING_BUFFER_MAX_BYTES: usize = LOG_FILE_SIZE_LIMIT_BYTES as usize; +const DEBUG_RING_BUFFER_MAX_LINE_BYTES: usize = 16 * 1024; // 16 KB per entry + +#[derive(Debug, Default)] +struct DebugRingBufferState { + lines: VecDeque, + bytes: usize, +} + +static DEBUG_RING_BUFFER: Lazy> = + Lazy::new(|| Mutex::new(DebugRingBufferState::default())); + +#[derive(Debug)] +struct DebugRingBufferAppender { + encoder: Box, +} + +impl DebugRingBufferAppender { + fn new() -> Self { + Self { + encoder: Box::new(PatternEncoder::new(LOG_PATTERN)), + } + } + + fn push_line(&self, line: String) { + let normalized_line = truncate_line_to_limit(&line, DEBUG_RING_BUFFER_MAX_LINE_BYTES); + if let Ok(mut state) = DEBUG_RING_BUFFER.lock() { + let line_bytes = normalized_line.as_bytes().len(); + + while state.bytes + line_bytes > DEBUG_RING_BUFFER_MAX_BYTES { + match state.lines.pop_front() { + Some(removed) => { + state.bytes = state.bytes.saturating_sub(removed.as_bytes().len()); + } + None => break, + } + } + + state.bytes += line_bytes; + state.lines.push_back(normalized_line); + } + } +} + +fn truncate_line_to_limit(line: &str, max_bytes: usize) -> String { + if line.as_bytes().len() <= max_bytes { + return line.to_string(); + } + + const SUFFIX: &str = " ... [truncated]\n"; + let suffix_bytes = SUFFIX.as_bytes().len(); + if max_bytes <= suffix_bytes { + return SUFFIX[..max_bytes].to_string(); + } + + let mut end = max_bytes - suffix_bytes; + while end > 0 && !line.is_char_boundary(end) { + end -= 1; + } + + let mut truncated = String::with_capacity(max_bytes); + truncated.push_str(&line[..end]); + truncated.push_str(SUFFIX); + truncated +} + +impl log::Log for DebugRingBufferAppender { + fn enabled(&self, _metadata: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { + return; + } + + let mut encoded = Vec::new(); + { + let mut writer = SimpleWriter(&mut encoded); + if let Err(e) = self.encoder.encode(&mut writer, record) { + eprintln!("[Logging] Failed to encode debug ring buffer line: {}", e); + return; + } + } + + self.push_line(String::from_utf8_lossy(&encoded).into_owned()); + + if record.level() == log::Level::Error { + let reason = format!("error: {}", record.args()); + if let Err(e) = dump_debug_buffer_to_file(&reason) { + eprintln!("[Logging] Failed to dump debug ring buffer on error: {}", e); + } + } + } + + fn flush(&self) {} +} + +pub fn dump_debug_buffer_to_file( + reason: &str, +) -> Result> { + let log_dir = LAUNCHER_DIRECTORY.root_dir().join(LOG_DIR_NAME); + std::fs::create_dir_all(&log_dir)?; + + let timestamp = Utc::now().format("%Y%m%d-%H%M%S%.3f"); + let dump_file_path = log_dir.join(format!("{}-{}.log", DEBUG_DUMP_FILE_PREFIX, timestamp)); + + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&dump_file_path)?; + + writeln!(file, "# NoRisk Launcher Debug Buffer Dump")?; + writeln!(file, "# Timestamp (UTC): {}", Utc::now().to_rfc3339())?; + writeln!(file, "# Reason: {}", reason)?; + let (current_buffer_len, current_buffer_bytes) = DEBUG_RING_BUFFER + .lock() + .map(|state| (state.lines.len(), state.bytes)) + .unwrap_or((0, 0)); + writeln!( + file, + "# Buffered Lines: {}", + current_buffer_len, + )?; + writeln!( + file, + "# Buffered Bytes: {} (max {})", + current_buffer_bytes, + DEBUG_RING_BUFFER_MAX_BYTES + )?; + writeln!( + file, + "# Max Line Bytes: {}", + DEBUG_RING_BUFFER_MAX_LINE_BYTES + )?; + writeln!(file)?; + + if let Ok(state) = DEBUG_RING_BUFFER.lock() { + for line in state.lines.iter() { + file.write_all(line.as_bytes())?; + } + } + + Ok(dump_file_path) +} /// Initializes the logging system using log4rs. /// Configures a rolling file appender and a console appender. @@ -52,22 +208,37 @@ pub async fn setup_logging() -> Result<(), Box> { .target(Target::Stdout) .build(); + // --- Configure In-Memory Debug Ring Buffer Appender --- + let debug_ring_buffer_appender = DebugRingBufferAppender::new(); + // --- Configure log4rs --- let config = Config::builder() - .appender(Appender::builder().build("file", Box::new(file_appender))) + .appender( + Appender::builder() + .filter(Box::new(ThresholdFilter::new(LevelFilter::Info))) + .build("file", Box::new(file_appender)), + ) .appender(Appender::builder().build("stdout", Box::new(console_appender))) // Add console appender + .appender( + Appender::builder() + .build("debug_ring_buffer", Box::new(debug_ring_buffer_appender)), + ) + // Suppress noisy event-loop warnings from window focus/tab-in transitions. + .logger(Logger::builder().build("winit", LevelFilter::Error)) // prevent winit from logging at all levels (WARN on window focus) + .logger(Logger::builder().build("tao", LevelFilter::Error)) // prevent tao from logging at all levels (WARN on window focus) .build( Root::builder() .appender("file") // Log to file .appender("stdout") // Log to console - .build(LevelFilter::Debug), // Log Debug and above to both + .appender("debug_ring_buffer") // Keep debug lines in RAM for crash/error dumps + .build(LevelFilter::Debug), )?; // Initialize log4rs log4rs::init_config(config)?; // Now we can use log::info! - log::info!("Logging initialized. Log directory: {}", log_dir.display()); + log::debug!("Logging initialized. Log directory: {}", log_dir.display()); Ok(()) } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 207fb7a1..e8dac24a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -118,7 +118,7 @@ use commands::vanilla_cape_command::{ use commands::assets_command::get_or_download_asset_model; // Import NRC commands -use commands::nrc_commands::{check_update_available_command, download_and_install_update_command, get_news_and_changelogs_command, get_advent_calendar_command, claim_advent_calendar_day_command}; +use commands::nrc_commands::{check_update_available_command, download_and_install_update_command, get_news_and_changelogs_command, get_advent_calendar_command, claim_advent_calendar_day_command, dump_debug_logs_command}; // Import Content commands use commands::content_command::{ @@ -149,6 +149,15 @@ async fn main() { eprintln!("FEHLER: Logging konnte nicht initialisiert werden: {}", e); } + let default_panic_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info| { + let reason = format!("panic: {}", panic_info); + if let Err(e) = logging::dump_debug_buffer_to_file(&reason) { + eprintln!("[Panic Hook] Failed to dump debug ring buffer: {}", e); + } + default_panic_hook(panic_info); + })); + info!("Starting NoRiskClient Launcher..."); tauri::Builder::default() @@ -156,7 +165,7 @@ async fn main() { .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_single_instance::init(|app, argv, _cwd| { - info!("SingleInstance plugin: Second instance triggered with args: {:?}", argv); + debug!("SingleInstance plugin: Second instance triggered with args: {:?}", argv); match app.get_webview_window("main") { Some(window) => { @@ -169,7 +178,7 @@ async fn main() { if let Err(e) = window.set_focus() { error!("SingleInstance: Failed to focus main window: {}", e); } - info!("SingleInstance: Brought existing window to front."); + debug!("SingleInstance: Brought existing window to front."); } None => { info!("SingleInstance: Main window not yet available, still starting up. Ignoring."); @@ -327,7 +336,7 @@ async fn main() { // --- Create Updater Window (but keep hidden initially) --- let updater_window = match updater_utils::create_updater_window(&state_init_app_handle).await { Ok(win) => { - info!("Updater window created successfully (initially hidden)."); + debug!("Updater window created successfully (initially hidden)."); Some(win) } Err(e) => { @@ -337,7 +346,7 @@ async fn main() { }; // --- State Initialization --- - info!("Initiating state initialization..."); + debug!("Initiating state initialization..."); if let Err(e) = state::state_manager::State::init(Arc::new(state_init_app_handle.clone())).await { error!("CRITICAL: Failed to initialize state: {}. Update check and main window might not proceed correctly.", e); if let Some(win) = updater_window { @@ -351,7 +360,7 @@ async fn main() { } info!("State initialization finished successfully."); - info!("Attempting to retrieve launcher configuration for update check..."); + debug!("Attempting to retrieve launcher configuration for update check..."); match state::state_manager::State::get().await { Ok(state_manager_instance) => { let config = state_manager_instance.config_manager.get_config().await; @@ -365,7 +374,7 @@ async fn main() { } if auto_check_updates_enabled { - info!("Initiating application update check (Channel determined by config: Beta={})...", check_beta_channel); + debug!("Initiating application update check (Channel determined by config: Beta={})...", check_beta_channel); updater_utils::check_for_updates(state_init_app_handle.clone(), check_beta_channel, updater_window.clone()).await; info!("Update check process has finished."); } else { @@ -396,7 +405,7 @@ async fn main() { if let Some(main_window) = state_init_app_handle.get_webview_window("main") { match main_window.show() { Ok(_) => { - info!("Main window shown successfully."); + debug!("Main window shown successfully."); if let Err(e) = main_window.set_focus() { error!("Failed to focus main window (non-critical): {}", e); } @@ -645,6 +654,7 @@ async fn main() { commands::nrc_commands::github_auth_unlink, commands::nrc_commands::submit_crash_log_command, commands::nrc_commands::log_message_command, + dump_debug_logs_command, commands::flagsmith_commands::set_blocked_mods_config, commands::flagsmith_commands::get_blocked_mods_config, commands::flagsmith_commands::is_filename_blocked, diff --git a/src-tauri/src/minecraft/api/norisk_api.rs b/src-tauri/src/minecraft/api/norisk_api.rs index 3d6e71cd..2cb936da 100644 --- a/src-tauri/src/minecraft/api/norisk_api.rs +++ b/src-tauri/src/minecraft/api/norisk_api.rs @@ -188,7 +188,7 @@ impl NoRiskApi { ))); } - info!("[NoRisk API] Server ID request successful: {}", server_id); + debug!("[NoRisk API] Server ID request successful: {}", server_id); Ok(server_response) } Err(e) => { @@ -363,7 +363,7 @@ impl NoRiskApi { force: bool, is_experimental: bool, ) -> Result { - info!("[NoRisk API] Refreshing NoRisk token v3 with SystemID: {}", system_id); + debug!("[NoRisk API] Refreshing NoRisk token v3 with SystemID: {}", system_id); debug!("[NoRisk API] Username: {}", username); debug!("[NoRisk API] Force refresh: {}", force); debug!("[NoRisk API] Experimental mode: {}", is_experimental); @@ -372,14 +372,14 @@ impl NoRiskApi { debug!("[NoRisk API] Step 1: Requesting server ID from NoRisk API"); let server_response = Self::request_server_id(is_experimental).await?; let server_id = &server_response.server_id; - info!("[NoRisk API] Received server ID: {}", server_id); + debug!("[NoRisk API] Received server ID: {}", server_id); // Step 2: Join the Minecraft server session (client-side authentication) debug!("[NoRisk API] Step 2: Joining Minecraft server session with server ID: {}", server_id); let mc_api = crate::minecraft::api::mc_api::MinecraftApiService::new(); match mc_api.join_server_session(access_token, selected_profile, server_id).await { Ok(_) => { - info!("[NoRisk API] Successfully joined Minecraft server session"); + debug!("[NoRisk API] Successfully joined Minecraft server session"); } Err(join_err) => { // Inspect the error text for the specific InsufficientPrivilegesException coming from @@ -462,7 +462,7 @@ impl NoRiskApi { debug!("[NoRisk API] Parsing v3 token refresh response body as JSON"); match response.json::().await { Ok(token) => { - info!("[NoRisk API] v3 token refresh successful"); + debug!("[NoRisk API] v3 token refresh successful"); debug!("[NoRisk API] Token valid status: {}", token.value.len() > 0); Ok(token) } @@ -694,7 +694,7 @@ impl NoRiskApi { ))); } - info!("[NoRisk API] Crash log submitted successfully."); + debug!("[NoRisk API] Crash log submitted successfully."); Ok(()) } @@ -707,7 +707,7 @@ impl NoRiskApi { let endpoint = "mcreal/user/mobileAppToken"; let url = format!("{}/{}", base_url, endpoint); - info!("[NoRisk API] Requesting mcreal app token"); + debug!("[NoRisk API] Requesting mcreal app token"); debug!("[NoRisk API] Full URL: {}", url); let response = HTTP_CLIENT @@ -754,7 +754,7 @@ impl NoRiskApi { let endpoint = "mcreal/user/mobileAppToken/reset"; let url = format!("{}/{}", base_url, endpoint); - info!("[NoRisk API] Resetting mcreal app token"); + debug!("[NoRisk API] Resetting mcreal app token"); debug!("[NoRisk API] Full URL: {}", url); let response = HTTP_CLIENT @@ -951,7 +951,7 @@ impl NoRiskApi { let base_url = Self::get_api_base(is_experimental); let url = format!("{}/launcher/referral/report", base_url); - info!("[NoRisk API] Reporting referral code: {} for account: {}", code, account_id); + debug!("[NoRisk API] Reporting referral code: {} for account: {}", code, account_id); debug!("[NoRisk API] Full URL: {}", url); #[derive(Serialize)] @@ -991,7 +991,7 @@ impl NoRiskApi { ))); } - info!("[NoRisk API] Successfully reported referral code"); + debug!("[NoRisk API] Successfully reported referral code"); Ok(()) } @@ -1001,7 +1001,7 @@ impl NoRiskApi { let base_url = Self::get_api_base(is_experimental); let url = format!("{}/launcher/referral/info", base_url); - info!("[NoRisk API] Fetching referral info for code: {}", code); + debug!("[NoRisk API] Fetching referral info for code: {}", code); debug!("[NoRisk API] Full URL: {}", url); let response = HTTP_CLIENT @@ -1037,7 +1037,7 @@ impl NoRiskApi { AppError::ParseError(format!("Failed to parse referral info: {}", e)) })?; - info!("[NoRisk API] Successfully fetched referral info for: {}", info.referrer_name); + debug!("[NoRisk API] Successfully fetched referral info for: {}", info.referrer_name); Ok(info) } diff --git a/src-tauri/src/minecraft/api/wordpress_api.rs b/src-tauri/src/minecraft/api/wordpress_api.rs index 7bc64330..81d92092 100644 --- a/src-tauri/src/minecraft/api/wordpress_api.rs +++ b/src-tauri/src/minecraft/api/wordpress_api.rs @@ -63,7 +63,7 @@ impl WordPressApi { let endpoint = "posts"; let url = format!("{}/{}", base_url, endpoint); - info!("[WordPress API] Fetching blog posts"); + debug!("[WordPress API] Fetching blog posts"); debug!("[WordPress API] Full URL: {}", url); let mut query_params: HashMap = HashMap::new(); @@ -147,7 +147,7 @@ impl WordPressApi { /// /// * `Result>` - A vector of blog posts or an error pub async fn get_news_and_changelogs() -> Result> { - info!("[WordPress API] Fetching news and changelog posts"); + debug!("[WordPress API] Fetching news and changelog posts"); Self::get_blog_posts(Some("21,2"), Some(10), Some(1)).await } @@ -157,7 +157,7 @@ impl WordPressApi { /// /// * `Result>` - A vector of blog posts or an error pub async fn get_news() -> Result> { - info!("[WordPress API] Fetching news posts"); + debug!("[WordPress API] Fetching news posts"); Self::get_blog_posts(Some("21"), Some(10), Some(1)).await } @@ -167,7 +167,7 @@ impl WordPressApi { /// /// * `Result>` - A vector of blog posts or an error pub async fn get_changelogs() -> Result> { - info!("[WordPress API] Fetching changelog posts"); + debug!("[WordPress API] Fetching changelog posts"); Self::get_blog_posts(Some("2"), Some(10), Some(1)).await } } diff --git a/src-tauri/src/minecraft/auth/minecraft_auth.rs b/src-tauri/src/minecraft/auth/minecraft_auth.rs index fb019195..c6e5607b 100644 --- a/src-tauri/src/minecraft/auth/minecraft_auth.rs +++ b/src-tauri/src/minecraft/auth/minecraft_auth.rs @@ -8,8 +8,7 @@ use base64::Engine; use chrono::{DateTime, Duration, Utc}; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; -use log::error; -use log::info; +use log::{debug, error, info, warn}; use machineid_rs::{Encryption, HWIDComponent, IdBuilder}; use p256::ecdsa::signature::Signer; use p256::ecdsa::{Signature, SigningKey, VerifyingKey}; @@ -238,24 +237,24 @@ impl MinecraftAuthStore { } pub async fn load(&self) -> Result<()> { - info!("[Storage] Starting load operation"); + debug!("[Storage] Starting load operation"); if self.store_path.try_exists()? { - info!( + debug!( "[Storage] Account file exists at: {}", self.store_path.display() ); - info!("[Storage] Reading account data"); + debug!("[Storage] Reading account data"); let data = fs::read_to_string(&self.store_path).await?; - info!( + debug!( "[Storage] Successfully read data, length: {} bytes", data.len() ); - info!("[Storage] Deserializing account data"); + debug!("[Storage] Deserializing account data"); let store: AccountStore = match serde_json::from_str(&data) { Ok(store) => { - info!("[Storage] Successfully deserialized data"); + debug!("[Storage] Successfully deserialized data"); store } Err(e) => { @@ -265,7 +264,7 @@ impl MinecraftAuthStore { ); // Create new empty store - no backup needed as corrupted data is useless - info!("[Storage] Creating new empty account store"); + debug!("[Storage] Creating new empty account store"); AccountStore { accounts: Vec::new(), token: None, @@ -273,26 +272,26 @@ impl MinecraftAuthStore { } }; - info!("[Storage] Acquiring write lock to update accounts"); + debug!("[Storage] Acquiring write lock to update accounts"); let mut accounts = self.accounts.write().await; - info!("[Storage] Successfully acquired write lock"); + debug!("[Storage] Successfully acquired write lock"); - info!( + debug!( "[Storage] Loading {} accounts into memory", store.accounts.len() ); *accounts = store.accounts; - info!("[Storage] Successfully loaded accounts"); + debug!("[Storage] Successfully loaded accounts"); // Also restore saved device token - info!("[Storage] Restoring saved device token (if any)"); + debug!("[Storage] Restoring saved device token (if any)"); { let mut token_guard = self.token.write().await; *token_guard = store.token; } - info!("[Storage] Device token restored"); + debug!("[Storage] Device token restored"); } else { - info!("[Storage] No account file found, starting with empty accounts"); + debug!("[Storage] No account file found, starting with empty accounts"); } info!("[Storage] Load operation completed successfully"); @@ -300,16 +299,16 @@ impl MinecraftAuthStore { } async fn save(&self) -> Result<()> { - info!("[Storage] Starting save operation"); - info!("[Storage] Acquiring read locks for accounts and device token"); + debug!("[Storage] Starting save operation"); + debug!("[Storage] Acquiring read locks for accounts and device token"); let accounts = self.accounts.read().await; - info!("[Storage] Successfully acquired accounts read lock"); + debug!("[Storage] Successfully acquired accounts read lock"); let device_token = self.token.read().await; - info!("[Storage] Successfully acquired device token read lock"); + debug!("[Storage] Successfully acquired device token read lock"); - info!( + debug!( "[Storage] Creating AccountStore with {} accounts", accounts.len() ); @@ -318,16 +317,16 @@ impl MinecraftAuthStore { token: device_token.clone(), }; - info!("[Storage] Serializing data to JSON"); + debug!("[Storage] Serializing data to JSON"); let data = serde_json::to_string_pretty(&store)?; - info!("[Storage] Successfully serialized data"); + debug!("[Storage] Successfully serialized data"); - info!( + debug!( "[Storage] Writing data to file: {}", self.store_path.display() ); fs::write(&self.store_path, data).await?; - info!("[Storage] Successfully wrote data to file"); + debug!("[Storage] Successfully wrote data to file"); info!("[Storage] Save operation completed successfully"); Ok(()) @@ -338,7 +337,7 @@ impl MinecraftAuthStore { current_date: DateTime, force_generate: bool, ) -> Result<(DeviceTokenKey, DeviceToken, DateTime, bool)> { - info!("refresh_and_get_device_token"); + debug!("refresh_and_get_device_token"); // Prefer reusing the existing key unless explicitly forced to generate a new one if !force_generate { @@ -411,7 +410,7 @@ impl MinecraftAuthStore { /// Starts a direct OAuth2 flow (for Flatpak/localhost redirect) /// This uses the direct OAuth2 endpoint instead of SISU pub async fn login_begin_direct_oauth(&self, redirect_uri: &str) -> Result { - info!("[Direct OAuth Flow] Starting direct OAuth2 login"); + debug!("[Direct OAuth Flow] Starting direct OAuth2 login"); // Generate OAuth challenge let verifier = generate_oauth_challenge(); @@ -437,7 +436,7 @@ impl MinecraftAuthStore { .append_pair("state", &state) .append_pair("prompt", "select_account"); - info!("[Direct OAuth Flow] Generated authorization URL"); + debug!("[Direct OAuth Flow] Generated authorization URL"); Ok(DirectOAuthFlow { verifier, @@ -450,11 +449,11 @@ impl MinecraftAuthStore { pub async fn login_begin(&self, redirect_uri: Option<&str>) -> Result { info!("[Auth Flow] Starting login_begin process"); - info!("[Auth Flow] Initializing device token refresh"); + debug!("[Auth Flow] Initializing device token refresh"); let (key, token, current_date, valid_date) = self.refresh_and_get_device_token(Utc::now(), false).await?; - info!("[Auth Flow] Generating OAuth challenge"); + debug!("[Auth Flow] Generating OAuth challenge"); let verifier = generate_oauth_challenge(); let mut hasher = sha2::Sha256::new(); hasher.update(&verifier); @@ -463,8 +462,8 @@ impl MinecraftAuthStore { match sisu_authenticate(&token.token, &challenge, &key, current_date, redirect_uri).await { Ok((session_id, redirect_uri)) => { - info!("[Auth Flow] SISU authentication successful"); - info!("[Auth Flow] Session ID generated: {}", session_id); + debug!("[Auth Flow] SISU authentication successful"); + debug!("[Auth Flow] Session ID generated: {}", session_id); Ok(MinecraftLoginFlow { verifier, challenge, @@ -473,20 +472,20 @@ impl MinecraftAuthStore { }) } Err(err) => { - info!("[Auth Flow] SISU authentication failed: {:?}", err); + error!("[Auth Flow] SISU authentication failed: {:?}", err); if !valid_date { - info!("[Auth Flow] Retrying with new device token due to invalid date"); + debug!("[Auth Flow] Retrying with new device token due to invalid date"); let (key, token, current_date, _) = self.refresh_and_get_device_token(Utc::now(), false).await?; - info!("[Auth Flow] Regenerating OAuth challenge for retry"); + debug!("[Auth Flow] Regenerating OAuth challenge for retry"); let verifier = generate_oauth_challenge(); let mut hasher = sha2::Sha256::new(); hasher.update(&verifier); let result = hasher.finalize(); let challenge = BASE64_URL_SAFE_NO_PAD.encode(result); - info!("[Auth Flow] Retrying SISU authentication"); + debug!("[Auth Flow] Retrying SISU authentication"); let (session_id, redirect_uri) = sisu_authenticate(&token.token, &challenge, &key, current_date, redirect_uri).await?; @@ -501,7 +500,7 @@ impl MinecraftAuthStore { redirect_uri: redirect_uri.value.msa_oauth_redirect, }) } else { - info!("[Auth Flow] Authentication failed and no retry possible"); + error!("[Auth Flow] Authentication failed and no retry possible"); Err(err) } } @@ -552,11 +551,11 @@ impl MinecraftAuthStore { /// Completes the direct OAuth2 flow with event emission (for Flatpak/localhost redirect) pub async fn login_finish_direct_oauth_with_events(&self, code: &str, flow: DirectOAuthFlow, event_id: Uuid) -> Result { - info!("[Direct OAuth Flow] Starting login_finish_direct_oauth"); + debug!("[Direct OAuth Flow] Starting login_finish_direct_oauth"); let state = crate::state::State::get().await?; // Exchange code for access token - info!("[Direct OAuth Flow] Exchanging code for access token"); + debug!("[Direct OAuth Flow] Exchanging code for access token"); let oauth_token = direct_oauth_token(code, &flow.verifier, &flow.redirect_uri).await .map_err(|e| { Self::emit_login_error_event(&state, event_id, format!("Failed to exchange authorization code: {}", e)); @@ -564,7 +563,7 @@ impl MinecraftAuthStore { })?; // Exchange Microsoft access token for Xbox token (RPS method, no SISU) - info!("[Direct OAuth Flow] Exchanging Microsoft token for Xbox token"); + debug!("[Direct OAuth Flow] Exchanging Microsoft token for Xbox token"); Self::emit_login_progress_event( &state, event_id, @@ -579,7 +578,7 @@ impl MinecraftAuthStore { })?; // Exchange Xbox token for XSTS token - info!("[Direct OAuth Flow] Exchanging Xbox token for XSTS token"); + debug!("[Direct OAuth Flow] Exchanging Xbox token for XSTS token"); Self::emit_login_progress_event( &state, event_id, @@ -594,7 +593,7 @@ impl MinecraftAuthStore { })?; // Get Minecraft token - info!("[Direct OAuth Flow] Getting Minecraft token"); + debug!("[Direct OAuth Flow] Getting Minecraft token"); Self::emit_login_progress_event( &state, event_id, @@ -609,7 +608,7 @@ impl MinecraftAuthStore { })?; // Check entitlements - info!("[Direct OAuth Flow] Checking Minecraft entitlements"); + debug!("[Direct OAuth Flow] Checking Minecraft entitlements"); Self::emit_login_progress_event( &state, event_id, @@ -624,7 +623,7 @@ impl MinecraftAuthStore { })?; // Get profile - info!("[Direct OAuth Flow] Fetching Minecraft profile"); + debug!("[Direct OAuth Flow] Fetching Minecraft profile"); Self::emit_login_progress_event( &state, event_id, @@ -637,7 +636,7 @@ impl MinecraftAuthStore { Self::emit_login_error_event(&state, event_id, format!("Failed to fetch Minecraft profile: {}", e)); e })?; - info!( + debug!( "[Direct OAuth Flow] Profile retrieved - ID: {:?}, Name: {}", profile.id, profile.name ); @@ -664,20 +663,20 @@ impl MinecraftAuthStore { }; self.update_or_insert(credentials.clone()).await?; - info!("[Direct OAuth Flow] Login process completed successfully (auth_flow: Direct)"); + debug!("[Direct OAuth Flow] Login process completed successfully (auth_flow: Direct)"); Ok(credentials) } pub async fn login_finish(&self, code: &str, flow: MinecraftLoginFlow) -> Result { - info!("[Auth Flow] Starting login_finish process"); - info!("[Auth Flow] Refreshing device token"); + debug!("[Auth Flow] Starting login_finish process"); + debug!("[Auth Flow] Refreshing device token"); let (key, token, _, _) = self.refresh_and_get_device_token(Utc::now(), false).await?; - info!("[Auth Flow] Getting OAuth token"); + debug!("[Auth Flow] Getting OAuth token"); let oauth_token = oauth_token(code, &flow.verifier).await?; - info!("[Auth Flow] Authorizing with SISU"); + debug!("[Auth Flow] Authorizing with SISU"); let sisu_authorize = sisu_authorize( Some(&flow.session_id), &oauth_token.value.access_token, @@ -687,7 +686,7 @@ impl MinecraftAuthStore { ) .await?; - info!("[Auth Flow] Authorizing with XSTS"); + debug!("[Auth Flow] Authorizing with XSTS"); let xbox_token = xsts_authorize( sisu_authorize.value, &token.token, @@ -696,24 +695,24 @@ impl MinecraftAuthStore { ) .await?; - info!("[Auth Flow] Getting Minecraft token"); + debug!("[Auth Flow] Getting Minecraft token"); let minecraft_token = minecraft_token(xbox_token.value).await?; - info!("[Auth Flow] Checking Minecraft entitlements"); + debug!("[Auth Flow] Checking Minecraft entitlements"); minecraft_entitlements(&minecraft_token.access_token).await?; - info!("[Auth Flow] Fetching Minecraft profile"); + debug!("[Auth Flow] Fetching Minecraft profile"); let profile = minecraft_profile(&minecraft_token.access_token).await?; - info!( + debug!( "[Auth Flow] Profile retrieved - ID: {:?}, Name: {}", profile.id, profile.name ); let profile_id = profile.id.unwrap_or_default(); - info!("[Auth Flow] Using profile ID: {}", profile_id); + debug!("[Auth Flow] Using profile ID: {}", profile_id); let existing_account = self.get_account_by_id(profile_id).await?; - info!( + debug!( "[Auth Flow] Existing account found: {}", existing_account.is_some() ); @@ -736,12 +735,12 @@ impl MinecraftAuthStore { auth_flow: Some(AuthFlow::Sisu), }; - info!( + debug!( "[Auth Flow] Updating/inserting credentials for account: {}", credentials.username ); self.update_or_insert(credentials.clone()).await?; - info!("[Auth Flow] Login process completed successfully (auth_flow: Sisu)"); + debug!("[Auth Flow] Login process completed successfully (auth_flow: Sisu)"); Ok(credentials) } @@ -752,7 +751,7 @@ impl MinecraftAuthStore { force_update: bool, experimental_mode: bool, ) -> Result { - info!( + debug!( "[Token Refresh] Starting NoRisk token refresh check for user: {}", creds.username ); @@ -772,7 +771,7 @@ impl MinecraftAuthStore { validation.insecure_disable_signature_validation(); match decode::(&token.value, &key, &validation) { Ok(data) => { - info!( + debug!( "[Token Refresh] Token expiration check - Expires at: {}", data.claims.exp ); @@ -786,11 +785,11 @@ impl MinecraftAuthStore { } Err(error) => { maybe_update = true; - info!("[Token Refresh] Error decoding token: {:?}", error); + error!("[Token Refresh] Error decoding token: {:?}", error); } }; } else { - info!("[Token Refresh] No token found for the selected mode"); + warn!("[Token Refresh] No token found for the selected mode"); maybe_update = true; } } @@ -810,16 +809,16 @@ impl MinecraftAuthStore { hasher.update(&hwid); let system_id = format!("{:x}", hasher.finalize()); - info!( + debug!( "[Token Refresh] Refreshing token - Force: {}, Maybe: {}, SystemID: {}", force_update, maybe_update, system_id ); // Use NoRiskApi for token refresh with proper error handling - info!("[NoRisk Token] Starting token refresh using NoRiskApi"); + debug!("[NoRisk Token] Starting token refresh using NoRiskApi"); // Use the experimental_mode parameter instead of hardcoded value - info!( + debug!( "[NoRisk Token] Mode: {}", if experimental_mode { "Experimental" @@ -828,7 +827,7 @@ impl MinecraftAuthStore { } ); - info!("[NoRisk Token] Account is known to have child protection enabled: {}", creds.ignore_child_protection_warning); + warn!("[NoRisk Token] Account is known to have child protection enabled: {}", creds.ignore_child_protection_warning); match NoRiskApi::refresh_norisk_token_v3( &system_id, @@ -845,10 +844,10 @@ impl MinecraftAuthStore { let mut copied_credentials = creds.clone(); if experimental_mode { - info!("[NoRisk Token] Storing token in experimental credentials"); + debug!("[NoRisk Token] Storing token in experimental credentials"); copied_credentials.norisk_credentials.experimental = Some(norisk_token); } else { - info!("[NoRisk Token] Storing token in production credentials"); + debug!("[NoRisk Token] Storing token in production credentials"); copied_credentials.norisk_credentials.production = Some(norisk_token); } @@ -856,14 +855,14 @@ impl MinecraftAuthStore { copied_credentials.ignore_child_protection_warning = false; // Update the account in storage - info!("[NoRisk Token] Updating account in storage"); + debug!("[NoRisk Token] Updating account in storage"); self.update_or_insert(copied_credentials.clone()).await?; info!("[Token Refresh] Token refresh completed successfully"); Ok(copied_credentials) } Err(e) => { - info!("[NoRisk Token] Token refresh failed: {:?}", e); + error!("[NoRisk Token] Token refresh failed: {:?}", e); info!("[NoRisk Token] Falling back to original credentials"); // Return the original credentials if token refresh fails let creds_mut = &mut creds.clone(); @@ -875,13 +874,13 @@ impl MinecraftAuthStore { } } } else { - info!("[Token Refresh] Token is still valid, no refresh needed"); + debug!("[Token Refresh] Token is still valid, no refresh needed"); Ok(creds.clone()) } } async fn refresh_token(&self, creds: &Credentials) -> Result> { - info!( + debug!( "[Token Refresh] Starting token refresh for account: {} (auth_flow: {:?})", creds.username, creds.auth_flow ); @@ -892,20 +891,20 @@ impl MinecraftAuthStore { // For backwards compatibility, None defaults to trying Direct first, then SISU match creds.auth_flow { Some(AuthFlow::Direct) => { - info!("[Token Refresh] Using Direct OAuth flow"); + debug!("[Token Refresh] Using Direct OAuth flow"); self.refresh_token_direct(creds, cred_id, profile_name).await } Some(AuthFlow::Sisu) => { - info!("[Token Refresh] Using SISU flow"); + debug!("[Token Refresh] Using SISU flow"); self.refresh_token_sisu(creds, cred_id, profile_name).await } None => { // Backwards compatibility: try SISU first (default), then Direct - info!("[Token Refresh] No auth_flow stored, trying SISU first (default)..."); + debug!("[Token Refresh] No auth_flow stored, trying SISU first (default)..."); match self.refresh_token_sisu(creds, cred_id, profile_name.clone()).await { Ok(result) => Ok(result), Err(sisu_err) => { - info!("[Token Refresh] SISU flow failed: {:?}, trying Direct...", sisu_err); + error!("[Token Refresh] SISU flow failed: {:?}, trying Direct...", sisu_err); self.refresh_token_direct(creds, cred_id, profile_name).await } } @@ -915,19 +914,19 @@ impl MinecraftAuthStore { /// Refresh token using Direct OAuth flow (browser-based login) async fn refresh_token_direct(&self, creds: &Credentials, cred_id: Uuid, profile_name: String) -> Result> { - info!("[Token Refresh] Getting OAuth refresh token (Direct flow)"); + debug!("[Token Refresh] Getting OAuth refresh token (Direct flow)"); let oauth_token = oauth_refresh_direct(&creds.refresh_token).await?; - info!("[Token Refresh] Getting Xbox token (direct via RPS)"); + debug!("[Token Refresh] Getting Xbox token (direct via RPS)"); let xbox_token = xbox_authenticate_rps(&oauth_token.value.access_token).await?; - info!("[Token Refresh] Authorizing with XSTS (direct)"); + debug!("[Token Refresh] Authorizing with XSTS (direct)"); let xsts_token = xsts_authorize_direct(xbox_token).await?; - info!("[Token Refresh] Getting Minecraft token"); + debug!("[Token Refresh] Getting Minecraft token"); let minecraft_token = minecraft_token(xsts_token).await?; - info!("[Token Refresh] Creating new credentials"); + debug!("[Token Refresh] Creating new credentials"); let val = Credentials { id: cred_id, username: profile_name, @@ -940,7 +939,7 @@ impl MinecraftAuthStore { auth_flow: Some(AuthFlow::Direct), }; - info!("[Token Refresh] Updating account in storage"); + debug!("[Token Refresh] Updating account in storage"); self.update_or_insert(val.clone()).await?; info!("[Token Refresh] Token refresh completed successfully (Direct flow)"); @@ -949,15 +948,15 @@ impl MinecraftAuthStore { /// Refresh token using SISU flow (device flow) async fn refresh_token_sisu(&self, creds: &Credentials, cred_id: Uuid, profile_name: String) -> Result> { - info!("[Token Refresh] Getting OAuth refresh token (SISU flow)"); + debug!("[Token Refresh] Getting OAuth refresh token (SISU flow)"); let oauth_token = oauth_refresh(&creds.refresh_token).await?; - info!("[Token Refresh] Refreshing device token"); + debug!("[Token Refresh] Refreshing device token"); let (key, token, current_date, _) = self .refresh_and_get_device_token(oauth_token.date, false) .await?; - info!("[Token Refresh] Authorizing with SISU"); + debug!("[Token Refresh] Authorizing with SISU"); let sisu_authorize = sisu_authorize( None, &oauth_token.value.access_token, @@ -967,7 +966,7 @@ impl MinecraftAuthStore { ) .await?; - info!("[Token Refresh] Authorizing with XSTS"); + debug!("[Token Refresh] Authorizing with XSTS"); let xbox_token = xsts_authorize( sisu_authorize.value, &token.token, @@ -976,10 +975,10 @@ impl MinecraftAuthStore { ) .await?; - info!("[Token Refresh] Getting Minecraft token"); + debug!("[Token Refresh] Getting Minecraft token"); let minecraft_token = minecraft_token(xbox_token.value).await?; - info!("[Token Refresh] Creating new credentials"); + debug!("[Token Refresh] Creating new credentials"); let val = Credentials { id: cred_id, username: profile_name, @@ -992,7 +991,7 @@ impl MinecraftAuthStore { auth_flow: Some(AuthFlow::Sisu), }; - info!("[Token Refresh] Updating account in storage"); + debug!("[Token Refresh] Updating account in storage"); self.update_or_insert(val.clone()).await?; info!("[Token Refresh] Token refresh completed successfully (SISU flow)"); @@ -1022,7 +1021,7 @@ impl MinecraftAuthStore { id: Uuid, experimental_mode: bool, ) -> Result> { - info!( + debug!( "[Account Manager] Getting account by ID with refresh: {}", id ); @@ -1031,7 +1030,7 @@ impl MinecraftAuthStore { let account = self.get_account_by_id(id).await?; if let Some(creds) = account { - info!( + debug!( "[Account Manager] Found account: {}. Refreshing tokens.", creds.username ); @@ -1044,11 +1043,11 @@ impl MinecraftAuthStore { if let Some(updated) = updated_account { // Update account in storage after refresh { - info!("[Account Manager] Acquiring write lock to update account"); + debug!("[Account Manager] Acquiring write lock to update account"); let mut accounts = self.accounts.write().await; - info!("[Account Manager] Successfully acquired write lock"); + debug!("[Account Manager] Successfully acquired write lock"); if let Some(existing) = accounts.iter_mut().find(|acc| acc.id == updated.id) { - info!("[Account Manager] Updating account in list"); + debug!("[Account Manager] Updating account in list"); // Preserve ignore flag from in-memory existing account to avoid // overwriting a recent user 'ignore' action performed concurrently. let existing_flag = existing.ignore_child_protection_warning; @@ -1056,10 +1055,10 @@ impl MinecraftAuthStore { merged.ignore_child_protection_warning = existing_flag || merged.ignore_child_protection_warning; *existing = merged; } - info!("[Account Manager] Releasing write lock"); + debug!("[Account Manager] Releasing write lock"); } // Write-Lock wird hier freigegeben - info!("[Account Manager] Saving updated account"); + debug!("[Account Manager] Saving updated account"); self.save().await?; info!("[Account Manager] Successfully saved account"); @@ -1069,22 +1068,22 @@ impl MinecraftAuthStore { Ok(Some(creds)) } } else { - info!("[Account Manager] Account with ID {} not found", id); + warn!("[Account Manager] Account with ID {} not found", id); Ok(None) } } pub async fn update_or_insert(&self, credentials: Credentials) -> Result<()> { - info!("[Account Manager] Starting account update/insert operation"); - info!("[Account Manager] Account ID: {}", credentials.id); - info!("[Account Manager] Username: {}", credentials.username); + debug!("[Account Manager] Starting account update/insert operation"); + debug!("[Account Manager] Account ID: {}", credentials.id); + debug!("[Account Manager] Username: {}", credentials.username); { let mut accounts = self.accounts.write().await; // If new credentials are active, deactivate all other accounts first if credentials.active { - info!("[Account Manager] New account is active, deactivating all other accounts"); + debug!("[Account Manager] New account is active, deactivating all other accounts"); for account in accounts.iter_mut() { account.active = false; } @@ -1092,7 +1091,7 @@ impl MinecraftAuthStore { // Wenn der Account existiert, aktualisiere ihn if let Some(existing) = accounts.iter_mut().find(|acc| acc.id == credentials.id) { - info!("[Account Manager] Found existing account, updating credentials"); + debug!("[Account Manager] Found existing account, updating credentials"); // Preserve the existing ignore_child_protection_warning flag to avoid // races where another concurrent flow set the flag while this flow // was constructing credentials from stale data. @@ -1103,13 +1102,13 @@ impl MinecraftAuthStore { info!("[Account Manager] Account successfully updated (merged ignore flag)"); } else { // Wenn der Account nicht existiert, füge ihn hinzu - info!("[Account Manager] No existing account found, creating new account"); + debug!("[Account Manager] No existing account found, creating new account"); accounts.push(credentials); info!("[Account Manager] New account successfully created"); } } // Write-Lock wird hier automatisch freigegeben - info!("[Account Manager] Saving account changes to storage"); + debug!("[Account Manager] Saving account changes to storage"); self.save().await?; info!("[Account Manager] Account changes successfully saved"); @@ -1121,17 +1120,17 @@ impl MinecraftAuthStore { creds: &Credentials, experimental_mode: bool, ) -> Result> { - info!( + debug!( "[Token Check] Starting token validation check for user: {}", creds.username ); - info!( + debug!( "[Token Check] Microsoft token expires at: {}", creds.expires ); if creds.expires <= Utc::now() + Duration::minutes(5) { - info!("[Token Check] Microsoft token nearing expiry, initiating proactive refresh"); + debug!("[Token Check] Microsoft token nearing expiry, initiating proactive refresh"); let old_credentials = creds.clone(); let res = self.refresh_token(&old_credentials).await; @@ -1149,7 +1148,7 @@ impl MinecraftAuthStore { .await?, )) } else { - info!("[Token Check] Failed to refresh Microsoft token - No credentials found"); + error!("[Token Check] Failed to refresh Microsoft token - No credentials found"); Err(AppError::NoCredentialsError) }; } @@ -1159,21 +1158,21 @@ impl MinecraftAuthStore { ) = err { if source.is_connect() || source.is_timeout() { - info!("[Token Check] Connection error during refresh, using old credentials"); + error!("[Token Check] Connection error during refresh, using old credentials"); return Ok(Some(old_credentials)); } } - info!("[Token Check] Error during token refresh: {:?}", err); + error!("[Token Check] Error during token refresh: {:?}", err); Err(err) } } } else { - info!("[Token Check] Microsoft token is still valid"); + debug!("[Token Check] Microsoft token is still valid"); if creds.ignore_child_protection_warning { - info!("[Token Check] Skipping NoRisk token check due to child protection warning ignore flag"); + warn!("[Token Check] Skipping NoRisk token check due to child protection warning ignore flag"); Ok(None) } else { - info!("[Token Check] Checking NoRisk token status"); + debug!("[Token Check] Checking NoRisk token status"); Ok(Some( self.refresh_norisk_token_if_necessary(&creds.clone(), false, experimental_mode) .await?, @@ -1183,23 +1182,23 @@ impl MinecraftAuthStore { } pub async fn get_active_account(&self) -> Result> { - info!("[Account Manager] Starting get_active_account process"); + debug!("[Account Manager] Starting get_active_account process"); // Get the global state to check the experimental mode let state = crate::state::State::get().await?; let is_experimental = state.config_manager.is_experimental_mode().await; - info!( + debug!( "[Account Manager] Global experimental mode is: {}", is_experimental ); // Zuerst nur lesen um den aktiven Account zu finden let active_account = { - info!("[Account Manager] Acquiring read lock to find active account"); + debug!("[Account Manager] Acquiring read lock to find active account"); let accounts = self.accounts.read().await; - info!("[Account Manager] Successfully acquired read lock"); + debug!("[Account Manager] Successfully acquired read lock"); let account = accounts.iter().find(|acc| acc.active).cloned(); - info!( + debug!( "[Account Manager] Active account found: {}", account.is_some() ); @@ -1207,7 +1206,7 @@ impl MinecraftAuthStore { }; if let Some(account) = active_account { - info!( + debug!( "[Account Manager] Refreshing credentials for active account: {}", account.username ); @@ -1219,11 +1218,11 @@ impl MinecraftAuthStore { if let Some(updated) = updated_account { // Aktualisiere den Account in der Liste { - info!("[Account Manager] Acquiring write lock to update account"); + debug!("[Account Manager] Acquiring write lock to update account"); let mut accounts = self.accounts.write().await; - info!("[Account Manager] Successfully acquired write lock"); + debug!("[Account Manager] Successfully acquired write lock"); if let Some(existing) = accounts.iter_mut().find(|acc| acc.id == updated.id) { - info!("[Account Manager] Updating account in list"); + debug!("[Account Manager] Updating account in list"); // Preserve ignore flag from in-memory existing account to avoid // overwriting a recent user 'ignore' action performed concurrently. let existing_flag = existing.ignore_child_protection_warning; @@ -1231,10 +1230,10 @@ impl MinecraftAuthStore { merged.ignore_child_protection_warning = existing_flag || merged.ignore_child_protection_warning; *existing = merged; } - info!("[Account Manager] Releasing write lock"); + debug!("[Account Manager] Releasing write lock"); } // Write-Lock wird hier freigegeben - info!("[Account Manager] Saving updated account"); + debug!("[Account Manager] Saving updated account"); self.save().await?; info!("[Account Manager] Successfully saved account"); @@ -1249,7 +1248,7 @@ impl MinecraftAuthStore { let first_account = { let mut accounts = self.accounts.write().await; if let Some(first_account) = accounts.first_mut() { - info!( + debug!( "[Account Manager] Setting first account as active: {}", first_account.username ); @@ -1261,7 +1260,7 @@ impl MinecraftAuthStore { }; // Write-Lock wird hier freigegeben if let Some(account) = first_account { - info!("[Account Manager] Saving changes"); + debug!("[Account Manager] Saving changes"); self.save().await?; info!("[Account Manager] Successfully saved changes"); Ok(Some(account)) @@ -1273,26 +1272,26 @@ impl MinecraftAuthStore { } pub async fn remove_account(&self, id: Uuid) -> Result<()> { - info!("[Account Manager] Starting account removal for ID: {}", id); + debug!("[Account Manager] Starting account removal for ID: {}", id); { - info!("[Account Manager] Acquiring write lock for account removal"); + debug!("[Account Manager] Acquiring write lock for account removal"); let mut accounts = self.accounts.write().await; - info!("[Account Manager] Successfully acquired write lock"); + debug!("[Account Manager] Successfully acquired write lock"); let initial_count = accounts.len(); accounts.retain(|acc| acc.id != id); let final_count = accounts.len(); if initial_count == final_count { - info!("[Account Manager] Warning: No account found with ID {}", id); + warn!("[Account Manager] Warning: No account found with ID {}", id); } else { info!("[Account Manager] Successfully removed account"); } - info!("[Account Manager] Releasing write lock"); + debug!("[Account Manager] Releasing write lock"); } // Write-Lock wird hier freigegeben - info!("[Account Manager] Saving changes after account removal"); + debug!("[Account Manager] Saving changes after account removal"); self.save().await?; info!("[Account Manager] Successfully saved changes"); @@ -1300,27 +1299,27 @@ impl MinecraftAuthStore { } pub async fn get_all_accounts(&self) -> Result> { - info!("[Account Manager] Starting get_all_accounts operation"); + debug!("[Account Manager] Starting get_all_accounts operation"); - info!("[Account Manager] Acquiring read lock"); + debug!("[Account Manager] Acquiring read lock"); let accounts = self.accounts.read().await; - info!("[Account Manager] Successfully acquired read lock"); + debug!("[Account Manager] Successfully acquired read lock"); - info!("[Account Manager] Found {} accounts", accounts.len()); + debug!("[Account Manager] Found {} accounts", accounts.len()); let accounts_clone = accounts.clone(); - info!("[Account Manager] Returning all accounts"); + debug!("[Account Manager] Returning all accounts"); Ok(accounts_clone) } pub async fn set_active_account(&self, account_id: Uuid) -> Result<()> { - info!("[Account Manager] Starting set_active_account operation"); - info!("[Account Manager] Setting account {} as active", account_id); + debug!("[Account Manager] Starting set_active_account operation"); + debug!("[Account Manager] Setting account {} as active", account_id); { - info!("[Account Manager] Acquiring write lock"); + debug!("[Account Manager] Acquiring write lock"); let mut accounts = self.accounts.write().await; - info!("[Account Manager] Successfully acquired write lock"); + debug!("[Account Manager] Successfully acquired write lock"); // Set all accounts to inactive first info!("[Account Manager] Deactivating all accounts"); @@ -1330,20 +1329,20 @@ impl MinecraftAuthStore { // Find and set the specified account as active if let Some(account) = accounts.iter_mut().find(|acc| acc.id == account_id) { - info!("[Account Manager] Found account, setting as active"); + debug!("[Account Manager] Found account, setting as active"); account.active = true; } else { - info!("[Account Manager] Warning: Account not found"); + warn!("[Account Manager] Warning: Account not found"); return Err(AppError::AccountError(format!( "Account with ID {} not found", account_id ))); } - info!("[Account Manager] Releasing write lock"); + debug!("[Account Manager] Releasing write lock"); } // Write-Lock wird hier freigegeben - info!("[Account Manager] Saving changes"); + debug!("[Account Manager] Saving changes"); self.save().await?; info!("[Account Manager] Successfully saved changes"); @@ -1702,7 +1701,7 @@ async fn xsts_authorize_direct(xbox_token: String) -> Result { if let Ok(xbox_error) = serde_json::from_str::(&text) { if let Some(xerr) = xbox_error.XErr { let message = xbox_error_to_message(xerr, xbox_error.Redirect.as_deref()); - info!("Xbox authentication error: XErr={}, Message={:?}", xerr, xbox_error.Message); + error!("Xbox authentication error: XErr={}, Message={:?}", xerr, xbox_error.Message); return Err(MinecraftAuthenticationError::XboxError(message).into()); } } @@ -2050,7 +2049,7 @@ async fn minecraft_entitlements( }); if !has_java_license { - info!("No Minecraft Java license found. Entitlements: {:?}", entitlements.items); + error!("No Minecraft Java license found. Entitlements: {:?}", entitlements.items); return Err(MinecraftAuthenticationError::NoMinecraftLicense); } @@ -2076,7 +2075,7 @@ where Err(err) => { if err.is_connect() || err.is_timeout() { if i < RETRY_COUNT - 1 { - info!("Request failed with connect error, retrying...",); + error!("Request failed with connect error, retrying...",); tokio::time::sleep(RETRY_WAIT).await; resp = reqwest_request().await; } else { @@ -2211,7 +2210,7 @@ async fn send_signed_request( if let Ok(xbox_error) = serde_json::from_str::(&text) { if let Some(xerr) = xbox_error.XErr { let message = xbox_error_to_message(xerr, xbox_error.Redirect.as_deref()); - info!("Xbox authentication error in signed request: XErr={}, Message={:?}", xerr, xbox_error.Message); + error!("Xbox authentication error in signed request: XErr={}, Message={:?}", xerr, xbox_error.Message); return Err(AppError::MinecraftAuthenticationError(MinecraftAuthenticationError::XboxError(message))); } } @@ -2259,7 +2258,7 @@ pub async fn start_oauth_callback_server( success_html: String, error_html: String, ) -> Result<(tokio::task::JoinHandle>, oneshot::Receiver>)> { - info!("[OAuth Server] Starting callback server on port {}", port); + debug!("[OAuth Server] Starting callback server on port {}", port); let (tx, rx) = oneshot::channel(); let tx_shared = Arc::new(tokio::sync::Mutex::new(Some(tx))); @@ -2279,7 +2278,7 @@ pub async fn start_oauth_callback_server( let error_html_shared = error_html_shared.clone(); async move { if let Some(code) = params.get("code") { - info!("[OAuth Server] Received authorization code"); + debug!("[OAuth Server] Received authorization code"); // Send the code through the channel if let Some(tx) = tx_shared.lock().await.take() { let _ = tx.send(Ok(code.clone())); diff --git a/src-tauri/src/minecraft/installer.rs b/src-tauri/src/minecraft/installer.rs index 6bc70ad4..869663a0 100644 --- a/src-tauri/src/minecraft/installer.rs +++ b/src-tauri/src/minecraft/installer.rs @@ -14,7 +14,7 @@ use crate::minecraft::{MinecraftLaunchParameters, MinecraftLauncher}; use crate::state::event_state::{EventPayload, EventType}; use crate::state::profile_state::{ModLoader, Profile}; use crate::state::state_manager::State; -use log::{error, info, warn}; +use log::{error, info, warn, debug}; use rand::Rng; use uuid::Uuid; @@ -97,16 +97,16 @@ pub async fn install_minecraft_version( }; // rng goes out of scope here if should_throw_error { - info!("[InstallTest] Randomly decided to throw test error."); + debug!("[InstallTest] Randomly decided to throw test error."); //return Err(AppError::Unknown("Testfehler (50% Chance) für das Error-Handling!".to_string())); } else { - info!("[InstallTest] Randomly decided NOT to throw test error. Proceeding normally."); + debug!("[InstallTest] Randomly decided NOT to throw test error. Proceeding normally."); } // <--- END HARDCODED TEST ERROR --- > // Execute migration if provided if let Some(migration) = &migration_info { - info!("[Launch] Executing migration before installation: {:?}", migration); + debug!("[Launch] Executing migration before installation: {:?}", migration); // Execute the migration (detailed progress events are sent from within execute_group_migration) match crate::utils::profile_utils::execute_group_migration(migration.clone(), Some(profile.id)).await { @@ -162,7 +162,7 @@ pub async fn install_minecraft_version( // Get Java version from Minecraft version manifest let java_version = piston_meta.java_version.major_version as u32; - info!("\nChecking Java {} for Minecraft...", java_version); + debug!("\nChecking Java {} for Minecraft...", java_version); // Emit Java installation event let event_id = emit_progress_event( @@ -181,7 +181,7 @@ pub async fn install_minecraft_version( { // Try to use the custom Java path let custom_path = profile.settings.java_path.as_ref().unwrap(); - info!("Using custom Java path from profile: {}", custom_path); + debug!("Using custom Java path from profile: {}", custom_path); // Verify that the custom Java path exists and is valid let path = std::path::PathBuf::from(custom_path); @@ -238,7 +238,7 @@ pub async fn install_minecraft_version( // Download and setup Java if necessary let java_path = if custom_java_valid { - info!("Using verified custom Java path: {:?}", java_path); + debug!("Using verified custom Java path: {:?}", java_path); // Update progress to 100% since we're using a custom path emit_progress_event( @@ -254,7 +254,7 @@ pub async fn install_minecraft_version( java_path } else { // Download Java since custom path is not valid or not set - info!("Downloading Java {}...", java_version); + debug!("Downloading Java {}...", java_version); let java_service = JavaDownloadService::new(); let downloaded_path = java_service .get_or_download_java( @@ -287,7 +287,7 @@ pub async fn install_minecraft_version( std::fs::create_dir_all(&game_directory)?; // --- NEW: Copy StartUpHelper data FIRST --- - info!("\nChecking for StartUpHelper data to import..."); + debug!("\nChecking for StartUpHelper data to import..."); // Load NoriskPackDefinition if a pack is selected let norisk_pack = if let Some(pack_id) = &profile.selected_norisk_pack_id { @@ -306,7 +306,7 @@ pub async fn install_minecraft_version( // --- END NEW --- // --- Copy initial data from default Minecraft installation --- - info!("\nChecking for user data to import..."); + debug!("\nChecking for user data to import..."); if let Err(e) = mc_utils::copy_initial_data_from_default_minecraft(profile, &game_directory).await { @@ -328,7 +328,7 @@ pub async fn install_minecraft_version( .await?; // Download all required files - info!("\nDownloading libraries..."); + debug!("\nDownloading libraries..."); let libraries_service = MinecraftLibrariesDownloadService::new() .with_concurrent_downloads(launcher_config.concurrent_downloads); libraries_service @@ -357,7 +357,7 @@ pub async fn install_minecraft_version( ) .await?; - info!("\nExtracting natives..."); + debug!("\nExtracting natives..."); let natives_service = MinecraftNativesDownloadService::new(); natives_service .extract_natives(&piston_meta.libraries, version_id) @@ -374,7 +374,7 @@ pub async fn install_minecraft_version( ) .await?; - info!("\nDownloading assets..."); + debug!("\nDownloading assets..."); let assets_service = MinecraftAssetsDownloadService::new() .with_concurrent_downloads(launcher_config.concurrent_downloads); assets_service @@ -383,7 +383,7 @@ pub async fn install_minecraft_version( info!("Asset download completed!"); // Download NoRiskClient assets if profile has a selected pack - info!("\nDownloading NoRiskClient assets..."); + debug!("\nDownloading NoRiskClient assets..."); let norisk_assets_service = NoriskClientAssetsDownloadService::new() .with_concurrent_downloads(launcher_config.concurrent_downloads); @@ -406,7 +406,7 @@ pub async fn install_minecraft_version( ) .await?; - info!("\nDownloading Minecraft client..."); + debug!("\nDownloading Minecraft client..."); let client_service = MinecraftClientDownloadService::new(); client_service .download_client(&piston_meta.downloads.client, &piston_meta.id) @@ -430,7 +430,7 @@ pub async fn install_minecraft_version( credentials.clone(), ); - info!("\nPreparing launch parameters..."); + debug!("\nPreparing launch parameters..."); // Get memory settings (global for standard profiles, profile-specific for custom) let memory_max = if profile.is_standard_version { @@ -567,7 +567,7 @@ pub async fn install_minecraft_version( .get_token_for_mode(is_experimental_mode) { Ok(norisk_token_value) => { - info!("Attempting to update Norisk pack configuration using obtained token for pack '{}'...", pack_id); + debug!("Attempting to update Norisk pack configuration using obtained token for pack '{}'...", pack_id); if let Err(update_err) = state .norisk_pack_manager .fetch_and_update_config(&norisk_token_value, is_experimental_mode) @@ -736,7 +736,7 @@ pub async fn install_minecraft_version( let mod_cache_dir = LAUNCHER_DIRECTORY.meta_dir().join("mod_cache"); // ---> NEW: Get custom mods for this profile <--- - info!("Listing custom mods for profile '{}'...", profile.name); + debug!("Listing custom mods for profile '{}'...", profile.name); let custom_mod_infos = state.profile_manager.list_custom_mods(&profile).await?; info!( "Found {} custom mods for profile '{}'", @@ -777,7 +777,7 @@ pub async fn install_minecraft_version( let mut current_jvm_args = launch_params.additional_jvm_args.clone(); current_jvm_args.push(add_mods_arg); launch_params = launch_params.with_additional_jvm_args(current_jvm_args); - info!("Configured Fabric addMods meta file for profile '{}'", profile.name); + debug!("Configured Fabric addMods meta file for profile '{}'", profile.name); } // --- Step: Sync mods from cache to profile directory --- @@ -827,18 +827,18 @@ pub async fn install_minecraft_version( // Download log4j configuration if available let mut log4j_arg = None; if let Some(logging) = &piston_meta.logging { - info!("\nDownloading log4j configuration..."); + debug!("\nDownloading log4j configuration..."); let logging_service = MinecraftLoggingDownloadService::new(); let config_path = logging_service .download_logging_config(&logging.client) .await?; log4j_arg = Some(logging_service.get_jvm_argument(&config_path)); - info!("Log4j configuration download completed!"); + debug!("Log4j configuration download completed!"); } // Add log4j configuration to JVM arguments if available if let Some(log4j_argument) = log4j_arg { - info!("Adding log4j configuration: {}", log4j_argument); + debug!("Adding log4j configuration: {}", log4j_argument); let mut jvm_args = launch_params.additional_jvm_args.clone(); jvm_args.push(log4j_argument); launch_params = launch_params.with_additional_jvm_args(jvm_args); @@ -847,7 +847,7 @@ pub async fn install_minecraft_version( // --- Execute pre-launch hooks --- let launcher_config = state.config_manager.get_config().await; if let Some(hook) = &launcher_config.hooks.pre_launch { - info!("Executing pre-launch hook: {}", hook); + debug!("Executing pre-launch hook: {}", hook); let hook_event_id = emit_progress_event( &state, EventType::LaunchingMinecraft, diff --git a/src-tauri/src/state/discord_state.rs b/src-tauri/src/state/discord_state.rs index 92cf446e..0c08a34c 100644 --- a/src-tauri/src/state/discord_state.rs +++ b/src-tauri/src/state/discord_state.rs @@ -29,7 +29,7 @@ pub struct DiscordManager { impl DiscordManager { pub async fn new(enabled: bool) -> Result { - info!( + debug!( "Initializing Discord Rich Presence Manager (enabled: {})", enabled ); @@ -58,9 +58,9 @@ impl DiscordManager { error!("Failed to set initial Discord state: {}", e); } } else { - info!("Discord Rich Presence is disabled"); + debug!("Discord Rich Presence is disabled"); } - info!("Successfully initialized Discord Rich Presence Manager"); + debug!("Successfully initialized Discord Rich Presence Manager"); Ok(manager) } @@ -86,7 +86,7 @@ impl DiscordManager { AppError::DiscordError(format!("Discord connection error: {}", e)) }) { Ok(_) => { - info!("Successfully connected to Discord client"); + debug!("Successfully connected to Discord client"); *client_lock = Some(client); } Err(e) => { @@ -118,7 +118,7 @@ impl DiscordManager { .map_err(|e| AppError::DiscordError(format!("Discord disconnect error: {}", e))) { Ok(_) => { - info!("Successfully disconnected from Discord client"); + debug!("Successfully disconnected from Discord client"); } Err(e) => { warn!("Error disconnecting from Discord client: {}", e); @@ -286,7 +286,7 @@ impl DiscordManager { // Clone self to move into spawned task let manager_clone = self.clone(); tokio::spawn(async move { - info!("Discord: Starting background connection..."); + debug!("Discord: Starting background connection..."); // Catch errors to prevent application crashes if let Err(e) = manager_clone.connect().await { @@ -300,7 +300,7 @@ impl DiscordManager { return; } - info!("Discord: Background connection completed successfully."); + debug!("Discord: Background connection completed successfully."); }); } else if was_enabled && !enabled { // Was enabled, now disabled - disconnect diff --git a/src-tauri/src/state/event_state.rs b/src-tauri/src/state/event_state.rs index c57fb868..93363c4c 100644 --- a/src-tauri/src/state/event_state.rs +++ b/src-tauri/src/state/event_state.rs @@ -1,7 +1,7 @@ use crate::error::Result; use crate::state::process_state::ProcessMetadata; use dashmap::DashMap; -use log::info; +use log::{info, debug}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tauri::Emitter; @@ -104,12 +104,12 @@ pub struct EventState { impl EventState { pub fn new(app: Option>) -> Self { - info!("Initializing EventState..."); + debug!("Initializing EventState..."); let state = Self { app, active_events: DashMap::new(), }; - info!("Successfully initialized EventState."); + debug!("Successfully initialized EventState."); state } diff --git a/src-tauri/src/state/norisk_packs_state.rs b/src-tauri/src/state/norisk_packs_state.rs index bcedd683..2aeb15eb 100644 --- a/src-tauri/src/state/norisk_packs_state.rs +++ b/src-tauri/src/state/norisk_packs_state.rs @@ -35,7 +35,7 @@ impl NoriskPackManager { /// Creates a new NoriskPackManager instance, loading the configuration from the specified path. /// If the file doesn't exist, it initializes with a default empty configuration. pub fn new(config_path: PathBuf) -> Result { - info!( + debug!( "NoriskPackManager: Initializing with path: {:?} (config loading deferred)", config_path ); @@ -81,7 +81,7 @@ impl NoriskPackManager { norisk_token: &str, is_experimental: bool, ) -> Result<()> { - info!("Fetching latest Norisk packs config from API..."); + debug!("Fetching latest Norisk packs config from API..."); match NoRiskApi::get_modpacks(norisk_token, is_experimental).await { Ok(new_config) => { @@ -185,7 +185,7 @@ impl NoriskPackManager { #[async_trait] impl PostInitializationHandler for NoriskPackManager { async fn on_state_ready(&self, _app_handle: Arc) -> Result<()> { - info!("NoriskPackManager: on_state_ready called. Loading configuration..."); + debug!("NoriskPackManager: on_state_ready called. Loading configuration..."); // Select load path based on experimental mode if accessible let load_path = if let Ok(state) = crate::state::state_manager::State::get().await { let is_exp = state.config_manager.is_experimental_mode().await; diff --git a/src-tauri/src/state/process_state.rs b/src-tauri/src/state/process_state.rs index fd71e245..19cbb478 100644 --- a/src-tauri/src/state/process_state.rs +++ b/src-tauri/src/state/process_state.rs @@ -92,7 +92,7 @@ impl ProcessManager { processes_file_path: PathBuf, app_handle: Arc, ) -> Result { - log::info!( + log::debug!( "Initializing ProcessManager with state file: {:?}", processes_file_path ); @@ -124,7 +124,7 @@ impl ProcessManager { app_handle: Arc, mut receiver: mpsc::Receiver, ) { - log::info!("Starting crash report event processor task."); + log::debug!("Starting crash report event processor task."); let global_state_res = State::get().await; let event_state_clone = match global_state_res { @@ -156,7 +156,7 @@ impl ProcessManager { .process_manager .crash_report_contents .insert(notification.process_id, content.clone()); - log::info!( + log::debug!( "Stored crash report content for process {} in ProcessManager.", notification.process_id ); @@ -200,7 +200,7 @@ impl ProcessManager { e ); } else { - log::info!( + log::debug!( "Successfully emitted crash report for process {} to UI.", notification.process_id ); @@ -223,20 +223,20 @@ impl ProcessManager { async fn load_processes_and_watchers(&self) -> Result<()> { let file_path = &self.processes_file_path; if !file_path.exists() { - log::info!( + log::warn!( "Processes file not found ('{:?}'), starting fresh.", file_path ); return Ok(()); } - log::info!("Loading processes metadata from '{:?}'...", file_path); + log::debug!("Loading processes metadata from '{:?}'...", file_path); let json_content = async_fs::read_to_string(&file_path) .await .map_err(AppError::Io)?; match serde_json::from_str::>(&json_content) { Ok(loaded_metadata) => { - log::info!( + log::debug!( "Successfully deserialized {} process metadata entries.", loaded_metadata.len() ); @@ -266,7 +266,7 @@ impl ProcessManager { ); metadata.state = ProcessState::Running; } - log::info!( + log::debug!( "Loading running process {} (PID: {}) metadata.", metadata.id, metadata.pid @@ -282,17 +282,17 @@ impl ProcessManager { ); // Watcher für diesen geladenen, laufenden Prozess starten - log::info!( + log::debug!( "Attempting to get global state for process {} to start watcher.", metadata.id ); match State::get().await { Ok(global_state) => { - log::info!( + log::debug!( "Successfully got global state for process {}.", metadata.id ); - log::info!( + log::debug!( "Attempting to get instance path for profile {} (process {}).", metadata.profile_id, metadata.id @@ -306,7 +306,7 @@ impl ProcessManager { log::info!("Successfully got instance path {:?} for profile {} (process {}).", instance_path, metadata.profile_id, metadata.id); let crash_reports_path = instance_path.join("crash-reports"); - log::info!("Attempting to start crash report watcher for process {} on path {:?}.", metadata.id, crash_reports_path); + log::debug!("Attempting to start crash report watcher for process {} on path {:?}.", metadata.id, crash_reports_path); if let Err(e) = self .start_crash_report_watcher( metadata.id, @@ -337,7 +337,7 @@ impl ProcessManager { ); } } - log::info!( + log::debug!( "Created {} active Process entries from loaded metadata.", loaded_count ); @@ -362,7 +362,7 @@ impl ProcessManager { async_fs::create_dir_all(parent_dir) .await .map_err(AppError::Io)?; - log::info!("Created directory for processes file: {:?}", parent_dir); + log::debug!("Created directory for processes file: {:?}", parent_dir); } } @@ -406,7 +406,7 @@ impl ProcessManager { ); return Err(AppError::Io(e)); } - log::info!( + log::debug!( "Created directory {:?} for crash report watcher.", path_to_watch ); @@ -660,7 +660,7 @@ impl ProcessManager { if let Ok(global_state) = State::get().await { let launcher_config = global_state.config_manager.get_config().await; if launcher_config.hide_on_process_start { - log::info!("Hiding main window as configured (hide_on_process_start = true)"); + log::debug!("Hiding main window as configured (hide_on_process_start = true)"); if let Some(main_window) = self.app_handle.get_webview_window("main") { if let Err(e) = main_window.hide() { log::error!("Failed to hide main window: {}", e); @@ -829,7 +829,7 @@ impl ProcessManager { if let Ok(global_state) = State::get().await { let launcher_config = global_state.config_manager.get_config().await; if launcher_config.hide_on_process_start { - log::info!("Showing main window after process exit (hide_on_process_start = true)"); + log::debug!("Showing main window after process exit (hide_on_process_start = true)"); if let Some(main_window) = app_handle_clone_for_monitor.get_webview_window("main") { if let Err(e) = main_window.show() { log::error!("Failed to show main window after process exit: {}", e); @@ -847,7 +847,7 @@ impl ProcessManager { } // Remove process from map - log::info!( + log::debug!( "Removing process entry {} from processes map post-exit.", process_id ); @@ -897,7 +897,7 @@ impl ProcessManager { } pub async fn stop_process(&self, process_id: Uuid) -> Result<()> { - log::info!("Attempting to stop process {}", process_id); + log::debug!("Attempting to stop process {}", process_id); let mut kill_successful = false; let mut pid_for_error: u32 = 0; @@ -909,7 +909,7 @@ impl ProcessManager { process.metadata.state = ProcessState::Stopping; let pid_to_kill = process.metadata.pid; - log::info!( + log::debug!( "Attempting to kill process {} via PID {}", process_id, pid_to_kill @@ -992,7 +992,7 @@ impl ProcessManager { notify_tx: mpsc::Sender, ) { let mut interval = interval(Duration::from_secs(10)); - log::info!("Starting periodic process and watcher checker task."); + log::debug!("Starting periodic process and watcher checker task."); loop { interval.tick().await; @@ -1124,7 +1124,7 @@ impl ProcessManager { async fn periodic_log_tailer(processes_arc: Arc>>) { let mut interval = interval(Duration::from_secs(1)); // Log-Tailing kann weiterhin häufig sein - log::info!("Starting periodic log tailing task (crash reports handled by notify)."); + log::debug!("Starting periodic log tailing task (crash reports handled by notify)."); loop { interval.tick().await; @@ -1224,7 +1224,7 @@ impl ProcessManager { let mut just_skipped_initial = false; if current_size < original_last_pos { - log::info!("Log file {:?} seems to have rotated or shrunk (current: {}, last: {}). Resetting read position to 0.", log_path, current_size, original_last_pos); + log::warn!("Log file {:?} seems to have rotated or shrunk (current: {}, last: {}). Resetting read position to 0.", log_path, current_size, original_last_pos); read_from_pos = 0; // After rotation, we read the new file from the start. } else if original_last_pos == 0 && current_size > 0 { @@ -1355,13 +1355,13 @@ impl ProcessManager { /// Emits ProcessMetricsUpdate events to the frontend. async fn periodic_metrics_collector(processes_arc: Arc>>) { let mut interval = interval(Duration::from_secs(2)); // Collect metrics every 2 seconds - log::info!("Starting periodic metrics collector task."); + log::debug!("Starting periodic metrics collector task."); let mut sys = System::new_all(); // Get number of CPU cores for normalizing CPU usage (sysinfo reports per-core %) let num_cpus = sys.cpus().len().max(1) as f32; - log::info!("Metrics collector: Detected {} CPU cores for normalization", num_cpus); + log::debug!("Metrics collector: Detected {} CPU cores for normalization", num_cpus); loop { interval.tick().await; @@ -1471,7 +1471,7 @@ impl ProcessManager { /// Retrieves the full content of the latest.log file for a given process. /// Internally accesses the global state to get the ProfileManager. pub async fn get_full_log_content(&self, process_id: Uuid) -> Result { - log::info!( + log::debug!( "Attempting to get full log content for process {}", process_id ); @@ -1637,20 +1637,20 @@ impl ProcessManager { /// Adds a task handle to the launching_processes map pub fn add_launching_process(&self, profile_id: Uuid, handle: JoinHandle<()>) { - log::info!("Adding launching task for profile ID: {}", profile_id); + log::debug!("Adding launching task for profile ID: {}", profile_id); self.launching_processes.insert(profile_id, handle); } /// Removes a task handle from the launching_processes map pub fn remove_launching_process(&self, profile_id: Uuid) { - log::info!("Removing launching task for profile ID: {}", profile_id); + log::debug!("Removing launching task for profile ID: {}", profile_id); self.launching_processes.remove(&profile_id); } /// Aborts an ongoing launch process for the given profile ID pub fn abort_launch_process(&self, profile_id: Uuid) -> Result<()> { if let Some((_, handle)) = self.launching_processes.remove(&profile_id) { - log::info!("Aborting launch task for profile ID: {}", profile_id); + log::debug!("Aborting launch task for profile ID: {}", profile_id); // Abort the task handle.abort(); @@ -1695,7 +1695,7 @@ impl ProcessManager { None => return, // No process metadata available }; - log::info!( + log::debug!( "Executing post-exit hook for process {}: {}", process_id, hook @@ -1783,7 +1783,7 @@ impl ProcessManager { Ok(global_state) => { let launcher_config = global_state.config_manager.get_config().await; if launcher_config.open_logs_after_starting { - log::info!( + log::debug!( "Config: Attempting to auto-open log windows for process {}", process_id ); @@ -1824,7 +1824,7 @@ impl ProcessManager { #[async_trait] impl PostInitializationHandler for ProcessManager { async fn on_state_ready(&self, app_handle: Arc) -> Result<()> { - log::info!("ProcessManager: on_state_ready called. Performing post-initialization tasks."); + log::debug!("ProcessManager: on_state_ready called. Performing post-initialization tasks."); // For process_crash_report_events: The task requires the receive end of an mpsc channel. // The most robust way is to initialize tx and rx in `new`, store rx in `self` (e.g., Arc>>) @@ -1834,7 +1834,7 @@ impl PostInitializationHandler for ProcessManager { // This was the critical call causing deadlock issues self.load_processes_and_watchers().await?; - log::info!("ProcessManager: Finished load_processes_and_watchers."); + log::debug!("ProcessManager: Finished load_processes_and_watchers."); let manager_clone_periodic_check_processes = Arc::clone(&self.processes); let manager_clone_periodic_check_watchers = Arc::clone(&self.active_watchers); @@ -1847,15 +1847,15 @@ impl PostInitializationHandler for ProcessManager { manager_clone_periodic_check_watchers, notify_tx_for_periodic_check, )); - log::info!("ProcessManager: Spawned periodic_process_check task."); + log::debug!("ProcessManager: Spawned periodic_process_check task."); let tailer_processes_arc = Arc::clone(&self.processes); tokio::spawn(Self::periodic_log_tailer(tailer_processes_arc)); - log::info!("ProcessManager: Spawned periodic_log_tailer task."); + log::debug!("ProcessManager: Spawned periodic_log_tailer task."); let metrics_processes_arc = Arc::clone(&self.processes); tokio::spawn(Self::periodic_metrics_collector(metrics_processes_arc)); - log::info!("ProcessManager: Spawned periodic_metrics_collector task."); + log::debug!("ProcessManager: Spawned periodic_metrics_collector task."); log::info!("ProcessManager: Successfully completed on_state_ready."); Ok(()) diff --git a/src-tauri/src/state/profile_state.rs b/src-tauri/src/state/profile_state.rs index 5dd63eab..796709bd 100644 --- a/src-tauri/src/state/profile_state.rs +++ b/src-tauri/src/state/profile_state.rs @@ -10,7 +10,7 @@ use crate::utils::path_utils; use async_trait::async_trait; use chrono::{DateTime, Utc}; use futures::future::BoxFuture; -use log::{error, info, warn}; +use log::{error, info, warn, debug}; use serde::{Deserialize, Serialize}; use serde_json; use std::collections::{HashMap, HashSet}; @@ -326,7 +326,7 @@ pub struct ProfileManager { impl ProfileManager { pub fn new(profiles_path: PathBuf) -> Result { - info!( + debug!( "ProfileManager: Initializing with path: {:?} (profiles loading deferred)", profiles_path ); @@ -356,7 +356,7 @@ impl ProfileManager { if !path.exists() { if attempt_count == 1 { - info!("ProfileManager: Profiles file doesn't exist, checking for backups to restore"); + warn!("ProfileManager: Profiles file doesn't exist, checking for backups to restore"); // Try to restore from backup if file doesn't exist match backup_utils::restore_from_backup(path, Some("profiles")).await { Ok(restored_path) => { @@ -377,7 +377,7 @@ impl ProfileManager { Ok(data) => { match serde_json::from_str::>(&data) { Ok(profiles) => { - info!("ProfileManager: Successfully loaded {} profiles from file", profiles.len()); + debug!("ProfileManager: Successfully loaded {} profiles from file", profiles.len()); return Ok(profiles.into_iter().map(|p| (p.id, p)).collect()); } Err(e) => { @@ -440,7 +440,7 @@ impl ProfileManager { async fn save_profiles(&self) -> Result<()> { let _guard = self.save_lock.lock().await; - info!("ProfileManager: Saving profiles to {:?}", self.profiles_path); + debug!("ProfileManager: Saving profiles to {:?}", self.profiles_path); let profiles_data = { let profiles_guard = self.profiles.read().await; @@ -492,7 +492,7 @@ impl ProfileManager { pub async fn create_profile(&self, profile: Profile) -> Result { // The 'profile.path' field is expected to be a relative path/name for the profile directory // e.g., "My Profile Name" or "some_group/My Profile Name" - info!( + debug!( "Attempting to create profile named '{}' with relative path identifier: {:?}", profile.name, profile.path ); @@ -500,7 +500,7 @@ impl ProfileManager { // Calculate the absolute path for the new profile's instance directory let new_profile_instance_path = self.calculate_instance_path_for_profile(&profile)?; - info!( + debug!( "Calculated absolute profile instance directory: {:?}", new_profile_instance_path ); @@ -508,7 +508,7 @@ impl ProfileManager { // Create the specific instance directory for this new profile. // This will also create any necessary parent directories, including the one // where profiles.json (self.profiles_path) will be stored, due to the nature of create_dir_all. - info!( + debug!( "Creating profile instance directory at: {:?}", new_profile_instance_path ); @@ -521,7 +521,7 @@ impl ProfileManager { // Other functions will use calculate_instance_path_for_profile to resolve it. profiles.insert(id, profile); } - info!("Saving profiles metadata to: {:?}", self.profiles_path); + debug!("Saving profiles metadata to: {:?}", self.profiles_path); self.save_profiles().await?; Ok(id) } @@ -543,7 +543,7 @@ impl ProfileManager { //info!("Found standard profile '{}' for ID {}", standard_profile.name, id); Ok(standard_profile) } else { - info!("Profile ID {} not found in standard versions either.", id); + error!("Profile ID {} not found in standard versions either.", id); Err(crate::error::AppError::ProfileNotFound(id)) } } @@ -583,7 +583,7 @@ impl ProfileManager { // Calculate the path for this profile and compare let other_path = path_calculator(profile); if other_path == *target_path { - info!( + warn!( "Found another profile '{}' (ID: {}) using the same path: {:?}", profile.name, profile_id, target_path ); @@ -609,7 +609,7 @@ impl ProfileManager { let profile_dir_path = if let Some(profile) = &profile_to_delete { match self.calculate_instance_path_for_profile(&profile) { Ok(path) => { - info!( + debug!( "Profile '{}' marked for deletion. Directory path: {:?}", profile.name, path ); @@ -627,7 +627,7 @@ impl ProfileManager { } } else { // Profile not found in map, nothing to delete on filesystem - info!("Profile with ID {} not found for deletion.", id); + error!("Profile with ID {} not found for deletion.", id); return Err(AppError::ProfileNotFound(id)); // Return error if profile doesn't exist }; @@ -636,13 +636,13 @@ impl ProfileManager { if self.has_other_profile_with_same_path(id, path, |profile| { self.calculate_instance_path_for_profile(profile).unwrap_or_default() }).await { - info!( + warn!( "Another profile is using the same directory path {:?}. Skipping directory deletion.", path ); false } else { - info!( + debug!( "No other profile uses the directory path {:?}. Safe to delete.", path ); @@ -656,7 +656,7 @@ impl ProfileManager { if should_delete_directory { if let Some(ref path) = profile_dir_path { if path.exists() { - info!("Moving profile directory to trash: {:?}", path); + debug!("Moving profile directory to trash: {:?}", path); match crate::utils::trash_utils::move_path_to_trash(path, Some("profiles")).await { Ok(wrapper) => info!("Profile directory moved to trash wrapper: {:?}", wrapper), Err(e) => { @@ -690,7 +690,7 @@ impl ProfileManager { ); false } else { - info!( + debug!( "No other profile uses the individual directory path {:?}. Safe to delete.", individual_path ); @@ -699,7 +699,7 @@ impl ProfileManager { if should_delete_individual_directory { if individual_path.exists() { - info!("Moving individual profile directory to trash: {:?}", individual_path); + debug!("Moving individual profile directory to trash: {:?}", individual_path); match crate::utils::trash_utils::move_path_to_trash(&individual_path, Some("profiles")).await { Ok(wrapper) => info!("Individual profile directory moved to trash wrapper: {:?}", wrapper), Err(e) => { @@ -744,7 +744,7 @@ impl ProfileManager { // Add a new mod to a specific profile pub async fn add_mod(&self, profile_id: Uuid, mod_info: Mod) -> Result<()> { - info!( + debug!( "Adding mod '{}' (Source: {:?}) to profile {}", mod_info .display_name @@ -765,10 +765,10 @@ impl ProfileManager { profile.mods.push(mod_info); drop(profiles); self.save_profiles().await?; - info!("Successfully added mod to profile {}", profile_id); + debug!("Successfully added mod to profile {}", profile_id); Ok(()) } else { - info!( + warn!( "Mod with the same source already exists in profile {}", profile_id ); @@ -807,14 +807,14 @@ impl ProfileManager { Box::pin(async move { let display_name_log = mod_name.as_deref().unwrap_or(&project_id); let version_log = version_number.as_deref().unwrap_or(&version_id); - info!( + debug!( "Processing Modrinth mod {} (Version {}) for profile {}. Add dependencies: {}", display_name_log, version_log, profile_id, add_dependencies ); let mod_key = (project_id.clone(), version_id.clone()); if visited_mods.contains(&mod_key) { - info!( + debug!( "Skipping already processed mod/dependency: {} ({})", display_name_log, version_log ); @@ -836,7 +836,7 @@ impl ProfileManager { let mut profiles = self.profiles.write().await; if let Some(profile) = profiles.get_mut(&profile_id) { if !profile.mods.iter().any(|m| m.source == source) { - info!( + debug!( "Adding mod {} ({}) to profile {}", display_name_log, version_log, profile_id ); @@ -858,7 +858,7 @@ impl ProfileManager { profile.mods.push(new_mod); needs_save = true; } else { - info!( + debug!( "Mod {} ({}) already exists in profile {}. Skipping addition.", display_name_log, version_log, profile_id ); @@ -877,7 +877,7 @@ impl ProfileManager { } if add_dependencies { - info!( + debug!( "Fetching dependencies for {} ({})", display_name_log, version_log ); @@ -891,7 +891,7 @@ impl ProfileManager { if let Some(version_info) = versions.into_iter().find(|v| v.id == version_id) { - info!( + debug!( "Found {} dependencies for {} ({})", version_info.dependencies.len(), display_name_log, @@ -900,10 +900,10 @@ impl ProfileManager { for dependency in version_info.dependencies { if dependency.dependency_type == ModrinthDependencyType::Required { - info!("Processing required dependency: Project={:?}, Version={:?}", dependency.project_id, dependency.version_id); + debug!("Processing required dependency: Project={:?}, Version={:?}", dependency.project_id, dependency.version_id); if let Some(dep_project_id) = dependency.project_id { - info!("Attempting to find compatible version for dependency project '{}'", dep_project_id); + debug!("Attempting to find compatible version for dependency project '{}'", dep_project_id); let target_version_id = dependency.version_id; @@ -955,7 +955,7 @@ impl ProfileManager { } if let Some(selected_dep_version) = best_dep_version { - info!("Selected version '{}' ({}) for dependency '{}'", selected_dep_version.name, selected_dep_version.id, dep_project_id); + debug!("Selected version '{}' ({}) for dependency '{}'", selected_dep_version.name, selected_dep_version.id, dep_project_id); if let Some(primary_file) = selected_dep_version.files.iter().find(|f| f.primary) { match self.add_modrinth_mod_internal( @@ -1069,7 +1069,7 @@ impl ProfileManager { ModPlatform::CurseForge => "CurseForge", }; - info!( + debug!( "Adding {} mod {} to profile {} (dependencies: {})", platform_name, display_name_log, payload.profile_id, add_dependencies ); @@ -1096,7 +1096,7 @@ impl ProfileManager { if let Some(profile) = profiles.get_mut(&payload.profile_id) { if !profile.mods.iter().any(|m| m.source == source) { - info!( + debug!( "Adding mod {} to profile {}", display_name_log, payload.profile_id ); @@ -1149,7 +1149,7 @@ impl ProfileManager { ) -> Result<()> { use crate::integrations::unified_mod::{ModPlatform, UnifiedModVersionsParams}; - info!( + debug!( "Installing dependencies for {} mod {} (version: {})", platform_name, display_name_log, payload.version_number.as_deref().unwrap_or("unknown") ); @@ -1173,7 +1173,7 @@ impl ProfileManager { }; if let Some(target_version) = versions_response.versions.into_iter().find(|v| v.id == payload.version_id) { - info!("Found {} dependencies for {} mod {}", target_version.files.len(), platform_name, display_name_log); + debug!("Found {} dependencies for {} mod {}", target_version.files.len(), platform_name, display_name_log); match payload.source { ModPlatform::Modrinth => { @@ -1216,7 +1216,7 @@ impl ProfileManager { // Only install required dependencies if let Some(relation_type) = CurseForgeFileRelationType::from_u32(dependency.relationType) { if relation_type.should_install() { - info!("Processing CurseForge dependency: ModId={}, RelationType={}", dependency.modId, relation_type.as_str()); + debug!("Processing CurseForge dependency: ModId={}, RelationType={}", dependency.modId, relation_type.as_str()); // Get dependency mod information match crate::integrations::curseforge::get_mod_info(dependency.modId).await { @@ -1302,7 +1302,7 @@ impl ProfileManager { for dependency in &version.dependencies { if dependency.dependency_type == ModrinthDependencyType::Required { - info!("Processing required Modrinth dependency: Project={:?}, Version={:?}", dependency.project_id, dependency.version_id); + debug!("Processing required Modrinth dependency: Project={:?}, Version={:?}", dependency.project_id, dependency.version_id); if let Some(dep_project_id) = &dependency.project_id { // Get compatible versions for the dependency @@ -1433,7 +1433,7 @@ impl ProfileManager { mod_id: Uuid, enabled: bool, ) -> Result<()> { - info!( + debug!( "Setting mod {} enabled status to {} for profile {}", mod_id, enabled, profile_id ); @@ -1475,7 +1475,7 @@ impl ProfileManager { mod_id: Uuid, updates_enabled: bool, ) -> Result<()> { - info!( + debug!( "Setting mod {} updates_enabled status to {} for profile {}", mod_id, updates_enabled, profile_id ); @@ -1512,7 +1512,7 @@ impl ProfileManager { // Remove a specific mod from a profile pub async fn delete_mod(&self, profile_id: Uuid, mod_id: Uuid) -> Result<()> { - info!("Deleting mod {} from profile {}", mod_id, profile_id); + debug!("Deleting mod {} from profile {}", mod_id, profile_id); let mut profiles = self.profiles.write().await; @@ -1550,7 +1550,7 @@ impl ProfileManager { loader: ModLoader, disabled: bool, ) -> Result<()> { - info!( + debug!( "Setting disabled state for pack mod '{}' (Pack: '{}', MC: {}, Loader: {:?}) for profile {} to {}", mod_id, pack_id, game_version, loader, profile_id, disabled ); @@ -1620,7 +1620,7 @@ impl ProfileManager { mod_id: Uuid, new_version_details: &crate::integrations::curseforge::CurseForgeFile, ) -> Result<()> { - info!( + debug!( "Attempting to update CurseForge mod instance {} in profile {} to version '{}' (ID: {})", mod_id, profile_id, new_version_details.displayName, new_version_details.id ); @@ -1635,7 +1635,7 @@ impl ProfileManager { AppError::ProfileNotFound(profile_id) })?; - info!( + debug!( "Checking required dependencies for new CurseForge version {}...", new_version_details.id ); @@ -1662,7 +1662,7 @@ impl ProfileManager { ); missing_deps.push(dependency.modId); } else { - info!( + debug!( "Required dependency project '{}' found in profile.", dependency.modId ); @@ -1693,7 +1693,7 @@ impl ProfileManager { ))); } - info!( + debug!( "Updating CurseForge mod instance {} from version {} to {} using file '{}'", mod_id, mod_to_update.version.as_deref().unwrap_or("?"), @@ -1752,7 +1752,7 @@ impl ProfileManager { // Now install any missing dependencies if !missing_deps.is_empty() { let display_name_log = new_version_details.displayName.as_str(); - info!("Installing {} missing CurseForge dependencies", missing_deps.len()); + debug!("Installing {} missing CurseForge dependencies", missing_deps.len()); match self.install_curseforge_dependencies(profile_id, &new_version_details, display_name_log).await { Ok(_) => info!("Successfully installed CurseForge dependencies for '{}'", display_name_log), Err(e) => error!("Failed to install some CurseForge dependencies for '{}': {}", display_name_log, e), @@ -1773,7 +1773,7 @@ impl ProfileManager { mod_id: Uuid, new_version_details: &ModrinthVersion, ) -> Result<()> { - info!( + debug!( "Attempting to update Modrinth mod instance {} in profile {} to version '{}' ({})", mod_id, profile_id, new_version_details.name, new_version_details.id ); @@ -1788,7 +1788,7 @@ impl ProfileManager { AppError::ProfileNotFound(profile_id) })?; - info!( + debug!( "Checking required dependencies for new version {}...", new_version_details.id ); @@ -1814,7 +1814,7 @@ impl ProfileManager { ); missing_deps.push((dep_project_id.clone(), dependency.version_id.clone())); } else { - info!( + debug!( "Required dependency project '{}' found in profile.", dep_project_id ); @@ -1852,7 +1852,7 @@ impl ProfileManager { match new_version_details.files.iter().find(|f| f.primary) { Some(primary_file) => { - info!( + debug!( "Updating mod instance {} from version {} to {} using file '{}'", mod_id, mod_to_update.version.as_deref().unwrap_or("?"), @@ -1919,7 +1919,7 @@ impl ProfileManager { let mut failed_deps = 0; for (dep_project_id, dep_version_id_opt) in missing_deps { - info!("Installing missing dependency: {}", dep_project_id); + debug!("Installing missing dependency: {}", dep_project_id); // Get the profile's game version and loader for compatibility check let profile = self.get_profile(profile_id).await?; @@ -2212,7 +2212,7 @@ impl ProfileManager { // Use standard logic for standard versions or profiles without group/shared folder let mods_path = if profile.is_standard_version || !profile.should_use_shared_minecraft_folder() { let path = self.get_profile_mods_path_single(profile)?; - log::info!( + log::debug!( "Calculated standard mods path for profile '{}': {:?}", profile.name, path @@ -2220,7 +2220,7 @@ impl ProfileManager { path } else { let path = self.get_profile_mods_path_shared(profile)?; - log::info!( + log::debug!( "Calculated shared mods path for profile '{}': {:?}", profile.name, path @@ -2334,7 +2334,7 @@ impl ProfileManager { } } - log::info!( + log::debug!( "Found {} relevant custom mod file(s) in {:?}", custom_mods.len(), custom_mods_path @@ -2386,7 +2386,7 @@ impl ProfileManager { // Check if the state is already the desired one if current_enabled == set_enabled { - log::info!( + log::debug!( "Custom mod '{}' is already {}. No action needed.", filename, if set_enabled { "enabled" } else { "disabled" } @@ -2397,7 +2397,7 @@ impl ProfileManager { // Perform the rename if the state needs changing if set_enabled { // --> Enable it: Rename file.disabled to file - log::info!( + log::debug!( "Enabling custom mod: Renaming {:?} to {:?}", disabled_path, enabled_path @@ -2415,7 +2415,7 @@ impl ProfileManager { })?; } else { // --> Disable it: Rename file to file.disabled - log::info!( + log::debug!( "Disabling custom mod: Renaming {:?} to {:?}", enabled_path, disabled_path @@ -2450,7 +2450,7 @@ impl ProfileManager { profile_id: Uuid, paths_enums: Vec, ) -> Result<()> { - info!( + debug!( "Processing {} selected files for import into profile {}", paths_enums.len(), profile_id @@ -2501,7 +2501,7 @@ impl ProfileManager { return Ok(()); } - info!( + debug!( "Attempting to look up {} unique hashes on Modrinth for profile {}...", hashes_to_check.len(), profile_id @@ -2532,7 +2532,7 @@ impl ProfileManager { match versions_map_result { Ok(versions_map) => { - info!( + debug!( "Successfully received results for {} hashes from Modrinth for profile {}.", versions_map.len(), profile_id @@ -2595,7 +2595,7 @@ impl ProfileManager { } } else { // Not found in Modrinth results -> Treat as custom mod - log::info!("Mod {:?} (hash: {}) not found on Modrinth for profile {}. Importing as custom mod.", src_path_buf.file_name().unwrap_or_default(), hash, profile_id); + log::debug!("Mod {:?} (hash: {}) not found on Modrinth for profile {}. Importing as custom mod.", src_path_buf.file_name().unwrap_or_default(), hash, profile_id); path_utils::copy_as_custom_mod( &src_path_buf, &mods_dir, @@ -2638,7 +2638,7 @@ impl ProfileManager { /// that doesn't already have a user copy, and updates existing copies with forced fields. /// Called during launcher startup. pub async fn sync_standard_profiles(&self) -> Result<()> { - info!("ProfileManager: Starting standard profiles synchronization..."); + debug!("ProfileManager: Starting standard profiles synchronization..."); // Ensure profiles are loaded before syncing to avoid race conditions self.ensure_profiles_loaded().await?; @@ -2653,7 +2653,7 @@ impl ProfileManager { }; let standard_profiles = state.norisk_version_manager.get_config().await.profiles; - info!("ProfileManager: Found {} standard profiles to sync", standard_profiles.len()); + debug!("ProfileManager: Found {} standard profiles to sync", standard_profiles.len()); if standard_profiles.is_empty() { info!("ProfileManager: No standard profiles found, skipping sync"); @@ -2679,7 +2679,7 @@ impl ProfileManager { match self.update_copy_with_forced_fields(*existing_copy_id, &standard_profile).await { Ok(updated) => { if updated { - info!("ProfileManager: Updated forced fields for copy {} of standard profile '{}'", existing_copy_id, standard_profile.name); + debug!("ProfileManager: Updated forced fields for copy {} of standard profile '{}'", existing_copy_id, standard_profile.name); copies_updated += 1; } } @@ -2691,7 +2691,7 @@ impl ProfileManager { // Create new copy match self.create_editable_copy_from_standard(&standard_profile).await { Ok(new_id) => { - info!("ProfileManager: Created editable copy {} for standard profile '{}'", new_id, standard_profile.name); + debug!("ProfileManager: Created editable copy {} for standard profile '{}'", new_id, standard_profile.name); copies_created += 1; } Err(e) => { @@ -2711,7 +2711,7 @@ impl ProfileManager { { let profiles_guard = self.profiles.read().await; if profiles_guard.is_empty() { - info!("ProfileManager: Profiles not loaded yet, loading them now..."); + debug!("ProfileManager: Profiles not loaded yet, loading them now..."); drop(profiles_guard); // Release read lock before loading // Load profiles from disk @@ -2722,7 +2722,7 @@ impl ProfileManager { // Save profiles to disk if migrations were performed if migration_count > 0 { - info!("ProfileManager: Saving migrated profiles to disk..."); + debug!("ProfileManager: Saving migrated profiles to disk..."); // Set profiles in memory first let mut profiles_write_guard = self.profiles.write().await; *profiles_write_guard = loaded_profiles; @@ -2772,77 +2772,77 @@ impl ProfileManager { // Force update name if different if copy.name != standard_profile.name { - info!("Updating name for copy {}: '{}' -> '{}'", copy_id, copy.name, standard_profile.name); + debug!("Updating name for copy {}: '{}' -> '{}'", copy_id, copy.name, standard_profile.name); copy.name = standard_profile.name.clone(); changed = true; } // Force update group if different if copy.group != standard_profile.group { - info!("Updating group for copy {}: {:?} -> {:?}", copy_id, copy.group, standard_profile.group); + debug!("Updating group for copy {}: {:?} -> {:?}", copy_id, copy.group, standard_profile.group); copy.group = standard_profile.group.clone(); changed = true; } // Force update game version if different if copy.game_version != standard_profile.game_version { - info!("Updating game version for copy {}: '{}' -> '{}'", copy_id, copy.game_version, standard_profile.game_version); + debug!("Updating game version for copy {}: '{}' -> '{}'", copy_id, copy.game_version, standard_profile.game_version); copy.game_version = standard_profile.game_version.clone(); changed = true; } // Force update loader if different if copy.loader != standard_profile.loader { - info!("Updating loader for copy {}: {:?} -> {:?}", copy_id, copy.loader, standard_profile.loader); + debug!("Updating loader for copy {}: {:?} -> {:?}", copy_id, copy.loader, standard_profile.loader); copy.loader = standard_profile.loader.clone(); changed = true; } // Force update loader version if different if copy.loader_version != standard_profile.loader_version { - info!("Updating loader version for copy {}: {:?} -> {:?}", copy_id, copy.loader_version, standard_profile.loader_version); + debug!("Updating loader version for copy {}: {:?} -> {:?}", copy_id, copy.loader_version, standard_profile.loader_version); copy.loader_version = standard_profile.loader_version.clone(); changed = true; } // Force update description if different if copy.description != standard_profile.description { - info!("Updating description for copy {}", copy_id); + debug!("Updating description for copy {}", copy_id); copy.description = standard_profile.description.clone(); changed = true; } // Force update NoRisk pack selection if different if copy.selected_norisk_pack_id != standard_profile.selected_norisk_pack_id { - info!("Updating NoRisk pack for copy {}: {:?} -> {:?}", copy_id, copy.selected_norisk_pack_id, standard_profile.selected_norisk_pack_id); + debug!("Updating NoRisk pack for copy {}: {:?} -> {:?}", copy_id, copy.selected_norisk_pack_id, standard_profile.selected_norisk_pack_id); copy.selected_norisk_pack_id = standard_profile.selected_norisk_pack_id.clone(); changed = true; } // Force update banner if different if copy.banner != standard_profile.banner { - info!("Updating banner for copy {}", copy_id); + debug!("Updating banner for copy {}", copy_id); copy.banner = standard_profile.banner.clone(); changed = true; } // Force update banner if different if copy.background != standard_profile.background { - info!("Updating background for copy {}", copy_id); + debug!("Updating background for copy {}", copy_id); copy.background = standard_profile.background.clone(); changed = true; } // Force update is_standard_version if different if copy.is_standard_version != standard_profile.is_standard_version { - info!("Updating is_standard_version for copy {}: {} -> {}", copy_id, copy.is_standard_version, standard_profile.is_standard_version); + debug!("Updating is_standard_version for copy {}: {} -> {}", copy_id, copy.is_standard_version, standard_profile.is_standard_version); copy.is_standard_version = standard_profile.is_standard_version; changed = true; } // Force update path if different if copy.path != standard_profile.path { - info!("Updating path for copy {}: '{}' -> '{}'", copy_id, copy.path, standard_profile.path); + debug!("Updating path for copy {}: '{}' -> '{}'", copy_id, copy.path, standard_profile.path); copy.path = standard_profile.path.clone(); changed = true; } @@ -2866,7 +2866,7 @@ impl ProfileManager { profile_id: Uuid, payload: &crate::commands::content_command::SwitchContentVersionPayload, ) -> Result<()> { - info!( + debug!( "Updating mod in profile {} using unified version switch", profile_id ); @@ -2975,7 +2975,7 @@ impl ProfileManager { // Install missing dependencies if any if !payload.new_version_details.dependencies.is_empty() { - info!("Processing {} dependencies for updated mod", payload.new_version_details.dependencies.len()); + debug!("Processing {} dependencies for updated mod", payload.new_version_details.dependencies.len()); if let Err(e) = self.install_missing_dependencies( profile_id, &payload.new_version_details.dependencies, @@ -3029,11 +3029,11 @@ impl ProfileManager { if let Some(dep_project_id) = &dependency.project_id { // Check if dependency is already installed if self.is_dependency_installed(&profile, dep_project_id) { - info!("Dependency {} already installed, skipping", dep_project_id); + debug!("Dependency {} already installed, skipping", dep_project_id); continue; } - info!("Installing missing dependency: {}", dep_project_id); + debug!("Installing missing dependency: {}", dep_project_id); // Get the dependency version details let versions_params = UnifiedModVersionsParams { @@ -3090,7 +3090,7 @@ impl ProfileManager { /// Deletes a custom mod file (either .jar or .jar.disabled) from the profile's custom_mods directory. pub async fn delete_custom_mod_file(&self, profile_id: Uuid, filename: &str) -> Result<()> { - info!( + debug!( "Attempting to delete custom mod file '{}' for profile {}", filename, profile_id ); @@ -3144,9 +3144,9 @@ impl ProfileManager { #[async_trait] impl PostInitializationHandler for ProfileManager { async fn on_state_ready(&self, _app_handle: Arc) -> Result<()> { - info!("ProfileManager: on_state_ready called. Loading profiles..."); + debug!("ProfileManager: on_state_ready called. Loading profiles..."); // PRIORITY 0: Create backup BEFORE ANYTHING else (including loading) - info!("ProfileManager: Creating pre-load backup of profiles.json..."); + debug!("ProfileManager: Creating pre-load backup of profiles.json..."); if self.profiles_path.exists() { match backup_utils::create_backup(&self.profiles_path, Some("profiles"), &self.backup_config).await { Ok(backup_path) => { @@ -3158,7 +3158,7 @@ impl PostInitializationHandler for ProfileManager { } } } else { - info!("ProfileManager: profiles.json doesn't exist yet - no backup needed at this stage"); + debug!("ProfileManager: profiles.json doesn't exist yet - no backup needed at this stage"); } // Load profiles with migrations (backup was already created above) diff --git a/src-tauri/src/state/skin_state.rs b/src-tauri/src/state/skin_state.rs index 488f192a..5955521a 100644 --- a/src-tauri/src/state/skin_state.rs +++ b/src-tauri/src/state/skin_state.rs @@ -52,7 +52,7 @@ pub struct SkinManager { impl SkinManager { /// Create a new skin manager pub fn new(skins_path: PathBuf) -> Result { - info!( + debug!( "SkinManager: Initializing with path: {:?} (skins loading deferred)", skins_path ); @@ -72,12 +72,12 @@ impl SkinManager { return Ok(()); } - info!("Loading skins database from: {:?}", self.skins_path); + debug!("Loading skins database from: {:?}", self.skins_path); let skins_data = fs::read_to_string(&self.skins_path).await?; match serde_json::from_str::(&skins_data) { Ok(loaded_skins) => { - info!( + debug!( "Successfully loaded skins database with {} skins", loaded_skins.skins.len() ); @@ -106,7 +106,7 @@ impl SkinManager { if let Some(parent_dir) = self.skins_path.parent() { if !parent_dir.exists() { fs::create_dir_all(parent_dir).await?; - info!( + debug!( "Created directory for skins database file: {:?}", parent_dir ); @@ -156,7 +156,7 @@ impl SkinManager { if let Some(index) = skins.skins.iter().position(|s| s.id == skin.id) { // Replace the existing skin skins.skins[index] = skin; - info!("Updated existing skin with ID: {}", skins.skins[index].id); + debug!("Updated existing skin with ID: {}", skins.skins[index].id); } else { // Add the new skin skins.skins.push(skin); @@ -180,7 +180,7 @@ impl SkinManager { let removed = skins.skins.len() < initial_len; if removed { - info!("Removed skin with ID: {}", id); + debug!("Removed skin with ID: {}", id); // Save the updated database drop(skins); // Release the write lock before saving self.save_skins().await?; @@ -227,9 +227,9 @@ impl SkinManager { #[async_trait] impl PostInitializationHandler for SkinManager { async fn on_state_ready(&self, _app_handle: Arc) -> Result<()> { - info!("SkinManager: on_state_ready called. Loading skins..."); + debug!("SkinManager: on_state_ready called. Loading skins..."); self.load_skins_internal().await?; - info!("SkinManager: Successfully loaded skins in on_state_ready."); + debug!("SkinManager: Successfully loaded skins in on_state_ready."); Ok(()) } } diff --git a/src-tauri/src/state/state_manager.rs b/src-tauri/src/state/state_manager.rs index 013f583b..f7999b48 100644 --- a/src-tauri/src/state/state_manager.rs +++ b/src-tauri/src/state/state_manager.rs @@ -40,7 +40,7 @@ impl State { pub async fn init(app: Arc) -> Result<()> { let initial_state_arc = LAUNCHER_STATE .get_or_try_init(|| async { - log::info!("State::init - Starting primary initialization of managers (Phase 1 - Lightweight Instantiation)..."); + log::debug!("State::init - Starting primary initialization of managers (Phase 1 - Lightweight Instantiation)..."); let config_manager = ConfigManager::new()?; let discord_manager = DiscordManager::new(false).await?; let io_semaphore = Arc::new(Semaphore::new(10)); @@ -52,7 +52,7 @@ impl State { let profile_manager = ProfileManager::new(LAUNCHER_DIRECTORY.root_dir().join("profiles.json"))?; let process_manager = ProcessManager::new(default_processes_path(), app.clone()).await?; - log::info!("State::init - Primary initialization of managers complete (Phase 1). Constructing State struct with initialized: false."); + log::debug!("State::init - Primary initialization of managers complete (Phase 1). Constructing State struct with initialized: false."); let friends_state = FriendsState::new(); Ok::, AppError>(Arc::new(Self { initialized: true, @@ -72,13 +72,13 @@ impl State { }) .await?; - log::info!("State::init - Global state Arc created. Running post-initialization handlers (Phase 2)..."); + log::debug!("State::init - Global state Arc created. Running post-initialization handlers (Phase 2)..."); initial_state_arc .config_manager .on_state_ready(app.clone()) .await?; - log::info!("State::init - ConfigManager post-initialization complete."); + log::debug!("State::init - ConfigManager post-initialization complete."); let loaded_config = initial_state_arc.config_manager.get_config().await; @@ -94,7 +94,7 @@ impl State { .discord_manager .set_enabled(loaded_config.enable_discord_presence) .await?; - log::info!( + log::debug!( "State::init - DiscordManager enabled status set based on loaded config: {}", loaded_config.enable_discord_presence ); @@ -103,31 +103,31 @@ impl State { .norisk_version_manager .on_state_ready(app.clone()) .await?; - log::info!("State::init - NoriskVersionManager post-initialization complete."); + log::debug!("State::init - NoriskVersionManager post-initialization complete."); initial_state_arc .norisk_pack_manager .on_state_ready(app.clone()) .await?; - log::info!("State::init - NoriskPackManager post-initialization complete."); + log::debug!("State::init - NoriskPackManager post-initialization complete."); initial_state_arc .profile_manager .on_state_ready(app.clone()) .await?; - log::info!("State::init - ProfileManager post-initialization complete."); + log::debug!("State::init - ProfileManager post-initialization complete."); initial_state_arc .process_manager .on_state_ready(app.clone()) .await?; - log::info!("State::init - ProcessManager post-initialization complete."); + log::debug!("State::init - ProcessManager post-initialization complete."); initial_state_arc .skin_manager .on_state_ready(app.clone()) .await?; - log::info!("State::init - SkinManager post-initialization complete."); + log::debug!("State::init - SkinManager post-initialization complete."); initial_state_arc .norisk_pack_manager @@ -139,11 +139,11 @@ impl State { .await; let final_config = initial_state_arc.config_manager.get_config().await; - tracing::info!( + tracing::debug!( "Launcher Config - Experimental mode: {}", final_config.is_experimental ); - tracing::info!( + tracing::debug!( "Launcher Config - Discord Rich Presence: {}", final_config.enable_discord_presence ); @@ -154,7 +154,7 @@ impl State { // Don't fail initialization, just log the error } - log::info!( + log::debug!( "State::init - Full initialization, including all post-init handlers, complete." ); @@ -164,7 +164,7 @@ impl State { // Get the current state instance pub async fn get() -> Result> { if !LAUNCHER_STATE.initialized() { - log::error!("Attempted to get state before initialization. Waiting..."); + log::debug!("Attempted to get state before initialization. Waiting..."); let mut wait_count = 0; while !LAUNCHER_STATE.initialized() { tokio::time::sleep(std::time::Duration::from_millis(100)).await; @@ -174,7 +174,7 @@ impl State { log::warn!("Still waiting for state initialization in State::get() after {} attempts...", wait_count); } } - log::info!( + log::debug!( "State has been initialized after {} attempts. Proceeding in State::get().", wait_count ); diff --git a/src-tauri/src/utils/debug_utils.rs b/src-tauri/src/utils/debug_utils.rs index 5d08f364..774300f5 100644 --- a/src-tauri/src/utils/debug_utils.rs +++ b/src-tauri/src/utils/debug_utils.rs @@ -1,18 +1,18 @@ use crate::integrations::unified_mod::{search_mods_unified, get_mod_versions_unified, ModPlatform, UnifiedProjectType, UnifiedSortType, UnifiedModSearchParams, UnifiedModVersionsParams, UnifiedVersionResponse}; use crate::state::state_manager::State; use crate::utils::mc_utils; -use log::{error, info}; +use log::{error, info, debug}; /// Debug function to list all worlds for all user profiles. /// This should only be called temporarily during development. pub async fn debug_print_all_profile_worlds() { - info!("--- [DEBUG] Starting World Check --- KAPPA"); + debug!("--- [DEBUG] Starting World Check --- KAPPA"); match State::get().await { Ok(state) => { match state.profile_manager.list_profiles().await { Ok(profiles) => { if profiles.is_empty() { - info!("--- [DEBUG] No profiles found."); + debug!("--- [DEBUG] No profiles found."); } else { info!( "--- [DEBUG] Checking worlds for {} profile(s)...", @@ -35,9 +35,9 @@ pub async fn debug_print_all_profile_worlds() { match mc_utils::get_profile_worlds(profile.id).await { Ok(worlds) => { if worlds.is_empty() { - info!(" No valid worlds found in saves directory."); + debug!(" No valid worlds found in saves directory."); } else { - info!(" Found Worlds:"); + debug!(" Found Worlds:"); for world in worlds { // Konvertiere Timestamp zu lesbarem Datum (optional, benötigt chrono crate) let last_played_str = world @@ -54,7 +54,7 @@ pub async fn debug_print_all_profile_worlds() { }) .unwrap_or_else(|| "N/A".to_string()); - info!(" - Folder: {}", world.folder_name); + debug!(" - Folder: {}", world.folder_name); info!( " Display Name: {}", world.display_name.as_deref().unwrap_or("N/A") @@ -63,7 +63,7 @@ pub async fn debug_print_all_profile_worlds() { " Last Played: {} ({:?})", last_played_str, world.last_played ); - info!(" Icon Path: {:?}", world.icon_path); + debug!(" Icon Path: {:?}", world.icon_path); } } } @@ -75,7 +75,7 @@ pub async fn debug_print_all_profile_worlds() { } } } - info!("--- [DEBUG] Finished World Check --- KAPPA"); + debug!("--- [DEBUG] Finished World Check --- KAPPA"); } } Err(e) => { @@ -92,13 +92,13 @@ pub async fn debug_print_all_profile_worlds() { /// Debug function to list all servers for all user profiles. /// This should only be called temporarily during development. pub async fn debug_print_all_profile_servers() { - info!("--- [DEBUG] Starting Server Check ---"); + debug!("--- [DEBUG] Starting Server Check ---"); match State::get().await { Ok(state) => { match state.profile_manager.list_profiles().await { Ok(profiles) => { if profiles.is_empty() { - info!("--- [DEBUG] No profiles found."); + debug!("--- [DEBUG] No profiles found."); } else { info!( "--- [DEBUG] Checking servers for {} profile(s)...", @@ -125,7 +125,7 @@ pub async fn debug_print_all_profile_servers() { " No servers found (servers.dat missing or empty)." ); } else { - info!(" Found Servers:"); + debug!(" Found Servers:"); for server in servers { info!( " - Name: {}", @@ -158,7 +158,7 @@ pub async fn debug_print_all_profile_servers() { } } } - info!("--- [DEBUG] Finished Server Check ---"); + debug!("--- [DEBUG] Finished Server Check ---"); } } Err(e) => { @@ -176,12 +176,12 @@ pub async fn debug_print_all_profile_servers() { /// This should only be called temporarily during development. pub async fn debug_print_news_and_changelogs() { use crate::minecraft::api::wordpress_api::WordPressApi; - info!("--- [DEBUG] Starting News/Changelog Check ---"); + debug!("--- [DEBUG] Starting News/Changelog Check ---"); match WordPressApi::get_news_and_changelogs().await { Ok(posts) => { if posts.is_empty() { - info!("--- [DEBUG] No news or changelog posts found."); + debug!("--- [DEBUG] No news or changelog posts found."); } else { info!( "--- [DEBUG] Fetched {} news/changelog post(s):", @@ -198,14 +198,14 @@ pub async fn debug_print_news_and_changelogs() { .map(|s| s.as_str()) .unwrap_or("N/A"); - //info!(" - Title: {}", title); - info!(" Date: {}", date); - info!(" OG Image: {}", og_image_url); + //debug!(" - Title: {}", title); + debug!(" Date: {}", date); + debug!(" OG Image: {}", og_image_url); // Optionally print more details like excerpt or link - // info!(" Excerpt: {}", post.excerpt.rendered); - // info!(" Link: {}", post.link); + // debug!(" Excerpt: {}", post.excerpt.rendered); + // debug!(" Link: {}", post.link); } - info!("--- [DEBUG] Finished News/Changelog Check ---"); + debug!("--- [DEBUG] Finished News/Changelog Check ---"); } } Err(e) => { @@ -217,7 +217,7 @@ pub async fn debug_print_news_and_changelogs() { /// Debug function to test unified mod search for both Modrinth and CurseForge. /// This should only be called temporarily during development. pub async fn debug_unified_mod_search() { - info!("--- [DEBUG] Starting Unified Mod Search Test ---"); + debug!("--- [DEBUG] Starting Unified Mod Search Test ---"); // Base parameters for testing let base_params = UnifiedModSearchParams { @@ -235,15 +235,15 @@ pub async fn debug_unified_mod_search() { }; // Test Modrinth search - info!("--- [DEBUG] Testing Modrinth search ---"); + debug!("--- [DEBUG] Testing Modrinth search ---"); let mut modrinth_params = base_params.clone(); modrinth_params.source = ModPlatform::Modrinth; match search_mods_unified(modrinth_params).await { Ok(response) => { - info!("Modrinth search successful: {} results", response.results.len()); + debug!("Modrinth search successful: {} results", response.results.len()); for result in &response.results { - info!(" - {} ({:?}) - {} downloads", result.title, result.source, result.downloads); + debug!(" - {} ({:?}) - {} downloads", result.title, result.source, result.downloads); } } Err(e) => { @@ -252,15 +252,15 @@ pub async fn debug_unified_mod_search() { } // Test CurseForge search - info!("--- [DEBUG] Testing CurseForge search ---"); + debug!("--- [DEBUG] Testing CurseForge search ---"); let mut curseforge_params = base_params.clone(); curseforge_params.source = ModPlatform::CurseForge; match search_mods_unified(curseforge_params).await { Ok(response) => { - info!("CurseForge search successful: {} results", response.results.len()); + debug!("CurseForge search successful: {} results", response.results.len()); for result in &response.results { - info!(" - {} ({:?}) - {} downloads", result.title, result.source, result.downloads); + debug!(" - {} ({:?}) - {} downloads", result.title, result.source, result.downloads); } } Err(e) => { @@ -268,14 +268,14 @@ pub async fn debug_unified_mod_search() { } } - info!("--- [DEBUG] Finished Unified Mod Search Test ---"); + debug!("--- [DEBUG] Finished Unified Mod Search Test ---"); } /// Debug function to test unified mod versions retrieval. /// Fetches the first mod from each platform and prints its available versions. /// This should only be called temporarily during development. pub async fn debug_unified_mod_versions() { - info!("--- [DEBUG] Starting Unified Mod Versions Test ---"); + debug!("--- [DEBUG] Starting Unified Mod Versions Test ---"); // Base parameters for testing let base_params = UnifiedModSearchParams { @@ -293,14 +293,14 @@ pub async fn debug_unified_mod_versions() { }; // Test Modrinth versions - info!("--- [DEBUG] Testing Modrinth versions ---"); + debug!("--- [DEBUG] Testing Modrinth versions ---"); let mut modrinth_params = base_params.clone(); modrinth_params.source = ModPlatform::Modrinth; match search_mods_unified(modrinth_params).await { Ok(search_response) => { if let Some(first_mod) = search_response.results.first() { - info!("Found Modrinth mod: {} (ID: {})", first_mod.title, first_mod.project_id); + debug!("Found Modrinth mod: {} (ID: {})", first_mod.title, first_mod.project_id); let modrinth_version_params = UnifiedModVersionsParams { source: ModPlatform::Modrinth, @@ -313,22 +313,22 @@ pub async fn debug_unified_mod_versions() { match get_mod_versions_unified(modrinth_version_params).await { Ok(version_response) => { - info!("Found {} versions for {}", version_response.versions.len(), first_mod.title); + debug!("Found {} versions for {}", version_response.versions.len(), first_mod.title); for version in &version_response.versions { - info!(" - Version: {} ({} downloads, {})", + debug!(" - Version: {} ({} downloads, {})", version.version_number, version.downloads, version.date_published ); if let Some(changelog) = &version.changelog { let preview: String = changelog.chars().take(50).collect(); - info!(" Changelog preview: {}...", preview); + debug!(" Changelog preview: {}...", preview); } - info!(" Files: {}", version.files.len()); + debug!(" Files: {}", version.files.len()); for file in &version.files { - info!(" - {} ({} bytes)", file.filename, file.size); + debug!(" - {} ({} bytes)", file.filename, file.size); if !file.hashes.is_empty() { - info!(" Hashes: {:?}", file.hashes.keys().collect::>()); + debug!(" Hashes: {:?}", file.hashes.keys().collect::>()); } } } @@ -338,7 +338,7 @@ pub async fn debug_unified_mod_versions() { } } } else { - info!("No Modrinth mods found"); + debug!("No Modrinth mods found"); } } Err(e) => { @@ -347,14 +347,14 @@ pub async fn debug_unified_mod_versions() { } // Test CurseForge versions - info!("--- [DEBUG] Testing CurseForge versions ---"); + debug!("--- [DEBUG] Testing CurseForge versions ---"); let mut curseforge_params = base_params.clone(); curseforge_params.source = ModPlatform::CurseForge; match search_mods_unified(curseforge_params).await { Ok(search_response) => { if let Some(first_mod) = search_response.results.first() { - info!("Found CurseForge mod: {} (ID: {})", first_mod.title, first_mod.project_id); + debug!("Found CurseForge mod: {} (ID: {})", first_mod.title, first_mod.project_id); let curseforge_version_params = UnifiedModVersionsParams { source: ModPlatform::CurseForge, @@ -367,18 +367,18 @@ pub async fn debug_unified_mod_versions() { match get_mod_versions_unified(curseforge_version_params).await { Ok(version_response) => { - info!("Found {} versions for {}", version_response.versions.len(), first_mod.title); + debug!("Found {} versions for {}", version_response.versions.len(), first_mod.title); for version in &version_response.versions { - info!(" - Version: {} ({} downloads, {})", + debug!(" - Version: {} ({} downloads, {})", version.version_number, version.downloads, version.date_published ); - info!(" Files: {}", version.files.len()); + debug!(" Files: {}", version.files.len()); for file in &version.files { - info!(" - {} ({} bytes)", file.filename, file.size); + debug!(" - {} ({} bytes)", file.filename, file.size); if !file.hashes.is_empty() { - info!(" Hashes: {:?}", file.hashes.keys().collect::>()); + debug!(" Hashes: {:?}", file.hashes.keys().collect::>()); } } } @@ -388,7 +388,7 @@ pub async fn debug_unified_mod_versions() { } } } else { - info!("No CurseForge mods found"); + debug!("No CurseForge mods found"); } } Err(e) => { @@ -396,5 +396,5 @@ pub async fn debug_unified_mod_versions() { } } - info!("--- [DEBUG] Finished Unified Mod Versions Test ---"); + debug!("--- [DEBUG] Finished Unified Mod Versions Test ---"); } diff --git a/src-tauri/src/utils/updater_utils.rs b/src-tauri/src/utils/updater_utils.rs index f8991414..9c993758 100644 --- a/src-tauri/src/utils/updater_utils.rs +++ b/src-tauri/src/utils/updater_utils.rs @@ -1,5 +1,5 @@ use crate::error::{AppError, Result as AppResult}; -use log::{error, info, warn}; +use log::{error, info, warn, debug}; use serde::Serialize; use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindow, WebviewWindowBuilder}; use tauri_plugin_updater::UpdaterExt; @@ -205,7 +205,7 @@ pub fn emit_status( /// /// * `Result` - The created Tauri webview window instance or an error. pub async fn create_updater_window(app_handle: &AppHandle) -> tauri::Result { - info!("Creating updater window..."); + debug!("Creating updater window..."); let window = WebviewWindowBuilder::new( app_handle, "updater", // Unique label @@ -221,7 +221,7 @@ pub async fn create_updater_window(app_handle: &AppHandle) -> tauri::Result { + const handleDebugDumpHotkey = async (event: KeyboardEvent) => { + const isCtrlOrCmdPressed = event.ctrlKey || event.metaKey; + const isShiftPressed = event.shiftKey; + const isD = event.key.toLowerCase() === "d"; + + if (!isCtrlOrCmdPressed || !isShiftPressed || !isD || event.repeat) { + return; + } + + const target = event.target as HTMLElement | null; + const isTypingTarget = + target?.tagName === "INPUT" || + target?.tagName === "TEXTAREA" || + target?.isContentEditable; + if (isTypingTarget) { + return; + } + + event.preventDefault(); + try { + const path = await dumpDebugLogs("hotkey_ctrl_shift_d"); + toast.success(`Debug dump saved: ${path}`); + } catch (error) { + console.error("[App.tsx] Failed to dump debug logs via hotkey:", error); + toast.error(t("app.errors.unexpected")); + } + }; + + window.addEventListener("keydown", handleDebugDumpHotkey); + return () => { + window.removeEventListener("keydown", handleDebugDumpHotkey); + }; + }, [t]); + // Fetch notifications when user is logged in useEffect(() => { if (activeAccount) { diff --git a/src/components/profiles/ProfileCardV2.tsx b/src/components/profiles/ProfileCardV2.tsx index bd13af72..4ca36ac1 100644 --- a/src/components/profiles/ProfileCardV2.tsx +++ b/src/components/profiles/ProfileCardV2.tsx @@ -1,14 +1,20 @@ "use client"; import type React from "react"; -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import { useNavigate } from "react-router-dom"; import type { Profile, ResolvedLoaderVersion } from "../../types/profile"; import { ProfileIconV2 } from "./ProfileIconV2"; import { toast } from "react-hot-toast"; -import { ProfileActionButtons, type ProfileActionButton } from "../ui/ProfileActionButtons"; -import { SettingsContextMenu, type ContextMenuItem } from "../ui/SettingsContextMenu"; +import { + ProfileActionButtons, + type ProfileActionButton, +} from "../ui/ProfileActionButtons"; +import { + SettingsContextMenu, + type ContextMenuItem, +} from "../ui/SettingsContextMenu"; import { Icon } from "@iconify/react"; import { useProfileSettingsStore } from "../../store/profile-settings-store"; import { useProfileDuplicateStore } from "../../store/profile-duplicate-store"; @@ -32,13 +38,19 @@ function StandardVersionTooltipContent() {
- {t('profiles.card.standardVersionInfo')} + {t("profiles.card.standardVersionInfo")}
- +
- {t('profiles.card.tip')}: {t('profiles.card.createOwnProfiles')} + + {t("profiles.card.tip")}: + {" "} + {t("profiles.card.createOwnProfiles")}
@@ -72,11 +84,14 @@ export function ProfileCardV2({ const [modsButtonHovered, setModsButtonHovered] = useState(false); const accentColor = useThemeStore((state) => state.accentColor); const { openContextMenuId, setOpenContextMenuId } = useThemeStore(); - + // Settings context menu state const [isContextMenuOpen, setIsContextMenuOpen] = useState(false); const contextMenuId = `profile-${profile.id}`; - const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); + const [contextMenuPosition, setContextMenuPosition] = useState({ + x: 0, + y: 0, + }); const settingsButtonRef = useRef(null); const { isPinned, togglePin } = usePinnedProfilesStore(); @@ -85,10 +100,10 @@ export function ProfileCardV2({ // Modpack versions state for conditional rendering const [modpackVersions, setModpackVersions] = useState(null); const [isLoadingVersions, setIsLoadingVersions] = useState(false); - + // Profile settings store const { openModal } = useProfileSettingsStore(); - + // Profile duplicate store const { openModal: openDuplicateModal } = useProfileDuplicateStore(); @@ -96,14 +111,15 @@ export function ProfileCardV2({ const { showModal, hideModal } = useGlobalModal(); // Resolved loader version state - const [resolvedLoaderVersion, setResolvedLoaderVersion] = useState(null); + const [resolvedLoaderVersion, setResolvedLoaderVersion] = + useState(null); // Get accounts from Minecraft Auth Store const accounts = useMinecraftAuthStore((state) => state.accounts); - + // Find preferred account if one is set - const preferredAccount = profile.preferred_account_id - ? accounts.find(acc => acc.id === profile.preferred_account_id) + const preferredAccount = profile.preferred_account_id + ? accounts.find((acc) => acc.id === profile.preferred_account_id) : null; // Load preferred account avatar @@ -111,6 +127,107 @@ export function ProfileCardV2({ uuid: preferredAccount?.id, overlay: true, }); + const estimatedContextMenuHeight = + profile.modpack_info?.source && modpackVersions ? 289 : 245; + + const calculateContextMenuPosition = useCallback( + ( + anchorClientX: number, + anchorClientY: number, + alignRight: boolean = false, + ) => { + const menuWidth = 200; + const maxMenuHeight = estimatedContextMenuHeight; + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + const spacing = 8; + + const spaceBelow = viewportHeight - anchorClientY - spacing; + const spaceAbove = anchorClientY - spacing; + const openDownward = + spaceBelow >= maxMenuHeight || spaceBelow >= spaceAbove; + const menuHeight = Math.max( + 0, + Math.min(maxMenuHeight, openDownward ? spaceBelow : spaceAbove), + ); + + let menuTop = openDownward + ? anchorClientY + 2 + : anchorClientY - menuHeight - 2; + menuTop = Math.max( + spacing, + Math.min(menuTop, viewportHeight - menuHeight - spacing), + ); + + let menuLeft = alignRight ? anchorClientX - menuWidth : anchorClientX; + menuLeft = Math.max( + spacing, + Math.min(menuLeft, viewportWidth - menuWidth - spacing), + ); + + return { + x: menuLeft, + y: menuTop, + }; + }, + [estimatedContextMenuHeight], + ); + + const openContextMenuAtCursor = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (openContextMenuId && openContextMenuId !== contextMenuId) { + setOpenContextMenuId(null); + } + + setContextMenuPosition( + calculateContextMenuPosition(e.clientX, e.clientY), + ); + setIsContextMenuOpen(true); + setOpenContextMenuId(contextMenuId); + }, + [ + calculateContextMenuPosition, + contextMenuId, + openContextMenuId, + setOpenContextMenuId, + ], + ); + + const toggleContextMenuFromButton = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (openContextMenuId && openContextMenuId !== contextMenuId) { + setOpenContextMenuId(null); + } + + const newState = !isContextMenuOpen; + setIsContextMenuOpen(newState); + setOpenContextMenuId(newState ? contextMenuId : null); + + if (newState) { + const buttonRect = e.currentTarget.getBoundingClientRect(); + setContextMenuPosition( + calculateContextMenuPosition( + buttonRect.right, + buttonRect.bottom, + true, + ), + ); + } + }, + [ + calculateContextMenuPosition, + contextMenuId, + isContextMenuOpen, + openContextMenuId, + setOpenContextMenuId, + ], + ); // Settings context menu items const contextMenuItems: ContextMenuItem[] = [ @@ -124,7 +241,7 @@ export function ProfileCardV2({ }, { id: "edit", - label: t('profiles.editProfile'), + label: t("profiles.editProfile"), icon: "solar:settings-bold", onClick: (profile) => { console.log("Edit Profile clicked for:", profile.name); @@ -133,7 +250,7 @@ export function ProfileCardV2({ }, { id: "duplicate", - label: t('profiles.duplicate'), + label: t("profiles.duplicate"), icon: "solar:copy-bold", onClick: (profile) => { console.log("Duplicate Profile clicked for:", profile.name); @@ -142,67 +259,86 @@ export function ProfileCardV2({ }, { id: "export", - label: t('profiles.export'), + label: t("profiles.export"), icon: "solar:download-bold", onClick: (profile) => { - showModal(`export-profile-${profile.id}`, ( + showModal( + `export-profile-${profile.id}`, hideModal(`export-profile-${profile.id}`)} - /> - )); + />, + ); }, }, { id: "open-folder", - label: t('profiles.openFolder'), + label: t("profiles.openFolder"), icon: "solar:folder-bold", onClick: (profile) => { if (onOpenFolder) { onOpenFolder(profile); } else { - toast.success(t('profiles.toast.opening_folder', { name: profile.name })); + toast.success( + t("profiles.toast.opening_folder", { name: profile.name }), + ); console.log("Opening folder for profile:", profile.name); } }, }, // Show modpack versions only if modpack info exists and versions are loaded - ...(profile.modpack_info?.source && modpackVersions ? [{ - id: "switch_modpack", - label: t('profiles.modpackVersions'), - icon: "solar:refresh-circle-bold", - onClick: (profile) => { - console.log("Switch modpack version for profile:", profile.name); - if (profile.modpack_info?.source) { - // Import ModpackVersionsModal dynamically to avoid circular imports - import("../modals/ModpackVersionsModal").then(({ ModpackVersionsModal }) => { - showModal(`modpack-versions-${profile.id}`, ( - hideModal(`modpack-versions-${profile.id}`)} - versions={modpackVersions} - modpackName={profile.name} - profileId={profile.id} - onSwitchComplete={async () => { - console.log("Modpack version switched successfully for:", profile.name); - // Refresh profiles to ensure the profile prop is updated - try { - const { fetchProfiles } = useProfileStore.getState(); - await fetchProfiles(); - } catch (err) { - console.error("Failed to refresh profiles after modpack switch:", err); - } - }} - /> - )); - }); - } - }, - }] : []), + ...(profile.modpack_info?.source && modpackVersions + ? [ + { + id: "switch_modpack", + label: t("profiles.modpackVersions"), + icon: "solar:refresh-circle-bold", + onClick: (profile) => { + console.log("Switch modpack version for profile:", profile.name); + if (profile.modpack_info?.source) { + // Import ModpackVersionsModal dynamically to avoid circular imports + import("../modals/ModpackVersionsModal").then( + ({ ModpackVersionsModal }) => { + showModal( + `modpack-versions-${profile.id}`, + + hideModal(`modpack-versions-${profile.id}`) + } + versions={modpackVersions} + modpackName={profile.name} + profileId={profile.id} + onSwitchComplete={async () => { + console.log( + "Modpack version switched successfully for:", + profile.name, + ); + // Refresh profiles to ensure the profile prop is updated + try { + const { fetchProfiles } = + useProfileStore.getState(); + await fetchProfiles(); + } catch (err) { + console.error( + "Failed to refresh profiles after modpack switch:", + err, + ); + } + }} + />, + ); + }, + ); + } + }, + }, + ] + : []), { id: "delete", - label: t('profiles.delete'), + label: t("profiles.delete"), icon: "solar:trash-bin-trash-bold", destructive: true, separator: true, // Trennstrich vor Delete @@ -210,7 +346,9 @@ export function ProfileCardV2({ if (onDelete) { onDelete(profile.id, profile.name); } else { - toast.error(t('profiles.toast.delete_fallback', { name: profile.name })); + toast.error( + t("profiles.toast.delete_fallback", { name: profile.name }), + ); console.log("Deleting profile:", profile.name); } }, @@ -229,11 +367,13 @@ export function ProfileCardV2({ skipLastPlayedUpdate: variant === "3d", // Skip for featured profiles in 3D mode }); - - // Close this menu if another context menu opens globally useEffect(() => { - if (openContextMenuId && openContextMenuId !== contextMenuId && isContextMenuOpen) { + if ( + openContextMenuId && + openContextMenuId !== contextMenuId && + isContextMenuOpen + ) { setIsContextMenuOpen(false); } }, [openContextMenuId, contextMenuId, isContextMenuOpen]); @@ -244,7 +384,7 @@ export function ProfileCardV2({ setIsLoadingVersions(true); UnifiedService.getModpackVersions(profile.modpack_info.source) .then(setModpackVersions) - .catch(err => { + .catch((err) => { console.error("Failed to load modpack versions:", err); setModpackVersions(null); }) @@ -254,10 +394,6 @@ export function ProfileCardV2({ } }, [profile.modpack_info?.source]); - - - - // Fetch resolved loader version useEffect(() => { async function fetchResolvedLoaderVersion() { @@ -276,11 +412,12 @@ export function ProfileCardV2({ } fetchResolvedLoaderVersion(); - }, [profile.id, profile.game_version, profile.loader, profile.loader_version]); - - - - + }, [ + profile.id, + profile.game_version, + profile.loader, + profile.loader_version, + ]); // Get mod loader icon - reused from ProfileCard.tsx const getModLoaderIcon = () => { @@ -366,7 +503,7 @@ export function ProfileCardV2({ // Format last played date const formatLastPlayed = (lastPlayed: string | null): string => { - if (!lastPlayed) return t('profiles.card.neverPlayed'); + if (!lastPlayed) return t("profiles.card.neverPlayed"); const date = new Date(lastPlayed); const now = new Date(); @@ -378,26 +515,33 @@ export function ProfileCardV2({ const diffInMonths = Math.floor(diffInDays / 30); const diffInYears = Math.floor(diffInDays / 365); - if (diffInMinutes < 1) return t('profiles.card.justNow'); - if (diffInMinutes < 60) return t('profiles.card.minutesAgo', { count: diffInMinutes }); - if (diffInHours < 24) return t('profiles.card.hoursAgo', { count: diffInHours }); - if (diffInDays < 7) return t('profiles.card.daysAgo', { count: diffInDays }); - if (diffInWeeks < 4) return t('profiles.card.weeksAgo', { count: diffInWeeks }); - if (diffInMonths < 12) return t('profiles.card.monthsAgo', { count: diffInMonths }); - - return t('profiles.card.yearsAgo', { count: diffInYears }); + if (diffInMinutes < 1) return t("profiles.card.justNow"); + if (diffInMinutes < 60) + return t("profiles.card.minutesAgo", { count: diffInMinutes }); + if (diffInHours < 24) + return t("profiles.card.hoursAgo", { count: diffInHours }); + if (diffInDays < 7) + return t("profiles.card.daysAgo", { count: diffInDays }); + if (diffInWeeks < 4) + return t("profiles.card.weeksAgo", { count: diffInWeeks }); + if (diffInMonths < 12) + return t("profiles.card.monthsAgo", { count: diffInMonths }); + + return t("profiles.card.yearsAgo", { count: diffInYears }); }; - - // Action button configuration const actionButtons: ProfileActionButton[] = [ { id: "play", - label: isLaunching ? t('profiles.stop').toUpperCase() : t('profiles.play').toUpperCase(), + label: isLaunching + ? t("profiles.stop").toUpperCase() + : t("profiles.play").toUpperCase(), icon: isLaunching ? "solar:stop-bold" : "solar:play-bold", variant: isLaunching ? "destructive" : "primary", - tooltip: isLaunching ? t('profiles.stopPlaying') : t('profiles.startPlaying'), + tooltip: isLaunching + ? t("profiles.stopPlaying") + : t("profiles.startPlaying"), onClick: (profile, e) => { if (onPlay) { onPlay(profile); @@ -411,12 +555,14 @@ export function ProfileCardV2({ label: "MODS", icon: "solar:box-bold", variant: "secondary", - tooltip: t('profiles.manageMods'), + tooltip: t("profiles.manageMods"), onClick: (profile, e) => { if (onMods) { onMods(profile); } else { - toast.success(t('profiles.toast.managing_mods', { name: profile.name })); + toast.success( + t("profiles.toast.managing_mods", { name: profile.name }), + ); console.log("Managing mods for profile:", profile.name); } }, @@ -426,44 +572,20 @@ export function ProfileCardV2({ label: "SETTINGS", icon: "solar:settings-bold", variant: "icon-only", - tooltip: t('profiles.profileOptions'), - onClick: (profile, e) => { - e.preventDefault(); - e.stopPropagation(); - - // Close any other open context menus first - if (openContextMenuId && openContextMenuId !== contextMenuId) { - setOpenContextMenuId(null); - } - - // Simple toggle like CustomDropdown - const newState = !isContextMenuOpen; - setIsContextMenuOpen(newState); - setOpenContextMenuId(newState ? contextMenuId : null); - - // Calculate position when opening - if (!isContextMenuOpen) { - const buttonRect = e.currentTarget.getBoundingClientRect(); - const cardRect = e.currentTarget.closest('.relative')?.getBoundingClientRect(); - - if (cardRect) { - setContextMenuPosition({ - x: buttonRect.right - cardRect.left - 200, // Position menu to the left of the button - y: buttonRect.bottom - cardRect.top + 4, // Position below the button - }); - } - } - }, + tooltip: t("profiles.profileOptions"), + onClick: (profile, e) => { + toggleContextMenuFromButton(e); + }, }, ]; - // Grid layout (more compact, similar to ProfileCard.tsx) + // Grid layout (more compact, similar to ProfileCard.tsx) if (layoutMode === "grid" || layoutMode === "compact") { const isCompact = layoutMode === "compact"; const iconSize = isCompact ? 16 : 20; // Smaller icons for compact mode const padding = isCompact ? "p-3" : "p-4"; // Less padding for compact mode const gap = isCompact ? "gap-2" : "gap-3"; // Smaller gaps for compact mode - + return (
{ // Don't trigger if clicking on action buttons or play overlay const target = e.target as Element; - if (e.target === e.currentTarget || (!target.closest('button') && !target.closest('.play-overlay'))) { + if ( + e.target === e.currentTarget || + (!target.closest("button") && !target.closest(".play-overlay")) + ) { if (variant === "3d") { // In 3D mode, launch the profile when clicking the card if (onPlay) { @@ -486,35 +611,27 @@ export function ProfileCardV2({ if (onMods) { onMods(profile); } else { - toast.success(t('profiles.toast.managing_mods', { name: profile.name })); + toast.success( + t("profiles.toast.managing_mods", { name: profile.name }), + ); console.log("Managing mods for profile:", profile.name); } } } }} - onContextMenu={(e) => { - e.preventDefault(); - e.stopPropagation(); - - if (openContextMenuId && openContextMenuId !== contextMenuId) { - setOpenContextMenuId(null); - } - setIsContextMenuOpen(true); - setOpenContextMenuId(contextMenuId); - - const cardRect = e.currentTarget.getBoundingClientRect(); - setContextMenuPosition({ - x: e.clientX - cardRect.left, - y: e.clientY - cardRect.top, - }); - }} + onContextMenu={openContextMenuAtCursor} > {/* Standard version badge */} {profile.is_standard_version && ( -
+
}>
- +
@@ -523,40 +640,17 @@ export function ProfileCardV2({
{variant === "default" && ( + ref={settingsButtonRef} + onClick={toggleContextMenuFromButton} + className={`${isCompact ? "w-6 h-6" : "w-8 h-8"} flex items-center justify-center rounded transition-all duration-200 bg-black/30 hover:bg-black/50 text-white/70 hover:text-white border border-white/10 hover:border-white/20`} + title={t("profiles.profileOptions")} + data-action="settings" + > + + )} {/* Mods button */} @@ -571,15 +665,24 @@ export function ProfileCardV2({ navigate(`/profilesv2/${profile.id}`); } }} - className={`${variant === "3d" ? (isCompact ? 'w-auto px-2 h-6' : 'w-auto px-3 h-8') : (isCompact ? 'w-6 h-6' : 'w-8 h-8')} flex items-center justify-center gap-1 rounded transition-all duration-200 ${variant === "3d" ? "" : "bg-black/30 hover:bg-black/50 text-white/70 hover:text-white border border-white/10 hover:border-white/20"}`} - style={variant === "3d" ? get3DButtonStyling(modsButtonHovered) : {}} + className={`${variant === "3d" ? (isCompact ? "w-auto px-2 h-6" : "w-auto px-3 h-8") : isCompact ? "w-6 h-6" : "w-8 h-8"} flex items-center justify-center gap-1 rounded transition-all duration-200 ${variant === "3d" ? "" : "bg-black/30 hover:bg-black/50 text-white/70 hover:text-white border border-white/10 hover:border-white/20"}`} + style={ + variant === "3d" ? get3DButtonStyling(modsButtonHovered) : {} + } onMouseEnter={() => setModsButtonHovered(true)} onMouseLeave={() => setModsButtonHovered(false)} - title={t('profiles.manageMods')} + title={t("profiles.manageMods")} > - + {variant === "3d" && ( - MODS + + MODS + )}
@@ -587,38 +690,56 @@ export function ProfileCardV2({
- - + + {/* Play button overlay - similar to ProfileCard.tsx */} {(isLaunching || isHovered) && (
)}
-
+

- +

{(pinned || isHovered) && (
{/* Settings Context Menu */} - { - setIsContextMenuOpen(false); - setOpenContextMenuId(null); - }} - triggerButtonRef={settingsButtonRef} - /> + { + setIsContextMenuOpen(false); + setOpenContextMenuId(null); + }} + triggerButtonRef={settingsButtonRef} + />
); } @@ -745,7 +880,7 @@ export function ProfileCardV2({ onClick={(e) => { // Don't trigger if clicking on action buttons const target = e.target as Element; - if (e.target === e.currentTarget || !target.closest('button')) { + if (e.target === e.currentTarget || !target.closest("button")) { if (variant === "3d") { // In 3D mode, launch the profile when clicking the card if (onPlay) { @@ -758,28 +893,15 @@ export function ProfileCardV2({ if (onMods) { onMods(profile); } else { - toast.success(t('profiles.toast.managing_mods', { name: profile.name })); + toast.success( + t("profiles.toast.managing_mods", { name: profile.name }), + ); console.log("Managing mods for profile:", profile.name); } } } }} - onContextMenu={(e) => { - e.preventDefault(); - e.stopPropagation(); - - if (openContextMenuId && openContextMenuId !== contextMenuId) { - setOpenContextMenuId(null); - } - setIsContextMenuOpen(true); - setOpenContextMenuId(contextMenuId); - - const cardRect = e.currentTarget.getBoundingClientRect(); - setContextMenuPosition({ - x: e.clientX - cardRect.left, - y: e.clientY - cardRect.top, - }); - }} + onContextMenu={openContextMenuAtCursor} > {/* Profile Icon */}
@@ -788,7 +910,10 @@ export function ProfileCardV2({
}>
- +
@@ -800,10 +925,14 @@ export function ProfileCardV2({

- +

{(pinned || isHovered) && (
- + {isLaunching ? (
- {statusMessage || t('profiles.card.starting')} + {statusMessage || t("profiles.card.starting")}
) : ( -
- {/* Minecraft Version */} -
- Minecraft - {profile.game_version} -
- -
- - {/* Loader Version */} -
- {profile.loader - - {profile.loader === "vanilla" - ? t('common.vanilla') - : `${resolvedLoaderVersion?.version || profile.loader_version || t('common.unknown')}` - } - -
- -
- - {/* Last Played */} -
- {formatLastPlayed(profile.last_played)} +
+ {/* Minecraft Version */} +
+ Minecraft + {profile.game_version} +
+ +
+ + {/* Loader Version */} +
+ {profile.loader + + {profile.loader === "vanilla" + ? t("common.vanilla") + : `${resolvedLoaderVersion?.version || profile.loader_version || t("common.unknown")}`} + +
+ +
+ + {/* Last Played */} +
+ {formatLastPlayed(profile.last_played)} +
-
)}
@@ -889,20 +1027,20 @@ export function ProfileCardV2({ flexSpacerAfterIndex={1} /> - {/* Settings Context Menu */} - { - setIsContextMenuOpen(false); - setOpenContextMenuId(null); - }} - triggerButtonRef={undefined} // List layout doesn't have direct button ref - /> - - + {/* Settings Context Menu */} + { + setIsContextMenuOpen(false); + setOpenContextMenuId(null); + }} + triggerButtonRef={undefined} // List layout doesn't have direct button ref + />
); } diff --git a/src/components/profiles/ProfileSettings.tsx b/src/components/profiles/ProfileSettings.tsx index 45ab4127..bf9a884a 100644 --- a/src/components/profiles/ProfileSettings.tsx +++ b/src/components/profiles/ProfileSettings.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { Icon } from "@iconify/react"; import { gsap } from "gsap"; import type { Profile } from "../../types/profile"; @@ -21,6 +21,7 @@ import { useFlags } from 'flagsmith/react'; import { useTranslation } from "react-i18next"; import { DesignerSettingsTab } from './settings/DesignerSettingsTab'; import { cn } from "../../lib/utils"; +import { getGlobalMemorySettings, setGlobalMemorySettings } from "../../services/launcher-config-service"; interface ProfileSettingsProps { profile: Profile; @@ -38,12 +39,29 @@ type SettingsTab = const DESIGNER_FEATURE_FLAG_NAME = "show_keep_local_assets"; +function normalizeForCompare(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(normalizeForCompare); + } + if (value && typeof value === "object") { + const entries = Object.entries(value as Record) + .filter(([, v]) => v !== undefined) + .map(([k, v]) => [k, normalizeForCompare(v)] as const) + .sort(([a], [b]) => a.localeCompare(b)); + return Object.fromEntries(entries); + } + return value; +} + export function ProfileSettings({ profile, onClose }: ProfileSettingsProps) { const { t } = useTranslation(); const { updateProfile, deleteProfile } = useProfileStore(); const [activeTab, setActiveTab] = useState("general"); const [editedProfile, setEditedProfile] = useState({ ...profile }); const [currentProfile, setCurrentProfile] = useState({ ...profile }); + const [baselineProfile, setBaselineProfile] = useState({ ...profile }); + const [baselineRamMb, setBaselineRamMb] = useState(profile.settings?.memory?.max ?? 3072); + const [isBaselineReady, setIsBaselineReady] = useState(false); const [isSaving, setIsSaving] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [systemRam, setSystemRam] = useState(8192); @@ -88,18 +106,62 @@ export function ProfileSettings({ profile, onClose }: ProfileSettingsProps) { }, [isBackgroundAnimationEnabled]); useEffect(() => { - setTempRamMb(profile.settings?.memory?.max ?? 3072); + let isMounted = true; + const initialRamMb = profile.settings?.memory?.max ?? 3072; + setEditedProfile({ ...profile }); + setCurrentProfile({ ...profile }); + setBaselineProfile({ ...profile }); + setIsBaselineReady(false); + + const initializeMemoryBaseline = async () => { + if (profile.is_standard_version) { + try { + const globalMemory = await getGlobalMemorySettings(); + if (!isMounted) return; + setBaselineRamMb(globalMemory.max); + setTempRamMb(globalMemory.max); + } catch { + if (!isMounted) return; + setBaselineRamMb(initialRamMb); + setTempRamMb(initialRamMb); + } + } else { + setBaselineRamMb(initialRamMb); + setTempRamMb(initialRamMb); + } + if (isMounted) { + setIsBaselineReady(true); + } + }; + + initializeMemoryBaseline(); + + return () => { + isMounted = false; + }; }, [profile]); const updateProfileData = (updates: Partial) => { setEditedProfile((prev) => ({ ...prev, ...updates })); }; + const hasUnsavedChanges = useMemo(() => { + if (!isBaselineReady) return false; + const profileChanged = + JSON.stringify(normalizeForCompare(editedProfile)) !== + JSON.stringify(normalizeForCompare(baselineProfile)); + const memoryChanged = tempRamMb !== baselineRamMb; + return profileChanged || memoryChanged; + }, [editedProfile, baselineProfile, tempRamMb, baselineRamMb, isBaselineReady]); + const handleRefresh = async () => { try { const updatedProfile = await ProfileService.getProfile(profile.id); setCurrentProfile(updatedProfile); setEditedProfile(updatedProfile); + setBaselineProfile(updatedProfile); + setBaselineRamMb(updatedProfile.settings?.memory?.max ?? 3072); + setTempRamMb(updatedProfile.settings?.memory?.max ?? 3072); // Update the global store as well to sync with ProfilesTab useProfileStore.getState().refreshSingleProfileInStore(updatedProfile); @@ -114,35 +176,57 @@ export function ProfileSettings({ profile, onClose }: ProfileSettingsProps) { const handleSave = async () => { try { setIsSaving(true); - await updateProfile(profile.id, { - name: editedProfile.name, - game_version: editedProfile.game_version, - loader: editedProfile.loader, - loader_version: editedProfile.loader_version || null || undefined, - settings: { - ...editedProfile.settings, - // Only save memory settings for custom profiles - // Standard profiles save memory to global settings directly via JavaSettingsTab - ...(profile.is_standard_version ? {} : { - memory: { - ...editedProfile.settings?.memory, - max: tempRamMb, - }, - }), - }, - selected_norisk_pack_id: editedProfile.selected_norisk_pack_id, - clear_selected_norisk_pack: !editedProfile.selected_norisk_pack_id, - group: editedProfile.group, - clear_group: !editedProfile.group, - description: editedProfile.description, - norisk_information: editedProfile.norisk_information, - use_shared_minecraft_folder: editedProfile.use_shared_minecraft_folder, - preferred_account_id: editedProfile.preferred_account_id, - clear_preferred_account: !editedProfile.preferred_account_id, - }); + + const profileChanged = + JSON.stringify(normalizeForCompare(editedProfile)) !== + JSON.stringify(normalizeForCompare(baselineProfile)); + const globalMemoryChanged = + profile.is_standard_version && tempRamMb !== baselineRamMb; + const profileMemoryChanged = + !profile.is_standard_version && tempRamMb !== baselineRamMb; + const shouldPersistProfile = profileChanged || profileMemoryChanged; + + if (globalMemoryChanged) { + const currentGlobalMemory = await getGlobalMemorySettings(); + await setGlobalMemorySettings({ + min: Math.min(currentGlobalMemory.min, tempRamMb), + max: tempRamMb, + }); + } + + if (shouldPersistProfile) { + await updateProfile(profile.id, { + name: editedProfile.name, + game_version: editedProfile.game_version, + loader: editedProfile.loader, + loader_version: editedProfile.loader_version || null || undefined, + settings: { + ...editedProfile.settings, + // Only save memory settings for custom profiles + // Standard profiles save memory to global settings in this handler + ...(profile.is_standard_version ? {} : { + memory: { + ...editedProfile.settings?.memory, + max: tempRamMb, + }, + }), + }, + selected_norisk_pack_id: editedProfile.selected_norisk_pack_id, + clear_selected_norisk_pack: !editedProfile.selected_norisk_pack_id, + group: editedProfile.group, + clear_group: !editedProfile.group, + description: editedProfile.description, + norisk_information: editedProfile.norisk_information, + use_shared_minecraft_folder: editedProfile.use_shared_minecraft_folder, + preferred_account_id: editedProfile.preferred_account_id, + clear_preferred_account: !editedProfile.preferred_account_id, + }); + } toast.success(t('profiles.settings.saveSuccess')); setRefreshTrigger(prev => prev + 1); + setBaselineProfile({ ...editedProfile }); + setBaselineRamMb(tempRamMb); } catch (err) { console.error("Failed to save profile:", err); toast.error(t('profiles.settings.saveError')); @@ -151,6 +235,13 @@ export function ProfileSettings({ profile, onClose }: ProfileSettingsProps) { } }; + const handleCancel = () => { + setEditedProfile({ ...baselineProfile }); + setCurrentProfile({ ...baselineProfile }); + setTempRamMb(baselineRamMb); + onClose(); + }; + const handleDelete = async () => { try { setIsDeleting(true); @@ -276,14 +367,22 @@ export function ProfileSettings({ profile, onClose }: ProfileSettingsProps) { const renderFooter = () => (
- +
+ + {hasUnsavedChanges && ( +
+ + Unsaved changes +
+ )} +
{item.id && ( + className="w-full flex items-center gap-3 px-4 py-3 text-left font-minecraft-ten text-sm text-white/80 hover:text-white transition-colors duration-150" + onMouseEnter={(e) => { + e.currentTarget.style.backgroundColor = `${accentColor.value}15`; + }} + onMouseLeave={(e) => { + e.currentTarget.style.backgroundColor = "transparent"; + }} + > + + + {(item.updates_enabled ?? true) + ? t("content.actions.disable_check") + : t("content.actions.enable_check")} + + )}
); // Determine if we can navigate to mod detail page - const canNavigateToDetail = item.modrinth_info?.project_id || item.curseforge_info?.project_id; - const handleTitleClick = canNavigateToDetail ? () => { - if (item.modrinth_info?.project_id) { - navigate(`/mods/modrinth/${item.modrinth_info.project_id}`); - } else if (item.curseforge_info?.project_id) { - navigate(`/mods/curseforge/${item.curseforge_info.project_id}`); - } - } : undefined; + const canNavigateToDetail = + item.modrinth_info?.project_id || item.curseforge_info?.project_id; + const handleTitleClick = canNavigateToDetail + ? () => { + if (item.modrinth_info?.project_id) { + navigate(`/mods/modrinth/${item.modrinth_info.project_id}`); + } else if (item.curseforge_info?.project_id) { + navigate(`/mods/curseforge/${item.curseforge_info.project_id}`); + } + } + : undefined; return ( ({ const getUpdatesToggleConfig = useCallback(() => { if (selectedItemIds.size === 0) return null; - const selectedItems = items.filter(item => selectedItemIds.has(item.filename)); - const validItems = selectedItems.filter(item => item.id); + const selectedItems = items.filter((item) => + selectedItemIds.has(item.filename), + ); + const validItems = selectedItems.filter((item) => item.id); if (validItems.length === 0) return null; // Count how many items have updates enabled vs disabled - const enabledCount = validItems.filter(item => item.updates_enabled ?? true).length; - const disabledCount = validItems.filter(item => !(item.updates_enabled ?? true)).length; + const enabledCount = validItems.filter( + (item) => item.updates_enabled ?? true, + ).length; + const disabledCount = validItems.filter( + (item) => !(item.updates_enabled ?? true), + ).length; // If more items have updates enabled, suggest disabling // If more items have updates disabled, suggest enabling @@ -1283,7 +1485,7 @@ export function LocalContentTabV2({
{contentUpdateError && (
- {t('content.update_check_error', { error: contentUpdateError })} + {t("content.update_check_error", { error: contentUpdateError })}
)} <> @@ -1295,13 +1497,13 @@ export function LocalContentTabV2({ onChange={(checked) => handleSelectAllToggle(checked)} label={ selectedItemIds.size > 0 - ? `${selectedItemIds.size} ${t('common.selected')}` - : t('common.select_all') + ? `${selectedItemIds.size} ${t("common.selected")}` + : t("common.select_all") } tooltip={ areAllFilteredSelected - ? t('content.actions.deselect_all') - : t('content.actions.select_all') + ? t("content.actions.deselect_all") + : t("content.actions.select_all") } /> @@ -1313,39 +1515,66 @@ export function LocalContentTabV2({ actions={[ { id: "batch-toggle", - label: isBatchToggling ? t('content.actions.toggling') : `${t('content.actions.toggle')} (${selectedItemIds.size})`, - icon: isBatchToggling ? LOCAL_CONTENT_TAB_ICONS_TO_PRELOAD[11] : "solar:refresh-bold", + label: isBatchToggling + ? t("content.actions.toggling") + : `${t("content.actions.toggle")} (${selectedItemIds.size})`, + icon: isBatchToggling + ? LOCAL_CONTENT_TAB_ICONS_TO_PRELOAD[11] + : "solar:refresh-bold", variant: "text" as const, disabled: isBatchToggling, loading: isBatchToggling, - tooltip: t('content.actions.toggle_selected', { count: selectedItemIds.size }), + tooltip: t("content.actions.toggle_selected", { + count: selectedItemIds.size, + }), onClick: handleBatchToggleSelected, }, // Smart updates toggle button - only show if we have valid items - ...(updatesToggleConfig ? [{ - id: "batch-toggle-updates", - label: updatesToggleConfig.shouldEnable - ? `${t('content.actions.enable_check')} (${updatesToggleConfig.actionCount})` - : `${t('content.actions.disable_check')} (${updatesToggleConfig.actionCount})`, - icon: updatesToggleConfig.shouldEnable - ? "solar:check-circle-bold" - : "solar:close-circle-bold", - variant: "text" as const, - tooltip: updatesToggleConfig.shouldEnable - ? t('content.actions.enable_checks_selected', { count: updatesToggleConfig.actionCount }) - : t('content.actions.disable_checks_selected', { count: updatesToggleConfig.actionCount }), - onClick: () => handleBatchToggleSelectedUpdatesEnabled(updatesToggleConfig.shouldEnable), - }] : []), - ...(contentType !== "NoRiskMod" ? [{ - id: "batch-delete", - label: isBatchDeleting ? t('content.actions.deleting') : `${t('common.delete')} (${selectedItemIds.size})`, - icon: isBatchDeleting ? LOCAL_CONTENT_TAB_ICONS_TO_PRELOAD[11] : LOCAL_CONTENT_TAB_ICONS_TO_PRELOAD[6], - variant: "text" as const, - disabled: isBatchDeleting, - loading: isBatchDeleting, - tooltip: t('content.actions.delete_selected', { count: selectedItemIds.size }), - onClick: handleBatchDeleteSelected, - }] : []), + ...(updatesToggleConfig + ? [ + { + id: "batch-toggle-updates", + label: updatesToggleConfig.shouldEnable + ? `${t("content.actions.enable_check")} (${updatesToggleConfig.actionCount})` + : `${t("content.actions.disable_check")} (${updatesToggleConfig.actionCount})`, + icon: updatesToggleConfig.shouldEnable + ? "solar:check-circle-bold" + : "solar:close-circle-bold", + variant: "text" as const, + tooltip: updatesToggleConfig.shouldEnable + ? t("content.actions.enable_checks_selected", { + count: updatesToggleConfig.actionCount, + }) + : t("content.actions.disable_checks_selected", { + count: updatesToggleConfig.actionCount, + }), + onClick: () => + handleBatchToggleSelectedUpdatesEnabled( + updatesToggleConfig.shouldEnable, + ), + }, + ] + : []), + ...(contentType !== "NoRiskMod" + ? [ + { + id: "batch-delete", + label: isBatchDeleting + ? t("content.actions.deleting") + : `${t("common.delete")} (${selectedItemIds.size})`, + icon: isBatchDeleting + ? LOCAL_CONTENT_TAB_ICONS_TO_PRELOAD[11] + : LOCAL_CONTENT_TAB_ICONS_TO_PRELOAD[6], + variant: "text" as const, + disabled: isBatchDeleting, + loading: isBatchDeleting, + tooltip: t("content.actions.delete_selected", { + count: selectedItemIds.size, + }), + onClick: handleBatchDeleteSelected, + }, + ] + : []), ]} size="sm" /> @@ -1365,15 +1594,16 @@ export function LocalContentTabV2({ handleSelectedPackChange(value === "" ? null : value) } options={noriskPackOptions} - placeholder={t('content.select_pack_placeholder')} + placeholder={t("content.select_pack_placeholder")} className="!h-9 text-sm min-w-[180px] max-w-[250px] truncate" size="sm" /> {profile?.selected_norisk_pack_id && - noriskPacksConfig?.packs[profile.selected_norisk_pack_id] - ?.isExperimental && ( + noriskPacksConfig?.packs[ + profile.selected_norisk_pack_id + ]?.isExperimental && (
- {t('content.experimental')} + {t("content.experimental")}
)}
@@ -1385,12 +1615,18 @@ export function LocalContentTabV2({ actions={[ { id: "update-all", - label: isUpdatingAll ? t('content.actions.updating_all') : `${t('content.actions.update_all')} (${updatableContentCount})`, - icon: isUpdatingAll ? LOCAL_CONTENT_TAB_ICONS_TO_PRELOAD[11] : LOCAL_CONTENT_TAB_ICONS_TO_PRELOAD[14], + label: isUpdatingAll + ? t("content.actions.updating_all") + : `${t("content.actions.update_all")} (${updatableContentCount})`, + icon: isUpdatingAll + ? LOCAL_CONTENT_TAB_ICONS_TO_PRELOAD[11] + : LOCAL_CONTENT_TAB_ICONS_TO_PRELOAD[14], variant: "highlight" as const, disabled: isUpdatingAll, loading: isUpdatingAll, - tooltip: t('content.actions.update_all_tooltip', { count: updatableContentCount }), + tooltip: t("content.actions.update_all_tooltip", { + count: updatableContentCount, + }), onClick: handleUpdateAllAvailableContent, }, ]} @@ -1399,47 +1635,55 @@ export function LocalContentTabV2({ )} {/* Browse and Add buttons - only for non-NoRiskMod types */} - {effectiveOnAddContent && contentType !== "NoRiskMod" && profile && ( - { - if (profile && onBrowseContentRequest) { - const browseContentType = getBrowseTabContentType(contentType); - onBrowseContentRequest(browseContentType); - } else if (profile) { - const browseContentType = getBrowseTabContentType(contentType); - navigate(`/profilesv2/${profile.id}/browse/${browseContentType}`); - } + {effectiveOnAddContent && + contentType !== "NoRiskMod" && + profile && ( + { + if (profile && onBrowseContentRequest) { + const browseContentType = + getBrowseTabContentType(contentType); + onBrowseContentRequest(browseContentType); + } else if (profile) { + const browseContentType = + getBrowseTabContentType(contentType); + navigate( + `/profilesv2/${profile.id}/browse/${browseContentType}`, + ); + } + }, }, - }, - { - id: "add", - label: t('content.actions.import'), - icon: "solar:folder-with-files-bold", - variant: "text" as const, - tooltip: addContentButtonText, - onClick: effectiveOnAddContent, - }, - ]} - size="sm" - /> - )} + { + id: "add", + label: t("content.actions.import"), + icon: "solar:folder-with-files-bold", + variant: "text" as const, + tooltip: addContentButtonText, + onClick: effectiveOnAddContent, + }, + ]} + size="sm" + /> + )} {/* Refresh button - visible when no items are selected */} fetchData(true), }, ]} @@ -1458,7 +1702,9 @@ export function LocalContentTabV2({ if (!profile) { return (
- {t('content.profile_unavailable', { itemType: itemTypeNamePlural.toLowerCase() })} + {t("content.profile_unavailable", { + itemType: itemTypeNamePlural.toLowerCase(), + })}
); } @@ -1472,12 +1718,12 @@ export function LocalContentTabV2({ try { const newName = await confirm({ - title: t('profiles.clone'), - inputLabel: t('profiles.new_name'), - inputPlaceholder: t('profiles.clone_name_placeholder'), + title: t("profiles.clone"), + inputLabel: t("profiles.new_name"), + inputPlaceholder: t("profiles.clone_name_placeholder"), inputInitialValue: `${profile.name} (Copy)`, inputRequired: true, - confirmText: t('profiles.clone_button'), + confirmText: t("profiles.clone_button"), type: "input", fullscreen: true, // Or false, depending on desired dialog style }); @@ -1491,7 +1737,10 @@ export function LocalContentTabV2({ ); toast.promise(clonePromise, { - loading: t('content.toast.cloning', { name: profile.name, newName }), + loading: t("content.toast.cloning", { + name: profile.name, + newName, + }), success: (newProfileId) => { // Immediately attempt to update the group after cloning is successful updateProfile(newProfileId, { group: "CUSTOM" }) @@ -1510,10 +1759,12 @@ export function LocalContentTabV2({ fetchProfiles(); // Refresh profiles list in the store if (onRefreshRequired) onRefreshRequired(); // Refresh parent view if callback provided navigate(`/profilesv2/${newProfileId}`); // Navigate to the new profile's detail view - return t('content.toast.clone_success', { name: newName }); // Toast for cloning success + return t("content.toast.clone_success", { name: newName }); // Toast for cloning success }, error: (err) => - t('content.toast.clone_failed', { error: err instanceof Error ? err.message : String(err.message) }), + t("content.toast.clone_failed", { + error: err instanceof Error ? err.message : String(err.message), + }), }); } } catch (err) { @@ -1522,7 +1773,7 @@ export function LocalContentTabV2({ if (err !== "cancel") { // Check if it's not a cancellation console.error("Error in clone setup or dialog: ", err); - toast.error(t('content.toast.clone_init_failed')); + toast.error(t("content.toast.clone_init_failed")); } } }; @@ -1534,7 +1785,7 @@ export function LocalContentTabV2({ onClick={handleCloneProfile} // Updated onClick icon={} > - {t('profiles.clone_profile')} + {t("profiles.clone_profile")} ); @@ -1542,8 +1793,8 @@ export function LocalContentTabV2({ <> ({ // Dynamic empty state messages const getEmptyStateMessage = () => { if (contentType === "NoRiskMod" && !profile?.selected_norisk_pack_id) { - return t('content.no_pack_selected'); + return t("content.no_pack_selected"); } else if (error) { return ""; // Remove title, show only button } else if ((isLoading || isFetchingPacksConfig) && items.length === 0) { - return t('content.loading_items', { itemType: itemTypeNamePlural }); + return t("content.loading_items", { itemType: itemTypeNamePlural }); } else if ( !searchQuery && items.length === 0 && @@ -1579,31 +1830,31 @@ export function LocalContentTabV2({ ) { return ""; // Remove title, show only button } else { - return t('content.manage_items', { itemType: itemTypeNamePlural }); + return t("content.manage_items", { itemType: itemTypeNamePlural }); } }; const getEmptyStateDescription = () => { if (contentType === "NoRiskMod" && !profile?.selected_norisk_pack_id) { - return t('content.select_pack_description'); + return t("content.select_pack_description"); } else if (error) { - return t('content.error_description'); + return t("content.error_description"); } else if ((isLoading || isFetchingPacksConfig) && items.length === 0) { - return t('content.loading_description'); + return t("content.loading_description"); } else if ( !searchQuery && items.length === 0 && selectedItemIds.size === 0 ) { - return t('content.empty_description', { itemType: itemTypeNamePlural }); + return t("content.empty_description", { itemType: itemTypeNamePlural }); } else if ( searchQuery && filteredItems.length === 0 && selectedItemIds.size === 0 ) { - return t('content.no_search_results'); + return t("content.no_search_results"); } else { - return t('content.select_to_manage', { itemType: itemTypeNamePlural }); + return t("content.select_to_manage", { itemType: itemTypeNamePlural }); } }; @@ -1652,17 +1903,23 @@ export function LocalContentTabV2({ emptyStateAction={ // Show browse button for empty states (except when NoRisk pack not selected and when loading) (isTrulyEmptyState || - (searchQuery && filteredItems.length === 0 && selectedItemIds.size === 0) || - (error && !isLoading)) && - !(contentType === "NoRiskMod" && !profile?.selected_norisk_pack_id) ? ( + (searchQuery && + filteredItems.length === 0 && + selectedItemIds.size === 0) || + (error && !isLoading)) && + !( + contentType === "NoRiskMod" && !profile?.selected_norisk_pack_id + ) ? ( ({ isDeleting={isDialogActionLoading} title={ itemToDeleteForDialog - ? t('content.delete_item_title', { name: getDisplayFileName(itemToDeleteForDialog) }) - : t('content.delete_selected_title', { itemType: itemTypeNamePlural }) + ? t("content.delete_item_title", { + name: getDisplayFileName(itemToDeleteForDialog), + }) + : t("content.delete_selected_title", { + itemType: itemTypeNamePlural, + }) } /> diff --git a/src/components/profiles/settings/JavaSettingsTab.tsx b/src/components/profiles/settings/JavaSettingsTab.tsx index 18b8892f..80e2cb0a 100644 --- a/src/components/profiles/settings/JavaSettingsTab.tsx +++ b/src/components/profiles/settings/JavaSettingsTab.tsx @@ -174,8 +174,6 @@ export function JavaSettingsTab({ getGlobalMemorySettings() .then((settings) => { setGlobalMemorySettingsState(settings); - // Synchronize tempRamMb with global settings for standard profiles - setTempRamMb(settings.max); }) .catch((error) => { console.error("Failed to load global memory settings:", error); @@ -283,32 +281,25 @@ export function JavaSettingsTab({ ? (globalMemorySettings || { min: 1024, max: recommendedMaxRam }) : (editedProfile.settings?.memory || { min: 1024, max: recommendedMaxRam }); - const handleMemoryChange = async (value: number) => { + const handleMemoryChange = (value: number) => { if (editedProfile.is_standard_version) { - // For standard profiles, save to global settings - const newGlobalSettings: MemorySettings = { + // For standard profiles, only update local UI state. + // Persisting happens in ProfileSettings handleSave. + setGlobalMemorySettingsState({ min: memory.min, max: value, - }; - - try { - await setGlobalMemorySettings(newGlobalSettings); - setGlobalMemorySettingsState(newGlobalSettings); - } catch (error) { - console.error("Failed to save global memory settings:", error); - toast.error(t('java.save_ram_failed')); - } + }); } else { - // For custom profiles, save to profile settings - const newSettings = { ...editedProfile.settings }; - if (!newSettings.memory) { - newSettings.memory = { - min: 1024, + // For non-standard profiles, keep memory as draft only. + // Persisting happens in ProfileSettings handleSave. + const currentMin = editedProfile.settings?.memory?.min ?? 1024; + const newSettings = { + ...editedProfile.settings, + memory: { + min: currentMin, max: value, - }; - } else { - newSettings.memory.max = value; - } + }, + }; updateProfile({ settings: newSettings }); } }; @@ -442,10 +433,7 @@ export function JavaSettingsTab({ value={tempRamMb} onChange={(value) => { setTempRamMb(value); - // For standard profiles, save to global settings immediately - if (editedProfile.is_standard_version) { - handleMemoryChange(value); - } + handleMemoryChange(value); }} min={512} max={systemRam} diff --git a/src/components/ui/SettingsContextMenu.tsx b/src/components/ui/SettingsContextMenu.tsx index 6708c31f..338ab0dd 100644 --- a/src/components/ui/SettingsContextMenu.tsx +++ b/src/components/ui/SettingsContextMenu.tsx @@ -35,6 +35,10 @@ export interface SettingsContextMenuProps { onClose: () => void; /** Optional ref to the settings button that triggers this menu */ triggerButtonRef?: React.RefObject; + /** Positioning strategy for the menu */ + positionMode?: "absolute" | "fixed"; + /** Close menu when scrolling/wheeling */ + closeOnScroll?: boolean; } export function SettingsContextMenu({ @@ -44,6 +48,8 @@ export function SettingsContextMenu({ items, onClose, triggerButtonRef, + positionMode = "absolute", + closeOnScroll = false, }: SettingsContextMenuProps) { const accentColor = useThemeStore((state) => state.accentColor); const menuRef = useRef(null); @@ -52,24 +58,24 @@ export function SettingsContextMenu({ useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as Node; - + // Don't close if clicking inside the menu if (menuRef.current && menuRef.current.contains(target)) { return; } - + // Don't close if clicking on the trigger button or any button with data-action="settings" (let the button handle the toggle) if (triggerButtonRef?.current && triggerButtonRef.current.contains(target)) { return; } - + // Additional check: look for any settings button in the DOM tree (for list mode) const clickedElement = target as Element; const settingsButton = clickedElement.closest('button[data-action="settings"], button[title*="Profile Options"], button[title*="Profil Optionen"]'); if (settingsButton) { return; } - + // Close if clicking anywhere else onClose(); }; @@ -98,6 +104,23 @@ export function SettingsContextMenu({ } }, [isOpen, onClose]); + // Optionally close menu when scrolling or using wheel + useEffect(() => { + if (!isOpen || !closeOnScroll) return; + + const handleScrollOrWheel = () => { + onClose(); + }; + + window.addEventListener("scroll", handleScrollOrWheel, true); + window.addEventListener("wheel", handleScrollOrWheel, { passive: true }); + + return () => { + window.removeEventListener("scroll", handleScrollOrWheel, true); + window.removeEventListener("wheel", handleScrollOrWheel); + }; + }, [isOpen, closeOnScroll, onClose]); + if (!isOpen) return null; return ( @@ -105,6 +128,7 @@ export function SettingsContextMenu({ ref={menuRef} className="absolute bg-black/90 backdrop-blur-sm border border-white/20 rounded-lg shadow-xl z-50 overflow-hidden" style={{ + position: positionMode, left: position.x, top: position.y, minWidth: "200px", @@ -114,10 +138,8 @@ export function SettingsContextMenu({ {items.map((item) => ( {/* Separator */} - {item.separator && ( -
- )} - + {item.separator &&
} +