From eebd58f5a68e3416e54c42c06d91e1eb28144f02 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sat, 11 May 2024 12:53:57 -0400 Subject: [PATCH 01/21] Add MediaType struct --- src/lib.rs | 2 ++ src/media_type.rs | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/media_type.rs diff --git a/src/lib.rs b/src/lib.rs index 89cd04e..23bc098 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,8 @@ pub mod users; pub mod search; pub mod fileserv; pub mod error_template; +pub mod media_type; + use cfg_if::cfg_if; cfg_if! { diff --git a/src/media_type.rs b/src/media_type.rs new file mode 100644 index 0000000..f97c5c2 --- /dev/null +++ b/src/media_type.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; + +/// Differentiates between different types of media +/// Used to display a short text near a corresponging image / title to indicate what type of media it is +#[derive(Serialize, Deserialize)] +pub enum MediaType { + Song, + Album, + Artist, +} + +impl ToString for MediaType { + fn to_string(&self) -> String { + match self { + MediaType::Song => "Song".to_string(), + MediaType::Album => "Album".to_string(), + MediaType::Artist => "Artist".to_string(), + } + } +} From ec43ab472d9896e6114ef95170471724cc32bf17 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sat, 11 May 2024 13:08:05 -0400 Subject: [PATCH 02/21] Add DashboardTile component --- src/components.rs | 3 +- src/components/dashboard_tile.rs | 69 ++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/components/dashboard_tile.rs diff --git a/src/components.rs b/src/components.rs index 4d0c8a5..5f7fff7 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,4 +1,5 @@ pub mod sidebar; pub mod dashboard; pub mod search; -pub mod personal; \ No newline at end of file +pub mod personal; +pub mod dashboard_tile; diff --git a/src/components/dashboard_tile.rs b/src/components/dashboard_tile.rs new file mode 100644 index 0000000..899981f --- /dev/null +++ b/src/components/dashboard_tile.rs @@ -0,0 +1,69 @@ +use leptos::leptos_dom::*; +use leptos::*; +use serde::{Deserialize, Serialize}; +use crate::media_type::MediaType; + +/// Info representing what will be displayed in a dashboard tile +#[derive(Serialize, Deserialize)] +pub struct DashboardTile { + pub image_path: String, + pub title: String, + pub media_type: Option, + pub artist: Option, +} + +impl DashboardTile { + pub fn new(image_path: String, title: String, media_type: Option, artist: Option) -> Self { + Self { + image_path, + title, + media_type, + artist: artist.map(|artist| artist.to_string()), + } + } + + /// Get the description of the dashboard tile + /// Will display the media type, and the artist if it is available and relevant + pub fn description(&self) -> String { + match self.media_type { + Some(MediaType::Song) => { + if let Some(artist) = &self.artist { + format!("{} • {}", MediaType::Song.to_string(), artist) + } else { + MediaType::Song.to_string() + } + }, + Some(MediaType::Album) => { + if let Some(artist) = &self.artist { + format!("{} • {}", MediaType::Album.to_string(), artist) + } else { + MediaType::Album.to_string() + } + }, + Some(MediaType::Artist) => { + MediaType::Artist.to_string() + }, + None => { + if let Some(artist) = &self.artist { + artist.to_string() + } else { + "".to_string() + } + } + } + } +} + +impl IntoView for DashboardTile { + fn into_view(self) -> View { + let description = self.description(); + + view! { +
+ dashboard-tile +

{self.title}

+

{description}

+
+ }.into_view() + } +} From b1cc0f156c2bad7c2070b0da2e02011c8caeb914 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sat, 11 May 2024 13:08:32 -0400 Subject: [PATCH 03/21] Add styling for dashboard tile --- style/dashboard_tile.scss | 23 +++++++++++++++++++++++ style/main.scss | 1 + style/theme.scss | 2 ++ 3 files changed, 26 insertions(+) create mode 100644 style/dashboard_tile.scss diff --git a/style/dashboard_tile.scss b/style/dashboard_tile.scss new file mode 100644 index 0000000..6a25a21 --- /dev/null +++ b/style/dashboard_tile.scss @@ -0,0 +1,23 @@ +.dashboard-tile { + img { + width: $dashboard-tile-size; + height: $dashboard-tile-size; + border-radius: 7px; + margin-right: 20px; + } + + 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; +} diff --git a/style/main.scss b/style/main.scss index 7793e25..ea1d847 100644 --- a/style/main.scss +++ b/style/main.scss @@ -8,6 +8,7 @@ @import 'home.scss'; @import 'search.scss'; @import 'personal.scss'; +@import 'dashboard_tile.scss'; body { font-family: sans-serif; diff --git a/style/theme.scss b/style/theme.scss index a7ebc14..fe96046 100644 --- a/style/theme.scss +++ b/style/theme.scss @@ -14,3 +14,5 @@ $queue-background-color: $play-bar-background-color; $auth-inputs: #796dd4; $auth-containers: white; + +$dashboard-tile-size: 200px; From f4908ad3b301658b8e1df8bde71bb3f08c8c19fa Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sat, 11 May 2024 13:16:02 -0400 Subject: [PATCH 04/21] Add DashboardRow component --- src/components.rs | 1 + src/components/dashboard_row.rs | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/components/dashboard_row.rs diff --git a/src/components.rs b/src/components.rs index 5f7fff7..6f919fa 100644 --- a/src/components.rs +++ b/src/components.rs @@ -3,3 +3,4 @@ pub mod dashboard; pub mod search; pub mod personal; pub mod dashboard_tile; +pub mod dashboard_row; diff --git a/src/components/dashboard_row.rs b/src/components/dashboard_row.rs new file mode 100644 index 0000000..2917b86 --- /dev/null +++ b/src/components/dashboard_row.rs @@ -0,0 +1,39 @@ +use leptos::leptos_dom::*; +use leptos::*; +use serde::{Deserialize, Serialize}; +use crate::components::dashboard_tile::DashboardTile; + +/// A row of dashboard tiles, with a title +#[derive(Serialize, Deserialize)] +pub struct DashboardRow { + pub title: String, + pub tiles: Vec, +} + +impl DashboardRow { + pub fn new(title: String, tiles: Vec) -> Self { + Self { + title, + tiles, + } + } +} + +impl IntoView for DashboardRow { + fn into_view(self) -> View { + view! { +
+

{self.title}

+
    + {self.tiles.into_iter().map(|tile_info| { + view! { +
  • + { tile_info } +
  • + } + }).collect::>()} +
+
+ }.into_view() + } +} From 683f979bc7ce26b0644be59d226e655c10468af2 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sat, 11 May 2024 13:16:31 -0400 Subject: [PATCH 05/21] Add styling for dashboard row --- style/dashboard_row.scss | 12 ++++++++++++ style/main.scss | 1 + 2 files changed, 13 insertions(+) create mode 100644 style/dashboard_row.scss diff --git a/style/dashboard_row.scss b/style/dashboard_row.scss new file mode 100644 index 0000000..2eb1041 --- /dev/null +++ b/style/dashboard_row.scss @@ -0,0 +1,12 @@ +.dashboard-tile-row { + ul { + display: flex; + + li { + list-style-type: none; + } + + -webkit-mask-image: linear-gradient(90deg, #000000 95%, transparent); + mask-image: linear-gradient(90deg, #000000 95%, transparent); + } +} diff --git a/style/main.scss b/style/main.scss index ea1d847..841e88a 100644 --- a/style/main.scss +++ b/style/main.scss @@ -9,6 +9,7 @@ @import 'search.scss'; @import 'personal.scss'; @import 'dashboard_tile.scss'; +@import 'dashboard_row.scss'; body { font-family: sans-serif; From af66381f5f354afd6a9d9b44196292af2755be28 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sat, 11 May 2024 15:23:49 -0400 Subject: [PATCH 06/21] Implement dashboard row sideways scrolling --- src/components/dashboard_row.rs | 61 +++++++++++++++++++++++++++++++-- style/dashboard_row.scss | 31 +++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/components/dashboard_row.rs b/src/components/dashboard_row.rs index 2917b86..4134161 100644 --- a/src/components/dashboard_row.rs +++ b/src/components/dashboard_row.rs @@ -1,7 +1,9 @@ +use leptos::html::Ul; use leptos::leptos_dom::*; use leptos::*; use serde::{Deserialize, Serialize}; use crate::components::dashboard_tile::DashboardTile; +use leptos_icons::*; /// A row of dashboard tiles, with a title #[derive(Serialize, Deserialize)] @@ -21,10 +23,65 @@ impl DashboardRow { impl IntoView for DashboardRow { fn into_view(self) -> View { + let list_ref = create_node_ref::
    (); + + // 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() { + 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)) + 15.0; + 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() { + 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) + 15.0; + 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"); + } + }; + view! {
    -

    {self.title}

    -
      +
      +

      {self.title}

      +
      + + +
      +
      +
        {self.tiles.into_iter().map(|tile_info| { view! {
      • diff --git a/style/dashboard_row.scss b/style/dashboard_row.scss index 2eb1041..1dea8d9 100644 --- a/style/dashboard_row.scss +++ b/style/dashboard_row.scss @@ -1,6 +1,37 @@ .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; li { list-style-type: none; From 7f4108e1c5c539bfd85efd3784d20f5beb8d02f0 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Tue, 30 Jul 2024 21:02:48 -0400 Subject: [PATCH 07/21] Add conversion from SongData to DashboardTile --- src/songdata.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/songdata.rs b/src/songdata.rs index 23ce415..9e08942 100644 --- a/src/songdata.rs +++ b/src/songdata.rs @@ -1,4 +1,6 @@ use crate::models::{Album, Artist, Song}; +use crate::components::dashboard_tile::DashboardTile; +use crate::media_type::MediaType; use time::Date; @@ -95,3 +97,14 @@ impl TryInto for SongData { }) } } + +impl Into for SongData { + fn into(self) -> DashboardTile { + DashboardTile { + image_path: self.image_path, + title: self.title, + media_type: Some(MediaType::Song), + artist: Some(Artist::display_list(&self.artists)), + } + } +} From fa811350ae3b2f72faa8dd39b5759bdef59d23f8 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Tue, 30 Jul 2024 21:06:23 -0400 Subject: [PATCH 08/21] Add leptos_use crate --- Cargo.lock | 132 +++++++++++++++++++++++++++++++++++++++++++++++++---- Cargo.toml | 2 + 2 files changed, 124 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1bc5c0a..0a6bd5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -308,7 +308,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c878c71c2821aa2058722038a59a67583a4240524687c6028571c9b395ded61f" dependencies = [ - "darling", + "darling 0.14.4", "proc-macro2", "quote", "syn 1.0.109", @@ -377,6 +377,15 @@ dependencies = [ "half", ] +[[package]] +name = "codee" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af40247be877a1e3353fb406aa27ab3ef4bd3ff18cef91e75e667bfa3fde701d" +dependencies = [ + "thiserror", +] + [[package]] name = "collection_literals" version = "1.0.1" @@ -499,8 +508,18 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core 0.20.10", + "darling_macro 0.20.10", ] [[package]] @@ -513,21 +532,46 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn 1.0.109", ] +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.58", +] + [[package]] name = "darling_macro" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ - "darling_core", + "darling_core 0.14.4", "quote", "syn 1.0.109", ] +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core 0.20.10", + "quote", + "syn 2.0.58", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -541,6 +585,18 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "default-struct-builder" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fa90da96b8fd491f5754d1f7a731f73921e3b7aa0ce333c821a0e43666ac14" +dependencies = [ + "darling 0.20.10", + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "deranged" version = "0.3.11" @@ -862,7 +918,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-sink", - "gloo-utils", + "gloo-utils 0.2.0 (git+https://github.com/rustwasm/gloo.git?rev=a823fab7ecc4068e9a28bd669da5eaf3f0a56380)", "http", "js-sys", "pin-project", @@ -874,6 +930,31 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gloo-utils" version = "0.2.0" @@ -1338,6 +1419,30 @@ dependencies = [ "web-sys", ] +[[package]] +name = "leptos-use" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac79c02d0e2998569116aa36d26fd00bfa8cadbe8cb630eb771b4d1676412a16" +dependencies = [ + "async-trait", + "cfg-if", + "codee", + "cookie", + "default-struct-builder", + "futures-util", + "gloo-timers", + "gloo-utils 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys", + "lazy_static", + "leptos", + "paste", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "leptos_axum" version = "0.6.10" @@ -1587,6 +1692,7 @@ dependencies = [ "icondata", "lazy_static", "leptos", + "leptos-use", "leptos_axum", "leptos_icons", "leptos_meta", @@ -2477,6 +2583,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.5.0" @@ -2580,18 +2692,18 @@ checksum = "384595c11a4e2969895cad5a8c4029115f5ab956a9e5ef4de79d11a426e5f20c" [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 953da57..b162619 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ multer = { version = "3.0.0", optional = true } log = { version = "0.4.21", optional = true } flexi_logger = { version = "0.28.0", optional = true, default-features = false } web-sys = "0.3.69" +leptos-use = "0.11.3" [patch.crates-io] gloo-net = { git = "https://github.com/rustwasm/gloo.git", rev = "a823fab7ecc4068e9a28bd669da5eaf3f0a56380" } @@ -74,6 +75,7 @@ ssr = [ "multer", "log", "flexi_logger", + "leptos-use/ssr", ] # Defines a size-optimized profile for the WASM bundle in release mode From 9d6013b8a4d8a86d02782d49108fa6885afd0446 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Tue, 30 Jul 2024 21:35:44 -0400 Subject: [PATCH 09/21] Move dashboard tile row left spacing to margin-left --- src/components/dashboard_row.rs | 4 ++-- style/dashboard_row.scss | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/dashboard_row.rs b/src/components/dashboard_row.rs index 4134161..10f1ee9 100644 --- a/src/components/dashboard_row.rs +++ b/src/components/dashboard_row.rs @@ -36,7 +36,7 @@ impl IntoView for DashboardRow { 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)) + 15.0; + 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"); @@ -56,7 +56,7 @@ impl IntoView for DashboardRow { 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) + 15.0; + 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"); diff --git a/style/dashboard_row.scss b/style/dashboard_row.scss index 1dea8d9..8bbb2c9 100644 --- a/style/dashboard_row.scss +++ b/style/dashboard_row.scss @@ -32,6 +32,8 @@ display: flex; overflow-x: hidden; scroll-behavior: smooth; + margin-left: 40px; + padding-inline-start: 0; li { list-style-type: none; From eab011070d7975b228347ef9cc91533293ce480a Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Tue, 30 Jul 2024 21:46:59 -0400 Subject: [PATCH 10/21] Don't track scroll_element in scroll button handlers --- src/components/dashboard_row.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/dashboard_row.rs b/src/components/dashboard_row.rs index 10f1ee9..a85159c 100644 --- a/src/components/dashboard_row.rs +++ b/src/components/dashboard_row.rs @@ -29,7 +29,7 @@ impl IntoView for DashboardRow { // 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() { + 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; @@ -49,7 +49,7 @@ impl IntoView for DashboardRow { }; let scroll_right = move |_| { - if let Some(scroll_element) = list_ref.get() { + 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; From 53cde3bed6c4051f137574628e12aa820bd46eae Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Tue, 30 Jul 2024 21:47:55 -0400 Subject: [PATCH 11/21] Hide scroll buttons when at end of scroll bar --- src/components/dashboard_row.rs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/components/dashboard_row.rs b/src/components/dashboard_row.rs index a85159c..1690d6d 100644 --- a/src/components/dashboard_row.rs +++ b/src/components/dashboard_row.rs @@ -1,6 +1,7 @@ use leptos::html::Ul; use leptos::leptos_dom::*; use leptos::*; +use leptos_use::{use_element_size, UseElementSizeReturn, use_scroll, UseScrollReturn}; use serde::{Deserialize, Serialize}; use crate::components::dashboard_tile::DashboardTile; use leptos_icons::*; @@ -68,15 +69,38 @@ impl IntoView for DashboardRow { } }; + 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! {

        {self.title}

        - -
        From 2c71e065b7c1a3bff400824daa5738dd3d5c141b Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 4 Oct 2024 15:14:32 -0400 Subject: [PATCH 12/21] Update tower and tower-http Remove gloo-net override --- Cargo.lock | 77 ++++++++++++++++++++++++++++++++++++------------------ Cargo.toml | 7 ++--- 2 files changed, 54 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 23984ed..079c4ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,7 +151,7 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http", + "http 1.1.0", "http-body", "http-body-util", "hyper", @@ -167,7 +167,7 @@ dependencies = [ "serde", "sync_wrapper 1.0.0", "tokio", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", ] @@ -181,7 +181,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", + "http 1.1.0", "http-body", "http-body-util", "mime", @@ -1017,13 +1017,14 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "gloo-net" 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 = [ "futures-channel", "futures-core", "futures-sink", "gloo-utils", - "http", + "http 0.2.12", "js-sys", "pin-project", "serde", @@ -1049,7 +1050,8 @@ dependencies = [ [[package]] name = "gloo-utils" 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 = [ "js-sys", "serde", @@ -1117,6 +1119,17 @@ dependencies = [ "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]] name = "http" version = "1.1.0" @@ -1135,7 +1148,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", - "http", + "http 1.1.0", ] [[package]] @@ -1146,7 +1159,7 @@ checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" dependencies = [ "bytes", "futures-core", - "http", + "http 1.1.0", "http-body", "pin-project-lite", ] @@ -1178,7 +1191,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", + "http 1.1.0", "http-body", "httparse", "httpdate", @@ -1196,7 +1209,7 @@ checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ "bytes", "futures-util", - "http", + "http 1.1.0", "http-body", "hyper", "pin-project-lite", @@ -1826,7 +1839,7 @@ dependencies = [ "diesel_migrations", "dotenv", "flexi_logger", - "http", + "http 1.1.0", "icondata", "image-convert", "lazy_static", @@ -1846,7 +1859,7 @@ dependencies = [ "thiserror", "time", "tokio", - "tower", + "tower 0.5.1", "tower-http", "tower-sessions-redis-store", "wasm-bindgen", @@ -2023,7 +2036,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http", + "http 1.1.0", "httparse", "log", "memchr", @@ -2670,7 +2683,7 @@ dependencies = [ "dashmap", "futures", "gloo-net", - "http", + "http 1.1.0", "http-body-util", "hyper", "inventory", @@ -2683,7 +2696,7 @@ dependencies = [ "serde_qs", "server_fn_macro_default", "thiserror", - "tower", + "tower 0.4.13", "tower-layer", "url", "wasm-bindgen", @@ -3109,6 +3122,20 @@ dependencies = [ "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]] name = "tower-cookies" version = "0.10.0" @@ -3119,7 +3146,7 @@ dependencies = [ "axum-core", "cookie", "futures-util", - "http", + "http 1.1.0", "parking_lot", "pin-project-lite", "tower-layer", @@ -3128,14 +3155,14 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97" dependencies = [ "bitflags 2.5.0", "bytes", "futures-util", - "http", + "http 1.1.0", "http-body", "http-body-util", "http-range-header", @@ -3153,15 +3180,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tower-sessions" @@ -3170,7 +3197,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b27326208b21807803c5f5aa1020d30ca0432b78cfe251b51a67a05e0baea102" dependencies = [ "async-trait", - "http", + "http 1.1.0", "time", "tokio", "tower-cookies", @@ -3191,7 +3218,7 @@ dependencies = [ "axum-core", "base64", "futures", - "http", + "http 1.1.0", "parking_lot", "rand", "serde", diff --git a/Cargo.toml b/Cargo.toml index f76ef2f..2bdcda4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,8 +28,8 @@ diesel_migrations = { version = "2.1.0", optional = true } pbkdf2 = { version = "0.12.2", features = ["simple"], optional = true } tokio = { version = "1", optional = true, features = ["rt-multi-thread"] } axum = { version = "0.7.5", features = ["tokio", "http1"], default-features = false, optional = true } -tower = { version = "0.4.13", optional = true } -tower-http = { version = "0.5", optional = true, features = ["fs"] } +tower = { version = "0.5.1", optional = true, features = ["util"] } +tower-http = { version = "0.6.1", optional = true, features = ["fs"] } thiserror = "1.0.57" tower-sessions-redis-store = { version = "0.11", optional = true } async-trait = { version = "0.1.79", optional = true } @@ -43,9 +43,6 @@ web-sys = "0.3.69" leptos-use = "0.13.5" 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] hydrate = [ "leptos/hydrate", From 2bb9b3bdd74e535464a7fdd7cb6ce74e5c6d8ef5 Mon Sep 17 00:00:00 2001 From: Aidan Westphal Date: Fri, 4 Oct 2024 23:19:07 +0000 Subject: [PATCH 13/21] Page Title Updates on Playing Song --- src/app.rs | 3 ++- src/playbar.rs | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index 67a6c7a..ed9fd1b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,5 @@ use crate::playbar::PlayBar; +use crate::playbar::CustomTitle; use crate::playstatus::PlayStatus; use crate::queue::Queue; use leptos::*; @@ -24,7 +25,7 @@ pub fn App() -> impl IntoView { // sets the document title - + <CustomTitle play_status=play_status/> // content for this welcome page <Router fallback=|| { diff --git a/src/playbar.rs b/src/playbar.rs index f0055e4..69ac98f 100644 --- a/src/playbar.rs +++ b/src/playbar.rs @@ -5,6 +5,7 @@ use crate::api::songs; use leptos::ev::MouseEvent; use leptos::html::{Audio, Div}; use leptos::leptos_dom::*; +use leptos_meta::Title; use leptos::*; use leptos_icons::*; use leptos_use::{utils::Pausable, use_interval_fn}; @@ -460,6 +461,22 @@ 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| { + match play_status.queue.front() { + Some(song_data) => song_data.title.clone(), + None => "LibreTunes".to_owned(), + } + }) + }); + view! { + <Title text=title /> + } +} + /// The main play bar component, containing the progress bar, media info, play controls, and play duration #[component] pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView { From 51858593c26d53a593befd12d39815079558052a Mon Sep 17 00:00:00 2001 From: Aidan Westphal <kylesteine1@gmail.com> Date: Fri, 4 Oct 2024 23:56:12 +0000 Subject: [PATCH 14/21] Updated Title Display --- src/playbar.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/playbar.rs b/src/playbar.rs index 69ac98f..d113b7a 100644 --- a/src/playbar.rs +++ b/src/playbar.rs @@ -466,12 +466,11 @@ fn QueueToggle(status: RwSignal<PlayStatus>) -> impl IntoView { pub fn CustomTitle(play_status: RwSignal<PlayStatus>) -> impl IntoView { let title = create_memo(move |_| { play_status.with(|play_status| { - match play_status.queue.front() { - Some(song_data) => song_data.title.clone(), - None => "LibreTunes".to_owned(), - } - }) - }); + 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 /> } From 553e24800b84b56d5acf6709e10e397b95225017 Mon Sep 17 00:00:00 2001 From: Ethan Girouard <ethan@girouard.com> Date: Sun, 6 Oct 2024 15:35:18 -0400 Subject: [PATCH 15/21] Refactor DashboardTile into trait --- src/components/dashboard_row.rs | 8 ++-- src/components/dashboard_tile.rs | 70 +++++++------------------------- 2 files changed, 17 insertions(+), 61 deletions(-) diff --git a/src/components/dashboard_row.rs b/src/components/dashboard_row.rs index 1690d6d..7abd258 100644 --- a/src/components/dashboard_row.rs +++ b/src/components/dashboard_row.rs @@ -2,19 +2,17 @@ use leptos::html::Ul; use leptos::leptos_dom::*; use leptos::*; use leptos_use::{use_element_size, UseElementSizeReturn, use_scroll, UseScrollReturn}; -use serde::{Deserialize, Serialize}; use crate::components::dashboard_tile::DashboardTile; use leptos_icons::*; /// A row of dashboard tiles, with a title -#[derive(Serialize, Deserialize)] pub struct DashboardRow { pub title: String, - pub tiles: Vec<DashboardTile>, + pub tiles: Vec<Box<dyn DashboardTile>>, } impl DashboardRow { - pub fn new(title: String, tiles: Vec<DashboardTile>) -> Self { + pub fn new(title: String, tiles: Vec<Box<dyn DashboardTile>>) -> Self { Self { title, tiles, @@ -109,7 +107,7 @@ impl IntoView for DashboardRow { {self.tiles.into_iter().map(|tile_info| { view! { <li> - { tile_info } + { tile_info.into_view() } </li> } }).collect::<Vec<_>>()} diff --git a/src/components/dashboard_tile.rs b/src/components/dashboard_tile.rs index 899981f..354aad5 100644 --- a/src/components/dashboard_tile.rs +++ b/src/components/dashboard_tile.rs @@ -1,68 +1,26 @@ use leptos::leptos_dom::*; use leptos::*; -use serde::{Deserialize, Serialize}; -use crate::media_type::MediaType; -/// Info representing what will be displayed in a dashboard tile -#[derive(Serialize, Deserialize)] -pub struct DashboardTile { - pub image_path: String, - pub title: String, - pub media_type: Option<MediaType>, - pub artist: Option<String>, +pub trait DashboardTile { + fn image_path(&self) -> String; + fn title(&self) -> String; + fn link(&self) -> String; + fn description(&self) -> Option<String> { None } } -impl DashboardTile { - pub fn new(image_path: String, title: String, media_type: Option<MediaType>, artist: Option<String>) -> Self { - Self { - image_path, - title, - media_type, - artist: artist.map(|artist| artist.to_string()), - } - } - - /// Get the description of the dashboard tile - /// Will display the media type, and the artist if it is available and relevant - pub fn description(&self) -> String { - match self.media_type { - Some(MediaType::Song) => { - if let Some(artist) = &self.artist { - format!("{} • {}", MediaType::Song.to_string(), artist) - } else { - MediaType::Song.to_string() - } - }, - Some(MediaType::Album) => { - if let Some(artist) = &self.artist { - format!("{} • {}", MediaType::Album.to_string(), artist) - } else { - MediaType::Album.to_string() - } - }, - Some(MediaType::Artist) => { - MediaType::Artist.to_string() - }, - None => { - if let Some(artist) = &self.artist { - artist.to_string() - } else { - "".to_string() - } - } - } - } -} - -impl IntoView for DashboardTile { +impl IntoView for &dyn DashboardTile { fn into_view(self) -> View { - let description = self.description(); + let link = self.link(); view! { <div class="dashboard-tile"> - <img src={self.image_path} alt="dashboard-tile" /> - <p class="dashboard-tile-title">{self.title}</p> - <p class="dashboard-tile-description">{description}</p> + <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() } From f02a22d80597f918ec2a6cf60523fe45f1321c65 Mon Sep 17 00:00:00 2001 From: Ethan Girouard <ethan@girouard.com> Date: Sun, 6 Oct 2024 15:35:52 -0400 Subject: [PATCH 16/21] Format text inside dashboard tiles normally --- style/dashboard_tile.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/style/dashboard_tile.scss b/style/dashboard_tile.scss index 6a25a21..925a985 100644 --- a/style/dashboard_tile.scss +++ b/style/dashboard_tile.scss @@ -6,6 +6,11 @@ margin-right: 20px; } + a { + text-decoration: none; + color: $text-controls-color; + } + p.dashboard-tile-title { font-size: 16px; font-weight: bold; From 097b1fc491f35d4fa8198cdd16e414ee1eacdb1d Mon Sep 17 00:00:00 2001 From: Ethan Girouard <ethan@girouard.com> Date: Sun, 6 Oct 2024 15:37:26 -0400 Subject: [PATCH 17/21] impl DashboardTile for Songdata --- src/songdata.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/songdata.rs b/src/songdata.rs index 99464c8..6851a85 100644 --- a/src/songdata.rs +++ b/src/songdata.rs @@ -1,6 +1,5 @@ use crate::models::{Album, Artist, Song}; use crate::components::dashboard_tile::DashboardTile; -use crate::media_type::MediaType; use time::Date; @@ -63,13 +62,20 @@ impl TryInto<Song> for SongData { } } -impl Into<DashboardTile> for SongData { - fn into(self) -> DashboardTile { - DashboardTile { - image_path: self.image_path, - title: self.title, - media_type: Some(MediaType::Song), - artist: Some(Artist::display_list(&self.artists)), - } +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))) } } From c18bf277b94995dab53c1fa08a93b59e6cb92d29 Mon Sep 17 00:00:00 2001 From: Ethan Girouard <ethan@girouard.com> Date: Sun, 6 Oct 2024 15:49:08 -0400 Subject: [PATCH 18/21] Create AlbumData, impl DashboardTile --- src/albumdata.rs | 39 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 40 insertions(+) create mode 100644 src/albumdata.rs diff --git a/src/albumdata.rs b/src/albumdata.rs new file mode 100644 index 0000000..e42baea --- /dev/null +++ b/src/albumdata.rs @@ -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))) + } +} diff --git a/src/lib.rs b/src/lib.rs index 19d61b2..a347039 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod app; pub mod auth; pub mod songdata; +pub mod albumdata; pub mod playstatus; pub mod playbar; pub mod database; From c678d93661d4852cbcdee06bdd75eee24fc9ec49 Mon Sep 17 00:00:00 2001 From: Ethan Girouard <ethan@girouard.com> Date: Sun, 6 Oct 2024 15:49:19 -0400 Subject: [PATCH 19/21] Create ArtistData, impl DashboardTile --- src/artistdata.rs | 32 ++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 33 insertions(+) create mode 100644 src/artistdata.rs diff --git a/src/artistdata.rs b/src/artistdata.rs new file mode 100644 index 0000000..e799679 --- /dev/null +++ b/src/artistdata.rs @@ -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()) + } +} diff --git a/src/lib.rs b/src/lib.rs index a347039..474fb99 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod app; pub mod auth; pub mod songdata; pub mod albumdata; +pub mod artistdata; pub mod playstatus; pub mod playbar; pub mod database; From 134b425ce603e02930e4077499ac29f346f37032 Mon Sep 17 00:00:00 2001 From: Ethan Girouard <ethan@girouard.com> Date: Sun, 6 Oct 2024 15:50:21 -0400 Subject: [PATCH 20/21] Remove unused MediaType --- src/lib.rs | 1 - src/media_type.rs | 20 -------------------- 2 files changed, 21 deletions(-) delete mode 100644 src/media_type.rs diff --git a/src/lib.rs b/src/lib.rs index 474fb99..95ac8ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,6 @@ pub mod users; pub mod search; pub mod fileserv; pub mod error_template; -pub mod media_type; pub mod api; pub mod upload; pub mod util; diff --git a/src/media_type.rs b/src/media_type.rs deleted file mode 100644 index f97c5c2..0000000 --- a/src/media_type.rs +++ /dev/null @@ -1,20 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// Differentiates between different types of media -/// Used to display a short text near a corresponging image / title to indicate what type of media it is -#[derive(Serialize, Deserialize)] -pub enum MediaType { - Song, - Album, - Artist, -} - -impl ToString for MediaType { - fn to_string(&self) -> String { - match self { - MediaType::Song => "Song".to_string(), - MediaType::Album => "Album".to_string(), - MediaType::Artist => "Artist".to_string(), - } - } -} From 0550b18d7720755edfa75c3a1763449fb4941059 Mon Sep 17 00:00:00 2001 From: Ethan Girouard <ethan@girouard.com> Date: Fri, 11 Oct 2024 13:22:57 -0400 Subject: [PATCH 21/21] Add SongList component --- src/components.rs | 1 + src/components/song_list.rs | 157 ++++++++++++++++++++++++++++++++++++ src/songdata.rs | 1 + style/main.scss | 1 + style/song_list.scss | 124 ++++++++++++++++++++++++++++ 5 files changed, 284 insertions(+) create mode 100644 src/components/song_list.rs create mode 100644 style/song_list.scss diff --git a/src/components.rs b/src/components.rs index 29ac621..893727c 100644 --- a/src/components.rs +++ b/src/components.rs @@ -5,3 +5,4 @@ pub mod personal; pub mod dashboard_tile; pub mod dashboard_row; pub mod upload; +pub mod song_list; diff --git a/src/components/song_list.rs b/src/components/song_list.rs new file mode 100644 index 0000000..2ace92a --- /dev/null +++ b/src/components/song_list.rs @@ -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> + } +} diff --git a/src/songdata.rs b/src/songdata.rs index 6851a85..36e5679 100644 --- a/src/songdata.rs +++ b/src/songdata.rs @@ -6,6 +6,7 @@ use time::Date; /// 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. +#[derive(Clone)] pub struct SongData { /// Song id pub id: i32, diff --git a/style/main.scss b/style/main.scss index 1a80b34..042e605 100644 --- a/style/main.scss +++ b/style/main.scss @@ -11,6 +11,7 @@ @import 'dashboard_tile.scss'; @import 'dashboard_row.scss'; @import 'upload.scss'; +@import 'song_list.scss'; body { font-family: sans-serif; diff --git a/style/song_list.scss b/style/song_list.scss new file mode 100644 index 0000000..88904ab --- /dev/null +++ b/style/song_list.scss @@ -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); + } + } + } +}