From 86e6d8df49a3f16bf10e566ede8162bea4246492 Mon Sep 17 00:00:00 2001
From: Broque Thomas <26755000+Nezreka@users.noreply.github.com>
Date: Thu, 23 Apr 2026 13:53:59 -0700
Subject: [PATCH] Fix source-picker review items: real logos, cached click
close, Soulseek clip
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Three follow-up fixes after browser testing:
1. Clicking a source whose results are already cached was closing the
results dropdown. The outside-click handler treated the icon click
as "outside" because the icon row lives above the input wrapper, not
inside it. The icon click handler now calls stopPropagation so the
document handler never runs. Also added an `#enh-source-row`
whitelist to the search-page outside-click handler as a second
layer of defense.
2. The icon chips used generic emojis (🎵, 🍎, 🎶, etc.) which don't
convey brand identity. SOURCE_LABELS now carries a `logo` URL per
source (mirroring the existing constants in core.js): the real
Spotify / Apple Music / Deezer / Discogs / MusicBrainz / Hydrabase /
Soulseek brand logos render as
inside the chip. Music Videos
stays on emoji since the codebase has no YouTube-specific logo
constant. renderSourceRow (Search page) and _gsSourceRowHtml (global
widget) both honor the new field; loading state still overrides
with an hourglass.
3. When Soulseek was selected, the icon row appeared clipped at the
top of the page. Caused by the flex parent (.downloads-main-panel)
compressing the row when .search-section.active competes for space
with flex-grow:1. Added `flex-shrink: 0` + explicit `overflow-y: visible`
on both .enh-source-row and .gsearch-source-row so the row keeps
its natural height even under layout pressure. Logo
elements
got explicit 22x22 / 18x18 containers so they render at chip scale
without the inline font-size hack.
---
webui/static/downloads.js | 7 ++++-
webui/static/search.js | 24 +++++++++++++--
webui/static/shared-helpers.js | 53 +++++++++++++++++++++++++++-------
webui/static/style.css | 33 ++++++++++++++++++++-
4 files changed, 102 insertions(+), 15 deletions(-)
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;