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 React from "react";
|
||||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Slider = React.forwardRef<
|
const Slider = React.forwardRef<
|
||||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
@ -11,7 +11,7 @@ const Slider = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full touch-none select-none items-center",
|
"relative flex w-full touch-none select-none items-center",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@ -20,8 +20,8 @@ const Slider = React.forwardRef<
|
|||||||
</SliderPrimitive.Track>
|
</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 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>
|
</SliderPrimitive.Root>
|
||||||
))
|
));
|
||||||
Slider.displayName = SliderPrimitive.Root.displayName
|
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|
||||||
const VolumeSlider = React.forwardRef<
|
const VolumeSlider = React.forwardRef<
|
||||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
@ -56,7 +56,7 @@ const NoThumbSlider = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SliderPrimitive.Track className="relative h-2 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-selected" />
|
||||||
</SliderPrimitive.Track>
|
</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.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>
|
||||||
@ -71,16 +71,17 @@ const DualThumbSlider = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full touch-none select-none items-center",
|
"relative flex w-full touch-none select-none items-center",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-selected/60">
|
||||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
<SliderPrimitive.Range className="absolute h-full bg-selected" />
|
||||||
</SliderPrimitive.Track>
|
</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>
|
</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 { baseUrl } from "@/api/baseUrl";
|
||||||
import FilterCheckBox from "@/components/filter/FilterCheckBox";
|
|
||||||
import {
|
import {
|
||||||
CamerasFilterButton,
|
CamerasFilterButton,
|
||||||
GeneralFilterContent,
|
GeneralFilterContent,
|
||||||
@ -17,10 +16,11 @@ import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
|||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { DualThumbSlider } from "@/components/ui/slider";
|
||||||
import { Event } from "@/types/event";
|
import { Event } from "@/types/event";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@ -53,6 +53,8 @@ export default function SubmitPlus() {
|
|||||||
is_submitted: 0,
|
is_submitted: 0,
|
||||||
cameras: selectedCameras ? selectedCameras.join(",") : null,
|
cameras: selectedCameras ? selectedCameras.join(",") : null,
|
||||||
labels: selectedLabels ? selectedLabels.join(",") : null,
|
labels: selectedLabels ? selectedLabels.join(",") : null,
|
||||||
|
min_score: scoreRange ? scoreRange[0] : null,
|
||||||
|
max_score: scoreRange ? scoreRange[1] : null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const [upload, setUpload] = useState<Event>();
|
const [upload, setUpload] = useState<Event>();
|
||||||
@ -112,9 +114,11 @@ export default function SubmitPlus() {
|
|||||||
<div className="size-full flex flex-col">
|
<div className="size-full flex flex-col">
|
||||||
<PlusFilterGroup
|
<PlusFilterGroup
|
||||||
selectedCameras={selectedCameras}
|
selectedCameras={selectedCameras}
|
||||||
setSelectedCameras={setSelectedCameras}
|
|
||||||
selectedLabels={selectedLabels}
|
selectedLabels={selectedLabels}
|
||||||
|
selectedScoreRange={scoreRange}
|
||||||
|
setSelectedCameras={setSelectedCameras}
|
||||||
setSelectedLabels={setSelectedLabels}
|
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="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">
|
<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 = {
|
type PlusFilterGroupProps = {
|
||||||
selectedCameras: string[] | undefined;
|
selectedCameras: string[] | undefined;
|
||||||
setSelectedCameras: (cameras: string[] | undefined) => void;
|
|
||||||
selectedLabels: string[] | undefined;
|
selectedLabels: string[] | undefined;
|
||||||
|
selectedScoreRange: number[] | undefined;
|
||||||
|
setSelectedCameras: (cameras: string[] | undefined) => void;
|
||||||
setSelectedLabels: (cameras: string[] | undefined) => void;
|
setSelectedLabels: (cameras: string[] | undefined) => void;
|
||||||
|
setSelectedScoreRange: (range: number[] | undefined) => void;
|
||||||
};
|
};
|
||||||
function PlusFilterGroup({
|
function PlusFilterGroup({
|
||||||
selectedCameras,
|
selectedCameras,
|
||||||
setSelectedCameras,
|
|
||||||
selectedLabels,
|
selectedLabels,
|
||||||
|
selectedScoreRange,
|
||||||
|
setSelectedCameras,
|
||||||
setSelectedLabels,
|
setSelectedLabels,
|
||||||
|
setSelectedScoreRange,
|
||||||
}: PlusFilterGroupProps) {
|
}: PlusFilterGroupProps) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
@ -226,12 +234,12 @@ function PlusFilterGroup({
|
|||||||
const [open, setOpen] = useState<"none" | "camera" | "label" | "score">(
|
const [open, setOpen] = useState<"none" | "camera" | "label" | "score">(
|
||||||
"none",
|
"none",
|
||||||
);
|
);
|
||||||
const [currentCameras, setCurrentCameras] = useState<string[] | undefined>(
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
|
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
const [currentScoreRange, setCurrentScoreRange] = useState<
|
||||||
|
number[] | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
const Menu = isMobile ? Drawer : DropdownMenu;
|
const Menu = isMobile ? Drawer : DropdownMenu;
|
||||||
const Trigger = isMobile ? DrawerTrigger : DropdownMenuTrigger;
|
const Trigger = isMobile ? DrawerTrigger : DropdownMenuTrigger;
|
||||||
@ -284,77 +292,79 @@ function PlusFilterGroup({
|
|||||||
<Menu
|
<Menu
|
||||||
open={open == "score"}
|
open={open == "score"}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
|
||||||
setCurrentCameras(selectedCameras);
|
|
||||||
}
|
|
||||||
setOpen(open ? "score" : "none");
|
setOpen(open ? "score" : "none");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trigger asChild>
|
<Trigger asChild>
|
||||||
<Button size="sm" className="flex items-center gap-2 capitalize">
|
<Button
|
||||||
<PiSlidersHorizontalFill className="text-secondary-foreground" />
|
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">
|
<div className="hidden md:block text-primary">
|
||||||
{selectedCameras == undefined
|
{selectedScoreRange == undefined
|
||||||
? "All Cameras"
|
? "Score Range"
|
||||||
: `${selectedCameras.length} Cameras`}
|
: `${selectedScoreRange[0] * 100}% - ${selectedScoreRange[1] * 100}%`}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Content className={isMobile ? "max-h-[75dvh]" : ""}>
|
<Content
|
||||||
<DropdownMenuLabel className="flex justify-center">
|
className={`min-w-80 p-2 flex flex-col justify-center ${isMobile ? "gap-2 *:max-h-[75dvh]" : ""}`}
|
||||||
Filter Cameras
|
>
|
||||||
</DropdownMenuLabel>
|
<div className="flex items-center gap-1">
|
||||||
<DropdownMenuSeparator />
|
<Input
|
||||||
<FilterCheckBox
|
className="w-12"
|
||||||
isChecked={currentCameras == undefined}
|
inputMode="numeric"
|
||||||
label="All Cameras"
|
value={Math.round((currentScoreRange?.at(0) ?? 0.5) * 100)}
|
||||||
onCheckedChange={(isChecked) => {
|
onChange={(e) =>
|
||||||
if (isChecked) {
|
setCurrentScoreRange([
|
||||||
setCurrentCameras(undefined);
|
parseInt(e.target.value) / 100.0,
|
||||||
|
currentScoreRange?.at(1) ?? 1.0,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<DropdownMenuSeparator />
|
<DualThumbSlider
|
||||||
<div className={isMobile ? "h-auto overflow-y-auto" : ""}>
|
className="w-full"
|
||||||
{allCameras.map((item) => (
|
min={0.5}
|
||||||
<FilterCheckBox
|
max={1.0}
|
||||||
key={item}
|
step={0.01}
|
||||||
isChecked={currentCameras?.includes(item) ?? false}
|
value={currentScoreRange ?? [0.5, 1.0]}
|
||||||
label={item.replaceAll("_", " ")}
|
onValueChange={setCurrentScoreRange}
|
||||||
onCheckedChange={(isChecked) => {
|
/>
|
||||||
if (isChecked) {
|
<Input
|
||||||
const updatedCameras = currentCameras
|
className="w-12"
|
||||||
? [...currentCameras]
|
inputMode="numeric"
|
||||||
: [];
|
value={Math.round((currentScoreRange?.at(1) ?? 1.0) * 100)}
|
||||||
|
onChange={(e) =>
|
||||||
updatedCameras.push(item);
|
setCurrentScoreRange([
|
||||||
setCurrentCameras(updatedCameras);
|
currentScoreRange?.at(0) ?? 0.5,
|
||||||
} else {
|
parseInt(e.target.value) / 100.0,
|
||||||
const updatedCameras = currentCameras
|
])
|
||||||
? [...currentCameras]
|
}
|
||||||
: [];
|
|
||||||
|
|
||||||
// can not deselect the last item
|
|
||||||
if (updatedCameras.length > 1) {
|
|
||||||
updatedCameras.splice(updatedCameras.indexOf(item), 1);
|
|
||||||
setCurrentCameras(updatedCameras);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<div className="flex justify-center items-center">
|
<div className="p-2 flex justify-evenly items-center">
|
||||||
<Button
|
<Button
|
||||||
variant="select"
|
variant="select"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedCameras(currentCameras);
|
setSelectedScoreRange(currentScoreRange);
|
||||||
setOpen("none");
|
setOpen("none");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Apply
|
Apply
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentScoreRange(undefined);
|
||||||
|
setSelectedScoreRange(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Content>
|
</Content>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user