diff --git a/spotizerr-ui/package.json b/spotizerr-ui/package.json
index a4f034c..f2d0260 100644
--- a/spotizerr-ui/package.json
+++ b/spotizerr-ui/package.json
@@ -22,6 +22,7 @@
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.57.0",
+ "react-icons": "^5.5.0",
"sonner": "^2.0.5",
"tailwindcss": "^4.1.8",
"use-debounce": "^10.0.5",
diff --git a/spotizerr-ui/pnpm-lock.yaml b/spotizerr-ui/pnpm-lock.yaml
index 541de6e..2e4b915 100644
--- a/spotizerr-ui/pnpm-lock.yaml
+++ b/spotizerr-ui/pnpm-lock.yaml
@@ -40,6 +40,9 @@ importers:
react-hook-form:
specifier: ^7.57.0
version: 7.57.0(react@19.1.0)
+ react-icons:
+ specifier: ^5.5.0
+ version: 5.5.0(react@19.1.0)
sonner:
specifier: ^2.0.5
version: 2.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -1719,6 +1722,12 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
+ react-icons@5.5.0:
+ resolution:
+ { integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw== }
+ peerDependencies:
+ react: "*"
+
react-refresh@0.17.0:
resolution:
{ integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ== }
@@ -3196,6 +3205,10 @@ snapshots:
dependencies:
react: 19.1.0
+ react-icons@5.5.0(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+
react-refresh@0.17.0: {}
react@19.1.0: {}
diff --git a/spotizerr-ui/src/components/AlbumCard.tsx b/spotizerr-ui/src/components/AlbumCard.tsx
new file mode 100644
index 0000000..5d5fbfa
--- /dev/null
+++ b/spotizerr-ui/src/components/AlbumCard.tsx
@@ -0,0 +1,44 @@
+import { Link } from "@tanstack/react-router";
+import type { AlbumType } from "../types/spotify";
+
+interface AlbumCardProps {
+ album: AlbumType;
+ onDownload?: () => void;
+}
+
+export const AlbumCard = ({ album, onDownload }: AlbumCardProps) => {
+ const imageUrl = album.images && album.images.length > 0 ? album.images[0].url : "/placeholder.jpg";
+ const subtitle = album.artists.map((artist) => artist.name).join(", ");
+
+ return (
+
+
+
+

+ {onDownload && (
+
+ )}
+
+
+
+
+ {album.name}
+
+ {subtitle &&
{subtitle}
}
+
+
+ );
+};
diff --git a/spotizerr-ui/src/components/Queue.tsx b/spotizerr-ui/src/components/Queue.tsx
index aea11d0..d5b89e0 100644
--- a/spotizerr-ui/src/components/Queue.tsx
+++ b/spotizerr-ui/src/components/Queue.tsx
@@ -1,162 +1,183 @@
-import { useQueue, type QueueItem } from "../contexts/queue-context";
+import { useContext } from "react";
+import {
+ FaTimes,
+ FaSync,
+ FaCheckCircle,
+ FaExclamationCircle,
+ FaHourglassHalf,
+ FaMusic,
+ FaCompactDisc,
+} from "react-icons/fa";
+import { QueueContext, type QueueItem, type QueueStatus } from "@/contexts/queue-context";
-export function Queue() {
- const { items, isVisible, removeItem, retryItem, clearQueue, toggleVisibility, clearCompleted } = useQueue();
+const statusStyles: Record = {
+ queued: {
+ icon: ,
+ color: "text-gray-500",
+ bgColor: "bg-gray-100",
+ name: "Queued",
+ },
+ initializing: {
+ icon: ,
+ color: "text-blue-500",
+ bgColor: "bg-blue-100",
+ name: "Initializing",
+ },
+ downloading: {
+ icon: ,
+ color: "text-blue-500",
+ bgColor: "bg-blue-100",
+ name: "Downloading",
+ },
+ processing: {
+ icon: ,
+ color: "text-purple-500",
+ bgColor: "bg-purple-100",
+ name: "Processing",
+ },
+ completed: {
+ icon: ,
+ color: "text-green-500",
+ bgColor: "bg-green-100",
+ name: "Completed",
+ },
+ done: {
+ icon: ,
+ color: "text-green-500",
+ bgColor: "bg-green-100",
+ name: "Done",
+ },
+ error: {
+ icon: ,
+ color: "text-red-500",
+ bgColor: "bg-red-100",
+ name: "Error",
+ },
+ cancelled: {
+ icon: ,
+ color: "text-yellow-500",
+ bgColor: "bg-yellow-100",
+ name: "Cancelled",
+ },
+ skipped: {
+ icon: ,
+ color: "text-gray-500",
+ bgColor: "bg-gray-100",
+ name: "Skipped",
+ },
+ pending: {
+ icon: ,
+ color: "text-gray-500",
+ bgColor: "bg-gray-100",
+ name: "Pending",
+ },
+};
+
+const QueueItemCard = ({ item }: { item: QueueItem }) => {
+ const { removeItem, retryItem } = useContext(QueueContext) || {};
+ const statusInfo = statusStyles[item.status] || statusStyles.queued;
+
+ const isTerminal = item.status === "completed" || item.status === "done";
+ const currentCount = isTerminal ? (item.summary?.successful?.length ?? item.totalTracks) : item.currentTrackNumber;
+
+ const progressText =
+ item.type === "album" || item.type === "playlist"
+ ? `${currentCount || 0}/${item.totalTracks || "?"}`
+ : item.progress
+ ? `${item.progress.toFixed(0)}%`
+ : "";
+
+ return (
+
+
+
+
{statusInfo.icon}
+
+
+ {item.type === "track" ? (
+
+ ) : (
+
+ )}
+
+ {item.name}
+
+
+
+
+ {item.artist}
+
+
+
+
+
+
{statusInfo.name}
+ {progressText &&
{progressText}
}
+
+
+ {item.canRetry && (
+
+ )}
+
+
+ {item.error &&
Error: {item.error}
}
+ {(item.status === "downloading" || item.status === "processing") && item.progress !== undefined && (
+
+ )}
+
+ );
+};
+
+export const Queue = () => {
+ const context = useContext(QueueContext);
+
+ if (!context) return null;
+ const { items, isVisible, toggleVisibility, clearQueue } = context;
if (!isVisible) return null;
- const handleClearQueue = () => {
- if (confirm("Are you sure you want to cancel all downloads and clear the queue?")) {
- clearQueue();
- }
- };
-
- const renderProgress = (item: QueueItem) => {
- if (item.status === "downloading" || item.status === "processing") {
- const isMultiTrack = item.totalTracks && item.totalTracks > 1;
- const overallProgress =
- isMultiTrack && item.totalTracks
- ? ((item.currentTrackNumber || 0) / item.totalTracks) * 100
- : item.progress || 0;
-
- return (
-
-
- {isMultiTrack && (
-
- )}
-
- );
- }
- return null;
- };
-
- const renderStatusDetails = (item: QueueItem) => {
- const statusClass = {
- initializing: "text-gray-400",
- pending: "text-gray-400",
- downloading: "text-blue-400",
- processing: "text-purple-400",
- completed: "text-green-500 font-semibold",
- error: "text-red-500 font-semibold",
- skipped: "text-yellow-500",
- cancelled: "text-gray-500",
- queued: "text-gray-400",
- }[item.status];
-
- const isMultiTrack = item.totalTracks && item.totalTracks > 1;
-
- return (
-
- {item.status.toUpperCase()}
- {item.status === "downloading" && (
- <>
- {item.progress?.toFixed(0)}%
- {item.speed}
- {item.eta}
- >
- )}
- {isMultiTrack && (
-
- {item.currentTrackNumber}/{item.totalTracks}
-
- )}
-
- );
- };
-
- const renderSummary = (item: QueueItem) => {
- if (item.status !== "completed" || !item.summary) return null;
-
- return (
-
-
- Success: {item.summary.successful}
- {" "}
- |{" "}
-
- Skipped: {item.summary.skipped}
- {" "}
- |{" "}
-
- Failed: {item.summary.failed}
-
-
- );
- };
-
return (
-