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 (
-
-
);
+});
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."
},