Merge branch '36-implement-likes-and-dislikes' into 'main'
Implement likes and dislikes Closes #36 See merge request libretunes/libretunes!32
This commit is contained in:
commit
7cb556e5ef
@ -0,0 +1,2 @@
|
|||||||
|
DROP TABLE song_likes;
|
||||||
|
DROP TABLE song_dislikes;
|
@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE song_likes (
|
||||||
|
song_id INTEGER REFERENCES songs(id) ON DELETE CASCADE NOT NULL,
|
||||||
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL,
|
||||||
|
PRIMARY KEY (song_id, user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE song_dislikes (
|
||||||
|
song_id INTEGER REFERENCES songs(id) ON DELETE CASCADE NOT NULL,
|
||||||
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL,
|
||||||
|
PRIMARY KEY (song_id, user_id)
|
||||||
|
);
|
1
src/api/mod.rs
Normal file
1
src/api/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod songs;
|
55
src/api/songs.rs
Normal file
55
src/api/songs.rs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
use leptos::*;
|
||||||
|
|
||||||
|
use cfg_if::cfg_if;
|
||||||
|
|
||||||
|
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "ssr")] {
|
||||||
|
use leptos::server_fn::error::NoCustomError;
|
||||||
|
use crate::database::get_db_conn;
|
||||||
|
use crate::auth::get_user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like or unlike a song
|
||||||
|
#[server(endpoint = "songs/set_like")]
|
||||||
|
pub async fn set_like_song(song_id: i32, like: bool) -> Result<(), ServerFnError> {
|
||||||
|
let user = get_user().await.map_err(|e| ServerFnError::<NoCustomError>::
|
||||||
|
ServerError(format!("Error getting user: {}", e)))?;
|
||||||
|
|
||||||
|
let db_con = &mut get_db_conn();
|
||||||
|
|
||||||
|
user.set_like_song(song_id, like, db_con).await.map_err(|e| ServerFnError::<NoCustomError>::
|
||||||
|
ServerError(format!("Error liking song: {}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dislike or remove dislike from a song
|
||||||
|
#[server(endpoint = "songs/set_dislike")]
|
||||||
|
pub async fn set_dislike_song(song_id: i32, dislike: bool) -> Result<(), ServerFnError> {
|
||||||
|
let user = get_user().await.map_err(|e| ServerFnError::<NoCustomError>::
|
||||||
|
ServerError(format!("Error getting user: {}", e)))?;
|
||||||
|
|
||||||
|
let db_con = &mut get_db_conn();
|
||||||
|
|
||||||
|
user.set_dislike_song(song_id, dislike, db_con).await.map_err(|e| ServerFnError::<NoCustomError>::
|
||||||
|
ServerError(format!("Error disliking song: {}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the like and dislike status of a song
|
||||||
|
#[server(endpoint = "songs/get_like_dislike")]
|
||||||
|
pub async fn get_like_dislike_song(song_id: i32) -> Result<(bool, bool), ServerFnError> {
|
||||||
|
let user = get_user().await.map_err(|e| ServerFnError::<NoCustomError>::
|
||||||
|
ServerError(format!("Error getting user: {}", e)))?;
|
||||||
|
|
||||||
|
let db_con = &mut get_db_conn();
|
||||||
|
|
||||||
|
// TODO this could probably be done more efficiently with a tokio::try_join, but
|
||||||
|
// doing so is much more complicated than it would initially seem
|
||||||
|
|
||||||
|
let like = user.get_like_song(song_id, db_con).await.map_err(|e| ServerFnError::<NoCustomError>::
|
||||||
|
ServerError(format!("Error getting song liked: {}", e)))?;
|
||||||
|
let dislike = user.get_dislike_song(song_id, db_con).await.map_err(|e| ServerFnError::<NoCustomError>::
|
||||||
|
ServerError(format!("Error getting song disliked: {}", e)))?;
|
||||||
|
|
||||||
|
Ok((like, dislike))
|
||||||
|
}
|
23
src/auth.rs
23
src/auth.rs
@ -122,6 +122,29 @@ pub async fn require_auth() -> Result<(), ServerFnError> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the current logged-in user
|
||||||
|
/// Returns a Result with the user if they are logged in
|
||||||
|
/// Returns an error if the user is not logged in, or if there is an error getting the user
|
||||||
|
/// Intended to be used in a route to get the current user:
|
||||||
|
/// ```rust
|
||||||
|
/// use leptos::*;
|
||||||
|
/// use libretunes::auth::get_user;
|
||||||
|
/// #[server(endpoint = "user_route")]
|
||||||
|
/// pub async fn user_route() -> Result<(), ServerFnError> {
|
||||||
|
/// let user = get_user().await?;
|
||||||
|
/// println!("Logged in as: {}", user.username);
|
||||||
|
/// // Do something with the user
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub async fn get_user() -> Result<User, ServerFnError> {
|
||||||
|
let auth_session = extract::<AuthSession<AuthBackend>>().await
|
||||||
|
.map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Error getting auth session: {}", e)))?;
|
||||||
|
|
||||||
|
auth_session.user.ok_or(ServerFnError::<NoCustomError>::ServerError("User not logged in".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if a user is an admin
|
/// Check if a user is an admin
|
||||||
/// Returns a Result with a boolean indicating if the user is logged in and an admin
|
/// Returns a Result with a boolean indicating if the user is logged in and an admin
|
||||||
#[server(endpoint = "check_admin")]
|
#[server(endpoint = "check_admin")]
|
||||||
|
@ -13,6 +13,7 @@ pub mod users;
|
|||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod fileserv;
|
pub mod fileserv;
|
||||||
pub mod error_template;
|
pub mod error_template;
|
||||||
|
pub mod api;
|
||||||
pub mod upload;
|
pub mod upload;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
|
129
src/models.rs
129
src/models.rs
@ -45,6 +45,135 @@ pub struct User {
|
|||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
/// Like or unlike a song for this user
|
||||||
|
/// If likeing a song, remove dislike if it exists
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub async fn set_like_song(self: &Self, song_id: i32, like: bool, conn: &mut PgPooledConn) ->
|
||||||
|
Result<(), Box<dyn Error>> {
|
||||||
|
use log::*;
|
||||||
|
debug!("Setting like for song {} to {}", song_id, like);
|
||||||
|
|
||||||
|
use crate::schema::song_likes;
|
||||||
|
use crate::schema::song_dislikes;
|
||||||
|
|
||||||
|
let my_id = self.id.ok_or("User id must be present (Some) to like/un-like a song")?;
|
||||||
|
|
||||||
|
if like {
|
||||||
|
diesel::insert_into(song_likes::table)
|
||||||
|
.values((song_likes::song_id.eq(song_id), song_likes::user_id.eq(my_id)))
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
// Remove dislike if it exists
|
||||||
|
diesel::delete(song_dislikes::table.filter(song_dislikes::song_id.eq(song_id)
|
||||||
|
.and(song_dislikes::user_id.eq(my_id))))
|
||||||
|
.execute(conn)?;
|
||||||
|
} else {
|
||||||
|
diesel::delete(song_likes::table.filter(song_likes::song_id.eq(song_id).and(song_likes::user_id.eq(my_id))))
|
||||||
|
.execute(conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the like status of a song for this user
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub async fn get_like_song(self: &Self, song_id: i32, conn: &mut PgPooledConn) -> Result<bool, Box<dyn Error>> {
|
||||||
|
use crate::schema::song_likes;
|
||||||
|
|
||||||
|
let my_id = self.id.ok_or("User id must be present (Some) to get like status of a song")?;
|
||||||
|
|
||||||
|
let like = song_likes::table
|
||||||
|
.filter(song_likes::song_id.eq(song_id).and(song_likes::user_id.eq(my_id)))
|
||||||
|
.first::<(i32, i32)>(conn)
|
||||||
|
.optional()?
|
||||||
|
.is_some();
|
||||||
|
|
||||||
|
Ok(like)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get songs liked by this user
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub async fn get_liked_songs(self: &Self, conn: &mut PgPooledConn) -> Result<Vec<Song>, Box<dyn Error>> {
|
||||||
|
use crate::schema::songs::dsl::*;
|
||||||
|
use crate::schema::song_likes::dsl::*;
|
||||||
|
|
||||||
|
let my_id = self.id.ok_or("User id must be present (Some) to get liked songs")?;
|
||||||
|
|
||||||
|
let my_songs = songs
|
||||||
|
.inner_join(song_likes)
|
||||||
|
.filter(user_id.eq(my_id))
|
||||||
|
.select(songs::all_columns())
|
||||||
|
.load(conn)?;
|
||||||
|
|
||||||
|
Ok(my_songs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dislike or remove dislike from a song for this user
|
||||||
|
/// If disliking a song, remove like if it exists
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub async fn set_dislike_song(self: &Self, song_id: i32, dislike: bool, conn: &mut PgPooledConn) ->
|
||||||
|
Result<(), Box<dyn Error>> {
|
||||||
|
use log::*;
|
||||||
|
debug!("Setting dislike for song {} to {}", song_id, dislike);
|
||||||
|
|
||||||
|
use crate::schema::song_likes;
|
||||||
|
use crate::schema::song_dislikes;
|
||||||
|
|
||||||
|
let my_id = self.id.ok_or("User id must be present (Some) to dislike/un-dislike a song")?;
|
||||||
|
|
||||||
|
if dislike {
|
||||||
|
diesel::insert_into(song_dislikes::table)
|
||||||
|
.values((song_dislikes::song_id.eq(song_id), song_dislikes::user_id.eq(my_id)))
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
// Remove like if it exists
|
||||||
|
diesel::delete(song_likes::table.filter(song_likes::song_id.eq(song_id)
|
||||||
|
.and(song_likes::user_id.eq(my_id))))
|
||||||
|
.execute(conn)?;
|
||||||
|
} else {
|
||||||
|
diesel::delete(song_dislikes::table.filter(song_dislikes::song_id.eq(song_id)
|
||||||
|
.and(song_dislikes::user_id.eq(my_id))))
|
||||||
|
.execute(conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the dislike status of a song for this user
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub async fn get_dislike_song(self: &Self, song_id: i32, conn: &mut PgPooledConn) -> Result<bool, Box<dyn Error>> {
|
||||||
|
use crate::schema::song_dislikes;
|
||||||
|
|
||||||
|
let my_id = self.id.ok_or("User id must be present (Some) to get dislike status of a song")?;
|
||||||
|
|
||||||
|
let dislike = song_dislikes::table
|
||||||
|
.filter(song_dislikes::song_id.eq(song_id).and(song_dislikes::user_id.eq(my_id)))
|
||||||
|
.first::<(i32, i32)>(conn)
|
||||||
|
.optional()?
|
||||||
|
.is_some();
|
||||||
|
|
||||||
|
Ok(dislike)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get songs disliked by this user
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub async fn get_disliked_songs(self: &Self, conn: &mut PgPooledConn) -> Result<Vec<Song>, Box<dyn Error>> {
|
||||||
|
use crate::schema::songs::dsl::*;
|
||||||
|
use crate::schema::song_likes::dsl::*;
|
||||||
|
|
||||||
|
let my_id = self.id.ok_or("User id must be present (Some) to get disliked songs")?;
|
||||||
|
|
||||||
|
let my_songs = songs
|
||||||
|
.inner_join(song_likes)
|
||||||
|
.filter(user_id.eq(my_id))
|
||||||
|
.select(songs::all_columns())
|
||||||
|
.load(conn)?;
|
||||||
|
|
||||||
|
Ok(my_songs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Model for an artist
|
/// Model for an artist
|
||||||
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable, Identifiable))]
|
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable, Identifiable))]
|
||||||
#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::artists))]
|
#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::artists))]
|
||||||
|
118
src/playbar.rs
118
src/playbar.rs
@ -1,5 +1,7 @@
|
|||||||
use crate::models::Artist;
|
use crate::models::Artist;
|
||||||
use crate::playstatus::PlayStatus;
|
use crate::playstatus::PlayStatus;
|
||||||
|
use crate::songdata::SongData;
|
||||||
|
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::*;
|
||||||
@ -269,13 +271,124 @@ fn MediaInfo(status: RwSignal<PlayStatus>) -> impl IntoView {
|
|||||||
});
|
});
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="media-info">
|
|
||||||
<img class="media-info-img" align="left" src={image}/>
|
<img class="media-info-img" align="left" src={image}/>
|
||||||
<div class="media-info-text">
|
<div class="media-info-text">
|
||||||
{name}
|
{name}
|
||||||
<br/>
|
<br/>
|
||||||
{artist} - {album}
|
{artist} - {album}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The like and dislike buttons
|
||||||
|
#[component]
|
||||||
|
fn LikeDislike(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||||
|
let like_icon = Signal::derive(move || {
|
||||||
|
status.with(|status| {
|
||||||
|
match status.queue.front() {
|
||||||
|
Some(SongData { like_dislike: Some((true, _)), .. }) => icondata::TbThumbUpFilled,
|
||||||
|
_ => icondata::TbThumbUp,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let dislike_icon = Signal::derive(move || {
|
||||||
|
status.with(|status| {
|
||||||
|
match status.queue.front() {
|
||||||
|
Some(SongData { like_dislike: Some((_, true)), .. }) => icondata::TbThumbDownFilled,
|
||||||
|
_ => icondata::TbThumbDown,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let toggle_like = move |_| {
|
||||||
|
status.update(|status| {
|
||||||
|
match status.queue.front_mut() {
|
||||||
|
Some(SongData { id, like_dislike: Some((liked, disliked)), .. }) => {
|
||||||
|
*liked = !*liked;
|
||||||
|
|
||||||
|
if *liked {
|
||||||
|
*disliked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = *id;
|
||||||
|
let liked = *liked;
|
||||||
|
spawn_local(async move {
|
||||||
|
if let Err(e) = songs::set_like_song(id, liked).await {
|
||||||
|
error!("Error liking song: {:?}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
Some(SongData { id, like_dislike, .. }) => {
|
||||||
|
// This arm should only be reached if like_dislike is None
|
||||||
|
// In this case, the buttons will show up not filled, indicating that the song is not
|
||||||
|
// liked or disliked. Therefore, clicking the like button should like the song.
|
||||||
|
|
||||||
|
*like_dislike = Some((true, false));
|
||||||
|
|
||||||
|
let id = *id;
|
||||||
|
spawn_local(async move {
|
||||||
|
if let Err(e) = songs::set_like_song(id, true).await {
|
||||||
|
error!("Error liking song: {:?}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
log!("Unable to like song: No song in queue");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let toggle_dislike = move |_| {
|
||||||
|
status.update(|status| {
|
||||||
|
match status.queue.front_mut() {
|
||||||
|
Some(SongData { id, like_dislike: Some((liked, disliked)), .. }) => {
|
||||||
|
*disliked = !*disliked;
|
||||||
|
|
||||||
|
if *disliked {
|
||||||
|
*liked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = *id;
|
||||||
|
let disliked = *disliked;
|
||||||
|
spawn_local(async move {
|
||||||
|
if let Err(e) = songs::set_dislike_song(id, disliked).await {
|
||||||
|
error!("Error disliking song: {:?}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
Some(SongData { id, like_dislike, .. }) => {
|
||||||
|
// This arm should only be reached if like_dislike is None
|
||||||
|
// In this case, the buttons will show up not filled, indicating that the song is not
|
||||||
|
// liked or disliked. Therefore, clicking the dislike button should dislike the song.
|
||||||
|
|
||||||
|
*like_dislike = Some((false, true));
|
||||||
|
|
||||||
|
let id = *id;
|
||||||
|
spawn_local(async move {
|
||||||
|
if let Err(e) = songs::set_dislike_song(id, true).await {
|
||||||
|
error!("Error disliking song: {:?}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
log!("Unable to dislike song: No song in queue");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="like-dislike">
|
||||||
|
<button on:click=toggle_dislike>
|
||||||
|
<Icon class="controlbtn hmirror" width=SKIP_BTN_SIZE height=SKIP_BTN_SIZE icon=dislike_icon />
|
||||||
|
</button>
|
||||||
|
<button on:click=toggle_like>
|
||||||
|
<Icon class="controlbtn" width=SKIP_BTN_SIZE height=SKIP_BTN_SIZE icon=like_icon />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -488,7 +601,10 @@ pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView {
|
|||||||
on:timeupdate=on_time_update on:ended=on_end type="audio/mpeg" />
|
on:timeupdate=on_time_update on:ended=on_end type="audio/mpeg" />
|
||||||
<div class="playbar">
|
<div class="playbar">
|
||||||
<ProgressBar percentage=percentage.into() status=status />
|
<ProgressBar percentage=percentage.into() status=status />
|
||||||
|
<div class="playbar-left-group">
|
||||||
<MediaInfo status=status />
|
<MediaInfo status=status />
|
||||||
|
<LikeDislike status=status />
|
||||||
|
</div>
|
||||||
<PlayControls status=status />
|
<PlayControls status=status />
|
||||||
<PlayDuration elapsed_secs=elapsed_secs.into() total_secs=total_secs.into() />
|
<PlayDuration elapsed_secs=elapsed_secs.into() total_secs=total_secs.into() />
|
||||||
<QueueToggle status=status />
|
<QueueToggle status=status />
|
||||||
|
@ -30,6 +30,20 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
song_dislikes (song_id, user_id) {
|
||||||
|
song_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
song_likes (song_id, user_id) {
|
||||||
|
song_id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
songs (id) {
|
songs (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
@ -58,6 +72,10 @@ diesel::joinable!(album_artists -> albums (album_id));
|
|||||||
diesel::joinable!(album_artists -> artists (artist_id));
|
diesel::joinable!(album_artists -> artists (artist_id));
|
||||||
diesel::joinable!(song_artists -> artists (artist_id));
|
diesel::joinable!(song_artists -> artists (artist_id));
|
||||||
diesel::joinable!(song_artists -> songs (song_id));
|
diesel::joinable!(song_artists -> songs (song_id));
|
||||||
|
diesel::joinable!(song_dislikes -> songs (song_id));
|
||||||
|
diesel::joinable!(song_dislikes -> users (user_id));
|
||||||
|
diesel::joinable!(song_likes -> songs (song_id));
|
||||||
|
diesel::joinable!(song_likes -> users (user_id));
|
||||||
diesel::joinable!(songs -> albums (album_id));
|
diesel::joinable!(songs -> albums (album_id));
|
||||||
|
|
||||||
diesel::allow_tables_to_appear_in_same_query!(
|
diesel::allow_tables_to_appear_in_same_query!(
|
||||||
@ -65,6 +83,8 @@ diesel::allow_tables_to_appear_in_same_query!(
|
|||||||
albums,
|
albums,
|
||||||
artists,
|
artists,
|
||||||
song_artists,
|
song_artists,
|
||||||
|
song_dislikes,
|
||||||
|
song_likes,
|
||||||
songs,
|
songs,
|
||||||
users,
|
users,
|
||||||
);
|
);
|
||||||
|
@ -26,45 +26,10 @@ pub struct SongData {
|
|||||||
/// Path to song image, relative to the root of the web server.
|
/// Path to song image, relative to the root of the web server.
|
||||||
/// For example, `"/assets/images/Song.jpg"`
|
/// For example, `"/assets/images/Song.jpg"`
|
||||||
pub image_path: String,
|
pub image_path: String,
|
||||||
|
/// Whether the song is liked by the user
|
||||||
|
pub like_dislike: Option<(bool, bool)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
impl TryInto<SongData> for Song {
|
|
||||||
type Error = Box<dyn std::error::Error>;
|
|
||||||
|
|
||||||
/// Convert a Song object into a SongData object
|
|
||||||
///
|
|
||||||
/// This conversion is expensive, as it requires database queries to get the artist and album objects.
|
|
||||||
/// The SongData/Song conversions are also not truly reversible,
|
|
||||||
/// due to the way the image_path, album, and artist data is handled.
|
|
||||||
fn try_into(self) -> Result<SongData, Self::Error> {
|
|
||||||
use crate::database;
|
|
||||||
let mut db_con = database::get_db_conn();
|
|
||||||
|
|
||||||
let album = self.get_album(&mut db_con)?;
|
|
||||||
|
|
||||||
// Use the song's image path if it exists, otherwise use the album's image path, or fallback to the placeholder
|
|
||||||
let image_path = self.image_path.clone().unwrap_or_else(|| {
|
|
||||||
album
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|album| album.image_path.clone())
|
|
||||||
.unwrap_or_else(|| "/assets/images/placeholder.jpg".to_string())
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(SongData {
|
|
||||||
id: self.id.ok_or("Song id must be present (Some) to convert to SongData")?,
|
|
||||||
title: self.title.clone(),
|
|
||||||
artists: self.get_artists(&mut db_con)?,
|
|
||||||
album: album,
|
|
||||||
track: self.track,
|
|
||||||
duration: self.duration,
|
|
||||||
release_date: self.release_date,
|
|
||||||
// TODO https://gitlab.mregirouard.com/libretunes/libretunes/-/issues/35
|
|
||||||
song_path: self.storage_path,
|
|
||||||
image_path: image_path,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryInto<Song> for SongData {
|
impl TryInto<Song> for SongData {
|
||||||
type Error = Box<dyn std::error::Error>;
|
type Error = Box<dyn std::error::Error>;
|
||||||
@ -72,7 +37,7 @@ impl TryInto<Song> for SongData {
|
|||||||
/// Convert a SongData object into a Song object
|
/// Convert a SongData object into a Song object
|
||||||
///
|
///
|
||||||
/// The SongData/Song conversions are also not truly reversible,
|
/// The SongData/Song conversions are also not truly reversible,
|
||||||
/// due to the way the image_path, album, and and artist data is handled.
|
/// due to the way the image_path data is handled.
|
||||||
fn try_into(self) -> Result<Song, Self::Error> {
|
fn try_into(self) -> Result<Song, Self::Error> {
|
||||||
Ok(Song {
|
Ok(Song {
|
||||||
id: Some(self.id),
|
id: Some(self.id),
|
||||||
|
@ -39,15 +39,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-info {
|
.playbar-left-group {
|
||||||
font-size: 16;
|
display: flex;
|
||||||
margin-left: 10px;
|
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
display: grid;
|
margin-left: 10px;
|
||||||
grid-template-columns: 50px 1fr;
|
|
||||||
|
|
||||||
.media-info-img {
|
.media-info-img {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
@ -57,6 +54,10 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.like-dislike {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.playcontrols {
|
.playcontrols {
|
||||||
@ -64,23 +65,6 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
button {
|
|
||||||
.controlbtn {
|
|
||||||
color: $text-controls-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controlbtn:hover {
|
|
||||||
color: $controls-hover-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controlbtn:active {
|
|
||||||
color: $controls-click-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
background-color: transparent;
|
|
||||||
border: transparent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.playduration {
|
.playduration {
|
||||||
@ -94,8 +78,17 @@
|
|||||||
bottom: 13px;
|
bottom: 13px;
|
||||||
top: 13px;
|
top: 13px;
|
||||||
right: 90px;
|
right: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
.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 {
|
.controlbtn {
|
||||||
color: $text-controls-color;
|
color: $text-controls-color;
|
||||||
}
|
}
|
||||||
@ -112,4 +105,3 @@
|
|||||||
border: transparent;
|
border: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user