Merge upstream/main into DNS and resolve conflicts in App.tsx

This commit is contained in:
paregi12 2026-02-18 13:10:58 +05:30
commit 0d771e8914
147 changed files with 23181 additions and 2144 deletions

3
.gitignore vendored
View file

@ -105,4 +105,5 @@ LibTorrent/
iTorrent/
simkl-docss
downloader.md
server
server
Deliverables 2

10
App.tsx
View file

@ -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();

View file

@ -1,6 +1,6 @@
<div align="center">
<img src="assets/nuviotext.png" alt="Nuvio" width="300" />
<img src="https://github.com/tapframe/NuvioTV/blob/main/assets/brand/app_logo_wordmark.png" alt="Nuvio" width="300" />
<br />
<br />

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -3,5 +3,5 @@
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
<string name="expo_system_ui_user_interface_style" translatable="false">dark</string>
<string name="expo_runtime_version">1.3.7</string>
<string name="expo_runtime_version">1.4.0</string>
</resources>

View file

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 760 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 840 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#FFFFFF" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19,3 C20.5976809,3 21.9036609,4.24891996 21.9949073,5.82372721 L22,6 L22,18 C22,19.5976809 20.75108,20.9036609 19.1762728,20.9949073 L19,21 L5,21 C3.40231912,21 2.09633912,19.75108 2.00509269,18.1762728 L2,18 L2,6 C2,4.40231912 3.24891996,3.09633912 4.82372721,3.00509269 L5,3 L19,3 Z M19,5 L5,5 C4.48716416,5 4.06449284,5.38604019 4.00672773,5.88337887 L4,6 L4,18 C4,18.5128358 4.38604019,18.9355072 4.88337887,18.9932723 L5,19 L19,19 C19.5128358,19 19.9355072,18.6139598 19.9932723,18.1166211 L20,18 L20,6 C20,5.48716416 19.6139598,5.06449284 19.1166211,5.00672773 L19,5 Z M17,12 C17.5128358,12 17.9355072,12.3860402 17.9932723,12.8833789 L18,13 L18,16 C18,16.5128358 17.6139598,16.9355072 17.1166211,16.9932723 L17,17 L14,17 C13.4477153,17 13,16.5522847 13,16 C13,15.4871642 13.3860402,15.0644928 13.8833789,15.0067277 L14,15 L16,15 L16,13 C16,12.4871642 16.3860402,12.0644928 16.8833789,12.0067277 L17,12 Z M10,7 C10.5522847,7 11,7.44771525 11,8 C11,8.51283584 10.6139598,8.93550716 10.1166211,8.99327227 L10,9 L8,9 L8,11 C8,11.5128358 7.61395981,11.9355072 7.11662113,11.9932723 L7,12 C6.48716416,12 6.06449284,11.6139598 6.00672773,11.1166211 L6,11 L6,8 C6,7.48716416 6.38604019,7.06449284 6.88337887,7.00672773 L7,7 L10,7 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 6.25C11.3096 6.25 10.75 6.80964 10.75 7.5C10.75 8.19036 11.3096 8.75 12 8.75C12.6904 8.75 13.25 8.19036 13.25 7.5C13.25 6.80964 12.6904 6.25 12 6.25Z" fill="#FFFFFF"/>
<path d="M9.75 15.5C9.75 14.2574 10.7574 13.25 12 13.25C13.2426 13.25 14.25 14.2574 14.25 15.5C14.25 16.7426 13.2426 17.75 12 17.75C10.7574 17.75 9.75 16.7426 9.75 15.5Z" fill="#FFFFFF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 10C4 6.22876 4 4.34315 5.17157 3.17157C6.34315 2 8.22876 2 12 2C15.7712 2 17.6569 2 18.8284 3.17157C20 4.34315 20 6.22876 20 10V14C20 17.7712 20 19.6569 18.8284 20.8284C17.6569 22 15.7712 22 12 22C8.22876 22 6.34315 22 5.17157 20.8284C4 19.6569 4 17.7712 4 14V10ZM9.25 7.5C9.25 5.98122 10.4812 4.75 12 4.75C13.5188 4.75 14.75 5.98122 14.75 7.5C14.75 9.01878 13.5188 10.25 12 10.25C10.4812 10.25 9.25 9.01878 9.25 7.5ZM12 11.75C9.92893 11.75 8.25 13.4289 8.25 15.5C8.25 17.5711 9.92893 19.25 12 19.25C14.0711 19.25 15.75 17.5711 15.75 15.5C15.75 13.4289 14.0711 11.75 12 11.75Z" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.5" d="M4 10C4 6.22876 4 4.34315 5.17157 3.17157C6.34315 2 8.22876 2 12 2C15.7712 2 17.6569 2 18.8284 3.17157C20 4.34315 20 6.22876 20 10V14C20 17.7712 20 19.6569 18.8284 20.8284C17.6569 22 15.7712 22 12 22C8.22876 22 6.34315 22 5.17157 20.8284C4 19.6569 4 17.7712 4 14V10Z" stroke="#FFFFFF" stroke-width="1.5"/>
<path d="M14 7.5C14 8.60457 13.1046 9.5 12 9.5C10.8954 9.5 10 8.60457 10 7.5C10 6.39543 10.8954 5.5 12 5.5C13.1046 5.5 14 6.39543 14 7.5Z" stroke="#FFFFFF" stroke-width="1.5"/>
<path d="M15 15.5C15 17.1569 13.6569 18.5 12 18.5C10.3431 18.5 9 17.1569 9 15.5C9 13.8431 10.3431 12.5 12 12.5C13.6569 12.5 15 13.8431 15 15.5Z" stroke="#FFFFFF" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 919 B

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#FFFFFF" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4,8 L14,8 C15.1045695,8 16,8.8954305 16,10 L16,20 C16,21.1045695 15.1045695,22 14,22 L4,22 C2.8954305,22 2,21.1045695 2,20 L2,10 C2,8.8954305 2.8954305,8 4,8 Z M4,10 L4,20 L14,20 L14,10 L4,10 Z M17,19 L17,8 C17,7.44771525 16.5522847,7 16,7 L5,7 C5,5.8954305 5.8954305,5 7,5 L17,5 C18.1045695,5 19,5.8954305 19,7 L19,17 C19,18.1045695 18.1045695,19 17,19 Z M20,16 L20,5 C20,4.44771525 19.5522847,4 19,4 L8,4 C8,2.8954305 8.8954305,2 10,2 L20,2 C21.1045695,2 22,2.8954305 22,4 L22,14 C22,15.1045695 21.1045695,16 20,16 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 785 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 6C2 4.11438 2 3.17157 2.58579 2.58579C3.17157 2 4.11438 2 6 2C7.88562 2 8.82843 2 9.41421 2.58579C10 3.17157 10 4.11438 10 6V18C10 19.8856 10 20.8284 9.41421 21.4142C8.82843 22 7.88562 22 6 22C4.11438 22 3.17157 22 2.58579 21.4142C2 20.8284 2 19.8856 2 18V6Z" fill="#FFFFFF"/>
<path d="M14 6C14 4.11438 14 3.17157 14.5858 2.58579C15.1716 2 16.1144 2 18 2C19.8856 2 20.8284 2 21.4142 2.58579C22 3.17157 22 4.11438 22 6V18C22 19.8856 22 20.8284 21.4142 21.4142C20.8284 22 19.8856 22 18 22C16.1144 22 15.1716 22 14.5858 21.4142C14 20.8284 14 19.8856 14 18V6Z" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 816 B

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.4086 9.35258C23.5305 10.5065 23.5305 13.4935 21.4086 14.6474L8.59662 21.6145C6.53435 22.736 4 21.2763 4 18.9671L4 5.0329C4 2.72368 6.53435 1.26402 8.59661 2.38548L21.4086 9.35258Z" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 13.6493C3 16.6044 5.41766 19 8.4 19L16.5 19C18.9853 19 21 16.9839 21 14.4969C21 12.6503 19.8893 10.9449 18.3 10.25C18.1317 7.32251 15.684 5 12.6893 5C10.3514 5 8.34694 6.48637 7.5 8.5C4.8 8.9375 3 11.2001 3 13.6493Z" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 542 B

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="#FFFFFF" stroke-width="2" d="M1,12 C1,5 2.5,4 12,4 C21.5,4 23,5 23,12 C23,19 21.5,20 12,20 C2.5,20 1,19 1,12 Z M5.25,14 C5.25,15.5 6,16 7.75,16 C9.5,16 10.25,15.5 10.25,14 L9.97861679,14 C9.97861671,15.25 8.97905547,16 7.75,16 C6.52094453,16 5.52138329,15.25 5.52138321,14 L5.52138321,10 C5.5,8.75 6.5,8 7.75,8 C9,8 10,8.75 9.97861679,10 L10.25,10 C10.25,8.75 9.2286998,8 7.75,8 C6.2713002,8 5.25,8.75 5.25,10 L5.25,14 Z M13.25,14 C13.25,15.5 14,16 15.75,16 C17.5,16 18.25,15.5 18.25,14 L17.9786168,14 C17.9786167,15.25 16.9790555,16 15.75,16 C14.5209445,16 13.5213833,15.25 13.5213832,14 L13.5213832,10 C13.5,8.75 14.5,8 15.75,8 C17,8 18,8.75 17.9786168,10 L18.25,10 C18.25,8.75 17.2286998,8 15.75,8 C14.2713002,8 13.25,8.75 13.25,10 L13.25,14 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 994 B

