Adjust selection method for mobile

This commit is contained in:
Nicolas Mowen 2025-03-21 08:14:28 -06:00
parent d6ac368a71
commit 9712c1f34a
2 changed files with 105 additions and 50 deletions

View File

@ -32,11 +32,11 @@ import { useFormattedTimestamp } from "@/hooks/use-date-utils";
import useKeyboardListener from "@/hooks/use-keyboard-listener";
import useOptimisticState from "@/hooks/use-optimistic-state";
import { cn } from "@/lib/utils";
import { RecognizedFaceData } from "@/types/face";
import { FaceLibraryData, RecognizedFaceData } from "@/types/face";
import { FrigateConfig } from "@/types/frigateConfig";
import axios from "axios";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { isDesktop } from "react-device-detect";
import { isDesktop, isMobile } from "react-device-detect";
import { useTranslation } from "react-i18next";
import { LuImagePlus, LuRefreshCw, LuScanFace, LuTrash2 } from "react-icons/lu";
import { toast } from "sonner";
@ -55,11 +55,11 @@ export default function FaceLibrary() {
const [page, setPage] = useState<string>();
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
const tabsRef = useRef<HTMLDivElement | null>(null);
// face data
const { data: faceData, mutate: refreshFaces } = useSWR("faces");
const { data: faceData, mutate: refreshFaces } =
useSWR<FaceLibraryData>("faces");
const faces = useMemo<string[]>(
() =>
@ -233,50 +233,13 @@ export default function FaceLibrary() {
/>
<div className="relative mb-2 flex h-11 w-full items-center justify-between">
<ScrollArea className="w-full whitespace-nowrap">
<div ref={tabsRef} className="flex flex-row">
<ToggleGroup
className="*:rounded-md *:px-3 *:py-4"
type="single"
size="sm"
value={pageToggle}
onValueChange={(value: string) => {
if (value) {
setPageToggle(value);
}
}}
>
{trainImages.length > 0 && (
<>
<ToggleGroupItem
value="train"
className={`flex scroll-mx-10 items-center justify-between gap-2 ${pageToggle == "train" ? "" : "*:text-muted-foreground"}`}
data-nav-item="train"
aria-label={t("train.aria")}
>
<div>{t("train.title")}</div>
</ToggleGroupItem>
<div>|</div>
</>
)}
{Object.values(faces).map((item) => (
<ToggleGroupItem
key={item}
className={`flex scroll-mx-10 items-center justify-between gap-2 ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
value={item}
data-nav-item={item}
aria-label={t("selectItem", { item })}
>
<div className="capitalize">
{item} ({faceData[item].length})
</div>
</ToggleGroupItem>
))}
</ToggleGroup>
<ScrollBar orientation="horizontal" className="h-0" />
</div>
</ScrollArea>
<LibrarySelector
pageToggle={pageToggle}
faceData={faceData}
faces={faces}
trainImages={trainImages}
setPageToggle={setPageToggle}
/>
{selectedFaces?.length > 0 ? (
<div className="flex items-center justify-center gap-2">
<Button
@ -323,6 +286,86 @@ export default function FaceLibrary() {
);
}
type LibrarySelectorProps = {
pageToggle: string | undefined;
faceData?: FaceLibraryData;
faces: string[];
trainImages: string[];
setPageToggle: (toggle: string | undefined) => void;
};
function LibrarySelector({
pageToggle,
faceData,
faces,
trainImages,
setPageToggle,
}: LibrarySelectorProps) {
const { t } = useTranslation(["views/faceLibrary"]);
return isDesktop ? (
<ScrollArea className="w-full whitespace-nowrap">
<div className="flex flex-row">
<ToggleGroup
className="*:rounded-md *:px-3 *:py-4"
type="single"
size="sm"
value={pageToggle}
onValueChange={(value: string) => {
if (value) {
setPageToggle(value);
}
}}
>
{trainImages.length > 0 && (
<>
<ToggleGroupItem
value="train"
className={`flex scroll-mx-10 items-center justify-between gap-2 ${pageToggle == "train" ? "" : "*:text-muted-foreground"}`}
data-nav-item="train"
aria-label={t("train.aria")}
>
<div>{t("train.title")}</div>
</ToggleGroupItem>
<div>|</div>
</>
)}
{Object.values(faces).map((face) => (
<ToggleGroupItem
key={face}
className={`flex scroll-mx-10 items-center justify-between gap-2 ${pageToggle == face ? "" : "*:text-muted-foreground"}`}
value={face}
data-nav-item={face}
aria-label={t("selectItem", { item: face })}
>
<div className="capitalize">
{face} ({faceData?.[face].length})
</div>
</ToggleGroupItem>
))}
</ToggleGroup>
<ScrollBar orientation="horizontal" className="h-0" />
</div>
</ScrollArea>
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="capitalize">{pageToggle}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{Object.values(faces).map((face) => (
<DropdownMenuItem
className="capitalize"
onClick={() => setPageToggle(face)}
>
{face} ({faceData?.[face].length})
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
type TrainingGridProps = {
config: FrigateConfig;
attemptImages: string[];
@ -579,7 +622,12 @@ type FaceGridProps = {
};
function FaceGrid({ faceImages, pageToggle, onDelete }: FaceGridProps) {
return (
<div className="scrollbar-container flex flex-wrap gap-2 overflow-y-scroll">
<div
className={cn(
"scrollbar-container gap-2 overflow-y-scroll",
isDesktop ? "flex flex-wrap" : "grid grid-cols-2",
)}
>
{faceImages.map((image: string) => (
<FaceImage
key={image}
@ -603,7 +651,10 @@ function FaceImage({ name, image, onDelete }: FaceImageProps) {
return (
<div className="relative flex flex-col rounded-lg">
<div className="w-full overflow-hidden rounded-t-lg border border-t-0 *:text-card-foreground">
<img className="h-40" src={`${baseUrl}clips/faces/${name}/${image}`} />
<img
className={cn("h-40", isMobile && "w-full")}
src={`${baseUrl}clips/faces/${name}/${image}`}
/>
</div>
<div className="rounded-b-lg bg-card p-2">
<div className="flex w-full flex-row items-center justify-between gap-2">

View File

@ -1,3 +1,7 @@
export type FaceLibraryData = {
[faceName: string]: string[];
};
export type RecognizedFaceData = {
timestamp: number;
eventId: string;