Merged in bugfix/uploadsounds-2 (pull request #35)
Bugfix/uploadsounds 2
This commit is contained in:
@@ -32,7 +32,6 @@ const SoundSettingsFields = () => {
|
|||||||
hotlistSoundVolume: state.hotlistSoundVolume,
|
hotlistSoundVolume: state.hotlistSoundVolume,
|
||||||
soundOptions: [...(state.soundOptions ?? [])],
|
soundOptions: [...(state.soundOptions ?? [])],
|
||||||
};
|
};
|
||||||
|
|
||||||
dispatch({ type: "UPDATE", payload: updatedValues });
|
dispatch({ type: "UPDATE", payload: updatedValues });
|
||||||
|
|
||||||
const result = await mutation.mutateAsync({
|
const result = await mutation.mutateAsync({
|
||||||
|
|||||||
@@ -4,16 +4,21 @@ import type { SoundUploadValue } from "../../../types/types";
|
|||||||
import { useSoundContext } from "../../../context/SoundContext";
|
import { useSoundContext } from "../../../context/SoundContext";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useCameraBlackboard } from "../../../hooks/useCameraBlackboard";
|
import { useCameraBlackboard } from "../../../hooks/useCameraBlackboard";
|
||||||
|
import { useFileUpload } from "../../../hooks/useFileUpload";
|
||||||
|
|
||||||
const SoundUpload = () => {
|
const SoundUpload = () => {
|
||||||
const { state, dispatch } = useSoundContext();
|
const { state, dispatch } = useSoundContext();
|
||||||
const { mutation } = useCameraBlackboard();
|
const { mutation } = useCameraBlackboard();
|
||||||
|
const { mutation: fileMutation } = useFileUpload({
|
||||||
|
queryKey: state.sightingSound ? [state.sightingSound] : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
const initialValues: SoundUploadValue = {
|
const initialValues: SoundUploadValue = {
|
||||||
name: "",
|
name: "",
|
||||||
soundFile: null,
|
soundFile: null,
|
||||||
soundFileName: "",
|
soundFileName: "",
|
||||||
soundUrl: "",
|
soundUrl: "",
|
||||||
|
uploadedAt: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (values: SoundUploadValue) => {
|
const handleSubmit = async (values: SoundUploadValue) => {
|
||||||
@@ -37,10 +42,9 @@ const SoundUpload = () => {
|
|||||||
path: "soundSettings",
|
path: "soundSettings",
|
||||||
value: updatedValues,
|
value: updatedValues,
|
||||||
});
|
});
|
||||||
|
await fileMutation.mutateAsync(values.soundFile);
|
||||||
if (result.reason !== "OK") {
|
if (result.reason !== "OK") {
|
||||||
toast.error("Cannot update sound settings");
|
toast.error("Cannot update sound settings");
|
||||||
} else {
|
|
||||||
toast.success(`${values.name} file added`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({ type: "ADD", payload: values });
|
dispatch({ type: "ADD", payload: values });
|
||||||
@@ -48,7 +52,7 @@ const SoundUpload = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik initialValues={initialValues} onSubmit={handleSubmit} enableReinitialize>
|
<Formik initialValues={initialValues} onSubmit={handleSubmit} enableReinitialize>
|
||||||
{({ setFieldValue, errors, setFieldError, values }) => (
|
{({ setFieldValue, errors, setFieldError }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<label htmlFor="soundFile" className="">
|
<label htmlFor="soundFile" className="">
|
||||||
Sound File
|
Sound File
|
||||||
@@ -67,6 +71,12 @@ const SoundUpload = () => {
|
|||||||
setFieldValue("name", e.target.files[0].name);
|
setFieldValue("name", e.target.files[0].name);
|
||||||
setFieldValue("soundFileName", e.target.files[0].name);
|
setFieldValue("soundFileName", e.target.files[0].name);
|
||||||
setFieldValue("soundFile", e.target.files[0]);
|
setFieldValue("soundFile", e.target.files[0]);
|
||||||
|
setFieldValue("uploadedAt", Date.now());
|
||||||
|
if (e?.target?.files[0]?.size >= 1 * 1024 * 1024) {
|
||||||
|
setFieldError("soundFile", "larger than 1mb");
|
||||||
|
toast.error("File larger than 1MB");
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setFieldError("soundFile", "Not an mp3 file");
|
setFieldError("soundFile", "Not an mp3 file");
|
||||||
toast.error("Not an mp3 file");
|
toast.error("Not an mp3 file");
|
||||||
@@ -76,11 +86,6 @@ const SoundUpload = () => {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<div className="mt-4 flex flex-col items-center justify-center rounded-2xl border border-slate-800 bg-slate-900/40 p-10 text-center">
|
<div className="mt-4 flex flex-col items-center justify-center rounded-2xl border border-slate-800 bg-slate-900/40 p-10 text-center">
|
||||||
{!values.soundFile && (
|
|
||||||
<div className="mb-3 rounded-xl bg-slate-800 px-3 py-1 text-xs uppercase tracking-wider text-slate-400">
|
|
||||||
No uploaded sound files
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="max-w-md text-slate-300">
|
<p className="max-w-md text-slate-300">
|
||||||
Uploaded Sound files will appear in the <span className="font-bold">drop downs</span> once they are
|
Uploaded Sound files will appear in the <span className="font-bold">drop downs</span> once they are
|
||||||
uploaded. They can be used for any <span className="text-blue-400">Sighting,</span>{" "}
|
uploaded. They can be used for any <span className="text-blue-400">Sighting,</span>{" "}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import SoundUpload from "./SoundUpload";
|
|||||||
|
|
||||||
const SoundUploadCard = () => {
|
const SoundUploadCard = () => {
|
||||||
return (
|
return (
|
||||||
<Card className="p-4 col-span-3 w-full">
|
<Card className="p-4 col-span-5 lg:col-span-3 w-full">
|
||||||
<CardHeader title={"Sound upload"} />
|
<CardHeader title={"Sound upload"} />
|
||||||
<SoundUpload />
|
<SoundUpload />
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -10,10 +10,7 @@ type BlobFileUpload = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function sendBlobFileUpload({
|
export async function sendBlobFileUpload({ file, opts }: BlobFileUpload): Promise<string> {
|
||||||
file,
|
|
||||||
opts,
|
|
||||||
}: BlobFileUpload): Promise<string> {
|
|
||||||
if (!file) throw new Error("No file supplied");
|
if (!file) throw new Error("No file supplied");
|
||||||
if (!opts?.uploadUrl) throw new Error("No URL supplied");
|
if (!opts?.uploadUrl) throw new Error("No URL supplied");
|
||||||
|
|
||||||
@@ -42,9 +39,7 @@ export async function sendBlobFileUpload({
|
|||||||
const bodyText = await resp.text();
|
const bodyText = await resp.text();
|
||||||
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
throw new Error(
|
throw new Error(`Upload failed (${resp.status} ${resp.statusText}) from ${opts.uploadUrl} — ${bodyText}`);
|
||||||
`Upload failed (${resp.status} ${resp.statusText}) from ${opts.uploadUrl} — ${bodyText}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return bodyText;
|
return bodyText;
|
||||||
@@ -54,9 +49,7 @@ export async function sendBlobFileUpload({
|
|||||||
}
|
}
|
||||||
// In browsers, fetch throws TypeError on network-level failures
|
// In browsers, fetch throws TypeError on network-level failures
|
||||||
if (err instanceof TypeError) {
|
if (err instanceof TypeError) {
|
||||||
throw new Error(
|
throw new Error(`HTTP error uploading to ${opts.uploadUrl}: ${err.message}`);
|
||||||
`HTTP error uploading to ${opts.uploadUrl}: ${err.message}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// Todo: fix error message response
|
// Todo: fix error message response
|
||||||
return `Hotlist Load OK`;
|
return `Hotlist Load OK`;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { HitKind, QueuedHit, ReducedSightingType, SightingType } from "../../types/types";
|
import type { HitKind, QueuedHit, ReducedSightingType, SightingType } from "../../types/types";
|
||||||
import { BLANK_IMG, getSoundFileURL } from "../../utils/utils";
|
import { BLANK_IMG } from "../../utils/utils";
|
||||||
import NumberPlate from "../PlateStack/NumberPlate";
|
import NumberPlate from "../PlateStack/NumberPlate";
|
||||||
import Card from "../UI/Card";
|
import Card from "../UI/Card";
|
||||||
import CardHeader from "../UI/CardHeader";
|
import CardHeader from "../UI/CardHeader";
|
||||||
@@ -19,6 +19,7 @@ import { useIntegrationsContext } from "../../context/IntegrationsContext";
|
|||||||
import { useSoundContext } from "../../context/SoundContext";
|
import { useSoundContext } from "../../context/SoundContext";
|
||||||
import Loading from "../UI/Loading";
|
import Loading from "../UI/Loading";
|
||||||
import { checkIsHotListHit, getNPEDCategory } from "../../utils/utils";
|
import { checkIsHotListHit, getNPEDCategory } from "../../utils/utils";
|
||||||
|
import { useCachedSoundSrc } from "../../hooks/usecachedSoundSrc";
|
||||||
|
|
||||||
function useNow(tickMs = 1000) {
|
function useNow(tickMs = 1000) {
|
||||||
const [, setNow] = useState(() => Date.now());
|
const [, setNow] = useState(() => Date.now());
|
||||||
@@ -43,21 +44,8 @@ export default function SightingHistoryWidget({ className, title }: SightingHist
|
|||||||
useNow(1000);
|
useNow(1000);
|
||||||
const { state } = useSoundContext();
|
const { state } = useSoundContext();
|
||||||
|
|
||||||
const soundSrcNped = useMemo(() => {
|
const { src: soundSrcHotlist } = useCachedSoundSrc(state?.hotlistSound, state?.soundOptions, notification);
|
||||||
if (state?.NPEDsound?.includes(".mp3") || state.NPEDsound?.includes(".wav")) {
|
const { src: soundSrcNped } = useCachedSoundSrc(state?.NPEDsound, state?.soundOptions, popup);
|
||||||
const file = state.soundOptions?.find((item) => item.name === state.NPEDsound);
|
|
||||||
return file?.soundUrl ?? popup;
|
|
||||||
}
|
|
||||||
return getSoundFileURL(state.NPEDsound) ?? popup;
|
|
||||||
}, [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.soundOptions]);
|
|
||||||
|
|
||||||
const { play: npedSound } = useSound(soundSrcNped, { volume: state.NPEDsoundVolume });
|
const { play: npedSound } = useSound(soundSrcNped, { volume: state.NPEDsoundVolume });
|
||||||
const { play: hotlistsound } = useSound(soundSrcHotlist, { volume: state.hotlistSoundVolume });
|
const { play: hotlistsound } = useSound(soundSrcHotlist, { volume: state.hotlistSoundVolume });
|
||||||
@@ -181,8 +169,8 @@ export default function SightingHistoryWidget({ className, title }: SightingHist
|
|||||||
if (!isSightingModalOpen && modalQueue.length > 0) {
|
if (!isSightingModalOpen && modalQueue.length > 0) {
|
||||||
const next = modalQueue[0];
|
const next = modalQueue[0];
|
||||||
|
|
||||||
// if (next.kind === "NPED") npedSound();
|
if (next.kind === "NPED") npedSound();
|
||||||
// else hotlistsound();
|
else hotlistsound();
|
||||||
|
|
||||||
setSelectedSighting(next.sighting);
|
setSelectedSighting(next.sighting);
|
||||||
setSightingModalOpen(true);
|
setSightingModalOpen(true);
|
||||||
|
|||||||
@@ -11,25 +11,18 @@ type CameraOverviewHeaderProps = {
|
|||||||
sighting?: SightingType | null;
|
sighting?: SightingType | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CardHeader = ({
|
const CardHeader = ({ title, icon, img, sighting }: CameraOverviewHeaderProps) => {
|
||||||
title,
|
|
||||||
icon,
|
|
||||||
img,
|
|
||||||
sighting,
|
|
||||||
}: CameraOverviewHeaderProps) => {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-full border-b border-gray-600 flex flex-row items-center space-x-2 md:mb-6 relative justify-between"
|
"w-full border-b border-gray-600 flex flex-row items-center space-x-2 mb-6 relative justify-between"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{icon && <FontAwesomeIcon icon={icon} className="size-4" />}
|
{icon && <FontAwesomeIcon icon={icon} className="size-4" />}
|
||||||
<h2 className="text-xl">{title}</h2>
|
<h2 className="text-xl">{title}</h2>
|
||||||
</div>
|
</div>
|
||||||
{img && (
|
{img && <img src={img} alt="Logo" width={100} height={50} className="ml-auto" />}
|
||||||
<img src={img} alt="Logo" width={100} height={50} className="ml-auto" />
|
|
||||||
)}
|
|
||||||
{sighting?.vrm && <NumberPlate vrm={sighting.vrm} motion={false} />}
|
{sighting?.vrm && <NumberPlate vrm={sighting.vrm} motion={false} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ export const initialState: SoundState = {
|
|||||||
{ name: "Ding", soundFileName: "ding" },
|
{ name: "Ding", soundFileName: "ding" },
|
||||||
{ name: "Shutter", soundFileName: "shutter" },
|
{ name: "Shutter", soundFileName: "shutter" },
|
||||||
{ name: "Warning (voice)", soundFileName: "warning" },
|
{ name: "Warning (voice)", soundFileName: "warning" },
|
||||||
|
{ name: "Attention (voice)", soundFileName: "attention" },
|
||||||
],
|
],
|
||||||
sightingVolume: 1,
|
sightingVolume: 1,
|
||||||
NPEDsoundVolume: 1,
|
NPEDsoundVolume: 1,
|
||||||
hotlistSoundVolume: 1,
|
hotlistSoundVolume: 1,
|
||||||
|
uploadedSound: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function reducer(state: SoundState, action: SoundAction): SoundState {
|
export function reducer(state: SoundState, action: SoundAction): SoundState {
|
||||||
@@ -62,7 +64,11 @@ export function reducer(state: SoundState, action: SoundAction): SoundState {
|
|||||||
...state,
|
...state,
|
||||||
hotlistSoundVolume: action.payload,
|
hotlistSoundVolume: action.payload,
|
||||||
};
|
};
|
||||||
|
case "UPLOADEDSOUND":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
uploadedSound: action.payload,
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
45
src/hooks/useFileUpload.ts
Normal file
45
src/hooks/useFileUpload.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import { CAM_BASE } from "../utils/config";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { getOrCacheBlob } from "../utils/cacheSound";
|
||||||
|
const camBase = import.meta.env.MODE !== "development" ? CAM_BASE : CAM_BASE;
|
||||||
|
|
||||||
|
type UseFileUploadProps = {
|
||||||
|
queryKey?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadFile = async (file: File) => {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("upload", file, file.name);
|
||||||
|
const response = await fetch(`${camBase}/upload/file-upload/3`, {
|
||||||
|
method: "POST",
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Cannot reach upload file endpoint");
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUploadFiles = async ({ queryKey }: { queryKey: string[] }) => {
|
||||||
|
const [, fileName] = queryKey;
|
||||||
|
const url = fileName;
|
||||||
|
return getOrCacheBlob(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFileUpload = ({ queryKey }: UseFileUploadProps) => {
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ["getUploadFiles", ...(queryKey ?? [])],
|
||||||
|
queryFn: () => getUploadFiles({ queryKey: ["getUploadFiles", ...(queryKey ?? [])] }),
|
||||||
|
enabled: !!queryKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (file: File) => uploadFile(file),
|
||||||
|
mutationKey: ["uploadFile"],
|
||||||
|
onError: (err) => toast.error(err ? err.message : ""),
|
||||||
|
onSuccess: async (msg) => toast.success(msg),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { query: queryKey ? query : undefined, mutation };
|
||||||
|
};
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import { Query, useQuery } from "@tanstack/react-query";
|
import { Query, useQuery } from "@tanstack/react-query";
|
||||||
import type { SightingType } from "../types/types";
|
import type { SightingType } from "../types/types";
|
||||||
import { useSound } from "react-sounds";
|
import { useSound } from "react-sounds";
|
||||||
import { useSoundContext } from "../context/SoundContext";
|
import { useSoundContext } from "../context/SoundContext";
|
||||||
import { checkIsHotListHit, getNPEDCategory, getSoundFileURL } from "../utils/utils";
|
import { checkIsHotListHit, getNPEDCategory } from "../utils/utils";
|
||||||
import switchSound from "../assets/sounds/ui/switch.mp3";
|
import switchSound from "../assets/sounds/ui/switch.mp3";
|
||||||
import notification from "../assets/sounds/ui/notification.mp3";
|
import notification from "../assets/sounds/ui/notification.mp3";
|
||||||
import popup from "../assets/sounds/ui/popup_open.mp3";
|
import popup from "../assets/sounds/ui/popup_open.mp3";
|
||||||
|
import { useCachedSoundSrc } from "./usecachedSoundSrc";
|
||||||
|
|
||||||
async function fetchSighting(url: string | undefined, ref: number): Promise<SightingType> {
|
async function fetchSighting(url: string | undefined, ref: number): Promise<SightingType> {
|
||||||
const res = await fetch(`${url}${ref}`, {
|
const res = await fetch(`${url}${ref}`, {
|
||||||
@@ -24,29 +25,9 @@ export function useSightingFeed(url: string | undefined) {
|
|||||||
const [sessionStarted, setSessionStarted] = useState(false);
|
const [sessionStarted, setSessionStarted] = useState(false);
|
||||||
const [selectedSighting, setSelectedSighting] = useState<SightingType | null>(null);
|
const [selectedSighting, setSelectedSighting] = useState<SightingType | null>(null);
|
||||||
|
|
||||||
const soundSrcHotlist = useMemo(() => {
|
const { src: soundSrc } = useCachedSoundSrc(state?.sightingSound, state?.soundOptions, switchSound);
|
||||||
if (state?.hotlistSound?.includes(".mp3") || state.hotlistSound?.includes(".wav")) {
|
const { src: soundSrcHotlist } = useCachedSoundSrc(state?.hotlistSound, state?.soundOptions, notification);
|
||||||
const file = state.soundOptions?.find((item) => item.name === state.hotlistSound);
|
const { src: soundSrcNped } = useCachedSoundSrc(state?.NPEDsound, state?.soundOptions, popup);
|
||||||
return file?.soundUrl ?? notification;
|
|
||||||
}
|
|
||||||
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.soundOptions]);
|
|
||||||
|
|
||||||
const { play: hotlistsound } = useSound(soundSrcHotlist, { volume: state.hotlistSoundVolume });
|
const { play: hotlistsound } = useSound(soundSrcHotlist, { volume: state.hotlistSoundVolume });
|
||||||
const { play: npedSound } = useSound(soundSrcNped, { volume: state.NPEDsoundVolume });
|
const { play: npedSound } = useSound(soundSrcNped, { volume: state.NPEDsoundVolume });
|
||||||
|
|||||||
56
src/hooks/usecachedSoundSrc.ts
Normal file
56
src/hooks/usecachedSoundSrc.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { useFileUpload } from "./useFileUpload";
|
||||||
|
import { getSoundFileURL } from "../utils/utils";
|
||||||
|
import type { SoundUploadValue } from "../types/types";
|
||||||
|
import { resolveSoundSource } from "../utils/soundResolver";
|
||||||
|
|
||||||
|
export function useCachedSoundSrc(
|
||||||
|
selected: string | undefined,
|
||||||
|
soundOptions: SoundUploadValue[] | undefined,
|
||||||
|
fallbackUrl: string
|
||||||
|
) {
|
||||||
|
const isUploaded = !!selected && (selected.endsWith(".mp3") || selected.endsWith(".wav"));
|
||||||
|
|
||||||
|
const resolved = resolveSoundSource(selected, soundOptions);
|
||||||
|
|
||||||
|
const { query } = useFileUpload({
|
||||||
|
queryKey: resolved?.type === "uploaded" ? [resolved?.url] : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [objectUrl, setObjectUrl] = useState<string>();
|
||||||
|
const objRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const blob = query?.data;
|
||||||
|
|
||||||
|
if (blob instanceof Blob) {
|
||||||
|
if (objRef.current) URL.revokeObjectURL(objRef.current);
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
objRef.current = url;
|
||||||
|
setObjectUrl(url);
|
||||||
|
} else {
|
||||||
|
if (objRef.current) URL.revokeObjectURL(objRef.current);
|
||||||
|
objRef.current = null;
|
||||||
|
setObjectUrl(undefined);
|
||||||
|
}
|
||||||
|
}, [query?.data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (objRef.current) URL.revokeObjectURL(objRef.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const src = useMemo(() => {
|
||||||
|
if (isUploaded && objectUrl) return objectUrl;
|
||||||
|
if (!selected) return fallbackUrl;
|
||||||
|
return getSoundFileURL(selected) ?? fallbackUrl;
|
||||||
|
}, [isUploaded, objectUrl, selected, fallbackUrl]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
src,
|
||||||
|
isUploaded,
|
||||||
|
isLoading: !!query?.isLoading,
|
||||||
|
error: (query?.error as Error) || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -302,6 +302,7 @@ export type SoundUploadValue = {
|
|||||||
soundFileName?: string;
|
soundFileName?: string;
|
||||||
soundFile?: File | null;
|
soundFile?: File | null;
|
||||||
soundUrl?: string;
|
soundUrl?: string;
|
||||||
|
uploadedAt?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SoundState = {
|
export type SoundState = {
|
||||||
@@ -313,6 +314,7 @@ export type SoundState = {
|
|||||||
sightingVolume: number;
|
sightingVolume: number;
|
||||||
NPEDsoundVolume: number;
|
NPEDsoundVolume: number;
|
||||||
hotlistSoundVolume: number;
|
hotlistSoundVolume: number;
|
||||||
|
uploadedSound?: Blob | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UpdateAction = {
|
type UpdateAction = {
|
||||||
@@ -339,7 +341,12 @@ type VolumeAction = {
|
|||||||
payload: number;
|
payload: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SoundAction = UpdateAction | AddAction | VolumeAction;
|
type UploadedState = {
|
||||||
|
type: "UPLOADEDSOUND";
|
||||||
|
payload: Blob | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SoundAction = UpdateAction | AddAction | VolumeAction | UploadedState;
|
||||||
export type WifiSettingValues = {
|
export type WifiSettingValues = {
|
||||||
ssid: string;
|
ssid: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
|||||||
16
src/utils/cacheSound.ts
Normal file
16
src/utils/cacheSound.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export async function getOrCacheBlob(url: string) {
|
||||||
|
const cache = await caches.open("app-sounds-v1");
|
||||||
|
const hit = await cache.match(url);
|
||||||
|
if (hit) return await hit.blob();
|
||||||
|
|
||||||
|
const res = await fetch(url, { cache: "no-store" });
|
||||||
|
if (!res.ok) throw new Error(`Fetch failed: ${res.status}`);
|
||||||
|
|
||||||
|
await cache.put(url, res.clone());
|
||||||
|
return await res.blob();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function evictFromCache(url: string) {
|
||||||
|
const cache = await caches.open("app-sounds-v1");
|
||||||
|
await cache.delete(url);
|
||||||
|
}
|
||||||
24
src/utils/soundResolver.ts
Normal file
24
src/utils/soundResolver.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { getSoundFileURL } from "./utils";
|
||||||
|
import { CAM_BASE } from "./config";
|
||||||
|
import type { SoundUploadValue } from "../types/types";
|
||||||
|
|
||||||
|
export function resolveSoundSource(
|
||||||
|
selected: string | undefined,
|
||||||
|
soundOptions: SoundUploadValue[] | undefined
|
||||||
|
): { type: "uploaded"; url: string } | { type: "builtin"; url: string } | undefined {
|
||||||
|
if (!selected) return undefined;
|
||||||
|
|
||||||
|
const isFile = selected.endsWith(".mp3") || selected.endsWith(".wav");
|
||||||
|
|
||||||
|
if (isFile) {
|
||||||
|
const match = soundOptions?.find((o) => o.soundFileName === selected);
|
||||||
|
const version = match?.uploadedAt ?? 0;
|
||||||
|
const url = `${CAM_BASE}/Mobile/${encodeURIComponent(selected)}?v=${version}`;
|
||||||
|
return { type: "uploaded", url };
|
||||||
|
}
|
||||||
|
|
||||||
|
const builtin = getSoundFileURL(selected);
|
||||||
|
if (builtin) return { type: "builtin", url: builtin };
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user