Add SongList component

This commit is contained in:
Ethan Girouard 2024-10-11 13:22:57 -04:00
parent 36e7a5827b
commit 0550b18d77
Signed by: eta357
GPG Key ID: 7BCDC36DFD11C146
5 changed files with 284 additions and 0 deletions

View File

@ -5,3 +5,4 @@ pub mod personal;
pub mod dashboard_tile; pub mod dashboard_tile;
pub mod dashboard_row; pub mod dashboard_row;
pub mod upload; pub mod upload;
pub mod song_list;

157
src/components/song_list.rs Normal file
View File

@ -0,0 +1,157 @@
use leptos::*;
use leptos_icons::*;
use crate::songdata::SongData;
use crate::models::{Album, Artist};
const LIKE_DISLIKE_BTN_SIZE: &str = "2em";
#[component]
pub fn SongList(songs: MaybeSignal<Vec<SongData>>) -> impl IntoView {
view! {
<table class="song-list">
{
songs.with(|songs| {
let mut first_song = true;
songs.iter().map(|song| {
let playing = first_song.into();
first_song = false;
view! {
<SongListItem song={song.clone()} song_playing=playing />
}
}).collect::<Vec<_>>()
})
}
</table>
}
}
#[component]
pub fn SongListItem(song: SongData, song_playing: MaybeSignal<bool>) -> impl IntoView {
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! {
<tr class="song-list-item">
<td class="song-image"><SongImage image_path=song.image_path song_playing /></td>
<td class="song-title"><p>{song.title}</p></td>
<td class="song-list-spacer"></td>
<td class="song-artists"><SongArtists artists=song.artists /></td>
<td class="song-list-spacer"></td>
<td class="song-album"><SongAlbum album=song.album /></td>
<td class="song-list-spacer-big"></td>
<td class="song-like-dislike"><SongLikeDislike liked disliked/></td>
<td>{format!("{}:{:02}", song.duration / 60, song.duration % 60)}</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]
fn SongImage(image_path: String, song_playing: MaybeSignal<bool>) -> impl IntoView {
view! {
<img class="song-image" src={image_path}/>
{if song_playing.get() {
view! { <Icon class="song-image-overlay song-playing-overlay" icon=icondata::BsPauseFill /> }.into_view()
} else {
view! { <Icon class="song-image-overlay hide-until-hover" icon=icondata::BsPlayFill /> }.into_view()
}}
}
}
/// Displays a song's artists, with links to their artist pages
#[component]
fn SongArtists(artists: Vec<Artist>) -> 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! { <a href={format!("/artist/{}", id)}>{artist.name.clone()}</a> }.into_view()
} else {
view! { <span>{artist.name.clone()}</span> }.into_view()
}
}
{if i < num_artists - 2 { ", " } else if i == num_artists - 2 { " & " } else { "" }}
}
}).collect::<Vec<_>>()
}
/// Display a song's album, with a link to the album page
#[component]
fn SongAlbum(album: Option<Album>) -> impl IntoView {
album.as_ref().map(|album| {
view! {
<span>
{
if let Some(id) = album.id {
view! { <a href={format!("/album/{}", id)}>{album.title.clone()}</a> }.into_view()
} else {
view! { <span>{album.title.clone()}</span> }.into_view()
}
}
</span>
}
})
}
/// Display like and dislike buttons for a song, and indicate if the song is liked or disliked
#[component]
fn SongLikeDislike(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 = 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"))
}
});
let toggle_like = move |_| {
liked.set(!liked.get_untracked());
disliked.set(disliked.get_untracked() && !liked.get_untracked());
};
let toggle_dislike = move |_| {
disliked.set(!disliked.get_untracked());
liked.set(liked.get_untracked() && !disliked.get_untracked());
};
view! {
<button on:click=toggle_dislike>
<Icon class=dislike_class width=LIKE_DISLIKE_BTN_SIZE height=LIKE_DISLIKE_BTN_SIZE icon=dislike_icon />
</button>
<button on:click=toggle_like>
<Icon class=like_class width=LIKE_DISLIKE_BTN_SIZE height=LIKE_DISLIKE_BTN_SIZE icon=like_icon />
</button>
}
}

View File

@ -6,6 +6,7 @@ use time::Date;
/// Holds information about a song /// Holds information about a song
/// ///
/// Intended to be used in the front-end, as it includes artist and album objects, rather than just their ids. /// Intended to be used in the front-end, as it includes artist and album objects, rather than just their ids.
#[derive(Clone)]
pub struct SongData { pub struct SongData {
/// Song id /// Song id
pub id: i32, pub id: i32,

View File

@ -11,6 +11,7 @@
@import 'dashboard_tile.scss'; @import 'dashboard_tile.scss';
@import 'dashboard_row.scss'; @import 'dashboard_row.scss';
@import 'upload.scss'; @import 'upload.scss';
@import 'song_list.scss';
body { body {
font-family: sans-serif; font-family: sans-serif;

124
style/song_list.scss Normal file
View File

@ -0,0 +1,124 @@
table.song-list {
width: 100%;
border-collapse: collapse;
tr.song-list-item {
border: solid;
border-width: 1px 0;
border-color: #303030;
position: relative;
td {
color: $text-controls-color;
white-space: nowrap;
padding-left: 10px;
padding-right: 10px;
a {
text-decoration: none;
color: $text-controls-color;
}
}
a:hover {
text-decoration: underline $controls-hover-color;
}
td.song-image {
width: 35px;
display: flex;
img.song-image {
position: absolute;
top: 50%;
-ms-transform: translateY(-50%);
transform: translateY(-50%);
width: 35px;
height: 35px;
border-radius: 5px;
}
svg.song-image-overlay {
position: absolute;
top: 50%;
-ms-transform: translateY(-50%);
transform: translateY(-50%);
width: 35px;
height: 35px;
border-radius: 5px;
fill: $text-controls-color;
}
svg.song-image-overlay:hover {
fill: $controls-hover-color;
}
svg.song-image-overlay:active {
fill: $controls-click-color;
}
}
td.song-list-spacer {
width: 20%;
}
td.song-list-spacer-big {
width: 40%;
}
button {
svg.hmirror {
-moz-transform: scale(-1, 1);
-webkit-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
transform: scale(-1, 1);
}
.controlbtn {
color: $text-controls-color;
}
.controlbtn:hover {
color: $controls-hover-color;
}
.controlbtn:active {
color: $controls-click-color;
}
background-color: transparent;
border: transparent;
}
.hide-until-hover {
visibility: hidden;
}
.song-playing-overlay {
background-color: rgba(0, 0, 0, 0.8);
}
}
tr.song-list-item:first-child {
border-top: none;
}
tr.song-list-item:last-child {
border-bottom: none;
}
tr.song-list-item:hover {
background-color: #303030;
.hide-until-hover {
visibility: visible;
}
td.song-image {
svg.song-image-overlay {
background-color: rgba(0, 0, 0, 0.8);
}
}
}
}