mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-04-13 14:56:39 +00:00
linux-rust(feat): add stem press track control and headless mode support (#469)
* feat: add stem press track control and headless mode support - Parse STEM_PRESS packets and emit AACPEvent::StemPress with press type and bud side - Enable double/triple tap detection on init via StemConfig control command (0x06) - Double press → next track, triple press → previous track via MPRIS D-Bus - Add next_track() and previous_track() to MediaController - Add --no-tray flag for headless operation without a GUI - Replace unwrap() on ui_tx.send() calls with graceful warn! logging (vibecoded) * Update main.rs * feat: make stem press track control optional with GUI toggle Add a --no-stem-control CLI flag and a toggle in the Settings tab for environments that handle AirPods AVRCP commands natively (e.g. via BlueZ/PipeWire). The feature remains enabled by default. - Load stem_control from app settings JSON on startup; --no-stem-control overrides it to false regardless of the saved value - Share an Arc<AtomicBool> between the async backend and the GUI thread; AirPodsDevice holds the Arc directly so the event loop reads the live value on every stem press — toggle takes effect immediately without reconnecting - Persist stem_control to settings JSON alongside theme and tray_text_mode - Add a "Controls" section to the Settings tab with a toggler labelled "Stem press track control", with a subtitle explaining the AVRCP conflict scenario - Fix StemConfig bitmask comment to clarify it uses a separate numbering scheme from the StemPressType event enum values (0x05–0x08)
This commit is contained in:
@@ -25,6 +25,7 @@ use iced::{
|
||||
};
|
||||
use log::{debug, error};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
@@ -33,13 +34,14 @@ pub fn start_ui(
|
||||
ui_rx: UnboundedReceiver<BluetoothUIMessage>,
|
||||
start_minimized: bool,
|
||||
device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>,
|
||||
stem_control: Arc<AtomicBool>,
|
||||
) -> iced::Result {
|
||||
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))
|
||||
.run_with(move || App::new(ui_rx, start_minimized, device_managers, stem_control))
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
@@ -57,6 +59,7 @@ pub struct App {
|
||||
device_type_state: combo_box::State<DeviceType>,
|
||||
selected_device_type: Option<DeviceType>,
|
||||
tray_text_mode: bool,
|
||||
stem_control: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
pub struct BluetoothState {
|
||||
@@ -88,6 +91,7 @@ pub enum Message {
|
||||
CancelAddDevice,
|
||||
StateChanged(String, DeviceState),
|
||||
TrayTextModeChanged(bool), // yes, I know I should add all settings to a struct, but I'm lazy
|
||||
StemControlChanged(bool),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
@@ -108,6 +112,7 @@ impl App {
|
||||
ui_rx: UnboundedReceiver<BluetoothUIMessage>,
|
||||
start_minimized: bool,
|
||||
device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>,
|
||||
stem_control: Arc<AtomicBool>,
|
||||
) -> (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);
|
||||
@@ -191,6 +196,7 @@ impl App {
|
||||
selected_device_type: None,
|
||||
device_managers,
|
||||
tray_text_mode,
|
||||
stem_control,
|
||||
},
|
||||
Task::batch(vec![open_task, wait_task]),
|
||||
)
|
||||
@@ -223,7 +229,11 @@ 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, "tray_text_mode": self.tray_text_mode});
|
||||
let settings = serde_json::json!({
|
||||
"theme": self.selected_theme,
|
||||
"tray_text_mode": self.tray_text_mode,
|
||||
"stem_control": self.stem_control.load(Ordering::Relaxed),
|
||||
});
|
||||
debug!(
|
||||
"Writing settings to {}: {}",
|
||||
app_settings_path.to_str().unwrap(),
|
||||
@@ -588,7 +598,27 @@ impl App {
|
||||
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});
|
||||
let settings = serde_json::json!({
|
||||
"theme": self.selected_theme,
|
||||
"tray_text_mode": self.tray_text_mode,
|
||||
"stem_control": self.stem_control.load(Ordering::Relaxed),
|
||||
});
|
||||
debug!(
|
||||
"Writing settings to {}: {}",
|
||||
app_settings_path.to_str().unwrap(),
|
||||
settings
|
||||
);
|
||||
std::fs::write(app_settings_path, settings.to_string()).ok();
|
||||
Task::none()
|
||||
}
|
||||
Message::StemControlChanged(is_enabled) => {
|
||||
self.stem_control.store(is_enabled, Ordering::Relaxed);
|
||||
let app_settings_path = get_app_settings_path();
|
||||
let settings = serde_json::json!({
|
||||
"theme": self.selected_theme,
|
||||
"tray_text_mode": self.tray_text_mode,
|
||||
"stem_control": self.stem_control.load(Ordering::Relaxed),
|
||||
});
|
||||
debug!(
|
||||
"Writing settings to {}: {}",
|
||||
app_settings_path.to_str().unwrap(),
|
||||
@@ -994,11 +1024,74 @@ impl App {
|
||||
]
|
||||
.spacing(12);
|
||||
|
||||
let stem_control_value = self.stem_control.load(Ordering::Relaxed);
|
||||
let stem_control_toggle = container(
|
||||
row![
|
||||
column![
|
||||
text("Stem press track control").size(16),
|
||||
text("Double press = next track, triple press = previous track. Disable if your environment handles AirPods AVRCP commands natively.").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(stem_control_value)
|
||||
.on_toggle(move |is_enabled| {
|
||||
Message::StemControlChanged(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 controls_settings_col = column![
|
||||
container(
|
||||
text("Controls").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,
|
||||
}),
|
||||
stem_control_toggle
|
||||
]
|
||||
.spacing(12);
|
||||
|
||||
container(
|
||||
column![
|
||||
appearance_settings_col,
|
||||
Space::with_height(Length::from(20)),
|
||||
tray_text_mode_toggle
|
||||
tray_text_mode_toggle,
|
||||
Space::with_height(Length::from(20)),
|
||||
controls_settings_col,
|
||||
]
|
||||
)
|
||||
.padding(20)
|
||||
|
||||
Reference in New Issue
Block a user