From d8718994516d3abe71315ecaed00efd53188384d Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 28 Jun 2026 21:50:21 +0200 Subject: [PATCH 01/11] Apply saved appearance on app startup --- webui/static/init.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/webui/static/init.js b/webui/static/init.js index aea68fab..aec092d7 100644 --- a/webui/static/init.js +++ b/webui/static/init.js @@ -274,6 +274,34 @@ function applyReduceEffects(enabled) { } })(); +async function bootstrapServerAppearanceSettings() { + try { + const response = await fetch('/api/settings', { credentials: 'same-origin' }); + const settings = await response.json(); + if (!response.ok || !settings || typeof settings !== 'object' || settings.error) return; + + const appearance = settings.ui_appearance || {}; + const preset = appearance.accent_preset || '#1db954'; + const custom = appearance.accent_color || '#1db954'; + const accent = preset === 'custom' ? custom : preset; + applyAccentColor(accent); + + if (Object.prototype.hasOwnProperty.call(appearance, 'particles_enabled')) { + applyParticlesSetting(appearance.particles_enabled !== false); + } + if (Object.prototype.hasOwnProperty.call(appearance, 'worker_orbs_enabled')) { + applyWorkerOrbsSetting(appearance.worker_orbs_enabled !== false); + } + if (localStorage.getItem('soulsync-reduce-effects') === null) { + applyReduceEffects(appearance.reduce_effects === true); + } + } catch (error) { + console.warn('Could not bootstrap appearance settings:', error); + } +} + +bootstrapServerAppearanceSettings(); + // ── Profile System ───────────────────────────────────────────── let currentProfile = null; const PROFILE_CONTEXT_CHANGED_EVENT = 'ss:webui-profile-context-changed'; From d4e2dccd73ab3031e5e4337247722b2355b9907b Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 28 Jun 2026 21:55:47 +0200 Subject: [PATCH 02/11] Render saved appearance before CSS paints --- web_server.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++- webui/index.html | 9 +++++- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/web_server.py b/web_server.py index 826c3fbe..84713700 100644 --- a/web_server.py +++ b/web_server.py @@ -303,6 +303,77 @@ app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 if DEV_STATIC_NO_CACHE else 31536000 import time as _cache_bust_time _STATIC_CACHE_BUST = str(int(_cache_bust_time.time())) +def _valid_hex_color(value, fallback='#1db954'): + value = str(value or '').strip() + return value if re.fullmatch(r'#[0-9a-fA-F]{6}', value) else fallback + + +def _hex_to_rgb(hex_color): + color = _valid_hex_color(hex_color) + return tuple(int(color[i:i + 2], 16) for i in (1, 3, 5)) + + +def _rgb_to_hsl(r, g, b): + rn, gn, bn = r / 255, g / 255, b / 255 + max_c = max(rn, gn, bn) + min_c = min(rn, gn, bn) + lightness = (max_c + min_c) / 2 + if max_c == min_c: + return 0, 0, lightness + + delta = max_c - min_c + saturation = delta / (2 - max_c - min_c) if lightness > 0.5 else delta / (max_c + min_c) + if max_c == rn: + hue = ((gn - bn) / delta + (6 if gn < bn else 0)) / 6 + elif max_c == gn: + hue = ((bn - rn) / delta + 2) / 6 + else: + hue = ((rn - gn) / delta + 4) / 6 + return hue, saturation, lightness + + +def _hsl_to_rgb(hue, saturation, lightness): + if saturation == 0: + value = round(lightness * 255) + return value, value, value + + def hue_to_rgb(p, q, t): + if t < 0: + t += 1 + if t > 1: + t -= 1 + if t < 1 / 6: + return p + (q - p) * 6 * t + if t < 1 / 2: + return q + if t < 2 / 3: + return p + (q - p) * (2 / 3 - t) * 6 + return p + + q = lightness * (1 + saturation) if lightness < 0.5 else lightness + saturation - lightness * saturation + p = 2 * lightness - q + return ( + round(hue_to_rgb(p, q, hue + 1 / 3) * 255), + round(hue_to_rgb(p, q, hue) * 255), + round(hue_to_rgb(p, q, hue - 1 / 3) * 255), + ) + + +def _initial_appearance_context(): + preset = config_manager.get('ui_appearance.accent_preset', '#1db954') + custom = config_manager.get('ui_appearance.accent_color', '#1db954') + accent = _valid_hex_color(custom if preset == 'custom' else preset) + r, g, b = _hex_to_rgb(accent) + hue, saturation, lightness = _rgb_to_hsl(r, g, b) + light = _hsl_to_rgb(hue, saturation, min(lightness + 0.16, 0.95)) + neon = _hsl_to_rgb(hue, min(saturation + 0.1, 1.0), min(lightness + 0.30, 0.95)) + return { + 'initial_accent_color': accent, + 'initial_accent_rgb': f'{r}, {g}, {b}', + 'initial_accent_light_rgb': f'{light[0]}, {light[1]}, {light[2]}', + 'initial_accent_neon_rgb': f'{neon[0]}, {neon[1]}, {neon[2]}', + } + @app.context_processor def _inject_static_cache_bust(): @@ -319,7 +390,7 @@ def _inject_static_cache_bust(): static_v = str(max(mtimes)) except Exception: static_v = _STATIC_CACHE_BUST - return {'static_v': static_v} + return {'static_v': static_v, **_initial_appearance_context()} @app.context_processor diff --git a/webui/index.html b/webui/index.html index 9690da8b..a5d1b4f8 100644 --- a/webui/index.html +++ b/webui/index.html @@ -7,12 +7,19 @@ SoulSync - Music Sync & Manager - + + {{ vite_assets('head')|safe }} From e2317de0a43af6c5939c79ec7c269a70fe497d92 Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 28 Jun 2026 22:25:28 +0200 Subject: [PATCH 03/11] Remove dead settings CSS --- webui/static/style.css | 335 ----------------------------------------- 1 file changed, 335 deletions(-) diff --git a/webui/static/style.css b/webui/static/style.css index ac4bcbfe..e4e741b0 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -3389,66 +3389,6 @@ body.helper-mode-active #dashboard-activity-feed:hover { color: #a78bfa; /* Usenet — violet */ } -/* Lidarr-style intro hero card on Indexers & Downloaders tab */ -.ind-hero { - padding: 18px 22px; - background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.08), rgba(35, 35, 35, 0.6)); - border: 1px solid rgba(var(--accent-rgb), 0.15); -} - -.ind-hero-title { - font-size: 13px; - font-weight: 700; - letter-spacing: 0.8px; - text-transform: uppercase; - color: rgb(var(--accent-rgb)); - margin-bottom: 12px; -} - -.ind-hero-flow { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 10px; - margin-bottom: 12px; - font-size: 13px; - color: rgba(255, 255, 255, 0.85); -} - -.ind-hero-step { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 6px 12px; - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 999px; -} - -.ind-hero-step-num { - width: 20px; - height: 20px; - border-radius: 50%; - background: rgba(var(--accent-rgb), 0.2); - color: rgb(var(--accent-rgb)); - font-weight: 700; - font-size: 11px; - display: inline-flex; - align-items: center; - justify-content: center; -} - -.ind-hero-arrow { - color: rgba(var(--accent-rgb), 0.55); - font-weight: 700; -} - -.ind-hero-sub { - font-size: 12.5px; - color: rgba(255, 255, 255, 0.55); - line-height: 1.55; -} - .ind-hero-warning { margin-top: 14px; padding: 12px 14px; @@ -3939,23 +3879,6 @@ body.helper-mode-active #dashboard-activity-feed:hover { font-size: 12px; color: rgba(255, 255, 255, 0.85); } -.info-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 16px; - height: 16px; - flex: 0 0 auto; - font-size: 12px; - line-height: 1; - color: rgba(var(--accent-rgb), 0.85); - cursor: pointer; - user-select: none; - transition: color 0.15s ease; -} -.info-icon:hover { color: rgba(var(--accent-rgb), 1); } -.setting-help-body { margin-top: 4px; } - /* Supported Formats */ .supported-formats { color: rgba(255, 255, 255, 0.7); @@ -4343,213 +4266,6 @@ body.helper-mode-active #dashboard-activity-feed:hover { background: rgba(99, 179, 237, 0.3); } -.quality-tier { - margin-bottom: 16px; - padding: 12px; - background: linear-gradient(135deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.01)); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 12px; - transition: all 0.2s ease; -} - -.quality-tier:hover { - border-color: rgba(255, 255, 255, 0.12); -} - -.quality-tier-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; -} - -.quality-tier-name { - color: rgba(255, 255, 255, 0.95); - font-size: 12px; - font-weight: 600; -} - -.quality-tier-priority { - color: rgba(255, 255, 255, 0.45); - font-size: 10px; - font-weight: 500; -} - -.quality-tier-sliders { - padding-left: 24px; - opacity: 1; - transition: opacity 0.3s ease; -} - -.quality-tier-sliders.disabled { - opacity: 0.35; - pointer-events: none; -} - -.flac-bit-depth-selector { - padding: 8px 12px; - transition: opacity 0.3s ease; -} - -.flac-bit-depth-selector.disabled { - opacity: 0.35; - pointer-events: none; -} - -.flac-bit-depth-selector label { - display: block; - margin-bottom: 6px; - color: rgba(255, 255, 255, 0.7); - font-size: 11px; -} - -.bit-depth-buttons { - display: flex; - gap: 8px; -} - -.bit-depth-btn { - flex: 1; - padding: 7px 12px; - background: linear-gradient(135deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.02)); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 10px; - color: rgba(255, 255, 255, 0.7); - font-size: 11px; - font-weight: 500; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -.bit-depth-btn.active { - background: linear-gradient(135deg, - rgba(var(--accent-rgb), 0.2) 0%, - rgba(var(--accent-light-rgb), 0.12) 100%); - border-color: rgba(var(--accent-rgb), 0.4); - color: rgb(var(--accent-light-rgb)); - font-weight: 600; - box-shadow: 0 4px 16px rgba(var(--accent-rgb), 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.05); -} - -.bit-depth-btn:hover { - background: rgba(255, 255, 255, 0.08); - border-color: rgba(255, 255, 255, 0.15); - color: #ffffff; - transform: translateY(-1px); -} - -.bit-depth-btn.active:hover { - background: linear-gradient(135deg, - rgba(var(--accent-rgb), 0.28) 0%, - rgba(var(--accent-light-rgb), 0.18) 100%); -} - -.slider-group { - margin-top: 8px; -} - -.slider-group label { - display: block; - margin-bottom: 8px; - color: rgba(255, 255, 255, 0.7); - font-size: 11px; -} - -.dual-slider-container { - position: relative; - height: 40px; - margin: 10px 0; -} - -.range-slider { - position: absolute; - width: 100%; - height: 4px; - -webkit-appearance: none; - appearance: none; - background: transparent; - outline: none; - pointer-events: none; -} - -.range-slider::-webkit-slider-track { - width: 100%; - height: 4px; - background: rgba(255, 255, 255, 0.08); - border-radius: 2px; -} - -.range-slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 16px; - height: 16px; - border-radius: 50%; - background: linear-gradient(135deg, rgb(var(--accent-rgb)), rgb(var(--accent-light-rgb))); - cursor: pointer; - pointer-events: all; - border: 2px solid #0a0a0a; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4), 0 0 8px rgba(var(--accent-rgb), 0.2); -} - -.range-slider::-webkit-slider-thumb:hover { - background: linear-gradient(135deg, rgb(var(--accent-light-rgb)), #22e968); - transform: scale(1.15); - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4), 0 0 12px rgba(var(--accent-rgb), 0.3); -} - -.range-slider::-moz-range-track { - width: 100%; - height: 4px; - background: rgba(255, 255, 255, 0.08); - border-radius: 2px; -} - -.range-slider::-moz-range-thumb { - width: 16px; - height: 16px; - border-radius: 50%; - background: linear-gradient(135deg, rgb(var(--accent-rgb)), rgb(var(--accent-light-rgb))); - cursor: pointer; - pointer-events: all; - border: 2px solid #0a0a0a; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4), 0 0 8px rgba(var(--accent-rgb), 0.2); -} - -.range-slider::-moz-range-thumb:hover { - background: linear-gradient(135deg, rgb(var(--accent-light-rgb)), #22e968); - transform: scale(1.15); - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4), 0 0 12px rgba(var(--accent-rgb), 0.3); -} - -.range-slider-track { - position: absolute; - top: 18px; - width: 100%; - height: 4px; - background: rgba(var(--accent-rgb), 0.25); - border-radius: 2px; - pointer-events: none; -} - -.slider-values { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 8px; - color: rgba(255, 255, 255, 0.5); - font-size: 11px; - font-weight: 500; -} - -.slider-values span:first-child, -.slider-values span:last-child { - color: rgb(var(--accent-light-rgb)); - font-weight: 700; -} - -/* ===== END QUALITY PROFILE STYLES ===== */ - .test-button { background: rgba(var(--accent-rgb), 0.1); color: rgb(var(--accent-light-rgb)); @@ -4603,12 +4319,6 @@ body.helper-mode-active #dashboard-activity-feed:hover { transform: translateY(0) scale(0.97); } -.settings-actions { - display: flex; - justify-content: center; - padding-top: 20px; -} - /* Additional Settings Components */ .path-input-group { display: flex; @@ -55841,18 +55551,6 @@ tr.tag-diff-same { flex-wrap: wrap; } -/* ── Quality section ── */ -#settings-page .quality-tier { - background: rgba(255, 255, 255, 0.02); - border: 1px solid rgba(255, 255, 255, 0.05); - border-radius: 12px; - margin-bottom: 10px; - transition: border-color 0.2s; -} -#settings-page .quality-tier:hover { - border-color: rgba(255, 255, 255, 0.1); - box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12); -} #settings-page .preset-button { border-radius: 8px; font-size: 0.84em; @@ -55902,30 +55600,6 @@ tr.tag-diff-same { padding: 0 18px 12px; } -/* ── Save button — centered, sticky feel ── */ -#settings-page .settings-actions { - max-width: 760px; - width: 100%; - margin: 0 auto; - padding: 24px 0 16px; -} -#settings-page .settings-actions .save-button { - width: 100%; - padding: 12px; - border-radius: 10px; - font-size: 0.92em; - font-weight: 600; - transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); -} -#settings-page .settings-actions .save-button:hover { - transform: translateY(-1px); - box-shadow: 0 4px 20px rgba(var(--accent-rgb, 29, 185, 84), 0.35); -} -#settings-page .settings-actions .save-button:active { - transform: scale(0.99); - box-shadow: 0 2px 8px rgba(var(--accent-rgb, 29, 185, 84), 0.2); -} - /* ── Path input groups (dir paths with Unlock buttons) ── */ #settings-page .path-input-group { flex: 1; @@ -56028,12 +55702,6 @@ tr.tag-diff-same { box-shadow: 0 2px 16px rgba(0, 0, 0, 0.12); } -/* ── Path inputs with lock buttons ── */ -#settings-page .form-group .path-input-wrapper { - flex: 1; - max-width: 340px; -} - /* ── Hybrid source priority list (drag and drop) ── */ .hybrid-source-list { display: flex; @@ -56415,9 +56083,6 @@ tr.tag-diff-same { /* 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%; } /* Section titles */ #settings-page .settings-group > h3 { padding: 24px 0 10px; } } From 60dee1b4d82e1d6208fb3eedc6619c9ac2fb5d6b Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 28 Jun 2026 22:47:37 +0200 Subject: [PATCH 04/11] Align source settings card spacing --- webui/static/style.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/webui/static/style.css b/webui/static/style.css index e4e741b0..1cfc2a35 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -68589,3 +68589,14 @@ body.app-locked > *:not(#launch-pin-overlay):not(#login-overlay):not(script):not .reid-replace-text em { color: #7f879e; font-style: normal; font-size: 11.5px; } .reid-footer-actions { display: flex; gap: 10px; } #reid-confirm-btn:disabled { opacity: 0.4; cursor: not-allowed; filter: grayscale(0.4); } + +/* Settings cleanup: source settings uses the same inner-card spacing as Retry Logic. */ +#settings-page .settings-section-body:not(.collapsed) > .settings-group.source-settings-wrapper { + padding: 0 !important; + display: flex; + flex-direction: column; + gap: 14px; +} +#settings-page .settings-section-body:not(.collapsed) > .settings-group.source-settings-wrapper > .settings-subcard { + margin: 0 !important; +} From 0276aa8764c7bf3f52670bf75685d4d47019b534 Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 28 Jun 2026 22:52:09 +0200 Subject: [PATCH 05/11] Update settings page overhaul --- webui/index.html | 978 +++++++++++++++++++++++---------------- webui/static/settings.js | 250 ++++++++-- webui/static/style.css | 350 +++++++++++++- 3 files changed, 1129 insertions(+), 449 deletions(-) diff --git a/webui/index.html b/webui/index.html index a5d1b4f8..46df8390 100644 --- a/webui/index.html +++ b/webui/index.html @@ -2148,7 +2148,7 @@
- @@ -3908,14 +3908,13 @@
-
+

Settings

-

Configure services, downloads, and preferences

- +
@@ -3923,7 +3922,7 @@
- + @@ -3937,6 +3936,7 @@

API Configuration

+

Metadata Source

@@ -3950,9 +3950,9 @@ -
-
-
Where artist, album, and track metadata comes from:
+ i +
- -
-
Runs the background metadata enrichment worker on the no-auth Spotify source instead of this connected account — keeping bulk enrichment off your official API quota (and dodging rate-limit bans), so your account is reserved for interactive search and playlist sync. On by default. Turn off to enrich through your connected account instead. Works even with no account connected. Note: the no-auth source can't supply artist genres.
+
+ + i
+
@@ -4571,81 +4573,17 @@
-
-

Download Settings

-
- These are container-internal paths. Only modify them if you know what you're doing — incorrect values will break downloads. -
+ + +