Files
shelfless/src/store/settingsStore.ts
Scarriffle 83d8b7b99d Initial commit: Shelfless – alternative Audiobookshelf frontend
React + Vite + TypeScript SPA covering the full ABS feature set (library
browsing, item detail, metadata/cover editing, podcasts, player with session
sync, admin: users/libraries/scanner/server settings). Dev uses a dynamic
CORS proxy; production is served by server/index.mjs (static + reverse proxy
to ABS_URL). Includes systemd unit and installer under deploy/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 20:23:04 +02:00

94 lines
3.3 KiB
TypeScript

import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export type AccentName = 'amber' | 'teal' | 'coral' | 'green' | 'blue'
export const ACCENTS: { name: AccentName; label: string; swatch: string }[] = [
{ name: 'amber', label: 'Amber', swatch: '#f59e0b' },
{ name: 'teal', label: 'Türkis', swatch: '#14b8a6' },
{ name: 'coral', label: 'Koralle', swatch: '#fb7185' },
{ name: 'green', label: 'Grün', swatch: '#22c55e' },
{ name: 'blue', label: 'Blau', swatch: '#3b82f6' },
]
export const PLAYBACK_SPEEDS = [0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3] as const
interface SettingsState {
accent: AccentName
/** When set, a freely chosen hex color overrides the named preset. */
customAccent: string | null
defaultSpeed: number
sidebarCollapsed: boolean
setAccent: (accent: AccentName) => void
setCustomAccent: (hex: string) => void
setDefaultSpeed: (speed: number) => void
toggleSidebar: () => void
}
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
accent: 'amber',
customAccent: null,
defaultSpeed: 1,
sidebarCollapsed: false,
setAccent: (accent) => {
applyTheme(accent, null)
set({ accent, customAccent: null })
},
setCustomAccent: (hex) => {
applyTheme('amber', hex)
set({ customAccent: hex })
},
setDefaultSpeed: (defaultSpeed) => set({ defaultSpeed }),
toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })),
}),
{
name: 'shelfless.settings',
onRehydrateStorage: () => (state) => {
if (state) applyTheme(state.accent, state.customAccent)
},
},
),
)
// ── Theme application ─────────────────────────────────────────────────────
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
let h = hex.trim().replace(/^#/, '')
if (h.length === 3) h = h.split('').map((c) => c + c).join('')
if (!/^[0-9a-fA-F]{6}$/.test(h)) return null
return {
r: parseInt(h.slice(0, 2), 16),
g: parseInt(h.slice(2, 4), 16),
b: parseInt(h.slice(4, 6), 16),
}
}
/** Pick readable foreground (near-black or near-white) for a given accent color. */
function onAccentFor(r: number, g: number, b: number): string {
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
return luminance > 0.6 ? '#0c0a09' : '#fafaf9'
}
/**
* Reflect the chosen accent onto the document. A named preset uses the
* `[data-accent]` CSS rules; a custom hex sets inline CSS variables (which win).
*/
export function applyTheme(accent: AccentName, customAccent: string | null) {
if (typeof document === 'undefined') return
const root = document.documentElement
const rgb = customAccent ? hexToRgb(customAccent) : null
if (customAccent && rgb) {
root.style.setProperty('--accent', customAccent)
root.style.setProperty('--accent-soft', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.14)`)
root.style.setProperty('--on-accent', onAccentFor(rgb.r, rgb.g, rgb.b))
root.setAttribute('data-accent', 'custom')
} else {
root.style.removeProperty('--accent')
root.style.removeProperty('--accent-soft')
root.style.removeProperty('--on-accent')
root.setAttribute('data-accent', accent)
}
}