feat(peer): remove downloaded game files safely
Downloaded but uninstalled games can still occupy significant disk space. Add a separate removal path for that state instead of overloading uninstall, which is reserved for deleting only `local/` installs. The peer runtime now exposes `RemoveDownloadedGame` with matching lifecycle and active-operation events. The filesystem delete is intentionally strict: the id must be a catalog game and a single path component, the target must be a direct child of the configured game directory, the root must not be a symlink, it must have a regular root-level `version.ini`, and it must not contain `local/`, `.local.installing/`, or `.local.backup/`. Only then do we recursively remove the game root. The Tauri bridge exposes this as `remove_downloaded_game`, the frontend shows a matching danger action only for downloaded-but-uninstalled games, and a confirmation dialog warns that re-downloading can take a long time. Test Plan: - git diff --check - just fmt - RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= just test - RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= just clippy - RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= just build Refs: user redesign nitpick about removing downloaded uninstalled games
This commit is contained in:
@@ -6,6 +6,7 @@ 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';
|
||||
@@ -50,6 +51,7 @@ export const MainWindow = () => {
|
||||
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]);
|
||||
@@ -65,6 +67,10 @@ export const MainWindow = () => {
|
||||
() => 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 });
|
||||
@@ -84,6 +90,15 @@ export const MainWindow = () => {
|
||||
actions.uninstall(game.id);
|
||||
}, [actions]);
|
||||
|
||||
const handleRemoveDownload = useCallback((game: Game) => {
|
||||
setRemoveGameId(game.id);
|
||||
}, []);
|
||||
|
||||
const confirmRemoveDownload = useCallback((game: Game) => {
|
||||
actions.removeDownload(game.id);
|
||||
setRemoveGameId(null);
|
||||
}, [actions]);
|
||||
|
||||
const kebabItems: ReadonlyArray<KebabItem> = useMemo(() => [
|
||||
{ kind: 'item', label: 'Settings', onClick: () => setSettingsOpen(true) },
|
||||
{ kind: 'item', label: 'Refresh library', onClick: () => rescan() },
|
||||
@@ -153,6 +168,15 @@ export const MainWindow = () => {
|
||||
onClose={() => setOpenGameId(null)}
|
||||
onPrimary={handlePrimary}
|
||||
onUninstall={handleUninstall}
|
||||
onRemoveDownload={handleRemoveDownload}
|
||||
/>
|
||||
)}
|
||||
|
||||
{removeGame && (
|
||||
<ConfirmRemoveDownloadModal
|
||||
game={removeGame}
|
||||
onCancel={() => setRemoveGameId(null)}
|
||||
onConfirm={confirmRemoveDownload}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user