feat(ui): delegate install lifecycle to the peer
Remove the Tauri-side whole-game backup and unpack flow. The Tauri shell now provides an injected unrar sidecar implementation and lets the peer own install, update, uninstall, rollback, and recovery decisions. Route install commands by local state: missing version.ini fetches from peers, downloaded archives without local/ send InstallGame directly, and already installed games are left to the Play action. Updates request a fresh download and uninstalls forward UninstallGame. The UI mirrors peer operation events for downloading, installing, updating, and uninstalling. Render installed-but-not-downloaded games as LocalOnly and surface the local version for downloaded-but-not-installed games. Add a secondary uninstall affordance that does not change the main Install/Open action. Test Plan: - just fmt - just clippy - just test - just build Refs: PLAN.md
This commit is contained in:
@@ -74,6 +74,29 @@ h1.align-center {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.badges {
|
||||
display: flex;
|
||||
min-height: 24px;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 10px 8px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
border: 1px solid #4866b9;
|
||||
border-radius: 4px;
|
||||
color: #D5DBFE;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
padding: 5px 7px;
|
||||
}
|
||||
|
||||
.badge.local-only {
|
||||
border-color: #8b6f2a;
|
||||
color: #f1d58a;
|
||||
}
|
||||
|
||||
.desc-text {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -127,6 +150,24 @@ h1.align-center {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.uninstall-button {
|
||||
align-self: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
margin: 6px 0 0;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #6c2942;
|
||||
background: #2a0714;
|
||||
color: #ffb4c8;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.uninstall-button:hover {
|
||||
border-color: #ff6d9d;
|
||||
background: #4d1025;
|
||||
}
|
||||
|
||||
@keyframes flicker {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.8; }
|
||||
|
||||
@@ -23,7 +23,8 @@ enum InstallStatus {
|
||||
NotInstalled = 'NotInstalled',
|
||||
CheckingPeers = 'CheckingPeers',
|
||||
Downloading = 'Downloading',
|
||||
Unpacking = 'Unpacking',
|
||||
Installing = 'Installing',
|
||||
Uninstalling = 'Uninstalling',
|
||||
Installed = 'Installed',
|
||||
}
|
||||
|
||||
@@ -83,7 +84,8 @@ const GameThumbnail = ({ gameId, alt, getThumbnailUrl }: GameThumbnailProps) =>
|
||||
const IN_PROGRESS_INSTALL_STATUSES = new Set<InstallStatus>([
|
||||
InstallStatus.CheckingPeers,
|
||||
InstallStatus.Downloading,
|
||||
InstallStatus.Unpacking,
|
||||
InstallStatus.Installing,
|
||||
InstallStatus.Uninstalling,
|
||||
]);
|
||||
|
||||
const isInProgressInstallStatus = (status: InstallStatus): boolean => {
|
||||
@@ -378,13 +380,82 @@ const App = () => {
|
||||
setGameItems(prev => prev.map(item => item.id === game_id
|
||||
? {
|
||||
...item,
|
||||
install_status: InstallStatus.Unpacking,
|
||||
install_status: InstallStatus.Installing,
|
||||
status_message: undefined,
|
||||
status_level: undefined,
|
||||
}
|
||||
: item));
|
||||
});
|
||||
|
||||
const unlisten_game_install_begin = await listen('game-install-begin', (event) => {
|
||||
const game_id = event.payload as string;
|
||||
console.log(`🗲 game-install-begin ${game_id} event received`);
|
||||
clearCheckingPeersTimeout(game_id);
|
||||
setGameItems(prev => prev.map(item => item.id === game_id
|
||||
? {
|
||||
...item,
|
||||
install_status: InstallStatus.Installing,
|
||||
status_message: undefined,
|
||||
status_level: undefined,
|
||||
}
|
||||
: item));
|
||||
});
|
||||
|
||||
const unlisten_game_install_failed = await listen('game-install-failed', (event) => {
|
||||
const game_id = event.payload as string;
|
||||
console.log(`❌ game-install-failed ${game_id} event received`);
|
||||
clearCheckingPeersTimeout(game_id);
|
||||
setGameItems(prev => prev.map(item => item.id === game_id
|
||||
? {
|
||||
...item,
|
||||
install_status: item.installed ? InstallStatus.Installed : InstallStatus.NotInstalled,
|
||||
status_message: 'Install failed. Please try again.',
|
||||
status_level: 'error',
|
||||
}
|
||||
: item));
|
||||
});
|
||||
|
||||
const unlisten_game_uninstall_begin = await listen('game-uninstall-begin', (event) => {
|
||||
const game_id = event.payload as string;
|
||||
console.log(`🗲 game-uninstall-begin ${game_id} event received`);
|
||||
setGameItems(prev => prev.map(item => item.id === game_id
|
||||
? {
|
||||
...item,
|
||||
install_status: InstallStatus.Uninstalling,
|
||||
status_message: undefined,
|
||||
status_level: undefined,
|
||||
}
|
||||
: item));
|
||||
});
|
||||
|
||||
const unlisten_game_uninstall_finished = await listen('game-uninstall-finished', (event) => {
|
||||
const game_id = event.payload as string;
|
||||
console.log(`🗲 game-uninstall-finished ${game_id} event received`);
|
||||
setGameItems(prev => prev.map(item => item.id === game_id
|
||||
? {
|
||||
...item,
|
||||
installed: false,
|
||||
install_status: InstallStatus.NotInstalled,
|
||||
status_message: undefined,
|
||||
status_level: undefined,
|
||||
}
|
||||
: item));
|
||||
|
||||
});
|
||||
|
||||
const unlisten_game_uninstall_failed = await listen('game-uninstall-failed', (event) => {
|
||||
const game_id = event.payload as string;
|
||||
console.log(`❌ game-uninstall-failed ${game_id} event received`);
|
||||
setGameItems(prev => prev.map(item => item.id === game_id
|
||||
? {
|
||||
...item,
|
||||
install_status: item.installed ? InstallStatus.Installed : InstallStatus.NotInstalled,
|
||||
status_message: 'Uninstall failed. Please try again.',
|
||||
status_level: 'error',
|
||||
}
|
||||
: item));
|
||||
});
|
||||
|
||||
// Initial request for games
|
||||
console.log('📤 Requesting initial games list');
|
||||
await invoke('request_games');
|
||||
@@ -395,6 +466,11 @@ const App = () => {
|
||||
unlisten_games();
|
||||
unlisten_game_download_begin();
|
||||
unlisten_game_download_finished();
|
||||
unlisten_game_install_begin();
|
||||
unlisten_game_install_failed();
|
||||
unlisten_game_uninstall_begin();
|
||||
unlisten_game_uninstall_finished();
|
||||
unlisten_game_uninstall_failed();
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Error in setup:', error);
|
||||
@@ -479,6 +555,25 @@ const App = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const uninstallGame = async (id: string) => {
|
||||
console.log(`🎯 Uninstalling game with id=${id}`);
|
||||
try {
|
||||
const success = await invoke('uninstall_game', { id });
|
||||
if (success) {
|
||||
setGameItems(prev => prev.map(item => item.id === id
|
||||
? {
|
||||
...item,
|
||||
install_status: InstallStatus.Uninstalling,
|
||||
status_message: undefined,
|
||||
status_level: undefined,
|
||||
}
|
||||
: item));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error uninstalling game:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const needsUpdate = (game: Game): boolean => {
|
||||
if (!game.installed) return false;
|
||||
|
||||
@@ -507,8 +602,10 @@ const App = () => {
|
||||
return 'Checking peers...';
|
||||
case InstallStatus.Downloading:
|
||||
return 'Downloading...';
|
||||
case InstallStatus.Unpacking:
|
||||
return 'Unpacking...';
|
||||
case InstallStatus.Installing:
|
||||
return 'Installing...';
|
||||
case InstallStatus.Uninstalling:
|
||||
return 'Uninstalling...';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
@@ -641,6 +738,14 @@ const App = () => {
|
||||
<span className="desc-text">{item.description.slice(0, 10)}</span>
|
||||
<span className="size-text">{(item.size / 1024 / 1024 / 1024).toFixed(1)} GB</span>
|
||||
</div>
|
||||
<div className="badges">
|
||||
{item.installed && !item.downloaded && (
|
||||
<span className="badge local-only">LocalOnly</span>
|
||||
)}
|
||||
{!item.installed && item.downloaded && item.local_version && (
|
||||
<span className="badge">v{item.local_version}</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`play-button${isUnavailable(item) ? ' unavailable' : ''}`}
|
||||
onClick={() => {
|
||||
@@ -658,6 +763,19 @@ const App = () => {
|
||||
}}>
|
||||
{getActionLabel(item)}
|
||||
</div>
|
||||
{item.installed && !isInProgressInstallStatus(item.install_status) && (
|
||||
<button
|
||||
className="uninstall-button"
|
||||
aria-label={`Uninstall ${item.name}`}
|
||||
title="Uninstall"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
uninstallGame(item.id);
|
||||
}}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
)}
|
||||
<div className={`item-info${item.status_level ? ` ${item.status_level}` : ''}`}>
|
||||
<div className="status-left">
|
||||
{item.status_message && item.peer_count === 0 && !item.installed && !item.downloaded ? item.status_message : ''}
|
||||
|
||||
Reference in New Issue
Block a user