diff --git a/Cargo.lock b/Cargo.lock index 51d8b05..56c6445 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1566,7 +1566,7 @@ dependencies = [ [[package]] name = "furumi_tui" -version = "0.2.1" +version = "0.2.2" dependencies = [ "anyhow", "blake3", diff --git a/src/app/mod.rs b/src/app/mod.rs index a10eca2..bc8ffad 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2784,6 +2784,12 @@ fn handle_device_playback_snapshot( runtime: &mut Runtime, snapshot: crate::devices::PlaybackSnapshot, ) { + // Personal-device reconciliation must never change Jam ownership. Jam + // has its own authority and lifecycle even when the same TUI also belongs + // to a trusted-device group. + if state.device_playback.role == state::DevicePlaybackRole::Jam { + return; + } if snapshot.device_id == state.device_playback.self_device_id { return; } @@ -2813,7 +2819,7 @@ fn handle_device_playback_snapshot( return; } let lease_expired = active_idle_lease_expired(&snapshot, now); - let already_controls_this_device = state.device_playback.is_control() + let already_controls_this_device = state.device_playback.is_personal_control() && state.device_playback.active_device_id.as_deref() == Some(snapshot.device_id.as_str()); if !lease_expired || already_controls_this_device { let was_active = state.device_playback.is_audio_owner(); @@ -2828,7 +2834,7 @@ fn handle_device_playback_snapshot( return; } - if state.device_playback.is_control() { + if state.device_playback.is_personal_control() { return; } become_active_device(state, runtime, false); @@ -2942,7 +2948,7 @@ fn handle_app_event(state: &mut AppState, runtime: &mut Runtime, event: AppEvent }) .unwrap_or(1) .max(1); - let active_revoked = state.device_playback.is_control() + let active_revoked = state.device_playback.is_personal_control() && state .device_playback .active_device_id @@ -2960,7 +2966,7 @@ fn handle_app_event(state: &mut AppState, runtime: &mut Runtime, event: AppEvent }) .is_some_and(|device| device.revoked) }); - let active_missing = state.device_playback.is_control() + let active_missing = state.device_playback.is_personal_control() && state .device_playback .active_device_id @@ -3022,6 +3028,11 @@ fn handle_app_event(state: &mut AppState, runtime: &mut Runtime, event: AppEvent AppEvent::DevicePlayback(snapshot) => { handle_device_playback_snapshot(state, runtime, snapshot); } + AppEvent::PlaybackCommand(_) + if state.device_playback.role == state::DevicePlaybackRole::Jam => + { + tracing::debug!("ignored personal-device playback command while Jam is active"); + } AppEvent::PlaybackCommand(command) => { handle_playback_command(state, runtime, command); } diff --git a/src/app/popup.rs b/src/app/popup.rs index f20683b..532360b 100644 --- a/src/app/popup.rs +++ b/src/app/popup.rs @@ -423,6 +423,12 @@ fn handle_connected_devices( }); } KeyCode::Enter => { + if state.device_playback.role == crate::app::state::DevicePlaybackRole::Jam { + state.status_message = + Some("leave the current Jam before switching personal devices".into()); + state.popup = Some(Popup::ConnectedDevices { cursor }); + return; + } if cursor == 0 { super::transfer_active_to_this_device(state, runtime); state.status_message = Some("active playback moved to this device".into()); diff --git a/src/app/state.rs b/src/app/state.rs index 134561e..4adfa15 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -1248,6 +1248,10 @@ impl DevicePlaybackState { || (self.role == DevicePlaybackRole::Jam && !self.jam_host) } + pub fn is_personal_control(&self) -> bool { + self.role == DevicePlaybackRole::Control + } + pub fn is_audio_owner(&self) -> bool { self.role == DevicePlaybackRole::Active || (self.role == DevicePlaybackRole::Jam && self.jam_host) diff --git a/src/ui/popup.rs b/src/ui/popup.rs index 63e64e4..30066ce 100644 --- a/src/ui/popup.rs +++ b/src/ui/popup.rs @@ -555,7 +555,7 @@ fn draw_connected_devices(frame: &mut Frame, state: &AppState, cursor: usize) { ); } - render_subtitle(frame, other_area, state, "Other devices"); + render_subtitle(frame, other_area, state, "My devices"); let list_area = Rect { x: other_area.x, y: other_area.y + 1, diff --git a/src/ui/theme.rs b/src/ui/theme.rs index f6c5165..9d65b1c 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -38,15 +38,10 @@ pub fn selection() -> Style { } pub fn selection_for(state: &AppState) -> Style { - if state.device_playback.is_control() { - let background = if state.device_playback.role == DevicePlaybackRole::Jam { - Color::Rgb(80, 24, 96) - } else { - Color::Rgb(92, 72, 0) - }; - Style::new().fg(Color::White).bg(background) - } else { - selection() + match state.device_playback.role { + DevicePlaybackRole::Jam => Style::new().fg(Color::White).bg(Color::Rgb(80, 24, 96)), + DevicePlaybackRole::Control => Style::new().fg(Color::White).bg(Color::Rgb(92, 72, 0)), + DevicePlaybackRole::Active => selection(), } } @@ -55,10 +50,10 @@ pub fn header_for(state: &AppState) -> Style { } pub fn border_for(state: &AppState) -> Style { - if state.device_playback.is_control() { - Style::new().fg(CONTROL_ACCENT) - } else { - dim() + match state.device_playback.role { + DevicePlaybackRole::Jam => Style::new().fg(JAM_ACCENT), + DevicePlaybackRole::Control => Style::new().fg(CONTROL_ACCENT), + DevicePlaybackRole::Active => dim(), } } @@ -79,11 +74,9 @@ pub fn role_pill(role: DevicePlaybackRole) -> Style { } fn accent_color_for(state: &AppState) -> Color { - if state.device_playback.role == DevicePlaybackRole::Jam { - JAM_ACCENT - } else if state.device_playback.is_control() { - CONTROL_ACCENT - } else { - ACCENT + match state.device_playback.role { + DevicePlaybackRole::Jam => JAM_ACCENT, + DevicePlaybackRole::Control => CONTROL_ACCENT, + DevicePlaybackRole::Active => ACCENT, } }