Added custom theming to UI
This commit is contained in:
@@ -15,72 +15,83 @@ const isTerminalStatus = (status: QueueStatus) =>
|
||||
|
||||
const statusStyles: Record<
|
||||
QueueStatus,
|
||||
{ icon: React.ReactNode; color: string; bgColor: string; name: string }
|
||||
{ icon: React.ReactNode; color: string; bgColor: string; borderColor: string; name: string }
|
||||
> = {
|
||||
queued: {
|
||||
icon: <FaHourglassHalf />,
|
||||
color: "text-gray-500",
|
||||
bgColor: "bg-gray-100",
|
||||
icon: <FaHourglassHalf className="icon-muted" />,
|
||||
color: "text-content-muted dark:text-content-muted-dark",
|
||||
bgColor: "bg-gradient-to-r from-surface-muted to-surface-accent dark:from-surface-muted-dark dark:to-surface-accent-dark",
|
||||
borderColor: "border-border dark:border-border-dark",
|
||||
name: "Queued",
|
||||
},
|
||||
initializing: {
|
||||
icon: <FaSync className="animate-spin" />,
|
||||
color: "text-blue-500",
|
||||
bgColor: "bg-blue-100",
|
||||
icon: <FaSync className="animate-spin icon-accent" />,
|
||||
color: "text-info",
|
||||
bgColor: "bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/30",
|
||||
borderColor: "border-info/30 dark:border-info/40",
|
||||
name: "Initializing",
|
||||
},
|
||||
downloading: {
|
||||
icon: <FaSync className="animate-spin" />,
|
||||
color: "text-blue-500",
|
||||
bgColor: "bg-blue-100",
|
||||
icon: <FaSync className="animate-spin icon-accent" />,
|
||||
color: "text-info",
|
||||
bgColor: "bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/30",
|
||||
borderColor: "border-info/30 dark:border-info/40",
|
||||
name: "Downloading",
|
||||
},
|
||||
processing: {
|
||||
icon: <FaSync className="animate-spin" />,
|
||||
color: "text-purple-500",
|
||||
bgColor: "bg-purple-100",
|
||||
icon: <FaSync className="animate-spin icon-warning" />,
|
||||
color: "text-processing",
|
||||
bgColor: "bg-gradient-to-r from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/30",
|
||||
borderColor: "border-processing/30 dark:border-processing/40",
|
||||
name: "Processing",
|
||||
},
|
||||
retrying: {
|
||||
icon: <FaSync className="animate-spin" />,
|
||||
color: "text-orange-500",
|
||||
bgColor: "bg-orange-100",
|
||||
icon: <FaSync className="animate-spin icon-warning" />,
|
||||
color: "text-warning",
|
||||
bgColor: "bg-gradient-to-r from-orange-50 to-orange-100 dark:from-orange-900/20 dark:to-orange-800/30",
|
||||
borderColor: "border-warning/30 dark:border-warning/40",
|
||||
name: "Retrying",
|
||||
},
|
||||
completed: {
|
||||
icon: <FaCheckCircle />,
|
||||
color: "text-green-500",
|
||||
bgColor: "bg-green-100",
|
||||
icon: <FaCheckCircle className="icon-success" />,
|
||||
color: "text-success",
|
||||
bgColor: "bg-gradient-to-r from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/30",
|
||||
borderColor: "border-success/30 dark:border-success/40",
|
||||
name: "Completed",
|
||||
},
|
||||
done: {
|
||||
icon: <FaCheckCircle />,
|
||||
color: "text-green-500",
|
||||
bgColor: "bg-green-100",
|
||||
icon: <FaCheckCircle className="icon-success" />,
|
||||
color: "text-success",
|
||||
bgColor: "bg-gradient-to-r from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/30",
|
||||
borderColor: "border-success/30 dark:border-success/40",
|
||||
name: "Done",
|
||||
},
|
||||
error: {
|
||||
icon: <FaExclamationCircle />,
|
||||
color: "text-red-500",
|
||||
bgColor: "bg-red-100",
|
||||
icon: <FaExclamationCircle className="icon-error" />,
|
||||
color: "text-error",
|
||||
bgColor: "bg-gradient-to-r from-red-50 to-red-100 dark:from-red-900/20 dark:to-red-800/30",
|
||||
borderColor: "border-error/30 dark:border-error/40",
|
||||
name: "Error",
|
||||
},
|
||||
cancelled: {
|
||||
icon: <FaTimes />,
|
||||
color: "text-yellow-500",
|
||||
bgColor: "bg-yellow-100",
|
||||
icon: <FaTimes className="icon-warning" />,
|
||||
color: "text-warning",
|
||||
bgColor: "bg-gradient-to-r from-orange-50 to-orange-100 dark:from-orange-900/20 dark:to-orange-800/30",
|
||||
borderColor: "border-warning/30 dark:border-warning/40",
|
||||
name: "Cancelled",
|
||||
},
|
||||
skipped: {
|
||||
icon: <FaTimes />,
|
||||
color: "text-gray-500",
|
||||
bgColor: "bg-gray-100",
|
||||
icon: <FaTimes className="icon-muted" />,
|
||||
color: "text-content-muted dark:text-content-muted-dark",
|
||||
bgColor: "bg-gradient-to-r from-surface-muted to-surface-accent dark:from-surface-muted-dark dark:to-surface-accent-dark",
|
||||
borderColor: "border-border dark:border-border-dark",
|
||||
name: "Skipped",
|
||||
},
|
||||
pending: {
|
||||
icon: <FaHourglassHalf />,
|
||||
color: "text-gray-500",
|
||||
bgColor: "bg-gray-100",
|
||||
icon: <FaHourglassHalf className="icon-muted" />,
|
||||
color: "text-content-muted dark:text-content-muted-dark",
|
||||
bgColor: "bg-gradient-to-r from-surface-muted to-surface-accent dark:from-surface-muted-dark dark:to-surface-accent-dark",
|
||||
borderColor: "border-border dark:border-border-dark",
|
||||
name: "Pending",
|
||||
},
|
||||
};
|
||||
@@ -116,24 +127,26 @@ const QueueItemCard = ({ item }: { item: QueueItem }) => {
|
||||
const progressText = getProgressText();
|
||||
|
||||
return (
|
||||
<div className={`p-4 rounded-lg shadow-md mb-3 transition-all duration-300 ${statusInfo.bgColor}`}>
|
||||
<div className={`p-4 rounded-xl border-2 shadow-lg mb-3 transition-all duration-300 hover:shadow-xl hover:scale-[1.02] ${statusInfo.bgColor} ${statusInfo.borderColor}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 min-w-0">
|
||||
<div className={`text-2xl ${statusInfo.color}`}>{statusInfo.icon}</div>
|
||||
<div className="flex items-center gap-4 min-w-0 flex-1">
|
||||
<div className={`text-2xl ${statusInfo.color} bg-white/80 dark:bg-surface-dark/80 p-2 rounded-full shadow-sm`}>
|
||||
{statusInfo.icon}
|
||||
</div>
|
||||
<div className="flex-grow min-w-0">
|
||||
{item.type === "track" && (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<FaMusic className="text-gray-500" />
|
||||
<p className="font-bold text-gray-800 truncate" title={item.name}>
|
||||
<FaMusic className="icon-muted text-sm" />
|
||||
<p className="font-bold text-content-primary dark:text-content-primary-dark truncate" title={item.name}>
|
||||
{item.name}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 truncate" title={item.artist}>
|
||||
<p className="text-sm text-content-secondary dark:text-content-secondary-dark truncate" title={item.artist}>
|
||||
{item.artist}
|
||||
</p>
|
||||
{item.albumName && (
|
||||
<p className="text-xs text-gray-500 truncate" title={item.albumName}>
|
||||
<p className="text-xs text-content-muted dark:text-content-muted-dark truncate" title={item.albumName}>
|
||||
{item.albumName}
|
||||
</p>
|
||||
)}
|
||||
@@ -142,16 +155,16 @@ const QueueItemCard = ({ item }: { item: QueueItem }) => {
|
||||
{item.type === "album" && (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<FaCompactDisc className="text-gray-500" />
|
||||
<p className="font-bold text-gray-800 truncate" title={item.name}>
|
||||
<FaCompactDisc className="icon-muted text-sm" />
|
||||
<p className="font-bold text-content-primary dark:text-content-primary-dark truncate" title={item.name}>
|
||||
{item.name}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 truncate" title={item.artist}>
|
||||
<p className="text-sm text-content-secondary dark:text-content-secondary-dark truncate" title={item.artist}>
|
||||
{item.artist}
|
||||
</p>
|
||||
{item.currentTrackTitle && (
|
||||
<p className="text-xs text-gray-500 truncate" title={item.currentTrackTitle}>
|
||||
<p className="text-xs text-content-muted dark:text-content-muted-dark truncate" title={item.currentTrackTitle}>
|
||||
{item.currentTrackNumber}/{item.totalTracks}: {item.currentTrackTitle}
|
||||
</p>
|
||||
)}
|
||||
@@ -160,16 +173,16 @@ const QueueItemCard = ({ item }: { item: QueueItem }) => {
|
||||
{item.type === "playlist" && (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<FaMusic className="text-gray-500" />
|
||||
<p className="font-bold text-gray-800 truncate" title={item.name}>
|
||||
<FaMusic className="icon-muted text-sm" />
|
||||
<p className="font-bold text-content-primary dark:text-content-primary-dark truncate" title={item.name}>
|
||||
{item.name}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 truncate" title={item.playlistOwner}>
|
||||
<p className="text-sm text-content-secondary dark:text-content-secondary-dark truncate" title={item.playlistOwner}>
|
||||
{item.playlistOwner}
|
||||
</p>
|
||||
{item.currentTrackTitle && (
|
||||
<p className="text-xs text-gray-500 truncate" title={item.currentTrackTitle}>
|
||||
<p className="text-xs text-content-muted dark:text-content-muted-dark truncate" title={item.currentTrackTitle}>
|
||||
{item.currentTrackNumber}/{item.totalTracks}: {item.currentTrackTitle}
|
||||
</p>
|
||||
)}
|
||||
@@ -177,60 +190,82 @@ const QueueItemCard = ({ item }: { item: QueueItem }) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-3 ml-4">
|
||||
<div className="text-right">
|
||||
<p className={`text-sm font-semibold ${statusInfo.color}`}>{statusInfo.name}</p>
|
||||
{progressText && <p className="text-xs text-gray-500">{progressText}</p>}
|
||||
<div className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold ${statusInfo.color} bg-white/60 dark:bg-surface-dark/60 shadow-sm`}>
|
||||
{statusInfo.name}
|
||||
</div>
|
||||
{progressText && <p className="text-xs text-content-muted dark:text-content-muted-dark mt-1">{progressText}</p>}
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
{isTerminal ? (
|
||||
<button
|
||||
onClick={() => removeItem?.(item.id)}
|
||||
className="p-2 rounded-full bg-white/60 dark:bg-surface-dark/60 text-content-muted dark:text-content-muted-dark hover:text-error hover:bg-error/10 transition-all duration-200 shadow-sm"
|
||||
aria-label="Remove"
|
||||
>
|
||||
<FaTimes className="text-sm" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => cancelItem?.(item.id)}
|
||||
className="p-2 rounded-full bg-white/60 dark:bg-surface-dark/60 text-content-muted dark:text-content-muted-dark hover:text-warning hover:bg-warning/10 transition-all duration-200 shadow-sm"
|
||||
aria-label="Cancel"
|
||||
>
|
||||
<FaTimes className="text-sm" />
|
||||
</button>
|
||||
)}
|
||||
{item.canRetry && (
|
||||
<button
|
||||
onClick={() => retryItem?.(item.id)}
|
||||
className="p-2 rounded-full bg-white/60 dark:bg-surface-dark/60 text-content-muted dark:text-content-muted-dark hover:text-info hover:bg-info/10 transition-all duration-200 shadow-sm"
|
||||
aria-label="Retry"
|
||||
>
|
||||
<FaSync className="text-sm" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{isTerminal ? (
|
||||
<button
|
||||
onClick={() => removeItem?.(item.id)}
|
||||
className="text-gray-400 hover:text-red-500 transition-colors"
|
||||
aria-label="Remove"
|
||||
>
|
||||
<FaTimes />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => cancelItem?.(item.id)}
|
||||
className="text-gray-400 hover:text-orange-500 transition-colors"
|
||||
aria-label="Cancel"
|
||||
>
|
||||
<FaTimes />
|
||||
</button>
|
||||
)}
|
||||
{item.canRetry && (
|
||||
<button
|
||||
onClick={() => retryItem?.(item.id)}
|
||||
className="text-gray-400 hover:text-blue-500 transition-colors"
|
||||
aria-label="Retry"
|
||||
>
|
||||
<FaSync />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{(item.status === "error" || item.status === "retrying") && item.error && (
|
||||
<p className="text-xs text-red-600 mt-2">Error: {item.error}</p>
|
||||
<div className="mt-3 p-2 bg-error/10 border border-error/20 rounded-lg">
|
||||
<p className="text-xs text-error font-medium">Error: {item.error}</p>
|
||||
</div>
|
||||
)}
|
||||
{isTerminal && item.summary && (item.summary.total_failed > 0 || item.summary.total_skipped > 0) && (
|
||||
<div className="mt-2 text-xs">
|
||||
{item.summary.total_failed > 0 && (
|
||||
<p className="text-red-600">{item.summary.total_failed} track(s) failed.</p>
|
||||
)}
|
||||
{item.summary.total_skipped > 0 && (
|
||||
<p className="text-yellow-600">{item.summary.total_skipped} track(s) skipped.</p>
|
||||
)}
|
||||
<div className="mt-3 p-2 bg-surface/50 dark:bg-surface-dark/50 rounded-lg">
|
||||
<div className="flex gap-4 text-xs">
|
||||
{item.summary.total_failed > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="w-2 h-2 bg-error rounded-full"></div>
|
||||
<span className="text-error font-medium">{item.summary.total_failed} failed</span>
|
||||
</span>
|
||||
)}
|
||||
{item.summary.total_skipped > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="w-2 h-2 bg-warning rounded-full"></div>
|
||||
<span className="text-warning font-medium">{item.summary.total_skipped} skipped</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(item.status === "downloading" || item.status === "processing") &&
|
||||
item.type === "track" &&
|
||||
item.progress !== undefined && (
|
||||
<div className="mt-2 h-1.5 w-full bg-gray-200 rounded-full">
|
||||
<div
|
||||
className={`h-1.5 rounded-full ${statusInfo.color.replace("text", "bg")}`}
|
||||
style={{ width: `${item.progress}%` }}
|
||||
/>
|
||||
<div className="mt-3">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs text-content-muted dark:text-content-muted-dark">Progress</span>
|
||||
<span className="text-xs font-semibold text-content-primary dark:text-content-primary-dark">{item.progress.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-surface/50 dark:bg-surface-dark/50 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-300 ease-out ${
|
||||
item.status === "downloading" ? "bg-info" : "bg-processing"
|
||||
}`}
|
||||
style={{ width: `${item.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -249,13 +284,15 @@ export const Queue = () => {
|
||||
const hasFinished = items.some((item) => isTerminalStatus(item.status));
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 w-full max-w-md bg-white rounded-lg shadow-xl border border-gray-200 z-50">
|
||||
<header className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||
<h2 className="text-lg font-bold">Download Queue ({items.length})</h2>
|
||||
<div className="fixed bottom-4 right-4 w-full max-w-md bg-surface dark:bg-surface-dark rounded-xl shadow-2xl border border-border dark:border-border-dark z-50 backdrop-blur-sm">
|
||||
<header className="flex items-center justify-between p-4 border-b border-border dark:border-border-dark bg-gradient-to-r from-surface to-surface-secondary dark:from-surface-dark dark:to-surface-secondary-dark rounded-t-xl">
|
||||
<h2 className="text-lg font-bold text-content-primary dark:text-content-primary-dark">
|
||||
Download Queue ({items.length})
|
||||
</h2>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={cancelAll}
|
||||
className="text-sm text-gray-500 hover:text-orange-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="text-sm text-content-muted dark:text-content-muted-dark hover:text-warning transition-colors px-2 py-1 rounded-md hover:bg-warning/10"
|
||||
disabled={!hasActive}
|
||||
aria-label="Cancel all active downloads"
|
||||
>
|
||||
@@ -263,20 +300,30 @@ export const Queue = () => {
|
||||
</button>
|
||||
<button
|
||||
onClick={clearCompleted}
|
||||
className="text-sm text-gray-500 hover:text-green-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="text-sm text-content-muted dark:text-content-muted-dark hover:text-success transition-colors px-2 py-1 rounded-md hover:bg-success/10"
|
||||
disabled={!hasFinished}
|
||||
aria-label="Clear all finished downloads"
|
||||
>
|
||||
Clear Finished
|
||||
</button>
|
||||
<button onClick={toggleVisibility} className="text-gray-500 hover:text-gray-800" aria-label="Close queue">
|
||||
<FaTimes />
|
||||
<button
|
||||
onClick={toggleVisibility}
|
||||
className="text-content-muted dark:text-content-muted-dark hover:text-content-primary dark:hover:text-content-primary-dark p-1 rounded-md hover:bg-surface-muted dark:hover:bg-surface-muted-dark transition-colors"
|
||||
aria-label="Close queue"
|
||||
>
|
||||
<FaTimes className="text-sm" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="p-4 overflow-y-auto max-h-96">
|
||||
<div className="p-4 overflow-y-auto max-h-96 bg-gradient-to-b from-surface-secondary/30 to-surface/30 dark:from-surface-secondary-dark/30 dark:to-surface-dark/30">
|
||||
{items.length === 0 ? (
|
||||
<p className="text-center text-gray-500 py-4">The queue is empty.</p>
|
||||
<div className="text-center py-8">
|
||||
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-surface-muted dark:bg-surface-muted-dark flex items-center justify-center">
|
||||
<FaMusic className="text-2xl icon-muted" />
|
||||
</div>
|
||||
<p className="text-content-muted dark:text-content-muted-dark">The queue is empty.</p>
|
||||
<p className="text-xs text-content-muted dark:text-content-muted-dark mt-1">Downloads will appear here</p>
|
||||
</div>
|
||||
) : (
|
||||
items.map((item) => <QueueItemCard key={item.id} item={item} />)
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user