mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-11 13:21:10 +03:00
add ptz controls to camera via wizard when onvif has already been probed
This commit is contained in:
@@ -3,7 +3,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useState, useCallback, useMemo, useEffect } from "react";
|
||||
import { LuPlus, LuTrash2, LuX } from "react-icons/lu";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import axios from "axios";
|
||||
@@ -32,6 +32,10 @@ import {
|
||||
LuExternalLink,
|
||||
LuCheck,
|
||||
LuChevronsUpDown,
|
||||
LuChevronDown,
|
||||
LuChevronRight,
|
||||
LuEye,
|
||||
LuEyeOff,
|
||||
} from "react-icons/lu";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||
@@ -44,6 +48,11 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
|
||||
type Step3StreamConfigProps = {
|
||||
wizardData: Partial<WizardFormData>;
|
||||
@@ -64,6 +73,51 @@ export default function Step3StreamConfig({
|
||||
const { getLocaleDocUrl } = useDocDomain();
|
||||
const [testingStreams, setTestingStreams] = useState<Set<string>>(new Set());
|
||||
const [openCombobox, setOpenCombobox] = useState<string | null>(null);
|
||||
const [showOnvifPassword, setShowOnvifPassword] = useState(false);
|
||||
const [onvifDetailsOpen, setOnvifDetailsOpen] = useState(false);
|
||||
|
||||
const onvif = wizardData.onvif;
|
||||
const ptzSupported = wizardData.probeResult?.ptz_supported === true;
|
||||
const onvifInvalid = !!onvif?.enabled && (!onvif.host?.trim() || !onvif.port);
|
||||
|
||||
// Seed the PTZ pane once from the successful ONVIF probe
|
||||
useEffect(() => {
|
||||
// run only on first entry and never clobber a user's later toggle-off or edits
|
||||
if (ptzSupported && wizardData.onvif === undefined) {
|
||||
onUpdate({
|
||||
onvif: {
|
||||
enabled: true,
|
||||
host: wizardData.host ?? "",
|
||||
port: wizardData.onvifPort ?? 8000,
|
||||
user: wizardData.username ?? "",
|
||||
password: wizardData.password ?? "",
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [
|
||||
ptzSupported,
|
||||
wizardData.onvif,
|
||||
wizardData.host,
|
||||
wizardData.onvifPort,
|
||||
wizardData.username,
|
||||
wizardData.password,
|
||||
onUpdate,
|
||||
]);
|
||||
|
||||
const updateOnvif = useCallback(
|
||||
(updates: Partial<NonNullable<WizardFormData["onvif"]>>) => {
|
||||
onUpdate({
|
||||
onvif: {
|
||||
enabled: false,
|
||||
host: "",
|
||||
port: 8000,
|
||||
...wizardData.onvif,
|
||||
...updates,
|
||||
},
|
||||
});
|
||||
},
|
||||
[onUpdate, wizardData.onvif],
|
||||
);
|
||||
|
||||
const streams = useMemo(() => wizardData.streams || [], [wizardData.streams]);
|
||||
|
||||
@@ -725,12 +779,136 @@ export default function Step3StreamConfig({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{ptzSupported && (
|
||||
<Card className="bg-secondary text-primary">
|
||||
<CardContent className="space-y-2 p-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-medium">
|
||||
{t("cameraWizard.step3.ptz.title")}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("cameraWizard.step3.ptz.detectedNote")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={onvif?.enabled ?? false}
|
||||
onCheckedChange={(checked) => updateOnvif({ enabled: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{onvif?.enabled && (
|
||||
<Collapsible
|
||||
open={onvifDetailsOpen || onvifInvalid}
|
||||
onOpenChange={setOnvifDetailsOpen}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start gap-2 pl-0 hover:bg-transparent"
|
||||
>
|
||||
{onvifDetailsOpen || onvifInvalid ? (
|
||||
<LuChevronDown className="size-4" />
|
||||
) : (
|
||||
<LuChevronRight className="size-4" />
|
||||
)}
|
||||
{t("cameraWizard.step3.ptz.connectionDetails")}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="mt-2 space-y-4 rounded-lg bg-background p-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-primary-variant">
|
||||
{t("cameraWizard.step3.ptz.host")}
|
||||
</Label>
|
||||
<Input
|
||||
value={onvif.host}
|
||||
onChange={(e) => updateOnvif({ host: e.target.value })}
|
||||
className="h-8"
|
||||
placeholder="192.168.1.100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-primary-variant">
|
||||
{t("cameraWizard.step3.ptz.port")}
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={onvif.port || ""}
|
||||
onChange={(e) => {
|
||||
const parsed = parseInt(e.target.value, 10);
|
||||
updateOnvif({ port: isNaN(parsed) ? 0 : parsed });
|
||||
}}
|
||||
className="h-8"
|
||||
placeholder="8000"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-primary-variant">
|
||||
{t("cameraWizard.step3.ptz.username")}
|
||||
</Label>
|
||||
<Input
|
||||
value={onvif.user ?? ""}
|
||||
onChange={(e) => updateOnvif({ user: e.target.value })}
|
||||
className="h-8"
|
||||
placeholder={t("cameraWizard.step1.usernamePlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-primary-variant">
|
||||
{t("cameraWizard.step3.ptz.password")}
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showOnvifPassword ? "text" : "password"}
|
||||
value={onvif.password ?? ""}
|
||||
onChange={(e) =>
|
||||
updateOnvif({ password: e.target.value })
|
||||
}
|
||||
className="h-8 pr-10"
|
||||
placeholder={t(
|
||||
"cameraWizard.step1.passwordPlaceholder",
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||
onClick={() => setShowOnvifPassword((s) => !s)}
|
||||
>
|
||||
{showOnvifPassword ? (
|
||||
<LuEyeOff className="size-4" />
|
||||
) : (
|
||||
<LuEye className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!hasDetectRole && (
|
||||
<div className="rounded-lg border border-danger/50 p-3 text-sm text-danger">
|
||||
{t("cameraWizard.step3.detectRoleWarning")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{onvifInvalid && (
|
||||
<div className="rounded-lg border border-danger/50 p-3 text-sm text-danger">
|
||||
{t("cameraWizard.step3.ptz.hostRequiredWarning")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col-reverse gap-2 pt-6 sm:flex-row sm:justify-end">
|
||||
{onBack && (
|
||||
<Button type="button" onClick={onBack} className="sm:flex-1">
|
||||
|
||||
Reference in New Issue
Block a user