only show watch button when enabled in settings; better ux when clicking download button
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
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";
|
import type { AlbumType } from "../types/spotify";
|
||||||
|
|
||||||
interface AlbumCardProps {
|
interface AlbumCardProps {
|
||||||
@@ -7,6 +10,19 @@ interface AlbumCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AlbumCard = ({ album, onDownload }: 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 imageUrl = album.images && album.images.length > 0 ? album.images[0].url : "/placeholder.jpg";
|
||||||
const subtitle = album.artists.map((artist) => artist.name).join(", ");
|
const subtitle = album.artists.map((artist) => artist.name).join(", ");
|
||||||
|
|
||||||
@@ -21,10 +37,26 @@ export const AlbumCard = ({ album, onDownload }: AlbumCardProps) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onDownload();
|
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"
|
disabled={!!status && status !== "error"}
|
||||||
title="Download album"
|
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"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<img src="/download.svg" alt="Download" className="w-5 h-5 icon-inverse" />
|
{status
|
||||||
|
? status === "queued"
|
||||||
|
? "Queued."
|
||||||
|
: status === "error"
|
||||||
|
? <img src="/download.svg" alt="Download" className="w-5 h-5 icon-inverse" />
|
||||||
|
: "Downloading..."
|
||||||
|
: <img src="/download.svg" alt="Download" className="w-5 h-5 icon-inverse" />
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { useContext, useEffect } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { QueueContext, getStatus } from "../contexts/queue-context";
|
||||||
|
|
||||||
interface SearchResultCardProps {
|
interface SearchResultCardProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -10,6 +13,19 @@ interface SearchResultCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SearchResultCard = ({ id, name, subtitle, imageUrl, type, onDownload }: 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 = () => {
|
const getLinkPath = () => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "track":
|
case "track":
|
||||||
@@ -32,10 +48,26 @@ export const SearchResultCard = ({ id, name, subtitle, imageUrl, type, onDownloa
|
|||||||
{onDownload && (
|
{onDownload && (
|
||||||
<button
|
<button
|
||||||
onClick={onDownload}
|
onClick={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 z-10"
|
disabled={!!status && status !== "error"}
|
||||||
title={`Download ${type}`}
|
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 z-10 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title={
|
||||||
|
status
|
||||||
|
? status === "queued"
|
||||||
|
? `${name} queued`
|
||||||
|
: status === "error"
|
||||||
|
? `Download ${type}`
|
||||||
|
: "Downloading..."
|
||||||
|
: `Download ${type}`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<img src="/download.svg" alt="Download" className="w-5 h-5 logo" />
|
{status
|
||||||
|
? status === "queued"
|
||||||
|
? "Queued."
|
||||||
|
: status === "error"
|
||||||
|
? <img src="/download.svg" alt="Download" className="w-5 h-5 logo" />
|
||||||
|
: "Downloading..."
|
||||||
|
: <img src="/download.svg" alt="Download" className="w-5 h-5 logo" />
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Link, useParams } from "@tanstack/react-router";
|
import { Link, useParams } from "@tanstack/react-router";
|
||||||
import { useEffect, useState, useContext, useRef, useCallback } from "react";
|
import { useEffect, useState, useContext, useRef, useCallback } from "react";
|
||||||
import apiClient from "../lib/api-client";
|
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 { useSettings } from "../contexts/settings-context";
|
||||||
import type { AlbumType, TrackType } from "../types/spotify";
|
import type { AlbumType, TrackType } from "../types/spotify";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -24,7 +24,19 @@ export const Album = () => {
|
|||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useQueue must be used within a QueueProvider");
|
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 totalTracks = album?.total_tracks ?? 0;
|
||||||
const hasMore = tracks.length < totalTracks;
|
const hasMore = tracks.length < totalTracks;
|
||||||
@@ -174,13 +186,27 @@ export const Album = () => {
|
|||||||
<div className="mt-4 md:mt-6">
|
<div className="mt-4 md:mt-6">
|
||||||
<button
|
<button
|
||||||
onClick={handleDownloadAlbum}
|
onClick={handleDownloadAlbum}
|
||||||
disabled={isExplicitFilterEnabled && hasExplicitTrack}
|
disabled={(isExplicitFilterEnabled && hasExplicitTrack) || (!!albumQueueItem && albumStatus !== "error")}
|
||||||
className="w-full px-6 py-3 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed font-semibold shadow-sm"
|
className="w-full px-6 py-3 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed font-semibold shadow-sm"
|
||||||
title={
|
title={
|
||||||
isExplicitFilterEnabled && hasExplicitTrack ? "Album contains explicit tracks" : "Download Full Album"
|
isExplicitFilterEnabled && hasExplicitTrack
|
||||||
|
? "Album contains explicit tracks"
|
||||||
|
: albumStatus
|
||||||
|
? albumStatus === "queued"
|
||||||
|
? "Album queued"
|
||||||
|
: albumStatus === "error"
|
||||||
|
? "Download Full Album"
|
||||||
|
: "Downloading..."
|
||||||
|
: "Download Full Album"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Download Album
|
{albumStatus
|
||||||
|
? albumStatus === "queued"
|
||||||
|
? "Queued."
|
||||||
|
: albumStatus === "error"
|
||||||
|
? "Download Album"
|
||||||
|
: "Downloading..."
|
||||||
|
: "Download Album"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useEffect, useState, useContext, useRef, useCallback } from "react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import apiClient from "../lib/api-client";
|
import apiClient from "../lib/api-client";
|
||||||
import type { AlbumType, ArtistType, TrackType } from "../types/spotify";
|
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 { useSettings } from "../contexts/settings-context";
|
||||||
import { FaArrowLeft, FaBookmark, FaRegBookmark, FaDownload } from "react-icons/fa";
|
import { FaArrowLeft, FaBookmark, FaRegBookmark, FaDownload } from "react-icons/fa";
|
||||||
import { AlbumCard } from "../components/AlbumCard";
|
import { AlbumCard } from "../components/AlbumCard";
|
||||||
@@ -14,6 +14,7 @@ export const Artist = () => {
|
|||||||
const [albums, setAlbums] = useState<AlbumType[]>([]);
|
const [albums, setAlbums] = useState<AlbumType[]>([]);
|
||||||
const [topTracks, setTopTracks] = useState<TrackType[]>([]);
|
const [topTracks, setTopTracks] = useState<TrackType[]>([]);
|
||||||
const [isWatched, setIsWatched] = useState(false);
|
const [isWatched, setIsWatched] = useState(false);
|
||||||
|
const [artistStatus, setArtistStatus] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const context = useContext(QueueContext);
|
const context = useContext(QueueContext);
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
@@ -30,7 +31,14 @@ export const Artist = () => {
|
|||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useQueue must be used within a QueueProvider");
|
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<string, string | null>);
|
||||||
|
|
||||||
const applyFilters = useCallback(
|
const applyFilters = useCallback(
|
||||||
(items: AlbumType[]) => {
|
(items: AlbumType[]) => {
|
||||||
@@ -194,6 +202,7 @@ export const Artist = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDownloadArtist = async () => {
|
const handleDownloadArtist = async () => {
|
||||||
|
setArtistStatus("downloading");
|
||||||
if (!artistId || !artist) return;
|
if (!artistId || !artist) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -203,13 +212,16 @@ export const Artist = () => {
|
|||||||
const response = await apiClient.get(`/artist/download/${artistId}`);
|
const response = await apiClient.get(`/artist/download/${artistId}`);
|
||||||
|
|
||||||
if (response.data.queued_albums?.length > 0) {
|
if (response.data.queued_albums?.length > 0) {
|
||||||
|
setArtistStatus("queued");
|
||||||
toast.success(`${artist.name} discography queued successfully!`, {
|
toast.success(`${artist.name} discography queued successfully!`, {
|
||||||
description: `${response.data.queued_albums.length} albums added to queue.`,
|
description: `${response.data.queued_albums.length} albums added to queue.`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
setArtistStatus(null);
|
||||||
toast.info("No new albums to download for this artist.");
|
toast.info("No new albums to download for this artist.");
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
setArtistStatus("error");
|
||||||
console.error("Artist download failed:", error);
|
console.error("Artist download failed:", error);
|
||||||
toast.error("Failed to download artist", {
|
toast.error("Failed to download artist", {
|
||||||
description: error.response?.data?.error || "An unexpected error occurred.",
|
description: error.response?.data?.error || "An unexpected error occurred.",
|
||||||
@@ -273,11 +285,32 @@ export const Artist = () => {
|
|||||||
<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-button-success hover:bg-button-success-hover text-button-success-text rounded-md transition-colors"
|
disabled={artistStatus === "downloading" || artistStatus === "queued"}
|
||||||
|
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 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title={
|
||||||
|
artistStatus === "downloading"
|
||||||
|
? "Downloading..."
|
||||||
|
: artistStatus === "queued"
|
||||||
|
? "Queued."
|
||||||
|
: "Download All"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
|
{artistStatus
|
||||||
|
? artistStatus === "queued"
|
||||||
|
? "Queued."
|
||||||
|
: artistStatus === "downloading"
|
||||||
|
? "Downloading..."
|
||||||
|
: <>
|
||||||
<FaDownload className="icon-inverse" />
|
<FaDownload className="icon-inverse" />
|
||||||
<span>Download All</span>
|
<span>Download All</span>
|
||||||
|
</>
|
||||||
|
: <>
|
||||||
|
<FaDownload className="icon-inverse" />
|
||||||
|
<span>Download All</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
|
{settings?.watch?.enabled && (
|
||||||
<button
|
<button
|
||||||
onClick={handleToggleWatch}
|
onClick={handleToggleWatch}
|
||||||
className={`flex items-center gap-2 px-4 py-2 rounded-md transition-colors border ${isWatched
|
className={`flex items-center gap-2 px-4 py-2 rounded-md transition-colors border ${isWatched
|
||||||
@@ -297,6 +330,7 @@ export const Artist = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -318,9 +352,16 @@ export const Artist = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDownloadTrack(track)}
|
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"
|
disabled={!!trackStatuses[track.id] && trackStatuses[track.id] !== "error"}
|
||||||
|
className="px-3 py-1 bg-button-secondary hover:bg-button-secondary-hover text-button-secondary-text hover:text-button-secondary-text-hover rounded disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
Download
|
{trackStatuses[track.id]
|
||||||
|
? trackStatuses[track.id] === "queued"
|
||||||
|
? "Queued."
|
||||||
|
: trackStatuses[track.id] === "error"
|
||||||
|
? "Download"
|
||||||
|
: "Downloading..."
|
||||||
|
: "Download"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import apiClient from "../lib/api-client";
|
|||||||
import { useSettings } from "../contexts/settings-context";
|
import { useSettings } from "../contexts/settings-context";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { TrackType, PlaylistMetadataType, PlaylistTracksResponseType, PlaylistItemType } from "../types/spotify";
|
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";
|
import { FaArrowLeft } from "react-icons/fa";
|
||||||
|
|
||||||
|
|
||||||
@@ -28,7 +28,21 @@ export const Playlist = () => {
|
|||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useQueue must be used within a QueueProvider");
|
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
|
// Load playlist metadata first
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -167,6 +181,14 @@ export const Playlist = () => {
|
|||||||
return <div className="p-8 text-center">Loading playlist...</div>;
|
return <div className="p-8 text-center">Loading playlist...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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<string, string | null>);
|
||||||
|
|
||||||
const filteredTracks = tracks.filter(({ track }) => {
|
const filteredTracks = tracks.filter(({ track }) => {
|
||||||
if (!track) return false;
|
if (!track) return false;
|
||||||
if (settings?.explicitFilter && track.explicit) return false;
|
if (settings?.explicitFilter && track.explicit) return false;
|
||||||
@@ -209,10 +231,18 @@ export const Playlist = () => {
|
|||||||
<div className="mt-4 md:mt-6 flex flex-col sm:flex-row gap-2 sm:gap-3">
|
<div className="mt-4 md:mt-6 flex flex-col sm:flex-row gap-2 sm:gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={handleDownloadPlaylist}
|
onClick={handleDownloadPlaylist}
|
||||||
className="flex-1 px-6 py-3 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-lg transition-all font-semibold shadow-sm"
|
disabled={!!playlistQueueItem && playlistStatus !== "error"}
|
||||||
|
className="flex-1 px-6 py-3 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-lg transition-all font-semibold shadow-sm disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
Download All
|
{playlistStatus
|
||||||
|
? playlistStatus === "queued"
|
||||||
|
? "Queued."
|
||||||
|
: playlistStatus === "error"
|
||||||
|
? "Download All"
|
||||||
|
: "Downloading..."
|
||||||
|
: "Download All"}
|
||||||
</button>
|
</button>
|
||||||
|
{settings?.watch?.enabled && (
|
||||||
<button
|
<button
|
||||||
onClick={handleToggleWatch}
|
onClick={handleToggleWatch}
|
||||||
className={`flex items-center justify-center gap-2 px-6 py-3 rounded-lg transition-all font-semibold shadow-sm ${
|
className={`flex items-center justify-center gap-2 px-6 py-3 rounded-lg transition-all font-semibold shadow-sm ${
|
||||||
@@ -228,6 +258,7 @@ export const Playlist = () => {
|
|||||||
/>
|
/>
|
||||||
{isWatched ? "Unwatch" : "Watch"}
|
{isWatched ? "Unwatch" : "Watch"}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -287,10 +318,26 @@ export const Playlist = () => {
|
|||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDownloadTrack(track)}
|
onClick={() => handleDownloadTrack(track)}
|
||||||
className="w-9 h-9 md:w-10 md:h-10 flex items-center justify-center bg-surface-muted dark:bg-surface-muted-dark hover:bg-surface-accent dark:hover:bg-surface-accent-dark border border-border-muted dark:border-border-muted-dark hover:border-border-accent dark:hover:border-border-accent-dark rounded-full transition-all hover:scale-105 hover:shadow-sm"
|
disabled={!!trackStatuses[track.id] && trackStatuses[track.id] !== "error"}
|
||||||
title="Download"
|
className="w-9 h-9 md:w-10 md:h-10 flex items-center justify-center bg-surface-muted dark:bg-surface-muted-dark hover:bg-surface-accent dark:hover:bg-surface-accent-dark border border-border-muted dark:border-border-muted-dark hover:border-border-accent dark:hover:border-border-accent-dark rounded-full transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title={
|
||||||
|
trackStatuses[track.id]
|
||||||
|
? trackStatuses[track.id] === "queued"
|
||||||
|
? "Queued."
|
||||||
|
: trackStatuses[track.id] === "error"
|
||||||
|
? "Download"
|
||||||
|
: "Downloading..."
|
||||||
|
: "Download"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<img src="/download.svg" alt="Download" className="w-4 h-4 logo" />
|
{trackStatuses[track.id]
|
||||||
|
? trackStatuses[track.id] === "queued"
|
||||||
|
? "Queued."
|
||||||
|
: trackStatuses[track.id] === "error"
|
||||||
|
? <img src="/download.svg" alt="Download" className="w-4 h-4 logo" />
|
||||||
|
: "Downloading..."
|
||||||
|
: <img src="/download.svg" alt="Download" className="w-4 h-4 logo" />
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useEffect, useState, useContext } from "react";
|
|||||||
import apiClient from "../lib/api-client";
|
import apiClient from "../lib/api-client";
|
||||||
import type { TrackType } from "../types/spotify";
|
import type { TrackType } from "../types/spotify";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { QueueContext } from "../contexts/queue-context";
|
import { QueueContext, getStatus } from "../contexts/queue-context";
|
||||||
import { FaSpotify, FaArrowLeft } from "react-icons/fa";
|
import { FaSpotify, FaArrowLeft } from "react-icons/fa";
|
||||||
|
|
||||||
// Helper to format milliseconds to mm:ss
|
// Helper to format milliseconds to mm:ss
|
||||||
@@ -22,7 +22,19 @@ export const Track = () => {
|
|||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useQueue must be used within a QueueProvider");
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchTrack = async () => {
|
const fetchTrack = async () => {
|
||||||
@@ -173,9 +185,16 @@ export const Track = () => {
|
|||||||
<div className="flex flex-col sm:flex-row items-center gap-4">
|
<div className="flex flex-col sm:flex-row items-center gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={handleDownloadTrack}
|
onClick={handleDownloadTrack}
|
||||||
className="w-full sm:w-auto bg-button-primary hover:bg-button-primary-hover text-button-primary-text font-bold py-3 px-8 rounded-full transition duration-300 shadow-lg hover:shadow-xl"
|
disabled={!!trackQueueItem && trackStatus !== "error"}
|
||||||
|
className="w-full sm:w-auto bg-button-primary hover:bg-button-primary-hover text-button-primary-text font-bold py-3 px-8 rounded-full transition duration-300 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
Download
|
{trackStatus
|
||||||
|
? trackStatus === "queued"
|
||||||
|
? "Queued."
|
||||||
|
: trackStatus === "error"
|
||||||
|
? "Download"
|
||||||
|
: "Downloading..."
|
||||||
|
: "Download"}
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
href={track.external_urls.spotify}
|
href={track.external_urls.spotify}
|
||||||
|
|||||||
Reference in New Issue
Block a user