BIN
assets/text_only_og.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

1254
docs/SUPABASE_SYNC.md Normal file

File diff suppressed because it is too large Load diff

View file

@ -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"

View file

@ -779,9 +779,9 @@
<div class="features-grid">
<div class="feature-item">
<div class="feature-number">01</div>
<h3 class="feature-title">Stremio Addon Support</h3>
<p class="feature-desc">Full compatibility with Stremio addons. Access your favorite content
providers seamlessly.</p>
<h3 class="feature-title">Stremio Addon Integration</h3>
<p class="feature-desc">Supports user-installed Stremio addons for metadata and source
integration.</p>
</div>
<div class="feature-item">
@ -984,9 +984,9 @@
<div class="privacy-section">
<h2>Copyright & DMCA</h2>
<p>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.</p>
<p>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.</p>
</div>
<div class="privacy-section">
@ -1157,4 +1157,4 @@
</script>
</body>
</html>
</html>

View file

@ -689,7 +689,7 @@
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.hub;
PRODUCT_NAME = Nuvio;
SUPPORTS_MACCATALYST = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 122 KiB

View file

@ -39,7 +39,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>35</string>
<string>36</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>

View file

@ -13,8 +13,8 @@
"name": "Nuvio",
"bundleIdentifier": "com.nuvio.app",
"developerName": "Tapframe",
"subtitle": "Streaming app for movies and TV shows",
"localizedDescription": "Nuvio is a comprehensive streaming application that provides access to a vast library of movies and TV shows.",
"subtitle": "Media player and discovery app",
"localizedDescription": "Nuvio is a media player and metadata discovery application for user-provided and user-installed sources.",
"iconURL": "https://github.com/tapframe/NuvioStreaming/blob/main/assets/android/playstore-icon.png?raw=true",
"tintColor": "#04dcfc",
"category": "entertainment",
@ -264,4 +264,4 @@
}
],
"news": []
}
}

