diff --git a/frigate/util/builtin.py b/frigate/util/builtin.py index aa009aa04..556b91ede 100644 --- a/frigate/util/builtin.py +++ b/frigate/util/builtin.py @@ -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): diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx index d0f84899b..ca7745849 100644 --- a/web/src/components/player/PreviewThumbnailPlayer.tsx +++ b/web/src/components/player/PreviewThumbnailPlayer.tsx @@ -47,14 +47,11 @@ export default function PreviewThumbnailPlayer({ }: PreviewPlayerProps) { const apiHost = useApiHost(); const { data: config } = useSWR("config"); - - const [hoverTimeout, setHoverTimeout] = useState(); - 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) => { if (!ignoreClick) { @@ -120,38 +117,39 @@ export default function PreviewThumbnailPlayer({ } }, [allPreviews, review]); + // Hover Playback + + const [hoverTimeout, setHoverTimeout] = useState(); + const [playback, setPlayback] = useState(false); const playingBack = useMemo(() => playback, [playback]); + const [isHovered, setIsHovered] = useState(false); - const onPlayback = useCallback( - (isHovered: boolean) => { - if (isHovered && scrollLock) { - return; + useEffect(() => { + if (isHovered && scrollLock) { + return; + } + + if (isHovered) { + setHoverTimeout( + setTimeout(() => { + setPlayback(true); + setHoverTimeout(null); + }, 500), + ); + } else { + if (hoverTimeout) { + clearTimeout(hoverTimeout); } - if (isHovered) { - setHoverTimeout( - setTimeout(() => { - setPlayback(true); - setHoverTimeout(null); - }, 500), - ); - } else { - if (hoverTimeout) { - clearTimeout(hoverTimeout); - } + setPlayback(false); - setPlayback(false); - - if (onTimeUpdate) { - onTimeUpdate(undefined); - } + if (onTimeUpdate) { + 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 (
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(null); + const sliderRef = useRef(null); // keep track of playback state const [progress, setProgress] = useState(0); + const [hoverTimeout, setHoverTimeout] = useState(); const playerStartTime = useMemo(() => { if (!relevantPreview) { return 0; @@ -458,6 +458,26 @@ function VideoPreview({ }, 500); }, [playerRef, setIgnoreClick]); + const onProgressHover = useCallback( + (event: React.MouseEvent) => { + 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 (
); @@ -500,12 +522,14 @@ function InProgressPreview({ onTimeUpdate, }: InProgressPreviewProps) { const apiHost = useApiHost(); + const sliderRef = useRef(null); const { data: previewFrames } = useSWR( `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(); const [key, setKey] = useState(0); const handleLoad = useCallback(() => { @@ -577,6 +601,34 @@ function InProgressPreview({ [setManualFrame, setIgnoreClick], ); + const onProgressHover = useCallback( + (event: React.MouseEvent) => { + 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 (
); diff --git a/web/src/components/ui/slider-no-thumb.tsx b/web/src/components/ui/slider-no-thumb.tsx index 021e6b806..97f8eba38 100644 --- a/web/src/components/ui/slider-no-thumb.tsx +++ b/web/src/components/ui/slider-no-thumb.tsx @@ -15,10 +15,10 @@ const Slider = React.forwardRef< )} {...props} > - + - + )); Slider.displayName = SliderPrimitive.Root.displayName; diff --git a/web/src/pages/SubmitPlus.tsx b/web/src/pages/SubmitPlus.tsx index 67dfd5230..7581f8cb3 100644 --- a/web/src/pages/SubmitPlus.tsx +++ b/web/src/pages/SubmitPlus.tsx @@ -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 (
- (!open ? setUpload(undefined) : null)} > - - - Submit To Frigate+ - + + + Submit To Frigate+ + Objects in locations you want to avoid are not false positives. Submitting them as false positives will confuse the model. - - + + {`${upload?.label}`} - - Cancel - + + + + + + {events?.map((event) => { return (
setUpload(event)} >
diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index 880ec1c1d..1dd68420f 100644 --- a/web/src/views/live/LiveDashboardView.tsx +++ b/web/src/views/live/LiveDashboardView.tsx @@ -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]);