feat: Smart play/pause overlay on DetailPanel album art
DetailPanel.tsx: - Added isPlaying and currentClipId props to track playback state - Added onTogglePlay prop for pausing currently-playing track - Album art click handler now smart: * If this track IS playing → pause it (onTogglePlay) * If this track is NOT playing → play it (onPlay) - Hover overlay icon adapts dynamically: * Currently playing + hover → shows PAUSE icon (⏸) * Not playing + hover → shows PLAY icon (▶) - Works for both artwork images and placeholder state App.tsx: - Passes isPlaying, currentClipId, and onTogglePlay to DetailPanel Visual states: - Different track in panel: ▶ on hover (click to play) - Same track playing: ⏸ on hover (click to pause) - Same track paused: ▶ on hover (click to resume)
This commit is contained in:
1 parent
4808406113
commit
fa3515acc9
2 files changed
+39
-9
No files matched your search
@@ -229,12 +229,15 @@ function App() {
|
|||||||
{selectedClip && (
|
{selectedClip && (
|
||||||
<DetailPanel
|
<DetailPanel
|
||||||
clip={selectedClip}
|
clip={selectedClip}
|
||||||
|
isPlaying={isPlaying}
|
||||||
|
currentClipId={currentClip?.id}
|
||||||
onClose={() => setSelectedClip(null)}
|
onClose={() => setSelectedClip(null)}
|
||||||
onRename={handleRename}
|
onRename={handleRename}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onUploadArtwork={handleUploadArtwork}
|
onUploadArtwork={handleUploadArtwork}
|
||||||
onRemoveArtwork={handleRemoveArtwork}
|
onRemoveArtwork={handleRemoveArtwork}
|
||||||
onPlay={handlePlay}
|
onPlay={handlePlay}
|
||||||
|
onTogglePlay={() => setIsPlaying(!isPlaying)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,15 @@ interface Clip {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
clip: Clip
|
clip: Clip
|
||||||
|
isPlaying: boolean
|
||||||
|
currentClipId?: string
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onRename: (id: string, title: string) => void
|
onRename: (id: string, title: string) => void
|
||||||
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
|
onPlay?: (clip: Clip) => void
|
||||||
|
onTogglePlay?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelName = (modelId: string) => {
|
const modelName = (modelId: string) => {
|
||||||
@@ -34,7 +37,7 @@ const modelName = (modelId: string) => {
|
|||||||
return names[modelId] || modelId
|
return names[modelId] || modelId
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DetailPanel({ clip, onClose, onRename, onDelete, onUploadArtwork, onRemoveArtwork, onPlay }: Props) {
|
export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, onRename, onDelete, onUploadArtwork, onRemoveArtwork, onPlay, onTogglePlay }: 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 || '')
|
||||||
@@ -149,7 +152,13 @@ export default function DetailPanel({ clip, onClose, onRename, onDelete, onUploa
|
|||||||
{clip.artwork_filename ? (
|
{clip.artwork_filename ? (
|
||||||
<div
|
<div
|
||||||
className="relative w-full aspect-square rounded-lg overflow-hidden cursor-pointer group"
|
className="relative w-full aspect-square rounded-lg overflow-hidden cursor-pointer group"
|
||||||
onClick={() => onPlay?.(clip)}
|
onClick={() => {
|
||||||
|
if (currentClipId === clip.id && isPlaying) {
|
||||||
|
onTogglePlay?.()
|
||||||
|
} else {
|
||||||
|
onPlay?.(clip)
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={`/api/clips/${clip.id}/artwork`}
|
src={`/api/clips/${clip.id}/artwork`}
|
||||||
@@ -157,24 +166,42 @@ export default function DetailPanel({ clip, onClose, onRename, onDelete, onUploa
|
|||||||
className="w-full h-full 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">
|
<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">
|
{currentClipId === clip.id && isPlaying ? (
|
||||||
<path d="M8 5v14l11-7z" />
|
<svg className="w-12 h-12 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||||
</svg>
|
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg className="w-12 h-12 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M8 5v14l11-7z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<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"
|
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)}
|
onClick={() => {
|
||||||
|
if (currentClipId === clip.id && isPlaying) {
|
||||||
|
onTogglePlay?.()
|
||||||
|
} else {
|
||||||
|
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">
|
<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">
|
{currentClipId === clip.id && isPlaying ? (
|
||||||
<path d="M8 5v14l11-7z" />
|
<svg className="w-12 h-12 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||||
</svg>
|
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg className="w-12 h-12 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M8 5v14l11-7z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in new issue
Block a user