Files
lanspread/crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx
T
ddidderr 59efe9e2d7 fix(ui): close detail modal when removing downloads
Confirming removal from the game detail modal used to clear only the
confirmation modal state. The detail modal remained open for the same game while
the removal operation was in flight, which could show stale removing or
post-removal state around the closed confirmation dialog.

Close the detail modal when it is showing the game whose downloaded copy is being
removed. Other open detail state is left alone so the change stays scoped to the
confirmed removal flow.

Test Plan:
- deno task build
- RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= just build
- git diff --check

Refs: local review feedback
2026-05-19 21:28:23 +02:00

194 lines
7.5 KiB
TypeScript

import { useCallback, useMemo, useState } from 'react';
import { open } from '@tauri-apps/plugin-dialog';
import { TopBar } from '../components/topbar/TopBar';
import { KebabItem } from '../components/topbar/KebabMenu';
import { ResultsBar } from '../components/grid/ResultsBar';
import { GameGrid } from '../components/grid/GameGrid';
import { GameDetailModal } from '../components/modals/GameDetailModal';
import { ConfirmRemoveDownloadModal } from '../components/modals/ConfirmRemoveDownloadModal';
import { SettingsDialog } from '../components/modals/SettingsDialog';
import { NoDirectoryState } from '../components/empty/NoDirectoryState';
import { EmptyResultsState } from '../components/empty/EmptyResultsState';
import { useGameDirectory } from '../hooks/useGameDirectory';
import { useGames } from '../hooks/useGames';
import { useGameActions } from '../hooks/useGameActions';
import { useThumbnails } from '../hooks/useThumbnails';
import { useSettings } from '../hooks/useSettings';
import { Game } from '../lib/types';
import { applyFilterAndSort, countByFilter, needsUpdate } from '../lib/gameState';
const openLogsWindow = async () => {
const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow');
try {
const existing = await WebviewWindow.getByLabel('unpack-logs');
if (existing) {
await existing.setFocus();
return;
}
const win = new WebviewWindow('unpack-logs', {
url: '/?view=unpack-logs',
title: 'Unpack Logs',
width: 900,
height: 700,
resizable: true,
});
await win.once<unknown>('tauri://error', (event) => {
console.error('Error opening unpack logs window:', event.payload);
});
} catch (err) {
console.error('Error opening unpack logs window:', err);
}
};
export const MainWindow = () => {
const { settings, set: setSetting } = useSettings();
const { gameDir, setGameDir, rescan } = useGameDirectory();
const games = useGames(rescan);
const actions = useGameActions(games);
const thumbnails = useThumbnails();
const [openGameId, setOpenGameId] = useState<string | null>(null);
const [removeGameId, setRemoveGameId] = useState<string | null>(null);
const [settingsOpen, setSettingsOpen] = useState(false);
const counts = useMemo(() => countByFilter(games.games), [games.games]);
// 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],
);
const openGame = useMemo<Game | null>(
() => openGameId ? games.games.find(g => g.id === openGameId) ?? null : null,
[openGameId, games.games],
);
const removeGame = useMemo<Game | null>(
() => removeGameId ? games.games.find(g => g.id === removeGameId) ?? null : null,
[removeGameId, games.games],
);
const pickDirectory = useCallback(async () => {
const picked = await open({ multiple: false, directory: true });
if (typeof picked === 'string' && picked) setGameDir(picked);
}, [setGameDir]);
const handlePrimary = useCallback((game: Game) => {
if (game.installed) {
if (needsUpdate(game)) actions.update(game.id);
else actions.play(game.id);
} else {
actions.install(game.id);
}
}, [actions]);
const handleUninstall = useCallback((game: Game) => {
actions.uninstall(game.id);
}, [actions]);
const handleRemoveDownload = useCallback((game: Game) => {
setRemoveGameId(game.id);
}, []);
const confirmRemoveDownload = useCallback((game: Game) => {
actions.removeDownload(game.id);
setRemoveGameId(null);
setOpenGameId(current => current === game.id ? null : current);
}, [actions]);
const kebabItems: ReadonlyArray<KebabItem> = useMemo(() => [
{ kind: 'item', label: 'Settings', onClick: () => setSettingsOpen(true) },
{ kind: 'item', label: 'Refresh library', onClick: () => rescan() },
{ kind: 'separator' },
{ kind: 'item', label: 'Unpack logs', onClick: () => void openLogsWindow() },
], [rescan]);
const rootStyle = { '--accent': settings.accent } as React.CSSProperties;
const className = [
'launcher',
`bg-${settings.bg}`,
`density-${settings.density}`,
].join(' ');
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">
<ResultsBar shown={filteredGames.length} total={counts.all} />
{filteredGames.length === 0 ? (
games.games.length === 0 ? (
<EmptyResultsState
title="Scanning for games"
hint="Looking for game bundles in your selected directory…"
/>
) : (
<EmptyResultsState
title="Nothing matches"
hint="No games match the current filter or search query."
/>
)
) : (
<GameGrid
games={filteredGames}
aspect={settings.aspect}
getThumbnail={thumbnails.get}
onOpen={(g) => setOpenGameId(g.id)}
onPrimary={handlePrimary}
/>
)}
</main>
</>
) : (
<main className="grid-wrap">
<NoDirectoryState onChooseDirectory={() => void pickDirectory()} />
</main>
)}
{openGame && (
<GameDetailModal
game={openGame}
thumbnailUrl={thumbnails.get(openGame.id)}
onClose={() => setOpenGameId(null)}
onPrimary={handlePrimary}
onUninstall={handleUninstall}
onRemoveDownload={handleRemoveDownload}
/>
)}
{removeGame && (
<ConfirmRemoveDownloadModal
game={removeGame}
onCancel={() => setRemoveGameId(null)}
onConfirm={confirmRemoveDownload}
/>
)}
{settingsOpen && (
<SettingsDialog
settings={settings}
onChange={setSetting}
onClose={() => setSettingsOpen(false)}
/>
)}
</div>
);
};