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
+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;
|
||||
|
||||
Reference in new issue
Block a user