diff --git a/Cargo.lock b/Cargo.lock index 079c4ae..44b8a0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" 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, + /// Album release date + pub release_date: Option, + /// 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 { + Some(format!("Album • {}", Artist::display_list(&self.artists))) + } +} 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 { + Some("Artist".to_string()) + } +} diff --git a/src/components.rs b/src/components.rs index ede9b31..29ac621 100644 --- a/src/components.rs +++ b/src/components.rs @@ -2,4 +2,6 @@ pub mod sidebar; pub mod dashboard; pub mod search; pub mod personal; +pub mod dashboard_tile; +pub mod dashboard_row; pub mod upload; diff --git a/src/components/dashboard_row.rs b/src/components/dashboard_row.rs new file mode 100644 index 0000000..7abd258 --- /dev/null +++ b/src/components/dashboard_row.rs @@ -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>, +} + +impl DashboardRow { + pub fn new(title: String, tiles: Vec>) -> Self { + Self { + title, + tiles, + } + } +} + +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_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! { +
    +
    +

    {self.title}

    +
    + + +
    +
    +
      + {self.tiles.into_iter().map(|tile_info| { + view! { +
    • + { tile_info.into_view() } +
    • + } + }).collect::>()} +
    +
    + }.into_view() + } +} diff --git a/src/components/dashboard_tile.rs b/src/components/dashboard_tile.rs new file mode 100644 index 0000000..354aad5 --- /dev/null +++ b/src/components/dashboard_tile.rs @@ -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 { None } +} + +impl IntoView for &dyn DashboardTile { + fn into_view(self) -> View { + let link = self.link(); + + view! { + + }.into_view() + } +} diff --git a/src/lib.rs b/src/lib.rs index 7c48d1b..95ac8ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ pub mod app; pub mod auth; pub mod songdata; +pub mod albumdata; +pub mod artistdata; pub mod playstatus; pub mod playbar; pub mod database; diff --git a/src/songdata.rs b/src/songdata.rs index 61e263f..6851a85 100644 --- a/src/songdata.rs +++ b/src/songdata.rs @@ -1,4 +1,5 @@ use crate::models::{Album, Artist, Song}; +use crate::components::dashboard_tile::DashboardTile; use time::Date; @@ -60,3 +61,21 @@ impl TryInto 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 { + Some(format!("Song • {}", Artist::display_list(&self.artists))) + } +} diff --git a/style/dashboard_row.scss b/style/dashboard_row.scss new file mode 100644 index 0000000..8bbb2c9 --- /dev/null +++ b/style/dashboard_row.scss @@ -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); + } +} diff --git a/style/dashboard_tile.scss b/style/dashboard_tile.scss new file mode 100644 index 0000000..925a985 --- /dev/null +++ b/style/dashboard_tile.scss @@ -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; +} diff --git a/style/main.scss b/style/main.scss index de15da6..1a80b34 100644 --- a/style/main.scss +++ b/style/main.scss @@ -8,6 +8,8 @@ @import 'home.scss'; @import 'search.scss'; @import 'personal.scss'; +@import 'dashboard_tile.scss'; +@import 'dashboard_row.scss'; @import 'upload.scss'; body { 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;