mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +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:
|
if len(new_value_list) > 1:
|
||||||
update_yaml_file(file_path, key_path, new_value_list)
|
update_yaml_file(file_path, key_path, new_value_list)
|
||||||
else:
|
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):
|
def update_yaml_file(file_path, key_path, new_value):
|
||||||
|
@ -47,14 +47,11 @@ export default function PreviewThumbnailPlayer({
|
|||||||
}: PreviewPlayerProps) {
|
}: PreviewPlayerProps) {
|
||||||
const apiHost = useApiHost();
|
const apiHost = useApiHost();
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
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();
|
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
|
||||||
|
|
||||||
// interaction
|
// interaction
|
||||||
|
|
||||||
|
const [ignoreClick, setIgnoreClick] = useState(false);
|
||||||
const handleOnClick = useCallback(
|
const handleOnClick = useCallback(
|
||||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
if (!ignoreClick) {
|
if (!ignoreClick) {
|
||||||
@ -120,38 +117,39 @@ export default function PreviewThumbnailPlayer({
|
|||||||
}
|
}
|
||||||
}, [allPreviews, review]);
|
}, [allPreviews, review]);
|
||||||
|
|
||||||
|
// Hover Playback
|
||||||
|
|
||||||
|
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout | null>();
|
||||||
|
const [playback, setPlayback] = useState(false);
|
||||||
const playingBack = useMemo(() => playback, [playback]);
|
const playingBack = useMemo(() => playback, [playback]);
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
const onPlayback = useCallback(
|
useEffect(() => {
|
||||||
(isHovered: boolean) => {
|
if (isHovered && scrollLock) {
|
||||||
if (isHovered && scrollLock) {
|
return;
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
if (isHovered) {
|
||||||
|
setHoverTimeout(
|
||||||
|
setTimeout(() => {
|
||||||
|
setPlayback(true);
|
||||||
|
setHoverTimeout(null);
|
||||||
|
}, 500),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (hoverTimeout) {
|
||||||
|
clearTimeout(hoverTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isHovered) {
|
setPlayback(false);
|
||||||
setHoverTimeout(
|
|
||||||
setTimeout(() => {
|
|
||||||
setPlayback(true);
|
|
||||||
setHoverTimeout(null);
|
|
||||||
}, 500),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (hoverTimeout) {
|
|
||||||
clearTimeout(hoverTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
setPlayback(false);
|
if (onTimeUpdate) {
|
||||||
|
onTimeUpdate(undefined);
|
||||||
if (onTimeUpdate) {
|
|
||||||
onTimeUpdate(undefined);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
// we know that these deps are correct
|
// we know that these deps are correct
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[hoverTimeout, scrollLock, review],
|
}, [isHovered, scrollLock]);
|
||||||
);
|
|
||||||
|
|
||||||
// date
|
// date
|
||||||
|
|
||||||
@ -163,8 +161,8 @@ export default function PreviewThumbnailPlayer({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative size-full cursor-pointer"
|
className="relative size-full cursor-pointer"
|
||||||
onMouseEnter={isMobile ? undefined : () => onPlayback(true)}
|
onMouseEnter={isMobile ? undefined : () => setIsHovered(true)}
|
||||||
onMouseLeave={isMobile ? undefined : () => onPlayback(false)}
|
onMouseLeave={isMobile ? undefined : () => setIsHovered(false)}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onClick(review.id, true);
|
onClick(review.id, true);
|
||||||
@ -294,10 +292,12 @@ function VideoPreview({
|
|||||||
onTimeUpdate,
|
onTimeUpdate,
|
||||||
}: VideoPreviewProps) {
|
}: VideoPreviewProps) {
|
||||||
const playerRef = useRef<HTMLVideoElement | null>(null);
|
const playerRef = useRef<HTMLVideoElement | null>(null);
|
||||||
|
const sliderRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
// keep track of playback state
|
// keep track of playback state
|
||||||
|
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
|
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout>();
|
||||||
const playerStartTime = useMemo(() => {
|
const playerStartTime = useMemo(() => {
|
||||||
if (!relevantPreview) {
|
if (!relevantPreview) {
|
||||||
return 0;
|
return 0;
|
||||||
@ -458,6 +458,26 @@ function VideoPreview({
|
|||||||
}, 500);
|
}, 500);
|
||||||
}, [playerRef, setIgnoreClick]);
|
}, [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 (
|
return (
|
||||||
<div className="relative size-full aspect-video bg-black">
|
<div className="relative size-full aspect-video bg-black">
|
||||||
<video
|
<video
|
||||||
@ -472,6 +492,7 @@ function VideoPreview({
|
|||||||
<source src={relevantPreview.src} type={relevantPreview.type} />
|
<source src={relevantPreview.src} type={relevantPreview.type} />
|
||||||
</video>
|
</video>
|
||||||
<Slider
|
<Slider
|
||||||
|
ref={sliderRef}
|
||||||
className="absolute inset-x-0 bottom-0 z-30"
|
className="absolute inset-x-0 bottom-0 z-30"
|
||||||
value={[progress]}
|
value={[progress]}
|
||||||
onValueChange={onManualSeek}
|
onValueChange={onManualSeek}
|
||||||
@ -479,6 +500,7 @@ function VideoPreview({
|
|||||||
min={0}
|
min={0}
|
||||||
step={1}
|
step={1}
|
||||||
max={100}
|
max={100}
|
||||||
|
onMouseMove={isMobile ? undefined : onProgressHover}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -500,12 +522,14 @@ function InProgressPreview({
|
|||||||
onTimeUpdate,
|
onTimeUpdate,
|
||||||
}: InProgressPreviewProps) {
|
}: InProgressPreviewProps) {
|
||||||
const apiHost = useApiHost();
|
const apiHost = useApiHost();
|
||||||
|
const sliderRef = useRef<HTMLDivElement | null>(null);
|
||||||
const { data: previewFrames } = useSWR<string[]>(
|
const { data: previewFrames } = useSWR<string[]>(
|
||||||
`preview/${review.camera}/start/${Math.floor(review.start_time) - PREVIEW_PADDING}/end/${
|
`preview/${review.camera}/start/${Math.floor(review.start_time) - PREVIEW_PADDING}/end/${
|
||||||
Math.ceil(review.end_time) + PREVIEW_PADDING
|
Math.ceil(review.end_time) + PREVIEW_PADDING
|
||||||
}/frames`,
|
}/frames`,
|
||||||
);
|
);
|
||||||
const [manualFrame, setManualFrame] = useState(false);
|
const [manualFrame, setManualFrame] = useState(false);
|
||||||
|
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout>();
|
||||||
const [key, setKey] = useState(0);
|
const [key, setKey] = useState(0);
|
||||||
|
|
||||||
const handleLoad = useCallback(() => {
|
const handleLoad = useCallback(() => {
|
||||||
@ -577,6 +601,34 @@ function InProgressPreview({
|
|||||||
[setManualFrame, setIgnoreClick],
|
[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) {
|
if (!previewFrames || previewFrames.length == 0) {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
@ -594,6 +646,7 @@ function InProgressPreview({
|
|||||||
onLoad={handleLoad}
|
onLoad={handleLoad}
|
||||||
/>
|
/>
|
||||||
<Slider
|
<Slider
|
||||||
|
ref={sliderRef}
|
||||||
className="absolute inset-x-0 bottom-0 z-30"
|
className="absolute inset-x-0 bottom-0 z-30"
|
||||||
value={[key]}
|
value={[key]}
|
||||||
onValueChange={onManualSeek}
|
onValueChange={onManualSeek}
|
||||||
@ -601,6 +654,7 @@ function InProgressPreview({
|
|||||||
min={0}
|
min={0}
|
||||||
step={1}
|
step={1}
|
||||||
max={previewFrames.length - 1}
|
max={previewFrames.length - 1}
|
||||||
|
onMouseMove={isMobile ? undefined : onProgressHover}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -15,10 +15,10 @@ const Slider = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...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.Range className="absolute h-full bg-blue-500" />
|
||||||
</SliderPrimitive.Track>
|
</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>
|
</SliderPrimitive.Root>
|
||||||
));
|
));
|
||||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { baseUrl } from "@/api/baseUrl";
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
Dialog,
|
||||||
AlertDialogAction,
|
DialogContent,
|
||||||
AlertDialogCancel,
|
DialogDescription,
|
||||||
AlertDialogContent,
|
DialogFooter,
|
||||||
AlertDialogDescription,
|
DialogHeader,
|
||||||
AlertDialogFooter,
|
DialogTitle,
|
||||||
AlertDialogHeader,
|
} from "@/components/ui/dialog";
|
||||||
AlertDialogTitle,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Event } from "@/types/event";
|
import { Event } from "@/types/event";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
@ -27,64 +26,75 @@ export default function SubmitPlus() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = (await falsePositive)
|
falsePositive
|
||||||
? await axios.put(`events/${upload.id}/false_positive`)
|
? axios.put(`events/${upload.id}/false_positive`)
|
||||||
: await axios.post(`events/${upload.id}/plus`, {
|
: axios.post(`events/${upload.id}/plus`, {
|
||||||
include_annotation: 1,
|
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],
|
[refresh, upload],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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">
|
<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}
|
open={upload != undefined}
|
||||||
onOpenChange={(open) => (!open ? setUpload(undefined) : null)}
|
onOpenChange={(open) => (!open ? setUpload(undefined) : null)}
|
||||||
>
|
>
|
||||||
<AlertDialogContent className="md:max-w-4xl">
|
<DialogContent className="md:max-w-4xl">
|
||||||
<AlertDialogHeader>
|
<DialogHeader>
|
||||||
<AlertDialogTitle>Submit To Frigate+</AlertDialogTitle>
|
<DialogTitle>Submit To Frigate+</DialogTitle>
|
||||||
<AlertDialogDescription>
|
<DialogDescription>
|
||||||
Objects in locations you want to avoid are not false positives.
|
Objects in locations you want to avoid are not false positives.
|
||||||
Submitting them as false positives will confuse the model.
|
Submitting them as false positives will confuse the model.
|
||||||
</AlertDialogDescription>
|
</DialogDescription>
|
||||||
</AlertDialogHeader>
|
</DialogHeader>
|
||||||
<img
|
<img
|
||||||
className="flex-grow-0"
|
className="flex-grow-0"
|
||||||
src={`${baseUrl}api/events/${upload?.id}/snapshot.jpg`}
|
src={`${baseUrl}api/events/${upload?.id}/snapshot.jpg`}
|
||||||
alt={`${upload?.label}`}
|
alt={`${upload?.label}`}
|
||||||
/>
|
/>
|
||||||
<AlertDialogFooter>
|
<DialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<Button>Cancel</Button>
|
||||||
<AlertDialogAction
|
<Button
|
||||||
className="bg-success"
|
className="bg-success"
|
||||||
onClick={() => onSubmitToPlus(false)}
|
onClick={() => onSubmitToPlus(false)}
|
||||||
>
|
>
|
||||||
This is a {upload?.label}
|
This is a {upload?.label}
|
||||||
</AlertDialogAction>
|
</Button>
|
||||||
<AlertDialogAction
|
<Button variant="destructive" onClick={() => onSubmitToPlus(true)}>
|
||||||
className="bg-danger"
|
|
||||||
onClick={() => onSubmitToPlus(true)}
|
|
||||||
>
|
|
||||||
This is not a {upload?.label}
|
This is not a {upload?.label}
|
||||||
</AlertDialogAction>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</DialogFooter>
|
||||||
</AlertDialogContent>
|
</DialogContent>
|
||||||
</AlertDialog>
|
</Dialog>
|
||||||
|
|
||||||
{events?.map((event) => {
|
{events?.map((event) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<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)}
|
onClick={() => setUpload(event)}
|
||||||
>
|
>
|
||||||
<img
|
<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`}
|
src={`${baseUrl}api/events/${event.id}/snapshot.jpg`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -43,7 +43,7 @@ export default function LiveDashboardView({
|
|||||||
|
|
||||||
// if event is ended and was saved, update events list
|
// if event is ended and was saved, update events list
|
||||||
if (eventUpdate.type == "end" && eventUpdate.review.severity == "alert") {
|
if (eventUpdate.type == "end" && eventUpdate.review.severity == "alert") {
|
||||||
updateEvents();
|
setTimeout(() => updateEvents(), 1000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}, [eventUpdate, updateEvents]);
|
}, [eventUpdate, updateEvents]);
|
||||||
|
Loading…
Reference in New Issue
Block a user