fix(tv): seek

This commit is contained in:
Fredrik Burmester
2026-01-20 22:15:00 +01:00
parent 096670a0c3
commit d2790f4997
3 changed files with 138 additions and 55 deletions

View File

@@ -129,6 +129,9 @@ export const Controls: FC<Props> = ({
type LastModalType = "audio" | "subtitle" | null;
const [lastOpenedModal, setLastOpenedModal] = useState<LastModalType>(null);
// Track if play button should have focus (when showing controls via up/down D-pad)
const [focusPlayButton, setFocusPlayButton] = useState(false);
// State for progress bar focus and focus guide refs
const [isProgressBarFocused, setIsProgressBarFocused] = useState(false);
const [playButtonRef, setPlayButtonRef] = useState<View | null>(null);
@@ -308,6 +311,27 @@ export const Controls: FC<Props> = ({
}, 2500);
}, []);
// Show minimal seek bar without auto-hide (for continuous seeking)
const showMinimalSeekPersistent = useCallback(() => {
setShowMinimalSeekBar(true);
// Clear existing timeout - don't set a new one
if (minimalSeekBarTimeoutRef.current) {
clearTimeout(minimalSeekBarTimeoutRef.current);
minimalSeekBarTimeoutRef.current = null;
}
}, []);
// Start the minimal seek bar hide timeout
const startMinimalSeekHideTimeout = useCallback(() => {
if (minimalSeekBarTimeoutRef.current) {
clearTimeout(minimalSeekBarTimeoutRef.current);
}
minimalSeekBarTimeoutRef.current = setTimeout(() => {
setShowMinimalSeekBar(false);
}, 2500);
}, []);
// Reset minimal seek bar timeout (call on each seek action)
const _resetMinimalSeekTimeout = useCallback(() => {
if (minimalSeekBarTimeoutRef.current) {
@@ -513,39 +537,7 @@ export const Controls: FC<Props> = ({
showMinimalSeek,
]);
// Callback for remote interactions to reset timeout
const handleRemoteInteraction = useCallback(() => {
controlsInteractionRef.current();
}, []);
const { isSliding: isRemoteSliding } = useRemoteControl({
showControls,
toggleControls,
togglePlay,
onBack: handleBack,
isProgressBarFocused,
onSeekLeft: handleProgressSeekLeft,
onSeekRight: handleProgressSeekRight,
onMinimalSeekLeft: handleMinimalSeekLeft,
onMinimalSeekRight: handleMinimalSeekRight,
onInteraction: handleRemoteInteraction,
});
const hideControls = useCallback(() => {
setShowControls(false);
}, [setShowControls]);
const { handleControlsInteraction } = useControlsTimeout({
showControls,
isSliding: isRemoteSliding,
episodeView: false,
onHideControls: hideControls,
timeout: TV_AUTO_HIDE_TIMEOUT,
disabled: false,
});
controlsInteractionRef.current = handleControlsInteraction;
// Continuous seeking functions (for button long-press and D-pad long-press)
const stopContinuousSeeking = useCallback(() => {
if (continuousSeekRef.current) {
clearInterval(continuousSeekRef.current);
@@ -559,7 +551,10 @@ export const Controls: FC<Props> = ({
seekBubbleTimeoutRef.current = setTimeout(() => {
setShowSeekBubble(false);
}, 2000);
}, []);
// Start minimal seekbar hide timeout (if it's showing)
startMinimalSeekHideTimeout();
}, [startMinimalSeekHideTimeout]);
const startContinuousSeekForward = useCallback(() => {
seekAccelerationRef.current = 1;
@@ -621,6 +616,65 @@ export const Controls: FC<Props> = ({
updateSeekBubbleTime,
]);
// D-pad long press handlers - show minimal seekbar when controls are hidden
const handleDpadLongSeekForward = useCallback(() => {
if (!showControls) {
showMinimalSeekPersistent();
}
startContinuousSeekForward();
}, [showControls, showMinimalSeekPersistent, startContinuousSeekForward]);
const handleDpadLongSeekBackward = useCallback(() => {
if (!showControls) {
showMinimalSeekPersistent();
}
startContinuousSeekBackward();
}, [showControls, showMinimalSeekPersistent, startContinuousSeekBackward]);
// Callback for remote interactions to reset timeout
const handleRemoteInteraction = useCallback(() => {
controlsInteractionRef.current();
}, []);
// Callback for up/down D-pad - show controls with play button focused
const handleVerticalDpad = useCallback(() => {
setFocusPlayButton(true);
setShowControls(true);
}, [setShowControls]);
const { isSliding: isRemoteSliding } = useRemoteControl({
showControls,
toggleControls,
togglePlay,
onBack: handleBack,
isProgressBarFocused,
onSeekLeft: handleProgressSeekLeft,
onSeekRight: handleProgressSeekRight,
onMinimalSeekLeft: handleMinimalSeekLeft,
onMinimalSeekRight: handleMinimalSeekRight,
onInteraction: handleRemoteInteraction,
onLongSeekLeftStart: handleDpadLongSeekBackward,
onLongSeekRightStart: handleDpadLongSeekForward,
onLongSeekStop: stopContinuousSeeking,
onVerticalDpad: handleVerticalDpad,
});
const hideControls = useCallback(() => {
setShowControls(false);
setFocusPlayButton(false);
}, [setShowControls]);
const { handleControlsInteraction } = useControlsTimeout({
showControls,
isSliding: isRemoteSliding,
episodeView: false,
onHideControls: hideControls,
timeout: TV_AUTO_HIDE_TIMEOUT,
disabled: false,
});
controlsInteractionRef.current = handleControlsInteraction;
const handlePlayPauseButton = useCallback(() => {
togglePlay();
controlsInteractionRef.current();
@@ -820,28 +874,13 @@ export const Controls: FC<Props> = ({
disabled={false || !previousItem}
size={28}
/>
<TVControlButton
icon='play-back'
onPress={handleSeekBackwardButton}
onLongPress={startContinuousSeekBackward}
onPressOut={stopContinuousSeeking}
disabled={false}
size={28}
/>
<TVControlButton
icon={isPlaying ? "pause" : "play"}
onPress={handlePlayPauseButton}
disabled={false}
size={36}
refSetter={setPlayButtonRef}
/>
<TVControlButton
icon='play-forward'
onPress={handleSeekForwardButton}
onLongPress={startContinuousSeekForward}
onPressOut={stopContinuousSeeking}
disabled={false}
size={28}
hasTVPreferredFocus={focusPlayButton && lastOpenedModal === null}
/>
<TVControlButton
icon='play-skip-forward'
@@ -906,7 +945,7 @@ export const Controls: FC<Props> = ({
onFocus={() => setIsProgressBarFocused(true)}
onBlur={() => setIsProgressBarFocused(false)}
refSetter={setProgressBarRef}
hasTVPreferredFocus={lastOpenedModal === null}
hasTVPreferredFocus={lastOpenedModal === null && !focusPlayButton}
/>
</TVFocusGuideView>

View File

@@ -5,8 +5,8 @@ export const CONTROLS_CONSTANTS = {
TILE_WIDTH: 150,
PROGRESS_UNIT_MS: 1000, // 1 second in ms
PROGRESS_UNIT_TICKS: 10000000, // 1 second in ticks
LONG_PRESS_INITIAL_SEEK: 10,
LONG_PRESS_ACCELERATION: 1.1,
LONG_PRESS_INITIAL_SEEK: 30,
LONG_PRESS_ACCELERATION: 1.2,
LONG_PRESS_INTERVAL: 300,
SLIDER_DEBOUNCE_MS: 3,
} as const;

View File

@@ -35,6 +35,14 @@ interface UseRemoteControlProps {
onMinimalSeekRight?: () => void;
/** Callback for any interaction that should reset the controls timeout */
onInteraction?: () => void;
/** Callback when long press seek left starts (eventKeyAction: 0) */
onLongSeekLeftStart?: () => void;
/** Callback when long press seek right starts (eventKeyAction: 0) */
onLongSeekRightStart?: () => void;
/** Callback when long press seek ends (eventKeyAction: 1) */
onLongSeekStop?: () => void;
/** Callback when up/down D-pad pressed (to show controls with play button focused) */
onVerticalDpad?: () => void;
// Legacy props - kept for backwards compatibility with mobile Controls.tsx
// These are ignored in the simplified implementation
progress?: SharedValue<number>;
@@ -67,6 +75,10 @@ export function useRemoteControl({
onMinimalSeekLeft,
onMinimalSeekRight,
onInteraction,
onLongSeekLeftStart,
onLongSeekRightStart,
onLongSeekStop,
onVerticalDpad,
}: UseRemoteControlProps) {
// Keep these for backward compatibility with the component
const remoteScrubProgress = useSharedValue<number | null>(null);
@@ -96,9 +108,33 @@ export function useRemoteControl({
return;
}
// Handle left/right D-pad - check controls hidden state FIRST
// Handle long press D-pad for continuous seeking (works in both modes)
// Must be checked BEFORE the showControls check to work when controls are hidden
if (evt.eventType === "longLeft") {
if (evt.eventKeyAction === 0 && onLongSeekLeftStart) {
// Key pressed - start continuous seeking backward
onLongSeekLeftStart();
} else if (evt.eventKeyAction === 1 && onLongSeekStop) {
// Key released - stop seeking
onLongSeekStop();
}
return;
}
if (evt.eventType === "longRight") {
if (evt.eventKeyAction === 0 && onLongSeekRightStart) {
// Key pressed - start continuous seeking forward
onLongSeekRightStart();
} else if (evt.eventKeyAction === 1 && onLongSeekStop) {
// Key released - stop seeking
onLongSeekStop();
}
return;
}
// Handle D-pad when controls are hidden
if (!showControls) {
// Minimal seek mode when controls are hidden
// Minimal seek mode for left/right
if (evt.eventType === "left" && onMinimalSeekLeft) {
onMinimalSeekLeft();
return;
@@ -107,6 +143,14 @@ export function useRemoteControl({
onMinimalSeekRight();
return;
}
// Up/down shows controls with play button focused
if (
(evt.eventType === "up" || evt.eventType === "down") &&
onVerticalDpad
) {
onVerticalDpad();
return;
}
// For other D-pad presses, show full controls
toggleControls();
return;