import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogTitle, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Search, X } from "lucide-react"; import { TooltipProvider } from "@/components/ui/tooltip"; import { getSettings, applyThemeMode } from "@/lib/settings"; import { applyTheme } from "@/lib/themes"; import { OpenFolder } from "../wailsjs/go/main/App"; import { toastWithSound as toast } from "@/lib/toast-with-sound"; // Components import { TitleBar } from "@/components/TitleBar"; import { Header } from "@/components/Header"; import { SearchBar } from "@/components/SearchBar"; import { TrackInfo } from "@/components/TrackInfo"; import { AlbumInfo } from "@/components/AlbumInfo"; import { PlaylistInfo } from "@/components/PlaylistInfo"; import { ArtistInfo } from "@/components/ArtistInfo"; import { DownloadProgressToast } from "@/components/DownloadProgressToast"; import type { HistoryItem } from "@/components/FetchHistory"; // Hooks import { useDownload } from "@/hooks/useDownload"; import { useMetadata } from "@/hooks/useMetadata"; import { useLyrics } from "@/hooks/useLyrics"; const HISTORY_KEY = "spotiflac_fetch_history"; const MAX_HISTORY = 5; function App() { const [spotifyUrl, setSpotifyUrl] = useState(""); const [selectedTracks, setSelectedTracks] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [sortBy, setSortBy] = useState("default"); const [currentPage, setCurrentPage] = useState(1); const [hasUpdate, setHasUpdate] = useState(false); const [fetchHistory, setFetchHistory] = useState([]); const ITEMS_PER_PAGE = 50; const CURRENT_VERSION = "6.3"; const download = useDownload(); const metadata = useMetadata(); const lyrics = useLyrics(); useEffect(() => { const settings = getSettings(); applyThemeMode(settings.themeMode); applyTheme(settings.theme); const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); const handleChange = () => { const currentSettings = getSettings(); if (currentSettings.themeMode === "auto") { applyThemeMode("auto"); applyTheme(currentSettings.theme); } }; mediaQuery.addEventListener("change", handleChange); checkForUpdates(); loadHistory(); return () => { mediaQuery.removeEventListener("change", handleChange); }; }, []); useEffect(() => { setSelectedTracks([]); setSearchQuery(""); download.resetDownloadedTracks(); lyrics.resetLyricsState(); setSortBy("default"); setCurrentPage(1); }, [metadata.metadata]); const checkForUpdates = async () => { try { const response = await fetch( "https://api.github.com/repos/afkarxyz/SpotiFLAC/releases/latest" ); const data = await response.json(); // tag_name format: "v6.1" -> extract "6.1" const latestVersion = data.tag_name?.replace(/^v/, "") || ""; if (latestVersion && latestVersion > CURRENT_VERSION) { setHasUpdate(true); } } catch (err) { console.error("Failed to check for updates:", err); } }; const loadHistory = () => { try { const saved = localStorage.getItem(HISTORY_KEY); if (saved) { setFetchHistory(JSON.parse(saved)); } } catch (err) { console.error("Failed to load history:", err); } }; const saveHistory = (history: HistoryItem[]) => { try { localStorage.setItem(HISTORY_KEY, JSON.stringify(history)); } catch (err) { console.error("Failed to save history:", err); } }; const addToHistory = (item: Omit) => { setFetchHistory((prev) => { const filtered = prev.filter((h) => h.url !== item.url); const newItem: HistoryItem = { ...item, id: crypto.randomUUID(), timestamp: Date.now(), }; const updated = [newItem, ...filtered].slice(0, MAX_HISTORY); saveHistory(updated); return updated; }); }; const removeFromHistory = (id: string) => { setFetchHistory((prev) => { const updated = prev.filter((h) => h.id !== id); saveHistory(updated); return updated; }); }; const handleHistorySelect = async (item: HistoryItem) => { setSpotifyUrl(item.url); const updatedUrl = await metadata.handleFetchMetadata(item.url); if (updatedUrl) { setSpotifyUrl(updatedUrl); } }; const handleFetchMetadata = async () => { const updatedUrl = await metadata.handleFetchMetadata(spotifyUrl); if (updatedUrl) { setSpotifyUrl(updatedUrl); } }; // Add to history when metadata is successfully fetched useEffect(() => { if (!metadata.metadata || !spotifyUrl) return; let historyItem: Omit | null = null; if ("track" in metadata.metadata) { const { track } = metadata.metadata; historyItem = { url: spotifyUrl, type: "track", name: track.name, artist: track.artists, image: track.images, }; } else if ("album_info" in metadata.metadata) { const { album_info } = metadata.metadata; historyItem = { url: spotifyUrl, type: "album", name: album_info.name, artist: album_info.artists, image: album_info.images, }; } else if ("playlist_info" in metadata.metadata) { const { playlist_info } = metadata.metadata; historyItem = { url: spotifyUrl, type: "playlist", name: playlist_info.owner.name, artist: `${playlist_info.tracks.total} tracks • ${playlist_info.owner.display_name}`, image: playlist_info.owner.images || "", }; } else if ("artist_info" in metadata.metadata) { const { artist_info } = metadata.metadata; historyItem = { url: spotifyUrl, type: "artist", name: artist_info.name, artist: `${artist_info.total_albums} albums`, image: artist_info.images, }; } if (historyItem) { addToHistory(historyItem); } }, [metadata.metadata]); const handleSearchChange = (value: string) => { setSearchQuery(value); setCurrentPage(1); }; const toggleTrackSelection = (isrc: string) => { setSelectedTracks((prev) => prev.includes(isrc) ? prev.filter((id) => id !== isrc) : [...prev, isrc] ); }; const toggleSelectAll = (tracks: any[]) => { const tracksWithIsrc = tracks.filter((track) => track.isrc).map((track) => track.isrc); if (selectedTracks.length === tracksWithIsrc.length) { setSelectedTracks([]); } else { setSelectedTracks(tracksWithIsrc); } }; const handleOpenFolder = async () => { const settings = getSettings(); if (!settings.downloadPath) { toast.error("Download path not set"); return; } try { await OpenFolder(settings.downloadPath); } catch (error) { console.error("Error opening folder:", error); toast.error(`Error opening folder: ${error}`); } }; const renderMetadata = () => { if (!metadata.metadata) return null; if ("track" in metadata.metadata) { const { track } = metadata.metadata; return ( ); } if ("album_info" in metadata.metadata) { const { album_info, track_list } = metadata.metadata; return ( lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, album_info.name) } onDownloadAll={() => download.handleDownloadAll(track_list, album_info.name)} onDownloadSelected={() => download.handleDownloadSelected(selectedTracks, track_list, album_info.name) } onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} onPageChange={setCurrentPage} onArtistClick={async (artist) => { const artistUrl = await metadata.handleArtistClick(artist); if (artistUrl) { setSpotifyUrl(artistUrl); } }} onTrackClick={async (track) => { if (track.external_urls) { setSpotifyUrl(track.external_urls); await metadata.handleFetchMetadata(track.external_urls); } }} /> ); } if ("playlist_info" in metadata.metadata) { const { playlist_info, track_list } = metadata.metadata; return ( lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, playlist_info.owner.name) } onDownloadAll={() => download.handleDownloadAll(track_list, playlist_info.owner.name)} onDownloadSelected={() => download.handleDownloadSelected( selectedTracks, track_list, playlist_info.owner.name ) } onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} onPageChange={setCurrentPage} onAlbumClick={metadata.handleAlbumClick} onArtistClick={async (artist) => { const artistUrl = await metadata.handleArtistClick(artist); if (artistUrl) { setSpotifyUrl(artistUrl); } }} onTrackClick={async (track) => { if (track.external_urls) { setSpotifyUrl(track.external_urls); await metadata.handleFetchMetadata(track.external_urls); } }} /> ); } if ("artist_info" in metadata.metadata) { const { artist_info, album_list, track_list } = metadata.metadata; return ( lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, artist_info.name, isArtistDiscography) } onDownloadAll={() => download.handleDownloadAll(track_list, artist_info.name, true)} onDownloadSelected={() => download.handleDownloadSelected(selectedTracks, track_list, artist_info.name, true) } onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} onAlbumClick={metadata.handleAlbumClick} onArtistClick={async (artist) => { const artistUrl = await metadata.handleArtistClick(artist); if (artistUrl) { setSpotifyUrl(artistUrl); } }} onPageChange={setCurrentPage} onTrackClick={async (track) => { if (track.external_urls) { setSpotifyUrl(track.external_urls); await metadata.handleFetchMetadata(track.external_urls); } }} /> ); } return null; }; return (
{/* Download Progress Toast */} {/* Timeout Dialog */}
Fetch Artist Set timeout for fetching metadata. Longer timeout is recommended for artists with large discography. {metadata.pendingArtistName && (

{metadata.pendingArtistName}

)}
metadata.setTimeoutValue(Number(e.target.value))} />

Default: 60 seconds. For large discographies, try 300-600 seconds (5-10 minutes).

{/* Album Fetch Dialog */}
Fetch Album Do you want to fetch metadata for this album? {metadata.selectedAlbum && (

{metadata.selectedAlbum.name}

)}
{metadata.metadata && renderMetadata()}
); } export default App;