This commit is contained in:
afkarxyz
2025-12-05 05:25:50 +07:00
parent cc3f7640c6
commit d16eaa324a
31 changed files with 2091 additions and 824 deletions
+201 -147
View File
@@ -11,13 +11,14 @@ 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 { 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";
@@ -27,43 +28,48 @@ import { ArtistInfo } from "@/components/ArtistInfo";
import { DownloadQueue } from "@/components/DownloadQueue";
import { DownloadProgressToast } from "@/components/DownloadProgressToast";
import { AudioAnalysisPage } from "@/components/AudioAnalysisPage";
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;
type PageType = "main" | "audio-analysis";
function App() {
const [currentPageView, setCurrentPageView] = useState<PageType>("main");
const [currentPage, setCurrentPage] = useState<PageType>("main");
const [spotifyUrl, setSpotifyUrl] = useState("");
const [selectedTracks, setSelectedTracks] = useState<string[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [sortBy, setSortBy] = useState<string>("default");
const [currentPage, setCurrentPage] = useState(1);
const [currentListPage, setCurrentListPage] = useState(1);
const [hasUpdate, setHasUpdate] = useState(false);
const [releaseDate, setReleaseDate] = useState<string | null>(null);
const [fetchHistory, setFetchHistory] = useState<HistoryItem[]>([]);
const ITEMS_PER_PAGE = 50;
const CURRENT_VERSION = "6.5";
const CURRENT_VERSION = "6.6";
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 = () => {
@@ -88,9 +94,10 @@ function App() {
setSearchQuery("");
download.resetDownloadedTracks();
lyrics.resetLyricsState();
cover.resetCoverState();
availability.clearAvailability();
setSortBy("default");
setCurrentPage(1);
setCurrentListPage(1);
}, [metadata.metadata]);
const checkForUpdates = async () => {
@@ -99,9 +106,12 @@ function App() {
"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 (data.published_at) {
setReleaseDate(data.published_at);
}
if (latestVersion && latestVersion > CURRENT_VERSION) {
setHasUpdate(true);
}
@@ -166,7 +176,6 @@ function App() {
}
};
// Add to history when metadata is successfully fetched
useEffect(() => {
if (!metadata.metadata || !spotifyUrl) return;
@@ -217,7 +226,7 @@ function App() {
const handleSearchChange = (value: string) => {
setSearchQuery(value);
setCurrentPage(1);
setCurrentListPage(1);
};
const toggleTrackSelection = (isrc: string) => {
@@ -250,6 +259,7 @@ function App() {
}
};
const renderMetadata = () => {
if (!metadata.metadata) return null;
@@ -269,9 +279,11 @@ function App() {
skippedLyrics={lyrics.skippedLyrics.has(track.spotify_id || "")}
checkingAvailability={availability.checkingTrackId === track.spotify_id}
availability={availability.getAvailability(track.spotify_id || "")}
downloadingCover={cover.downloadingCover}
onDownload={download.handleDownloadTrack}
onDownloadLyrics={lyrics.handleDownloadLyrics}
onCheckAvailability={availability.checkAvailability}
onDownloadCover={cover.handleDownloadCover}
onOpenFolder={handleOpenFolder}
/>
);
@@ -294,7 +306,7 @@ function App() {
bulkDownloadType={download.bulkDownloadType}
downloadProgress={download.downloadProgress}
currentDownloadInfo={download.currentDownloadInfo}
currentPage={currentPage}
currentPage={currentListPage}
itemsPerPage={ITEMS_PER_PAGE}
downloadedLyrics={lyrics.downloadedLyrics}
failedLyrics={lyrics.failedLyrics}
@@ -302,6 +314,11 @@ function App() {
downloadingLyricsTrack={lyrics.downloadingLyricsTrack}
checkingAvailabilityTrack={availability.checkingTrackId}
availabilityMap={availability.availabilityMap}
downloadedCovers={cover.downloadedCovers}
failedCovers={cover.failedCovers}
skippedCovers={cover.skippedCovers}
downloadingCoverTrack={cover.downloadingCoverTrack}
isBulkDownloadingCovers={cover.isBulkDownloadingCovers}
onSearchChange={handleSearchChange}
onSortChange={setSortBy}
onToggleTrack={toggleTrackSelection}
@@ -310,14 +327,18 @@ function App() {
onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position) =>
lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, album_info.name, false, position)
}
onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId) =>
cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, album_info.name, false, position, trackId)
}
onCheckAvailability={availability.checkAvailability}
onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, 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}
onPageChange={setCurrentListPage}
onArtistClick={async (artist) => {
const artistUrl = await metadata.handleArtistClick(artist);
if (artistUrl) {
@@ -351,7 +372,7 @@ function App() {
bulkDownloadType={download.bulkDownloadType}
downloadProgress={download.downloadProgress}
currentDownloadInfo={download.currentDownloadInfo}
currentPage={currentPage}
currentPage={currentListPage}
itemsPerPage={ITEMS_PER_PAGE}
downloadedLyrics={lyrics.downloadedLyrics}
failedLyrics={lyrics.failedLyrics}
@@ -359,6 +380,11 @@ function App() {
downloadingLyricsTrack={lyrics.downloadingLyricsTrack}
checkingAvailabilityTrack={availability.checkingTrackId}
availabilityMap={availability.availabilityMap}
downloadedCovers={cover.downloadedCovers}
failedCovers={cover.failedCovers}
skippedCovers={cover.skippedCovers}
downloadingCoverTrack={cover.downloadingCoverTrack}
isBulkDownloadingCovers={cover.isBulkDownloadingCovers}
onSearchChange={handleSearchChange}
onSortChange={setSortBy}
onToggleTrack={toggleTrackSelection}
@@ -367,7 +393,11 @@ function App() {
onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position) =>
lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, playlist_info.owner.name, false, position)
}
onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId) =>
cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, playlist_info.owner.name, false, position, trackId)
}
onCheckAvailability={availability.checkAvailability}
onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, playlist_info.owner.name)}
onDownloadAll={() => download.handleDownloadAll(track_list, playlist_info.owner.name)}
onDownloadSelected={() =>
download.handleDownloadSelected(
@@ -378,7 +408,7 @@ function App() {
}
onStopDownload={download.handleStopDownload}
onOpenFolder={handleOpenFolder}
onPageChange={setCurrentPage}
onPageChange={setCurrentListPage}
onAlbumClick={metadata.handleAlbumClick}
onArtistClick={async (artist) => {
const artistUrl = await metadata.handleArtistClick(artist);
@@ -414,7 +444,7 @@ function App() {
bulkDownloadType={download.bulkDownloadType}
downloadProgress={download.downloadProgress}
currentDownloadInfo={download.currentDownloadInfo}
currentPage={currentPage}
currentPage={currentListPage}
itemsPerPage={ITEMS_PER_PAGE}
downloadedLyrics={lyrics.downloadedLyrics}
failedLyrics={lyrics.failedLyrics}
@@ -422,6 +452,11 @@ function App() {
downloadingLyricsTrack={lyrics.downloadingLyricsTrack}
checkingAvailabilityTrack={availability.checkingTrackId}
availabilityMap={availability.availabilityMap}
downloadedCovers={cover.downloadedCovers}
failedCovers={cover.failedCovers}
skippedCovers={cover.skippedCovers}
downloadingCoverTrack={cover.downloadingCoverTrack}
isBulkDownloadingCovers={cover.isBulkDownloadingCovers}
onSearchChange={handleSearchChange}
onSortChange={setSortBy}
onToggleTrack={toggleTrackSelection}
@@ -430,7 +465,11 @@ function App() {
onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, isArtistDiscography, position) =>
lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, artist_info.name, isArtistDiscography, position)
}
onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, isArtistDiscography, position, trackId) =>
cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, artist_info.name, isArtistDiscography, position, trackId)
}
onCheckAvailability={availability.checkAvailability}
onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, artist_info.name, true)}
onDownloadAll={() => download.handleDownloadAll(track_list, artist_info.name, true)}
onDownloadSelected={() =>
download.handleDownloadSelected(selectedTracks, track_list, artist_info.name, true)
@@ -444,7 +483,7 @@ function App() {
setSpotifyUrl(artistUrl);
}
}}
onPageChange={setCurrentPage}
onPageChange={setCurrentListPage}
onTrackClick={async (track) => {
if (track.external_urls) {
setSpotifyUrl(track.external_urls);
@@ -458,144 +497,159 @@ function App() {
return null;
};
const renderPage = () => {
switch (currentPage) {
case "settings":
return <SettingsPage />;
case "debug":
return <DebugLoggerPage />;
case "audio-analysis":
return <AudioAnalysisPage />;
default:
return (
<>
<Header
version={CURRENT_VERSION}
hasUpdate={hasUpdate}
releaseDate={releaseDate}
/>
{/* Timeout Dialog */}
<Dialog
open={metadata.showTimeoutDialog}
onOpenChange={metadata.setShowTimeoutDialog}
>
<DialogContent className="sm:max-w-[425px] p-6 [&>button]:hidden">
<div className="absolute right-4 top-4">
<Button
variant="ghost"
size="icon"
className="h-6 w-6 opacity-70 hover:opacity-100"
onClick={() => metadata.setShowTimeoutDialog(false)}
>
<X className="h-4 w-4" />
</Button>
</div>
<DialogTitle className="text-sm font-medium">Fetch Artist</DialogTitle>
<DialogDescription>
Set timeout for fetching metadata. Longer timeout is recommended for artists
with large discography.
</DialogDescription>
{metadata.pendingArtistName && (
<div className="py-2">
<p className="font-medium bg-muted/50 rounded-md px-3 py-2">{metadata.pendingArtistName}</p>
</div>
)}
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="timeout">Timeout (seconds)</Label>
<Input
id="timeout"
type="number"
min="10"
max="600"
value={metadata.timeoutValue}
onChange={(e) => metadata.setTimeoutValue(Number(e.target.value))}
/>
<p className="text-xs text-muted-foreground">
Default: 60 seconds. For large discographies, try 300-600 seconds (5-10
minutes).
</p>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => metadata.setShowTimeoutDialog(false)}
>
Cancel
</Button>
<Button onClick={metadata.handleConfirmFetch}>
<Search className="h-4 w-4" />
Fetch
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Album Fetch Dialog */}
<Dialog open={metadata.showAlbumDialog} onOpenChange={metadata.setShowAlbumDialog}>
<DialogContent className="sm:max-w-[425px] p-6 [&>button]:hidden">
<div className="absolute right-4 top-4">
<Button
variant="ghost"
size="icon"
className="h-6 w-6 opacity-70 hover:opacity-100"
onClick={() => metadata.setShowAlbumDialog(false)}
>
<X className="h-4 w-4" />
</Button>
</div>
<DialogTitle className="text-sm font-medium">Fetch Album</DialogTitle>
<DialogDescription>
Do you want to fetch metadata for this album?
</DialogDescription>
{metadata.selectedAlbum && (
<div className="py-2">
<p className="font-medium bg-muted/50 rounded-md px-3 py-2">{metadata.selectedAlbum.name}</p>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => metadata.setShowAlbumDialog(false)}>
Cancel
</Button>
<Button onClick={async () => {
const albumUrl = await metadata.handleConfirmAlbumFetch();
if (albumUrl) {
setSpotifyUrl(albumUrl);
}
}}>
<Search className="h-4 w-4" />
Fetch Album
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<SearchBar
url={spotifyUrl}
loading={metadata.loading}
onUrlChange={setSpotifyUrl}
onFetch={handleFetchMetadata}
history={fetchHistory}
onHistorySelect={handleHistorySelect}
onHistoryRemove={removeFromHistory}
hasResult={!!metadata.metadata}
/>
{metadata.metadata && renderMetadata()}
</>
);
}
};
return (
<TooltipProvider>
<div className="min-h-screen bg-background flex flex-col">
<TitleBar />
<div className="flex-1 p-4 md:p-8">
<Sidebar currentPage={currentPage} onPageChange={setCurrentPage} />
{/* Main content area with sidebar offset */}
<div className="flex-1 ml-14 mt-10 p-4 md:p-8">
<div className="max-w-4xl mx-auto space-y-6">
{currentPageView === "audio-analysis" ? (
<AudioAnalysisPage onBack={() => setCurrentPageView("main")} />
) : (
<>
<Header
version={CURRENT_VERSION}
hasUpdate={hasUpdate}
onOpenAudioAnalysis={() => setCurrentPageView("audio-analysis")}
/>
{/* Download Progress Toast - Bottom Left */}
<DownloadProgressToast onClick={downloadQueue.openQueue} />
{/* Download Queue Dialog */}
<DownloadQueue
isOpen={downloadQueue.isOpen}
onClose={downloadQueue.closeQueue}
/>
{/* Timeout Dialog */}
<Dialog
open={metadata.showTimeoutDialog}
onOpenChange={metadata.setShowTimeoutDialog}
>
<DialogContent className="sm:max-w-[425px] p-6 [&>button]:hidden">
<div className="absolute right-4 top-4">
<Button
variant="ghost"
size="icon"
className="h-6 w-6 opacity-70 hover:opacity-100"
onClick={() => metadata.setShowTimeoutDialog(false)}
>
<X className="h-4 w-4" />
</Button>
</div>
<DialogTitle className="text-sm font-medium">Fetch Artist</DialogTitle>
<DialogDescription>
Set timeout for fetching metadata. Longer timeout is recommended for artists
with large discography.
</DialogDescription>
{metadata.pendingArtistName && (
<div className="py-2">
<p className="font-medium bg-muted/50 rounded-md px-3 py-2">{metadata.pendingArtistName}</p>
</div>
)}
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="timeout">Timeout (seconds)</Label>
<Input
id="timeout"
type="number"
min="10"
max="600"
value={metadata.timeoutValue}
onChange={(e) => metadata.setTimeoutValue(Number(e.target.value))}
/>
<p className="text-xs text-muted-foreground">
Default: 60 seconds. For large discographies, try 300-600 seconds (5-10
minutes).
</p>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => metadata.setShowTimeoutDialog(false)}
>
Cancel
</Button>
<Button onClick={metadata.handleConfirmFetch}>
<Search className="h-4 w-4" />
Fetch
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Album Fetch Dialog */}
<Dialog open={metadata.showAlbumDialog} onOpenChange={metadata.setShowAlbumDialog}>
<DialogContent className="sm:max-w-[425px] p-6 [&>button]:hidden">
<div className="absolute right-4 top-4">
<Button
variant="ghost"
size="icon"
className="h-6 w-6 opacity-70 hover:opacity-100"
onClick={() => metadata.setShowAlbumDialog(false)}
>
<X className="h-4 w-4" />
</Button>
</div>
<DialogTitle className="text-sm font-medium">Fetch Album</DialogTitle>
<DialogDescription>
Do you want to fetch metadata for this album?
</DialogDescription>
{metadata.selectedAlbum && (
<div className="py-2">
<p className="font-medium bg-muted/50 rounded-md px-3 py-2">{metadata.selectedAlbum.name}</p>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => metadata.setShowAlbumDialog(false)}>
Cancel
</Button>
<Button onClick={async () => {
const albumUrl = await metadata.handleConfirmAlbumFetch();
if (albumUrl) {
setSpotifyUrl(albumUrl);
}
}}>
<Search className="h-4 w-4" />
Fetch Album
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<SearchBar
url={spotifyUrl}
loading={metadata.loading}
onUrlChange={setSpotifyUrl}
onFetch={handleFetchMetadata}
history={fetchHistory}
onHistorySelect={handleHistorySelect}
onHistoryRemove={removeFromHistory}
hasResult={!!metadata.metadata}
/>
{metadata.metadata && renderMetadata()}
</>
)}
{renderPage()}
</div>
</div>
{/* Download Progress Toast - Bottom Left */}
<DownloadProgressToast onClick={downloadQueue.openQueue} />
{/* Download Queue Dialog */}
<DownloadQueue
isOpen={downloadQueue.isOpen}
onClose={downloadQueue.closeQueue}
/>
</div>
</TooltipProvider>
);