This commit is contained in:
afkarxyz
2026-01-15 13:20:04 +07:00
parent b160d3c790
commit 1e99d8b5c6
5 changed files with 323 additions and 153 deletions
+17 -1
View File
@@ -714,6 +714,17 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter
} }
} }
if albumArtistsString == "" {
albumArtists := extractArtists(getMap(albumData, "artists"))
if len(albumArtists) > 0 {
albumArtistNames := []string{}
for _, artist := range albumArtists {
albumArtistNames = append(albumArtistNames, getString(artist, "name"))
}
albumArtistsString = strings.Join(albumArtistNames, ", ")
}
}
albumInfo = map[string]interface{}{ albumInfo = map[string]interface{}{
"id": albumID, "id": albumID,
"name": getString(albumData, "name"), "name": getString(albumData, "name"),
@@ -1228,6 +1239,11 @@ func extractDiscographyItems(itemsData map[string]interface{}) []map[string]inte
return items return items
} }
func stripHTMLTags(s string) string {
re := regexp.MustCompile(`<[^>]*>`)
return re.ReplaceAllString(s, "")
}
func FilterArtist(data map[string]interface{}) map[string]interface{} { func FilterArtist(data map[string]interface{}) map[string]interface{} {
dataMap := getMap(data, "data") dataMap := getMap(data, "data")
artistData := getMap(dataMap, "artistUnion") artistData := getMap(dataMap, "artistUnion")
@@ -1243,7 +1259,7 @@ func FilterArtist(data map[string]interface{}) map[string]interface{} {
if ok { if ok {
biographyText := getString(biographyMap, "text") biographyText := getString(biographyMap, "text")
if biographyText != "" { if biographyText != "" {
profile["biography"] = html.UnescapeString(biographyText) profile["biography"] = html.UnescapeString(stripHTMLTags(biographyText))
} }
} }
} }
+18
View File
@@ -0,0 +1,18 @@
export const langColors: Record<string, string> = {
"TypeScript": "#2b7489",
"Go": "#375eab",
"Python": "#3572A5",
"CSS": "#563d7c",
"HTML": "#e44b23",
"JavaScript": "#f1e05a",
"Java": "#b07219",
"C": "#555555",
"C Sharp": "#178600",
"cpp": "#f34b7d",
"Ruby": "#701516",
"PHP": "#4F5D95",
"Swift": "#ffac45",
"Kotlin": "#F18E33",
"Rust": "#dea584",
"Shell": "#89e051"
};
+134 -2
View File
@@ -8,11 +8,12 @@ import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { Bug, Lightbulb, ExternalLink } from "lucide-react"; import { Bug, Lightbulb, ExternalLink, Star, GitFork, Clock, Download } from "lucide-react";
import ExyezedIcon from "@/assets/icons/exyezed.svg"; import ExyezedIcon from "@/assets/icons/exyezed.svg";
import SpotubeDLIcon from "@/assets/icons/spotubedl.svg"; import SpotubeDLIcon from "@/assets/icons/spotubedl.svg";
import SpotiDownloaderIcon from "@/assets/icons/spotidownloader.svg"; import SpotiDownloaderIcon from "@/assets/icons/spotidownloader.svg";
import XBatchDLIcon from "@/assets/icons/xbatchdl.svg"; import XBatchDLIcon from "@/assets/icons/xbatchdl.svg";
import { langColors } from "@/assets/github-lang-colors";
interface AboutPageProps { interface AboutPageProps {
version: string; version: string;
} }
@@ -27,6 +28,7 @@ export function AboutPage({ version }: AboutPageProps) {
const [featureDesc, setFeatureDesc] = useState(""); const [featureDesc, setFeatureDesc] = useState("");
const [useCase, setUseCase] = useState(""); const [useCase, setUseCase] = useState("");
const [featureContext, setFeatureContext] = useState(""); const [featureContext, setFeatureContext] = useState("");
const [repoStats, setRepoStats] = useState<Record<string, any>>({});
useEffect(() => { useEffect(() => {
const fetchOS = async () => { const fetchOS = async () => {
try { try {
@@ -66,6 +68,80 @@ export function AboutPage({ version }: AboutPageProps) {
} }
}; };
fetchLocation(); fetchLocation();
const fetchRepoStats = async () => {
const CACHE_KEY = 'github_repo_stats';
const CACHE_DURATION = 1000 * 60 * 60;
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
try {
const { data, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < CACHE_DURATION) {
setRepoStats(data);
return;
}
}
catch (err) {
console.error('Failed to parse cache:', err);
}
}
const repos = [
{ name: 'SpotiDownloader', owner: 'afkarxyz' },
{ name: 'Twitter-X-Media-Batch-Downloader', owner: 'afkarxyz' }
];
const stats: Record<string, any> = {};
for (const repo of repos) {
try {
const [repoRes, releasesRes, langsRes] = await Promise.all([
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}`),
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/releases`),
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/languages`)
]);
if (repoRes.status === 403) {
if (cached) {
const { data } = JSON.parse(cached);
setRepoStats(data);
}
return;
}
if (repoRes.ok && releasesRes.ok && langsRes.ok) {
const repoData = await repoRes.json();
const releases = await releasesRes.json();
const languages = await langsRes.json();
let totalDownloads = 0;
let latestDownloads = 0;
if (releases.length > 0) {
latestDownloads = releases[0].assets?.reduce((sum: number, asset: any) => sum + (asset.download_count || 0), 0) || 0;
totalDownloads = releases.reduce((sum: number, release: any) => {
return sum + (release.assets?.reduce((s: number, a: any) => s + (a.download_count || 0), 0) || 0);
}, 0);
}
const topLangs = Object.entries(languages)
.sort(([, a]: any, [, b]: any) => b - a)
.slice(0, 4)
.map(([lang]) => lang);
stats[repo.name] = {
stars: repoData.stargazers_count,
forks: repoData.forks_count,
createdAt: repoData.created_at,
totalDownloads,
latestDownloads,
languages: topLangs
};
}
}
catch (err) {
console.error(`Failed to fetch stats for ${repo.name}:`, err);
if (cached) {
const { data } = JSON.parse(cached);
setRepoStats(data);
return;
}
}
}
setRepoStats(stats);
localStorage.setItem(CACHE_KEY, JSON.stringify({ data: stats, timestamp: Date.now() }));
};
fetchRepoStats();
}, []); }, []);
const faqs = [ const faqs = [
{ {
@@ -92,6 +168,34 @@ export function AboutPage({ version }: AboutPageProps) {
const sanitizeForURL = (text: string): string => { const sanitizeForURL = (text: string): string => {
return text.replace(/[()]/g, "").replace(/,/g, " -"); return text.replace(/[()]/g, "").replace(/,/g, " -");
}; };
const formatTimeAgo = (dateString: string): string => {
const now = new Date();
const updated = new Date(dateString);
const diffMs = now.getTime() - updated.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const diffMonths = Math.floor(diffDays / 30);
if (diffDays === 0)
return 'today';
if (diffDays === 1)
return '1d';
if (diffDays < 30)
return `${diffDays}d`;
if (diffMonths === 1)
return '1mo';
if (diffMonths < 12)
return `${diffMonths}mo`;
const diffYears = Math.floor(diffMonths / 12);
return `${diffYears}y`;
};
const formatNumber = (num: number): string => {
if (num >= 1000) {
return num.toLocaleString();
}
return num.toString();
};
const getLangColor = (lang: string): string => {
return langColors[lang] || '#858585';
};
const handleSubmit = () => { const handleSubmit = () => {
let title = ""; let title = "";
let body = ""; let body = "";
@@ -256,12 +360,40 @@ ${location || "Unknown"}
<CardTitle className="flex items-center gap-2"><img src={SpotiDownloaderIcon} className="h-5 w-5" alt="SpotiDownloader" /> SpotiDownloader</CardTitle> <CardTitle className="flex items-center gap-2"><img src={SpotiDownloaderIcon} className="h-5 w-5" alt="SpotiDownloader" /> SpotiDownloader</CardTitle>
<CardDescription>Get Spotify tracks in MP3 and FLAC via the spotidownloader.com API.</CardDescription> <CardDescription>Get Spotify tracks in MP3 and FLAC via the spotidownloader.com API.</CardDescription>
</CardHeader> </CardHeader>
{repoStats['SpotiDownloader'] && (<CardContent className="space-y-3">
<div className="flex flex-wrap gap-2 text-xs">
{repoStats['SpotiDownloader'].languages?.map((lang: string) => (<span key={lang} className="px-2 py-0.5 rounded-full font-medium" style={{ backgroundColor: getLangColor(lang) + '20', color: getLangColor(lang) }}>{lang}</span>))}
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1"><Star className="h-3.5 w-3.5 fill-amber-500 text-amber-500" /> {formatNumber(repoStats['SpotiDownloader'].stars)}</span>
<span className="flex items-center gap-1"><GitFork className="h-3.5 w-3.5" /> {repoStats['SpotiDownloader'].forks}</span>
<span className="flex items-center gap-1"><Clock className="h-3.5 w-3.5" /> {formatTimeAgo(repoStats['SpotiDownloader'].createdAt)}</span>
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1"><Download className="h-3.5 w-3.5" /> TOTAL: {formatNumber(repoStats['SpotiDownloader'].totalDownloads)}</span>
<span className="flex items-center gap-1 text-green-600 dark:text-green-400"><Download className="h-3.5 w-3.5" /> LATEST: {formatNumber(repoStats['SpotiDownloader'].latestDownloads)}</span>
</div>
</CardContent>)}
</Card> </Card>
<Card className="hover:bg-muted/50 hover:border-primary/50 transition-colors cursor-pointer" onClick={() => openExternal("https://github.com/afkarxyz/Twitter-X-Media-Batch-Downloader")}> <Card className="hover:bg-muted/50 hover:border-primary/50 transition-colors cursor-pointer" onClick={() => openExternal("https://github.com/afkarxyz/Twitter-X-Media-Batch-Downloader")}>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"><img src={XBatchDLIcon} className="h-5 w-5" alt="Twitter/X Media Batch Downloader" /> Twitter/X Batch Downloader</CardTitle> <CardTitle className="flex items-center gap-2"><img src={XBatchDLIcon} className="h-5 w-5" alt="Twitter/X Media Batch Downloader" /> Twitter/X Media Batch Downloader</CardTitle>
<CardDescription>A GUI tool to download original-quality images and videos from Twitter/X accounts, powered by gallery-dl by @mikf</CardDescription> <CardDescription>A GUI tool to download original-quality images and videos from Twitter/X accounts, powered by gallery-dl by @mikf</CardDescription>
</CardHeader> </CardHeader>
{repoStats['Twitter-X-Media-Batch-Downloader'] && (<CardContent className="space-y-3">
<div className="flex flex-wrap gap-2 text-xs">
{repoStats['Twitter-X-Media-Batch-Downloader'].languages?.map((lang: string) => (<span key={lang} className="px-2 py-0.5 rounded-full font-medium" style={{ backgroundColor: getLangColor(lang) + '20', color: getLangColor(lang) }}>{lang}</span>))}
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1"><Star className="h-3.5 w-3.5 fill-amber-500 text-amber-500" /> {formatNumber(repoStats['Twitter-X-Media-Batch-Downloader'].stars)}</span>
<span className="flex items-center gap-1"><GitFork className="h-3.5 w-3.5" /> {repoStats['Twitter-X-Media-Batch-Downloader'].forks}</span>
<span className="flex items-center gap-1"><Clock className="h-3.5 w-3.5" /> {formatTimeAgo(repoStats['Twitter-X-Media-Batch-Downloader'].createdAt)}</span>
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1"><Download className="h-3.5 w-3.5" /> TOTAL: {formatNumber(repoStats['Twitter-X-Media-Batch-Downloader'].totalDownloads)}</span>
<span className="flex items-center gap-1 text-green-600 dark:text-green-400"><Download className="h-3.5 w-3.5" /> LATEST: {formatNumber(repoStats['Twitter-X-Media-Batch-Downloader'].latestDownloads)}</span>
</div>
</CardContent>)}
</Card> </Card>
</div> </div>
</TabsContent> </TabsContent>
+142 -138
View File
@@ -131,151 +131,155 @@ export function HistoryPage() {
setShowClearConfirm(false); setShowClearConfirm(false);
}; };
return (<div className="space-y-6"> return (<div className="space-y-6">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h2 className="text-2xl font-bold tracking-tight">Download History</h2>
{history.length > 0 && (<Badge variant="secondary" className="font-mono">
{history.length.toLocaleString('en-US')}
</Badge>)}
</div>
<Button variant="outline" size="sm" onClick={() => setShowClearConfirm(true)} disabled={history.length === 0} className="cursor-pointer gap-2">
<Trash2 className="h-4 w-4"/> Clear
</Button>
</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="relative flex-1"> <h2 className="text-2xl font-bold tracking-tight">Download History</h2>
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground"/> {history.length > 0 && (<Badge variant="secondary" className="font-mono">
<Input placeholder="Search history..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="pl-8 h-9"/> {history.length.toLocaleString('en-US')}
</div> </Badge>)}
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-[180px] h-9">
<ArrowUpDown className="mr-2 h-4 w-4"/>
<SelectValue placeholder="Sort by"/>
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="date_desc">Date (Newest)</SelectItem>
<SelectItem value="date_asc">Date (Oldest)</SelectItem>
<SelectItem value="title_asc">Title (A-Z)</SelectItem>
<SelectItem value="title_desc">Title (Z-A)</SelectItem>
<SelectItem value="artist_asc">Artist (A-Z)</SelectItem>
<SelectItem value="artist_desc">Artist (Z-A)</SelectItem>
<SelectItem value="duration_asc">Duration (Short)</SelectItem>
<SelectItem value="duration_desc">Duration (Long)</SelectItem>
</SelectContent>
</Select>
</div> </div>
<Button variant="outline" size="sm" onClick={() => setShowClearConfirm(true)} disabled={history.length === 0} className="cursor-pointer gap-2">
<Trash2 className="h-4 w-4" /> Clear
</Button>
</div> </div>
<div className="rounded-md border overflow-hidden"> <div className="flex items-center gap-2">
{paginatedHistory.length === 0 ? (<div className="flex flex-col items-center justify-center p-16 text-center text-muted-foreground gap-3"> <div className="relative flex-1">
<div className="rounded-full bg-muted/50 p-4 ring-8 ring-muted/20"> <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<History className="h-10 w-10 opacity-40"/> <Input placeholder="Search history..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="pl-8 h-9" />
</div> </div>
<div className="space-y-1"> <Select value={sortBy} onValueChange={setSortBy}>
<p className="font-medium text-foreground/80">No download history</p> <SelectTrigger className="w-[180px] h-9">
<p className="text-sm">Your downloaded tracks will appear here.</p> <ArrowUpDown className="mr-2 h-4 w-4" />
</div> <SelectValue placeholder="Sort by" />
</div>) : (<table className="w-full table-fixed"> </SelectTrigger>
<thead> <SelectContent>
<tr className="border-b bg-muted/50"> <SelectItem value="default">Default</SelectItem>
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground w-12 text-xs uppercase">#</th> <SelectItem value="date_desc">Date (Newest)</SelectItem>
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground text-xs uppercase">Title</th> <SelectItem value="date_asc">Date (Oldest)</SelectItem>
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground hidden md:table-cell text-xs uppercase w-1/4">Album</th> <SelectItem value="title_asc">Title (A-Z)</SelectItem>
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground hidden lg:table-cell w-32 text-xs uppercase">Format</th> <SelectItem value="title_desc">Title (Z-A)</SelectItem>
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground hidden xl:table-cell w-16 text-xs uppercase text-nowrap">Dur</th> <SelectItem value="artist_asc">Artist (A-Z)</SelectItem>
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground hidden md:table-cell w-40 text-xs uppercase text-nowrap">Downloaded At</th> <SelectItem value="artist_desc">Artist (Z-A)</SelectItem>
<th className="h-10 px-4 text-center align-middle font-medium text-muted-foreground w-16 text-xs uppercase text-nowrap">Link</th> <SelectItem value="duration_asc">Duration (Short)</SelectItem>
</tr> <SelectItem value="duration_desc">Duration (Long)</SelectItem>
</thead> </SelectContent>
<tbody> </Select>
{paginatedHistory.map((item, index) => (<tr key={item.id} className="border-b transition-colors hover:bg-muted/50">
<td className="p-3 align-middle text-sm text-muted-foreground text-left font-mono">
{startIndex + index + 1}
</td>
<td className="p-3 align-middle min-w-0">
<div className="flex items-center gap-3 min-w-0">
<img src={item.cover_url || "https://placehold.co/300?text=No+Cover"} alt={item.album} className="h-10 w-10 rounded shrink-0 bg-secondary object-cover" onError={(e) => { (e.target as HTMLImageElement).src = "https://placehold.co/300?text=No+Cover"; }}/>
<div className="flex flex-col min-w-0 flex-1">
<span className="font-medium text-sm truncate">{item.title}</span>
<span className="text-xs text-muted-foreground truncate">{item.artists}</span>
</div>
</div>
</td>
<td className="p-3 align-middle text-sm text-muted-foreground hidden md:table-cell">
<div className="truncate">{item.album}</div>
</td>
<td className="p-3 align-middle text-left hidden lg:table-cell">
<div className="flex flex-col items-start gap-1">
<span className="text-[10px] font-bold text-foreground">{item.format}</span>
{item.quality && <span className="text-[9px] text-muted-foreground leading-none whitespace-nowrap">{item.quality}</span>}
</div>
</td>
<td className="p-3 align-middle text-sm text-muted-foreground text-left hidden xl:table-cell font-mono">
{item.duration_str}
</td>
<td className="p-3 align-middle text-xs text-muted-foreground hidden md:table-cell whitespace-nowrap">
{formatDate(item.timestamp)}
</td>
<td className="p-3 align-middle text-center">
<Button variant="ghost" size="icon" className="h-8 w-8 cursor-pointer" onClick={() => openExternal(`https://open.spotify.com/track/${item.spotify_id}`)}>
<ExternalLink className="h-4 w-4"/>
</Button>
</td>
</tr>))}
</tbody>
</table>)}
</div> </div>
</div>
{totalPages > 1 && (<Pagination> <div className="rounded-md border overflow-hidden">
<PaginationContent> {paginatedHistory.length === 0 ? (<div className="flex flex-col items-center justify-center p-16 text-center text-muted-foreground gap-3">
<PaginationItem> <div className="rounded-full bg-muted/50 p-4 ring-8 ring-muted/20">
<PaginationPrevious href="#" onClick={(e) => { <History className="h-10 w-10 opacity-40" />
e.preventDefault(); </div>
if (currentPage > 1) <div className="space-y-1">
setCurrentPage(currentPage - 1); <p className="font-medium text-foreground/80">No download history</p>
}} className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}/> <p className="text-sm">Your downloaded tracks will appear here.</p>
</PaginationItem> </div>
</div>) : (<table className="w-full table-fixed">
<thead>
<tr className="border-b bg-muted/50">
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground w-12 text-xs uppercase">#</th>
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground text-xs uppercase">Title</th>
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground hidden md:table-cell text-xs uppercase w-1/4">Album</th>
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground hidden lg:table-cell w-32 text-xs uppercase">Format</th>
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground hidden xl:table-cell w-16 text-xs uppercase text-nowrap">Dur</th>
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground hidden md:table-cell w-40 text-xs uppercase text-nowrap">Downloaded At</th>
<th className="h-10 px-4 text-center align-middle font-medium text-muted-foreground w-16 text-xs uppercase text-nowrap">Link</th>
</tr>
</thead>
<tbody>
{paginatedHistory.map((item, index) => (<tr key={item.id} className="border-b transition-colors hover:bg-muted/50">
<td className="p-3 align-middle text-sm text-muted-foreground text-left font-mono">
{startIndex + index + 1}
</td>
<td className="p-3 align-middle min-w-0">
<div className="flex items-center gap-3 min-w-0">
<img src={item.cover_url || "https://placehold.co/300?text=No+Cover"} alt={item.album} className="h-10 w-10 rounded shrink-0 bg-secondary object-cover" onError={(e) => { (e.target as HTMLImageElement).src = "https://placehold.co/300?text=No+Cover"; }} />
<div className="flex flex-col min-w-0 flex-1">
<span className="font-medium text-sm truncate">{item.title}</span>
<span className="text-xs text-muted-foreground truncate">{item.artists}</span>
</div>
</div>
</td>
<td className="p-3 align-middle text-sm text-muted-foreground hidden md:table-cell">
<div className="truncate">{item.album}</div>
</td>
<td className="p-3 align-middle text-left hidden lg:table-cell">
<div className="flex flex-col items-start gap-1">
<span className="text-xs font-bold text-foreground">
{['HI_RES_LOSSLESS', 'LOSSLESS'].includes(item.format) ? 'FLAC' : item.format}
</span>
{item.quality && <span className="text-[11px] text-muted-foreground leading-none whitespace-nowrap">{item.quality}</span>}
</div>
</td>
<td className="p-3 align-middle text-sm text-muted-foreground text-left hidden xl:table-cell font-mono">
{item.duration_str}
</td>
<td className="p-3 align-middle text-xs text-muted-foreground hidden md:table-cell whitespace-nowrap">
{formatDate(item.timestamp)}
</td>
<td className="p-3 align-middle text-center">
<Button variant="ghost" size="icon" className="h-8 w-8 cursor-pointer" onClick={() => openExternal(`https://open.spotify.com/track/${item.spotify_id}`)}>
<ExternalLink className="h-4 w-4" />
</Button>
</td>
</tr>))}
</tbody>
</table>)}
</div>
{getPaginationPages(currentPage, totalPages).map((page, index) => (page === 'ellipsis' ? (<PaginationItem key={`ellipsis-${index}`}> {
<PaginationEllipsis /> totalPages > 1 && (<Pagination>
</PaginationItem>) : (<PaginationItem key={page}> <PaginationContent>
<PaginationLink href="#" onClick={(e) => { <PaginationItem>
e.preventDefault(); <PaginationPrevious href="#" onClick={(e) => {
setCurrentPage(page); e.preventDefault();
}} isActive={currentPage === page} className="cursor-pointer"> if (currentPage > 1)
{page} setCurrentPage(currentPage - 1);
</PaginationLink> }} className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} />
</PaginationItem>)))} </PaginationItem>
<PaginationItem> {getPaginationPages(currentPage, totalPages).map((page, index) => (page === 'ellipsis' ? (<PaginationItem key={`ellipsis-${index}`}>
<PaginationNext href="#" onClick={(e) => { <PaginationEllipsis />
e.preventDefault(); </PaginationItem>) : (<PaginationItem key={page}>
if (currentPage < totalPages) <PaginationLink href="#" onClick={(e) => {
setCurrentPage(currentPage + 1); e.preventDefault();
}} className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"}/> setCurrentPage(page);
</PaginationItem> }} isActive={currentPage === page} className="cursor-pointer">
</PaginationContent> {page}
</Pagination>)} </PaginationLink>
</PaginationItem>)))}
<Dialog open={showClearConfirm} onOpenChange={setShowClearConfirm}> <PaginationItem>
<DialogContent className="max-w-md [&>button]:hidden"> <PaginationNext href="#" onClick={(e) => {
<DialogHeader> e.preventDefault();
<DialogTitle>Clear Download History?</DialogTitle> if (currentPage < totalPages)
<DialogDescription> setCurrentPage(currentPage + 1);
This will remove all entries from your download history. This action cannot be undone. }} className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"} />
Note: The actual downloaded files will NOT be deleted. </PaginationItem>
</DialogDescription> </PaginationContent>
</DialogHeader> </Pagination>)
<DialogFooter> }
<Button variant="outline" onClick={() => setShowClearConfirm(false)} className="cursor-pointer">Cancel</Button>
<Button variant="destructive" onClick={handleClearHistory} className="cursor-pointer"> <Dialog open={showClearConfirm} onOpenChange={setShowClearConfirm}>
Clear History <DialogContent className="max-w-md [&>button]:hidden">
</Button> <DialogHeader>
</DialogFooter> <DialogTitle>Clear Download History?</DialogTitle>
</DialogContent> <DialogDescription>
</Dialog> This will remove all entries from your download history. This action cannot be undone.
</div>); Note: The actual downloaded files will NOT be deleted.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setShowClearConfirm(false)} className="cursor-pointer">Cancel</Button>
<Button variant="destructive" onClick={handleClearHistory} className="cursor-pointer">
Clear History
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div >);
} }
+12 -12
View File
@@ -266,40 +266,40 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest }: Setting
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="tidal-qobuz"> <SelectItem value="tidal-qobuz">
<span className="flex items-center gap-1.5"><TidalIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><TidalIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="tidal-amazon"> <SelectItem value="tidal-amazon">
<span className="flex items-center gap-1.5"><TidalIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><TidalIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="qobuz-tidal"> <SelectItem value="qobuz-tidal">
<span className="flex items-center gap-1.5"><QobuzIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><QobuzIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="qobuz-amazon"> <SelectItem value="qobuz-amazon">
<span className="flex items-center gap-1.5"><QobuzIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><QobuzIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="amazon-tidal"> <SelectItem value="amazon-tidal">
<span className="flex items-center gap-1.5"><AmazonIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><AmazonIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="amazon-qobuz"> <SelectItem value="amazon-qobuz">
<span className="flex items-center gap-1.5"><AmazonIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><AmazonIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="tidal-qobuz-amazon"> <SelectItem value="tidal-qobuz-amazon">
<span className="flex items-center gap-1.5"><TidalIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><TidalIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="tidal-amazon-qobuz"> <SelectItem value="tidal-amazon-qobuz">
<span className="flex items-center gap-1.5"><TidalIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><TidalIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="qobuz-tidal-amazon"> <SelectItem value="qobuz-tidal-amazon">
<span className="flex items-center gap-1.5"><QobuzIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><QobuzIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="qobuz-amazon-tidal"> <SelectItem value="qobuz-amazon-tidal">
<span className="flex items-center gap-1.5"><QobuzIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><QobuzIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="amazon-tidal-qobuz"> <SelectItem value="amazon-tidal-qobuz">
<span className="flex items-center gap-1.5"><AmazonIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><AmazonIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="amazon-qobuz-tidal"> <SelectItem value="amazon-qobuz-tidal">
<span className="flex items-center gap-1.5"><AmazonIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><AmazonIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>