feat(leptos): Phase 4 Task 1 - voice/media control hooks
This commit is contained in:
@@ -0,0 +1 @@
|
||||
// Empty for now - components will be added in future phases
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod use_voice_control;
|
||||
pub mod use_media_control;
|
||||
@@ -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<dyn Fn(String, String) + Send + Sync>;
|
||||
/// Callback type for skip_track
|
||||
pub type SkipTrackCallback = Arc<dyn Fn() + Send + Sync>;
|
||||
/// Callback type for stop_playback
|
||||
pub type StopPlaybackCallback = Arc<dyn Fn() + Send + Sync>;
|
||||
/// Callback type for set_volume
|
||||
pub type SetVolumeCallback = Arc<dyn Fn(f64) + Send + Sync>;
|
||||
/// Callback type for refresh
|
||||
pub type RefreshCallback = Arc<dyn Fn() + Send + Sync>;
|
||||
|
||||
/// State returned by use_media_control hook
|
||||
#[derive(Clone)]
|
||||
pub struct MediaControlState {
|
||||
/// Current media playback state
|
||||
pub media_state: RwSignal<Option<MediaState>>,
|
||||
/// Whether we're currently loading data
|
||||
pub loading: RwSignal<bool>,
|
||||
/// Last error message if any
|
||||
pub error: RwSignal<Option<String>>,
|
||||
/// 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::<MediaState>);
|
||||
let loading_signal = RwSignal::new(false);
|
||||
let error_signal = RwSignal::new(None::<String>);
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
@@ -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<dyn Fn(String, String) + Send + Sync>;
|
||||
/// Callback type for leave_voice
|
||||
pub type LeaveVoiceCallback = Arc<dyn Fn() + Send + Sync>;
|
||||
/// Callback type for load_guilds
|
||||
pub type LoadGuildsCallback = Arc<dyn Fn() + Send + Sync>;
|
||||
/// Callback type for load_voice_channels
|
||||
pub type LoadVoiceChannelsCallback = Arc<dyn Fn(String) + Send + Sync>;
|
||||
/// Callback type for load_text_channels
|
||||
pub type LoadTextChannelsCallback = Arc<dyn Fn(String) + Send + Sync>;
|
||||
|
||||
/// State returned by use_voice_control hook
|
||||
#[derive(Clone)]
|
||||
pub struct VoiceControlState {
|
||||
/// List of available guilds
|
||||
pub guilds: RwSignal<Vec<Guild>>,
|
||||
/// List of voice channels for current guild
|
||||
pub voice_channels: RwSignal<Vec<Channel>>,
|
||||
/// List of text channels for current guild
|
||||
pub text_channels: RwSignal<Vec<Channel>>,
|
||||
/// Current voice connection status
|
||||
pub voice_status: RwSignal<Option<VoiceStatus>>,
|
||||
/// Whether we're currently loading data
|
||||
pub loading: RwSignal<bool>,
|
||||
/// Last error message if any
|
||||
pub error: RwSignal<Option<String>>,
|
||||
/// 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::<Guild>::new());
|
||||
let voice_channels_signal = RwSignal::new(Vec::<Channel>::new());
|
||||
let text_channels_signal = RwSignal::new(Vec::<Channel>::new());
|
||||
let voice_status_signal = RwSignal::new(None::<VoiceStatus>);
|
||||
let loading_signal = RwSignal::new(false);
|
||||
let error_signal = RwSignal::new(None::<String>);
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
@@ -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! {
|
||||
<Card class="p-6">
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-xl font-bold">Live Monitoring</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Live panel components coming in Phase 4
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod live;
|
||||
pub mod messages;
|
||||
|
||||
Reference in New Issue
Block a user