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 e308009a08
This commit is contained in:
@@ -14,7 +14,8 @@ It is designed to run headless – other crates (most notably
|
||||
roots are announced or served.
|
||||
- `PeerCommand` represents the small control surface exposed to the UI layer:
|
||||
`ListGames`, `GetGame`, `FetchLatestFromPeers`, `DownloadGameFiles`,
|
||||
`InstallGame`, `UninstallGame`, `SetGameDir`, and `GetPeerCount`.
|
||||
`InstallGame`, `UninstallGame`, `RemoveDownloadedGame`, `CancelDownload`,
|
||||
`SetGameDir`, and `GetPeerCount`.
|
||||
- `PeerEvent` enumerates everything the peer runtime reports back to the UI:
|
||||
library snapshots, download/install/uninstall lifecycle updates, runtime
|
||||
failures, and peer membership changes.
|
||||
@@ -82,6 +83,11 @@ When the UI asks to download a game:
|
||||
6. After a successful sentinel commit, `PeerEvent::DownloadGameFilesFinished`
|
||||
is emitted and the peer auto-runs the install transaction.
|
||||
|
||||
`PeerCommand::CancelDownload` cancels the tracked download token for an active
|
||||
transfer. The transfer task remains responsible for clearing `active_operations`,
|
||||
so the UI continues to treat active-operation snapshots as the single source of
|
||||
truth for whether a download is still running.
|
||||
|
||||
### Install Transactions
|
||||
|
||||
Install, update, uninstall, downloaded-file removal, and startup recovery live
|
||||
|
||||
@@ -408,6 +408,21 @@ pub async fn handle_remove_downloaded_game_command(
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn handle_cancel_download_command(
|
||||
ctx: &Ctx,
|
||||
_tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
id: String,
|
||||
) {
|
||||
let cancel_token = ctx.active_downloads.read().await.get(&id).cloned();
|
||||
let Some(cancel_token) = cancel_token else {
|
||||
log::warn!("Ignoring cancel request for inactive download {id}");
|
||||
return;
|
||||
};
|
||||
|
||||
log::info!("Cancelling download for game {id}");
|
||||
cancel_token.cancel();
|
||||
}
|
||||
|
||||
fn spawn_install_operation(ctx: &Ctx, tx_notify_ui: &UnboundedSender<PeerEvent>, id: String) {
|
||||
let ctx = ctx.clone();
|
||||
let tx_notify_ui = tx_notify_ui.clone();
|
||||
@@ -1504,6 +1519,32 @@ mod tests {
|
||||
assert!(ctx.active_downloads.read().await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cancel_download_command_only_cancels_active_token() {
|
||||
let temp = TempDir::new("lanspread-handler-cancel-download");
|
||||
let ctx = test_ctx(temp.path().to_path_buf());
|
||||
let cancel = CancellationToken::new();
|
||||
ctx.active_operations
|
||||
.write()
|
||||
.await
|
||||
.insert("game".to_string(), OperationKind::Downloading);
|
||||
ctx.active_downloads
|
||||
.write()
|
||||
.await
|
||||
.insert("game".to_string(), cancel.clone());
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
|
||||
handle_cancel_download_command(&ctx, &tx, "game".to_string()).await;
|
||||
|
||||
assert!(cancel.is_cancelled());
|
||||
assert_eq!(
|
||||
ctx.active_operations.read().await.get("game"),
|
||||
Some(&OperationKind::Downloading),
|
||||
"the running transfer owns operation cleanup after cancellation"
|
||||
);
|
||||
assert_no_event(&mut rx).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_refreshes_settled_state_before_operation_clear() {
|
||||
let temp = TempDir::new("lanspread-handler-update");
|
||||
|
||||
@@ -61,6 +61,7 @@ use crate::{
|
||||
context::Ctx,
|
||||
handlers::{
|
||||
GameDetailSource,
|
||||
handle_cancel_download_command,
|
||||
handle_connect_peer_command,
|
||||
handle_download_game_files_command,
|
||||
handle_get_game_command,
|
||||
@@ -235,6 +236,8 @@ pub enum PeerCommand {
|
||||
UninstallGame { id: String },
|
||||
/// Remove downloaded archive files for an uninstalled game.
|
||||
RemoveDownloadedGame { id: String },
|
||||
/// Cancel an active peer download without emitting a user-facing failure.
|
||||
CancelDownload { id: String },
|
||||
/// Set the local game directory.
|
||||
SetGameDir(PathBuf),
|
||||
/// Request the current peer count.
|
||||
@@ -419,6 +422,9 @@ async fn handle_peer_commands(
|
||||
PeerCommand::RemoveDownloadedGame { id } => {
|
||||
handle_remove_downloaded_game_command(ctx, tx_notify_ui, id).await;
|
||||
}
|
||||
PeerCommand::CancelDownload { id } => {
|
||||
handle_cancel_download_command(ctx, tx_notify_ui, id).await;
|
||||
}
|
||||
PeerCommand::SetGameDir(game_dir) => {
|
||||
handle_set_game_dir_command(ctx, tx_notify_ui, game_dir).await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user