Merge branch 'feat/tv-interface' of https://github.com/streamyfin/streamyfin into feat/tv-interface

Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
This commit is contained in:
Lance Chant
2026-05-25 10:35:14 +02:00
17 changed files with 141 additions and 57 deletions

View File

@@ -14,6 +14,7 @@ import {
} from "react";
import { useTranslation } from "react-i18next";
import {
Pressable,
StyleSheet,
TVFocusGuideView,
useWindowDimensions,
@@ -278,6 +279,9 @@ export const Controls: FC<Props> = ({
null,
);
// Ref for the invisible focus-stealing overlay (prevents hidden buttons from receiving select events)
const focusOverlayRef = useRef<View>(null);
const audioTracks = useMemo(() => {
return mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") ?? [];
}, [mediaSource]);
@@ -908,6 +912,33 @@ export const Controls: FC<Props> = ({
setFocusPlayButton(false);
}, [setShowControls]);
// On initial mount when controls start visible, focus the play button.
// playButtonRef transitions from null → View on first render; once set,
// this effect won't re-fire (playButtonRef is a stable reference).
const initialFocusDone = useRef(false);
useEffect(() => {
if (!initialFocusDone.current && playButtonRef && showControls) {
initialFocusDone.current = true;
const t = setTimeout(() => {
playButtonRef.focus();
}, 100);
return () => clearTimeout(t);
}
}, [showControls, playButtonRef]);
// When controls hide (and no skip/countdown overlay is visible), move focus
// to the invisible overlay so hidden buttons can't receive select events.
useEffect(() => {
if (!showControls && !isSkipOrCountdownVisible) {
// Small delay to let the controls fade-out animation start and
// the focus engine settle before stealing focus
const t = setTimeout(() => {
focusOverlayRef.current?.focus();
}, 100);
return () => clearTimeout(t);
}
}, [showControls, isSkipOrCountdownVisible]);
const handleBack = useCallback(() => {
router.back();
}, [router]);
@@ -1025,6 +1056,24 @@ export const Controls: FC<Props> = ({
pointerEvents='none'
/>
{/* Invisible overlay that steals focus when controls are hidden.
Prevents hidden control buttons from receiving select/enter events
from the TV remote. Pressing center button here toggles play/pause. */}
<Pressable
ref={focusOverlayRef}
style={styles.focusStealingOverlay}
pointerEvents={
showControls || isSkipOrCountdownVisible ? "none" : "auto"
}
focusable={!showControls && !isSkipOrCountdownVisible}
hasTVPreferredFocus={!showControls && !isSkipOrCountdownVisible}
onPress={() => {
togglePlay();
setShowControls(true);
setFocusPlayButton(true);
}}
/>
{getTechnicalInfo && (
<TechnicalInfoOverlay
showControls={showControls}
@@ -1177,6 +1226,7 @@ export const Controls: FC<Props> = ({
<Animated.View
style={[styles.bottomContainer, bottomAnimatedStyle]}
pointerEvents={showControls ? "auto" : "none"}
focusable={showControls}
>
<View
style={[
@@ -1379,6 +1429,10 @@ const styles = StyleSheet.create({
...StyleSheet.absoluteFillObject,
backgroundColor: "rgba(0, 0, 0, 0.4)",
},
focusStealingOverlay: {
...StyleSheet.absoluteFillObject,
zIndex: 1,
},
bottomContainer: {
position: "absolute",
bottom: 0,