0d2520fd16
A game that the user deletes from disk while the launcher is running stayed
visible as "Installed" in the UI indefinitely, both as a status label and as
a member of the Installed tab. After a restart the Install button reappeared
but the game still wrongly showed up under Installed. The backend rescan
(`set_all_uninstalled` + `update_game_installation_state` in
src-tauri/src/lib.rs) was already producing the correct `installed: false`
on each refresh; the React store was just refusing to honour it.
Two independent UI bugs were in play:
1. The `games-list-updated` listener merged each update with
`previous?.install_status ?? ...`, which preserved a prior `Installed`
value regardless of what the backend now reported. The fix introduces
`mergeGameUpdate`: the backend `installed` flag wins for settled state
(Installed vs NotInstalled), while genuine in-progress states
(CheckingPeers / Downloading / Unpacking) are preserved across refreshes
so concurrent backend ticks cannot blow away an active download UI.
`status_message` and `status_level` are cleared only when the local
`installed` / `downloaded` flags actually flip, so a transient error
("No peers currently have this game.") survives a cosmetic refresh but
is wiped once the underlying state changes.
2. The Installed tab filter was `installed || downloaded`, which leaked
downloaded-but-not-yet-installed games into a tab whose label promises
only ready-to-play titles. It now filters on `installed` alone, matching
`getActionLabel`'s own definition of when "Install" appears.
While the install-state semantics were being sorted out, the filter
taxonomy was clarified to match what users actually mean:
| Button | Filter |
|------------|---------------------------------------------------|
| All Games | installed || downloaded || peer_count > 0 |
| Local | installed || downloaded |
| Installed | installed |
The "Available" button was renamed "Local" because users do not think of
themselves as a peer; Local means "on my system, whether the archive is
still packed or already installed". "All Games" previously surfaced every
row in the bundled game.db, including catalogue entries that no peer on
the LAN holds — confusing, since those games cannot be acted on. It now
scopes to LAN-reachable games. The `isUnavailable` helper and its
`Unavailable` action label are left in place: with this filter no
displayed game can hit that state today, but the helper is cheap to keep
as a safety net for transient peer-count flips and for a possible future
"also show catalogue-only entries" toggle.
Tooltips were rewritten to a consistent `Show games … on your system` /
`Show all games available on the LAN` pattern, all phrased from the user's
point of view (no "peer" jargon in user-facing strings; doc/code comments
still use "peer" where it reflects the actual protocol).
Two stale comments were dropped along the way: a note on
`getInitialGameDir` that claimed it only sets the directory if not already
set (the function unconditionally calls `setGameDir` when a value is
persisted), and a leftover `// Rest of your component remains the same`
marker from an earlier scaffold.
Test plan:
- `npm --prefix crates/lanspread-tauri-deno-ts exec tsc -- --noEmit`
passes (run as part of this change).
- `just run`, point the launcher at a game directory holding two installed
games, then manually `rm -rf` each game's local folder. Within one
refresh cycle the Installed tab should empty and each game's action
button should flip to Install / Download as appropriate, without
needing a restart.
- Start a download and verify the UI does not regress to NotInstalled
when the next `games-list-updated` arrives mid-flight.
- Cycle through All Games / Local / Installed and confirm membership
matches the table above; in particular, a game whose archive is
downloaded but not installed appears under Local and All Games but not
Installed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>