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, applyFont } 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 { Sidebar, type PageType } from "@/components/Sidebar"; 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 { DownloadQueue } from "@/components/DownloadQueue"; import { DownloadProgressToast } from "@/components/DownloadProgressToast"; import { AudioAnalysisPage } from "@/components/AudioAnalysisPage"; import { AudioConverterPage } from "@/components/AudioConverterPage"; import { SettingsPage } from "@/components/SettingsPage"; import { DebugLoggerPage } from "@/components/DebugLoggerPage"; import type { HistoryItem } from "@/components/FetchHistory"; // Hooks import { useDownload } from "@/hooks/useDownload"; import { useMetadata } from "@/hooks/useMetadata"; import { useLyrics } from "@/hooks/useLyrics"; import { useCover } from "@/hooks/useCover"; import { useAvailability } from "@/hooks/useAvailability"; import { useDownloadQueueDialog } from "@/hooks/useDownloadQueueDialog"; const HISTORY_KEY = "spotiflac_fetch_history"; const MAX_HISTORY = 5; function App() { const [currentPage, setCurrentPage] = useState("main"); const [spotifyUrl, setSpotifyUrl] = useState(""); const [selectedTracks, setSelectedTracks] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [sortBy, setSortBy] = useState("default"); const [currentListPage, setCurrentListPage] = useState(1); const [hasUpdate, setHasUpdate] = useState(false); const [releaseDate, setReleaseDate] = useState(null); const [fetchHistory, setFetchHistory] = useState([]); const ITEMS_PER_PAGE = 50; const CURRENT_VERSION = "6.8"; const download = useDownload(); const metadata = useMetadata(); const lyrics = useLyrics(); const cover = useCover(); const availability = useAvailability(); const downloadQueue = useDownloadQueueDialog(); useEffect(() => { const settings = getSettings(); applyThemeMode(settings.themeMode); applyTheme(settings.theme); applyFont(settings.fontFamily); 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(); cover.resetCoverState(); availability.clearAvailability(); setSortBy("default"); setCurrentListPage(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(); const latestVersion = data.tag_name?.replace(/^v/, "") || ""; if (data.published_at) { setReleaseDate(data.published_at); } 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); } }; 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); setCurrentListPage(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, position) } onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, album_info.name, position, trackId) } onCheckAvailability={availability.checkAvailability} onDownloadAllLyrics={() => lyrics.handleDownloadAllLyrics(track_list, album_info.name)} onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, album_info.name)} onDownloadAll={() => download.handleDownloadAll(track_list, undefined, true)} onDownloadSelected={() => download.handleDownloadSelected(selectedTracks, track_list, undefined, true) } onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} onPageChange={setCurrentListPage} 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, position) } onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, playlist_info.owner.name, position, trackId) } onCheckAvailability={availability.checkAvailability} onDownloadAllLyrics={() => lyrics.handleDownloadAllLyrics(track_list, playlist_info.owner.name)} onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, 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={setCurrentListPage} 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, position) } onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, artist_info.name, position, trackId) } onCheckAvailability={availability.checkAvailability} onDownloadAllLyrics={() => lyrics.handleDownloadAllLyrics(track_list, artist_info.name)} onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, artist_info.name)} onDownloadAll={() => download.handleDownloadAll(track_list, artist_info.name)} onDownloadSelected={() => download.handleDownloadSelected(selectedTracks, track_list, artist_info.name) } onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} onAlbumClick={metadata.handleAlbumClick} onArtistClick={async (artist) => { const artistUrl = await metadata.handleArtistClick(artist); if (artistUrl) { setSpotifyUrl(artistUrl); } }} onPageChange={setCurrentListPage} onTrackClick={async (track) => { if (track.external_urls) { setSpotifyUrl(track.external_urls); await metadata.handleFetchMetadata(track.external_urls); } }} /> ); } return null; }; const renderPage = () => { switch (currentPage) { case "settings": return ; case "debug": return ; case "audio-analysis": return ; case "audio-converter": return ; default: return ( <>
{/* 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()} ); } }; return (
{/* Main content area with sidebar offset */}
{renderPage()}
{/* Download Progress Toast - Bottom Left */} {/* Download Queue Dialog */}
); } export default App;