256 lines
10 KiB
TypeScript

import { useEffect, 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';
// enum with install status
enum InstallStatus {
NotInstalled = 'NotInstalled',
CheckingServer = 'CheckingServer',
Downloading = 'Downloading',
Unpacking = 'Unpacking',
Installed = 'Installed',
}
interface Game {
id: string;
name: string;
description: string;
size: number;
thumbnail: Uint8Array;
installed: boolean;
install_status: InstallStatus;
}
const App = () => {
const [gameItems, setGameItems] = useState<Game[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [gameDir, setGameDir] = useState('');
const filteredGames = gameItems.filter(item =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
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<string>(GAME_DIR_KEY);
if (savedGameDir) {
setGameDir(savedGameDir);
}
};
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`);
console.log('Current gameDir in listener:', gameDir); // Add this log
setGameItems(prev => prev.map(item => item.id === game_id
? {...item, install_status: InstallStatus.Installed}
: 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(games);
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');
// 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...`);
// 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,
directory: true,
});
if (file) {
setGameDir(file);
}
};
// Rest of your component remains the same
return (
<main className="container">
<div className="fixed-header">
<h1 className="align-center">SoftLAN Launcher</h1>
<div className="main-header">
{gameItems.length > 0 ? (
<div className="search-settings-wrapper">
<div></div>
<div className="search-container">
<input
type="text"
placeholder="Search games..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
</div>
<div className="settings-container">
<button onClick={dialogGameDir} className="settings-button">Set Game Directory</button>
<span className="settings-text">{gameDir}</span>
</div>
</div>
) : (
<div className="search-container">
Waiting for connection to server...
</div>
)}
</div>
</div>
<div className="grid-container">
{filteredGames.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 (
<div key={item.id} className="item">
<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 / 1024 / 1024 / 1024).toFixed(1)} GB</span>
</div>
<div className="play-button"
onClick={() => 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'}
</div>
</div>
);
})}
</div>
</main>
);
};
export default App;