Files
party-mix-app/apps/web/src/store/overlayStore.ts
Kirko 428548a620 feat: nginx reverse proxy, Spotify import, overlay system, UI overhaul
- Add nginx as single entry point: /api/* → backend, /* → web
- NEXT_PUBLIC_API_URL="" so all API calls are relative (go through nginx)
- Add Spotify playlist import (Client Credentials OAuth, up to 500 tracks)
- Add Yandex/Spotify tabbed import UI on /playlists
- Add stream overlay system (SSE + polling fallback, 9 styles)
- Reorganize pages into (main) route group
- Add QueuePanel, VersionsPanel, Toaster components
- Add overlay settings tab in /settings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 00:45:53 +03:00

216 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { create } from 'zustand'
const KEY_ENABLED = 'pm_overlay_enabled'
const KEY_DESIGN = 'pm_overlay_design'
const KEY_STYLE = 'pm_overlay_style'
const KEY_ACCENT = 'pm_overlay_accent'
const KEY_POSITION = 'pm_overlay_position'
const KEY_FONT = 'pm_overlay_font'
const KEY_TEXTCOLOR = 'pm_overlay_textcolor'
const KEY_SHOWCVR = 'pm_overlay_showcvr'
const KEY_SHOWEQ = 'pm_overlay_showeq'
const KEY_PALETTE = 'pm_overlay_palette'
const KEY_CUSTPALETTES = 'pm_overlay_custpalettes'
const KEY_MARGIN = 'pm_overlay_margin'
const KEY_SCALE = 'pm_overlay_scale'
const KEY_OPACITY = 'pm_overlay_opacity'
export type OverlayDesign = 'minimal' | 'card' | 'bar'
export type OverlayStyle =
| 'classic' | 'aero' | 'retro' | 'neon' | 'clean'
| 'y2k' | 'lofi' | 'glam' | 'matrix'
export type OverlayPosition = 'bl' | 'br' | 'tl' | 'tr'
export interface StyleConfig {
bg: string
blur: string
border: string
shadow: string
radius: string
text: string
text2: string
eqColor: string
fontStyle: 'normal' | 'italic'
fontWeight: number
letterSpacing: string
}
export const OVERLAY_STYLES: Record<OverlayStyle, { name: string; desc: string; cfg: StyleConfig }> = {
classic: {
name: 'Классика', desc: 'Тёмное стекло, нейтральный',
cfg: {
bg: 'rgba(10,10,15,0.82)', blur: '20px',
border: '1px solid rgba(255,255,255,0.08)',
shadow: '0 8px 32px rgba(0,0,0,0.5)',
radius: '16px', text: '#ffffff', text2: 'rgba(255,255,255,0.45)',
eqColor: 'var(--accent)', fontStyle: 'normal', fontWeight: 600, letterSpacing: '0',
},
},
aero: {
name: 'Фрутигер Аеро', desc: 'Глянцевый XP-стиль',
cfg: {
bg: 'linear-gradient(145deg,rgba(200,235,255,0.55) 0%,rgba(140,195,255,0.4) 100%)',
blur: '24px',
border: '1.5px solid rgba(255,255,255,0.75)',
shadow: '0 4px 20px rgba(80,160,255,0.35),inset 0 1px 0 rgba(255,255,255,0.6)',
radius: '22px', text: '#003a6e', text2: 'rgba(0,60,120,0.6)',
eqColor: '#0077cc', fontStyle: 'normal', fontWeight: 700, letterSpacing: '-0.02em',
},
},
retro: {
name: 'Ретро', desc: 'VHS / кассетная эстетика',
cfg: {
bg: 'rgba(18,8,2,0.93)',
blur: '0px',
border: '2px solid #b06828',
shadow: '0 0 24px rgba(180,100,20,0.45),4px 4px 0 rgba(0,0,0,0.5)',
radius: '4px', text: '#f8d090', text2: '#9a6020',
eqColor: '#e08030', fontStyle: 'normal', fontWeight: 700, letterSpacing: '0.05em',
},
},
neon: {
name: 'Неон', desc: 'Киберпанк, светящиеся линии',
cfg: {
bg: 'rgba(0,0,10,0.9)',
blur: '8px',
border: '1px solid rgba(var(--accent-rgb),0.55)',
shadow: '0 0 24px rgba(var(--accent-rgb),0.35),inset 0 0 16px rgba(var(--accent-rgb),0.06)',
radius: '8px', text: 'var(--accent)', text2: 'rgba(var(--accent-rgb),0.55)',
eqColor: 'var(--accent)', fontStyle: 'normal', fontWeight: 800, letterSpacing: '0.04em',
},
},
clean: {
name: 'Минимализм', desc: 'Только текст, без фона',
cfg: {
bg: 'transparent',
blur: '0px',
border: 'none',
shadow: 'none',
radius: '0', text: '#ffffff', text2: 'rgba(255,255,255,0.5)',
eqColor: 'var(--accent)', fontStyle: 'normal', fontWeight: 600, letterSpacing: '0',
},
},
y2k: {
name: 'Y2K', desc: 'Хром, ранние 2000-е',
cfg: {
bg: 'linear-gradient(160deg,#d8d8e8 0%,#b0b8d0 50%,#c8cce0 100%)',
blur: '4px',
border: '2px solid rgba(255,255,255,0.9)',
shadow: '3px 3px 0 rgba(0,0,80,0.25),inset 0 1px 0 rgba(255,255,255,0.8)',
radius: '6px', text: '#000060', text2: '#444488',
eqColor: '#0044cc', fontStyle: 'normal', fontWeight: 700, letterSpacing: '-0.01em',
},
},
lofi: {
name: 'Ло-фай', desc: 'Тёплые тона, уютная атмосфера',
cfg: {
bg: 'rgba(38,24,12,0.9)',
blur: '14px',
border: '1px solid rgba(240,180,100,0.18)',
shadow: '0 6px 28px rgba(0,0,0,0.45)',
radius: '14px', text: '#fde8c0', text2: 'rgba(240,180,100,0.55)',
eqColor: '#e8a060', fontStyle: 'italic', fontWeight: 500, letterSpacing: '0.01em',
},
},
glam: {
name: 'Гламур', desc: 'Золото, роскошь',
cfg: {
bg: 'linear-gradient(145deg,rgba(28,18,4,0.93) 0%,rgba(18,12,2,0.93) 100%)',
blur: '16px',
border: '1px solid rgba(212,175,55,0.45)',
shadow: '0 8px 32px rgba(0,0,0,0.65),0 0 0 1px rgba(212,175,55,0.2)',
radius: '14px', text: '#f0c840', text2: 'rgba(200,150,30,0.65)',
eqColor: '#d4af37', fontStyle: 'normal', fontWeight: 700, letterSpacing: '0.02em',
},
},
matrix: {
name: 'Матрица', desc: 'Зелёный терминал, хакер',
cfg: {
bg: 'rgba(0,8,0,0.92)',
blur: '4px',
border: '1px solid rgba(0,255,50,0.3)',
shadow: '0 0 20px rgba(0,255,50,0.18)',
radius: '4px', text: '#00ff32', text2: 'rgba(0,200,30,0.55)',
eqColor: '#00ff32', fontStyle: 'normal', fontWeight: 700, letterSpacing: '0.08em',
},
},
}
interface OverlayStore {
enabled: boolean
design: OverlayDesign
style: OverlayStyle
accentColor: string
position: OverlayPosition
font: string
textColor: string
showCover: boolean
showEq: boolean
palette: string
customPalettes: Record<string, Record<string, string>>
margin: number
scale: number
opacity: number
setEnabled: (v: boolean) => void
setDesign: (d: OverlayDesign) => void
setStyle: (s: OverlayStyle) => void
setAccentColor: (c: string) => void
setPosition: (p: OverlayPosition) => void
setFont: (f: string) => void
setTextColor: (c: string) => void
setShowCover: (v: boolean) => void
setShowEq: (v: boolean) => void
setPalette: (p: string) => void
setCustomPaletteField: (style: string, field: string, value: string) => void
setMargin: (v: number) => void
setScale: (v: number) => void
setOpacity: (v: number) => void
}
const ls = (key: string) => (typeof window !== 'undefined' ? localStorage.getItem(key) : null)
const lsSet = (key: string, v: string) => { if (typeof window !== 'undefined') localStorage.setItem(key, v) }
export const useOverlayStore = create<OverlayStore>((set) => ({
enabled: ls(KEY_ENABLED) !== 'false',
design: (ls(KEY_DESIGN) ?? 'minimal') as OverlayDesign,
style: (ls(KEY_STYLE) ?? 'classic') as OverlayStyle,
accentColor: ls(KEY_ACCENT) ?? '#de9cfe',
position: (ls(KEY_POSITION) ?? 'bl') as OverlayPosition,
font: ls(KEY_FONT) ?? '',
textColor: ls(KEY_TEXTCOLOR) ?? '',
showCover: ls(KEY_SHOWCVR) !== 'false',
showEq: ls(KEY_SHOWEQ) !== 'false',
palette: ls(KEY_PALETTE) ?? 'default',
customPalettes: (() => { try { return JSON.parse(ls(KEY_CUSTPALETTES) ?? '{}') } catch { return {} } })(),
margin: Number(ls(KEY_MARGIN) ?? 24),
scale: Number(ls(KEY_SCALE) ?? 1),
opacity: Number(ls(KEY_OPACITY) ?? 1),
setEnabled: (v) => { lsSet(KEY_ENABLED, String(v)); set({ enabled: v }) },
setDesign: (d) => { lsSet(KEY_DESIGN, d); set({ design: d }) },
setStyle: (s) => { lsSet(KEY_STYLE, s); set({ style: s }) },
setAccentColor: (c) => { lsSet(KEY_ACCENT, c); set({ accentColor: c }) },
setPosition: (p) => { lsSet(KEY_POSITION, p); set({ position: p }) },
setFont: (f) => { lsSet(KEY_FONT, f); set({ font: f }) },
setTextColor: (c) => { lsSet(KEY_TEXTCOLOR, c); set({ textColor: c }) },
setShowCover: (v) => { lsSet(KEY_SHOWCVR, String(v)); set({ showCover: v }) },
setShowEq: (v) => { lsSet(KEY_SHOWEQ, String(v)); set({ showEq: v }) },
setPalette: (p) => { lsSet(KEY_PALETTE, p); set({ palette: p }) },
setCustomPaletteField: (style, field, value) => {
set((state) => {
const next = {
...state.customPalettes,
[style]: { ...(state.customPalettes[style] ?? {}), [field]: value },
}
lsSet(KEY_CUSTPALETTES, JSON.stringify(next))
return { customPalettes: next }
})
},
setMargin: (v) => { lsSet(KEY_MARGIN, String(v)); set({ margin: v }) },
setScale: (v) => { lsSet(KEY_SCALE, String(v)); set({ scale: v }) },
setOpacity: (v) => { lsSet(KEY_OPACITY, String(v)); set({ opacity: v }) },
}))