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.

Merge pull request #171 from tsirysndr/feat/bluetooth-ui

Add Bluetooth device support to UIs and gRPC

authored by

Tsiry Sandratraina and committed by
GitHub
044e84d8 a47f3021

+2190 -7
+1
gpui/assets/icons/bluetooth.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-bluetooth"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M7 8l10 8l-5 4l0 -16l5 4l-10 8" /></svg>
+1
gpui/build.rs
··· 14 14 "proto/rockbox/v1alpha1/system.proto", 15 15 "proto/rockbox/v1alpha1/saved_playlist.proto", 16 16 "proto/rockbox/v1alpha1/smart_playlist.proto", 17 + "proto/rockbox/v1alpha1/bluetooth.proto", 17 18 ], 18 19 &["proto"], 19 20 )?;
+581
gpui/src/api/rockbox.v1alpha1.rs
··· 11994 11994 const NAME: &'static str = SERVICE_NAME; 11995 11995 } 11996 11996 } 11997 + #[derive(Clone, PartialEq, ::prost::Message)] 11998 + pub struct BluetoothDevice { 11999 + #[prost(string, tag = "1")] 12000 + pub address: ::prost::alloc::string::String, 12001 + #[prost(string, tag = "2")] 12002 + pub name: ::prost::alloc::string::String, 12003 + #[prost(bool, tag = "3")] 12004 + pub paired: bool, 12005 + #[prost(bool, tag = "4")] 12006 + pub trusted: bool, 12007 + #[prost(bool, tag = "5")] 12008 + pub connected: bool, 12009 + #[prost(int32, optional, tag = "6")] 12010 + pub rssi: ::core::option::Option<i32>, 12011 + } 12012 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 12013 + pub struct ScanBluetoothRequest { 12014 + #[prost(uint32, tag = "1")] 12015 + pub timeout_secs: u32, 12016 + } 12017 + #[derive(Clone, PartialEq, ::prost::Message)] 12018 + pub struct ScanBluetoothResponse { 12019 + #[prost(message, repeated, tag = "1")] 12020 + pub devices: ::prost::alloc::vec::Vec<BluetoothDevice>, 12021 + } 12022 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 12023 + pub struct GetBluetoothDevicesRequest {} 12024 + #[derive(Clone, PartialEq, ::prost::Message)] 12025 + pub struct GetBluetoothDevicesResponse { 12026 + #[prost(message, repeated, tag = "1")] 12027 + pub devices: ::prost::alloc::vec::Vec<BluetoothDevice>, 12028 + } 12029 + #[derive(Clone, PartialEq, ::prost::Message)] 12030 + pub struct ConnectBluetoothDeviceRequest { 12031 + #[prost(string, tag = "1")] 12032 + pub address: ::prost::alloc::string::String, 12033 + } 12034 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 12035 + pub struct ConnectBluetoothDeviceResponse {} 12036 + #[derive(Clone, PartialEq, ::prost::Message)] 12037 + pub struct DisconnectBluetoothDeviceRequest { 12038 + #[prost(string, tag = "1")] 12039 + pub address: ::prost::alloc::string::String, 12040 + } 12041 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 12042 + pub struct DisconnectBluetoothDeviceResponse {} 12043 + /// Generated client implementations. 12044 + pub mod bluetooth_service_client { 12045 + #![allow( 12046 + unused_variables, 12047 + dead_code, 12048 + missing_docs, 12049 + clippy::wildcard_imports, 12050 + clippy::let_unit_value, 12051 + )] 12052 + use tonic::codegen::*; 12053 + use tonic::codegen::http::Uri; 12054 + #[derive(Debug, Clone)] 12055 + pub struct BluetoothServiceClient<T> { 12056 + inner: tonic::client::Grpc<T>, 12057 + } 12058 + impl BluetoothServiceClient<tonic::transport::Channel> { 12059 + /// Attempt to create a new client by connecting to a given endpoint. 12060 + pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error> 12061 + where 12062 + D: TryInto<tonic::transport::Endpoint>, 12063 + D::Error: Into<StdError>, 12064 + { 12065 + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; 12066 + Ok(Self::new(conn)) 12067 + } 12068 + } 12069 + impl<T> BluetoothServiceClient<T> 12070 + where 12071 + T: tonic::client::GrpcService<tonic::body::BoxBody>, 12072 + T::Error: Into<StdError>, 12073 + T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static, 12074 + <T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send, 12075 + { 12076 + pub fn new(inner: T) -> Self { 12077 + let inner = tonic::client::Grpc::new(inner); 12078 + Self { inner } 12079 + } 12080 + pub fn with_origin(inner: T, origin: Uri) -> Self { 12081 + let inner = tonic::client::Grpc::with_origin(inner, origin); 12082 + Self { inner } 12083 + } 12084 + pub fn with_interceptor<F>( 12085 + inner: T, 12086 + interceptor: F, 12087 + ) -> BluetoothServiceClient<InterceptedService<T, F>> 12088 + where 12089 + F: tonic::service::Interceptor, 12090 + T::ResponseBody: Default, 12091 + T: tonic::codegen::Service< 12092 + http::Request<tonic::body::BoxBody>, 12093 + Response = http::Response< 12094 + <T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody, 12095 + >, 12096 + >, 12097 + <T as tonic::codegen::Service< 12098 + http::Request<tonic::body::BoxBody>, 12099 + >>::Error: Into<StdError> + std::marker::Send + std::marker::Sync, 12100 + { 12101 + BluetoothServiceClient::new(InterceptedService::new(inner, interceptor)) 12102 + } 12103 + /// Compress requests with the given encoding. 12104 + /// 12105 + /// This requires the server to support it otherwise it might respond with an 12106 + /// error. 12107 + #[must_use] 12108 + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 12109 + self.inner = self.inner.send_compressed(encoding); 12110 + self 12111 + } 12112 + /// Enable decompressing responses. 12113 + #[must_use] 12114 + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 12115 + self.inner = self.inner.accept_compressed(encoding); 12116 + self 12117 + } 12118 + /// Limits the maximum size of a decoded message. 12119 + /// 12120 + /// Default: `4MB` 12121 + #[must_use] 12122 + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { 12123 + self.inner = self.inner.max_decoding_message_size(limit); 12124 + self 12125 + } 12126 + /// Limits the maximum size of an encoded message. 12127 + /// 12128 + /// Default: `usize::MAX` 12129 + #[must_use] 12130 + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { 12131 + self.inner = self.inner.max_encoding_message_size(limit); 12132 + self 12133 + } 12134 + pub async fn scan( 12135 + &mut self, 12136 + request: impl tonic::IntoRequest<super::ScanBluetoothRequest>, 12137 + ) -> std::result::Result< 12138 + tonic::Response<super::ScanBluetoothResponse>, 12139 + tonic::Status, 12140 + > { 12141 + self.inner 12142 + .ready() 12143 + .await 12144 + .map_err(|e| { 12145 + tonic::Status::unknown( 12146 + format!("Service was not ready: {}", e.into()), 12147 + ) 12148 + })?; 12149 + let codec = tonic::codec::ProstCodec::default(); 12150 + let path = http::uri::PathAndQuery::from_static( 12151 + "/rockbox.v1alpha1.BluetoothService/Scan", 12152 + ); 12153 + let mut req = request.into_request(); 12154 + req.extensions_mut() 12155 + .insert(GrpcMethod::new("rockbox.v1alpha1.BluetoothService", "Scan")); 12156 + self.inner.unary(req, path, codec).await 12157 + } 12158 + pub async fn get_devices( 12159 + &mut self, 12160 + request: impl tonic::IntoRequest<super::GetBluetoothDevicesRequest>, 12161 + ) -> std::result::Result< 12162 + tonic::Response<super::GetBluetoothDevicesResponse>, 12163 + tonic::Status, 12164 + > { 12165 + self.inner 12166 + .ready() 12167 + .await 12168 + .map_err(|e| { 12169 + tonic::Status::unknown( 12170 + format!("Service was not ready: {}", e.into()), 12171 + ) 12172 + })?; 12173 + let codec = tonic::codec::ProstCodec::default(); 12174 + let path = http::uri::PathAndQuery::from_static( 12175 + "/rockbox.v1alpha1.BluetoothService/GetDevices", 12176 + ); 12177 + let mut req = request.into_request(); 12178 + req.extensions_mut() 12179 + .insert( 12180 + GrpcMethod::new("rockbox.v1alpha1.BluetoothService", "GetDevices"), 12181 + ); 12182 + self.inner.unary(req, path, codec).await 12183 + } 12184 + pub async fn connect_device( 12185 + &mut self, 12186 + request: impl tonic::IntoRequest<super::ConnectBluetoothDeviceRequest>, 12187 + ) -> std::result::Result< 12188 + tonic::Response<super::ConnectBluetoothDeviceResponse>, 12189 + tonic::Status, 12190 + > { 12191 + self.inner 12192 + .ready() 12193 + .await 12194 + .map_err(|e| { 12195 + tonic::Status::unknown( 12196 + format!("Service was not ready: {}", e.into()), 12197 + ) 12198 + })?; 12199 + let codec = tonic::codec::ProstCodec::default(); 12200 + let path = http::uri::PathAndQuery::from_static( 12201 + "/rockbox.v1alpha1.BluetoothService/ConnectDevice", 12202 + ); 12203 + let mut req = request.into_request(); 12204 + req.extensions_mut() 12205 + .insert( 12206 + GrpcMethod::new("rockbox.v1alpha1.BluetoothService", "ConnectDevice"), 12207 + ); 12208 + self.inner.unary(req, path, codec).await 12209 + } 12210 + pub async fn disconnect( 12211 + &mut self, 12212 + request: impl tonic::IntoRequest<super::DisconnectBluetoothDeviceRequest>, 12213 + ) -> std::result::Result< 12214 + tonic::Response<super::DisconnectBluetoothDeviceResponse>, 12215 + tonic::Status, 12216 + > { 12217 + self.inner 12218 + .ready() 12219 + .await 12220 + .map_err(|e| { 12221 + tonic::Status::unknown( 12222 + format!("Service was not ready: {}", e.into()), 12223 + ) 12224 + })?; 12225 + let codec = tonic::codec::ProstCodec::default(); 12226 + let path = http::uri::PathAndQuery::from_static( 12227 + "/rockbox.v1alpha1.BluetoothService/Disconnect", 12228 + ); 12229 + let mut req = request.into_request(); 12230 + req.extensions_mut() 12231 + .insert( 12232 + GrpcMethod::new("rockbox.v1alpha1.BluetoothService", "Disconnect"), 12233 + ); 12234 + self.inner.unary(req, path, codec).await 12235 + } 12236 + } 12237 + } 12238 + /// Generated server implementations. 12239 + pub mod bluetooth_service_server { 12240 + #![allow( 12241 + unused_variables, 12242 + dead_code, 12243 + missing_docs, 12244 + clippy::wildcard_imports, 12245 + clippy::let_unit_value, 12246 + )] 12247 + use tonic::codegen::*; 12248 + /// Generated trait containing gRPC methods that should be implemented for use with BluetoothServiceServer. 12249 + #[async_trait] 12250 + pub trait BluetoothService: std::marker::Send + std::marker::Sync + 'static { 12251 + async fn scan( 12252 + &self, 12253 + request: tonic::Request<super::ScanBluetoothRequest>, 12254 + ) -> std::result::Result< 12255 + tonic::Response<super::ScanBluetoothResponse>, 12256 + tonic::Status, 12257 + >; 12258 + async fn get_devices( 12259 + &self, 12260 + request: tonic::Request<super::GetBluetoothDevicesRequest>, 12261 + ) -> std::result::Result< 12262 + tonic::Response<super::GetBluetoothDevicesResponse>, 12263 + tonic::Status, 12264 + >; 12265 + async fn connect_device( 12266 + &self, 12267 + request: tonic::Request<super::ConnectBluetoothDeviceRequest>, 12268 + ) -> std::result::Result< 12269 + tonic::Response<super::ConnectBluetoothDeviceResponse>, 12270 + tonic::Status, 12271 + >; 12272 + async fn disconnect( 12273 + &self, 12274 + request: tonic::Request<super::DisconnectBluetoothDeviceRequest>, 12275 + ) -> std::result::Result< 12276 + tonic::Response<super::DisconnectBluetoothDeviceResponse>, 12277 + tonic::Status, 12278 + >; 12279 + } 12280 + #[derive(Debug)] 12281 + pub struct BluetoothServiceServer<T> { 12282 + inner: Arc<T>, 12283 + accept_compression_encodings: EnabledCompressionEncodings, 12284 + send_compression_encodings: EnabledCompressionEncodings, 12285 + max_decoding_message_size: Option<usize>, 12286 + max_encoding_message_size: Option<usize>, 12287 + } 12288 + impl<T> BluetoothServiceServer<T> { 12289 + pub fn new(inner: T) -> Self { 12290 + Self::from_arc(Arc::new(inner)) 12291 + } 12292 + pub fn from_arc(inner: Arc<T>) -> Self { 12293 + Self { 12294 + inner, 12295 + accept_compression_encodings: Default::default(), 12296 + send_compression_encodings: Default::default(), 12297 + max_decoding_message_size: None, 12298 + max_encoding_message_size: None, 12299 + } 12300 + } 12301 + pub fn with_interceptor<F>( 12302 + inner: T, 12303 + interceptor: F, 12304 + ) -> InterceptedService<Self, F> 12305 + where 12306 + F: tonic::service::Interceptor, 12307 + { 12308 + InterceptedService::new(Self::new(inner), interceptor) 12309 + } 12310 + /// Enable decompressing requests with the given encoding. 12311 + #[must_use] 12312 + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 12313 + self.accept_compression_encodings.enable(encoding); 12314 + self 12315 + } 12316 + /// Compress responses with the given encoding, if the client supports it. 12317 + #[must_use] 12318 + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 12319 + self.send_compression_encodings.enable(encoding); 12320 + self 12321 + } 12322 + /// Limits the maximum size of a decoded message. 12323 + /// 12324 + /// Default: `4MB` 12325 + #[must_use] 12326 + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { 12327 + self.max_decoding_message_size = Some(limit); 12328 + self 12329 + } 12330 + /// Limits the maximum size of an encoded message. 12331 + /// 12332 + /// Default: `usize::MAX` 12333 + #[must_use] 12334 + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { 12335 + self.max_encoding_message_size = Some(limit); 12336 + self 12337 + } 12338 + } 12339 + impl<T, B> tonic::codegen::Service<http::Request<B>> for BluetoothServiceServer<T> 12340 + where 12341 + T: BluetoothService, 12342 + B: Body + std::marker::Send + 'static, 12343 + B::Error: Into<StdError> + std::marker::Send + 'static, 12344 + { 12345 + type Response = http::Response<tonic::body::BoxBody>; 12346 + type Error = std::convert::Infallible; 12347 + type Future = BoxFuture<Self::Response, Self::Error>; 12348 + fn poll_ready( 12349 + &mut self, 12350 + _cx: &mut Context<'_>, 12351 + ) -> Poll<std::result::Result<(), Self::Error>> { 12352 + Poll::Ready(Ok(())) 12353 + } 12354 + fn call(&mut self, req: http::Request<B>) -> Self::Future { 12355 + match req.uri().path() { 12356 + "/rockbox.v1alpha1.BluetoothService/Scan" => { 12357 + #[allow(non_camel_case_types)] 12358 + struct ScanSvc<T: BluetoothService>(pub Arc<T>); 12359 + impl< 12360 + T: BluetoothService, 12361 + > tonic::server::UnaryService<super::ScanBluetoothRequest> 12362 + for ScanSvc<T> { 12363 + type Response = super::ScanBluetoothResponse; 12364 + type Future = BoxFuture< 12365 + tonic::Response<Self::Response>, 12366 + tonic::Status, 12367 + >; 12368 + fn call( 12369 + &mut self, 12370 + request: tonic::Request<super::ScanBluetoothRequest>, 12371 + ) -> Self::Future { 12372 + let inner = Arc::clone(&self.0); 12373 + let fut = async move { 12374 + <T as BluetoothService>::scan(&inner, request).await 12375 + }; 12376 + Box::pin(fut) 12377 + } 12378 + } 12379 + let accept_compression_encodings = self.accept_compression_encodings; 12380 + let send_compression_encodings = self.send_compression_encodings; 12381 + let max_decoding_message_size = self.max_decoding_message_size; 12382 + let max_encoding_message_size = self.max_encoding_message_size; 12383 + let inner = self.inner.clone(); 12384 + let fut = async move { 12385 + let method = ScanSvc(inner); 12386 + let codec = tonic::codec::ProstCodec::default(); 12387 + let mut grpc = tonic::server::Grpc::new(codec) 12388 + .apply_compression_config( 12389 + accept_compression_encodings, 12390 + send_compression_encodings, 12391 + ) 12392 + .apply_max_message_size_config( 12393 + max_decoding_message_size, 12394 + max_encoding_message_size, 12395 + ); 12396 + let res = grpc.unary(method, req).await; 12397 + Ok(res) 12398 + }; 12399 + Box::pin(fut) 12400 + } 12401 + "/rockbox.v1alpha1.BluetoothService/GetDevices" => { 12402 + #[allow(non_camel_case_types)] 12403 + struct GetDevicesSvc<T: BluetoothService>(pub Arc<T>); 12404 + impl< 12405 + T: BluetoothService, 12406 + > tonic::server::UnaryService<super::GetBluetoothDevicesRequest> 12407 + for GetDevicesSvc<T> { 12408 + type Response = super::GetBluetoothDevicesResponse; 12409 + type Future = BoxFuture< 12410 + tonic::Response<Self::Response>, 12411 + tonic::Status, 12412 + >; 12413 + fn call( 12414 + &mut self, 12415 + request: tonic::Request<super::GetBluetoothDevicesRequest>, 12416 + ) -> Self::Future { 12417 + let inner = Arc::clone(&self.0); 12418 + let fut = async move { 12419 + <T as BluetoothService>::get_devices(&inner, request).await 12420 + }; 12421 + Box::pin(fut) 12422 + } 12423 + } 12424 + let accept_compression_encodings = self.accept_compression_encodings; 12425 + let send_compression_encodings = self.send_compression_encodings; 12426 + let max_decoding_message_size = self.max_decoding_message_size; 12427 + let max_encoding_message_size = self.max_encoding_message_size; 12428 + let inner = self.inner.clone(); 12429 + let fut = async move { 12430 + let method = GetDevicesSvc(inner); 12431 + let codec = tonic::codec::ProstCodec::default(); 12432 + let mut grpc = tonic::server::Grpc::new(codec) 12433 + .apply_compression_config( 12434 + accept_compression_encodings, 12435 + send_compression_encodings, 12436 + ) 12437 + .apply_max_message_size_config( 12438 + max_decoding_message_size, 12439 + max_encoding_message_size, 12440 + ); 12441 + let res = grpc.unary(method, req).await; 12442 + Ok(res) 12443 + }; 12444 + Box::pin(fut) 12445 + } 12446 + "/rockbox.v1alpha1.BluetoothService/ConnectDevice" => { 12447 + #[allow(non_camel_case_types)] 12448 + struct ConnectDeviceSvc<T: BluetoothService>(pub Arc<T>); 12449 + impl< 12450 + T: BluetoothService, 12451 + > tonic::server::UnaryService<super::ConnectBluetoothDeviceRequest> 12452 + for ConnectDeviceSvc<T> { 12453 + type Response = super::ConnectBluetoothDeviceResponse; 12454 + type Future = BoxFuture< 12455 + tonic::Response<Self::Response>, 12456 + tonic::Status, 12457 + >; 12458 + fn call( 12459 + &mut self, 12460 + request: tonic::Request<super::ConnectBluetoothDeviceRequest>, 12461 + ) -> Self::Future { 12462 + let inner = Arc::clone(&self.0); 12463 + let fut = async move { 12464 + <T as BluetoothService>::connect_device(&inner, request) 12465 + .await 12466 + }; 12467 + Box::pin(fut) 12468 + } 12469 + } 12470 + let accept_compression_encodings = self.accept_compression_encodings; 12471 + let send_compression_encodings = self.send_compression_encodings; 12472 + let max_decoding_message_size = self.max_decoding_message_size; 12473 + let max_encoding_message_size = self.max_encoding_message_size; 12474 + let inner = self.inner.clone(); 12475 + let fut = async move { 12476 + let method = ConnectDeviceSvc(inner); 12477 + let codec = tonic::codec::ProstCodec::default(); 12478 + let mut grpc = tonic::server::Grpc::new(codec) 12479 + .apply_compression_config( 12480 + accept_compression_encodings, 12481 + send_compression_encodings, 12482 + ) 12483 + .apply_max_message_size_config( 12484 + max_decoding_message_size, 12485 + max_encoding_message_size, 12486 + ); 12487 + let res = grpc.unary(method, req).await; 12488 + Ok(res) 12489 + }; 12490 + Box::pin(fut) 12491 + } 12492 + "/rockbox.v1alpha1.BluetoothService/Disconnect" => { 12493 + #[allow(non_camel_case_types)] 12494 + struct DisconnectSvc<T: BluetoothService>(pub Arc<T>); 12495 + impl< 12496 + T: BluetoothService, 12497 + > tonic::server::UnaryService< 12498 + super::DisconnectBluetoothDeviceRequest, 12499 + > for DisconnectSvc<T> { 12500 + type Response = super::DisconnectBluetoothDeviceResponse; 12501 + type Future = BoxFuture< 12502 + tonic::Response<Self::Response>, 12503 + tonic::Status, 12504 + >; 12505 + fn call( 12506 + &mut self, 12507 + request: tonic::Request< 12508 + super::DisconnectBluetoothDeviceRequest, 12509 + >, 12510 + ) -> Self::Future { 12511 + let inner = Arc::clone(&self.0); 12512 + let fut = async move { 12513 + <T as BluetoothService>::disconnect(&inner, request).await 12514 + }; 12515 + Box::pin(fut) 12516 + } 12517 + } 12518 + let accept_compression_encodings = self.accept_compression_encodings; 12519 + let send_compression_encodings = self.send_compression_encodings; 12520 + let max_decoding_message_size = self.max_decoding_message_size; 12521 + let max_encoding_message_size = self.max_encoding_message_size; 12522 + let inner = self.inner.clone(); 12523 + let fut = async move { 12524 + let method = DisconnectSvc(inner); 12525 + let codec = tonic::codec::ProstCodec::default(); 12526 + let mut grpc = tonic::server::Grpc::new(codec) 12527 + .apply_compression_config( 12528 + accept_compression_encodings, 12529 + send_compression_encodings, 12530 + ) 12531 + .apply_max_message_size_config( 12532 + max_decoding_message_size, 12533 + max_encoding_message_size, 12534 + ); 12535 + let res = grpc.unary(method, req).await; 12536 + Ok(res) 12537 + }; 12538 + Box::pin(fut) 12539 + } 12540 + _ => { 12541 + Box::pin(async move { 12542 + let mut response = http::Response::new(empty_body()); 12543 + let headers = response.headers_mut(); 12544 + headers 12545 + .insert( 12546 + tonic::Status::GRPC_STATUS, 12547 + (tonic::Code::Unimplemented as i32).into(), 12548 + ); 12549 + headers 12550 + .insert( 12551 + http::header::CONTENT_TYPE, 12552 + tonic::metadata::GRPC_CONTENT_TYPE, 12553 + ); 12554 + Ok(response) 12555 + }) 12556 + } 12557 + } 12558 + } 12559 + } 12560 + impl<T> Clone for BluetoothServiceServer<T> { 12561 + fn clone(&self) -> Self { 12562 + let inner = self.inner.clone(); 12563 + Self { 12564 + inner, 12565 + accept_compression_encodings: self.accept_compression_encodings, 12566 + send_compression_encodings: self.send_compression_encodings, 12567 + max_decoding_message_size: self.max_decoding_message_size, 12568 + max_encoding_message_size: self.max_encoding_message_size, 12569 + } 12570 + } 12571 + } 12572 + /// Generated gRPC service name 12573 + pub const SERVICE_NAME: &str = "rockbox.v1alpha1.BluetoothService"; 12574 + impl<T> tonic::server::NamedService for BluetoothServiceServer<T> { 12575 + const NAME: &'static str = SERVICE_NAME; 12576 + } 12577 + }
gpui/src/api/rockbox_descriptor.bin

