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
This commit is contained in:
2026-05-21 19:14:11 +02:00
parent e0efb69bf0
commit 31ace174e3
7 changed files with 94 additions and 47 deletions
@@ -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<string | null>(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<Game | null>(
@@ -116,25 +121,26 @@ export const MainWindow = () => {
return (
<div className={className} style={rootStyle}>
{gameDir ? (
<>
<TopBar
peerCount={games.totalPeerCount}
filter={settings.filter}
setFilter={(v) => setSetting('filter', v)}
counts={counts}
query={query}
setQuery={setQuery}
sort={settings.sort}
setSort={(v) => setSetting('sort', v)}
gameDir={gameDir}
onPickDirectory={() => void pickDirectory()}
kebabItems={kebabItems}
/>
<main className="grid-wrap">
<TopBar
peerCount={games.totalPeerCount}
filter={settings.filter}
setFilter={(v) => 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}
/>
<main className="grid-wrap">
{hasGameDirectory ? (
<>
<ResultsBar shown={filteredGames.length} total={counts.all} />
{filteredGames.length === 0 ? (
games.games.length === 0 ? (
visibleGames.length === 0 ? (
<EmptyResultsState
title="Scanning for games"
hint="Looking for game bundles in your selected directory…"
@@ -155,13 +161,11 @@ export const MainWindow = () => {
onCancelDownload={(g) => actions.cancelDownload(g.id)}
/>
)}
</main>
</>
) : (
<main className="grid-wrap">
</>
) : (
<NoDirectoryState onChooseDirectory={() => void pickDirectory()} />
</main>
)}
)}
</main>
{openGame && (
<GameDetailModal