66
package-lock.json generated
View file

@ -1540,7 +1540,6 @@
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.9.0"
}
@ -2098,7 +2097,6 @@
"resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-6.1.2.tgz",
"integrity": "sha512-nvM+Qv45QH7pmYvP8JB1G8JpScrWND3KrMA6ZKe62cwwNiX/BjHU28Ear0v/4bQWXlOY0mv6B8CDIm8JxXde9g==",
"license": "MIT",
"peer": true,
"dependencies": {
"anser": "^1.4.9",
"pretty-format": "^29.7.0",
@ -2587,7 +2585,6 @@
"resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz",
"integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/core": "^0.22.12"
}
@ -2773,7 +2770,6 @@
"resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.13.5.tgz",
"integrity": "sha512-4U5okwjRqDPkjB572hfZtLXJ/LGfCo6vDwUB2KIPEUoSgqbIlw+UrbnaqVp3GS+dRvhMD27F2JObpHpYRlpF0Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@lottiefiles/dotlottie-web": "0.44.0"
},
@ -3129,7 +3125,7 @@
"version": "0.72.8",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.72.8.tgz",
"integrity": "sha512-J3Q4Bkuo99k7mu+jPS9gSUSgq+lLRSI/+ahXNwV92XgJ/8UgOTxu2LPwhJnBk/sQKxq7E8WkZBnBiozukQMqrw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"invariant": "^2.2.4",
@ -3250,7 +3246,6 @@
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.25.tgz",
"integrity": "sha512-zQeWK9txDePWbYfqTs0C6jeRdJTm/7VhQtW/1IbJNDi9/rFIRzZule8bdQPAnf8QWUsNujRmi1J9OG/hhfbalg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@react-navigation/core": "^7.13.6",
"escape-string-regexp": "^4.0.0",
@ -3892,7 +3887,6 @@
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0",
@ -4113,7 +4107,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@ -4123,9 +4116,8 @@
"version": "0.72.8",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.72.8.tgz",
"integrity": "sha512-St6xA7+EoHN5mEYfdWnfYt0e8u6k2FR0P9s2arYgakQGFgU1f9FlPrIEcj0X24pLCF5c5i3WVuLCUdiCYHmOoA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@react-native/virtualized-lists": "^0.72.4",
"@types/react": "*"
@ -4661,7 +4653,6 @@
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"peer": true,
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
@ -5065,7 +5056,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@ -6295,7 +6285,6 @@
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.29.tgz",
"integrity": "sha512-9C90gyOzV83y2S3XzCbRDCuKYNaiyCzuP9ketv46acHCEZn+QTamPK/DobdghoSiofCmlfoaiD6/SzfxDiHMnw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
"@expo/cli": "54.0.19",
@ -6499,7 +6488,6 @@
"resolved": "https://registry.npmjs.org/expo-device/-/expo-device-8.0.10.tgz",
"integrity": "sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA==",
"license": "MIT",
"peer": true,
"dependencies": {
"ua-parser-js": "^0.7.33"
},
@ -6527,7 +6515,6 @@
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz",
"integrity": "sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"expo": "*",
"react-native": "*"
@ -6538,7 +6525,6 @@
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.10.tgz",
"integrity": "sha512-UqyNaaLKRpj4pKAP4HZSLnuDQqueaO5tB1c/NWu5vh1/LF9ulItyyg2kF/IpeOp0DeOLk0GY0HrIXaKUMrwB+Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"fontfaceobserver": "^2.1.0"
},
@ -6634,7 +6620,6 @@
"resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-17.0.8.tgz",
"integrity": "sha512-UrdwklZBDJ+t+ZszMMiE0SXZ2eJxcquCuQcl6EvGHM9K+e6YqKVRQ+w8qE+iIB3H75v2RJy6MHAaLK+Mqeo04g==",
"license": "MIT",
"peer": true,
"dependencies": {
"rtl-detect": "^1.0.2"
},
@ -7694,7 +7679,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4"
},
@ -10601,7 +10585,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -10642,7 +10625,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@ -10700,7 +10682,6 @@
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.4.tgz",
"integrity": "sha512-bt5bz3A/+Cv46KcjV0VQa+fo7MKxs17RCcpzjftINlen4ZDUl0I6Ut+brQ2FToa5oD0IB0xvQHfmsg2EDqsZdQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/create-cache-key-function": "^29.7.0",
"@react-native/assets-registry": "0.81.4",
@ -10789,7 +10770,6 @@
"resolved": "https://registry.npmjs.org/react-native-bottom-tabs/-/react-native-bottom-tabs-1.1.0.tgz",
"integrity": "sha512-Uu1gvM3i1Hb4DjVvR/38J1QVQEs0RkPc7K6yon99HgvRWWOyLs7kjPDhUswtb8ije4pKW712skIXWJ0lgKzbyQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"react-freeze": "^1.0.0",
"sf-symbols-typescript": "^2.0.0",
@ -10820,7 +10800,6 @@
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.29.1.tgz",
"integrity": "sha512-du3qmv0e3Sm7qsd9SfmHps+AggLiylcBBQ8ztz7WUtd8ZjKs5V3kekAbi9R2W9bRLSg47Ntp4GGMYZOhikQdZA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@egjs/hammerjs": "^2.0.17",
"hoist-non-react-statics": "^3.3.0",
@ -10919,7 +10898,6 @@
"integrity": "sha512-hcvjTu9YJE9fMmnAUvhG8CxvYLpOuMQ/2eyi/S6GyrecezF6Rmk/uRQEL6v09BRFWA/xRVZNQVulQPS+2HS3mQ==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "*",
"react-native": "*"
@ -10985,7 +10963,6 @@
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.2.0.tgz",
"integrity": "sha512-frhu5b8/m/VvaMWz48V8RxcsXnE3hrlErQ5chr21MzAeDCpY4X14sQjvm+jvu3aOI+7Cz2atdRpyhhIuqxVaXg==",
"license": "MIT",
"peer": true,
"dependencies": {
"react-native-is-edge-to-edge": "1.2.1",
"semver": "7.7.3"
@ -11025,7 +11002,6 @@
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
"integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "*",
"react-native": "*"
@ -11036,7 +11012,6 @@
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.18.0.tgz",
"integrity": "sha512-mRTLWL7Uc1p/RFNveEIIrhP22oxHduC2ZnLr/2iHwBeYpGXR0rJZ7Bgc0ktxQSHRjWTPT70qc/7yd4r9960PBQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"react-freeze": "^1.0.0",
"warn-once": "^0.1.0"
@ -11051,7 +11026,6 @@
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.1.tgz",
"integrity": "sha512-ZUD1xwc3Hwo4cOmOLumjJVoc7lEf9oQFlHnLmgccLC19fNm6LVEdtB+Cnip6gEi0PG3wfvVzskViEtrySQP8Fw==",
"license": "MIT",
"peer": true,
"dependencies": {
"css-select": "^5.1.0",
"css-tree": "^1.1.3",
@ -11280,7 +11254,6 @@
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
"integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.6",
"@react-native/normalize-colors": "^0.74.1",
@ -11537,7 +11510,6 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -13004,24 +12976,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/tldts": {
"version": "7.0.19",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz",
"integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==",
"license": "MIT",
"dependencies": {
"tldts-core": "^7.0.19"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "7.0.19",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz",
"integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==",
"license": "MIT"
},
"node_modules/tmp": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
@ -13076,19 +13030,6 @@
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/tough-cookie": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
"integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"tldts": "^7.0.5"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@ -13163,9 +13104,8 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View file

@ -6,7 +6,7 @@
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios",
"build": "export NODE_ENV=production && cd android && ./gradlew assembleRelease",
"build": "export NODE_ENV=production && export SENTRY_DISABLE_AUTO_UPLOAD=true && cd android && ./gradlew assembleRelease",
"postinstall": "patch-package"
},
"dependencies": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 153 KiB

View file

@ -30,6 +30,7 @@ import Animated, {
Extrapolation,
useAnimatedScrollHandler,
SharedValue,
useAnimatedReaction,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { StreamingContent } from '../../services/catalogService';
@ -163,10 +164,25 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
const [shouldResume, setShouldResume] = useState(false);
const [type, setType] = useState<'movie' | 'series'>('movie');
// Create internal scrollY if not provided externally
// Shared value for scroll position (for parallax effects)
const internalScrollY = useSharedValue(0);
const scrollY = externalScrollY || internalScrollY;
const [isOutOfView, setIsOutOfView] = useState(false);
// Track if hero is in view
useAnimatedReaction(
() => scrollY.value,
(currentScrollY) => {
// If hero is more than 80% scrolled out of view, consider it out of view
const outOfView = currentScrollY > HERO_HEIGHT * 0.8;
if (outOfView !== isOutOfView) {
runOnJS(setIsOutOfView)(outOfView);
}
},
[isOutOfView]
);
// Determine items to display
const items = useMemo(() => {
if (allFeaturedContent && allFeaturedContent.length > 0) {
@ -354,9 +370,9 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
}
}, [currentItem, loading, heroOpacity]);
// Stop trailer when screen loses focus
// Stop trailer when screen loses focus or scrolled out of view
useEffect(() => {
if (!isFocused) {
if (!isFocused || isOutOfView) {
// Pause this screen's trailer
setTrailerShouldBePaused(true);
setTrailerPlaying(false);
@ -365,20 +381,24 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
trailerOpacity.value = withTiming(0, { duration: 300 });
thumbnailOpacity.value = withTiming(1, { duration: 300 });
logger.info('[AppleTVHero] Screen lost focus - pausing trailer');
if (!isFocused) {
logger.info('[AppleTVHero] Screen lost focus - pausing trailer');
} else {
logger.info('[AppleTVHero] Scrolled out of view - pausing trailer');
}
} else {
// Screen gained focus - allow trailer to resume if it was ready
// Screen gained focus and is in view - allow trailer to resume if it was ready
setTrailerShouldBePaused(false);
// If trailer was ready and loaded, restore the video opacity
if (trailerReady && trailerUrl) {
logger.info('[AppleTVHero] Screen gained focus - restoring trailer');
logger.info('[AppleTVHero] Screen in focus and in view - restoring trailer');
thumbnailOpacity.value = withTiming(0, { duration: 800 });
trailerOpacity.value = withTiming(1, { duration: 800 });
setTrailerPlaying(true);
}
}
}, [isFocused, setTrailerPlaying, trailerOpacity, thumbnailOpacity, trailerReady, trailerUrl]);
}, [isFocused, isOutOfView, setTrailerPlaying, trailerOpacity, thumbnailOpacity, trailerReady, trailerUrl]);
// Listen to navigation events to stop trailer when navigating to other screens
useEffect(() => {
@ -714,7 +734,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
updateSetting('trailerMuted', !trailerMuted);
}, [trailerMuted, updateSetting]);
// Auto-advance timer - PAUSE when trailer is playing
// Auto-advance timer - PAUSE when trailer is playing or out of view
const startAutoPlay = useCallback(() => {
if (autoPlayTimerRef.current) {
clearTimeout(autoPlayTimerRef.current);
@ -722,16 +742,20 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
if (items.length <= 1) return;
// Don't auto-advance if trailer is playing
if (globalTrailerPlaying && trailerReady) {
logger.info('[AppleTVHero] Auto-rotation paused - trailer is playing');
// Don't auto-advance if trailer is playing or out of view
if ((globalTrailerPlaying && trailerReady) || isOutOfView) {
if (isOutOfView) {
logger.info('[AppleTVHero] Auto-rotation paused - out of view');
} else {
logger.info('[AppleTVHero] Auto-rotation paused - trailer is playing');
}
return;
}
autoPlayTimerRef.current = setTimeout(() => {
const timeSinceInteraction = Date.now() - lastInteractionRef.current;
// Only auto-advance if user hasn't interacted recently (5 seconds) and no trailer playing
if (timeSinceInteraction >= 5000 && (!globalTrailerPlaying || !trailerReady)) {
if (timeSinceInteraction >= 5000 && (!globalTrailerPlaying || !trailerReady) && !isOutOfView) {
// Set next index preview for crossfade
const nextIdx = (currentIndex + 1) % items.length;
setNextIndex(nextIdx);
@ -757,7 +781,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
startAutoPlay();
}
}, 25000); // Auto-advance every 25 seconds
}, [items.length, globalTrailerPlaying, trailerReady, currentIndex, dragDirection, dragProgress]);
}, [items.length, globalTrailerPlaying, trailerReady, currentIndex, dragDirection, dragProgress, isOutOfView]);
useEffect(() => {
startAutoPlay();

View file

@ -119,6 +119,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
const [loading, setLoading] = useState(true);
const appState = useRef(AppState.currentState);
const refreshTimerRef = useRef<NodeJS.Timeout | null>(null);
const pendingRefreshRef = useRef(false);
const [deletingItemId, setDeletingItemId] = useState<string | null>(null);
const longPressTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@ -326,6 +327,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
// Modified loadContinueWatching to render incrementally
const loadContinueWatching = useCallback(async (isBackgroundRefresh = false) => {
if (isRefreshingRef.current) {
pendingRefreshRef.current = true;
return;
}
@ -368,6 +370,20 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
return candidateProgress > existingProgress;
};
const compareCwItems = (a: ContinueWatchingItem, b: ContinueWatchingItem): number => {
const aProgress = a.progress ?? 0;
const bProgress = b.progress ?? 0;
const aIsUpNext = a.type === 'series' && aProgress <= 0;
const bIsUpNext = b.type === 'series' && bProgress <= 0;
// Keep active in-progress items ahead of "Up Next" placeholders.
if (aIsUpNext !== bIsUpNext) {
return aIsUpNext ? 1 : -1;
}
return (b.lastUpdated ?? 0) - (a.lastUpdated ?? 0);
};
type LocalProgressEntry = {
episodeId?: string;
season?: number;
@ -466,7 +482,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
}
const merged = Array.from(map.values());
merged.sort((a, b) => (b.lastUpdated ?? 0) - (a.lastUpdated ?? 0));
merged.sort(compareCwItems);
return merged;
});
@ -1272,7 +1288,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
});
// Sort by lastUpdated descending and set directly
adjustedItems.sort((a, b) => (b.lastUpdated ?? 0) - (a.lastUpdated ?? 0));
adjustedItems.sort(compareCwItems);
// Debug final order (only if changed)
try {
@ -1515,7 +1531,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
return it;
});
adjustedItems.sort((a, b) => (b.lastUpdated ?? 0) - (a.lastUpdated ?? 0));
adjustedItems.sort(compareCwItems);
setContinueWatchingItems(adjustedItems);
} catch (err) {
logger.error('[SimklSync] Error in Simkl merge:', err);
@ -1529,6 +1545,12 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
} finally {
setLoading(false);
isRefreshingRef.current = false;
if (pendingRefreshRef.current) {
pendingRefreshRef.current = false;
setTimeout(() => {
loadContinueWatching(true);
}, 0);
}
}
}, [getCachedMetadata]);
@ -1602,6 +1624,13 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
// Initial load
useEffect(() => {
loadContinueWatching();
const trailingRefreshId = setTimeout(() => {
loadContinueWatching(true);
}, 4000);
return () => {
clearTimeout(trailingRefreshId);
};
}, [loadContinueWatching]);
// Refresh on screen focus (lightweight, no polling)
@ -1879,7 +1908,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
}, [computedPosterWidth]);
// Memoized render function for poster-style continue watching items
const renderPosterStyleItem = useCallback(({ item }: { item: ContinueWatchingItem }) => (
const renderPosterStyleItem = useCallback(({ item }: { item: ContinueWatchingItem }) => {
return (
<TouchableOpacity
style={[
styles.posterContentItem,
@ -1978,10 +2008,12 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
)}
</View>
</TouchableOpacity>
), [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedPosterWidth, computedPosterHeight, isTV, isLargeTablet, settings.posterBorderRadius]);
);
}, [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedPosterWidth, computedPosterHeight, isTV, isLargeTablet, settings.posterBorderRadius]);
// Memoized render function for wide-style continue watching items
const renderWideStyleItem = useCallback(({ item }: { item: ContinueWatchingItem }) => (
const renderWideStyleItem = useCallback(({ item }: { item: ContinueWatchingItem }) => {
return (
<TouchableOpacity
style={[
styles.wideContentItem,
@ -2143,7 +2175,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
)}
</View>
</TouchableOpacity>
), [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedItemWidth, computedItemHeight, isTV, isLargeTablet, isTablet, settings.posterBorderRadius]);
);
}, [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedItemWidth, computedItemHeight, isTV, isLargeTablet, isTablet, settings.posterBorderRadius, t]);
// Choose the appropriate render function based on settings
const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => {
@ -2190,7 +2223,14 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
</View>
<FlatList
data={[...continueWatchingItems].sort((a, b) => (b.lastUpdated ?? 0) - (a.lastUpdated ?? 0))}
data={[...continueWatchingItems].sort((a, b) => {
const aProgress = a.progress ?? 0;
const bProgress = b.progress ?? 0;
const aIsUpNext = a.type === 'series' && aProgress <= 0;
const bIsUpNext = b.type === 'series' && bProgress <= 0;
if (aIsUpNext !== bIsUpNext) return aIsUpNext ? 1 : -1;
return (b.lastUpdated ?? 0) - (a.lastUpdated ?? 0);
})}
renderItem={renderContinueWatchingItem}
keyExtractor={keyExtractor}
horizontal

View file

@ -11,7 +11,8 @@ import {
usePlayerModals,
useSpeedControl,
useOpeningAnimation,
useWatchProgress
useWatchProgress,
useSkipSegments
} from './hooks';
// Android-specific hooks
@ -222,6 +223,16 @@ const AndroidVideoPlayer: React.FC = () => {
const nextEpisodeHook = useNextEpisode(type, season, episode, groupedEpisodes, (metadataResult as any)?.groupedEpisodes, episodeId);
const { segments: skipIntervals, outroSegment } = useSkipSegments({
imdbId: imdbId || (id?.startsWith('tt') ? id : undefined),
type,
season,
episode,
malId: (metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id,
kitsuId: id?.startsWith('kitsu:') ? id.split(':')[1] : undefined,
enabled: settings.skipIntroEnabled
});
const fadeAnim = useRef(new Animated.Value(1)).current;
useEffect(() => {
@ -975,6 +986,7 @@ const AndroidVideoPlayer: React.FC = () => {
episode={episode}
malId={(metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id}
kitsuId={id?.startsWith('kitsu:') ? id.split(':')[1] : undefined}
skipIntervals={skipIntervals}
currentTime={playerState.currentTime}
onSkip={(endTime) => controlsHook.seekToTime(endTime)}
controlsVisible={playerState.showControls}
@ -1002,6 +1014,7 @@ const AndroidVideoPlayer: React.FC = () => {
metadata={metadataResult?.metadata ? { poster: metadataResult.metadata.poster, id: metadataResult.metadata.id } : undefined}
controlsVisible={playerState.showControls}
controlsFixedOffset={100}
outroSegment={outroSegment}
/>
</View>

View file

@ -36,7 +36,8 @@ import {
usePlayerControls,
usePlayerSetup,
useWatchProgress,
useNextEpisode
useNextEpisode,
useSkipSegments
} from './hooks';
// Platform-specific hooks
@ -209,6 +210,16 @@ const KSPlayerCore: React.FC = () => {
episodeId
});
const { segments: skipIntervals, outroSegment } = useSkipSegments({
imdbId: imdbId || (id?.startsWith('tt') ? id : undefined),
type,
season,
episode,
malId: (metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id,
kitsuId: id?.startsWith('kitsu:') ? id.split(':')[1] : undefined,
enabled: settings.skipIntroEnabled
});
const controls = usePlayerControls({
playerRef: ksPlayerRef,
paused,
@ -945,6 +956,7 @@ const KSPlayerCore: React.FC = () => {
episode={episode}
malId={(metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id}
kitsuId={id?.startsWith('kitsu:') ? id.split(':')[1] : undefined}
skipIntervals={skipIntervals}
currentTime={currentTime}
onSkip={(endTime) => controls.seekToTime(endTime)}
controlsVisible={showControls}
@ -972,6 +984,7 @@ const KSPlayerCore: React.FC = () => {
metadata={metadata ? { poster: metadata.poster, id: metadata.id } : undefined}
controlsVisible={showControls}
controlsFixedOffset={126}
outroSegment={outroSegment}
/>
{/* Modals */}
@ -1102,4 +1115,4 @@ const KSPlayerCore: React.FC = () => {
);
};
export default KSPlayerCore;
export default KSPlayerCore;

View file

@ -4,6 +4,7 @@ import { Animated } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { logger } from '../../../utils/logger';
import { LinearGradient } from 'expo-linear-gradient';
import { SkipInterval } from '../../../services/introService';
export interface Insets {
top: number;
@ -33,6 +34,7 @@ interface UpNextButtonProps {
metadata?: { poster?: string; id?: string }; // Added metadata prop
controlsVisible?: boolean;
controlsFixedOffset?: number;
outroSegment?: SkipInterval | null;
}
const UpNextButton: React.FC<UpNextButtonProps> = ({
@ -49,6 +51,7 @@ const UpNextButton: React.FC<UpNextButtonProps> = ({
metadata,
controlsVisible = false,
controlsFixedOffset = 100,
outroSegment,
}) => {
const [visible, setVisible] = useState(false);
const opacity = useRef(new Animated.Value(0)).current;
@ -76,10 +79,20 @@ const UpNextButton: React.FC<UpNextButtonProps> = ({
const shouldShow = useMemo(() => {
if (!nextEpisode || duration <= 0) return false;
// 1. Determine if we have a valid ending outro (within last 5 mins)
const hasValidEndingOutro = outroSegment && (duration - outroSegment.endTime < 300);
if (hasValidEndingOutro) {
// If we have a valid outro, ONLY show after it finishes
// This prevents the 60s fallback from "jumping the gun"
return currentTime >= outroSegment.endTime;
}
// 2. Standard Fallback (only if no valid ending outro was found)
const timeRemaining = duration - currentTime;
// Be tolerant to timer jitter: show when under ~1 minute and above 10s
return timeRemaining < 61 && timeRemaining > 10;
}, [nextEpisode, duration, currentTime]);
return timeRemaining < 61 && timeRemaining > 0;
}, [nextEpisode, duration, currentTime, outroSegment]);
// Debug logging removed to reduce console noise
// The state is computed in shouldShow useMemo above

View file

@ -12,6 +12,14 @@ import { useSettings } from '../../../hooks/useSettings';
import { introService } from '../../../services/introService';
import { toastService } from '../../../services/toastService';
import PlayerAspectRatioIcon from '../../../../assets/player-icons/ic_player_aspect_ratio.svg';
import PlayerAudioFilledIcon from '../../../../assets/player-icons/ic_player_audio_filled.svg';
import PlayerAudioOutlineIcon from '../../../../assets/player-icons/ic_player_audio_outline.svg';
import PlayerEpisodesIcon from '../../../../assets/player-icons/ic_player_episodes.svg';
import PlayerPauseIcon from '../../../../assets/player-icons/ic_player_pause.svg';
import PlayerPlayIcon from '../../../../assets/player-icons/ic_player_play.svg';
import PlayerSourceIcon from '../../../../assets/player-icons/ic_player_source.svg';
import PlayerSubtitlesIcon from '../../../../assets/player-icons/ic_player_subtitles.svg';
interface PlayerControlsProps {
showControls: boolean;
@ -498,11 +506,11 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
{isBuffering ? (
<ActivityIndicator size="large" color="#FFFFFF" />
) : (
<Ionicons
name={paused ? "play" : "pause"}
size={playIconSizeCalculated}
color="#FFFFFF"
/>
paused ? (
<PlayerPlayIcon width={playIconSizeCalculated} height={playIconSizeCalculated} />
) : (
<PlayerPauseIcon width={playIconSizeCalculated} height={playIconSizeCalculated} />
)
)}
</Animated.View>
</View>
@ -594,7 +602,7 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
<View style={styles.centerControlsContainer} pointerEvents="box-none">
{/* Left Side: Aspect Ratio Button */}
<TouchableOpacity style={styles.iconButton} onPress={cycleAspectRatio}>
<Ionicons name="expand-outline" size={24} color="white" />
<PlayerAspectRatioIcon width={24} height={24} />
</TouchableOpacity>
{/* Subtitle Button */}
@ -602,7 +610,7 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
style={styles.iconButton}
onPress={() => setShowSubtitleModal(!isSubtitleModalOpen)}
>
<Ionicons name="text" size={24} color="white" />
<PlayerSubtitlesIcon width={24} height={24} />
</TouchableOpacity>
{/* Change Source Button */}
@ -611,7 +619,7 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
style={styles.iconButton}
onPress={() => setShowSourcesModal(true)}
>
<Ionicons name="cloud-outline" size={24} color="white" />
<PlayerSourceIcon width={24} height={24} />
</TouchableOpacity>
)}
@ -626,11 +634,11 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
onPress={() => setShowAudioModal(true)}
disabled={ksAudioTracks.length <= 1}
>
<Ionicons
name="musical-notes-outline"
size={24}
color={ksAudioTracks.length <= 1 ? 'grey' : 'white'}
/>
{ksAudioTracks.length <= 1 ? (
<PlayerAudioOutlineIcon width={24} height={24} opacity={0.55} />
) : (
<PlayerAudioFilledIcon width={24} height={24} />
)}
</TouchableOpacity>
{/* Submit Intro Button */}
@ -653,7 +661,7 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
style={styles.iconButton}
onPress={() => setShowEpisodesModal(true)}
>
<Ionicons name="list" size={24} color="white" />
<PlayerEpisodesIcon width={24} height={24} />
</TouchableOpacity>
)}
</View>

Some files were not shown because too many files have changed in this diff Show more