mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Preview player upgrades (#10152)
* Implement manual slider control for previews * Automatically end preview video on mobile * Show current time on timeline * remove z height
This commit is contained in:
		
							parent
							
								
									cb30450060
								
							
						
					
					
						commit
						e7f8bca2c3
					
				@ -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";
 | 
			
		||||
@ -29,6 +29,7 @@ type PreviewPlayerProps = {
 | 
			
		||||
  autoPlayback?: boolean;
 | 
			
		||||
  setReviewed?: (reviewId: string) => void;
 | 
			
		||||
  onClick?: (reviewId: string) => void;
 | 
			
		||||
  onTimeUpdate?: (time: number | undefined) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type Preview = {
 | 
			
		||||
@ -44,25 +45,26 @@ export default function PreviewThumbnailPlayer({
 | 
			
		||||
  relevantPreview,
 | 
			
		||||
  setReviewed,
 | 
			
		||||
  onClick,
 | 
			
		||||
  onTimeUpdate,
 | 
			
		||||
}: PreviewPlayerProps) {
 | 
			
		||||
  const apiHost = useApiHost();
 | 
			
		||||
  const { data: config } = useSWR<FrigateConfig>("config");
 | 
			
		||||
 | 
			
		||||
  const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout | null>();
 | 
			
		||||
  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),
 | 
			
		||||
    onSwipedLeft: () => (setReviewed ? setReviewed(review.id) : null),
 | 
			
		||||
    onSwipedRight: () => setPlayback(true),
 | 
			
		||||
    preventScrollOnSwipe: true,
 | 
			
		||||
  });
 | 
			
		||||
@ -92,7 +94,10 @@ export default function PreviewThumbnailPlayer({
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setPlayback(false);
 | 
			
		||||
        setProgress(0);
 | 
			
		||||
 | 
			
		||||
        if (onTimeUpdate) {
 | 
			
		||||
          onTimeUpdate(undefined);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@ -123,8 +128,10 @@ export default function PreviewThumbnailPlayer({
 | 
			
		||||
              <PreviewContent
 | 
			
		||||
                review={review}
 | 
			
		||||
                relevantPreview={relevantPreview}
 | 
			
		||||
                setProgress={setProgress}
 | 
			
		||||
                setReviewed={handleSetReviewed}
 | 
			
		||||
                setIgnoreClick={setIgnoreClick}
 | 
			
		||||
                isPlayingBack={setPlayback}
 | 
			
		||||
                onTimeUpdate={onTimeUpdate}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
@ -174,15 +181,6 @@ export default function PreviewThumbnailPlayer({
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
          {playingBack && (
 | 
			
		||||
            <Slider
 | 
			
		||||
              className="absolute inset-x-0 bottom-0 z-10"
 | 
			
		||||
              value={[progress]}
 | 
			
		||||
              min={0}
 | 
			
		||||
              step={1}
 | 
			
		||||
              max={100}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
          {!playingBack && imgLoaded && review.has_been_reviewed && (
 | 
			
		||||
            <div className="absolute inset-0 z-10 bg-black bg-opacity-60" />
 | 
			
		||||
          )}
 | 
			
		||||
@ -193,23 +191,70 @@ 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;
 | 
			
		||||
  isPlayingBack: (ended: boolean) => void;
 | 
			
		||||
  onTimeUpdate?: (time: number | undefined) => void;
 | 
			
		||||
};
 | 
			
		||||
function PreviewContent({
 | 
			
		||||
  review,
 | 
			
		||||
  relevantPreview,
 | 
			
		||||
  setProgress,
 | 
			
		||||
  setReviewed,
 | 
			
		||||
  setIgnoreClick,
 | 
			
		||||
  isPlayingBack,
 | 
			
		||||
  onTimeUpdate,
 | 
			
		||||
}: PreviewContentProps) {
 | 
			
		||||
  // preview
 | 
			
		||||
 | 
			
		||||
  if (relevantPreview) {
 | 
			
		||||
    return (
 | 
			
		||||
      <VideoPreview
 | 
			
		||||
        review={review}
 | 
			
		||||
        relevantPreview={relevantPreview}
 | 
			
		||||
        setReviewed={setReviewed}
 | 
			
		||||
        setIgnoreClick={setIgnoreClick}
 | 
			
		||||
        isPlayingBack={isPlayingBack}
 | 
			
		||||
        onTimeUpdate={onTimeUpdate}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  } else if (isCurrentHour(review.start_time)) {
 | 
			
		||||
    return (
 | 
			
		||||
      <InProgressPreview
 | 
			
		||||
        review={review}
 | 
			
		||||
        setReviewed={setReviewed}
 | 
			
		||||
        setIgnoreClick={setIgnoreClick}
 | 
			
		||||
        isPlayingBack={isPlayingBack}
 | 
			
		||||
        onTimeUpdate={onTimeUpdate}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const PREVIEW_PADDING = 16;
 | 
			
		||||
type VideoPreviewProps = {
 | 
			
		||||
  review: ReviewSegment;
 | 
			
		||||
  relevantPreview: Preview;
 | 
			
		||||
  setReviewed?: () => void;
 | 
			
		||||
  setIgnoreClick: (ignore: boolean) => void;
 | 
			
		||||
  isPlayingBack: (ended: boolean) => void;
 | 
			
		||||
  onTimeUpdate?: (time: number | undefined) => void;
 | 
			
		||||
};
 | 
			
		||||
function VideoPreview({
 | 
			
		||||
  review,
 | 
			
		||||
  relevantPreview,
 | 
			
		||||
  setReviewed,
 | 
			
		||||
  setIgnoreClick,
 | 
			
		||||
  isPlayingBack,
 | 
			
		||||
  onTimeUpdate,
 | 
			
		||||
}: VideoPreviewProps) {
 | 
			
		||||
  const playerRef = useRef<HTMLVideoElement | null>(null);
 | 
			
		||||
 | 
			
		||||
  // keep track of playback state
 | 
			
		||||
 | 
			
		||||
  const [progress, setProgress] = useState(0);
 | 
			
		||||
  const playerStartTime = useMemo(() => {
 | 
			
		||||
    if (!relevantPreview) {
 | 
			
		||||
      return 0;
 | 
			
		||||
@ -224,6 +269,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
 | 
			
		||||
@ -248,16 +299,16 @@ function PreviewContent({
 | 
			
		||||
  // time progress update
 | 
			
		||||
 | 
			
		||||
  const onProgress = useCallback(() => {
 | 
			
		||||
    if (!setProgress) {
 | 
			
		||||
      return;
 | 
			
		||||
    if (onTimeUpdate) {
 | 
			
		||||
      onTimeUpdate(
 | 
			
		||||
        relevantPreview.start + (playerRef.current?.currentTime || 0),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const playerProgress =
 | 
			
		||||
      (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 (
 | 
			
		||||
@ -272,7 +323,16 @@ function PreviewContent({
 | 
			
		||||
    setLastPercent(playerPercent);
 | 
			
		||||
 | 
			
		||||
    if (playerPercent > 100) {
 | 
			
		||||
      playerRef.current?.pause();
 | 
			
		||||
      if (isMobile) {
 | 
			
		||||
        isPlayingBack(false);
 | 
			
		||||
 | 
			
		||||
        if (onTimeUpdate) {
 | 
			
		||||
          onTimeUpdate(undefined);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        playerRef.current?.pause();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      setManualPlayback(false);
 | 
			
		||||
      setProgress(100.0);
 | 
			
		||||
    } else {
 | 
			
		||||
@ -306,10 +366,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 (
 | 
			
		||||
    <div className="relative size-full aspect-video bg-black">
 | 
			
		||||
      <video
 | 
			
		||||
        ref={playerRef}
 | 
			
		||||
        className="size-full aspect-video bg-black"
 | 
			
		||||
@ -321,35 +424,41 @@ function PreviewContent({
 | 
			
		||||
      >
 | 
			
		||||
        <source src={relevantPreview.src} type={relevantPreview.type} />
 | 
			
		||||
      </video>
 | 
			
		||||
    );
 | 
			
		||||
  } else if (isCurrentHour(review.start_time)) {
 | 
			
		||||
    return (
 | 
			
		||||
      <InProgressPreview
 | 
			
		||||
        review={review}
 | 
			
		||||
        setProgress={setProgress}
 | 
			
		||||
        setReviewed={setReviewed}
 | 
			
		||||
      <Slider
 | 
			
		||||
        className="absolute inset-x-0 bottom-0"
 | 
			
		||||
        value={[progress]}
 | 
			
		||||
        onValueChange={onManualSeek}
 | 
			
		||||
        onValueCommit={onStopManualSeek}
 | 
			
		||||
        min={0}
 | 
			
		||||
        step={1}
 | 
			
		||||
        max={100}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const MIN_LOAD_TIMEOUT_MS = 200;
 | 
			
		||||
type InProgressPreviewProps = {
 | 
			
		||||
  review: ReviewSegment;
 | 
			
		||||
  setProgress?: (progress: number) => void;
 | 
			
		||||
  setReviewed?: (reviewId: string) => void;
 | 
			
		||||
  setIgnoreClick: (ignore: boolean) => void;
 | 
			
		||||
  isPlayingBack: (ended: boolean) => void;
 | 
			
		||||
  onTimeUpdate?: (time: number | undefined) => void;
 | 
			
		||||
};
 | 
			
		||||
function InProgressPreview({
 | 
			
		||||
  review,
 | 
			
		||||
  setProgress,
 | 
			
		||||
  setReviewed,
 | 
			
		||||
  setIgnoreClick,
 | 
			
		||||
  isPlayingBack,
 | 
			
		||||
  onTimeUpdate,
 | 
			
		||||
}: InProgressPreviewProps) {
 | 
			
		||||
  const apiHost = useApiHost();
 | 
			
		||||
  const { data: previewFrames } = useSWR<string[]>(
 | 
			
		||||
    `preview/${review.camera}/start/${Math.floor(review.start_time) - 4}/end/${
 | 
			
		||||
      Math.ceil(review.end_time) + 4
 | 
			
		||||
    `preview/${review.camera}/start/${Math.floor(review.start_time) - PREVIEW_PADDING}/end/${
 | 
			
		||||
      Math.ceil(review.end_time) + PREVIEW_PADDING
 | 
			
		||||
    }/frames`,
 | 
			
		||||
  );
 | 
			
		||||
  const [manualFrame, setManualFrame] = useState(false);
 | 
			
		||||
  const [key, setKey] = useState(0);
 | 
			
		||||
 | 
			
		||||
  const handleLoad = useCallback(() => {
 | 
			
		||||
@ -357,19 +466,27 @@ function InProgressPreview({
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (onTimeUpdate) {
 | 
			
		||||
      onTimeUpdate(review.start_time - PREVIEW_PADDING + key);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (manualFrame) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (key == previewFrames.length - 1) {
 | 
			
		||||
      if (setProgress) {
 | 
			
		||||
        setProgress(100);
 | 
			
		||||
      if (isMobile) {
 | 
			
		||||
        isPlayingBack(false);
 | 
			
		||||
 | 
			
		||||
        if (onTimeUpdate) {
 | 
			
		||||
          onTimeUpdate(undefined);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      if (setProgress) {
 | 
			
		||||
        setProgress((key / (previewFrames.length - 1)) * 100);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (setReviewed && key == Math.floor(previewFrames.length / 2)) {
 | 
			
		||||
        setReviewed(review.id);
 | 
			
		||||
      }
 | 
			
		||||
@ -379,7 +496,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 +536,21 @@ function InProgressPreview({
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="size-full flex items-center bg-black">
 | 
			
		||||
    <div className="relative size-full flex items-center bg-black">
 | 
			
		||||
      <img
 | 
			
		||||
        className="size-full object-contain"
 | 
			
		||||
        src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.jpg`}
 | 
			
		||||
        onLoad={handleLoad}
 | 
			
		||||
      />
 | 
			
		||||
      <Slider
 | 
			
		||||
        className="absolute inset-x-0 bottom-0"
 | 
			
		||||
        value={[key]}
 | 
			
		||||
        onValueChange={onManualSeek}
 | 
			
		||||
        onValueCommit={onStopManualSeek}
 | 
			
		||||
        min={0}
 | 
			
		||||
        step={1}
 | 
			
		||||
        max={previewFrames.length - 1}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										26
									
								
								web/src/components/ui/slider-no-thumb.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								web/src/components/ui/slider-no-thumb.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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<typeof SliderPrimitive.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <SliderPrimitive.Root
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "relative flex w-full touch-none select-none items-center",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full">
 | 
			
		||||
      <SliderPrimitive.Range className="absolute h-full bg-blue-500" />
 | 
			
		||||
    </SliderPrimitive.Track>
 | 
			
		||||
    <SliderPrimitive.Thumb className="block h-2 w-12 rounded-full border-2 border-transparent bg-transparent ring-offset-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
 | 
			
		||||
  </SliderPrimitive.Root>
 | 
			
		||||
));
 | 
			
		||||
Slider.displayName = SliderPrimitive.Root.displayName;
 | 
			
		||||
 | 
			
		||||
export { Slider };
 | 
			
		||||
@ -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<typeof SliderPrimitive.Root>,
 | 
			
		||||
@ -15,11 +15,12 @@ const Slider = React.forwardRef<
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full">
 | 
			
		||||
      <SliderPrimitive.Range className="absolute h-full bg-blue-500" />
 | 
			
		||||
    <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
 | 
			
		||||
      <SliderPrimitive.Range className="absolute h-full bg-primary" />
 | 
			
		||||
    </SliderPrimitive.Track>
 | 
			
		||||
    <SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
 | 
			
		||||
  </SliderPrimitive.Root>
 | 
			
		||||
));
 | 
			
		||||
Slider.displayName = SliderPrimitive.Root.displayName;
 | 
			
		||||
))
 | 
			
		||||
Slider.displayName = SliderPrimitive.Root.displayName
 | 
			
		||||
 | 
			
		||||
export { Slider };
 | 
			
		||||
export { Slider }
 | 
			
		||||
 | 
			
		||||
@ -187,6 +187,10 @@ export default function EventView({
 | 
			
		||||
    return data;
 | 
			
		||||
  }, [minimap]);
 | 
			
		||||
 | 
			
		||||
  // preview playback
 | 
			
		||||
 | 
			
		||||
  const [previewTime, setPreviewTime] = useState<number>();
 | 
			
		||||
 | 
			
		||||
  if (!config) {
 | 
			
		||||
    return <ActivityIndicator />;
 | 
			
		||||
  }
 | 
			
		||||
@ -287,6 +291,7 @@ export default function EventView({
 | 
			
		||||
                        review={value}
 | 
			
		||||
                        relevantPreview={relevantPreview}
 | 
			
		||||
                        setReviewed={markItemAsReviewed}
 | 
			
		||||
                        onTimeUpdate={setPreviewTime}
 | 
			
		||||
                        onClick={onSelectReview}
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
@ -308,6 +313,8 @@ export default function EventView({
 | 
			
		||||
            showMinimap={showMinimap}
 | 
			
		||||
            minimapStartTime={minimapBounds.start}
 | 
			
		||||
            minimapEndTime={minimapBounds.end}
 | 
			
		||||
            showHandlebar={previewTime != undefined}
 | 
			
		||||
            handlebarTime={previewTime}
 | 
			
		||||
            events={reviewItems.all}
 | 
			
		||||
            severityType={severity}
 | 
			
		||||
            contentRef={contentRef}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user