mirror of
https://github.com/Jellify-Music/App.git
synced 2026-04-21 09:08:56 -05:00
Merge branch 'main' of https://github.com/Jellify-Music/App into feature/playlist-user-operations
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
OTA_UPDATE_ENABLED=true
|
||||
IS_MAESTRO_BUILD = false
|
||||
IS_MAESTRO_BUILD = false
|
||||
GLITCHTIP_DSN= ""
|
||||
@@ -31,6 +31,7 @@ import { CarPlay } from 'react-native-carplay'
|
||||
import { useAutoStore } from './src/stores/auto'
|
||||
import { registerAutoService } from './src/player'
|
||||
import QueryPersistenceConfig from './src/configs/query-persistence.config'
|
||||
import { ReducedMotionConfig, ReduceMotion } from 'react-native-reanimated'
|
||||
|
||||
LogBox.ignoreAllLogs()
|
||||
|
||||
@@ -135,6 +136,7 @@ function Container({ playerIsReady }: { playerIsReady: boolean }): React.JSX.Ele
|
||||
theme={getJellifyNavTheme(colorPreset, resolvedMode)}
|
||||
>
|
||||
<GestureHandlerRootView>
|
||||
<ReducedMotionConfig mode={ReduceMotion.System} />
|
||||
<TamaguiProvider config={jellifyConfig}>
|
||||
{playerIsReady && <Jellify />}
|
||||
</TamaguiProvider>
|
||||
|
||||
@@ -188,16 +188,15 @@ Install via [Altstore](https://altstore.io) or your favorite sideloading utility
|
||||
### Roadmap
|
||||
|
||||
#### 1.1.0 (Socket To Me Baby) - March '26
|
||||
- Android Auto/CarPlay Support
|
||||
- Websocket Support (Server online status)
|
||||
- Home Screen Updates
|
||||
- Discover Screen Updates
|
||||
- Artist Screen Redesign
|
||||
- Library Redesign
|
||||
- Gapless Playback
|
||||
- WebSocket Support (Server online status)
|
||||
- Library Enhancements
|
||||
- Quick Connect Support
|
||||
- Allow Self-Signed Certificates
|
||||
|
||||
#### 1.2.0 (We Made a Language For Us Two...) - June '26
|
||||
- Android Auto/CarPlay Support
|
||||
- EQ Controls
|
||||
- Collaborative Playlists
|
||||
- App Customization Options
|
||||
- Desktop Support (Experimental)
|
||||
@@ -207,12 +206,10 @@ Install via [Altstore](https://altstore.io) or your favorite sideloading utility
|
||||
- Tablet Support
|
||||
|
||||
#### 2.0.0 - December '26
|
||||
- Gapless Playback
|
||||
- Seerr (formerly Jellyseerr) Integration
|
||||
- JellyJam
|
||||
- EQ Controls
|
||||
|
||||
#### 3.0.0 - TBD
|
||||
#### 3.0.0 - December '27
|
||||
- Watch Support
|
||||
- tvOS (Apple and Android)
|
||||
|
||||
|
||||
@@ -88,8 +88,8 @@ android {
|
||||
applicationId "com.cosmonautical.jellify"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 174
|
||||
versionName "1.0.15"
|
||||
versionCode 177
|
||||
versionName "1.0.18"
|
||||
|
||||
}
|
||||
signingConfigs {
|
||||
|
||||
@@ -5,23 +5,23 @@
|
||||
"": {
|
||||
"name": "jellify",
|
||||
"dependencies": {
|
||||
"@jellify-music/react-native-reanimated-slider": "0.3.4",
|
||||
"@jellify-music/react-native-reanimated-slider": "0.4.0",
|
||||
"@jellyfin/sdk": "0.13.0",
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
"@react-native-community/cli": "20.0.0",
|
||||
"@react-native-community/netinfo": "11.5.1",
|
||||
"@react-native-community/cli": "20.1.1",
|
||||
"@react-native-community/netinfo": "11.5.2",
|
||||
"@react-native-masked-view/masked-view": "^0.3.2",
|
||||
"@react-native-vector-icons/material-design-icons": "12.4.0",
|
||||
"@react-navigation/bottom-tabs": "7.12.0",
|
||||
"@react-navigation/material-top-tabs": "7.4.13",
|
||||
"@react-navigation/native": "7.1.28",
|
||||
"@react-navigation/native-stack": "7.12.0",
|
||||
"@sentry/react-native": "7.8.0",
|
||||
"@shopify/flash-list": "2.2.1",
|
||||
"@sentry/react-native": "7.12.0",
|
||||
"@shopify/flash-list": "2.2.2",
|
||||
"@tamagui/config": "1.144.3",
|
||||
"@tanstack/query-async-storage-persister": "5.90.12",
|
||||
"@tanstack/react-query": "5.90.12",
|
||||
"@tanstack/react-query-persist-client": "5.90.12",
|
||||
"@tanstack/query-async-storage-persister": "5.90.20",
|
||||
"@tanstack/react-query": "5.90.20",
|
||||
"@tanstack/react-query-persist-client": "5.90.20",
|
||||
"@testing-library/react-native": "13.3.3",
|
||||
"@typedigital/telemetrydeck-react": "0.4.1",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -30,7 +30,7 @@
|
||||
"react-freeze": "^1.0.4",
|
||||
"react-native": "0.83.1",
|
||||
"react-native-background-actions": "^4.0.1",
|
||||
"react-native-blob-util": "^0.22.2",
|
||||
"react-native-blob-util": "0.24.6",
|
||||
"react-native-blurhash": "^2.1.3",
|
||||
"react-native-carplay": "^2.4.1-beta.0",
|
||||
"react-native-device-info": "15.0.1",
|
||||
@@ -39,24 +39,24 @@
|
||||
"react-native-google-cast": "^4.9.1",
|
||||
"react-native-haptic-feedback": "^2.3.3",
|
||||
"react-native-linear-gradient": "^2.8.3",
|
||||
"react-native-mmkv": "^4.1.1",
|
||||
"react-native-mmkv": "4.1.2",
|
||||
"react-native-nitro-fetch": "0.1.7",
|
||||
"react-native-nitro-modules": "0.33.2",
|
||||
"react-native-nitro-ota": "^0.10.0",
|
||||
"react-native-nitro-modules": "0.33.7",
|
||||
"react-native-nitro-ota": "^0.11.0",
|
||||
"react-native-pager-view": "8.0.0",
|
||||
"react-native-reanimated": "4.1.6",
|
||||
"react-native-safe-area-context": "5.6.2",
|
||||
"react-native-screens": "4.21.0",
|
||||
"react-native-screens": "4.23.0",
|
||||
"react-native-sortables": "1.9.4",
|
||||
"react-native-superconfig": "^0.6.0",
|
||||
"react-native-superconfig": "^0.10.0",
|
||||
"react-native-text-ticker": "^1.15.0",
|
||||
"react-native-toast-message": "^2.3.3",
|
||||
"react-native-track-player": "5.0.0-alpha0",
|
||||
"react-native-turbo-image": "1.24.1",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-url-polyfill": "3.0.0",
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-worklets": "^0.7.1",
|
||||
"react-native-worklets-core": "^1.6.2",
|
||||
"react-native-worklets": "0.7.2",
|
||||
"react-native-worklets-core": "1.6.2",
|
||||
"ruby": "^0.6.1",
|
||||
"scheduler": "^0.26.0",
|
||||
"tamagui": "1.144.3",
|
||||
@@ -68,15 +68,15 @@
|
||||
"@babel/runtime": "7.28.6",
|
||||
"@eslint/eslintrc": "3.3.3",
|
||||
"@eslint/js": "9.39.2",
|
||||
"@react-native-community/cli-platform-android": "20.0.0",
|
||||
"@react-native-community/cli-platform-ios": "20.0.0",
|
||||
"@react-native-community/cli-platform-android": "20.1.1",
|
||||
"@react-native-community/cli-platform-ios": "20.1.1",
|
||||
"@react-native/babel-preset": "0.83.1",
|
||||
"@react-native/eslint-config": "0.83.1",
|
||||
"@react-native/metro-config": "0.83.1",
|
||||
"@react-native/typescript-config": "0.83.1",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/lodash": "^4.17.21",
|
||||
"@types/node": "25.2.0",
|
||||
"@types/node": "25.2.2",
|
||||
"@types/react": "19.2.0",
|
||||
"@types/react-native-vector-icons": "^6.4.18",
|
||||
"@types/react-test-renderer": "19.1.0",
|
||||
@@ -85,14 +85,14 @@
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-prettier": "5.5.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-native": "^5.0.0",
|
||||
"globals": "17.3.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "30.2.0",
|
||||
"jscodeshift": "^17.3.0",
|
||||
"lint-staged": "^16.1.5",
|
||||
"lint-staged": "16.2.7",
|
||||
"patch-package": "8.0.1",
|
||||
"prettier": "3.8.1",
|
||||
"react-dom": "19.2.0",
|
||||
@@ -408,6 +408,10 @@
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||
|
||||
"@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
|
||||
|
||||
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="],
|
||||
|
||||
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
|
||||
"@isaacs/ttlcache": ["@isaacs/ttlcache@1.4.1", "", {}, "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA=="],
|
||||
@@ -416,7 +420,7 @@
|
||||
|
||||
"@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="],
|
||||
|
||||
"@jellify-music/react-native-reanimated-slider": ["@jellify-music/react-native-reanimated-slider@0.3.4", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-B8ncisTQlKMDobwYWnTuvr8kvcx/NHW6yMjW7U+GzXCvKxxI4+8YVtHg1NYGgktWFcjNMBwELdZ7Iw6loUTnUA=="],
|
||||
"@jellify-music/react-native-reanimated-slider": ["@jellify-music/react-native-reanimated-slider@0.4.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">= 2.30.0", "react-native-reanimated": "~4.1.0", "react-native-worklets": ">= 0.5.1" } }, "sha512-kOWIjezMfW8wHApKAvu7LrkvoLJk1OzmZWjPc0fMYdeSTwxaokQhljGRcu2NbAwZsJ/tIK7vahKTSeSzkNIhfg=="],
|
||||
|
||||
"@jellyfin/sdk": ["@jellyfin/sdk@0.13.0", "", { "peerDependencies": { "axios": "^1.12.0" } }, "sha512-oiBAOXH6s+dKdReSsYgNktBDzbxtg4JVWhEzIxZSxKcWMdSKmBtK41MhXRO7IWAC40DguKUm3nU/Z493qPAlWA=="],
|
||||
|
||||
@@ -498,31 +502,31 @@
|
||||
|
||||
"@react-native-async-storage/async-storage": ["@react-native-async-storage/async-storage@2.2.0", "", { "dependencies": { "merge-options": "^3.0.4" }, "peerDependencies": { "react-native": "^0.0.0-0 || >=0.65 <1.0" } }, "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw=="],
|
||||
|
||||
"@react-native-community/cli": ["@react-native-community/cli@20.0.0", "", { "dependencies": { "@react-native-community/cli-clean": "20.0.0", "@react-native-community/cli-config": "20.0.0", "@react-native-community/cli-doctor": "20.0.0", "@react-native-community/cli-server-api": "20.0.0", "@react-native-community/cli-tools": "20.0.0", "@react-native-community/cli-types": "20.0.0", "chalk": "^4.1.2", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-/cMnGl5V1rqnbElY1Fvga1vfw0d3bnqiJLx2+2oh7l9ulnXfVRWb5tU2kgBqiMxuDOKA+DQoifC9q/tvkj5K2w=="],
|
||||
"@react-native-community/cli": ["@react-native-community/cli@20.1.1", "", { "dependencies": { "@react-native-community/cli-clean": "20.1.1", "@react-native-community/cli-config": "20.1.1", "@react-native-community/cli-doctor": "20.1.1", "@react-native-community/cli-server-api": "20.1.1", "@react-native-community/cli-tools": "20.1.1", "@react-native-community/cli-types": "20.1.1", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "picocolors": "^1.1.1", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-aLPUx43+WSeTOaUepR2FBD5a1V0OAZ1QB2DOlRlW4fOEjtBXgv40eM/ho8g3WCvAOKfPvTvx4fZdcuovTyV81Q=="],
|
||||
|
||||
"@react-native-community/cli-clean": ["@react-native-community/cli-clean@20.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-YmdNRcT+Dp8lC7CfxSDIfPMbVPEXVFzBH62VZNbYGxjyakqAvoQUFTYPgM2AyFusAr4wDFbDOsEv88gCDwR3ig=="],
|
||||
"@react-native-community/cli-clean": ["@react-native-community/cli-clean@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "execa": "^5.0.0", "fast-glob": "^3.3.2", "picocolors": "^1.1.1" } }, "sha512-6nGQ08w2+EcDwTFC4JFiW/wI2pLwzMrk9thz4um7tKRNW8sADX0IyCsfM2F4rHS720C0UNKYBZE9nAsfp8Vkcw=="],
|
||||
|
||||
"@react-native-community/cli-config": ["@react-native-community/cli-config@20.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "cosmiconfig": "^9.0.0", "deepmerge": "^4.3.0", "fast-glob": "^3.3.2", "joi": "^17.2.1" } }, "sha512-5Ky9ceYuDqG62VIIpbOmkg8Lybj2fUjf/5wK4UO107uRqejBgNgKsbGnIZgEhREcaSEOkujWrroJ9gweueLfBg=="],
|
||||
"@react-native-community/cli-config": ["@react-native-community/cli-config@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "cosmiconfig": "^9.0.0", "deepmerge": "^4.3.0", "fast-glob": "^3.3.2", "joi": "^17.2.1", "picocolors": "^1.1.1" } }, "sha512-ajs2i56MANie/v0bMQ1BmRcrOb6MEvLT2rh/I1CA62NXGqF1Rxv6QwsN84LrADMXHRg8QiEMAIADkyDeQHt7Kg=="],
|
||||
|
||||
"@react-native-community/cli-config-android": ["@react-native-community/cli-config-android@20.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "fast-glob": "^3.3.2", "fast-xml-parser": "^4.4.1" } }, "sha512-asv60qYCnL1v0QFWcG9r1zckeFlKG+14GGNyPXY72Eea7RX5Cxdx8Pb6fIPKroWH1HEWjYH9KKHksMSnf9FMKw=="],
|
||||
"@react-native-community/cli-config-android": ["@react-native-community/cli-config-android@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "fast-glob": "^3.3.2", "fast-xml-parser": "^4.4.1", "picocolors": "^1.1.1" } }, "sha512-1iUV2rPAyoWPo8EceAFC2vZTF+pEd9YqS87c0aqpbGOFE0gs1rHEB+auVR8CdjzftR4U9sq6m2jrdst0rvpIkg=="],
|
||||
|
||||
"@react-native-community/cli-config-apple": ["@react-native-community/cli-config-apple@20.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-PS1gNOdpeQ6w7dVu1zi++E+ix2D0ZkGC2SQP6Y/Qp002wG4se56esLXItYiiLrJkhH21P28fXdmYvTEkjSm9/Q=="],
|
||||
"@react-native-community/cli-config-apple": ["@react-native-community/cli-config-apple@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "execa": "^5.0.0", "fast-glob": "^3.3.2", "picocolors": "^1.1.1" } }, "sha512-doepJgLJVqeJb5tNoP9hyFIcoZ1OMGO7QN/YMuCCIjbThUQe/J87XdwPol3Qrjr58KRt9xeBVz+kHeW5mtSutw=="],
|
||||
|
||||
"@react-native-community/cli-doctor": ["@react-native-community/cli-doctor@20.0.0", "", { "dependencies": { "@react-native-community/cli-config": "20.0.0", "@react-native-community/cli-platform-android": "20.0.0", "@react-native-community/cli-platform-apple": "20.0.0", "@react-native-community/cli-platform-ios": "20.0.0", "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "command-exists": "^1.2.8", "deepmerge": "^4.3.0", "envinfo": "^7.13.0", "execa": "^5.0.0", "node-stream-zip": "^1.9.1", "ora": "^5.4.1", "semver": "^7.5.2", "wcwidth": "^1.0.1", "yaml": "^2.2.1" } }, "sha512-cPHspi59+Fy41FDVxt62ZWoicCZ1o34k8LAl64NVSY0lwPl+CEi78jipXJhtfkVqSTetloA8zexa/vSAcJy57Q=="],
|
||||
"@react-native-community/cli-doctor": ["@react-native-community/cli-doctor@20.1.1", "", { "dependencies": { "@react-native-community/cli-config": "20.1.1", "@react-native-community/cli-platform-android": "20.1.1", "@react-native-community/cli-platform-apple": "20.1.1", "@react-native-community/cli-platform-ios": "20.1.1", "@react-native-community/cli-tools": "20.1.1", "command-exists": "^1.2.8", "deepmerge": "^4.3.0", "envinfo": "^7.13.0", "execa": "^5.0.0", "node-stream-zip": "^1.9.1", "ora": "^5.4.1", "picocolors": "^1.1.1", "semver": "^7.5.2", "wcwidth": "^1.0.1", "yaml": "^2.2.1" } }, "sha512-eFpg5wWnV7uGqvLemshpgj2trPD8cckqxBuI4nT7sxKF/YpA/e3nnnyytHxPP5EnYfWbMcqfaq8hDJoOnJinGQ=="],
|
||||
|
||||
"@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@20.0.0", "", { "dependencies": { "@react-native-community/cli-config-android": "20.0.0", "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "logkitty": "^0.7.1" } }, "sha512-th3ji1GRcV6ACelgC0wJtt9daDZ+63/52KTwL39xXGoqczFjml4qERK90/ppcXU0Ilgq55ANF8Pr+UotQ2AB/A=="],
|
||||
"@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@20.1.1", "", { "dependencies": { "@react-native-community/cli-config-android": "20.1.1", "@react-native-community/cli-tools": "20.1.1", "execa": "^5.0.0", "logkitty": "^0.7.1", "picocolors": "^1.1.1" } }, "sha512-KPheizJQI0tVvBLy9owzpo+A9qDsDAa87e7a8xNaHnwqGpExnIzFPrbdvrltiZjstU2eB/+/UgNQxYIEd4Oc+g=="],
|
||||
|
||||
"@react-native-community/cli-platform-apple": ["@react-native-community/cli-platform-apple@20.0.0", "", { "dependencies": { "@react-native-community/cli-config-apple": "20.0.0", "@react-native-community/cli-tools": "20.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-xml-parser": "^4.4.1" } }, "sha512-rZZCnAjUHN1XBgiWTAMwEKpbVTO4IHBSecdd1VxJFeTZ7WjmstqA6L/HXcnueBgxrzTCRqvkRIyEQXxC1OfhGw=="],
|
||||
"@react-native-community/cli-platform-apple": ["@react-native-community/cli-platform-apple@20.1.1", "", { "dependencies": { "@react-native-community/cli-config-apple": "20.1.1", "@react-native-community/cli-tools": "20.1.1", "execa": "^5.0.0", "fast-xml-parser": "^4.4.1", "picocolors": "^1.1.1" } }, "sha512-mQEjOzRFCcQTrCt73Q/+5WWTfUg6U2vLZv5rPuFiNrLbrwRqxVH3OLaXg5gilJkDTJC80z8iOSsdd8MRxONOig=="],
|
||||
|
||||
"@react-native-community/cli-platform-ios": ["@react-native-community/cli-platform-ios@20.0.0", "", { "dependencies": { "@react-native-community/cli-platform-apple": "20.0.0" } }, "sha512-Z35M+4gUJgtS4WqgpKU9/XYur70nmj3Q65c9USyTq6v/7YJ4VmBkmhC9BticPs6wuQ9Jcv0NyVCY0Wmh6kMMYw=="],
|
||||
"@react-native-community/cli-platform-ios": ["@react-native-community/cli-platform-ios@20.1.1", "", { "dependencies": { "@react-native-community/cli-platform-apple": "20.1.1" } }, "sha512-6vr10/oSjKkZO/BBgfFJNQTC/0CDF4WrN8iW9ss+Kt6ZL2QrBXLYz7fobrrboOlHwqqs5EyQadlEaNii7gKRJg=="],
|
||||
|
||||
"@react-native-community/cli-server-api": ["@react-native-community/cli-server-api@20.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.0.0", "body-parser": "^1.20.3", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", "nocache": "^3.0.1", "open": "^6.2.0", "pretty-format": "^29.7.0", "serve-static": "^1.13.1", "ws": "^6.2.3" } }, "sha512-Ves21bXtjUK3tQbtqw/NdzpMW1vR2HvYCkUQ/MXKrJcPjgJnXQpSnTqHXz6ZdBlMbbwLJXOhSPiYzxb5/v4CDg=="],
|
||||
"@react-native-community/cli-server-api": ["@react-native-community/cli-server-api@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "body-parser": "^1.20.3", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", "nocache": "^3.0.1", "open": "^6.2.0", "pretty-format": "^29.7.0", "serve-static": "^1.13.1", "strict-url-sanitise": "0.0.1", "ws": "^6.2.3" } }, "sha512-phHfiCa4WqfKfaoV2vGVR3ZrYQDQTpI1k+C+i6rXAxFGxPuy8IgFFVOSL543qjKPpHBVwLcA+/xAJCVpdyCtVQ=="],
|
||||
|
||||
"@react-native-community/cli-tools": ["@react-native-community/cli-tools@20.0.0", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-akSZGxr1IajJ8n0YCwQoA3DI0HttJ0WB7M3nVpb0lOM+rJpsBN7WG5Ft+8ozb6HyIPX+O+lLeYazxn5VNG/Xhw=="],
|
||||
"@react-native-community/cli-tools": ["@react-native-community/cli-tools@20.1.1", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "picocolors": "^1.1.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-j+zX/H2X+6ZGneIDj56tZ1Hbnip5nSfnq7yGlMyF/zm3U1hKp3G1jN5v0YEfnz/zEmjr7zruh4Y06KmZrF1lrA=="],
|
||||
|
||||
"@react-native-community/cli-types": ["@react-native-community/cli-types@20.0.0", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-7J4hzGWOPTBV1d30Pf2NidV+bfCWpjfCOiGO3HUhz1fH4MvBM0FbbBmE9LE5NnMz7M8XSRSi68ZGYQXgLBB2Qw=="],
|
||||
"@react-native-community/cli-types": ["@react-native-community/cli-types@20.1.1", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-Tp+s27I/RDONrGvWVj4IzEmga2HhJhXi8ZlZTfycMMyAcv4LG/CTPira+BUZs8nzLAJNrlJ79pVVPJPqQAe+aw=="],
|
||||
|
||||
"@react-native-community/netinfo": ["@react-native-community/netinfo@11.5.1", "", { "peerDependencies": { "react": "*", "react-native": ">=0.59" } }, "sha512-ZiLvVoNW6fIOZqU99Jdm+dfwv2T76AHifKtEDydPQOkzKg/xLIkSrOEMUigkmlNQu4MeCgFJlrapscbRzU+bKg=="],
|
||||
"@react-native-community/netinfo": ["@react-native-community/netinfo@11.5.2", "", { "peerDependencies": { "react": "*", "react-native": ">=0.59" } }, "sha512-/g0m65BtX9HU+bPiCH2517bOHpEIUsGrWFXDzi1a5nNKn5KujQgm04WhL7/OSXWKHyrT8VVtUoJA0XKRxueBpQ=="],
|
||||
|
||||
"@react-native-masked-view/masked-view": ["@react-native-masked-view/masked-view@0.3.2", "", { "peerDependencies": { "react": ">=16", "react-native": ">=0.57" } }, "sha512-XwuQoW7/GEgWRMovOQtX3A4PrXhyaZm0lVUiY8qJDvdngjLms9Cpdck6SmGAUNqQwcj2EadHC1HwL0bEyoa/SQ=="],
|
||||
|
||||
@@ -582,17 +586,17 @@
|
||||
|
||||
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
||||
|
||||
"@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.30.0", "", { "dependencies": { "@sentry/core": "10.30.0" } }, "sha512-dVsHTUbvgaLNetWAQC6yJFnmgD0xUbVgCkmzNB7S28wIP570GcZ4cxFGPOkXbPx6dEBUfoOREeXzLqjJLtJPfg=="],
|
||||
"@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.38.0", "", { "dependencies": { "@sentry/core": "10.38.0" } }, "sha512-UOJtYmdcxHCcV0NPfXFff/a95iXl/E0EhuQ1y0uE0BuZDMupWSF5t2BgC4HaE5Aw3RTjDF3XkSHWoIF6ohy7eA=="],
|
||||
|
||||
"@sentry-internal/feedback": ["@sentry-internal/feedback@10.30.0", "", { "dependencies": { "@sentry/core": "10.30.0" } }, "sha512-+bnQZ6SNF265nTXrRlXTmq5Ila1fRfraDOAahlOT/VM4j6zqCvNZzmeDD9J6IbxiAdhlp/YOkrG3zbr5vgYo0A=="],
|
||||
"@sentry-internal/feedback": ["@sentry-internal/feedback@10.38.0", "", { "dependencies": { "@sentry/core": "10.38.0" } }, "sha512-JXneg9zRftyfy1Fyfc39bBlF/Qd8g4UDublFFkVvdc1S6JQPlK+P6q22DKz3Pc8w3ySby+xlIq/eTu9Pzqi4KA=="],
|
||||
|
||||
"@sentry-internal/replay": ["@sentry-internal/replay@10.30.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.30.0", "@sentry/core": "10.30.0" } }, "sha512-Pj/fMIZQkXzIw6YWpxKWUE5+GXffKq6CgXwHszVB39al1wYz1gTIrTqJqt31IBLIihfCy8XxYddglR2EW0BVIQ=="],
|
||||
"@sentry-internal/replay": ["@sentry-internal/replay@10.38.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.38.0", "@sentry/core": "10.38.0" } }, "sha512-YWIkL6/dnaiQyFiZXJ/nN+NXGv/15z45ia86bE/TMq01CubX/DUOilgsFz0pk2v/pg3tp/U2MskLO9Hz0cnqeg=="],
|
||||
|
||||
"@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.30.0", "", { "dependencies": { "@sentry-internal/replay": "10.30.0", "@sentry/core": "10.30.0" } }, "sha512-RIlIz+XQ4DUWaN60CjfmicJq2O2JRtDKM5lw0wB++M5ha0TBh6rv+Ojf6BDgiV3LOQ7lZvCM57xhmNUtrGmelg=="],
|
||||
"@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.38.0", "", { "dependencies": { "@sentry-internal/replay": "10.38.0", "@sentry/core": "10.38.0" } }, "sha512-OXWM9jEqNYh4VTvrMu7v+z1anz+QKQ/fZXIZdsO7JTT2lGNZe58UUMeoq386M+Saxen8F9SUH7yTORy/8KI5qw=="],
|
||||
|
||||
"@sentry/babel-plugin-component-annotate": ["@sentry/babel-plugin-component-annotate@4.6.1", "", {}, "sha512-aSIk0vgBqv7PhX6/Eov+vlI4puCE0bRXzUG5HdCsHBpAfeMkI8Hva6kSOusnzKqs8bf04hU7s3Sf0XxGTj/1AA=="],
|
||||
"@sentry/babel-plugin-component-annotate": ["@sentry/babel-plugin-component-annotate@4.9.0", "", {}, "sha512-TJ7sVoa2Bf36lpJjBAzpNDC5Hg+evjsQnqUPeDx9Nz/YFw0u9rK1cwvi95gVWpx7PJSDCkljIv3aw0m4RatHpQ=="],
|
||||
|
||||
"@sentry/browser": ["@sentry/browser@10.30.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.30.0", "@sentry-internal/feedback": "10.30.0", "@sentry-internal/replay": "10.30.0", "@sentry-internal/replay-canvas": "10.30.0", "@sentry/core": "10.30.0" } }, "sha512-7M/IJUMLo0iCMLNxDV/OHTPI0WKyluxhCcxXJn7nrCcolu8A1aq9R8XjKxm0oTCO8ht5pz8bhGXUnYJj4eoEBA=="],
|
||||
"@sentry/browser": ["@sentry/browser@10.38.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.38.0", "@sentry-internal/feedback": "10.38.0", "@sentry-internal/replay": "10.38.0", "@sentry-internal/replay-canvas": "10.38.0", "@sentry/core": "10.38.0" } }, "sha512-3phzp1YX4wcQr9mocGWKbjv0jwtuoDBv7+Y6Yfrys/kwyaL84mDLjjQhRf4gL5SX7JdYkhBp4WaiNlR0UC4kTA=="],
|
||||
|
||||
"@sentry/cli": ["@sentry/cli@2.58.4", "", { "dependencies": { "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.7", "progress": "^2.0.3", "proxy-from-env": "^1.1.0", "which": "^2.0.2" }, "optionalDependencies": { "@sentry/cli-darwin": "2.58.4", "@sentry/cli-linux-arm": "2.58.4", "@sentry/cli-linux-arm64": "2.58.4", "@sentry/cli-linux-i686": "2.58.4", "@sentry/cli-linux-x64": "2.58.4", "@sentry/cli-win32-arm64": "2.58.4", "@sentry/cli-win32-i686": "2.58.4", "@sentry/cli-win32-x64": "2.58.4" }, "bin": { "sentry-cli": "bin/sentry-cli" } }, "sha512-ArDrpuS8JtDYEvwGleVE+FgR+qHaOp77IgdGSacz6SZy6Lv90uX0Nu4UrHCQJz8/xwIcNxSqnN22lq0dH4IqTg=="],
|
||||
|
||||
@@ -612,15 +616,15 @@
|
||||
|
||||
"@sentry/cli-win32-x64": ["@sentry/cli-win32-x64@2.58.4", "", { "os": "win32", "cpu": "x64" }, "sha512-cSzN4PjM1RsCZ4pxMjI0VI7yNCkxiJ5jmWncyiwHXGiXrV1eXYdQ3n1LhUYLZ91CafyprR0OhDcE+RVZ26Qb5w=="],
|
||||
|
||||
"@sentry/core": ["@sentry/core@10.30.0", "", {}, "sha512-IfNuqIoGVO9pwphwbOptAEJJI1SCAfewS5LBU1iL7hjPBHYAnE8tCVzyZN+pooEkQQ47Q4rGanaG1xY8mjTT1A=="],
|
||||
"@sentry/core": ["@sentry/core@10.38.0", "", {}, "sha512-1pubWDZE5y5HZEPMAZERP4fVl2NH3Ihp1A+vMoVkb3Qc66Diqj1WierAnStlZP7tCx0TBa0dK85GTW/ZFYyB9g=="],
|
||||
|
||||
"@sentry/react": ["@sentry/react@10.30.0", "", { "dependencies": { "@sentry/browser": "10.30.0", "@sentry/core": "10.30.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-3co0QwAU9VrCVBWgpRf/4G19MwzR+DM0sDe9tgN7P3pv/tMlEHhnPFv88nPfuSa2W8uVCpHehvV+GnUPF4V7Ag=="],
|
||||
"@sentry/react": ["@sentry/react@10.38.0", "", { "dependencies": { "@sentry/browser": "10.38.0", "@sentry/core": "10.38.0" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-3UiKo6QsqTyPGUt0XWRY9KLaxc/cs6Kz4vlldBSOXEL6qPDL/EfpwNJT61osRo81VFWu8pKu7ZY2bvLPryrnBQ=="],
|
||||
|
||||
"@sentry/react-native": ["@sentry/react-native@7.8.0", "", { "dependencies": { "@sentry/babel-plugin-component-annotate": "4.6.1", "@sentry/browser": "10.30.0", "@sentry/cli": "2.58.4", "@sentry/core": "10.30.0", "@sentry/react": "10.30.0", "@sentry/types": "10.30.0" }, "peerDependencies": { "expo": ">=49.0.0", "react": ">=17.0.0", "react-native": ">=0.65.0" }, "optionalPeers": ["expo"], "bin": { "sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js" } }, "sha512-0YMD0ObuGPbJVfHCBaYTfmRtS7tUd64W2GMNPA3b6rGlVlMQlL7bfdkCUouVBZzFDJYLV8ik1PzJKPWunKHCvw=="],
|
||||
"@sentry/react-native": ["@sentry/react-native@7.12.0", "", { "dependencies": { "@sentry/babel-plugin-component-annotate": "4.9.0", "@sentry/browser": "10.38.0", "@sentry/cli": "2.58.4", "@sentry/core": "10.38.0", "@sentry/react": "10.38.0", "@sentry/types": "10.38.0" }, "peerDependencies": { "expo": ">=49.0.0", "react": ">=17.0.0", "react-native": ">=0.65.0" }, "optionalPeers": ["expo"], "bin": { "sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js" } }, "sha512-40vrz+wkwCbKJ1fvJYBvPZWzoBvP3zKyNyfoS2LTcrY0yuoeTBHWfLswqwWV545vflwEeE7l4YKpLnekEmjCdQ=="],
|
||||
|
||||
"@sentry/types": ["@sentry/types@10.30.0", "", { "dependencies": { "@sentry/core": "10.30.0" } }, "sha512-tSyzG/JunWjbuQDDwP3DKgt8KP23ZSuNUEudMSv2jCF/956o8ksamPeidCTSVMXoEyTt5tvimWNeNvUFIFq3EA=="],
|
||||
"@sentry/types": ["@sentry/types@10.38.0", "", { "dependencies": { "@sentry/core": "10.38.0" } }, "sha512-DoeyTv/TvnoVDhHgdyv/wehieAKdyjLjEMtPOqqq/AjkP02BxeC0JYUrrWKOjV0wdLq5ZP8jKcCX8GN7awZonQ=="],
|
||||
|
||||
"@shopify/flash-list": ["@shopify/flash-list@2.2.1", "", { "peerDependencies": { "@babel/runtime": "*", "react": "*", "react-native": "*" } }, "sha512-Squs4SneVNXl8WoCZ7zuer7SidbjXiSSafF5nAYQTtSzylchl5sc/P1swYY7snxWBZwYH9aHy7kCvuMPjiCh1g=="],
|
||||
"@shopify/flash-list": ["@shopify/flash-list@2.2.2", "", { "peerDependencies": { "@babel/runtime": "*", "react": "*", "react-native": "*" } }, "sha512-YrvLBK5FCpvuX+d9QvJvjVqyi4eBUaEamkyfh9CjPdF6c+AukP0RSBh97qHyTwOEaVq21A5ukwgyWMDIbmxpmQ=="],
|
||||
|
||||
"@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="],
|
||||
|
||||
@@ -830,15 +834,15 @@
|
||||
|
||||
"@tamagui/z-index-stack": ["@tamagui/z-index-stack@1.144.3", "", { "peerDependencies": { "react": "*" } }, "sha512-183HkUdi5uo0KjuMx8HcwRiueBZHeamVUkPlLaHBaBJ1M2mWgDVkra3wocMKZr+fnFTPhOOqvQNb3eNDnefeUA=="],
|
||||
|
||||
"@tanstack/query-async-storage-persister": ["@tanstack/query-async-storage-persister@5.90.12", "", { "dependencies": { "@tanstack/query-core": "5.90.10", "@tanstack/query-persist-client-core": "5.91.9" } }, "sha512-bLOs6ZLTki88if8oDQDdnxk7wgMaKMAVTRxn+WiSI0An7rj3C/7/yDTjOLhwPaoipbTiFLF0/PnbpVXIbWqQYQ=="],
|
||||
"@tanstack/query-async-storage-persister": ["@tanstack/query-async-storage-persister@5.90.20", "", { "dependencies": { "@tanstack/query-core": "5.90.18", "@tanstack/query-persist-client-core": "5.91.17" } }, "sha512-SO7U/v/NcWTL4aJTyZmdW6i6KmN/YlEmNTEiU4Fm8cwGDTBQ2ABpaHY6A+Q/gsDdG7AeDwFvVs/wf9C4A0UKAw=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.90.10", "", {}, "sha512-EhZVFu9rl7GfRNuJLJ3Y7wtbTnENsvzp+YpcAV7kCYiXni1v8qZh++lpw4ch4rrwC0u/EZRnBHIehzCGzwXDSQ=="],
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.90.18", "", {}, "sha512-rbGx6bHgPNVzutP7BEr+53UPKohpckqlMAad+To9UxTbeaQ+kC/1SDRj+QzkwbQ7qhLT/1IKp34yS6thda6fzA=="],
|
||||
|
||||
"@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.91.9", "", { "dependencies": { "@tanstack/query-core": "5.90.10" } }, "sha512-LliMZl/pkO/6vRf5//fO8nl4UCfM1LQsnT+N0aRYkK7bqoM3QdqHxD65EApmJRypKkqaWmiyulPG3Mi1NYuyIA=="],
|
||||
"@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.91.17", "", { "dependencies": { "@tanstack/query-core": "5.90.18" } }, "sha512-NfXUCUzar8Y3fw0F6lPlrnJWfg148IXvWBIr/0x2LbYAoGdAnJDU3PzVaLTafI2vKQqIphe3uAq1068+nhn+nQ=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.12", "", { "dependencies": { "@tanstack/query-core": "5.90.12" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg=="],
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.20", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw=="],
|
||||
|
||||
"@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.90.12", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.91.9" }, "peerDependencies": { "@tanstack/react-query": "^5.90.10", "react": "^18 || ^19" } }, "sha512-o51hwImpKgb85FnFljtCXcUzuLXpKONF9N6bhKfifPL3SNSj8neh1a2aHQd7sN9mbeIeNfGMGJuDpSt/Fc3GwQ=="],
|
||||
"@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.90.20", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.91.17" }, "peerDependencies": { "@tanstack/react-query": "^5.90.18", "react": "^18 || ^19" } }, "sha512-FiKLxu7haxUAVV9hUEaUkUF6CsOM8i0ERtE6ETLQ0t5aKefY9tRByP9J1C4qf1r1m2TSWV0WwkmOsmsy1pZqsQ=="],
|
||||
|
||||
"@telemetrydeck/sdk": ["@telemetrydeck/sdk@2.0.4", "", {}, "sha512-x4S83AqSo6wvLJ6nRYdyJEqd9qmblUdBgsTRrjH5z++b9pnf2NMc8NpVAa48KIB1pRuP/GTGzXxVYdNoie/DVg=="],
|
||||
|
||||
@@ -876,7 +880,7 @@
|
||||
|
||||
"@types/lodash": ["@types/lodash@4.17.23", "", {}, "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="],
|
||||
"@types/node": ["@types/node@25.2.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA=="],
|
||||
|
||||
@@ -978,7 +982,7 @@
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
"ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
@@ -1266,7 +1270,7 @@
|
||||
|
||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||
|
||||
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
|
||||
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
@@ -1890,7 +1894,7 @@
|
||||
|
||||
"react-native-background-actions": ["react-native-background-actions@4.0.1", "", { "dependencies": { "eventemitter3": "^4.0.7" }, "peerDependencies": { "react-native": ">=0.47.0" } }, "sha512-LADhnb4ag1oH5Lotq0j8K9e2cFmrafFyg2PCME88VkTjqDUgNcJonkNdMCTHN0N3fh+hwAA7nDR4Cxkj9Q8eCw=="],
|
||||
|
||||
"react-native-blob-util": ["react-native-blob-util@0.22.2", "", { "dependencies": { "base-64": "0.1.0", "glob": "^10.3.10" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-Czx01QMg7aLsm/4F/7+eqoRAi1q/qjLY2Kao16g+n2SRnTH1+qkD8Qhx2q9okB+VNQvZKB1LbiXhktzYQV52xQ=="],
|
||||
"react-native-blob-util": ["react-native-blob-util@0.24.6", "", { "dependencies": { "base-64": "0.1.0", "glob": "13.0.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-e4231VTT0bO1Yh9KGtY6jXq7pkLXgDTz+ziClEEbiGwWBuG92Z+wFRLptlzdVbibW8XtrO1XrdtiE8AOlplEXQ=="],
|
||||
|
||||
"react-native-blurhash": ["react-native-blurhash@2.1.3", "", { "peerDependencies": { "react": ">=16.8.1", "react-native": ">=0.60.0-rc.0 <1.0.x" } }, "sha512-tYyFketZkrrEIABJ5Z1Pt6N2dqPqtSp7w4Lce17sx1bvspdppgVu/ehtZoPRZu02fYyX92RkyCeGXtCbtUNntg=="],
|
||||
|
||||
@@ -1912,13 +1916,13 @@
|
||||
|
||||
"react-native-linear-gradient": ["react-native-linear-gradient@2.8.3", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-KflAXZcEg54PXkLyflaSZQ3PJp4uC4whM7nT/Uot9m0e/qxFV3p6uor1983D1YOBJbJN7rrWdqIjq0T42jOJyA=="],
|
||||
|
||||
"react-native-mmkv": ["react-native-mmkv@4.1.1", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "*" } }, "sha512-nYFjM27l7zVhIiyAqWEFRagGASecb13JMIlzAuOeakRRz9GMJ49hCQntUBE2aeuZRE4u4ehSqTOomB0mTF56Ew=="],
|
||||
"react-native-mmkv": ["react-native-mmkv@4.1.2", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "*" } }, "sha512-6LHb2DQBXuo96Aues13EugmlWw/HAYuh3KoJoQNrC4JsBwn3J3KiRYAg2mCm5Je0VYq2YsmbgZG7XJwX/WFYZA=="],
|
||||
|
||||
"react-native-nitro-fetch": ["react-native-nitro-fetch@0.1.7", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "^0.29.2", "react-native-worklets-core": "^1.6.0" }, "optionalPeers": ["react-native-worklets-core"] }, "sha512-4kdDgo8CBaR3SaxIcvQxUVDIUFDr4UierJHigV9jr59jj9wnf7O1gUGcbmBJmE1M2EkVSe4nUU8Fq5tbSMg4kw=="],
|
||||
|
||||
"react-native-nitro-modules": ["react-native-nitro-modules@0.33.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ZlfOe6abODeHv/eZf8PxeSkrxIUhEKha6jaAAA9oXy7I6VPr7Ff4dUsAq3cyF3kX0L6qt2Dh9nzD2NdSsDwGpA=="],
|
||||
"react-native-nitro-modules": ["react-native-nitro-modules@0.33.7", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-WepMobWe4j1Ae5GQ5RxYGBdBpJBwzP6zaOxJ7r6nhbY5iyl01DL3Gsh4gk8edzNFRuAh1rvXDAHIipq8SahxeQ=="],
|
||||
|
||||
"react-native-nitro-ota": ["react-native-nitro-ota@0.10.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "0.32.0" } }, "sha512-pxmdaeNdUdnYdD1M8BpbtQo4mZrtljWFg0gspuIohTJqi97JYIRq0b+SReN0sMMo0w912k4XXSGMr/IduGoMNg=="],
|
||||
"react-native-nitro-ota": ["react-native-nitro-ota@0.11.0", "", { "dependencies": { "react-native-nitro-ota": "^0.10.1" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "0.32.0" } }, "sha512-jM4Z2Ntysk8dNJvUod/2NLpo1e2HrEaxb0X6EqZnSXr9Vg6nAGqnInSQUCFSvgqo4M83qWCF6nt7OxSdh1MCnA=="],
|
||||
|
||||
"react-native-pager-view": ["react-native-pager-view@8.0.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-oAwlWT1lhTkIs9HhODnjNNl/owxzn9DP1MbP+az6OTUdgbmzA16Up83sBH8NRKwrH8rNm7iuWnX1qMqiiWOLhg=="],
|
||||
|
||||
@@ -1926,11 +1930,11 @@
|
||||
|
||||
"react-native-safe-area-context": ["react-native-safe-area-context@5.6.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg=="],
|
||||
|
||||
"react-native-screens": ["react-native-screens@4.21.0", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-vUgbfKntx4LZ/1k+UU/miKohK0Ih6xoF3gYJr9QqZNOpPARksPxt4hq3HdCirvCLClieLYC9oLpGdizz/S+BGg=="],
|
||||
"react-native-screens": ["react-native-screens@4.23.0", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-XhO3aK0UeLpBn4kLecd+J+EDeRRJlI/Ro9Fze06vo1q163VeYtzfU9QS09/VyDFMWR1qxDC1iazCArTPSFFiPw=="],
|
||||
|
||||
"react-native-sortables": ["react-native-sortables@1.9.4", "", { "optionalDependencies": { "react-native-haptic-feedback": ">=2.0.0" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-a6hxT+gl14HA5Sm8UiLXJqF8KMEQVa+mUJd75OnzoVsmrxUDtjAatlMdV0kI9qTQDT/ZSFLPRmdUhOR762IA4g=="],
|
||||
|
||||
"react-native-superconfig": ["react-native-superconfig@0.6.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "*" } }, "sha512-kW9SjpKmuB7F54JNzaWHX5Ncr7jM8852FcIcBvfIyR0ofACaGvqf59hLKg7i8DfSIi0f5DXOCXbvu88cr8PIVw=="],
|
||||
"react-native-superconfig": ["react-native-superconfig@0.10.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "*" } }, "sha512-1AvFP03JqiXtd5EICBQXjVCegBCY+6v8ZNjwMCfOtgX0PFUdNDtWG/TbN+vZlAwbglaXDd3rfoEYomrx3AHEmQ=="],
|
||||
|
||||
"react-native-tab-view": ["react-native-tab-view@4.2.2", "", { "dependencies": { "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0" } }, "sha512-NXtrG6OchvbGjsvbySJGVocXxo4Y2vA17ph4rAaWtA2jh+AasD8OyikKBRg2SmllEfeQ+GEhcKe8kulHv8BhTg=="],
|
||||
|
||||
@@ -1942,7 +1946,7 @@
|
||||
|
||||
"react-native-turbo-image": ["react-native-turbo-image@1.24.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-Sj41a3WfZJvg/J+oC9KEjjLr/EFlLQqCDzyw2HU+mrA342drIfpS9oe7AZr+arzGS8hJpaey2xL2OmEGypZ54g=="],
|
||||
|
||||
"react-native-url-polyfill": ["react-native-url-polyfill@2.0.0", "", { "dependencies": { "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "react-native": "*" } }, "sha512-My330Do7/DvKnEvwQc0WdcBnFPploYKp9CYlefDXzIdEaA+PAhDYllkvGeEroEzvc4Kzzj2O4yVdz8v6fjRvhA=="],
|
||||
"react-native-url-polyfill": ["react-native-url-polyfill@3.0.0", "", { "dependencies": { "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "react-native": "*" } }, "sha512-aA5CiuUCUb/lbrliVCJ6lZ17/RpNJzvTO/C7gC/YmDQhTUoRD5q5HlJfwLWcxz4VgAhHwXKzhxH+wUN24tAdqg=="],
|
||||
|
||||
"react-native-uuid": ["react-native-uuid@2.0.3", "", {}, "sha512-f/YfIS2f5UB+gut7t/9BKGSCYbRA9/74A5R1MDp+FLYsuS+OSWoiM/D8Jko6OJB6Jcu3v6ONuddvZKHdIGpeiw=="],
|
||||
|
||||
@@ -2078,6 +2082,8 @@
|
||||
|
||||
"strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="],
|
||||
|
||||
"strict-url-sanitise": ["strict-url-sanitise@0.0.1", "", {}, "sha512-nuFtF539K8jZg3FjaWH/L8eocCR6gegz5RDOsaWxfdbF5Jqr2VXWxZayjTwUzsWJDC91k2EbnJXp6FuWW+Z4hg=="],
|
||||
|
||||
"string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
|
||||
|
||||
"string-hash-64": ["string-hash-64@1.0.3", "", {}, "sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw=="],
|
||||
@@ -2624,7 +2630,7 @@
|
||||
|
||||
"@react-native/metro-babel-transformer/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||
|
||||
"@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.90.12", "", {}, "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg=="],
|
||||
"@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
|
||||
|
||||
"@types/babel__core/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||
|
||||
@@ -2678,6 +2684,8 @@
|
||||
|
||||
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"chrome-launcher/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
|
||||
"chromium-edge-launcher/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
@@ -2696,8 +2704,6 @@
|
||||
|
||||
"error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
|
||||
|
||||
"eslint/@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
|
||||
|
||||
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||
|
||||
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||
@@ -2926,8 +2932,6 @@
|
||||
|
||||
"pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
|
||||
|
||||
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
|
||||
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
@@ -2948,7 +2952,7 @@
|
||||
|
||||
"react-native/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
||||
|
||||
"react-native-blob-util/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||
"react-native-blob-util/glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="],
|
||||
|
||||
"react-native-reanimated/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
@@ -2998,6 +3002,8 @@
|
||||
|
||||
"wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"write-file-atomic/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="],
|
||||
@@ -3216,8 +3222,6 @@
|
||||
|
||||
"@react-native-community/cli-server-api/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
|
||||
|
||||
"@react-native-community/cli-server-api/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"@react-native-community/cli-server-api/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
|
||||
"@react-native-community/cli/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
||||
@@ -3406,6 +3410,8 @@
|
||||
|
||||
"cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
@@ -3438,8 +3444,6 @@
|
||||
|
||||
"eslint-plugin-react-hooks/hermes-parser/hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
|
||||
|
||||
"eslint/@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
|
||||
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"find-cache-dir/pkg-dir/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
|
||||
@@ -3544,8 +3548,6 @@
|
||||
|
||||
"jest-validate/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
|
||||
|
||||
"jest-validate/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"jest-validate/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
|
||||
"jest-worker/jest-util/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="],
|
||||
@@ -3680,10 +3682,12 @@
|
||||
|
||||
"pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
|
||||
|
||||
"react-native-blob-util/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
"react-native-blob-util/glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="],
|
||||
|
||||
"react-native-blob-util/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"react-native-blob-util/glob/path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="],
|
||||
|
||||
"react-native-worklets/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="],
|
||||
|
||||
"react-native-worklets/@babel/plugin-transform-class-properties/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
||||
@@ -3706,8 +3710,6 @@
|
||||
|
||||
"react-native/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
|
||||
|
||||
"react-native/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"react-native/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
|
||||
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
@@ -3798,8 +3800,6 @@
|
||||
|
||||
"@jest/fake-timers/jest-message-util/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
|
||||
|
||||
"@jest/fake-timers/jest-message-util/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"@jest/fake-timers/jest-message-util/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
|
||||
"@jest/globals/@jest/environment/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="],
|
||||
@@ -4014,7 +4014,7 @@
|
||||
|
||||
"pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
|
||||
|
||||
"react-native-blob-util/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
"react-native-blob-util/glob/path-scurry/lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="],
|
||||
|
||||
"react-native-worklets/@babel/plugin-transform-class-properties/@babel/helper-create-class-features-plugin/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="],
|
||||
|
||||
@@ -4146,6 +4146,8 @@
|
||||
|
||||
"jscodeshift/@babel/plugin-transform-private-methods/@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||
|
||||
"logkitty/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"logkitty/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||
|
||||
"pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||
|
||||
@@ -543,7 +543,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 282;
|
||||
CURRENT_PROJECT_VERSION = 285;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = WAH9CZ8BPG;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -554,7 +554,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.15;
|
||||
MARKETING_VERSION = 1.0.18;
|
||||
NEW_SETTING = "";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -585,7 +585,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 282;
|
||||
CURRENT_PROJECT_VERSION = 285;
|
||||
DEVELOPMENT_TEAM = WAH9CZ8BPG;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = WAH9CZ8BPG;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@@ -595,7 +595,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.15;
|
||||
MARKETING_VERSION = 1.0.18;
|
||||
NEW_SETTING = "";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -823,7 +823,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 282;
|
||||
CURRENT_PROJECT_VERSION = 285;
|
||||
DEVELOPMENT_TEAM = WAH9CZ8BPG;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = WAH9CZ8BPG;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@@ -834,7 +834,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.15;
|
||||
MARKETING_VERSION = 1.0.18;
|
||||
NEW_SETTING = "";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
|
||||
+24
-24
@@ -46,7 +46,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- NitroMmkv (4.1.1):
|
||||
- NitroMmkv (4.1.2):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -77,7 +77,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- NitroModules (0.33.2):
|
||||
- NitroModules (0.33.7):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -106,7 +106,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- NitroOta (0.10.0):
|
||||
- NitroOta (0.11.0):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -114,7 +114,7 @@ PODS:
|
||||
- glog
|
||||
- hermes-engine
|
||||
- NitroModules
|
||||
- NitroOtaBundleManager (= 0.10.0)
|
||||
- NitroOtaBundleManager (= 0.11.0)
|
||||
- RCT-Folly
|
||||
- RCT-Folly/Fabric
|
||||
- RCTRequired
|
||||
@@ -138,8 +138,8 @@ PODS:
|
||||
- SocketRocket
|
||||
- SSZipArchive
|
||||
- Yoga
|
||||
- NitroOtaBundleManager (0.10.0)
|
||||
- NitroSuperconfig (0.6.0):
|
||||
- NitroOtaBundleManager (0.11.0)
|
||||
- NitroSuperconfig (0.10.0):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -2056,7 +2056,7 @@ PODS:
|
||||
- SocketRocket
|
||||
- react-native-background-actions (4.0.1):
|
||||
- React-Core
|
||||
- react-native-blob-util (0.22.2):
|
||||
- react-native-blob-util (0.24.6):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -2118,7 +2118,7 @@ PODS:
|
||||
- google-cast-sdk
|
||||
- PromisesObjC
|
||||
- React
|
||||
- react-native-netinfo (11.5.1):
|
||||
- react-native-netinfo (11.5.2):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -3116,7 +3116,7 @@ PODS:
|
||||
- RNWorklets
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- RNScreens (4.21.0):
|
||||
- RNScreens (4.23.0):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -3143,10 +3143,10 @@ PODS:
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- RNScreens/common (= 4.21.0)
|
||||
- RNScreens/common (= 4.23.0)
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- RNScreens/common (4.21.0):
|
||||
- RNScreens/common (4.23.0):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -3175,7 +3175,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- RNSentry (7.8.0):
|
||||
- RNSentry (7.12.0):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -3202,7 +3202,7 @@ PODS:
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Sentry/HybridSDK (= 8.57.3)
|
||||
- Sentry/HybridSDK (= 8.58.0)
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- RNWorklets (0.7.2):
|
||||
@@ -3294,7 +3294,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- Sentry/HybridSDK (8.57.3)
|
||||
- Sentry/HybridSDK (8.58.0)
|
||||
- SocketRocket (0.7.1)
|
||||
- SSZipArchive (2.4.3)
|
||||
- SwiftAudioEx (1.1.0)
|
||||
@@ -3653,11 +3653,11 @@ SPEC CHECKSUMS:
|
||||
hermes-engine: 83ac7cadb2a3a158ae6d9e4192417c5232065e99
|
||||
MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df
|
||||
NitroFetch: 1a268c80f654b3018397672d264f1e4d646076b1
|
||||
NitroMmkv: 8ed7ef6f41b91785fc580c975f68d6d675214767
|
||||
NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3
|
||||
NitroOta: 92d4eb528566b6babf5e4a30adbda44bfa803a9b
|
||||
NitroOtaBundleManager: 8fad871db2daf6b9ee6f04a100c79605cfa81e8d
|
||||
NitroSuperconfig: 54d86ee90bb78cbca09d119ea775a53ffbedb0fc
|
||||
NitroMmkv: 0be91455465952f2b943f753b9ee7df028d89e5c
|
||||
NitroModules: e8ec707a245a85cf1eb4289f91a9372f9590a897
|
||||
NitroOta: a3136533eb2397ff8cc3e6ff07ab603f122f3d0a
|
||||
NitroOtaBundleManager: 4b9c1669d0dc7d594f3f82becdd20b213bfbe567
|
||||
NitroSuperconfig: 81e51ed1b659657b86eef5d78aec675d8089abdb
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
|
||||
RCTDeprecation: a41bbdd9af30bf2e5715796b313e44ec43eefff1
|
||||
@@ -3696,11 +3696,11 @@ SPEC CHECKSUMS:
|
||||
React-Mapbuffer: 7b72a669e94662359dad4f42b5af005eb24b4e83
|
||||
React-microtasksnativemodule: cdc02da075f2857803ed63f24f5f72fc40e094c0
|
||||
react-native-background-actions: 48e6bad9e2a47e3b04858634c5a05ea11062f680
|
||||
react-native-blob-util: e2162ce4757849682559754bca954b65dc7eeb2f
|
||||
react-native-blob-util: 04189c38b188b2b76ee453be34765241c552547f
|
||||
react-native-blurhash: 93b024ff78f7912d22b1cdba262f3c91d3e2002e
|
||||
react-native-carplay: 8f388f6f73e5e0f73ed154ad8794371343ee20c0
|
||||
react-native-google-cast: 7be68a5d0b7eeb95a5924c3ecef8d319ef6c0a44
|
||||
react-native-netinfo: a666a1d5ddadd04d87de48ac941b34f4d64fdf64
|
||||
react-native-netinfo: 57447b5a45c98808f8eae292cf641f3d91d13830
|
||||
react-native-pager-view: d7d2aa47f54343bf55fdcee3973503dd27c2bd37
|
||||
react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460
|
||||
react-native-track-player: 591b4734e248c57154cde36545a985e007805c9e
|
||||
@@ -3747,10 +3747,10 @@ SPEC CHECKSUMS:
|
||||
RNGestureHandler: cd4be101cfa17ea6bbd438710caa02e286a84381
|
||||
RNReactNativeHapticFeedback: be4f1b4bf0398c30b59b76ed92ecb0a2ff3a69c6
|
||||
RNReanimated: 942d757148da78f5663d1fdf9ab71d1e75946c22
|
||||
RNScreens: e66f520506371042666e38a06d5d7e9580f0d3a0
|
||||
RNSentry: fdb39d5f294e492aa2f08ad80e510310dc223772
|
||||
RNScreens: afaf526a9c804c3b4503f950cf3e67ed81e29ada
|
||||
RNSentry: 47538022dd91abf23db1a954a09cdb8db13b1b98
|
||||
RNWorklets: 01efdd402d236a13651ea5ea5437ca85a44e7afa
|
||||
Sentry: c643eb180df401dd8c734c5036ddd9dd9218daa6
|
||||
Sentry: d587a8fe91ca13503ecd69a1905f3e8a0fcf61be
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
|
||||
SwiftAudioEx: f6aa653770f3a0d3851edaf8d834a30aee4a7646
|
||||
|
||||
+25
-24
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jellify",
|
||||
"version": "1.0.15",
|
||||
"version": "1.0.18",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"init-android": "bun i",
|
||||
@@ -34,26 +34,27 @@
|
||||
"sendOTA:iOS": "bash scripts/ota-iOS.sh",
|
||||
"sendOTA:PR": "bash scripts/ota-PR.sh",
|
||||
"android-build": "cd android && ./gradlew generateCodegenArtifactsFromSchema && ./gradlew assembleRelease",
|
||||
"postinstall": "patch-package"
|
||||
"generate-config": "node ./node_modules/react-native-superconfig/scripts/generate-config.js",
|
||||
"postinstall": "bun run generate-config && patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jellify-music/react-native-reanimated-slider": "0.3.4",
|
||||
"@jellify-music/react-native-reanimated-slider": "0.4.0",
|
||||
"@jellyfin/sdk": "0.13.0",
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
"@react-native-community/cli": "20.0.0",
|
||||
"@react-native-community/netinfo": "11.5.1",
|
||||
"@react-native-community/cli": "20.1.1",
|
||||
"@react-native-community/netinfo": "11.5.2",
|
||||
"@react-native-masked-view/masked-view": "^0.3.2",
|
||||
"@react-native-vector-icons/material-design-icons": "12.4.0",
|
||||
"@react-navigation/bottom-tabs": "7.12.0",
|
||||
"@react-navigation/material-top-tabs": "7.4.13",
|
||||
"@react-navigation/native": "7.1.28",
|
||||
"@react-navigation/native-stack": "7.12.0",
|
||||
"@sentry/react-native": "7.8.0",
|
||||
"@shopify/flash-list": "2.2.1",
|
||||
"@sentry/react-native": "7.12.0",
|
||||
"@shopify/flash-list": "2.2.2",
|
||||
"@tamagui/config": "1.144.3",
|
||||
"@tanstack/query-async-storage-persister": "5.90.12",
|
||||
"@tanstack/react-query": "5.90.12",
|
||||
"@tanstack/react-query-persist-client": "5.90.12",
|
||||
"@tanstack/query-async-storage-persister": "5.90.20",
|
||||
"@tanstack/react-query": "5.90.20",
|
||||
"@tanstack/react-query-persist-client": "5.90.20",
|
||||
"@testing-library/react-native": "13.3.3",
|
||||
"@typedigital/telemetrydeck-react": "0.4.1",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -62,7 +63,7 @@
|
||||
"react-freeze": "^1.0.4",
|
||||
"react-native": "0.83.1",
|
||||
"react-native-background-actions": "^4.0.1",
|
||||
"react-native-blob-util": "^0.22.2",
|
||||
"react-native-blob-util": "0.24.6",
|
||||
"react-native-blurhash": "^2.1.3",
|
||||
"react-native-carplay": "^2.4.1-beta.0",
|
||||
"react-native-device-info": "15.0.1",
|
||||
@@ -71,24 +72,24 @@
|
||||
"react-native-google-cast": "^4.9.1",
|
||||
"react-native-haptic-feedback": "^2.3.3",
|
||||
"react-native-linear-gradient": "^2.8.3",
|
||||
"react-native-mmkv": "^4.1.1",
|
||||
"react-native-mmkv": "4.1.2",
|
||||
"react-native-nitro-fetch": "0.1.7",
|
||||
"react-native-nitro-modules": "0.33.2",
|
||||
"react-native-nitro-ota": "^0.10.0",
|
||||
"react-native-nitro-modules": "0.33.7",
|
||||
"react-native-nitro-ota": "^0.11.0",
|
||||
"react-native-pager-view": "8.0.0",
|
||||
"react-native-reanimated": "4.1.6",
|
||||
"react-native-safe-area-context": "5.6.2",
|
||||
"react-native-screens": "4.21.0",
|
||||
"react-native-screens": "4.23.0",
|
||||
"react-native-sortables": "1.9.4",
|
||||
"react-native-superconfig": "^0.6.0",
|
||||
"react-native-superconfig": "^0.10.0",
|
||||
"react-native-text-ticker": "^1.15.0",
|
||||
"react-native-toast-message": "^2.3.3",
|
||||
"react-native-track-player": "5.0.0-alpha0",
|
||||
"react-native-turbo-image": "1.24.1",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-url-polyfill": "3.0.0",
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-worklets": "^0.7.1",
|
||||
"react-native-worklets-core": "^1.6.2",
|
||||
"react-native-worklets": "0.7.2",
|
||||
"react-native-worklets-core": "1.6.2",
|
||||
"ruby": "^0.6.1",
|
||||
"scheduler": "^0.26.0",
|
||||
"tamagui": "1.144.3",
|
||||
@@ -100,15 +101,15 @@
|
||||
"@babel/runtime": "7.28.6",
|
||||
"@eslint/eslintrc": "3.3.3",
|
||||
"@eslint/js": "9.39.2",
|
||||
"@react-native-community/cli-platform-android": "20.0.0",
|
||||
"@react-native-community/cli-platform-ios": "20.0.0",
|
||||
"@react-native-community/cli-platform-android": "20.1.1",
|
||||
"@react-native-community/cli-platform-ios": "20.1.1",
|
||||
"@react-native/babel-preset": "0.83.1",
|
||||
"@react-native/eslint-config": "0.83.1",
|
||||
"@react-native/metro-config": "0.83.1",
|
||||
"@react-native/typescript-config": "0.83.1",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/lodash": "^4.17.21",
|
||||
"@types/node": "25.2.0",
|
||||
"@types/node": "25.2.2",
|
||||
"@types/react": "19.2.0",
|
||||
"@types/react-native-vector-icons": "^6.4.18",
|
||||
"@types/react-test-renderer": "19.1.0",
|
||||
@@ -117,14 +118,14 @@
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-prettier": "5.5.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-native": "^5.0.0",
|
||||
"globals": "17.3.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "30.2.0",
|
||||
"jscodeshift": "^17.3.0",
|
||||
"lint-staged": "^16.1.5",
|
||||
"lint-staged": "16.2.7",
|
||||
"patch-package": "8.0.1",
|
||||
"prettier": "3.8.1",
|
||||
"react-dom": "19.2.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/node_modules/react-native-reanimated/compatibility.json b/node_modules/react-native-reanimated/compatibility.json
|
||||
index f1ba9cb..234363b 100644
|
||||
index f1ba9cb..ffab7fe 100644
|
||||
--- a/node_modules/react-native-reanimated/compatibility.json
|
||||
+++ b/node_modules/react-native-reanimated/compatibility.json
|
||||
@@ -4,7 +4,7 @@
|
||||
@@ -7,7 +7,7 @@ index f1ba9cb..234363b 100644
|
||||
},
|
||||
"4.1.x": {
|
||||
- "react-native": ["0.78", "0.79", "0.80", "0.81", "0.82"],
|
||||
+ "react-native": ["0.78", "0.79", "0.80", "0.81", "0.82", "0.83"],
|
||||
+ "react-native": ["0.78", "0.79", "0.80", "0.81", "0.82", "0.83", "0.84"],
|
||||
"react-native-worklets": ["0.5.x", "0.6.x", "0.7.x"]
|
||||
},
|
||||
"4.0.x": {
|
||||
|
||||
@@ -48,6 +48,8 @@ const useAlbums: () => [
|
||||
: ItemSortBy.Album
|
||||
const sortDescending = librarySortDescendingState.albums ?? false
|
||||
const isFavorites = filters.albums.isFavorites
|
||||
const yearMin = filters.albums.yearMin
|
||||
const yearMax = filters.albums.yearMax
|
||||
|
||||
const albumPageParams = useRef<Set<string>>(new Set<string>())
|
||||
|
||||
@@ -72,6 +74,8 @@ const useAlbums: () => [
|
||||
library?.musicLibraryId,
|
||||
librarySortBy,
|
||||
sortDescending,
|
||||
yearMin,
|
||||
yearMax,
|
||||
],
|
||||
queryFn: ({ pageParam }) =>
|
||||
fetchAlbums(
|
||||
@@ -82,6 +86,8 @@ const useAlbums: () => [
|
||||
isFavorites,
|
||||
[librarySortBy ?? ItemSortBy.SortName],
|
||||
[sortDescending ? SortOrder.Descending : SortOrder.Ascending],
|
||||
yearMin,
|
||||
yearMax,
|
||||
),
|
||||
initialPageParam: 0,
|
||||
select: selectAlbums,
|
||||
|
||||
@@ -9,9 +9,9 @@ import { JellifyLibrary } from '../../../../types/JellifyLibrary'
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import { fetchItem, fetchItems } from '../../item'
|
||||
import { JellifyUser } from '../../../../types/JellifyUser'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { ApiLimits } from '../../../../configs/query.config'
|
||||
import { nitroFetch } from '../../../utils/nitro'
|
||||
import buildYearsParam from '../../../../utils/mapping/build-years-param'
|
||||
|
||||
export function fetchAlbums(
|
||||
api: Api | undefined,
|
||||
@@ -21,12 +21,16 @@ export function fetchAlbums(
|
||||
isFavorite: boolean | undefined,
|
||||
sortBy: ItemSortBy[] = [ItemSortBy.SortName],
|
||||
sortOrder: SortOrder[] = [SortOrder.Ascending],
|
||||
yearMin?: number,
|
||||
yearMax?: number,
|
||||
): Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!api) return reject('No API instance provided')
|
||||
if (!user) return reject('No user provided')
|
||||
if (!library) return reject('Library has not been set')
|
||||
|
||||
const yearsParam = buildYearsParam(yearMin, yearMax)
|
||||
|
||||
nitroFetch<{ Items: BaseItemDto[] }>(api, '/Items', {
|
||||
ParentId: library.musicLibraryId,
|
||||
IncludeItemTypes: [BaseItemKind.MusicAlbum],
|
||||
@@ -38,9 +42,15 @@ export function fetchAlbums(
|
||||
IsFavorite: isFavorite,
|
||||
Fields: [ItemFields.SortName],
|
||||
Recursive: true,
|
||||
}).then((data) => {
|
||||
return data.Items ? resolve(data.Items) : resolve([])
|
||||
Years: yearsParam,
|
||||
})
|
||||
.then((data) => {
|
||||
return data.Items ? resolve(data.Items) : resolve([])
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
return reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { fetchSearchResults } from './utils'
|
||||
import { ONE_MINUTE } from '../../../constants/query-client'
|
||||
import { useJellifyLibrary } from '../../../stores'
|
||||
|
||||
const useSearchResults = (searchString: string | undefined) => {
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
return useQuery({
|
||||
queryKey: [QueryKeys.Search, library?.musicLibraryId, searchString],
|
||||
queryFn: () => fetchSearchResults(library?.musicLibraryId, searchString),
|
||||
staleTime: ONE_MINUTE * 10, // Cache results for 10 minutes
|
||||
gcTime: ONE_MINUTE * 15, // Garbage collect after 15 minutes
|
||||
enabled: !!library?.musicLibraryId && !!searchString, // Only run if we have a library ID and a search string
|
||||
})
|
||||
}
|
||||
|
||||
export default useSearchResults
|
||||
@@ -1,9 +1,8 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { isEmpty, isUndefined, trim } from 'lodash'
|
||||
import QueryConfig from '../../configs/query.config'
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import { JellifyUser } from '../../types/JellifyUser'
|
||||
import QueryConfig from '../../../../configs/query.config'
|
||||
import { getApi, getUser } from '../../../../stores'
|
||||
/**
|
||||
* Performs a search for items against the Jellyfin server, trimming whitespace
|
||||
* around the search term for the best possible results.
|
||||
@@ -11,12 +10,13 @@ import { JellifyUser } from '../../types/JellifyUser'
|
||||
* @returns A promise of a BaseItemDto array, be it empty or not
|
||||
*/
|
||||
export async function fetchSearchResults(
|
||||
api: Api | undefined,
|
||||
user: JellifyUser | undefined,
|
||||
libraryId: string | undefined,
|
||||
searchString: string | undefined,
|
||||
): Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const api = getApi()
|
||||
const user = getUser()
|
||||
|
||||
if (isEmpty(searchString)) resolve([])
|
||||
|
||||
if (isUndefined(api)) return reject('Client instance not set')
|
||||
@@ -44,6 +44,8 @@ const useTracks: (
|
||||
const isDownloaded = filters.tracks.isDownloaded ?? false
|
||||
const isLibraryUnplayed = filters.tracks.isUnplayed ?? false
|
||||
const libraryGenreIds = filters.tracks.genreIds
|
||||
const libraryYearMin = filters.tracks.yearMin
|
||||
const libraryYearMax = filters.tracks.yearMax
|
||||
|
||||
// Use provided values or fallback to library context
|
||||
// If artistId is present, we use isFavoritesParam if provided, otherwise false (default to showing all artist tracks)
|
||||
@@ -95,6 +97,8 @@ const useTracks: (
|
||||
finalSortBy,
|
||||
finalSortOrder,
|
||||
isDownloaded ? undefined : libraryGenreIds,
|
||||
libraryYearMin,
|
||||
libraryYearMax,
|
||||
),
|
||||
queryFn: ({ pageParam }) => {
|
||||
if (!isDownloaded) {
|
||||
@@ -109,21 +113,33 @@ const useTracks: (
|
||||
finalSortOrder,
|
||||
artistId,
|
||||
libraryGenreIds,
|
||||
libraryYearMin,
|
||||
libraryYearMax,
|
||||
)
|
||||
} else
|
||||
return (downloadedTracks ?? [])
|
||||
.map(({ item }) => item)
|
||||
.sort((a, b) => {
|
||||
const aName = a.Name ?? ''
|
||||
const bName = b.Name ?? ''
|
||||
if (aName < bName) return -1
|
||||
else if (aName === bName) return 0
|
||||
else return 1
|
||||
})
|
||||
.filter((track) => {
|
||||
if (!isFavorites) return true
|
||||
else return isDownloadedTrackAlsoFavorite(user, track)
|
||||
} else {
|
||||
let items = (downloadedTracks ?? []).map(({ item }) => item)
|
||||
if (libraryYearMin != null || libraryYearMax != null) {
|
||||
const min = libraryYearMin ?? 0
|
||||
const max = libraryYearMax ?? new Date().getFullYear()
|
||||
items = items.filter((track) => {
|
||||
const y =
|
||||
'ProductionYear' in track
|
||||
? (track as BaseItemDto).ProductionYear
|
||||
: undefined
|
||||
if (y == null) return false
|
||||
return y >= min && y <= max
|
||||
})
|
||||
}
|
||||
const sortByForCompare =
|
||||
finalSortBy === ItemSortBy.SortName ? ItemSortBy.Name : finalSortBy
|
||||
items = items.sort((a, b) =>
|
||||
compareDownloadedTracks(a, b, sortByForCompare, finalSortOrder),
|
||||
)
|
||||
return items.filter((track) => {
|
||||
if (!isFavorites) return true
|
||||
else return isDownloadedTrackAlsoFavorite(user, track)
|
||||
})
|
||||
}
|
||||
},
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||
@@ -147,3 +163,45 @@ function isDownloadedTrackAlsoFavorite(user: JellifyUser | undefined, track: Bas
|
||||
|
||||
return userData?.IsFavorite ?? false
|
||||
}
|
||||
|
||||
function getSortValue(item: BaseItemDto, sortBy: ItemSortBy): string | number {
|
||||
switch (sortBy) {
|
||||
case ItemSortBy.Name:
|
||||
case ItemSortBy.SortName:
|
||||
return item.Name ?? item.SortName ?? ''
|
||||
case ItemSortBy.Album:
|
||||
return item.Album ?? ''
|
||||
case ItemSortBy.Artist:
|
||||
return item.AlbumArtist ?? item.Artists?.[0] ?? ''
|
||||
case ItemSortBy.DateCreated:
|
||||
return item.DateCreated ? new Date(item.DateCreated).getTime() : 0
|
||||
case ItemSortBy.PlayCount:
|
||||
return item.UserData?.PlayCount ?? 0
|
||||
case ItemSortBy.PremiereDate:
|
||||
return item.PremiereDate ? new Date(item.PremiereDate).getTime() : 0
|
||||
case ItemSortBy.Runtime:
|
||||
return item.RunTimeTicks ?? 0
|
||||
default:
|
||||
return item.Name ?? item.SortName ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
function compareDownloadedTracks(
|
||||
a: BaseItemDto,
|
||||
b: BaseItemDto,
|
||||
sortBy: ItemSortBy,
|
||||
sortOrder: SortOrder,
|
||||
): number {
|
||||
const aVal = getSortValue(a, sortBy)
|
||||
const bVal = getSortValue(b, sortBy)
|
||||
const isDesc = sortOrder === SortOrder.Descending
|
||||
let cmp: number
|
||||
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
||||
cmp = aVal - bVal
|
||||
} else {
|
||||
const aStr = String(aVal)
|
||||
const bStr = String(bVal)
|
||||
cmp = aStr.localeCompare(bStr, undefined, { sensitivity: 'base' })
|
||||
}
|
||||
return isDesc ? -cmp : cmp
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ export const TracksQueryKey = (
|
||||
sortBy?: string,
|
||||
sortOrder?: string,
|
||||
genreIds?: string[],
|
||||
yearMin?: number,
|
||||
yearMax?: number,
|
||||
) => [
|
||||
TrackQueryKeys.AllTracks,
|
||||
library?.musicLibraryId,
|
||||
@@ -27,4 +29,6 @@ export const TracksQueryKey = (
|
||||
sortBy,
|
||||
sortOrder,
|
||||
genreIds && genreIds.length > 0 ? `genres:${genreIds.sort().join(',')}` : undefined,
|
||||
yearMin,
|
||||
yearMax,
|
||||
]
|
||||
|
||||
@@ -12,6 +12,7 @@ import { nitroFetch } from '../../../utils/nitro'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { ApiLimits } from '../../../../configs/query.config'
|
||||
import { JellifyUser } from '../../../../types/JellifyUser'
|
||||
import buildYearsParam from '../../../../utils/mapping/build-years-param'
|
||||
|
||||
export default function fetchTracks(
|
||||
api: Api | undefined,
|
||||
@@ -24,6 +25,8 @@ export default function fetchTracks(
|
||||
sortOrder: SortOrder = SortOrder.Ascending,
|
||||
artistId?: string,
|
||||
genreIds?: string[],
|
||||
yearMin?: number,
|
||||
yearMax?: number,
|
||||
) {
|
||||
return new Promise<BaseItemDto[]>((resolve, reject) => {
|
||||
if (isUndefined(api)) return reject('Client instance not set')
|
||||
@@ -43,6 +46,8 @@ export default function fetchTracks(
|
||||
filters.push(ItemFilter.IsUnplayed)
|
||||
}
|
||||
|
||||
const yearsParam = buildYearsParam(yearMin, yearMax)
|
||||
|
||||
nitroFetch<{ Items: BaseItemDto[] }>(api, '/Items', {
|
||||
IncludeItemTypes: [BaseItemKind.Audio],
|
||||
ParentId: library.musicLibraryId,
|
||||
@@ -56,6 +61,7 @@ export default function fetchTracks(
|
||||
Fields: [ItemFields.SortName],
|
||||
ArtistIds: artistId ? [artistId] : undefined,
|
||||
GenreIds: genreIds && genreIds.length > 0 ? genreIds : undefined,
|
||||
Years: yearsParam,
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.Items) return resolve(data.Items)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { fetchLibraryYears } from './utils'
|
||||
import { LibraryYearsQueryKey } from './keys'
|
||||
import { getApi, getUser, useJellifyLibrary } from '../../../stores'
|
||||
|
||||
export function useLibraryYears(): {
|
||||
years: number[]
|
||||
isPending: boolean
|
||||
isError: boolean
|
||||
} {
|
||||
const api = getApi()
|
||||
const user = getUser()
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
const {
|
||||
data: years = [],
|
||||
isPending,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: LibraryYearsQueryKey(library?.musicLibraryId, user?.id),
|
||||
queryFn: () => fetchLibraryYears(api, library, user?.id),
|
||||
enabled: Boolean(api && library && user?.id),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
|
||||
return { years, isPending, isError }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const LibraryYearsQueryKey = (libraryId: string | undefined, userId: string | undefined) => [
|
||||
'LibraryYears',
|
||||
libraryId,
|
||||
userId,
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import { JellifyLibrary } from '../../../../types/JellifyLibrary'
|
||||
import { nitroFetch } from '../../../utils/nitro'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
export type ItemsFiltersResponse = {
|
||||
Genres?: string[] | null
|
||||
Years?: number[] | null
|
||||
Tags?: string[] | null
|
||||
OfficialRatings?: string[] | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches available filter values (genres, years) for the music library via /Items/Filters.
|
||||
* Uses MusicAlbum so years reflect album release dates in the library.
|
||||
* Returns sorted ascending list of year numbers.
|
||||
*/
|
||||
export async function fetchLibraryYears(
|
||||
api: Api | undefined,
|
||||
library: JellifyLibrary | undefined,
|
||||
userId: string | undefined,
|
||||
): Promise<number[]> {
|
||||
if (isUndefined(api)) throw new Error('Client instance not set')
|
||||
if (isUndefined(library)) throw new Error('Library instance not set')
|
||||
if (isUndefined(userId)) throw new Error('User id required')
|
||||
|
||||
const data = await nitroFetch<ItemsFiltersResponse>(api, '/Items/Filters', {
|
||||
UserId: userId,
|
||||
ParentId: library.musicLibraryId,
|
||||
IncludeItemTypes: [BaseItemKind.MusicAlbum],
|
||||
})
|
||||
|
||||
const years = data?.Years ?? []
|
||||
return [...years].filter((y) => typeof y === 'number' && !Number.isNaN(y)).sort((a, b) => a - b)
|
||||
}
|
||||
@@ -77,7 +77,11 @@ export default function Albums({
|
||||
<FlashListStickyHeader text={album.toUpperCase()} />
|
||||
)
|
||||
) : typeof album === 'number' ? null : typeof album === 'object' ? (
|
||||
<ItemRow item={album} navigation={navigation} />
|
||||
<ItemRow
|
||||
item={album}
|
||||
navigation={navigation}
|
||||
sortingByReleasedDate={sortBy === ItemSortBy.PremiereDate}
|
||||
/>
|
||||
) : null
|
||||
|
||||
const onEndReached = () => {
|
||||
|
||||
@@ -7,9 +7,9 @@ import { useNavigation } from '@react-navigation/native'
|
||||
import DiscoverStackParamList from '../../../screens/Discover/types'
|
||||
import navigationRef from '../../../../navigation'
|
||||
import { useRecentlyAddedAlbums } from '../../../api/queries/album'
|
||||
import Animated, { Easing, FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'
|
||||
import AnimatedRow from '../../Global/helpers/animated-row'
|
||||
|
||||
export default function RecentlyAdded(): React.JSX.Element | null {
|
||||
export default function RecentlyAdded(): React.JSX.Element {
|
||||
const recentlyAddedAlbumsInfinityQuery = useRecentlyAddedAlbums()
|
||||
|
||||
const navigation = useNavigation<NativeStackNavigationProp<DiscoverStackParamList>>()
|
||||
@@ -18,15 +18,7 @@ export default function RecentlyAdded(): React.JSX.Element | null {
|
||||
recentlyAddedAlbumsInfinityQuery.data && recentlyAddedAlbumsInfinityQuery.data.length > 0
|
||||
|
||||
return recentlyAddedExists ? (
|
||||
<Animated.View
|
||||
entering={FadeIn.easing(Easing.in(Easing.ease))}
|
||||
exiting={FadeOut.easing(Easing.out(Easing.ease))}
|
||||
layout={LinearTransition.springify()}
|
||||
testID='discover-recently-added'
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<AnimatedRow testID='discover-recently-added'>
|
||||
<XStack
|
||||
alignItems='center'
|
||||
onPress={() => {
|
||||
@@ -64,6 +56,8 @@ export default function RecentlyAdded(): React.JSX.Element | null {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Animated.View>
|
||||
) : null
|
||||
</AnimatedRow>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ import DiscoverStackParamList from '../../../screens/Discover/types'
|
||||
import navigationRef from '../../../../navigation'
|
||||
import { useJellifyServer } from '../../../stores'
|
||||
import { usePublicPlaylists } from '../../../api/queries/playlist'
|
||||
import Animated, { Easing, FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'
|
||||
import AnimatedRow from '../../Global/helpers/animated-row'
|
||||
|
||||
export default function PublicPlaylists(): React.JSX.Element | null {
|
||||
export default function PublicPlaylists(): React.JSX.Element {
|
||||
const {
|
||||
data: playlists,
|
||||
fetchNextPage,
|
||||
@@ -29,15 +29,7 @@ export default function PublicPlaylists(): React.JSX.Element | null {
|
||||
const publicPlaylistsExist = playlists && playlists.length > 0
|
||||
|
||||
return publicPlaylistsExist ? (
|
||||
<Animated.View
|
||||
entering={FadeIn.easing(Easing.in(Easing.ease))}
|
||||
exiting={FadeOut.easing(Easing.out(Easing.ease))}
|
||||
layout={LinearTransition.springify()}
|
||||
testID='discover-public-playlists'
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<AnimatedRow testID='discover-public-playlists'>
|
||||
<XStack
|
||||
alignItems='center'
|
||||
onPress={() => {
|
||||
@@ -80,6 +72,8 @@ export default function PublicPlaylists(): React.JSX.Element | null {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Animated.View>
|
||||
) : null
|
||||
</AnimatedRow>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import navigationRef from '../../../../navigation'
|
||||
import { formatArtistNames } from '../../../utils/formatting/artist-names'
|
||||
import Animated, { Easing, FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'
|
||||
import ItemCard from '../../Global/components/item-card'
|
||||
import HorizontalCardList from '../../Global/components/horizontal-list'
|
||||
import { XStack } from 'tamagui'
|
||||
@@ -10,6 +9,7 @@ import { useNavigation } from '@react-navigation/native'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import DiscoverStackParamList from '../../../screens/Discover/types'
|
||||
import { useDiscoverAlbums } from '../../../api/queries/suggestions'
|
||||
import AnimatedRow from '../../Global/helpers/animated-row'
|
||||
|
||||
export default function SuggestedAlbums() {
|
||||
const suggestedAlbumsInfiniteQuery = useDiscoverAlbums()
|
||||
@@ -20,15 +20,7 @@ export default function SuggestedAlbums() {
|
||||
suggestedAlbumsInfiniteQuery.data && suggestedAlbumsInfiniteQuery.data.length > 0
|
||||
|
||||
return suggestedAlbumsExist ? (
|
||||
<Animated.View
|
||||
entering={FadeIn.easing(Easing.in(Easing.ease))}
|
||||
exiting={FadeOut.easing(Easing.out(Easing.ease))}
|
||||
layout={LinearTransition.springify()}
|
||||
testID='discover-suggested-albums'
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<AnimatedRow testID='discover-suggested-albums'>
|
||||
<XStack
|
||||
alignItems='center'
|
||||
onPress={() => {
|
||||
@@ -64,6 +56,8 @@ export default function SuggestedAlbums() {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Animated.View>
|
||||
) : null
|
||||
</AnimatedRow>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ import { useNavigation } from '@react-navigation/native'
|
||||
import DiscoverStackParamList from '../../../screens/Discover/types'
|
||||
import navigationRef from '../../../../navigation'
|
||||
import { pickFirstGenre } from '../../../utils/formatting/genres'
|
||||
import Animated, { Easing, FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'
|
||||
import { useDiscoverArtists } from '../../../api/queries/suggestions'
|
||||
import AnimatedRow from '../../Global/helpers/animated-row'
|
||||
|
||||
export default function SuggestedArtists(): React.JSX.Element | null {
|
||||
export default function SuggestedArtists(): React.JSX.Element {
|
||||
const suggestedArtistsInfiniteQuery = useDiscoverArtists()
|
||||
|
||||
const navigation = useNavigation<NativeStackNavigationProp<DiscoverStackParamList>>()
|
||||
@@ -19,15 +19,7 @@ export default function SuggestedArtists(): React.JSX.Element | null {
|
||||
suggestedArtistsInfiniteQuery.data && suggestedArtistsInfiniteQuery.data.length > 0
|
||||
|
||||
return suggestedArtistsExist ? (
|
||||
<Animated.View
|
||||
entering={FadeIn.easing(Easing.in(Easing.ease))}
|
||||
exiting={FadeOut.easing(Easing.out(Easing.ease))}
|
||||
layout={LinearTransition.springify()}
|
||||
testID='discover-suggested-artists'
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<AnimatedRow testID='discover-suggested-artists'>
|
||||
<XStack
|
||||
alignItems='center'
|
||||
onPress={() => {
|
||||
@@ -63,6 +55,8 @@ export default function SuggestedArtists(): React.JSX.Element | null {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Animated.View>
|
||||
) : null
|
||||
</AnimatedRow>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { FiltersProps } from './types'
|
||||
import Icon from '../Global/components/icon'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { RootStackParamList } from '../../screens/types'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
|
||||
export default function Filters({
|
||||
currentTab,
|
||||
@@ -27,6 +26,11 @@ export default function Filters({
|
||||
const isUnplayed = currentFilters.isUnplayed ?? false
|
||||
const selectedGenreIds = currentFilters.genreIds ?? []
|
||||
const hasGenresSelected = selectedGenreIds.length > 0
|
||||
const yearMin = currentFilters.yearMin
|
||||
const yearMax = currentFilters.yearMax
|
||||
const hasYearRange = yearMin != null || yearMax != null
|
||||
const yearRangeLabel =
|
||||
yearMin != null || yearMax != null ? `${yearMin ?? '…'} – ${yearMax ?? '…'}` : null
|
||||
|
||||
const handleFavoritesToggle = (checked: boolean | 'indeterminate') => {
|
||||
triggerHaptic('impactLight')
|
||||
@@ -60,6 +64,13 @@ export default function Filters({
|
||||
navigation?.navigate('GenreSelection')
|
||||
}
|
||||
|
||||
const handleYearRangeSelect = () => {
|
||||
triggerHaptic('impactLight')
|
||||
navigation?.navigate('YearSelection', {
|
||||
tab: currentTab === 'Tracks' || currentTab === 'Albums' ? currentTab : 'Tracks',
|
||||
})
|
||||
}
|
||||
|
||||
const handleUnplayedToggle = (checked: boolean | 'indeterminate') => {
|
||||
triggerHaptic('impactLight')
|
||||
if (currentTab === 'Tracks') {
|
||||
@@ -144,6 +155,37 @@ export default function Filters({
|
||||
</Button>
|
||||
</XStack>
|
||||
)}
|
||||
|
||||
{(isTracksTab || currentTab === 'Albums') && (
|
||||
<XStack alignItems='center' justifyContent='space-between' marginTop='$4'>
|
||||
<Button
|
||||
variant='outlined'
|
||||
size='$4'
|
||||
onPress={handleYearRangeSelect}
|
||||
pressStyle={{ opacity: 0.6 }}
|
||||
animation='quick'
|
||||
flex={1}
|
||||
justifyContent='space-between'
|
||||
disabled={isTracksTab && isDownloaded}
|
||||
>
|
||||
<Text
|
||||
color={
|
||||
isTracksTab && isDownloaded
|
||||
? '$borderColor'
|
||||
: hasYearRange
|
||||
? '$primary'
|
||||
: '$neutral'
|
||||
}
|
||||
>
|
||||
{hasYearRange ? `Year range ${yearRangeLabel}` : 'Year range'}
|
||||
</Text>
|
||||
<Icon
|
||||
name={hasYearRange ? 'filter-variant' : 'filter'}
|
||||
color={hasYearRange ? '$primary' : '$borderColor'}
|
||||
/>
|
||||
</Button>
|
||||
</XStack>
|
||||
)}
|
||||
</YStack>
|
||||
</YStack>
|
||||
)
|
||||
|
||||
@@ -142,15 +142,26 @@ export default function TrackRowContent({
|
||||
|
||||
<SlidingTextArea leftGapWidth={artworkAreaWidth} hasArtwork={!!showArtwork}>
|
||||
<YStack alignItems='flex-start' justifyContent='center'>
|
||||
<Text
|
||||
key={`${track.Id}-name`}
|
||||
bold
|
||||
color={textColor}
|
||||
lineBreakStrategyIOS='standard'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{trackName}
|
||||
</Text>
|
||||
<XStack alignItems='center'>
|
||||
<Text
|
||||
key={`${track.Id}-name`}
|
||||
bold
|
||||
color={textColor}
|
||||
lineBreakStrategyIOS='standard'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{trackName}
|
||||
</Text>
|
||||
{!shouldShowArtists && isExplicit(track as JellifyTrack) && (
|
||||
<XStack alignSelf='center' paddingLeft='$2'>
|
||||
<Icon
|
||||
name='alpha-e-box-outline'
|
||||
color={'$borderColor'}
|
||||
xxsmall
|
||||
/>
|
||||
</XStack>
|
||||
)}
|
||||
</XStack>
|
||||
|
||||
{shouldShowArtists && (
|
||||
<XStack alignItems='center'>
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface TrackProps {
|
||||
editing?: boolean | undefined
|
||||
sortingByAlbum?: boolean | undefined
|
||||
sortingByReleasedDate?: boolean | undefined
|
||||
sortingByPlayCount?: boolean | undefined
|
||||
}
|
||||
|
||||
export default function Track({
|
||||
@@ -54,6 +55,7 @@ export default function Track({
|
||||
editing,
|
||||
sortingByAlbum,
|
||||
sortingByReleasedDate,
|
||||
sortingByPlayCount,
|
||||
}: TrackProps): React.JSX.Element {
|
||||
const theme = useTheme()
|
||||
const [artworkAreaWidth, setArtworkAreaWidth] = useState(0)
|
||||
@@ -141,7 +143,9 @@ export default function Track({
|
||||
? track.Album
|
||||
: sortingByReleasedDate
|
||||
? `${track.ProductionYear?.toString()} • ${track.Artists?.join(' • ')}`
|
||||
: track.Artists?.join(' • ')) ?? ''
|
||||
: sortingByPlayCount
|
||||
? `${track.UserData?.PlayCount?.toString()} • ${track.Artists?.join(' • ')}`
|
||||
: track.Artists?.join(' • ')) ?? ''
|
||||
|
||||
// Memoize track name
|
||||
const trackName = track.Name ?? 'Untitled Track'
|
||||
|
||||
@@ -38,6 +38,7 @@ interface ItemRowProps {
|
||||
onLongPress?: () => void
|
||||
navigation?: Pick<NativeStackNavigationProp<BaseStackParamList>, 'navigate' | 'dispatch'>
|
||||
queueName?: Queue
|
||||
sortingByReleasedDate?: boolean | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,6 +59,7 @@ function ItemRow({
|
||||
onPress,
|
||||
onLongPress,
|
||||
queueName,
|
||||
sortingByReleasedDate,
|
||||
}: ItemRowProps): React.JSX.Element {
|
||||
const artworkAreaWidth = useSharedValue(0)
|
||||
|
||||
@@ -170,7 +172,7 @@ function ItemRow({
|
||||
>
|
||||
<HideableArtwork item={item} circular={circular} onLayout={handleArtworkLayout} />
|
||||
<SlidingTextArea leftGapWidth={artworkAreaWidth}>
|
||||
<ItemRowDetails item={item} />
|
||||
<ItemRowDetails item={item} sortingByReleasedDate={sortingByReleasedDate} />
|
||||
</SlidingTextArea>
|
||||
|
||||
<XStack justifyContent='flex-end' alignItems='center' flexShrink={1}>
|
||||
@@ -235,7 +237,13 @@ function ItemRow({
|
||||
)
|
||||
}
|
||||
|
||||
function ItemRowDetails({ item }: { item: BaseItemDto }): React.JSX.Element {
|
||||
function ItemRowDetails({
|
||||
item,
|
||||
sortingByReleasedDate,
|
||||
}: {
|
||||
item: BaseItemDto
|
||||
sortingByReleasedDate?: boolean | undefined
|
||||
}): React.JSX.Element {
|
||||
const route = useRoute<RouteProp<BaseStackParamList>>()
|
||||
|
||||
const shouldRenderArtistName =
|
||||
@@ -253,7 +261,10 @@ function ItemRowDetails({ item }: { item: BaseItemDto }): React.JSX.Element {
|
||||
|
||||
{shouldRenderArtistName && (
|
||||
<Text color={'$borderColor'} lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{formatArtistName(item.AlbumArtist)}
|
||||
{formatArtistName(
|
||||
item.AlbumArtist,
|
||||
sortingByReleasedDate ? item.ProductionYear?.toString() : undefined,
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
ReduceMotion,
|
||||
FadeOut,
|
||||
LinearTransition,
|
||||
Easing,
|
||||
} from 'react-native-reanimated'
|
||||
|
||||
interface AnimatedRowProps {
|
||||
children: React.ReactNode
|
||||
testID?: string
|
||||
}
|
||||
|
||||
export default function AnimatedRow({ children, testID }: AnimatedRowProps) {
|
||||
return (
|
||||
<Animated.View
|
||||
testID={testID}
|
||||
entering={FadeIn.easing(Easing.in(Easing.ease)).reduceMotion(ReduceMotion.System)}
|
||||
exiting={FadeOut.easing(Easing.out(Easing.ease)).reduceMotion(ReduceMotion.System)}
|
||||
layout={LinearTransition.springify().reduceMotion(ReduceMotion.System)}
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { RootStackParamList } from '../../../screens/types'
|
||||
import { useFrequentlyPlayedArtists } from '../../../api/queries/frequents'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
import { pickFirstGenre } from '../../../utils/formatting/genres'
|
||||
import Animated, { Easing, FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'
|
||||
import AnimatedRow from '../../Global/helpers/animated-row'
|
||||
|
||||
export default function FrequentArtists(): React.JSX.Element {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<HomeStackParamList>>()
|
||||
@@ -41,14 +41,7 @@ export default function FrequentArtists(): React.JSX.Element {
|
||||
)
|
||||
|
||||
return frequentArtistsInfiniteQuery.data ? (
|
||||
<Animated.View
|
||||
entering={FadeIn.easing(Easing.in(Easing.ease))}
|
||||
exiting={FadeOut.easing(Easing.out(Easing.ease))}
|
||||
layout={LinearTransition.springify()}
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<AnimatedRow testID='home-frequent-artists'>
|
||||
<XStack
|
||||
alignItems='center'
|
||||
onPress={() => {
|
||||
@@ -63,7 +56,7 @@ export default function FrequentArtists(): React.JSX.Element {
|
||||
data={frequentArtistsInfiniteQuery.data.slice(0, horizontalItems) ?? []}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</Animated.View>
|
||||
</AnimatedRow>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ import HomeStackParamList from '../../../screens/Home/types'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { RootStackParamList } from '../../../screens/types'
|
||||
import { useFrequentlyPlayedTracks } from '../../../api/queries/frequents'
|
||||
import Animated, { Easing, FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'
|
||||
import AnimatedRow from '../../Global/helpers/animated-row'
|
||||
|
||||
export default function FrequentlyPlayedTracks(): React.JSX.Element {
|
||||
const tracksInfiniteQuery = useFrequentlyPlayedTracks()
|
||||
@@ -23,14 +23,7 @@ export default function FrequentlyPlayedTracks(): React.JSX.Element {
|
||||
const { horizontalItems } = useDisplayContext()
|
||||
|
||||
return tracksInfiniteQuery.data ? (
|
||||
<Animated.View
|
||||
entering={FadeIn.easing(Easing.in(Easing.ease))}
|
||||
exiting={FadeOut.easing(Easing.out(Easing.ease))}
|
||||
layout={LinearTransition.springify()}
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<AnimatedRow testID='home-frequent-tracks'>
|
||||
<XStack
|
||||
alignItems='center'
|
||||
onPress={() => {
|
||||
@@ -75,7 +68,7 @@ export default function FrequentlyPlayedTracks(): React.JSX.Element {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Animated.View>
|
||||
</AnimatedRow>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import HomeStackParamList from '../../../screens/Home/types'
|
||||
import { useRecentArtists } from '../../../api/queries/recents'
|
||||
import { pickFirstGenre } from '../../../utils/formatting/genres'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'
|
||||
import Animated, { Easing, FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'
|
||||
import AnimatedRow from '../../Global/helpers/animated-row'
|
||||
|
||||
export default function RecentArtists(): React.JSX.Element {
|
||||
const recentArtistsInfiniteQuery = useRecentArtists()
|
||||
@@ -47,14 +47,7 @@ export default function RecentArtists(): React.JSX.Element {
|
||||
)
|
||||
|
||||
return recentArtistsInfiniteQuery.data ? (
|
||||
<Animated.View
|
||||
entering={FadeIn.easing(Easing.in(Easing.ease))}
|
||||
exiting={FadeOut.easing(Easing.out(Easing.ease))}
|
||||
layout={LinearTransition.springify()}
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<AnimatedRow testID='home-recent-artists'>
|
||||
<XStack alignItems='center' onPress={handleHeaderPress}>
|
||||
<H5 marginLeft={'$2'}>Recent Artists</H5>
|
||||
<Icon name='arrow-right' />
|
||||
@@ -64,7 +57,7 @@ export default function RecentArtists(): React.JSX.Element {
|
||||
data={recentArtistsInfiniteQuery.data.slice(0, horizontalItems)}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</Animated.View>
|
||||
</AnimatedRow>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
|
||||
@@ -11,8 +11,8 @@ import { useDisplayContext } from '../../../providers/Display/display-provider'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import HomeStackParamList from '../../../screens/Home/types'
|
||||
import { useRecentlyPlayedTracks } from '../../../api/queries/recents'
|
||||
import Animated, { Easing, FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'
|
||||
import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client'
|
||||
import AnimatedRow from '../../Global/helpers/animated-row'
|
||||
|
||||
export default function RecentlyPlayed(): React.JSX.Element {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<HomeStackParamList>>()
|
||||
@@ -44,14 +44,7 @@ export default function RecentlyPlayed(): React.JSX.Element {
|
||||
}
|
||||
|
||||
return tracksInfiniteQuery.data ? (
|
||||
<Animated.View
|
||||
entering={FadeIn.easing(Easing.in(Easing.ease))}
|
||||
exiting={FadeOut.easing(Easing.out(Easing.ease))}
|
||||
layout={LinearTransition.springify()}
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<AnimatedRow testID='home-recently-played'>
|
||||
<XStack
|
||||
alignItems='center'
|
||||
onPress={() => {
|
||||
@@ -88,7 +81,7 @@ export default function RecentlyPlayed(): React.JSX.Element {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Animated.View>
|
||||
</AnimatedRow>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
|
||||
@@ -32,7 +32,9 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
|
||||
(currentFilters.isFavorites === true ||
|
||||
currentFilters.isDownloaded === true ||
|
||||
currentFilters.isUnplayed === true ||
|
||||
(currentFilters.genreIds && currentFilters.genreIds.length > 0))
|
||||
(currentFilters.genreIds && currentFilters.genreIds.length > 0) ||
|
||||
currentFilters.yearMin != null ||
|
||||
currentFilters.yearMax != null)
|
||||
|
||||
const handleShufflePress = async () => {
|
||||
triggerHaptic('impactLight')
|
||||
@@ -163,11 +165,15 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
|
||||
isDownloaded: false,
|
||||
isUnplayed: false,
|
||||
genreIds: undefined,
|
||||
yearMin: undefined,
|
||||
yearMax: undefined,
|
||||
})
|
||||
} else if (currentTab === 'Albums') {
|
||||
useLibraryStore
|
||||
.getState()
|
||||
.setAlbumsFilters({ isFavorites: undefined })
|
||||
useLibraryStore.getState().setAlbumsFilters({
|
||||
isFavorites: undefined,
|
||||
yearMin: undefined,
|
||||
yearMax: undefined,
|
||||
})
|
||||
} else if (currentTab === 'Artists') {
|
||||
useLibraryStore
|
||||
.getState()
|
||||
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
} from '../../../hooks/player/callbacks'
|
||||
import { useRepeatModeStoreValue, useShuffle } from '../../../stores/player/queue'
|
||||
|
||||
export default function Controls(): React.JSX.Element {
|
||||
export default function Controls({
|
||||
onLyricsScreen,
|
||||
}: {
|
||||
onLyricsScreen?: boolean
|
||||
}): React.JSX.Element {
|
||||
const previous = usePrevious()
|
||||
const skip = useSkip()
|
||||
const repeatMode = useRepeatModeStoreValue()
|
||||
@@ -24,12 +28,14 @@ export default function Controls(): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<XStack alignItems='center' justifyContent='space-between'>
|
||||
<Icon
|
||||
small
|
||||
color={shuffled ? '$primary' : '$color'}
|
||||
name='shuffle'
|
||||
onPress={() => toggleShuffle(shuffled)}
|
||||
/>
|
||||
{!onLyricsScreen && (
|
||||
<Icon
|
||||
small
|
||||
color={shuffled ? '$primary' : '$color'}
|
||||
name='shuffle'
|
||||
onPress={() => toggleShuffle(shuffled)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Spacer />
|
||||
|
||||
@@ -54,12 +60,14 @@ export default function Controls(): React.JSX.Element {
|
||||
|
||||
<Spacer />
|
||||
|
||||
<Icon
|
||||
small
|
||||
color={repeatMode === RepeatMode.Off ? '$color' : '$primary'}
|
||||
name={repeatMode === RepeatMode.Track ? 'repeat-once' : 'repeat'}
|
||||
onPress={async () => toggleRepeatMode()}
|
||||
/>
|
||||
{!onLyricsScreen && (
|
||||
<Icon
|
||||
small
|
||||
color={repeatMode === RepeatMode.Off ? '$color' : '$primary'}
|
||||
name={repeatMode === RepeatMode.Track ? 'repeat-once' : 'repeat'}
|
||||
onPress={async () => toggleRepeatMode()}
|
||||
/>
|
||||
)}
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { useProgress } from '../../../hooks/player/queries'
|
||||
import { useSeekTo } from '../../../hooks/player/callbacks'
|
||||
import { UPDATE_INTERVAL } from '../../../configs/player.config'
|
||||
import React, { useEffect, useMemo, useRef, useCallback } from 'react'
|
||||
import React, { useEffect, useMemo, useRef, useCallback, useState } from 'react'
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
@@ -17,9 +17,13 @@ import Animated, {
|
||||
SharedValue,
|
||||
} from 'react-native-reanimated'
|
||||
import { FlatList, ListRenderItem } from 'react-native'
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import useRawLyrics from '../../../api/queries/lyrics'
|
||||
import { useCurrentTrack } from '../../../stores/player/queue'
|
||||
import Scrubber from './scrubber'
|
||||
import Controls from './controls'
|
||||
|
||||
interface LyricLine {
|
||||
Text: string
|
||||
@@ -33,7 +37,7 @@ interface ParsedLyricLine {
|
||||
}
|
||||
|
||||
const AnimatedText = Animated.createAnimatedComponent(Text)
|
||||
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList<ParsedLyricLine>)
|
||||
const AnimatedFlatList = Animated.FlatList<ParsedLyricLine>
|
||||
|
||||
// Memoized lyric line component for better performance
|
||||
const LyricLineItem = React.memo(
|
||||
@@ -42,17 +46,19 @@ const LyricLineItem = React.memo(
|
||||
index,
|
||||
currentLineIndex,
|
||||
onPress,
|
||||
onLayout: onItemLayout,
|
||||
}: {
|
||||
item: ParsedLyricLine
|
||||
index: number
|
||||
currentLineIndex: SharedValue<number>
|
||||
onPress: (startTime: number, index: number) => void
|
||||
onLayout?: (index: number, height: number) => void
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
|
||||
// Get theme-aware colors
|
||||
const primaryColor = theme.color.val // Primary text color (adapts to dark/light)
|
||||
const neutralColor = theme.neutral.val // Secondary text color
|
||||
const neutralColor = theme.color.val + '95' // Secondary text color
|
||||
const highlightColor = theme.primary.val // Highlight color (primaryDark/primaryLight)
|
||||
const translucentColor = theme.translucent?.val // Theme-aware translucent background
|
||||
const backgroundHighlight = translucentColor || theme.primary.val + '15' // Fallback with 15% opacity
|
||||
@@ -114,49 +120,70 @@ const LyricLineItem = React.memo(
|
||||
}
|
||||
})
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
onPress(item.startTime, index)
|
||||
}, [item.startTime, index, onPress])
|
||||
const tapGesture = useMemo(
|
||||
() =>
|
||||
Gesture.Tap()
|
||||
.maxDistance(10)
|
||||
.maxDuration(500)
|
||||
.runOnJS(true)
|
||||
.onEnd((_e, success) => {
|
||||
if (success) {
|
||||
onPress(item.startTime, index)
|
||||
}
|
||||
}),
|
||||
[item.startTime, index, onPress],
|
||||
)
|
||||
|
||||
const handleLayout = useCallback(
|
||||
(e: { nativeEvent: { layout: { height: number } } }) => {
|
||||
onItemLayout?.(index, e.nativeEvent.layout.height)
|
||||
},
|
||||
[index, onItemLayout],
|
||||
)
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
onLayout={handleLayout}
|
||||
style={[
|
||||
{
|
||||
paddingVertical: 12,
|
||||
paddingVertical: 3,
|
||||
paddingHorizontal: 20,
|
||||
minHeight: 60,
|
||||
justifyContent: 'center',
|
||||
marginHorizontal: 16,
|
||||
marginVertical: 4,
|
||||
marginVertical: 0,
|
||||
},
|
||||
animatedStyle,
|
||||
]}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 8,
|
||||
},
|
||||
backgroundStyle,
|
||||
]}
|
||||
onTouchEnd={handlePress}
|
||||
>
|
||||
<AnimatedText
|
||||
<GestureDetector gesture={tapGesture}>
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
fontSize: 18,
|
||||
lineHeight: 28,
|
||||
textAlign: 'center',
|
||||
fontWeight: '500',
|
||||
alignSelf: 'stretch',
|
||||
minWidth: 0,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 10,
|
||||
borderRadius: 8,
|
||||
},
|
||||
textColorStyle,
|
||||
backgroundStyle,
|
||||
]}
|
||||
>
|
||||
{item.text}
|
||||
</AnimatedText>
|
||||
</Animated.View>
|
||||
<AnimatedText
|
||||
style={[
|
||||
{
|
||||
fontSize: 18,
|
||||
lineHeight: 28,
|
||||
textAlign: 'left',
|
||||
fontWeight: '500',
|
||||
},
|
||||
textColorStyle,
|
||||
]}
|
||||
>
|
||||
{item.text}
|
||||
</AnimatedText>
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
</Animated.View>
|
||||
)
|
||||
},
|
||||
@@ -169,16 +196,30 @@ export default function Lyrics({
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<PlayerParamList>
|
||||
}): React.JSX.Element {
|
||||
const theme = useTheme()
|
||||
const { data: lyrics } = useRawLyrics()
|
||||
const nowPlaying = useCurrentTrack()
|
||||
const { height } = useWindowDimensions()
|
||||
const { position } = useProgress(UPDATE_INTERVAL)
|
||||
const seekTo = useSeekTo()
|
||||
|
||||
const flatListRef = useRef<FlatList<ParsedLyricLine>>(null)
|
||||
const viewportHeightRef = useRef(height)
|
||||
const isInitialMountRef = useRef(true)
|
||||
const itemHeightsRef = useRef<Record<number, number>>({})
|
||||
const currentLineIndex = useSharedValue(-1)
|
||||
const scrollY = useSharedValue(0)
|
||||
const isUserScrolling = useSharedValue(false)
|
||||
|
||||
const color = theme.color.val
|
||||
|
||||
const handleFlatListLayout = useCallback(
|
||||
(e: { nativeEvent: { layout: { height: number } } }) => {
|
||||
viewportHeightRef.current = e.nativeEvent.layout.height
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
// Convert lyrics from ticks to seconds and parse
|
||||
const parsedLyrics = useMemo<ParsedLyricLine[]>(() => {
|
||||
if (!lyrics) return []
|
||||
@@ -204,6 +245,17 @@ export default function Lyrics({
|
||||
[parsedLyrics],
|
||||
)
|
||||
|
||||
// Delay showing "No lyrics available" to avoid flash during track transitions
|
||||
const [showNoLyricsMessage, setShowNoLyricsMessage] = useState(false)
|
||||
useEffect(() => {
|
||||
if (parsedLyrics.length > 0) {
|
||||
setShowNoLyricsMessage(false)
|
||||
return
|
||||
}
|
||||
const timer = setTimeout(() => setShowNoLyricsMessage(true), 3000)
|
||||
return () => clearTimeout(timer)
|
||||
}, [parsedLyrics.length])
|
||||
|
||||
// Track manually selected lyric for immediate feedback
|
||||
const manuallySelectedIndex = useSharedValue(-1)
|
||||
const manualSelectTimeout = useRef<NodeJS.Timeout | null>(null)
|
||||
@@ -230,48 +282,94 @@ export default function Lyrics({
|
||||
return found
|
||||
}, [position, lyricStartTimes])
|
||||
|
||||
// Simple auto-scroll that keeps highlighted lyric in center
|
||||
const scrollToCurrentLyric = useCallback(() => {
|
||||
if (
|
||||
currentLyricIndex >= 0 &&
|
||||
currentLyricIndex < parsedLyrics.length &&
|
||||
flatListRef.current &&
|
||||
!isUserScrolling.value
|
||||
) {
|
||||
try {
|
||||
// Use scrollToIndex with viewPosition 0.5 to center the lyric
|
||||
flatListRef.current.scrollToIndex({
|
||||
index: currentLyricIndex,
|
||||
animated: true,
|
||||
viewPosition: 0.5, // 0.5 = center of visible area
|
||||
})
|
||||
} catch (error) {
|
||||
// Fallback to scrollToOffset if scrollToIndex fails
|
||||
console.warn('scrollToIndex failed, using fallback')
|
||||
const estimatedItemHeight = 80
|
||||
const targetOffset = Math.max(
|
||||
0,
|
||||
currentLyricIndex * estimatedItemHeight - height * 0.4,
|
||||
)
|
||||
const ESTIMATED_ITEM_HEIGHT = 70
|
||||
const CONTENT_PADDING_TOP = height * 0.1
|
||||
|
||||
flatListRef.current.scrollToOffset({
|
||||
offset: targetOffset,
|
||||
animated: true,
|
||||
})
|
||||
const pendingScrollOffsetRef = useRef<number | null>(null)
|
||||
|
||||
const getItemHeight = useCallback((index: number) => {
|
||||
return itemHeightsRef.current[index] ?? ESTIMATED_ITEM_HEIGHT
|
||||
}, [])
|
||||
|
||||
const getItemCenterY = useCallback(
|
||||
(index: number) => {
|
||||
let offset = CONTENT_PADDING_TOP
|
||||
for (let i = 0; i < index; i++) {
|
||||
offset += getItemHeight(i)
|
||||
}
|
||||
}
|
||||
}, [currentLyricIndex, parsedLyrics.length, height])
|
||||
return offset + getItemHeight(index) / 2
|
||||
},
|
||||
[CONTENT_PADDING_TOP, getItemHeight],
|
||||
)
|
||||
|
||||
const onItemLayout = useCallback((index: number, itemHeight: number) => {
|
||||
itemHeightsRef.current[index] = itemHeight
|
||||
}, [])
|
||||
|
||||
// On mount: scroll to center current line. Otherwise: only scroll when current line is within center 75%
|
||||
useEffect(() => {
|
||||
// Only update if there's no manual selection active
|
||||
if (manuallySelectedIndex.value === -1) {
|
||||
currentLineIndex.value = withTiming(currentLyricIndex, { duration: 300 })
|
||||
}
|
||||
|
||||
// Delay scroll to allow for smooth animation
|
||||
const scrollTimeout = setTimeout(scrollToCurrentLyric, 100)
|
||||
if (
|
||||
currentLyricIndex < 0 ||
|
||||
currentLyricIndex >= parsedLyrics.length ||
|
||||
!flatListRef.current ||
|
||||
isUserScrolling.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const forceScroll = isInitialMountRef.current
|
||||
if (!forceScroll) {
|
||||
// Center 75% check: only scroll when current line is within center 75% of viewport
|
||||
const viewportHeight = viewportHeightRef.current
|
||||
const currentScrollY = scrollY.value
|
||||
const center75Top = currentScrollY + viewportHeight * 0.125
|
||||
const center75Bottom = currentScrollY + viewportHeight * 0.875
|
||||
const currentLineCenter = getItemCenterY(currentLyricIndex)
|
||||
const isInCenter75 =
|
||||
currentLineCenter >= center75Top && currentLineCenter <= center75Bottom
|
||||
if (!isInCenter75) return
|
||||
}
|
||||
|
||||
const viewportHeight = viewportHeightRef.current
|
||||
const itemCenterY = getItemCenterY(currentLyricIndex)
|
||||
const targetOffset = Math.max(0, itemCenterY - viewportHeight / 2)
|
||||
|
||||
const doScroll = () => {
|
||||
if (!flatListRef.current) return
|
||||
if (forceScroll) isInitialMountRef.current = false
|
||||
pendingScrollOffsetRef.current = null
|
||||
flatListRef.current.scrollToOffset({
|
||||
offset: targetOffset,
|
||||
animated: true,
|
||||
})
|
||||
}
|
||||
|
||||
if (forceScroll) {
|
||||
pendingScrollOffsetRef.current = targetOffset
|
||||
}
|
||||
|
||||
const scrollTimeout = setTimeout(doScroll, 300)
|
||||
return () => clearTimeout(scrollTimeout)
|
||||
}, [currentLyricIndex, scrollToCurrentLyric])
|
||||
}, [currentLyricIndex, parsedLyrics.length, height, getItemCenterY])
|
||||
|
||||
// When track changes (next song), scroll to top
|
||||
const prevTrackIdRef = useRef<string | undefined>(undefined)
|
||||
useEffect(() => {
|
||||
const trackId = nowPlaying?.item?.Id
|
||||
if (prevTrackIdRef.current !== undefined && prevTrackIdRef.current !== trackId) {
|
||||
if (flatListRef.current && parsedLyrics.length) {
|
||||
flatListRef.current.scrollToOffset({ offset: 0, animated: false })
|
||||
}
|
||||
isInitialMountRef.current = true
|
||||
}
|
||||
prevTrackIdRef.current = trackId
|
||||
itemHeightsRef.current = {}
|
||||
}, [nowPlaying?.item?.Id, parsedLyrics.length])
|
||||
|
||||
// Reset manual selection when the actual position catches up
|
||||
useEffect(() => {
|
||||
@@ -319,6 +417,27 @@ export default function Lyrics({
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Find lyric index for a given playback position (same logic as currentLyricIndex)
|
||||
const findLyricIndexForPosition = useCallback(
|
||||
(pos: number) => {
|
||||
if (lyricStartTimes.length === 0) return -1
|
||||
let low = 0
|
||||
let high = lyricStartTimes.length - 1
|
||||
let found = -1
|
||||
while (low <= high) {
|
||||
const mid = Math.floor((low + high) / 2)
|
||||
if (pos >= lyricStartTimes[mid]) {
|
||||
found = mid
|
||||
low = mid + 1
|
||||
} else {
|
||||
high = mid - 1
|
||||
}
|
||||
}
|
||||
return found
|
||||
},
|
||||
[lyricStartTimes],
|
||||
)
|
||||
|
||||
// Scroll to specific lyric keeping it centered
|
||||
const scrollToLyric = useCallback(
|
||||
(lyricIndex: number) => {
|
||||
@@ -333,11 +452,8 @@ export default function Lyrics({
|
||||
} catch (error) {
|
||||
// Fallback to scrollToOffset if scrollToIndex fails
|
||||
console.warn('scrollToIndex failed, using fallback')
|
||||
const estimatedItemHeight = 80
|
||||
const targetOffset = Math.max(
|
||||
0,
|
||||
lyricIndex * estimatedItemHeight - height * 0.4,
|
||||
)
|
||||
const itemCenterY = getItemCenterY(lyricIndex)
|
||||
const targetOffset = Math.max(0, itemCenterY - viewportHeightRef.current / 2)
|
||||
|
||||
flatListRef.current.scrollToOffset({
|
||||
offset: targetOffset,
|
||||
@@ -346,7 +462,7 @@ export default function Lyrics({
|
||||
}
|
||||
}
|
||||
},
|
||||
[parsedLyrics.length, height],
|
||||
[parsedLyrics.length, height, getItemCenterY],
|
||||
)
|
||||
|
||||
// Handle seeking to specific lyric timestamp
|
||||
@@ -382,10 +498,13 @@ export default function Lyrics({
|
||||
)
|
||||
|
||||
// Handle back navigation
|
||||
const handleBackPress = useCallback(() => {
|
||||
trigger('impactLight') // Haptic feedback for navigation
|
||||
navigation.goBack()
|
||||
}, [navigation])
|
||||
const handleBackPress = useCallback(
|
||||
(triggerHaptic: boolean | undefined = true) => {
|
||||
if (triggerHaptic) trigger('impactLight') // Haptic feedback for navigation
|
||||
navigation.goBack()
|
||||
},
|
||||
[navigation],
|
||||
)
|
||||
|
||||
// Optimized render item for FlatList
|
||||
const renderLyricItem: ListRenderItem<ParsedLyricLine> = useCallback(
|
||||
@@ -394,46 +513,65 @@ export default function Lyrics({
|
||||
<LyricLineItem
|
||||
item={item}
|
||||
index={index}
|
||||
onLayout={onItemLayout}
|
||||
currentLineIndex={currentLineIndex}
|
||||
onPress={handleLyricPress}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[currentLineIndex, handleLyricPress],
|
||||
[currentLineIndex, handleLyricPress, onItemLayout],
|
||||
)
|
||||
|
||||
// Removed getItemLayout to prevent crashes with dynamic content heights
|
||||
const contentPaddingTop = height * 0.1
|
||||
|
||||
const getItemOffset = useCallback(
|
||||
(index: number) => {
|
||||
let offset = contentPaddingTop
|
||||
for (let i = 0; i < index; i++) {
|
||||
offset += getItemHeight(i)
|
||||
}
|
||||
return offset
|
||||
},
|
||||
[contentPaddingTop, getItemHeight],
|
||||
)
|
||||
|
||||
const getItemLayout = useCallback(
|
||||
(_: unknown, index: number) => ({
|
||||
length: getItemHeight(index),
|
||||
offset: getItemOffset(index),
|
||||
index,
|
||||
}),
|
||||
[getItemHeight, getItemOffset],
|
||||
)
|
||||
|
||||
const handleContentSizeChange = useCallback((_w: number, contentHeight: number) => {
|
||||
const pending = pendingScrollOffsetRef.current
|
||||
const viewportHeight = viewportHeightRef.current
|
||||
// Content must be tall enough to scroll to target (max offset = contentHeight - viewportHeight)
|
||||
if (pending !== null && flatListRef.current && contentHeight >= pending + viewportHeight) {
|
||||
pendingScrollOffsetRef.current = null
|
||||
isInitialMountRef.current = false
|
||||
flatListRef.current.scrollToOffset({
|
||||
offset: pending,
|
||||
animated: true,
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
const keyExtractor = useCallback(
|
||||
(item: ParsedLyricLine, index: number) => `lyric-${index}-${item.startTime}`,
|
||||
[],
|
||||
)
|
||||
|
||||
if (!parsedLyrics.length) {
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View flex={1}>
|
||||
<ZStack fullscreen>
|
||||
<BlurredBackground />
|
||||
<YStack fullscreen justifyContent='center' alignItems='center'>
|
||||
<Text fontSize={18} color='$neutral' textAlign='center'>
|
||||
No lyrics available
|
||||
</Text>
|
||||
</YStack>
|
||||
</ZStack>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
const blockSwipeGesture = Gesture.Pan().minDistance(0)
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
|
||||
<View flex={1}>
|
||||
<ZStack fullscreen>
|
||||
<BlurredBackground />
|
||||
|
||||
<YStack fullscreen>
|
||||
{/* Header with back button */}
|
||||
<XStack
|
||||
alignItems='center'
|
||||
justifyContent='space-between'
|
||||
@@ -443,46 +581,97 @@ export default function Lyrics({
|
||||
>
|
||||
<XStack
|
||||
alignItems='center'
|
||||
onPress={handleBackPress}
|
||||
onPress={() => handleBackPress()}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Icon small name='chevron-left' />
|
||||
</XStack>
|
||||
<YStack>
|
||||
<Text
|
||||
fontSize={16}
|
||||
fontWeight='bold'
|
||||
color={color}
|
||||
textAlign='center'
|
||||
>
|
||||
{nowPlaying?.item?.Name}
|
||||
</Text>
|
||||
<Text fontSize={14} color={color} textAlign='center'>
|
||||
{nowPlaying?.item?.ArtistItems?.map(
|
||||
(artist) => artist.Name,
|
||||
).join(', ')}
|
||||
</Text>
|
||||
</YStack>
|
||||
<Spacer width={28} /> {/* Balance the layout */}
|
||||
</XStack>
|
||||
|
||||
<AnimatedFlatList
|
||||
ref={flatListRef}
|
||||
data={parsedLyrics}
|
||||
renderItem={renderLyricItem}
|
||||
keyExtractor={keyExtractor}
|
||||
onScroll={scrollHandler}
|
||||
scrollEventThrottle={16}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{
|
||||
paddingTop: height * 0.1,
|
||||
paddingBottom: height * 0.5,
|
||||
}}
|
||||
style={{ flex: 1 }}
|
||||
removeClippedSubviews={false}
|
||||
maxToRenderPerBatch={15}
|
||||
windowSize={15}
|
||||
initialNumToRender={15}
|
||||
onScrollToIndexFailed={(error) => {
|
||||
console.warn('ScrollToIndex failed:', error)
|
||||
// Fallback to scrollToOffset
|
||||
if (flatListRef.current) {
|
||||
const targetOffset = Math.max(
|
||||
0,
|
||||
error.index * 80 - height * 0.4,
|
||||
)
|
||||
flatListRef.current.scrollToOffset({
|
||||
offset: targetOffset,
|
||||
animated: true,
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{parsedLyrics.length > 0 ? (
|
||||
<AnimatedFlatList
|
||||
ref={flatListRef}
|
||||
data={parsedLyrics}
|
||||
renderItem={renderLyricItem}
|
||||
keyExtractor={keyExtractor}
|
||||
getItemLayout={getItemLayout}
|
||||
onLayout={handleFlatListLayout}
|
||||
onContentSizeChange={handleContentSizeChange}
|
||||
onScroll={scrollHandler}
|
||||
scrollEventThrottle={16}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{
|
||||
paddingTop: height * 0.1,
|
||||
paddingBottom: height * 0.5 + 100, // Extra for miniplayer overlay
|
||||
}}
|
||||
style={{ flex: 1 }}
|
||||
removeClippedSubviews={false}
|
||||
maxToRenderPerBatch={15}
|
||||
windowSize={15}
|
||||
initialNumToRender={15}
|
||||
onScrollToIndexFailed={(error) => {
|
||||
console.warn('ScrollToIndex failed:', error)
|
||||
// Fallback to scrollToOffset
|
||||
if (flatListRef.current) {
|
||||
const itemCenterY = getItemCenterY(error.index)
|
||||
const targetOffset = Math.max(
|
||||
0,
|
||||
itemCenterY - viewportHeightRef.current / 2,
|
||||
)
|
||||
flatListRef.current.scrollToOffset({
|
||||
offset: targetOffset,
|
||||
animated: true,
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<YStack justifyContent='center' alignItems='center' flex={1}>
|
||||
{showNoLyricsMessage && (
|
||||
<Text fontSize={18} color='$color' textAlign='center'>
|
||||
No lyrics available
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
)}
|
||||
<GestureDetector gesture={blockSwipeGesture}>
|
||||
<YStack
|
||||
justifyContent='flex-start'
|
||||
gap={'$3'}
|
||||
flexShrink={1}
|
||||
padding='$5'
|
||||
paddingBottom='$7'
|
||||
>
|
||||
<Scrubber
|
||||
onSeekComplete={(position) => {
|
||||
const index = findLyricIndexForPosition(position)
|
||||
if (index >= 0) {
|
||||
currentLineIndex.value = withTiming(index, {
|
||||
duration: 200,
|
||||
})
|
||||
scrollToLyric(index)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Controls onLyricsScreen />
|
||||
</YStack>
|
||||
</GestureDetector>
|
||||
</YStack>
|
||||
</ZStack>
|
||||
</View>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { getTokenValue, Spacer, Text, useTheme, XStack, YStack } from 'tamagui'
|
||||
import { useSeekTo } from '../../../hooks/player/callbacks'
|
||||
import {
|
||||
@@ -15,7 +15,11 @@ import { runOnJS } from 'react-native-worklets'
|
||||
import Slider from '@jellify-music/react-native-reanimated-slider'
|
||||
import { triggerHaptic } from '../../../hooks/use-haptic-feedback'
|
||||
|
||||
export default function Scrubber(): React.JSX.Element {
|
||||
interface ScrubberProps {
|
||||
onSeekComplete?: (position: number) => void
|
||||
}
|
||||
|
||||
export default function Scrubber({ onSeekComplete }: ScrubberProps = {}): React.JSX.Element {
|
||||
const seekTo = useSeekTo()
|
||||
const nowPlaying = useCurrentTrack()
|
||||
|
||||
@@ -67,6 +71,11 @@ export default function Scrubber(): React.JSX.Element {
|
||||
},
|
||||
)
|
||||
|
||||
const handleValueChange = async (value: number) => {
|
||||
await seekTo(value)
|
||||
onSeekComplete?.(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<YStack alignItems='stretch' gap={'$3'}>
|
||||
<Slider
|
||||
@@ -74,7 +83,7 @@ export default function Scrubber(): React.JSX.Element {
|
||||
maxValue={duration}
|
||||
backgroundColor={theme.neutral.val}
|
||||
color={theme.primary.val}
|
||||
onValueChange={seekTo}
|
||||
onValueChange={handleValueChange}
|
||||
thumbWidth={getTokenValue('$3')}
|
||||
trackHeight={getTokenValue('$2')}
|
||||
gestureActiveRef={isSeeking}
|
||||
|
||||
@@ -31,7 +31,6 @@ export default function Miniplayer(): React.JSX.Element {
|
||||
const skip = useSkip()
|
||||
const previous = usePrevious()
|
||||
const theme = useTheme()
|
||||
|
||||
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
|
||||
|
||||
const translateX = useSharedValue(0)
|
||||
@@ -73,12 +72,28 @@ export default function Miniplayer(): React.JSX.Element {
|
||||
}
|
||||
})
|
||||
|
||||
const openPlayer = () => navigation.navigate('PlayerRoot', { screen: 'PlayerScreen' })
|
||||
const openPlayer = () => {
|
||||
navigation.navigate('PlayerRoot', { screen: 'PlayerScreen' })
|
||||
}
|
||||
|
||||
const pressStyle = {
|
||||
opacity: 0.6,
|
||||
}
|
||||
|
||||
// Guard: during track transitions nowPlaying can be briefly null
|
||||
if (!nowPlaying?.item) {
|
||||
return (
|
||||
<YStack
|
||||
backgroundColor={theme.background.val}
|
||||
padding={'$2'}
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
<Text> </Text>
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<GestureDetector gesture={gesture}>
|
||||
<Animated.View
|
||||
@@ -99,10 +114,10 @@ export default function Miniplayer(): React.JSX.Element {
|
||||
<Animated.View
|
||||
entering={FadeIn.easing(Easing.in(Easing.ease))}
|
||||
exiting={FadeOut.easing(Easing.out(Easing.ease))}
|
||||
key={`${nowPlaying!.item.AlbumId}-album-image`}
|
||||
key={`${nowPlaying.item.AlbumId}-album-image`}
|
||||
>
|
||||
<ItemImage
|
||||
item={nowPlaying!.item}
|
||||
item={nowPlaying.item}
|
||||
width={'$11'}
|
||||
height={'$11'}
|
||||
imageOptions={{ maxWidth: 120, maxHeight: 120 }}
|
||||
@@ -119,15 +134,15 @@ export default function Miniplayer(): React.JSX.Element {
|
||||
<Animated.View
|
||||
entering={FadeIn.easing(Easing.in(Easing.ease))}
|
||||
exiting={FadeOut.easing(Easing.out(Easing.ease))}
|
||||
key={`${nowPlaying!.item.Id}-mini-player-song-info`}
|
||||
key={`${nowPlaying.item.Id}-mini-player-song-info`}
|
||||
>
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text bold>{nowPlaying?.title ?? 'Nothing Playing'}</Text>
|
||||
<Text bold>{nowPlaying.title ?? 'Nothing Playing'}</Text>
|
||||
</TextTicker>
|
||||
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text height={'$0.5'}>
|
||||
{nowPlaying?.artist ?? 'Unknown Artist'}
|
||||
{nowPlaying.artist ?? 'Unknown Artist'}
|
||||
</Text>
|
||||
</TextTicker>
|
||||
</Animated.View>
|
||||
@@ -163,5 +178,6 @@ function MiniPlayerProgress(): React.JSX.Element {
|
||||
}
|
||||
|
||||
function calculateProgressPercentage(progress: TrackPlayerProgress | undefined): number {
|
||||
return Math.round((progress!.position / progress!.duration) * 100)
|
||||
if (!progress || progress.duration <= 0) return 0
|
||||
return Math.round((progress.position / progress.duration) * 100)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Input from '../Global/helpers/input'
|
||||
import { H5, Text } from '../Global/helpers/text'
|
||||
import ItemRow from '../Global/components/item-row'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { fetchSearchResults } from '../../api/queries/search'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { getToken, H3, Spinner, YStack } from 'tamagui'
|
||||
import Suggestions from './suggestions'
|
||||
import { isEmpty } from 'lodash'
|
||||
@@ -13,7 +10,6 @@ import HorizontalCardList from '../Global/components/horizontal-list'
|
||||
import ItemCard from '../Global/components/item-card'
|
||||
import SearchParamList from '../../screens/Search/types'
|
||||
import { closeAllSwipeableRows } from '../Global/components/swipeable-row-registry'
|
||||
import { getApi, getUser, useJellifyLibrary } from '../../stores'
|
||||
import { FlashList } from '@shopify/flash-list'
|
||||
import navigationRef from '../../../navigation'
|
||||
import { StackActions } from '@react-navigation/native'
|
||||
@@ -22,41 +18,35 @@ import Track from '../Global/components/Track'
|
||||
import { pickRandomItemFromArray } from '../../utils/parsing/random'
|
||||
import { SEARCH_PLACEHOLDERS } from '../../configs/placeholder.config'
|
||||
import { formatArtistName } from '../../utils/formatting/artist-names'
|
||||
import useSearchResults from '../../api/queries/search'
|
||||
|
||||
export default function Search({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<SearchParamList, 'SearchScreen'>
|
||||
}): React.JSX.Element {
|
||||
const api = getApi()
|
||||
const user = getUser()
|
||||
const [library] = useJellifyLibrary()
|
||||
/**
|
||||
* Raw text input value from the user, updates immediately as they type
|
||||
*/
|
||||
const [inputValue, setInputValue] = useState<string | undefined>(undefined)
|
||||
|
||||
/**
|
||||
* Debounced search string that updates 500ms after the user stops typing, used to trigger the search query
|
||||
* which is keyed off of this value for caching.
|
||||
*/
|
||||
const [searchString, setSearchString] = useState<string | undefined>(undefined)
|
||||
|
||||
const {
|
||||
data: items,
|
||||
refetch,
|
||||
isFetching: fetchingResults,
|
||||
} = useQuery({
|
||||
queryKey: [QueryKeys.Search, library?.musicLibraryId, searchString],
|
||||
queryFn: () => fetchSearchResults(api, user, library?.musicLibraryId, searchString),
|
||||
})
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setSearchString(inputValue || undefined)
|
||||
}, 500)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [inputValue])
|
||||
|
||||
const search = () => {
|
||||
let timeout: ReturnType<typeof setTimeout>
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
refetch()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
const { data: items, isFetching: fetchingResults } = useSearchResults(searchString)
|
||||
|
||||
const handleSearchStringUpdate = (value: string | undefined) => {
|
||||
setSearchString(value)
|
||||
search()
|
||||
setInputValue(value || undefined)
|
||||
}
|
||||
|
||||
const handleScrollBeginDrag = () => {
|
||||
@@ -90,7 +80,7 @@ export default function Search({
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
onChangeText={handleSearchStringUpdate}
|
||||
value={searchString}
|
||||
value={inputValue}
|
||||
testID='search-input'
|
||||
clearButtonMode='always'
|
||||
/>
|
||||
|
||||
@@ -20,7 +20,6 @@ const TRACK_SORT_OPTIONS: { value: ItemSortBy; label: string }[] = [
|
||||
const ALBUM_SORT_OPTIONS: { value: ItemSortBy; label: string }[] = [
|
||||
{ value: ItemSortBy.SortName, label: 'Album' },
|
||||
{ value: ItemSortBy.Artist, label: 'Artist' },
|
||||
{ value: ItemSortBy.PlayCount, label: 'Play Count' },
|
||||
{ value: ItemSortBy.DateCreated, label: 'Date Added' },
|
||||
{ value: ItemSortBy.PremiereDate, label: 'Release Date' },
|
||||
]
|
||||
|
||||
@@ -96,6 +96,7 @@ export default function Tracks({
|
||||
queue={queue}
|
||||
sortingByAlbum={sortBy === ItemSortBy.Album}
|
||||
sortingByReleasedDate={sortBy === ItemSortBy.PremiereDate}
|
||||
sortingByPlayCount={sortBy === ItemSortBy.PlayCount}
|
||||
/>
|
||||
) : (
|
||||
<ItemRow navigation={navigation} item={track} />
|
||||
|
||||
@@ -2,7 +2,7 @@ import Config from 'react-native-superconfig'
|
||||
|
||||
const OTA_UPDATE_ENABLED = Config.OTA_UPDATE_ENABLED === 'true'
|
||||
const IS_MAESTRO_BUILD = Config.IS_MAESTRO_BUILD === 'true'
|
||||
const GLITCHTIP_DSN = Config.GLITCHTIP_DSN
|
||||
const GLITCHTIP_DSN = Config.GLITCHTIP_DSN ?? ''
|
||||
|
||||
export { OTA_UPDATE_ENABLED, IS_MAESTRO_BUILD, GLITCHTIP_DSN }
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@ export async function handleShuffle(keepCurrentTrack: boolean = true): Promise<J
|
||||
const isDownloaded = filters.isDownloaded === true
|
||||
const isUnplayed = filters.isUnplayed === true
|
||||
const genreIds = filters.genreIds
|
||||
const yearMin = filters.yearMin
|
||||
const yearMax = filters.yearMax
|
||||
|
||||
let randomTracks: JellifyTrack[] = []
|
||||
|
||||
@@ -87,6 +89,16 @@ export async function handleShuffle(keepCurrentTrack: boolean = true): Promise<J
|
||||
// Filter downloaded tracks
|
||||
let filteredDownloads = downloadedTracks
|
||||
|
||||
// Filter by year range
|
||||
if (yearMin != null || yearMax != null) {
|
||||
const min = yearMin ?? 0
|
||||
const max = yearMax ?? new Date().getFullYear()
|
||||
filteredDownloads = filteredDownloads.filter((download) => {
|
||||
const y = download.item.ProductionYear
|
||||
return y != null && y >= min && y <= max
|
||||
})
|
||||
}
|
||||
|
||||
// Filter by favorites
|
||||
if (isFavorites) {
|
||||
filteredDownloads = filteredDownloads.filter((download) => {
|
||||
@@ -118,6 +130,19 @@ export async function handleShuffle(keepCurrentTrack: boolean = true): Promise<J
|
||||
apiFilters.push(ItemFilter.IsUnplayed)
|
||||
}
|
||||
|
||||
// Build years param for year range filter
|
||||
const yearsParam =
|
||||
yearMin != null || yearMax != null
|
||||
? (() => {
|
||||
const min = yearMin ?? 0
|
||||
const max = yearMax ?? new Date().getFullYear()
|
||||
if (min > max) return undefined
|
||||
const years: string[] = []
|
||||
for (let y = min; y <= max; y++) years.push(String(y))
|
||||
return years.length > 0 ? years : undefined
|
||||
})()
|
||||
: undefined
|
||||
|
||||
// Fetch random tracks from Jellyfin with filters
|
||||
const data = await nitroFetch<{ Items: BaseItemDto[] }>(api, '/Items', {
|
||||
ParentId: library.musicLibraryId,
|
||||
@@ -127,6 +152,7 @@ export async function handleShuffle(keepCurrentTrack: boolean = true): Promise<J
|
||||
SortBy: [ItemSortBy.Random],
|
||||
Filters: apiFilters.length > 0 ? apiFilters : undefined,
|
||||
GenreIds: genreIds && genreIds.length > 0 ? genreIds : undefined,
|
||||
Years: yearsParam,
|
||||
Limit: ApiLimits.LibraryShuffle,
|
||||
Fields: [
|
||||
ItemFields.MediaSources,
|
||||
|
||||
@@ -94,6 +94,44 @@ export default function GenreSelectionScreen({
|
||||
})
|
||||
}, [triggerHaptic])
|
||||
|
||||
const allLoadedGenreIds = useMemo(
|
||||
() => genres?.map((g) => g.Id!).filter(Boolean) ?? [],
|
||||
[genres],
|
||||
)
|
||||
const allSelected =
|
||||
allLoadedGenreIds.length > 0 && selectedGenreIds.length === allLoadedGenreIds.length
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
triggerHaptic('impactLight')
|
||||
setSelectedGenreIds([...allLoadedGenreIds])
|
||||
}, [allLoadedGenreIds, triggerHaptic])
|
||||
|
||||
const renderListHeader = useCallback(
|
||||
() => (
|
||||
<XStack
|
||||
alignItems='center'
|
||||
padding='$3'
|
||||
gap='$3'
|
||||
pressStyle={{ opacity: 0.6 }}
|
||||
animation='quick'
|
||||
onPress={handleSelectAll}
|
||||
backgroundColor='$backgroundHover'
|
||||
>
|
||||
<YStack flex={1}>
|
||||
<Text bold>Select all</Text>
|
||||
{genres != null && (
|
||||
<Text color='$borderColor'>{`${allLoadedGenreIds.length} genres`}</Text>
|
||||
)}
|
||||
</YStack>
|
||||
<Icon
|
||||
name={allSelected ? 'check-circle-outline' : 'circle-outline'}
|
||||
color={allSelected ? '$primary' : '$borderColor'}
|
||||
/>
|
||||
</XStack>
|
||||
),
|
||||
[handleSelectAll, allSelected, allLoadedGenreIds.length, genres],
|
||||
)
|
||||
|
||||
const renderItem: ListRenderItem<BaseItemDto | string> = ({ item }) => {
|
||||
if (typeof item === 'string') {
|
||||
// Section header
|
||||
@@ -181,6 +219,7 @@ export default function GenreSelectionScreen({
|
||||
data={flattenedGenres}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
ListHeaderComponent={renderListHeader}
|
||||
// @ts-expect-error - estimatedItemSize is required by FlashList but types are incorrect
|
||||
estimatedItemSize={70}
|
||||
onEndReached={() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { View } from 'react-native'
|
||||
import PlayerScreen from '../../components/Player'
|
||||
import Queue from '../../components/Queue'
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { YStack, XStack, Button, Spinner } from 'tamagui'
|
||||
import { Modal, ScrollView, Pressable } from 'react-native'
|
||||
import { Text } from '../../components/Global/helpers/text'
|
||||
import Icon from '../../components/Global/components/icon'
|
||||
import { triggerHaptic } from '../../hooks/use-haptic-feedback'
|
||||
import { YearSelectionProps } from '../types'
|
||||
import useLibraryStore from '../../stores/library'
|
||||
import { useLibraryYears } from '../../api/queries/years'
|
||||
|
||||
const ANY = 'any'
|
||||
type Picking = 'min' | 'max' | null
|
||||
|
||||
export default function YearSelectionScreen({
|
||||
navigation,
|
||||
route,
|
||||
}: YearSelectionProps): React.JSX.Element {
|
||||
const tab = route.params?.tab ?? 'Tracks'
|
||||
const { years: availableYears, isPending, isError } = useLibraryYears()
|
||||
const storeFilters = useLibraryStore.getState().filters[tab === 'Albums' ? 'albums' : 'tracks']
|
||||
const [minYear, setMinYear] = useState<number | typeof ANY>(storeFilters.yearMin ?? ANY)
|
||||
const [maxYear, setMaxYear] = useState<number | typeof ANY>(storeFilters.yearMax ?? ANY)
|
||||
const [picking, setPicking] = useState<Picking>(null)
|
||||
|
||||
// Min year options: if maxYear is set, only years <= maxYear
|
||||
const minYearOptions = useMemo(() => {
|
||||
if (availableYears.length === 0) return []
|
||||
const max = typeof maxYear === 'number' ? maxYear : Math.max(...availableYears)
|
||||
return availableYears.filter((y) => y <= max)
|
||||
}, [availableYears, maxYear])
|
||||
|
||||
// Max year options: if minYear is set, only years >= minYear
|
||||
const maxYearOptions = useMemo(() => {
|
||||
if (availableYears.length === 0) return []
|
||||
const min = typeof minYear === 'number' ? minYear : Math.min(...availableYears)
|
||||
return availableYears.filter((y) => y >= min)
|
||||
}, [availableYears, minYear])
|
||||
|
||||
const handleOpenMin = useCallback(() => {
|
||||
triggerHaptic('impactLight')
|
||||
setPicking('min')
|
||||
}, [])
|
||||
|
||||
const handleOpenMax = useCallback(() => {
|
||||
triggerHaptic('impactLight')
|
||||
setPicking('max')
|
||||
}, [])
|
||||
|
||||
const handleSelectMin = useCallback(
|
||||
(year: number | typeof ANY) => {
|
||||
triggerHaptic('impactLight')
|
||||
setMinYear(year)
|
||||
setPicking(null)
|
||||
if (year !== ANY && typeof maxYear === 'number' && year > maxYear) {
|
||||
setMaxYear(year)
|
||||
}
|
||||
},
|
||||
[maxYear],
|
||||
)
|
||||
|
||||
const handleSelectMax = useCallback(
|
||||
(year: number | typeof ANY) => {
|
||||
triggerHaptic('impactLight')
|
||||
setMaxYear(year)
|
||||
setPicking(null)
|
||||
if (year !== ANY && typeof minYear === 'number' && year < minYear) {
|
||||
setMinYear(year)
|
||||
}
|
||||
},
|
||||
[minYear],
|
||||
)
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
triggerHaptic('impactLight')
|
||||
const payload = {
|
||||
yearMin: minYear === ANY ? undefined : minYear,
|
||||
yearMax: maxYear === ANY ? undefined : maxYear,
|
||||
}
|
||||
if (tab === 'Albums') {
|
||||
useLibraryStore.getState().setAlbumsFilters(payload)
|
||||
} else {
|
||||
useLibraryStore.getState().setTracksFilters(payload)
|
||||
}
|
||||
navigation.goBack()
|
||||
}, [minYear, maxYear, navigation, tab])
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
triggerHaptic('impactLight')
|
||||
setMinYear(ANY)
|
||||
setMaxYear(ANY)
|
||||
const payload = { yearMin: undefined, yearMax: undefined }
|
||||
if (tab === 'Albums') {
|
||||
useLibraryStore.getState().setAlbumsFilters(payload)
|
||||
} else {
|
||||
useLibraryStore.getState().setTracksFilters(payload)
|
||||
}
|
||||
}, [tab])
|
||||
|
||||
const hasSelection = minYear !== ANY || maxYear !== ANY
|
||||
const rangeLabel =
|
||||
minYear !== ANY || maxYear !== ANY
|
||||
? `${minYear === ANY ? '…' : minYear} – ${maxYear === ANY ? '…' : maxYear}`
|
||||
: null
|
||||
|
||||
const minLabel = minYear === ANY ? 'Any' : String(minYear)
|
||||
const maxLabel = maxYear === ANY ? 'Any' : String(maxYear)
|
||||
|
||||
if (isPending && availableYears.length === 0) {
|
||||
return (
|
||||
<YStack flex={1} alignItems='center' justifyContent='center'>
|
||||
<Spinner size='large' />
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<YStack flex={1} alignItems='center' justifyContent='center' padding='$4'>
|
||||
<Text color='$borderColor'>Could not load years</Text>
|
||||
<Button marginTop='$4' onPress={() => navigation.goBack()}>
|
||||
Go back
|
||||
</Button>
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
|
||||
const pickerOptions = picking === 'min' ? minYearOptions : maxYearOptions
|
||||
const onSelectOption = picking === 'min' ? handleSelectMin : handleSelectMax
|
||||
const currentValue = picking === 'min' ? minYear : maxYear
|
||||
|
||||
return (
|
||||
<YStack flex={1} backgroundColor='$background'>
|
||||
<XStack
|
||||
justifyContent='space-between'
|
||||
alignItems='center'
|
||||
padding='$4'
|
||||
borderBottomWidth={1}
|
||||
borderBottomColor='$borderColor'
|
||||
>
|
||||
<Button variant='outlined' size='$3' onPress={() => navigation.goBack()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Text bold fontSize='$6'>
|
||||
Year range
|
||||
</Text>
|
||||
<Button variant='outlined' size='$3' onPress={handleClear} disabled={!hasSelection}>
|
||||
Clear
|
||||
</Button>
|
||||
</XStack>
|
||||
|
||||
<YStack flex={1} padding='$4' gap='$2'>
|
||||
<Text bold fontSize='$5' marginBottom='$2'>
|
||||
Min year
|
||||
</Text>
|
||||
<Pressable onPress={handleOpenMin}>
|
||||
<XStack
|
||||
alignItems='center'
|
||||
justifyContent='space-between'
|
||||
padding='$3'
|
||||
backgroundColor='$backgroundHover'
|
||||
borderRadius='$2'
|
||||
borderWidth={1}
|
||||
borderColor='$borderColor'
|
||||
>
|
||||
<Text>{minLabel}</Text>
|
||||
<Icon name='chevron-down' color='$borderColor' />
|
||||
</XStack>
|
||||
</Pressable>
|
||||
|
||||
<Text bold fontSize='$5' marginBottom='$2' marginTop='$3'>
|
||||
Max year
|
||||
</Text>
|
||||
<Pressable onPress={handleOpenMax}>
|
||||
<XStack
|
||||
alignItems='center'
|
||||
justifyContent='space-between'
|
||||
padding='$3'
|
||||
backgroundColor='$backgroundHover'
|
||||
borderRadius='$2'
|
||||
borderWidth={1}
|
||||
borderColor='$borderColor'
|
||||
>
|
||||
<Text>{maxLabel}</Text>
|
||||
<Icon name='chevron-down' color='$borderColor' />
|
||||
</XStack>
|
||||
</Pressable>
|
||||
</YStack>
|
||||
|
||||
{/* Dropdown picker modal */}
|
||||
<Modal
|
||||
visible={picking !== null}
|
||||
transparent
|
||||
animationType='fade'
|
||||
onRequestClose={() => setPicking(null)}
|
||||
>
|
||||
<Pressable
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
}}
|
||||
onPress={() => setPicking(null)}
|
||||
>
|
||||
<Pressable style={{ maxHeight: '100%' }} onPress={(e) => e.stopPropagation()}>
|
||||
<YStack
|
||||
backgroundColor='$background'
|
||||
borderTopLeftRadius='$4'
|
||||
borderTopRightRadius='$4'
|
||||
padding='$4'
|
||||
maxHeight='100%'
|
||||
>
|
||||
<Text bold fontSize='$5' marginBottom='$3'>
|
||||
{picking === 'min' ? 'Select min year' : 'Select max year'}
|
||||
</Text>
|
||||
<ScrollView
|
||||
style={{ maxHeight: 330 }}
|
||||
showsVerticalScrollIndicator
|
||||
keyboardShouldPersistTaps='handled'
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => onSelectOption(ANY)}
|
||||
style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })}
|
||||
>
|
||||
<XStack
|
||||
padding='$3'
|
||||
alignItems='center'
|
||||
backgroundColor={
|
||||
currentValue === ANY
|
||||
? '$backgroundHover'
|
||||
: 'transparent'
|
||||
}
|
||||
borderRadius='$2'
|
||||
>
|
||||
<Text
|
||||
fontWeight={currentValue === ANY ? 'bold' : undefined}
|
||||
>
|
||||
Any
|
||||
</Text>
|
||||
</XStack>
|
||||
</Pressable>
|
||||
{pickerOptions.map((y) => (
|
||||
<Pressable
|
||||
key={y}
|
||||
onPress={() => onSelectOption(y)}
|
||||
style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })}
|
||||
>
|
||||
<XStack
|
||||
padding='$3'
|
||||
alignItems='center'
|
||||
backgroundColor={
|
||||
currentValue === y
|
||||
? '$backgroundHover'
|
||||
: 'transparent'
|
||||
}
|
||||
borderRadius='$2'
|
||||
>
|
||||
<Text
|
||||
fontWeight={currentValue === y ? 'bold' : undefined}
|
||||
>
|
||||
{String(y)}
|
||||
</Text>
|
||||
</XStack>
|
||||
</Pressable>
|
||||
))}
|
||||
</ScrollView>
|
||||
</YStack>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</Modal>
|
||||
|
||||
{hasSelection && (
|
||||
<XStack
|
||||
justifyContent='space-evenly'
|
||||
alignItems='center'
|
||||
padding='$4'
|
||||
borderTopWidth={1}
|
||||
borderTopColor='$borderColor'
|
||||
>
|
||||
<Text fontSize='$3' bold color='$primary'>
|
||||
{rangeLabel ?? ''}
|
||||
</Text>
|
||||
<Button
|
||||
variant='outlined'
|
||||
borderColor='$primary'
|
||||
color='$primary'
|
||||
size='$3'
|
||||
onPress={handleSave}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</XStack>
|
||||
)}
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { formatArtistNames } from '../utils/formatting/artist-names'
|
||||
import FiltersSheet from './Filters'
|
||||
import SortOptionsSheet from './SortOptions'
|
||||
import GenreSelectionScreen from './GenreSelection'
|
||||
import YearSelectionScreen from './YearSelection'
|
||||
|
||||
const RootStack = createNativeStackNavigator<RootStackParamList>()
|
||||
|
||||
@@ -132,6 +133,16 @@ export default function Root(): React.JSX.Element {
|
||||
sheetGrabberVisible: true,
|
||||
}}
|
||||
/>
|
||||
|
||||
<RootStack.Screen
|
||||
name='YearSelection'
|
||||
component={YearSelectionScreen}
|
||||
options={{
|
||||
headerTitle: 'Year range',
|
||||
presentation: 'modal',
|
||||
sheetGrabberVisible: true,
|
||||
}}
|
||||
/>
|
||||
</RootStack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -74,6 +74,7 @@ export type RootStackParamList = {
|
||||
}
|
||||
|
||||
GenreSelection: undefined
|
||||
YearSelection: { tab?: 'Tracks' | 'Albums' }
|
||||
|
||||
AudioSpecs: {
|
||||
item: BaseItemDto
|
||||
@@ -99,6 +100,7 @@ export type DeletePlaylistProps = NativeStackScreenProps<RootStackParamList, 'De
|
||||
export type FiltersProps = NativeStackScreenProps<RootStackParamList, 'Filters'>
|
||||
export type SortOptionsProps = NativeStackScreenProps<RootStackParamList, 'SortOptions'>
|
||||
export type GenreSelectionProps = NativeStackScreenProps<RootStackParamList, 'GenreSelection'>
|
||||
export type YearSelectionProps = NativeStackScreenProps<RootStackParamList, 'YearSelection'>
|
||||
|
||||
export type GenresProps = {
|
||||
genres: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
|
||||
@@ -10,6 +10,8 @@ type TabFilterState = {
|
||||
isDownloaded?: boolean // Only for Tracks tab
|
||||
isUnplayed?: boolean // Only for Tracks tab
|
||||
genreIds?: string[] // Only for Tracks tab
|
||||
yearMin?: number // Tracks and Albums
|
||||
yearMax?: number // Tracks and Albums
|
||||
}
|
||||
|
||||
type SortState = Record<LibraryTab, ItemSortBy>
|
||||
@@ -93,9 +95,13 @@ const useLibraryStore = create<LibraryStore>()(
|
||||
isDownloaded: false,
|
||||
isUnplayed: undefined,
|
||||
genreIds: undefined,
|
||||
yearMin: undefined,
|
||||
yearMax: undefined,
|
||||
},
|
||||
albums: {
|
||||
isFavorites: undefined,
|
||||
yearMin: undefined,
|
||||
yearMax: undefined,
|
||||
},
|
||||
artists: {
|
||||
isFavorites: undefined,
|
||||
|
||||
@@ -16,6 +16,7 @@ export type BaseItemDtoSlimified = Pick<
|
||||
| 'RunTimeTicks'
|
||||
| 'OfficialRating'
|
||||
| 'CustomRating'
|
||||
| 'ProductionYear'
|
||||
>
|
||||
|
||||
interface JellifyTrack extends Track {
|
||||
|
||||
Vendored
-10
@@ -1,10 +0,0 @@
|
||||
declare module 'react-native-superconfig' {
|
||||
export interface NativeConfig {
|
||||
OTA_UPDATE_ENABLED?: string
|
||||
IS_MAESTRO_BUILD?: string
|
||||
GLITCHTIP_DSN?: string
|
||||
}
|
||||
|
||||
export const Config: NativeConfig
|
||||
export default Config
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
export function formatArtistName(artistName: string | null | undefined): string {
|
||||
if (!artistName) return 'Unknown Artist'
|
||||
return artistName
|
||||
export function formatArtistName(
|
||||
artistName: string | null | undefined,
|
||||
releaseDate?: string | null | undefined,
|
||||
): string {
|
||||
const unknownArtist = 'Unknown Artist'
|
||||
if (!artistName) return releaseDate ? `${releaseDate} • ${unknownArtist}` : unknownArtist
|
||||
return releaseDate ? `${releaseDate} • ${artistName}` : artistName
|
||||
}
|
||||
|
||||
export function formatArtistNames(artistNames: string[] | null | undefined): string {
|
||||
if (!artistNames || artistNames.length === 0) return 'Unknown Artist'
|
||||
return artistNames.map(formatArtistName).join(' • ')
|
||||
return artistNames.map((artistName) => formatArtistName(artistName)).join(' • ')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export default function buildYearsParam(yearMin?: number, yearMax?: number): string[] | undefined {
|
||||
if (yearMin == null && yearMax == null) return undefined
|
||||
const min = yearMin ?? 0
|
||||
const max = yearMax ?? new Date().getFullYear()
|
||||
if (min > max) return undefined
|
||||
const years: string[] = []
|
||||
for (let y = min; y <= max; y++) years.push(String(y))
|
||||
return years.length > 0 ? years : undefined
|
||||
}
|
||||
Reference in New Issue
Block a user