[backup] games from server with images
This commit is contained in:
parent
5d45c4ce4b
commit
a6ed6e04fe
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -2303,6 +2303,7 @@ dependencies = [
|
|||||||
name = "lanspread-client"
|
name = "lanspread-client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
"clap",
|
"clap",
|
||||||
"eyre",
|
"eyre",
|
||||||
"lanspread-db",
|
"lanspread-db",
|
||||||
@ -2330,6 +2331,7 @@ dependencies = [
|
|||||||
name = "lanspread-db"
|
name = "lanspread-db"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
"eyre",
|
"eyre",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -12,7 +12,7 @@ members = [
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
bytes = "1.8"
|
bytes = { version = "1.8", features = ["serde"] }
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
eyre = "0.6"
|
eyre = "0.6"
|
||||||
itertools = "0.13"
|
itertools = "0.13"
|
||||||
|
@ -16,7 +16,9 @@ unwrap_used = "warn"
|
|||||||
lanspread-db = { path = "../lanspread-db" }
|
lanspread-db = { path = "../lanspread-db" }
|
||||||
lanspread-proto = { path = "../lanspread-proto" }
|
lanspread-proto = { path = "../lanspread-proto" }
|
||||||
lanspread-utils = { path = "../lanspread-utils" }
|
lanspread-utils = { path = "../lanspread-utils" }
|
||||||
|
|
||||||
# external
|
# external
|
||||||
|
bytes = { workspace = true }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
eyre = { workspace = true }
|
eyre = { workspace = true }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use std::{net::SocketAddr, time::Duration};
|
use std::{net::SocketAddr, time::Duration};
|
||||||
|
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
use lanspread_db::db::Game;
|
use lanspread_db::db::Game;
|
||||||
use lanspread_proto::{Message as _, Request, Response};
|
use lanspread_proto::{Message as _, Request, Response};
|
||||||
use lanspread_utils::maybe_addr;
|
use lanspread_utils::maybe_addr;
|
||||||
@ -69,7 +70,7 @@ pub async fn run(
|
|||||||
ClientCommand::ListGames => Request::ListGames,
|
ClientCommand::ListGames => Request::ListGames,
|
||||||
ClientCommand::GetGame(id) => Request::GetGame { id },
|
ClientCommand::GetGame(id) => Request::GetGame { id },
|
||||||
ClientCommand::ServerAddr(_) => Request::Invalid(
|
ClientCommand::ServerAddr(_) => Request::Invalid(
|
||||||
[].into(),
|
Bytes::new(),
|
||||||
"invalid control message (ServerAddr), should not happen".into(),
|
"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);
|
log::error!("failed to send request to server {:?}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut data: Vec<u8> = Vec::new();
|
let mut data = BytesMut::new();
|
||||||
while let Ok(Some(bytes)) = rx.receive().await {
|
while let Ok(Some(bytes)) = rx.receive().await {
|
||||||
data.extend_from_slice(&bytes);
|
data.extend_from_slice(&bytes);
|
||||||
}
|
}
|
||||||
log::debug!("{} bytes received from server", data.len());
|
log::debug!("{} bytes received from server", data.len());
|
||||||
log::trace!("server response (RAW): {}", String::from_utf8_lossy(&data));
|
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:?}");
|
log::trace!("server response (DECODED): {response:?}");
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
Response::Games(games) => {
|
Response::Games(games) => {
|
||||||
for game in &games {
|
for game in &games {
|
||||||
log::debug!("{game}");
|
log::trace!("{game}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = tx_notify_ui.send(ClientEvent::ListGames(games)) {
|
if let Err(e) = tx_notify_ui.send(ClientEvent::ListGames(games)) {
|
||||||
log::debug!("failed to send ClientEvent::ListGames to client {e:?}");
|
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:?}"),
|
Response::Game(game) => log::debug!("game received: {game:?}"),
|
||||||
|
@ -13,6 +13,7 @@ unwrap_used = "warn"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# external
|
# external
|
||||||
|
bytes = { workspace = true }
|
||||||
eyre = { workspace = true }
|
eyre = { workspace = true }
|
||||||
semver = { workspace = true }
|
semver = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
use std::{collections::HashMap, fmt};
|
use std::{collections::HashMap, fmt};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// A game
|
/// A game
|
||||||
@ -26,6 +27,8 @@ pub struct Game {
|
|||||||
pub genre: String,
|
pub genre: String,
|
||||||
/// size in bytes: example: 3455063152
|
/// size in bytes: example: 3455063152
|
||||||
pub size: u64,
|
pub size: u64,
|
||||||
|
/// thumbnail image
|
||||||
|
pub thumbnail: Option<Bytes>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Game {
|
impl fmt::Debug for Game {
|
||||||
@ -103,7 +106,9 @@ impl GameDB {
|
|||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn all_games(&self) -> Vec<&Game> {
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ use tracing::error;
|
|||||||
pub enum Request {
|
pub enum Request {
|
||||||
ListGames,
|
ListGames,
|
||||||
GetGame { id: String },
|
GetGame { id: String },
|
||||||
Invalid(Vec<u8>, String),
|
Invalid(Bytes, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
@ -15,29 +15,29 @@ pub enum Response {
|
|||||||
Games(Vec<Game>),
|
Games(Vec<Game>),
|
||||||
Game(Game),
|
Game(Game),
|
||||||
GameNotFound(String),
|
GameNotFound(String),
|
||||||
InvalidRequest(Vec<u8>, String),
|
InvalidRequest(Bytes, String),
|
||||||
EncodingError(String),
|
EncodingError(String),
|
||||||
DecodingError(Vec<u8>, String),
|
DecodingError(Bytes, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Message trait
|
// Add Message trait
|
||||||
pub trait Message {
|
pub trait Message {
|
||||||
fn decode(bytes: &[u8]) -> Self;
|
fn decode(bytes: Bytes) -> Self;
|
||||||
fn encode(&self) -> Bytes;
|
fn encode(&self) -> Bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement for Request
|
// Implement for Request
|
||||||
impl Message for Request {
|
impl Message for Request {
|
||||||
fn decode(bytes: &[u8]) -> Self {
|
fn decode(bytes: Bytes) -> Self {
|
||||||
match serde_json::from_slice(bytes) {
|
match serde_json::from_slice(&bytes) {
|
||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
"got invalid request from client (error: {}): {}",
|
"got invalid request from client (error: {}): {}",
|
||||||
e,
|
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
|
// Implement for Response
|
||||||
impl Message for Response {
|
impl Message for Response {
|
||||||
fn decode(bytes: &[u8]) -> Self {
|
fn decode(bytes: Bytes) -> Self {
|
||||||
match serde_json::from_slice(bytes) {
|
match serde_json::from_slice(&bytes) {
|
||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(e) => Response::DecodingError(bytes.into(), e.to_string()),
|
Err(e) => Response::DecodingError(bytes, e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
24
crates/lanspread-server/src/assets.rs
Normal file
24
crates/lanspread-server/src/assets.rs
Normal file
@ -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<Bytes> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ use std::{
|
|||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use assets::Thumbnails;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use lanspread_compat::eti::{self, EtiGame};
|
use lanspread_compat::eti::{self, EtiGame};
|
||||||
use lanspread_db::db::{Game, GameDB};
|
use lanspread_db::db::{Game, GameDB};
|
||||||
@ -21,10 +22,12 @@ use s2n_quic::Server as QuicServer;
|
|||||||
use tokio::{io::AsyncWriteExt, sync::Mutex};
|
use tokio::{io::AsyncWriteExt, sync::Mutex};
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
mod assets;
|
||||||
|
|
||||||
static KEY_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../key.pem"));
|
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"));
|
static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem"));
|
||||||
|
|
||||||
pub(crate) struct Server;
|
struct Server;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct ServerCtx {
|
struct ServerCtx {
|
||||||
@ -73,7 +76,7 @@ impl Server {
|
|||||||
String::from_utf8_lossy(&data)
|
String::from_utf8_lossy(&data)
|
||||||
);
|
);
|
||||||
|
|
||||||
let request = Request::decode(&data);
|
let request = Request::decode(data);
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"{} client request (decoded): {:?}",
|
"{} client request (decoded): {:?}",
|
||||||
conn_ctx.remote_addr,
|
conn_ctx.remote_addr,
|
||||||
@ -160,6 +163,12 @@ struct Cli {
|
|||||||
/// Game database path (SQLite).
|
/// Game database path (SQLite).
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
db: PathBuf,
|
db: PathBuf,
|
||||||
|
/// Games folder.
|
||||||
|
#[clap(long)]
|
||||||
|
folder: PathBuf,
|
||||||
|
/// Thumbnails folder.
|
||||||
|
#[clap(long)]
|
||||||
|
thumbnails: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eti_game_to_game(eti_game: EtiGame) -> Game {
|
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,
|
version: eti_game.game_version,
|
||||||
genre: eti_game.genre_de,
|
genre: eti_game.genre_de,
|
||||||
size: (eti_game.game_size * 1024.0 * 1024.0 * 1024.0) as u64,
|
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 cli = Cli::parse();
|
||||||
|
|
||||||
let eti_games = eti::get_games(&cli.db).await?;
|
let eti_games = eti::get_games(&cli.db).await?;
|
||||||
let games: Vec<Game> = eti_games.into_iter().map(eti_game_to_game).collect();
|
let mut games: Vec<Game> = 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 game_db = GameDB::from(games);
|
||||||
|
|
||||||
let mdns = MdnsAdvertiser::new(
|
let mdns = MdnsAdvertiser::new(
|
||||||
|
@ -2,7 +2,7 @@ use std::net::SocketAddr;
|
|||||||
|
|
||||||
use lanspread_client::{ClientCommand, ClientEvent};
|
use lanspread_client::{ClientCommand, ClientEvent};
|
||||||
use lanspread_mdns::{discover_service, LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE};
|
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};
|
use tokio::sync::{mpsc::UnboundedSender, Mutex};
|
||||||
|
|
||||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||||
@ -57,7 +57,6 @@ async fn find_server(app: AppHandle) {
|
|||||||
.client_ctrl
|
.client_ctrl
|
||||||
.send(ClientCommand::ServerAddr(server_addr))
|
.send(ClientCommand::ServerAddr(server_addr))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
request_games(state);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -75,7 +74,7 @@ pub fn run() {
|
|||||||
tauri_plugin_log::TargetKind::Stdout,
|
tauri_plugin_log::TargetKind::Stdout,
|
||||||
))
|
))
|
||||||
.level(log::LevelFilter::Info)
|
.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("lanspread_tauri_leptos_lib", log::LevelFilter::Debug)
|
||||||
.level_for("mdns_sd::service_daemon", log::LevelFilter::Off);
|
.level_for("mdns_sd::service_daemon", log::LevelFilter::Off);
|
||||||
|
|
||||||
@ -116,7 +115,6 @@ pub fn run() {
|
|||||||
log::trace!("client event ListGames iter: {game:?}");
|
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)) {
|
if let Err(e) = app_handle.emit("games-list-updated", Some(games)) {
|
||||||
log::error!("Failed to emit games-list-updated event: {e}");
|
log::error!("Failed to emit games-list-updated event: {e}");
|
||||||
} else {
|
} else {
|
||||||
|
@ -6,7 +6,7 @@ body {
|
|||||||
|
|
||||||
.grid-container {
|
.grid-container {
|
||||||
display: grid;
|
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;
|
gap: 20px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
@ -20,6 +20,7 @@ body {
|
|||||||
transition: background 0.3s;
|
transition: background 0.3s;
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
/* max-width: 280px; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.item:hover {
|
.item:hover {
|
||||||
@ -27,9 +28,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item img {
|
.item img {
|
||||||
width: 100%;
|
width: 280px; /* Fixed width */
|
||||||
height: 150px;
|
height: 200px; /* Fixed height */
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
display: block; /* Removes any unwanted spacing */
|
||||||
|
margin: 0 auto; /* Centers the image if container is wider */
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-name {
|
.item-name {
|
||||||
@ -93,3 +96,31 @@ body {
|
|||||||
50% { opacity: 0.8; }
|
50% { opacity: 0.8; }
|
||||||
100% { opacity: 1; }
|
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;
|
||||||
|
}
|
||||||
|
@ -1,92 +1,109 @@
|
|||||||
// types.ts
|
import {useEffect, useState} from 'react';
|
||||||
|
import {invoke} from '@tauri-apps/api/core';
|
||||||
|
import {listen} from '@tauri-apps/api/event';
|
||||||
|
import "./App.css";
|
||||||
|
|
||||||
interface Game {
|
interface Game {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
size: number;
|
size: number;
|
||||||
|
thumbnail: Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 "./App.css";
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [gameItems, setGameItems] = useState<Game[]>([]);
|
const [gameItems, setGameItems] = useState<Game[]>([]);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
const filteredGames = gameItems.filter(item =>
|
||||||
|
item.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Listen for games list updates
|
console.log('🔵 Effect starting - setting up listener and requesting games');
|
||||||
|
|
||||||
|
let isSubscribed = true; // For tracking if component is still mounted
|
||||||
|
|
||||||
const setupEventListener = async () => {
|
const setupEventListener = async () => {
|
||||||
try {
|
try {
|
||||||
const unlisten = await listen('games-list-updated', (event) => {
|
const unlisten = await listen('games-list-updated', (event) => {
|
||||||
console.log('Received 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[];
|
const games = event.payload as Game[];
|
||||||
games.forEach(game => {
|
games.forEach(game => {
|
||||||
console.log(`game: ${JSON.stringify(game)}`);
|
console.log(`🎮 game: ${JSON.stringify(game.id)}`);
|
||||||
});
|
});
|
||||||
setGameItems(games);
|
setGameItems(games);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cleanup listener on component unmount
|
// Initial request for games
|
||||||
|
console.log('📤 Requesting initial games list');
|
||||||
|
await invoke('request_games');
|
||||||
|
|
||||||
|
// Cleanup function
|
||||||
return () => {
|
return () => {
|
||||||
|
console.log('🧹 Cleaning up - removing listener');
|
||||||
|
isSubscribed = false;
|
||||||
unlisten();
|
unlisten();
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting up event listener:', error);
|
console.error('❌ Error in setup:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setupEventListener();
|
setupEventListener();
|
||||||
|
|
||||||
// Uncomment if you want to request games on mount
|
// Cleanup
|
||||||
// const requestGames = async () => {
|
return () => {
|
||||||
// try {
|
console.log('🚫 Effect cleanup - component unmounting');
|
||||||
// await invoke('request_games');
|
isSubscribed = false;
|
||||||
// } catch (error) {
|
};
|
||||||
// console.error('Error requesting games:', error);
|
}, []); // Empty dependency array means this runs once on mount
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// requestGames();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const runGame = async (id: string) => {
|
const runGame = async (id: string) => {
|
||||||
console.log(`id=${id}`);
|
console.log(`🎯 Running game with id=${id}`);
|
||||||
try {
|
try {
|
||||||
const result = await invoke('run_game_backend', { id });
|
const result = await invoke('run_game_backend', {id});
|
||||||
console.log(`id=${result}`);
|
console.log(`✅ Game started, result=${result}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error running game:', error);
|
console.error('❌ Error running game:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Rest of your component remains the same
|
||||||
return (
|
return (
|
||||||
<main className="container">
|
<main className="container">
|
||||||
<h1 className="align-center">SoftLAN Launcher</h1>
|
<h1 className="align-center">SoftLAN Launcher</h1>
|
||||||
<div className="main-header">HEADER</div>
|
<div className="main-header">
|
||||||
<div className="grid-container">
|
{/* Search input */}
|
||||||
{gameItems.map((item) => (
|
<div className="search-container">
|
||||||
<div
|
<input
|
||||||
key={item.id}
|
type="text"
|
||||||
className="item"
|
placeholder="Search games..."
|
||||||
onClick={() => runGame(item.id)}
|
value={searchTerm}
|
||||||
>
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
<img
|
className="search-input"
|
||||||
src="https://via.placeholder.com/200x150"
|
|
||||||
alt="Item Image"
|
|
||||||
/>
|
/>
|
||||||
|
</div></div>
|
||||||
|
<div className="grid-container">
|
||||||
|
{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 (
|
||||||
|
<div key={item.id} className="item" onClick={() => runGame(item.id)}>
|
||||||
|
<img src={thumbnailUrl} alt={`${item.name} thumbnail`} />
|
||||||
<div className="item-name">{item.name}</div>
|
<div className="item-name">{item.name}</div>
|
||||||
<div className="description">
|
<div className="description">
|
||||||
<span className="desc-text">{item.description}</span>
|
<span className="desc-text">{item.description.slice(0, 10)}</span>
|
||||||
<span className="size-text">{item.size.toString()}</span>
|
<span className="size-text">{item.size.toString()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="play-button">Play</div>
|
<div className="play-button">Play</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>,
|
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user