mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-03-22 00:17:13 +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)
|
||||
max_length = request.args.get("max_length", type=float)
|
||||
|
||||
sort = request.args.get("sort", type=str)
|
||||
|
||||
clauses = []
|
||||
|
||||
selected_columns = [
|
||||
@ -219,10 +221,22 @@ def events():
|
||||
if len(clauses) == 0:
|
||||
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 = (
|
||||
Event.select(*selected_columns)
|
||||
.where(reduce(operator.and_, clauses))
|
||||
.order_by(Event.start_time.desc())
|
||||
.order_by(order_by)
|
||||
.limit(limit)
|
||||
.dicts()
|
||||
.iterator()
|
||||
|
@ -209,7 +209,7 @@ type CameraFilterButtonProps = {
|
||||
selectedCameras: string[] | undefined;
|
||||
updateCameraFilter: (cameras: string[] | undefined) => void;
|
||||
};
|
||||
function CamerasFilterButton({
|
||||
export function CamerasFilterButton({
|
||||
allCameras,
|
||||
groups,
|
||||
selectedCameras,
|
||||
@ -227,7 +227,7 @@ function CamerasFilterButton({
|
||||
size="sm"
|
||||
>
|
||||
<FaVideo
|
||||
className={`${selectedCameras?.length == 1 ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||
className={`${(selectedCameras?.length ?? 0) >= 1 ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||
/>
|
||||
<div
|
||||
className={`hidden md:block ${selectedCameras?.length ? "text-selected-foreground" : "text-primary"}`}
|
||||
|
@ -8,7 +8,6 @@ import React, {
|
||||
import { useApiHost } from "@/api";
|
||||
import { isCurrentHour } from "@/utils/dateUtil";
|
||||
import { ReviewSegment } from "@/types/review";
|
||||
import { Slider } from "../ui/slider-no-thumb";
|
||||
import { getIconForLabel } from "@/utils/iconUtil";
|
||||
import TimeAgo from "../dynamic/TimeAgo";
|
||||
import useSWR from "swr";
|
||||
@ -23,6 +22,7 @@ import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator";
|
||||
import useContextMenu from "@/hooks/use-contextmenu";
|
||||
import ActivityIndicator from "../indicators/activity-indicator";
|
||||
import { TimeRange } from "@/types/timeline";
|
||||
import { NoThumbSlider } from "../ui/slider";
|
||||
|
||||
type PreviewPlayerProps = {
|
||||
review: ReviewSegment;
|
||||
@ -543,7 +543,7 @@ function VideoPreview({
|
||||
>
|
||||
<source src={relevantPreview.src} type={relevantPreview.type} />
|
||||
</video>
|
||||
<Slider
|
||||
<NoThumbSlider
|
||||
ref={sliderRef}
|
||||
className="absolute inset-x-0 bottom-0 z-30"
|
||||
value={[progress]}
|
||||
@ -707,7 +707,7 @@ function InProgressPreview({
|
||||
src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.webp`}
|
||||
onLoad={handleLoad}
|
||||
/>
|
||||
<Slider
|
||||
<NoThumbSlider
|
||||
ref={sliderRef}
|
||||
className="absolute inset-x-0 bottom-0 z-30"
|
||||
value={[key]}
|
||||
|
@ -16,8 +16,8 @@ import {
|
||||
MdVolumeOff,
|
||||
MdVolumeUp,
|
||||
} from "react-icons/md";
|
||||
import { Slider } from "../ui/slider-volume";
|
||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||
import { VolumeSlider } from "../ui/slider";
|
||||
|
||||
type VideoControls = {
|
||||
volume?: boolean;
|
||||
@ -154,7 +154,7 @@ export default function VideoControls({
|
||||
}}
|
||||
/>
|
||||
{video.muted == false && (
|
||||
<Slider
|
||||
<VolumeSlider
|
||||
className="w-20"
|
||||
value={[video.volume]}
|
||||
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 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,7 +20,68 @@ 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;
|
||||
|
||||
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 FilterCheckBox from "@/components/filter/FilterCheckBox";
|
||||
import {
|
||||
CamerasFilterButton,
|
||||
GeneralFilterContent,
|
||||
} from "@/components/filter/ReviewFilterGroup";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@ -13,16 +16,25 @@ 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 { 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 { FrigateConfig } from "@/types/frigateConfig";
|
||||
import axios from "axios";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
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";
|
||||
|
||||
export default function SubmitPlus() {
|
||||
@ -36,6 +48,11 @@ export default function SubmitPlus() {
|
||||
|
||||
const [selectedCameras, setSelectedCameras] = useState<string[]>();
|
||||
const [selectedLabels, setSelectedLabels] = useState<string[]>();
|
||||
const [scoreRange, setScoreRange] = useState<number[]>();
|
||||
|
||||
// sort
|
||||
|
||||
const [sort, setSort] = useState<string>();
|
||||
|
||||
// data
|
||||
|
||||
@ -47,6 +64,9 @@ 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,
|
||||
sort: sort ? sort : null,
|
||||
},
|
||||
]);
|
||||
const [upload, setUpload] = useState<Event>();
|
||||
@ -104,12 +124,17 @@ export default function SubmitPlus() {
|
||||
|
||||
return (
|
||||
<div className="size-full flex flex-col">
|
||||
<PlusFilterGroup
|
||||
selectedCameras={selectedCameras}
|
||||
setSelectedCameras={setSelectedCameras}
|
||||
selectedLabels={selectedLabels}
|
||||
setSelectedLabels={setSelectedLabels}
|
||||
/>
|
||||
<div className="w-full h-16 px-2 flex items-center justify-between overflow-x-auto">
|
||||
<PlusFilterGroup
|
||||
selectedCameras={selectedCameras}
|
||||
selectedLabels={selectedLabels}
|
||||
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="w-full p-2 grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2">
|
||||
<Dialog
|
||||
@ -178,15 +203,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");
|
||||
|
||||
@ -217,97 +246,28 @@ function PlusFilterGroup({
|
||||
return [...labels].sort();
|
||||
}, [config, selectedCameras]);
|
||||
|
||||
const [open, setOpen] = useState<"none" | "camera" | "label">("none");
|
||||
const [currentCameras, setCurrentCameras] = useState<string[] | undefined>(
|
||||
undefined,
|
||||
const [open, setOpen] = useState<"none" | "camera" | "label" | "score">(
|
||||
"none",
|
||||
);
|
||||
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [currentScoreRange, setCurrentScoreRange] = useState<
|
||||
number[] | undefined
|
||||
>(undefined);
|
||||
|
||||
const Menu = isMobile ? Drawer : DropdownMenu;
|
||||
const Trigger = isMobile ? DrawerTrigger : DropdownMenuTrigger;
|
||||
const Content = isMobile ? DrawerContent : DropdownMenuContent;
|
||||
|
||||
return (
|
||||
<div className="w-full h-16 flex justify-start gap-2 items-center">
|
||||
<Menu
|
||||
open={open == "camera"}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setCurrentCameras(selectedCameras);
|
||||
}
|
||||
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>
|
||||
<div className="h-full flex justify-start gap-2 items-center">
|
||||
<CamerasFilterButton
|
||||
allCameras={allCameras}
|
||||
groups={[]}
|
||||
selectedCameras={selectedCameras}
|
||||
updateCameraFilter={setSelectedCameras}
|
||||
/>
|
||||
<Menu
|
||||
open={open == "label"}
|
||||
onOpenChange={(open) => {
|
||||
@ -318,8 +278,14 @@ function PlusFilterGroup({
|
||||
}}
|
||||
>
|
||||
<Trigger asChild>
|
||||
<Button size="sm" className="mx-1 capitalize">
|
||||
<FaList className="md:mr-[10px] text-secondary-foreground" />
|
||||
<Button
|
||||
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">
|
||||
{selectedLabels == undefined
|
||||
? "All Labels"
|
||||
@ -328,60 +294,250 @@ function PlusFilterGroup({
|
||||
</Button>
|
||||
</Trigger>
|
||||
<Content className={isMobile ? "max-h-[75dvh]" : ""}>
|
||||
<DropdownMenuLabel className="flex justify-center">
|
||||
Filter Labels
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<FilterCheckBox
|
||||
isChecked={currentLabels == undefined}
|
||||
label="All Labels"
|
||||
onCheckedChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
setCurrentLabels(undefined);
|
||||
}
|
||||
}}
|
||||
<GeneralFilterContent
|
||||
allLabels={allLabels}
|
||||
selectedLabels={selectedLabels}
|
||||
currentLabels={currentLabels}
|
||||
setCurrentLabels={setCurrentLabels}
|
||||
updateLabelFilter={setSelectedLabels}
|
||||
onClose={() => setOpen("none")}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<div className={isMobile ? "h-auto overflow-y-auto" : ""}>
|
||||
{allLabels.map((item) => (
|
||||
<FilterCheckBox
|
||||
key={item}
|
||||
isChecked={currentLabels?.includes(item) ?? false}
|
||||
label={item.replaceAll("_", " ")}
|
||||
onCheckedChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
const updatedLabels = currentLabels
|
||||
? [...currentLabels]
|
||||
: [];
|
||||
|
||||
updatedLabels.push(item);
|
||||
setCurrentLabels(updatedLabels);
|
||||
} else {
|
||||
const updatedLabels = currentLabels
|
||||
? [...currentLabels]
|
||||
: [];
|
||||
|
||||
// can not deselect the last item
|
||||
if (updatedLabels.length > 1) {
|
||||
updatedLabels.splice(updatedLabels.indexOf(item), 1);
|
||||
setCurrentLabels(updatedLabels);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Content>
|
||||
</Menu>
|
||||
<Menu
|
||||
open={open == "score"}
|
||||
onOpenChange={(open) => {
|
||||
setOpen(open ? "score" : "none");
|
||||
}}
|
||||
>
|
||||
<Trigger asChild>
|
||||
<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">
|
||||
{selectedScoreRange == undefined
|
||||
? "Score Range"
|
||||
: `${selectedScoreRange[0] * 100}% - ${selectedScoreRange[1] * 100}%`}
|
||||
</div>
|
||||
</Button>
|
||||
</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>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex justify-center items-center">
|
||||
<div className="p-2 flex justify-evenly items-center">
|
||||
<Button
|
||||
variant="select"
|
||||
onClick={() => {
|
||||
setSelectedLabels(currentLabels);
|
||||
setSelectedScoreRange(currentScoreRange);
|
||||
setOpen("none");
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</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>
|
||||
</Content>
|
||||
</Menu>
|
||||
|
@ -366,7 +366,7 @@ export function RecordingView({
|
||||
key={mainCamera}
|
||||
className={
|
||||
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"}`
|
||||
}
|
||||
style={{
|
||||
|
Loading…
Reference in New Issue
Block a user