feat(library): sort control for the Tracks tab (SQL-side)
The Tracks tab is server-paginated (200 rows/page), so the new 8-option sort dropdown (Default / Artist / Title / Year / Date added, mirroring the Albums toolbar control) rides the SQL query through a validated ORDER BY allowlist in search_with_filter_page (same pattern as the albums page query), re-queries page 1 on change (gen-bumped, stale in-flight pages drop), and persists in locallibrary_prefs. Every sort key ends in id so LIMIT/OFFSET pagination is deterministic across ties. The once-only Plex page-1 merge re-sorts client-side with a comparator matching the SQL semantics. Group modes keep their client-side visual reorder on top, unchanged. Zero new msgids — all labels already exist. cc
This commit is contained in:
7 files changed
+172
-25
No files matched your search
@@ -2905,7 +2905,7 @@ impl LibraryDatabase {
|
||||
}
|
||||
|
||||
/// Paged variant of `search_with_filter` — the performant path for the
|
||||
/// Tracks tab. Returns one page (`LIMIT`/`OFFSET`) with the SAME ordering,
|
||||
/// Tracks tab. Returns one page (`LIMIT`/`OFFSET`) in the `sort` order,
|
||||
/// so the frontend never materializes the whole table (the documented
|
||||
/// ~16K-row freeze). An empty `query` matches everything.
|
||||
pub fn search_with_filter_page(
|
||||
@@ -2915,6 +2915,7 @@ impl LibraryDatabase {
|
||||
limit: u64,
|
||||
include_qobuz_downloads: bool,
|
||||
exclude_network_folders: bool,
|
||||
sort: &str,
|
||||
) -> Result<Vec<LocalTrack>, LibraryError> {
|
||||
let pattern = format!("%{}%", query);
|
||||
let source_filter = if include_qobuz_downloads {
|
||||
@@ -2931,19 +2932,38 @@ impl LibraryDatabase {
|
||||
} else {
|
||||
""
|
||||
};
|
||||
// ORDER BY clause is built from a validated allowlist so user
|
||||
// input never reaches the SQL string directly. NULL years always
|
||||
// sort last regardless of direction; the fallback ("default" or
|
||||
// any unknown key) is the historical album-grouped order. Every
|
||||
// explicit key ends in `id` so LIMIT/OFFSET pagination is
|
||||
// deterministic across ties (mass ties are real: a batch scan
|
||||
// shares one indexed_at).
|
||||
let order_clause = match sort {
|
||||
"title-asc" => "title COLLATE NOCASE, artist COLLATE NOCASE, id",
|
||||
"title-desc" => "title COLLATE NOCASE DESC, artist COLLATE NOCASE, id",
|
||||
"artist-asc" => "COALESCE(album_artist, artist) COLLATE NOCASE, album COLLATE NOCASE, disc_number, track_number, id",
|
||||
"artist-desc" => "COALESCE(album_artist, artist) COLLATE NOCASE DESC, album COLLATE NOCASE, disc_number, track_number, id",
|
||||
"year-desc" => "year IS NULL, year DESC, album COLLATE NOCASE, disc_number, track_number, id",
|
||||
"year-asc" => "year IS NULL, year ASC, album COLLATE NOCASE, disc_number, track_number, id",
|
||||
"added-desc" => "indexed_at DESC, album COLLATE NOCASE, disc_number, track_number, id",
|
||||
// Default = the pre-sort hardcoded order (album-grouped).
|
||||
_ => "album COLLATE NOCASE, \
|
||||
COALESCE(album_artist, artist) COLLATE NOCASE, \
|
||||
disc_number, \
|
||||
track_number, \
|
||||
title COLLATE NOCASE",
|
||||
};
|
||||
let sql = format!(
|
||||
"SELECT {} FROM local_tracks \
|
||||
WHERE (title LIKE ?1 OR artist LIKE ?1 OR album LIKE ?1) \
|
||||
{} {} \
|
||||
ORDER BY album COLLATE NOCASE, \
|
||||
COALESCE(album_artist, artist) COLLATE NOCASE, \
|
||||
disc_number, \
|
||||
track_number, \
|
||||
title COLLATE NOCASE \
|
||||
ORDER BY {} \
|
||||
LIMIT ?2 OFFSET ?3",
|
||||
Self::TRACK_COLUMNS,
|
||||
source_filter,
|
||||
network_filter,
|
||||
order_clause,
|
||||
);
|
||||
let mut stmt = self
|
||||
.conn
|
||||
|
||||
Reference in new issue
Block a user