import { useContainerPortrait } from '@/hooks/useContainerPortrait' import { useCoverWidth } from '@/hooks/useCoverWidth' import { useVote } from '@/hooks/useVote' import { getProxiedCoverUrl } from '@/lib/cover' import { cn } from '@/lib/utils' import { usePlayerStore } from '@/stores/playerStore' import { useSettingsStore } from '@/stores/settingsStore' import { BackgroundRender } from '@applemusic-like-lyrics/react' import { AnimatePresence, LayoutGroup, motion } from 'motion/react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { VoteBanner } from '../Vote/VoteBanner' import { LyricDisplay } from './LyricDisplay' import { NowPlaying } from './NowPlaying' import { PlayerControls } from './PlayerControls' import { SongInfoBar } from './SongInfoBar' const FULL_SIZE_STYLE = { width: '100%', height: '100%' } as const const MOBILE_LYRIC_AUTO_EXPAND_DELAY_MS = 900 const SMOOTH_EASE = [0.22, 1, 0.36, 1] as const const LYRIC_MASK_STYLE = { maskImage: 'linear-gradient(to bottom, transparent 0%, black 8%, black 92%, transparent 100%)', WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, black 8%, black 92%, transparent 100%)', } as const interface AudioPlayerProps { onPlay: () => void onPause: () => void onSeek: (time: number) => void onNext: () => void onPrev: () => void onOpenChat: () => void onOpenQueue: () => void chatUnreadCount: number } export function AudioPlayer({ onPlay, onPause, onSeek, onNext, onPrev, onOpenChat, onOpenQueue, chatUnreadCount, }: AudioPlayerProps) { const currentTrack = usePlayerStore((s) => s.currentTrack) const { activeVote, castVote, startVote } = useVote() const bgFps = useSettingsStore((s) => s.bgFps) const bgFlowSpeed = useSettingsStore((s) => s.bgFlowSpeed) const bgRenderScale = useSettingsStore((s) => s.bgRenderScale) const { ref: playerRef, isPortrait } = useContainerPortrait() // 封面 URL 代理:解决 QQ 音乐 / 酷狗等 CDN 的 CORS 限制 const proxiedCover = currentTrack?.cover ? getProxiedCoverUrl(currentTrack.cover) : undefined // Suppress the document referrer for the renderer's own image request. const backgroundCover = useMemo(() => { if (!proxiedCover || typeof Image === 'undefined') return undefined const image = new Image() image.crossOrigin = 'anonymous' image.referrerPolicy = 'no-referrer' image.src = proxiedCover return image }, [proxiedCover]) // Mobile: toggle between cover view and lyric view const [lyricExpanded, setLyricExpanded] = useState(false) const hasAutoExpandedLyrics = useRef(false) // Let the cover render first, then automatically enter the mobile lyric view // through the same shared-layout transition used by a cover tap. useEffect(() => { if (!isPortrait || hasAutoExpandedLyrics.current) return const timer = window.setTimeout(() => { hasAutoExpandedLyrics.current = true setLyricExpanded(true) }, MOBILE_LYRIC_AUTO_EXPAND_DELAY_MS) return () => window.clearTimeout(timer) }, [isPortrait]) // Measure cover area to constrain info/controls width (paused during lyric mode) const { ref: coverAreaRef, coverWidth } = useCoverWidth(lyricExpanded) const toggleLyricView = useCallback(() => setLyricExpanded((v) => !v), []) // Derived styles to constrain info/controls to cover width const coverMaxStyle = coverWidth ? { maxWidth: coverWidth } : undefined const coverMaxStyleUnlessExpanded = lyricExpanded ? undefined : coverMaxStyle const playerControlsProps = { onPlay, onPause, onSeek, onNext, onPrev, onOpenQueue, onStartVote: startVote, } as const const songInfoProps = { onOpenChat, chatUnreadCount, } as const return (
{/* AMLL fluid dynamic background powered by pixi.js */} {backgroundCover && (
)} {/* Content with padding */}
{/* ----------------------------------------------------------------- */} {/* Mobile layout: dual-mode (cover view / lyric view) */} {/* ----------------------------------------------------------------- */} {isPortrait ? (
{/* 1. Cover — fills remaining space in cover mode, centered within */}
{/* Lyrics — popLayout so exiting lyrics don't occupy flex space */} {lyricExpanded && ( )} {/* 2. Song info + action buttons (independent zoom module) */} {!lyricExpanded && (
)} {/* 3. Controls (independent zoom module) */}
{/* Vote banner: absolute overlay at the bottom */} {activeVote && (
)}
) : ( // --------------------------------------------------------------- // Desktop layout: left panel (cover + info + controls) + right lyrics // --------------------------------------------------------------- <>
{/* 1. Cover — flex-1 fills remaining space, centered */}
{/* 2. Song info + action buttons */}
{/* 3. Controls */}
{activeVote && (
)}
)}
) }