.final
This commit is contained in:
+91
-2
@@ -36,6 +36,80 @@ import { useDownloadQueueDialog } from "@/hooks/useDownloadQueueDialog";
|
||||
import { useDownloadProgress } from "@/hooks/useDownloadProgress";
|
||||
const HISTORY_KEY = "spotiflac_fetch_history";
|
||||
const MAX_HISTORY = 5;
|
||||
|
||||
function extractSpotifyEntityFromURL(url: string): { type: string; id: string; } | null {
|
||||
const trimmed = url.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const spotifyUriMatch = trimmed.match(/^spotify:(track|album|playlist|artist):([A-Za-z0-9]+)$/i);
|
||||
if (spotifyUriMatch) {
|
||||
return {
|
||||
type: spotifyUriMatch[1].toLowerCase(),
|
||||
id: spotifyUriMatch[2],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
const segments = parsed.pathname.split("/").filter(Boolean);
|
||||
const supportedTypes = new Set(["track", "album", "playlist", "artist"]);
|
||||
for (let i = 0; i < segments.length - 1; i++) {
|
||||
const segment = segments[i].toLowerCase();
|
||||
if (!supportedTypes.has(segment)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const id = segments[i + 1];
|
||||
if (id) {
|
||||
return { type: segment, id };
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeHistoryURL(url: string): string {
|
||||
const trimmed = url.trim();
|
||||
if (!trimmed)
|
||||
return trimmed;
|
||||
|
||||
const withoutQuery = trimmed.split("?")[0].replace(/\/+$/, "");
|
||||
const spotifyEntity = extractSpotifyEntityFromURL(withoutQuery);
|
||||
if (spotifyEntity) {
|
||||
return `https://open.spotify.com/${spotifyEntity.type}/${spotifyEntity.id}`;
|
||||
}
|
||||
return withoutQuery.replace(/(\/artist\/[A-Za-z0-9]+)\/discography\/all$/i, "$1");
|
||||
}
|
||||
|
||||
function getHistoryIdentityKey(type: HistoryItem["type"], url: string): string {
|
||||
const normalizedUrl = normalizeHistoryURL(url);
|
||||
const spotifyEntity = extractSpotifyEntityFromURL(normalizedUrl);
|
||||
if (spotifyEntity) {
|
||||
return `${type}:${spotifyEntity.id}`;
|
||||
}
|
||||
|
||||
return `${type}:${normalizedUrl}`;
|
||||
}
|
||||
|
||||
function dedupeHistoryItems(items: HistoryItem[]): HistoryItem[] {
|
||||
const seen = new Set<string>();
|
||||
const deduped: HistoryItem[] = [];
|
||||
for (const item of items) {
|
||||
const normalizedUrl = normalizeHistoryURL(item.url);
|
||||
const key = getHistoryIdentityKey(item.type, normalizedUrl);
|
||||
if (seen.has(key))
|
||||
continue;
|
||||
seen.add(key);
|
||||
deduped.push({ ...item, url: normalizedUrl });
|
||||
}
|
||||
return deduped;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [currentPage, setCurrentPage] = useState<PageType>("main");
|
||||
const [spotifyUrl, setSpotifyUrl] = useState("");
|
||||
@@ -167,7 +241,9 @@ function App() {
|
||||
try {
|
||||
const saved = localStorage.getItem(HISTORY_KEY);
|
||||
if (saved) {
|
||||
setFetchHistory(JSON.parse(saved));
|
||||
const deduped = dedupeHistoryItems(JSON.parse(saved));
|
||||
setFetchHistory(deduped);
|
||||
localStorage.setItem(HISTORY_KEY, JSON.stringify(deduped));
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
@@ -222,9 +298,12 @@ function App() {
|
||||
};
|
||||
const addToHistory = (item: Omit<HistoryItem, "id" | "timestamp">) => {
|
||||
setFetchHistory((prev) => {
|
||||
const filtered = prev.filter((h) => h.url !== item.url);
|
||||
const normalizedUrl = normalizeHistoryURL(item.url);
|
||||
const identityKey = getHistoryIdentityKey(item.type, normalizedUrl);
|
||||
const filtered = prev.filter((h) => getHistoryIdentityKey(h.type, h.url) !== identityKey);
|
||||
const newItem: HistoryItem = {
|
||||
...item,
|
||||
url: normalizedUrl,
|
||||
id: crypto.randomUUID(),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
@@ -345,6 +424,8 @@ function App() {
|
||||
if ("album_info" in metadata.metadata) {
|
||||
const { album_info, track_list } = metadata.metadata;
|
||||
return (<AlbumInfo albumInfo={album_info} trackList={track_list} searchQuery={searchQuery} sortBy={sortBy} selectedTracks={selectedTracks} downloadedTracks={download.downloadedTracks} failedTracks={download.failedTracks} skippedTracks={download.skippedTracks} downloadingTrack={download.downloadingTrack} isDownloading={download.isDownloading} bulkDownloadType={download.bulkDownloadType} downloadProgress={download.downloadProgress} currentDownloadInfo={download.currentDownloadInfo} currentPage={currentListPage} itemsPerPage={ITEMS_PER_PAGE} downloadedLyrics={lyrics.downloadedLyrics} failedLyrics={lyrics.failedLyrics} skippedLyrics={lyrics.skippedLyrics} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} checkingAvailabilityTrack={availability.checkingTrackId} availabilityMap={availability.availabilityMap} downloadedCovers={cover.downloadedCovers} failedCovers={cover.failedCovers} skippedCovers={cover.skippedCovers} downloadingCoverTrack={cover.downloadingCoverTrack} isBulkDownloadingCovers={cover.isBulkDownloadingCovers} isBulkDownloadingLyrics={lyrics.isBulkDownloadingLyrics} onSearchChange={handleSearchChange} onSortChange={setSortBy} onToggleTrack={toggleTrackSelection} onToggleSelectAll={toggleSelectAll} onDownloadTrack={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position, albumArtist, releaseDate, discNumber) => lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, album_info.name, position, albumArtist, releaseDate, discNumber, true)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, album_info.name, position, trackId, albumArtist, releaseDate, discNumber, true)} onCheckAvailability={availability.checkAvailability} onDownloadAllLyrics={() => lyrics.handleDownloadAllLyrics(track_list, album_info.name, undefined, true)} onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, album_info.name, true)} onDownloadAll={() => download.handleDownloadAll(track_list, album_info.name, true)} onDownloadSelected={() => download.handleDownloadSelected(selectedTracks, track_list, album_info.name, true)} onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} onPageChange={setCurrentListPage} onBack={metadata.resetMetadata} onArtistClick={async (artist) => {
|
||||
const pendingArtistUrl = artist.external_urls.replace(/\/$/, "") + "/discography/all";
|
||||
setSpotifyUrl(pendingArtistUrl);
|
||||
const artistUrl = await metadata.handleArtistClick(artist);
|
||||
if (artistUrl) {
|
||||
setSpotifyUrl(artistUrl);
|
||||
@@ -359,6 +440,8 @@ function App() {
|
||||
if ("playlist_info" in metadata.metadata) {
|
||||
const { playlist_info, track_list } = metadata.metadata;
|
||||
return (<PlaylistInfo playlistInfo={playlist_info} trackList={track_list} searchQuery={searchQuery} sortBy={sortBy} selectedTracks={selectedTracks} downloadedTracks={download.downloadedTracks} failedTracks={download.failedTracks} skippedTracks={download.skippedTracks} downloadingTrack={download.downloadingTrack} isDownloading={download.isDownloading} bulkDownloadType={download.bulkDownloadType} downloadProgress={download.downloadProgress} currentDownloadInfo={download.currentDownloadInfo} currentPage={currentListPage} itemsPerPage={ITEMS_PER_PAGE} downloadedLyrics={lyrics.downloadedLyrics} failedLyrics={lyrics.failedLyrics} skippedLyrics={lyrics.skippedLyrics} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} checkingAvailabilityTrack={availability.checkingTrackId} availabilityMap={availability.availabilityMap} downloadedCovers={cover.downloadedCovers} failedCovers={cover.failedCovers} skippedCovers={cover.skippedCovers} downloadingCoverTrack={cover.downloadingCoverTrack} isBulkDownloadingCovers={cover.isBulkDownloadingCovers} isBulkDownloadingLyrics={lyrics.isBulkDownloadingLyrics} onSearchChange={handleSearchChange} onSortChange={setSortBy} onToggleTrack={toggleTrackSelection} onToggleSelectAll={toggleSelectAll} onDownloadTrack={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position, albumArtist, releaseDate, discNumber) => lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, playlist_info.owner.name, position, albumArtist, releaseDate, discNumber)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, playlist_info.owner.name, position, trackId, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onDownloadAllLyrics={() => lyrics.handleDownloadAllLyrics(track_list, playlist_info.owner.name)} onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, playlist_info.owner.name)} onDownloadAll={() => download.handleDownloadAll(track_list, playlist_info.owner.name)} onDownloadSelected={() => download.handleDownloadSelected(selectedTracks, track_list, playlist_info.owner.name)} onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} onPageChange={setCurrentListPage} onBack={metadata.resetMetadata} onAlbumClick={metadata.handleAlbumClick} onArtistClick={async (artist) => {
|
||||
const pendingArtistUrl = artist.external_urls.replace(/\/$/, "") + "/discography/all";
|
||||
setSpotifyUrl(pendingArtistUrl);
|
||||
const artistUrl = await metadata.handleArtistClick(artist);
|
||||
if (artistUrl) {
|
||||
setSpotifyUrl(artistUrl);
|
||||
@@ -373,6 +456,8 @@ function App() {
|
||||
if ("artist_info" in metadata.metadata) {
|
||||
const { artist_info, album_list, track_list } = metadata.metadata;
|
||||
return (<ArtistInfo artistInfo={artist_info} albumList={album_list} trackList={track_list} searchQuery={searchQuery} sortBy={sortBy} selectedTracks={selectedTracks} downloadedTracks={download.downloadedTracks} failedTracks={download.failedTracks} skippedTracks={download.skippedTracks} downloadingTrack={download.downloadingTrack} isDownloading={download.isDownloading} bulkDownloadType={download.bulkDownloadType} downloadProgress={download.downloadProgress} currentDownloadInfo={download.currentDownloadInfo} currentPage={currentListPage} itemsPerPage={ITEMS_PER_PAGE} downloadedLyrics={lyrics.downloadedLyrics} failedLyrics={lyrics.failedLyrics} skippedLyrics={lyrics.skippedLyrics} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} checkingAvailabilityTrack={availability.checkingTrackId} availabilityMap={availability.availabilityMap} downloadedCovers={cover.downloadedCovers} failedCovers={cover.failedCovers} skippedCovers={cover.skippedCovers} downloadingCoverTrack={cover.downloadingCoverTrack} isBulkDownloadingCovers={cover.isBulkDownloadingCovers} isBulkDownloadingLyrics={lyrics.isBulkDownloadingLyrics} onSearchChange={handleSearchChange} onSortChange={setSortBy} onToggleTrack={toggleTrackSelection} onToggleSelectAll={toggleSelectAll} onDownloadTrack={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position, albumArtist, releaseDate, discNumber) => lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, artist_info.name, position, albumArtist, releaseDate, discNumber)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, artist_info.name, position, trackId, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onDownloadAllLyrics={() => lyrics.handleDownloadAllLyrics(track_list, artist_info.name)} onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, artist_info.name)} onDownloadAll={() => download.handleDownloadAll(track_list, artist_info.name)} onDownloadSelected={() => download.handleDownloadSelected(selectedTracks, track_list, artist_info.name)} onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} onAlbumClick={metadata.handleAlbumClick} onBack={metadata.resetMetadata} onArtistClick={async (artist) => {
|
||||
const pendingArtistUrl = artist.external_urls.replace(/\/$/, "") + "/discography/all";
|
||||
setSpotifyUrl(pendingArtistUrl);
|
||||
const artistUrl = await metadata.handleArtistClick(artist);
|
||||
if (artistUrl) {
|
||||
setSpotifyUrl(artistUrl);
|
||||
@@ -459,6 +544,10 @@ function App() {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={async () => {
|
||||
const pendingAlbumUrl = metadata.selectedAlbum?.external_urls;
|
||||
if (pendingAlbumUrl) {
|
||||
setSpotifyUrl(pendingAlbumUrl);
|
||||
}
|
||||
const albumUrl = await metadata.handleConfirmAlbumFetch();
|
||||
if (albumUrl) {
|
||||
setSpotifyUrl(albumUrl);
|
||||
|
||||
@@ -122,7 +122,7 @@ export function AlbumInfo({ albumInfo, trackList, searchQuery, sortBy, selectedT
|
||||
if (response.already_exists)
|
||||
toast.info("Cover already exists");
|
||||
else
|
||||
toast.success("Album cover downloaded");
|
||||
toast.success("Separate album cover downloaded");
|
||||
}
|
||||
else {
|
||||
toast.error(response.error || "Failed to download cover");
|
||||
@@ -153,7 +153,7 @@ export function AlbumInfo({ albumInfo, trackList, searchQuery, sortBy, selectedT
|
||||
{downloadingAlbumCover ? <Spinner /> : <ImageDown className="h-4 w-4"/>}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Download Album Cover</p></TooltipContent>
|
||||
<TooltipContent><p>Download Separate Album Cover</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>)}
|
||||
@@ -203,7 +203,7 @@ export function AlbumInfo({ albumInfo, trackList, searchQuery, sortBy, selectedT
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Download All Covers</p>
|
||||
<p>Download All Separate Covers</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>)}
|
||||
{downloadedTracks.size > 0 && (<Button onClick={onOpenFolder} variant="outline">
|
||||
|
||||
@@ -100,7 +100,31 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
|
||||
const [downloadingGalleryIndex, setDownloadingGalleryIndex] = useState<number | null>(null);
|
||||
const [downloadingAllGallery, setDownloadingAllGallery] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<"albums" | "tracks" | "gallery">("albums");
|
||||
const [activeAlbumFilter, setActiveAlbumFilter] = useState<string>("all");
|
||||
const displayedAlbumCount = artistInfo.total_albums || albumList.length;
|
||||
const albumFilterCounts = useMemo(() => {
|
||||
const counts = new Map<string, number>();
|
||||
counts.set("all", (albumList || []).length);
|
||||
for (const album of albumList || []) {
|
||||
const type = (album.album_type || "").trim().toLowerCase();
|
||||
if (!type)
|
||||
continue;
|
||||
counts.set(type, (counts.get(type) || 0) + 1);
|
||||
}
|
||||
return counts;
|
||||
}, [albumList]);
|
||||
const albumFilters = useMemo(() => {
|
||||
const uniqueTypes = Array.from(new Set((albumList || [])
|
||||
.map((album) => (album.album_type || "").trim().toLowerCase())
|
||||
.filter(Boolean)));
|
||||
return ["all", ...uniqueTypes];
|
||||
}, [albumList]);
|
||||
const filteredAlbums = useMemo(() => {
|
||||
if (activeAlbumFilter === "all") {
|
||||
return albumList || [];
|
||||
}
|
||||
return (albumList || []).filter((album) => (album.album_type || "").trim().toLowerCase() === activeAlbumFilter);
|
||||
}, [albumList, activeAlbumFilter]);
|
||||
const filteredAlbumGroups = useMemo(() => {
|
||||
const albumTypeMap = new Map(albumList.map(a => [a.name, a.album_type]));
|
||||
const albumGroups = trackList.reduce((acc, track) => {
|
||||
@@ -127,6 +151,17 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
|
||||
return dateB.localeCompare(dateA);
|
||||
});
|
||||
}, [trackList, albumList]);
|
||||
const formatAlbumFilterLabel = (value: string) => {
|
||||
const count = albumFilterCounts.get(value) || 0;
|
||||
if (value === "all")
|
||||
return `All (${count})`;
|
||||
const label = value
|
||||
.split(/[_\s]+/)
|
||||
.filter(Boolean)
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join(" ");
|
||||
return `${label} (${count})`;
|
||||
};
|
||||
const handleDownloadHeader = async () => {
|
||||
if (!artistInfo.header)
|
||||
return;
|
||||
@@ -461,8 +496,13 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
|
||||
</Button>)}
|
||||
</div>
|
||||
</div>
|
||||
{albumFilters.length > 1 && (<div className="flex flex-wrap gap-2">
|
||||
{albumFilters.map((filter) => (<Button key={filter} size="sm" variant={activeAlbumFilter === filter ? "default" : "outline"} onClick={() => setActiveAlbumFilter(filter)}>
|
||||
{formatAlbumFilterLabel(filter)}
|
||||
</Button>))}
|
||||
</div>)}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
{albumList.map((album) => {
|
||||
{filteredAlbums.map((album) => {
|
||||
const albumTracks = trackList.filter(t => t.album_name === album.name);
|
||||
const tracksWithId = albumTracks.filter(t => t.spotify_id);
|
||||
const isSelected = tracksWithId.length > 0 && tracksWithId.every(t => selectedTracks.includes(t.spotify_id!));
|
||||
@@ -495,6 +535,9 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
|
||||
</div>);
|
||||
})}
|
||||
</div>
|
||||
{filteredAlbums.length === 0 && (<div className="rounded-lg border border-dashed border-border p-6 text-sm text-muted-foreground">
|
||||
No releases found for the selected discography filter.
|
||||
</div>)}
|
||||
</div>)}
|
||||
|
||||
{activeTab === "tracks" && trackList.length > 0 && (<div className="space-y-4">
|
||||
@@ -564,7 +607,7 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Download All Covers</p>
|
||||
<p>Download All Separate Covers</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>)}
|
||||
{downloadedTracks.size > 0 && (<Button onClick={onOpenFolder} size="sm" variant="outline">
|
||||
|
||||
@@ -13,18 +13,18 @@ export function DownloadProgressToast({ onClick }: DownloadProgressToastProps) {
|
||||
return null;
|
||||
}
|
||||
return (<div className="fixed bottom-4 left-[calc(56px+1rem)] z-50 animate-in slide-in-from-bottom-5 data-[state=closed]:animate-out data-[state=closed]:slide-out-to-bottom-5">
|
||||
<Button variant="outline" className="bg-background border rounded-lg shadow-lg p-3 h-auto hover:bg-muted/50 transition-colors cursor-pointer" onClick={onClick}>
|
||||
<Button variant="outline" className="h-auto cursor-pointer rounded-lg border-border bg-background p-3 text-foreground shadow-lg transition-colors hover:bg-muted dark:border-blue-800 dark:bg-blue-950 dark:text-blue-100 dark:hover:bg-blue-900" onClick={onClick}>
|
||||
<div className="flex items-center gap-3">
|
||||
<Download className={`h-4 w-4 text-primary ${progress.is_downloading ? 'animate-bounce' : ''}`}/>
|
||||
<Download className={`h-4 w-4 text-blue-600 dark:text-blue-400 ${progress.is_downloading ? 'animate-bounce' : ''}`}/>
|
||||
<div className="flex flex-col min-w-[80px]">
|
||||
<p className="text-sm font-medium font-mono tabular-nums">
|
||||
{progress.mb_downloaded.toFixed(2)} MB
|
||||
</p>
|
||||
{progress.speed_mbps > 0 && (<p className="text-xs text-muted-foreground font-mono tabular-nums">
|
||||
{progress.speed_mbps > 0 && (<p className="text-xs font-mono tabular-nums text-muted-foreground dark:text-blue-300">
|
||||
{progress.speed_mbps.toFixed(2)} MB/s
|
||||
</p>)}
|
||||
</div>
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground ml-1"/>
|
||||
<ChevronRight className="ml-1 h-4 w-4 text-muted-foreground dark:text-blue-300"/>
|
||||
</div>
|
||||
</Button>
|
||||
</div>);
|
||||
|
||||
@@ -134,7 +134,7 @@ export function PlaylistInfo({ playlistInfo, trackList, searchQuery, sortBy, sel
|
||||
if (response.already_exists)
|
||||
toast.info("Cover already exists");
|
||||
else
|
||||
toast.success("Playlist cover downloaded");
|
||||
toast.success("Separate playlist cover downloaded");
|
||||
}
|
||||
else {
|
||||
toast.error(response.error || "Failed to download cover");
|
||||
@@ -165,7 +165,7 @@ export function PlaylistInfo({ playlistInfo, trackList, searchQuery, sortBy, sel
|
||||
{downloadingPlaylistCover ? <Spinner /> : <ImageDown className="h-4 w-4"/>}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><p>Download Playlist Cover</p></TooltipContent>
|
||||
<TooltipContent><p>Download Separate Playlist Cover</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>)}
|
||||
@@ -213,7 +213,7 @@ export function PlaylistInfo({ playlistInfo, trackList, searchQuery, sortBy, sel
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Download All Covers</p>
|
||||
<p>Download All Separate Covers</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>)}
|
||||
{downloadedTracks.size > 0 && (<Button onClick={onOpenFolder} variant="outline">
|
||||
|
||||
Reference in New Issue
Block a user