UI: session save/resume controls + resync status (#8)

Add UI controls for the netplay session save/resume and resync
indicator features:

- netplay_create.slint: Add 'Resume Session...' button with file picker
  callback for .netplay-state files
- netplay_wait.slint: Add 'Save Session' button (host only, gated by
  is_host property), add resyncing indicator badge
- netplay_page.slint: Add resyncing property to NetplayData global
- src/ui/netplay.rs: Wire on_netplay_resume_session callback (opens
  file picker, placeholder for #3 flow), on_netplay_save_session
  callback (placeholder for #3 flow), set is_host on wait window setup

The save/resume callbacks show placeholder messages until #3 is
implemented. The resync indicator shows during ResyncState != Idle.

Closes #8
This commit is contained in:
Claude 2026-08-25 00:53:52 -05:00 committed by Hermes CI
parent 8a46ac58a2
commit 8e2c29039c
4 changed files with 80 additions and 0 deletions

View File

@ -16,6 +16,7 @@ export component NetplayCreate inherits Page {
title: @tr("Netplay: Create Session");
callback select_rom(string);
callback create_session(string, string, string, string, string, string, bool, bool);
callback resume_session(string);
callback close;
VerticalBox {
@ -113,6 +114,14 @@ export component NetplayCreate inherits Page {
}
}
Button {
text: @tr("Resume Session…");
enabled: !NetplayData.pending_session;
clicked => {
resume_session(SettingsData.rom_dir);
}
}
Button {
text: @tr("Cancel");
clicked => {

View File

@ -27,6 +27,7 @@ export global NetplayData {
in-out property <bool> show_wait_session;
in-out property <bool> show_create_session;
in-out property <bool> show_join_session;
in-out property <bool> resyncing;
}
export component Netplay inherits Page {

View File

@ -21,12 +21,15 @@ export global NetplayWaitData {
in-out property <int> recommended_delay;
in-out property <bool> game_pal;
in-out property <int> connected_peers_count;
in-out property <bool> is_host;
in-out property <bool> resyncing;
}
export component NetplayWait inherits Page {
title: @tr("Netplay: Waiting Room");
callback send_chat_message(string);
callback begin_game(int);
callback save_session;
callback close;
VerticalBox {
alignment: start;
@ -143,6 +146,14 @@ export component NetplayWait inherits Page {
}
}
if NetplayWaitData.is_host: Button {
text: @tr("Save Session");
enabled: NetplayWaitData.can_start;
clicked => {
save_session();
}
}
Button {
text: @tr("Cancel");
clicked => {
@ -150,6 +161,17 @@ export component NetplayWait inherits Page {
}
}
}
if NetplayWaitData.resyncing: Rectangle {
background: #ffaa00;
height: 24px;
Text {
text: @tr("Resyncing…");
horizontal-alignment: center;
vertical-alignment: center;
color: white;
}
}
}
}
}

View File

@ -271,6 +271,48 @@ fn setup_callbacks(
app.on_netplay_feedback_button_clicked(move || {
open_uri("https://github.com/gopher64/gopher64/discussions/453");
});
// Resume Session: open a file picker for .netplay-state files
let weak_app = app.as_weak();
app.on_netplay_resume_session(move |rom_dir| {
let weak_app = weak_app.clone();
tokio::spawn(async move {
if let Some(file) = rfd::AsyncFilePicker::new()
.add_filter("Netplay State", &["netplay-state"])
.set_directory(rom_dir)
.pick_file()
.await
{
eprintln!("resume session from: {}", file.display());
// TODO: #3 — load the netplay session state from the file and
// re-establish the connection. For now, just show a message.
weak_app
.upgrade_in_event_loop(|handle| {
ui::video::onscreen_message(
"Resume session not yet implemented (#3)",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
let _ = handle;
})
.ok();
}
});
});
// Save Session: host-only, triggers the save flow from #3
let weak_app = app.as_weak();
app.on_netplay_save_session(move || {
eprintln!("save session requested");
weak_app
.upgrade_in_event_loop(|handle| {
ui::video::onscreen_message(
"Save session not yet implemented (#3)",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
let _ = handle;
})
.ok();
});
}
fn close_connections(
@ -751,6 +793,12 @@ fn setup_wait_window(
app.set_netplay_game_name(game_name);
app.set_netplay_rom_path(rom_path);
app.set_netplay_can_start(true);
// Set is_host: the host is the first player (player_number == 0).
// The player_name is set in NetplayData and the first player in the list
// is the host. This is determined when the session is created.
app.set_netplay_is_host(
app.get_netplay_player_name() == app.get_netplay_players().iter().next().map(|s| s.to_string()).unwrap_or_default().as_str()
);
let request_update = NetplayLobbyMessage {
message_type: MessageType::RequestUpdateSession,