feat: Add persistent configuration and enumerate PipeWire audio devices
This commit is contained in:
Generated
+39
@@ -1215,6 +1215,27 @@ dependencies = [
|
|||||||
"crypto-common 0.2.2",
|
"crypto-common 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dispatch"
|
name = "dispatch"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -4049,6 +4070,12 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opus"
|
name = "opus"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -4149,6 +4176,7 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"dirs",
|
||||||
"iced",
|
"iced",
|
||||||
"iroh",
|
"iroh",
|
||||||
"iroh-gossip",
|
"iroh-gossip",
|
||||||
@@ -4581,6 +4609,17 @@ dependencies = [
|
|||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"libredox",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.12.3"
|
version = "1.12.3"
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ anyhow = "1.0.102"
|
|||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
bytes = "1.11.1"
|
bytes = "1.11.1"
|
||||||
|
dirs = "6.0.0"
|
||||||
iced = "0.14.0"
|
iced = "0.14.0"
|
||||||
iroh = "1.0.0-rc.0"
|
iroh = "1.0.0-rc.0"
|
||||||
iroh-gossip = "0.99.0"
|
iroh-gossip = "0.99.0"
|
||||||
|
|||||||
+65
-24
@@ -1,8 +1,10 @@
|
|||||||
use crate::core::{CoreController, messages::{CoreCommand, UiEvent}};
|
use crate::core::{CoreController, messages::{CoreCommand, UiEvent}};
|
||||||
use crate::network::PeerState;
|
use crate::network::PeerState;
|
||||||
|
use crate::audio::pw_cli::{AudioDevice, enumerate_audio_devices};
|
||||||
|
use crate::config::AppConfig;
|
||||||
|
|
||||||
use iced::widget::{
|
use iced::widget::{
|
||||||
container, column, row, text, button, text_input, scrollable, slider, checkbox, Column, Space,
|
container, column, row, text, button, text_input, scrollable, slider, checkbox, pick_list, Column, Space,
|
||||||
};
|
};
|
||||||
use iced::{
|
use iced::{
|
||||||
Color, Background, Border, Element, Subscription, Task, Theme, Event, keyboard,
|
Color, Background, Border, Element, Subscription, Task, Theme, Event, keyboard,
|
||||||
@@ -35,8 +37,8 @@ pub enum AppMessage {
|
|||||||
TogglePtt(bool),
|
TogglePtt(bool),
|
||||||
StartSettingHotkey,
|
StartSettingHotkey,
|
||||||
PeerVolumeChanged(EndpointId, f32),
|
PeerVolumeChanged(EndpointId, f32),
|
||||||
InputDeviceChanged(String),
|
InputDeviceSelected(AudioDevice),
|
||||||
OutputDeviceChanged(String),
|
OutputDeviceSelected(AudioDevice),
|
||||||
EventOccurred(Event),
|
EventOccurred(Event),
|
||||||
NavigateToSettings,
|
NavigateToSettings,
|
||||||
NavigateBack,
|
NavigateBack,
|
||||||
@@ -68,8 +70,11 @@ pub struct AppState {
|
|||||||
ptt_active: bool,
|
ptt_active: bool,
|
||||||
ptt_hotkey: keyboard::Key,
|
ptt_hotkey: keyboard::Key,
|
||||||
is_setting_hotkey: bool,
|
is_setting_hotkey: bool,
|
||||||
input_device: String,
|
input_devices: Vec<AudioDevice>,
|
||||||
output_device: String,
|
output_devices: Vec<AudioDevice>,
|
||||||
|
selected_input: Option<AudioDevice>,
|
||||||
|
selected_output: Option<AudioDevice>,
|
||||||
|
config: AppConfig,
|
||||||
peers: HashMap<EndpointId, PeerState>,
|
peers: HashMap<EndpointId, PeerState>,
|
||||||
peer_volumes: HashMap<EndpointId, f32>,
|
peer_volumes: HashMap<EndpointId, f32>,
|
||||||
audio_levels: HashMap<EndpointId, f32>,
|
audio_levels: HashMap<EndpointId, f32>,
|
||||||
@@ -83,6 +88,15 @@ impl Default for AppState {
|
|||||||
let controller = Arc::new(CoreController::new(ui_tx));
|
let controller = Arc::new(CoreController::new(ui_tx));
|
||||||
let _ = UI_RX.set(Mutex::new(Some(ui_rx)));
|
let _ = UI_RX.set(Mutex::new(Some(ui_rx)));
|
||||||
|
|
||||||
|
let config = AppConfig::load();
|
||||||
|
let all_devices = enumerate_audio_devices();
|
||||||
|
let input_devices: Vec<_> = all_devices.iter().filter(|d| d.is_input).cloned().collect();
|
||||||
|
let output_devices: Vec<_> = all_devices.iter().filter(|d| !d.is_input).cloned().collect();
|
||||||
|
|
||||||
|
let selected_input = input_devices.iter().find(|d| d.name == config.input_device).cloned();
|
||||||
|
let selected_output = output_devices.iter().find(|d| d.name == config.output_device).cloned();
|
||||||
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
name: "Peer".to_string(),
|
name: "Peer".to_string(),
|
||||||
ticket_input: "".to_string(),
|
ticket_input: "".to_string(),
|
||||||
@@ -95,8 +109,11 @@ impl Default for AppState {
|
|||||||
ptt_active: false,
|
ptt_active: false,
|
||||||
ptt_hotkey: keyboard::Key::Named(keyboard::key::Named::Space),
|
ptt_hotkey: keyboard::Key::Named(keyboard::key::Named::Space),
|
||||||
is_setting_hotkey: false,
|
is_setting_hotkey: false,
|
||||||
input_device: "".to_string(),
|
input_devices,
|
||||||
output_device: "".to_string(),
|
output_devices,
|
||||||
|
selected_input,
|
||||||
|
selected_output,
|
||||||
|
config,
|
||||||
peers: HashMap::new(),
|
peers: HashMap::new(),
|
||||||
peer_volumes: HashMap::new(),
|
peer_volumes: HashMap::new(),
|
||||||
audio_levels: HashMap::new(),
|
audio_levels: HashMap::new(),
|
||||||
@@ -139,21 +156,27 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
|||||||
state.ticket_input = val;
|
state.ticket_input = val;
|
||||||
}
|
}
|
||||||
AppMessage::JoinPressed => {
|
AppMessage::JoinPressed => {
|
||||||
state.status_message = "Connecting to room...".to_string();
|
let input_device = state.selected_input.as_ref().map(|d| d.name.clone());
|
||||||
let _ = state.controller.send(CoreCommand::Join {
|
let output_device = state.selected_output.as_ref().map(|d| d.name.clone());
|
||||||
name: state.name.clone(),
|
if !state.ticket_input.is_empty() {
|
||||||
ticket: state.ticket_input.clone(),
|
state.status_message = "Joining room...".to_string();
|
||||||
input_device: if state.input_device.is_empty() { None } else { Some(state.input_device.clone()) },
|
let _ = state.controller.send(CoreCommand::Join {
|
||||||
output_device: if state.output_device.is_empty() { None } else { Some(state.output_device.clone()) },
|
name: state.name.clone(),
|
||||||
});
|
ticket: state.ticket_input.clone(),
|
||||||
|
input_device,
|
||||||
|
output_device,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AppMessage::CreatePressed => {
|
AppMessage::CreatePressed => {
|
||||||
|
let input_device = state.selected_input.as_ref().map(|d| d.name.clone());
|
||||||
|
let output_device = state.selected_output.as_ref().map(|d| d.name.clone());
|
||||||
state.status_message = "Creating room...".to_string();
|
state.status_message = "Creating room...".to_string();
|
||||||
let _ = state.controller.send(CoreCommand::Join {
|
let _ = state.controller.send(CoreCommand::Join {
|
||||||
name: state.name.clone(),
|
name: state.name.clone(),
|
||||||
ticket: "".to_string(),
|
ticket: "create".to_string(),
|
||||||
input_device: if state.input_device.is_empty() { None } else { Some(state.input_device.clone()) },
|
input_device,
|
||||||
output_device: if state.output_device.is_empty() { None } else { Some(state.output_device.clone()) },
|
output_device,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
AppMessage::LeavePressed => {
|
AppMessage::LeavePressed => {
|
||||||
@@ -218,11 +241,15 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
|||||||
state.peer_volumes.insert(id, vol);
|
state.peer_volumes.insert(id, vol);
|
||||||
let _ = state.controller.send(CoreCommand::SetPeerVolume(id, vol));
|
let _ = state.controller.send(CoreCommand::SetPeerVolume(id, vol));
|
||||||
}
|
}
|
||||||
AppMessage::InputDeviceChanged(val) => {
|
AppMessage::InputDeviceSelected(dev) => {
|
||||||
state.input_device = val;
|
state.config.input_device = dev.name.clone();
|
||||||
|
state.config.save();
|
||||||
|
state.selected_input = Some(dev);
|
||||||
}
|
}
|
||||||
AppMessage::OutputDeviceChanged(val) => {
|
AppMessage::OutputDeviceSelected(dev) => {
|
||||||
state.output_device = val;
|
state.config.output_device = dev.name.clone();
|
||||||
|
state.config.save();
|
||||||
|
state.selected_output = Some(dev);
|
||||||
}
|
}
|
||||||
AppMessage::EventOccurred(Event::Keyboard(keyboard::Event::KeyPressed { key, .. })) => {
|
AppMessage::EventOccurred(Event::Keyboard(keyboard::Event::KeyPressed { key, .. })) => {
|
||||||
if state.is_setting_hotkey {
|
if state.is_setting_hotkey {
|
||||||
@@ -337,11 +364,25 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
column![
|
column![
|
||||||
text("Settings").size(24).color(color_blue),
|
text("Settings").size(24).color(color_blue),
|
||||||
vertical_space(20.0),
|
vertical_space(20.0),
|
||||||
text("Device Settings (Optional Node Target IDs)").size(14).color(color_subtext),
|
text("Device Settings").size(14).color(color_subtext),
|
||||||
row![
|
row![
|
||||||
text_input("Input Target", &state.input_device).on_input(AppMessage::InputDeviceChanged).style(t_style.clone()).padding(8),
|
column![
|
||||||
|
text("Input Target").size(12).color(color_subtext),
|
||||||
|
pick_list(
|
||||||
|
&state.input_devices[..],
|
||||||
|
state.selected_input.clone(),
|
||||||
|
AppMessage::InputDeviceSelected,
|
||||||
|
).width(iced::Length::Fixed(160.0))
|
||||||
|
].spacing(4),
|
||||||
horizontal_space(),
|
horizontal_space(),
|
||||||
text_input("Output Target", &state.output_device).on_input(AppMessage::OutputDeviceChanged).style(t_style.clone()).padding(8),
|
column![
|
||||||
|
text("Output Target").size(12).color(color_subtext),
|
||||||
|
pick_list(
|
||||||
|
&state.output_devices[..],
|
||||||
|
state.selected_output.clone(),
|
||||||
|
AppMessage::OutputDeviceSelected,
|
||||||
|
).width(iced::Length::Fixed(160.0))
|
||||||
|
].spacing(4),
|
||||||
].spacing(10),
|
].spacing(10),
|
||||||
vertical_space(30.0),
|
vertical_space(30.0),
|
||||||
button(
|
button(
|
||||||
|
|||||||
@@ -29,3 +29,4 @@ pub trait AudioBackend: Send + Sync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod pipewire_impl;
|
pub mod pipewire_impl;
|
||||||
|
pub mod pw_cli;
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct AudioDevice {
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub is_input: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for AudioDevice {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enumerate_audio_devices() -> Vec<AudioDevice> {
|
||||||
|
let mut devices = Vec::new();
|
||||||
|
let output = Command::new("pw-cli")
|
||||||
|
.arg("list-objects")
|
||||||
|
.arg("Node")
|
||||||
|
.output();
|
||||||
|
|
||||||
|
if let Ok(out) = output {
|
||||||
|
let text = String::from_utf8_lossy(&out.stdout);
|
||||||
|
let mut current_name = String::new();
|
||||||
|
let mut current_desc = String::new();
|
||||||
|
let mut current_class = String::new();
|
||||||
|
|
||||||
|
for line in text.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.starts_with("id ") {
|
||||||
|
// Save previous if valid
|
||||||
|
if !current_name.is_empty() && current_class.starts_with("Audio/") {
|
||||||
|
devices.push(AudioDevice {
|
||||||
|
name: current_name.clone(),
|
||||||
|
description: if current_desc.is_empty() { current_name.clone() } else { current_desc.clone() },
|
||||||
|
is_input: current_class == "Audio/Source",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
current_name.clear();
|
||||||
|
current_desc.clear();
|
||||||
|
current_class.clear();
|
||||||
|
} else if let Some(val) = line.strip_prefix("node.name = \"") {
|
||||||
|
current_name = val.trim_end_matches('"').to_string();
|
||||||
|
} else if let Some(val) = line.strip_prefix("node.description = \"") {
|
||||||
|
current_desc = val.trim_end_matches('"').to_string();
|
||||||
|
} else if let Some(val) = line.strip_prefix("media.class = \"") {
|
||||||
|
current_class = val.trim_end_matches('"').to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final one
|
||||||
|
if !current_name.is_empty() && current_class.starts_with("Audio/") {
|
||||||
|
devices.push(AudioDevice {
|
||||||
|
name: current_name.clone(),
|
||||||
|
description: if current_desc.is_empty() { current_name } else { current_desc },
|
||||||
|
is_input: current_class == "Audio/Source",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort devices for consistent display
|
||||||
|
devices.sort_by(|a, b| a.description.cmp(&b.description));
|
||||||
|
devices
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct AppConfig {
|
||||||
|
pub input_device: String,
|
||||||
|
pub output_device: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppConfig {
|
||||||
|
fn config_path() -> Option<PathBuf> {
|
||||||
|
dirs::config_dir().map(|mut p| {
|
||||||
|
p.push("peerspeak");
|
||||||
|
p.push("config.json");
|
||||||
|
p
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load() -> Self {
|
||||||
|
if let Some(path) = Self::config_path() {
|
||||||
|
if let Ok(contents) = fs::read_to_string(&path) {
|
||||||
|
if let Ok(config) = serde_json::from_str(&contents) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self) {
|
||||||
|
if let Some(path) = Self::config_path() {
|
||||||
|
if let Some(dir) = path.parent() {
|
||||||
|
let _ = fs::create_dir_all(dir);
|
||||||
|
}
|
||||||
|
if let Ok(json) = serde_json::to_string_pretty(self) {
|
||||||
|
let _ = fs::write(path, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ pub mod codec;
|
|||||||
pub mod network;
|
pub mod network;
|
||||||
pub mod core;
|
pub mod core;
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
pub mod config;
|
||||||
|
|
||||||
pub fn log_msg(msg: &str) {
|
pub fn log_msg(msg: &str) {
|
||||||
if let Ok(mut file) = std::fs::OpenOptions::new()
|
if let Ok(mut file) = std::fs::OpenOptions::new()
|
||||||
|
|||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open("src/app/mod.rs", "r") as f:
|
||||||
|
text = f.read()
|
||||||
|
|
||||||
|
# 1. Imports
|
||||||
|
text = text.replace(
|
||||||
|
"use crate::core::{CoreController, messages::{CoreCommand, UiEvent}};\nuse crate::network::PeerState;",
|
||||||
|
"use crate::core::{CoreController, messages::{CoreCommand, UiEvent}};\nuse crate::network::PeerState;\nuse crate::audio::pw_cli::{AudioDevice, enumerate_audio_devices};\nuse crate::config::AppConfig;"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Iced imports
|
||||||
|
text = text.replace(
|
||||||
|
"container, column, row, text, button, text_input, scrollable, slider, checkbox, Column, Space,",
|
||||||
|
"container, column, row, text, button, text_input, scrollable, slider, checkbox, pick_list, Column, Space,"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. AppMessage
|
||||||
|
text = text.replace(
|
||||||
|
"InputDeviceChanged(String),\n OutputDeviceChanged(String),",
|
||||||
|
"InputDeviceSelected(AudioDevice),\n OutputDeviceSelected(AudioDevice),"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. AppState
|
||||||
|
text = text.replace(
|
||||||
|
"input_device: String,\n output_device: String,",
|
||||||
|
"input_devices: Vec<AudioDevice>,\n output_devices: Vec<AudioDevice>,\n selected_input: Option<AudioDevice>,\n selected_output: Option<AudioDevice>,\n config: AppConfig,"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. AppState::default()
|
||||||
|
default_start = text.find("let _ = UI_RX.set(Mutex::new(Some(ui_rx)));")
|
||||||
|
if default_start != -1:
|
||||||
|
default_end = default_start + len("let _ = UI_RX.set(Mutex::new(Some(ui_rx)));")
|
||||||
|
insertion = """
|
||||||
|
let config = AppConfig::load();
|
||||||
|
let all_devices = enumerate_audio_devices();
|
||||||
|
let input_devices: Vec<_> = all_devices.iter().filter(|d| d.is_input).cloned().collect();
|
||||||
|
let output_devices: Vec<_> = all_devices.iter().filter(|d| !d.is_input).cloned().collect();
|
||||||
|
|
||||||
|
let selected_input = input_devices.iter().find(|d| d.name == config.input_device).cloned();
|
||||||
|
let selected_output = output_devices.iter().find(|d| d.name == config.output_device).cloned();
|
||||||
|
"""
|
||||||
|
text = text[:default_end] + "\n" + insertion + text[default_end:]
|
||||||
|
|
||||||
|
text = text.replace(
|
||||||
|
"input_device: \"\".to_string(),\n output_device: \"\".to_string(),",
|
||||||
|
"input_devices,\n output_devices,\n selected_input,\n selected_output,\n config,"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 6. JoinPressed / CreatePressed
|
||||||
|
join_old = """ AppMessage::JoinPressed => {
|
||||||
|
state.status_message = "Connecting to room...".to_string();
|
||||||
|
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()) },
|
||||||
|
});
|
||||||
|
}"""
|
||||||
|
join_new = """ AppMessage::JoinPressed => {
|
||||||
|
let input_device = state.selected_input.as_ref().map(|d| d.name.clone());
|
||||||
|
let output_device = state.selected_output.as_ref().map(|d| d.name.clone());
|
||||||
|
if !state.ticket_input.is_empty() {
|
||||||
|
state.status_message = "Joining room...".to_string();
|
||||||
|
let _ = state.controller.send(CoreCommand::Join {
|
||||||
|
name: state.name.clone(),
|
||||||
|
ticket: state.ticket_input.clone(),
|
||||||
|
input_device,
|
||||||
|
output_device,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
text = text.replace(join_old, join_new)
|
||||||
|
|
||||||
|
create_old = """ AppMessage::CreatePressed => {
|
||||||
|
state.status_message = "Creating room...".to_string();
|
||||||
|
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()) },
|
||||||
|
});
|
||||||
|
}"""
|
||||||
|
create_new = """ AppMessage::CreatePressed => {
|
||||||
|
let input_device = state.selected_input.as_ref().map(|d| d.name.clone());
|
||||||
|
let output_device = state.selected_output.as_ref().map(|d| d.name.clone());
|
||||||
|
state.status_message = "Creating room...".to_string();
|
||||||
|
let _ = state.controller.send(CoreCommand::Join {
|
||||||
|
name: state.name.clone(),
|
||||||
|
ticket: "create".to_string(),
|
||||||
|
input_device,
|
||||||
|
output_device,
|
||||||
|
});
|
||||||
|
}"""
|
||||||
|
text = text.replace(create_old, create_new)
|
||||||
|
|
||||||
|
# 7. DeviceChanged -> DeviceSelected
|
||||||
|
text = text.replace(
|
||||||
|
"AppMessage::InputDeviceChanged(val) => {\n state.input_device = val;\n }",
|
||||||
|
"AppMessage::InputDeviceSelected(dev) => {\n state.config.input_device = dev.name.clone();\n state.config.save();\n state.selected_input = Some(dev);\n }"
|
||||||
|
)
|
||||||
|
text = text.replace(
|
||||||
|
"AppMessage::OutputDeviceChanged(val) => {\n state.output_device = val;\n }",
|
||||||
|
"AppMessage::OutputDeviceSelected(dev) => {\n state.config.output_device = dev.name.clone();\n state.config.save();\n state.selected_output = Some(dev);\n }"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 8. View Settings
|
||||||
|
settings_old = """ 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),"""
|
||||||
|
|
||||||
|
settings_new = """ text("Device Settings").size(14).color(color_subtext),
|
||||||
|
row![
|
||||||
|
column![
|
||||||
|
text("Input Target").size(12).color(color_subtext),
|
||||||
|
pick_list(
|
||||||
|
&state.input_devices[..],
|
||||||
|
state.selected_input.clone(),
|
||||||
|
AppMessage::InputDeviceSelected,
|
||||||
|
).width(iced::Length::Fixed(160.0))
|
||||||
|
].spacing(4),
|
||||||
|
horizontal_space(),
|
||||||
|
column![
|
||||||
|
text("Output Target").size(12).color(color_subtext),
|
||||||
|
pick_list(
|
||||||
|
&state.output_devices[..],
|
||||||
|
state.selected_output.clone(),
|
||||||
|
AppMessage::OutputDeviceSelected,
|
||||||
|
).width(iced::Length::Fixed(160.0))
|
||||||
|
].spacing(4),
|
||||||
|
].spacing(10),"""
|
||||||
|
|
||||||
|
text = text.replace(settings_old, settings_new)
|
||||||
|
|
||||||
|
with open("src/app/mod.rs", "w") as f:
|
||||||
|
f.write(text)
|
||||||
Reference in New Issue
Block a user