From d42737f856e6516c198aa7787cf9ea76fee71ee9 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 15 Nov 2024 18:35:06 -0500 Subject: [PATCH 1/2] Create GlobalState --- src/util/mod.rs | 2 ++ src/util/state.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 src/util/state.rs diff --git a/src/util/mod.rs b/src/util/mod.rs index cf7196e..d7ad020 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -5,3 +5,5 @@ cfg_if! { pub mod audio; } } + +pub mod state; diff --git a/src/util/state.rs b/src/util/state.rs new file mode 100644 index 0000000..b2fa59c --- /dev/null +++ b/src/util/state.rs @@ -0,0 +1,49 @@ +use leptos::*; +use leptos::logging::*; + +use crate::playstatus::PlayStatus; +use crate::models::User; +use crate::auth::get_logged_in_user; + +/// Global front-end state +/// Contains anything frequently needed across multiple components +/// Behaves like a singleton, in that provide/expect_context will +/// always return the same instance +#[derive(Clone)] +pub struct GlobalState { + /// A resource that fetches the logged in user + /// This will not automatically refetch, so any login/logout related code + /// should call `refetch` on this resource + pub logged_in_user: Resource<(), Option>, + + /// The current play status + pub play_status: RwSignal, +} + +impl GlobalState { + pub fn new() -> Self { + let play_status = create_rw_signal(PlayStatus::default()); + + let logged_in_user = create_resource(|| (), |_| async { + get_logged_in_user().await + .inspect_err(|e| { + error!("Error getting logged in user: {:?}", e); + }) + .ok() + .flatten() + }); + + Self { + logged_in_user, + play_status, + } + } + + pub fn logged_in_user() -> Resource<(), Option> { + expect_context::().logged_in_user + } + + pub fn play_status() -> RwSignal { + expect_context::().play_status + } +} From f0f34d4abe093dc800e3ddacde3326027ce3d13f Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 15 Nov 2024 18:49:19 -0500 Subject: [PATCH 2/2] Use GlobalState instead of passing play_status/logged_in_user everywhere --- src/app.rs | 41 ++++++------------ src/pages/login.rs | 6 ++- src/pages/profile.rs | 10 ++--- src/pages/signup.rs | 6 ++- src/playbar.rs | 100 +++++++++++++++++++++++-------------------- src/queue.rs | 11 ++--- 6 files changed, 85 insertions(+), 89 deletions(-) diff --git a/src/app.rs b/src/app.rs index 15d877f..2058312 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,40 +1,23 @@ use crate::playbar::PlayBar; use crate::playbar::CustomTitle; -use crate::playstatus::PlayStatus; use crate::queue::Queue; use leptos::*; -use leptos::logging::*; use leptos_meta::*; use leptos_router::*; use crate::pages::login::*; use crate::pages::signup::*; use crate::pages::profile::*; use crate::error_template::{AppError, ErrorTemplate}; -use crate::auth::get_logged_in_user; -use crate::models::User; - -pub type LoggedInUserResource = Resource<(), Option>; +use crate::util::state::GlobalState; #[component] pub fn App() -> impl IntoView { // Provides context that manages stylesheets, titles, meta tags, etc. provide_meta_context(); - let play_status = PlayStatus::default(); - let play_status = create_rw_signal(play_status); - let upload_open = create_rw_signal(false); + provide_context(GlobalState::new()); - // A resource that fetches the logged in user - // This will not automatically refetch, so any login/logout related code - // should call `refetch` on this resource - let logged_in_user: LoggedInUserResource = create_resource(|| (), |_| async { - get_logged_in_user().await - .inspect_err(|e| { - error!("Error getting logged in user: {:?}", e); - }) - .ok() - .flatten() - }); + let upload_open = create_rw_signal(false); view! { // injects a stylesheet into the document @@ -42,7 +25,7 @@ pub fn App() -> impl IntoView { // sets the document title - + // content for this welcome page impl IntoView { }>
- }> + }> - } /> - } /> + + - } /> - } /> + +
@@ -78,7 +61,7 @@ use crate::components::upload::*; /// Renders the home page of your application. #[component] -fn HomePage(play_status: RwSignal, upload_open: RwSignal) -> impl IntoView { +fn HomePage(upload_open: RwSignal) -> impl IntoView { view! {
@@ -86,8 +69,8 @@ fn HomePage(play_status: RwSignal, upload_open: RwSignal) -> i // This will render the child route components - - + +
} } diff --git a/src/pages/login.rs b/src/pages/login.rs index 585f2f0..7b9bffa 100644 --- a/src/pages/login.rs +++ b/src/pages/login.rs @@ -1,12 +1,12 @@ use crate::auth::login; +use crate::util::state::GlobalState; use leptos::leptos_dom::*; use leptos::*; use leptos_icons::*; use crate::users::UserCredentials; -use crate::app::LoggedInUserResource; #[component] -pub fn Login(user: LoggedInUserResource) -> impl IntoView { +pub fn Login() -> impl IntoView { let (username_or_email, set_username_or_email) = create_signal("".to_string()); let (password, set_password) = create_signal("".to_string()); @@ -28,6 +28,8 @@ pub fn Login(user: LoggedInUserResource) -> impl IntoView { username_or_email: username_or_email1, password: password1 }; + + let user = GlobalState::logged_in_user(); let login_result = login(user_credentials).await; if let Err(err) = login_result { diff --git a/src/pages/profile.rs b/src/pages/profile.rs index 744229f..2367470 100644 --- a/src/pages/profile.rs +++ b/src/pages/profile.rs @@ -11,9 +11,9 @@ use crate::components::error::*; use crate::api::profile::*; -use crate::app::LoggedInUserResource; use crate::models::User; use crate::users::get_user_by_id; +use crate::util::state::GlobalState; /// Duration in seconds backwards from now to aggregate history data for const HISTORY_SECS: i64 = 60 * 60 * 24 * 30; @@ -29,7 +29,7 @@ const TOP_ARTISTS_COUNT: i64 = 10; /// Profile page /// Shows the current user's profile if no id is specified, or a user's profile if an id is specified in the path #[component] -pub fn Profile(logged_in_user: LoggedInUserResource) -> impl IntoView { +pub fn Profile() -> impl IntoView { let params = use_params_map(); view! { @@ -38,7 +38,7 @@ pub fn Profile(logged_in_user: LoggedInUserResource) -> impl IntoView { match params.get("id").map(|id| id.parse::()) { None => { // No id specified, show the current user's profile - view! { }.into_view() + view! { }.into_view() }, Some(Ok(id)) => { // Id specified, get the user and show their profile @@ -61,12 +61,12 @@ pub fn Profile(logged_in_user: LoggedInUserResource) -> impl IntoView { /// Show the logged in user's profile #[component] -fn OwnProfile(logged_in_user: LoggedInUserResource) -> impl IntoView { +fn OwnProfile() -> impl IntoView { view! { } > - {move || logged_in_user.get().map(|user| { + {move || GlobalState::logged_in_user().get().map(|user| { match user { Some(user) => { let user_id = user.id.unwrap(); diff --git a/src/pages/signup.rs b/src/pages/signup.rs index 8e9a0ac..69fe77d 100644 --- a/src/pages/signup.rs +++ b/src/pages/signup.rs @@ -1,12 +1,12 @@ use crate::auth::signup; use crate::models::User; +use crate::util::state::GlobalState; use leptos::leptos_dom::*; use leptos::*; use leptos_icons::*; -use crate::app::LoggedInUserResource; #[component] -pub fn Signup(user: LoggedInUserResource) -> impl IntoView { +pub fn Signup() -> impl IntoView { let (username, set_username) = create_signal("".to_string()); let (email, set_email) = create_signal("".to_string()); let (password, set_password) = create_signal("".to_string()); @@ -30,6 +30,8 @@ pub fn Signup(user: LoggedInUserResource) -> impl IntoView { }; log!("new user: {:?}", new_user); + let user = GlobalState::logged_in_user(); + spawn_local(async move { if let Err(err) = signup(new_user.clone()).await { // Handle the error here, e.g., log it or display to the user diff --git a/src/playbar.rs b/src/playbar.rs index d113b7a..e581101 100644 --- a/src/playbar.rs +++ b/src/playbar.rs @@ -1,7 +1,7 @@ use crate::models::Artist; -use crate::playstatus::PlayStatus; 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::*; @@ -40,8 +40,8 @@ const HISTORY_LISTEN_THRESHOLD: u64 = MIN_SKIP_BACK_TIME as u64; /// * `None` if the audio element is not available /// * `Some((current_time, duration))` if the audio element is available /// -pub fn get_song_time_duration(status: impl SignalWithUntracked) -> Option<(f64, f64)> { - status.with_untracked(|status| { +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 { @@ -61,13 +61,13 @@ pub fn get_song_time_duration(status: impl SignalWithUntracked, time: f64) { +pub fn skip_to(time: f64) { if time.is_infinite() || time.is_nan() { error!("Unable to skip to non-finite time: {}", time); return } - status.update(|status| { + GlobalState::play_status().update(|status| { if let Some(audio) = status.get_audio() { audio.set_current_time(time); log!("Player skipped to time: {}", time); @@ -85,8 +85,8 @@ pub fn skip_to(status: impl SignalUpdate, time: f64) { /// * `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(status: impl SignalUpdate, play: bool) { - status.update(|status| { +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() { @@ -109,8 +109,8 @@ pub fn set_playing(status: impl SignalUpdate, play: bool) { }); } -fn toggle_queue(status: impl SignalUpdate) { - status.update(|status| { +fn toggle_queue() { + GlobalState::play_status().update(|status| { status.queue_open = !status.queue_open; }); @@ -126,8 +126,8 @@ fn toggle_queue(status: impl SignalUpdate) { /// * `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(status: impl SignalUpdate, src: String) { - status.update(|status| { +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); @@ -139,11 +139,13 @@ fn set_play_src(status: impl SignalUpdate, src: String) { /// The play, pause, and skip buttons #[component] -fn PlayControls(status: RwSignal) -> impl IntoView { +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(status) { + 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 @@ -160,8 +162,8 @@ fn PlayControls(status: RwSignal) -> impl IntoView { // 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(status, next_src); - set_playing(status, true); + set_play_src(next_src); + set_playing(true); } else { warn!("Unable to skip back: No previous song"); } @@ -170,14 +172,14 @@ fn PlayControls(status: RwSignal) -> impl IntoView { // Default to skipping to start of current song, and playing log!("Skipping to start of current song"); - skip_to(status, 0.0); - set_playing(status, true); + skip_to(0.0); + set_playing(true); }; let skip_forward = move |_| { - if let Some(duration) = get_song_time_duration(status) { - skip_to(status, duration.1); - set_playing(status, true); + 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"); } @@ -185,7 +187,7 @@ fn PlayControls(status: RwSignal) -> impl IntoView { let toggle_play = move |_| { let playing = status.with_untracked(|status| { status.playing }); - set_playing(status, !playing); + set_playing(!playing); }; // We use this to prevent the buttons from being focused when clicked @@ -248,7 +250,9 @@ fn PlayDuration(elapsed_secs: MaybeSignal, total_secs: MaybeSignal) -> /// The name, artist, and album of the current song #[component] -fn MediaInfo(status: RwSignal) -> impl IntoView { +fn MediaInfo() -> impl IntoView { + let status = GlobalState::play_status(); + let name = Signal::derive(move || { status.with(|status| { status.queue.front().map_or("No media playing".into(), |song| song.title.clone()) @@ -287,7 +291,9 @@ fn MediaInfo(status: RwSignal) -> impl IntoView { /// The like and dislike buttons #[component] -fn LikeDislike(status: RwSignal) -> impl IntoView { +fn LikeDislike() -> impl IntoView { + let status = GlobalState::play_status(); + let like_icon = Signal::derive(move || { status.with(|status| { match status.queue.front() { @@ -400,7 +406,7 @@ fn LikeDislike(status: RwSignal) -> impl IntoView { /// The play progress bar, and click handler for skipping to a certain time in the song #[component] -fn ProgressBar(percentage: MaybeSignal, status: RwSignal) -> impl IntoView { +fn ProgressBar(percentage: MaybeSignal) -> impl IntoView { // Keep a reference to the progress bar div so we can get its width and calculate the time to skip to let progress_bar_ref = create_node_ref::
(); @@ -412,10 +418,10 @@ fn ProgressBar(percentage: MaybeSignal, status: RwSignal) -> im let width = progress_bar.offset_width() as f64; let percentage = x_click_pos / width * 100.0; - if let Some(duration) = get_song_time_duration(status) { + if let Some(duration) = get_song_time_duration() { let time = duration.1 * percentage / 100.0; - skip_to(status, time); - set_playing(status, true); + skip_to(time); + set_playing(true); } else { error!("Unable to skip to time: Unable to get current duration"); } @@ -438,11 +444,11 @@ fn ProgressBar(percentage: MaybeSignal, status: RwSignal) -> im } #[component] -fn QueueToggle(status: RwSignal) -> impl IntoView { - +fn QueueToggle() -> impl IntoView { let update_queue = move |_| { - toggle_queue(status); - log!("queue button pressed, queue status: {:?}", status.with_untracked(|status| status.queue_open)); + toggle_queue(); + log!("queue button pressed, queue status: {:?}", + GlobalState::play_status().with_untracked(|status| status.queue_open)); }; // We use this to prevent the buttons from being focused when clicked @@ -463,9 +469,9 @@ fn QueueToggle(status: RwSignal) -> impl IntoView { /// Renders the title of the page based on the currently playing song #[component] -pub fn CustomTitle(play_status: RwSignal) -> impl IntoView { +pub fn CustomTitle() -> impl IntoView { let title = create_memo(move |_| { - play_status.with(|play_status| { + GlobalState::play_status().with(|play_status| { play_status.queue.front().map_or("LibreTunes".to_string(), |song_data| { format!("{} - {} | {}",song_data.title.clone(),Artist::display_list(&song_data.artists), "LibreTunes") }) @@ -478,18 +484,20 @@ pub fn CustomTitle(play_status: RwSignal) -> impl IntoView { /// The main play bar component, containing the progress bar, media info, play controls, and play duration #[component] -pub fn PlayBar(status: RwSignal) -> impl IntoView { +pub fn PlayBar() -> impl IntoView { + let status = GlobalState::play_status(); + // Listen for key down events -- arrow keys don't seem to trigger key press events let _arrow_key_handle = window_event_listener(ev::keydown, move |e: ev::KeyboardEvent| { if e.key() == "ArrowRight" { e.prevent_default(); log!("Right arrow key pressed, skipping forward by {} seconds", ARROW_KEY_SKIP_TIME); - if let Some(duration) = get_song_time_duration(status) { + if let Some(duration) = get_song_time_duration() { let mut time = duration.0 + ARROW_KEY_SKIP_TIME; time = time.clamp(0.0, duration.1); - skip_to(status, time); - set_playing(status, true); + skip_to(time); + set_playing(true); } else { error!("Unable to skip forward: Unable to get current duration"); } @@ -498,11 +506,11 @@ pub fn PlayBar(status: RwSignal) -> impl IntoView { e.prevent_default(); log!("Left arrow key pressed, skipping backward by {} seconds", ARROW_KEY_SKIP_TIME); - if let Some(duration) = get_song_time_duration(status) { + if let Some(duration) = get_song_time_duration() { let mut time = duration.0 - ARROW_KEY_SKIP_TIME; time = time.clamp(0.0, duration.1); - skip_to(status, time); - set_playing(status, true); + skip_to(time); + set_playing(true); } else { error!("Unable to skip backward: Unable to get current duration"); } @@ -516,7 +524,7 @@ pub fn PlayBar(status: RwSignal) -> impl IntoView { log!("Space bar pressed, toggling play/pause"); let playing = status.with_untracked(|status| status.playing); - set_playing(status, !playing); + set_playing(!playing); } }); @@ -659,14 +667,14 @@ pub fn PlayBar(status: RwSignal) -> impl IntoView {