This is a binary file and will not be displayed.

+42 -2
gpui/src/client.rs
··· 1 1 use crate::api::v1alpha1::{ 2 - browse_service_client::BrowseServiceClient, library_service_client::LibraryServiceClient, 2 + bluetooth_service_client::BluetoothServiceClient, browse_service_client::BrowseServiceClient, 3 + library_service_client::LibraryServiceClient, 3 4 playback_service_client::PlaybackServiceClient, playlist_service_client::PlaylistServiceClient, 4 5 settings_service_client::SettingsServiceClient, sound_service_client::SoundServiceClient, 5 6 system_service_client::SystemServiceClient, AdjustVolumeRequest, FastForwardRewindRequest, 6 7 GetArtistsRequest, GetCurrentRequest, GetGlobalSettingsRequest, GetGlobalStatusRequest, 7 8 GetAlbumRequest, GetLikedTracksRequest, GetTracksRequest, InsertDirectoryRequest, 8 - InsertTracksRequest, 9 + ConnectBluetoothDeviceRequest, DisconnectBluetoothDeviceRequest, 10 + GetBluetoothDevicesRequest, InsertTracksRequest, 9 11 LikeTrackRequest, NextRequest, PauseRequest, PlayAlbumRequest, PlayAllTracksRequest, 10 12 PlayArtistTracksRequest, PlayDirectoryRequest, PlayTrackRequest, PlaylistResumeRequest, 11 13 PreviousRequest, RemoveTracksRequest, ResumeRequest, ResumeTrackRequest, SaveSettingsRequest, ··· 984 986 .await?; 985 987 Ok(()) 986 988 } 989 + 990 + // ── Bluetooth API (gRPC) ────────────────────────────────────────────────────── 991 + 992 + pub async fn check_bluetooth_available() -> bool { 993 + BluetoothServiceClient::connect(url()) 994 + .await 995 + .is_ok() 996 + } 997 + 998 + pub async fn fetch_bluetooth_devices() -> Result<Vec<crate::state::BluetoothDevice>> { 999 + let mut c = BluetoothServiceClient::connect(url()).await?; 1000 + let resp = c.get_devices(GetBluetoothDevicesRequest {}).await?; 1001 + Ok(resp 1002 + .into_inner() 1003 + .devices 1004 + .into_iter() 1005 + .map(|d| crate::state::BluetoothDevice { 1006 + address: d.address, 1007 + name: d.name, 1008 + paired: d.paired, 1009 + trusted: d.trusted, 1010 + connected: d.connected, 1011 + rssi: d.rssi, 1012 + }) 1013 + .collect()) 1014 + } 1015 + 1016 + pub async fn connect_bluetooth_device(address: String) -> Result<()> { 1017 + let mut c = BluetoothServiceClient::connect(url()).await?; 1018 + c.connect_device(ConnectBluetoothDeviceRequest { address }).await?; 1019 + Ok(()) 1020 + } 1021 + 1022 + pub async fn disconnect_bluetooth_device(address: String) -> Result<()> { 1023 + let mut c = BluetoothServiceClient::connect(url()).await?; 1024 + c.disconnect(DisconnectBluetoothDeviceRequest { address }).await?; 1025 + Ok(()) 1026 + }
+18
gpui/src/state.rs
··· 181 181 format!("{}:{:02}", secs / 60, secs % 60) 182 182 } 183 183 184 + #[derive(Clone, Debug, Default)] 185 + pub struct BluetoothDevice { 186 + pub address: String, 187 + pub name: String, 188 + pub paired: bool, 189 + pub trusted: bool, 190 + pub connected: bool, 191 + pub rssi: Option<i32>, 192 + } 193 + 194 + #[derive(Clone, Default)] 195 + pub struct BluetoothState { 196 + pub devices: Vec<BluetoothDevice>, 197 + pub picker_open: bool, 198 + pub available: bool, 199 + } 200 + impl gpui::Global for BluetoothState {} 201 + 184 202 /// Stores the Tokio runtime handle so GPUI code can run async tasks that require a Tokio reactor. 185 203 #[derive(Clone)] 186 204 pub struct TokioHandle(pub tokio::runtime::Handle);
+196
gpui/src/ui/components/bluetooth_picker.rs
··· 1 + use crate::controller::Controller; 2 + use crate::state::{BluetoothDevice, BluetoothState}; 3 + use crate::ui::components::icons::{Icon, Icons}; 4 + use crate::ui::theme::Theme; 5 + use gpui::prelude::FluentBuilder; 6 + use gpui::{ 7 + div, px, App, Context, FontWeight, InteractiveElement, IntoElement, MouseButton, 8 + MouseMoveEvent, ParentElement, Render, StatefulInteractiveElement, Styled, Window, 9 + }; 10 + 11 + pub struct BluetoothPicker; 12 + 13 + impl Render for BluetoothPicker { 14 + fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { 15 + let theme = *cx.global::<Theme>(); 16 + let state = cx.global::<BluetoothState>().clone(); 17 + 18 + if !state.picker_open { 19 + return div().into_any_element(); 20 + } 21 + 22 + div() 23 + .id("bluetooth-picker-backdrop") 24 + .absolute() 25 + .top_0() 26 + .left_0() 27 + .w_full() 28 + .h_full() 29 + .on_mouse_down(MouseButton::Left, |_, _, cx| { 30 + let mut state = cx.global::<BluetoothState>().clone(); 31 + state.picker_open = false; 32 + cx.set_global(state); 33 + cx.stop_propagation(); 34 + }) 35 + .on_mouse_move(|_: &MouseMoveEvent, _, cx| { 36 + cx.stop_propagation(); 37 + }) 38 + .on_scroll_wheel(|_, _, _| {}) 39 + .child( 40 + div() 41 + .id("bluetooth-picker-popup") 42 + .absolute() 43 + .bottom(px(80.0)) 44 + .right(px(52.0)) 45 + .w(px(280.0)) 46 + .bg(theme.app_bg) 47 + .border_1() 48 + .border_color(theme.border) 49 + .rounded_lg() 50 + .shadow_lg() 51 + .on_mouse_down(MouseButton::Left, |_, _, cx| { 52 + cx.stop_propagation(); 53 + }) 54 + .on_scroll_wheel(|_, _, cx| { 55 + cx.stop_propagation(); 56 + }) 57 + .child( 58 + div() 59 + .px_4() 60 + .py_3() 61 + .border_b_1() 62 + .border_color(theme.border) 63 + .child( 64 + div() 65 + .text_sm() 66 + .font_weight(FontWeight(600.0)) 67 + .text_color(theme.player_title_text) 68 + .child("Bluetooth speakers"), 69 + ), 70 + ) 71 + .child( 72 + div() 73 + .id("bluetooth-picker-list") 74 + .py_1() 75 + .max_h(px(280.0)) 76 + .overflow_y_scroll() 77 + .when(state.devices.is_empty(), |this| { 78 + this.child( 79 + div() 80 + .px_4() 81 + .py_3() 82 + .text_sm() 83 + .text_color(theme.player_title_text) 84 + .child("No bluetooth devices found."), 85 + ) 86 + }) 87 + .children(state.devices.iter().cloned().map(|device| { 88 + let address = device.address.clone(); 89 + let is_connected = device.connected; 90 + let name = device.name.clone(); 91 + 92 + div() 93 + .id(gpui::SharedString::from(format!( 94 + "bt-row-{}", 95 + device.address 96 + ))) 97 + .px_4() 98 + .py_2() 99 + .flex() 100 + .items_center() 101 + .gap_x_3() 102 + .cursor_pointer() 103 + .text_color(if is_connected { 104 + theme.player_icons_text_active 105 + } else { 106 + theme.player_title_text 107 + }) 108 + .when(is_connected, |this: gpui::Stateful<gpui::Div>| { 109 + this.bg(theme.player_icons_bg_active) 110 + }) 111 + .hover(|this| this.bg(theme.player_icons_bg_hover)) 112 + .on_click(move |_, _, cx: &mut App| { 113 + let rt = cx.global::<Controller>().rt(); 114 + let addr = address.clone(); 115 + if is_connected { 116 + rt.spawn(async move { 117 + let _ = crate::client::disconnect_bluetooth_device(addr).await; 118 + }); 119 + } else { 120 + rt.spawn(async move { 121 + let _ = crate::client::connect_bluetooth_device(addr).await; 122 + }); 123 + } 124 + let mut state = cx.global::<BluetoothState>().clone(); 125 + for d in state.devices.iter_mut() { 126 + if d.address == address { 127 + d.connected = !is_connected; 128 + } 129 + } 130 + state.picker_open = false; 131 + cx.set_global(state); 132 + }) 133 + .child( 134 + div() 135 + .flex_shrink_0() 136 + .child(Icon::new(Icons::Bluetooth).size_4()), 137 + ) 138 + .child( 139 + div() 140 + .flex_1() 141 + .text_sm() 142 + .truncate() 143 + .child(name), 144 + ) 145 + .when(is_connected, |this: gpui::Stateful<gpui::Div>| { 146 + this.child( 147 + div() 148 + .flex_shrink_0() 149 + .child(Icon::new(Icons::Bluetooth).size_3()), 150 + ) 151 + }) 152 + })), 153 + ), 154 + ) 155 + .into_any_element() 156 + } 157 + } 158 + 159 + pub fn fetch_and_update_bluetooth_devices(cx: &mut App) { 160 + let rt = cx.global::<Controller>().rt(); 161 + let (tx, mut rx) = tokio::sync::mpsc::channel::<Vec<BluetoothDevice>>(1); 162 + rt.spawn(async move { 163 + if let Ok(devices) = crate::client::fetch_bluetooth_devices().await { 164 + let _ = tx.send(devices).await; 165 + } 166 + }); 167 + 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 + }); 174 + } 175 + }) 176 + .detach(); 177 + } 178 + 179 + pub fn check_and_set_bluetooth_available(cx: &mut App) { 180 + let rt = cx.global::<Controller>().rt(); 181 + let (tx, mut rx) = tokio::sync::mpsc::channel::<bool>(1); 182 + rt.spawn(async move { 183 + let available = crate::client::check_bluetooth_available().await; 184 + let _ = tx.send(available).await; 185 + }); 186 + cx.spawn(async move |cx| { 187 + if let Some(available) = rx.recv().await { 188 + let _ = cx.update(|cx| { 189 + let mut state = cx.global::<BluetoothState>().clone(); 190 + state.available = available; 191 + cx.set_global(state); 192 + }); 193 + } 194 + }) 195 + .detach(); 196 + }
+31 -3
gpui/src/ui/components/controlbar.rs
··· 1 1 use crate::controller::Controller; 2 - use crate::state::{format_duration, DevicesState}; 2 + use crate::state::{format_duration, BluetoothState, DevicesState}; 3 + use crate::ui::components::bluetooth_picker::fetch_and_update_bluetooth_devices; 3 4 use crate::ui::components::device_picker::{device_icon, fetch_and_update_devices}; 4 5 use crate::ui::components::icons::{Icon, Icons}; 5 6 use crate::ui::components::seek_bar::SeekBar; 6 7 use crate::ui::theme::Theme; 8 + use gpui::prelude::FluentBuilder; 7 9 use gpui::{ 8 - div, px, App, Context, InteractiveElement, IntoElement, ParentElement, Render, 10 + div, px, App, Context, Div, InteractiveElement, IntoElement, ParentElement, Render, 9 11 StatefulInteractiveElement, Styled, Window, 10 12 }; 11 13 ··· 30 32 .find(|d| d.is_current_device) 31 33 .map(|d| device_icon(d)) 32 34 .unwrap_or(Icons::Speaker); 35 + let bluetooth_available = cx.global::<BluetoothState>().available; 33 36 34 37 div() 35 38 .w_full() ··· 76 79 .child(format_duration(duration)), 77 80 ), 78 81 ) 79 - // Device picker — right 82 + // Device + Bluetooth pickers — right 80 83 .child( 81 84 div() 82 85 .w(px(160.0)) 83 86 .flex() 84 87 .items_center() 85 88 .justify_end() 89 + .gap_x_1() 90 + .when(bluetooth_available, |this: Div| { 91 + this.child( 92 + div() 93 + .id("controlbar-bluetooth-btn") 94 + .p_1p5() 95 + .rounded_md() 96 + .flex() 97 + .items_center() 98 + .justify_center() 99 + .cursor_pointer() 100 + .text_color(theme.player_icons_text) 101 + .hover(|this| { 102 + this.bg(theme.player_icons_bg_hover) 103 + .text_color(theme.player_icons_text_hover) 104 + }) 105 + .on_click(move |_, _, cx: &mut App| { 106 + fetch_and_update_bluetooth_devices(cx); 107 + let mut state = cx.global::<BluetoothState>().clone(); 108 + state.picker_open = !state.picker_open; 109 + cx.set_global(state); 110 + }) 111 + .child(Icon::new(Icons::Bluetooth).size_4()), 112 + ) 113 + }) 86 114 .child( 87 115 div() 88 116 .id("controlbar-device-btn")
+2
gpui/src/ui/components/icons.rs
··· 180 180 Chromecast, 181 181 Airplay, 182 182 Upnp, 183 + Bluetooth, 183 184 } 184 185 185 186 impl IconNamed for Icons { ··· 218 219 Icons::Chromecast => "icons/chromecast.svg", 219 220 Icons::Airplay => "icons/airplay.svg", 220 221 Icons::Upnp => "icons/upnp.svg", 222 + Icons::Bluetooth => "icons/bluetooth.svg", 221 223 } 222 224 .into() 223 225 }
+1
gpui/src/ui/components/mod.rs
··· 1 + pub mod bluetooth_picker; 1 2 pub mod controlbar; 2 3 pub mod device_picker; 3 4 pub mod icons;
+14 -1
gpui/src/ui/rockbox.rs
··· 1 - use crate::state::DevicesState; 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 6 use crate::ui::components::controlbar::ControlBar; 4 7 use crate::ui::components::device_picker::{fetch_and_update_devices, DevicePicker}; 5 8 use crate::ui::components::pages::{library::LibraryPage, player::PlayerPage, queue::QueuePage}; ··· 21 24 pub library_page: Entity<LibraryPage>, 22 25 pub queue_page: Entity<QueuePage>, 23 26 pub device_picker: Entity<DevicePicker>, 27 + pub bluetooth_picker: Entity<BluetoothPicker>, 24 28 } 25 29 26 30 impl Rockbox { ··· 28 32 cx.set_global(Theme::default()); 29 33 cx.set_global(Page::Player); 30 34 cx.set_global(DevicesState::default()); 35 + cx.set_global(BluetoothState::default()); 31 36 global_keybinds::register_keybinds(cx); 32 37 let titlebar = cx.new(|cx| Titlebar::new(cx)); 33 38 let controlbar = cx.new(|cx| { 34 39 let _ = cx.observe_global::<DevicesState>(|_, cx| cx.notify()); 40 + let _ = cx.observe_global::<BluetoothState>(|_, cx| cx.notify()); 35 41 ControlBar 36 42 }); 37 43 let player_page = cx.new(|cx| PlayerPage::new(cx, controlbar)); ··· 41 47 let _ = cx.observe_global::<DevicesState>(|_, cx| cx.notify()); 42 48 DevicePicker 43 49 }); 50 + let bluetooth_picker = cx.new(|cx| { 51 + let _ = cx.observe_global::<BluetoothState>(|_, cx| cx.notify()); 52 + BluetoothPicker 53 + }); 44 54 fetch_and_update_devices(cx); 55 + check_and_set_bluetooth_available(cx); 45 56 Rockbox { 46 57 focus_handle: cx.focus_handle(), 47 58 titlebar, ··· 49 60 library_page, 50 61 queue_page, 51 62 device_picker, 63 + bluetooth_picker, 52 64 } 53 65 } 54 66 } ··· 135 147 }), 136 148 ) 137 149 .child(self.device_picker.clone()) 150 + .child(self.bluetooth_picker.clone()) 138 151 } 139 152 }
+3
macos/Rockbox/RockboxApp.swift
··· 12 12 @StateObject private var navigation = NavigationManager() 13 13 @StateObject private var searchManager = SearchManager() 14 14 @StateObject private var deviceState = DeviceState() 15 + @StateObject private var bluetoothState = BluetoothState() 15 16 @State private var startupFailed = false 16 17 @State private var startupError: Error? 17 18 ··· 22 23 .environmentObject(navigation) 23 24 .environmentObject(searchManager) 24 25 .environmentObject(deviceState) 26 + .environmentObject(bluetoothState) 25 27 .alert("Connection Failed", isPresented: $startupFailed) { 26 28 Button("Retry") { 27 29 retry() ··· 67 69 player.startStreaming() 68 70 player.fetchSettings() 69 71 await deviceState.refresh() 72 + await bluetoothState.checkAvailability() 70 73 71 74 } catch { 72 75 startupError = error
+47
macos/Rockbox/Services/BluetoothService.swift
··· 1 + // 2 + // BluetoothService.swift 3 + // Rockbox 4 + // 5 + 6 + import Foundation 7 + import GRPCCore 8 + import GRPCNIOTransportHTTP2 9 + 10 + @available(macOS 15.0, *) 11 + func fetchBluetoothDevices() async throws -> [Rockbox_V1alpha1_BluetoothDevice] { 12 + try await withRockboxGRPCClient { grpcClient in 13 + let bt = Rockbox_V1alpha1_BluetoothService.Client(wrapping: grpcClient) 14 + let resp = try await bt.getDevices(Rockbox_V1alpha1_GetBluetoothDevicesRequest()) 15 + return resp.devices 16 + } 17 + } 18 + 19 + @available(macOS 15.0, *) 20 + func checkBluetoothAvailable() async -> Bool { 21 + do { 22 + _ = try await fetchBluetoothDevices() 23 + return true 24 + } catch { 25 + return false 26 + } 27 + } 28 + 29 + @available(macOS 15.0, *) 30 + func connectBluetoothDevice(address: String) async throws { 31 + try await withRockboxGRPCClient { grpcClient in 32 + let bt = Rockbox_V1alpha1_BluetoothService.Client(wrapping: grpcClient) 33 + var req = Rockbox_V1alpha1_ConnectBluetoothDeviceRequest() 34 + req.address = address 35 + let _ = try await bt.connectDevice(req) 36 + } 37 + } 38 + 39 + @available(macOS 15.0, *) 40 + func disconnectBluetoothDevice(address: String) async throws { 41 + try await withRockboxGRPCClient { grpcClient in 42 + let bt = Rockbox_V1alpha1_BluetoothService.Client(wrapping: grpcClient) 43 + var req = Rockbox_V1alpha1_DisconnectBluetoothDeviceRequest() 44 + req.address = address 45 + let _ = try await bt.disconnect(req) 46 + } 47 + }
+281
macos/Rockbox/Services/rockbox/v1alpha1/bluetooth.grpc.swift
··· 1 + // DO NOT EDIT. 2 + // swift-format-ignore-file 3 + // swiftlint:disable all 4 + // 5 + // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. 6 + // Source: rockbox/v1alpha1/bluetooth.proto 7 + // 8 + // For information on using the generated types, please see the documentation: 9 + // https://github.com/grpc/grpc-swift 10 + 11 + import GRPCCore 12 + import GRPCProtobuf 13 + 14 + // MARK: - rockbox.v1alpha1.BluetoothService 15 + 16 + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) 17 + internal enum Rockbox_V1alpha1_BluetoothService: Sendable { 18 + internal static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "rockbox.v1alpha1.BluetoothService") 19 + internal enum Method: Sendable { 20 + internal enum Scan: Sendable { 21 + internal typealias Input = Rockbox_V1alpha1_ScanBluetoothRequest 22 + internal typealias Output = Rockbox_V1alpha1_ScanBluetoothResponse 23 + internal static let descriptor = GRPCCore.MethodDescriptor( 24 + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "rockbox.v1alpha1.BluetoothService"), 25 + method: "Scan" 26 + ) 27 + } 28 + internal enum GetDevices: Sendable { 29 + internal typealias Input = Rockbox_V1alpha1_GetBluetoothDevicesRequest 30 + internal typealias Output = Rockbox_V1alpha1_GetBluetoothDevicesResponse 31 + internal static let descriptor = GRPCCore.MethodDescriptor( 32 + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "rockbox.v1alpha1.BluetoothService"), 33 + method: "GetDevices" 34 + ) 35 + } 36 + internal enum ConnectDevice: Sendable { 37 + internal typealias Input = Rockbox_V1alpha1_ConnectBluetoothDeviceRequest 38 + internal typealias Output = Rockbox_V1alpha1_ConnectBluetoothDeviceResponse 39 + internal static let descriptor = GRPCCore.MethodDescriptor( 40 + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "rockbox.v1alpha1.BluetoothService"), 41 + method: "ConnectDevice" 42 + ) 43 + } 44 + internal enum Disconnect: Sendable { 45 + internal typealias Input = Rockbox_V1alpha1_DisconnectBluetoothDeviceRequest 46 + internal typealias Output = Rockbox_V1alpha1_DisconnectBluetoothDeviceResponse 47 + internal static let descriptor = GRPCCore.MethodDescriptor( 48 + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "rockbox.v1alpha1.BluetoothService"), 49 + method: "Disconnect" 50 + ) 51 + } 52 + internal static let descriptors: [GRPCCore.MethodDescriptor] = [ 53 + Scan.descriptor, 54 + GetDevices.descriptor, 55 + ConnectDevice.descriptor, 56 + Disconnect.descriptor, 57 + ] 58 + } 59 + } 60 + 61 + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) 62 + extension GRPCCore.ServiceDescriptor { 63 + internal static let rockbox_v1Alpha1_BluetoothService = GRPCCore.ServiceDescriptor(fullyQualifiedService: "rockbox.v1alpha1.BluetoothService") 64 + } 65 + 66 + // MARK: rockbox.v1alpha1.BluetoothService (client) 67 + 68 + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) 69 + extension Rockbox_V1alpha1_BluetoothService { 70 + internal protocol ClientProtocol: Sendable { 71 + func scan<Result>( 72 + request: GRPCCore.ClientRequest<Rockbox_V1alpha1_ScanBluetoothRequest>, 73 + serializer: some GRPCCore.MessageSerializer<Rockbox_V1alpha1_ScanBluetoothRequest>, 74 + deserializer: some GRPCCore.MessageDeserializer<Rockbox_V1alpha1_ScanBluetoothResponse>, 75 + options: GRPCCore.CallOptions, 76 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Rockbox_V1alpha1_ScanBluetoothResponse>) async throws -> Result 77 + ) async throws -> Result where Result: Sendable 78 + 79 + func getDevices<Result>( 80 + request: GRPCCore.ClientRequest<Rockbox_V1alpha1_GetBluetoothDevicesRequest>, 81 + serializer: some GRPCCore.MessageSerializer<Rockbox_V1alpha1_GetBluetoothDevicesRequest>, 82 + deserializer: some GRPCCore.MessageDeserializer<Rockbox_V1alpha1_GetBluetoothDevicesResponse>, 83 + options: GRPCCore.CallOptions, 84 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Rockbox_V1alpha1_GetBluetoothDevicesResponse>) async throws -> Result 85 + ) async throws -> Result where Result: Sendable 86 + 87 + func connectDevice<Result>( 88 + request: GRPCCore.ClientRequest<Rockbox_V1alpha1_ConnectBluetoothDeviceRequest>, 89 + serializer: some GRPCCore.MessageSerializer<Rockbox_V1alpha1_ConnectBluetoothDeviceRequest>, 90 + deserializer: some GRPCCore.MessageDeserializer<Rockbox_V1alpha1_ConnectBluetoothDeviceResponse>, 91 + options: GRPCCore.CallOptions, 92 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Rockbox_V1alpha1_ConnectBluetoothDeviceResponse>) async throws -> Result 93 + ) async throws -> Result where Result: Sendable 94 + 95 + func disconnect<Result>( 96 + request: GRPCCore.ClientRequest<Rockbox_V1alpha1_DisconnectBluetoothDeviceRequest>, 97 + serializer: some GRPCCore.MessageSerializer<Rockbox_V1alpha1_DisconnectBluetoothDeviceRequest>, 98 + deserializer: some GRPCCore.MessageDeserializer<Rockbox_V1alpha1_DisconnectBluetoothDeviceResponse>, 99 + options: GRPCCore.CallOptions, 100 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Rockbox_V1alpha1_DisconnectBluetoothDeviceResponse>) async throws -> Result 101 + ) async throws -> Result where Result: Sendable 102 + } 103 + 104 + internal struct Client<Transport>: ClientProtocol where Transport: GRPCCore.ClientTransport { 105 + private let client: GRPCCore.GRPCClient<Transport> 106 + 107 + internal init(wrapping client: GRPCCore.GRPCClient<Transport>) { 108 + self.client = client 109 + } 110 + 111 + internal func scan<Result>( 112 + request: GRPCCore.ClientRequest<Rockbox_V1alpha1_ScanBluetoothRequest>, 113 + serializer: some GRPCCore.MessageSerializer<Rockbox_V1alpha1_ScanBluetoothRequest>, 114 + deserializer: some GRPCCore.MessageDeserializer<Rockbox_V1alpha1_ScanBluetoothResponse>, 115 + options: GRPCCore.CallOptions = .defaults, 116 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Rockbox_V1alpha1_ScanBluetoothResponse>) async throws -> Result = { response in try response.message } 117 + ) async throws -> Result where Result: Sendable { 118 + try await self.client.unary( 119 + request: request, 120 + descriptor: Rockbox_V1alpha1_BluetoothService.Method.Scan.descriptor, 121 + serializer: serializer, 122 + deserializer: deserializer, 123 + options: options, 124 + onResponse: handleResponse 125 + ) 126 + } 127 + 128 + internal func getDevices<Result>( 129 + request: GRPCCore.ClientRequest<Rockbox_V1alpha1_GetBluetoothDevicesRequest>, 130 + serializer: some GRPCCore.MessageSerializer<Rockbox_V1alpha1_GetBluetoothDevicesRequest>, 131 + deserializer: some GRPCCore.MessageDeserializer<Rockbox_V1alpha1_GetBluetoothDevicesResponse>, 132 + options: GRPCCore.CallOptions = .defaults, 133 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Rockbox_V1alpha1_GetBluetoothDevicesResponse>) async throws -> Result = { response in try response.message } 134 + ) async throws -> Result where Result: Sendable { 135 + try await self.client.unary( 136 + request: request, 137 + descriptor: Rockbox_V1alpha1_BluetoothService.Method.GetDevices.descriptor, 138 + serializer: serializer, 139 + deserializer: deserializer, 140 + options: options, 141 + onResponse: handleResponse 142 + ) 143 + } 144 + 145 + internal func connectDevice<Result>( 146 + request: GRPCCore.ClientRequest<Rockbox_V1alpha1_ConnectBluetoothDeviceRequest>, 147 + serializer: some GRPCCore.MessageSerializer<Rockbox_V1alpha1_ConnectBluetoothDeviceRequest>, 148 + deserializer: some GRPCCore.MessageDeserializer<Rockbox_V1alpha1_ConnectBluetoothDeviceResponse>, 149 + options: GRPCCore.CallOptions = .defaults, 150 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Rockbox_V1alpha1_ConnectBluetoothDeviceResponse>) async throws -> Result = { response in try response.message } 151 + ) async throws -> Result where Result: Sendable { 152 + try await self.client.unary( 153 + request: request, 154 + descriptor: Rockbox_V1alpha1_BluetoothService.Method.ConnectDevice.descriptor, 155 + serializer: serializer, 156 + deserializer: deserializer, 157 + options: options, 158 + onResponse: handleResponse 159 + ) 160 + } 161 + 162 + internal func disconnect<Result>( 163 + request: GRPCCore.ClientRequest<Rockbox_V1alpha1_DisconnectBluetoothDeviceRequest>, 164 + serializer: some GRPCCore.MessageSerializer<Rockbox_V1alpha1_DisconnectBluetoothDeviceRequest>, 165 + deserializer: some GRPCCore.MessageDeserializer<Rockbox_V1alpha1_DisconnectBluetoothDeviceResponse>, 166 + options: GRPCCore.CallOptions = .defaults, 167 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Rockbox_V1alpha1_DisconnectBluetoothDeviceResponse>) async throws -> Result = { response in try response.message } 168 + ) async throws -> Result where Result: Sendable { 169 + try await self.client.unary( 170 + request: request, 171 + descriptor: Rockbox_V1alpha1_BluetoothService.Method.Disconnect.descriptor, 172 + serializer: serializer, 173 + deserializer: deserializer, 174 + options: options, 175 + onResponse: handleResponse 176 + ) 177 + } 178 + } 179 + } 180 + 181 + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) 182 + extension Rockbox_V1alpha1_BluetoothService.ClientProtocol { 183 + internal func scan<Result>( 184 + request: GRPCCore.ClientRequest<Rockbox_V1alpha1_ScanBluetoothRequest>, 185 + options: GRPCCore.CallOptions = .defaults, 186 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Rockbox_V1alpha1_ScanBluetoothResponse>) async throws -> Result = { response in try response.message } 187 + ) async throws -> Result where Result: Sendable { 188 + try await self.scan( 189 + request: request, 190 + serializer: GRPCProtobuf.ProtobufSerializer<Rockbox_V1alpha1_ScanBluetoothRequest>(), 191 + deserializer: GRPCProtobuf.ProtobufDeserializer<Rockbox_V1alpha1_ScanBluetoothResponse>(), 192 + options: options, 193 + onResponse: handleResponse 194 + ) 195 + } 196 + 197 + internal func getDevices<Result>( 198 + request: GRPCCore.ClientRequest<Rockbox_V1alpha1_GetBluetoothDevicesRequest>, 199 + options: GRPCCore.CallOptions = .defaults, 200 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Rockbox_V1alpha1_GetBluetoothDevicesResponse>) async throws -> Result = { response in try response.message } 201 + ) async throws -> Result where Result: Sendable { 202 + try await self.getDevices( 203 + request: request, 204 + serializer: GRPCProtobuf.ProtobufSerializer<Rockbox_V1alpha1_GetBluetoothDevicesRequest>(), 205 + deserializer: GRPCProtobuf.ProtobufDeserializer<Rockbox_V1alpha1_GetBluetoothDevicesResponse>(), 206 + options: options, 207 + onResponse: handleResponse 208 + ) 209 + } 210 + 211 + internal func connectDevice<Result>( 212 + request: GRPCCore.ClientRequest<Rockbox_V1alpha1_ConnectBluetoothDeviceRequest>, 213 + options: GRPCCore.CallOptions = .defaults, 214 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Rockbox_V1alpha1_ConnectBluetoothDeviceResponse>) async throws -> Result = { response in try response.message } 215 + ) async throws -> Result where Result: Sendable { 216 + try await self.connectDevice( 217 + request: request, 218 + serializer: GRPCProtobuf.ProtobufSerializer<Rockbox_V1alpha1_ConnectBluetoothDeviceRequest>(), 219 + deserializer: GRPCProtobuf.ProtobufDeserializer<Rockbox_V1alpha1_ConnectBluetoothDeviceResponse>(), 220 + options: options, 221 + onResponse: handleResponse 222 + ) 223 + } 224 + 225 + internal func disconnect<Result>( 226 + request: GRPCCore.ClientRequest<Rockbox_V1alpha1_DisconnectBluetoothDeviceRequest>, 227 + options: GRPCCore.CallOptions = .defaults, 228 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Rockbox_V1alpha1_DisconnectBluetoothDeviceResponse>) async throws -> Result = { response in try response.message } 229 + ) async throws -> Result where Result: Sendable { 230 + try await self.disconnect( 231 + request: request, 232 + serializer: GRPCProtobuf.ProtobufSerializer<Rockbox_V1alpha1_DisconnectBluetoothDeviceRequest>(), 233 + deserializer: GRPCProtobuf.ProtobufDeserializer<Rockbox_V1alpha1_DisconnectBluetoothDeviceResponse>(), 234 + options: options, 235 + onResponse: handleResponse 236 + ) 237 + } 238 + } 239 + 240 + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) 241 + extension Rockbox_V1alpha1_BluetoothService.ClientProtocol { 242 + internal func scan<Result>( 243 + _ message: Rockbox_V1alpha1_ScanBluetoothRequest, 244 + metadata: GRPCCore.Metadata = [:], 245 + options: GRPCCore.CallOptions = .defaults, 246 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Rockbox_V1alpha1_ScanBluetoothResponse>) async throws -> Result = { response in try response.message } 247 + ) async throws -> Result where Result: Sendable { 248 + let request = GRPCCore.ClientRequest<Rockbox_V1alpha1_ScanBluetoothRequest>(message: message, metadata: metadata) 249 + return try await self.scan(request: request, options: options, onResponse: handleResponse) 250 + } 251 + 252 + internal func getDevices<Result>( 253 + _ message: Rockbox_V1alpha1_GetBluetoothDevicesRequest, 254 + metadata: GRPCCore.Metadata = [:], 255 + options: GRPCCore.CallOptions = .defaults, 256 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Rockbox_V1alpha1_GetBluetoothDevicesResponse>) async throws -> Result = { response in try response.message } 257 + ) async throws -> Result where Result: Sendable { 258 + let request = GRPCCore.ClientRequest<Rockbox_V1alpha1_GetBluetoothDevicesRequest>(message: message, metadata: metadata) 259 + return try await self.getDevices(request: request, options: options, onResponse: handleResponse) 260 + } 261 + 262 + internal func connectDevice<Result>( 263 + _ message: Rockbox_V1alpha1_ConnectBluetoothDeviceRequest, 264 + metadata: GRPCCore.Metadata = [:], 265 + options: GRPCCore.CallOptions = .defaults, 266 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Rockbox_V1alpha1_ConnectBluetoothDeviceResponse>) async throws -> Result = { response in try response.message } 267 + ) async throws -> Result where Result: Sendable { 268 + let request = GRPCCore.ClientRequest<Rockbox_V1alpha1_ConnectBluetoothDeviceRequest>(message: message, metadata: metadata) 269 + return try await self.connectDevice(request: request, options: options, onResponse: handleResponse) 270 + } 271 + 272 + internal func disconnect<Result>( 273 + _ message: Rockbox_V1alpha1_DisconnectBluetoothDeviceRequest, 274 + metadata: GRPCCore.Metadata = [:], 275 + options: GRPCCore.CallOptions = .defaults, 276 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Rockbox_V1alpha1_DisconnectBluetoothDeviceResponse>) async throws -> Result = { response in try response.message } 277 + ) async throws -> Result where Result: Sendable { 278 + let request = GRPCCore.ClientRequest<Rockbox_V1alpha1_DisconnectBluetoothDeviceRequest>(message: message, metadata: metadata) 279 + return try await self.disconnect(request: request, options: options, onResponse: handleResponse) 280 + } 281 + }
+344
macos/Rockbox/Services/rockbox/v1alpha1/bluetooth.pb.swift
··· 1 + // DO NOT EDIT. 2 + // swift-format-ignore-file 3 + // swiftlint:disable all 4 + // 5 + // Generated by the Swift generator plugin for the protocol buffer compiler. 6 + // Source: rockbox/v1alpha1/bluetooth.proto 7 + // 8 + // For information on using the generated types, please see the documentation: 9 + // https://github.com/apple/swift-protobuf/ 10 + 11 + import SwiftProtobuf 12 + 13 + fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { 14 + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} 15 + typealias Version = _2 16 + } 17 + 18 + struct Rockbox_V1alpha1_BluetoothDevice: Sendable { 19 + var address: String = String() 20 + var name: String = String() 21 + var paired: Bool = false 22 + var trusted: Bool = false 23 + var connected: Bool = false 24 + 25 + var rssi: Int32 { 26 + get {return _rssi ?? 0} 27 + set {_rssi = newValue} 28 + } 29 + var hasRssi: Bool {return self._rssi != nil} 30 + mutating func clearRssi() {self._rssi = nil} 31 + 32 + var unknownFields = SwiftProtobuf.UnknownStorage() 33 + 34 + init() {} 35 + 36 + fileprivate var _rssi: Int32? = nil 37 + } 38 + 39 + struct Rockbox_V1alpha1_ScanBluetoothRequest: Sendable { 40 + var timeoutSecs: UInt32 = 0 41 + var unknownFields = SwiftProtobuf.UnknownStorage() 42 + init() {} 43 + } 44 + 45 + struct Rockbox_V1alpha1_ScanBluetoothResponse: Sendable { 46 + var devices: [Rockbox_V1alpha1_BluetoothDevice] = [] 47 + var unknownFields = SwiftProtobuf.UnknownStorage() 48 + init() {} 49 + } 50 + 51 + struct Rockbox_V1alpha1_GetBluetoothDevicesRequest: Sendable { 52 + var unknownFields = SwiftProtobuf.UnknownStorage() 53 + init() {} 54 + } 55 + 56 + struct Rockbox_V1alpha1_GetBluetoothDevicesResponse: Sendable { 57 + var devices: [Rockbox_V1alpha1_BluetoothDevice] = [] 58 + var unknownFields = SwiftProtobuf.UnknownStorage() 59 + init() {} 60 + } 61 + 62 + struct Rockbox_V1alpha1_ConnectBluetoothDeviceRequest: Sendable { 63 + var address: String = String() 64 + var unknownFields = SwiftProtobuf.UnknownStorage() 65 + init() {} 66 + } 67 + 68 + struct Rockbox_V1alpha1_ConnectBluetoothDeviceResponse: Sendable { 69 + var unknownFields = SwiftProtobuf.UnknownStorage() 70 + init() {} 71 + } 72 + 73 + struct Rockbox_V1alpha1_DisconnectBluetoothDeviceRequest: Sendable { 74 + var address: String = String() 75 + var unknownFields = SwiftProtobuf.UnknownStorage() 76 + init() {} 77 + } 78 + 79 + struct Rockbox_V1alpha1_DisconnectBluetoothDeviceResponse: Sendable { 80 + var unknownFields = SwiftProtobuf.UnknownStorage() 81 + init() {} 82 + } 83 + 84 + // MARK: - Code below here is support for the SwiftProtobuf runtime. 85 + 86 + fileprivate let _protobuf_package = "rockbox.v1alpha1" 87 + 88 + extension Rockbox_V1alpha1_BluetoothDevice: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 89 + static let protoMessageName: String = _protobuf_package + ".BluetoothDevice" 90 + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 91 + 1: .same(proto: "address"), 92 + 2: .same(proto: "name"), 93 + 3: .same(proto: "paired"), 94 + 4: .same(proto: "trusted"), 95 + 5: .same(proto: "connected"), 96 + 6: .same(proto: "rssi"), 97 + ] 98 + 99 + mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { 100 + while let fieldNumber = try decoder.nextFieldNumber() { 101 + switch fieldNumber { 102 + case 1: try { try decoder.decodeSingularStringField(value: &self.address) }() 103 + case 2: try { try decoder.decodeSingularStringField(value: &self.name) }() 104 + case 3: try { try decoder.decodeSingularBoolField(value: &self.paired) }() 105 + case 4: try { try decoder.decodeSingularBoolField(value: &self.trusted) }() 106 + case 5: try { try decoder.decodeSingularBoolField(value: &self.connected) }() 107 + case 6: try { try decoder.decodeSingularInt32Field(value: &self._rssi) }() 108 + default: break 109 + } 110 + } 111 + } 112 + 113 + func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws { 114 + if !self.address.isEmpty { 115 + try visitor.visitSingularStringField(value: self.address, fieldNumber: 1) 116 + } 117 + if !self.name.isEmpty { 118 + try visitor.visitSingularStringField(value: self.name, fieldNumber: 2) 119 + } 120 + if self.paired != false { 121 + try visitor.visitSingularBoolField(value: self.paired, fieldNumber: 3) 122 + } 123 + if self.trusted != false { 124 + try visitor.visitSingularBoolField(value: self.trusted, fieldNumber: 4) 125 + } 126 + if self.connected != false { 127 + try visitor.visitSingularBoolField(value: self.connected, fieldNumber: 5) 128 + } 129 + try { if let v = self._rssi { 130 + try visitor.visitSingularInt32Field(value: v, fieldNumber: 6) 131 + } }() 132 + try unknownFields.traverse(visitor: &visitor) 133 + } 134 + 135 + static func ==(lhs: Rockbox_V1alpha1_BluetoothDevice, rhs: Rockbox_V1alpha1_BluetoothDevice) -> Bool { 136 + if lhs.address != rhs.address {return false} 137 + if lhs.name != rhs.name {return false} 138 + if lhs.paired != rhs.paired {return false} 139 + if lhs.trusted != rhs.trusted {return false} 140 + if lhs.connected != rhs.connected {return false} 141 + if lhs._rssi != rhs._rssi {return false} 142 + if lhs.unknownFields != rhs.unknownFields {return false} 143 + return true 144 + } 145 + } 146 + 147 + extension Rockbox_V1alpha1_ScanBluetoothRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 148 + static let protoMessageName: String = _protobuf_package + ".ScanBluetoothRequest" 149 + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 150 + 1: .standard(proto: "timeout_secs"), 151 + ] 152 + 153 + mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { 154 + while let fieldNumber = try decoder.nextFieldNumber() { 155 + switch fieldNumber { 156 + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.timeoutSecs) }() 157 + default: break 158 + } 159 + } 160 + } 161 + 162 + func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws { 163 + if self.timeoutSecs != 0 { 164 + try visitor.visitSingularUInt32Field(value: self.timeoutSecs, fieldNumber: 1) 165 + } 166 + try unknownFields.traverse(visitor: &visitor) 167 + } 168 + 169 + static func ==(lhs: Rockbox_V1alpha1_ScanBluetoothRequest, rhs: Rockbox_V1alpha1_ScanBluetoothRequest) -> Bool { 170 + if lhs.timeoutSecs != rhs.timeoutSecs {return false} 171 + if lhs.unknownFields != rhs.unknownFields {return false} 172 + return true 173 + } 174 + } 175 + 176 + extension Rockbox_V1alpha1_ScanBluetoothResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 177 + static let protoMessageName: String = _protobuf_package + ".ScanBluetoothResponse" 178 + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 179 + 1: .same(proto: "devices"), 180 + ] 181 + 182 + mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { 183 + while let fieldNumber = try decoder.nextFieldNumber() { 184 + switch fieldNumber { 185 + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.devices) }() 186 + default: break 187 + } 188 + } 189 + } 190 + 191 + func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws { 192 + if !self.devices.isEmpty { 193 + try visitor.visitRepeatedMessageField(value: self.devices, fieldNumber: 1) 194 + } 195 + try unknownFields.traverse(visitor: &visitor) 196 + } 197 + 198 + static func ==(lhs: Rockbox_V1alpha1_ScanBluetoothResponse, rhs: Rockbox_V1alpha1_ScanBluetoothResponse) -> Bool { 199 + if lhs.devices != rhs.devices {return false} 200 + if lhs.unknownFields != rhs.unknownFields {return false} 201 + return true 202 + } 203 + } 204 + 205 + extension Rockbox_V1alpha1_GetBluetoothDevicesRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 206 + static let protoMessageName: String = _protobuf_package + ".GetBluetoothDevicesRequest" 207 + static let _protobuf_nameMap = SwiftProtobuf._NameMap() 208 + 209 + mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { 210 + while try decoder.nextFieldNumber() != nil {} 211 + } 212 + 213 + func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws { 214 + try unknownFields.traverse(visitor: &visitor) 215 + } 216 + 217 + static func ==(lhs: Rockbox_V1alpha1_GetBluetoothDevicesRequest, rhs: Rockbox_V1alpha1_GetBluetoothDevicesRequest) -> Bool { 218 + if lhs.unknownFields != rhs.unknownFields {return false} 219 + return true 220 + } 221 + } 222 + 223 + extension Rockbox_V1alpha1_GetBluetoothDevicesResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 224 + static let protoMessageName: String = _protobuf_package + ".GetBluetoothDevicesResponse" 225 + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 226 + 1: .same(proto: "devices"), 227 + ] 228 + 229 + mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { 230 + while let fieldNumber = try decoder.nextFieldNumber() { 231 + switch fieldNumber { 232 + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.devices) }() 233 + default: break 234 + } 235 + } 236 + } 237 + 238 + func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws { 239 + if !self.devices.isEmpty { 240 + try visitor.visitRepeatedMessageField(value: self.devices, fieldNumber: 1) 241 + } 242 + try unknownFields.traverse(visitor: &visitor) 243 + } 244 + 245 + static func ==(lhs: Rockbox_V1alpha1_GetBluetoothDevicesResponse, rhs: Rockbox_V1alpha1_GetBluetoothDevicesResponse) -> Bool { 246 + if lhs.devices != rhs.devices {return false} 247 + if lhs.unknownFields != rhs.unknownFields {return false} 248 + return true 249 + } 250 + } 251 + 252 + extension Rockbox_V1alpha1_ConnectBluetoothDeviceRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 253 + static let protoMessageName: String = _protobuf_package + ".ConnectBluetoothDeviceRequest" 254 + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 255 + 1: .same(proto: "address"), 256 + ] 257 + 258 + mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { 259 + while let fieldNumber = try decoder.nextFieldNumber() { 260 + switch fieldNumber { 261 + case 1: try { try decoder.decodeSingularStringField(value: &self.address) }() 262 + default: break 263 + } 264 + } 265 + } 266 + 267 + func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws { 268 + if !self.address.isEmpty { 269 + try visitor.visitSingularStringField(value: self.address, fieldNumber: 1) 270 + } 271 + try unknownFields.traverse(visitor: &visitor) 272 + } 273 + 274 + static func ==(lhs: Rockbox_V1alpha1_ConnectBluetoothDeviceRequest, rhs: Rockbox_V1alpha1_ConnectBluetoothDeviceRequest) -> Bool { 275 + if lhs.address != rhs.address {return false} 276 + if lhs.unknownFields != rhs.unknownFields {return false} 277 + return true 278 + } 279 + } 280 + 281 + extension Rockbox_V1alpha1_ConnectBluetoothDeviceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 282 + static let protoMessageName: String = _protobuf_package + ".ConnectBluetoothDeviceResponse" 283 + static let _protobuf_nameMap = SwiftProtobuf._NameMap() 284 + 285 + mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { 286 + while try decoder.nextFieldNumber() != nil {} 287 + } 288 + 289 + func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws { 290 + try unknownFields.traverse(visitor: &visitor) 291 + } 292 + 293 + static func ==(lhs: Rockbox_V1alpha1_ConnectBluetoothDeviceResponse, rhs: Rockbox_V1alpha1_ConnectBluetoothDeviceResponse) -> Bool { 294 + if lhs.unknownFields != rhs.unknownFields {return false} 295 + return true 296 + } 297 + } 298 + 299 + extension Rockbox_V1alpha1_DisconnectBluetoothDeviceRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 300 + static let protoMessageName: String = _protobuf_package + ".DisconnectBluetoothDeviceRequest" 301 + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 302 + 1: .same(proto: "address"), 303 + ] 304 + 305 + mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { 306 + while let fieldNumber = try decoder.nextFieldNumber() { 307 + switch fieldNumber { 308 + case 1: try { try decoder.decodeSingularStringField(value: &self.address) }() 309 + default: break 310 + } 311 + } 312 + } 313 + 314 + func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws { 315 + if !self.address.isEmpty { 316 + try visitor.visitSingularStringField(value: self.address, fieldNumber: 1) 317 + } 318 + try unknownFields.traverse(visitor: &visitor) 319 + } 320 + 321 + static func ==(lhs: Rockbox_V1alpha1_DisconnectBluetoothDeviceRequest, rhs: Rockbox_V1alpha1_DisconnectBluetoothDeviceRequest) -> Bool { 322 + if lhs.address != rhs.address {return false} 323 + if lhs.unknownFields != rhs.unknownFields {return false} 324 + return true 325 + } 326 + } 327 + 328 + extension Rockbox_V1alpha1_DisconnectBluetoothDeviceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 329 + static let protoMessageName: String = _protobuf_package + ".DisconnectBluetoothDeviceResponse" 330 + static let _protobuf_nameMap = SwiftProtobuf._NameMap() 331 + 332 + mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { 333 + while try decoder.nextFieldNumber() != nil {} 334 + } 335 + 336 + func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws { 337 + try unknownFields.traverse(visitor: &visitor) 338 + } 339 + 340 + static func ==(lhs: Rockbox_V1alpha1_DisconnectBluetoothDeviceResponse, rhs: Rockbox_V1alpha1_DisconnectBluetoothDeviceResponse) -> Bool { 341 + if lhs.unknownFields != rhs.unknownFields {return false} 342 + return true 343 + } 344 + }
+66
macos/Rockbox/State/BluetoothState.swift
··· 1 + // 2 + // BluetoothState.swift 3 + // Rockbox 4 + // 5 + 6 + import Foundation 7 + import SwiftUI 8 + 9 + struct BluetoothDeviceInfo: Identifiable, Hashable { 10 + var id: String { address } 11 + let address: String 12 + let name: String 13 + let paired: Bool 14 + let trusted: Bool 15 + var connected: Bool 16 + } 17 + 18 + @available(macOS 15.0, *) 19 + @MainActor 20 + class BluetoothState: ObservableObject { 21 + @Published var devices: [BluetoothDeviceInfo] = [] 22 + @Published var isLoading = false 23 + @Published var available = false 24 + 25 + func checkAvailability() async { 26 + available = await checkBluetoothAvailable() 27 + } 28 + 29 + func refresh() async { 30 + guard available else { return } 31 + if devices.isEmpty { isLoading = true } 32 + do { 33 + let raw = try await fetchBluetoothDevices() 34 + devices = raw.map { 35 + BluetoothDeviceInfo( 36 + address: $0.address, 37 + name: $0.name.isEmpty ? $0.address : $0.name, 38 + paired: $0.paired, 39 + trusted: $0.trusted, 40 + connected: $0.connected 41 + ) 42 + } 43 + } catch {} 44 + isLoading = false 45 + } 46 + 47 + func connect(_ device: BluetoothDeviceInfo) async { 48 + do { 49 + try await connectBluetoothDevice(address: device.address) 50 + for i in devices.indices { 51 + devices[i].connected = devices[i].address == device.address 52 + } 53 + } catch {} 54 + } 55 + 56 + func disconnect(_ device: BluetoothDeviceInfo) async { 57 + do { 58 + try await disconnectBluetoothDevice(address: device.address) 59 + for i in devices.indices { 60 + if devices[i].address == device.address { 61 + devices[i].connected = false 62 + } 63 + } 64 + } catch {} 65 + } 66 + }
+106
macos/Rockbox/Views/Components/BluetoothListView.swift
··· 1 + // 2 + // BluetoothListView.swift 3 + // Rockbox 4 + // 5 + 6 + import SwiftUI 7 + 8 + @available(macOS 15.0, *) 9 + struct BluetoothListView: View { 10 + @EnvironmentObject var bluetoothState: BluetoothState 11 + @Environment(\.dismiss) private var dismiss 12 + 13 + var body: some View { 14 + VStack(alignment: .leading, spacing: 0) { 15 + Text("Bluetooth speakers") 16 + .font(.system(size: 11, weight: .semibold)) 17 + .foregroundStyle(.secondary) 18 + .padding(.horizontal, 16) 19 + .padding(.top, 14) 20 + .padding(.bottom, 8) 21 + 22 + Divider() 23 + .padding(.horizontal, 8) 24 + 25 + if bluetoothState.isLoading { 26 + HStack { 27 + Spacer() 28 + ProgressView() 29 + .padding() 30 + Spacer() 31 + } 32 + } else if bluetoothState.devices.isEmpty { 33 + Text("No bluetooth devices found.") 34 + .font(.system(size: 12)) 35 + .foregroundStyle(.secondary) 36 + .frame(maxWidth: .infinity, alignment: .center) 37 + .padding() 38 + } else { 39 + ScrollView { 40 + VStack(spacing: 2) { 41 + ForEach(bluetoothState.devices) { device in 42 + BluetoothDeviceRow(device: device) { 43 + Task { 44 + if device.connected { 45 + await bluetoothState.disconnect(device) 46 + } else { 47 + await bluetoothState.connect(device) 48 + } 49 + dismiss() 50 + } 51 + } 52 + } 53 + } 54 + .padding(.vertical, 6) 55 + .padding(.horizontal, 8) 56 + } 57 + .frame(maxHeight: 280) 58 + } 59 + } 60 + .frame(width: 280) 61 + .task { await bluetoothState.refresh() } 62 + } 63 + } 64 + 65 + @available(macOS 15.0, *) 66 + private struct BluetoothDeviceRow: View { 67 + let device: BluetoothDeviceInfo 68 + let onTap: () -> Void 69 + 70 + @State private var isHovering = false 71 + 72 + var body: some View { 73 + Button(action: onTap) { 74 + HStack(spacing: 10) { 75 + ZStack { 76 + RoundedRectangle(cornerRadius: 6) 77 + .fill(Color(hex: "1a91ff").opacity(0.12)) 78 + .frame(width: 30, height: 30) 79 + Image(systemName: "bluetooth") 80 + .font(.system(size: 14)) 81 + .foregroundStyle(Color(hex: "1a91ff")) 82 + } 83 + 84 + Text(device.name) 85 + .font(.system(size: 13)) 86 + .lineLimit(1) 87 + .frame(maxWidth: .infinity, alignment: .leading) 88 + 89 + if device.connected { 90 + Image(systemName: "checkmark") 91 + .font(.system(size: 11, weight: .semibold)) 92 + .foregroundStyle(Color(hex: "28fce3")) 93 + } 94 + } 95 + .padding(.horizontal, 8) 96 + .padding(.vertical, 6) 97 + .background( 98 + RoundedRectangle(cornerRadius: 6) 99 + .fill(isHovering ? Color.secondary.opacity(0.1) : Color.clear) 100 + ) 101 + .contentShape(RoundedRectangle(cornerRadius: 6)) 102 + } 103 + .buttonStyle(.plain) 104 + .onHover { isHovering = $0 } 105 + } 106 + }
+28
macos/Rockbox/Views/Components/PlayerControlsView.swift
··· 11 11 @EnvironmentObject var player: PlayerState 12 12 @EnvironmentObject var navigation: NavigationManager 13 13 @EnvironmentObject var deviceState: DeviceState 14 + @EnvironmentObject var bluetoothState: BluetoothState 14 15 @State private var isHoveringProgress = false 15 16 @State private var isHoveringTrackInfo = false 16 17 @State private var isHoveringQueue = false 17 18 @State private var isHoveringDevice = false 19 + @State private var isHoveringBluetooth = false 18 20 @State private var isHoveringMenu = false 19 21 @State private var isHoveringShuffle = false 20 22 @State private var isHoveringRepeat = false 21 23 @State private var showDevicePicker = false 24 + @State private var showBluetoothPicker = false 22 25 @State private var errorText: String? = nil 23 26 @ObservedObject var library: MusicLibrary 24 27 @Binding var showQueue: Bool ··· 235 238 } 236 239 237 240 HStack(spacing: 6) { 241 + // Bluetooth picker button — only shown when bluetooth is available 242 + if bluetoothState.available { 243 + Button(action: { showBluetoothPicker.toggle() }) { 244 + Image(systemName: "bluetooth") 245 + .font(.system(size: 14)) 246 + .foregroundStyle(showBluetoothPicker ? Color(hex: "1a91ff") : (isHoveringBluetooth ? .primary : .secondary)) 247 + .frame(width: 32, height: 32) 248 + .background( 249 + RoundedRectangle(cornerRadius: 6) 250 + .fill(isHoveringBluetooth || showBluetoothPicker ? Color.secondary.opacity(0.15) : Color.clear) 251 + ) 252 + .contentShape(Rectangle()) 253 + } 254 + .buttonStyle(.plain) 255 + .onHover { hovering in 256 + withAnimation(.easeInOut(duration: 0.1)) { 257 + isHoveringBluetooth = hovering 258 + } 259 + } 260 + .popover(isPresented: $showBluetoothPicker, arrowEdge: .top) { 261 + BluetoothListView() 262 + .environmentObject(bluetoothState) 263 + } 264 + } 265 + 238 266 // Device picker button 239 267 Button(action: { showDevicePicker.toggle() }) { 240 268 let symbol = deviceState.currentDevice.map { d in
+16
webui/rockbox/src/Components/AlbumDetails/__snapshots__/AlbumDetails.test.tsx.snap
··· 403 403 style="cursor: pointer;" 404 404 > 405 405 <svg 406 + fill="#9090a0" 407 + height="18" 408 + viewBox="0 0 24 24" 409 + width="18" 410 + > 411 + <path 412 + d="M17.71 7.71L12 2h-1v7.59L6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 11 14.41V22h1l5.71-5.71-4.3-4.29 4.3-4.29zM13 5.83l1.88 1.88L13 9.59V5.83zm1.88 10.46L13 18.17v-3.76l1.88 1.88z" 413 + /> 414 + </svg> 415 + </button> 416 + <button 417 + aria-expanded="false" 418 + aria-haspopup="true" 419 + style="cursor: pointer;" 420 + > 421 + <svg 406 422 aria-hidden="true" 407 423 class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 408 424 color="#9090a0"
+16
webui/rockbox/src/Components/Albums/__snapshots__/Albums.test.tsx.snap
··· 403 403 style="cursor: pointer;" 404 404 > 405 405 <svg 406 + fill="#9090a0" 407 + height="18" 408 + viewBox="0 0 24 24" 409 + width="18" 410 + > 411 + <path 412 + d="M17.71 7.71L12 2h-1v7.59L6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 11 14.41V22h1l5.71-5.71-4.3-4.29 4.3-4.29zM13 5.83l1.88 1.88L13 9.59V5.83zm1.88 10.46L13 18.17v-3.76l1.88 1.88z" 413 + /> 414 + </svg> 415 + </button> 416 + <button 417 + aria-expanded="false" 418 + aria-haspopup="true" 419 + style="cursor: pointer;" 420 + > 421 + <svg 406 422 aria-hidden="true" 407 423 class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 408 424 color="#9090a0"
+16
webui/rockbox/src/Components/ArtistDetails/__snapshots__/ArtistDetails.test.tsx.snap
··· 400 400 style="cursor: pointer;" 401 401 > 402 402 <svg 403 + fill="#9090a0" 404 + height="18" 405 + viewBox="0 0 24 24" 406 + width="18" 407 + > 408 + <path 409 + d="M17.71 7.71L12 2h-1v7.59L6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 11 14.41V22h1l5.71-5.71-4.3-4.29 4.3-4.29zM13 5.83l1.88 1.88L13 9.59V5.83zm1.88 10.46L13 18.17v-3.76l1.88 1.88z" 410 + /> 411 + </svg> 412 + </button> 413 + <button 414 + aria-expanded="false" 415 + aria-haspopup="true" 416 + style="cursor: pointer;" 417 + > 418 + <svg 403 419 aria-hidden="true" 404 420 class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 405 421 color="#9090a0"
+16
webui/rockbox/src/Components/Artists/__snapshots__/Artists.test.tsx.snap
··· 403 403 style="cursor: pointer;" 404 404 > 405 405 <svg 406 + fill="#9090a0" 407 + height="18" 408 + viewBox="0 0 24 24" 409 + width="18" 410 + > 411 + <path 412 + d="M17.71 7.71L12 2h-1v7.59L6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 11 14.41V22h1l5.71-5.71-4.3-4.29 4.3-4.29zM13 5.83l1.88 1.88L13 9.59V5.83zm1.88 10.46L13 18.17v-3.76l1.88 1.88z" 413 + /> 414 + </svg> 415 + </button> 416 + <button 417 + aria-expanded="false" 418 + aria-haspopup="true" 419 + style="cursor: pointer;" 420 + > 421 + <svg 406 422 aria-hidden="true" 407 423 class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 408 424 color="#9090a0"
+107
webui/rockbox/src/Components/ControlBar/BluetoothList/BluetoothList.tsx
··· 1 + import { useTheme } from "@emotion/react"; 2 + import styled from "@emotion/styled"; 3 + import { ListItem, ListItemLabel } from "baseui/list"; 4 + import { FC } from "react"; 5 + import { BluetoothDeviceGql } from "../../../Hooks/GraphQL"; 6 + import { Container, List, Placeholder, Title } from "./styles"; 7 + 8 + const ACCENT = "#1a91ff"; 9 + 10 + const DeviceNameText = styled.div` 11 + font-size: 13px; 12 + `; 13 + 14 + const CheckMark = styled.span` 15 + color: #28fce3; 16 + font-size: 12px; 17 + margin-left: 6px; 18 + `; 19 + 20 + const BluetoothIcon: FC = () => ( 21 + <div 22 + style={{ 23 + width: 30, 24 + height: 30, 25 + borderRadius: 6, 26 + background: "rgba(26,145,255,0.12)", 27 + display: "flex", 28 + alignItems: "center", 29 + justifyContent: "center", 30 + }} 31 + > 32 + <svg viewBox="0 0 24 24" width={14} height={14} fill={ACCENT}> 33 + <path d="M17.71 7.71L12 2h-1v7.59L6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 11 14.41V22h1l5.71-5.71-4.3-4.29 4.3-4.29zM13 5.83l1.88 1.88L13 9.59V5.83zm1.88 10.46L13 18.17v-3.76l1.88 1.88z" /> 34 + </svg> 35 + </div> 36 + ); 37 + 38 + export type BluetoothListProps = { 39 + devices: BluetoothDeviceGql[]; 40 + loading: boolean; 41 + connect: (address: string) => void; 42 + disconnect: (address: string) => void; 43 + close: () => void; 44 + }; 45 + 46 + const BluetoothList: FC<BluetoothListProps> = ({ 47 + devices, 48 + loading, 49 + connect, 50 + disconnect, 51 + close, 52 + }) => { 53 + const theme = useTheme(); 54 + 55 + const handleTap = (device: BluetoothDeviceGql) => { 56 + if (device.connected) { 57 + disconnect(device.address); 58 + } else { 59 + connect(device.address); 60 + } 61 + close(); 62 + }; 63 + 64 + return ( 65 + <Container> 66 + {devices.length > 0 && !loading && ( 67 + <Title>Bluetooth speakers</Title> 68 + )} 69 + <List> 70 + {!loading && devices.length === 0 && ( 71 + <Placeholder>No bluetooth devices found.</Placeholder> 72 + )} 73 + {devices.map((device) => ( 74 + <div key={device.address} onClick={() => handleTap(device)}> 75 + <ListItem 76 + artwork={() => <BluetoothIcon />} 77 + endEnhancer={() => 78 + device.connected ? <CheckMark>✓</CheckMark> : null 79 + } 80 + overrides={{ 81 + Root: { 82 + style: { 83 + cursor: "pointer", 84 + backgroundColor: "transparent", 85 + ":hover": { 86 + backgroundColor: theme.colors.hover, 87 + }, 88 + borderRadius: "5px", 89 + }, 90 + }, 91 + Content: { 92 + style: { borderBottom: "none" }, 93 + }, 94 + }} 95 + > 96 + <ListItemLabel> 97 + <DeviceNameText>{device.name || device.address}</DeviceNameText> 98 + </ListItemLabel> 99 + </ListItem> 100 + </div> 101 + ))} 102 + </List> 103 + </Container> 104 + ); 105 + }; 106 + 107 + export default BluetoothList;
+43
webui/rockbox/src/Components/ControlBar/BluetoothList/BluetoothListWithData.tsx
··· 1 + import { FC } from "react"; 2 + import BluetoothList from "./BluetoothList"; 3 + import { 4 + useGetBluetoothDevicesQuery, 5 + useBluetoothConnectMutation, 6 + useBluetoothDisconnectMutation, 7 + } from "../../../Hooks/GraphQL"; 8 + 9 + export type BluetoothListWithDataProps = { 10 + close: () => void; 11 + }; 12 + 13 + const BluetoothListWithData: FC<BluetoothListWithDataProps> = ({ close }) => { 14 + const { data, isLoading, refetch } = useGetBluetoothDevicesQuery({ 15 + retry: false, 16 + }); 17 + const { mutateAsync: connectAsync } = useBluetoothConnectMutation(); 18 + const { mutateAsync: disconnectAsync } = useBluetoothDisconnectMutation(); 19 + 20 + const devices = data?.bluetoothDevices ?? []; 21 + 22 + const connect = async (address: string) => { 23 + await connectAsync({ address }); 24 + await refetch(); 25 + }; 26 + 27 + const disconnect = async (address: string) => { 28 + await disconnectAsync({ address }); 29 + await refetch(); 30 + }; 31 + 32 + return ( 33 + <BluetoothList 34 + devices={devices} 35 + loading={isLoading} 36 + connect={connect} 37 + disconnect={disconnect} 38 + close={close} 39 + /> 40 + ); 41 + }; 42 + 43 + export default BluetoothListWithData;
+3
webui/rockbox/src/Components/ControlBar/BluetoothList/index.tsx
··· 1 + import BluetoothListWithData from "./BluetoothListWithData"; 2 + 3 + export default BluetoothListWithData;
+36
webui/rockbox/src/Components/ControlBar/BluetoothList/styles.ts
··· 1 + import styled from "@emotion/styled"; 2 + 3 + export const Container = styled.div` 4 + max-height: calc(100vh - 153px); 5 + padding-top: 15px; 6 + padding-bottom: 15px; 7 + overflow-y: auto; 8 + width: 280px; 9 + min-height: 120px; 10 + `; 11 + 12 + export const List = styled.div` 13 + max-height: calc(100vh - 273px); 14 + padding-left: 15px; 15 + padding-right: 15px; 16 + overflow-y: auto; 17 + min-height: 80px; 18 + `; 19 + 20 + export const Title = styled.div` 21 + margin: 10px; 22 + margin-left: 25px; 23 + margin-right: 25px; 24 + font-family: "RockfordSansBold"; 25 + `; 26 + 27 + export const Placeholder = styled.div` 28 + display: flex; 29 + align-items: center; 30 + justify-content: center; 31 + height: 120px; 32 + text-align: center; 33 + padding-left: 20px; 34 + padding-right: 20px; 35 + font-size: 14px; 36 + `;
+42
webui/rockbox/src/Components/ControlBar/RightMenu/RightMenu.tsx
··· 9 9 import _ from "lodash"; 10 10 import { Speaker } from "@styled-icons/bootstrap"; 11 11 import DeviceList from "../DeviceList"; 12 + import BluetoothList from "../BluetoothList"; 13 + import { useGetBluetoothDevicesQuery } from "../../../Hooks/GraphQL"; 14 + 15 + const BluetoothIcon: FC<{ color: string }> = ({ color }) => ( 16 + <svg viewBox="0 0 24 24" width={18} height={18} fill={color}> 17 + <path d="M17.71 7.71L12 2h-1v7.59L6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 11 14.41V22h1l5.71-5.71-4.3-4.29 4.3-4.29zM13 5.83l1.88 1.88L13 9.59V5.83zm1.88 10.46L13 18.17v-3.76l1.88 1.88z" /> 18 + </svg> 19 + ); 12 20 13 21 const RightMenu: FC = () => { 14 22 const theme = useTheme(); 23 + const { isError: bluetoothUnavailable } = useGetBluetoothDevicesQuery({ 24 + retry: false, 25 + }); 26 + const bluetoothAvailable = !bluetoothUnavailable; 15 27 16 28 return ( 17 29 <Container> 18 30 <Volume /> 31 + {bluetoothAvailable && ( 32 + <StatefulPopover 33 + placement="bottom" 34 + content={({ close }) => <BluetoothList close={close} />} 35 + overrides={{ 36 + Body: { 37 + style: { 38 + top: "10px", 39 + left: "-40px", 40 + zIndex: 100, 41 + }, 42 + }, 43 + Inner: { 44 + style: { 45 + backgroundColor: theme.colors.popoverBackground, 46 + }, 47 + }, 48 + }} 49 + > 50 + <button 51 + style={{ 52 + border: "none", 53 + backgroundColor: "initial", 54 + cursor: "pointer", 55 + }} 56 + > 57 + <BluetoothIcon color={theme.colors.icon} /> 58 + </button> 59 + </StatefulPopover> 60 + )} 19 61 <StatefulPopover 20 62 placement="bottom" 21 63 content={({ close }) => <DeviceList close={close} />}
+16
webui/rockbox/src/Components/ControlBar/__snapshots__/ControlBar.test.tsx.snap
··· 269 269 style="cursor: pointer;" 270 270 > 271 271 <svg 272 + fill="#9090a0" 273 + height="18" 274 + viewBox="0 0 24 24" 275 + width="18" 276 + > 277 + <path 278 + d="M17.71 7.71L12 2h-1v7.59L6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 11 14.41V22h1l5.71-5.71-4.3-4.29 4.3-4.29zM13 5.83l1.88 1.88L13 9.59V5.83zm1.88 10.46L13 18.17v-3.76l1.88 1.88z" 279 + /> 280 + </svg> 281 + </button> 282 + <button 283 + aria-expanded="false" 284 + aria-haspopup="true" 285 + style="cursor: pointer;" 286 + > 287 + <svg 272 288 aria-hidden="true" 273 289 class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 274 290 color="#9090a0"
+16
webui/rockbox/src/Components/Files/__snapshots__/Files.test.tsx.snap
··· 403 403 style="cursor: pointer;" 404 404 > 405 405 <svg 406 + fill="#9090a0" 407 + height="18" 408 + viewBox="0 0 24 24" 409 + width="18" 410 + > 411 + <path 412 + d="M17.71 7.71L12 2h-1v7.59L6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 11 14.41V22h1l5.71-5.71-4.3-4.29 4.3-4.29zM13 5.83l1.88 1.88L13 9.59V5.83zm1.88 10.46L13 18.17v-3.76l1.88 1.88z" 413 + /> 414 + </svg> 415 + </button> 416 + <button 417 + aria-expanded="false" 418 + aria-haspopup="true" 419 + style="cursor: pointer;" 420 + > 421 + <svg 406 422 aria-hidden="true" 407 423 class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 408 424 color="#9090a0"
+16
webui/rockbox/src/Components/Tracks/__snapshots__/Tracks.test.tsx.snap
··· 403 403 style="cursor: pointer;" 404 404 > 405 405 <svg 406 + fill="#9090a0" 407 + height="18" 408 + viewBox="0 0 24 24" 409 + width="18" 410 + > 411 + <path 412 + d="M17.71 7.71L12 2h-1v7.59L6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 11 14.41V22h1l5.71-5.71-4.3-4.29 4.3-4.29zM13 5.83l1.88 1.88L13 9.59V5.83zm1.88 10.46L13 18.17v-3.76l1.88 1.88z" 413 + /> 414 + </svg> 415 + </button> 416 + <button 417 + aria-expanded="false" 418 + aria-haspopup="true" 419 + style="cursor: pointer;" 420 + > 421 + <svg 406 422 aria-hidden="true" 407 423 class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 408 424 color="#9090a0"
+84
webui/rockbox/src/Hooks/GraphQL.tsx
··· 3076 3076 useSubscription<PlaylistChangedSubscription>( 3077 3077 PlaylistChangedDocument.toString() 3078 3078 ); 3079 + 3080 + // ── Bluetooth ───────────────────────────────────────────────────────────────── 3081 + 3082 + export type BluetoothDeviceGql = { 3083 + address: string; 3084 + name: string; 3085 + paired: boolean; 3086 + trusted: boolean; 3087 + connected: boolean; 3088 + rssi?: Maybe<number>; 3089 + }; 3090 + 3091 + export type GetBluetoothDevicesQuery = { bluetoothDevices: Array<BluetoothDeviceGql> }; 3092 + export type BluetoothConnectMutation = { bluetoothConnect: boolean }; 3093 + export type BluetoothConnectMutationVariables = { address: string }; 3094 + export type BluetoothDisconnectMutation = { bluetoothDisconnect: boolean }; 3095 + export type BluetoothDisconnectMutationVariables = { address: string }; 3096 + 3097 + export const GetBluetoothDevicesDocument = new TypedDocumentString(` 3098 + query GetBluetoothDevices { 3099 + bluetoothDevices { 3100 + address 3101 + name 3102 + paired 3103 + trusted 3104 + connected 3105 + rssi 3106 + } 3107 + } 3108 + `); 3109 + 3110 + export const BluetoothConnectDocument = new TypedDocumentString(` 3111 + mutation BluetoothConnect($address: String!) { 3112 + bluetoothConnect(address: $address) 3113 + } 3114 + `); 3115 + 3116 + export const BluetoothDisconnectDocument = new TypedDocumentString(` 3117 + mutation BluetoothDisconnect($address: String!) { 3118 + bluetoothDisconnect(address: $address) 3119 + } 3120 + `); 3121 + 3122 + export const useGetBluetoothDevicesQuery = < 3123 + TData = GetBluetoothDevicesQuery, 3124 + TError = unknown 3125 + >( 3126 + options?: Omit<UseQueryOptions<GetBluetoothDevicesQuery, TError, TData>, 'queryKey'> & 3127 + { queryKey?: UseQueryOptions<GetBluetoothDevicesQuery, TError, TData>['queryKey'] } 3128 + ) => { 3129 + return useQuery<GetBluetoothDevicesQuery, TError, TData>({ 3130 + queryKey: ['GetBluetoothDevices'], 3131 + queryFn: fetchData<GetBluetoothDevicesQuery, Record<string, never>>(GetBluetoothDevicesDocument), 3132 + retry: false, 3133 + ...options, 3134 + }); 3135 + }; 3136 + 3137 + useGetBluetoothDevicesQuery.document = GetBluetoothDevicesDocument; 3138 + useGetBluetoothDevicesQuery.getKey = () => ['GetBluetoothDevices']; 3139 + 3140 + export const useBluetoothConnectMutation = < 3141 + TError = unknown, 3142 + TContext = unknown 3143 + >(options?: UseMutationOptions<BluetoothConnectMutation, TError, BluetoothConnectMutationVariables, TContext>) => { 3144 + return useMutation<BluetoothConnectMutation, TError, BluetoothConnectMutationVariables, TContext>({ 3145 + mutationKey: ['BluetoothConnect'], 3146 + mutationFn: (variables: BluetoothConnectMutationVariables) => 3147 + fetchData<BluetoothConnectMutation, BluetoothConnectMutationVariables>(BluetoothConnectDocument, variables)(), 3148 + ...options, 3149 + }); 3150 + }; 3151 + 3152 + export const useBluetoothDisconnectMutation = < 3153 + TError = unknown, 3154 + TContext = unknown 3155 + >(options?: UseMutationOptions<BluetoothDisconnectMutation, TError, BluetoothDisconnectMutationVariables, TContext>) => { 3156 + return useMutation<BluetoothDisconnectMutation, TError, BluetoothDisconnectMutationVariables, TContext>({ 3157 + mutationKey: ['BluetoothDisconnect'], 3158 + mutationFn: (variables: BluetoothDisconnectMutationVariables) => 3159 + fetchData<BluetoothDisconnectMutation, BluetoothDisconnectMutationVariables>(BluetoothDisconnectDocument, variables)(), 3160 + ...options, 3161 + }); 3162 + };
+1 -1
webui/rockbox/tsconfig.app.tsbuildinfo
··· 1 - {"root":["./src/app.tsx","./src/theme.ts","./src/constants.ts","./src/emotion.d.ts","./src/main.tsx","./src/mocks.ts","./src/vite-env.d.ts","./src/components/album/album.stories.tsx","./src/components/album/album.test.tsx","./src/components/album/album.tsx","./src/components/album/albumwithdata.tsx","./src/components/album/index.tsx","./src/components/album/styles.tsx","./src/components/album/contextmenu/childmenu.tsx","./src/components/album/contextmenu/contextmenu.stories.tsx","./src/components/album/contextmenu/contextmenu.test.tsx","./src/components/album/contextmenu/contextmenu.tsx","./src/components/album/contextmenu/contextmenuwithdata.tsx","./src/components/album/contextmenu/index.tsx","./src/components/album/contextmenu/styles.tsx","./src/components/albumdetails/albumdetails.stories.tsx","./src/components/albumdetails/albumdetails.test.tsx","./src/components/albumdetails/albumdetails.tsx","./src/components/albumdetails/albumdetailswithdata.tsx","./src/components/albumdetails/index.tsx","./src/components/albumdetails/mocks.tsx","./src/components/albumdetails/styles.tsx","./src/components/albums/albums.stories.tsx","./src/components/albums/albums.test.tsx","./src/components/albums/albums.tsx","./src/components/albums/albumswithdata.tsx","./src/components/albums/index.tsx","./src/components/albums/mocks.tsx","./src/components/albums/styles.tsx","./src/components/artistdetails/artistdetails.stories.tsx","./src/components/artistdetails/artistdetails.test.tsx","./src/components/artistdetails/artistdetails.tsx","./src/components/artistdetails/artistdetailswithdata.tsx","./src/components/artistdetails/index.tsx","./src/components/artistdetails/mocks.tsx","./src/components/artistdetails/styles.tsx","./src/components/artists/artists.stories.tsx","./src/components/artists/artists.test.tsx","./src/components/artists/artists.tsx","./src/components/artists/artistswithdata.tsx","./src/components/artists/index.tsx","./src/components/artists/mocks.tsx","./src/components/artists/styles.tsx","./src/components/button/button.test.tsx","./src/components/button/button.tsx","./src/components/button/index.tsx","./src/components/contextmenu/childmenu.tsx","./src/components/contextmenu/contextmenu.stories.tsx","./src/components/contextmenu/contextmenu.test.tsx","./src/components/contextmenu/contextmenu.tsx","./src/components/contextmenu/contextmenuwithdata.tsx","./src/components/contextmenu/index.tsx","./src/components/contextmenu/styles.tsx","./src/components/controlbar/controlbar.stories.tsx","./src/components/controlbar/controlbar.test.tsx","./src/components/controlbar/controlbar.tsx","./src/components/controlbar/controlbarstate.tsx","./src/components/controlbar/controlbarwithdata.tsx","./src/components/controlbar/index.tsx","./src/components/controlbar/styles.tsx","./src/components/controlbar/currenttrack/currenttrack.tsx","./src/components/controlbar/currenttrack/index.tsx","./src/components/controlbar/currenttrack/styles.ts","./src/components/controlbar/devicelist/devicelist.tsx","./src/components/controlbar/devicelist/devicelistwithdata.tsx","./src/components/controlbar/devicelist/devicestate.tsx","./src/components/controlbar/devicelist/index.tsx","./src/components/controlbar/devicelist/styles.ts","./src/components/controlbar/playqueue/playqueue.stories.tsx","./src/components/controlbar/playqueue/playqueue.test.tsx","./src/components/controlbar/playqueue/playqueue.tsx","./src/components/controlbar/playqueue/playqueuewithdata.tsx","./src/components/controlbar/playqueue/index.tsx","./src/components/controlbar/playqueue/mocks.tsx","./src/components/controlbar/playqueue/styles.tsx","./src/components/controlbar/rightmenu/rightmenu.tsx","./src/components/controlbar/rightmenu/index.tsx","./src/components/controlbar/rightmenu/styles.tsx","./src/components/controlbar/rightmenu/volume/volume.stories.tsx","./src/components/controlbar/rightmenu/volume/volume.test.tsx","./src/components/controlbar/rightmenu/volume/volume.tsx","./src/components/controlbar/rightmenu/volume/volumewithdata.tsx","./src/components/controlbar/rightmenu/volume/index.tsx","./src/components/controlbar/rightmenu/volume/styles.tsx","./src/components/extensions/extensions.stories.tsx","./src/components/extensions/extensions.tsx","./src/components/extensions/index.tsx","./src/components/files/files.stories.tsx","./src/components/files/files.test.tsx","./src/components/files/files.tsx","./src/components/files/fileswithdata.tsx","./src/components/files/index.tsx","./src/components/files/mocks.tsx","./src/components/files/styles.tsx","./src/components/files/contextmenu/childmenu.tsx","./src/components/files/contextmenu/contextmenu.stories.tsx","./src/components/files/contextmenu/contextmenu.test.tsx","./src/components/files/contextmenu/contextmenu.tsx","./src/components/files/contextmenu/contextmenuwithdata.tsx","./src/components/files/contextmenu/index.tsx","./src/components/files/contextmenu/styles.tsx","./src/components/filter/filter.test.tsx","./src/components/filter/filter.tsx","./src/components/filter/filterstate.tsx","./src/components/filter/filterwithdata.tsx","./src/components/filter/index.tsx","./src/components/folder/folder.stories.tsx","./src/components/folder/folder.tsx","./src/components/folder/index.tsx","./src/components/icons/add.tsx","./src/components/icons/albumcover.tsx","./src/components/icons/arrowback.tsx","./src/components/icons/artist.tsx","./src/components/icons/heart.tsx","./src/components/icons/heartoutline.tsx","./src/components/icons/next.tsx","./src/components/icons/pause.tsx","./src/components/icons/play.tsx","./src/components/icons/previous.tsx","./src/components/icons/repeat.tsx","./src/components/icons/search.tsx","./src/components/icons/shuffle.tsx","./src/components/icons/speaker.tsx","./src/components/icons/track.tsx","./src/components/likes/likes.tsx","./src/components/likes/likesstate.ts","./src/components/likes/likeswithdata.tsx","./src/components/likes/index.tsx","./src/components/likes/styles.tsx","./src/components/mainview/mainview.tsx","./src/components/mainview/mainviewwithdata.tsx","./src/components/mainview/index.tsx","./src/components/mainview/styles.tsx","./src/components/playlistdetails/playlistdetails.stories.tsx","./src/components/playlistdetails/playlistdetails.tsx","./src/components/playlistdetails/playlistdetailswithdata.tsx","./src/components/playlistdetails/index.tsx","./src/components/playlistdetails/styles.tsx","./src/components/playlists/playlistmodal.tsx","./src/components/playlists/playlists.stories.tsx","./src/components/playlists/playlists.tsx","./src/components/playlists/playlistswithdata.tsx","./src/components/playlists/index.tsx","./src/components/playlists/styles.tsx","./src/components/settings/settings.tsx","./src/components/settings/settingsstate.ts","./src/components/settings/settingswithdata.tsx","./src/components/settings/index.tsx","./src/components/settings/styles.tsx","./src/components/settings/library/library.tsx","./src/components/settings/library/librarywithdata.tsx","./src/components/settings/library/index.tsx","./src/components/settings/library/styles.tsx","./src/components/settings/playback/playback.tsx","./src/components/settings/playback/playbackwithdata.tsx","./src/components/settings/playback/consts.ts","./src/components/settings/playback/index.tsx","./src/components/settings/playback/styles.tsx","./src/components/settings/sound/sound.tsx","./src/components/settings/sound/soundwithdata.tsx","./src/components/settings/sound/index.tsx","./src/components/settings/sound/styles.tsx","./src/components/settings/sound/equalizer/equalizer.tsx","./src/components/settings/sound/equalizer/equalizerwithdata.tsx","./src/components/settings/sound/equalizer/index.tsx","./src/components/settings/sound/equalizer/styles.tsx","./src/components/sidebar/sidebar.test.tsx","./src/components/sidebar/sidebar.tsx","./src/components/sidebar/sidebarwithdata.tsx","./src/components/sidebar/stidebar.stories.tsx","./src/components/sidebar/index.tsx","./src/components/sidebar/styles.tsx","./src/components/skeletons/albumcardskeleton.tsx","./src/components/skeletons/artistcardskeleton.tsx","./src/components/skeletons/artistheaderskeleton.tsx","./src/components/skeletons/detailheaderskeleton.tsx","./src/components/skeletons/filelistskeleton.tsx","./src/components/skeletons/tracklistskeleton.tsx","./src/components/skeletons/trackrowskeleton.tsx","./src/components/skeletons/useskeletoncolors.ts","./src/components/switch/switch.tsx","./src/components/switch/index.tsx","./src/components/table/table.tsx","./src/components/table/index.tsx","./src/components/tracks/tracks.stories.tsx","./src/components/tracks/tracks.test.tsx","./src/components/tracks/tracks.tsx","./src/components/tracks/trackswithdata.tsx","./src/components/tracks/index.tsx","./src/components/tracks/mocks.tsx","./src/components/tracks/styles.tsx","./src/components/virtualizedtable/virtualizedtable.tsx","./src/components/virtualizedtable/index.tsx","./src/containers/albumdetails/albumdetailspage.tsx","./src/containers/albumdetails/index.tsx","./src/containers/albums/albumspage.tsx","./src/containers/albums/index.tsx","./src/containers/artistdetails/artistdetailspage.tsx","./src/containers/artistdetails/index.tsx","./src/containers/artists/artistspage.tsx","./src/containers/artists/index.tsx","./src/containers/extensions/extensionspage.tsx","./src/containers/extensions/index.tsx","./src/containers/files/filespage.tsx","./src/containers/files/index.tsx","./src/containers/likes/likespage.tsx","./src/containers/likes/index.tsx","./src/containers/playlistdetails/smartplaylistdetailspage.tsx","./src/containers/playlistdetails/index.tsx","./src/containers/playlists/playlistspage.tsx","./src/containers/playlists/index.tsx","./src/containers/settings/settingspage.tsx","./src/containers/settings/index.tsx","./src/containers/tracks/trackspage.tsx","./src/containers/tracks/index.tsx","./src/graphql/browse/query.ts","./src/graphql/device/mutation.ts","./src/graphql/device/query.ts","./src/graphql/library/mutation.ts","./src/graphql/library/query.ts","./src/graphql/playback/mutation.ts","./src/graphql/playback/query.ts","./src/graphql/playback/subscription.ts","./src/graphql/playlist/mutation.ts","./src/graphql/playlist/query.ts","./src/graphql/playlist/subscription.ts","./src/graphql/savedplaylist/mutation.ts","./src/graphql/savedplaylist/query.ts","./src/graphql/settings/mutation.ts","./src/graphql/settings/query.ts","./src/graphql/smartplaylist/mutation.ts","./src/graphql/smartplaylist/query.ts","./src/graphql/sound/mutation.tsx","./src/graphql/system/query.ts","./src/hooks/graphql.tsx","./src/hooks/useformat.tsx","./src/hooks/useplayqueue.tsx","./src/hooks/useresumeplaylist.tsx","./src/hooks/usesettings.tsx","./src/providers/graphqlprovider.tsx","./src/providers/themeprovider.tsx","./src/providers/index.tsx","./src/types/file.ts","./src/types/playlist.ts","./src/types/track.ts","./src/lib/graphql-client.ts","./src/lib/subscription-client.ts","./src/stories/button.stories.ts","./src/stories/button.tsx","./src/stories/header.stories.ts","./src/stories/header.tsx","./src/stories/page.stories.ts","./src/stories/page.tsx"],"version":"5.6.2"} 1 + {"root":["./src/app.tsx","./src/theme.ts","./src/constants.ts","./src/emotion.d.ts","./src/main.tsx","./src/mocks.ts","./src/vite-env.d.ts","./src/components/album/album.stories.tsx","./src/components/album/album.test.tsx","./src/components/album/album.tsx","./src/components/album/albumwithdata.tsx","./src/components/album/index.tsx","./src/components/album/styles.tsx","./src/components/album/contextmenu/childmenu.tsx","./src/components/album/contextmenu/contextmenu.stories.tsx","./src/components/album/contextmenu/contextmenu.test.tsx","./src/components/album/contextmenu/contextmenu.tsx","./src/components/album/contextmenu/contextmenuwithdata.tsx","./src/components/album/contextmenu/index.tsx","./src/components/album/contextmenu/styles.tsx","./src/components/albumdetails/albumdetails.stories.tsx","./src/components/albumdetails/albumdetails.test.tsx","./src/components/albumdetails/albumdetails.tsx","./src/components/albumdetails/albumdetailswithdata.tsx","./src/components/albumdetails/index.tsx","./src/components/albumdetails/mocks.tsx","./src/components/albumdetails/styles.tsx","./src/components/albums/albums.stories.tsx","./src/components/albums/albums.test.tsx","./src/components/albums/albums.tsx","./src/components/albums/albumswithdata.tsx","./src/components/albums/index.tsx","./src/components/albums/mocks.tsx","./src/components/albums/styles.tsx","./src/components/artistdetails/artistdetails.stories.tsx","./src/components/artistdetails/artistdetails.test.tsx","./src/components/artistdetails/artistdetails.tsx","./src/components/artistdetails/artistdetailswithdata.tsx","./src/components/artistdetails/index.tsx","./src/components/artistdetails/mocks.tsx","./src/components/artistdetails/styles.tsx","./src/components/artists/artists.stories.tsx","./src/components/artists/artists.test.tsx","./src/components/artists/artists.tsx","./src/components/artists/artistswithdata.tsx","./src/components/artists/index.tsx","./src/components/artists/mocks.tsx","./src/components/artists/styles.tsx","./src/components/button/button.test.tsx","./src/components/button/button.tsx","./src/components/button/index.tsx","./src/components/contextmenu/childmenu.tsx","./src/components/contextmenu/contextmenu.stories.tsx","./src/components/contextmenu/contextmenu.test.tsx","./src/components/contextmenu/contextmenu.tsx","./src/components/contextmenu/contextmenuwithdata.tsx","./src/components/contextmenu/index.tsx","./src/components/contextmenu/styles.tsx","./src/components/controlbar/controlbar.stories.tsx","./src/components/controlbar/controlbar.test.tsx","./src/components/controlbar/controlbar.tsx","./src/components/controlbar/controlbarstate.tsx","./src/components/controlbar/controlbarwithdata.tsx","./src/components/controlbar/index.tsx","./src/components/controlbar/styles.tsx","./src/components/controlbar/bluetoothlist/bluetoothlist.tsx","./src/components/controlbar/bluetoothlist/bluetoothlistwithdata.tsx","./src/components/controlbar/bluetoothlist/index.tsx","./src/components/controlbar/bluetoothlist/styles.ts","./src/components/controlbar/currenttrack/currenttrack.tsx","./src/components/controlbar/currenttrack/index.tsx","./src/components/controlbar/currenttrack/styles.ts","./src/components/controlbar/devicelist/devicelist.tsx","./src/components/controlbar/devicelist/devicelistwithdata.tsx","./src/components/controlbar/devicelist/devicestate.tsx","./src/components/controlbar/devicelist/index.tsx","./src/components/controlbar/devicelist/styles.ts","./src/components/controlbar/playqueue/playqueue.stories.tsx","./src/components/controlbar/playqueue/playqueue.test.tsx","./src/components/controlbar/playqueue/playqueue.tsx","./src/components/controlbar/playqueue/playqueuewithdata.tsx","./src/components/controlbar/playqueue/index.tsx","./src/components/controlbar/playqueue/mocks.tsx","./src/components/controlbar/playqueue/styles.tsx","./src/components/controlbar/rightmenu/rightmenu.tsx","./src/components/controlbar/rightmenu/index.tsx","./src/components/controlbar/rightmenu/styles.tsx","./src/components/controlbar/rightmenu/volume/volume.stories.tsx","./src/components/controlbar/rightmenu/volume/volume.test.tsx","./src/components/controlbar/rightmenu/volume/volume.tsx","./src/components/controlbar/rightmenu/volume/volumewithdata.tsx","./src/components/controlbar/rightmenu/volume/index.tsx","./src/components/controlbar/rightmenu/volume/styles.tsx","./src/components/extensions/extensions.stories.tsx","./src/components/extensions/extensions.tsx","./src/components/extensions/index.tsx","./src/components/files/files.stories.tsx","./src/components/files/files.test.tsx","./src/components/files/files.tsx","./src/components/files/fileswithdata.tsx","./src/components/files/index.tsx","./src/components/files/mocks.tsx","./src/components/files/styles.tsx","./src/components/files/contextmenu/childmenu.tsx","./src/components/files/contextmenu/contextmenu.stories.tsx","./src/components/files/contextmenu/contextmenu.test.tsx","./src/components/files/contextmenu/contextmenu.tsx","./src/components/files/contextmenu/contextmenuwithdata.tsx","./src/components/files/contextmenu/index.tsx","./src/components/files/contextmenu/styles.tsx","./src/components/filter/filter.test.tsx","./src/components/filter/filter.tsx","./src/components/filter/filterstate.tsx","./src/components/filter/filterwithdata.tsx","./src/components/filter/index.tsx","./src/components/folder/folder.stories.tsx","./src/components/folder/folder.tsx","./src/components/folder/index.tsx","./src/components/icons/add.tsx","./src/components/icons/albumcover.tsx","./src/components/icons/arrowback.tsx","./src/components/icons/artist.tsx","./src/components/icons/heart.tsx","./src/components/icons/heartoutline.tsx","./src/components/icons/next.tsx","./src/components/icons/pause.tsx","./src/components/icons/play.tsx","./src/components/icons/previous.tsx","./src/components/icons/repeat.tsx","./src/components/icons/search.tsx","./src/components/icons/shuffle.tsx","./src/components/icons/speaker.tsx","./src/components/icons/track.tsx","./src/components/likes/likes.tsx","./src/components/likes/likesstate.ts","./src/components/likes/likeswithdata.tsx","./src/components/likes/index.tsx","./src/components/likes/styles.tsx","./src/components/mainview/mainview.tsx","./src/components/mainview/mainviewwithdata.tsx","./src/components/mainview/index.tsx","./src/components/mainview/styles.tsx","./src/components/playlistdetails/playlistdetails.stories.tsx","./src/components/playlistdetails/playlistdetails.tsx","./src/components/playlistdetails/playlistdetailswithdata.tsx","./src/components/playlistdetails/index.tsx","./src/components/playlistdetails/styles.tsx","./src/components/playlists/playlistmodal.tsx","./src/components/playlists/playlists.stories.tsx","./src/components/playlists/playlists.tsx","./src/components/playlists/playlistswithdata.tsx","./src/components/playlists/index.tsx","./src/components/playlists/styles.tsx","./src/components/settings/settings.tsx","./src/components/settings/settingsstate.ts","./src/components/settings/settingswithdata.tsx","./src/components/settings/index.tsx","./src/components/settings/styles.tsx","./src/components/settings/library/library.tsx","./src/components/settings/library/librarywithdata.tsx","./src/components/settings/library/index.tsx","./src/components/settings/library/styles.tsx","./src/components/settings/playback/playback.tsx","./src/components/settings/playback/playbackwithdata.tsx","./src/components/settings/playback/consts.ts","./src/components/settings/playback/index.tsx","./src/components/settings/playback/styles.tsx","./src/components/settings/sound/sound.tsx","./src/components/settings/sound/soundwithdata.tsx","./src/components/settings/sound/index.tsx","./src/components/settings/sound/styles.tsx","./src/components/settings/sound/equalizer/equalizer.tsx","./src/components/settings/sound/equalizer/equalizerwithdata.tsx","./src/components/settings/sound/equalizer/index.tsx","./src/components/settings/sound/equalizer/styles.tsx","./src/components/sidebar/sidebar.test.tsx","./src/components/sidebar/sidebar.tsx","./src/components/sidebar/sidebarwithdata.tsx","./src/components/sidebar/stidebar.stories.tsx","./src/components/sidebar/index.tsx","./src/components/sidebar/styles.tsx","./src/components/skeletons/albumcardskeleton.tsx","./src/components/skeletons/artistcardskeleton.tsx","./src/components/skeletons/artistheaderskeleton.tsx","./src/components/skeletons/detailheaderskeleton.tsx","./src/components/skeletons/filelistskeleton.tsx","./src/components/skeletons/tracklistskeleton.tsx","./src/components/skeletons/trackrowskeleton.tsx","./src/components/skeletons/useskeletoncolors.ts","./src/components/switch/switch.tsx","./src/components/switch/index.tsx","./src/components/table/table.tsx","./src/components/table/index.tsx","./src/components/tracks/tracks.stories.tsx","./src/components/tracks/tracks.test.tsx","./src/components/tracks/tracks.tsx","./src/components/tracks/trackswithdata.tsx","./src/components/tracks/index.tsx","./src/components/tracks/mocks.tsx","./src/components/tracks/styles.tsx","./src/components/virtualizedtable/virtualizedtable.tsx","./src/components/virtualizedtable/index.tsx","./src/containers/albumdetails/albumdetailspage.tsx","./src/containers/albumdetails/index.tsx","./src/containers/albums/albumspage.tsx","./src/containers/albums/index.tsx","./src/containers/artistdetails/artistdetailspage.tsx","./src/containers/artistdetails/index.tsx","./src/containers/artists/artistspage.tsx","./src/containers/artists/index.tsx","./src/containers/extensions/extensionspage.tsx","./src/containers/extensions/index.tsx","./src/containers/files/filespage.tsx","./src/containers/files/index.tsx","./src/containers/likes/likespage.tsx","./src/containers/likes/index.tsx","./src/containers/playlistdetails/smartplaylistdetailspage.tsx","./src/containers/playlistdetails/index.tsx","./src/containers/playlists/playlistspage.tsx","./src/containers/playlists/index.tsx","./src/containers/settings/settingspage.tsx","./src/containers/settings/index.tsx","./src/containers/tracks/trackspage.tsx","./src/containers/tracks/index.tsx","./src/graphql/browse/query.ts","./src/graphql/device/mutation.ts","./src/graphql/device/query.ts","./src/graphql/library/mutation.ts","./src/graphql/library/query.ts","./src/graphql/playback/mutation.ts","./src/graphql/playback/query.ts","./src/graphql/playback/subscription.ts","./src/graphql/playlist/mutation.ts","./src/graphql/playlist/query.ts","./src/graphql/playlist/subscription.ts","./src/graphql/savedplaylist/mutation.ts","./src/graphql/savedplaylist/query.ts","./src/graphql/settings/mutation.ts","./src/graphql/settings/query.ts","./src/graphql/smartplaylist/mutation.ts","./src/graphql/smartplaylist/query.ts","./src/graphql/sound/mutation.tsx","./src/graphql/system/query.ts","./src/hooks/graphql.tsx","./src/hooks/useformat.tsx","./src/hooks/useplayqueue.tsx","./src/hooks/useresumeplaylist.tsx","./src/hooks/usesettings.tsx","./src/providers/graphqlprovider.tsx","./src/providers/themeprovider.tsx","./src/providers/index.tsx","./src/types/file.ts","./src/types/playlist.ts","./src/types/track.ts","./src/lib/graphql-client.ts","./src/lib/subscription-client.ts","./src/stories/button.stories.ts","./src/stories/button.tsx","./src/stories/header.stories.ts","./src/stories/header.tsx","./src/stories/page.stories.ts","./src/stories/page.tsx"],"version":"5.6.2"}