feat: theme system with persistent selection and Dark Ocean starter theme
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
1bb8494fdb
commit
c046aea776
5 files changed
+115
-9
No files matched your search
+11
-4
@@ -324,7 +324,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M8 11.973c2.5-1.473 5.5-.973 7.5.527M9.5 14.473c1.5-.973 3.5-.973 5 .027M7 9c3-1.5 6.5-1 9 1"/></svg>
|
||||
<input id="spotify-link-input" type="url" placeholder="Spotify Link einfügen (Track, Album oder Playlist)…" style="font-size:15px" oninput="clearSpotifyPreview()">
|
||||
</div>
|
||||
<button class="btn-outline" style="padding:0 20px;font-size:14px;font-weight:700;white-space:nowrap;flex-shrink:0;background:linear-gradient(135deg,var(--accent),#c07010);color:#000;border-color:transparent" onclick="resolveSpotifyLink()">Importieren</button>
|
||||
<button class="btn-outline" style="padding:0 20px;font-size:14px;font-weight:700;white-space:nowrap;flex-shrink:0;background:linear-gradient(135deg,var(--accent),var(--accent-dark));color:#000;border-color:transparent" onclick="resolveSpotifyLink()">Importieren</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="spotify-importer-hint" style="background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:16px;font-size:13px;color:var(--text2);line-height:1.8">
|
||||
@@ -464,7 +464,7 @@
|
||||
<!-- Sticky header -->
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:20px 24px 16px;border-bottom:1px solid var(--border);flex-shrink:0">
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<div style="width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,var(--accent),#c07010);display:flex;align-items:center;justify-content:center;font-size:17px;box-shadow:0 4px 12px var(--accent-glow)">⚙</div>
|
||||
<div style="width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,var(--accent),var(--accent-dark));display:flex;align-items:center;justify-content:center;font-size:17px;box-shadow:0 4px 12px var(--accent-glow)">⚙</div>
|
||||
<div>
|
||||
<div style="font-size:17px;font-weight:800;letter-spacing:-.3px">Einstellungen</div>
|
||||
<div style="font-size:11px;color:var(--text2);margin-top:1px">NovaSonic</div>
|
||||
@@ -509,11 +509,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||||
<span style="width:24px;height:24px;border-radius:6px;background:var(--surface2);display:inline-flex;align-items:center;justify-content:center;font-size:12px">🎨</span>
|
||||
Theme
|
||||
</h3>
|
||||
<div id="theme-picker" class="theme-picker"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sticky footer -->
|
||||
<div style="display:flex;gap:8px;padding:12px 24px 16px;border-top:1px solid var(--border);flex-shrink:0;background:var(--surface);border-radius:0 0 18px 18px">
|
||||
<button style="flex:1;padding:12px;border-radius:9px;font-family:var(--font);font-size:14px;font-weight:600;cursor:pointer;border:none;background:var(--surface2);color:var(--text2)" onclick="closeSettings()">Abbrechen</button>
|
||||
<button style="flex:1;padding:12px;border-radius:9px;font-family:var(--font);font-size:14px;font-weight:600;cursor:pointer;border:none;background:linear-gradient(135deg,var(--accent),#c07010);color:#000" onclick="saveSettings()">Speichern</button>
|
||||
<button style="flex:1;padding:12px;border-radius:9px;font-family:var(--font);font-size:14px;font-weight:600;cursor:pointer;border:none;background:linear-gradient(135deg,var(--accent),var(--accent-dark));color:#000" onclick="saveSettings()">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -537,7 +544,7 @@
|
||||
<input class="pl-input" id="pl-new-name" placeholder="Neue Playlist erstellen…" type="text">
|
||||
<div class="pl-btns">
|
||||
<button style="background:var(--surface2);color:var(--text2)" onclick="closePlModal()">Abbrechen</button>
|
||||
<button style="background:linear-gradient(135deg,var(--accent),#c07010);color:#000" onclick="createAndAddPlaylist()">Erstellen & Hinzufügen</button>
|
||||
<button style="background:linear-gradient(135deg,var(--accent),var(--accent-dark));color:#000" onclick="createAndAddPlaylist()">Erstellen & Hinzufügen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+5
-1
@@ -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,
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 => `
|
||||
<div class="theme-swatch${t.id === current ? ' active' : ''}" data-id="${t.id}" onclick="_pickTheme('${t.id}')" style="--sw-bg:${t.bg};--sw-accent:${t.accent}">
|
||||
<div class="theme-swatch-dot"></div>
|
||||
<span>${esc(t.name)}</span>
|
||||
</div>`).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;
|
||||
|
||||
+11
-4
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user