This commit is contained in:
Fredrik Burmester
2026-01-16 10:06:41 +01:00
parent fe26a74451
commit 55b897883b
5 changed files with 61 additions and 5 deletions

View File

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

View File

@@ -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 */}

View File

@@ -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<Props> = ({
const segments = useSegments();
const from = (segments as string[])[2] || "(home)";
// Track focus within section and scroll back to start when leaving
const flatListRef = useRef<FlatList<BaseItemDto>>(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<Props> = ({
<TVFocusablePoster
onPress={() => handleItemPress(item)}
hasTVPreferredFocus={isFirstItem}
onFocus={handleItemFocus}
onBlur={handleItemBlur}
>
{renderPoster()}
</TVFocusablePoster>
@@ -236,7 +259,14 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
</View>
);
},
[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<Props> = ({
</View>
) : (
<FlatList
ref={flatListRef}
horizontal
data={allItems}
keyExtractor={(item) => item.Id!}

View File

@@ -8,6 +8,8 @@ interface TVFocusablePosterProps {
glowColor?: "white" | "purple";
scaleAmount?: number;
style?: ViewStyle;
onFocus?: () => void;
onBlur?: () => void;
}
export const TVFocusablePoster: React.FC<TVFocusablePosterProps> = ({
@@ -17,6 +19,8 @@ export const TVFocusablePoster: React.FC<TVFocusablePosterProps> = ({
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<TVFocusablePosterProps> = ({
onFocus={() => {
setFocused(true);
animateTo(scaleAmount);
onFocusProp?.();
}}
onBlur={() => {
setFocused(false);
animateTo(1);
onBlurProp?.();
}}
hasTVPreferredFocus={hasTVPreferredFocus}
>

View File

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