Merge pull request 'Use provide_context for global state' (#143) from 142-use-providecontext-for-global-state into main
Reviewed-on: LibreTunes/LibreTunes#143
This commit is contained in:
commit
8f01ff24d8
41
src/app.rs
41
src/app.rs
@ -1,40 +1,23 @@
|
|||||||
use crate::playbar::PlayBar;
|
use crate::playbar::PlayBar;
|
||||||
use crate::playbar::CustomTitle;
|
use crate::playbar::CustomTitle;
|
||||||
use crate::playstatus::PlayStatus;
|
|
||||||
use crate::queue::Queue;
|
use crate::queue::Queue;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos::logging::*;
|
|
||||||
use leptos_meta::*;
|
use leptos_meta::*;
|
||||||
use leptos_router::*;
|
use leptos_router::*;
|
||||||
use crate::pages::login::*;
|
use crate::pages::login::*;
|
||||||
use crate::pages::signup::*;
|
use crate::pages::signup::*;
|
||||||
use crate::pages::profile::*;
|
use crate::pages::profile::*;
|
||||||
use crate::error_template::{AppError, ErrorTemplate};
|
use crate::error_template::{AppError, ErrorTemplate};
|
||||||
use crate::auth::get_logged_in_user;
|
use crate::util::state::GlobalState;
|
||||||
use crate::models::User;
|
|
||||||
|
|
||||||
pub type LoggedInUserResource = Resource<(), Option<User>>;
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||||
provide_meta_context();
|
provide_meta_context();
|
||||||
|
|
||||||
let play_status = PlayStatus::default();
|
provide_context(GlobalState::new());
|
||||||
let play_status = create_rw_signal(play_status);
|
|
||||||
let upload_open = create_rw_signal(false);
|
|
||||||
|
|
||||||
// A resource that fetches the logged in user
|
let upload_open = create_rw_signal(false);
|
||||||
// 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()
|
|
||||||
});
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
// injects a stylesheet into the document <head>
|
// injects a stylesheet into the document <head>
|
||||||
@ -42,7 +25,7 @@ pub fn App() -> impl IntoView {
|
|||||||
<Stylesheet id="leptos" href="/pkg/libretunes.css"/>
|
<Stylesheet id="leptos" href="/pkg/libretunes.css"/>
|
||||||
|
|
||||||
// sets the document title
|
// sets the document title
|
||||||
<CustomTitle play_status=play_status/>
|
<CustomTitle />
|
||||||
|
|
||||||
// content for this welcome page
|
// content for this welcome page
|
||||||
<Router fallback=|| {
|
<Router fallback=|| {
|
||||||
@ -55,15 +38,15 @@ pub fn App() -> impl IntoView {
|
|||||||
}>
|
}>
|
||||||
<main>
|
<main>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="" view=move || view! { <HomePage play_status=play_status upload_open=upload_open/> }>
|
<Route path="" view=move || view! { <HomePage upload_open=upload_open/> }>
|
||||||
<Route path="" view=Dashboard />
|
<Route path="" view=Dashboard />
|
||||||
<Route path="dashboard" view=Dashboard />
|
<Route path="dashboard" view=Dashboard />
|
||||||
<Route path="search" view=Search />
|
<Route path="search" view=Search />
|
||||||
<Route path="user/:id" view=move || view!{ <Profile logged_in_user /> } />
|
<Route path="user/:id" view=Profile />
|
||||||
<Route path="user" view=move || view!{ <Profile logged_in_user /> } />
|
<Route path="user" view=Profile />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/login" view=move || view!{ <Login user=logged_in_user /> } />
|
<Route path="/login" view=Login />
|
||||||
<Route path="/signup" view=move || view!{ <Signup user=logged_in_user /> } />
|
<Route path="/signup" view=Signup />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</Router>
|
</Router>
|
||||||
@ -78,7 +61,7 @@ use crate::components::upload::*;
|
|||||||
|
|
||||||
/// Renders the home page of your application.
|
/// Renders the home page of your application.
|
||||||
#[component]
|
#[component]
|
||||||
fn HomePage(play_status: RwSignal<PlayStatus>, upload_open: RwSignal<bool>) -> impl IntoView {
|
fn HomePage(upload_open: RwSignal<bool>) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<div class="home-container">
|
<div class="home-container">
|
||||||
<Upload open=upload_open/>
|
<Upload open=upload_open/>
|
||||||
@ -86,8 +69,8 @@ fn HomePage(play_status: RwSignal<PlayStatus>, upload_open: RwSignal<bool>) -> i
|
|||||||
// This <Outlet /> will render the child route components
|
// This <Outlet /> will render the child route components
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<Personal />
|
<Personal />
|
||||||
<PlayBar status=play_status/>
|
<PlayBar />
|
||||||
<Queue status=play_status/>
|
<Queue />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
use crate::auth::login;
|
use crate::auth::login;
|
||||||
|
use crate::util::state::GlobalState;
|
||||||
use leptos::leptos_dom::*;
|
use leptos::leptos_dom::*;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_icons::*;
|
use leptos_icons::*;
|
||||||
use crate::users::UserCredentials;
|
use crate::users::UserCredentials;
|
||||||
use crate::app::LoggedInUserResource;
|
|
||||||
|
|
||||||
#[component]
|
#[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 (username_or_email, set_username_or_email) = create_signal("".to_string());
|
||||||
let (password, set_password) = create_signal("".to_string());
|
let (password, set_password) = create_signal("".to_string());
|
||||||
|
|
||||||
@ -29,6 +29,8 @@ pub fn Login(user: LoggedInUserResource) -> impl IntoView {
|
|||||||
password: password1
|
password: password1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let user = GlobalState::logged_in_user();
|
||||||
|
|
||||||
let login_result = login(user_credentials).await;
|
let login_result = login(user_credentials).await;
|
||||||
if let Err(err) = login_result {
|
if let Err(err) = login_result {
|
||||||
// Handle the error here, e.g., log it or display to the user
|
// Handle the error here, e.g., log it or display to the user
|
||||||
|
@ -11,9 +11,9 @@ use crate::components::error::*;
|
|||||||
|
|
||||||
use crate::api::profile::*;
|
use crate::api::profile::*;
|
||||||
|
|
||||||
use crate::app::LoggedInUserResource;
|
|
||||||
use crate::models::User;
|
use crate::models::User;
|
||||||
use crate::users::get_user_by_id;
|
use crate::users::get_user_by_id;
|
||||||
|
use crate::util::state::GlobalState;
|
||||||
|
|
||||||
/// Duration in seconds backwards from now to aggregate history data for
|
/// Duration in seconds backwards from now to aggregate history data for
|
||||||
const HISTORY_SECS: i64 = 60 * 60 * 24 * 30;
|
const HISTORY_SECS: i64 = 60 * 60 * 24 * 30;
|
||||||
@ -29,7 +29,7 @@ const TOP_ARTISTS_COUNT: i64 = 10;
|
|||||||
/// Profile page
|
/// 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
|
/// 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]
|
#[component]
|
||||||
pub fn Profile(logged_in_user: LoggedInUserResource) -> impl IntoView {
|
pub fn Profile() -> impl IntoView {
|
||||||
let params = use_params_map();
|
let params = use_params_map();
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
@ -38,7 +38,7 @@ pub fn Profile(logged_in_user: LoggedInUserResource) -> impl IntoView {
|
|||||||
match params.get("id").map(|id| id.parse::<i32>()) {
|
match params.get("id").map(|id| id.parse::<i32>()) {
|
||||||
None => {
|
None => {
|
||||||
// No id specified, show the current user's profile
|
// No id specified, show the current user's profile
|
||||||
view! { <OwnProfile logged_in_user /> }.into_view()
|
view! { <OwnProfile /> }.into_view()
|
||||||
},
|
},
|
||||||
Some(Ok(id)) => {
|
Some(Ok(id)) => {
|
||||||
// Id specified, get the user and show their profile
|
// 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
|
/// Show the logged in user's profile
|
||||||
#[component]
|
#[component]
|
||||||
fn OwnProfile(logged_in_user: LoggedInUserResource) -> impl IntoView {
|
fn OwnProfile() -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<Transition
|
<Transition
|
||||||
fallback=move || view! { <LoadingPage /> }
|
fallback=move || view! { <LoadingPage /> }
|
||||||
>
|
>
|
||||||
{move || logged_in_user.get().map(|user| {
|
{move || GlobalState::logged_in_user().get().map(|user| {
|
||||||
match user {
|
match user {
|
||||||
Some(user) => {
|
Some(user) => {
|
||||||
let user_id = user.id.unwrap();
|
let user_id = user.id.unwrap();
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
use crate::auth::signup;
|
use crate::auth::signup;
|
||||||
use crate::models::User;
|
use crate::models::User;
|
||||||
|
use crate::util::state::GlobalState;
|
||||||
use leptos::leptos_dom::*;
|
use leptos::leptos_dom::*;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_icons::*;
|
use leptos_icons::*;
|
||||||
use crate::app::LoggedInUserResource;
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Signup(user: LoggedInUserResource) -> impl IntoView {
|
pub fn Signup() -> impl IntoView {
|
||||||
let (username, set_username) = create_signal("".to_string());
|
let (username, set_username) = create_signal("".to_string());
|
||||||
let (email, set_email) = create_signal("".to_string());
|
let (email, set_email) = create_signal("".to_string());
|
||||||
let (password, set_password) = 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);
|
log!("new user: {:?}", new_user);
|
||||||
|
|
||||||
|
let user = GlobalState::logged_in_user();
|
||||||
|
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
if let Err(err) = signup(new_user.clone()).await {
|
if let Err(err) = signup(new_user.clone()).await {
|
||||||
// Handle the error here, e.g., log it or display to the user
|
// Handle the error here, e.g., log it or display to the user
|
||||||
|
100
src/playbar.rs
100
src/playbar.rs
@ -1,7 +1,7 @@
|
|||||||
use crate::models::Artist;
|
use crate::models::Artist;
|
||||||
use crate::playstatus::PlayStatus;
|
|
||||||
use crate::songdata::SongData;
|
use crate::songdata::SongData;
|
||||||
use crate::api::songs;
|
use crate::api::songs;
|
||||||
|
use crate::util::state::GlobalState;
|
||||||
use leptos::ev::MouseEvent;
|
use leptos::ev::MouseEvent;
|
||||||
use leptos::html::{Audio, Div};
|
use leptos::html::{Audio, Div};
|
||||||
use leptos::leptos_dom::*;
|
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
|
/// * `None` if the audio element is not available
|
||||||
/// * `Some((current_time, duration))` if the audio element is available
|
/// * `Some((current_time, duration))` if the audio element is available
|
||||||
///
|
///
|
||||||
pub fn get_song_time_duration(status: impl SignalWithUntracked<Value = PlayStatus>) -> Option<(f64, f64)> {
|
pub fn get_song_time_duration() -> Option<(f64, f64)> {
|
||||||
status.with_untracked(|status| {
|
GlobalState::play_status().with_untracked(|status| {
|
||||||
if let Some(audio) = status.get_audio() {
|
if let Some(audio) = status.get_audio() {
|
||||||
Some((audio.current_time(), audio.duration()))
|
Some((audio.current_time(), audio.duration()))
|
||||||
} else {
|
} else {
|
||||||
@ -61,13 +61,13 @@ pub fn get_song_time_duration(status: impl SignalWithUntracked<Value = PlayStatu
|
|||||||
/// * `status` - The `PlayStatus` to get the audio element from, as a signal
|
/// * `status` - The `PlayStatus` to get the audio element from, as a signal
|
||||||
/// * `time` - The time to skip to, in seconds
|
/// * `time` - The time to skip to, in seconds
|
||||||
///
|
///
|
||||||
pub fn skip_to(status: impl SignalUpdate<Value = PlayStatus>, time: f64) {
|
pub fn skip_to(time: f64) {
|
||||||
if time.is_infinite() || time.is_nan() {
|
if time.is_infinite() || time.is_nan() {
|
||||||
error!("Unable to skip to non-finite time: {}", time);
|
error!("Unable to skip to non-finite time: {}", time);
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
status.update(|status| {
|
GlobalState::play_status().update(|status| {
|
||||||
if let Some(audio) = status.get_audio() {
|
if let Some(audio) = status.get_audio() {
|
||||||
audio.set_current_time(time);
|
audio.set_current_time(time);
|
||||||
log!("Player skipped to time: {}", time);
|
log!("Player skipped to time: {}", time);
|
||||||
@ -85,8 +85,8 @@ pub fn skip_to(status: impl SignalUpdate<Value = PlayStatus>, time: f64) {
|
|||||||
/// * `status` - The `PlayStatus` to get the audio element from, as a signal
|
/// * `status` - The `PlayStatus` to get the audio element from, as a signal
|
||||||
/// * `play` - `true` to play the song, `false` to pause it
|
/// * `play` - `true` to play the song, `false` to pause it
|
||||||
///
|
///
|
||||||
pub fn set_playing(status: impl SignalUpdate<Value = PlayStatus>, play: bool) {
|
pub fn set_playing(play: bool) {
|
||||||
status.update(|status| {
|
GlobalState::play_status().update(|status| {
|
||||||
if let Some(audio) = status.get_audio() {
|
if let Some(audio) = status.get_audio() {
|
||||||
if play {
|
if play {
|
||||||
if let Err(e) = audio.play() {
|
if let Err(e) = audio.play() {
|
||||||
@ -109,8 +109,8 @@ pub fn set_playing(status: impl SignalUpdate<Value = PlayStatus>, play: bool) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_queue(status: impl SignalUpdate<Value = PlayStatus>) {
|
fn toggle_queue() {
|
||||||
status.update(|status| {
|
GlobalState::play_status().update(|status| {
|
||||||
status.queue_open = !status.queue_open;
|
status.queue_open = !status.queue_open;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -126,8 +126,8 @@ fn toggle_queue(status: impl SignalUpdate<Value = PlayStatus>) {
|
|||||||
/// * `status` - The `PlayStatus` to get the audio element from, as a signal
|
/// * `status` - The `PlayStatus` to get the audio element from, as a signal
|
||||||
/// * `src` - The source to set the audio player to
|
/// * `src` - The source to set the audio player to
|
||||||
///
|
///
|
||||||
fn set_play_src(status: impl SignalUpdate<Value = PlayStatus>, src: String) {
|
fn set_play_src(src: String) {
|
||||||
status.update(|status| {
|
GlobalState::play_status().update(|status| {
|
||||||
if let Some(audio) = status.get_audio() {
|
if let Some(audio) = status.get_audio() {
|
||||||
audio.set_src(&src);
|
audio.set_src(&src);
|
||||||
log!("Player set src to: {}", src);
|
log!("Player set src to: {}", src);
|
||||||
@ -139,11 +139,13 @@ fn set_play_src(status: impl SignalUpdate<Value = PlayStatus>, src: String) {
|
|||||||
|
|
||||||
/// The play, pause, and skip buttons
|
/// The play, pause, and skip buttons
|
||||||
#[component]
|
#[component]
|
||||||
fn PlayControls(status: RwSignal<PlayStatus>) -> impl IntoView {
|
fn PlayControls() -> impl IntoView {
|
||||||
|
let status = GlobalState::play_status();
|
||||||
|
|
||||||
// On click handlers for the skip and play/pause buttons
|
// On click handlers for the skip and play/pause buttons
|
||||||
|
|
||||||
let skip_back = move |_| {
|
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
|
// 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
|
// 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
|
// 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<PlayStatus>) -> impl IntoView {
|
|||||||
// Push the popped song to the front of the queue, and play it
|
// Push the popped song to the front of the queue, and play it
|
||||||
let next_src = last_played_song.song_path.clone();
|
let next_src = last_played_song.song_path.clone();
|
||||||
status.update(|status| status.queue.push_front(last_played_song));
|
status.update(|status| status.queue.push_front(last_played_song));
|
||||||
set_play_src(status, next_src);
|
set_play_src(next_src);
|
||||||
set_playing(status, true);
|
set_playing(true);
|
||||||
} else {
|
} else {
|
||||||
warn!("Unable to skip back: No previous song");
|
warn!("Unable to skip back: No previous song");
|
||||||
}
|
}
|
||||||
@ -170,14 +172,14 @@ fn PlayControls(status: RwSignal<PlayStatus>) -> impl IntoView {
|
|||||||
|
|
||||||
// Default to skipping to start of current song, and playing
|
// Default to skipping to start of current song, and playing
|
||||||
log!("Skipping to start of current song");
|
log!("Skipping to start of current song");
|
||||||
skip_to(status, 0.0);
|
skip_to(0.0);
|
||||||
set_playing(status, true);
|
set_playing(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
let skip_forward = move |_| {
|
let skip_forward = move |_| {
|
||||||
if let Some(duration) = get_song_time_duration(status) {
|
if let Some(duration) = get_song_time_duration() {
|
||||||
skip_to(status, duration.1);
|
skip_to(duration.1);
|
||||||
set_playing(status, true);
|
set_playing(true);
|
||||||
} else {
|
} else {
|
||||||
error!("Unable to skip forward: Unable to get current duration");
|
error!("Unable to skip forward: Unable to get current duration");
|
||||||
}
|
}
|
||||||
@ -185,7 +187,7 @@ fn PlayControls(status: RwSignal<PlayStatus>) -> impl IntoView {
|
|||||||
|
|
||||||
let toggle_play = move |_| {
|
let toggle_play = move |_| {
|
||||||
let playing = status.with_untracked(|status| { status.playing });
|
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
|
// We use this to prevent the buttons from being focused when clicked
|
||||||
@ -248,7 +250,9 @@ fn PlayDuration(elapsed_secs: MaybeSignal<i64>, total_secs: MaybeSignal<i64>) ->
|
|||||||
|
|
||||||
/// The name, artist, and album of the current song
|
/// The name, artist, and album of the current song
|
||||||
#[component]
|
#[component]
|
||||||
fn MediaInfo(status: RwSignal<PlayStatus>) -> impl IntoView {
|
fn MediaInfo() -> impl IntoView {
|
||||||
|
let status = GlobalState::play_status();
|
||||||
|
|
||||||
let name = Signal::derive(move || {
|
let name = Signal::derive(move || {
|
||||||
status.with(|status| {
|
status.with(|status| {
|
||||||
status.queue.front().map_or("No media playing".into(), |song| song.title.clone())
|
status.queue.front().map_or("No media playing".into(), |song| song.title.clone())
|
||||||
@ -287,7 +291,9 @@ fn MediaInfo(status: RwSignal<PlayStatus>) -> impl IntoView {
|
|||||||
|
|
||||||
/// The like and dislike buttons
|
/// The like and dislike buttons
|
||||||
#[component]
|
#[component]
|
||||||
fn LikeDislike(status: RwSignal<PlayStatus>) -> impl IntoView {
|
fn LikeDislike() -> impl IntoView {
|
||||||
|
let status = GlobalState::play_status();
|
||||||
|
|
||||||
let like_icon = Signal::derive(move || {
|
let like_icon = Signal::derive(move || {
|
||||||
status.with(|status| {
|
status.with(|status| {
|
||||||
match status.queue.front() {
|
match status.queue.front() {
|
||||||
@ -400,7 +406,7 @@ fn LikeDislike(status: RwSignal<PlayStatus>) -> impl IntoView {
|
|||||||
|
|
||||||
/// The play progress bar, and click handler for skipping to a certain time in the song
|
/// The play progress bar, and click handler for skipping to a certain time in the song
|
||||||
#[component]
|
#[component]
|
||||||
fn ProgressBar(percentage: MaybeSignal<f64>, status: RwSignal<PlayStatus>) -> impl IntoView {
|
fn ProgressBar(percentage: MaybeSignal<f64>) -> impl IntoView {
|
||||||
// Keep a reference to the progress bar div so we can get its width and calculate the time to skip to
|
// 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::<Div>();
|
let progress_bar_ref = create_node_ref::<Div>();
|
||||||
|
|
||||||
@ -412,10 +418,10 @@ fn ProgressBar(percentage: MaybeSignal<f64>, status: RwSignal<PlayStatus>) -> im
|
|||||||
let width = progress_bar.offset_width() as f64;
|
let width = progress_bar.offset_width() as f64;
|
||||||
let percentage = x_click_pos / width * 100.0;
|
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;
|
let time = duration.1 * percentage / 100.0;
|
||||||
skip_to(status, time);
|
skip_to(time);
|
||||||
set_playing(status, true);
|
set_playing(true);
|
||||||
} else {
|
} else {
|
||||||
error!("Unable to skip to time: Unable to get current duration");
|
error!("Unable to skip to time: Unable to get current duration");
|
||||||
}
|
}
|
||||||
@ -438,11 +444,11 @@ fn ProgressBar(percentage: MaybeSignal<f64>, status: RwSignal<PlayStatus>) -> im
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn QueueToggle(status: RwSignal<PlayStatus>) -> impl IntoView {
|
fn QueueToggle() -> impl IntoView {
|
||||||
|
|
||||||
let update_queue = move |_| {
|
let update_queue = move |_| {
|
||||||
toggle_queue(status);
|
toggle_queue();
|
||||||
log!("queue button pressed, queue status: {:?}", status.with_untracked(|status| status.queue_open));
|
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
|
// We use this to prevent the buttons from being focused when clicked
|
||||||
@ -463,9 +469,9 @@ fn QueueToggle(status: RwSignal<PlayStatus>) -> impl IntoView {
|
|||||||
|
|
||||||
/// Renders the title of the page based on the currently playing song
|
/// Renders the title of the page based on the currently playing song
|
||||||
#[component]
|
#[component]
|
||||||
pub fn CustomTitle(play_status: RwSignal<PlayStatus>) -> impl IntoView {
|
pub fn CustomTitle() -> impl IntoView {
|
||||||
let title = create_memo(move |_| {
|
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| {
|
play_status.queue.front().map_or("LibreTunes".to_string(), |song_data| {
|
||||||
format!("{} - {} | {}",song_data.title.clone(),Artist::display_list(&song_data.artists), "LibreTunes")
|
format!("{} - {} | {}",song_data.title.clone(),Artist::display_list(&song_data.artists), "LibreTunes")
|
||||||
})
|
})
|
||||||
@ -478,18 +484,20 @@ pub fn CustomTitle(play_status: RwSignal<PlayStatus>) -> impl IntoView {
|
|||||||
|
|
||||||
/// The main play bar component, containing the progress bar, media info, play controls, and play duration
|
/// The main play bar component, containing the progress bar, media info, play controls, and play duration
|
||||||
#[component]
|
#[component]
|
||||||
pub fn PlayBar(status: RwSignal<PlayStatus>) -> 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
|
// 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| {
|
let _arrow_key_handle = window_event_listener(ev::keydown, move |e: ev::KeyboardEvent| {
|
||||||
if e.key() == "ArrowRight" {
|
if e.key() == "ArrowRight" {
|
||||||
e.prevent_default();
|
e.prevent_default();
|
||||||
log!("Right arrow key pressed, skipping forward by {} seconds", ARROW_KEY_SKIP_TIME);
|
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;
|
let mut time = duration.0 + ARROW_KEY_SKIP_TIME;
|
||||||
time = time.clamp(0.0, duration.1);
|
time = time.clamp(0.0, duration.1);
|
||||||
skip_to(status, time);
|
skip_to(time);
|
||||||
set_playing(status, true);
|
set_playing(true);
|
||||||
} else {
|
} else {
|
||||||
error!("Unable to skip forward: Unable to get current duration");
|
error!("Unable to skip forward: Unable to get current duration");
|
||||||
}
|
}
|
||||||
@ -498,11 +506,11 @@ pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView {
|
|||||||
e.prevent_default();
|
e.prevent_default();
|
||||||
log!("Left arrow key pressed, skipping backward by {} seconds", ARROW_KEY_SKIP_TIME);
|
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;
|
let mut time = duration.0 - ARROW_KEY_SKIP_TIME;
|
||||||
time = time.clamp(0.0, duration.1);
|
time = time.clamp(0.0, duration.1);
|
||||||
skip_to(status, time);
|
skip_to(time);
|
||||||
set_playing(status, true);
|
set_playing(true);
|
||||||
} else {
|
} else {
|
||||||
error!("Unable to skip backward: Unable to get current duration");
|
error!("Unable to skip backward: Unable to get current duration");
|
||||||
}
|
}
|
||||||
@ -516,7 +524,7 @@ pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView {
|
|||||||
log!("Space bar pressed, toggling play/pause");
|
log!("Space bar pressed, toggling play/pause");
|
||||||
|
|
||||||
let playing = status.with_untracked(|status| status.playing);
|
let playing = status.with_untracked(|status| status.playing);
|
||||||
set_playing(status, !playing);
|
set_playing(!playing);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -659,14 +667,14 @@ pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView {
|
|||||||
<audio _ref=audio_ref on:play=on_play on:pause=on_pause
|
<audio _ref=audio_ref on:play=on_play on:pause=on_pause
|
||||||
on:timeupdate=on_time_update on:ended=on_end type="audio/mpeg" />
|
on:timeupdate=on_time_update on:ended=on_end type="audio/mpeg" />
|
||||||
<div class="playbar">
|
<div class="playbar">
|
||||||
<ProgressBar percentage=percentage.into() status=status />
|
<ProgressBar percentage=percentage.into() />
|
||||||
<div class="playbar-left-group">
|
<div class="playbar-left-group">
|
||||||
<MediaInfo status=status />
|
<MediaInfo />
|
||||||
<LikeDislike status=status />
|
<LikeDislike />
|
||||||
</div>
|
</div>
|
||||||
<PlayControls status=status />
|
<PlayControls />
|
||||||
<PlayDuration elapsed_secs=elapsed_secs.into() total_secs=total_secs.into() />
|
<PlayDuration elapsed_secs=elapsed_secs.into() total_secs=total_secs.into() />
|
||||||
<QueueToggle status=status />
|
<QueueToggle />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
src/queue.rs
11
src/queue.rs
@ -1,6 +1,6 @@
|
|||||||
use crate::models::Artist;
|
use crate::models::Artist;
|
||||||
use crate::playstatus::PlayStatus;
|
|
||||||
use crate::song::Song;
|
use crate::song::Song;
|
||||||
|
use crate::util::state::GlobalState;
|
||||||
use leptos::ev::MouseEvent;
|
use leptos::ev::MouseEvent;
|
||||||
use leptos::leptos_dom::*;
|
use leptos::leptos_dom::*;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
@ -9,22 +9,23 @@ use leptos::ev::DragEvent;
|
|||||||
|
|
||||||
const RM_BTN_SIZE: &str = "2.5rem";
|
const RM_BTN_SIZE: &str = "2.5rem";
|
||||||
|
|
||||||
fn remove_song_fn(index: usize, status: RwSignal<PlayStatus>) {
|
fn remove_song_fn(index: usize) {
|
||||||
if index == 0 {
|
if index == 0 {
|
||||||
log!("Error: Trying to remove currently playing song (index 0) from queue");
|
log!("Error: Trying to remove currently playing song (index 0) from queue");
|
||||||
} else {
|
} else {
|
||||||
log!("Remove Song from Queue: Song is not currently playing, deleting song from queue and not adding to history");
|
log!("Remove Song from Queue: Song is not currently playing, deleting song from queue and not adding to history");
|
||||||
status.update(|status| {
|
GlobalState::play_status().update(|status| {
|
||||||
status.queue.remove(index);
|
status.queue.remove(index);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Queue(status: RwSignal<PlayStatus>) -> impl IntoView {
|
pub fn Queue() -> impl IntoView {
|
||||||
|
let status = GlobalState::play_status();
|
||||||
|
|
||||||
let remove_song = move |index: usize| {
|
let remove_song = move |index: usize| {
|
||||||
remove_song_fn(index, status);
|
remove_song_fn(index);
|
||||||
log!("Removed song {}", index + 1);
|
log!("Removed song {}", index + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,3 +5,5 @@ cfg_if! {
|
|||||||
pub mod audio;
|
pub mod audio;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod state;
|
||||||
|
49
src/util/state.rs
Normal file
49
src/util/state.rs
Normal file
@ -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<User>>,
|
||||||
|
|
||||||
|
/// The current play status
|
||||||
|
pub play_status: RwSignal<PlayStatus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<User>> {
|
||||||
|
expect_context::<Self>().logged_in_user
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play_status() -> RwSignal<PlayStatus> {
|
||||||
|
expect_context::<Self>().play_status
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user