use std::rc::Rc; use leptos::*; use leptos::logging::*; use leptos_icons::*; use crate::api::songs::*; use crate::songdata::SongData; use crate::models::{Album, Artist}; use crate::util::state::GlobalState; const LIKE_DISLIKE_BTN_SIZE: &str = "2em"; #[component] pub fn SongList(songs: Vec) -> impl IntoView { __SongListInner(songs.into_iter().map(|song| (song, ())).collect::>(), false) } #[component] pub fn SongListExtra(songs: Vec<(SongData, T)>) -> impl IntoView where T: Clone + IntoView + 'static { __SongListInner(songs, true) } #[component] fn SongListInner(songs: Vec<(SongData, T)>, show_extra: bool) -> impl IntoView where T: Clone + IntoView + 'static { let songs = Rc::new(songs); let songs_2 = songs.clone(); // Signal that acts as a callback for a song list item to queue songs after it in the list let (handle_queue_remaining, do_queue_remaining) = create_signal(None); create_effect(move |_| { let clicked_index = handle_queue_remaining.get(); if let Some(index) = clicked_index { GlobalState::play_status().update(|status| { let song: &(SongData, T) = songs.get(index).expect("Invalid song list item index"); if status.queue.front().map(|song| song.id) == Some(song.0.id) { // If the clicked song is already at the front of the queue, just play it status.playing = true; } else { // Otherwise, add the currently playing song to the history, // clear the queue, and queue the clicked song and other after it if let Some(last_playing) = status.queue.pop_front() { status.history.push_back(last_playing); } status.queue.clear(); status.queue.extend(songs.iter().skip(index).map(|(song, _)| song.clone())); status.playing = true; } }); } }); view! { { songs_2.iter().enumerate().map(|(list_index, (song, extra))| { let song_id = song.id; let playing = create_rw_signal(false); create_effect(move |_| { GlobalState::play_status().with(|status| { playing.set(status.queue.front().map(|song| song.id) == Some(song_id) && status.playing); }); }); view! { } }).collect::>() }
} } #[component] pub fn SongListItem(song: SongData, song_playing: MaybeSignal, extra: Option, list_index: usize, do_queue_remaining: WriteSignal>) -> impl IntoView where T: IntoView + 'static { let liked = create_rw_signal(song.like_dislike.map(|(liked, _)| liked).unwrap_or(false)); let disliked = create_rw_signal(song.like_dislike.map(|(_, disliked)| disliked).unwrap_or(false)); view! {

{song.title}

{format!("{}:{:02}", song.duration / 60, song.duration % 60)} {extra.map(|extra| view! { {extra} })} } } /// Display the song's image, with an overlay if the song is playing /// When the song list item is hovered, the overlay will show the play button #[component] fn SongImage(image_path: String, song_playing: MaybeSignal, list_index: usize, do_queue_remaining: WriteSignal>) -> impl IntoView { let play_song = move |_| { do_queue_remaining.set(Some(list_index)); }; let pause_song = move |_| { GlobalState::play_status().update(|status| { status.playing = false; }); }; view! { {move || if song_playing.get() { view! { }.into_view() } else { view! { }.into_view() }} } } /// Displays a song's artists, with links to their artist pages #[component] fn SongArtists(artists: Vec) -> impl IntoView { let num_artists = artists.len() as isize; artists.iter().enumerate().map(|(i, artist)| { let i = i as isize; view! { { if let Some(id) = artist.id { view! { {artist.name.clone()} }.into_view() } else { view! { {artist.name.clone()} }.into_view() } } {if i < num_artists - 2 { ", " } else if i == num_artists - 2 { " & " } else { "" }} } }).collect::>() } /// Display a song's album, with a link to the album page #[component] fn SongAlbum(album: Option) -> impl IntoView { album.as_ref().map(|album| { view! { { if let Some(id) = album.id { view! { {album.title.clone()} }.into_view() } else { view! { {album.title.clone()} }.into_view() } } } }) } /// Display like and dislike buttons for a song, and indicate if the song is liked or disliked #[component] fn SongLikeDislike( #[prop(into)] song_id: MaybeSignal, liked: RwSignal, disliked: RwSignal) -> impl IntoView { let like_icon = Signal::derive(move || { if liked.get() { icondata::TbThumbUpFilled } else { icondata::TbThumbUp } }); let dislike_icon = Signal::derive(move || { if disliked.get() { icondata::TbThumbDownFilled } else { icondata::TbThumbDown } }); let like_class = MaybeProp::derive(move || { if liked.get() { Some(TextProp::from("controlbtn")) } else { Some(TextProp::from("controlbtn hide-until-hover")) } }); let dislike_class = MaybeProp::derive(move || { if disliked.get() { Some(TextProp::from("controlbtn hmirror")) } else { Some(TextProp::from("controlbtn hmirror hide-until-hover")) } }); // If an error occurs, check the like/dislike status again to ensure consistency let check_like_dislike = move || { spawn_local(async move { match get_like_dislike_song(song_id.get_untracked()).await { Ok((like, dislike)) => { liked.set(like); disliked.set(dislike); }, Err(_) => {} } }); }; let toggle_like = move |_| { let new_liked = !liked.get_untracked(); liked.set(new_liked); disliked.set(disliked.get_untracked() && !liked.get_untracked()); spawn_local(async move { match set_like_song(song_id.get_untracked(), new_liked).await { Ok(_) => {}, Err(e) => { error!("Error setting like: {}", e); check_like_dislike(); } } }); }; let toggle_dislike = move |_| { disliked.set(!disliked.get_untracked()); liked.set(liked.get_untracked() && !disliked.get_untracked()); spawn_local(async move { match set_dislike_song(song_id.get_untracked(), disliked.get_untracked()).await { Ok(_) => {}, Err(e) => { error!("Error setting dislike: {}", e); check_like_dislike(); } } }); }; view! { } }