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
build-and-publish-app-releases.sh
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"?>
<resources>
<color name="ic_launcher_background">#151515</color>
<color name="ic_launcher_background">#d1d1d2</color>
</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++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.nuviohub.app;
PRODUCT_NAME = Nuvio;
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
PRODUCT_NAME = "Nuvio";
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@ -492,8 +492,8 @@
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.nuviohub.app;
PRODUCT_NAME = Nuvio;
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
PRODUCT_NAME = "Nuvio";
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
SWIFT_VERSION = 5.0;
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"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Nuvio</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.2.5</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>nuvio</string>
<string>com.nuvio.app</string>
</array>
</dict>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>exp+nuvio</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>20</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBonjourServices</key>
<array>
<string>_http._tcp</string>
</array>
<key>RCTNewArchEnabled</key>
<true/>
<key>RCTRootViewBackgroundColor</key>
<integer>4278322180</integer>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>SplashScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UIRequiresFullScreen</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Nuvio</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.2.5</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>nuvio</string>
<string>com.nuvio.app</string>
</array>
</dict>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>exp+nuvio</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>20</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBonjourServices</key>
<array>
<string>_http._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your local network</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app does not require microphone access.</string>
<key>RCTNewArchEnabled</key>
<true/>
<key>RCTRootViewBackgroundColor</key>
<integer>4278322180</integer>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>SplashScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UIRequiresFullScreen</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View file

