Files
var-monorepo/apps/dispatch/app/_components/navbar/Settings.tsx
2025-07-24 13:30:54 -07:00

292 lines
8.4 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { GearIcon } from "@radix-ui/react-icons";
import { Bell, SettingsIcon, Volume2 } from "lucide-react";
import MicVolumeBar from "_components/MicVolumeIndication";
import { useMutation, useQuery } from "@tanstack/react-query";
import { editUserAPI, getUserAPI } from "_querys/user";
import { useSession } from "next-auth/react";
import { useAudioStore } from "_store/audioStore";
import toast from "react-hot-toast";
import Link from "next/link";
export const SettingsBtn = () => {
const session = useSession();
const [inputDevices, setInputDevices] = useState<MediaDeviceInfo[]>([]);
const { data: user } = useQuery({
queryKey: ["user", session.data?.user.id],
queryFn: () => getUserAPI(session.data!.user.id),
});
const testSoundRef = useRef<HTMLAudioElement | null>(null);
const editUserMutation = useMutation({
mutationFn: editUserAPI,
});
useEffect(() => {
if (typeof window !== "undefined") {
testSoundRef.current = new Audio("/sounds/DME-new-mission.wav");
}
}, []);
const modalRef = useRef<HTMLDialogElement>(null);
const [showIndication, setShowIndication] = useState<boolean>(false);
const [settings, setSettings] = useState({
micDeviceId: user?.settingsMicDevice || null,
micVolume: user?.settingsMicVolume || 1,
radioVolume: user?.settingsRadioVolume || 0.8,
dmeVolume: user?.settingsDmeVolume || 0.8,
pilotNtfyRoom: user?.settingsNtfyRoom || "",
});
const { setSettings: setAudioSettings } = useAudioStore((state) => state);
useEffect(() => {
if (user) {
setAudioSettings({
micDeviceId: user.settingsMicDevice,
micVolume: user.settingsMicVolume || 1,
radioVolume: user.settingsRadioVolume || 0.8,
dmeVolume: user.settingsDmeVolume || 0.8,
});
setSettings({
micDeviceId: user.settingsMicDevice,
micVolume: user.settingsMicVolume || 1,
radioVolume: user.settingsRadioVolume || 0.8,
dmeVolume: user.settingsDmeVolume || 0.8,
pilotNtfyRoom: user.settingsNtfyRoom || "",
});
}
}, [user, setSettings, setAudioSettings]);
const setSettingsPartial = (newSettings: Partial<typeof settings>) => {
setSettings((prev) => ({
...prev,
...newSettings,
}));
};
useEffect(() => {
const setDevices = async () => {
if (typeof navigator !== "undefined" && navigator.mediaDevices?.enumerateDevices) {
const stream = await navigator.mediaDevices.getUserMedia({ video: false, audio: true });
const devices = await navigator.mediaDevices.enumerateDevices();
setInputDevices(devices.filter((d) => d.kind === "audioinput"));
stream.getTracks().forEach((track) => track.stop());
}
};
setDevices();
}, []);
return (
<div>
<button
className="btn btn-ghost"
onSubmit={() => false}
onClick={() => {
modalRef.current?.showModal();
}}
>
<GearIcon className="h-5 w-5" />
</button>
<dialog ref={modalRef} className="modal">
<div className="modal-box">
<h3 className="mb-5 flex items-center gap-2 text-lg font-bold">
<SettingsIcon size={20} /> Einstellungen
</h3>
<div className="flex flex-col items-center justify-center">
<fieldset className="fieldset mb-2 w-full">
<label className="floating-label w-full text-base">
<span>Eingabegerät</span>
<select
className="input w-full"
value={settings.micDeviceId ? settings.micDeviceId : ""}
onChange={(e) => {
setSettingsPartial({ micDeviceId: e.target.value });
setShowIndication(true);
}}
>
<option key={0} value={0} disabled>
Bitte wähle ein Eingabegerät...
</option>
{inputDevices.map((device, index) => (
<option key={index} value={device.deviceId}>
{device.label}
</option>
))}
</select>
</label>
</fieldset>
<p className="mb-2 flex w-full items-center justify-start gap-2 text-base">
<Volume2 size={20} /> Eingabelautstärke
</p>
<div className="w-full">
<input
type="range"
min={0}
max={3}
step={0.01}
onChange={(e) => {
const value = parseFloat(e.target.value);
setSettingsPartial({ micVolume: value });
setShowIndication(true);
}}
value={settings.micVolume}
className="range range-xs range-accent w-full"
/>
<div className="mt-2 flex justify-between px-2.5 text-xs">
<span>0%</span>
<span>25%</span>
<span>50%</span>
<span>75%</span>
<span>100%</span>
</div>
</div>
{showIndication && (
<MicVolumeBar
deviceId={settings.micDeviceId ? settings.micDeviceId : ""}
volumeInput={settings.micVolume}
/>
)}
<div className="divider w-full" />
</div>
<p className="mb-2 flex items-center gap-2 text-base">
<Volume2 size={20} /> Funk Lautstärke
</p>
<div className="mb-2 w-full">
<input
type="range"
min={0}
max={1}
step={0.01}
onChange={(e) => {
const value = parseFloat(e.target.value);
setSettingsPartial({ radioVolume: value });
}}
value={settings.radioVolume}
className="range range-xs range-primary w-full"
/>
<div className="mt-2 flex justify-between px-2.5 text-xs">
<span>0%</span>
<span>25%</span>
<span>50%</span>
<span>75%</span>
<span>100%</span>
</div>
</div>
<p className="mb-2 flex items-center gap-2 text-base">
<Volume2 size={20} /> Melder Lautstärke
</p>
<div className="w-full">
<input
type="range"
min={0}
max={1}
step={0.01}
onChange={(e) => {
const value = parseFloat(e.target.value);
setSettingsPartial({ dmeVolume: value });
if (!testSoundRef.current) return;
testSoundRef.current.volume = value;
testSoundRef.current.play();
}}
value={settings.dmeVolume}
className="range range-xs range-primary w-full"
/>
<div className="mt-2 flex justify-between px-2.5 text-xs">
<span>0%</span>
<span>25%</span>
<span>50%</span>
<span>75%</span>
<span>100%</span>
</div>
</div>
<div className="flex w-full justify-center">
<div className="divider w-full" />
</div>
<div className="w-full">
<label className="floating-label w-full">
<span className="flex items-center gap-2 text-lg">
<Bell /> NTFY room
</span>
<input
placeholder="Erhalte eine Benachrichtigung auf dein Handy über NTFY"
className="input input-bordered w-full"
value={settings.pilotNtfyRoom}
onChange={(e) => setSettingsPartial({ pilotNtfyRoom: e.target.value })}
/>
</label>
<p className="label mt-2 w-full">
<Link
href="https://docs.virtualairrescue.com/pilotenbereich/app-alarmierung.html#download"
target="_blank"
rel="noopener noreferrer"
className="link link-hover link-primary"
>
Hier
</Link>
findest du mehr Informationen!
</p>
</div>
<div className="modal-action flex justify-between">
<button
className="btn btn-soft"
type="submit"
onSubmit={() => false}
onClick={() => {
modalRef.current?.close();
testSoundRef.current?.pause();
}}
>
Schließen
</button>
<button
className="btn btn-soft btn-success"
type="submit"
onSubmit={() => false}
onClick={async () => {
testSoundRef.current?.pause();
await editUserMutation.mutateAsync({
id: session.data!.user.id,
user: {
settingsMicDevice: settings.micDeviceId,
settingsMicVolume: settings.micVolume,
settingsRadioVolume: settings.radioVolume,
settingsDmeVolume: settings.dmeVolume,
settingsNtfyRoom: settings.pilotNtfyRoom,
},
});
setAudioSettings({
micDeviceId: settings.micDeviceId,
micVolume: settings.micVolume,
radioVolume: settings.radioVolume,
dmeVolume: settings.dmeVolume,
});
modalRef.current?.close();
toast.success("Einstellungen gespeichert");
}}
>
Speichern
</button>
</div>
</div>
</dialog>
</div>
);
};
export const Settings = () => {
return (
<div>
<SettingsBtn />
</div>
);
};