mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-08 06:58:38 +01:00
feat(tv): minimal seekbar
This commit is contained in:
@@ -134,6 +134,13 @@ export const Controls: FC<Props> = ({
|
|||||||
const [playButtonRef, setPlayButtonRef] = useState<View | null>(null);
|
const [playButtonRef, setPlayButtonRef] = useState<View | null>(null);
|
||||||
const [progressBarRef, setProgressBarRef] = useState<View | null>(null);
|
const [progressBarRef, setProgressBarRef] = useState<View | null>(null);
|
||||||
|
|
||||||
|
// Minimal seek bar state (shows only progress bar when seeking while controls hidden)
|
||||||
|
const [showMinimalSeekBar, setShowMinimalSeekBar] = useState(false);
|
||||||
|
const minimalSeekBarOpacity = useSharedValue(0);
|
||||||
|
const minimalSeekBarTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const audioTracks = useMemo(() => {
|
const audioTracks = useMemo(() => {
|
||||||
return mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") ?? [];
|
return mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") ?? [];
|
||||||
}, [mediaSource]);
|
}, [mediaSource]);
|
||||||
@@ -200,6 +207,22 @@ export const Controls: FC<Props> = ({
|
|||||||
transform: [{ translateY: bottomTranslateY.value }],
|
transform: [{ translateY: bottomTranslateY.value }],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Minimal seek bar animation
|
||||||
|
useEffect(() => {
|
||||||
|
const animationConfig = {
|
||||||
|
duration: 200,
|
||||||
|
easing: Easing.out(Easing.quad),
|
||||||
|
};
|
||||||
|
minimalSeekBarOpacity.value = withTiming(
|
||||||
|
showMinimalSeekBar ? 1 : 0,
|
||||||
|
animationConfig,
|
||||||
|
);
|
||||||
|
}, [showMinimalSeekBar, minimalSeekBarOpacity]);
|
||||||
|
|
||||||
|
const minimalSeekBarAnimatedStyle = useAnimatedStyle(() => ({
|
||||||
|
opacity: minimalSeekBarOpacity.value,
|
||||||
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (item) {
|
if (item) {
|
||||||
progress.value = ticksToMs(item?.UserData?.PlaybackPositionTicks);
|
progress.value = ticksToMs(item?.UserData?.PlaybackPositionTicks);
|
||||||
@@ -255,6 +278,31 @@ export const Controls: FC<Props> = ({
|
|||||||
// No longer needed since modals are screen-based
|
// No longer needed since modals are screen-based
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Show minimal seek bar (only progress bar, no buttons)
|
||||||
|
const showMinimalSeek = useCallback(() => {
|
||||||
|
setShowMinimalSeekBar(true);
|
||||||
|
|
||||||
|
// Clear existing timeout
|
||||||
|
if (minimalSeekBarTimeoutRef.current) {
|
||||||
|
clearTimeout(minimalSeekBarTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-hide after timeout
|
||||||
|
minimalSeekBarTimeoutRef.current = setTimeout(() => {
|
||||||
|
setShowMinimalSeekBar(false);
|
||||||
|
}, 2500);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Reset minimal seek bar timeout (call on each seek action)
|
||||||
|
const _resetMinimalSeekTimeout = useCallback(() => {
|
||||||
|
if (minimalSeekBarTimeoutRef.current) {
|
||||||
|
clearTimeout(minimalSeekBarTimeoutRef.current);
|
||||||
|
}
|
||||||
|
minimalSeekBarTimeoutRef.current = setTimeout(() => {
|
||||||
|
setShowMinimalSeekBar(false);
|
||||||
|
}, 2500);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleOpenAudioSheet = useCallback(() => {
|
const handleOpenAudioSheet = useCallback(() => {
|
||||||
setLastOpenedModal("audio");
|
setLastOpenedModal("audio");
|
||||||
showOptions({
|
showOptions({
|
||||||
@@ -395,6 +443,61 @@ export const Controls: FC<Props> = ({
|
|||||||
controlsInteractionRef.current();
|
controlsInteractionRef.current();
|
||||||
}, [progress, min, seek, calculateTrickplayUrl, updateSeekBubbleTime]);
|
}, [progress, min, seek, calculateTrickplayUrl, updateSeekBubbleTime]);
|
||||||
|
|
||||||
|
// Minimal seek mode handlers (only show progress bar, not full controls)
|
||||||
|
const handleMinimalSeekRight = useCallback(() => {
|
||||||
|
const newPosition = Math.min(max.value, progress.value + 10 * 1000);
|
||||||
|
progress.value = newPosition;
|
||||||
|
seek(newPosition);
|
||||||
|
|
||||||
|
calculateTrickplayUrl(msToTicks(newPosition));
|
||||||
|
updateSeekBubbleTime(newPosition);
|
||||||
|
setShowSeekBubble(true);
|
||||||
|
|
||||||
|
// Show minimal seek bar and reset its timeout
|
||||||
|
showMinimalSeek();
|
||||||
|
|
||||||
|
if (seekBubbleTimeoutRef.current) {
|
||||||
|
clearTimeout(seekBubbleTimeoutRef.current);
|
||||||
|
}
|
||||||
|
seekBubbleTimeoutRef.current = setTimeout(() => {
|
||||||
|
setShowSeekBubble(false);
|
||||||
|
}, 2000);
|
||||||
|
}, [
|
||||||
|
progress,
|
||||||
|
max,
|
||||||
|
seek,
|
||||||
|
calculateTrickplayUrl,
|
||||||
|
updateSeekBubbleTime,
|
||||||
|
showMinimalSeek,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleMinimalSeekLeft = useCallback(() => {
|
||||||
|
const newPosition = Math.max(min.value, progress.value - 10 * 1000);
|
||||||
|
progress.value = newPosition;
|
||||||
|
seek(newPosition);
|
||||||
|
|
||||||
|
calculateTrickplayUrl(msToTicks(newPosition));
|
||||||
|
updateSeekBubbleTime(newPosition);
|
||||||
|
setShowSeekBubble(true);
|
||||||
|
|
||||||
|
// Show minimal seek bar and reset its timeout
|
||||||
|
showMinimalSeek();
|
||||||
|
|
||||||
|
if (seekBubbleTimeoutRef.current) {
|
||||||
|
clearTimeout(seekBubbleTimeoutRef.current);
|
||||||
|
}
|
||||||
|
seekBubbleTimeoutRef.current = setTimeout(() => {
|
||||||
|
setShowSeekBubble(false);
|
||||||
|
}, 2000);
|
||||||
|
}, [
|
||||||
|
progress,
|
||||||
|
min,
|
||||||
|
seek,
|
||||||
|
calculateTrickplayUrl,
|
||||||
|
updateSeekBubbleTime,
|
||||||
|
showMinimalSeek,
|
||||||
|
]);
|
||||||
|
|
||||||
// Callback for remote interactions to reset timeout
|
// Callback for remote interactions to reset timeout
|
||||||
const handleRemoteInteraction = useCallback(() => {
|
const handleRemoteInteraction = useCallback(() => {
|
||||||
controlsInteractionRef.current();
|
controlsInteractionRef.current();
|
||||||
@@ -408,6 +511,8 @@ export const Controls: FC<Props> = ({
|
|||||||
isProgressBarFocused,
|
isProgressBarFocused,
|
||||||
onSeekLeft: handleProgressSeekLeft,
|
onSeekLeft: handleProgressSeekLeft,
|
||||||
onSeekRight: handleProgressSeekRight,
|
onSeekRight: handleProgressSeekRight,
|
||||||
|
onMinimalSeekLeft: handleMinimalSeekLeft,
|
||||||
|
onMinimalSeekRight: handleMinimalSeekRight,
|
||||||
onInteraction: handleRemoteInteraction,
|
onInteraction: handleRemoteInteraction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -598,6 +703,63 @@ export const Controls: FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Minimal seek bar - shows only progress bar when seeking while controls hidden */}
|
||||||
|
<Animated.View
|
||||||
|
style={[styles.minimalSeekBarContainer, minimalSeekBarAnimatedStyle]}
|
||||||
|
pointerEvents={showMinimalSeekBar && !showControls ? "auto" : "none"}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.minimalSeekBarInner,
|
||||||
|
{
|
||||||
|
paddingRight: Math.max(insets.right, 48),
|
||||||
|
paddingLeft: Math.max(insets.left, 48),
|
||||||
|
paddingBottom: Math.max(insets.bottom, 24),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{showSeekBubble && (
|
||||||
|
<View style={styles.minimalTrickplayContainer}>
|
||||||
|
<TrickplayBubble
|
||||||
|
trickPlayUrl={trickPlayUrl}
|
||||||
|
trickplayInfo={trickplayInfo}
|
||||||
|
time={seekBubbleTime}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.minimalProgressContainer}>
|
||||||
|
<View style={styles.progressTrack}>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.cacheProgress,
|
||||||
|
useAnimatedStyle(() => ({
|
||||||
|
width: `${max.value > 0 ? (cacheProgress.value / max.value) * 100 : 0}%`,
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.progressFill,
|
||||||
|
useAnimatedStyle(() => ({
|
||||||
|
width: `${max.value > 0 ? (effectiveProgress.value / max.value) * 100 : 0}%`,
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.minimalTimeContainer}>
|
||||||
|
<Text style={styles.timeText}>
|
||||||
|
{formatTimeString(currentTime, "ms")}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.timeText}>
|
||||||
|
-{formatTimeString(remainingTime, "ms")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[styles.bottomContainer, bottomAnimatedStyle]}
|
style={[styles.bottomContainer, bottomAnimatedStyle]}
|
||||||
pointerEvents={showControls && !false ? "auto" : "none"}
|
pointerEvents={showControls && !false ? "auto" : "none"}
|
||||||
@@ -847,4 +1009,29 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
|
// Minimal seek bar styles
|
||||||
|
minimalSeekBarContainer: {
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: 5,
|
||||||
|
},
|
||||||
|
minimalSeekBarInner: {
|
||||||
|
flexDirection: "column",
|
||||||
|
},
|
||||||
|
minimalTrickplayContainer: {
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
minimalProgressContainer: {
|
||||||
|
height: TV_SEEKBAR_HEIGHT,
|
||||||
|
justifyContent: "center",
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
minimalTimeContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ interface UseRemoteControlProps {
|
|||||||
onSeekLeft?: () => void;
|
onSeekLeft?: () => void;
|
||||||
/** Callback for seeking right when progress bar is focused */
|
/** Callback for seeking right when progress bar is focused */
|
||||||
onSeekRight?: () => void;
|
onSeekRight?: () => void;
|
||||||
|
/** Callback for seeking left when controls are hidden (minimal seek mode) */
|
||||||
|
onMinimalSeekLeft?: () => void;
|
||||||
|
/** Callback for seeking right when controls are hidden (minimal seek mode) */
|
||||||
|
onMinimalSeekRight?: () => void;
|
||||||
/** Callback for any interaction that should reset the controls timeout */
|
/** Callback for any interaction that should reset the controls timeout */
|
||||||
onInteraction?: () => void;
|
onInteraction?: () => void;
|
||||||
// Legacy props - kept for backwards compatibility with mobile Controls.tsx
|
// Legacy props - kept for backwards compatibility with mobile Controls.tsx
|
||||||
@@ -60,6 +64,8 @@ export function useRemoteControl({
|
|||||||
isProgressBarFocused,
|
isProgressBarFocused,
|
||||||
onSeekLeft,
|
onSeekLeft,
|
||||||
onSeekRight,
|
onSeekRight,
|
||||||
|
onMinimalSeekLeft,
|
||||||
|
onMinimalSeekRight,
|
||||||
onInteraction,
|
onInteraction,
|
||||||
}: UseRemoteControlProps) {
|
}: UseRemoteControlProps) {
|
||||||
// Keep these for backward compatibility with the component
|
// Keep these for backward compatibility with the component
|
||||||
@@ -90,7 +96,23 @@ export function useRemoteControl({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle left/right D-pad seeking when progress bar is focused
|
// Handle left/right D-pad - check controls hidden state FIRST
|
||||||
|
if (!showControls) {
|
||||||
|
// Minimal seek mode when controls are hidden
|
||||||
|
if (evt.eventType === "left" && onMinimalSeekLeft) {
|
||||||
|
onMinimalSeekLeft();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (evt.eventType === "right" && onMinimalSeekRight) {
|
||||||
|
onMinimalSeekRight();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// For other D-pad presses, show full controls
|
||||||
|
toggleControls();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controls are showing - handle seeking when progress bar is focused
|
||||||
if (isProgressBarFocused) {
|
if (isProgressBarFocused) {
|
||||||
if (evt.eventType === "left" && onSeekLeft) {
|
if (evt.eventType === "left" && onSeekLeft) {
|
||||||
onSeekLeft();
|
onSeekLeft();
|
||||||
@@ -102,13 +124,8 @@ export function useRemoteControl({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show controls on any D-pad press, or reset timeout if already showing
|
// Reset the timeout on any D-pad navigation when controls are showing
|
||||||
if (!showControls) {
|
onInteraction?.();
|
||||||
toggleControls();
|
|
||||||
} else {
|
|
||||||
// Reset the timeout on any D-pad navigation when controls are showing
|
|
||||||
onInteraction?.();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user