mirror of
https://lavaforge.org/spotizerr/spotizerr.git
synced 2025-12-24 02:39:14 -05:00
feat(ui): Add spinner for "Downloading..." element in UI and add save icon in config page
This commit is contained in:
@@ -54,7 +54,7 @@ export const AlbumCard = ({ album, onDownload }: AlbumCardProps) => {
|
||||
? "Queued."
|
||||
: status === "error"
|
||||
? <img src="/download.svg" alt="Download" className="w-5 h-5 icon-inverse" />
|
||||
: "Downloading..."
|
||||
: <img src="/spinner.svg" alt="Loading" className="w-5 h-5 animate-spin" />
|
||||
: <img src="/download.svg" alt="Download" className="w-5 h-5 icon-inverse" />
|
||||
}
|
||||
</button>
|
||||
|
||||
@@ -65,7 +65,7 @@ export const SearchResultCard = ({ id, name, subtitle, imageUrl, type, onDownloa
|
||||
? "Queued."
|
||||
: status === "error"
|
||||
? <img src="/download.svg" alt="Download" className="w-5 h-5 logo" />
|
||||
: "Downloading..."
|
||||
: <img src="/spinner.svg" alt="Loading" className="w-5 h-5 animate-spin" />
|
||||
: <img src="/download.svg" alt="Download" className="w-5 h-5 logo" />
|
||||
}
|
||||
</button>
|
||||
|
||||
@@ -174,8 +174,13 @@ export function AccountsTab() {
|
||||
type="submit"
|
||||
disabled={addMutation.isPending}
|
||||
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
||||
title="Save Account"
|
||||
>
|
||||
{addMutation.isPending ? "Saving..." : "Save Account"}
|
||||
{addMutation.isPending ? (
|
||||
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin logo" />
|
||||
) : (
|
||||
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -100,8 +100,8 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
|
||||
queryClient.invalidateQueries({ queryKey: ["config"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to save settings", error.message);
|
||||
toast.error(`Failed to save settings: ${error.message}`);
|
||||
console.error("Failed to save settings", (error as any).message);
|
||||
toast.error(`Failed to save settings: ${(error as any).message}`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -180,8 +180,13 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
|
||||
type="submit"
|
||||
disabled={mutation.isPending || !!validationError}
|
||||
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
||||
title="Save Download Settings"
|
||||
>
|
||||
{mutation.isPending ? "Saving..." : "Save Download Settings"}
|
||||
{mutation.isPending ? (
|
||||
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin logo" />
|
||||
) : (
|
||||
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -359,7 +364,7 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
|
||||
type="number"
|
||||
min="1"
|
||||
{...register("retryDelaySeconds")}
|
||||
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"
|
||||
className="block w_full p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -369,7 +374,7 @@ export function DownloadsTab({ config, isLoading }: DownloadsTabProps) {
|
||||
type="number"
|
||||
min="0"
|
||||
{...register("retryDelayIncrease")}
|
||||
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"
|
||||
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>
|
||||
|
||||
@@ -88,8 +88,8 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
|
||||
queryClient.invalidateQueries({ queryKey: ["config"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to save formatting settings:", error.message);
|
||||
toast.error(`Failed to save settings: ${error.message}`);
|
||||
console.error("Failed to save formatting settings:", (error as any).message);
|
||||
toast.error(`Failed to save settings: ${(error as any).message}`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -131,8 +131,13 @@ export function FormattingTab({ config, isLoading }: FormattingTabProps) {
|
||||
type="submit"
|
||||
disabled={mutation.isPending}
|
||||
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
||||
title="Save Formatting Settings"
|
||||
>
|
||||
{mutation.isPending ? "Saving..." : "Save Formatting Settings"}
|
||||
{mutation.isPending ? (
|
||||
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin logo" />
|
||||
) : (
|
||||
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,8 +83,13 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro
|
||||
type="submit"
|
||||
disabled={mutation.isPending}
|
||||
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
||||
title="Save General Settings"
|
||||
>
|
||||
{mutation.isPending ? "Saving..." : "Save General Settings"}
|
||||
{mutation.isPending ? (
|
||||
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin logo" />
|
||||
) : (
|
||||
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -126,7 +126,7 @@ export function ProfileTab() {
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-content-secondary dark:text-content-secondary-dark mb-1">
|
||||
<label className="block text_sm font-medium text-content-secondary dark:text-content-secondary-dark mb-1">
|
||||
Role
|
||||
</label>
|
||||
<p className="text-content-primary dark:text-content-primary-dark">
|
||||
@@ -177,7 +177,7 @@ export function ProfileTab() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-content-secondary dark:text-content-secondary-dark mb-2">
|
||||
<label className="block text-sm font_medium text-content-secondary dark:text-content-secondary-dark mb-2">
|
||||
New Password
|
||||
</label>
|
||||
<input
|
||||
@@ -226,8 +226,13 @@ export function ProfileTab() {
|
||||
type="submit"
|
||||
disabled={isChangingPassword}
|
||||
className="px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Save Password"
|
||||
>
|
||||
{isChangingPassword ? "Changing Password..." : "Change Password"}
|
||||
{isChangingPassword ? (
|
||||
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin inline-block logo" />
|
||||
) : (
|
||||
<img src="/save.svg" alt="Save" className="w-5 h-5 inline-block logo" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -252,7 +257,7 @@ export function ProfileTab() {
|
||||
{/* SSO User Notice */}
|
||||
{user?.is_sso_user && (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6">
|
||||
<h3 className="text-lg font-medium text-blue-900 dark:text-blue-100 mb-2">
|
||||
<h3 className="text-lg font-semibold text-blue-900 dark:text-blue-100 mb-2">
|
||||
SSO Account
|
||||
</h3>
|
||||
<p className="text-blue-800 dark:text-blue-200">
|
||||
|
||||
@@ -54,8 +54,8 @@ function SpotifyApiForm() {
|
||||
queryClient.invalidateQueries({ queryKey: ["spotifyApiConfig"] });
|
||||
},
|
||||
onError: (e) => {
|
||||
console.error("Failed to save Spotify API settings:", e.message);
|
||||
toast.error(`Failed to save: ${e.message}`);
|
||||
console.error("Failed to save Spotify API settings:", (e as any).message);
|
||||
toast.error(`Failed to save: ${(e as any).message}`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -75,8 +75,13 @@ function SpotifyApiForm() {
|
||||
type="submit"
|
||||
disabled={mutation.isPending}
|
||||
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
||||
title="Save Spotify API"
|
||||
>
|
||||
{mutation.isPending ? "Saving..." : "Save Spotify API"}
|
||||
{mutation.isPending ? (
|
||||
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin logo" />
|
||||
) : (
|
||||
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,7 +124,7 @@ function WebhookForm() {
|
||||
queryClient.invalidateQueries({ queryKey: ["webhookConfig"] });
|
||||
},
|
||||
onError: (e) => {
|
||||
toast.error(`Failed to save: ${e.message}`);
|
||||
toast.error(`Failed to save: ${(e as any).message}`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -128,7 +133,7 @@ function WebhookForm() {
|
||||
onSuccess: () => {
|
||||
// No toast needed
|
||||
},
|
||||
onError: (e) => toast.error(`Webhook test failed: ${e.message}`),
|
||||
onError: (e) => toast.error(`Webhook test failed: ${(e as any).message}`),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -147,8 +152,13 @@ function WebhookForm() {
|
||||
type="submit"
|
||||
disabled={mutation.isPending}
|
||||
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
||||
title="Save Webhook"
|
||||
>
|
||||
{mutation.isPending ? "Saving..." : "Save Webhook"}
|
||||
{mutation.isPending ? (
|
||||
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin logo" />
|
||||
) : (
|
||||
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -252,7 +252,7 @@ export function UserManagementTab() {
|
||||
errors.email
|
||||
? "border-error focus:border-error"
|
||||
: "border-input-border dark:border-input-border-dark focus:border-primary"
|
||||
} bg-input-background dark:bg-input-background-dark text-content-primary dark:text-content-primary-dark focus:outline-none focus:ring-2 focus:ring-primary/20`}
|
||||
} bg-input-background dark:bg-input-background-dark text-content-primary dark:text-content-primary-dark focus:outline_none focus:ring-2 focus:ring-primary/20`}
|
||||
placeholder="Enter email (optional)"
|
||||
disabled={isCreating}
|
||||
/>
|
||||
@@ -302,15 +302,13 @@ export function UserManagementTab() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isCreating}
|
||||
className="px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
className="px-4 py-2 bg-primary hover:bg-primary-hover text_white rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
title="Save User"
|
||||
>
|
||||
{isCreating ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
<img src="/spinner.svg" alt="Saving" className="w-4 h-4 animate-spin logo" />
|
||||
) : (
|
||||
"Create User"
|
||||
<img src="/save.svg" alt="Save" className="w-4 h-4 logo" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@@ -474,14 +472,12 @@ export function UserManagementTab() {
|
||||
type="submit"
|
||||
disabled={isResettingPassword}
|
||||
className="px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
title="Save Password"
|
||||
>
|
||||
{isResettingPassword ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
Resetting...
|
||||
</>
|
||||
<img src="/spinner.svg" alt="Saving" className="w-4 h-4 animate-spin logo" />
|
||||
) : (
|
||||
"Reset Password"
|
||||
<img src="/save.svg" alt="Save" className="w-4 h-4 logo" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -181,8 +181,13 @@ export function WatchTab() {
|
||||
type="submit"
|
||||
disabled={mutation.isPending || !!validationError}
|
||||
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
||||
title="Save Watch Settings"
|
||||
>
|
||||
{mutation.isPending ? "Saving..." : "Save Watch Settings"}
|
||||
{mutation.isPending ? (
|
||||
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin logo" />
|
||||
) : (
|
||||
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -205,7 +205,7 @@ export const Album = () => {
|
||||
? "Queued."
|
||||
: albumStatus === "error"
|
||||
? "Download Album"
|
||||
: "Downloading..."
|
||||
: <img src="/spinner.svg" alt="Loading" className="w-5 h-5 animate-spin inline-block" />
|
||||
: "Download Album"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -300,7 +300,7 @@ export const Artist = () => {
|
||||
? artistStatus === "queued"
|
||||
? "Queued."
|
||||
: artistStatus === "downloading"
|
||||
? "Downloading..."
|
||||
? <img src="/spinner.svg" alt="Loading" className="w-5 h-5 animate-spin" />
|
||||
: <>
|
||||
<FaDownload className="icon-inverse" />
|
||||
<span>Download All</span>
|
||||
@@ -361,7 +361,7 @@ export const Artist = () => {
|
||||
? "Queued."
|
||||
: trackStatuses[track.id] === "error"
|
||||
? "Download"
|
||||
: "Downloading..."
|
||||
: <img src="/spinner.svg" alt="Loading" className="w-4 h-4 animate-spin inline-block" />
|
||||
: "Download"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -239,7 +239,7 @@ export const Playlist = () => {
|
||||
? "Queued."
|
||||
: playlistStatus === "error"
|
||||
? "Download All"
|
||||
: "Downloading..."
|
||||
: <img src="/spinner.svg" alt="Loading" className="w-5 h-5 animate-spin inline-block" />
|
||||
: "Download All"}
|
||||
</button>
|
||||
{settings?.watch?.enabled && (
|
||||
@@ -264,7 +264,7 @@ export const Playlist = () => {
|
||||
|
||||
{/* Tracks Section */}
|
||||
<div className="space-y-3 md:space-y-4">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<div className="flex items-center justify_between px-1">
|
||||
<h2 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Tracks</h2>
|
||||
{tracks.length > 0 && (
|
||||
<span className="text-sm text-content-muted dark:text-content-muted-dark">
|
||||
@@ -335,7 +335,7 @@ export const Playlist = () => {
|
||||
? "Queued."
|
||||
: trackStatuses[track.id] === "error"
|
||||
? <img src="/download.svg" alt="Download" className="w-4 h-4 logo" />
|
||||
: "Downloading..."
|
||||
: <img src="/spinner.svg" alt="Loading" className="w-4 h-4 animate-spin" />
|
||||
: <img src="/download.svg" alt="Download" className="w-4 h-4 logo" />
|
||||
}
|
||||
</button>
|
||||
|
||||
@@ -174,7 +174,7 @@ export const Track = () => {
|
||||
style={{ width: `${track.popularity}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-content-secondary dark:text-content-secondary-dark">
|
||||
<span className="text-sm font-medium text-content_secondary dark:text-content-secondary-dark">
|
||||
{track.popularity}%
|
||||
</span>
|
||||
</div>
|
||||
@@ -193,14 +193,14 @@ export const Track = () => {
|
||||
? "Queued."
|
||||
: trackStatus === "error"
|
||||
? "Download"
|
||||
: "Downloading..."
|
||||
: <img src="/spinner.svg" alt="Loading" className="w-5 h-5 animate-spin inline-block" />
|
||||
: "Download"}
|
||||
</button>
|
||||
<a
|
||||
href={track.external_urls.spotify}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-full sm:w-auto flex items-center justify-center gap-3 text-content-secondary dark:text-content-secondary-dark hover:text-content-primary dark:hover:text-content-primary-dark transition duration-300 py-3 px-8 border border-border dark:border-border-dark rounded-full hover:border-border-accent dark:hover:border-border-accent-dark"
|
||||
className="w-full sm:w-auto flex items_center justify-center gap-3 text-content-secondary dark:text-content-secondary-dark hover:text-content-primary dark:hover:text-content-primary-dark transition duration-300 py-3 px-8 border border-border dark:border-border-dark rounded-full hover:border-border-accent dark:hover:border-border-accent-dark"
|
||||
aria-label="Listen on Spotify"
|
||||
>
|
||||
<FaSpotify size={20} className="icon-secondary hover:icon-primary" />
|
||||
|
||||
Reference in New Issue
Block a user