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

View File

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

View File

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

View File

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

View File

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