mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-21 20:31:21 +00:00
feat(library): sort and badge by personal difficulty rating
Adds sort=difficulty/difficulty-desc to the library API (correlated subquery over song_user_meta.user_difficulty, unrated songs pushed to the bottom either direction, same pattern as the existing mastery sort) and surfaces the rating as a badge on library cards in both the v2 grid/tree views and the v3 grid. The rating itself already existed (song_user_meta) — this just makes it sortable and visible, so it's no longer only readable in the per-song edit drawer. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
115c3529e9
commit
75673c3b47
12
server.py
12
server.py
@ -4107,6 +4107,18 @@ class MetadataDB:
|
||||
"((SELECT MAX(best_accuracy) FROM song_stats s WHERE s.filename = songs.filename) IS NULL) ASC, "
|
||||
"(SELECT MAX(best_accuracy) FROM song_stats s WHERE s.filename = songs.filename) DESC"
|
||||
),
|
||||
# Personal difficulty rating (song_user_meta.user_difficulty, 1..5 —
|
||||
# manually set or seeded by the difficulty_tagger plugin), via a
|
||||
# correlated subquery like mastery above (drops to OFFSET paging).
|
||||
# Unrated songs push to the bottom in both directions.
|
||||
"difficulty": (
|
||||
"((SELECT user_difficulty FROM song_user_meta u WHERE u.filename = songs.filename) IS NULL) ASC, "
|
||||
"(SELECT user_difficulty FROM song_user_meta u WHERE u.filename = songs.filename) ASC"
|
||||
),
|
||||
"difficulty-desc": (
|
||||
"((SELECT user_difficulty FROM song_user_meta u WHERE u.filename = songs.filename) IS NULL) ASC, "
|
||||
"(SELECT user_difficulty FROM song_user_meta u WHERE u.filename = songs.filename) DESC"
|
||||
),
|
||||
}
|
||||
if group and sort in ("mastery", "mastery-desc"):
|
||||
# Sort law (§7.1): mastery aggregates MAX across the WHOLE group —
|
||||
|
||||
@ -1110,6 +1110,7 @@ const _LIB_VIEW_VALUES = new Set(['grid', 'tree', 'folder']);
|
||||
const _LIB_SORT_VALUES = new Set([
|
||||
'artist', 'artist-desc', 'title', 'title-desc',
|
||||
'recent', 'year-desc', 'year', 'tuning',
|
||||
'difficulty', 'difficulty-desc',
|
||||
]);
|
||||
const _LIB_FORMAT_VALUES = new Set(['', 'sloppak', 'loose']);
|
||||
// Tree-view expand/collapse persistence. Three states per tree:
|
||||
@ -2078,6 +2079,7 @@ function renderGridCards(songs, containerId = 'lib-grid', mode = 'replace') {
|
||||
${(() => { const _nm = _getArrangementNamingMode(); return (song.arrangements || []).map(a => _arrangementBadgeHtml(a, _nm)).join(''); })()}
|
||||
${tuning ? `<span class="px-1.5 py-0.5 rounded ${tuning === 'E Standard' ? 'bg-green-900/30 text-green-400' : 'bg-yellow-900/30 text-yellow-400'}">${esc(tuning)}</span>` : ''}
|
||||
${song.has_lyrics ? `<span class="px-1.5 py-0.5 bg-purple-900/30 rounded text-purple-300">Lyrics</span>` : ''}
|
||||
${song.user_difficulty != null ? `<span class="px-1.5 py-0.5 bg-blue-900/30 rounded text-blue-300" title="Your difficulty rating">◆${song.user_difficulty}</span>` : ''}
|
||||
${duration ? `<span class="text-gray-600">${duration}</span>` : ''}
|
||||
</div>
|
||||
${retuneBtn}
|
||||
@ -2277,6 +2279,8 @@ async function renderTreeInto(containerId, countId, stats, letter, q, favoritesO
|
||||
html += `<span class="px-1.5 py-0.5 rounded ${tuning === 'E Standard' ? 'bg-green-900/30 text-green-400' : 'bg-yellow-900/30 text-yellow-400'}">${esc(tuning)}</span>`;
|
||||
if (song.has_lyrics)
|
||||
html += `<span class="px-1.5 py-0.5 bg-purple-900/30 rounded text-purple-300">Lyrics</span>`;
|
||||
if (song.user_difficulty != null)
|
||||
html += `<span class="px-1.5 py-0.5 bg-blue-900/30 rounded text-blue-300" title="Your difficulty rating">◆${song.user_difficulty}</span>`;
|
||||
if (duration)
|
||||
html += `<span class="text-gray-600 w-10 text-right">${duration}</span>`;
|
||||
if (stdRetune)
|
||||
|
||||
@ -119,6 +119,8 @@
|
||||
<option value="year-desc">Year (newest)</option>
|
||||
<option value="year">Year (oldest)</option>
|
||||
<option value="tuning">Tuning</option>
|
||||
<option value="difficulty">Difficulty (easiest first)</option>
|
||||
<option value="difficulty-desc">Difficulty (hardest first)</option>
|
||||
</select>
|
||||
<!-- Format filter (shared) -->
|
||||
<select id="lib-format" onchange="sortLibrary()"
|
||||
|
||||
@ -38,6 +38,9 @@
|
||||
// Mastery = best accuracy across arrangements (song_stats); unscored songs
|
||||
// sort last either way. Ascending surfaces what needs work; never default.
|
||||
['mastery', 'Needs practice first'], ['mastery-desc', 'Most mastered first'],
|
||||
// Personal difficulty (song_user_meta.user_difficulty, 1-5); unrated
|
||||
// songs sort last either way.
|
||||
['difficulty', 'Difficulty (easiest first)'], ['difficulty-desc', 'Difficulty (hardest first)'],
|
||||
];
|
||||
const FORMATS = [['', 'All formats'], ['sloppak', 'Feedpak'], ['loose', 'Folder']];
|
||||
const ARRANGEMENTS = ['Lead', 'Rhythm', 'Bass', 'Combo', 'Vocals'];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user