[improve] set game dir on client -> updates Play/Install button based on games existing
This commit is contained in:
@ -1,10 +1,12 @@
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
use std::{net::SocketAddr, time::Duration};
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use lanspread_db::db::Game;
|
||||
use lanspread_proto::{Message as _, Request, Response};
|
||||
use lanspread_utils::maybe_addr;
|
||||
use s2n_quic::{client::Connect, provider::limits::Limits, Client as QuicClient};
|
||||
use s2n_quic::{client::Connect, provider::limits::Limits, Client as QuicClient, Connection};
|
||||
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
|
||||
static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem"));
|
||||
@ -21,7 +23,39 @@ pub enum ClientCommand {
|
||||
ServerAddr(SocketAddr),
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
async fn initial_server_alive_check(conn: &mut Connection) -> bool {
|
||||
let stream = match conn.open_bidirectional_stream().await {
|
||||
Ok(stream) => stream,
|
||||
Err(e) => {
|
||||
log::error!("failed to open stream: {e}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let (mut rx, mut tx) = stream.split();
|
||||
|
||||
// send ping
|
||||
if let Err(e) = tx.send(Request::Ping.encode()).await {
|
||||
log::error!("failed to send ping to server: {e}");
|
||||
return false;
|
||||
}
|
||||
let _ = tx.close().await;
|
||||
|
||||
// receive pong
|
||||
if let Ok(Some(response)) = rx.receive().await {
|
||||
let response = Response::decode(response);
|
||||
if let Response::Pong = response {
|
||||
log::info!("server is alive");
|
||||
return true;
|
||||
}
|
||||
|
||||
log::error!("server sent invalid response to ping: {response:?}");
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn run(
|
||||
mut rx_control: UnboundedReceiver<ClientCommand>,
|
||||
tx_notify_ui: UnboundedSender<ClientEvent>,
|
||||
@ -58,6 +92,10 @@ pub async fn run(
|
||||
|
||||
conn.keep_alive(true)?;
|
||||
|
||||
if !initial_server_alive_check(&mut conn).await {
|
||||
continue;
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"connected: (server: {}) (client: {})",
|
||||
maybe_addr!(conn.remote_addr()),
|
||||
@ -131,6 +169,7 @@ pub async fn run(
|
||||
String::from_utf8_lossy(&data)
|
||||
);
|
||||
}
|
||||
Response::Pong => (), // ignore (should never happen)
|
||||
}
|
||||
|
||||
if let Err(err) = tx.close().await {
|
||||
|
@ -29,6 +29,8 @@ pub struct Game {
|
||||
pub size: u64,
|
||||
/// thumbnail image
|
||||
pub thumbnail: Option<Bytes>,
|
||||
/// only relevant for client (yeah... I know)
|
||||
pub installed: bool,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Game {
|
||||
@ -76,7 +78,7 @@ pub struct GameDB {
|
||||
|
||||
impl GameDB {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
pub fn empty() -> Self {
|
||||
GameDB {
|
||||
games: HashMap::new(),
|
||||
}
|
||||
@ -84,7 +86,7 @@ impl GameDB {
|
||||
|
||||
#[must_use]
|
||||
pub fn from(games: Vec<Game>) -> Self {
|
||||
let mut db = GameDB::new();
|
||||
let mut db = GameDB::empty();
|
||||
for game in games {
|
||||
db.games.insert(game.id.clone(), game);
|
||||
}
|
||||
@ -99,6 +101,14 @@ impl GameDB {
|
||||
self.games.get(id.as_ref())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_mut_game_by_id<S>(&mut self, id: S) -> Option<&mut Game>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
self.games.get_mut(id.as_ref())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_game_by_name(&self, name: &str) -> Option<&Game> {
|
||||
self.games.values().find(|game| game.name == name)
|
||||
@ -110,10 +120,16 @@ impl GameDB {
|
||||
games.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
games
|
||||
}
|
||||
|
||||
pub fn set_all_uninstalled(&mut self) {
|
||||
for game in self.games.values_mut() {
|
||||
game.installed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GameDB {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ use tracing::error;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum Request {
|
||||
Ping,
|
||||
ListGames,
|
||||
GetGame { id: String },
|
||||
Invalid(Bytes, String),
|
||||
@ -12,6 +13,7 @@ pub enum Request {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum Response {
|
||||
Pong,
|
||||
Games(Vec<Game>),
|
||||
Game(Game),
|
||||
GameNotFound(String),
|
||||
|
@ -116,6 +116,7 @@ impl RequestHandler {
|
||||
|
||||
async fn handle_request(&self, request: Request) -> Response {
|
||||
match request {
|
||||
Request::Ping => Response::Pong,
|
||||
Request::ListGames => {
|
||||
let db = self.db.lock().await;
|
||||
Response::Games(db.all_games().into_iter().cloned().collect())
|
||||
@ -172,6 +173,7 @@ fn eti_game_to_game(eti_game: EtiGame) -> Game {
|
||||
genre: eti_game.genre_de,
|
||||
size: (eti_game.game_size * 1024.0 * 1024.0 * 1024.0) as u64,
|
||||
thumbnail: None,
|
||||
installed: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
16
crates/lanspread-tauri-deno-ts/deno.lock
generated
16
crates/lanspread-tauri-deno-ts/deno.lock
generated
@ -3,7 +3,9 @@
|
||||
"specifiers": {
|
||||
"npm:@tauri-apps/api@2": "2.1.1",
|
||||
"npm:@tauri-apps/cli@2": "2.1.0",
|
||||
"npm:@tauri-apps/plugin-dialog@~2.0.1": "2.0.1",
|
||||
"npm:@tauri-apps/plugin-shell@2": "2.0.1",
|
||||
"npm:@tauri-apps/plugin-store@2.1": "2.1.0",
|
||||
"npm:@types/react-dom@^18.2.7": "18.3.1",
|
||||
"npm:@types/react@^18.2.15": "18.3.12",
|
||||
"npm:@vitejs/plugin-react@^4.2.1": "4.3.3_vite@5.4.11_@babel+core@7.26.0",
|
||||
@ -348,12 +350,24 @@
|
||||
"@tauri-apps/cli-win32-x64-msvc"
|
||||
]
|
||||
},
|
||||
"@tauri-apps/plugin-dialog@2.0.1": {
|
||||
"integrity": "sha512-fnUrNr6EfvTqdls/ufusU7h6UbNFzLKvHk/zTuOiBq01R3dTODqwctZlzakdbfSp/7pNwTKvgKTAgl/NAP/Z0Q==",
|
||||
"dependencies": [
|
||||
"@tauri-apps/api"
|
||||
]
|
||||
},
|
||||
"@tauri-apps/plugin-shell@2.0.1": {
|
||||
"integrity": "sha512-akU1b77sw3qHiynrK0s930y8zKmcdrSD60htjH+mFZqv5WaakZA/XxHR3/sF1nNv9Mgmt/Shls37HwnOr00aSw==",
|
||||
"dependencies": [
|
||||
"@tauri-apps/api"
|
||||
]
|
||||
},
|
||||
"@tauri-apps/plugin-store@2.1.0": {
|
||||
"integrity": "sha512-GADqrc17opUKYIAKnGHIUgEeTZ2wJGu1ZITKQ1WMuOFdv8fvXRFBAqsqPjE3opgWohbczX6e1NpwmZK1AnuWVw==",
|
||||
"dependencies": [
|
||||
"@tauri-apps/api"
|
||||
]
|
||||
},
|
||||
"@types/babel__core@7.20.5": {
|
||||
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
||||
"dependencies": [
|
||||
@ -604,7 +618,9 @@
|
||||
"dependencies": [
|
||||
"npm:@tauri-apps/api@2",
|
||||
"npm:@tauri-apps/cli@2",
|
||||
"npm:@tauri-apps/plugin-dialog@~2.0.1",
|
||||
"npm:@tauri-apps/plugin-shell@2",
|
||||
"npm:@tauri-apps/plugin-store@2.1",
|
||||
"npm:@types/react-dom@^18.2.7",
|
||||
"npm:@types/react@^18.2.15",
|
||||
"npm:@vitejs/plugin-react@^4.2.1",
|
||||
|
@ -10,6 +10,8 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/plugin-dialog": "~2.0.1",
|
||||
"@tauri-apps/plugin-store": "~2.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"@tauri-apps/api": "^2",
|
||||
|
@ -32,4 +32,6 @@ tauri-plugin-shell = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { workspace = true }
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-store = "2"
|
||||
|
||||
|
@ -2,9 +2,13 @@
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"shell:allow-open"
|
||||
"shell:allow-open",
|
||||
"dialog:default",
|
||||
"store:default"
|
||||
]
|
||||
}
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use lanspread_client::{ClientCommand, ClientEvent};
|
||||
use lanspread_db::db::{Game, GameDB};
|
||||
use lanspread_mdns::{discover_service, LANSPREAD_INSTANCE_NAME, LANSPREAD_SERVICE_TYPE};
|
||||
use tauri::{AppHandle, Emitter as _, Listener as _, Manager};
|
||||
use tauri::{AppHandle, Emitter as _, Manager};
|
||||
use tokio::sync::{mpsc::UnboundedSender, Mutex};
|
||||
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
@ -10,6 +15,7 @@ use tokio::sync::{mpsc::UnboundedSender, Mutex};
|
||||
struct LanSpreadState {
|
||||
server_addr: Mutex<Option<SocketAddr>>,
|
||||
client_ctrl: UnboundedSender<ClientCommand>,
|
||||
games: Arc<Mutex<GameDB>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -40,6 +46,67 @@ fn run_game_backend(id: String, state: tauri::State<LanSpreadState>) -> String {
|
||||
// }
|
||||
}
|
||||
|
||||
fn set_game_install_state_from_path(game_db: &mut GameDB, path: &Path, installed: bool) {
|
||||
if let Some(file_name) = path.file_name() {
|
||||
if let Some(file_name) = file_name.to_str() {
|
||||
if let Some(game) = game_db.get_mut_game_by_id(file_name) {
|
||||
if installed {
|
||||
log::error!("Game is installed: {game}");
|
||||
} else {
|
||||
log::error!("Game is missing: {game}");
|
||||
}
|
||||
game.installed = installed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn update_game_directory(app_handle: tauri::AppHandle, path: String) {
|
||||
let path = PathBuf::from(path);
|
||||
if !path.exists() {
|
||||
log::error!("game dir {path:?} does not exist");
|
||||
}
|
||||
|
||||
let entries = match path.read_dir() {
|
||||
Ok(entries) => entries,
|
||||
Err(e) => {
|
||||
log::error!("Failed to read game dir: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mut game_db = app_handle
|
||||
.state::<LanSpreadState>()
|
||||
.inner()
|
||||
.games
|
||||
.lock()
|
||||
.await;
|
||||
|
||||
// Reset all games to uninstalled
|
||||
game_db.set_all_uninstalled();
|
||||
|
||||
// update game_db with installed games from real game directory
|
||||
entries.into_iter().for_each(|entry| {
|
||||
if let Ok(entry) = entry {
|
||||
if let Ok(path_type) = entry.file_type() {
|
||||
if path_type.is_dir() {
|
||||
let path = entry.path();
|
||||
if path.join(".softlan_game_installed").exists() {
|
||||
set_game_install_state_from_path(&mut game_db, &path, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Err(e) = app_handle.emit("games-list-updated", Some(game_db.all_games())) {
|
||||
log::error!("Failed to emit games-list-updated event: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn find_server(app: AppHandle) {
|
||||
log::info!("Looking for server...");
|
||||
|
||||
@ -57,6 +124,7 @@ async fn find_server(app: AppHandle) {
|
||||
.client_ctrl
|
||||
.send(ClientCommand::ServerAddr(server_addr))
|
||||
.unwrap();
|
||||
request_games(state);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
@ -66,6 +134,25 @@ async fn find_server(app: AppHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_game_db(games: Vec<Game>, app: AppHandle) {
|
||||
for game in &games {
|
||||
log::trace!("client event ListGames iter: {game:?}");
|
||||
}
|
||||
|
||||
let state = app.state::<LanSpreadState>();
|
||||
|
||||
// Store games list
|
||||
let mut state_games = state.games.lock().await;
|
||||
*state_games = GameDB::from(games.clone());
|
||||
|
||||
// Tell Frontend about new games list
|
||||
if let Err(e) = app.emit("games-list-updated", Some(games)) {
|
||||
log::error!("Failed to emit games-list-updated event: {e}");
|
||||
} else {
|
||||
log::info!("Emitted games-list-updated event");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let tauri_logger_builder = tauri_plugin_log::Builder::new()
|
||||
@ -89,12 +176,19 @@ pub fn run() {
|
||||
let lanspread_state = LanSpreadState {
|
||||
server_addr: Mutex::new(None),
|
||||
client_ctrl: tx_client_control,
|
||||
games: Arc::new(Mutex::new(GameDB::empty())),
|
||||
};
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_logger_builder.build())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.invoke_handler(tauri::generate_handler![run_game_backend, request_games])
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
run_game_backend,
|
||||
request_games,
|
||||
update_game_directory
|
||||
])
|
||||
.manage(lanspread_state)
|
||||
.setup(|app| {
|
||||
let app_handle = app.handle().clone();
|
||||
@ -110,16 +204,7 @@ pub fn run() {
|
||||
match event {
|
||||
ClientEvent::ListGames(games) => {
|
||||
log::debug!("Received client event: ListGames");
|
||||
|
||||
for game in &games {
|
||||
log::trace!("client event ListGames iter: {game:?}");
|
||||
}
|
||||
|
||||
if let Err(e) = app_handle.emit("games-list-updated", Some(games)) {
|
||||
log::error!("Failed to emit games-list-updated event: {e}");
|
||||
} else {
|
||||
log::info!("Emitted games-list-updated event");
|
||||
}
|
||||
update_game_db(games, app_handle.clone()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -102,13 +102,6 @@ h1.align-center {
|
||||
box-shadow: 0 8px 15px rgba(0, 191, 255, 0.2);
|
||||
}
|
||||
|
||||
/* .play-button:hover {
|
||||
background: linear-gradient(45deg, #09305a, #4866b9);
|
||||
/* box-shadow: 0 15px 20px rgba(0, 191, 255, 0.4); */
|
||||
/* box-shadow: 0 20px 25px rgba(0, 191, 255, 0.6), 0 0 15px rgba(0, 191, 255, 0.5);
|
||||
transform: translateY(-5px);
|
||||
} */
|
||||
|
||||
.play-button:hover {
|
||||
background: linear-gradient(45deg, #09305a, #4866b9);
|
||||
box-shadow: 0 20px 25px rgba(0, 191, 255, 0.6);
|
||||
@ -124,7 +117,6 @@ h1.align-center {
|
||||
}
|
||||
|
||||
.search-container {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
@ -150,3 +142,45 @@ h1.align-center {
|
||||
.search-input::placeholder {
|
||||
color: #8892b0;
|
||||
}
|
||||
|
||||
.search-settings-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
padding: 8px 16px;
|
||||
background: linear-gradient(45deg, #09305a, #37529c);
|
||||
color: #D5DBFE;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 8px rgba(0, 191, 255, 0.2);
|
||||
}
|
||||
|
||||
.settings-button:hover {
|
||||
background: linear-gradient(45deg, #09305a, #4866b9);
|
||||
box-shadow: 0 8px 12px rgba(0, 191, 255, 0.4);
|
||||
border: 1px solid rgba(0, 191, 255, 0.6);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.settings-text {
|
||||
color: #8892b0;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
@ -1,40 +1,78 @@
|
||||
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');
|
||||
|
||||
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');
|
||||
// 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[];
|
||||
games.forEach(game => {
|
||||
console.log(`🎮 game: ${JSON.stringify(game.id)}`);
|
||||
});
|
||||
console.log(`🎮 ${games.length} Games received`);
|
||||
setGameItems(games);
|
||||
getInitialGameDir();
|
||||
});
|
||||
|
||||
// Initial request for games
|
||||
@ -44,8 +82,7 @@ const App = () => {
|
||||
// Cleanup function
|
||||
return () => {
|
||||
console.log('🧹 Cleaning up - removing listener');
|
||||
isSubscribed = false;
|
||||
unlisten();
|
||||
unlisten_games();
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Error in setup:', error);
|
||||
@ -57,7 +94,6 @@ const App = () => {
|
||||
// Cleanup
|
||||
return () => {
|
||||
console.log('🚫 Effect cleanup - component unmounting');
|
||||
isSubscribed = false;
|
||||
};
|
||||
}, []); // Empty dependency array means this runs once on mount
|
||||
|
||||
@ -71,20 +107,38 @@ const App = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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-container">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search games..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
<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>
|
||||
@ -101,7 +155,7 @@ const App = () => {
|
||||
<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 className="play-button">{item.installed ? 'Play' : 'Install'}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
Reference in New Issue
Block a user