This commit is contained in:
afkarxyz
2026-02-25 14:39:52 +07:00
parent fdca1ab461
commit 74001462b4
4 changed files with 1107 additions and 1258 deletions
+15 -15
View File
@@ -26,32 +26,32 @@
"@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.2.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.562.0", "lucide-react": "^0.575.0",
"motion": "^12.26.2", "motion": "^12.34.3",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.2.3", "react": "^19.2.4",
"react-dom": "^19.2.3", "react-dom": "^19.2.4",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.18" "tailwindcss": "^4.2.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.2", "@eslint/js": "^10.0.1",
"@types/node": "^25.0.8", "@types/node": "^25.3.0",
"@types/react": "^19.2.8", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^5.1.4",
"eslint": "^9.39.2", "eslint": "^10.0.2",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26", "eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.0.0", "globals": "^17.3.0",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"typescript-eslint": "^8.53.0", "typescript-eslint": "^8.56.1",
"vite": "^7.3.1" "vite": "^7.3.1"
} }
} }
+1 -1
View File
@@ -1 +1 @@
9fee02ec6592ede9ade4b36d56bd4d6d 3ca7ac3e41fb33a6fc3e30c16b39657b
+1080 -1208
View File
File diff suppressed because it is too large Load Diff
+6 -29
View File
@@ -14,7 +14,6 @@ import { useState, useMemo } from "react";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
interface ArtistInfoProps { interface ArtistInfoProps {
artistInfo: { artistInfo: {
name: string; name: string;
@@ -94,14 +93,12 @@ interface ArtistInfoProps {
onTrackClick?: (track: TrackMetadata) => void; onTrackClick?: (track: TrackMetadata) => void;
onBack?: () => void; onBack?: () => void;
} }
export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sortBy, selectedTracks, downloadedTracks, failedTracks, skippedTracks, downloadingTrack, isDownloading, bulkDownloadType, downloadProgress, currentDownloadInfo, currentPage, itemsPerPage, downloadedLyrics, failedLyrics, skippedLyrics, downloadingLyricsTrack, checkingAvailabilityTrack, availabilityMap, downloadedCovers, failedCovers, skippedCovers, downloadingCoverTrack, isBulkDownloadingCovers, isBulkDownloadingLyrics, onSearchChange, onSortChange, onToggleTrack, onToggleSelectAll, onDownloadTrack, onDownloadLyrics, onDownloadCover, onCheckAvailability, onDownloadAllLyrics, onDownloadAllCovers, onDownloadAll, onDownloadSelected, onStopDownload, onOpenFolder, onAlbumClick, onArtistClick, onPageChange, onTrackClick, onBack, }: ArtistInfoProps) { export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sortBy, selectedTracks, downloadedTracks, failedTracks, skippedTracks, downloadingTrack, isDownloading, bulkDownloadType, downloadProgress, currentDownloadInfo, currentPage, itemsPerPage, downloadedLyrics, failedLyrics, skippedLyrics, downloadingLyricsTrack, checkingAvailabilityTrack, availabilityMap, downloadedCovers, failedCovers, skippedCovers, downloadingCoverTrack, isBulkDownloadingCovers, isBulkDownloadingLyrics, onSearchChange, onSortChange, onToggleTrack, onToggleSelectAll, onDownloadTrack, onDownloadLyrics, onDownloadCover, onCheckAvailability, onDownloadAllLyrics, onDownloadAllCovers, onDownloadAll, onDownloadSelected, onStopDownload, onOpenFolder, onAlbumClick, onArtistClick, onPageChange, onTrackClick, onBack, }: ArtistInfoProps) {
const [downloadingHeader, setDownloadingHeader] = useState(false); const [downloadingHeader, setDownloadingHeader] = useState(false);
const [downloadingAvatar, setDownloadingAvatar] = useState(false); const [downloadingAvatar, setDownloadingAvatar] = useState(false);
const [downloadingGalleryIndex, setDownloadingGalleryIndex] = useState<number | null>(null); const [downloadingGalleryIndex, setDownloadingGalleryIndex] = useState<number | null>(null);
const [downloadingAllGallery, setDownloadingAllGallery] = useState(false); const [downloadingAllGallery, setDownloadingAllGallery] = useState(false);
const [activeTab, setActiveTab] = useState<"albums" | "tracks" | "gallery">("albums"); const [activeTab, setActiveTab] = useState<"albums" | "tracks" | "gallery">("albums");
const filteredAlbumGroups = useMemo(() => { const filteredAlbumGroups = useMemo(() => {
const albumTypeMap = new Map(albumList.map(a => [a.name, a.album_type])); const albumTypeMap = new Map(albumList.map(a => [a.name, a.album_type]));
const albumGroups = trackList.reduce((acc, track) => { const albumGroups = trackList.reduce((acc, track) => {
@@ -128,7 +125,6 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
return dateB.localeCompare(dateA); return dateB.localeCompare(dateA);
}); });
}, [trackList, albumList]); }, [trackList, albumList]);
const handleDownloadHeader = async () => { const handleDownloadHeader = async () => {
if (!artistInfo.header) if (!artistInfo.header)
return; return;
@@ -159,7 +155,6 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
setDownloadingHeader(false); setDownloadingHeader(false);
} }
}; };
const handleDownloadAvatar = async () => { const handleDownloadAvatar = async () => {
if (!artistInfo.images) if (!artistInfo.images)
return; return;
@@ -190,7 +185,6 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
setDownloadingAvatar(false); setDownloadingAvatar(false);
} }
}; };
const handleDownloadGalleryImage = async (imageUrl: string, index: number) => { const handleDownloadGalleryImage = async (imageUrl: string, index: number) => {
setDownloadingGalleryIndex(index); setDownloadingGalleryIndex(index);
try { try {
@@ -220,7 +214,6 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
setDownloadingGalleryIndex(null); setDownloadingGalleryIndex(null);
} }
}; };
const handleDownloadAllGallery = async () => { const handleDownloadAllGallery = async () => {
if (!artistInfo.gallery || artistInfo.gallery.length === 0) if (!artistInfo.gallery || artistInfo.gallery.length === 0)
return; return;
@@ -277,9 +270,7 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
setDownloadingAllGallery(false); setDownloadingAllGallery(false);
} }
}; };
const hasGallery = artistInfo.gallery && artistInfo.gallery.length > 0; const hasGallery = artistInfo.gallery && artistInfo.gallery.length > 0;
return (<div className="space-y-6"> return (<div className="space-y-6">
<Card className="overflow-hidden p-0 relative"> <Card className="overflow-hidden p-0 relative">
{artistInfo.header ? (<> {artistInfo.header ? (<>
@@ -462,42 +453,28 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
{isDownloading && bulkDownloadType === "all" ? (<Spinner />) : (<Download className="h-4 w-4"/>)} {isDownloading && bulkDownloadType === "all" ? (<Spinner />) : (<Download className="h-4 w-4"/>)}
Download Discography Download Discography
</Button> </Button>
{selectedTracks.length > 0 && ( {selectedTracks.length > 0 && (<Button onClick={onDownloadSelected} size="sm" variant="secondary" disabled={isDownloading}>
<Button onClick={onDownloadSelected} size="sm" variant="secondary" disabled={isDownloading}>
{isDownloading && bulkDownloadType === "selected" ? (<Spinner />) : (<Download className="h-4 w-4"/>)} {isDownloading && bulkDownloadType === "selected" ? (<Spinner />) : (<Download className="h-4 w-4"/>)}
Download Selected ({selectedTracks.length}) Download Selected ({selectedTracks.length})
</Button> </Button>)}
)}
</div> </div>
</div> </div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4"> <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{albumList.map((album) => { {albumList.map((album) => {
// Filter tracks for this specific album to handle selection
const albumTracks = trackList.filter(t => t.album_name === album.name); const albumTracks = trackList.filter(t => t.album_name === album.name);
const tracksWithId = albumTracks.filter(t => t.spotify_id); const tracksWithId = albumTracks.filter(t => t.spotify_id);
// Check if all tracks in this album are currently selected
const isSelected = tracksWithId.length > 0 && tracksWithId.every(t => selectedTracks.includes(t.spotify_id!)); const isSelected = tracksWithId.length > 0 && tracksWithId.every(t => selectedTracks.includes(t.spotify_id!));
const hasTracks = tracksWithId.length > 0; const hasTracks = tracksWithId.length > 0;
return (<div key={album.id} className="group cursor-pointer relative" onClick={() => onAlbumClick({ return (<div key={album.id} className="group cursor-pointer relative" onClick={() => onAlbumClick({
id: album.id, id: album.id,
name: album.name, name: album.name,
external_urls: album.external_urls, external_urls: album.external_urls,
})}> })}>
<div className="relative mb-2"> <div className="relative mb-2">
{/* Checkbox for Album Selection */}
{hasTracks && ( {hasTracks && (<div className={`absolute top-2 left-2 z-20 ${isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} transition-opacity`} onClick={(e) => e.stopPropagation()}>
<div <Checkbox checked={isSelected} onCheckedChange={() => onToggleSelectAll(albumTracks)} className="bg-black/50 border-white/70 data-[state=checked]:bg-primary data-[state=checked]:border-primary"/>
className={`absolute top-2 left-2 z-20 ${isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} transition-opacity`} </div>)}
onClick={(e) => e.stopPropagation()}
>
<Checkbox
checked={isSelected}
onCheckedChange={() => onToggleSelectAll(albumTracks)}
className="bg-black/50 border-white/70 data-[state=checked]:bg-primary data-[state=checked]:border-primary"
/>
</div>
)}
{album.images && (<img src={album.images} alt={album.name} className="w-full aspect-square object-cover rounded-md shadow-md transition-shadow group-hover:shadow-xl"/>)} {album.images && (<img src={album.images} alt={album.name} className="w-full aspect-square object-cover rounded-md shadow-md transition-shadow group-hover:shadow-xl"/>)}
<div className="absolute bottom-2 right-2"> <div className="absolute bottom-2 right-2">
<span className="text-[10px] uppercase font-bold px-1.5 py-0.5 rounded bg-black/60 text-white backdrop-blur-[2px]"> <span className="text-[10px] uppercase font-bold px-1.5 py-0.5 rounded bg-black/60 text-white backdrop-blur-[2px]">