mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-03-09 19:26:15 +00:00
fix(tv): seek
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user