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

webui: update play queue in real time

+1309 -158
+2 -2
crates/graphql/src/schema/mod.rs
··· 2 2 use browse::BrowseQuery; 3 3 use library::LibraryQuery; 4 4 use playback::{PlaybackMutation, PlaybackQuery, PlaybackSubscription}; 5 - use playlist::{PlaylistMutation, PlaylistQuery}; 5 + use playlist::{PlaylistMutation, PlaylistQuery, PlaylistSubscription}; 6 6 use settings::SettingsQuery; 7 7 use sound::{SoundMutation, SoundQuery}; 8 8 use system::SystemQuery; ··· 32 32 pub struct Mutation(PlaybackMutation, PlaylistMutation, SoundMutation); 33 33 34 34 #[derive(MergedSubscription, Default)] 35 - pub struct Subscription(PlaybackSubscription); 35 + pub struct Subscription(PlaybackSubscription, PlaylistSubscription);
+37 -2
crates/graphql/src/schema/objects/playlist.rs
··· 1 - use async_graphql::*; 2 1 use crate::schema::objects::track::Track; 2 + use async_graphql::*; 3 3 use serde::Serialize; 4 4 5 5 #[derive(Default, Clone, Serialize)] 6 6 pub struct Playlist { 7 + pub amount: i32, 8 + pub index: i32, 9 + pub max_playlist_size: i32, 10 + pub first_index: i32, 11 + pub last_insert_pos: i32, 12 + pub seed: i32, 13 + pub last_shuffled_start: i32, 7 14 pub tracks: Vec<Track>, 8 15 } 9 16 10 17 #[Object] 11 18 impl Playlist { 19 + async fn amount(&self) -> i32 { 20 + self.amount 21 + } 22 + 23 + async fn index(&self) -> i32 { 24 + self.index 25 + } 26 + 27 + async fn max_playlist_size(&self) -> i32 { 28 + self.max_playlist_size 29 + } 30 + 31 + async fn first_index(&self) -> i32 { 32 + self.first_index 33 + } 34 + 35 + async fn last_insert_pos(&self) -> i32 { 36 + self.last_insert_pos 37 + } 38 + 39 + async fn seed(&self) -> i32 { 40 + self.seed 41 + } 42 + 43 + async fn last_shuffled_start(&self) -> i32 { 44 + self.last_shuffled_start 45 + } 46 + 12 47 async fn tracks(&self) -> &Vec<Track> { 13 48 &self.tracks 14 49 } 15 - } 50 + }
+56 -3
crates/graphql/src/schema/playlist.rs
··· 1 1 use std::sync::{mpsc::Sender, Arc, Mutex}; 2 2 3 3 use async_graphql::*; 4 + use futures_util::Stream; 4 5 use rockbox_sys::{ 5 6 events::RockboxCommand, 6 7 types::{playlist_amount::PlaylistAmount, playlist_info::PlaylistInfo}, 7 8 }; 8 9 9 - use crate::{rockbox_url, schema::objects::playlist::Playlist}; 10 + use crate::{rockbox_url, schema::objects::playlist::Playlist, simplebroker::SimpleBroker}; 10 11 11 12 #[derive(Default)] 12 13 pub struct PlaylistQuery; ··· 19 20 let response = client.get(&url).send().await?; 20 21 let response = response.json::<PlaylistInfo>().await?; 21 22 Ok(Playlist { 23 + amount: response.amount, 24 + index: response.index, 25 + max_playlist_size: response.max_playlist_size, 26 + first_index: response.first_index, 27 + last_insert_pos: response.last_insert_pos, 28 + seed: response.seed, 29 + last_shuffled_start: response.last_shuffled_start, 22 30 tracks: response.entries.into_iter().map(|t| t.into()).collect(), 23 31 }) 24 32 } ··· 75 83 "set modified".to_string() 76 84 } 77 85 78 - async fn playlist_start(&self, ctx: &Context<'_>) -> Result<i32, Error> { 86 + async fn playlist_start( 87 + &self, 88 + ctx: &Context<'_>, 89 + start_index: Option<i32>, 90 + elapsed: Option<i32>, 91 + offset: Option<i32>, 92 + ) -> Result<i32, Error> { 79 93 let client = ctx.data::<reqwest::Client>().unwrap(); 80 - let url = format!("{}/playlists/start", rockbox_url()); 94 + let mut url = format!("{}/playlists/start", rockbox_url()); 95 + 96 + if let Some(start_index) = start_index { 97 + url = format!("{}?start_index={}", url, start_index); 98 + } 99 + 100 + if let Some(elapsed) = elapsed { 101 + url = match url.contains("?") { 102 + true => format!("{}&elapsed={}", url, elapsed), 103 + false => format!("{}?elapsed={}", url, elapsed), 104 + }; 105 + } 106 + 107 + if let Some(offset) = offset { 108 + url = match url.contains("?") { 109 + true => format!("{}&offset={}", url, offset), 110 + false => format!("{}?offset={}", url, offset), 111 + }; 112 + } 113 + 81 114 client.put(&url).send().await?; 115 + Ok(0) 116 + } 117 + 118 + async fn playlist_remove_track(&self, ctx: &Context<'_>, index: i32) -> Result<i32, Error> { 119 + let client = ctx.data::<reqwest::Client>().unwrap(); 120 + let url = format!("{}/playlists/current/tracks", rockbox_url()); 121 + let body = serde_json::json!({ 122 + "positions": vec![index], 123 + }); 124 + client.delete(&url).json(&body).send().await?; 82 125 Ok(0) 83 126 } 84 127 ··· 167 210 Ok(ret) 168 211 } 169 212 } 213 + 214 + #[derive(Default)] 215 + pub struct PlaylistSubscription; 216 + 217 + #[Subscription] 218 + impl PlaylistSubscription { 219 + async fn playlist_changed(&self) -> impl Stream<Item = Playlist> { 220 + SimpleBroker::<Playlist>::subscribe() 221 + } 222 + }
+13 -2
crates/rpc/proto/rockbox/v1alpha1/playlist.proto
··· 7 7 message GetCurrentRequest {} 8 8 9 9 message GetCurrentResponse { 10 - repeated rockbox.v1alpha1.CurrentTrackResponse tracks = 1; 10 + int32 index = 1; 11 + int32 amount = 2; 12 + int32 max_playlist_size = 3; 13 + int32 first_index = 4; 14 + int32 last_insert_pos = 5; 15 + int32 seed = 6; 16 + int32 last_shuffled_start = 7; 17 + repeated rockbox.v1alpha1.CurrentTrackResponse tracks = 8; 11 18 } 12 19 13 20 message GetResumeInfoRequest {} ··· 49 56 50 57 message SetModifiedResponse {} 51 58 52 - message StartRequest {} 59 + message StartRequest { 60 + optional int32 start_index = 1; 61 + optional int32 elapsed = 2; 62 + optional int32 offset = 3; 63 + } 53 64 54 65 message StartResponse {} 55 66
+23 -2
crates/rpc/src/api/rockbox.v1alpha1.rs
··· 2611 2611 pub struct GetCurrentRequest {} 2612 2612 #[derive(Clone, PartialEq, ::prost::Message)] 2613 2613 pub struct GetCurrentResponse { 2614 - #[prost(message, repeated, tag = "1")] 2614 + #[prost(int32, tag = "1")] 2615 + pub index: i32, 2616 + #[prost(int32, tag = "2")] 2617 + pub amount: i32, 2618 + #[prost(int32, tag = "3")] 2619 + pub max_playlist_size: i32, 2620 + #[prost(int32, tag = "4")] 2621 + pub first_index: i32, 2622 + #[prost(int32, tag = "5")] 2623 + pub last_insert_pos: i32, 2624 + #[prost(int32, tag = "6")] 2625 + pub seed: i32, 2626 + #[prost(int32, tag = "7")] 2627 + pub last_shuffled_start: i32, 2628 + #[prost(message, repeated, tag = "8")] 2615 2629 pub tracks: ::prost::alloc::vec::Vec<CurrentTrackResponse>, 2616 2630 } 2617 2631 #[derive(Clone, Copy, PartialEq, ::prost::Message)] ··· 2659 2673 #[derive(Clone, Copy, PartialEq, ::prost::Message)] 2660 2674 pub struct SetModifiedResponse {} 2661 2675 #[derive(Clone, Copy, PartialEq, ::prost::Message)] 2662 - pub struct StartRequest {} 2676 + pub struct StartRequest { 2677 + #[prost(int32, optional, tag = "1")] 2678 + pub start_index: ::core::option::Option<i32>, 2679 + #[prost(int32, optional, tag = "2")] 2680 + pub elapsed: ::core::option::Option<i32>, 2681 + #[prost(int32, optional, tag = "3")] 2682 + pub offset: ::core::option::Option<i32>, 2683 + } 2663 2684 #[derive(Clone, Copy, PartialEq, ::prost::Message)] 2664 2685 pub struct StartResponse {} 2665 2686 #[derive(Clone, Copy, PartialEq, ::prost::Message)]
crates/rpc/src/api/rockbox_descriptor.bin

This is a binary file and will not be displayed.

