diff --git a/app.json b/app.json index 288f3e3a..38b77a23 100644 --- a/app.json +++ b/app.json @@ -36,6 +36,10 @@ "icon": "./assets/images/icon-ios-liquid-glass.icon", "appleTeamId": "MWD5K362T8" }, + "tvos": { + "icon": "./assets/images/icon.png", + "bundleIdentifier": "com.fredrikburmester.streamyfin" + }, "android": { "jsEngine": "hermes", "versionCode": 92, diff --git a/components/ItemContentSkeleton.tv.tsx b/components/ItemContentSkeleton.tv.tsx index 2e4a77ea..6b106937 100644 --- a/components/ItemContentSkeleton.tv.tsx +++ b/components/ItemContentSkeleton.tv.tsx @@ -9,8 +9,8 @@ export const ItemContentSkeletonTV: React.FC = () => { style={{ flex: 1, flexDirection: "row", - paddingTop: 140, - paddingHorizontal: 80, + paddingTop: 180, + paddingHorizontal: 160, }} > {/* Left side - Poster placeholder */} diff --git a/components/home/InfiniteScrollingCollectionList.tv.tsx b/components/home/InfiniteScrollingCollectionList.tv.tsx index ee834e7b..b09a72f7 100644 --- a/components/home/InfiniteScrollingCollectionList.tv.tsx +++ b/components/home/InfiniteScrollingCollectionList.tv.tsx @@ -5,7 +5,7 @@ import { useInfiniteQuery, } from "@tanstack/react-query"; import { useSegments } from "expo-router"; -import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { ActivityIndicator, @@ -95,6 +95,27 @@ export const InfiniteScrollingCollectionList: React.FC = ({ const segments = useSegments(); const from = (segments as string[])[2] || "(home)"; + // Track focus within section and scroll back to start when leaving + const flatListRef = useRef>(null); + const [focusedCount, setFocusedCount] = useState(0); + const prevFocusedCount = useRef(0); + + // When section loses all focus, scroll back to start + useEffect(() => { + if (prevFocusedCount.current > 0 && focusedCount === 0) { + flatListRef.current?.scrollToOffset({ offset: 0, animated: true }); + } + prevFocusedCount.current = focusedCount; + }, [focusedCount]); + + const handleItemFocus = useCallback(() => { + setFocusedCount((c) => c + 1); + }, []); + + const handleItemBlur = useCallback(() => { + setFocusedCount((c) => Math.max(0, c - 1)); + }, []); + const { data, isLoading, @@ -229,6 +250,8 @@ export const InfiniteScrollingCollectionList: React.FC = ({ handleItemPress(item)} hasTVPreferredFocus={isFirstItem} + onFocus={handleItemFocus} + onBlur={handleItemBlur} > {renderPoster()} @@ -236,7 +259,14 @@ export const InfiniteScrollingCollectionList: React.FC = ({ ); }, - [orientation, isFirstSection, itemWidth, handleItemPress], + [ + orientation, + isFirstSection, + itemWidth, + handleItemPress, + handleItemFocus, + handleItemBlur, + ], ); if (hideIfEmpty === true && allItems.length === 0 && !isLoading) return null; @@ -310,6 +340,7 @@ export const InfiniteScrollingCollectionList: React.FC = ({ ) : ( item.Id!} diff --git a/components/tv/TVFocusablePoster.tsx b/components/tv/TVFocusablePoster.tsx index 9748ed41..fc89b70f 100644 --- a/components/tv/TVFocusablePoster.tsx +++ b/components/tv/TVFocusablePoster.tsx @@ -8,6 +8,8 @@ interface TVFocusablePosterProps { glowColor?: "white" | "purple"; scaleAmount?: number; style?: ViewStyle; + onFocus?: () => void; + onBlur?: () => void; } export const TVFocusablePoster: React.FC = ({ @@ -17,6 +19,8 @@ export const TVFocusablePoster: React.FC = ({ glowColor = "white", scaleAmount = 1.05, style, + onFocus: onFocusProp, + onBlur: onBlurProp, }) => { const [focused, setFocused] = useState(false); const scale = useRef(new Animated.Value(1)).current; @@ -37,10 +41,12 @@ export const TVFocusablePoster: React.FC = ({ onFocus={() => { setFocused(true); animateTo(scaleAmount); + onFocusProp?.(); }} onBlur={() => { setFocused(false); animateTo(1); + onBlurProp?.(); }} hasTVPreferredFocus={hasTVPreferredFocus} > diff --git a/eas.json b/eas.json index 8a9736d3..5ecd93c3 100644 --- a/eas.json +++ b/eas.json @@ -43,6 +43,13 @@ "EXPO_PUBLIC_WRITE_DEBUG": "1" } }, + "preview_tv": { + "distribution": "internal", + "env": { + "EXPO_TV": "1", + "EXPO_PUBLIC_WRITE_DEBUG": "1" + } + }, "production": { "environment": "production", "channel": "0.52.0", @@ -68,9 +75,17 @@ "env": { "EXPO_TV": "1" } + }, + "production_tv": { + "environment": "production", + "channel": "0.52.0", + "env": { + "EXPO_TV": "1" + } } }, "submit": { - "production": {} + "production": {}, + "production_tv": {} } }