Fix source-picker review items: real logos, cached click close, Soulseek clip
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 <img> 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 <img> elements got explicit 22x22 / 18x18 containers so they render at chip scale without the inline font-size hack.
This commit is contained in:
parent
553ede8de9
commit
86e6d8df49
4 changed files with 102 additions and 15 deletions
|
|
@ -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
|
||||
? `<img src="${_escAttr(info.logo)}" alt="" loading="lazy">`
|
||||
: info.icon);
|
||||
return `<button class="${classes}" data-source="${src}" onclick="_gsSetActiveSource('${src}')" title="${_escAttr(title)}">
|
||||
<span class="gsearch-source-icon-glyph">${loading ? '⏳' : info.icon}</span>
|
||||
<span class="gsearch-source-icon-glyph">${glyph}</span>
|
||||
<span class="gsearch-source-icon-label">${_escToast(info.text)}</span>
|
||||
</button>`;
|
||||
}).join('') + '</div>';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
? `<img src="${escapeHtml(info.logo)}" alt="" loading="lazy">`
|
||||
: info.icon);
|
||||
return `
|
||||
<button class="${classes}" data-source="${src}" role="tab"
|
||||
aria-selected="${active}" title="${escapeHtml(title)}">
|
||||
<span class="enh-source-icon-glyph">${loading ? '⏳' : info.icon}</span>
|
||||
<span class="enh-source-icon-glyph">${glyph}</span>
|
||||
<span class="enh-source-icon-label">${escapeHtml(info.text)}</span>
|
||||
</button>`;
|
||||
}).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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <img> 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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue