mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-01 07:39:11 +00:00
linux-rust: add bt communication to ui
This commit is contained in:
@@ -9,6 +9,7 @@ use ksni::Handle;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::{sleep, Duration};
|
||||
use crate::ui::tray::MyTray;
|
||||
use crate::ui::messages::UIMessage;
|
||||
|
||||
pub struct AirPodsDevice {
|
||||
pub mac_address: Address,
|
||||
@@ -18,7 +19,7 @@ pub struct AirPodsDevice {
|
||||
}
|
||||
|
||||
impl AirPodsDevice {
|
||||
pub async fn new(mac_address: Address, tray_handle: Option<Handle<MyTray>>) -> Self {
|
||||
pub async fn new(mac_address: Address, tray_handle: Option<Handle<MyTray>>, ui_tx: tokio::sync::mpsc::UnboundedSender<UIMessage>) -> Self {
|
||||
info!("Creating new AirPodsDevice for {}", mac_address);
|
||||
let mut aacp_manager = AACPManager::new();
|
||||
aacp_manager.connect(mac_address).await;
|
||||
@@ -146,7 +147,9 @@ impl AirPodsDevice {
|
||||
let aacp_manager_clone_events = aacp_manager.clone();
|
||||
let local_mac_events = local_mac.clone();
|
||||
tokio::spawn(async move {
|
||||
let ui_tx_clone = ui_tx.clone();
|
||||
while let Some(event) = rx.recv().await {
|
||||
let event_clone = event.clone();
|
||||
match event {
|
||||
AACPEvent::EarDetection(old_status, new_status) => {
|
||||
debug!("Received EarDetection event: old_status={:?}, new_status={:?}", old_status, new_status);
|
||||
@@ -178,9 +181,14 @@ impl AirPodsDevice {
|
||||
}).await;
|
||||
}
|
||||
debug!("Updated tray with new battery info");
|
||||
|
||||
let _ = ui_tx_clone.send(UIMessage::AACPUIEvent(mac_address.to_string(), event_clone));
|
||||
debug!("Sent BatteryInfo event to UI");
|
||||
}
|
||||
AACPEvent::ControlCommand(status) => {
|
||||
debug!("Received ControlCommand event: {:?}", status);
|
||||
let _ = ui_tx_clone.send(UIMessage::AACPUIEvent(mac_address.to_string(), event_clone));
|
||||
debug!("Sent ControlCommand event to UI");
|
||||
}
|
||||
AACPEvent::ConversationalAwareness(status) => {
|
||||
debug!("Received ConversationalAwareness event: {}", status);
|
||||
@@ -218,7 +226,11 @@ impl AirPodsDevice {
|
||||
controller.pause_all_media().await;
|
||||
controller.deactivate_a2dp_profile().await;
|
||||
}
|
||||
_ => {}
|
||||
_ => {
|
||||
debug!("Received unhandled AACP event: {:?}", event);
|
||||
let _ = ui_tx_clone.send(UIMessage::AACPUIEvent(mac_address.to_string(), event_clone));
|
||||
debug!("Sent unhandled AACP event to UI");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ use tokio::time::{sleep, Instant};
|
||||
use std::collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use std::path::PathBuf;
|
||||
use crate::utils::get_devices_path;
|
||||
|
||||
const PSM: u16 = 0x1001;
|
||||
@@ -127,6 +126,49 @@ impl ControlCommandIdentifiers {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ControlCommandIdentifiers {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let name = match self {
|
||||
ControlCommandIdentifiers::MicMode => "Mic Mode",
|
||||
ControlCommandIdentifiers::ButtonSendMode => "Button Send Mode",
|
||||
ControlCommandIdentifiers::VoiceTrigger => "Voice Trigger",
|
||||
ControlCommandIdentifiers::SingleClickMode => "Single Click Mode",
|
||||
ControlCommandIdentifiers::DoubleClickMode => "Double Click Mode",
|
||||
ControlCommandIdentifiers::ClickHoldMode => "Click Hold Mode",
|
||||
ControlCommandIdentifiers::DoubleClickInterval => "Double Click Interval",
|
||||
ControlCommandIdentifiers::ClickHoldInterval => "Click Hold Interval",
|
||||
ControlCommandIdentifiers::ListeningModeConfigs => "Listening Mode Configs",
|
||||
ControlCommandIdentifiers::OneBudAncMode => "One Bud ANC Mode",
|
||||
ControlCommandIdentifiers::CrownRotationDirection => "Crown Rotation Direction",
|
||||
ControlCommandIdentifiers::ListeningMode => "Listening Mode",
|
||||
ControlCommandIdentifiers::AutoAnswerMode => "Auto Answer Mode",
|
||||
ControlCommandIdentifiers::ChimeVolume => "Chime Volume",
|
||||
ControlCommandIdentifiers::VolumeSwipeInterval => "Volume Swipe Interval",
|
||||
ControlCommandIdentifiers::CallManagementConfig => "Call Management Config",
|
||||
ControlCommandIdentifiers::VolumeSwipeMode => "Volume Swipe Mode",
|
||||
ControlCommandIdentifiers::AdaptiveVolumeConfig => "Adaptive Volume Config",
|
||||
ControlCommandIdentifiers::SoftwareMuteConfig => "Software Mute Config",
|
||||
ControlCommandIdentifiers::ConversationDetectConfig => "Conversation Detect Config",
|
||||
ControlCommandIdentifiers::Ssl => "SSL",
|
||||
ControlCommandIdentifiers::HearingAid => "Hearing Aid",
|
||||
ControlCommandIdentifiers::AutoAncStrength => "Auto ANC Strength",
|
||||
ControlCommandIdentifiers::HpsGainSwipe => "HPS Gain Swipe",
|
||||
ControlCommandIdentifiers::HrmState => "HRM State",
|
||||
ControlCommandIdentifiers::InCaseToneConfig => "In Case Tone Config",
|
||||
ControlCommandIdentifiers::SiriMultitoneConfig => "Siri Multitone Config",
|
||||
ControlCommandIdentifiers::HearingAssistConfig => "Hearing Assist Config",
|
||||
ControlCommandIdentifiers::AllowOffOption => "Allow Off Option",
|
||||
ControlCommandIdentifiers::StemConfig => "Stem Config",
|
||||
ControlCommandIdentifiers::SleepDetectionConfig => "Sleep Detection Config",
|
||||
ControlCommandIdentifiers::AllowAutoConnect => "Allow Auto Connect",
|
||||
ControlCommandIdentifiers::EarDetectionConfig => "Ear Detection Config",
|
||||
ControlCommandIdentifiers::AutomaticConnectionConfig => "Automatic Connection Config",
|
||||
ControlCommandIdentifiers::OwnsConnection => "Owns Connection",
|
||||
};
|
||||
write!(f, "{}", name)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
pub enum ProximityKeyType {
|
||||
|
||||
@@ -19,6 +19,7 @@ use crate::ui::tray::MyTray;
|
||||
use clap::Parser;
|
||||
use crate::bluetooth::le::start_le_monitor;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
use crate::ui::messages::UIMessage;
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
@@ -34,11 +35,11 @@ fn main() -> iced::Result {
|
||||
let args = Args::parse();
|
||||
let log_level = if args.debug { "debug" } else { "info" };
|
||||
if env::var("RUST_LOG").is_err() {
|
||||
unsafe { env::set_var("RUST_LOG", log_level); }
|
||||
unsafe { env::set_var("RUST_LOG", log_level.to_owned() + ",wgpu_core=off,librepods_rust::bluetooth::le=off,cosmic_text=off,naga=off,iced_winit=off") };
|
||||
}
|
||||
env_logger::init();
|
||||
|
||||
let (ui_tx, ui_rx) = unbounded_channel::<()>();
|
||||
let (ui_tx, ui_rx) = unbounded_channel::<UIMessage>();
|
||||
std::thread::spawn(|| {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async_main(ui_tx)).unwrap();
|
||||
@@ -48,7 +49,7 @@ fn main() -> iced::Result {
|
||||
}
|
||||
|
||||
|
||||
async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender<()>) -> bluer::Result<()> {
|
||||
async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender<UIMessage>) -> bluer::Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let tray_handle = if args.no_tray {
|
||||
@@ -66,7 +67,7 @@ async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender<()>) -> bluer::Res
|
||||
listening_mode: None,
|
||||
allow_off_option: None,
|
||||
command_tx: None,
|
||||
ui_tx: Some(ui_tx),
|
||||
ui_tx: Some(ui_tx.clone()),
|
||||
};
|
||||
let handle = tray.spawn().await.unwrap();
|
||||
Some(handle)
|
||||
@@ -91,7 +92,9 @@ async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender<()>) -> bluer::Res
|
||||
Ok(device) => {
|
||||
let name = device.name().await?.unwrap_or_else(|| "Unknown".to_string());
|
||||
info!("Found connected AirPods: {}, initializing.", name);
|
||||
let _airpods_device = AirPodsDevice::new(device.address(), tray_handle.clone()).await;
|
||||
let ui_tx_clone = ui_tx.clone();
|
||||
ui_tx_clone.send(UIMessage::DeviceConnected(device.address().to_string())).unwrap();
|
||||
let _airpods_device = AirPodsDevice::new(device.address(), tray_handle.clone(), ui_tx_clone).await;
|
||||
}
|
||||
Err(_) => {
|
||||
info!("No connected AirPods found.");
|
||||
@@ -128,8 +131,10 @@ async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender<()>) -> bluer::Res
|
||||
let Ok(addr) = addr_str.parse::<Address>() else { return true; };
|
||||
info!("AirPods connected: {}, initializing", name);
|
||||
let handle_clone = tray_handle.clone();
|
||||
let ui_tx_clone = ui_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let _airpods_device = AirPodsDevice::new(addr, handle_clone).await;
|
||||
ui_tx_clone.send(UIMessage::DeviceConnected(addr_str)).unwrap();
|
||||
let _airpods_device = AirPodsDevice::new(addr, handle_clone, ui_tx_clone).await;
|
||||
});
|
||||
true
|
||||
})?;
|
||||
|
||||
15
linux-rust/src/ui/messages.rs
Normal file
15
linux-rust/src/ui/messages.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use crate::bluetooth::aacp::{AACPEvent, ControlCommandIdentifiers};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum UIMessage {
|
||||
OpenWindow,
|
||||
DeviceConnected(String),
|
||||
DeviceDisconnected(String),
|
||||
AACPUIEvent(String, AACPEvent),
|
||||
NoOp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum UICommand {
|
||||
SetControlCommandStatus(String, ControlCommandIdentifiers, Vec<u8>),
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod tray;
|
||||
pub mod window;
|
||||
pub mod window;
|
||||
pub mod messages;
|
||||
@@ -5,6 +5,7 @@ use ksni::{Icon, ToolTip};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::bluetooth::aacp::ControlCommandIdentifiers;
|
||||
use crate::ui::messages::UIMessage;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MyTray {
|
||||
@@ -18,8 +19,8 @@ pub(crate) struct MyTray {
|
||||
pub(crate) connected: bool,
|
||||
pub(crate) listening_mode: Option<u8>,
|
||||
pub(crate) allow_off_option: Option<u8>,
|
||||
pub(crate) command_tx: Option<tokio::sync::mpsc::UnboundedSender<(ControlCommandIdentifiers, Vec<u8>)>>,
|
||||
pub(crate) ui_tx: Option<UnboundedSender<()>>,
|
||||
pub(crate) command_tx: Option<UnboundedSender<(ControlCommandIdentifiers, Vec<u8>)>>,
|
||||
pub(crate) ui_tx: Option<UnboundedSender<UIMessage>>,
|
||||
}
|
||||
|
||||
impl ksni::Tray for MyTray {
|
||||
@@ -113,7 +114,7 @@ impl ksni::Tray for MyTray {
|
||||
icon_name: "window-new".into(),
|
||||
activate: Box::new(|this: &mut Self| {
|
||||
if let Some(tx) = &this.ui_tx {
|
||||
let _ = tx.send(());
|
||||
let _ = tx.send(UIMessage::OpenWindow);
|
||||
}
|
||||
}),
|
||||
..Default::default()
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
use iced::widget::button::Style;
|
||||
use iced::widget::{button, column, container, pane_grid, text, Space, combo_box, row, text_input};
|
||||
use iced::{daemon, window, Background, Border, Center, Color, Element, Length, Subscription, Task, Theme};
|
||||
use iced::{daemon, window, Background, Border, Center, Color, Element, Length, Size, Subscription, Task, Theme};
|
||||
use std::sync::Arc;
|
||||
use iced::border::Radius;
|
||||
use iced::overlay::menu;
|
||||
use log::{debug, error};
|
||||
use tokio::sync::{mpsc::UnboundedReceiver, Mutex};
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
use tokio::sync::Mutex;
|
||||
use crate::bluetooth::aacp::{DeviceData, DeviceInformation, DeviceType};
|
||||
use crate::ui::messages::UIMessage;
|
||||
use crate::utils::{get_devices_path, get_app_settings_path, MyTheme};
|
||||
|
||||
pub fn start_ui(ui_rx: UnboundedReceiver<()>, start_minimized: bool) -> iced::Result {
|
||||
pub fn start_ui(ui_rx: UnboundedReceiver<UIMessage>, start_minimized: bool) -> iced::Result {
|
||||
daemon(App::title, App::update, App::view)
|
||||
.subscription(App::subscription)
|
||||
.theme(App::theme)
|
||||
@@ -21,9 +24,22 @@ pub struct App {
|
||||
window: Option<window::Id>,
|
||||
panes: pane_grid::State<Pane>,
|
||||
selected_tab: Tab,
|
||||
ui_rx: Arc<Mutex<UnboundedReceiver<()>>>,
|
||||
theme_state: combo_box::State<MyTheme>,
|
||||
selected_theme: MyTheme,
|
||||
ui_rx: Arc<Mutex<UnboundedReceiver<UIMessage>>>,
|
||||
bluetooth_state: BluetoothState
|
||||
}
|
||||
|
||||
pub struct BluetoothState {
|
||||
connected_devices: Vec<String>
|
||||
}
|
||||
|
||||
impl BluetoothState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
connected_devices: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -32,9 +48,9 @@ pub enum Message {
|
||||
WindowClosed(window::Id),
|
||||
Resized(pane_grid::ResizeEvent),
|
||||
SelectTab(Tab),
|
||||
OpenMainWindow,
|
||||
ThemeSelected(MyTheme),
|
||||
CopyToClipboard(String),
|
||||
UIMessage(UIMessage),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
@@ -51,21 +67,25 @@ pub enum Pane {
|
||||
|
||||
|
||||
impl App {
|
||||
pub fn new(ui_rx: UnboundedReceiver<()>, start_minimized: bool) -> (Self, Task<Message>) {
|
||||
pub fn new(ui_rx: UnboundedReceiver<UIMessage>, start_minimized: bool) -> (Self, Task<Message>) {
|
||||
let (mut panes, first_pane) = pane_grid::State::new(Pane::Sidebar);
|
||||
let split = panes.split(pane_grid::Axis::Vertical, first_pane, Pane::Content);
|
||||
panes.resize(split.unwrap().1, 0.2);
|
||||
|
||||
let ui_rx = Arc::new(Mutex::new(ui_rx));
|
||||
|
||||
let wait_task = Task::perform(
|
||||
wait_for_message(Arc::clone(&ui_rx)),
|
||||
|_| Message::OpenMainWindow,
|
||||
|msg| msg,
|
||||
);
|
||||
|
||||
let (window, open_task) = if start_minimized {
|
||||
(None, Task::none())
|
||||
} else {
|
||||
let (id, open) = window::open(window::Settings::default());
|
||||
let mut settings = window::Settings::default();
|
||||
settings.min_size = Some(Size::new(400.0, 300.0));
|
||||
settings.icon = window::icon::from_file("../../assets/icon.png").ok();
|
||||
let (id, open) = window::open(settings);
|
||||
(Some(id), open.map(Message::WindowOpened))
|
||||
};
|
||||
|
||||
@@ -77,12 +97,13 @@ impl App {
|
||||
.and_then(|t| serde_json::from_value(t).ok())
|
||||
.unwrap_or(MyTheme::Dark);
|
||||
|
||||
let bluetooth_state = BluetoothState::new();
|
||||
|
||||
(
|
||||
Self {
|
||||
window,
|
||||
panes,
|
||||
selected_tab: Tab::Device("none".to_string()),
|
||||
ui_rx,
|
||||
theme_state: combo_box::State::new(vec![
|
||||
MyTheme::Light,
|
||||
MyTheme::Dark,
|
||||
@@ -108,8 +129,10 @@ impl App {
|
||||
MyTheme::Ferra,
|
||||
]),
|
||||
selected_theme,
|
||||
ui_rx,
|
||||
bluetooth_state,
|
||||
},
|
||||
Task::batch(vec![open_task, wait_task]),
|
||||
Task::batch(vec![open_task, wait_task])
|
||||
)
|
||||
}
|
||||
|
||||
@@ -127,11 +150,7 @@ impl App {
|
||||
if self.window == Some(id) {
|
||||
self.window = None;
|
||||
}
|
||||
let wait_task = Task::perform(
|
||||
wait_for_message(Arc::clone(&self.ui_rx)),
|
||||
|_| Message::OpenMainWindow,
|
||||
);
|
||||
wait_task
|
||||
Task::none()
|
||||
}
|
||||
Message::Resized(event) => {
|
||||
self.panes.resize(event.split, event.ratio);
|
||||
@@ -141,17 +160,6 @@ impl App {
|
||||
self.selected_tab = tab;
|
||||
Task::none()
|
||||
}
|
||||
Message::OpenMainWindow => {
|
||||
if let Some(window_id) = self.window {
|
||||
Task::batch(vec![
|
||||
window::gain_focus(window_id),
|
||||
])
|
||||
} else {
|
||||
let (new_window_task, open_task) = window::open(window::Settings::default());
|
||||
self.window = Some(new_window_task);
|
||||
open_task.map(Message::WindowOpened)
|
||||
}
|
||||
}
|
||||
Message::ThemeSelected(theme) => {
|
||||
self.selected_theme = theme;
|
||||
let app_settings_path = get_app_settings_path();
|
||||
@@ -163,6 +171,86 @@ impl App {
|
||||
Message::CopyToClipboard(data) => {
|
||||
iced::clipboard::write(data)
|
||||
}
|
||||
Message::UIMessage(ui_message) => {
|
||||
match ui_message {
|
||||
UIMessage::NoOp => {
|
||||
let ui_rx = Arc::clone(&self.ui_rx);
|
||||
let wait_task = Task::perform(
|
||||
wait_for_message(ui_rx),
|
||||
|msg| msg,
|
||||
);
|
||||
wait_task
|
||||
}
|
||||
UIMessage::OpenWindow => {
|
||||
let ui_rx = Arc::clone(&self.ui_rx);
|
||||
let wait_task = Task::perform(
|
||||
wait_for_message(ui_rx),
|
||||
|msg| msg,
|
||||
);
|
||||
debug!("Opening main window...");
|
||||
if let Some(window_id) = self.window {
|
||||
Task::batch(vec![
|
||||
window::gain_focus(window_id),
|
||||
wait_task,
|
||||
])
|
||||
} else {
|
||||
let mut settings = window::Settings::default();
|
||||
settings.min_size = Some(Size::new(400.0, 300.0));
|
||||
settings.icon = window::icon::from_file("../../assets/icon.png").ok();
|
||||
let (new_window_task, open_task) = window::open(settings);
|
||||
self.window = Some(new_window_task);
|
||||
Task::batch(vec![
|
||||
open_task.map(Message::WindowOpened),
|
||||
wait_task,
|
||||
])
|
||||
}
|
||||
}
|
||||
UIMessage::DeviceConnected(mac) => {
|
||||
let ui_rx = Arc::clone(&self.ui_rx);
|
||||
let wait_task = Task::perform(
|
||||
wait_for_message(ui_rx),
|
||||
|msg| msg,
|
||||
);
|
||||
debug!("Device connected: {}. Adding to connected devices list", mac);
|
||||
let mut already_connected = false;
|
||||
for device in &self.bluetooth_state.connected_devices {
|
||||
if device == &mac {
|
||||
already_connected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !already_connected {
|
||||
self.bluetooth_state.connected_devices.push(mac.clone());
|
||||
}
|
||||
|
||||
Task::batch(vec![
|
||||
wait_task,
|
||||
])
|
||||
}
|
||||
UIMessage::DeviceDisconnected(mac) => {
|
||||
let ui_rx = Arc::clone(&self.ui_rx);
|
||||
let wait_task = Task::perform(
|
||||
wait_for_message(ui_rx),
|
||||
|msg| msg,
|
||||
);
|
||||
debug!("Device disconnected: {}", mac);
|
||||
Task::batch(vec![
|
||||
wait_task,
|
||||
])
|
||||
}
|
||||
UIMessage::AACPUIEvent(mac, event) => {
|
||||
let ui_rx = Arc::clone(&self.ui_rx);
|
||||
let wait_task = Task::perform(
|
||||
wait_for_message(ui_rx),
|
||||
|msg| msg,
|
||||
);
|
||||
debug!("AACP UI Event for {}: {:?}", mac, event);
|
||||
Task::batch(vec![
|
||||
wait_task,
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,30 +266,35 @@ impl App {
|
||||
let pane_grid = pane_grid::PaneGrid::new(&self.panes, |_pane_id, pane, _is_maximized| {
|
||||
match pane {
|
||||
Pane::Sidebar => {
|
||||
let create_tab_button = |tab: Tab, label: &str, description: Option<&str>| -> Element<'_, Message> {
|
||||
let create_tab_button = |tab: Tab, label: &str, description: &str, connected: bool| -> Element<'_, Message> {
|
||||
let label = label.to_string();
|
||||
let description = description.map(|d| d.to_string());
|
||||
let is_selected = self.selected_tab == tab;
|
||||
let mut col = column![text(label).size(18)];
|
||||
if let Some(desc) = description {
|
||||
col = col.push(text(desc).size(12));
|
||||
}
|
||||
let col = column![
|
||||
text(label).size(16),
|
||||
text(
|
||||
if connected {
|
||||
format!("Connected - {}", description)
|
||||
} else {
|
||||
format!("{}", description)
|
||||
}
|
||||
).size(12)
|
||||
];
|
||||
let content = container(col)
|
||||
.padding(10);
|
||||
.padding(8);
|
||||
let style = move |theme: &Theme, _status| {
|
||||
if is_selected {
|
||||
let mut style = Style::default()
|
||||
.with_background(theme.palette().primary);
|
||||
let mut border = Border::default();
|
||||
border.color = theme.palette().text;
|
||||
style.border = border.rounded(20);
|
||||
style.border = border.rounded(12);
|
||||
style
|
||||
} else {
|
||||
let mut style = Style::default()
|
||||
.with_background(theme.palette().primary.scale_alpha(0.1));
|
||||
let mut border = Border::default();
|
||||
border.color = theme.palette().primary.scale_alpha(0.1);
|
||||
style.border = border.rounded(10);
|
||||
style.border = border.rounded(8);
|
||||
style.text_color = theme.palette().text;
|
||||
style
|
||||
}
|
||||
@@ -214,6 +307,38 @@ impl App {
|
||||
.into()
|
||||
};
|
||||
|
||||
let create_settings_button = || -> Element<'_, Message> {
|
||||
let label = "Settings".to_string();
|
||||
let is_selected = self.selected_tab == Tab::Settings;
|
||||
let col = column![text(label).size(16)];
|
||||
let content = container(col)
|
||||
.padding(8);
|
||||
let style = move |theme: &Theme, _status| {
|
||||
if is_selected {
|
||||
let mut style = Style::default()
|
||||
.with_background(theme.palette().primary);
|
||||
let mut border = Border::default();
|
||||
border.color = theme.palette().text;
|
||||
style.border = border.rounded(12);
|
||||
style
|
||||
} else {
|
||||
let mut style = Style::default()
|
||||
.with_background(theme.palette().primary.scale_alpha(0.1));
|
||||
let mut border = Border::default();
|
||||
border.color = theme.palette().primary.scale_alpha(0.1);
|
||||
style.border = border.rounded(8);
|
||||
style.text_color = theme.palette().text;
|
||||
style
|
||||
}
|
||||
};
|
||||
button(content)
|
||||
.style(style)
|
||||
.padding(5)
|
||||
.on_press(Message::SelectTab(Tab::Settings))
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
};
|
||||
|
||||
let mut devices = column!().spacing(4);
|
||||
let mut devices_vec: Vec<(String, DeviceData)> = devices_list.clone().into_iter().collect();
|
||||
devices_vec.sort_by(|a, b| a.1.name.cmp(&b.1.name));
|
||||
@@ -222,12 +347,13 @@ impl App {
|
||||
let tab_button = create_tab_button(
|
||||
Tab::Device(mac.clone()),
|
||||
&name,
|
||||
Some(&mac)
|
||||
&mac,
|
||||
self.bluetooth_state.connected_devices.contains(&mac)
|
||||
);
|
||||
devices = devices.push(tab_button);
|
||||
}
|
||||
|
||||
let settings = create_tab_button(Tab::Settings, "Settings", None);
|
||||
let settings = create_settings_button();
|
||||
|
||||
let content = column![
|
||||
devices,
|
||||
@@ -522,8 +648,15 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_message(rx: Arc<Mutex<UnboundedReceiver<()>>>) {
|
||||
debug!("Waiting for message to open main window...");
|
||||
let mut guard = rx.lock().await;
|
||||
let _ = guard.recv().await;
|
||||
async fn wait_for_message(
|
||||
ui_rx: Arc<Mutex<UnboundedReceiver<UIMessage>>>,
|
||||
) -> Message {
|
||||
let mut rx = ui_rx.lock().await;
|
||||
match rx.recv().await {
|
||||
Some(msg) => Message::UIMessage(msg),
|
||||
None => {
|
||||
error!("UI message channel closed");
|
||||
Message::UIMessage(UIMessage::NoOp)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user