A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd
2
fork

Configure Feed

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

webui: display chromcast devices

+1268 -259
+5
crates/graphql/src/schema/device.rs
··· 21 21 let client = reqwest::Client::new(); 22 22 let url = format!("{}/devices/{}", rockbox_url(), id); 23 23 let response = client.get(&url).send().await?; 24 + 25 + if response.status() == 404 { 26 + return Ok(None); 27 + } 28 + 24 29 let response = response.json::<Option<Device>>().await?; 25 30 Ok(response) 26 31 }
+5
crates/rpc/src/device.rs
··· 51 51 .send() 52 52 .await 53 53 .map_err(|e| tonic::Status::internal(e.to_string()))?; 54 + 55 + if response.status() == 404 { 56 + return Ok(tonic::Response::new(GetDeviceResponse { device: None })); 57 + } 58 + 54 59 let response = response 55 60 .json::<Option<rockbox_types::device::Device>>() 56 61 .await
+10
crates/server/src/handlers/devices.rs
··· 48 48 49 49 pub async fn get_device(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { 50 50 let id = &req.params[0]; 51 + if id == "current" { 52 + let current_device = ctx.current_device.lock().unwrap(); 53 + if let Some(device) = current_device.as_ref() { 54 + res.json(&device.clone()); 55 + return Ok(()); 56 + } 57 + res.set_status(404); 58 + return Ok(()); 59 + } 60 + 51 61 let devices = ctx.devices.lock().unwrap(); 52 62 let device = devices.iter().find(|d| d.id == *id); 53 63
+17 -1
crates/server/src/handlers/player.rs
··· 1 1 use std::env; 2 2 3 - use crate::http::{Context, Request, Response}; 3 + use crate::{ 4 + http::{Context, Request, Response}, 5 + GLOBAL_MUTEX, 6 + }; 4 7 use anyhow::Error; 5 8 use local_ip_addr::get_local_ip_address; 6 9 use rand::seq::SliceRandom; 10 + use rockbox_chromecast::Chromecast; 7 11 use rockbox_sys::{ 8 12 self as rb, 9 13 types::{audio_status::AudioStatus, mp3_entry::Mp3Entry}, ··· 16 20 if player.is_none() { 17 21 res.set_status(404); 18 22 return Ok(()); 23 + } 24 + 25 + let mut current_device = ctx.current_device.lock().unwrap(); 26 + let devices = ctx.devices.lock().unwrap(); 27 + let device = devices 28 + .iter() 29 + .find(|d| d.id == *current_device.as_ref().unwrap().id); 30 + if let Some(device) = device { 31 + let mut mutex = GLOBAL_MUTEX.lock().unwrap(); 32 + *mutex = 1; 33 + *player = Chromecast::connect(device.clone())?; 34 + *current_device = Some(device.clone()); 19 35 } 20 36 21 37 let player = player.as_deref_mut().unwrap();
+82 -4
crates/server/src/handlers/playlists.rs
··· 1 + use std::env; 2 + 1 3 use crate::http::{Context, Request, Response}; 2 4 use anyhow::Error; 5 + use local_ip_addr::get_local_ip_address; 3 6 use rand::seq::SliceRandom; 4 7 use rockbox_graphql::read_files; 5 8 use rockbox_library::repo; 6 9 use rockbox_sys::{ 7 - self as rb, types::playlist_amount::PlaylistAmount, PLAYLIST_INSERT_LAST, 8 - PLAYLIST_INSERT_LAST_SHUFFLED, 10 + self as rb, 11 + types::{playlist_amount::PlaylistAmount, playlist_info::PlaylistInfo}, 12 + PLAYLIST_INSERT_LAST, PLAYLIST_INSERT_LAST_SHUFFLED, 9 13 }; 14 + use rockbox_traits::types::track::Track; 10 15 use rockbox_types::{DeleteTracks, InsertTracks, NewPlaylist, StatusCode}; 11 16 12 17 pub async fn create_playlist( ··· 139 144 Ok(()) 140 145 } 141 146 142 - pub async fn insert_tracks(_ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { 147 + pub async fn insert_tracks(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { 143 148 let req_body = req.body.as_ref().unwrap(); 144 149 let mut tracklist: InsertTracks = serde_json::from_str(&req_body).unwrap(); 145 150 let amount = rb::playlist::amount(); 146 151 152 + let mut player = ctx.player.lock().unwrap(); 153 + 154 + if let Some(player) = player.as_deref_mut() { 155 + let kv = ctx.kv.lock().unwrap(); 156 + let rockbox_addr = 157 + env::var("ROCKBOX_ADDR").unwrap_or_else(|_| get_local_ip_address().unwrap()); 158 + let rockbox_port = env::var("ROCKBOX_GRAPHQL_PORT").unwrap_or_else(|_| "6062".to_string()); 159 + 160 + let tracks = tracklist 161 + .tracks 162 + .iter() 163 + .filter(|t| kv.get(*t).is_some()) 164 + .map(|t| { 165 + let track = kv.get(t).unwrap(); 166 + Track { 167 + id: track.id.clone(), 168 + title: track.title.clone(), 169 + artist: track.artist.clone(), 170 + album: track.album.clone(), 171 + album_artist: Some(track.album_artist.clone()), 172 + artist_id: Some(track.artist_id.clone()), 173 + album_id: Some(track.album_id.clone()), 174 + album_cover: track.album_art.clone().map(|cover| { 175 + format!("http://{}:{}/covers/{}", rockbox_addr, rockbox_port, cover) 176 + }), 177 + track_number: track.track_number, 178 + path: track.path.clone(), 179 + uri: format!( 180 + "http://{}:{}/tracks/{}", 181 + rockbox_addr, rockbox_port, track.id 182 + ), 183 + disc_number: track.disc_number, 184 + duration: Some(track.length as f32 / 1000.0), 185 + ..Default::default() 186 + } 187 + }) 188 + .collect::<Vec<Track>>(); 189 + 190 + for track in tracks { 191 + player.play_next(track).await?; 192 + } 193 + 194 + res.text("0"); 195 + return Ok(()); 196 + } 197 + 147 198 if let Some(dir) = &tracklist.directory { 148 199 tracklist.tracks = read_files(dir.clone()).await?; 149 200 } ··· 188 239 Ok(()) 189 240 } 190 241 191 - pub async fn remove_tracks(_ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { 242 + pub async fn remove_tracks(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { 243 + let player = ctx.player.lock().unwrap(); 244 + 245 + if let Some(_) = player.as_deref() { 246 + res.text("0"); 247 + return Ok(()); 248 + } 249 + 192 250 let req_body = req.body.as_ref().unwrap(); 193 251 let params = serde_json::from_str::<DeleteTracks>(&req_body)?; 194 252 let mut ret = 0; ··· 248 306 } 249 307 250 308 pub async fn get_playlist(ctx: &Context, _req: &Request, res: &mut Response) -> Result<(), Error> { 309 + let mut player = ctx.player.lock().unwrap(); 310 + 311 + if let Some(player) = player.as_deref_mut() { 312 + let current_playback = player.get_current_playback().await?; 313 + let tracks = current_playback.items; 314 + let index = match tracks.len() >= 2 { 315 + true => tracks.len() - 2, 316 + false => 0, 317 + } as i32; 318 + 319 + let result = PlaylistInfo { 320 + amount: tracks.len() as i32, 321 + index, 322 + entries: tracks.into_iter().map(|(t, _)| t.into()).collect(), 323 + ..Default::default() 324 + }; 325 + res.json(&result); 326 + return Ok(()); 327 + } 328 + 251 329 let mut metadata_cache = ctx.metadata_cache.lock().await; 252 330 let mut result = rb::playlist::get_current(); 253 331 let mut entries = vec![];
+17 -1
crates/server/src/player_events.rs
··· 5 5 }; 6 6 7 7 use rockbox_graphql::{ 8 - schema::objects::{audio_status::AudioStatus, track::Track}, 8 + schema::objects::{audio_status::AudioStatus, playlist::Playlist, track::Track}, 9 9 simplebroker::SimpleBroker, 10 10 }; 11 11 use rockbox_library::repo; 12 + use rockbox_sys::types::mp3_entry::Mp3Entry; 12 13 use rockbox_traits::Player; 13 14 use sqlx::{Pool, Sqlite}; 14 15 use url::Url; ··· 79 80 true => 1, 80 81 false => 3, 81 82 }, 83 + }); 84 + 85 + let tracks = current_playback.items; 86 + let index = match tracks.len() >= 2 { 87 + true => tracks.len() - 2, 88 + false => 0, 89 + } as i32; 90 + 91 + let tracks: Vec<Mp3Entry> = 92 + tracks.into_iter().map(|(t, _)| t.into()).collect(); 93 + SimpleBroker::publish(Playlist { 94 + amount: tracks.len() as i32, 95 + index, 96 + tracks: tracks.into_iter().map(|t| t.into()).collect(), 97 + ..Default::default() 82 98 }); 83 99 } 84 100 }
+8 -1
crates/server/src/scan.rs
··· 13 13 let services = discover(CHROMECAST_SERVICE_NAME); 14 14 tokio::pin!(services); 15 15 while let Some(info) = services.next().await { 16 - devices.lock().unwrap().push(Device::from(info.clone())); 16 + let mut devices = devices.lock().unwrap(); 17 + if devices 18 + .iter() 19 + .any(|d| d.id == info.get_fullname().to_owned()) 20 + { 21 + continue; 22 + } 23 + devices.push(Device::from(info.clone())); 17 24 SimpleBroker::<Device>::publish(Device::from(info.clone())); 18 25 } 19 26 });
+320 -2
webui/rockbox/graphql.schema.json
··· 430 430 }, 431 431 { 432 432 "kind": "OBJECT", 433 + "name": "Device", 434 + "description": null, 435 + "fields": [ 436 + { 437 + "name": "app", 438 + "description": null, 439 + "args": [], 440 + "type": { 441 + "kind": "NON_NULL", 442 + "name": null, 443 + "ofType": { 444 + "kind": "SCALAR", 445 + "name": "String", 446 + "ofType": null 447 + } 448 + }, 449 + "isDeprecated": false, 450 + "deprecationReason": null 451 + }, 452 + { 453 + "name": "baseUrl", 454 + "description": null, 455 + "args": [], 456 + "type": { 457 + "kind": "SCALAR", 458 + "name": "String", 459 + "ofType": null 460 + }, 461 + "isDeprecated": false, 462 + "deprecationReason": null 463 + }, 464 + { 465 + "name": "host", 466 + "description": null, 467 + "args": [], 468 + "type": { 469 + "kind": "NON_NULL", 470 + "name": null, 471 + "ofType": { 472 + "kind": "SCALAR", 473 + "name": "String", 474 + "ofType": null 475 + } 476 + }, 477 + "isDeprecated": false, 478 + "deprecationReason": null 479 + }, 480 + { 481 + "name": "id", 482 + "description": null, 483 + "args": [], 484 + "type": { 485 + "kind": "NON_NULL", 486 + "name": null, 487 + "ofType": { 488 + "kind": "SCALAR", 489 + "name": "String", 490 + "ofType": null 491 + } 492 + }, 493 + "isDeprecated": false, 494 + "deprecationReason": null 495 + }, 496 + { 497 + "name": "ip", 498 + "description": null, 499 + "args": [], 500 + "type": { 501 + "kind": "NON_NULL", 502 + "name": null, 503 + "ofType": { 504 + "kind": "SCALAR", 505 + "name": "String", 506 + "ofType": null 507 + } 508 + }, 509 + "isDeprecated": false, 510 + "deprecationReason": null 511 + }, 512 + { 513 + "name": "isCastDevice", 514 + "description": null, 515 + "args": [], 516 + "type": { 517 + "kind": "NON_NULL", 518 + "name": null, 519 + "ofType": { 520 + "kind": "SCALAR", 521 + "name": "Boolean", 522 + "ofType": null 523 + } 524 + }, 525 + "isDeprecated": false, 526 + "deprecationReason": null 527 + }, 528 + { 529 + "name": "isConnected", 530 + "description": null, 531 + "args": [], 532 + "type": { 533 + "kind": "NON_NULL", 534 + "name": null, 535 + "ofType": { 536 + "kind": "SCALAR", 537 + "name": "Boolean", 538 + "ofType": null 539 + } 540 + }, 541 + "isDeprecated": false, 542 + "deprecationReason": null 543 + }, 544 + { 545 + "name": "isCurrentDevice", 546 + "description": null, 547 + "args": [], 548 + "type": { 549 + "kind": "NON_NULL", 550 + "name": null, 551 + "ofType": { 552 + "kind": "SCALAR", 553 + "name": "Boolean", 554 + "ofType": null 555 + } 556 + }, 557 + "isDeprecated": false, 558 + "deprecationReason": null 559 + }, 560 + { 561 + "name": "isSourceDevice", 562 + "description": null, 563 + "args": [], 564 + "type": { 565 + "kind": "NON_NULL", 566 + "name": null, 567 + "ofType": { 568 + "kind": "SCALAR", 569 + "name": "Boolean", 570 + "ofType": null 571 + } 572 + }, 573 + "isDeprecated": false, 574 + "deprecationReason": null 575 + }, 576 + { 577 + "name": "name", 578 + "description": null, 579 + "args": [], 580 + "type": { 581 + "kind": "NON_NULL", 582 + "name": null, 583 + "ofType": { 584 + "kind": "SCALAR", 585 + "name": "String", 586 + "ofType": null 587 + } 588 + }, 589 + "isDeprecated": false, 590 + "deprecationReason": null 591 + }, 592 + { 593 + "name": "port", 594 + "description": null, 595 + "args": [], 596 + "type": { 597 + "kind": "NON_NULL", 598 + "name": null, 599 + "ofType": { 600 + "kind": "SCALAR", 601 + "name": "Int", 602 + "ofType": null 603 + } 604 + }, 605 + "isDeprecated": false, 606 + "deprecationReason": null 607 + }, 608 + { 609 + "name": "service", 610 + "description": null, 611 + "args": [], 612 + "type": { 613 + "kind": "NON_NULL", 614 + "name": null, 615 + "ofType": { 616 + "kind": "SCALAR", 617 + "name": "String", 618 + "ofType": null 619 + } 620 + }, 621 + "isDeprecated": false, 622 + "deprecationReason": null 623 + } 624 + ], 625 + "inputFields": null, 626 + "interfaces": [], 627 + "enumValues": null, 628 + "possibleTypes": null 629 + }, 630 + { 631 + "kind": "OBJECT", 433 632 "name": "Entry", 434 633 "description": null, 435 634 "fields": [ ··· 699 898 "ofType": { 700 899 "kind": "SCALAR", 701 900 "name": "String", 901 + "ofType": null 902 + } 903 + }, 904 + "isDeprecated": false, 905 + "deprecationReason": null 906 + }, 907 + { 908 + "name": "connect", 909 + "description": null, 910 + "args": [ 911 + { 912 + "name": "id", 913 + "description": null, 914 + "type": { 915 + "kind": "NON_NULL", 916 + "name": null, 917 + "ofType": { 918 + "kind": "SCALAR", 919 + "name": "String", 920 + "ofType": null 921 + } 922 + }, 923 + "defaultValue": null, 924 + "isDeprecated": false, 925 + "deprecationReason": null 926 + } 927 + ], 928 + "type": { 929 + "kind": "NON_NULL", 930 + "name": null, 931 + "ofType": { 932 + "kind": "SCALAR", 933 + "name": "Boolean", 934 + "ofType": null 935 + } 936 + }, 937 + "isDeprecated": false, 938 + "deprecationReason": null 939 + }, 940 + { 941 + "name": "disconnect", 942 + "description": null, 943 + "args": [ 944 + { 945 + "name": "id", 946 + "description": null, 947 + "type": { 948 + "kind": "NON_NULL", 949 + "name": null, 950 + "ofType": { 951 + "kind": "SCALAR", 952 + "name": "String", 953 + "ofType": null 954 + } 955 + }, 956 + "defaultValue": null, 957 + "isDeprecated": false, 958 + "deprecationReason": null 959 + } 960 + ], 961 + "type": { 962 + "kind": "NON_NULL", 963 + "name": null, 964 + "ofType": { 965 + "kind": "SCALAR", 966 + "name": "Boolean", 702 967 "ofType": null 703 968 } 704 969 }, ··· 2383 2648 "description": null, 2384 2649 "type": { 2385 2650 "kind": "SCALAR", 2386 - "name": "Boolean", 2651 + "name": "Int", 2387 2652 "ofType": null 2388 2653 }, 2389 2654 "defaultValue": null, ··· 2685 2950 "kind": "OBJECT", 2686 2951 "name": "Track", 2687 2952 "ofType": null 2953 + }, 2954 + "isDeprecated": false, 2955 + "deprecationReason": null 2956 + }, 2957 + { 2958 + "name": "device", 2959 + "description": null, 2960 + "args": [ 2961 + { 2962 + "name": "id", 2963 + "description": null, 2964 + "type": { 2965 + "kind": "NON_NULL", 2966 + "name": null, 2967 + "ofType": { 2968 + "kind": "SCALAR", 2969 + "name": "String", 2970 + "ofType": null 2971 + } 2972 + }, 2973 + "defaultValue": null, 2974 + "isDeprecated": false, 2975 + "deprecationReason": null 2976 + } 2977 + ], 2978 + "type": { 2979 + "kind": "OBJECT", 2980 + "name": "Device", 2981 + "ofType": null 2982 + }, 2983 + "isDeprecated": false, 2984 + "deprecationReason": null 2985 + }, 2986 + { 2987 + "name": "devices", 2988 + "description": null, 2989 + "args": [], 2990 + "type": { 2991 + "kind": "NON_NULL", 2992 + "name": null, 2993 + "ofType": { 2994 + "kind": "LIST", 2995 + "name": null, 2996 + "ofType": { 2997 + "kind": "NON_NULL", 2998 + "name": null, 2999 + "ofType": { 3000 + "kind": "OBJECT", 3001 + "name": "Device", 3002 + "ofType": null 3003 + } 3004 + } 3005 + } 2688 3006 }, 2689 3007 "isDeprecated": false, 2690 3008 "deprecationReason": null ··· 6612 6930 "name": null, 6613 6931 "ofType": { 6614 6932 "kind": "SCALAR", 6615 - "name": "Boolean", 6933 + "name": "Int", 6616 6934 "ofType": null 6617 6935 } 6618 6936 },
+24
webui/rockbox/src/Components/AlbumDetails/__snapshots__/AlbumDetails.test.tsx.snap
··· 378 378 <button 379 379 aria-expanded="false" 380 380 aria-haspopup="true" 381 + style="cursor: pointer;" 382 + > 383 + <svg 384 + aria-hidden="true" 385 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 386 + color="#000" 387 + fill="currentColor" 388 + focusable="false" 389 + height="18" 390 + viewBox="0 0 16 16" 391 + width="18" 392 + xmlns="http://www.w3.org/2000/svg" 393 + > 394 + <path 395 + d="M12 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h8zM4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H4z" 396 + /> 397 + <path 398 + d="M8 4.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5zM8 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 3a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-3.5 1.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z" 399 + /> 400 + </svg> 401 + </button> 402 + <button 403 + aria-expanded="false" 404 + aria-haspopup="true" 381 405 class="css-174s4i9" 382 406 > 383 407 <svg
+24
webui/rockbox/src/Components/Albums/__snapshots__/Albums.test.tsx.snap
··· 378 378 <button 379 379 aria-expanded="false" 380 380 aria-haspopup="true" 381 + style="cursor: pointer;" 382 + > 383 + <svg 384 + aria-hidden="true" 385 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 386 + color="#000" 387 + fill="currentColor" 388 + focusable="false" 389 + height="18" 390 + viewBox="0 0 16 16" 391 + width="18" 392 + xmlns="http://www.w3.org/2000/svg" 393 + > 394 + <path 395 + d="M12 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h8zM4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H4z" 396 + /> 397 + <path 398 + d="M8 4.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5zM8 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 3a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-3.5 1.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z" 399 + /> 400 + </svg> 401 + </button> 402 + <button 403 + aria-expanded="false" 404 + aria-haspopup="true" 381 405 class="css-174s4i9" 382 406 > 383 407 <svg
+24
webui/rockbox/src/Components/ArtistDetails/__snapshots__/ArtistDetails.test.tsx.snap
··· 378 378 <button 379 379 aria-expanded="false" 380 380 aria-haspopup="true" 381 + style="cursor: pointer;" 382 + > 383 + <svg 384 + aria-hidden="true" 385 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 386 + color="#000" 387 + fill="currentColor" 388 + focusable="false" 389 + height="18" 390 + viewBox="0 0 16 16" 391 + width="18" 392 + xmlns="http://www.w3.org/2000/svg" 393 + > 394 + <path 395 + d="M12 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h8zM4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H4z" 396 + /> 397 + <path 398 + d="M8 4.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5zM8 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 3a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-3.5 1.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z" 399 + /> 400 + </svg> 401 + </button> 402 + <button 403 + aria-expanded="false" 404 + aria-haspopup="true" 381 405 class="css-174s4i9" 382 406 > 383 407 <svg
+24
webui/rockbox/src/Components/Artists/__snapshots__/Artists.test.tsx.snap
··· 378 378 <button 379 379 aria-expanded="false" 380 380 aria-haspopup="true" 381 + style="cursor: pointer;" 382 + > 383 + <svg 384 + aria-hidden="true" 385 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 386 + color="#000" 387 + fill="currentColor" 388 + focusable="false" 389 + height="18" 390 + viewBox="0 0 16 16" 391 + width="18" 392 + xmlns="http://www.w3.org/2000/svg" 393 + > 394 + <path 395 + d="M12 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h8zM4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H4z" 396 + /> 397 + <path 398 + d="M8 4.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5zM8 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 3a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-3.5 1.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z" 399 + /> 400 + </svg> 401 + </button> 402 + <button 403 + aria-expanded="false" 404 + aria-haspopup="true" 381 405 class="css-174s4i9" 382 406 > 383 407 <svg
+159
webui/rockbox/src/Components/ControlBar/DeviceList/DeviceList.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 { MusicPlayer } from "@styled-icons/bootstrap"; 6 + import { Laptop } from "@styled-icons/ionicons-outline"; 7 + import { Kodi, Airplayaudio, Chromecast } from "@styled-icons/simple-icons"; 8 + import { Speaker } from "@styled-icons/remix-fill"; 9 + import { 10 + Container, 11 + CurrentDevice, 12 + CurrentDeviceName, 13 + CurrentDeviceWrapper, 14 + Disconnect, 15 + Icon, 16 + IconWrapper, 17 + List, 18 + Placeholder, 19 + Title, 20 + } from "./styles"; 21 + 22 + export type Device = { 23 + id: string; 24 + name: string; 25 + type: string; 26 + isConnected: boolean; 27 + }; 28 + 29 + export type ArtworkProps = { 30 + icon?: string; 31 + color?: string; 32 + }; 33 + 34 + const Artwork: FC<ArtworkProps> = ( 35 + { icon, color } = { 36 + icon: "music-player", 37 + } 38 + ) => { 39 + const theme = useTheme(); 40 + return ( 41 + <Icon color={color}> 42 + {icon === "music-player" && <MusicPlayer size={18} color="#28fce3" />} 43 + {icon === "xbmc" && <Kodi size={18} color="#28cbfc" />} 44 + {icon === "airplay" && <Airplayaudio size={18} color={"#ff00c3"} />} 45 + {icon === "chromecast" && ( 46 + <Chromecast size={18} color={theme.colors.text} /> 47 + )} 48 + {icon === "dlna" && <Speaker size={18} color={"#ff00c3"} />} 49 + </Icon> 50 + ); 51 + }; 52 + 53 + const DeviceName = styled.div` 54 + font-size: 14px; 55 + color: "#fe099c"; 56 + `; 57 + 58 + export type DeviceListProps = { 59 + currentCastDevice?: Device | null; 60 + castDevices: Device[]; 61 + connectToCastDevice: (deviceId: string) => void; 62 + disconnectFromCastDevice: () => void; 63 + close: () => void; 64 + loading: boolean; 65 + }; 66 + 67 + const DeviceList: FC<DeviceListProps> = ({ 68 + castDevices, 69 + close, 70 + connectToCastDevice, 71 + disconnectFromCastDevice, 72 + currentCastDevice, 73 + loading, 74 + }) => { 75 + const theme = useTheme(); 76 + const colors: { 77 + [key: string]: string; 78 + } = { 79 + "music-player": "rgba(40, 252, 227, 0.088)", 80 + xbmc: "rgba(40, 203, 252, 0.082)", 81 + airplay: "rgba(255, 0, 195, 0.063)", 82 + dlna: "rgba(255, 0, 195, 0.063)", 83 + }; 84 + 85 + const _onConnectToCastDevice = (deviceId: string) => { 86 + connectToCastDevice(deviceId); 87 + close(); 88 + }; 89 + 90 + const _onDisconnectFromCastDevice = () => { 91 + disconnectFromCastDevice(); 92 + close(); 93 + }; 94 + 95 + return ( 96 + <Container> 97 + <CurrentDeviceWrapper> 98 + <IconWrapper> 99 + <Laptop size={30} color={"#fe099c"} /> 100 + </IconWrapper> 101 + <div style={{ flex: 1 }}> 102 + <CurrentDevice>Current device</CurrentDevice> 103 + <CurrentDeviceName> 104 + {currentCastDevice ? currentCastDevice.name : "Rockbox"} 105 + </CurrentDeviceName> 106 + </div> 107 + {currentCastDevice && ( 108 + <Disconnect onClick={_onDisconnectFromCastDevice}> 109 + disconnect 110 + </Disconnect> 111 + )} 112 + </CurrentDeviceWrapper> 113 + {!loading && <Title>Select another output device</Title>} 114 + <List> 115 + {castDevices.length === 0 && !loading && ( 116 + <Placeholder> 117 + No devices found. Please make sure your device is connected to the 118 + same network as this device. 119 + </Placeholder> 120 + )} 121 + {castDevices.map((device) => ( 122 + <div 123 + key={device.id} 124 + onClick={() => _onConnectToCastDevice(device.id)} 125 + > 126 + <ListItem 127 + key={device.id} 128 + artwork={() => ( 129 + <Artwork icon={device.type} color={colors[device.type]} /> 130 + )} 131 + overrides={{ 132 + Root: { 133 + style: { 134 + cursor: "pointer", 135 + ":hover": { 136 + backgroundColor: theme.colors.hover, 137 + }, 138 + borderRadius: "5px", 139 + }, 140 + }, 141 + Content: { 142 + style: { 143 + borderBottom: "none", 144 + }, 145 + }, 146 + }} 147 + > 148 + <ListItemLabel> 149 + <DeviceName>{device.name}</DeviceName> 150 + </ListItemLabel> 151 + </ListItem> 152 + </div> 153 + ))} 154 + </List> 155 + </Container> 156 + ); 157 + }; 158 + 159 + export default DeviceList;
+89
webui/rockbox/src/Components/ControlBar/DeviceList/DeviceListWithData.tsx
··· 1 + import { FC, useEffect, useMemo } from "react"; 2 + import DeviceList from "./DeviceList"; 3 + import { 4 + useConnectToDeviceMutation, 5 + useDisconnectFromDeviceMutation, 6 + useGetDeviceQuery, 7 + useGetDevicesQuery, 8 + } from "../../../Hooks/GraphQL"; 9 + import { useRecoilState } from "recoil"; 10 + import { deviceState } from "./DeviceState"; 11 + import { controlBarState } from "../ControlBarState"; 12 + 13 + export type DeviceListWithDataProps = { 14 + close: () => void; 15 + }; 16 + 17 + const DeviceListWithData: FC<DeviceListWithDataProps> = ({ close }) => { 18 + const [, setControlBarState] = useRecoilState(controlBarState); 19 + const [device, setDeviceState] = useRecoilState(deviceState); 20 + const { data: currentDevice } = useGetDeviceQuery({ 21 + variables: { id: "current" }, 22 + fetchPolicy: "network-only", 23 + }); 24 + const { data, loading } = useGetDevicesQuery(); 25 + const [connect] = useConnectToDeviceMutation(); 26 + const [disconnect] = useDisconnectFromDeviceMutation(); 27 + const devices = useMemo(() => { 28 + if (loading || !data) { 29 + return []; 30 + } 31 + return (data.devices || []).map((x) => ({ 32 + id: x.id, 33 + name: x.name, 34 + type: x.app, 35 + isConnected: x.isConnected, 36 + })); 37 + }, [data, loading]); 38 + 39 + useEffect(() => { 40 + if (currentDevice) { 41 + if (!currentDevice.device) { 42 + setDeviceState({ 43 + currentDevice: null, 44 + }); 45 + return; 46 + } 47 + setDeviceState({ 48 + currentDevice: { 49 + id: currentDevice.device.id || "", 50 + name: currentDevice.device.name || "", 51 + type: currentDevice.device.app || "", 52 + isConnected: currentDevice.device.isConnected || false, 53 + }, 54 + }); 55 + } 56 + // eslint-disable-next-line react-hooks/exhaustive-deps 57 + }, [currentDevice]); 58 + 59 + const connectToCastDevice = async (id: string) => { 60 + await connect({ variables: { id } }); 61 + setControlBarState((state) => ({ 62 + ...state, 63 + nowPlaying: undefined, 64 + })); 65 + }; 66 + 67 + const disconnectDevice = async () => { 68 + await disconnect({ variables: { id: currentDevice?.device?.id || "" } }); 69 + // reload page to reset the state 70 + window.location.reload(); 71 + }; 72 + 73 + return ( 74 + <> 75 + { 76 + <DeviceList 77 + loading={loading} 78 + castDevices={devices} 79 + connectToCastDevice={connectToCastDevice} 80 + disconnectFromCastDevice={disconnectDevice} 81 + close={close} 82 + currentCastDevice={device.currentDevice} 83 + /> 84 + } 85 + </> 86 + ); 87 + }; 88 + 89 + export default DeviceListWithData;
+11
webui/rockbox/src/Components/ControlBar/DeviceList/DeviceState.tsx
··· 1 + import { atom } from "recoil"; 2 + import { Device } from "./DeviceList"; 3 + 4 + export const deviceState = atom<{ 5 + currentDevice: Device | null; 6 + }>({ 7 + key: "deviceState", 8 + default: { 9 + currentDevice: null, 10 + }, 11 + });
+2 -234
webui/rockbox/src/Components/ControlBar/DeviceList/index.tsx
··· 1 - import { css, useTheme } from "@emotion/react"; 2 - import styled from "@emotion/styled"; 3 - import { ListItem, ListItemLabel } from "baseui/list"; 4 - import { FC } from "react"; 5 - import { MusicPlayer } from "@styled-icons/bootstrap"; 6 - import { Laptop } from "@styled-icons/ionicons-outline"; 7 - import { Kodi, Airplayaudio, Chromecast } from "@styled-icons/simple-icons"; 8 - import { Speaker } from "@styled-icons/remix-fill"; 9 - 10 - export type Device = { 11 - id: string; 12 - name: string; 13 - type: string; 14 - isConnected: boolean; 15 - }; 16 - 17 - const Container = styled.div` 18 - max-height: calc(100vh - 153px); /* - 90px */ 19 - padding-top: 15px; 20 - padding-bottom: 15px; 21 - overflow-y: auto; 22 - width: 370px; 23 - min-height: 200px; 24 - `; 25 - 26 - const List = styled.div` 27 - max-height: calc(100vh - 273px); /* - 210px */ 28 - padding-left: 15px; 29 - padding-right: 15px; 30 - overflow-y: auto; 31 - min-height: 200px; 32 - `; 33 - 34 - const Icon = styled.div` 35 - height: 40px; 36 - width: 40px; 37 - border-radius: 50%; 38 - display: flex; 39 - align-items: center; 40 - justify-content: center; 41 - background-color: ${(props) => props.theme.colors.cover}; 42 - ${(props) => 43 - props.color && 44 - css` 45 - background-color: ${props.color}; 46 - `} 47 - `; 48 - 49 - const Title = styled.div` 50 - margin: 10px; 51 - margin-left: 25px; 52 - margin-right: 25px; 53 - font-family: "RockfordSansBold"; 54 - `; 55 - 56 - const CurrentDeviceWrapper = styled.div` 57 - height: 60px; 58 - display: flex; 59 - margin-left: 25px; 60 - margin-right: 25px; 61 - align-items: center; 62 - `; 63 - 64 - const CurrentDevice = styled.div` 65 - font-size: 18px; 66 - `; 67 - 68 - const CurrentDeviceName = styled.div` 69 - color: #fe099c; 70 - font-size: 14px; 71 - `; 72 - 73 - const IconWrapper = styled.div` 74 - margin-top: 3px; 75 - margin-right: 16px; 76 - `; 77 - 78 - const Disconnect = styled.button` 79 - background-color: #000; 80 - border: none; 81 - color: #fff; 82 - height: 21px; 83 - border-radius: 12px; 84 - font-family: "RockfordSansRegular"; 85 - font-size: 12px; 86 - display: flex; 87 - align-items: center; 88 - justify-content: center; 89 - width: 80px; 90 - padding-bottom: 4px; 91 - cursor: pointer; 92 - `; 93 - 94 - const Placeholder = styled.div` 95 - display: flex; 96 - align-items: center; 97 - justify-content: center; 98 - height: 300px; 99 - text-align: center; 100 - padding-left: 20px; 101 - padding-right: 20px; 102 - font-size: 14px; 103 - `; 104 - 105 - export type ArtworkProps = { 106 - icon?: string; 107 - color?: string; 108 - }; 109 - 110 - const Artwork: FC<ArtworkProps> = ({ icon, color }) => { 111 - const theme = useTheme(); 112 - return ( 113 - <Icon color={color}> 114 - {icon === "music-player" && <MusicPlayer size={18} color="#28fce3" />} 115 - {icon === "xbmc" && <Kodi size={18} color="#28cbfc" />} 116 - {icon === "airplay" && <Airplayaudio size={18} color={"#ff00c3"} />} 117 - {icon === "chromecast" && ( 118 - <Chromecast size={18} color={theme.colors.text} /> 119 - )} 120 - {icon === "dlna" && <Speaker size={18} color={"#ff00c3"} />} 121 - </Icon> 122 - ); 123 - }; 124 - 125 - Artwork.defaultProps = { 126 - icon: "music-player", 127 - }; 128 - 129 - const DeviceName = styled.div` 130 - font-size: 14px; 131 - color: "#fe099c"; 132 - `; 133 - 134 - export type DeviceListProps = { 135 - currentCastDevice?: Device; 136 - castDevices: Device[]; 137 - connectToCastDevice: (deviceId: string) => void; 138 - disconnectFromCastDevice: () => void; 139 - close: () => void; 140 - }; 141 - 142 - const DeviceList: FC<DeviceListProps> = ({ 143 - castDevices, 144 - close, 145 - connectToCastDevice, 146 - disconnectFromCastDevice, 147 - currentCastDevice, 148 - }) => { 149 - const theme = useTheme(); 150 - const colors: { 151 - [key: string]: string; 152 - } = { 153 - "music-player": "rgba(40, 252, 227, 0.088)", 154 - xbmc: "rgba(40, 203, 252, 0.082)", 155 - airplay: "rgba(255, 0, 195, 0.063)", 156 - dlna: "rgba(255, 0, 195, 0.063)", 157 - }; 158 - 159 - const _onConnectToCastDevice = (deviceId: string) => { 160 - connectToCastDevice(deviceId); 161 - close(); 162 - }; 163 - 164 - const _onDisconnectFromCastDevice = () => { 165 - disconnectFromCastDevice(); 166 - close(); 167 - }; 168 - 169 - return ( 170 - <Container> 171 - <CurrentDeviceWrapper> 172 - <IconWrapper> 173 - <Laptop size={30} color={"#fe099c"} /> 174 - </IconWrapper> 175 - <div style={{ flex: 1 }}> 176 - <CurrentDevice>Current device</CurrentDevice> 177 - <CurrentDeviceName> 178 - {currentCastDevice ? currentCastDevice.name : "Rockbox"} 179 - </CurrentDeviceName> 180 - </div> 181 - {currentCastDevice && ( 182 - <Disconnect onClick={_onDisconnectFromCastDevice}> 183 - disconnect 184 - </Disconnect> 185 - )} 186 - </CurrentDeviceWrapper> 187 - <Title>Select another output device</Title> 188 - <List> 189 - {castDevices.length === 0 && ( 190 - <Placeholder> 191 - No devices found. Please make sure your device is connected to the 192 - same network as this device. 193 - </Placeholder> 194 - )} 195 - {castDevices.map((device) => ( 196 - <div 197 - key={device.id} 198 - onClick={() => _onConnectToCastDevice(device.id)} 199 - > 200 - <ListItem 201 - key={device.id} 202 - artwork={() => ( 203 - <Artwork icon={device.type} color={colors[device.type]} /> 204 - )} 205 - overrides={{ 206 - Root: { 207 - style: { 208 - cursor: "pointer", 209 - ":hover": { 210 - backgroundColor: theme.colors.hover, 211 - }, 212 - borderRadius: "5px", 213 - }, 214 - }, 215 - Content: { 216 - style: { 217 - borderBottom: "none", 218 - }, 219 - }, 220 - }} 221 - > 222 - <ListItemLabel> 223 - <DeviceName>{device.name}</DeviceName> 224 - </ListItemLabel> 225 - </ListItem> 226 - </div> 227 - ))} 228 - </List> 229 - </Container> 230 - ); 231 - }; 1 + import DeviceListWithData from "./DeviceListWithData"; 232 2 233 - DeviceList.defaultProps = {}; 234 - 235 - export default DeviceList; 3 + export default DeviceListWithData;
+90
webui/rockbox/src/Components/ControlBar/DeviceList/styles.ts
··· 1 + import { css } from "@emotion/react"; 2 + import styled from "@emotion/styled"; 3 + 4 + export const Container = styled.div` 5 + max-height: calc(100vh - 153px); /* - 90px */ 6 + padding-top: 15px; 7 + padding-bottom: 15px; 8 + overflow-y: auto; 9 + width: 370px; 10 + min-height: 200px; 11 + `; 12 + 13 + export const List = styled.div` 14 + max-height: calc(100vh - 273px); /* - 210px */ 15 + padding-left: 15px; 16 + padding-right: 15px; 17 + overflow-y: auto; 18 + min-height: 200px; 19 + `; 20 + 21 + export const Icon = styled.div` 22 + height: 40px; 23 + width: 40px; 24 + border-radius: 50%; 25 + display: flex; 26 + align-items: center; 27 + justify-content: center; 28 + background-color: ${(props) => props.theme.colors.cover}; 29 + ${(props) => 30 + props.color && 31 + css` 32 + background-color: ${props.color}; 33 + `} 34 + `; 35 + 36 + export const Title = styled.div` 37 + margin: 10px; 38 + margin-left: 25px; 39 + margin-right: 25px; 40 + font-family: "RockfordSansBold"; 41 + `; 42 + 43 + export const CurrentDeviceWrapper = styled.div` 44 + height: 60px; 45 + display: flex; 46 + margin-left: 25px; 47 + margin-right: 25px; 48 + align-items: center; 49 + `; 50 + 51 + export const CurrentDevice = styled.div` 52 + font-size: 18px; 53 + `; 54 + 55 + export const CurrentDeviceName = styled.div` 56 + color: #fe099c; 57 + font-size: 14px; 58 + `; 59 + 60 + export const IconWrapper = styled.div` 61 + margin-top: 3px; 62 + margin-right: 16px; 63 + `; 64 + 65 + export const Disconnect = styled.button` 66 + background-color: #000; 67 + border: none; 68 + color: #fff; 69 + height: 21px; 70 + border-radius: 12px; 71 + font-family: "RockfordSansRegular"; 72 + font-size: 12px; 73 + display: flex; 74 + align-items: center; 75 + justify-content: center; 76 + width: 80px; 77 + padding-bottom: 4px; 78 + cursor: pointer; 79 + `; 80 + 81 + export const Placeholder = styled.div` 82 + display: flex; 83 + align-items: center; 84 + justify-content: center; 85 + height: 300px; 86 + text-align: center; 87 + padding-left: 20px; 88 + padding-right: 20px; 89 + font-size: 14px; 90 + `;
+2 -8
webui/rockbox/src/Components/ControlBar/RightMenu/RightMenu.tsx
··· 18 18 <Volume /> 19 19 <StatefulPopover 20 20 placement="bottom" 21 - content={({ close }) => ( 22 - <DeviceList 23 - castDevices={[]} 24 - connectToCastDevice={() => {}} 25 - disconnectFromCastDevice={() => {}} 26 - close={close} 27 - /> 28 - )} 21 + content={({ close }) => <DeviceList close={close} />} 29 22 overrides={{ 30 23 Body: { 31 24 style: { 25 + top: "10px", 32 26 left: "-70px", 33 27 }, 34 28 },
+24
webui/rockbox/src/Components/ControlBar/__snapshots__/ControlBar.test.tsx.snap
··· 266 266 <button 267 267 aria-expanded="false" 268 268 aria-haspopup="true" 269 + style="cursor: pointer;" 270 + > 271 + <svg 272 + aria-hidden="true" 273 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 274 + color="#000" 275 + fill="currentColor" 276 + focusable="false" 277 + height="18" 278 + viewBox="0 0 16 16" 279 + width="18" 280 + xmlns="http://www.w3.org/2000/svg" 281 + > 282 + <path 283 + d="M12 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h8zM4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H4z" 284 + /> 285 + <path 286 + d="M8 4.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5zM8 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 3a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-3.5 1.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z" 287 + /> 288 + </svg> 289 + </button> 290 + <button 291 + aria-expanded="false" 292 + aria-haspopup="true" 269 293 class="css-174s4i9" 270 294 > 271 295 <svg
+24
webui/rockbox/src/Components/Files/__snapshots__/Files.test.tsx.snap
··· 378 378 <button 379 379 aria-expanded="false" 380 380 aria-haspopup="true" 381 + style="cursor: pointer;" 382 + > 383 + <svg 384 + aria-hidden="true" 385 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 386 + color="#000" 387 + fill="currentColor" 388 + focusable="false" 389 + height="18" 390 + viewBox="0 0 16 16" 391 + width="18" 392 + xmlns="http://www.w3.org/2000/svg" 393 + > 394 + <path 395 + d="M12 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h8zM4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H4z" 396 + /> 397 + <path 398 + d="M8 4.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5zM8 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 3a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-3.5 1.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z" 399 + /> 400 + </svg> 401 + </button> 402 + <button 403 + aria-expanded="false" 404 + aria-haspopup="true" 381 405 class="css-174s4i9" 382 406 > 383 407 <svg
+24
webui/rockbox/src/Components/Tracks/__snapshots__/Tracks.test.tsx.snap
··· 378 378 <button 379 379 aria-expanded="false" 380 380 aria-haspopup="true" 381 + style="cursor: pointer;" 382 + > 383 + <svg 384 + aria-hidden="true" 385 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 386 + color="#000" 387 + fill="currentColor" 388 + focusable="false" 389 + height="18" 390 + viewBox="0 0 16 16" 391 + width="18" 392 + xmlns="http://www.w3.org/2000/svg" 393 + > 394 + <path 395 + d="M12 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h8zM4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H4z" 396 + /> 397 + <path 398 + d="M8 4.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5zM8 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 3a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-3.5 1.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z" 399 + /> 400 + </svg> 401 + </button> 402 + <button 403 + aria-expanded="false" 404 + aria-haspopup="true" 381 405 class="css-174s4i9" 382 406 > 383 407 <svg
+13
webui/rockbox/src/GraphQL/Device/Mutation.ts
··· 1 + import { gql } from "@apollo/client"; 2 + 3 + export const CONNECT_TO_DEVICE = gql` 4 + mutation ConnectToDevice($id: String!) { 5 + connect(id: $id) 6 + } 7 + `; 8 + 9 + export const DISCONNECT_FROM_DEVICE = gql` 10 + mutation DisconnectFromDevice($id: String!) { 11 + disconnect(id: $id) 12 + } 13 + `;
+33
webui/rockbox/src/GraphQL/Device/Query.ts
··· 1 + import { gql } from "@apollo/client"; 2 + 3 + export const GET_DEVICES = gql` 4 + query GetDevices { 5 + devices { 6 + id 7 + name 8 + app 9 + ip 10 + host 11 + port 12 + isCastDevice 13 + service 14 + isConnected 15 + } 16 + } 17 + `; 18 + 19 + export const GET_DEVICE = gql` 20 + query GetDevice($id: String!) { 21 + device(id: $id) { 22 + id 23 + name 24 + app 25 + ip 26 + host 27 + port 28 + isCastDevice 29 + service 30 + isConnected 31 + } 32 + } 33 + `;
+221 -3
webui/rockbox/src/Hooks/GraphQL.tsx
··· 55 55 threshold: Scalars['Int']['output']; 56 56 }; 57 57 58 + export type Device = { 59 + __typename?: 'Device'; 60 + app: Scalars['String']['output']; 61 + baseUrl?: Maybe<Scalars['String']['output']>; 62 + host: Scalars['String']['output']; 63 + id: Scalars['String']['output']; 64 + ip: Scalars['String']['output']; 65 + isCastDevice: Scalars['Boolean']['output']; 66 + isConnected: Scalars['Boolean']['output']; 67 + isCurrentDevice: Scalars['Boolean']['output']; 68 + isSourceDevice: Scalars['Boolean']['output']; 69 + name: Scalars['String']['output']; 70 + port: Scalars['Int']['output']; 71 + service: Scalars['String']['output']; 72 + }; 73 + 58 74 export type Entry = { 59 75 __typename?: 'Entry'; 60 76 attr: Scalars['Int']['output']; ··· 80 96 __typename?: 'Mutation'; 81 97 adjustVolume: Scalars['Int']['output']; 82 98 beepPlay: Scalars['String']['output']; 99 + connect: Scalars['Boolean']['output']; 100 + disconnect: Scalars['Boolean']['output']; 83 101 fastForwardRewind: Scalars['Int']['output']; 84 102 flushAndReloadTracks: Scalars['Int']['output']; 85 103 hardStop: Scalars['Int']['output']; ··· 129 147 130 148 export type MutationAdjustVolumeArgs = { 131 149 steps: Scalars['Int']['input']; 150 + }; 151 + 152 + 153 + export type MutationConnectArgs = { 154 + id: Scalars['String']['input']; 155 + }; 156 + 157 + 158 + export type MutationDisconnectArgs = { 159 + id: Scalars['String']['input']; 132 160 }; 133 161 134 162 ··· 284 312 surroundBalance?: InputMaybe<Scalars['Int']['input']>; 285 313 surroundEnabled?: InputMaybe<Scalars['Boolean']['input']>; 286 314 surroundFx1?: InputMaybe<Scalars['Int']['input']>; 287 - surroundFx2?: InputMaybe<Scalars['Boolean']['input']>; 315 + surroundFx2?: InputMaybe<Scalars['Int']['input']>; 288 316 treble?: InputMaybe<Scalars['Int']['input']>; 289 317 trebleCutoff?: InputMaybe<Scalars['Int']['input']>; 290 318 }; ··· 308 336 artist?: Maybe<Artist>; 309 337 artists: Array<Artist>; 310 338 currentTrack?: Maybe<Track>; 339 + device?: Maybe<Device>; 340 + devices: Array<Device>; 311 341 getDisplayIndex: Scalars['String']['output']; 312 342 getFilePosition: Scalars['Int']['output']; 313 343 getFirstIndex: Scalars['String']['output']; ··· 339 369 340 370 341 371 export type QueryArtistArgs = { 372 + id: Scalars['String']['input']; 373 + }; 374 + 375 + 376 + export type QueryDeviceArgs = { 342 377 id: Scalars['String']['input']; 343 378 }; 344 379 ··· 593 628 surroundBalance: Scalars['Int']['output']; 594 629 surroundEnabled: Scalars['Int']['output']; 595 630 surroundFx1: Scalars['Int']['output']; 596 - surroundFx2: Scalars['Boolean']['output']; 631 + surroundFx2: Scalars['Int']['output']; 597 632 surroundMethod2: Scalars['Boolean']['output']; 598 633 surroundMix: Scalars['Int']['output']; 599 634 tagcacheAutoupdate: Scalars['Boolean']['output']; ··· 639 674 640 675 export type GetEntriesQuery = { __typename?: 'Query', treeGetEntries: Array<{ __typename?: 'Entry', name: string, attr: number, timeWrite: number }> }; 641 676 677 + export type ConnectToDeviceMutationVariables = Exact<{ 678 + id: Scalars['String']['input']; 679 + }>; 680 + 681 + 682 + export type ConnectToDeviceMutation = { __typename?: 'Mutation', connect: boolean }; 683 + 684 + export type DisconnectFromDeviceMutationVariables = Exact<{ 685 + id: Scalars['String']['input']; 686 + }>; 687 + 688 + 689 + export type DisconnectFromDeviceMutation = { __typename?: 'Mutation', disconnect: boolean }; 690 + 691 + export type GetDevicesQueryVariables = Exact<{ [key: string]: never; }>; 692 + 693 + 694 + export type GetDevicesQuery = { __typename?: 'Query', devices: Array<{ __typename?: 'Device', id: string, name: string, app: string, ip: string, host: string, port: number, isCastDevice: boolean, service: string, isConnected: boolean }> }; 695 + 696 + export type GetDeviceQueryVariables = Exact<{ 697 + id: Scalars['String']['input']; 698 + }>; 699 + 700 + 701 + export type GetDeviceQuery = { __typename?: 'Query', device?: { __typename?: 'Device', id: string, name: string, app: string, ip: string, host: string, port: number, isCastDevice: boolean, service: string, isConnected: boolean } | null }; 702 + 642 703 export type LikeTrackMutationVariables = Exact<{ 643 704 trackId: Scalars['String']['input']; 644 705 }>; ··· 897 958 export type GetGlobalSettingsQueryVariables = Exact<{ [key: string]: never; }>; 898 959 899 960 900 - export type GetGlobalSettingsQuery = { __typename?: 'Query', globalSettings: { __typename?: 'UserSettings', musicDir: string, volume: number, playlistShuffle: boolean, repeatMode: number, bass: number, bassCutoff: number, treble: number, trebleCutoff: number, crossfade: number, fadeOnStop: boolean, crossfadeFadeInDelay: number, crossfadeFadeInDuration: number, crossfadeFadeOutDelay: number, crossfadeFadeOutDuration: number, crossfadeFadeOutMixmode: number, balance: number, stereoWidth: number, stereoswMode: number, surroundEnabled: number, surroundBalance: number, surroundFx1: number, surroundFx2: boolean, partyMode: boolean, ditheringEnabled: boolean, channelConfig: number, playerName: string, eqEnabled: boolean, eqBandSettings: Array<{ __typename?: 'EqBandSetting', q: number, cutoff: number, gain: number }>, replaygainSettings: { __typename?: 'ReplaygainSettings', noclip: boolean, type: number, preamp: number } } }; 961 + export type GetGlobalSettingsQuery = { __typename?: 'Query', globalSettings: { __typename?: 'UserSettings', musicDir: string, volume: number, playlistShuffle: boolean, repeatMode: number, bass: number, bassCutoff: number, treble: number, trebleCutoff: number, crossfade: number, fadeOnStop: boolean, crossfadeFadeInDelay: number, crossfadeFadeInDuration: number, crossfadeFadeOutDelay: number, crossfadeFadeOutDuration: number, crossfadeFadeOutMixmode: number, balance: number, stereoWidth: number, stereoswMode: number, surroundEnabled: number, surroundBalance: number, surroundFx1: number, surroundFx2: number, partyMode: boolean, ditheringEnabled: boolean, channelConfig: number, playerName: string, eqEnabled: boolean, eqBandSettings: Array<{ __typename?: 'EqBandSetting', q: number, cutoff: number, gain: number }>, replaygainSettings: { __typename?: 'ReplaygainSettings', noclip: boolean, type: number, preamp: number } } }; 901 962 902 963 export type AdjustVolumeMutationVariables = Exact<{ 903 964 steps: Scalars['Int']['input']; ··· 959 1020 export type GetEntriesLazyQueryHookResult = ReturnType<typeof useGetEntriesLazyQuery>; 960 1021 export type GetEntriesSuspenseQueryHookResult = ReturnType<typeof useGetEntriesSuspenseQuery>; 961 1022 export type GetEntriesQueryResult = Apollo.QueryResult<GetEntriesQuery, GetEntriesQueryVariables>; 1023 + export const ConnectToDeviceDocument = gql` 1024 + mutation ConnectToDevice($id: String!) { 1025 + connect(id: $id) 1026 + } 1027 + `; 1028 + export type ConnectToDeviceMutationFn = Apollo.MutationFunction<ConnectToDeviceMutation, ConnectToDeviceMutationVariables>; 1029 + 1030 + /** 1031 + * __useConnectToDeviceMutation__ 1032 + * 1033 + * To run a mutation, you first call `useConnectToDeviceMutation` within a React component and pass it any options that fit your needs. 1034 + * When your component renders, `useConnectToDeviceMutation` returns a tuple that includes: 1035 + * - A mutate function that you can call at any time to execute the mutation 1036 + * - An object with fields that represent the current status of the mutation's execution 1037 + * 1038 + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; 1039 + * 1040 + * @example 1041 + * const [connectToDeviceMutation, { data, loading, error }] = useConnectToDeviceMutation({ 1042 + * variables: { 1043 + * id: // value for 'id' 1044 + * }, 1045 + * }); 1046 + */ 1047 + export function useConnectToDeviceMutation(baseOptions?: Apollo.MutationHookOptions<ConnectToDeviceMutation, ConnectToDeviceMutationVariables>) { 1048 + const options = {...defaultOptions, ...baseOptions} 1049 + return Apollo.useMutation<ConnectToDeviceMutation, ConnectToDeviceMutationVariables>(ConnectToDeviceDocument, options); 1050 + } 1051 + export type ConnectToDeviceMutationHookResult = ReturnType<typeof useConnectToDeviceMutation>; 1052 + export type ConnectToDeviceMutationResult = Apollo.MutationResult<ConnectToDeviceMutation>; 1053 + export type ConnectToDeviceMutationOptions = Apollo.BaseMutationOptions<ConnectToDeviceMutation, ConnectToDeviceMutationVariables>; 1054 + export const DisconnectFromDeviceDocument = gql` 1055 + mutation DisconnectFromDevice($id: String!) { 1056 + disconnect(id: $id) 1057 + } 1058 + `; 1059 + export type DisconnectFromDeviceMutationFn = Apollo.MutationFunction<DisconnectFromDeviceMutation, DisconnectFromDeviceMutationVariables>; 1060 + 1061 + /** 1062 + * __useDisconnectFromDeviceMutation__ 1063 + * 1064 + * To run a mutation, you first call `useDisconnectFromDeviceMutation` within a React component and pass it any options that fit your needs. 1065 + * When your component renders, `useDisconnectFromDeviceMutation` returns a tuple that includes: 1066 + * - A mutate function that you can call at any time to execute the mutation 1067 + * - An object with fields that represent the current status of the mutation's execution 1068 + * 1069 + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; 1070 + * 1071 + * @example 1072 + * const [disconnectFromDeviceMutation, { data, loading, error }] = useDisconnectFromDeviceMutation({ 1073 + * variables: { 1074 + * id: // value for 'id' 1075 + * }, 1076 + * }); 1077 + */ 1078 + export function useDisconnectFromDeviceMutation(baseOptions?: Apollo.MutationHookOptions<DisconnectFromDeviceMutation, DisconnectFromDeviceMutationVariables>) { 1079 + const options = {...defaultOptions, ...baseOptions} 1080 + return Apollo.useMutation<DisconnectFromDeviceMutation, DisconnectFromDeviceMutationVariables>(DisconnectFromDeviceDocument, options); 1081 + } 1082 + export type DisconnectFromDeviceMutationHookResult = ReturnType<typeof useDisconnectFromDeviceMutation>; 1083 + export type DisconnectFromDeviceMutationResult = Apollo.MutationResult<DisconnectFromDeviceMutation>; 1084 + export type DisconnectFromDeviceMutationOptions = Apollo.BaseMutationOptions<DisconnectFromDeviceMutation, DisconnectFromDeviceMutationVariables>; 1085 + export const GetDevicesDocument = gql` 1086 + query GetDevices { 1087 + devices { 1088 + id 1089 + name 1090 + app 1091 + ip 1092 + host 1093 + port 1094 + isCastDevice 1095 + service 1096 + isConnected 1097 + } 1098 + } 1099 + `; 1100 + 1101 + /** 1102 + * __useGetDevicesQuery__ 1103 + * 1104 + * To run a query within a React component, call `useGetDevicesQuery` and pass it any options that fit your needs. 1105 + * When your component renders, `useGetDevicesQuery` returns an object from Apollo Client that contains loading, error, and data properties 1106 + * you can use to render your UI. 1107 + * 1108 + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; 1109 + * 1110 + * @example 1111 + * const { data, loading, error } = useGetDevicesQuery({ 1112 + * variables: { 1113 + * }, 1114 + * }); 1115 + */ 1116 + export function useGetDevicesQuery(baseOptions?: Apollo.QueryHookOptions<GetDevicesQuery, GetDevicesQueryVariables>) { 1117 + const options = {...defaultOptions, ...baseOptions} 1118 + return Apollo.useQuery<GetDevicesQuery, GetDevicesQueryVariables>(GetDevicesDocument, options); 1119 + } 1120 + export function useGetDevicesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetDevicesQuery, GetDevicesQueryVariables>) { 1121 + const options = {...defaultOptions, ...baseOptions} 1122 + return Apollo.useLazyQuery<GetDevicesQuery, GetDevicesQueryVariables>(GetDevicesDocument, options); 1123 + } 1124 + export function useGetDevicesSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<GetDevicesQuery, GetDevicesQueryVariables>) { 1125 + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} 1126 + return Apollo.useSuspenseQuery<GetDevicesQuery, GetDevicesQueryVariables>(GetDevicesDocument, options); 1127 + } 1128 + export type GetDevicesQueryHookResult = ReturnType<typeof useGetDevicesQuery>; 1129 + export type GetDevicesLazyQueryHookResult = ReturnType<typeof useGetDevicesLazyQuery>; 1130 + export type GetDevicesSuspenseQueryHookResult = ReturnType<typeof useGetDevicesSuspenseQuery>; 1131 + export type GetDevicesQueryResult = Apollo.QueryResult<GetDevicesQuery, GetDevicesQueryVariables>; 1132 + export const GetDeviceDocument = gql` 1133 + query GetDevice($id: String!) { 1134 + device(id: $id) { 1135 + id 1136 + name 1137 + app 1138 + ip 1139 + host 1140 + port 1141 + isCastDevice 1142 + service 1143 + isConnected 1144 + } 1145 + } 1146 + `; 1147 + 1148 + /** 1149 + * __useGetDeviceQuery__ 1150 + * 1151 + * To run a query within a React component, call `useGetDeviceQuery` and pass it any options that fit your needs. 1152 + * When your component renders, `useGetDeviceQuery` returns an object from Apollo Client that contains loading, error, and data properties 1153 + * you can use to render your UI. 1154 + * 1155 + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; 1156 + * 1157 + * @example 1158 + * const { data, loading, error } = useGetDeviceQuery({ 1159 + * variables: { 1160 + * id: // value for 'id' 1161 + * }, 1162 + * }); 1163 + */ 1164 + export function useGetDeviceQuery(baseOptions: Apollo.QueryHookOptions<GetDeviceQuery, GetDeviceQueryVariables> & ({ variables: GetDeviceQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { 1165 + const options = {...defaultOptions, ...baseOptions} 1166 + return Apollo.useQuery<GetDeviceQuery, GetDeviceQueryVariables>(GetDeviceDocument, options); 1167 + } 1168 + export function useGetDeviceLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetDeviceQuery, GetDeviceQueryVariables>) { 1169 + const options = {...defaultOptions, ...baseOptions} 1170 + return Apollo.useLazyQuery<GetDeviceQuery, GetDeviceQueryVariables>(GetDeviceDocument, options); 1171 + } 1172 + export function useGetDeviceSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<GetDeviceQuery, GetDeviceQueryVariables>) { 1173 + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} 1174 + return Apollo.useSuspenseQuery<GetDeviceQuery, GetDeviceQueryVariables>(GetDeviceDocument, options); 1175 + } 1176 + export type GetDeviceQueryHookResult = ReturnType<typeof useGetDeviceQuery>; 1177 + export type GetDeviceLazyQueryHookResult = ReturnType<typeof useGetDeviceLazyQuery>; 1178 + export type GetDeviceSuspenseQueryHookResult = ReturnType<typeof useGetDeviceSuspenseQuery>; 1179 + export type GetDeviceQueryResult = Apollo.QueryResult<GetDeviceQuery, GetDeviceQueryVariables>; 962 1180 export const LikeTrackDocument = gql` 963 1181 mutation LikeTrack($trackId: String!) { 964 1182 likeTrack(id: $trackId)
+16 -5
webui/rockbox/src/Hooks/usePlayQueue.tsx
··· 6 6 import _ from "lodash"; 7 7 import { useRecoilValue } from "recoil"; 8 8 import { controlBarState } from "../Components/ControlBar/ControlBarState"; 9 + import { deviceState } from "../Components/ControlBar/DeviceList/DeviceState"; 9 10 10 11 export const usePlayQueue = () => { 12 + const { currentDevice } = useRecoilValue(deviceState); 11 13 const { resumeIndex } = useRecoilValue(controlBarState); 12 14 const { data: playlistSubscription } = usePlaylistChangedSubscription({ 13 15 fetchPolicy: "network-only", ··· 18 20 const previousTracks = useMemo(() => { 19 21 if (playlistSubscription?.playlistChanged) { 20 22 const currentTrackIndex = 21 - resumeIndex > -1 23 + resumeIndex > -1 && currentDevice === null 22 24 ? resumeIndex 23 25 : _.get(playlistSubscription, "playlistChanged.index", 0); 24 26 const tracks = _.get(playlistSubscription, "playlistChanged.tracks", []); ··· 26 28 ...x, 27 29 id: index.toString(), 28 30 cover: x.albumArt 29 - ? `http://localhost:6062/covers/${x.albumArt}` 31 + ? x.albumArt.startsWith("http") 32 + ? x.albumArt 33 + : `http://localhost:6062/covers/${x.albumArt}` 30 34 : undefined, 31 35 })); 32 36 } ··· 39 43 ...x, 40 44 id: index.toString(), 41 45 cover: x.albumArt 42 - ? `http://localhost:6062/covers/${x.albumArt}` 46 + ? x.albumArt.startsWith("http") 47 + ? x.albumArt 48 + : `http://localhost:6062/covers/${x.albumArt}` 43 49 : undefined, 44 50 })); 51 + // eslint-disable-next-line react-hooks/exhaustive-deps 45 52 }, [data, playlistSubscription, resumeIndex]); 46 53 47 54 const nextTracks = useMemo(() => { ··· 55 62 ...x, 56 63 id: index.toString(), 57 64 cover: x.albumArt 58 - ? `http://localhost:6062/covers/${x.albumArt}` 65 + ? x.albumArt.startsWith("http") 66 + ? x.albumArt 67 + : `http://localhost:6062/covers/${x.albumArt}` 59 68 : undefined, 60 69 })); 61 70 } ··· 68 77 ...x, 69 78 id: index.toString(), 70 79 cover: x.albumArt 71 - ? `http://localhost:6062/covers/${x.albumArt}` 80 + ? x.albumArt.startsWith("http") 81 + ? x.albumArt 82 + : `http://localhost:6062/covers/${x.albumArt}` 72 83 : undefined, 73 84 })); 74 85 }, [data, playlistSubscription, resumeIndex]);