8 Commits

Author SHA1 Message Date
db18298832 Set playbar noderef from GlobalState
Some checks failed
Push Workflows / rustfmt (push) Successful in 17s
Push Workflows / mdbook (push) Successful in 26s
Push Workflows / mdbook-server (push) Successful in 5m16s
Push Workflows / build (push) Failing after 6m1s
Push Workflows / clippy (push) Failing after 6m39s
Push Workflows / docs (push) Successful in 7m1s
Push Workflows / test (push) Successful in 9m36s
Push Workflows / leptos-test (push) Successful in 10m11s
Push Workflows / nix-build (push) Successful in 11m48s
Push Workflows / docker-build (push) Successful in 14m4s
2025-12-01 12:34:49 -05:00
5acdaf9d55 Add playbar_element to GlobalState 2025-12-01 12:34:31 -05:00
28effd4024 Fix styling of personal component to avoid resizing issues 2025-11-22 14:37:49 -05:00
636c811e24 Add get_songs_by_id API function 2025-11-22 14:35:58 -05:00
c5654fc9f7 clippy 2025-11-22 14:35:28 -05:00
dd13cdd2cc fmt 2025-11-22 14:31:18 -05:00
ff8f2ecfca Upgrade to Rust edition 2024 2025-11-22 14:30:50 -05:00
067ce69c4b Create structure for songs and song_list 2025-11-21 10:45:18 -05:00
23 changed files with 137 additions and 24 deletions

View File

@@ -1,7 +1,7 @@
[package]
name = "libretunes"
version = "0.1.0"
edition = "2021"
edition = "2024"
build = "src/build.rs"
[profile.dev]

View File

@@ -201,7 +201,7 @@ pub async fn create_playlist(
user: backend::User,
db_conn: &mut PgPooledConn,
) -> BackendResult<()> {
use image_convert::{to_webp, ImageResource, WEBPConfig};
use image_convert::{ImageResource, WEBPConfig, to_webp};
// Safe to unwrap - "On the server side, this always returns Some(_). On the client side, always returns None."
let mut data = data.into_inner().unwrap();
@@ -278,7 +278,7 @@ pub async fn edit_playlist_image(
state: BackendState,
db_conn: &mut PgPooledConn,
) -> BackendResult<()> {
use image_convert::{to_webp, ImageResource, WEBPConfig};
use image_convert::{ImageResource, WEBPConfig, to_webp};
// Safe to unwrap - "On the server side, this always returns Some(_). On the client side, always returns None."
let mut data = data.into_inner().unwrap();

View File

@@ -37,7 +37,7 @@ pub async fn upload_picture(
}
// Read the image, and convert it to webp
use image_convert::{to_webp, ImageResource, WEBPConfig};
use image_convert::{ImageResource, WEBPConfig, to_webp};
let bytes = field
.bytes()

View File

@@ -196,6 +196,91 @@ pub async fn get_song_by_id(
}
}
#[api_fn(endpoint = "songs/get_many")]
pub async fn get_songs_by_id(
song_ids: Vec<i32>,
user: backend::User,
db_conn: &mut PgPooledConn,
) -> BackendResult<Vec<Option<frontend::Song>>> {
let song_parts: Vec<(
backend::Song,
Option<backend::Album>,
Option<backend::Artist>,
Option<(i32, i32)>,
Option<(i32, i32)>,
)> = songs::table
.filter(songs::id.eq_any(song_ids.clone()))
.left_join(albums::table.on(songs::album_id.eq(albums::id.nullable())))
.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.id))),
)
.left_join(
song_dislikes::table.on(songs::id
.eq(song_dislikes::song_id)
.and(song_dislikes::user_id.eq(user.id))),
)
.select((
songs::all_columns,
albums::all_columns.nullable(),
artists::all_columns.nullable(),
song_likes::all_columns.nullable(),
song_dislikes::all_columns.nullable(),
))
.load(db_conn)
.context("Error loading song from database")?;
let mut songs: HashMap<i32, frontend::Song> = HashMap::new();
for (song, album, artist, like, dislike) in song_parts {
if let Some(last_song) = songs.get_mut(&song.id) {
if let Some(artist) = artist {
last_song.artists.push(artist);
}
} else {
let image_path = song.image_web_path_or_placeholder(album.as_ref());
let song_path = song
.storage_path
.to_web_path(AssetType::Audio)
.context(format!(
"Error converting audio path to web path for song {} (id: {})",
song.title.clone(),
song.id
))?;
let new_song = frontend::Song {
id: song.id,
title: song.title.clone(),
artists: artist.map(|artist| vec![artist]).unwrap_or(vec![]),
album: album.clone(),
track: song.track,
duration: song.duration,
release_date: song.release_date,
song_path,
image_path,
like_dislike: Some((like.is_some(), dislike.is_some())),
added_date: song.added_date,
};
songs.insert(song.id, new_song);
}
}
let songs = song_ids
.iter()
.map(|song_id| songs.remove(song_id))
.collect();
Ok(songs)
}
#[api_fn(endpoint = "songs/plays")]
pub async fn get_song_plays(song_id: i32, db_conn: &mut PgPooledConn) -> BackendResult<i64> {
let plays = song_history::table

View File

@@ -91,7 +91,7 @@ fn HomePage(
</div>
<Personal />
<Queue />
<PlayBar />
<PlayBar {..} node_ref={GlobalState::playbar_element()} />
</section>
}
}

