59efe9e2d7
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
194 lines
7.5 KiB
TypeScript
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>
|
|
);
|
|
};
|