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:
|
||||
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,38 +117,39 @@ export default function PreviewThumbnailPlayer({
|
||||
}
|
||||
}, [allPreviews, review]);
|
||||
|
||||
// Hover Playback
|
||||
|
||||
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout | null>();
|
||||
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 (
|
||||
<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