Remove old song list component
All checks were successful
Push Workflows / rustfmt (push) Successful in 10s
Push Workflows / mdbook (push) Successful in 14s
Push Workflows / docs (push) Successful in 2m40s
Push Workflows / mdbook-server (push) Successful in 2m30s
Push Workflows / leptos-test (push) Successful in 3m2s
Push Workflows / test (push) Successful in 3m13s
Push Workflows / clippy (push) Successful in 3m23s
Push Workflows / docker-build (push) Successful in 5m49s
Push Workflows / build (push) Successful in 6m58s
Push Workflows / nix-build (push) Successful in 7m49s

This commit is contained in:
2025-12-01 16:53:46 -05:00
parent aa3132feb9
commit 2e42d0e964
3 changed files with 5 additions and 318 deletions

View File

@@ -14,7 +14,6 @@ pub mod playbar;
pub mod queue;
pub mod sidebar;
pub mod song;
pub mod song_list;
pub mod songs;
pub mod upload;
pub mod upload_dropdown;
@@ -38,9 +37,6 @@ pub mod all {
pub use queue::Queue;
pub use sidebar::{Playlists, Sidebar};
pub use song::Song;
pub use song_list::{
SongAlbum, SongArtists, SongImage, SongLikeDislike, SongList, SongListExtra, SongListItem,
};
pub use songs::all::*;
pub use upload::{Album, Artist, Upload, UploadBtn};
pub use upload_dropdown::{UploadDropdown, UploadDropdownBtn};

View File

@@ -1,304 +0,0 @@
use crate::prelude::*;
use std::rc::Rc;
const LIKE_DISLIKE_BTN_SIZE: &str = "2em";
#[component]
pub fn SongList(songs: Vec<frontend::Song>) -> impl IntoView {
let songs = songs.into_iter().map(|song| (song, ())).collect::<Vec<_>>();
view! {
<SongListInner _songs=songs _show_extra=false />
}
}
#[component]
pub fn SongListExtra<T>(songs: Vec<(frontend::Song, T)>) -> impl IntoView
where
T: Clone + IntoView + 'static,
{
view! {
<SongListInner _songs=songs _show_extra=true />
}
}
// TODO these arguments shouldn't need a leading underscore,
// but for some reason the compiler thinks they are unused
#[component]
fn SongListInner<T>(_songs: Vec<(frontend::Song, 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) = signal(None);
Effect::new(move |_| {
let clicked_index = handle_queue_remaining.get();
if let Some(index) = clicked_index {
GlobalState::play_status().update(|status| {
let song: &(frontend::Song, 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! {
<table class="w-full">
<tbody>
{
songs_2.iter().enumerate().map(|(list_index, (song, extra))| {
let song_id = song.id;
let playing = RwSignal::new(false);
Effect::new(move |_| {
GlobalState::play_status().with(|status| {
playing.set(status.queue.front().map(|song| song.id) == Some(song_id) && status.playing);
});
});
view! {
<SongListItem song={song.clone()} song_playing=playing.into()
extra={if _show_extra { Some(extra.clone()) } else { None }} list_index do_queue_remaining/>
}
}).collect::<Vec<_>>()
}
</tbody>
</table>
}
}
#[component]
pub fn SongListItem<T>(
song: frontend::Song,
song_playing: Signal<bool>,
extra: Option<T>,
list_index: usize,
do_queue_remaining: WriteSignal<Option<usize>>,
) -> impl IntoView
where
T: IntoView + 'static,
{
let liked = RwSignal::new(song.like_dislike.map(|(liked, _)| liked).unwrap_or(false));
let disliked = RwSignal::new(
song.like_dislike
.map(|(_, disliked)| disliked)
.unwrap_or(false),
);
view! {
<tr class="group border-b border-t border-neutral-600 last-of-type:border-b-0
first-of-type:border-t-0 hover:bg-neutral-700 [&>*]:px-2">
<td class="relative w-13 h-13"><SongImage image_path=song.image_path.path() song_playing
list_index do_queue_remaining /></td>
<td><p>{song.title}</p></td>
<td></td>
<td><SongArtists artists=song.artists /></td>
<td></td>
<td><SongAlbum album=song.album /></td>
<td></td>
<td><SongLikeDislike song_id=song.id liked disliked/></td>
<td>{format!("{}:{:02}", song.duration / 60, song.duration % 60)}</td>
{extra.map(|extra| view! {
<td></td>
<td>{extra}</td>
})}
</tr>
}
}
/// 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]
pub fn SongImage(
image_path: String,
song_playing: Signal<bool>,
list_index: usize,
do_queue_remaining: WriteSignal<Option<usize>>,
) -> impl IntoView {
let toggle_play = move |_| {
if song_playing.get() {
GlobalState::play_status().update(|status| {
status.playing = false;
});
} else {
do_queue_remaining.set(Some(list_index));
}
};
let icon = Signal::derive(move || {
if song_playing.get() {
icondata::BsPauseFill
} else {
icondata::BsPlayFill
}
});
let style = Signal::derive(move || {
if song_playing.get() {
"w-6 h-6 absolute top-1/2 left-1/2 translate-[-50%]"
} else {
"w-6 h-6 opacity-0 group-hover:opacity-100 absolute top-1/2 left-1/2 translate-[-50%]"
}
});
view! {
<img class="group-hover:brightness-45" src={image_path}/>
<Icon icon on:click={toggle_play} {..} class=style />
}
}
/// Displays a song's artists, with links to their artist pages
#[component]
pub fn SongArtists(artists: Vec<backend::Artist>) -> impl IntoView {
let num_artists = artists.len() as isize;
artists
.iter()
.enumerate()
.map(|(i, artist)| {
let i = i as isize;
view! {
<a class="hover:underline active:text-controls-active"
href={format!("/artist/{}", artist.id)}>{artist.name.clone()}</a>
{
use std::cmp::Ordering;
match i.cmp(&(num_artists - 2)) {
Ordering::Less => ", ",
Ordering::Equal => " & ",
Ordering::Greater => "",
}
}
}
})
.collect::<Vec<_>>()
}
/// Display a song's album, with a link to the album page
#[component]
pub fn SongAlbum(album: Option<backend::Album>) -> impl IntoView {
album.as_ref().map(|album| {
view! {
<span>
<a class="hover:underline active:text-controls-active"
href={format!("/album/{}", album.id)}>{album.title.clone()}</a>
</span>
}
})
}
/// Display like and dislike buttons for a song, and indicate if the song is liked or disliked
#[component]
pub fn SongLikeDislike(
#[prop(into)] song_id: Signal<i32>,
liked: RwSignal<bool>,
disliked: RwSignal<bool>,
) -> 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 = Signal::derive(move || {
if liked.get() {
""
} else {
"opacity-0 group-hover:opacity-100"
}
});
let dislike_class = Signal::derive(move || {
if disliked.get() {
""
} else {
"opacity-0 group-hover:opacity-100"
}
});
// If an error occurs, check the like/dislike status again to ensure consistency
let check_like_dislike = move || {
spawn_local(async move {
if let Ok((like, dislike)) =
api::songs::get_like_dislike_song(song_id.get_untracked()).await
{
liked.set(like);
disliked.set(dislike);
}
});
};
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 api::songs::set_like_song(song_id.get_untracked(), new_liked).await {
Ok(_) => {}
Err(e) => {
leptos_err!("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 api::songs::set_dislike_song(song_id.get_untracked(), disliked.get_untracked())
.await
{
Ok(_) => {}
Err(e) => {
leptos_err!("Error setting dislike: {}", e);
check_like_dislike();
}
}
});
};
view! {
<button class="control scale-x-[-1]" on:click=toggle_dislike>
<Icon width=LIKE_DISLIKE_BTN_SIZE height=LIKE_DISLIKE_BTN_SIZE icon={dislike_icon} {..} class=dislike_class />
</button>
<button class="control" on:click=toggle_like>
<Icon width=LIKE_DISLIKE_BTN_SIZE height=LIKE_DISLIKE_BTN_SIZE icon={like_icon} {..} class=like_class />
</button>
}
}

View File

@@ -30,8 +30,7 @@ pub fn default_song_list_content(
view! {
<SongListIndex list_index />
// TODO remote module path when able
<super::song::SongImage
<SongImage
song={song.clone()}
song_playing={Signal::stored(false)}
list_index
@@ -75,8 +74,7 @@ pub fn queue_content(
view! {
<SongListIndex list_index />
// TODO remote module path when able
<super::song::SongImage
<SongImage
song={song.clone()}
list_index
play_callback
@@ -123,8 +121,7 @@ pub fn album_content(
view! {
<SongListIndex list_index />
// TODO remote module path when able
<super::song::SongImage
<SongImage
song={song.clone()}
list_index
play_callback
@@ -154,8 +151,7 @@ where
S: DisplaySongList<RowArgs = (usize, PlayCallback, frontend::Song)>,
{
view! {
// TODO remote module path when able
<super::song_list::SongList
<SongList
song_ids
headers={queue_headers()}
cols={queue_content}
@@ -169,8 +165,7 @@ where
S: DisplaySongList<RowArgs = (usize, PlayCallback, frontend::Song)>,
{
view! {
// TODO remote module path when able
<super::song_list::SongList
<SongList
song_ids
headers={album_headers()}
cols={album_content}