From a6ed6e04fe12850a4bc2ae178033f6c02529b336 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Wed, 13 Nov 2024 23:51:28 +0100 Subject: [PATCH] [backup] games from server with images --- Cargo.lock | 2 + Cargo.toml | 2 +- crates/lanspread-client/Cargo.toml | 2 + crates/lanspread-client/src/lib.rs | 11 +- crates/lanspread-db/Cargo.toml | 1 + crates/lanspread-db/src/db.rs | 7 +- crates/lanspread-proto/src/lib.rs | 22 +-- crates/lanspread-server/src/assets.rs | 24 +++ crates/lanspread-server/src/main.rs | 27 ++- .../src-tauri/src/lib.rs | 6 +- crates/lanspread-tauri-deno-ts/src/App.css | 37 +++- crates/lanspread-tauri-deno-ts/src/App.tsx | 183 ++++++++++-------- crates/lanspread-tauri-deno-ts/src/main.tsx | 3 - 13 files changed, 214 insertions(+), 113 deletions(-) create mode 100644 crates/lanspread-server/src/assets.rs diff --git a/Cargo.lock b/Cargo.lock index 296f3f2..bfdada5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2303,6 +2303,7 @@ dependencies = [ name = "lanspread-client" version = "0.1.0" dependencies = [ + "bytes", "clap", "eyre", "lanspread-db", @@ -2330,6 +2331,7 @@ dependencies = [ name = "lanspread-db" version = "0.1.0" dependencies = [ + "bytes", "eyre", "semver", "serde", diff --git a/Cargo.toml b/Cargo.toml index e025630..3ba0ce9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ resolver = "2" [workspace.dependencies] -bytes = "1.8" +bytes = { version = "1.8", features = ["serde"] } clap = { version = "4.5", features = ["derive"] } eyre = "0.6" itertools = "0.13" diff --git a/crates/lanspread-client/Cargo.toml b/crates/lanspread-client/Cargo.toml index f63ed05..a162b34 100644 --- a/crates/lanspread-client/Cargo.toml +++ b/crates/lanspread-client/Cargo.toml @@ -16,7 +16,9 @@ unwrap_used = "warn" lanspread-db = { path = "../lanspread-db" } lanspread-proto = { path = "../lanspread-proto" } lanspread-utils = { path = "../lanspread-utils" } + # external +bytes = { workspace = true } clap = { workspace = true } eyre = { workspace = true } log = "0.4" diff --git a/crates/lanspread-client/src/lib.rs b/crates/lanspread-client/src/lib.rs index fb06960..452a378 100644 --- a/crates/lanspread-client/src/lib.rs +++ b/crates/lanspread-client/src/lib.rs @@ -1,5 +1,6 @@ use std::{net::SocketAddr, time::Duration}; +use bytes::{Bytes, BytesMut}; use lanspread_db::db::Game; use lanspread_proto::{Message as _, Request, Response}; use lanspread_utils::maybe_addr; @@ -69,7 +70,7 @@ pub async fn run( ClientCommand::ListGames => Request::ListGames, ClientCommand::GetGame(id) => Request::GetGame { id }, ClientCommand::ServerAddr(_) => Request::Invalid( - [].into(), + Bytes::new(), "invalid control message (ServerAddr), should not happen".into(), ), }; @@ -91,24 +92,26 @@ pub async fn run( log::error!("failed to send request to server {:?}", e); } - let mut data: Vec = Vec::new(); + let mut data = BytesMut::new(); while let Ok(Some(bytes)) = rx.receive().await { data.extend_from_slice(&bytes); } log::debug!("{} bytes received from server", data.len()); log::trace!("server response (RAW): {}", String::from_utf8_lossy(&data)); - let response = Response::decode(&data); + let response = Response::decode(data.freeze()); log::trace!("server response (DECODED): {response:?}"); match response { Response::Games(games) => { for game in &games { - log::debug!("{game}"); + log::trace!("{game}"); } if let Err(e) = tx_notify_ui.send(ClientEvent::ListGames(games)) { log::debug!("failed to send ClientEvent::ListGames to client {e:?}"); + } else { + log::info!("sent ClientEvent::ListGames to Tauri client"); } } Response::Game(game) => log::debug!("game received: {game:?}"), diff --git a/crates/lanspread-db/Cargo.toml b/crates/lanspread-db/Cargo.toml index 3865984..b842446 100644 --- a/crates/lanspread-db/Cargo.toml +++ b/crates/lanspread-db/Cargo.toml @@ -13,6 +13,7 @@ unwrap_used = "warn" [dependencies] # external +bytes = { workspace = true } eyre = { workspace = true } semver = { workspace = true } serde = { workspace = true } diff --git a/crates/lanspread-db/src/db.rs b/crates/lanspread-db/src/db.rs index c5e2f99..6de0f7d 100644 --- a/crates/lanspread-db/src/db.rs +++ b/crates/lanspread-db/src/db.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, fmt}; +use bytes::Bytes; use serde::{Deserialize, Serialize}; /// A game @@ -26,6 +27,8 @@ pub struct Game { pub genre: String, /// size in bytes: example: 3455063152 pub size: u64, + /// thumbnail image + pub thumbnail: Option, } impl fmt::Debug for Game { @@ -103,7 +106,9 @@ impl GameDB { #[must_use] pub fn all_games(&self) -> Vec<&Game> { - self.games.values().collect() + let mut games: Vec<_> = self.games.values().collect(); + games.sort_by(|a, b| a.name.cmp(&b.name)); + games } } diff --git a/crates/lanspread-proto/src/lib.rs b/crates/lanspread-proto/src/lib.rs index 74ded32..b613b87 100644 --- a/crates/lanspread-proto/src/lib.rs +++ b/crates/lanspread-proto/src/lib.rs @@ -7,7 +7,7 @@ use tracing::error; pub enum Request { ListGames, GetGame { id: String }, - Invalid(Vec, String), + Invalid(Bytes, String), } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -15,29 +15,29 @@ pub enum Response { Games(Vec), Game(Game), GameNotFound(String), - InvalidRequest(Vec, String), + InvalidRequest(Bytes, String), EncodingError(String), - DecodingError(Vec, String), + DecodingError(Bytes, String), } // Add Message trait pub trait Message { - fn decode(bytes: &[u8]) -> Self; + fn decode(bytes: Bytes) -> Self; fn encode(&self) -> Bytes; } // Implement for Request impl Message for Request { - fn decode(bytes: &[u8]) -> Self { - match serde_json::from_slice(bytes) { + fn decode(bytes: Bytes) -> Self { + match serde_json::from_slice(&bytes) { Ok(t) => t, Err(e) => { tracing::error!( "got invalid request from client (error: {}): {}", e, - String::from_utf8_lossy(bytes) + String::from_utf8_lossy(&bytes) ); - Request::Invalid(bytes.into(), e.to_string()) + Request::Invalid(bytes, e.to_string()) } } } @@ -55,10 +55,10 @@ impl Message for Request { // Implement for Response impl Message for Response { - fn decode(bytes: &[u8]) -> Self { - match serde_json::from_slice(bytes) { + fn decode(bytes: Bytes) -> Self { + match serde_json::from_slice(&bytes) { Ok(t) => t, - Err(e) => Response::DecodingError(bytes.into(), e.to_string()), + Err(e) => Response::DecodingError(bytes, e.to_string()), } } diff --git a/crates/lanspread-server/src/assets.rs b/crates/lanspread-server/src/assets.rs new file mode 100644 index 0000000..fb1d007 --- /dev/null +++ b/crates/lanspread-server/src/assets.rs @@ -0,0 +1,24 @@ +use std::path::PathBuf; + +use bytes::Bytes; + +pub(crate) struct Thumbnails { + folder: PathBuf, +} + +impl Thumbnails { + pub(crate) fn new(folder: PathBuf) -> Thumbnails { + Thumbnails { folder } + } + + pub(crate) fn get(&self, path: &str) -> Option { + let asset = self.folder.join(format!("{path}.jpg")); + + if let Ok(data) = std::fs::read(asset) { + return Some(Bytes::from(data)); + } + + tracing::warn!("Thumbnail not found: {path}"); + None + } +} diff --git a/crates/lanspread-server/src/main.rs b/crates/lanspread-server/src/main.rs index 0946737..b473459 100644 --- a/crates/lanspread-server/src/main.rs +++ b/crates/lanspread-server/src/main.rs @@ -6,6 +6,7 @@ use std::{ sync::Arc, }; +use assets::Thumbnails; use clap::Parser; use lanspread_compat::eti::{self, EtiGame}; use lanspread_db::db::{Game, GameDB}; @@ -21,10 +22,12 @@ use s2n_quic::Server as QuicServer; use tokio::{io::AsyncWriteExt, sync::Mutex}; use tracing_subscriber::EnvFilter; +mod assets; + static KEY_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../key.pem")); static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem")); -pub(crate) struct Server; +struct Server; #[derive(Clone, Debug)] struct ServerCtx { @@ -73,7 +76,7 @@ impl Server { String::from_utf8_lossy(&data) ); - let request = Request::decode(&data); + let request = Request::decode(data); tracing::debug!( "{} client request (decoded): {:?}", conn_ctx.remote_addr, @@ -160,6 +163,12 @@ struct Cli { /// Game database path (SQLite). #[clap(long)] db: PathBuf, + /// Games folder. + #[clap(long)] + folder: PathBuf, + /// Thumbnails folder. + #[clap(long)] + thumbnails: PathBuf, } fn eti_game_to_game(eti_game: EtiGame) -> Game { @@ -175,6 +184,7 @@ fn eti_game_to_game(eti_game: EtiGame) -> Game { version: eti_game.game_version, genre: eti_game.genre_de, size: (eti_game.game_size * 1024.0 * 1024.0 * 1024.0) as u64, + thumbnail: None, } } @@ -187,7 +197,18 @@ async fn main() -> eyre::Result<()> { let cli = Cli::parse(); let eti_games = eti::get_games(&cli.db).await?; - let games: Vec = eti_games.into_iter().map(eti_game_to_game).collect(); + let mut games: Vec = eti_games.into_iter().map(eti_game_to_game).collect(); + let thumbnails = Thumbnails::new(cli.thumbnails); + + // add thumbnails to games + for game in &mut games { + if let Some(thumbnail) = thumbnails.get(&game.id) { + game.thumbnail = Some(thumbnail); + } else { + tracing::warn!("No thumbnail found: {}", game.id); + } + } + let game_db = GameDB::from(games); let mdns = MdnsAdvertiser::new( 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 0cb0036..1bc2d53 100644 --- a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs +++ b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs @@ -2,7 +2,7 @@ use std::net::SocketAddr; use lanspread_client::{ClientCommand, ClientEvent}; use lanspread_mdns::{discover_service, LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE}; -use tauri::{AppHandle, Emitter as _, Manager}; +use tauri::{AppHandle, Emitter as _, Listener as _, Manager}; use tokio::sync::{mpsc::UnboundedSender, Mutex}; // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ @@ -57,7 +57,6 @@ async fn find_server(app: AppHandle) { .client_ctrl .send(ClientCommand::ServerAddr(server_addr)) .unwrap(); - request_games(state); break; } Err(e) => { @@ -75,7 +74,7 @@ pub fn run() { tauri_plugin_log::TargetKind::Stdout, )) .level(log::LevelFilter::Info) - .level_for("lanspread_client", log::LevelFilter::Trace) + .level_for("lanspread_client", log::LevelFilter::Debug) .level_for("lanspread_tauri_leptos_lib", log::LevelFilter::Debug) .level_for("mdns_sd::service_daemon", log::LevelFilter::Off); @@ -116,7 +115,6 @@ pub fn run() { log::trace!("client event ListGames iter: {game:?}"); } - tokio::time::sleep(std::time::Duration::from_secs(1)).await; if let Err(e) = app_handle.emit("games-list-updated", Some(games)) { log::error!("Failed to emit games-list-updated event: {e}"); } else { diff --git a/crates/lanspread-tauri-deno-ts/src/App.css b/crates/lanspread-tauri-deno-ts/src/App.css index 1098b59..3aa9c87 100644 --- a/crates/lanspread-tauri-deno-ts/src/App.css +++ b/crates/lanspread-tauri-deno-ts/src/App.css @@ -6,7 +6,7 @@ body { .grid-container { display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); /* Changed from 220px to 140px */ gap: 20px; padding: 20px; } @@ -20,6 +20,7 @@ body { transition: background 0.3s; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); cursor: pointer; + /* max-width: 280px; */ } .item:hover { @@ -27,9 +28,11 @@ body { } .item img { - width: 100%; - height: 150px; + width: 280px; /* Fixed width */ + height: 200px; /* Fixed height */ object-fit: cover; + display: block; /* Removes any unwanted spacing */ + margin: 0 auto; /* Centers the image if container is wider */ } .item-name { @@ -93,3 +96,31 @@ body { 50% { opacity: 0.8; } 100% { opacity: 1; } } + +.search-container { + padding: 20px; + display: flex; + justify-content: center; +} + +.search-input { + width: 100%; + max-width: 400px; + padding: 10px 15px; + font-size: 16px; + color: #D5DBFE; + background: #000938; + border: 1px solid #444; + border-radius: 25px; + outline: none; + transition: all 0.3s ease; +} + +.search-input:focus { + border-color: #4866b9; + box-shadow: 0 0 10px rgba(0, 191, 255, 0.2); +} + +.search-input::placeholder { + color: #8892b0; +} diff --git a/crates/lanspread-tauri-deno-ts/src/App.tsx b/crates/lanspread-tauri-deno-ts/src/App.tsx index 60cd188..ab194bb 100644 --- a/crates/lanspread-tauri-deno-ts/src/App.tsx +++ b/crates/lanspread-tauri-deno-ts/src/App.tsx @@ -1,95 +1,112 @@ -// types.ts -interface Game { - id: string; - name: string; - description: string; - size: number; -} - -interface RunGameArgs { - id: string; -} - -// App.tsx -import { useEffect, useState } from 'react'; -import { invoke } from '@tauri-apps/api/core'; -import { listen } from '@tauri-apps/api/event'; +import {useEffect, useState} from 'react'; +import {invoke} from '@tauri-apps/api/core'; +import {listen} from '@tauri-apps/api/event'; import "./App.css"; +interface Game { + id: string; + name: string; + description: string; + size: number; + thumbnail: Uint8Array; +} + const App = () => { - const [gameItems, setGameItems] = useState([]); + const [gameItems, setGameItems] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); - useEffect(() => { - // Listen for games list updates - const setupEventListener = async () => { - try { - const unlisten = await listen('games-list-updated', (event) => { - console.log('Received games-list-updated event'); - const games = event.payload as Game[]; - games.forEach(game => { - console.log(`game: ${JSON.stringify(game)}`); - }); - setGameItems(games); - }); + const filteredGames = gameItems.filter(item => + item.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); - // Cleanup listener on component unmount - return () => { - unlisten(); + useEffect(() => { + console.log('๐Ÿ”ต Effect starting - setting up listener and requesting games'); + + let isSubscribed = true; // For tracking if component is still mounted + + const setupEventListener = async () => { + try { + const unlisten = await listen('games-list-updated', (event) => { + if (!isSubscribed) return; // Don't update state if unmounted + + console.log('๐Ÿ“ฅ Received games-list-updated event'); + const games = event.payload as Game[]; + games.forEach(game => { + console.log(`๐ŸŽฎ game: ${JSON.stringify(game.id)}`); + }); + setGameItems(games); + }); + + // Initial request for games + console.log('๐Ÿ“ค Requesting initial games list'); + await invoke('request_games'); + + // Cleanup function + return () => { + console.log('๐Ÿงน Cleaning up - removing listener'); + isSubscribed = false; + unlisten(); + }; + } catch (error) { + console.error('โŒ Error in setup:', error); + } }; - } catch (error) { - console.error('Error setting up event listener:', error); - } + + setupEventListener(); + + // Cleanup + return () => { + console.log('๐Ÿšซ Effect cleanup - component unmounting'); + isSubscribed = false; + }; + }, []); // Empty dependency array means this runs once on mount + + const runGame = async (id: string) => { + console.log(`๐ŸŽฏ Running game with id=${id}`); + try { + const result = await invoke('run_game_backend', {id}); + console.log(`โœ… Game started, result=${result}`); + } catch (error) { + console.error('โŒ Error running game:', error); + } }; - setupEventListener(); - - // Uncomment if you want to request games on mount - // const requestGames = async () => { - // try { - // await invoke('request_games'); - // } catch (error) { - // console.error('Error requesting games:', error); - // } - // }; - // requestGames(); - }, []); - - const runGame = async (id: string) => { - console.log(`id=${id}`); - try { - const result = await invoke('run_game_backend', { id }); - console.log(`id=${result}`); - } catch (error) { - console.error('Error running game:', error); - } - }; - - return ( -
-

SoftLAN Launcher

-
HEADER
-
- {gameItems.map((item) => ( -
runGame(item.id)} - > - Item Image -
{item.name}
-
- {item.description} - {item.size.toString()} + // Rest of your component remains the same + return ( +
+

SoftLAN Launcher

+
+ {/* Search input */} +
+ setSearchTerm(e.target.value)} + className="search-input" + /> +
+
+ {filteredGames.map((item) => { + // Convert the thumbnail bytes to base64 + const uint8Array = new Uint8Array(item.thumbnail); + 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()} +
+
Play
+
+ ); + })}
-
Play
-
- ))} -
-
- ); + + ); }; export default App; diff --git a/crates/lanspread-tauri-deno-ts/src/main.tsx b/crates/lanspread-tauri-deno-ts/src/main.tsx index 2be325e..278dedf 100644 --- a/crates/lanspread-tauri-deno-ts/src/main.tsx +++ b/crates/lanspread-tauri-deno-ts/src/main.tsx @@ -1,9 +1,6 @@ -import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - , );