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">
<!-- API Configuration -->
<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) -->
<div class="api-service-frame">
@ -3972,305 +3972,371 @@
</div>
<!-- Spotify Settings -->
<div class="api-service-frame">
<h4 class="service-title spotify-title">Spotify</h4>
<div class="form-group">
<label>Client ID:</label>
<input type="text" id="spotify-client-id" placeholder="Spotify Client ID">
<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>
<span class="stg-service-chevron"></span>
</div>
<div class="form-group">
<label>Client Secret:</label>
<input type="password" id="spotify-client-secret"
placeholder="Spotify Client Secret">
</div>
<div class="form-group">
<label>Redirect URI:</label>
<input type="text" id="spotify-redirect-uri"
placeholder="http://127.0.0.1:8888/callback">
</div>
<div class="callback-info">
<div class="callback-label">Current Redirect URI:</div>
<div class="callback-url" id="spotify-callback-display">
http://127.0.0.1:8888/callback</div>
<div class="callback-help">Add this URL to your Spotify app's 'Redirect URIs' in
the Spotify Developer Dashboard</div>
</div>
<div class="form-actions">
<button class="auth-button" onclick="authenticateSpotify()">🔐
Authenticate</button>
<button class="auth-button disconnect-button" id="spotify-disconnect-btn"
onclick="disconnectSpotify()" style="display: none;">🔌
Disconnect</button>
<button class="auth-button disconnect-button"
onclick="clearSpotifyCacheAndFallback()"
title="Clear Spotify token cache and switch to your configured fallback metadata source">🗑️
Clear Cache &amp; Use Fallback</button>
<div class="stg-service-body">
<div class="form-group">
<label>Client ID:</label>
<input type="text" id="spotify-client-id" placeholder="Spotify Client ID">
</div>
<div class="form-group">
<label>Client Secret:</label>
<input type="password" id="spotify-client-secret"
placeholder="Spotify Client Secret">
</div>
<div class="form-group">
<label>Redirect URI:</label>
<input type="text" id="spotify-redirect-uri"
placeholder="http://127.0.0.1:8888/callback">
</div>
<div class="callback-info">
<div class="callback-label">Current Redirect URI:</div>
<div class="callback-url" id="spotify-callback-display">
http://127.0.0.1:8888/callback</div>
<div class="callback-help">Add this URL to your Spotify app's 'Redirect URIs' in
the Spotify Developer Dashboard</div>
</div>
<div class="form-actions">
<button class="auth-button" onclick="authenticateSpotify()">🔐
Authenticate</button>
<button class="auth-button disconnect-button" id="spotify-disconnect-btn"
onclick="disconnectSpotify()" style="display: none;">🔌
Disconnect</button>
<button class="auth-button disconnect-button"
onclick="clearSpotifyCacheAndFallback()"
title="Clear Spotify token cache and switch to your configured fallback metadata source">🗑️
Clear Cache &amp; Use Fallback</button>
</div>
</div>
</div>
<!-- iTunes Settings -->
<div class="api-service-frame">
<h4 class="service-title itunes-title">iTunes / Apple Music</h4>
<div class="form-group">
<label>Storefront Country:</label>
<select id="itunes-country">
<option value="US">United States (US)</option>
<option value="GB">United Kingdom (GB)</option>
<option value="CA">Canada (CA)</option>
<option value="AU">Australia (AU)</option>
<option value="DE">Germany (DE)</option>
<option value="FR">France (FR)</option>
<option value="JP">Japan (JP)</option>
<option value="KR">South Korea (KR)</option>
<option value="BR">Brazil (BR)</option>
<option value="SE">Sweden (SE)</option>
<option value="NL">Netherlands (NL)</option>
<option value="IT">Italy (IT)</option>
<option value="ES">Spain (ES)</option>
<option value="MX">Mexico (MX)</option>
<option value="IN">India (IN)</option>
<option value="RU">Russia (RU)</option>
</select>
<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>
<span class="stg-service-chevron"></span>
</div>
<div class="callback-info">
<div class="callback-help">Sets the primary Apple Music storefront. Region-specific albums are auto-searched across other storefronts as fallback.</div>
<div class="stg-service-body">
<div class="form-group">
<label>Storefront Country:</label>
<select id="itunes-country">
<option value="US">United States (US)</option>
<option value="GB">United Kingdom (GB)</option>
<option value="CA">Canada (CA)</option>
<option value="AU">Australia (AU)</option>
<option value="DE">Germany (DE)</option>
<option value="FR">France (FR)</option>
<option value="JP">Japan (JP)</option>
<option value="KR">South Korea (KR)</option>
<option value="BR">Brazil (BR)</option>
<option value="SE">Sweden (SE)</option>
<option value="NL">Netherlands (NL)</option>
<option value="IT">Italy (IT)</option>
<option value="ES">Spain (ES)</option>
<option value="MX">Mexico (MX)</option>
<option value="IN">India (IN)</option>
<option value="RU">Russia (RU)</option>
</select>
</div>
<div class="callback-info">
<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>
<!-- Deezer OAuth Auth -->
<div class="api-service-frame">
<h4 class="service-title deezer-title">Deezer (Favorites & Playlists)</h4>
<div class="form-group">
<label>App ID:</label>
<input type="text" id="deezer-app-id" placeholder="Deezer App ID">
<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>
<span class="stg-service-chevron"></span>
</div>
<div class="form-group">
<label>App Secret:</label>
<input type="password" id="deezer-app-secret" placeholder="Deezer App Secret">
</div>
<div class="form-group">
<label>Redirect URI:</label>
<input type="text" id="deezer-redirect-uri"
placeholder="http://127.0.0.1:8008/deezer/callback">
</div>
<div class="callback-info">
<div class="callback-label">Current Redirect URI:</div>
<div class="callback-url" id="deezer-callback-display">
http://127.0.0.1:8008/deezer/callback</div>
<div class="callback-help">Add this URL to your Deezer app at developers.deezer.com</div>
</div>
<div class="form-actions">
<button class="auth-button" onclick="authenticateDeezer()">🔐
Authenticate</button>
<div class="stg-service-body">
<div class="form-group">
<label>App ID:</label>
<input type="text" id="deezer-app-id" placeholder="Deezer App ID">
</div>
<div class="form-group">
<label>App Secret:</label>
<input type="password" id="deezer-app-secret" placeholder="Deezer App Secret">
</div>
<div class="form-group">
<label>Redirect URI:</label>
<input type="text" id="deezer-redirect-uri"
placeholder="http://127.0.0.1:8008/deezer/callback">
</div>
<div class="callback-info">
<div class="callback-label">Current Redirect URI:</div>
<div class="callback-url" id="deezer-callback-display">
http://127.0.0.1:8008/deezer/callback</div>
<div class="callback-help">Add this URL to your Deezer app at developers.deezer.com</div>
</div>
<div class="form-actions">
<button class="auth-button" onclick="authenticateDeezer()">🔐
Authenticate</button>
</div>
</div>
</div>
<!-- Discogs Settings -->
<div class="api-service-frame">
<h4 class="service-title" style="color: #333;">Discogs</h4>
<div class="form-group">
<label>Personal Access Token:</label>
<input type="password" id="discogs-token"
placeholder="Discogs Personal Access Token">
<div class="api-service-frame stg-service">
<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="callback-info">
<div class="callback-help">Get your free token from <a
href="https://www.discogs.com/settings/developers" target="_blank"
style="color: #ffff64;">Discogs Developer Settings</a></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 class="stg-service-body">
<div class="form-group">
<label>Personal Access Token:</label>
<input type="password" id="discogs-token"
placeholder="Discogs Personal Access Token">
</div>
<div class="callback-info">
<div class="callback-help">Get your free token from <a
href="https://www.discogs.com/settings/developers" target="_blank"
style="color: #ffff64;">Discogs Developer Settings</a></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>
<!-- Tidal Playlist/Metadata Auth -->
<div class="api-service-frame">
<h4 class="service-title tidal-title">Tidal (Playlists & Metadata)</h4>
<div class="form-group">
<label>Client ID:</label>
<input type="text" id="tidal-client-id" placeholder="Tidal Client ID">
<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>
<span class="stg-service-chevron"></span>
</div>
<div class="form-group">
<label>Client Secret:</label>
<input type="password" id="tidal-client-secret"
placeholder="Tidal Client Secret">
</div>
<div class="form-group">
<label>Redirect URI:</label>
<input type="text" id="tidal-redirect-uri"
placeholder="http://127.0.0.1:8889/tidal/callback">
</div>
<div class="callback-info">
<div class="callback-label">Current Redirect URI:</div>
<div class="callback-url" id="tidal-callback-display">
http://127.0.0.1:8889/tidal/callback</div>
<div class="callback-help">Add this URL to your Tidal app configuration</div>
</div>
<div class="form-actions">
<button class="auth-button" onclick="authenticateTidal()">🔐
Authenticate</button>
<div class="stg-service-body">
<div class="form-group">
<label>Client ID:</label>
<input type="text" id="tidal-client-id" placeholder="Tidal Client ID">
</div>
<div class="form-group">
<label>Client Secret:</label>
<input type="password" id="tidal-client-secret"
placeholder="Tidal Client Secret">
</div>
<div class="form-group">
<label>Redirect URI:</label>
<input type="text" id="tidal-redirect-uri"
placeholder="http://127.0.0.1:8889/tidal/callback">
</div>
<div class="callback-info">
<div class="callback-label">Current Redirect URI:</div>
<div class="callback-url" id="tidal-callback-display">
http://127.0.0.1:8889/tidal/callback</div>
<div class="callback-help">Add this URL to your Tidal app configuration</div>
</div>
<div class="form-actions">
<button class="auth-button" onclick="authenticateTidal()">🔐
Authenticate</button>
</div>
</div>
</div>
<!-- Qobuz Metadata/Enrichment Auth -->
<div class="api-service-frame">
<h4 class="service-title qobuz-title">Qobuz (Metadata & Enrichment)</h4>
<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>
<button class="auth-button" onclick="logoutQobuz()" style="margin-left: 8px;">
Disconnect
</button>
<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>
<span class="stg-service-chevron"></span>
</div>
<div id="qobuz-connection-form">
<div class="form-group">
<label>Email:</label>
<input type="email" id="qobuz-connection-email" class="form-input"
placeholder="Qobuz email" autocomplete="email">
</div>
<div class="form-group">
<label>Password:</label>
<input type="password" id="qobuz-connection-password" class="form-input"
placeholder="Qobuz password" autocomplete="current-password">
</div>
<div class="form-actions">
<button class="auth-button" id="qobuz-connection-login-btn" onclick="loginQobuzFromConnections()">
Connect Qobuz
<div class="stg-service-body">
<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>
<button class="auth-button" onclick="logoutQobuz()" style="margin-left: 8px;">
Disconnect
</button>
<span id="qobuz-connection-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
</div>
<div class="setting-help-text" style="margin-top: 6px;">
Connects Qobuz for metadata enrichment (ISRC, labels, copyright). Also used for downloads if Qobuz is your download source.
<div id="qobuz-connection-form">
<div class="form-group">
<label>Email:</label>
<input type="email" id="qobuz-connection-email" class="form-input"
placeholder="Qobuz email" autocomplete="email">
</div>
<div class="form-group">
<label>Password:</label>
<input type="password" id="qobuz-connection-password" class="form-input"
placeholder="Qobuz password" autocomplete="current-password">
</div>
<div class="form-actions">
<button class="auth-button" id="qobuz-connection-login-btn" onclick="loginQobuzFromConnections()">
Connect Qobuz
</button>
<span id="qobuz-connection-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
</div>
<div class="setting-help-text" style="margin-top: 6px;">
Connects Qobuz for metadata enrichment (ISRC, labels, copyright). Also used for downloads if Qobuz is your download source.
</div>
</div>
</div>
<!-- Last.fm Settings -->
<div class="api-service-frame">
<h4 class="service-title lastfm-title">Last.fm</h4>
<div class="form-group">
<label>API Key:</label>
<input type="password" id="lastfm-api-key"
placeholder="Last.fm API Key">
<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>
<span class="stg-service-chevron"></span>
</div>
<div class="form-group">
<label>API Secret:</label>
<input type="password" id="lastfm-api-secret"
placeholder="Last.fm API Secret (required for scrobbling)">
</div>
<div class="callback-info">
<div class="callback-help">Get your API key and secret from <a
href="https://www.last.fm/api/account/create" target="_blank"
style="color: #d51007;">Last.fm API Account</a></div>
<div class="callback-help">API key: used for metadata enrichment. API secret: required for scrobbling.</div>
</div>
<div class="form-group" style="margin-top: 12px;">
<label class="checkbox-label">
<input type="checkbox" id="lastfm-scrobble-enabled">
Scrobble plays to Last.fm
</label>
</div>
<div class="form-actions" id="lastfm-scrobble-actions">
<button class="test-button" onclick="authorizeLastfmScrobbling()">Authorize Scrobbling</button>
<span class="setting-help-text" id="lastfm-scrobble-status"></span>
<div class="stg-service-body">
<div class="form-group">
<label>API Key:</label>
<input type="password" id="lastfm-api-key"
placeholder="Last.fm API Key">
</div>
<div class="form-group">
<label>API Secret:</label>
<input type="password" id="lastfm-api-secret"
placeholder="Last.fm API Secret (required for scrobbling)">
</div>
<div class="callback-info">
<div class="callback-help">Get your API key and secret from <a
href="https://www.last.fm/api/account/create" target="_blank"
style="color: #d51007;">Last.fm API Account</a></div>
<div class="callback-help">API key: used for metadata enrichment. API secret: required for scrobbling.</div>
</div>
<div class="form-group" style="margin-top: 12px;">
<label class="checkbox-label">
<input type="checkbox" id="lastfm-scrobble-enabled">
Scrobble plays to Last.fm
</label>
</div>
<div class="form-actions" id="lastfm-scrobble-actions">
<button class="test-button" onclick="authorizeLastfmScrobbling()">Authorize Scrobbling</button>
<span class="setting-help-text" id="lastfm-scrobble-status"></span>
</div>
</div>
</div>
<!-- Genius Settings -->
<div class="api-service-frame">
<h4 class="service-title genius-title">Genius</h4>
<div class="form-group">
<label>Client Access Token:</label>
<input type="password" id="genius-access-token"
placeholder="Genius Client Access Token">
<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>
<span class="stg-service-chevron"></span>
</div>
<div class="callback-info">
<div class="callback-help">Get your token from <a
href="https://genius.com/api-clients" target="_blank"
style="color: #ffff64;">Genius API Clients</a></div>
<div class="callback-help">Generate a "Client Access Token" — no OAuth flow needed.</div>
<div class="stg-service-body">
<div class="form-group">
<label>Client Access Token:</label>
<input type="password" id="genius-access-token"
placeholder="Genius Client Access Token">
</div>
<div class="callback-info">
<div class="callback-help">Get your token from <a
href="https://genius.com/api-clients" target="_blank"
style="color: #ffff64;">Genius API Clients</a></div>
<div class="callback-help">Generate a "Client Access Token" — no OAuth flow needed.</div>
</div>
</div>
</div>
<!-- AcoustID Settings -->
<div class="api-service-frame">
<h4 class="service-title acoustid-title">AcoustID Verification</h4>
<div class="form-group" style="margin-bottom: 12px;">
<label class="checkbox-label"
style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="acoustid-enabled"
style="width: 16px; height: 16px;">
<span>Enable Download Verification</span>
</label>
<div style="color: #888; font-size: 0.8em; margin-top: 4px; margin-left: 24px;">
Verifies downloaded audio matches expected track using fingerprints
<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>
<span class="stg-service-chevron"></span>
</div>
<div class="stg-service-body">
<div class="form-group" style="margin-bottom: 12px;">
<label class="checkbox-label"
style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="acoustid-enabled"
style="width: 16px; height: 16px;">
<span>Enable Download Verification</span>
</label>
<div style="color: #888; font-size: 0.8em; margin-top: 4px; margin-left: 24px;">
Verifies downloaded audio matches expected track using fingerprints
</div>
</div>
</div>
<div class="form-group">
<label>API Key:</label>
<input type="password" id="acoustid-api-key" placeholder="AcoustID API Key">
</div>
<div class="callback-info">
<div class="callback-help">Get your free API key from <a
href="https://acoustid.org/new-application" target="_blank"
style="color: #ba55d3;">AcoustID Applications</a></div>
<div class="callback-help"
style="opacity: 0.7; font-size: 0.85em; margin-top: 4px;">
The fpcalc fingerprint tool is automatically downloaded if needed.
Failed verifications move files to Quarantine folder.
<div class="form-group">
<label>API Key:</label>
<input type="password" id="acoustid-api-key" placeholder="AcoustID API Key">
</div>
<div class="callback-info">
<div class="callback-help">Get your free API key from <a
href="https://acoustid.org/new-application" target="_blank"
style="color: #ba55d3;">AcoustID Applications</a></div>
<div class="callback-help"
style="opacity: 0.7; font-size: 0.85em; margin-top: 4px;">
The fpcalc fingerprint tool is automatically downloaded if needed.
Failed verifications move files to Quarantine folder.
</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>
</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>
</div>
<!-- ListenBrainz Settings -->
<div class="api-service-frame">
<h4 class="service-title listenbrainz-title">ListenBrainz</h4>
<div class="form-group">
<label>API Base URL:</label>
<input type="text" id="listenbrainz-base-url"
placeholder="Leave empty for official (api.listenbrainz.org)">
<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>
<span class="stg-service-chevron"></span>
</div>
<div class="form-group">
<label>User Token:</label>
<input type="password" id="listenbrainz-token"
placeholder="ListenBrainz User Token">
</div>
<div class="callback-info">
<div class="callback-help">Get your token from <a
href="https://listenbrainz.org/profile/" target="_blank"
style="color: #eb743b;">ListenBrainz Settings</a></div>
<div class="callback-help">Self-hosted? Enter your server URL (e.g. http://localhost:8093)</div>
</div>
<div class="form-group" style="margin-top: 12px;">
<label class="checkbox-label">
<input type="checkbox" id="listenbrainz-scrobble-enabled">
Scrobble plays to ListenBrainz
</label>
<div class="stg-service-body">
<div class="form-group">
<label>API Base URL:</label>
<input type="text" id="listenbrainz-base-url"
placeholder="Leave empty for official (api.listenbrainz.org)">
</div>
<div class="form-group">
<label>User Token:</label>
<input type="password" id="listenbrainz-token"
placeholder="ListenBrainz User Token">
</div>
<div class="callback-info">
<div class="callback-help">Get your token from <a
href="https://listenbrainz.org/profile/" target="_blank"
style="color: #eb743b;">ListenBrainz Settings</a></div>
<div class="callback-help">Self-hosted? Enter your server URL (e.g. http://localhost:8093)</div>
</div>
<div class="form-group" style="margin-top: 12px;">
<label class="checkbox-label">
<input type="checkbox" id="listenbrainz-scrobble-enabled">
Scrobble plays to ListenBrainz
</label>
</div>
</div>
</div>
<!-- Hydrabase P2P Metadata -->
<div class="api-service-frame" data-stg="connections">
<h4 class="service-title" style="color: #00b4d8;">Hydrabase</h4>
<input type="hidden" id="hydrabase-enabled" value="false">
<div class="form-group">
<label>WebSocket URL:</label>
<input type="text" id="hydrabase-url" placeholder="ws://localhost:4545">
<div class="api-service-frame stg-service" data-stg="connections">
<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="form-group">
<label>API Key:</label>
<input type="password" id="hydrabase-api-key" placeholder="Hydrabase API Key">
</div>
<div class="form-group" style="margin-bottom: 8px;">
<label class="checkbox-label" style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 0;">
<input type="checkbox" id="hydrabase-auto-connect" style="width: 16px; height: 16px;">
<span>Auto-connect on startup</span>
</label>
</div>
<div class="callback-info">
<div class="callback-help">P2P metadata network. When enabled and connected, replaces Spotify/iTunes as the primary metadata source for searches.</div>
</div>
<div class="form-actions">
<button class="test-button" id="hydrabase-connect-btn" onclick="toggleHydrabaseFromSettings()">Connect</button>
<span id="hydrabase-settings-status" style="font-size: 0.82em; color: rgba(255,255,255,0.4); margin-left: 8px;"></span>
<div class="stg-service-body">
<input type="hidden" id="hydrabase-enabled" value="false">
<div class="form-group">
<label>WebSocket URL:</label>
<input type="text" id="hydrabase-url" placeholder="ws://localhost:4545">
</div>
<div class="form-group">
<label>API Key:</label>
<input type="password" id="hydrabase-api-key" placeholder="Hydrabase API Key">
</div>
<div class="form-group" style="margin-bottom: 8px;">
<label class="checkbox-label" style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 0;">
<input type="checkbox" id="hydrabase-auto-connect" style="width: 16px; height: 16px;">
<span>Auto-connect on startup</span>
</label>
</div>
<div class="callback-info">
<div class="callback-help">P2P metadata network. When enabled and connected, replaces Spotify/iTunes as the primary metadata source for searches.</div>
</div>
<div class="form-actions">
<button class="test-button" id="hydrabase-connect-btn" onclick="toggleHydrabaseFromSettings()">Connect</button>
<span id="hydrabase-settings-status" style="font-size: 0.82em; color: rgba(255,255,255,0.4); margin-left: 8px;"></span>
</div>
</div>
</div>

View file

@ -5555,6 +5555,12 @@ function toggleStgService(el) {
const service = el.closest('.stg-service');
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) ──
const HYBRID_SOURCES = [

View file

@ -51081,6 +51081,119 @@ tr.tag-diff-same {
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 ── */
#settings-page .accent-color-selector {
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 .callback-info { padding: 2px 12px 10px; }
#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 */
#settings-page .settings-actions { padding: 16px 8px; }
#settings-page .settings-actions .save-button { width: 100%; }