diff --git a/.gitignore b/.gitignore index e968fcb0..5262efb7 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,4 @@ SDK54_UPGRADE_SUMMARY.md SDK54_UPGRADE_SUMMARY.md build-and-publish-app-releases.sh bottomnav.md +/TrailerServices diff --git a/TrailerService b/TrailerServices similarity index 100% rename from TrailerService rename to TrailerServices diff --git a/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png index 29ed05b1..4438ec3f 100644 Binary files a/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png and b/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png index 1e3a3784..1ed3f86f 100644 Binary files a/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png and b/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png index 6b7ddf9a..72b74fd7 100644 Binary files a/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png and b/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png index b977b3fe..f2b28821 100644 Binary files a/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png and b/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png index a79fe8b5..dda7ae63 100644 Binary files a/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png and b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp index 4f554927..90eae328 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp index acff3848..13f0a542 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index 68b2b6be..739fbfc6 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 7ce788ce..925188e3 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp index 695874a4..8f037534 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index de65e827..24765420 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index f48cd8b2..03031102 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp index b9693cb0..f32047c9 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index d48403bb..bc419a2a 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index eebf7ad6..0bb42972 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp index 5f719166..1bf77f34 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 4f6de845..92407f12 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index bee06ef1..578225fa 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp index 42083685..cf53f52c 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 34afa472..7f31cf8e 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/assets/adaptive-icon.png b/assets/adaptive-icon.png index d5a9df52..302b45f1 100644 Binary files a/assets/adaptive-icon.png and b/assets/adaptive-icon.png differ diff --git a/assets/android/ic_launcher-web.png b/assets/android/ic_launcher-web.png index 2efccc34..70a03a85 100644 Binary files a/assets/android/ic_launcher-web.png and b/assets/android/ic_launcher-web.png differ diff --git a/assets/android/mipmap-hdpi/ic_launcher.png b/assets/android/mipmap-hdpi/ic_launcher.png index af5bd9d2..38ab3369 100644 Binary files a/assets/android/mipmap-hdpi/ic_launcher.png and b/assets/android/mipmap-hdpi/ic_launcher.png differ diff --git a/assets/android/mipmap-hdpi/ic_launcher_foreground.png b/assets/android/mipmap-hdpi/ic_launcher_foreground.png index 4aaa7084..940dccdb 100644 Binary files a/assets/android/mipmap-hdpi/ic_launcher_foreground.png and b/assets/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/assets/android/mipmap-hdpi/ic_launcher_round.png b/assets/android/mipmap-hdpi/ic_launcher_round.png index ce446f7f..66437aa9 100644 Binary files a/assets/android/mipmap-hdpi/ic_launcher_round.png and b/assets/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/assets/android/mipmap-ldpi/ic_launcher.png b/assets/android/mipmap-ldpi/ic_launcher.png index 426e2aef..e55a9925 100644 Binary files a/assets/android/mipmap-ldpi/ic_launcher.png and b/assets/android/mipmap-ldpi/ic_launcher.png differ diff --git a/assets/android/mipmap-mdpi/ic_launcher.png b/assets/android/mipmap-mdpi/ic_launcher.png index c450f864..761b690b 100644 Binary files a/assets/android/mipmap-mdpi/ic_launcher.png and b/assets/android/mipmap-mdpi/ic_launcher.png differ diff --git a/assets/android/mipmap-mdpi/ic_launcher_foreground.png b/assets/android/mipmap-mdpi/ic_launcher_foreground.png index 7ec84d6e..e0b31c69 100644 Binary files a/assets/android/mipmap-mdpi/ic_launcher_foreground.png and b/assets/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/assets/android/mipmap-mdpi/ic_launcher_round.png b/assets/android/mipmap-mdpi/ic_launcher_round.png index 1185a4dc..a627ea09 100644 Binary files a/assets/android/mipmap-mdpi/ic_launcher_round.png and b/assets/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/assets/android/mipmap-xhdpi/ic_launcher.png b/assets/android/mipmap-xhdpi/ic_launcher.png index 81d12cd8..90076544 100644 Binary files a/assets/android/mipmap-xhdpi/ic_launcher.png and b/assets/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/assets/android/mipmap-xhdpi/ic_launcher_foreground.png b/assets/android/mipmap-xhdpi/ic_launcher_foreground.png index dbdd8352..b41d4338 100644 Binary files a/assets/android/mipmap-xhdpi/ic_launcher_foreground.png and b/assets/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/assets/android/mipmap-xhdpi/ic_launcher_round.png b/assets/android/mipmap-xhdpi/ic_launcher_round.png index 17ea551b..16e10232 100644 Binary files a/assets/android/mipmap-xhdpi/ic_launcher_round.png and b/assets/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/assets/android/mipmap-xxhdpi/ic_launcher.png b/assets/android/mipmap-xxhdpi/ic_launcher.png index dd251f4b..93d17d51 100644 Binary files a/assets/android/mipmap-xxhdpi/ic_launcher.png and b/assets/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/assets/android/mipmap-xxhdpi/ic_launcher_foreground.png b/assets/android/mipmap-xxhdpi/ic_launcher_foreground.png index 1a315dbf..176e8e6d 100644 Binary files a/assets/android/mipmap-xxhdpi/ic_launcher_foreground.png and b/assets/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/assets/android/mipmap-xxhdpi/ic_launcher_round.png b/assets/android/mipmap-xxhdpi/ic_launcher_round.png index c16b8d9f..3e19591b 100644 Binary files a/assets/android/mipmap-xxhdpi/ic_launcher_round.png and b/assets/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/assets/android/mipmap-xxxhdpi/ic_launcher.png b/assets/android/mipmap-xxxhdpi/ic_launcher.png index b017bf50..cbdc61fc 100644 Binary files a/assets/android/mipmap-xxxhdpi/ic_launcher.png and b/assets/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/assets/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/assets/android/mipmap-xxxhdpi/ic_launcher_foreground.png index 1c276506..a1941086 100644 Binary files a/assets/android/mipmap-xxxhdpi/ic_launcher_foreground.png and b/assets/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/assets/android/mipmap-xxxhdpi/ic_launcher_round.png b/assets/android/mipmap-xxxhdpi/ic_launcher_round.png index 055fe00a..0f726c45 100644 Binary files a/assets/android/mipmap-xxxhdpi/ic_launcher_round.png and b/assets/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/assets/android/playstore-icon.png b/assets/android/playstore-icon.png index 8e950cfc..f97a895f 100644 Binary files a/assets/android/playstore-icon.png and b/assets/android/playstore-icon.png differ diff --git a/assets/android/values/ic_launcher_background.xml b/assets/android/values/ic_launcher_background.xml index dcdf0321..cb9e1607 100644 --- a/assets/android/values/ic_launcher_background.xml +++ b/assets/android/values/ic_launcher_background.xml @@ -1,4 +1,4 @@ - #151515 + #d1d1d2 \ No newline at end of file diff --git a/assets/bootsplash/android/drawable-hdpi/bootsplash_logo.png b/assets/bootsplash/android/drawable-hdpi/bootsplash_logo.png index bd6895d7..03695531 100644 Binary files a/assets/bootsplash/android/drawable-hdpi/bootsplash_logo.png and b/assets/bootsplash/android/drawable-hdpi/bootsplash_logo.png differ diff --git a/assets/bootsplash/android/drawable-mdpi/bootsplash_logo.png b/assets/bootsplash/android/drawable-mdpi/bootsplash_logo.png index b6d09cc1..03695531 100644 Binary files a/assets/bootsplash/android/drawable-mdpi/bootsplash_logo.png and b/assets/bootsplash/android/drawable-mdpi/bootsplash_logo.png differ diff --git a/assets/bootsplash/android/drawable-xhdpi/bootsplash_logo.png b/assets/bootsplash/android/drawable-xhdpi/bootsplash_logo.png index 45de6313..03695531 100644 Binary files a/assets/bootsplash/android/drawable-xhdpi/bootsplash_logo.png and b/assets/bootsplash/android/drawable-xhdpi/bootsplash_logo.png differ diff --git a/assets/bootsplash/android/drawable-xxhdpi/bootsplash_logo.png b/assets/bootsplash/android/drawable-xxhdpi/bootsplash_logo.png index 850078ad..03695531 100644 Binary files a/assets/bootsplash/android/drawable-xxhdpi/bootsplash_logo.png and b/assets/bootsplash/android/drawable-xxhdpi/bootsplash_logo.png differ diff --git a/assets/bootsplash/android/drawable-xxxhdpi/bootsplash_logo.png b/assets/bootsplash/android/drawable-xxxhdpi/bootsplash_logo.png index 5d38cd59..03695531 100644 Binary files a/assets/bootsplash/android/drawable-xxxhdpi/bootsplash_logo.png and b/assets/bootsplash/android/drawable-xxxhdpi/bootsplash_logo.png differ diff --git a/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f.png b/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f.png index 19e5b3cf..03695531 100644 Binary files a/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f.png and b/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f.png differ diff --git a/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f@2x.png b/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f@2x.png index 115594f7..03695531 100644 Binary files a/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f@2x.png and b/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f@2x.png differ diff --git a/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f@3x.png b/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f@3x.png index 061180d3..03695531 100644 Binary files a/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f@3x.png and b/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f@3x.png differ diff --git a/assets/icon.png b/assets/icon.png index d5a9df52..03695531 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/assets/ios/AppIcon.appiconset/Icon-App-20x20@1x.png b/assets/ios/AppIcon.appiconset/Icon-App-20x20@1x.png index ffc8aaa2..5837ee46 100644 Binary files a/assets/ios/AppIcon.appiconset/Icon-App-20x20@1x.png and b/assets/ios/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/assets/ios/AppIcon.appiconset/Icon-App-20x20@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-20x20@2x.png index 4a52eaa1..8fa15efa 100644 Binary files a/assets/ios/AppIcon.appiconset/Icon-App-20x20@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/assets/ios/AppIcon.appiconset/Icon-App-20x20@3x.png b/assets/ios/AppIcon.appiconset/Icon-App-20x20@3x.png index d5eea9b8..004058f0 100644 Binary files a/assets/ios/AppIcon.appiconset/Icon-App-20x20@3x.png and b/assets/ios/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/assets/ios/AppIcon.appiconset/Icon-App-29x29@1x.png b/assets/ios/AppIcon.appiconset/Icon-App-29x29@1x.png index 379634b3..4a853b68 100644 Binary files a/assets/ios/AppIcon.appiconset/Icon-App-29x29@1x.png and b/assets/ios/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/assets/ios/AppIcon.appiconset/Icon-App-29x29@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-29x29@2x.png index 4ff0ef21..a3e794a1 100644 Binary files a/assets/ios/AppIcon.appiconset/Icon-App-29x29@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/assets/ios/AppIcon.appiconset/Icon-App-29x29@3x.png b/assets/ios/AppIcon.appiconset/Icon-App-29x29@3x.png index edcf4d5e..3e317c18 100644 Binary files a/assets/ios/AppIcon.appiconset/Icon-App-29x29@3x.png and b/assets/ios/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/assets/ios/AppIcon.appiconset/Icon-App-40x40@1x.png b/assets/ios/AppIcon.appiconset/Icon-App-40x40@1x.png index 4a52eaa1..8fa15efa 100644 Binary files a/assets/ios/AppIcon.appiconset/Icon-App-40x40@1x.png and b/assets/ios/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/assets/ios/AppIcon.appiconset/Icon-App-40x40@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-40x40@2x.png index e4afe636..718e4cae 100644 Binary files a/assets/ios/AppIcon.appiconset/Icon-App-40x40@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/assets/ios/AppIcon.appiconset/Icon-App-40x40@3x.png b/assets/ios/AppIcon.appiconset/Icon-App-40x40@3x.png index 152a8e64..147ca19b 100644 Binary files a/assets/ios/AppIcon.appiconset/Icon-App-40x40@3x.png and b/assets/ios/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/assets/ios/AppIcon.appiconset/Icon-App-60x60@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-60x60@2x.png index 152a8e64..147ca19b 100644 Binary files a/assets/ios/AppIcon.appiconset/Icon-App-60x60@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png b/assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png index bbc2ad23..2faf8a04 100644 Binary files a/assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png and b/assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/assets/ios/AppIcon.appiconset/Icon-App-76x76@1x.png b/assets/ios/AppIcon.appiconset/Icon-App-76x76@1x.png index a67bc43f..556ef587 100644 Binary files a/assets/ios/AppIcon.appiconset/Icon-App-76x76@1x.png and b/assets/ios/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/assets/ios/AppIcon.appiconset/Icon-App-76x76@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-76x76@2x.png index 3a87610d..1eba0909 100644 Binary files a/assets/ios/AppIcon.appiconset/Icon-App-76x76@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/assets/ios/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 0f242d68..47de3304 100644 Binary files a/assets/ios/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/assets/ios/AppIcon.appiconset/ItunesArtwork@2x.png b/assets/ios/AppIcon.appiconset/ItunesArtwork@2x.png index e2f386ee..5f69402b 100644 Binary files a/assets/ios/AppIcon.appiconset/ItunesArtwork@2x.png and b/assets/ios/AppIcon.appiconset/ItunesArtwork@2x.png differ diff --git a/assets/ios/iTunesArtwork@1x.png b/assets/ios/iTunesArtwork@1x.png index 8e950cfc..f97a895f 100644 Binary files a/assets/ios/iTunesArtwork@1x.png and b/assets/ios/iTunesArtwork@1x.png differ diff --git a/assets/ios/iTunesArtwork@2x.png b/assets/ios/iTunesArtwork@2x.png index e2f386ee..5f69402b 100644 Binary files a/assets/ios/iTunesArtwork@2x.png and b/assets/ios/iTunesArtwork@2x.png differ diff --git a/assets/ios/iTunesArtwork@3x.png b/assets/ios/iTunesArtwork@3x.png index 5c2b7583..938011a7 100644 Binary files a/assets/ios/iTunesArtwork@3x.png and b/assets/ios/iTunesArtwork@3x.png differ diff --git a/assets/splash-icon.png b/assets/splash-icon.png index 5fa61298..03695531 100644 Binary files a/assets/splash-icon.png and b/assets/splash-icon.png differ diff --git a/enginefs b/enginefs deleted file mode 160000 index 3a70b36f..00000000 --- a/enginefs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3a70b36f873307cd83fb3178bb891f73cf73aa87 diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj index 1d96622b..a1ef76a1 100644 --- a/ios/Nuvio.xcodeproj/project.pbxproj +++ b/ios/Nuvio.xcodeproj/project.pbxproj @@ -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"; diff --git a/ios/Nuvio/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/ios/Nuvio/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png index e9a189f6..4e9f3449 100644 Binary files a/ios/Nuvio/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png and b/ios/Nuvio/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png differ diff --git a/ios/Nuvio/Images.xcassets/SplashScreenLegacy.imageset/image.png b/ios/Nuvio/Images.xcassets/SplashScreenLegacy.imageset/image.png index efcdf22a..83330c32 100644 Binary files a/ios/Nuvio/Images.xcassets/SplashScreenLegacy.imageset/image.png and b/ios/Nuvio/Images.xcassets/SplashScreenLegacy.imageset/image.png differ diff --git a/ios/Nuvio/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png b/ios/Nuvio/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png index efcdf22a..83330c32 100644 Binary files a/ios/Nuvio/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png and b/ios/Nuvio/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png differ diff --git a/ios/Nuvio/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png b/ios/Nuvio/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png index efcdf22a..83330c32 100644 Binary files a/ios/Nuvio/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png and b/ios/Nuvio/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png differ diff --git a/ios/Nuvio/Info.plist b/ios/Nuvio/Info.plist index 4ab9b8d0..d701baa9 100644 --- a/ios/Nuvio/Info.plist +++ b/ios/Nuvio/Info.plist @@ -1,98 +1,101 @@ - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Nuvio - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.2.5 - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleURLSchemes - - nuvio - com.nuvio.app - - - - CFBundleURLSchemes - - exp+nuvio - - - - CFBundleVersion - 20 - LSMinimumSystemVersion - 12.0 - LSRequiresIPhoneOS - - LSSupportsOpeningDocumentsInPlace - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - NSBonjourServices - - _http._tcp - - RCTNewArchEnabled - - RCTRootViewBackgroundColor - 4278322180 - UIBackgroundModes - - audio - fetch - - UIFileSharingEnabled - - UILaunchStoryboardName - SplashScreen - UIRequiredDeviceCapabilities - - arm64 - - UIRequiresFullScreen - - UIStatusBarStyle - UIStatusBarStyleDefault - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIUserInterfaceStyle - Dark - UIViewControllerBasedStatusBarAppearance - - - + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Nuvio + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.2.5 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + nuvio + com.nuvio.app + + + + CFBundleURLSchemes + + exp+nuvio + + + + CFBundleVersion + 20 + LSMinimumSystemVersion + 12.0 + LSRequiresIPhoneOS + + LSSupportsOpeningDocumentsInPlace + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSBonjourServices + + _http._tcp + + NSLocalNetworkUsageDescription + Allow $(PRODUCT_NAME) to access your local network + NSMicrophoneUsageDescription + This app does not require microphone access. + RCTNewArchEnabled + + RCTRootViewBackgroundColor + 4278322180 + UIBackgroundModes + + audio + + UIFileSharingEnabled + + UILaunchStoryboardName + SplashScreen + UIRequiredDeviceCapabilities + + arm64 + + UIRequiresFullScreen + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIUserInterfaceStyle + Dark + UIViewControllerBasedStatusBarAppearance + + + \ No newline at end of file diff --git a/ios/Nuvio/NuvioRelease.entitlements b/ios/Nuvio/NuvioRelease.entitlements index 0c67376e..a0bc443f 100644 --- a/ios/Nuvio/NuvioRelease.entitlements +++ b/ios/Nuvio/NuvioRelease.entitlements @@ -1,5 +1,10 @@ - - + + aps-environment + development + com.apple.developer.associated-domains + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 79e673e1..2641fdf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 6c96b1e0..f07ab8eb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/home/CatalogSection.tsx b/src/components/home/CatalogSection.tsx index 7639a9f0..1f344fb3 100644 --- a/src/components/home/CatalogSection.tsx +++ b/src/components/home/CatalogSection.tsx @@ -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 ( - + {catalog.name} diff --git a/src/components/home/ContentItem.tsx b/src/components/home/ContentItem.tsx index 3a23b178..2e93eeac 100644 --- a/src/components/home/ContentItem.tsx +++ b/src/components/home/ContentItem.tsx @@ -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 ( <> - + ((props, re } return ( - + Continue Watching diff --git a/src/components/home/HeroCarousel.tsx b/src/components/home/HeroCarousel.tsx index 58b13981..b5108752 100644 --- a/src/components/home/HeroCarousel.tsx +++ b/src/components/home/HeroCarousel.tsx @@ -532,21 +532,20 @@ const CarouselCard: React.FC = memo(({ item, colors, logoFail {/* Static genres positioned absolutely over the card */} {item.genres && ( - - {item.genres.slice(0, 3).join(' • ')} - + + + {item.genres.slice(0, 3).join(' • ')} + + )} {/* Static action buttons positioned absolutely over the card */} - + + = memo(({ item, colors, logoFail Info + {/* Static logo positioned absolutely over the card */} diff --git a/src/components/home/ThisWeekSection.tsx b/src/components/home/ThisWeekSection.tsx index 96f14f46..b58d5b94 100644 --- a/src/components/home/ThisWeekSection.tsx +++ b/src/components/home/ThisWeekSection.tsx @@ -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 ( - + This Week diff --git a/src/contexts/AccountContext.tsx b/src/contexts/AccountContext.tsx index f82fff46..a2d36997 100644 --- a/src/contexts/AccountContext.tsx +++ b/src/contexts/AccountContext.tsx @@ -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(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(() => ({ diff --git a/src/hooks/useCalendarData.ts b/src/hooks/useCalendarData.ts index a2091468..5797ea0f 100644 --- a/src/hooks/useCalendarData.ts +++ b/src/hooks/useCalendarData.ts @@ -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, diff --git a/src/hooks/useFeaturedContent.ts b/src/hooks/useFeaturedContent.ts index ca2ecdf4..0f04e60a 100644 --- a/src/hooks/useFeaturedContent.ts +++ b/src/hooks/useFeaturedContent.ts @@ -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); diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 3a166030..16322c63 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -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); } diff --git a/src/screens/CalendarScreen.tsx b/src/screens/CalendarScreen.tsx index e2eb31a6..a3318d37 100644 --- a/src/screens/CalendarScreen.tsx +++ b/src/screens/CalendarScreen.tsx @@ -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) => { diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index ca1c5967..ee93c7e5 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -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 {memoizedThisWeekSection}; + return memoizedThisWeekSection; case 'continueWatching': return null; // Moved to ListHeaderComponent to avoid remounts on scroll case 'catalog': - return ( - - - - ); + return ; case 'placeholder': return ( @@ -701,7 +707,7 @@ const HomeScreen = () => { ); case 'welcome': - return ; + return ; 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 ; }; -export default React.memo(HomeScreenWithFocusSync); \ No newline at end of file +export default React.memo(HomeScreenWithFocusSync); + diff --git a/src/services/AccountService.ts b/src/services/AccountService.ts index 029cea1b..084af94f 100644 --- a/src/services/AccountService.ts +++ b/src/services/AccountService.ts @@ -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 { - await supabase.auth.signOut(); + await AsyncStorage.removeItem(USER_DATA_KEY); await AsyncStorage.setItem(USER_SCOPE_KEY, 'local'); } async getCurrentUser(): Promise { - 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 { - 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 { diff --git a/src/services/SyncService.ts b/src/services/SyncService.ts deleted file mode 100644 index 3c4783ab..00000000 --- a/src/services/SyncService.ts +++ /dev/null @@ -1,1146 +0,0 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; -import supabase from './supabaseClient'; -import accountService from './AccountService'; -import { storageService } from './storageService'; -import { addonEmitter, ADDON_EVENTS, stremioService } from './stremioService'; -import { catalogService, StreamingContent } from './catalogService'; -// import localScraperService from './localScraperService'; -import { settingsEmitter } from '../hooks/useSettings'; -import { logger } from '../utils/logger'; -import { traktService } from './traktService'; - -type WatchProgressRow = { - user_id: string; - media_type: string; - media_id: string; - episode_id: string; - current_time_seconds: number; - duration_seconds: number; - last_updated_ms: number; - trakt_synced?: boolean; - trakt_last_synced_ms?: number | null; - trakt_progress_percent?: number | null; -}; - -const SYNC_QUEUE_KEY = '@sync_queue'; - -class SyncService { - private static instance: SyncService; - private syncing = false; - private suppressPush = false; - private realtimeChannels: any[] = []; - private pullDebounceTimer: NodeJS.Timeout | null = null; - private addonsPollInterval: NodeJS.Timeout | null = null; - private suppressLibraryPush: boolean = false; - private libraryUnsubscribe: (() => void) | null = null; - - static getInstance(): SyncService { - if (!SyncService.instance) SyncService.instance = new SyncService(); - return SyncService.instance; - } - - init(): void { - // Watch progress updates - storageService.subscribeToWatchProgressUpdates(() => { - if (this.suppressPush) return; - logger.log('[Sync] watch_progress local change → push'); - this.pushWatchProgress().catch(() => undefined); - }); - storageService.onWatchProgressRemoved((id, type, episodeId) => { - if (this.suppressPush) return; - logger.log(`[Sync] watch_progress removed → soft delete ${type}:${id}:${episodeId || ''}`); - this.softDeleteWatchProgress(type, id, episodeId).catch(() => undefined); - }); - - // Addon order and changes - addonEmitter.on(ADDON_EVENTS.ORDER_CHANGED, () => { logger.log('[Sync] addon order changed → push'); this.pushAddons(); }); - addonEmitter.on(ADDON_EVENTS.ADDON_ADDED, () => { logger.log('[Sync] addon added → push'); this.pushAddons(); }); - addonEmitter.on(ADDON_EVENTS.ADDON_REMOVED, () => { logger.log('[Sync] addon removed → push'); this.pushAddons(); }); - - // Settings updates: no realtime push; sync only on app restart - logger.log('[Sync] init completed (listeners wired; settings push disabled)'); - - // Library local change → push - if (this.libraryUnsubscribe) { - try { this.libraryUnsubscribe(); } catch {} - this.libraryUnsubscribe = null; - } - const unsubAdd = catalogService.onLibraryAdd((item) => { - if (this.suppressLibraryPush) return; - logger.log(`[Sync] library add → push ${item.type}:${item.id}`); - this.pushLibraryAdd(item).catch(() => undefined); - }); - const unsubRem = catalogService.onLibraryRemove((type, id) => { - if (this.suppressLibraryPush) return; - logger.log(`[Sync] library remove → push ${type}:${id}`); - this.pushLibraryRemove(type, id).catch(() => undefined); - }); - this.libraryUnsubscribe = () => { try { unsubAdd(); unsubRem(); } catch {} }; - } - - subscribeRealtime = async (): Promise => { - const user = await accountService.getCurrentUser(); - if (!user) return; - const userId = user.id; - const traktActive = await traktService.isAuthenticated(); - - const addChannel = (table: string, handler: (payload: any) => void) => { - const channel = supabase - .channel(`rt-${table}`) - .on('postgres_changes', { event: '*', schema: 'public', table, filter: `user_id=eq.${userId}` }, handler) - .subscribe(); - this.realtimeChannels.push(channel); - logger.log(`[Sync] Realtime subscribed: ${table}`); - }; - - // Watch progress realtime is disabled when Trakt is active - if (!traktActive) { - // Watch progress: apply granular updates (ignore self-caused pushes via suppressPush) - addChannel('watch_progress', async (payload) => { - try { - const row = (payload.new || payload.old); - if (!row) return; - const type = row.media_type as string; - const id = row.media_id as string; - const episodeId = (payload.eventType === 'DELETE') ? (row.episode_id || '') : (row.episode_id || ''); - this.suppressPush = true; - const deletedAt = (row as any).deleted_at; - if (payload.eventType === 'DELETE' || deletedAt) { - await storageService.removeWatchProgress(id, type, episodeId || undefined); - // Record tombstone with remote timestamp if available - try { - const remoteUpdated = (row as any).updated_at ? new Date((row as any).updated_at).getTime() : Date.now(); - await storageService.addWatchProgressTombstone(id, type, episodeId || undefined, remoteUpdated); - } catch {} - } else { - // Preserve the most recent timestamp between local and remote to maintain proper continue watching order - const remoteTimestamp = row.last_updated_ms || Date.now(); - const existingProgress = await storageService.getWatchProgress(id, type, (row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined); - const localTimestamp = existingProgress?.lastUpdated || 0; - - // Use the newer timestamp to maintain proper continue watching order across devices - const finalTimestamp = Math.max(remoteTimestamp, localTimestamp); - - await storageService.setWatchProgress( - id, - type, - { - currentTime: row.current_time_seconds || 0, - duration: row.duration_seconds || 0, - lastUpdated: finalTimestamp, - traktSynced: row.trakt_synced ?? undefined, - traktLastSynced: row.trakt_last_synced_ms ?? undefined, - traktProgress: row.trakt_progress_percent ?? undefined, - }, - // Ensure we pass through the full remote episode_id as-is; empty string becomes undefined - (row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined, - { preserveTimestamp: true, forceNotify: true, forceWrite: true } - ); - } - } catch {} - finally { - this.suppressPush = false; - } - }); - } else { - logger.log('[Sync] Trakt active → skipping watch_progress realtime subscription'); - } - - const debouncedPull = (payload?: any) => { - if (payload?.table) logger.log(`[Sync][rt] change on ${payload.table} → debounced fullPull`); - if (this.pullDebounceTimer) clearTimeout(this.pullDebounceTimer); - this.pullDebounceTimer = setTimeout(() => { - logger.log('[Sync] fullPull (debounced) start'); - this.fullPull() - .then(() => logger.log('[Sync] fullPull (debounced) done')) - .catch((e) => { if (__DEV__) console.warn('[Sync] fullPull (debounced) error', e); }); - }, 300); - }; - - // Addons: just re-pull snapshot quickly - addChannel('installed_addons', () => debouncedPull({ table: 'installed_addons' })); - // Library realtime: apply row-level changes - addChannel('user_library', async (payload) => { - try { - const row = (payload.new || payload.old); - if (!row) return; - const mediaType = (row.media_type as string) === 'movie' ? 'movie' : 'series'; - const mediaId = row.media_id as string; - this.suppressLibraryPush = true; - const deletedAt = (row as any).deleted_at; - if (payload.eventType === 'DELETE' || deletedAt) { - await catalogService.removeFromLibrary(mediaType, mediaId); - logger.log(`[Sync][rt] user_library DELETE ${mediaType}:${mediaId}`); - } else { - const content: StreamingContent = { - id: mediaId, - type: mediaType, - name: (row.title as string) || mediaId, - poster: (row.poster_url as string) || '', - inLibrary: true, - year: row.year ?? undefined, - } as any; - await catalogService.addToLibrary(content); - logger.log(`[Sync][rt] user_library ${payload.eventType} ${mediaType}:${mediaId}`); - } - } catch (e) { - if (__DEV__) console.warn('[Sync][rt] user_library handler error', e); - } finally { - this.suppressLibraryPush = false; - } - }); - // Excluded: local_scrapers, scraper_repository from realtime sync - logger.log('[Sync] Realtime subscriptions active'); - - // Fallback polling for addons (in case realtime isn't enabled) - if (this.addonsPollInterval) clearInterval(this.addonsPollInterval); - this.addonsPollInterval = setInterval(async () => { - try { - const u = await accountService.getCurrentUser(); - if (!u) return; - // Compare excluding preinstalled addons - const exclude = new Set(['com.linvo.cinemeta', 'org.stremio.opensubtitlesv3']); - const localIds = new Set( - (await stremioService.getInstalledAddonsAsync()) - .map((a: any) => a.id) - .filter((id: string) => !exclude.has(id)) - ); - const { data: remote } = await supabase - .from('installed_addons') - .select('addon_id') - .eq('user_id', u.id); - const remoteIds = new Set( - ((remote || []) as any[]) - .map(r => r.addon_id as string) - .filter((id: string) => !exclude.has(id)) - ); - if (localIds.size !== remoteIds.size) { - logger.log('[Sync][poll] addons mismatch by count → pull snapshot'); - await this.pullAddonsSnapshot(u.id); - return; - } - for (const id of remoteIds) { - if (!localIds.has(id)) { - logger.log('[Sync][poll] addons mismatch by set → pull snapshot'); - await this.pullAddonsSnapshot(u.id); - break; - } - } - } catch (e) { - // silent - } - }, 21600000); // Increased from 4 hours to 6 hours to reduce background CPU - }; - - unsubscribeRealtime = (): void => { - try { - logger.log(`[Sync] Realtime unsubscribe (${this.realtimeChannels.length})`); - for (const ch of this.realtimeChannels) { - try { ch.unsubscribe?.(); } catch {} - } - } finally { - this.realtimeChannels = []; - if (this.addonsPollInterval) { - clearInterval(this.addonsPollInterval); - this.addonsPollInterval = null; - } - if (this.libraryUnsubscribe) { - try { this.libraryUnsubscribe(); } catch {} - this.libraryUnsubscribe = null; - } - } - }; - - async migrateLocalScopeToUser(): Promise { - const user = await accountService.getCurrentUser(); - if (!user) return; - const userId = user.id; - const keys = await AsyncStorage.getAllKeys(); - const migrations: Array> = []; - const moveKey = async (from: string, to: string) => { - const val = await AsyncStorage.getItem(from); - if (val == null) return; - const exists = await AsyncStorage.getItem(to); - if (!exists) { - await AsyncStorage.setItem(to, val); - } else { - // Prefer the one with newer lastUpdated if JSON - try { - const a = JSON.parse(val); - const b = JSON.parse(exists); - const aLU = a?.lastUpdated ?? 0; - const bLU = b?.lastUpdated ?? 0; - if (aLU > bLU) await AsyncStorage.setItem(to, val); - } catch { - // Keep existing if equal - } - } - await AsyncStorage.removeItem(from); - }; - - // Watch progress/content durations/subtitles/app settings - for (const k of keys) { - if (k.startsWith('@user:local:@watch_progress:')) { - const suffix = k.replace('@user:local:@watch_progress:', ''); - migrations.push(moveKey(k, `@user:${userId}:@watch_progress:${suffix}`)); - } else if (k.startsWith('@user:local:@content_duration:')) { - const suffix = k.replace('@user:local:@content_duration:', ''); - migrations.push(moveKey(k, `@user:${userId}:@content_duration:${suffix}`)); - } else if (k === '@user:local:@subtitle_settings') { - migrations.push(moveKey(k, `@user:${userId}:@subtitle_settings`)); - } else if (k === 'app_settings') { - migrations.push(moveKey('app_settings', `@user:${userId}:app_settings`)); - } else if (k === '@user:local:app_settings') { - migrations.push(moveKey(k, `@user:${userId}:app_settings`)); - } else if (k === '@user:local:stremio-addons' || k === 'stremio-addons') { - migrations.push(moveKey(k, `@user:${userId}:stremio-addons`)); - } else if (k === '@user:local:stremio-addon-order') { - migrations.push(moveKey(k, `@user:${userId}:stremio-addon-order`)); - // Do NOT migrate local scraper keys; they are device-local and unscoped - } else if (k === '@user:local:local-scrapers') { - // intentionally skip - } else if (k === '@user:local:scraper-repository-url') { - // intentionally skip - } else if (k === '@user:local:stremio-library') { - migrations.push((async () => { - const val = (await AsyncStorage.getItem(k)) || '{}'; - await moveKey(k, `@user:${userId}:stremio-library`); - try { - const parsed = JSON.parse(val) as Record; - const count = Array.isArray(parsed) ? parsed.length : Object.keys(parsed || {}).length; - if (count > 0) await AsyncStorage.setItem(`@user:${userId}:library_initialized`, 'true'); - } catch {} - })()); - } else if (k === 'stremio-library') { - migrations.push((async () => { - const val = (await AsyncStorage.getItem(k)) || '{}'; - await moveKey(k, `@user:${userId}:stremio-library`); - try { - const parsed = JSON.parse(val) as Record; - const count = Array.isArray(parsed) ? parsed.length : Object.keys(parsed || {}).length; - if (count > 0) await AsyncStorage.setItem(`@user:${userId}:library_initialized`, 'true'); - } catch {} - })()); - } - } - // Migrate legacy theme keys into scoped app_settings - try { - const legacyThemeId = await AsyncStorage.getItem('current_theme'); - const legacyCustomThemesJson = await AsyncStorage.getItem('custom_themes'); - const scopedSettingsKey = `@user:${userId}:app_settings`; - let scopedSettings: any = {}; - try { scopedSettings = JSON.parse((await AsyncStorage.getItem(scopedSettingsKey)) || '{}'); } catch {} - let changed = false; - if (legacyThemeId && scopedSettings.themeId !== legacyThemeId) { - scopedSettings.themeId = legacyThemeId; - changed = true; - } - if (legacyCustomThemesJson) { - const legacyCustomThemes = JSON.parse(legacyCustomThemesJson); - if (Array.isArray(legacyCustomThemes)) { - scopedSettings.customThemes = legacyCustomThemes; - changed = true; - } - } - if (changed) { - await AsyncStorage.setItem(scopedSettingsKey, JSON.stringify(scopedSettings)); - } - } catch {} - await Promise.all(migrations); - logger.log(`[Sync] migrateLocalScopeToUser done (moved ~${migrations.length} keys)`); - } - - async fullPush(): Promise { - logger.log('[Sync] fullPush start'); - await Promise.allSettled([ - this.pushWatchProgress(), - // Settings push only at app start/sign-in handled by fullPush itself; keep here OK - this.pushSettings(), - this.pushAddons(), - // Excluded: this.pushLocalScrapers(), - this.pushLibrary(), - ]); - logger.log('[Sync] fullPush done'); - } - - async fullPull(): Promise { - logger.log('[Sync] fullPull start'); - const user = await accountService.getCurrentUser(); - if (!user) return; - const userId = user.id; - const traktActive = await traktService.isAuthenticated(); - - await Promise.allSettled([ - (!traktActive ? (async () => { - logger.log('[Sync] pull watch_progress'); - const { data: wp } = await supabase - .from('watch_progress') - .select('*') - .eq('user_id', userId) - .is('deleted_at', null); - if (wp && Array.isArray(wp)) { - const remoteActiveKeys = new Set(); - for (const row of wp as any[]) { - // Preserve the most recent timestamp between local and remote to maintain proper continue watching order - const remoteTimestamp = row.last_updated_ms || Date.now(); - const existingProgress = await storageService.getWatchProgress( - row.media_id, - row.media_type, - (row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined - ); - const localTimestamp = existingProgress?.lastUpdated || 0; - - // Use the newer timestamp to maintain proper continue watching order across devices - const finalTimestamp = Math.max(remoteTimestamp, localTimestamp); - - await storageService.setWatchProgress( - row.media_id, - row.media_type, - { - currentTime: row.current_time_seconds, - duration: row.duration_seconds, - lastUpdated: finalTimestamp, - traktSynced: row.trakt_synced ?? undefined, - traktLastSynced: row.trakt_last_synced_ms ?? undefined, - traktProgress: row.trakt_progress_percent ?? undefined, - }, - // Ensure full episode_id is preserved; treat empty as undefined - (row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined, - { preserveTimestamp: true, forceNotify: true, forceWrite: true } - ); - remoteActiveKeys.add(`${row.media_type}|${row.media_id}|${row.episode_id || ''}`); - } - // Remove any local progress not present on server (server is source of truth) - try { - const allLocal = await storageService.getAllWatchProgress(); - for (const [key] of Object.entries(allLocal)) { - const parts = key.split(':'); - const type = parts[0]; - const id = parts[1]; - const ep = parts[2] || ''; - const k = `${type}|${id}|${ep}`; - if (!remoteActiveKeys.has(k)) { - this.suppressPush = true; - await storageService.removeWatchProgress(id, type, ep || undefined); - this.suppressPush = false; - } - } - } catch {} - } - })() : Promise.resolve()), - (async () => { - logger.log('[Sync] pull user_settings'); - const { data: us } = await supabase - .from('user_settings') - .select('*') - .eq('user_id', userId) - .single(); - if (us) { - // Merge remote settings with existing local settings, preferring remote values - // but preserving any local-only keys (e.g., newly added client-side settings - // not yet present on the server). This avoids losing local preferences on restart. - try { - const localScopedJson = (await AsyncStorage.getItem(`@user:${userId}:app_settings`)) || '{}'; - const localLegacyJson = (await AsyncStorage.getItem('app_settings')) || '{}'; - // Prefer scoped local if available; fall back to legacy - let localSettings: Record = {}; - try { localSettings = JSON.parse(localScopedJson); } catch {} - if (!localSettings || Object.keys(localSettings).length === 0) { - try { localSettings = JSON.parse(localLegacyJson); } catch { localSettings = {}; } - } - - const remoteRaw: Record = (us.app_settings || {}) as Record; - // Exclude episodeLayoutStyle from remote to keep it local-only - const { episodeLayoutStyle: _remoteEpisodeLayoutStyle, ...remoteSettingsSansLocalOnly } = remoteRaw || {}; - // Merge: start from local, override with remote (sans excluded keys) - const mergedSettings = { ...(localSettings || {}), ...(remoteSettingsSansLocalOnly || {}) }; - - await AsyncStorage.setItem(`@user:${userId}:app_settings`, JSON.stringify(mergedSettings)); - await AsyncStorage.setItem('app_settings', JSON.stringify(mergedSettings)); - - // Sync continue watching removed items (stored in app_settings) - if (remoteSettingsSansLocalOnly?.continue_watching_removed) { - await AsyncStorage.setItem(`@user:${userId}:@continue_watching_removed`, JSON.stringify(remoteSettingsSansLocalOnly.continue_watching_removed)); - } - - await storageService.saveSubtitleSettings(us.subtitle_settings || {}); - // Notify listeners that settings changed due to sync - try { settingsEmitter.emit(); } catch {} - } catch (e) { - // Fallback to writing remote settings as-is if merge fails - const remoteRaw: Record = (us.app_settings || {}) as Record; - const { episodeLayoutStyle: _remoteEpisodeLayoutStyle, ...remoteSettingsSansLocalOnly } = remoteRaw || {}; - await AsyncStorage.setItem(`@user:${userId}:app_settings`, JSON.stringify(remoteSettingsSansLocalOnly)); - await AsyncStorage.setItem('app_settings', JSON.stringify(remoteSettingsSansLocalOnly)); - - // Sync continue watching removed items in fallback (stored in app_settings) - if (remoteSettingsSansLocalOnly?.continue_watching_removed) { - await AsyncStorage.setItem(`@user:${userId}:@continue_watching_removed`, JSON.stringify(remoteSettingsSansLocalOnly.continue_watching_removed)); - } - - await storageService.saveSubtitleSettings(us.subtitle_settings || {}); - try { settingsEmitter.emit(); } catch {} - } - } - })(), - this.smartPullAddons(userId), // Use smart pull instead of destructive pull - this.pullLibrary(userId), - ]); - logger.log('[Sync] fullPull done'); - } - - private async pullLibrary(userId: string): Promise { - try { - logger.log('[Sync] pull user_library'); - const { data, error } = await supabase - .from('user_library') - .select('media_type, media_id, title, poster_url, year, deleted_at, updated_at') - .eq('user_id', userId); - if (error) { - if (__DEV__) console.warn('[SyncService] pull library error', error); - return; - } - const obj: Record = {}; - for (const row of (data || []) as any[]) { - if (row.deleted_at) continue; - const key = `${row.media_type}:${row.media_id}`; - obj[key] = { - id: row.media_id, - type: row.media_type, - name: row.title || row.media_id, - poster: row.poster_url || '', - year: row.year || undefined, - inLibrary: true, - }; - } - await AsyncStorage.setItem(`@user:${userId}:stremio-library`, JSON.stringify(obj)); - await AsyncStorage.setItem('stremio-library', JSON.stringify(obj)); - logger.log(`[Sync] pull user_library wrote items=${Object.keys(obj).length}`); - } catch (e) { - if (__DEV__) console.warn('[SyncService] pullLibrary exception', e); - } - } - - private async pushLibrary(): Promise { - const user = await accountService.getCurrentUser(); - if (!user) return; - try { - const scope = (await AsyncStorage.getItem('@user:current')) || 'local'; - const json = - (await AsyncStorage.getItem(`@user:${scope}:stremio-library`)) || - (await AsyncStorage.getItem('stremio-library')) || '{}'; - const itemsObj = JSON.parse(json) as Record; - const entries = Object.values(itemsObj) as any[]; - logger.log(`[Sync] push user_library entries=${entries.length}`); - const initialized = (await AsyncStorage.getItem(`@user:${user.id}:library_initialized`)) === 'true'; - // If not initialized and local entries are 0, attempt to import from server first - if (!initialized && entries.length === 0) { - logger.log('[Sync] user_library not initialized and local empty → pulling before deletions'); - await this.pullLibrary(user.id); - const post = (await AsyncStorage.getItem(`@user:${user.id}:stremio-library`)) || '{}'; - const postObj = JSON.parse(post) as Record; - const postEntries = Object.values(postObj) as any[]; - if (postEntries.length > 0) { - await AsyncStorage.setItem(`@user:${user.id}:library_initialized`, 'true'); - } - } - // Upsert rows - if (entries.length > 0) { - const rows = entries.map((it) => ({ - user_id: user.id, - media_type: it.type === 'movie' ? 'movie' : 'series', - media_id: it.id, - title: it.name || it.title || it.id, - poster_url: it.poster || it.poster_url || null, - year: normalizeYear(it.year), - updated_at: new Date().toISOString(), - })); - const { error: upErr } = await supabase - .from('user_library') - .upsert(rows, { onConflict: 'user_id,media_type,media_id' }); - if (upErr && __DEV__) console.warn('[SyncService] push library upsert error', upErr); - else await AsyncStorage.setItem(`@user:${user.id}:library_initialized`, 'true'); - } - // No computed deletions; removals happen only via explicit user action (soft delete) - } catch (e) { - if (__DEV__) console.warn('[SyncService] pushLibrary exception', e); - } - } - - private async pushLibraryAdd(item: StreamingContent): Promise { - const user = await accountService.getCurrentUser(); - if (!user) return; - try { - const row = { - user_id: user.id, - media_type: item.type === 'movie' ? 'movie' : 'series', - media_id: item.id, - title: (item as any).name || (item as any).title || item.id, - poster_url: (item as any).poster || null, - year: normalizeYear((item as any).year), - deleted_at: null as any, - updated_at: new Date().toISOString(), - }; - const { error } = await supabase.from('user_library').upsert(row, { onConflict: 'user_id,media_type,media_id' }); - if (error && __DEV__) console.warn('[SyncService] pushLibraryAdd error', error); - } catch (e) { - if (__DEV__) console.warn('[SyncService] pushLibraryAdd exception', e); - } - } - - private async pushLibraryRemove(type: string, id: string): Promise { - const user = await accountService.getCurrentUser(); - if (!user) return; - try { - const { error } = await supabase - .from('user_library') - .update({ deleted_at: new Date().toISOString(), updated_at: new Date().toISOString() }) - .eq('user_id', user.id) - .eq('media_type', type === 'movie' ? 'movie' : 'series') - .eq('media_id', id); - if (error && __DEV__) console.warn('[SyncService] pushLibraryRemove error', error); - } catch (e) { - if (__DEV__) console.warn('[SyncService] pushLibraryRemove exception', e); - } - } - - private async pullAddonsSnapshot(userId: string): Promise { - logger.log('[Sync] pull installed_addons'); - const { data: addons, error: addonsErr } = await supabase - .from('installed_addons') - .select('*') - .eq('user_id', userId) - .order('position', { ascending: true }); - if (addonsErr) { - if (__DEV__) console.warn('[SyncService] pull addons error', addonsErr); - return; - } - if (!(addons && Array.isArray(addons))) return; - - // Start from currently installed (to preserve pre-installed like Cinemeta/OpenSubtitles) - const map = new Map(); - - for (const a of addons as any[]) { - try { - // Skip server addon if user explicitly removed it locally (tombstone) - try { - const removed = await stremioService.hasUserRemovedAddon(a.addon_id); - if (removed && a.addon_id !== 'com.linvo.cinemeta') { - continue; - } - } catch {} - let manifest = a.manifest_data; - if (!manifest) { - const urlToUse = a.original_url || a.url; - if (urlToUse) { - manifest = await stremioService.getManifest(urlToUse); - } - } - if (!manifest) { - manifest = { - id: a.addon_id, - name: a.name || a.addon_id, - version: a.version || '1.0.0', - description: a.description || '', - url: a.url || a.original_url || '', - originalUrl: a.original_url || a.url || '', - catalogs: [], - resources: [], - types: [], - }; - } - manifest.id = a.addon_id; - map.set(a.addon_id, manifest); - } catch (e) { - if (__DEV__) console.warn('[SyncService] failed to fetch manifest for', a.addon_id, e); - } - } - - // Always include preinstalled regardless of server - try { map.set('com.linvo.cinemeta', await stremioService.getManifest('https://v3-cinemeta.strem.io/manifest.json')); } catch {} - - // Only include OpenSubtitles if user hasn't explicitly removed it - const hasUserRemovedOpenSubtitles = await stremioService.hasUserRemovedAddon('org.stremio.opensubtitlesv3'); - if (!hasUserRemovedOpenSubtitles) { - try { map.set('org.stremio.opensubtitlesv3', await stremioService.getManifest('https://opensubtitles-v3.strem.io/manifest.json')); } catch {} - } - - (stremioService as any).installedAddons = map; - let order = (addons as any[]).map(a => a.addon_id); - const ensureFront = (arr: string[], id: string) => { - const idx = arr.indexOf(id); - if (idx === -1) arr.unshift(id); - else if (idx > 0) { arr.splice(idx, 1); arr.unshift(id); } - }; - ensureFront(order, 'com.linvo.cinemeta'); - - // Only ensure OpenSubtitles is in order if user hasn't removed it - if (!hasUserRemovedOpenSubtitles) { - ensureFront(order, 'org.stremio.opensubtitlesv3'); - } - // Prefer local order if it exists; otherwise use remote - try { - const userScope = `@user:${userId}:stremio-addon-order`; - const [localScopedOrder, localLegacyOrder, localGuestOrder] = await Promise.all([ - AsyncStorage.getItem(userScope), - AsyncStorage.getItem('stremio-addon-order'), - AsyncStorage.getItem('@user:local:stremio-addon-order'), - ]); - const localOrderRaw = localScopedOrder || localLegacyOrder || localGuestOrder; - if (localOrderRaw) { - const localOrder = JSON.parse(localOrderRaw) as string[]; - // Filter to only installed ids - const localFiltered = localOrder.filter(id => map.has(id)); - if (localFiltered.length > 0) { - order = localFiltered; - } - } - } catch {} - - (stremioService as any).addonOrder = order; - await (stremioService as any).saveInstalledAddons(); - await (stremioService as any).saveAddonOrder(); - // Mark addons initialized for this user to prevent destructive merges on first push - try { await AsyncStorage.setItem(`@user:${userId}:addons_initialized`, 'true'); } catch {} - // Push merged order to server to preserve across devices - try { - const rows = order.map((addonId: string, idx: number) => ({ - user_id: userId, - addon_id: addonId, - position: idx, - })); - const { error } = await supabase - .from('installed_addons') - .upsert(rows, { onConflict: 'user_id,addon_id' }); - if (error) logger.warn('[SyncService] push merged addon order error', error); - } catch (e) { - logger.warn('[SyncService] push merged addon order exception', e); - } - } - - async pushWatchProgress(): Promise { - const user = await accountService.getCurrentUser(); - if (!user) return; - // When Trakt is authenticated, disable account push for continue watching - try { - if (await traktService.isAuthenticated()) { - logger.log('[Sync] Trakt active → skipping push watch_progress'); - return; - } - } catch {} - const userId = user.id; - const unsynced = await storageService.getUnsyncedProgress(); - logger.log(`[Sync] push watch_progress rows=${unsynced.length}`); - const rows: any[] = unsynced.map(({ id, type, episodeId, progress }) => ({ - user_id: userId, - media_type: type, - media_id: id, - episode_id: episodeId || '', - current_time_seconds: Math.floor(progress.currentTime || 0), - duration_seconds: Math.floor(progress.duration || 0), - last_updated_ms: progress.lastUpdated || Date.now(), - trakt_synced: progress.traktSynced ?? undefined, - trakt_last_synced_ms: progress.traktLastSynced ?? undefined, - trakt_progress_percent: progress.traktProgress ?? undefined, - deleted_at: null, - updated_at: new Date().toISOString(), - })); - if (rows.length > 0) { - // Prevent resurrecting remotely-deleted rows when server has newer update - try { - const keys = rows.map(r => ({ media_type: r.media_type, media_id: r.media_id, episode_id: r.episode_id })); - const { data: remote } = await supabase - .from('watch_progress') - .select('media_type,media_id,episode_id,deleted_at,updated_at') - .eq('user_id', userId) - .in('media_type', keys.map(k => k.media_type)) - .in('media_id', keys.map(k => k.media_id)) - .in('episode_id', keys.map(k => k.episode_id)); - const shouldSkip = new Set(); - if (remote) { - for (const r of remote as any[]) { - const key = `${r.media_type}|${r.media_id}|${r.episode_id || ''}`; - if (r.deleted_at && r.updated_at) { - const remoteUpdatedMs = new Date(r.updated_at as string).getTime(); - // Find matching local row - const local = rows.find(x => x.media_type === r.media_type && x.media_id === r.media_id && x.episode_id === (r.episode_id || '')); - const localUpdatedMs = local?.last_updated_ms ?? 0; - if (remoteUpdatedMs >= localUpdatedMs) { - shouldSkip.add(key); - // also write a tombstone locally - try { await storageService.addWatchProgressTombstone(r.media_id, r.media_type, r.episode_id || undefined, remoteUpdatedMs); } catch {} - } - } - } - } - if (shouldSkip.size > 0) { - logger.log(`[Sync] push watch_progress skipping resurrect count=${shouldSkip.size}`); - } - // Filter rows to upsert - const filteredRows = rows.filter(r => !shouldSkip.has(`${r.media_type}|${r.media_id}|${r.episode_id}`)); - if (filteredRows.length > 0) { - const { error } = await supabase - .from('watch_progress') - .upsert(filteredRows, { onConflict: 'user_id,media_type,media_id,episode_id' }); - if (error && __DEV__) console.warn('[SyncService] push watch_progress error', error); - else logger.log('[Sync] push watch_progress upsert ok'); - } - } catch (e) { - // Fallback to normal upsert if pre-check fails - const { error } = await supabase - .from('watch_progress') - .upsert(rows, { onConflict: 'user_id,media_type,media_id,episode_id' }); - if (error && __DEV__) console.warn('[SyncService] push watch_progress error', error); - else logger.log('[Sync] push watch_progress upsert ok'); - } - } - - // Deletions occur only on explicit remove; no bulk deletions here - } - - private async softDeleteWatchProgress(type: string, id: string, episodeId?: string): Promise { - const user = await accountService.getCurrentUser(); - if (!user) return; - // When Trakt is authenticated, do not propagate deletes to account server for watch progress - try { - if (await traktService.isAuthenticated()) { - logger.log('[Sync] Trakt active → skipping softDelete watch_progress'); - return; - } - } catch {} - try { - const { error } = await supabase - .from('watch_progress') - .update({ deleted_at: new Date().toISOString(), updated_at: new Date().toISOString() }) - .eq('user_id', user.id) - .eq('media_type', type) - .eq('media_id', id) - .eq('episode_id', episodeId || ''); - if (error && __DEV__) console.warn('[SyncService] softDeleteWatchProgress error', error); - } catch (e) { - if (__DEV__) console.warn('[SyncService] softDeleteWatchProgress exception', e); - } - } - - async pushSettings(): Promise { - const user = await accountService.getCurrentUser(); - if (!user) return; - const userId = user.id; - logger.log('[Sync] push user_settings start'); - const scope = (await AsyncStorage.getItem('@user:current')) || 'local'; - const appSettingsJson = - (await AsyncStorage.getItem(`@user:${scope}:app_settings`)) || - (await AsyncStorage.getItem('app_settings')) || - '{}'; - const parsed = JSON.parse(appSettingsJson) as Record; - // Exclude local-only settings from push - const { episodeLayoutStyle: _localEpisodeLayoutStyle, ...appSettings } = parsed || {}; - const subtitleSettings = (await storageService.getSubtitleSettings()) || {}; - const continueWatchingRemoved = await storageService.getContinueWatchingRemoved(); - - // Include continue watching removed items in app_settings - const appSettingsWithRemoved = { - ...appSettings, - continue_watching_removed: continueWatchingRemoved - }; - - const { error } = await supabase.from('user_settings').upsert({ - user_id: userId, - app_settings: appSettingsWithRemoved, - subtitle_settings: subtitleSettings, - }); - if (error && __DEV__) console.warn('[SyncService] push settings error', error); - else logger.log('[Sync] push user_settings ok'); - } - - async pushAddons(): Promise { - const user = await accountService.getCurrentUser(); - if (!user) return; - const userId = user.id; - let addons = await stremioService.getInstalledAddonsAsync(); - logger.log(`[Sync] push installed_addons count=${addons.length}`); - let order = (stremioService as any).addonOrder as string[]; - - // Safety: Check if this device has ever synced addons for this user - // Only pull if this is truly a first-time sync AND remote has significantly more addons - try { - const deviceInitialized = (await AsyncStorage.getItem(`@user:${userId}:addons_initialized`)) === 'true'; - const { data: remoteBefore } = await supabase - .from('installed_addons') - .select('addon_id') - .eq('user_id', userId); - const remoteCount = (remoteBefore || []).length; - - // Only pull if: - // 1. This device hasn't initialized addons for this user - // 2. Remote has significantly more addons (not just pre-installed ones) - // 3. Local has only pre-installed addons (2 or fewer) - const hasOnlyPreInstalled = addons.length <= 2 && - addons.every(a => ['com.linvo.cinemeta', 'org.stremio.opensubtitlesv3'].includes(a.id)); - - if (!deviceInitialized && remoteCount > 2 && hasOnlyPreInstalled) { - logger.log('[Sync] Device first-time sync with only pre-installed addons → pulling before push'); - await this.pullAddonsSnapshot(userId); - // refresh local state after pull - addons = await stremioService.getInstalledAddonsAsync(); - order = (stremioService as any).addonOrder as string[]; - } else if (!deviceInitialized && remoteCount > addons.length) { - logger.log('[Sync] Device first-time sync but local has custom addons → merging instead of pulling'); - // Don't pull - merge the addons instead - await this.mergeAddonsFromServer(userId); - addons = await stremioService.getInstalledAddonsAsync(); - order = (stremioService as any).addonOrder as string[]; - } - } catch {} - - const removedListJson = (await AsyncStorage.getItem('user_removed_addons')) || '[]'; - let removedList: string[] = []; - try { removedList = JSON.parse(removedListJson); } catch { removedList = []; } - - const rows = addons.map((a: any) => ({ - user_id: userId, - addon_id: a.id, - name: a.name, - url: a.url, - original_url: a.originalUrl, - version: a.version, - description: a.description, - position: Math.max(0, order.indexOf(a.id)), - manifest_data: a, - })); - // Delete remote addons that no longer exist locally (excluding pre-installed to be safe) - // Enhanced safety: only delete if device has been initialized and user explicitly removed addons - try { - const { data: remote, error: rErr } = await supabase - .from('installed_addons') - .select('addon_id') - .eq('user_id', userId); - if (!rErr && remote) { - const deviceInitialized = (await AsyncStorage.getItem(`@user:${userId}:addons_initialized`)) === 'true'; - - // Only perform deletions if: - // 1. Device has been initialized (not first-time sync) - // 2. Local addons are not just pre-installed ones - const hasOnlyPreInstalled = addons.length <= 2 && - addons.every(a => ['com.linvo.cinemeta', 'org.stremio.opensubtitlesv3'].includes(a.id)); - - if (!deviceInitialized || hasOnlyPreInstalled) { - logger.log('[Sync] skipping deletions during first-time sync or when only pre-installed addons present'); - } else { - const localIds = new Set(addons.map((a: any) => a.id)); - const toDeletePromises = (remote as any[]) - .map(r => r.addon_id as string) - .map(async id => { - if (localIds.has(id)) return null; // Don't delete if still installed locally - // If user removed Cinemeta locally, allow server deletion as well - // If user explicitly removed this addon locally, prefer local removal and delete remotely - if (removedList.includes(id)) return id; - return id; // Delete other addons that are no longer installed locally - }); - - const toDeleteResults = await Promise.all(toDeletePromises); - const toDelete = toDeleteResults.filter(id => id !== null); - logger.log(`[Sync] push installed_addons deletions=${toDelete.length}`); - if (toDelete.length > 0) { - const del = await supabase - .from('installed_addons') - .delete() - .eq('user_id', userId) - .in('addon_id', toDelete); - if (del.error && __DEV__) console.warn('[SyncService] delete addons error', del.error); - } - } - } - } catch (e) { - if (__DEV__) console.warn('[SyncService] deletion sync for addons failed', e); - } - const { error } = await supabase.from('installed_addons').upsert(rows, { onConflict: 'user_id,addon_id' }); - if (error && __DEV__) console.warn('[SyncService] push addons error', error); - } - - // Excluded: pushLocalScrapers (local scrapers are device-local only) - - private async smartPullAddons(userId: string): Promise { - logger.log('[Sync] smartPullAddons: intelligent addon synchronization'); - try { - // Check if this device has been initialized for this user - const deviceInitialized = (await AsyncStorage.getItem(`@user:${userId}:addons_initialized`)) === 'true'; - - // Get current local addons - const localAddons = await stremioService.getInstalledAddonsAsync(); - const localAddonIds = new Set(localAddons.map(a => a.id)); - - // Get remote addons - const { data: remoteAddons } = await supabase - .from('installed_addons') - .select('*') - .eq('user_id', userId) - .order('position', { ascending: true }); - - if (!remoteAddons || remoteAddons.length === 0) { - logger.log('[Sync] smartPullAddons: no remote addons found'); - return; - } - - const remoteAddonIds = new Set(remoteAddons.map(a => a.addon_id)); - - // Determine sync strategy based on context - const hasOnlyPreInstalled = localAddons.length <= 2 && - localAddons.every(a => ['com.linvo.cinemeta', 'org.stremio.opensubtitlesv3'].includes(a.id)); - - if (!deviceInitialized && hasOnlyPreInstalled && remoteAddons.length > 2) { - // First-time sync with only pre-installed addons - safe to pull - logger.log('[Sync] smartPullAddons: first-time sync with only pre-installed → pulling'); - await this.pullAddonsSnapshot(userId); - } else if (!deviceInitialized && localAddons.length > 2) { - // First-time sync but user has custom addons - merge instead - logger.log('[Sync] smartPullAddons: first-time sync with custom addons → merging'); - await this.mergeAddonsFromServer(userId); - } else if (deviceInitialized) { - // Device already initialized - only merge missing addons - logger.log('[Sync] smartPullAddons: device initialized → merging missing addons only'); - await this.mergeMissingAddonsOnly(userId, localAddonIds, remoteAddons); - } else { - // Default case - merge - logger.log('[Sync] smartPullAddons: default case → merging'); - await this.mergeAddonsFromServer(userId); - } - - // Mark device as initialized after successful sync - if (!deviceInitialized) { - await AsyncStorage.setItem(`@user:${userId}:addons_initialized`, 'true'); - logger.log('[Sync] smartPullAddons: marked device as initialized'); - } - - } catch (e) { - logger.error('[Sync] smartPullAddons failed:', e); - } - } - - private async mergeMissingAddonsOnly(userId: string, localAddonIds: Set, remoteAddons: any[]): Promise { - logger.log('[Sync] mergeMissingAddonsOnly: adding only missing addons'); - try { - const addonsToInstall: any[] = []; - - for (const remoteAddon of remoteAddons) { - if (!localAddonIds.has(remoteAddon.addon_id)) { - // Honor local tombstone: skip addons user explicitly removed - try { - const removed = await stremioService.hasUserRemovedAddon(remoteAddon.addon_id); - if (removed && remoteAddon.addon_id !== 'com.linvo.cinemeta') { - continue; - } - } catch {} - try { - let manifest = remoteAddon.manifest_data; - if (!manifest && remoteAddon.original_url) { - manifest = await stremioService.getManifest(remoteAddon.original_url); - } - if (manifest) { - addonsToInstall.push(manifest); - } - } catch (e) { - logger.warn('[Sync] Failed to fetch manifest for missing addon:', remoteAddon.addon_id); - } - } - } - - // Install missing addons locally - for (const manifest of addonsToInstall) { - try { - await stremioService.installAddon(manifest.originalUrl || manifest.url); - logger.log('[Sync] Installed missing addon:', manifest.id); - } catch (e) { - logger.warn('[Sync] Failed to install missing addon:', manifest.id); - } - } - - logger.log(`[Sync] mergeMissingAddonsOnly completed: ${addonsToInstall.length} addons installed`); - } catch (e) { - logger.error('[Sync] mergeMissingAddonsOnly failed:', e); - } - } - - private async mergeAddonsFromServer(userId: string): Promise { - logger.log('[Sync] mergeAddonsFromServer: merging server addons with local addons'); - try { - const { data: remoteAddons } = await supabase - .from('installed_addons') - .select('*') - .eq('user_id', userId) - .order('position', { ascending: true }); - - if (!remoteAddons || remoteAddons.length === 0) return; - - // Get current local addons - const localAddons = await stremioService.getInstalledAddonsAsync(); - const localAddonIds = new Set(localAddons.map(a => a.id)); - - // Merge remote addons that aren't already local - const addonsToInstall: any[] = []; - for (const remoteAddon of remoteAddons as any[]) { - if (!localAddonIds.has(remoteAddon.addon_id)) { - // Honor local tombstone: skip addons user explicitly removed - try { - const removed = await stremioService.hasUserRemovedAddon(remoteAddon.addon_id); - if (removed && remoteAddon.addon_id !== 'com.linvo.cinemeta') { - continue; - } - } catch {} - try { - let manifest = remoteAddon.manifest_data; - if (!manifest && remoteAddon.original_url) { - manifest = await stremioService.getManifest(remoteAddon.original_url); - } - if (manifest) { - addonsToInstall.push(manifest); - } - } catch (e) { - logger.warn('[Sync] Failed to fetch manifest for remote addon:', remoteAddon.addon_id); - } - } - } - - // Install missing addons locally - for (const manifest of addonsToInstall) { - try { - await stremioService.installAddon(manifest.originalUrl || manifest.url); - logger.log('[Sync] Merged addon from server:', manifest.id); - } catch (e) { - logger.warn('[Sync] Failed to install merged addon:', manifest.id); - } - } - - logger.log(`[Sync] mergeAddonsFromServer completed: ${addonsToInstall.length} addons merged`); - } catch (e) { - logger.error('[Sync] mergeAddonsFromServer failed:', e); - } - } -} - -export const syncService = SyncService.getInstance(); -export default syncService; - -// Small helper to batch delete operations -function chunkArray(arr: T[], size: number): T[][] { - const res: T[][] = []; - for (let i = 0; i < arr.length; i += size) res.push(arr.slice(i, i + size)); - return res; -} - -// Normalize year values to integer or null -function normalizeYear(value: any): number | null { - if (value == null) return null; - if (typeof value === 'number' && Number.isInteger(value)) return value; - if (typeof value === 'string') { - // Extract first 4 consecutive digits - const m = value.match(/\d{4}/); - if (m) { - const y = parseInt(m[0], 10); - if (y >= 1900 && y <= 2100) return y; - return y; - } - } - return null; -} - diff --git a/src/services/catalogService.ts b/src/services/catalogService.ts index d2381e5b..09236b33 100644 --- a/src/services/catalogService.ts +++ b/src/services/catalogService.ts @@ -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, diff --git a/src/services/robustCalendarCache.ts b/src/services/robustCalendarCache.ts index 9ca3296d..7e9a4de3 100644 --- a/src/services/robustCalendarCache.ts +++ b/src/services/robustCalendarCache.ts @@ -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 diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts index c1a39478..3ad571a4 100644 --- a/src/services/stremioService.ts +++ b/src/services/stremioService.ts @@ -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 { - 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; diff --git a/src/services/supabaseClient.ts b/src/services/supabaseClient.ts deleted file mode 100644 index bf3949b0..00000000 --- a/src/services/supabaseClient.ts +++ /dev/null @@ -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; -