diff --git a/.gitignore b/.gitignore
index efd2b6c5..6486d03e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -105,4 +105,5 @@ LibTorrent/
iTorrent/
simkl-docss
downloader.md
-server
\ No newline at end of file
+server
+Deliverables 2
\ No newline at end of file
diff --git a/App.tsx b/App.tsx
index 87b97907..40642842 100644
--- a/App.tsx
+++ b/App.tsx
@@ -50,6 +50,7 @@ import { mmkvStorage } from './src/services/mmkvStorage';
import { CampaignManager } from './src/components/promotions/CampaignManager';
import { isErrorReportingEnabledSync } from './src/services/telemetryService';
import { networkPrivacyService } from './src/services/networkPrivacyService';
+import { supabaseSyncService } from './src/services/supabaseSyncService';
// Initialize Sentry with privacy-first defaults
// Settings are loaded from telemetryService and can be controlled by user
@@ -183,6 +184,15 @@ const ThemedApp = () => {
const onboardingCompleted = await mmkvStorage.getItem('hasCompletedOnboarding');
setHasCompletedOnboarding(onboardingCompleted === 'true');
+ // Initialize Supabase auth/session and start background sync.
+ // This is intentionally non-blocking for app startup UX.
+ supabaseSyncService
+ .initialize()
+ .then(() => supabaseSyncService.startupSync())
+ .catch((error) => {
+ console.warn('[App] Supabase sync bootstrap failed:', error);
+ });
+
// Initialize update service
await UpdateService.initialize();
diff --git a/README.md b/README.md
index 671cef36..dca575ef 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-
+
diff --git a/android/.kotlin/sessions/kotlin-compiler-17947464936783636493.salive b/android/.kotlin/sessions/kotlin-compiler-17947464936783636493.salive
new file mode 100644
index 00000000..e69de29b
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 86a13334..13cefd46 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -95,8 +95,8 @@ android {
applicationId 'com.nuvio.app'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 35
- versionName "1.3.7"
+ versionCode 36
+ versionName "1.4.0"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
@@ -118,7 +118,7 @@ android {
def abiVersionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
applicationVariants.all { variant ->
variant.outputs.each { output ->
- def baseVersionCode = 35 // Current versionCode 35 from defaultConfig
+ def baseVersionCode = 36 // Current versionCode 36 from defaultConfig
def abiName = output.getFilter(com.android.build.OutputFile.ABI)
def versionCode = baseVersionCode * 100 // Base multiplier
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 15b512eb..84066f8c 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 8b78e2f9..a1907f1b 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 528962ab..5e73c55f 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 0969e6a3..81d907ff 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 7de47821..bb8a75f5 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 70320a98..409cd825 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 46b9e444..fa1a496b 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 63e85ffe..9ea4744d 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 a5a1dfa9..0d22646c 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 133272a5..7acd5195 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 a1156c2f..e152d6b3 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 705ab974..fa2054ea 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 401543d6..f1994a0d 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 111ae461..8d958805 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 76fb597c..72ed3e7e 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 5e694a8a..e0989481 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 a06585cb..6f370d8c 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 57b30c91..7ec5130e 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 ab60c474..9ac056b7 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 acdda655..6fe44f1f 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/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 9b0596a4..0c2f2351 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -3,5 +3,5 @@
containfalsedark
- 1.3.7
+ 1.4.0
\ No newline at end of file
diff --git a/app.json b/app.json
index d5510500..4fd63fba 100644
--- a/app.json
+++ b/app.json
@@ -2,7 +2,7 @@
"expo": {
"name": "Nuvio",
"slug": "nuvio",
- "version": "1.3.7",
+ "version": "1.4.0",
"orientation": "default",
"backgroundColor": "#020404",
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
@@ -17,7 +17,7 @@
"ios": {
"supportsTablet": true,
"icon": "./assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png",
- "buildNumber": "35",
+ "buildNumber": "36",
"infoPlist": {
"NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": true
@@ -35,7 +35,7 @@
"LSSupportsOpeningDocumentsInPlace": true,
"UIFileSharingEnabled": true
},
- "bundleIdentifier": "com.nuvio.app",
+ "bundleIdentifier": "com.nuvio.hub",
"associatedDomains": [],
"jsEngine": "hermes",
"appleTeamId": "8QBDZ766S3"
@@ -52,7 +52,7 @@
"android.permission.WRITE_SETTINGS"
],
"package": "com.nuvio.app",
- "versionCode": 35,
+ "versionCode": 36,
"architectures": [
"arm64-v8a",
"armeabi-v7a",
@@ -105,6 +105,6 @@
"fallbackToCacheTimeout": 30000,
"url": "https://ota.nuvioapp.space/api/manifest"
},
- "runtimeVersion": "1.3.7"
+ "runtimeVersion": "1.4.0"
}
}
diff --git a/assets/AppIcons/android/mipmap-hdpi/ic_launcher.png b/assets/AppIcons/android/mipmap-hdpi/ic_launcher.png
index fc396770..589a0139 100644
Binary files a/assets/AppIcons/android/mipmap-hdpi/ic_launcher.png and b/assets/AppIcons/android/mipmap-hdpi/ic_launcher.png differ
diff --git a/assets/AppIcons/android/mipmap-mdpi/ic_launcher.png b/assets/AppIcons/android/mipmap-mdpi/ic_launcher.png
index 02d4de67..3a9261d7 100644
Binary files a/assets/AppIcons/android/mipmap-mdpi/ic_launcher.png and b/assets/AppIcons/android/mipmap-mdpi/ic_launcher.png differ
diff --git a/assets/AppIcons/android/mipmap-xhdpi/ic_launcher.png b/assets/AppIcons/android/mipmap-xhdpi/ic_launcher.png
index 41b56cf1..3c101f3f 100644
Binary files a/assets/AppIcons/android/mipmap-xhdpi/ic_launcher.png and b/assets/AppIcons/android/mipmap-xhdpi/ic_launcher.png differ
diff --git a/assets/AppIcons/android/mipmap-xxhdpi/ic_launcher.png b/assets/AppIcons/android/mipmap-xxhdpi/ic_launcher.png
index 570413cc..4a50f3a8 100644
Binary files a/assets/AppIcons/android/mipmap-xxhdpi/ic_launcher.png and b/assets/AppIcons/android/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/assets/AppIcons/android/mipmap-xxxhdpi/ic_launcher.png b/assets/AppIcons/android/mipmap-xxxhdpi/ic_launcher.png
index a101d4a9..c72a7215 100644
Binary files a/assets/AppIcons/android/mipmap-xxxhdpi/ic_launcher.png and b/assets/AppIcons/android/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/assets/android/ic_launcher-web.png b/assets/android/ic_launcher-web.png
index 493e3b98..b2f544c0 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 558dfbae..d044d633 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 2f49c5f1..0d8e9ca8 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 1b48a3ef..589a0139 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 3524b2fc..7912de17 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-ldpi/ic_launcher_foreground.png b/assets/android/mipmap-ldpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..efd269fb
Binary files /dev/null and b/assets/android/mipmap-ldpi/ic_launcher_foreground.png differ
diff --git a/assets/android/mipmap-ldpi/ic_launcher_round.png b/assets/android/mipmap-ldpi/ic_launcher_round.png
index 02020f6f..1a964dbd 100644
Binary files a/assets/android/mipmap-ldpi/ic_launcher_round.png and b/assets/android/mipmap-ldpi/ic_launcher_round.png differ
diff --git a/assets/android/mipmap-mdpi/ic_launcher.png b/assets/android/mipmap-mdpi/ic_launcher.png
index 6c259c80..526eaffd 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 64546171..335c655b 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 f637369f..3a9261d7 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 9fb69a54..3c101f3f 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 f03be67e..f964ed8d 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 c34a4836..3c101f3f 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 d16402dd..4a50f3a8 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 8605f5e6..0e2377c7 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 749d0724..4a50f3a8 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 2e35f619..c72a7215 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 6cc901eb..dd2e6bee 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 06cdb2f5..c72a7215 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 e5dd4a6b..b2f544c0 100644
Binary files a/assets/android/playstore-icon.png and b/assets/android/playstore-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 18d719cf..1fdc796e 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 5bd9ac8e..35572c94 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 a526217a..92d5c55a 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 e2841b48..8954f0f4 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 34e2d79c..37175fd9 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 11953d6c..77d6dc8d 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 5bd9ac8e..330fb482 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 56a3b787..2cda25b7 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 c36efb6e..3678d9c9 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 c36efb6e..3678d9c9 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 d84612d1..7e8b1ce9 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 53317602..52b302e8 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 082be8c5..7e342789 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 2218f825..48877c47 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 8ac40dd4..8c09f13f 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 637db4d5..95e87d0b 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 8ac40dd4..8c09f13f 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 18b16be3..ce974813 100644
Binary files a/assets/ios/iTunesArtwork@3x.png and b/assets/ios/iTunesArtwork@3x.png differ
diff --git a/assets/nuvio-sync-icon-og.png b/assets/nuvio-sync-icon-og.png
new file mode 100644
index 00000000..28bf19e5
Binary files /dev/null and b/assets/nuvio-sync-icon-og.png differ
diff --git a/assets/player-icons/ic_player_aspect_ratio.svg b/assets/player-icons/ic_player_aspect_ratio.svg
new file mode 100644
index 00000000..12b2f7b6
--- /dev/null
+++ b/assets/player-icons/ic_player_aspect_ratio.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/player-icons/ic_player_audio_filled.svg b/assets/player-icons/ic_player_audio_filled.svg
new file mode 100644
index 00000000..2961dd6a
--- /dev/null
+++ b/assets/player-icons/ic_player_audio_filled.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/assets/player-icons/ic_player_audio_outline.svg b/assets/player-icons/ic_player_audio_outline.svg
new file mode 100644
index 00000000..87c64646
--- /dev/null
+++ b/assets/player-icons/ic_player_audio_outline.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/assets/player-icons/ic_player_episodes.svg b/assets/player-icons/ic_player_episodes.svg
new file mode 100644
index 00000000..c09c7205
--- /dev/null
+++ b/assets/player-icons/ic_player_episodes.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/player-icons/ic_player_pause.svg b/assets/player-icons/ic_player_pause.svg
new file mode 100644
index 00000000..69f83449
--- /dev/null
+++ b/assets/player-icons/ic_player_pause.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/assets/player-icons/ic_player_play.svg b/assets/player-icons/ic_player_play.svg
new file mode 100644
index 00000000..d375a176
--- /dev/null
+++ b/assets/player-icons/ic_player_play.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/player-icons/ic_player_source.svg b/assets/player-icons/ic_player_source.svg
new file mode 100644
index 00000000..1e79c2a3
--- /dev/null
+++ b/assets/player-icons/ic_player_source.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/player-icons/ic_player_subtitles.svg b/assets/player-icons/ic_player_subtitles.svg
new file mode 100644
index 00000000..bf8041e3
--- /dev/null
+++ b/assets/player-icons/ic_player_subtitles.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/text_only_og.png b/assets/text_only_og.png
new file mode 100644
index 00000000..35eca9a4
Binary files /dev/null and b/assets/text_only_og.png differ
diff --git a/docs/SUPABASE_SYNC.md b/docs/SUPABASE_SYNC.md
new file mode 100644
index 00000000..494c2ddc
--- /dev/null
+++ b/docs/SUPABASE_SYNC.md
@@ -0,0 +1,1254 @@
+# NuvioTV Supabase Sync Documentation
+
+This document describes the complete Supabase backend used by NuvioTV for cross-device data synchronization. It covers database schema, RPC functions, authentication, device linking, and integration patterns.
+
+---
+
+## Table of Contents
+
+1. [Overview](#overview)
+2. [Prerequisites](#prerequisites)
+3. [Database Schema](#database-schema)
+4. [RPC Functions](#rpc-functions)
+5. [Integration Guide](#integration-guide)
+6. [Data Models](#data-models)
+7. [Sync Behavior & Restrictions](#sync-behavior--restrictions)
+8. [Error Handling](#error-handling)
+
+---
+
+## Overview
+
+NuvioTV syncs the following data to Supabase so linked devices share the same state:
+
+| Data | Description | Trakt Override |
+|------|-------------|----------------|
+| **Plugins** | JavaScript plugin repository URLs | No (always syncs) |
+| **Addons** | Stremio-compatible addon manifest URLs | No (always syncs) |
+| **Watch Progress** | Per-movie/episode playback position | Yes (skipped when Trakt connected) |
+| **Library** | Saved movies & TV shows | Yes (skipped when Trakt connected) |
+| **Watched Items** | Permanent watched history (movies & episodes) | Yes (skipped when Trakt connected) |
+
+### Authentication Model
+
+- **Anonymous**: Auto-created account, can generate/claim sync codes
+- **Email/Password**: Full account with permanent data storage
+- **Linked Device**: A device linked to another account via sync code; reads/writes the owner's data
+
+### Security Model
+
+All data operations use **SECURITY DEFINER** RPC functions that call `get_sync_owner()` to resolve the effective user ID. This allows linked devices to transparently access the owner's data without needing direct RLS access.
+
+---
+
+## Prerequisites
+
+- Supabase project with:
+ - **Auth** enabled (anonymous sign-in + email/password)
+ - **pgcrypto** extension enabled (for `crypt()`, `gen_salt()`)
+- Environment variables:
+ - `SUPABASE_URL` — Your Supabase project URL
+ - `SUPABASE_ANON_KEY` — Your Supabase anonymous/public key
+
+---
+
+## Database Schema
+
+### Tables
+
+#### `sync_codes`
+
+Temporary codes for device linking, protected by a bcrypt-hashed PIN.
+
+```sql
+CREATE TABLE sync_codes (
+ id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
+ owner_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
+ code TEXT NOT NULL,
+ pin_hash TEXT NOT NULL,
+ is_active BOOLEAN NOT NULL DEFAULT true,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ expires_at TIMESTAMPTZ DEFAULT 'infinity'::TIMESTAMPTZ
+);
+
+ALTER TABLE sync_codes ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Users can manage own sync codes"
+ ON sync_codes FOR ALL
+ USING (auth.uid() = owner_id)
+ WITH CHECK (auth.uid() = owner_id);
+```
+
+#### `linked_devices`
+
+Maps a child device's user ID to a parent (owner) user ID.
+
+```sql
+CREATE TABLE linked_devices (
+ id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
+ owner_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
+ device_user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
+ device_name TEXT,
+ linked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ UNIQUE(owner_id, device_user_id)
+);
+
+ALTER TABLE linked_devices ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Owners can read their linked devices"
+ ON linked_devices FOR SELECT
+ USING (auth.uid() = owner_id);
+
+CREATE POLICY "Devices can read their own link"
+ ON linked_devices FOR SELECT
+ USING (auth.uid() = device_user_id);
+```
+
+#### `plugins`
+
+Plugin repository URLs synced across devices.
+
+```sql
+CREATE TABLE plugins (
+ id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
+ user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
+ url TEXT NOT NULL,
+ name TEXT,
+ enabled BOOLEAN NOT NULL DEFAULT true,
+ sort_order INTEGER NOT NULL DEFAULT 0,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE INDEX idx_plugins_user_id ON plugins(user_id);
+ALTER TABLE plugins ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Users can manage own plugins"
+ ON plugins FOR ALL
+ USING (auth.uid() = user_id)
+ WITH CHECK (auth.uid() = user_id);
+```
+
+#### `addons`
+
+Addon manifest URLs synced across devices.
+
+```sql
+CREATE TABLE addons (
+ id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
+ user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
+ url TEXT NOT NULL,
+ name TEXT,
+ enabled BOOLEAN NOT NULL DEFAULT true,
+ sort_order INTEGER NOT NULL DEFAULT 0,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE INDEX idx_addons_user_id ON addons(user_id);
+ALTER TABLE addons ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Users can manage own addons"
+ ON addons FOR ALL
+ USING (auth.uid() = user_id)
+ WITH CHECK (auth.uid() = user_id);
+```
+
+#### `watch_progress`
+
+Per-movie or per-episode playback progress.
+
+```sql
+CREATE TABLE watch_progress (
+ id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
+ user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
+ content_id TEXT NOT NULL,
+ content_type TEXT NOT NULL,
+ video_id TEXT NOT NULL,
+ season INTEGER,
+ episode INTEGER,
+ position BIGINT NOT NULL DEFAULT 0,
+ duration BIGINT NOT NULL DEFAULT 0,
+ last_watched BIGINT NOT NULL DEFAULT 0,
+ progress_key TEXT NOT NULL
+);
+
+CREATE INDEX idx_watch_progress_user_id ON watch_progress(user_id);
+ALTER TABLE watch_progress ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Users can manage own watch progress"
+ ON watch_progress FOR ALL
+ USING (auth.uid() = user_id)
+ WITH CHECK (auth.uid() = user_id);
+```
+
+#### `library_items`
+
+Saved movies and TV shows (bookmarks/favorites).
+
+```sql
+CREATE TABLE library_items (
+ id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
+ user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
+ content_id TEXT NOT NULL,
+ content_type TEXT NOT NULL,
+ name TEXT NOT NULL DEFAULT '',
+ poster TEXT,
+ poster_shape TEXT NOT NULL DEFAULT 'POSTER',
+ background TEXT,
+ description TEXT,
+ release_info TEXT,
+ imdb_rating REAL,
+ genres TEXT[] DEFAULT '{}',
+ addon_base_url TEXT,
+ added_at BIGINT NOT NULL DEFAULT 0,
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now(),
+ UNIQUE(user_id, content_id, content_type)
+);
+
+CREATE INDEX idx_library_items_user_id ON library_items(user_id);
+ALTER TABLE library_items ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Users can manage own library items"
+ ON library_items FOR ALL
+ USING (auth.uid() = user_id)
+ WITH CHECK (auth.uid() = user_id);
+```
+
+#### `watched_items`
+
+Permanent watched history. Unlike `watch_progress` (which is capped and stores playback position), this table is a permanent record of everything the user has watched or marked as watched. Used to determine if a movie or episode should show a "watched" checkmark.
+
+```sql
+CREATE TABLE watched_items (
+ id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
+ user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
+ content_id TEXT NOT NULL,
+ content_type TEXT NOT NULL,
+ title TEXT NOT NULL DEFAULT '',
+ season INTEGER,
+ episode INTEGER,
+ watched_at BIGINT NOT NULL,
+ created_at TIMESTAMPTZ DEFAULT now()
+);
+
+CREATE UNIQUE INDEX idx_watched_items_unique
+ ON watched_items (user_id, content_id, COALESCE(season, -1), COALESCE(episode, -1));
+
+CREATE INDEX idx_watched_items_user_id ON watched_items(user_id);
+
+ALTER TABLE watched_items ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY "Users can manage own watched items"
+ ON watched_items FOR ALL
+ USING (auth.uid() = user_id)
+ WITH CHECK (auth.uid() = user_id);
+```
+
+> **Note:** The unique index uses `COALESCE(season, -1)` and `COALESCE(episode, -1)` because PostgreSQL treats NULLs as distinct in unique constraints. Movies have `NULL` season/episode, so without COALESCE, multiple entries for the same movie would be allowed.
+
+### Triggers
+
+```sql
+-- Auto-update updated_at timestamp
+CREATE OR REPLACE FUNCTION set_updated_at()
+RETURNS TRIGGER
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ NEW.updated_at = now();
+ RETURN NEW;
+END;
+$$;
+
+-- Apply to tables with updated_at
+CREATE TRIGGER set_updated_at BEFORE UPDATE ON plugins FOR EACH ROW EXECUTE FUNCTION set_updated_at();
+CREATE TRIGGER set_updated_at BEFORE UPDATE ON addons FOR EACH ROW EXECUTE FUNCTION set_updated_at();
+CREATE TRIGGER set_updated_at BEFORE UPDATE ON sync_codes FOR EACH ROW EXECUTE FUNCTION set_updated_at();
+```
+
+---
+
+## RPC Functions
+
+### Core: `get_sync_owner()`
+
+Resolves the effective user ID. If the current user is a linked device, returns the owner's ID. Otherwise returns the caller's own ID. This is the foundation of the linked-device sync model.
+
+```sql
+CREATE OR REPLACE FUNCTION get_sync_owner()
+RETURNS UUID
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+DECLARE
+ v_owner_id uuid;
+BEGIN
+ SELECT owner_id INTO v_owner_id
+ FROM linked_devices
+ WHERE device_user_id = auth.uid()
+ LIMIT 1;
+
+ RETURN COALESCE(v_owner_id, auth.uid());
+END;
+$$;
+
+GRANT EXECUTE ON FUNCTION get_sync_owner() TO authenticated;
+```
+
+### Core: `can_access_user_data(p_user_id UUID)`
+
+Helper to check if the current user can access another user's data (either they are that user, or they are a linked device).
+
+```sql
+CREATE OR REPLACE FUNCTION can_access_user_data(p_user_id UUID)
+RETURNS BOOLEAN
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+BEGIN
+ IF auth.uid() = p_user_id THEN
+ RETURN true;
+ END IF;
+
+ IF EXISTS (
+ SELECT 1 FROM public.linked_devices
+ WHERE owner_id = p_user_id
+ AND device_user_id = auth.uid()
+ ) THEN
+ RETURN true;
+ END IF;
+
+ RETURN false;
+END;
+$$;
+
+GRANT EXECUTE ON FUNCTION can_access_user_data(UUID) TO authenticated;
+```
+
+### Device Linking: `generate_sync_code(p_pin TEXT)`
+
+Generates a sync code for the current user. If a code already exists, updates the PIN. The code format is `XXXX-XXXX-XXXX-XXXX-XXXX` (uppercase hex). PIN is bcrypt-hashed.
+
+```sql
+CREATE OR REPLACE FUNCTION generate_sync_code(p_pin TEXT)
+RETURNS TABLE(code TEXT)
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+DECLARE
+ v_user_id uuid;
+ v_existing_code text;
+ v_new_code text;
+ v_pin_hash text;
+BEGIN
+ v_user_id := auth.uid();
+
+ IF v_user_id IS NULL THEN
+ RAISE EXCEPTION 'Not authenticated';
+ END IF;
+
+ SELECT sc.code INTO v_existing_code
+ FROM sync_codes sc
+ WHERE sc.owner_id = v_user_id
+ ORDER BY sc.created_at DESC
+ LIMIT 1;
+
+ IF v_existing_code IS NOT NULL THEN
+ v_pin_hash := crypt(p_pin, gen_salt('bf'));
+ UPDATE sync_codes
+ SET pin_hash = v_pin_hash
+ WHERE sync_codes.owner_id = v_user_id
+ AND sync_codes.code = v_existing_code;
+ RETURN QUERY SELECT v_existing_code;
+ RETURN;
+ END IF;
+
+ v_new_code := upper(
+ substr(md5(random()::text || clock_timestamp()::text), 1, 4) || '-' ||
+ substr(md5(random()::text || clock_timestamp()::text), 5, 4) || '-' ||
+ substr(md5(random()::text || clock_timestamp()::text), 9, 4) || '-' ||
+ substr(md5(random()::text || clock_timestamp()::text), 13, 4) || '-' ||
+ substr(md5(random()::text || clock_timestamp()::text), 17, 4)
+ );
+
+ v_pin_hash := crypt(p_pin, gen_salt('bf'));
+
+ INSERT INTO sync_codes (owner_id, code, pin_hash)
+ VALUES (v_user_id, v_new_code, v_pin_hash);
+
+ RETURN QUERY SELECT v_new_code;
+END;
+$$;
+
+GRANT EXECUTE ON FUNCTION generate_sync_code(TEXT) TO authenticated;
+```
+
+### Device Linking: `get_sync_code(p_pin TEXT)`
+
+Retrieves the existing sync code for the current user, validated by PIN.
+
+```sql
+CREATE OR REPLACE FUNCTION get_sync_code(p_pin TEXT)
+RETURNS TABLE(code TEXT)
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+DECLARE
+ v_user_id uuid;
+ v_existing_code text;
+ v_existing_pin_hash text;
+BEGIN
+ v_user_id := auth.uid();
+
+ IF v_user_id IS NULL THEN
+ RAISE EXCEPTION 'Not authenticated';
+ END IF;
+
+ SELECT sc.code, sc.pin_hash
+ INTO v_existing_code, v_existing_pin_hash
+ FROM sync_codes sc
+ WHERE sc.owner_id = v_user_id
+ ORDER BY sc.created_at DESC
+ LIMIT 1;
+
+ IF v_existing_code IS NULL THEN
+ RAISE EXCEPTION 'No sync code found. Generate one first.';
+ END IF;
+
+ IF v_existing_pin_hash != crypt(p_pin, v_existing_pin_hash) THEN
+ RAISE EXCEPTION 'Incorrect PIN';
+ END IF;
+
+ RETURN QUERY SELECT v_existing_code;
+END;
+$$;
+
+GRANT EXECUTE ON FUNCTION get_sync_code(TEXT) TO authenticated;
+```
+
+### Device Linking: `claim_sync_code(p_code TEXT, p_pin TEXT, p_device_name TEXT)`
+
+Links the current device to the owner of the sync code. Validates the PIN, then creates a `linked_devices` row.
+
+```sql
+CREATE OR REPLACE FUNCTION claim_sync_code(p_code TEXT, p_pin TEXT, p_device_name TEXT DEFAULT NULL)
+RETURNS TABLE(result_owner_id UUID, success BOOLEAN, message TEXT)
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+DECLARE
+ v_owner_id uuid;
+ v_pin_hash text;
+BEGIN
+ SELECT sc.owner_id, sc.pin_hash
+ INTO v_owner_id, v_pin_hash
+ FROM sync_codes sc
+ WHERE sc.code = p_code;
+
+ IF v_owner_id IS NULL THEN
+ RETURN QUERY SELECT NULL::uuid, false, 'Sync code not found'::text;
+ RETURN;
+ END IF;
+
+ IF crypt(p_pin, v_pin_hash) != v_pin_hash THEN
+ RETURN QUERY SELECT NULL::uuid, false, 'Incorrect PIN'::text;
+ RETURN;
+ END IF;
+
+ INSERT INTO linked_devices (owner_id, device_user_id, device_name)
+ VALUES (v_owner_id, auth.uid(), p_device_name)
+ ON CONFLICT (owner_id, device_user_id) DO UPDATE
+ SET device_name = EXCLUDED.device_name;
+
+ RETURN QUERY SELECT v_owner_id, true, 'Device linked successfully'::text;
+END;
+$$;
+
+GRANT EXECUTE ON FUNCTION claim_sync_code(TEXT, TEXT, TEXT) TO authenticated;
+```
+
+### Device Linking: `unlink_device(p_device_user_id UUID)`
+
+Removes a linked device. Only the owner can unlink their devices.
+
+```sql
+CREATE OR REPLACE FUNCTION unlink_device(p_device_user_id UUID)
+RETURNS VOID
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+BEGIN
+ DELETE FROM linked_devices
+ WHERE (owner_id = auth.uid() AND device_user_id = p_device_user_id)
+ OR (device_user_id = auth.uid() AND device_user_id = p_device_user_id);
+END;
+$$;
+
+GRANT EXECUTE ON FUNCTION unlink_device(UUID) TO authenticated;
+```
+
+### Sync: `sync_push_plugins(p_plugins JSONB)`
+
+Full-replace push of plugin repository URLs.
+
+```sql
+CREATE OR REPLACE FUNCTION sync_push_plugins(p_plugins JSONB)
+RETURNS VOID
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+DECLARE
+ v_effective_user_id uuid;
+ v_plugin jsonb;
+BEGIN
+ SELECT get_sync_owner() INTO v_effective_user_id;
+
+ DELETE FROM plugins WHERE user_id = v_effective_user_id;
+
+ FOR v_plugin IN SELECT * FROM jsonb_array_elements(p_plugins)
+ LOOP
+ INSERT INTO plugins (user_id, url, name, enabled, sort_order)
+ VALUES (
+ v_effective_user_id,
+ v_plugin->>'url',
+ v_plugin->>'name',
+ COALESCE((v_plugin->>'enabled')::boolean, true),
+ (v_plugin->>'sort_order')::int
+ );
+ END LOOP;
+END;
+$$;
+
+GRANT EXECUTE ON FUNCTION sync_push_plugins(JSONB) TO authenticated;
+```
+
+### Sync: `sync_push_addons(p_addons JSONB)`
+
+Full-replace push of addon manifest URLs.
+
+```sql
+CREATE OR REPLACE FUNCTION sync_push_addons(p_addons JSONB)
+RETURNS VOID
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+DECLARE
+ v_effective_user_id uuid;
+ v_addon jsonb;
+BEGIN
+ SELECT get_sync_owner() INTO v_effective_user_id;
+
+ DELETE FROM addons WHERE user_id = v_effective_user_id;
+
+ FOR v_addon IN SELECT * FROM jsonb_array_elements(p_addons)
+ LOOP
+ INSERT INTO addons (user_id, url, sort_order)
+ VALUES (
+ v_effective_user_id,
+ v_addon->>'url',
+ (v_addon->>'sort_order')::int
+ );
+ END LOOP;
+END;
+$$;
+
+GRANT EXECUTE ON FUNCTION sync_push_addons(JSONB) TO authenticated;
+```
+
+### Sync: `sync_push_watch_progress(p_entries JSONB)`
+
+Full-replace push of watch progress entries.
+
+```sql
+CREATE OR REPLACE FUNCTION sync_push_watch_progress(p_entries JSONB)
+RETURNS VOID
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+DECLARE
+ v_effective_user_id UUID;
+BEGIN
+ v_effective_user_id := get_sync_owner();
+
+ DELETE FROM watch_progress WHERE user_id = v_effective_user_id;
+
+ INSERT INTO watch_progress (
+ user_id, content_id, content_type, video_id,
+ season, episode, position, duration, last_watched, progress_key
+ )
+ SELECT
+ v_effective_user_id,
+ (entry->>'content_id'),
+ (entry->>'content_type'),
+ (entry->>'video_id'),
+ (entry->>'season')::INTEGER,
+ (entry->>'episode')::INTEGER,
+ (entry->>'position')::BIGINT,
+ (entry->>'duration')::BIGINT,
+ (entry->>'last_watched')::BIGINT,
+ (entry->>'progress_key')
+ FROM jsonb_array_elements(p_entries) AS entry;
+END;
+$$;
+
+GRANT EXECUTE ON FUNCTION sync_push_watch_progress(JSONB) TO authenticated;
+```
+
+### Sync: `sync_pull_watch_progress()`
+
+Returns all watch progress for the effective user (owner or linked device's owner).
+
+```sql
+CREATE OR REPLACE FUNCTION sync_pull_watch_progress()
+RETURNS SETOF watch_progress
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+DECLARE
+ v_effective_user_id UUID;
+BEGIN
+ v_effective_user_id := get_sync_owner();
+ RETURN QUERY SELECT * FROM watch_progress WHERE user_id = v_effective_user_id;
+END;
+$$;
+
+GRANT EXECUTE ON FUNCTION sync_pull_watch_progress() TO authenticated;
+```
+
+### Sync: `sync_push_library(p_items JSONB)`
+
+Full-replace push of library items.
+
+```sql
+CREATE OR REPLACE FUNCTION sync_push_library(p_items JSONB)
+RETURNS VOID
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+DECLARE
+ v_effective_user_id UUID;
+BEGIN
+ v_effective_user_id := get_sync_owner();
+
+ DELETE FROM library_items WHERE user_id = v_effective_user_id;
+
+ INSERT INTO library_items (
+ user_id, content_id, content_type, name, poster, poster_shape,
+ background, description, release_info, imdb_rating, genres,
+ addon_base_url, added_at
+ )
+ SELECT
+ v_effective_user_id,
+ (item->>'content_id'),
+ (item->>'content_type'),
+ COALESCE(item->>'name', ''),
+ (item->>'poster'),
+ COALESCE(item->>'poster_shape', 'POSTER'),
+ (item->>'background'),
+ (item->>'description'),
+ (item->>'release_info'),
+ (item->>'imdb_rating')::REAL,
+ COALESCE(
+ (SELECT array_agg(g::TEXT) FROM jsonb_array_elements_text(item->'genres') AS g),
+ '{}'
+ ),
+ (item->>'addon_base_url'),
+ COALESCE((item->>'added_at')::BIGINT, EXTRACT(EPOCH FROM now())::BIGINT * 1000)
+ FROM jsonb_array_elements(p_items) AS item;
+END;
+$$;
+
+GRANT EXECUTE ON FUNCTION sync_push_library(JSONB) TO authenticated;
+```
+
+### Sync: `sync_pull_library()`
+
+Returns all library items for the effective user.
+
+```sql
+CREATE OR REPLACE FUNCTION sync_pull_library()
+RETURNS SETOF library_items
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+DECLARE
+ v_effective_user_id UUID;
+BEGIN
+ v_effective_user_id := get_sync_owner();
+ RETURN QUERY SELECT * FROM library_items WHERE user_id = v_effective_user_id;
+END;
+$$;
+
+GRANT EXECUTE ON FUNCTION sync_pull_library() TO authenticated;
+```
+
+### Sync: `sync_push_watched_items(p_items JSONB)`
+
+Full-replace push of watched items (permanent watched history).
+
+```sql
+CREATE OR REPLACE FUNCTION sync_push_watched_items(p_items JSONB)
+RETURNS VOID
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+DECLARE
+ v_effective_user_id UUID;
+BEGIN
+ v_effective_user_id := get_sync_owner();
+ DELETE FROM watched_items WHERE user_id = v_effective_user_id;
+ INSERT INTO watched_items (user_id, content_id, content_type, title, season, episode, watched_at)
+ SELECT
+ v_effective_user_id,
+ (item->>'content_id'),
+ (item->>'content_type'),
+ COALESCE(item->>'title', ''),
+ (item->>'season')::INTEGER,
+ (item->>'episode')::INTEGER,
+ (item->>'watched_at')::BIGINT
+ FROM jsonb_array_elements(p_items) AS item;
+END;
+$$;
+
+GRANT EXECUTE ON FUNCTION sync_push_watched_items(JSONB) TO authenticated;
+```
+
+### Sync: `sync_pull_watched_items()`
+
+Returns all watched items for the effective user.
+
+```sql
+CREATE OR REPLACE FUNCTION sync_pull_watched_items()
+RETURNS SETOF watched_items
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+DECLARE
+ v_effective_user_id UUID;
+BEGIN
+ v_effective_user_id := get_sync_owner();
+ RETURN QUERY SELECT * FROM watched_items WHERE user_id = v_effective_user_id;
+END;
+$$;
+
+GRANT EXECUTE ON FUNCTION sync_pull_watched_items() TO authenticated;
+```
+
+---
+
+## Integration Guide
+
+### 1. Authentication
+
+All API calls require a Supabase auth session. Initialize the Supabase client and authenticate:
+
+```
+POST {SUPABASE_URL}/auth/v1/signup
+Headers: apikey: {SUPABASE_ANON_KEY}
+Body: { "email": "user@example.com", "password": "..." }
+```
+
+Or for anonymous sign-in:
+
+```
+POST {SUPABASE_URL}/auth/v1/signup
+Headers: apikey: {SUPABASE_ANON_KEY}
+Body: {}
+```
+
+All subsequent requests include:
+```
+Headers:
+ apikey: {SUPABASE_ANON_KEY}
+ Authorization: Bearer {ACCESS_TOKEN}
+```
+
+### 2. Calling RPC Functions
+
+All RPCs are called via the Supabase PostgREST endpoint:
+
+```
+POST {SUPABASE_URL}/rest/v1/rpc/{function_name}
+Headers:
+ apikey: {SUPABASE_ANON_KEY}
+ Authorization: Bearer {ACCESS_TOKEN}
+ Content-Type: application/json
+Body: { ...parameters... }
+```
+
+### 3. Device Linking Flow
+
+**Device A (Parent) — Generate Sync Code:**
+
+```json
+// POST /rest/v1/rpc/generate_sync_code
+{ "p_pin": "1234" }
+
+// Response:
+[{ "code": "A1B2-C3D4-E5F6-G7H8-I9J0" }]
+```
+
+**Device B (Child) — Claim Sync Code:**
+
+```json
+// POST /rest/v1/rpc/claim_sync_code
+{
+ "p_code": "A1B2-C3D4-E5F6-G7H8-I9J0",
+ "p_pin": "1234",
+ "p_device_name": "Living Room TV"
+}
+
+// Response:
+[{
+ "result_owner_id": "uuid-of-device-a-user",
+ "success": true,
+ "message": "Device linked successfully"
+}]
+```
+
+After claiming, Device B's `get_sync_owner()` will return Device A's user ID, so all push/pull operations operate on the shared data.
+
+**Retrieve Existing Code (with PIN):**
+
+```json
+// POST /rest/v1/rpc/get_sync_code
+{ "p_pin": "1234" }
+
+// Response:
+[{ "code": "A1B2-C3D4-E5F6-G7H8-I9J0" }]
+```
+
+**Get Linked Devices:**
+
+```
+GET {SUPABASE_URL}/rest/v1/linked_devices?select=*&owner_id=eq.{your_user_id}
+```
+
+**Unlink a Device:**
+
+```json
+// POST /rest/v1/rpc/unlink_device
+{ "p_device_user_id": "uuid-of-device-to-unlink" }
+```
+
+### 4. Pushing Data
+
+All push RPCs use a **full-replace** strategy: existing data for the effective user is deleted, then the new data is inserted. This means you must always push the **complete** local dataset, not just changes.
+
+#### Push Plugins
+
+```json
+// POST /rest/v1/rpc/sync_push_plugins
+{
+ "p_plugins": [
+ {
+ "url": "https://example.com/plugin-repo",
+ "name": "My Plugin Repo",
+ "enabled": true,
+ "sort_order": 0
+ }
+ ]
+}
+```
+
+#### Push Addons
+
+```json
+// POST /rest/v1/rpc/sync_push_addons
+{
+ "p_addons": [
+ {
+ "url": "https://example.com/addon/manifest.json",
+ "sort_order": 0
+ }
+ ]
+}
+```
+
+#### Push Watch Progress
+
+```json
+// POST /rest/v1/rpc/sync_push_watch_progress
+{
+ "p_entries": [
+ {
+ "content_id": "tt1234567",
+ "content_type": "movie",
+ "video_id": "tt1234567",
+ "season": null,
+ "episode": null,
+ "position": 3600000,
+ "duration": 7200000,
+ "last_watched": 1700000000000,
+ "progress_key": "tt1234567"
+ },
+ {
+ "content_id": "tt7654321",
+ "content_type": "series",
+ "video_id": "tt7654321:2:5",
+ "season": 2,
+ "episode": 5,
+ "position": 1800000,
+ "duration": 3600000,
+ "last_watched": 1700000000000,
+ "progress_key": "tt7654321_s2e5"
+ }
+ ]
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `content_id` | string | IMDB ID or content identifier |
+| `content_type` | string | `"movie"` or `"series"` |
+| `video_id` | string | Video stream identifier |
+| `season` | int/null | Season number (null for movies) |
+| `episode` | int/null | Episode number (null for movies) |
+| `position` | long | Playback position in milliseconds |
+| `duration` | long | Total duration in milliseconds |
+| `last_watched` | long | Unix timestamp in milliseconds |
+| `progress_key` | string | Unique key: `contentId` for movies, `contentId_s{S}e{E}` for episodes |
+
+#### Push Library Items
+
+```json
+// POST /rest/v1/rpc/sync_push_library
+{
+ "p_items": [
+ {
+ "content_id": "tt1234567",
+ "content_type": "movie",
+ "name": "Example Movie",
+ "poster": "https://image.tmdb.org/t/p/w500/poster.jpg",
+ "poster_shape": "POSTER",
+ "background": "https://image.tmdb.org/t/p/original/backdrop.jpg",
+ "description": "A great movie about...",
+ "release_info": "2024",
+ "imdb_rating": 8.5,
+ "genres": ["Action", "Thriller"],
+ "addon_base_url": "https://example.com/addon"
+ }
+ ]
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `content_id` | string | Yes | IMDB ID or content identifier |
+| `content_type` | string | Yes | `"movie"` or `"series"` |
+| `name` | string | No | Display name (defaults to `""`) |
+| `poster` | string | No | Poster image URL |
+| `poster_shape` | string | No | `"POSTER"`, `"LANDSCAPE"`, or `"SQUARE"` (defaults to `"POSTER"`) |
+| `background` | string | No | Background/backdrop image URL |
+| `description` | string | No | Content description |
+| `release_info` | string | No | Release year or date string |
+| `imdb_rating` | float | No | IMDB rating (0.0-10.0) |
+| `genres` | string[] | No | Genre list (defaults to `[]`) |
+| `addon_base_url` | string | No | Source addon base URL |
+| `added_at` | long | No | Timestamp in ms (defaults to current time) |
+
+#### Push Watched Items
+
+```json
+// POST /rest/v1/rpc/sync_push_watched_items
+{
+ "p_items": [
+ {
+ "content_id": "tt1234567",
+ "content_type": "movie",
+ "title": "Example Movie",
+ "season": null,
+ "episode": null,
+ "watched_at": 1700000000000
+ },
+ {
+ "content_id": "tt7654321",
+ "content_type": "series",
+ "title": "Example Series",
+ "season": 2,
+ "episode": 5,
+ "watched_at": 1700000000000
+ }
+ ]
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `content_id` | string | Yes | IMDB ID or content identifier |
+| `content_type` | string | Yes | `"movie"` or `"series"` |
+| `title` | string | No | Display name (defaults to `""`) |
+| `season` | int/null | No | Season number (null for movies) |
+| `episode` | int/null | No | Episode number (null for movies) |
+| `watched_at` | long | Yes | Unix timestamp in milliseconds |
+
+### 5. Pulling Data
+
+#### Pull Watch Progress
+
+```json
+// POST /rest/v1/rpc/sync_pull_watch_progress
+{}
+
+// Response: array of watch_progress rows
+[
+ {
+ "id": "uuid",
+ "user_id": "uuid",
+ "content_id": "tt1234567",
+ "content_type": "movie",
+ "video_id": "tt1234567",
+ "season": null,
+ "episode": null,
+ "position": 3600000,
+ "duration": 7200000,
+ "last_watched": 1700000000000,
+ "progress_key": "tt1234567"
+ }
+]
+```
+
+#### Pull Library Items
+
+```json
+// POST /rest/v1/rpc/sync_pull_library
+{}
+
+// Response: array of library_items rows
+[
+ {
+ "id": "uuid",
+ "user_id": "uuid",
+ "content_id": "tt1234567",
+ "content_type": "movie",
+ "name": "Example Movie",
+ "poster": "https://...",
+ "poster_shape": "POSTER",
+ "background": "https://...",
+ "description": "...",
+ "release_info": "2024",
+ "imdb_rating": 8.5,
+ "genres": ["Action", "Thriller"],
+ "addon_base_url": "https://...",
+ "added_at": 1700000000000,
+ "created_at": "2024-01-01T00:00:00Z",
+ "updated_at": "2024-01-01T00:00:00Z"
+ }
+]
+```
+
+#### Pull Watched Items
+
+```json
+// POST /rest/v1/rpc/sync_pull_watched_items
+{}
+
+// Response: array of watched_items rows
+[
+ {
+ "id": "uuid",
+ "user_id": "uuid",
+ "content_id": "tt1234567",
+ "content_type": "movie",
+ "title": "Example Movie",
+ "season": null,
+ "episode": null,
+ "watched_at": 1700000000000,
+ "created_at": "2024-01-01T00:00:00Z"
+ }
+]
+```
+
+#### Pull Plugins/Addons (Direct Table Query)
+
+Plugins and addons are pulled via direct table queries using the effective user ID:
+
+```
+// First, get the effective user ID
+POST /rest/v1/rpc/get_sync_owner
+{}
+// Response: "uuid-of-effective-owner"
+
+// Then query tables
+GET /rest/v1/addons?select=*&user_id=eq.{effective_user_id}&order=sort_order
+GET /rest/v1/plugins?select=*&user_id=eq.{effective_user_id}&order=sort_order
+```
+
+---
+
+## Data Models
+
+### Plugin
+
+```json
+{
+ "url": "string (required)",
+ "name": "string (optional)",
+ "enabled": "boolean (default: true)",
+ "sort_order": "integer (default: 0)"
+}
+```
+
+### Addon
+
+```json
+{
+ "url": "string (required)",
+ "sort_order": "integer (default: 0)"
+}
+```
+
+### Watch Progress Entry
+
+```json
+{
+ "content_id": "string (required)",
+ "content_type": "string (required) - 'movie' | 'series'",
+ "video_id": "string (required)",
+ "season": "integer (optional, null for movies)",
+ "episode": "integer (optional, null for movies)",
+ "position": "long (required) - playback position in ms",
+ "duration": "long (required) - total duration in ms",
+ "last_watched": "long (required) - unix timestamp in ms",
+ "progress_key": "string (required) - unique key per entry"
+}
+```
+
+### Library Item
+
+```json
+{
+ "content_id": "string (required)",
+ "content_type": "string (required) - 'movie' | 'series'",
+ "name": "string (default: '')",
+ "poster": "string (optional) - poster image URL",
+ "poster_shape": "string (default: 'POSTER') - 'POSTER' | 'LANDSCAPE' | 'SQUARE'",
+ "background": "string (optional) - backdrop image URL",
+ "description": "string (optional)",
+ "release_info": "string (optional) - release year/date",
+ "imdb_rating": "float (optional) - 0.0 to 10.0",
+ "genres": "string[] (default: []) - list of genre names",
+ "addon_base_url": "string (optional) - source addon URL",
+ "added_at": "long (default: current time) - unix timestamp in ms"
+}
+```
+
+### Watched Item
+
+```json
+{
+ "content_id": "string (required)",
+ "content_type": "string (required) - 'movie' | 'series'",
+ "title": "string (default: '') - display name",
+ "season": "integer (optional, null for movies)",
+ "episode": "integer (optional, null for movies)",
+ "watched_at": "long (required) - unix timestamp in ms"
+}
+```
+
+### Linked Device
+
+```json
+{
+ "owner_id": "uuid (required) - parent account user ID",
+ "device_user_id": "uuid (required) - this device's user ID",
+ "device_name": "string (optional) - human-readable device name",
+ "linked_at": "timestamptz (auto-set)"
+}
+```
+
+### Sync Code
+
+```json
+{
+ "owner_id": "uuid - user who generated the code",
+ "code": "string - format: XXXX-XXXX-XXXX-XXXX-XXXX",
+ "pin_hash": "string - bcrypt hash of the PIN",
+ "is_active": "boolean (default: true)",
+ "expires_at": "timestamptz (default: infinity)"
+}
+```
+
+---
+
+## Sync Behavior & Restrictions
+
+### Startup Sync Flow
+
+When the app starts and the user is authenticated (anonymous or full account):
+
+1. **Pull plugins** from remote → install any new ones locally
+2. **Pull addons** from remote → install any new ones locally
+3. If Trakt is **NOT** connected:
+ - **Pull watch progress** → merge into local (additive)
+ - **Push watch progress** → so linked devices can pull
+ - **Pull library items** → merge into local (additive)
+ - **Push library items** → so linked devices can pull
+ - **Pull watched items** → merge into local (additive)
+ - **Push watched items** → so linked devices can pull
+
+### On-Demand Sync
+
+- **Plugins/Addons**: Pushed to remote immediately when added or removed
+- **Watch Progress**: Pushed with a 2-second debounce after any playback position update
+- **Library Items**: Pushed with a 2-second debounce after add or remove
+- **Watched Items**: Pushed with a 2-second debounce after mark/unmark as watched
+
+### Merge Strategy
+
+- **Push**: Full-replace. The entire local dataset replaces the remote dataset.
+- **Pull (merge)**: Additive. Remote items not already present locally are added. Existing local items are preserved. Match keys vary by data type: `content_id` + `content_type` for library, `content_id` + `season` + `episode` for watched items.
+
+### Trakt Override
+
+When Trakt is connected:
+- **Watch progress**, **library**, and **watched items** sync via Supabase is **completely skipped**
+- Trakt becomes the source of truth for these data types
+- **Plugins** and **addons** always sync regardless of Trakt status
+
+### Push on Account Events
+
+| Event | Action |
+|-------|--------|
+| Sign up (email) | Push all local data to remote |
+| Sign in (email) | Pull all remote data to local |
+| Generate sync code | Push all local data to remote, then generate code |
+| Claim sync code | Pull all remote data from owner to local |
+
+---
+
+## Error Handling
+
+### Sync Code Errors
+
+| Error Message | Cause |
+|---------------|-------|
+| `Not authenticated` | No auth session |
+| `No sync code found. Generate one first.` | Calling `get_sync_code` before generating |
+| `Incorrect PIN` | Wrong PIN for `get_sync_code` or `claim_sync_code` |
+| `Sync code not found` | Invalid or non-existent code in `claim_sync_code` |
+| `Device linked successfully` | Success response from `claim_sync_code` |
+
+### Auth Errors
+
+| Error Message | Cause |
+|---------------|-------|
+| `Invalid login credentials` | Wrong email or password |
+| `Email not confirmed` | Email verification pending |
+| `User already registered` | Duplicate email signup |
+| `Password is too short/weak` | Password policy violation |
+| `Signup is disabled` | Admin disabled signups |
+| `Rate limit` / `Too many requests` | Too many auth attempts |
+
+### Network Errors
+
+| Error Message | Cause |
+|---------------|-------|
+| `Unable to resolve host` | No internet |
+| `Timeout` / `Timed out` | Connection timeout |
+| `Connection refused` | Server unreachable |
+| `404` | RPC function not found (missing migration) |
+| `400` / `Bad request` | Invalid parameters |
diff --git a/eas.json b/eas.json
index b208a76d..d68dbe69 100644
--- a/eas.json
+++ b/eas.json
@@ -5,13 +5,22 @@
},
"build": {
"development": {
+ "env": {
+ "SENTRY_DISABLE_AUTO_UPLOAD": "true"
+ },
"developmentClient": true,
"distribution": "internal"
},
"preview": {
+ "env": {
+ "SENTRY_DISABLE_AUTO_UPLOAD": "true"
+ },
"distribution": "internal"
},
"production": {
+ "env": {
+ "SENTRY_DISABLE_AUTO_UPLOAD": "true"
+ },
"autoIncrement": true,
"extends": "apk",
"android": {
@@ -21,12 +30,18 @@
}
},
"release": {
+ "env": {
+ "SENTRY_DISABLE_AUTO_UPLOAD": "true"
+ },
"distribution": "store",
"android": {
"buildType": "app-bundle"
}
},
"apk": {
+ "env": {
+ "SENTRY_DISABLE_AUTO_UPLOAD": "true"
+ },
"android": {
"buildType": "apk",
"gradleCommand": ":app:assembleRelease"
diff --git a/index.html b/index.html
index 749e2f0a..fe298212 100644
--- a/index.html
+++ b/index.html
@@ -779,9 +779,9 @@
01
-
Stremio Addon Support
-
Full compatibility with Stremio addons. Access your favorite content
- providers seamlessly.
+
Stremio Addon Integration
+
Supports user-installed Stremio addons for metadata and source
+ integration.
@@ -984,9 +984,9 @@
Copyright & DMCA
-
We respect the intellectual property rights of others. Since Nuvio does not host any content, we
- cannot remove content from the internet. However, if you believe that the application interface
- itself infringes on your rights, please contact us.
+
We respect the intellectual property rights of others. Nuvio does not host media content.
+ If you believe this project's code, assets, or interface infringes your rights, please submit
+ a notice through the official project contact channels listed on this site and repository.