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:
parent
c605904a5c
commit
dd20298df4
3 changed files with 97 additions and 9 deletions
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue