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 29ed05b..4438ec3 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 1e3a378..1ed3f86 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 6b7ddf9..72b74fd 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 b977b3f..f2b2882 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 a79fe8b..dda7ae6 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 4f55492..90eae32 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 acff384..13f0a54 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 68b2b6b..739fbfc 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 7ce788c..925188e 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 695874a..8f03753 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 de65e82..2476542 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 f48cd8b..0303110 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 b9693cb..f32047c 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 d48403b..bc419a2 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 eebf7ad..0bb4297 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 5f71916..1bf77f3 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 4f6de84..92407f1 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 bee06ef..578225f 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 4208368..cf53f52 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 34afa47..7f31cf8 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 d5a9df5..302b45f 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 2efccc3..70a03a8 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 af5bd9d..38ab336 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 4aaa708..940dccd 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 ce446f7..66437aa 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 426e2ae..e55a992 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 c450f86..761b690 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 7ec84d6..e0b31c6 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 1185a4d..a627ea0 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 81d12cd..9007654 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 dbdd835..b41d433 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 17ea551..16e1023 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 dd251f4..93d17d5 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 1a315db..176e8e6 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 c16b8d9..3e19591 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 b017bf5..cbdc61f 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 1c27650..a194108 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 055fe00..0f726c4 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 8e950cf..f97a895 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 dcdf032..cb9e160 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 bd6895d..0369553 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 b6d09cc..0369553 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 45de631..0369553 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 850078a..0369553 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 5d38cd5..0369553 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 19e5b3c..0369553 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 115594f..0369553 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 061180d..0369553 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 d5a9df5..0369553 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 ffc8aaa..5837ee4 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 4a52eaa..8fa15ef 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 d5eea9b..004058f 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 379634b..4a853b6 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 4ff0ef2..a3e794a 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 edcf4d5..3e317c1 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 4a52eaa..8fa15ef 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 e4afe63..718e4ca 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 152a8e6..147ca19 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 152a8e6..147ca19 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 bbc2ad2..2faf8a0 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 a67bc43..556ef58 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 3a87610..1eba090 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 0f242d6..47de330 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 e2f386e..5f69402 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 8e950cf..f97a895 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 e2f386e..5f69402 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 5c2b758..938011a 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 5fa6129..0369553 100644 Binary files a/assets/splash-icon.png and b/assets/splash-icon.png differ diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj index 1d96622..084418f 100644 --- a/ios/Nuvio.xcodeproj/project.pbxproj +++ b/ios/Nuvio.xcodeproj/project.pbxproj @@ -460,7 +460,7 @@ "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; - PRODUCT_BUNDLE_IDENTIFIER = com.nuviohub.app; + PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app; PRODUCT_NAME = Nuvio; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -492,7 +492,7 @@ "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; - PRODUCT_BUNDLE_IDENTIFIER = com.nuviohub.app; + PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app; PRODUCT_NAME = Nuvio; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/ios/Nuvio/Info.plist b/ios/Nuvio/Info.plist index 4ab9b8d..d701baa 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 0c67376..a0bc443 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 79e673e..2641fdf 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 6c96b1e..f07ab8e 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/contexts/AccountContext.tsx b/src/contexts/AccountContext.tsx index f82fff4..a2d3699 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/useSettings.ts b/src/hooks/useSettings.ts index 7dc62db..45812d4 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/services/AccountService.ts b/src/services/AccountService.ts index 029cea1..084af94 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 3c4783a..0000000 --- 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/stremioService.ts b/src/services/stremioService.ts index c1a3947..225d428 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); } @@ -1634,11 +1632,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 +1646,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 bf3949b..0000000 --- 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; -