diff --git a/src/albumdata.rs b/src/albumdata.rs index 2b86b25..f1481ae 100644 --- a/src/albumdata.rs +++ b/src/albumdata.rs @@ -1,11 +1,14 @@ use crate::models::Artist; use crate::components::dashboard_tile::DashboardTile; +use serde::{Serialize, Deserialize}; use chrono::NaiveDate; /// Holds information about an album /// /// Intended to be used in the front-end + +#[derive(Serialize, Deserialize, Clone)] pub struct AlbumData { /// Album id pub id: i32, @@ -36,4 +39,4 @@ impl DashboardTile for AlbumData { fn description(&self) -> Option { Some(format!("Album • {}", Artist::display_list(&self.artists))) } -} +} \ No newline at end of file diff --git a/src/api/album.rs b/src/api/album.rs new file mode 100644 index 0000000..5c4d55e --- /dev/null +++ b/src/api/album.rs @@ -0,0 +1,33 @@ +use leptos::*; +use crate::albumdata::AlbumData; +use crate::songdata::SongData; + +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(feature = "ssr")] { + use leptos::server_fn::error::NoCustomError; + use crate::database::get_db_conn; + } +} + +#[server(endpoint = "album/get")] +pub async fn get_album(id: i32) -> Result { + use crate::models::Album; + let db_con = &mut get_db_conn(); + let album = Album::get_album_data(id,db_con) + .map_err(|e| ServerFnError::::ServerError(format!("Error getting album: {}", e)))?; + Ok(album) +} + +#[server(endpoint = "album/get_songs")] +pub async fn get_songs(id: i32) -> Result, ServerFnError> { + use crate::models::Album; + use crate::auth::get_logged_in_user; + let user = get_logged_in_user().await?; + let db_con = &mut get_db_conn(); + // TODO: NEEDS SONG DATA QUERIES + let songdata = Album::get_song_data(id,user,db_con) + .map_err(|e| ServerFnError::::ServerError(format!("Error getting song data: {}", e)))?; + Ok(songdata) +} \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs index c287a07..e9ee6c9 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,3 +1,4 @@ pub mod history; pub mod profile; pub mod songs; +pub mod album; diff --git a/src/app.rs b/src/app.rs index 2058312..4784702 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,6 +7,7 @@ use leptos_router::*; use crate::pages::login::*; use crate::pages::signup::*; use crate::pages::profile::*; +use crate::pages::albumpage::*; use crate::error_template::{AppError, ErrorTemplate}; use crate::util::state::GlobalState; @@ -44,6 +45,7 @@ pub fn App() -> impl IntoView { + diff --git a/src/components.rs b/src/components.rs index 2624877..de281a7 100644 --- a/src/components.rs +++ b/src/components.rs @@ -8,3 +8,4 @@ pub mod upload; pub mod song_list; pub mod loading; pub mod error; +pub mod album_info; \ No newline at end of file diff --git a/src/components/album_info.rs b/src/components/album_info.rs new file mode 100644 index 0000000..182cc75 --- /dev/null +++ b/src/components/album_info.rs @@ -0,0 +1,25 @@ +use leptos::leptos_dom::*; +use leptos::*; +use crate::albumdata::AlbumData; + +#[component] +pub fn AlbumInfo(albumdata: AlbumData) -> impl IntoView { + view! { +
+ dashboard-tile +
+

{albumdata.title}

