diff --git a/.gitignore b/.gitignore index dc5441f..f9393c1 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,4 @@ temp/ # Build notes (optional - uncomment if you don't want to commit) # BUILD_NOTES.md +build.txt \ No newline at end of file diff --git a/app.go b/app.go index a0343b1..d0fd541 100644 --- a/app.go +++ b/app.go @@ -159,6 +159,11 @@ func (a *App) OpenFolder(path string) error { return nil } +// SelectFolder opens a folder selection dialog and returns the selected path +func (a *App) SelectFolder(defaultPath string) (string, error) { + return backend.SelectFolderDialog(a.ctx, defaultPath) +} + // GetDefaults returns the default configuration func (a *App) GetDefaults() map[string]string { return map[string]string{ diff --git a/backend/folder.go b/backend/folder.go index 17fffe4..e6b7a68 100644 --- a/backend/folder.go +++ b/backend/folder.go @@ -1,8 +1,11 @@ package backend import ( + "context" "os/exec" "runtime" + + wailsRuntime "github.com/wailsapp/wails/v2/pkg/runtime" ) func OpenFolderInExplorer(path string) error { @@ -21,3 +24,27 @@ func OpenFolderInExplorer(path string) error { return cmd.Start() } + +func SelectFolderDialog(ctx context.Context, defaultPath string) (string, error) { + // If defaultPath is empty, use default music path + if defaultPath == "" { + defaultPath = GetDefaultMusicPath() + } + + options := wailsRuntime.OpenDialogOptions{ + Title: "Select Download Folder", + DefaultDirectory: defaultPath, + } + + selectedPath, err := wailsRuntime.OpenDirectoryDialog(ctx, options) + if err != nil { + return "", err + } + + // If user cancelled, selectedPath will be empty + if selectedPath == "" { + return "", nil + } + + return selectedPath, nil +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 730ad90..fa8fbac 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -19,7 +19,7 @@ import type { SpotifyMetadataResponse, TrackMetadata } from "@/types/api"; import { Settings } from "@/components/Settings"; import { getSettings, applyThemeMode } from "@/lib/settings"; import { applyTheme } from "@/lib/themes"; -import { Download, Search, CheckCircle, Info } from "lucide-react"; +import { Download, Search, CheckCircle, Info, XCircle, ArrowUpDown, StopCircle, FolderOpen } from "lucide-react"; import { toastWithSound as toast } from "@/lib/toast-with-sound"; import { Pagination, @@ -36,7 +36,15 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { Spinner } from "@/components/ui/spinner"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { joinPath, sanitizePath } from "./lib/utils"; +import { OpenFolder } from "../wailsjs/go/main/App"; function App() { const [spotifyUrl, setSpotifyUrl] = useState(""); @@ -57,6 +65,7 @@ function App() { const [hasUpdate, setHasUpdate] = useState(false); const [showAlbumDialog, setShowAlbumDialog] = useState(false); const [selectedAlbum, setSelectedAlbum] = useState<{ id: string; name: string; external_urls: string } | null>(null); + const [sortBy, setSortBy] = useState("default"); const shouldStopDownloadRef = useRef(false); const ITEMS_PER_PAGE = 50; @@ -104,10 +113,11 @@ function App() { }; useEffect(() => { - // Clear selection, search, downloaded tracks, and reset page when metadata changes + // Clear selection, search, downloaded tracks, sort, and reset page when metadata changes setSelectedTracks([]); setSearchQuery(""); setDownloadedTracks(new Set()); + setSortBy("default"); setCurrentPage(1); }, [metadata]); @@ -485,6 +495,21 @@ function App() { toast.info('Stopping download...'); }; + 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 renderDownloadProgress = () => { if (!isDownloading) return null; @@ -497,6 +522,7 @@ function App() { size="sm" onClick={handleStopDownload} > + Stop @@ -508,7 +534,7 @@ function App() { }; const renderTrackList = (tracks: TrackMetadata[], showCheckboxes: boolean = false, hideAlbumColumn: boolean = false) => { - const filteredTracks = tracks.filter(track => { + let filteredTracks = tracks.filter(track => { if (!searchQuery) return true; const query = searchQuery.toLowerCase(); return ( @@ -518,6 +544,33 @@ function App() { ); }); + // Apply sorting + if (sortBy === "title-asc") { + filteredTracks = [...filteredTracks].sort((a, b) => a.name.localeCompare(b.name)); + } else if (sortBy === "title-desc") { + filteredTracks = [...filteredTracks].sort((a, b) => b.name.localeCompare(a.name)); + } else if (sortBy === "artist-asc") { + filteredTracks = [...filteredTracks].sort((a, b) => a.artists.localeCompare(b.artists)); + } else if (sortBy === "artist-desc") { + filteredTracks = [...filteredTracks].sort((a, b) => b.artists.localeCompare(a.artists)); + } else if (sortBy === "duration-asc") { + filteredTracks = [...filteredTracks].sort((a, b) => a.duration_ms - b.duration_ms); + } else if (sortBy === "duration-desc") { + filteredTracks = [...filteredTracks].sort((a, b) => b.duration_ms - a.duration_ms); + } else if (sortBy === "downloaded") { + filteredTracks = [...filteredTracks].sort((a, b) => { + const aDownloaded = downloadedTracks.has(a.isrc); + const bDownloaded = downloadedTracks.has(b.isrc); + return (bDownloaded ? 1 : 0) - (aDownloaded ? 1 : 0); + }); + } else if (sortBy === "not-downloaded") { + filteredTracks = [...filteredTracks].sort((a, b) => { + const aDownloaded = downloadedTracks.has(a.isrc); + const bDownloaded = downloadedTracks.has(b.isrc); + return (aDownloaded ? 1 : 0) - (bDownloaded ? 1 : 0); + }); + } + // Pagination const totalPages = Math.ceil(filteredTracks.length / ITEMS_PER_PAGE); const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; @@ -676,7 +729,7 @@ function App() { const { track } = metadata; return ( - +
{track.images && ( - +
{album_info.images && ( )} + {downloadedTracks.size > 0 && ( + + )}
{renderDownloadProgress()}
@@ -773,14 +832,33 @@ function App() {
-
- - handleSearchChange(e.target.value)} - className="pl-10" - /> +
+
+ + handleSearchChange(e.target.value)} + className="pl-10" + /> +
+
{renderTrackList(track_list, true, true)}
@@ -793,7 +871,7 @@ function App() { return (
- +
{playlist_info.owner.images && ( )} + {downloadedTracks.size > 0 && ( + + )}
{renderDownloadProgress()}
@@ -840,14 +924,33 @@ function App() {
-
- - handleSearchChange(e.target.value)} - className="pl-10" - /> +
+
+ + handleSearchChange(e.target.value)} + className="pl-10" + /> +
+
{renderTrackList(track_list, true)}
@@ -860,7 +963,7 @@ function App() { return (
- +
{artist_info.images && ( )} + {downloadedTracks.size > 0 && ( + + )}
{renderDownloadProgress()} -
- - handleSearchChange(e.target.value)} - className="pl-10" - /> +
+
+ + handleSearchChange(e.target.value)} + className="pl-10" + /> +
+
{renderTrackList(track_list, true)}
@@ -996,8 +1124,8 @@ function App() {
- SpotiFLAC -

SpotiFLAC

+ SpotiFLAC window.location.reload()} /> +

window.location.reload()}>SpotiFLAC

- +
@@ -1131,13 +1259,25 @@ function App() {
- setSpotifyUrl(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleFetchMetadata()} - /> +
+ setSpotifyUrl(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleFetchMetadata()} + className="pr-8" + /> + {spotifyUrl && ( + + )} +
diff --git a/frontend/src/lib/settings.ts b/frontend/src/lib/settings.ts index dffb8be..c0d2db7 100644 --- a/frontend/src/lib/settings.ts +++ b/frontend/src/lib/settings.ts @@ -24,17 +24,14 @@ export const DEFAULT_SETTINGS: Settings = { operatingSystem: "Windows" }; -// TODO: add mac/linux defaults -const DEFAULT_PATH : string = "C:\\Users\\Public\\Music"; - async function fetchDefaultPath(): Promise { try { const data = await GetDefaults(); - return data.downloadPath || DEFAULT_PATH; + return data.downloadPath || ""; } catch (error) { console.error("Failed to fetch default path:", error); + return ""; } - return DEFAULT_PATH; } const SETTINGS_KEY = "spotiflac-settings";