From 8f10094e404d4d7b51c990cee681bf74a1441dab Mon Sep 17 00:00:00 2001 From: afkarxyz Date: Mon, 8 Dec 2025 19:33:43 +0700 Subject: [PATCH] v6.7 --- .gitignore | 3 + app.go | 3 + backend/amazon.go | 42 +- backend/cover.go | 44 +- backend/deezer.go | 49 +- backend/filename.go | 85 ++- backend/lyrics.go | 44 +- backend/progress.go | 7 + backend/qobuz.go | 50 +- backend/tidal.go | 423 ++++++++++-- frontend/package.json | 14 +- frontend/package.json.md5 | 2 +- frontend/pnpm-lock.yaml | 812 +++++++++++------------ frontend/src/App.tsx | 28 +- frontend/src/components/Settings.tsx | 144 ++-- frontend/src/components/SettingsPage.tsx | 144 ++-- frontend/src/components/TrackList.tsx | 4 +- frontend/src/hooks/useCover.ts | 67 +- frontend/src/hooks/useDownload.ts | 148 +++-- frontend/src/hooks/useLyrics.ts | 38 +- frontend/src/lib/settings.ts | 116 +++- tidal.json | 16 +- version.json | 2 +- wails.json | 2 +- 24 files changed, 1506 insertions(+), 781 deletions(-) diff --git a/.gitignore b/.gitignore index f9393c1..144dd72 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,9 @@ temp/ *.bak *.old +# Test files +test + # 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 4a38da3..03e29c9 100644 --- a/app.go +++ b/app.go @@ -131,6 +131,9 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) { if req.OutputDir == "" { req.OutputDir = "." + } else { + // Sanitize output directory path to remove invalid characters + req.OutputDir = backend.SanitizeFolderPath(req.OutputDir) } if req.AudioFormat == "" { diff --git a/backend/amazon.go b/backend/amazon.go index ae5528c..991d361 100644 --- a/backend/amazon.go +++ b/backend/amazon.go @@ -10,6 +10,7 @@ import ( "net/url" "os" "path/filepath" + "regexp" "strings" "time" ) @@ -390,18 +391,37 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, filenameFormat st // Build filename based on format settings var newFilename string - switch filenameFormat { - case "artist-title": - newFilename = fmt.Sprintf("%s - %s", safeArtist, safeTitle) - case "title": - newFilename = safeTitle - default: // "title-artist" - newFilename = fmt.Sprintf("%s - %s", safeTitle, safeArtist) - } - // Add track number prefix if enabled - if includeTrackNumber && position > 0 { - newFilename = fmt.Sprintf("%02d. %s", position, newFilename) + // Check if format is a template (contains {}) + if strings.Contains(filenameFormat, "{") { + newFilename = filenameFormat + newFilename = strings.ReplaceAll(newFilename, "{title}", safeTitle) + newFilename = strings.ReplaceAll(newFilename, "{artist}", safeArtist) + + // Handle track number - if position is 0, remove {track} and surrounding separators + if position > 0 { + newFilename = strings.ReplaceAll(newFilename, "{track}", fmt.Sprintf("%02d", position)) + } else { + // Remove {track} with common separators + newFilename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(newFilename, "") + newFilename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(newFilename, "") + newFilename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(newFilename, "") + } + } else { + // Legacy format support + switch filenameFormat { + case "artist-title": + newFilename = fmt.Sprintf("%s - %s", safeArtist, safeTitle) + case "title": + newFilename = safeTitle + default: // "title-artist" + newFilename = fmt.Sprintf("%s - %s", safeTitle, safeArtist) + } + + // Add track number prefix if enabled (legacy behavior) + if includeTrackNumber && position > 0 { + newFilename = fmt.Sprintf("%02d. %s", position, newFilename) + } } newFilename = newFilename + ".flac" diff --git a/backend/cover.go b/backend/cover.go index bab960b..d051c7f 100644 --- a/backend/cover.go +++ b/backend/cover.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "path/filepath" + "regexp" "strings" "time" ) @@ -55,19 +56,36 @@ func buildCoverFilename(trackName, artistName, filenameFormat string, includeTra var filename string - // Build base filename based on format - switch filenameFormat { - case "artist-title": - filename = fmt.Sprintf("%s - %s", safeArtist, safeTitle) - case "title": - filename = safeTitle - default: // "title-artist" - filename = fmt.Sprintf("%s - %s", safeTitle, safeArtist) - } + // Check if format is a template (contains {}) + if strings.Contains(filenameFormat, "{") { + filename = filenameFormat + filename = strings.ReplaceAll(filename, "{title}", safeTitle) + filename = strings.ReplaceAll(filename, "{artist}", safeArtist) - // Add track number prefix if enabled - if includeTrackNumber && position > 0 { - filename = fmt.Sprintf("%02d. %s", position, filename) + // Handle track number - if position is 0, remove {track} and surrounding separators + if position > 0 { + filename = strings.ReplaceAll(filename, "{track}", fmt.Sprintf("%02d", position)) + } else { + // Remove {track} with common separators + filename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(filename, "") + filename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(filename, "") + filename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(filename, "") + } + } else { + // Legacy format support + switch filenameFormat { + case "artist-title": + filename = fmt.Sprintf("%s - %s", safeArtist, safeTitle) + case "title": + filename = safeTitle + default: // "title-artist" + filename = fmt.Sprintf("%s - %s", safeTitle, safeArtist) + } + + // Add track number prefix if enabled (legacy behavior) + if includeTrackNumber && position > 0 { + filename = fmt.Sprintf("%02d. %s", position, filename) + } } return filename + ".jpg" @@ -102,6 +120,8 @@ func (c *CoverClient) DownloadCover(req CoverDownloadRequest) (*CoverDownloadRes outputDir := req.OutputDir if outputDir == "" { outputDir = GetDefaultMusicPath() + } else { + outputDir = SanitizeFolderPath(outputDir) } if err := os.MkdirAll(outputDir, 0755); err != nil { diff --git a/backend/deezer.go b/backend/deezer.go index 57627b9..9e553be 100644 --- a/backend/deezer.go +++ b/backend/deezer.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "path/filepath" + "regexp" "strings" "time" ) @@ -229,24 +230,42 @@ func (d *DeezerDownloader) DownloadCoverArt(coverURL, filepath string) error { func buildFilename(title, artist string, trackNumber int, format string, includeTrackNumber bool, position int, useAlbumTrackNumber bool) string { var filename string - // Build base filename based on format - switch format { - case "artist-title": - filename = fmt.Sprintf("%s - %s", artist, title) - case "title": - filename = title - default: // "title-artist" - filename = fmt.Sprintf("%s - %s", title, artist) + // Determine track number to use + numberToUse := position + if useAlbumTrackNumber && trackNumber > 0 { + numberToUse = trackNumber } - // Add track number prefix if enabled - if includeTrackNumber && position > 0 { - // Use album track number if in album folder structure, otherwise use playlist position - numberToUse := position - if useAlbumTrackNumber && trackNumber > 0 { - numberToUse = trackNumber + // Check if format is a template (contains {}) + if strings.Contains(format, "{") { + filename = format + filename = strings.ReplaceAll(filename, "{title}", title) + filename = strings.ReplaceAll(filename, "{artist}", artist) + + // Handle track number - if numberToUse is 0, remove {track} and surrounding separators + if numberToUse > 0 { + filename = strings.ReplaceAll(filename, "{track}", fmt.Sprintf("%02d", numberToUse)) + } else { + // Remove {track} with common separators + filename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(filename, "") + filename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(filename, "") + filename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(filename, "") + } + } else { + // Legacy format support + switch format { + case "artist-title": + filename = fmt.Sprintf("%s - %s", artist, title) + case "title": + filename = title + default: // "title-artist" + filename = fmt.Sprintf("%s - %s", title, artist) + } + + // Add track number prefix if enabled (legacy behavior) + if includeTrackNumber && position > 0 { + filename = fmt.Sprintf("%02d. %s", numberToUse, filename) } - filename = fmt.Sprintf("%02d. %s", numberToUse, filename) } return filename + ".flac" diff --git a/backend/filename.go b/backend/filename.go index d635525..a05ec91 100644 --- a/backend/filename.go +++ b/backend/filename.go @@ -2,6 +2,7 @@ package backend import ( "fmt" + "path/filepath" "regexp" "strings" ) @@ -14,21 +15,36 @@ func BuildExpectedFilename(trackName, artistName, filenameFormat string, include var filename string - // Build base filename based on format - switch filenameFormat { - case "artist-title": - filename = fmt.Sprintf("%s - %s", safeArtist, safeTitle) - case "title": - filename = safeTitle - default: // "title-artist" - filename = fmt.Sprintf("%s - %s", safeTitle, safeArtist) - } + // Check if format is a template (contains {}) + if strings.Contains(filenameFormat, "{") { + filename = filenameFormat + filename = strings.ReplaceAll(filename, "{title}", safeTitle) + filename = strings.ReplaceAll(filename, "{artist}", safeArtist) - // Add track number prefix if enabled - // Note: We can't determine the exact track number without fetching from API - // So we only add it if position > 0 (bulk download) - if includeTrackNumber && position > 0 { - filename = fmt.Sprintf("%02d. %s", position, filename) + // Handle track number - if position is 0, remove {track} and surrounding separators + if position > 0 { + filename = strings.ReplaceAll(filename, "{track}", fmt.Sprintf("%02d", position)) + } else { + // Remove {track} with common separators like ". " or " - " or ". " + filename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(filename, "") + filename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(filename, "") + filename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(filename, "") + } + } else { + // Legacy format support + switch filenameFormat { + case "artist-title": + filename = fmt.Sprintf("%s - %s", safeArtist, safeTitle) + case "title": + filename = safeTitle + default: // "title-artist" + filename = fmt.Sprintf("%s - %s", safeTitle, safeArtist) + } + + // Add track number prefix if enabled (legacy behavior) + if includeTrackNumber && position > 0 { + filename = fmt.Sprintf("%02d. %s", position, filename) + } } return filename + ".flac" @@ -44,3 +60,44 @@ func sanitizeFilename(name string) string { } return sanitized } + +// SanitizeFolderPath sanitizes each component of a folder path and normalizes separators +func SanitizeFolderPath(folderPath string) string { + // Normalize all forward slashes to backslashes on Windows + normalizedPath := strings.ReplaceAll(folderPath, "/", string(filepath.Separator)) + + // Detect separator + sep := string(filepath.Separator) + + // Split path into components + parts := strings.Split(normalizedPath, sep) + sanitizedParts := make([]string, 0, len(parts)) + + for i, part := range parts { + // Keep drive letter intact on Windows (e.g., "C:") + if i == 0 && len(part) == 2 && part[1] == ':' { + sanitizedParts = append(sanitizedParts, part) + continue + } + + // Sanitize each folder name (but don't replace / or \ since we already normalized) + sanitized := sanitizeFolderName(part) + if sanitized != "" { + sanitizedParts = append(sanitizedParts, sanitized) + } + } + + return strings.Join(sanitizedParts, sep) +} + +// sanitizeFolderName removes invalid characters from a single folder name +func sanitizeFolderName(name string) string { + // Remove or replace invalid characters for folder names (excluding path separators) + re := regexp.MustCompile(`[<>:"|?*]`) + sanitized := re.ReplaceAllString(name, "_") + sanitized = strings.TrimSpace(sanitized) + if sanitized == "" { + return "Unknown" + } + return sanitized +} diff --git a/backend/lyrics.go b/backend/lyrics.go index 42727c3..d796832 100644 --- a/backend/lyrics.go +++ b/backend/lyrics.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "path/filepath" + "regexp" "strings" "time" ) @@ -132,19 +133,36 @@ func buildLyricsFilename(trackName, artistName, filenameFormat string, includeTr var filename string - // Build base filename based on format - switch filenameFormat { - case "artist-title": - filename = fmt.Sprintf("%s - %s", safeArtist, safeTitle) - case "title": - filename = safeTitle - default: // "title-artist" - filename = fmt.Sprintf("%s - %s", safeTitle, safeArtist) - } + // Check if format is a template (contains {}) + if strings.Contains(filenameFormat, "{") { + filename = filenameFormat + filename = strings.ReplaceAll(filename, "{title}", safeTitle) + filename = strings.ReplaceAll(filename, "{artist}", safeArtist) - // Add track number prefix if enabled - if includeTrackNumber && position > 0 { - filename = fmt.Sprintf("%02d. %s", position, filename) + // Handle track number - if position is 0, remove {track} and surrounding separators + if position > 0 { + filename = strings.ReplaceAll(filename, "{track}", fmt.Sprintf("%02d", position)) + } else { + // Remove {track} with common separators + filename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(filename, "") + filename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(filename, "") + filename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(filename, "") + } + } else { + // Legacy format support + switch filenameFormat { + case "artist-title": + filename = fmt.Sprintf("%s - %s", safeArtist, safeTitle) + case "title": + filename = safeTitle + default: // "title-artist" + filename = fmt.Sprintf("%s - %s", safeTitle, safeArtist) + } + + // Add track number prefix if enabled (legacy behavior) + if includeTrackNumber && position > 0 { + filename = fmt.Sprintf("%02d. %s", position, filename) + } } return filename + ".lrc" @@ -163,6 +181,8 @@ func (c *LyricsClient) DownloadLyrics(req LyricsDownloadRequest) (*LyricsDownloa outputDir := req.OutputDir if outputDir == "" { outputDir = GetDefaultMusicPath() + } else { + outputDir = SanitizeFolderPath(outputDir) } if err := os.MkdirAll(outputDir, 0755); err != nil { diff --git a/backend/progress.go b/backend/progress.go index 3915197..abebc85 100644 --- a/backend/progress.go +++ b/backend/progress.go @@ -264,6 +264,13 @@ func UpdateItemProgress(id string, progress, speed float64) { } } +// GetCurrentItemID returns the ID of the currently downloading item +func GetCurrentItemID() string { + currentItemLock.RLock() + defer currentItemLock.RUnlock() + return currentItemID +} + // CompleteDownloadItem marks an item as completed func CompleteDownloadItem(id, filePath string, finalSize float64) { downloadQueueLock.Lock() diff --git a/backend/qobuz.go b/backend/qobuz.go index b2cff6c..13819c7 100644 --- a/backend/qobuz.go +++ b/backend/qobuz.go @@ -8,6 +8,8 @@ import ( "net/http" "os" "path/filepath" + "regexp" + "strings" "time" ) @@ -225,24 +227,42 @@ func (q *QobuzDownloader) DownloadCoverArt(coverURL, filepath string) error { func buildQobuzFilename(title, artist string, trackNumber int, format string, includeTrackNumber bool, position int, useAlbumTrackNumber bool) string { var filename string - // Build base filename based on format - switch format { - case "artist-title": - filename = fmt.Sprintf("%s - %s", artist, title) - case "title": - filename = title - default: // "title-artist" - filename = fmt.Sprintf("%s - %s", title, artist) + // Determine track number to use + numberToUse := position + if useAlbumTrackNumber && trackNumber > 0 { + numberToUse = trackNumber } - // Add track number prefix if enabled - if includeTrackNumber && position > 0 { - // Use album track number if in album folder structure, otherwise use playlist position - numberToUse := position - if useAlbumTrackNumber && trackNumber > 0 { - numberToUse = trackNumber + // Check if format is a template (contains {}) + if strings.Contains(format, "{") { + filename = format + filename = strings.ReplaceAll(filename, "{title}", title) + filename = strings.ReplaceAll(filename, "{artist}", artist) + + // Handle track number - if numberToUse is 0, remove {track} and surrounding separators + if numberToUse > 0 { + filename = strings.ReplaceAll(filename, "{track}", fmt.Sprintf("%02d", numberToUse)) + } else { + // Remove {track} with common separators + filename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(filename, "") + filename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(filename, "") + filename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(filename, "") + } + } else { + // Legacy format support + switch format { + case "artist-title": + filename = fmt.Sprintf("%s - %s", artist, title) + case "title": + filename = title + default: // "title-artist" + filename = fmt.Sprintf("%s - %s", title, artist) + } + + // Add track number prefix if enabled (legacy behavior) + if includeTrackNumber && position > 0 { + filename = fmt.Sprintf("%02d. %s", numberToUse, filename) } - filename = fmt.Sprintf("%02d. %s", numberToUse, filename) } return filename + ".flac" diff --git a/backend/tidal.go b/backend/tidal.go index a3ee2f2..00c778b 100644 --- a/backend/tidal.go +++ b/backend/tidal.go @@ -3,12 +3,15 @@ package backend import ( "encoding/base64" "encoding/json" + "encoding/xml" "fmt" "io" "net/http" "net/url" "os" + "os/exec" "path/filepath" + "regexp" "strings" "time" ) @@ -59,11 +62,35 @@ type TidalAPIResponse struct { OriginalTrackURL string `json:"OriginalTrackUrl"` } +// TidalAPIResponseV2 is the new API response format (version 2.0) +type TidalAPIResponseV2 struct { + Version string `json:"version"` + Data struct { + TrackID int64 `json:"trackId"` + AssetPresentation string `json:"assetPresentation"` + AudioMode string `json:"audioMode"` + AudioQuality string `json:"audioQuality"` + ManifestMimeType string `json:"manifestMimeType"` + ManifestHash string `json:"manifestHash"` + Manifest string `json:"manifest"` + BitDepth int `json:"bitDepth"` + SampleRate int `json:"sampleRate"` + } `json:"data"` +} + type TidalAPIInfo struct { URL string `json:"url"` Status string `json:"status"` } +// TidalBTSManifest is the BTS (application/vnd.tidal.bts) manifest format +type TidalBTSManifest struct { + MimeType string `json:"mimeType"` + Codecs string `json:"codecs"` + EncryptionType string `json:"encryptionType"` + URLs []string `json:"urls"` +} + func NewTidalDownloader(apiURL string) *TidalDownloader { clientID, _ := base64.StdEncoding.DecodeString("NkJEU1JkcEs5aHFFQlRnVQ==") clientSecret, _ := base64.StdEncoding.DecodeString("eGV1UG1ZN25icFo5SUliTEFjUTkzc2hrYTFWTmhlVUFxTjZJY3N6alRHOD0=") @@ -72,7 +99,7 @@ func NewTidalDownloader(apiURL string) *TidalDownloader { if apiURL == "" { downloader := &TidalDownloader{ client: &http.Client{ - Timeout: 5 * time.Second, // Fast timeout for quick API fallback + Timeout: 5 * time.Second, }, timeout: 5 * time.Second, maxRetries: 3, @@ -84,13 +111,13 @@ func NewTidalDownloader(apiURL string) *TidalDownloader { // Try to get available APIs apis, err := downloader.GetAvailableAPIs() if err == nil && len(apis) > 0 { - apiURL = apis[0] // Use first available API + apiURL = apis[0] } } return &TidalDownloader{ client: &http.Client{ - Timeout: 5 * time.Second, // Fast timeout for quick API fallback + Timeout: 5 * time.Second, }, timeout: 5 * time.Second, maxRetries: 3, @@ -527,8 +554,23 @@ func (t *TidalDownloader) GetDownloadURL(trackID int64, quality string) (string, return "", fmt.Errorf("API returned status code: %d", resp.StatusCode) } + // Read body to try both formats + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Printf("✗ Failed to read response body: %v\n", err) + return "", fmt.Errorf("failed to read response: %w", err) + } + + // Try v2 format first (object with manifest) + var v2Response TidalAPIResponseV2 + if err := json.Unmarshal(body, &v2Response); err == nil && v2Response.Data.Manifest != "" { + fmt.Println("✓ Tidal manifest found (v2 API)") + return "MANIFEST:" + v2Response.Data.Manifest, nil + } + + // Fallback to v1 format (array with OriginalTrackUrl) var apiResponses []TidalAPIResponse - if err := json.NewDecoder(resp.Body).Decode(&apiResponses); err != nil { + if err := json.Unmarshal(body, &apiResponses); err != nil { fmt.Printf("✗ Failed to decode Tidal API response: %v\n", err) return "", fmt.Errorf("failed to decode response: %w", err) } @@ -569,6 +611,11 @@ func (t *TidalDownloader) DownloadAlbumArt(albumID string) ([]byte, error) { } func (t *TidalDownloader) DownloadFile(url, filepath string) error { + // Check if this is a manifest-based download + if strings.HasPrefix(url, "MANIFEST:") { + return t.DownloadFromManifest(strings.TrimPrefix(url, "MANIFEST:"), filepath) + } + resp, err := t.client.Get(url) if err != nil { return fmt.Errorf("failed to download file: %w", err) @@ -599,6 +646,155 @@ func (t *TidalDownloader) DownloadFile(url, filepath string) error { return nil } +// DownloadFromManifest downloads audio from manifest (supports BTS and DASH formats) +func (t *TidalDownloader) DownloadFromManifest(manifestB64, outputPath string) error { + directURL, initURL, mediaURLs, err := parseManifest(manifestB64) + if err != nil { + return fmt.Errorf("failed to parse manifest: %w", err) + } + + // Create HTTP client with longer timeout + client := &http.Client{ + Timeout: 120 * time.Second, + } + + // If we have a direct URL (BTS format), download directly + if directURL != "" { + fmt.Println("Downloading file...") + + resp, err := client.Get(directURL) + if err != nil { + return fmt.Errorf("failed to download file: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("download failed with status %d", resp.StatusCode) + } + + out, err := os.Create(outputPath) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + defer out.Close() + + // Use progress writer to track download + pw := NewProgressWriter(out) + _, err = io.Copy(pw, resp.Body) + if err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + + fmt.Printf("\rDownloaded: %.2f MB (Complete)\n", float64(pw.GetTotal())/(1024*1024)) + fmt.Println("Download complete") + return nil + } + + // DASH format - download segments to temporary M4A file, then remux to FLAC + fmt.Printf("Downloading %d segments...\n", len(mediaURLs)+1) + + // Create temporary file for M4A segments + tempPath := outputPath + ".m4a.tmp" + out, err := os.Create(tempPath) + if err != nil { + return fmt.Errorf("failed to create temp file: %w", err) + } + + // Download initialization segment + fmt.Print("Downloading init segment... ") + resp, err := client.Get(initURL) + if err != nil { + out.Close() + os.Remove(tempPath) + return fmt.Errorf("failed to download init segment: %w", err) + } + if resp.StatusCode != 200 { + resp.Body.Close() + out.Close() + os.Remove(tempPath) + return fmt.Errorf("init segment download failed with status %d", resp.StatusCode) + } + _, err = io.Copy(out, resp.Body) + resp.Body.Close() + if err != nil { + out.Close() + os.Remove(tempPath) + return fmt.Errorf("failed to write init segment: %w", err) + } + fmt.Println("OK") + + // Download media segments with progress tracking + totalSegments := len(mediaURLs) + var totalBytes int64 + lastTime := time.Now() + var lastBytes int64 + for i, mediaURL := range mediaURLs { + resp, err := client.Get(mediaURL) + if err != nil { + out.Close() + os.Remove(tempPath) + return fmt.Errorf("failed to download segment %d: %w", i+1, err) + } + if resp.StatusCode != 200 { + resp.Body.Close() + out.Close() + os.Remove(tempPath) + return fmt.Errorf("segment %d download failed with status %d", i+1, resp.StatusCode) + } + n, err := io.Copy(out, resp.Body) + totalBytes += n + resp.Body.Close() + if err != nil { + out.Close() + os.Remove(tempPath) + return fmt.Errorf("failed to write segment %d: %w", i+1, err) + } + + // Calculate speed and update progress for frontend + mbDownloaded := float64(totalBytes) / (1024 * 1024) + now := time.Now() + timeDiff := now.Sub(lastTime).Seconds() + var speedMBps float64 + if timeDiff > 0.1 { // Update speed every 100ms + bytesDiff := float64(totalBytes - lastBytes) + speedMBps = (bytesDiff / (1024 * 1024)) / timeDiff + SetDownloadSpeed(speedMBps) + lastTime = now + lastBytes = totalBytes + } + SetDownloadProgress(mbDownloaded) + + // Show progress with size in terminal + fmt.Printf("\rDownloading: %.2f MB (%d/%d segments)", mbDownloaded, i+1, totalSegments) + } + + // Close temp file before remuxing + out.Close() + + // Get temp file size + tempInfo, _ := os.Stat(tempPath) + fmt.Printf("\rDownloaded: %.2f MB (Complete) \n", float64(tempInfo.Size())/(1024*1024)) + + // Remux M4A to FLAC using ffmpeg + // DASH segments are in fMP4 container with FLAC codec, need to extract to native FLAC + fmt.Println("Converting to FLAC...") + cmd := exec.Command("ffmpeg", "-y", "-i", tempPath, "-vn", "-c:a", "flac", outputPath) + var stderr strings.Builder + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + // If ffmpeg fails, try to keep the M4A file for debugging + m4aPath := strings.TrimSuffix(outputPath, ".flac") + ".m4a" + os.Rename(tempPath, m4aPath) + return fmt.Errorf("ffmpeg conversion failed (M4A saved as %s): %w - %s", m4aPath, err, stderr.String()) + } + + // Remove temp file + os.Remove(tempPath) + fmt.Println("Download complete") + + return nil +} + func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) { if outputDir != "." { if err := os.MkdirAll(outputDir, 0755); err != nil { @@ -1050,21 +1246,132 @@ func (t *TidalDownloader) DownloadBySearchWithISRC(trackName, artistName, albumN return outputFilename, nil } -// apiResult holds the result from a parallel API request -type apiResult struct { - apiURL string - downloadURL string - err error +// DASH MPD XML structures for parsing manifest +type MPD struct { + XMLName xml.Name `xml:"MPD"` + Period struct { + AdaptationSet struct { + Representation struct { + SegmentTemplate struct { + Initialization string `xml:"initialization,attr"` + Media string `xml:"media,attr"` + Timeline struct { + Segments []struct { + Duration int `xml:"d,attr"` + Repeat int `xml:"r,attr"` + } `xml:"S"` + } `xml:"SegmentTimeline"` + } `xml:"SegmentTemplate"` + } `xml:"Representation"` + } `xml:"AdaptationSet"` + } `xml:"Period"` +} + +// parseManifest extracts download URL from base64 encoded manifest +// Supports both BTS (JSON) and DASH (XML) formats +// Returns: directURL (for BTS), or initURL + mediaURLs (for DASH) +func parseManifest(manifestB64 string) (directURL string, initURL string, mediaURLs []string, err error) { + // Decode base64 manifest + manifestBytes, err := base64.StdEncoding.DecodeString(manifestB64) + if err != nil { + return "", "", nil, fmt.Errorf("failed to decode manifest: %w", err) + } + + manifestStr := string(manifestBytes) + + // Check if it's BTS format (JSON) or DASH format (XML) + if strings.HasPrefix(manifestStr, "{") { + // BTS format - JSON with direct URLs + var btsManifest TidalBTSManifest + if err := json.Unmarshal(manifestBytes, &btsManifest); err != nil { + return "", "", nil, fmt.Errorf("failed to parse BTS manifest: %w", err) + } + + if len(btsManifest.URLs) == 0 { + return "", "", nil, fmt.Errorf("no URLs in BTS manifest") + } + + fmt.Printf("Manifest: BTS format (%s, %s)\n", btsManifest.MimeType, btsManifest.Codecs) + return btsManifest.URLs[0], "", nil, nil + } + + // DASH format - XML with segments + fmt.Println("Manifest: DASH format") + + // Parse XML + var mpd MPD + if err := xml.Unmarshal(manifestBytes, &mpd); err != nil { + return "", "", nil, fmt.Errorf("failed to parse manifest XML: %w", err) + } + + segTemplate := mpd.Period.AdaptationSet.Representation.SegmentTemplate + initURL = segTemplate.Initialization + mediaTemplate := segTemplate.Media + + if initURL == "" || mediaTemplate == "" { + // Fallback: try regex extraction + initRe := regexp.MustCompile(`initialization="([^"]+)"`) + mediaRe := regexp.MustCompile(`media="([^"]+)"`) + + if match := initRe.FindStringSubmatch(manifestStr); len(match) > 1 { + initURL = match[1] + } + if match := mediaRe.FindStringSubmatch(manifestStr); len(match) > 1 { + mediaTemplate = match[1] + } + } + + if initURL == "" { + return "", "", nil, fmt.Errorf("no initialization URL found in manifest") + } + + // Unescape HTML entities in URLs + initURL = strings.ReplaceAll(initURL, "&", "&") + mediaTemplate = strings.ReplaceAll(mediaTemplate, "&", "&") + + // Calculate segment count from timeline + segmentCount := 0 + for _, seg := range segTemplate.Timeline.Segments { + segmentCount += seg.Repeat + 1 + } + + // If no segments found via XML, try regex + if segmentCount == 0 { + segRe := regexp.MustCompile(` 1 && match[1] != "" { + fmt.Sscanf(match[1], "%d", &repeat) + } + segmentCount += repeat + 1 + } + } + + // Generate media URLs for each segment + for i := 1; i <= segmentCount; i++ { + mediaURL := strings.ReplaceAll(mediaTemplate, "$Number$", fmt.Sprintf("%d", i)) + mediaURLs = append(mediaURLs, mediaURL) + } + + return "", initURL, mediaURLs, nil +} + +// manifestResult holds the result from a parallel API request for v2 API +type manifestResult struct { + apiURL string + manifest string + err error } // getDownloadURLParallel requests download URL from all APIs in parallel -// Returns the first successful result +// Returns the first successful result (supports both v1 and v2 API formats) func getDownloadURLParallel(apis []string, trackID int64, quality string) (string, string, error) { if len(apis) == 0 { return "", "", fmt.Errorf("no APIs available") } - resultChan := make(chan apiResult, len(apis)) + resultChan := make(chan manifestResult, len(apis)) // Start all requests in parallel with longer timeout client fmt.Printf("Requesting download URL from %d APIs in parallel...\n", len(apis)) @@ -1078,30 +1385,43 @@ func getDownloadURLParallel(apis []string, trackID int64, quality string) (strin url := fmt.Sprintf("%s/track/?id=%d&quality=%s", api, trackID, quality) resp, err := client.Get(url) if err != nil { - resultChan <- apiResult{apiURL: api, err: err} + resultChan <- manifestResult{apiURL: api, err: err} return } defer resp.Body.Close() if resp.StatusCode != 200 { - resultChan <- apiResult{apiURL: api, err: fmt.Errorf("HTTP %d", resp.StatusCode)} + resultChan <- manifestResult{apiURL: api, err: fmt.Errorf("HTTP %d", resp.StatusCode)} return } - var apiResponses []TidalAPIResponse - if err := json.NewDecoder(resp.Body).Decode(&apiResponses); err != nil { - resultChan <- apiResult{apiURL: api, err: err} + // Read body to try both formats + body, err := io.ReadAll(resp.Body) + if err != nil { + resultChan <- manifestResult{apiURL: api, err: err} return } - for _, item := range apiResponses { - if item.OriginalTrackURL != "" { - resultChan <- apiResult{apiURL: api, downloadURL: item.OriginalTrackURL, err: nil} - return + // Try v2 format first (object with manifest) + var v2Response TidalAPIResponseV2 + if err := json.Unmarshal(body, &v2Response); err == nil && v2Response.Data.Manifest != "" { + resultChan <- manifestResult{apiURL: api, manifest: v2Response.Data.Manifest, err: nil} + return + } + + // Fallback to v1 format (array with OriginalTrackUrl) + var v1Responses []TidalAPIResponse + if err := json.Unmarshal(body, &v1Responses); err == nil { + for _, item := range v1Responses { + if item.OriginalTrackURL != "" { + // For v1, we store the URL directly with a prefix to distinguish + resultChan <- manifestResult{apiURL: api, manifest: "DIRECT:" + item.OriginalTrackURL, err: nil} + return + } } } - resultChan <- apiResult{apiURL: api, err: fmt.Errorf("no download URL in response")} + resultChan <- manifestResult{apiURL: api, err: fmt.Errorf("no download URL or manifest in response")} }(apiURL) } @@ -1111,10 +1431,17 @@ func getDownloadURLParallel(apis []string, trackID int64, quality string) (strin for i := 0; i < len(apis); i++ { result := <-resultChan - if result.err == nil && result.downloadURL != "" { + if result.err == nil && result.manifest != "" { // First success - use this one - fmt.Printf("✓ Got download URL from: %s\n", result.apiURL) - return result.apiURL, result.downloadURL, nil + fmt.Printf("✓ Got response from: %s\n", result.apiURL) + + // Check if it's a direct URL (v1) or manifest (v2) + if strings.HasPrefix(result.manifest, "DIRECT:") { + return result.apiURL, strings.TrimPrefix(result.manifest, "DIRECT:"), nil + } + + // It's a v2 manifest - return it with MANIFEST: prefix + return result.apiURL, "MANIFEST:" + result.manifest, nil } else { errMsg := result.err.Error() if len(errMsg) > 50 { @@ -1311,24 +1638,42 @@ func (t *TidalDownloader) DownloadWithFallbackAndISRC(spotifyTrackID, spotifyISR func buildTidalFilename(title, artist string, trackNumber int, format string, includeTrackNumber bool, position int, useAlbumTrackNumber bool) string { var filename string - // Build base filename based on format - switch format { - case "artist-title": - filename = fmt.Sprintf("%s - %s", artist, title) - case "title": - filename = title - default: // "title-artist" - filename = fmt.Sprintf("%s - %s", title, artist) + // Determine track number to use + numberToUse := position + if useAlbumTrackNumber && trackNumber > 0 { + numberToUse = trackNumber } - // Add track number prefix if enabled - if includeTrackNumber && position > 0 { - // Use album track number if in album folder structure, otherwise use playlist position - numberToUse := position - if useAlbumTrackNumber && trackNumber > 0 { - numberToUse = trackNumber + // Check if format is a template (contains {}) + if strings.Contains(format, "{") { + filename = format + filename = strings.ReplaceAll(filename, "{title}", title) + filename = strings.ReplaceAll(filename, "{artist}", artist) + + // Handle track number - if numberToUse is 0, remove {track} and surrounding separators + if numberToUse > 0 { + filename = strings.ReplaceAll(filename, "{track}", fmt.Sprintf("%02d", numberToUse)) + } else { + // Remove {track} with common separators + filename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(filename, "") + filename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(filename, "") + filename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(filename, "") + } + } else { + // Legacy format support + switch format { + case "artist-title": + filename = fmt.Sprintf("%s - %s", artist, title) + case "title": + filename = title + default: // "title-artist" + filename = fmt.Sprintf("%s - %s", title, artist) + } + + // Add track number prefix if enabled (legacy behavior) + if includeTrackNumber && position > 0 { + filename = fmt.Sprintf("%02d. %s", numberToUse, filename) } - filename = fmt.Sprintf("%02d. %s", numberToUse, filename) } return filename + ".flac" diff --git a/frontend/package.json b/frontend/package.json index d92b6ed..b0926b5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,16 +21,16 @@ "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.1.17", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "lucide-react": "^0.555.0", + "lucide-react": "^0.556.0", "next-themes": "^0.4.6", - "react": "^19.2.0", - "react-dom": "^19.2.0", + "react": "^19.2.1", + "react-dom": "^19.2.1", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.17" @@ -40,7 +40,7 @@ "@types/node": "^24.10.1", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", + "@vitejs/plugin-react": "^5.1.2", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", @@ -48,7 +48,7 @@ "sharp": "^0.34.5", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.48.0", - "vite": "^7.2.6" + "typescript-eslint": "^8.48.1", + "vite": "^7.2.7" } } diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 9434da5..dd4b7ad 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -b7a549e463d5f6a2fad25f5ce939cdd7 \ No newline at end of file +58400d5c8f7b03bac8ab784b5e775687 \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index f0432a6..58d02af 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -10,43 +10,43 @@ importers: dependencies: '@radix-ui/react-checkbox': specifier: ^1.3.3 - version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@radix-ui/react-context-menu': specifier: ^2.2.16 - version: 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@radix-ui/react-dialog': specifier: ^1.1.15 - version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@radix-ui/react-label': specifier: ^2.1.8 - version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@radix-ui/react-progress': specifier: ^1.1.8 - version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@radix-ui/react-radio-group': specifier: ^1.3.8 - version: 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@radix-ui/react-scroll-area': specifier: ^1.2.10 - version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@radix-ui/react-select': specifier: ^2.2.6 - version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@radix-ui/react-slot': specifier: ^1.2.4 - version: 1.2.4(@types/react@19.2.7)(react@19.2.0) + version: 1.2.4(@types/react@19.2.7)(react@19.2.1) '@radix-ui/react-switch': - specifier: ^1.1.3 - version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: ^1.2.6 + version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@radix-ui/react-tabs': specifier: ^1.1.13 - version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@radix-ui/react-tooltip': specifier: ^1.2.8 - version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@tailwindcss/vite': specifier: ^4.1.17 - version: 4.1.17(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) + version: 4.1.17(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -54,20 +54,20 @@ importers: specifier: ^2.1.1 version: 2.1.1 lucide-react: - specifier: ^0.555.0 - version: 0.555.0(react@19.2.0) + specifier: ^0.556.0 + version: 0.556.0(react@19.2.1) next-themes: specifier: ^0.4.6 - version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: - specifier: ^19.2.0 - version: 19.2.0 + specifier: ^19.2.1 + version: 19.2.1 react-dom: - specifier: ^19.2.0 - version: 19.2.0(react@19.2.0) + specifier: ^19.2.1 + version: 19.2.1(react@19.2.1) sonner: specifier: ^2.0.7 - version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 2.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1) tailwind-merge: specifier: ^3.4.0 version: 3.4.0 @@ -88,8 +88,8 @@ importers: specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.7) '@vitejs/plugin-react': - specifier: ^5.1.1 - version: 5.1.1(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) + specifier: ^5.1.2 + version: 5.1.2(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) eslint: specifier: ^9.39.1 version: 9.39.1(jiti@2.6.1) @@ -112,11 +112,11 @@ importers: specifier: ~5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.48.0 - version: 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.48.1 + version: 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) vite: - specifier: ^7.2.6 - version: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) + specifier: ^7.2.7 + version: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) packages: @@ -1036,8 +1036,8 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@rolldown/pluginutils@1.0.0-beta.47': - resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} '@rollup/rollup-android-arm-eabi@4.53.3': resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} @@ -1268,67 +1268,67 @@ packages: '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} - '@typescript-eslint/eslint-plugin@8.48.0': - resolution: {integrity: sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==} + '@typescript-eslint/eslint-plugin@8.48.1': + resolution: {integrity: sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.48.0 + '@typescript-eslint/parser': ^8.48.1 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.48.0': - resolution: {integrity: sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==} + '@typescript-eslint/parser@8.48.1': + resolution: {integrity: sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.48.0': - resolution: {integrity: sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==} + '@typescript-eslint/project-service@8.48.1': + resolution: {integrity: sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.48.0': - resolution: {integrity: sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==} + '@typescript-eslint/scope-manager@8.48.1': + resolution: {integrity: sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.48.0': - resolution: {integrity: sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==} + '@typescript-eslint/tsconfig-utils@8.48.1': + resolution: {integrity: sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.48.0': - resolution: {integrity: sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==} + '@typescript-eslint/type-utils@8.48.1': + resolution: {integrity: sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.48.0': - resolution: {integrity: sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==} + '@typescript-eslint/types@8.48.1': + resolution: {integrity: sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.48.0': - resolution: {integrity: sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==} + '@typescript-eslint/typescript-estree@8.48.1': + resolution: {integrity: sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.48.0': - resolution: {integrity: sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==} + '@typescript-eslint/utils@8.48.1': + resolution: {integrity: sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.48.0': - resolution: {integrity: sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==} + '@typescript-eslint/visitor-keys@8.48.1': + resolution: {integrity: sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitejs/plugin-react@5.1.1': - resolution: {integrity: sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==} + '@vitejs/plugin-react@5.1.2': + resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -1360,8 +1360,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.8.32: - resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==} + baseline-browser-mapping@2.9.4: + resolution: {integrity: sha512-ZCQ9GEWl73BVm8bu5Fts8nt7MHdbt5vY9bP6WGnUh+r3l8M7CgfyTlwsgCbMC66BNxPr6Xoce3j66Ms5YUQTNA==} hasBin: true brace-expansion@1.1.12: @@ -1370,8 +1370,8 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - browserslist@4.28.0: - resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -1379,8 +1379,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001757: - resolution: {integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==} + caniuse-lite@1.0.30001759: + resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -1432,8 +1432,8 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - electron-to-chromium@1.5.262: - resolution: {integrity: sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==} + electron-to-chromium@1.5.266: + resolution: {integrity: sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==} enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} @@ -1723,8 +1723,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-react@0.555.0: - resolution: {integrity: sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==} + lucide-react@0.556.0: + resolution: {integrity: sha512-iOb8dRk7kLaYBZhR2VlV1CeJGxChBgUthpSP8wom9jfj79qovgG6qcSdiy6vkoREKPnbUYzJsCn4o4PtG3Iy+A==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1801,10 +1801,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - react-dom@19.2.0: - resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} + react-dom@19.2.1: + resolution: {integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==} peerDependencies: - react: ^19.2.0 + react: ^19.2.1 react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} @@ -1840,8 +1840,8 @@ packages: '@types/react': optional: true - react@19.2.0: - resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} + react@19.2.1: + resolution: {integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==} engines: {node: '>=0.10.0'} resolve-from@4.0.0: @@ -1925,8 +1925,8 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript-eslint@8.48.0: - resolution: {integrity: sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==} + typescript-eslint@8.48.1: + resolution: {integrity: sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -1940,8 +1940,8 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - update-browserslist-db@1.1.4: - resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} + update-browserslist-db@1.2.2: + resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -1969,8 +1969,8 @@ packages: '@types/react': optional: true - vite@7.2.6: - resolution: {integrity: sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==} + vite@7.2.7: + resolution: {integrity: sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2076,7 +2076,7 @@ snapshots: dependencies: '@babel/compat-data': 7.28.5 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.0 + browserslist: 4.28.1 lru-cache: 5.1.1 semver: 6.3.1 @@ -2286,11 +2286,11 @@ snapshots: '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@floating-ui/react-dom@2.1.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@floating-ui/dom': 1.7.4 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) '@floating-ui/utils@0.2.10': {} @@ -2424,453 +2424,453 @@ snapshots: '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.7)(react@19.2.0)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.7)(react@19.2.1)': dependencies: - react: 19.2.0 + react: 19.2.1 optionalDependencies: '@types/react': 19.2.7 - '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-context@1.1.2(@types/react@19.2.7)(react@19.2.0)': + '@radix-ui/react-context@1.1.2(@types/react@19.2.7)(react@19.2.1)': dependencies: - react: 19.2.0 + react: 19.2.1 optionalDependencies: '@types/react': 19.2.7 - '@radix-ui/react-context@1.1.3(@types/react@19.2.7)(react@19.2.0)': + '@radix-ui/react-context@1.1.3(@types/react@19.2.7)(react@19.2.1)': dependencies: - react: 19.2.0 + react: 19.2.1 optionalDependencies: '@types/react': 19.2.7 - '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.1) aria-hidden: 1.2.6 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - react-remove-scroll: 2.7.2(@types/react@19.2.7)(react@19.2.0) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + react-remove-scroll: 2.7.2(@types/react@19.2.7)(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-direction@1.1.1(@types/react@19.2.7)(react@19.2.0)': + '@radix-ui/react-direction@1.1.1(@types/react@19.2.7)(react@19.2.1)': dependencies: - react: 19.2.0 + react: 19.2.1 optionalDependencies: '@types/react': 19.2.7 - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.7)(react@19.2.0)': + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.7)(react@19.2.1)': dependencies: - react: 19.2.0 + react: 19.2.1 optionalDependencies: '@types/react': 19.2.7 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-id@1.1.1(@types/react@19.2.7)(react@19.2.0)': + '@radix-ui/react-id@1.1.1(@types/react@19.2.7)(react@19.2.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 optionalDependencies: '@types/react': 19.2.7 - '@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1) aria-hidden: 1.2.6 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - react-remove-scroll: 2.7.2(@types/react@19.2.7)(react@19.2.0) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + react-remove-scroll: 2.7.2(@types/react@19.2.7)(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: - '@floating-ui/react-dom': 2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.0) + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.1) '@radix-ui/rect': 1.1.1 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: - '@radix-ui/react-slot': 1.2.4(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-progress@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-progress@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: - '@radix-ui/react-context': 1.1.3(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-context': 1.1.3(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) aria-hidden: 1.2.6 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - react-remove-scroll: 2.7.2(@types/react@19.2.7)(react@19.2.0) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + react-remove-scroll: 2.7.2(@types/react@19.2.7)(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-slot@1.2.3(@types/react@19.2.7)(react@19.2.0)': + '@radix-ui/react-slot@1.2.3(@types/react@19.2.7)(react@19.2.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 optionalDependencies: '@types/react': 19.2.7 - '@radix-ui/react-slot@1.2.4(@types/react@19.2.7)(react@19.2.0)': + '@radix-ui/react-slot@1.2.4(@types/react@19.2.7)(react@19.2.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 optionalDependencies: '@types/react': 19.2.7 - '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.7)(react@19.2.0)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.7)(react@19.2.1)': dependencies: - react: 19.2.0 + react: 19.2.1 optionalDependencies: '@types/react': 19.2.7 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.7)(react@19.2.0)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.7)(react@19.2.1)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.7)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.7)(react@19.2.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 optionalDependencies: '@types/react': 19.2.7 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.7)(react@19.2.0)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.7)(react@19.2.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 optionalDependencies: '@types/react': 19.2.7 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.7)(react@19.2.0)': + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.7)(react@19.2.1)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 optionalDependencies: '@types/react': 19.2.7 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.7)(react@19.2.0)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.7)(react@19.2.1)': dependencies: - react: 19.2.0 + react: 19.2.1 optionalDependencies: '@types/react': 19.2.7 - '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.7)(react@19.2.0)': + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.7)(react@19.2.1)': dependencies: - react: 19.2.0 + react: 19.2.1 optionalDependencies: '@types/react': 19.2.7 - '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.7)(react@19.2.0)': + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.7)(react@19.2.1)': dependencies: '@radix-ui/rect': 1.1.1 - react: 19.2.0 + react: 19.2.1 optionalDependencies: '@types/react': 19.2.7 - '@radix-ui/react-use-size@1.1.1(@types/react@19.2.7)(react@19.2.0)': + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.7)(react@19.2.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1) + react: 19.2.1 optionalDependencies: '@types/react': 19.2.7 - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) '@radix-ui/rect@1.1.1': {} - '@rolldown/pluginutils@1.0.0-beta.47': {} + '@rolldown/pluginutils@1.0.0-beta.53': {} '@rollup/rollup-android-arm-eabi@4.53.3': optional: true @@ -2999,12 +2999,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17 '@tailwindcss/oxide-win32-x64-msvc': 4.1.17 - '@tailwindcss/vite@4.1.17(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))': + '@tailwindcss/vite@4.1.17(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: '@tailwindcss/node': 4.1.17 '@tailwindcss/oxide': 4.1.17 tailwindcss: 4.1.17 - vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) '@types/babel__core@7.20.5': dependencies: @@ -3043,14 +3043,14 @@ snapshots: dependencies: csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.48.0 - '@typescript-eslint/type-utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.48.0 + '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.48.1 + '@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.48.1 eslint: 9.39.1(jiti@2.6.1) graphemer: 1.4.0 ignore: 7.0.5 @@ -3060,41 +3060,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.48.0 - '@typescript-eslint/types': 8.48.0 - '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.48.0 + '@typescript-eslint/scope-manager': 8.48.1 + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.48.1 debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.48.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.48.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3) - '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3) + '@typescript-eslint/types': 8.48.1 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.48.0': + '@typescript-eslint/scope-manager@8.48.1': dependencies: - '@typescript-eslint/types': 8.48.0 - '@typescript-eslint/visitor-keys': 8.48.0 + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/visitor-keys': 8.48.1 - '@typescript-eslint/tsconfig-utils@8.48.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.48.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.48.0 - '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) @@ -3102,14 +3102,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.48.0': {} + '@typescript-eslint/types@8.48.1': {} - '@typescript-eslint/typescript-estree@8.48.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.48.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.48.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3) - '@typescript-eslint/types': 8.48.0 - '@typescript-eslint/visitor-keys': 8.48.0 + '@typescript-eslint/project-service': 8.48.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3) + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/visitor-keys': 8.48.1 debug: 4.4.3 minimatch: 9.0.5 semver: 7.7.3 @@ -3119,31 +3119,31 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.48.0 - '@typescript-eslint/types': 8.48.0 - '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.48.1 + '@typescript-eslint/types': 8.48.1 + '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.48.0': + '@typescript-eslint/visitor-keys@8.48.1': dependencies: - '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/types': 8.48.1 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@5.1.1(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))': + '@vitejs/plugin-react@5.1.2(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) - '@rolldown/pluginutils': 1.0.0-beta.47 + '@rolldown/pluginutils': 1.0.0-beta.53 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) transitivePeerDependencies: - supports-color @@ -3172,7 +3172,7 @@ snapshots: balanced-match@1.0.2: {} - baseline-browser-mapping@2.8.32: {} + baseline-browser-mapping@2.9.4: {} brace-expansion@1.1.12: dependencies: @@ -3183,17 +3183,17 @@ snapshots: dependencies: balanced-match: 1.0.2 - browserslist@4.28.0: + browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.8.32 - caniuse-lite: 1.0.30001757 - electron-to-chromium: 1.5.262 + baseline-browser-mapping: 2.9.4 + caniuse-lite: 1.0.30001759 + electron-to-chromium: 1.5.266 node-releases: 2.0.27 - update-browserslist-db: 1.1.4(browserslist@4.28.0) + update-browserslist-db: 1.2.2(browserslist@4.28.1) callsites@3.1.0: {} - caniuse-lite@1.0.30001757: {} + caniuse-lite@1.0.30001759: {} chalk@4.1.2: dependencies: @@ -3234,7 +3234,7 @@ snapshots: detect-node-es@1.1.0: {} - electron-to-chromium@1.5.262: {} + electron-to-chromium@1.5.266: {} enhanced-resolve@5.18.3: dependencies: @@ -3515,9 +3515,9 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@0.555.0(react@19.2.0): + lucide-react@0.556.0(react@19.2.1): dependencies: - react: 19.2.0 + react: 19.2.1 magic-string@0.30.21: dependencies: @@ -3537,10 +3537,10 @@ snapshots: natural-compare@1.4.0: {} - next-themes@0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + next-themes@0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) node-releases@2.0.27: {} @@ -3583,41 +3583,41 @@ snapshots: punycode@2.3.1: {} - react-dom@19.2.0(react@19.2.0): + react-dom@19.2.1(react@19.2.1): dependencies: - react: 19.2.0 + react: 19.2.1 scheduler: 0.27.0 react-refresh@0.18.0: {} - react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.0): + react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.1): dependencies: - react: 19.2.0 - react-style-singleton: 2.2.3(@types/react@19.2.7)(react@19.2.0) + react: 19.2.1 + react-style-singleton: 2.2.3(@types/react@19.2.7)(react@19.2.1) tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.7 - react-remove-scroll@2.7.2(@types/react@19.2.7)(react@19.2.0): + react-remove-scroll@2.7.2(@types/react@19.2.7)(react@19.2.1): dependencies: - react: 19.2.0 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.7)(react@19.2.0) - react-style-singleton: 2.2.3(@types/react@19.2.7)(react@19.2.0) + react: 19.2.1 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.7)(react@19.2.1) + react-style-singleton: 2.2.3(@types/react@19.2.7)(react@19.2.1) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.7)(react@19.2.0) - use-sidecar: 1.1.3(@types/react@19.2.7)(react@19.2.0) + use-callback-ref: 1.3.3(@types/react@19.2.7)(react@19.2.1) + use-sidecar: 1.1.3(@types/react@19.2.7)(react@19.2.1) optionalDependencies: '@types/react': 19.2.7 - react-style-singleton@2.2.3(@types/react@19.2.7)(react@19.2.0): + react-style-singleton@2.2.3(@types/react@19.2.7)(react@19.2.1): dependencies: get-nonce: 1.0.1 - react: 19.2.0 + react: 19.2.1 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.7 - react@19.2.0: {} + react@19.2.1: {} resolve-from@4.0.0: {} @@ -3692,10 +3692,10 @@ snapshots: shebang-regex@3.0.0: {} - sonner@2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + sonner@2.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) source-map-js@1.2.1: {} @@ -3728,12 +3728,12 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -3743,9 +3743,9 @@ snapshots: undici-types@7.16.0: {} - update-browserslist-db@1.1.4(browserslist@4.28.0): + update-browserslist-db@1.2.2(browserslist@4.28.1): dependencies: - browserslist: 4.28.0 + browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 @@ -3753,22 +3753,22 @@ snapshots: dependencies: punycode: 2.3.1 - use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.0): + use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.1): dependencies: - react: 19.2.0 + react: 19.2.1 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.7 - use-sidecar@1.1.3(@types/react@19.2.7)(react@19.2.0): + use-sidecar@1.1.3(@types/react@19.2.7)(react@19.2.1): dependencies: detect-node-es: 1.1.0 - react: 19.2.0 + react: 19.2.1 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.7 - vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2): + vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 56d0e95..d938d16 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -55,7 +55,7 @@ function App() { const [fetchHistory, setFetchHistory] = useState([]); const ITEMS_PER_PAGE = 50; - const CURRENT_VERSION = "6.6"; + const CURRENT_VERSION = "6.7"; const download = useDownload(); const metadata = useMetadata(); @@ -325,16 +325,16 @@ function App() { onToggleSelectAll={toggleSelectAll} onDownloadTrack={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position) => - lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, album_info.name, false, position) + lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, album_info.name, position) } onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId) => - cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, album_info.name, false, position, trackId) + cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, album_info.name, position, trackId) } onCheckAvailability={availability.checkAvailability} onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, album_info.name)} - onDownloadAll={() => download.handleDownloadAll(track_list, album_info.name)} + onDownloadAll={() => download.handleDownloadAll(track_list, undefined, true)} onDownloadSelected={() => - download.handleDownloadSelected(selectedTracks, track_list, album_info.name) + download.handleDownloadSelected(selectedTracks, track_list, undefined, true) } onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} @@ -391,10 +391,10 @@ function App() { onToggleSelectAll={toggleSelectAll} onDownloadTrack={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position) => - lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, playlist_info.owner.name, false, position) + lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, playlist_info.owner.name, position) } onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId) => - cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, playlist_info.owner.name, false, position, trackId) + cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, playlist_info.owner.name, position, trackId) } onCheckAvailability={availability.checkAvailability} onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, playlist_info.owner.name)} @@ -462,17 +462,17 @@ function App() { onToggleTrack={toggleTrackSelection} onToggleSelectAll={toggleSelectAll} onDownloadTrack={download.handleDownloadTrack} - onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, isArtistDiscography, position) => - lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, artist_info.name, isArtistDiscography, position) + onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position) => + lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, artist_info.name, position) } - onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, isArtistDiscography, position, trackId) => - cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, artist_info.name, isArtistDiscography, position, trackId) + onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId) => + cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, artist_info.name, position, trackId) } onCheckAvailability={availability.checkAvailability} - onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, artist_info.name, true)} - onDownloadAll={() => download.handleDownloadAll(track_list, artist_info.name, true)} + onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, artist_info.name)} + onDownloadAll={() => download.handleDownloadAll(track_list, artist_info.name)} onDownloadSelected={() => - download.handleDownloadSelected(selectedTracks, track_list, artist_info.name, true) + download.handleDownloadSelected(selectedTracks, track_list, artist_info.name) } onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} diff --git a/frontend/src/components/Settings.tsx b/frontend/src/components/Settings.tsx index 0979540..2946878 100644 --- a/frontend/src/components/Settings.tsx +++ b/frontend/src/components/Settings.tsx @@ -16,12 +16,11 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Checkbox } from "@/components/ui/checkbox"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; + import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Settings as SettingsIcon, FolderOpen, Save, RotateCcw, Info, X, Volume2 } from "lucide-react"; import { Switch } from "@/components/ui/switch"; -import { getSettings, getSettingsWithDefaults, saveSettings, resetToDefaultSettings, applyThemeMode, type Settings as SettingsType } from "@/lib/settings"; +import { getSettings, getSettingsWithDefaults, saveSettings, resetToDefaultSettings, applyThemeMode, FOLDER_PRESETS, FILENAME_PRESETS, TEMPLATE_VARIABLES, type Settings as SettingsType, type FolderPreset, type FilenamePreset } from "@/lib/settings"; import { themes, applyTheme } from "@/lib/themes"; import { SelectFolder } from "../../wailsjs/go/main/App"; @@ -325,81 +324,102 @@ export function Settings() { {/* Right Column */}
- {/* Filename Format */} + {/* Folder Structure */}
- - setTempSettings(prev => ({ ...prev, filenameFormat: value as any }))} +
+ + + + + + +

