// OVE — Settings tab (v6.5): appearance controls — base theme + highlight // color pickers. Everything is per-visitor (localStorage only, nothing on // the server) and applies instantly. The Native tab that previously lived // in this slot is parked (see control-panel.tsx) and returns unchanged // when the project nears completion. // // v7.7: the Music Services log in / "log out of all" section is PARKED // (MUSIC_SERVICES_UI below). Stream-a-Link already plays for everyone // through the operator's built-in house account, so the section was noise — // it returns (reworked) in a later update. Nothing server-side is affected // by this flag: saved visitor logins keep riding along with resolves, and // the house account keeps working exactly as before. 'use client' import { useSyncExternalStore, useState, useEffect } from 'react' import { Check, LogIn, LogOut, Music4, Palette, RotateCcw } from 'lucide-react' import { useTheme } from 'next-themes' import { Button } from '@/components/ui/button' import { Separator } from '@/components/ui/separator' import { ServiceLoginDialog } from '@/components/ava/service-login-dialog' import { SERVICES, type ServiceId } from '@/lib/music-services' import { clearAllMusicSessions, listMusicSessions, type SessionSummary, } from '@/lib/music-sessions' import { ACCENTS, DEFAULT_ACCENT, DEFAULT_BASE, THEME_BASES, applyAccent, isAccentId, storeDarkBase, type AccentId, } from '@/lib/theme' import { useToast } from '@/hooks/use-toast' import { cn } from '@/lib/utils' /** * v7.7 — Music Services login UI parked. The built-in house account already * powers Stream-a-Link for every visitor, so the log in / "log out of all" * section is hidden until it is reworked (the underlying session storage, * resolve auth attach and house-account behavior all keep working). */ const MUSIC_SERVICES_UI = false const emptySubscribe = () => () => {} function useMounted() { return useSyncExternalStore(emptySubscribe, () => true, () => false) } /** Re-read data-accent whenever applyAccent() mutates it on . */ function subscribeToAccent(onChange: () => void) { const obs = new MutationObserver(onChange) obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-accent'] }) return () => obs.disconnect() } const getAccentAttribute = () => document.documentElement.getAttribute('data-accent') export function SettingsTab() { const { resolvedTheme, setTheme } = useTheme() const mounted = useMounted() const isLight = mounted && resolvedTheme === 'light' const { toast } = useToast() // Current accent — the data-accent attribute on is the source of // truth (set pre-paint by the boot script, mutated by applyAccent). The // server snapshot returns null so the first client render matches SSR // ("unselected") and the selection appears right after hydration. const accentAttr = useSyncExternalStore(subscribeToAccent, getAccentAttribute, () => null) const accent: AccentId | null = isAccentId(accentAttr) ? accentAttr : null const activeBase = mounted ? resolvedTheme : null const activeAccentMeta = ACCENTS.find((a) => a.id === accent) // The little accent chip inside each base preview follows the current // highlight (its light-theme variant for the white base card). const chipFor = (baseIsLight: boolean) => (baseIsLight ? activeAccentMeta?.lightMain : activeAccentMeta?.main) ?? '#60a5fa' function pickBase(id: (typeof THEME_BASES)[number]['id']) { setTheme(id) const base = THEME_BASES.find((b) => b.id === id) if (base?.dark) storeDarkBase(id) // Pair the outline base with the BLUE highlight — v7.2's default showcase // pairing (the same look a new visitor gets); the visitor can re-pick any // highlight afterwards. if (id === 'outline' && accent !== 'blue') applyAccent('blue') } function pickAccent(id: AccentId) { applyAccent(id) } function resetToDefaults() { setTheme(DEFAULT_BASE) storeDarkBase(DEFAULT_BASE) applyAccent(DEFAULT_ACCENT) } // ---- music service logins (v6.6; UI parked in v7.7) ---- const [sessions, setSessions] = useState(null) const [loginOpen, setLoginOpen] = useState(false) const [chipService, setChipService] = useState(null) const [reloadTick, setReloadTick] = useState(0) // v6.9: which services have the operator's shared "house" account // configured (GET /api/resolve — booleans only, no cookie data) const [house, setHouse] = useState> | null>(null) useEffect(() => { if (!MUSIC_SERVICES_UI) return let cancelled = false void listMusicSessions().then((s) => { if (!cancelled) setSessions(s) }) fetch('/api/resolve') .then((r) => (r.ok ? r.json() : null)) .then((d) => { if (!cancelled && d && typeof d === 'object' && typeof d.house === 'object') { setHouse(d.house as Partial>) } }) .catch(() => {}) return () => { cancelled = true } }, [reloadTick]) const loggedInIds = Object.keys(sessions?.loggedIn ?? {}) as ServiceId[] const anyLoggedIn = loggedInIds.length > 0 const houseNames = house ? SERVICES.filter((s) => house[s.id]).map((s) => s.name.split(' / ')[0]) : [] function refreshSessions() { setReloadTick((t) => t + 1) } async function logoutAll() { await clearAllMusicSessions() refreshSessions() toast({ title: 'Logged out of all music services', description: 'Every saved login cookie was removed from this browser.', }) } return (

Appearance

Base theme

{THEME_BASES.map((b) => { const selected = activeBase === b.id const chip = chipFor(!b.dark) return ( ) })}

Highlight color

{ACCENTS.map((a) => { const selected = accent === a.id const dot = isLight ? a.lightMain : a.main return ( ) })}
{MUSIC_SERVICES_UI && ( <>

Music services

Log in to your own accounts -- the Link button then opens your playlists and liked music with your account, so you never get stuck behind bot-detection walls. Login cookies are encrypted and stored on this computer only.

{houseNames.length > 0 && (

Built-in account active -- {houseNames.join(', ')} links play for everyone, no login needed. Log in only to reach your own playlists, liked music and uploads.

)}
{SERVICES.map((s) => { const active = Boolean(sessions?.loggedIn[s.id]) return ( ) })}
{sessions?.unavailable && (

This browser context is not secure (https), so login cookies cannot be stored here.

)}
{anyLoggedIn && ( )}
)}
{MUSIC_SERVICES_UI && ( )}
) }