mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Face tweaks (#17290)
* Ensure doesn't fail due to missing dir * Remove redundant settings from tabs * Adjust selection method for mobile * Fix button descendent error * Ensure train is option on mobile * Cleanup face images * Cleanup
This commit is contained in:
		
							parent
							
								
									060659044e
								
							
						
					
					
						commit
						08cf0def6e
					
				@ -30,6 +30,9 @@ router = APIRouter(tags=[Tags.events])
 | 
				
			|||||||
def get_faces():
 | 
					def get_faces():
 | 
				
			||||||
    face_dict: dict[str, list[str]] = {}
 | 
					    face_dict: dict[str, list[str]] = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not os.path.exists(FACE_DIR):
 | 
				
			||||||
 | 
					        return JSONResponse(status_code=200, content={})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for name in os.listdir(FACE_DIR):
 | 
					    for name in os.listdir(FACE_DIR):
 | 
				
			||||||
        face_dir = os.path.join(FACE_DIR, name)
 | 
					        face_dir = os.path.join(FACE_DIR, name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -506,6 +506,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
 | 
				
			|||||||
            if self.config.face_recognition.save_attempts:
 | 
					            if self.config.face_recognition.save_attempts:
 | 
				
			||||||
                # write face to library
 | 
					                # write face to library
 | 
				
			||||||
                folder = os.path.join(FACE_DIR, "train")
 | 
					                folder = os.path.join(FACE_DIR, "train")
 | 
				
			||||||
 | 
					                os.makedirs(folder, exist_ok=True)
 | 
				
			||||||
                new_file = os.path.join(
 | 
					                new_file = os.path.join(
 | 
				
			||||||
                    folder, f"{id}-{sub_label}-{score}-{face_score}.webp"
 | 
					                    folder, f"{id}-{sub_label}-{score}-{face_score}.webp"
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
				
			|||||||
@ -32,11 +32,11 @@ import { useFormattedTimestamp } from "@/hooks/use-date-utils";
 | 
				
			|||||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
 | 
					import useKeyboardListener from "@/hooks/use-keyboard-listener";
 | 
				
			||||||
import useOptimisticState from "@/hooks/use-optimistic-state";
 | 
					import useOptimisticState from "@/hooks/use-optimistic-state";
 | 
				
			||||||
import { cn } from "@/lib/utils";
 | 
					import { cn } from "@/lib/utils";
 | 
				
			||||||
import { RecognizedFaceData } from "@/types/face";
 | 
					import { FaceLibraryData, RecognizedFaceData } from "@/types/face";
 | 
				
			||||||
import { FrigateConfig } from "@/types/frigateConfig";
 | 
					import { FrigateConfig } from "@/types/frigateConfig";
 | 
				
			||||||
import axios from "axios";
 | 
					import axios from "axios";
 | 
				
			||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
 | 
					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 { useTranslation } from "react-i18next";
 | 
				
			||||||
import { LuImagePlus, LuRefreshCw, LuScanFace, LuTrash2 } from "react-icons/lu";
 | 
					import { LuImagePlus, LuRefreshCw, LuScanFace, LuTrash2 } from "react-icons/lu";
 | 
				
			||||||
import { toast } from "sonner";
 | 
					import { toast } from "sonner";
 | 
				
			||||||
@ -55,11 +55,11 @@ export default function FaceLibrary() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const [page, setPage] = useState<string>();
 | 
					  const [page, setPage] = useState<string>();
 | 
				
			||||||
  const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
 | 
					  const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
 | 
				
			||||||
  const tabsRef = useRef<HTMLDivElement | null>(null);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // face data
 | 
					  // face data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { data: faceData, mutate: refreshFaces } = useSWR("faces");
 | 
					  const { data: faceData, mutate: refreshFaces } =
 | 
				
			||||||
 | 
					    useSWR<FaceLibraryData>("faces");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const faces = useMemo<string[]>(
 | 
					  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">
 | 
					      <div className="relative mb-2 flex h-11 w-full items-center justify-between">
 | 
				
			||||||
        <ScrollArea className="w-full whitespace-nowrap">
 | 
					        <LibrarySelector
 | 
				
			||||||
          <div ref={tabsRef} className="flex flex-row">
 | 
					          pageToggle={pageToggle}
 | 
				
			||||||
            <ToggleGroup
 | 
					          faceData={faceData}
 | 
				
			||||||
              className="*:rounded-md *:px-3 *:py-4"
 | 
					          faces={faces}
 | 
				
			||||||
              type="single"
 | 
					          trainImages={trainImages}
 | 
				
			||||||
              size="sm"
 | 
					          setPageToggle={setPageToggle}
 | 
				
			||||||
              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>
 | 
					 | 
				
			||||||
        {selectedFaces?.length > 0 ? (
 | 
					        {selectedFaces?.length > 0 ? (
 | 
				
			||||||
          <div className="flex items-center justify-center gap-2">
 | 
					          <div className="flex items-center justify-center gap-2">
 | 
				
			||||||
            <Button
 | 
					            <Button
 | 
				
			||||||
@ -323,6 +286,95 @@ 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>
 | 
				
			||||||
 | 
					        {trainImages.length > 0 && (
 | 
				
			||||||
 | 
					          <DropdownMenuItem
 | 
				
			||||||
 | 
					            className="capitalize"
 | 
				
			||||||
 | 
					            aria-label={t("train.aria")}
 | 
				
			||||||
 | 
					            onClick={() => setPageToggle("train")}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <div>{t("train.title")}</div>
 | 
				
			||||||
 | 
					          </DropdownMenuItem>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {Object.values(faces).map((face) => (
 | 
				
			||||||
 | 
					          <DropdownMenuItem
 | 
				
			||||||
 | 
					            className="capitalize"
 | 
				
			||||||
 | 
					            onClick={() => setPageToggle(face)}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {face} ({faceData?.[face].length})
 | 
				
			||||||
 | 
					          </DropdownMenuItem>
 | 
				
			||||||
 | 
					        ))}
 | 
				
			||||||
 | 
					      </DropdownMenuContent>
 | 
				
			||||||
 | 
					    </DropdownMenu>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type TrainingGridProps = {
 | 
					type TrainingGridProps = {
 | 
				
			||||||
  config: FrigateConfig;
 | 
					  config: FrigateConfig;
 | 
				
			||||||
  attemptImages: string[];
 | 
					  attemptImages: string[];
 | 
				
			||||||
@ -536,7 +588,7 @@ function FaceAttempt({
 | 
				
			|||||||
          <div className="flex flex-row items-start justify-end gap-5 md:gap-4">
 | 
					          <div className="flex flex-row items-start justify-end gap-5 md:gap-4">
 | 
				
			||||||
            <Tooltip>
 | 
					            <Tooltip>
 | 
				
			||||||
              <DropdownMenu>
 | 
					              <DropdownMenu>
 | 
				
			||||||
                <DropdownMenuTrigger>
 | 
					                <DropdownMenuTrigger asChild>
 | 
				
			||||||
                  <TooltipTrigger>
 | 
					                  <TooltipTrigger>
 | 
				
			||||||
                    <AddFaceIcon className="size-5 cursor-pointer text-primary-variant hover:text-primary" />
 | 
					                    <AddFaceIcon className="size-5 cursor-pointer text-primary-variant hover:text-primary" />
 | 
				
			||||||
                  </TooltipTrigger>
 | 
					                  </TooltipTrigger>
 | 
				
			||||||
@ -579,7 +631,12 @@ type FaceGridProps = {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
function FaceGrid({ faceImages, pageToggle, onDelete }: FaceGridProps) {
 | 
					function FaceGrid({ faceImages, pageToggle, onDelete }: FaceGridProps) {
 | 
				
			||||||
  return (
 | 
					  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) => (
 | 
					      {faceImages.map((image: string) => (
 | 
				
			||||||
        <FaceImage
 | 
					        <FaceImage
 | 
				
			||||||
          key={image}
 | 
					          key={image}
 | 
				
			||||||
@ -602,7 +659,12 @@ function FaceImage({ name, image, onDelete }: FaceImageProps) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="relative flex flex-col rounded-lg">
 | 
					    <div className="relative flex flex-col rounded-lg">
 | 
				
			||||||
      <div className="w-full overflow-hidden rounded-t-lg border border-t-0 *:text-card-foreground">
 | 
					      <div
 | 
				
			||||||
 | 
					        className={cn(
 | 
				
			||||||
 | 
					          "w-full overflow-hidden rounded-t-lg border border-t-0 *:text-card-foreground",
 | 
				
			||||||
 | 
					          isMobile && "flex justify-center",
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
        <img className="h-40" src={`${baseUrl}clips/faces/${name}/${image}`} />
 | 
					        <img className="h-40" src={`${baseUrl}clips/faces/${name}/${image}`} />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div className="rounded-b-lg bg-card p-2">
 | 
					      <div className="rounded-b-lg bg-card p-2">
 | 
				
			||||||
 | 
				
			|||||||
@ -47,9 +47,9 @@ import { useIsAdmin } from "@/hooks/use-is-admin";
 | 
				
			|||||||
import { useTranslation } from "react-i18next";
 | 
					import { useTranslation } from "react-i18next";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const allSettingsViews = [
 | 
					const allSettingsViews = [
 | 
				
			||||||
  "uiSettings",
 | 
					  "ui",
 | 
				
			||||||
  "classificationSettings",
 | 
					  "classification",
 | 
				
			||||||
  "cameraSettings",
 | 
					  "cameras",
 | 
				
			||||||
  "masksAndZones",
 | 
					  "masksAndZones",
 | 
				
			||||||
  "motionTuner",
 | 
					  "motionTuner",
 | 
				
			||||||
  "debug",
 | 
					  "debug",
 | 
				
			||||||
@ -61,7 +61,7 @@ type SettingsType = (typeof allSettingsViews)[number];
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export default function Settings() {
 | 
					export default function Settings() {
 | 
				
			||||||
  const { t } = useTranslation(["views/settings"]);
 | 
					  const { t } = useTranslation(["views/settings"]);
 | 
				
			||||||
  const [page, setPage] = useState<SettingsType>("uiSettings");
 | 
					  const [page, setPage] = useState<SettingsType>("ui");
 | 
				
			||||||
  const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
 | 
					  const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
 | 
				
			||||||
  const tabsRef = useRef<HTMLDivElement | null>(null);
 | 
					  const tabsRef = useRef<HTMLDivElement | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -73,7 +73,7 @@ export default function Settings() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const isAdmin = useIsAdmin();
 | 
					  const isAdmin = useIsAdmin();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const allowedViewsForViewer: SettingsType[] = ["uiSettings", "debug"];
 | 
					  const allowedViewsForViewer: SettingsType[] = ["ui", "debug"];
 | 
				
			||||||
  const visibleSettingsViews = !isAdmin
 | 
					  const visibleSettingsViews = !isAdmin
 | 
				
			||||||
    ? allowedViewsForViewer
 | 
					    ? allowedViewsForViewer
 | 
				
			||||||
    : allSettingsViews;
 | 
					    : allSettingsViews;
 | 
				
			||||||
@ -135,10 +135,7 @@ export default function Settings() {
 | 
				
			|||||||
        const firstEnabledCamera =
 | 
					        const firstEnabledCamera =
 | 
				
			||||||
          cameras.find((cam) => cameraEnabledStates[cam.name]) || cameras[0];
 | 
					          cameras.find((cam) => cameraEnabledStates[cam.name]) || cameras[0];
 | 
				
			||||||
        setSelectedCamera(firstEnabledCamera.name);
 | 
					        setSelectedCamera(firstEnabledCamera.name);
 | 
				
			||||||
      } else if (
 | 
					      } else if (!cameraEnabledStates[selectedCamera] && page !== "cameras") {
 | 
				
			||||||
        !cameraEnabledStates[selectedCamera] &&
 | 
					 | 
				
			||||||
        page !== "cameraSettings"
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        // Switch to first enabled camera if current one is disabled, unless on "camera settings" page
 | 
					        // Switch to first enabled camera if current one is disabled, unless on "camera settings" page
 | 
				
			||||||
        const firstEnabledCamera =
 | 
					        const firstEnabledCamera =
 | 
				
			||||||
          cameras.find((cam) => cameraEnabledStates[cam.name]) || cameras[0];
 | 
					          cameras.find((cam) => cameraEnabledStates[cam.name]) || cameras[0];
 | 
				
			||||||
@ -167,8 +164,8 @@ export default function Settings() {
 | 
				
			|||||||
  useSearchEffect("page", (page: string) => {
 | 
					  useSearchEffect("page", (page: string) => {
 | 
				
			||||||
    if (allSettingsViews.includes(page as SettingsType)) {
 | 
					    if (allSettingsViews.includes(page as SettingsType)) {
 | 
				
			||||||
      // Restrict viewer to UI settings
 | 
					      // Restrict viewer to UI settings
 | 
				
			||||||
      if (!isAdmin && !["uiSettings", "debug"].includes(page)) {
 | 
					      if (!isAdmin && !["ui", "debug"].includes(page)) {
 | 
				
			||||||
        setPage("uiSettings");
 | 
					        setPage("ui");
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        setPage(page as SettingsType);
 | 
					        setPage(page as SettingsType);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -203,8 +200,8 @@ export default function Settings() {
 | 
				
			|||||||
              onValueChange={(value: SettingsType) => {
 | 
					              onValueChange={(value: SettingsType) => {
 | 
				
			||||||
                if (value) {
 | 
					                if (value) {
 | 
				
			||||||
                  // Restrict viewer navigation
 | 
					                  // Restrict viewer navigation
 | 
				
			||||||
                  if (!isAdmin && !["uiSettings", "debug"].includes(value)) {
 | 
					                  if (!isAdmin && !["ui", "debug"].includes(value)) {
 | 
				
			||||||
                    setPageToggle("uiSettings");
 | 
					                    setPageToggle("ui");
 | 
				
			||||||
                  } else {
 | 
					                  } else {
 | 
				
			||||||
                    setPageToggle(value);
 | 
					                    setPageToggle(value);
 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
@ -214,7 +211,7 @@ export default function Settings() {
 | 
				
			|||||||
              {visibleSettingsViews.map((item) => (
 | 
					              {visibleSettingsViews.map((item) => (
 | 
				
			||||||
                <ToggleGroupItem
 | 
					                <ToggleGroupItem
 | 
				
			||||||
                  key={item}
 | 
					                  key={item}
 | 
				
			||||||
                  className={`flex scroll-mx-10 items-center justify-between gap-2 ${page == "uiSettings" ? "last:mr-20" : ""} ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
 | 
					                  className={`flex scroll-mx-10 items-center justify-between gap-2 ${page == "ui" ? "last:mr-20" : ""} ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
 | 
				
			||||||
                  value={item}
 | 
					                  value={item}
 | 
				
			||||||
                  data-nav-item={item}
 | 
					                  data-nav-item={item}
 | 
				
			||||||
                  aria-label={t("selectItem", {
 | 
					                  aria-label={t("selectItem", {
 | 
				
			||||||
@ -230,7 +227,7 @@ export default function Settings() {
 | 
				
			|||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </ScrollArea>
 | 
					        </ScrollArea>
 | 
				
			||||||
        {(page == "debug" ||
 | 
					        {(page == "debug" ||
 | 
				
			||||||
          page == "cameraSettings" ||
 | 
					          page == "cameras" ||
 | 
				
			||||||
          page == "masksAndZones" ||
 | 
					          page == "masksAndZones" ||
 | 
				
			||||||
          page == "motionTuner") && (
 | 
					          page == "motionTuner") && (
 | 
				
			||||||
          <div className="ml-2 flex flex-shrink-0 items-center gap-2">
 | 
					          <div className="ml-2 flex flex-shrink-0 items-center gap-2">
 | 
				
			||||||
@ -251,14 +248,14 @@ export default function Settings() {
 | 
				
			|||||||
        )}
 | 
					        )}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div className="mt-2 flex h-full w-full flex-col items-start md:h-dvh md:pb-24">
 | 
					      <div className="mt-2 flex h-full w-full flex-col items-start md:h-dvh md:pb-24">
 | 
				
			||||||
        {page == "uiSettings" && <UiSettingsView />}
 | 
					        {page == "ui" && <UiSettingsView />}
 | 
				
			||||||
        {page == "classificationSettings" && (
 | 
					        {page == "classification" && (
 | 
				
			||||||
          <ClassificationSettingsView setUnsavedChanges={setUnsavedChanges} />
 | 
					          <ClassificationSettingsView setUnsavedChanges={setUnsavedChanges} />
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
        {page == "debug" && (
 | 
					        {page == "debug" && (
 | 
				
			||||||
          <ObjectSettingsView selectedCamera={selectedCamera} />
 | 
					          <ObjectSettingsView selectedCamera={selectedCamera} />
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
        {page == "cameraSettings" && (
 | 
					        {page == "cameras" && (
 | 
				
			||||||
          <CameraSettingsView
 | 
					          <CameraSettingsView
 | 
				
			||||||
            selectedCamera={selectedCamera}
 | 
					            selectedCamera={selectedCamera}
 | 
				
			||||||
            setUnsavedChanges={setUnsavedChanges}
 | 
					            setUnsavedChanges={setUnsavedChanges}
 | 
				
			||||||
@ -363,7 +360,7 @@ function CameraSelectButton({
 | 
				
			|||||||
        <div className="flex flex-col gap-2.5">
 | 
					        <div className="flex flex-col gap-2.5">
 | 
				
			||||||
          {allCameras.map((item) => {
 | 
					          {allCameras.map((item) => {
 | 
				
			||||||
            const isEnabled = cameraEnabledStates[item.name];
 | 
					            const isEnabled = cameraEnabledStates[item.name];
 | 
				
			||||||
            const isCameraSettingsPage = currentPage === "cameraSettings";
 | 
					            const isCameraSettingsPage = currentPage === "cameras";
 | 
				
			||||||
            return (
 | 
					            return (
 | 
				
			||||||
              <FilterSwitch
 | 
					              <FilterSwitch
 | 
				
			||||||
                key={item.name}
 | 
					                key={item.name}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,7 @@
 | 
				
			|||||||
 | 
					export type FaceLibraryData = {
 | 
				
			||||||
 | 
					  [faceName: string]: string[];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type RecognizedFaceData = {
 | 
					export type RecognizedFaceData = {
 | 
				
			||||||
  timestamp: number;
 | 
					  timestamp: number;
 | 
				
			||||||
  eventId: string;
 | 
					  eventId: string;
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user