Variables: {TEMPLATE_VARIABLES.map(v => v.key).join(", ")}

+
+
+
+ + {tempSettings.folderPreset === "custom" && ( + setTempSettings(prev => ({ ...prev, folderTemplate: e.target.value }))} + placeholder="{artist}/{album}" + className="h-9 text-sm" + /> + )} + {tempSettings.folderTemplate && ( +

+ Preview: {tempSettings.folderTemplate.replace(/\{artist\}/g, "Taylor Swift").replace(/\{album\}/g, "1989").replace(/\{year\}/g, "2014")}/ +

+ )}
- {/* Folder Settings */} + {/* Filename Format */}
-

Folder Settings

- setTempSettings(prev => ({ ...prev, trackNumber: checked as boolean }))} - /> - + - -

Adds track numbers to filenames. Uses album track numbers when Album Subfolder is enabled, otherwise uses playlist position

+ +

Variables: {TEMPLATE_VARIABLES.map(v => v.key).join(", ")}

-
- setTempSettings(prev => ({ ...prev, artistSubfolder: checked as boolean }))} + + {tempSettings.filenamePreset === "custom" && ( + setTempSettings(prev => ({ ...prev, filenameTemplate: e.target.value }))} + placeholder="{track}. {title}" + className="h-9 text-sm" /> - - - - - - -

Playlist only

-
-
-
-
- setTempSettings(prev => ({ ...prev, albumSubfolder: checked as boolean }))} - /> - - - - - - -

