Add collapsible accordion UI to Settings Connections tab

Visual overhaul of the API Configuration section: each service frame
is now a collapsible accordion with brand-colored dots, chevron
indicators, and smooth expand/collapse animations. Includes an
Expand All / Collapse All toggle. No functional changes — all element
IDs, save/load logic, and tab switching preserved.
This commit is contained in:
Broque Thomas 2026-04-06 12:39:05 -07:00
parent e73c1e69c2
commit afd5125262
3 changed files with 434 additions and 246 deletions

View file

@ -3952,7 +3952,7 @@
<div class="settings-left-column"> <div class="settings-left-column">
<!-- API Configuration --> <!-- API Configuration -->
<div class="settings-group" data-stg="connections"> <div class="settings-group" data-stg="connections">
<h3>API Configuration</h3> <h3>API Configuration <button class="stg-accordion-toggle" onclick="toggleAllServiceAccordions(this)">Expand All</button></h3>
<!-- Metadata Source Selection (first — tells user what they're configuring for) --> <!-- Metadata Source Selection (first — tells user what they're configuring for) -->
<div class="api-service-frame"> <div class="api-service-frame">
@ -3972,8 +3972,13 @@
</div> </div>
<!-- Spotify Settings --> <!-- Spotify Settings -->
<div class="api-service-frame"> <div class="api-service-frame stg-service">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #1DB954;"></span>
<h4 class="service-title spotify-title">Spotify</h4> <h4 class="service-title spotify-title">Spotify</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group"> <div class="form-group">
<label>Client ID:</label> <label>Client ID:</label>
<input type="text" id="spotify-client-id" placeholder="Spotify Client ID"> <input type="text" id="spotify-client-id" placeholder="Spotify Client ID">
@ -4007,10 +4012,16 @@
Clear Cache &amp; Use Fallback</button> Clear Cache &amp; Use Fallback</button>
</div> </div>
</div> </div>
</div>
<!-- iTunes Settings --> <!-- iTunes Settings -->
<div class="api-service-frame"> <div class="api-service-frame stg-service">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #fc3c44;"></span>
<h4 class="service-title itunes-title">iTunes / Apple Music</h4> <h4 class="service-title itunes-title">iTunes / Apple Music</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group"> <div class="form-group">
<label>Storefront Country:</label> <label>Storefront Country:</label>
<select id="itunes-country"> <select id="itunes-country">
@ -4036,10 +4047,16 @@
<div class="callback-help">Sets the primary Apple Music storefront. Region-specific albums are auto-searched across other storefronts as fallback.</div> <div class="callback-help">Sets the primary Apple Music storefront. Region-specific albums are auto-searched across other storefronts as fallback.</div>
</div> </div>
</div> </div>
</div>
<!-- Deezer OAuth Auth --> <!-- Deezer OAuth Auth -->
<div class="api-service-frame"> <div class="api-service-frame stg-service">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #A238FF;"></span>
<h4 class="service-title deezer-title">Deezer (Favorites & Playlists)</h4> <h4 class="service-title deezer-title">Deezer (Favorites & Playlists)</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group"> <div class="form-group">
<label>App ID:</label> <label>App ID:</label>
<input type="text" id="deezer-app-id" placeholder="Deezer App ID"> <input type="text" id="deezer-app-id" placeholder="Deezer App ID">
@ -4064,10 +4081,16 @@
Authenticate</button> Authenticate</button>
</div> </div>
</div> </div>
</div>
<!-- Discogs Settings --> <!-- Discogs Settings -->
<div class="api-service-frame"> <div class="api-service-frame stg-service">
<h4 class="service-title" style="color: #333;">Discogs</h4> <div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #e0d4b8;"></span>
<h4 class="service-title">Discogs</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group"> <div class="form-group">
<label>Personal Access Token:</label> <label>Personal Access Token:</label>
<input type="password" id="discogs-token" <input type="password" id="discogs-token"
@ -4080,10 +4103,16 @@
<div class="callback-help">Click "Generate new token" — no app registration needed. Provides 60 req/min and cover art in search results.</div> <div class="callback-help">Click "Generate new token" — no app registration needed. Provides 60 req/min and cover art in search results.</div>
</div> </div>
</div> </div>
</div>
<!-- Tidal Playlist/Metadata Auth --> <!-- Tidal Playlist/Metadata Auth -->
<div class="api-service-frame"> <div class="api-service-frame stg-service">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #ff6600;"></span>
<h4 class="service-title tidal-title">Tidal (Playlists & Metadata)</h4> <h4 class="service-title tidal-title">Tidal (Playlists & Metadata)</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group"> <div class="form-group">
<label>Client ID:</label> <label>Client ID:</label>
<input type="text" id="tidal-client-id" placeholder="Tidal Client ID"> <input type="text" id="tidal-client-id" placeholder="Tidal Client ID">
@ -4109,10 +4138,16 @@
Authenticate</button> Authenticate</button>
</div> </div>
</div> </div>
</div>
<!-- Qobuz Metadata/Enrichment Auth --> <!-- Qobuz Metadata/Enrichment Auth -->
<div class="api-service-frame"> <div class="api-service-frame stg-service">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #4285f4;"></span>
<h4 class="service-title qobuz-title">Qobuz (Metadata & Enrichment)</h4> <h4 class="service-title qobuz-title">Qobuz (Metadata & Enrichment)</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div id="qobuz-connection-logged-in" style="display: none; margin-bottom: 8px;"> <div id="qobuz-connection-logged-in" style="display: none; margin-bottom: 8px;">
<span id="qobuz-connection-user-info" class="setting-help-text" style="color: #4caf50;"></span> <span id="qobuz-connection-user-info" class="setting-help-text" style="color: #4caf50;"></span>
<button class="auth-button" onclick="logoutQobuz()" style="margin-left: 8px;"> <button class="auth-button" onclick="logoutQobuz()" style="margin-left: 8px;">
@ -4141,10 +4176,16 @@
Connects Qobuz for metadata enrichment (ISRC, labels, copyright). Also used for downloads if Qobuz is your download source. Connects Qobuz for metadata enrichment (ISRC, labels, copyright). Also used for downloads if Qobuz is your download source.
</div> </div>
</div> </div>
</div>
<!-- Last.fm Settings --> <!-- Last.fm Settings -->
<div class="api-service-frame"> <div class="api-service-frame stg-service">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #d51007;"></span>
<h4 class="service-title lastfm-title">Last.fm</h4> <h4 class="service-title lastfm-title">Last.fm</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group"> <div class="form-group">
<label>API Key:</label> <label>API Key:</label>
<input type="password" id="lastfm-api-key" <input type="password" id="lastfm-api-key"
@ -4172,10 +4213,16 @@
<span class="setting-help-text" id="lastfm-scrobble-status"></span> <span class="setting-help-text" id="lastfm-scrobble-status"></span>
</div> </div>
</div> </div>
</div>
<!-- Genius Settings --> <!-- Genius Settings -->
<div class="api-service-frame"> <div class="api-service-frame stg-service">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #ffff64;"></span>
<h4 class="service-title genius-title">Genius</h4> <h4 class="service-title genius-title">Genius</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group"> <div class="form-group">
<label>Client Access Token:</label> <label>Client Access Token:</label>
<input type="password" id="genius-access-token" <input type="password" id="genius-access-token"
@ -4188,10 +4235,16 @@
<div class="callback-help">Generate a "Client Access Token" — no OAuth flow needed.</div> <div class="callback-help">Generate a "Client Access Token" — no OAuth flow needed.</div>
</div> </div>
</div> </div>
</div>
<!-- AcoustID Settings --> <!-- AcoustID Settings -->
<div class="api-service-frame"> <div class="api-service-frame stg-service">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #ba55d3;"></span>
<h4 class="service-title acoustid-title">AcoustID Verification</h4> <h4 class="service-title acoustid-title">AcoustID Verification</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group" style="margin-bottom: 12px;"> <div class="form-group" style="margin-bottom: 12px;">
<label class="checkbox-label" <label class="checkbox-label"
style="display: flex; align-items: center; gap: 8px; cursor: pointer;"> style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
@ -4219,10 +4272,16 @@
</div> </div>
<button class="test-button" onclick="clearQuarantine()" style="margin-top: 10px; background: rgba(255,82,82,0.15); border-color: rgba(255,82,82,0.3); color: #ff5252;">Clear Quarantine</button> <button class="test-button" onclick="clearQuarantine()" style="margin-top: 10px; background: rgba(255,82,82,0.15); border-color: rgba(255,82,82,0.3); color: #ff5252;">Clear Quarantine</button>
</div> </div>
</div>
<!-- ListenBrainz Settings --> <!-- ListenBrainz Settings -->
<div class="api-service-frame"> <div class="api-service-frame stg-service">
<div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #eb743b;"></span>
<h4 class="service-title listenbrainz-title">ListenBrainz</h4> <h4 class="service-title listenbrainz-title">ListenBrainz</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group"> <div class="form-group">
<label>API Base URL:</label> <label>API Base URL:</label>
<input type="text" id="listenbrainz-base-url" <input type="text" id="listenbrainz-base-url"
@ -4246,10 +4305,16 @@
</label> </label>
</div> </div>
</div> </div>
</div>
<!-- Hydrabase P2P Metadata --> <!-- Hydrabase P2P Metadata -->
<div class="api-service-frame" data-stg="connections"> <div class="api-service-frame stg-service" data-stg="connections">
<h4 class="service-title" style="color: #00b4d8;">Hydrabase</h4> <div class="stg-service-header" onclick="toggleStgService(this)">
<span class="stg-service-dot" style="color: #00b4d8;"></span>
<h4 class="service-title">Hydrabase</h4>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<input type="hidden" id="hydrabase-enabled" value="false"> <input type="hidden" id="hydrabase-enabled" value="false">
<div class="form-group"> <div class="form-group">
<label>WebSocket URL:</label> <label>WebSocket URL:</label>
@ -4273,6 +4338,7 @@
<span id="hydrabase-settings-status" style="font-size: 0.82em; color: rgba(255,255,255,0.4); margin-left: 8px;"></span> <span id="hydrabase-settings-status" style="font-size: 0.82em; color: rgba(255,255,255,0.4); margin-left: 8px;"></span>
</div> </div>
</div> </div>
</div>
<!-- Test Connection Buttons --> <!-- Test Connection Buttons -->
<div class="api-test-buttons"> <div class="api-test-buttons">

