From 02bfd85b5adb1d21a3a31025215e28a08ba0b4f3 Mon Sep 17 00:00:00 2001 From: tapframe Date: Mon, 8 Sep 2025 17:28:10 +0530 Subject: [PATCH] perfomnce optimziations --- App.tsx | 8 +- package-lock.json | 312 +-------------------------- package.json | 2 +- src/components/home/HeroCarousel.tsx | 13 +- src/contexts/AccountContext.tsx | 38 ++-- src/hooks/useMetadata.ts | 40 +++- src/navigation/AppNavigator.tsx | 9 +- src/screens/HomeScreen.tsx | 7 +- src/screens/MetadataScreen.tsx | 25 ++- src/services/tmdbService.ts | 57 +++-- 10 files changed, 142 insertions(+), 369 deletions(-) diff --git a/App.tsx b/App.tsx index ca88199..804eca2 100644 --- a/App.tsx +++ b/App.tsx @@ -40,10 +40,10 @@ Sentry.init({ // For more information, visit: https://docs.sentry.io/platforms/react-native/data-management/data-collected/ sendDefaultPii: true, - // Configure Session Replay - replaysSessionSampleRate: 0.1, - replaysOnErrorSampleRate: 1, - integrations: [Sentry.mobileReplayIntegration(), Sentry.feedbackIntegration()], + // Configure Session Replay conservatively to avoid startup overhead in production + replaysSessionSampleRate: __DEV__ ? 0.1 : 0, + replaysOnErrorSampleRate: __DEV__ ? 1 : 0, + integrations: [Sentry.feedbackIntegration()], // uncomment the line below to enable Spotlight (https://spotlightjs.com) // spotlight: __DEV__, diff --git a/package-lock.json b/package-lock.json index 893adde..5fc83d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@react-navigation/native-stack": "^7.3.10", "@react-navigation/stack": "^7.2.10", "@sentry/react-native": "~6.10.0", - "@shopify/flash-list": "1.7.3", + "@shopify/flash-list": "2.0.3", "@supabase/supabase-js": "^2.54.0", "@types/lodash": "^4.17.16", "@types/react-native-video": "^5.0.20", @@ -4204,108 +4204,6 @@ "node": ">=10" } }, - "node_modules/@sentry/cli-linux-arm": { - "version": "2.42.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.42.4.tgz", - "integrity": "sha512-lBn0oeeg62h68/4Eo6zbPq99Idz5t0VRV48rEU/WKeM4MtQCvG/iGGQ3lBFW2yNiUBzXZIK9poXLEcgbwmcRVw==", - "cpu": [ - "arm" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "linux", - "freebsd" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-arm64": { - "version": "2.42.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.42.4.tgz", - "integrity": "sha512-Ex8vRnryyzC/9e43daEmEqPS+9uirY/l6Hw2lAvhBblFaL7PTWNx52H+8GnYGd9Zy2H3rWNyBDYfHwnErg38zA==", - "cpu": [ - "arm64" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "linux", - "freebsd" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-i686": { - "version": "2.42.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.42.4.tgz", - "integrity": "sha512-IBJg0aHjsLCL4LvcFa3cXIjA+4t5kPqBT9y+PoDu4goIFxYD8zl7mbUdGJutvJafTk8Akf4ss4JJXQBjg019zA==", - "cpu": [ - "x86", - "ia32" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "linux", - "freebsd" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-x64": { - "version": "2.42.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.42.4.tgz", - "integrity": "sha512-gXI5OEiOSNiAEz7VCE6AZcAgHJ47mlgal3+NmbE8XcHmFOnyDws9FNie6PJAy8KZjXi3nqoBP9JVAbnmOix3uA==", - "cpu": [ - "x64" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "linux", - "freebsd" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-win32-i686": { - "version": "2.42.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.42.4.tgz", - "integrity": "sha512-vZuR3UPHKqOMniyrijrrsNwn9usaRysXq78F6WV0cL0ZyPLAmY+KBnTDSFk1Oig2pURnzaTm+RtcZu2fc8mlzg==", - "cpu": [ - "x86", - "ia32" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-win32-x64": { - "version": "2.42.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.42.4.tgz", - "integrity": "sha512-OIBj3uaQ6nAERSm5Dcf8UIhyElEEwMNsZEEppQpN4IKl0mrwb/57AznM23Dvpu6GR8WGbVQUSolt879YZR5E9g==", - "cpu": [ - "x64" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, "node_modules/@sentry/core": { "version": "8.54.0", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.54.0.tgz", @@ -4385,12 +4283,11 @@ } }, "node_modules/@shopify/flash-list": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-1.7.3.tgz", - "integrity": "sha512-RLhNptm02aqpqZvjj9pJPcU+EVYxOAJhPRCmDOaUbUP86+636w+plsbjpBPSYGvPZhPj56RtZ9FBlvolPeEmYA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-2.0.3.tgz", + "integrity": "sha512-jUlHuZFoPdqRCDvOqsb2YkTttRPyV8Tb/EjCx3gE2wjr4UTM+fE0Ltv9bwBg0K7yo/SxRNXaW7xu5utusRb0xA==", "license": "MIT", "dependencies": { - "recyclerlistview": "4.2.1", "tslib": "2.8.1" }, "peerDependencies": { @@ -9903,26 +9800,6 @@ "lightningcss-win32-x64-msvc": "1.27.0" } }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.27.0.tgz", - "integrity": "sha512-Gl/lqIXY+d+ySmMbgDf0pgaWSqrWYxVHoc88q+Vhf2YNzZ8DwoRzGt5NZDVqqIW5ScpSnmmjcgXP87Dn2ylSSQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lightningcss-darwin-x64": { "version": "1.27.0", "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.27.0.tgz", @@ -9943,166 +9820,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.27.0.tgz", - "integrity": "sha512-n1sEf85fePoU2aDN2PzYjoI8gbBqnmLGEhKq7q0DKLj0UTVmOTwDC7PtLcy/zFxzASTSBlVQYJUhwIStQMIpRA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.27.0.tgz", - "integrity": "sha512-MUMRmtdRkOkd5z3h986HOuNBD1c2lq2BSQA1Jg88d9I7bmPGx08bwGcnB75dvr17CwxjxD6XPi3Qh8ArmKFqCA==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.27.0.tgz", - "integrity": "sha512-cPsxo1QEWq2sfKkSq2Bq5feQDHdUEwgtA9KaB27J5AX22+l4l0ptgjMZZtYtUnteBofjee+0oW1wQ1guv04a7A==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.27.0.tgz", - "integrity": "sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.27.0.tgz", - "integrity": "sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.27.0.tgz", - "integrity": "sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.27.0.tgz", - "integrity": "sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.27.0.tgz", - "integrity": "sha512-/OJLj94Zm/waZShL8nB5jsNj3CfNATLCTyFxZyouilfTmSoLDX7VlVAmhPHoZWVFp4vdmoiEbPEYC8HID3m6yw==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -13048,21 +12765,6 @@ "node": ">= 4" } }, - "node_modules/recyclerlistview": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/recyclerlistview/-/recyclerlistview-4.2.1.tgz", - "integrity": "sha512-NtVYjofwgUCt1rEsTp6jHQg/47TWjnO92TU2kTVgJ9wsc/ely4HnizHHa+f/dI7qaw4+zcSogElrLjhMltN2/g==", - "license": "Apache-2.0", - "dependencies": { - "lodash.debounce": "4.0.8", - "prop-types": "15.8.1", - "ts-object-utils": "0.0.5" - }, - "peerDependencies": { - "react": ">= 15.2.1", - "react-native": ">= 0.30.0" - } - }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -14746,12 +14448,6 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "license": "Apache-2.0" }, - "node_modules/ts-object-utils": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/ts-object-utils/-/ts-object-utils-0.0.5.tgz", - "integrity": "sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA==", - "license": "ISC" - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", diff --git a/package.json b/package.json index d3cc5e8..6521a3f 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@react-navigation/native-stack": "^7.3.10", "@react-navigation/stack": "^7.2.10", "@sentry/react-native": "~6.10.0", - "@shopify/flash-list": "1.7.3", + "@shopify/flash-list": "2.0.3", "@supabase/supabase-js": "^2.54.0", "@types/lodash": "^4.17.16", "@types/react-native-video": "^5.0.20", diff --git a/src/components/home/HeroCarousel.tsx b/src/components/home/HeroCarousel.tsx index d3cd9d3..e9b68e9 100644 --- a/src/components/home/HeroCarousel.tsx +++ b/src/components/home/HeroCarousel.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useState, useEffect, useCallback, memo } from 'react'; -import { View, Text, StyleSheet, Dimensions, TouchableOpacity, ViewStyle, TextStyle, ImageStyle, FlatList, StyleProp } from 'react-native'; +import { View, Text, StyleSheet, Dimensions, TouchableOpacity, ViewStyle, TextStyle, ImageStyle, FlatList, StyleProp, Platform } from 'react-native'; import Animated, { FadeIn, FadeOut, Easing, useSharedValue, withTiming, useAnimatedStyle, useAnimatedScrollHandler, useAnimatedReaction, runOnJS } from 'react-native-reanimated'; import { LinearGradient } from 'expo-linear-gradient'; import { Image as ExpoImage } from 'expo-image'; @@ -157,9 +157,9 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = source={{ uri: item.banner || item.poster }} style={styles.backgroundImage as ImageStyle} contentFit="cover" - blurRadius={24} + blurRadius={Platform.OS === 'android' ? 12 : 20} cachePolicy="memory-disk" - transition={300} + transition={200} priority="high" /> = ({ items, loading = false }) = decelerationRate="fast" contentContainerStyle={contentPadding} onScroll={scrollHandler} - scrollEventThrottle={16} - initialNumToRender={3} + scrollEventThrottle={32} + disableIntervalMomentum + initialNumToRender={2} windowSize={3} - maxToRenderPerBatch={3} + maxToRenderPerBatch={2} updateCellsBatchingPeriod={50} removeClippedSubviews getItemLayout={getItemLayout} diff --git a/src/contexts/AccountContext.tsx b/src/contexts/AccountContext.tsx index db366be..1e4021a 100644 --- a/src/contexts/AccountContext.tsx +++ b/src/contexts/AccountContext.tsx @@ -1,4 +1,5 @@ import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; +import { InteractionManager } from 'react-native'; import accountService, { AuthUser } from '../services/AccountService'; import supabase from '../services/supabaseClient'; import syncService from '../services/SyncService'; @@ -20,19 +21,29 @@ export const AccountProvider: React.FC<{ children: React.ReactNode }> = ({ child useEffect(() => { // Initial session (load full profile) - (async () => { - const u = await accountService.getCurrentUser(); - setUser(u); - setLoading(false); - syncService.init(); - if (u) { - await syncService.migrateLocalScopeToUser(); - await syncService.subscribeRealtime(); - // Pull first to hydrate local state, then push to avoid wiping server with empty local - await syncService.fullPull(); - await syncService.fullPush(); - } - })(); + // Defer heavy work until after initial interactions to reduce launch CPU spike + const task = InteractionManager.runAfterInteractions(() => { + (async () => { + const u = await accountService.getCurrentUser(); + setUser(u); + setLoading(false); + // Stage sync operations to avoid blocking the JS thread + syncService.init(); + if (u) { + try { + await syncService.migrateLocalScopeToUser(); + // Small yield to event loop + await new Promise(resolve => setTimeout(resolve, 50)); + await syncService.subscribeRealtime(); + await new Promise(resolve => setTimeout(resolve, 50)); + // Pull first to hydrate local state, then push to avoid wiping server with empty local + await syncService.fullPull(); + await new Promise(resolve => setTimeout(resolve, 50)); + await syncService.fullPush(); + } catch {} + } + })(); + }); // Auth state listener const { data: subscription } = supabase.auth.onAuthStateChange(async (_event, session) => { @@ -51,6 +62,7 @@ export const AccountProvider: React.FC<{ children: React.ReactNode }> = ({ child return () => { subscription.subscription.unsubscribe(); + task.cancel(); }; }, []); diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index 9ba604f..1c51613 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -1141,20 +1141,24 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const tmdbService = TMDBService.getInstance(); const fetchedTmdbId = await tmdbService.extractTMDBIdFromStremioId(id); if (fetchedTmdbId) { + console.log('[useMetadata] extracted TMDB id from content id', { id, fetchedTmdbId }); setTmdbId(fetchedTmdbId); // Fetch certification const certification = await tmdbService.getCertification(type, fetchedTmdbId); if (certification) { + console.log('[useMetadata] fetched certification via TMDB id (extract path)', { type, fetchedTmdbId, certification }); setMetadata(prev => prev ? { ...prev, certification } : null); + } else { + console.warn('[useMetadata] certification not returned from TMDB (extract path)', { type, fetchedTmdbId }); } } else { - console.warn('Could not determine TMDB ID for recommendations.'); + console.warn('[useMetadata] Could not determine TMDB ID for recommendations / certification', { id }); } } catch (error) { - console.error('Error fetching TMDB ID:', error); + console.error('[useMetadata] Error fetching TMDB ID (extract path):', error); } } }; @@ -1164,6 +1168,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat useEffect(() => { if (tmdbId) { + console.log('[useMetadata] tmdbId available; loading recommendations and enabling certification checks', { tmdbId }); loadRecommendations(); // Reset recommendations when tmdbId changes return () => { @@ -1173,6 +1178,37 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat } }, [tmdbId, loadRecommendations]); + // Ensure certification is attached whenever a TMDB id is known and metadata lacks it + useEffect(() => { + const maybeAttachCertification = async () => { + try { + if (!metadata) { + console.warn('[useMetadata] skip certification attach: metadata not ready'); + return; + } + if (!tmdbId) { + console.warn('[useMetadata] skip certification attach: tmdbId not available yet'); + return; + } + if ((metadata as any).certification) { + console.log('[useMetadata] certification already present on metadata; skipping fetch'); + return; + } + const tmdbSvc = TMDBService.getInstance(); + const cert = await tmdbSvc.getCertification(type, tmdbId); + if (cert) { + console.log('[useMetadata] fetched certification (attach path)', { type, tmdbId, cert }); + setMetadata(prev => prev ? { ...prev, certification: cert } : prev); + } else { + console.warn('[useMetadata] TMDB returned no certification (attach path)', { type, tmdbId }); + } + } catch (err) { + console.error('[useMetadata] error attaching certification', err); + } + }; + maybeAttachCertification(); + }, [tmdbId, metadata, type]); + // Reset tmdbId when id changes useEffect(() => { setTmdbId(null); diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 2d08886..c2e7742 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -718,7 +718,10 @@ const MainTabs = () => { elevation: 0, backgroundColor: currentTheme.colors.darkBackground, }, - detachInactiveScreens: false, + // Ensure background tabs are frozen and detached + freezeOnBlur: true, + lazy: true, + detachInactiveScreens: true, })} > { let catalogIndex = 0; // Limit concurrent catalog loading to prevent overwhelming the system - const MAX_CONCURRENT_CATALOGS = 5; + const MAX_CONCURRENT_CATALOGS = 3; // Lower concurrency to reduce CPU/network spikes let activeCatalogLoads = 0; const catalogQueue: (() => Promise)[] = []; @@ -167,8 +167,10 @@ const HomeScreen = () => { const catalogLoader = catalogQueue.shift(); if (catalogLoader) { activeCatalogLoads++; - catalogLoader().finally(() => { + catalogLoader().finally(async () => { activeCatalogLoads--; + // Yield to event loop to avoid JS thread starvation + await new Promise(resolve => setTimeout(resolve, 10)); processCatalogQueue(); // Process next in queue }); } @@ -767,7 +769,6 @@ const HomeScreen = () => { onEndReached={handleLoadMoreCatalogs} onEndReachedThreshold={0.6} scrollEventThrottle={32} - estimatedItemSize={220} onScroll={event => { const y = event.nativeEvent.contentOffset.y; const dy = y - lastScrollYRef.current; diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index b2942c0..9dbff95 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -111,14 +111,17 @@ const MetadataScreen: React.FC = () => { // Extract dominant color from hero image for dynamic background const heroImageUri = useMemo(() => { + if (!settings.useDominantBackgroundColor) return null; if (!metadata) return null; return assetData.bannerImage || metadata.banner || metadata.poster || null; - }, [metadata, assetData.bannerImage]); + }, [settings.useDominantBackgroundColor, metadata, assetData.bannerImage]); // Preload color extraction as soon as we have the URI useEffect(() => { if (heroImageUri) { - preloadDominantColor(heroImageUri); + InteractionManager.runAfterInteractions(() => { + preloadDominantColor(heroImageUri); + }); } }, [heroImageUri]); @@ -188,7 +191,7 @@ const MetadataScreen: React.FC = () => { // Debug logging for color extraction timing useEffect(() => { - if (heroImageUri && dominantColor) { + if (__DEV__ && heroImageUri && dominantColor) { console.log('[MetadataScreen] Dynamic background color:', { dominantColor, fallback: currentTheme.colors.darkBackground, @@ -266,7 +269,7 @@ const MetadataScreen: React.FC = () => { if (relevantProgress.length === 0) return; // Log only essential progress information for performance - console.log(`[MetadataScreen] Found ${relevantProgress.length} Trakt progress items for ${type}`); + if (__DEV__) console.log(`[MetadataScreen] Found ${relevantProgress.length} Trakt progress items for ${type}`); // Find most recent progress if multiple episodes if (type === 'series' && relevantProgress.length > 1) { @@ -275,7 +278,7 @@ const MetadataScreen: React.FC = () => { )[0]; if (mostRecent.episode && mostRecent.show) { - console.log(`[MetadataScreen] Most recent: S${mostRecent.episode.season}E${mostRecent.episode.number} - ${mostRecent.progress.toFixed(1)}%`); + if (__DEV__) console.log(`[MetadataScreen] Most recent: S${mostRecent.episode.season}E${mostRecent.episode.number} - ${mostRecent.progress.toFixed(1)}%`); } } @@ -396,7 +399,7 @@ const MetadataScreen: React.FC = () => { // DIRECT APPROACH: Just create the next episode ID directly // This ensures we navigate to the next episode even if it's not yet in our episodes array const nextEpisodeId = `${id}:${currentSeason}:${currentEpisode + 1}`; - console.log(`[MetadataScreen] Created next episode ID directly: ${nextEpisodeId}`); + if (__DEV__) console.log(`[MetadataScreen] Created next episode ID directly: ${nextEpisodeId}`); // Still try to find the episode in our list to verify it exists const nextEpisodeExists = episodes.some(ep => @@ -404,9 +407,9 @@ const MetadataScreen: React.FC = () => { ); if (nextEpisodeExists) { - console.log(`[MetadataScreen] Verified next episode S${currentSeason}E${currentEpisode + 1} exists in episodes list`); + if (__DEV__) console.log(`[MetadataScreen] Verified next episode S${currentSeason}E${currentEpisode + 1} exists in episodes list`); } else { - console.log(`[MetadataScreen] Warning: Next episode S${currentSeason}E${currentEpisode + 1} not found in episodes list, but proceeding anyway`); + if (__DEV__) console.log(`[MetadataScreen] Warning: Next episode S${currentSeason}E${currentEpisode + 1} not found in episodes list, but proceeding anyway`); } targetEpisodeId = nextEpisodeId; @@ -416,7 +419,7 @@ const MetadataScreen: React.FC = () => { // Fallback logic: if not finished or nextEp not found if (!targetEpisodeId) { targetEpisodeId = watchProgress?.episodeId || episodeId || (episodes.length > 0 ? buildEpisodeId(episodes[0]) : undefined); - console.log(`[MetadataScreen] Using fallback episode ID: ${targetEpisodeId}`); + if (__DEV__) console.log(`[MetadataScreen] Using fallback episode ID: ${targetEpisodeId}`); } if (targetEpisodeId) { @@ -426,7 +429,7 @@ const MetadataScreen: React.FC = () => { if (epParts.length === 2) { normalizedEpisodeId = `${id}:${epParts[0]}:${epParts[1]}`; } - console.log(`[MetadataScreen] Navigating to streams with episodeId: ${normalizedEpisodeId}`); + if (__DEV__) console.log(`[MetadataScreen] Navigating to streams with episodeId: ${normalizedEpisodeId}`); navigation.navigate('Streams', { id, type, episodeId: normalizedEpisodeId }); return; } @@ -438,7 +441,7 @@ const MetadataScreen: React.FC = () => { const p = episodeId.split(':'); fallbackEpisodeId = `${id}:${p[0]}:${p[1]}`; } - console.log(`[MetadataScreen] Navigating with fallback episodeId: ${fallbackEpisodeId}`); + if (__DEV__) console.log(`[MetadataScreen] Navigating with fallback episodeId: ${fallbackEpisodeId}`); navigation.navigate('Streams', { id, type, episodeId: fallbackEpisodeId }); }, [navigation, id, type, episodes, episodeId, watchProgressData.watchProgress]); diff --git a/src/services/tmdbService.ts b/src/services/tmdbService.ts index c0431f5..a377213 100644 --- a/src/services/tmdbService.ts +++ b/src/services/tmdbService.ts @@ -816,30 +816,47 @@ export class TMDBService { */ async getCertification(type: string, id: number): Promise { try { - // Different endpoints for movies and TV shows - const endpoint = type === 'movie' ? 'movie' : 'tv'; - const response = await axios.get(`${BASE_URL}/${endpoint}/${id}/release_dates`, { - headers: await this.getHeaders(), - params: await this.getParams() - }); + if (type === 'movie') { + const response = await axios.get(`${BASE_URL}/movie/${id}/release_dates`, { + headers: await this.getHeaders(), + params: await this.getParams() + }); - if (response.data && response.data.results) { - // Try to find US certification first - const usRelease = response.data.results.find((r: any) => r.iso_3166_1 === 'US'); - if (usRelease && usRelease.release_dates && usRelease.release_dates.length > 0) { - const certification = usRelease.release_dates.find((rd: any) => rd.certification)?.certification; - if (certification) return certification; - } - - // Fallback to any certification if US is not available - for (const country of response.data.results) { - if (country.release_dates && country.release_dates.length > 0) { - const certification = country.release_dates.find((rd: any) => rd.certification)?.certification; - if (certification) return certification; + if (response.data && response.data.results) { + // Prefer US, then GB, then any + const countryPriority = ['US', 'GB']; + for (const code of countryPriority) { + const rel = response.data.results.find((r: any) => r.iso_3166_1 === code); + if (rel?.release_dates?.length) { + const cert = rel.release_dates.find((rd: any) => rd.certification)?.certification; + if (cert) return cert; + } + } + for (const country of response.data.results) { + const cert = country.release_dates?.find((rd: any) => rd.certification)?.certification; + if (cert) return cert; } } + return null; + } else { + // TV uses content ratings endpoint, not release_dates + const response = await axios.get(`${BASE_URL}/tv/${id}/content_ratings`, { + headers: await this.getHeaders(), + params: await this.getParams() + }); + + if (response.data && response.data.results) { + // Prefer US, then GB, then any + const countryPriority = ['US', 'GB']; + for (const code of countryPriority) { + const rating = response.data.results.find((r: any) => r.iso_3166_1 === code); + if (rating?.rating) return rating.rating; + } + const any = response.data.results.find((r: any) => !!r.rating); + if (any?.rating) return any.rating; + } + return null; } - return null; } catch (error) { return null; }