diff --git a/crates/lanspread-client/src/lib.rs b/crates/lanspread-client/src/lib.rs index 822dffe..48794d2 100644 --- a/crates/lanspread-client/src/lib.rs +++ b/crates/lanspread-client/src/lib.rs @@ -22,6 +22,8 @@ static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../.. pub enum ClientEvent { ListGames(Vec), GotGameFiles(Vec), + DownloadGameFilesBegin { id: String }, + DownloadGameFilesFinished { id: String }, } #[derive(Debug)] @@ -69,6 +71,7 @@ async fn download_game_files( game_file_descs: Vec, games_dir: String, server_addr: SocketAddr, + tx_notify_ui: UnboundedSender, ) -> eyre::Result<()> { let limits = Limits::default() .with_max_handshake_duration(Duration::from_secs(3))? @@ -89,6 +92,21 @@ async fn download_game_files( .filter(|desc| !desc.is_dir) .collect::>(); + if game_file_descs.is_empty() { + log::error!("game_file_descs empty: no game files to download"); + return Ok(()); + } + + let game_id = game_file_descs + .first() + .expect("game_file_descs empty: 2nd case CANNOT HAPPEN") + .game_id + .clone(); + + tx_notify_ui.send(ClientEvent::DownloadGameFilesBegin { + id: game_id.clone(), + })?; + for file_desc in game_file_descs { log::info!("downloading file: {}", file_desc.relative_path); @@ -125,6 +143,8 @@ async fn download_game_files( } } + log::info!("all files downloaded for game: {game_id}"); + tx_notify_ui.send(ClientEvent::DownloadGameFilesFinished { id: game_id })?; Ok(()) } @@ -205,9 +225,15 @@ pub async fn run( let games_dir = { ctx.game_dir.lock().await.clone() }; if let Some(games_dir) = games_dir { + let tx_notify_ui = tx_notify_ui.clone(); tokio::task::spawn(async move { - if let Err(e) = - download_game_files(game_file_descs, games_dir, server_addr).await + if let Err(e) = download_game_files( + game_file_descs, + games_dir, + server_addr, + tx_notify_ui, + ) + .await { log::error!("failed to download game files: {e}"); } 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 975e0d0..edde1f3 100644 --- a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs +++ b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashSet, net::SocketAddr, path::{Path, PathBuf}, sync::Arc, @@ -16,6 +17,7 @@ struct LanSpreadState { server_addr: Mutex>, client_ctrl: UnboundedSender, games: Arc>, + games_in_download: Arc>>, } #[tauri::command] @@ -28,22 +30,26 @@ fn request_games(state: tauri::State) { } #[tauri::command] -fn install_game(id: String, state: tauri::State) -> String { +fn install_game(id: String, state: tauri::State) -> bool { log::error!("Running game with id {id}"); - // let result = Command::new(r#"C:\Users\ddidderr\scoop\apps\mpv\0.39.0\mpv.exe"#).spawn(); + let already_in_download = tauri::async_runtime::block_on(async { + if state.inner().games_in_download.lock().await.contains(&id) { + log::error!("Game is already downloading: {id}"); + return true; + } + false + }); + + if already_in_download { + return false; + } if let Err(e) = state.inner().client_ctrl.send(ClientCommand::GetGame(id)) { log::error!("Failed to send message to client: {e:?}"); } - "TODO".to_string() - - // if result.is_ok() { - // "Ok".to_string() - // } else { - // "Failed to run game".to_string() - // } + true } fn set_game_install_state_from_path(game_db: &mut GameDB, path: &Path, installed: bool) { @@ -183,6 +189,7 @@ pub fn run() { server_addr: Mutex::new(None), client_ctrl: tx_client_control, games: Arc::new(Mutex::new(GameDB::empty())), + games_in_download: Arc::new(Mutex::new(HashSet::new())), }; tauri::Builder::default() @@ -209,21 +216,50 @@ pub fn run() { while let Some(event) = rx_client_event.recv().await { match event { ClientEvent::ListGames(games) => { - log::debug!("Received client event: ListGames"); + log::info!("ClientEvent::ListGames received"); update_game_db(games, app_handle.clone()).await; } ClientEvent::GotGameFiles(game_file_descs) => { - log::debug!("Received client event: GotGameFiles"); - if let Err(e) = app_handle.emit("game-download-in-progress", Some(())) { - log::error!("Failed to emit game-files event: {e}"); + log::info!("ClientEvent::GotGameFiles received"); + + if let Some(first_desc_file) = game_file_descs.first() { + if let Err(e) = app_handle.emit( + "game-download-pre", + Some(first_desc_file.game_id.clone()), + ) { + log::error!("ClientEvent::GotGameFiles: Failed to emit game-download-pre event: {e}"); + } + + app_handle + .state::() + .inner() + .client_ctrl + .send(ClientCommand::DownloadGameFiles(game_file_descs)) + .unwrap(); + } else { + log::error!("ClientEvent::GotGameFiles: Got empty game files list"); } + } + ClientEvent::DownloadGameFilesBegin { id } => { + log::info!("ClientEvent::DownloadGameFilesBegin received"); app_handle .state::() .inner() - .client_ctrl - .send(ClientCommand::DownloadGameFiles(game_file_descs)) - .unwrap(); + .games_in_download + .lock() + .await + .insert(id.clone()); + + if let Err(e) = app_handle.emit("game-download-begin", Some(id)) { + log::error!("ClientEvent::DownloadGameFilesBegin: Failed to emit game-download-begin event: {e}"); + } + } + ClientEvent::DownloadGameFilesFinished { id } => { + log::info!("ClientEvent::DownloadGameFilesFinished received"); + if let Err(e) = app_handle.emit("game-download-finished", Some(id)) { + log::error!("ClientEvent::DownloadGameFilesFinished: Failed to emit game-download-finished event: {e}"); + } } } } diff --git a/crates/lanspread-tauri-deno-ts/src/App.tsx b/crates/lanspread-tauri-deno-ts/src/App.tsx index f4e4538..384e440 100644 --- a/crates/lanspread-tauri-deno-ts/src/App.tsx +++ b/crates/lanspread-tauri-deno-ts/src/App.tsx @@ -9,6 +9,15 @@ import "./App.css"; const FILE_STORAGE = 'launcher-settings.json'; const GAME_DIR_KEY = 'game-directory'; +// enum with install status +enum InstallStatus { + NotInstalled = 'NotInstalled', + CheckingServer = 'CheckingServer', + Downloading = 'Downloading', + Unpacking = 'Unpacking', + Installed = 'Installed', +} + interface Game { id: string; name: string; @@ -16,6 +25,7 @@ interface Game { size: number; thumbnail: Uint8Array; installed: boolean; + install_status: InstallStatus; } const App = () => { @@ -64,7 +74,7 @@ const App = () => { const setupEventListener = async () => { try { - // Listen for events that update the game list + // Listen for games-list-updated events const unlisten_games = await listen('games-list-updated', (event) => { console.log('๐Ÿ—ฒ Received games-list-updated event'); const games = event.payload as Game[]; @@ -73,6 +83,24 @@ const App = () => { getInitialGameDir(); }); + // Listen for game-download-begin events + const unlisten_game_download_begin = await listen('game-download-begin', (event) => { + const game_id = event.payload as string; + console.log(`๐Ÿ—ฒ game-download-begin ${game_id} event received`); + setGameItems(prev => prev.map(item => item.id === game_id + ? {...item, install_status: InstallStatus.Downloading} + : item)); + }); + + // Listen for game-download-finished events + const unlisten_game_download_finished = await listen('game-download-finished', (event) => { + const game_id = event.payload as string; + console.log(`๐Ÿ—ฒ game-download-finished ${game_id} event received`); + setGameItems(prev => prev.map(item => item.id === game_id + ? {...item, install_status: InstallStatus.Unpacking} + : item)); + }); + // Initial request for games console.log('๐Ÿ“ค Requesting initial games list'); await invoke('request_games'); @@ -81,6 +109,8 @@ const App = () => { return () => { console.log('๐Ÿงน Cleaning up - removing listener'); unlisten_games(); + unlisten_game_download_begin(); + unlisten_game_download_finished(); }; } catch (error) { console.error('โŒ Error in setup:', error); @@ -98,13 +128,32 @@ const App = () => { const runGame = async (id: string) => { console.log(`๐ŸŽฏ Running game with id=${id}`); try { - const result = await invoke('install_game', {id}); + const result = await invoke('run_game', {id}); console.log(`โœ… Game started, result=${result}`); } catch (error) { console.error('โŒ Error running game:', error); } }; + const installGame = async (id: string) => { + console.log(`๐ŸŽฏ Installing game with id=${id}`); + try { + const success = await invoke('install_game', {id}); + if (success) { + console.log(`โœ… Game install for id=${id} started...`); + // update install status in gameItems for this game + setGameItems(prev => prev.map(item => item.id === id + ? {...item, install_status: InstallStatus.CheckingServer} + : item)); + } else { + // game is already being installed + console.warn(`๐Ÿšง Game with id=${id} is already being installed`); + } + } catch (error) { + console.error('โŒ Error installing game:', error); + } + }; + const dialogGameDir = async () => { const file = await open({ multiple: false, @@ -146,14 +195,23 @@ const App = () => { const binaryString = uint8Array.reduce((acc, byte) => acc + String.fromCharCode(byte), ''); const thumbnailUrl = `data:image/jpeg;base64,${btoa(binaryString)}`; return ( -
runGame(item.id)}> +
{`${item.name}
{item.name}
{item.description.slice(0, 10)} {item.size.toString()}
-
{item.installed ? 'Play' : 'Install'}
+
item.installed + ? runGame(item.id) + : installGame(item.id)}> + {item.installed ? 'Play' + : item.install_status === InstallStatus.CheckingServer ? 'Checking server...' + : item.install_status === InstallStatus.Downloading ? 'Downloading...' + : item.install_status === InstallStatus.Unpacking ? 'Unpacking...' + : 'Install'} +
); })}