Merge branch 'tapframe:main' into main

This commit is contained in:
qarqun 2025-10-19 15:42:24 +08:00 committed by GitHub
commit 6e975ffe26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
97 changed files with 395 additions and 1677 deletions

1
.gitignore vendored
View file

@ -72,3 +72,4 @@ SDK54_UPGRADE_SUMMARY.md
SDK54_UPGRADE_SUMMARY.md SDK54_UPGRADE_SUMMARY.md
build-and-publish-app-releases.sh build-and-publish-app-releases.sh
bottomnav.md bottomnav.md
/TrailerServices

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="ic_launcher_background">#151515</color> <color name="ic_launcher_background">#d1d1d2</color>
</resources> </resources>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 718 B

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 KiB

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 111 KiB

@ -1 +0,0 @@
Subproject commit 3a70b36f873307cd83fb3178bb891f73cf73aa87

View file

@ -460,8 +460,8 @@
"-lc++", "-lc++",
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.nuviohub.app; PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
PRODUCT_NAME = Nuvio; PRODUCT_NAME = "Nuvio";
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -492,8 +492,8 @@
"-lc++", "-lc++",
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.nuviohub.app; PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
PRODUCT_NAME = Nuvio; PRODUCT_NAME = "Nuvio";
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 122 KiB

View file

@ -1,98 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Nuvio</string> <string>Nuvio</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string> <string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.2.5</string> <string>1.2.5</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
<array> <array>
<dict> <dict>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>nuvio</string> <string>nuvio</string>
<string>com.nuvio.app</string> <string>com.nuvio.app</string>
</array> </array>
</dict> </dict>
<dict> <dict>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>exp+nuvio</string> <string>exp+nuvio</string>
</array> </array>
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>20</string> <string>20</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>12.0</string> <string>12.0</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>LSSupportsOpeningDocumentsInPlace</key> <key>LSSupportsOpeningDocumentsInPlace</key>
<true/> <true/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<true/> <true/>
</dict> </dict>
<key>NSBonjourServices</key> <key>NSBonjourServices</key>
<array> <array>
<string>_http._tcp</string> <string>_http._tcp</string>
</array> </array>
<key>RCTNewArchEnabled</key> <key>NSLocalNetworkUsageDescription</key>
<true/> <string>Allow $(PRODUCT_NAME) to access your local network</string>
<key>RCTRootViewBackgroundColor</key> <key>NSMicrophoneUsageDescription</key>
<integer>4278322180</integer> <string>This app does not require microphone access.</string>
<key>UIBackgroundModes</key> <key>RCTNewArchEnabled</key>
<array> <true/>
<string>audio</string> <key>RCTRootViewBackgroundColor</key>
<string>fetch</string> <integer>4278322180</integer>
</array> <key>UIBackgroundModes</key>
<key>UIFileSharingEnabled</key> <array>
<true/> <string>audio</string>
<key>UILaunchStoryboardName</key> </array>
<string>SplashScreen</string> <key>UIFileSharingEnabled</key>
<key>UIRequiredDeviceCapabilities</key> <true/>
<array> <key>UILaunchStoryboardName</key>
<string>arm64</string> <string>SplashScreen</string>
</array> <key>UIRequiredDeviceCapabilities</key>
<key>UIRequiresFullScreen</key> <array>
<false/> <string>arm64</string>
<key>UIStatusBarStyle</key> </array>
<string>UIStatusBarStyleDefault</string> <key>UIRequiresFullScreen</key>
<key>UISupportedInterfaceOrientations</key> <false/>
<array> <key>UIStatusBarStyle</key>
<string>UIInterfaceOrientationPortrait</string> <string>UIStatusBarStyleDefault</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <key>UISupportedInterfaceOrientations</key>
<string>UIInterfaceOrientationLandscapeLeft</string> <array>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationPortrait</string>
</array> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<key>UISupportedInterfaceOrientations~ipad</key> <string>UIInterfaceOrientationLandscapeLeft</string>
<array> <string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string> </array>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <key>UISupportedInterfaceOrientations~ipad</key>
<string>UIInterfaceOrientationLandscapeLeft</string> <array>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationPortrait</string>
</array> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<key>UIUserInterfaceStyle</key> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>Dark</string> <string>UIInterfaceOrientationLandscapeRight</string>
<key>UIViewControllerBasedStatusBarAppearance</key> </array>
<false/> <key>UIUserInterfaceStyle</key>
</dict> <string>Dark</string>
</plist> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict/> <dict>
</plist> <key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array/>
</dict>
</plist>

146
package-lock.json generated
View file

