diff --git a/webui/static/downloads.js b/webui/static/downloads.js index 5ae02166..b82e8c76 100644 --- a/webui/static/downloads.js +++ b/webui/static/downloads.js @@ -5304,8 +5304,13 @@ function _gsSourceRowHtml() { const title = fallback ? `${info.text} unavailable — served from ${(SOURCE_LABELS[fallback] || {}).text || fallback}` : info.text; + const glyph = loading + ? '⏳' + : (info.logo + ? `` + : info.icon); return ``; }).join('') + ''; diff --git a/webui/static/search.js b/webui/static/search.js index e88287b7..e3fe134a 100644 --- a/webui/static/search.js +++ b/webui/static/search.js @@ -106,15 +106,29 @@ function initializeSearchModeToggle() { const title = fallback ? `${info.text} unavailable — served from ${(SOURCE_LABELS[fallback] || {}).text || fallback}` : info.text; + // Prefer the brand logo when available; fall back to the emoji. + // Spinner glyph overrides both while loading. + const glyph = loading + ? '⏳' + : (info.logo + ? `` + : info.icon); return ` `; }).join(''); sourceRow.querySelectorAll('.enh-source-icon').forEach(btn => { - btn.addEventListener('click', () => setActiveSource(btn.dataset.source)); + btn.addEventListener('click', (e) => { + // Stop the outside-click document handler from dismissing the + // dropdown. `renderSourceRow` re-renders after a source switch, + // detaching this button from the DOM, so the bubbled handler's + // `closest('#enh-source-row')` would fail on the detached target. + e.stopPropagation(); + setActiveSource(btn.dataset.source); + }); }); } @@ -424,9 +438,13 @@ function initializeSearchModeToggle() { const dropdown = document.getElementById('enhanced-dropdown'); if (dropdown && !dropdown.classList.contains('hidden')) { const isClickInside = e.target.closest('.enhanced-search-input-wrapper'); + // Source icons live above the input, outside the dropdown — they + // control which cached source is shown, so don't dismiss when the + // user clicks them. + const isClickOnSourceRow = e.target.closest('#enh-source-row'); // Modal sits above the dropdown; closing it shouldn't dismiss results. const isClickInModal = e.target.closest('.download-missing-modal'); - if (!isClickInside && !isClickInModal) { + if (!isClickInside && !isClickOnSourceRow && !isClickInModal) { hideDropdown(); } } diff --git a/webui/static/shared-helpers.js b/webui/static/shared-helpers.js index 6b4b5796..662f881e 100644 --- a/webui/static/shared-helpers.js +++ b/webui/static/shared-helpers.js @@ -32,17 +32,50 @@ async function enhancedSearchFetch(query, { source = null, signal = null } = {}) } // Per-source labels + tab/badge CSS classes + icon glyph for the source -// picker row. Referenced by both the Search page and the global search -// widget for consistent badge/icon rendering. +// picker row. The `logo` URL (when present) renders as an in the +// source-picker chip; `icon` stays as the emoji fallback for sources +// without a canonical logo. Logo URLs mirror the constants in core.js so +// both places stay in sync. const SOURCE_LABELS = { - spotify: { text: 'Spotify', icon: '🎵', tabClass: 'enh-tab-spotify', badgeClass: 'enh-badge-spotify' }, - itunes: { text: 'Apple Music', icon: '🍎', tabClass: 'enh-tab-itunes', badgeClass: 'enh-badge-itunes' }, - deezer: { text: 'Deezer', icon: '🎶', tabClass: 'enh-tab-deezer', badgeClass: 'enh-badge-deezer' }, - discogs: { text: 'Discogs', icon: '📀', tabClass: 'enh-tab-discogs', badgeClass: 'enh-badge-discogs' }, - hydrabase: { text: 'Hydrabase', icon: '💎', tabClass: 'enh-tab-hydrabase', badgeClass: 'enh-badge-hydrabase' }, - musicbrainz: { text: 'MusicBrainz', icon: '🧠', tabClass: 'enh-tab-musicbrainz', badgeClass: 'enh-badge-musicbrainz' }, - youtube_videos: { text: 'Music Videos', icon: '🎬', tabClass: 'enh-tab-youtube', badgeClass: 'enh-badge-youtube' }, - soulseek: { text: 'Soulseek', icon: '🎼', tabClass: 'enh-tab-soulseek', badgeClass: 'enh-badge-soulseek' }, + spotify: { + text: 'Spotify', icon: '🎵', + logo: 'https://storage.googleapis.com/pr-newsroom-wp/1/2023/05/Spotify_Primary_Logo_RGB_Green.png', + tabClass: 'enh-tab-spotify', badgeClass: 'enh-badge-spotify', + }, + itunes: { + text: 'Apple Music', icon: '🍎', + logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/ITunes_logo.svg/960px-ITunes_logo.svg.png', + tabClass: 'enh-tab-itunes', badgeClass: 'enh-badge-itunes', + }, + deezer: { + text: 'Deezer', icon: '🎶', + logo: 'https://cdn.brandfetch.io/idEUKgCNtu/theme/dark/symbol.svg?c=1bxid64Mup7aczewSAYMX&t=1758260798610', + tabClass: 'enh-tab-deezer', badgeClass: 'enh-badge-deezer', + }, + discogs: { + text: 'Discogs', icon: '📀', + logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/6b/Discogs_icon.svg/960px-Discogs_icon.svg.png', + tabClass: 'enh-tab-discogs', badgeClass: 'enh-badge-discogs', + }, + hydrabase: { + text: 'Hydrabase', icon: '💎', + logo: '/static/hydrabase.png', + tabClass: 'enh-tab-hydrabase', badgeClass: 'enh-badge-hydrabase', + }, + musicbrainz: { + text: 'MusicBrainz', icon: '🧠', + logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/MusicBrainz_Logo_%282016%29.svg/500px-MusicBrainz_Logo_%282016%29.svg.png', + tabClass: 'enh-tab-musicbrainz', badgeClass: 'enh-badge-musicbrainz', + }, + youtube_videos: { + text: 'Music Videos', icon: '🎬', + tabClass: 'enh-tab-youtube', badgeClass: 'enh-badge-youtube', + }, + soulseek: { + // No canonical brand logo available — stick with a basic music glyph. + text: 'Soulseek', icon: '🎼', + tabClass: 'enh-tab-soulseek', badgeClass: 'enh-badge-soulseek', + }, }; // Canonical display order for the source picker. Standard metadata sources diff --git a/webui/static/style.css b/webui/static/style.css index 307f0bef..4f3b54c0 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -5484,6 +5484,7 @@ body.helper-mode-active #dashboard-activity-feed:hover { display: flex; flex-wrap: nowrap; overflow-x: auto; + overflow-y: visible; gap: 6px; padding: 10px 14px 8px; flex-shrink: 0; @@ -5520,7 +5521,21 @@ body.helper-mode-active #dashboard-activity-feed:hover { color: #fff; border-color: rgba(255, 255, 255, 0.14); } -.gsearch-source-icon-glyph { font-size: 16px; line-height: 1; } +.gsearch-source-icon-glyph { + font-size: 16px; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; +} +.gsearch-source-icon-glyph img { + width: 18px; + height: 18px; + object-fit: contain; + display: block; +} .gsearch-source-icon-label { font-size: 10px; letter-spacing: 0.02em; } .gsearch-source-icon[data-source="spotify"].active { background: rgba(29, 185, 84, 0.18); color: #1db954; border-color: rgba(29, 185, 84, 0.4); } @@ -33603,11 +33618,16 @@ div.artist-hero-badge { display: flex; flex-wrap: nowrap; overflow-x: auto; + overflow-y: visible; gap: 10px; padding: 10px 4px 8px; margin: 8px 0 12px; scrollbar-width: thin; scrollbar-color: rgba(255, 255, 255, 0.2) transparent; + /* Don't compress when the parent flex container is tight (e.g. when a + `.search-section.active` sibling has flex-grow:1 and the viewport + forces a layout squeeze — would otherwise clip the icons). */ + flex-shrink: 0; } .enh-source-row::-webkit-scrollbar { height: 6px; } .enh-source-row::-webkit-scrollbar-track { background: transparent; } @@ -33644,6 +33664,17 @@ div.artist-hero-badge { .enh-source-icon-glyph { font-size: 20px; line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; +} +.enh-source-icon-glyph img { + width: 22px; + height: 22px; + object-fit: contain; + display: block; } .enh-source-icon-label { font-size: 11px;