Profiles: richer Service Status modal + surface configured-vs-effective source
Visual rework toward the Manage Workers feel:
- Cards are now circular brand-logo discs on white, with each service's brand
color (Spotify green, Deezer purple, Plex gold, …) driving the logo ring +
active glow/gradient + hover lift. Replaces the flat emoji tiles.
- The left rail is alive: each tab shows its category + the CURRENT active
choice's logo and label (e.g. "Metadata · Deezer"), with the active tab in a
brand-tinted gradient + accent bar — mirroring the worker rows.
Correctness fix (answers "modal says spotify, settings says spotify (no auth)"):
the modal read the RAW configured source, but the rest of the app shows the
EFFECTIVE one. get_primary_source() silently downgrades a configured 'spotify'
to the default (deezer) when Spotify isn't authenticated — so configured and
effective diverge. The endpoint now returns `effective` alongside `active`, and
the Metadata panel shows a note ("Configured source isn't connected — actually
using Deezer right now") whenever they differ. Settings was never broken; the
modal just wasn't showing the resolved source.
78 tests pass (integrity + endpoints); smoke confirms configured spotify →
effective deezer surfaces, spotify_free stays itself.
This commit is contained in:
parent
22202104ef
commit
cd59d75531
3 changed files with 107 additions and 36 deletions
|
|
@ -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': {
|
||||
|
|
|
|||
|
|
@ -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 => `
|
||||
<button class="ss-tab${t.id === _ssState.tab ? ' active' : ''}" onclick="switchServiceSwitchTab('${t.id}')">
|
||||
<span class="ss-tab-emoji">${t.emoji}</span>
|
||||
<span class="ss-tab-label">${t.name}</span>
|
||||
</button>`).join('');
|
||||
rail.innerHTML = _SS_TABS.map(t => {
|
||||
const cur = _ssRailCurrent(t.id);
|
||||
const media = cur
|
||||
? (cur.logo
|
||||
? `<img class="ss-tab-logo" src="${cur.logo}" onerror="this.outerHTML='<span class=\\'ss-tab-emoji\\'>${cur.emoji}</span>'">`
|
||||
: `<span class="ss-tab-emoji">${cur.emoji}</span>`)
|
||||
: `<span class="ss-tab-emoji">${t.emoji}</span>`;
|
||||
return `
|
||||
<button class="ss-tab${t.id === _ssState.tab ? ' active' : ''}" style="--ss-brand:${cur ? cur.brand : '#7c5cff'}"
|
||||
onclick="switchServiceSwitchTab('${t.id}')">
|
||||
<span class="ss-tab-disc">${media}</span>
|
||||
<span class="ss-tab-text">
|
||||
<span class="ss-tab-cat">${t.name}</span>
|
||||
<span class="ss-tab-cur">${cur ? _ssEsc(cur.label) : '…'}</span>
|
||||
</span>
|
||||
</button>`;
|
||||
}).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
|
||||
? `<img class="ss-card-logo" src="${logo}" alt="" onerror="this.outerHTML='<span class=\\'ss-card-emoji\\'>${emoji || '🎵'}</span>'">`
|
||||
: `<span class="ss-card-emoji">${emoji || '🎵'}</span>`;
|
||||
return `
|
||||
<button class="ss-card${act}${dim}" ${onclick ? `onclick="${onclick}"` : 'disabled'}>
|
||||
${media}
|
||||
<button class="ss-card${act}${dim}" style="--ss-brand:${brand || '#7c5cff'}" ${onclick ? `onclick="${onclick}"` : 'disabled'}>
|
||||
<span class="ss-card-disc">${media}</span>
|
||||
<span class="ss-card-label">${_ssEsc(label)}</span>
|
||||
${badge ? `<span class="ss-card-badge">${_ssEsc(badge)}</span>` : ''}
|
||||
${active ? '<span class="ss-card-check">✓</span>' : ''}
|
||||
|
|
@ -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 = `<div class="ss-section-title">Metadata source</div><div class="ss-grid">${cards}</div>`;
|
||||
// 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)
|
||||
? `<div class="ss-effective-note">Configured source isn't connected — actually using <b>${_ssEsc((_ssMetaInfo(eff).text) || eff)}</b> right now.</div>`
|
||||
: '';
|
||||
panel.innerHTML = `<div class="ss-section-title">Metadata source</div>${note}<div class="ss-grid">${cards}</div>`;
|
||||
} 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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue