Artist Explorer: search-and-select instead of free text
The Explorer prompt accepted any loose text and explored whatever you typed. Now it's a proper picker: type -> debounced search of the metadata source (reuses /api/discover/build-playlist/search-artists — Hydrabase if active, Spotify if configured, else the active metadata source) -> shows real artist results with images -> click one to explore that resolved artist. Enter picks the top match (never explores raw text); Escape/Cancel/backdrop close. Pure frontend: rebuilds _showArtistMapSearchPrompt() (same Promise<name|null> contract, so the caller is unchanged), reusing the playlist-builder's search endpoint + picker styling. No backend change.
This commit is contained in:
parent
1b21983ce7
commit
b85392977c
2 changed files with 101 additions and 16 deletions
|
|
@ -6431,14 +6431,22 @@ async function _openArtistMapExplorerWithName(name) {
|
|||
}
|
||||
|
||||
function _showArtistMapSearchPrompt() {
|
||||
// Search the metadata source and make the user PICK a real artist, rather
|
||||
// than exploring whatever loose text they typed. Resolves with the chosen
|
||||
// artist's resolved name (which the explorer hands to /artist-map/explore),
|
||||
// or null if cancelled.
|
||||
return new Promise(resolve => {
|
||||
const existing = document.getElementById('artmap-search-prompt');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
let done = false;
|
||||
let overlay;
|
||||
const finish = (val) => { if (done) return; done = true; if (overlay) overlay.remove(); resolve(val); };
|
||||
|
||||
overlay = document.createElement('div');
|
||||
overlay.id = 'artmap-search-prompt';
|
||||
overlay.className = 'modal-overlay';
|
||||
overlay.onclick = (e) => { if (e.target === overlay) { overlay.remove(); resolve(null); } };
|
||||
overlay.onclick = (e) => { if (e.target === overlay) finish(null); };
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="artmap-search-prompt-modal">
|
||||
|
|
@ -6449,32 +6457,79 @@ function _showArtistMapSearchPrompt() {
|
|||
</svg>
|
||||
<div>
|
||||
<h3>Artist Explorer</h3>
|
||||
<p>Enter an artist to explore their connections</p>
|
||||
<p>Search and pick an artist to explore</p>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" id="artmap-explore-input" class="artmap-explore-input" placeholder="Artist name..." autofocus>
|
||||
<div class="artmap-explore-search-wrap">
|
||||
<input type="text" id="artmap-explore-input" class="artmap-explore-input"
|
||||
placeholder="Search artists…" autocomplete="off" autofocus>
|
||||
<div class="artmap-explore-spinner" id="artmap-explore-spinner" style="display:none">
|
||||
<div class="watch-all-loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="artmap-explore-results" id="artmap-explore-results"></div>
|
||||
<div class="artmap-search-prompt-actions">
|
||||
<button class="btn btn--sm btn--secondary ya-header-btn" onclick="document.getElementById('artmap-search-prompt').remove()">Cancel</button>
|
||||
<button class="btn btn--sm btn--secondary ya-header-btn ya-viewall-btn" id="artmap-explore-go">
|
||||
<span>Explore</span>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg>
|
||||
</button>
|
||||
<button class="btn btn--sm btn--secondary ya-header-btn" id="artmap-explore-cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const input = overlay.querySelector('#artmap-explore-input');
|
||||
const goBtn = overlay.querySelector('#artmap-explore-go');
|
||||
const results = overlay.querySelector('#artmap-explore-results');
|
||||
const spinner = overlay.querySelector('#artmap-explore-spinner');
|
||||
overlay.querySelector('#artmap-explore-cancel').onclick = () => finish(null);
|
||||
|
||||
const submit = () => {
|
||||
const val = input.value.trim();
|
||||
overlay.remove();
|
||||
resolve(val || null);
|
||||
const renderResults = (artists) => {
|
||||
results.innerHTML = '';
|
||||
if (!artists.length) {
|
||||
results.innerHTML = '<div class="artmap-explore-empty">No artists found</div>';
|
||||
return;
|
||||
}
|
||||
artists.forEach(a => {
|
||||
const img = a.image_url || '/static/placeholder-album.png';
|
||||
const row = document.createElement('div');
|
||||
row.className = 'artmap-explore-result';
|
||||
row.innerHTML = `
|
||||
<img src="${escapeHtml(img)}" alt="" loading="lazy" onerror="this.src='/static/placeholder-album.png'">
|
||||
<span class="artmap-explore-result-name">${escapeHtml(a.name)}</span>
|
||||
<span class="artmap-explore-result-go">Explore →</span>`;
|
||||
row.onclick = () => finish(a.name); // pick the resolved artist, not raw text
|
||||
results.appendChild(row);
|
||||
});
|
||||
};
|
||||
|
||||
goBtn.onclick = submit;
|
||||
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit(); });
|
||||
let timer = null;
|
||||
let token = 0;
|
||||
const doSearch = () => {
|
||||
const q = input.value.trim();
|
||||
if (!q) { results.innerHTML = ''; spinner.style.display = 'none'; clearTimeout(timer); return; }
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(async () => {
|
||||
const myToken = ++token;
|
||||
spinner.style.display = 'flex';
|
||||
try {
|
||||
const resp = await fetch(`/api/discover/build-playlist/search-artists?query=${encodeURIComponent(q)}`);
|
||||
const data = await resp.json();
|
||||
if (myToken !== token) return; // a newer keystroke superseded this
|
||||
renderResults((data && data.success && Array.isArray(data.artists)) ? data.artists : []);
|
||||
} catch (e) {
|
||||
if (myToken === token) results.innerHTML = '<div class="artmap-explore-empty">Search failed — try again</div>';
|
||||
} finally {
|
||||
if (myToken === token) spinner.style.display = 'none';
|
||||
}
|
||||
}, 350);
|
||||
};
|
||||
|
||||
input.addEventListener('input', doSearch);
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const first = results.querySelector('.artmap-explore-result');
|
||||
if (first) first.click(); // Enter = pick top match, never raw text
|
||||
} else if (e.key === 'Escape') {
|
||||
finish(null);
|
||||
}
|
||||
});
|
||||
setTimeout(() => input.focus(), 50);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34358,6 +34358,36 @@ div.artist-hero-badge {
|
|||
.artmap-explore-input::placeholder { color: rgba(255,255,255,0.2); }
|
||||
.artmap-search-prompt-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
|
||||
/* Artist Explorer — search + select picker */
|
||||
.artmap-explore-search-wrap { position: relative; }
|
||||
.artmap-explore-search-wrap .artmap-explore-input { margin-bottom: 12px; }
|
||||
.artmap-explore-spinner { position: absolute; right: 12px; top: 11px; pointer-events: none; }
|
||||
.artmap-explore-spinner .watch-all-loading-spinner { width: 18px; height: 18px; border-width: 2px; }
|
||||
.artmap-explore-results {
|
||||
max-height: 320px; overflow-y: auto; display: flex; flex-direction: column; gap: 4px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.artmap-explore-results:empty { margin-bottom: 0; }
|
||||
.artmap-explore-result {
|
||||
display: flex; align-items: center; gap: 12px; padding: 8px 10px; border-radius: 10px;
|
||||
cursor: pointer; transition: background 0.15s ease;
|
||||
}
|
||||
.artmap-explore-result:hover { background: rgba(138,43,226,0.14); }
|
||||
.artmap-explore-result img {
|
||||
width: 40px; height: 40px; border-radius: 50%; object-fit: cover; flex: 0 0 auto;
|
||||
background: rgba(255,255,255,0.06);
|
||||
}
|
||||
.artmap-explore-result-name {
|
||||
flex: 1 1 auto; min-width: 0; color: #fff; font-size: 14px; font-weight: 600;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.artmap-explore-result-go {
|
||||
flex: 0 0 auto; font-size: 11px; font-weight: 700; color: rgba(138,43,226,0.9);
|
||||
opacity: 0; transition: opacity 0.15s ease;
|
||||
}
|
||||
.artmap-explore-result:hover .artmap-explore-result-go { opacity: 1; }
|
||||
.artmap-explore-empty { padding: 18px; text-align: center; color: rgba(255,255,255,0.35); font-size: 13px; }
|
||||
|
||||
/* Genre picker modal */
|
||||
.artmap-genre-picker-modal {
|
||||
background: linear-gradient(165deg, #1e1e32 0%, #181828 100%);
|
||||
|
|
|
|||
Loading…
Reference in a new issue