Library page empty state: offer to search metadata sources for the query

When a user types an artist name into the library search and gets no
hits, the old empty state just said "No artists found — try adjusting
your search or filters." Dead end for the common case of "I searched
for someone I don't own yet."

The empty state now detects when libraryPageState.currentSearch is
non-empty and swaps in a CTA that hands the query off to /search:

  "kendrick" isn't in your library
  They might be available on a connected metadata source.
  [🔍 Search online for "kendrick" →]

Clicking the button navigates to /search, pre-fills the enhanced search
input, and dispatches an input event so the existing debounced search
fires automatically. Uses the same hand-off pattern _gsNavigateToSearchPage
already uses for Soulseek, so the Search page's source-picker flow
picks up naturally from there.

No change to the generic empty state (no query active) or to any other
library page behaviour.
This commit is contained in:
Broque Thomas 2026-04-23 14:27:29 -07:00
parent c605904a5c
commit dd20298df4
3 changed files with 97 additions and 9 deletions

View file

@ -2307,11 +2307,19 @@
<!-- Artist cards will be populated here -->
</div>
<!-- Empty State -->
<!-- Empty State — populated by showLibraryEmpty() in library.js.
Shows a generic "no artists" message by default; when the
user's search has no library matches, it switches to a
search-online CTA that hands off to the /search page. -->
<div class="library-empty hidden" id="library-empty">
<div class="empty-icon">🎵</div>
<div class="empty-title">No artists found</div>
<div class="empty-subtitle">Try adjusting your search or filters</div>
<div class="empty-icon" id="library-empty-icon">🎵</div>
<div class="empty-title" id="library-empty-title">No artists found</div>
<div class="empty-subtitle" id="library-empty-subtitle">Try adjusting your search or filters</div>
<button class="library-empty-search-cta hidden" id="library-empty-search-cta">
<span class="library-empty-search-cta-icon">🔍</span>
<span class="library-empty-search-cta-text">Search online for <span id="library-empty-search-cta-query"></span></span>
<span class="library-empty-search-cta-arrow"></span>
</button>
</div>
<!-- Pagination -->

View file

@ -358,13 +358,60 @@ function showLibraryLoading(show) {
function showLibraryEmpty(show) {
const emptyElement = document.getElementById("library-empty");
if (emptyElement) {
if (show) {
emptyElement.classList.remove("hidden");
} else {
emptyElement.classList.add("hidden");
if (!emptyElement) return;
if (!show) {
emptyElement.classList.add("hidden");
return;
}
// When a search query is active and returned zero library hits, swap the
// generic "no artists" copy for a CTA that hands the query off to /search
// so the user can look the artist up across metadata sources without
// retyping.
const query = (libraryPageState.currentSearch || '').trim();
const iconEl = document.getElementById('library-empty-icon');
const titleEl = document.getElementById('library-empty-title');
const subtitleEl = document.getElementById('library-empty-subtitle');
const ctaEl = document.getElementById('library-empty-search-cta');
const ctaQueryEl = document.getElementById('library-empty-search-cta-query');
if (query) {
if (iconEl) iconEl.textContent = '🔎';
if (titleEl) titleEl.textContent = `"${query}" isn't in your library`;
if (subtitleEl) subtitleEl.textContent = 'They might be available on a connected metadata source.';
if (ctaQueryEl) ctaQueryEl.textContent = `"${query}"`;
if (ctaEl) {
ctaEl.classList.remove('hidden');
// Rebind cleanly — onclick avoids duplicate listeners across renders.
ctaEl.onclick = () => _handoffLibrarySearchToEnhancedSearch(query);
}
} else {
if (iconEl) iconEl.textContent = '🎵';
if (titleEl) titleEl.textContent = 'No artists found';
if (subtitleEl) subtitleEl.textContent = 'Try adjusting your search or filters';
if (ctaEl) {
ctaEl.classList.add('hidden');
ctaEl.onclick = null;
}
}
emptyElement.classList.remove("hidden");
}
// Navigate to /search and pre-fill the enhanced search input with the query
// the user had typed into the library search. Uses the same hand-off pattern
// the global widget uses for Soulseek — navigate, then dispatch an `input`
// event so the Search page's existing debounced search kicks in.
function _handoffLibrarySearchToEnhancedSearch(query) {
if (typeof navigateToPage !== 'function') return;
navigateToPage('search');
setTimeout(() => {
const input = document.getElementById('enhanced-search-input');
if (input && query) {
input.value = query;
input.dispatchEvent(new Event('input', { bubbles: true }));
}
}, 300);
}
async function openWatchAllUnwatchedModal() {

View file

@ -23054,6 +23054,39 @@ body.helper-mode-active #dashboard-activity-feed:hover {
max-width: 400px;
}
/* Hand-off CTA shown in the library empty state when the user's search
returns no library matches offers to run the same query against the
configured metadata source on the /search page. */
.library-empty-search-cta {
display: inline-flex;
align-items: center;
gap: 10px;
margin-top: 8px;
padding: 12px 20px;
background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.22), rgba(var(--accent-rgb), 0.08));
border: 1px solid rgba(var(--accent-rgb), 0.35);
border-radius: 10px;
color: rgb(var(--accent-light-rgb, var(--accent-rgb)));
font-family: inherit;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.library-empty-search-cta:hover {
transform: translateY(-1px);
border-color: rgba(var(--accent-rgb), 0.55);
box-shadow: 0 6px 20px rgba(var(--accent-rgb), 0.22);
}
.library-empty-search-cta:active { transform: translateY(0); }
.library-empty-search-cta-icon { font-size: 16px; }
.library-empty-search-cta-arrow {
font-weight: 700;
transition: transform 0.15s ease;
}
.library-empty-search-cta:hover .library-empty-search-cta-arrow { transform: translateX(3px); }
#library-empty-search-cta-query { color: #fff; }
/* Pagination */
.library-pagination {
display: flex;