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.

webui: update current track in real time

+707 -126
+35 -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", 5683 5685 "md5", 5686 + "once_cell", 5684 5687 "owo-colors 4.1.0", 5685 5688 "reqwest", 5686 5689 "rockbox-library", 5687 5690 "rockbox-sys", 5688 5691 "serde", 5689 5692 "serde_json", 5693 + "slab", 5690 5694 "sqlx", 5691 5695 "tokio", 5692 5696 ] ··· 5732 5736 version = "0.1.0" 5733 5737 dependencies = [ 5734 5738 "anyhow", 5739 + "md5", 5735 5740 "owo-colors 4.1.0", 5736 5741 "queryst", 5737 5742 "reqwest", ··· 7346 7351 7347 7352 [[package]] 7348 7353 name = "toml_edit" 7349 - version = "0.22.20" 7354 + version = "0.22.22" 7350 7355 source = "registry+https://github.com/rust-lang/crates.io-index" 7351 - checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" 7356 + checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" 7352 7357 dependencies = [ 7353 7358 "indexmap 2.5.0", 7354 7359 "toml_datetime", ··· 8326 8331 8327 8332 [[package]] 8328 8333 name = "winnow" 8329 - version = "0.6.18" 8334 + version = "0.6.20" 8330 8335 source = "registry+https://github.com/rust-lang/crates.io-index" 8331 - checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" 8336 + checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" 8332 8337 dependencies = [ 8333 8338 "memchr", 8334 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
+4
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" 13 15 md5 = "0.7.0" 16 + once_cell = "1.20.2" 14 17 owo-colors = "4.1.0" 15 18 reqwest = {version = "0.12.7", features = ["rustls-tls", "json"], default-features = false} 16 19 rockbox-library = {path = "../library"} 17 20 rockbox-sys = {path = "../sys"} 18 21 serde = "1.0.210" 19 22 serde_json = "1.0.128" 23 + slab = "0.4.9" 20 24 sqlx = {version = "0.8.2", features = ["runtime-tokio", "tls-rustls", "sqlite", "chrono", "derive", "macros"]} 21 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;
+17 -1
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; 4 6 use rockbox_library::repo; 5 7 use rockbox_sys::{ 6 8 events::RockboxCommand, ··· 8 10 }; 9 11 use sqlx::{Pool, Sqlite}; 10 12 11 - use crate::{rockbox_url, schema::objects::track::Track}; 13 + use crate::{rockbox_url, schema::objects::track::Track, simplebroker::SimpleBroker}; 12 14 13 15 #[derive(Default)] 14 16 pub struct PlaybackQuery; ··· 148 150 Ok("hard_stop".to_string()) 149 151 } 150 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
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 }
webui/rockbox/bun.lockb

This is a binary file and will not be displayed.

+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 },
+24 -21
webui/rockbox/src/Components/AlbumDetails/AlbumDetails.test.tsx
··· 5 5 import { MemoryRouter } from "react-router-dom"; 6 6 import { MockedProvider } from "@apollo/client/testing"; 7 7 import { mocks } from "../../mocks"; 8 + import { RecoilRoot } from "recoil"; 8 9 9 10 describe("AlbumDetails", () => { 10 11 it("should render", () => { ··· 16 17 const { container } = render( 17 18 <MemoryRouter initialEntries={["/"]}> 18 19 <MockedProvider mocks={mocks}> 19 - <AlbumDetails 20 - onGoBack={onGoBack} 21 - onLike={onLike} 22 - onPlayAll={onPlayAll} 23 - onShuffleAll={onShuffleAll} 24 - onUnlike={onUnlike} 25 - tracks={tracks} 26 - album={{ 27 - id: "1", 28 - title: "One Cold Night (Live)", 29 - artist: "Seether", 30 - year: 2006, 31 - albumArt: 32 - "https://resources.tidal.com/images/f6f5f0a6/dc95/4561/9ca6/6ba1e0f6a062/320x320.jpg", 33 - artistId: "1", 34 - md5: "md5", 35 - yearString: "2006", 36 - tracks: [], 37 - }} 38 - volumes={[]} 39 - /> 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> 40 43 </MockedProvider> 41 44 </MemoryRouter> 42 45 );
+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 -2
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 Track } from "../../Types/track"; 11 + import { CurrentTrack as NowPlaying } from "../../Types/track"; 12 12 13 13 export type ControlBarProps = { 14 - nowPlaying?: Track; 14 + nowPlaying?: NowPlaying; 15 15 onPlay: () => void; 16 16 onPause: () => void; 17 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 + });
+77 -7
webui/rockbox/src/Components/ControlBar/ControlBarWithData.tsx
··· 1 - import { FC, useEffect, useState } from "react"; 1 + import { FC, useEffect } from "react"; 2 2 import ControlBar from "./ControlBar"; 3 3 import { 4 + useCurrentlyPlayingSongSubscription, 4 5 useGetCurrentTrackQuery, 5 6 useGetPlaybackStatusQuery, 6 7 useNextMutation, 7 8 usePauseMutation, 9 + usePlaybackStatusSubscription, 8 10 usePreviousMutation, 9 11 useResumeMutation, 10 12 } from "../../Hooks/GraphQL"; 11 13 import { CurrentTrack } from "../../Types/track"; 14 + import _ from "lodash"; 15 + import { useRecoilState } from "recoil"; 16 + import { controlBarState } from "./ControlBarState"; 12 17 13 18 const ControlBarWithData: FC = () => { 14 - const [nowPlaying, setNowPlaying] = useState<CurrentTrack | undefined>( 15 - undefined 16 - ); 19 + const [{ nowPlaying, locked }, setControlBarState] = 20 + useRecoilState(controlBarState); 17 21 const { data, loading } = useGetCurrentTrackQuery(); 18 - const { data: playback } = useGetPlaybackStatusQuery(); 22 + const { data: playback } = useGetPlaybackStatusQuery({ 23 + fetchPolicy: "network-only", 24 + }); 19 25 const [pause] = usePauseMutation(); 20 26 const [resume] = useResumeMutation(); 21 27 const [previous] = usePreviousMutation(); 22 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]); 23 59 24 60 useEffect(() => { 25 61 if (loading || !data) { 62 + return; 63 + } 64 + 65 + if (nowPlaying) { 26 66 return; 27 67 } 28 68 ··· 40 80 isPlaying: playback?.status === 1, 41 81 albumId: data.currentTrack?.albumId, 42 82 }); 83 + // eslint-disable-next-line react-hooks/exhaustive-deps 43 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 + 44 114 return ( 45 115 <ControlBar 46 116 nowPlaying={nowPlaying} 47 - onPlay={() => resume()} 48 - onPause={() => pause()} 117 + onPlay={onPlay} 118 + onPause={onPause} 49 119 onNext={() => next()} 50 120 onPrevious={() => previous()} 51 121 onShuffle={() => {}}
+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"
+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 + `;
+89
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 = { ··· 518 529 519 530 520 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 } }; 521 542 522 543 export type GetRockboxVersionQueryVariables = Exact<{ [key: string]: never; }>; 523 544 ··· 1123 1144 export type GetPlaybackStatusLazyQueryHookResult = ReturnType<typeof useGetPlaybackStatusLazyQuery>; 1124 1145 export type GetPlaybackStatusSuspenseQueryHookResult = ReturnType<typeof useGetPlaybackStatusSuspenseQuery>; 1125 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>; 1126 1215 export const GetRockboxVersionDocument = gql` 1127 1216 query GetRockboxVersion { 1128 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