From 31ace174e35ce566400a129a95d3e5d7d02f6dcb Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 21 May 2026 19:14:11 +0200 Subject: [PATCH] fix(ui): treat missing game folders as unset Validate the persisted game directory before sending it to the backend or showing library content for it. When the saved path no longer exists, the launcher keeps the top bar visible but shows the folder picker empty state and labels the Game Folder button as an unset folder. This keeps stale local data from being presented as the active library when an old path is deleted or disconnected. Test Plan: - git diff --check - just frontend-test - just build --- .../src-tauri/src/lib.rs | 6 ++ .../src/components/empty/NoDirectoryState.tsx | 6 +- .../src/components/topbar/DirectoryButton.tsx | 12 ++-- .../src/components/topbar/TopBar.tsx | 4 +- .../src/hooks/useGameDirectory.ts | 52 ++++++++++++++--- .../src/styles/launcher.css | 3 + .../src/windows/MainWindow.tsx | 58 ++++++++++--------- 7 files changed, 94 insertions(+), 47 deletions(-) diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs index 4071da6..30e8908 100644 --- a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs +++ b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs @@ -856,6 +856,11 @@ fn ui_operation_from_peer(operation: ActiveOperationKind) -> UiOperationKind { } } +#[tauri::command] +fn game_directory_exists(path: String) -> bool { + PathBuf::from(path).is_dir() +} + #[tauri::command] async fn update_game_directory(app_handle: tauri::AppHandle, path: String) -> tauri::Result<()> { log::info!("update_game_directory: {path}"); @@ -1803,6 +1808,7 @@ pub fn run() { install_game, run_game, start_server, + game_directory_exists, update_game_directory, update_game, uninstall_game, diff --git a/crates/lanspread-tauri-deno-ts/src/components/empty/NoDirectoryState.tsx b/crates/lanspread-tauri-deno-ts/src/components/empty/NoDirectoryState.tsx index 0097825..0c15609 100644 --- a/crates/lanspread-tauri-deno-ts/src/components/empty/NoDirectoryState.tsx +++ b/crates/lanspread-tauri-deno-ts/src/components/empty/NoDirectoryState.tsx @@ -7,11 +7,7 @@ interface Props { export const NoDirectoryState = ({ onChooseDirectory }: Props) => (
-

Pick a game directory

-

- SoftLAN scans the folder you point it at for installable game bundles - and tracks what your peers on the LAN have available. -

+

Please select a game folder

