diff --git a/services/frontend-leptos/frontend/src/features/live/components/mod.rs b/services/frontend-leptos/frontend/src/features/live/components/mod.rs index 4227868..09f2c04 100644 --- a/services/frontend-leptos/frontend/src/features/live/components/mod.rs +++ b/services/frontend-leptos/frontend/src/features/live/components/mod.rs @@ -2,8 +2,14 @@ pub mod voice_connection_card; pub mod active_speakers; pub mod audio_visualizer; pub mod mic_level_meter; +pub mod now_playing; +pub mod music_sub_panel; +pub mod screen_sub_panel; pub use voice_connection_card::VoiceConnectionCard; pub use active_speakers::ActiveSpeakers; pub use audio_visualizer::AudioVisualizer; pub use mic_level_meter::MicLevelMeter; +pub use now_playing::NowPlaying; +pub use music_sub_panel::MusicSubPanel; +pub use screen_sub_panel::ScreenSubPanel; diff --git a/services/frontend-leptos/frontend/src/features/live/components/music_sub_panel.rs b/services/frontend-leptos/frontend/src/features/live/components/music_sub_panel.rs new file mode 100644 index 0000000..deb0f23 --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/live/components/music_sub_panel.rs @@ -0,0 +1,56 @@ +use leptos::prelude::*; + +/// MusicSubPanel — Music playlist controls and URL input +#[component] +pub fn MusicSubPanel( + #[prop(optional)] on_queue: Option>, +) -> impl IntoView { + let (url_input, set_url_input) = create_signal::(String::new()); + let (is_loading, set_is_loading) = create_signal::(false); + + let handle_queue_click = move |_| { + let url = url_input.get().trim().to_string(); + if !url.is_empty() { + if let Some(ref cb) = on_queue { + set_is_loading.set(true); + cb(url.clone()); + set_url_input.set(String::new()); + set_is_loading.set(false); + } + } + }; + + view! { +
+
+
+ + + + + "Music" +
+
+
+
+ + +
+ +
+
+ } +} diff --git a/services/frontend-leptos/frontend/src/features/live/components/now_playing.rs b/services/frontend-leptos/frontend/src/features/live/components/now_playing.rs new file mode 100644 index 0000000..f18ba9b --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/live/components/now_playing.rs @@ -0,0 +1,104 @@ +use leptos::prelude::*; +use shared_types::media::MediaState; + +/// NowPlaying — Displays current media item and queue info +#[component] +pub fn NowPlaying( + #[prop(optional)] state: Option, + #[prop(optional)] on_skip: Option>, + #[prop(optional)] on_stop: Option>, +) -> impl IntoView { + let media_state = create_rw_signal::>(state); + + // Wrap callbacks in StoredValue for shareable non-Clone ownership in Leptos context + let skip_cb = StoredValue::new(on_skip); + let stop_cb = StoredValue::new(on_stop); + let has_skip = skip_cb.with_value(|v| v.is_some()); + let has_stop = stop_cb.with_value(|v| v.is_some()); + + view! { +
+
+
"Now Playing"
+
+
+ {move || { + media_state.get().map(|ms| { + let current = ms.current.as_ref().cloned(); + let queue_len = ms.queue.len(); + + view! { + <> + {current.map(|item| { + let title = item.title.clone().unwrap_or_else(|| "Unknown".to_string()); + let duration_ms = item.duration_ms.unwrap_or(0); + let duration_sec = duration_ms / 1000; + view! { +
+
+ {title} +
+
+ {format!("{}s", duration_sec)} +
+
+ {has_skip.then(|| { + view! { + + } + })} + {has_stop.then(|| { + view! { + + } + })} +
+
+ } + })} + + {(queue_len > 0).then(|| { + view! { +
+
+ {format!("Queue: {} item{}", queue_len, if queue_len == 1 { "" } else { "s" })} +
+
+ } + })} + + {(queue_len == 0).then(|| { + view! { +
+ "Queue is empty" +
+ } + })} + + } + }) + }} + + {move || { + media_state.get().is_none().then(|| { + view! { +
+ "No media connected" +
+ } + }) + }} +
+
+ } +} diff --git a/services/frontend-leptos/frontend/src/features/live/components/screen_sub_panel.rs b/services/frontend-leptos/frontend/src/features/live/components/screen_sub_panel.rs new file mode 100644 index 0000000..7106b34 --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/live/components/screen_sub_panel.rs @@ -0,0 +1,84 @@ +use leptos::prelude::*; + +/// ScreenSubPanel — Screenshare controls +#[component] +pub fn ScreenSubPanel( + #[prop(optional)] on_start_stream: Option>, + #[prop(optional)] on_stop_stream: Option>, +) -> impl IntoView { + let (is_streaming, set_is_streaming) = create_signal::(false); + + let has_start = on_start_stream.is_some(); + let has_stop = on_stop_stream.is_some(); + + view! { +
+
+
+ + + + + + "Screenshare" +
+
+
+

+ "Stream your screen to the voice channel for everyone to see." +

+ +
+ {has_start.then(|| { + view! { + + } + })} + + {has_stop.then(|| { + view! { + + } + })} +
+ + {move || { + is_streaming.get().then(|| { + view! { +
+ "🔴 Live streaming..." +
+ } + }) + }} +
+
+ } +} +