dev, Dispatch + Pilot Settings
This commit is contained in:
@@ -2,7 +2,7 @@ import { Connection } from "./_components/Connection";
|
||||
import { Audio } from "../../../../_components/Audio/Audio";
|
||||
import { ExitIcon, ExternalLinkIcon } from "@radix-ui/react-icons";
|
||||
import Link from "next/link";
|
||||
import { Settings } from "_components/navbar/Settings";
|
||||
import { Settings } from "./_components/Settings";
|
||||
import AdminPanel from "_components/navbar/AdminPanel";
|
||||
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||
import { WarningAlert } from "_components/navbar/PageAlert";
|
||||
@@ -12,9 +12,9 @@ export default async function Navbar() {
|
||||
const session = await getServerSession();
|
||||
|
||||
return (
|
||||
<div className="navbar bg-base-100 shadow-sm flex gap-5 justify-between">
|
||||
<div className="navbar bg-base-100 flex justify-between gap-5 shadow-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="normal-case text-xl font-semibold">VAR Leitstelle V2</p>
|
||||
<p className="text-xl font-semibold normal-case">VAR Leitstelle V2</p>
|
||||
{session?.user.permissions.includes("ADMIN_KICK") && <AdminPanel />}
|
||||
</div>
|
||||
<WarningAlert />
|
||||
@@ -38,12 +38,12 @@ export default async function Navbar() {
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<button className="btn btn-ghost">
|
||||
<ExternalLinkIcon className="w-4 h-4" /> HUB
|
||||
<ExternalLinkIcon className="h-4 w-4" /> HUB
|
||||
</button>
|
||||
</Link>
|
||||
<Link href={"/logout"}>
|
||||
<button className="btn btn-ghost">
|
||||
<ExitIcon className="w-4 h-4" />
|
||||
<ExitIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { GearIcon } from "@radix-ui/react-icons";
|
||||
import { 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";
|
||||
|
||||
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,
|
||||
autoCloseMapPopup: user?.settingsAutoCloseMapPopup || false,
|
||||
});
|
||||
|
||||
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,
|
||||
autoCloseMapPopup: user.settingsAutoCloseMapPopup || false,
|
||||
});
|
||||
}
|
||||
}, [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>
|
||||
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="divider w-full">
|
||||
<div>
|
||||
<legend className="fieldset-legend">Login options</legend>
|
||||
<label className="label">
|
||||
<input type="checkbox" defaultChecked className="toggle" />
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</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,
|
||||
},
|
||||
});
|
||||
setAudioSettings({
|
||||
micDeviceId: settings.micDeviceId,
|
||||
micVolume: settings.micVolume,
|
||||
radioVolume: settings.radioVolume,
|
||||
});
|
||||
modalRef.current?.close();
|
||||
toast.success("Einstellungen gespeichert");
|
||||
}}
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const Settings = () => {
|
||||
return (
|
||||
<div>
|
||||
<SettingsBtn />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,15 +2,15 @@ import { Connection } from "./_components/Connection";
|
||||
import { Audio } from "_components/Audio/Audio";
|
||||
import { ExitIcon, ExternalLinkIcon } from "@radix-ui/react-icons";
|
||||
import Link from "next/link";
|
||||
import { Settings } from "_components/navbar/Settings";
|
||||
import { Settings } from "./_components/Settings";
|
||||
import { WarningAlert } from "_components/navbar/PageAlert";
|
||||
import { Radar } from "lucide-react";
|
||||
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<div className="navbar bg-base-100 shadow-sm flex gap-5 justify-between">
|
||||
<div className="navbar bg-base-100 flex justify-between gap-5 shadow-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="normal-case text-xl font-semibold">VAR Operations Center</p>
|
||||
<p className="text-xl font-semibold normal-case">VAR Operations Center</p>
|
||||
</div>
|
||||
<WarningAlert />
|
||||
<div className="flex items-center gap-5">
|
||||
@@ -33,12 +33,12 @@ export default function Navbar() {
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<button className="btn btn-ghost">
|
||||
<ExternalLinkIcon className="w-4 h-4" /> HUB
|
||||
<ExternalLinkIcon className="h-4 w-4" /> HUB
|
||||
</button>
|
||||
</Link>
|
||||
<Link href={"/logout"}>
|
||||
<button className="btn btn-ghost">
|
||||
<ExitIcon className="w-4 h-4" />
|
||||
<ExitIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
"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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user