diff --git a/crates/lanspread-tauri-deno-ts/src/hooks/useGameDirectory.ts b/crates/lanspread-tauri-deno-ts/src/hooks/useGameDirectory.ts index 80eba16..f65c8a2 100644 --- a/crates/lanspread-tauri-deno-ts/src/hooks/useGameDirectory.ts +++ b/crates/lanspread-tauri-deno-ts/src/hooks/useGameDirectory.ts @@ -11,6 +11,7 @@ import { GAME_DIR_KEY, SETTINGS_FILE, SETTINGS_FILE_OPTIONS } from '../lib/store */ export const useGameDirectory = () => { const [gameDir, setGameDir] = useState(''); + const [gameDirExists, setGameDirExists] = useState(false); useEffect(() => { let cancelled = false; @@ -30,7 +31,11 @@ export const useGameDirectory = () => { }, []); useEffect(() => { - if (!gameDir) return; + if (!gameDir.trim()) { + setGameDirExists(false); + return; + } + let cancelled = false; const sync = async () => { try { const store = await load(SETTINGS_FILE, SETTINGS_FILE_OPTIONS); @@ -38,19 +43,48 @@ export const useGameDirectory = () => { } catch (err) { console.error('Failed to persist game directory:', err); } + + let exists = false; + try { + exists = await invoke('game_directory_exists', { path: gameDir }); + } catch (err) { + console.error('Failed to validate game directory:', err); + } + if (cancelled) return; + setGameDirExists(exists); + if (!exists) return; + + invoke('update_game_directory', { path: gameDir }).catch(err => + console.error('Failed to push game directory to backend:', err), + ); }; void sync(); - invoke('update_game_directory', { path: gameDir }).catch(err => - console.error('Failed to push game directory to backend:', err), - ); + return () => { + cancelled = true; + }; }, [gameDir]); const rescan = useCallback(() => { - if (!gameDir) return; - invoke('update_game_directory', { path: gameDir }).catch(err => - console.error('Failed to rescan game directory:', err), - ); + if (!gameDir.trim()) { + setGameDirExists(false); + return; + } + const sync = async () => { + let exists = false; + try { + exists = await invoke('game_directory_exists', { path: gameDir }); + } catch (err) { + console.error('Failed to validate game directory:', err); + } + setGameDirExists(exists); + if (!exists) return; + + invoke('update_game_directory', { path: gameDir }).catch(err => + console.error('Failed to rescan game directory:', err), + ); + }; + void sync(); }, [gameDir]); - return { gameDir, setGameDir, rescan }; + return { gameDir, gameDirExists, setGameDir, rescan }; }; diff --git a/crates/lanspread-tauri-deno-ts/src/styles/launcher.css b/crates/lanspread-tauri-deno-ts/src/styles/launcher.css index 242c9ea..7a29a19 100644 --- a/crates/lanspread-tauri-deno-ts/src/styles/launcher.css +++ b/crates/lanspread-tauri-deno-ts/src/styles/launcher.css @@ -1718,6 +1718,9 @@ margin: 0 0 20px; max-width: 44ch; } +.empty-state-title + .ghost-btn { + margin-top: 14px; +} .empty-state .ghost-btn { background: var(--accent); color: white; diff --git a/crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx b/crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx index 837705c..947a84a 100644 --- a/crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx +++ b/crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx @@ -45,7 +45,7 @@ const openLogsWindow = async () => { export const MainWindow = () => { const { settings, set: setSetting } = useSettings(); - const { gameDir, setGameDir, rescan } = useGameDirectory(); + const { gameDir, gameDirExists, setGameDir, rescan } = useGameDirectory(); const games = useGames(rescan); const actions = useGameActions(games, settings); const thumbnails = useThumbnails(); @@ -54,13 +54,18 @@ export const MainWindow = () => { const [removeGameId, setRemoveGameId] = useState(null); const [settingsOpen, setSettingsOpen] = useState(false); - const counts = useMemo(() => countByFilter(games.games), [games.games]); + const hasGameDirectory = !!gameDir.trim() && gameDirExists; + const visibleGames = useMemo( + () => hasGameDirectory ? games.games : [], + [games.games, hasGameDirectory], + ); + const counts = useMemo(() => countByFilter(visibleGames), [visibleGames]); // Query is local UI state (no need to persist). const [query, setQuery] = useState(''); const filteredGames = useMemo( - () => applyFilterAndSort(games.games, settings.filter, settings.sort, query), - [games.games, settings.filter, settings.sort, query], + () => applyFilterAndSort(visibleGames, settings.filter, settings.sort, query), + [visibleGames, settings.filter, settings.sort, query], ); const openGame = useMemo( @@ -116,25 +121,26 @@ export const MainWindow = () => { return (
- {gameDir ? ( - <> - setSetting('filter', v)} - counts={counts} - query={query} - setQuery={setQuery} - sort={settings.sort} - setSort={(v) => setSetting('sort', v)} - gameDir={gameDir} - onPickDirectory={() => void pickDirectory()} - kebabItems={kebabItems} - /> -
+ setSetting('filter', v)} + counts={counts} + query={query} + setQuery={setQuery} + sort={settings.sort} + setSort={(v) => setSetting('sort', v)} + gameDir={gameDir} + gameDirExists={gameDirExists} + onPickDirectory={() => void pickDirectory()} + kebabItems={kebabItems} + /> +
+ {hasGameDirectory ? ( + <> {filteredGames.length === 0 ? ( - games.games.length === 0 ? ( + visibleGames.length === 0 ? ( { onCancelDownload={(g) => actions.cancelDownload(g.id)} /> )} -
- - ) : ( -
+ + ) : ( void pickDirectory()} /> -
- )} + )} +
{openGame && (