From 6b4ad16882d6c93952b7184fa965227509d91fbc Mon Sep 17 00:00:00 2001 From: afkarxyz Date: Tue, 25 Nov 2025 13:15:43 +0700 Subject: [PATCH] v6.2 --- backend/spotify_metadata.go | 79 ++++++-- frontend/src/App.tsx | 222 +++++++++++++++++++--- frontend/src/components/AlbumInfo.tsx | 30 ++- frontend/src/components/ArtistInfo.tsx | 20 +- frontend/src/components/DebugLogger.tsx | 132 +++++++++++++ frontend/src/components/FetchHistory.tsx | 91 +++++++++ frontend/src/components/PlaylistInfo.tsx | 15 +- frontend/src/components/SearchAndSort.tsx | 13 +- frontend/src/components/SearchBar.tsx | 114 ++++++----- frontend/src/components/Settings.tsx | 76 ++++++-- frontend/src/components/TrackList.tsx | 94 ++++++++- frontend/src/hooks/useDownload.ts | 59 ++++-- frontend/src/hooks/useMetadata.ts | 96 +++++++++- frontend/src/index.css | 6 +- frontend/src/lib/logger.ts | 66 +++++++ frontend/src/lib/toast-with-sound.ts | 42 ++-- frontend/src/main.tsx | 20 +- frontend/src/types/api.ts | 11 ++ wails.json | 2 +- 19 files changed, 1034 insertions(+), 154 deletions(-) create mode 100644 frontend/src/components/DebugLogger.tsx create mode 100644 frontend/src/components/FetchHistory.tsx create mode 100644 frontend/src/lib/logger.ts diff --git a/backend/spotify_metadata.go b/backend/spotify_metadata.go index b99e26c..aba2982 100644 --- a/backend/spotify_metadata.go +++ b/backend/spotify_metadata.go @@ -69,19 +69,31 @@ type TrackMetadata struct { SpotifyID string `json:"spotify_id,omitempty"` } +// ArtistSimple holds basic artist info for clickable artists +type ArtistSimple struct { + ID string `json:"id"` + Name string `json:"name"` + ExternalURL string `json:"external_urls"` +} + // AlbumTrackMetadata holds per-track info for album / playlist formatting. type AlbumTrackMetadata struct { - Artists string `json:"artists"` - Name string `json:"name"` - AlbumName string `json:"album_name"` - DurationMS int `json:"duration_ms"` - Images string `json:"images"` - ReleaseDate string `json:"release_date"` - TrackNumber int `json:"track_number"` - ExternalURL string `json:"external_urls"` - ISRC string `json:"isrc"` - AlbumType string `json:"album_type,omitempty"` - SpotifyID string `json:"spotify_id,omitempty"` + Artists string `json:"artists"` + Name string `json:"name"` + AlbumName string `json:"album_name"` + DurationMS int `json:"duration_ms"` + Images string `json:"images"` + ReleaseDate string `json:"release_date"` + TrackNumber int `json:"track_number"` + ExternalURL string `json:"external_urls"` + ISRC string `json:"isrc"` + AlbumType string `json:"album_type,omitempty"` + SpotifyID string `json:"spotify_id,omitempty"` + AlbumID string `json:"album_id,omitempty"` + AlbumURL string `json:"album_url,omitempty"` + ArtistID string `json:"artist_id,omitempty"` + ArtistURL string `json:"artist_url,omitempty"` + ArtistsData []ArtistSimple `json:"artists_data,omitempty"` } type TrackResponse struct { @@ -95,6 +107,8 @@ type AlbumInfoMetadata struct { Artists string `json:"artists"` Images string `json:"images"` Batch string `json:"batch,omitempty"` + ArtistID string `json:"artist_id,omitempty"` + ArtistURL string `json:"artist_url,omitempty"` } type AlbumResponsePayload struct { @@ -474,6 +488,19 @@ func (c *SpotifyMetadataClient) formatPlaylistData(raw *playlistRaw) PlaylistRes if item.Track == nil { continue } + var artistID, artistURL string + if len(item.Track.Artists) > 0 { + artistID = item.Track.Artists[0].ID + artistURL = fmt.Sprintf("https://open.spotify.com/artist/%s", item.Track.Artists[0].ID) + } + artistsData := make([]ArtistSimple, 0, len(item.Track.Artists)) + for _, a := range item.Track.Artists { + artistsData = append(artistsData, ArtistSimple{ + ID: a.ID, + Name: a.Name, + ExternalURL: fmt.Sprintf("https://open.spotify.com/artist/%s", a.ID), + }) + } tracks = append(tracks, AlbumTrackMetadata{ Artists: joinArtists(item.Track.Artists), Name: item.Track.Name, @@ -485,6 +512,11 @@ func (c *SpotifyMetadataClient) formatPlaylistData(raw *playlistRaw) PlaylistRes ExternalURL: item.Track.ExternalURL.Spotify, ISRC: item.Track.ExternalID.ISRC, SpotifyID: item.Track.ID, + AlbumID: item.Track.Album.ID, + AlbumURL: item.Track.Album.ExternalURL.Spotify, + ArtistID: artistID, + ArtistURL: artistURL, + ArtistsData: artistsData, }) } @@ -496,12 +528,19 @@ func (c *SpotifyMetadataClient) formatPlaylistData(raw *playlistRaw) PlaylistRes func (c *SpotifyMetadataClient) formatAlbumData(ctx context.Context, raw *albumRaw) (*AlbumResponsePayload, error) { albumImage := firstImageURL(raw.Data.Images) + var artistID, artistURL string + if len(raw.Data.Artists) > 0 { + artistID = raw.Data.Artists[0].ID + artistURL = fmt.Sprintf("https://open.spotify.com/artist/%s", raw.Data.Artists[0].ID) + } info := AlbumInfoMetadata{ TotalTracks: raw.Data.TotalTracks, Name: raw.Data.Name, ReleaseDate: raw.Data.ReleaseDate, Artists: joinArtists(raw.Data.Artists), Images: albumImage, + ArtistID: artistID, + ArtistURL: artistURL, } if raw.BatchEnabled { info.Batch = strconv.Itoa(maxInt(1, raw.BatchCount)) @@ -576,6 +615,19 @@ func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context, for _, tr := range tracks { isrc := c.fetchTrackISRC(ctx, tr.ID, raw.Token, isrcCache) + var artistID, artistURL string + if len(tr.Artists) > 0 { + artistID = tr.Artists[0].ID + artistURL = fmt.Sprintf("https://open.spotify.com/artist/%s", tr.Artists[0].ID) + } + artistsData := make([]ArtistSimple, 0, len(tr.Artists)) + for _, a := range tr.Artists { + artistsData = append(artistsData, ArtistSimple{ + ID: a.ID, + Name: a.Name, + ExternalURL: fmt.Sprintf("https://open.spotify.com/artist/%s", a.ID), + }) + } allTracks = append(allTracks, AlbumTrackMetadata{ Artists: joinArtists(tr.Artists), Name: tr.Name, @@ -588,6 +640,11 @@ func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context, ExternalURL: tr.ExternalURL.Spotify, ISRC: isrc, SpotifyID: tr.ID, + AlbumID: alb.ID, + AlbumURL: alb.ExternalURL.Spotify, + ArtistID: artistID, + ArtistURL: artistURL, + ArtistsData: artistsData, }) } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b3f9a9e..193085d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,12 +5,11 @@ import { DialogContent, DialogDescription, DialogFooter, - DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; -import { Search } from "lucide-react"; +import { Search, X } from "lucide-react"; import { TooltipProvider } from "@/components/ui/tooltip"; import { getSettings, applyThemeMode } from "@/lib/settings"; import { applyTheme } from "@/lib/themes"; @@ -26,11 +25,15 @@ import { AlbumInfo } from "@/components/AlbumInfo"; import { PlaylistInfo } from "@/components/PlaylistInfo"; import { ArtistInfo } from "@/components/ArtistInfo"; import { DownloadProgressToast } from "@/components/DownloadProgressToast"; +import type { HistoryItem } from "@/components/FetchHistory"; // Hooks import { useDownload } from "@/hooks/useDownload"; import { useMetadata } from "@/hooks/useMetadata"; +const HISTORY_KEY = "spotiflac_fetch_history"; +const MAX_HISTORY = 5; + function App() { const [spotifyUrl, setSpotifyUrl] = useState(""); const [selectedTracks, setSelectedTracks] = useState([]); @@ -38,9 +41,10 @@ function App() { const [sortBy, setSortBy] = useState("default"); const [currentPage, setCurrentPage] = useState(1); const [hasUpdate, setHasUpdate] = useState(false); + const [fetchHistory, setFetchHistory] = useState([]); const ITEMS_PER_PAGE = 50; - const CURRENT_VERSION = "6.1"; + const CURRENT_VERSION = "6.2"; const download = useDownload(); const metadata = useMetadata(); @@ -61,6 +65,7 @@ function App() { mediaQuery.addEventListener("change", handleChange); checkForUpdates(); + loadHistory(); return () => { mediaQuery.removeEventListener("change", handleChange); @@ -78,12 +83,13 @@ function App() { const checkForUpdates = async () => { try { const response = await fetch( - "https://cdn.jsdelivr.net/gh/afkarxyz/SpotiFLAC@refs/heads/main/version.json" + "https://api.github.com/repos/afkarxyz/SpotiFLAC/releases/latest" ); const data = await response.json(); - const latestVersion = data.version; + // tag_name format: "v6.1" -> extract "6.1" + const latestVersion = data.tag_name?.replace(/^v/, "") || ""; - if (latestVersion > CURRENT_VERSION) { + if (latestVersion && latestVersion > CURRENT_VERSION) { setHasUpdate(true); } } catch (err) { @@ -91,6 +97,55 @@ function App() { } }; + const loadHistory = () => { + try { + const saved = localStorage.getItem(HISTORY_KEY); + if (saved) { + setFetchHistory(JSON.parse(saved)); + } + } catch (err) { + console.error("Failed to load history:", err); + } + }; + + const saveHistory = (history: HistoryItem[]) => { + try { + localStorage.setItem(HISTORY_KEY, JSON.stringify(history)); + } catch (err) { + console.error("Failed to save history:", err); + } + }; + + const addToHistory = (item: Omit) => { + setFetchHistory((prev) => { + const filtered = prev.filter((h) => h.url !== item.url); + const newItem: HistoryItem = { + ...item, + id: crypto.randomUUID(), + timestamp: Date.now(), + }; + const updated = [newItem, ...filtered].slice(0, MAX_HISTORY); + saveHistory(updated); + return updated; + }); + }; + + const removeFromHistory = (id: string) => { + setFetchHistory((prev) => { + const updated = prev.filter((h) => h.id !== id); + saveHistory(updated); + return updated; + }); + }; + + const handleHistorySelect = async (item: HistoryItem) => { + setSpotifyUrl(item.url); + const updatedUrl = await metadata.handleFetchMetadata(item.url); + if (updatedUrl) { + setSpotifyUrl(updatedUrl); + } + }; + const handleFetchMetadata = async () => { const updatedUrl = await metadata.handleFetchMetadata(spotifyUrl); if (updatedUrl) { @@ -98,6 +153,55 @@ function App() { } }; + // Add to history when metadata is successfully fetched + useEffect(() => { + if (!metadata.metadata || !spotifyUrl) return; + + let historyItem: Omit | null = null; + + if ("track" in metadata.metadata) { + const { track } = metadata.metadata; + historyItem = { + url: spotifyUrl, + type: "track", + name: track.name, + artist: track.artists, + image: track.images, + }; + } else if ("album_info" in metadata.metadata) { + const { album_info } = metadata.metadata; + historyItem = { + url: spotifyUrl, + type: "album", + name: album_info.name, + artist: album_info.artists, + image: album_info.images, + }; + } else if ("playlist_info" in metadata.metadata) { + const { playlist_info } = metadata.metadata; + historyItem = { + url: spotifyUrl, + type: "playlist", + name: playlist_info.owner.name, + artist: `${playlist_info.tracks.total} tracks • ${playlist_info.owner.display_name}`, + image: playlist_info.owner.images || "", + }; + } else if ("artist_info" in metadata.metadata) { + const { artist_info } = metadata.metadata; + historyItem = { + url: spotifyUrl, + type: "artist", + name: artist_info.name, + artist: `${artist_info.total_albums} albums`, + image: artist_info.images, + }; + } + + if (historyItem) { + addToHistory(historyItem); + } + }, [metadata.metadata]); + const handleSearchChange = (value: string) => { setSearchQuery(value); setCurrentPage(1); @@ -162,6 +266,7 @@ function App() { selectedTracks={selectedTracks} downloadedTracks={download.downloadedTracks} failedTracks={download.failedTracks} + skippedTracks={download.skippedTracks} downloadingTrack={download.downloadingTrack} isDownloading={download.isDownloading} bulkDownloadType={download.bulkDownloadType} @@ -181,6 +286,18 @@ function App() { onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} onPageChange={setCurrentPage} + onArtistClick={async (artist) => { + const artistUrl = await metadata.handleArtistClick(artist); + if (artistUrl) { + setSpotifyUrl(artistUrl); + } + }} + onTrackClick={async (track) => { + if (track.external_urls) { + setSpotifyUrl(track.external_urls); + await metadata.handleFetchMetadata(track.external_urls); + } + }} /> ); } @@ -196,6 +313,7 @@ function App() { selectedTracks={selectedTracks} downloadedTracks={download.downloadedTracks} failedTracks={download.failedTracks} + skippedTracks={download.skippedTracks} downloadingTrack={download.downloadingTrack} isDownloading={download.isDownloading} bulkDownloadType={download.bulkDownloadType} @@ -219,6 +337,19 @@ function App() { onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} onPageChange={setCurrentPage} + onAlbumClick={metadata.handleAlbumClick} + onArtistClick={async (artist) => { + const artistUrl = await metadata.handleArtistClick(artist); + if (artistUrl) { + setSpotifyUrl(artistUrl); + } + }} + onTrackClick={async (track) => { + if (track.external_urls) { + setSpotifyUrl(track.external_urls); + await metadata.handleFetchMetadata(track.external_urls); + } + }} /> ); } @@ -235,6 +366,7 @@ function App() { selectedTracks={selectedTracks} downloadedTracks={download.downloadedTracks} failedTracks={download.failedTracks} + skippedTracks={download.skippedTracks} downloadingTrack={download.downloadingTrack} isDownloading={download.isDownloading} bulkDownloadType={download.bulkDownloadType} @@ -254,7 +386,19 @@ function App() { onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} onAlbumClick={metadata.handleAlbumClick} + onArtistClick={async (artist) => { + const artistUrl = await metadata.handleArtistClick(artist); + if (artistUrl) { + setSpotifyUrl(artistUrl); + } + }} onPageChange={setCurrentPage} + onTrackClick={async (track) => { + if (track.external_urls) { + setSpotifyUrl(track.external_urls); + await metadata.handleFetchMetadata(track.external_urls); + } + }} /> ); } @@ -278,14 +422,27 @@ function App() { open={metadata.showTimeoutDialog} onOpenChange={metadata.setShowTimeoutDialog} > - - - Fetch Settings - - Set timeout for fetching metadata. Longer timeout is recommended for artists - with large discography. - - + +
+ +
+ Fetch Artist + + Set timeout for fetching metadata. Longer timeout is recommended for artists + with large discography. + + {metadata.pendingArtistName && ( +
+

{metadata.pendingArtistName}

+
+ )}
@@ -320,23 +477,36 @@ function App() { {/* Album Fetch Dialog */} - - - Fetch Album - - Do you want to fetch metadata for this album? - - + +
+ +
+ Fetch Album + + Do you want to fetch metadata for this album? + {metadata.selectedAlbum && ( -
-

{metadata.selectedAlbum.name}

+
+

{metadata.selectedAlbum.name}

)} - @@ -349,6 +519,10 @@ function App() { loading={metadata.loading} onUrlChange={setSpotifyUrl} onFetch={handleFetchMetadata} + history={fetchHistory} + onHistorySelect={handleHistorySelect} + onHistoryRemove={removeFromHistory} + hasResult={!!metadata.metadata} /> {metadata.metadata && renderMetadata()} diff --git a/frontend/src/components/AlbumInfo.tsx b/frontend/src/components/AlbumInfo.tsx index 05ffac6..302f66b 100644 --- a/frontend/src/components/AlbumInfo.tsx +++ b/frontend/src/components/AlbumInfo.tsx @@ -14,6 +14,8 @@ interface AlbumInfoProps { images: string; release_date: string; total_tracks: number; + artist_id?: string; + artist_url?: string; }; trackList: TrackMetadata[]; searchQuery: string; @@ -21,6 +23,7 @@ interface AlbumInfoProps { selectedTracks: string[]; downloadedTracks: Set; failedTracks: Set; + skippedTracks: Set; downloadingTrack: string | null; isDownloading: boolean; bulkDownloadType: "all" | "selected" | null; @@ -32,12 +35,14 @@ interface AlbumInfoProps { onSortChange: (value: string) => void; onToggleTrack: (isrc: string) => void; onToggleSelectAll: (tracks: TrackMetadata[]) => void; - onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string) => void; + onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string) => void; onDownloadAll: () => void; onDownloadSelected: () => void; onStopDownload: () => void; onOpenFolder: () => void; onPageChange: (page: number) => void; + onArtistClick?: (artist: { id: string; name: string; external_urls: string }) => void; + onTrackClick?: (track: TrackMetadata) => void; } export function AlbumInfo({ @@ -48,6 +53,7 @@ export function AlbumInfo({ selectedTracks, downloadedTracks, failedTracks, + skippedTracks, downloadingTrack, isDownloading, bulkDownloadType, @@ -65,6 +71,8 @@ export function AlbumInfo({ onStopDownload, onOpenFolder, onPageChange, + onArtistClick, + onTrackClick, }: AlbumInfoProps) { return (
@@ -83,7 +91,22 @@ export function AlbumInfo({

Album

{albumInfo.name}

- {albumInfo.artists} + {onArtistClick && albumInfo.artist_id && albumInfo.artist_url ? ( + + onArtistClick({ + id: albumInfo.artist_id!, + name: albumInfo.artists, + external_urls: albumInfo.artist_url!, + }) + } + > + {albumInfo.artists} + + ) : ( + {albumInfo.artists} + )} {albumInfo.release_date} @@ -148,16 +171,19 @@ export function AlbumInfo({ selectedTracks={selectedTracks} downloadedTracks={downloadedTracks} failedTracks={failedTracks} + skippedTracks={skippedTracks} downloadingTrack={downloadingTrack} isDownloading={isDownloading} currentPage={currentPage} itemsPerPage={itemsPerPage} showCheckboxes={true} hideAlbumColumn={true} + folderName={albumInfo.name} onToggleTrack={onToggleTrack} onToggleSelectAll={onToggleSelectAll} onDownloadTrack={onDownloadTrack} onPageChange={onPageChange} + onTrackClick={onTrackClick} />
diff --git a/frontend/src/components/ArtistInfo.tsx b/frontend/src/components/ArtistInfo.tsx index 8974cd1..d4e2eb3 100644 --- a/frontend/src/components/ArtistInfo.tsx +++ b/frontend/src/components/ArtistInfo.tsx @@ -28,6 +28,7 @@ interface ArtistInfoProps { selectedTracks: string[]; downloadedTracks: Set; failedTracks: Set; + skippedTracks: Set; downloadingTrack: string | null; isDownloading: boolean; bulkDownloadType: "all" | "selected" | null; @@ -39,13 +40,15 @@ interface ArtistInfoProps { onSortChange: (value: string) => void; onToggleTrack: (isrc: string) => void; onToggleSelectAll: (tracks: TrackMetadata[]) => void; - onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string) => void; + onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string) => void; onDownloadAll: () => void; onDownloadSelected: () => void; onStopDownload: () => void; onOpenFolder: () => void; onAlbumClick: (album: { id: string; name: string; external_urls: string }) => void; + onArtistClick: (artist: { id: string; name: string; external_urls: string }) => void; onPageChange: (page: number) => void; + onTrackClick?: (track: TrackMetadata) => void; } export function ArtistInfo({ @@ -57,6 +60,7 @@ export function ArtistInfo({ selectedTracks, downloadedTracks, failedTracks, + skippedTracks, downloadingTrack, isDownloading, bulkDownloadType, @@ -74,7 +78,9 @@ export function ArtistInfo({ onStopDownload, onOpenFolder, onAlbumClick, + onArtistClick, onPageChange, + onTrackClick, }: ArtistInfoProps) { return (
@@ -91,8 +97,12 @@ export function ArtistInfo({

Artist

{artistInfo.name}

-
+
{artistInfo.followers.toLocaleString()} followers + + {albumList.length} albums + + {trackList.length} tracks {artistInfo.genres.length > 0 && ( <> @@ -200,16 +210,22 @@ export function ArtistInfo({ selectedTracks={selectedTracks} downloadedTracks={downloadedTracks} failedTracks={failedTracks} + skippedTracks={skippedTracks} downloadingTrack={downloadingTrack} isDownloading={isDownloading} currentPage={currentPage} itemsPerPage={itemsPerPage} showCheckboxes={true} hideAlbumColumn={false} + folderName={artistInfo.name} + isArtistDiscography={true} onToggleTrack={onToggleTrack} onToggleSelectAll={onToggleSelectAll} onDownloadTrack={onDownloadTrack} onPageChange={onPageChange} + onAlbumClick={onAlbumClick} + onArtistClick={onArtistClick} + onTrackClick={onTrackClick} />
)} diff --git a/frontend/src/components/DebugLogger.tsx b/frontend/src/components/DebugLogger.tsx new file mode 100644 index 0000000..dd3ea32 --- /dev/null +++ b/frontend/src/components/DebugLogger.tsx @@ -0,0 +1,132 @@ +import { useState, useEffect, useRef } from "react"; +import { Bug, Trash2, X, Copy, Check } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { logger, type LogEntry } from "@/lib/logger"; + +const levelColors: Record = { + info: "text-blue-500", + success: "text-green-500", + warning: "text-yellow-500", + error: "text-red-500", + debug: "text-gray-500", +}; + +function formatTime(date: Date): string { + return date.toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); +} + +export function DebugLogger() { + const [open, setOpen] = useState(false); + const [logs, setLogs] = useState([]); + const [copied, setCopied] = useState(false); + const scrollRef = useRef(null); + + useEffect(() => { + const unsubscribe = logger.subscribe(() => { + setLogs(logger.getLogs()); + }); + setLogs(logger.getLogs()); + return () => { + unsubscribe(); + }; + }, []); + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [logs]); + + const handleClear = () => { + logger.clear(); + }; + + const handleCopy = async () => { + const logText = logs + .map((log) => `[${formatTime(log.timestamp)}] [${log.level}] ${log.message}`) + .join("\n"); + + try { + await navigator.clipboard.writeText(logText); + setCopied(true); + setTimeout(() => setCopied(false), 500); + } catch (err) { + console.error("Failed to copy logs:", err); + } + }; + + return ( + + + + + + Debug Logs +
+ + + +
+
+ {logs.length === 0 ? ( +

no logs yet...

+ ) : ( + logs.map((log, i) => ( +
+ + [{formatTime(log.timestamp)}] + + + [{log.level}] + + {log.message} +
+ )) + )} +
+
+
+ ); +} diff --git a/frontend/src/components/FetchHistory.tsx b/frontend/src/components/FetchHistory.tsx new file mode 100644 index 0000000..9d9ff83 --- /dev/null +++ b/frontend/src/components/FetchHistory.tsx @@ -0,0 +1,91 @@ +import { X } from "lucide-react"; + +export interface HistoryItem { + id: string; + url: string; + type: "track" | "album" | "playlist" | "artist"; + name: string; + artist: string; + image: string; + timestamp: number; +} + +interface FetchHistoryProps { + history: HistoryItem[]; + onSelect: (item: HistoryItem) => void; + onRemove: (id: string) => void; +} + +export function FetchHistory({ history, onSelect, onRemove }: FetchHistoryProps) { + if (history.length === 0) return null; + + const getTypeLabel = (type: string) => { + switch (type) { + case "track": + return "Track"; + case "album": + return "Album"; + case "playlist": + return "Playlist"; + case "artist": + return "Artist"; + default: + return type; + } + }; + + return ( +
+ Recent Fetches +
+ {history.map((item) => ( +
onSelect(item)} + > + +
+
+ {item.image ? ( + {item.name} + ) : ( +
+ No Image +
+ )} +
+
+

+ {item.name} +

+

+ {item.artist} +

+ + {getTypeLabel(item.type)} + +
+
+
+ ))} +
+
+ ); +} diff --git a/frontend/src/components/PlaylistInfo.tsx b/frontend/src/components/PlaylistInfo.tsx index 6e55b22..8f7d762 100644 --- a/frontend/src/components/PlaylistInfo.tsx +++ b/frontend/src/components/PlaylistInfo.tsx @@ -27,6 +27,7 @@ interface PlaylistInfoProps { selectedTracks: string[]; downloadedTracks: Set; failedTracks: Set; + skippedTracks: Set; downloadingTrack: string | null; isDownloading: boolean; bulkDownloadType: "all" | "selected" | null; @@ -38,12 +39,15 @@ interface PlaylistInfoProps { onSortChange: (value: string) => void; onToggleTrack: (isrc: string) => void; onToggleSelectAll: (tracks: TrackMetadata[]) => void; - onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string) => void; + onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string) => void; onDownloadAll: () => void; onDownloadSelected: () => void; onStopDownload: () => void; onOpenFolder: () => void; onPageChange: (page: number) => void; + onAlbumClick: (album: { id: string; name: string; external_urls: string }) => void; + onArtistClick: (artist: { id: string; name: string; external_urls: string }) => void; + onTrackClick: (track: TrackMetadata) => void; } export function PlaylistInfo({ @@ -54,6 +58,7 @@ export function PlaylistInfo({ selectedTracks, downloadedTracks, failedTracks, + skippedTracks, downloadingTrack, isDownloading, bulkDownloadType, @@ -71,6 +76,9 @@ export function PlaylistInfo({ onStopDownload, onOpenFolder, onPageChange, + onAlbumClick, + onArtistClick, + onTrackClick, }: PlaylistInfoProps) { return (
@@ -154,16 +162,21 @@ export function PlaylistInfo({ selectedTracks={selectedTracks} downloadedTracks={downloadedTracks} failedTracks={failedTracks} + skippedTracks={skippedTracks} downloadingTrack={downloadingTrack} isDownloading={isDownloading} currentPage={currentPage} itemsPerPage={itemsPerPage} showCheckboxes={true} hideAlbumColumn={false} + folderName={playlistInfo.owner.name} onToggleTrack={onToggleTrack} onToggleSelectAll={onToggleSelectAll} onDownloadTrack={onDownloadTrack} onPageChange={onPageChange} + onAlbumClick={onAlbumClick} + onArtistClick={onArtistClick} + onTrackClick={onTrackClick} />
diff --git a/frontend/src/components/SearchAndSort.tsx b/frontend/src/components/SearchAndSort.tsx index a2e14c6..32c401c 100644 --- a/frontend/src/components/SearchAndSort.tsx +++ b/frontend/src/components/SearchAndSort.tsx @@ -6,7 +6,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Search, ArrowUpDown } from "lucide-react"; +import { Search, ArrowUpDown, XCircle } from "lucide-react"; interface SearchAndSortProps { searchQuery: string; @@ -29,8 +29,17 @@ export function SearchAndSort({ placeholder="Search tracks..." value={searchQuery} onChange={(e) => onSearchChange(e.target.value)} - className="pl-10" + className="pl-10 pr-8" /> + {searchQuery && ( + + )}