mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
feat(v3): flag library songs by working-tuning match (working-tuning PR 6) (#668)
* feat(v3): flag library songs by working-tuning match (working-tuning PR 6) Each song's tuning chip in the v3 library grid is now coloured by whether your CURRENT working tuning covers it: green = play it now, amber = needs a retune (with a matching tooltip). Uses the tuner plugin's coverageReport (async), so it runs as a post-paint decoration pass — chips render instantly, then colour a tick later; a token cancels a superseded pass so scrolling stays snappy. Re-flags on working-tuning-changed (retune / instrument swap / reset), no re-fetch. Fully feature-detected: without the tuner coverage API + the host workingTuning state, the chips render exactly as before. v3-only, single file (static/v3/songs.js). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(tuner/library): correct bass matching + memoize player tuning (PR #668 review) Review fixes for working-tuning PR 6 (library tuning-match chips): - Bass songs were scored against the guitar tuning. The chip passed no arrangement to coverageReport, so isBassArrangement fell back to guitar — a 4-string bass drop-D read as guitar could FALSE-MATCH a drop-D guitar player (green). songCard now flags a bass-only song (every arrangement name matches /\bbass\b/) with data-tuning-bass, and decorateTuningChips passes arrangement 'Bass'/'Lead' so coverage uses the right base pitches. Mixed guitar+bass songs → guitar (the song-level tuning is the guitar one); least-wrong given one tuning per song. - Per-chip /api/settings fetch storm. coverageReport()→_playerTuning() fetched /api/settings once per visible chip per grid paint (~60). _playerTuning is now memoized (the player's tuning is song-independent) so all callers share one read; invalidated on instrument:changed / working-tuning-changed, with a 3s TTL so a settings write that doesn't emit an event still heals. A transient fetch failure is NOT cached (next read retries) — else one hiccup would freeze coverage. Tests: player tuning shared across songs (one fetch); transient-failure retry (fails without the fix). The prior #680 dedup test updated for the memoized behavior. 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
6aed8510d7
commit
df4e17bc99
+57
-2
@@ -604,6 +604,41 @@
|
||||
'<button data-arr="' + esc(a.index != null ? a.index : '') + '" title="Play ' + esc(a.name) + '" class="text-[10px] px-1.5 py-0.5 rounded bg-gray-800/60 text-fb-textDim hover:bg-fb-primary hover:text-white transition">' + esc(a.name) + '</button>').join('');
|
||||
}
|
||||
|
||||
// ── Tuning-match flags (working-tuning PR 6) ───────────────────────────────
|
||||
// Colour each song's tuning chip by whether your CURRENT working tuning covers
|
||||
// it: green = play it now, amber = needs a retune. Uses the tuner plugin's
|
||||
// coverage check (async) + the host workingTuning state — BOTH feature-detected,
|
||||
// so without them the chips render exactly as before. Decoration runs AFTER the
|
||||
// (sync) window paint so scrolling stays snappy; a token cancels a superseded pass.
|
||||
let _tuningDecorToken = 0;
|
||||
function _applyChipMatch(chip, stateName) {
|
||||
chip.classList.remove('bg-fb-mid', 'bg-emerald-500', 'bg-amber-400');
|
||||
chip.classList.add(stateName === 'match' ? 'bg-emerald-500'
|
||||
: stateName === 'retune' ? 'bg-amber-400' : 'bg-fb-mid');
|
||||
if (!chip.dataset.baseTitle) chip.dataset.baseTitle = chip.getAttribute('title') || '';
|
||||
chip.setAttribute('title', chip.dataset.baseTitle + (stateName === 'match'
|
||||
? ' — matches your tuning' : stateName === 'retune' ? ' — needs a retune' : ''));
|
||||
}
|
||||
async function decorateTuningChips(grid) {
|
||||
if (!grid) return;
|
||||
const cov = window._tunerAutoOpen && window._tunerAutoOpen.coverageReport;
|
||||
const hasWT = window.feedBack && window.feedBack.workingTuning
|
||||
&& typeof window.feedBack.workingTuning.get === 'function';
|
||||
if (typeof cov !== 'function' || !hasWT) return; // feature-detect → no flags
|
||||
const token = ++_tuningDecorToken;
|
||||
const chips = grid.querySelectorAll('[data-tuning-chip][data-tuning-offsets]');
|
||||
for (const chip of chips) {
|
||||
const offs = chip.getAttribute('data-tuning-offsets').split(',').map(Number);
|
||||
if (!offs.length || offs.some((n) => !isFinite(n))) continue;
|
||||
// Pass the instrument so coverage uses the right base pitches (bass vs guitar).
|
||||
const arrangement = chip.dataset.tuningBass === '1' ? 'Bass' : 'Lead';
|
||||
let rep = null;
|
||||
try { rep = await cov({ tuning: offs, stringCount: offs.length, arrangement: arrangement }); } catch (_) { rep = null; }
|
||||
if (token !== _tuningDecorToken) return; // superseded by a re-paint / tuning change
|
||||
if (rep) _applyChipMatch(chip, rep.covered ? 'match' : 'retune');
|
||||
}
|
||||
}
|
||||
|
||||
function songCard(song) {
|
||||
const fav = song.favorite;
|
||||
const key = cardKey(song);
|
||||
@@ -626,11 +661,22 @@
|
||||
? ('Custom Tuning: ' + targetNotes)
|
||||
: tuningLabel;
|
||||
const pos = 'absolute top-2 ' + (state.selectMode ? 'left-9' : 'left-2');
|
||||
// Tag the chip with its offsets so decorateTuningChips() can colour it
|
||||
// green (matches your current tuning) / amber (needs a retune) after paint.
|
||||
// Also flag a bass-only song (every arrangement is a bass part) so coverage
|
||||
// scores its bass tuning against the bass base pitches, not guitar — otherwise
|
||||
// a 4-string bass tuning read as guitar can false-match a guitar player.
|
||||
const chipArrs = song.arrangements || [];
|
||||
const chipIsBass = chipArrs.length > 0
|
||||
&& chipArrs.every((a) => /\bbass\b/i.test((a && a.name) || ''));
|
||||
const matchAttr = (rawOffsets && rawOffsets.length)
|
||||
? ' data-tuning-chip data-tuning-offsets="' + esc(rawOffsets.join(',')) + '"'
|
||||
+ (chipIsBass ? ' data-tuning-bass="1"' : '') : '';
|
||||
if (targetNotes) {
|
||||
tuning = '<span class="' + pos + ' bg-fb-mid text-black text-[9px] font-bold px-1.5 py-0.5 rounded-sm leading-tight max-w-[5.5rem] text-center" title="' + esc(badgeTitle) + '">'
|
||||
tuning = '<span class="' + pos + ' bg-fb-mid text-black text-[9px] font-bold px-1.5 py-0.5 rounded-sm leading-tight max-w-[5.5rem] text-center"' + matchAttr + ' title="' + esc(badgeTitle) + '">'
|
||||
+ esc('Custom Tuning') + '<br><span class="font-semibold tracking-wide">' + esc(targetNotes) + '</span></span>';
|
||||
} else {
|
||||
tuning = '<span class="' + pos + ' bg-fb-mid text-black text-[10px] font-bold px-1.5 py-0.5 rounded-sm" title="' + esc(badgeTitle) + '">' + esc(tuningLabel) + '</span>';
|
||||
tuning = '<span class="' + pos + ' bg-fb-mid text-black text-[10px] font-bold px-1.5 py-0.5 rounded-sm"' + matchAttr + ' title="' + esc(badgeTitle) + '">' + esc(tuningLabel) + '</span>';
|
||||
}
|
||||
}
|
||||
// Display-only (pointer-events-none) so a click falls through to the
|
||||
@@ -1032,6 +1078,7 @@
|
||||
grid.style.top = (firstRow * rowH) + 'px';
|
||||
grid.innerHTML = _renderCardsRange(start, end);
|
||||
wireCards(grid);
|
||||
decorateTuningChips(grid); // colour tuning chips by working-tuning match (async, feature-detected)
|
||||
state.winRange = { start, end };
|
||||
state.renderedSelectMode = state.selectMode;
|
||||
if (sm && typeof sm.emit === 'function') {
|
||||
@@ -1858,5 +1905,13 @@
|
||||
if (active && active.id === 'v3-songs') { _libraryDirty = false; reload(); }
|
||||
else _libraryDirty = true;
|
||||
});
|
||||
// Your live tuning changed (retune / instrument swap / reset) → re-colour the
|
||||
// visible tuning chips against the new tuning. Cheap: re-decorates in place,
|
||||
// no re-fetch or re-paint. No-op off the Songs grid or without the capability.
|
||||
sm.on('working-tuning-changed', () => {
|
||||
if (typeof songsActive === 'function' && !songsActive()) return;
|
||||
if (state.view !== 'grid') return;
|
||||
decorateTuningChips(_gridEl());
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user