v6.9
This commit is contained in:
@@ -684,6 +684,11 @@ func (a *App) SelectAudioFiles() ([]string, error) {
|
|||||||
return files, nil
|
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
|
// ListDirectoryFiles lists files and folders in a directory
|
||||||
func (a *App) ListDirectoryFiles(dirPath string) ([]backend.FileInfo, error) {
|
func (a *App) ListDirectoryFiles(dirPath string) ([]backend.FileInfo, error) {
|
||||||
if dirPath == "" {
|
if dirPath == "" {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
// AnalysisResult contains the audio analysis data
|
// AnalysisResult contains the audio analysis data
|
||||||
type AnalysisResult struct {
|
type AnalysisResult struct {
|
||||||
FilePath string `json:"file_path"`
|
FilePath string `json:"file_path"`
|
||||||
|
FileSize int64 `json:"file_size"`
|
||||||
SampleRate uint32 `json:"sample_rate"`
|
SampleRate uint32 `json:"sample_rate"`
|
||||||
Channels uint8 `json:"channels"`
|
Channels uint8 `json:"channels"`
|
||||||
BitsPerSample uint8 `json:"bits_per_sample"`
|
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)
|
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
|
// Parse FLAC file
|
||||||
f, err := flac.ParseFile(filepath)
|
f, err := flac.ParseFile(filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -38,6 +45,7 @@ func AnalyzeTrack(filepath string) (*AnalysisResult, error) {
|
|||||||
|
|
||||||
result := &AnalysisResult{
|
result := &AnalysisResult{
|
||||||
FilePath: filepath,
|
FilePath: filepath,
|
||||||
|
FileSize: fileInfo.Size(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract basic audio properties from STREAMINFO block
|
// Extract basic audio properties from STREAMINFO block
|
||||||
|
|||||||
@@ -344,6 +344,18 @@ func PreviewRename(files []string, format string) []RenamePreview {
|
|||||||
return previews
|
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
|
// RenameFiles renames files based on their metadata
|
||||||
func RenameFiles(files []string, format string) []RenameResult {
|
func RenameFiles(files []string, format string) []RenameResult {
|
||||||
var results []RenameResult
|
var results []RenameResult
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
TrendingUp,
|
TrendingUp,
|
||||||
FileAudio,
|
FileAudio,
|
||||||
Clock,
|
Clock,
|
||||||
Gauge
|
Gauge,
|
||||||
|
HardDrive
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { AnalysisResult } from "@/types/api";
|
import type { AnalysisResult } from "@/types/api";
|
||||||
|
|
||||||
@@ -78,6 +79,14 @@ export function AudioAnalysis({
|
|||||||
return num.toFixed(2);
|
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)
|
// Calculate Nyquist frequency (half of sample rate)
|
||||||
const nyquistFreq = result.sample_rate / 2;
|
const nyquistFreq = result.sample_rate / 2;
|
||||||
|
|
||||||
@@ -117,6 +126,13 @@ export function AudioAnalysis({
|
|||||||
<span className="text-muted-foreground">Nyquist:</span>
|
<span className="text-muted-foreground">Nyquist:</span>
|
||||||
<span className="font-semibold">{(nyquistFreq / 1000).toFixed(1)} kHz</span>
|
<span className="font-semibold">{(nyquistFreq / 1000).toFixed(1)} kHz</span>
|
||||||
</div>
|
</div>
|
||||||
|
{result.file_size > 0 && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<HardDrive className="h-3 w-3 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">Size:</span>
|
||||||
|
<span className="font-semibold">{formatFileSize(result.file_size)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dynamic Range - Single line */}
|
{/* Dynamic Range - Single line */}
|
||||||
|
|||||||
@@ -30,11 +30,20 @@ interface AudioFile {
|
|||||||
path: string;
|
path: string;
|
||||||
name: string;
|
name: string;
|
||||||
format: string;
|
format: string;
|
||||||
|
size: number;
|
||||||
status: "pending" | "converting" | "success" | "error";
|
status: "pending" | "converting" | "success" | "error";
|
||||||
error?: string;
|
error?: string;
|
||||||
outputPath?: 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 = [
|
const BITRATE_OPTIONS = [
|
||||||
{ value: "320k", label: "320k" },
|
{ value: "320k", label: "320k" },
|
||||||
{ value: "256k", label: "256k" },
|
{ value: "256k", label: "256k" },
|
||||||
@@ -141,11 +150,20 @@ export function AudioConverterPage() {
|
|||||||
if (allMP3 && outputFormat !== "m4a") {
|
if (allMP3 && outputFormat !== "m4a") {
|
||||||
setOutputFormat("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)
|
// Check if format selection should be disabled (all files are MP3)
|
||||||
const isFormatDisabled = files.length > 0 && files.every((f) => f.format === "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
|
// Detect fullscreen/maximized window
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkFullscreen = () => {
|
const checkFullscreen = () => {
|
||||||
@@ -268,7 +286,7 @@ export function AudioConverterPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addFiles = useCallback((paths: string[]) => {
|
const addFiles = useCallback(async (paths: string[]) => {
|
||||||
const validExtensions = [".mp3", ".flac"];
|
const validExtensions = [".mp3", ".flac"];
|
||||||
|
|
||||||
// Check for M4A files specifically
|
// Check for M4A files specifically
|
||||||
@@ -283,12 +301,19 @@ export function AudioConverterPage() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setFiles((prev) => {
|
// Get file sizes from backend
|
||||||
const newFiles: AudioFile[] = paths
|
const GetFileSizes = (files: string[]): Promise<Record<string, number>> =>
|
||||||
.filter((path) => {
|
(window as any)["go"]["main"]["App"]["GetFileSizes"](files);
|
||||||
|
|
||||||
|
const validPaths = paths.filter((path) => {
|
||||||
const ext = path.toLowerCase().slice(path.lastIndexOf("."));
|
const ext = path.toLowerCase().slice(path.lastIndexOf("."));
|
||||||
return validExtensions.includes(ext);
|
return validExtensions.includes(ext);
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const fileSizes = validPaths.length > 0 ? await GetFileSizes(validPaths) : {};
|
||||||
|
|
||||||
|
setFiles((prev) => {
|
||||||
|
const newFiles: AudioFile[] = validPaths
|
||||||
.filter((path) => !prev.some((f) => f.path === path))
|
.filter((path) => !prev.some((f) => f.path === path))
|
||||||
.map((path) => {
|
.map((path) => {
|
||||||
const name = path.split(/[/\\]/).pop() || path;
|
const name = path.split(/[/\\]/).pop() || path;
|
||||||
@@ -297,6 +322,7 @@ export function AudioConverterPage() {
|
|||||||
path,
|
path,
|
||||||
name,
|
name,
|
||||||
format: ext,
|
format: ext,
|
||||||
|
size: fileSizes[path] || 0,
|
||||||
status: "pending" as const,
|
status: "pending" as const,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -598,8 +624,8 @@ export function AudioConverterPage() {
|
|||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
</div>
|
</div>
|
||||||
{/* Codec selection for M4A */}
|
{/* Codec selection for M4A - only show ALAC option when input has FLAC files */}
|
||||||
{outputFormat === "m4a" && (
|
{outputFormat === "m4a" && hasFlacFiles && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label className="whitespace-nowrap">Codec:</Label>
|
<Label className="whitespace-nowrap">Codec:</Label>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
@@ -672,6 +698,9 @@ export function AudioConverterPage() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{formatFileSize(file.size)}
|
||||||
|
</span>
|
||||||
<span className="text-xs uppercase text-muted-foreground">
|
<span className="text-xs uppercase text-muted-foreground">
|
||||||
{file.format}
|
{file.format}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -76,7 +76,11 @@ const FORMAT_PRESETS: Record<string, { label: string; template: string }> = {
|
|||||||
"track-artist-title": { label: "Track. Artist - Title", template: "{track}. {artist} - {title}" },
|
"track-artist-title": { label: "Track. Artist - Title", template: "{track}. {artist} - {title}" },
|
||||||
"title-album-artist": { label: "Title - Album Artist", template: "{title} - {album_artist}" },
|
"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}" },
|
"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";
|
const STORAGE_KEY = "spotiflac_file_manager_state";
|
||||||
@@ -128,7 +132,7 @@ export function FileManagerPage() {
|
|||||||
return DEFAULT_CUSTOM_FORMAT;
|
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 [showPreview, setShowPreview] = useState(false);
|
||||||
const [previewData, setPreviewData] = useState<backend.RenamePreview[]>([]);
|
const [previewData, setPreviewData] = useState<backend.RenamePreview[]>([]);
|
||||||
const [renaming, setRenaming] = useState(false);
|
const [renaming, setRenaming] = useState(false);
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ export function SettingsPage() {
|
|||||||
setTempSettings(prev => ({
|
setTempSettings(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
folderPreset: value,
|
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 => ({
|
setTempSettings(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
filenamePreset: value,
|
filenamePreset: value,
|
||||||
filenameTemplate: value === "custom" ? prev.filenameTemplate : preset.template
|
filenameTemplate: value === "custom" ? (prev.filenameTemplate || preset.template) : preset.template
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,125 +6,141 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { buttonVariants } from "@/components/ui/button"
|
import { buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
const AlertDialog = AlertDialogPrimitive.Root
|
function AlertDialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||||
|
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
function AlertDialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
function AlertDialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDialogOverlay = React.forwardRef<
|
function AlertDialogOverlay({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
<AlertDialogPrimitive.Overlay
|
<AlertDialogPrimitive.Overlay
|
||||||
|
data-slot="alert-dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
}
|
||||||
|
|
||||||
const AlertDialogContent = React.forwardRef<
|
function AlertDialogContent({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||||
|
return (
|
||||||
<AlertDialogPortal>
|
<AlertDialogPortal>
|
||||||
<AlertDialogOverlay />
|
<AlertDialogOverlay />
|
||||||
<AlertDialogPrimitive.Content
|
<AlertDialogPrimitive.Content
|
||||||
ref={ref}
|
data-slot="alert-dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</AlertDialogPortal>
|
</AlertDialogPortal>
|
||||||
))
|
)
|
||||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
}
|
||||||
|
|
||||||
const AlertDialogHeader = ({
|
function AlertDialogHeader({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
|
data-slot="alert-dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogFooter({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-dialog-footer"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col space-y-2 text-center sm:text-left",
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
}
|
||||||
|
|
||||||
const AlertDialogFooter = ({
|
function AlertDialogTitle({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
|
||||||
|
|
||||||
const AlertDialogTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AlertDialogPrimitive.Title
|
<AlertDialogPrimitive.Title
|
||||||
ref={ref}
|
data-slot="alert-dialog-title"
|
||||||
className={cn("text-lg font-semibold", className)}
|
className={cn("text-lg font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
}
|
||||||
|
|
||||||
const AlertDialogDescription = React.forwardRef<
|
function AlertDialogDescription({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
<AlertDialogPrimitive.Description
|
<AlertDialogPrimitive.Description
|
||||||
ref={ref}
|
data-slot="alert-dialog-description"
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
AlertDialogDescription.displayName =
|
}
|
||||||
AlertDialogPrimitive.Description.displayName
|
|
||||||
|
|
||||||
const AlertDialogAction = React.forwardRef<
|
function AlertDialogAction({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||||
|
return (
|
||||||
<AlertDialogPrimitive.Action
|
<AlertDialogPrimitive.Action
|
||||||
ref={ref}
|
|
||||||
className={cn(buttonVariants(), className)}
|
className={cn(buttonVariants(), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
}
|
||||||
|
|
||||||
const AlertDialogCancel = React.forwardRef<
|
function AlertDialogCancel({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||||
|
return (
|
||||||
<AlertDialogPrimitive.Cancel
|
<AlertDialogPrimitive.Cancel
|
||||||
ref={ref}
|
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||||
className={cn(
|
|
||||||
buttonVariants({ variant: "outline" }),
|
|
||||||
"mt-2 sm:mt-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
|
|||||||
@@ -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";
|
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
|
// 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
|
// 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 {
|
export interface Settings {
|
||||||
downloadPath: string;
|
downloadPath: string;
|
||||||
@@ -38,12 +38,18 @@ export const FOLDER_PRESETS: Record<FolderPreset, { label: string; template: str
|
|||||||
"none": { label: "No Subfolder", template: "" },
|
"none": { label: "No Subfolder", template: "" },
|
||||||
"artist": { label: "Artist", template: "{artist}" },
|
"artist": { label: "Artist", template: "{artist}" },
|
||||||
"album": { label: "Album", template: "{album}" },
|
"album": { label: "Album", template: "{album}" },
|
||||||
|
"year-album": { label: "[Year] Album", template: "[{year}] {album}" },
|
||||||
|
"year-artist-album": { label: "[Year] Artist - Album", template: "[{year}] {artist} - {album}" },
|
||||||
"artist-album": { label: "Artist / Album", template: "{artist}/{album}" },
|
"artist-album": { label: "Artist / Album", template: "{artist}/{album}" },
|
||||||
"artist-year-album": { label: "Artist / [Year] Album", template: "{artist}/[{year}] {album}" },
|
"artist-year-album": { label: "Artist / [Year] Album", template: "{artist}/[{year}] {album}" },
|
||||||
|
"artist-year-nested-album": { label: "Artist / Year / Album", template: "{artist}/{year}/{album}" },
|
||||||
"album-artist": { label: "Album Artist", template: "{album_artist}" },
|
"album-artist": { label: "Album Artist", template: "{album_artist}" },
|
||||||
"album-artist-album": { label: "Album Artist / Album", template: "{album_artist}/{album}" },
|
"album-artist-album": { label: "Album Artist / Album", template: "{album_artist}/{album}" },
|
||||||
"album-artist-year-album": { label: "Album Artist / [Year] Album", template: "{album_artist}/[{year}] {album}" },
|
"album-artist-year-album": { label: "Album Artist / [Year] Album", template: "{album_artist}/[{year}] {album}" },
|
||||||
"custom": { label: "Custom...", template: "" },
|
"album-artist-year-nested-album": { label: "Album Artist / Year / Album", template: "{album_artist}/{year}/{album}" },
|
||||||
|
"year": { label: "Year", template: "{year}" },
|
||||||
|
"year-artist": { label: "Year / Artist", template: "{year}/{artist}" },
|
||||||
|
"custom": { label: "Custom...", template: "{artist}/{album}" },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filename preset templates
|
// Filename preset templates
|
||||||
@@ -56,7 +62,11 @@ export const FILENAME_PRESETS: Record<FilenamePreset, { label: string; template:
|
|||||||
"track-artist-title": { label: "Track. Artist - Title", template: "{track}. {artist} - {title}" },
|
"track-artist-title": { label: "Track. Artist - Title", template: "{track}. {artist} - {title}" },
|
||||||
"title-album-artist": { label: "Title - Album Artist", template: "{title} - {album_artist}" },
|
"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}" },
|
"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}" },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Available template variables
|
// Available template variables
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ export interface SpectrumData {
|
|||||||
|
|
||||||
export interface AnalysisResult {
|
export interface AnalysisResult {
|
||||||
file_path: string;
|
file_path: string;
|
||||||
|
file_size: number;
|
||||||
sample_rate: number;
|
sample_rate: number;
|
||||||
channels: number;
|
channels: number;
|
||||||
bits_per_sample: number;
|
bits_per_sample: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user