Rockbox open source high quality audio player as a Music Player Daemon
mpris rockbox mpd libadwaita audio rust zig deno
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Check Bluetooth availability in background

Add a background task that polls the server (and re-checks on server
notifications) and updates BluetoothState::available. Use
std::sync::mpsc to avoid cross-runtime waker issues when bridging tokio
-> GPUI. Separate device fetching (now updates BluetoothState::devices)
and make observe_global registrations use .detach() instead of assigning
to an unused result.

+56 -31
+45 -17
gpui/src/ui/components/bluetooth_picker.rs
··· 1 1 use crate::controller::Controller; 2 - use crate::state::{BluetoothDevice, BluetoothState}; 2 + use crate::state::BluetoothState; 3 3 use crate::ui::components::icons::{Icon, Icons}; 4 4 use crate::ui::theme::Theme; 5 5 use gpui::prelude::FluentBuilder; ··· 156 156 } 157 157 } 158 158 159 - pub fn fetch_and_update_bluetooth_devices(cx: &mut App) { 159 + /// Checks whether the connected server supports Bluetooth (by calling get_devices). 160 + /// Re-checks automatically whenever the active server changes. 161 + /// Updates BluetoothState::available so the UI can show/hide the bluetooth button. 162 + pub fn check_and_set_bluetooth_available(cx: &mut App) { 160 163 let rt = cx.global::<Controller>().rt(); 161 - let (tx, mut rx) = tokio::sync::mpsc::channel::<Vec<BluetoothDevice>>(1); 164 + // std::sync::mpsc avoids cross-runtime waker issues — tokio side sends without await, 165 + // GPUI side polls with try_recv. 166 + let (tx, rx) = std::sync::mpsc::channel::<bool>(); 167 + 162 168 rt.spawn(async move { 163 - if let Ok(devices) = crate::client::fetch_bluetooth_devices().await { 164 - let _ = tx.send(devices).await; 169 + let available = crate::client::check_bluetooth_available().await; 170 + let _ = tx.send(available); 171 + 172 + let notify = crate::server::server_notify(); 173 + loop { 174 + notify.notified().await; 175 + tokio::time::sleep(std::time::Duration::from_millis(500)).await; 176 + let available = crate::client::check_bluetooth_available().await; 177 + if tx.send(available).is_err() { 178 + break; 179 + } 165 180 } 166 181 }); 182 + 167 183 cx.spawn(async move |cx| { 168 - if let Some(devices) = rx.recv().await { 169 - let _ = cx.update(|cx| { 170 - let mut state = cx.global::<BluetoothState>().clone(); 171 - state.devices = devices; 172 - cx.set_global(state); 173 - }); 184 + loop { 185 + while let Ok(available) = rx.try_recv() { 186 + if cx 187 + .update(|cx| { 188 + let mut state = cx.global::<BluetoothState>().clone(); 189 + state.available = available; 190 + cx.set_global(state); 191 + }) 192 + .is_err() 193 + { 194 + return; 195 + } 196 + } 197 + cx.background_executor() 198 + .timer(std::time::Duration::from_millis(200)) 199 + .await; 174 200 } 175 201 }) 176 202 .detach(); 177 203 } 178 204 179 - pub fn check_and_set_bluetooth_available(cx: &mut App) { 205 + pub fn fetch_and_update_bluetooth_devices(cx: &mut App) { 180 206 let rt = cx.global::<Controller>().rt(); 181 - let (tx, mut rx) = tokio::sync::mpsc::channel::<bool>(1); 207 + let (tx, mut rx) = tokio::sync::mpsc::channel::<Vec<crate::state::BluetoothDevice>>(1); 182 208 rt.spawn(async move { 183 - let available = crate::client::check_bluetooth_available().await; 184 - let _ = tx.send(available).await; 209 + if let Ok(devices) = crate::client::fetch_bluetooth_devices().await { 210 + let _ = tx.send(devices).await; 211 + } 185 212 }); 186 213 cx.spawn(async move |cx| { 187 - if let Some(available) = rx.recv().await { 214 + if let Some(devices) = rx.recv().await { 188 215 let _ = cx.update(|cx| { 189 216 let mut state = cx.global::<BluetoothState>().clone(); 190 - state.available = available; 217 + state.devices = devices; 191 218 cx.set_global(state); 192 219 }); 193 220 } 194 221 }) 195 222 .detach(); 196 223 } 224 +
+2 -3
gpui/src/ui/components/controlbar.rs
··· 7 7 use crate::ui::theme::Theme; 8 8 use gpui::prelude::FluentBuilder; 9 9 use gpui::{ 10 - div, px, App, Context, Div, InteractiveElement, IntoElement, ParentElement, Render, 10 + div, px, App, Context, InteractiveElement, IntoElement, ParentElement, Render, 11 11 StatefulInteractiveElement, Styled, Window, 12 12 }; 13 13 ··· 33 33 .map(|d| device_icon(d)) 34 34 .unwrap_or(Icons::Speaker); 35 35 let bluetooth_available = cx.global::<BluetoothState>().available; 36 - 37 36 div() 38 37 .w_full() 39 38 .flex_shrink_0() ··· 87 86 .items_center() 88 87 .justify_end() 89 88 .gap_x_1() 90 - .when(bluetooth_available, |this: Div| { 89 + .when(bluetooth_available, |this| { 91 90 this.child( 92 91 div() 93 92 .id("controlbar-bluetooth-btn")
+2 -2
gpui/src/ui/components/miniplayer.rs
··· 13 13 use crate::ui::theme::Theme; 14 14 use gpui::prelude::FluentBuilder; 15 15 use gpui::{ 16 - div, img, px, App, Context, Div, FontWeight, InteractiveElement, IntoElement, 16 + div, img, px, App, Context, FontWeight, InteractiveElement, IntoElement, 17 17 ObjectFit, ParentElement, Render, ScrollWheelEvent, StatefulInteractiveElement, Styled, 18 18 StyledImage, Window, 19 19 }; ··· 389 389 .items_center() 390 390 .justify_end() 391 391 .gap_x_2() 392 - .when(bluetooth_available, |this: Div| { 392 + .when(bluetooth_available, |this| { 393 393 this.child( 394 394 div() 395 395 .id("mini_bluetooth")
+1 -1
gpui/src/ui/components/pages/library.rs
··· 265 265 detail_scroll_handle: UniformListScrollHandle::new(), 266 266 miniplayer: { 267 267 cx.new(|cx| { 268 - let _ = cx.observe_global::<DevicesState>(|_, cx| cx.notify()); 268 + cx.observe_global::<DevicesState>(|_, cx| cx.notify()).detach(); 269 269 MiniPlayer 270 270 }) 271 271 },
+1 -1
gpui/src/ui/components/pages/queue.rs
··· 22 22 QueuePage { 23 23 scroll_handle: UniformListScrollHandle::new(), 24 24 miniplayer: cx.new(|cx| { 25 - let _ = cx.observe_global::<DevicesState>(|_, cx| cx.notify()); 25 + cx.observe_global::<DevicesState>(|_, cx| cx.notify()).detach(); 26 26 MiniPlayer 27 27 }), 28 28 last_scrolled_idx: None,
+5 -7
gpui/src/ui/rockbox.rs
··· 1 1 use crate::state::{BluetoothState, DevicesState}; 2 2 use crate::ui::animations::ease_in_out_expo; 3 - use crate::ui::components::bluetooth_picker::{ 4 - check_and_set_bluetooth_available, BluetoothPicker, 5 - }; 3 + use crate::ui::components::bluetooth_picker::{check_and_set_bluetooth_available, BluetoothPicker}; 6 4 use crate::ui::components::controlbar::ControlBar; 7 5 use crate::ui::components::device_picker::{fetch_and_update_devices, DevicePicker}; 8 6 use crate::ui::components::pages::{library::LibraryPage, player::PlayerPage, queue::QueuePage}; ··· 36 34 global_keybinds::register_keybinds(cx); 37 35 let titlebar = cx.new(|cx| Titlebar::new(cx)); 38 36 let controlbar = cx.new(|cx| { 39 - let _ = cx.observe_global::<DevicesState>(|_, cx| cx.notify()); 40 - let _ = cx.observe_global::<BluetoothState>(|_, cx| cx.notify()); 37 + cx.observe_global::<DevicesState>(|_, cx| cx.notify()).detach(); 38 + cx.observe_global::<BluetoothState>(|_, cx| cx.notify()).detach(); 41 39 ControlBar 42 40 }); 43 41 let player_page = cx.new(|cx| PlayerPage::new(cx, controlbar)); 44 42 let library_page = cx.new(|cx| LibraryPage::new(cx)); 45 43 let queue_page = cx.new(|cx| QueuePage::new(cx)); 46 44 let device_picker = cx.new(|cx| { 47 - let _ = cx.observe_global::<DevicesState>(|_, cx| cx.notify()); 45 + cx.observe_global::<DevicesState>(|_, cx| cx.notify()).detach(); 48 46 DevicePicker 49 47 }); 50 48 let bluetooth_picker = cx.new(|cx| { 51 - let _ = cx.observe_global::<BluetoothState>(|_, cx| cx.notify()); 49 + cx.observe_global::<BluetoothState>(|_, cx| cx.notify()).detach(); 52 50 BluetoothPicker 53 51 }); 54 52 fetch_and_update_devices(cx);