diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 5cdc587dc..e91f2d29c 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -204,12 +204,21 @@ "detectionMethodDescription": "Probe the camera with ONVIF (if supported) to find camera stream URLs, or manually select the camera brand to use pre-defined URLs. To enter a custom RTSP URL, choose the manual method and select \"Other\".", "onvifPortDescription": "For cameras that support ONVIF, this is usually 80 or 8080.", "probingDevice": "Probing device...", + "probeSuccessful": "Probe successful", "probeError": "Probe Error", "probeNoSuccess": "Probe unsuccessful", + "manufacturer": "Manufacturer", + "model": "Model", + "firmware": "Firmware", + "profiles": "Profiles", + "ptzSupport": "PTZ Support", + "autotrackingSupport": "Autotracking Support", + "presets": "Presets", "deviceInfo": "Device Information", "rtspCandidates": "RTSP Candidates", + "candidateStreamTitle": "Candidate {{number}}", "useCandidate": "Use", - "uriCopied": "URI copied", + "uriCopy": "Copy", "probeFailed": "Failed to probe camera: {{error}}", "probeButton": "Probe camera", "warnings": { diff --git a/web/src/components/settings/wizard/OnvifProbeResults.tsx b/web/src/components/settings/wizard/OnvifProbeResults.tsx index 9ad3809fb..cf17b7204 100644 --- a/web/src/components/settings/wizard/OnvifProbeResults.tsx +++ b/web/src/components/settings/wizard/OnvifProbeResults.tsx @@ -1,8 +1,12 @@ import { useTranslation } from "react-i18next"; import { Card, CardContent, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; +// Input removed: URL shown as text import ActivityIndicator from "@/components/indicators/activity-indicator"; -import { FaExclamationCircle, FaCopy, FaCheck } from "react-icons/fa"; +import { FaCopy, FaCheck } from "react-icons/fa"; +import { LuX } from "react-icons/lu"; +import { CiCircleAlert } from "react-icons/ci"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { useState } from "react"; import { toast } from "sonner"; import type { @@ -11,6 +15,8 @@ import type { TestResult, CandidateTestMap, } from "@/types/cameraWizard"; +import { FaCircleCheck } from "react-icons/fa6"; +import { cn } from "@/lib/utils"; type OnvifProbeResultsProps = { isLoading: boolean; @@ -61,15 +67,11 @@ export default function OnvifProbeResults({ if (isError) { return (
-
- -
-

- {t("cameraWizard.step1.probeError")} -

- {error &&

{error}

} -
-
+ + + {t("cameraWizard.step1.probeError")} + {error && {error}} + @@ -80,19 +82,13 @@ export default function OnvifProbeResults({ if (!probeResult?.success) { return (
-
- -
-

- {t("cameraWizard.step1.probeNoSuccess")} -

- {probeResult?.message && ( -

- {probeResult.message} -

- )} -
-
+ + + {t("cameraWizard.step1.probeNoSuccess")} + {probeResult?.message && ( + {probeResult.message} + )} + @@ -104,6 +100,13 @@ export default function OnvifProbeResults({ return (
+ {/* Probe success header (green check + text) */} + {probeResult?.success && ( +
+ + {t("cameraWizard.step1.probeSuccessful")} +
+ )} {t("cameraWizard.step1.deviceInfo")} @@ -111,49 +114,73 @@ export default function OnvifProbeResults({ {probeResult.manufacturer && (
- Manufacturer:{" "} - {probeResult.manufacturer} + + {t("cameraWizard.step1.manufacturer")}: + {" "} + + {probeResult.manufacturer} +
)} {probeResult.model && (
- Model:{" "} - {probeResult.model} + + {t("cameraWizard.step1.model")}: + {" "} + {probeResult.model}
)} {probeResult.firmware_version && (
- Firmware:{" "} - + + {t("cameraWizard.step1.firmware")}: + {" "} + {probeResult.firmware_version}
)} {probeResult.profiles_count !== undefined && (
- Profiles:{" "} - {probeResult.profiles_count} + + {t("cameraWizard.step1.profiles")}: + {" "} + + {probeResult.profiles_count} +
)} {probeResult.ptz_supported !== undefined && (
- PTZ Support:{" "} - - {probeResult.ptz_supported ? "Yes" : "No"} + + {t("cameraWizard.step1.ptzSupport")}: + {" "} + + {probeResult.ptz_supported + ? t("yes", { ns: "common" }) + : t("no", { ns: "common" })}
)} {probeResult.ptz_supported && probeResult.autotrack_supported && (
- Autotrack Support:{" "} - Yes + + {t("cameraWizard.step1.autotrackingSupport")}: + {" "} + + {t("yes", { ns: "common" })} +
)} {probeResult.ptz_supported && probeResult.presets_count !== undefined && (
- Presets:{" "} - {probeResult.presets_count} + + {t("cameraWizard.step1.presets")}: + {" "} + + {probeResult.presets_count} +
)}
@@ -161,9 +188,7 @@ export default function OnvifProbeResults({ {rtspCandidates.length > 0 && (
-

- {t("cameraWizard.step1.rtspCandidates")} -

+

{t("cameraWizard.step1.rtspCandidates")}

{rtspCandidates.map((candidate, idx) => { @@ -174,12 +199,13 @@ export default function OnvifProbeResults({ return ( handleCopyUri(candidate.uri)} onUse={() => onSelectCandidate(candidate.uri)} isSelected={isSelected} - onTest={() => testCandidate && testCandidate(candidate.uri)} + testCandidate={testCandidate} candidateTest={candidateTest} isTesting={isTesting} /> @@ -194,129 +220,142 @@ export default function OnvifProbeResults({ type CandidateItemProps = { candidate: OnvifRtspCandidate; - isTested?: boolean; + index?: number; + // isTested?: boolean; (unused) copiedUri: string | null; onCopy: () => void; onUse: () => void; isSelected?: boolean; - onTest?: () => void; + // onTest?: () => void; (unused) + testCandidate?: (uri: string) => void; candidateTest?: TestResult | { success: false; error: string }; isTesting?: boolean; }; function CandidateItem({ + index, candidate, - isTested, copiedUri, onCopy, onUse, isSelected, - onTest, + testCandidate, candidateTest, isTesting, }: CandidateItemProps) { const { t } = useTranslation(["views/settings"]); const [showFull, setShowFull] = useState(false); - // Mask credentials for display const maskUri = (uri: string) => { const match = uri.match(/rtsp:\/\/([^:]+):([^@]+)@(.+)/); - if (match) { - return `rtsp://${match[1]}:••••@${match[3]}`; - } + if (match) return `rtsp://${match[1]}:••••@${match[3]}`; return uri; }; return (
-
-
-
-
- {isTested !== undefined && ( - - {isTested ? "✓ OK" : "✗ Failed"} - - )} -
-
-

setShowFull(!showFull)} - title="Click to toggle masked/full view" - > - {showFull ? candidate.uri : maskUri(candidate.uri)} -

- -
+
+
+
+

+ {t("cameraWizard.step1.candidateStreamTitle", { + number: (index ?? 0) + 1, + })} +

+ {candidateTest?.success && ( +
+ {[ + candidateTest.resolution, + candidateTest.fps + ? `${candidateTest.fps} ${t( + "cameraWizard.testResultLabels.fps", + )}` + : null, + candidateTest.videoCodec, + candidateTest.audioCodec, + ] + .filter(Boolean) + .join(" · ")} +
+ )}
+
+ {candidateTest?.success && ( +
+ + + {t("cameraWizard.step2.connected")} + +
+ )} + + {candidateTest && !candidateTest.success && ( +
+ + + {t("cameraWizard.step2.notConnected")} + +
+ )} +
+
+ +
+

setShowFull((s) => !s)} + title={t("cameraWizard.step1.toggleUriView")} + > + {showFull ? candidate.uri : maskUri(candidate.uri)} +

+ +
+ + -
- {candidateTest && candidateTest.success && ( -
- {candidateTest.resolution && ( - - {`${t("cameraWizard.testResultLabels.resolution")} ${candidateTest.resolution}`} - - )} - {candidateTest.fps && ( - - {t("cameraWizard.testResultLabels.fps")} {candidateTest.fps} - - )} - {candidateTest.videoCodec && ( - - {`${t("cameraWizard.testResultLabels.video")} ${candidateTest.videoCodec}`} - - )} - {candidateTest.audioCodec && ( - - {`${t("cameraWizard.testResultLabels.audio")} ${candidateTest.audioCodec}`} - - )} -
- )} + +
+ +
); diff --git a/web/src/components/settings/wizard/Step1NameCamera.tsx b/web/src/components/settings/wizard/Step1NameCamera.tsx index 9d76cf3c3..b4b4928f0 100644 --- a/web/src/components/settings/wizard/Step1NameCamera.tsx +++ b/web/src/components/settings/wizard/Step1NameCamera.tsx @@ -458,7 +458,13 @@ export default function Step1NameCamera({ if (streamConfigs.length > 0) { // Add all successful streams and navigate to Step 2 - onNext({ streams: streamConfigs, customUrl: firstSuccessfulUri }); + const values = form.getValues(); + const cameraName = values.cameraName; + onNext({ + cameraName, + streams: streamConfigs, + customUrl: firstSuccessfulUri, + }); toast.success(t("cameraWizard.step1.testSuccess")); setProbeDialogOpen(false); setProbeResult(null); @@ -490,7 +496,7 @@ export default function Step1NameCamera({ setIsTesting(false); setTestStatus(""); } - }, [selectedCandidateUris, t, onNext, probeUri]); + }, [selectedCandidateUris, t, onNext, probeUri, form]); const testCandidate = useCallback( async (uri: string) => {