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:
parent
d349754a93
commit
52894d3c65
3 changed files with 102 additions and 2 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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, '"')})` : ''}">
|
||||
<span>ⓘ</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>💿</span> View Discography
|
||||
</div>
|
||||
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); toggleYourArtistWatchlist(0,'${escapeForInlineJs(node.name)}','${escapeForInlineJs(bestId)}','${bestSource}',null)">
|
||||
<span>👁</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>🎵</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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue