diff --git a/spotizerr-ui/src/components/AlbumCard.tsx b/spotizerr-ui/src/components/AlbumCard.tsx index b144983..00026e9 100644 --- a/spotizerr-ui/src/components/AlbumCard.tsx +++ b/spotizerr-ui/src/components/AlbumCard.tsx @@ -1,4 +1,7 @@ import { Link } from "@tanstack/react-router"; +import { useContext, useEffect } from "react"; +import { toast } from "sonner"; +import { QueueContext, getStatus } from "../contexts/queue-context"; import type { AlbumType } from "../types/spotify"; interface AlbumCardProps { @@ -7,6 +10,19 @@ interface AlbumCardProps { } export const AlbumCard = ({ album, onDownload }: AlbumCardProps) => { + const context = useContext(QueueContext); + if (!context) throw new Error("useQueue must be used within a QueueProvider"); + const { items } = context; + const queueItem = items.find(item => item.downloadType === "album" && item.spotifyId === album.id); + const status = queueItem ? getStatus(queueItem) : null; + + useEffect(() => { + if (status === "queued") { + toast.success(`${album.name} queued.`); + } else if (status === "error") { + toast.error(`Failed to queue ${album.name}`); + } + }, [status, album.name]); const imageUrl = album.images && album.images.length > 0 ? album.images[0].url : "/placeholder.jpg"; const subtitle = album.artists.map((artist) => artist.name).join(", "); @@ -21,10 +37,26 @@ export const AlbumCard = ({ album, onDownload }: AlbumCardProps) => { e.preventDefault(); onDownload(); }} - 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" + disabled={!!status && status !== "error"} + 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 disabled:opacity-50 disabled:cursor-not-allowed" + title={ + status + ? status === "queued" + ? "Album queued" + : status === "error" + ? "Download album" + : "Downloading..." + : "Download album" + } > - Download + {status + ? status === "queued" + ? "Queued." + : status === "error" + ? Download + : "Downloading..." + : Download + } )} diff --git a/spotizerr-ui/src/components/SearchResultCard.tsx b/spotizerr-ui/src/components/SearchResultCard.tsx index 317d3d6..7522bcd 100644 --- a/spotizerr-ui/src/components/SearchResultCard.tsx +++ b/spotizerr-ui/src/components/SearchResultCard.tsx @@ -1,4 +1,7 @@ import { Link } from "@tanstack/react-router"; +import { useContext, useEffect } from "react"; +import { toast } from "sonner"; +import { QueueContext, getStatus } from "../contexts/queue-context"; interface SearchResultCardProps { id: string; @@ -10,6 +13,19 @@ interface SearchResultCardProps { } export const SearchResultCard = ({ id, name, subtitle, imageUrl, type, onDownload }: SearchResultCardProps) => { + const context = useContext(QueueContext); + if (!context) throw new Error("useQueue must be used within a QueueProvider"); + const { items } = context; + const queueItem = items.find(item => item.downloadType === type && item.spotifyId === id); + const status = queueItem ? getStatus(queueItem) : null; + + useEffect(() => { + if (status === "queued") { + toast.success(`${name} queued.`); + } else if (status === "error") { + toast.error(`Failed to queue ${name}`); + } + }, [status]); const getLinkPath = () => { switch (type) { case "track": @@ -32,10 +48,26 @@ export const SearchResultCard = ({ id, name, subtitle, imageUrl, type, onDownloa {onDownload && ( )} diff --git a/spotizerr-ui/src/routes/album.tsx b/spotizerr-ui/src/routes/album.tsx index 502001c..992e844 100644 --- a/spotizerr-ui/src/routes/album.tsx +++ b/spotizerr-ui/src/routes/album.tsx @@ -1,7 +1,7 @@ import { Link, useParams } from "@tanstack/react-router"; import { useEffect, useState, useContext, useRef, useCallback } from "react"; import apiClient from "../lib/api-client"; -import { QueueContext } from "../contexts/queue-context"; +import { QueueContext, getStatus } from "../contexts/queue-context"; import { useSettings } from "../contexts/settings-context"; import type { AlbumType, TrackType } from "../types/spotify"; import { toast } from "sonner"; @@ -24,7 +24,19 @@ export const Album = () => { if (!context) { throw new Error("useQueue must be used within a QueueProvider"); } - const { addItem } = context; + const { addItem, items } = context; + + // Queue status for this album + const albumQueueItem = items.find(item => item.downloadType === "album" && item.spotifyId === album?.id); + const albumStatus = albumQueueItem ? getStatus(albumQueueItem) : null; + + useEffect(() => { + if (albumStatus === "queued") { + toast.success(`${album?.name} queued.`); + } else if (albumStatus === "error") { + toast.error(`Failed to queue ${album?.name}`); + } + }, [albumStatus]); const totalTracks = album?.total_tracks ?? 0; const hasMore = tracks.length < totalTracks; @@ -174,13 +186,27 @@ export const Album = () => {
diff --git a/spotizerr-ui/src/routes/artist.tsx b/spotizerr-ui/src/routes/artist.tsx index 7a59725..9f74b6c 100644 --- a/spotizerr-ui/src/routes/artist.tsx +++ b/spotizerr-ui/src/routes/artist.tsx @@ -3,7 +3,7 @@ import { useEffect, useState, useContext, useRef, useCallback } from "react"; import { toast } from "sonner"; import apiClient from "../lib/api-client"; import type { AlbumType, ArtistType, TrackType } from "../types/spotify"; -import { QueueContext } from "../contexts/queue-context"; +import { QueueContext, getStatus } from "../contexts/queue-context"; import { useSettings } from "../contexts/settings-context"; import { FaArrowLeft, FaBookmark, FaRegBookmark, FaDownload } from "react-icons/fa"; import { AlbumCard } from "../components/AlbumCard"; @@ -14,6 +14,7 @@ export const Artist = () => { const [albums, setAlbums] = useState([]); const [topTracks, setTopTracks] = useState([]); const [isWatched, setIsWatched] = useState(false); + const [artistStatus, setArtistStatus] = useState(null); const [error, setError] = useState(null); const context = useContext(QueueContext); const { settings } = useSettings(); @@ -30,7 +31,14 @@ export const Artist = () => { if (!context) { throw new Error("useQueue must be used within a QueueProvider"); } - const { addItem } = context; + const { addItem, items } = context; + + // Track queue status mapping + const trackStatuses = topTracks.reduce((acc, t) => { + const qi = items.find(item => item.downloadType === "track" && item.spotifyId === t.id); + acc[t.id] = qi ? getStatus(qi) : null; + return acc; + }, {} as Record); const applyFilters = useCallback( (items: AlbumType[]) => { @@ -194,6 +202,7 @@ export const Artist = () => { }; const handleDownloadArtist = async () => { + setArtistStatus("downloading"); if (!artistId || !artist) return; try { @@ -203,13 +212,16 @@ export const Artist = () => { const response = await apiClient.get(`/artist/download/${artistId}`); if (response.data.queued_albums?.length > 0) { + setArtistStatus("queued"); toast.success(`${artist.name} discography queued successfully!`, { description: `${response.data.queued_albums.length} albums added to queue.`, }); } else { + setArtistStatus(null); toast.info("No new albums to download for this artist."); } } catch (error: any) { + setArtistStatus("error"); console.error("Artist download failed:", error); toast.error("Failed to download artist", { description: error.response?.data?.error || "An unexpected error occurred.", @@ -273,30 +285,52 @@ export const Artist = () => {
- + {settings?.watch?.enabled && ( + + )}
@@ -318,9 +352,16 @@ export const Artist = () => { ))} diff --git a/spotizerr-ui/src/routes/playlist.tsx b/spotizerr-ui/src/routes/playlist.tsx index 342a65a..93096b5 100644 --- a/spotizerr-ui/src/routes/playlist.tsx +++ b/spotizerr-ui/src/routes/playlist.tsx @@ -4,7 +4,7 @@ import apiClient from "../lib/api-client"; import { useSettings } from "../contexts/settings-context"; import { toast } from "sonner"; import type { TrackType, PlaylistMetadataType, PlaylistTracksResponseType, PlaylistItemType } from "../types/spotify"; -import { QueueContext } from "../contexts/queue-context"; +import { QueueContext, getStatus } from "../contexts/queue-context"; import { FaArrowLeft } from "react-icons/fa"; @@ -28,7 +28,21 @@ export const Playlist = () => { if (!context) { throw new Error("useQueue must be used within a QueueProvider"); } - const { addItem } = context; + const { addItem, items } = context; + + // Playlist queue status + const playlistQueueItem = playlistMetadata + ? items.find(item => item.downloadType === "playlist" && item.spotifyId === playlistMetadata.id) + : undefined; + const playlistStatus = playlistQueueItem ? getStatus(playlistQueueItem) : null; + + useEffect(() => { + if (playlistStatus === "queued") { + toast.success(`${playlistMetadata?.name} queued.`); + } else if (playlistStatus === "error") { + toast.error(`Failed to queue ${playlistMetadata?.name}`); + } + }, [playlistStatus]); // Load playlist metadata first useEffect(() => { @@ -167,6 +181,14 @@ export const Playlist = () => { return
Loading playlist...
; } + // Map track download statuses + const trackStatuses = tracks.reduce((acc, { track }) => { + if (!track) return acc; + const qi = items.find(item => item.downloadType === "track" && item.spotifyId === track.id); + acc[track.id] = qi ? getStatus(qi) : null; + return acc; + }, {} as Record); + const filteredTracks = tracks.filter(({ track }) => { if (!track) return false; if (settings?.explicitFilter && track.explicit) return false; @@ -209,25 +231,34 @@ export const Playlist = () => {
- + {settings?.watch?.enabled && ( + + )}
@@ -287,10 +318,26 @@ export const Playlist = () => { diff --git a/spotizerr-ui/src/routes/track.tsx b/spotizerr-ui/src/routes/track.tsx index ad85aab..387f7e4 100644 --- a/spotizerr-ui/src/routes/track.tsx +++ b/spotizerr-ui/src/routes/track.tsx @@ -3,7 +3,7 @@ import { useEffect, useState, useContext } from "react"; import apiClient from "../lib/api-client"; import type { TrackType } from "../types/spotify"; import { toast } from "sonner"; -import { QueueContext } from "../contexts/queue-context"; +import { QueueContext, getStatus } from "../contexts/queue-context"; import { FaSpotify, FaArrowLeft } from "react-icons/fa"; // Helper to format milliseconds to mm:ss @@ -22,7 +22,19 @@ export const Track = () => { if (!context) { throw new Error("useQueue must be used within a QueueProvider"); } - const { addItem } = context; + const { addItem, items } = context; + + // Track queue status + const trackQueueItem = track ? items.find(item => item.downloadType === "track" && item.spotifyId === track.id) : undefined; + const trackStatus = trackQueueItem ? getStatus(trackQueueItem) : null; + + useEffect(() => { + if (trackStatus === "queued") { + toast.success(`${track?.name} queued.`); + } else if (trackStatus === "error") { + toast.error(`Failed to queue ${track?.name}`); + } + }, [trackStatus]); useEffect(() => { const fetchTrack = async () => { @@ -173,9 +185,16 @@ export const Track = () => {