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 0000000..6fa81e8 Binary files /dev/null and b/src/assets/sounds/ui/notification.mp3 differ 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; +};