+33 -3
crates/rpc/src/playlist.rs
··· 43 43 .iter() 44 44 .map(|track| CurrentTrackResponse::from(track.clone())) 45 45 .collect::<Vec<CurrentTrackResponse>>(); 46 - Ok(tonic::Response::new(GetCurrentResponse { tracks })) 46 + Ok(tonic::Response::new(GetCurrentResponse { 47 + index: data.index, 48 + amount: data.amount, 49 + max_playlist_size: data.max_playlist_size, 50 + first_index: data.first_index, 51 + last_insert_pos: data.last_insert_pos, 52 + seed: data.seed, 53 + last_shuffled_start: data.last_shuffled_start, 54 + tracks, 55 + })) 47 56 } 48 57 49 58 async fn get_resume_info( ··· 127 136 128 137 async fn start( 129 138 &self, 130 - _request: tonic::Request<StartRequest>, 139 + request: tonic::Request<StartRequest>, 131 140 ) -> Result<tonic::Response<StartResponse>, tonic::Status> { 132 - let url = format!("{}/playlists/start", rockbox_url()); 141 + let request = request.into_inner(); 142 + 143 + let mut url = format!("{}/playlists/start", rockbox_url()); 144 + 145 + if let Some(start_index) = request.start_index { 146 + url = format!("{}?start_index={}", url, start_index); 147 + } 148 + 149 + if let Some(elapsed) = request.elapsed { 150 + url = match url.contains("?") { 151 + true => format!("{}&elapsed={}", url, elapsed), 152 + false => format!("{}?elapsed={}", url, elapsed), 153 + }; 154 + } 155 + 156 + if let Some(offset) = request.offset { 157 + url = match url.contains("?") { 158 + true => format!("{}&offset={}", url, offset), 159 + false => format!("{}?offset={}", url, offset), 160 + }; 161 + } 162 + 133 163 self.client 134 164 .put(&url) 135 165 .send()
+57
crates/server/openapi.json
··· 903 903 ] 904 904 } 905 905 }, 906 + "/playlists/current": { 907 + "get": { 908 + "summary": "Get Playlist Details", 909 + "tags": [ 910 + "Playlists" 911 + ], 912 + "responses": { 913 + "200": { 914 + "description": "OK", 915 + "content": { 916 + "application/json": { 917 + "schema": { 918 + "type": "object", 919 + "properties": { 920 + "amount": { 921 + "type": "integer" 922 + }, 923 + "index": { 924 + "type": "integer" 925 + }, 926 + "max_playlist_size": { 927 + "type": "integer" 928 + }, 929 + "first_index": { 930 + "type": "integer" 931 + }, 932 + "last_insert_pos": { 933 + "type": "integer" 934 + }, 935 + "seed": { 936 + "type": "integer" 937 + }, 938 + "last_shuffled_start": { 939 + "type": "integer" 940 + }, 941 + "tracks": { 942 + "type": "array", 943 + "items": { 944 + "$ref": "#/components/schemas/Track.v1", 945 + "x-stoplight": { 946 + "id": "gz6dzxyv1j0ws" 947 + } 948 + } 949 + } 950 + } 951 + } 952 + } 953 + } 954 + } 955 + } 956 + }, 957 + "operationId": "get-current-playlist", 958 + "description": "", 959 + "requestBody": { 960 + "content": {} 961 + } 962 + }, 906 963 "/playlists/current/tracks": { 907 964 "get": { 908 965 "summary": "Get Playlist Tracks",
+2
crates/server/src/handlers/mod.rs
··· 44 44 async_handler!(player, next); 45 45 async_handler!(player, previous); 46 46 async_handler!(player, stop); 47 + async_handler!(player, get_file_position); 47 48 async_handler!(playlists, create_playlist); 48 49 async_handler!(playlists, start_playlist); 49 50 async_handler!(playlists, shuffle_playlist); ··· 53 54 async_handler!(playlists, get_playlist_tracks); 54 55 async_handler!(playlists, insert_tracks); 55 56 async_handler!(playlists, remove_tracks); 57 + async_handler!(playlists, get_playlist); 56 58 async_handler!(tracks, get_tracks); 57 59 async_handler!(tracks, get_track); 58 60 async_handler!(system, get_rockbox_version);
+28 -20
crates/server/src/handlers/player.rs
··· 3 3 use rockbox_sys as rb; 4 4 5 5 pub async fn play(_ctx: &Context, req: &Request, _res: &mut Response) -> Result<(), Error> { 6 - let elapsed = req 7 - .query_params 8 - .get("elapsed") 9 - .unwrap() 10 - .as_i64() 11 - .unwrap_or(0); 12 - let offset = req 13 - .query_params 14 - .get("offset") 15 - .unwrap() 16 - .as_i64() 17 - .unwrap_or(0); 6 + let elapsed = match req.query_params.get("elapsed") { 7 + Some(elapsed) => elapsed.as_str().unwrap_or("0").parse().unwrap_or(0), 8 + None => 0, 9 + }; 10 + let offset = match req.query_params.get("offset") { 11 + Some(offset) => offset.as_str().unwrap_or("0").parse().unwrap_or(0), 12 + None => 0, 13 + }; 18 14 rb::playback::play(elapsed, offset); 19 15 Ok(()) 20 16 } ··· 25 21 } 26 22 27 23 pub async fn ff_rewind(_ctx: &Context, req: &Request, _res: &mut Response) -> Result<(), Error> { 28 - let newtime = req 29 - .query_params 30 - .get("newtime") 31 - .unwrap() 32 - .as_i64() 33 - .unwrap_or(0); 34 - rb::playback::ff_rewind(newtime as i32); 24 + let newtime = match req.query_params.get("newtime") { 25 + Some(newtime) => newtime.as_str().unwrap_or("0").parse().unwrap_or(0), 26 + None => 0, 27 + }; 28 + rb::playback::ff_rewind(newtime); 35 29 Ok(()) 36 30 } 37 31 ··· 41 35 Ok(()) 42 36 } 43 37 44 - pub async fn current_track(_ctx: &Context, _req: &Request, res: &mut Response) -> Result<(), Error> { 38 + pub async fn current_track( 39 + _ctx: &Context, 40 + _req: &Request, 41 + res: &mut Response, 42 + ) -> Result<(), Error> { 45 43 let track = rb::playback::current_track(); 46 44 res.json(&track); 47 45 Ok(()) ··· 81 79 rb::playback::hard_stop(); 82 80 Ok(()) 83 81 } 82 + 83 + pub async fn get_file_position( 84 + _ctx: &Context, 85 + _req: &Request, 86 + res: &mut Response, 87 + ) -> Result<(), Error> { 88 + let position = rb::playback::get_file_pos(); 89 + res.json(&position); 90 + Ok(()) 91 + }
+42 -26
crates/server/src/handlers/playlists.rs
··· 43 43 req: &Request, 44 44 _res: &mut Response, 45 45 ) -> Result<(), Error> { 46 - let start_index = req 47 - .query_params 48 - .get("start_index") 49 - .unwrap() 50 - .as_i64() 51 - .unwrap_or(0); 52 - let elapsed = req 53 - .query_params 54 - .get("elapsed") 55 - .unwrap() 56 - .as_i64() 57 - .unwrap_or(0); 58 - let offset = req 59 - .query_params 60 - .get("offset") 61 - .unwrap() 62 - .as_i64() 63 - .unwrap_or(0); 64 - rb::playlist::start(start_index as i32, elapsed as u64, offset as u64); 46 + let start_index = match req.query_params.get("start_index") { 47 + Some(start_index) => start_index.as_str().unwrap_or("0").parse().unwrap_or(0), 48 + None => 0, 49 + }; 50 + let elapsed = match req.query_params.get("elapsed") { 51 + Some(elapsed) => elapsed.as_str().unwrap_or("0").parse().unwrap_or(0), 52 + None => 0, 53 + }; 54 + let offset = match req.query_params.get("offset") { 55 + Some(offset) => offset.as_str().unwrap_or("0").parse().unwrap_or(0), 56 + None => 0, 57 + }; 58 + rb::playlist::start(start_index, elapsed, offset); 65 59 Ok(()) 66 60 } 67 61 ··· 70 64 req: &Request, 71 65 res: &mut Response, 72 66 ) -> Result<(), Error> { 73 - let start_index = req 74 - .query_params 75 - .get("start_index") 76 - .unwrap() 77 - .as_i64() 78 - .unwrap_or(0); 67 + let start_index = match req.query_params.get("start_index") { 68 + Some(start_index) => start_index.as_str().unwrap_or("0").parse().unwrap_or(0), 69 + None => 0, 70 + }; 79 71 let seed = rb::system::current_tick(); 80 72 let ret = rb::playlist::shuffle(seed as i32, start_index as i32); 81 73 res.text(&ret.to_string()); ··· 191 183 192 184 pub async fn remove_tracks(_ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { 193 185 let req_body = req.body.as_ref().unwrap(); 194 - let params = serde_json::from_str::<DeleteTracks>(&req_body).unwrap(); 186 + let params = serde_json::from_str::<DeleteTracks>(&req_body)?; 195 187 let mut ret = 0; 196 188 197 189 for position in &params.positions { ··· 226 218 227 219 Ok(()) 228 220 } 221 + 222 + pub async fn get_playlist(_ctx: &Context, _req: &Request, res: &mut Response) -> Result<(), Error> { 223 + let mut result = rb::playlist::get_current(); 224 + let mut entries = vec![]; 225 + let amount = rb::playlist::amount(); 226 + 227 + for i in 0..amount { 228 + let info = rb::playlist::get_track_info(i); 229 + let entry = rb::metadata::get_metadata(-1, &info.filename); 230 + entries.push(entry); 231 + } 232 + 233 + result.amount = amount; 234 + result.max_playlist_size = rb::playlist::max_playlist_size(); 235 + result.index = rb::playlist::index(); 236 + result.first_index = rb::playlist::first_index(); 237 + result.last_insert_pos = rb::playlist::last_insert_pos(); 238 + result.seed = rb::playlist::seed(); 239 + result.last_shuffled_start = rb::playlist::last_shuffled_start(); 240 + result.entries = entries; 241 + 242 + res.json(&result); 243 + Ok(()) 244 + }
+39 -1
crates/server/src/lib.rs
··· 2 2 3 3 use http::RockboxHttpServer; 4 4 use rockbox_graphql::{ 5 - schema::objects::{audio_status::AudioStatus, track::Track}, 5 + schema::objects::{self, audio_status::AudioStatus, track::Track}, 6 6 simplebroker::SimpleBroker, 7 7 }; 8 8 use rockbox_library::repo; ··· 57 57 app.put("/player/next", next); 58 58 app.put("/player/previous", previous); 59 59 app.put("/player/stop", stop); 60 + app.get("/player/file-position", get_file_position); 60 61 61 62 app.post("/playlists", create_playlist); 62 63 app.put("/playlists/start", start_playlist); ··· 67 68 app.get("/playlists/:id/tracks", get_playlist_tracks); 68 69 app.post("/playlists/:id/tracks", insert_tracks); 69 70 app.delete("/playlists/:id/tracks", remove_tracks); 71 + app.get("/playlists/:id", get_playlist); 70 72 71 73 app.get("/tracks", get_tracks); 72 74 app.get("/tracks/:id", get_track); ··· 212 214 } 213 215 None => {} 214 216 }; 217 + 218 + let mut current_playlist = rb::playlist::get_current(); 219 + let amount = rb::playlist::amount(); 220 + 221 + let mut entries = vec![]; 222 + 223 + for i in 0..amount { 224 + let info = rb::playlist::get_track_info(i); 225 + let entry = rb::metadata::get_metadata(-1, &info.filename); 226 + entries.push(entry); 227 + } 228 + 229 + current_playlist.amount = amount; 230 + current_playlist.max_playlist_size = rb::playlist::max_playlist_size(); 231 + current_playlist.index = rb::playlist::index(); 232 + current_playlist.first_index = rb::playlist::first_index(); 233 + current_playlist.last_insert_pos = rb::playlist::last_insert_pos(); 234 + current_playlist.seed = rb::playlist::seed(); 235 + current_playlist.last_shuffled_start = rb::playlist::last_shuffled_start(); 236 + current_playlist.entries = entries; 237 + 238 + SimpleBroker::publish(objects::playlist::Playlist { 239 + amount: current_playlist.amount, 240 + index: current_playlist.index, 241 + max_playlist_size: current_playlist.max_playlist_size, 242 + first_index: current_playlist.first_index, 243 + last_insert_pos: current_playlist.last_insert_pos, 244 + seed: current_playlist.seed, 245 + last_shuffled_start: current_playlist.last_shuffled_start, 246 + tracks: current_playlist 247 + .entries 248 + .into_iter() 249 + .map(|t| t.into()) 250 + .collect(), 251 + }); 252 + 215 253 thread::sleep(std::time::Duration::from_millis(100)); 216 254 rb::system::sleep(rb::HZ); 217 255 }
+18 -12
crates/sys/src/lib.rs
··· 148 148 pub struct PlaylistInfo { 149 149 pub utf8: bool, // bool utf8 150 150 pub control_created: bool, // bool control_created 151 - pub flags: c_uint, // unsigned int flags 152 - pub fd: c_int, // int fd 153 - pub control_fd: c_int, // int control_fd 154 - pub max_playlist_size: c_int, // int max_playlist_size 151 + pub flags: u32, // unsigned int flags 152 + pub fd: i32, // int fd 153 + pub control_fd: i32, // int control_fd 154 + pub max_playlist_size: i32, // int max_playlist_size 155 155 pub indices: [c_ulong; 200], // unsigned long* indices 156 - pub index: c_int, // int index 157 - pub first_index: c_int, // int first_index 158 - pub amount: c_int, // int amount 159 - pub last_insert_pos: c_int, // int last_insert_pos 156 + pub index: i32, // int index 157 + pub first_index: i32, // int first_index 158 + pub amount: i32, // int amount 159 + pub last_insert_pos: i32, // int last_insert_pos 160 160 pub started: bool, // bool started 161 - pub last_shuffled_start: c_int, // int last_shuffled_start 162 - pub seed: c_int, // int seed 161 + pub last_shuffled_start: i32, // int last_shuffled_start 162 + pub seed: i32, // int seed 163 163 pub mutex: *mut c_void, // struct mutex (convert to a void pointer for FFI) 164 - pub dirlen: c_int, // int dirlen 164 + pub dirlen: i32, // int dirlen 165 165 pub filename: [c_uchar; MAX_PATH], // char filename[MAX_PATH] 166 166 pub control_filename: 167 167 [c_uchar; std::mem::size_of::<[u8; PLAYLIST_CONTROL_FILE.len() + 100 + 8]>()], // char control_filename[sizeof(PLAYLIST_CONTROL_FILE) + 8] 168 - pub dcfrefs_handle: c_int, // int dcfrefs_handle 168 + pub dcfrefs_handle: i32, // int dcfrefs_handle 169 169 } 170 170 171 171 #[repr(C)] ··· 1095 1095 queue: c_uchar, 1096 1096 ) -> c_int; 1097 1097 fn playlist_shuffle(random_seed: c_int, start_index: c_int) -> c_int; 1098 + fn rb_playlist_index() -> i32; 1099 + fn rb_playlist_first_index() -> i32; 1100 + fn rb_playlist_last_insert_pos() -> i32; 1101 + fn rb_playlist_seed() -> i32; 1102 + fn rb_playlist_last_shuffled_start() -> i32; 1103 + fn rb_max_playlist_size() -> i32; 1098 1104 fn warn_on_pl_erase() -> c_uchar; 1099 1105 1100 1106 // Sound
+24
crates/sys/src/playlist.rs
··· 108 108 pub fn delete_track(index: i32) -> i32 { 109 109 unsafe { crate::rb_playlist_delete_track(index) } 110 110 } 111 + 112 + pub fn index() -> i32 { 113 + unsafe { crate::rb_playlist_index() } 114 + } 115 + 116 + pub fn first_index() -> i32 { 117 + unsafe { crate::rb_playlist_first_index() } 118 + } 119 + 120 + pub fn last_insert_pos() -> i32 { 121 + unsafe { crate::rb_playlist_last_insert_pos() } 122 + } 123 + 124 + pub fn seed() -> i32 { 125 + unsafe { crate::rb_playlist_seed() } 126 + } 127 + 128 + pub fn last_shuffled_start() -> i32 { 129 + unsafe { crate::rb_playlist_last_shuffled_start() } 130 + } 131 + 132 + pub fn max_playlist_size() -> i32 { 133 + unsafe { crate::rb_max_playlist_size() } 134 + }
docs/rockbox-ui.png

This is a binary file and will not be displayed.

+24
src/main.zig
··· 74 74 export fn rb_playlist_remove_all_tracks() c_int { 75 75 return playlist.remove_all_tracks(); 76 76 } 77 + 78 + export fn rb_playlist_index() c_int { 79 + return playlist.playlist_index(); 80 + } 81 + 82 + export fn rb_playlist_first_index() c_int { 83 + return playlist.playlist_first_index(); 84 + } 85 + 86 + export fn rb_playlist_last_insert_pos() c_int { 87 + return playlist.playlist_last_insert_pos(); 88 + } 89 + 90 + export fn rb_playlist_seed() c_int { 91 + return playlist.playlist_seed(); 92 + } 93 + 94 + export fn rb_playlist_last_shuffled_start() c_int { 95 + return playlist.playlist_last_shuffled_start(); 96 + } 97 + 98 + export fn rb_max_playlist_size() c_int { 99 + return playlist.max_playlist_size(); 100 + }
+30
src/rockbox/playlist.zig
··· 105 105 const playlist = playlist_get_current(); 106 106 return playlist_remove_all_tracks(playlist); 107 107 } 108 + 109 + pub fn playlist_index() c_int { 110 + const playlist = playlist_get_current(); 111 + return playlist.index; 112 + } 113 + 114 + pub fn playlist_first_index() c_int { 115 + const playlist = playlist_get_current(); 116 + return playlist.first_index; 117 + } 118 + 119 + pub fn playlist_last_insert_pos() c_int { 120 + const playlist = playlist_get_current(); 121 + return playlist.last_insert_pos; 122 + } 123 + 124 + pub fn playlist_seed() c_int { 125 + const playlist = playlist_get_current(); 126 + return playlist.seed; 127 + } 128 + 129 + pub fn playlist_last_shuffled_start() c_int { 130 + const playlist = playlist_get_current(); 131 + return playlist.last_shuffled_start; 132 + } 133 + 134 + pub fn max_playlist_size() c_int { 135 + const playlist = playlist_get_current(); 136 + return playlist.max_playlist_size; 137 + }
+199 -1
webui/rockbox/graphql.schema.json
··· 1068 1068 "deprecationReason": null 1069 1069 }, 1070 1070 { 1071 + "name": "playlistRemoveTrack", 1072 + "description": null, 1073 + "args": [ 1074 + { 1075 + "name": "index", 1076 + "description": null, 1077 + "type": { 1078 + "kind": "NON_NULL", 1079 + "name": null, 1080 + "ofType": { 1081 + "kind": "SCALAR", 1082 + "name": "Int", 1083 + "ofType": null 1084 + } 1085 + }, 1086 + "defaultValue": null, 1087 + "isDeprecated": false, 1088 + "deprecationReason": null 1089 + } 1090 + ], 1091 + "type": { 1092 + "kind": "NON_NULL", 1093 + "name": null, 1094 + "ofType": { 1095 + "kind": "SCALAR", 1096 + "name": "Int", 1097 + "ofType": null 1098 + } 1099 + }, 1100 + "isDeprecated": false, 1101 + "deprecationReason": null 1102 + }, 1103 + { 1071 1104 "name": "playlistResume", 1072 1105 "description": null, 1073 1106 "args": [], ··· 1102 1135 { 1103 1136 "name": "playlistStart", 1104 1137 "description": null, 1105 - "args": [], 1138 + "args": [ 1139 + { 1140 + "name": "elapsed", 1141 + "description": null, 1142 + "type": { 1143 + "kind": "SCALAR", 1144 + "name": "Int", 1145 + "ofType": null 1146 + }, 1147 + "defaultValue": null, 1148 + "isDeprecated": false, 1149 + "deprecationReason": null 1150 + }, 1151 + { 1152 + "name": "offset", 1153 + "description": null, 1154 + "type": { 1155 + "kind": "SCALAR", 1156 + "name": "Int", 1157 + "ofType": null 1158 + }, 1159 + "defaultValue": null, 1160 + "isDeprecated": false, 1161 + "deprecationReason": null 1162 + }, 1163 + { 1164 + "name": "startIndex", 1165 + "description": null, 1166 + "type": { 1167 + "kind": "SCALAR", 1168 + "name": "Int", 1169 + "ofType": null 1170 + }, 1171 + "defaultValue": null, 1172 + "isDeprecated": false, 1173 + "deprecationReason": null 1174 + } 1175 + ], 1106 1176 "type": { 1107 1177 "kind": "NON_NULL", 1108 1178 "name": null, ··· 1302 1372 "name": "Playlist", 1303 1373 "description": null, 1304 1374 "fields": [ 1375 + { 1376 + "name": "amount", 1377 + "description": null, 1378 + "args": [], 1379 + "type": { 1380 + "kind": "NON_NULL", 1381 + "name": null, 1382 + "ofType": { 1383 + "kind": "SCALAR", 1384 + "name": "Int", 1385 + "ofType": null 1386 + } 1387 + }, 1388 + "isDeprecated": false, 1389 + "deprecationReason": null 1390 + }, 1391 + { 1392 + "name": "firstIndex", 1393 + "description": null, 1394 + "args": [], 1395 + "type": { 1396 + "kind": "NON_NULL", 1397 + "name": null, 1398 + "ofType": { 1399 + "kind": "SCALAR", 1400 + "name": "Int", 1401 + "ofType": null 1402 + } 1403 + }, 1404 + "isDeprecated": false, 1405 + "deprecationReason": null 1406 + }, 1407 + { 1408 + "name": "index", 1409 + "description": null, 1410 + "args": [], 1411 + "type": { 1412 + "kind": "NON_NULL", 1413 + "name": null, 1414 + "ofType": { 1415 + "kind": "SCALAR", 1416 + "name": "Int", 1417 + "ofType": null 1418 + } 1419 + }, 1420 + "isDeprecated": false, 1421 + "deprecationReason": null 1422 + }, 1423 + { 1424 + "name": "lastInsertPos", 1425 + "description": null, 1426 + "args": [], 1427 + "type": { 1428 + "kind": "NON_NULL", 1429 + "name": null, 1430 + "ofType": { 1431 + "kind": "SCALAR", 1432 + "name": "Int", 1433 + "ofType": null 1434 + } 1435 + }, 1436 + "isDeprecated": false, 1437 + "deprecationReason": null 1438 + }, 1439 + { 1440 + "name": "lastShuffledStart", 1441 + "description": null, 1442 + "args": [], 1443 + "type": { 1444 + "kind": "NON_NULL", 1445 + "name": null, 1446 + "ofType": { 1447 + "kind": "SCALAR", 1448 + "name": "Int", 1449 + "ofType": null 1450 + } 1451 + }, 1452 + "isDeprecated": false, 1453 + "deprecationReason": null 1454 + }, 1455 + { 1456 + "name": "maxPlaylistSize", 1457 + "description": null, 1458 + "args": [], 1459 + "type": { 1460 + "kind": "NON_NULL", 1461 + "name": null, 1462 + "ofType": { 1463 + "kind": "SCALAR", 1464 + "name": "Int", 1465 + "ofType": null 1466 + } 1467 + }, 1468 + "isDeprecated": false, 1469 + "deprecationReason": null 1470 + }, 1471 + { 1472 + "name": "seed", 1473 + "description": null, 1474 + "args": [], 1475 + "type": { 1476 + "kind": "NON_NULL", 1477 + "name": null, 1478 + "ofType": { 1479 + "kind": "SCALAR", 1480 + "name": "Int", 1481 + "ofType": null 1482 + } 1483 + }, 1484 + "isDeprecated": false, 1485 + "deprecationReason": null 1486 + }, 1305 1487 { 1306 1488 "name": "tracks", 1307 1489 "description": null, ··· 1903 2085 "ofType": { 1904 2086 "kind": "OBJECT", 1905 2087 "name": "AudioStatus", 2088 + "ofType": null 2089 + } 2090 + }, 2091 + "isDeprecated": false, 2092 + "deprecationReason": null 2093 + }, 2094 + { 2095 + "name": "playlistChanged", 2096 + "description": null, 2097 + "args": [], 2098 + "type": { 2099 + "kind": "NON_NULL", 2100 + "name": null, 2101 + "ofType": { 2102 + "kind": "OBJECT", 2103 + "name": "Playlist", 1906 2104 "ofType": null 1907 2105 } 1908 2106 },
+3 -3
webui/rockbox/src/Components/AlbumDetails/AlbumDetailsWithData.tsx
··· 11 11 const { formatTime } = useTimeFormat(); 12 12 const navigate = useNavigate(); 13 13 const { id } = useParams<{ id: string }>(); 14 - const { data, loading, refetch } = useGetAlbumQuery({ 14 + const { data, refetch } = useGetAlbumQuery({ 15 15 variables: { 16 16 id: id!, 17 17 }, ··· 31 31 ); 32 32 33 33 useEffect(() => { 34 - if (loading || !album) { 34 + if (!album) { 35 35 return; 36 36 } 37 37 setTracks( ··· 46 46 })) || [] 47 47 ); 48 48 // eslint-disable-next-line react-hooks/exhaustive-deps 49 - }, [loading, album]); 49 + }, [album]); 50 50 51 51 useEffect(() => { 52 52 refetch();
+8 -1
webui/rockbox/src/Components/AlbumDetails/__snapshots__/AlbumDetails.test.tsx.snap
··· 8 8 <div 9 9 class="css-1fkc5kv" 10 10 > 11 + <img 12 + alt="Rockbox" 13 + src="/src/Assets/rockbox-icon.svg" 14 + style="width: 40px; margin-bottom: 20px; margin-left: 12px;" 15 + /> 11 16 <a 12 17 class="css-8v05qj" 13 18 color="#fe099c" ··· 130 135 class="css-v8cdaf" 131 136 > 132 137 <div 133 - class="css-1dlrxtg" 138 + class="css-14bvc2y" 134 139 > 135 140 <div 136 141 class="css-1h69jy4" ··· 257 262 class="css-nb4c1y" 258 263 > 259 264 <button 265 + aria-expanded="false" 266 + aria-haspopup="true" 260 267 class="css-10asy2d" 261 268 > 262 269 <svg
+8 -1
webui/rockbox/src/Components/Albums/__snapshots__/Albums.test.tsx.snap
··· 11 11 <div 12 12 class="css-1fkc5kv" 13 13 > 14 + <img 15 + alt="Rockbox" 16 + src="/src/Assets/rockbox-icon.svg" 17 + style="width: 40px; margin-bottom: 20px; margin-left: 12px;" 18 + /> 14 19 <a 15 20 class="css-8v05qj" 16 21 color="#fe099c" ··· 133 138 class="css-v8cdaf" 134 139 > 135 140 <div 136 - class="css-1dlrxtg" 141 + class="css-14bvc2y" 137 142 > 138 143 <div 139 144 class="css-1h69jy4" ··· 260 265 class="css-nb4c1y" 261 266 > 262 267 <button 268 + aria-expanded="false" 269 + aria-haspopup="true" 263 270 class="css-10asy2d" 264 271 > 265 272 <svg
+8 -1
webui/rockbox/src/Components/ArtistDetails/__snapshots__/ArtistDetails.test.tsx.snap
··· 11 11 <div 12 12 class="css-1fkc5kv" 13 13 > 14 + <img 15 + alt="Rockbox" 16 + src="/src/Assets/rockbox-icon.svg" 17 + style="width: 40px; margin-bottom: 20px; margin-left: 12px;" 18 + /> 14 19 <a 15 20 class="css-tujm8o" 16 21 color="initial" ··· 133 138 class="css-v8cdaf" 134 139 > 135 140 <div 136 - class="css-1dlrxtg" 141 + class="css-14bvc2y" 137 142 > 138 143 <div 139 144 class="css-1h69jy4" ··· 260 265 class="css-nb4c1y" 261 266 > 262 267 <button 268 + aria-expanded="false" 269 + aria-haspopup="true" 263 270 class="css-10asy2d" 264 271 > 265 272 <svg
+8 -1
webui/rockbox/src/Components/Artists/__snapshots__/Artists.test.tsx.snap
··· 11 11 <div 12 12 class="css-1fkc5kv" 13 13 > 14 + <img 15 + alt="Rockbox" 16 + src="/src/Assets/rockbox-icon.svg" 17 + style="width: 40px; margin-bottom: 20px; margin-left: 12px;" 18 + /> 14 19 <a 15 20 class="css-tujm8o" 16 21 color="initial" ··· 133 138 class="css-v8cdaf" 134 139 > 135 140 <div 136 - class="css-1dlrxtg" 141 + class="css-14bvc2y" 137 142 > 138 143 <div 139 144 class="css-1h69jy4" ··· 260 265 class="css-nb4c1y" 261 266 > 262 267 <button 268 + aria-expanded="false" 269 + aria-haspopup="true" 263 270 class="css-10asy2d" 264 271 > 265 272 <svg
+5 -1
webui/rockbox/src/Components/ControlBar/ControlBarState.tsx
··· 1 1 import { atom } from "recoil"; 2 - import { CurrentTrack } from "../../Types/track"; 2 + import { CurrentTrack, Track } from "../../Types/track"; 3 3 4 4 export const controlBarState = atom<{ 5 5 nowPlaying?: CurrentTrack; 6 6 locked?: boolean; 7 + previousTracks?: Track[]; 8 + nextTracks?: Track[]; 7 9 }>({ 8 10 key: "controlBarState", 9 11 default: { 10 12 nowPlaying: undefined, 11 13 locked: false, 14 + previousTracks: [], 15 + nextTracks: [], 12 16 }, 13 17 });
+25 -4
webui/rockbox/src/Components/ControlBar/ControlBarWithData.tsx
··· 14 14 import _ from "lodash"; 15 15 import { useRecoilState } from "recoil"; 16 16 import { controlBarState } from "./ControlBarState"; 17 + import { usePlayQueue } from "../../Hooks/usePlayQueue"; 17 18 18 19 const ControlBarWithData: FC = () => { 19 20 const [{ nowPlaying, locked }, setControlBarState] = ··· 28 29 const [next] = useNextMutation(); 29 30 const { data: playbackSubscription } = useCurrentlyPlayingSongSubscription(); 30 31 const { data: playbackStatus } = usePlaybackStatusSubscription(); 32 + const { previousTracks, nextTracks } = usePlayQueue(); 31 33 32 34 const setNowPlaying = (nowPlaying: CurrentTrack) => { 33 35 setControlBarState((state) => ({ ··· 37 39 }; 38 40 39 41 useEffect(() => { 42 + setControlBarState((state) => ({ 43 + ...state, 44 + nextTracks, 45 + previousTracks, 46 + })); 47 + // eslint-disable-next-line react-hooks/exhaustive-deps 48 + }, [nextTracks, previousTracks]); 49 + 50 + useEffect(() => { 40 51 if (_.get(playbackSubscription, "currentlyPlayingSong.length", 0) > 0) { 41 52 const currentSong = playbackSubscription?.currentlyPlayingSong; 42 53 setNowPlaying({ ··· 50 61 : "", 51 62 duration: currentSong?.length || 0, 52 63 progress: currentSong?.elapsed || 0, 53 - isPlaying: playbackStatus?.playbackStatus.status === 1 && !locked, 64 + isPlaying: !locked 65 + ? playbackStatus?.playbackStatus.status === 1 66 + : nowPlaying?.isPlaying, 54 67 albumId: currentSong?.albumId, 55 68 }); 56 69 } ··· 86 99 const onPlay = () => { 87 100 setControlBarState((state) => ({ 88 101 ...state, 102 + nowPlaying: { 103 + ...nowPlaying!, 104 + isPlaying: true, 105 + }, 89 106 locked: true, 90 107 })); 91 108 resume(); ··· 94 111 ...state, 95 112 locked: false, 96 113 })); 97 - }, 2000); 114 + }, 3000); 98 115 }; 99 116 100 117 const onPause = () => { 101 118 setControlBarState((state) => ({ 102 119 ...state, 120 + nowPlaying: { 121 + ...nowPlaying!, 122 + isPlaying: false, 123 + }, 103 124 locked: true, 104 125 })); 105 126 pause(); 106 127 setTimeout(() => { 107 128 setControlBarState((state) => ({ 108 129 ...state, 109 - locked: false, 130 + locked: true, 110 131 })); 111 - }, 2000); 132 + }, 3000); 112 133 }; 113 134 114 135 return (
+6 -2
webui/rockbox/src/Components/ControlBar/CurrentTrack/CurrentTrack.tsx
··· 43 43 )} 44 44 {nowPlaying && nowPlaying?.duration > 0 && ( 45 45 <> 46 - <Title>{nowPlaying.title}</Title> 46 + <Title> 47 + {_.get(nowPlaying, "title.length", 0) > 75 48 + ? `${nowPlaying.title?.substring(0, 75)}...` 49 + : nowPlaying.title} 50 + </Title> 47 51 <div 48 52 style={{ 49 53 display: "flex", ··· 54 58 > 55 59 <Time>{formatTime(nowPlaying.progress)}</Time> 56 60 <ArtistAlbum> 57 - {_.get(nowPlaying, "artist.length", 0) > 75 61 + {_.get(nowPlaying, "artist.length", 0) > 65 58 62 ? `${nowPlaying.artist?.substring(0, 54)}...` 59 63 : nowPlaying.artist} 60 64 <Separator>-</Separator>
+9
webui/rockbox/src/Components/ControlBar/PlayQueue/PlayQueue.stories.tsx
··· 26 26 onRemoveTrackAt: fn(), 27 27 }, 28 28 }; 29 + 30 + export const EmptyList: Story = { 31 + args: { 32 + previousTracks: [], 33 + nextTracks: [], 34 + onPlayTrackAt: fn(), 35 + onRemoveTrackAt: fn(), 36 + }, 37 + };
+100 -50
webui/rockbox/src/Components/ControlBar/PlayQueue/PlayQueue.tsx
··· 1 - import { FC, useState } from "react"; 1 + import { FC, useMemo, useRef, useState } from "react"; 2 2 import { Track } from "../../../Types/track"; 3 3 import { Link } from "react-router-dom"; 4 4 import { useTheme } from "@emotion/react"; ··· 20 20 TrackDetails, 21 21 TrackTitle, 22 22 } from "./styles"; 23 + import "./styles.css"; 24 + import { useVirtualizer } from "@tanstack/react-virtual"; 23 25 24 26 export type PlayQueueProps = { 25 27 previousTracks?: Track[]; ··· 37 39 }) => { 38 40 const theme = useTheme(); 39 41 const [active, setActive] = useState("playqueue"); 42 + const parentRef = useRef<HTMLDivElement>(null); 43 + const { currentIndex, amount } = useMemo(() => { 44 + return { 45 + currentIndex: previousTracks.length, 46 + amount: previousTracks.length + nextTracks.length, 47 + }; 48 + }, [previousTracks.length, nextTracks.length]); 49 + 50 + // The virtualizer 51 + const rowVirtualizer = useVirtualizer({ 52 + count: active === "playqueue" ? nextTracks.length : previousTracks.length, 53 + getScrollElement: () => parentRef.current, 54 + estimateSize: () => 64, 55 + }); 40 56 41 57 const onSwitch = () => { 42 58 if (active === "playqueue") { ··· 67 83 <Container> 68 84 <Header> 69 85 <Title>{active === "playqueue" ? "Play Queue" : "History"}</Title> 86 + {amount > 0 && ( 87 + <Title 88 + style={{ fontSize: 14, color: "#616161", textAlign: "center" }} 89 + > 90 + {currentIndex} 91 + {" / "} 92 + {amount} 93 + </Title> 94 + )} 70 95 <Switch onClick={onSwitch}> 71 96 {active === "playqueue" ? "History" : "Play Queue"} 72 97 </Switch> 73 98 </Header> 74 - <List> 75 - {tracks.map((track, index) => ( 76 - <ListItem key={track.id}> 77 - {track.cover && ( 78 - <div className="album-cover-container"> 79 - <AlbumCover src={track.cover} /> 80 - <div 81 - onClick={() => _onPlayTrackAt(index)} 82 - className="floating-play" 83 - > 84 - <Play size={16} color={track.cover ? "#fff" : "#000"} /> 99 + <List ref={parentRef}> 100 + <div 101 + style={{ 102 + height: rowVirtualizer.getTotalSize() 103 + ? `${rowVirtualizer.getTotalSize()}px` 104 + : undefined, 105 + width: "100%", 106 + position: "relative", 107 + }} 108 + > 109 + {rowVirtualizer.getVirtualItems().map((virtualItem) => ( 110 + <ListItem 111 + key={virtualItem.key} 112 + style={{ 113 + position: "absolute", 114 + top: 0, 115 + left: 0, 116 + width: "calc(100% - 34px)", 117 + transform: `translateY(${virtualItem.start}px)`, 118 + }} 119 + > 120 + {tracks[virtualItem.index].cover && ( 121 + <div className="album-cover-container"> 122 + <AlbumCover src={tracks[virtualItem.index].cover!} /> 123 + <div 124 + onClick={() => _onPlayTrackAt(virtualItem.index)} 125 + className="floating-play" 126 + > 127 + <Play 128 + size={16} 129 + color={tracks[virtualItem.index].cover ? "#fff" : "#000"} 130 + /> 131 + </div> 85 132 </div> 86 - </div> 87 - )} 88 - {!track.cover && ( 89 - <div className="album-cover-container"> 90 - <AlbumCoverAlt> 91 - <TrackIcon width={28} height={28} color="#a4a3a3" /> 92 - </AlbumCoverAlt> 93 - <div 94 - onClick={() => _onPlayTrackAt(index)} 95 - className="floating-play" 133 + )} 134 + {!tracks[virtualItem.index].cover && ( 135 + <div className="album-cover-container"> 136 + <AlbumCoverAlt> 137 + <TrackIcon width={28} height={28} color="#a4a3a3" /> 138 + </AlbumCoverAlt> 139 + <div 140 + onClick={() => _onPlayTrackAt(virtualItem.index)} 141 + className="floating-play" 142 + > 143 + <Play 144 + size={16} 145 + color={tracks[virtualItem.index].cover ? "#fff" : "#000"} 146 + /> 147 + </div> 148 + </div> 149 + )} 150 + <TrackDetails> 151 + <TrackTitle>{tracks[virtualItem.index].title}</TrackTitle> 152 + <Link 153 + to={`/artists/${tracks[virtualItem.index].artistId}`} 154 + style={{ textDecoration: "none" }} 96 155 > 97 - <Play size={16} color={track.cover ? "#fff" : "#000"} /> 98 - </div> 99 - </div> 100 - )} 101 - <TrackDetails> 102 - <TrackTitle>{track.title}</TrackTitle> 103 - <Link 104 - to={`/artists/${track.artistId}`} 105 - style={{ textDecoration: "none" }} 106 - > 107 - <Artist>{track.artist}</Artist> 108 - </Link> 109 - </TrackDetails> 110 - <Remove onClick={() => _onRemoveTrack(index)}> 111 - <CloseOutline size={24} color={theme.colors.text} /> 112 - </Remove> 113 - </ListItem> 114 - ))} 115 - {tracks.length === 0 && active === "playqueue" && ( 116 - <Placeholder> 117 - No upcoming tracks. Add some to your play queue. 118 - </Placeholder> 119 - )} 120 - {tracks.length === 0 && active === "history" && ( 121 - <Placeholder> 122 - No history. Play some tracks to see them here. 123 - </Placeholder> 124 - )} 156 + <Artist>{tracks[virtualItem.index].artist}</Artist> 157 + </Link> 158 + </TrackDetails> 159 + <Remove onClick={() => _onRemoveTrack(virtualItem.index)}> 160 + <CloseOutline size={24} color={theme.colors.text} /> 161 + </Remove> 162 + </ListItem> 163 + ))} 164 + {tracks.length === 0 && active === "playqueue" && ( 165 + <Placeholder> 166 + No upcoming tracks. Add some to your play queue. 167 + </Placeholder> 168 + )} 169 + {tracks.length === 0 && active === "history" && ( 170 + <Placeholder> 171 + No history. Play some tracks to see them here. 172 + </Placeholder> 173 + )} 174 + </div> 125 175 </List> 126 176 </Container> 127 177 );
+29 -4
webui/rockbox/src/Components/ControlBar/PlayQueue/PlayQueueWithData.tsx
··· 1 1 import { FC } from "react"; 2 2 import PlayQueue from "./PlayQueue"; 3 + import { usePlayQueue } from "../../../Hooks/usePlayQueue"; 4 + import { 5 + usePlaylistRemoveTrackMutation, 6 + useStartPlaylistMutation, 7 + } from "../../../Hooks/GraphQL"; 3 8 4 9 const PlayQueueWithData: FC = () => { 10 + const { nextTracks, previousTracks } = usePlayQueue(); 11 + const [removeTrack] = usePlaylistRemoveTrackMutation(); 12 + const [startPlaylist] = useStartPlaylistMutation(); 13 + 14 + const onPlayTrackAt = (startIndex: number) => { 15 + startPlaylist({ 16 + variables: { 17 + startIndex, 18 + }, 19 + }); 20 + }; 21 + 22 + const onRemoveTrackAt = (index: number) => { 23 + removeTrack({ 24 + variables: { 25 + index, 26 + }, 27 + }); 28 + }; 29 + 5 30 return ( 6 31 <PlayQueue 7 - previousTracks={[]} 8 - nextTracks={[]} 32 + previousTracks={previousTracks} 33 + nextTracks={nextTracks} 9 34 currentTrack={undefined} 10 - onPlayTrackAt={() => {}} 11 - onRemoveTrackAt={() => {}} 35 + onPlayTrackAt={onPlayTrackAt} 36 + onRemoveTrackAt={onRemoveTrackAt} 12 37 /> 13 38 ); 14 39 };
+46
webui/rockbox/src/Components/ControlBar/PlayQueue/__snapshots__/PlayQueue.test.tsx.snap
··· 1 + // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 + 3 + exports[`PlayQueue > should render 1`] = ` 4 + <div> 5 + <div 6 + class="" 7 + > 8 + <div 9 + class="css-17m5cz6" 10 + > 11 + <div 12 + class="css-o2j9ze" 13 + > 14 + <div 15 + class="css-p7veh6" 16 + > 17 + Play Queue 18 + </div> 19 + <div 20 + class="css-p7veh6" 21 + style="font-size: 14px; color: rgb(97, 97, 97); text-align: center;" 22 + > 23 + 1 24 + / 25 + 4 26 + </div> 27 + <div 28 + class="css-14tdrps" 29 + > 30 + History 31 + </div> 32 + </div> 33 + <div 34 + class="css-1wmblyr" 35 + > 36 + <div 37 + style="height: 192px; width: 100%; position: relative;" 38 + /> 39 + </div> 40 + </div> 41 + </div> 42 + <div 43 + class="" 44 + /> 45 + </div> 46 + `;
+22
webui/rockbox/src/Components/ControlBar/PlayQueue/styles.css
··· 1 + .album-cover-container { 2 + height: 64px; 3 + justify-content: center; 4 + align-items: center; 5 + display: flex; 6 + position: relative; 7 + } 8 + 9 + .album-cover-container .floating-play { 10 + display: none; 11 + position: absolute; 12 + left: 19px; 13 + top: 17px; 14 + } 15 + 16 + .album-cover-container:hover .floating-play { 17 + display: block; 18 + } 19 + 20 + .album-cover-container:hover img { 21 + opacity: 0.4; 22 + }
+3 -2
webui/rockbox/src/Components/ControlBar/PlayQueue/styles.tsx
··· 21 21 22 22 export const Switch = styled(Title)` 23 23 color: #fe099c; 24 - flex: initial; 24 + flex: 1; 25 + text-align: end; 25 26 cursor: pointer; 26 27 -webkit-user-select: none; 27 28 -ms-user-select: none; ··· 30 31 31 32 export const List = styled.div` 32 33 height: calc(100% - 59.5px); 33 - overflow-y: scroll; 34 + overflow-y: auto; 34 35 `; 35 36 36 37 export const ListItem = styled.div`
+29 -3
webui/rockbox/src/Components/ControlBar/RightMenu/RightMenu.tsx
··· 1 1 import { FC } from "react"; 2 2 import { Container } from "./styles"; 3 3 import { List } from "@styled-icons/entypo"; 4 + import { StatefulPopover } from "baseui/popover"; 4 5 import { Button } from "../styles"; 6 + import PlayQueue from "../PlayQueue"; 7 + import { useTheme } from "@emotion/react"; 8 + import _ from "lodash"; 5 9 6 10 const RightMenu: FC = () => { 11 + const theme = useTheme(); 7 12 return ( 8 13 <Container> 9 - <Button> 10 - <List size={21} /> 11 - </Button> 14 + <StatefulPopover 15 + placement="bottom" 16 + content={() => <PlayQueue />} 17 + overrides={{ 18 + Body: { 19 + style: { 20 + left: "-21px", 21 + }, 22 + }, 23 + Inner: { 24 + style: { 25 + backgroundColor: _.get( 26 + theme, 27 + "colors.popoverBackground", 28 + "#fff" 29 + ), 30 + }, 31 + }, 32 + }} 33 + > 34 + <Button> 35 + <List size={21} /> 36 + </Button> 37 + </StatefulPopover> 12 38 </Container> 13 39 ); 14 40 };
+3 -1
webui/rockbox/src/Components/ControlBar/__snapshots__/ControlBar.test.tsx.snap
··· 6 6 class="" 7 7 > 8 8 <div 9 - class="css-1dlrxtg" 9 + class="css-14bvc2y" 10 10 > 11 11 <div 12 12 class="css-1h69jy4" ··· 181 181 class="css-nb4c1y" 182 182 > 183 183 <button 184 + aria-expanded="false" 185 + aria-haspopup="true" 184 186 class="css-10asy2d" 185 187 > 186 188 <svg
+1
webui/rockbox/src/Components/ControlBar/styles.tsx
··· 6 6 height: 60px; 7 7 margin-top: 5px; 8 8 margin-bottom: 20px; 9 + padding-right: 20px; 9 10 `; 10 11 11 12 export const ControlsContainer = styled.div`
+8 -1
webui/rockbox/src/Components/Files/__snapshots__/Files.test.tsx.snap
··· 11 11 <div 12 12 class="css-1fkc5kv" 13 13 > 14 + <img 15 + alt="Rockbox" 16 + src="/src/Assets/rockbox-icon.svg" 17 + style="width: 40px; margin-bottom: 20px; margin-left: 12px;" 18 + /> 14 19 <a 15 20 class="css-tujm8o" 16 21 color="initial" ··· 133 138 class="css-g1mxd4" 134 139 > 135 140 <div 136 - class="css-1dlrxtg" 141 + class="css-14bvc2y" 137 142 > 138 143 <div 139 144 class="css-1h69jy4" ··· 260 265 class="css-nb4c1y" 261 266 > 262 267 <button 268 + aria-expanded="false" 269 + aria-haspopup="true" 263 270 class="css-10asy2d" 264 271 > 265 272 <svg
+5
webui/rockbox/src/Components/Sidebar/__snapshots__/Sidebar.test.tsx.snap
··· 8 8 <div 9 9 class="css-1fkc5kv" 10 10 > 11 + <img 12 + alt="Rockbox" 13 + src="/src/Assets/rockbox-icon.svg" 14 + style="width: 40px; margin-bottom: 20px; margin-left: 12px;" 15 + /> 11 16 <a 12 17 class="css-8v05qj" 13 18 color="#fe099c"
+8 -1
webui/rockbox/src/Components/Tracks/__snapshots__/Tracks.test.tsx.snap
··· 11 11 <div 12 12 class="css-1fkc5kv" 13 13 > 14 + <img 15 + alt="Rockbox" 16 + src="/src/Assets/rockbox-icon.svg" 17 + style="width: 40px; margin-bottom: 20px; margin-left: 12px;" 18 + /> 14 19 <a 15 20 class="css-tujm8o" 16 21 color="initial" ··· 133 138 class="css-v8cdaf" 134 139 > 135 140 <div 136 - class="css-1dlrxtg" 141 + class="css-14bvc2y" 137 142 > 138 143 <div 139 144 class="css-1h69jy4" ··· 260 265 class="css-nb4c1y" 261 266 > 262 267 <button 268 + aria-expanded="false" 269 + aria-haspopup="true" 263 270 class="css-10asy2d" 264 271 > 265 272 <svg
webui/rockbox/src/GraphQL/Playlist/.gitkeep

This is a binary file and will not be displayed.

+13
webui/rockbox/src/GraphQL/Playlist/Mutation.ts
··· 1 + import { gql } from "@apollo/client"; 2 + 3 + export const PLAYLIST_REMOVE_TRACK = gql` 4 + mutation PlaylistRemoveTrack($index: Int!) { 5 + playlistRemoveTrack(index: $index) 6 + } 7 + `; 8 + 9 + export const START_PLAYLIST = gql` 10 + mutation StartPlaylist($startIndex: Int, $elapsed: Int, $offset: Int) { 11 + playlistStart(startIndex: $startIndex, elapsed: $elapsed, offset: $offset) 12 + } 13 + `;
+20
webui/rockbox/src/GraphQL/Playlist/Query.ts
··· 1 + import { gql } from "@apollo/client"; 2 + 3 + export const GET_CURRENT_PLAYLIST = gql` 4 + query GetCurrentPlaylist { 5 + playlistGetCurrent { 6 + index 7 + amount 8 + maxPlaylistSize 9 + tracks { 10 + id 11 + title 12 + artist 13 + albumArt 14 + artistId 15 + albumId 16 + path 17 + } 18 + } 19 + } 20 + `;
+20
webui/rockbox/src/GraphQL/Playlist/Subscription.ts
··· 1 + import { gql } from "@apollo/client"; 2 + 3 + export const PLAYLIST_CHANGED = gql` 4 + subscription PlaylistChanged { 5 + playlistChanged { 6 + index 7 + amount 8 + maxPlaylistSize 9 + tracks { 10 + id 11 + title 12 + artist 13 + albumArt 14 + artistId 15 + albumId 16 + path 17 + } 18 + } 19 + } 20 + `;
+201
webui/rockbox/src/Hooks/GraphQL.tsx
··· 89 89 playlistInsertDirectory: Scalars['Int']['output']; 90 90 playlistInsertTracks: Scalars['Int']['output']; 91 91 playlistRemoveAllTracks: Scalars['Int']['output']; 92 + playlistRemoveTrack: Scalars['Int']['output']; 92 93 playlistResume: Scalars['String']['output']; 93 94 playlistSetModified: Scalars['String']['output']; 94 95 playlistStart: Scalars['Int']['output']; ··· 140 141 tracks: Array<Scalars['String']['input']>; 141 142 }; 142 143 144 + 145 + export type MutationPlaylistRemoveTrackArgs = { 146 + index: Scalars['Int']['input']; 147 + }; 148 + 149 + 150 + export type MutationPlaylistStartArgs = { 151 + elapsed?: InputMaybe<Scalars['Int']['input']>; 152 + offset?: InputMaybe<Scalars['Int']['input']>; 153 + startIndex?: InputMaybe<Scalars['Int']['input']>; 154 + }; 155 + 143 156 export type Playlist = { 144 157 __typename?: 'Playlist'; 158 + amount: Scalars['Int']['output']; 159 + firstIndex: Scalars['Int']['output']; 160 + index: Scalars['Int']['output']; 161 + lastInsertPos: Scalars['Int']['output']; 162 + lastShuffledStart: Scalars['Int']['output']; 163 + maxPlaylistSize: Scalars['Int']['output']; 164 + seed: Scalars['Int']['output']; 145 165 tracks: Array<Track>; 146 166 }; 147 167 ··· 204 224 __typename?: 'Subscription'; 205 225 currentlyPlayingSong: Track; 206 226 playbackStatus: AudioStatus; 227 + playlistChanged: Playlist; 207 228 }; 208 229 209 230 export type SystemStatus = { ··· 539 560 540 561 541 562 export type PlaybackStatusSubscription = { __typename?: 'Subscription', playbackStatus: { __typename?: 'AudioStatus', status: number } }; 563 + 564 + export type PlaylistRemoveTrackMutationVariables = Exact<{ 565 + index: Scalars['Int']['input']; 566 + }>; 567 + 568 + 569 + export type PlaylistRemoveTrackMutation = { __typename?: 'Mutation', playlistRemoveTrack: number }; 570 + 571 + export type StartPlaylistMutationVariables = Exact<{ 572 + startIndex?: InputMaybe<Scalars['Int']['input']>; 573 + elapsed?: InputMaybe<Scalars['Int']['input']>; 574 + offset?: InputMaybe<Scalars['Int']['input']>; 575 + }>; 576 + 577 + 578 + export type StartPlaylistMutation = { __typename?: 'Mutation', playlistStart: number }; 579 + 580 + export type GetCurrentPlaylistQueryVariables = Exact<{ [key: string]: never; }>; 581 + 582 + 583 + export type GetCurrentPlaylistQuery = { __typename?: 'Query', playlistGetCurrent: { __typename?: 'Playlist', index: number, amount: number, maxPlaylistSize: number, tracks: Array<{ __typename?: 'Track', id?: string | null, title: string, artist: string, albumArt?: string | null, artistId?: string | null, albumId?: string | null, path: string }> } }; 584 + 585 + export type PlaylistChangedSubscriptionVariables = Exact<{ [key: string]: never; }>; 586 + 587 + 588 + export type PlaylistChangedSubscription = { __typename?: 'Subscription', playlistChanged: { __typename?: 'Playlist', index: number, amount: number, maxPlaylistSize: number, tracks: Array<{ __typename?: 'Track', id?: string | null, title: string, artist: string, albumArt?: string | null, artistId?: string | null, albumId?: string | null, path: string }> } }; 542 589 543 590 export type GetRockboxVersionQueryVariables = Exact<{ [key: string]: never; }>; 544 591 ··· 1212 1259 } 1213 1260 export type PlaybackStatusSubscriptionHookResult = ReturnType<typeof usePlaybackStatusSubscription>; 1214 1261 export type PlaybackStatusSubscriptionResult = Apollo.SubscriptionResult<PlaybackStatusSubscription>; 1262 + export const PlaylistRemoveTrackDocument = gql` 1263 + mutation PlaylistRemoveTrack($index: Int!) { 1264 + playlistRemoveTrack(index: $index) 1265 + } 1266 + `; 1267 + export type PlaylistRemoveTrackMutationFn = Apollo.MutationFunction<PlaylistRemoveTrackMutation, PlaylistRemoveTrackMutationVariables>; 1268 + 1269 + /** 1270 + * __usePlaylistRemoveTrackMutation__ 1271 + * 1272 + * To run a mutation, you first call `usePlaylistRemoveTrackMutation` within a React component and pass it any options that fit your needs. 1273 + * When your component renders, `usePlaylistRemoveTrackMutation` returns a tuple that includes: 1274 + * - A mutate function that you can call at any time to execute the mutation 1275 + * - An object with fields that represent the current status of the mutation's execution 1276 + * 1277 + * @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; 1278 + * 1279 + * @example 1280 + * const [playlistRemoveTrackMutation, { data, loading, error }] = usePlaylistRemoveTrackMutation({ 1281 + * variables: { 1282 + * index: // value for 'index' 1283 + * }, 1284 + * }); 1285 + */ 1286 + export function usePlaylistRemoveTrackMutation(baseOptions?: Apollo.MutationHookOptions<PlaylistRemoveTrackMutation, PlaylistRemoveTrackMutationVariables>) { 1287 + const options = {...defaultOptions, ...baseOptions} 1288 + return Apollo.useMutation<PlaylistRemoveTrackMutation, PlaylistRemoveTrackMutationVariables>(PlaylistRemoveTrackDocument, options); 1289 + } 1290 + export type PlaylistRemoveTrackMutationHookResult = ReturnType<typeof usePlaylistRemoveTrackMutation>; 1291 + export type PlaylistRemoveTrackMutationResult = Apollo.MutationResult<PlaylistRemoveTrackMutation>; 1292 + export type PlaylistRemoveTrackMutationOptions = Apollo.BaseMutationOptions<PlaylistRemoveTrackMutation, PlaylistRemoveTrackMutationVariables>; 1293 + export const StartPlaylistDocument = gql` 1294 + mutation StartPlaylist($startIndex: Int, $elapsed: Int, $offset: Int) { 1295 + playlistStart(startIndex: $startIndex, elapsed: $elapsed, offset: $offset) 1296 + } 1297 + `; 1298 + export type StartPlaylistMutationFn = Apollo.MutationFunction<StartPlaylistMutation, StartPlaylistMutationVariables>; 1299 + 1300 + /** 1301 + * __useStartPlaylistMutation__ 1302 + * 1303 + * To run a mutation, you first call `useStartPlaylistMutation` within a React component and pass it any options that fit your needs. 1304 + * When your component renders, `useStartPlaylistMutation` returns a tuple that includes: 1305 + * - A mutate function that you can call at any time to execute the mutation 1306 + * - An object with fields that represent the current status of the mutation's execution 1307 + * 1308 + * @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; 1309 + * 1310 + * @example 1311 + * const [startPlaylistMutation, { data, loading, error }] = useStartPlaylistMutation({ 1312 + * variables: { 1313 + * startIndex: // value for 'startIndex' 1314 + * elapsed: // value for 'elapsed' 1315 + * offset: // value for 'offset' 1316 + * }, 1317 + * }); 1318 + */ 1319 + export function useStartPlaylistMutation(baseOptions?: Apollo.MutationHookOptions<StartPlaylistMutation, StartPlaylistMutationVariables>) { 1320 + const options = {...defaultOptions, ...baseOptions} 1321 + return Apollo.useMutation<StartPlaylistMutation, StartPlaylistMutationVariables>(StartPlaylistDocument, options); 1322 + } 1323 + export type StartPlaylistMutationHookResult = ReturnType<typeof useStartPlaylistMutation>; 1324 + export type StartPlaylistMutationResult = Apollo.MutationResult<StartPlaylistMutation>; 1325 + export type StartPlaylistMutationOptions = Apollo.BaseMutationOptions<StartPlaylistMutation, StartPlaylistMutationVariables>; 1326 + export const GetCurrentPlaylistDocument = gql` 1327 + query GetCurrentPlaylist { 1328 + playlistGetCurrent { 1329 + index 1330 + amount 1331 + maxPlaylistSize 1332 + tracks { 1333 + id 1334 + title 1335 + artist 1336 + albumArt 1337 + artistId 1338 + albumId 1339 + path 1340 + } 1341 + } 1342 + } 1343 + `; 1344 + 1345 + /** 1346 + * __useGetCurrentPlaylistQuery__ 1347 + * 1348 + * To run a query within a React component, call `useGetCurrentPlaylistQuery` and pass it any options that fit your needs. 1349 + * When your component renders, `useGetCurrentPlaylistQuery` returns an object from Apollo Client that contains loading, error, and data properties 1350 + * you can use to render your UI. 1351 + * 1352 + * @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; 1353 + * 1354 + * @example 1355 + * const { data, loading, error } = useGetCurrentPlaylistQuery({ 1356 + * variables: { 1357 + * }, 1358 + * }); 1359 + */ 1360 + export function useGetCurrentPlaylistQuery(baseOptions?: Apollo.QueryHookOptions<GetCurrentPlaylistQuery, GetCurrentPlaylistQueryVariables>) { 1361 + const options = {...defaultOptions, ...baseOptions} 1362 + return Apollo.useQuery<GetCurrentPlaylistQuery, GetCurrentPlaylistQueryVariables>(GetCurrentPlaylistDocument, options); 1363 + } 1364 + export function useGetCurrentPlaylistLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetCurrentPlaylistQuery, GetCurrentPlaylistQueryVariables>) { 1365 + const options = {...defaultOptions, ...baseOptions} 1366 + return Apollo.useLazyQuery<GetCurrentPlaylistQuery, GetCurrentPlaylistQueryVariables>(GetCurrentPlaylistDocument, options); 1367 + } 1368 + export function useGetCurrentPlaylistSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<GetCurrentPlaylistQuery, GetCurrentPlaylistQueryVariables>) { 1369 + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} 1370 + return Apollo.useSuspenseQuery<GetCurrentPlaylistQuery, GetCurrentPlaylistQueryVariables>(GetCurrentPlaylistDocument, options); 1371 + } 1372 + export type GetCurrentPlaylistQueryHookResult = ReturnType<typeof useGetCurrentPlaylistQuery>; 1373 + export type GetCurrentPlaylistLazyQueryHookResult = ReturnType<typeof useGetCurrentPlaylistLazyQuery>; 1374 + export type GetCurrentPlaylistSuspenseQueryHookResult = ReturnType<typeof useGetCurrentPlaylistSuspenseQuery>; 1375 + export type GetCurrentPlaylistQueryResult = Apollo.QueryResult<GetCurrentPlaylistQuery, GetCurrentPlaylistQueryVariables>; 1376 + export const PlaylistChangedDocument = gql` 1377 + subscription PlaylistChanged { 1378 + playlistChanged { 1379 + index 1380 + amount 1381 + maxPlaylistSize 1382 + tracks { 1383 + id 1384 + title 1385 + artist 1386 + albumArt 1387 + artistId 1388 + albumId 1389 + path 1390 + } 1391 + } 1392 + } 1393 + `; 1394 + 1395 + /** 1396 + * __usePlaylistChangedSubscription__ 1397 + * 1398 + * To run a query within a React component, call `usePlaylistChangedSubscription` and pass it any options that fit your needs. 1399 + * When your component renders, `usePlaylistChangedSubscription` returns an object from Apollo Client that contains loading, error, and data properties 1400 + * you can use to render your UI. 1401 + * 1402 + * @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; 1403 + * 1404 + * @example 1405 + * const { data, loading, error } = usePlaylistChangedSubscription({ 1406 + * variables: { 1407 + * }, 1408 + * }); 1409 + */ 1410 + export function usePlaylistChangedSubscription(baseOptions?: Apollo.SubscriptionHookOptions<PlaylistChangedSubscription, PlaylistChangedSubscriptionVariables>) { 1411 + const options = {...defaultOptions, ...baseOptions} 1412 + return Apollo.useSubscription<PlaylistChangedSubscription, PlaylistChangedSubscriptionVariables>(PlaylistChangedDocument, options); 1413 + } 1414 + export type PlaylistChangedSubscriptionHookResult = ReturnType<typeof usePlaylistChangedSubscription>; 1415 + export type PlaylistChangedSubscriptionResult = Apollo.SubscriptionResult<PlaylistChangedSubscription>; 1215 1416 export const GetRockboxVersionDocument = gql` 1216 1417 query GetRockboxVersion { 1217 1418 rockboxVersion
+58
webui/rockbox/src/Hooks/usePlayQueue.tsx
··· 1 + import { useMemo } from "react"; 2 + import { 3 + useGetCurrentPlaylistQuery, 4 + usePlaylistChangedSubscription, 5 + } from "./GraphQL"; 6 + import _ from "lodash"; 7 + 8 + export const usePlayQueue = () => { 9 + const { data: playlistSubscription } = usePlaylistChangedSubscription({ 10 + fetchPolicy: "network-only", 11 + }); 12 + const { data } = useGetCurrentPlaylistQuery({ 13 + fetchPolicy: "cache-and-network", 14 + }); 15 + const previousTracks = useMemo(() => { 16 + if (playlistSubscription?.playlistChanged) { 17 + const currentTrackIndex = _.get( 18 + playlistSubscription, 19 + "playlistChanged.index", 20 + 0 21 + ); 22 + const tracks = _.get(playlistSubscription, "playlistChanged.tracks", []); 23 + return tracks.slice(0, currentTrackIndex + 1).map((x, index) => ({ 24 + ...x, 25 + id: index.toString(), 26 + })); 27 + } 28 + const currentTrackIndex = _.get(data, "playlistGetCurrent.index", 0); 29 + const tracks = _.get(data, "playlistGetCurrent.tracks", []); 30 + return tracks.slice(0, currentTrackIndex + 1).map((x, index) => ({ 31 + ...x, 32 + id: index.toString(), 33 + })); 34 + }, [data, playlistSubscription]); 35 + 36 + const nextTracks = useMemo(() => { 37 + if (playlistSubscription?.playlistChanged) { 38 + const currentTrackIndex = _.get( 39 + playlistSubscription, 40 + "playlistChanged.index", 41 + 0 42 + ); 43 + const tracks = _.get(playlistSubscription, "playlistChanged.tracks", []); 44 + return tracks.slice(currentTrackIndex + 1).map((x, index) => ({ 45 + ...x, 46 + id: index.toString(), 47 + })); 48 + } 49 + const currentTrackIndex = _.get(data, "playlistGetCurrent.index", 0); 50 + const tracks = _.get(data, "playlistGetCurrent.tracks", []); 51 + return tracks.slice(currentTrackIndex + 1).map((x, index) => ({ 52 + ...x, 53 + id: index.toString(), 54 + })); 55 + }, [data, playlistSubscription]); 56 + 57 + return { previousTracks, nextTracks }; 58 + };
+8 -8
webui/rockbox/src/Types/track.ts
··· 1 1 export type Track = { 2 2 id: string; 3 - trackNumber?: number; 3 + trackNumber?: number | null; 4 4 title: string; 5 5 artist: string; 6 6 album?: string; 7 - time?: string; 8 - duration?: number; 9 - albumArt?: string; 10 - cover?: string; 11 - albumId?: string; 12 - artistId?: string; 13 - discnum?: number; 7 + time?: string | null; 8 + duration?: number | null; 9 + albumArt?: string | null; 10 + cover?: string | null; 11 + albumId?: string | null; 12 + artistId?: string | null; 13 + discnum?: number | null; 14 14 }; 15 15 16 16 export type CurrentTrack = {
+28
webui/rockbox/src/mocks.ts
··· 1 + import { 2 + nextTracks, 3 + previousTracks, 4 + } from "./Components/ControlBar/PlayQueue/mocks"; 1 5 import { 2 6 GET_CURRENT_TRACK, 3 7 GET_PLAYBACK_STATUS, 4 8 } from "./GraphQL/Playback/Query"; 9 + import { GET_CURRENT_PLAYLIST } from "./GraphQL/Playlist/Query"; 5 10 6 11 export const mocks = [ 7 12 { ··· 32 37 query: GET_PLAYBACK_STATUS, 33 38 }, 34 39 result: { data: { status: 1 } }, 40 + }, 41 + { 42 + request: { 43 + query: GET_CURRENT_PLAYLIST, 44 + }, 45 + result: { 46 + data: { 47 + playlistGetCurrent: { 48 + index: 2, 49 + amount: previousTracks.length + nextTracks.length, 50 + maxPlaylistSize: 10000, 51 + tracks: [...previousTracks, ...nextTracks].map((x) => ({ 52 + id: x.id, 53 + title: x.title, 54 + artist: x.artist, 55 + albumArt: x.cover, 56 + artistId: null, 57 + albumId: null, 58 + path: "", 59 + })), 60 + }, 61 + }, 62 + }, 35 63 }, 36 64 ];