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:
@@ -5,7 +5,7 @@ import { StateChip } from '../StateChip';
|
||||
import { ActionButton } from '../ActionButton';
|
||||
|
||||
import { Game } from '../../lib/types';
|
||||
import { deriveState } from '../../lib/gameState';
|
||||
import { deriveState, isInProgress } from '../../lib/gameState';
|
||||
import { formatBytes, formatEtiVersion, formatPlayers } from '../../lib/format';
|
||||
|
||||
interface Props {
|
||||
@@ -14,6 +14,7 @@ interface Props {
|
||||
onClose: () => void;
|
||||
onPrimary: (game: Game) => void;
|
||||
onUninstall: (game: Game) => void;
|
||||
onRemoveDownload: (game: Game) => void;
|
||||
}
|
||||
|
||||
const tagsFromGame = (game: Game): string[] => {
|
||||
@@ -33,8 +34,18 @@ const statusLabelFor = (game: Game): string => {
|
||||
}
|
||||
};
|
||||
|
||||
export const GameDetailModal = ({ game, thumbnailUrl, onClose, onPrimary, onUninstall }: Props) => {
|
||||
export const GameDetailModal = ({
|
||||
game,
|
||||
thumbnailUrl,
|
||||
onClose,
|
||||
onPrimary,
|
||||
onUninstall,
|
||||
onRemoveDownload,
|
||||
}: Props) => {
|
||||
const tags = tagsFromGame(game);
|
||||
const canRemoveDownload = game.downloaded
|
||||
&& !game.installed
|
||||
&& !isInProgress(game.install_status);
|
||||
return (
|
||||
<Modal onClose={onClose}>
|
||||
<button className="modal-close" type="button" onClick={onClose} aria-label="Close">
|
||||
@@ -102,6 +113,16 @@ export const GameDetailModal = ({ game, thumbnailUrl, onClose, onPrimary, onUnin
|
||||
<span>Uninstall</span>
|
||||
</button>
|
||||
)}
|
||||
{canRemoveDownload && (
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn ghost-danger"
|
||||
onClick={() => onRemoveDownload(game)}
|
||||
>
|
||||
<Icon.trash />
|
||||
<span>Remove files</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user