mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Tweaks fixes (#10311)
* Save numbers as int instead of string * Fix hover logic * Fix delay for new alerts * Fixup dialog and marking item as uploaded * Make preview progress larger and easier to grab * Allow hovering to control preview on desktop
This commit is contained in:
		
							parent
							
								
									b2931bcaa9
								
							
						
					
					
						commit
						8776cdfd5b
					
				@ -208,7 +208,12 @@ def update_yaml_from_url(file_path, url):
 | 
			
		||||
        if len(new_value_list) > 1:
 | 
			
		||||
            update_yaml_file(file_path, key_path, new_value_list)
 | 
			
		||||
        else:
 | 
			
		||||
            update_yaml_file(file_path, key_path, new_value_list[0])
 | 
			
		||||
            value = str(new_value_list[0])
 | 
			
		||||
 | 
			
		||||
            if value.isnumeric():
 | 
			
		||||
                value = int(value)
 | 
			
		||||
 | 
			
		||||
            update_yaml_file(file_path, key_path, value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_yaml_file(file_path, key_path, new_value):
 | 
			
		||||
 | 
			
		||||
@ -47,14 +47,11 @@ export default function PreviewThumbnailPlayer({
 | 
			
		||||
}: PreviewPlayerProps) {
 | 
			
		||||
  const apiHost = useApiHost();
 | 
			
		||||
  const { data: config } = useSWR<FrigateConfig>("config");
 | 
			
		||||
 | 
			
		||||
  const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout | null>();
 | 
			
		||||
  const [playback, setPlayback] = useState(false);
 | 
			
		||||
  const [ignoreClick, setIgnoreClick] = useState(false);
 | 
			
		||||
  const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
 | 
			
		||||
 | 
			
		||||
  // interaction
 | 
			
		||||
 | 
			
		||||
  const [ignoreClick, setIgnoreClick] = useState(false);
 | 
			
		||||
  const handleOnClick = useCallback(
 | 
			
		||||
    (e: React.MouseEvent<HTMLDivElement>) => {
 | 
			
		||||
      if (!ignoreClick) {
 | 
			
		||||
@ -120,10 +117,14 @@ export default function PreviewThumbnailPlayer({
 | 
			
		||||
    }
 | 
			
		||||
  }, [allPreviews, review]);
 | 
			
		||||
 | 
			
		||||
  const playingBack = useMemo(() => playback, [playback]);
 | 
			
		||||
  // Hover Playback
 | 
			
		||||
 | 
			
		||||
  const onPlayback = useCallback(
 | 
			
		||||
    (isHovered: boolean) => {
 | 
			
		||||
  const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout | null>();
 | 
			
		||||
  const [playback, setPlayback] = useState(false);
 | 
			
		||||
  const playingBack = useMemo(() => playback, [playback]);
 | 
			
		||||
  const [isHovered, setIsHovered] = useState(false);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isHovered && scrollLock) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@ -146,12 +147,9 @@ export default function PreviewThumbnailPlayer({
 | 
			
		||||
        onTimeUpdate(undefined);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // we know that these deps are correct
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
    [hoverTimeout, scrollLock, review],
 | 
			
		||||
  );
 | 
			
		||||
  }, [isHovered, scrollLock]);
 | 
			
		||||
 | 
			
		||||
  // date
 | 
			
		||||
 | 
			
		||||
@ -163,8 +161,8 @@ export default function PreviewThumbnailPlayer({
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className="relative size-full cursor-pointer"
 | 
			
		||||
      onMouseEnter={isMobile ? undefined : () => onPlayback(true)}
 | 
			
		||||
      onMouseLeave={isMobile ? undefined : () => onPlayback(false)}
 | 
			
		||||
      onMouseEnter={isMobile ? undefined : () => setIsHovered(true)}
 | 
			
		||||
      onMouseLeave={isMobile ? undefined : () => setIsHovered(false)}
 | 
			
		||||
      onContextMenu={(e) => {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        onClick(review.id, true);
 | 
			
		||||
@ -294,10 +292,12 @@ function VideoPreview({
 | 
			
		||||
  onTimeUpdate,
 | 
			
		||||
}: VideoPreviewProps) {
 | 
			
		||||
  const playerRef = useRef<HTMLVideoElement | null>(null);
 | 
			
		||||
  const sliderRef = useRef<HTMLDivElement | null>(null);
 | 
			
		||||
 | 
			
		||||
  // keep track of playback state
 | 
			
		||||
 | 
			
		||||
  const [progress, setProgress] = useState(0);
 | 
			
		||||
  const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout>();
 | 
			
		||||
  const playerStartTime = useMemo(() => {
 | 
			
		||||
    if (!relevantPreview) {
 | 
			
		||||
      return 0;
 | 
			
		||||
@ -458,6 +458,26 @@ function VideoPreview({
 | 
			
		||||
    }, 500);
 | 
			
		||||
  }, [playerRef, setIgnoreClick]);
 | 
			
		||||
 | 
			
		||||
  const onProgressHover = useCallback(
 | 
			
		||||
    (event: React.MouseEvent<HTMLDivElement>) => {
 | 
			
		||||
      if (!sliderRef.current) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const rect = sliderRef.current.getBoundingClientRect();
 | 
			
		||||
      const positionX = event.clientX - rect.left;
 | 
			
		||||
      const width = sliderRef.current.clientWidth;
 | 
			
		||||
      onManualSeek([Math.round((positionX / width) * 100)]);
 | 
			
		||||
 | 
			
		||||
      if (hoverTimeout) {
 | 
			
		||||
        clearTimeout(hoverTimeout);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      setHoverTimeout(setTimeout(() => onStopManualSeek(), 500));
 | 
			
		||||
    },
 | 
			
		||||
    [sliderRef, hoverTimeout, onManualSeek, onStopManualSeek, setHoverTimeout],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="relative size-full aspect-video bg-black">
 | 
			
		||||
      <video
 | 
			
		||||
@ -472,6 +492,7 @@ function VideoPreview({
 | 
			
		||||
        <source src={relevantPreview.src} type={relevantPreview.type} />
 | 
			
		||||
      </video>
 | 
			
		||||
      <Slider
 | 
			
		||||
        ref={sliderRef}
 | 
			
		||||
        className="absolute inset-x-0 bottom-0 z-30"
 | 
			
		||||
        value={[progress]}
 | 
			
		||||
        onValueChange={onManualSeek}
 | 
			
		||||
@ -479,6 +500,7 @@ function VideoPreview({
 | 
			
		||||
        min={0}
 | 
			
		||||
        step={1}
 | 
			
		||||
        max={100}
 | 
			
		||||
        onMouseMove={isMobile ? undefined : onProgressHover}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
@ -500,12 +522,14 @@ function InProgressPreview({
 | 
			
		||||
  onTimeUpdate,
 | 
			
		||||
}: InProgressPreviewProps) {
 | 
			
		||||
  const apiHost = useApiHost();
 | 
			
		||||
  const sliderRef = useRef<HTMLDivElement | null>(null);
 | 
			
		||||
  const { data: previewFrames } = useSWR<string[]>(
 | 
			
		||||
    `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 [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout>();
 | 
			
		||||
  const [key, setKey] = useState(0);
 | 
			
		||||
 | 
			
		||||
  const handleLoad = useCallback(() => {
 | 
			
		||||
@ -577,6 +601,34 @@ function InProgressPreview({
 | 
			
		||||
    [setManualFrame, setIgnoreClick],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const onProgressHover = useCallback(
 | 
			
		||||
    (event: React.MouseEvent<HTMLDivElement>) => {
 | 
			
		||||
      if (!sliderRef.current || !previewFrames) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const rect = sliderRef.current.getBoundingClientRect();
 | 
			
		||||
      const positionX = event.clientX - rect.left;
 | 
			
		||||
      const width = sliderRef.current.clientWidth;
 | 
			
		||||
      const progress = [Math.round((positionX / width) * previewFrames.length)];
 | 
			
		||||
      onManualSeek(progress);
 | 
			
		||||
 | 
			
		||||
      if (hoverTimeout) {
 | 
			
		||||
        clearTimeout(hoverTimeout);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      setHoverTimeout(setTimeout(() => onStopManualSeek(progress), 500));
 | 
			
		||||
    },
 | 
			
		||||
    [
 | 
			
		||||
      sliderRef,
 | 
			
		||||
      hoverTimeout,
 | 
			
		||||
      previewFrames,
 | 
			
		||||
      onManualSeek,
 | 
			
		||||
      onStopManualSeek,
 | 
			
		||||
      setHoverTimeout,
 | 
			
		||||
    ],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (!previewFrames || previewFrames.length == 0) {
 | 
			
		||||
    return (
 | 
			
		||||
      <img
 | 
			
		||||
@ -594,6 +646,7 @@ function InProgressPreview({
 | 
			
		||||
        onLoad={handleLoad}
 | 
			
		||||
      />
 | 
			
		||||
      <Slider
 | 
			
		||||
        ref={sliderRef}
 | 
			
		||||
        className="absolute inset-x-0 bottom-0 z-30"
 | 
			
		||||
        value={[key]}
 | 
			
		||||
        onValueChange={onManualSeek}
 | 
			
		||||
@ -601,6 +654,7 @@ function InProgressPreview({
 | 
			
		||||
        min={0}
 | 
			
		||||
        step={1}
 | 
			
		||||
        max={previewFrames.length - 1}
 | 
			
		||||
        onMouseMove={isMobile ? undefined : onProgressHover}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
@ -15,10 +15,10 @@ const Slider = React.forwardRef<
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full">
 | 
			
		||||
    <SliderPrimitive.Track className="relative h-2 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 cursor-col-resize" />
 | 
			
		||||
    <SliderPrimitive.Thumb className="block h-4 w-16 rounded-full bg-transparent -translate-y-[50%] ring-offset-transparent focus-visible:outline-none focus-visible:ring-transparent disabled:pointer-events-none disabled:opacity-50 cursor-col-resize" />
 | 
			
		||||
  </SliderPrimitive.Root>
 | 
			
		||||
));
 | 
			
		||||
Slider.displayName = SliderPrimitive.Root.displayName;
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,13 @@
 | 
			
		||||
import { baseUrl } from "@/api/baseUrl";
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import {
 | 
			
		||||
  AlertDialog,
 | 
			
		||||
  AlertDialogAction,
 | 
			
		||||
  AlertDialogCancel,
 | 
			
		||||
  AlertDialogContent,
 | 
			
		||||
  AlertDialogDescription,
 | 
			
		||||
  AlertDialogFooter,
 | 
			
		||||
  AlertDialogHeader,
 | 
			
		||||
  AlertDialogTitle,
 | 
			
		||||
} from "@/components/ui/alert-dialog";
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogDescription,
 | 
			
		||||
  DialogFooter,
 | 
			
		||||
  DialogHeader,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
} from "@/components/ui/dialog";
 | 
			
		||||
import { Event } from "@/types/event";
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
import { useCallback, useState } from "react";
 | 
			
		||||
@ -27,64 +26,75 @@ export default function SubmitPlus() {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const resp = (await falsePositive)
 | 
			
		||||
        ? await axios.put(`events/${upload.id}/false_positive`)
 | 
			
		||||
        : await axios.post(`events/${upload.id}/plus`, {
 | 
			
		||||
      falsePositive
 | 
			
		||||
        ? axios.put(`events/${upload.id}/false_positive`)
 | 
			
		||||
        : axios.post(`events/${upload.id}/plus`, {
 | 
			
		||||
            include_annotation: 1,
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
      if (resp.status == 200) {
 | 
			
		||||
        refresh();
 | 
			
		||||
      refresh(
 | 
			
		||||
        (data: Event[] | undefined) => {
 | 
			
		||||
          if (!data) {
 | 
			
		||||
            return data;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const index = data.findIndex((e) => e.id == upload.id);
 | 
			
		||||
 | 
			
		||||
          if (index == -1) {
 | 
			
		||||
            return data;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return [...data.slice(0, index), ...data.slice(index + 1)];
 | 
			
		||||
        },
 | 
			
		||||
        { revalidate: false, populateCache: true },
 | 
			
		||||
      );
 | 
			
		||||
      setUpload(undefined);
 | 
			
		||||
    },
 | 
			
		||||
    [refresh, upload],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="size-full p-2 grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2 overflow-auto">
 | 
			
		||||
      <AlertDialog
 | 
			
		||||
      <Dialog
 | 
			
		||||
        open={upload != undefined}
 | 
			
		||||
        onOpenChange={(open) => (!open ? setUpload(undefined) : null)}
 | 
			
		||||
      >
 | 
			
		||||
        <AlertDialogContent className="md:max-w-4xl">
 | 
			
		||||
          <AlertDialogHeader>
 | 
			
		||||
            <AlertDialogTitle>Submit To Frigate+</AlertDialogTitle>
 | 
			
		||||
            <AlertDialogDescription>
 | 
			
		||||
        <DialogContent className="md:max-w-4xl">
 | 
			
		||||
          <DialogHeader>
 | 
			
		||||
            <DialogTitle>Submit To Frigate+</DialogTitle>
 | 
			
		||||
            <DialogDescription>
 | 
			
		||||
              Objects in locations you want to avoid are not false positives.
 | 
			
		||||
              Submitting them as false positives will confuse the model.
 | 
			
		||||
            </AlertDialogDescription>
 | 
			
		||||
          </AlertDialogHeader>
 | 
			
		||||
            </DialogDescription>
 | 
			
		||||
          </DialogHeader>
 | 
			
		||||
          <img
 | 
			
		||||
            className="flex-grow-0"
 | 
			
		||||
            src={`${baseUrl}api/events/${upload?.id}/snapshot.jpg`}
 | 
			
		||||
            alt={`${upload?.label}`}
 | 
			
		||||
          />
 | 
			
		||||
          <AlertDialogFooter>
 | 
			
		||||
            <AlertDialogCancel>Cancel</AlertDialogCancel>
 | 
			
		||||
            <AlertDialogAction
 | 
			
		||||
          <DialogFooter>
 | 
			
		||||
            <Button>Cancel</Button>
 | 
			
		||||
            <Button
 | 
			
		||||
              className="bg-success"
 | 
			
		||||
              onClick={() => onSubmitToPlus(false)}
 | 
			
		||||
            >
 | 
			
		||||
              This is a {upload?.label}
 | 
			
		||||
            </AlertDialogAction>
 | 
			
		||||
            <AlertDialogAction
 | 
			
		||||
              className="bg-danger"
 | 
			
		||||
              onClick={() => onSubmitToPlus(true)}
 | 
			
		||||
            >
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Button variant="destructive" onClick={() => onSubmitToPlus(true)}>
 | 
			
		||||
              This is not a {upload?.label}
 | 
			
		||||
            </AlertDialogAction>
 | 
			
		||||
          </AlertDialogFooter>
 | 
			
		||||
        </AlertDialogContent>
 | 
			
		||||
      </AlertDialog>
 | 
			
		||||
            </Button>
 | 
			
		||||
          </DialogFooter>
 | 
			
		||||
        </DialogContent>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
 | 
			
		||||
      {events?.map((event) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div
 | 
			
		||||
            className="size-full aspect-video rounded-2xl flex justify-center items-center bg-black cursor-pointer"
 | 
			
		||||
            className="size-full rounded-2xl flex justify-center items-center bg-black cursor-pointer"
 | 
			
		||||
            onClick={() => setUpload(event)}
 | 
			
		||||
          >
 | 
			
		||||
            <img
 | 
			
		||||
              className="h-full object-contain rounded-2xl"
 | 
			
		||||
              className="aspect-video h-full object-contain rounded-2xl"
 | 
			
		||||
              src={`${baseUrl}api/events/${event.id}/snapshot.jpg`}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
@ -43,7 +43,7 @@ export default function LiveDashboardView({
 | 
			
		||||
 | 
			
		||||
    // if event is ended and was saved, update events list
 | 
			
		||||
    if (eventUpdate.type == "end" && eventUpdate.review.severity == "alert") {
 | 
			
		||||
      updateEvents();
 | 
			
		||||
      setTimeout(() => updateEvents(), 1000);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }, [eventUpdate, updateEvents]);
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user