feat(ui): Add spinner for "Downloading..." element in UI and add save icon in config page
This commit is contained in:
4
spotizerr-ui/public/save.svg
Normal file
4
spotizerr-ui/public/save.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.1716 1C18.702 1 19.2107 1.21071 19.5858 1.58579L22.4142 4.41421C22.7893 4.78929 23 5.29799 23 5.82843V20C23 21.6569 21.6569 23 20 23H4C2.34315 23 1 21.6569 1 20V4C1 2.34315 2.34315 1 4 1H18.1716ZM4 3C3.44772 3 3 3.44772 3 4V20C3 20.5523 3.44772 21 4 21L5 21L5 15C5 13.3431 6.34315 12 8 12L16 12C17.6569 12 19 13.3431 19 15V21H20C20.5523 21 21 20.5523 21 20V6.82843C21 6.29799 20.7893 5.78929 20.4142 5.41421L18.5858 3.58579C18.2107 3.21071 17.702 3 17.1716 3H17V5C17 6.65685 15.6569 8 14 8H10C8.34315 8 7 6.65685 7 5V3H4ZM17 21V15C17 14.4477 16.5523 14 16 14L8 14C7.44772 14 7 14.4477 7 15L7 21L17 21ZM9 3H15V5C15 5.55228 14.5523 6 14 6H10C9.44772 6 9 5.55228 9 5V3Z" fill="#0F0F0F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 968 B |
2
spotizerr-ui/public/spinner.svg
Normal file
2
spotizerr-ui/public/spinner.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13 2a1 1 0 0 0-2 0v4.167a1 1 0 1 0 2 0V2ZM13 17.833a1 1 0 0 0-2 0V22a1 1 0 1 0 2 0v-4.167ZM16.834 12a1 1 0 0 1 1-1H22a1 1 0 0 1 0 2h-4.166a1 1 0 0 1-1-1ZM2 11a1 1 0 0 0 0 2h4.167a1 1 0 1 0 0-2H2ZM19.916 4.085a1 1 0 0 1 0 1.414l-2.917 2.917A1 1 0 1 1 15.585 7l2.917-2.916a1 1 0 0 1 1.414 0ZM8.415 16.999a1 1 0 0 0-1.414-1.414L4.084 18.5A1 1 0 1 0 5.5 19.916l2.916-2.917ZM15.585 15.585a1 1 0 0 1 1.414 0l2.917 2.916a1 1 0 1 1-1.414 1.415l-2.917-2.917a1 1 0 0 1 0-1.414ZM5.499 4.085a1 1 0 0 0-1.415 1.414l2.917 2.917A1 1 0 0 0 8.415 7L5.5 4.085Z" fill="#000000"/></svg>
|
||||
|
After Width: | Height: | Size: 796 B |
@@ -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