+
+ { + albumdata.artists.iter().map(|artist| { + view! { + {artist.name.clone()} + } + }).collect::>() + } +
+
+
+ }.into_view() +} + diff --git a/src/models.rs b/src/models.rs index fd3e217..40fd7f9 100644 --- a/src/models.rs +++ b/src/models.rs @@ -6,8 +6,10 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "ssr")] { use diesel::prelude::*; - use crate::database::PgPooledConn; + use crate::database::*; use std::error::Error; + use crate::songdata::SongData; + use crate::albumdata::AlbumData; } } @@ -498,7 +500,7 @@ impl Album { Ok(()) } - /// Get songs by this artist from the database + /// Get songs by this album from the database /// /// The `id` field of this album must be present (Some) to get songs /// @@ -525,6 +527,153 @@ impl Album { Ok(my_songs) } + + /// Obtain an album from its albumid + /// # Arguments + /// + /// * `album_id` - The id of the album to select + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result>` - A result indicating success with the desired album, or an error + /// + #[cfg(feature = "ssr")] + pub fn get_album_data(album_id: i32, conn: &mut PgPooledConn) -> Result> { + use crate::schema::*; + + let album: Vec<(Album, std::option::Option)> = albums::table + .find(album_id) + .left_join(songs::table.on(albums::id.nullable().eq(songs::album_id))) + .left_join(song_artists::table.inner_join(artists::table).on(songs::id.eq(song_artists::song_id))) + .select(( + albums::all_columns, + artists::all_columns.nullable() + )) + .distinct() + .load(conn)?; + + let mut artist_list: Vec = Vec::new(); + + for (_, artist) in album { + if let Some(artist) = artist { + artist_list.push(artist); + } + } + // Get info of album + let albuminfo = albums::table + .filter(albums::id.eq(album_id)) + .first::(conn)?; + + let img = albuminfo.image_path.unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string()); + + let albumdata = AlbumData { + id: albuminfo.id.unwrap(), + title: albuminfo.title, + artists: artist_list, + release_date: albuminfo.release_date, + image_path: img + }; + + Ok(albumdata) + } + + /// Obtain an album from its albumid + /// # Arguments + /// + /// * `album_id` - The id of the album to select + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result>` - A result indicating success with the desired album, or an error + /// + #[cfg(feature = "ssr")] + pub fn get_song_data(album_id: i32, user_like_dislike: Option, conn: &mut PgPooledConn) -> Result, Box> { + use crate::schema::*; + use std::collections::HashMap; + + let song_list = if let Some(user_like_dislike) = user_like_dislike { + let user_like_dislike_id = user_like_dislike.id.unwrap(); + let song_list: Vec<(Album, Option, Option, Option<(i32, i32)>, Option<(i32, i32)>)> = + albums::table + .find(album_id) + .left_join(songs::table.on(albums::id.nullable().eq(songs::album_id))) + .left_join(song_artists::table.inner_join(artists::table).on(songs::id.eq(song_artists::song_id))) + .left_join(song_likes::table.on(songs::id.eq(song_likes::song_id).and(song_likes::user_id.eq(user_like_dislike_id)))) + .left_join(song_dislikes::table.on(songs::id.eq(song_dislikes::song_id).and(song_dislikes::user_id.eq(user_like_dislike_id)))) + .select(( + albums::all_columns, + songs::all_columns.nullable(), + artists::all_columns.nullable(), + song_likes::all_columns.nullable(), + song_dislikes::all_columns.nullable() + )) + .order(songs::track.asc()) + .load(conn)?; + song_list + } else { + let song_list: Vec<(Album, Option, Option)> = + albums::table + .find(album_id) + .left_join(songs::table.on(albums::id.nullable().eq(songs::album_id))) + .left_join(song_artists::table.inner_join(artists::table).on(songs::id.eq(song_artists::song_id))) + .select(( + albums::all_columns, + songs::all_columns.nullable(), + artists::all_columns.nullable() + )) + .order(songs::track.asc()) + .load(conn)?; + + let song_list: Vec<(Album, Option, Option, Option<(i32, i32)>, Option<(i32, i32)>)> = + song_list.into_iter().map( |(album, song, artist)| (album, song, artist, None, None) ).collect(); + song_list + }; + + let mut album_songs: HashMap = HashMap::with_capacity(song_list.len()); + + for (album, song, artist, like, dislike) in song_list { + if let Some(song) = song { + if let Some(stored_songdata) = album_songs.get_mut(&song.id.unwrap()) { + // If the song is already in the map, update the artists + if let Some(artist) = artist { + stored_songdata.artists.push(artist); + } + } else { + let like_dislike = match (like, dislike) { + (Some(_), Some(_)) => Some((true, true)), + (Some(_), None) => Some((true, false)), + (None, Some(_)) => Some((false, true)), + _ => None, + }; + + let image_path = song.image_path.unwrap_or( + album.image_path.clone().unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string())); + + let songdata = SongData { + id: song.id.unwrap(), + title: song.title, + artists: artist.map(|artist| vec![artist]).unwrap_or_default(), + album: Some(album), + track: song.track, + duration: song.duration, + release_date: song.release_date, + song_path: song.storage_path, + image_path: image_path, + like_dislike: like_dislike, + }; + + album_songs.insert(song.id.unwrap(), songdata); + } + } + } + + // Sort the songs by date + let mut songdata: Vec = album_songs.into_values().collect(); + songdata.sort_by(|a, b| b.track.cmp(&a.track)); + Ok(songdata) + } } /// Model for a song diff --git a/src/pages.rs b/src/pages.rs index 1e65f58..0ad5d6f 100644 --- a/src/pages.rs +++ b/src/pages.rs @@ -1,3 +1,4 @@ pub mod login; pub mod signup; pub mod profile; +pub mod albumpage; diff --git a/src/pages/albumpage.rs b/src/pages/albumpage.rs new file mode 100644 index 0000000..f65d71e --- /dev/null +++ b/src/pages/albumpage.rs @@ -0,0 +1,87 @@ +use leptos::leptos_dom::*; +use leptos::*; +use leptos_router::*; +use crate::components::song_list::*; +use crate::api::album::*; +use crate::components::album_info::*; + + +#[derive(Params, PartialEq)] +struct AlbumParams { + id: i32 +} + +#[component] +pub fn AlbumPage() -> impl IntoView { + let params = use_params::(); + + let id = move || {params.with(|params| { + params.as_ref() + .map(|params| params.id) + .map_err(|e| e.clone()) + }) + }; + + let song_list = create_resource( + id, + |value| async move { + match value { + Ok(v) => {get_songs(v).await}, + Err(e) => {Err(ServerFnError::Request(format!("Error getting song data: {}", e).into()))}, + } + }, + ); + + let albumdata = create_resource( + id, + |value| async move { + match value { + Ok(v) => {get_album(v).await}, + Err(e) => {Err(ServerFnError::Request(format!("Error getting song data: {}", e).into()))}, + } + }, + ); + + view! { +
+
+ "Loading..."

} + > + {move || { + albumdata.with( |albumdata| { + match albumdata { + Some(Ok(s)) => { + view! { } + }, + Some(Err(e)) => { + view! {
{format!("Error loading album : {}",e)}
}.into_view() + }, + None => {view! { }.into_view()} + } + }) + }} +
+
+ + "Loading..."

} + > + {move || { + song_list.with( |song_list| { + match song_list { + Some(Ok(s)) => { + view! { } + }, + Some(Err(e)) => { + view! {
{format!("Error loading albums: : {}",e)}
}.into_view() + }, + None => {view! { }.into_view()} + } + }) + }} +
+
+ } +} + diff --git a/src/playbar.rs b/src/playbar.rs index e581101..f8e656a 100644 --- a/src/playbar.rs +++ b/src/playbar.rs @@ -450,7 +450,7 @@ fn QueueToggle() -> impl IntoView { log!("queue button pressed, queue status: {:?}", GlobalState::play_status().with_untracked(|status| status.queue_open)); }; - + // We use this to prevent the buttons from being focused when clicked // If buttons were focused on clicks, then pressing space bar to play/pause would "click" the button // and trigger unwanted behavior diff --git a/style/album_page.scss b/style/album_page.scss new file mode 100644 index 0000000..b63e906 --- /dev/null +++ b/style/album_page.scss @@ -0,0 +1,80 @@ +@import 'theme.scss'; + +.album-page-container { + width: 90vw; + + .album-header { + height: 40vh; + width: 65vw; + margin: auto; + + + padding:20px; + + background-image: linear-gradient($accent-color, $background-color); + border-radius: 15px; + + .album-info { + width: 100%; + height: 100%; + } + } +} + +.album-info { + display: flex; + flex-flow: row nowrap; + justify-content: space-around; + + .album-image { + max-width: 80%; + max-height: 80%; + box-shadow: 10px 10px 50px -10px $background-color; + } + + .album-body { + display: flex; + flex-flow: column nowrap; + justify-content: center; + + .album-title { + color: $text-controls-color; + font-size: 40px; + font-weight: bold; + margin:15px; + text-align: center; + } + + .album-artists { + display: flex; + flex-flow: row wrap; + justify-content: space-around; + align-content: space-around; + margin:15px; + + color: $text-controls-color; + font-size: 20px; + + .album-artist { + margin: 5px; + text-align: center; + text-decoration: underline; + } + } + } + + a { + color: $text-controls-color; + } + a:visited { + color: $text-controls-color; + } + a:hover { + color: $controls-hover-color; + } + + a:active { + color: $controls-click-color; + } + +} \ No newline at end of file diff --git a/style/main.scss b/style/main.scss index a19abe7..e2be23c 100644 --- a/style/main.scss +++ b/style/main.scss @@ -15,6 +15,7 @@ @import 'song_list.scss'; @import 'profile.scss'; @import 'loading.scss'; +@import 'album_page.scss'; body { font-family: sans-serif; diff --git a/style/theme.scss b/style/theme.scss index b0cea43..0569ca6 100644 --- a/style/theme.scss +++ b/style/theme.scss @@ -10,10 +10,11 @@ $controls-click-color: #909090; $play-bar-background-color: #212121; $play-grad-start: #0a0533; $play-grad-end: $accent-color; +$border-color: #7851ed; $queue-background-color: $play-bar-background-color; $auth-inputs: #796dd4; $auth-containers: white; $dashboard-tile-size: 200px; -$playbar-size: 75px; +$playbar-size: 75px; \ No newline at end of file