Artist Map polish: keyboard shortcuts, context menu, filter toggle

- Keyboard: Escape close, +/- zoom, F fit, S search, H toggle similar
- Right-click context menu: Artist Info, View Discography, Watchlist,
  Open on Spotify — glass-style with auto-close
- Filter toggle button + H key: hide/show similar artists for clean
  watchlist-only overview
- Removed node dragging (caused visual desync with offscreen buffer)
- Better loading progress text
- Cleanup: keyboard handler removed on close, context menu hidden
This commit is contained in:
Broque Thomas 2026-04-04 11:41:23 -07:00
parent d349754a93
commit 52894d3c65
3 changed files with 102 additions and 2 deletions

View file

@ -3079,6 +3079,9 @@
<div class="artist-map-search">
<input type="text" id="artist-map-search" placeholder="Search artists..." oninput="artMapSearch(this.value)">
</div>
<button class="artist-map-zoom-btn" id="artmap-toggle-similar" onclick="artMapToggleSimilar()" title="Toggle similar artists (H)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><circle cx="12" cy="16" r="0.5" fill="currentColor"/></svg>
</button>
<div class="artist-map-zoom-controls">
<button class="artist-map-zoom-btn" onclick="artMapZoom(1.3)" title="Zoom in">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>

View file

@ -55660,7 +55660,7 @@ async function openArtistMap() {
loadingEl.innerHTML = `
<div class="artist-map-loading-content">
<div class="watch-all-loading-spinner"></div>
<div class="artist-map-loading-text">Building map ${_artMap.placed.length} artists</div>
<div class="artist-map-loading-text" id="artmap-loading-text">Placing ${_artMap.placed.length} artists on the map...</div>
</div>
`;
container.appendChild(loadingEl);
@ -55777,6 +55777,8 @@ function closeArtistMap() {
const container = document.getElementById('artist-map-container');
if (container) container.style.display = 'none';
if (_artMap.animFrame) cancelAnimationFrame(_artMap.animFrame);
if (_artMap._keyHandler) window.removeEventListener('keydown', _artMap._keyHandler);
_artMapHideContextMenu();
// Restore discover sections
document.querySelectorAll('#discover-page > .discover-container > *:not(#artist-map-container)').forEach(el => {
@ -55826,11 +55828,12 @@ function _artMapRebuildBuffer() {
placed.forEach(n => { _artMap._nodeById[n.id] = n; });
}
// Draw ALL nodes — similar first (background), watchlist on top
// Two passes for z-order
const hideSimilar = _artMap._hideSimilar || false;
for (let pass = 0; pass < 2; pass++) {
const isWatchlistPass = pass === 1;
for (const n of visible) {
if ((n.type === 'watchlist') !== isWatchlistPass) continue;
if (hideSimilar && n.type !== 'watchlist') continue;
const op = n.opacity || 0;
if (op < 0.01) continue;
const r = n.radius;
@ -56164,6 +56167,20 @@ function _artMapAnimateConstellation() {
}
}
function artMapToggleSimilar() {
_artMap._hideSimilar = !_artMap._hideSimilar;
_artMap.dirty = true;
_artMapRender();
const btn = document.getElementById('artmap-toggle-similar');
if (btn) btn.style.opacity = _artMap._hideSimilar ? '0.4' : '1';
showToast(_artMap._hideSimilar ? 'Showing watchlist only' : 'Showing all artists', 'info', 1500);
}
function _artMapHideContextMenu() {
const m = document.getElementById('artist-map-context');
if (m) m.style.display = 'none';
}
function _artMapSetupInteraction(canvas) {
let isPanning = false, panStartX = 0, panStartY = 0;
@ -56183,6 +56200,71 @@ function _artMapSetupInteraction(canvas) {
let clickStart = null;
// Keyboard shortcuts
function _artMapKeyHandler(e) {
if (!document.getElementById('artist-map-container') || document.getElementById('artist-map-container').style.display === 'none') return;
if (e.target.tagName === 'INPUT') return; // don't intercept search typing
if (e.key === 'Escape') { closeArtistMap(); e.preventDefault(); }
else if (e.key === '=' || e.key === '+') { artMapZoom(1.3); e.preventDefault(); }
else if (e.key === '-') { artMapZoom(0.7); e.preventDefault(); }
else if (e.key === '0') { artMapFitToView(); e.preventDefault(); }
else if (e.key === 'f' || e.key === 'F') { artMapFitToView(); e.preventDefault(); }
else if (e.key === 's' || e.key === 'S') {
const input = document.getElementById('artist-map-search');
if (input) { input.focus(); e.preventDefault(); }
}
else if (e.key === 'h' || e.key === 'H') {
// Toggle similar artists visibility
_artMap._hideSimilar = !_artMap._hideSimilar;
_artMap.dirty = true;
_artMapRender();
}
}
window.addEventListener('keydown', _artMapKeyHandler);
_artMap._keyHandler = _artMapKeyHandler;
// Right-click context menu
canvas.addEventListener('contextmenu', (e) => {
e.preventDefault();
const { nx, ny } = _artMapScreenToWorld(e, canvas);
const node = _artMapHitTest(nx, ny);
if (!node) { _artMapHideContextMenu(); return; }
const menu = document.getElementById('artist-map-context') || (() => {
const m = document.createElement('div');
m.id = 'artist-map-context';
m.className = 'artmap-context-menu';
document.getElementById('artist-map-container').appendChild(m);
return m;
})();
const hasId = node.spotify_id || node.itunes_id || node.deezer_id;
const activeSource = window._yaActiveSource || 'spotify';
const bestId = node[activeSource + '_id'] || node.spotify_id || node.itunes_id || node.deezer_id || '';
const bestSource = node[activeSource + '_id'] ? activeSource : node.spotify_id ? 'spotify' : node.itunes_id ? 'itunes' : 'deezer';
menu.innerHTML = `
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); ${hasId ? `openYourArtistInfoModal_direct(${JSON.stringify(node).replace(/"/g, '&quot;')})` : ''}">
<span>&#9432;</span> Artist Info
</div>
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); navigateToPage('artists'); setTimeout(() => selectArtistForDetail({id:'${escapeForInlineJs(bestId)}',name:'${escapeForInlineJs(node.name)}',image_url:'${escapeForInlineJs(node.image_url || '')}'},{source:'${bestSource}'}), 200)">
<span>&#128191;</span> View Discography
</div>
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); toggleYourArtistWatchlist(0,'${escapeForInlineJs(node.name)}','${escapeForInlineJs(bestId)}','${bestSource}',null)">
<span>&#128065;</span> ${node.type === 'watchlist' ? 'On Watchlist' : 'Add to Watchlist'}
</div>
${node.spotify_id ? `<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); window.open('https://open.spotify.com/artist/${node.spotify_id}','_blank')"><span>&#127925;</span> Open on Spotify</div>` : ''}
`;
menu.style.display = 'block';
menu.style.left = Math.min(e.clientX, window.innerWidth - 200) + 'px';
menu.style.top = Math.min(e.clientY, window.innerHeight - 200) + 'px';
// Close on next click anywhere
setTimeout(() => {
window.addEventListener('click', _artMapHideContextMenu, { once: true });
}, 10);
});
canvas.addEventListener('mousedown', (e) => {
clickStart = { x: e.clientX, y: e.clientY, time: Date.now() };
isPanning = true;

View file

@ -29780,6 +29780,21 @@ body.helper-mode-active #dashboard-activity-feed:hover {
font-size: 14px; color: rgba(255,255,255,0.5); font-weight: 500;
}
/* Context menu */
.artmap-context-menu {
display: none; position: fixed; z-index: 30;
background: rgba(20,20,32,0.95); border: 1px solid rgba(255,255,255,0.1);
border-radius: 10px; min-width: 180px; overflow: hidden;
backdrop-filter: blur(12px); box-shadow: 0 8px 24px rgba(0,0,0,0.5);
}
.artmap-ctx-item {
padding: 10px 14px; font-size: 13px; color: rgba(255,255,255,0.7);
cursor: pointer; display: flex; align-items: center; gap: 8px;
transition: background 0.1s;
}
.artmap-ctx-item:hover { background: rgba(138,43,226,0.12); color: #fff; }
.artmap-ctx-item span { font-size: 14px; width: 18px; text-align: center; }
/* Loading state */
.ya-loading {
display: flex; align-items: center; gap: 12px; padding: 30px 20px;