Compare commits

...

49 Commits

Author SHA1 Message Date
622b79f79b Changed units to be more responsive and removed unnecessary properties 2024-12-11 04:59:00 +00:00
71d410bc0a Adjusted units to be more responsive and other fixes 2024-12-11 04:57:27 +00:00
a2ae5f78a7 Adjusted units, redundant prefixes, and other things 2024-12-11 04:54:15 +00:00
1c13d0534a Adjusted login page css 2024-12-11 04:52:16 +00:00
9fb3cd745b Merge pull request 'Fix Album Artists Displayed Wrong' (#165) from 164-album-artists-displayed-wrong into main
Reviewed-on: LibreTunes/LibreTunes#165
Reviewed-by: Ethan Girouard <ethan@girouard.com>
2024-12-04 05:54:37 +00:00
a7905624a6 Bugfixes 2024-12-04 05:29:16 +00:00
aaa9db93fb Merge pull request 'Require login to fetch audio and image assets' (#160) from 110-require-login-to-fetch-audio-and into main
Reviewed-on: LibreTunes/LibreTunes#160
2024-11-24 21:35:41 +00:00
fac33bb1f1 Merge pull request 'Add environment variable to disable signup' (#159) from 157-add-environment-variable-to-disable-signup into main
Reviewed-on: LibreTunes/LibreTunes#159
2024-11-24 21:34:32 +00:00
97f50b38c5 Add example usage of LIBRETUNES_DISABLE_SIGNUP env var 2024-11-24 14:37:10 -05:00
5ecb71ce9d Return error on /api/signup if signup disabled 2024-11-24 14:36:56 -05:00
d1c8615105 Add router layer to require authentication 2024-11-24 14:28:06 -05:00
6592d66f87 Merge pull request 'Fixed casing warning' (#158) from 155-fix-dockerfile-fromas-casing-warning into main
Reviewed-on: LibreTunes/LibreTunes#158
2024-11-24 19:04:06 +00:00
51a9e8c4b3 Fixed casing warning 2024-11-24 04:29:53 +00:00
1a1516ff92 Merge pull request 'Set new container registry url in docker-compose' (#156) from 112-set-new-container-registry-url-in into main
Reviewed-on: LibreTunes/LibreTunes#156
2024-11-23 07:16:40 +00:00
c9d3053c5a Change registry URL to Gitea registry 2024-11-23 02:02:53 -05:00
aced8723c2 Merge pull request 'Indicate loading and errors on login/signup pages' (#154) from 147-indicate-loading-and-errors-on-loginsignup into main
Reviewed-on: LibreTunes/LibreTunes#154
2024-11-23 06:15:25 +00:00
ede248d961 Add loading and error messages to signup page 2024-11-23 00:57:29 -05:00
930618dcad Add loading and error messages to login page 2024-11-23 00:46:30 -05:00
9b48fc0204 Merge pull request '95-fix-home-screen-account-button-ui' (#124) from 95-fix-home-screen-account-button-ui into main
Reviewed-on: https://gitea.mregirouard.com/LibreTunes/LibreTunes/pulls/124
2024-11-22 22:32:53 +00:00
dfbb217988 Merge pull request '114-create-album-page' (#149) from 114-create-album-page into main
Reviewed-on: https://gitea.mregirouard.com/LibreTunes/LibreTunes/pulls/149
2024-11-22 22:31:27 +00:00
954cc0edce Artist links and styling 2024-11-22 22:13:43 +00:00
e5ce0eab76 Fix Build Warnings 2024-11-22 21:55:59 +00:00
45eb7191f0 Bugfix and small changes to styling 2024-11-22 21:47:24 +00:00
21a17a8eb5 Album Page Styling 2024-11-22 21:34:21 +00:00
fcd987d433 change unwrap or default to unwrap or false for readability 2024-11-22 16:31:55 -05:00
dd14aa0b4d AlbumData Query, API Endpoint, and Integration into AlbumPage 2024-11-20 04:43:53 +00:00
3b6035dd71 Album pages for users not signed in 2024-11-20 02:12:09 +00:00
22cee4a265 Merge remote-tracking branch 'origin/main' into 114-create-album-page 2024-11-19 22:21:46 +00:00
b25cb4549c Revert get_user to not be server function 2024-11-19 16:21:35 -05:00
73b4b7674e Use global state user instead of local resource in personal page 2024-11-17 18:42:57 -05:00
3d1dc94b06 Merge remote-tracking branch 'origin/main' into 95-fix-home-screen-account-button-ui 2024-11-17 17:38:29 -05:00
d8fab0e068 Reduce calls to get_user by not having logged_in resource 2024-11-12 17:11:06 -05:00
568f6ada0e Working Albums Page (BETA) 2024-11-12 21:44:30 +00:00
7a1ffaad47 Finished Query for Song Data from Album 2024-11-08 22:16:46 +00:00
f1affc66bc Remove email display on personal and rescale icon/pfp to be bigger & consistent 2024-11-01 17:17:59 -04:00
e60243e50c Redirect to login when logging out 2024-11-01 17:17:06 -04:00
ff24f68eed A very big SQL Query 2024-10-26 03:53:03 +00:00
6aa933be09 Refactor profile picture display to account for no profile picture 2024-10-25 22:59:06 -04:00
fe1b76e6e4 API Endpoints for Album Queries 2024-10-25 21:23:21 +00:00
acf15961cd Fix formatting and styling of profile picture display, as well as display email when logged in as well 2024-10-25 17:01:50 -04:00
df01bafbd1 Album Page Component 2024-10-22 03:32:55 +00:00
2d7b91413b Display username in profile container when logged in 2024-10-18 19:31:20 -04:00
c2ebd8307f Display user profile picture when logged in instead of generic profile icon 2024-10-18 19:30:45 -04:00
2be665c549 Track logged in status and user as local resource instead of signal 2024-10-18 19:26:10 -04:00
f104a14f98 Remove derive Default for User and make user signal Option type instead 2024-10-15 17:26:30 -04:00
f78066d7a8 Display username in profile when logged in 2024-10-15 17:18:41 -04:00
071dcad0cc Convert borders and margins to rem from px in profile 2024-10-15 17:17:44 -04:00
3149f65a97 Detect user logged in and display "logged in" for profile 2024-10-11 17:21:11 -04:00
ab50826d31 Update formatting of profile login buttons and display 2024-10-04 17:44:10 -04:00
29 changed files with 864 additions and 401 deletions

View File

@ -18,3 +18,4 @@ DATABASE_URL=postgresql://libretunes:password@localhost:5432/libretunes
LIBRETUNES_AUDIO_PATH=assets/audio LIBRETUNES_AUDIO_PATH=assets/audio
LIBRETUNES_IMAGE_PATH=assets/images LIBRETUNES_IMAGE_PATH=assets/images
LIBRETUNES_DISABLE_SIGNUP=true

View File

@ -1,4 +1,4 @@
FROM rust:slim as builder FROM rust:slim AS builder
WORKDIR /app WORKDIR /app

View File

@ -3,7 +3,7 @@ name: libretunes
services: services:
libretunes: libretunes:
container_name: libretunes container_name: libretunes
# image: registry.mregirouard.com/libretunes/libretunes:latest # image: git.libretunes.xyz/libretunes/libretunes:latest
build: . build: .
ports: ports:
- "3000:3000" - "3000:3000"
@ -15,6 +15,7 @@ services:
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB}
LIBRETUNES_AUDIO_PATH: /assets/audio LIBRETUNES_AUDIO_PATH: /assets/audio
LIBRETUNES_IMAGE_PATH: /assets/images LIBRETUNES_IMAGE_PATH: /assets/images
LIBRETUNES_DISABLE_SIGNUP: "true"
volumes: volumes:
- libretunes-audio:/assets/audio - libretunes-audio:/assets/audio
- libretunes-images:/assets/images - libretunes-images:/assets/images

View File

@ -1,11 +1,14 @@
use crate::models::Artist; use crate::models::Artist;
use crate::components::dashboard_tile::DashboardTile; use crate::components::dashboard_tile::DashboardTile;
use serde::{Serialize, Deserialize};
use chrono::NaiveDate; use chrono::NaiveDate;
/// Holds information about an album /// Holds information about an album
/// ///
/// Intended to be used in the front-end /// Intended to be used in the front-end
#[derive(Serialize, Deserialize, Clone)]
pub struct AlbumData { pub struct AlbumData {
/// Album id /// Album id
pub id: i32, pub id: i32,

33
src/api/album.rs Normal file
View File

@ -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<AlbumData, ServerFnError> {
use crate::models::Album;
let db_con = &mut get_db_conn();
let album = Album::get_album_data(id,db_con)
.map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Error getting album: {}", e)))?;
Ok(album)
}
#[server(endpoint = "album/get_songs")]
pub async fn get_songs(id: i32) -> Result<Vec<SongData>, 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::<NoCustomError>::ServerError(format!("Error getting song data: {}", e)))?;
Ok(songdata)
}

View File

@ -1,3 +1,4 @@
pub mod history; pub mod history;
pub mod profile; pub mod profile;
pub mod songs; pub mod songs;
pub mod album;

View File

@ -7,6 +7,7 @@ use leptos_router::*;
use crate::pages::login::*; use crate::pages::login::*;
use crate::pages::signup::*; use crate::pages::signup::*;
use crate::pages::profile::*; use crate::pages::profile::*;
use crate::pages::albumpage::*;
use crate::error_template::{AppError, ErrorTemplate}; use crate::error_template::{AppError, ErrorTemplate};
use crate::util::state::GlobalState; use crate::util::state::GlobalState;
@ -44,6 +45,7 @@ pub fn App() -> impl IntoView {
<Route path="search" view=Search /> <Route path="search" view=Search />
<Route path="user/:id" view=Profile /> <Route path="user/:id" view=Profile />
<Route path="user" view=Profile /> <Route path="user" view=Profile />
<Route path="album/:id" view=AlbumPage />
</Route> </Route>
<Route path="/login" view=Login /> <Route path="/login" view=Login />
<Route path="/signup" view=Signup /> <Route path="/signup" view=Signup />

View File

@ -19,6 +19,11 @@ use crate::users::UserCredentials;
/// Returns a Result with the error message if the user could not be created /// Returns a Result with the error message if the user could not be created
#[server(endpoint = "signup")] #[server(endpoint = "signup")]
pub async fn signup(new_user: User) -> Result<(), ServerFnError> { pub async fn signup(new_user: User) -> Result<(), ServerFnError> {
// Check LIBRETUNES_DISABLE_SIGNUP env var
if std::env::var("LIBRETUNES_DISABLE_SIGNUP").is_ok_and(|v| v == "true") {
return Err(ServerFnError::<NoCustomError>::ServerError("Signup is disabled".to_string()));
}
use crate::users::create_user; use crate::users::create_user;
// Ensure the user has no id, and is not a self-proclaimed admin // Ensure the user has no id, and is not a self-proclaimed admin
@ -87,6 +92,7 @@ pub async fn logout() -> Result<(), ServerFnError> {
auth_session.logout().await auth_session.logout().await
.map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Error getting auth session: {}", e)))?; .map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Error getting auth session: {}", e)))?;
leptos_axum::redirect("/login");
Ok(()) Ok(())
} }

View File

@ -8,3 +8,4 @@ pub mod upload;
pub mod song_list; pub mod song_list;
pub mod loading; pub mod loading;
pub mod error; pub mod error;
pub mod album_info;

View File

@ -0,0 +1,25 @@
use leptos::leptos_dom::*;
use leptos::*;
use crate::albumdata::AlbumData;
#[component]
pub fn AlbumInfo(albumdata: AlbumData) -> impl IntoView {
view! {
<div class="album-info">
<img class="album-image" src={albumdata.image_path} alt="dashboard-tile" />
<div class="album-body">
<p class="album-title">{albumdata.title}</p>
<div class="album-artists">
{
albumdata.artists.iter().map(|artist| {
view! {
<a class="album-artist" href={format!("/artist/{}", artist.id.unwrap())}>{artist.name.clone()}</a>
}
}).collect::<Vec<_>>()
}
</div>
</div>
</div>
}.into_view()
}

View File

@ -1,6 +1,8 @@
use leptos::leptos_dom::*; use leptos::leptos_dom::*;
use leptos::*; use leptos::*;
use leptos_icons::*; use leptos_icons::*;
use crate::auth::logout;
use crate::util::state::GlobalState;
#[component] #[component]
pub fn Personal() -> impl IntoView { pub fn Personal() -> impl IntoView {
@ -14,18 +16,66 @@ pub fn Personal() -> impl IntoView {
#[component] #[component]
pub fn Profile() -> impl IntoView { pub fn Profile() -> impl IntoView {
let (dropdown_open, set_dropdown_open) = create_signal(false); let (dropdown_open, set_dropdown_open) = create_signal(false);
let user = GlobalState::logged_in_user();
let open_dropdown = move |_| { let open_dropdown = move |_| {
set_dropdown_open.update(|value| *value = !*value); set_dropdown_open.update(|value| *value = !*value);
log!("opened dropdown");
}; };
let user_profile_picture = move || {
user.get().and_then(|user| {
if let Some(user) = user {
if user.id.is_none() {
return None;
}
Some(format!("/assets/images/profile/{}.webp", user.id.unwrap()))
} else {
None
}
})
};
view! { view! {
<div class="profile-container"> <div class="profile-container">
<div class="profile-name">
<Suspense
fallback=|| view!{
<h1>Not Logged In</h1>
}>
<Show
when=move || user.get().map(|user| user.is_some()).unwrap_or(false)
fallback=|| view!{
<h1>Not Logged In</h1>
}>
<h1>{move || user.get().map(|user| user.map(|user| user.username))}</h1>
</Show>
</Suspense>
</div>
<div class="profile-icon" on:click=open_dropdown> <div class="profile-icon" on:click=open_dropdown>
<Icon icon=icondata::CgProfile /> <Suspense fallback=|| view! { <Icon icon=icondata::CgProfile width="45" height="45"/> }>
<Show
when=move || user.get().map(|user| user.is_some()).unwrap_or(false)
fallback=|| view! { <Icon icon=icondata::CgProfile width="45" height="45"/> }
>
<object class="profile-image" data={user_profile_picture} type="image/webp">
<Icon class="profile-image" icon=icondata::CgProfile width="45" height="45"/>
</object>
</Show>
</Suspense>
</div> </div>
<div class="dropdown-container" style={move || if dropdown_open() {"display: flex"} else {"display: none"}}> <div class="dropdown-container" style={move || if dropdown_open() {"display: flex"} else {"display: none"}}>
<Suspense
fallback=|| view!{
<DropDownNotLoggedIn /> <DropDownNotLoggedIn />
}>
<Show
when=move || user.get().map(|user| user.is_some()).unwrap_or(false)
fallback=|| view!{
<DropDownNotLoggedIn />
}>
<DropDownLoggedIn />
</Show>
</Suspense>
</div> </div>
</div> </div>
} }
@ -33,10 +83,33 @@ pub fn Profile() -> impl IntoView {
#[component] #[component]
pub fn DropDownNotLoggedIn() -> impl IntoView { pub fn DropDownNotLoggedIn() -> impl IntoView {
view! { view! {
<div class="dropdown-not-logged"> <div class="dropdown-logged">
<h1>Not Logged in!</h1> <h1>Not Logged In</h1>
<a href="/login"><button class="auth-button">Log In</button></a> <a href="/login"><button class="auth-button">Log In</button></a>
<a href="/signup"><button class="auth-button">Sign up</button></a> <a href="/signup"><button class="auth-button">Sign Up</button></a>
</div>
}
}
#[component]
pub fn DropDownLoggedIn() -> impl IntoView {
let logout = move |_| {
spawn_local(async move {
let result = logout().await;
if let Err(err) = result {
log!("Error logging out: {:?}", err);
} else {
let user = GlobalState::logged_in_user();
user.refetch();
log!("Logged out successfully");
}
});
};
view! {
<div class="dropdown-logged">
<h1>"Logged In"</h1>
<button on:click=logout class="auth-button">Log Out</button>
</div> </div>
} }
} }

View File

@ -14,10 +14,11 @@ extern crate diesel_migrations;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
use axum::{routing::get, Router, extract::Path}; use axum::{routing::get, Router, extract::Path, middleware::from_fn};
use leptos::*; use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes}; use leptos_axum::{generate_route_list, LeptosRoutes};
use libretunes::app::*; use libretunes::app::*;
use libretunes::util::require_auth::require_auth_middleware;
use libretunes::fileserv::{file_and_error_handler, get_asset_file, get_static_file, AssetType}; use libretunes::fileserv::{file_and_error_handler, get_asset_file, get_static_file, AssetType};
use axum_login::tower_sessions::SessionManagerLayer; use axum_login::tower_sessions::SessionManagerLayer;
use tower_sessions_redis_store::{fred::prelude::*, RedisStore}; use tower_sessions_redis_store::{fred::prelude::*, RedisStore};
@ -63,6 +64,7 @@ async fn main() {
.route("/assets/audio/:song", get(|Path(song) : Path<String>| get_asset_file(song, AssetType::Audio))) .route("/assets/audio/:song", get(|Path(song) : Path<String>| get_asset_file(song, AssetType::Audio)))
.route("/assets/images/:image", get(|Path(image) : Path<String>| get_asset_file(image, AssetType::Image))) .route("/assets/images/:image", get(|Path(image) : Path<String>| get_asset_file(image, AssetType::Image)))
.route("/assets/*uri", get(|uri| get_static_file(uri, ""))) .route("/assets/*uri", get(|uri| get_static_file(uri, "")))
.layer(from_fn(require_auth_middleware))
.layer(auth_layer) .layer(auth_layer)
.fallback(file_and_error_handler) .fallback(file_and_error_handler)
.with_state(leptos_options); .with_state(leptos_options);

View File

@ -6,8 +6,10 @@ use cfg_if::cfg_if;
cfg_if! { cfg_if! {
if #[cfg(feature = "ssr")] { if #[cfg(feature = "ssr")] {
use diesel::prelude::*; use diesel::prelude::*;
use crate::database::PgPooledConn; use crate::database::*;
use std::error::Error; use std::error::Error;
use crate::songdata::SongData;
use crate::albumdata::AlbumData;
} }
} }
@ -498,7 +500,7 @@ impl Album {
Ok(()) 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 /// The `id` field of this album must be present (Some) to get songs
/// ///
@ -525,6 +527,143 @@ impl Album {
Ok(my_songs) 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<Album, Box<dyn Error>>` - 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<AlbumData, Box<dyn Error>> {
use crate::schema::*;
let artist_list: Vec<Artist> = album_artists::table
.filter(album_artists::album_id.eq(album_id))
.inner_join(artists::table.on(album_artists::artist_id.eq(artists::id)))
.select(
artists::all_columns
)
.load(conn)?;
// Get info of album
let albuminfo = albums::table
.filter(albums::id.eq(album_id))
.first::<Album>(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<Album, Box<dyn Error>>` - 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<User>, conn: &mut PgPooledConn) -> Result<Vec<SongData>, Box<dyn Error>> {
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<Song>, Option<Artist>, 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<Song>, Option<Artist>)> =
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<Song>, Option<Artist>, 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<i32, SongData> = 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<SongData> = album_songs.into_values().collect();
songdata.sort_by(|a, b| a.track.cmp(&b.track));
Ok(songdata)
}
} }
/// Model for a song /// Model for a song

View File

@ -1,3 +1,4 @@
pub mod login; pub mod login;
pub mod signup; pub mod signup;
pub mod profile; pub mod profile;
pub mod albumpage;

87
src/pages/albumpage.rs Normal file
View File

@ -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::<AlbumParams>();
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! {
<div class="album-page-container">
<div class="album-header">
<Suspense
fallback=move || view! { <p class="loading">"Loading..."</p> }
>
{move || {
albumdata.with( |albumdata| {
match albumdata {
Some(Ok(s)) => {
view! { <AlbumInfo albumdata=(*s).clone() /> }
},
Some(Err(e)) => {
view! { <div class="error">{format!("Error loading album : {}",e)}</div> }.into_view()
},
None => {view! { }.into_view()}
}
})
}}
</Suspense>
</div>
<Suspense
fallback=move || view! { <p class="loading">"Loading..."</p> }
>
{move || {
song_list.with( |song_list| {
match song_list {
Some(Ok(s)) => {
view! { <SongList songs=(*s).clone()/> }
},
Some(Err(e)) => {
view! { <div class="error">{format!("Error loading albums: : {}",e)}</div> }.into_view()
},
None => {view! { }.into_view()}
}
})
}}
</Suspense>
</div>
}
}

View File

@ -4,6 +4,7 @@ use leptos::leptos_dom::*;
use leptos::*; use leptos::*;
use leptos_icons::*; use leptos_icons::*;
use crate::users::UserCredentials; use crate::users::UserCredentials;
use crate::components::loading::Loading;
#[component] #[component]
pub fn Login() -> impl IntoView { pub fn Login() -> impl IntoView {
@ -12,6 +13,9 @@ pub fn Login() -> impl IntoView {
let (show_password, set_show_password) = create_signal(false); let (show_password, set_show_password) = create_signal(false);
let loading = create_rw_signal(false);
let error_msg = create_rw_signal(None);
let toggle_password = move |_| { let toggle_password = move |_| {
set_show_password.update(|show_password| *show_password = !*show_password); set_show_password.update(|show_password| *show_password = !*show_password);
log!("showing password"); log!("showing password");
@ -24,6 +28,9 @@ pub fn Login() -> impl IntoView {
let password1 = password.get(); let password1 = password.get();
spawn_local(async move { spawn_local(async move {
loading.set(true);
error_msg.set(None);
let user_credentials = UserCredentials { let user_credentials = UserCredentials {
username_or_email: username_or_email1, username_or_email: username_or_email1,
password: password1 password: password1
@ -35,6 +42,7 @@ pub fn Login() -> impl IntoView {
if let Err(err) = login_result { if let Err(err) = login_result {
// Handle the error here, e.g., log it or display to the user // Handle the error here, e.g., log it or display to the user
log!("Error logging in: {:?}", err); log!("Error logging in: {:?}", err);
error_msg.set(Some(err.to_string()));
// Since we're not sure what the state is, manually refetch the user // Since we're not sure what the state is, manually refetch the user
user.refetch(); user.refetch();
@ -48,10 +56,13 @@ pub fn Login() -> impl IntoView {
log!("Navigated to home page after login"); log!("Navigated to home page after login");
} else if let Ok(None) = login_result { } else if let Ok(None) = login_result {
log!("Invalid username or password"); log!("Invalid username or password");
error_msg.set(Some("Invalid username or password".to_string()));
// User could be already logged in or not, so refetch the user // User could be already logged in or not, so refetch the user
user.refetch(); user.refetch();
} }
loading.set(false);
}); });
}; };
@ -96,7 +107,13 @@ pub fn Login() -> impl IntoView {
</Show> </Show>
</div> </div>
<a href="" class="forgot-pw">Forgot Password?</a> <a href="" class="forgot-pw">Forgot Password?</a>
<div class="error-msg" >{ move || error_msg.get() }</div>
<Show
when=move || !loading.get()
fallback=move || view! { <Loading /> }
>
<input type="submit" value="Login" /> <input type="submit" value="Login" />
</Show>
<span class="go-to-signup"> <span class="go-to-signup">
New here? <a href="/signup">Create an Account</a> New here? <a href="/signup">Create an Account</a>
</span> </span>

View File

@ -4,6 +4,7 @@ use crate::util::state::GlobalState;
use leptos::leptos_dom::*; use leptos::leptos_dom::*;
use leptos::*; use leptos::*;
use leptos_icons::*; use leptos_icons::*;
use crate::components::loading::Loading;
#[component] #[component]
pub fn Signup() -> impl IntoView { pub fn Signup() -> impl IntoView {
@ -13,6 +14,9 @@ pub fn Signup() -> impl IntoView {
let (show_password, set_show_password) = create_signal(false); let (show_password, set_show_password) = create_signal(false);
let loading = create_rw_signal(false);
let error_msg = create_rw_signal(None);
let toggle_password = move |_| { let toggle_password = move |_| {
set_show_password.update(|show_password| *show_password = !*show_password); set_show_password.update(|show_password| *show_password = !*show_password);
log!("showing password"); log!("showing password");
@ -30,12 +34,16 @@ pub fn Signup() -> impl IntoView {
}; };
log!("new user: {:?}", new_user); log!("new user: {:?}", new_user);
loading.set(true);
error_msg.set(None);
let user = GlobalState::logged_in_user(); let user = GlobalState::logged_in_user();
spawn_local(async move { spawn_local(async move {
if let Err(err) = signup(new_user.clone()).await { if let Err(err) = signup(new_user.clone()).await {
// Handle the error here, e.g., log it or display to the user // Handle the error here, e.g., log it or display to the user
log!("Error signing up: {:?}", err); log!("Error signing up: {:?}", err);
error_msg.set(Some(err.to_string()));
// Since we're not sure what the state is, manually refetch the user // Since we're not sure what the state is, manually refetch the user
user.refetch(); user.refetch();
@ -49,6 +57,8 @@ pub fn Signup() -> impl IntoView {
leptos_router::use_navigate()("/", Default::default()); leptos_router::use_navigate()("/", Default::default());
log!("Navigated to home page after signup") log!("Navigated to home page after signup")
} }
loading.set(false);
}); });
}; };
@ -99,7 +109,13 @@ pub fn Signup() -> impl IntoView {
</button> </button>
</Show> </Show>
</div> </div>
<div class="error-msg">{ move || error_msg.get() }</div>
<Show
when=move || !loading.get()
fallback=move || view!{ <Loading /> }
>
<input type="submit" value="Sign Up" /> <input type="submit" value="Sign Up" />
</Show>
<span class="go-to-login"> <span class="go-to-login">
Already Have an Account? <a href="/login" class="link" >Go to Login</a> Already Have an Account? <a href="/login" class="link" >Go to Login</a>
</span> </span>

View File

@ -117,7 +117,7 @@ pub async fn validate_user(credentials: UserCredentials) -> Result<Option<User>,
/// Get a user from the database by username or email /// Get a user from the database by username or email
/// Returns a Result with the user if found, None if not found, or an error if there was a problem /// Returns a Result with the user if found, None if not found, or an error if there was a problem
#[server(endpoint = "get_user")] #[server(endpoint = "find_user")]
pub async fn get_user(username_or_email: String) -> Result<Option<User>, ServerFnError> { pub async fn get_user(username_or_email: String) -> Result<Option<User>, ServerFnError> {
let mut user = find_user(username_or_email).await?; let mut user = find_user(username_or_email).await?;

View File

@ -3,6 +3,7 @@ use cfg_if::cfg_if;
cfg_if! { cfg_if! {
if #[cfg(feature = "ssr")] { if #[cfg(feature = "ssr")] {
pub mod audio; pub mod audio;
pub mod require_auth;
} }
} }

46
src/util/require_auth.rs Normal file
View File

@ -0,0 +1,46 @@
use axum::extract::Request;
use axum::response::Response;
use axum::body::Body;
use axum::middleware::Next;
use axum_login::AuthSession;
use http::StatusCode;
use crate::auth_backend::AuthBackend;
use axum::extract::FromRequestParts;
// Things in pkg/ are allowed automatically. This includes the CSS/JS/WASM files
const ALLOWED_PATHS: [&str; 5] = ["/login", "/signup", "/api/login", "/api/signup", "/favicon.ico"];
/**
* Middleware to require authentication for all paths except those in ALLOWED_PATHS
*
* If a user is not authenticated, they will be redirected to the login page
*/
pub async fn require_auth_middleware(req: Request, next: Next) -> Result<Response<Body>, (StatusCode, &'static str)> {
let path = req.uri().path();
if !ALLOWED_PATHS.iter().any(|&x| x == path) {
let (mut parts, body) = req.into_parts();
let auth_session = AuthSession::<AuthBackend>::from_request_parts(&mut parts, &())
.await?;
if auth_session.user.is_none() {
let response = Response::builder()
.status(StatusCode::TEMPORARY_REDIRECT)
.header("Location", "/login")
.body(Body::empty())
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Failed to build response"))?;
return Ok(response);
}
let req = Request::from_parts(parts, body);
let response = next.run(req).await;
Ok(response)
} else {
let response = next.run(req).await;
Ok(response)
}
}

80
style/album_page.scss Normal file
View File

@ -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;
}
}

View File

@ -8,144 +8,56 @@
top: 50%; top: 50%;
left: 50%; left: 50%;
width: 27rem; width: 27rem;
height: 30rem; height: 30rem; /* Slightly adjusted height for better layout */
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
background: $auth-containers; background: $auth-containers;
z-index: 1; z-index: 10; /* Increased z-index for better stacking context */
border-radius: 8px; border-radius: 0.5rem; /* Adjusted unit */
overflow: hidden; overflow: hidden;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1); /* Added subtle shadow */
} }
.login-container .header h1 { .login-container .header h1 {
margin-top: 3rem; margin-top: 2.5rem; /* Adjusted margin for alignment */
font-size: 2.5rem; font-size: 2.4rem; /* Slightly smaller for consistency */
color: $accent-color; color: $accent-color;
} text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.2); /* Added slight text shadow */
.login-container .login-form {
width: 75%;
}
.login-form .input-box:first-child {
margin-top: 1rem;
}
.login-form .input-box {
position: relative;
margin-top: 3rem;
}
.login-form .input-box input {
position: relative;
width: 100%;
max-width: 34vw;
padding: 17px 0px 10px;
background: transparent;
outline: none;
border: none;
box-shadow: none;
color: #23242a;
font-size: 1.1em;
font-family: "Roboto", sans-serif;
font-weight: 400;
letter-spacing: 0px;
text-indent: 10px;
vertical-align: middle;
z-index: 10;
color: #fff;
}
.login-form .input-box span {
position: absolute;
left: 0;
padding: 15px 0px 10px;
pointer-events: none;
color: black;
font-size: 1.19em;
letter-spacing: 0.5px;
transition: 0.5s;
}
.login-form .input-box input:valid ~ span,
.login-form .input-box input:focus ~ span {
color: rgb(94, 93, 93);
font-size: 0.9rem;
transform: translateY(-30px);
font-weight: 400;
}
.login-form .input-box i {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 2px;
background: $auth-inputs;
border-radius: 4px;
overflow: hidden;
transition: 0.5s;
pointer-events: none;
} }
.login-form .input-box input:valid ~ i, .login-container .login-form {
.login-form .input-box input:focus ~ i { width: 80%; /* Adjusted width for responsiveness */
height: 2.6rem;
} }
.login-form .forgot-pw {
display: inline-flex; .login-form .input-box {
margin-top: 3px; position: relative;
font-size: 0.9rem; margin-top: 2.5rem; /* Adjusted spacing */
cursor: pointer;
color: #8f8f8f;
text-decoration: underline;
} }
.login-form .forgot-pw:hover {
color: #fff; .login-form .input-box input {
transition: all 0.2s;
}
.login-form input[type="submit"] {
margin-top: 3rem;
width: 100%; width: 100%;
padding: 1.1rem 0 0.6rem; /* Adjusted padding */
border: 1px solid rgba(255, 255, 255, 0.2); /* Added subtle border */
border-radius: 0.3rem; /* Added border radius */
}
.login-form .input-box span {
padding: 0.8rem 0 0.5rem; /* Adjusted padding */
}
.login-form .input-box i {
height: 0.2rem; /* Adjusted height for better visual hierarchy */
}
.login-form input[type="submit"] {
margin-top: 2.5rem; /* Adjusted spacing */
height: 3rem; height: 3rem;
border: none;
border-radius: 8px;
color: rgb(210, 207, 207);
cursor: pointer;
font-size: 1.1rem;
font-weight: 600;
background-color: $accent-color; background-color: $accent-color;
border: 1px solid rgba(255, 255, 255, 0.3); /* Added border for consistency */
} }
.login-form .go-to-signup {
color: #8f8f8f;
font-size: 0.9rem;
}
.login-form .go-to-signup a {
cursor: pointer;
color: #8f8f8f;
text-decoration: underline;
}
.login-form .go-to-signup a:hover {
color: black;
transition: all 0.2s;
}
.login-container .return { .login-container .return {
position: absolute; top: 15px; /* Adjusted position for better alignment */
left: 10px; left: 15px; /* Adjusted position for better alignment */
top: 10px; padding: 0.4rem; /* Adjusted padding */
font-size: 1.8rem; background-color: rgba(255, 255, 255, 0.1); /* Added subtle background */
color: white;
cursor: pointer;
transition: all 0.3s;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
padding: 0.3rem;
}
.login-container .return:hover {
background-color: rgba(0, 0, 0, 0.4);
}
.login-password-visibility {
position: absolute;
font-size: 1.7rem;
top: 28%;
right: 5px;
z-index: 5;
cursor: pointer;
border: none;
background-color: transparent;
color: white;
} }

View File

@ -15,6 +15,7 @@
@import 'song_list.scss'; @import 'song_list.scss';
@import 'profile.scss'; @import 'profile.scss';
@import 'loading.scss'; @import 'loading.scss';
@import 'album_page.scss';
body { body {
font-family: sans-serif; font-family: sans-serif;

View File

@ -11,9 +11,19 @@
border-radius: 0.4rem; border-radius: 0.4rem;
margin: 0.2rem; margin: 0.2rem;
min-height: 6rem; min-height: 6rem;
border: 2px solid rgba(89, 89, 89, 0.199); border: 0.2rem solid rgba(89, 89, 89, 0.199);
padding: 0.5rem; padding: 0.5rem;
.profile-name {
display: flex;
flex-direction: column;
margin-left: 0.5rem;
h1 {
font-size: 1.2rem;
margin: 0;
}
}
.profile-icon { .profile-icon {
display: inline-flex; display: inline-flex;
padding: 0.2rem; padding: 0.2rem;
@ -21,17 +31,23 @@
font-size: 2rem; font-size: 2rem;
border-radius: 50%; border-radius: 50%;
transition: all 0.3s; transition: all 0.3s;
height: max-content; height: 45;
width: 45;
margin-left: auto; margin-left: auto;
.profile-image {
width: 100%;
height: 100%;
border-radius: 50%;
}
} }
.profile-icon:hover { .profile-icon:hover {
transform: scale(1.1); transform: scale(1.05);
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }
.profile-icon:active { .profile-icon:active {
transform: scale(0.8); transform: scale(0.95);
} }
.dropdown-container { .dropdown-container {
position: absolute; position: absolute;
@ -41,10 +57,10 @@
border-radius: 0.5rem; border-radius: 0.5rem;
width: 10rem; width: 10rem;
z-index: 1; z-index: 1;
background-color: red; background-color: #1c1c1c;
border: 1px solid grey; border: 0.2rem solid rgba(89, 89, 89, 0.199);
.dropdown-not-logged { .dropdown-logged {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
@ -53,21 +69,29 @@
h1 { h1 {
font-size: 1.2rem; font-size: 1.2rem;
} }
.profile-info {
display: flex;
width: 100%;
justify-content: center;
border-top: 0.2rem solid rgba(89, 89, 89, 0.199);
border-bottom: 0.2rem solid rgba(89, 89, 89, 0.199);
h1 {
font-size: 1rem;
margin-top: 0.5rem;
}
}
.auth-button { .auth-button {
margin-top: 0.5rem;
padding: 0.5rem;
border-radius: 0.5rem;
background-color: #1c1c1c;
border: 0.2rem solid rgba(89, 89, 89, 0.199);
color: white;
cursor: pointer;
transition: all 0.3s;
margin-bottom: 0.5rem;
} }
} }
} }
.dropdown-container:before {
content: "";
position: absolute;
top: -0.4rem;
right: 0.92rem;
width: 10px;
height: 10px;
transform: rotate(45deg);
background-color: red;
border-left: 1px solid grey;
border-top: 1px solid grey;
}
} }
} }

View File

@ -1,5 +1,4 @@
@import 'theme.scss'; @import "theme.scss";
.playbar { .playbar {
width: 100%; width: 100%;
height: $playbar-size; height: $playbar-size;
@ -7,101 +6,81 @@
opacity: 0.9; opacity: 0.9;
position: fixed; position: fixed;
bottom: 0; bottom: 0;
.invisible-media-progress { .invisible-media-progress {
width: 100%; width: 100%;
height: 14px; height: 0.875rem;
background-color: transparent; background-color: transparent;
position: absolute; position: absolute;
transform: translateY(-50%); transform: translateY(-50%);
padding-top: 7px; padding-top: 0.4375rem;
.media-progress { .media-progress {
width: 100%; width: 100%;
height: 3px; height: 0.1875rem;
background-color: $controls-click-color; background-color: $controls-click-color;
.media-progress-solid { .media-progress-solid {
position: relative; position: relative;
height: 3px; height: 0.1875rem;
background: linear-gradient(90deg, $play-grad-start, $play-grad-end); background: linear-gradient(90deg, $play-grad-start, $play-grad-end);
} }
} }
.media-progress-solid::before { .media-progress-solid::before {
content: ""; content: "";
position: absolute; position: absolute;
height: 3px; height: 0.1875rem;
width: 100%; width: 100%;
background: linear-gradient(90deg, $play-grad-start, $play-grad-end); background: linear-gradient(90deg, $play-grad-start, $play-grad-end);
z-index: -1; z-index: -1;
filter: blur(3px); filter: blur(0.1875rem);
} }
} }
.playbar-left-group { .playbar-left-group {
display: flex; display: flex;
position: absolute; position: absolute;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
margin-left: 10px; margin-left: 0.625rem;
.media-info-img { .media-info-img {
width: 50px; width: 3.125rem;
} }
.media-info-text { .media-info-text {
text-align: left; text-align: left;
margin-left: 10px; margin-left: 0.625rem;
} }
.like-dislike { .like-dislike {
margin-left: 20px; margin-left: 1.25rem;
} }
} }
.playcontrols { .playcontrols {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.playduration { .playduration {
position: absolute; position: absolute;
right: 10px; right: 0.625rem;
top: 13px; top: 0.8125rem;
} }
.queue-toggle { .queue-toggle {
position: absolute; position: absolute;
bottom: 13px; bottom: 0.8125rem;
top: 13px; top: 0.8125rem;
right: 90px; right: 5.625rem;
} }
button { button {
.hmirror { .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); transform: scale(-1, 1);
} }
.controlbtn { .controlbtn {
color: $text-controls-color; color: $text-controls-color;
} }
.controlbtn:hover { .controlbtn:hover {
color: $controls-hover-color; color: $controls-hover-color;
} }
.controlbtn:active { .controlbtn:active {
color: $controls-click-color; color: $controls-click-color;
} }
background-color: transparent; background-color: transparent;
border: transparent; border: none;
} }
} }

View File

@ -17,135 +17,154 @@
top: 50%; top: 50%;
left: 50%; left: 50%;
width: 27rem; width: 27rem;
height: 35rem; height: 36rem;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
background: $auth-containers; background: $auth-containers;
z-index: 1; z-index: 1;
border-radius: 8px; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
} }
.signup-container .header h1 { .signup-container .header h1 {
margin-top: 3rem; margin-top: 3rem;
font-size: 2.5rem; font-size: 2.5rem;
color: $accent-color; color: $accent-color;
} }
.signup-container .signup-form { .signup-container .signup-form {
width: 80%; width: 80%;
} }
.signup-form .input-box { .signup-form .input-box {
position: relative; position: relative;
margin-top: 3rem; margin-top: 3rem;
} }
.signup-form .input-box:first-child { .signup-form .input-box:first-child {
margin-top: 0.7rem; margin-top: 0.7rem;
} }
.signup-form .input-box input { .signup-form .input-box input {
position: relative;
width: 100%; width: 100%;
max-width: 34vw; padding: 1.0625rem 0 0.625rem;
padding: 17px 0px 10px;
background: transparent; background: transparent;
outline: none; outline: none;
border: none; border: none;
box-shadow: none; color: #fff;
color: #23242a; font-size: 1.1rem;
font-size: 1.1em;
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
font-weight: 400; font-weight: 400;
letter-spacing: 0px; text-indent: 0.625rem;
text-indent: 10px;
vertical-align: middle; vertical-align: middle;
z-index: 10;
color: #fff;
} }
.signup-form .input-box span { .signup-form .input-box span {
position: absolute; position: absolute;
left: 0; left: 0;
padding: 15px 0px 10px; padding: 0.9375rem 0 0.625rem;
pointer-events: none; pointer-events: none;
color: black; color: black;
font-size: 1.19em; font-size: 1.19rem;
letter-spacing: 0.5px; letter-spacing: 0.5px;
transition: 0.5s; transition: 0.5s;
} }
.signup-form .input-box input:valid ~ span, .signup-form .input-box input:valid ~ span,
.signup-form .input-box input:focus ~ span { .signup-form .input-box input:focus ~ span {
color: rgb(94, 93, 93); color: rgb(94, 93, 93);
font-size: 0.9rem; font-size: 0.9rem;
transform: translateY(-30px); transform: translateY(-1.875rem);
font-weight: 400; font-weight: 400;
} }
.signup-form .input-box i { .signup-form .input-box i {
position: absolute; position: absolute;
left: 0; left: 0;
bottom: 0; bottom: 0;
width: 100%; width: 100%;
height: 2px; height: 0.125rem;
background: $auth-inputs; background: $auth-inputs;
border-radius: 4px; border-radius: 0.25rem;
overflow: hidden;
transition: 0.5s; transition: 0.5s;
pointer-events: none; pointer-events: none;
} }
.signup-form .input-box input:valid ~ i, .signup-form .input-box input:valid ~ i,
.signup-form .input-box input:focus ~ i { .signup-form .input-box input:focus ~ i {
height: 2.6rem; height: 1.625rem;
}
.signup-form .error-msg {
color: $error-color;
margin-top: 1rem;
height: 1rem;
}
.signup-form .loading {
margin-top: 4.5rem;
margin-left: auto;
margin-right: auto;
margin-bottom: calc(1.5rem - 10px);
} }
.signup-form input[type="submit"] { .signup-form input[type="submit"] {
margin-top: 3.5rem; margin-top: 3.5rem;
width: 100%; width: 100%;
height: 45px; height: 2.8125rem;
border: none; border: none;
border-radius: 8px; border-radius: 0.5rem;
color: white; color: white;
cursor: pointer; cursor: pointer;
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 600; font-weight: 600;
background-color: $accent-color; background-color: $accent-color;
} }
.signup-form .go-to-login { .signup-form .go-to-login {
color: #8f8f8f; color: #8f8f8f;
font-size: 0.9rem; font-size: 0.9rem;
} }
.signup-form .go-to-login a { .signup-form .go-to-login a {
cursor: pointer;
color: #8f8f8f; color: #8f8f8f;
text-decoration: underline; text-decoration: underline;
} }
.signup-form .go-to-login a:hover { .signup-form .go-to-login a:hover {
color: black; color: black;
transition: all 0.2s; transition: color 0.2s;
} }
.signup-container .return { .signup-container .return {
position: absolute; position: absolute;
left: 10px; left: 0.625rem;
top: 10px; top: 0.625rem;
font-size: 1.8rem; font-size: 1.8rem;
color: white; color: white;
cursor: pointer; cursor: pointer;
transition: all 0.3s; border-radius: 0.5rem;
border-radius: 8px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0.3rem; padding: 0.3rem;
transition: background-color 0.3s;
} }
.signup-container .return:hover { .signup-container .return:hover {
background-color: rgba(0, 0, 0, 0.4); background-color: rgba(0, 0, 0, 0.4);
} }
.password-visibility { .password-visibility {
position: absolute; position: absolute;
font-size: 1.7rem; font-size: 1.7rem;
top: 28%; top: 28%;
right: 5px; right: 0.3125rem;
z-index: 5;
cursor: pointer; cursor: pointer;
border: none; border: none;
background-color: transparent; background-color: transparent;
color: white; color: white;
} }
.pw-requirements { .pw-requirements {
font-size: 0.7rem; font-size: 0.7rem;
} }

View File

@ -3,16 +3,13 @@ table.song-list {
border-collapse: collapse; border-collapse: collapse;
tr.song-list-item { tr.song-list-item {
border: solid; border: 1px solid #303030; /* Simplified border shorthand */
border-width: 1px 0;
border-color: #303030;
position: relative; position: relative;
td { td {
color: $text-controls-color; color: $text-controls-color;
white-space: nowrap; white-space: nowrap;
padding-left: 10px; padding: 0.6rem; /* Adjusted padding for consistency */
padding-right: 10px;
a { a {
text-decoration: none; text-decoration: none;
@ -25,27 +22,25 @@ table.song-list {
} }
td.song-image { td.song-image {
width: 35px; width: 2.2rem; /* Replaced static px with rem */
display: flex; display: flex;
img.song-image { img.song-image {
position: absolute; position: absolute;
top: 50%; top: 50%;
-ms-transform: translateY(-50%);
transform: translateY(-50%); transform: translateY(-50%);
width: 35px; width: 2.2rem; /* Adjusted to rem */
height: 35px; height: 2.2rem; /* Adjusted to rem */
border-radius: 5px; border-radius: 0.3rem; /* Adjusted radius */
} }
svg.song-image-overlay { svg.song-image-overlay {
position: absolute; position: absolute;
top: 50%; top: 50%;
-ms-transform: translateY(-50%);
transform: translateY(-50%); transform: translateY(-50%);
width: 35px; width: 2.2rem; /* Adjusted to rem */
height: 35px; height: 2.2rem; /* Adjusted to rem */
border-radius: 5px; border-radius: 0.3rem; /* Adjusted radius */
fill: $text-controls-color; fill: $text-controls-color;
} }
@ -68,11 +63,7 @@ table.song-list {
button { button {
svg.hmirror { svg.hmirror {
-moz-transform: scale(-1, 1); transform: scale(-1, 1); /* Removed redundant prefixes */
-webkit-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
transform: scale(-1, 1);
} }
.controlbtn { .controlbtn {
@ -88,7 +79,7 @@ table.song-list {
} }
background-color: transparent; background-color: transparent;
border: transparent; border: none; /* Simplified border */
} }
.hide-until-hover { .hide-until-hover {
@ -121,4 +112,4 @@ table.song-list {
} }
} }
} }
} }

View File

@ -10,7 +10,9 @@ $controls-click-color: #909090;
$play-bar-background-color: #212121; $play-bar-background-color: #212121;
$play-grad-start: #0a0533; $play-grad-start: #0a0533;
$play-grad-end: $accent-color; $play-grad-end: $accent-color;
$border-color: #7851ed;
$queue-background-color: $play-bar-background-color; $queue-background-color: $play-bar-background-color;
$error-color: red;
$auth-inputs: #796dd4; $auth-inputs: #796dd4;
$auth-containers: white; $auth-containers: white;