diff --git a/webui/static/search.js b/webui/static/search.js
index 0cae5787..42f9b45a 100644
--- a/webui/static/search.js
+++ b/webui/static/search.js
@@ -1,21 +1,8 @@
// SEARCH FUNCTIONALITY
// ===============================
-
-// Shared enhanced-search fetch used by the Search page and the global widget.
-// Pass source to restrict results to a single metadata provider; omit or pass
-// null/'auto' to let the backend fan out across all configured sources.
-async function enhancedSearchFetch(query, { source = null, signal = null } = {}) {
- const body = { query };
- if (source && source !== 'auto') body.source = source;
- const res = await fetch('/api/enhanced-search', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body),
- signal: signal || undefined,
- });
- if (!res.ok) throw new Error(`Enhanced search failed: ${res.status}`);
- return res.json();
-}
+// `enhancedSearchFetch`, `SOURCE_LABELS`, and `renderCompactSection` live in
+// shared-helpers.js so the Search page and the global widget share the same
+// implementations.
function initializeSearch() {
// --- FIX: Corrected the element IDs to match the HTML ---
@@ -107,15 +94,7 @@ function initializeSearchModeToggle() {
let _activeSearchSource = null; // Currently displayed source tab
let _altSourceController = null; // AbortController for alternate source fetches
- const SOURCE_LABELS = {
- spotify: { text: 'Spotify', tabClass: 'enh-tab-spotify', badgeClass: 'enh-badge-spotify' },
- itunes: { text: 'Apple Music', tabClass: 'enh-tab-itunes', badgeClass: 'enh-badge-itunes' },
- deezer: { text: 'Deezer', tabClass: 'enh-tab-deezer', badgeClass: 'enh-badge-deezer' },
- discogs: { text: 'Discogs', tabClass: 'enh-tab-discogs', badgeClass: 'enh-badge-discogs' },
- hydrabase: { text: 'Hydrabase', tabClass: 'enh-tab-hydrabase', badgeClass: 'enh-badge-hydrabase' },
- youtube_videos: { text: 'Music Videos', tabClass: 'enh-tab-youtube', badgeClass: 'enh-badge-youtube' },
- musicbrainz: { text: 'MusicBrainz', tabClass: 'enh-tab-musicbrainz', badgeClass: 'enh-badge-musicbrainz' },
- };
+ // SOURCE_LABELS now lives in shared-helpers.js.
// Live search with debouncing
if (enhancedInput) {
@@ -808,118 +787,7 @@ function initializeSearchModeToggle() {
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
- function renderCompactSection(sectionId, listId, countId, items, mapItem) {
- const section = document.getElementById(sectionId);
- const list = document.getElementById(listId);
- const count = document.getElementById(countId);
-
- if (!list) return;
-
- list.innerHTML = '';
-
- if (!items || items.length === 0) {
- section.classList.add('hidden');
- return;
- }
-
- section.classList.remove('hidden');
- count.textContent = items.length;
-
- // Determine type based on section ID
- const isArtist = sectionId.includes('artists');
- const isAlbum = sectionId.includes('albums') || sectionId.includes('singles');
- const isTrack = sectionId.includes('tracks');
-
- // Add appropriate grid class to list
- if (isArtist) {
- list.classList.add('enh-artists-grid');
- } else if (isAlbum) {
- list.classList.add('enh-albums-grid');
- } else if (isTrack) {
- list.classList.add('enh-tracks-list');
- }
-
- items.forEach(item => {
- const config = mapItem(item);
- const elem = document.createElement('div');
-
- // Add appropriate card class
- if (isArtist) {
- elem.className = 'enh-compact-item artist-card';
- // Add data attributes for lazy loading
- if (item.id) {
- elem.dataset.artistId = item.id;
- elem.dataset.needsImage = config.image ? 'false' : 'true';
- }
- } else if (isAlbum) {
- elem.className = 'enh-compact-item album-card';
- } else if (isTrack) {
- elem.className = 'enh-compact-item track-item';
- }
-
- // Build image HTML with type-specific classes
- let imageClass = 'enh-item-image';
- let placeholderClass = 'enh-item-image-placeholder';
-
- if (isArtist) {
- imageClass += ' artist-image';
- placeholderClass += ' artist-placeholder';
- } else if (isAlbum) {
- imageClass += ' album-cover';
- placeholderClass += ' album-placeholder';
- } else if (isTrack) {
- imageClass += ' track-cover';
- placeholderClass += ' track-placeholder';
- }
-
- const imageHtml = config.image
- ? `
`
- : `
${config.placeholder}
`;
-
- const badgeHtml = config.badge
- ? `${config.badge.text}
`
- : '';
-
- const durationHtml = config.duration && isTrack
- ? `
- ${escapeHtml(config.duration)}
-
-
`
- : '';
-
- elem.innerHTML = `
- ${imageHtml}
-
-
${escapeHtml(config.name)}
-
${escapeHtml(config.meta)}
-
- ${durationHtml}
- ${badgeHtml}
- `;
-
- elem.addEventListener('click', config.onClick);
-
- // Add play button handler for tracks
- if (isTrack && config.onPlay) {
- const playBtn = elem.querySelector('.enh-item-play-btn');
- if (playBtn) {
- playBtn.addEventListener('click', (e) => {
- e.stopPropagation(); // Don't trigger main onClick
- config.onPlay();
- });
- }
- }
-
- list.appendChild(elem);
-
- // Extract colors from image for dynamic glow effect
- if (config.image) {
- extractImageColors(config.image, (colors) => {
- applyDynamicGlow(elem, colors);
- });
- }
- });
- }
+ // renderCompactSection now lives in shared-helpers.js.
async function handleEnhancedSearchAlbumClick(album) {
console.log(`💿 Enhanced search album clicked: ${album.name} by ${album.artist}`);
diff --git a/webui/static/shared-helpers.js b/webui/static/shared-helpers.js
index 312278fb..1a565973 100644
--- a/webui/static/shared-helpers.js
+++ b/webui/static/shared-helpers.js
@@ -12,6 +12,155 @@
// ============================================================================
+// ----------------------------------------------------------------------------
+// Enhanced search shared utilities (used by Search page + global widget)
+// ----------------------------------------------------------------------------
+
+// Pass source to restrict results to a single metadata provider; omit or pass
+// null/'auto' to let the backend fan out across all configured sources.
+async function enhancedSearchFetch(query, { source = null, signal = null } = {}) {
+ const body = { query };
+ if (source && source !== 'auto') body.source = source;
+ const res = await fetch('/api/enhanced-search', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ signal: signal || undefined,
+ });
+ if (!res.ok) throw new Error(`Enhanced search failed: ${res.status}`);
+ return res.json();
+}
+
+// Per-source labels + tab/badge CSS classes. Referenced by both the Search
+// page and the global search widget for consistent badge/icon rendering.
+const SOURCE_LABELS = {
+ spotify: { text: 'Spotify', tabClass: 'enh-tab-spotify', badgeClass: 'enh-badge-spotify' },
+ itunes: { text: 'Apple Music', tabClass: 'enh-tab-itunes', badgeClass: 'enh-badge-itunes' },
+ deezer: { text: 'Deezer', tabClass: 'enh-tab-deezer', badgeClass: 'enh-badge-deezer' },
+ discogs: { text: 'Discogs', tabClass: 'enh-tab-discogs', badgeClass: 'enh-badge-discogs' },
+ hydrabase: { text: 'Hydrabase', tabClass: 'enh-tab-hydrabase', badgeClass: 'enh-badge-hydrabase' },
+ youtube_videos: { text: 'Music Videos', tabClass: 'enh-tab-youtube', badgeClass: 'enh-badge-youtube' },
+ musicbrainz: { text: 'MusicBrainz', tabClass: 'enh-tab-musicbrainz', badgeClass: 'enh-badge-musicbrainz' },
+ soulseek: { text: 'Soulseek', tabClass: 'enh-tab-soulseek', badgeClass: 'enh-badge-soulseek' },
+};
+
+// Render a single enhanced-search result section (artists / albums / tracks).
+// Shared between the Search page and the global widget. The mapItem callback
+// projects each backend item to the card config consumed here.
+function renderCompactSection(sectionId, listId, countId, items, mapItem) {
+ const section = document.getElementById(sectionId);
+ const list = document.getElementById(listId);
+ const count = document.getElementById(countId);
+
+ if (!list) return;
+
+ list.innerHTML = '';
+
+ if (!items || items.length === 0) {
+ section.classList.add('hidden');
+ return;
+ }
+
+ section.classList.remove('hidden');
+ count.textContent = items.length;
+
+ // Determine type based on section ID
+ const isArtist = sectionId.includes('artists');
+ const isAlbum = sectionId.includes('albums') || sectionId.includes('singles');
+ const isTrack = sectionId.includes('tracks');
+
+ // Add appropriate grid class to list
+ if (isArtist) {
+ list.classList.add('enh-artists-grid');
+ } else if (isAlbum) {
+ list.classList.add('enh-albums-grid');
+ } else if (isTrack) {
+ list.classList.add('enh-tracks-list');
+ }
+
+ items.forEach(item => {
+ const config = mapItem(item);
+ const elem = document.createElement('div');
+
+ // Add appropriate card class
+ if (isArtist) {
+ elem.className = 'enh-compact-item artist-card';
+ // Add data attributes for lazy loading
+ if (item.id) {
+ elem.dataset.artistId = item.id;
+ elem.dataset.needsImage = config.image ? 'false' : 'true';
+ }
+ } else if (isAlbum) {
+ elem.className = 'enh-compact-item album-card';
+ } else if (isTrack) {
+ elem.className = 'enh-compact-item track-item';
+ }
+
+ // Build image HTML with type-specific classes
+ let imageClass = 'enh-item-image';
+ let placeholderClass = 'enh-item-image-placeholder';
+
+ if (isArtist) {
+ imageClass += ' artist-image';
+ placeholderClass += ' artist-placeholder';
+ } else if (isAlbum) {
+ imageClass += ' album-cover';
+ placeholderClass += ' album-placeholder';
+ } else if (isTrack) {
+ imageClass += ' track-cover';
+ placeholderClass += ' track-placeholder';
+ }
+
+ const imageHtml = config.image
+ ? `
`
+ : `${config.placeholder}
`;
+
+ const badgeHtml = config.badge
+ ? `${config.badge.text}
`
+ : '';
+
+ const durationHtml = config.duration && isTrack
+ ? `
+ ${escapeHtml(config.duration)}
+
+
`
+ : '';
+
+ elem.innerHTML = `
+ ${imageHtml}
+
+
${escapeHtml(config.name)}
+
${escapeHtml(config.meta)}
+
+ ${durationHtml}
+ ${badgeHtml}
+ `;
+
+ elem.addEventListener('click', config.onClick);
+
+ // Add play button handler for tracks
+ if (isTrack && config.onPlay) {
+ const playBtn = elem.querySelector('.enh-item-play-btn');
+ if (playBtn) {
+ playBtn.addEventListener('click', (e) => {
+ e.stopPropagation(); // Don't trigger main onClick
+ config.onPlay();
+ });
+ }
+ }
+
+ list.appendChild(elem);
+
+ // Extract colors from image for dynamic glow effect
+ if (config.image) {
+ extractImageColors(config.image, (colors) => {
+ applyDynamicGlow(elem, colors);
+ });
+ }
+ });
+}
+
+
// ----------------------------------------------------------------------------
// Discography completion checking (for artist-detail pages, library page)
// ----------------------------------------------------------------------------