Files
ace-step-ui/components/Player.tsx
T
fspecii 325046eaf2 Apply i18n to all components, add collapsible sidebar, playback speed control, inline title editing, and ConfirmDialog for deletions
Phase 4: i18n all 15 components from PR #19. Sidebar gains collapse/expand
toggle. Player gets playback speed selector (0.25x-2.0x) fixing bug B1
(hardcoded Chinese '正常'). SongList adds inline title editing and model
version badge. Delete actions now use ConfirmDialog instead of
window.confirm. Volume persisted to localStorage. Training nav item
deferred to Phase 7.
2026-02-08 18:35:09 +02:00

828 lines
46 KiB
TypeScript

import React, { useRef, useState, useEffect } from 'react';
import { Song } from '../types';
import { Play, Pause, SkipBack, SkipForward, Repeat, Shuffle, Download, Heart, MoreVertical, Volume2, VolumeX, Maximize2, Repeat1, ChevronDown, ChevronUp } from 'lucide-react';
import { useAuth } from '../context/AuthContext';
import { useResponsive } from '../context/ResponsiveContext';
import { useI18n } from '../context/I18nContext';
import { SongDropdownMenu } from './SongDropdownMenu';
import { ShareModal } from './ShareModal';
import { AlbumCover } from './AlbumCover';
interface PlayerProps {
currentSong: Song | null;
isPlaying: boolean;
onTogglePlay: () => void;
currentTime: number;
duration: number;
onSeek: (time: number) => void;
onNext: () => void;
onPrevious: () => void;
volume: number;
onVolumeChange: (val: number) => void;
playbackRate: number;
onPlaybackRateChange: (rate: number) => void;
audioRef: React.RefObject<HTMLAudioElement>;
isShuffle: boolean;
onToggleShuffle: () => void;
repeatMode: 'none' | 'all' | 'one';
onToggleRepeat: () => void;
isLiked: boolean;
onToggleLike: () => void;
onNavigateToSong?: (songId: string) => void;
onOpenVideo?: () => void;
onReusePrompt?: () => void;
onAddToPlaylist?: () => void;
onDelete?: () => void;
}
export const Player: React.FC<PlayerProps> = ({
currentSong,
isPlaying,
onTogglePlay,
currentTime,
duration,
onSeek,
onNext,
onPrevious,
volume,
onVolumeChange,
playbackRate,
onPlaybackRateChange,
audioRef,
isShuffle,
onToggleShuffle,
repeatMode,
onToggleRepeat,
isLiked,
onToggleLike,
onNavigateToSong,
onOpenVideo,
onReusePrompt,
onAddToPlaylist,
onDelete
}) => {
const { user } = useAuth();
const { isMobile } = useResponsive();
const { t } = useI18n();
const progressBarRef = useRef<HTMLDivElement>(null);
const fullscreenProgressRef = useRef<HTMLDivElement>(null);
const [isHoveringVolume, setIsHoveringVolume] = useState(false);
const [showDropdown, setShowDropdown] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const [shareModalOpen, setShareModalOpen] = useState(false);
const [showSpeedMenu, setShowSpeedMenu] = useState(false);
const speedMenuRef = useRef<HTMLDivElement>(null);
// Close fullscreen on Escape key
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isFullscreen) {
setIsFullscreen(false);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isFullscreen]);
// Close speed menu when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (speedMenuRef.current && !speedMenuRef.current.contains(event.target as Node)) {
setShowSpeedMenu(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// Show minimal player when no song is playing
if (!currentSong) {
return (
<div className="h-20 lg:h-24 bg-white dark:bg-black/95 backdrop-blur border-t border-zinc-200 dark:border-white/10 flex items-center justify-center z-50 transition-colors duration-300 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)] dark:shadow-none">
<div className="flex items-center gap-3 text-zinc-400 dark:text-zinc-600">
<div className="w-10 h-10 lg:w-12 lg:h-12 rounded bg-zinc-200 dark:bg-zinc-800 flex items-center justify-center">
<Play size={20} className="text-zinc-400 dark:text-zinc-600" />
</div>
<span className="text-sm font-medium">Select a song to play</span>
</div>
</div>
);
}
const formatTime = (time: number) => {
if (isNaN(time)) return "0:00";
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
const handleSeekInteraction = (e: React.MouseEvent<HTMLDivElement>, ref: React.RefObject<HTMLDivElement>) => {
if (!ref.current || !duration) return;
const rect = ref.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const width = rect.width;
const percentage = Math.max(0, Math.min(1, x / width));
onSeek(percentage * duration);
};
const progressPercent = duration ? (currentTime / duration) * 100 : 0;
const handleDownload = async () => {
if (!currentSong?.audioUrl) return;
try {
const response = await fetch(currentSong.audioUrl);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${currentSong.title || 'song'}.mp3`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.error('Download failed:', error);
}
};
if (isMobile) {
if (isFullscreen) {
return (
<div className="fixed inset-0 z-50 bg-gradient-to-b from-zinc-100 to-zinc-50 dark:from-zinc-900 dark:to-black flex flex-col safe-area-inset-top safe-area-inset-bottom transition-colors duration-300">
{/* Header with close button */}
<div className="flex items-center justify-between px-4 py-3">
<button
onClick={() => setIsFullscreen(false)}
className="p-2 text-zinc-600 dark:text-white/70 tap-highlight-none"
>
<ChevronDown size={28} />
</button>
<span className="text-xs text-zinc-500 dark:text-white/50 uppercase tracking-wider">{t('nowPlaying')}</span>
<div className="w-11" />
</div>
{/* Album Art */}
<div className="flex-1 flex items-center justify-center px-8 py-4">
<div className="w-full max-w-[280px] aspect-square rounded-lg overflow-hidden shadow-2xl">
{currentSong.coverUrl ? (
<img
src={currentSong.coverUrl}
className="w-full h-full object-cover"
alt="cover"
onError={(e) => { e.currentTarget.style.display = 'none'; e.currentTarget.nextElementSibling?.classList.remove('hidden'); }}
/>
) : null}
<AlbumCover seed={currentSong.id || currentSong.title} size="full" className={`w-full h-full ${currentSong.coverUrl ? 'hidden' : ''}`} />
</div>
</div>
{/* Song Info */}
<div className="px-6 mb-4">
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0 mr-4">
<h2
onClick={() => {
setIsFullscreen(false);
onNavigateToSong?.(currentSong.id);
}}
className="text-xl font-bold text-zinc-900 dark:text-white truncate"
>
{currentSong.title}
</h2>
<p className="text-sm text-zinc-500 dark:text-white/60 truncate mt-1">
{currentSong.creator || 'Unknown Artist'}
</p>
</div>
<button
onClick={onToggleLike}
className={`p-2 tap-highlight-none ${isLiked ? 'text-pink-600 dark:text-pink-500' : 'text-zinc-400 dark:text-white/50'}`}
>
<Heart size={24} fill={isLiked ? "currentColor" : "none"} />
</button>
</div>
</div>
{/* Progress Bar */}
<div className="px-6 mb-2">
<div
ref={fullscreenProgressRef}
className="w-full h-1.5 bg-zinc-300 dark:bg-white/20 rounded-full cursor-pointer relative"
onClick={(e) => handleSeekInteraction(e, fullscreenProgressRef)}
>
<div
className="h-full bg-zinc-900 dark:bg-white rounded-full relative"
style={{ width: `${progressPercent}%` }}
>
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-4 h-4 bg-zinc-900 dark:bg-white rounded-full shadow-lg -mr-2" />
</div>
</div>
<div className="flex justify-between mt-2 text-xs text-zinc-500 dark:text-white/50 font-mono">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration || 0)}</span>
</div>
</div>
{/* Main Controls */}
<div className="flex items-center justify-center gap-8 py-4">
<button
onClick={onToggleShuffle}
className={`p-2 tap-highlight-none ${isShuffle ? 'text-pink-600 dark:text-pink-500' : 'text-zinc-400 dark:text-white/50'}`}
>
<Shuffle size={22} />
</button>
<button
onClick={onPrevious}
className="p-2 text-zinc-800 dark:text-white tap-highlight-none"
>
<SkipBack size={32} fill="currentColor" />
</button>
<button
onClick={onTogglePlay}
className="w-16 h-16 rounded-full bg-zinc-900 dark:bg-white text-white dark:text-black flex items-center justify-center shadow-lg tap-highlight-none"
>
{isPlaying ? <Pause size={32} fill="currentColor" /> : <Play size={32} fill="currentColor" className="ml-1" />}
</button>
<button
onClick={onNext}
className="p-2 text-zinc-800 dark:text-white tap-highlight-none"
>
<SkipForward size={32} fill="currentColor" />
</button>
<button
onClick={onToggleRepeat}
className={`p-2 tap-highlight-none relative ${repeatMode !== 'none' ? 'text-pink-600 dark:text-pink-500' : 'text-zinc-400 dark:text-white/50'}`}
>
{repeatMode === 'one' ? <Repeat1 size={22} /> : <Repeat size={22} />}
</button>
</div>
{/* Volume Control - Vertical */}
<div className="flex flex-col items-center gap-3 px-6 py-4">
<div className="relative h-32 w-8 flex items-center justify-center">
<input
type="range"
min="0"
max="1"
step="0.01"
value={volume}
onChange={(e) => onVolumeChange(parseFloat(e.target.value))}
className="w-32 h-8 -rotate-90 origin-center appearance-none bg-transparent cursor-pointer"
style={{
WebkitAppearance: 'none',
background: `linear-gradient(to right, rgb(236 72 153) 0%, rgb(236 72 153) ${volume * 100}%, rgb(228 228 231) ${volume * 100}%, rgb(228 228 231) 100%)`
}}
/>
</div>
<button
onClick={() => onVolumeChange(volume === 0 ? 0.8 : 0)}
className="text-zinc-400 dark:text-white/50 tap-highlight-none"
>
{volume === 0 ? <VolumeX size={20} /> : <Volume2 size={20} />}
</button>
</div>
{/* Extra Actions */}
<div className="flex items-center justify-center gap-6 px-6 pb-6 text-zinc-400 dark:text-white/50">
{onOpenVideo && (
<button onClick={onOpenVideo} className="p-3 tap-highlight-none">
<Maximize2 size={20} />
</button>
)}
<button
onClick={handleDownload}
className="p-3 tap-highlight-none"
title={t('downloadAudio')}
>
<Download size={20} />
</button>
<button
onClick={() => setShowDropdown(!showDropdown)}
className="p-3 tap-highlight-none relative"
>
<MoreVertical size={20} />
</button>
</div>
{showDropdown && (
<div className="absolute bottom-24 left-1/2 -translate-x-1/2">
<SongDropdownMenu
song={currentSong}
isOpen={showDropdown}
onClose={() => setShowDropdown(false)}
isOwner={user?.id === currentSong.userId}
position="center"
direction="up"
onCreateVideo={onOpenVideo}
onReusePrompt={onReusePrompt}
onAddToPlaylist={onAddToPlaylist}
onDelete={onDelete}
onShare={() => setShareModalOpen(true)}
/>
</div>
)}
</div>
);
}
return (
<div className="bg-white dark:bg-black/95 backdrop-blur border-t border-zinc-200 dark:border-white/10 flex flex-col z-50 transition-colors duration-300 safe-area-inset-bottom">
{/* Progress Bar - taller for touch */}
<div
ref={progressBarRef}
className="w-full h-1 bg-zinc-200 dark:bg-zinc-800 cursor-pointer relative"
onClick={(e) => handleSeekInteraction(e, progressBarRef)}
>
<div
className="h-full bg-pink-600 dark:bg-pink-500"
style={{ width: `${progressPercent}%` }}
/>
</div>
{/* Main content: Song info left, controls right */}
<div className="flex items-center px-3 py-2 gap-3">
{/* Song Info - takes available space, tap to expand */}
<div
className="flex items-center gap-3 flex-1 min-w-0"
onClick={() => setIsFullscreen(true)}
>
<div className="w-11 h-11 rounded bg-zinc-200 dark:bg-zinc-800 overflow-hidden shadow-sm flex-shrink-0 relative">
{currentSong.coverUrl ? (
<img src={currentSong.coverUrl} className="w-full h-full object-cover" alt="cover" onError={(e) => { e.currentTarget.style.display = 'none'; }} />
) : null}
{!currentSong.coverUrl && <AlbumCover seed={currentSong.id || currentSong.title} size="full" className="w-full h-full" />}
<div className="absolute inset-0 flex items-center justify-center bg-black/20 opacity-0 active:opacity-100 transition-opacity">
<ChevronUp size={20} className="text-white" />
</div>
</div>
<div className="overflow-hidden flex-1 min-w-0">
<h4 className="text-sm font-semibold text-zinc-900 dark:text-white truncate">
{currentSong.title}
</h4>
<p className="text-xs text-zinc-500 dark:text-zinc-400 truncate">
{currentSong.creator || 'Unknown Artist'}
</p>
</div>
</div>
{/* Mobile Controls - compact */}
<div className="flex items-center gap-1 flex-shrink-0">
<button
onClick={onToggleLike}
className={`p-2 tap-highlight-none ${isLiked ? 'text-pink-600 dark:text-pink-500' : 'text-zinc-400'}`}
>
<Heart size={20} fill={isLiked ? "currentColor" : "none"} />
</button>
<button
onClick={onPrevious}
className="p-2 text-zinc-700 dark:text-zinc-300 tap-highlight-none"
>
<SkipBack size={22} fill="currentColor" />
</button>
<button
onClick={onTogglePlay}
className="w-11 h-11 rounded-full bg-zinc-900 dark:bg-white text-white dark:text-black flex items-center justify-center shadow-lg tap-highlight-none"
>
{isPlaying ? <Pause size={22} fill="currentColor" /> : <Play size={22} fill="currentColor" className="ml-0.5" />}
</button>
<button
onClick={onNext}
className="p-2 text-zinc-700 dark:text-zinc-300 tap-highlight-none"
>
<SkipForward size={22} fill="currentColor" />
</button>
</div>
</div>
</div>
);
}
// Desktop fullscreen mode
if (isFullscreen) {
return (
<div
className="fixed inset-0 z-50 bg-gradient-to-b from-zinc-100 to-zinc-50 dark:from-zinc-900 dark:to-black flex flex-col transition-colors duration-300"
onClick={() => setIsFullscreen(false)}
>
{/* Header with close button */}
<div className="flex items-center justify-between px-6 py-4" onClick={(e) => e.stopPropagation()}>
<button
onClick={() => setIsFullscreen(false)}
className="p-2 text-zinc-600 dark:text-white/70 hover:bg-zinc-200 dark:hover:bg-white/10 rounded-full transition-colors"
>
<ChevronDown size={28} />
</button>
<span className="text-sm text-zinc-500 dark:text-white/50 uppercase tracking-wider font-medium">{t('nowPlaying')}</span>
<div className="w-11" />
</div>
{/* Main content area */}
<div className="flex-1 flex items-center justify-center px-8 py-4 overflow-hidden" onClick={(e) => e.stopPropagation()}>
<div className="flex flex-col lg:flex-row items-center gap-8 lg:gap-16 max-w-5xl w-full">
{/* Album Art */}
<div className="w-full max-w-[320px] lg:max-w-[400px] aspect-square rounded-lg overflow-hidden shadow-2xl flex-shrink-0">
{currentSong.coverUrl ? (
<img
src={currentSong.coverUrl}
className="w-full h-full object-cover"
alt="cover"
onError={(e) => { e.currentTarget.style.display = 'none'; e.currentTarget.nextElementSibling?.classList.remove('hidden'); }}
/>
) : null}
<AlbumCover seed={currentSong.id || currentSong.title} size="full" className={`w-full h-full ${currentSong.coverUrl ? 'hidden' : ''}`} />
</div>
{/* Right side: Song info and controls */}
<div className="flex flex-col items-center lg:items-start gap-6 flex-1 min-w-0 max-w-lg">
{/* Song Info */}
<div className="text-center lg:text-left w-full">
<h2
onClick={() => {
setIsFullscreen(false);
onNavigateToSong?.(currentSong.id);
}}
className="text-2xl lg:text-3xl font-bold text-zinc-900 dark:text-white truncate cursor-pointer hover:underline"
>
{currentSong.title}
</h2>
<p className="text-base lg:text-lg text-zinc-500 dark:text-white/60 truncate mt-2">
{currentSong.creator || 'Unknown Artist'}
</p>
</div>
{/* Progress Bar */}
<div className="w-full">
<div
ref={fullscreenProgressRef}
className="w-full h-2 bg-zinc-300 dark:bg-white/20 rounded-full cursor-pointer relative group"
onClick={(e) => handleSeekInteraction(e, fullscreenProgressRef)}
>
<div
className="h-full bg-zinc-900 dark:bg-white rounded-full relative group-hover:bg-pink-600 dark:group-hover:bg-pink-500 transition-colors"
style={{ width: `${progressPercent}%` }}
>
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-4 h-4 bg-zinc-900 dark:bg-white group-hover:bg-pink-600 dark:group-hover:bg-pink-500 rounded-full shadow-lg -mr-2 opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</div>
<div className="flex justify-between mt-2 text-sm text-zinc-500 dark:text-white/50 font-mono">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration || 0)}</span>
</div>
</div>
{/* Main Controls */}
<div className="flex items-center justify-center gap-8 py-2 w-full">
<button
onClick={onToggleShuffle}
className={`p-2 transition-colors ${isShuffle ? 'text-pink-600 dark:text-pink-500' : 'text-zinc-400 hover:text-zinc-900 dark:hover:text-white'}`}
>
<Shuffle size={22} />
</button>
<button
onClick={onPrevious}
className="p-2 text-zinc-800 dark:text-white hover:scale-110 transition-transform"
>
<SkipBack size={36} fill="currentColor" />
</button>
<button
onClick={onTogglePlay}
className="w-18 h-18 p-5 rounded-full bg-zinc-900 dark:bg-white text-white dark:text-black flex items-center justify-center shadow-lg hover:scale-105 transition-transform"
>
{isPlaying ? <Pause size={36} fill="currentColor" /> : <Play size={36} fill="currentColor" className="ml-1" />}
</button>
<button
onClick={onNext}
className="p-2 text-zinc-800 dark:text-white hover:scale-110 transition-transform"
>
<SkipForward size={36} fill="currentColor" />
</button>
<button
onClick={onToggleRepeat}
className={`p-2 transition-colors relative ${repeatMode !== 'none' ? 'text-pink-600 dark:text-pink-500' : 'text-zinc-400 hover:text-zinc-900 dark:hover:text-white'}`}
>
{repeatMode === 'one' ? <Repeat1 size={22} /> : <Repeat size={22} />}
{repeatMode !== 'none' && <div className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 bg-current rounded-full"></div>}
</button>
</div>
{/* Playback Speed Dropdown */}
<div className="relative group hidden lg:block" ref={speedMenuRef}>
<button
className="px-2 py-1 text-[11px] font-mono font-bold hover:bg-zinc-200 dark:hover:bg-white/10 rounded transition-colors min-w-[42px] text-center"
onClick={() => setShowSpeedMenu(!showSpeedMenu)}
>
{playbackRate}x
</button>
{showSpeedMenu && (
<div className="absolute bottom-full right-0 mb-2 bg-white dark:bg-zinc-800 rounded-lg shadow-xl border border-zinc-200 dark:border-white/10 py-1 min-w-[80px] z-50">
{[0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0].map((rate) => (
<button
key={rate}
onClick={() => {
onPlaybackRateChange(rate);
setShowSpeedMenu(false);
}}
className={`w-full px-3 py-1.5 text-left text-xs font-mono hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors ${
playbackRate === rate ? 'text-pink-600 dark:text-pink-500 font-bold' : 'text-zinc-700 dark:text-zinc-300'
}`}
>
{rate === 1.0 ? t('normalSpeed') : `${rate}x`}
</button>
))}
</div>
)}
</div>
{/* Volume Control */}
<div className="flex items-center gap-4 w-full max-w-xs">
<button
onClick={() => onVolumeChange(volume === 0 ? 0.8 : 0)}
className="text-zinc-500 dark:text-white/50 hover:text-zinc-900 dark:hover:text-white transition-colors"
>
{volume === 0 ? <VolumeX size={22} /> : <Volume2 size={22} />}
</button>
<div className="flex-1 h-1.5 bg-zinc-300 dark:bg-white/20 rounded-full relative">
<input
type="range"
min="0"
max="1"
step="0.01"
value={volume}
onChange={(e) => onVolumeChange(parseFloat(e.target.value))}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10"
/>
<div
className="h-full bg-zinc-700 dark:bg-white/70 rounded-full"
style={{ width: `${volume * 100}%` }}
/>
<div
className="absolute top-1/2 -translate-y-1/2 w-3.5 h-3.5 bg-zinc-700 dark:bg-white/70 rounded-full shadow pointer-events-none"
style={{
left: `clamp(0px, calc(${volume * 100}% - 7px), calc(100% - 14px))`
}}
/>
</div>
</div>
{/* Extra Actions */}
<div className="flex items-center justify-center gap-4 text-zinc-400 dark:text-white/50">
<button
onClick={onToggleLike}
className={`p-3 rounded-full hover:bg-zinc-200 dark:hover:bg-white/10 transition-colors ${isLiked ? 'text-pink-600 dark:text-pink-500' : ''}`}
>
<Heart size={22} fill={isLiked ? "currentColor" : "none"} />
</button>
{onOpenVideo && (
<button
onClick={onOpenVideo}
className="p-3 rounded-full hover:bg-zinc-200 dark:hover:bg-white/10 transition-colors"
>
<Maximize2 size={20} />
</button>
)}
<button
onClick={handleDownload}
className="p-3 rounded-full hover:bg-zinc-200 dark:hover:bg-white/10 transition-colors"
title={t('downloadAudio')}
>
<Download size={20} />
</button>
<div className="relative">
<button
onClick={() => setShowDropdown(!showDropdown)}
className="p-3 rounded-full hover:bg-zinc-200 dark:hover:bg-white/10 transition-colors"
>
<MoreVertical size={20} />
</button>
{showDropdown && (
<SongDropdownMenu
song={currentSong}
isOpen={showDropdown}
onClose={() => setShowDropdown(false)}
isOwner={user?.id === currentSong.userId}
position="center"
direction="up"
onCreateVideo={onOpenVideo}
onReusePrompt={onReusePrompt}
onAddToPlaylist={onAddToPlaylist}
onDelete={onDelete}
onShare={() => setShareModalOpen(true)}
/>
)}
</div>
</div>
</div>
</div>
</div>
<ShareModal
isOpen={shareModalOpen}
onClose={() => setShareModalOpen(false)}
song={currentSong}
/>
</div>
);
}
return (
<div className="h-20 lg:h-24 bg-white dark:bg-black/95 backdrop-blur border-t border-zinc-200 dark:border-white/10 flex flex-col z-50 transition-colors duration-300 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)] dark:shadow-none">
{/* Progress Bar */}
<div
ref={progressBarRef}
className="w-full h-1 lg:h-1.5 bg-zinc-200 dark:bg-zinc-800 cursor-pointer group relative"
onClick={(e) => handleSeekInteraction(e, progressBarRef)}
>
<div
className="h-full bg-zinc-900 dark:bg-white relative group-hover:bg-pink-600 dark:group-hover:bg-pink-500 transition-colors"
style={{ width: `${progressPercent}%` }}
>
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-3 h-3 bg-zinc-900 dark:bg-white group-hover:bg-pink-600 dark:group-hover:bg-pink-500 rounded-full shadow-lg -mr-2 opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
{/* Hit area for easier clicking */}
<div className="absolute top-1/2 -translate-y-1/2 w-full h-4 -z-10"></div>
</div>
<div className="flex-1 flex items-center justify-between px-2 sm:px-4 lg:px-6 gap-2 sm:gap-4">
{/* Song Info */}
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-1 max-w-[30%] lg:max-w-[33%]">
<div className="w-10 h-10 lg:w-12 lg:h-12 rounded bg-zinc-200 dark:bg-zinc-800 overflow-hidden shadow-sm flex-shrink-0">
{currentSong.coverUrl ? (
<img src={currentSong.coverUrl} className="w-full h-full object-cover" alt="cover" onError={(e) => { e.currentTarget.style.display = 'none'; }} />
) : null}
{!currentSong.coverUrl && <AlbumCover seed={currentSong.id || currentSong.title} size="full" className="w-full h-full" />}
</div>
<div className="overflow-hidden min-w-0">
<h4
onClick={() => onNavigateToSong?.(currentSong.id)}
className="text-xs sm:text-sm font-bold text-zinc-900 dark:text-white truncate cursor-pointer hover:underline"
>
{currentSong.title}
</h4>
<p className="text-[10px] sm:text-xs text-zinc-500 dark:text-zinc-400 truncate hover:underline cursor-pointer">{currentSong.creator || 'Unknown Artist'}</p>
</div>
<button
onClick={onToggleLike}
className={`ml-1 sm:ml-2 transition-colors flex-shrink-0 hidden sm:block ${isLiked ? 'text-pink-600 dark:text-pink-500' : 'text-zinc-400 hover:text-zinc-900 dark:hover:text-white'}`}
>
<Heart size={18} fill={isLiked ? "currentColor" : "none"} />
</button>
</div>
{/* Controls */}
<div className="flex flex-col items-center justify-center flex-shrink-0">
<div className="flex items-center gap-2 sm:gap-4 lg:gap-6">
<button
onClick={onToggleShuffle}
className={`transition-colors hidden sm:block ${isShuffle ? 'text-pink-600 dark:text-pink-500' : 'text-zinc-400 hover:text-zinc-900 dark:hover:text-white'}`}
>
<Shuffle size={16} />
</button>
<button
onClick={onPrevious}
className="text-zinc-700 dark:text-zinc-300 hover:text-black dark:hover:text-white transition-colors"
>
<SkipBack size={18} className="sm:w-[22px] sm:h-[22px]" fill="currentColor" />
</button>
<button
onClick={onTogglePlay}
className="w-9 h-9 sm:w-10 sm:h-10 rounded-full bg-zinc-900 dark:bg-white text-white dark:text-black flex items-center justify-center hover:scale-105 transition-transform shadow-lg"
>
{isPlaying ? <Pause size={18} className="sm:w-5 sm:h-5" fill="currentColor" /> : <Play size={18} className="sm:w-5 sm:h-5 ml-0.5" fill="currentColor" />}
</button>
<button
onClick={onNext}
className="text-zinc-700 dark:text-zinc-300 hover:text-black dark:hover:text-white transition-colors"
>
<SkipForward size={18} className="sm:w-[22px] sm:h-[22px]" fill="currentColor" />
</button>
<button
onClick={onToggleRepeat}
className={`transition-colors hidden sm:block ${repeatMode !== 'none' ? 'text-pink-600 dark:text-pink-500' : 'text-zinc-400 hover:text-zinc-900 dark:hover:text-white'} relative`}
>
{repeatMode === 'one' ? <Repeat1 size={16} /> : <Repeat size={16} />}
{repeatMode !== 'none' && <div className="absolute -bottom-1.5 left-1/2 -translate-x-1/2 w-1 h-1 bg-current rounded-full"></div>}
</button>
</div>
</div>
{/* Volume & Extras */}
<div className="flex items-center justify-end gap-1 sm:gap-2 lg:gap-3 min-w-0 flex-1 max-w-[30%] lg:max-w-[33%] text-zinc-500 dark:text-zinc-400">
<span className="text-[10px] sm:text-xs font-mono text-right text-zinc-600 dark:text-zinc-400 hidden md:block">
{formatTime(currentTime)} / {formatTime(duration || 0)}
</span>
{/* Playback Speed */}
<div className="relative group hidden lg:block" ref={speedMenuRef}>
<button
className="px-2 py-1 text-[11px] font-mono font-bold hover:bg-zinc-200 dark:hover:bg-white/10 rounded transition-colors min-w-[42px] text-center"
onClick={() => setShowSpeedMenu(!showSpeedMenu)}
>
{playbackRate}x
</button>
{showSpeedMenu && (
<div className="absolute bottom-full right-0 mb-2 bg-white dark:bg-zinc-800 rounded-lg shadow-xl border border-zinc-200 dark:border-white/10 py-1 min-w-[80px] z-50">
{[0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0].map((rate) => (
<button
key={rate}
onClick={() => {
onPlaybackRateChange(rate);
setShowSpeedMenu(false);
}}
className={`w-full px-3 py-1.5 text-left text-xs font-mono hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors ${
playbackRate === rate ? 'text-pink-600 dark:text-pink-500 font-bold' : 'text-zinc-700 dark:text-zinc-300'
}`}
>
{rate === 1.0 ? t('normalSpeed') : `${rate}x`}
</button>
))}
</div>
)}
</div>
{/* Volume Control with Vertical Slider */}
<div
className="relative group hidden md:block"
onMouseEnter={() => setIsHoveringVolume(true)}
onMouseLeave={() => setIsHoveringVolume(false)}
>
<button
onClick={() => onVolumeChange(volume === 0 ? 0.8 : 0)}
className="p-1.5 lg:p-2 hover:bg-zinc-100 dark:hover:bg-white/10 rounded-full transition-colors"
>
{volume === 0 ? <VolumeX size={18} /> : <Volume2 size={18} />}
</button>
{/* Vertical Volume Slider */}
{isHoveringVolume && (
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 pb-2">
<div className="bg-white dark:bg-zinc-800 rounded-lg shadow-xl border border-zinc-200 dark:border-white/10 p-2">
<div className="relative h-24 w-8 flex items-center justify-center">
<input
type="range"
min="0"
max="1"
step="0.01"
value={volume}
onChange={(e) => onVolumeChange(parseFloat(e.target.value))}
className="w-24 h-8 -rotate-90 origin-center appearance-none bg-transparent cursor-pointer"
style={{
WebkitAppearance: 'none',
background: `linear-gradient(to right, rgb(236 72 153) 0%, rgb(236 72 153) ${volume * 100}%, rgb(228 228 231) ${volume * 100}%, rgb(228 228 231) 100%)`
}}
/>
</div>
<div className="text-[10px] text-center font-mono text-zinc-600 dark:text-zinc-400 mt-1">
{Math.round(volume * 100)}%
</div>
</div>
</div>
)}
</div>
<button
onClick={handleDownload}
className="p-1.5 lg:p-2 hover:bg-zinc-100 dark:hover:bg-white/10 rounded-full transition-colors hidden lg:block"
title={t('downloadAudio')}
>
<Download size={18} />
</button>
<button
onClick={() => setIsFullscreen(true)}
className="p-1.5 lg:p-2 hover:bg-zinc-100 dark:hover:bg-white/10 rounded-full transition-colors"
>
<Maximize2 size={16} />
</button>
<div className="relative hidden sm:block">
<button
onClick={() => setShowDropdown(!showDropdown)}
className="p-1.5 lg:p-2 hover:bg-zinc-100 dark:hover:bg-white/10 rounded-full transition-colors"
>
<MoreVertical size={18} />
</button>
<SongDropdownMenu
song={currentSong}
isOpen={showDropdown}
onClose={() => setShowDropdown(false)}
isOwner={user?.id === currentSong.userId}
position="right"
direction="up"
onCreateVideo={onOpenVideo}
onReusePrompt={onReusePrompt}
onAddToPlaylist={onAddToPlaylist}
onDelete={onDelete}
onShare={() => setShareModalOpen(true)}
/>
</div>
</div>
</div>
<ShareModal
isOpen={shareModalOpen}
onClose={() => setShareModalOpen(false)}
song={currentSong}
/>
</div>
);
};