aidxnCC/components/widgets/NowPlaying.tsx

293 lines
No EOL
13 KiB
TypeScript

"use client"
import type React from "react"
import { useEffect, useState } from "react"
import { Loader2, AlertCircle } from "lucide-react"
import { PiMusicNotesFill } from "react-icons/pi";
import { FaBluetoothB } from "react-icons/fa6";
import { IoBatteryFullSharp } from "react-icons/io5"
import { IoIosPlay } from "react-icons/io"
import { TbDiscOff } from "react-icons/tb"
import { Progress } from "@/components/ui/progress"
import Link from "@/components/objects/Link"
import ScrollTxt from "@/components/objects/MusicText"
import { connectSocket, disconnectSocket } from "@/lib/socket"
import { effects } from '@/lib/theme/effects'
interface LastFmResponse {
album?: {
image?: Array<{ size: string; '#text': string }>
}
track?: {
album?: {
image?: Array<{ size: string; '#text': string }>
}
}
}
interface NowPlayingData {
track_name?: string
artist_name?: string
release_name?: string
mbid?: string
coverArt?: string | null
lastFmData?: LastFmResponse
status: 'loading' | 'partial' | 'complete' | 'error'
message?: string
}
const NowPlaying: React.FC = () => {
const [nowPlaying, setNowPlaying] = useState<NowPlayingData>({ status: 'loading' })
const [currentTime, setCurrentTime] = useState(new Date())
const [volume, setVolume] = useState(25)
const [screenOn, setScreenOn] = useState(true)
const [progressSteps, setProgressSteps] = useState({ current: 0, total: 3 })
useEffect(() => {
const socket = connectSocket()
socket.on('connect', () => {
console.log('Connected to server')
socket.emit('requestNowPlaying')
socket.emit('startAutoRefresh')
})
socket.on('disconnect', () => {
console.log('[i] Disconnected from server')
})
socket.on('nowPlaying', (data: NowPlayingData) => {
console.log('Received now playing data:', data)
setNowPlaying(prevState => ({
...prevState,
...data
}))
if (data.status === 'loading') {
setProgressSteps({ current: 1, total: 3 })
} else if (data.status === 'partial') {
setProgressSteps({ current: 2, total: 3 })
} else if (data.status === 'complete') {
setProgressSteps({ current: 3, total: 3 })
}
})
socket.on('connect_error', (error) => {
console.error('[!] Connection error:', error)
setNowPlaying({ status: 'error', message: 'Connection failed' })
})
return () => {
socket.off('connect')
socket.off('disconnect')
socket.off('nowPlaying')
socket.off('connect_error')
disconnectSocket()
}
}, [])
useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(new Date())
}, 1000)
return () => clearInterval(timer)
}, [])
const formatTime = (date: Date) => {
return date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
})
}
const renderScreenContent = () => {
if (nowPlaying.status === 'loading') {
return (
<div className="flex flex-col items-center justify-center h-full">
<Loader2 className="animate-spin text-white mb-4" size={32} />
<div className="text-white text-xs text-center px-4">
<div className="mb-2">{nowPlaying.message || 'Connecting...'}</div>
<Progress
value={progressSteps.total > 0 ? (progressSteps.current * 100) / progressSteps.total : 0}
className="h-1"
/>
</div>
</div>
)
}
if (nowPlaying.status === 'error') {
return (
<div className="flex flex-col items-center justify-center h-full">
<AlertCircle className="text-red-500 mb-4" size={32} />
<div className="text-red-500 text-xs text-center px-4">
{nowPlaying.message || 'Error loading data'}
</div>
</div>
)
}
if (!nowPlaying.track_name) {
return (
<div className="flex flex-col items-center justify-center h-full">
<TbDiscOff className="text-gray-400 mb-4" size={32} />
<div className="text-gray-400 text-xs text-center px-4">
Nothing playing
</div>
<div className="text-gray-500 text-xs text-center px-4 mt-2">
Check my <Link href="https://listenbrainz.org/user/p0ntus" target="_blank" rel="noopener noreferrer" className="text-blue-400">ListenBrainz</Link>
</div>
</div>
)
}
// normal state
return (
<>
<a
href={nowPlaying.mbid ? `https://musicbrainz.org/release/${nowPlaying.mbid}` : `https://listenbrainz.org/user/p0ntus`}
target="_blank"
rel="noopener noreferrer"
className="border-b border-gray-700 px-2 py-0 block" style={{background: effects.gradients.musicPlayer}}
>
<div className="text-center leading-none pb-1">
<ScrollTxt text={nowPlaying.artist_name?.toUpperCase() || ''} type="artist" className="-mt-0.5" />
<ScrollTxt text={nowPlaying.track_name || ''} type="track" className="-mt-0.5" />
{nowPlaying.release_name && <ScrollTxt text={nowPlaying.release_name} type="release" className="-mt-1.5" />}
</div>
</a>
{/* Album art */}
<div className="relative w-full aspect-square">
{nowPlaying.status === 'partial' && !nowPlaying.coverArt ? (
<div className="w-full h-full bg-gray-700 flex flex-col items-center justify-center">
<Loader2 className="animate-spin text-gray-400 mb-2" size={32} />
<div className="text-gray-400 text-xs">Fetching Album Art</div>
</div>
) : nowPlaying.coverArt ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={nowPlaying.coverArt}
alt={nowPlaying.track_name || 'Album cover'}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full bg-gray-700 flex items-center justify-center">
<PiMusicNotesFill size={74} className="text-gray-400" />
</div>
)}
</div>
</>
)
}
return (
<div className="flex justify-center items-center">
<div className={`relative w-52 bg-[#D4C29A] rounded-xs border border-[#BFAF8A] z-10 ${nowPlaying.release_name ? "h-[24.1rem]" : "h-[23.6rem]"}`}>
{/* Volume buttons */}
<div className="absolute -left-[2.55px] top-8 rounded-l w-[1.75px] flex flex-col z-0">
<div className="h-8 bg-[#BFAF8A] border-b border-[#A09070] rounded-l cursor-pointer" onClick={() => setVolume(v => Math.min(100, v + 5))}></div> {/* up */}
<div className="h-12 bg-[#A09070] translate-x-[0.65px] -my-[0.85px]"></div> {/* play/pause */}
<div className="h-8 bg-[#BFAF8A] border-t border-[#A09070] rounded-l cursor-pointer" onClick={() => setVolume(v => Math.max(0, v - 5))}></div> {/* down */}
</div>
{/* Top power button */}
<div className="absolute right-1 -top-[3px] w-9 h-[3px] bg-[#BFAF8A] rounded-t-2xl cursor-pointer" onClick={() => setScreenOn(prev => !prev)}></div>
{/* White bezel (fuses screen+home btn) */}
<div className="absolute inset-2 bg-white rounded-sm overflow-hidden flex flex-col">
{/* Virtual screen */}
<div className="mx-2 mt-2 flex-1 bg-black overflow-hidden flex flex-col">
{screenOn && (
<div className="border-b border-gray-700" style={{background: effects.gradients.musicPlayer}}>
<div className="relative flex items-center pr-1 py-0.5">
<FaBluetoothB size={14} className="text-gray-400" />
<div className="absolute left-1/2 transform -translate-x-1/2 text-white text-xs font-medium">{formatTime(currentTime)}</div>
<div className="flex items-center gap-0.5 ml-auto ">
<IoIosPlay size={14} className="text-white" />
<IoBatteryFullSharp size={18} className="text-white" />
</div>
</div>
</div>
)}
{screenOn ? renderScreenContent() : (
<div className="w-full h-full bg-black"></div>
)}
{/* Player controls and seekbar */}
{screenOn && nowPlaying.track_name && (
<div className={`${nowPlaying.release_name ? "pb-3" : "pb-[12.5px]"} flex flex-col items-center`} style={{background: effects.gradients.musicPlayer}}>
<div className="flex justify-center items-center gap-0 px-2">
<button className="hover:drop-shadow-[0_0_8px_rgba(255,255,255,0.9)] hover:filter hover:brightness-110 transition-all duration-200 p-1 rounded-full overflow-hidden">
<svg width="38" height="34" viewBox="0 0 24 20" className="drop-shadow-sm">
<defs>
<linearGradient id="skipBackGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#f9fafb" />
<stop offset="49%" stopColor="#e5e7eb" />
<stop offset="51%" stopColor="#6b7280" />
<stop offset="100%" stopColor="#d1d5db" />
</linearGradient>
</defs>
<rect x="2" y="4" width="2" height="12" fill="url(#skipBackGradient)" />
<polygon points="12,4 6,10 12,16" fill="url(#skipBackGradient)" />
<polygon points="20,4 12,10 20,16" fill="url(#skipBackGradient)" />
</svg>
</button>
<div className="w-[1px] h-6 bg-gray-800 mx-0.5"></div>
<button className="hover:drop-shadow-[0_0_8px_rgba(255,255,255,0.9)] hover:filter hover:brightness-110 transition-all duration-200 p-1 rounded-full overflow-hidden">
<svg width="38" height="38" viewBox="0 0 24 24" className="drop-shadow-sm">
<defs>
<linearGradient id="pauseGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#f9fafb" />
<stop offset="49%" stopColor="#e5e7eb" />
<stop offset="51%" stopColor="#6b7280" />
<stop offset="100%" stopColor="#d1d5db" />
</linearGradient>
</defs>
<rect x="6" y="4" width="4" height="16" fill="url(#pauseGradient)" />
<rect x="14" y="4" width="4" height="16" fill="url(#pauseGradient)" />
</svg>
</button>
<div className="w-[1px] h-6 bg-gray-800 mx-1"></div>
<button className="hover:drop-shadow-[0_0_8px_rgba(255,255,255,0.9)] hover:filter hover:brightness-110 transition-all duration-200 p-1 rounded-full overflow-hidden">
<svg width="38" height="34" viewBox="0 0 24 20" className="drop-shadow-sm">
<defs>
<linearGradient id="skipForwardGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#f9fafb" />
<stop offset="49%" stopColor="#e5e7eb" />
<stop offset="51%" stopColor="#6b7280" />
<stop offset="100%" stopColor="#d1d5db" />
</linearGradient>
</defs>
<polygon points="2,4 9,10 2,16" fill="url(#skipForwardGradient)" />
<polygon points="9,4 17,10 9,16" fill="url(#skipForwardGradient)" />
<rect x="18" y="4" width="2" height="12" fill="url(#skipForwardGradient)" />
</svg>
</button>
</div>
<div className="relative w-full flex justify-center mt-1">
<div className="w-38 h-2 bg-gray-800 rounded-full relative">
<div className="absolute inset-0 bg-gradient-to-b from-white to-gray-600 rounded-full" style={{width: `${volume}%`}} />
<div
className="absolute top-1/2 transform -translate-y-1/2 w-3.5 h-3.5 bg-gradient-to-b from-gray-200 via-gray-300 to-gray-500 rounded-full border border-gray-400 shadow-inner" style={{
left: `calc(${volume}% - 8px)`,
backgroundImage: 'radial-gradient(circle at 30% 30%, #f0f0f0 0%, #c0c0c0 60%, #808080 100%), repeating-conic-gradient(#f9fafb 0deg 45deg, #9ca3af 45deg 90deg)',
backgroundBlendMode: 'overlay',
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.3), 0 1px 2px rgba(255,255,255,0.5)'
}}></div>
<input type="range" min="0" max="100" value={volume} onChange={(e) => setVolume(Number(e.target.value))} className="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" />
</div>
</div>
</div>
)}
</div>
{/* Home button */}
<div className="flex justify-center py-2">
<div className="w-8 h-8 bg-white rounded-full border border-gray-300 shadow flex items-center justify-center">
<div className="w-4 h-4 border-1 border-[#D4C29A] rounded-full"></div>
</div>
</div>
</div>
</div>
</div>
)
}
export default NowPlaying