diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 680b5df..0a6a418 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -e92e100705a0bb90f6783cd4074df1a7 \ No newline at end of file +23a60910537eca7800052fa01bf45b7a \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2c8cf0c..f47e1dd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -26,6 +26,7 @@ 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 type { HistoryItem } from "@/components/FetchHistory"; // Hooks @@ -38,7 +39,10 @@ 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("main"); const [spotifyUrl, setSpotifyUrl] = useState(""); const [selectedTracks, setSelectedTracks] = useState([]); const [searchQuery, setSearchQuery] = useState(""); @@ -48,7 +52,7 @@ function App() { const [fetchHistory, setFetchHistory] = useState([]); const ITEMS_PER_PAGE = 50; - const CURRENT_VERSION = "6.4"; + const CURRENT_VERSION = "6.5"; const download = useDownload(); const metadata = useMetadata(); @@ -258,6 +262,7 @@ function App() { downloadingTrack={download.downloadingTrack} isDownloaded={download.downloadedTracks.has(track.isrc)} isFailed={download.failedTracks.has(track.isrc)} + isSkipped={download.skippedTracks.has(track.isrc)} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} downloadedLyrics={lyrics.downloadedLyrics.has(track.spotify_id || "")} failedLyrics={lyrics.failedLyrics.has(track.spotify_id || "")} @@ -459,10 +464,15 @@ function App() {
-
+ {currentPageView === "audio-analysis" ? ( + setCurrentPageView("main")} /> + ) : ( + <> +
setCurrentPageView("audio-analysis")} + /> {/* Download Progress Toast - Bottom Left */} @@ -581,7 +591,9 @@ function App() { hasResult={!!metadata.metadata} /> - {metadata.metadata && renderMetadata()} + {metadata.metadata && renderMetadata()} + + )}
diff --git a/frontend/src/components/AudioAnalysisDialog.tsx b/frontend/src/components/AudioAnalysisDialog.tsx deleted file mode 100644 index c94795e..0000000 --- a/frontend/src/components/AudioAnalysisDialog.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { useState, useCallback } from "react"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { Activity, Upload, X } from "lucide-react"; -import { AudioAnalysis } from "@/components/AudioAnalysis"; -import { SpectrumVisualization } from "@/components/SpectrumVisualization"; -import { useAudioAnalysis } from "@/hooks/useAudioAnalysis"; -import { SelectFile } from "../../wailsjs/go/main/App"; -import { toastWithSound as toast } from "@/lib/toast-with-sound"; -import { OnFileDrop, OnFileDropOff } from "../../wailsjs/runtime/runtime"; -import { useEffect } from "react"; - -export function AudioAnalysisDialog() { - const [open, setOpen] = useState(false); - const { analyzing, result, analyzeFile, clearResult } = useAudioAnalysis(); - const [selectedFilePath, setSelectedFilePath] = useState(""); - const [isDragging, setIsDragging] = useState(false); - - const handleSelectFile = async () => { - try { - const filePath = await SelectFile(); - if (filePath) { - setSelectedFilePath(filePath); - await analyzeFile(filePath); - } - } catch (err) { - toast.error("File Selection Failed", { - description: err instanceof Error ? err.message : "Failed to select file", - }); - } - }; - - const handleFileDrop = useCallback(async (_x: number, _y: number, paths: string[]) => { - setIsDragging(false); - - if (paths.length === 0) return; - - const filePath = paths[0]; - - // Check if it's a FLAC file - if (!filePath.toLowerCase().endsWith('.flac')) { - toast.error("Invalid File Type", { - description: "Please drop a FLAC file for analysis", - }); - return; - } - - setSelectedFilePath(filePath); - await analyzeFile(filePath); - }, [analyzeFile]); - - // Register drag and drop handlers when dialog is open - useEffect(() => { - if (open) { - OnFileDrop((x, y, paths) => { - handleFileDrop(x, y, paths); - }, true); - - return () => { - OnFileDropOff(); - }; - } - }, [open, handleFileDrop]); - - const handleClose = () => { - setOpen(false); - setTimeout(() => { - clearResult(); - setSelectedFilePath(""); - }, 200); - }; - - return ( - { - if (!isOpen) { - handleClose(); - } else { - setOpen(true); - } - }}> - - - - - - - -

Audio Quality Analyzer

-
-
- -
- -
- - Audio Quality Analyzer - -
- {/* File Selection */} - {!result && !analyzing && ( -
{ - e.preventDefault(); - setIsDragging(true); - }} - onDragLeave={(e) => { - e.preventDefault(); - setIsDragging(false); - }} - onDrop={(e) => { - e.preventDefault(); - setIsDragging(false); - }} - style={{ "--wails-drop-target": "drop" } as React.CSSProperties} - > - -

Analyze FLAC Audio Quality

-

- {isDragging - ? "Drop your FLAC file here" - : "Drag and drop a FLAC file here, or click the button below to select"} -

- -
- )} - - {/* Analysis Results */} - {result && ( -
- {/* File Info */} -
-

Analyzing file:

-

{selectedFilePath}

-
- - {/* Spectrum Visualization */} - - - {/* Detailed Analysis */} - - - {/* Actions */} -
- -
-
- )} - - {/* Loading State */} - {analyzing && !result && ( -
-
-

Analyzing audio file...

-
- )} -
-
-
- ); -} diff --git a/frontend/src/components/AudioAnalysisPage.tsx b/frontend/src/components/AudioAnalysisPage.tsx new file mode 100644 index 0000000..264d954 --- /dev/null +++ b/frontend/src/components/AudioAnalysisPage.tsx @@ -0,0 +1,162 @@ +import { useState, useCallback, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Activity, Upload, ArrowLeft } from "lucide-react"; +import { AudioAnalysis } from "@/components/AudioAnalysis"; +import { SpectrumVisualization } from "@/components/SpectrumVisualization"; +import { useAudioAnalysis } from "@/hooks/useAudioAnalysis"; +import { SelectFile } from "../../wailsjs/go/main/App"; +import { toastWithSound as toast } from "@/lib/toast-with-sound"; +import { OnFileDrop, OnFileDropOff } from "../../wailsjs/runtime/runtime"; + +interface AudioAnalysisPageProps { + onBack: () => void; +} + +export function AudioAnalysisPage({ onBack }: AudioAnalysisPageProps) { + const { analyzing, result, analyzeFile, clearResult } = useAudioAnalysis(); + const [selectedFilePath, setSelectedFilePath] = useState(""); + const [isDragging, setIsDragging] = useState(false); + + const handleSelectFile = async () => { + try { + const filePath = await SelectFile(); + if (filePath) { + setSelectedFilePath(filePath); + await analyzeFile(filePath); + } + } catch (err) { + toast.error("File Selection Failed", { + description: err instanceof Error ? err.message : "Failed to select file", + }); + } + }; + + const handleFileDrop = useCallback( + async (_x: number, _y: number, paths: string[]) => { + setIsDragging(false); + + if (paths.length === 0) return; + + const filePath = paths[0]; + + if (!filePath.toLowerCase().endsWith(".flac")) { + toast.error("Invalid File Type", { + description: "Please drop a FLAC file for analysis", + }); + return; + } + + setSelectedFilePath(filePath); + await analyzeFile(filePath); + }, + [analyzeFile] + ); + + useEffect(() => { + OnFileDrop((x, y, paths) => { + handleFileDrop(x, y, paths); + }, true); + + return () => { + OnFileDropOff(); + }; + }, [handleFileDrop]); + + const handleAnalyzeAnother = () => { + clearResult(); + setSelectedFilePath(""); + }; + + return ( +
+ {/* Header */} +
+ +
+

Audio Quality Analyzer

+

+ Analyze FLAC files to verify true lossless quality +

+
+
+ + {/* File Selection */} + {!result && !analyzing && ( +
{ + e.preventDefault(); + setIsDragging(true); + }} + onDragLeave={(e) => { + e.preventDefault(); + setIsDragging(false); + }} + onDrop={(e) => { + e.preventDefault(); + setIsDragging(false); + }} + style={{ "--wails-drop-target": "drop" } as React.CSSProperties} + > + +

Analyze FLAC Audio Quality

+

+ {isDragging + ? "Drop your FLAC file here" + : "Drag and drop a FLAC file here, or click the button below to select"} +

+ +
+ )} + + {/* Loading State */} + {analyzing && !result && ( +
+
+

Analyzing audio file...

+
+ )} + + {/* Analysis Results */} + {result && ( +
+ {/* File Info */} +
+

Analyzing file:

+

{selectedFilePath}

+
+ + {/* Spectrum Visualization */} + + + {/* Detailed Analysis */} + + + {/* Actions */} +
+ +
+
+ )} +
+ ); +} diff --git a/frontend/src/components/DownloadQueue.tsx b/frontend/src/components/DownloadQueue.tsx index 445fe5b..67229ab 100644 --- a/frontend/src/components/DownloadQueue.tsx +++ b/frontend/src/components/DownloadQueue.tsx @@ -7,7 +7,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { Progress } from "@/components/ui/progress"; + import { Badge } from "@/components/ui/badge"; import { GetDownloadQueue, ClearCompletedDownloads } from "../../wailsjs/go/main/App"; import { backend } from "../../wailsjs/go/models"; @@ -227,26 +227,23 @@ export function DownloadQueue({ isOpen, onClose }: DownloadQueueProps) { {getStatusBadge(item.status)} - {/* Progress bar for downloading items */} + {/* Info for downloading items */} {item.status === "downloading" && ( -
-
- - {item.progress > 0 - ? `${item.progress.toFixed(2)} MB` - : queueInfo.is_downloading && queueInfo.current_speed > 0 - ? "Downloading..." - : "Starting..."} - - - {item.speed > 0 - ? `${item.speed.toFixed(2)} MB/s` - : queueInfo.current_speed > 0 - ? `${queueInfo.current_speed.toFixed(2)} MB/s` - : "—"} - -
- +
+ + {item.progress > 0 + ? `${item.progress.toFixed(2)} MB` + : queueInfo.is_downloading && queueInfo.current_speed > 0 + ? "Downloading..." + : "Starting..."} + + + {item.speed > 0 + ? `${item.speed.toFixed(2)} MB/s` + : queueInfo.current_speed > 0 + ? `${queueInfo.current_speed.toFixed(2)} MB/s` + : "—"} +
)} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 8207962..1995a94 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -1,19 +1,21 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Settings } from "@/components/Settings"; -import { AudioAnalysisDialog } from "@/components/AudioAnalysisDialog"; +import { Activity } from "lucide-react"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import { openExternal } from "@/lib/utils"; interface HeaderProps { version: string; hasUpdate: boolean; + onOpenAudioAnalysis: () => void; } -export function Header({ version, hasUpdate }: HeaderProps) { +export function Header({ version, hasUpdate, onOpenAudioAnalysis }: HeaderProps) { return (
@@ -32,14 +34,13 @@ export function Header({ version, hasUpdate }: HeaderProps) {
- openExternal("https://github.com/afkarxyz/SpotiFLAC/releases")} className="cursor-pointer hover:opacity-80 transition-opacity" > v{version} - + {hasUpdate && ( @@ -56,24 +57,31 @@ export function Header({ version, hasUpdate }: HeaderProps) {
-

Report bug or request feature

- + + + + + +

Audio Quality Analyzer

+
+
diff --git a/frontend/src/components/TrackInfo.tsx b/frontend/src/components/TrackInfo.tsx index a7f3de5..ea89390 100644 --- a/frontend/src/components/TrackInfo.tsx +++ b/frontend/src/components/TrackInfo.tsx @@ -1,6 +1,6 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; -import { Download, FolderOpen, CheckCircle, XCircle, FileText, SkipForward, Globe } from "lucide-react"; +import { Download, FolderOpen, CheckCircle, XCircle, FileText, FileCheck, Globe } from "lucide-react"; import { Spinner } from "@/components/ui/spinner"; import { Tooltip, @@ -16,6 +16,7 @@ interface TrackInfoProps { downloadingTrack: string | null; isDownloaded: boolean; isFailed: boolean; + isSkipped: boolean; downloadingLyricsTrack?: string | null; downloadedLyrics?: boolean; failedLyrics?: boolean; @@ -34,6 +35,7 @@ export function TrackInfo({ downloadingTrack, isDownloaded, isFailed, + isSkipped, downloadingLyricsTrack, downloadedLyrics, failedLyrics, @@ -57,62 +59,18 @@ export function TrackInfo({ className="w-48 h-48 rounded-md shadow-lg object-cover" /> )} - {/* Availability Icons - below cover art */} - {availability && ( -
- - -
- -
-
- -

Tidal

-
-
- - -
- -
-
- -

Deezer

-
-
- - -
- -
-
- -

Amazon Music

-
-
- - -
- -
-
- -

Qobuz

-
-
-
- )}

{track.name}

- {isDownloaded && ( + {isSkipped ? ( + + ) : isDownloaded ? ( - )} - {isFailed && ( + ) : isFailed ? ( - )} + ) : null}

{track.artists}

@@ -151,13 +109,24 @@ export function TrackInfo({ > {checkingAvailability ? ( + ) : availability ? ( + ) : ( )} -

Check Availability

+ {availability ? ( +
+ + + + +
+ ) : ( +

Check Availability

+ )}
)} @@ -172,7 +141,7 @@ export function TrackInfo({ {downloadingLyricsTrack === track.spotify_id ? ( ) : skippedLyrics ? ( - + ) : downloadedLyrics ? ( ) : failedLyrics ? ( diff --git a/frontend/src/components/TrackList.tsx b/frontend/src/components/TrackList.tsx index f6c8538..dab6b63 100644 --- a/frontend/src/components/TrackList.tsx +++ b/frontend/src/components/TrackList.tsx @@ -1,6 +1,6 @@ import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; -import { Download, CheckCircle, XCircle, SkipForward, FileText, Globe } from "lucide-react"; +import { Download, CheckCircle, XCircle, FileCheck, FileText, Globe } from "lucide-react"; import { Spinner } from "@/components/ui/spinner"; import { Tooltip, @@ -210,7 +210,7 @@ export function TrackList({ {track.name} )} {skippedTracks.has(track.isrc) ? ( - + ) : downloadedTracks.has(track.isrc) ? ( ) : failedTracks.has(track.isrc) ? ( @@ -350,7 +350,7 @@ export function TrackList({ {downloadingLyricsTrack === track.spotify_id ? ( ) : skippedLyrics?.has(track.spotify_id) ? ( - + ) : downloadedLyrics?.has(track.spotify_id) ? ( ) : failedLyrics?.has(track.spotify_id) ? ( diff --git a/frontend/src/components/ui/input-with-context.tsx b/frontend/src/components/ui/input-with-context.tsx index 37c3ca4..d0246b7 100644 --- a/frontend/src/components/ui/input-with-context.tsx +++ b/frontend/src/components/ui/input-with-context.tsx @@ -32,7 +32,7 @@ const InputWithContext = React.forwardRef { try { const text = await navigator.clipboard.readText(); @@ -42,10 +42,6 @@ const InputWithContext = React.forwardRef { - checkClipboard(); - }, []); - const handleCut = async () => { const input = inputRef.current; if (!input) return; @@ -156,7 +152,13 @@ const InputWithContext = React.forwardRef + { + if (open) { + checkClipboard(); + } + }} + >