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>
This commit is contained in:
368
apps/web/src/lib/overlayPalettes.ts
Normal file
368
apps/web/src/lib/overlayPalettes.ts
Normal file
@@ -0,0 +1,368 @@
|
||||
import type { OverlayStyle } from '@/store/overlayStore'
|
||||
|
||||
export interface OverlayPalette {
|
||||
id: string
|
||||
name: string
|
||||
swatches: string[] // preview dots (2-3 hex/rgba)
|
||||
bg?: string // panel background (color or gradient)
|
||||
border?: string // full CSS border declaration
|
||||
shadow?: string // box-shadow
|
||||
text?: string // primary text color
|
||||
text2?: string // secondary text color
|
||||
chroma?: string // characteristic color: retro border hex, neon/matrix phosphor hex, glam metal rgb-string
|
||||
chroma2?: string // for RGB string "r,g,b" when chroma is hex (used for --accent-rgb override)
|
||||
titleBg?: string // Y2K titlebar gradient
|
||||
bodyBg?: string // Y2K body gradient
|
||||
}
|
||||
|
||||
const PALETTES: Record<OverlayStyle, OverlayPalette[]> = {
|
||||
classic: [
|
||||
{
|
||||
id: 'default', name: 'Тёмный', swatches: ['#0a0a10', '#ffffff', '#888888'],
|
||||
bg: 'rgba(10,10,16,0.82)', border: '1px solid rgba(255,255,255,0.09)',
|
||||
shadow: '0 8px 32px rgba(0,0,0,0.55)',
|
||||
text: '#ffffff', text2: 'rgba(255,255,255,0.45)', chroma: 'rgba(255,255,255,0.06)',
|
||||
},
|
||||
{
|
||||
id: 'warm', name: 'Тёплый', swatches: ['#160c06', '#ffeedd', '#dca850'],
|
||||
bg: 'rgba(22,12,6,0.86)', border: '1px solid rgba(220,168,80,0.20)',
|
||||
shadow: '0 8px 32px rgba(20,8,0,0.6)',
|
||||
text: '#ffeedd', text2: 'rgba(255,200,120,0.50)', chroma: 'rgba(220,168,80,0.10)',
|
||||
},
|
||||
{
|
||||
id: 'ocean', name: 'Морской', swatches: ['#040a1a', '#ddeeff', '#3c82ff'],
|
||||
bg: 'rgba(4,10,26,0.88)', border: '1px solid rgba(60,130,255,0.22)',
|
||||
shadow: '0 8px 32px rgba(0,10,40,0.6)',
|
||||
text: '#ddeeff', text2: 'rgba(100,170,255,0.50)', chroma: 'rgba(60,130,255,0.08)',
|
||||
},
|
||||
{
|
||||
id: 'frost', name: 'Белый', swatches: ['#ffffff', '#111111', '#bbbbbb'],
|
||||
bg: 'rgba(255,255,255,0.90)', border: '1px solid rgba(0,0,0,0.09)',
|
||||
shadow: '0 8px 32px rgba(0,0,0,0.15)',
|
||||
text: '#111111', text2: 'rgba(0,0,0,0.42)', chroma: 'rgba(0,0,0,0.05)',
|
||||
},
|
||||
],
|
||||
|
||||
aero: [
|
||||
{
|
||||
id: 'default', name: 'Небо', swatches: ['#bee6ff', '#78b9ff', '#003a6e'],
|
||||
bg: 'linear-gradient(160deg,rgba(190,230,255,0.55) 0%,rgba(120,185,255,0.42) 100%)',
|
||||
border: '1.5px solid rgba(255,255,255,0.72)',
|
||||
shadow: '0 4px 20px rgba(80,160,255,0.30),inset 0 1.5px 0 rgba(255,255,255,0.65)',
|
||||
text: '#003a6e', text2: 'rgba(0,60,130,0.60)',
|
||||
},
|
||||
{
|
||||
id: 'rose', name: 'Роза', swatches: ['#ffc8d7', '#ff87af', '#6e0030'],
|
||||
bg: 'linear-gradient(160deg,rgba(255,200,215,0.55) 0%,rgba(255,135,175,0.42) 100%)',
|
||||
border: '1.5px solid rgba(255,255,255,0.72)',
|
||||
shadow: '0 4px 20px rgba(255,100,160,0.30),inset 0 1.5px 0 rgba(255,255,255,0.65)',
|
||||
text: '#6e0030', text2: 'rgba(130,0,60,0.60)',
|
||||
},
|
||||
{
|
||||
id: 'mint', name: 'Мята', swatches: ['#b4ffd2', '#50dc9b', '#003a20'],
|
||||
bg: 'linear-gradient(160deg,rgba(180,255,210,0.55) 0%,rgba(80,220,155,0.42) 100%)',
|
||||
border: '1.5px solid rgba(255,255,255,0.72)',
|
||||
shadow: '0 4px 20px rgba(60,200,130,0.30),inset 0 1.5px 0 rgba(255,255,255,0.65)',
|
||||
text: '#003a20', text2: 'rgba(0,80,50,0.60)',
|
||||
},
|
||||
{
|
||||
id: 'lavender', name: 'Лаванда', swatches: ['#dcc8ff', '#9b7dff', '#3a006e'],
|
||||
bg: 'linear-gradient(160deg,rgba(220,200,255,0.55) 0%,rgba(155,125,255,0.42) 100%)',
|
||||
border: '1.5px solid rgba(255,255,255,0.72)',
|
||||
shadow: '0 4px 20px rgba(130,80,255,0.30),inset 0 1.5px 0 rgba(255,255,255,0.65)',
|
||||
text: '#3a006e', text2: 'rgba(70,0,140,0.55)',
|
||||
},
|
||||
],
|
||||
|
||||
retro: [
|
||||
{
|
||||
id: 'default', name: 'Янтарь', swatches: ['#0c0500', '#c07030', '#f8d090'],
|
||||
bg: 'rgba(12,5,0,0.94)', border: '2px solid #c07030',
|
||||
shadow: '0 0 28px rgba(180,90,20,0.45),4px 4px 0 rgba(0,0,0,0.6)',
|
||||
text: '#f8d090', text2: '#9a6020', chroma: 'rgba(180,100,20,0.30)',
|
||||
},
|
||||
{
|
||||
id: 'phosphor', name: 'Фосфор', swatches: ['#000c04', '#30c060', '#90f8b0'],
|
||||
bg: 'rgba(0,12,4,0.94)', border: '2px solid #30c060',
|
||||
shadow: '0 0 28px rgba(20,160,60,0.45),4px 4px 0 rgba(0,0,0,0.6)',
|
||||
text: '#90f8b0', text2: '#207840', chroma: 'rgba(20,160,60,0.28)',
|
||||
},
|
||||
{
|
||||
id: 'crt', name: 'CRT синий', swatches: ['#000412', '#3060c0', '#90b8f8'],
|
||||
bg: 'rgba(0,4,18,0.94)', border: '2px solid #3060c0',
|
||||
shadow: '0 0 28px rgba(30,80,200,0.45),4px 4px 0 rgba(0,0,0,0.6)',
|
||||
text: '#90b8f8', text2: '#204880', chroma: 'rgba(30,80,200,0.30)',
|
||||
},
|
||||
{
|
||||
id: 'blood', name: 'Красный', swatches: ['#0e0202', '#c02828', '#f89090'],
|
||||
bg: 'rgba(14,2,2,0.94)', border: '2px solid #c02828',
|
||||
shadow: '0 0 28px rgba(180,28,20,0.45),4px 4px 0 rgba(0,0,0,0.6)',
|
||||
text: '#f89090', text2: '#802020', chroma: 'rgba(180,28,20,0.28)',
|
||||
},
|
||||
],
|
||||
|
||||
neon: [
|
||||
{
|
||||
id: 'default', name: 'Акцент', swatches: ['#00000a', 'var(--accent)', 'var(--accent)'],
|
||||
bg: 'rgba(0,0,10,0.90)',
|
||||
},
|
||||
{
|
||||
id: 'cyan', name: 'Голубой', swatches: ['#00040c', '#00e5ff', '#00e5ff'],
|
||||
bg: 'rgba(0,4,12,0.92)', chroma: '#00e5ff', chroma2: '0,229,255',
|
||||
},
|
||||
{
|
||||
id: 'magenta', name: 'Пурпур', swatches: ['#0a0008', '#ff00cc', '#ff00cc'],
|
||||
bg: 'rgba(10,0,8,0.92)', chroma: '#ff00cc', chroma2: '255,0,204',
|
||||
},
|
||||
{
|
||||
id: 'lime', name: 'Лайм', swatches: ['#000a04', '#00ff88', '#00ff88'],
|
||||
bg: 'rgba(0,10,4,0.92)', chroma: '#00ff88', chroma2: '0,255,136',
|
||||
},
|
||||
],
|
||||
|
||||
clean: [
|
||||
{
|
||||
id: 'default', name: 'Стандарт', swatches: ['transparent', '#ffffff', 'var(--accent)'],
|
||||
},
|
||||
{
|
||||
id: 'shadow', name: 'С тенью', swatches: ['transparent', '#ffffff', '#666666'],
|
||||
text: '#ffffff',
|
||||
},
|
||||
{
|
||||
id: 'dark-text', name: 'Тёмный', swatches: ['transparent', '#111111', '#333333'],
|
||||
text: '#111111', text2: 'rgba(0,0,0,0.50)',
|
||||
},
|
||||
{
|
||||
id: 'warm-text', name: 'Тёплый', swatches: ['transparent', '#ffeedd', '#ddaa66'],
|
||||
text: '#ffeedd', text2: 'rgba(255,200,120,0.60)',
|
||||
},
|
||||
],
|
||||
|
||||
y2k: [
|
||||
{
|
||||
id: 'default', name: 'Лунный', swatches: ['#c8d0e0', '#1a4090', '#000060'],
|
||||
titleBg: 'linear-gradient(90deg,#1a4090 0%,#4a80d0 40%,#1a4090 100%)',
|
||||
bodyBg: 'linear-gradient(160deg,#dce4f0,#c0c8e0)',
|
||||
text: '#000060', text2: '#404880',
|
||||
},
|
||||
{
|
||||
id: 'rose', name: 'Розовый', swatches: ['#f0d4dc', '#901440', '#600020'],
|
||||
titleBg: 'linear-gradient(90deg,#901440 0%,#d04080 40%,#901440 100%)',
|
||||
bodyBg: 'linear-gradient(160deg,#f0d4dc,#d0b0c0)',
|
||||
text: '#600020', text2: '#804060',
|
||||
},
|
||||
{
|
||||
id: 'dark', name: 'Тёмный', swatches: ['#c0c0c8', '#484848', '#000020'],
|
||||
titleBg: 'linear-gradient(90deg,#282828 0%,#484848 40%,#282828 100%)',
|
||||
bodyBg: 'linear-gradient(160deg,#c0c0c8,#a0a0b0)',
|
||||
text: '#000020', text2: '#303040',
|
||||
},
|
||||
{
|
||||
id: 'forest', name: 'Лесной', swatches: ['#d4ecd8', '#106020', '#004010'],
|
||||
titleBg: 'linear-gradient(90deg,#106020 0%,#308040 40%,#106020 100%)',
|
||||
bodyBg: 'linear-gradient(160deg,#d4ecd8,#b8d8c0)',
|
||||
text: '#004010', text2: '#306040',
|
||||
},
|
||||
],
|
||||
|
||||
lofi: [
|
||||
{
|
||||
id: 'default', name: 'Коричневый', swatches: ['#2a1a0e', '#fde8c0', '#e8a060'],
|
||||
bg: 'rgba(42,26,14,0.92)',
|
||||
text: '#fde8c0', text2: 'rgba(var(--accent-rgb),0.60)',
|
||||
},
|
||||
{
|
||||
id: 'night', name: 'Ночной', swatches: ['#0c1020', '#c0d4f8', '#7090d8'],
|
||||
bg: 'rgba(12,16,32,0.92)',
|
||||
text: '#c0d4f8', text2: 'rgba(var(--accent-rgb),0.60)',
|
||||
},
|
||||
{
|
||||
id: 'forest', name: 'Лесной', swatches: ['#0c1c10', '#c8f0c8', '#60c870'],
|
||||
bg: 'rgba(12,28,16,0.92)',
|
||||
text: '#c8f0c8', text2: 'rgba(var(--accent-rgb),0.60)',
|
||||
},
|
||||
{
|
||||
id: 'plum', name: 'Сливовый', swatches: ['#1c0c20', '#e8c0f8', '#c060d8'],
|
||||
bg: 'rgba(28,12,32,0.92)',
|
||||
text: '#e8c0f8', text2: 'rgba(var(--accent-rgb),0.60)',
|
||||
},
|
||||
],
|
||||
|
||||
glam: [
|
||||
{
|
||||
id: 'default', name: 'Золото', swatches: ['#1c1204', '#d4af37', '#f0c840'],
|
||||
bg: 'linear-gradient(145deg,rgba(28,18,4,0.95),rgba(16,10,2,0.95))',
|
||||
chroma: '212,175,55', text: '#f0c840', text2: 'rgba(200,150,30,0.65)',
|
||||
},
|
||||
{
|
||||
id: 'silver', name: 'Серебро', swatches: ['#101214', '#c0c8d2', '#e0e8f0'],
|
||||
bg: 'linear-gradient(145deg,rgba(16,18,20,0.96),rgba(10,12,14,0.96))',
|
||||
chroma: '192,200,210', text: '#e0e8f0', text2: 'rgba(160,170,180,0.65)',
|
||||
},
|
||||
{
|
||||
id: 'rose-gold', name: 'Розовое золото', swatches: ['#160e0e', '#dc9e8e', '#f0c8b8'],
|
||||
bg: 'linear-gradient(145deg,rgba(22,14,14,0.95),rgba(14,8,8,0.95))',
|
||||
chroma: '220,158,142', text: '#f0c8b8', text2: 'rgba(200,140,120,0.65)',
|
||||
},
|
||||
{
|
||||
id: 'emerald', name: 'Изумруд', swatches: ['#08140e', '#32b478', '#80f8c0'],
|
||||
bg: 'linear-gradient(145deg,rgba(8,20,14,0.95),rgba(4,12,8,0.95))',
|
||||
chroma: '50,180,120', text: '#80f8c0', text2: 'rgba(40,160,100,0.65)',
|
||||
},
|
||||
],
|
||||
|
||||
matrix: [
|
||||
{
|
||||
id: 'default', name: 'Зелёный', swatches: ['#000800', '#00ff32', '#00ff32'],
|
||||
bg: 'rgba(0,8,0,0.93)', chroma: '#00ff32', chroma2: '0,255,50',
|
||||
border: '1px solid rgba(0,255,50,0.35)',
|
||||
},
|
||||
{
|
||||
id: 'cyan', name: 'Голубой', swatches: ['#00040c', '#00e5ff', '#00e5ff'],
|
||||
bg: 'rgba(0,4,12,0.93)', chroma: '#00e5ff', chroma2: '0,229,255',
|
||||
border: '1px solid rgba(0,229,255,0.35)',
|
||||
},
|
||||
{
|
||||
id: 'amber', name: 'Янтарь', swatches: ['#0c0800', '#ffb400', '#ffb400'],
|
||||
bg: 'rgba(12,8,0,0.93)', chroma: '#ffb400', chroma2: '255,180,0',
|
||||
border: '1px solid rgba(255,180,0,0.35)',
|
||||
},
|
||||
{
|
||||
id: 'violet', name: 'Пурпур', swatches: ['#08000c', '#cc00ff', '#cc00ff'],
|
||||
bg: 'rgba(8,0,12,0.93)', chroma: '#cc00ff', chroma2: '204,0,255',
|
||||
border: '1px solid rgba(204,0,255,0.35)',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export function getPalettes(style: OverlayStyle): OverlayPalette[] {
|
||||
return PALETTES[style] ?? PALETTES.classic
|
||||
}
|
||||
|
||||
export function getPalette(style: OverlayStyle, paletteId: string): OverlayPalette {
|
||||
const list = getPalettes(style)
|
||||
return list.find(p => p.id === paletteId) ?? list[0]
|
||||
}
|
||||
|
||||
// ── Custom palette ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ColorFieldDef {
|
||||
key: string
|
||||
label: string
|
||||
default: string // hex default for the color picker
|
||||
}
|
||||
|
||||
export const STYLE_CUSTOM_FIELDS: Record<OverlayStyle, ColorFieldDef[]> = {
|
||||
classic: [
|
||||
{ key: 'bg', label: 'Фон', default: '#0a0a10' },
|
||||
{ key: 'chroma', label: 'Рамка', default: '#ffffff' },
|
||||
{ key: 'text', label: 'Текст', default: '#ffffff' },
|
||||
{ key: 'text2', label: 'Текст 2', default: '#888888' },
|
||||
],
|
||||
aero: [
|
||||
{ key: 'bg', label: 'Цвет', default: '#bee6ff' },
|
||||
{ key: 'text', label: 'Текст', default: '#003a6e' },
|
||||
{ key: 'text2', label: 'Текст 2', default: '#004488' },
|
||||
],
|
||||
retro: [
|
||||
{ key: 'bg', label: 'Фон', default: '#0c0500' },
|
||||
{ key: 'chroma', label: 'Хром', default: '#c07030' },
|
||||
{ key: 'text', label: 'Текст', default: '#f8d090' },
|
||||
{ key: 'text2', label: 'Текст 2', default: '#9a6020' },
|
||||
],
|
||||
neon: [
|
||||
{ key: 'bg', label: 'Фон', default: '#00000a' },
|
||||
{ key: 'chroma', label: 'Свечение', default: '#de9cfe' },
|
||||
],
|
||||
clean: [
|
||||
{ key: 'text', label: 'Текст', default: '#ffffff' },
|
||||
{ key: 'text2', label: 'Текст 2', default: '#888888' },
|
||||
],
|
||||
y2k: [
|
||||
{ key: 'titleBg', label: 'Заголовок', default: '#1a4090' },
|
||||
{ key: 'bodyBg', label: 'Тело', default: '#c0c8e0' },
|
||||
{ key: 'text', label: 'Текст', default: '#000060' },
|
||||
{ key: 'text2', label: 'Текст 2', default: '#404880' },
|
||||
],
|
||||
lofi: [
|
||||
{ key: 'bg', label: 'Фон', default: '#2a1a0e' },
|
||||
{ key: 'text', label: 'Текст', default: '#fde8c0' },
|
||||
{ key: 'text2', label: 'Текст 2', default: '#e8a060' },
|
||||
],
|
||||
glam: [
|
||||
{ key: 'bg', label: 'Фон', default: '#1c1204' },
|
||||
{ key: 'chroma', label: 'Металл', default: '#d4af37' },
|
||||
{ key: 'text', label: 'Текст', default: '#f0c840' },
|
||||
],
|
||||
matrix: [
|
||||
{ key: 'bg', label: 'Фон', default: '#000800' },
|
||||
{ key: 'chroma', label: 'Фосфор', default: '#00ff32' },
|
||||
],
|
||||
}
|
||||
|
||||
function hexToRgba(hex: string, a: number): string {
|
||||
if (!/^#[0-9a-fA-F]{6}$/.test(hex)) return hex
|
||||
const r = parseInt(hex.slice(1, 3), 16)
|
||||
const g = parseInt(hex.slice(3, 5), 16)
|
||||
const b = parseInt(hex.slice(5, 7), 16)
|
||||
return `rgba(${r},${g},${b},${a})`
|
||||
}
|
||||
|
||||
function hexToRgb(hex: string): string {
|
||||
if (!/^#[0-9a-fA-F]{6}$/.test(hex)) return '128,128,128'
|
||||
const r = parseInt(hex.slice(1, 3), 16)
|
||||
const g = parseInt(hex.slice(3, 5), 16)
|
||||
const b = parseInt(hex.slice(5, 7), 16)
|
||||
return `${r},${g},${b}`
|
||||
}
|
||||
|
||||
export function buildCustomPalette(
|
||||
style: OverlayStyle,
|
||||
data: Record<string, string>,
|
||||
): OverlayPalette {
|
||||
const pal: OverlayPalette = { id: 'custom', name: 'Свой', swatches: [] }
|
||||
|
||||
if (data.text) pal.text = data.text
|
||||
if (data.text2) pal.text2 = data.text2
|
||||
|
||||
if (data.bg) {
|
||||
if (style === 'aero') {
|
||||
pal.bg = `linear-gradient(160deg,${hexToRgba(data.bg, 0.55)} 0%,${hexToRgba(data.bg, 0.38)} 100%)`
|
||||
} else {
|
||||
const alpha = style === 'classic' ? 0.85 : 0.92
|
||||
pal.bg = hexToRgba(data.bg, alpha)
|
||||
}
|
||||
}
|
||||
|
||||
if (data.chroma) {
|
||||
switch (style) {
|
||||
case 'glam':
|
||||
pal.chroma = hexToRgb(data.chroma)
|
||||
break
|
||||
case 'neon':
|
||||
case 'matrix':
|
||||
pal.chroma = data.chroma
|
||||
pal.chroma2 = hexToRgb(data.chroma)
|
||||
if (style === 'matrix') {
|
||||
pal.border = `1px solid ${hexToRgba(data.chroma, 0.38)}`
|
||||
}
|
||||
break
|
||||
case 'retro':
|
||||
pal.chroma = hexToRgba(data.chroma, 0.30)
|
||||
pal.border = `2px solid ${data.chroma}`
|
||||
pal.shadow = `0 0 28px ${hexToRgba(data.chroma, 0.45)},4px 4px 0 rgba(0,0,0,0.6)`
|
||||
break
|
||||
case 'classic':
|
||||
pal.border = `1px solid ${hexToRgba(data.chroma, 0.28)}`
|
||||
pal.chroma = hexToRgba(data.chroma, 0.10)
|
||||
break
|
||||
default:
|
||||
pal.chroma = data.chroma
|
||||
}
|
||||
}
|
||||
|
||||
if (data.titleBg) pal.titleBg = data.titleBg
|
||||
if (data.bodyBg) pal.bodyBg = data.bodyBg
|
||||
|
||||
return pal
|
||||
}
|
||||
Reference in New Issue
Block a user