homescreenc changes

This commit is contained in:
tapframe 2025-08-12 12:42:56 +05:30
parent 11030c5601
commit 99dc34cb65
11 changed files with 253 additions and 544 deletions

1
nuvio-providers Submodule

@ -0,0 +1 @@
Subproject commit 96be1f53604182cb53f027160db9fc969ed3bdcc

347
package-lock.json generated
View file

@ -18,6 +18,7 @@
"@react-navigation/native-stack": "^7.3.10", "@react-navigation/native-stack": "^7.3.10",
"@react-navigation/stack": "^7.2.10", "@react-navigation/stack": "^7.2.10",
"@sentry/react-native": "^6.15.1", "@sentry/react-native": "^6.15.1",
"@shopify/flash-list": "^1.8.3",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"@types/react-native-video": "^5.0.20", "@types/react-native-video": "^5.0.20",
"axios": "^1.10.0", "axios": "^1.10.0",
@ -40,7 +41,7 @@
"react-native-gesture-handler": "~2.20.2", "react-native-gesture-handler": "~2.20.2",
"react-native-immersive-mode": "^2.0.2", "react-native-immersive-mode": "^2.0.2",
"react-native-paper": "^5.13.1", "react-native-paper": "^5.13.1",
"react-native-reanimated": "~3.6.0", "react-native-reanimated": "3.6.0",
"react-native-safe-area-context": "4.12.0", "react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0", "react-native-screens": "~4.4.0",
"react-native-svg": "^15.11.2", "react-native-svg": "^15.11.2",
@ -3046,128 +3047,6 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@sentry/cli/node_modules/@sentry/cli-linux-arm": {
"version": "2.50.2",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.50.2.tgz",
"integrity": "sha512-jzFwg9AeeuFAFtoCcyaDEPG05TU02uOy1nAX09c1g7FtsyQlPcbhI94JQGmnPzdRjjDmORtwIUiVZQrVTkDM7w==",
"cpu": [
"arm"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"linux",
"freebsd",
"android"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli/node_modules/@sentry/cli-linux-arm64": {
"version": "2.50.2",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.50.2.tgz",
"integrity": "sha512-03Cj215M3IdoHAwevCxm5oOm9WICFpuLR05DQnODFCeIUsGvE1pZsc+Gm0Ky/ZArq2PlShBJTpbHvXbCUka+0w==",
"cpu": [
"arm64"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"linux",
"freebsd",
"android"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli/node_modules/@sentry/cli-linux-i686": {
"version": "2.50.2",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.50.2.tgz",
"integrity": "sha512-J+POvB34uVyHbIYF++Bc/OCLw+gqKW0H/y/mY7rRZCiocgpk266M4NtsOBl6bEaurMx1D+BCIEjr4nc01I/rqA==",
"cpu": [
"x86",
"ia32"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"linux",
"freebsd",
"android"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli/node_modules/@sentry/cli-linux-x64": {
"version": "2.50.2",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.50.2.tgz",
"integrity": "sha512-81yQVRLj8rnuHoYcrM7QbOw8ubA3weiMdPtTxTim1s6WExmPgnPTKxLCr9xzxGJxFdYo3xIOhtf5JFpUX/3j4A==",
"cpu": [
"x64"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"linux",
"freebsd",
"android"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli/node_modules/@sentry/cli-win32-arm64": {
"version": "2.50.2",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.50.2.tgz",
"integrity": "sha512-QjentLGvpibgiZlmlV9ifZyxV73lnGH6pFZWU5wLeRiaYKxWtNrrHpVs+HiWlRhkwQ0mG1/S40PGNgJ20DJ3gA==",
"cpu": [
"arm64"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli/node_modules/@sentry/cli-win32-i686": {
"version": "2.50.2",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.50.2.tgz",
"integrity": "sha512-UkBIIzkQkQ1UkjQX8kHm/+e7IxnEhK6CdgSjFyNlxkwALjDWHJjMztevqAPz3kv4LdM6q1MxpQ/mOqXICNhEGg==",
"cpu": [
"x86",
"ia32"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli/node_modules/@sentry/cli-win32-x64": {
"version": "2.50.2",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.50.2.tgz",
"integrity": "sha512-tE27pu1sRRub1Jpmemykv3QHddBcyUk39Fsvv+n4NDpQyMgsyVPcboxBZyby44F0jkpI/q3bUH2tfCB1TYDNLg==",
"cpu": [
"x64"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/core": { "node_modules/@sentry/core": {
"version": "8.55.0", "version": "8.55.0",
"license": "MIT", "license": "MIT",
@ -3236,6 +3115,21 @@
"node": ">=14.18" "node": ">=14.18"
} }
}, },
"node_modules/@shopify/flash-list": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-1.8.3.tgz",
"integrity": "sha512-vXuj6JyuMjONVOXjEhWFeaONPuWN/53Cl2LeyeM8TZ0JzUcNU+BE6iyga1/yyJeDf0K7YPgAE/PcUX2+DM1LiA==",
"license": "MIT",
"dependencies": {
"recyclerlistview": "4.2.3",
"tslib": "2.8.1"
},
"peerDependencies": {
"@babel/runtime": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/@sinclair/typebox": { "node_modules/@sinclair/typebox": {
"version": "0.27.8", "version": "0.27.8",
"license": "MIT" "license": "MIT"
@ -7148,186 +7042,6 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/lightningcss/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/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/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/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/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/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/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/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/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": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"license": "MIT" "license": "MIT"
@ -9149,9 +8863,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-native-reanimated": { "node_modules/react-native-reanimated": {
"version": "3.6.3", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.6.3.tgz", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.6.0.tgz",
"integrity": "sha512-2KkkPozoIvDbJcHuf8qeyoLROXQxizSi+2CTCkuNVkVZOxxY4B0Omvgq61aOQhSZUh/649x1YHoAaTyGMGDJUw==", "integrity": "sha512-eDdhZTRYofrIqFB/Z5xLTWxcB7wDj4ifrNm+gZ2xHSZPjAQ747ukDdH9rglPyPmi+GcmDH7Wff411Xsw5fm45Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/plugin-transform-object-assign": "^7.16.7", "@babel/plugin-transform-object-assign": "^7.16.7",
@ -9521,6 +9235,21 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/recyclerlistview": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/recyclerlistview/-/recyclerlistview-4.2.3.tgz",
"integrity": "sha512-STR/wj/FyT8EMsBzzhZ1l2goYirMkIgfV3gYEPxI3Kf3lOnu6f7Dryhyw7/IkQrgX5xtTcDrZMqytvteH9rL3g==",
"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": { "node_modules/regenerate": {
"version": "1.4.2", "version": "1.4.2",
"license": "MIT" "license": "MIT"
@ -10834,6 +10563,12 @@
"version": "0.1.13", "version": "0.1.13",
"license": "Apache-2.0" "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": { "node_modules/tslib": {
"version": "2.8.1", "version": "2.8.1",
"license": "0BSD" "license": "0BSD"

View file

@ -23,6 +23,7 @@
"@react-navigation/native-stack": "^7.3.10", "@react-navigation/native-stack": "^7.3.10",
"@react-navigation/stack": "^7.2.10", "@react-navigation/stack": "^7.2.10",
"@sentry/react-native": "^6.15.1", "@sentry/react-native": "^6.15.1",
"@shopify/flash-list": "^1.8.3",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"@types/react-native-video": "^5.0.20", "@types/react-native-video": "^5.0.20",
"axios": "^1.10.0", "axios": "^1.10.0",
@ -31,25 +32,21 @@
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
"expo": "~52.0.43", "expo": "~52.0.43",
"expo-blur": "^14.0.3", "expo-blur": "^14.0.3",
"expo-haptics": "~14.0.1", "expo-haptics": "~14.0.1",
"expo-image": "~2.0.7", "expo-image": "~2.0.7",
"expo-intent-launcher": "~12.0.2", "expo-intent-launcher": "~12.0.2",
"expo-linear-gradient": "~14.0.2", "expo-linear-gradient": "~14.0.2",
"expo-random": "^14.0.1", "expo-random": "^14.0.1",
"expo-status-bar": "~2.0.1", "expo-status-bar": "~2.0.1",
"expo-system-ui": "^4.0.9", "expo-system-ui": "^4.0.9",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "18.3.1", "react": "18.3.1",
"react-native": "npm:react-native-tvos@latest", "react-native": "npm:react-native-tvos@latest",
"react-native-gesture-handler": "~2.20.2", "react-native-gesture-handler": "~2.20.2",
"react-native-immersive-mode": "^2.0.2", "react-native-immersive-mode": "^2.0.2",
"react-native-paper": "^5.13.1", "react-native-paper": "^5.13.1",
"react-native-reanimated": "@latest", "react-native-reanimated": "3.6.0",
"react-native-safe-area-context": "4.12.0", "react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0", "react-native-screens": "~4.4.0",
"react-native-svg": "^15.11.2", "react-native-svg": "^15.11.2",

View file

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, FlatList, Platform, Dimensions } from 'react-native'; import { View, Text, StyleSheet, TouchableOpacity, Platform, Dimensions } from 'react-native';
import { FlashList } from '@shopify/flash-list';
import { NavigationProp, useNavigation } from '@react-navigation/native'; import { NavigationProp, useNavigation } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
@ -12,6 +13,7 @@ import { RootStackParamList } from '../../navigation/AppNavigator';
interface CatalogSectionProps { interface CatalogSectionProps {
catalog: CatalogContent; catalog: CatalogContent;
onPosterPress?: (content: StreamingContent) => void; onPosterPress?: (content: StreamingContent) => void;
onPosterFocus?: (content: StreamingContent) => void;
} }
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
@ -54,7 +56,7 @@ const calculatePosterLayout = (screenWidth: number) => {
const posterLayout = calculatePosterLayout(width); const posterLayout = calculatePosterLayout(width);
const POSTER_WIDTH = posterLayout.posterWidth; const POSTER_WIDTH = posterLayout.posterWidth;
const CatalogSection = ({ catalog, onPosterPress }: CatalogSectionProps) => { const CatalogSection = ({ catalog, onPosterPress, onPosterFocus }: CatalogSectionProps) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
@ -75,6 +77,7 @@ const CatalogSection = ({ catalog, onPosterPress }: CatalogSectionProps) => {
<ContentItem <ContentItem
item={item} item={item}
onPress={handleContentPress} onPress={handleContentPress}
onFocusItem={onPosterFocus}
/> />
</Animated.View> </Animated.View>
); );
@ -105,7 +108,7 @@ const CatalogSection = ({ catalog, onPosterPress }: CatalogSectionProps) => {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<FlatList <FlashList
data={catalog.items} data={catalog.items}
renderItem={renderContentItem} renderItem={renderContentItem}
keyExtractor={(item) => `${item.id}-${item.type}`} keyExtractor={(item) => `${item.id}-${item.type}`}
@ -116,19 +119,7 @@ const CatalogSection = ({ catalog, onPosterPress }: CatalogSectionProps) => {
decelerationRate="fast" decelerationRate="fast"
snapToAlignment="start" snapToAlignment="start"
ItemSeparatorComponent={() => <View style={{ width: 8 }} />} ItemSeparatorComponent={() => <View style={{ width: 8 }} />}
initialNumToRender={3} estimatedItemSize={POSTER_WIDTH + 8}
maxToRenderPerBatch={2}
windowSize={3}
removeClippedSubviews={Platform.OS === 'android'}
updateCellsBatchingPeriod={50}
getItemLayout={(data, index) => ({
length: POSTER_WIDTH + 8,
offset: (POSTER_WIDTH + 8) * index,
index,
})}
maintainVisibleContentPosition={{
minIndexForVisible: 0
}}
onEndReachedThreshold={1} onEndReachedThreshold={1}
// TV-specific focus navigation properties // TV-specific focus navigation properties
{...(Platform.isTV && { {...(Platform.isTV && {

View file

@ -9,6 +9,7 @@ import { DropUpMenu } from './DropUpMenu';
interface ContentItemProps { interface ContentItemProps {
item: StreamingContent; item: StreamingContent;
onPress: (id: string, type: string) => void; onPress: (id: string, type: string) => void;
onFocusItem?: (item: StreamingContent) => void;
} }
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
@ -51,7 +52,7 @@ const calculatePosterLayout = (screenWidth: number) => {
const posterLayout = calculatePosterLayout(width); const posterLayout = calculatePosterLayout(width);
const POSTER_WIDTH = posterLayout.posterWidth; const POSTER_WIDTH = posterLayout.posterWidth;
const ContentItem = React.memo(({ item, onPress }: ContentItemProps) => { const ContentItem = React.memo(({ item, onPress, onFocusItem }: ContentItemProps) => {
const [menuVisible, setMenuVisible] = useState(false); const [menuVisible, setMenuVisible] = useState(false);
const [isWatched, setIsWatched] = useState(false); const [isWatched, setIsWatched] = useState(false);
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
@ -102,6 +103,9 @@ const ContentItem = React.memo(({ item, onPress }: ContentItemProps) => {
friction: 6, friction: 6,
}).start(); }).start();
} }
if (onFocusItem) {
onFocusItem(item);
}
}, [scaleAnim]); }, [scaleAnim]);
const handleBlur = useCallback(() => { const handleBlur = useCallback(() => {

View file

@ -3,7 +3,6 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
FlatList,
TouchableOpacity, TouchableOpacity,
Dimensions, Dimensions,
AppState, AppState,
@ -13,6 +12,7 @@ import {
Platform, Platform,
Animated Animated
} from 'react-native'; } from 'react-native';
import { FlashList } from '@shopify/flash-list';
// Removed react-native-reanimated import // Removed react-native-reanimated import
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native';
@ -737,7 +737,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
</View> </View>
</View> </View>
<FlatList <FlashList
data={continueWatchingItems} data={continueWatchingItems}
renderItem={({ item }) => ( renderItem={({ item }) => (
<ContinueWatchingItem <ContinueWatchingItem
@ -756,6 +756,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
decelerationRate="fast" decelerationRate="fast"
snapToAlignment="start" snapToAlignment="start"
ItemSeparatorComponent={() => <View style={{ width: 16 }} />} ItemSeparatorComponent={() => <View style={{ width: 16 }} />}
estimatedItemSize={280 + 16}
// TV-specific focus navigation properties // TV-specific focus navigation properties
{...(Platform.isTV && { {...(Platform.isTV && {
directionalLockEnabled: true, directionalLockEnabled: true,

View file

@ -252,29 +252,7 @@ const FeaturedContent = ({ featuredContent }: FeaturedContentProps) => {
)} )}
</View> </View>
{/* Action Buttons */} {/* Action buttons removed per design */}
<View style={styles.actionButtonsContainer}>
<TouchableOpacity
style={styles.playButton}
onPress={() => navigation.navigate('Metadata', {
id: featuredContent.id,
type: featuredContent.type
})}
hasTVPreferredFocus={Platform.isTV}
tvParallaxProperties={Platform.isTV ? {
enabled: true,
shiftDistanceX: 4.0,
shiftDistanceY: 4.0,
tiltAngle: 0.05,
magnification: 1.1,
} : undefined}
>
<MaterialIcons name="play-arrow" size={Platform.isTV ? 28 : 24} color="#000" />
<Text style={styles.playButtonText}>Play</Text>
</TouchableOpacity>
</View>
</View> </View>
@ -287,7 +265,7 @@ const FeaturedContent = ({ featuredContent }: FeaturedContentProps) => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
featuredContainer: { featuredContainer: {
width: '100%', width: '100%',
height: Platform.isTV ? height * 0.5 : height * 0.4, height: Platform.isTV ? height * 0.65 : height * 0.5,
marginTop: 0, marginTop: 0,
marginBottom: Platform.isTV ? 16 : 8, marginBottom: Platform.isTV ? 16 : 8,
position: 'relative', position: 'relative',
@ -471,33 +449,7 @@ const styles = StyleSheet.create({
fontSize: Platform.isTV ? 18 : 16, fontSize: Platform.isTV ? 18 : 16,
fontWeight: '600', fontWeight: '600',
}, },
actionButtonsContainer: { // Action buttons removed
flexDirection: 'row',
alignItems: 'center',
gap: Platform.isTV ? 20 : 16,
marginTop: Platform.isTV ? 24 : 16,
},
playButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#FFFFFF',
paddingHorizontal: Platform.isTV ? 48 : 32,
paddingVertical: Platform.isTV ? 16 : 12,
borderRadius: Platform.isTV ? 12 : 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 4,
minWidth: Platform.isTV ? 200 : 140,
},
playButtonText: {
color: '#000',
fontSize: Platform.isTV ? 18 : 16,
fontWeight: '700',
marginLeft: 8,
},
}); });

View file

@ -3,11 +3,11 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
FlatList,
TouchableOpacity, TouchableOpacity,
ActivityIndicator, ActivityIndicator,
Dimensions Dimensions
} from 'react-native'; } from 'react-native';
import { FlashList } from '@shopify/flash-list';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
@ -191,7 +191,7 @@ export const ThisWeekSection = React.memo(() => {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<FlatList <FlashList
data={thisWeekEpisodes} data={thisWeekEpisodes}
keyExtractor={(item) => item.id} keyExtractor={(item) => item.id}
renderItem={renderEpisodeItem} renderItem={renderEpisodeItem}
@ -202,6 +202,7 @@ export const ThisWeekSection = React.memo(() => {
decelerationRate="fast" decelerationRate="fast"
snapToAlignment="start" snapToAlignment="start"
ItemSeparatorComponent={() => <View style={{ width: 16 }} />} ItemSeparatorComponent={() => <View style={{ width: 16 }} />}
estimatedItemSize={ITEM_WIDTH + 16}
/> />
</View> </View>
); );

View file

@ -10,7 +10,6 @@ import { MaterialCommunityIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { BlurView } from 'expo-blur'; import { BlurView } from 'expo-blur';
import { colors } from '../styles/colors'; import { colors } from '../styles/colors';
import { NuvioHeader } from '../components/NuvioHeader';
import { Stream } from '../types/streams'; import { Stream } from '../types/streams';
import { SafeAreaProvider } from 'react-native-safe-area-context'; import { SafeAreaProvider } from 'react-native-safe-area-context';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
@ -372,11 +371,11 @@ const TabScreenWrapper: React.FC<{children: React.ReactNode}> = ({ children }) =
position: 'relative', position: 'relative',
overflow: 'hidden' overflow: 'hidden'
}}> }}>
{/* Reserve consistent space for the header area on all screens */} {/* Optional reserved space behind content if needed */}
<View style={{ <View style={{
height: Platform.OS === 'android' ? 80 : 60, height: Platform.OS === 'android' ? 56 : 56,
width: '100%', width: '100%',
backgroundColor: colors.darkBackground, backgroundColor: 'transparent',
position: 'absolute', position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,
@ -403,136 +402,168 @@ const MainTabs = () => {
const renderTabBar = (props: BottomTabBarProps) => { const renderTabBar = (props: BottomTabBarProps) => {
return ( return (
<View style={{ <View
position: 'absolute', style={{
bottom: 0, position: 'absolute',
left: 0, top: 8,
right: 0, left: 0,
height: 85, right: 0,
backgroundColor: 'transparent', height: 40,
overflow: 'hidden', backgroundColor: 'transparent',
}}> alignItems: 'center',
justifyContent: 'flex-start',
overflow: 'visible',
zIndex: 100,
}}
>
{Platform.OS === 'ios' ? ( {Platform.OS === 'ios' ? (
<BlurView <BlurView
tint="dark" tint="dark"
intensity={75} intensity={75}
style={{ style={{
position: 'absolute', borderRadius: 20,
height: '100%', overflow: 'hidden',
width: '100%', paddingVertical: 6,
borderTopColor: currentTheme.colors.border, paddingHorizontal: 8,
borderTopWidth: 0.5, minWidth: 200,
shadowColor: currentTheme.colors.black, maxWidth: '90%',
shadowOffset: { width: 0, height: -2 },
shadowOpacity: 0.1,
shadowRadius: 3,
}} }}
/> >
) : ( <View style={{ flexDirection: 'row', alignItems: 'center' }}>
<LinearGradient {props.state.routes.map((route, index) => {
colors={[ const { options } = props.descriptors[route.key];
'rgba(0, 0, 0, 0)', const label =
'rgba(0, 0, 0, 0.65)', options.tabBarLabel !== undefined
'rgba(0, 0, 0, 0.85)', ? options.tabBarLabel
'rgba(0, 0, 0, 0.98)', : options.title !== undefined
]} ? options.title
locations={[0, 0.2, 0.4, 0.8]} : route.name;
style={{
position: 'absolute',
height: '100%',
width: '100%',
}}
/>
)}
<View
style={{
height: '100%',
paddingBottom: 20,
paddingTop: 12,
backgroundColor: 'transparent',
}}
>
<View style={{ flexDirection: 'row', paddingTop: 4 }}>
{props.state.routes.map((route, index) => {
const { options } = props.descriptors[route.key];
const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;
const isFocused = props.state.index === index; const isFocused = props.state.index === index;
const onPress = () => { const onPress = () => {
const event = props.navigation.emit({ const event = props.navigation.emit({
type: 'tabPress', type: 'tabPress',
target: route.key, target: route.key,
canPreventDefault: true, canPreventDefault: true,
}); });
if (!isFocused && !event.defaultPrevented) { if (!isFocused && !event.defaultPrevented) {
props.navigation.navigate(route.name); props.navigation.navigate(route.name);
} }
}; };
let iconName: IconNameType = 'home'; return (
switch (route.name) { <TouchableOpacity
case 'Home': key={route.key}
iconName = 'home'; activeOpacity={0.8}
break; onPress={onPress}
case 'Library': hasTVPreferredFocus={index === 0 && Platform.isTV}
iconName = 'play-box-multiple'; tvParallaxProperties={Platform.isTV ? {
break; enabled: true,
case 'Search': shiftDistanceX: 2.0,
iconName = 'feature-search'; shiftDistanceY: 2.0,
break; tiltAngle: 0.05,
case 'Settings': magnification: 1.05,
iconName = 'cog'; } : undefined}
break;
}
return (
<TouchableOpacity
key={route.key}
activeOpacity={0.7}
onPress={onPress}
hasTVPreferredFocus={index === 0 && Platform.isTV}
tvParallaxProperties={Platform.isTV ? {
enabled: true,
shiftDistanceX: 2.0,
shiftDistanceY: 2.0,
tiltAngle: 0.05,
magnification: 1.1,
} : undefined}
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'transparent',
}}
>
<TabIcon
focused={isFocused}
color={isFocused ? currentTheme.colors.primary : currentTheme.colors.white}
iconName={iconName}
/>
<Text
style={{ style={{
fontSize: 12, paddingVertical: 4,
fontWeight: '600', paddingHorizontal: 12,
marginTop: 4, marginHorizontal: 2,
color: isFocused ? currentTheme.colors.primary : currentTheme.colors.white, borderRadius: 14,
opacity: isFocused ? 1 : 0.7, backgroundColor: 'transparent',
}} }}
> >
{typeof label === 'string' ? label : ''} <Text
</Text> style={{
</TouchableOpacity> fontSize: 12,
); fontWeight: '600',
})} color: isFocused ? currentTheme.colors.primary : currentTheme.colors.white,
opacity: isFocused ? 1 : 0.75,
}}
>
{typeof label === 'string' ? label : ''}
</Text>
</TouchableOpacity>
);
})}
</View>
</BlurView>
) : (
<View
style={{
borderRadius: 20,
overflow: 'hidden',
paddingVertical: 6,
paddingHorizontal: 8,
minWidth: 200,
maxWidth: '90%',
backgroundColor: 'rgba(0,0,0,0.6)',
borderWidth: 0.5,
borderColor: currentTheme.colors.border,
}}
>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{props.state.routes.map((route, index) => {
const { options } = props.descriptors[route.key];
const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;
const isFocused = props.state.index === index;
const onPress = () => {
const event = props.navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
props.navigation.navigate(route.name);
}
};
return (
<TouchableOpacity
key={route.key}
activeOpacity={0.8}
onPress={onPress}
hasTVPreferredFocus={index === 0 && Platform.isTV}
tvParallaxProperties={Platform.isTV ? {
enabled: true,
shiftDistanceX: 2.0,
shiftDistanceY: 2.0,
tiltAngle: 0.05,
magnification: 1.05,
} : undefined}
style={{
paddingVertical: 4,
paddingHorizontal: 12,
marginHorizontal: 2,
borderRadius: 14,
backgroundColor: 'transparent',
}}
>
<Text
style={{
fontSize: 12,
fontWeight: '600',
color: isFocused ? currentTheme.colors.primary : currentTheme.colors.white,
opacity: isFocused ? 1 : 0.8,
}}
>
{typeof label === 'string' ? label : ''}
</Text>
</TouchableOpacity>
);
})}
</View>
</View> </View>
</View> )}
</View> </View>
); );
}; };
@ -578,8 +609,7 @@ const MainTabs = () => {
], ],
}, },
}), }),
header: () => (route.name === 'Home' ? <NuvioHeader /> : null), headerShown: false,
headerShown: route.name === 'Home',
tabBarShowLabel: false, tabBarShowLabel: false,
tabBarStyle: { tabBarStyle: {
position: 'absolute', position: 'absolute',

View file

@ -3,7 +3,6 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
FlatList,
TouchableOpacity, TouchableOpacity,
ActivityIndicator, ActivityIndicator,
SafeAreaView, SafeAreaView,
@ -18,6 +17,7 @@ import {
Pressable, Pressable,
Alert Alert
} from 'react-native'; } from 'react-native';
import { FlashList } from '@shopify/flash-list';
import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native';
import { RootStackParamList } from '../navigation/AppNavigator'; import { RootStackParamList } from '../navigation/AppNavigator';
@ -241,6 +241,12 @@ const HomeScreen = () => {
handleSaveToLibrary, handleSaveToLibrary,
refreshFeatured refreshFeatured
} = useFeaturedContent(); } = useFeaturedContent();
const [selectedContent, setSelectedContent] = useState<StreamingContent | null>(null);
useEffect(() => {
if (!selectedContent && featuredContent) {
setSelectedContent(featuredContent);
}
}, [featuredContent, selectedContent]);
// Progressive catalog loading function // Progressive catalog loading function
const loadCatalogsProgressively = useCallback(async () => { const loadCatalogsProgressively = useCallback(async () => {
@ -587,9 +593,7 @@ const HomeScreen = () => {
} }
// Normal flow when addons are present // Normal flow when addons are present
if (showHeroSection) { // Hero section will be rendered locked at the top outside the list
data.push({ type: 'featured', key: 'featured' });
}
data.push({ type: 'thisWeek', key: 'thisWeek' }); data.push({ type: 'thisWeek', key: 'thisWeek' });
data.push({ type: 'continueWatching', key: 'continueWatching' }); data.push({ type: 'continueWatching', key: 'continueWatching' });
@ -604,19 +608,12 @@ const HomeScreen = () => {
}); });
return data; return data;
}, [hasAddons, showHeroSection, catalogs]); }, [hasAddons, catalogs]);
const renderListItem = useCallback(({ item }: { item: HomeScreenListItem }) => { const renderListItem = useCallback(({ item }: { item: HomeScreenListItem }) => {
switch (item.type) { switch (item.type) {
case 'featured': case 'featured':
return ( return null;
<FeaturedContent
key={`featured-${showHeroSection}-${featuredContentSource}`}
featuredContent={featuredContent}
isSaved={isSaved}
handleSaveToLibrary={handleSaveToLibrary}
/>
);
case 'thisWeek': case 'thisWeek':
return <Animated.View entering={FadeIn.duration(300).delay(100)}><ThisWeekSection /></Animated.View>; return <Animated.View entering={FadeIn.duration(300).delay(100)}><ThisWeekSection /></Animated.View>;
case 'continueWatching': case 'continueWatching':
@ -624,7 +621,11 @@ const HomeScreen = () => {
case 'catalog': case 'catalog':
return ( return (
<Animated.View entering={FadeIn.duration(300)}> <Animated.View entering={FadeIn.duration(300)}>
<CatalogSection catalog={item.catalog} onPosterPress={handlePosterPress} /> <CatalogSection
catalog={item.catalog}
onPosterPress={handlePosterPress}
onPosterFocus={(content) => setSelectedContent(content)}
/>
</Animated.View> </Animated.View>
); );
case 'placeholder': case 'placeholder':
@ -691,12 +692,10 @@ const HomeScreen = () => {
</> </>
), [catalogsLoading, catalogs, loadedCatalogCount, totalCatalogsRef.current, navigation, currentTheme.colors]); ), [catalogsLoading, catalogs, loadedCatalogCount, totalCatalogsRef.current, navigation, currentTheme.colors]);
// Memoize the main content section // Handle poster press from catalogs to navigate to Metadata
const [selectedContent, setSelectedContent] = useState<StreamingContent | null>(null);
const handlePosterPress = useCallback((content: StreamingContent) => { const handlePosterPress = useCallback((content: StreamingContent) => {
setSelectedContent(content); navigation.navigate('Metadata', { id: content.id, type: content.type });
}, []); }, [navigation]);
const renderMainContent = useMemo(() => { const renderMainContent = useMemo(() => {
if (isLoading) return null; if (isLoading) return null;
@ -708,31 +707,25 @@ const HomeScreen = () => {
backgroundColor="transparent" backgroundColor="transparent"
translucent translucent
/> />
<FlatList {/* Locked hero section using selectedContent */}
{showHeroSection && (
<View>
<FeaturedContent
featuredContent={selectedContent}
isSaved={isSaved}
handleSaveToLibrary={handleSaveToLibrary}
/>
</View>
)}
<FlashList
data={listData} data={listData}
renderItem={renderListItem} renderItem={renderListItem}
keyExtractor={item => item.key} keyExtractor={item => item.key}
contentContainerStyle={[ contentContainerStyle={StyleSheet.flatten([styles.scrollContent, { paddingTop: 0 }])}
styles.scrollContent,
{ paddingTop: 0 }
]}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
ListFooterComponent={ListFooterComponent} ListFooterComponent={ListFooterComponent}
initialNumToRender={5} estimatedItemSize={280}
maxToRenderPerBatch={5}
windowSize={11}
removeClippedSubviews={Platform.OS === 'android'}
onEndReachedThreshold={0.5} onEndReachedThreshold={0.5}
updateCellsBatchingPeriod={50}
maintainVisibleContentPosition={{
minIndexForVisible: 0,
autoscrollToTopThreshold: 10
}}
getItemLayout={(data, index) => ({
length: 280,
offset: index * 280,
index,
})}
/> />
</View> </View>
); );
@ -741,7 +734,11 @@ const HomeScreen = () => {
currentTheme.colors, currentTheme.colors,
listData, listData,
renderListItem, renderListItem,
ListFooterComponent ListFooterComponent,
showHeroSection,
selectedContent,
isSaved,
handleSaveToLibrary
]); ]);
return isLoading ? renderLoadingScreen : renderMainContent; return isLoading ? renderLoadingScreen : renderMainContent;

BIN
src/utils/.logger.ts.swp Normal file

Binary file not shown.