Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/components/AddToPlaylistSheet.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { usePlaylists } from '../state/useLibrary'
import { usePlayer } from '../state/PlayerProvider'
import { addToPlaylist, createPlaylist, toggleStar, deleteTrack } from '../lib/db'
import PromptModal from './PromptModal'
import ConfirmModal from './ConfirmModal'

// Track actions sheet (opened from a TrackRow's ⋯). Quick playback actions on
// top (Play next / Add to queue / Favorite), then the "add to playlist" picker.
Expand All @@ -12,6 +13,7 @@ export default function AddToPlaylistSheet() {
const playlists = usePlaylists()
const { playNext, addToQueue } = usePlayer()
const [creating, setCreating] = useState(false)
const [confirmDelete, setConfirmDelete] = useState(false)
if (!addTarget) return null

const close = closeAddToPlaylist
Expand Down Expand Up @@ -55,7 +57,7 @@ export default function AddToPlaylistSheet() {
{addTarget.srcType === 'idb' && (
<button
className="sheet__item sheet__item--danger"
onClick={() => { deleteTrack(addTarget.id); close() }}
onClick={() => setConfirmDelete(true)}
>
<span>Delete from library</span>
<svg viewBox="0 0 18 18" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M3 5h12M7 5V3h4v2M6 5l.8 10h4.4L12 5" /></svg>
Expand All @@ -78,6 +80,20 @@ export default function AddToPlaylistSheet() {
}}
/>
)}

{confirmDelete && (
<ConfirmModal
title="Delete from library?"
message={`"${addTarget.title}" and its downloaded audio will be removed. This can't be undone.`}
confirmLabel="Delete"
onConfirm={async () => {
await deleteTrack(addTarget.id)
setConfirmDelete(false)
close()
}}
onClose={() => setConfirmDelete(false)}
/>
)}
</>
)
}
Expand Down
20 changes: 18 additions & 2 deletions src/components/Artwork.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useState, useEffect } from 'react'

// Square artwork. Uses the YouTube thumbnail when present; otherwise renders a
// deterministic gradient tile (same track → same colors) so the library still
// looks intentional for the sample tones and any thumbnail-less tracks.
Expand All @@ -15,8 +17,22 @@ export function warmGlow(id) {

export default function Artwork({ track, size = 48, radius = 8 }) {
const style = { width: size, height: size, borderRadius: radius }
if (track?.thumbnailUrl) {
return <img className="artwork" src={track.thumbnailUrl} alt="" style={style} />
// A present-but-broken thumbnail URL (expired/offline/deleted video) should
// fall back to the gradient tile, not the browser's broken-image glyph.
// Reset the flag when the URL changes since this instance is reused in lists.
const [failed, setFailed] = useState(false)
useEffect(() => setFailed(false), [track?.thumbnailUrl])

if (track?.thumbnailUrl && !failed) {
return (
<img
className="artwork"
src={track.thumbnailUrl}
alt=""
style={style}
onError={() => setFailed(true)}
/>
)
}
// Keep the hue in a warm band (deep amber → sienna → olive) and low-ish
// saturation so placeholders stay cohesive with the palette — never rainbow.
Expand Down
Loading