Merge branch '106-create-song-list-component' into 42-create-profile-page

This commit is contained in:
Ethan Girouard 2024-11-01 13:22:37 -04:00
commit 4e4c94a189
Signed by: eta357
GPG Key ID: 7BCDC36DFD11C146
17 changed files with 673 additions and 33 deletions

79
Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "addr2line" name = "addr2line"
@ -151,7 +151,7 @@ dependencies = [
"axum-core", "axum-core",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http 1.1.0",
"http-body", "http-body",
"http-body-util", "http-body-util",
"hyper", "hyper",
@ -167,7 +167,7 @@ dependencies = [
"serde", "serde",
"sync_wrapper 1.0.0", "sync_wrapper 1.0.0",
"tokio", "tokio",
"tower", "tower 0.4.13",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
] ]
@ -181,7 +181,7 @@ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http 1.1.0",
"http-body", "http-body",
"http-body-util", "http-body-util",
"mime", "mime",
@ -1017,13 +1017,14 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]] [[package]]
name = "gloo-net" name = "gloo-net"
version = "0.5.0" version = "0.5.0"
source = "git+https://github.com/rustwasm/gloo.git?rev=a823fab7ecc4068e9a28bd669da5eaf3f0a56380#a823fab7ecc4068e9a28bd669da5eaf3f0a56380" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"gloo-utils", "gloo-utils",
"http", "http 0.2.12",
"js-sys", "js-sys",
"pin-project", "pin-project",
"serde", "serde",
@ -1049,7 +1050,8 @@ dependencies = [
[[package]] [[package]]
name = "gloo-utils" name = "gloo-utils"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/rustwasm/gloo.git?rev=a823fab7ecc4068e9a28bd669da5eaf3f0a56380#a823fab7ecc4068e9a28bd669da5eaf3f0a56380" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"serde", "serde",
@ -1117,6 +1119,17 @@ dependencies = [
"utf8-width", "utf8-width",
] ]
[[package]]
name = "http"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]] [[package]]
name = "http" name = "http"
version = "1.1.0" version = "1.1.0"
@ -1135,7 +1148,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http 1.1.0",
] ]
[[package]] [[package]]
@ -1146,7 +1159,7 @@ checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",
"http", "http 1.1.0",
"http-body", "http-body",
"pin-project-lite", "pin-project-lite",
] ]
@ -1178,7 +1191,7 @@ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-util", "futures-util",
"http", "http 1.1.0",
"http-body", "http-body",
"httparse", "httparse",
"httpdate", "httpdate",
@ -1196,7 +1209,7 @@ checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http 1.1.0",
"http-body", "http-body",
"hyper", "hyper",
"pin-project-lite", "pin-project-lite",
@ -1826,7 +1839,7 @@ dependencies = [
"diesel_migrations", "diesel_migrations",
"dotenv", "dotenv",
"flexi_logger", "flexi_logger",
"http", "http 1.1.0",
"icondata", "icondata",
"image-convert", "image-convert",
"lazy_static", "lazy_static",
@ -1846,7 +1859,7 @@ dependencies = [
"thiserror", "thiserror",
"time", "time",
"tokio", "tokio",
"tower", "tower 0.5.1",
"tower-http", "tower-http",
"tower-sessions-redis-store", "tower-sessions-redis-store",
"wasm-bindgen", "wasm-bindgen",
@ -2023,7 +2036,7 @@ dependencies = [
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-util", "futures-util",
"http", "http 1.1.0",
"httparse", "httparse",
"log", "log",
"memchr", "memchr",
@ -2670,7 +2683,7 @@ dependencies = [
"dashmap", "dashmap",
"futures", "futures",
"gloo-net", "gloo-net",
"http", "http 1.1.0",
"http-body-util", "http-body-util",
"hyper", "hyper",
"inventory", "inventory",
@ -2683,7 +2696,7 @@ dependencies = [
"serde_qs", "serde_qs",
"server_fn_macro_default", "server_fn_macro_default",
"thiserror", "thiserror",
"tower", "tower 0.4.13",
"tower-layer", "tower-layer",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
@ -3109,6 +3122,20 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "tower"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper 0.1.2",
"tower-layer",
"tower-service",
]
[[package]] [[package]]
name = "tower-cookies" name = "tower-cookies"
version = "0.10.0" version = "0.10.0"
@ -3119,7 +3146,7 @@ dependencies = [
"axum-core", "axum-core",
"cookie", "cookie",
"futures-util", "futures-util",
"http", "http 1.1.0",
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"tower-layer", "tower-layer",
@ -3128,14 +3155,14 @@ dependencies = [
[[package]] [[package]]
name = "tower-http" name = "tower-http"
version = "0.5.2" version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97"
dependencies = [ dependencies = [
"bitflags 2.5.0", "bitflags 2.5.0",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http 1.1.0",
"http-body", "http-body",
"http-body-util", "http-body-util",
"http-range-header", "http-range-header",
@ -3153,15 +3180,15 @@ dependencies = [
[[package]] [[package]]
name = "tower-layer" name = "tower-layer"
version = "0.3.2" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.2" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]] [[package]]
name = "tower-sessions" name = "tower-sessions"
@ -3170,7 +3197,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b27326208b21807803c5f5aa1020d30ca0432b78cfe251b51a67a05e0baea102" checksum = "b27326208b21807803c5f5aa1020d30ca0432b78cfe251b51a67a05e0baea102"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"http", "http 1.1.0",
"time", "time",
"tokio", "tokio",
"tower-cookies", "tower-cookies",
@ -3191,7 +3218,7 @@ dependencies = [
"axum-core", "axum-core",
"base64", "base64",
"futures", "futures",
"http", "http 1.1.0",
"parking_lot", "parking_lot",
"rand", "rand",
"serde", "serde",

View File

@ -28,8 +28,8 @@ diesel_migrations = { version = "2.1.0", optional = true }
pbkdf2 = { version = "0.12.2", features = ["simple"], optional = true } pbkdf2 = { version = "0.12.2", features = ["simple"], optional = true }
tokio = { version = "1", optional = true, features = ["rt-multi-thread"] } tokio = { version = "1", optional = true, features = ["rt-multi-thread"] }
axum = { version = "0.7.5", features = ["tokio", "http1"], default-features = false, optional = true } axum = { version = "0.7.5", features = ["tokio", "http1"], default-features = false, optional = true }
tower = { version = "0.4.13", optional = true } tower = { version = "0.5.1", optional = true, features = ["util"] }
tower-http = { version = "0.5", optional = true, features = ["fs"] } tower-http = { version = "0.6.1", optional = true, features = ["fs"] }
thiserror = "1.0.57" thiserror = "1.0.57"
tower-sessions-redis-store = { version = "0.11", optional = true } tower-sessions-redis-store = { version = "0.11", optional = true }
async-trait = { version = "0.1.79", optional = true } async-trait = { version = "0.1.79", optional = true }
@ -43,9 +43,6 @@ web-sys = "0.3.69"
leptos-use = "0.13.5" leptos-use = "0.13.5"
image-convert = { version = "0.18.0", optional = true, default-features = false } image-convert = { version = "0.18.0", optional = true, default-features = false }
[patch.crates-io]
gloo-net = { git = "https://github.com/rustwasm/gloo.git", rev = "a823fab7ecc4068e9a28bd669da5eaf3f0a56380" }
[features] [features]
hydrate = [ hydrate = [
"leptos/hydrate", "leptos/hydrate",

39
src/albumdata.rs Normal file
View File

@ -0,0 +1,39 @@
use crate::models::Artist;
use crate::components::dashboard_tile::DashboardTile;
use time::Date;
/// Holds information about an album
///
/// Intended to be used in the front-end
pub struct AlbumData {
/// Album id
pub id: i32,
/// Album title
pub title: String,
/// Album artists
pub artists: Vec<Artist>,
/// Album release date
pub release_date: Option<Date>,
/// Path to album image, relative to the root of the web server.
/// For example, `"/assets/images/Album.jpg"`
pub image_path: String,
}
impl DashboardTile for AlbumData {
fn image_path(&self) -> String {
self.image_path.clone()
}
fn title(&self) -> String {
self.title.clone()
}
fn link(&self) -> String {
format!("/album/{}", self.id)
}
fn description(&self) -> Option<String> {
Some(format!("Album • {}", Artist::display_list(&self.artists)))
}
}

View File

@ -1,4 +1,5 @@
use crate::playbar::PlayBar; use crate::playbar::PlayBar;
use crate::playbar::CustomTitle;
use crate::playstatus::PlayStatus; use crate::playstatus::PlayStatus;
use crate::queue::Queue; use crate::queue::Queue;
use leptos::*; use leptos::*;
@ -24,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
<Title text="LibreTunes"/> <CustomTitle play_status=play_status/>
// content for this welcome page // content for this welcome page
<Router fallback=|| { <Router fallback=|| {

32
src/artistdata.rs Normal file
View File

@ -0,0 +1,32 @@
use crate::components::dashboard_tile::DashboardTile;
/// Holds information about an artist
///
/// Intended to be used in the front-end
pub struct ArtistData {
/// Artist id
pub id: i32,
/// Artist name
pub name: String,
/// Path to artist image, relative to the root of the web server.
/// For example, `"/assets/images/Artist.jpg"`
pub image_path: String,
}
impl DashboardTile for ArtistData {
fn image_path(&self) -> String {
self.image_path.clone()
}
fn title(&self) -> String {
self.name.clone()
}
fn link(&self) -> String {
format!("/artist/{}", self.id)
}
fn description(&self) -> Option<String> {
Some("Artist".to_string())
}
}

View File

@ -2,4 +2,7 @@ pub mod sidebar;
pub mod dashboard; pub mod dashboard;
pub mod search; pub mod search;
pub mod personal; pub mod personal;
pub mod dashboard_tile;
pub mod dashboard_row;
pub mod upload; pub mod upload;
pub mod song_list;

View File

@ -0,0 +1,118 @@
use leptos::html::Ul;
use leptos::leptos_dom::*;
use leptos::*;
use leptos_use::{use_element_size, UseElementSizeReturn, use_scroll, UseScrollReturn};
use crate::components::dashboard_tile::DashboardTile;
use leptos_icons::*;
/// A row of dashboard tiles, with a title
pub struct DashboardRow {
pub title: String,
pub tiles: Vec<Box<dyn DashboardTile>>,
}
impl DashboardRow {
pub fn new(title: String, tiles: Vec<Box<dyn DashboardTile>>) -> Self {
Self {
title,
tiles,
}
}
}
impl IntoView for DashboardRow {
fn into_view(self) -> View {
let list_ref = create_node_ref::<Ul>();
// Scroll functions attempt to align the left edge of the scroll area with the left edge of a tile
// This is done by scrolling to the nearest multiple of the tile width, plus some for padding
let scroll_left = move |_| {
if let Some(scroll_element) = list_ref.get_untracked() {
let client_width = scroll_element.client_width() as f64;
let current_pos = scroll_element.scroll_left() as f64;
let desired_pos = current_pos - client_width;
if let Some(first_tile) = scroll_element.first_element_child() {
let tile_width = first_tile.client_width() as f64;
let scroll_pos = desired_pos + (tile_width - (desired_pos % tile_width));
scroll_element.scroll_to_with_x_and_y(scroll_pos, 0.0);
} else {
warn!("Could not get first tile to scroll left");
// Fall back to scrolling by the client width if we can't get the tile width
scroll_element.scroll_to_with_x_and_y(desired_pos, 0.0);
}
} else {
warn!("Could not get scroll element to scroll left");
}
};
let scroll_right = move |_| {
if let Some(scroll_element) = list_ref.get_untracked() {
let client_width = scroll_element.client_width() as f64;
let current_pos = scroll_element.scroll_left() as f64;
let desired_pos = current_pos + client_width;
if let Some(first_tile) = scroll_element.first_element_child() {
let tile_width = first_tile.client_width() as f64;
let scroll_pos = desired_pos - (desired_pos % tile_width);
scroll_element.scroll_to_with_x_and_y(scroll_pos, 0.0);
} else {
warn!("Could not get first tile to scroll right");
// Fall back to scrolling by the client width if we can't get the tile width
scroll_element.scroll_to_with_x_and_y(desired_pos, 0.0);
}
} else {
warn!("Could not get scroll element to scroll right");
}
};
let UseElementSizeReturn { width: scroll_element_width, .. } = use_element_size(list_ref);
let UseScrollReturn { x: scroll_x, .. } = use_scroll(list_ref);
let scroll_right_hidden = Signal::derive(move || {
if let Some(scroll_element) = list_ref.get() {
if scroll_element.scroll_width() as f64 - scroll_element_width.get() <= scroll_x.get() {
"visibility: hidden"
} else {
""
}
} else {
""
}
});
let scroll_left_hidden = Signal::derive(move || {
if scroll_x.get() <= 0.0 {
"visibility: hidden"
} else {
""
}
});
view! {
<div class="dashboard-tile-row">
<div class="dashboard-tile-row-title-row">
<h2>{self.title}</h2>
<div class="dashboard-tile-row-scroll-btn">
<button on:click=scroll_left tabindex=-1 style=scroll_left_hidden>
<Icon class="dashboard-tile-row-scroll" icon=icondata::FiChevronLeft />
</button>
<button on:click=scroll_right tabindex=-1 style=scroll_right_hidden>
<Icon class="dashboard-tile-row-scroll" icon=icondata::FiChevronRight />
</button>
</div>
</div>
<ul _ref={list_ref}>
{self.tiles.into_iter().map(|tile_info| {
view! {
<li>
{ tile_info.into_view() }
</li>
}
}).collect::<Vec<_>>()}
</ul>
</div>
}.into_view()
}
}

View File

@ -0,0 +1,27 @@
use leptos::leptos_dom::*;
use leptos::*;
pub trait DashboardTile {
fn image_path(&self) -> String;
fn title(&self) -> String;
fn link(&self) -> String;
fn description(&self) -> Option<String> { None }
}
impl IntoView for &dyn DashboardTile {
fn into_view(self) -> View {
let link = self.link();
view! {
<div class="dashboard-tile">
<a href={link}>
<img src={self.image_path()} alt="dashboard-tile" />
<p class="dashboard-tile-title">{self.title()}</p>
<p class="dashboard-tile-description">
{self.description().unwrap_or_default()}
</p>
</a>
</div>
}.into_view()
}
}

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

@ -1,6 +1,8 @@
pub mod app; pub mod app;
pub mod auth; pub mod auth;
pub mod songdata; pub mod songdata;
pub mod albumdata;
pub mod artistdata;
pub mod playstatus; pub mod playstatus;
pub mod playbar; pub mod playbar;
pub mod database; pub mod database;

View File

@ -5,6 +5,7 @@ use crate::api::songs;
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::*;
use leptos_meta::Title;
use leptos::*; use leptos::*;
use leptos_icons::*; use leptos_icons::*;
use leptos_use::{utils::Pausable, use_interval_fn}; use leptos_use::{utils::Pausable, use_interval_fn};
@ -460,6 +461,21 @@ fn QueueToggle(status: RwSignal<PlayStatus>) -> impl IntoView {
} }
} }
/// Renders the title of the page based on the currently playing song
#[component]
pub fn CustomTitle(play_status: RwSignal<PlayStatus>) -> impl IntoView {
let title = create_memo(move |_| {
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")
})
})
});
view! {
<Title text=title />
}
}
/// 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(status: RwSignal<PlayStatus>) -> impl IntoView {

View File

@ -1,4 +1,5 @@
use crate::models::{Album, Artist, Song}; use crate::models::{Album, Artist, Song};
use crate::components::dashboard_tile::DashboardTile;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use time::Date; use time::Date;
@ -6,7 +7,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(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Clone)]
pub struct SongData { pub struct SongData {
/// Song id /// Song id
pub id: i32, pub id: i32,
@ -62,3 +63,21 @@ impl TryInto<Song> for SongData {
}) })
} }
} }
impl DashboardTile for SongData {
fn image_path(&self) -> String {
self.image_path.clone()
}
fn title(&self) -> String {
self.title.clone()
}
fn link(&self) -> String {
format!("/song/{}", self.id)
}
fn description(&self) -> Option<String> {
Some(format!("Song • {}", Artist::display_list(&self.artists)))
}
}

45
style/dashboard_row.scss Normal file
View File

@ -0,0 +1,45 @@
.dashboard-tile-row {
.dashboard-tile-row-title-row {
display: flex;
.dashboard-tile-row-scroll-btn {
margin-left: auto;
margin-top: auto;
margin-bottom: auto;
button {
background-color: transparent;
border: none;
.dashboard-tile-row-scroll {
color: $text-controls-color;
width: 2.5rem;
height: 2.5rem;
}
.dashboard-tile-row-scroll:hover {
color: $controls-hover-color;
}
.dashboard-tile-row-scroll:active {
color: $controls-click-color;
}
}
}
}
ul {
display: flex;
overflow-x: hidden;
scroll-behavior: smooth;
margin-left: 40px;
padding-inline-start: 0;
li {
list-style-type: none;
}
-webkit-mask-image: linear-gradient(90deg, #000000 95%, transparent);
mask-image: linear-gradient(90deg, #000000 95%, transparent);
}
}

28
style/dashboard_tile.scss Normal file
View File

@ -0,0 +1,28 @@
.dashboard-tile {
img {
width: $dashboard-tile-size;
height: $dashboard-tile-size;
border-radius: 7px;
margin-right: 20px;
}
a {
text-decoration: none;
color: $text-controls-color;
}
p.dashboard-tile-title {
font-size: 16px;
font-weight: bold;
margin: 0;
padding: 0;
}
p.dashboard-tile-description {
font-size: 12px;
margin: 0;
padding: 0;
}
margin-right: 10px;
}

View File

@ -8,8 +8,11 @@
@import 'home.scss'; @import 'home.scss';
@import 'search.scss'; @import 'search.scss';
@import 'personal.scss'; @import 'personal.scss';
@import 'dashboard_tile.scss';
@import 'dashboard_row.scss';
@import 'upload.scss'; @import 'upload.scss';
@import 'error.scss'; @import 'error.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);
}
}
}
}

View File

@ -14,3 +14,5 @@ $queue-background-color: $play-bar-background-color;
$auth-inputs: #796dd4; $auth-inputs: #796dd4;
$auth-containers: white; $auth-containers: white;
$dashboard-tile-size: 200px;