diff --git a/app.go b/app.go index fe534bf..fc91334 100644 --- a/app.go +++ b/app.go @@ -5,10 +5,12 @@ import ( "encoding/base64" "encoding/json" "fmt" + "io" "os" "path/filepath" + "net/http" "strings" "time" @@ -51,10 +53,11 @@ func (a *App) shutdown(ctx context.Context) { } type SpotifyMetadataRequest struct { - URL string `json:"url"` - Batch bool `json:"batch"` - Delay float64 `json:"delay"` - Timeout float64 `json:"timeout"` + URL string `json:"url"` + Batch bool `json:"batch"` + Delay float64 `json:"delay"` + Timeout float64 `json:"timeout"` + Separator string `json:"separator,omitempty"` } type DownloadRequest struct { @@ -91,6 +94,7 @@ type DownloadRequest struct { UseFirstArtistOnly bool `json:"use_first_artist_only,omitempty"` UseSingleGenre bool `json:"use_single_genre,omitempty"` EmbedGenre bool `json:"embed_genre,omitempty"` + Separator string `json:"separator,omitempty"` } type DownloadResponse struct { @@ -342,7 +346,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) { if req.EmbedLyrics { go func() { client := backend.NewLyricsClient() - resp, _, err := client.FetchLyricsAllSources(req.SpotifyID, req.TrackName, req.ArtistName, req.Duration) + resp, _, err := client.FetchLyricsAllSources(req.SpotifyID, req.TrackName, req.ArtistName, req.AlbumName, req.Duration) if err == nil && resp != nil && len(resp.Lines) > 0 { lrc := client.ConvertToLRC(resp, req.TrackName, req.ArtistName) lyricsChan <- lrc @@ -402,10 +406,6 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) { } filename, err = downloader.DownloadTrackWithISRC(isrc, req.SpotifyID, req.OutputDir, quality, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre) - case "deezer": - downloader := backend.NewDeezerDownloader() - filename, err = downloader.Download(req.SpotifyID, req.OutputDir, req.FilenameFormat, req.PlaylistName, req.PlaylistOwner, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre) - default: return DownloadResponse{ Success: false, @@ -548,6 +548,18 @@ func (a *App) OpenFolder(path string) error { return nil } +func (a *App) OpenConfigFolder() error { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %v", err) + } + configDir := filepath.Join(homeDir, ".spotiflac") + if err := os.MkdirAll(configDir, 0755); err != nil { + return fmt.Errorf("failed to create config directory: %v", err) + } + return backend.OpenFolderInExplorer(configDir) +} + func (a *App) SelectFolder(defaultPath string) (string, error) { return backend.SelectFolderDialog(a.ctx, defaultPath) } @@ -660,6 +672,52 @@ func (a *App) ExportFailedDownloads() (string, error) { return fmt.Sprintf("Successfully exported %d failed downloads to %s", count, path), nil } +func (a *App) CheckAPIStatus(apiType string, apiURL string) bool { + var checkURL string + if apiType == "tidal" { + checkURL = fmt.Sprintf("%s/track/?id=441821360&quality=HI_RES_LOSSLESS", apiURL) + } else if apiType == "qobuz" { + checkURL = fmt.Sprintf("%s/api/stream?trackId=360735657&format_id=27", apiURL) + } else if apiType == "qbz" { + checkURL = fmt.Sprintf("%s/api/track/360735657?quality=27", apiURL) + } else if apiType == "amazon" { + checkURL = fmt.Sprintf("%s/status", apiURL) + } else { + checkURL = apiURL + } + + client := &http.Client{Timeout: 15 * time.Second} + req, err := http.NewRequest("GET", checkURL, nil) + if err != nil { + return false + } + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36") + + maxRetries := 3 + for i := 0; i < maxRetries; i++ { + resp, err := client.Do(req) + if err == nil { + statusCode := resp.StatusCode + if apiType == "amazon" && statusCode == 200 { + body, readErr := io.ReadAll(resp.Body) + resp.Body.Close() + if readErr == nil && strings.Contains(string(body), `"amazonMusic":"up"`) { + return true + } + } else { + resp.Body.Close() + if statusCode == 200 { + return true + } + } + } + if i < maxRetries-1 { + time.Sleep(1 * time.Second) + } + } + return false +} + func (a *App) Quit() { panic("quit") @@ -1088,23 +1146,6 @@ func (a *App) RenameFileTo(oldPath, newName string) error { return os.Rename(oldPath, newPath) } -func (a *App) UploadImage(filePath string) (string, error) { - return backend.UploadToSendNow(filePath) -} - -func (a *App) UploadImageBytes(filename string, base64Data string) (string, error) { - - if idx := strings.Index(base64Data, ","); idx != -1 { - base64Data = base64Data[idx+1:] - } - - data, err := base64.StdEncoding.DecodeString(base64Data) - if err != nil { - return "", fmt.Errorf("failed to decode base64: %v", err) - } - return backend.UploadBytesToSendNow(filename, data) -} - func (a *App) SelectImageVideo() ([]string, error) { return backend.SelectImageVideoDialog(a.ctx) } @@ -1370,10 +1411,6 @@ func (a *App) CheckFFmpegInstalled() (bool, error) { return backend.IsFFmpegInstalled() } -func (a *App) GetOSInfo() (string, error) { - return backend.GetOSInfo() -} - func (a *App) CreateM3U8File(m3u8Name string, outputDir string, filePaths []string) error { if len(filePaths) == 0 { return nil diff --git a/backend/cover.go b/backend/cover.go index f7142c9..843dae1 100644 --- a/backend/cover.go +++ b/backend/cover.go @@ -117,7 +117,7 @@ func buildCoverFilename(trackName, artistName, albumName, albumArtist, releaseDa } } - return filename + ".cover.jpg" + return filename + ".jpg" } func convertSmallToMedium(imageURL string) string { diff --git a/backend/deezer.go b/backend/deezer.go deleted file mode 100644 index 13f9d13..0000000 --- a/backend/deezer.go +++ /dev/null @@ -1,273 +0,0 @@ -package backend - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "strings" - "time" -) - -type DeezerDownloader struct { - client *http.Client -} - -func NewDeezerDownloader() *DeezerDownloader { - return &DeezerDownloader{ - client: &http.Client{ - Timeout: 300 * time.Second, - }, - } -} - -type YoinkifyRequest struct { - URL string `json:"url"` - Format string `json:"format"` - GenreSource string `json:"genreSource"` -} - -func (d *DeezerDownloader) DownloadFromYoinkify(spotifyURL, outputDir string) (string, error) { - apiURL := "https://yoinkify.lol/api/download" - - payload := YoinkifyRequest{ - URL: spotifyURL, - Format: "flac", - GenreSource: "spotify", - } - - jsonData, err := json.Marshal(payload) - if err != nil { - return "", err - } - - req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData)) - if err != nil { - return "", err - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36") - - fmt.Printf("Fetching from Deezer API (Yoinkify)...\n") - resp, err := d.client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return "", fmt.Errorf("Deezer API returned status %d", resp.StatusCode) - } - - tempFileName := fmt.Sprintf("deezer_%d.flac", time.Now().UnixNano()) - filePath := filepath.Join(outputDir, tempFileName) - - out, err := os.Create(filePath) - if err != nil { - return "", err - } - defer out.Close() - - fmt.Printf("Downloading track from Deezer...\n") - pw := NewProgressWriter(out) - _, err = io.Copy(pw, resp.Body) - if err != nil { - out.Close() - os.Remove(filePath) - return "", err - } - - fmt.Printf("\rDownloaded: %.2f MB (Complete)\n", float64(pw.GetTotal())/(1024*1024)) - return filePath, nil -} - -func (d *DeezerDownloader) Download(spotifyID, outputDir, filenameFormat, playlistName, playlistOwner string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) { - - if outputDir != "." { - if err := os.MkdirAll(outputDir, 0755); err != nil { - return "", fmt.Errorf("failed to create output directory: %w", err) - } - } - - if spotifyTrackName != "" && spotifyArtistName != "" { - filenameArtist := spotifyArtistName - filenameAlbumArtist := spotifyAlbumArtist - if useFirstArtistOnly { - filenameArtist = GetFirstArtist(spotifyArtistName) - filenameAlbumArtist = GetFirstArtist(spotifyAlbumArtist) - } - expectedFilename := BuildExpectedFilename(spotifyTrackName, filenameArtist, spotifyAlbumName, filenameAlbumArtist, spotifyReleaseDate, filenameFormat, playlistName, playlistOwner, includeTrackNumber, position, spotifyDiscNumber, false) - expectedPath := filepath.Join(outputDir, expectedFilename) - - if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 0 { - fmt.Printf("File already exists: %s (%.2f MB)\n", expectedPath, float64(fileInfo.Size())/(1024*1024)) - return "EXISTS:" + expectedPath, nil - } - } - - type mbResult struct { - ISRC string - Metadata Metadata - } - - metaChan := make(chan mbResult, 1) - if (embedGenre || true) && spotifyURL != "" { - go func() { - res := mbResult{} - var isrc string - parts := strings.Split(spotifyURL, "/") - if len(parts) > 0 { - sID := strings.Split(parts[len(parts)-1], "?")[0] - if sID != "" { - client := NewSongLinkClient() - if val, err := client.GetISRC(sID); err == nil { - isrc = val - } - } - } - res.ISRC = isrc - if isrc != "" && embedGenre { - fmt.Println("Fetching MusicBrainz metadata...") - if fetchedMeta, err := FetchMusicBrainzMetadata(isrc, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useSingleGenre, embedGenre); err == nil { - res.Metadata = fetchedMeta - fmt.Println("✓ MusicBrainz metadata fetched") - } else { - fmt.Printf("Warning: Failed to fetch MusicBrainz metadata: %v\n", err) - } - } - metaChan <- res - }() - } else { - close(metaChan) - } - - filePath, err := d.DownloadFromYoinkify(spotifyURL, outputDir) - if err != nil { - return "", err - } - - var isrc string - var mbMeta Metadata - if spotifyURL != "" { - result := <-metaChan - isrc = result.ISRC - mbMeta = result.Metadata - } - - if spotifyTrackName != "" && spotifyArtistName != "" { - safeArtist := sanitizeFilename(spotifyArtistName) - safeAlbumArtist := sanitizeFilename(spotifyAlbumArtist) - - if useFirstArtistOnly { - safeArtist = sanitizeFilename(GetFirstArtist(spotifyArtistName)) - safeAlbumArtist = sanitizeFilename(GetFirstArtist(spotifyAlbumArtist)) - } - - safeTitle := sanitizeFilename(spotifyTrackName) - safeAlbum := sanitizeFilename(spotifyAlbumName) - - year := "" - if len(spotifyReleaseDate) >= 4 { - year = spotifyReleaseDate[:4] - } - - var newFilename string - - if strings.Contains(filenameFormat, "{") { - newFilename = filenameFormat - newFilename = strings.ReplaceAll(newFilename, "{title}", safeTitle) - newFilename = strings.ReplaceAll(newFilename, "{artist}", safeArtist) - newFilename = strings.ReplaceAll(newFilename, "{album}", safeAlbum) - newFilename = strings.ReplaceAll(newFilename, "{album_artist}", safeAlbumArtist) - newFilename = strings.ReplaceAll(newFilename, "{year}", year) - newFilename = strings.ReplaceAll(newFilename, "{date}", SanitizeFilename(spotifyReleaseDate)) - - if spotifyDiscNumber > 0 { - newFilename = strings.ReplaceAll(newFilename, "{disc}", fmt.Sprintf("%d", spotifyDiscNumber)) - } else { - newFilename = strings.ReplaceAll(newFilename, "{disc}", "") - } - - if position > 0 { - newFilename = strings.ReplaceAll(newFilename, "{track}", fmt.Sprintf("%02d", position)) - } else { - newFilename = strings.ReplaceAll(newFilename, "{track}", "") - } - } else { - switch filenameFormat { - case "artist-title": - newFilename = fmt.Sprintf("%s - %s", safeArtist, safeTitle) - case "title": - newFilename = safeTitle - default: - newFilename = fmt.Sprintf("%s - %s", safeTitle, safeArtist) - } - - if includeTrackNumber && position > 0 { - newFilename = fmt.Sprintf("%02d. %s", position, newFilename) - } - } - - ext := ".flac" - newFilename = newFilename + ext - newFilePath := filepath.Join(outputDir, newFilename) - - if err := os.Rename(filePath, newFilePath); err != nil { - fmt.Printf("Warning: Failed to rename file: %v\n", err) - } else { - filePath = newFilePath - fmt.Printf("Renamed to: %s\n", newFilename) - } - } - - fmt.Println("Embedding Spotify metadata...") - - coverPath := "" - if spotifyCoverURL != "" { - coverPath = filePath + ".cover.jpg" - coverClient := NewCoverClient() - if err := coverClient.DownloadCoverToPath(spotifyCoverURL, coverPath, embedMaxQualityCover); err != nil { - fmt.Printf("Warning: Failed to download Spotify cover: %v\n", err) - coverPath = "" - } else { - defer os.Remove(coverPath) - fmt.Println("Spotify cover downloaded") - } - } - - trackNumberToEmbed := spotifyTrackNumber - if trackNumberToEmbed == 0 { - trackNumberToEmbed = 1 - } - - metadata := Metadata{ - Title: spotifyTrackName, - Artist: spotifyArtistName, - Album: spotifyAlbumName, - AlbumArtist: spotifyAlbumArtist, - Date: spotifyReleaseDate, - TrackNumber: trackNumberToEmbed, - TotalTracks: spotifyTotalTracks, - DiscNumber: spotifyDiscNumber, - TotalDiscs: spotifyTotalDiscs, - URL: spotifyURL, - Copyright: spotifyCopyright, - Publisher: spotifyPublisher, - Description: "https://github.com/afkarxyz/SpotiFLAC", - ISRC: isrc, - Genre: mbMeta.Genre, - } - - if err := EmbedMetadataToConvertedFile(filePath, metadata, coverPath); err != nil { - fmt.Printf("Warning: Failed to embed metadata: %v\n", err) - } else { - fmt.Println("Metadata embedded successfully") - } - - fmt.Println("Done") - fmt.Println("✓ Downloaded successfully from Deezer") - return filePath, nil -} diff --git a/backend/ffmpeg.go b/backend/ffmpeg.go index a84bfa9..e9cdc75 100644 --- a/backend/ffmpeg.go +++ b/backend/ffmpeg.go @@ -81,28 +81,6 @@ func GetFFmpegPath() (string, error) { return localPath, nil } - if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" { - homebrewPath := "/opt/homebrew/bin/" + ffmpegName - if _, err := os.Stat(homebrewPath); err == nil { - return homebrewPath, nil - } - } else if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" { - homebrewPath := "/usr/local/bin/" + ffmpegName - if _, err := os.Stat(homebrewPath); err == nil { - return homebrewPath, nil - } - } - - if runtime.GOOS != "windows" { - path, err := exec.Command("which", ffmpegName).Output() - if err == nil { - trimmed := strings.TrimSpace(string(path)) - if trimmed != "" { - return trimmed, nil - } - } - } - path, err := exec.LookPath(ffmpegName) if err == nil { return path, nil @@ -127,28 +105,6 @@ func GetFFprobePath() (string, error) { return localPath, nil } - if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" { - homebrewPath := "/opt/homebrew/bin/" + ffprobeName - if _, err := os.Stat(homebrewPath); err == nil { - return homebrewPath, nil - } - } else if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" { - homebrewPath := "/usr/local/bin/" + ffprobeName - if _, err := os.Stat(homebrewPath); err == nil { - return homebrewPath, nil - } - } - - if runtime.GOOS != "windows" { - path, err := exec.Command("which", ffprobeName).Output() - if err == nil { - trimmed := strings.TrimSpace(string(path)) - if trimmed != "" { - return trimmed, nil - } - } - } - path, err := exec.LookPath(ffprobeName) if err == nil { return path, nil @@ -295,7 +251,7 @@ func downloadAndExtract(url, destDir string, progressCallback func(int), progres if err != nil { return fmt.Errorf("failed to create request: %w", err) } - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36") resp, err := client.Do(req) if err != nil { diff --git a/backend/filename.go b/backend/filename.go index 5d2be9b..1e6b3d4 100644 --- a/backend/filename.go +++ b/backend/filename.go @@ -1,7 +1,9 @@ package backend import ( + "encoding/json" "fmt" + "os" "path/filepath" "regexp" "strings" @@ -133,10 +135,34 @@ func GetFirstArtist(artistString string) string { } func NormalizePath(folderPath string) string { - return strings.ReplaceAll(folderPath, "/", string(filepath.Separator)) } +func GetSeparator() string { + dir, err := GetFFmpegDir() + if err != nil { + return "; " + } + configPath := filepath.Join(dir, "config.json") + data, err := os.ReadFile(configPath) + if err != nil { + return "; " + } + + var settings map[string]interface{} + if err := json.Unmarshal(data, &settings); err == nil { + if sep, ok := settings["separator"].(string); ok { + if sep == "comma" { + return ", " + } + if sep == "semicolon" { + return "; " + } + } + } + return "; " +} + func SanitizeFolderPath(folderPath string) string { normalizedPath := strings.ReplaceAll(folderPath, "/", string(filepath.Separator)) diff --git a/backend/lyrics.go b/backend/lyrics.go index c007bd5..3feba0d 100644 --- a/backend/lyrics.go +++ b/backend/lyrics.go @@ -37,17 +37,6 @@ type LyricsResponse struct { Lines []LyricsLine `json:"lines"` } -type SpotifyLyricsLine struct { - TimeTag string `json:"timeTag"` - Words string `json:"words"` -} - -type SpotifyLyricsAPIResponse struct { - Error bool `json:"error"` - SyncType string `json:"syncType"` - Lines []SpotifyLyricsLine `json:"lines"` -} - type LyricsDownloadRequest struct { SpotifyID string `json:"spotify_id"` TrackName string `json:"track_name"` @@ -81,12 +70,16 @@ func NewLyricsClient() *LyricsClient { } } -func (c *LyricsClient) FetchLyricsWithMetadata(trackName, artistName string, duration int) (*LyricsResponse, error) { +func (c *LyricsClient) FetchLyricsWithMetadata(trackName, artistName, albumName string, duration int) (*LyricsResponse, error) { apiURL := fmt.Sprintf("https://lrclib.net/api/get?artist_name=%s&track_name=%s", url.QueryEscape(artistName), url.QueryEscape(trackName)) + if albumName != "" { + apiURL = fmt.Sprintf("%s&album_name=%s", apiURL, url.QueryEscape(albumName)) + } + if duration > 0 { apiURL = fmt.Sprintf("%s&duration=%d", apiURL, duration) } @@ -111,6 +104,10 @@ func (c *LyricsClient) FetchLyricsWithMetadata(trackName, artistName string, dur return nil, fmt.Errorf("failed to parse LRCLIB response: %v", err) } + if lrcLibResp.SyncedLyrics == "" && lrcLibResp.PlainLyrics == "" { + return nil, fmt.Errorf("LRCLIB returned empty lyrics") + } + return c.convertLRCLibToLyricsResponse(&lrcLibResp), nil } @@ -174,8 +171,10 @@ func lrcTimestampToMs(timestamp string) int64 { } func (c *LyricsClient) FetchLyricsFromLRCLibSearch(trackName, artistName string) (*LyricsResponse, error) { - query := fmt.Sprintf("%s %s", artistName, trackName) - apiURL := fmt.Sprintf("https://lrclib.net/api/search?q=%s", url.QueryEscape(query)) + + apiURL := fmt.Sprintf("https://lrclib.net/api/search?artist_name=%s&track_name=%s", + url.QueryEscape(artistName), + url.QueryEscape(trackName)) resp, err := c.httpClient.Get(apiURL) if err != nil { @@ -201,79 +200,35 @@ func (c *LyricsClient) FetchLyricsFromLRCLibSearch(trackName, artistName string) return nil, fmt.Errorf("no results found") } - var best *LRCLibResponse + var bestSynced *LRCLibResponse + var bestPlain *LRCLibResponse for i := range results { - if results[i].SyncedLyrics != "" { - best = &results[i] - break + if results[i].SyncedLyrics != "" && bestSynced == nil { + bestSynced = &results[i] } - if best == nil && results[i].PlainLyrics != "" { - best = &results[i] + if results[i].PlainLyrics != "" && bestPlain == nil { + bestPlain = &results[i] + } + if bestSynced != nil { + break } } + best := bestSynced + if best == nil { + best = bestPlain + } if best == nil { best = &results[0] } + if best.SyncedLyrics == "" && best.PlainLyrics == "" { + return nil, fmt.Errorf("no lyrics found in search results") + } + return c.convertLRCLibToLyricsResponse(best), nil } -func (c *LyricsClient) FetchLyricsFromSpotifyAPI(spotifyID string) (*LyricsResponse, error) { - if spotifyID == "" { - return nil, fmt.Errorf("spotify ID is empty") - } - - apiURL := fmt.Sprintf("https://spotify-lyrics-api-pi.vercel.app/?trackid=%s&format=lrc", spotifyID) - - resp, err := c.httpClient.Get(apiURL) - if err != nil { - return nil, fmt.Errorf("failed to fetch from Spotify Lyrics API: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, fmt.Errorf("Spotify Lyrics API returned status %d", resp.StatusCode) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read Spotify Lyrics API response: %v", err) - } - - var apiResp SpotifyLyricsAPIResponse - if err := json.Unmarshal(body, &apiResp); err != nil { - return nil, fmt.Errorf("failed to parse Spotify Lyrics API response: %v", err) - } - - if apiResp.Error { - return nil, fmt.Errorf("Spotify Lyrics API returned error") - } - - result := &LyricsResponse{ - Error: false, - SyncType: apiResp.SyncType, - Lines: []LyricsLine{}, - } - - for _, line := range apiResp.Lines { - if line.TimeTag == "" && line.Words == "" { - continue - } - ms := lrcTimestampToMs(line.TimeTag) - result.Lines = append(result.Lines, LyricsLine{ - StartTimeMs: fmt.Sprintf("%d", ms), - Words: line.Words, - }) - } - - if len(result.Lines) == 0 { - return nil, fmt.Errorf("Spotify Lyrics API returned empty lines") - } - - return result, nil -} - func simplifyTrackName(name string) string { if idx := strings.Index(name, "("); idx > 0 { @@ -286,41 +241,88 @@ func simplifyTrackName(name string) string { return name } -func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName string, duration int) (*LyricsResponse, string, error) { +func isSynced(resp *LyricsResponse) bool { + return resp != nil && !resp.Error && resp.SyncType == "LINE_SYNCED" && len(resp.Lines) > 0 +} - resp, err := c.FetchLyricsFromSpotifyAPI(spotifyID) - if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 { - return resp, "Spotify", nil - } - fmt.Printf(" Spotify Lyrics API: %v\n", err) +func hasLyrics(resp *LyricsResponse) bool { + return resp != nil && !resp.Error && len(resp.Lines) > 0 +} - resp, err = c.FetchLyricsWithMetadata(trackName, artistName, duration) - if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 { - return resp, "LRCLIB", nil - } - fmt.Printf(" LRCLIB exact: %v\n", err) +func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName, albumName string, duration int) (*LyricsResponse, string, error) { - resp, err = c.FetchLyricsFromLRCLibSearch(trackName, artistName) - if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 { - return resp, "LRCLIB Search", nil + var unsyncedFallback *LyricsResponse + var unsyncedSource string + + check := func(resp *LyricsResponse, err error, source string) (*LyricsResponse, string, bool) { + if err != nil || resp == nil || resp.Error || len(resp.Lines) == 0 { + return nil, "", false + } + if isSynced(resp) { + return resp, source, true + } + + if unsyncedFallback == nil { + unsyncedFallback = resp + unsyncedSource = source + } + return nil, "", false } - fmt.Printf(" LRCLIB search: %v\n", err) + + var resp *LyricsResponse + var src string + var found bool + + resp, _ = c.FetchLyricsWithMetadata(trackName, artistName, albumName, duration) + resp, src, found = check(resp, nil, "LRCLIB") + if found { + fmt.Printf(" [LRCLIB] Synced found via exact match (with album)\n") + return resp, src, nil + } + fmt.Printf(" LRCLIB exact (with album): no synced\n") + + if albumName != "" { + resp, _ = c.FetchLyricsWithMetadata(trackName, artistName, "", duration) + resp, src, found = check(resp, nil, "LRCLIB (no album)") + if found { + fmt.Printf(" [LRCLIB] Synced found via exact match (no album)\n") + return resp, src, nil + } + fmt.Printf(" LRCLIB exact (no album): no synced\n") + } + + resp, _ = c.FetchLyricsFromLRCLibSearch(trackName, artistName) + resp, src, found = check(resp, nil, "LRCLIB Search") + if found { + fmt.Printf(" [LRCLIB] Synced found via search\n") + return resp, src, nil + } + fmt.Printf(" LRCLIB search: no synced\n") simplifiedTrack := simplifyTrackName(trackName) if simplifiedTrack != trackName { fmt.Printf(" Trying simplified name: %s\n", simplifiedTrack) - resp, err = c.FetchLyricsWithMetadata(simplifiedTrack, artistName, duration) - if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 { - return resp, "LRCLIB (simplified)", nil + resp, _ = c.FetchLyricsWithMetadata(simplifiedTrack, artistName, albumName, duration) + resp, src, found = check(resp, nil, "LRCLIB (simplified)") + if found { + fmt.Printf(" [LRCLIB] Synced found via simplified exact\n") + return resp, src, nil } - resp, err = c.FetchLyricsFromLRCLibSearch(simplifiedTrack, artistName) - if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 { - return resp, "LRCLIB Search (simplified)", nil + resp, _ = c.FetchLyricsFromLRCLibSearch(simplifiedTrack, artistName) + resp, src, found = check(resp, nil, "LRCLIB Search (simplified)") + if found { + fmt.Printf(" [LRCLIB] Synced found via simplified search\n") + return resp, src, nil } } + if unsyncedFallback != nil { + fmt.Printf(" [LRCLIB] No synced found, using unsynced from: %s\n", unsyncedSource) + return unsyncedFallback, unsyncedSource + " (unsynced)", nil + } + return nil, "", fmt.Errorf("lyrics not found in any source") } @@ -472,25 +474,6 @@ func (c *LyricsClient) DownloadLyrics(req LyricsDownloadRequest) (*LyricsDownloa outputDir = NormalizePath(outputDir) } - safeArtist := sanitizeFilename(req.AlbumArtist) - if safeArtist == "" { - safeArtist = sanitizeFilename(req.ArtistName) - } - safeAlbum := sanitizeFilename(req.AlbumName) - - if safeArtist != "" && safeAlbum != "" { - artistAlbumPath := filepath.Join(outputDir, safeArtist, safeAlbum) - if info, err := os.Stat(artistAlbumPath); err == nil && info.IsDir() { - outputDir = artistAlbumPath - } else { - - artistPath := filepath.Join(outputDir, safeArtist) - if info, err := os.Stat(artistPath); err == nil && info.IsDir() { - outputDir = artistPath - } - } - } - if err := os.MkdirAll(outputDir, 0755); err != nil { return &LyricsDownloadResponse{ Success: false, @@ -524,7 +507,7 @@ func (c *LyricsClient) DownloadLyrics(req LyricsDownloadRequest) (*LyricsDownloa } } - lyrics, _, err := c.FetchLyricsAllSources(req.SpotifyID, req.TrackName, req.ArtistName, audioDuration) + lyrics, _, err := c.FetchLyricsAllSources(req.SpotifyID, req.TrackName, req.ArtistName, req.AlbumName, audioDuration) if err != nil { return &LyricsDownloadResponse{ Success: false, diff --git a/backend/musicbrainz.go b/backend/musicbrainz.go index 63e77b0..84ab114 100644 --- a/backend/musicbrainz.go +++ b/backend/musicbrainz.go @@ -146,7 +146,7 @@ func FetchMusicBrainzMetadata(isrc, title, artist, album string, useSingleGenre if len(genres) > 5 { genres = genres[:5] } - meta.Genre = strings.Join(genres, "; ") + meta.Genre = strings.Join(genres, GetSeparator()) } } diff --git a/backend/qobuz.go b/backend/qobuz.go index ad438fb..7dffe75 100644 --- a/backend/qobuz.go +++ b/backend/qobuz.go @@ -118,8 +118,15 @@ func (q *QobuzDownloader) searchByISRC(isrc string) (*QobuzTrack, error) { return &searchResp.Tracks.Items[0], nil } +func buildQobuzAPIURL(apiBase string, trackID int64, quality string) string { + if strings.Contains(apiBase, "qbz.afkarxyz.fun") { + return fmt.Sprintf("%s%d?quality=%s", apiBase, trackID, quality) + } + return fmt.Sprintf("%s%d&quality=%s", apiBase, trackID, quality) +} + func (q *QobuzDownloader) DownloadFromStandard(apiBase string, trackID int64, quality string) (string, error) { - apiURL := fmt.Sprintf("%s%d&quality=%s", apiBase, trackID, quality) + apiURL := buildQobuzAPIURL(apiBase, trackID, quality) resp, err := q.client.Get(apiURL) if err != nil { return "", err @@ -167,6 +174,7 @@ func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string, allowFal standardAPIs := []string{ "https://dab.yeet.su/api/stream?trackId=", "https://dabmusic.xyz/api/stream?trackId=", + "https://qbz.afkarxyz.fun/api/track/", } downloadFunc := func(qual string) (string, error) { diff --git a/backend/spotfetch.go b/backend/spotfetch.go index 404ce76..440154d 100644 --- a/backend/spotfetch.go +++ b/backend/spotfetch.go @@ -665,7 +665,7 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter for _, artist := range albumArtists { albumArtistNames = append(albumArtistNames, getString(artist, "name")) } - albumArtistsString = strings.Join(albumArtistNames, ", ") + albumArtistsString = strings.Join(albumArtistNames, GetSeparator()) } if albumArtistsString == "" { albumArtistsString = getString(albumUnionData, "artists") @@ -681,7 +681,7 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter for _, artist := range albumArtists { albumArtistNames = append(albumArtistNames, getString(artist, "name")) } - albumArtistsString = strings.Join(albumArtistNames, ", ") + albumArtistsString = strings.Join(albumArtistNames, GetSeparator()) } } @@ -715,13 +715,13 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter for _, artist := range artists { artistNames = append(artistNames, getString(artist, "name")) } - artistsString := strings.Join(artistNames, ", ") + artistsString := strings.Join(artistNames, GetSeparator()) copyrightTexts := []string{} for _, item := range copyrightInfo { copyrightTexts = append(copyrightTexts, getString(item, "text")) } - copyrightString := strings.Join(copyrightTexts, ", ") + copyrightString := strings.Join(copyrightTexts, GetSeparator()) discNumber := int(getFloat64(trackData, "discNumber")) if discNumber == 0 { @@ -814,7 +814,7 @@ func FilterAlbum(data map[string]interface{}) map[string]interface{} { for _, artist := range artists { artistNames = append(artistNames, getString(artist, "name")) } - albumArtistsString := strings.Join(artistNames, ", ") + albumArtistsString := strings.Join(artistNames, GetSeparator()) coverObj := extractCoverImage(getMap(albumData, "coverArt")) var cover interface{} @@ -875,7 +875,7 @@ func FilterAlbum(data map[string]interface{}) map[string]interface{} { for _, artist := range trackArtists { trackArtistNames = append(trackArtistNames, getString(artist, "name")) } - trackArtistsString := strings.Join(trackArtistNames, ", ") + trackArtistsString := strings.Join(trackArtistNames, GetSeparator()) trackURI := getString(track, "uri") trackID := "" @@ -1075,7 +1075,7 @@ func FilterPlaylist(data map[string]interface{}) map[string]interface{} { for _, artist := range trackArtists { trackArtistNames = append(trackArtistNames, getString(artist, "name")) } - artistsString := strings.Join(trackArtistNames, ", ") + artistsString := strings.Join(trackArtistNames, GetSeparator()) trackDurationMs := getFloat64(getMap(trackData, "trackDuration"), "totalMilliseconds") durationObj := extractDuration(trackDurationMs) @@ -1121,7 +1121,7 @@ func FilterPlaylist(data map[string]interface{}) map[string]interface{} { for _, artist := range albumArtists { albumArtistNames = append(albumArtistNames, getString(artist, "name")) } - albumArtistsString = strings.Join(albumArtistNames, ", ") + albumArtistsString = strings.Join(albumArtistNames, GetSeparator()) } } @@ -1514,7 +1514,7 @@ func FilterSearch(data map[string]interface{}) map[string]interface{} { for _, artist := range trackArtists { trackArtistNames = append(trackArtistNames, getString(artist, "name")) } - trackArtistsString := strings.Join(trackArtistNames, ", ") + trackArtistsString := strings.Join(trackArtistNames, GetSeparator()) durationString := getString(trackDuration, "formatted") @@ -1586,7 +1586,7 @@ func FilterSearch(data map[string]interface{}) map[string]interface{} { for _, artist := range albumArtists { albumArtistNames = append(albumArtistNames, getString(artist, "name")) } - albumArtistsString := strings.Join(albumArtistNames, ", ") + albumArtistsString := strings.Join(albumArtistNames, GetSeparator()) dateInfo := getMap(album, "date") var year interface{} diff --git a/backend/system_unix.go b/backend/system_unix.go deleted file mode 100644 index 8783175..0000000 --- a/backend/system_unix.go +++ /dev/null @@ -1,41 +0,0 @@ -//go:build !windows - -package backend - -import ( - "fmt" - "os/exec" - "runtime" - "strings" -) - -func GetOSInfo() (string, error) { - osType := runtime.GOOS - arch := runtime.GOARCH - - switch osType { - case "darwin": - out, err := exec.Command("sw_vers", "-productVersion").Output() - if err != nil { - return fmt.Sprintf("macOS %s", arch), nil - } - version := strings.TrimSpace(string(out)) - return fmt.Sprintf("macOS %s (%s)", version, arch), nil - - case "linux": - out, err := exec.Command("cat", "/etc/os-release").Output() - if err == nil { - lines := strings.Split(string(out), "\n") - for _, line := range lines { - if strings.HasPrefix(line, "PRETTY_NAME=") { - name := strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"") - return fmt.Sprintf("%s (%s)", name, arch), nil - } - } - } - return fmt.Sprintf("Linux %s", arch), nil - - default: - return fmt.Sprintf("%s %s", osType, arch), nil - } -} diff --git a/backend/system_windows.go b/backend/system_windows.go deleted file mode 100644 index 3330a44..0000000 --- a/backend/system_windows.go +++ /dev/null @@ -1,41 +0,0 @@ -package backend - -import ( - "fmt" - "os/exec" - "runtime" - "strings" - "syscall" -) - -func GetOSInfo() (string, error) { - arch := runtime.GOARCH - - cmd := exec.Command("wmic", "os", "get", "Caption,Version", "/value") - cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} - out, err := cmd.Output() - if err != nil { - cmdVer := exec.Command("cmd", "/c", "ver") - cmdVer.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} - outVer, errVer := cmdVer.Output() - if errVer != nil { - return fmt.Sprintf("Windows %s", arch), nil - } - return strings.TrimSpace(string(outVer)), nil - } - - lines := strings.Split(string(out), "\n") - var caption, version string - for _, line := range lines { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "Caption=") { - caption = strings.TrimPrefix(line, "Caption=") - } else if strings.HasPrefix(line, "Version=") { - version = strings.TrimPrefix(line, "Version=") - } - } - if caption != "" && version != "" { - return fmt.Sprintf("%s (%s, %s)", caption, version, arch), nil - } - return strings.TrimSpace(string(out)), nil -} diff --git a/backend/tidal.go b/backend/tidal.go index ecbe095..cf0aae2 100644 --- a/backend/tidal.go +++ b/backend/tidal.go @@ -79,9 +79,13 @@ func NewTidalDownloader(apiURL string) *TidalDownloader { func (t *TidalDownloader) GetAvailableAPIs() ([]string, error) { apis := []string{ - "https://triton.squid.wtf", "https://hifi-one.spotisaver.net", "https://hifi-two.spotisaver.net", + "https://eu-central.monochrome.tf", + "https://us-west.monochrome.tf", + "https://api.monochrome.tf", + "https://monochrome-api.samidy.com", + "https://tidal.kinoplus.online", } return apis, nil } diff --git a/backend/uploader.go b/backend/uploader.go deleted file mode 100644 index 5f200ca..0000000 --- a/backend/uploader.go +++ /dev/null @@ -1,217 +0,0 @@ -package backend - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/http" - "os" - "path/filepath" - "regexp" - "strings" - "time" -) - -type SendNowResponse []struct { - FileCode string `json:"file_code"` -} - -func UploadToSendNow(filePath string) (string, error) { - file, err := os.Open(filePath) - if err != nil { - return "", fmt.Errorf("failed to open file: %v", err) - } - defer file.Close() - - return uploadToService(filepath.Base(filePath), file) -} - -func UploadBytesToSendNow(filename string, data []byte) (string, error) { - return uploadToService(filename, bytes.NewReader(data)) -} - -func uploadToService(filename string, fileReader io.Reader) (string, error) { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - fields := map[string]string{ - "sess_id": "", - "utype": "anon", - "hidden": "", - "enableemail": "", - "link_rcpt": "", - "link_pass": "", - "file_expire_time": "", - "file_expire_unit": "DAY", - "file_max_dl": "1", - "file_public": "1", - "keepalive": "1", - } - - for key, val := range fields { - if err := writer.WriteField(key, val); err != nil { - return "", err - } - } - - part, err := writer.CreateFormFile("file_0", filename) - if err != nil { - return "", err - } - if _, err := io.Copy(part, fileReader); err != nil { - return "", err - } - - writer.Close() - - uploadURL, err := getUploadURL() - if err != nil { - return "", fmt.Errorf("failed to get upload server: %v", err) - } - - req, err := http.NewRequest("POST", uploadURL, body) - if err != nil { - return "", err - } - - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36") - req.Header.Set("Origin", "https://send.now") - req.Header.Set("Referer", "https://send.now/") - req.Header.Set("Content-Type", writer.FormDataContentType()) - - client := &http.Client{Timeout: 60 * time.Second} - resp, err := client.Do(req) - if err != nil { - return "", fmt.Errorf("upload failed: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - respBytes, _ := io.ReadAll(resp.Body) - return "", fmt.Errorf("server error %d: %s", resp.StatusCode, string(respBytes)) - } - - respBytes, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - var result SendNowResponse - if err := json.Unmarshal(respBytes, &result); err != nil { - return "", fmt.Errorf("failed to parse response: %v, raw: %s", err, string(respBytes)) - } - - if len(result) == 0 || result[0].FileCode == "" { - return "", fmt.Errorf("invalid response format") - } - - fileCode := result[0].FileCode - downloadLink := fmt.Sprintf("https://send.now/%s", fileCode) - - ext := strings.ToLower(filepath.Ext(filename)) - if ext == ".mp4" || ext == ".mov" || ext == ".mkv" || ext == ".webm" || ext == ".avi" { - return fmt.Sprintf("[Video](%s)", downloadLink), nil - } - - return fetchDirectImageLink(downloadLink) -} - -func getUploadURL() (string, error) { - req, err := http.NewRequest("GET", "https://send.now/", nil) - if err != nil { - return "", err - } - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36") - - client := &http.Client{Timeout: 30 * time.Second} - resp, err := client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return "", fmt.Errorf("failed to fetch main page: status %d", resp.StatusCode) - } - - bodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - body := string(bodyBytes) - - re := regexp.MustCompile(`action=["'](https://[^"']+/cgi-bin/upload\.cgi\?upload_type=file[^"']*)["']`) - matches := re.FindStringSubmatch(body) - if len(matches) > 1 { - return matches[1], nil - } - - reFallback := regexp.MustCompile(`action=["'](https://[^"']+/cgi-bin/upload\.cgi)`) - matchesFallback := reFallback.FindStringSubmatch(body) - if len(matchesFallback) > 1 { - return matchesFallback[1] + "?upload_type=file&utype=anon", nil - } - - return "", fmt.Errorf("upload URL not found in main page") -} - -func fetchDirectImageLink(url string) (string, error) { - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return "", err - } - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36") - - client := &http.Client{Timeout: 30 * time.Second} - resp, err := client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - htmlBytes, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - htmlStr := string(htmlBytes) - - reFullRes := regexp.MustCompile(`(?i)]+href=["']([^"']+)["'][^>]*title=["']Open image on new tab["']`) - matchesFull := reFullRes.FindStringSubmatch(htmlStr) - if len(matchesFull) > 1 { - return fmt.Sprintf("![image](%s)", matchesFull[1]), nil - } - - reClipboard := regexp.MustCompile(`(?s)data-clipboard-text=['"] 1 { - return fmt.Sprintf("![image](%s)", matches[1]), nil - } - - reImg := regexp.MustCompile(`(?i)]+src=["']([^"']*?\.send\.now/i/[^"']+)["']`) - matchesImg := reImg.FindStringSubmatch(htmlStr) - if len(matchesImg) > 1 { - return fmt.Sprintf("![image](%s)", matchesImg[1]), nil - } - - reAnchor := regexp.MustCompile(`(?i)]+href=["']([^"']+\.(?:jpg|jpeg|png|gif|webp))["']`) - matchesAnchor := reAnchor.FindStringSubmatch(htmlStr) - if len(matchesAnchor) > 1 { - return fmt.Sprintf("![image](%s)", matchesAnchor[1]), nil - } - - reGeneric := regexp.MustCompile(`(?i)]+src=["']([^"']+\.(?:jpg|jpeg|png|gif|webp))["']`) - matchesGeneric := reGeneric.FindAllStringSubmatch(htmlStr, -1) - for _, match := range matchesGeneric { - if len(match) > 1 { - link := match[1] - - if !regexp.MustCompile(`(?i)(logo|icon|button|assets)`).MatchString(filepath.Base(link)) { - return fmt.Sprintf("![image](%s)", link), nil - } - } - } - - return fmt.Sprintf("[View File](%s)", url), nil -} diff --git a/frontend/package.json b/frontend/package.json index 15b7cdb..3276b8b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,6 +32,7 @@ "lucide-react": "^0.575.0", "motion": "^12.34.3", "next-themes": "^0.4.6", + "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", "sonner": "^2.0.7", diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 53144bb..1018354 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -3ca7ac3e41fb33a6fc3e30c16b39657b \ No newline at end of file +867c45db7982e126a7249d80210f23be \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 64e8c33..154f505 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + radix-ui: + specifier: ^1.4.3 + version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: ^19.2.4 version: 19.2.4 @@ -616,6 +619,45 @@ packages: '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + '@radix-ui/react-accessible-icon@1.1.7': + resolution: {integrity: sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-arrow@1.1.7': resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} peerDependencies: @@ -629,6 +671,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-aspect-ratio@1.1.7': + resolution: {integrity: sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-checkbox@1.3.3': resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} peerDependencies: @@ -642,6 +710,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.7': resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} peerDependencies: @@ -730,6 +811,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-guards@1.1.3': resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} peerDependencies: @@ -752,6 +846,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-form@0.1.8': + resolution: {integrity: sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-id@1.1.1': resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} peerDependencies: @@ -761,6 +881,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-label@2.1.8': resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==} peerDependencies: @@ -800,6 +933,58 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-one-time-password-field@0.1.8': + resolution: {integrity: sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-password-toggle-field@0.1.3': + resolution: {integrity: sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popper@1.2.8': resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} peerDependencies: @@ -865,6 +1050,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-progress@1.1.8': resolution: {integrity: sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==} peerDependencies: @@ -878,6 +1076,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.11': resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} peerDependencies: @@ -917,6 +1128,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.2.3': resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: @@ -961,6 +1198,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-toast@1.2.15': + resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-toggle-group@1.1.11': resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} peerDependencies: @@ -987,6 +1237,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-toolbar@1.1.11': + resolution: {integrity: sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tooltip@1.2.8': resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} peerDependencies: @@ -1036,6 +1299,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-layout-effect@1.1.1': resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} peerDependencies: @@ -1872,6 +2144,19 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + radix-ui@1.4.3: + resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -2028,6 +2313,11 @@ packages: '@types/react': optional: true + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vite@7.3.1: resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2471,6 +2761,46 @@ snapshots: '@radix-ui/primitive@1.1.3': {} + '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -2480,6 +2810,28 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -2496,6 +2848,22 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) @@ -2581,6 +2949,21 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.4)': dependencies: react: 19.2.4 @@ -2598,6 +2981,37 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) @@ -2605,6 +3019,15 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -2658,6 +3081,87 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + 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.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@floating-ui/react-dom': 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -2714,6 +3218,16 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-progress@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/react-context': 1.1.3(@types/react@19.2.14)(react@19.2.4) @@ -2724,6 +3238,24 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -2787,6 +3319,34 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + 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.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) @@ -2832,6 +3392,26 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -2858,6 +3438,21 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -2906,6 +3501,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: react: 19.2.4 @@ -3636,6 +4238,69 @@ snapshots: punycode@2.3.1: {} + radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 @@ -3816,6 +4481,10 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1): dependencies: esbuild: 0.27.3 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 900c043..ca09792 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -336,7 +336,7 @@ function App() { if ("track" in metadata.metadata) { const { track } = metadata.metadata; const trackId = track.spotify_id || ""; - return ( lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, track.album_name, undefined, albumArtist, releaseDate, discNumber)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _playlistName, _position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, track.album_name, undefined, trackId, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onOpenFolder={handleOpenFolder} onBack={metadata.resetMetadata}/>); + return ( lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, undefined, undefined, albumArtist, releaseDate, discNumber)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _playlistName, _position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, undefined, undefined, trackId, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onOpenFolder={handleOpenFolder} onBack={metadata.resetMetadata}/>); } if ("album_info" in metadata.metadata) { const { album_info, track_list } = metadata.metadata; @@ -415,7 +415,7 @@ function App() { case "debug": return ; case "about": - return ; + return ; case "history": return { metadata.loadFromCache(cachedData); diff --git a/frontend/src/assets/ko-fi.gif b/frontend/src/assets/ko-fi.gif new file mode 100644 index 0000000..221d0b9 Binary files /dev/null and b/frontend/src/assets/ko-fi.gif differ diff --git a/frontend/src/assets/usdt.jpg b/frontend/src/assets/usdt.jpg new file mode 100644 index 0000000..5732bda Binary files /dev/null and b/frontend/src/assets/usdt.jpg differ diff --git a/frontend/src/components/AboutPage.tsx b/frontend/src/components/AboutPage.tsx index ee75a7f..3ce9ee1 100644 --- a/frontend/src/components/AboutPage.tsx +++ b/frontend/src/components/AboutPage.tsx @@ -1,13 +1,8 @@ import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { openExternal } from "@/lib/utils"; -import { GetOSInfo } from "../../wailsjs/go/main/App"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; -import { Label } from "@/components/ui/label"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; -import { Bug, Lightbulb, ExternalLink, Star, GitFork, Clock, Download, CircleHelp, Blocks, Heart, } from "lucide-react"; +import { Star, GitFork, Clock, Download, Blocks, Heart, Copy, CircleCheck } from "lucide-react"; import AudioTTSProIcon from "@/assets/audiotts-pro.webp"; import ChatGPTTTSIcon from "@/assets/chatgpt-tts.webp"; import XProIcon from "@/assets/x-pro.webp"; @@ -15,64 +10,15 @@ import SpotubeDLIcon from "@/assets/icons/spotubedl.svg"; import SpotiDownloaderIcon from "@/assets/icons/spotidownloader.svg"; import XBatchDLIcon from "@/assets/icons/xbatchdl.svg"; import SpotiFLACNextIcon from "@/assets/icons/next.svg"; -import KofiLogo from "@/assets/kofi_symbol.svg"; +import KofiLogo from "@/assets/ko-fi.gif"; +import KofiSvg from "@/assets/kofi_symbol.svg"; +import UsdtBarcode from "@/assets/usdt.jpg"; import { langColors } from "@/assets/github-lang-colors"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { DragDropMedia } from "./DragDropTextarea"; -interface AboutPageProps { - version: string; -} -export function AboutPage({ version }: AboutPageProps) { - const [os, setOs] = useState("Unknown"); - const [location, setLocation] = useState("Unknown"); - const [activeTab, setActiveTab] = useState<"bug_report" | "feature_request" | "faq" | "projects" | "support">("bug_report"); - const [bugType, setBugType] = useState("Track"); - const [problem, setProblem] = useState(""); - const [spotifyUrl, setSpotifyUrl] = useState(""); - const [bugContext, setBugContext] = useState(""); - const [featureDesc, setFeatureDesc] = useState(""); - const [useCase, setUseCase] = useState(""); - const [featureContext, setFeatureContext] = useState(""); +export function AboutPage() { + const [activeTab, setActiveTab] = useState<"projects" | "support">("projects"); const [repoStats, setRepoStats] = useState>({}); + const [copiedUsdt, setCopiedUsdt] = useState(false); useEffect(() => { - const fetchOS = async () => { - try { - const info = await GetOSInfo(); - setOs(info); - } - catch (err) { - const userAgent = window.navigator.userAgent; - if (userAgent.indexOf("Win") !== -1) - setOs("Windows"); - else if (userAgent.indexOf("Mac") !== -1) - setOs("macOS"); - else if (userAgent.indexOf("Linux") !== -1) - setOs("Linux"); - } - }; - fetchOS(); - const fetchLocation = async () => { - try { - const response = await fetch("https://ipapi.co/json/"); - if (response.ok) { - const data = await response.json(); - const city = data.city || ""; - const region = data.region || ""; - const country = data.country_name || ""; - const parts = [city, region, country].filter(Boolean); - setLocation(parts.join(", ") || "Unknown"); - } - else { - const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - setLocation(timezone); - } - } - catch (err) { - const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - setLocation(timezone); - } - }; - fetchLocation(); const fetchRepoStats = async () => { const CACHE_KEY = "github_repo_stats"; const CACHE_DURATION = 1000 * 60 * 60; @@ -115,7 +61,9 @@ export function AboutPage({ version }: AboutPageProps) { const languages = await langsRes.json(); let totalDownloads = 0; let latestDownloads = 0; + let latestVersion = ""; if (releases.length > 0) { + latestVersion = releases[0].tag_name || ""; latestDownloads = releases[0].assets?.reduce((sum: number, asset: any) => sum + (asset.download_count || 0), 0) || 0; totalDownloads = releases.reduce((sum: number, release: any) => { @@ -133,6 +81,7 @@ export function AboutPage({ version }: AboutPageProps) { createdAt: repoData.created_at, totalDownloads, latestDownloads, + latestVersion, languages: topLangs, }; } @@ -151,28 +100,6 @@ export function AboutPage({ version }: AboutPageProps) { }; fetchRepoStats(); }, []); - const faqs = [ - { - q: "Is this software free?", - a: "Yes. This software is completely free. You do not need an account, login, or subscription. All you need is an internet connection.", - }, - { - q: "Can using this software get my Spotify account suspended or banned?", - a: "No. This software has no connection to your Spotify account. Spotify data is obtained through reverse engineering of the Spotify Web Player, not through user authentication.", - }, - { - q: "Where does the audio come from?", - a: "The audio is fetched using third-party APIs.", - }, - { - q: "Why does metadata fetching sometimes fail?", - a: "This usually happens because your IP address has been rate-limited. You can wait and try again later, or use a VPN to bypass the rate limit.", - }, - { - q: "Why does Windows Defender or antivirus flag or delete the file?", - a: "This is a false positive. It likely happens because the executable is compressed using UPX. If you are concerned, you can fork the repository and build the software yourself from source.", - }, - ]; const formatTimeAgo = (dateString: string): string => { const now = new Date(); const updated = new Date(dateString); @@ -201,74 +128,12 @@ export function AboutPage({ version }: AboutPageProps) { const getLangColor = (lang: string): string => { return langColors[lang] || "#858585"; }; - const handleSubmit = () => { - const title = activeTab === "bug_report" - ? `[Bug Report] ${problem.substring(0, 50)}${problem.length > 50 ? "..." : ""}` - : `[Feature Request] ${featureDesc.substring(0, 50)}${featureDesc.length > 50 ? "..." : ""}`; - let bodyContent = ""; - if (activeTab === "bug_report") { - const contextContent = bugContext.trim() - ? bugContext.trim() - : "Type here or send screenshot/recording"; - bodyContent = `### [Bug Report] - -#### Problem -${problem || "Type here"} - -#### Type -${bugType} - -#### Spotify URL -${spotifyUrl || "Type here"} - -#### Additional Context -${contextContent} - -#### Environment -- SpotiFLAC Version: ${version} -- OS: ${os} -- Location: ${location}`; - } - else { - const contextContent = featureContext.trim() - ? featureContext.trim() - : "Type here or send screenshot/recording"; - bodyContent = `### [Feature Request] - -#### Description -${featureDesc || "Type here"} - -#### Use Case -${useCase || "Type here"} - -#### Additional Context -${contextContent}`; - } - const params = new URLSearchParams({ - title: title, - body: bodyContent, - }); - const url = `https://github.com/afkarxyz/SpotiFLAC/issues/new?${params.toString()}`; - openExternal(url); - }; - return (
+ return (

About

- - -
-
- {activeTab === "bug_report" && (
-
-
-
-
- -