From 1cc04bda664cf4f383e02c91a857056da7752bcb Mon Sep 17 00:00:00 2001 From: asepharyana Date: Fri, 3 Jul 2026 20:56:06 +0700 Subject: [PATCH] feat(leptos): Phase 4 Task 2 - VoiceConnectionCard component --- .../live/components/voice_connection_card.rs | 211 ++++++++++++++++++ .../frontend/src/features/messages/mod.rs | 3 +- 2 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 services/frontend-leptos/frontend/src/features/live/components/voice_connection_card.rs diff --git a/services/frontend-leptos/frontend/src/features/live/components/voice_connection_card.rs b/services/frontend-leptos/frontend/src/features/live/components/voice_connection_card.rs new file mode 100644 index 0000000..fc5e79f --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/live/components/voice_connection_card.rs @@ -0,0 +1,211 @@ +use leptos::prelude::*; +use crate::features::live::hooks::use_voice_control::{use_voice_control, VoiceControlState}; +use crate::ui::button::{Button, ButtonVariant}; +use crate::ui::card::{Card, CardContent, CardDescription, CardHeader, CardTitle}; +use shared_types::guild::{Guild, Channel}; + +/// VoiceConnectionCard component for Leptos +/// Renders guild and voice channel selectors with connect/disconnect controls +#[component] +pub fn VoiceConnectionCard( + #[prop(optional)] voice_state: Option, + #[prop(optional)] class: &'static str, +) -> impl IntoView { + let default_state = use_voice_control(); + let state = voice_state.unwrap_or(default_state); + + // Reactive signal for selected guild + let (selected_guild, set_selected_guild) = create_signal::(String::new()); + // Reactive signal for selected channel + let (selected_channel, set_selected_channel) = create_signal::(String::new()); + + // When guild is selected, load voice channels + create_effect(move |_| { + let guild_id = selected_guild.get(); + if !guild_id.is_empty() { + (state.load_voice_channels)(guild_id); + } + }); + + // Load guilds on mount + create_effect(move |_| { + (state.load_guilds)(); + }); + + let on_guild_change = move |ev: leptos::ev::Event| { + if let Some(target) = ev.target().and_then(|t| t.dyn_into::().ok()) { + set_selected_guild(target.value()); + } + }; + + let on_channel_change = move |ev: leptos::ev::Event| { + if let Some(target) = ev.target().and_then(|t| t.dyn_into::().ok()) { + set_selected_channel(target.value()); + } + }; + + let on_join_click = move |_| { + let guild_id = selected_guild.get(); + let channel_id = selected_channel.get(); + if !guild_id.is_empty() && !channel_id.is_empty() { + (state.join_voice)(guild_id, channel_id); + } + }; + + let on_disconnect_click = move |_| { + (state.leave_voice)(); + }; + + // Read signals for reactive rendering + let guilds = state.guilds; + let voice_channels = state.voice_channels; + let loading = state.loading; + let error = state.error; + let voice_status = state.voice_status; + + let is_connected = move || { + voice_status.get().map(|s| s.connected).unwrap_or(false) + }; + + let can_join = move || { + !selected_guild.get().is_empty() && !selected_channel.get().is_empty() && !loading.get() + }; + + let can_disconnect = move || { + is_connected() && !loading.get() + }; + + view! { + + +
+ + + + "Voice Bridge" +
+ + "Join a Discord voice channel, listen, and transmit audio." + +
+ + + {/* Guild and Channel Selectors */} +
+
+ + +
+ +
+ + +
+
+ + {/* Error Display */} + {move || { + error.get().map(|err| { + view! { +
+ {err} +
+ } + }) + }} + + {/* Status Display */} + {move || { + voice_status.get().map(|status| { + view! { +
+
+ + {move || if status.connected { "Connected" } else { "Disconnected" }} + + {move || { + status.active_channel_name.clone().map(|name| { + view! { + + " - "{name} + + } + }) + }} +
+ } + }) + }} + + {/* Control Buttons */} +
+ + + + + {move || { + if loading.get() { + view! { + + "Loading..." + + }.into_view() + } else { + view! { }.into_view() + } + }} +
+
+
+ } +} diff --git a/services/frontend-leptos/frontend/src/features/messages/mod.rs b/services/frontend-leptos/frontend/src/features/messages/mod.rs index 5af8065..6569235 100644 --- a/services/frontend-leptos/frontend/src/features/messages/mod.rs +++ b/services/frontend-leptos/frontend/src/features/messages/mod.rs @@ -287,6 +287,7 @@ pub fn MessagesPanel() -> impl IntoView { let load_more_cb = state.load_more.clone(); let empty_text: &'static str = if show_search.get() { "No messages found matching your search." } else { "No captures yet." }; let has_more = if show_search.get() { false } else { state.has_more.get() }; + let on_load_more_clone: Arc = Arc::new(move || load_more_cb()); view! { impl IntoView { loading=state.loading.get() has_more=has_more loading_more=state.loading_more.get() - on_load_more=Arc::new(move || load_more_cb()) as Arc + on_load_more=on_load_more_clone on_reanalyze=state.reanalyze.clone() /> }