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;