Playlist & Discography

-
-
-
+ )} + {tempSettings.filenameTemplate && ( +

+ Preview: {tempSettings.filenameTemplate.replace(/\{artist\}/g, "Taylor Swift").replace(/\{title\}/g, "Shake It Off").replace(/\{track\}/g, "01").replace(/\{year\}/g, "2014")}.flac +

+ )}
diff --git a/frontend/src/components/SettingsPage.tsx b/frontend/src/components/SettingsPage.tsx index 1ef6014..e9da0cb 100644 --- a/frontend/src/components/SettingsPage.tsx +++ b/frontend/src/components/SettingsPage.tsx @@ -9,12 +9,11 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Checkbox } from "@/components/ui/checkbox"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; + import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { FolderOpen, Save, RotateCcw, Info, Volume2 } from "lucide-react"; import { Switch } from "@/components/ui/switch"; -import { getSettings, getSettingsWithDefaults, saveSettings, resetToDefaultSettings, applyThemeMode, applyFont, FONT_OPTIONS, type Settings as SettingsType, type FontFamily } from "@/lib/settings"; +import { getSettings, getSettingsWithDefaults, saveSettings, resetToDefaultSettings, applyThemeMode, applyFont, FONT_OPTIONS, FOLDER_PRESETS, FILENAME_PRESETS, TEMPLATE_VARIABLES, type Settings as SettingsType, type FontFamily, type FolderPreset, type FilenamePreset } from "@/lib/settings"; import { themes, applyTheme } from "@/lib/themes"; import { SelectFolder } from "../../wailsjs/go/main/App"; import { toastWithSound as toast } from "@/lib/toast-with-sound"; @@ -237,81 +236,102 @@ export function SettingsPage() { {/* Right Column */}
- {/* Filename Format */} + {/* Folder Structure */}
- - setTempSettings(prev => ({ ...prev, filenameFormat: value as any }))} +
+ + + + + + +

