feat: Add PTT, Volume Sliders, and Device Node Selection
This commit is contained in:
+99
-6
@@ -2,10 +2,10 @@ use crate::core::{CoreController, messages::{CoreCommand, UiEvent}};
|
||||
use crate::network::PeerState;
|
||||
|
||||
use iced::widget::{
|
||||
container, column, row, text, button, text_input, scrollable, Column,
|
||||
container, column, row, text, button, text_input, scrollable, slider, checkbox, Column, Space,
|
||||
};
|
||||
use iced::{
|
||||
Color, Background, Border, Element, Subscription, Task, Theme,
|
||||
Color, Background, Border, Element, Subscription, Task, Theme, Event, keyboard,
|
||||
};
|
||||
use iroh::EndpointId;
|
||||
use std::collections::HashMap;
|
||||
@@ -25,6 +25,12 @@ pub enum AppMessage {
|
||||
ToggleDeafenPressed,
|
||||
UiEventReceived(UiEvent),
|
||||
CopyToClipboard,
|
||||
TogglePtt(bool),
|
||||
StartSettingHotkey,
|
||||
PeerVolumeChanged(EndpointId, f32),
|
||||
InputDeviceChanged(String),
|
||||
OutputDeviceChanged(String),
|
||||
EventOccurred(Event),
|
||||
}
|
||||
|
||||
fn core_subscription() -> impl iced::futures::Stream<Item = UiEvent> {
|
||||
@@ -49,7 +55,14 @@ pub struct AppState {
|
||||
ticket: String,
|
||||
is_muted: bool,
|
||||
is_deafened: bool,
|
||||
ptt_enabled: bool,
|
||||
ptt_active: bool,
|
||||
ptt_hotkey: keyboard::Key,
|
||||
is_setting_hotkey: bool,
|
||||
input_device: String,
|
||||
output_device: String,
|
||||
peers: HashMap<EndpointId, PeerState>,
|
||||
peer_volumes: HashMap<EndpointId, f32>,
|
||||
audio_levels: HashMap<EndpointId, f32>,
|
||||
controller: Arc<CoreController>,
|
||||
}
|
||||
@@ -68,7 +81,14 @@ impl Default for AppState {
|
||||
ticket: "".to_string(),
|
||||
is_muted: false,
|
||||
is_deafened: false,
|
||||
ptt_enabled: false,
|
||||
ptt_active: false,
|
||||
ptt_hotkey: keyboard::Key::Named(keyboard::key::Named::Space),
|
||||
is_setting_hotkey: false,
|
||||
input_device: "".to_string(),
|
||||
output_device: "".to_string(),
|
||||
peers: HashMap::new(),
|
||||
peer_volumes: HashMap::new(),
|
||||
audio_levels: HashMap::new(),
|
||||
controller,
|
||||
}
|
||||
@@ -93,8 +113,10 @@ pub fn run_gui() -> iced::Result {
|
||||
.run()
|
||||
}
|
||||
|
||||
fn subscription(_state: &AppState) -> Subscription<AppMessage> {
|
||||
Subscription::run(core_subscription).map(AppMessage::UiEventReceived)
|
||||
fn subscription(state: &AppState) -> Subscription<AppMessage> {
|
||||
let core_sub = Subscription::run(core_subscription).map(AppMessage::UiEventReceived);
|
||||
let event_sub = iced::event::listen().map(AppMessage::EventOccurred);
|
||||
Subscription::batch(vec![core_sub, event_sub])
|
||||
}
|
||||
|
||||
fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
@@ -110,6 +132,8 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
let _ = state.controller.send(CoreCommand::Join {
|
||||
name: state.name.clone(),
|
||||
ticket: state.ticket_input.clone(),
|
||||
input_device: if state.input_device.is_empty() { None } else { Some(state.input_device.clone()) },
|
||||
output_device: if state.output_device.is_empty() { None } else { Some(state.output_device.clone()) },
|
||||
});
|
||||
}
|
||||
AppMessage::CreatePressed => {
|
||||
@@ -117,6 +141,8 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
let _ = state.controller.send(CoreCommand::Join {
|
||||
name: state.name.clone(),
|
||||
ticket: "".to_string(),
|
||||
input_device: if state.input_device.is_empty() { None } else { Some(state.input_device.clone()) },
|
||||
output_device: if state.output_device.is_empty() { None } else { Some(state.output_device.clone()) },
|
||||
});
|
||||
}
|
||||
AppMessage::LeavePressed => {
|
||||
@@ -170,6 +196,39 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
return iced::clipboard::write(state.ticket.clone());
|
||||
}
|
||||
}
|
||||
AppMessage::TogglePtt(enabled) => {
|
||||
state.ptt_enabled = enabled;
|
||||
let _ = state.controller.send(CoreCommand::SetPttMode(enabled));
|
||||
}
|
||||
AppMessage::StartSettingHotkey => {
|
||||
state.is_setting_hotkey = true;
|
||||
}
|
||||
AppMessage::PeerVolumeChanged(id, vol) => {
|
||||
state.peer_volumes.insert(id, vol);
|
||||
let _ = state.controller.send(CoreCommand::SetPeerVolume(id, vol));
|
||||
}
|
||||
AppMessage::InputDeviceChanged(val) => {
|
||||
state.input_device = val;
|
||||
}
|
||||
AppMessage::OutputDeviceChanged(val) => {
|
||||
state.output_device = val;
|
||||
}
|
||||
AppMessage::EventOccurred(Event::Keyboard(keyboard::Event::KeyPressed { key, .. })) => {
|
||||
if state.is_setting_hotkey {
|
||||
state.ptt_hotkey = key.clone();
|
||||
state.is_setting_hotkey = false;
|
||||
} else if state.ptt_enabled && key == state.ptt_hotkey && !state.ptt_active {
|
||||
state.ptt_active = true;
|
||||
let _ = state.controller.send(CoreCommand::SetPttActive(true));
|
||||
}
|
||||
}
|
||||
AppMessage::EventOccurred(Event::Keyboard(keyboard::Event::KeyReleased { key, .. })) => {
|
||||
if state.ptt_enabled && key == state.ptt_hotkey && state.ptt_active {
|
||||
state.ptt_active = false;
|
||||
let _ = state.controller.send(CoreCommand::SetPttActive(false));
|
||||
}
|
||||
}
|
||||
AppMessage::EventOccurred(_) => {}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
@@ -308,6 +367,13 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
text("— OR —").size(12).color(color_surface).align_x(iced::alignment::Horizontal::Center),
|
||||
vertical_space(16.0),
|
||||
join_group,
|
||||
vertical_space(16.0),
|
||||
text("Device Settings (Optional Node Target IDs)").size(14).color(color_subtext),
|
||||
row![
|
||||
text_input("Input Target", &state.input_device).on_input(AppMessage::InputDeviceChanged).style(t_style.clone()).padding(8),
|
||||
horizontal_space(),
|
||||
text_input("Output Target", &state.output_device).on_input(AppMessage::OutputDeviceChanged).style(t_style.clone()).padding(8),
|
||||
].spacing(10),
|
||||
vertical_space(20.0),
|
||||
status
|
||||
]
|
||||
@@ -380,7 +446,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
text("[Idle]").size(14).color(color_subtext)
|
||||
};
|
||||
|
||||
let card = container(
|
||||
let mut card_content = column![
|
||||
row![
|
||||
column![
|
||||
text(&peer.name).size(16).color(color_text),
|
||||
@@ -390,7 +456,19 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
indicator
|
||||
]
|
||||
.align_y(iced::alignment::Vertical::Center)
|
||||
)
|
||||
].spacing(8);
|
||||
|
||||
// Peer volume slider
|
||||
let current_vol = state.peer_volumes.get(peer_id).copied().unwrap_or(1.0);
|
||||
let peer_id_clone = *peer_id;
|
||||
card_content = card_content.push(
|
||||
row![
|
||||
text("Vol:").size(12).color(color_subtext),
|
||||
slider(0.0..=2.0, current_vol, move |v| AppMessage::PeerVolumeChanged(peer_id_clone, v))
|
||||
].spacing(8).align_y(iced::alignment::Vertical::Center)
|
||||
);
|
||||
|
||||
let card = container(card_content)
|
||||
.style(c_style(
|
||||
if is_speaking { color_base } else { color_mantle },
|
||||
if is_speaking { color_green } else { color_surface },
|
||||
@@ -446,6 +524,21 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
.style(b_style(deafen_bg, deafen_hover, deafen_fg, 8.0))
|
||||
.padding(14)
|
||||
.width(iced::Length::Fill),
|
||||
vertical_space(20.0),
|
||||
checkbox(state.ptt_enabled).label("Push-to-Talk").on_toggle(AppMessage::TogglePtt),
|
||||
vertical_space(10.0),
|
||||
if state.ptt_enabled {
|
||||
column![
|
||||
text(format!("Hotkey: {}", if state.is_setting_hotkey { "Press any key...".to_string() } else { format!("{:?}", state.ptt_hotkey) })).size(14).color(color_subtext),
|
||||
button(text("Set Hotkey").size(12).align_x(iced::alignment::Horizontal::Center))
|
||||
.on_press(AppMessage::StartSettingHotkey)
|
||||
.style(b_style(color_surface, color_blue, color_text, 6.0))
|
||||
.padding(8)
|
||||
.width(iced::Length::Fill)
|
||||
].spacing(8)
|
||||
} else {
|
||||
column![]
|
||||
},
|
||||
vertical_space(30.0),
|
||||
button(
|
||||
text("Leave Room")
|
||||
|
||||
Reference in New Issue
Block a user