Files
smart-home-tablet/components/SpotifyWidget.tsx
Cosmo feafde37dc
Some checks failed
Deploy / deploy (push) Has been cancelled
feat: Spotify widget on home tab
2026-05-01 11:18:51 +00:00

125 lines
4.4 KiB
TypeScript
Raw Permalink 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 { useState, useEffect, useCallback } from 'react'
import { SkipBack, SkipForward, Play, Pause, Music } from 'lucide-react'
interface SpotifyState {
playing: boolean
track?: string
artist?: string
album?: string
volume?: number
device?: string
}
export default function SpotifyWidget() {
const [state, setState] = useState<SpotifyState | null>(null)
const [loading, setLoading] = useState(true)
const [actionLoading, setActionLoading] = useState(false)
const fetchState = useCallback(async () => {
try {
const r = await fetch('/api/voice/tools/spotify', {
headers: {
'x-voice-internal': process.env.NEXT_PUBLIC_VOICE_API_KEY || '',
},
})
if (r.ok) {
const d = await r.json()
setState(d)
}
} catch {}
finally { setLoading(false) }
}, [])
useEffect(() => {
fetchState()
const t = setInterval(fetchState, 15_000)
return () => clearInterval(t)
}, [fetchState])
const control = async (action: string) => {
setActionLoading(true)
try {
await fetch('/api/voice/tools/spotify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-voice-internal': process.env.NEXT_PUBLIC_VOICE_API_KEY || '',
},
body: JSON.stringify({ action }),
})
setTimeout(fetchState, 500)
} catch {}
finally { setActionLoading(false) }
}
const btnStyle = (active?: boolean): React.CSSProperties => ({
width: 40, height: 40, borderRadius: 12,
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: active ? 'rgba(30,215,96,0.15)' : 'rgba(255,255,255,0.04)',
border: `1px solid ${active ? 'rgba(30,215,96,0.3)' : 'rgba(255,255,255,0.07)'}`,
color: active ? '#1ED760' : 'var(--text-secondary)',
transition: 'all 0.2s ease',
cursor: actionLoading ? 'not-allowed' : 'pointer',
opacity: actionLoading ? 0.6 : 1,
})
return (
<div className="card" style={{ padding: '16px 20px', display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{
width: 28, height: 28, borderRadius: 8,
background: 'rgba(30,215,96,0.12)',
border: '1px solid rgba(30,215,96,0.2)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<Music size={14} color="#1ED760" />
</div>
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--text-secondary)' }}>Spotify</span>
{state?.device && (
<span style={{ fontSize: 10, color: 'var(--text-tertiary)', marginLeft: 'auto' }}>
{state.device}
</span>
)}
</div>
{/* Track info */}
{loading ? (
<div style={{ fontSize: 13, color: 'var(--text-tertiary)', textAlign: 'center', padding: '8px 0' }}>Загрузка...</div>
) : !state?.track ? (
<div style={{ fontSize: 13, color: 'var(--text-tertiary)', textAlign: 'center', padding: '8px 0' }}>Ничего не играет</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<div style={{
fontSize: 14, fontWeight: 700, color: 'var(--text-primary)',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{state.track}</div>
<div style={{
fontSize: 12, color: 'var(--text-secondary)',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{state.artist}</div>
</div>
)}
{/* Controls */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10 }}>
<button style={btnStyle()} onClick={() => control('previous')} disabled={actionLoading}>
<SkipBack size={16} />
</button>
<button
style={{ ...btnStyle(state?.playing), width: 48, height: 48, borderRadius: 14 }}
onClick={() => control(state?.playing ? 'pause' : 'play')}
disabled={actionLoading}
>
{state?.playing ? <Pause size={18} /> : <Play size={18} />}
</button>
<button style={btnStyle()} onClick={() => control('next')} disabled={actionLoading}>
<SkipForward size={16} />
</button>
</div>
</div>
)
}