diff --git a/services/frontend-leptos/frontend/src/features/live/components/mod.rs b/services/frontend-leptos/frontend/src/features/live/components/mod.rs new file mode 100644 index 0000000..9f1039a --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/live/components/mod.rs @@ -0,0 +1 @@ +// Empty for now - components will be added in future phases diff --git a/services/frontend-leptos/frontend/src/features/live/hooks/mod.rs b/services/frontend-leptos/frontend/src/features/live/hooks/mod.rs new file mode 100644 index 0000000..88d7ece --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/live/hooks/mod.rs @@ -0,0 +1,2 @@ +pub mod use_voice_control; +pub mod use_media_control; diff --git a/services/frontend-leptos/frontend/src/features/live/hooks/use_media_control.rs b/services/frontend-leptos/frontend/src/features/live/hooks/use_media_control.rs new file mode 100644 index 0000000..bc9ca30 --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/live/hooks/use_media_control.rs @@ -0,0 +1,157 @@ +use leptos::prelude::*; +use shared_types::media::MediaState; +use crate::api::voice::{ + get_media_status, media_queue, media_skip, media_stop, media_volume, +}; +use std::sync::Arc; +use wasm_bindgen_futures::spawn_local; + +/// Callback type for enqueue +pub type EnqueueCallback = Arc; +/// Callback type for skip_track +pub type SkipTrackCallback = Arc; +/// Callback type for stop_playback +pub type StopPlaybackCallback = Arc; +/// Callback type for set_volume +pub type SetVolumeCallback = Arc; +/// Callback type for refresh +pub type RefreshCallback = Arc; + +/// State returned by use_media_control hook +#[derive(Clone)] +pub struct MediaControlState { + /// Current media playback state + pub media_state: RwSignal>, + /// Whether we're currently loading data + pub loading: RwSignal, + /// Last error message if any + pub error: RwSignal>, + /// Enqueue media (source URL, mode: "music" or "screen") + pub enqueue: EnqueueCallback, + /// Skip to next track + pub skip_track: SkipTrackCallback, + /// Stop all playback + pub stop_playback: StopPlaybackCallback, + /// Set volume level (0.0 - 1.0) + pub set_volume: SetVolumeCallback, + /// Refresh media status from server + pub refresh: RefreshCallback, +} + +/// Hook to manage media playback state and controls +pub fn use_media_control() -> MediaControlState { + // Core signals + let media_state_signal = RwSignal::new(None::); + let loading_signal = RwSignal::new(false); + let error_signal = RwSignal::new(None::); + + // Enqueue media + let enqueue_impl = Arc::new(move |source: String, mode: String| { + spawn_local({ + let source = source.clone(); + let mode = mode.clone(); + async move { + error_signal.set(None); + loading_signal.set(true); + + match media_queue(&source, &mode).await { + Ok(state) => { + media_state_signal.set(Some(state)); + loading_signal.set(false); + } + Err(e) => { + error_signal.set(Some(format!("Failed to enqueue media: {}", e))); + loading_signal.set(false); + } + } + } + }); + }); + + // Skip to next track + let skip_track_impl = Arc::new(move || { + spawn_local(async move { + error_signal.set(None); + loading_signal.set(true); + + match media_skip().await { + Ok(state) => { + media_state_signal.set(Some(state)); + loading_signal.set(false); + } + Err(e) => { + error_signal.set(Some(format!("Failed to skip track: {}", e))); + loading_signal.set(false); + } + } + }); + }); + + // Stop all playback + let stop_playback_impl = Arc::new(move || { + spawn_local(async move { + error_signal.set(None); + loading_signal.set(true); + + match media_stop().await { + Ok(state) => { + media_state_signal.set(Some(state)); + loading_signal.set(false); + } + Err(e) => { + error_signal.set(Some(format!("Failed to stop playback: {}", e))); + loading_signal.set(false); + } + } + }); + }); + + // Set volume level + let set_volume_impl = Arc::new(move |volume: f64| { + spawn_local(async move { + error_signal.set(None); + loading_signal.set(true); + + match media_volume(volume).await { + Ok(state) => { + media_state_signal.set(Some(state)); + loading_signal.set(false); + } + Err(e) => { + error_signal.set(Some(format!("Failed to set volume: {}", e))); + loading_signal.set(false); + } + } + }); + }); + + // Refresh media status + let refresh_impl = Arc::new(move || { + spawn_local(async move { + error_signal.set(None); + loading_signal.set(true); + + match get_media_status().await { + Ok(state) => { + media_state_signal.set(Some(state)); + loading_signal.set(false); + } + Err(e) => { + error_signal.set(Some(format!("Failed to refresh media status: {}", e))); + loading_signal.set(false); + } + } + }); + }); + + MediaControlState { + media_state: media_state_signal, + loading: loading_signal, + error: error_signal, + enqueue: enqueue_impl, + skip_track: skip_track_impl, + stop_playback: stop_playback_impl, + set_volume: set_volume_impl, + refresh: refresh_impl, + } +} diff --git a/services/frontend-leptos/frontend/src/features/live/hooks/use_voice_control.rs b/services/frontend-leptos/frontend/src/features/live/hooks/use_voice_control.rs new file mode 100644 index 0000000..8af2bca --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/live/hooks/use_voice_control.rs @@ -0,0 +1,177 @@ +use leptos::prelude::*; +use shared_types::guild::{Guild, Channel}; +use shared_types::voice::VoiceStatus; +use crate::api::voice::{ + get_guilds, get_voice_channels, get_text_channels, get_voice_status, + connect_voice, disconnect_voice, +}; +use std::sync::Arc; +use wasm_bindgen_futures::spawn_local; + +/// Callback type for join_voice +pub type JoinVoiceCallback = Arc; +/// Callback type for leave_voice +pub type LeaveVoiceCallback = Arc; +/// Callback type for load_guilds +pub type LoadGuildsCallback = Arc; +/// Callback type for load_voice_channels +pub type LoadVoiceChannelsCallback = Arc; +/// Callback type for load_text_channels +pub type LoadTextChannelsCallback = Arc; + +/// State returned by use_voice_control hook +#[derive(Clone)] +pub struct VoiceControlState { + /// List of available guilds + pub guilds: RwSignal>, + /// List of voice channels for current guild + pub voice_channels: RwSignal>, + /// List of text channels for current guild + pub text_channels: RwSignal>, + /// Current voice connection status + pub voice_status: RwSignal>, + /// Whether we're currently loading data + pub loading: RwSignal, + /// Last error message if any + pub error: RwSignal>, + /// Join a voice channel + pub join_voice: JoinVoiceCallback, + /// Leave the current voice channel + pub leave_voice: LeaveVoiceCallback, + /// Fetch list of guilds + pub load_guilds: LoadGuildsCallback, + /// Fetch voice channels for a guild + pub load_voice_channels: LoadVoiceChannelsCallback, + /// Fetch text channels for a guild + pub load_text_channels: LoadTextChannelsCallback, +} + +/// Hook to manage voice connection state and controls +pub fn use_voice_control() -> VoiceControlState { + // Core signals + let guilds_signal = RwSignal::new(Vec::::new()); + let voice_channels_signal = RwSignal::new(Vec::::new()); + let text_channels_signal = RwSignal::new(Vec::::new()); + let voice_status_signal = RwSignal::new(None::); + let loading_signal = RwSignal::new(false); + let error_signal = RwSignal::new(None::); + + // Join a voice channel + let join_voice_impl = Arc::new(move |guild_id: String, channel_id: String| { + spawn_local({ + let guild_id = guild_id.clone(); + let channel_id = channel_id.clone(); + async move { + error_signal.set(None); + loading_signal.set(true); + + match connect_voice(&guild_id, &channel_id).await { + Ok(status) => { + voice_status_signal.set(Some(status)); + loading_signal.set(false); + } + Err(e) => { + error_signal.set(Some(format!("Failed to join voice: {}", e))); + loading_signal.set(false); + } + } + } + }); + }); + + // Leave the current voice channel + let leave_voice_impl = Arc::new(move || { + spawn_local(async move { + error_signal.set(None); + loading_signal.set(true); + + match disconnect_voice().await { + Ok(status) => { + voice_status_signal.set(Some(status)); + loading_signal.set(false); + } + Err(e) => { + error_signal.set(Some(format!("Failed to leave voice: {}", e))); + loading_signal.set(false); + } + } + }); + }); + + // Fetch list of guilds + let load_guilds_impl = Arc::new(move || { + spawn_local(async move { + error_signal.set(None); + loading_signal.set(true); + + match get_guilds().await { + Ok(guilds) => { + guilds_signal.set(guilds); + loading_signal.set(false); + } + Err(e) => { + error_signal.set(Some(format!("Failed to load guilds: {}", e))); + loading_signal.set(false); + } + } + }); + }); + + // Fetch voice channels for a guild + let load_voice_channels_impl = Arc::new(move |guild_id: String| { + spawn_local({ + let guild_id = guild_id.clone(); + async move { + error_signal.set(None); + loading_signal.set(true); + + match get_voice_channels(&guild_id).await { + Ok(channels) => { + voice_channels_signal.set(channels); + loading_signal.set(false); + } + Err(e) => { + error_signal.set(Some(format!("Failed to load voice channels: {}", e))); + loading_signal.set(false); + } + } + } + }); + }); + + // Fetch text channels for a guild + let load_text_channels_impl = Arc::new(move |guild_id: String| { + spawn_local({ + let guild_id = guild_id.clone(); + async move { + error_signal.set(None); + loading_signal.set(true); + + match get_text_channels(&guild_id).await { + Ok(channels) => { + text_channels_signal.set(channels); + loading_signal.set(false); + } + Err(e) => { + error_signal.set(Some(format!("Failed to load text channels: {}", e))); + loading_signal.set(false); + } + } + } + }); + }); + + VoiceControlState { + guilds: guilds_signal, + voice_channels: voice_channels_signal, + text_channels: text_channels_signal, + voice_status: voice_status_signal, + loading: loading_signal, + error: error_signal, + join_voice: join_voice_impl, + leave_voice: leave_voice_impl, + load_guilds: load_guilds_impl, + load_voice_channels: load_voice_channels_impl, + load_text_channels: load_text_channels_impl, + } +} diff --git a/services/frontend-leptos/frontend/src/features/live/mod.rs b/services/frontend-leptos/frontend/src/features/live/mod.rs new file mode 100644 index 0000000..7f745eb --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/live/mod.rs @@ -0,0 +1,21 @@ +pub mod components; +pub mod hooks; + +use leptos::prelude::*; +use crate::ui::card::Card; + +/// Placeholder LivePanel component for Phase 4 Task 1 +/// Will be expanded with voice connection, music player, and recordings components +#[component] +pub fn LivePanel() -> impl IntoView { + view! { + +
+

Live Monitoring

+

+ Live panel components coming in Phase 4 +

+
+
+ } +} diff --git a/services/frontend-leptos/frontend/src/features/mod.rs b/services/frontend-leptos/frontend/src/features/mod.rs index ba63992..e660cd8 100644 --- a/services/frontend-leptos/frontend/src/features/mod.rs +++ b/services/frontend-leptos/frontend/src/features/mod.rs @@ -1 +1,2 @@ +pub mod live; pub mod messages;