feat: Click album art in DetailPanel to play track

DetailPanel.tsx:
- Added optional onPlay prop to interface
- Album art image now wrapped in clickable container with cursor-pointer
- Hover overlay: semi-transparent black bg with white play triangle icon
- Works for both artwork images and placeholder (treble clef) state
- Placeholder gets hover:bg-white/10 for subtle feedback
- App.tsx: passes handlePlay to DetailPanel onPlay prop

Visual feedback:
- group-hover opacity transition on overlay (0 → 1)
- Play icon: w-12 h-12 white triangle (▶) centered
- Smooth 150ms transition on overlay opacity
This commit is contained in:
rin.kagamine committed 2026-05-29 21:46:58 -04:00
1 parent dff48eac13
commit 4808406113
2 files changed
+23 -3

No files matched your search

+1
View File
@@ -234,6 +234,7 @@ function App() {
onDelete={handleDelete} onDelete={handleDelete}
onUploadArtwork={handleUploadArtwork} onUploadArtwork={handleUploadArtwork}
onRemoveArtwork={handleRemoveArtwork} onRemoveArtwork={handleRemoveArtwork}
onPlay={handlePlay}
/> />
)} )}
+22 -3
View File
@@ -23,6 +23,7 @@ interface Props {
onDelete: (id: string) => void onDelete: (id: string) => void
onUploadArtwork: (id: string, file: File) => void onUploadArtwork: (id: string, file: File) => void
onRemoveArtwork: (id: string) => void onRemoveArtwork: (id: string) => void
onPlay?: (clip: Clip) => void
} }
const modelName = (modelId: string) => { const modelName = (modelId: string) => {
@@ -33,7 +34,7 @@ const modelName = (modelId: string) => {
return names[modelId] || modelId return names[modelId] || modelId
} }
export default function DetailPanel({ clip, onClose, onRename, onDelete, onUploadArtwork, onRemoveArtwork }: Props) { export default function DetailPanel({ clip, onClose, onRename, onDelete, onUploadArtwork, onRemoveArtwork, onPlay }: Props) {
const artworkInputRef = useRef<HTMLInputElement>(null) const artworkInputRef = useRef<HTMLInputElement>(null)
const [isEditingTitle, setIsEditingTitle] = useState(false) const [isEditingTitle, setIsEditingTitle] = useState(false)
const [editTitle, setEditTitle] = useState(clip.title || '') const [editTitle, setEditTitle] = useState(clip.title || '')
@@ -146,17 +147,35 @@ export default function DetailPanel({ clip, onClose, onRename, onDelete, onUploa
</div> </div>
</div> </div>
{clip.artwork_filename ? ( {clip.artwork_filename ? (
<div
className="relative w-full aspect-square rounded-lg overflow-hidden cursor-pointer group"
onClick={() => onPlay?.(clip)}
>
<img <img
src={`/api/clips/${clip.id}/artwork`} src={`/api/clips/${clip.id}/artwork`}
alt="Album art" alt="Album art"
className="w-full aspect-square rounded-lg object-cover" className="w-full h-full object-cover"
/> />
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<svg className="w-12 h-12 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
</div>
</div>
) : ( ) : (
<div className="w-full aspect-square bg-white/5 rounded-lg flex flex-col items-center justify-center text-lyria-accent/20"> <div
className="relative w-full aspect-square bg-white/5 rounded-lg flex flex-col items-center justify-center text-lyria-accent/20 cursor-pointer group hover:bg-white/10 transition-colors"
onClick={() => onPlay?.(clip)}
>
<svg className="w-14 h-14" viewBox="0 0 24 24"> <svg className="w-14 h-14" viewBox="0 0 24 24">
<text x="12" y="19" textAnchor="middle" fontSize="20" fill="currentColor">𝄞</text> <text x="12" y="19" textAnchor="middle" fontSize="20" fill="currentColor">𝄞</text>
</svg> </svg>
<span className="text-xs mt-1 text-lyria-muted/40">No album art</span> <span className="text-xs mt-1 text-lyria-muted/40">No album art</span>
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<svg className="w-12 h-12 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
</div>
</div> </div>
)} )}
<input <input