- added feature to cache sounds for cross devices - should work in theory

This commit is contained in:
2025-10-29 15:04:40 +00:00
parent cf72a1e1d3
commit a8abed2246
10 changed files with 56 additions and 26 deletions

View File

@@ -18,9 +18,11 @@ const SoundUpload = () => {
soundFile: null, soundFile: null,
soundFileName: "", soundFileName: "",
soundUrl: "", soundUrl: "",
uploadedAt: Date.now(),
}; };
const handleSubmit = async (values: SoundUploadValue) => { const handleSubmit = async (values: SoundUploadValue) => {
console.log(values);
if (!values.soundFile) { if (!values.soundFile) {
toast.warning("Please select an audio file"); toast.warning("Please select an audio file");
return; return;
@@ -65,15 +67,12 @@ 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" 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) => { onChange={(e) => {
if (e.target?.files && e.target?.files[0]?.type === "audio/mpeg") { if (e.target?.files && e.target?.files[0]?.type === "audio/mpeg") {
if (e.target.files[0].size / (1024 * 1024) >= 1) {
toast.error("File is too large. Max size is 1MB");
return;
}
const url = URL.createObjectURL(e.target.files[0]); const url = URL.createObjectURL(e.target.files[0]);
setFieldValue("soundUrl", url); setFieldValue("soundUrl", url);
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());
} 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");

View File

@@ -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>

View File

@@ -44,8 +44,8 @@ export default function SightingHistoryWidget({ className, title }: SightingHist
useNow(1000); useNow(1000);
const { state } = useSoundContext(); const { state } = useSoundContext();
const { src: soundSrcHotlist } = useCachedSoundSrc(state?.hotlistSound, notification); const { src: soundSrcHotlist } = useCachedSoundSrc(state?.hotlistSound, state?.soundOptions, notification);
const { src: soundSrcNped } = useCachedSoundSrc(state?.NPEDsound, popup); const { src: soundSrcNped } = useCachedSoundSrc(state?.NPEDsound, state?.soundOptions, popup);
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 });

View File

@@ -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>
); );

View File

@@ -23,7 +23,9 @@ const uploadFile = async (file: File) => {
const getUploadFiles = async ({ queryKey }: { queryKey: string[] }) => { const getUploadFiles = async ({ queryKey }: { queryKey: string[] }) => {
const [, fileName] = queryKey; const [, fileName] = queryKey;
const url = `${camBase}/Mobile/${fileName}`;
// console.log(`${camBase}/Mobile/${fileName}`);
const url = fileName;
return getOrCacheBlob(url); return getOrCacheBlob(url);
}; };
@@ -38,7 +40,7 @@ export const useFileUpload = ({ queryKey }: UseFileUploadProps) => {
mutationFn: (file: File) => uploadFile(file), mutationFn: (file: File) => uploadFile(file),
mutationKey: ["uploadFile"], mutationKey: ["uploadFile"],
onError: (err) => toast.error(err ? err.message : ""), onError: (err) => toast.error(err ? err.message : ""),
onSuccess: (msg) => toast.success(msg), onSuccess: async (msg) => toast.success(msg),
}); });
return { query: queryKey ? query : undefined, mutation }; return { query: queryKey ? query : undefined, mutation };

View File

@@ -25,9 +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 { src: soundSrc } = useCachedSoundSrc(state?.sightingSound, switchSound); const { src: soundSrc } = useCachedSoundSrc(state?.sightingSound, state?.soundOptions, switchSound);
const { src: soundSrcHotlist } = useCachedSoundSrc(state?.hotlistSound, notification); const { src: soundSrcHotlist } = useCachedSoundSrc(state?.hotlistSound, state?.soundOptions, notification);
const { src: soundSrcNped } = useCachedSoundSrc(state?.NPEDsound, popup); const { src: soundSrcNped } = useCachedSoundSrc(state?.NPEDsound, state?.soundOptions, popup);
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 });

View File

@@ -1,13 +1,20 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useFileUpload } from "./useFileUpload"; import { useFileUpload } from "./useFileUpload";
import { getSoundFileURL } from "../utils/utils"; import { getSoundFileURL } from "../utils/utils";
import type { SoundUploadValue } from "../types/types";
import { resolveSoundSource } from "../utils/soundResolver";
export function useCachedSoundSrc(selected: string | undefined, fallbackUrl: string) { export function useCachedSoundSrc(
selected: string | undefined,
soundOptions: SoundUploadValue[] | undefined,
fallbackUrl: string
) {
const isUploaded = !!selected && (selected.endsWith(".mp3") || selected.endsWith(".wav")); const isUploaded = !!selected && (selected.endsWith(".mp3") || selected.endsWith(".wav"));
const fileName = isUploaded ? selected : undefined;
const resolved = resolveSoundSource(selected, soundOptions);
const { query } = useFileUpload({ const { query } = useFileUpload({
queryKey: fileName ? [fileName] : undefined, queryKey: resolved?.type === "uploaded" ? [resolved?.url] : undefined,
}); });
const [objectUrl, setObjectUrl] = useState<string>(); const [objectUrl, setObjectUrl] = useState<string>();
@@ -15,6 +22,7 @@ export function useCachedSoundSrc(selected: string | undefined, fallbackUrl: str
useEffect(() => { useEffect(() => {
const blob = query?.data; const blob = query?.data;
if (blob instanceof Blob) { if (blob instanceof Blob) {
if (objRef.current) URL.revokeObjectURL(objRef.current); if (objRef.current) URL.revokeObjectURL(objRef.current);
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);

View File

@@ -294,6 +294,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 = {

View File

@@ -1,12 +1,15 @@
export async function getOrCacheBlob(url: string) { export async function getOrCacheBlob(url: string) {
console.log(url);
const cache = await caches.open("app-sounds-v1"); const cache = await caches.open("app-sounds-v1");
if (cache) console.log(cache);
const hit = await cache.match(url); const hit = await cache.match(url);
if (hit) return await hit.blob(); if (hit) return await hit.blob();
const res = await fetch(url, { cache: "no-store" }); const res = await fetch(url, { cache: "no-store" });
console.log("fetching...");
if (!res.ok) throw new Error(`Fetch failed: ${res.status}`); if (!res.ok) throw new Error(`Fetch failed: ${res.status}`);
await cache.put(url, res.clone());
await cache.put(url, res.clone());
return await res.blob(); return await res.blob();
} }

View 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;
}