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

Configure Feed

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

Merge pull request #32 from tsirysndr/feat/display-current-track

webui: display current track

authored by

Tsiry Sandratraina and committed by
GitHub
95229e75 94593eaf

+1479 -628
+36 -30
Cargo.lock
··· 582 582 583 583 [[package]] 584 584 name = "async-graphql" 585 - version = "7.0.9" 585 + version = "7.0.11" 586 586 source = "registry+https://github.com/rust-lang/crates.io-index" 587 - checksum = "9d37c3e9ba322eb00e9e5e997d58f08e8b6de037325b9367ac59bca8e3cd46af" 587 + checksum = "0ba6d24703c5adc5ba9116901b92ee4e4c0643c01a56c4fd303f3818638d7449" 588 588 dependencies = [ 589 589 "async-graphql-derive", 590 590 "async-graphql-parser", ··· 616 616 617 617 [[package]] 618 618 name = "async-graphql-actix-web" 619 - version = "7.0.9" 619 + version = "7.0.11" 620 620 source = "registry+https://github.com/rust-lang/crates.io-index" 621 - checksum = "b84194aa7c13d234db9eb496945a345f2e76cf14b60de15ce95610207df2e93d" 621 + checksum = "b0644a2f8812b8787d2fbd725a22bce3c79c2afb66d5f8d70fbda6919736abf7" 622 622 dependencies = [ 623 623 "actix", 624 624 "actix-http", ··· 635 635 636 636 [[package]] 637 637 name = "async-graphql-derive" 638 - version = "7.0.9" 638 + version = "7.0.11" 639 639 source = "registry+https://github.com/rust-lang/crates.io-index" 640 - checksum = "f1141703c11c6ad4fa9b3b0e1e476dea01dbd18a44db00f949b804afaab2f344" 640 + checksum = "a94c2d176893486bd37cd1b6defadd999f7357bf5804e92f510c08bcf16c538f" 641 641 dependencies = [ 642 642 "Inflector", 643 643 "async-graphql-parser", ··· 652 652 653 653 [[package]] 654 654 name = "async-graphql-parser" 655 - version = "7.0.9" 655 + version = "7.0.11" 656 656 source = "registry+https://github.com/rust-lang/crates.io-index" 657 - checksum = "2f66edcce4c38c18f7eb181fdf561c3d3aa2d644ce7358fc7a928c00a4ffef17" 657 + checksum = "79272bdbf26af97866e149f05b2b546edb5c00e51b5f916289931ed233e208ad" 658 658 dependencies = [ 659 659 "async-graphql-value", 660 660 "pest", ··· 664 664 665 665 [[package]] 666 666 name = "async-graphql-value" 667 - version = "7.0.9" 667 + version = "7.0.11" 668 668 source = "registry+https://github.com/rust-lang/crates.io-index" 669 - checksum = "3b0206011cad065420c27988f17dd7fe201a0e056b20c262209b7bffcd6fa176" 669 + checksum = "ef5ec94176a12a8cbe985cd73f2e54dc9c702c88c766bdef12f1f3a67cedbee1" 670 670 dependencies = [ 671 671 "bytes", 672 672 "indexmap 2.5.0", ··· 3039 3039 3040 3040 [[package]] 3041 3041 name = "futures-channel" 3042 - version = "0.3.30" 3042 + version = "0.3.31" 3043 3043 source = "registry+https://github.com/rust-lang/crates.io-index" 3044 - checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 3044 + checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 3045 3045 dependencies = [ 3046 3046 "futures-core", 3047 3047 "futures-sink", ··· 3049 3049 3050 3050 [[package]] 3051 3051 name = "futures-core" 3052 - version = "0.3.30" 3052 + version = "0.3.31" 3053 3053 source = "registry+https://github.com/rust-lang/crates.io-index" 3054 - checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 3054 + checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 3055 3055 3056 3056 [[package]] 3057 3057 name = "futures-executor" ··· 3077 3077 3078 3078 [[package]] 3079 3079 name = "futures-io" 3080 - version = "0.3.30" 3080 + version = "0.3.31" 3081 3081 source = "registry+https://github.com/rust-lang/crates.io-index" 3082 - checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 3082 + checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 3083 3083 3084 3084 [[package]] 3085 3085 name = "futures-macro" 3086 - version = "0.3.30" 3086 + version = "0.3.31" 3087 3087 source = "registry+https://github.com/rust-lang/crates.io-index" 3088 - checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 3088 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 3089 3089 dependencies = [ 3090 3090 "proc-macro2", 3091 3091 "quote", ··· 3094 3094 3095 3095 [[package]] 3096 3096 name = "futures-sink" 3097 - version = "0.3.30" 3097 + version = "0.3.31" 3098 3098 source = "registry+https://github.com/rust-lang/crates.io-index" 3099 - checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 3099 + checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 3100 3100 3101 3101 [[package]] 3102 3102 name = "futures-task" 3103 - version = "0.3.30" 3103 + version = "0.3.31" 3104 3104 source = "registry+https://github.com/rust-lang/crates.io-index" 3105 - checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 3105 + checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 3106 3106 3107 3107 [[package]] 3108 3108 name = "futures-timer" ··· 3112 3112 3113 3113 [[package]] 3114 3114 name = "futures-util" 3115 - version = "0.3.30" 3115 + version = "0.3.31" 3116 3116 source = "registry+https://github.com/rust-lang/crates.io-index" 3117 - checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 3117 + checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 3118 3118 dependencies = [ 3119 3119 "futures-channel", 3120 3120 "futures-core", ··· 4689 4689 4690 4690 [[package]] 4691 4691 name = "once_cell" 4692 - version = "1.19.0" 4692 + version = "1.20.2" 4693 4693 source = "registry+https://github.com/rust-lang/crates.io-index" 4694 - checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 4694 + checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 4695 4695 4696 4696 [[package]] 4697 4697 name = "opaque-debug" ··· 5680 5680 "anyhow", 5681 5681 "async-graphql", 5682 5682 "async-graphql-actix-web", 5683 + "futures-channel", 5684 + "futures-util", 5685 + "md5", 5686 + "once_cell", 5683 5687 "owo-colors 4.1.0", 5684 5688 "reqwest", 5685 5689 "rockbox-library", 5686 5690 "rockbox-sys", 5687 5691 "serde", 5688 5692 "serde_json", 5693 + "slab", 5689 5694 "sqlx", 5690 5695 "tokio", 5691 5696 ] ··· 5731 5736 version = "0.1.0" 5732 5737 dependencies = [ 5733 5738 "anyhow", 5739 + "md5", 5734 5740 "owo-colors 4.1.0", 5735 5741 "queryst", 5736 5742 "reqwest", ··· 7345 7351 7346 7352 [[package]] 7347 7353 name = "toml_edit" 7348 - version = "0.22.20" 7354 + version = "0.22.22" 7349 7355 source = "registry+https://github.com/rust-lang/crates.io-index" 7350 - checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" 7356 + checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" 7351 7357 dependencies = [ 7352 7358 "indexmap 2.5.0", 7353 7359 "toml_datetime", ··· 8325 8331 8326 8332 [[package]] 8327 8333 name = "winnow" 8328 - version = "0.6.18" 8334 + version = "0.6.20" 8329 8335 source = "registry+https://github.com/rust-lang/crates.io-index" 8330 - checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" 8336 + checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" 8331 8337 dependencies = [ 8332 8338 "memchr", 8333 8339 ]
+69
apps/broker_thread.c
··· 1 + /*************************************************************************** 2 + * __________ __ ___. 3 + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ 4 + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / 5 + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < 6 + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ 7 + * \/ \/ \/ \/ \/ 8 + * $Id$ 9 + * 10 + * Copyright (C) 2024 - Tsiry Sandratraina 11 + * 12 + * This program is free software; you can redistribute it and/or 13 + * modify it under the terms of the GNU General Public License 14 + * as published by the Free Software Foundation; either version 2 15 + * of the License, or (at your option) any later version. 16 + * 17 + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY 18 + * KIND, either express or implied. 19 + * 20 + ****************************************************************************/ 21 + #include "config.h" 22 + #include "system.h" 23 + #include "kernel.h" 24 + #include "logf.h" 25 + #include "appevents.h" 26 + 27 + bool broker_is_initialized = false; 28 + 29 + /* Broker thread */ 30 + static long broker_stack[(DEFAULT_STACK_SIZE * 4)/sizeof(long)]; 31 + static const char broker_thread_name[] = "broker"; 32 + unsigned int broker_thread_id = 0; 33 + 34 + extern void start_broker(void); 35 + 36 + extern void debugfn(const char *fmt); 37 + 38 + static void broker_thread(void) { 39 + start_broker(); 40 + } 41 + 42 + /** -- Startup -- **/ 43 + 44 + /* Initialize the broker - called from init() in main.c */ 45 + void INIT_ATTR broker_init(void) 46 + { 47 + /* Can never do this twice */ 48 + if (broker_is_initialized) 49 + { 50 + logf("broker: already initialized"); 51 + return; 52 + } 53 + 54 + logf("broker: initializing"); 55 + 56 + /* Initialize queues before giving control elsewhere in case it likes 57 + to send messages. Thread creation will be delayed however so nothing 58 + starts running until ready if something yields such as talk_init. */ 59 + // queue_init(&server_queue, true); 60 + broker_thread_id = create_thread(broker_thread, broker_stack, 61 + sizeof(broker_stack), 0, broker_thread_name 62 + IF_PRIO(, PRIORITY_USER_INTERFACE) 63 + IF_COP(, CPU)); 64 + 65 + sleep(HZ); /* Give it a chance to start */ 66 + 67 + /* Probably safe to say */ 68 + broker_is_initialized = true; 69 + }
+27
apps/broker_thread.h
··· 1 + /*************************************************************************** 2 + * __________ __ ___. 3 + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ 4 + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / 5 + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < 6 + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ 7 + * \/ \/ \/ \/ \/ 8 + * $Id$ 9 + * 10 + * Copyright (C) 2024 - Tsiry Sandratraina 11 + * 12 + * This program is free software; you can redistribute it and/or 13 + * modify it under the terms of the GNU General Public License 14 + * as published by the Free Software Foundation; either version 2 15 + * of the License, or (at your option) any later version. 16 + * 17 + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY 18 + * KIND, either express or implied. 19 + * 20 + ****************************************************************************/ 21 + #ifndef BROKER_THREAD_H 22 + #define BROKER_THREAD_H 23 + 24 + 25 + void broker_init(void); 26 + 27 + #endif /* BROKER_THREAD_H */
+2
apps/main.c
··· 95 95 #include "playback.h" 96 96 #include "tdspeed.h" 97 97 #include "server_thread.h" 98 + #include "broker_thread.h" 98 99 #if defined(HAVE_RECORDING) && !defined(SIMULATOR) 99 100 #include "pcm_record.h" 100 101 #endif ··· 454 455 455 456 #ifdef ROCKBOX_SERVER 456 457 server_init(); 458 + broker_init(); 457 459 #endif 458 460 } 459 461
+1 -7
apps/server_thread.c
··· 7 7 * \/ \/ \/ \/ \/ 8 8 * $Id$ 9 9 * 10 - * Copyright (C) 2005-2007 Miika Pekkarinen 11 - * Copyright (C) 2007-2008 Nicolas Pennequin 12 - * Copyright (C) 2011-2013 Michael Sevakis 10 + * Copyright (C) 2024 - Tsiry Sandratraina 13 11 * 14 12 * This program is free software; you can redistribute it and/or 15 13 * modify it under the terms of the GNU General Public License ··· 27 25 #include "appevents.h" 28 26 29 27 bool server_is_initialized = false; 30 - 31 - /* Event queues */ 32 - // struct event_queue server_queue SHAREDBSS_ATTR; 33 - // static struct queue_sender_list server_queue_sender_list SHAREDBSS_ATTR; 34 28 35 29 /* Server thread */ 36 30 static long server_stack[(DEFAULT_STACK_SIZE * 4)/sizeof(long)];
+1 -7
apps/server_thread.h
··· 7 7 * \/ \/ \/ \/ \/ 8 8 * $Id$ 9 9 * 10 - * Copyright (C) 2005-2007 Miika Pekkarinen 11 - * Copyright (C) 2007-2008 Nicolas Pennequin 12 - * Copyright (C) 2011-2013 Michael Sevakis 10 + * Copyright (C) 2024 - Tsiry Sandratraina 13 11 * 14 12 * This program is free software; you can redistribute it and/or 15 13 * modify it under the terms of the GNU General Public License ··· 22 20 ****************************************************************************/ 23 21 #ifndef SERVER_THREAD_H 24 22 #define SERVER_THREAD_H 25 - 26 - 27 - /* (*) If you change these, you must check audio_clear_track_notifications 28 - in playback.c for correctness */ 29 23 30 24 void server_init(void); 31 25
+1
build.zig
··· 4241 4241 "apps/keymaps/keymap-touchscreen.c", 4242 4242 "apps/keymaps/keymap-sdl.c", 4243 4243 "apps/server_thread.c", 4244 + "apps/broker_thread.c", 4244 4245 "build/lang/lang_core.c", 4245 4246 }; 4246 4247
+5
crates/graphql/Cargo.toml
··· 10 10 anyhow = "1.0.87" 11 11 async-graphql = "7.0.9" 12 12 async-graphql-actix-web = "7.0.9" 13 + futures-channel = "0.3.31" 14 + futures-util = "0.3.31" 15 + md5 = "0.7.0" 16 + once_cell = "1.20.2" 13 17 owo-colors = "4.1.0" 14 18 reqwest = {version = "0.12.7", features = ["rustls-tls", "json"], default-features = false} 15 19 rockbox-library = {path = "../library"} 16 20 rockbox-sys = {path = "../sys"} 17 21 serde = "1.0.210" 18 22 serde_json = "1.0.128" 23 + slab = "0.4.9" 19 24 sqlx = {version = "0.8.2", features = ["runtime-tokio", "tls-rustls", "sqlite", "chrono", "derive", "macros"]} 20 25 tokio = {version = "1.36.0", features = ["full"]}
+5 -3
crates/graphql/src/lib.rs
··· 1 - use async_graphql::{EmptySubscription, Schema}; 2 - use schema::{Mutation, Query}; 1 + use async_graphql::Schema; 2 + use schema::{Mutation, Query, Subscription}; 3 3 4 4 pub mod schema; 5 5 pub mod server; 6 - pub type RockboxSchema = Schema<Query, Mutation, EmptySubscription>; 6 + pub mod simplebroker; 7 + 8 + pub type RockboxSchema = Schema<Query, Mutation, Subscription>; 7 9 8 10 pub const AUDIO_EXTENSIONS: [&str; 17] = [ 9 11 "mp3", "ogg", "flac", "m4a", "aac", "mp4", "alac", "wav", "wv", "mpc", "aiff", "ac3", "opus",
+5 -2
crates/graphql/src/schema/mod.rs
··· 1 - use async_graphql::MergedObject; 1 + use async_graphql::{MergedObject, MergedSubscription}; 2 2 use browse::BrowseQuery; 3 3 use library::LibraryQuery; 4 - use playback::{PlaybackMutation, PlaybackQuery}; 4 + use playback::{PlaybackMutation, PlaybackQuery, PlaybackSubscription}; 5 5 use playlist::{PlaylistMutation, PlaylistQuery}; 6 6 use settings::SettingsQuery; 7 7 use sound::{SoundMutation, SoundQuery}; ··· 30 30 31 31 #[derive(MergedObject, Default)] 32 32 pub struct Mutation(PlaybackMutation, PlaylistMutation, SoundMutation); 33 + 34 + #[derive(MergedSubscription, Default)] 35 + pub struct Subscription(PlaybackSubscription);
+23
crates/graphql/src/schema/objects/audio_status.rs
··· 1 + use async_graphql::*; 2 + use rockbox_sys as rb; 3 + use serde::{Deserialize, Serialize}; 4 + 5 + #[derive(Default, Clone, Serialize, Deserialize)] 6 + pub struct AudioStatus { 7 + pub status: i32, 8 + } 9 + 10 + #[Object] 11 + impl AudioStatus { 12 + async fn status(&self) -> i32 { 13 + self.status 14 + } 15 + } 16 + 17 + impl From<rb::types::audio_status::AudioStatus> for AudioStatus { 18 + fn from(status: rb::types::audio_status::AudioStatus) -> Self { 19 + Self { 20 + status: status.status, 21 + } 22 + } 23 + }
+1
crates/graphql/src/schema/objects/mod.rs
··· 1 1 pub mod album; 2 2 pub mod artist; 3 + pub mod audio_status; 3 4 pub mod compressor_settings; 4 5 pub mod entry; 5 6 pub mod eq_band_setting;
+46 -4
crates/graphql/src/schema/playback.rs
··· 1 1 use std::sync::{mpsc::Sender, Arc, Mutex}; 2 2 3 + use crate::schema::objects; 3 4 use async_graphql::*; 5 + use futures_util::Stream; 6 + use rockbox_library::repo; 4 7 use rockbox_sys::{ 5 8 events::RockboxCommand, 6 9 types::{audio_status::AudioStatus, file_position::FilePosition, mp3_entry::Mp3Entry}, 7 10 }; 11 + use sqlx::{Pool, Sqlite}; 8 12 9 - use crate::{rockbox_url, schema::objects::track::Track}; 13 + use crate::{rockbox_url, schema::objects::track::Track, simplebroker::SimpleBroker}; 10 14 11 15 #[derive(Default)] 12 16 pub struct PlaybackQuery; ··· 15 19 impl PlaybackQuery { 16 20 async fn status(&self) -> Result<i32, Error> { 17 21 let client = reqwest::Client::new(); 18 - let url = format!("{}/audio_status", rockbox_url()); 22 + let url = format!("{}/player/status", rockbox_url()); 19 23 let response = client.get(&url).send().await?; 20 24 let response = response.json::<AudioStatus>().await?; 21 25 Ok(response.status) 22 26 } 23 27 24 28 async fn current_track(&self, ctx: &Context<'_>) -> Result<Option<Track>, Error> { 29 + let pool = ctx.data::<Pool<Sqlite>>()?; 25 30 let client = ctx.data::<reqwest::Client>().unwrap(); 26 31 let url = format!("{}/player/current-track", rockbox_url()); 27 32 let response = client.get(&url).send().await?; 28 33 let track = response.json::<Option<Mp3Entry>>().await?; 29 - Ok(track.map(|t| t.into())) 34 + let mut track: Option<Track> = track.map(|t| t.into()); 35 + let path: Option<String> = track.as_ref().map(|t| t.path.clone()); 36 + if let Some(path) = path { 37 + let hash = format!("{:x}", md5::compute(path.as_bytes())); 38 + if let Some(metadata) = repo::track::find_by_md5(pool.clone(), &hash).await? { 39 + track.as_mut().unwrap().id = Some(metadata.id); 40 + track.as_mut().unwrap().album_art = metadata.album_art; 41 + track.as_mut().unwrap().album_id = Some(metadata.album_id); 42 + track.as_mut().unwrap().artist_id = Some(metadata.artist_id); 43 + } 44 + } 45 + Ok(track) 30 46 } 31 47 32 48 async fn next_track(&self, ctx: &Context<'_>) -> Result<Option<Track>, Error> { 49 + let pool = ctx.data::<Pool<Sqlite>>()?; 33 50 let client = ctx.data::<reqwest::Client>().unwrap(); 34 51 let url = format!("{}/player/next-track", rockbox_url()); 35 52 let response = client.get(&url).send().await?; 36 53 let track = response.json::<Option<Mp3Entry>>().await?; 37 - Ok(track.map(|t| t.into())) 54 + let mut track: Option<Track> = track.map(|t| t.into()); 55 + let path: Option<String> = track.as_ref().map(|t| t.path.clone()); 56 + if let Some(path) = path { 57 + let hash = format!("{:x}", md5::compute(path.as_bytes())); 58 + if let Some(metadata) = repo::track::find_by_md5(pool.clone(), &hash).await? { 59 + track.as_mut().unwrap().id = Some(metadata.id); 60 + track.as_mut().unwrap().album_art = metadata.album_art; 61 + track.as_mut().unwrap().album_id = Some(metadata.album_id); 62 + track.as_mut().unwrap().artist_id = Some(metadata.artist_id); 63 + } 64 + } 65 + Ok(track) 38 66 } 39 67 40 68 async fn get_file_position(&self, ctx: &Context<'_>) -> Result<i32, Error> { ··· 122 150 Ok("hard_stop".to_string()) 123 151 } 124 152 } 153 + 154 + #[derive(Default)] 155 + pub struct PlaybackSubscription; 156 + 157 + #[Subscription] 158 + impl PlaybackSubscription { 159 + async fn currently_playing_song(&self) -> impl Stream<Item = Track> { 160 + SimpleBroker::<Track>::subscribe() 161 + } 162 + 163 + async fn playback_status(&self) -> impl Stream<Item = objects::audio_status::AudioStatus> { 164 + SimpleBroker::<objects::audio_status::AudioStatus>::subscribe() 165 + } 166 + }
+19 -4
crates/graphql/src/server.rs
··· 7 7 use actix_files::{self as fs, NamedFile}; 8 8 use actix_web::{ 9 9 error::ErrorNotFound, 10 + guard, 10 11 http::header::{ContentDisposition, DispositionType, HOST}, 11 12 web::{self, Data}, 12 13 App, HttpRequest, HttpResponse, HttpServer, Result, 13 14 }; 14 15 use anyhow::Error; 15 - use async_graphql::{http::GraphiQLSource, EmptySubscription, Schema}; 16 - use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse}; 16 + use async_graphql::{http::GraphiQLSource, Schema}; 17 + use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse, GraphQLSubscription}; 17 18 use owo_colors::OwoColorize; 18 19 use rockbox_library::{create_connection_pool, repo}; 19 20 use rockbox_sys::events::RockboxCommand; 20 21 use sqlx::{Pool, Sqlite}; 21 22 22 23 use crate::{ 23 - schema::{Mutation, Query}, 24 + schema::{Mutation, Query, Subscription}, 24 25 RockboxSchema, 25 26 }; 27 + 28 + async fn index_ws( 29 + schema: web::Data<RockboxSchema>, 30 + req: HttpRequest, 31 + payload: web::Payload, 32 + ) -> Result<HttpResponse> { 33 + GraphQLSubscription::new(Schema::clone(&*schema)).start(&req, payload) 34 + } 26 35 27 36 #[actix_web::post("/graphql")] 28 37 async fn index_graphql(schema: web::Data<RockboxSchema>, req: GraphQLRequest) -> GraphQLResponse { ··· 83 92 let schema = Schema::build( 84 93 Query::default(), 85 94 Mutation::default(), 86 - EmptySubscription::default(), 95 + Subscription::default(), 87 96 ) 88 97 .data(cmd_tx) 89 98 .data(client) ··· 111 120 .wrap(cors) 112 121 .service(index_graphql) 113 122 .service(index_graphiql) 123 + .service( 124 + web::resource("/graphql") 125 + .guard(guard::Get()) 126 + .guard(guard::Header("upgrade", "websocket")) 127 + .to(index_ws), 128 + ) 114 129 .service(fs::Files::new("/covers", covers_path).show_files_listing()) 115 130 .route("/tracks/{id}", web::get().to(index_file)) 116 131 .route("/tracks/{id}", web::head().to(index_file))
+68
crates/graphql/src/simplebroker.rs
··· 1 + use std::{ 2 + any::{Any, TypeId}, 3 + collections::HashMap, 4 + marker::PhantomData, 5 + pin::Pin, 6 + sync::Mutex, 7 + task::{Context, Poll}, 8 + }; 9 + 10 + use futures_channel::mpsc::{self, UnboundedReceiver, UnboundedSender}; 11 + use futures_util::{Stream, StreamExt}; 12 + use once_cell::sync::Lazy; 13 + use slab::Slab; 14 + 15 + static SUBSCRIBERS: Lazy<Mutex<HashMap<TypeId, Box<dyn Any + Send>>>> = Lazy::new(Default::default); 16 + 17 + struct Senders<T>(Slab<UnboundedSender<T>>); 18 + 19 + struct BrokerStream<T: Sync + Send + Clone + 'static>(usize, UnboundedReceiver<T>); 20 + 21 + fn with_senders<T, F, R>(f: F) -> R 22 + where 23 + T: Sync + Send + Clone + 'static, 24 + F: FnOnce(&mut Senders<T>) -> R, 25 + { 26 + let mut map = SUBSCRIBERS.lock().unwrap(); 27 + let senders = map 28 + .entry(TypeId::of::<Senders<T>>()) 29 + .or_insert_with(|| Box::new(Senders::<T>(Default::default()))); 30 + f(senders.downcast_mut::<Senders<T>>().unwrap()) 31 + } 32 + 33 + impl<T: Sync + Send + Clone + 'static> Drop for BrokerStream<T> { 34 + fn drop(&mut self) { 35 + with_senders::<T, _, _>(|senders| senders.0.remove(self.0)); 36 + } 37 + } 38 + 39 + impl<T: Sync + Send + Clone + 'static> Stream for BrokerStream<T> { 40 + type Item = T; 41 + 42 + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { 43 + self.1.poll_next_unpin(cx) 44 + } 45 + } 46 + 47 + /// A simple broker based on memory 48 + pub struct SimpleBroker<T>(PhantomData<T>); 49 + 50 + impl<T: Sync + Send + Clone + 'static> SimpleBroker<T> { 51 + /// Publish a message that all subscription streams can receive. 52 + pub fn publish(msg: T) { 53 + with_senders::<T, _, _>(|senders| { 54 + for (_, sender) in senders.0.iter_mut() { 55 + sender.start_send(msg.clone()).ok(); 56 + } 57 + }); 58 + } 59 + 60 + /// Subscribe to the message of the specified type and returns a `Stream`. 61 + pub fn subscribe() -> impl Stream<Item = T> { 62 + with_senders::<T, _, _>(|senders| { 63 + let (tx, rx) = mpsc::unbounded(); 64 + let id = senders.0.insert(tx); 65 + BrokerStream(id, rx) 66 + }) 67 + } 68 + }
+1 -1
crates/library/src/entity/track.rs
··· 1 1 use chrono::{DateTime, Utc}; 2 2 use serde::{Deserialize, Serialize}; 3 3 4 - #[derive(sqlx::FromRow, Default, Debug, Serialize, Deserialize)] 4 + #[derive(sqlx::FromRow, Default, Debug, Clone, Serialize, Deserialize)] 5 5 pub struct Track { 6 6 pub id: String, 7 7 pub path: String,
+1 -1
crates/rpc/src/playback.rs
··· 99 99 ) -> Result<tonic::Response<StatusResponse>, tonic::Status> { 100 100 let response = self 101 101 .client 102 - .get(&format!("{}/audio_status", rockbox_url())) 102 + .get(&format!("{}/player/status", rockbox_url())) 103 103 .send() 104 104 .await 105 105 .map_err(|e| tonic::Status::internal(e.to_string()))?
+1
crates/server/Cargo.toml
··· 8 8 9 9 [dependencies] 10 10 anyhow = "1.0.89" 11 + md5 = "0.7.0" 11 12 owo-colors = "4.0.0" 12 13 queryst = "3.0.0" 13 14 reqwest = {version = "0.12.7", features = ["blocking", "rustls-tls"], default-features = false}
+1 -1
crates/server/openapi.json
··· 718 718 ] 719 719 } 720 720 }, 721 - "/player/prev": { 721 + "/player/previous": { 722 722 "parameters": [], 723 723 "put": { 724 724 "summary": "Play Previous Track",
+40 -1
crates/server/src/lib.rs
··· 1 1 use handlers::*; 2 2 3 3 use http::RockboxHttpServer; 4 + use rockbox_graphql::{ 5 + schema::objects::{audio_status::AudioStatus, track::Track}, 6 + simplebroker::SimpleBroker, 7 + }; 8 + use rockbox_library::repo; 9 + use rockbox_sys as rb; 4 10 use rockbox_sys::events::RockboxCommand; 5 11 use std::{ 6 12 ffi::c_char, ··· 116 122 client.put(&format!("{}/player/next", url)).send().unwrap(); 117 123 } 118 124 RockboxCommand::Prev => { 119 - client.put(&format!("{}/player/prev", url)).send().unwrap(); 125 + client 126 + .put(&format!("{}/player/previous", url)) 127 + .send() 128 + .unwrap(); 120 129 } 121 130 RockboxCommand::FfRewind(newtime) => { 122 131 client ··· 177 186 } 178 187 }); 179 188 } 189 + 190 + #[no_mangle] 191 + pub extern "C" fn start_broker() { 192 + let rt = tokio::runtime::Runtime::new().unwrap(); 193 + let pool = rt 194 + .block_on(rockbox_library::create_connection_pool()) 195 + .unwrap(); 196 + loop { 197 + let playback_status: AudioStatus = rb::playback::status().into(); 198 + SimpleBroker::publish(playback_status); 199 + match rb::playback::current_track() { 200 + Some(current_track) => { 201 + let hash = format!("{:x}", md5::compute(current_track.path.as_bytes())); 202 + if let Ok(Some(metadata)) = 203 + rt.block_on(repo::track::find_by_md5(pool.clone(), &hash)) 204 + { 205 + let mut track: Track = current_track.into(); 206 + track.id = Some(metadata.id); 207 + track.album_art = metadata.album_art; 208 + track.album_id = Some(metadata.album_id); 209 + track.artist_id = Some(metadata.artist_id); 210 + SimpleBroker::publish(track); 211 + } 212 + } 213 + None => {} 214 + }; 215 + thread::sleep(std::time::Duration::from_millis(100)); 216 + rb::system::sleep(rb::HZ); 217 + } 218 + }
+1 -1
crates/sys/src/types/audio_status.rs
··· 1 1 use serde::{Deserialize, Serialize}; 2 2 3 - #[derive(Debug, Serialize, Deserialize)] 3 + #[derive(Debug, Clone, Serialize, Deserialize)] 4 4 pub struct AudioStatus { 5 5 pub status: i32, 6 6 }
+5 -1
webui/rockbox/.storybook/preview.tsx
··· 3 3 import Providers from "../src/Providers"; 4 4 import "../src/index.css"; 5 5 import { MemoryRouter, Routes, Route } from "react-router-dom"; 6 + import { mocks } from "../src/mocks"; 7 + import { MockedProvider } from "@apollo/client/testing"; 6 8 7 9 const preview: Preview = { 8 10 parameters: { ··· 31 33 reactRouterDecorator, 32 34 (Story) => ( 33 35 <Providers> 34 - <Story /> 36 + <MockedProvider mocks={mocks} addTypename={false}> 37 + <Story /> 38 + </MockedProvider> 35 39 </Providers> 36 40 ), 37 41 ];
webui/rockbox/bun.lockb

This is a binary file and will not be displayed.

+1 -1
webui/rockbox/codegen.ts
··· 2 2 3 3 const config: CodegenConfig = { 4 4 schema: "http://127.0.0.1:6062/graphql", 5 - documents: ["src/**/*.tsx"], 5 + documents: ["src/**/*.tsx", "src/**/*.ts"], 6 6 ignoreNoDocuments: true, // for better experience with the watcher 7 7 generates: { 8 8 "src/Hooks/GraphQL.tsx": {
+73 -1
webui/rockbox/graphql.schema.json
··· 6 6 "mutationType": { 7 7 "name": "Mutation" 8 8 }, 9 - "subscriptionType": null, 9 + "subscriptionType": { 10 + "name": "Subscription" 11 + }, 10 12 "types": [ 11 13 { 12 14 "kind": "OBJECT", ··· 271 273 "ofType": null 272 274 } 273 275 } 276 + } 277 + }, 278 + "isDeprecated": false, 279 + "deprecationReason": null 280 + } 281 + ], 282 + "inputFields": null, 283 + "interfaces": [], 284 + "enumValues": null, 285 + "possibleTypes": null 286 + }, 287 + { 288 + "kind": "OBJECT", 289 + "name": "AudioStatus", 290 + "description": null, 291 + "fields": [ 292 + { 293 + "name": "status", 294 + "description": null, 295 + "args": [], 296 + "type": { 297 + "kind": "NON_NULL", 298 + "name": null, 299 + "ofType": { 300 + "kind": "SCALAR", 301 + "name": "Int", 302 + "ofType": null 274 303 } 275 304 }, 276 305 "isDeprecated": false, ··· 1840 1869 "fields": null, 1841 1870 "inputFields": null, 1842 1871 "interfaces": null, 1872 + "enumValues": null, 1873 + "possibleTypes": null 1874 + }, 1875 + { 1876 + "kind": "OBJECT", 1877 + "name": "Subscription", 1878 + "description": null, 1879 + "fields": [ 1880 + { 1881 + "name": "currentlyPlayingSong", 1882 + "description": null, 1883 + "args": [], 1884 + "type": { 1885 + "kind": "NON_NULL", 1886 + "name": null, 1887 + "ofType": { 1888 + "kind": "OBJECT", 1889 + "name": "Track", 1890 + "ofType": null 1891 + } 1892 + }, 1893 + "isDeprecated": false, 1894 + "deprecationReason": null 1895 + }, 1896 + { 1897 + "name": "playbackStatus", 1898 + "description": null, 1899 + "args": [], 1900 + "type": { 1901 + "kind": "NON_NULL", 1902 + "name": null, 1903 + "ofType": { 1904 + "kind": "OBJECT", 1905 + "name": "AudioStatus", 1906 + "ofType": null 1907 + } 1908 + }, 1909 + "isDeprecated": false, 1910 + "deprecationReason": null 1911 + } 1912 + ], 1913 + "inputFields": null, 1914 + "interfaces": [], 1843 1915 "enumValues": null, 1844 1916 "possibleTypes": null 1845 1917 },
+28 -21
webui/rockbox/src/Components/AlbumDetails/AlbumDetails.test.tsx
··· 3 3 import AlbumDetails from "./AlbumDetails"; 4 4 import { tracks } from "./mocks"; 5 5 import { MemoryRouter } from "react-router-dom"; 6 + import { MockedProvider } from "@apollo/client/testing"; 7 + import { mocks } from "../../mocks"; 8 + import { RecoilRoot } from "recoil"; 6 9 7 10 describe("AlbumDetails", () => { 8 11 it("should render", () => { ··· 13 16 const onUnlike = vi.fn(); 14 17 const { container } = render( 15 18 <MemoryRouter initialEntries={["/"]}> 16 - <AlbumDetails 17 - onGoBack={onGoBack} 18 - onLike={onLike} 19 - onPlayAll={onPlayAll} 20 - onShuffleAll={onShuffleAll} 21 - onUnlike={onUnlike} 22 - tracks={tracks} 23 - album={{ 24 - id: "1", 25 - title: "One Cold Night (Live)", 26 - artist: "Seether", 27 - year: 2006, 28 - albumArt: 29 - "https://resources.tidal.com/images/f6f5f0a6/dc95/4561/9ca6/6ba1e0f6a062/320x320.jpg", 30 - artistId: "1", 31 - md5: "md5", 32 - yearString: "2006", 33 - tracks: [], 34 - }} 35 - volumes={[]} 36 - /> 19 + <MockedProvider mocks={mocks}> 20 + <RecoilRoot> 21 + <AlbumDetails 22 + onGoBack={onGoBack} 23 + onLike={onLike} 24 + onPlayAll={onPlayAll} 25 + onShuffleAll={onShuffleAll} 26 + onUnlike={onUnlike} 27 + tracks={tracks} 28 + album={{ 29 + id: "1", 30 + title: "One Cold Night (Live)", 31 + artist: "Seether", 32 + year: 2006, 33 + albumArt: 34 + "https://resources.tidal.com/images/f6f5f0a6/dc95/4561/9ca6/6ba1e0f6a062/320x320.jpg", 35 + artistId: "1", 36 + md5: "md5", 37 + yearString: "2006", 38 + tracks: [], 39 + }} 40 + volumes={[]} 41 + /> 42 + </RecoilRoot> 43 + </MockedProvider> 37 44 </MemoryRouter> 38 45 ); 39 46 expect(container).toMatchSnapshot();
+19 -62
webui/rockbox/src/Components/AlbumDetails/__snapshots__/AlbumDetails.test.tsx.snap
··· 177 177 class="css-10asy2d" 178 178 > 179 179 <svg 180 + fill="none" 180 181 height="32" 181 - viewBox="0 0 512 512" 182 182 width="32" 183 183 xmlns="http://www.w3.org/2000/svg" 184 184 > 185 - <title> 186 - Pause 187 - </title> 188 185 <path 189 - d="M208 432h-48a16 16 0 0 1-16-16V96a16 16 0 0 1 16-16h48a16 16 0 0 1 16 16v320a16 16 0 0 1-16 16zm144 0h-48a16 16 0 0 1-16-16V96a16 16 0 0 1 16-16h48a16 16 0 0 1 16 16v320a16 16 0 0 1-16 16z" 186 + d="M10.657 27a2.842 2.842 0 0 1-1.258-.292C8.536 26.283 8 25.458 8 24.563V6.438c0-.899.536-1.721 1.399-2.146a2.856 2.856 0 0 1 2.571.028l17.817 9.273c.755.411 1.213 1.131 1.213 1.906 0 .774-.458 1.495-1.213 1.906l-17.82 9.275a2.846 2.846 0 0 1-1.31.32Z" 190 187 style="fill: #000;" 191 188 /> 192 189 </svg> ··· 231 228 <div 232 229 class="css-frud38" 233 230 > 234 - <img 235 - alt="Album cover" 236 - class="css-31cka" 237 - src="https://resources.tidal.com/images/fe6787d5/4ba5/4d3e/8576/48943ee6a768/320x320.jpg" 238 - /> 239 231 <div 240 - class="css-rbpzuh" 232 + class="css-4ijxew" 241 233 > 242 - <div 243 - class="css-1cojt9j" 234 + <svg 235 + height="28" 236 + viewBox="0 0 24 24" 237 + width="28" 238 + xmlns="http://www.w3.org/2000/svg" 244 239 > 245 - Drankin N Smokin 246 - </div> 240 + <path 241 + d="M8.1 4.65v11.26a3.45 3.45 0 1 0 1.5 2.84V5.85l10.2-2.36v10.62A3.45 3.45 0 1 0 21.3 17V1.61Zm-2 16a2 2 0 1 1 2-2 2 2 0 0 1-1.95 2.05Zm11.7-1.8a1.95 1.95 0 1 1 2-1.85 2 2 0 0 1-1.95 1.9Z" 242 + fill="#b1b2b5" 243 + /> 244 + </svg> 245 + </div> 246 + <div 247 + class="css-rbpzuh" 248 + > 247 249 <div 248 - style="display: flex; flex-direction: row; align-items: center; justify-content: space-between;" 250 + style="color: rgb(177, 178, 181); text-align: center;" 249 251 > 250 - <div 251 - class="css-1jnbdma" 252 - > 253 - 02:03 254 - </div> 255 - <div 256 - class="css-bdl275" 257 - > 258 - Future, Lil Uzi Vert 259 - <span 260 - class="css-c9cgcr" 261 - > 262 - - 263 - </span> 264 - Pluto x Baby Pluto (Deluxe) 265 - </div> 266 - <div 267 - class="css-1jnbdma" 268 - > 269 - 04:15 270 - </div> 271 - </div> 272 - <div 273 - class="css-1n7uud4" 274 - > 275 - <div 276 - aria-label="48% Loaded" 277 - aria-valuemax="100" 278 - aria-valuemin="0" 279 - aria-valuenow="48.3219508605943" 280 - class="" 281 - data-baseweb="progress-bar" 282 - role="progressbar" 283 - > 284 - <div 285 - class="" 286 - > 287 - <div 288 - class="" 289 - > 290 - <div 291 - class="" 292 - /> 293 - </div> 294 - </div> 295 - </div> 252 + No track playing 296 253 </div> 297 254 </div> 298 255 </div>
+13 -9
webui/rockbox/src/Components/Albums/Albums.test.tsx
··· 4 4 import { albums } from "./mocks"; 5 5 import Providers from "../../Providers"; 6 6 import { MemoryRouter } from "react-router-dom"; 7 + import { MockedProvider } from "@apollo/client/testing"; 8 + import { mocks } from "../../mocks"; 7 9 8 10 describe("Albums", () => { 9 11 it("should render", () => { ··· 13 15 const onLike = vi.fn(); 14 16 const { container } = render( 15 17 <MemoryRouter initialEntries={["/"]}> 16 - <Providers> 17 - <Albums 18 - albums={albums} 19 - onPlay={onPlay} 20 - onFilter={onFilter} 21 - onUnLike={onUnLike} 22 - onLike={onLike} 23 - /> 24 - </Providers> 18 + <MockedProvider mocks={mocks}> 19 + <Providers> 20 + <Albums 21 + albums={albums} 22 + onPlay={onPlay} 23 + onFilter={onFilter} 24 + onUnLike={onUnLike} 25 + onLike={onLike} 26 + /> 27 + </Providers> 28 + </MockedProvider> 25 29 </MemoryRouter> 26 30 ); 27 31 expect(container).toMatchSnapshot();
+70 -113
webui/rockbox/src/Components/Albums/__snapshots__/Albums.test.tsx.snap
··· 180 180 class="css-10asy2d" 181 181 > 182 182 <svg 183 + fill="none" 183 184 height="32" 184 - viewBox="0 0 512 512" 185 185 width="32" 186 186 xmlns="http://www.w3.org/2000/svg" 187 187 > 188 - <title> 189 - Pause 190 - </title> 191 188 <path 192 - d="M208 432h-48a16 16 0 0 1-16-16V96a16 16 0 0 1 16-16h48a16 16 0 0 1 16 16v320a16 16 0 0 1-16 16zm144 0h-48a16 16 0 0 1-16-16V96a16 16 0 0 1 16-16h48a16 16 0 0 1 16 16v320a16 16 0 0 1-16 16z" 189 + d="M10.657 27a2.842 2.842 0 0 1-1.258-.292C8.536 26.283 8 25.458 8 24.563V6.438c0-.899.536-1.721 1.399-2.146a2.856 2.856 0 0 1 2.571.028l17.817 9.273c.755.411 1.213 1.131 1.213 1.906 0 .774-.458 1.495-1.213 1.906l-17.82 9.275a2.846 2.846 0 0 1-1.31.32Z" 193 190 style="fill: #000;" 194 191 /> 195 192 </svg> ··· 234 231 <div 235 232 class="css-frud38" 236 233 > 237 - <img 238 - alt="Album cover" 239 - class="css-31cka" 240 - src="https://resources.tidal.com/images/fe6787d5/4ba5/4d3e/8576/48943ee6a768/320x320.jpg" 241 - /> 242 234 <div 243 - class="css-rbpzuh" 235 + class="css-4ijxew" 244 236 > 245 - <div 246 - class="css-1cojt9j" 247 - > 248 - Drankin N Smokin 249 - </div> 250 - <div 251 - style="display: flex; flex-direction: row; align-items: center; justify-content: space-between;" 237 + <svg 238 + height="28" 239 + viewBox="0 0 24 24" 240 + width="28" 241 + xmlns="http://www.w3.org/2000/svg" 252 242 > 253 - <div 254 - class="css-1jnbdma" 255 - > 256 - 02:03 257 - </div> 258 - <div 259 - class="css-bdl275" 260 - > 261 - Future, Lil Uzi Vert 262 - <span 263 - class="css-c9cgcr" 264 - > 265 - - 266 - </span> 267 - Pluto x Baby Pluto (Deluxe) 268 - </div> 269 - <div 270 - class="css-1jnbdma" 271 - > 272 - 04:15 273 - </div> 274 - </div> 243 + <path 244 + d="M8.1 4.65v11.26a3.45 3.45 0 1 0 1.5 2.84V5.85l10.2-2.36v10.62A3.45 3.45 0 1 0 21.3 17V1.61Zm-2 16a2 2 0 1 1 2-2 2 2 0 0 1-1.95 2.05Zm11.7-1.8a1.95 1.95 0 1 1 2-1.85 2 2 0 0 1-1.95 1.9Z" 245 + fill="#b1b2b5" 246 + /> 247 + </svg> 248 + </div> 249 + <div 250 + class="css-rbpzuh" 251 + > 275 252 <div 276 - class="css-1n7uud4" 253 + style="color: rgb(177, 178, 181); text-align: center;" 277 254 > 278 - <div 279 - aria-label="48% Loaded" 280 - aria-valuemax="100" 281 - aria-valuemin="0" 282 - aria-valuenow="48.3219508605943" 283 - class="ae" 284 - data-baseweb="progress-bar" 285 - role="progressbar" 286 - > 287 - <div 288 - class="af ag ah ai aj" 289 - > 290 - <div 291 - class="ak al am an ao ap aq ar" 292 - > 293 - <div 294 - class="ak al am an as at ae au av" 295 - /> 296 - </div> 297 - </div> 298 - </div> 255 + No track playing 299 256 </div> 300 257 </div> 301 258 </div> ··· 334 291 class="css-16zcn16" 335 292 > 336 293 <div 337 - class="aw af ar ax ay az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bh bi bj bk bl bm bn bo" 294 + class="ae af ag ah ai aj ak al am an ao ap aq ar as at au av aw ax ay az b0 b1 b2 b3 b4 b5 b6 b7 b8" 338 295 data-baseweb="input" 339 296 > 340 297 <div 341 - class="af bp bq br b7 b8 bd be bf bg bn bs bt bl bu bv" 298 + class="af b9 ba bb ar as ax ay az b0 b7 bc bd b5 be bf" 342 299 > 343 300 <svg 344 301 data-test="icon-ActionActiveSearch" ··· 356 313 </svg> 357 314 </div> 358 315 <div 359 - class="af ae bw b7 b8 bd be bf bg bt bl" 316 + class="af bg bh ar as ax ay az b0 bd b5" 360 317 data-baseweb="base-input" 361 318 > 362 319 <input 363 320 aria-invalid="false" 364 321 aria-required="false" 365 322 autocomplete="on" 366 - class="aw bl bx by bz c0 c1 c2 c3 c4 c5 ae c6 c7 c8 c9 ca cb bm cc bd cd bf bg ce cf cg" 323 + class="ae b5 bi bj bk bl bm bn bo bp bq bg br bs bt bu bv bw b6 bx ax by az b0 bz c0 c1" 367 324 inputmode="text" 368 325 name="filter" 369 326 placeholder="Search albums" ··· 377 334 style="margin-bottom: 100px;" 378 335 > 379 336 <div 380 - class="ch ci" 337 + class="c2 c3" 381 338 > 382 339 <div 383 - class="aw af cj ci ck cl cm cn co cp cq cr cs ct cu cv cw cx cy cz" 340 + class="ae af c4 c3 c5 c6 c7 c8 c9 ca cb cc cd ce cf cg ch ci cj ck" 384 341 > 385 342 <div 386 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 343 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 387 344 > 388 345 <div 389 346 style="position: relative; width: 100%;" ··· 509 466 </div> 510 467 </div> 511 468 <div 512 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 469 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 513 470 > 514 471 <div 515 472 style="position: relative; width: 100%;" ··· 635 592 </div> 636 593 </div> 637 594 <div 638 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 595 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 639 596 > 640 597 <div 641 598 style="position: relative; width: 100%;" ··· 761 718 </div> 762 719 </div> 763 720 <div 764 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 721 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 765 722 > 766 723 <div 767 724 style="position: relative; width: 100%;" ··· 887 844 </div> 888 845 </div> 889 846 <div 890 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 847 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 891 848 > 892 849 <div 893 850 style="position: relative; width: 100%;" ··· 1013 970 </div> 1014 971 </div> 1015 972 <div 1016 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 973 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 1017 974 > 1018 975 <div 1019 976 style="position: relative; width: 100%;" ··· 1139 1096 </div> 1140 1097 </div> 1141 1098 <div 1142 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 1099 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 1143 1100 > 1144 1101 <div 1145 1102 style="position: relative; width: 100%;" ··· 1265 1222 </div> 1266 1223 </div> 1267 1224 <div 1268 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 1225 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 1269 1226 > 1270 1227 <div 1271 1228 style="position: relative; width: 100%;" ··· 1391 1348 </div> 1392 1349 </div> 1393 1350 <div 1394 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 1351 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 1395 1352 > 1396 1353 <div 1397 1354 style="position: relative; width: 100%;" ··· 1517 1474 </div> 1518 1475 </div> 1519 1476 <div 1520 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 1477 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 1521 1478 > 1522 1479 <div 1523 1480 style="position: relative; width: 100%;" ··· 1643 1600 </div> 1644 1601 </div> 1645 1602 <div 1646 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 1603 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 1647 1604 > 1648 1605 <div 1649 1606 style="position: relative; width: 100%;" ··· 1769 1726 </div> 1770 1727 </div> 1771 1728 <div 1772 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 1729 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 1773 1730 > 1774 1731 <div 1775 1732 style="position: relative; width: 100%;" ··· 1895 1852 </div> 1896 1853 </div> 1897 1854 <div 1898 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 1855 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 1899 1856 > 1900 1857 <div 1901 1858 style="position: relative; width: 100%;" ··· 2021 1978 </div> 2022 1979 </div> 2023 1980 <div 2024 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 1981 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 2025 1982 > 2026 1983 <div 2027 1984 style="position: relative; width: 100%;" ··· 2147 2104 </div> 2148 2105 </div> 2149 2106 <div 2150 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 2107 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 2151 2108 > 2152 2109 <div 2153 2110 style="position: relative; width: 100%;" ··· 2273 2230 </div> 2274 2231 </div> 2275 2232 <div 2276 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 2233 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 2277 2234 > 2278 2235 <div 2279 2236 style="position: relative; width: 100%;" ··· 2399 2356 </div> 2400 2357 </div> 2401 2358 <div 2402 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 2359 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 2403 2360 > 2404 2361 <div 2405 2362 style="position: relative; width: 100%;" ··· 2525 2482 </div> 2526 2483 </div> 2527 2484 <div 2528 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 2485 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 2529 2486 > 2530 2487 <div 2531 2488 style="position: relative; width: 100%;" ··· 2651 2608 </div> 2652 2609 </div> 2653 2610 <div 2654 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 2611 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 2655 2612 > 2656 2613 <div 2657 2614 style="position: relative; width: 100%;" ··· 2777 2734 </div> 2778 2735 </div> 2779 2736 <div 2780 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 2737 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 2781 2738 > 2782 2739 <div 2783 2740 style="position: relative; width: 100%;" ··· 2903 2860 </div> 2904 2861 </div> 2905 2862 <div 2906 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 2863 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 2907 2864 > 2908 2865 <div 2909 2866 style="position: relative; width: 100%;" ··· 3029 2986 </div> 3030 2987 </div> 3031 2988 <div 3032 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 2989 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 3033 2990 > 3034 2991 <div 3035 2992 style="position: relative; width: 100%;" ··· 3155 3112 </div> 3156 3113 </div> 3157 3114 <div 3158 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 3115 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 3159 3116 > 3160 3117 <div 3161 3118 style="position: relative; width: 100%;" ··· 3281 3238 </div> 3282 3239 </div> 3283 3240 <div 3284 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 3241 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 3285 3242 > 3286 3243 <div 3287 3244 style="position: relative; width: 100%;" ··· 3407 3364 </div> 3408 3365 </div> 3409 3366 <div 3410 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 3367 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 3411 3368 > 3412 3369 <div 3413 3370 style="position: relative; width: 100%;" ··· 3533 3490 </div> 3534 3491 </div> 3535 3492 <div 3536 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 3493 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 3537 3494 > 3538 3495 <div 3539 3496 style="position: relative; width: 100%;" ··· 3659 3616 </div> 3660 3617 </div> 3661 3618 <div 3662 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 3619 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 3663 3620 > 3664 3621 <div 3665 3622 style="position: relative; width: 100%;" ··· 3785 3742 </div> 3786 3743 </div> 3787 3744 <div 3788 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 3745 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 3789 3746 > 3790 3747 <div 3791 3748 style="position: relative; width: 100%;" ··· 3911 3868 </div> 3912 3869 </div> 3913 3870 <div 3914 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 3871 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 3915 3872 > 3916 3873 <div 3917 3874 style="position: relative; width: 100%;" ··· 4037 3994 </div> 4038 3995 </div> 4039 3996 <div 4040 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 3997 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 4041 3998 > 4042 3999 <div 4043 4000 style="position: relative; width: 100%;" ··· 4163 4120 </div> 4164 4121 </div> 4165 4122 <div 4166 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 4123 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 4167 4124 > 4168 4125 <div 4169 4126 style="position: relative; width: 100%;" ··· 4289 4246 </div> 4290 4247 </div> 4291 4248 <div 4292 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 4249 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 4293 4250 > 4294 4251 <div 4295 4252 style="position: relative; width: 100%;" ··· 4415 4372 </div> 4416 4373 </div> 4417 4374 <div 4418 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 4375 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 4419 4376 > 4420 4377 <div 4421 4378 style="position: relative; width: 100%;" ··· 4541 4498 </div> 4542 4499 </div> 4543 4500 <div 4544 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 4501 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 4545 4502 > 4546 4503 <div 4547 4504 style="position: relative; width: 100%;" ··· 4667 4624 </div> 4668 4625 </div> 4669 4626 <div 4670 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 4627 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 4671 4628 > 4672 4629 <div 4673 4630 style="position: relative; width: 100%;" ··· 4793 4750 </div> 4794 4751 </div> 4795 4752 <div 4796 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 4753 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 4797 4754 > 4798 4755 <div 4799 4756 style="position: relative; width: 100%;" ··· 4919 4876 </div> 4920 4877 </div> 4921 4878 <div 4922 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 4879 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 4923 4880 > 4924 4881 <div 4925 4882 style="position: relative; width: 100%;" ··· 5045 5002 </div> 5046 5003 </div> 5047 5004 <div 5048 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 5005 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 5049 5006 > 5050 5007 <div 5051 5008 style="position: relative; width: 100%;" ··· 5171 5128 </div> 5172 5129 </div> 5173 5130 <div 5174 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 5131 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 5175 5132 > 5176 5133 <div 5177 5134 style="position: relative; width: 100%;" ··· 5297 5254 </div> 5298 5255 </div> 5299 5256 <div 5300 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 5257 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 5301 5258 > 5302 5259 <div 5303 5260 style="position: relative; width: 100%;" ··· 5423 5380 </div> 5424 5381 </div> 5425 5382 <div 5426 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 5383 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 5427 5384 > 5428 5385 <div 5429 5386 style="position: relative; width: 100%;" ··· 5549 5506 </div> 5550 5507 </div> 5551 5508 <div 5552 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 5509 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 5553 5510 > 5554 5511 <div 5555 5512 style="position: relative; width: 100%;" ··· 5675 5632 </div> 5676 5633 </div> 5677 5634 <div 5678 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 5635 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 5679 5636 > 5680 5637 <div 5681 5638 style="position: relative; width: 100%;" ··· 5801 5758 </div> 5802 5759 </div> 5803 5760 <div 5804 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 5761 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 5805 5762 > 5806 5763 <div 5807 5764 style="position: relative; width: 100%;" ··· 5927 5884 </div> 5928 5885 </div> 5929 5886 <div 5930 - class="aw ae d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df dg dh di dj dk dl" 5887 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6" 5931 5888 > 5932 5889 <div 5933 5890 style="position: relative; width: 100%;"
+19 -15
webui/rockbox/src/Components/ArtistDetails/ArtistDetails.test.tsx
··· 4 4 import { albums, tracks } from "./mocks"; 5 5 import Providers from "../../Providers"; 6 6 import { MemoryRouter } from "react-router-dom"; 7 + import { MockedProvider } from "@apollo/client/testing"; 8 + import { mocks } from "../../mocks"; 7 9 8 10 describe("ArtistDetails", () => { 9 11 it("should render", () => { 10 12 const { container } = render( 11 13 <MemoryRouter initialEntries={["/"]}> 12 - <Providers> 13 - <ArtistDetails 14 - name="Daft Punk" 15 - tracks={tracks} 16 - albums={albums} 17 - onPlayAll={vi.fn()} 18 - onShuffleAll={vi.fn()} 19 - onPlayAlbum={vi.fn()} 20 - onLikeAlbum={vi.fn()} 21 - onUnLikeAlbum={vi.fn()} 22 - onLikeTrack={vi.fn()} 23 - onUnlikeTrack={vi.fn()} 24 - onGoBack={vi.fn()} 25 - /> 26 - </Providers> 14 + <MockedProvider mocks={mocks}> 15 + <Providers> 16 + <ArtistDetails 17 + name="Daft Punk" 18 + tracks={tracks} 19 + albums={albums} 20 + onPlayAll={vi.fn()} 21 + onShuffleAll={vi.fn()} 22 + onPlayAlbum={vi.fn()} 23 + onLikeAlbum={vi.fn()} 24 + onUnLikeAlbum={vi.fn()} 25 + onLikeTrack={vi.fn()} 26 + onUnlikeTrack={vi.fn()} 27 + onGoBack={vi.fn()} 28 + /> 29 + </Providers> 30 + </MockedProvider> 27 31 </MemoryRouter> 28 32 ); 29 33 expect(container).toMatchSnapshot();
+26 -69
webui/rockbox/src/Components/ArtistDetails/__snapshots__/ArtistDetails.test.tsx.snap
··· 180 180 class="css-10asy2d" 181 181 > 182 182 <svg 183 + fill="none" 183 184 height="32" 184 - viewBox="0 0 512 512" 185 185 width="32" 186 186 xmlns="http://www.w3.org/2000/svg" 187 187 > 188 - <title> 189 - Pause 190 - </title> 191 188 <path 192 - d="M208 432h-48a16 16 0 0 1-16-16V96a16 16 0 0 1 16-16h48a16 16 0 0 1 16 16v320a16 16 0 0 1-16 16zm144 0h-48a16 16 0 0 1-16-16V96a16 16 0 0 1 16-16h48a16 16 0 0 1 16 16v320a16 16 0 0 1-16 16z" 189 + d="M10.657 27a2.842 2.842 0 0 1-1.258-.292C8.536 26.283 8 25.458 8 24.563V6.438c0-.899.536-1.721 1.399-2.146a2.856 2.856 0 0 1 2.571.028l17.817 9.273c.755.411 1.213 1.131 1.213 1.906 0 .774-.458 1.495-1.213 1.906l-17.82 9.275a2.846 2.846 0 0 1-1.31.32Z" 193 190 style="fill: #000;" 194 191 /> 195 192 </svg> ··· 234 231 <div 235 232 class="css-frud38" 236 233 > 237 - <img 238 - alt="Album cover" 239 - class="css-31cka" 240 - src="https://resources.tidal.com/images/fe6787d5/4ba5/4d3e/8576/48943ee6a768/320x320.jpg" 241 - /> 234 + <div 235 + class="css-4ijxew" 236 + > 237 + <svg 238 + height="28" 239 + viewBox="0 0 24 24" 240 + width="28" 241 + xmlns="http://www.w3.org/2000/svg" 242 + > 243 + <path 244 + d="M8.1 4.65v11.26a3.45 3.45 0 1 0 1.5 2.84V5.85l10.2-2.36v10.62A3.45 3.45 0 1 0 21.3 17V1.61Zm-2 16a2 2 0 1 1 2-2 2 2 0 0 1-1.95 2.05Zm11.7-1.8a1.95 1.95 0 1 1 2-1.85 2 2 0 0 1-1.95 1.9Z" 245 + fill="#b1b2b5" 246 + /> 247 + </svg> 248 + </div> 242 249 <div 243 250 class="css-rbpzuh" 244 251 > 245 252 <div 246 - class="css-1cojt9j" 253 + style="color: rgb(177, 178, 181); text-align: center;" 247 254 > 248 - Drankin N Smokin 249 - </div> 250 - <div 251 - style="display: flex; flex-direction: row; align-items: center; justify-content: space-between;" 252 - > 253 - <div 254 - class="css-1jnbdma" 255 - > 256 - 02:03 257 - </div> 258 - <div 259 - class="css-bdl275" 260 - > 261 - Future, Lil Uzi Vert 262 - <span 263 - class="css-c9cgcr" 264 - > 265 - - 266 - </span> 267 - Pluto x Baby Pluto (Deluxe) 268 - </div> 269 - <div 270 - class="css-1jnbdma" 271 - > 272 - 04:15 273 - </div> 274 - </div> 275 - <div 276 - class="css-1n7uud4" 277 - > 278 - <div 279 - aria-label="48% Loaded" 280 - aria-valuemax="100" 281 - aria-valuemin="0" 282 - aria-valuenow="48.3219508605943" 283 - class="ae" 284 - data-baseweb="progress-bar" 285 - role="progressbar" 286 - > 287 - <div 288 - class="af ag ah ai aj" 289 - > 290 - <div 291 - class="ak al am an ao ap aq ar" 292 - > 293 - <div 294 - class="ak al am an as at ae au av" 295 - /> 296 - </div> 297 - </div> 298 - </div> 255 + No track playing 299 256 </div> 300 257 </div> 301 258 </div> ··· 362 319 class="css-14zx4vh" 363 320 > 364 321 <button 365 - class="aw ax ay az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bh bi ag bj ah bk bl bm bn bo bp bq br bs bt bu bv bw bx as by bz c0 c1 c2" 322 + class="ae af ag ah ai aj ak al am an ao ap aq ar as at au av aw ax ay az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bh bi bj bk bl bm bn" 366 323 data-baseweb="button" 367 324 > 368 325 <div ··· 391 348 class="css-wa9s5g" 392 349 /> 393 350 <button 394 - class="aw ax ay az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bh bi ag bj ah bk bl bm bn bo bp bq br bs bt bu bv bw c3 c4 c5 c6 c0 c1 c2" 351 + class="ae af ag ah ai aj ak al am an ao ap aq ar as at au av aw ax ay az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bo bp bq br bl bm bn" 395 352 data-baseweb="button" 396 353 > 397 354 <div ··· 962 919 style="margin-bottom: 100px;" 963 920 > 964 921 <div 965 - class="c7 c8" 922 + class="bs bt" 966 923 > 967 924 <div 968 - class="c9 af ca c8 cb cc cd ce cf cg ch ci cj ck cl cm cn co cp cq" 925 + class="bu bv bw bt bx by bz c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc" 969 926 > 970 927 <div 971 - class="c9 ae cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc" 928 + class="bu cd ce cf cg ch ci cj ck cl cm cn co cp cq cr cs ct cu cv cw cx cy cz" 972 929 > 973 930 <div 974 931 style="position: relative; width: 100%;" ··· 1093 1050 </div> 1094 1051 </div> 1095 1052 <div 1096 - class="c9 ae cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc" 1053 + class="bu cd ce cf cg ch ci cj ck cl cm cn co cp cq cr cs ct cu cv cw cx cy cz" 1097 1054 > 1098 1055 <div 1099 1056 style="position: relative; width: 100%;" ··· 1218 1175 </div> 1219 1176 </div> 1220 1177 <div 1221 - class="c9 ae cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc" 1178 + class="bu cd ce cf cg ch ci cj ck cl cm cn co cp cq cr cs ct cu cv cw cx cy cz" 1222 1179 > 1223 1180 <div 1224 1181 style="position: relative; width: 100%;"
+11 -7
webui/rockbox/src/Components/Artists/Artists.test.tsx
··· 4 4 import { artists } from "./mocks"; 5 5 import Providers from "../../Providers"; 6 6 import { MemoryRouter } from "react-router-dom"; 7 + import { MockedProvider } from "@apollo/client/testing"; 8 + import { mocks } from "../../mocks"; 7 9 8 10 describe("Artists", () => { 9 11 it("should render", () => { 10 12 const { container } = render( 11 13 <MemoryRouter initialEntries={["/"]}> 12 - <Providers> 13 - <Artists 14 - artists={artists} 15 - onClickArtist={vi.fn()} 16 - onFilter={vi.fn()} 17 - /> 18 - </Providers> 14 + <MockedProvider mocks={mocks}> 15 + <Providers> 16 + <Artists 17 + artists={artists} 18 + onClickArtist={vi.fn()} 19 + onFilter={vi.fn()} 20 + /> 21 + </Providers> 22 + </MockedProvider> 19 23 </MemoryRouter> 20 24 ); 21 25 expect(container).toMatchSnapshot();
+26 -69
webui/rockbox/src/Components/Artists/__snapshots__/Artists.test.tsx.snap
··· 180 180 class="css-10asy2d" 181 181 > 182 182 <svg 183 + fill="none" 183 184 height="32" 184 - viewBox="0 0 512 512" 185 185 width="32" 186 186 xmlns="http://www.w3.org/2000/svg" 187 187 > 188 - <title> 189 - Pause 190 - </title> 191 188 <path 192 - d="M208 432h-48a16 16 0 0 1-16-16V96a16 16 0 0 1 16-16h48a16 16 0 0 1 16 16v320a16 16 0 0 1-16 16zm144 0h-48a16 16 0 0 1-16-16V96a16 16 0 0 1 16-16h48a16 16 0 0 1 16 16v320a16 16 0 0 1-16 16z" 189 + d="M10.657 27a2.842 2.842 0 0 1-1.258-.292C8.536 26.283 8 25.458 8 24.563V6.438c0-.899.536-1.721 1.399-2.146a2.856 2.856 0 0 1 2.571.028l17.817 9.273c.755.411 1.213 1.131 1.213 1.906 0 .774-.458 1.495-1.213 1.906l-17.82 9.275a2.846 2.846 0 0 1-1.31.32Z" 193 190 style="fill: #000;" 194 191 /> 195 192 </svg> ··· 234 231 <div 235 232 class="css-frud38" 236 233 > 237 - <img 238 - alt="Album cover" 239 - class="css-31cka" 240 - src="https://resources.tidal.com/images/fe6787d5/4ba5/4d3e/8576/48943ee6a768/320x320.jpg" 241 - /> 234 + <div 235 + class="css-4ijxew" 236 + > 237 + <svg 238 + height="28" 239 + viewBox="0 0 24 24" 240 + width="28" 241 + xmlns="http://www.w3.org/2000/svg" 242 + > 243 + <path 244 + d="M8.1 4.65v11.26a3.45 3.45 0 1 0 1.5 2.84V5.85l10.2-2.36v10.62A3.45 3.45 0 1 0 21.3 17V1.61Zm-2 16a2 2 0 1 1 2-2 2 2 0 0 1-1.95 2.05Zm11.7-1.8a1.95 1.95 0 1 1 2-1.85 2 2 0 0 1-1.95 1.9Z" 245 + fill="#b1b2b5" 246 + /> 247 + </svg> 248 + </div> 242 249 <div 243 250 class="css-rbpzuh" 244 251 > 245 252 <div 246 - class="css-1cojt9j" 253 + style="color: rgb(177, 178, 181); text-align: center;" 247 254 > 248 - Drankin N Smokin 249 - </div> 250 - <div 251 - style="display: flex; flex-direction: row; align-items: center; justify-content: space-between;" 252 - > 253 - <div 254 - class="css-1jnbdma" 255 - > 256 - 02:03 257 - </div> 258 - <div 259 - class="css-bdl275" 260 - > 261 - Future, Lil Uzi Vert 262 - <span 263 - class="css-c9cgcr" 264 - > 265 - - 266 - </span> 267 - Pluto x Baby Pluto (Deluxe) 268 - </div> 269 - <div 270 - class="css-1jnbdma" 271 - > 272 - 04:15 273 - </div> 274 - </div> 275 - <div 276 - class="css-1n7uud4" 277 - > 278 - <div 279 - aria-label="48% Loaded" 280 - aria-valuemax="100" 281 - aria-valuemin="0" 282 - aria-valuenow="48.3219508605943" 283 - class="ae" 284 - data-baseweb="progress-bar" 285 - role="progressbar" 286 - > 287 - <div 288 - class="af ag ah ai aj" 289 - > 290 - <div 291 - class="ak al am an ao ap aq ar" 292 - > 293 - <div 294 - class="ak al am an as at ae au av" 295 - /> 296 - </div> 297 - </div> 298 - </div> 255 + No track playing 299 256 </div> 300 257 </div> 301 258 </div> ··· 334 291 style="margin-bottom: 100px;" 335 292 > 336 293 <div 337 - class="aw ax" 294 + class="ae af" 338 295 > 339 296 <div 340 - class="ay af az ax b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf" 297 + class="ag ah ai af aj ak al am an ao ap aq ar as at au av aw ax ay" 341 298 > 342 299 <div 343 - class="ay ae bg bh bi bj bk bl bm bn bo bp bq br bs bt bu bv bw bx by bz c0 c1 c2 c3 c4 c5" 300 + class="ag az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bh bi bj bk bl bm bn bo bp" 344 301 > 345 302 <a 346 303 href="/artists/1" ··· 358 315 </a> 359 316 </div> 360 317 <div 361 - class="ay ae bg bh bi bj bk bl bm bn bo bp bq br bs bt bu bv bw bx by bz c0 c1 c2 c3 c4 c5" 318 + class="ag az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bh bi bj bk bl bm bn bo bp" 362 319 > 363 320 <a 364 321 href="/artists/2" ··· 376 333 </a> 377 334 </div> 378 335 <div 379 - class="ay ae bg bh bi bj bk bl bm bn bo bp bq br bs bt bu bv bw bx by bz c0 c1 c2 c3 c4 c5" 336 + class="ag az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bh bi bj bk bl bm bn bo bp" 380 337 > 381 338 <a 382 339 href="/artists/3" ··· 394 351 </a> 395 352 </div> 396 353 <div 397 - class="ay ae bg bh bi bj bk bl bm bn bo bp bq br bs bt bu bv bw bx by bz c0 c1 c2 c3 c4 c5" 354 + class="ag az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bh bi bj bk bl bm bn bo bp" 398 355 > 399 356 <a 400 357 href="/artists/4" ··· 412 369 </a> 413 370 </div> 414 371 <div 415 - class="ay ae bg bh bi bj bk bl bm bn bo bp bq br bs bt bu bv bw bx by bz c0 c1 c2 c3 c4 c5" 372 + class="ag az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bh bi bj bk bl bm bn bo bp" 416 373 > 417 374 <a 418 375 href="/artists/4"
+26 -19
webui/rockbox/src/Components/ControlBar/ControlBar.test.tsx
··· 2 2 import { vi } from "vitest"; 3 3 import ControlBar from "./ControlBar"; 4 4 import Providers from "../../Providers"; 5 + import { MockedProvider } from "@apollo/client/testing"; 6 + import { mocks } from "../../mocks"; 7 + import { MemoryRouter } from "react-router-dom"; 5 8 6 9 describe("ControlBar", () => { 7 10 it("should render", () => { 8 11 const { container } = render( 9 12 <Providers> 10 - <ControlBar 11 - nowPlaying={{ 12 - album: "Forgotten Shapes", 13 - artist: "Waveshaper", 14 - title: "Disco on the Baltic Sea", 15 - cover: 16 - "https://resources.tidal.com/images/5b33d07f/7d28/417d/8f8f/86291d0b6b34/320x320.jpg", 17 - duration: 255488.00659179688, 18 - progress: 123456.789, 19 - isPlaying: true, 20 - albumId: "229251493", 21 - }} 22 - onPlay={vi.fn()} 23 - onPause={vi.fn()} 24 - onNext={vi.fn()} 25 - onPrevious={vi.fn()} 26 - onShuffle={vi.fn()} 27 - onRepeat={vi.fn()} 28 - /> 13 + <MemoryRouter initialEntries={["/"]}> 14 + <MockedProvider mocks={mocks}> 15 + <ControlBar 16 + nowPlaying={{ 17 + album: "Forgotten Shapes", 18 + artist: "Waveshaper", 19 + title: "Disco on the Baltic Sea", 20 + cover: 21 + "https://resources.tidal.com/images/5b33d07f/7d28/417d/8f8f/86291d0b6b34/320x320.jpg", 22 + duration: 255488.00659179688, 23 + progress: 123456.789, 24 + isPlaying: true, 25 + albumId: "229251493", 26 + }} 27 + onPlay={vi.fn()} 28 + onPause={vi.fn()} 29 + onNext={vi.fn()} 30 + onPrevious={vi.fn()} 31 + onShuffle={vi.fn()} 32 + onRepeat={vi.fn()} 33 + /> 34 + </MockedProvider> 35 + </MemoryRouter> 29 36 </Providers> 30 37 ); 31 38 expect(container).toMatchSnapshot();
+2 -10
webui/rockbox/src/Components/ControlBar/ControlBar.tsx
··· 8 8 import CurrentTrack from "./CurrentTrack"; 9 9 import RightMenu from "./RightMenu"; 10 10 import Pause from "../Icons/Pause"; 11 + import { CurrentTrack as NowPlaying } from "../../Types/track"; 11 12 12 13 export type ControlBarProps = { 13 - nowPlaying?: { 14 - album?: string; 15 - artist?: string; 16 - title?: string; 17 - cover?: string; 18 - duration: number; 19 - progress: number; 20 - isPlaying?: boolean; 21 - albumId?: string; 22 - }; 14 + nowPlaying?: NowPlaying; 23 15 onPlay: () => void; 24 16 onPause: () => void; 25 17 onNext: () => void;
+13
webui/rockbox/src/Components/ControlBar/ControlBarState.tsx
··· 1 + import { atom } from "recoil"; 2 + import { CurrentTrack } from "../../Types/track"; 3 + 4 + export const controlBarState = atom<{ 5 + nowPlaying?: CurrentTrack; 6 + locked?: boolean; 7 + }>({ 8 + key: "controlBarState", 9 + default: { 10 + nowPlaying: undefined, 11 + locked: false, 12 + }, 13 + });
+115 -16
webui/rockbox/src/Components/ControlBar/ControlBarWithData.tsx
··· 1 - import { FC } from "react"; 1 + import { FC, useEffect } from "react"; 2 2 import ControlBar from "./ControlBar"; 3 + import { 4 + useCurrentlyPlayingSongSubscription, 5 + useGetCurrentTrackQuery, 6 + useGetPlaybackStatusQuery, 7 + useNextMutation, 8 + usePauseMutation, 9 + usePlaybackStatusSubscription, 10 + usePreviousMutation, 11 + useResumeMutation, 12 + } from "../../Hooks/GraphQL"; 13 + import { CurrentTrack } from "../../Types/track"; 14 + import _ from "lodash"; 15 + import { useRecoilState } from "recoil"; 16 + import { controlBarState } from "./ControlBarState"; 3 17 4 18 const ControlBarWithData: FC = () => { 19 + const [{ nowPlaying, locked }, setControlBarState] = 20 + useRecoilState(controlBarState); 21 + const { data, loading } = useGetCurrentTrackQuery(); 22 + const { data: playback } = useGetPlaybackStatusQuery({ 23 + fetchPolicy: "network-only", 24 + }); 25 + const [pause] = usePauseMutation(); 26 + const [resume] = useResumeMutation(); 27 + const [previous] = usePreviousMutation(); 28 + const [next] = useNextMutation(); 29 + const { data: playbackSubscription } = useCurrentlyPlayingSongSubscription(); 30 + const { data: playbackStatus } = usePlaybackStatusSubscription(); 31 + 32 + const setNowPlaying = (nowPlaying: CurrentTrack) => { 33 + setControlBarState((state) => ({ 34 + ...state, 35 + nowPlaying, 36 + })); 37 + }; 38 + 39 + useEffect(() => { 40 + if (_.get(playbackSubscription, "currentlyPlayingSong.length", 0) > 0) { 41 + const currentSong = playbackSubscription?.currentlyPlayingSong; 42 + setNowPlaying({ 43 + album: currentSong?.album, 44 + artist: currentSong?.artist, 45 + title: currentSong?.title, 46 + cover: currentSong?.albumArt 47 + ? currentSong?.albumArt.startsWith("http") 48 + ? currentSong.albumArt 49 + : `http://localhost:6062/covers/${currentSong?.albumArt}` 50 + : "", 51 + duration: currentSong?.length || 0, 52 + progress: currentSong?.elapsed || 0, 53 + isPlaying: playbackStatus?.playbackStatus.status === 1 && !locked, 54 + albumId: currentSong?.albumId, 55 + }); 56 + } 57 + // eslint-disable-next-line react-hooks/exhaustive-deps 58 + }, [playbackSubscription, playbackStatus]); 59 + 60 + useEffect(() => { 61 + if (loading || !data) { 62 + return; 63 + } 64 + 65 + if (nowPlaying) { 66 + return; 67 + } 68 + 69 + setNowPlaying({ 70 + album: data.currentTrack?.album, 71 + artist: data.currentTrack?.artist, 72 + title: data.currentTrack?.title, 73 + cover: data.currentTrack?.albumArt 74 + ? data.currentTrack?.albumArt.startsWith("http") 75 + ? data.currentTrack?.albumArt 76 + : `http://localhost:6062/covers/${data.currentTrack?.albumArt}` 77 + : "", 78 + duration: data.currentTrack?.length || 0, 79 + progress: data.currentTrack?.elapsed || 0, 80 + isPlaying: playback?.status === 1, 81 + albumId: data.currentTrack?.albumId, 82 + }); 83 + // eslint-disable-next-line react-hooks/exhaustive-deps 84 + }, [data, loading, playback]); 85 + 86 + const onPlay = () => { 87 + setControlBarState((state) => ({ 88 + ...state, 89 + locked: true, 90 + })); 91 + resume(); 92 + setTimeout(() => { 93 + setControlBarState((state) => ({ 94 + ...state, 95 + locked: false, 96 + })); 97 + }, 2000); 98 + }; 99 + 100 + const onPause = () => { 101 + setControlBarState((state) => ({ 102 + ...state, 103 + locked: true, 104 + })); 105 + pause(); 106 + setTimeout(() => { 107 + setControlBarState((state) => ({ 108 + ...state, 109 + locked: false, 110 + })); 111 + }, 2000); 112 + }; 113 + 5 114 return ( 6 115 <ControlBar 7 - nowPlaying={{ 8 - album: "Pluto x Baby Pluto (Deluxe)", 9 - artist: "Future, Lil Uzi Vert", 10 - title: "Drankin N Smokin", 11 - cover: 12 - "https://resources.tidal.com/images/fe6787d5/4ba5/4d3e/8576/48943ee6a768/320x320.jpg", 13 - duration: 255488.00659179688, 14 - progress: 123456.789, 15 - isPlaying: true, 16 - albumId: "229251493", 17 - }} 18 - onPlay={() => {}} 19 - onPause={() => {}} 20 - onNext={() => {}} 21 - onPrevious={() => {}} 116 + nowPlaying={nowPlaying} 117 + onPlay={onPlay} 118 + onPause={onPause} 119 + onNext={() => next()} 120 + onPrevious={() => previous()} 22 121 onShuffle={() => {}} 23 122 onRepeat={() => {}} 24 123 />
+11 -13
webui/rockbox/src/Components/ControlBar/CurrentTrack/CurrentTrack.tsx
··· 1 1 import { FC } from "react"; 2 2 import { ProgressBar } from "baseui/progress-bar"; 3 3 import { 4 + Album, 4 5 AlbumCover, 5 6 ArtistAlbum, 6 7 Container, ··· 14 15 } from "./styles"; 15 16 import Track from "../../Icons/Track"; 16 17 import { useTimeFormat } from "../../../Hooks/useFormat"; 18 + import { CurrentTrack as NowPlaying } from "../../../Types/track"; 17 19 18 20 export type CurrentTrackProps = { 19 - nowPlaying?: { 20 - album?: string; 21 - artist?: string; 22 - title?: string; 23 - cover?: string; 24 - duration: number; 25 - progress: number; 26 - isPlaying?: boolean; 27 - albumId?: string; 28 - }; 21 + nowPlaying?: NowPlaying; 29 22 }; 30 23 31 24 const CurrentTrack: FC<CurrentTrackProps> = ({ nowPlaying }) => { 32 25 const { formatTime } = useTimeFormat(); 26 + const album = `${nowPlaying?.artist} - ${nowPlaying?.album}`; 33 27 return ( 34 28 <Container> 35 29 {!nowPlaying?.cover && ( ··· 41 35 <AlbumCover src={nowPlaying.cover} alt="Album cover" /> 42 36 )} 43 37 <TrackInfo> 44 - {!nowPlaying && ( 38 + {(!nowPlaying || nowPlaying?.duration === 0) && ( 45 39 <div style={{ color: "#b1b2b5", textAlign: "center" }}> 46 40 No track playing 47 41 </div> 48 42 )} 49 - {nowPlaying && ( 43 + {nowPlaying && nowPlaying?.duration > 0 && ( 50 44 <> 51 45 <Title>{nowPlaying.title}</Title> 52 46 <div ··· 61 55 <ArtistAlbum> 62 56 {nowPlaying.artist} 63 57 <Separator>-</Separator> 64 - {nowPlaying.album} 58 + <Album to={`/albums/${nowPlaying.albumId}`}> 59 + {album.length > 75 60 + ? `${nowPlaying.album?.substring(0, 30)}...` 61 + : nowPlaying.album} 62 + </Album> 65 63 </ArtistAlbum> 66 64 <Time>{formatTime(nowPlaying.duration)}</Time> 67 65 </div>
+9
webui/rockbox/src/Components/ControlBar/CurrentTrack/styles.tsx
··· 1 1 import styled from "@emotion/styled"; 2 + import { Link } from "react-router-dom"; 2 3 3 4 export const Container = styled.div` 4 5 display: flex; ··· 75 76 width: calc(100% - 20px); 76 77 margin-left: 10px; 77 78 margin-right: 10px; 79 + `; 80 + 81 + export const Album = styled(Link)` 82 + text-decoration: none; 83 + color: inherit; 84 + &:hover { 85 + text-decoration: underline; 86 + } 78 87 `; 79 88 80 89 export const styles = {
+6 -1
webui/rockbox/src/Components/ControlBar/__snapshots__/ControlBar.test.tsx.snap
··· 137 137 > 138 138 - 139 139 </span> 140 - Forgotten Shapes 140 + <a 141 + class="css-1759b6u" 142 + href="/albums/229251493" 143 + > 144 + Forgotten Shapes 145 + </a> 141 146 </div> 142 147 <div 143 148 class="css-1jnbdma"
+7 -3
webui/rockbox/src/Components/Files/Files.test.tsx
··· 4 4 import { files } from "./mocks"; 5 5 import { MemoryRouter } from "react-router-dom"; 6 6 import { vi } from "vitest"; 7 + import { MockedProvider } from "@apollo/client/testing"; 8 + import { mocks } from "../../mocks"; 7 9 8 10 describe("Files", () => { 9 11 it("should render", () => { 10 12 const { container } = render( 11 13 <MemoryRouter initialEntries={["/"]}> 12 - <Providers> 13 - <Files files={files} canGoBack={true} onGoBack={vi.fn()} /> 14 - </Providers> 14 + <MockedProvider mocks={mocks}> 15 + <Providers> 16 + <Files files={files} canGoBack={true} onGoBack={vi.fn()} /> 17 + </Providers> 18 + </MockedProvider> 15 19 </MemoryRouter> 16 20 ); 17 21 expect(container).toMatchSnapshot();
+1 -1
webui/rockbox/src/Components/Files/FilesWithData.tsx
··· 6 6 const FilesWithData: FC = () => { 7 7 const navigate = useNavigate(); 8 8 const [refetching, setRefetching] = useState(false); 9 - const { data, refetch, loading } = useGetEntriesQuery(); 9 + const { data, refetch } = useGetEntriesQuery(); 10 10 const [params] = useSearchParams(); 11 11 const path = params.get("q"); 12 12 const canGoBack = !!path;
+19 -62
webui/rockbox/src/Components/Files/__snapshots__/Files.test.tsx.snap
··· 180 180 class="css-10asy2d" 181 181 > 182 182 <svg 183 + fill="none" 183 184 height="32" 184 - viewBox="0 0 512 512" 185 185 width="32" 186 186 xmlns="http://www.w3.org/2000/svg" 187 187 > 188 - <title> 189 - Pause 190 - </title> 191 188 <path 192 - d="M208 432h-48a16 16 0 0 1-16-16V96a16 16 0 0 1 16-16h48a16 16 0 0 1 16 16v320a16 16 0 0 1-16 16zm144 0h-48a16 16 0 0 1-16-16V96a16 16 0 0 1 16-16h48a16 16 0 0 1 16 16v320a16 16 0 0 1-16 16z" 189 + d="M10.657 27a2.842 2.842 0 0 1-1.258-.292C8.536 26.283 8 25.458 8 24.563V6.438c0-.899.536-1.721 1.399-2.146a2.856 2.856 0 0 1 2.571.028l17.817 9.273c.755.411 1.213 1.131 1.213 1.906 0 .774-.458 1.495-1.213 1.906l-17.82 9.275a2.846 2.846 0 0 1-1.31.32Z" 193 190 style="fill: #000;" 194 191 /> 195 192 </svg> ··· 234 231 <div 235 232 class="css-frud38" 236 233 > 237 - <img 238 - alt="Album cover" 239 - class="css-31cka" 240 - src="https://resources.tidal.com/images/fe6787d5/4ba5/4d3e/8576/48943ee6a768/320x320.jpg" 241 - /> 242 234 <div 243 - class="css-rbpzuh" 235 + class="css-4ijxew" 244 236 > 245 - <div 246 - class="css-1cojt9j" 237 + <svg 238 + height="28" 239 + viewBox="0 0 24 24" 240 + width="28" 241 + xmlns="http://www.w3.org/2000/svg" 247 242 > 248 - Drankin N Smokin 249 - </div> 243 + <path 244 + d="M8.1 4.65v11.26a3.45 3.45 0 1 0 1.5 2.84V5.85l10.2-2.36v10.62A3.45 3.45 0 1 0 21.3 17V1.61Zm-2 16a2 2 0 1 1 2-2 2 2 0 0 1-1.95 2.05Zm11.7-1.8a1.95 1.95 0 1 1 2-1.85 2 2 0 0 1-1.95 1.9Z" 245 + fill="#b1b2b5" 246 + /> 247 + </svg> 248 + </div> 249 + <div 250 + class="css-rbpzuh" 251 + > 250 252 <div 251 - style="display: flex; flex-direction: row; align-items: center; justify-content: space-between;" 253 + style="color: rgb(177, 178, 181); text-align: center;" 252 254 > 253 - <div 254 - class="css-1jnbdma" 255 - > 256 - 02:03 257 - </div> 258 - <div 259 - class="css-bdl275" 260 - > 261 - Future, Lil Uzi Vert 262 - <span 263 - class="css-c9cgcr" 264 - > 265 - - 266 - </span> 267 - Pluto x Baby Pluto (Deluxe) 268 - </div> 269 - <div 270 - class="css-1jnbdma" 271 - > 272 - 04:15 273 - </div> 274 - </div> 275 - <div 276 - class="css-1n7uud4" 277 - > 278 - <div 279 - aria-label="48% Loaded" 280 - aria-valuemax="100" 281 - aria-valuemin="0" 282 - aria-valuenow="48.3219508605943" 283 - class="ae" 284 - data-baseweb="progress-bar" 285 - role="progressbar" 286 - > 287 - <div 288 - class="af ag ah ai aj" 289 - > 290 - <div 291 - class="ak al am an ao ap aq ar" 292 - > 293 - <div 294 - class="ak al am an as at ae au av" 295 - /> 296 - </div> 297 - </div> 298 - </div> 255 + No track playing 299 256 </div> 300 257 </div> 301 258 </div>
+7 -3
webui/rockbox/src/Components/Tracks/Tracks.test.tsx
··· 3 3 import Providers from "../../Providers"; 4 4 import { tracks } from "./mocks"; 5 5 import { MemoryRouter } from "react-router-dom"; 6 + import { MockedProvider } from "@apollo/client/testing"; 7 + import { mocks } from "../../mocks"; 6 8 7 9 describe("Tracks", () => { 8 10 it("should render", () => { 9 11 const { container } = render( 10 12 <MemoryRouter initialEntries={["/"]}> 11 - <Providers> 12 - <Tracks tracks={tracks} /> 13 - </Providers> 13 + <MockedProvider mocks={mocks} addTypename={false}> 14 + <Providers> 15 + <Tracks tracks={tracks} /> 16 + </Providers> 17 + </MockedProvider> 14 18 </MemoryRouter> 15 19 ); 16 20 expect(container).toMatchSnapshot();
+23 -66
webui/rockbox/src/Components/Tracks/__snapshots__/Tracks.test.tsx.snap
··· 180 180 class="css-10asy2d" 181 181 > 182 182 <svg 183 + fill="none" 183 184 height="32" 184 - viewBox="0 0 512 512" 185 185 width="32" 186 186 xmlns="http://www.w3.org/2000/svg" 187 187 > 188 - <title> 189 - Pause 190 - </title> 191 188 <path 192 - d="M208 432h-48a16 16 0 0 1-16-16V96a16 16 0 0 1 16-16h48a16 16 0 0 1 16 16v320a16 16 0 0 1-16 16zm144 0h-48a16 16 0 0 1-16-16V96a16 16 0 0 1 16-16h48a16 16 0 0 1 16 16v320a16 16 0 0 1-16 16z" 189 + d="M10.657 27a2.842 2.842 0 0 1-1.258-.292C8.536 26.283 8 25.458 8 24.563V6.438c0-.899.536-1.721 1.399-2.146a2.856 2.856 0 0 1 2.571.028l17.817 9.273c.755.411 1.213 1.131 1.213 1.906 0 .774-.458 1.495-1.213 1.906l-17.82 9.275a2.846 2.846 0 0 1-1.31.32Z" 193 190 style="fill: #000;" 194 191 /> 195 192 </svg> ··· 234 231 <div 235 232 class="css-frud38" 236 233 > 237 - <img 238 - alt="Album cover" 239 - class="css-31cka" 240 - src="https://resources.tidal.com/images/fe6787d5/4ba5/4d3e/8576/48943ee6a768/320x320.jpg" 241 - /> 234 + <div 235 + class="css-4ijxew" 236 + > 237 + <svg 238 + height="28" 239 + viewBox="0 0 24 24" 240 + width="28" 241 + xmlns="http://www.w3.org/2000/svg" 242 + > 243 + <path 244 + d="M8.1 4.65v11.26a3.45 3.45 0 1 0 1.5 2.84V5.85l10.2-2.36v10.62A3.45 3.45 0 1 0 21.3 17V1.61Zm-2 16a2 2 0 1 1 2-2 2 2 0 0 1-1.95 2.05Zm11.7-1.8a1.95 1.95 0 1 1 2-1.85 2 2 0 0 1-1.95 1.9Z" 245 + fill="#b1b2b5" 246 + /> 247 + </svg> 248 + </div> 242 249 <div 243 250 class="css-rbpzuh" 244 251 > 245 252 <div 246 - class="css-1cojt9j" 253 + style="color: rgb(177, 178, 181); text-align: center;" 247 254 > 248 - Drankin N Smokin 249 - </div> 250 - <div 251 - style="display: flex; flex-direction: row; align-items: center; justify-content: space-between;" 252 - > 253 - <div 254 - class="css-1jnbdma" 255 - > 256 - 02:03 257 - </div> 258 - <div 259 - class="css-bdl275" 260 - > 261 - Future, Lil Uzi Vert 262 - <span 263 - class="css-c9cgcr" 264 - > 265 - - 266 - </span> 267 - Pluto x Baby Pluto (Deluxe) 268 - </div> 269 - <div 270 - class="css-1jnbdma" 271 - > 272 - 04:15 273 - </div> 274 - </div> 275 - <div 276 - class="css-1n7uud4" 277 - > 278 - <div 279 - aria-label="48% Loaded" 280 - aria-valuemax="100" 281 - aria-valuemin="0" 282 - aria-valuenow="48.3219508605943" 283 - class="ae" 284 - data-baseweb="progress-bar" 285 - role="progressbar" 286 - > 287 - <div 288 - class="af ag ah ai aj" 289 - > 290 - <div 291 - class="ak al am an ao ap aq ar" 292 - > 293 - <div 294 - class="ak al am an as at ae au av" 295 - /> 296 - </div> 297 - </div> 298 - </div> 255 + No track playing 299 256 </div> 300 257 </div> 301 258 </div> ··· 334 291 class="css-9wcbnz" 335 292 > 336 293 <div 337 - class="aw af ar ax ay az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bh bi bj bk bl bm bn bo" 294 + class="ae af ag ah ai aj ak al am an ao ap aq ar as at au av aw ax ay az b0 b1 b2 b3 b4 b5 b6 b7 b8" 338 295 data-baseweb="input" 339 296 > 340 297 <div 341 - class="af bp bq br b7 b8 bd be bf bg bn bs bt bl bu bv" 298 + class="af b9 ba bb ar as ax ay az b0 b7 bc bd b5 be bf" 342 299 > 343 300 <svg 344 301 data-test="icon-ActionActiveSearch" ··· 356 313 </svg> 357 314 </div> 358 315 <div 359 - class="af ae bw b7 b8 bd be bf bg bt bl" 316 + class="af bg bh ar as ax ay az b0 bd b5" 360 317 data-baseweb="base-input" 361 318 > 362 319 <input 363 320 aria-invalid="false" 364 321 aria-required="false" 365 322 autocomplete="on" 366 - class="aw bl bx by bz c0 c1 c2 c3 c4 c5 ae c6 c7 c8 c9 ca cb bm cc bd cd bf bg ce cf cg" 323 + class="ae b5 bi bj bk bl bm bn bo bp bq bg br bs bt bu bv bw b6 bx ax by az b0 bz c0 c1" 367 324 inputmode="text" 368 325 name="filter" 369 326 placeholder="Search song"
webui/rockbox/src/GraphQL/Browse/Query.tsx webui/rockbox/src/GraphQL/Browse/Query.ts
webui/rockbox/src/GraphQL/Library/Query.tsx webui/rockbox/src/GraphQL/Library/Query.ts
webui/rockbox/src/GraphQL/Playback/.gitkeep

This is a binary file and will not be displayed.

+31
webui/rockbox/src/GraphQL/Playback/Mutation.ts
··· 1 + import { gql } from "@apollo/client"; 2 + 3 + export const PLAY = gql` 4 + mutation Play($elapsed: Int!, $offset: Int!) { 5 + play(elapsed: $elapsed, offset: $offset) 6 + } 7 + `; 8 + 9 + export const PAUSE = gql` 10 + mutation Pause { 11 + pause 12 + } 13 + `; 14 + 15 + export const RESUME = gql` 16 + mutation Resume { 17 + resume 18 + } 19 + `; 20 + 21 + export const PREVIOUS = gql` 22 + mutation Previous { 23 + previous 24 + } 25 + `; 26 + 27 + export const NEXT = gql` 28 + mutation Next { 29 + next 30 + } 31 + `;
+42
webui/rockbox/src/GraphQL/Playback/Query.ts
··· 1 + import { gql } from "@apollo/client"; 2 + 3 + export const GET_CURRENT_TRACK = gql` 4 + query GetCurrentTrack { 5 + currentTrack { 6 + id 7 + title 8 + artist 9 + album 10 + albumArt 11 + artistId 12 + albumId 13 + elapsed 14 + length 15 + year 16 + yearString 17 + } 18 + } 19 + `; 20 + 21 + export const GET_NEXT_TRACK = gql` 22 + query GetNextTrack { 23 + nextTrack { 24 + id 25 + title 26 + artist 27 + album 28 + albumArt 29 + artistId 30 + albumId 31 + length 32 + year 33 + yearString 34 + } 35 + } 36 + `; 37 + 38 + export const GET_PLAYBACK_STATUS = gql` 39 + query GetPlaybackStatus { 40 + status 41 + } 42 + `;
+27
webui/rockbox/src/GraphQL/Playback/Subscription.ts
··· 1 + import { gql } from "@apollo/client"; 2 + 3 + export const CURRENTLY_PLAYING_SONG = gql` 4 + subscription CurrentlyPlayingSong { 5 + currentlyPlayingSong { 6 + id 7 + title 8 + artist 9 + album 10 + albumArt 11 + artistId 12 + albumId 13 + elapsed 14 + length 15 + year 16 + yearString 17 + } 18 + } 19 + `; 20 + 21 + export const PLAYBACK_STATUS = gql` 22 + subscription PlaybackStatus { 23 + playbackStatus { 24 + status 25 + } 26 + } 27 + `;
webui/rockbox/src/GraphQL/System/Query.tsx webui/rockbox/src/GraphQL/System/Query.ts
+418
webui/rockbox/src/Hooks/GraphQL.tsx
··· 40 40 tracks: Array<Track>; 41 41 }; 42 42 43 + export type AudioStatus = { 44 + __typename?: 'AudioStatus'; 45 + status: Scalars['Int']['output']; 46 + }; 47 + 43 48 export type CompressorSettings = { 44 49 __typename?: 'CompressorSettings'; 45 50 attackTime: Scalars['Int']['output']; ··· 193 198 noclip: Scalars['Boolean']['output']; 194 199 preamp: Scalars['Int']['output']; 195 200 type: Scalars['Int']['output']; 201 + }; 202 + 203 + export type Subscription = { 204 + __typename?: 'Subscription'; 205 + currentlyPlayingSong: Track; 206 + playbackStatus: AudioStatus; 196 207 }; 197 208 198 209 export type SystemStatus = { ··· 476 487 477 488 export type GetAlbumQuery = { __typename?: 'Query', album?: { __typename?: 'Album', id: string, title: string, artist: string, albumArt?: string | null, year: number, yearString: string, artistId: string, md5: string, tracks: Array<{ __typename?: 'Track', id?: string | null, title: string, tracknum: number, artist: string, album: string, discnum: number, albumArtist: string, artistId?: string | null, albumId?: string | null, path: string, length: number }> } | null }; 478 489 490 + export type PlayMutationVariables = Exact<{ 491 + elapsed: Scalars['Int']['input']; 492 + offset: Scalars['Int']['input']; 493 + }>; 494 + 495 + 496 + export type PlayMutation = { __typename?: 'Mutation', play: string }; 497 + 498 + export type PauseMutationVariables = Exact<{ [key: string]: never; }>; 499 + 500 + 501 + export type PauseMutation = { __typename?: 'Mutation', pause: string }; 502 + 503 + export type ResumeMutationVariables = Exact<{ [key: string]: never; }>; 504 + 505 + 506 + export type ResumeMutation = { __typename?: 'Mutation', resume: string }; 507 + 508 + export type PreviousMutationVariables = Exact<{ [key: string]: never; }>; 509 + 510 + 511 + export type PreviousMutation = { __typename?: 'Mutation', previous: string }; 512 + 513 + export type NextMutationVariables = Exact<{ [key: string]: never; }>; 514 + 515 + 516 + export type NextMutation = { __typename?: 'Mutation', next: string }; 517 + 518 + export type GetCurrentTrackQueryVariables = Exact<{ [key: string]: never; }>; 519 + 520 + 521 + export type GetCurrentTrackQuery = { __typename?: 'Query', currentTrack?: { __typename?: 'Track', id?: string | null, title: string, artist: string, album: string, albumArt?: string | null, artistId?: string | null, albumId?: string | null, elapsed: number, length: number, year: number, yearString: string } | null }; 522 + 523 + export type GetNextTrackQueryVariables = Exact<{ [key: string]: never; }>; 524 + 525 + 526 + export type GetNextTrackQuery = { __typename?: 'Query', nextTrack?: { __typename?: 'Track', id?: string | null, title: string, artist: string, album: string, albumArt?: string | null, artistId?: string | null, albumId?: string | null, length: number, year: number, yearString: string } | null }; 527 + 528 + export type GetPlaybackStatusQueryVariables = Exact<{ [key: string]: never; }>; 529 + 530 + 531 + export type GetPlaybackStatusQuery = { __typename?: 'Query', status: number }; 532 + 533 + export type CurrentlyPlayingSongSubscriptionVariables = Exact<{ [key: string]: never; }>; 534 + 535 + 536 + export type CurrentlyPlayingSongSubscription = { __typename?: 'Subscription', currentlyPlayingSong: { __typename?: 'Track', id?: string | null, title: string, artist: string, album: string, albumArt?: string | null, artistId?: string | null, albumId?: string | null, elapsed: number, length: number, year: number, yearString: string } }; 537 + 538 + export type PlaybackStatusSubscriptionVariables = Exact<{ [key: string]: never; }>; 539 + 540 + 541 + export type PlaybackStatusSubscription = { __typename?: 'Subscription', playbackStatus: { __typename?: 'AudioStatus', status: number } }; 542 + 479 543 export type GetRockboxVersionQueryVariables = Exact<{ [key: string]: never; }>; 480 544 481 545 ··· 794 858 export type GetAlbumLazyQueryHookResult = ReturnType<typeof useGetAlbumLazyQuery>; 795 859 export type GetAlbumSuspenseQueryHookResult = ReturnType<typeof useGetAlbumSuspenseQuery>; 796 860 export type GetAlbumQueryResult = Apollo.QueryResult<GetAlbumQuery, GetAlbumQueryVariables>; 861 + export const PlayDocument = gql` 862 + mutation Play($elapsed: Int!, $offset: Int!) { 863 + play(elapsed: $elapsed, offset: $offset) 864 + } 865 + `; 866 + export type PlayMutationFn = Apollo.MutationFunction<PlayMutation, PlayMutationVariables>; 867 + 868 + /** 869 + * __usePlayMutation__ 870 + * 871 + * To run a mutation, you first call `usePlayMutation` within a React component and pass it any options that fit your needs. 872 + * When your component renders, `usePlayMutation` returns a tuple that includes: 873 + * - A mutate function that you can call at any time to execute the mutation 874 + * - An object with fields that represent the current status of the mutation's execution 875 + * 876 + * @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; 877 + * 878 + * @example 879 + * const [playMutation, { data, loading, error }] = usePlayMutation({ 880 + * variables: { 881 + * elapsed: // value for 'elapsed' 882 + * offset: // value for 'offset' 883 + * }, 884 + * }); 885 + */ 886 + export function usePlayMutation(baseOptions?: Apollo.MutationHookOptions<PlayMutation, PlayMutationVariables>) { 887 + const options = {...defaultOptions, ...baseOptions} 888 + return Apollo.useMutation<PlayMutation, PlayMutationVariables>(PlayDocument, options); 889 + } 890 + export type PlayMutationHookResult = ReturnType<typeof usePlayMutation>; 891 + export type PlayMutationResult = Apollo.MutationResult<PlayMutation>; 892 + export type PlayMutationOptions = Apollo.BaseMutationOptions<PlayMutation, PlayMutationVariables>; 893 + export const PauseDocument = gql` 894 + mutation Pause { 895 + pause 896 + } 897 + `; 898 + export type PauseMutationFn = Apollo.MutationFunction<PauseMutation, PauseMutationVariables>; 899 + 900 + /** 901 + * __usePauseMutation__ 902 + * 903 + * To run a mutation, you first call `usePauseMutation` within a React component and pass it any options that fit your needs. 904 + * When your component renders, `usePauseMutation` returns a tuple that includes: 905 + * - A mutate function that you can call at any time to execute the mutation 906 + * - An object with fields that represent the current status of the mutation's execution 907 + * 908 + * @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; 909 + * 910 + * @example 911 + * const [pauseMutation, { data, loading, error }] = usePauseMutation({ 912 + * variables: { 913 + * }, 914 + * }); 915 + */ 916 + export function usePauseMutation(baseOptions?: Apollo.MutationHookOptions<PauseMutation, PauseMutationVariables>) { 917 + const options = {...defaultOptions, ...baseOptions} 918 + return Apollo.useMutation<PauseMutation, PauseMutationVariables>(PauseDocument, options); 919 + } 920 + export type PauseMutationHookResult = ReturnType<typeof usePauseMutation>; 921 + export type PauseMutationResult = Apollo.MutationResult<PauseMutation>; 922 + export type PauseMutationOptions = Apollo.BaseMutationOptions<PauseMutation, PauseMutationVariables>; 923 + export const ResumeDocument = gql` 924 + mutation Resume { 925 + resume 926 + } 927 + `; 928 + export type ResumeMutationFn = Apollo.MutationFunction<ResumeMutation, ResumeMutationVariables>; 929 + 930 + /** 931 + * __useResumeMutation__ 932 + * 933 + * To run a mutation, you first call `useResumeMutation` within a React component and pass it any options that fit your needs. 934 + * When your component renders, `useResumeMutation` returns a tuple that includes: 935 + * - A mutate function that you can call at any time to execute the mutation 936 + * - An object with fields that represent the current status of the mutation's execution 937 + * 938 + * @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; 939 + * 940 + * @example 941 + * const [resumeMutation, { data, loading, error }] = useResumeMutation({ 942 + * variables: { 943 + * }, 944 + * }); 945 + */ 946 + export function useResumeMutation(baseOptions?: Apollo.MutationHookOptions<ResumeMutation, ResumeMutationVariables>) { 947 + const options = {...defaultOptions, ...baseOptions} 948 + return Apollo.useMutation<ResumeMutation, ResumeMutationVariables>(ResumeDocument, options); 949 + } 950 + export type ResumeMutationHookResult = ReturnType<typeof useResumeMutation>; 951 + export type ResumeMutationResult = Apollo.MutationResult<ResumeMutation>; 952 + export type ResumeMutationOptions = Apollo.BaseMutationOptions<ResumeMutation, ResumeMutationVariables>; 953 + export const PreviousDocument = gql` 954 + mutation Previous { 955 + previous 956 + } 957 + `; 958 + export type PreviousMutationFn = Apollo.MutationFunction<PreviousMutation, PreviousMutationVariables>; 959 + 960 + /** 961 + * __usePreviousMutation__ 962 + * 963 + * To run a mutation, you first call `usePreviousMutation` within a React component and pass it any options that fit your needs. 964 + * When your component renders, `usePreviousMutation` returns a tuple that includes: 965 + * - A mutate function that you can call at any time to execute the mutation 966 + * - An object with fields that represent the current status of the mutation's execution 967 + * 968 + * @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; 969 + * 970 + * @example 971 + * const [previousMutation, { data, loading, error }] = usePreviousMutation({ 972 + * variables: { 973 + * }, 974 + * }); 975 + */ 976 + export function usePreviousMutation(baseOptions?: Apollo.MutationHookOptions<PreviousMutation, PreviousMutationVariables>) { 977 + const options = {...defaultOptions, ...baseOptions} 978 + return Apollo.useMutation<PreviousMutation, PreviousMutationVariables>(PreviousDocument, options); 979 + } 980 + export type PreviousMutationHookResult = ReturnType<typeof usePreviousMutation>; 981 + export type PreviousMutationResult = Apollo.MutationResult<PreviousMutation>; 982 + export type PreviousMutationOptions = Apollo.BaseMutationOptions<PreviousMutation, PreviousMutationVariables>; 983 + export const NextDocument = gql` 984 + mutation Next { 985 + next 986 + } 987 + `; 988 + export type NextMutationFn = Apollo.MutationFunction<NextMutation, NextMutationVariables>; 989 + 990 + /** 991 + * __useNextMutation__ 992 + * 993 + * To run a mutation, you first call `useNextMutation` within a React component and pass it any options that fit your needs. 994 + * When your component renders, `useNextMutation` returns a tuple that includes: 995 + * - A mutate function that you can call at any time to execute the mutation 996 + * - An object with fields that represent the current status of the mutation's execution 997 + * 998 + * @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; 999 + * 1000 + * @example 1001 + * const [nextMutation, { data, loading, error }] = useNextMutation({ 1002 + * variables: { 1003 + * }, 1004 + * }); 1005 + */ 1006 + export function useNextMutation(baseOptions?: Apollo.MutationHookOptions<NextMutation, NextMutationVariables>) { 1007 + const options = {...defaultOptions, ...baseOptions} 1008 + return Apollo.useMutation<NextMutation, NextMutationVariables>(NextDocument, options); 1009 + } 1010 + export type NextMutationHookResult = ReturnType<typeof useNextMutation>; 1011 + export type NextMutationResult = Apollo.MutationResult<NextMutation>; 1012 + export type NextMutationOptions = Apollo.BaseMutationOptions<NextMutation, NextMutationVariables>; 1013 + export const GetCurrentTrackDocument = gql` 1014 + query GetCurrentTrack { 1015 + currentTrack { 1016 + id 1017 + title 1018 + artist 1019 + album 1020 + albumArt 1021 + artistId 1022 + albumId 1023 + elapsed 1024 + length 1025 + year 1026 + yearString 1027 + } 1028 + } 1029 + `; 1030 + 1031 + /** 1032 + * __useGetCurrentTrackQuery__ 1033 + * 1034 + * To run a query within a React component, call `useGetCurrentTrackQuery` and pass it any options that fit your needs. 1035 + * When your component renders, `useGetCurrentTrackQuery` returns an object from Apollo Client that contains loading, error, and data properties 1036 + * you can use to render your UI. 1037 + * 1038 + * @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; 1039 + * 1040 + * @example 1041 + * const { data, loading, error } = useGetCurrentTrackQuery({ 1042 + * variables: { 1043 + * }, 1044 + * }); 1045 + */ 1046 + export function useGetCurrentTrackQuery(baseOptions?: Apollo.QueryHookOptions<GetCurrentTrackQuery, GetCurrentTrackQueryVariables>) { 1047 + const options = {...defaultOptions, ...baseOptions} 1048 + return Apollo.useQuery<GetCurrentTrackQuery, GetCurrentTrackQueryVariables>(GetCurrentTrackDocument, options); 1049 + } 1050 + export function useGetCurrentTrackLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetCurrentTrackQuery, GetCurrentTrackQueryVariables>) { 1051 + const options = {...defaultOptions, ...baseOptions} 1052 + return Apollo.useLazyQuery<GetCurrentTrackQuery, GetCurrentTrackQueryVariables>(GetCurrentTrackDocument, options); 1053 + } 1054 + export function useGetCurrentTrackSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<GetCurrentTrackQuery, GetCurrentTrackQueryVariables>) { 1055 + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} 1056 + return Apollo.useSuspenseQuery<GetCurrentTrackQuery, GetCurrentTrackQueryVariables>(GetCurrentTrackDocument, options); 1057 + } 1058 + export type GetCurrentTrackQueryHookResult = ReturnType<typeof useGetCurrentTrackQuery>; 1059 + export type GetCurrentTrackLazyQueryHookResult = ReturnType<typeof useGetCurrentTrackLazyQuery>; 1060 + export type GetCurrentTrackSuspenseQueryHookResult = ReturnType<typeof useGetCurrentTrackSuspenseQuery>; 1061 + export type GetCurrentTrackQueryResult = Apollo.QueryResult<GetCurrentTrackQuery, GetCurrentTrackQueryVariables>; 1062 + export const GetNextTrackDocument = gql` 1063 + query GetNextTrack { 1064 + nextTrack { 1065 + id 1066 + title 1067 + artist 1068 + album 1069 + albumArt 1070 + artistId 1071 + albumId 1072 + length 1073 + year 1074 + yearString 1075 + } 1076 + } 1077 + `; 1078 + 1079 + /** 1080 + * __useGetNextTrackQuery__ 1081 + * 1082 + * To run a query within a React component, call `useGetNextTrackQuery` and pass it any options that fit your needs. 1083 + * When your component renders, `useGetNextTrackQuery` returns an object from Apollo Client that contains loading, error, and data properties 1084 + * you can use to render your UI. 1085 + * 1086 + * @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; 1087 + * 1088 + * @example 1089 + * const { data, loading, error } = useGetNextTrackQuery({ 1090 + * variables: { 1091 + * }, 1092 + * }); 1093 + */ 1094 + export function useGetNextTrackQuery(baseOptions?: Apollo.QueryHookOptions<GetNextTrackQuery, GetNextTrackQueryVariables>) { 1095 + const options = {...defaultOptions, ...baseOptions} 1096 + return Apollo.useQuery<GetNextTrackQuery, GetNextTrackQueryVariables>(GetNextTrackDocument, options); 1097 + } 1098 + export function useGetNextTrackLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetNextTrackQuery, GetNextTrackQueryVariables>) { 1099 + const options = {...defaultOptions, ...baseOptions} 1100 + return Apollo.useLazyQuery<GetNextTrackQuery, GetNextTrackQueryVariables>(GetNextTrackDocument, options); 1101 + } 1102 + export function useGetNextTrackSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<GetNextTrackQuery, GetNextTrackQueryVariables>) { 1103 + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} 1104 + return Apollo.useSuspenseQuery<GetNextTrackQuery, GetNextTrackQueryVariables>(GetNextTrackDocument, options); 1105 + } 1106 + export type GetNextTrackQueryHookResult = ReturnType<typeof useGetNextTrackQuery>; 1107 + export type GetNextTrackLazyQueryHookResult = ReturnType<typeof useGetNextTrackLazyQuery>; 1108 + export type GetNextTrackSuspenseQueryHookResult = ReturnType<typeof useGetNextTrackSuspenseQuery>; 1109 + export type GetNextTrackQueryResult = Apollo.QueryResult<GetNextTrackQuery, GetNextTrackQueryVariables>; 1110 + export const GetPlaybackStatusDocument = gql` 1111 + query GetPlaybackStatus { 1112 + status 1113 + } 1114 + `; 1115 + 1116 + /** 1117 + * __useGetPlaybackStatusQuery__ 1118 + * 1119 + * To run a query within a React component, call `useGetPlaybackStatusQuery` and pass it any options that fit your needs. 1120 + * When your component renders, `useGetPlaybackStatusQuery` returns an object from Apollo Client that contains loading, error, and data properties 1121 + * you can use to render your UI. 1122 + * 1123 + * @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; 1124 + * 1125 + * @example 1126 + * const { data, loading, error } = useGetPlaybackStatusQuery({ 1127 + * variables: { 1128 + * }, 1129 + * }); 1130 + */ 1131 + export function useGetPlaybackStatusQuery(baseOptions?: Apollo.QueryHookOptions<GetPlaybackStatusQuery, GetPlaybackStatusQueryVariables>) { 1132 + const options = {...defaultOptions, ...baseOptions} 1133 + return Apollo.useQuery<GetPlaybackStatusQuery, GetPlaybackStatusQueryVariables>(GetPlaybackStatusDocument, options); 1134 + } 1135 + export function useGetPlaybackStatusLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetPlaybackStatusQuery, GetPlaybackStatusQueryVariables>) { 1136 + const options = {...defaultOptions, ...baseOptions} 1137 + return Apollo.useLazyQuery<GetPlaybackStatusQuery, GetPlaybackStatusQueryVariables>(GetPlaybackStatusDocument, options); 1138 + } 1139 + export function useGetPlaybackStatusSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<GetPlaybackStatusQuery, GetPlaybackStatusQueryVariables>) { 1140 + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} 1141 + return Apollo.useSuspenseQuery<GetPlaybackStatusQuery, GetPlaybackStatusQueryVariables>(GetPlaybackStatusDocument, options); 1142 + } 1143 + export type GetPlaybackStatusQueryHookResult = ReturnType<typeof useGetPlaybackStatusQuery>; 1144 + export type GetPlaybackStatusLazyQueryHookResult = ReturnType<typeof useGetPlaybackStatusLazyQuery>; 1145 + export type GetPlaybackStatusSuspenseQueryHookResult = ReturnType<typeof useGetPlaybackStatusSuspenseQuery>; 1146 + export type GetPlaybackStatusQueryResult = Apollo.QueryResult<GetPlaybackStatusQuery, GetPlaybackStatusQueryVariables>; 1147 + export const CurrentlyPlayingSongDocument = gql` 1148 + subscription CurrentlyPlayingSong { 1149 + currentlyPlayingSong { 1150 + id 1151 + title 1152 + artist 1153 + album 1154 + albumArt 1155 + artistId 1156 + albumId 1157 + elapsed 1158 + length 1159 + year 1160 + yearString 1161 + } 1162 + } 1163 + `; 1164 + 1165 + /** 1166 + * __useCurrentlyPlayingSongSubscription__ 1167 + * 1168 + * To run a query within a React component, call `useCurrentlyPlayingSongSubscription` and pass it any options that fit your needs. 1169 + * When your component renders, `useCurrentlyPlayingSongSubscription` returns an object from Apollo Client that contains loading, error, and data properties 1170 + * you can use to render your UI. 1171 + * 1172 + * @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; 1173 + * 1174 + * @example 1175 + * const { data, loading, error } = useCurrentlyPlayingSongSubscription({ 1176 + * variables: { 1177 + * }, 1178 + * }); 1179 + */ 1180 + export function useCurrentlyPlayingSongSubscription(baseOptions?: Apollo.SubscriptionHookOptions<CurrentlyPlayingSongSubscription, CurrentlyPlayingSongSubscriptionVariables>) { 1181 + const options = {...defaultOptions, ...baseOptions} 1182 + return Apollo.useSubscription<CurrentlyPlayingSongSubscription, CurrentlyPlayingSongSubscriptionVariables>(CurrentlyPlayingSongDocument, options); 1183 + } 1184 + export type CurrentlyPlayingSongSubscriptionHookResult = ReturnType<typeof useCurrentlyPlayingSongSubscription>; 1185 + export type CurrentlyPlayingSongSubscriptionResult = Apollo.SubscriptionResult<CurrentlyPlayingSongSubscription>; 1186 + export const PlaybackStatusDocument = gql` 1187 + subscription PlaybackStatus { 1188 + playbackStatus { 1189 + status 1190 + } 1191 + } 1192 + `; 1193 + 1194 + /** 1195 + * __usePlaybackStatusSubscription__ 1196 + * 1197 + * To run a query within a React component, call `usePlaybackStatusSubscription` and pass it any options that fit your needs. 1198 + * When your component renders, `usePlaybackStatusSubscription` returns an object from Apollo Client that contains loading, error, and data properties 1199 + * you can use to render your UI. 1200 + * 1201 + * @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; 1202 + * 1203 + * @example 1204 + * const { data, loading, error } = usePlaybackStatusSubscription({ 1205 + * variables: { 1206 + * }, 1207 + * }); 1208 + */ 1209 + export function usePlaybackStatusSubscription(baseOptions?: Apollo.SubscriptionHookOptions<PlaybackStatusSubscription, PlaybackStatusSubscriptionVariables>) { 1210 + const options = {...defaultOptions, ...baseOptions} 1211 + return Apollo.useSubscription<PlaybackStatusSubscription, PlaybackStatusSubscriptionVariables>(PlaybackStatusDocument, options); 1212 + } 1213 + export type PlaybackStatusSubscriptionHookResult = ReturnType<typeof usePlaybackStatusSubscription>; 1214 + export type PlaybackStatusSubscriptionResult = Apollo.SubscriptionResult<PlaybackStatusSubscription>; 797 1215 export const GetRockboxVersionDocument = gql` 798 1216 query GetRockboxVersion { 799 1217 rockboxVersion
+22 -1
webui/rockbox/src/Providers/GraphQLProvider.tsx
··· 3 3 createHttpLink, 4 4 InMemoryCache, 5 5 ApolloProvider, 6 + split, 6 7 } from "@apollo/client"; 8 + import { WebSocketLink } from "@apollo/client/link/ws"; 9 + import { getMainDefinition } from "@apollo/client/utilities"; 10 + import { SubscriptionClient } from "subscriptions-transport-ws"; 7 11 import { FC, ReactNode } from "react"; 8 12 9 13 const uri = ··· 16 20 uri, 17 21 }); 18 22 23 + const wsLink = new WebSocketLink( 24 + new SubscriptionClient(uri.replace("http", "ws")) 25 + ); 26 + 27 + const splitLink = split( 28 + ({ query }) => { 29 + const definition = getMainDefinition(query); 30 + return ( 31 + definition.kind === "OperationDefinition" && 32 + definition.operation === "subscription" 33 + ); 34 + }, 35 + wsLink, 36 + httpLink 37 + ); 38 + const link = splitLink; 39 + 19 40 const client = new ApolloClient({ 20 - link: httpLink, 41 + link, 21 42 cache: new InMemoryCache(), 22 43 }); 23 44
+6 -3
webui/rockbox/src/Providers/index.tsx
··· 2 2 import ThemeProvider from "./ThemeProvider"; 3 3 import { Provider as StyletronProvider } from "styletron-react"; 4 4 import { Client as Styletron } from "styletron-engine-atomic"; 5 + import { RecoilRoot } from "recoil"; 5 6 6 7 const engine = new Styletron(); 7 8 ··· 11 12 12 13 const Providers: FC<ProvidersProps> = ({ children }) => { 13 14 return ( 14 - <StyletronProvider value={engine}> 15 - <ThemeProvider>{children}</ThemeProvider> 16 - </StyletronProvider> 15 + <RecoilRoot> 16 + <StyletronProvider value={engine}> 17 + <ThemeProvider>{children}</ThemeProvider> 18 + </StyletronProvider> 19 + </RecoilRoot> 17 20 ); 18 21 }; 19 22
+13
webui/rockbox/src/Types/track.ts
··· 11 11 artistId?: string; 12 12 discnum?: number; 13 13 }; 14 + 15 + export type CurrentTrack = { 16 + id?: string; 17 + album?: string; 18 + artist?: string; 19 + title?: string; 20 + cover?: string; 21 + duration: number; 22 + progress: number; 23 + isPlaying?: boolean; 24 + albumId?: string | null; 25 + artistId?: string | null; 26 + };
+36
webui/rockbox/src/mocks.ts
··· 1 + import { 2 + GET_CURRENT_TRACK, 3 + GET_PLAYBACK_STATUS, 4 + } from "./GraphQL/Playback/Query"; 5 + 6 + export const mocks = [ 7 + { 8 + request: { 9 + query: GET_CURRENT_TRACK, 10 + }, 11 + result: { 12 + data: { 13 + currentTrack: { 14 + id: "cm272oeon00esm9634q1lw5ae", 15 + title: "Set It Off", 16 + artist: "Boosie Badazz", 17 + album: "Bad Azz", 18 + albumArt: 19 + "https://resources.tidal.com/images/31ce8fc7/b10c/47ee/991d/6fab3e15dbe4/320x320.jpg", 20 + artistId: "cm272ocoi003km963fy45i7cn", 21 + albumId: "cm272ocoi003lm963xo1d7wb2", 22 + elapsed: 153762, 23 + length: 284633, 24 + year: 2006, 25 + yearString: "2006-09-19", 26 + }, 27 + }, 28 + }, 29 + }, 30 + { 31 + request: { 32 + query: GET_PLAYBACK_STATUS, 33 + }, 34 + result: { data: { status: 1 } }, 35 + }, 36 + ];