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;
|
pub mod messages;
|
||||||
|
|||||||
Reference in New Issue
Block a user