Added custom theming to UI

This commit is contained in:
Xoconoch
2025-07-27 11:13:04 -06:00
parent c92821ee48
commit a459e0eee6
19 changed files with 730 additions and 369 deletions

View File

@@ -11,7 +11,7 @@ export const AlbumCard = ({ album, onDownload }: AlbumCardProps) => {
const subtitle = album.artists.map((artist) => artist.name).join(", ");
return (
<div className="group flex flex-col rounded-lg overflow-hidden bg-white dark:bg-gray-800 shadow-xl hover:shadow-2xl transition-all duration-300 ease-in-out hover:-translate-y-1 hover:scale-105">
<div className="group flex flex-col rounded-lg overflow-hidden bg-surface dark:bg-surface-secondary-dark hover:bg-surface-secondary dark:hover:bg-surface-muted-dark shadow-xl hover:shadow-2xl transition-all duration-300 ease-in-out hover:-translate-y-1 hover:scale-105">
<div className="relative">
<Link to="/album/$albumId" params={{ albumId: album.id }}>
<img src={imageUrl} alt={album.name} className="w-full aspect-square object-cover" />
@@ -21,10 +21,10 @@ export const AlbumCard = ({ album, onDownload }: AlbumCardProps) => {
e.preventDefault();
onDownload();
}}
className="absolute bottom-2 right-2 p-2 bg-green-600 text-white rounded-full hover:bg-green-700 transition-opacity shadow-lg opacity-0 group-hover:opacity-100 duration-300"
className="absolute bottom-2 right-2 p-2 bg-button-success hover:bg-button-success-hover text-button-success-text rounded-full transition-opacity shadow-lg opacity-0 group-hover:opacity-100 duration-300"
title="Download album"
>
<img src="/download.svg" alt="Download" className="w-5 h-5" />
<img src="/download.svg" alt="Download" className="w-5 h-5 icon-inverse" />
</button>
)}
</Link>
@@ -33,11 +33,11 @@ export const AlbumCard = ({ album, onDownload }: AlbumCardProps) => {
<Link
to="/album/$albumId"
params={{ albumId: album.id }}
className="font-semibold text-gray-900 dark:text-white truncate block"
className="font-semibold text-content-primary dark:text-content-primary-dark truncate block"
>
{album.name}
</Link>
{subtitle && <p className="text-sm text-gray-600 dark:text-gray-400 mt-1 truncate">{subtitle}</p>}
{subtitle && <p className="text-sm text-content-secondary dark:text-content-secondary-dark mt-1 truncate">{subtitle}</p>}
</div>
</div>
);

View File

@@ -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,61 +190,83 @@ 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="text-gray-400 hover:text-red-500 transition-colors"
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 />
<FaTimes className="text-sm" />
</button>
) : (
<button
onClick={() => cancelItem?.(item.id)}
className="text-gray-400 hover:text-orange-500 transition-colors"
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 />
<FaTimes className="text-sm" />
</button>
)}
{item.canRetry && (
<button
onClick={() => retryItem?.(item.id)}
className="text-gray-400 hover:text-blue-500 transition-colors"
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 />
<FaSync className="text-sm" />
</button>
)}
</div>
</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">
<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 && (
<p className="text-red-600">{item.summary.total_failed} track(s) failed.</p>
<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 && (
<p className="text-yellow-600">{item.summary.total_skipped} track(s) skipped.</p>
<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="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-1.5 rounded-full ${statusInfo.color.replace("text", "bg")}`}
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} />)
)}

View File

