mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-03-27 00:17:27 +01:00
Implement score filtering on Frigate+ Page (#10968)
* Fix portrait layout disappearing * Refactor sliders * Reuse camera filter * Reuse label filter content * Implement score slider including keyboard input * Implement ability to sort frigate plus submissions
This commit is contained in:
parent
b65656fa87
commit
a3e2171675
@ -77,6 +77,8 @@ def events():
|
|||||||
min_length = request.args.get("min_length", type=float)
|
min_length = request.args.get("min_length", type=float)
|
||||||
max_length = request.args.get("max_length", type=float)
|
max_length = request.args.get("max_length", type=float)
|
||||||
|
|
||||||
|
sort = request.args.get("sort", type=str)
|
||||||
|
|
||||||
clauses = []
|
clauses = []
|
||||||
|
|
||||||
selected_columns = [
|
selected_columns = [
|
||||||
@ -219,10 +221,22 @@ def events():
|
|||||||
if len(clauses) == 0:
|
if len(clauses) == 0:
|
||||||
clauses.append((True))
|
clauses.append((True))
|
||||||
|
|
||||||
|
if sort:
|
||||||
|
if sort == "score_asc":
|
||||||
|
order_by = Event.data["score"].asc()
|
||||||
|
elif sort == "score_desc":
|
||||||
|
order_by = Event.data["score"].desc()
|
||||||
|
elif sort == "date_asc":
|
||||||
|
Event.start_time.asc()
|
||||||
|
elif sort == "date_desc":
|
||||||
|
Event.start_time.desc()
|
||||||
|
else:
|
||||||
|
order_by = Event.start_time.desc()
|
||||||
|
|
||||||
events = (
|
events = (
|
||||||
Event.select(*selected_columns)
|
Event.select(*selected_columns)
|
||||||
.where(reduce(operator.and_, clauses))
|
.where(reduce(operator.and_, clauses))
|
||||||
.order_by(Event.start_time.desc())
|
.order_by(order_by)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.dicts()
|
.dicts()
|
||||||
.iterator()
|
.iterator()
|
||||||
|
@ -209,7 +209,7 @@ type CameraFilterButtonProps = {
|
|||||||
selectedCameras: string[] | undefined;
|
selectedCameras: string[] | undefined;
|
||||||
updateCameraFilter: (cameras: string[] | undefined) => void;
|
updateCameraFilter: (cameras: string[] | undefined) => void;
|
||||||
};
|
};
|
||||||
function CamerasFilterButton({
|
export function CamerasFilterButton({
|
||||||
allCameras,
|
allCameras,
|
||||||
groups,
|
groups,
|
||||||
selectedCameras,
|
selectedCameras,
|
||||||
@ -227,7 +227,7 @@ function CamerasFilterButton({
|
|||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<FaVideo
|
<FaVideo
|
||||||
className={`${selectedCameras?.length == 1 ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
className={`${(selectedCameras?.length ?? 0) >= 1 ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`hidden md:block ${selectedCameras?.length ? "text-selected-foreground" : "text-primary"}`}
|
className={`hidden md:block ${selectedCameras?.length ? "text-selected-foreground" : "text-primary"}`}
|
||||||
|
@ -8,7 +8,6 @@ import React, {
|
|||||||
import { useApiHost } from "@/api";
|
import { useApiHost } from "@/api";
|
||||||
import { isCurrentHour } from "@/utils/dateUtil";
|
import { isCurrentHour } from "@/utils/dateUtil";
|
||||||
import { ReviewSegment } from "@/types/review";
|
import { ReviewSegment } from "@/types/review";
|
||||||
import { Slider } from "../ui/slider-no-thumb";
|
|
||||||
import { getIconForLabel } from "@/utils/iconUtil";
|
import { getIconForLabel } from "@/utils/iconUtil";
|
||||||
import TimeAgo from "../dynamic/TimeAgo";
|
import TimeAgo from "../dynamic/TimeAgo";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -23,6 +22,7 @@ import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator";
|
|||||||
import useContextMenu from "@/hooks/use-contextmenu";
|
import useContextMenu from "@/hooks/use-contextmenu";
|
||||||
import ActivityIndicator from "../indicators/activity-indicator";
|
import ActivityIndicator from "../indicators/activity-indicator";
|
||||||
import { TimeRange } from "@/types/timeline";
|
import { TimeRange } from "@/types/timeline";
|
||||||
|
import { NoThumbSlider } from "../ui/slider";
|
||||||
|
|
||||||
type PreviewPlayerProps = {
|
type PreviewPlayerProps = {
|
||||||
review: ReviewSegment;
|
review: ReviewSegment;
|
||||||
@ -543,7 +543,7 @@ function VideoPreview({
|
|||||||
>
|
>
|
||||||
<source src={relevantPreview.src} type={relevantPreview.type} />
|
<source src={relevantPreview.src} type={relevantPreview.type} />
|
||||||
</video>
|
</video>
|
||||||
<Slider
|
<NoThumbSlider
|
||||||
ref={sliderRef}
|
ref={sliderRef}
|
||||||
className="absolute inset-x-0 bottom-0 z-30"
|
className="absolute inset-x-0 bottom-0 z-30"
|
||||||
value={[progress]}
|
value={[progress]}
|
||||||
@ -707,7 +707,7 @@ function InProgressPreview({
|
|||||||
src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.webp`}
|
src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.webp`}
|
||||||
onLoad={handleLoad}
|
onLoad={handleLoad}
|
||||||
/>
|
/>
|
||||||
<Slider
|
<NoThumbSlider
|
||||||
ref={sliderRef}
|
ref={sliderRef}
|
||||||
className="absolute inset-x-0 bottom-0 z-30"
|
className="absolute inset-x-0 bottom-0 z-30"
|
||||||
value={[key]}
|
value={[key]}
|
||||||
|
@ -16,8 +16,8 @@ import {
|
|||||||
MdVolumeOff,
|
MdVolumeOff,
|
||||||
MdVolumeUp,
|
MdVolumeUp,
|
||||||
} from "react-icons/md";
|
} from "react-icons/md";
|
||||||
import { Slider } from "../ui/slider-volume";
|
|
||||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||||
|
import { VolumeSlider } from "../ui/slider";
|
||||||
|
|
||||||
type VideoControls = {
|
type VideoControls = {
|
||||||
volume?: boolean;
|
volume?: boolean;
|
||||||
@ -154,7 +154,7 @@ export default function VideoControls({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{video.muted == false && (
|
{video.muted == false && (
|
||||||
<Slider
|
<VolumeSlider
|
||||||
className="w-20"
|
className="w-20"
|
||||||
value={[video.volume]}
|
value={[video.volume]}
|
||||||
min={0}
|
min={0}
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Slider = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SliderPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex w-full touch-none select-none items-center",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<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-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;
|
|
||||||
|
|
||||||
export { Slider };
|
|
@ -1,26 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const Slider = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SliderPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex w-full touch-none select-none items-center",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-muted">
|
|
||||||
<SliderPrimitive.Range className="absolute h-full bg-white" />
|
|
||||||
</SliderPrimitive.Track>
|
|
||||||
<SliderPrimitive.Thumb className="block h-3 w-3 rounded-full bg-white ring-white focus:ring-white disabled:pointer-events-none disabled:opacity-50" />
|
|
||||||
</SliderPrimitive.Root>
|
|
||||||
));
|
|
||||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
export { Slider };
|
|
@ -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,7 +20,68 @@ 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;
|
||||||
|
|
||||||
export { Slider }
|
const VolumeSlider = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full touch-none select-none items-center",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-muted">
|
||||||
|
<SliderPrimitive.Range className="absolute h-full bg-white" />
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
<SliderPrimitive.Thumb className="block h-3 w-3 rounded-full bg-white ring-white focus:ring-white disabled:pointer-events-none disabled:opacity-50" />
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
));
|
||||||
|
VolumeSlider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const NoThumbSlider = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full touch-none select-none items-center",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full">
|
||||||
|
<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>
|
||||||
|
));
|
||||||
|
NoThumbSlider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const DualThumbSlider = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full touch-none select-none items-center",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<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 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;
|
||||||
|
|
||||||
|
export { DualThumbSlider, Slider, NoThumbSlider, VolumeSlider };
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { baseUrl } from "@/api/baseUrl";
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
import FilterCheckBox from "@/components/filter/FilterCheckBox";
|
import {
|
||||||
|
CamerasFilterButton,
|
||||||
|
GeneralFilterContent,
|
||||||
|
} from "@/components/filter/ReviewFilterGroup";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -13,16 +16,25 @@ 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 { Label } from "@/components/ui/label";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
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";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import { FaList, FaVideo } from "react-icons/fa";
|
import {
|
||||||
|
FaList,
|
||||||
|
FaSort,
|
||||||
|
FaSortAmountDown,
|
||||||
|
FaSortAmountUp,
|
||||||
|
} from "react-icons/fa";
|
||||||
|
import { PiSlidersHorizontalFill } from "react-icons/pi";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
export default function SubmitPlus() {
|
export default function SubmitPlus() {
|
||||||
@ -36,6 +48,11 @@ export default function SubmitPlus() {
|
|||||||
|
|
||||||
const [selectedCameras, setSelectedCameras] = useState<string[]>();
|
const [selectedCameras, setSelectedCameras] = useState<string[]>();
|
||||||
const [selectedLabels, setSelectedLabels] = useState<string[]>();
|
const [selectedLabels, setSelectedLabels] = useState<string[]>();
|
||||||
|
const [scoreRange, setScoreRange] = useState<number[]>();
|
||||||
|
|
||||||
|
// sort
|
||||||
|
|
||||||
|
const [sort, setSort] = useState<string>();
|
||||||
|
|
||||||
// data
|
// data
|
||||||
|
|
||||||
@ -47,6 +64,9 @@ 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,
|
||||||
|
sort: sort ? sort : null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const [upload, setUpload] = useState<Event>();
|
const [upload, setUpload] = useState<Event>();
|
||||||
@ -104,12 +124,17 @@ export default function SubmitPlus() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="size-full flex flex-col">
|
<div className="size-full flex flex-col">
|
||||||
<PlusFilterGroup
|
<div className="w-full h-16 px-2 flex items-center justify-between overflow-x-auto">
|
||||||
selectedCameras={selectedCameras}
|
<PlusFilterGroup
|
||||||
setSelectedCameras={setSelectedCameras}
|
selectedCameras={selectedCameras}
|
||||||
selectedLabels={selectedLabels}
|
selectedLabels={selectedLabels}
|
||||||
setSelectedLabels={setSelectedLabels}
|
selectedScoreRange={scoreRange}
|
||||||
/>
|
setSelectedCameras={setSelectedCameras}
|
||||||
|
setSelectedLabels={setSelectedLabels}
|
||||||
|
setSelectedScoreRange={setScoreRange}
|
||||||
|
/>
|
||||||
|
<PlusSortSelector selectedSort={sort} setSelectedSort={setSort} />
|
||||||
|
</div>
|
||||||
<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">
|
||||||
<Dialog
|
<Dialog
|
||||||
@ -178,15 +203,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");
|
||||||
|
|
||||||
@ -217,97 +246,28 @@ function PlusFilterGroup({
|
|||||||
return [...labels].sort();
|
return [...labels].sort();
|
||||||
}, [config, selectedCameras]);
|
}, [config, selectedCameras]);
|
||||||
|
|
||||||
const [open, setOpen] = useState<"none" | "camera" | "label">("none");
|
const [open, setOpen] = useState<"none" | "camera" | "label" | "score">(
|
||||||
const [currentCameras, setCurrentCameras] = useState<string[] | undefined>(
|
"none",
|
||||||
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;
|
||||||
const Content = isMobile ? DrawerContent : DropdownMenuContent;
|
const Content = isMobile ? DrawerContent : DropdownMenuContent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-16 flex justify-start gap-2 items-center">
|
<div className="h-full flex justify-start gap-2 items-center">
|
||||||
<Menu
|
<CamerasFilterButton
|
||||||
open={open == "camera"}
|
allCameras={allCameras}
|
||||||
onOpenChange={(open) => {
|
groups={[]}
|
||||||
if (!open) {
|
selectedCameras={selectedCameras}
|
||||||
setCurrentCameras(selectedCameras);
|
updateCameraFilter={setSelectedCameras}
|
||||||
}
|
/>
|
||||||
setOpen(open ? "camera" : "none");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trigger asChild>
|
|
||||||
<Button size="sm" className="mx-1 capitalize">
|
|
||||||
<FaVideo className="md:mr-[10px] text-secondary-foreground" />
|
|
||||||
<div className="hidden md:block text-primary">
|
|
||||||
{selectedCameras == undefined
|
|
||||||
? "All Cameras"
|
|
||||||
: `${selectedCameras.length} Cameras`}
|
|
||||||
</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);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<div className="flex justify-center items-center">
|
|
||||||
<Button
|
|
||||||
variant="select"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedCameras(currentCameras);
|
|
||||||
setOpen("none");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Apply
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Content>
|
|
||||||
</Menu>
|
|
||||||
<Menu
|
<Menu
|
||||||
open={open == "label"}
|
open={open == "label"}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
@ -318,8 +278,14 @@ function PlusFilterGroup({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trigger asChild>
|
<Trigger asChild>
|
||||||
<Button size="sm" className="mx-1 capitalize">
|
<Button
|
||||||
<FaList className="md:mr-[10px] text-secondary-foreground" />
|
className="flex items-center gap-2 capitalize"
|
||||||
|
size="sm"
|
||||||
|
variant={selectedLabels == undefined ? "default" : "select"}
|
||||||
|
>
|
||||||
|
<FaList
|
||||||
|
className={`${selectedLabels == undefined ? "text-secondary-foreground" : "text-selected-foreground"}`}
|
||||||
|
/>
|
||||||
<div className="hidden md:block text-primary">
|
<div className="hidden md:block text-primary">
|
||||||
{selectedLabels == undefined
|
{selectedLabels == undefined
|
||||||
? "All Labels"
|
? "All Labels"
|
||||||
@ -328,60 +294,250 @@ function PlusFilterGroup({
|
|||||||
</Button>
|
</Button>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Content className={isMobile ? "max-h-[75dvh]" : ""}>
|
<Content className={isMobile ? "max-h-[75dvh]" : ""}>
|
||||||
<DropdownMenuLabel className="flex justify-center">
|
<GeneralFilterContent
|
||||||
Filter Labels
|
allLabels={allLabels}
|
||||||
</DropdownMenuLabel>
|
selectedLabels={selectedLabels}
|
||||||
<DropdownMenuSeparator />
|
currentLabels={currentLabels}
|
||||||
<FilterCheckBox
|
setCurrentLabels={setCurrentLabels}
|
||||||
isChecked={currentLabels == undefined}
|
updateLabelFilter={setSelectedLabels}
|
||||||
label="All Labels"
|
onClose={() => setOpen("none")}
|
||||||
onCheckedChange={(isChecked) => {
|
|
||||||
if (isChecked) {
|
|
||||||
setCurrentLabels(undefined);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<DropdownMenuSeparator />
|
</Content>
|
||||||
<div className={isMobile ? "h-auto overflow-y-auto" : ""}>
|
</Menu>
|
||||||
{allLabels.map((item) => (
|
<Menu
|
||||||
<FilterCheckBox
|
open={open == "score"}
|
||||||
key={item}
|
onOpenChange={(open) => {
|
||||||
isChecked={currentLabels?.includes(item) ?? false}
|
setOpen(open ? "score" : "none");
|
||||||
label={item.replaceAll("_", " ")}
|
}}
|
||||||
onCheckedChange={(isChecked) => {
|
>
|
||||||
if (isChecked) {
|
<Trigger asChild>
|
||||||
const updatedLabels = currentLabels
|
<Button
|
||||||
? [...currentLabels]
|
className="flex items-center gap-2 capitalize"
|
||||||
: [];
|
size="sm"
|
||||||
|
variant={selectedScoreRange == undefined ? "default" : "select"}
|
||||||
updatedLabels.push(item);
|
>
|
||||||
setCurrentLabels(updatedLabels);
|
<PiSlidersHorizontalFill
|
||||||
} else {
|
className={`${selectedScoreRange == undefined ? "text-secondary-foreground" : "text-selected-foreground"}`}
|
||||||
const updatedLabels = currentLabels
|
/>
|
||||||
? [...currentLabels]
|
<div className="hidden md:block text-primary">
|
||||||
: [];
|
{selectedScoreRange == undefined
|
||||||
|
? "Score Range"
|
||||||
// can not deselect the last item
|
: `${selectedScoreRange[0] * 100}% - ${selectedScoreRange[1] * 100}%`}
|
||||||
if (updatedLabels.length > 1) {
|
</div>
|
||||||
updatedLabels.splice(updatedLabels.indexOf(item), 1);
|
</Button>
|
||||||
setCurrentLabels(updatedLabels);
|
</Trigger>
|
||||||
}
|
<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,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<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>
|
</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={() => {
|
||||||
setSelectedLabels(currentLabels);
|
setSelectedScoreRange(currentScoreRange);
|
||||||
setOpen("none");
|
setOpen("none");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Apply
|
Apply
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentScoreRange(undefined);
|
||||||
|
setSelectedScoreRange(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Content>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlusSortSelectorProps = {
|
||||||
|
selectedSort?: string;
|
||||||
|
setSelectedSort: (sort: string | undefined) => void;
|
||||||
|
};
|
||||||
|
function PlusSortSelector({
|
||||||
|
selectedSort,
|
||||||
|
setSelectedSort,
|
||||||
|
}: PlusSortSelectorProps) {
|
||||||
|
// menu state
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
// sort
|
||||||
|
|
||||||
|
const [currentSort, setCurrentSort] = useState<string>();
|
||||||
|
const [currentDir, setCurrentDir] = useState<string>("desc");
|
||||||
|
|
||||||
|
// components
|
||||||
|
|
||||||
|
const Sort = selectedSort
|
||||||
|
? selectedSort.split("_")[1] == "desc"
|
||||||
|
? FaSortAmountDown
|
||||||
|
: FaSortAmountUp
|
||||||
|
: FaSort;
|
||||||
|
const Menu = isMobile ? Drawer : DropdownMenu;
|
||||||
|
const Trigger = isMobile ? DrawerTrigger : DropdownMenuTrigger;
|
||||||
|
const Content = isMobile ? DrawerContent : DropdownMenuContent;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex justify-start gap-2 items-center">
|
||||||
|
<Menu
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setOpen(open);
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
const parts = selectedSort?.split("_");
|
||||||
|
|
||||||
|
if (parts?.length == 2) {
|
||||||
|
setCurrentSort(parts[0]);
|
||||||
|
setCurrentDir(parts[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trigger asChild>
|
||||||
|
<Button
|
||||||
|
className="flex items-center gap-2 capitalize"
|
||||||
|
size="sm"
|
||||||
|
variant={selectedSort == undefined ? "default" : "select"}
|
||||||
|
>
|
||||||
|
<Sort
|
||||||
|
className={`${selectedSort == undefined ? "text-secondary-foreground" : "text-selected-foreground"}`}
|
||||||
|
/>
|
||||||
|
<div className="hidden md:block text-primary">
|
||||||
|
{selectedSort == undefined ? "Sort" : selectedSort.split("_")[0]}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Trigger>
|
||||||
|
<Content
|
||||||
|
className={`p-2 flex flex-col justify-center gap-2 ${isMobile ? "max-h-[75dvh]" : ""}`}
|
||||||
|
>
|
||||||
|
<RadioGroup
|
||||||
|
className={`flex flex-col gap-4 ${isMobile ? "mt-4" : ""}`}
|
||||||
|
onValueChange={(value) => setCurrentSort(value)}
|
||||||
|
>
|
||||||
|
<div className="w-full flex items-center gap-2">
|
||||||
|
<RadioGroupItem
|
||||||
|
className={
|
||||||
|
currentSort == "date"
|
||||||
|
? "from-selected/50 to-selected/90 text-selected bg-selected"
|
||||||
|
: "from-secondary/50 to-secondary/90 text-secondary bg-secondary"
|
||||||
|
}
|
||||||
|
id="date"
|
||||||
|
value="date"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
className="w-full cursor-pointer capitalize"
|
||||||
|
htmlFor="date"
|
||||||
|
>
|
||||||
|
Date
|
||||||
|
</Label>
|
||||||
|
{currentSort == "date" ? (
|
||||||
|
currentDir == "desc" ? (
|
||||||
|
<FaSortAmountDown
|
||||||
|
className="size-5 cursor-pointer"
|
||||||
|
onClick={() => setCurrentDir("asc")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FaSortAmountUp
|
||||||
|
className="size-5 cursor-pointer"
|
||||||
|
onClick={() => setCurrentDir("desc")}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="size-5" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex items-center gap-2">
|
||||||
|
<RadioGroupItem
|
||||||
|
className={
|
||||||
|
currentSort == "score"
|
||||||
|
? "from-selected/50 to-selected/90 text-selected bg-selected"
|
||||||
|
: "from-secondary/50 to-secondary/90 text-secondary bg-secondary"
|
||||||
|
}
|
||||||
|
id="score"
|
||||||
|
value="score"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
className="w-full cursor-pointer capitalize"
|
||||||
|
htmlFor="score"
|
||||||
|
>
|
||||||
|
Score
|
||||||
|
</Label>
|
||||||
|
{currentSort == "score" ? (
|
||||||
|
currentDir == "desc" ? (
|
||||||
|
<FaSortAmountDown
|
||||||
|
className="size-5 cursor-pointer"
|
||||||
|
onClick={() => setCurrentDir("asc")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FaSortAmountUp
|
||||||
|
className="size-5 cursor-pointer"
|
||||||
|
onClick={() => setCurrentDir("desc")}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="size-5" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<div className="p-2 flex justify-evenly items-center">
|
||||||
|
<Button
|
||||||
|
variant="select"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedSort(`${currentSort}_${currentDir}`);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentSort(undefined);
|
||||||
|
setCurrentDir("desc");
|
||||||
|
setSelectedSort(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Content>
|
</Content>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
@ -366,7 +366,7 @@ export function RecordingView({
|
|||||||
key={mainCamera}
|
key={mainCamera}
|
||||||
className={
|
className={
|
||||||
isDesktop
|
isDesktop
|
||||||
? `${mainCameraAspect == "tall" ? "xl:h-[90%]" : mainCameraAspect == "wide" ? "w-full" : "w-[78%]"} px-4 flex justify-center`
|
? `${mainCameraAspect == "tall" ? "h-[50%] md:h-[60%] lg:h-[75%] xl:h-[90%]" : mainCameraAspect == "wide" ? "w-full" : "w-[78%]"} px-4 flex justify-center`
|
||||||
: `portrait:w-full pt-2 ${mainCameraAspect == "wide" ? "landscape:w-full aspect-wide" : "landscape:h-[94%] aspect-video"}`
|
: `portrait:w-full pt-2 ${mainCameraAspect == "wide" ? "landscape:w-full aspect-wide" : "landscape:h-[94%] aspect-video"}`
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
|
Loading…
Reference in New Issue
Block a user