@ -17,6 +17,7 @@
"@expo/metro-runtime": "~6.1.2", "@expo/metro-runtime": "~6.1.2",
"@expo/vector-icons": "^15.0.2", "@expo/vector-icons": "^15.0.2",
"@gorhom/bottom-sheet": "^5.2.6", "@gorhom/bottom-sheet": "^5.2.6",
"@legendapp/list": "^2.0.13",
"@lottiefiles/dotlottie-react": "^0.6.5", "@lottiefiles/dotlottie-react": "^0.6.5",
"@react-native-async-storage/async-storage": "2.2.0", "@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/blur": "^4.4.1", "@react-native-community/blur": "^4.4.1",
@ -29,7 +30,6 @@
"@react-navigation/stack": "^7.2.10", "@react-navigation/stack": "^7.2.10",
"@sentry/react-native": "~7.3.0", "@sentry/react-native": "~7.3.0",
"@shopify/flash-list": "^2.1.0", "@shopify/flash-list": "^2.1.0",
"@supabase/supabase-js": "^2.54.0",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"@types/react-native-video": "^5.0.20", "@types/react-native-video": "^5.0.20",
"axios": "^1.12.2", "axios": "^1.12.2",
@ -67,6 +67,7 @@
"posthog-react-native": "^4.4.0", "posthog-react-native": "^4.4.0",
"react": "19.1.0", "react": "19.1.0",
"react-native": "0.81.4", "react-native": "0.81.4",
"react-native-boost": "^0.6.2",
"react-native-bottom-tabs": "^0.12.2", "react-native-bottom-tabs": "^0.12.2",
"react-native-gesture-handler": "~2.28.0", "react-native-gesture-handler": "~2.28.0",
"react-native-get-random-values": "^1.11.0", "react-native-get-random-values": "^1.11.0",
@ -80,7 +81,7 @@
"react-native-svg": "15.12.1", "react-native-svg": "15.12.1",
"react-native-url-polyfill": "^2.0.0", "react-native-url-polyfill": "^2.0.0",
"react-native-vector-icons": "^10.3.0", "react-native-vector-icons": "^10.3.0",
"react-native-video": "^6.12.0", "react-native-video": "^6.17.0",
"react-native-web": "^0.21.0", "react-native-web": "^0.21.0",
"react-native-wheel-color-picker": "^1.3.1", "react-native-wheel-color-picker": "^1.3.1",
"react-native-worklets": "^0.6.1", "react-native-worklets": "^0.6.1",
@ -2347,6 +2348,27 @@
"integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==", "integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -3636,80 +3658,6 @@
"@sinonjs/commons": "^3.0.0" "@sinonjs/commons": "^3.0.0"
} }
}, },
"node_modules/@supabase/auth-js": {
"version": "2.75.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.75.0.tgz",
"integrity": "sha512-J8TkeqCOMCV4KwGKVoxmEBuDdHRwoInML2vJilthOo7awVCro2SM+tOcpljORwuBQ1vHUtV62Leit+5wlxrNtw==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "2.6.15"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.75.0",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.75.0.tgz",
"integrity": "sha512-18yk07Moj/xtQ28zkqswxDavXC3vbOwt1hDuYM3/7xPnwwpKnsmPyZ7bQ5th4uqiJzQ135t74La9tuaxBR6e7w==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "2.6.15"
}
},
"node_modules/@supabase/node-fetch": {
"version": "2.6.15",
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "2.75.0",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.75.0.tgz",
"integrity": "sha512-YfBz4W/z7eYCFyuvHhfjOTTzRrQIvsMG2bVwJAKEVVUqGdzqfvyidXssLBG0Fqlql1zJFgtsPpK1n4meHrI7tg==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "2.6.15"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.75.0",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.75.0.tgz",
"integrity": "sha512-B4Xxsf2NHd5cEnM6MGswOSPSsZKljkYXpvzKKmNxoUmNQOfB7D8HOa6NwHcUBSlxcjV+vIrYKcYXtavGJqeGrw==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "2.6.15",
"@types/phoenix": "^1.6.6",
"@types/ws": "^8.18.1",
"ws": "^8.18.2"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.75.0",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.75.0.tgz",
"integrity": "sha512-wpJMYdfFDckDiHQaTpK+Ib14N/O2o0AAWWhguKvmmMurB6Unx17GGmYp5rrrqCTf8S1qq4IfIxTXxS4hzrUySg==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "2.6.15"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.75.0",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.75.0.tgz",
"integrity": "sha512-8UN/vATSgS2JFuJlMVr51L3eUDz+j1m7Ww63wlvHLKULzCDaVWYzvacCjBTLW/lX/vedI2LBI4Vg+01G9ufsJQ==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.75.0",
"@supabase/functions-js": "2.75.0",
"@supabase/node-fetch": "2.6.15",
"@supabase/postgrest-js": "2.75.0",
"@supabase/realtime-js": "2.75.0",
"@supabase/storage-js": "2.75.0"
}
},
"node_modules/@svgr/babel-plugin-add-jsx-attribute": { "node_modules/@svgr/babel-plugin-add-jsx-attribute": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
@ -4234,12 +4182,6 @@
"undici-types": "~7.14.0" "undici-types": "~7.14.0"
} }
}, },
"node_modules/@types/phoenix": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
"license": "MIT"
},
"node_modules/@types/prop-types": { "node_modules/@types/prop-types": {
"version": "15.7.15", "version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
@ -4283,15 +4225,6 @@
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/yargs": { "node_modules/@types/yargs": {
"version": "17.0.33", "version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
@ -10759,6 +10692,37 @@
} }
} }
}, },
"node_modules/react-native-boost": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/react-native-boost/-/react-native-boost-0.6.2.tgz",
"integrity": "sha512-6w9PdGvFzyI1dyN516+mLfFF5vETPsjoc26rUFlzWav7PNbC7WV0KyfTBr0q/cDjZkWLMleWQZkGTqSQ1H4PHg==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.0",
"@babel/helper-module-imports": "^7.25.0",
"@babel/helper-plugin-utils": "^7.25.0",
"minimatch": "^10.0.1"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-boost/node_modules/minimatch": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"license": "ISC",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/react-native-bottom-tabs": { "node_modules/react-native-bottom-tabs": {
"version": "0.12.2", "version": "0.12.2",
"resolved": "https://registry.npmjs.org/react-native-bottom-tabs/-/react-native-bottom-tabs-0.12.2.tgz", "resolved": "https://registry.npmjs.org/react-native-bottom-tabs/-/react-native-bottom-tabs-0.12.2.tgz",

View file

@ -30,7 +30,6 @@
"@react-navigation/stack": "^7.2.10", "@react-navigation/stack": "^7.2.10",
"@sentry/react-native": "~7.3.0", "@sentry/react-native": "~7.3.0",
"@shopify/flash-list": "^2.1.0", "@shopify/flash-list": "^2.1.0",
"@supabase/supabase-js": "^2.54.0",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"@types/react-native-video": "^5.0.20", "@types/react-native-video": "^5.0.20",
"axios": "^1.12.2", "axios": "^1.12.2",

View file

@ -7,7 +7,7 @@ import { LinearGradient } from 'expo-linear-gradient';
import { CatalogContent, StreamingContent } from '../../services/catalogService'; import { CatalogContent, StreamingContent } from '../../services/catalogService';
import { useTheme } from '../../contexts/ThemeContext'; import { useTheme } from '../../contexts/ThemeContext';
import ContentItem from './ContentItem'; import ContentItem from './ContentItem';
import Animated, { FadeIn } from 'react-native-reanimated'; import Animated, { FadeIn, Layout } from 'react-native-reanimated';
import { RootStackParamList } from '../../navigation/AppNavigator'; import { RootStackParamList } from '../../navigation/AppNavigator';
interface CatalogSectionProps { interface CatalogSectionProps {
@ -77,7 +77,10 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => {
const keyExtractor = useCallback((item: StreamingContent) => `${item.id}-${item.type}`, []); const keyExtractor = useCallback((item: StreamingContent) => `${item.id}-${item.type}`, []);
return ( return (
<Animated.View style={styles.catalogContainer} entering={FadeIn.duration(350)}> <Animated.View
style={styles.catalogContainer}
entering={FadeIn.duration(400)}
>
<View style={styles.catalogHeader}> <View style={styles.catalogHeader}>
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<Text style={[styles.catalogTitle, { color: currentTheme.colors.text }]} numberOfLines={1}>{catalog.name}</Text> <Text style={[styles.catalogTitle, { color: currentTheme.colors.text }]} numberOfLines={1}>{catalog.name}</Text>

View file

@ -1,7 +1,7 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'; import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Toast } from 'toastify-react-native'; import { Toast } from 'toastify-react-native';
import { DeviceEventEmitter } from 'react-native'; import { DeviceEventEmitter } from 'react-native';
import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions, Platform, Text, Animated, Share } from 'react-native'; import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions, Platform, Text, Share } from 'react-native';
import FastImage from '@d11/react-native-fast-image'; import FastImage from '@d11/react-native-fast-image';
import { MaterialIcons, Feather } from '@expo/vector-icons'; import { MaterialIcons, Feather } from '@expo/vector-icons';
import { useTheme } from '../../contexts/ThemeContext'; import { useTheme } from '../../contexts/ThemeContext';
@ -11,6 +11,7 @@ import { DropUpMenu } from './DropUpMenu';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { storageService } from '../../services/storageService'; import { storageService } from '../../services/storageService';
import { TraktService } from '../../services/traktService'; import { TraktService } from '../../services/traktService';
import Animated, { FadeIn } from 'react-native-reanimated';
interface ContentItemProps { interface ContentItemProps {
item: StreamingContent; item: StreamingContent;
@ -96,7 +97,6 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { settings, isLoaded } = useSettings(); const { settings, isLoaded } = useSettings();
const posterRadius = typeof settings.posterBorderRadius === 'number' ? settings.posterBorderRadius : 12; const posterRadius = typeof settings.posterBorderRadius === 'number' ? settings.posterBorderRadius : 12;
const fadeInOpacity = React.useRef(new Animated.Value(1)).current;
// Memoize poster width calculation to avoid recalculating on every render // Memoize poster width calculation to avoid recalculating on every render
const posterWidth = React.useMemo(() => { const posterWidth = React.useMemo(() => {
switch (settings.posterSize) { switch (settings.posterSize) {
@ -232,7 +232,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
return ( return (
<> <>
<Animated.View style={[styles.itemContainer, { width: posterWidth, opacity: fadeInOpacity }]}> <Animated.View style={[styles.itemContainer, { width: posterWidth }]} entering={FadeIn.duration(300)}>
<TouchableOpacity <TouchableOpacity
style={[styles.contentItem, { width: posterWidth, borderRadius: posterRadius }]} style={[styles.contentItem, { width: posterWidth, borderRadius: posterRadius }]}
activeOpacity={0.7} activeOpacity={0.7}

View file

@ -10,7 +10,7 @@ import {
ActivityIndicator ActivityIndicator
} from 'react-native'; } from 'react-native';
import { FlashList } from '@shopify/flash-list'; import { FlashList } from '@shopify/flash-list';
import Animated, { FadeIn } from 'react-native-reanimated'; import Animated, { FadeIn, Layout } from 'react-native-reanimated';
import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native';
import { RootStackParamList } from '../../navigation/AppNavigator'; import { RootStackParamList } from '../../navigation/AppNavigator';
@ -726,7 +726,10 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
} }
return ( return (
<Animated.View style={styles.container} entering={FadeIn.duration(350)}> <Animated.View
style={styles.container}
entering={FadeIn.duration(350)}
>
<View style={styles.header}> <View style={styles.header}>
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<Text style={[styles.title, { color: currentTheme.colors.text }]}>Continue Watching</Text> <Text style={[styles.title, { color: currentTheme.colors.text }]}>Continue Watching</Text>

View file

@ -532,21 +532,20 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
{/* Static genres positioned absolutely over the card */} {/* Static genres positioned absolutely over the card */}
{item.genres && ( {item.genres && (
<View style={styles.genresOverlay as ViewStyle} pointerEvents="none"> <View style={styles.genresOverlay as ViewStyle} pointerEvents="none">
<Animated.Text <Animated.View entering={FadeIn.duration(400).delay(100)}>
entering={FadeIn.duration(400).delay(100)} <Animated.Text
style={[styles.genres as TextStyle, { color: colors.mediumEmphasis, textAlign: 'center' }, genresAnimatedStyle]} style={[styles.genres as TextStyle, { color: colors.mediumEmphasis, textAlign: 'center' }, genresAnimatedStyle]}
numberOfLines={1} numberOfLines={1}
> >
{item.genres.slice(0, 3).join(' • ')} {item.genres.slice(0, 3).join(' • ')}
</Animated.Text> </Animated.Text>
</Animated.View>
</View> </View>
)} )}
{/* Static action buttons positioned absolutely over the card */} {/* Static action buttons positioned absolutely over the card */}
<View style={styles.actionsOverlay as ViewStyle} pointerEvents="box-none"> <View style={styles.actionsOverlay as ViewStyle} pointerEvents="box-none">
<Animated.View <Animated.View entering={FadeIn.duration(500).delay(200)}>
entering={FadeIn.duration(500).delay(200)} <Animated.View style={[styles.actions as ViewStyle, actionsAnimatedStyle]}>
style={[styles.actions as ViewStyle, actionsAnimatedStyle]}
>
<TouchableOpacity <TouchableOpacity
style={[styles.playButton as ViewStyle, { backgroundColor: colors.white }]} style={[styles.playButton as ViewStyle, { backgroundColor: colors.white }]}
onPress={onPressPlay} onPress={onPressPlay}
@ -563,6 +562,7 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
<MaterialIcons name="info-outline" size={18} color={colors.white} /> <MaterialIcons name="info-outline" size={18} color={colors.white} />
<Text style={[styles.secondaryText as TextStyle, { color: colors.white }]}>Info</Text> <Text style={[styles.secondaryText as TextStyle, { color: colors.white }]}>Info</Text>
</TouchableOpacity> </TouchableOpacity>
</Animated.View>
</Animated.View> </Animated.View>
</View> </View>
{/* Static logo positioned absolutely over the card */} {/* Static logo positioned absolutely over the card */}

View file

@ -18,7 +18,7 @@ import { useTraktContext } from '../../contexts/TraktContext';
import { useLibrary } from '../../hooks/useLibrary'; import { useLibrary } from '../../hooks/useLibrary';
import { RootStackParamList } from '../../navigation/AppNavigator'; import { RootStackParamList } from '../../navigation/AppNavigator';
import { parseISO, isThisWeek, format, isAfter, isBefore } from 'date-fns'; import { parseISO, isThisWeek, format, isAfter, isBefore } from 'date-fns';
import Animated, { FadeIn } from 'react-native-reanimated'; import Animated, { FadeIn, Layout } from 'react-native-reanimated';
import { useCalendarData } from '../../hooks/useCalendarData'; import { useCalendarData } from '../../hooks/useCalendarData';
import { memoryManager } from '../../utils/memoryManager'; import { memoryManager } from '../../utils/memoryManager';
import { tmdbService } from '../../services/tmdbService'; import { tmdbService } from '../../services/tmdbService';
@ -185,7 +185,10 @@ export const ThisWeekSection = React.memo(() => {
}; };
return ( return (
<Animated.View style={styles.container} entering={FadeIn.duration(350)}> <Animated.View
style={styles.container}
entering={FadeIn.duration(350)}
>
<View style={styles.header}> <View style={styles.header}>
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<Text style={[styles.title, { color: currentTheme.colors.text }]}>This Week</Text> <Text style={[styles.title, { color: currentTheme.colors.text }]}>This Week</Text>

View file

@ -1,8 +1,5 @@
import React, { createContext, useContext, useEffect, useMemo, useState, useRef } from 'react'; import React, { createContext, useContext, useEffect, useMemo, useState, useRef } from 'react';
import { InteractionManager } from 'react-native';
import accountService, { AuthUser } from '../services/AccountService'; import accountService, { AuthUser } from '../services/AccountService';
import supabase from '../services/supabaseClient';
import syncService from '../services/SyncService';
type AccountContextValue = { type AccountContextValue = {
user: AuthUser | null; user: AuthUser | null;
@ -22,73 +19,19 @@ export const AccountProvider: React.FC<{ children: React.ReactNode }> = ({ child
const loadingTimeoutRef = useRef<NodeJS.Timeout | null>(null); const loadingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => { useEffect(() => {
// Initial session (load full profile) // Initial user load
// Defer heavy work until after initial interactions to reduce launch CPU spike const loadUser = async () => {
const task = InteractionManager.runAfterInteractions(() => { try {
(async () => {
const u = await accountService.getCurrentUser(); const u = await accountService.getCurrentUser();
setUser(u); setUser(u);
} catch (error) {
console.warn('[AccountContext] Failed to load user:', error);
} finally {
setLoading(false); setLoading(false);
// Stage sync operations to avoid blocking the JS thread
syncService.init();
if (u) {
try {
await syncService.migrateLocalScopeToUser();
// Longer yield to event loop to reduce CPU pressure
await new Promise(resolve => setTimeout(resolve, 100));
await syncService.subscribeRealtime();
await new Promise(resolve => setTimeout(resolve, 100));
// Pull first to hydrate local state, then push to avoid wiping server with empty local
await syncService.fullPull();
await new Promise(resolve => setTimeout(resolve, 100));
await syncService.fullPush();
} catch {}
}
})();
});
// Auth state listener
const { data: subscription } = supabase.auth.onAuthStateChange(async (event, session) => {
// Only set loading for actual auth changes, not initial session
if (event !== 'INITIAL_SESSION') {
setLoading(true);
}
try {
const fullUser = session?.user ? await accountService.getCurrentUser() : null;
setUser(fullUser);
// Immediately clear loading so UI can transition to MainTabs/Auth
setLoading(false);
if (fullUser) {
// Run sync in background without blocking UI
setTimeout(async () => {
try {
await syncService.migrateLocalScopeToUser();
await new Promise(r => setTimeout(r, 0));
await syncService.subscribeRealtime();
await new Promise(r => setTimeout(r, 0));
await syncService.fullPull();
await new Promise(r => setTimeout(r, 0));
await syncService.fullPush();
} catch (error) {
console.warn('[AccountContext] Background sync failed:', error);
}
}, 0);
} else {
syncService.unsubscribeRealtime();
}
} catch (e) {
setLoading(false);
}
});
return () => {
subscription.subscription.unsubscribe();
task.cancel();
if (loadingTimeoutRef.current) {
clearTimeout(loadingTimeoutRef.current);
loadingTimeoutRef.current = null;
} }
}; };
loadUser();
}, []); }, []);
const value = useMemo<AccountContextValue>(() => ({ const value = useMemo<AccountContextValue>(() => ({

View file

@ -55,7 +55,7 @@ export const useCalendarData = (): UseCalendarDataReturn => {
try { try {
// Check memory pressure and cleanup if needed // Check memory pressure and cleanup if needed
memoryManager.checkMemoryPressure(); memoryManager.checkMemoryPressure();
if (!forceRefresh) { if (!forceRefresh) {
const cachedData = await robustCalendarCache.getCachedCalendarData( const cachedData = await robustCalendarCache.getCachedCalendarData(
libraryItems, libraryItems,
@ -65,7 +65,7 @@ export const useCalendarData = (): UseCalendarDataReturn => {
watched: watchedShows, watched: watchedShows,
} }
); );
if (cachedData) { if (cachedData) {
setCalendarData(cachedData); setCalendarData(cachedData);
setLoading(false); setLoading(false);
@ -156,11 +156,11 @@ export const useCalendarData = (): UseCalendarDataReturn => {
allSeries, allSeries,
async (series: StreamingContent, index: number) => { async (series: StreamingContent, index: number) => {
try { try {
// Use the new memory-efficient method to fetch only upcoming episodes // Use the new memory-efficient method to fetch upcoming and recent episodes
const episodeData = await stremioService.getUpcomingEpisodes(series.type, series.id, { const episodeData = await stremioService.getUpcomingEpisodes(series.type, series.id, {
daysBack: 14, // 2 weeks back daysBack: 90, // 3 months back for recently released episodes
daysAhead: 28, // 4 weeks ahead daysAhead: 60, // 2 months ahead for upcoming episodes
maxEpisodes: 25, // Limit episodes per series maxEpisodes: 50, // Increased limit to get more episodes per series
}); });
if (episodeData && episodeData.episodes.length > 0) { if (episodeData && episodeData.episodes.length > 0) {
@ -191,7 +191,7 @@ export const useCalendarData = (): UseCalendarDataReturn => {
// Transform episodes with memory-efficient processing // Transform episodes with memory-efficient processing
const transformedEpisodes = episodeData.episodes.map(video => { const transformedEpisodes = episodeData.episodes.map(video => {
const tmdbEpisode = tmdbEpisodes[`${video.season}:${video.episode}`] || {}; const tmdbEpisode = tmdbEpisodes[`${video.season}:${video.episode}`] || {};
return { const episode = {
id: video.id, id: video.id,
seriesId: series.id, seriesId: series.id,
title: tmdbEpisode.name || video.title || `Episode ${video.episode}`, title: tmdbEpisode.name || video.title || `Episode ${video.episode}`,
@ -205,6 +205,15 @@ export const useCalendarData = (): UseCalendarDataReturn => {
still_path: tmdbEpisode.still_path || null, still_path: tmdbEpisode.still_path || null,
season_poster_path: tmdbEpisode.season_poster_path || null season_poster_path: tmdbEpisode.season_poster_path || null
}; };
// Debug log for episodes
if (episode.releaseDate) {
logger.log(`[CalendarData] Episode with date: ${episode.seriesName} - ${episode.title} (${episode.releaseDate})`);
} else {
logger.log(`[CalendarData] Episode without date: ${episode.seriesName} - ${episode.title}`);
}
return episode;
}); });
// Clear references to help garbage collection // Clear references to help garbage collection
@ -257,10 +266,17 @@ export const useCalendarData = (): UseCalendarDataReturn => {
// Process results and separate episodes from no-episode series // Process results and separate episodes from no-episode series
for (const result of processedSeries) { for (const result of processedSeries) {
if (!result) {
logger.error(`[CalendarData] Null/undefined result in processedSeries`);
continue;
}
if (result.type === 'episodes' && Array.isArray(result.data)) { if (result.type === 'episodes' && Array.isArray(result.data)) {
allEpisodes.push(...result.data); allEpisodes.push(...result.data);
} else if (result.type === 'no-episodes') { } else if (result.type === 'no-episodes' && result.data) {
seriesWithoutEpisodes.push(result.data as CalendarEpisode); seriesWithoutEpisodes.push(result.data as CalendarEpisode);
} else {
logger.warn(`[CalendarData] Unexpected result type or missing data:`, result);
} }
} }
@ -271,35 +287,111 @@ export const useCalendarData = (): UseCalendarDataReturn => {
allEpisodes = memoryManager.limitArraySize(allEpisodes, 500); allEpisodes = memoryManager.limitArraySize(allEpisodes, 500);
seriesWithoutEpisodes = memoryManager.limitArraySize(seriesWithoutEpisodes, 100); seriesWithoutEpisodes = memoryManager.limitArraySize(seriesWithoutEpisodes, 100);
// Sort episodes by release date // Sort episodes by release date with error handling
allEpisodes.sort((a, b) => new Date(a.releaseDate).getTime() - new Date(b.releaseDate).getTime()); allEpisodes.sort((a, b) => {
try {
// Use memory-efficient filtering const dateA = new Date(a.releaseDate).getTime();
const dateB = new Date(b.releaseDate).getTime();
return dateA - dateB;
} catch (error) {
logger.warn(`[CalendarData] Error sorting episodes: ${a.releaseDate} vs ${b.releaseDate}`, error);
return 0; // Keep original order if sorting fails
}
});
logger.log(`[CalendarData] Total episodes fetched: ${allEpisodes.length}`);
// Use memory-efficient filtering with error handling
const thisWeekEpisodes = await memoryManager.filterLargeArray( const thisWeekEpisodes = await memoryManager.filterLargeArray(
allEpisodes, allEpisodes,
ep => isThisWeek(parseISO(ep.releaseDate)) ep => {
try {
if (!ep.releaseDate) return false;
const parsed = parseISO(ep.releaseDate);
return isThisWeek(parsed) && isAfter(parsed, new Date());
} catch (error) {
logger.warn(`[CalendarData] Error parsing date for this week filtering: ${ep.releaseDate}`, error);
return false;
}
}
); );
const upcomingEpisodes = await memoryManager.filterLargeArray( const upcomingEpisodes = await memoryManager.filterLargeArray(
allEpisodes, allEpisodes,
ep => isAfter(parseISO(ep.releaseDate), new Date()) && !isThisWeek(parseISO(ep.releaseDate)) ep => {
try {
if (!ep.releaseDate) return false;
const parsed = parseISO(ep.releaseDate);
return isAfter(parsed, new Date()) && !isThisWeek(parsed);
} catch (error) {
logger.warn(`[CalendarData] Error parsing date for upcoming filtering: ${ep.releaseDate}`, error);
return false;
}
}
); );
const recentEpisodes = await memoryManager.filterLargeArray( const recentEpisodes = await memoryManager.filterLargeArray(
allEpisodes, allEpisodes,
ep => isBefore(parseISO(ep.releaseDate), new Date()) && !isThisWeek(parseISO(ep.releaseDate)) ep => {
try {
if (!ep.releaseDate) return false;
const parsed = parseISO(ep.releaseDate);
return isBefore(parsed, new Date());
} catch (error) {
logger.warn(`[CalendarData] Error parsing date for recent filtering: ${ep.releaseDate}`, error);
return false;
}
}
); );
logger.log(`[CalendarData] Episode categorization: This Week: ${thisWeekEpisodes.length}, Upcoming: ${upcomingEpisodes.length}, Recently Released: ${recentEpisodes.length}, Series without episodes: ${seriesWithoutEpisodes.length}`);
// Debug: Show some example episodes from each category
if (thisWeekEpisodes && thisWeekEpisodes.length > 0) {
logger.log(`[CalendarData] This Week examples:`, thisWeekEpisodes.slice(0, 3).map(ep => ({
title: ep.title,
date: ep.releaseDate,
series: ep.seriesName
})));
}
if (recentEpisodes && recentEpisodes.length > 0) {
logger.log(`[CalendarData] Recently Released examples:`, recentEpisodes.slice(0, 3).map(ep => ({
title: ep.title,
date: ep.releaseDate,
series: ep.seriesName
})));
}
const sections: CalendarSection[] = []; const sections: CalendarSection[] = [];
if (thisWeekEpisodes.length > 0) sections.push({ title: 'This Week', data: thisWeekEpisodes }); if (thisWeekEpisodes.length > 0) {
if (upcomingEpisodes.length > 0) sections.push({ title: 'Upcoming', data: upcomingEpisodes }); sections.push({ title: 'This Week', data: thisWeekEpisodes });
if (recentEpisodes.length > 0) sections.push({ title: 'Recently Released', data: recentEpisodes }); logger.log(`[CalendarData] Added 'This Week' section with ${thisWeekEpisodes.length} episodes`);
if (seriesWithoutEpisodes.length > 0) sections.push({ title: 'Series with No Scheduled Episodes', data: seriesWithoutEpisodes }); }
if (upcomingEpisodes.length > 0) {
sections.push({ title: 'Upcoming', data: upcomingEpisodes });
logger.log(`[CalendarData] Added 'Upcoming' section with ${upcomingEpisodes.length} episodes`);
}
if (recentEpisodes.length > 0) {
sections.push({ title: 'Recently Released', data: recentEpisodes });
logger.log(`[CalendarData] Added 'Recently Released' section with ${recentEpisodes.length} episodes`);
}
if (seriesWithoutEpisodes.length > 0) {
sections.push({ title: 'Series with No Scheduled Episodes', data: seriesWithoutEpisodes });
logger.log(`[CalendarData] Added 'Series with No Scheduled Episodes' section with ${seriesWithoutEpisodes.length} episodes`);
}
// Log section details before setting
logger.log(`[CalendarData] About to set calendarData with ${sections.length} sections:`);
sections.forEach((section, index) => {
logger.log(` Section ${index}: "${section.title}" with ${section.data?.length || 0} episodes`);
});
setCalendarData(sections); setCalendarData(sections);
// Clear large arrays to help garbage collection // Clear large arrays to help garbage collection
memoryManager.clearObjects(allEpisodes, thisWeekEpisodes, upcomingEpisodes, recentEpisodes); // Note: Don't clear the arrays that are referenced in sections (thisWeekEpisodes, upcomingEpisodes, recentEpisodes, seriesWithoutEpisodes)
// as they would empty the section data
memoryManager.clearObjects(allEpisodes);
await robustCalendarCache.setCachedCalendarData( await robustCalendarCache.setCachedCalendarData(
sections, sections,

View file

@ -59,25 +59,16 @@ export function useFeaturedContent() {
const loadFeaturedContent = useCallback(async (forceRefresh = false) => { const loadFeaturedContent = useCallback(async (forceRefresh = false) => {
const t0 = Date.now(); const t0 = Date.now();
logger.info('[useFeaturedContent] load:start', { forceRefresh, contentSource: 'catalogs', selectedCatalogsCount: (selectedCatalogs || []).length });
// Check if we should use cached data (disabled if DISABLE_CACHE) // Check if we should use cached data (disabled if DISABLE_CACHE)
const now = Date.now(); const now = Date.now();
const cacheAge = now - persistentStore.lastFetchTime; const cacheAge = now - persistentStore.lastFetchTime;
logger.debug('[useFeaturedContent] cache:status', {
disabled: DISABLE_CACHE,
hasFeatured: Boolean(persistentStore.featuredContent),
allCount: persistentStore.allFeaturedContent?.length || 0,
cacheAgeMs: cacheAge,
timeoutMs: CACHE_TIMEOUT,
});
if (!DISABLE_CACHE) { if (!DISABLE_CACHE) {
if (!forceRefresh && if (!forceRefresh &&
persistentStore.featuredContent && persistentStore.featuredContent &&
persistentStore.allFeaturedContent.length > 0 && persistentStore.allFeaturedContent.length > 0 &&
cacheAge < CACHE_TIMEOUT) { cacheAge < CACHE_TIMEOUT) {
// Use cached data // Use cached data
logger.info('[useFeaturedContent] cache:use', { duration: `${Date.now() - t0}ms` });
setFeaturedContent(persistentStore.featuredContent); setFeaturedContent(persistentStore.featuredContent);
setAllFeaturedContent(persistentStore.allFeaturedContent); setAllFeaturedContent(persistentStore.allFeaturedContent);
setLoading(false); setLoading(false);
@ -86,7 +77,6 @@ export function useFeaturedContent() {
} }
} }
logger.info('[useFeaturedContent] fetch:start', { source: 'catalogs' });
setLoading(true); setLoading(true);
cleanup(); cleanup();
abortControllerRef.current = new AbortController(); abortControllerRef.current = new AbortController();
@ -99,7 +89,6 @@ export function useFeaturedContent() {
// Load from installed catalogs // Load from installed catalogs
const tCats = Date.now(); const tCats = Date.now();
const catalogs = await catalogService.getHomeCatalogs(); const catalogs = await catalogService.getHomeCatalogs();
logger.info('[useFeaturedContent] catalogs:list', { count: catalogs?.length || 0, duration: `${Date.now() - tCats}ms` });
if (signal.aborted) return; if (signal.aborted) return;
@ -114,7 +103,6 @@ export function useFeaturedContent() {
return selectedCatalogs.includes(catalogId); return selectedCatalogs.includes(catalogId);
}) })
: catalogs; // Use all catalogs if none specifically selected : catalogs; // Use all catalogs if none specifically selected
logger.debug('[useFeaturedContent] catalogs:filtered', { filteredCount: filteredCatalogs.length, selectedCount: selectedCatalogs?.length || 0 });
// Flatten all catalog items into a single array, filter out items without posters // Flatten all catalog items into a single array, filter out items without posters
const tFlat = Date.now(); const tFlat = Date.now();
@ -124,7 +112,6 @@ export function useFeaturedContent() {
// Remove duplicates based on ID // Remove duplicates based on ID
index === self.findIndex(t => t.id === item.id) index === self.findIndex(t => t.id === item.id)
); );
logger.info('[useFeaturedContent] catalogs:items', { total: allItems.length, duration: `${Date.now() - tFlat}ms` });
// Sort by popular, newest, etc. (possibly enhanced later) and take first 10 // Sort by popular, newest, etc. (possibly enhanced later) and take first 10
const topItems = allItems.sort(() => Math.random() - 0.5).slice(0, 10); const topItems = allItems.sort(() => Math.random() - 0.5).slice(0, 10);
@ -149,10 +136,8 @@ export function useFeaturedContent() {
// If enrichment is disabled, use addon logo if available // If enrichment is disabled, use addon logo if available
if (!settings.enrichMetadataWithTMDB) { if (!settings.enrichMetadataWithTMDB) {
if (base.logo && !isTmdbUrl(base.logo)) { if (base.logo && !isTmdbUrl(base.logo)) {
logger.debug('[useFeaturedContent] enrichment disabled, using addon logo', { name: item.name, logo: base.logo });
return base; return base;
} }
logger.debug('[useFeaturedContent] enrichment disabled, no addon logo available', { name: item.name });
return { ...base, logo: undefined }; return { ...base, logo: undefined };
} }
@ -172,16 +157,13 @@ export function useFeaturedContent() {
if (!tmdbId && !imdbId) return base; if (!tmdbId && !imdbId) return base;
// Try TMDB if we have a TMDB id // Try TMDB if we have a TMDB id
if (tmdbId) { if (tmdbId) {
logger.debug('[useFeaturedContent] logo:try:tmdb', { name: item.name, id: item.id, tmdbId, lang: preferredLanguage });
const logoUrl = await tmdbService.getContentLogo(item.type === 'series' ? 'tv' : 'movie', tmdbId as string, preferredLanguage); const logoUrl = await tmdbService.getContentLogo(item.type === 'series' ? 'tv' : 'movie', tmdbId as string, preferredLanguage);
if (logoUrl) { if (logoUrl) {
logger.debug('[useFeaturedContent] logo:tmdb:ok', { name: item.name, id: item.id, url: logoUrl, lang: preferredLanguage });
return { ...base, logo: logoUrl }; return { ...base, logo: logoUrl };
} }
} }
return base; return base;
} catch (error) { } catch (error) {
logger.error('[useFeaturedContent] logo:error', { name: item.name, id: item.id, error: String(error) });
return base; return base;
} }
}; };
@ -197,7 +179,6 @@ export function useFeaturedContent() {
logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none', logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none',
logo: c.logo || undefined, logo: c.logo || undefined,
})); }));
logger.info('[useFeaturedContent] catalogs:logos:details (enrich=true)', { items: details });
} catch {} } catch {}
} else { } else {
// When enrichment is disabled, prefer addon-provided logos; if missing, fetch basic meta to pull logo (like HeroSection) // When enrichment is disabled, prefer addon-provided logos; if missing, fetch basic meta to pull logo (like HeroSection)
@ -219,18 +200,15 @@ export function useFeaturedContent() {
// Attempt to fill missing logos from addon meta details for a limited subset // Attempt to fill missing logos from addon meta details for a limited subset
const candidates = baseItems.filter(i => !i.logo).slice(0, 10); const candidates = baseItems.filter(i => !i.logo).slice(0, 10);
logger.debug('[useFeaturedContent] catalogs:no-enrich:missing-logos', { count: candidates.length });
try { try {
const filled = await Promise.allSettled(candidates.map(async (item) => { const filled = await Promise.allSettled(candidates.map(async (item) => {
try { try {
const meta = await catalogService.getBasicContentDetails(item.type, item.id); const meta = await catalogService.getBasicContentDetails(item.type, item.id);
if (meta?.logo) { if (meta?.logo) {
logger.debug('[useFeaturedContent] catalogs:no-enrich:filled-logo', { id: item.id, name: item.name, logo: meta.logo });
return { id: item.id, logo: meta.logo } as { id: string; logo: string }; return { id: item.id, logo: meta.logo } as { id: string; logo: string };
} }
} catch (e) { } catch (e) {
logger.warn('[useFeaturedContent] catalogs:no-enrich:fill-failed', { id: item.id, error: String(e) });
} }
return { id: item.id, logo: undefined as any }; return { id: item.id, logo: undefined as any };
})); }));
@ -257,7 +235,6 @@ export function useFeaturedContent() {
logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none', logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none',
logo: c.logo || undefined, logo: c.logo || undefined,
})); }));
logger.info('[useFeaturedContent] catalogs:logos:details (no-enrich)', { items: details });
} catch {} } catch {}
} }
} }
@ -267,7 +244,6 @@ export function useFeaturedContent() {
// Safety guard: if nothing came back within a reasonable time, stop loading // Safety guard: if nothing came back within a reasonable time, stop loading
if (!formattedContent || formattedContent.length === 0) { if (!formattedContent || formattedContent.length === 0) {
logger.warn('[useFeaturedContent] results:empty');
// Fall back to any cached featured item so UI can render something // Fall back to any cached featured item so UI can render something
const cachedJson = await AsyncStorage.getItem(STORAGE_KEY).catch(() => null); const cachedJson = await AsyncStorage.getItem(STORAGE_KEY).catch(() => null);
if (cachedJson) { if (cachedJson) {
@ -277,7 +253,6 @@ export function useFeaturedContent() {
formattedContent = Array.isArray(parsed.allFeaturedContent) && parsed.allFeaturedContent.length > 0 formattedContent = Array.isArray(parsed.allFeaturedContent) && parsed.allFeaturedContent.length > 0
? parsed.allFeaturedContent ? parsed.allFeaturedContent
: [parsed.featuredContent]; : [parsed.featuredContent];
logger.info('[useFeaturedContent] fallback:storage', { count: formattedContent.length });
} }
} catch {} } catch {}
} }
@ -295,12 +270,6 @@ export function useFeaturedContent() {
if (formattedContent.length > 0) { if (formattedContent.length > 0) {
persistentStore.featuredContent = formattedContent[0]; persistentStore.featuredContent = formattedContent[0];
setFeaturedContent(formattedContent[0]); setFeaturedContent(formattedContent[0]);
logger.info('[useFeaturedContent] setting featuredContent', {
id: formattedContent[0].id,
name: formattedContent[0].name,
hasLogo: Boolean(formattedContent[0].logo),
logo: formattedContent[0].logo
});
currentIndexRef.current = 0; currentIndexRef.current = 0;
// Persist cache for fast startup (skipped when cache disabled) // Persist cache for fast startup (skipped when cache disabled)
if (!DISABLE_CACHE) { if (!DISABLE_CACHE) {
@ -313,7 +282,6 @@ export function useFeaturedContent() {
allFeaturedContent: formattedContent, allFeaturedContent: formattedContent,
}) })
); );
logger.debug('[useFeaturedContent] cache:written', { firstId: formattedContent[0]?.id });
} catch {} } catch {}
} }
} else { } else {
@ -326,16 +294,13 @@ export function useFeaturedContent() {
} }
} catch (error) { } catch (error) {
if (signal.aborted) { if (signal.aborted) {
logger.info('[useFeaturedContent] fetch:aborted');
} else { } else {
logger.error('[useFeaturedContent] fetch:error', { error: String(error) });
} }
setFeaturedContent(null); setFeaturedContent(null);
setAllFeaturedContent([]); setAllFeaturedContent([]);
} finally { } finally {
if (!signal.aborted) { if (!signal.aborted) {
setLoading(false); setLoading(false);
logger.info('[useFeaturedContent] load:done', { duration: `${Date.now() - t0}ms` });
} }
} }
}, [cleanup, genreMap, loadingGenres, selectedCatalogs]); }, [cleanup, genreMap, loadingGenres, selectedCatalogs]);
@ -344,7 +309,6 @@ export function useFeaturedContent() {
useEffect(() => { useEffect(() => {
if (DISABLE_CACHE) { if (DISABLE_CACHE) {
// Skip hydration entirely // Skip hydration entirely
logger.debug('[useFeaturedContent] hydrate:skipped');
return; return;
} }
let cancelled = false; let cancelled = false;
@ -364,7 +328,6 @@ export function useFeaturedContent() {
setFeaturedContent(parsed.featuredContent); setFeaturedContent(parsed.featuredContent);
setAllFeaturedContent(persistentStore.allFeaturedContent); setAllFeaturedContent(persistentStore.allFeaturedContent);
setLoading(false); setLoading(false);
logger.info('[useFeaturedContent] hydrate:storage', { allCount: persistentStore.allFeaturedContent.length });
} }
} }
} catch {} } catch {}
@ -392,7 +355,6 @@ export function useFeaturedContent() {
// Force refresh if settings changed during app restart, but only if we have content // Force refresh if settings changed during app restart, but only if we have content
if (settingsChanged && persistentStore.featuredContent) { if (settingsChanged && persistentStore.featuredContent) {
logger.info('[useFeaturedContent] settings:changed', { selectedCount: settings.selectedHeroCatalogs?.length || 0 });
loadFeaturedContent(true); loadFeaturedContent(true);
} }
}, [settings, loadFeaturedContent]); }, [settings, loadFeaturedContent]);
@ -410,11 +372,6 @@ export function useFeaturedContent() {
const tmdbLangChanged = persistentStore.lastSettings.tmdbLanguagePreference !== nextTmdbLang; const tmdbLangChanged = persistentStore.lastSettings.tmdbLanguagePreference !== nextTmdbLang;
if (catalogsChanged || logoPrefChanged || tmdbLangChanged) { if (catalogsChanged || logoPrefChanged || tmdbLangChanged) {
logger.info('[useFeaturedContent] event:settings-changed:immediate-refresh', {
catalogsChanged,
logoPrefChanged,
tmdbLangChanged
});
// Update internal state immediately so dependent effects are in sync // Update internal state immediately so dependent effects are in sync
setSelectedCatalogs(nextSelected); setSelectedCatalogs(nextSelected);

View file

@ -1,5 +1,4 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { syncService } from '../services/SyncService';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
// Simple event emitter for settings changes // Simple event emitter for settings changes
@ -230,8 +229,6 @@ export const useSettings = () => {
settingsEmitter.emit(); settingsEmitter.emit();
} }
// If authenticated, push settings to server to prevent overwrite on next pull
try { syncService.pushSettings(); } catch {}
} catch (error) { } catch (error) {
if (__DEV__) console.error('Failed to save settings:', error); if (__DEV__) console.error('Failed to save settings:', error);
} }

View file

@ -232,6 +232,20 @@ const CalendarScreen = () => {
// Log when rendering with relevant state info // Log when rendering with relevant state info
logger.log(`[Calendar] Rendering: loading=${loading}, calendarData sections=${calendarData.length}, allEpisodes=${allEpisodes.length}`); logger.log(`[Calendar] Rendering: loading=${loading}, calendarData sections=${calendarData.length}, allEpisodes=${allEpisodes.length}`);
// Log section details
if (calendarData.length > 0) {
calendarData.forEach((section, index) => {
logger.log(`[Calendar] Section ${index}: "${section.title}" with ${section.data.length} episodes`);
if (section.data && section.data.length > 0) {
logger.log(`[Calendar] First episode in "${section.title}": ${section.data[0].seriesName} - ${section.data[0].title} (${section.data[0].releaseDate})`);
} else {
logger.log(`[Calendar] Section "${section.title}" has empty or undefined data array`);
}
});
} else {
logger.log(`[Calendar] No calendarData sections available`);
}
// Handle date selection from calendar // Handle date selection from calendar
const handleDateSelect = useCallback((date: Date) => { const handleDateSelect = useCallback((date: Date) => {

View file

@ -29,7 +29,7 @@ import { Stream } from '../types/metadata';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import FastImage from '@d11/react-native-fast-image'; import FastImage from '@d11/react-native-fast-image';
import Animated, { FadeIn } from 'react-native-reanimated'; import Animated, { FadeIn, Layout } from 'react-native-reanimated';
import { PanGestureHandler } from 'react-native-gesture-handler'; import { PanGestureHandler } from 'react-native-gesture-handler';
import { import {
Gesture, Gesture,
@ -124,6 +124,16 @@ const HomeScreen = () => {
const totalCatalogsRef = useRef(0); const totalCatalogsRef = useRef(0);
const [visibleCatalogCount, setVisibleCatalogCount] = useState(5); // Reduced for memory const [visibleCatalogCount, setVisibleCatalogCount] = useState(5); // Reduced for memory
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
// Stabilize insets to prevent iOS layout shifts
const [stableInsetsTop, setStableInsetsTop] = useState(insets.top);
useEffect(() => {
// Only update insets after initial mount to prevent shifting
const timer = setTimeout(() => {
setStableInsetsTop(insets.top);
}, 100);
return () => clearTimeout(timer);
}, [insets.top]);
const { const {
featuredContent, featuredContent,
@ -653,15 +663,11 @@ const HomeScreen = () => {
const renderListItem = useCallback(({ item }: { item: HomeScreenListItem; index: number }) => { const renderListItem = useCallback(({ item }: { item: HomeScreenListItem; index: number }) => {
switch (item.type) { switch (item.type) {
case 'thisWeek': case 'thisWeek':
return <Animated.View>{memoizedThisWeekSection}</Animated.View>; return memoizedThisWeekSection;
case 'continueWatching': case 'continueWatching':
return null; // Moved to ListHeaderComponent to avoid remounts on scroll return null; // Moved to ListHeaderComponent to avoid remounts on scroll
case 'catalog': case 'catalog':
return ( return <CatalogSection catalog={item.catalog} />;
<Animated.View>
<CatalogSection catalog={item.catalog} />
</Animated.View>
);
case 'placeholder': case 'placeholder':
return ( return (
<Animated.View> <Animated.View>
@ -701,7 +707,7 @@ const HomeScreen = () => {
</Animated.View> </Animated.View>
); );
case 'welcome': case 'welcome':
return <Animated.View><FirstTimeWelcome /></Animated.View>; return <FirstTimeWelcome />;
default: default:
return null; return null;
} }
@ -747,10 +753,10 @@ const HomeScreen = () => {
} }
}, [toggleHeader]); }, [toggleHeader]);
// Memoize content container style // Memoize content container style - use stable insets to prevent iOS shifting
const contentContainerStyle = useMemo(() => const contentContainerStyle = useMemo(() =>
StyleSheet.flatten([styles.scrollContent, { paddingTop: insets.top }]), StyleSheet.flatten([styles.scrollContent, { paddingTop: stableInsetsTop }]),
[insets.top] [stableInsetsTop]
); );
// Memoize the main content section // Memoize the main content section
@ -775,7 +781,7 @@ const HomeScreen = () => {
onEndReached={handleLoadMoreCatalogs} onEndReached={handleLoadMoreCatalogs}
onEndReachedThreshold={0.6} onEndReachedThreshold={0.6}
recycleItems={true} recycleItems={true}
maintainVisibleContentPosition maintainVisibleContentPosition={Platform.OS !== 'ios'} // Disable on iOS to prevent shifting
onScroll={handleScroll} onScroll={handleScroll}
/> />
{/* Toasts are rendered globally at root */} {/* Toasts are rendered globally at root */}
@ -1341,4 +1347,5 @@ const HomeScreenWithFocusSync = (props: any) => {
return <HomeScreen {...props} />; return <HomeScreen {...props} />;
}; };
export default React.memo(HomeScreenWithFocusSync); export default React.memo(HomeScreenWithFocusSync);

View file

@ -1,5 +1,4 @@
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import supabase from './supabaseClient';
export type AuthUser = { export type AuthUser = {
id: string; id: string;
@ -8,6 +7,7 @@ export type AuthUser = {
displayName?: string; displayName?: string;
}; };
const USER_DATA_KEY = '@user:data';
const USER_SCOPE_KEY = '@user:current'; const USER_SCOPE_KEY = '@user:current';
class AccountService { class AccountService {
@ -20,53 +20,41 @@ class AccountService {
} }
async signUpWithEmail(email: string, password: string): Promise<{ user?: AuthUser; error?: string }> { async signUpWithEmail(email: string, password: string): Promise<{ user?: AuthUser; error?: string }> {
const { data, error } = await supabase.auth.signUp({ email, password }); // Since signup is disabled, always return error
if (error) return { error: error.message }; return { error: 'Sign up is currently disabled due to upcoming system changes' };
const user = data.user ? { id: data.user.id, email: data.user.email ?? undefined } : undefined;
if (user) await AsyncStorage.setItem(USER_SCOPE_KEY, user.id);
// Initialize profile row
if (user) {
await supabase.from('user_profiles').upsert({ user_id: user.id }, { onConflict: 'user_id' });
}
return { user };
} }
async signInWithEmail(email: string, password: string): Promise<{ user?: AuthUser; error?: string }> { async signInWithEmail(email: string, password: string): Promise<{ user?: AuthUser; error?: string }> {
const { data, error } = await supabase.auth.signInWithPassword({ email, password }); // Since signin is disabled, always return error
if (error) return { error: error.message }; return { error: 'Authentication is currently disabled' };
const user = data.user ? { id: data.user.id, email: data.user.email ?? undefined } : undefined;
if (user) await AsyncStorage.setItem(USER_SCOPE_KEY, user.id);
return { user };
} }
async signOut(): Promise<void> { async signOut(): Promise<void> {
await supabase.auth.signOut(); await AsyncStorage.removeItem(USER_DATA_KEY);
await AsyncStorage.setItem(USER_SCOPE_KEY, 'local'); await AsyncStorage.setItem(USER_SCOPE_KEY, 'local');
} }
async getCurrentUser(): Promise<AuthUser | null> { async getCurrentUser(): Promise<AuthUser | null> {
const { data } = await supabase.auth.getUser(); try {
const u = data.user; const userData = await AsyncStorage.getItem(USER_DATA_KEY);
if (!u) return null; if (!userData) return null;
// Fetch profile for avatar and display name return JSON.parse(userData);
const { data: profile } = await supabase } catch {
.from('user_profiles') return null;
.select('avatar_url, display_name') }
.eq('user_id', u.id)
.maybeSingle();
return { id: u.id, email: u.email ?? undefined, avatarUrl: profile?.avatar_url ?? undefined, displayName: profile?.display_name ?? undefined };
} }
async updateProfile(partial: { avatarUrl?: string; displayName?: string }): Promise<string | null> { async updateProfile(partial: { avatarUrl?: string; displayName?: string }): Promise<string | null> {
const { data } = await supabase.auth.getUser(); try {
const userId = data.user?.id; const currentUser = await this.getCurrentUser();
if (!userId) return 'Not authenticated'; if (!currentUser) return 'Not authenticated';
const { error } = await supabase.from('user_profiles').upsert({
user_id: userId, const updatedUser = { ...currentUser, ...partial };
avatar_url: partial.avatarUrl, await AsyncStorage.setItem(USER_DATA_KEY, JSON.stringify(updatedUser));
display_name: partial.displayName, return null;
}, { onConflict: 'user_id' }); } catch {
return error?.message ?? null; return 'Failed to update profile';
}
} }
async getCurrentUserIdScoped(): Promise<string> { async getCurrentUserIdScoped(): Promise<string> {

File diff suppressed because it is too large Load diff

View file

@ -714,14 +714,6 @@ class CatalogService {
if (!logoUrl || logoUrl.trim() === '' || logoUrl === 'null' || logoUrl === 'undefined') { if (!logoUrl || logoUrl.trim() === '' || logoUrl === 'null' || logoUrl === 'undefined') {
logoUrl = undefined; logoUrl = undefined;
} }
try {
logger.debug('[CatalogService] convertMetaToStreamingContent:logo', {
id: meta.id,
name: meta.name,
hasLogo: Boolean(logoUrl),
logo: logoUrl || undefined,
});
} catch {}
return { return {
id: meta.id, id: meta.id,

View file

@ -16,7 +16,7 @@ interface TraktCollections {
} }
const THIS_WEEK_CACHE_KEY = 'this_week_episodes_cache'; const THIS_WEEK_CACHE_KEY = 'this_week_episodes_cache';
const CALENDAR_CACHE_KEY = 'calendar_data_cache'; const CALENDAR_CACHE_KEY = 'calendar_data_cache_v2';
const CACHE_DURATION_MS = 30 * 60 * 1000; // 30 minutes (increased to reduce API calls) const CACHE_DURATION_MS = 30 * 60 * 1000; // 30 minutes (increased to reduce API calls)
const ERROR_CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes for error recovery const ERROR_CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes for error recovery

View file

@ -573,7 +573,6 @@ class StremioService {
await this.saveInstalledAddons(); await this.saveInstalledAddons();
await this.saveAddonOrder(); await this.saveAddonOrder();
try { (require('./SyncService').syncService as any).pushAddons?.(); } catch {}
// Emit an event that an addon was added // Emit an event that an addon was added
addonEmitter.emit(ADDON_EVENTS.ADDON_ADDED, manifest.id); addonEmitter.emit(ADDON_EVENTS.ADDON_ADDED, manifest.id);
} else { } else {
@ -596,7 +595,6 @@ class StremioService {
// Persist removals before app possibly exits // Persist removals before app possibly exits
await this.saveInstalledAddons(); await this.saveInstalledAddons();
await this.saveAddonOrder(); await this.saveAddonOrder();
try { (require('./SyncService').syncService as any).pushAddons?.(); } catch {}
// Emit an event that an addon was removed // Emit an event that an addon was removed
addonEmitter.emit(ADDON_EVENTS.ADDON_REMOVED, id); addonEmitter.emit(ADDON_EVENTS.ADDON_REMOVED, id);
} }
@ -754,13 +752,11 @@ class StremioService {
const urlPathStyle = `${baseUrl}/catalog/${type}/${encodedId}/skip=${pageSkip}.json${queryParams ? `?${queryParams}` : ''}`; const urlPathStyle = `${baseUrl}/catalog/${type}/${encodedId}/skip=${pageSkip}.json${queryParams ? `?${queryParams}` : ''}`;
// Add filters to path style (append with & or ? based on presence of queryParams) // Add filters to path style (append with & or ? based on presence of queryParams)
const urlPathWithFilters = urlPathStyle + (urlPathStyle.includes('?') ? filterQuery : (filterQuery ? `?${filterQuery.slice(1)}` : '')); const urlPathWithFilters = urlPathStyle + (urlPathStyle.includes('?') ? filterQuery : (filterQuery ? `?${filterQuery.slice(1)}` : ''));
try { logger.log('[StremioService] getCatalog URL (path-style)', { url: urlPathWithFilters, page, pageSize: this.DEFAULT_PAGE_SIZE, type, id, filters, manifest: manifest.id }); } catch {}
// Candidate 2: Query-style skip URL: /catalog/{type}/{id}.json?skip={N}&limit={PAGE_SIZE} // Candidate 2: Query-style skip URL: /catalog/{type}/{id}.json?skip={N}&limit={PAGE_SIZE}
let urlQueryStyle = `${baseUrl}/catalog/${type}/${encodedId}.json?skip=${pageSkip}&limit=${this.DEFAULT_PAGE_SIZE}`; let urlQueryStyle = `${baseUrl}/catalog/${type}/${encodedId}.json?skip=${pageSkip}&limit=${this.DEFAULT_PAGE_SIZE}`;
if (queryParams) urlQueryStyle += `&${queryParams}`; if (queryParams) urlQueryStyle += `&${queryParams}`;
urlQueryStyle += filterQuery; urlQueryStyle += filterQuery;
try { logger.log('[StremioService] getCatalog URL (query-style)', { url: urlQueryStyle, page, pageSize: this.DEFAULT_PAGE_SIZE, type, id, filters, manifest: manifest.id }); } catch {}
// Try path-style first, then fallback to query-style // Try path-style first, then fallback to query-style
let response; let response;
@ -779,7 +775,6 @@ class StremioService {
try { try {
const key = `${manifest.id}|${type}|${id}`; const key = `${manifest.id}|${type}|${id}`;
if (typeof hasMore === 'boolean') this.catalogHasMore.set(key, hasMore); if (typeof hasMore === 'boolean') this.catalogHasMore.set(key, hasMore);
logger.log('[StremioService] getCatalog response meta', { hasMore, count: Array.isArray(response.data.metas) ? response.data.metas.length : 0 });
} catch {} } catch {}
if (response.data.metas && Array.isArray(response.data.metas)) { if (response.data.metas && Array.isArray(response.data.metas)) {
return response.data.metas; return response.data.metas;
@ -798,23 +793,18 @@ class StremioService {
} }
async getMetaDetails(type: string, id: string, preferredAddonId?: string): Promise<MetaDetails | null> { async getMetaDetails(type: string, id: string, preferredAddonId?: string): Promise<MetaDetails | null> {
console.log(`🔍 [StremioService] getMetaDetails called:`, { type, id, preferredAddonId });
try { try {
// Validate content ID first // Validate content ID first
const isValidId = await this.isValidContentId(type, id); const isValidId = await this.isValidContentId(type, id);
console.log(`🔍 [StremioService] Content ID validation:`, { type, id, isValidId });
if (!isValidId) { if (!isValidId) {
console.log(`🔍 [StremioService] Invalid content ID, returning null`);
return null; return null;
} }
const addons = this.getInstalledAddons(); const addons = this.getInstalledAddons();
console.log(`🔍 [StremioService] Found ${addons.length} installed addons`);
// If a preferred addon is specified, try it first // If a preferred addon is specified, try it first
if (preferredAddonId) { if (preferredAddonId) {
console.log(`🔍 [StremioService] Preferred addon specified:`, { preferredAddonId });
const preferredAddon = addons.find(addon => addon.id === preferredAddonId); const preferredAddon = addons.find(addon => addon.id === preferredAddonId);
if (preferredAddon && preferredAddon.resources) { if (preferredAddon && preferredAddon.resources) {
@ -859,49 +849,26 @@ class StremioService {
} }
} }
console.log(`🔍 [StremioService] Preferred addon support check:`, {
hasMetaSupport,
supportsIdPrefix,
addonId: preferredAddon.id,
addonName: preferredAddon.name,
hasDeclaredPrefixes: preferredAddon.idPrefixes && preferredAddon.idPrefixes.length > 0
});
// Only require ID prefix compatibility if the addon has declared specific prefixes // Only require ID prefix compatibility if the addon has declared specific prefixes
const requiresIdPrefix = preferredAddon.idPrefixes && preferredAddon.idPrefixes.length > 0; const requiresIdPrefix = preferredAddon.idPrefixes && preferredAddon.idPrefixes.length > 0;
const isSupported = hasMetaSupport && (!requiresIdPrefix || supportsIdPrefix); const isSupported = hasMetaSupport && (!requiresIdPrefix || supportsIdPrefix);
if (isSupported) { if (isSupported) {
console.log(`🔍 [StremioService] Requesting metadata from preferred addon:`, { url });
try { try {
const response = await this.retryRequest(async () => { const response = await this.retryRequest(async () => {
return await axios.get(url, { timeout: 10000 }); return await axios.get(url, { timeout: 10000 });
}); });
console.log(`🔍 [StremioService] Preferred addon response:`, {
hasData: !!response.data,
hasMeta: !!response.data?.meta,
metaId: response.data?.meta?.id,
metaName: response.data?.meta?.name
});
if (response.data && response.data.meta) { if (response.data && response.data.meta) {
console.log(`🔍 [StremioService] Successfully got metadata from preferred addon`);
return response.data.meta; return response.data.meta;
} else { } else {
console.log(`🔍 [StremioService] Preferred addon returned no metadata`);
} }
} catch (error: any) { } catch (error: any) {
console.log(`🔍 [StremioService] Preferred addon request failed:`, {
errorMessage: error.message,
isAxiosError: error.isAxiosError,
responseStatus: error.response?.status,
responseData: error.response?.data
});
// Continue trying other addons // Continue trying other addons
} }
} else { } else {
console.log(`🔍 [StremioService] Preferred addon doesn't support this content type${requiresIdPrefix ? ' or ID prefix' : ''}`);
} }
} }
} }
@ -912,40 +879,23 @@ class StremioService {
'http://v3-cinemeta.strem.io' 'http://v3-cinemeta.strem.io'
]; ];
console.log(`🔍 [StremioService] Trying Cinemeta URLs:`, { cinemetaUrls });
for (const baseUrl of cinemetaUrls) { for (const baseUrl of cinemetaUrls) {
try { try {
const encodedId = encodeURIComponent(id); const encodedId = encodeURIComponent(id);
const url = `${baseUrl}/meta/${type}/${encodedId}.json`; const url = `${baseUrl}/meta/${type}/${encodedId}.json`;
console.log(`🔍 [StremioService] Requesting from Cinemeta:`, { url });
const response = await this.retryRequest(async () => { const response = await this.retryRequest(async () => {
return await axios.get(url, { timeout: 10000 }); return await axios.get(url, { timeout: 10000 });
}); });
console.log(`🔍 [StremioService] Cinemeta response:`, {
hasData: !!response.data,
hasMeta: !!response.data?.meta,
metaId: response.data?.meta?.id,
metaName: response.data?.meta?.name
});
if (response.data && response.data.meta) { if (response.data && response.data.meta) {
console.log(`🔍 [StremioService] Successfully got metadata from Cinemeta`);
return response.data.meta; return response.data.meta;
} else { } else {
console.log(`🔍 [StremioService] Cinemeta returned no metadata`);
} }
} catch (error: any) { } catch (error: any) {
console.log(`🔍 [StremioService] Cinemeta request failed:`, {
baseUrl,
errorMessage: error.message,
isAxiosError: error.isAxiosError,
responseStatus: error.response?.status,
responseData: error.response?.data
});
continue; // Try next URL continue; // Try next URL
} }
} }
@ -991,20 +941,12 @@ class StremioService {
} }
// Require meta support, but allow any ID if addon doesn't declare specific prefixes // Require meta support, but allow any ID if addon doesn't declare specific prefixes
console.log(`🔍 [StremioService] Addon support check:`, {
addonId: addon.id,
addonName: addon.name,
hasMetaSupport,
supportsIdPrefix,
hasDeclaredPrefixes: addon.idPrefixes && addon.idPrefixes.length > 0
});
// Only require ID prefix compatibility if the addon has declared specific prefixes // Only require ID prefix compatibility if the addon has declared specific prefixes
const requiresIdPrefix = addon.idPrefixes && addon.idPrefixes.length > 0; const requiresIdPrefix = addon.idPrefixes && addon.idPrefixes.length > 0;
const isSupported = hasMetaSupport && (!requiresIdPrefix || supportsIdPrefix); const isSupported = hasMetaSupport && (!requiresIdPrefix || supportsIdPrefix);
if (!isSupported) { if (!isSupported) {
console.log(`🔍 [StremioService] Addon doesn't support this content type${requiresIdPrefix ? ' or ID prefix' : ''}, skipping`);
continue; continue;
} }
@ -1013,52 +955,23 @@ class StremioService {
const encodedId = encodeURIComponent(id); const encodedId = encodeURIComponent(id);
const url = queryParams ? `${baseUrl}/meta/${type}/${encodedId}.json?${queryParams}` : `${baseUrl}/meta/${type}/${encodedId}.json`; const url = queryParams ? `${baseUrl}/meta/${type}/${encodedId}.json?${queryParams}` : `${baseUrl}/meta/${type}/${encodedId}.json`;
console.log(`🔍 [StremioService] Requesting from addon:`, {
addonId: addon.id,
addonName: addon.name,
url
});
const response = await this.retryRequest(async () => { const response = await this.retryRequest(async () => {
return await axios.get(url, { timeout: 10000 }); return await axios.get(url, { timeout: 10000 });
}); });
console.log(`🔍 [StremioService] Addon response:`, {
addonId: addon.id,
hasData: !!response.data,
hasMeta: !!response.data?.meta,
metaId: response.data?.meta?.id,
metaName: response.data?.meta?.name
});
if (response.data && response.data.meta) { if (response.data && response.data.meta) {
console.log(`🔍 [StremioService] Successfully got metadata from addon:`, { addonId: addon.id });
return response.data.meta; return response.data.meta;
} else { } else {
console.log(`🔍 [StremioService] Addon returned no metadata:`, { addonId: addon.id });
} }
} catch (error: any) { } catch (error: any) {
console.log(`🔍 [StremioService] Addon request failed:`, {
addonId: addon.id,
addonName: addon.name,
errorMessage: error.message,
isAxiosError: error.isAxiosError,
responseStatus: error.response?.status,
responseData: error.response?.data
});
continue; // Try next addon continue; // Try next addon
} }
} }
console.log(`🔍 [StremioService] No metadata found from any addon`);
return null; return null;
} catch (error) { } catch (error) {
console.log(`🔍 [StremioService] getMetaDetails caught error:`, {
errorMessage: error instanceof Error ? error.message : String(error),
isAxiosError: (error as any)?.isAxiosError,
responseStatus: (error as any)?.response?.status,
responseData: (error as any)?.response?.data
});
logger.error('Error in getMetaDetails:', error); logger.error('Error in getMetaDetails:', error);
return null; return null;
} }
@ -1102,15 +1015,24 @@ class StremioService {
// Filter episodes to only include those within our date range // Filter episodes to only include those within our date range
// This is done immediately after fetching to reduce memory footprint // This is done immediately after fetching to reduce memory footprint
logger.log(`[StremioService] Filtering ${metadata.videos.length} episodes for ${id}, date range: ${startDate.toISOString()} to ${endDate.toISOString()}`);
const filteredEpisodes = metadata.videos const filteredEpisodes = metadata.videos
.filter(video => { .filter(video => {
if (!video.released) return false; if (!video.released) {
logger.log(`[StremioService] Episode ${video.id} has no release date`);
return false;
}
const releaseDate = new Date(video.released); const releaseDate = new Date(video.released);
return releaseDate >= startDate && releaseDate <= endDate; const inRange = releaseDate >= startDate && releaseDate <= endDate;
logger.log(`[StremioService] Episode ${video.id}: released=${video.released}, inRange=${inRange}`);
return inRange;
}) })
.sort((a, b) => new Date(a.released).getTime() - new Date(b.released).getTime()) .sort((a, b) => new Date(a.released).getTime() - new Date(b.released).getTime())
.slice(0, maxEpisodes); // Limit number of episodes to prevent memory overflow .slice(0, maxEpisodes); // Limit number of episodes to prevent memory overflow
logger.log(`[StremioService] After filtering: ${filteredEpisodes.length} episodes remain`);
return { return {
seriesName: metadata.name, seriesName: metadata.name,
poster: metadata.poster || '', poster: metadata.poster || '',
@ -1634,11 +1556,9 @@ class StremioService {
const index = this.addonOrder.indexOf(id); const index = this.addonOrder.indexOf(id);
if (index > 0) { if (index > 0) {
// Swap with the previous item // Swap with the previous item
[this.addonOrder[index - 1], this.addonOrder[index]] = [this.addonOrder[index - 1], this.addonOrder[index]] =
[this.addonOrder[index], this.addonOrder[index - 1]]; [this.addonOrder[index], this.addonOrder[index - 1]];
this.saveAddonOrder(); this.saveAddonOrder();
// Immediately push to server to avoid resets on restart
try { (require('./SyncService').syncService as any).pushAddons?.(); } catch {}
// Emit an event that the order has changed // Emit an event that the order has changed
addonEmitter.emit(ADDON_EVENTS.ORDER_CHANGED); addonEmitter.emit(ADDON_EVENTS.ORDER_CHANGED);
return true; return true;
@ -1650,11 +1570,9 @@ class StremioService {
const index = this.addonOrder.indexOf(id); const index = this.addonOrder.indexOf(id);
if (index >= 0 && index < this.addonOrder.length - 1) { if (index >= 0 && index < this.addonOrder.length - 1) {
// Swap with the next item // Swap with the next item
[this.addonOrder[index], this.addonOrder[index + 1]] = [this.addonOrder[index], this.addonOrder[index + 1]] =
[this.addonOrder[index + 1], this.addonOrder[index]]; [this.addonOrder[index + 1], this.addonOrder[index]];
this.saveAddonOrder(); this.saveAddonOrder();
// Immediately push to server to avoid resets on restart
try { (require('./SyncService').syncService as any).pushAddons?.(); } catch {}
// Emit an event that the order has changed // Emit an event that the order has changed
addonEmitter.emit(ADDON_EVENTS.ORDER_CHANGED); addonEmitter.emit(ADDON_EVENTS.ORDER_CHANGED);
return true; return true;

View file

@ -1,24 +0,0 @@
import 'react-native-url-polyfill/auto';
import 'react-native-get-random-values';
import { createClient } from '@supabase/supabase-js';
import AsyncStorage from '@react-native-async-storage/async-storage';
const SUPABASE_URL = process.env.EXPO_PUBLIC_SUPABASE_URL;
const SUPABASE_ANON_KEY = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY;
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
throw new Error('Missing Supabase environment variables. Please check your .env file.');
}
export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: {
persistSession: true,
storage: AsyncStorage as unknown as Storage,
autoRefreshToken: true,
detectSessionInUrl: false,
},
});
export default supabase;