detect if a game is deleted, added, modified locally
This commit is contained in:
@@ -5,7 +5,7 @@ mod peer;
|
|||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Reverse,
|
cmp::Reverse,
|
||||||
collections::{HashMap, VecDeque},
|
collections::{HashMap, HashSet, VecDeque},
|
||||||
net::{IpAddr, SocketAddr},
|
net::{IpAddr, SocketAddr},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
@@ -130,6 +130,7 @@ pub enum PeerEvent {
|
|||||||
PeerDiscovered(SocketAddr),
|
PeerDiscovered(SocketAddr),
|
||||||
PeerLost(SocketAddr),
|
PeerLost(SocketAddr),
|
||||||
PeerCountUpdated(usize),
|
PeerCountUpdated(usize),
|
||||||
|
LocalGamesUpdated(Vec<Game>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[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
|
!local_dir_has_content(game_path.as_path()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
struct Ctx {
|
struct Ctx {
|
||||||
game_dir: Arc<RwLock<Option<String>>>,
|
game_dir: Arc<RwLock<Option<String>>>,
|
||||||
local_game_db: Arc<RwLock<Option<GameDB>>>,
|
local_game_db: Arc<RwLock<Option<GameDB>>>,
|
||||||
@@ -1345,6 +1347,13 @@ pub async fn run_peer(
|
|||||||
run_ping_service(tx_notify_ui_ping, peer_game_db_ping).await;
|
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
|
// Handle client commands
|
||||||
loop {
|
loop {
|
||||||
let Some(cmd) = rx_control.recv().await else {
|
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<PeerEvent>, 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::<HashSet<_>>())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let current_game_ids =
|
||||||
|
current_games.games.keys().cloned().collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
// Check if any games were removed
|
||||||
|
let removed_games: Vec<String> = 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::<Vec<Game>>()
|
||||||
|
})
|
||||||
|
.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::<Vec<Game>>())
|
||||||
|
.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<GameDB> {
|
||||||
|
load_local_game_db(game_dir).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn ping_peer(peer_addr: SocketAddr) -> eyre::Result<bool> {
|
async fn ping_peer(peer_addr: SocketAddr) -> eyre::Result<bool> {
|
||||||
let limits = Limits::default().with_max_handshake_duration(Duration::from_secs(3))?;
|
let limits = Limits::default().with_max_handshake_duration(Duration::from_secs(3))?;
|
||||||
|
|
||||||
|
|||||||
@@ -525,6 +525,45 @@ async fn update_game_db(games: Vec<Game>, app: AppHandle) {
|
|||||||
refresh_games_list(&app).await;
|
refresh_games_list(&app).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_local_games_in_db(local_games: Vec<Game>, app: AppHandle) {
|
||||||
|
let state = app.state::<LanSpreadState>();
|
||||||
|
|
||||||
|
// Collect local game IDs first to avoid move issues
|
||||||
|
let local_game_ids: HashSet<String> = 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 {
|
fn add_final_slash(path: &str) -> String {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
const SLASH_CHAR: char = '\\';
|
const SLASH_CHAR: char = '\\';
|
||||||
@@ -699,6 +738,10 @@ pub fn run() {
|
|||||||
log::info!("PeerEvent::ListGames received");
|
log::info!("PeerEvent::ListGames received");
|
||||||
update_game_db(games, app_handle.clone()).await;
|
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 } => {
|
PeerEvent::GotGameFiles { id, file_descriptions } => {
|
||||||
log::info!("PeerEvent::GotGameFiles received");
|
log::info!("PeerEvent::GotGameFiles received");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user