9bafd981d7ffd8b7b45b967a405e23dcf4d0a143
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9bafd981d7
|
feat(install): write launcher language marker files
Some games include a language.txt marker in the unpacked local tree, similar in spirit to account_name.txt. Installs and updates now carry the launcher language alongside the account name so those game-provided marker files are rewritten before staged files are promoted into local/. The Tauri command boundary keeps the UI setting vocabulary as de/en, then maps it to the file vocabulary expected by games: german or english. Unknown values continue through the existing DEFAULT_LANGUAGE path, so the marker file falls back to english just like script launch arguments fall back to en. The transaction layer deliberately reuses the same first-match traversal helper for both marker files. The searches stay independent, so games may place account_name.txt and language.txt in different directories if their archive layout requires that. Test Plan: - just fmt - just test - just frontend-test - just clippy - deno task build - git diff --check Refs: none |
||
|
|
574acfca45
|
feat(install): stamp username into account_name.txt after install
Some games ship an `account_name.txt` file somewhere under the unpacked `local/` tree (location varies per game). After install or update, write the configured username into the first such file we find so the game launches under the user's account instead of whatever default the archive contains. The search is a deterministic alphabetical DFS rooted at the install staging dir (`.local.installing/`, which becomes `local/` on rename), stopping at the first regular-file match. Symlinks named `account_name.txt` are skipped (`is_file()` is false for symlinks on Linux), so a hostile archive can't redirect the write outside the game tree. If no `account_name.txt` exists anywhere in the install, the step is a no-op. If the write fails, the existing install rollback (cleanup of staging on fresh installs, restore from backup on updates) handles it — no partial state is left behind. The username flows from the Tauri layer, where it is already sanitized by `sanitize_username`, down through `PeerCommand` variants (`InstallGame`, `DownloadGameFiles`, `DownloadGameFilesWithOptions`) into `install`/`update`, which now take an `Option<&str> account_name`. For the "install game that isn't downloaded yet" path the username has to bridge the async gap between the `GetGame` / `FetchLatestFromPeers` request and the eventual `GotGameFiles` event; we park it in a per-game-id map on `LanSpreadState` and pop it when forwarding the download command. The map is also cleared defensively on `cancel_download`, `DownloadGameFilesFailed`, and `DownloadGameFilesAllPeersGone` so a stale entry can't bleed into a subsequent install with a different username. `PeerCommand` is the in-process command channel, not the wire protocol; no on-wire types changed, so the "one wire version" policy is preserved. The peer-cli harness keeps passing `account_name: None` since it tests peer interop, not user-facing settings. # Test Plan Unit tests in `crates/lanspread-peer/src/install/transaction.rs`: - `install_overwrites_first_account_name_file` — unpacker creates `a/account_name.txt` and `z/account_name.txt`; after install with username "Alice", `a/` is overwritten and `z/` is left untouched, pinning the sorted-DFS "first match wins" behavior. - `install_account_name_missing_file_is_noop` — install with a username but no `account_name.txt` anywhere in the archive succeeds and creates no spurious file. Manual GUI check: in Settings, set a username; install a game whose archive contains `account_name.txt`; open `local/` and confirm the file now holds the configured username. Repeat for the update flow (install, change username, click update). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
4f34c4a249
|
feat: pass profile settings to launch scripts
Add launcher profile settings for username and language, then thread those values into the Windows script launch path. The game setup, game start, and server start scripts now share the same argument shape: - game path: local - game id - language: en or de - player name Expose a local can_host_server flag in the games payload by checking for server_start.cmd in an installed game's root directory. The detail modal uses that flag to show Start Server only for installed games with the script, and the new start_server command invokes server_start.cmd with the same sanitized settings used by game_setup.cmd and game_start.cmd. Test Plan: - just fmt - just test - just frontend-test - just build - just clippy - git diff --check Refs: design/README.md |
||
|
|
47e2bbd454
|
feat(ui): add download progress controls
Replace the downloading action button with a dedicated progress component in
both card and detail views. The card now shows percent plus current speed, while
the detail modal shows bytes, speed, ETA, percent, and an inline cancel affordance
using the same backend progress payload.
Expose download cancellation as a peer command that cancels the tracked transfer
token and lets the running operation clear the authoritative active-operation
snapshot. Add a View Files action that resolves the game root safely and opens it
with the platform file viewer through Tauri's shell plugin.
Test Plan:
- just fmt
- just frontend-test
- just test
- just build
- just clippy
- git diff --cached --check
Refs: design reference
|
||
|
|
5df82aa4f3
|
fix(ui): derive operation status from snapshots
The launcher was mixing lifecycle event handlers with the games-list snapshot when deciding the card status. That left multiple writers for the same install_status field and made event ordering visible in React. Make games-list-updated active_operations the authoritative source for busy status. Lifecycle events no longer mutate the card status; they only keep their non-status side effects such as rescans and error messages. The only remaining optimistic status is CheckingPeers before the backend emits its next snapshot. Add a frontend reducer test that proves an install stays in Installing while an active install snapshot exists, then settles to Installed only after the active operation clears with installed local state. Test Plan: - git diff --check - just fmt - just frontend-test - just build Refs: local install/download status snapshot cleanup |
||
|
|
62ceb063ac
|
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 |
||
|
|
74d9266723
|
fix(ui): show installing for downloaded games
The redesigned action hook marked every accepted install command as Checking Peers. That is correct while the launcher is asking peers for file details, but it is wrong for a game that is already downloaded and only needs local archive installation. Track the already-downloaded path separately and optimistically show Installing until the backend install lifecycle event arrives. Peer-backed downloads keep the existing Checking Peers state. Test Plan: - git diff --check Refs: user redesign nitpick about install button state |
||
|
|
640214ec38
|
feat(tauri): implement Steam-style launcher redesign per design handoff
Replace the previous monolithic 900-line `App.tsx` launcher UI with the
Steam-inspired dark redesign specified in `design/README.md` (handoff
committed in the previous commit). The new UI is split across small,
single-responsibility React modules instead of one file.
What changes from the user's perspective
----------------------------------------
- Dark, gradient-tinted background with sticky 64px top bar (glass blur
+ saturate). Single-row chrome (handoff variant A).
- Pill-style filter toggle (`All Games` / `Local` / `Installed`) with an
animated thumb that slides between options.
- Search field with magnifying-glass icon and a `/` keyboard shortcut to
focus it from anywhere outside an input.
- Sort menu (Name A–Z / Size / Status) as a dropdown.
- Game directory button shows the current path with leading-ellipsis
truncation; clicking it opens the native folder picker.
- Kebab menu hosts Settings, Refresh library, and Unpack logs (existing
companion window). The standalone Unpack-Logs button is removed from
the chrome.
- Game grid uses CSS `auto-fill` minmax with three density presets
(compact / normal / large) and three cover aspects (box / square /
banner), persisted via the Settings dialog.
- Game cards render with the real thumbnail when the backend has one
(via `get_game_thumbnail`) and fall back to a procedurally-generated
gradient + accent-blob placeholder with a Bebas Neue title burned in.
Each card carries a color-coded state chip (Installed = green,
Downloaded = amber, busy = pulsing accent), a peers chip when at
least one peer holds the game, the title, size · genre meta line, a
status line (errors in red), and a single color-coded primary action
button: Play (green gradient), Update / Install (accent), Download
(neutral), animated "busy" spinner during in-flight operations, or a
disabled "Unavailable" state when no peer has the game.
- Clicking anywhere on a card except the action button opens a detail
modal: 16:7 hero (uses the thumbnail), state chip, tag pills derived
from genre/publisher/release_year, large title, 4-cell meta grid
(size, players from `max_players`, version from `local_version` or
`eti_game_version` formatted YYYY.MM.DD, status), description, and an
action row with the primary action plus an Uninstall ghost-danger
button when the game is installed. Esc, scrim click, and the close
button all dismiss the modal.
- Settings dialog (opened from the kebab menu) lets the user change the
accent color (six swatches), background style (flat / gradient /
animated), grid density, and cover aspect. Changes apply live and
persist immediately to the Tauri store under `launcher-settings.json`
(key `ui-settings`); the existing `game-directory` key in the same
file is unchanged.
- Empty state when no directory is chosen offers a centered prompt with
a single CTA. Empty state when filters/search match nothing shows a
distinct "Nothing matches" message.
Why this approach
-----------------
The handoff selected variant A (single-row chrome) explicitly, so only
that variant is implemented; variant B underlined tabs and the
storage-meter widget are intentionally omitted (no free-space data
available from the backend yet).
Real cover art from `get_game_thumbnail` is preferred over the
placeholder generator. When a thumbnail is present, the Bebas Neue
title overlay is suppressed because shipped cover art already has its
own title. When the thumbnail is absent, the placeholder gradient (with
per-id stable hue/blob/angle) plus the burned-in title takes over —
this is the same procedural look as the design reference.
Architecture / file layout
--------------------------
The previous single-file design is decomposed top-down:
```
src/
main.tsx entry; loads tokens + launcher CSS
App.tsx thin router (main vs. unpack-logs view)
styles/
tokens.css CSS custom props + body reset
launcher.css port of the design reference styles.css
(single-row chrome only)
windows/
MainWindow.tsx composition root: top bar + grid + modals
lib/
types.ts Game, InstallStatus, GameAvailability,
ActiveOperationKind, GameFilter / GameSort,
DerivedState
gameState.ts derive() + isUnavailable + needsUpdate +
primaryActionFor + actionLabel +
mergeGameUpdate (event reconciliation) +
countByFilter + applyFilterAndSort
format.ts formatBytes, formatEtiVersion (YYYYMMDD),
truncatePath, formatPlayers
cover.ts coverColorsFor(id) — stable palette pick +
gradient angle + blob position from id
hash; titleFontSize
store.ts file + key constants for plugin-store
hooks/
useSettings.ts UISettings + accent/bg/density/aspect/
sort/filter, persisted via plugin-store
useGameDirectory.ts loads + persists the chosen directory and
pushes it to update_game_directory
useGames.ts owns the games list; listens to every
backend event (games-list-updated,
game-download-begin/finished/failed/
peers-gone, game-no-peers, game-install-
begin/finished/failed, game-uninstall-
begin/finished/failed, peer-count-updated);
exposes markChecking with a 5s fallback to
clear "Checking peers…" when nothing comes
back from the backend
useGameActions.ts play / install / update / uninstall
wrappers around the corresponding invoke
commands
useThumbnails.ts lazy per-id cache for get_game_thumbnail
components/
Icon.tsx inline SVG icon set (currentColor)
Brand.tsx brand mark + name + peer-count chip
Modal.tsx generic scrim + panel + Esc handler
StateChip.tsx corner pill with state-coded dot
ActionButton.tsx color-coded primary action; disabled when
unavailable; spinner when busy
SegmentedRadio.tsx generic 3-way segmented control
ColorSwatchPicker.tsx 6-swatch picker with check overlay
topbar/
TopBar.tsx chrome composition
SegmentedFilters.tsx All / Local / Installed with sliding thumb
SearchField.tsx input + `/` shortcut
SortMenu.tsx dropdown sort selector
DirectoryButton.tsx folder picker trigger
KebabMenu.tsx generic dropdown menu
grid/
ResultsBar.tsx "Showing N of M games"
GameGrid.tsx CSS-grid wrapper
GameCard.tsx full card composition
GameCover.tsx thumbnail OR placeholder cover art
modals/
GameDetailModal.tsx hero + meta grid + actions
SettingsDialog.tsx appearance + library preferences
empty/
NoDirectoryState.tsx onboarding CTA
EmptyResultsState.tsx "scanning" / "nothing matches"
```
`UnpackLogsWindow.tsx` and its CSS are untouched — the unpack-logs
companion window is rendered as before via the existing `?view=unpack-
logs` route in `App.tsx`.
The previous `App.css` is removed entirely (its styles are superseded
by `styles/launcher.css`).
Bebas Neue is loaded via Google Fonts in `index.html` (preconnect +
swap), used for the brand mark and the placeholder cover-art titles.
Tradeoffs and intentional omissions
-----------------------------------
- Storage meter: omitted. The handoff specifies installed/local/free
bytes, but no Tauri command currently provides free-space data.
- Variant B (two-row chrome with underline tabs): omitted; the handoff
picked variant A.
- "View files" action in the detail modal: omitted. The backend doesn't
expose per-game install paths and `shell.open` of the user-chosen
root directory would be misleading.
- "Delete from disk" ghost-danger action for `local` games: omitted.
No backend command currently distinguishes "delete downloaded
archive" from `uninstall_game`. Only installed games get an Uninstall
button.
- "Recently Played" sort: omitted (no play-time tracking yet). The sort
menu offers Name / Size / Status instead.
- Keyboard arrow grid navigation: not yet implemented (out of scope per
the handoff).
- Per-game progress bar during downloads/installs: not implemented; the
action button shows a spinner + "Downloading…" / "Installing…" label
instead, matching the existing event-driven status text.
Persistence
-----------
UI preferences (accent, bg, density, aspect, sort, filter) live in
`launcher-settings.json` under a new `ui-settings` key. The existing
`game-directory` key in the same file is preserved untouched, so users
keep their previously selected directory.
Test plan
---------
Frontend build verified locally:
cd crates/lanspread-tauri-deno-ts && deno task build
→ `tsc && vite build` completes with no diagnostics; bundle ~228 kB.
Manual verification (recommended once the app boots end-to-end):
- [ ] Launch with no directory set: only the "Pick a game directory"
empty state is visible; clicking the button opens the native
folder picker.
- [ ] Pick a directory: top bar appears, grid populates as games arrive.
- [ ] Click the All / Local / Installed pills: the thumb slides; the
count chips reflect the right subset.
- [ ] Press `/`: focus moves to the search input; type a substring and
confirm the grid filters live.
- [ ] Open the Sort menu, switch between sorts; the grid reorders.
- [ ] Open the Settings dialog from the kebab: change accent → the
thumb, brand mark, search-focus ring, and Install button all
switch color live. Change density → grid card size changes.
Change cover aspect → cards re-shape (2/3, 1/1, 16/9). Close and
reopen: choices are remembered.
- [ ] Click anywhere on a card except the action button → detail modal
opens with the right metadata; Esc / scrim click / close button
all dismiss it.
- [ ] Click the action button on an `installed` card → game launches.
- [ ] Click the action button on a `local` card → install starts;
button shows the spinner + "Installing…".
- [ ] Click on a `none` card with peer_count > 0 → download starts; the
lifecycle events update the button label correctly.
- [ ] Card for a game with peer_count == 0 and not downloaded → button
reads "Unavailable" and is disabled.
- [ ] Trigger a `game-download-failed` from the backend: the error
status line appears under the card title in red.
- [ ] Open Unpack Logs from the kebab: the companion window opens
exactly as before.
Trailer
-------
Refs: design/README.md (canonical handoff), design/design_reference/
|