mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-11 05:35:25 +03:00
Implement score slider including keyboard input
This commit is contained in:
parent
0b14f3bd9f
commit
4870f976d4
@ -1,7 +1,7 @@
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
import * as React from "react";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
@ -11,7 +11,7 @@ const Slider = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none select-none items-center",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -20,8 +20,8 @@ const Slider = React.forwardRef<
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
))
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
));
|
||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||
|
||||
const VolumeSlider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
@ -56,7 +56,7 @@ const NoThumbSlider = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
<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-selected" />
|
||||
</SliderPrimitive.Track>
|
||||
<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>
|
||||
@ -71,16 +71,17 @@ const DualThumbSlider = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none select-none items-center",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-selected/60">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-selected" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||
<SliderPrimitive.Thumb className="block size-3 rounded-full bg-selected transition-colors cursor-col-resize disabled:pointer-events-none disabled:opacity-50" />
|
||||
<SliderPrimitive.Thumb className="block size-3 rounded-full bg-selected transition-colors cursor-col-resize disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
))
|
||||
DualThumbSlider.displayName = SliderPrimitive.Root.displayName
|
||||
));
|
||||
DualThumbSlider.displayName = SliderPrimitive.Root.displayName;
|
||||
|
||||
export { DualThumbSlider, Slider, NoThumbSlider, VolumeSlider }
|
||||
export { DualThumbSlider, Slider, NoThumbSlider, VolumeSlider };
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { baseUrl } from "@/api/baseUrl";
|
||||
import FilterCheckBox from "@/components/filter/FilterCheckBox";
|
||||
import {
|
||||
CamerasFilterButton,
|
||||
GeneralFilterContent,
|
||||
@ -17,10 +16,11 @@ import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { DualThumbSlider } from "@/components/ui/slider";
|
||||
import { Event } from "@/types/event";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import axios from "axios";
|
||||
@ -53,6 +53,8 @@ export default function SubmitPlus() {
|
||||
is_submitted: 0,
|
||||
cameras: selectedCameras ? selectedCameras.join(",") : null,
|
||||
labels: selectedLabels ? selectedLabels.join(",") : null,
|
||||
min_score: scoreRange ? scoreRange[0] : null,
|
||||
max_score: scoreRange ? scoreRange[1] : null,
|
||||
},
|
||||
]);
|
||||
const [upload, setUpload] = useState<Event>();
|
||||
@ -112,9 +114,11 @@ export default function SubmitPlus() {
|
||||
<div className="size-full flex flex-col">
|
||||
<PlusFilterGroup
|
||||
selectedCameras={selectedCameras}
|
||||
setSelectedCameras={setSelectedCameras}
|
||||
selectedLabels={selectedLabels}
|
||||
selectedScoreRange={scoreRange}
|
||||
setSelectedCameras={setSelectedCameras}
|
||||
setSelectedLabels={setSelectedLabels}
|
||||
setSelectedScoreRange={setScoreRange}
|
||||
/>
|
||||
<div className="size-full flex flex-1 flex-wrap content-start gap-2 md:gap-4 overflow-y-auto no-scrollbar">
|
||||
<div className="w-full p-2 grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2">
|
||||
@ -184,15 +188,19 @@ const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
|
||||
|
||||
type PlusFilterGroupProps = {
|
||||
selectedCameras: string[] | undefined;
|
||||
setSelectedCameras: (cameras: string[] | undefined) => void;
|
||||
selectedLabels: string[] | undefined;
|
||||
selectedScoreRange: number[] | undefined;
|
||||
setSelectedCameras: (cameras: string[] | undefined) => void;
|
||||
setSelectedLabels: (cameras: string[] | undefined) => void;
|
||||
setSelectedScoreRange: (range: number[] | undefined) => void;
|
||||
};
|
||||
function PlusFilterGroup({
|
||||
selectedCameras,
|
||||
setSelectedCameras,
|
||||
selectedLabels,
|
||||
selectedScoreRange,
|
||||
setSelectedCameras,
|
||||
setSelectedLabels,
|
||||
setSelectedScoreRange,
|
||||
}: PlusFilterGroupProps) {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
@ -226,12 +234,12 @@ function PlusFilterGroup({
|
||||
const [open, setOpen] = useState<"none" | "camera" | "label" | "score">(
|
||||
"none",
|
||||
);
|
||||
const [currentCameras, setCurrentCameras] = useState<string[] | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [currentScoreRange, setCurrentScoreRange] = useState<
|
||||
number[] | undefined
|
||||
>(undefined);
|
||||
|
||||
const Menu = isMobile ? Drawer : DropdownMenu;
|
||||
const Trigger = isMobile ? DrawerTrigger : DropdownMenuTrigger;
|
||||
@ -284,77 +292,79 @@ function PlusFilterGroup({
|
||||
<Menu
|
||||
open={open == "score"}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setCurrentCameras(selectedCameras);
|
||||
}
|
||||
setOpen(open ? "score" : "none");
|
||||
}}
|
||||
>
|
||||
<Trigger asChild>
|
||||
<Button size="sm" className="flex items-center gap-2 capitalize">
|
||||
<PiSlidersHorizontalFill className="text-secondary-foreground" />
|
||||
<Button
|
||||
className="flex items-center gap-2 capitalize"
|
||||
size="sm"
|
||||
variant={selectedScoreRange == undefined ? "default" : "select"}
|
||||
>
|
||||
<PiSlidersHorizontalFill
|
||||
className={`${selectedScoreRange == undefined ? "text-secondary-foreground" : "text-selected-foreground"}`}
|
||||
/>
|
||||
<div className="hidden md:block text-primary">
|
||||
{selectedCameras == undefined
|
||||
? "All Cameras"
|
||||
: `${selectedCameras.length} Cameras`}
|
||||
{selectedScoreRange == undefined
|
||||
? "Score Range"
|
||||
: `${selectedScoreRange[0] * 100}% - ${selectedScoreRange[1] * 100}%`}
|
||||
</div>
|
||||
</Button>
|
||||
</Trigger>
|
||||
<Content className={isMobile ? "max-h-[75dvh]" : ""}>
|
||||
<DropdownMenuLabel className="flex justify-center">
|
||||
Filter Cameras
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<FilterCheckBox
|
||||
isChecked={currentCameras == undefined}
|
||||
label="All Cameras"
|
||||
onCheckedChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
setCurrentCameras(undefined);
|
||||
<Content
|
||||
className={`min-w-80 p-2 flex flex-col justify-center ${isMobile ? "gap-2 *:max-h-[75dvh]" : ""}`}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
className="w-12"
|
||||
inputMode="numeric"
|
||||
value={Math.round((currentScoreRange?.at(0) ?? 0.5) * 100)}
|
||||
onChange={(e) =>
|
||||
setCurrentScoreRange([
|
||||
parseInt(e.target.value) / 100.0,
|
||||
currentScoreRange?.at(1) ?? 1.0,
|
||||
])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<div className={isMobile ? "h-auto overflow-y-auto" : ""}>
|
||||
{allCameras.map((item) => (
|
||||
<FilterCheckBox
|
||||
key={item}
|
||||
isChecked={currentCameras?.includes(item) ?? false}
|
||||
label={item.replaceAll("_", " ")}
|
||||
onCheckedChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
const updatedCameras = currentCameras
|
||||
? [...currentCameras]
|
||||
: [];
|
||||
|
||||
updatedCameras.push(item);
|
||||
setCurrentCameras(updatedCameras);
|
||||
} else {
|
||||
const updatedCameras = currentCameras
|
||||
? [...currentCameras]
|
||||
: [];
|
||||
|
||||
// can not deselect the last item
|
||||
if (updatedCameras.length > 1) {
|
||||
updatedCameras.splice(updatedCameras.indexOf(item), 1);
|
||||
setCurrentCameras(updatedCameras);
|
||||
}
|
||||
}
|
||||
}}
|
||||
<DualThumbSlider
|
||||
className="w-full"
|
||||
min={0.5}
|
||||
max={1.0}
|
||||
step={0.01}
|
||||
value={currentScoreRange ?? [0.5, 1.0]}
|
||||
onValueChange={setCurrentScoreRange}
|
||||
/>
|
||||
<Input
|
||||
className="w-12"
|
||||
inputMode="numeric"
|
||||
value={Math.round((currentScoreRange?.at(1) ?? 1.0) * 100)}
|
||||
onChange={(e) =>
|
||||
setCurrentScoreRange([
|
||||
currentScoreRange?.at(0) ?? 0.5,
|
||||
parseInt(e.target.value) / 100.0,
|
||||
])
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex justify-center items-center">
|
||||
<div className="p-2 flex justify-evenly items-center">
|
||||
<Button
|
||||
variant="select"
|
||||
onClick={() => {
|
||||
setSelectedCameras(currentCameras);
|
||||
setSelectedScoreRange(currentScoreRange);
|
||||
setOpen("none");
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setCurrentScoreRange(undefined);
|
||||
setSelectedScoreRange(undefined);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</Content>
|
||||
</Menu>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user