diff --git a/backend/spotfetch.go b/backend/spotfetch.go new file mode 100644 index 0000000..f55e498 --- /dev/null +++ b/backend/spotfetch.go @@ -0,0 +1,1686 @@ +package backend + +import ( + "bytes" + "encoding/base32" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "html" + "io" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "sort" + + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" +) + +var SpotifyError = errors.New("spotify error") + +type SpotifyClient struct { + client *http.Client + accessToken string + clientToken string + clientID string + deviceID string + clientVersion string + cookies map[string]string +} + +func NewSpotifyClient() *SpotifyClient { + return &SpotifyClient{ + client: &http.Client{Timeout: 30 * time.Second}, + cookies: make(map[string]string), + } +} + +func (c *SpotifyClient) getTOTPSecret() (int, []byte) { + secrets := map[int][]byte{ + 59: {123, 105, 79, 70, 110, 59, 52, 125, 60, 49, 80, 70, 89, 75, 80, 86, 63, 53, 123, 37, 117, 49, 52, 93, 77, 62, 47, 86, 48, 104, 68, 72}, + 60: {79, 109, 69, 123, 90, 65, 46, 74, 94, 34, 58, 48, 70, 71, 92, 85, 122, 63, 91, 64, 87, 87}, + 61: {44, 55, 47, 42, 70, 40, 34, 114, 76, 74, 50, 111, 120, 97, 75, 76, 94, 102, 43, 69, 49, 120, 118, 80, 64, 78}, + } + + version := 61 + secretList := secrets[version] + return version, secretList +} + +func (c *SpotifyClient) generateTOTP() (string, int, error) { + version, secretList := c.getTOTPSecret() + + transformed := make([]byte, len(secretList)) + for i, b := range secretList { + transformed[i] = b ^ byte((i%33)+9) + } + + var joined strings.Builder + for _, b := range transformed { + joined.WriteString(strconv.Itoa(int(b))) + } + + hexStr := hex.EncodeToString([]byte(joined.String())) + hexBytes, err := hex.DecodeString(hexStr) + if err != nil { + return "", 0, err + } + + secret := base32Encode(hexBytes) + secret = strings.TrimRight(secret, "=") + + key, err := otp.NewKeyFromURL(fmt.Sprintf("otpauth://totp/secret?secret=%s", secret)) + if err != nil { + return "", 0, err + } + + totpCode, err := totp.GenerateCode(key.Secret(), time.Now()) + if err != nil { + return "", 0, err + } + + return totpCode, version, nil +} + +func base32Encode(data []byte) string { + b32 := base32.StdEncoding.WithPadding(base32.NoPadding) + return b32.EncodeToString(data) +} + +func (c *SpotifyClient) getAccessToken() error { + totpCode, version, err := c.generateTOTP() + if err != nil { + return err + } + + req, err := http.NewRequest("GET", "https://open.spotify.com/api/token", nil) + if err != nil { + return err + } + + q := req.URL.Query() + q.Add("reason", "init") + q.Add("productType", "web-player") + q.Add("totp", totpCode) + q.Add("totpVer", strconv.Itoa(version)) + q.Add("totpServer", totpCode) + req.URL.RawQuery = q.Encode() + + 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("Content-Type", "application/json;charset=UTF-8") + + resp, err := c.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("%w: access token request failed: HTTP %d", SpotifyError, resp.StatusCode) + } + + var data map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return err + } + + c.accessToken = getString(data, "accessToken") + c.clientID = getString(data, "clientId") + + for _, cookie := range resp.Cookies() { + if cookie.Name == "sp_t" { + c.deviceID = cookie.Value + } + c.cookies[cookie.Name] = cookie.Value + } + + return nil +} + +func (c *SpotifyClient) getSessionInfo() error { + req, err := http.NewRequest("GET", "https://open.spotify.com", 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/120.0.0.0 Safari/537.36") + + for name, value := range c.cookies { + req.AddCookie(&http.Cookie{Name: name, Value: value}) + } + + resp, err := c.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("%w: session initialization failed: HTTP %d", SpotifyError, resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + re := regexp.MustCompile(``) + matches := re.FindStringSubmatch(string(body)) + if len(matches) > 1 { + decoded, err := base64.StdEncoding.DecodeString(matches[1]) + if err == nil { + var cfg map[string]interface{} + if json.Unmarshal(decoded, &cfg) == nil { + c.clientVersion = getString(cfg, "clientVersion") + } + } + } + + for _, cookie := range resp.Cookies() { + if cookie.Name == "sp_t" { + c.deviceID = cookie.Value + } + c.cookies[cookie.Name] = cookie.Value + } + + return nil +} + +func (c *SpotifyClient) getClientToken() error { + if c.clientID == "" || c.deviceID == "" || c.clientVersion == "" { + if err := c.getSessionInfo(); err != nil { + return err + } + if err := c.getAccessToken(); err != nil { + return err + } + } + + payload := map[string]interface{}{ + "client_data": map[string]interface{}{ + "client_version": c.clientVersion, + "client_id": c.clientID, + "js_sdk_data": map[string]interface{}{ + "device_brand": "unknown", + "device_model": "unknown", + "os": "windows", + "os_version": "NT 10.0", + "device_id": c.deviceID, + "device_type": "computer", + }, + }, + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", "https://clienttoken.spotify.com/v1/clienttoken", bytes.NewBuffer(jsonData)) + if err != nil { + return err + } + + req.Header.Set("Authority", "clienttoken.spotify.com") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + 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") + + resp, err := c.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("%w: client token request failed: HTTP %d", SpotifyError, resp.StatusCode) + } + + var data map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return err + } + + if getString(data, "response_type") != "RESPONSE_GRANTED_TOKEN_RESPONSE" { + return fmt.Errorf("%w: invalid client token response type", SpotifyError) + } + + grantedToken := getMap(data, "granted_token") + c.clientToken = getString(grantedToken, "token") + + return nil +} + +func (c *SpotifyClient) Initialize() error { + if err := c.getSessionInfo(); err != nil { + return err + } + if err := c.getAccessToken(); err != nil { + return err + } + return c.getClientToken() +} + +func (c *SpotifyClient) Query(payload map[string]interface{}) (map[string]interface{}, error) { + if c.accessToken == "" || c.clientToken == "" { + if err := c.Initialize(); err != nil { + return nil, err + } + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", "https://api-partner.spotify.com/pathfinder/v2/query", bytes.NewBuffer(jsonData)) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+c.accessToken) + req.Header.Set("Client-Token", c.clientToken) + req.Header.Set("Spotify-App-Version", c.clientVersion) + 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/120.0.0.0 Safari/537.36") + + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != 200 { + errorText := string(body) + if len(errorText) > 200 { + errorText = errorText[:200] + } + return nil, fmt.Errorf("%w: API query failed: HTTP %d | %s", SpotifyError, resp.StatusCode, errorText) + } + + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return nil, err + } + + return result, nil +} + +func getString(m map[string]interface{}, key string) string { + if val, ok := m[key].(string); ok { + return val + } + return "" +} + +func getMap(m map[string]interface{}, key string) map[string]interface{} { + if val, ok := m[key].(map[string]interface{}); ok { + return val + } + return make(map[string]interface{}) +} + +func getSlice(m map[string]interface{}, key string) []interface{} { + if val, ok := m[key].([]interface{}); ok { + return val + } + return nil +} + +func getFloat64(m map[string]interface{}, key string) float64 { + if val, ok := m[key].(float64); ok { + return val + } + return 0 +} + +func getInt(m map[string]interface{}, key string) int { + if val, ok := m[key].(int); ok { + return val + } + if val, ok := m[key].(float64); ok { + return int(val) + } + return 0 +} + +func getBool(m map[string]interface{}, key string) bool { + if val, ok := m[key].(bool); ok { + return val + } + return false +} + +func extractArtists(artistsData map[string]interface{}) []map[string]interface{} { + items := getSlice(artistsData, "items") + if items == nil { + return []map[string]interface{}{} + } + + artists := []map[string]interface{}{} + for _, item := range items { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + profile := getMap(itemMap, "profile") + artistInfo := map[string]interface{}{ + "name": getString(profile, "name"), + } + artists = append(artists, artistInfo) + } + return artists +} + +func extractCoverImage(coverData map[string]interface{}) map[string]interface{} { + if coverData == nil || len(coverData) == 0 { + return nil + } + + var sources []interface{} + if srcs, ok := coverData["sources"].([]interface{}); ok { + sources = srcs + } else if squareImg, ok := coverData["squareCoverImage"].(map[string]interface{}); ok { + if img, ok := squareImg["image"].(map[string]interface{}); ok { + if data, ok := img["data"].(map[string]interface{}); ok { + if srcs, ok := data["sources"].([]interface{}); ok { + sources = srcs + } + } + } + } + + if sources == nil || len(sources) == 0 { + return nil + } + + type sourceInfo struct { + url string + width float64 + height float64 + } + + filteredSources := []sourceInfo{} + for _, s := range sources { + sMap, ok := s.(map[string]interface{}) + if !ok { + continue + } + url := getString(sMap, "url") + if url == "" { + continue + } + + width := getFloat64(sMap, "width") + if width == 0 { + width = getFloat64(sMap, "maxWidth") + } + height := getFloat64(sMap, "height") + if height == 0 { + height = getFloat64(sMap, "maxHeight") + } + + if (width > 64 && height > 64) || (width == 0 && height == 0 && url != "") { + filteredSources = append(filteredSources, sourceInfo{url: url, width: width, height: height}) + } + } + + if len(filteredSources) == 0 { + return nil + } + + sort.Slice(filteredSources, func(i, j int) bool { + return filteredSources[i].width < filteredSources[j].width + }) + + var smallURL, mediumURL, imageID, fallbackURL string + + for _, source := range filteredSources { + if source.width == 300 { + smallURL = source.url + } else if source.width == 640 { + mediumURL = source.url + } else if source.width == 0 { + fallbackURL = source.url + } + + if imageID == "" && source.url != "" { + if strings.Contains(source.url, "ab67616d0000b273") { + parts := strings.Split(source.url, "ab67616d0000b273") + if len(parts) > 1 { + imageID = parts[len(parts)-1] + } + } else if strings.Contains(source.url, "ab67616d00001e02") { + parts := strings.Split(source.url, "ab67616d00001e02") + if len(parts) > 1 { + imageID = parts[len(parts)-1] + } + } else if strings.Contains(source.url, "/image/") { + parts := strings.Split(source.url, "/image/") + if len(parts) > 1 { + imagePart := strings.Split(parts[len(parts)-1], "?")[0] + if len(imagePart) > 20 { + prefixes := []string{"ab67616d0000b273", "ab67616d00001e02", "ab67616d00004851"} + for _, prefix := range prefixes { + if strings.Contains(imagePart, prefix) { + subParts := strings.Split(imagePart, prefix) + if len(subParts) > 1 { + imageID = subParts[len(subParts)-1] + break + } + } + } + } + } + } + } + } + + largeURL := "" + if imageID != "" { + largeURL = "https://i.scdn.co/image/ab67616d000082c1" + imageID + } + + result := map[string]interface{}{} + if smallURL != "" { + result["small"] = smallURL + } + if mediumURL != "" { + result["medium"] = mediumURL + } + if largeURL != "" { + result["large"] = largeURL + } + + if len(result) == 0 && fallbackURL != "" { + result["small"] = fallbackURL + result["medium"] = fallbackURL + result["large"] = fallbackURL + } + + if len(result) == 0 { + return nil + } + return result +} + +func extractDuration(ms float64) map[string]interface{} { + totalSeconds := int(ms) / 1000 + minutes := totalSeconds / 60 + seconds := totalSeconds % 60 + return map[string]interface{}{ + "formatted": fmt.Sprintf("%d:%02d", minutes, seconds), + } +} + +func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]interface{}) map[string]interface{} { + dataMap := getMap(data, "data") + trackData := getMap(dataMap, "trackUnion") + if len(trackData) == 0 { + return make(map[string]interface{}) + } + + var albumFetchDataMap map[string]interface{} + if len(albumFetchData) > 0 && albumFetchData[0] != nil { + albumFetchDataMap = albumFetchData[0] + } + + artists := extractArtists(getMap(trackData, "artists")) + + if len(artists) == 0 { + artists = []map[string]interface{}{} + firstArtistItems := getSlice(getMap(trackData, "firstArtist"), "items") + if firstArtistItems != nil { + for _, item := range firstArtistItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + if profile, exists := itemMap["profile"]; exists { + profileMap, ok := profile.(map[string]interface{}) + if ok { + artistInfo := map[string]interface{}{ + "name": getString(profileMap, "name"), + } + artists = append(artists, artistInfo) + } + } + } + } + + otherArtistItems := getSlice(getMap(trackData, "otherArtists"), "items") + if otherArtistItems != nil { + for _, item := range otherArtistItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + if profile, exists := itemMap["profile"]; exists { + profileMap, ok := profile.(map[string]interface{}) + if ok { + artistInfo := map[string]interface{}{ + "name": getString(profileMap, "name"), + } + artists = append(artists, artistInfo) + } + } + } + } + } + + if len(artists) == 0 { + albumData := getMap(trackData, "albumOfTrack") + if len(albumData) > 0 { + artists = extractArtists(getMap(albumData, "artists")) + } + } + + albumData := getMap(trackData, "albumOfTrack") + var albumInfo map[string]interface{} + copyrightInfo := []map[string]interface{}{} + discInfo := map[string]interface{}{ + "discNumber": getFloat64(trackData, "discNumber"), + "totalDiscs": nil, + } + + if len(albumData) > 0 { + copyrightData := getMap(albumData, "copyright") + if len(copyrightData) > 0 { + copyrightItems := getSlice(copyrightData, "items") + if copyrightItems != nil { + for _, item := range copyrightItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + if getString(itemMap, "type") != "P" { + copyrightInfo = append(copyrightInfo, map[string]interface{}{ + "text": getString(itemMap, "text"), + }) + } + } + } + } + + tracksData := getMap(albumData, "tracks") + if len(tracksData) > 0 { + discNumbers := make(map[int]bool) + trackItems := getSlice(tracksData, "items") + if trackItems != nil { + for _, item := range trackItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + trackItem := getMap(itemMap, "track") + if len(trackItem) > 0 { + discNum := int(getFloat64(trackItem, "discNumber")) + if discNum == 0 { + discNum = 1 + } + discNumbers[discNum] = true + } + } + } + if len(discNumbers) > 0 { + maxDisc := 1 + for discNum := range discNumbers { + if discNum > maxDisc { + maxDisc = discNum + } + } + discInfo["totalDiscs"] = maxDisc + } + } + + dateInfo := getMap(albumData, "date") + releaseDate := getString(dateInfo, "isoString") + var releaseYear interface{} + if releaseDate == "" && len(dateInfo) > 0 { + yearStr := getString(dateInfo, "year") + monthStr := getString(dateInfo, "month") + dayStr := getString(dateInfo, "day") + if yearStr != "" { + year, err := strconv.Atoi(yearStr) + if err == nil { + releaseYear = year + if monthStr != "" && dayStr != "" { + month, _ := strconv.Atoi(monthStr) + day, _ := strconv.Atoi(dayStr) + releaseDate = fmt.Sprintf("%s-%02d-%02d", yearStr, month, day) + } else { + releaseDate = yearStr + } + } + } + } else if releaseDate != "" { + parts := strings.Split(releaseDate, "T") + if len(parts) > 0 { + releaseDate = parts[0] + } else { + parts = strings.Split(releaseDate, " ") + if len(parts) > 0 { + releaseDate = parts[0] + } + } + dateParts := strings.Split(releaseDate, "-") + if len(dateParts) > 0 && dateParts[0] != "" { + year, err := strconv.Atoi(dateParts[0]) + if err == nil { + releaseYear = year + } + } + } + + tracksTotalCount := float64(0) + if len(tracksData) > 0 { + tracksTotalCount = getFloat64(tracksData, "totalCount") + } + + albumID := getString(albumData, "id") + if albumID == "" { + albumURI := getString(albumData, "uri") + if strings.Contains(albumURI, ":") { + parts := strings.Split(albumURI, ":") + albumID = parts[len(parts)-1] + } + } + + albumArtistsString := "" + albumLabel := "" + if albumFetchDataMap != nil && len(albumFetchDataMap) > 0 { + albumUnionData := getMap(getMap(albumFetchDataMap, "data"), "albumUnion") + if len(albumUnionData) > 0 { + albumArtists := extractArtists(getMap(albumUnionData, "artists")) + if len(albumArtists) > 0 { + albumArtistNames := []string{} + for _, artist := range albumArtists { + albumArtistNames = append(albumArtistNames, getString(artist, "name")) + } + albumArtistsString = strings.Join(albumArtistNames, ", ") + } + albumLabel = getString(albumUnionData, "label") + } + } + + albumInfo = map[string]interface{}{ + "id": albumID, + "name": getString(albumData, "name"), + "released": releaseDate, + "year": releaseYear, + "tracks": int(tracksTotalCount), + } + + if albumArtistsString != "" { + albumInfo["artists"] = albumArtistsString + } + + if albumLabel != "" { + albumInfo["label"] = albumLabel + } + } + + cover := extractCoverImage(getMap(trackData, "visualIdentity")) + if cover == nil && len(albumData) > 0 { + cover = extractCoverImage(getMap(albumData, "coverArt")) + } + + durationMs := getFloat64(getMap(trackData, "duration"), "totalMilliseconds") + durationObj := extractDuration(durationMs) + durationString := getString(durationObj, "formatted") + + artistNames := []string{} + for _, artist := range artists { + artistNames = append(artistNames, getString(artist, "name")) + } + artistsString := strings.Join(artistNames, ", ") + + copyrightTexts := []string{} + for _, item := range copyrightInfo { + copyrightTexts = append(copyrightTexts, getString(item, "text")) + } + copyrightString := strings.Join(copyrightTexts, ", ") + + discNumber := int(getFloat64(trackData, "discNumber")) + if discNumber == 0 { + discNumber = 1 + } + totalDiscs := 1 + if discInfo["totalDiscs"] != nil { + totalDiscs = discInfo["totalDiscs"].(int) + } + + filtered := map[string]interface{}{ + "id": getString(trackData, "id"), + "name": getString(trackData, "name"), + "artists": artistsString, + "album": albumInfo, + "duration": durationString, + "track": int(getFloat64(trackData, "trackNumber")), + "disc": discNumber, + "discs": totalDiscs, + "copyright": copyrightString, + "plays": getString(trackData, "playcount"), + "cover": cover, + } + + return filtered +} + +func FilterAlbum(data map[string]interface{}) map[string]interface{} { + dataMap := getMap(data, "data") + albumData := getMap(dataMap, "albumUnion") + if len(albumData) == 0 { + return make(map[string]interface{}) + } + + artists := extractArtists(getMap(albumData, "artists")) + artistNames := []string{} + for _, artist := range artists { + artistNames = append(artistNames, getString(artist, "name")) + } + albumArtistsString := strings.Join(artistNames, ", ") + + coverObj := extractCoverImage(getMap(albumData, "coverArt")) + var cover interface{} + if coverObj != nil { + cover = getString(coverObj, "medium") + } + + tracks := []map[string]interface{}{} + tracksData := getMap(albumData, "tracksV2") + trackItems := getSlice(tracksData, "items") + if trackItems != nil { + for _, item := range trackItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + track := getMap(itemMap, "track") + if len(track) == 0 { + continue + } + + artistsData := getMap(track, "artists") + trackArtists := extractArtists(artistsData) + trackDurationMs := getFloat64(getMap(track, "duration"), "totalMilliseconds") + durationObj := extractDuration(trackDurationMs) + durationString := getString(durationObj, "formatted") + + trackArtistNames := []string{} + artistIDs := []string{} + + artistItems := getSlice(artistsData, "items") + if artistItems != nil { + for _, artistItem := range artistItems { + artistItemMap, ok := artistItem.(map[string]interface{}) + if !ok { + continue + } + artistURI := getString(artistItemMap, "uri") + if artistURI != "" && strings.Contains(artistURI, ":") { + parts := strings.Split(artistURI, ":") + if len(parts) > 0 { + artistID := parts[len(parts)-1] + if artistID != "" { + artistIDs = append(artistIDs, artistID) + } + } + } + } + } + + for _, artist := range trackArtists { + trackArtistNames = append(trackArtistNames, getString(artist, "name")) + } + trackArtistsString := strings.Join(trackArtistNames, ", ") + + trackURI := getString(track, "uri") + trackID := "" + if strings.Contains(trackURI, ":") { + parts := strings.Split(trackURI, ":") + trackID = parts[len(parts)-1] + } + + trackInfo := map[string]interface{}{ + "id": trackID, + "name": getString(track, "name"), + "artists": trackArtistsString, + "artistIds": artistIDs, + "duration": durationString, + "plays": getString(track, "playcount"), + } + tracks = append(tracks, trackInfo) + } + } + + dateInfo := getMap(albumData, "date") + releaseDate := getString(dateInfo, "isoString") + if releaseDate != "" && strings.Contains(releaseDate, "T") { + parts := strings.Split(releaseDate, "T") + releaseDate = parts[0] + } + + albumURI := getString(albumData, "uri") + albumID := "" + if strings.Contains(albumURI, ":") { + parts := strings.Split(albumURI, ":") + albumID = parts[len(parts)-1] + } + + filtered := map[string]interface{}{ + "id": albumID, + "name": getString(albumData, "name"), + "artists": albumArtistsString, + "cover": cover, + "releaseDate": releaseDate, + "count": len(tracks), + "tracks": tracks, + } + + return filtered +} + +func FilterPlaylist(data map[string]interface{}) map[string]interface{} { + dataMap := getMap(data, "data") + playlistData := getMap(dataMap, "playlistV2") + if len(playlistData) == 0 { + return make(map[string]interface{}) + } + + ownerData := getMap(getMap(playlistData, "ownerV2"), "data") + var ownerInfo map[string]interface{} + if len(ownerData) > 0 { + var avatarURL interface{} + avatarData := getMap(ownerData, "avatar") + if len(avatarData) > 0 { + sources := getSlice(avatarData, "sources") + if sources != nil { + for _, source := range sources { + sourceMap, ok := source.(map[string]interface{}) + if !ok { + continue + } + if getFloat64(sourceMap, "width") == 300 { + avatarURL = getString(sourceMap, "url") + break + } + } + if avatarURL == nil && len(sources) > 0 { + if firstSource, ok := sources[0].(map[string]interface{}); ok { + avatarURL = getString(firstSource, "url") + } + } + } + } + + ownerInfo = map[string]interface{}{ + "name": getString(ownerData, "name"), + "avatar": avatarURL, + } + } + + imagesData := getMap(playlistData, "images") + if len(imagesData) == 0 { + imagesData = getMap(playlistData, "imagesV2") + } + var cover interface{} + if len(imagesData) > 0 { + imageItems := getSlice(imagesData, "items") + if imageItems != nil && len(imageItems) > 0 { + if firstImage, ok := imageItems[0].(map[string]interface{}); ok { + firstSources := getSlice(firstImage, "sources") + if firstSources != nil && len(firstSources) > 0 { + if firstSource, ok := firstSources[0].(map[string]interface{}); ok { + sourceURL := getString(firstSource, "url") + if sourceURL != "" { + cover = sourceURL + } + } + } + } + } + if cover == nil { + imageSources := getSlice(imagesData, "sources") + if imageSources != nil && len(imageSources) > 0 { + if firstSource, ok := imageSources[0].(map[string]interface{}); ok { + sourceURL := getString(firstSource, "url") + if sourceURL != "" { + cover = sourceURL + } + } + } + } + } + + tracks := []map[string]interface{}{} + content := getMap(playlistData, "content") + contentItems := getSlice(content, "items") + if contentItems != nil { + for _, item := range contentItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + trackData := getMap(getMap(itemMap, "itemV2"), "data") + if len(trackData) == 0 { + continue + } + + var rank interface{} + var status interface{} + attributes := getSlice(itemMap, "attributes") + if attributes != nil { + for _, attr := range attributes { + attrMap, ok := attr.(map[string]interface{}) + if !ok { + continue + } + key := getString(attrMap, "key") + if key == "rank" { + rank = getString(attrMap, "value") + } else if key == "status" { + status = getString(attrMap, "value") + } + } + } + + artistsData := getMap(trackData, "artists") + trackArtists := extractArtists(artistsData) + trackArtistNames := []string{} + artistIDs := []string{} + + artistItems := getSlice(artistsData, "items") + if artistItems != nil { + for _, artistItem := range artistItems { + artistItemMap, ok := artistItem.(map[string]interface{}) + if !ok { + continue + } + artistURI := getString(artistItemMap, "uri") + if artistURI != "" && strings.Contains(artistURI, ":") { + parts := strings.Split(artistURI, ":") + if len(parts) > 0 { + artistID := parts[len(parts)-1] + if artistID != "" { + artistIDs = append(artistIDs, artistID) + } + } + } + } + } + + for _, artist := range trackArtists { + trackArtistNames = append(trackArtistNames, getString(artist, "name")) + } + artistsString := strings.Join(trackArtistNames, ", ") + + trackDurationMs := getFloat64(getMap(trackData, "trackDuration"), "totalMilliseconds") + durationObj := extractDuration(trackDurationMs) + durationString := getString(durationObj, "formatted") + + trackURI := getString(trackData, "uri") + trackID := getString(trackData, "id") + if trackID == "" { + if strings.Contains(trackURI, ":") { + parts := strings.Split(trackURI, ":") + trackID = parts[len(parts)-1] + } + } + + albumData := getMap(trackData, "albumOfTrack") + albumName := "" + albumID := "" + var trackCover interface{} + + if len(albumData) > 0 { + albumName = getString(albumData, "name") + albumURI := getString(albumData, "uri") + if strings.Contains(albumURI, ":") { + parts := strings.Split(albumURI, ":") + albumID = parts[len(parts)-1] + } + coverObj := extractCoverImage(getMap(albumData, "coverArt")) + if coverObj != nil { + trackCover = getString(coverObj, "small") + } + } + + trackInfo := map[string]interface{}{ + "id": trackID, + "cover": trackCover, + "title": getString(trackData, "name"), + "artist": artistsString, + "artistIds": artistIDs, + "plays": rank, + "status": status, + "album": albumName, + "albumId": albumID, + "duration": durationString, + } + tracks = append(tracks, trackInfo) + } + } + + followersData, exists := playlistData["followers"] + var followersCount interface{} + if exists { + if followersMap, ok := followersData.(map[string]interface{}); ok { + followersCount = getFloat64(followersMap, "totalCount") + } else if count, ok := followersData.(float64); ok { + followersCount = count + } else if count, ok := followersData.(int); ok { + followersCount = float64(count) + } else { + followersCount = float64(0) + } + } else { + followersCount = float64(0) + } + + playlistURI := getString(playlistData, "uri") + playlistID := "" + if strings.Contains(playlistURI, ":") { + parts := strings.Split(playlistURI, ":") + playlistID = parts[len(parts)-1] + } + + totalCount := getFloat64(content, "totalCount") + count := len(tracks) + if totalCount > 0 { + count = int(totalCount) + } + + filtered := map[string]interface{}{ + "id": playlistID, + "name": getString(playlistData, "name"), + "description": getString(playlistData, "description"), + "owner": ownerInfo, + "cover": cover, + "count": count, + "tracks": tracks, + "followers": followersCount, + } + + return filtered +} + +func extractRelease(release map[string]interface{}) map[string]interface{} { + if len(release) == 0 { + return nil + } + + dateInfo := getMap(release, "date") + releaseDate := getString(dateInfo, "isoString") + if releaseDate == "" && len(dateInfo) > 0 { + yearStr := getString(dateInfo, "year") + monthStr := getString(dateInfo, "month") + dayStr := getString(dateInfo, "day") + if yearStr != "" { + if monthStr != "" && dayStr != "" { + month, _ := strconv.Atoi(monthStr) + day, _ := strconv.Atoi(dayStr) + releaseDate = fmt.Sprintf("%s-%02d-%02d", yearStr, month, day) + } else { + releaseDate = yearStr + } + } + } else if releaseDate != "" && strings.Contains(releaseDate, "T") { + parts := strings.Split(releaseDate, "T") + releaseDate = parts[0] + } + + coverObj := extractCoverImage(getMap(release, "coverArt")) + var cover interface{} + if coverObj != nil { + cover = getString(coverObj, "medium") + } + + releaseID := getString(release, "id") + if releaseID == "" { + releaseURI := getString(release, "uri") + if strings.Contains(releaseURI, ":") { + parts := strings.Split(releaseURI, ":") + releaseID = parts[len(parts)-1] + } + } + + var year interface{} + if yearVal, exists := dateInfo["year"]; exists { + year = yearVal + } + + return map[string]interface{}{ + "id": releaseID, + "name": getString(release, "name"), + "cover": cover, + "date": releaseDate, + "year": year, + } +} + +func extractDiscographyItems(itemsData map[string]interface{}) []map[string]interface{} { + items := []map[string]interface{}{} + dataItems := getSlice(itemsData, "items") + if dataItems != nil { + for _, item := range dataItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + releases := getMap(itemMap, "releases") + var release map[string]interface{} + if len(releases) > 0 { + releaseItems := getSlice(releases, "items") + if releaseItems != nil && len(releaseItems) > 0 { + if releaseMap, ok := releaseItems[0].(map[string]interface{}); ok { + release = releaseMap + } + } + } else { + release = getMap(itemMap, "album") + } + + if len(release) > 0 { + extracted := extractRelease(release) + if extracted != nil { + items = append(items, extracted) + } + } + } + } + return items +} + +func FilterArtist(data map[string]interface{}) map[string]interface{} { + dataMap := getMap(data, "data") + artistData := getMap(dataMap, "artistUnion") + if len(artistData) == 0 { + return make(map[string]interface{}) + } + + profileRaw := getMap(artistData, "profile") + profile := make(map[string]interface{}) + if len(profileRaw) > 0 { + if biography, exists := profileRaw["biography"]; exists { + biographyMap, ok := biography.(map[string]interface{}) + if ok { + biographyText := getString(biographyMap, "text") + if biographyText != "" { + profile["biography"] = html.UnescapeString(biographyText) + } + } + } + if _, exists := profileRaw["name"]; exists { + profile["name"] = getString(profileRaw, "name") + } + if _, exists := profileRaw["verified"]; exists { + profile["verified"] = getBool(profileRaw, "verified") + } + } + + headerImageData := getMap(artistData, "headerImage") + var headerImage interface{} + if len(headerImageData) > 0 { + headerData := getMap(headerImageData, "data") + if len(headerData) > 0 { + sources := getSlice(headerData, "sources") + if sources != nil && len(sources) > 0 { + if firstSource, ok := sources[0].(map[string]interface{}); ok { + headerImage = getString(firstSource, "url") + } + } + } + } + + statsRaw := getMap(artistData, "stats") + stats := make(map[string]interface{}) + if len(statsRaw) > 0 { + if _, exists := statsRaw["followers"]; exists { + stats["followers"] = getFloat64(statsRaw, "followers") + } + if _, exists := statsRaw["monthlyListeners"]; exists { + stats["listeners"] = getFloat64(statsRaw, "monthlyListeners") + } + if _, exists := statsRaw["worldRank"]; exists { + stats["rank"] = getFloat64(statsRaw, "worldRank") + } + } + + discography := getMap(artistData, "discography") + discographyResult := make(map[string]interface{}) + + allData := getMap(discography, "all") + if len(allData) > 0 { + discographyResult["all"] = extractDiscographyItems(allData) + if totalCount, exists := allData["totalCount"]; exists { + var total float64 + if tc, ok := totalCount.(float64); ok { + total = tc + } else if tc, ok := totalCount.(int); ok { + total = float64(tc) + } else if tc, ok := totalCount.(int64); ok { + total = float64(tc) + } + discographyResult["total"] = total + } + } + + visualsData := getMap(artistData, "visuals") + galleryData := getMap(visualsData, "gallery") + gallery := []interface{}{} + if len(galleryData) > 0 { + galleryItems := getSlice(galleryData, "items") + if galleryItems != nil { + for _, item := range galleryItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + sources := getSlice(itemMap, "sources") + if sources != nil && len(sources) > 0 { + if firstSource, ok := sources[0].(map[string]interface{}); ok { + galleryURL := getString(firstSource, "url") + if galleryURL != "" { + gallery = append(gallery, galleryURL) + } + } + } + } + } + } + + avatarObj := extractCoverImage(getMap(visualsData, "avatarImage")) + var avatar interface{} + if avatarObj != nil { + if mediumURL, ok := avatarObj["medium"].(string); ok && mediumURL != "" { + avatar = mediumURL + } else if smallURL, ok := avatarObj["small"].(string); ok && smallURL != "" { + avatar = smallURL + } + } + + artistURI := getString(artistData, "uri") + artistID := "" + if strings.Contains(artistURI, ":") { + parts := strings.Split(artistURI, ":") + artistID = parts[len(parts)-1] + } + + filtered := map[string]interface{}{ + "id": artistID, + "name": getString(profile, "name"), + "profile": profile, + "avatar": avatar, + "header": headerImage, + "stats": stats, + "gallery": gallery, + "discography": discographyResult, + } + + return filtered +} + +func FilterSearch(data map[string]interface{}) map[string]interface{} { + dataMap := getMap(data, "data") + searchData := getMap(dataMap, "searchV2") + if len(searchData) == 0 { + return make(map[string]interface{}) + } + + results := map[string]interface{}{ + "tracks": []map[string]interface{}{}, + "albums": []map[string]interface{}{}, + "artists": []map[string]interface{}{}, + "playlists": []map[string]interface{}{}, + } + + tracksData := getMap(searchData, "tracksV2") + if len(tracksData) == 0 { + tracksData = getMap(searchData, "tracks") + } + trackItems := getSlice(tracksData, "items") + if trackItems != nil { + for _, item := range trackItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + var track map[string]interface{} + if itemData, exists := itemMap["item"]; exists { + itemDataMap, ok := itemData.(map[string]interface{}) + if ok { + track = getMap(itemDataMap, "data") + } + } else if trackData, exists := itemMap["track"]; exists { + if trackMap, ok := trackData.(map[string]interface{}); ok { + track = trackMap + } + } + + if len(track) == 0 { + continue + } + + trackArtists := extractArtists(getMap(track, "artists")) + trackDurationMs := getFloat64(getMap(track, "duration"), "totalMilliseconds") + if trackDurationMs == 0 { + trackDurationMs = getFloat64(getMap(track, "trackDuration"), "totalMilliseconds") + } + trackDuration := extractDuration(trackDurationMs) + + albumData := getMap(track, "albumOfTrack") + var albumInfo map[string]interface{} + if len(albumData) > 0 { + albumURI := getString(albumData, "uri") + albumID := getString(albumData, "id") + if albumID == "" { + if strings.Contains(albumURI, ":") { + parts := strings.Split(albumURI, ":") + albumID = parts[len(parts)-1] + } + } + albumInfo = map[string]interface{}{ + "name": getString(albumData, "name"), + "uri": albumURI, + "id": albumID, + } + } + + trackURI := getString(track, "uri") + trackID := getString(track, "id") + if trackID == "" { + if strings.Contains(trackURI, ":") { + parts := strings.Split(trackURI, ":") + trackID = parts[len(parts)-1] + } + } + + coverObj := extractCoverImage(getMap(albumData, "coverArt")) + var cover interface{} + if coverObj != nil { + cover = getString(coverObj, "medium") + } + + trackName := getString(track, "name") + if trackName == "" { + continue + } + + trackArtistNames := []string{} + for _, artist := range trackArtists { + trackArtistNames = append(trackArtistNames, getString(artist, "name")) + } + trackArtistsString := strings.Join(trackArtistNames, ", ") + + durationString := getString(trackDuration, "formatted") + + albumName := "" + if albumInfo != nil { + albumName = getString(albumInfo, "name") + } + + trackResults := results["tracks"].([]map[string]interface{}) + trackResults = append(trackResults, map[string]interface{}{ + "id": trackID, + "name": trackName, + "artists": trackArtistsString, + "album": albumName, + "duration": durationString, + "cover": cover, + }) + results["tracks"] = trackResults + } + } + + albumsData := getMap(searchData, "albumsV2") + if len(albumsData) == 0 { + albumsData = getMap(searchData, "albums") + } + albumItems := getSlice(albumsData, "items") + if albumItems != nil { + for _, item := range albumItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + var album map[string]interface{} + if itemData, exists := itemMap["data"]; exists { + if albumMap, ok := itemData.(map[string]interface{}); ok { + album = albumMap + } + } else if albumData, exists := itemMap["album"]; exists { + if albumMap, ok := albumData.(map[string]interface{}); ok { + album = albumMap + } + } + + if len(album) == 0 { + continue + } + + albumArtists := extractArtists(getMap(album, "artists")) + albumURI := getString(album, "uri") + albumID := getString(album, "id") + if albumID == "" { + if strings.Contains(albumURI, ":") { + parts := strings.Split(albumURI, ":") + albumID = parts[len(parts)-1] + } + } + + coverObj := extractCoverImage(getMap(album, "coverArt")) + var cover interface{} + if coverObj != nil { + cover = getString(coverObj, "medium") + } + + albumArtistNames := []string{} + for _, artist := range albumArtists { + albumArtistNames = append(albumArtistNames, getString(artist, "name")) + } + albumArtistsString := strings.Join(albumArtistNames, ", ") + + dateInfo := getMap(album, "date") + var year interface{} + if len(dateInfo) > 0 { + if yearVal, exists := dateInfo["year"]; exists { + year = yearVal + } + } + + albumName := getString(album, "name") + if albumName == "" || albumArtistsString == "" { + continue + } + + albumResult := map[string]interface{}{ + "id": albumID, + "name": albumName, + "artists": albumArtistsString, + "cover": cover, + } + + if year != nil { + albumResult["year"] = year + } + + albumResults := results["albums"].([]map[string]interface{}) + albumResults = append(albumResults, albumResult) + results["albums"] = albumResults + } + } + + artistsData := getMap(searchData, "artistsV2") + if len(artistsData) == 0 { + artistsData = getMap(searchData, "artists") + } + artistItems := getSlice(artistsData, "items") + if artistItems != nil { + for _, item := range artistItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + var artist map[string]interface{} + if itemData, exists := itemMap["data"]; exists { + if artistMap, ok := itemData.(map[string]interface{}); ok { + artist = artistMap + } + } else if artistData, exists := itemMap["artist"]; exists { + if artistMap, ok := artistData.(map[string]interface{}); ok { + artist = artistMap + } + } + + if len(artist) == 0 { + continue + } + + artistURI := getString(artist, "uri") + artistID := "" + if strings.Contains(artistURI, ":") { + parts := strings.Split(artistURI, ":") + artistID = parts[len(parts)-1] + } + + coverObj := extractCoverImage(getMap(artist, "visualIdentity")) + if coverObj == nil { + visuals := getMap(artist, "visuals") + if len(visuals) > 0 { + coverObj = extractCoverImage(getMap(visuals, "avatarImage")) + } + } + + var cover interface{} + if coverObj != nil { + cover = getString(coverObj, "medium") + } + + artistName := getString(getMap(artist, "profile"), "name") + if artistName == "" { + artistName = getString(artist, "name") + } + + if artistName == "" { + continue + } + + artistResults := results["artists"].([]map[string]interface{}) + artistResults = append(artistResults, map[string]interface{}{ + "id": artistID, + "name": artistName, + "cover": cover, + }) + results["artists"] = artistResults + } + } + + playlistsData := getMap(searchData, "playlistsV2") + if len(playlistsData) == 0 { + playlistsData = getMap(searchData, "playlists") + } + playlistItems := getSlice(playlistsData, "items") + if playlistItems != nil { + for _, item := range playlistItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + var playlist map[string]interface{} + if itemData, exists := itemMap["data"]; exists { + if playlistMap, ok := itemData.(map[string]interface{}); ok { + playlist = playlistMap + } + } else if playlistData, exists := itemMap["playlist"]; exists { + if playlistMap, ok := playlistData.(map[string]interface{}); ok { + playlist = playlistMap + } + } + + if len(playlist) == 0 { + continue + } + + playlistURI := getString(playlist, "uri") + playlistID := "" + if strings.Contains(playlistURI, ":") { + parts := strings.Split(playlistURI, ":") + playlistID = parts[len(parts)-1] + } + + playlistImages := getMap(playlist, "images") + if len(playlistImages) == 0 { + playlistImages = getMap(playlist, "imagesV2") + } + var playlistCoverObj map[string]interface{} + if len(playlistImages) > 0 { + imageItems := getSlice(playlistImages, "items") + if imageItems != nil && len(imageItems) > 0 { + if firstImage, ok := imageItems[0].(map[string]interface{}); ok { + firstSources := getSlice(firstImage, "sources") + if firstSources != nil { + playlistCoverObj = extractCoverImage(map[string]interface{}{"sources": firstSources}) + } + } + } + if playlistCoverObj == nil { + playlistCoverObj = extractCoverImage(playlistImages) + } + } + + var playlistCover interface{} + if playlistCoverObj != nil { + playlistCover = getString(playlistCoverObj, "medium") + } + + ownerData := getMap(getMap(playlist, "ownerV2"), "data") + ownerName := getString(ownerData, "name") + + playlistName := getString(playlist, "name") + if playlistName == "" { + continue + } + + playlistResult := map[string]interface{}{ + "id": playlistID, + "name": playlistName, + "cover": playlistCover, + } + + if ownerName != "" { + playlistResult["owner"] = ownerName + } + + playlistResults := results["playlists"].([]map[string]interface{}) + playlistResults = append(playlistResults, playlistResult) + results["playlists"] = playlistResults + } + } + + tracks := results["tracks"].([]map[string]interface{}) + albums := results["albums"].([]map[string]interface{}) + artists := results["artists"].([]map[string]interface{}) + playlists := results["playlists"].([]map[string]interface{}) + + return map[string]interface{}{ + "results": results, + "totalResults": map[string]interface{}{ + "tracks": len(tracks), + "albums": len(albums), + "artists": len(artists), + "playlists": len(playlists), + }, + } +} diff --git a/backend/spotify_metadata.go b/backend/spotify_metadata.go index 2a0da23..f46706a 100644 --- a/backend/spotify_metadata.go +++ b/backend/spotify_metadata.go @@ -2,11 +2,9 @@ package backend import ( "context" - "encoding/base64" "encoding/json" "errors" "fmt" - "io" "net/http" "net/url" "strconv" @@ -14,11 +12,6 @@ import ( "time" ) -const ( - apiBaseURL = "https://afkarxyz.web.id" - apiKey = "NDAwNDAxNDAzNDA0NTAwNTAyNTAz" -) - var ( errInvalidSpotifyURL = errors.New("invalid or unsupported Spotify URL") ) @@ -186,7 +179,6 @@ type apiTrackResponse struct { Disc int `json:"disc"` Discs int `json:"discs"` Copyright string `json:"copyright"` - Label string `json:"label"` Plays string `json:"plays"` Album struct { ID string `json:"id"` @@ -386,39 +378,429 @@ func (c *SpotifyMetadataClient) processSpotifyData(ctx context.Context, raw inte } func (c *SpotifyMetadataClient) fetchTrack(ctx context.Context, trackID string) (*apiTrackResponse, error) { - url := fmt.Sprintf("%s/track/%s", apiBaseURL, trackID) - var data apiTrackResponse - if err := c.getJSON(ctx, url, &data); err != nil { - return nil, err + client := NewSpotifyClient() + if err := client.Initialize(); err != nil { + return nil, fmt.Errorf("failed to initialize spotify client: %w", err) } - return &data, nil + + payload := map[string]interface{}{ + "variables": map[string]interface{}{ + "uri": fmt.Sprintf("spotify:track:%s", trackID), + }, + "operationName": "getTrack", + "extensions": map[string]interface{}{ + "persistedQuery": map[string]interface{}{ + "version": 1, + "sha256Hash": "612585ae06ba435ad26369870deaae23b5c8800a256cd8a57e08eddc25a37294", + }, + }, + } + + data, err := client.Query(payload) + if err != nil { + return nil, fmt.Errorf("failed to query track: %w", err) + } + + var albumFetchData map[string]interface{} + if trackData, ok := data["data"].(map[string]interface{}); ok { + if trackUnion, ok := trackData["trackUnion"].(map[string]interface{}); ok { + if albumOfTrack, ok := trackUnion["albumOfTrack"].(map[string]interface{}); ok { + albumID := "" + if id, ok := albumOfTrack["id"].(string); ok && id != "" { + albumID = id + } else if uri, ok := albumOfTrack["uri"].(string); ok && uri != "" { + if strings.Contains(uri, ":") { + parts := strings.Split(uri, ":") + if len(parts) > 0 { + albumID = parts[len(parts)-1] + } + } + } + + if albumID != "" { + albumPayload := map[string]interface{}{ + "variables": map[string]interface{}{ + "uri": fmt.Sprintf("spotify:album:%s", albumID), + "locale": "", + "offset": 0, + "limit": 1, + }, + "operationName": "getAlbum", + "extensions": map[string]interface{}{ + "persistedQuery": map[string]interface{}{ + "version": 1, + "sha256Hash": "b9bfabef66ed756e5e13f68a942deb60bd4125ec1f1be8cc42769dc0259b4b10", + }, + }, + } + albumFetchData, _ = client.Query(albumPayload) + } + } + } + } + + filteredData := FilterTrack(data, albumFetchData) + + jsonData, err := json.Marshal(filteredData) + if err != nil { + return nil, fmt.Errorf("failed to marshal filtered data: %w", err) + } + + var result apiTrackResponse + if err := json.Unmarshal(jsonData, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal to apiTrackResponse: %w", err) + } + + return &result, nil } func (c *SpotifyMetadataClient) fetchAlbum(ctx context.Context, albumID string) (*apiAlbumResponse, error) { - url := fmt.Sprintf("%s/album/%s", apiBaseURL, albumID) - var data apiAlbumResponse - if err := c.getJSON(ctx, url, &data); err != nil { - return nil, err + client := NewSpotifyClient() + if err := client.Initialize(); err != nil { + return nil, fmt.Errorf("failed to initialize spotify client: %w", err) } - return &data, nil + + allItems := []interface{}{} + offset := 0 + limit := 1000 + var totalCount interface{} + var data map[string]interface{} + + for { + payload := map[string]interface{}{ + "variables": map[string]interface{}{ + "uri": fmt.Sprintf("spotify:album:%s", albumID), + "locale": "", + "offset": offset, + "limit": limit, + }, + "operationName": "getAlbum", + "extensions": map[string]interface{}{ + "persistedQuery": map[string]interface{}{ + "version": 1, + "sha256Hash": "b9bfabef66ed756e5e13f68a942deb60bd4125ec1f1be8cc42769dc0259b4b10", + }, + }, + } + + response, err := client.Query(payload) + if err != nil { + return nil, fmt.Errorf("failed to query album: %w", err) + } + + if data == nil { + data = response + } + + albumData := getMap(getMap(response, "data"), "albumUnion") + tracksData := getMap(albumData, "tracksV2") + items := getSlice(tracksData, "items") + + if items == nil || len(items) == 0 { + break + } + + allItems = append(allItems, items...) + + if totalCount == nil { + if tc, ok := tracksData["totalCount"].(float64); ok { + totalCount = int(tc) + } else { + totalCount = len(items) + } + } + + tcInt := 0 + if tc, ok := totalCount.(int); ok { + tcInt = tc + } else if tc, ok := totalCount.(float64); ok { + tcInt = int(tc) + } + + if len(allItems) >= tcInt || len(items) < limit { + break + } + + offset += limit + } + + if data != nil && len(allItems) > 0 { + dataMap := getMap(data, "data") + albumUnion := getMap(dataMap, "albumUnion") + tracksV2 := getMap(albumUnion, "tracksV2") + tracksV2["items"] = allItems + tracksV2["totalCount"] = len(allItems) + } + + filteredData := FilterAlbum(data) + + jsonData, err := json.Marshal(filteredData) + if err != nil { + return nil, fmt.Errorf("failed to marshal filtered data: %w", err) + } + + var result apiAlbumResponse + if err := json.Unmarshal(jsonData, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal to apiAlbumResponse: %w", err) + } + + return &result, nil } func (c *SpotifyMetadataClient) fetchPlaylist(ctx context.Context, playlistID string) (*apiPlaylistResponse, error) { - url := fmt.Sprintf("%s/playlist/%s", apiBaseURL, playlistID) - var data apiPlaylistResponse - if err := c.getJSON(ctx, url, &data); err != nil { - return nil, err + client := NewSpotifyClient() + if err := client.Initialize(); err != nil { + return nil, fmt.Errorf("failed to initialize spotify client: %w", err) } - return &data, nil + + allItems := []interface{}{} + offset := 0 + limit := 1000 + var totalCount interface{} + var data map[string]interface{} + + for { + payload := map[string]interface{}{ + "variables": map[string]interface{}{ + "uri": fmt.Sprintf("spotify:playlist:%s", playlistID), + "offset": offset, + "limit": limit, + "enableWatchFeedEntrypoint": false, + }, + "operationName": "fetchPlaylist", + "extensions": map[string]interface{}{ + "persistedQuery": map[string]interface{}{ + "version": 1, + "sha256Hash": "bb67e0af06e8d6f52b531f97468ee4acd44cd0f82b988e15c2ea47b1148efc77", + }, + }, + } + + response, err := client.Query(payload) + if err != nil { + return nil, fmt.Errorf("failed to query playlist: %w", err) + } + + if data == nil { + data = response + } + + playlistData := getMap(getMap(response, "data"), "playlistV2") + content := getMap(playlistData, "content") + items := getSlice(content, "items") + + if items == nil || len(items) == 0 { + break + } + + allItems = append(allItems, items...) + + if totalCount == nil { + if tc, ok := content["totalCount"].(float64); ok { + totalCount = int(tc) + } else { + totalCount = len(items) + } + } + + tcInt := 0 + if tc, ok := totalCount.(int); ok { + tcInt = tc + } else if tc, ok := totalCount.(float64); ok { + tcInt = int(tc) + } + + if len(allItems) >= tcInt || len(items) < limit { + break + } + + offset += limit + } + + if data != nil && len(allItems) > 0 { + dataMap := getMap(data, "data") + playlistV2 := getMap(dataMap, "playlistV2") + content := getMap(playlistV2, "content") + content["items"] = allItems + content["totalCount"] = len(allItems) + } + + filteredData := FilterPlaylist(data) + + jsonData, err := json.Marshal(filteredData) + if err != nil { + return nil, fmt.Errorf("failed to marshal filtered data: %w", err) + } + + var result apiPlaylistResponse + if err := json.Unmarshal(jsonData, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal to apiPlaylistResponse: %w", err) + } + + return &result, nil } func (c *SpotifyMetadataClient) fetchArtistDiscography(ctx context.Context, parsed spotifyURI) (*apiArtistResponse, error) { - url := fmt.Sprintf("%s/artist/%s", apiBaseURL, parsed.ID) - var data apiArtistResponse - if err := c.getJSON(ctx, url, &data); err != nil { - return nil, err + client := NewSpotifyClient() + if err := client.Initialize(); err != nil { + return nil, fmt.Errorf("failed to initialize spotify client: %w", err) } - return &data, nil + + overviewPayload := map[string]interface{}{ + "variables": map[string]interface{}{ + "uri": fmt.Sprintf("spotify:artist:%s", parsed.ID), + "locale": "", + }, + "operationName": "queryArtistOverview", + "extensions": map[string]interface{}{ + "persistedQuery": map[string]interface{}{ + "version": 1, + "sha256Hash": "446130b4a0aa6522a686aafccddb0ae849165b5e0436fd802f96e0243617b5d8", + }, + }, + } + + data, err := client.Query(overviewPayload) + if err != nil { + return nil, fmt.Errorf("failed to query artist overview: %w", err) + } + + allDiscographyItems := []interface{}{} + offset := 0 + limit := 50 + var totalCount interface{} + + for { + discographyPayload := map[string]interface{}{ + "variables": map[string]interface{}{ + "uri": fmt.Sprintf("spotify:artist:%s", parsed.ID), + "offset": offset, + "limit": limit, + "order": "DATE_DESC", + }, + "operationName": "queryArtistDiscographyAll", + "extensions": map[string]interface{}{ + "persistedQuery": map[string]interface{}{ + "version": 1, + "sha256Hash": "5e07d323febb57b4a56a42abbf781490e58764aa45feb6e3dc0591564fc56599", + }, + }, + } + + response, err := client.Query(discographyPayload) + if err != nil { + break + } + + discographyData := getMap(getMap(getMap(response, "data"), "artistUnion"), "discography") + allData := getMap(discographyData, "all") + items := getSlice(allData, "items") + + if items == nil || len(items) == 0 { + break + } + + allDiscographyItems = append(allDiscographyItems, items...) + + if totalCount == nil { + if tc, ok := allData["totalCount"].(float64); ok { + totalCount = int(tc) + } else { + totalCount = len(items) + } + } + + tcInt := 0 + if tc, ok := totalCount.(int); ok { + tcInt = tc + } else if tc, ok := totalCount.(float64); ok { + tcInt = int(tc) + } + + if len(allDiscographyItems) >= tcInt || len(items) < limit { + break + } + + offset += limit + } + + albumsItems := []interface{}{} + compilationsItems := []interface{}{} + singlesItems := []interface{}{} + + for _, item := range allDiscographyItems { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + + releases := getMap(itemMap, "releases") + releaseItems := getSlice(releases, "items") + var release map[string]interface{} + if len(releaseItems) > 0 { + if r, ok := releaseItems[0].(map[string]interface{}); ok { + release = r + } + } + + if release != nil { + releaseType := getString(release, "type") + switch releaseType { + case "ALBUM": + albumsItems = append(albumsItems, item) + case "COMPILATION": + compilationsItems = append(compilationsItems, item) + case "SINGLE": + singlesItems = append(singlesItems, item) + default: + singlesItems = append(singlesItems, item) + } + } + } + + if len(allDiscographyItems) > 0 { + dataMap := getMap(data, "data") + artistUnion := getMap(dataMap, "artistUnion") + discographyMap := getMap(artistUnion, "discography") + + if len(albumsItems) > 0 { + discographyMap["albums"] = map[string]interface{}{ + "items": albumsItems, + "totalCount": len(albumsItems), + } + } + if len(compilationsItems) > 0 { + discographyMap["compilations"] = map[string]interface{}{ + "items": compilationsItems, + "totalCount": len(compilationsItems), + } + } + if len(singlesItems) > 0 { + discographyMap["singles"] = map[string]interface{}{ + "items": singlesItems, + "totalCount": len(singlesItems), + } + } + + discographyMap["all"] = map[string]interface{}{ + "items": allDiscographyItems, + "totalCount": len(allDiscographyItems), + } + } + + filteredData := FilterArtist(data) + + jsonData, err := json.Marshal(filteredData) + if err != nil { + return nil, fmt.Errorf("failed to marshal filtered data: %w", err) + } + + var result apiArtistResponse + if err := json.Unmarshal(jsonData, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal to apiArtistResponse: %w", err) + } + + return &result, nil } func (c *SpotifyMetadataClient) formatTrackData(raw *apiTrackResponse) TrackResponse { @@ -690,39 +1072,6 @@ func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context, }, nil } -func (c *SpotifyMetadataClient) getJSON(ctx context.Context, endpoint string, dst interface{}) error { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return err - } - - decodedKey, err := base64.StdEncoding.DecodeString(apiKey) - if err != nil { - return fmt.Errorf("failed to decode API key: %w", err) - } - req.Header.Set("Accept", "application/json") - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") - req.Header.Set("X-API-Key", string(decodedKey)) - - resp, err := c.httpClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return fmt.Errorf("API returned status %d for %s: %s", resp.StatusCode, endpoint, string(body)) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - - return json.Unmarshal(body, dst) -} - func parseDuration(durationStr string) int { if durationStr == "" { return 0 @@ -821,7 +1170,6 @@ func cleanPathParts(path string) []string { } func parseArtistIDsFromString(artists string) []string { - return []string{} } @@ -834,12 +1182,46 @@ func (c *SpotifyMetadataClient) Search(ctx context.Context, query string, limit limit = 50 } - encodedQuery := url.QueryEscape(query) - searchURL := fmt.Sprintf("%s/search?q=%s&limit=%d&offset=0", apiBaseURL, encodedQuery, limit) + client := NewSpotifyClient() + if err := client.Initialize(); err != nil { + return nil, fmt.Errorf("failed to initialize spotify client: %w", err) + } + + payload := map[string]interface{}{ + "variables": map[string]interface{}{ + "searchTerm": query, + "offset": 0, + "limit": limit, + "numberOfTopResults": 5, + "includeAudiobooks": true, + "includeArtistHasConcertsField": false, + "includePreReleases": true, + "includeAuthors": false, + }, + "operationName": "searchDesktop", + "extensions": map[string]interface{}{ + "persistedQuery": map[string]interface{}{ + "version": 1, + "sha256Hash": "fcad5a3e0d5af727fb76966f06971c19cfa2275e6ff7671196753e008611873c", + }, + }, + } + + data, err := client.Query(payload) + if err != nil { + return nil, fmt.Errorf("failed to query search: %w", err) + } + + filteredData := FilterSearch(data) + + jsonData, err := json.Marshal(filteredData) + if err != nil { + return nil, fmt.Errorf("failed to marshal filtered data: %w", err) + } var apiResp apiSearchResponse - if err := c.getJSON(ctx, searchURL, &apiResp); err != nil { - return nil, fmt.Errorf("search failed: %w", err) + if err := json.Unmarshal(jsonData, &apiResp); err != nil { + return nil, fmt.Errorf("failed to unmarshal to apiSearchResponse: %w", err) } response := &SearchResponse{ @@ -916,12 +1298,46 @@ func (c *SpotifyMetadataClient) SearchByType(ctx context.Context, query string, offset = 0 } - encodedQuery := url.QueryEscape(query) - searchURL := fmt.Sprintf("%s/search?q=%s&limit=%d&offset=%d", apiBaseURL, encodedQuery, limit, offset) + client := NewSpotifyClient() + if err := client.Initialize(); err != nil { + return nil, fmt.Errorf("failed to initialize spotify client: %w", err) + } + + payload := map[string]interface{}{ + "variables": map[string]interface{}{ + "searchTerm": query, + "offset": offset, + "limit": limit, + "numberOfTopResults": 5, + "includeAudiobooks": true, + "includeArtistHasConcertsField": false, + "includePreReleases": true, + "includeAuthors": false, + }, + "operationName": "searchDesktop", + "extensions": map[string]interface{}{ + "persistedQuery": map[string]interface{}{ + "version": 1, + "sha256Hash": "fcad5a3e0d5af727fb76966f06971c19cfa2275e6ff7671196753e008611873c", + }, + }, + } + + data, err := client.Query(payload) + if err != nil { + return nil, fmt.Errorf("failed to query search: %w", err) + } + + filteredData := FilterSearch(data) + + jsonData, err := json.Marshal(filteredData) + if err != nil { + return nil, fmt.Errorf("failed to marshal filtered data: %w", err) + } var apiResp apiSearchResponse - if err := c.getJSON(ctx, searchURL, &apiResp); err != nil { - return nil, fmt.Errorf("search failed: %w", err) + if err := json.Unmarshal(jsonData, &apiResp); err != nil { + return nil, fmt.Errorf("failed to unmarshal to apiSearchResponse: %w", err) } results := make([]SearchResult, 0) diff --git a/frontend/package.json b/frontend/package.json index a4f3e89..45d3cf8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,7 +27,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", - "motion": "^12.24.12", + "motion": "^12.25.0", "next-themes": "^0.4.6", "react": "^19.2.3", "react-dom": "^19.2.3", @@ -37,8 +37,8 @@ }, "devDependencies": { "@eslint/js": "^9.39.2", - "@types/node": "^25.0.3", - "@types/react": "^19.2.7", + "@types/node": "^25.0.6", + "@types/react": "^19.2.8", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.2", "eslint": "^9.39.2", diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index a438c3e..e0bb2c9 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -be90455e8d3a26cf5c12d4fa0779bc1a \ No newline at end of file +6f2a6dc27f7d8d215283f6d07b4eaa54 \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 461df76..9d25865 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -10,40 +10,40 @@ importers: dependencies: '@radix-ui/react-checkbox': specifier: ^1.3.3 - version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-context-menu': specifier: ^2.2.16 - version: 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-dialog': specifier: ^1.1.15 - version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-label': specifier: ^2.1.8 - version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-progress': specifier: ^1.1.8 - version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-select': specifier: ^2.2.6 - version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-slot': specifier: ^1.2.4 - version: 1.2.4(@types/react@19.2.7)(react@19.2.3) + version: 1.2.4(@types/react@19.2.8)(react@19.2.3) '@radix-ui/react-switch': specifier: ^1.2.6 - version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-toggle': specifier: ^1.1.10 - version: 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-toggle-group': specifier: ^1.1.11 - version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-tooltip': specifier: ^1.2.8 - version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tailwindcss/vite': specifier: ^4.1.18 - version: 4.1.18(vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)) + version: 4.1.18(vite@7.3.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -54,8 +54,8 @@ importers: specifier: ^0.562.0 version: 0.562.0(react@19.2.3) motion: - specifier: ^12.24.12 - version: 12.24.12(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^12.25.0 + version: 12.25.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -79,17 +79,17 @@ importers: specifier: ^9.39.2 version: 9.39.2 '@types/node': - specifier: ^25.0.3 - version: 25.0.3 + specifier: ^25.0.6 + version: 25.0.6 '@types/react': - specifier: ^19.2.7 - version: 19.2.7 + specifier: ^19.2.8 + version: 19.2.8 '@types/react-dom': specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.7) + version: 19.2.3(@types/react@19.2.8) '@vitejs/plugin-react': specifier: ^5.1.2 - version: 5.1.2(vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)) + version: 5.1.2(vite@7.3.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)) eslint: specifier: ^9.39.2 version: 9.39.2(jiti@2.6.1) @@ -116,7 +116,7 @@ importers: version: 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.3.1 - version: 7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2) + version: 7.3.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2) packages: @@ -1259,16 +1259,16 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@25.0.3': - resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} + '@types/node@25.0.6': + resolution: {integrity: sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q==} '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: '@types/react': ^19.2.0 - '@types/react@19.2.7': - resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + '@types/react@19.2.8': + resolution: {integrity: sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==} '@typescript-eslint/eslint-plugin@8.52.0': resolution: {integrity: sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q==} @@ -1362,8 +1362,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.9.13: - resolution: {integrity: sha512-WhtvB2NG2wjr04+h77sg3klAIwrgOqnjS49GGudnUPGFFgg7G17y7Qecqp+2Dr5kUDxNRBca0SK7cG8JwzkWDQ==} + baseline-browser-mapping@2.9.14: + resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==} hasBin: true brace-expansion@1.1.12: @@ -1381,8 +1381,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001763: - resolution: {integrity: sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==} + caniuse-lite@1.0.30001764: + resolution: {integrity: sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -1540,8 +1540,8 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - framer-motion@12.24.12: - resolution: {integrity: sha512-W+tBOI1SDGNMH4D4mADY95qYd16Drke2Tj9zlGlwTGSCi6yy8wbMmPY1mvirfcTK8HBeuuCd2PflHdN/zbL4ew==} + framer-motion@12.25.0: + resolution: {integrity: sha512-mlWqd0rApIjeyhTCSNCqPYsUAEhkcUukZxH3ke6KbstBRPcxhEpuIjmiUQvB+1E9xkEm5SpNHBgHCapH/QHTWg==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -1757,8 +1757,8 @@ packages: motion-utils@12.24.10: resolution: {integrity: sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==} - motion@12.24.12: - resolution: {integrity: sha512-usaP62NpHmM8++QrEnNoCco6qrtK1AtzkeHfgW+4qICE0k7ykK+dPJGaRjEzo7sF1GcrYskrGBB/r5RtqnminQ==} + motion@12.25.0: + resolution: {integrity: sha512-jBFohEYklpZ+TL64zv03sHdqr1Tsc8/yDy7u68hVzi7hTJYtv53AduchqCiY3aWi4vY1hweS8DWtgCuckusYdQ==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -2457,424 +2457,424 @@ snapshots: '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.7)(react@19.2.3)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.8)(react@19.2.3)': dependencies: react: 19.2.3 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-context@1.1.2(@types/react@19.2.7)(react@19.2.3)': + '@radix-ui/react-context@1.1.2(@types/react@19.2.8)(react@19.2.3)': dependencies: react: 19.2.3 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@radix-ui/react-context@1.1.3(@types/react@19.2.7)(react@19.2.3)': + '@radix-ui/react-context@1.1.3(@types/react@19.2.8)(react@19.2.3)': dependencies: react: 19.2.3 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) aria-hidden: 1.2.6 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - react-remove-scroll: 2.7.2(@types/react@19.2.7)(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-direction@1.1.1(@types/react@19.2.7)(react@19.2.3)': + '@radix-ui/react-direction@1.1.1(@types/react@19.2.8)(react@19.2.3)': dependencies: react: 19.2.3 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.7)(react@19.2.3)': + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.8)(react@19.2.3)': dependencies: react: 19.2.3 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-id@1.1.1(@types/react@19.2.7)(react@19.2.3)': + '@radix-ui/react-id@1.1.1(@types/react@19.2.8)(react@19.2.3)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) aria-hidden: 1.2.6 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - react-remove-scroll: 2.7.2(@types/react@19.2.7)(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@floating-ui/react-dom': 2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.3) '@radix-ui/rect': 1.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-slot': 1.2.4(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-progress@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-progress@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-context': 1.1.3(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-context': 1.1.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) aria-hidden: 1.2.6 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - react-remove-scroll: 2.7.2(@types/react@19.2.7)(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-slot@1.2.3(@types/react@19.2.7)(react@19.2.3)': + '@radix-ui/react-slot@1.2.3(@types/react@19.2.8)(react@19.2.3)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@radix-ui/react-slot@1.2.4(@types/react@19.2.7)(react@19.2.3)': + '@radix-ui/react-slot@1.2.4(@types/react@19.2.8)(react@19.2.3)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.7)(react@19.2.3)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.8)(react@19.2.3)': dependencies: react: 19.2.3 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.7)(react@19.2.3)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.8)(react@19.2.3)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.7)(react@19.2.3) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.7)(react@19.2.3)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.8)(react@19.2.3)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.7)(react@19.2.3)': + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.8)(react@19.2.3)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.7)(react@19.2.3)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.8)(react@19.2.3)': dependencies: react: 19.2.3 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.7)(react@19.2.3)': + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.8)(react@19.2.3)': dependencies: react: 19.2.3 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.7)(react@19.2.3)': + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.8)(react@19.2.3)': dependencies: '@radix-ui/rect': 1.1.1 react: 19.2.3 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@radix-ui/react-use-size@1.1.1(@types/react@19.2.7)(react@19.2.3)': + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.8)(react@19.2.3)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) react: 19.2.3 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 - '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) '@radix-ui/rect@1.1.1': {} @@ -3016,12 +3016,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 - '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2))': + '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: '@tailwindcss/node': 4.1.18 '@tailwindcss/oxide': 4.1.18 tailwindcss: 4.1.18 - vite: 7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2) '@types/babel__core@7.20.5': dependencies: @@ -3048,15 +3048,15 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@25.0.3': + '@types/node@25.0.6': dependencies: undici-types: 7.16.0 - '@types/react-dom@19.2.3(@types/react@19.2.7)': + '@types/react-dom@19.2.3(@types/react@19.2.8)': dependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@types/react@19.2.7': + '@types/react@19.2.8': dependencies: csstype: 3.2.3 @@ -3151,7 +3151,7 @@ snapshots: '@typescript-eslint/types': 8.52.0 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2))': + '@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) @@ -3159,7 +3159,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.53 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2) transitivePeerDependencies: - supports-color @@ -3188,7 +3188,7 @@ snapshots: balanced-match@1.0.2: {} - baseline-browser-mapping@2.9.13: {} + baseline-browser-mapping@2.9.14: {} brace-expansion@1.1.12: dependencies: @@ -3201,15 +3201,15 @@ snapshots: browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.9.13 - caniuse-lite: 1.0.30001763 + baseline-browser-mapping: 2.9.14 + caniuse-lite: 1.0.30001764 electron-to-chromium: 1.5.267 node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) callsites@3.1.0: {} - caniuse-lite@1.0.30001763: {} + caniuse-lite@1.0.30001764: {} chalk@4.1.2: dependencies: @@ -3399,7 +3399,7 @@ snapshots: flatted@3.3.3: {} - framer-motion@12.24.12(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + framer-motion@12.25.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: motion-dom: 12.24.11 motion-utils: 12.24.10 @@ -3560,9 +3560,9 @@ snapshots: motion-utils@12.24.10: {} - motion@12.24.12(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + motion@12.25.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - framer-motion: 12.24.12(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.25.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tslib: 2.8.1 optionalDependencies: react: 19.2.3 @@ -3627,32 +3627,32 @@ snapshots: react-refresh@0.18.0: {} - react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.3): + react-remove-scroll-bar@2.3.8(@types/react@19.2.8)(react@19.2.3): dependencies: react: 19.2.3 - react-style-singleton: 2.2.3(@types/react@19.2.7)(react@19.2.3) + react-style-singleton: 2.2.3(@types/react@19.2.8)(react@19.2.3) tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - react-remove-scroll@2.7.2(@types/react@19.2.7)(react@19.2.3): + react-remove-scroll@2.7.2(@types/react@19.2.8)(react@19.2.3): dependencies: react: 19.2.3 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.7)(react@19.2.3) - react-style-singleton: 2.2.3(@types/react@19.2.7)(react@19.2.3) + react-remove-scroll-bar: 2.3.8(@types/react@19.2.8)(react@19.2.3) + react-style-singleton: 2.2.3(@types/react@19.2.8)(react@19.2.3) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.7)(react@19.2.3) - use-sidecar: 1.1.3(@types/react@19.2.7)(react@19.2.3) + use-callback-ref: 1.3.3(@types/react@19.2.8)(react@19.2.3) + use-sidecar: 1.1.3(@types/react@19.2.8)(react@19.2.3) optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - react-style-singleton@2.2.3(@types/react@19.2.7)(react@19.2.3): + react-style-singleton@2.2.3(@types/react@19.2.8)(react@19.2.3): dependencies: get-nonce: 1.0.1 react: 19.2.3 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 react@19.2.3: {} @@ -3793,22 +3793,22 @@ snapshots: dependencies: punycode: 2.3.1 - use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.3): + use-callback-ref@1.3.3(@types/react@19.2.8)(react@19.2.3): dependencies: react: 19.2.3 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - use-sidecar@1.1.3(@types/react@19.2.7)(react@19.2.3): + use-sidecar@1.1.3(@types/react@19.2.8)(react@19.2.3): dependencies: detect-node-es: 1.1.0 react: 19.2.3 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 - vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2): + vite@7.3.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -3817,7 +3817,7 @@ snapshots: rollup: 4.55.1 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.0.3 + '@types/node': 25.0.6 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9ab6032..da281db 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -50,7 +50,7 @@ function App() { const [showUnsavedChangesDialog, setShowUnsavedChangesDialog] = useState(false); const [resetSettingsFn, setResetSettingsFn] = useState<(() => void) | null>(null); const ITEMS_PER_PAGE = 50; - const CURRENT_VERSION = "7.0.1"; + const CURRENT_VERSION = "7.0.2"; const download = useDownload(); const metadata = useMetadata(); const lyrics = useLyrics(); diff --git a/frontend/src/components/ui/file-music.tsx b/frontend/src/components/ui/file-music.tsx index 64b2e40..aa0d576 100644 --- a/frontend/src/components/ui/file-music.tsx +++ b/frontend/src/components/ui/file-music.tsx @@ -4,16 +4,13 @@ import type { HTMLAttributes } from 'react'; import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; import { motion, useAnimation } from 'motion/react'; import { cn } from '@/lib/utils'; - export interface FileMusicIconHandle { startAnimation: () => void; stopAnimation: () => void; } - interface FileMusicIconProps extends HTMLAttributes { size?: number; } - const PATH_VARIANTS: Variants = { normal: { pathLength: 1, @@ -28,91 +25,40 @@ const PATH_VARIANTS: Variants = { }, }, }; - -const FileMusicIcon = forwardRef( - ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => { - const controls = useAnimation(); - const isControlledRef = useRef(false); - - useImperativeHandle(ref, () => { - isControlledRef.current = true; - return { - startAnimation: () => controls.start('animate'), - stopAnimation: () => controls.start('normal'), - }; - }); - - const handleMouseEnter = useCallback( - (e: React.MouseEvent) => { - if (!isControlledRef.current) { - controls.start('animate'); - } else { - onMouseEnter?.(e); - } - }, - [controls, onMouseEnter] - ); - - const handleMouseLeave = useCallback( - (e: React.MouseEvent) => { - if (!isControlledRef.current) { - controls.start('normal'); - } else { - onMouseLeave?.(e); - } - }, - [controls, onMouseLeave] - ); - - return ( -
- - - - - +const FileMusicIcon = forwardRef(({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => { + const controls = useAnimation(); + const isControlledRef = useRef(false); + useImperativeHandle(ref, () => { + isControlledRef.current = true; + return { + startAnimation: () => controls.start('animate'), + stopAnimation: () => controls.start('normal'), + }; + }); + const handleMouseEnter = useCallback((e: React.MouseEvent) => { + if (!isControlledRef.current) { + controls.start('animate'); + } + else { + onMouseEnter?.(e); + } + }, [controls, onMouseEnter]); + const handleMouseLeave = useCallback((e: React.MouseEvent) => { + if (!isControlledRef.current) { + controls.start('normal'); + } + else { + onMouseLeave?.(e); + } + }, [controls, onMouseLeave]); + return (
+ + + + + -
- ); - } -); - +
); +}); FileMusicIcon.displayName = 'FileMusicIcon'; export { FileMusicIcon }; diff --git a/frontend/src/components/ui/file-pen.tsx b/frontend/src/components/ui/file-pen.tsx index 7fae9f7..ccc727f 100644 --- a/frontend/src/components/ui/file-pen.tsx +++ b/frontend/src/components/ui/file-pen.tsx @@ -4,16 +4,13 @@ import type { HTMLAttributes } from 'react'; import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; import { motion, useAnimation } from 'motion/react'; import { cn } from '@/lib/utils'; - export interface FilePenIconHandle { startAnimation: () => void; stopAnimation: () => void; } - interface FilePenIconProps extends HTMLAttributes { size?: number; } - const PATH_VARIANTS: Variants = { normal: { pathLength: 1, @@ -28,83 +25,39 @@ const PATH_VARIANTS: Variants = { }, }, }; - -const FilePenIcon = forwardRef( - ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => { - const controls = useAnimation(); - const isControlledRef = useRef(false); - - useImperativeHandle(ref, () => { - isControlledRef.current = true; - return { - startAnimation: () => controls.start('animate'), - stopAnimation: () => controls.start('normal'), - }; - }); - - const handleMouseEnter = useCallback( - (e: React.MouseEvent) => { - if (!isControlledRef.current) { - controls.start('animate'); - } else { - onMouseEnter?.(e); - } - }, - [controls, onMouseEnter] - ); - - const handleMouseLeave = useCallback( - (e: React.MouseEvent) => { - if (!isControlledRef.current) { - controls.start('normal'); - } else { - onMouseLeave?.(e); - } - }, - [controls, onMouseLeave] - ); - - return ( -
- - - - +const FilePenIcon = forwardRef(({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => { + const controls = useAnimation(); + const isControlledRef = useRef(false); + useImperativeHandle(ref, () => { + isControlledRef.current = true; + return { + startAnimation: () => controls.start('animate'), + stopAnimation: () => controls.start('normal'), + }; + }); + const handleMouseEnter = useCallback((e: React.MouseEvent) => { + if (!isControlledRef.current) { + controls.start('animate'); + } + else { + onMouseEnter?.(e); + } + }, [controls, onMouseEnter]); + const handleMouseLeave = useCallback((e: React.MouseEvent) => { + if (!isControlledRef.current) { + controls.start('normal'); + } + else { + onMouseLeave?.(e); + } + }, [controls, onMouseLeave]); + return (
+ + + + -
- ); - } -); - +
); +}); FilePenIcon.displayName = 'FilePenIcon'; export { FilePenIcon }; diff --git a/go.mod b/go.mod index 815ad05..fd24061 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,14 @@ require ( github.com/go-flac/flacvorbis v0.2.0 github.com/go-flac/go-flac v1.0.0 github.com/mewkiz/flac v1.0.13 + github.com/pquerna/otp v1.5.0 github.com/ulikunitz/xz v0.5.15 github.com/wailsapp/wails/v2 v2.11.0 ) require ( github.com/bep/debounce v1.2.1 // indirect + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.2.0 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/go.sum b/go.sum index bf3672d..abf832d 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,9 @@ github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bogem/id3v2/v2 v2.1.4 h1:CEwe+lS2p6dd9UZRlPc1zbFNIha2mb2qzT1cCEoNWoI= github.com/bogem/id3v2/v2 v2.1.4/go.mod h1:l+gR8MZ6rc9ryPTPkX77smS5Me/36gxkMgDayZ9G1vY= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-flac/flacpicture v0.3.0 h1:LkmTxzFLIynwfhHiZsX0s8xcr3/u33MzvV89u+zOT8I= @@ -57,11 +60,15 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= +github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= diff --git a/wails.json b/wails.json index bf0bb68..3cc8acb 100644 --- a/wails.json +++ b/wails.json @@ -12,7 +12,7 @@ }, "info": { "productName": "SpotiFLAC", - "productVersion": "7.0.1", + "productVersion": "7.0.2", "copyright": "© 2026 afkarxyz", "comments": "Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music — no account required." },