mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-10 18:59:56 +00:00
feat(v3 library): A–Z fast-scroll jump rail on the Songs grid (#634)
* feat(v3 library): A–Z fast-scroll jump rail on the Songs grid Adds a vertical letter rail (Plex/Radarr/iOS-contacts pattern) pinned to the right edge next to the scrollbar so you can jump the library to a starting letter — tap, drag-to-scrub with a live letter bubble, or arrow-key between letters. The classic (v2) tree already had letter selection; this brings the new v3 grid to parity (it was the gap behind the "alphabetical scroll selection next to the scrollbar" idea). It shows ONLY for the grid view + alphabetical (artist/title) sorts, and only offers letters present in the current sort AND filter set, so a tap always lands on a real card (absent letters are dimmed + non-interactive). The grid is forward-only, server-paged infinite scroll with no virtualization, so a jump pages through to the target card then scrolls to it; a token guards overlapping jumps (drag) so the newest wins. A keyset-seek + virtualized window is the scaling follow-up for very large libraries. Backend: /api/library/stats gains an optional `sort` param and an additive `sort_letters` map — songs-per-first-letter of the ACTIVE sort column (artist or title), filter-synced — so the rail's present-letters match the grid's real order. The legacy `letters` (distinct-artist) field is unchanged, so the dashboard + classic tree are unaffected. `sort` is dropped for providers whose query_stats predates it (existing kwarg-filter), so third-party library providers keep working (rail simply falls back / hides). Frontend: static/v3/songs.js (refreshRail / jumpToLetter / pointer-drag + keyboard, cards tagged data-letter), static/v3/v3.css (.v3-azrail + bubble). Tests: tests/test_library_filters.py (sort_letters artist/title, song-vs- distinct-artist counting), tests/test_library_providers.py (sort forwarded), tests/js/v3_az_rail.test.js (gating, data-letter, load-through, drag/keys). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(v3 library): harden A–Z jump rail (review P2/P3) Addresses the PR #634 review findings (manual + Codex): P2 correctness - refreshRail prefers the active-sort `sort_letters`; falls back to the artist-based `letters` only on an artist sort, and hides the rail on a title sort when a legacy provider returns none (was mislabeling letters). - reload() bumps `_jumpToken` so an in-flight letter jump can't scroll a grid that's being rebuilt from page 0. - songBucket no longer trims, matching the server SQL + grid ORDER BY raw first-char bucketing (a leading-space title now buckets under '#' on both sides). P3 polish - Paging guard is total-derived (ceil(total/PAGE_SIZE)+2) instead of a magic 4000, keeping large libraries reachable while still bounded. - Roving tabindex: only the first present letter is tabbable; arrow keys move it. Removes up to 27 page tab stops. - `sort_letters` is computed only when the caller opts in (want_sort_letters / route `sort_letters=1`); the dashboard + v2 tree skip the extra GROUP BY. Added sort + want_sort_letters to the optional provider-kwargs so non-introspectable legacy providers drop them. - _railToken supersedes stale refreshRail responses; hide the rail when no letters are present instead of rendering disabled buttons. Tests updated accordingly (v3_az_rail.test.js, test_library_filters.py). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
byrongamatos
parent
b29bab1884
commit
6a71577e05
@@ -1995,10 +1995,24 @@ class MetadataDB:
|
||||
stems_lacks: list[str] | None = None,
|
||||
has_lyrics: int | None = None,
|
||||
tunings: list[str] | None = None,
|
||||
sort: str = "artist",
|
||||
want_sort_letters: bool = False,
|
||||
naming_mode: str = "legacy") -> dict:
|
||||
"""Aggregate stats for the letter bar. Accepts the same filter
|
||||
params as query_page so the letter counts stay synchronized
|
||||
with the grid when filters are active."""
|
||||
with the grid when filters are active.
|
||||
|
||||
`sort` selects the column the v3 jump rail's `sort_letters`
|
||||
breakdown keys on (artist for artist sorts, title for title
|
||||
sorts) so the rail's present-letters match the grid's actual
|
||||
order; other sorts fall back to artist (the rail is hidden for
|
||||
them client-side anyway). The legacy `letters` field is always
|
||||
the artist breakdown, unchanged, for the dashboard + classic tree.
|
||||
|
||||
`sort_letters` is computed (and the key included) ONLY when
|
||||
`want_sort_letters` is set — the jump rail opts in, while the
|
||||
dashboard / v2 tree read only `letters` and skip the extra
|
||||
per-letter aggregate scan."""
|
||||
where, params = self._build_where(
|
||||
q=q, favorites_only=favorites_only, format_filter=format_filter,
|
||||
artist_filter=artist_filter, album_filter=album_filter,
|
||||
@@ -2029,7 +2043,30 @@ class MetadataDB:
|
||||
letters[key] = letters.get(key, 0) + count
|
||||
else:
|
||||
letters["#"] = letters.get("#", 0) + count
|
||||
return {"total_songs": total, "total_artists": artist_count, "letters": letters}
|
||||
result = {"total_songs": total, "total_artists": artist_count, "letters": letters}
|
||||
# Active-sort letter buckets for the v3 jump rail. Counts SONGS (the
|
||||
# grid's unit, unlike `letters` which counts distinct artists) per
|
||||
# first-letter bucket of the column the active sort keys on, so a tap
|
||||
# on a present letter always finds a card. Non-A–Z first chars bucket
|
||||
# under '#'. Only artist/title sorts are alphabetical; anything else
|
||||
# keys on artist here but the client hides the rail for it. Computed
|
||||
# only when the caller opts in, so non-rail callers skip the scan.
|
||||
if want_sort_letters:
|
||||
sort_col = "title" if sort in ("title", "title-desc") else "artist"
|
||||
sort_rows = self.conn.execute(
|
||||
f"SELECT UPPER(SUBSTR(COALESCE({sort_col}, ''), 1, 1)) AS letter, COUNT(*) "
|
||||
f"FROM songs {where} GROUP BY letter", params
|
||||
).fetchall()
|
||||
sort_letters: dict[str, int] = {}
|
||||
for letter, count in sort_rows:
|
||||
count = int(count or 0)
|
||||
if count <= 0:
|
||||
continue
|
||||
key = str(letter or "")
|
||||
bucket = key if (key.isascii() and key.isalpha()) else "#"
|
||||
sort_letters[bucket] = sort_letters.get(bucket, 0) + count
|
||||
result["sort_letters"] = sort_letters
|
||||
return result
|
||||
|
||||
|
||||
class AudioEffectsMappingDB:
|
||||
@@ -2602,7 +2639,7 @@ def _require_library_provider_capability(provider: object, capability: str) -> N
|
||||
)
|
||||
|
||||
|
||||
_OPTIONAL_NEW_PROVIDER_KWARGS = ("naming_mode",)
|
||||
_OPTIONAL_NEW_PROVIDER_KWARGS = ("naming_mode", "sort", "want_sort_letters")
|
||||
|
||||
|
||||
def _filter_provider_kwargs(method: object, kwargs: dict) -> dict:
|
||||
@@ -4539,15 +4576,21 @@ async def library_stats(favorites: int = 0, q: str = "", format: str = "",
|
||||
arrangements_has: str = "", arrangements_lacks: str = "",
|
||||
stems_has: str = "", stems_lacks: str = "",
|
||||
has_lyrics: str = "", tunings: str = "", provider: str = "local",
|
||||
sort: str = "artist", sort_letters: int = 0,
|
||||
naming_mode: str = "legacy"):
|
||||
"""Aggregate stats for the UI. Accepts the same filter params as
|
||||
/api/library so the letter bar mirrors the active grid filter set."""
|
||||
/api/library so the letter bar mirrors the active grid filter set.
|
||||
`sort` selects the column the jump rail's `sort_letters` keys on;
|
||||
`sort_letters=1` opts into that breakdown (the rail), so non-rail
|
||||
callers skip the extra per-letter aggregate."""
|
||||
library_provider = _get_library_provider(provider)
|
||||
_require_library_provider_capability(library_provider, "library.read")
|
||||
return await _call_library_provider_async(
|
||||
library_provider,
|
||||
"query_stats",
|
||||
naming_mode=naming_mode,
|
||||
sort=sort,
|
||||
want_sort_letters=bool(sort_letters),
|
||||
**_library_filter_args(
|
||||
q=q, favorites=favorites, format=format,
|
||||
artist=artist, album=album,
|
||||
|
||||
Reference in New Issue
Block a user