import { useEffect, useRef, useState } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; import { open } from '@tauri-apps/plugin-dialog'; import { load } from '@tauri-apps/plugin-store'; import "./App.css"; const FILE_STORAGE = 'launcher-settings.json'; const GAME_DIR_KEY = 'game-directory'; const CHECKING_PEERS_TIMEOUT_MS = 5000; // enum with install status enum InstallStatus { NotInstalled = 'NotInstalled', CheckingPeers = 'CheckingPeers', Downloading = 'Downloading', Unpacking = 'Unpacking', Installed = 'Installed', } type StatusLevel = 'info' | 'error'; type GameFilter = 'all' | 'available' | 'installed'; interface Game { id: string; name: string; description: string; size: number; thumbnail: Uint8Array | number[]; installed: boolean; install_status: InstallStatus; eti_game_version?: string; local_version?: string; status_message?: string; status_level?: StatusLevel; peer_count: number; } const App = () => { const [gameItems, setGameItems] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const [gameDir, setGameDir] = useState(''); const [currentFilter, setCurrentFilter] = useState('available'); const checkingPeersTimeouts = useRef>>({}); const getFilteredGames = (games: Game[], filter: GameFilter): Game[] => { switch (filter) { case 'available': return games.filter(game => game.peer_count > 0); case 'installed': return games.filter(game => game.installed); case 'all': default: return games; } }; const filteredAndSearchedGames = getFilteredGames(gameItems, currentFilter).filter(item => item.name.toLowerCase().includes(searchTerm.toLowerCase()) ); const clearCheckingPeersTimeout = (gameId: string) => { const timeoutId = checkingPeersTimeouts.current[gameId]; if (timeoutId !== undefined) { clearTimeout(timeoutId); delete checkingPeersTimeouts.current[gameId]; } }; const scheduleCheckingPeersFallback = (gameId: string, fallbackMessage?: string, fallbackLevel?: StatusLevel) => { clearCheckingPeersTimeout(gameId); checkingPeersTimeouts.current[gameId] = setTimeout(() => { setGameItems(prev => prev.map(item => { if (item.id !== gameId || item.install_status !== InstallStatus.CheckingPeers) { return item; } return { ...item, install_status: item.installed ? InstallStatus.Installed : InstallStatus.NotInstalled, status_message: fallbackMessage ?? 'No peers currently have this game.', status_level: fallbackLevel ?? 'error', }; })); delete checkingPeersTimeouts.current[gameId]; }, CHECKING_PEERS_TIMEOUT_MS); }; useEffect(() => { return () => { Object.values(checkingPeersTimeouts.current).forEach(clearTimeout); checkingPeersTimeouts.current = {}; }; }, []); const getInitialGameDir = async () => { // update game directory from storage (if exists) // only if it's not already set await new Promise(resolve => setTimeout(resolve, 1000)); const store = await load(FILE_STORAGE, { autoSave: true }); const savedGameDir = await store.get(GAME_DIR_KEY); if (savedGameDir) { setGameDir(savedGameDir); } }; useEffect(() => { // Listen for game-download-failed events specifically const setupDownloadFailedListener = async () => { const unlisten = await listen('game-download-failed', (event) => { const game_id = event.payload as string; console.log(`โŒ game-download-failed ${game_id} event received`); clearCheckingPeersTimeout(game_id); setGameItems(prev => prev.map(item => item.id === game_id ? { ...item, install_status: item.installed ? InstallStatus.Installed : InstallStatus.NotInstalled, status_message: 'Download failed. Please try again.', status_level: 'error', } : item)); // Convert to string explicitly and verify it's not empty const pathString = String(gameDir); if (!pathString) { console.error('gameDir is empty before invoke!'); return; } invoke('update_game_directory', { path: pathString }) .catch(error => console.error('โŒ Error updating game directory:', error)); }); return unlisten; }; const setupNoPeersListener = async () => { const unlisten = await listen('game-no-peers', (event) => { const game_id = event.payload as string; console.log(`โš ๏ธ game-no-peers ${game_id} event received`); clearCheckingPeersTimeout(game_id); setGameItems(prev => prev.map(item => item.id === game_id ? { ...item, install_status: item.installed ? InstallStatus.Installed : InstallStatus.NotInstalled, status_message: 'No peers currently have this game.', status_level: 'error', } : item)); }); return unlisten; }; setupDownloadFailedListener(); setupNoPeersListener(); }, [gameDir]); useEffect(() => { // Listen for game-unpack-finished events specifically const setupUnpackListener = async () => { const unlisten = await listen('game-unpack-finished', (event) => { const game_id = event.payload as string; console.log(`๐Ÿ—ฒ game-unpack-finished ${game_id} event received`); clearCheckingPeersTimeout(game_id); setGameItems(prev => prev.map(item => item.id === game_id ? { ...item, install_status: InstallStatus.Installed, status_message: undefined, status_level: undefined, } : item)); // Convert to string explicitly and verify it's not empty const pathString = String(gameDir); if (!pathString) { console.error('gameDir is empty before invoke!'); return; } invoke('update_game_directory', { path: pathString }) .catch(error => console.error('โŒ Error updating game directory:', error)); }); return unlisten; }; setupUnpackListener(); }, [gameDir]); useEffect(() => { if (gameDir) { // store game directory in persistent storage const updateStorage = async (game_dir: string) => { try { const store = await load(FILE_STORAGE, { autoSave: true }); await store.set(GAME_DIR_KEY, game_dir); console.info(`๐Ÿ“ฆ Storage updated with game directory: ${game_dir}`); } catch (error) { console.error('โŒ Error updating storage:', error); } }; updateStorage(gameDir); console.log(`๐Ÿ“‚ Game directory changed to: ${gameDir}`); invoke('update_game_directory', { path: gameDir }) .catch(error => console.error('โŒ Error updating game directory:', error)); } }, [gameDir]); useEffect(() => { console.log('๐Ÿ”ต Effect starting - setting up listener and requesting games'); const setupEventListener = async () => { try { // 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[]; console.log(`๐ŸŽฎ ${games.length} Games received`); setGameItems(prev => { const previousById = new Map(prev.map(item => [item.id, item])); return games.map(game => { const previous = previousById.get(game.id); const installStatus = previous?.install_status ?? (game.installed ? InstallStatus.Installed : InstallStatus.NotInstalled); return { ...game, install_status: installStatus, status_message: previous?.status_message, status_level: previous?.status_level, peer_count: game.peer_count ?? 0, // Ensure peer_count is always set }; }); }); 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`); clearCheckingPeersTimeout(game_id); setGameItems(prev => prev.map(item => item.id === game_id ? { ...item, install_status: InstallStatus.Downloading, status_message: undefined, status_level: undefined, } : 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`); clearCheckingPeersTimeout(game_id); setGameItems(prev => prev.map(item => item.id === game_id ? { ...item, install_status: InstallStatus.Unpacking, status_message: undefined, status_level: undefined, } : item)); }); // Initial request for games console.log('๐Ÿ“ค Requesting initial games list'); await invoke('request_games'); // Cleanup function 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); } }; setupEventListener(); // Cleanup return () => { console.log('๐Ÿšซ Effect cleanup - component unmounting'); }; }, []); // 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', { 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...`); let fallbackMessage: string | undefined; let fallbackLevel: StatusLevel | undefined; // update install status in gameItems for this game setGameItems(prev => prev.map(item => { if (item.id === id) { fallbackMessage = item.status_message; fallbackLevel = item.status_level; return { ...item, install_status: InstallStatus.CheckingPeers, }; } return item; })); scheduleCheckingPeersFallback(id, fallbackMessage, fallbackLevel); } 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 updateGame = async (id: string) => { console.log(`๐ŸŽฏ Updating game with id=${id}`); try { const success = await invoke('update_game', { id }); if (success) { console.log(`โœ… Game update for id=${id} started...`); let fallbackMessage: string | undefined; let fallbackLevel: StatusLevel | undefined; // update install status in gameItems for this game setGameItems(prev => prev.map(item => { if (item.id === id) { fallbackMessage = item.status_message; fallbackLevel = item.status_level; return { ...item, install_status: InstallStatus.CheckingPeers, }; } return item; })); scheduleCheckingPeersFallback(id, fallbackMessage, fallbackLevel); } else { // game is already being installed/updated console.warn(`๐Ÿšง Game with id=${id} is already being updated`); } } catch (error) { console.error('โŒ Error updating game:', error); } }; const needsUpdate = (game: Game): boolean => { if (!game.installed) return false; // Check if peers have a version and we have a local version const peerVersion = game.eti_game_version; const localVersion = game.local_version; // If we don't have local version but peers have one, we need update if (!localVersion && peerVersion) { return true; } // If we have both versions, compare them numerically if (localVersion && peerVersion) { const localNum = parseInt(localVersion, 10); const peerNum = parseInt(peerVersion, 10); return peerNum > localNum; } return false; }; const dialogGameDir = async () => { const file = await open({ multiple: false, directory: true, }); if (file) { setGameDir(file); } }; // Rest of your component remains the same return (

