mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 13:34:13 +03:00
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* feat: add zones friendly name * fix: fix the issue where the input field was empty when there was no friendly_name * chore: fix the issue where the friendly name would replace spaces with underscores * docs: update zones docs * Update web/src/components/settings/ZoneEditPane.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Add friendly_name option for zone configuration Added optional friendly name for zones in configuration. * fix: fix the logical error in the null/empty check for the polygons parameter * fix: remove the toast name for zones will use the friendly_name instead * docs: remove emoji tips * revert: revert zones doc ui tips * Update docs/docs/configuration/zones.md Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update docs/docs/configuration/zones.md Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update docs/docs/configuration/zones.md Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * feat: add friendly zone names to tracking details and lifecycle item descriptions * chore: lint fix * refactor: add friendly zone names to timeline entries and clean up unused code * refactor: add formatList --------- Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
205 lines
7.0 KiB
TypeScript
205 lines
7.0 KiB
TypeScript
import Heading from "@/components/ui/heading";
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { Toaster } from "sonner";
|
|
import { Button } from "@/components/ui/button";
|
|
import useSWR from "swr";
|
|
import { FrigateConfig } from "@/types/frigateConfig";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Label } from "@/components/ui/label";
|
|
import CameraEditForm from "@/components/settings/CameraEditForm";
|
|
import CameraWizardDialog from "@/components/settings/CameraWizardDialog";
|
|
import { LuPlus } from "react-icons/lu";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { IoMdArrowRoundBack } from "react-icons/io";
|
|
import { isDesktop } from "react-device-detect";
|
|
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Trans } from "react-i18next";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { useEnabledState } from "@/api/ws";
|
|
|
|
type CameraManagementViewProps = {
|
|
setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>;
|
|
};
|
|
|
|
export default function CameraManagementView({
|
|
setUnsavedChanges,
|
|
}: CameraManagementViewProps) {
|
|
const { t } = useTranslation(["views/settings"]);
|
|
|
|
const { data: config, mutate: updateConfig } =
|
|
useSWR<FrigateConfig>("config");
|
|
|
|
const [viewMode, setViewMode] = useState<"settings" | "add" | "edit">(
|
|
"settings",
|
|
); // Control view state
|
|
const [editCameraName, setEditCameraName] = useState<string | undefined>(
|
|
undefined,
|
|
); // Track camera being edited
|
|
const [showWizard, setShowWizard] = useState(false);
|
|
|
|
// List of cameras for dropdown
|
|
const cameras = useMemo(() => {
|
|
if (config) {
|
|
return Object.keys(config.cameras).sort();
|
|
}
|
|
return [];
|
|
}, [config]);
|
|
|
|
useEffect(() => {
|
|
document.title = t("documentTitle.cameraManagement");
|
|
}, [t]);
|
|
|
|
// Handle back navigation from add/edit form
|
|
const handleBack = useCallback(() => {
|
|
setViewMode("settings");
|
|
setEditCameraName(undefined);
|
|
setUnsavedChanges(false);
|
|
updateConfig();
|
|
}, [updateConfig, setUnsavedChanges]);
|
|
|
|
return (
|
|
<>
|
|
<Toaster
|
|
richColors
|
|
className="z-[1000]"
|
|
position="top-center"
|
|
closeButton
|
|
/>
|
|
<div className="flex size-full flex-col md:flex-row">
|
|
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
|
|
{viewMode === "settings" ? (
|
|
<>
|
|
<Heading as="h4" className="mb-2">
|
|
{t("cameraManagement.title")}
|
|
</Heading>
|
|
<div className="my-4 flex flex-col gap-4">
|
|
<Button
|
|
variant="select"
|
|
onClick={() => setShowWizard(true)}
|
|
className="flex max-w-48 items-center gap-2"
|
|
>
|
|
<LuPlus className="h-4 w-4" />
|
|
{t("cameraManagement.addCamera")}
|
|
</Button>
|
|
{cameras.length > 0 && (
|
|
<>
|
|
<div className="my-4 flex flex-col gap-2">
|
|
<Label>{t("cameraManagement.editCamera")}</Label>
|
|
<Select
|
|
onValueChange={(value) => {
|
|
setEditCameraName(value);
|
|
setViewMode("edit");
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-[180px]">
|
|
<SelectValue
|
|
placeholder={t("cameraManagement.selectCamera")}
|
|
/>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{cameras.map((camera) => {
|
|
return (
|
|
<SelectItem key={camera} value={camera}>
|
|
<CameraNameLabel camera={camera} />
|
|
</SelectItem>
|
|
);
|
|
})}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<Separator className="my-2 flex bg-secondary" />
|
|
<div className="max-w-7xl space-y-4">
|
|
<Heading as="h4" className="my-2">
|
|
<Trans ns="views/settings">
|
|
cameraManagement.streams.title
|
|
</Trans>
|
|
</Heading>
|
|
<div className="mt-3 text-sm text-muted-foreground">
|
|
<Trans ns="views/settings">
|
|
cameraManagement.streams.desc
|
|
</Trans>
|
|
</div>
|
|
|
|
<div className="max-w-md space-y-2 rounded-lg bg-secondary p-4">
|
|
{cameras.map((camera) => (
|
|
<div
|
|
key={camera}
|
|
className="flex items-center justify-between smart-capitalize"
|
|
>
|
|
<CameraNameLabel camera={camera} />
|
|
<CameraEnableSwitch cameraName={camera} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<Separator className="mb-2 mt-4 flex bg-secondary" />
|
|
</>
|
|
)}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<>
|
|
<div className="mb-4 flex items-center gap-2">
|
|
<Button
|
|
className={`flex items-center gap-2.5 rounded-lg`}
|
|
aria-label={t("label.back", { ns: "common" })}
|
|
size="sm"
|
|
onClick={handleBack}
|
|
>
|
|
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
|
|
{isDesktop && (
|
|
<div className="text-primary">
|
|
{t("button.back", { ns: "common" })}
|
|
</div>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
<div className="md:max-w-5xl">
|
|
<CameraEditForm
|
|
cameraName={viewMode === "edit" ? editCameraName : undefined}
|
|
onSave={handleBack}
|
|
onCancel={handleBack}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<CameraWizardDialog
|
|
open={showWizard}
|
|
onClose={() => setShowWizard(false)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
type CameraEnableSwitchProps = {
|
|
cameraName: string;
|
|
};
|
|
|
|
function CameraEnableSwitch({ cameraName }: CameraEnableSwitchProps) {
|
|
const { payload: enabledState, send: sendEnabled } =
|
|
useEnabledState(cameraName);
|
|
|
|
return (
|
|
<div className="flex flex-row items-center">
|
|
<Switch
|
|
id={`camera-enabled-${cameraName}`}
|
|
checked={enabledState === "ON"}
|
|
onCheckedChange={(isChecked) => {
|
|
sendEnabled(isChecked ? "ON" : "OFF");
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|