Variables: {TEMPLATE_VARIABLES.map(v => v.key).join(", ")}

+
+
+
+ + {tempSettings.folderPreset === "custom" && ( + setTempSettings(prev => ({ ...prev, folderTemplate: e.target.value }))} + placeholder="{artist}/{album}" + className="h-9 text-sm" + /> + )} + {tempSettings.folderTemplate && ( +

+ Preview: {tempSettings.folderTemplate.replace(/\{artist\}/g, "Taylor Swift").replace(/\{album\}/g, "1989").replace(/\{year\}/g, "2014")}/ +

+ )}
- {/* Folder Settings */} + {/* Filename Format */}
-

Folder Settings

- setTempSettings(prev => ({ ...prev, trackNumber: checked as boolean }))} - /> - + - -

Adds track numbers to filenames

+ +

Variables: {TEMPLATE_VARIABLES.map(v => v.key).join(", ")}

-
- setTempSettings(prev => ({ ...prev, artistSubfolder: checked as boolean }))} + + {tempSettings.filenamePreset === "custom" && ( + setTempSettings(prev => ({ ...prev, filenameTemplate: e.target.value }))} + placeholder="{track}. {title}" + className="h-9 text-sm" /> - - - - - - -

Playlist only

-
-
-
-
- setTempSettings(prev => ({ ...prev, albumSubfolder: checked as boolean }))} - /> - - - - - - -

Playlist & Discography

-
-
-
+ )} + {tempSettings.filenameTemplate && ( +

+ Preview: {tempSettings.filenameTemplate.replace(/\{artist\}/g, "Taylor Swift").replace(/\{title\}/g, "Shake It Off").replace(/\{track\}/g, "01").replace(/\{year\}/g, "2014")}.flac +

+ )}
diff --git a/frontend/src/components/TrackList.tsx b/frontend/src/components/TrackList.tsx index 3a85807..4ea3196 100644 --- a/frontend/src/components/TrackList.tsx +++ b/frontend/src/components/TrackList.tsx @@ -49,7 +49,7 @@ interface TrackListProps { downloadingCoverTrack?: string | null; onToggleTrack: (isrc: string) => void; onToggleSelectAll: (tracks: TrackMetadata[]) => void; - onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, isArtistDiscography?: boolean) => void; + onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number) => void; onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number) => void; onCheckAvailability?: (spotifyId: string, isrc?: string) => void; onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void; @@ -301,7 +301,7 @@ export function TrackList({