SoftLAN Launcher

{gameDir ? (
setSearchTerm(e.target.value)} className="search-input" />
{gameDir}
) : (
Please set a game directory to start scanning for games...
)}
{gameDir && filteredAndSearchedGames.length === 0 && gameItems.length === 0 ? (
Scanning for games in your directory...
) : gameDir && filteredAndSearchedGames.length === 0 && gameItems.length > 0 ? (
No games found matching your search and filters.
) : null} {filteredAndSearchedGames.map((item) => { 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 (
{`${item.name}
{item.name}
{item.description.slice(0, 10)} {(item.size / 1024 / 1024 / 1024).toFixed(1)} GB
{ if (!item.installed) { installGame(item.id); } else if (needsUpdate(item)) { updateGame(item.id); } else { runGame(item.id); } }}> {!item.installed ? item.install_status === InstallStatus.CheckingPeers ? 'Checking peers...' : item.install_status === InstallStatus.Downloading ? 'Downloading...' : item.install_status === InstallStatus.Unpacking ? 'Unpacking...' : 'Install' : needsUpdate(item) ? item.install_status === InstallStatus.CheckingPeers ? 'Checking peers...' : item.install_status === InstallStatus.Downloading ? 'Downloading...' : item.install_status === InstallStatus.Unpacking ? 'Unpacking...' : 'Update' : 'Play'}
{item.status_message && item.peer_count === 0 && !item.installed ? item.status_message : ''}
{item.peer_count > 0 && ( ๐Ÿ‘ฅ {item.peer_count} )}
); })}
); }; export default App;