168 lines
6.2 KiB
TypeScript
168 lines
6.2 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';
|
|
|
|
interface Game {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
size: number;
|
|
thumbnail: Uint8Array;
|
|
installed: boolean;
|
|
}
|
|
|
|
|
|
|
|
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(() => {
|
|
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 events that update the game list
|
|
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();
|
|
});
|
|
|
|
// 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();
|
|
};
|
|
} 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_backend', {id});
|
|
console.log(`✅ Game started, result=${result}`);
|
|
} catch (error) {
|
|
console.error('❌ Error running 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">
|
|
<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>
|
|
</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" 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">{item.installed ? 'Play' : 'Install'}</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</main>
|
|
);
|
|
};
|
|
|
|
export default App;
|