View File

@@ -14,6 +14,7 @@ pub mod queue;
pub mod sidebar;
pub mod song;
pub mod song_list;
pub mod songs;
pub mod upload;
pub mod upload_dropdown;
@@ -38,6 +39,7 @@ pub mod all {
pub use song_list::{
SongAlbum, SongArtists, SongImage, SongLikeDislike, SongList, SongListExtra, SongListItem,
};
pub use songs::all::*;
pub use upload::{Album, Artist, Upload, UploadBtn};
pub use upload_dropdown::{UploadDropdown, UploadDropdownBtn};
}

View File

@@ -5,7 +5,7 @@ use leptos::html::Div;
#[component]
pub fn Personal() -> impl IntoView {
view! {
<div class="home-card">
<div class="home-card w-[250px] min-w-[250px]">
<Profile />
</div>
}
@@ -29,7 +29,7 @@ pub fn Profile() -> impl IntoView {
let user_profile_picture = move || user.get().flatten().map(|user| user.image_path.path());
view! {
<div class="flex w-50 relative">
<div class="flex relative">
<div class="text-lg self-center">
<Suspense
fallback=|| view!{

View File

@@ -9,7 +9,9 @@ fn remove_song_fn(index: usize) {
if index == 0 {
leptos_log!("Error: Trying to remove currently playing song (index 0) from queue");
} else {
leptos_log!("Remove Song from Queue: Song is not currently playing, deleting song from queue and not adding to history");
leptos_log!(
"Remove Song from Queue: Song is not currently playing, deleting song from queue and not adding to history"
);
GlobalState::play_status().update(|status| {
status.queue.remove(index);
});

View File

@@ -1,7 +1,7 @@
use crate::prelude::*;
use leptos::html::Div;
use leptos_router::components::{Form, A};
use leptos_router::components::{A, Form};
use leptos_router::hooks::use_location;
use std::sync::Arc;
use web_sys::Response;

View File

@@ -0,0 +1,7 @@
pub mod song;
pub mod song_list;
pub mod song_list_defaults;
pub mod all {
use super::*;
}

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -1,8 +1,8 @@
use crate::ingest::scan::full_scan;
use crate::prelude::*;
use tokio::task::{spawn, JoinHandle};
use tokio::time::{interval_at, Duration, Instant};
use tokio::task::{JoinHandle, spawn};
use tokio::time::{Duration, Instant, interval_at};
pub const INITIAL_SCAN_DELAY: Duration = Duration::from_secs(10);
pub const SCAN_INTERVAL: Duration = Duration::from_hours(1);

View File

@@ -6,12 +6,12 @@ extern crate diesel_migrations;
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{Router, middleware::from_fn};
use axum::{body::Body, extract::Request, http::Response, middleware::Next};
use axum::{middleware::from_fn, Router};
use axum_login::tower_sessions::SessionManagerLayer;
use axum_login::AuthManagerLayerBuilder;
use axum_login::tower_sessions::SessionManagerLayer;
use http::StatusCode;
use leptos_axum::{file_and_error_handler, generate_route_list, LeptosRoutes};
use leptos_axum::{LeptosRoutes, file_and_error_handler, generate_route_list};
use libretunes::app::*;
use libretunes::prelude::*;
use libretunes::util::config::load_config;

View File

@@ -40,10 +40,10 @@ impl PlayStatus {
/// }
/// ```
pub fn get_audio(&self) -> Option<HtmlAudioElement> {
if let Some(audio) = &self.audio_player {
if let Some(audio) = audio.get() {
return Some(audio);
}
if let Some(audio) = &self.audio_player
&& let Some(audio) = audio.get()
{
return Some(audio);
}
None

View File

@@ -1,6 +1,6 @@
use crate::prelude::*;
use leptos::ev::{keydown, KeyboardEvent};
use leptos::ev::{KeyboardEvent, keydown};
use leptos::html::{Button, Input};
use leptos_router::components::Form;
use leptos_router::hooks::{use_navigate, use_params_map};

View File

@@ -5,7 +5,7 @@ use crate::util::database::{PgPool, PgPooledConn};
use axum::extract::FromRequestParts;
use diesel::r2d2::ConnectionManager;
use http::{request::Parts, StatusCode};
use http::{StatusCode, request::Parts};
use leptos_axum::extract;
use std::ops::Deref;
use std::path::PathBuf;

View File

@@ -93,7 +93,9 @@ impl Config {
Some(url) => url.clone(),
None => match &self.postgres_config {
Some(config) => config.to_url(),
None => panic!("Both database_url and postgres_config are missing. This error shouldn't be possible."),
None => panic!(
"Both database_url and postgres_config are missing. This error shouldn't be possible."
),
},
}
}

View File

@@ -2,7 +2,7 @@ use diesel::{
pg::PgConnection,
r2d2::{ConnectionManager, Pool, PooledConnection},
};
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
pub type PgPool = Pool<ConnectionManager<PgConnection>>;
pub type PgPooledConn = PooledConnection<ConnectionManager<PgConnection>>;

View File

@@ -18,7 +18,7 @@ pub const MUSIC_PLACEHOLDER_WEB_PATH: &str = "/placeholders/MusicPlaceholder.svg
/// Uses 128 bits of randomness encoded in hexadecimal,
/// where `XX` is the first byte and `XXXXXXXXXX` is the remaining bytes.
fn random_path() -> PathBuf {
use rand::{rng, Rng};
use rand::{Rng, rng};
let mut rng = rng();

View File

@@ -1,5 +1,7 @@
use crate::prelude::*;
use leptos::html::Div;
/// Global front-end state
/// Contains anything frequently needed across multiple components
/// Behaves like a singleton, in that `provide_context`/`expect_context` will
@@ -16,10 +18,15 @@ pub struct GlobalState {
/// A resource that fetches the playlists
pub playlists: Resource<BackendResult<Vec<frontend::Playlist>>>,
/// A reference to the playbar
pub playbar_element: NodeRef<Div>,
}
impl GlobalState {
pub fn new() -> Self {
let playbar_element = NodeRef::<Div>::new();
let play_status = RwSignal::new(frontend::PlayStatus::default());
let logged_in_user = Resource::new(
@@ -53,6 +60,7 @@ impl GlobalState {
logged_in_user,
play_status,
playlists,
playbar_element,
}
}
@@ -67,6 +75,10 @@ impl GlobalState {
pub fn playlists() -> Resource<BackendResult<Vec<frontend::Playlist>>> {
expect_context::<Self>().playlists
}
pub fn playbar_element() -> NodeRef<Div> {
expect_context::<Self>().playbar_element
}
}
impl Default for GlobalState {

View File

@@ -45,7 +45,7 @@
@apply text-white;
@apply last-of-type:grow;
@apply last-of-type:pb-[85px]; /* Hard-coded height of the playbar + 5px */
@apply overflow-scroll;
@apply overflow-y-scroll;
}
.menu-btn {