use crate::models::Artist; use crate::songdata::SongData; use crate::api::songs; use crate::util::state::GlobalState; use leptos::ev::MouseEvent; use leptos::html::{Audio, Div}; use leptos::leptos_dom::*; use leptos_meta::Title; use leptos::*; use leptos_icons::*; use leptos_use::{utils::Pausable, use_interval_fn}; /// Width and height of the forward/backward skip buttons const SKIP_BTN_SIZE: &str = "3.5em"; /// Width and height of the play/pause button const PLAY_BTN_SIZE: &str = "5em"; // Width and height of the queue button const QUEUE_BTN_SIZE: &str = "3.5em"; /// Threshold in seconds for skipping to the previous song instead of skipping to the start of the current song const MIN_SKIP_BACK_TIME: f64 = 5.0; /// How many seconds to skip forward/backward when the user presses the arrow keys const ARROW_KEY_SKIP_TIME: f64 = 5.0; /// Threshold in seconds for considering when the user has listened to a song, for adding it to the history const HISTORY_LISTEN_THRESHOLD: u64 = MIN_SKIP_BACK_TIME as u64; // TODO Handle errors better, when getting audio HTML element and when playing/pausing audio /// Get the current time and duration of the current song, if available /// /// # Arguments /// /// * `status` - The `PlayStatus` to get the audio element from, as a signal /// /// # Returns /// /// * `None` if the audio element is not available /// * `Some((current_time, duration))` if the audio element is available /// pub fn get_song_time_duration() -> Option<(f64, f64)> { GlobalState::play_status().with_untracked(|status| { if let Some(audio) = status.get_audio() { Some((audio.current_time(), audio.duration())) } else { error!("Unable to get current duration: Audio element not available"); None } }) } /// Skip to a certain time in the current song, optionally playing it /// /// If the given time is +/- infinity or NaN, logs an error and returns /// Logs an error if the audio element is not available, or if playing the song fails /// /// # Arguments /// /// * `status` - The `PlayStatus` to get the audio element from, as a signal /// * `time` - The time to skip to, in seconds /// pub fn skip_to(time: f64) { if time.is_infinite() || time.is_nan() { error!("Unable to skip to non-finite time: {}", time); return } GlobalState::play_status().update(|status| { if let Some(audio) = status.get_audio() { audio.set_current_time(time); log!("Player skipped to time: {}", time); } else { error!("Unable to skip to time: Audio element not available"); } }); } /// Play or pause the current song /// /// Logs an error if the audio element is not available, or if playing/pausing the song fails /// /// # Arguments /// * `status` - The `PlayStatus` to get the audio element from, as a signal /// * `play` - `true` to play the song, `false` to pause it /// pub fn set_playing(play: bool) { GlobalState::play_status().update(|status| { if let Some(audio) = status.get_audio() { if play { if let Err(e) = audio.play() { error!("Unable to play audio: {:?}", e); } else { status.playing = true; log!("Successfully played audio"); } } else { if let Err(e) = audio.pause() { error!("Unable to pause audio: {:?}", e); } else { status.playing = false; log!("Successfully paused audio"); } } } else { error!("Unable to play/pause audio: Audio element not available"); } }); } fn toggle_queue() { GlobalState::play_status().update(|status| { status.queue_open = !status.queue_open; }); } /// Set the source of the audio player /// /// Logs an error if the audio element is not available /// /// /// # Arguments /// * `status` - The `PlayStatus` to get the audio element from, as a signal /// * `src` - The source to set the audio player to /// fn set_play_src(src: String) { GlobalState::play_status().update(|status| { if let Some(audio) = status.get_audio() { audio.set_src(&src); log!("Player set src to: {}", src); } else { error!("Unable to set src: Audio element not available"); } }); } /// The play, pause, and skip buttons #[component] fn PlayControls() -> impl IntoView { let status = GlobalState::play_status(); // On click handlers for the skip and play/pause buttons let skip_back = move |_| { if let Some(duration) = get_song_time_duration() { // Skip to previous song if the current song is near the start // Also skip to the previous song if we're at the end of the current song // This is because after running out of songs in the queue, the current song will be at the end // but our queue will be empty. We *do* want to "skip the start of the current song", // but first we need to get the *previous* song from the history since that's what we were playing before if duration.0 < MIN_SKIP_BACK_TIME || duration.0 >= duration.1 { log!("Skipping to the previous song"); // Pop the most recently played song from the history if possible let mut last_played_song = None; status.update(|status| last_played_song = status.history.pop_back()); if let Some(last_played_song) = last_played_song { // Push the popped song to the front of the queue, and play it let next_src = last_played_song.song_path.clone(); status.update(|status| status.queue.push_front(last_played_song)); set_play_src(next_src); set_playing(true); } else { warn!("Unable to skip back: No previous song"); } } } // Default to skipping to start of current song, and playing log!("Skipping to start of current song"); skip_to(0.0); set_playing(true); }; let skip_forward = move |_| { if let Some(duration) = get_song_time_duration() { skip_to(duration.1); set_playing(true); } else { error!("Unable to skip forward: Unable to get current duration"); } }; let toggle_play = move |_| { let playing = status.with_untracked(|status| { status.playing }); set_playing(!playing); }; // We use this to prevent the buttons from being focused when clicked // If buttons were focused on clicks, then pressing space bar to play/pause would "click" the button // and trigger unwanted behavior let prevent_focus = move |e: MouseEvent| { e.prevent_default(); }; // Change the icon based on whether the song is playing or not let icon = Signal::derive(move || { status.with(|status| { if status.playing { icondata::BsPauseFill } else { icondata::BsPlayFill } }) }); view! {