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(", "); const subtitle = album.artists.map((artist) => artist.name).join(", ");
return ( 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"> <div className="relative">
<Link to="/album/$albumId" params={{ albumId: album.id }}> <Link to="/album/$albumId" params={{ albumId: album.id }}>
<img src={imageUrl} alt={album.name} className="w-full aspect-square object-cover" /> <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(); e.preventDefault();
onDownload(); 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" 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> </button>
)} )}
</Link> </Link>
@@ -33,11 +33,11 @@ export const AlbumCard = ({ album, onDownload }: AlbumCardProps) => {
<Link <Link
to="/album/$albumId" to="/album/$albumId"
params={{ albumId: album.id }} 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} {album.name}
</Link> </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>
</div> </div>
); );

View File

@@ -15,72 +15,83 @@ const isTerminalStatus = (status: QueueStatus) =>
const statusStyles: Record< const statusStyles: Record<
QueueStatus, QueueStatus,
{ icon: React.ReactNode; color: string; bgColor: string; name: string } { icon: React.ReactNode; color: string; bgColor: string; borderColor: string; name: string }
> = { > = {
queued: { queued: {
icon: <FaHourglassHalf />, icon: <FaHourglassHalf className="icon-muted" />,
color: "text-gray-500", color: "text-content-muted dark:text-content-muted-dark",
bgColor: "bg-gray-100", 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", name: "Queued",
}, },
initializing: { initializing: {
icon: <FaSync className="animate-spin" />, icon: <FaSync className="animate-spin icon-accent" />,
color: "text-blue-500", color: "text-info",
bgColor: "bg-blue-100", 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", name: "Initializing",
}, },
downloading: { downloading: {
icon: <FaSync className="animate-spin" />, icon: <FaSync className="animate-spin icon-accent" />,
color: "text-blue-500", color: "text-info",
bgColor: "bg-blue-100", 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", name: "Downloading",
}, },
processing: { processing: {
icon: <FaSync className="animate-spin" />, icon: <FaSync className="animate-spin icon-warning" />,
color: "text-purple-500", color: "text-processing",
bgColor: "bg-purple-100", 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", name: "Processing",
}, },
retrying: { retrying: {
icon: <FaSync className="animate-spin" />, icon: <FaSync className="animate-spin icon-warning" />,
color: "text-orange-500", color: "text-warning",
bgColor: "bg-orange-100", 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", name: "Retrying",
}, },
completed: { completed: {
icon: <FaCheckCircle />, icon: <FaCheckCircle className="icon-success" />,
color: "text-green-500", color: "text-success",
bgColor: "bg-green-100", 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", name: "Completed",
}, },
done: { done: {
icon: <FaCheckCircle />, icon: <FaCheckCircle className="icon-success" />,
color: "text-green-500", color: "text-success",
bgColor: "bg-green-100", 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", name: "Done",
}, },
error: { error: {
icon: <FaExclamationCircle />, icon: <FaExclamationCircle className="icon-error" />,
color: "text-red-500", color: "text-error",
bgColor: "bg-red-100", 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", name: "Error",
}, },
cancelled: { cancelled: {
icon: <FaTimes />, icon: <FaTimes className="icon-warning" />,
color: "text-yellow-500", color: "text-warning",
bgColor: "bg-yellow-100", 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", name: "Cancelled",
}, },
skipped: { skipped: {
icon: <FaTimes />, icon: <FaTimes className="icon-muted" />,
color: "text-gray-500", color: "text-content-muted dark:text-content-muted-dark",
bgColor: "bg-gray-100", 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", name: "Skipped",
}, },
pending: { pending: {
icon: <FaHourglassHalf />, icon: <FaHourglassHalf className="icon-muted" />,
color: "text-gray-500", color: "text-content-muted dark:text-content-muted-dark",
bgColor: "bg-gray-100", 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", name: "Pending",
}, },
}; };
@@ -116,24 +127,26 @@ const QueueItemCard = ({ item }: { item: QueueItem }) => {
const progressText = getProgressText(); const progressText = getProgressText();
return ( 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 justify-between">
<div className="flex items-center gap-4 min-w-0"> <div className="flex items-center gap-4 min-w-0 flex-1">
<div className={`text-2xl ${statusInfo.color}`}>{statusInfo.icon}</div> <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"> <div className="flex-grow min-w-0">
{item.type === "track" && ( {item.type === "track" && (
<> <>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaMusic className="text-gray-500" /> <FaMusic className="icon-muted text-sm" />
<p className="font-bold text-gray-800 truncate" title={item.name}> <p className="font-bold text-content-primary dark:text-content-primary-dark truncate" title={item.name}>
{item.name} {item.name}
</p> </p>
</div> </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} {item.artist}
</p> </p>
{item.albumName && ( {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} {item.albumName}
</p> </p>
)} )}
@@ -142,16 +155,16 @@ const QueueItemCard = ({ item }: { item: QueueItem }) => {
{item.type === "album" && ( {item.type === "album" && (
<> <>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaCompactDisc className="text-gray-500" /> <FaCompactDisc className="icon-muted text-sm" />
<p className="font-bold text-gray-800 truncate" title={item.name}> <p className="font-bold text-content-primary dark:text-content-primary-dark truncate" title={item.name}>
{item.name} {item.name}
</p> </p>
</div> </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} {item.artist}
</p> </p>
{item.currentTrackTitle && ( {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} {item.currentTrackNumber}/{item.totalTracks}: {item.currentTrackTitle}
</p> </p>
)} )}
@@ -160,16 +173,16 @@ const QueueItemCard = ({ item }: { item: QueueItem }) => {
{item.type === "playlist" && ( {item.type === "playlist" && (
<> <>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaMusic className="text-gray-500" /> <FaMusic className="icon-muted text-sm" />
<p className="font-bold text-gray-800 truncate" title={item.name}> <p className="font-bold text-content-primary dark:text-content-primary-dark truncate" title={item.name}>
{item.name} {item.name}
</p> </p>
</div> </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} {item.playlistOwner}
</p> </p>
{item.currentTrackTitle && ( {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} {item.currentTrackNumber}/{item.totalTracks}: {item.currentTrackTitle}
</p> </p>
)} )}
@@ -177,60 +190,82 @@ const QueueItemCard = ({ item }: { item: QueueItem }) => {
)} )}
</div> </div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-3 ml-4">
<div className="text-right"> <div className="text-right">
<p className={`text-sm font-semibold ${statusInfo.color}`}>{statusInfo.name}</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`}>
{progressText && <p className="text-xs text-gray-500">{progressText}</p>} {statusInfo.name}
</div>
{progressText && <p className="text-xs text-content-muted dark:text-content-muted-dark mt-1">{progressText}</p>}
</div>
<div className="flex gap-1">
{isTerminal ? (
<button
onClick={() => removeItem?.(item.id)}
className="p-2 rounded-full bg-white/60 dark:bg-surface-dark/60 text-content-muted dark:text-content-muted-dark hover:text-error hover:bg-error/10 transition-all duration-200 shadow-sm"
aria-label="Remove"
>
<FaTimes className="text-sm" />
</button>
) : (
<button
onClick={() => cancelItem?.(item.id)}
className="p-2 rounded-full bg-white/60 dark:bg-surface-dark/60 text-content-muted dark:text-content-muted-dark hover:text-warning hover:bg-warning/10 transition-all duration-200 shadow-sm"
aria-label="Cancel"
>
<FaTimes className="text-sm" />
</button>
)}
{item.canRetry && (
<button
onClick={() => retryItem?.(item.id)}
className="p-2 rounded-full bg-white/60 dark:bg-surface-dark/60 text-content-muted dark:text-content-muted-dark hover:text-info hover:bg-info/10 transition-all duration-200 shadow-sm"
aria-label="Retry"
>
<FaSync className="text-sm" />
</button>
)}
</div> </div>
{isTerminal ? (
<button
onClick={() => removeItem?.(item.id)}
className="text-gray-400 hover:text-red-500 transition-colors"
aria-label="Remove"
>
<FaTimes />
</button>
) : (
<button
onClick={() => cancelItem?.(item.id)}
className="text-gray-400 hover:text-orange-500 transition-colors"
aria-label="Cancel"
>
<FaTimes />
</button>
)}
{item.canRetry && (
<button
onClick={() => retryItem?.(item.id)}
className="text-gray-400 hover:text-blue-500 transition-colors"
aria-label="Retry"
>
<FaSync />
</button>
)}
</div> </div>
</div> </div>
{(item.status === "error" || item.status === "retrying") && item.error && ( {(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) && ( {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">
{item.summary.total_failed > 0 && ( <div className="flex gap-4 text-xs">
<p className="text-red-600">{item.summary.total_failed} track(s) failed.</p> {item.summary.total_failed > 0 && (
)} <span className="flex items-center gap-1">
{item.summary.total_skipped > 0 && ( <div className="w-2 h-2 bg-error rounded-full"></div>
<p className="text-yellow-600">{item.summary.total_skipped} track(s) skipped.</p> <span className="text-error font-medium">{item.summary.total_failed} failed</span>
)} </span>
)}
{item.summary.total_skipped > 0 && (
<span className="flex items-center gap-1">
<div className="w-2 h-2 bg-warning rounded-full"></div>
<span className="text-warning font-medium">{item.summary.total_skipped} skipped</span>
</span>
)}
</div>
</div> </div>
)} )}
{(item.status === "downloading" || item.status === "processing") && {(item.status === "downloading" || item.status === "processing") &&
item.type === "track" && item.type === "track" &&
item.progress !== undefined && ( item.progress !== undefined && (
<div className="mt-2 h-1.5 w-full bg-gray-200 rounded-full"> <div className="mt-3">
<div <div className="flex justify-between items-center mb-1">
className={`h-1.5 rounded-full ${statusInfo.color.replace("text", "bg")}`} <span className="text-xs text-content-muted dark:text-content-muted-dark">Progress</span>
style={{ width: `${item.progress}%` }} <span className="text-xs font-semibold text-content-primary dark:text-content-primary-dark">{item.progress.toFixed(0)}%</span>
/> </div>
<div className="h-2 w-full bg-surface/50 dark:bg-surface-dark/50 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-300 ease-out ${
item.status === "downloading" ? "bg-info" : "bg-processing"
}`}
style={{ width: `${item.progress}%` }}
/>
</div>
</div> </div>
)} )}
</div> </div>
@@ -249,13 +284,15 @@ export const Queue = () => {
const hasFinished = items.some((item) => isTerminalStatus(item.status)); const hasFinished = items.some((item) => isTerminalStatus(item.status));
return ( 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"> <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-gray-200"> <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">Download Queue ({items.length})</h2> <h2 className="text-lg font-bold text-content-primary dark:text-content-primary-dark">
Download Queue ({items.length})
</h2>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={cancelAll} 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} disabled={!hasActive}
aria-label="Cancel all active downloads" aria-label="Cancel all active downloads"
> >
@@ -263,20 +300,30 @@ export const Queue = () => {
</button> </button>
<button <button
onClick={clearCompleted} 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} disabled={!hasFinished}
aria-label="Clear all finished downloads" aria-label="Clear all finished downloads"
> >
Clear Finished Clear Finished
</button> </button>
<button onClick={toggleVisibility} className="text-gray-500 hover:text-gray-800" aria-label="Close queue"> <button
<FaTimes /> 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> </button>
</div> </div>
</header> </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 ? ( {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} />) 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 ( 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"> <div className="relative">
<img src={imageUrl || "/placeholder.jpg"} alt={name} className="w-full aspect-square object-cover" /> <img src={imageUrl || "/placeholder.jpg"} alt={name} className="w-full aspect-square object-cover" />
{onDownload && ( {onDownload && (
<button <button
onClick={onDownload} 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}`} 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> </button>
)} )}
</div> </div>
<div className="p-4 flex-grow flex flex-col"> <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} {name}
</Link> </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>
</div> </div>
); );

View File

@@ -87,61 +87,61 @@ export function AccountsTab() {
}; };
const renderAddForm = () => ( const renderAddForm = () => (
<form onSubmit={handleSubmit(onSubmit)} className="p-4 border rounded-lg mt-4 space-y-4"> <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">Add New {activeService === "spotify" ? "Spotify" : "Deezer"} Account</h4> <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"> <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 <input
id="accountName" id="accountName"
{...register("accountName", { required: "This field is required" })} {...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> </div>
{activeService === "spotify" && ( {activeService === "spotify" && (
<div className="flex flex-col gap-2"> <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 <textarea
id="authBlob" id="authBlob"
{...register("authBlob", { required: activeService === "spotify" ? "Auth Blob is required" : false })} {...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} rows={4}
></textarea> ></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> </div>
)} )}
{activeService === "deezer" && ( {activeService === "deezer" && (
<div className="flex flex-col gap-2"> <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 <input
id="arl" id="arl"
{...register("arl", { required: activeService === "deezer" ? "ARL is required" : false })} {...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>
)} )}
<div className="flex flex-col gap-2"> <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 <input
id="accountRegion" id="accountRegion"
{...register("accountRegion")} {...register("accountRegion")}
placeholder="e.g. US, GB" 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>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
type="submit" type="submit"
disabled={addMutation.isPending} 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"} {addMutation.isPending ? "Saving..." : "Save Account"}
</button> </button>
<button <button
type="button" type="button"
onClick={() => setIsAdding(false)} 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 Cancel
</button> </button>
@@ -151,35 +151,35 @@ export function AccountsTab() {
return ( return (
<div className="space-y-6"> <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 <button
onClick={() => setActiveService("spotify")} 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 Spotify
</button> </button>
<button <button
onClick={() => setActiveService("deezer")} 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 Deezer
</button> </button>
</div> </div>
{isLoading ? ( {isLoading ? (
<p>Loading accounts...</p> <p className="text-content-muted dark:text-content-muted-dark">Loading accounts...</p>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{credentials?.map((cred) => ( {credentials?.map((cred) => (
<div <div
key={cred.name} 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> <span>{cred.name}</span>
<button <button
onClick={() => deleteMutation.mutate({ service: activeService, name: cred.name })} onClick={() => deleteMutation.mutate({ service: activeService, name: cred.name })}
disabled={deleteMutation.isPending && deleteMutation.variables?.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 Delete
</button> </button>
@@ -191,7 +191,7 @@ export function AccountsTab() {
{!isAdding && ( {!isAdding && (
<button <button
onClick={() => setIsAdding(true)} 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 Add Account
</button> </button>

View File

@@ -89,36 +89,36 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
{/* Download Settings */} {/* Download Settings */}
<div className="space-y-4"> <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"> <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 <input
id="maxConcurrentDownloads" id="maxConcurrentDownloads"
type="number" type="number"
min="1" min="1"
{...register("maxConcurrentDownloads")} {...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>
<div className="flex items-center justify-between"> <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" /> <input id="realTimeToggle" type="checkbox" {...register("realTime")} className="h-6 w-6 rounded" />
</div> </div>
<div className="flex items-center justify-between"> <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" /> <input id="fallbackToggle" type="checkbox" {...register("fallback")} className="h-6 w-6 rounded" />
</div> </div>
</div> </div>
{/* Source Quality Settings */} {/* Source Quality Settings */}
<div className="space-y-4"> <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"> <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 <select
id="spotifyQuality" id="spotifyQuality"
{...register("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="NORMAL">OGG 96kbps</option>
<option value="HIGH">OGG 160kbps</option> <option value="HIGH">OGG 160kbps</option>
@@ -126,31 +126,31 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
</select> </select>
</div> </div>
<div className="flex flex-col gap-2"> <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 <select
id="deezerQuality" id="deezerQuality"
{...register("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_128">MP3 128kbps</option>
<option value="MP3_320">MP3 320kbps</option> <option value="MP3_320">MP3 320kbps</option>
<option value="FLAC">FLAC (HiFi)</option> <option value="FLAC">FLAC (HiFi)</option>
</select> </select>
</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">
This sets the quality of the original download. Conversion settings below are applied after download. This sets the quality of the original download. Conversion settings below are applied after download.
</p> </p>
</div> </div>
{/* Conversion Settings */} {/* Conversion Settings */}
<div className="space-y-4"> <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"> <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 <select
id="convertToSelect" id="convertToSelect"
{...register("convertTo")} {...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> <option value="">No Conversion</option>
{Object.keys(CONVERSION_FORMATS).map((format) => ( {Object.keys(CONVERSION_FORMATS).map((format) => (
@@ -161,11 +161,11 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
</select> </select>
</div> </div>
<div className="flex flex-col gap-2"> <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 <select
id="bitrateSelect" id="bitrateSelect"
{...register("bitrate")} {...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} disabled={!selectedFormat || CONVERSION_FORMATS[selectedFormat]?.length === 0}
> >
<option value="">Auto</option> <option value="">Auto</option>
@@ -180,35 +180,35 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
{/* Retry Options */} {/* Retry Options */}
<div className="space-y-4"> <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"> <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 <input
id="maxRetries" id="maxRetries"
type="number" type="number"
min="0" min="0"
{...register("maxRetries")} {...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>
<div className="flex flex-col gap-2"> <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 <input
id="retryDelaySeconds" id="retryDelaySeconds"
type="number" type="number"
min="1" min="1"
{...register("retryDelaySeconds")} {...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>
<div className="flex flex-col gap-2"> <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 <input
id="retryDelayIncrease" id="retryDelayIncrease"
type="number" type="number"
min="0" min="0"
{...register("retryDelayIncrease")} {...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>
</div> </div>
@@ -216,7 +216,7 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
<button <button
type="submit" type="submit"
disabled={mutation.isPending} 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"} {mutation.isPending ? "Saving..." : "Save Download Settings"}
</button> </button>

View File

@@ -50,7 +50,7 @@ const placeholders = {
const PlaceholderSelector = ({ onSelect }: { onSelect: (value: string) => void }) => ( const PlaceholderSelector = ({ onSelect }: { onSelect: (value: string) => void }) => (
<select <select
onChange={(e) => onSelect(e.target.value)} 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> <option value="">-- Insert Placeholder --</option>
{Object.entries(placeholders).map(([group, options]) => ( {Object.entries(placeholders).map(([group, options]) => (
@@ -104,15 +104,15 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
}; };
if (isLoading) { if (isLoading) {
return <div>Loading formatting settings...</div>; return <div className="text-content-muted dark:text-content-muted-dark">Loading formatting settings...</div>;
} }
return ( return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
<div className="space-y-4"> <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"> <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 <input
id="customDirFormat" id="customDirFormat"
type="text" type="text"
@@ -121,12 +121,12 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
dirFormatRef(e); dirFormatRef(e);
dirInputRef.current = 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)} /> <PlaceholderSelector onSelect={handlePlaceholderSelect("customDirFormat", dirInputRef)} />
</div> </div>
<div className="flex flex-col gap-2"> <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 <input
id="customTrackFormat" id="customTrackFormat"
type="text" type="text"
@@ -135,12 +135,12 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
trackFormatRef(e); trackFormatRef(e);
trackInputRef.current = 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)} /> <PlaceholderSelector onSelect={handlePlaceholderSelect("customTrackFormat", trackInputRef)} />
</div> </div>
<div className="flex items-center justify-between"> <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 <input
id="tracknumPaddingToggle" id="tracknumPaddingToggle"
type="checkbox" type="checkbox"
@@ -149,7 +149,7 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
/> />
</div> </div>
<div className="flex items-center justify-between"> <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" /> <input id="saveCoverToggle" type="checkbox" {...register("saveCover")} className="h-6 w-6 rounded" />
</div> </div>
</div> </div>
@@ -157,7 +157,7 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
<button <button
type="submit" type="submit"
disabled={mutation.isPending} 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"} {mutation.isPending ? "Saving..." : "Save Formatting Settings"}
</button> </button>

View File

@@ -70,18 +70,18 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
}; };
const isLoading = isConfigLoading || spotifyLoading || deezerLoading || settingsLoading; 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 ( return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
<div className="space-y-4"> <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"> <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 <select
id="service" id="service"
{...register("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="spotify">Spotify</option>
<option value="deezer">Deezer</option> <option value="deezer">Deezer</option>
@@ -90,13 +90,13 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
</div> </div>
<div className="space-y-4"> <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"> <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 <select
id="spotifyAccount" id="spotifyAccount"
{...register("spotify")} {...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) => ( {spotifyAccounts?.map((acc) => (
<option key={acc.name} value={acc.name}> <option key={acc.name} value={acc.name}>
@@ -108,13 +108,13 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
</div> </div>
<div className="space-y-4"> <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"> <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 <select
id="deezerAccount" id="deezerAccount"
{...register("deezer")} {...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) => ( {deezerAccounts?.map((acc) => (
<option key={acc.name} value={acc.name}> <option key={acc.name} value={acc.name}>
@@ -126,17 +126,17 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
</div> </div>
<div className="space-y-4"> <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"> <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"> <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"} {globalSettings?.explicitFilter ? "Enabled" : "Disabled"}
</span> </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>
</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. The explicit content filter is controlled by an environment variable and cannot be changed here.
</p> </p>
</div> </div>
@@ -144,7 +144,7 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
<button <button
type="submit" type="submit"
disabled={mutation.isPending} 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"} {mutation.isPending ? "Saving..." : "Save General Settings"}
</button> </button>

View File

@@ -62,34 +62,34 @@ function SpotifyApiForm() {
const onSubmit = (formData: SpotifyApiSettings) => mutation.mutate(formData); 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 ( return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="flex flex-col gap-2"> <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 <input
id="client_id" id="client_id"
type="password" type="password"
{...register("client_id")} {...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" placeholder="Optional"
/> />
</div> </div>
<div className="flex flex-col gap-2"> <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 <input
id="client_secret" id="client_secret"
type="password" type="password"
{...register("client_secret")} {...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" placeholder="Optional"
/> />
</div> </div>
<button <button
type="submit" type="submit"
disabled={mutation.isPending} 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"} {mutation.isPending ? "Saving..." : "Save Spotify API"}
</button> </button>
@@ -126,22 +126,22 @@ function WebhookForm() {
const onSubmit = (formData: WebhookSettings) => mutation.mutate(formData); 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 ( return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<div className="flex flex-col gap-2"> <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 <input
id="webhookUrl" id="webhookUrl"
type="url" type="url"
{...register("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" placeholder="https://example.com/webhook"
/> />
</div> </div>
<div className="flex flex-col gap-2"> <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"> <div className="grid grid-cols-2 gap-4 pt-2">
{data?.available_events.map((event) => ( {data?.available_events.map((event) => (
<Controller <Controller
@@ -149,7 +149,7 @@ function WebhookForm() {
name="events" name="events"
control={control} control={control}
render={({ field }) => ( 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 <input
type="checkbox" type="checkbox"
className="h-5 w-5 rounded" className="h-5 w-5 rounded"
@@ -171,7 +171,7 @@ function WebhookForm() {
<button <button
type="submit" type="submit"
disabled={mutation.isPending} 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"} {mutation.isPending ? "Saving..." : "Save Webhook"}
</button> </button>
@@ -179,7 +179,7 @@ function WebhookForm() {
type="button" type="button"
onClick={() => testMutation.mutate(currentUrl)} onClick={() => testMutation.mutate(currentUrl)}
disabled={!currentUrl || testMutation.isPending} 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 Test
</button> </button>
@@ -192,14 +192,14 @@ export function ServerTab() {
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<div> <div>
<h3 className="text-xl font-semibold">Spotify API</h3> <h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Spotify API</h3>
<p className="text-sm text-gray-500 mt-1">Provide your own API credentials to avoid rate-limiting issues.</p> <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 /> <SpotifyApiForm />
</div> </div>
<hr className="border-gray-600" /> <hr className="border-border dark:border-border-dark" />
<div> <div>
<h3 className="text-xl font-semibold">Webhooks</h3> <h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Webhooks</h3>
<p className="text-sm text-gray-500 mt-1"> <p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">
Get notifications for events like download completion. (Currently disabled) Get notifications for events like download completion. (Currently disabled)
</p> </p>
<WebhookForm /> <WebhookForm />

View File

@@ -62,33 +62,33 @@ export function WatchTab() {
}; };
if (isLoading) { if (isLoading) {
return <div>Loading watch settings...</div>; return <div className="text-content-muted dark:text-content-muted-dark">Loading watch settings...</div>;
} }
return ( return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
<div className="space-y-4"> <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"> <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" /> <input id="watchEnabledToggle" type="checkbox" {...register("enabled")} className="h-6 w-6 rounded" />
</div> </div>
<div className="flex flex-col gap-2"> <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 <input
id="watchPollIntervalSeconds" id="watchPollIntervalSeconds"
type="number" type="number"
min="60" min="60"
{...register("watchPollIntervalSeconds")} {...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> </div>
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-xl font-semibold">Artist Album Groups</h3> <h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Artist Album Groups</h3>
<p className="text-sm text-gray-500">Select which album groups to monitor for watched artists.</p> <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"> <div className="grid grid-cols-2 gap-4 pt-2">
{ALBUM_GROUPS.map((group) => ( {ALBUM_GROUPS.map((group) => (
<Controller <Controller
@@ -96,7 +96,7 @@ export function WatchTab() {
name="watchedArtistAlbumGroup" name="watchedArtistAlbumGroup"
control={control} control={control}
render={({ field }) => ( 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 <input
type="checkbox" type="checkbox"
className="h-5 w-5 rounded" className="h-5 w-5 rounded"
@@ -118,7 +118,7 @@ export function WatchTab() {
<button <button
type="submit" type="submit"
disabled={mutation.isPending} 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"} {mutation.isPending ? "Saving..." : "Save Watch Settings"}
</button> </button>

View File

@@ -1,7 +1,319 @@
@import "tailwindcss"; @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 { @layer base {
a { a {
@apply no-underline hover:underline cursor-pointer; @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"> <div className="mb-6">
<button <button
onClick={() => window.history.back()} 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> <span>Back to results</span>
</button> </button>
</div> </div>
@@ -87,8 +87,8 @@ export const Album = () => {
className="w-48 h-48 object-cover rounded-lg shadow-lg" className="w-48 h-48 object-cover rounded-lg shadow-lg"
/> />
<div className="flex-grow space-y-2"> <div className="flex-grow space-y-2">
<h1 className="text-3xl font-bold">{album.name}</h1> <h1 className="text-3xl font-bold text-content-primary dark:text-content-primary-dark">{album.name}</h1>
<p className="text-lg text-gray-500 dark:text-gray-400"> <p className="text-lg text-content-secondary dark:text-content-secondary-dark">
By{" "} By{" "}
{album.artists.map((artist, index) => ( {album.artists.map((artist, index) => (
<span key={artist.id}> <span key={artist.id}>
@@ -99,16 +99,16 @@ export const Album = () => {
</span> </span>
))} ))}
</p> </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 {new Date(album.release_date).getFullYear()} {album.total_tracks} songs
</p> </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>
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<button <button
onClick={handleDownloadAlbum} onClick={handleDownloadAlbum}
disabled={isExplicitFilterEnabled && hasExplicitTrack} 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={ title={
isExplicitFilterEnabled && hasExplicitTrack ? "Album contains explicit tracks" : "Download Full Album" isExplicitFilterEnabled && hasExplicitTrack ? "Album contains explicit tracks" : "Download Full Album"
} }
@@ -119,33 +119,33 @@ export const Album = () => {
</div> </div>
<div className="space-y-4"> <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"> <div className="space-y-2">
{album.tracks.items.map((track, index) => { {album.tracks.items.map((track, index) => {
if (isExplicitFilterEnabled && track.explicit) { if (isExplicitFilterEnabled && track.explicit) {
return ( return (
<div <div
key={index} 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"> <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>
<p className="font-medium text-gray-500">Explicit track filtered</p> <p className="font-medium text-content-muted dark:text-content-muted-dark">Explicit track filtered</p>
</div> </div>
<span className="text-gray-500">--:--</span> <span className="text-content-muted dark:text-content-muted-dark">--:--</span>
</div> </div>
); );
} }
return ( return (
<div <div
key={track.id} 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"> <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> <div>
<p className="font-medium">{track.name}</p> <p className="font-medium text-content-primary dark:text-content-primary-dark">{track.name}</p>
<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) => ( {track.artists.map((artist, index) => (
<span key={artist.id}> <span key={artist.id}>
<Link <Link
@@ -164,16 +164,16 @@ export const Album = () => {
</div> </div>
</div> </div>
<div className="flex items-center gap-4"> <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)}: {Math.floor(track.duration_ms / 60000)}:
{((track.duration_ms % 60000) / 1000).toFixed(0).padStart(2, "0")} {((track.duration_ms % 60000) / 1000).toFixed(0).padStart(2, "0")}
</span> </span>
<button <button
onClick={() => handleDownloadTrack(track)} 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" 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> </button>
</div> </div>
</div> </div>

View File

@@ -133,9 +133,9 @@ export const Artist = () => {
<div className="mb-6"> <div className="mb-6">
<button <button
onClick={() => window.history.back()} 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> <span>Back to results</span>
</button> </button>
</div> </div>
@@ -147,31 +147,31 @@ export const Artist = () => {
className="artist-image w-48 h-48 rounded-full mx-auto mb-4 shadow-lg" 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"> <div className="flex gap-4 justify-center mt-4">
<button <button
onClick={handleDownloadArtist} 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> <span>Download All</span>
</button> </button>
<button <button
onClick={handleToggleWatch} onClick={handleToggleWatch}
className={`flex items-center gap-2 px-4 py-2 rounded-md transition-colors border ${ className={`flex items-center gap-2 px-4 py-2 rounded-md transition-colors border ${
isWatched isWatched
? "bg-blue-500 text-white border-blue-500" ? "bg-button-primary text-button-primary-text border-primary"
: "bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800" : "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 ? ( {isWatched ? (
<> <>
<FaBookmark /> <FaBookmark className="icon-inverse" />
<span>Watching</span> <span>Watching</span>
</> </>
) : ( ) : (
<> <>
<FaRegBookmark /> <FaRegBookmark className="icon-primary" />
<span>Watch</span> <span>Watch</span>
</> </>
)} )}
@@ -181,17 +181,20 @@ export const Artist = () => {
{topTracks.length > 0 && ( {topTracks.length > 0 && (
<div className="mb-12"> <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"> <div className="track-list space-y-2">
{topTracks.map((track) => ( {topTracks.map((track) => (
<div <div
key={track.id} 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} {track.name}
</Link> </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 Download
</button> </button>
</div> </div>
@@ -202,7 +205,7 @@ export const Artist = () => {
{artistAlbums.length > 0 && ( {artistAlbums.length > 0 && (
<div className="mb-12"> <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"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
{artistAlbums.map((album) => ( {artistAlbums.map((album) => (
<AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} /> <AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} />
@@ -213,7 +216,7 @@ export const Artist = () => {
{artistSingles.length > 0 && ( {artistSingles.length > 0 && (
<div className="mb-12"> <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"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
{artistSingles.map((album) => ( {artistSingles.map((album) => (
<AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} /> <AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} />
@@ -224,7 +227,7 @@ export const Artist = () => {
{artistCompilations.length > 0 && ( {artistCompilations.length > 0 && (
<div className="mb-12"> <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"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
{artistCompilations.map((album) => ( {artistCompilations.map((album) => (
<AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} /> <AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} />

View File

@@ -14,8 +14,8 @@ const ConfigComponent = () => {
const { settings: config, isLoading } = useSettings(); const { settings: config, isLoading } = useSettings();
const renderTabContent = () => { const renderTabContent = () => {
if (isLoading) return <p className="text-center">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-red-500">Error 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) { switch (activeTab) {
case "general": case "general":
@@ -38,8 +38,8 @@ const ConfigComponent = () => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h1 className="text-3xl font-bold">Configuration</h1> <h1 className="text-3xl font-bold text-content-primary dark:text-content-primary-dark">Configuration</h1>
<p className="text-gray-500">Manage application settings and services.</p> <p className="text-content-muted dark:text-content-muted-dark">Manage application settings and services.</p>
</div> </div>
<div className="flex gap-8"> <div className="flex gap-8">
@@ -47,37 +47,37 @@ const ConfigComponent = () => {
<nav className="flex flex-col space-y-1"> <nav className="flex flex-col space-y-1">
<button <button
onClick={() => setActiveTab("general")} 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 General
</button> </button>
<button <button
onClick={() => setActiveTab("downloads")} 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 Downloads
</button> </button>
<button <button
onClick={() => setActiveTab("formatting")} 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 Formatting
</button> </button>
<button <button
onClick={() => setActiveTab("accounts")} 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 Accounts
</button> </button>
<button <button
onClick={() => setActiveTab("watch")} 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 Watch
</button> </button>
<button <button
onClick={() => setActiveTab("server")} 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 Server
</button> </button>

View File

@@ -32,10 +32,10 @@ type HistoryEntry = {
}; };
const STATUS_CLASS: Record<string, string> = { const STATUS_CLASS: Record<string, string> = {
COMPLETED: "text-green-500", COMPLETED: "text-success",
ERROR: "text-red-500", ERROR: "text-error",
CANCELLED: "text-gray-500", CANCELLED: "text-content-muted dark:text-content-muted-dark",
SKIPPED: "text-yellow-500", SKIPPED: "text-warning",
}; };
const QUALITY_MAP: Record<string, Record<string, string>> = { const QUALITY_MAP: Record<string, Record<string, string>> = {
@@ -140,12 +140,12 @@ export const History = () => {
const statusKey = (status || "").toUpperCase(); const statusKey = (status || "").toUpperCase();
const statusClass = const statusClass =
{ {
COMPLETED: "text-green-500", COMPLETED: "text-success",
SUCCESSFUL: "text-green-500", SUCCESSFUL: "text-success",
ERROR: "text-red-500", ERROR: "text-error",
FAILED: "text-red-500", FAILED: "text-error",
CANCELLED: "text-gray-500", CANCELLED: "text-content-muted dark:text-content-muted-dark",
SKIPPED: "text-yellow-500", SKIPPED: "text-warning",
}[statusKey] || "text-gray-500"; }[statusKey] || "text-gray-500";
return <span className={`font-semibold ${statusClass}`}>{status}</span>; return <span className={`font-semibold ${statusClass}`}>{status}</span>;
@@ -304,43 +304,43 @@ export const History = () => {
<div className="space-y-4"> <div className="space-y-4">
{parentTaskId && parentTask ? ( {parentTaskId && parentTask ? (
<div className="space-y-4"> <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 &larr; Back to All History
</button> </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="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="md:col-span-2 space-y-1.5"> <div className="md:col-span-2 space-y-1.5">
<h2 className="text-3xl font-bold tracking-tight">{parentTask.item_name}</h2> <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-muted-foreground">{parentTask.item_artist}</p> <p className="text-xl text-content-secondary dark:text-content-secondary-dark">{parentTask.item_artist}</p>
<div className="pt-2"> <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} {parentTask.download_type}
</span> </span>
</div> </div>
</div> </div>
<div className="space-y-2 text-sm md:text-right"> <div className="space-y-2 text-sm md:text-right">
<div <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] STATUS_CLASS[parentTask.status_final]
}`} }`}
> >
{parentTask.status_final} {parentTask.status_final}
</div> </div>
<p className="text-muted-foreground pt-2"> <p className="text-content-muted dark:text-content-muted-dark pt-2">
<span className="font-semibold text-foreground">Quality: </span> <span className="font-semibold text-content-primary dark:text-content-primary-dark">Quality: </span>
{formatQuality(parentTask)} {formatQuality(parentTask)}
</p> </p>
<p className="text-muted-foreground"> <p className="text-content-muted dark:text-content-muted-dark">
<span className="font-semibold text-foreground">Completed: </span> <span className="font-semibold text-content-primary dark:text-content-primary-dark">Completed: </span>
{new Date(parentTask.timestamp_completed * 1000).toLocaleString()} {new Date(parentTask.timestamp_completed * 1000).toLocaleString()}
</p> </p>
</div> </div>
</div> </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> </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 */} {/* Filter Controls */}
@@ -349,7 +349,7 @@ export const History = () => {
<select <select
value={statusFilter} value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)} 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="">All Statuses</option>
<option value="COMPLETED">Completed</option> <option value="COMPLETED">Completed</option>
@@ -360,7 +360,7 @@ export const History = () => {
<select <select
value={typeFilter} value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)} 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="">All Types</option>
<option value="track">Track</option> <option value="track">Track</option>
@@ -371,14 +371,14 @@ export const History = () => {
<select <select
value={trackStatusFilter} value={trackStatusFilter}
onChange={(e) => setTrackStatusFilter(e.target.value)} 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="">All Track Statuses</option>
<option value="SUCCESSFUL">Successful</option> <option value="SUCCESSFUL">Successful</option>
<option value="SKIPPED">Skipped</option> <option value="SKIPPED">Skipped</option>
<option value="FAILED">Failed</option> <option value="FAILED">Failed</option>
</select> </select>
<label className="flex items-center gap-2"> <label className="flex items-center gap-2 text-content-primary dark:text-content-primary-dark">
<input <input
type="checkbox" type="checkbox"
checked={showChildTracks} checked={showChildTracks}
@@ -397,7 +397,7 @@ export const History = () => {
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}> <tr key={headerGroup.id}>
{headerGroup.headers.map((header) => ( {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 : ( {header.isPlaceholder ? null : (
<div <div
{...{ {...{
@@ -417,13 +417,13 @@ export const History = () => {
<tbody> <tbody>
{isLoading ? ( {isLoading ? (
<tr> <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... Loading...
</td> </td>
</tr> </tr>
) : table.getRowModel().rows.length === 0 ? ( ) : table.getRowModel().rows.length === 0 ? (
<tr> <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. No history entries found.
</td> </td>
</tr> </tr>
@@ -433,17 +433,17 @@ export const History = () => {
!row.original.parent_task_id && !row.original.parent_task_id &&
(row.original.download_type === "album" || row.original.download_type === "playlist"); (row.original.download_type === "album" || row.original.download_type === "playlist");
const isChild = !!row.original.parent_task_id; 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) { 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) { } 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 ( 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) => ( {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())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</td> </td>
))} ))}
@@ -460,11 +460,11 @@ export const History = () => {
<button <button
onClick={() => table.previousPage()} onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()} 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 Previous
</button> </button>
<span> <span className="text-content-primary dark:text-content-primary-dark">
Page{" "} Page{" "}
<strong> <strong>
{table.getState().pagination.pageIndex + 1} of {table.getPageCount()} {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
@@ -473,14 +473,14 @@ export const History = () => {
<button <button
onClick={() => table.nextPage()} onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()} 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 Next
</button> </button>
<select <select
value={table.getState().pagination.pageSize} value={table.getState().pagination.pageSize}
onChange={(e) => table.setPageSize(Number(e.target.value))} 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) => ( {[10, 25, 50, 100].map((size) => (
<option key={size} value={size}> <option key={size} value={size}>

View File

@@ -130,19 +130,19 @@ export const Home = () => {
return ( return (
<div className="max-w-4xl mx-auto p-4"> <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"> <div className="flex flex-col sm:flex-row gap-3 mb-6">
<input <input
type="text" type="text"
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder="Search for a track, album, or artist" 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 <select
value={searchType} value={searchType}
onChange={(e) => setSearchType(e.target.value as "track" | "album" | "artist" | "playlist")} 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="track">Track</option>
<option value="album">Album</option> <option value="album">Album</option>
@@ -150,13 +150,13 @@ export const Home = () => {
<option value="playlist">Playlist</option> <option value="playlist">Playlist</option>
</select> </select>
</div> </div>
{isLoading ? ( {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} {resultComponent}
<div ref={loaderRef} /> <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> </div>

View File

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

View File

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

View File

@@ -47,7 +47,7 @@ export const Track = () => {
if (error) { if (error) {
return ( return (
<div className="flex justify-center items-center h-full"> <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> </div>
); );
} }
@@ -55,7 +55,7 @@ export const Track = () => {
if (!track) { if (!track) {
return ( return (
<div className="flex justify-center items-center h-full"> <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> </div>
); );
} }
@@ -67,13 +67,13 @@ export const Track = () => {
<div className="mb-6"> <div className="mb-6">
<button <button
onClick={() => window.history.back()} 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> <span>Back to results</span>
</button> </button>
</div> </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 && ( {imageUrl && (
<div className="md:w-1/3"> <div className="md:w-1/3">
<img src={imageUrl} alt={track.album.name} className="w-full h-auto object-cover" /> <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 className="p-6 md:w-2/3 flex flex-col justify-between">
<div> <div>
<div className="flex items-baseline justify-between"> <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 && ( {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>
<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) => ( {track.artists.map((artist, index) => (
<span key={artist.id}> <span key={artist.id}>
<Link to="/artist/$artistId" params={{ artistId: artist.id }}> <Link to="/artist/$artistId" params={{ artistId: artist.id }}>
@@ -97,27 +97,27 @@ export const Track = () => {
</span> </span>
))} ))}
</div> </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{" "} From the album{" "}
<Link to="/album/$albumId" params={{ albumId: track.album.id }} className="font-semibold"> <Link to="/album/$albumId" params={{ albumId: track.album.id }} className="font-semibold">
{track.album.name} {track.album.name}
</Link> </Link>
</p> </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>Release Date: {track.album.release_date}</p>
<p>Duration: {formatDuration(track.duration_ms)}</p> <p>Duration: {formatDuration(track.duration_ms)}</p>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<p className="text-sm text-gray-600">Popularity:</p> <p className="text-sm text-content-secondary dark:text-content-secondary-dark">Popularity:</p>
<div className="w-full bg-gray-200 rounded-full h-2.5"> <div className="w-full bg-surface-muted dark:bg-surface-muted-dark rounded-full h-2.5">
<div className="bg-green-500 h-2.5 rounded-full" style={{ width: `${track.popularity}%` }}></div> <div className="bg-success h-2.5 rounded-full" style={{ width: `${track.popularity}%` }}></div>
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center gap-4 mt-6"> <div className="flex items-center gap-4 mt-6">
<button <button
onClick={handleDownloadTrack} 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 Download
</button> </button>
@@ -125,10 +125,10 @@ export const Track = () => {
href={track.external_urls.spotify} href={track.external_urls.spotify}
target="_blank" target="_blank"
rel="noopener noreferrer" 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" 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> <span className="font-semibold">Listen on Spotify</span>
</a> </a>
</div> </div>

View File

@@ -95,15 +95,15 @@ export const Watchlist = () => {
}; };
if (isLoading || settingsLoading) { 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) { if (!settings?.watch?.enabled) {
return ( return (
<div className="text-center p-8"> <div className="text-center p-8">
<h2 className="text-2xl font-bold mb-2">Watchlist Disabled</h2> <h2 className="text-2xl font-bold mb-2 text-content-primary dark:text-content-primary-dark">Watchlist Disabled</h2>
<p>The watchlist feature is currently disabled. You can enable it in the settings.</p> <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-blue-500 hover:underline mt-4 inline-block"> <Link to="/config" className="text-primary hover:underline mt-4 inline-block">
Go to Settings Go to Settings
</Link> </Link>
</div> </div>
@@ -113,8 +113,8 @@ export const Watchlist = () => {
if (items.length === 0) { if (items.length === 0) {
return ( return (
<div className="text-center p-8"> <div className="text-center p-8">
<h2 className="text-2xl font-bold mb-2">Watchlist is Empty</h2> <h2 className="text-2xl font-bold mb-2 text-content-primary dark:text-content-primary-dark">Watchlist is Empty</h2>
<p>Start watching artists or playlists to see them here.</p> <p className="text-content-secondary dark:text-content-secondary-dark">Start watching artists or playlists to see them here.</p>
</div> </div>
); );
} }
@@ -122,38 +122,38 @@ export const Watchlist = () => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex justify-between items-center"> <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 <button
onClick={handleCheckAll} 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> </button>
</div> </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"> <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) => ( {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"> <a href={`/${item.itemType}/${item.id}`} className="flex-grow">
<img <img
src={item.images?.[0]?.url || "/images/placeholder.jpg"} src={item.images?.[0]?.url || "/images/placeholder.jpg"}
alt={item.name} alt={item.name}
className="w-full h-auto object-cover rounded-md aspect-square" className="w-full h-auto object-cover rounded-md aspect-square"
/> />
<h3 className="font-bold pt-2 truncate">{item.name}</h3> <h3 className="font-bold pt-2 truncate text-content-primary dark:text-content-primary-dark">{item.name}</h3>
<p className="text-sm text-muted-foreground capitalize">{item.itemType}</p> <p className="text-sm text-content-muted dark:text-content-muted-dark capitalize">{item.itemType}</p>
</a> </a>
<div className="flex gap-2 pt-2"> <div className="flex gap-2 pt-2">
<button <button
onClick={() => handleUnwatch(item)} 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>
<button <button
onClick={() => handleCheck(item)} 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> </button>
</div> </div>
</div> </div>