From c5c8218e1ad924d22fee19f89da81997ac7af6fa Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Mon, 29 Sep 2025 15:21:22 +0100 Subject: [PATCH 01/22] added zoom functionality, need to add endpoint to post --- .../CameraOverview/SnapshotContainer.tsx | 28 ++++-- .../CameraSettings/CameraSettingFields.tsx | 96 ++++++++++++++++++- .../CameraSettings/CameraSettings.tsx | 25 +++-- .../OverviewVideoContainer.tsx | 12 ++- src/hooks/useGetOverviewSnapshot.ts | 2 +- src/pages/Dashboard.tsx | 2 +- src/pages/FrontCamera.tsx | 20 +++- src/types/types.ts | 10 ++ src/utils/config.ts | 2 +- 9 files changed, 171 insertions(+), 26 deletions(-) diff --git a/src/components/CameraOverview/SnapshotContainer.tsx b/src/components/CameraOverview/SnapshotContainer.tsx index e5ad76b..cc491db 100644 --- a/src/components/CameraOverview/SnapshotContainer.tsx +++ b/src/components/CameraOverview/SnapshotContainer.tsx @@ -1,17 +1,21 @@ import { useGetOverviewSnapshot } from "../../hooks/useGetOverviewSnapshot"; +import type { ZoomLevel } from "../../types/types"; import NavigationArrow from "../UI/NavigationArrow"; type SnapshotContainerProps = { side: string; settingsPage?: boolean; + zoomLevel?: ZoomLevel; + onZoomLevelChange?: (level: ZoomLevel) => void; }; export const SnapshotContainer = ({ side, settingsPage, + zoomLevel, + onZoomLevelChange, }: SnapshotContainerProps) => { const { canvasRef, isError, isPending } = useGetOverviewSnapshot(); - if (isError) return

An error occurred

; if (isPending) return

Loading...

; @@ -27,14 +31,20 @@ export const SnapshotContainer = ({ if (!cw || !ch) return; const px = x / cw; const py = y / ch; - console.log({ - left, - top, - x, - y, - px, - py, - }); + + const baseLevel = zoomLevel?.level ?? 1; + const newLevel = baseLevel >= 8 ? 1 : baseLevel * 2; + + if (onZoomLevelChange) + onZoomLevelChange({ + left, + top, + x, + y, + px, + py, + level: newLevel, + }); }; return ( diff --git a/src/components/CameraSettings/CameraSettingFields.tsx b/src/components/CameraSettings/CameraSettingFields.tsx index 6f31f46..74db4bc 100644 --- a/src/components/CameraSettings/CameraSettingFields.tsx +++ b/src/components/CameraSettings/CameraSettingFields.tsx @@ -3,19 +3,25 @@ import type { CameraConfig, CameraSettingErrorValues, CameraSettingValues, + ZoomLevel, } from "../../types/types"; import { useMemo, useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons"; +import CardHeader from "../UI/CardHeader"; type CameraSettingsProps = { initialData: CameraConfig; updateCameraConfig: (values: CameraSettingValues) => Promise | void; + zoomLevel?: ZoomLevel; + onZoomLevelChange?: (level: ZoomLevel) => void; }; const CameraSettingFields = ({ initialData, updateCameraConfig, + zoomLevel, + onZoomLevelChange, }: CameraSettingsProps) => { const [showPwd, setShowPwd] = useState(false); @@ -26,8 +32,9 @@ const CameraSettingFields = ({ userName: "", password: "", id: initialData?.id, + zoom: zoomLevel?.level ? zoomLevel.level : 1, }), - [initialData?.id, initialData?.propURI?.value] + [initialData?.id, initialData?.propURI?.value, zoomLevel?.level] ); const validateValues = (values: CameraSettingValues) => { @@ -41,16 +48,24 @@ const CameraSettingFields = ({ }; const handleSubmit = (values: CameraSettingValues) => { + console.log(values); updateCameraConfig(values); }; + const handleRadioButtonChange = (levelNumber: number) => { + if (!onZoomLevelChange || !zoomLevel) return; + onZoomLevelChange({ + ...zoomLevel, + level: zoomLevel?.level !== levelNumber ? levelNumber : zoomLevel?.level, + }); + }; + return ( {({ errors, touched }) => (
@@ -111,7 +126,7 @@ const CameraSettingFields = ({ {errors.password} )} -
+
+
+ +
+
+ handleRadioButtonChange(1)} + /> + +
+
+ handleRadioButtonChange(2)} + /> + +
+ +
+ handleRadioButtonChange(4)} + /> + +
+ +
+ handleRadioButtonChange(8)} + /> + +
+
+
diff --git a/src/components/FrontCameraSettings/OverviewVideoContainer.tsx b/src/components/FrontCameraSettings/OverviewVideoContainer.tsx index 7ed4c87..db16aeb 100644 --- a/src/components/FrontCameraSettings/OverviewVideoContainer.tsx +++ b/src/components/FrontCameraSettings/OverviewVideoContainer.tsx @@ -3,14 +3,19 @@ import { SnapshotContainer } from "../CameraOverview/SnapshotContainer"; import Card from "../UI/Card"; import { useNavigate } from "react-router"; import { useSwipeable } from "react-swipeable"; +import type { ZoomLevel } from "../../types/types"; const OverviewVideoContainer = ({ side, settingsPage, + zoomLevel, + onZoomLevelChange, }: { title: string; side: string; settingsPage?: boolean; + zoomLevel?: ZoomLevel; + onZoomLevelChange?: (level: ZoomLevel) => void; }) => { const navigate = useNavigate(); const handlers = useSwipeable({ @@ -24,7 +29,12 @@ const OverviewVideoContainer = ({ )} >
- +
); diff --git a/src/hooks/useGetOverviewSnapshot.ts b/src/hooks/useGetOverviewSnapshot.ts index 5bf7e51..5e00b0e 100644 --- a/src/hooks/useGetOverviewSnapshot.ts +++ b/src/hooks/useGetOverviewSnapshot.ts @@ -5,7 +5,7 @@ import { CAM_BASE } from "../utils/config"; const apiUrl = CAM_BASE; async function fetchSnapshot() { - const response = await fetch(`${apiUrl}/CameraA-preview`); + const response = await fetch(`${apiUrl}/CameraRear-preview`); if (!response.ok) { throw new Error("Cannot reach endpoint"); } diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 92a8efe..844a26b 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -4,7 +4,7 @@ import { SightingFeedProvider } from "../context/providers/SightingFeedProvider" import { CAM_BASE } from "../utils/config"; const Dashboard = () => { - const base_url = `${CAM_BASE}/SightingList/sightingSummary?mostRecentRef=`; + const base_url = `${CAM_BASE}/SightingListFront/sightingSummary?mostRecentRef=`; return (
diff --git a/src/pages/FrontCamera.tsx b/src/pages/FrontCamera.tsx index dd97803..cc4bc03 100644 --- a/src/pages/FrontCamera.tsx +++ b/src/pages/FrontCamera.tsx @@ -1,16 +1,34 @@ +import { useState } from "react"; import CameraSettings from "../components/CameraSettings/CameraSettings"; import OverviewVideoContainer from "../components/FrontCameraSettings/OverviewVideoContainer"; import { Toaster } from "sonner"; +import type { ZoomLevel } from "../types/types"; const FrontCamera = () => { + const [zoomLevel, setZoomLevel] = useState({ + left: 0, + top: 0, + x: 0, + y: 0, + px: 0, + py: 0, + level: 1, + }); return (
+ -
); diff --git a/src/types/types.ts b/src/types/types.ts index a43d6a0..e70d706 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -251,3 +251,13 @@ export type CameraBlackBoardOptions = { export type CameraBlackboardResponse = { data: object; }; + +export type ZoomLevel = { + left: number; + top: number; + x: number; + y: number; + px: number; + py: number; + level?: number; +}; diff --git a/src/utils/config.ts b/src/utils/config.ts index 3cd72f3..f403ad1 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,4 +1,4 @@ -const rawCamBase = import.meta.env.VITE_CAM_BASE; +const rawCamBase = import.meta.env.VITE_OUTSIDE_BASEURL; export const CAM_BASE = rawCamBase && rawCamBase.trim().length > 0 ? rawCamBase From 369ff3e17e2be65b9ebd19e94a0a9ff96fb6a0dd Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Mon, 29 Sep 2025 15:55:25 +0100 Subject: [PATCH 02/22] added loading state for camera form --- src/components/CameraSettings/CameraSettings.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/CameraSettings/CameraSettings.tsx b/src/components/CameraSettings/CameraSettings.tsx index 3a7ac69..1c3cb9b 100644 --- a/src/components/CameraSettings/CameraSettings.tsx +++ b/src/components/CameraSettings/CameraSettings.tsx @@ -18,19 +18,20 @@ const CameraSettings = ({ }) => { const { data, isError, isPending, updateCameraConfig } = useFetchCameraConfig(side); - return ( {isPending && <>Loading camera config} {isError && <>Error fetching camera config}
- + {!isPending && ( + + )}
); From 087b3613aeeac011ed4b75e49b61bce0177619e7 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Tue, 30 Sep 2025 09:07:22 +0100 Subject: [PATCH 03/22] Minor fixes: removed clock added navigation arrow to main sighting screen added zoom functionality to rear (camera B) settings brought back navigation to rear cam page --- .../FrontCameraOverviewCard.tsx | 1 + .../OverviewVideoContainer.tsx | 13 +++- .../SightingOverview/SightingOverview.tsx | 1 + src/components/UI/Header.tsx | 75 ++----------------- src/components/UI/NavigationArrow.tsx | 25 +++---- src/pages/Dashboard.tsx | 2 +- src/pages/FrontCamera.tsx | 2 +- src/pages/RearCamera.tsx | 41 +++++----- 8 files changed, 57 insertions(+), 103 deletions(-) diff --git a/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx b/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx index 6c449dd..b7689fa 100644 --- a/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx +++ b/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx @@ -10,6 +10,7 @@ const FrontCameraOverviewCard = () => { const navigate = useNavigate(); const handlers = useSwipeable({ onSwipedRight: () => navigate("/camera-settings"), + onSwipedLeft: () => navigate("/rear-camera-settings"), trackMouse: true, }); diff --git a/src/components/FrontCameraSettings/OverviewVideoContainer.tsx b/src/components/FrontCameraSettings/OverviewVideoContainer.tsx index db16aeb..631270f 100644 --- a/src/components/FrontCameraSettings/OverviewVideoContainer.tsx +++ b/src/components/FrontCameraSettings/OverviewVideoContainer.tsx @@ -1,7 +1,7 @@ import clsx from "clsx"; import { SnapshotContainer } from "../CameraOverview/SnapshotContainer"; import Card from "../UI/Card"; -import { useNavigate } from "react-router"; +import { useNavigate, useLocation } from "react-router"; import { useSwipeable } from "react-swipeable"; import type { ZoomLevel } from "../../types/types"; @@ -18,8 +18,17 @@ const OverviewVideoContainer = ({ onZoomLevelChange?: (level: ZoomLevel) => void; }) => { const navigate = useNavigate(); + const location = useLocation(); + console.log(location); const handlers = useSwipeable({ - onSwipedLeft: () => navigate("/"), + onSwipedLeft: () => { + if (location.pathname === "/rear-camera-settings") return; + navigate("/"); + }, + onSwipedRight: () => { + if (location.pathname === "/camera-settings") return; + navigate("/"); + }, trackMouse: true, }); return ( diff --git a/src/components/SightingOverview/SightingOverview.tsx b/src/components/SightingOverview/SightingOverview.tsx index 14b012b..07b62c0 100644 --- a/src/components/SightingOverview/SightingOverview.tsx +++ b/src/components/SightingOverview/SightingOverview.tsx @@ -30,6 +30,7 @@ const SightingOverview = () => { return (
+
{mostRecent && (
diff --git a/src/components/UI/Header.tsx b/src/components/UI/Header.tsx index 1e0866a..2de4734 100644 --- a/src/components/UI/Header.tsx +++ b/src/components/UI/Header.tsx @@ -3,56 +3,25 @@ import Logo from "/MAV.svg"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faGear, + faHome, faListCheck, faMaximize, faMinimize, faRotate, } from "@fortawesome/free-solid-svg-icons"; -import type { VersionFieldType } from "../../types/types"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import SoundBtn from "./SoundBtn"; import { useNPEDContext } from "../../context/NPEDUserContext"; -async function fetchVersions( - signal?: AbortSignal -): Promise { - try { - const res = await fetch("http://192.168.75.11/api/versions", { signal }); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - return res.json(); - } catch (error) { - console.log(error); - return undefined; - } -} - -const pad = (n: number) => String(n).padStart(2, "0"); -const normalizeToMs = (ts: number) => (ts < 1e12 ? ts * 1000 : ts); // seconds → ms if needed - -function formatFromMs(ms: number, tz: "local" | "utc" = "local") { - const d = new Date(ms); - const h = tz === "utc" ? d.getUTCHours() : d.getHours(); - const m = tz === "utc" ? d.getUTCMinutes() : d.getMinutes(); - const s = tz === "utc" ? d.getUTCSeconds() : d.getSeconds(); - const day = tz === "utc" ? d.getUTCDate() : d.getDate(); - const month = (tz === "utc" ? d.getUTCMonth() : d.getMonth()) + 1; - const year = tz === "utc" ? d.getUTCFullYear() : d.getFullYear(); - return `${pad(h)}:${pad(m)}:${pad(s)} ${pad(day)}-${pad(month)}-${year}`; -} - export default function Header() { - const [offsetMs, setOffsetMs] = useState(null); - const [nowMs, setNowMs] = useState(Date.now()); const [isFullscreen, setIsFullscreen] = useState(false); const { sessionStarted } = useNPEDContext(); const toggleFullscreen = () => { if (!document.fullscreenElement) { - // Enter fullscreen on the entire app document.documentElement.requestFullscreen(); setIsFullscreen(true); } else { - // Exit fullscreen document.exitFullscreen(); setIsFullscreen(false); } @@ -62,38 +31,8 @@ export default function Header() { window.location.reload(); }; - useEffect(() => { - const ac = new AbortController(); - fetchVersions(ac.signal) - .then((data) => { - if (!data) throw new Error("No data"); - const serverMs = normalizeToMs(data?.timeStamp); - setOffsetMs(serverMs - Date.now()); - }) - .catch((err) => { - console.log(err); - }); - return () => ac.abort("failed"); - }, []); - - useEffect(() => { - let timer: number; - const schedule = () => { - const now = Date.now(); - setNowMs(now); - const delay = 1000 - (now % 1000); - timer = window.setTimeout(schedule, delay); - }; - schedule(); - return () => clearTimeout(timer); - }, []); - - const serverNowMs = offsetMs == null ? nowMs : nowMs + offsetMs; - const localStr = formatFromMs(serverNowMs, "local"); - const utcStr = formatFromMs(serverNowMs, "utc"); - return ( -
+
Logo @@ -103,11 +42,11 @@ export default function Header() { {sessionStarted && (
Session Active
)} -
-

Local: {localStr}

-

UTC: {utcStr}

-
+
+ + +
{isFullscreen ? ( diff --git a/src/components/UI/NavigationArrow.tsx b/src/components/UI/NavigationArrow.tsx index 65c14d7..a879255 100644 --- a/src/components/UI/NavigationArrow.tsx +++ b/src/components/UI/NavigationArrow.tsx @@ -9,7 +9,6 @@ type NavigationArrowProps = { const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => { const navigate = useNavigate(); - const navigationDest = (side: string | undefined) => { if (settingsPage) { navigate("/"); @@ -44,19 +43,17 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => { } return ( <> - {side === "Front" ? ( - navigationDest(side)} - /> - ) : ( - navigationDest(side)} - /> - )} + navigationDest(side)} + /> + + navigationDest(side)} + /> ); }; diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 844a26b..7af2040 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -6,7 +6,7 @@ import { CAM_BASE } from "../utils/config"; const Dashboard = () => { const base_url = `${CAM_BASE}/SightingListFront/sightingSummary?mostRecentRef=`; return ( - +
diff --git a/src/pages/FrontCamera.tsx b/src/pages/FrontCamera.tsx index cc4bc03..3393e5e 100644 --- a/src/pages/FrontCamera.tsx +++ b/src/pages/FrontCamera.tsx @@ -18,7 +18,7 @@ const FrontCamera = () => {
{ - const navigate = useNavigate(); - const handlers = useSwipeable({ - onSwipedRight: () => navigate("/"), - trackMouse: true, + const [zoomLevel, setZoomLevel] = useState({ + left: 0, + top: 0, + x: 0, + y: 0, + px: 0, + py: 0, + level: 1, }); - return ( -
- - -
- -
+
+ +
); From 633435df8dc3223620a81d340c44591d2908313b Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Tue, 30 Sep 2025 09:10:10 +0100 Subject: [PATCH 04/22] removed console.logs --- src/components/CameraSettings/CameraSettingFields.tsx | 1 - src/components/FrontCameraSettings/OverviewVideoContainer.tsx | 1 - src/components/UI/CardHeader.tsx | 1 - src/hooks/useCameraConfig.ts | 1 - src/hooks/useNPEDAuth.ts | 2 -- 5 files changed, 6 deletions(-) diff --git a/src/components/CameraSettings/CameraSettingFields.tsx b/src/components/CameraSettings/CameraSettingFields.tsx index 74db4bc..284d249 100644 --- a/src/components/CameraSettings/CameraSettingFields.tsx +++ b/src/components/CameraSettings/CameraSettingFields.tsx @@ -48,7 +48,6 @@ const CameraSettingFields = ({ }; const handleSubmit = (values: CameraSettingValues) => { - console.log(values); updateCameraConfig(values); }; diff --git a/src/components/FrontCameraSettings/OverviewVideoContainer.tsx b/src/components/FrontCameraSettings/OverviewVideoContainer.tsx index 631270f..c58d3cd 100644 --- a/src/components/FrontCameraSettings/OverviewVideoContainer.tsx +++ b/src/components/FrontCameraSettings/OverviewVideoContainer.tsx @@ -19,7 +19,6 @@ const OverviewVideoContainer = ({ }) => { const navigate = useNavigate(); const location = useLocation(); - console.log(location); const handlers = useSwipeable({ onSwipedLeft: () => { if (location.pathname === "/rear-camera-settings") return; diff --git a/src/components/UI/CardHeader.tsx b/src/components/UI/CardHeader.tsx index d39e675..2d7c369 100644 --- a/src/components/UI/CardHeader.tsx +++ b/src/components/UI/CardHeader.tsx @@ -17,7 +17,6 @@ const CardHeader = ({ img, sighting, }: CameraOverviewHeaderProps) => { - // console.log(sighting?.debug.toLowerCase()); return (
{ const [, cameraSide] = queryKey; diff --git a/src/hooks/useNPEDAuth.ts b/src/hooks/useNPEDAuth.ts index ed48fc1..f1844d4 100644 --- a/src/hooks/useNPEDAuth.ts +++ b/src/hooks/useNPEDAuth.ts @@ -48,8 +48,6 @@ async function signIn(loginDetails: NPEDFieldType) { if (!frontCameraResponse.ok) throw new Error("cannot reach NPED endpoint"); if (!rearCameraResponse.ok) throw new Error("cannot reach NPED endpoint"); - const data = await frontCameraResponse.json(); - console.log(data); return { frontResponse: frontCameraResponse.json(), rearResponse: rearCameraResponse.json(), From eb74c2c6495ae7eaabedf36b46f44f577e13a589 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Tue, 30 Sep 2025 11:11:46 +0100 Subject: [PATCH 05/22] Refactor camera configuration and overview snapshot hooks; update environment variables and changed camera names to A and B --- .env | 14 +------- .../CameraOverview/SnapshotContainer.tsx | 2 +- .../CameraSettings/CameraSettingFields.tsx | 3 +- .../FrontCameraOverviewCard.tsx | 3 +- src/hooks/useCameraConfig.ts | 4 +-- src/hooks/useGetOverviewSnapshot.ts | 8 ++--- src/hooks/useOverviewVideo.ts | 36 ------------------- src/pages/Dashboard.tsx | 5 ++- src/pages/FrontCamera.tsx | 8 ++--- src/pages/RearCamera.tsx | 8 ++--- src/utils/config.ts | 2 +- 11 files changed, 24 insertions(+), 69 deletions(-) delete mode 100644 src/hooks/useOverviewVideo.ts diff --git a/.env b/.env index 81e1238..8914832 100644 --- a/.env +++ b/.env @@ -6,16 +6,4 @@ VITE_OUTSIDE_BASEURL=http://100.82.205.44 VITE_FOLKESTONE_URL=http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef= VITE_MAV_URL=http://192.168.75.11/SightingListFront/sightingSummary?mostRecentRef= -VITE_AGX_BOX_FRONT_URL=http://192.168.0.90:8080/SightingListFront/sightingSummary?mostRecentRef= -VITE_AGX_BOX_REAR_URL=http://192.168.0.90:8080/SightingListRear/sightingSummary?mostRecentRef= - -VITE_AGX=http://100.72.72.70:8080/SightingListRear/sightingSummary?mostRecentRef= -VITE_AGX_FRONT=http://100.72.72.70:8080/SightingListFront/sightingSummary?mostRecentRef= - -VITE_AGX_FRONT_BASE=http://100.72.72.70:8080/ - -VITE_LOCAL=http://10.42.0.1:8080/SightingListRear/sightingSummary?mostRecentRef= -VITE_LOCAL_FRONT=http://10.42.0.1:8080/SightingListFront/sightingSummary?mostRecentRef= - -VITE_LOCAL_BASE=http://10.42.0.1:8080/ -VITE_LOCAL_BASE_NEW=http://100.113.222.39 \ No newline at end of file +VITE_AGX_BOX_URL=http://100.118.196.113:8080 diff --git a/src/components/CameraOverview/SnapshotContainer.tsx b/src/components/CameraOverview/SnapshotContainer.tsx index cc491db..5e1698f 100644 --- a/src/components/CameraOverview/SnapshotContainer.tsx +++ b/src/components/CameraOverview/SnapshotContainer.tsx @@ -15,7 +15,7 @@ export const SnapshotContainer = ({ zoomLevel, onZoomLevelChange, }: SnapshotContainerProps) => { - const { canvasRef, isError, isPending } = useGetOverviewSnapshot(); + const { canvasRef, isError, isPending } = useGetOverviewSnapshot(side); if (isError) return

An error occurred

; if (isPending) return

Loading...

; diff --git a/src/components/CameraSettings/CameraSettingFields.tsx b/src/components/CameraSettings/CameraSettingFields.tsx index 284d249..90cf47b 100644 --- a/src/components/CameraSettings/CameraSettingFields.tsx +++ b/src/components/CameraSettings/CameraSettingFields.tsx @@ -24,7 +24,7 @@ const CameraSettingFields = ({ onZoomLevelChange, }: CameraSettingsProps) => { const [showPwd, setShowPwd] = useState(false); - + console.log(initialData); const initialValues = useMemo( () => ({ friendlyName: initialData?.id ?? "", @@ -48,6 +48,7 @@ const CameraSettingFields = ({ }; const handleSubmit = (values: CameraSettingValues) => { + console.log(values); updateCameraConfig(values); }; diff --git a/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx b/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx index b7689fa..75503b1 100644 --- a/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx +++ b/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx @@ -2,11 +2,10 @@ import clsx from "clsx"; import Card from "../UI/Card"; import { useSwipeable } from "react-swipeable"; import { useNavigate } from "react-router"; -import { useOverviewVideo } from "../../hooks/useOverviewVideo"; + import SightingOverview from "../SightingOverview/SightingOverview"; const FrontCameraOverviewCard = () => { - useOverviewVideo(); const navigate = useNavigate(); const handlers = useSwipeable({ onSwipedRight: () => navigate("/camera-settings"), diff --git a/src/hooks/useCameraConfig.ts b/src/hooks/useCameraConfig.ts index cd04942..c07d540 100644 --- a/src/hooks/useCameraConfig.ts +++ b/src/hooks/useCameraConfig.ts @@ -19,10 +19,10 @@ const updateCamerasideConfig = async (data: { const updateUrl = `${base_url}/update-config?id=${data.id}`; const updateConfigPayload = { - id: data.id, + id: data.friendlyName, fields: [ { - property: "propLEDDriverControlURI", + property: "id", value: data.friendlyName, }, ], diff --git a/src/hooks/useGetOverviewSnapshot.ts b/src/hooks/useGetOverviewSnapshot.ts index 5e00b0e..9ca68b8 100644 --- a/src/hooks/useGetOverviewSnapshot.ts +++ b/src/hooks/useGetOverviewSnapshot.ts @@ -4,8 +4,8 @@ import { CAM_BASE } from "../utils/config"; const apiUrl = CAM_BASE; -async function fetchSnapshot() { - const response = await fetch(`${apiUrl}/CameraRear-preview`); +async function fetchSnapshot(cameraSide: string) { + const response = await fetch(`${apiUrl}/${cameraSide}-preview`); if (!response.ok) { throw new Error("Cannot reach endpoint"); } @@ -13,7 +13,7 @@ async function fetchSnapshot() { return await response.blob(); } -export function useGetOverviewSnapshot() { +export function useGetOverviewSnapshot(side: string) { const latestUrlRef = useRef(null); const canvasRef = useRef(null); const imageRef = useRef(null); @@ -38,7 +38,7 @@ export function useGetOverviewSnapshot() { isPending, } = useQuery({ queryKey: ["overviewSnapshot"], - queryFn: () => fetchSnapshot(), + queryFn: () => fetchSnapshot(side), refetchOnWindowFocus: false, refetchInterval: 250, }); diff --git a/src/hooks/useOverviewVideo.ts b/src/hooks/useOverviewVideo.ts deleted file mode 100644 index df56363..0000000 --- a/src/hooks/useOverviewVideo.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { useRef } from "react"; -import { CAM_BASE } from "../utils/config"; - -async function fetchOverviewImage(cameraSide: string) { - const response = await fetch(`${CAM_BASE}/${cameraSide}-preview`); - if (!response.ok) throw new Error("could not fetch overview image"); - return response.blob(); -} - -export function useOverviewVideo() { - const canvasRef = useRef(null); - const { isPending, isError, data } = useQuery({ - queryKey: ["overviewVideo"], - queryFn: () => fetchOverviewImage("CameraFront"), - // refetchInterval: () => - // typeof document !== "undefined" && document.visibilityState === "hidden" - // ? SLOW_MS - // : FAST_MS, - // refetchIntervalInBackground: false, - }); - - if (isPending) return; - - if (isError) return; - - const img = new Image(); - const imgUrl = URL.createObjectURL(data); - img.src = imgUrl; - - const canvas = canvasRef.current; - if (!canvas) return; - const ctx = canvas.getContext("2d"); - - ctx?.drawImage(img, 0, 0); -} diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 7af2040..b46ffe1 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -4,7 +4,10 @@ import { SightingFeedProvider } from "../context/providers/SightingFeedProvider" import { CAM_BASE } from "../utils/config"; const Dashboard = () => { - const base_url = `${CAM_BASE}/SightingListFront/sightingSummary?mostRecentRef=`; + const mode = import.meta.env.MODE; + const base_url = `${CAM_BASE}/SightingList/sightingSummary?mostRecentRef=`; + console.log(mode); + console.log(base_url); return (
diff --git a/src/pages/FrontCamera.tsx b/src/pages/FrontCamera.tsx index 3393e5e..daa5214 100644 --- a/src/pages/FrontCamera.tsx +++ b/src/pages/FrontCamera.tsx @@ -17,15 +17,15 @@ const FrontCamera = () => { return (
diff --git a/src/pages/RearCamera.tsx b/src/pages/RearCamera.tsx index 9644871..62f4ac1 100644 --- a/src/pages/RearCamera.tsx +++ b/src/pages/RearCamera.tsx @@ -17,14 +17,14 @@ const RearCamera = () => { return (
0 ? rawCamBase From 673df1a4f4330b2a5d9b64082bdad452d627af20 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Tue, 30 Sep 2025 13:25:11 +0100 Subject: [PATCH 06/22] added ui for sound settings --- .../SettingForms/Sound/SoundSettingsCard.tsx | 14 ++ .../Sound/SoundSettingsFields.tsx | 127 ++++++++++++++++++ .../SettingForms/Sound/SoundUpload.tsx | 45 +++++++ .../SettingForms/Sound/SoundUploadCard.tsx | 14 ++ src/pages/SystemSettings.tsx | 9 ++ src/types/types.ts | 17 +++ 6 files changed, 226 insertions(+) create mode 100644 src/components/SettingForms/Sound/SoundSettingsCard.tsx create mode 100644 src/components/SettingForms/Sound/SoundSettingsFields.tsx create mode 100644 src/components/SettingForms/Sound/SoundUpload.tsx create mode 100644 src/components/SettingForms/Sound/SoundUploadCard.tsx diff --git a/src/components/SettingForms/Sound/SoundSettingsCard.tsx b/src/components/SettingForms/Sound/SoundSettingsCard.tsx new file mode 100644 index 0000000..e2b904c --- /dev/null +++ b/src/components/SettingForms/Sound/SoundSettingsCard.tsx @@ -0,0 +1,14 @@ +import Card from "../../UI/Card"; +import CardHeader from "../../UI/CardHeader"; +import SoundSettingsFields from "./SoundSettingsFields"; + +const SoundSettingsCard = () => { + return ( + + + + + ); +}; + +export default SoundSettingsCard; diff --git a/src/components/SettingForms/Sound/SoundSettingsFields.tsx b/src/components/SettingForms/Sound/SoundSettingsFields.tsx new file mode 100644 index 0000000..18a84c9 --- /dev/null +++ b/src/components/SettingForms/Sound/SoundSettingsFields.tsx @@ -0,0 +1,127 @@ +import { Field, FieldArray, Form, Formik } from "formik"; +import FormGroup from "../components/FormGroup"; +import type { FormValues, Hotlist } from "../../../types/types"; + +const SoundSettingsFields = () => { + const hotlists: Hotlist[] = [ + { name: "hotlist0", sound: "" }, + { name: "hotlist1", sound: "" }, + { name: "hotlist2", sound: "" }, + ]; + + const soundOptions = [ + { + value: "switch", + label: "Switch (Default)", + }, + { + value: "notification", + label: "Notification", + }, + { + value: "popup", + label: "popup", + }, + ]; + + const initialValues: FormValues = { + sightingSound: "switch", + NPEDsound: "popup", + hotlists, + }; + + const handleSubmit = (values: FormValues) => { + console.log(values); + }; + + return ( + + {({ values }) => ( + + + + + {soundOptions.map(({ value, label }) => { + return ( + + ); + })} + + + + + + {soundOptions.map(({ value, label }) => ( + + ))} + + + +
+

Hotlist Sounds

+ + + ( +
+ {values.hotlists.length > 0 ? ( + values.hotlists.map((hotlist, index) => ( +
+ + + {soundOptions.map(({ value, label }) => ( + + ))} + +
+ )) + ) : ( + <>No hotlists yet, Add one + )} +
+ )} + /> +
+
+ + + + )} +
+ ); +}; + +export default SoundSettingsFields; diff --git a/src/components/SettingForms/Sound/SoundUpload.tsx b/src/components/SettingForms/Sound/SoundUpload.tsx new file mode 100644 index 0000000..46dc654 --- /dev/null +++ b/src/components/SettingForms/Sound/SoundUpload.tsx @@ -0,0 +1,45 @@ +import { Form, Formik } from "formik"; +import FormGroup from "../components/FormGroup"; +import type { SoundUploadValue } from "../../../types/types"; + +const SoundUpload = () => { + const initialValues: SoundUploadValue = { + soundFile: null, + }; + + const handleSubmit = (values: SoundUploadValue) => { + console.log(values); + }; + + return ( + + {({ setFieldValue }) => ( +
+ + + { + if ( + e.target.files && + e.target.files[0].type.lastIndexOf(".mp3") + ) + setFieldValue("sightingSound", e.target.files[0]); + }} + /> + + +
+ )} +
+ ); +}; + +export default SoundUpload; diff --git a/src/components/SettingForms/Sound/SoundUploadCard.tsx b/src/components/SettingForms/Sound/SoundUploadCard.tsx new file mode 100644 index 0000000..e751959 --- /dev/null +++ b/src/components/SettingForms/Sound/SoundUploadCard.tsx @@ -0,0 +1,14 @@ +import Card from "../../UI/Card"; +import CardHeader from "../../UI/CardHeader"; +import SoundUpload from "./SoundUpload"; + +const SoundUploadCard = () => { + return ( + + + + + ); +}; + +export default SoundUploadCard; diff --git a/src/pages/SystemSettings.tsx b/src/pages/SystemSettings.tsx index ec6d4bd..138070c 100644 --- a/src/pages/SystemSettings.tsx +++ b/src/pages/SystemSettings.tsx @@ -8,6 +8,8 @@ import ModemCard from "../components/SettingForms/WiFi&Modem/ModemCard"; import SystemCard from "../components/SettingForms/System/SystemCard"; import { Toaster } from "sonner"; import { useNPEDAuth } from "../hooks/useNPEDAuth"; +import SoundSettingsCard from "../components/SettingForms/Sound/SoundSettingsCard"; +import SoundUploadCard from "../components/SettingForms/Sound/SoundUploadCard"; const SystemSettings = () => { useNPEDAuth(); @@ -20,6 +22,7 @@ const SystemSettings = () => { Output Integrations WiFi and Modem + Sound
@@ -43,6 +46,12 @@ const SystemSettings = () => {
+ +
+ + +
+
diff --git a/src/types/types.ts b/src/types/types.ts index e70d706..2034252 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -261,3 +261,20 @@ export type ZoomLevel = { py: number; level?: number; }; + +export type SoundValue = string; + +export type Hotlist = { + name: string; + sound: SoundValue; +}; + +export type FormValues = { + sightingSound: SoundValue; + NPEDsound: SoundValue; + hotlists: Hotlist[]; +}; + +export type SoundUploadValue = { + soundFile: File | null; +}; From 2aeae761f89ec8972dc72ce053fddf70f5fe92bf Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Tue, 30 Sep 2025 14:51:37 +0100 Subject: [PATCH 07/22] added sound context, and functionality to select sighting sound --- src/App.tsx | 35 +++++----- src/assets/sounds/ui/notification.mp3 | Bin 0 -> 13837 bytes .../Sound/SoundSettingsFields.tsx | 10 +-- src/context/SoundContext.ts | 18 +++++ .../providers/SoundContextProvider.tsx | 18 +++++ src/context/reducers/SoundContextReducer.ts | 19 ++++++ src/hooks/useSightingFeed.ts | 20 +++++- src/hooks/useSound.ts | 64 ------------------ src/types/types.ts | 11 +++ 9 files changed, 109 insertions(+), 86 deletions(-) create mode 100644 src/assets/sounds/ui/notification.mp3 create mode 100644 src/context/SoundContext.ts create mode 100644 src/context/providers/SoundContextProvider.tsx create mode 100644 src/context/reducers/SoundContextReducer.ts diff --git a/src/App.tsx b/src/App.tsx index 67ea871..3d9f929 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,25 +8,28 @@ import Session from "./pages/Session"; import { NPEDUserProvider } from "./context/providers/NPEDUserContextProvider"; import { AlertHitProvider } from "./context/providers/AlertHitProvider"; import { SoundProvider } from "react-sounds"; +import SoundContextProvider from "./context/providers/SoundContextProvider"; function App() { return ( - - - - - }> - } /> - } /> - } /> - } /> - } /> - } /> - - - - - + + + + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + + ); } diff --git a/src/assets/sounds/ui/notification.mp3 b/src/assets/sounds/ui/notification.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..6fa81e893681be5359c7c190fca590b257decbd9 GIT binary patch literal 13837 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 Ug{}sLW(583d;9;E|Mw&CUjk^((*OVf literal 0 HcmV?d00001 diff --git a/src/components/SettingForms/Sound/SoundSettingsFields.tsx b/src/components/SettingForms/Sound/SoundSettingsFields.tsx index 18a84c9..c8a712e 100644 --- a/src/components/SettingForms/Sound/SoundSettingsFields.tsx +++ b/src/components/SettingForms/Sound/SoundSettingsFields.tsx @@ -1,8 +1,10 @@ import { Field, FieldArray, Form, Formik } from "formik"; import FormGroup from "../components/FormGroup"; import type { FormValues, Hotlist } from "../../../types/types"; +import { useSoundContext } from "../../../context/SoundContext"; const SoundSettingsFields = () => { + const { state, dispatch } = useSoundContext(); const hotlists: Hotlist[] = [ { name: "hotlist0", sound: "" }, { name: "hotlist1", sound: "" }, @@ -25,15 +27,15 @@ const SoundSettingsFields = () => { ]; const initialValues: FormValues = { - sightingSound: "switch", - NPEDsound: "popup", + sightingSound: state.sightingSound ?? "switch", + NPEDsound: state.NPEDsound ?? "popup", hotlists, }; const handleSubmit = (values: FormValues) => { - console.log(values); + dispatch({ type: "UPDATE", payload: values }); }; - + console.log(state); return ( {({ values }) => ( diff --git a/src/context/SoundContext.ts b/src/context/SoundContext.ts new file mode 100644 index 0000000..e7938f3 --- /dev/null +++ b/src/context/SoundContext.ts @@ -0,0 +1,18 @@ +import { createContext, useContext, type Dispatch } from "react"; +import type { SoundPayload, SoundState } from "../types/types"; + +type SoundContextType = { + state: SoundState; + dispatch: Dispatch; +}; + +export const SoundContext = createContext( + undefined +); + +export const useSoundContext = () => { + const ctx = useContext(SoundContext); + if (!ctx) + throw new Error("useSoundContext must be used within "); + return ctx; +}; diff --git a/src/context/providers/SoundContextProvider.tsx b/src/context/providers/SoundContextProvider.tsx new file mode 100644 index 0000000..b7e1b6a --- /dev/null +++ b/src/context/providers/SoundContextProvider.tsx @@ -0,0 +1,18 @@ +import { useReducer, type ReactNode } from "react"; +import { SoundContext } from "../SoundContext"; +import { initalState, reducer } from "../reducers/SoundContextReducer"; + +type SoundContextProviderProps = { + children: ReactNode; +}; + +const SoundContextProvider = ({ children }: SoundContextProviderProps) => { + const [state, dispatch] = useReducer(reducer, initalState); + return ( + + {children} + + ); +}; + +export default SoundContextProvider; diff --git a/src/context/reducers/SoundContextReducer.ts b/src/context/reducers/SoundContextReducer.ts new file mode 100644 index 0000000..2b7bf4b --- /dev/null +++ b/src/context/reducers/SoundContextReducer.ts @@ -0,0 +1,19 @@ +import type { SoundPayload, SoundState } from "../../types/types"; + +export const initalState: SoundState = { + sightingSound: "switch", + NPEDsound: "popup", + hotlists: [], +}; + +export function reducer(state: SoundState, action: SoundPayload) { + switch (action.type) { + case "UPDATE": + return { + ...state, + sightingSound: action.payload.sightingSound, + NPEDsound: action.payload.NPEDsound, + }; + } + return state; +} diff --git a/src/hooks/useSightingFeed.ts b/src/hooks/useSightingFeed.ts index e880f07..0722cfa 100644 --- a/src/hooks/useSightingFeed.ts +++ b/src/hooks/useSightingFeed.ts @@ -1,8 +1,11 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import type { SightingType } from "../types/types"; import { useSoundOnChange } from "react-sounds"; import switchSound from "../assets/sounds/ui/switch.mp3"; +import popup from "../assets/sounds/ui/popup_open.mp3"; +import notification from "../assets/sounds/ui/notification.mp3"; +import { useSoundContext } from "../context/SoundContext"; async function fetchSighting( url: string | undefined, @@ -13,7 +16,17 @@ async function fetchSighting( return res.json(); } +function getSoundFileName(name: string) { + const sounds: Record = { + switch: switchSound, + popup: popup, + notification: notification, + }; + return sounds[name] ?? null; +} + export function useSightingFeed(url: string | undefined) { + const { state } = useSoundContext(); const [sightings, setSightings] = useState([]); const [selectedRef, setSelectedRef] = useState(null); const [sessionStarted, setSessionStarted] = useState(false); @@ -23,8 +36,11 @@ export function useSightingFeed(url: string | undefined) { const [selectedSighting, setSelectedSighting] = useState( null ); + const soundSrc = useMemo(() => { + return getSoundFileName(state.sightingSound) ?? switchSound; + }, [state.sightingSound]); - useSoundOnChange(switchSound, latestRef, { + useSoundOnChange(soundSrc, latestRef, { volume: 1, }); diff --git a/src/hooks/useSound.ts b/src/hooks/useSound.ts index 21f3455..e69de29 100644 --- a/src/hooks/useSound.ts +++ b/src/hooks/useSound.ts @@ -1,64 +0,0 @@ -// useBeep.ts -import { useEffect, useRef } from "react"; -import { useSoundEnabled } from "react-sounds"; // so it respects your SoundBtn toggle - -/** - * Plays a sound whenever `latestRef` changes. - * - * @param src Path to the sound file - * @param latestRef The primitive value to watch (e.g. sighting.ref) - * @param opts volume: 0..1, enabledOverride: force enable/disable, minGapMs: throttle interval - */ -export function useBeep( - src: string, - latestRef: number | null, - opts?: { volume?: number; enabledOverride?: boolean; minGapMs?: number } -) { - const audioRef = useRef(undefined); - const prevRef = useRef(null); - const lastPlay = useRef(0); - const [enabled] = useSoundEnabled(); - - const minGap = opts?.minGapMs ?? 250; // don’t play more than 4 times/sec - - // Create the audio element once - useEffect(() => { - const a = new Audio(src); - a.preload = "auto"; - if (opts?.volume !== undefined) a.volume = opts.volume; - audioRef.current = a; - return () => { - a.pause(); - }; - }, [src, opts?.volume]); - - // Watch for ref changes - useEffect(() => { - if (latestRef == null) return; - - const canPlay = - (opts?.enabledOverride ?? enabled) && - document.visibilityState === "visible"; - if (!canPlay) { - prevRef.current = latestRef; // consume the change - return; - } - - if (prevRef.current !== null && latestRef !== prevRef.current) { - const now = Date.now(); - if (now - lastPlay.current >= minGap) { - const a = audioRef.current; - if (a) { - try { - a.currentTime = 0; // restart from beginning - void a.play(); // fire and forget - lastPlay.current = now; - } catch (err) { - console.warn("Audio play failed:", err); - } - } - } - } - prevRef.current = latestRef; - }, [latestRef, enabled, opts?.enabledOverride, minGap]); -} diff --git a/src/types/types.ts b/src/types/types.ts index 2034252..2f0cbf9 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -278,3 +278,14 @@ export type FormValues = { export type SoundUploadValue = { soundFile: File | null; }; + +export type SoundState = { + sightingSound: SoundValue; + NPEDsound: SoundValue; + hotlists: Hotlist[]; +}; + +export type SoundPayload = { + type: string; + payload: SoundState; +}; From 1b7b2eec37d51623ff2e2fca8327ca745e8411fd Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Tue, 30 Sep 2025 15:32:00 +0100 Subject: [PATCH 08/22] Implement sound settings update and integrate sound context in sightings widget --- .../SettingForms/Sound/SoundSettingsFields.tsx | 11 +++++------ src/components/SightingsWidget/SightingWidget.tsx | 11 +++++++++-- src/components/UI/NavigationArrow.tsx | 3 ++- src/context/reducers/SoundContextReducer.ts | 4 ++++ src/hooks/useSightingFeed.ts | 15 +++------------ src/utils/utils.ts | 13 +++++++++++++ 6 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/components/SettingForms/Sound/SoundSettingsFields.tsx b/src/components/SettingForms/Sound/SoundSettingsFields.tsx index c8a712e..0b5c5d0 100644 --- a/src/components/SettingForms/Sound/SoundSettingsFields.tsx +++ b/src/components/SettingForms/Sound/SoundSettingsFields.tsx @@ -2,6 +2,7 @@ import { Field, FieldArray, Form, Formik } from "formik"; import FormGroup from "../components/FormGroup"; import type { FormValues, Hotlist } from "../../../types/types"; import { useSoundContext } from "../../../context/SoundContext"; +import { toast } from "sonner"; const SoundSettingsFields = () => { const { state, dispatch } = useSoundContext(); @@ -34,6 +35,7 @@ const SoundSettingsFields = () => { const handleSubmit = (values: FormValues) => { dispatch({ type: "UPDATE", payload: values }); + toast.success("Sound settings updated"); }; console.log(state); return ( @@ -70,20 +72,18 @@ const SoundSettingsFields = () => { ))} -

Hotlist Sounds

- ( -
+
{values.hotlists.length > 0 ? ( values.hotlists.map((hotlist, index) => (
)) ) : ( - <>No hotlists yet, Add one +

No hotlists yet, Add one

)}
)} />
- diff --git a/src/components/SightingsWidget/SightingWidget.tsx b/src/components/SightingsWidget/SightingWidget.tsx index 2ea65b7..ee5936d 100644 --- a/src/components/SightingsWidget/SightingWidget.tsx +++ b/src/components/SightingsWidget/SightingWidget.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { SightingType } from "../../types/types"; -import { BLANK_IMG, getSoundFileName } from "../../utils/utils"; +import { BLANK_IMG, getSoundFileURL } from "../../utils/utils"; import NumberPlate from "../PlateStack/NumberPlate"; import Card from "../UI/Card"; import CardHeader from "../UI/CardHeader"; @@ -43,8 +43,8 @@ export default function SightingHistoryWidget({ const { state } = useSoundContext(); const soundSrc = useMemo(() => { - return getSoundFileName(state.sightingSound) ?? popup; - }, [state.sightingSound]); + return getSoundFileURL(state.NPEDsound) ?? popup; + }, [state.NPEDsound]); const { play } = useSound(soundSrc); const { diff --git a/src/context/SoundContext.ts b/src/context/SoundContext.ts index e7938f3..d115d42 100644 --- a/src/context/SoundContext.ts +++ b/src/context/SoundContext.ts @@ -1,9 +1,9 @@ import { createContext, useContext, type Dispatch } from "react"; -import type { SoundPayload, SoundState } from "../types/types"; +import type { SoundAction, SoundState } from "../types/types"; type SoundContextType = { state: SoundState; - dispatch: Dispatch; + dispatch: Dispatch; }; export const SoundContext = createContext( diff --git a/src/context/providers/SoundContextProvider.tsx b/src/context/providers/SoundContextProvider.tsx index b7e1b6a..20211df 100644 --- a/src/context/providers/SoundContextProvider.tsx +++ b/src/context/providers/SoundContextProvider.tsx @@ -1,17 +1,16 @@ -import { useReducer, type ReactNode } from "react"; +import { useMemo, useReducer, type ReactNode } from "react"; import { SoundContext } from "../SoundContext"; -import { initalState, reducer } from "../reducers/SoundContextReducer"; +import { initialState, reducer } from "../reducers/SoundContextReducer"; type SoundContextProviderProps = { children: ReactNode; }; const SoundContextProvider = ({ children }: SoundContextProviderProps) => { - const [state, dispatch] = useReducer(reducer, initalState); + const [state, dispatch] = useReducer(reducer, initialState); + const value = useMemo(() => ({ state, dispatch }), [state, dispatch]); return ( - - {children} - + {children} ); }; diff --git a/src/context/reducers/SoundContextReducer.ts b/src/context/reducers/SoundContextReducer.ts index 1887518..d5b4b2c 100644 --- a/src/context/reducers/SoundContextReducer.ts +++ b/src/context/reducers/SoundContextReducer.ts @@ -1,14 +1,19 @@ -import type { SoundPayload, SoundState } from "../../types/types"; +import type { SoundAction, SoundState } from "../../types/types"; -export const initalState: SoundState = { +export const initialState: SoundState = { sightingSound: "switch", NPEDsound: "popup", hotlists: [], + soundOptions: [ + { name: "switch (Default)", soundFile: null }, + { name: "popup", soundFile: null }, + { name: "notification", soundFile: null }, + ], }; -export function reducer(state: SoundState, action: SoundPayload) { +export function reducer(state: SoundState, action: SoundAction): SoundState { switch (action.type) { - case "UPDATE": + case "UPDATE": { return { ...state, sightingSound: action.payload.sightingSound, @@ -18,6 +23,16 @@ export function reducer(state: SoundState, action: SoundPayload) { sound: hotlist.sound, })), }; + } + + case "ADD": { + return { + ...state, + soundOptions: [...(state.soundOptions ?? []), action.payload], + }; + } + + default: + return state; } - return state; } diff --git a/src/hooks/useSightingFeed.ts b/src/hooks/useSightingFeed.ts index 102fb46..8f742ff 100644 --- a/src/hooks/useSightingFeed.ts +++ b/src/hooks/useSightingFeed.ts @@ -2,9 +2,8 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import type { SightingType } from "../types/types"; import { useSoundOnChange } from "react-sounds"; - import { useSoundContext } from "../context/SoundContext"; -import { getSoundFileName } from "../utils/utils"; +import { getSoundFileURL } from "../utils/utils"; import switchSound from "../assets/sounds/ui/switch.mp3"; async function fetchSighting( @@ -27,11 +26,22 @@ export function useSightingFeed(url: string | undefined) { const [selectedSighting, setSelectedSighting] = useState( null ); + const first = useRef(true); + + const trigger = useMemo(() => { + if (latestRef == null) return null; + if (first.current) { + first.current = false; + return Symbol("skip"); + } + return latestRef; + }, [latestRef]); const soundSrc = useMemo(() => { - return getSoundFileName(state.sightingSound) ?? switchSound; + return getSoundFileURL(state.sightingSound) ?? switchSound; }, [state.sightingSound]); - useSoundOnChange(soundSrc, latestRef, { + //use latestref instead of trigger to revert back + useSoundOnChange(soundSrc, trigger, { volume: 1, }); diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index b46ffe1..fb0182a 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -7,7 +7,6 @@ const Dashboard = () => { const mode = import.meta.env.MODE; const base_url = `${CAM_BASE}/SightingList/sightingSummary?mostRecentRef=`; console.log(mode); - console.log(base_url); return (
diff --git a/src/types/types.ts b/src/types/types.ts index 2f0cbf9..c3036aa 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -276,6 +276,7 @@ export type FormValues = { }; export type SoundUploadValue = { + name: string; soundFile: File | null; }; @@ -283,9 +284,21 @@ export type SoundState = { sightingSound: SoundValue; NPEDsound: SoundValue; hotlists: Hotlist[]; + soundOptions?: SoundUploadValue[]; }; -export type SoundPayload = { - type: string; - payload: SoundState; +type UpdateAction = { + type: "UPDATE"; + payload: { + sightingSound: SoundValue; + NPEDsound: SoundValue; + hotlists: Hotlist[]; + }; }; + +type AddAction = { + type: "ADD"; + payload: SoundUploadValue; +}; + +export type SoundAction = UpdateAction | AddAction; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index fd4a7d3..296a4a3 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -2,7 +2,7 @@ import switchSound from "../assets/sounds/ui/switch.mp3"; import popup from "../assets/sounds/ui/popup_open.mp3"; import notification from "../assets/sounds/ui/notification.mp3"; -export function getSoundFileName(name: string) { +export function getSoundFileURL(name: string) { const sounds: Record = { switch: switchSound, popup: popup, From 82ef562046c9ca226725d52f2cfe54a5e7d094e8 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Thu, 2 Oct 2025 16:07:05 +0100 Subject: [PATCH 12/22] camera zoom handling across components; unify zoom level type and improve state management --- .../CameraOverview/SnapshotContainer.tsx | 43 ++--- .../CameraSettings/CameraSettingFields.tsx | 168 +++++++----------- .../CameraSettings/CameraSettings.tsx | 6 +- .../OverviewVideoContainer.tsx | 5 +- src/hooks/useCameraZoom.ts | 30 +++- src/pages/FrontCamera.tsx | 11 +- src/types/types.ts | 4 + 7 files changed, 111 insertions(+), 156 deletions(-) diff --git a/src/components/CameraOverview/SnapshotContainer.tsx b/src/components/CameraOverview/SnapshotContainer.tsx index 7d85dad..c7a26b9 100644 --- a/src/components/CameraOverview/SnapshotContainer.tsx +++ b/src/components/CameraOverview/SnapshotContainer.tsx @@ -1,5 +1,5 @@ import { useGetOverviewSnapshot } from "../../hooks/useGetOverviewSnapshot"; -import type { ZoomInOptions, ZoomLevel } from "../../types/types"; +import type { ZoomInOptions } from "../../types/types"; import NavigationArrow from "../UI/NavigationArrow"; import { useCameraZoom } from "../../hooks/useCameraZoom"; import { useEffect } from "react"; @@ -7,8 +7,8 @@ import { useEffect } from "react"; type SnapshotContainerProps = { side: string; settingsPage?: boolean; - zoomLevel?: ZoomLevel; - onZoomLevelChange?: (level: ZoomLevel) => void; + zoomLevel?: number; + onZoomLevelChange?: (level: number) => void; }; export const SnapshotContainer = ({ @@ -18,50 +18,29 @@ export const SnapshotContainer = ({ onZoomLevelChange, }: SnapshotContainerProps) => { const { canvasRef, isError, isPending } = useGetOverviewSnapshot(side); - const { mutation } = useCameraZoom(); const cameraControllerSide = side === "CameraA" ? "CameraControllerA" : "CameraControllerB"; + const { mutation } = useCameraZoom({ camera: cameraControllerSide }); - const handleZoomClick = (event: React.MouseEvent) => { - const bounds = canvasRef.current?.getBoundingClientRect(); - if (!bounds) return; - const left = bounds.left; - const top = bounds.top; - const x = event.pageX; - const y = event.pageY; - const cw = canvasRef.current?.clientWidth; - const ch = canvasRef.current?.clientHeight; - if (!cw || !ch) return; - const px = x / cw; - const py = y / ch; - - const baseLevel = zoomLevel?.level ?? 1; + const handleZoomClick = () => { + const baseLevel = zoomLevel ?? 1; const newLevel = baseLevel >= 8 ? 1 : baseLevel * 2; - if (onZoomLevelChange) - onZoomLevelChange({ - left, - top, - x, - y, - px, - py, - level: newLevel, - }); + if (onZoomLevelChange) onZoomLevelChange(newLevel); - if (!zoomLevel?.level) return; + if (!zoomLevel) return; }; useEffect(() => { - if (zoomLevel?.level) { + if (zoomLevel) { const zoomInOptions: ZoomInOptions = { camera: cameraControllerSide, - multiplier: zoomLevel.level, + multiplier: zoomLevel, }; mutation.mutate(zoomInOptions); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [zoomLevel?.level]); + }, [zoomLevel]); if (isError) return

An error occurred

; if (isPending) return

Loading...

; diff --git a/src/components/CameraSettings/CameraSettingFields.tsx b/src/components/CameraSettings/CameraSettingFields.tsx index 8b5c42e..d530db3 100644 --- a/src/components/CameraSettings/CameraSettingFields.tsx +++ b/src/components/CameraSettings/CameraSettingFields.tsx @@ -4,20 +4,18 @@ import type { CameraSettingErrorValues, CameraSettingValues, ZoomInOptions, - ZoomLevel, } from "../../types/types"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons"; import CardHeader from "../UI/CardHeader"; import { useCameraZoom } from "../../hooks/useCameraZoom"; -import { useCameraBlackboard } from "../../hooks/useCameraBlackboard"; type CameraSettingsProps = { initialData: CameraConfig; updateCameraConfig: (values: CameraSettingValues) => Promise | void; - zoomLevel?: ZoomLevel; - onZoomLevelChange?: (level: ZoomLevel) => void; + zoomLevel?: number; + onZoomLevelChange?: (level: number) => void; }; const CameraSettingFields = ({ @@ -27,9 +25,38 @@ const CameraSettingFields = ({ onZoomLevelChange, }: CameraSettingsProps) => { const [showPwd, setShowPwd] = useState(false); - const { mutation } = useCameraZoom(); - const { mutation: blackboardMuation, query: blackboardQuery } = - useCameraBlackboard(); + const cameraControllerSide = + initialData?.id === "CameraA" ? "CameraControllerA" : "CameraControllerB"; + const { mutation, query } = useCameraZoom({ camera: cameraControllerSide }); + const zoomOptions = [1, 2, 4, 8]; + + useEffect(() => { + if (!query.data) return; + const apiZoom = getZoomLevel(query.data); + onZoomLevelChange?.(apiZoom); + }, [query.data, onZoomLevelChange]); + + const getZoomLevel = (levelstring: string | undefined) => { + switch (levelstring) { + case "1x": + return 1; + break; + case "2x": + return 2; + break; + case "4x": + return 4; + break; + case "8x": + return 8; + default: + return 1; + } + }; + const level = getZoomLevel(query.data); + + console.log("level from get", level); + console.log("zoomLevel state", zoomLevel); const initialValues = useMemo( () => ({ friendlyName: initialData?.id ?? "", @@ -37,10 +64,10 @@ const CameraSettingFields = ({ userName: "", password: "", id: initialData?.id, - //TODO: update zoomlevel to query data - zoom: zoomLevel?.level ? zoomLevel.level : 1, + + zoom: zoomLevel, }), - [initialData?.id, initialData?.propURI?.value, zoomLevel?.level] + [initialData?.id, initialData?.propURI?.value, zoomLevel] ); const validateValues = (values: CameraSettingValues) => { @@ -56,12 +83,7 @@ const CameraSettingFields = ({ const handleRadioButtonChange = async (levelNumber: number) => { if (!onZoomLevelChange || !zoomLevel) return; - onZoomLevelChange({ - ...zoomLevel, - level: zoomLevel?.level !== levelNumber ? levelNumber : zoomLevel?.level, - }); - const cameraControllerSide = - initialData?.id === "CameraA" ? "CameraControllerA" : "CameraControllerB"; + onZoomLevelChange(levelNumber); const zoomInOptions: ZoomInOptions = { camera: cameraControllerSide, @@ -69,28 +91,16 @@ const CameraSettingFields = ({ }; mutation.mutate(zoomInOptions); - - if (!blackboardQuery.data.cameraZoom) { - blackboardMuation.mutate({ - operation: "INSERT", - path: "cameraZoom", - value: zoomInOptions, - }); - } else { - blackboardMuation.mutate({ - operation: "APPEND", - path: "cameraZoom", - value: zoomInOptions, - }); - } }; - + const selectedZoom = zoomLevel ?? 1; + console.log(selectedZoom); return ( {({ errors, touched }) => ( @@ -169,78 +179,28 @@ const CameraSettingFields = ({
-
-
- handleRadioButtonChange(1)} - /> - -
- -
- handleRadioButtonChange(2)} - /> - -
- -
- handleRadioButtonChange(4)} - /> - -
- -
- handleRadioButtonChange(8)} - /> - -
+
+ {zoomOptions.map((zoom) => ( +
+ handleRadioButtonChange(zoom)} + /> + +
+ ))}
-
+ ); }; diff --git a/src/components/SettingForms/WiFi&Modem/WiFiSettingsForm.tsx b/src/components/SettingForms/WiFi&Modem/WiFiSettingsForm.tsx new file mode 100644 index 0000000..3f81cd0 --- /dev/null +++ b/src/components/SettingForms/WiFi&Modem/WiFiSettingsForm.tsx @@ -0,0 +1,106 @@ +import { Field, Form, Formik } from "formik"; +import FormGroup from "../components/FormGroup"; +import type { WifiSettingValues } from "../../../types/types"; +import { useWifiAndModem } from "../../../hooks/useCameraWifiandModem"; +import { toast } from "sonner"; + +const WiFiSettingsForm = () => { + const { wifiQuery, wifiMutation } = useWifiAndModem(); + + const wifiSSID = wifiQuery?.data?.propSSID?.value; + const wifiPassword = wifiQuery?.data?.propPassword?.value; + + const initialValues = { + ssid: wifiSSID ?? "", + password: wifiPassword ?? "", + encryption: "WPA2", + }; + + const handleSubmit = (values: WifiSettingValues) => { + const wifiConfig = { + id: "ModemAndWifiManager-wifi", + configHash: "206890572", + propSSID: { + value: values.ssid, + datatype: "java.lang.String", + }, + propPassword: { + value: values.password, + datatype: "java.lang.String", + }, + }; + + wifiMutation.mutate(wifiConfig); + //todo: check what response is + toast.success("WiFi settings updated"); + }; + return ( + + {() => ( + + + + + + + + + + + + + + + + + + + + + )} + + ); +}; + +export default WiFiSettingsForm; diff --git a/src/hooks/useCameraWifiandModem.ts b/src/hooks/useCameraWifiandModem.ts new file mode 100644 index 0000000..e256080 --- /dev/null +++ b/src/hooks/useCameraWifiandModem.ts @@ -0,0 +1,45 @@ +import { useQuery, useMutation } from "@tanstack/react-query"; +import { CAM_BASE } from "../utils/config"; +import type { WifiConfig } from "../types/types"; + +const getWiFiSettings = async () => { + const response = await fetch( + `${CAM_BASE}/api/fetch-config?id=ModemAndWifiManager-wifi` + ); + if (!response.ok) { + throw new Error("Cannot fetch Wifi settings"); + } + return response.json(); +}; + +const updateWifiSettings = async (wifiConfig: WifiConfig) => { + const response = await fetch( + `${CAM_BASE}/api/update-config?id=ModemAndWifiManager-wifi`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(wifiConfig), + } + ); + if (!response.ok) { + throw new Error("Cannot update wifi settings"); + } + return response.json(); +}; + +export const useWifiAndModem = () => { + const wifiQuery = useQuery({ + queryKey: ["getWifiSettings"], + queryFn: getWiFiSettings, + }); + + const wifiMutation = useMutation({ + mutationKey: ["updateWifiSettings"], + mutationFn: (wifiConfig: WifiConfig) => updateWifiSettings(wifiConfig), + }); + + return { + wifiQuery, + wifiMutation, + }; +}; diff --git a/src/types/types.ts b/src/types/types.ts index e70d706..cbe327b 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -261,3 +261,22 @@ export type ZoomLevel = { py: number; level?: number; }; + +export type WifiSettingValues = { + ssid: string; + password: string; + encryption: string; +}; + +export type WifiConfig = { + id: string; + configHash: string; + propSSID: { + value: string; + datatype: string; + }; + propPassword: { + value: string; + datatype: string; + }; +}; diff --git a/src/utils/config.ts b/src/utils/config.ts index f403ad1..3cd72f3 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,4 +1,4 @@ -const rawCamBase = import.meta.env.VITE_OUTSIDE_BASEURL; +const rawCamBase = import.meta.env.VITE_CAM_BASE; export const CAM_BASE = rawCamBase && rawCamBase.trim().length > 0 ? rawCamBase From a972234e226bce224c856662152dc941b427e6b1 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Fri, 3 Oct 2025 10:19:49 +0100 Subject: [PATCH 14/22] modem settings management: integrate ModemSettings and ModemToggle components, update hooks for modem configuration, and enhance WiFiSettingsForm submission handling. --- .../SettingForms/WiFi&Modem/ModemCard.tsx | 89 +---------- .../SettingForms/WiFi&Modem/ModemSettings.tsx | 148 ++++++++++++++++++ .../SettingForms/WiFi&Modem/ModemToggle.tsx | 30 ++++ .../WiFi&Modem/WiFiSettingsForm.tsx | 23 +-- src/components/UI/Header.tsx | 2 +- src/hooks/useCameraWifiandModem.ts | 40 ++++- src/types/types.ts | 31 ++-- 7 files changed, 257 insertions(+), 106 deletions(-) create mode 100644 src/components/SettingForms/WiFi&Modem/ModemSettings.tsx create mode 100644 src/components/SettingForms/WiFi&Modem/ModemToggle.tsx diff --git a/src/components/SettingForms/WiFi&Modem/ModemCard.tsx b/src/components/SettingForms/WiFi&Modem/ModemCard.tsx index 79bcc52..dcc2dfc 100644 --- a/src/components/SettingForms/WiFi&Modem/ModemCard.tsx +++ b/src/components/SettingForms/WiFi&Modem/ModemCard.tsx @@ -1,96 +1,13 @@ import Card from "../../UI/Card"; import CardHeader from "../../UI/CardHeader"; -import { useState } from "react"; -import FormGroup from "../components/FormGroup"; +import ModemSettings from "./ModemSettings"; const ModemCard = () => { - const [apn, setApn] = useState(""); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [authType, setAuthType] = useState("PAP"); - return ( - // TODO: Add switch for Auto vs Manual settings -
- - - setApn(e.target.value)} - /> - - - - setUsername(e.target.value)} - /> - - - - setPassword(e.target.value)} - /> - - - - - - -
+ +
); }; diff --git a/src/components/SettingForms/WiFi&Modem/ModemSettings.tsx b/src/components/SettingForms/WiFi&Modem/ModemSettings.tsx new file mode 100644 index 0000000..ccf6402 --- /dev/null +++ b/src/components/SettingForms/WiFi&Modem/ModemSettings.tsx @@ -0,0 +1,148 @@ +import { Formik, Form, Field } from "formik"; +import FormGroup from "../components/FormGroup"; +import type { ModemSettingsType } from "../../../types/types"; +import { useWifiAndModem } from "../../../hooks/useCameraWifiandModem"; +import { useEffect, useState } from "react"; +import ModemToggle from "./ModemToggle"; +import { toast } from "sonner"; + +const ModemSettings = () => { + const [showSettings, setShowSettings] = useState(false); + const { modemQuery, modemMutation } = useWifiAndModem(); + + const apn = modemQuery?.data?.propAPN?.value; + const username = modemQuery?.data?.propUsername.value; + const password = modemQuery?.data?.propPassword?.value; + const mode = modemQuery?.data?.propMode?.value; + + useEffect(() => { + setShowSettings(mode === "AUTO"); + }, [mode]); + + const inititalValues = { + apn: apn ?? "", + username: username ?? "", + password: password ?? "", + authenticationType: "PAP", + }; + + const handleSubmit = (values: ModemSettingsType) => { + const modemConfig = { + id: "ModemAndWifiManager-modem", + fields: [ + { + property: "propAPN", + value: values.apn, + }, + { + property: "propPassword", + value: values.password, + }, + { + property: "propUsername", + value: values.username, + }, + + { + property: "propMode", + value: showSettings ? "AUTO" : "MANUAL", + }, + ], + }; + modemMutation.mutate(modemConfig); + if (modemMutation.error) { + toast.error("Failed to update modem settings"); + return; + } + toast.success("Modem settings updated"); + }; + + return ( + <> + + {!showSettings && ( + +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ )} + + ); +}; + +export default ModemSettings; diff --git a/src/components/SettingForms/WiFi&Modem/ModemToggle.tsx b/src/components/SettingForms/WiFi&Modem/ModemToggle.tsx new file mode 100644 index 0000000..dab4f41 --- /dev/null +++ b/src/components/SettingForms/WiFi&Modem/ModemToggle.tsx @@ -0,0 +1,30 @@ +type ModemToggleProps = { + showSettings: boolean; + onShowSettings: (showSettings: boolean) => void; +}; + +const ModemToggle = ({ showSettings, onShowSettings }: ModemToggleProps) => { + return ( +
+ +
+ ); +}; + +export default ModemToggle; diff --git a/src/components/SettingForms/WiFi&Modem/WiFiSettingsForm.tsx b/src/components/SettingForms/WiFi&Modem/WiFiSettingsForm.tsx index 3f81cd0..6d1588b 100644 --- a/src/components/SettingForms/WiFi&Modem/WiFiSettingsForm.tsx +++ b/src/components/SettingForms/WiFi&Modem/WiFiSettingsForm.tsx @@ -19,19 +19,24 @@ const WiFiSettingsForm = () => { const handleSubmit = (values: WifiSettingValues) => { const wifiConfig = { id: "ModemAndWifiManager-wifi", - configHash: "206890572", - propSSID: { - value: values.ssid, - datatype: "java.lang.String", - }, - propPassword: { - value: values.password, - datatype: "java.lang.String", - }, + fields: [ + { + property: "propSSID", + value: values.ssid, + }, + { + property: "propPassword", + value: values.password, + }, + ], }; wifiMutation.mutate(wifiConfig); //todo: check what response is + if (wifiMutation.error) { + toast.error("Failed to update WiFi settings"); + return; + } toast.success("WiFi settings updated"); }; return ( diff --git a/src/components/UI/Header.tsx b/src/components/UI/Header.tsx index 2de4734..daceab3 100644 --- a/src/components/UI/Header.tsx +++ b/src/components/UI/Header.tsx @@ -32,7 +32,7 @@ export default function Header() { }; return ( -
+
Logo diff --git a/src/hooks/useCameraWifiandModem.ts b/src/hooks/useCameraWifiandModem.ts index e256080..d501a6b 100644 --- a/src/hooks/useCameraWifiandModem.ts +++ b/src/hooks/useCameraWifiandModem.ts @@ -1,6 +1,6 @@ import { useQuery, useMutation } from "@tanstack/react-query"; import { CAM_BASE } from "../utils/config"; -import type { WifiConfig } from "../types/types"; +import type { ModemConfig, WifiConfig } from "../types/types"; const getWiFiSettings = async () => { const response = await fetch( @@ -27,6 +27,31 @@ const updateWifiSettings = async (wifiConfig: WifiConfig) => { return response.json(); }; +const getModemSettings = async () => { + const response = await fetch( + `${CAM_BASE}/api/fetch-config?id=ModemAndWifiManager-modem` + ); + if (!response.ok) { + throw new Error("Cannot fetch modem settings"); + } + return response.json(); +}; + +const updateModemSettings = async (modemConfig: ModemConfig) => { + const response = await fetch( + `${CAM_BASE}/api/update-config?id=ModemAndWifiManager-modem`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(modemConfig), + } + ); + if (!response.ok) { + throw new Error("cannot update modem settings"); + } + return response.json(); +}; + export const useWifiAndModem = () => { const wifiQuery = useQuery({ queryKey: ["getWifiSettings"], @@ -36,10 +61,23 @@ export const useWifiAndModem = () => { const wifiMutation = useMutation({ mutationKey: ["updateWifiSettings"], mutationFn: (wifiConfig: WifiConfig) => updateWifiSettings(wifiConfig), + onError: (error) => console.log(error), + }); + + const modemQuery = useQuery({ + queryKey: ["getModemSettings"], + queryFn: getModemSettings, + }); + + const modemMutation = useMutation({ + mutationKey: ["updateModemSettings"], + mutationFn: (modemConfig: ModemConfig) => updateModemSettings(modemConfig), }); return { wifiQuery, wifiMutation, + modemQuery, + modemMutation, }; }; diff --git a/src/types/types.ts b/src/types/types.ts index 4add9a4..234d015 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -267,19 +267,25 @@ export type WifiSettingValues = { password: string; encryption: string; }; +export type ModemConfigPayload = { + property: string; + value: string; +}; +export type WifiConfigPayload = { + property: string; + value: string; +}; export type WifiConfig = { id: string; - configHash: string; - propSSID: { - value: string; - datatype: string; - }; - propPassword: { - value: string; - datatype: string; - }; + fields: WifiConfigPayload[]; }; + +export type ModemConfig = { + id: string; + fields: ModemConfigPayload[]; +}; + export type ZoomInOptions = { camera: string; multiplier: number; @@ -288,3 +294,10 @@ export type ZoomInOptions = { export type zoomConfig = { camera: string; }; + +export type ModemSettingsType = { + apn: string; + username: string; + password: string; + authenticationType: string; +}; From e047c77cd187148a3443468ddd9e19574b14e6de Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Fri, 3 Oct 2025 13:08:21 +0100 Subject: [PATCH 15/22] Add RTSP URL parsing utility and enhance camera settings handling - Implement parseRTSPUrl function to extract username, password, IP, port, and path from RTSP URLs. - Update CameraSettingFields to utilize parsed RTSP URL data for camera configuration. - Modify WiFiSettingsForm to allow password visibility toggle. - Improve SightingOverview loading and error handling UI. - Adjust NavigationArrow component to reflect updated camera side logic. --- .../CameraSettings/CameraSettingFields.tsx | 9 +++++--- .../WiFi&Modem/WiFiSettingsForm.tsx | 14 +++++++++++-- .../SightingOverview/SightingOverview.tsx | 21 ++++++++++++++++--- src/components/UI/NavigationArrow.tsx | 2 +- src/hooks/useCameraZoom.ts | 6 ++---- src/utils/utils.ts | 17 +++++++++++++++ 6 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/components/CameraSettings/CameraSettingFields.tsx b/src/components/CameraSettings/CameraSettingFields.tsx index d530db3..46211c9 100644 --- a/src/components/CameraSettings/CameraSettingFields.tsx +++ b/src/components/CameraSettings/CameraSettingFields.tsx @@ -10,6 +10,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons"; import CardHeader from "../UI/CardHeader"; import { useCameraZoom } from "../../hooks/useCameraZoom"; +import { parseRTSPUrl } from "../../utils/utils"; type CameraSettingsProps = { initialData: CameraConfig; @@ -30,6 +31,8 @@ const CameraSettingFields = ({ const { mutation, query } = useCameraZoom({ camera: cameraControllerSide }); const zoomOptions = [1, 2, 4, 8]; + const parsed = parseRTSPUrl(initialData?.propURI?.value); + useEffect(() => { if (!query.data) return; const apiZoom = getZoomLevel(query.data); @@ -61,8 +64,8 @@ const CameraSettingFields = ({ () => ({ friendlyName: initialData?.id ?? "", cameraAddress: initialData?.propURI?.value ?? "", - userName: "", - password: "", + userName: parsed?.username ?? "", + password: parsed?.password ?? "", id: initialData?.id, zoom: zoomLevel, @@ -179,7 +182,7 @@ const CameraSettingFields = ({
-
+
{zoomOptions.map((zoom) => (
{ + const [showPwd, setShowPwd] = useState(false); const { wifiQuery, wifiMutation } = useWifiAndModem(); const wifiSSID = wifiQuery?.data?.propSSID?.value; @@ -32,7 +36,7 @@ const WiFiSettingsForm = () => { }; wifiMutation.mutate(wifiConfig); - //todo: check what response is + if (wifiMutation.error) { toast.error("Failed to update WiFi settings"); return; @@ -72,10 +76,16 @@ const WiFiSettingsForm = () => { + setShowPwd((s) => !s)} + icon={showPwd ? faEyeSlash : faEye} + />