diff --git a/app.go b/app.go index c6f8fdd..e3b355c 100644 --- a/app.go +++ b/app.go @@ -684,6 +684,11 @@ func (a *App) SelectAudioFiles() ([]string, error) { return files, nil } +// GetFileSizes returns file sizes for a list of file paths +func (a *App) GetFileSizes(files []string) map[string]int64 { + return backend.GetFileSizes(files) +} + // ListDirectoryFiles lists files and folders in a directory func (a *App) ListDirectoryFiles(dirPath string) ([]backend.FileInfo, error) { if dirPath == "" { diff --git a/backend/analysis.go b/backend/analysis.go index f551ae5..0ca4214 100644 --- a/backend/analysis.go +++ b/backend/analysis.go @@ -12,6 +12,7 @@ import ( // AnalysisResult contains the audio analysis data type AnalysisResult struct { FilePath string `json:"file_path"` + FileSize int64 `json:"file_size"` SampleRate uint32 `json:"sample_rate"` Channels uint8 `json:"channels"` BitsPerSample uint8 `json:"bits_per_sample"` @@ -30,6 +31,12 @@ func AnalyzeTrack(filepath string) (*AnalysisResult, error) { return nil, fmt.Errorf("file does not exist: %s", filepath) } + // Get file size + fileInfo, err := os.Stat(filepath) + if err != nil { + return nil, fmt.Errorf("failed to get file info: %w", err) + } + // Parse FLAC file f, err := flac.ParseFile(filepath) if err != nil { @@ -38,6 +45,7 @@ func AnalyzeTrack(filepath string) (*AnalysisResult, error) { result := &AnalysisResult{ FilePath: filepath, + FileSize: fileInfo.Size(), } // Extract basic audio properties from STREAMINFO block diff --git a/backend/filemanager.go b/backend/filemanager.go index 8cb6242..be1a754 100644 --- a/backend/filemanager.go +++ b/backend/filemanager.go @@ -344,6 +344,18 @@ func PreviewRename(files []string, format string) []RenamePreview { return previews } +// GetFileSizes returns file sizes for a list of file paths +func GetFileSizes(files []string) map[string]int64 { + result := make(map[string]int64) + for _, filePath := range files { + info, err := os.Stat(filePath) + if err == nil { + result[filePath] = info.Size() + } + } + return result +} + // RenameFiles renames files based on their metadata func RenameFiles(files []string, format string) []RenameResult { var results []RenameResult diff --git a/frontend/src/components/AudioAnalysis.tsx b/frontend/src/components/AudioAnalysis.tsx index 18b87e4..589bd35 100644 --- a/frontend/src/components/AudioAnalysis.tsx +++ b/frontend/src/components/AudioAnalysis.tsx @@ -8,7 +8,8 @@ import { TrendingUp, FileAudio, Clock, - Gauge + Gauge, + HardDrive } from "lucide-react"; import type { AnalysisResult } from "@/types/api"; @@ -78,6 +79,14 @@ export function AudioAnalysis({ return num.toFixed(2); }; + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; + }; + // Calculate Nyquist frequency (half of sample rate) const nyquistFreq = result.sample_rate / 2; @@ -117,6 +126,13 @@ export function AudioAnalysis({ Nyquist: {(nyquistFreq / 1000).toFixed(1)} kHz + {result.file_size > 0 && ( +
+ + Size: + {formatFileSize(result.file_size)} +
+ )} {/* Dynamic Range - Single line */} diff --git a/frontend/src/components/AudioConverterPage.tsx b/frontend/src/components/AudioConverterPage.tsx index 8353752..b42020e 100644 --- a/frontend/src/components/AudioConverterPage.tsx +++ b/frontend/src/components/AudioConverterPage.tsx @@ -30,11 +30,20 @@ interface AudioFile { path: string; name: string; format: string; + size: number; status: "pending" | "converting" | "success" | "error"; error?: string; outputPath?: string; } +function formatFileSize(bytes: number): string { + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; +} + const BITRATE_OPTIONS = [ { value: "320k", label: "320k" }, { value: "256k", label: "256k" }, @@ -141,10 +150,19 @@ export function AudioConverterPage() { if (allMP3 && outputFormat !== "m4a") { setOutputFormat("m4a"); } - }, [files, outputFormat]); + + // Reset to AAC if no FLAC files (ALAC doesn't make sense for lossy input) + const hasFlac = files.some((f) => f.format === "flac"); + if (!hasFlac && m4aCodec === "alac") { + setM4aCodec("aac"); + } + }, [files, outputFormat, m4aCodec]); // Check if format selection should be disabled (all files are MP3) const isFormatDisabled = files.length > 0 && files.every((f) => f.format === "mp3"); + + // Check if any file is FLAC (ALAC only makes sense for lossless input) + const hasFlacFiles = files.some((f) => f.format === "flac"); // Detect fullscreen/maximized window useEffect(() => { @@ -268,7 +286,7 @@ export function AudioConverterPage() { } }; - const addFiles = useCallback((paths: string[]) => { + const addFiles = useCallback(async (paths: string[]) => { const validExtensions = [".mp3", ".flac"]; // Check for M4A files specifically @@ -283,12 +301,19 @@ export function AudioConverterPage() { }); } + // Get file sizes from backend + const GetFileSizes = (files: string[]): Promise> => + (window as any)["go"]["main"]["App"]["GetFileSizes"](files); + + const validPaths = paths.filter((path) => { + const ext = path.toLowerCase().slice(path.lastIndexOf(".")); + return validExtensions.includes(ext); + }); + + const fileSizes = validPaths.length > 0 ? await GetFileSizes(validPaths) : {}; + setFiles((prev) => { - const newFiles: AudioFile[] = paths - .filter((path) => { - const ext = path.toLowerCase().slice(path.lastIndexOf(".")); - return validExtensions.includes(ext); - }) + const newFiles: AudioFile[] = validPaths .filter((path) => !prev.some((f) => f.path === path)) .map((path) => { const name = path.split(/[/\\]/).pop() || path; @@ -297,6 +322,7 @@ export function AudioConverterPage() { path, name, format: ext, + size: fileSizes[path] || 0, status: "pending" as const, }; }); @@ -598,8 +624,8 @@ export function AudioConverterPage() { - {/* Codec selection for M4A */} - {outputFormat === "m4a" && ( + {/* Codec selection for M4A - only show ALAC option when input has FLAC files */} + {outputFormat === "m4a" && hasFlacFiles && (
)}
+ + {formatFileSize(file.size)} + {file.format} diff --git a/frontend/src/components/FileManagerPage.tsx b/frontend/src/components/FileManagerPage.tsx index 8383bc8..892e96e 100644 --- a/frontend/src/components/FileManagerPage.tsx +++ b/frontend/src/components/FileManagerPage.tsx @@ -76,7 +76,11 @@ const FORMAT_PRESETS: Record = { "track-artist-title": { label: "Track. Artist - Title", template: "{track}. {artist} - {title}" }, "title-album-artist": { label: "Title - Album Artist", template: "{title} - {album_artist}" }, "track-title-album-artist": { label: "Track. Title - Album Artist", template: "{track}. {title} - {album_artist}" }, - "custom": { label: "Custom...", template: "" }, + "artist-album-title": { label: "Artist - Album - Title", template: "{artist} - {album} - {title}" }, + "track-dash-title": { label: "Track - Title", template: "{track} - {title}" }, + "disc-track-title": { label: "Disc-Track. Title", template: "{disc}-{track}. {title}" }, + "disc-track-title-artist": { label: "Disc-Track. Title - Artist", template: "{disc}-{track}. {title} - {artist}" }, + "custom": { label: "Custom...", template: "{title} - {artist}" }, }; const STORAGE_KEY = "spotiflac_file_manager_state"; @@ -128,7 +132,7 @@ export function FileManagerPage() { return DEFAULT_CUSTOM_FORMAT; }); - const renameFormat = formatPreset === "custom" ? customFormat : FORMAT_PRESETS[formatPreset].template; + const renameFormat = formatPreset === "custom" ? (customFormat || FORMAT_PRESETS["custom"].template) : FORMAT_PRESETS[formatPreset].template; const [showPreview, setShowPreview] = useState(false); const [previewData, setPreviewData] = useState([]); const [renaming, setRenaming] = useState(false); diff --git a/frontend/src/components/SettingsPage.tsx b/frontend/src/components/SettingsPage.tsx index 6ff4c39..9e4e2cd 100644 --- a/frontend/src/components/SettingsPage.tsx +++ b/frontend/src/components/SettingsPage.tsx @@ -325,7 +325,7 @@ export function SettingsPage() { setTempSettings(prev => ({ ...prev, folderPreset: value, - folderTemplate: value === "custom" ? prev.folderTemplate : preset.template + folderTemplate: value === "custom" ? (prev.folderTemplate || preset.template) : preset.template })); }} > @@ -377,7 +377,7 @@ export function SettingsPage() { setTempSettings(prev => ({ ...prev, filenamePreset: value, - filenameTemplate: value === "custom" ? prev.filenameTemplate : preset.template + filenameTemplate: value === "custom" ? (prev.filenameTemplate || preset.template) : preset.template })); }} > diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx index 5ec5d52..0863e40 100644 --- a/frontend/src/components/ui/alert-dialog.tsx +++ b/frontend/src/components/ui/alert-dialog.tsx @@ -6,125 +6,141 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" -const AlertDialog = AlertDialogPrimitive.Root +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} -const AlertDialogTrigger = AlertDialogPrimitive.Trigger +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} -const AlertDialogPortal = AlertDialogPrimitive.Portal +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} -const AlertDialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName - -const AlertDialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - ) { + return ( + - -)) -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + ) +} -const AlertDialogHeader = ({ +function AlertDialogContent({ className, ...props -}: React.HTMLAttributes) => ( -
-) -AlertDialogHeader.displayName = "AlertDialogHeader" +}: React.ComponentProps) { + return ( + + + + + ) +} -const AlertDialogFooter = ({ +function AlertDialogHeader({ className, ...props -}: React.HTMLAttributes) => ( -
-) -AlertDialogFooter.displayName = "AlertDialogFooter" +}: React.ComponentProps<"div">) { + return ( +
+ ) +} -const AlertDialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} -const AlertDialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogDescription.displayName = - AlertDialogPrimitive.Description.displayName +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} -const AlertDialogAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} -const AlertDialogCancel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} export { AlertDialog, diff --git a/frontend/src/lib/settings.ts b/frontend/src/lib/settings.ts index 529cf4b..e858778 100644 --- a/frontend/src/lib/settings.ts +++ b/frontend/src/lib/settings.ts @@ -3,10 +3,10 @@ import { GetDefaults } from "../../wailsjs/go/main/App"; export type FontFamily = "google-sans" | "inter" | "poppins" | "roboto" | "dm-sans" | "plus-jakarta-sans" | "manrope" | "space-grotesk" | "noto-sans" | "nunito-sans" | "figtree" | "raleway" | "public-sans" | "outfit" | "jetbrains-mono" | "geist-sans"; // Folder structure presets -export type FolderPreset = "none" | "artist" | "album" | "artist-album" | "artist-year-album" | "album-artist" | "album-artist-album" | "album-artist-year-album" | "custom"; +export type FolderPreset = "none" | "artist" | "album" | "year-album" | "year-artist-album" | "artist-album" | "artist-year-album" | "artist-year-nested-album" | "album-artist" | "album-artist-album" | "album-artist-year-album" | "album-artist-year-nested-album" | "year" | "year-artist" | "custom"; // Filename format presets -export type FilenamePreset = "title" | "title-artist" | "artist-title" | "track-title" | "track-title-artist" | "track-artist-title" | "title-album-artist" | "track-title-album-artist" | "custom"; +export type FilenamePreset = "title" | "title-artist" | "artist-title" | "track-title" | "track-title-artist" | "track-artist-title" | "title-album-artist" | "track-title-album-artist" | "artist-album-title" | "track-dash-title" | "disc-track-title" | "disc-track-title-artist" | "custom"; export interface Settings { downloadPath: string; @@ -38,12 +38,18 @@ export const FOLDER_PRESETS: Record