From c046aea77632aa693786f9ddda09f0cdd42f263a Mon Sep 17 00:00:00 2001 From: Honey Date: Tue, 28 Apr 2026 10:10:44 +0200 Subject: [PATCH] feat: theme system with persistent selection and Dark Ocean starter theme Co-Authored-By: Claude Sonnet 4.6 --- index.html | 15 ++++++++--- src/main.js | 6 ++++- src/themes.js | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/ui.js | 18 +++++++++++++ styles.css | 15 ++++++++--- 5 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 src/themes.js diff --git a/index.html b/index.html index 09181c4..1eeaf45 100644 --- a/index.html +++ b/index.html @@ -324,7 +324,7 @@ - +
@@ -464,7 +464,7 @@
-
⚙
+
⚙
Einstellungen
NovaSonic
@@ -509,11 +509,18 @@
+
+

+ 🎨 + Theme +

+
+
- +
@@ -537,7 +544,7 @@
- +
diff --git a/src/main.js b/src/main.js index 4901040..73d0b53 100644 --- a/src/main.js +++ b/src/main.js @@ -2,6 +2,7 @@ import { S } from './state.js'; import { toast } from './utils.js'; import { api } from './api.js'; import { injectPWA } from './pwa.js'; +import { loadSavedTheme } from './themes.js'; import { playQueue, playCurrent, togglePlay, prevSong, nextSong, onEnded, toggleShuffle, toggleRepeat, toggleQueue, setVolume, seekTo, @@ -31,6 +32,7 @@ import { deletePlaylist, _confirmOk, _confirmCancel, toggleMobileSearch, closeMobileSearch, switchSetupTab, toggleNotifPanel, deleteNotif, clearNotifs, initNotifBell, _notifNavigate, + _pickTheme, } from './ui.js'; // ─── CONNECT ─── @@ -92,6 +94,8 @@ async function startApp() { // ─── INIT ─── window.addEventListener('load', () => { + loadSavedTheme(); + // Wire up audio elements from DOM S.audio = document.getElementById('audio-element'); S.previewAudio = document.getElementById('preview-audio'); @@ -241,7 +245,7 @@ Object.assign(window, { openEditPlModal, closeEditPlModal, saveEditPlaylist, editPlUploadCover, editPlCoverSelected, editPlRemoveCover, deletePlaylist, _confirmOk, _confirmCancel, toggleMobileSearch, closeMobileSearch, switchSetupTab, - toggleNotifPanel, deleteNotif, clearNotifs, _notifNavigate, + toggleNotifPanel, deleteNotif, clearNotifs, _notifNavigate, _pickTheme, // App doConnect, disconnect, }); diff --git a/src/themes.js b/src/themes.js new file mode 100644 index 0000000..aa0b23d --- /dev/null +++ b/src/themes.js @@ -0,0 +1,70 @@ +const THEME_KEY = 'ns_theme'; +const DEFAULT_ID = 'dark-amber'; + +export const THEMES = [ + { + id: 'dark-amber', + name: 'Dark Amber', + accent: '#e8a020', + bg: '#07070f', + vars: { + '--bg': '#07070f', + '--bg2': '#0d0d1a', + '--bg3': '#12121f', + '--surface': '#1a1a2e', + '--surface2': '#22223a', + '--border': 'rgba(255,255,255,0.07)', + '--border2': 'rgba(255,255,255,0.12)', + '--accent': '#e8a020', + '--accent2': '#f0b840', + '--accent-dark':'#c07010', + '--accent-dim': 'rgba(232,160,32,0.15)', + '--accent-glow':'rgba(232,160,32,0.3)', + '--text': '#f0eefc', + '--text2': '#8888aa', + '--text3': '#555577', + '--danger': '#e05555', + }, + }, + { + id: 'dark-ocean', + name: 'Dark Ocean', + accent: '#4f8ef7', + bg: '#070d14', + vars: { + '--bg': '#070d14', + '--bg2': '#0c1520', + '--bg3': '#111c28', + '--surface': '#182435', + '--surface2': '#1f2f42', + '--border': 'rgba(255,255,255,0.07)', + '--border2': 'rgba(255,255,255,0.13)', + '--accent': '#4f8ef7', + '--accent2': '#70aaff', + '--accent-dark':'#2a6cd6', + '--accent-dim': 'rgba(79,142,247,0.15)', + '--accent-glow':'rgba(79,142,247,0.3)', + '--text': '#edf2ff', + '--text2': '#7088aa', + '--text3': '#445566', + '--danger': '#e05555', + }, + }, +]; + +export function applyTheme(id) { + const theme = THEMES.find(t => t.id === id) || THEMES.find(t => t.id === DEFAULT_ID); + const root = document.documentElement; + Object.entries(theme.vars).forEach(([k, v]) => root.style.setProperty(k, v)); + localStorage.setItem(THEME_KEY, theme.id); +} + +export function loadSavedTheme() { + const saved = localStorage.getItem(THEME_KEY) || DEFAULT_ID; + applyTheme(saved); + return saved; +} + +export function getCurrentThemeId() { + return localStorage.getItem(THEME_KEY) || DEFAULT_ID; +} diff --git a/src/ui.js b/src/ui.js index bc2b8e2..94c3ccb 100644 --- a/src/ui.js +++ b/src/ui.js @@ -1,6 +1,7 @@ import { S } from './state.js'; import { esc, formatDur, toast } from './utils.js'; import { api, coverUrl, ndAuth } from './api.js'; +import { THEMES, applyTheme, getCurrentThemeId } from './themes.js'; // Circular imports — safe (only used inside function bodies) import { playQueue, dlSongLocal, toggleStar, togglePlay, renderQueue, syncNpStar, updateStarBtn, npToggleStar, playCurrent, _syncNpDirect, setPlayIcon } from './player.js'; import { navigate, loadPlaylists } from './views.js'; @@ -61,9 +62,26 @@ export function openSettings() { document.getElementById('s-pass').value = ''; document.getElementById('s-lastfm').value = S.cfg.lastfmKey || ''; document.getElementById('s-backend').value = S.cfg.backendUrl || ''; + _renderThemePicker(); document.getElementById('settings-modal').classList.add('open'); checkBackend(); } + +function _renderThemePicker() { + const el = document.getElementById('theme-picker'); + if (!el) return; + const current = getCurrentThemeId(); + el.innerHTML = THEMES.map(t => ` +
+
+ ${esc(t.name)} +
`).join(''); +} + +export function _pickTheme(id) { + applyTheme(id); + _renderThemePicker(); +} export function closeSettings() { document.getElementById('settings-modal').classList.remove('open'); } export function saveSettings() { const pass = document.getElementById('s-pass').value; diff --git a/styles.css b/styles.css index 5d1be12..583256e 100644 --- a/styles.css +++ b/styles.css @@ -2,7 +2,7 @@ --bg:#07070f;--bg2:#0d0d1a;--bg3:#12121f; --surface:#1a1a2e;--surface2:#22223a; --border:rgba(255,255,255,0.07);--border2:rgba(255,255,255,0.12); - --accent:#e8a020;--accent2:#f0b840; + --accent:#e8a020;--accent2:#f0b840;--accent-dark:#c07010; --accent-dim:rgba(232,160,32,0.15);--accent-glow:rgba(232,160,32,0.3); --blue:#4f8ef7;--blue-dim:rgba(79,142,247,0.15); --green:#4caf7d;--green-dim:rgba(76,175,125,0.15); @@ -20,7 +20,7 @@ html,body{height:100%;font-family:var(--font);background:var(--bg);color:var(--t #setup-screen{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;background:var(--bg);background-image:radial-gradient(ellipse 60% 50% at 30% 50%,rgba(232,160,32,.08) 0%,transparent 70%),radial-gradient(ellipse 40% 60% at 80% 30%,rgba(80,60,160,.1) 0%,transparent 70%)} .setup-card{width:min(420px,calc(100vw - 32px));background:var(--surface);border:1px solid var(--border2);border-radius:20px;padding:40px 36px;box-shadow:0 40px 80px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.04)} .setup-logo{display:flex;align-items:center;gap:12px;margin-bottom:28px} -.logo-icon{width:44px;height:44px;border-radius:12px;background:linear-gradient(135deg,var(--accent),#c07010);display:flex;align-items:center;justify-content:center;font-size:20px;box-shadow:0 8px 24px var(--accent-glow)} +.logo-icon{width:44px;height:44px;border-radius:12px;background:linear-gradient(135deg,var(--accent),var(--accent-dark));display:flex;align-items:center;justify-content:center;font-size:20px;box-shadow:0 8px 24px var(--accent-glow)} .setup-logo h1{font-size:24px;font-weight:700;letter-spacing:-.5px} .setup-logo h1 em{color:var(--accent);font-style:normal} .setup-tabs{display:flex;gap:4px;background:var(--bg2);border-radius:8px;padding:3px;margin-bottom:24px} @@ -32,7 +32,7 @@ html,body{height:100%;font-family:var(--font);background:var(--bg);color:var(--t .field input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-dim)} .field input::placeholder{color:var(--text3)} .field .hint{font-size:11px;color:var(--text3);margin-top:4px;line-height:1.4} -.btn-primary{width:100%;margin-top:20px;padding:13px;background:linear-gradient(135deg,var(--accent),#c07010);color:#000;font-family:var(--font);font-size:15px;font-weight:700;border:none;border-radius:var(--radius-sm);cursor:pointer;transition:opacity .2s,transform .15s;letter-spacing:.2px} +.btn-primary{width:100%;margin-top:20px;padding:13px;background:linear-gradient(135deg,var(--accent),var(--accent-dark));color:#000;font-family:var(--font);font-size:15px;font-weight:700;border:none;border-radius:var(--radius-sm);cursor:pointer;transition:opacity .2s,transform .15s;letter-spacing:.2px} .btn-primary:hover{opacity:.9;transform:translateY(-1px)} .btn-primary:disabled{opacity:.5;cursor:default;transform:none} .setup-error{margin-top:12px;padding:10px 14px;background:rgba(224,85,85,.12);border:1px solid rgba(224,85,85,.3);border-radius:var(--radius-sm);color:#ff8a8a;font-size:13px;display:none} @@ -46,7 +46,7 @@ html,body{height:100%;font-family:var(--font);background:var(--bg);color:var(--t .sidebar{width:var(--sidebar-w);flex-shrink:0;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden;padding-bottom:var(--player-h)} .sidebar-header{padding:18px 18px 10px;display:flex;align-items:center;justify-content:space-between} .sidebar-logo{display:flex;align-items:center;gap:9px} -.sidebar-logo .icon{width:30px;height:30px;border-radius:8px;background:linear-gradient(135deg,var(--accent),#c07010);display:flex;align-items:center;justify-content:center;font-size:14px;box-shadow:0 4px 12px var(--accent-glow)} +.sidebar-logo .icon{width:30px;height:30px;border-radius:8px;background:linear-gradient(135deg,var(--accent),var(--accent-dark));display:flex;align-items:center;justify-content:center;font-size:14px;box-shadow:0 4px 12px var(--accent-glow)} .sidebar-logo span{font-size:16px;font-weight:700} .sidebar-logo em{color:var(--accent);font-style:normal} .header-btns{display:flex;gap:4px} @@ -737,6 +737,13 @@ nav{flex:1;overflow-y:auto;padding:4px 10px} @keyframes toastIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}} @keyframes toastOut{to{opacity:0;transform:translateY(8px)}} +/* ── THEME PICKER ── */ +.theme-picker{display:flex;flex-wrap:wrap;gap:8px} +.theme-swatch{display:flex;align-items:center;gap:8px;padding:8px 12px;border-radius:9px;border:2px solid var(--border2);background:var(--bg2);cursor:pointer;transition:border-color .15s,background .15s;font-size:13px;font-weight:500;color:var(--text2)} +.theme-swatch:hover{border-color:var(--border2);background:var(--surface)} +.theme-swatch.active{border-color:var(--accent);color:var(--text)} +.theme-swatch-dot{width:18px;height:18px;border-radius:50%;background:var(--sw-accent);flex-shrink:0;box-shadow:inset 0 0 0 2px rgba(0,0,0,.2)} + /* ── SETTINGS MODAL ── */ .settings-section{margin-bottom:20px} .settings-section h3{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text2);margin-bottom:10px}