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:
Nicolas Mowen 2024-03-07 07:34:11 -07:00 committed by GitHub
parent b2931bcaa9
commit 8776cdfd5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 139 additions and 70 deletions

View File

@ -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):

View File

@ -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,10 +117,14 @@ export default function PreviewThumbnailPlayer({
} }
}, [allPreviews, review]); }, [allPreviews, review]);
const playingBack = useMemo(() => playback, [playback]); // Hover Playback
const onPlayback = useCallback( const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout | null>();
(isHovered: boolean) => { const [playback, setPlayback] = useState(false);
const playingBack = useMemo(() => playback, [playback]);
const [isHovered, setIsHovered] = useState(false);
useEffect(() => {
if (isHovered && scrollLock) { if (isHovered && scrollLock) {
return; return;
} }
@ -146,12 +147,9 @@ export default function PreviewThumbnailPlayer({
onTimeUpdate(undefined); 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>
); );

View File

@ -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;

View File

@ -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>

View File

@ -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]);