mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-01 07:39:11 +00:00
linux-rust: add battery to window and add option for text in tray
This commit is contained in:
BIN
linux-rust/assets/font/sf_pro.otf
Normal file
BIN
linux-rust/assets/font/sf_pro.otf
Normal file
Binary file not shown.
@@ -10,7 +10,6 @@ command: librepods
|
||||
finish-args:
|
||||
- --socket=wayland
|
||||
- --socket=fallback-x11
|
||||
- --share=ipc
|
||||
- --socket=pulseaudio
|
||||
- --system-talk-name=org.bluez
|
||||
- --allow=bluetooth
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::fmt::Display;
|
||||
use iced::widget::{combo_box, ComboBox};
|
||||
use iced::widget::combo_box;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::bluetooth::aacp::BatteryInfo;
|
||||
use crate::devices::airpods::AirPodsInformation;
|
||||
use crate::devices::nothing::NothingInformation;
|
||||
|
||||
@@ -58,7 +59,8 @@ pub struct AirPodsState {
|
||||
pub noise_control_state: combo_box::State<AirPodsNoiseControlMode>,
|
||||
pub conversation_awareness_enabled: bool,
|
||||
pub personalized_volume_enabled: bool,
|
||||
pub allow_off_mode: bool
|
||||
pub allow_off_mode: bool,
|
||||
pub battery: Vec<BatteryInfo>
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -28,19 +28,33 @@ use crate::utils::get_devices_path;
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
#[arg(long)]
|
||||
#[arg(long, short='d', help="Enable debug logging")]
|
||||
debug: bool,
|
||||
#[arg(long)]
|
||||
#[arg(long, help="Disable system tray, useful if your environment doesn't support AppIndicator or StatusNotifier")]
|
||||
no_tray: bool,
|
||||
#[arg(long)]
|
||||
#[arg(long, help="Start the application minimized to tray")]
|
||||
start_minimized: bool,
|
||||
#[arg(long, help="Enable Bluetooth LE debug logging. Only use when absolutely necessary; this produces a lot of logs.")]
|
||||
le_debug: bool,
|
||||
#[arg(long, short='v', help="Show application version and exit")]
|
||||
version: bool
|
||||
}
|
||||
|
||||
fn main() -> iced::Result {
|
||||
let args = Args::parse();
|
||||
|
||||
if args.version {
|
||||
println!("You are running LibrePods version {}", env!("CARGO_PKG_VERSION"));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let log_level = if args.debug { "debug" } else { "info" };
|
||||
let wayland_display = env::var("WAYLAND_DISPLAY").is_ok();
|
||||
if env::var("RUST_LOG").is_err() {
|
||||
unsafe { env::set_var("RUST_LOG", log_level.to_owned() + ",winit=warn,tracing=warn,,iced_wgpu=warn,wgpu_hal=warn,wgpu_core=warn,librepods_rust::bluetooth::le=warn,cosmic_text=warn,naga=warn,iced_winit=warn") };
|
||||
if wayland_display {
|
||||
unsafe { env::set_var("WGPU_BACKEND", "gl") };
|
||||
}
|
||||
unsafe { env::set_var("RUST_LOG", log_level.to_owned() + &format!(",winit=warn,tracing=warn,iced_wgpu=warn,wgpu_hal=warn,wgpu_core=warn,cosmic_text=warn,naga=warn,iced_winit=warn,librepods_rust::bluetooth::le={}", if args.le_debug { "debug" } else { "warn" })) };
|
||||
}
|
||||
env_logger::init();
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ pub fn airpods_view<'a>(
|
||||
) -> iced::widget::Container<'a, Message> {
|
||||
let mac = mac.to_string();
|
||||
// order: name, noise control, press and hold config, call controls (not sure if why it might be needed, adding it just in case), audio (personalized volume, conversational awareness, adaptive audio slider), connection settings, microphone, head gestures (not adding this), off listening mode, device information
|
||||
|
||||
let aacp_manager_for_rename = aacp_manager.clone();
|
||||
let rename_input = container(
|
||||
row![
|
||||
@@ -221,14 +222,13 @@ pub fn airpods_view<'a>(
|
||||
column![
|
||||
text("Personalized Volume").size(16),
|
||||
text("Adjusts the volume in response to your environment.").size(12).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text.scale_alpha(0.7));
|
||||
style
|
||||
}
|
||||
)
|
||||
],
|
||||
Space::with_width(Length::Fill),
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text.scale_alpha(0.7));
|
||||
style
|
||||
}
|
||||
).width(Length::Fill),
|
||||
].width(Length::Fill),
|
||||
toggler(state.personalized_volume_enabled)
|
||||
.on_toggle(
|
||||
{
|
||||
@@ -255,6 +255,7 @@ pub fn airpods_view<'a>(
|
||||
.size(20)
|
||||
]
|
||||
.align_y(Center)
|
||||
.spacing(8)
|
||||
},
|
||||
Rule::horizontal(8).style(
|
||||
|theme: &Theme| {
|
||||
@@ -272,14 +273,13 @@ pub fn airpods_view<'a>(
|
||||
column![
|
||||
text("Conversation Awareness").size(16),
|
||||
text("Lowers the volume of your audio when it detects that you are speaking.").size(12).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text.scale_alpha(0.7));
|
||||
style
|
||||
}
|
||||
)
|
||||
],
|
||||
Space::with_width(Length::Fill),
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text.scale_alpha(0.7));
|
||||
style
|
||||
}
|
||||
).width(Length::Fill),
|
||||
].width(Length::Fill),
|
||||
toggler(state.conversation_awareness_enabled)
|
||||
.on_toggle(move |is_enabled| {
|
||||
let aacp_manager = aacp_manager_conv_detect.clone();
|
||||
@@ -299,6 +299,7 @@ pub fn airpods_view<'a>(
|
||||
.size(20)
|
||||
]
|
||||
.align_y(Center)
|
||||
.spacing(8)
|
||||
}
|
||||
]
|
||||
.spacing(4)
|
||||
@@ -328,15 +329,14 @@ pub fn airpods_view<'a>(
|
||||
container(row![
|
||||
column![
|
||||
text("Off Listening Mode").size(16),
|
||||
text("When this is on, AIrPods listening modes will include an Off option. Loud sound levels are not reduced when listening mode is set to Off.").size(12).style(
|
||||
text("When this is on, AirPods listening modes will include an Off option. Loud sound levels are not reduced when listening mode is set to Off.").size(12).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text.scale_alpha(0.7));
|
||||
style
|
||||
}
|
||||
)
|
||||
],
|
||||
Space::with_width(Length::Fill),
|
||||
).width(Length::Fill)
|
||||
].width(Length::Fill),
|
||||
toggler(state.allow_off_mode)
|
||||
.on_toggle(move |is_enabled| {
|
||||
let aacp_manager = aacp_manager_olm.clone();
|
||||
@@ -356,6 +356,7 @@ pub fn airpods_view<'a>(
|
||||
.size(20)
|
||||
]
|
||||
.align_y(Center)
|
||||
.spacing(8)
|
||||
)
|
||||
.padding(Padding{
|
||||
top: 5.0,
|
||||
|
||||
@@ -6,6 +6,7 @@ use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::bluetooth::aacp::ControlCommandIdentifiers;
|
||||
use crate::ui::messages::BluetoothUIMessage;
|
||||
use crate::utils::get_app_settings_path;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MyTray {
|
||||
@@ -57,7 +58,15 @@ impl ksni::Tray for MyTray {
|
||||
};
|
||||
let any_bud_charging = matches!(self.battery_l_status, Some(crate::bluetooth::aacp::BatteryStatus::Charging))
|
||||
|| matches!(self.battery_r_status, Some(crate::bluetooth::aacp::BatteryStatus::Charging));
|
||||
let icon = generate_icon(&text, false, any_bud_charging);
|
||||
let app_settings_path = get_app_settings_path();
|
||||
let settings = std::fs::read_to_string(&app_settings_path)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
|
||||
let text_mode = settings.clone()
|
||||
.and_then(|v| v.get("tray_text_mode").cloned())
|
||||
.and_then(|ttm| serde_json::from_value(ttm).ok())
|
||||
.unwrap_or(false);
|
||||
let icon = generate_icon(&text, text_mode, any_bud_charging);
|
||||
vec![icon]
|
||||
}
|
||||
fn tool_tip(&self) -> ToolTip {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use iced::widget::button::Style;
|
||||
use iced::widget::{button, column, container, pane_grid, text, Space, combo_box, row, text_input, scrollable, vertical_rule, rule};
|
||||
use iced::{daemon, window, Background, Border, Center, Color, Element, Length, Size, Subscription, Task, Theme};
|
||||
use iced::widget::{button, column, container, pane_grid, text, Space, combo_box, row, text_input, scrollable, vertical_rule, rule, toggler};
|
||||
use iced::{daemon, window, Background, Border, Center, Color, Element, Font, Length, Padding, Size, Subscription, Task, Theme};
|
||||
use std::sync::Arc;
|
||||
use bluer::{Address, Session};
|
||||
use iced::border::Radius;
|
||||
@@ -10,7 +10,7 @@ use iced::widget::rule::FillMode;
|
||||
use log::{debug, error};
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use crate::bluetooth::aacp::{AACPEvent, ControlCommandIdentifiers};
|
||||
use crate::bluetooth::aacp::{AACPEvent, ControlCommandIdentifiers, BatteryComponent, BatteryStatus};
|
||||
use crate::bluetooth::managers::DeviceManagers;
|
||||
use crate::devices::enums::{AirPodsNoiseControlMode, AirPodsState, DeviceData, DeviceState, DeviceType, NothingAncMode, NothingState};
|
||||
use crate::ui::messages::BluetoothUIMessage;
|
||||
@@ -26,6 +26,8 @@ pub fn start_ui(
|
||||
daemon(App::title, App::update, App::view)
|
||||
.subscription(App::subscription)
|
||||
.theme(App::theme)
|
||||
.font(include_bytes!("../../assets/font/sf_pro.otf").as_slice())
|
||||
.default_font(Font::with_name("SF Pro Text"))
|
||||
.run_with(move || App::new(ui_rx, start_minimized, device_managers))
|
||||
}
|
||||
|
||||
@@ -43,6 +45,7 @@ pub struct App {
|
||||
pending_add_device: Option<(String, Address)>,
|
||||
device_type_state: combo_box::State<DeviceType>,
|
||||
selected_device_type: Option<DeviceType>,
|
||||
tray_text_mode: bool
|
||||
}
|
||||
|
||||
pub struct BluetoothState {
|
||||
@@ -73,6 +76,7 @@ pub enum Message {
|
||||
ConfirmAddDevice,
|
||||
CancelAddDevice,
|
||||
StateChanged(String, DeviceState),
|
||||
TrayTextModeChanged(bool) // yes, I know I should add all settings to a struct, but I'm lazy
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
@@ -116,12 +120,17 @@ impl App {
|
||||
};
|
||||
|
||||
let app_settings_path = get_app_settings_path();
|
||||
let selected_theme = std::fs::read_to_string(&app_settings_path)
|
||||
let settings = std::fs::read_to_string(&app_settings_path)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
||||
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
|
||||
let selected_theme = settings.clone()
|
||||
.and_then(|v| v.get("theme").cloned())
|
||||
.and_then(|t| serde_json::from_value(t).ok())
|
||||
.unwrap_or(MyTheme::Dark);
|
||||
let tray_text_mode = settings.clone()
|
||||
.and_then(|v| v.get("tray_text_mode").cloned())
|
||||
.and_then(|ttm| serde_json::from_value(ttm).ok())
|
||||
.unwrap_or(false);
|
||||
|
||||
let bluetooth_state = BluetoothState::new();
|
||||
|
||||
@@ -132,6 +141,7 @@ impl App {
|
||||
// ("28:2D:7F:C2:05:5B".to_string(), dummy_device_state),
|
||||
// ]);
|
||||
|
||||
|
||||
let device_states = HashMap::new();
|
||||
(
|
||||
Self {
|
||||
@@ -172,7 +182,8 @@ impl App {
|
||||
DeviceType::Nothing
|
||||
]),
|
||||
selected_device_type: None,
|
||||
device_managers
|
||||
device_managers,
|
||||
tray_text_mode
|
||||
},
|
||||
Task::batch(vec![open_task, wait_task])
|
||||
)
|
||||
@@ -205,7 +216,7 @@ impl App {
|
||||
Message::ThemeSelected(theme) => {
|
||||
self.selected_theme = theme;
|
||||
let app_settings_path = get_app_settings_path();
|
||||
let settings = serde_json::json!({"theme": self.selected_theme});
|
||||
let settings = serde_json::json!({"theme": self.selected_theme, "tray_text_mode": self.tray_text_mode});
|
||||
debug!("Writing settings to {}: {}", app_settings_path.to_str().unwrap() , settings);
|
||||
std::fs::write(app_settings_path, settings.to_string()).ok();
|
||||
Task::none()
|
||||
@@ -301,6 +312,7 @@ impl App {
|
||||
};
|
||||
self.device_states.insert(mac.clone(), DeviceState::AirPods(AirPodsState {
|
||||
device_name,
|
||||
battery: state.battery_info.clone(),
|
||||
noise_control_mode: state.control_command_status_list.iter().find_map(|status| {
|
||||
if status.identifier == ControlCommandIdentifiers::ListeningMode {
|
||||
status.value.get(0).map(|b| AirPodsNoiseControlMode::from_byte(b))
|
||||
@@ -444,6 +456,12 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
AACPEvent::BatteryInfo(battery_info) => {
|
||||
if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) {
|
||||
state.battery = battery_info;
|
||||
debug!("Updated battery info for {}: {:?}", mac, state.battery);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Task::batch(vec![
|
||||
@@ -554,6 +572,14 @@ impl App {
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::TrayTextModeChanged(is_enabled) => {
|
||||
self.tray_text_mode = is_enabled;
|
||||
let app_settings_path = get_app_settings_path();
|
||||
let settings = serde_json::json!({"theme": self.selected_theme, "tray_text_mode": self.tray_text_mode});
|
||||
debug!("Writing settings to {}: {}", app_settings_path.to_str().unwrap() , settings);
|
||||
std::fs::write(app_settings_path, settings.to_string()).ok();
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,18 +595,44 @@ 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: &str, connected: bool| -> Element<'_, Message> {
|
||||
let label = label.to_string();
|
||||
let create_tab_button = |tab: Tab, label: &str, mac_addr: &str, connected: bool| -> Element<'_, Message> {
|
||||
let label = label.to_string() + if connected { " " } else { "" };
|
||||
let is_selected = self.selected_tab == tab;
|
||||
let col = column![
|
||||
text(label).size(16),
|
||||
text(
|
||||
text({
|
||||
if connected {
|
||||
format!("Connected - {}", description)
|
||||
let mac = match tab {
|
||||
Tab::Device(ref mac) => mac.as_str(),
|
||||
_ => "",
|
||||
};
|
||||
|
||||
match self.device_states.get(mac) {
|
||||
Some(DeviceState::AirPods(state)) => {
|
||||
let b = &state.battery;
|
||||
let left = b.iter().find(|x| x.component == BatteryComponent::Left)
|
||||
.map(|x| x.level).unwrap_or_default();
|
||||
let right = b.iter().find(|x| x.component == BatteryComponent::Right)
|
||||
.map(|x| x.level).unwrap_or_default();
|
||||
let case = b.iter().find(|x| x.component == BatteryComponent::Case)
|
||||
.map(|x| x.level).unwrap_or_default();
|
||||
let left_charging = b.iter().find(|x| x.component == BatteryComponent::Left)
|
||||
.map(|x| x.status == BatteryStatus::Charging).unwrap_or(false);
|
||||
let right_charging = b.iter().find(|x| x.component == BatteryComponent::Right)
|
||||
.map(|x| x.status == BatteryStatus::Charging).unwrap_or(false);
|
||||
let case_charging = b.iter().find(|x| x.component == BatteryComponent::Case)
|
||||
.map(|x| x.status == BatteryStatus::Charging).unwrap_or(false);
|
||||
format!(
|
||||
"\u{1018E5} {}%{} \u{1018E8} {}%{} \u{100E6C} {}%{}",
|
||||
left, if left_charging {"\u{1002E6}"} else {""}, right, if right_charging {"\u{1002E6}"} else {""}, case, if case_charging {"\u{1002E6}"} else {""}
|
||||
)
|
||||
}
|
||||
_ => "Connected".to_string(),
|
||||
}
|
||||
} else {
|
||||
format!("{}", description)
|
||||
mac_addr.to_string()
|
||||
}
|
||||
).size(12)
|
||||
}).size(12)
|
||||
];
|
||||
let content = container(col)
|
||||
.padding(8);
|
||||
@@ -791,12 +843,65 @@ impl App {
|
||||
}
|
||||
}
|
||||
Tab::Settings => {
|
||||
container(
|
||||
column![
|
||||
text("Settings").size(40),
|
||||
Space::with_height(Length::from(20)),
|
||||
let tray_text_mode_toggle = container(
|
||||
row![
|
||||
column![
|
||||
text("Use text in tray").size(16),
|
||||
text("Use text for battery status in tray instead of a progress bar.").size(12).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text.scale_alpha(0.7));
|
||||
style
|
||||
}
|
||||
).width(Length::Fill)
|
||||
].width(Length::Fill),
|
||||
toggler(self.tray_text_mode)
|
||||
.on_toggle(move |is_enabled| {
|
||||
Message::TrayTextModeChanged(is_enabled)
|
||||
})
|
||||
.spacing(0)
|
||||
.size(20)
|
||||
]
|
||||
.align_y(Center)
|
||||
.spacing(12)
|
||||
)
|
||||
.padding(Padding{
|
||||
top: 5.0,
|
||||
bottom: 5.0,
|
||||
left: 18.0,
|
||||
right: 18.0,
|
||||
})
|
||||
.style(
|
||||
|theme: &Theme| {
|
||||
let mut style = container::Style::default();
|
||||
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
||||
let mut border = Border::default();
|
||||
border.color = theme.palette().primary.scale_alpha(0.5);
|
||||
style.border = border.rounded(16);
|
||||
style
|
||||
}
|
||||
)
|
||||
.align_y(Center);
|
||||
|
||||
let appearance_settings_col = column![
|
||||
container(
|
||||
text("Appearance").size(20).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().primary);
|
||||
style
|
||||
}
|
||||
)
|
||||
)
|
||||
.padding(Padding{
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
left: 18.0,
|
||||
right: 18.0,
|
||||
}),
|
||||
container(
|
||||
row![
|
||||
text("Theme:")
|
||||
text("Theme")
|
||||
.size(16),
|
||||
Space::with_width(Length::Fill),
|
||||
combo_box(
|
||||
@@ -808,23 +913,23 @@ impl App {
|
||||
.input_style(
|
||||
|theme: &Theme, _status| {
|
||||
text_input::Style {
|
||||
background: Background::Color(Color::TRANSPARENT),
|
||||
background: Background::Color(theme.palette().primary.scale_alpha(0.2)),
|
||||
border: Border {
|
||||
width: 1.0,
|
||||
color: theme.palette().text,
|
||||
radius: Radius::from(8.0),
|
||||
color: theme.palette().text.scale_alpha(0.3),
|
||||
radius: Radius::from(4.0)
|
||||
},
|
||||
icon: Default::default(),
|
||||
placeholder: theme.palette().text.scale_alpha(0.5),
|
||||
placeholder: theme.palette().text,
|
||||
value: theme.palette().text,
|
||||
selection: theme.palette().primary
|
||||
selection: Default::default(),
|
||||
}
|
||||
}
|
||||
)
|
||||
.menu_style(
|
||||
|theme: &Theme| {
|
||||
menu::Style {
|
||||
background: Background::Color(Color::TRANSPARENT),
|
||||
background: Background::Color(theme.palette().background),
|
||||
border: Border {
|
||||
width: 1.0,
|
||||
color: theme.palette().text,
|
||||
@@ -836,9 +941,40 @@ impl App {
|
||||
}
|
||||
}
|
||||
)
|
||||
.width(Length::from(350))
|
||||
.padding(Padding{
|
||||
top: 5.0,
|
||||
bottom: 5.0,
|
||||
left: 10.0,
|
||||
right: 10.0,
|
||||
})
|
||||
.width(Length::from(200))
|
||||
]
|
||||
.align_y(Center)
|
||||
)
|
||||
.padding(Padding{
|
||||
top: 5.0,
|
||||
bottom: 5.0,
|
||||
left: 18.0,
|
||||
right: 18.0,
|
||||
})
|
||||
.style(
|
||||
|theme: &Theme| {
|
||||
let mut style = container::Style::default();
|
||||
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
||||
let mut border = Border::default();
|
||||
border.color = theme.palette().primary.scale_alpha(0.5);
|
||||
style.border = border.rounded(16);
|
||||
style
|
||||
}
|
||||
)
|
||||
]
|
||||
.spacing(12);
|
||||
|
||||
container(
|
||||
column![
|
||||
appearance_settings_col,
|
||||
Space::with_height(Length::from(20)),
|
||||
tray_text_mode_toggle
|
||||
]
|
||||
)
|
||||
.padding(20)
|
||||
@@ -862,7 +998,6 @@ impl App {
|
||||
].into(),
|
||||
Space::with_width(Length::Fill).into(),
|
||||
];
|
||||
// Only show "Add" button if this device is not the pending one
|
||||
if !matches!(&self.pending_add_device, Some((_, addr)) if addr == &device.1) {
|
||||
row_elements.push(
|
||||
button(
|
||||
|
||||
Reference in New Issue
Block a user