diff --git a/web_server.py b/web_server.py index 6fc0780e..8e4d3eea 100644 --- a/web_server.py +++ b/web_server.py @@ -25645,7 +25645,13 @@ def get_active_sources(): 'success': True, 'editable': get_current_profile_id() == 1, # admin writes the global default 'metadata': { + # `active` = the configured choice (what the user picked / edits). + # `effective` = what's actually used after auth/availability + # fallback (e.g. configured 'spotify' but not authenticated → + # the app falls back). Surfacing both stops the modal disagreeing + # with the sidebar/Settings status. 'active': config_manager.get('metadata.fallback_source', 'deezer') or 'deezer', + 'effective': _get_metadata_fallback_source(), 'options': [{'id': s, 'available': _qs_metadata_available(s)} for s in _QS_METADATA_SOURCES], }, 'server': { diff --git a/webui/static/service-switch.js b/webui/static/service-switch.js index 9a33bfb0..f046f4a2 100644 --- a/webui/static/service-switch.js +++ b/webui/static/service-switch.js @@ -27,6 +27,15 @@ const _SS_SERVER_INFO = { const _SS_META_FALLBACK = { spotify_free: { text: 'Spotify (no auth)', icon: '🆓', logo: 'https://storage.googleapis.com/pr-newsroom-wp/1/2023/05/Spotify_Primary_Logo_RGB_Green.png' }, }; +// Brand colors drive each card's logo ring + active glow (the Manage-Workers feel). +const _SS_BRAND = { + spotify: '#1db954', spotify_free: '#1db954', itunes: '#fc5c7d', deezer: '#a238ff', + discogs: '#ff5500', musicbrainz: '#ba478f', amazon: '#ff9900', + plex: '#e5a00d', jellyfin: '#aa5cc3', navidrome: '#3b6cf6', soulsync: '#7c5cff', + soulseek: '#22a7f0', youtube: '#ff0000', tidal: '#00cfe8', qobuz: '#0a6e9e', + hifi: '#16c79a', torrent: '#8a2be2', usenet: '#e67e22', +}; +function _ssBrand(id) { return _SS_BRAND[id] || 'var(--accent-light-rgb-hex, #7c5cff)'; } let _ssState = { tab: 'metadata', data: null }; @@ -99,17 +108,48 @@ async function _ssLoad() { } catch (e) { _ssState.data = null; } + _ssRenderRail(); // re-render now that we know each tab's active choice _ssRenderPanel(); } +function _ssRailCurrent(tabId) { + // The active choice for a tab → {logo/emoji, label, brand} for the rail chip. + const d = _ssState.data; + if (!d || !d.success) return null; + if (tabId === 'metadata') { + const id = d.metadata.active; const info = _ssMetaInfo(id); + return { logo: info.logo, emoji: info.icon, label: info.text || id, brand: _ssBrand(id) }; + } + if (tabId === 'server') { + const id = d.server.active; const info = _SS_SERVER_INFO[id] || { name: id }; + return { logo: info.logo, emoji: '🖥️', label: info.name, brand: _ssBrand(id) }; + } + const id = d.download.mode; + if (id === 'hybrid') return { emoji: '🔀', label: 'Hybrid', brand: 'var(--accent-light-rgb-hex,#7c5cff)' }; + const info = _ssDownloadInfo(id); + return { logo: info.logo, emoji: info.emoji, label: info.name, brand: _ssBrand(id) }; +} + function _ssRenderRail() { const rail = document.getElementById('ss-rail'); if (!rail) return; - rail.innerHTML = _SS_TABS.map(t => ` - - ${t.emoji} - ${t.name} - `).join(''); + rail.innerHTML = _SS_TABS.map(t => { + const cur = _ssRailCurrent(t.id); + const media = cur + ? (cur.logo + ? `` + : `${cur.emoji}`) + : `${t.emoji}`; + return ` + + ${media} + + ${t.name} + ${cur ? _ssEsc(cur.label) : '…'} + + `; + }).join(''); } function switchServiceSwitchTab(tab) { @@ -118,15 +158,15 @@ function switchServiceSwitchTab(tab) { _ssRenderPanel(); } -function _ssCard({ logo, emoji, label, active, available, onclick, badge }) { +function _ssCard({ logo, emoji, label, active, available, onclick, badge, brand }) { const dim = available === false ? ' ss-card--locked' : ''; const act = active ? ' active' : ''; const media = logo ? `` : `${emoji || '🎵'}`; return ` - - ${media} + + ${media} ${_ssEsc(label)} ${badge ? `${_ssEsc(badge)}` : ''} ${active ? '✓' : ''} @@ -148,17 +188,23 @@ function _ssRenderPanel() { const cards = d.metadata.options.map(o => { const info = _ssMetaInfo(o.id); return _ssCard({ - logo: info.logo, emoji: info.icon, label: info.text || o.id, + logo: info.logo, emoji: info.icon, label: info.text || o.id, brand: _ssBrand(o.id), active: d.metadata.active === o.id, available: o.available, onclick: (editable && o.available) ? `setActiveSource('metadata','${o.id}')` : null, }); }).join(''); - panel.innerHTML = `Metadata source${cards}`; + // Surface the EFFECTIVE source when it differs from the configured one + // (e.g. configured Spotify but not authenticated → running on a fallback). + const eff = d.metadata.effective; + const note = (eff && eff !== d.metadata.active) + ? `Configured source isn't connected — actually using ${_ssEsc((_ssMetaInfo(eff).text) || eff)} right now.` + : ''; + panel.innerHTML = `Metadata source${note}${cards}`; } else if (_ssState.tab === 'server') { const cards = d.server.options.map(o => { const info = _SS_SERVER_INFO[o.id] || { name: o.id }; return _ssCard({ - logo: info.logo, emoji: '🖥️', label: info.name, + logo: info.logo, emoji: '🖥️', label: info.name, brand: _ssBrand(o.id), active: d.server.active === o.id, available: o.available, onclick: (editable && o.available) ? `setActiveSource('server','${o.id}')` : null, }); @@ -196,7 +242,7 @@ function _ssRenderDownloadPanel(panel, d, editable) { const cards = d.download.options.map(o => { const info = _ssDownloadInfo(o.id); return _ssCard({ - logo: info.logo, emoji: info.emoji, label: info.name, + logo: info.logo, emoji: info.emoji, label: info.name, brand: _ssBrand(o.id), active: d.download.mode === o.id, available: true, onclick: editable ? `setActiveSource('download','${o.id}')` : null, }); diff --git a/webui/static/style.css b/webui/static/style.css index e205e946..af9e44ff 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -67643,45 +67643,64 @@ body.em-scroll-lock { overflow: hidden; } .ss-body { display: flex; min-height: 320px; max-height: calc(86vh - 78px); } .ss-rail { - width: 168px; flex-shrink: 0; padding: 14px 10px; display: flex; flex-direction: column; gap: 6px; - border-right: 1px solid rgba(255, 255, 255, 0.06); background: rgba(0, 0, 0, 0.18); + width: 196px; flex-shrink: 0; padding: 14px 10px; display: flex; flex-direction: column; gap: 8px; + border-right: 1px solid rgba(255, 255, 255, 0.06); background: rgba(0, 0, 0, 0.22); } .ss-tab { - display: flex; align-items: center; gap: 10px; padding: 11px 14px; - background: transparent; border: none; border-radius: 10px; cursor: pointer; - color: rgba(255, 255, 255, 0.65); font-size: 0.92rem; text-align: left; transition: all 0.15s; + position: relative; display: flex; align-items: center; gap: 12px; padding: 11px 12px; + background: rgba(255, 255, 255, 0.02); border: 1px solid transparent; border-radius: 12px; cursor: pointer; + color: rgba(255, 255, 255, 0.7); text-align: left; transition: all 0.18s; overflow: hidden; } .ss-tab:hover { background: rgba(255, 255, 255, 0.05); color: #fff; } .ss-tab.active { - background: rgba(var(--accent-light-rgb), 0.16); - color: rgb(var(--accent-light-rgb)); font-weight: 600; - box-shadow: inset 2px 0 0 rgb(var(--accent-light-rgb)); + background: linear-gradient(100deg, color-mix(in srgb, var(--ss-brand) 22%, transparent), rgba(255,255,255,0.02)); + border-color: color-mix(in srgb, var(--ss-brand) 55%, transparent); color: #fff; } -.ss-tab-emoji { font-size: 1.1rem; } +.ss-tab.active::before { content: ''; position: absolute; left: 0; top: 8px; bottom: 8px; width: 3px; border-radius: 3px; background: var(--ss-brand); } +.ss-tab-disc { + width: 38px; height: 38px; flex-shrink: 0; border-radius: 50%; display: flex; align-items: center; justify-content: center; + background: #fff; box-shadow: 0 0 0 2px color-mix(in srgb, var(--ss-brand) 60%, transparent); +} +.ss-tab-logo { width: 24px; height: 24px; object-fit: contain; } +.ss-tab-disc .ss-tab-emoji { font-size: 1.2rem; } +.ss-tab-text { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; } +.ss-tab-cat { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; color: rgba(255,255,255,0.45); } +.ss-tab.active .ss-tab-cat { color: color-mix(in srgb, var(--ss-brand) 75%, white); } +.ss-tab-cur { font-size: 0.92rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.ss-panel { flex: 1; padding: 20px 22px; overflow-y: auto; } -.ss-section-title { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.08em; color: rgba(255, 255, 255, 0.45); margin-bottom: 14px; } +.ss-panel { flex: 1; padding: 22px 24px; overflow-y: auto; } +.ss-section-title { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.08em; color: rgba(255, 255, 255, 0.45); margin-bottom: 16px; } .ss-hint { color: rgba(255, 255, 255, 0.5); font-size: 0.82rem; margin-bottom: 12px; } .ss-empty { color: rgba(255, 255, 255, 0.4); font-style: italic; padding: 30px 0; text-align: center; } +.ss-effective-note { + background: rgba(255, 180, 60, 0.1); border: 1px solid rgba(255, 180, 60, 0.3); + color: #ffcf8a; border-radius: 10px; padding: 9px 13px; font-size: 0.82rem; margin-bottom: 16px; +} -.ss-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px; } +.ss-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); gap: 14px; } .ss-card { - position: relative; display: flex; flex-direction: column; align-items: center; gap: 10px; - padding: 18px 12px; background: rgba(255, 255, 255, 0.035); - border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 14px; - cursor: pointer; transition: all 0.16s; color: #fff; + position: relative; display: flex; flex-direction: column; align-items: center; gap: 12px; + padding: 20px 12px 16px; background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 16px; + cursor: pointer; transition: transform 0.16s, box-shadow 0.16s, border-color 0.16s, background 0.16s; color: #fff; } -.ss-card:hover:not([disabled]) { background: rgba(255, 255, 255, 0.07); transform: translateY(-2px); border-color: rgba(var(--accent-light-rgb), 0.4); } +.ss-card:hover:not([disabled]) { transform: translateY(-3px); border-color: color-mix(in srgb, var(--ss-brand) 50%, transparent); } .ss-card.active { - border-color: rgb(var(--accent-light-rgb)); - background: rgba(var(--accent-light-rgb), 0.12); - box-shadow: 0 0 0 1px rgb(var(--accent-light-rgb)), 0 8px 24px rgba(var(--accent-light-rgb), 0.18); + border-color: var(--ss-brand); + background: linear-gradient(160deg, color-mix(in srgb, var(--ss-brand) 16%, transparent), rgba(255,255,255,0.02)); + box-shadow: 0 0 0 1px var(--ss-brand), 0 10px 30px color-mix(in srgb, var(--ss-brand) 30%, transparent); } -.ss-card--locked { opacity: 0.4; cursor: not-allowed; } +.ss-card-disc { + width: 58px; height: 58px; border-radius: 50%; background: #fff; + display: flex; align-items: center; justify-content: center; + box-shadow: 0 0 0 2px color-mix(in srgb, var(--ss-brand) 45%, transparent); transition: box-shadow 0.16s; +} +.ss-card.active .ss-card-disc { box-shadow: 0 0 0 3px var(--ss-brand), 0 0 22px color-mix(in srgb, var(--ss-brand) 55%, transparent); } +.ss-card--locked { opacity: 0.38; cursor: not-allowed; } .ss-card[disabled] { cursor: default; } -.ss-card-logo { width: 42px; height: 42px; object-fit: contain; } -.ss-card-emoji { font-size: 2rem; line-height: 1; } -.ss-card-label { font-size: 0.85rem; text-align: center; } +.ss-card-logo { width: 34px; height: 34px; object-fit: contain; } +.ss-card-emoji { font-size: 1.7rem; line-height: 1; } +.ss-card-label { font-size: 0.85rem; text-align: center; font-weight: 500; } .ss-card-badge { position: absolute; top: 6px; left: 6px; font-size: 0.62rem; padding: 2px 6px; background: rgba(0, 0, 0, 0.5); border-radius: 6px; color: rgba(255, 255, 255, 0.7);