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/stack": "^7.2.10",
"@sentry/react-native": "^6.15.1",
"@shopify/flash-list": "^1.8.3",
"@types/lodash": "^4.17.16",
"@types/react-native-video": "^5.0.20",
"axios": "^1.10.0",
@ -40,7 +41,7 @@
"react-native-gesture-handler": "~2.20.2",
"react-native-immersive-mode": "^2.0.2",
"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-screens": "~4.4.0",
"react-native-svg": "^15.11.2",
@ -3046,128 +3047,6 @@
"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": {
"version": "8.55.0",
"license": "MIT",
@ -3236,6 +3115,21 @@
"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": {
"version": "0.27.8",
"license": "MIT"
@ -7148,186 +7042,6 @@
"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": {
"version": "1.2.4",
"license": "MIT"
@ -9149,9 +8863,9 @@
"license": "MIT"
},
"node_modules/react-native-reanimated": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.6.3.tgz",
"integrity": "sha512-2KkkPozoIvDbJcHuf8qeyoLROXQxizSi+2CTCkuNVkVZOxxY4B0Omvgq61aOQhSZUh/649x1YHoAaTyGMGDJUw==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.6.0.tgz",
"integrity": "sha512-eDdhZTRYofrIqFB/Z5xLTWxcB7wDj4ifrNm+gZ2xHSZPjAQ747ukDdH9rglPyPmi+GcmDH7Wff411Xsw5fm45Q==",
"license": "MIT",
"dependencies": {
"@babel/plugin-transform-object-assign": "^7.16.7",
@ -9521,6 +9235,21 @@
"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": {
"version": "1.4.2",
"license": "MIT"
@ -10834,6 +10563,12 @@
"version": "0.1.13",
"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",
"license": "0BSD"

View file

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

View file

@ -1,5 +1,6 @@
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 { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
@ -12,6 +13,7 @@ import { RootStackParamList } from '../../navigation/AppNavigator';
interface CatalogSectionProps {
catalog: CatalogContent;
onPosterPress?: (content: StreamingContent) => void;
onPosterFocus?: (content: StreamingContent) => void;
}
const { width } = Dimensions.get('window');
@ -54,7 +56,7 @@ const calculatePosterLayout = (screenWidth: number) => {
const posterLayout = calculatePosterLayout(width);
const POSTER_WIDTH = posterLayout.posterWidth;
const CatalogSection = ({ catalog, onPosterPress }: CatalogSectionProps) => {
const CatalogSection = ({ catalog, onPosterPress, onPosterFocus }: CatalogSectionProps) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
@ -75,6 +77,7 @@ const CatalogSection = ({ catalog, onPosterPress }: CatalogSectionProps) => {
<ContentItem
item={item}
onPress={handleContentPress}
onFocusItem={onPosterFocus}
/>
</Animated.View>
);
@ -105,7 +108,7 @@ const CatalogSection = ({ catalog, onPosterPress }: CatalogSectionProps) => {
</TouchableOpacity>
</View>
<FlatList
<FlashList
data={catalog.items}
renderItem={renderContentItem}
keyExtractor={(item) => `${item.id}-${item.type}`}
@ -116,19 +119,7 @@ const CatalogSection = ({ catalog, onPosterPress }: CatalogSectionProps) => {
decelerationRate="fast"
snapToAlignment="start"
ItemSeparatorComponent={() => <View style={{ width: 8 }} />}
initialNumToRender={3}
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
}}
estimatedItemSize={POSTER_WIDTH + 8}
onEndReachedThreshold={1}
// TV-specific focus navigation properties
{...(Platform.isTV && {

View file

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

View file

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

View file

@ -252,29 +252,7 @@ const FeaturedContent = ({ featuredContent }: FeaturedContentProps) => {
)}
</View>
{/* Action Buttons */}
<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>
{/* Action buttons removed per design */}
</View>
@ -287,7 +265,7 @@ const FeaturedContent = ({ featuredContent }: FeaturedContentProps) => {
const styles = StyleSheet.create({
featuredContainer: {
width: '100%',
height: Platform.isTV ? height * 0.5 : height * 0.4,
height: Platform.isTV ? height * 0.65 : height * 0.5,
marginTop: 0,
marginBottom: Platform.isTV ? 16 : 8,
position: 'relative',
@ -471,33 +449,7 @@ const styles = StyleSheet.create({
fontSize: Platform.isTV ? 18 : 16,
fontWeight: '600',
},
actionButtonsContainer: {
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,
},
// Action buttons removed
});

View file

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

View file

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

View file

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

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

Binary file not shown.