@ -1,5 +1,10 @@
<?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">
<plist version="1.0">
<dict/>
</plist>
<dict>
<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/vector-icons": "^15.0.2",
"@gorhom/bottom-sheet": "^5.2.6",
"@legendapp/list": "^2.0.13",
"@lottiefiles/dotlottie-react": "^0.6.5",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/blur": "^4.4.1",
@ -29,7 +30,6 @@
"@react-navigation/stack": "^7.2.10",
"@sentry/react-native": "~7.3.0",
"@shopify/flash-list": "^2.1.0",
"@supabase/supabase-js": "^2.54.0",
"@types/lodash": "^4.17.16",
"@types/react-native-video": "^5.0.20",
"axios": "^1.12.2",
@ -67,6 +67,7 @@
"posthog-react-native": "^4.4.0",
"react": "19.1.0",
"react-native": "0.81.4",
"react-native-boost": "^0.6.2",
"react-native-bottom-tabs": "^0.12.2",
"react-native-gesture-handler": "~2.28.0",
"react-native-get-random-values": "^1.11.0",
@ -80,7 +81,7 @@
"react-native-svg": "15.12.1",
"react-native-url-polyfill": "^2.0.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-wheel-color-picker": "^1.3.1",
"react-native-worklets": "^0.6.1",
@ -2347,6 +2348,27 @@
"integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==",
"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": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -3636,80 +3658,6 @@
"@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": {
"version": "8.0.0",
"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"
}
},
"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": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
@ -4283,15 +4225,6 @@
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"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": {
"version": "17.0.33",
"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": {
"version": "0.12.2",
"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",
"@sentry/react-native": "~7.3.0",
"@shopify/flash-list": "^2.1.0",
"@supabase/supabase-js": "^2.54.0",
"@types/lodash": "^4.17.16",
"@types/react-native-video": "^5.0.20",
"axios": "^1.12.2",

View file

@ -7,7 +7,7 @@ import { LinearGradient } from 'expo-linear-gradient';
import { CatalogContent, StreamingContent } from '../../services/catalogService';
import { useTheme } from '../../contexts/ThemeContext';
import ContentItem from './ContentItem';
import Animated, { FadeIn } from 'react-native-reanimated';
import Animated, { FadeIn, Layout } from 'react-native-reanimated';
import { RootStackParamList } from '../../navigation/AppNavigator';
interface CatalogSectionProps {
@ -77,7 +77,10 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => {
const keyExtractor = useCallback((item: StreamingContent) => `${item.id}-${item.type}`, []);
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.titleContainer}>
<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 { Toast } from 'toastify-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 { MaterialIcons, Feather } from '@expo/vector-icons';
import { useTheme } from '../../contexts/ThemeContext';
@ -11,6 +11,7 @@ import { DropUpMenu } from './DropUpMenu';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { storageService } from '../../services/storageService';
import { TraktService } from '../../services/traktService';
import Animated, { FadeIn } from 'react-native-reanimated';
interface ContentItemProps {
item: StreamingContent;
@ -96,7 +97,6 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
const { currentTheme } = useTheme();
const { settings, isLoaded } = useSettings();
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
const posterWidth = React.useMemo(() => {
switch (settings.posterSize) {
@ -232,7 +232,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
return (
<>
<Animated.View style={[styles.itemContainer, { width: posterWidth, opacity: fadeInOpacity }]}>
<Animated.View style={[styles.itemContainer, { width: posterWidth }]} entering={FadeIn.duration(300)}>
<TouchableOpacity
style={[styles.contentItem, { width: posterWidth, borderRadius: posterRadius }]}
activeOpacity={0.7}

View file

@ -10,7 +10,7 @@ import {
ActivityIndicator
} from 'react-native';
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 { NavigationProp } from '@react-navigation/native';
import { RootStackParamList } from '../../navigation/AppNavigator';
@ -726,7 +726,10 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
}
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.titleContainer}>
<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 */}
{item.genres && (
<View style={styles.genresOverlay as ViewStyle} pointerEvents="none">
<Animated.Text
entering={FadeIn.duration(400).delay(100)}
style={[styles.genres as TextStyle, { color: colors.mediumEmphasis, textAlign: 'center' }, genresAnimatedStyle]}
numberOfLines={1}
>
{item.genres.slice(0, 3).join(' • ')}
</Animated.Text>
<Animated.View entering={FadeIn.duration(400).delay(100)}>
<Animated.Text
style={[styles.genres as TextStyle, { color: colors.mediumEmphasis, textAlign: 'center' }, genresAnimatedStyle]}
numberOfLines={1}
>
{item.genres.slice(0, 3).join(' • ')}
</Animated.Text>
</Animated.View>
</View>
)}
{/* Static action buttons positioned absolutely over the card */}
<View style={styles.actionsOverlay as ViewStyle} pointerEvents="box-none">
<Animated.View
entering={FadeIn.duration(500).delay(200)}
style={[styles.actions as ViewStyle, actionsAnimatedStyle]}
>
<Animated.View entering={FadeIn.duration(500).delay(200)}>
<Animated.View style={[styles.actions as ViewStyle, actionsAnimatedStyle]}>
<TouchableOpacity
style={[styles.playButton as ViewStyle, { backgroundColor: colors.white }]}
onPress={onPressPlay}
@ -563,6 +562,7 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
<MaterialIcons name="info-outline" size={18} color={colors.white} />
<Text style={[styles.secondaryText as TextStyle, { color: colors.white }]}>Info</Text>
</TouchableOpacity>
</Animated.View>
</Animated.View>
</View>
{/* Static logo positioned absolutely over the card */}

View file

@ -18,7 +18,7 @@ import { useTraktContext } from '../../contexts/TraktContext';
import { useLibrary } from '../../hooks/useLibrary';
import { RootStackParamList } from '../../navigation/AppNavigator';
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 { memoryManager } from '../../utils/memoryManager';
import { tmdbService } from '../../services/tmdbService';
@ -185,7 +185,10 @@ export const ThisWeekSection = React.memo(() => {
};
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.titleContainer}>
<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 { InteractionManager } from 'react-native';
import accountService, { AuthUser } from '../services/AccountService';
import supabase from '../services/supabaseClient';
import syncService from '../services/SyncService';
type AccountContextValue = {
user: AuthUser | null;
@ -22,73 +19,19 @@ export const AccountProvider: React.FC<{ children: React.ReactNode }> = ({ child
const loadingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
// Initial session (load full profile)
// Defer heavy work until after initial interactions to reduce launch CPU spike
const task = InteractionManager.runAfterInteractions(() => {
(async () => {
// Initial user load
const loadUser = async () => {
try {
const u = await accountService.getCurrentUser();
setUser(u);
} catch (error) {
console.warn('[AccountContext] Failed to load user:', error);
} finally {
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>(() => ({

View file

@ -55,7 +55,7 @@ export const useCalendarData = (): UseCalendarDataReturn => {
try {
// Check memory pressure and cleanup if needed
memoryManager.checkMemoryPressure();
if (!forceRefresh) {
const cachedData = await robustCalendarCache.getCachedCalendarData(
libraryItems,
@ -65,7 +65,7 @@ export const useCalendarData = (): UseCalendarDataReturn => {
watched: watchedShows,
}
);
if (cachedData) {
setCalendarData(cachedData);
setLoading(false);
@ -156,11 +156,11 @@ export const useCalendarData = (): UseCalendarDataReturn => {
allSeries,
async (series: StreamingContent, index: number) => {
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, {
daysBack: 14, // 2 weeks back
daysAhead: 28, // 4 weeks ahead
maxEpisodes: 25, // Limit episodes per series
daysBack: 90, // 3 months back for recently released episodes
daysAhead: 60, // 2 months ahead for upcoming episodes
maxEpisodes: 50, // Increased limit to get more episodes per series
});
if (episodeData && episodeData.episodes.length > 0) {
@ -191,7 +191,7 @@ export const useCalendarData = (): UseCalendarDataReturn => {
// Transform episodes with memory-efficient processing
const transformedEpisodes = episodeData.episodes.map(video => {
const tmdbEpisode = tmdbEpisodes[`${video.season}:${video.episode}`] || {};
return {
const episode = {
id: video.id,
seriesId: series.id,
title: tmdbEpisode.name || video.title || `Episode ${video.episode}`,
@ -205,6 +205,15 @@ export const useCalendarData = (): UseCalendarDataReturn => {
still_path: tmdbEpisode.still_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
@ -257,10 +266,17 @@ export const useCalendarData = (): UseCalendarDataReturn => {
// Process results and separate episodes from no-episode series
for (const result of processedSeries) {
if (!result) {
logger.error(`[CalendarData] Null/undefined result in processedSeries`);
continue;
}
if (result.type === 'episodes' && Array.isArray(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);
} else {
logger.warn(`[CalendarData] Unexpected result type or missing data:`, result);
}
}
@ -271,35 +287,111 @@ export const useCalendarData = (): UseCalendarDataReturn => {
allEpisodes = memoryManager.limitArraySize(allEpisodes, 500);
seriesWithoutEpisodes = memoryManager.limitArraySize(seriesWithoutEpisodes, 100);
// Sort episodes by release date
allEpisodes.sort((a, b) => new Date(a.releaseDate).getTime() - new Date(b.releaseDate).getTime());
// Use memory-efficient filtering
// Sort episodes by release date with error handling
allEpisodes.sort((a, b) => {
try {
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(
allEpisodes,
ep => isThisWeek(parseISO(ep.releaseDate))
allEpisodes,
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(
allEpisodes,
ep => isAfter(parseISO(ep.releaseDate), new Date()) && !isThisWeek(parseISO(ep.releaseDate))
allEpisodes,
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(
allEpisodes,
ep => isBefore(parseISO(ep.releaseDate), new Date()) && !isThisWeek(parseISO(ep.releaseDate))
allEpisodes,
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[] = [];
if (thisWeekEpisodes.length > 0) sections.push({ title: 'This Week', data: thisWeekEpisodes });
if (upcomingEpisodes.length > 0) sections.push({ title: 'Upcoming', data: upcomingEpisodes });
if (recentEpisodes.length > 0) sections.push({ title: 'Recently Released', data: recentEpisodes });
if (seriesWithoutEpisodes.length > 0) sections.push({ title: 'Series with No Scheduled Episodes', data: seriesWithoutEpisodes });
if (thisWeekEpisodes.length > 0) {
sections.push({ title: 'This Week', data: thisWeekEpisodes });
logger.log(`[CalendarData] Added 'This Week' section with ${thisWeekEpisodes.length} episodes`);
}
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);
// 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(
sections,

View file

@ -59,25 +59,16 @@ export function useFeaturedContent() {
const loadFeaturedContent = useCallback(async (forceRefresh = false) => {
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)
const now = Date.now();
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 (!forceRefresh &&
persistentStore.featuredContent &&
persistentStore.allFeaturedContent.length > 0 &&
cacheAge < CACHE_TIMEOUT) {
// Use cached data
logger.info('[useFeaturedContent] cache:use', { duration: `${Date.now() - t0}ms` });
setFeaturedContent(persistentStore.featuredContent);
setAllFeaturedContent(persistentStore.allFeaturedContent);
setLoading(false);
@ -86,7 +77,6 @@ export function useFeaturedContent() {
}
}
logger.info('[useFeaturedContent] fetch:start', { source: 'catalogs' });
setLoading(true);
cleanup();
abortControllerRef.current = new AbortController();
@ -99,7 +89,6 @@ export function useFeaturedContent() {
// Load from installed catalogs
const tCats = Date.now();
const catalogs = await catalogService.getHomeCatalogs();
logger.info('[useFeaturedContent] catalogs:list', { count: catalogs?.length || 0, duration: `${Date.now() - tCats}ms` });
if (signal.aborted) return;
@ -114,7 +103,6 @@ export function useFeaturedContent() {
return selectedCatalogs.includes(catalogId);
})
: 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
const tFlat = Date.now();
@ -124,7 +112,6 @@ export function useFeaturedContent() {
// Remove duplicates based on 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
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 (!settings.enrichMetadataWithTMDB) {
if (base.logo && !isTmdbUrl(base.logo)) {
logger.debug('[useFeaturedContent] enrichment disabled, using addon logo', { name: item.name, logo: base.logo });
return base;
}
logger.debug('[useFeaturedContent] enrichment disabled, no addon logo available', { name: item.name });
return { ...base, logo: undefined };
}
@ -172,16 +157,13 @@ export function useFeaturedContent() {
if (!tmdbId && !imdbId) return base;
// Try TMDB if we have a TMDB id
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);
if (logoUrl) {
logger.debug('[useFeaturedContent] logo:tmdb:ok', { name: item.name, id: item.id, url: logoUrl, lang: preferredLanguage });
return { ...base, logo: logoUrl };
}
}
return base;
} catch (error) {
logger.error('[useFeaturedContent] logo:error', { name: item.name, id: item.id, error: String(error) });
return base;
}
};
@ -197,7 +179,6 @@ export function useFeaturedContent() {
logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none',
logo: c.logo || undefined,
}));
logger.info('[useFeaturedContent] catalogs:logos:details (enrich=true)', { items: details });
} catch {}
} else {
// 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
const candidates = baseItems.filter(i => !i.logo).slice(0, 10);
logger.debug('[useFeaturedContent] catalogs:no-enrich:missing-logos', { count: candidates.length });
try {
const filled = await Promise.allSettled(candidates.map(async (item) => {
try {
const meta = await catalogService.getBasicContentDetails(item.type, item.id);
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 };
}
} catch (e) {
logger.warn('[useFeaturedContent] catalogs:no-enrich:fill-failed', { id: item.id, error: String(e) });
}
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',
logo: c.logo || undefined,
}));
logger.info('[useFeaturedContent] catalogs:logos:details (no-enrich)', { items: details });
} catch {}
}
}
@ -267,7 +244,6 @@ export function useFeaturedContent() {
// Safety guard: if nothing came back within a reasonable time, stop loading
if (!formattedContent || formattedContent.length === 0) {
logger.warn('[useFeaturedContent] results:empty');
// Fall back to any cached featured item so UI can render something
const cachedJson = await AsyncStorage.getItem(STORAGE_KEY).catch(() => null);
if (cachedJson) {
@ -277,7 +253,6 @@ export function useFeaturedContent() {
formattedContent = Array.isArray(parsed.allFeaturedContent) && parsed.allFeaturedContent.length > 0
? parsed.allFeaturedContent
: [parsed.featuredContent];
logger.info('[useFeaturedContent] fallback:storage', { count: formattedContent.length });
}
} catch {}
}
@ -295,12 +270,6 @@ export function useFeaturedContent() {
if (formattedContent.length > 0) {
persistentStore.featuredContent = 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;
// Persist cache for fast startup (skipped when cache disabled)
if (!DISABLE_CACHE) {
@ -313,7 +282,6 @@ export function useFeaturedContent() {
allFeaturedContent: formattedContent,
})
);
logger.debug('[useFeaturedContent] cache:written', { firstId: formattedContent[0]?.id });
} catch {}
}
} else {
@ -326,16 +294,13 @@ export function useFeaturedContent() {
}
} catch (error) {
if (signal.aborted) {
logger.info('[useFeaturedContent] fetch:aborted');
} else {
logger.error('[useFeaturedContent] fetch:error', { error: String(error) });
}
setFeaturedContent(null);
setAllFeaturedContent([]);
} finally {
if (!signal.aborted) {
setLoading(false);
logger.info('[useFeaturedContent] load:done', { duration: `${Date.now() - t0}ms` });
}
}
}, [cleanup, genreMap, loadingGenres, selectedCatalogs]);
@ -344,7 +309,6 @@ export function useFeaturedContent() {
useEffect(() => {
if (DISABLE_CACHE) {
// Skip hydration entirely
logger.debug('[useFeaturedContent] hydrate:skipped');
return;
}
let cancelled = false;
@ -364,7 +328,6 @@ export function useFeaturedContent() {
setFeaturedContent(parsed.featuredContent);
setAllFeaturedContent(persistentStore.allFeaturedContent);
setLoading(false);
logger.info('[useFeaturedContent] hydrate:storage', { allCount: persistentStore.allFeaturedContent.length });
}
}
} catch {}
@ -392,7 +355,6 @@ export function useFeaturedContent() {
// Force refresh if settings changed during app restart, but only if we have content
if (settingsChanged && persistentStore.featuredContent) {
logger.info('[useFeaturedContent] settings:changed', { selectedCount: settings.selectedHeroCatalogs?.length || 0 });
loadFeaturedContent(true);
}
}, [settings, loadFeaturedContent]);
@ -410,11 +372,6 @@ export function useFeaturedContent() {
const tmdbLangChanged = persistentStore.lastSettings.tmdbLanguagePreference !== nextTmdbLang;
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
setSelectedCatalogs(nextSelected);

View file

@ -1,5 +1,4 @@
import { useState, useEffect, useCallback } from 'react';
import { syncService } from '../services/SyncService';
import AsyncStorage from '@react-native-async-storage/async-storage';
// Simple event emitter for settings changes
@ -230,8 +229,6 @@ export const useSettings = () => {
settingsEmitter.emit();
}
// If authenticated, push settings to server to prevent overwrite on next pull
try { syncService.pushSettings(); } catch {}
} catch (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
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
const handleDateSelect = useCallback((date: Date) => {

View file

@ -29,7 +29,7 @@ import { Stream } from '../types/metadata';
import { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
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 {
Gesture,
@ -124,6 +124,16 @@ const HomeScreen = () => {
const totalCatalogsRef = useRef(0);
const [visibleCatalogCount, setVisibleCatalogCount] = useState(5); // Reduced for memory
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 {
featuredContent,
@ -653,15 +663,11 @@ const HomeScreen = () => {
const renderListItem = useCallback(({ item }: { item: HomeScreenListItem; index: number }) => {
switch (item.type) {
case 'thisWeek':
return <Animated.View>{memoizedThisWeekSection}</Animated.View>;
return memoizedThisWeekSection;
case 'continueWatching':
return null; // Moved to ListHeaderComponent to avoid remounts on scroll
case 'catalog':
return (
<Animated.View>
<CatalogSection catalog={item.catalog} />
</Animated.View>
);
return <CatalogSection catalog={item.catalog} />;
case 'placeholder':
return (
<Animated.View>
@ -701,7 +707,7 @@ const HomeScreen = () => {
</Animated.View>
);
case 'welcome':
return <Animated.View><FirstTimeWelcome /></Animated.View>;
return <FirstTimeWelcome />;
default:
return null;
}
@ -747,10 +753,10 @@ const HomeScreen = () => {
}
}, [toggleHeader]);
// Memoize content container style
const contentContainerStyle = useMemo(() =>
StyleSheet.flatten([styles.scrollContent, { paddingTop: insets.top }]),
[insets.top]
// Memoize content container style - use stable insets to prevent iOS shifting
const contentContainerStyle = useMemo(() =>
StyleSheet.flatten([styles.scrollContent, { paddingTop: stableInsetsTop }]),
[stableInsetsTop]
);
// Memoize the main content section
@ -775,7 +781,7 @@ const HomeScreen = () => {
onEndReached={handleLoadMoreCatalogs}
onEndReachedThreshold={0.6}
recycleItems={true}
maintainVisibleContentPosition
maintainVisibleContentPosition={Platform.OS !== 'ios'} // Disable on iOS to prevent shifting
onScroll={handleScroll}
/>
{/* Toasts are rendered globally at root */}
@ -1341,4 +1347,5 @@ const HomeScreenWithFocusSync = (props: any) => {
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 supabase from './supabaseClient';
export type AuthUser = {
id: string;
@ -8,6 +7,7 @@ export type AuthUser = {
displayName?: string;
};
const USER_DATA_KEY = '@user:data';
const USER_SCOPE_KEY = '@user:current';
class AccountService {
@ -20,53 +20,41 @@ class AccountService {
}
async signUpWithEmail(email: string, password: string): Promise<{ user?: AuthUser; error?: string }> {
const { data, error } = await supabase.auth.signUp({ email, password });
if (error) return { error: error.message };
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 };
// Since signup is disabled, always return error
return { error: 'Sign up is currently disabled due to upcoming system changes' };
}
async signInWithEmail(email: string, password: string): Promise<{ user?: AuthUser; error?: string }> {
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
if (error) return { error: error.message };
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 };
// Since signin is disabled, always return error
return { error: 'Authentication is currently disabled' };
}
async signOut(): Promise<void> {
await supabase.auth.signOut();
await AsyncStorage.removeItem(USER_DATA_KEY);
await AsyncStorage.setItem(USER_SCOPE_KEY, 'local');
}
async getCurrentUser(): Promise<AuthUser | null> {
const { data } = await supabase.auth.getUser();
const u = data.user;
if (!u) return null;
// Fetch profile for avatar and display name
const { data: profile } = await supabase
.from('user_profiles')
.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 };
try {
const userData = await AsyncStorage.getItem(USER_DATA_KEY);
if (!userData) return null;
return JSON.parse(userData);
} catch {
return null;
}
}
async updateProfile(partial: { avatarUrl?: string; displayName?: string }): Promise<string | null> {
const { data } = await supabase.auth.getUser();
const userId = data.user?.id;
if (!userId) return 'Not authenticated';
const { error } = await supabase.from('user_profiles').upsert({
user_id: userId,
avatar_url: partial.avatarUrl,
display_name: partial.displayName,
}, { onConflict: 'user_id' });
return error?.message ?? null;
try {
const currentUser = await this.getCurrentUser();
if (!currentUser) return 'Not authenticated';
const updatedUser = { ...currentUser, ...partial };
await AsyncStorage.setItem(USER_DATA_KEY, JSON.stringify(updatedUser));
return null;
} catch {
return 'Failed to update profile';
}
}
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') {
logoUrl = undefined;
}
try {
logger.debug('[CatalogService] convertMetaToStreamingContent:logo', {
id: meta.id,
name: meta.name,
hasLogo: Boolean(logoUrl),
logo: logoUrl || undefined,
});
} catch {}
return {
id: meta.id,

View file

@ -16,7 +16,7 @@ interface TraktCollections {
}
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 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.saveAddonOrder();
try { (require('./SyncService').syncService as any).pushAddons?.(); } catch {}
// Emit an event that an addon was added
addonEmitter.emit(ADDON_EVENTS.ADDON_ADDED, manifest.id);
} else {
@ -596,7 +595,6 @@ class StremioService {
// Persist removals before app possibly exits
await this.saveInstalledAddons();
await this.saveAddonOrder();
try { (require('./SyncService').syncService as any).pushAddons?.(); } catch {}
// Emit an event that an addon was removed
addonEmitter.emit(ADDON_EVENTS.ADDON_REMOVED, id);
}
@ -754,13 +752,11 @@ class StremioService {
const urlPathStyle = `${baseUrl}/catalog/${type}/${encodedId}/skip=${pageSkip}.json${queryParams ? `?${queryParams}` : ''}`;
// Add filters to path style (append with & or ? based on presence of queryParams)
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}
let urlQueryStyle = `${baseUrl}/catalog/${type}/${encodedId}.json?skip=${pageSkip}&limit=${this.DEFAULT_PAGE_SIZE}`;
if (queryParams) urlQueryStyle += `&${queryParams}`;
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
let response;
@ -779,7 +775,6 @@ class StremioService {
try {
const key = `${manifest.id}|${type}|${id}`;
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 {}
if (response.data.metas && Array.isArray(response.data.metas)) {
return response.data.metas;
@ -798,23 +793,18 @@ class StremioService {
}
async getMetaDetails(type: string, id: string, preferredAddonId?: string): Promise<MetaDetails | null> {
console.log(`🔍 [StremioService] getMetaDetails called:`, { type, id, preferredAddonId });
try {
// Validate content ID first
const isValidId = await this.isValidContentId(type, id);
console.log(`🔍 [StremioService] Content ID validation:`, { type, id, isValidId });
if (!isValidId) {
console.log(`🔍 [StremioService] Invalid content ID, returning null`);
return null;
}
const addons = this.getInstalledAddons();
console.log(`🔍 [StremioService] Found ${addons.length} installed addons`);
// If a preferred addon is specified, try it first
if (preferredAddonId) {
console.log(`🔍 [StremioService] Preferred addon specified:`, { preferredAddonId });
const preferredAddon = addons.find(addon => addon.id === preferredAddonId);
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
const requiresIdPrefix = preferredAddon.idPrefixes && preferredAddon.idPrefixes.length > 0;
const isSupported = hasMetaSupport && (!requiresIdPrefix || supportsIdPrefix);
if (isSupported) {
console.log(`🔍 [StremioService] Requesting metadata from preferred addon:`, { url });
try {
const response = await this.retryRequest(async () => {
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) {
console.log(`🔍 [StremioService] Successfully got metadata from preferred addon`);
return response.data.meta;
} else {
console.log(`🔍 [StremioService] Preferred addon returned no metadata`);
}
} 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
}
} 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'
];
console.log(`🔍 [StremioService] Trying Cinemeta URLs:`, { cinemetaUrls });
for (const baseUrl of cinemetaUrls) {
try {
const encodedId = encodeURIComponent(id);
const url = `${baseUrl}/meta/${type}/${encodedId}.json`;
console.log(`🔍 [StremioService] Requesting from Cinemeta:`, { url });
const response = await this.retryRequest(async () => {
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) {
console.log(`🔍 [StremioService] Successfully got metadata from Cinemeta`);
return response.data.meta;
} else {
console.log(`🔍 [StremioService] Cinemeta returned no metadata`);
}
} 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
}
}
@ -991,20 +941,12 @@ class StremioService {
}
// 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
const requiresIdPrefix = addon.idPrefixes && addon.idPrefixes.length > 0;
const isSupported = hasMetaSupport && (!requiresIdPrefix || supportsIdPrefix);
if (!isSupported) {
console.log(`🔍 [StremioService] Addon doesn't support this content type${requiresIdPrefix ? ' or ID prefix' : ''}, skipping`);
continue;
}
@ -1013,52 +955,23 @@ class StremioService {
const encodedId = encodeURIComponent(id);
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 () => {
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) {
console.log(`🔍 [StremioService] Successfully got metadata from addon:`, { addonId: addon.id });
return response.data.meta;
} else {
console.log(`🔍 [StremioService] Addon returned no metadata:`, { addonId: addon.id });
}
} 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
}
}
console.log(`🔍 [StremioService] No metadata found from any addon`);
return null;
} 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);
return null;
}
@ -1102,15 +1015,24 @@ class StremioService {
// Filter episodes to only include those within our date range
// 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
.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);
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())
.slice(0, maxEpisodes); // Limit number of episodes to prevent memory overflow
logger.log(`[StremioService] After filtering: ${filteredEpisodes.length} episodes remain`);
return {
seriesName: metadata.name,
poster: metadata.poster || '',
@ -1634,11 +1556,9 @@ class StremioService {
const index = this.addonOrder.indexOf(id);
if (index > 0) {
// 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.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
addonEmitter.emit(ADDON_EVENTS.ORDER_CHANGED);
return true;
@ -1650,11 +1570,9 @@ class StremioService {
const index = this.addonOrder.indexOf(id);
if (index >= 0 && index < this.addonOrder.length - 1) {
// 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.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
addonEmitter.emit(ADDON_EVENTS.ORDER_CHANGED);
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;