diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx index a3d936e31..c75e018de 100644 --- a/web/src/components/player/PreviewThumbnailPlayer.tsx +++ b/web/src/components/player/PreviewThumbnailPlayer.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useApiHost } from "@/api"; import { isCurrentHour } from "@/utils/dateUtil"; import { ReviewSegment } from "@/types/review"; -import { Slider } from "../ui/slider"; +import { Slider } from "../ui/slider-no-thumb"; import { getIconForLabel, getIconForSubLabel } from "@/utils/iconUtil"; import TimeAgo from "../dynamic/TimeAgo"; import useSWR from "swr"; @@ -50,16 +50,16 @@ export default function PreviewThumbnailPlayer({ const [hoverTimeout, setHoverTimeout] = useState(); const [playback, setPlayback] = useState(false); - const [progress, setProgress] = useState(0); + const [ignoreClick, setIgnoreClick] = useState(false); const [imgRef, imgLoaded, onImgLoad] = useImageLoaded(); // interaction const handleOnClick = useCallback(() => { - if (onClick) { + if (onClick && !ignoreClick) { onClick(review.id); } - }, [review, onClick]); + }, [ignoreClick, review, onClick]); const swipeHandlers = useSwipeable({ onSwipedLeft: () => setPlayback(false), @@ -92,7 +92,6 @@ export default function PreviewThumbnailPlayer({ } setPlayback(false); - setProgress(0); } }, @@ -123,8 +122,8 @@ export default function PreviewThumbnailPlayer({ )} @@ -174,15 +173,6 @@ export default function PreviewThumbnailPlayer({ )} - {playingBack && ( - - )} {!playingBack && imgLoaded && review.has_been_reviewed && (
)} @@ -193,23 +183,58 @@ export default function PreviewThumbnailPlayer({ ); } -const PREVIEW_PADDING = 16; type PreviewContentProps = { review: ReviewSegment; relevantPreview: Preview | undefined; - setProgress?: (progress: number) => void; setReviewed?: () => void; + setIgnoreClick: (ignore: boolean) => void; }; function PreviewContent({ review, relevantPreview, - setProgress, setReviewed, + setIgnoreClick, }: PreviewContentProps) { + // preview + + if (relevantPreview) { + return ( + + ); + } else if (isCurrentHour(review.start_time)) { + return ( + + ); + } +} + +const PREVIEW_PADDING = 16; +type VideoPreviewProps = { + review: ReviewSegment; + relevantPreview: Preview; + setReviewed?: () => void; + setIgnoreClick: (ignore: boolean) => void; +}; +function VideoPreview({ + review, + relevantPreview, + setReviewed, + setIgnoreClick, +}: VideoPreviewProps) { const playerRef = useRef(null); // keep track of playback state + const [progress, setProgress] = useState(0); const playerStartTime = useMemo(() => { if (!relevantPreview) { return 0; @@ -224,6 +249,12 @@ function PreviewContent({ // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const playerDuration = useMemo( + () => review.end_time - review.start_time + PREVIEW_PADDING, + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); const [lastPercent, setLastPercent] = useState(0.0); // initialize player correctly @@ -256,8 +287,6 @@ function PreviewContent({ (playerRef.current?.currentTime || 0) - playerStartTime; // end with a bit of padding - const playerDuration = - review.end_time - review.start_time + PREVIEW_PADDING; const playerPercent = (playerProgress / playerDuration) * 100; if ( @@ -306,10 +335,53 @@ function PreviewContent({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [manualPlayback, playerRef]); - // preview + // user interaction - if (relevantPreview) { - return ( + const onManualSeek = useCallback( + (values: number[]) => { + const value = values[0]; + + if (!playerRef.current) { + return; + } + + if (manualPlayback) { + setManualPlayback(false); + setIgnoreClick(true); + } + + if (playerRef.current.paused == false) { + playerRef.current.pause(); + setIgnoreClick(true); + } + + setProgress(value); + playerRef.current.currentTime = + playerStartTime + (value / 100.0) * playerDuration; + }, + [ + manualPlayback, + playerDuration, + playerRef, + playerStartTime, + setIgnoreClick, + ], + ); + + const onStopManualSeek = useCallback(() => { + setTimeout(() => { + setIgnoreClick(false); + + if (isSafari || (isFirefox && isMobile)) { + setManualPlayback(true); + } else { + playerRef.current?.play(); + } + }, 500); + }, [playerRef, setIgnoreClick]); + + return ( +
- ); - } else if (isCurrentHour(review.start_time)) { - return ( - - ); - } +
+ ); } const MIN_LOAD_TIMEOUT_MS = 200; type InProgressPreviewProps = { review: ReviewSegment; - setProgress?: (progress: number) => void; setReviewed?: (reviewId: string) => void; + setIgnoreClick: (ignore: boolean) => void; }; function InProgressPreview({ review, - setProgress, setReviewed, + setIgnoreClick, }: InProgressPreviewProps) { const apiHost = useApiHost(); const { data: previewFrames } = useSWR( @@ -350,6 +423,7 @@ function InProgressPreview({ Math.ceil(review.end_time) + 4 }/frames`, ); + const [manualFrame, setManualFrame] = useState(false); const [key, setKey] = useState(0); const handleLoad = useCallback(() => { @@ -357,19 +431,15 @@ function InProgressPreview({ return; } - if (key == previewFrames.length - 1) { - if (setProgress) { - setProgress(100); - } + if (manualFrame) { + return; + } + if (key == previewFrames.length - 1) { return; } setTimeout(() => { - if (setProgress) { - setProgress((key / (previewFrames.length - 1)) * 100); - } - if (setReviewed && key == Math.floor(previewFrames.length / 2)) { setReviewed(review.id); } @@ -379,7 +449,35 @@ function InProgressPreview({ // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps - }, [key, previewFrames]); + }, [key, manualFrame, previewFrames]); + + // user interaction + + const onManualSeek = useCallback( + (values: number[]) => { + const value = values[0]; + + if (!manualFrame) { + setManualFrame(true); + setIgnoreClick(true); + } + + setKey(value); + }, + [manualFrame, setIgnoreClick, setManualFrame, setKey], + ); + + const onStopManualSeek = useCallback( + (values: number[]) => { + const value = values[0]; + setTimeout(() => { + setIgnoreClick(false); + setManualFrame(false); + setKey(value - 1); + }, 500); + }, + [setManualFrame, setIgnoreClick], + ); if (!previewFrames || previewFrames.length == 0) { return ( @@ -391,12 +489,21 @@ function InProgressPreview({ } return ( -
+
+
); } diff --git a/web/src/components/ui/slider-no-thumb.tsx b/web/src/components/ui/slider-no-thumb.tsx new file mode 100644 index 000000000..23f4d4ef1 --- /dev/null +++ b/web/src/components/ui/slider-no-thumb.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; +import * as SliderPrimitive from "@radix-ui/react-slider"; + +import { cn } from "@/lib/utils"; + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/web/src/components/ui/slider.tsx b/web/src/components/ui/slider.tsx index 2ed9fa769..e161daec0 100644 --- a/web/src/components/ui/slider.tsx +++ b/web/src/components/ui/slider.tsx @@ -1,7 +1,7 @@ -import * as React from "react"; -import * as SliderPrimitive from "@radix-ui/react-slider"; +import * as React from "react" +import * as SliderPrimitive from "@radix-ui/react-slider" -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils" const Slider = React.forwardRef< React.ElementRef, @@ -15,11 +15,12 @@ const Slider = React.forwardRef< )} {...props} > - - + + + -)); -Slider.displayName = SliderPrimitive.Root.displayName; +)) +Slider.displayName = SliderPrimitive.Root.displayName -export { Slider }; +export { Slider }