[backup] games from server with images
This commit is contained in:
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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<Game[]>([]);
|
||||
const [gameItems, setGameItems] = useState<Game[]>([]);
|
||||
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 (
|
||||
<main className="container">
|
||||
<h1 className="align-center">SoftLAN Launcher</h1>
|
||||
<div className="main-header">HEADER</div>
|
||||
<div className="grid-container">
|
||||
{gameItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="item"
|
||||
onClick={() => runGame(item.id)}
|
||||
>
|
||||
<img
|
||||
src="https://via.placeholder.com/200x150"
|
||||
alt="Item Image"
|
||||
/>
|
||||
<div className="item-name">{item.name}</div>
|
||||
<div className="description">
|
||||
<span className="desc-text">{item.description}</span>
|
||||
<span className="size-text">{item.size.toString()}</span>
|
||||
// Rest of your component remains the same
|
||||
return (
|
||||
<main className="container">
|
||||
<h1 className="align-center">SoftLAN Launcher</h1>
|
||||
<div className="main-header">
|
||||
{/* Search input */}
|
||||
<div className="search-container">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search games..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
</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="description">
|
||||
<span className="desc-text">{item.description.slice(0, 10)}</span>
|
||||
<span className="size-text">{item.size.toString()}</span>
|
||||
</div>
|
||||
<div className="play-button">Play</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="play-button">Play</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
@ -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(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
Reference in New Issue
Block a user