@@ -24,24 +24,24 @@ export const SearchResultCard = ({ id, name, subtitle, imageUrl, type, onDownloa
};
return (
<div className="group flex flex-col rounded-lg overflow-hidden bg-white dark:bg-gray-800 shadow-xl hover:shadow-2xl transition-shadow duration-300 ease-in-out">
<div className="group flex flex-col rounded-lg overflow-hidden bg-surface dark:bg-surface-secondary-dark hover:bg-surface-secondary dark:hover:bg-surface-muted-dark shadow-xl hover:shadow-2xl transition-shadow duration-300 ease-in-out">
<div className="relative">
<img src={imageUrl || "/placeholder.jpg"} alt={name} className="w-full aspect-square object-cover" />
{onDownload && (
<button
onClick={onDownload}
className="absolute bottom-2 right-2 p-2 bg-green-600 text-white rounded-full hover:bg-green-700 transition-opacity shadow-lg opacity-0 group-hover:opacity-100 duration-300"
className="absolute bottom-2 right-2 p-2 bg-button-success hover:bg-button-success-hover text-button-success-text rounded-full transition-opacity shadow-lg opacity-0 group-hover:opacity-100 duration-300"
title={`Download ${type}`}
>
<img src="/download.svg" alt="Download" className="w-5 h-5" />
<img src="/download.svg" alt="Download" className="w-5 h-5 icon-inverse" />
</button>
)}
</div>
<div className="p-4 flex-grow flex flex-col">
<Link to={getLinkPath()} className="font-semibold text-gray-900 dark:text-white truncate block">
<Link to={getLinkPath()} className="font-semibold text-content-primary dark:text-content-primary-dark truncate block">
{name}
</Link>
{subtitle && <p className="text-sm text-gray-600 dark:text-gray-400 mt-1 truncate">{subtitle}</p>}
{subtitle && <p className="text-sm text-content-secondary dark:text-content-secondary-dark mt-1 truncate">{subtitle}</p>}
</div>
</div>
);

View File

@@ -87,61 +87,61 @@ export function AccountsTab() {
};
const renderAddForm = () => (
<form onSubmit={handleSubmit(onSubmit)} className="p-4 border rounded-lg mt-4 space-y-4">
<h4 className="font-semibold">Add New {activeService === "spotify" ? "Spotify" : "Deezer"} Account</h4>
<form onSubmit={handleSubmit(onSubmit)} className="p-4 border border-line dark:border-border-dark rounded-lg mt-4 space-y-4">
<h4 className="font-semibold text-content-primary dark:text-content-primary-dark">Add New {activeService === "spotify" ? "Spotify" : "Deezer"} Account</h4>
<div className="flex flex-col gap-2">
<label htmlFor="accountName">Account Name</label>
<label htmlFor="accountName" className="text-content-primary dark:text-content-primary-dark">Account Name</label>
<input
id="accountName"
{...register("accountName", { required: "This field is required" })}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
/>
{errors.accountName && <p className="text-red-500 text-sm">{errors.accountName.message}</p>}
{errors.accountName && <p className="text-error-text bg-error-muted px-2 py-1 rounded text-sm">{errors.accountName.message}</p>}
</div>
{activeService === "spotify" && (
<div className="flex flex-col gap-2">
<label htmlFor="authBlob">Auth Blob (JSON)</label>
<label htmlFor="authBlob" className="text-content-primary dark:text-content-primary-dark">Auth Blob (JSON)</label>
<textarea
id="authBlob"
{...register("authBlob", { required: activeService === "spotify" ? "Auth Blob is required" : false })}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
rows={4}
></textarea>
{errors.authBlob && <p className="text-red-500 text-sm">{errors.authBlob.message}</p>}
{errors.authBlob && <p className="text-error-text bg-error-muted px-2 py-1 rounded text-sm">{errors.authBlob.message}</p>}
</div>
)}
{activeService === "deezer" && (
<div className="flex flex-col gap-2">
<label htmlFor="arl">ARL Token</label>
<label htmlFor="arl" className="text-content-primary dark:text-content-primary-dark">ARL Token</label>
<input
id="arl"
{...register("arl", { required: activeService === "deezer" ? "ARL is required" : false })}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
/>
{errors.arl && <p className="text-red-500 text-sm">{errors.arl.message}</p>}
{errors.arl && <p className="text-error-text bg-error-muted px-2 py-1 rounded text-sm">{errors.arl.message}</p>}
</div>
)}
<div className="flex flex-col gap-2">
<label htmlFor="accountRegion">Region (Optional)</label>
<label htmlFor="accountRegion" className="text-content-primary dark:text-content-primary-dark">Region (Optional)</label>
<input
id="accountRegion"
{...register("accountRegion")}
placeholder="e.g. US, GB"
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
/>
</div>
<div className="flex gap-2">
<button
type="submit"
disabled={addMutation.isPending}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
>
{addMutation.isPending ? "Saving..." : "Save Account"}
</button>
<button
type="button"
onClick={() => setIsAdding(false)}
className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700"
className="px-4 py-2 bg-button-secondary hover:bg-button-secondary-hover text-button-secondary-text hover:text-button-secondary-text-hover rounded-md"
>
Cancel
</button>
@@ -151,35 +151,35 @@ export function AccountsTab() {
return (
<div className="space-y-6">
<div className="flex gap-2 border-b">
<div className="flex gap-2 border-b border-line dark:border-border-dark">
<button
onClick={() => setActiveService("spotify")}
className={`p-2 ${activeService === "spotify" ? "border-b-2 border-blue-500 font-semibold" : ""}`}
className={`p-2 text-content-primary dark:text-content-primary-dark ${activeService === "spotify" ? "border-b-2 border-primary font-semibold" : ""}`}
>
Spotify
</button>
<button
onClick={() => setActiveService("deezer")}
className={`p-2 ${activeService === "deezer" ? "border-b-2 border-blue-500 font-semibold" : ""}`}
className={`p-2 text-content-primary dark:text-content-primary-dark ${activeService === "deezer" ? "border-b-2 border-primary font-semibold" : ""}`}
>
Deezer
</button>
</div>
{isLoading ? (
<p>Loading accounts...</p>
<p className="text-content-muted dark:text-content-muted-dark">Loading accounts...</p>
) : (
<div className="space-y-2">
{credentials?.map((cred) => (
<div
key={cred.name}
className="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white rounded-md"
className="flex justify-between items-center p-3 bg-surface-muted dark:bg-surface-muted-dark text-content-primary dark:text-content-primary-dark rounded-md"
>
<span>{cred.name}</span>
<button
onClick={() => deleteMutation.mutate({ service: activeService, name: cred.name })}
disabled={deleteMutation.isPending && deleteMutation.variables?.name === cred.name}
className="text-red-500 hover:text-red-400"
className="text-error hover:text-error-hover icon-error"
>
Delete
</button>
@@ -191,7 +191,7 @@ export function AccountsTab() {
{!isAdding && (
<button
onClick={() => setIsAdding(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
>
Add Account
</button>

View File

@@ -89,36 +89,36 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
{/* Download Settings */}
<div className="space-y-4">
<h3 className="text-xl font-semibold">Download Behavior</h3>
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Download Behavior</h3>
<div className="flex flex-col gap-2">
<label htmlFor="maxConcurrentDownloads">Max Concurrent Downloads</label>
<label htmlFor="maxConcurrentDownloads" className="text-content-primary dark:text-content-primary-dark">Max Concurrent Downloads</label>
<input
id="maxConcurrentDownloads"
type="number"
min="1"
{...register("maxConcurrentDownloads")}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
/>
</div>
<div className="flex items-center justify-between">
<label htmlFor="realTimeToggle">Real-time downloading</label>
<label htmlFor="realTimeToggle" className="text-content-primary dark:text-content-primary-dark">Real-time downloading</label>
<input id="realTimeToggle" type="checkbox" {...register("realTime")} className="h-6 w-6 rounded" />
</div>
<div className="flex items-center justify-between">
<label htmlFor="fallbackToggle">Download Fallback</label>
<label htmlFor="fallbackToggle" className="text-content-primary dark:text-content-primary-dark">Download Fallback</label>
<input id="fallbackToggle" type="checkbox" {...register("fallback")} className="h-6 w-6 rounded" />
</div>
</div>
{/* Source Quality Settings */}
<div className="space-y-4">
<h3 className="text-xl font-semibold">Source Quality</h3>
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Source Quality</h3>
<div className="flex flex-col gap-2">
<label htmlFor="spotifyQuality">Spotify Quality</label>
<label htmlFor="spotifyQuality" className="text-content-primary dark:text-content-primary-dark">Spotify Quality</label>
<select
id="spotifyQuality"
{...register("spotifyQuality")}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
>
<option value="NORMAL">OGG 96kbps</option>
<option value="HIGH">OGG 160kbps</option>
@@ -126,31 +126,31 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
</select>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="deezerQuality">Deezer Quality</label>
<label htmlFor="deezerQuality" className="text-content-primary dark:text-content-primary-dark">Deezer Quality</label>
<select
id="deezerQuality"
{...register("deezerQuality")}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
>
<option value="MP3_128">MP3 128kbps</option>
<option value="MP3_320">MP3 320kbps</option>
<option value="FLAC">FLAC (HiFi)</option>
</select>
</div>
<p className="text-sm text-gray-500 mt-1">
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">
This sets the quality of the original download. Conversion settings below are applied after download.
</p>
</div>
{/* Conversion Settings */}
<div className="space-y-4">
<h3 className="text-xl font-semibold">Conversion</h3>
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Conversion</h3>
<div className="flex flex-col gap-2">
<label htmlFor="convertToSelect">Convert To Format</label>
<label htmlFor="convertToSelect" className="text-content-primary dark:text-content-primary-dark">Convert To Format</label>
<select
id="convertToSelect"
{...register("convertTo")}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
>
<option value="">No Conversion</option>
{Object.keys(CONVERSION_FORMATS).map((format) => (
@@ -161,11 +161,11 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
</select>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="bitrateSelect">Bitrate</label>
<label htmlFor="bitrateSelect" className="text-content-primary dark:text-content-primary-dark">Bitrate</label>
<select
id="bitrateSelect"
{...register("bitrate")}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
disabled={!selectedFormat || CONVERSION_FORMATS[selectedFormat]?.length === 0}
>
<option value="">Auto</option>
@@ -180,35 +180,35 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
{/* Retry Options */}
<div className="space-y-4">
<h3 className="text-xl font-semibold">Retries</h3>
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Retries</h3>
<div className="flex flex-col gap-2">
<label htmlFor="maxRetries">Max Retry Attempts</label>
<label htmlFor="maxRetries" className="text-content-primary dark:text-content-primary-dark">Max Retry Attempts</label>
<input
id="maxRetries"
type="number"
min="0"
{...register("maxRetries")}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="retryDelaySeconds">Initial Retry Delay (s)</label>
<label htmlFor="retryDelaySeconds" className="text-content-primary dark:text-content-primary-dark">Initial Retry Delay (s)</label>
<input
id="retryDelaySeconds"
type="number"
min="1"
{...register("retryDelaySeconds")}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="retryDelayIncrease">Retry Delay Increase (s)</label>
<label htmlFor="retryDelayIncrease" className="text-content-primary dark:text-content-primary-dark">Retry Delay Increase (s)</label>
<input
id="retryDelayIncrease"
type="number"
min="0"
{...register("retryDelayIncrease")}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
/>
</div>
</div>
@@ -216,7 +216,7 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
<button
type="submit"
disabled={mutation.isPending}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
>
{mutation.isPending ? "Saving..." : "Save Download Settings"}
</button>

View File

@@ -50,7 +50,7 @@ const placeholders = {
const PlaceholderSelector = ({ onSelect }: { onSelect: (value: string) => void }) => (
<select
onChange={(e) => onSelect(e.target.value)}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm mt-1"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus text-sm mt-1"
>
<option value="">-- Insert Placeholder --</option>
{Object.entries(placeholders).map(([group, options]) => (
@@ -104,15 +104,15 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
};
if (isLoading) {
return <div>Loading formatting settings...</div>;
return <div className="text-content-muted dark:text-content-muted-dark">Loading formatting settings...</div>;
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
<div className="space-y-4">
<h3 className="text-xl font-semibold">File Naming</h3>
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">File Naming</h3>
<div className="flex flex-col gap-2">
<label htmlFor="customDirFormat">Custom Directory Format</label>
<label htmlFor="customDirFormat" className="text-content-primary dark:text-content-primary-dark">Custom Directory Format</label>
<input
id="customDirFormat"
type="text"
@@ -121,12 +121,12 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
dirFormatRef(e);
dirInputRef.current = e;
}}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
/>
<PlaceholderSelector onSelect={handlePlaceholderSelect("customDirFormat", dirInputRef)} />
</div>
<div className="flex flex-col gap-2">
<label htmlFor="customTrackFormat">Custom Track Format</label>
<label htmlFor="customTrackFormat" className="text-content-primary dark:text-content-primary-dark">Custom Track Format</label>
<input
id="customTrackFormat"
type="text"
@@ -135,12 +135,12 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
trackFormatRef(e);
trackInputRef.current = e;
}}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
/>
<PlaceholderSelector onSelect={handlePlaceholderSelect("customTrackFormat", trackInputRef)} />
</div>
<div className="flex items-center justify-between">
<label htmlFor="tracknumPaddingToggle">Track Number Padding</label>
<label htmlFor="tracknumPaddingToggle" className="text-content-primary dark:text-content-primary-dark">Track Number Padding</label>
<input
id="tracknumPaddingToggle"
type="checkbox"
@@ -149,7 +149,7 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
/>
</div>
<div className="flex items-center justify-between">
<label htmlFor="saveCoverToggle">Save Album Cover</label>
<label htmlFor="saveCoverToggle" className="text-content-primary dark:text-content-primary-dark">Save Album Cover</label>
<input id="saveCoverToggle" type="checkbox" {...register("saveCover")} className="h-6 w-6 rounded" />
</div>
</div>
@@ -157,7 +157,7 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
<button
type="submit"
disabled={mutation.isPending}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
>
{mutation.isPending ? "Saving..." : "Save Formatting Settings"}
</button>

View File

@@ -70,18 +70,18 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
};
const isLoading = isConfigLoading || spotifyLoading || deezerLoading || settingsLoading;
if (isLoading) return <p>Loading general settings...</p>;
if (isLoading) return <p className="text-content-muted dark:text-content-muted-dark">Loading general settings...</p>;
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
<div className="space-y-4">
<h3 className="text-xl font-semibold">Service Defaults</h3>
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Service Defaults</h3>
<div className="flex flex-col gap-2">
<label htmlFor="service">Default Service</label>
<label htmlFor="service" className="text-content-primary dark:text-content-primary-dark">Default Service</label>
<select
id="service"
{...register("service")}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
>
<option value="spotify">Spotify</option>
<option value="deezer">Deezer</option>
@@ -90,13 +90,13 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold">Spotify Settings</h3>
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Spotify Settings</h3>
<div className="flex flex-col gap-2">
<label htmlFor="spotifyAccount">Active Spotify Account</label>
<label htmlFor="spotifyAccount" className="text-content-primary dark:text-content-primary-dark">Active Spotify Account</label>
<select
id="spotifyAccount"
{...register("spotify")}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
>
{spotifyAccounts?.map((acc) => (
<option key={acc.name} value={acc.name}>
@@ -108,13 +108,13 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold">Deezer Settings</h3>
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Deezer Settings</h3>
<div className="flex flex-col gap-2">
<label htmlFor="deezerAccount">Active Deezer Account</label>
<label htmlFor="deezerAccount" className="text-content-primary dark:text-content-primary-dark">Active Deezer Account</label>
<select
id="deezerAccount"
{...register("deezer")}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
>
{deezerAccounts?.map((acc) => (
<option key={acc.name} value={acc.name}>
@@ -126,17 +126,17 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold">Content Filters</h3>
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Content Filters</h3>
<div className="form-item--row">
<label>Filter Explicit Content</label>
<label className="text-content-primary dark:text-content-primary-dark">Filter Explicit Content</label>
<div className="flex items-center gap-2">
<span className={`font-semibold ${globalSettings?.explicitFilter ? "text-green-400" : "text-red-400"}`}>
<span className={`font-semibold ${globalSettings?.explicitFilter ? "text-success" : "text-error"}`}>
{globalSettings?.explicitFilter ? "Enabled" : "Disabled"}
</span>
<span className="text-xs bg-gray-600 text-white px-2 py-1 rounded-full">ENV</span>
<span className="text-xs bg-surface-accent dark:bg-surface-accent-dark text-content-inverse dark:text-content-inverse-dark px-2 py-1 rounded-full">ENV</span>
</div>
</div>
<p className="text-sm text-gray-500 mt-1">
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">
The explicit content filter is controlled by an environment variable and cannot be changed here.
</p>
</div>
@@ -144,7 +144,7 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
<button
type="submit"
disabled={mutation.isPending}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
>
{mutation.isPending ? "Saving..." : "Save General Settings"}
</button>

View File

@@ -62,34 +62,34 @@ function SpotifyApiForm() {
const onSubmit = (formData: SpotifyApiSettings) => mutation.mutate(formData);
if (isLoading) return <p>Loading Spotify API settings...</p>;
if (isLoading) return <p className="text-content-muted dark:text-content-muted-dark">Loading Spotify API settings...</p>;
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="flex flex-col gap-2">
<label htmlFor="client_id">Client ID</label>
<label htmlFor="client_id" className="text-content-primary dark:text-content-primary-dark">Client ID</label>
<input
id="client_id"
type="password"
{...register("client_id")}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
placeholder="Optional"
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="client_secret">Client Secret</label>
<label htmlFor="client_secret" className="text-content-primary dark:text-content-primary-dark">Client Secret</label>
<input
id="client_secret"
type="password"
{...register("client_secret")}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
placeholder="Optional"
/>
</div>
<button
type="submit"
disabled={mutation.isPending}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
>
{mutation.isPending ? "Saving..." : "Save Spotify API"}
</button>
@@ -126,22 +126,22 @@ function WebhookForm() {
const onSubmit = (formData: WebhookSettings) => mutation.mutate(formData);
if (isLoading) return <p>Loading Webhook settings...</p>;
if (isLoading) return <p className="text-content-muted dark:text-content-muted-dark">Loading Webhook settings...</p>;
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<div className="flex flex-col gap-2">
<label htmlFor="webhookUrl">Webhook URL</label>
<label htmlFor="webhookUrl" className="text-content-primary dark:text-content-primary-dark">Webhook URL</label>
<input
id="webhookUrl"
type="url"
{...register("url")}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
placeholder="https://example.com/webhook"
/>
</div>
<div className="flex flex-col gap-2">
<label>Webhook Events</label>
<label className="text-content-primary dark:text-content-primary-dark">Webhook Events</label>
<div className="grid grid-cols-2 gap-4 pt-2">
{data?.available_events.map((event) => (
<Controller
@@ -149,7 +149,7 @@ function WebhookForm() {
name="events"
control={control}
render={({ field }) => (
<label className="flex items-center gap-2">
<label className="flex items-center gap-2 text-content-primary dark:text-content-primary-dark">
<input
type="checkbox"
className="h-5 w-5 rounded"
@@ -171,7 +171,7 @@ function WebhookForm() {
<button
type="submit"
disabled={mutation.isPending}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
>
{mutation.isPending ? "Saving..." : "Save Webhook"}
</button>
@@ -179,7 +179,7 @@ function WebhookForm() {
type="button"
onClick={() => testMutation.mutate(currentUrl)}
disabled={!currentUrl || testMutation.isPending}
className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 disabled:opacity-50"
className="px-4 py-2 bg-button-secondary hover:bg-button-secondary-hover text-button-secondary-text hover:text-button-secondary-text-hover rounded-md disabled:opacity-50"
>
Test
</button>
@@ -192,14 +192,14 @@ export function ServerTab() {
return (
<div className="space-y-8">
<div>
<h3 className="text-xl font-semibold">Spotify API</h3>
<p className="text-sm text-gray-500 mt-1">Provide your own API credentials to avoid rate-limiting issues.</p>
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Spotify API</h3>
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">Provide your own API credentials to avoid rate-limiting issues.</p>
<SpotifyApiForm />
</div>
<hr className="border-gray-600" />
<hr className="border-border dark:border-border-dark" />
<div>
<h3 className="text-xl font-semibold">Webhooks</h3>
<p className="text-sm text-gray-500 mt-1">
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Webhooks</h3>
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">
Get notifications for events like download completion. (Currently disabled)
</p>
<WebhookForm />

View File

@@ -62,33 +62,33 @@ export function WatchTab() {
};
if (isLoading) {
return <div>Loading watch settings...</div>;
return <div className="text-content-muted dark:text-content-muted-dark">Loading watch settings...</div>;
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
<div className="space-y-4">
<h3 className="text-xl font-semibold">Watchlist Behavior</h3>
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Watchlist Behavior</h3>
<div className="flex items-center justify-between">
<label htmlFor="watchEnabledToggle">Enable Watchlist</label>
<label htmlFor="watchEnabledToggle" className="text-content-primary dark:text-content-primary-dark">Enable Watchlist</label>
<input id="watchEnabledToggle" type="checkbox" {...register("enabled")} className="h-6 w-6 rounded" />
</div>
<div className="flex flex-col gap-2">
<label htmlFor="watchPollIntervalSeconds">Watch Poll Interval (seconds)</label>
<label htmlFor="watchPollIntervalSeconds" className="text-content-primary dark:text-content-primary-dark">Watch Poll Interval (seconds)</label>
<input
id="watchPollIntervalSeconds"
type="number"
min="60"
{...register("watchPollIntervalSeconds")}
className="block w-full p-2 border rounded-md bg-gray-50 dark:bg-gray-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="block w-full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
/>
<p className="text-sm text-gray-500 mt-1">How often to check watched items for updates.</p>
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">How often to check watched items for updates.</p>
</div>
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold">Artist Album Groups</h3>
<p className="text-sm text-gray-500">Select which album groups to monitor for watched artists.</p>
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Artist Album Groups</h3>
<p className="text-sm text-content-muted dark:text-content-muted-dark">Select which album groups to monitor for watched artists.</p>
<div className="grid grid-cols-2 gap-4 pt-2">
{ALBUM_GROUPS.map((group) => (
<Controller
@@ -96,7 +96,7 @@ export function WatchTab() {
name="watchedArtistAlbumGroup"
control={control}
render={({ field }) => (
<label className="flex items-center gap-2">
<label className="flex items-center gap-2 text-content-primary dark:text-content-primary-dark">
<input
type="checkbox"
className="h-5 w-5 rounded"
@@ -118,7 +118,7 @@ export function WatchTab() {
<button
type="submit"
disabled={mutation.isPending}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
>
{mutation.isPending ? "Saving..." : "Save Watch Settings"}
</button>

View File

@@ -1,7 +1,319 @@
@import "tailwindcss";
@theme {
/* Background Colors */
--color-surface: #ffffff;
--color-surface-secondary: #f8fafc;
--color-surface-muted: #f1f5f9;
--color-surface-accent: #e2e8f0;
--color-surface-overlay: rgba(255, 255, 255, 0.95);
/* Dark mode backgrounds */
--color-surface-dark: #0f172a;
--color-surface-secondary-dark: #1e293b;
--color-surface-muted-dark: #334155;
--color-surface-accent-dark: #475569;
--color-surface-overlay-dark: rgba(15, 23, 42, 0.95);
/* Text Colors */
--color-content-primary: #0f172a;
--color-content-secondary: #475569;
--color-content-muted: #64748b;
--color-content-accent: #94a3b8;
--color-content-inverse: #ffffff;
/* Dark mode text */
--color-content-primary-dark: #f8fafc;
--color-content-secondary-dark: #e2e8f0;
--color-content-muted-dark: #cbd5e1;
--color-content-accent-dark: #94a3b8;
--color-content-inverse-dark: #0f172a;
/* Interactive Colors */
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-primary-active: #1d4ed8;
--color-primary-muted: #dbeafe;
--color-secondary: #64748b;
--color-secondary-hover: #475569;
--color-secondary-active: #334155;
--color-secondary-muted: #f1f5f9;
/* Status Colors */
--color-success: #22c55e;
--color-success-hover: #16a34a;
--color-success-muted: #dcfce7;
--color-success-text: #15803d;
--color-error: #ef4444;
--color-error-hover: #dc2626;
--color-error-muted: #fef2f2;
--color-error-text: #dc2626;
--color-warning: #f59e0b;
--color-warning-hover: #d97706;
--color-warning-muted: #fef3c7;
--color-warning-text: #d97706;
--color-info: #3b82f6;
--color-info-hover: #2563eb;
--color-info-muted: #dbeafe;
--color-info-text: #2563eb;
--color-processing: #a855f7;
--color-processing-hover: #9333ea;
--color-processing-muted: #f3e8ff;
--color-processing-text: #9333ea;
/* Border Colors */
--color-border: #e2e8f0;
--color-border-muted: #f1f5f9;
--color-border-accent: #cbd5e1;
--color-border-focus: #3b82f6;
--color-border-dark: #475569;
--color-border-muted-dark: #334155;
--color-border-accent-dark: #64748b;
/* Input Colors */
--color-input-background: #f1f5f9;
--color-input-border: #e2e8f0;
--color-input-focus: #3b82f6;
--color-input-background-dark: #1e293b;
--color-input-border-dark: #475569;
/* Button Colors */
--color-button-primary: #3b82f6;
--color-button-primary-hover: #2563eb;
--color-button-primary-text: #ffffff;
--color-button-secondary: #f1f5f9;
--color-button-secondary-hover: #64748b;
--color-button-secondary-text: #64748b;
--color-button-secondary-text-hover: #ffffff;
--color-button-success: #22c55e;
--color-button-success-hover: #16a34a;
--color-button-success-text: #ffffff;
--color-icon-button-hover: #e5e7eb;
--color-icon-button-hover-dark: #334155;
/* Icon Colors */
--color-icon-primary: #475569;
--color-icon-primary-hover: #334155;
--color-icon-primary-active: #1e293b;
--color-icon-primary-dark: #cbd5e1;
--color-icon-primary-hover-dark: #e2e8f0;
--color-icon-primary-active-dark: #f8fafc;
--color-icon-secondary: #64748b;
--color-icon-secondary-hover: #475569;
--color-icon-secondary-active: #334155;
--color-icon-secondary-dark: #94a3b8;
--color-icon-secondary-hover-dark: #cbd5e1;
--color-icon-secondary-active-dark: #e2e8f0;
--color-icon-muted: #94a3b8;
--color-icon-muted-hover: #64748b;
--color-icon-muted-active: #475569;
--color-icon-muted-dark: #64748b;
--color-icon-muted-hover-dark: #94a3b8;
--color-icon-muted-active-dark: #cbd5e1;
--color-icon-accent: #3b82f6;
--color-icon-accent-hover: #2563eb;
--color-icon-accent-active: #1d4ed8;
--color-icon-accent-dark: #60a5fa;
--color-icon-accent-hover-dark: #93c5fd;
--color-icon-accent-active-dark: #bfdbfe;
--color-icon-success: #22c55e;
--color-icon-success-hover: #16a34a;
--color-icon-success-active: #15803d;
--color-icon-success-dark: #4ade80;
--color-icon-success-hover-dark: #86efac;
--color-icon-success-active-dark: #bbf7d0;
--color-icon-error: #ef4444;
--color-icon-error-hover: #dc2626;
--color-icon-error-active: #b91c1c;
--color-icon-error-dark: #f87171;
--color-icon-error-hover-dark: #fca5a5;
--color-icon-error-active-dark: #fecaca;
--color-icon-warning: #f59e0b;
--color-icon-warning-hover: #d97706;
--color-icon-warning-active: #b45309;
--color-icon-warning-dark: #fbbf24;
--color-icon-warning-hover-dark: #fcd34d;
--color-icon-warning-active-dark: #fde68a;
--color-icon-inverse: #ffffff;
--color-icon-inverse-dark: #0f172a;
}
@layer base {
a {
@apply no-underline hover:underline cursor-pointer;
}
/* SVG Icon Utility Classes - Using filters for img elements */
.icon-primary {
fill: var(--color-icon-primary);
color: var(--color-icon-primary);
filter: brightness(0) saturate(100%) invert(27%) sepia(8%) saturate(1363%) hue-rotate(183deg) brightness(96%) contrast(89%);
}
.dark .icon-primary {
fill: var(--color-icon-primary-dark);
color: var(--color-icon-primary-dark);
filter: brightness(0) saturate(100%) invert(83%) sepia(8%) saturate(1018%) hue-rotate(183deg) brightness(96%) contrast(91%);
}
.icon-primary:hover {
fill: var(--color-icon-primary-hover);
color: var(--color-icon-primary-hover);
filter: brightness(0) saturate(100%) invert(21%) sepia(12%) saturate(1190%) hue-rotate(183deg) brightness(96%) contrast(92%);
}
.dark .icon-primary:hover {
fill: var(--color-icon-primary-hover-dark);
color: var(--color-icon-primary-hover-dark);
filter: brightness(0) saturate(100%) invert(90%) sepia(6%) saturate(668%) hue-rotate(183deg) brightness(97%) contrast(91%);
}
.icon-secondary {
fill: var(--color-icon-secondary);
color: var(--color-icon-secondary);
filter: brightness(0) saturate(100%) invert(48%) sepia(14%) saturate(725%) hue-rotate(183deg) brightness(94%) contrast(88%);
}
.dark .icon-secondary {
fill: var(--color-icon-secondary-dark);
color: var(--color-icon-secondary-dark);
filter: brightness(0) saturate(100%) invert(69%) sepia(11%) saturate(763%) hue-rotate(183deg) brightness(93%) contrast(89%);
}
.icon-secondary:hover {
fill: var(--color-icon-secondary-hover);
color: var(--color-icon-secondary-hover);
filter: brightness(0) saturate(100%) invert(27%) sepia(8%) saturate(1363%) hue-rotate(183deg) brightness(96%) contrast(89%);
}
.dark .icon-secondary:hover {
fill: var(--color-icon-secondary-hover-dark);
color: var(--color-icon-secondary-hover-dark);
filter: brightness(0) saturate(100%) invert(83%) sepia(8%) saturate(1018%) hue-rotate(183deg) brightness(96%) contrast(91%);
}
.icon-muted {
fill: var(--color-icon-muted);
color: var(--color-icon-muted);
filter: brightness(0) saturate(100%) invert(69%) sepia(11%) saturate(763%) hue-rotate(183deg) brightness(93%) contrast(89%);
}
.dark .icon-muted {
fill: var(--color-icon-muted-dark);
color: var(--color-icon-muted-dark);
filter: brightness(0) saturate(100%) invert(48%) sepia(14%) saturate(725%) hue-rotate(183deg) brightness(94%) contrast(88%);
}
.icon-muted:hover {
fill: var(--color-icon-muted-hover);
color: var(--color-icon-muted-hover);
filter: brightness(0) saturate(100%) invert(48%) sepia(14%) saturate(725%) hue-rotate(183deg) brightness(94%) contrast(88%);
}
.dark .icon-muted:hover {
fill: var(--color-icon-muted-hover-dark);
color: var(--color-icon-muted-hover-dark);
filter: brightness(0) saturate(100%) invert(69%) sepia(11%) saturate(763%) hue-rotate(183deg) brightness(93%) contrast(89%);
}
.icon-accent {
fill: var(--color-icon-accent);
color: var(--color-icon-accent);
filter: brightness(0) saturate(100%) invert(41%) sepia(96%) saturate(1347%) hue-rotate(215deg) brightness(99%) contrast(86%);
}
.dark .icon-accent {
fill: var(--color-icon-accent-dark);
color: var(--color-icon-accent-dark);
filter: brightness(0) saturate(100%) invert(67%) sepia(25%) saturate(2334%) hue-rotate(215deg) brightness(102%) contrast(96%);
}
.icon-accent:hover {
fill: var(--color-icon-accent-hover);
color: var(--color-icon-accent-hover);
filter: brightness(0) saturate(100%) invert(29%) sepia(81%) saturate(2476%) hue-rotate(215deg) brightness(98%) contrast(86%);
}
.dark .icon-accent:hover {
fill: var(--color-icon-accent-hover-dark);
color: var(--color-icon-accent-hover-dark);
filter: brightness(0) saturate(100%) invert(75%) sepia(28%) saturate(1388%) hue-rotate(215deg) brightness(104%) contrast(96%);
}
.icon-success {
fill: var(--color-icon-success);
color: var(--color-icon-success);
filter: brightness(0) saturate(100%) invert(47%) sepia(95%) saturate(450%) hue-rotate(92deg) brightness(98%) contrast(91%);
}
.dark .icon-success {
fill: var(--color-icon-success-dark);
color: var(--color-icon-success-dark);
filter: brightness(0) saturate(100%) invert(64%) sepia(78%) saturate(394%) hue-rotate(92deg) brightness(101%) contrast(89%);
}
.icon-success:hover {
fill: var(--color-icon-success-hover);
color: var(--color-icon-success-hover);
filter: brightness(0) saturate(100%) invert(42%) sepia(78%) saturate(440%) hue-rotate(92deg) brightness(96%) contrast(89%);
}
.dark .icon-success:hover {
fill: var(--color-icon-success-hover-dark);
color: var(--color-icon-success-hover-dark);
filter: brightness(0) saturate(100%) invert(75%) sepia(64%) saturate(295%) hue-rotate(92deg) brightness(103%) contrast(87%);
}
.icon-error {
fill: var(--color-icon-error);
color: var(--color-icon-error);
filter: brightness(0) saturate(100%) invert(30%) sepia(93%) saturate(1742%) hue-rotate(339deg) brightness(98%) contrast(94%);
}
.dark .icon-error {
fill: var(--color-icon-error-dark);
color: var(--color-icon-error-dark);
filter: brightness(0) saturate(100%) invert(62%) sepia(93%) saturate(1360%) hue-rotate(339deg) brightness(99%) contrast(96%);
}
.icon-error:hover {
fill: var(--color-icon-error-hover);
color: var(--color-icon-error-hover);
filter: brightness(0) saturate(100%) invert(17%) sepia(95%) saturate(2341%) hue-rotate(339deg) brightness(96%) contrast(94%);
}
.dark .icon-error:hover {
fill: var(--color-icon-error-hover-dark);
color: var(--color-icon-error-hover-dark);
filter: brightness(0) saturate(100%) invert(74%) sepia(29%) saturate(1214%) hue-rotate(339deg) brightness(99%) contrast(94%);
}
.icon-warning {
fill: var(--color-icon-warning);
color: var(--color-icon-warning);
filter: brightness(0) saturate(100%) invert(67%) sepia(98%) saturate(1284%) hue-rotate(12deg) brightness(95%) contrast(92%);
}
.dark .icon-warning {
fill: var(--color-icon-warning-dark);
color: var(--color-icon-warning-dark);
filter: brightness(0) saturate(100%) invert(84%) sepia(36%) saturate(1043%) hue-rotate(12deg) brightness(97%) contrast(91%);
}
.icon-warning:hover {
fill: var(--color-icon-warning-hover);
color: var(--color-icon-warning-hover);
filter: brightness(0) saturate(100%) invert(58%) sepia(89%) saturate(1619%) hue-rotate(12deg) brightness(94%) contrast(96%);
}
.dark .icon-warning:hover {
fill: var(--color-icon-warning-hover-dark);
color: var(--color-icon-warning-hover-dark);
filter: brightness(0) saturate(100%) invert(89%) sepia(21%) saturate(789%) hue-rotate(12deg) brightness(98%) contrast(88%);
}
.icon-inverse {
fill: var(--color-icon-inverse);
color: var(--color-icon-inverse);
filter: brightness(0) saturate(100%) invert(100%);
}
.dark .icon-inverse {
fill: var(--color-icon-inverse-dark);
color: var(--color-icon-inverse-dark);
filter: brightness(0) saturate(100%) invert(5%) sepia(8%) saturate(7470%) hue-rotate(183deg) brightness(97%) contrast(108%);
}
}

View File

@@ -74,9 +74,9 @@ export const Album = () => {
<div className="mb-6">
<button
onClick={() => window.history.back()}
className="flex items-center gap-2 text-sm font-semibold text-gray-600 hover:text-gray-900 transition-colors"
className="flex items-center gap-2 text-sm font-semibold text-content-secondary dark:text-content-secondary-dark hover:text-content-primary dark:hover:text-content-primary-dark transition-colors"
>
<FaArrowLeft />
<FaArrowLeft className="icon-secondary hover:icon-primary" />
<span>Back to results</span>
</button>
</div>
@@ -87,8 +87,8 @@ export const Album = () => {
className="w-48 h-48 object-cover rounded-lg shadow-lg"
/>
<div className="flex-grow space-y-2">
<h1 className="text-3xl font-bold">{album.name}</h1>
<p className="text-lg text-gray-500 dark:text-gray-400">
<h1 className="text-3xl font-bold text-content-primary dark:text-content-primary-dark">{album.name}</h1>
<p className="text-lg text-content-secondary dark:text-content-secondary-dark">
By{" "}
{album.artists.map((artist, index) => (
<span key={artist.id}>
@@ -99,16 +99,16 @@ export const Album = () => {
</span>
))}
</p>
<p className="text-sm text-gray-400 dark:text-gray-500">
<p className="text-sm text-content-muted dark:text-content-muted-dark">
{new Date(album.release_date).getFullYear()} {album.total_tracks} songs
</p>
<p className="text-xs text-gray-400 dark:text-gray-600">{album.label}</p>
<p className="text-xs text-content-muted dark:text-content-muted-dark">{album.label}</p>
</div>
<div className="flex flex-col items-center gap-2">
<button
onClick={handleDownloadAlbum}
disabled={isExplicitFilterEnabled && hasExplicitTrack}
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed"
className="w-full px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title={
isExplicitFilterEnabled && hasExplicitTrack ? "Album contains explicit tracks" : "Download Full Album"
}
@@ -119,33 +119,33 @@ export const Album = () => {
</div>
<div className="space-y-4">
<h2 className="text-xl font-semibold">Tracks</h2>
<h2 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Tracks</h2>
<div className="space-y-2">
{album.tracks.items.map((track, index) => {
if (isExplicitFilterEnabled && track.explicit) {
return (
<div
key={index}
className="flex items-center justify-between p-3 bg-gray-100 dark:bg-gray-800 rounded-lg opacity-50"
className="flex items-center justify-between p-3 bg-surface-muted dark:bg-surface-muted-dark rounded-lg opacity-50"
>
<div className="flex items-center gap-4">
<span className="text-gray-500 dark:text-gray-400 w-8 text-right">{index + 1}</span>
<p className="font-medium text-gray-500">Explicit track filtered</p>
<span className="text-content-muted dark:text-content-muted-dark w-8 text-right">{index + 1}</span>
<p className="font-medium text-content-muted dark:text-content-muted-dark">Explicit track filtered</p>
</div>
<span className="text-gray-500">--:--</span>
<span className="text-content-muted dark:text-content-muted-dark">--:--</span>
</div>
);
}
return (
<div
key={track.id}
className="flex items-center justify-between p-3 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
className="flex items-center justify-between p-3 hover:bg-surface-muted dark:hover:bg-surface-muted-dark rounded-lg transition-colors"
>
<div className="flex items-center gap-4">
<span className="text-gray-500 dark:text-gray-400 w-8 text-right">{index + 1}</span>
<span className="text-content-muted dark:text-content-muted-dark w-8 text-right">{index + 1}</span>
<div>
<p className="font-medium">{track.name}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
<p className="font-medium text-content-primary dark:text-content-primary-dark">{track.name}</p>
<p className="text-sm text-content-secondary dark:text-content-secondary-dark">
{track.artists.map((artist, index) => (
<span key={artist.id}>
<Link
@@ -164,16 +164,16 @@ export const Album = () => {
</div>
</div>
<div className="flex items-center gap-4">
<span className="text-gray-500 dark:text-gray-400">
<span className="text-content-muted dark:text-content-muted-dark">
{Math.floor(track.duration_ms / 60000)}:
{((track.duration_ms % 60000) / 1000).toFixed(0).padStart(2, "0")}
</span>
<button
onClick={() => handleDownloadTrack(track)}
className="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full"
className="p-2 hover:bg-surface-secondary dark:hover:bg-surface-secondary-dark rounded-full"
title="Download"
>
<img src="/download.svg" alt="Download" className="w-5 h-5" />
<img src="/download.svg" alt="Download" className="w-5 h-5 icon-secondary hover:icon-success" />
</button>
</div>
</div>

View File

@@ -133,9 +133,9 @@ export const Artist = () => {
<div className="mb-6">
<button
onClick={() => window.history.back()}
className="flex items-center gap-2 text-sm font-semibold text-gray-600 hover:text-gray-900 transition-colors"
className="flex items-center gap-2 text-sm font-semibold text-content-secondary dark:text-content-secondary-dark hover:text-content-primary dark:hover:text-content-primary-dark transition-colors"
>
<FaArrowLeft />
<FaArrowLeft className="icon-secondary hover:icon-primary" />
<span>Back to results</span>
</button>
</div>
@@ -147,31 +147,31 @@ export const Artist = () => {
className="artist-image w-48 h-48 rounded-full mx-auto mb-4 shadow-lg"
/>
)}
<h1 className="text-5xl font-bold">{artist.name}</h1>
<h1 className="text-5xl font-bold text-content-primary dark:text-content-primary-dark">{artist.name}</h1>
<div className="flex gap-4 justify-center mt-4">
<button
onClick={handleDownloadArtist}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors"
className="flex items-center gap-2 px-4 py-2 bg-button-success hover:bg-button-success-hover text-button-success-text rounded-md transition-colors"
>
<FaDownload />
<FaDownload className="icon-inverse" />
<span>Download All</span>
</button>
<button
onClick={handleToggleWatch}
className={`flex items-center gap-2 px-4 py-2 rounded-md transition-colors border ${
isWatched
? "bg-blue-500 text-white border-blue-500"
: "bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800"
? "bg-button-primary text-button-primary-text border-primary"
: "bg-surface dark:bg-surface-dark hover:bg-surface-muted dark:hover:bg-surface-muted-dark border-border dark:border-border-dark text-content-primary dark:text-content-primary-dark"
}`}
>
{isWatched ? (
<>
<FaBookmark />
<FaBookmark className="icon-inverse" />
<span>Watching</span>
</>
) : (
<>
<FaRegBookmark />
<FaRegBookmark className="icon-primary" />
<span>Watch</span>
</>
)}
@@ -181,17 +181,20 @@ export const Artist = () => {
{topTracks.length > 0 && (
<div className="mb-12">
<h2 className="text-3xl font-bold mb-6">Top Tracks</h2>
<h2 className="text-3xl font-bold mb-6 text-content-primary dark:text-content-primary-dark">Top Tracks</h2>
<div className="track-list space-y-2">
{topTracks.map((track) => (
<div
key={track.id}
className="track-item flex items-center justify-between p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
className="track-item flex items-center justify-between p-2 rounded-md hover:bg-surface-muted dark:hover:bg-surface-muted-dark transition-colors"
>
<Link to="/track/$trackId" params={{ trackId: track.id }} className="font-semibold">
<Link to="/track/$trackId" params={{ trackId: track.id }} className="font-semibold text-content-primary dark:text-content-primary-dark">
{track.name}
</Link>
<button onClick={() => handleDownloadTrack(track)} className="download-btn">
<button
onClick={() => handleDownloadTrack(track)}
className="px-3 py-1 bg-button-secondary hover:bg-button-secondary-hover text-button-secondary-text hover:text-button-secondary-text-hover rounded"
>
Download
</button>
</div>
@@ -202,7 +205,7 @@ export const Artist = () => {
{artistAlbums.length > 0 && (
<div className="mb-12">
<h2 className="text-3xl font-bold mb-6">Albums</h2>
<h2 className="text-3xl font-bold mb-6 text-content-primary dark:text-content-primary-dark">Albums</h2>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
{artistAlbums.map((album) => (
<AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} />
@@ -213,7 +216,7 @@ export const Artist = () => {
{artistSingles.length > 0 && (
<div className="mb-12">
<h2 className="text-3xl font-bold mb-6">Singles</h2>
<h2 className="text-3xl font-bold mb-6 text-content-primary dark:text-content-primary-dark">Singles</h2>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
{artistSingles.map((album) => (
<AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} />
@@ -224,7 +227,7 @@ export const Artist = () => {
{artistCompilations.length > 0 && (
<div className="mb-12">
<h2 className="text-3xl font-bold mb-6">Compilations</h2>
<h2 className="text-3xl font-bold mb-6 text-content-primary dark:text-content-primary-dark">Compilations</h2>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
{artistCompilations.map((album) => (
<AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} />

View File

@@ -14,8 +14,8 @@ const ConfigComponent = () => {
const { settings: config, isLoading } = useSettings();
const renderTabContent = () => {
if (isLoading) return <p className="text-center">Loading configuration...</p>;
if (!config) return <p className="text-center text-red-500">Error loading configuration.</p>;
if (isLoading) return <p className="text-center text-content-muted dark:text-content-muted-dark">Loading configuration...</p>;
if (!config) return <p className="text-center text-error-text bg-error-muted p-2 rounded">Error loading configuration.</p>;
switch (activeTab) {
case "general":
@@ -38,8 +38,8 @@ const ConfigComponent = () => {
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">Configuration</h1>
<p className="text-gray-500">Manage application settings and services.</p>
<h1 className="text-3xl font-bold text-content-primary dark:text-content-primary-dark">Configuration</h1>
<p className="text-content-muted dark:text-content-muted-dark">Manage application settings and services.</p>
</div>
<div className="flex gap-8">
@@ -47,37 +47,37 @@ const ConfigComponent = () => {
<nav className="flex flex-col space-y-1">
<button
onClick={() => setActiveTab("general")}
className={`p-2 rounded-md text-left ${activeTab === "general" ? "bg-gray-100 dark:bg-gray-800 font-semibold" : ""}`}
className={`p-2 rounded-md text-left transition-colors ${activeTab === "general" ? "bg-surface-accent dark:bg-surface-accent-dark font-semibold text-content-primary dark:text-content-primary-dark" : "text-content-secondary dark:text-content-secondary-dark hover:bg-surface-muted dark:hover:bg-surface-muted-dark"}`}
>
General
</button>
<button
onClick={() => setActiveTab("downloads")}
className={`p-2 rounded-md text-left ${activeTab === "downloads" ? "bg-gray-100 dark:bg-gray-800 font-semibold" : ""}`}
className={`p-2 rounded-md text-left transition-colors ${activeTab === "downloads" ? "bg-surface-accent dark:bg-surface-accent-dark font-semibold text-content-primary dark:text-content-primary-dark" : "text-content-secondary dark:text-content-secondary-dark hover:bg-surface-muted dark:hover:bg-surface-muted-dark"}`}
>
Downloads
</button>
<button
onClick={() => setActiveTab("formatting")}
className={`p-2 rounded-md text-left ${activeTab === "formatting" ? "bg-gray-100 dark:bg-gray-800 font-semibold" : ""}`}
className={`p-2 rounded-md text-left transition-colors ${activeTab === "formatting" ? "bg-surface-accent dark:bg-surface-accent-dark font-semibold text-content-primary dark:text-content-primary-dark" : "text-content-secondary dark:text-content-secondary-dark hover:bg-surface-muted dark:hover:bg-surface-muted-dark"}`}
>
Formatting
</button>
<button
onClick={() => setActiveTab("accounts")}
className={`p-2 rounded-md text-left ${activeTab === "accounts" ? "bg-gray-100 dark:bg-gray-800 font-semibold" : ""}`}
className={`p-2 rounded-md text-left transition-colors ${activeTab === "accounts" ? "bg-surface-accent dark:bg-surface-accent-dark font-semibold text-content-primary dark:text-content-primary-dark" : "text-content-secondary dark:text-content-secondary-dark hover:bg-surface-muted dark:hover:bg-surface-muted-dark"}`}
>
Accounts
</button>
<button
onClick={() => setActiveTab("watch")}
className={`p-2 rounded-md text-left ${activeTab === "watch" ? "bg-gray-100 dark:bg-gray-800 font-semibold" : ""}`}
className={`p-2 rounded-md text-left transition-colors ${activeTab === "watch" ? "bg-surface-accent dark:bg-surface-accent-dark font-semibold text-content-primary dark:text-content-primary-dark" : "text-content-secondary dark:text-content-secondary-dark hover:bg-surface-muted dark:hover:bg-surface-muted-dark"}`}
>
Watch
</button>
<button
onClick={() => setActiveTab("server")}
className={`p-2 rounded-md text-left ${activeTab === "server" ? "bg-gray-100 dark:bg-gray-800 font-semibold" : ""}`}
className={`p-2 rounded-md text-left transition-colors ${activeTab === "server" ? "bg-surface-accent dark:bg-surface-accent-dark font-semibold text-content-primary dark:text-content-primary-dark" : "text-content-secondary dark:text-content-secondary-dark hover:bg-surface-muted dark:hover:bg-surface-muted-dark"}`}
>
Server
</button>

View File

@@ -32,10 +32,10 @@ type HistoryEntry = {
};
const STATUS_CLASS: Record<string, string> = {
COMPLETED: "text-green-500",
ERROR: "text-red-500",
CANCELLED: "text-gray-500",
SKIPPED: "text-yellow-500",
COMPLETED: "text-success",
ERROR: "text-error",
CANCELLED: "text-content-muted dark:text-content-muted-dark",
SKIPPED: "text-warning",
};
const QUALITY_MAP: Record<string, Record<string, string>> = {
@@ -140,12 +140,12 @@ export const History = () => {
const statusKey = (status || "").toUpperCase();
const statusClass =
{
COMPLETED: "text-green-500",
SUCCESSFUL: "text-green-500",
ERROR: "text-red-500",
FAILED: "text-red-500",
CANCELLED: "text-gray-500",
SKIPPED: "text-yellow-500",
COMPLETED: "text-success",
SUCCESSFUL: "text-success",
ERROR: "text-error",
FAILED: "text-error",
CANCELLED: "text-content-muted dark:text-content-muted-dark",
SKIPPED: "text-warning",
}[statusKey] || "text-gray-500";
return <span className={`font-semibold ${statusClass}`}>{status}</span>;
@@ -304,43 +304,43 @@ export const History = () => {
<div className="space-y-4">
{parentTaskId && parentTask ? (
<div className="space-y-4">
<button onClick={viewParentTask} className="flex items-center gap-2 text-sm hover:underline">
<button onClick={viewParentTask} className="flex items-center gap-2 text-sm hover:underline text-content-secondary dark:text-content-secondary-dark hover:text-content-primary dark:hover:text-content-primary-dark">
&larr; Back to All History
</button>
<div className="rounded-lg border bg-gradient-to-br from-card to-muted/30 p-6 shadow-lg">
<div className="rounded-lg border border-border dark:border-border-dark bg-gradient-to-br from-surface to-surface-muted dark:from-surface-dark dark:to-surface-muted-dark p-6 shadow-lg">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="md:col-span-2 space-y-1.5">
<h2 className="text-3xl font-bold tracking-tight">{parentTask.item_name}</h2>
<p className="text-xl text-muted-foreground">{parentTask.item_artist}</p>
<h2 className="text-3xl font-bold tracking-tight text-content-primary dark:text-content-primary-dark">{parentTask.item_name}</h2>
<p className="text-xl text-content-secondary dark:text-content-secondary-dark">{parentTask.item_artist}</p>
<div className="pt-2">
<span className="capitalize inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold bg-secondary text-secondary-foreground">
<span className="capitalize inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold bg-surface-accent dark:bg-surface-accent-dark text-content-primary dark:text-content-primary-dark">
{parentTask.download_type}
</span>
</div>
</div>
<div className="space-y-2 text-sm md:text-right">
<div
className={`inline-flex items-center rounded-full border px-3 py-1 text-base font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${
className={`inline-flex items-center rounded-full border border-border dark:border-border-dark px-3 py-1 text-base font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 ${
STATUS_CLASS[parentTask.status_final]
}`}
>
{parentTask.status_final}
</div>
<p className="text-muted-foreground pt-2">
<span className="font-semibold text-foreground">Quality: </span>
<p className="text-content-muted dark:text-content-muted-dark pt-2">
<span className="font-semibold text-content-primary dark:text-content-primary-dark">Quality: </span>
{formatQuality(parentTask)}
</p>
<p className="text-muted-foreground">
<span className="font-semibold text-foreground">Completed: </span>
<p className="text-content-muted dark:text-content-muted-dark">
<span className="font-semibold text-content-primary dark:text-content-primary-dark">Completed: </span>
{new Date(parentTask.timestamp_completed * 1000).toLocaleString()}
</p>
</div>
</div>
</div>
<h3 className="text-2xl font-bold tracking-tight pt-4">Tracks</h3>
<h3 className="text-2xl font-bold tracking-tight pt-4 text-content-primary dark:text-content-primary-dark">Tracks</h3>
</div>
) : (
<h1 className="text-3xl font-bold">Download History</h1>
<h1 className="text-3xl font-bold text-content-primary dark:text-content-primary-dark">Download History</h1>
)}
{/* Filter Controls */}
@@ -349,7 +349,7 @@ export const History = () => {
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="p-2 border rounded-md dark:bg-gray-800 dark:border-gray-700"
className="p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
>
<option value="">All Statuses</option>
<option value="COMPLETED">Completed</option>
@@ -360,7 +360,7 @@ export const History = () => {
<select
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
className="p-2 border rounded-md dark:bg-gray-800 dark:border-gray-700"
className="p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
>
<option value="">All Types</option>
<option value="track">Track</option>
@@ -371,14 +371,14 @@ export const History = () => {
<select
value={trackStatusFilter}
onChange={(e) => setTrackStatusFilter(e.target.value)}
className="p-2 border rounded-md dark:bg-gray-800 dark:border-gray-700"
className="p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
>
<option value="">All Track Statuses</option>
<option value="SUCCESSFUL">Successful</option>
<option value="SKIPPED">Skipped</option>
<option value="FAILED">Failed</option>
</select>
<label className="flex items-center gap-2">
<label className="flex items-center gap-2 text-content-primary dark:text-content-primary-dark">
<input
type="checkbox"
checked={showChildTracks}
@@ -397,7 +397,7 @@ export const History = () => {
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id} className="p-2 text-left">
<th key={header.id} className="p-2 text-left text-content-primary dark:text-content-primary-dark">
{header.isPlaceholder ? null : (
<div
{...{
@@ -417,13 +417,13 @@ export const History = () => {
<tbody>
{isLoading ? (
<tr>
<td colSpan={columns.length} className="text-center p-4">
<td colSpan={columns.length} className="text-center p-4 text-content-muted dark:text-content-muted-dark">
Loading...
</td>
</tr>
) : table.getRowModel().rows.length === 0 ? (
<tr>
<td colSpan={columns.length} className="text-center p-4">
<td colSpan={columns.length} className="text-center p-4 text-content-muted dark:text-content-muted-dark">
No history entries found.
</td>
</tr>
@@ -433,17 +433,17 @@ export const History = () => {
!row.original.parent_task_id &&
(row.original.download_type === "album" || row.original.download_type === "playlist");
const isChild = !!row.original.parent_task_id;
let rowClass = "hover:bg-muted/50";
let rowClass = "hover:bg-surface-muted dark:hover:bg-surface-muted-dark";
if (isParent) {
rowClass += " bg-muted/50 font-semibold hover:bg-muted";
rowClass += " bg-surface-accent dark:bg-surface-accent-dark font-semibold hover:bg-surface-muted dark:hover:bg-surface-muted-dark";
} else if (isChild) {
rowClass += " border-t border-dashed border-muted-foreground/20";
rowClass += " border-t border-dashed border-content-muted dark:border-content-muted-dark border-opacity-20";
}
return (
<tr key={row.id} className={`border-b border-border ${rowClass}`}>
<tr key={row.id} className={`border-b border-border dark:border-border-dark ${rowClass}`}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="p-3">
<td key={cell.id} className="p-3 text-content-primary dark:text-content-primary-dark">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
@@ -460,11 +460,11 @@ export const History = () => {
<button
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
className="p-2 border rounded-md disabled:opacity-50"
className="p-2 border bg-button-secondary hover:bg-button-secondary-hover text-button-secondary-text hover:text-button-secondary-text-hover border-border dark:border-border-dark rounded-md disabled:opacity-50"
>
Previous
</button>
<span>
<span className="text-content-primary dark:text-content-primary-dark">
Page{" "}
<strong>
{table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
@@ -473,14 +473,14 @@ export const History = () => {
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
className="p-2 border rounded-md disabled:opacity-50"
className="p-2 border bg-button-secondary hover:bg-button-secondary-hover text-button-secondary-text hover:text-button-secondary-text-hover border-border dark:border-border-dark rounded-md disabled:opacity-50"
>
Next
</button>
<select
value={table.getState().pagination.pageSize}
onChange={(e) => table.setPageSize(Number(e.target.value))}
className="p-2 border rounded-md dark:bg-gray-800 dark:border-gray-700"
className="p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
>
{[10, 25, 50, 100].map((size) => (
<option key={size} value={size}>

View File

@@ -130,19 +130,19 @@ export const Home = () => {
return (
<div className="max-w-4xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">Search Spotify</h1>
<h1 className="text-2xl font-bold mb-6 text-content-primary dark:text-content-primary-dark">Search Spotify</h1>
<div className="flex flex-col sm:flex-row gap-3 mb-6">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search for a track, album, or artist"
className="flex-1 p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
className="flex-1 p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
/>
<select
value={searchType}
onChange={(e) => setSearchType(e.target.value as "track" | "album" | "artist" | "playlist")}
className="p-2 border rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
className="p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
>
<option value="track">Track</option>
<option value="album">Album</option>
@@ -151,12 +151,12 @@ export const Home = () => {
</select>
</div>
{isLoading ? (
<p className="text-center my-4">Loading results...</p>
<p className="text-center my-4 text-content-muted dark:text-content-muted-dark">Loading results...</p>
) : (
<>
{resultComponent}
<div ref={loaderRef} />
{isLoadingMore && <p className="text-center my-4">Loading more results...</p>}
{isLoadingMore && <p className="text-center my-4 text-content-muted dark:text-content-muted-dark">Loading more results...</p>}
</>
)}
</div>

View File

@@ -179,9 +179,9 @@ export const Playlist = () => {
<div className="mb-6">
<button
onClick={() => window.history.back()}
className="flex items-center gap-2 text-sm font-semibold text-gray-600 hover:text-gray-900 transition-colors"
className="flex items-center gap-2 text-sm font-semibold text-content-secondary dark:text-content-secondary-dark hover:text-content-primary dark:hover:text-content-primary-dark transition-colors"
>
<FaArrowLeft />
<FaArrowLeft className="icon-secondary hover:icon-primary" />
<span>Back to results</span>
</button>
</div>
@@ -194,11 +194,11 @@ export const Playlist = () => {
className="w-48 h-48 object-cover rounded-lg shadow-lg"
/>
<div className="flex-grow space-y-2">
<h1 className="text-3xl font-bold">{playlistMetadata.name}</h1>
<h1 className="text-3xl font-bold text-content-primary dark:text-content-primary-dark">{playlistMetadata.name}</h1>
{playlistMetadata.description && (
<p className="text-gray-500 dark:text-gray-400">{playlistMetadata.description}</p>
<p className="text-content-secondary dark:text-content-secondary-dark">{playlistMetadata.description}</p>
)}
<div className="text-sm text-gray-400 dark:text-gray-500">
<div className="text-sm text-content-muted dark:text-content-muted-dark">
<p>
By {playlistMetadata.owner.display_name} {playlistMetadata.followers.total.toLocaleString()} followers {" "}
{totalTracks} songs
@@ -207,7 +207,7 @@ export const Playlist = () => {
<div className="flex gap-2 pt-2">
<button
onClick={handleDownloadPlaylist}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-lg transition-colors"
>
Download All
</button>
@@ -215,15 +215,14 @@ export const Playlist = () => {
onClick={handleToggleWatch}
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors ${
isWatched
? "bg-red-600 text-white hover:bg-red-700"
: "bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600"
? "bg-error hover:bg-error-hover text-button-primary-text"
: "bg-surface-muted dark:bg-surface-muted-dark hover:bg-surface-accent dark:hover:bg-surface-accent-dark text-content-primary dark:text-content-primary-dark"
}`}
>
<img
src={isWatched ? "/eye-crossed.svg" : "/eye.svg"}
alt="Watch status"
className="w-5 h-5"
style={{ filter: !isWatched ? "invert(1)" : undefined }}
className={`w-5 h-5 ${isWatched ? "icon-inverse" : "icon-primary"}`}
/>
{isWatched ? "Unwatch" : "Watch"}
</button>
@@ -234,9 +233,9 @@ export const Playlist = () => {
{/* Tracks Section */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold">Tracks</h2>
<h2 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Tracks</h2>
{tracks.length > 0 && (
<span className="text-sm text-gray-500">
<span className="text-sm text-content-muted dark:text-content-muted-dark">
Showing {tracks.length} of {totalTracks} tracks
</span>
)}
@@ -248,20 +247,20 @@ export const Playlist = () => {
return (
<div
key={track.id}
className="flex items-center justify-between p-3 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
className="flex items-center justify-between p-3 hover:bg-surface-muted dark:hover:bg-surface-muted-dark rounded-lg transition-colors"
>
<div className="flex items-center gap-4">
<span className="text-gray-500 dark:text-gray-400 w-8 text-right">{index + 1}</span>
<span className="text-content-muted dark:text-content-muted-dark w-8 text-right">{index + 1}</span>
<img
src={track.album.images.at(-1)?.url}
alt={track.album.name}
className="w-10 h-10 object-cover rounded"
/>
<div>
<Link to="/track/$trackId" params={{ trackId: track.id }} className="font-medium hover:underline">
<Link to="/track/$trackId" params={{ trackId: track.id }} className="font-medium hover:underline text-content-primary dark:text-content-primary-dark">
{track.name}
</Link>
<p className="text-sm text-gray-500 dark:text-gray-400">
<p className="text-sm text-content-secondary dark:text-content-secondary-dark">
{track.artists.map((artist, index) => (
<span key={artist.id}>
<Link to="/artist/$artistId" params={{ artistId: artist.id }} className="hover:underline">
@@ -274,16 +273,16 @@ export const Playlist = () => {
</div>
</div>
<div className="flex items-center gap-4">
<span className="text-gray-500 dark:text-gray-400">
<span className="text-content-muted dark:text-content-muted-dark">
{Math.floor(track.duration_ms / 60000)}:
{((track.duration_ms % 60000) / 1000).toFixed(0).padStart(2, "0")}
</span>
<button
onClick={() => handleDownloadTrack(track)}
className="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full"
className="p-2 hover:bg-surface-secondary dark:hover:bg-surface-secondary-dark rounded-full"
title="Download"
>
<FaDownload />
<FaDownload className="icon-secondary hover:icon-success" />
</button>
</div>
</div>
@@ -293,7 +292,7 @@ export const Playlist = () => {
{/* Loading indicator */}
{loadingTracks && (
<div className="flex justify-center py-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
)}
@@ -304,7 +303,7 @@ export const Playlist = () => {
{/* End of tracks indicator */}
{!hasMoreTracks && tracks.length > 0 && (
<div className="text-center py-4 text-gray-500">
<div className="text-center py-4 text-content-muted dark:text-content-muted-dark">
All tracks loaded
</div>
)}

View File

@@ -15,25 +15,25 @@ function AppLayout() {
return (
<>
<div className="min-h-screen bg-background text-foreground">
<header className="sticky top-0 z-40 w-full border-b bg-background/95 backdrop-blur-sm">
<div className="min-h-screen bg-surface dark:bg-surface-dark text-content-primary dark:text-content-primary-dark">
<header className="sticky top-0 z-40 w-full border-b border-border dark:border-border-dark bg-surface-overlay dark:bg-surface-overlay-dark backdrop-blur-sm">
<div className="container mx-auto h-14 flex items-center justify-between">
<Link to="/" className="flex items-center gap-2">
<img src="/music.svg" alt="Logo" className="w-6 h-6" />
<img src="/music.svg" alt="Logo" className="w-6 h-6 icon-inverse" />
<h1 className="text-xl font-bold">Spotizerr</h1>
</Link>
<div className="flex items-center gap-2">
<Link to="/watchlist" className="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<img src="/binoculars.svg" alt="Watchlist" className="w-6 h-6" />
<Link to="/watchlist" className="p-2 rounded-full hover:bg-icon-button-hover dark:hover:bg-icon-button-hover-dark">
<img src="/binoculars.svg" alt="Watchlist" className="w-6 h-6 icon-inverse hover:icon-accent" />
</Link>
<Link to="/history" className="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<img src="/history.svg" alt="History" className="w-6 h-6" />
<Link to="/history" className="p-2 rounded-full hover:bg-icon-button-hover dark:hover:bg-icon-button-hover-dark">
<img src="/history.svg" alt="History" className="w-6 h-6 icon-inverse hover:icon-accent" />
</Link>
<Link to="/config" className="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<img src="/settings.svg" alt="Settings" className="w-6 h-6" />
<Link to="/config" className="p-2 rounded-full hover:bg-icon-button-hover dark:hover:bg-icon-button-hover-dark">
<img src="/settings.svg" alt="Settings" className="w-6 h-6 icon-inverse hover:icon-accent" />
</Link>
<button onClick={toggleVisibility} className="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<img src="/queue.svg" alt="Queue" className="w-6 h-6" />
<button onClick={toggleVisibility} className="p-2 rounded-full hover:bg-icon-button-hover dark:hover:bg-icon-button-hover-dark">
<img src="/queue.svg" alt="Queue" className="w-6 h-6 icon-inverse hover:icon-accent" />
</button>
</div>
</div>
@@ -48,7 +48,7 @@ function AppLayout() {
);
}
export function Root() {
export const Root = () => {
return (
<QueryClientProvider client={queryClient}>
<SettingsProvider>
@@ -58,4 +58,4 @@ export function Root() {
</SettingsProvider>
</QueryClientProvider>
);
}
};

View File

@@ -47,7 +47,7 @@ export const Track = () => {
if (error) {
return (
<div className="flex justify-center items-center h-full">
<p className="text-red-500 text-lg">{error}</p>
<p className="text-error text-lg">{error}</p>
</div>
);
}
@@ -55,7 +55,7 @@ export const Track = () => {
if (!track) {
return (
<div className="flex justify-center items-center h-full">
<p className="text-lg">Loading...</p>
<p className="text-lg text-content-muted dark:text-content-muted-dark">Loading...</p>
</div>
);
}
@@ -67,13 +67,13 @@ export const Track = () => {
<div className="mb-6">
<button
onClick={() => window.history.back()}
className="flex items-center gap-2 text-sm font-semibold text-gray-600 hover:text-gray-900 transition-colors"
className="flex items-center gap-2 text-sm font-semibold text-content-secondary dark:text-content-secondary-dark hover:text-content-primary dark:hover:text-content-primary-dark transition-colors"
>
<FaArrowLeft />
<FaArrowLeft className="icon-secondary hover:icon-primary" />
<span>Back to results</span>
</button>
</div>
<div className="bg-white shadow-lg rounded-lg overflow-hidden md:flex">
<div className="bg-surface dark:bg-surface-secondary-dark shadow-lg rounded-lg overflow-hidden md:flex">
{imageUrl && (
<div className="md:w-1/3">
<img src={imageUrl} alt={track.album.name} className="w-full h-auto object-cover" />
@@ -82,12 +82,12 @@ export const Track = () => {
<div className="p-6 md:w-2/3 flex flex-col justify-between">
<div>
<div className="flex items-baseline justify-between">
<h1 className="text-3xl font-bold text-gray-900">{track.name}</h1>
<h1 className="text-3xl font-bold text-content-primary dark:text-content-primary-dark">{track.name}</h1>
{track.explicit && (
<span className="text-xs bg-gray-700 text-white px-2 py-1 rounded-full">EXPLICIT</span>
<span className="text-xs bg-surface-accent dark:bg-surface-accent-dark text-content-inverse dark:text-content-inverse-dark px-2 py-1 rounded-full">EXPLICIT</span>
)}
</div>
<div className="text-lg text-gray-600 mt-1">
<div className="text-lg text-content-secondary dark:text-content-secondary-dark mt-1">
{track.artists.map((artist, index) => (
<span key={artist.id}>
<Link to="/artist/$artistId" params={{ artistId: artist.id }}>
@@ -97,27 +97,27 @@ export const Track = () => {
</span>
))}
</div>
<p className="text-md text-gray-500 mt-4">
<p className="text-md text-content-muted dark:text-content-muted-dark mt-4">
From the album{" "}
<Link to="/album/$albumId" params={{ albumId: track.album.id }} className="font-semibold">
{track.album.name}
</Link>
</p>
<div className="mt-4 text-sm text-gray-600">
<div className="mt-4 text-sm text-content-secondary dark:text-content-secondary-dark">
<p>Release Date: {track.album.release_date}</p>
<p>Duration: {formatDuration(track.duration_ms)}</p>
</div>
<div className="mt-4">
<p className="text-sm text-gray-600">Popularity:</p>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div className="bg-green-500 h-2.5 rounded-full" style={{ width: `${track.popularity}%` }}></div>
<p className="text-sm text-content-secondary dark:text-content-secondary-dark">Popularity:</p>
<div className="w-full bg-surface-muted dark:bg-surface-muted-dark rounded-full h-2.5">
<div className="bg-success h-2.5 rounded-full" style={{ width: `${track.popularity}%` }}></div>
</div>
</div>
</div>
<div className="flex items-center gap-4 mt-6">
<button
onClick={handleDownloadTrack}
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-full transition duration-300"
className="bg-button-primary hover:bg-button-primary-hover text-button-primary-text font-bold py-2 px-4 rounded-full transition duration-300"
>
Download
</button>
@@ -125,10 +125,10 @@ export const Track = () => {
href={track.external_urls.spotify}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-gray-700 hover:text-black transition duration-300"
className="flex items-center gap-2 text-content-secondary dark:text-content-secondary-dark hover:text-content-primary dark:hover:text-content-primary-dark transition duration-300"
aria-label="Listen on Spotify"
>
<FaSpotify size={24} />
<FaSpotify size={24} className="icon-secondary hover:icon-primary" />
<span className="font-semibold">Listen on Spotify</span>
</a>
</div>

View File

@@ -95,15 +95,15 @@ export const Watchlist = () => {
};
if (isLoading || settingsLoading) {
return <div className="text-center">Loading Watchlist...</div>;
return <div className="text-center text-content-muted dark:text-content-muted-dark">Loading Watchlist...</div>;
}
if (!settings?.watch?.enabled) {
return (
<div className="text-center p-8">
<h2 className="text-2xl font-bold mb-2">Watchlist Disabled</h2>
<p>The watchlist feature is currently disabled. You can enable it in the settings.</p>
<Link to="/config" className="text-blue-500 hover:underline mt-4 inline-block">
<h2 className="text-2xl font-bold mb-2 text-content-primary dark:text-content-primary-dark">Watchlist Disabled</h2>
<p className="text-content-secondary dark:text-content-secondary-dark">The watchlist feature is currently disabled. You can enable it in the settings.</p>
<Link to="/config" className="text-primary hover:underline mt-4 inline-block">
Go to Settings
</Link>
</div>
@@ -113,8 +113,8 @@ export const Watchlist = () => {
if (items.length === 0) {
return (
<div className="text-center p-8">
<h2 className="text-2xl font-bold mb-2">Watchlist is Empty</h2>
<p>Start watching artists or playlists to see them here.</p>
<h2 className="text-2xl font-bold mb-2 text-content-primary dark:text-content-primary-dark">Watchlist is Empty</h2>
<p className="text-content-secondary dark:text-content-secondary-dark">Start watching artists or playlists to see them here.</p>
</div>
);
}
@@ -122,38 +122,38 @@ export const Watchlist = () => {
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold">Watched Artists & Playlists</h1>
<h1 className="text-3xl font-bold text-content-primary dark:text-content-primary-dark">Watched Artists & Playlists</h1>
<button
onClick={handleCheckAll}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center gap-2"
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md flex items-center gap-2"
>
<FaSearch /> Check All
<FaSearch className="icon-inverse" /> Check All
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{items.map((item) => (
<div key={item.id} className="bg-card p-4 rounded-lg shadow space-y-2 flex flex-col">
<div key={item.id} className="bg-surface dark:bg-surface-secondary-dark p-4 rounded-lg shadow space-y-2 flex flex-col">
<a href={`/${item.itemType}/${item.id}`} className="flex-grow">
<img
src={item.images?.[0]?.url || "/images/placeholder.jpg"}
alt={item.name}
className="w-full h-auto object-cover rounded-md aspect-square"
/>
<h3 className="font-bold pt-2 truncate">{item.name}</h3>
<p className="text-sm text-muted-foreground capitalize">{item.itemType}</p>
<h3 className="font-bold pt-2 truncate text-content-primary dark:text-content-primary-dark">{item.name}</h3>
<p className="text-sm text-content-muted dark:text-content-muted-dark capitalize">{item.itemType}</p>
</a>
<div className="flex gap-2 pt-2">
<button
onClick={() => handleUnwatch(item)}
className="w-full px-3 py-1.5 text-sm bg-red-600 text-white rounded-md hover:bg-red-700 flex items-center justify-center gap-2"
className="w-full px-3 py-1.5 text-sm bg-error hover:bg-error-hover text-button-primary-text rounded-md flex items-center justify-center gap-2"
>
<FaRegTrashAlt /> Unwatch
<FaRegTrashAlt className="icon-inverse" /> Unwatch
</button>
<button
onClick={() => handleCheck(item)}
className="w-full px-3 py-1.5 text-sm bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center justify-center gap-2"
className="w-full px-3 py-1.5 text-sm bg-button-secondary hover:bg-button-secondary-hover text-button-secondary-text hover:text-button-secondary-text-hover rounded-md flex items-center justify-center gap-2"
>
<FaSearch /> Check
<FaSearch className="icon-secondary hover:icon-primary" /> Check
</button>
</div>
</div>