Move enhancedSearchFetch, SOURCE_LABELS, renderCompactSection to shared-helpers
These three utilities lived inside search.js — the fetch helper at module scope, and SOURCE_LABELS plus renderCompactSection as closures inside initializeSearchModeToggle. The global search widget in downloads.js already depends on enhancedSearchFetch via global scope and re-implements the rendering inline. Hoist all three to shared-helpers.js so both surfaces share the same implementations. No behavior change — this is the refactor step that precedes the source-picker redesign. Also adds a 'soulseek' entry to SOURCE_LABELS for the upcoming icon row.
This commit is contained in:
parent
4b619951ff
commit
f85564a2de
2 changed files with 154 additions and 137 deletions
|
|
@ -1,21 +1,8 @@
|
||||||
// SEARCH FUNCTIONALITY
|
// SEARCH FUNCTIONALITY
|
||||||
// ===============================
|
// ===============================
|
||||||
|
// `enhancedSearchFetch`, `SOURCE_LABELS`, and `renderCompactSection` live in
|
||||||
// Shared enhanced-search fetch used by the Search page and the global widget.
|
// shared-helpers.js so the Search page and the global widget share the same
|
||||||
// Pass source to restrict results to a single metadata provider; omit or pass
|
// implementations.
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeSearch() {
|
function initializeSearch() {
|
||||||
// --- FIX: Corrected the element IDs to match the HTML ---
|
// --- FIX: Corrected the element IDs to match the HTML ---
|
||||||
|
|
@ -107,15 +94,7 @@ function initializeSearchModeToggle() {
|
||||||
let _activeSearchSource = null; // Currently displayed source tab
|
let _activeSearchSource = null; // Currently displayed source tab
|
||||||
let _altSourceController = null; // AbortController for alternate source fetches
|
let _altSourceController = null; // AbortController for alternate source fetches
|
||||||
|
|
||||||
const SOURCE_LABELS = {
|
// SOURCE_LABELS now lives in shared-helpers.js.
|
||||||
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' },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Live search with debouncing
|
// Live search with debouncing
|
||||||
if (enhancedInput) {
|
if (enhancedInput) {
|
||||||
|
|
@ -808,118 +787,7 @@ function initializeSearchModeToggle() {
|
||||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCompactSection(sectionId, listId, countId, items, mapItem) {
|
// renderCompactSection now lives in shared-helpers.js.
|
||||||
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
|
|
||||||
? `<img src="${escapeHtml(config.image)}" class="${imageClass}" alt="${escapeHtml(config.name)}">`
|
|
||||||
: `<div class="${placeholderClass}" data-lazy-image="true">${config.placeholder}</div>`;
|
|
||||||
|
|
||||||
const badgeHtml = config.badge
|
|
||||||
? `<div class="enh-item-badge ${config.badge.class}">${config.badge.text}</div>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const durationHtml = config.duration && isTrack
|
|
||||||
? `<div class="enh-item-duration">
|
|
||||||
${escapeHtml(config.duration)}
|
|
||||||
<button class="enh-item-play-btn" title="Stream this track">▶</button>
|
|
||||||
</div>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
elem.innerHTML = `
|
|
||||||
${imageHtml}
|
|
||||||
<div class="enh-item-info">
|
|
||||||
<div class="enh-item-name">${escapeHtml(config.name)}</div>
|
|
||||||
<div class="enh-item-meta">${escapeHtml(config.meta)}</div>
|
|
||||||
</div>
|
|
||||||
${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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleEnhancedSearchAlbumClick(album) {
|
async function handleEnhancedSearchAlbumClick(album) {
|
||||||
console.log(`💿 Enhanced search album clicked: ${album.name} by ${album.artist}`);
|
console.log(`💿 Enhanced search album clicked: ${album.name} by ${album.artist}`);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
? `<img src="${escapeHtml(config.image)}" class="${imageClass}" alt="${escapeHtml(config.name)}">`
|
||||||
|
: `<div class="${placeholderClass}" data-lazy-image="true">${config.placeholder}</div>`;
|
||||||
|
|
||||||
|
const badgeHtml = config.badge
|
||||||
|
? `<div class="enh-item-badge ${config.badge.class}">${config.badge.text}</div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const durationHtml = config.duration && isTrack
|
||||||
|
? `<div class="enh-item-duration">
|
||||||
|
${escapeHtml(config.duration)}
|
||||||
|
<button class="enh-item-play-btn" title="Stream this track">▶</button>
|
||||||
|
</div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
elem.innerHTML = `
|
||||||
|
${imageHtml}
|
||||||
|
<div class="enh-item-info">
|
||||||
|
<div class="enh-item-name">${escapeHtml(config.name)}</div>
|
||||||
|
<div class="enh-item-meta">${escapeHtml(config.meta)}</div>
|
||||||
|
</div>
|
||||||
|
${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)
|
// Discography completion checking (for artist-detail pages, library page)
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue