From d482b7ab3ad48b685cedd3e13aa6bfacc5448251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20Dani=C3=ABl=20Maat?= Date: Sat, 29 Nov 2025 23:30:23 +0800 Subject: [PATCH] WIP: feat(webserver): Reimplement music player --- pkgs/packages/webserver/Cargo.lock | 282 ++++++++++++++++++ pkgs/packages/webserver/Cargo.toml | 3 +- pkgs/packages/webserver/src/app.rs | 3 + .../webserver/src/app/music_sample.rs | 75 +++++ .../src/app/music_sample/ssr_safe.rs | 121 ++++++++ .../webserver/style/custom-bulma.scss | 7 +- 6 files changed, 485 insertions(+), 6 deletions(-) create mode 100644 pkgs/packages/webserver/src/app/music_sample.rs create mode 100644 pkgs/packages/webserver/src/app/music_sample/ssr_safe.rs diff --git a/pkgs/packages/webserver/Cargo.lock b/pkgs/packages/webserver/Cargo.lock index a6db6ec..ce1dfea 100644 --- a/pkgs/packages/webserver/Cargo.lock +++ b/pkgs/packages/webserver/Cargo.lock @@ -11,6 +11,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "any_spawner" version = "0.3.0" @@ -96,6 +105,12 @@ dependencies = [ "syn", ] +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "axum" version = "0.8.7" @@ -231,6 +246,19 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "codee" version = "0.3.4" @@ -330,6 +358,17 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -371,6 +410,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -391,6 +465,27 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "default-struct-builder" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0df63c21a4383f94bd5388564829423f35c316aed85dc4f8427aded372c7c0d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive-where" version = "1.6.0" @@ -701,6 +796,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "gloo-utils" version = "0.2.0" @@ -916,6 +1023,30 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -997,6 +1128,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1090,6 +1227,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "leptos" version = "0.8.3" @@ -1129,6 +1272,31 @@ dependencies = [ "web-sys", ] +[[package]] +name = "leptos-use" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2162c453100c7d6bc0b6f188ef1df582e35c2458caf6cb69fcddc87619c0db" +dependencies = [ + "cfg-if", + "chrono", + "codee", + "cookie", + "default-struct-builder", + "futures-util", + "gloo-timers", + "js-sys", + "lazy_static", + "leptos", + "paste", + "send_wrapper", + "thiserror 2.0.17", + "unic-langid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "leptos_axum" version = "0.8.3" @@ -1469,6 +1637,21 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28" +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.17.0" @@ -1662,6 +1845,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2397,6 +2586,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -2563,6 +2758,37 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -2570,6 +2796,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -2596,6 +2823,7 @@ dependencies = [ "console_error_panic_hook", "figment", "leptos", + "leptos-use", "leptos_axum", "leptos_meta", "leptos_router", @@ -2886,6 +3114,24 @@ dependencies = [ "version_check", ] +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "tinystr", +] + [[package]] name = "unicase" version = "2.8.1" @@ -3125,6 +3371,41 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -3436,6 +3717,7 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", diff --git a/pkgs/packages/webserver/Cargo.toml b/pkgs/packages/webserver/Cargo.toml index 6b53991..0b70aad 100644 --- a/pkgs/packages/webserver/Cargo.toml +++ b/pkgs/packages/webserver/Cargo.toml @@ -11,6 +11,7 @@ axum = { version = "0.8.7", features = ["macros"], optional = true } console_error_panic_hook = { version = "0.1.7", optional = true } figment = { version = "0.10.19", features = ["toml", "env"] } leptos = "0.8.3" +leptos-use = "0.16.3" leptos_axum = { version = "0.8.3", optional = true } leptos_meta = "0.8.3" leptos_router = "0.8.3" @@ -21,7 +22,7 @@ thiserror = "2.0.17" tokio = { version = "1.48.0", features = ["rt-multi-thread"], optional = true } url = "2.5.7" wasm-bindgen = { version = "=0.2.100", optional = true } -web-sys = "^0.3.77" +web-sys = { version = "^0.3.77", features = ["AnalyserNode", "AudioContext", "AudioDestinationNode", "GainNode", "HtmlMediaElement", "MediaElementAudioSourceNode"] } [features] hydrate = [ diff --git a/pkgs/packages/webserver/src/app.rs b/pkgs/packages/webserver/src/app.rs index 913c640..d8e02ba 100644 --- a/pkgs/packages/webserver/src/app.rs +++ b/pkgs/packages/webserver/src/app.rs @@ -7,10 +7,12 @@ use leptos_router::{ mod homepage; mod mail; +mod music_sample; use crate::components::Navbar; use homepage::HomePage; use mail::Mail; +use music_sample::MusicSample; pub fn shell(options: LeptosOptions) -> impl IntoView { view! { @@ -49,6 +51,7 @@ pub fn App() -> impl IntoView { + diff --git a/pkgs/packages/webserver/src/app/music_sample.rs b/pkgs/packages/webserver/src/app/music_sample.rs new file mode 100644 index 0000000..f9fc1cb --- /dev/null +++ b/pkgs/packages/webserver/src/app/music_sample.rs @@ -0,0 +1,75 @@ +#![allow(dead_code, unused_variables)] +use leptos::{logging, prelude::*}; +use leptos_meta::{Meta, Title}; +use leptos_use::use_event_listener; +use ssr_safe::{MediaPlayer, MediaPlayerError}; + +mod ssr_safe; + +#[component] +fn Controls() -> impl IntoView { + let player: LocalResource> = expect_context(); + + Effect::new(move || { + let audio_element = if let Some(Ok(p)) = player.get() { + Some(p.audio_element()) + } else { + None + }; + + use_event_listener(audio_element, ssr_safe::media_events::error, |ev| { + logging::error!("{:?}", ev); + }); + }); + + view! { +
+ + + +
+
+ // The play/pause/etc button +
+ +
+
+ + // The title display +
+ {move || { + Ok::<_, MediaPlayerError>(player.get().transpose()?.map(|p| p.get_title())) + }} +
+ + // The artist display +
+
Artist
+
+
+ +
+
+
+ } +} + +#[component] +pub fn MusicSample() -> impl IntoView { + let player = LocalResource::new(MediaPlayer::new); + provide_context(player); + + view! { + + + + <section class="hero is-fullheight-with-navbar"> + <div class="hero-body p-0">Body</div> + <div class="hero-foot"> + <Controls /> + </div> + </section> + } +} diff --git a/pkgs/packages/webserver/src/app/music_sample/ssr_safe.rs b/pkgs/packages/webserver/src/app/music_sample/ssr_safe.rs new file mode 100644 index 0000000..13d2269 --- /dev/null +++ b/pkgs/packages/webserver/src/app/music_sample/ssr_safe.rs @@ -0,0 +1,121 @@ +use leptos::{ev::EventDescriptor, logging}; +use leptos_use::use_event_listener; +use web_sys::EventTarget; + +pub const DEFAULT_MP3: &str = "/Mseq_-_Journey.mp3a"; + +#[derive(Clone)] +pub struct MediaPlayer { + context: web_sys::AudioContext, + audio_element: web_sys::HtmlAudioElement, +} + +impl MediaPlayer { + pub async fn new() -> Result<Self, MediaPlayerError> { + let context = web_sys::AudioContext::new()?; + let audio_element = web_sys::HtmlAudioElement::new_with_src(DEFAULT_MP3)?; + let source_node = context.create_media_element_source(&audio_element)?; + let gain_node = context.create_gain()?; + let analyser_node = context.create_analyser()?; + analyser_node.set_fft_size(2048); + analyser_node.set_smoothing_time_constant(0.8); + + source_node.connect_with_audio_node(&analyser_node)?; + source_node.connect_with_audio_node(&gain_node)?; + gain_node.connect_with_audio_node(&context.destination())?; + + Ok(Self { + context, + audio_element, + }) + } + + pub fn set_title(&self, title: &str) { + self.audio_element.set_src(title); + } + + pub fn get_title(&self) -> String { + // Hardcoded for now, eventually I'll make this a proper + // player again... + "Journey".to_owned() + } + + pub fn context(&self) -> EventTarget { + self.context.clone().into() + } + + pub fn audio_element(&self) -> EventTarget { + self.audio_element.clone().into() + } + + pub fn use_media_event<Ev, F>(&self, event: Ev, handler: F) -> impl Fn() + Clone + Send + Sync + use<Ev, F> + where + F: FnMut(<Ev as EventDescriptor>::EventType) + 'static, + Ev: EventDescriptor + 'static, + { + use_event_listener(self.audio_element.clone(), event, handler) + } + + pub fn use_statechange<F>(&self, handler: F) -> impl Fn() + Clone + Send + Sync + where + F: FnMut(<media_events::statechange as EventDescriptor>::EventType) + 'static, + { + use_event_listener(self.context.clone(), media_events::statechange, handler) + } +} + +#[derive(thiserror::Error, Debug, Clone)] +pub enum MediaPlayerError { + #[error("todo")] + Todo, +} + +impl From<web_sys::wasm_bindgen::JsValue> for MediaPlayerError { + fn from(value: web_sys::wasm_bindgen::JsValue) -> Self { + logging::error!("Some kind of error"); + Self::Todo {} + } +} + +pub mod media_events { + use leptos::ev::EventDescriptor; + use std::borrow::Cow; + + #[derive(Copy, Clone, Debug)] + #[allow(non_camel_case_types)] + pub struct error; + + impl EventDescriptor for error { + type EventType = web_sys::Event; + const BUBBLES: bool = false; + + #[inline(always)] + fn name(&self) -> Cow<'static, str> { + "error".into() + } + + #[inline(always)] + fn event_delegation_key(&self) -> Cow<'static, str> { + "$$$error".into() + } + } + + #[derive(Copy, Clone, Debug)] + #[allow(non_camel_case_types)] + pub struct statechange; + + impl EventDescriptor for statechange { + type EventType = web_sys::Event; + const BUBBLES: bool = false; + + #[inline(always)] + fn name(&self) -> Cow<'static, str> { + "statechange".into() + } + + #[inline(always)] + fn event_delegation_key(&self) -> Cow<'static, str> { + "$$$statechange".into() + } + } +} diff --git a/pkgs/packages/webserver/style/custom-bulma.scss b/pkgs/packages/webserver/style/custom-bulma.scss index 0f15681..455d381 100644 --- a/pkgs/packages/webserver/style/custom-bulma.scss +++ b/pkgs/packages/webserver/style/custom-bulma.scss @@ -46,11 +46,8 @@ iv.$family-monospace: "Hack", iv.$family-monospace; @forward "bulma/sass/grid/columns"; -@forward "bulma/sass/helpers/typography"; -@forward "bulma/sass/helpers/color"; - -@forward "bulma/sass/layout/container"; -@forward "bulma/sass/layout/section"; +@forward "bulma/sass/helpers"; +@forward "bulma/sass/layout"; @forward "bulma/sass/components/navbar" with ( $navbar-burger-color: iv.$grey-light,