Merge branch 'tapframe:main' into main
1
.gitignore
vendored
|
|
@ -72,3 +72,4 @@ SDK54_UPGRADE_SUMMARY.md
|
|||
SDK54_UPGRADE_SUMMARY.md
|
||||
build-and-publish-app-releases.sh
|
||||
bottomnav.md
|
||||
/TrailerServices
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 55 KiB |
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 111 KiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 718 B After Width: | Height: | Size: 675 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 423 KiB After Width: | Height: | Size: 288 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 111 KiB |
1
enginefs
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 3a70b36f873307cd83fb3178bb891f73cf73aa87
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 122 KiB |
|
|
@ -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>
|
||||
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>(() => ({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||