diff --git a/crates/lanspread-peer/src/lib.rs b/crates/lanspread-peer/src/lib.rs index 8c22097..b2dc772 100644 --- a/crates/lanspread-peer/src/lib.rs +++ b/crates/lanspread-peer/src/lib.rs @@ -5,7 +5,7 @@ mod peer; use std::{ cmp::Reverse, - collections::{HashMap, VecDeque}, + collections::{HashMap, HashSet, VecDeque}, net::{IpAddr, SocketAddr}, path::{Path, PathBuf}, sync::Arc, @@ -130,6 +130,7 @@ pub enum PeerEvent { PeerDiscovered(SocketAddr), PeerLost(SocketAddr), PeerCountUpdated(usize), + LocalGamesUpdated(Vec), } #[derive(Clone, Debug)] @@ -1265,6 +1266,7 @@ async fn local_download_available(game_dir: &str, game_id: &str) -> bool { !local_dir_has_content(game_path.as_path()).await } +#[derive(Clone)] struct Ctx { game_dir: Arc>>, local_game_db: Arc>>, @@ -1345,6 +1347,13 @@ pub async fn run_peer( run_ping_service(tx_notify_ui_ping, peer_game_db_ping).await; }); + // Start local game directory monitoring task + let tx_notify_ui_monitor = tx_notify_ui.clone(); + let ctx_monitor = ctx.clone(); + tokio::spawn(async move { + run_local_game_monitor(tx_notify_ui_monitor, ctx_monitor).await; + }); + // Handle client commands loop { let Some(cmd) = rx_control.recv().await else { @@ -2170,6 +2179,89 @@ async fn run_ping_service( } } +/// Monitor local game directory for changes and update the local game database +async fn run_local_game_monitor(tx_notify_ui: UnboundedSender, ctx: Ctx) { + log::info!("Starting local game directory monitor (5s interval)"); + + let mut interval = tokio::time::interval(Duration::from_secs(5)); + + loop { + interval.tick().await; + + let game_dir = { + let guard = ctx.game_dir.read().await; + guard.clone() + }; + + if let Some(ref game_dir) = game_dir { + match scan_local_games(game_dir).await { + Ok(current_games) => { + let local_game_db = ctx.local_game_db.clone(); + let mut db_guard = local_game_db.write().await; + + let previous_games = db_guard + .as_ref() + .map(|db| db.games.keys().cloned().collect::>()) + .unwrap_or_default(); + + let current_game_ids = + current_games.games.keys().cloned().collect::>(); + + // Check if any games were removed + let removed_games: Vec = previous_games + .difference(¤t_game_ids) + .cloned() + .collect(); + + if removed_games.is_empty() { + // Check if any games were added or updated + if previous_games != current_game_ids { + log::debug!( + "Local games directory structure changed, updating database" + ); + *db_guard = Some(current_games); + + let all_games = db_guard + .as_ref() + .map(|db| { + db.all_games().into_iter().cloned().collect::>() + }) + .unwrap_or_default(); + + if let Err(e) = + tx_notify_ui.send(PeerEvent::LocalGamesUpdated(all_games)) + { + log::error!("Failed to send LocalGamesUpdated event: {e}"); + } + } + } else { + log::info!("Detected removed games: {removed_games:?}"); + *db_guard = Some(current_games); + + // Notify UI about the change + let all_games = db_guard + .as_ref() + .map(|db| db.all_games().into_iter().cloned().collect::>()) + .unwrap_or_default(); + + if let Err(e) = tx_notify_ui.send(PeerEvent::LocalGamesUpdated(all_games)) { + log::error!("Failed to send LocalGamesUpdated event: {e}"); + } + } + } + Err(e) => { + log::error!("Failed to scan local games directory: {e}"); + } + } + } + } +} + +/// Scan the local games directory and return a `GameDB` with current games +async fn scan_local_games(game_dir: &str) -> eyre::Result { + load_local_game_db(game_dir).await +} + async fn ping_peer(peer_addr: SocketAddr) -> eyre::Result { let limits = Limits::default().with_max_handshake_duration(Duration::from_secs(3))?; diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs index f191f20..8055569 100644 --- a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs +++ b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs @@ -525,6 +525,45 @@ async fn update_game_db(games: Vec, app: AppHandle) { refresh_games_list(&app).await; } +async fn update_local_games_in_db(local_games: Vec, app: AppHandle) { + let state = app.state::(); + + // Collect local game IDs first to avoid move issues + let local_game_ids: HashSet = local_games.iter().map(|g| g.id.clone()).collect(); + + { + let mut game_db = state.games.write().await; + + // Update installation status for games that exist locally + for local_game in &local_games { + if let Some(existing_game) = game_db.get_mut_game_by_id(&local_game.id) { + existing_game.downloaded = local_game.downloaded; + existing_game.installed = local_game.installed; + existing_game + .local_version + .clone_from(&local_game.local_version); + log::debug!("Updated local game status for: {}", local_game.id); + } + } + + // For games in the main DB that are not in the local list, + // mark them as not downloaded/installed (they were deleted) + for game in game_db.games.values_mut() { + if !local_game_ids.contains(&game.id) && (game.downloaded || game.installed) { + log::info!( + "Game {} no longer exists locally, marking as uninstalled", + game.id + ); + game.downloaded = false; + game.installed = false; + game.local_version = None; + } + } + } + + refresh_games_list(&app).await; +} + fn add_final_slash(path: &str) -> String { #[cfg(target_os = "windows")] const SLASH_CHAR: char = '\\'; @@ -699,6 +738,10 @@ pub fn run() { log::info!("PeerEvent::ListGames received"); update_game_db(games, app_handle.clone()).await; } + PeerEvent::LocalGamesUpdated(local_games) => { + log::info!("PeerEvent::LocalGamesUpdated received"); + update_local_games_in_db(local_games, app_handle.clone()).await; + } PeerEvent::GotGameFiles { id, file_descriptions } => { log::info!("PeerEvent::GotGameFiles received");