Compare commits

...

1 Commits

Author SHA1 Message Date
Connor Johnstone 07aa9908e8 added the scroll bar 2026-03-31 13:18:18 -04:00
4 changed files with 95 additions and 4 deletions
+55 -2
View File
@@ -1,3 +1,6 @@
use std::collections::{HashMap, HashSet};
use wasm_bindgen::JsCast;
use yew::prelude::*; use yew::prelude::*;
use yew_router::prelude::*; use yew_router::prelude::*;
@@ -17,7 +20,7 @@ pub fn library_page() -> Html {
let artists = artists.clone(); let artists = artists.clone();
let error = error.clone(); let error = error.clone();
wasm_bindgen_futures::spawn_local(async move { wasm_bindgen_futures::spawn_local(async move {
match api::list_artists(200, 0).await { match api::list_artists(0, 0).await {
Ok(a) => artists.set(Some(a)), Ok(a) => artists.set(Some(a)),
Err(e) => error.set(Some(e.0)), Err(e) => error.set(Some(e.0)),
} }
@@ -40,6 +43,33 @@ pub fn library_page() -> Html {
return html! { <p class="loading">{ "Loading..." }</p> }; return html! { <p class="loading">{ "Loading..." }</p> };
}; };
// Pre-compute which artist IDs are first in their letter group (for anchor IDs)
let mut seen_letters = HashSet::new();
let first_of_letter: HashMap<i32, char> = artists
.iter()
.filter_map(|a| {
let first = a.name.chars().next().unwrap_or('#').to_ascii_uppercase();
let letter = if first.is_ascii_alphabetic() {
first
} else {
'#'
};
if seen_letters.insert(letter) {
Some((a.id, letter))
} else {
None
}
})
.collect();
let active_letters: HashSet<char> = first_of_letter.values().copied().collect();
// Build scroll track letter data
let letters: Vec<char> = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ".chars().collect();
let scroll_track_items: Vec<(String, bool)> = letters
.iter()
.map(|&c| (c.to_string(), active_letters.contains(&c)))
.collect();
html! { html! {
<div> <div>
<div class="page-header"> <div class="page-header">
@@ -50,6 +80,28 @@ pub fn library_page() -> Html {
if artists.is_empty() { if artists.is_empty() {
<p class="text-muted">{ "No artists in library. Use Search to add some!" }</p> <p class="text-muted">{ "No artists in library. Use Search to add some!" }</p>
} else { } else {
<div class="scroll-track">
{ for scroll_track_items.iter().map(|(letter, active)| {
let letter_c = letter.clone();
let active = *active;
let class = if active { "scroll-track-letter has-artists" } else { "scroll-track-letter" };
let onclick = Callback::from(move |_: MouseEvent| {
if active {
if let Some(window) = web_sys::window() {
if let Some(doc) = window.document() {
if let Some(el) = doc.get_element_by_id(&format!("letter-{}", letter_c)) {
let _ = el.dyn_into::<web_sys::HtmlElement>()
.map(|el| el.scroll_into_view());
}
}
}
}
});
html! {
<div class={class} onclick={onclick}>{ letter }</div>
}
})}
</div>
<table> <table>
<thead> <thead>
<tr> <tr>
@@ -62,6 +114,7 @@ pub fn library_page() -> Html {
</thead> </thead>
<tbody> <tbody>
{ for artists.iter().map(|a| { { for artists.iter().map(|a| {
let anchor_id = first_of_letter.get(&a.id).map(|c| format!("letter-{c}"));
let artist_id = a.id; let artist_id = a.id;
let artist_name = a.name.clone(); let artist_name = a.name.clone();
let artist_mbid = a.musicbrainz_id.clone(); let artist_mbid = a.musicbrainz_id.clone();
@@ -157,7 +210,7 @@ pub fn library_page() -> Html {
let watched_bar_style = format!("width:{watched_pct}%;background:{watched_color};"); let watched_bar_style = format!("width:{watched_pct}%;background:{watched_color};");
html! { html! {
<tr> <tr id={anchor_id}>
<td> <td>
<Link<Route> to={Route::Artist { id: a.id.to_string() }}> <Link<Route> to={Route::Artist { id: a.id.to_string() }}>
{ &a.name } { &a.name }
+1 -1
View File
@@ -17,7 +17,7 @@ pub fn playlists_page() -> Html {
let all_artists = all_artists.clone(); let all_artists = all_artists.clone();
use_effect_with((), move |_| { use_effect_with((), move |_| {
wasm_bindgen_futures::spawn_local(async move { wasm_bindgen_futures::spawn_local(async move {
if let Ok(artists) = api::list_artists(500, 0).await { if let Ok(artists) = api::list_artists(0, 0).await {
all_artists.set(artists); all_artists.set(artists);
} }
}); });
+34
View File
@@ -445,4 +445,38 @@ tr[draggable="true"]:active { cursor: grabbing; }
.album-art-lg { width: 120px; height: 120px; } .album-art-lg { width: 120px; height: 120px; }
.album-header { flex-direction: column; } .album-header { flex-direction: column; }
.artist-photo { width: 80px; height: 80px; } .artist-photo { width: 80px; height: 80px; }
.scroll-track { right: 2px; width: 14px; }
.scroll-track-letter { font-size: 0.5rem; }
}
/* Alphabetical scroll track */
.scroll-track {
position: fixed;
right: 8px;
top: 60px;
bottom: 20px;
width: 24px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
z-index: 100;
}
.scroll-track-letter {
font-size: 1.4rem;
font-weight: 600;
color: var(--text-muted);
cursor: default;
padding: 0 2px;
line-height: 1;
user-select: none;
opacity: 0.3;
}
.scroll-track-letter.has-artists {
color: var(--text-secondary);
cursor: pointer;
opacity: 1;
}
.scroll-track-letter.has-artists:hover {
color: var(--accent);
} }
+5 -1
View File
@@ -94,7 +94,11 @@ async fn list_artists(
query: web::Query<PaginationParams>, query: web::Query<PaginationParams>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
auth::require_auth(&session)?; auth::require_auth(&session)?;
let artists = queries::artists::list(state.db.conn(), query.limit, query.offset).await?; let artists = if query.limit == 0 {
queries::artists::list_all(state.db.conn()).await?
} else {
queries::artists::list(state.db.conn(), query.limit, query.offset).await?
};
let wanted = queries::wanted::list(state.db.conn(), None, None).await?; let wanted = queries::wanted::list(state.db.conn(), None, None).await?;