View file

@ -5555,6 +5555,12 @@ function toggleStgService(el) {
const service = el.closest('.stg-service'); const service = el.closest('.stg-service');
if (service) service.classList.toggle('expanded'); if (service) service.classList.toggle('expanded');
} }
function toggleAllServiceAccordions(btn) {
const services = document.querySelectorAll('#settings-page .stg-service');
const allExpanded = Array.from(services).every(s => s.classList.contains('expanded'));
services.forEach(s => s.classList.toggle('expanded', !allExpanded));
btn.textContent = allExpanded ? 'Expand All' : 'Collapse All';
}
// ── Hybrid source priority list (drag-and-drop) ── // ── Hybrid source priority list (drag-and-drop) ──
const HYBRID_SOURCES = [ const HYBRID_SOURCES = [

View file

@ -51081,6 +51081,119 @@ tr.tag-diff-same {
transform: translateX(16px); transform: translateX(16px);
} }
/* ── Settings Accordion Services — visual upgrade ── */
#settings-page .api-service-frame.stg-service {
overflow: hidden;
transition: border-color 0.25s, box-shadow 0.25s, transform 0.25s, border-left-color 0.3s;
}
#settings-page .api-service-frame.stg-service:hover {
transform: none;
}
#settings-page .stg-service > .stg-service-header {
display: flex;
align-items: center;
gap: 12px;
padding: 13px 18px;
cursor: pointer;
transition: background 0.2s ease, padding-left 0.25s ease;
user-select: none;
}
#settings-page .stg-service > .stg-service-header:hover {
background: rgba(255, 255, 255, 0.035);
padding-left: 22px;
}
/* Override service-title styles inside accordion header */
#settings-page .stg-service .stg-service-header .service-title {
padding: 0 !important;
margin: 0 !important;
border-bottom: none !important;
font-size: 0.88em !important;
font-weight: 500;
color: rgba(255, 255, 255, 0.7);
letter-spacing: 0.02em;
text-transform: none;
flex: 1;
transition: color 0.2s;
}
#settings-page .stg-service:hover .stg-service-header .service-title,
#settings-page .stg-service.expanded .stg-service-header .service-title {
color: rgba(255, 255, 255, 0.95);
}
/* Brand color dot */
#settings-page .stg-service-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
background: currentColor;
opacity: 0.45;
transition: opacity 0.25s, transform 0.25s, box-shadow 0.25s;
}
#settings-page .stg-service:hover .stg-service-dot {
opacity: 0.75;
}
#settings-page .stg-service.expanded .stg-service-dot {
opacity: 1;
transform: scale(1.25);
box-shadow: 0 0 8px currentColor;
}
/* Chevron */
#settings-page .stg-service > .stg-service-header > .stg-service-chevron {
color: rgba(255, 255, 255, 0.2);
font-size: 1.1em;
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), color 0.25s;
flex-shrink: 0;
margin-left: auto;
line-height: 1;
}
#settings-page .stg-service.expanded > .stg-service-header > .stg-service-chevron {
transform: rotate(90deg);
color: var(--accent-color, #1db954);
}
/* Collapsible body */
#settings-page .stg-service > .stg-service-body {
display: none;
padding: 0 0 6px;
border-top: 1px solid rgba(255, 255, 255, 0.03);
}
#settings-page .stg-service.expanded > .stg-service-body {
display: block;
animation: stgSlideIn 0.25s ease-out;
}
/* Expanded state — accent left border + slightly brighter bg */
#settings-page .api-service-frame.stg-service.expanded {
border-left: 2px solid rgba(var(--accent-rgb, 29, 185, 84), 0.4);
background: rgba(255, 255, 255, 0.025);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.03);
}
/* Expand All / Collapse All toggle button */
#settings-page .stg-accordion-toggle {
background: none;
border: 1px solid rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.35);
font-size: 0.72em;
font-weight: 500;
cursor: pointer;
padding: 4px 10px;
border-radius: 6px;
transition: color 0.2s, background 0.2s, border-color 0.2s;
margin-left: auto;
letter-spacing: 0.03em;
font-family: inherit;
}
#settings-page .stg-accordion-toggle:hover {
color: rgba(255, 255, 255, 0.7);
background: rgba(255, 255, 255, 0.04);
border-color: rgba(255, 255, 255, 0.1);
}
/* ── Accent color picker ── */ /* ── Accent color picker ── */
#settings-page .accent-color-selector { #settings-page .accent-color-selector {
display: flex; display: flex;
@ -51121,6 +51234,9 @@ tr.tag-diff-same {
#settings-page .api-service-frame .form-group { padding: 8px 12px !important; } #settings-page .api-service-frame .form-group { padding: 8px 12px !important; }
#settings-page .api-service-frame .callback-info { padding: 2px 12px 10px; } #settings-page .api-service-frame .callback-info { padding: 2px 12px 10px; }
#settings-page .api-service-frame .form-actions { padding: 4px 12px 12px; } #settings-page .api-service-frame .form-actions { padding: 4px 12px 12px; }
/* Accordion headers */
#settings-page .stg-service > .stg-service-header { padding: 12px 14px; }
#settings-page .stg-service > .stg-service-body { padding: 0 0 8px; }
/* Save button */ /* Save button */
#settings-page .settings-actions { padding: 16px 8px; } #settings-page .settings-actions { padding: 16px 8px; }
#settings-page .settings-actions .save-button { width: 100%; } #settings-page .settings-actions .save-button { width: 100%; }