Compare commits
20 Commits
bugfix/hos
...
bugfix/min
| Author | SHA1 | Date | |
|---|---|---|---|
| ae0a6f9249 | |||
| e46460f41d | |||
| 6c441a0a4b | |||
| 2d5b264041 | |||
| 251a2f5e7b | |||
| 18534ceb2c | |||
| 9975e6a6ca | |||
| c83122cd52 | |||
| abc8007fc6 | |||
| 7903633809 | |||
| 359f3781f2 | |||
| a958901bed | |||
| b58181e551 | |||
| df6bf75184 | |||
| c5cea81532 | |||
| 78905b09e0 | |||
| 1ffad51503 | |||
| d16f55413c | |||
| 3598f8d069 | |||
| 0a3a543d6f |
@@ -32,7 +32,8 @@
|
||||
"react-tabs": "^6.1.0",
|
||||
"react-use": "^17.6.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwindcss": "^4.1.11"
|
||||
"tailwindcss": "^4.1.11",
|
||||
"use-debounce": "^10.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.32.0",
|
||||
|
||||
@@ -5,7 +5,7 @@ import FrontCamera from "./pages/FrontCamera";
|
||||
import RearCamera from "./pages/RearCamera";
|
||||
import SystemSettings from "./pages/SystemSettings";
|
||||
import Session from "./pages/Session";
|
||||
import { NPEDUserProvider } from "./context/providers/NPEDUserContextProvider";
|
||||
import { IntegrationsProvider } from "./context/providers/IntegrationsContextProvider";
|
||||
import { AlertHitProvider } from "./context/providers/AlertHitProvider";
|
||||
import { SoundProvider } from "react-sounds";
|
||||
import SoundContextProvider from "./context/providers/SoundContextProvider";
|
||||
@@ -14,7 +14,7 @@ function App() {
|
||||
return (
|
||||
<SoundContextProvider>
|
||||
<SoundProvider initialEnabled={true}>
|
||||
<NPEDUserProvider>
|
||||
<IntegrationsProvider>
|
||||
<AlertHitProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<Container />}>
|
||||
@@ -27,7 +27,7 @@ function App() {
|
||||
</Route>
|
||||
</Routes>
|
||||
</AlertHitProvider>
|
||||
</NPEDUserProvider>
|
||||
</IntegrationsProvider>
|
||||
</SoundProvider>
|
||||
</SoundContextProvider>
|
||||
);
|
||||
|
||||
BIN
src/assets/sounds/ui/Attention.wav
Normal file
BIN
src/assets/sounds/ui/Attention.wav
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,30 +1,36 @@
|
||||
import Card from "../UI/Card";
|
||||
import CardHeader from "../UI/CardHeader";
|
||||
import { useNPEDContext } from "../../context/NPEDUserContext";
|
||||
import { useIntegrationsContext } from "../../context/IntegrationsContext";
|
||||
import type { ReducedSightingType } from "../../types/types";
|
||||
import { toast } from "sonner";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faFloppyDisk, faPause, faPlay, faStop } from "@fortawesome/free-solid-svg-icons";
|
||||
import VehicleSessionItem from "../UI/VehicleSessionItem";
|
||||
import { useCameraBlackboard } from "../../hooks/useCameraBlackboard";
|
||||
|
||||
const SessionCard = () => {
|
||||
const { sessionStarted, setSessionStarted, sessionList } = useNPEDContext();
|
||||
const { state, dispatch } = useIntegrationsContext();
|
||||
const { mutation } = useCameraBlackboard();
|
||||
|
||||
const handleStartClick = () => {
|
||||
setSessionStarted(!sessionStarted);
|
||||
toast(`${sessionStarted ? "Vehicle tracking session Ended" : "Vehicle tracking session Started"}`);
|
||||
};
|
||||
const sessionStarted = state.sessionStarted;
|
||||
const sessionPaused = state.sessionPaused;
|
||||
const sessionList = state.sessionList;
|
||||
|
||||
const sightings = [...new Map(sessionList.map((vehicle) => [vehicle.vrm, vehicle]))];
|
||||
const sightings = [...new Map(sessionList?.map((vehicle) => [vehicle.vrm, vehicle]))];
|
||||
|
||||
const dedupedSightings = sightings.map((sighting) => sighting[1]);
|
||||
|
||||
const vehicles = dedupedSightings.reduce<Record<string, ReducedSightingType[]>>(
|
||||
(acc, item) => {
|
||||
const hotlisthit = Object.values(item.metadata?.hotlistMatches ?? {}).includes(true);
|
||||
if (item.metadata?.npedJSON["NPED CATEGORY"] === "A") acc.npedCatA.push(item);
|
||||
if (item.metadata?.npedJSON["NPED CATEGORY"] === "B") acc.npedCatB.push(item);
|
||||
if (item.metadata?.npedJSON["NPED CATEGORY"] === "C") acc.npedCatC.push(item);
|
||||
if (item.metadata?.npedJSON["NPED CATEGORY"] === "D") acc.npedCatD.push(item);
|
||||
if (item.metadata?.npedJSON["TAX STATUS"] === false) acc.notTaxed.push(item);
|
||||
if (item.metadata?.npedJSON["MOT STATUS"] === false) acc.notMOT.push(item);
|
||||
|
||||
if (hotlisthit) acc.hotlistHit.push(item);
|
||||
acc.vehicles.push(item);
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
@@ -34,9 +40,32 @@ const SessionCard = () => {
|
||||
npedCatD: [],
|
||||
notTaxed: [],
|
||||
notMOT: [],
|
||||
hotlistHit: [],
|
||||
vehicles: [],
|
||||
}
|
||||
);
|
||||
|
||||
const handleStartClick = () => {
|
||||
dispatch({ type: "SESSIONSTART", payload: !sessionStarted });
|
||||
dispatch({ type: "SESSIONPAUSE", payload: false });
|
||||
toast(`${sessionStarted ? "Vehicle tracking session ended" : "Vehicle tracking session started"}`);
|
||||
};
|
||||
|
||||
const handlepauseClick = () => {
|
||||
dispatch({ type: "SESSIONPAUSE", payload: !sessionPaused });
|
||||
toast(`${sessionStarted ? "Vehicle tracking session paused" : "Vehicle tracking session resumed"}`);
|
||||
};
|
||||
|
||||
const handleSaveCick = async () => {
|
||||
const result = await mutation.mutateAsync({
|
||||
operation: "INSERT",
|
||||
path: "sessionStats",
|
||||
value: dedupedSightings,
|
||||
});
|
||||
|
||||
if (result.reason === "OK") toast.success("Session saved");
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-4 col-span-3">
|
||||
<CardHeader title="Session" />
|
||||
@@ -47,34 +76,72 @@ const SessionCard = () => {
|
||||
} transition w-full`}
|
||||
onClick={handleStartClick}
|
||||
>
|
||||
{sessionStarted ? "End Session" : "Start Session"}
|
||||
<div className="flex flex-row gap-3 items-center justify-self-center">
|
||||
<FontAwesomeIcon icon={sessionStarted ? faStop : faPlay} />
|
||||
<p>{sessionStarted ? "End Session" : "Start Session"}</p>
|
||||
</div>
|
||||
</button>
|
||||
<div className="flex flex-col lg:flex-row gap-5">
|
||||
{sessionStarted && (
|
||||
<button
|
||||
className={`bg-blue-600 text-white px-4 py-2 rounded transition w-full lg:w-[50%]`}
|
||||
onClick={handleSaveCick}
|
||||
>
|
||||
<div className="flex flex-row gap-3 items-center justify-self-center">
|
||||
<FontAwesomeIcon icon={faFloppyDisk} />
|
||||
<p>Save session</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
{sessionStarted && (
|
||||
<button
|
||||
className={`bg-gray-300 text-gray-800 px-4 py-2 rounded transition w-full lg:w-[50%]`}
|
||||
onClick={handlepauseClick}
|
||||
>
|
||||
<div className="flex flex-row gap-3 items-center justify-self-center">
|
||||
<FontAwesomeIcon icon={sessionPaused ? faPlay : faPause} />
|
||||
<p>{sessionPaused ? "Resume session" : "Pause session"}</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ul className="text-white space-y-2">
|
||||
<li className="rounded-xl border border-slate-800 bg-slate-800/60 p-3 shadow-sm flex flex-row justify-between">
|
||||
<p>Number of Vehicles:</p>
|
||||
<span className="font-bold text-green-600 text-xl">{dedupedSightings.length}</span>
|
||||
</li>
|
||||
<li className="rounded-xl border border-slate-800 bg-slate-800/60 p-3 shadow-sm flex flex-row justify-between">
|
||||
<p>Vehicles without Tax:</p>
|
||||
<span className="font-bold text-amber-600 text-xl">{vehicles.notTaxed.length}</span>
|
||||
</li>
|
||||
<li className="rounded-xl border border-slate-800 bg-slate-800/60 p-3 shadow-sm flex flex-row justify-between">
|
||||
<p>Vehicles without MOT:</p>{" "}
|
||||
<span className="font-bold text-red-500 text-xl">{vehicles.notMOT.length}</span>
|
||||
</li>
|
||||
<li className="rounded-xl border border-slate-800 bg-slate-800/60 p-3 shadow-sm flex flex-row justify-between">
|
||||
<p>Vehicles with NPED Cat A:</p>
|
||||
<span className="font-bold text-gray-300 text-xl">{vehicles.npedCatA.length}</span>
|
||||
</li>
|
||||
<li className="rounded-xl border border-slate-800 bg-slate-800/60 p-3 shadow-sm flex flex-row justify-between">
|
||||
<p>Vehicles with NPED Cat B:</p>{" "}
|
||||
<span className="font-bold text-gray-300text-xl">{vehicles.npedCatB.length}</span>
|
||||
</li>
|
||||
<li className="rounded-xl border border-slate-800 bg-slate-800/60 p-3 shadow-sm flex flex-row justify-between">
|
||||
Vehicles with NPED Cat C:{" "}
|
||||
<span className="font-bold text-gray-300 text-xl">{vehicles.npedCatC.length}</span>
|
||||
</li>
|
||||
<VehicleSessionItem
|
||||
sessionNumber={vehicles.vehicles.length}
|
||||
textColour="text-green-400"
|
||||
vehicleTag={"Number of Vehicles sightings:"}
|
||||
/>
|
||||
<VehicleSessionItem
|
||||
sessionNumber={vehicles.notTaxed.length}
|
||||
textColour="text-amber-400"
|
||||
vehicleTag={"Vehicles without Tax:"}
|
||||
/>
|
||||
<VehicleSessionItem
|
||||
sessionNumber={vehicles.notMOT.length}
|
||||
textColour="text-red-500"
|
||||
vehicleTag={"Vehicles without MOT:"}
|
||||
/>
|
||||
<VehicleSessionItem
|
||||
sessionNumber={vehicles.hotlistHit.length}
|
||||
textColour="text-blue-400"
|
||||
vehicleTag={"Vehicles on Hotlists:"}
|
||||
/>
|
||||
<VehicleSessionItem
|
||||
sessionNumber={vehicles.npedCatA.length}
|
||||
textColour="text-gray-300"
|
||||
vehicleTag={"Vehicles with NPED Cat A:"}
|
||||
/>
|
||||
<VehicleSessionItem
|
||||
sessionNumber={vehicles.npedCatB.length}
|
||||
textColour="text-gray-300"
|
||||
vehicleTag={"Vehicles with NPED Cat B:"}
|
||||
/>
|
||||
<VehicleSessionItem
|
||||
sessionNumber={vehicles.npedCatC.length}
|
||||
textColour="text-gray-300"
|
||||
vehicleTag={"Vehicles with NPED Cat C:"}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -6,16 +6,18 @@ import { toast } from "sonner";
|
||||
import { faEyeSlash, faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useState } from "react";
|
||||
import { useIntegrationsContext } from "../../../context/IntegrationsContext";
|
||||
|
||||
const NPEDFields = () => {
|
||||
const { state } = useIntegrationsContext();
|
||||
const [showPwd, setShowPwd] = useState(false);
|
||||
const { signIn, user, signOut } = useNPEDAuth();
|
||||
const { signIn, signOut } = useNPEDAuth();
|
||||
|
||||
const initialValues = user
|
||||
const initialValues = state.npedUser
|
||||
? {
|
||||
username: user?.propUsername?.value,
|
||||
password: user?.propPassword?.value,
|
||||
clientId: user?.propClientID?.value,
|
||||
username: state.npedUser?.propUsername?.value,
|
||||
password: state.npedUser?.propPassword?.value,
|
||||
clientId: state.npedUser?.propClientID?.value,
|
||||
frontId: "NPED",
|
||||
rearId: "NPED",
|
||||
}
|
||||
@@ -48,20 +50,13 @@ const NPEDFields = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
validate={validateValues}
|
||||
enableReinitialize
|
||||
>
|
||||
<Formik initialValues={initialValues} onSubmit={handleSubmit} validate={validateValues} enableReinitialize>
|
||||
{({ errors, touched, isSubmitting }) => (
|
||||
<Form className="flex flex-col space-y-5 px-2">
|
||||
<FormGroup>
|
||||
<label htmlFor="username">Username</label>
|
||||
{touched.username && errors.username && (
|
||||
<small className="absolute right-0 -top-5 text-red-500">
|
||||
{errors.username}
|
||||
</small>
|
||||
<small className="absolute right-0 -top-5 text-red-500">{errors.username}</small>
|
||||
)}
|
||||
<Field
|
||||
name="username"
|
||||
@@ -82,9 +77,7 @@ const NPEDFields = () => {
|
||||
className="p-2 border border-gray-400 rounded-lg w-full"
|
||||
/>
|
||||
{touched.password && errors.password && (
|
||||
<small className="absolute right-0 -top-5 text-red-500">
|
||||
{errors.password}
|
||||
</small>
|
||||
<small className="absolute right-0 -top-5 text-red-500">{errors.password}</small>
|
||||
)}
|
||||
<FontAwesomeIcon
|
||||
type="button"
|
||||
@@ -97,9 +90,7 @@ const NPEDFields = () => {
|
||||
<FormGroup>
|
||||
<label htmlFor="clientId">Client ID</label>
|
||||
{touched.clientId && errors.clientId && (
|
||||
<small className="absolute right-0 -top-5 text-red-500">
|
||||
{errors.clientId}
|
||||
</small>
|
||||
<small className="absolute right-0 -top-5 text-red-500">{errors.clientId}</small>
|
||||
)}
|
||||
<Field
|
||||
name="clientId"
|
||||
@@ -109,7 +100,7 @@ const NPEDFields = () => {
|
||||
className="p-1.5 border border-gray-400 rounded-lg"
|
||||
/>
|
||||
</FormGroup>
|
||||
{!user?.propClientID?.value ? (
|
||||
{!state.npedUser?.propClientID?.value ? (
|
||||
<button
|
||||
type="submit"
|
||||
className="w-1/4 text-white bg-green-700 hover:bg-green-800 font-small rounded-lg text-sm px-2 py-2.5 hover:cursor-pointer"
|
||||
|
||||
@@ -30,9 +30,11 @@ const SoundSettingsFields = () => {
|
||||
sightingVolume: state.sightingVolume,
|
||||
NPEDsoundVolume: state.NPEDsoundVolume,
|
||||
hotlistSoundVolume: state.hotlistSoundVolume,
|
||||
soundOptions: [...(state.soundOptions ?? [])],
|
||||
};
|
||||
|
||||
dispatch({ type: "UPDATE", payload: updatedValues });
|
||||
|
||||
const result = await mutation.mutateAsync({
|
||||
operation: "INSERT",
|
||||
path: "soundSettings",
|
||||
|
||||
@@ -3,21 +3,47 @@ import FormGroup from "../components/FormGroup";
|
||||
import type { SoundUploadValue } from "../../../types/types";
|
||||
import { useSoundContext } from "../../../context/SoundContext";
|
||||
import { toast } from "sonner";
|
||||
import { useCameraBlackboard } from "../../../hooks/useCameraBlackboard";
|
||||
|
||||
const SoundUpload = () => {
|
||||
const { dispatch } = useSoundContext();
|
||||
const { state, dispatch } = useSoundContext();
|
||||
const { mutation } = useCameraBlackboard();
|
||||
|
||||
const initialValues: SoundUploadValue = {
|
||||
name: "",
|
||||
soundFile: null,
|
||||
soundFileName: "",
|
||||
soundUrl: "",
|
||||
};
|
||||
|
||||
const handleSubmit = (values: SoundUploadValue) => {
|
||||
const handleSubmit = async (values: SoundUploadValue) => {
|
||||
if (!values.soundFile) {
|
||||
toast.warning("Please select an audio file");
|
||||
} else {
|
||||
dispatch({ type: "ADD", payload: values });
|
||||
toast.success("Sound file upload successfully");
|
||||
return;
|
||||
}
|
||||
const alreadyExists = state?.soundOptions?.some((soundOption) => soundOption.name === values.name);
|
||||
if (state.soundOptions?.includes(values) || alreadyExists) {
|
||||
toast.warning("Sound already in list");
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedValues = {
|
||||
...state,
|
||||
soundOptions: [...(state.soundOptions ?? []), values],
|
||||
};
|
||||
|
||||
const result = await mutation.mutateAsync({
|
||||
operation: "INSERT",
|
||||
path: "soundSettings",
|
||||
value: updatedValues,
|
||||
});
|
||||
if (result.reason !== "OK") {
|
||||
toast.error("Cannot update sound settings");
|
||||
} else {
|
||||
toast.success(`${values.name} file added`);
|
||||
}
|
||||
|
||||
dispatch({ type: "ADD", payload: values });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -36,7 +62,10 @@ const SoundUpload = () => {
|
||||
className="mt-4 w-full flex flex-col items-center justify-center rounded-2xl border border-slate-800 bg-slate-900/40 p-10 text-center file:px-3 file:border file:border-gray-500 file:rounded-lg file:bg-blue-800 file:mr-5"
|
||||
onChange={(e) => {
|
||||
if (e.target?.files && e.target?.files[0]?.type === "audio/mpeg") {
|
||||
const url = URL.createObjectURL(e.target.files[0]);
|
||||
setFieldValue("soundUrl", url);
|
||||
setFieldValue("name", e.target.files[0].name);
|
||||
setFieldValue("soundFileName", e.target.files[0].name);
|
||||
setFieldValue("soundFile", e.target.files[0]);
|
||||
} else {
|
||||
setFieldError("soundFile", "Not an mp3 file");
|
||||
|
||||
@@ -23,8 +23,7 @@ const SightingModal = ({ isSightingModalOpen, handleClose, sighting, onDelete }:
|
||||
const { dispatch } = useAlertHitContext();
|
||||
const { query, mutation } = useCameraBlackboard();
|
||||
|
||||
const hotlistName = getHotlistName(sighting?.metadata?.hotlistMatches);
|
||||
|
||||
const hotlistNames = getHotlistName(sighting?.metadata?.hotlistMatches);
|
||||
const handleAcknowledgeButton = () => {
|
||||
try {
|
||||
if (!sighting) {
|
||||
@@ -117,16 +116,6 @@ const SightingModal = ({ isSightingModalOpen, handleClose, sighting, onDelete }:
|
||||
<div className="flex flex-col md:flex-row gap-3 items-center">
|
||||
<NumberPlate vrm={sighting?.vrm} motion={motionAway} />
|
||||
<img src={sighting?.plateUrlColour} alt="plate patch" className="h-16 object-contain rounded-md" />
|
||||
{hotlistName && (
|
||||
<div>
|
||||
<p className="text-gray-300">Hotlist</p>
|
||||
<div className="items-center px-2.5 py-0.5 rounded-sm me-2 bg-amber-500">
|
||||
<p className="font-medium text-2xl break-all text-amber-800">
|
||||
{hotlistName ? hotlistName[0].replace(/\.csv$/i, "") : "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isHotListHit && <img src={HotListImg} alt="hotlistHit" className="h-20 object-contain rounded-md" />}
|
||||
@@ -134,6 +123,20 @@ const SightingModal = ({ isSightingModalOpen, handleClose, sighting, onDelete }:
|
||||
{isNPEDHitB && <img src={NPED_CAT_B} alt="hotlistHit" className="h-20 object-contain rounded-md" />}
|
||||
{isNPEDHitC && <img src={NPED_CAT_C} alt="hotlistHit" className="h-20 object-contain rounded-md" />}
|
||||
</div>
|
||||
{hotlistNames && (
|
||||
<div className="flex flex-col border-b border-gray-600 mb-4">
|
||||
<p className="text-gray-300">Hotlists</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-[90%] lg:gap-x-[15%] w-[50%]">
|
||||
{hotlistNames.map((hotlistName, index) => (
|
||||
<div className="items-center px-2.5 py-0.5 rounded-sm me-2 bg-amber-500 w-55 m-2" key={index}>
|
||||
<p className="font-medium text-2xl break-all text-amber-800">
|
||||
{hotlistName ? hotlistName?.replace(/\.csv$/i, "") : "-"}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col lg:flex-row items-center gap-3">
|
||||
<img
|
||||
src={sighting?.overviewUrl}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { ReducedSightingType, SightingType } from "../../types/types";
|
||||
import type { HitKind, QueuedHit, ReducedSightingType, SightingType } from "../../types/types";
|
||||
import { BLANK_IMG, getSoundFileURL } from "../../utils/utils";
|
||||
import NumberPlate from "../PlateStack/NumberPlate";
|
||||
import Card from "../UI/Card";
|
||||
@@ -15,7 +15,7 @@ import NPED_CAT_C from "/NPED_Cat_C.svg";
|
||||
import popup from "../../assets/sounds/ui/popup_open.mp3";
|
||||
import notification from "../../assets/sounds/ui/notification.mp3";
|
||||
import { useSound } from "react-sounds";
|
||||
import { useNPEDContext } from "../../context/NPEDUserContext";
|
||||
import { useIntegrationsContext } from "../../context/IntegrationsContext";
|
||||
import { useSoundContext } from "../../context/SoundContext";
|
||||
import Loading from "../UI/Loading";
|
||||
import { checkIsHotListHit, getNPEDCategory } from "../../utils/utils";
|
||||
@@ -39,16 +39,25 @@ type SightingHistoryProps = {
|
||||
};
|
||||
|
||||
export default function SightingHistoryWidget({ className, title }: SightingHistoryProps) {
|
||||
const [modalQueue, setModalQueue] = useState<QueuedHit[]>([]);
|
||||
useNow(1000);
|
||||
const { state } = useSoundContext();
|
||||
|
||||
const soundSrcNped = useMemo(() => {
|
||||
if (state?.NPEDsound?.includes(".mp3") || state.NPEDsound?.includes(".wav")) {
|
||||
const file = state.soundOptions?.find((item) => item.name === state.NPEDsound);
|
||||
return file?.soundUrl ?? popup;
|
||||
}
|
||||
return getSoundFileURL(state.NPEDsound) ?? popup;
|
||||
}, [state.NPEDsound]);
|
||||
}, [state.NPEDsound, state.soundOptions]);
|
||||
|
||||
const soundSrcHotlist = useMemo(() => {
|
||||
if (state?.hotlistSound?.includes(".mp3") || state.hotlistSound?.includes(".wav")) {
|
||||
const file = state.soundOptions?.find((item) => item.name === state.hotlistSound);
|
||||
return file?.soundUrl ?? notification;
|
||||
}
|
||||
return getSoundFileURL(state?.hotlistSound) ?? notification;
|
||||
}, [state?.hotlistSound]);
|
||||
}, [state.hotlistSound, state.soundOptions]);
|
||||
|
||||
const { play: npedSound } = useSound(soundSrcNped, { volume: state.NPEDsoundVolume });
|
||||
const { play: hotlistsound } = useSound(soundSrcHotlist, { volume: state.hotlistSoundVolume });
|
||||
@@ -63,13 +72,23 @@ export default function SightingHistoryWidget({ className, title }: SightingHist
|
||||
} = useSightingFeedContext();
|
||||
|
||||
const { dispatch } = useAlertHitContext();
|
||||
const { sessionStarted, setSessionList, sessionList } = useNPEDContext();
|
||||
const { state: integrationState, dispatch: integrationDispatch } = useIntegrationsContext();
|
||||
const sessionStarted = integrationState.sessionStarted;
|
||||
const sessionPaused = integrationState.sessionPaused;
|
||||
|
||||
const processedRefs = useRef<Set<number | string>>(new Set());
|
||||
|
||||
const hasAutoOpenedRef = useRef(false);
|
||||
const npedRef = useRef(false);
|
||||
|
||||
const enqueue = useCallback((sighting: SightingType, kind: HitKind) => {
|
||||
const id = sighting.vrm ?? sighting.ref;
|
||||
if (processedRefs.current.has(id)) return;
|
||||
processedRefs.current.add(id);
|
||||
|
||||
setModalQueue((q) => [...q, { id, sighting, kind }]);
|
||||
}, []);
|
||||
|
||||
const reduceObject = (obj: SightingType): ReducedSightingType => {
|
||||
return {
|
||||
vrm: obj.vrm,
|
||||
@@ -80,11 +99,12 @@ export default function SightingHistoryWidget({ className, title }: SightingHist
|
||||
useEffect(() => {
|
||||
if (sessionStarted) {
|
||||
if (!mostRecent) return;
|
||||
if (sessionPaused) return;
|
||||
const reducedMostRecent = reduceObject(mostRecent);
|
||||
setSessionList([...sessionList, reducedMostRecent]);
|
||||
integrationDispatch({ type: "ADD", payload: reducedMostRecent });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mostRecent, sessionStarted, setSessionList]);
|
||||
}, [mostRecent, sessionStarted]);
|
||||
|
||||
const onRowClick = useCallback(
|
||||
(sighting: SightingType) => {
|
||||
@@ -104,26 +124,15 @@ export default function SightingHistoryWidget({ className, title }: SightingHist
|
||||
const id = sighting.vrm;
|
||||
|
||||
if (processedRefs.current.has(id)) continue;
|
||||
const isHot = checkIsHotListHit(sighting);
|
||||
const cat = sighting?.metadata?.npedJSON?.["NPED CATEGORY"];
|
||||
const isHotlistHit = checkIsHotListHit(sighting);
|
||||
const npedcategory = sighting?.metadata?.npedJSON?.["NPED CATEGORY"];
|
||||
const isNPED = npedcategory === "A" || npedcategory === "B" || npedcategory === "C";
|
||||
|
||||
if (cat === "A" || cat === "B" || cat === "C") {
|
||||
npedSound();
|
||||
setSelectedSighting(sighting);
|
||||
setSightingModalOpen(true);
|
||||
processedRefs.current.add(id);
|
||||
break; // stop after one new open per render cycle
|
||||
}
|
||||
|
||||
if (isHot) {
|
||||
hotlistsound();
|
||||
setSelectedSighting(sighting);
|
||||
setSightingModalOpen(true);
|
||||
processedRefs.current.add(id);
|
||||
break;
|
||||
if (isNPED || isHotlistHit) {
|
||||
enqueue(sighting, isNPED ? "NPED" : "HOTLIST"); // enqueue ONLY
|
||||
}
|
||||
}
|
||||
}, [rows, hotlistsound, npedSound, setSightingModalOpen, setSelectedSighting]);
|
||||
}, [rows, enqueue]);
|
||||
|
||||
useEffect(() => {
|
||||
rows?.forEach((obj) => {
|
||||
@@ -156,22 +165,33 @@ export default function SightingHistoryWidget({ className, title }: SightingHist
|
||||
});
|
||||
|
||||
if (firstNPED) {
|
||||
setSelectedSighting(firstNPED);
|
||||
npedSound();
|
||||
setSightingModalOpen(true);
|
||||
enqueue(firstNPED, "NPED");
|
||||
|
||||
npedRef.current = true;
|
||||
}
|
||||
|
||||
if (firstHot) {
|
||||
setSelectedSighting(firstHot);
|
||||
hotlistsound();
|
||||
setSightingModalOpen(true);
|
||||
enqueue(firstHot, "HOTLIST");
|
||||
|
||||
hasAutoOpenedRef.current = true;
|
||||
}
|
||||
}, [hotlistsound, npedSound, setSelectedSighting]);
|
||||
}, [enqueue, hotlistsound, npedSound, rows, setSelectedSighting, setSightingModalOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSightingModalOpen && modalQueue.length > 0) {
|
||||
const next = modalQueue[0];
|
||||
|
||||
// if (next.kind === "NPED") npedSound();
|
||||
// else hotlistsound();
|
||||
|
||||
setSelectedSighting(next.sighting);
|
||||
setSightingModalOpen(true);
|
||||
}
|
||||
}, [isSightingModalOpen, npedSound, hotlistsound, setSelectedSighting, setSightingModalOpen, modalQueue]);
|
||||
|
||||
const handleClose = () => {
|
||||
setSightingModalOpen(false);
|
||||
setModalQueue((q) => q.slice(1));
|
||||
};
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import { Link } from "react-router";
|
||||
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 { faGear, faHome, faListCheck, faMaximize, faMinimize, faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useState } from "react";
|
||||
import SoundBtn from "./SoundBtn";
|
||||
import { useNPEDContext } from "../../context/NPEDUserContext";
|
||||
import { useIntegrationsContext } from "../../context/IntegrationsContext";
|
||||
|
||||
export default function Header() {
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const { sessionStarted } = useNPEDContext();
|
||||
const { state } = useIntegrationsContext();
|
||||
|
||||
const sessionStarted = state.sessionStarted;
|
||||
|
||||
const sessionPaused = state.sessionPaused;
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
@@ -39,9 +36,13 @@ export default function Header() {
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row items-center space-x-24 justify-items-center">
|
||||
{sessionStarted && (
|
||||
<div className="text-green-400 font-bold">Session Active</div>
|
||||
)}
|
||||
<div className="flex flex-row lg:flex-row space-x-2">
|
||||
{sessionStarted && sessionPaused ? (
|
||||
<p className="text-gray-400 font-bold">Session Paused</p>
|
||||
) : (
|
||||
sessionStarted && <p className="text-green-400 font-bold">Session Active</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row space-x-8">
|
||||
<Link to={"/"}>
|
||||
@@ -59,11 +60,7 @@ export default function Header() {
|
||||
</div>
|
||||
<SoundBtn />
|
||||
<Link to={"/session-settings"}>
|
||||
<FontAwesomeIcon
|
||||
className="text-white"
|
||||
icon={faListCheck}
|
||||
size="2x"
|
||||
/>
|
||||
<FontAwesomeIcon className="text-white" icon={faListCheck} size="2x" />
|
||||
</Link>
|
||||
|
||||
<Link to={"/system-settings"}>
|
||||
|
||||
18
src/components/UI/VehicleSessionItem.tsx
Normal file
18
src/components/UI/VehicleSessionItem.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
type VehicleSessionItemProps = {
|
||||
sessionNumber: number;
|
||||
textColour: string;
|
||||
vehicleTag: string;
|
||||
};
|
||||
|
||||
const VehicleSessionItem = ({ sessionNumber, textColour, vehicleTag }: VehicleSessionItemProps) => {
|
||||
return (
|
||||
<li className="rounded-xl border border-slate-800 bg-slate-800/60 p-3 shadow-sm flex flex-row justify-between items-center">
|
||||
<p>{vehicleTag}</p>
|
||||
<span className={`font-bold text-xl bg-slate-700 px-2 rounded-md ${clsx(textColour)}`}>{sessionNumber}</span>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default VehicleSessionItem;
|
||||
14
src/context/IntegrationsContext.ts
Normal file
14
src/context/IntegrationsContext.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createContext, useContext, type ActionDispatch } from "react";
|
||||
import type { NPEDACTION, NPEDSTATE } from "../types/types";
|
||||
|
||||
type IntegrationsValue = {
|
||||
state: NPEDSTATE;
|
||||
dispatch: ActionDispatch<[action: NPEDACTION]>;
|
||||
};
|
||||
|
||||
export const IntegrationsContext = createContext<IntegrationsValue | undefined>(undefined);
|
||||
export const useIntegrationsContext = () => {
|
||||
const ctx = useContext(IntegrationsContext);
|
||||
if (!ctx) throw new Error("useNPEDContext must be used within <IntegrationsProvider>");
|
||||
return ctx;
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { createContext, useContext, type SetStateAction } from "react";
|
||||
import type { NPEDUser, ReducedSightingType } from "../types/types";
|
||||
|
||||
type UserContextValue = {
|
||||
user: NPEDUser | null;
|
||||
setUser: React.Dispatch<SetStateAction<NPEDUser | null>>;
|
||||
sessionStarted: boolean;
|
||||
setSessionStarted: React.Dispatch<SetStateAction<boolean>>;
|
||||
sessionList: ReducedSightingType[];
|
||||
setSessionList: React.Dispatch<SetStateAction<ReducedSightingType[]>>;
|
||||
};
|
||||
|
||||
export const NPEDUserContext = createContext<UserContextValue | undefined>(
|
||||
undefined
|
||||
);
|
||||
export const useNPEDContext = () => {
|
||||
const ctx = useContext(NPEDUserContext);
|
||||
if (!ctx)
|
||||
throw new Error("useNPEDContext must be used within <NPEDUserProvider>");
|
||||
return ctx;
|
||||
};
|
||||
36
src/context/providers/IntegrationsContextProvider.tsx
Normal file
36
src/context/providers/IntegrationsContextProvider.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useEffect, useReducer, type ReactNode } from "react";
|
||||
import { IntegrationsContext } from "../IntegrationsContext";
|
||||
import { useCameraBlackboard } from "../../hooks/useCameraBlackboard";
|
||||
import { initialState, reducer } from "../reducers/IntegrationsContextReducer";
|
||||
|
||||
type IntegrationsProviderType = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const IntegrationsProvider = ({ children }: IntegrationsProviderType) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const { mutation } = useCameraBlackboard();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const result = await mutation.mutateAsync({
|
||||
operation: "VIEW",
|
||||
path: "sessionStats",
|
||||
});
|
||||
if (!result.result || typeof result.result === "string") return;
|
||||
|
||||
dispatch({ type: "UPDATE", payload: result?.result });
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
return (
|
||||
<IntegrationsContext.Provider
|
||||
value={{
|
||||
state,
|
||||
dispatch,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</IntegrationsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import { useState, type ReactNode } from "react";
|
||||
import type { NPEDUser, ReducedSightingType } from "../../types/types";
|
||||
import { NPEDUserContext } from "../NPEDUserContext";
|
||||
|
||||
type NPEDUserProviderType = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const NPEDUserProvider = ({ children }: NPEDUserProviderType) => {
|
||||
const [user, setUser] = useState<NPEDUser | null>(null);
|
||||
const [sessionStarted, setSessionStarted] = useState(false);
|
||||
const [sessionList, setSessionList] = useState<ReducedSightingType[]>([]);
|
||||
|
||||
return (
|
||||
<NPEDUserContext.Provider
|
||||
value={{
|
||||
user,
|
||||
setUser,
|
||||
setSessionStarted,
|
||||
sessionStarted,
|
||||
sessionList,
|
||||
setSessionList,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</NPEDUserContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -21,7 +21,11 @@ const SoundContextProvider = ({ children }: SoundContextProviderProps) => {
|
||||
path: "soundSettings",
|
||||
});
|
||||
|
||||
dispatch({ type: "UPDATE", payload: result.result });
|
||||
if (!result.result || typeof result.result !== "object") {
|
||||
dispatch({ type: "UPDATE", payload: state });
|
||||
} else {
|
||||
dispatch({ type: "UPDATE", payload: result.result });
|
||||
}
|
||||
};
|
||||
fetchSound();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
46
src/context/reducers/IntegrationsContextReducer.ts
Normal file
46
src/context/reducers/IntegrationsContextReducer.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { NPEDACTION, NPEDSTATE } from "../../types/types";
|
||||
|
||||
export const initialState = {
|
||||
sessionStarted: false,
|
||||
sessionList: [],
|
||||
sessionPaused: false,
|
||||
savedSightings: [],
|
||||
npedUser: null,
|
||||
};
|
||||
|
||||
export function reducer(state: NPEDSTATE, action: NPEDACTION) {
|
||||
switch (action.type) {
|
||||
case "SESSIONSTART":
|
||||
return {
|
||||
...state,
|
||||
sessionStarted: action.payload,
|
||||
};
|
||||
case "LOGIN":
|
||||
return {
|
||||
...state,
|
||||
npedUser: action.payload,
|
||||
};
|
||||
case "LOGOUT":
|
||||
return {
|
||||
...state,
|
||||
npedUser: action.payload,
|
||||
};
|
||||
case "SESSIONPAUSE":
|
||||
return {
|
||||
...state,
|
||||
sessionPaused: action.payload,
|
||||
};
|
||||
case "ADD":
|
||||
return {
|
||||
...state,
|
||||
sessionList: [...state.sessionList, action.payload],
|
||||
};
|
||||
case "UPDATE":
|
||||
return {
|
||||
...state,
|
||||
sessionList: action.payload,
|
||||
};
|
||||
default:
|
||||
return { ...state };
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ export function reducer(state: SoundState, action: SoundAction): SoundState {
|
||||
NPEDsoundVolume: action.payload.NPEDsoundVolume,
|
||||
sightingVolume: action.payload.sightingVolume,
|
||||
hotlistSoundVolume: action.payload.hotlistSoundVolume,
|
||||
soundOptions: action.payload.soundOptions,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import type { NPEDFieldType } from "../types/types";
|
||||
import { useNPEDContext } from "../context/NPEDUserContext";
|
||||
import { useIntegrationsContext } from "../context/IntegrationsContext";
|
||||
import { useEffect } from "react";
|
||||
import { CAM_BASE } from "../utils/config";
|
||||
import { toast } from "sonner";
|
||||
@@ -42,8 +42,7 @@ async function signIn(loginDetails: NPEDFieldType) {
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!frontRes.ok || !rearRes.ok)
|
||||
throw new Error("Cannot reach NPED endpoint");
|
||||
if (!frontRes.ok || !rearRes.ok) throw new Error("Cannot reach NPED endpoint");
|
||||
|
||||
return {
|
||||
frontResponse: frontRes.json(),
|
||||
@@ -73,7 +72,7 @@ async function signOut() {
|
||||
}
|
||||
|
||||
export const useNPEDAuth = () => {
|
||||
const { setUser, user } = useNPEDContext();
|
||||
const { dispatch } = useIntegrationsContext();
|
||||
|
||||
const signInMutation = useMutation({
|
||||
mutationKey: ["NPEDSignin"],
|
||||
@@ -84,7 +83,8 @@ export const useNPEDAuth = () => {
|
||||
onSuccess: async (data) => {
|
||||
toast.dismiss();
|
||||
toast.success("Signed in successfully!");
|
||||
setUser(await data.frontResponse);
|
||||
|
||||
dispatch({ type: "LOGIN", payload: await data.frontResponse });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.dismiss();
|
||||
@@ -101,7 +101,7 @@ export const useNPEDAuth = () => {
|
||||
mutationFn: signOut,
|
||||
onSuccess: () => {
|
||||
toast.success("Signed out successfully");
|
||||
setUser(null);
|
||||
dispatch({ type: "LOGOUT", payload: null });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Sign-out failed: ${error.message}`);
|
||||
@@ -115,11 +115,11 @@ export const useNPEDAuth = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchdataQuery.isSuccess && fetchdataQuery.data) {
|
||||
setUser(fetchdataQuery.data);
|
||||
dispatch({ type: "LOGIN", payload: fetchdataQuery.data });
|
||||
} else {
|
||||
setUser(null);
|
||||
dispatch({ type: "LOGOUT", payload: null });
|
||||
}
|
||||
}, [fetchdataQuery.data, fetchdataQuery.isSuccess, setUser]);
|
||||
}, [dispatch, fetchdataQuery.data, fetchdataQuery.isSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchdataQuery.isError) toast.error(fetchdataQuery.error.message);
|
||||
@@ -134,8 +134,6 @@ export const useNPEDAuth = () => {
|
||||
data: signInMutation.data,
|
||||
fetchdataQueryError: fetchdataQuery.error,
|
||||
fetchdataQueryLoading: fetchdataQuery.isLoading,
|
||||
user,
|
||||
setUser,
|
||||
signOut: signOutMutation.mutate,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { Query, useQuery } from "@tanstack/react-query";
|
||||
import type { SightingType } from "../types/types";
|
||||
import { useSoundOnChange } from "react-sounds";
|
||||
import { useSound } from "react-sounds";
|
||||
import { useSoundContext } from "../context/SoundContext";
|
||||
import { getSoundFileURL } from "../utils/utils";
|
||||
import { checkIsHotListHit, getNPEDCategory, getSoundFileURL } from "../utils/utils";
|
||||
import switchSound from "../assets/sounds/ui/switch.mp3";
|
||||
import notification from "../assets/sounds/ui/notification.mp3";
|
||||
import popup from "../assets/sounds/ui/popup_open.mp3";
|
||||
|
||||
async function fetchSighting(url: string | undefined, ref: number): Promise<SightingType> {
|
||||
const res = await fetch(`${url}${ref}`, {
|
||||
@@ -21,30 +24,38 @@ export function useSightingFeed(url: string | undefined) {
|
||||
const [sessionStarted, setSessionStarted] = useState(false);
|
||||
const [selectedSighting, setSelectedSighting] = useState<SightingType | null>(null);
|
||||
|
||||
const mostRecent = sightings[0] ?? null;
|
||||
const latestRef = mostRecent?.ref ?? null;
|
||||
|
||||
const first = useRef(true);
|
||||
const lastSoundAt = useRef(0);
|
||||
const COOLDOWN_MS = 1500;
|
||||
const currentRef = useRef<number>(-1);
|
||||
const lastValidTimestamp = useRef<number>(Date.now());
|
||||
|
||||
const trigger = useMemo(() => {
|
||||
if (latestRef == null || !audioArmed) return null;
|
||||
if (first.current) {
|
||||
first.current = false;
|
||||
return Symbol("skip");
|
||||
const soundSrcHotlist = useMemo(() => {
|
||||
if (state?.hotlistSound?.includes(".mp3") || state.hotlistSound?.includes(".wav")) {
|
||||
const file = state.soundOptions?.find((item) => item.name === state.hotlistSound);
|
||||
return file?.soundUrl ?? notification;
|
||||
}
|
||||
const now = Date.now();
|
||||
if (now - lastSoundAt.current < COOLDOWN_MS) return Symbol("skip");
|
||||
lastSoundAt.current = now;
|
||||
return latestRef;
|
||||
}, [audioArmed, latestRef]);
|
||||
return getSoundFileURL(state?.hotlistSound) ?? notification;
|
||||
}, [state.hotlistSound, state.soundOptions]);
|
||||
|
||||
const soundSrcNped = useMemo(() => {
|
||||
if (state?.NPEDsound?.includes(".mp3") || state.NPEDsound?.includes(".wav")) {
|
||||
const file = state.soundOptions?.find((item) => item.name === state.NPEDsound);
|
||||
return file?.soundUrl ?? popup;
|
||||
}
|
||||
return getSoundFileURL(state.NPEDsound) ?? popup;
|
||||
}, [state.NPEDsound, state.soundOptions]);
|
||||
|
||||
const soundSrc = useMemo(() => {
|
||||
if (state?.sightingSound?.includes(".mp3") || state.sightingSound?.includes(".wav")) {
|
||||
const file = state.soundOptions?.find((item) => item.name === state.sightingSound);
|
||||
return file?.soundUrl ?? switchSound;
|
||||
}
|
||||
return getSoundFileURL(state?.sightingSound) ?? switchSound;
|
||||
}, [state.sightingSound]);
|
||||
}, [state.sightingSound, state.soundOptions]);
|
||||
|
||||
const { play: hotlistsound } = useSound(soundSrcHotlist, { volume: state.hotlistSoundVolume });
|
||||
const { play: npedSound } = useSound(soundSrcNped, { volume: state.NPEDsoundVolume });
|
||||
const { play: sightingSound } = useSound(soundSrc, { volume: state.sightingVolume });
|
||||
|
||||
const mostRecent = sightings[0] ?? null;
|
||||
|
||||
const currentRef = useRef<number>(-1);
|
||||
const lastValidTimestamp = useRef<number>(Date.now());
|
||||
|
||||
function refetchInterval(query: Query<SightingType, Error, SightingType, (string | undefined)[]>) {
|
||||
if (!query) return;
|
||||
@@ -74,16 +85,36 @@ export function useSightingFeed(url: string | undefined) {
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
//use latestref instead of trigger to revert back
|
||||
const playHotlistsound = useDebouncedCallback(() => {
|
||||
hotlistsound();
|
||||
}, 500);
|
||||
|
||||
useSoundOnChange(soundSrc, trigger, {
|
||||
volume: state.sightingVolume,
|
||||
initial: false,
|
||||
});
|
||||
const playNPEDHitSound = useDebouncedCallback(() => {
|
||||
npedSound();
|
||||
}, 500);
|
||||
|
||||
const playSightingHitSound = useDebouncedCallback(() => {
|
||||
sightingSound();
|
||||
}, 500);
|
||||
|
||||
useEffect(() => {
|
||||
const data = query.data;
|
||||
|
||||
if (!data || data.ref === -1) return;
|
||||
const isHotListHit = checkIsHotListHit(data);
|
||||
const cat = getNPEDCategory(data);
|
||||
|
||||
const isNPEDHitA = cat === "A";
|
||||
const isNPEDHitB = cat === "B";
|
||||
const isNPEDHitC = cat === "C";
|
||||
|
||||
if ((isNPEDHitA && audioArmed) || (isNPEDHitB && audioArmed) || (isNPEDHitC && audioArmed)) {
|
||||
playNPEDHitSound();
|
||||
} else if (isHotListHit && audioArmed) {
|
||||
playHotlistsound();
|
||||
} else if (audioArmed) {
|
||||
playSightingHitSound();
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
|
||||
@@ -286,12 +286,14 @@ export type FormValues = {
|
||||
NPEDsound: SoundValue;
|
||||
hotlists: Hotlist[];
|
||||
hotlistSound: SoundValue;
|
||||
soundOptions?: SoundUploadValue[];
|
||||
};
|
||||
|
||||
export type SoundUploadValue = {
|
||||
name: string;
|
||||
soundFileName?: string;
|
||||
soundFile?: File | null;
|
||||
soundUrl?: string;
|
||||
};
|
||||
|
||||
export type SoundState = {
|
||||
@@ -315,6 +317,7 @@ type UpdateAction = {
|
||||
NPEDsoundVolume: number;
|
||||
hotlistSoundVolume: number;
|
||||
hotlistSound: SoundValue;
|
||||
soundOptions?: SoundUploadValue[];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -368,3 +371,26 @@ export type ModemSettingsType = {
|
||||
password: string;
|
||||
authenticationType: string;
|
||||
};
|
||||
|
||||
export type HitKind = "NPED" | "HOTLIST";
|
||||
|
||||
export type QueuedHit = {
|
||||
id: number | string;
|
||||
sighting: SightingType;
|
||||
kind: HitKind;
|
||||
};
|
||||
|
||||
export type DedupedSightings = ReducedSightingType[];
|
||||
|
||||
export type NPEDSTATE = {
|
||||
sessionStarted: boolean;
|
||||
sessionList: ReducedSightingType[];
|
||||
sessionPaused: boolean;
|
||||
savedSightings: DedupedSightings;
|
||||
npedUser: NPEDUser;
|
||||
};
|
||||
|
||||
export type NPEDACTION = {
|
||||
type: string;
|
||||
payload: any;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import beep from "../assets/sounds/ui/Beep.wav";
|
||||
import warning from "../assets/sounds/ui/Warning.wav";
|
||||
import ding from "../assets/sounds/ui/Ding.wav";
|
||||
import shutter from "../assets/sounds/ui/shutter.mp3";
|
||||
|
||||
import attention from "../assets/sounds/ui/Attention.wav";
|
||||
import type { HotlistMatches, SightingType } from "../types/types";
|
||||
|
||||
export function getSoundFileURL(name: string) {
|
||||
@@ -17,10 +17,15 @@ export function getSoundFileURL(name: string) {
|
||||
warning: warning,
|
||||
ding: ding,
|
||||
shutter: shutter,
|
||||
attention: attention,
|
||||
};
|
||||
return sounds[name] ?? null;
|
||||
}
|
||||
|
||||
export const showSoundURL = (url: URL | string | undefined) => {
|
||||
console.log(url);
|
||||
};
|
||||
|
||||
const randomChars = () => {
|
||||
const uppercaseAsciiStart = 65;
|
||||
const letterIndex = Math.floor(Math.random() * 26);
|
||||
@@ -143,11 +148,13 @@ export const checkIsHotListHit = (sigthing: SightingType | null) => {
|
||||
};
|
||||
|
||||
export function getHotlistName(obj: HotlistMatches | undefined) {
|
||||
if (!obj || Object.values(obj).includes(false)) return;
|
||||
if (!obj) return;
|
||||
|
||||
const keys = Object.keys(obj);
|
||||
return keys;
|
||||
const hotlistNames = Object.entries(obj)
|
||||
.filter(([, value]) => value === true)
|
||||
.map(([key]) => key);
|
||||
return hotlistNames;
|
||||
}
|
||||
|
||||
export const getNPEDCategory = (r?: SightingType | null) =>
|
||||
r?.metadata?.npedJSON?.["NPED CATEGORY"] as "A" | "B" | "C" | undefined;
|
||||
r?.metadata?.npedJSON?.["NPED CATEGORY"] as "A" | "B" | "C" | "D" | undefined;
|
||||
|
||||
@@ -2267,6 +2267,11 @@ uri-js@^4.2.2:
|
||||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
use-debounce@^10.0.6:
|
||||
version "10.0.6"
|
||||
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-10.0.6.tgz#e05060a5e561432ec740c653698f3eb162bd28ec"
|
||||
integrity sha512-C5OtPyhAZgVoteO9heXMTdW7v/IbFI+8bSVKYCJrSmiWWCLsbUxiBSp4t9v0hNBTGY97bT72ydDIDyGSFWfwXg==
|
||||
|
||||
vite@^7.1.0:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz"
|
||||
|
||||
Reference in New Issue
Block a user