feat: enhance MobileVideoPlayer with improved control responsiveness and progress seeking

refactor: clean up SourceBadges component by removing unused elements
refactor: update useDoubleTap hook to support continuous skipping in skip mode
This commit is contained in:
kuekhaoyang
2025-11-18 12:52:58 +08:00
parent a7480fb47a
commit 5df494e8c8
3 changed files with 117 additions and 33 deletions
+86 -22
View File
@@ -77,38 +77,51 @@ export function MobileVideoPlayer({
videoRef.current.currentTime = targetTime;
setCurrentTime(targetTime);
// Hide indicator after 800ms
// Hide indicator after 1500ms of inactivity
skipTimeoutRef.current = setTimeout(() => {
setShowSkipIndicator(false);
setSkipAmount(0);
setSkipSide(null);
}, 800);
}, 1500);
};
// Double tap handler
const { handleTap } = useDoubleTap({
onDoubleTapLeft: () => skipVideo(10, 'left'),
onDoubleTapRight: () => skipVideo(10, 'right'),
onSkipContinueLeft: () => skipVideo(10, 'left'),
onSkipContinueRight: () => skipVideo(10, 'right'),
isSkipModeActive: showSkipIndicator,
onSingleTap: () => {
// Single tap in center: toggle play/pause
// Single tap toggles play/pause immediately
togglePlay();
// Also show controls briefly
// Show controls
setShowControls(true);
// If playing, set timeout to hide controls
// If paused, keep controls visible
if (controlsTimeoutRef.current) {
clearTimeout(controlsTimeoutRef.current);
}
if (isPlaying) {
controlsTimeoutRef.current = setTimeout(() => {
setShowControls(false);
}, 3000);
}
},
});
// Auto-hide controls only when playing
useEffect(() => {
// If paused, always show controls
if (!isPlaying) {
setShowControls(true);
if (controlsTimeoutRef.current) {
clearTimeout(controlsTimeoutRef.current);
}
controlsTimeoutRef.current = setTimeout(() => {
if (isPlaying) {
setShowControls(false);
}
}, 3000);
},
});
// Auto-hide controls
useEffect(() => {
if (!isPlaying) return;
return;
}
// If playing, auto-hide after 3 seconds
const hideControls = () => {
if (controlsTimeoutRef.current) {
clearTimeout(controlsTimeoutRef.current);
@@ -171,16 +184,65 @@ export function MobileVideoPlayer({
}
};
// Progress bar seeking
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>) => {
// Progress bar seeking with real-time drag
const updateProgressFromEvent = (e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement> | TouchEvent | MouseEvent) => {
if (!videoRef.current || !progressBarRef.current) return;
const rect = progressBarRef.current.getBoundingClientRect();
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
const pos = (clientX - rect.left) / rect.width;
let clientX: number;
// Handle both mouse and touch events
if ('touches' in e) {
const touch = e.touches[0] || (e as React.TouchEvent<HTMLDivElement>).changedTouches?.[0];
if (!touch) return;
clientX = touch.clientX;
} else {
clientX = (e as MouseEvent | React.MouseEvent<HTMLDivElement>).clientX;
}
const pos = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
const newTime = pos * duration;
videoRef.current.currentTime = newTime;
setCurrentTime(newTime);
return newTime;
};
const handleProgressTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
isDraggingProgressRef.current = true;
const newTime = updateProgressFromEvent(e);
if (newTime !== undefined) {
setCurrentTime(newTime);
}
};
const handleProgressTouchMove = (e: React.TouchEvent<HTMLDivElement>) => {
if (!isDraggingProgressRef.current) return;
e.preventDefault(); // Prevent scrolling while dragging
const newTime = updateProgressFromEvent(e);
if (newTime !== undefined) {
setCurrentTime(newTime);
// Update video in real-time
if (videoRef.current) {
videoRef.current.currentTime = newTime;
}
}
};
const handleProgressTouchEnd = (e: React.TouchEvent<HTMLDivElement>) => {
if (!isDraggingProgressRef.current) return;
isDraggingProgressRef.current = false;
const newTime = updateProgressFromEvent(e);
if (newTime !== undefined && videoRef.current) {
videoRef.current.currentTime = newTime;
setCurrentTime(newTime);
}
};
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
const newTime = updateProgressFromEvent(e);
if (newTime !== undefined && videoRef.current) {
videoRef.current.currentTime = newTime;
setCurrentTime(newTime);
}
};
const toggleMute = () => {
@@ -300,7 +362,9 @@ export function MobileVideoPlayer({
ref={progressBarRef}
className="h-1 bg-white/30 rounded-full cursor-pointer"
onClick={handleProgressClick}
onTouchEnd={handleProgressClick}
onTouchStart={handleProgressTouchStart}
onTouchMove={handleProgressTouchMove}
onTouchEnd={handleProgressTouchEnd}
>
<div
className="h-full bg-[var(--accent-color)] rounded-full relative"
-4
View File
@@ -23,10 +23,6 @@ export function SourceBadges({ sources, className = '' }: SourceBadgesProps) {
return (
<Card hover={false} className={`p-4 ${className}`}>
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-semibold text-[var(--text-color)] flex items-center gap-2">
<Icons.Check size={16} className="text-[var(--accent-color)]" />
({sources.length}):
</span>
{sources.map((source) => (
<Badge
key={source.id}
+31 -7
View File
@@ -6,6 +6,9 @@ interface DoubleTapHandler {
onDoubleTapLeft: () => void;
onDoubleTapRight: () => void;
onSingleTap: () => void;
onSkipContinueLeft: () => void;
onSkipContinueRight: () => void;
isSkipModeActive: boolean;
}
/**
@@ -16,11 +19,15 @@ export function useDoubleTap({
onDoubleTapLeft,
onDoubleTapRight,
onSingleTap,
onSkipContinueLeft,
onSkipContinueRight,
isSkipModeActive,
}: DoubleTapHandler) {
const lastTapRef = useRef<{ time: number; side: 'left' | 'right' | null }>({
time: 0,
side: null,
});
const singleTapTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const handleTap = (e: React.TouchEvent<HTMLVideoElement>) => {
const currentTime = Date.now();
@@ -38,6 +45,23 @@ export function useDoubleTap({
const timeDiff = currentTime - lastTapRef.current.time;
const sameSide = lastTapRef.current.side === side;
// Clear any pending single tap
if (singleTapTimeoutRef.current) {
clearTimeout(singleTapTimeoutRef.current);
singleTapTimeoutRef.current = null;
}
// If skip mode is active, single tap continues skipping
if (isSkipModeActive) {
if (side === 'left') {
onSkipContinueLeft();
} else {
onSkipContinueRight();
}
lastTapRef.current = { time: currentTime, side };
return;
}
// Double tap detected (within 300ms on the same side)
if (timeDiff < 300 && sameSide) {
e.preventDefault();
@@ -51,14 +75,14 @@ export function useDoubleTap({
// Reset to prevent triple-tap
lastTapRef.current = { time: 0, side: null };
} else {
// Single tap - delay to check for double tap
setTimeout(() => {
if (Date.now() - currentTime >= 300) {
onSingleTap();
}
}, 300);
// Possible single tap - wait to see if there's a double tap
lastTapRef.current = { time: currentTime, side };
singleTapTimeoutRef.current = setTimeout(() => {
// After 300ms, no double tap detected, execute single tap action
onSingleTap();
singleTapTimeoutRef.current = null;
}, 300);
}
};