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:
Nicolas Mowen 2024-04-14 10:06:11 -06:00 committed by GitHub
parent b65656fa87
commit a3e2171675
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 384 additions and 205 deletions

View File

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

View File

@ -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"}`}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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