mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-04-21 21:52:33 +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::sync::Mutex;
|
||||||
use tokio::time::{sleep, Duration};
|
use tokio::time::{sleep, Duration};
|
||||||
use crate::ui::tray::MyTray;
|
use crate::ui::tray::MyTray;
|
||||||
|
use crate::ui::messages::UIMessage;
|
||||||
|
|
||||||
pub struct AirPodsDevice {
|
pub struct AirPodsDevice {
|
||||||
pub mac_address: Address,
|
pub mac_address: Address,
|
||||||
@@ -18,7 +19,7 @@ pub struct AirPodsDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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);
|
info!("Creating new AirPodsDevice for {}", mac_address);
|
||||||
let mut aacp_manager = AACPManager::new();
|
let mut aacp_manager = AACPManager::new();
|
||||||
aacp_manager.connect(mac_address).await;
|
aacp_manager.connect(mac_address).await;
|
||||||
@@ -146,7 +147,9 @@ impl AirPodsDevice {
|
|||||||
let aacp_manager_clone_events = aacp_manager.clone();
|
let aacp_manager_clone_events = aacp_manager.clone();
|
||||||
let local_mac_events = local_mac.clone();
|
let local_mac_events = local_mac.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
let ui_tx_clone = ui_tx.clone();
|
||||||
while let Some(event) = rx.recv().await {
|
while let Some(event) = rx.recv().await {
|
||||||
|
let event_clone = event.clone();
|
||||||
match event {
|
match event {
|
||||||
AACPEvent::EarDetection(old_status, new_status) => {
|
AACPEvent::EarDetection(old_status, new_status) => {
|
||||||
debug!("Received EarDetection event: old_status={:?}, new_status={:?}", old_status, new_status);
|
debug!("Received EarDetection event: old_status={:?}, new_status={:?}", old_status, new_status);
|
||||||
@@ -178,9 +181,14 @@ impl AirPodsDevice {
|
|||||||
}).await;
|
}).await;
|
||||||
}
|
}
|
||||||
debug!("Updated tray with new battery info");
|
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) => {
|
AACPEvent::ControlCommand(status) => {
|
||||||
debug!("Received ControlCommand event: {:?}", 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) => {
|
AACPEvent::ConversationalAwareness(status) => {
|
||||||
debug!("Received ConversationalAwareness event: {}", status);
|
debug!("Received ConversationalAwareness event: {}", status);
|
||||||
@@ -218,7 +226,11 @@ impl AirPodsDevice {
|
|||||||
controller.pause_all_media().await;
|
controller.pause_all_media().await;
|
||||||
controller.deactivate_a2dp_profile().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 std::collections::HashMap;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use std::path::PathBuf;
|
|
||||||
use crate::utils::get_devices_path;
|
use crate::utils::get_devices_path;
|
||||||
|
|
||||||
const PSM: u16 = 0x1001;
|
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)]
|
#[repr(u8)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||||
pub enum ProximityKeyType {
|
pub enum ProximityKeyType {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use crate::ui::tray::MyTray;
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use crate::bluetooth::le::start_le_monitor;
|
use crate::bluetooth::le::start_le_monitor;
|
||||||
use tokio::sync::mpsc::unbounded_channel;
|
use tokio::sync::mpsc::unbounded_channel;
|
||||||
|
use crate::ui::messages::UIMessage;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
struct Args {
|
struct Args {
|
||||||
@@ -34,11 +35,11 @@ fn main() -> iced::Result {
|
|||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
let log_level = if args.debug { "debug" } else { "info" };
|
let log_level = if args.debug { "debug" } else { "info" };
|
||||||
if env::var("RUST_LOG").is_err() {
|
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();
|
env_logger::init();
|
||||||
|
|
||||||
let (ui_tx, ui_rx) = unbounded_channel::<()>();
|
let (ui_tx, ui_rx) = unbounded_channel::<UIMessage>();
|
||||||
std::thread::spawn(|| {
|
std::thread::spawn(|| {
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
rt.block_on(async_main(ui_tx)).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 args = Args::parse();
|
||||||
|
|
||||||
let tray_handle = if args.no_tray {
|
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,
|
listening_mode: None,
|
||||||
allow_off_option: None,
|
allow_off_option: None,
|
||||||
command_tx: None,
|
command_tx: None,
|
||||||
ui_tx: Some(ui_tx),
|
ui_tx: Some(ui_tx.clone()),
|
||||||
};
|
};
|
||||||
let handle = tray.spawn().await.unwrap();
|
let handle = tray.spawn().await.unwrap();
|
||||||
Some(handle)
|
Some(handle)
|
||||||
@@ -91,7 +92,9 @@ async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender<()>) -> bluer::Res
|
|||||||
Ok(device) => {
|
Ok(device) => {
|
||||||
let name = device.name().await?.unwrap_or_else(|| "Unknown".to_string());
|
let name = device.name().await?.unwrap_or_else(|| "Unknown".to_string());
|
||||||
info!("Found connected AirPods: {}, initializing.", name);
|
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(_) => {
|
Err(_) => {
|
||||||
info!("No connected AirPods found.");
|
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; };
|
let Ok(addr) = addr_str.parse::<Address>() else { return true; };
|
||||||
info!("AirPods connected: {}, initializing", name);
|
info!("AirPods connected: {}, initializing", name);
|
||||||
let handle_clone = tray_handle.clone();
|
let handle_clone = tray_handle.clone();
|
||||||
|
let ui_tx_clone = ui_tx.clone();
|
||||||
tokio::spawn(async move {
|
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
|
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 tray;
|
||||||
pub mod window;
|
pub mod window;
|
||||||
|
pub mod messages;
|
||||||
@@ -5,6 +5,7 @@ use ksni::{Icon, ToolTip};
|
|||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
use crate::bluetooth::aacp::ControlCommandIdentifiers;
|
use crate::bluetooth::aacp::ControlCommandIdentifiers;
|
||||||
|
use crate::ui::messages::UIMessage;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct MyTray {
|
pub(crate) struct MyTray {
|
||||||
@@ -18,8 +19,8 @@ pub(crate) struct MyTray {
|
|||||||
pub(crate) connected: bool,
|
pub(crate) connected: bool,
|
||||||
pub(crate) listening_mode: Option<u8>,
|
pub(crate) listening_mode: Option<u8>,
|
||||||
pub(crate) allow_off_option: Option<u8>,
|
pub(crate) allow_off_option: Option<u8>,
|
||||||
pub(crate) command_tx: Option<tokio::sync::mpsc::UnboundedSender<(ControlCommandIdentifiers, Vec<u8>)>>,
|
pub(crate) command_tx: Option<UnboundedSender<(ControlCommandIdentifiers, Vec<u8>)>>,
|
||||||
pub(crate) ui_tx: Option<UnboundedSender<()>>,
|
pub(crate) ui_tx: Option<UnboundedSender<UIMessage>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ksni::Tray for MyTray {
|
impl ksni::Tray for MyTray {
|
||||||
@@ -113,7 +114,7 @@ impl ksni::Tray for MyTray {
|
|||||||
icon_name: "window-new".into(),
|
icon_name: "window-new".into(),
|
||||||
activate: Box::new(|this: &mut Self| {
|
activate: Box::new(|this: &mut Self| {
|
||||||
if let Some(tx) = &this.ui_tx {
|
if let Some(tx) = &this.ui_tx {
|
||||||
let _ = tx.send(());
|
let _ = tx.send(UIMessage::OpenWindow);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use iced::widget::button::Style;
|
use iced::widget::button::Style;
|
||||||
use iced::widget::{button, column, container, pane_grid, text, Space, combo_box, row, text_input};
|
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 std::sync::Arc;
|
||||||
use iced::border::Radius;
|
use iced::border::Radius;
|
||||||
use iced::overlay::menu;
|
use iced::overlay::menu;
|
||||||
use log::{debug, error};
|
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::bluetooth::aacp::{DeviceData, DeviceInformation, DeviceType};
|
||||||
|
use crate::ui::messages::UIMessage;
|
||||||
use crate::utils::{get_devices_path, get_app_settings_path, MyTheme};
|
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)
|
daemon(App::title, App::update, App::view)
|
||||||
.subscription(App::subscription)
|
.subscription(App::subscription)
|
||||||
.theme(App::theme)
|
.theme(App::theme)
|
||||||
@@ -21,9 +24,22 @@ pub struct App {
|
|||||||
window: Option<window::Id>,
|
window: Option<window::Id>,
|
||||||
panes: pane_grid::State<Pane>,
|
panes: pane_grid::State<Pane>,
|
||||||
selected_tab: Tab,
|
selected_tab: Tab,
|
||||||
ui_rx: Arc<Mutex<UnboundedReceiver<()>>>,
|
|
||||||
theme_state: combo_box::State<MyTheme>,
|
theme_state: combo_box::State<MyTheme>,
|
||||||
selected_theme: 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)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -32,9 +48,9 @@ pub enum Message {
|
|||||||
WindowClosed(window::Id),
|
WindowClosed(window::Id),
|
||||||
Resized(pane_grid::ResizeEvent),
|
Resized(pane_grid::ResizeEvent),
|
||||||
SelectTab(Tab),
|
SelectTab(Tab),
|
||||||
OpenMainWindow,
|
|
||||||
ThemeSelected(MyTheme),
|
ThemeSelected(MyTheme),
|
||||||
CopyToClipboard(String),
|
CopyToClipboard(String),
|
||||||
|
UIMessage(UIMessage),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
@@ -51,21 +67,25 @@ pub enum Pane {
|
|||||||
|
|
||||||
|
|
||||||
impl App {
|
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 (mut panes, first_pane) = pane_grid::State::new(Pane::Sidebar);
|
||||||
let split = panes.split(pane_grid::Axis::Vertical, first_pane, Pane::Content);
|
let split = panes.split(pane_grid::Axis::Vertical, first_pane, Pane::Content);
|
||||||
panes.resize(split.unwrap().1, 0.2);
|
panes.resize(split.unwrap().1, 0.2);
|
||||||
|
|
||||||
let ui_rx = Arc::new(Mutex::new(ui_rx));
|
let ui_rx = Arc::new(Mutex::new(ui_rx));
|
||||||
|
|
||||||
let wait_task = Task::perform(
|
let wait_task = Task::perform(
|
||||||
wait_for_message(Arc::clone(&ui_rx)),
|
wait_for_message(Arc::clone(&ui_rx)),
|
||||||
|_| Message::OpenMainWindow,
|
|msg| msg,
|
||||||
);
|
);
|
||||||
|
|
||||||
let (window, open_task) = if start_minimized {
|
let (window, open_task) = if start_minimized {
|
||||||
(None, Task::none())
|
(None, Task::none())
|
||||||
} else {
|
} 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))
|
(Some(id), open.map(Message::WindowOpened))
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,12 +97,13 @@ impl App {
|
|||||||
.and_then(|t| serde_json::from_value(t).ok())
|
.and_then(|t| serde_json::from_value(t).ok())
|
||||||
.unwrap_or(MyTheme::Dark);
|
.unwrap_or(MyTheme::Dark);
|
||||||
|
|
||||||
|
let bluetooth_state = BluetoothState::new();
|
||||||
|
|
||||||
(
|
(
|
||||||
Self {
|
Self {
|
||||||
window,
|
window,
|
||||||
panes,
|
panes,
|
||||||
selected_tab: Tab::Device("none".to_string()),
|
selected_tab: Tab::Device("none".to_string()),
|
||||||
ui_rx,
|
|
||||||
theme_state: combo_box::State::new(vec![
|
theme_state: combo_box::State::new(vec![
|
||||||
MyTheme::Light,
|
MyTheme::Light,
|
||||||
MyTheme::Dark,
|
MyTheme::Dark,
|
||||||
@@ -108,8 +129,10 @@ impl App {
|
|||||||
MyTheme::Ferra,
|
MyTheme::Ferra,
|
||||||
]),
|
]),
|
||||||
selected_theme,
|
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) {
|
if self.window == Some(id) {
|
||||||
self.window = None;
|
self.window = None;
|
||||||
}
|
}
|
||||||
let wait_task = Task::perform(
|
Task::none()
|
||||||
wait_for_message(Arc::clone(&self.ui_rx)),
|
|
||||||
|_| Message::OpenMainWindow,
|
|
||||||
);
|
|
||||||
wait_task
|
|
||||||
}
|
}
|
||||||
Message::Resized(event) => {
|
Message::Resized(event) => {
|
||||||
self.panes.resize(event.split, event.ratio);
|
self.panes.resize(event.split, event.ratio);
|
||||||
@@ -141,17 +160,6 @@ impl App {
|
|||||||
self.selected_tab = tab;
|
self.selected_tab = tab;
|
||||||
Task::none()
|
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) => {
|
Message::ThemeSelected(theme) => {
|
||||||
self.selected_theme = theme;
|
self.selected_theme = theme;
|
||||||
let app_settings_path = get_app_settings_path();
|
let app_settings_path = get_app_settings_path();
|
||||||
@@ -163,6 +171,86 @@ impl App {
|
|||||||
Message::CopyToClipboard(data) => {
|
Message::CopyToClipboard(data) => {
|
||||||
iced::clipboard::write(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| {
|
let pane_grid = pane_grid::PaneGrid::new(&self.panes, |_pane_id, pane, _is_maximized| {
|
||||||
match pane {
|
match pane {
|
||||||
Pane::Sidebar => {
|
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 label = label.to_string();
|
||||||
let description = description.map(|d| d.to_string());
|
|
||||||
let is_selected = self.selected_tab == tab;
|
let is_selected = self.selected_tab == tab;
|
||||||
let mut col = column![text(label).size(18)];
|
let col = column![
|
||||||
if let Some(desc) = description {
|
text(label).size(16),
|
||||||
col = col.push(text(desc).size(12));
|
text(
|
||||||
}
|
if connected {
|
||||||
|
format!("Connected - {}", description)
|
||||||
|
} else {
|
||||||
|
format!("{}", description)
|
||||||
|
}
|
||||||
|
).size(12)
|
||||||
|
];
|
||||||
let content = container(col)
|
let content = container(col)
|
||||||
.padding(10);
|
.padding(8);
|
||||||
let style = move |theme: &Theme, _status| {
|
let style = move |theme: &Theme, _status| {
|
||||||
if is_selected {
|
if is_selected {
|
||||||
let mut style = Style::default()
|
let mut style = Style::default()
|
||||||
.with_background(theme.palette().primary);
|
.with_background(theme.palette().primary);
|
||||||
let mut border = Border::default();
|
let mut border = Border::default();
|
||||||
border.color = theme.palette().text;
|
border.color = theme.palette().text;
|
||||||
style.border = border.rounded(20);
|
style.border = border.rounded(12);
|
||||||
style
|
style
|
||||||
} else {
|
} else {
|
||||||
let mut style = Style::default()
|
let mut style = Style::default()
|
||||||
.with_background(theme.palette().primary.scale_alpha(0.1));
|
.with_background(theme.palette().primary.scale_alpha(0.1));
|
||||||
let mut border = Border::default();
|
let mut border = Border::default();
|
||||||
border.color = theme.palette().primary.scale_alpha(0.1);
|
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.text_color = theme.palette().text;
|
||||||
style
|
style
|
||||||
}
|
}
|
||||||
@@ -214,6 +307,38 @@ impl App {
|
|||||||
.into()
|
.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 = column!().spacing(4);
|
||||||
let mut devices_vec: Vec<(String, DeviceData)> = devices_list.clone().into_iter().collect();
|
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));
|
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(
|
let tab_button = create_tab_button(
|
||||||
Tab::Device(mac.clone()),
|
Tab::Device(mac.clone()),
|
||||||
&name,
|
&name,
|
||||||
Some(&mac)
|
&mac,
|
||||||
|
self.bluetooth_state.connected_devices.contains(&mac)
|
||||||
);
|
);
|
||||||
devices = devices.push(tab_button);
|
devices = devices.push(tab_button);
|
||||||
}
|
}
|
||||||
|
|
||||||
let settings = create_tab_button(Tab::Settings, "Settings", None);
|
let settings = create_settings_button();
|
||||||
|
|
||||||
let content = column![
|
let content = column![
|
||||||
devices,
|
devices,
|
||||||
@@ -522,8 +648,15 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_message(rx: Arc<Mutex<UnboundedReceiver<()>>>) {
|
async fn wait_for_message(
|
||||||
debug!("Waiting for message to open main window...");
|
ui_rx: Arc<Mutex<UnboundedReceiver<UIMessage>>>,
|
||||||
let mut guard = rx.lock().await;
|
) -> Message {
|
||||||
let _ = guard.recv().await;
|
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