mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Completed the audio panel
This commit is contained in:
parent
6cc1572b11
commit
065cdf3648
@ -209,6 +209,10 @@ export class AudioManager {
|
||||
if (sink instanceof RadioSink) sink.setName(`Radio ${idx++}`);
|
||||
});
|
||||
document.dispatchEvent(new CustomEvent("audioSinksUpdated"));
|
||||
this.#sources.forEach((source) => {
|
||||
if (source.getConnectedTo().includes(sink))
|
||||
source.disconnect(sink)
|
||||
})
|
||||
}
|
||||
|
||||
getGuid() {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { getApp } from "../olympusapp";
|
||||
|
||||
/* Base audio sink class */
|
||||
export class AudioSink {
|
||||
export abstract class AudioSink {
|
||||
#name: string;
|
||||
#gainNode: GainNode;
|
||||
|
||||
@ -25,4 +25,7 @@ export class AudioSink {
|
||||
getInputNode() {
|
||||
return this.#gainNode;
|
||||
}
|
||||
|
||||
abstract setPtt(ptt: boolean): void;
|
||||
abstract getPtt(): boolean;
|
||||
}
|
||||
|
||||
@ -18,9 +18,11 @@ export abstract class AudioSource {
|
||||
}
|
||||
|
||||
connect(sink: AudioSink) {
|
||||
this.getOutputNode().connect(sink.getInputNode());
|
||||
this.#connectedTo.push(sink);
|
||||
document.dispatchEvent(new CustomEvent("audioSourcesUpdated"));
|
||||
if (!this.#connectedTo.includes(sink)) {
|
||||
this.getOutputNode().connect(sink.getInputNode());
|
||||
this.#connectedTo.push(sink);
|
||||
document.dispatchEvent(new CustomEvent("audioSourcesUpdated"));
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(sinkToDisconnect?: AudioSink) {
|
||||
|
||||
@ -3,8 +3,6 @@ import { Unit } from "../unit/unit";
|
||||
import { Filter, Noise } from "./audiolibrary";
|
||||
import { AudioPacket } from "./audiopacket";
|
||||
|
||||
const MAX_DISTANCE = 1852; // Ignore clients that are further away than 1NM, to save performance.
|
||||
|
||||
export class AudioUnitPipeline {
|
||||
#inputNode: GainNode;
|
||||
#sourceUnit: Unit;
|
||||
@ -14,11 +12,15 @@ export class AudioUnitPipeline {
|
||||
#audioTrackProcessor: any;
|
||||
#encoder: AudioEncoder;
|
||||
|
||||
#wetGainNode: GainNode;
|
||||
#delayNode: DelayNode;
|
||||
#convolverNode: ConvolverNode;
|
||||
#tailOsc: Noise;
|
||||
#tailOscillator: Noise;
|
||||
|
||||
#distance: number = 0;
|
||||
#packetID = 0;
|
||||
#ptt: boolean = false;
|
||||
#maxDistance: number = 1852;
|
||||
|
||||
constructor(sourceUnit: Unit, unitID: number, inputNode: GainNode) {
|
||||
this.#sourceUnit = sourceUnit;
|
||||
@ -64,6 +66,27 @@ export class AudioUnitPipeline {
|
||||
/* Create the pipeline */
|
||||
this.#inputNode = inputNode;
|
||||
this.#setupEffects();
|
||||
|
||||
/* Create the interval task to update the data */
|
||||
setInterval(() => {
|
||||
/* Get the destination unit and compute the distance to it */
|
||||
let destinationUnit = getApp().getUnitsManager().getUnitByID(this.#unitID);
|
||||
if (destinationUnit) {
|
||||
let distance = destinationUnit?.getPosition().distanceTo(this.#sourceUnit.getPosition());
|
||||
|
||||
/* The units positions are updated at a low frequency. Filter the distance to avoid sudden volume jumps */
|
||||
this.#distance = 0.9 * this.#distance + 0.1 * distance;
|
||||
|
||||
/* Don't bother updating parameters if the client is too far away */
|
||||
if (this.#distance < this.#maxDistance) {
|
||||
/* Compute a new gain decreasing with distance. */
|
||||
let newGain = 1.0 - Math.pow(this.#distance / this.#maxDistance, 2); // Arbitrary
|
||||
|
||||
/* Set the values of the main gain node and the multitap gain node, used for reverb effect */
|
||||
this.#gainNode.gain.setValueAtTime(newGain, getApp().getAudioManager().getAudioContext().currentTime);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
handleEncodedData(encodedAudioChunk, unitID) {
|
||||
@ -92,7 +115,7 @@ export class AudioUnitPipeline {
|
||||
|
||||
handleRawData(audioData) {
|
||||
/* Ignore players that are too far away */
|
||||
if (this.#distance < MAX_DISTANCE) {
|
||||
if (this.#distance < this.#maxDistance && this.#ptt) {
|
||||
this.#encoder.encode(audioData);
|
||||
audioData.close();
|
||||
}
|
||||
@ -102,18 +125,18 @@ export class AudioUnitPipeline {
|
||||
/* Create the nodes necessary for the pipeline */
|
||||
this.#convolverNode = getApp().getAudioManager().getAudioContext().createConvolver();
|
||||
|
||||
let wetGainNode = getApp().getAudioManager().getAudioContext().createGain();
|
||||
wetGainNode.gain.setValueAtTime(2.0, getApp().getAudioManager().getAudioContext().currentTime)
|
||||
let delayNode = getApp().getAudioManager().getAudioContext().createDelay(1);
|
||||
delayNode.delayTime.setValueAtTime(0.09, getApp().getAudioManager().getAudioContext().currentTime)
|
||||
this.#wetGainNode = getApp().getAudioManager().getAudioContext().createGain();
|
||||
this.#wetGainNode.gain.setValueAtTime(2.0, getApp().getAudioManager().getAudioContext().currentTime)
|
||||
this.#delayNode = getApp().getAudioManager().getAudioContext().createDelay(1);
|
||||
this.#delayNode.delayTime.setValueAtTime(0.09, getApp().getAudioManager().getAudioContext().currentTime)
|
||||
|
||||
this.#inputNode.connect(this.#gainNode);
|
||||
|
||||
this.#gainNode.connect(this.#destinationNode);
|
||||
|
||||
this.#gainNode.connect(wetGainNode);
|
||||
wetGainNode.connect(delayNode);
|
||||
delayNode.connect(this.#convolverNode);
|
||||
this.#gainNode.connect(this.#wetGainNode);
|
||||
this.#wetGainNode.connect(this.#delayNode);
|
||||
this.#delayNode.connect(this.#convolverNode);
|
||||
this.#convolverNode.connect(this.#destinationNode);
|
||||
|
||||
/* Render the random noise needed for the convolver node to simulate reverb */
|
||||
@ -132,17 +155,17 @@ export class AudioUnitPipeline {
|
||||
);
|
||||
|
||||
/* A noise oscillator and a two filters are added to smooth the reverb */
|
||||
this.#tailOsc = new Noise(tailContext, 1);
|
||||
this.#tailOscillator = new Noise(tailContext, 1);
|
||||
const tailLPFilter = new Filter(tailContext, "lowpass", 5000, 1);
|
||||
const tailHPFilter = new Filter(tailContext, "highpass", 500, 1);
|
||||
|
||||
/* Initialize and connect the oscillator with the filters */
|
||||
this.#tailOsc.init();
|
||||
this.#tailOsc.connect(tailHPFilter.input);
|
||||
this.#tailOscillator.init();
|
||||
this.#tailOscillator.connect(tailHPFilter.input);
|
||||
tailHPFilter.connect(tailLPFilter.input);
|
||||
tailLPFilter.connect(tailContext.destination);
|
||||
this.#tailOsc.attack = attack;
|
||||
this.#tailOsc.decay = decay;
|
||||
this.#tailOscillator.attack = attack;
|
||||
this.#tailOscillator.decay = decay;
|
||||
|
||||
setTimeout(() => {
|
||||
/* Set the buffer of the convolver node */
|
||||
@ -150,8 +173,16 @@ export class AudioUnitPipeline {
|
||||
this.#convolverNode.buffer = buffer;
|
||||
});
|
||||
|
||||
this.#tailOsc.on({ frequency: 500, velocity: 127 });
|
||||
this.#tailOscillator.on({ frequency: 500, velocity: 127 });
|
||||
//tailOsc.off(); // TODO In the original example I copied, this was turned off. No idea why but it seems to work correctly if left on. To investigate.
|
||||
}, 20);
|
||||
}
|
||||
|
||||
setPtt(ptt) {
|
||||
this.#ptt = ptt;
|
||||
}
|
||||
|
||||
setMaxDistance(maxDistance) {
|
||||
this.#maxDistance = maxDistance;
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,8 @@ scramble calls and so on. Ideally, one may want to move this code to the backend
|
||||
export class UnitSink extends AudioSink {
|
||||
#unit: Unit;
|
||||
#unitPipelines: { [key: string]: AudioUnitPipeline } = {};
|
||||
#ptt: boolean = false;
|
||||
#maxDistance: number = 1852;
|
||||
|
||||
constructor(unit: Unit) {
|
||||
super();
|
||||
@ -33,6 +35,8 @@ export class UnitSink extends AudioSink {
|
||||
.forEach((unitID) => {
|
||||
if (unitID !== 0 && !(unitID in this.#unitPipelines)) {
|
||||
this.#unitPipelines[unitID] = new AudioUnitPipeline(this.#unit, unitID, this.getInputNode());
|
||||
this.#unitPipelines[unitID].setPtt(false);
|
||||
this.#unitPipelines[unitID].setMaxDistance(this.#maxDistance);
|
||||
}
|
||||
});
|
||||
|
||||
@ -42,4 +46,28 @@ export class UnitSink extends AudioSink {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setPtt(ptt) {
|
||||
this.#ptt = ptt;
|
||||
Object.values(this.#unitPipelines).forEach((pipeline) => {
|
||||
pipeline.setPtt(ptt);
|
||||
})
|
||||
document.dispatchEvent(new CustomEvent("audioSinksUpdated"));
|
||||
}
|
||||
|
||||
getPtt() {
|
||||
return this.#ptt;
|
||||
}
|
||||
|
||||
setMaxDistance(maxDistance) {
|
||||
this.#maxDistance = maxDistance;
|
||||
Object.values(this.#unitPipelines).forEach((pipeline) => {
|
||||
pipeline.setMaxDistance(maxDistance);
|
||||
})
|
||||
document.dispatchEvent(new CustomEvent("audioSinksUpdated"));
|
||||
}
|
||||
|
||||
getMaxDistance() {
|
||||
return this.#maxDistance;
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ export const EventsContext = createContext({
|
||||
setDrawingMenuVisible: (e: boolean) => {},
|
||||
setOptionsMenuVisible: (e: boolean) => {},
|
||||
setAirbaseMenuVisible: (e: boolean) => {},
|
||||
setRadioMenuVisible: (e: boolean) => {},
|
||||
setAudioMenuVisible: (e: boolean) => {},
|
||||
toggleMainMenuVisible: () => {},
|
||||
toggleSpawnMenuVisible: () => {},
|
||||
@ -17,7 +16,6 @@ export const EventsContext = createContext({
|
||||
toggleDrawingMenuVisible: () => {},
|
||||
toggleOptionsMenuVisible: () => {},
|
||||
toggleAirbaseMenuVisible: () => {},
|
||||
toggleRadioMenuVisible: () => {},
|
||||
toggleAudioMenuVisible: () => {},
|
||||
});
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import { Converter } from "usng";
|
||||
import { MGRS } from "../types/types";
|
||||
import { getApp } from "../olympusapp";
|
||||
import { featureCollection } from "turf";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
export function bearing(lat1: number, lon1: number, lat2: number, lon2: number) {
|
||||
const φ1 = deg2rad(lat1); // φ, λ in radians
|
||||
@ -110,8 +111,8 @@ export function reciprocalHeading(heading: number): number {
|
||||
* @param decimal <boolean> whether this is a decimal number or not
|
||||
*
|
||||
* */
|
||||
export const zeroAppend = function (num: number, places: number, decimal: boolean = false) {
|
||||
var string = decimal ? num.toFixed(2) : String(num);
|
||||
export const zeroAppend = function (num: number, places: number, decimal: boolean = false, decimalPlaces: number = 2) {
|
||||
var string = decimal ? num.toFixed(decimalPlaces) : String(num);
|
||||
while (string.length < places) {
|
||||
string = "0" + string;
|
||||
}
|
||||
@ -576,3 +577,14 @@ export function doubleToByteArray(number) {
|
||||
export function byteArrayToDouble(array) {
|
||||
return new DataView(array.reverse().buffer).getFloat64(0);
|
||||
}
|
||||
|
||||
export function rand(min, max) {
|
||||
return min + Math.random() * (max - min);
|
||||
}
|
||||
|
||||
export function getRandomColor(seed) {
|
||||
var h = (seed * Math.PI * 100) % 360 + 1;
|
||||
var s = 50;
|
||||
var l = 50;
|
||||
return 'hsl(' + h + ',' + s + '%,' + l + '%)';
|
||||
}
|
||||
@ -128,9 +128,6 @@ export class ShortcutManager {
|
||||
getApp()
|
||||
.getAudioManager()
|
||||
.getSinks()
|
||||
.filter((sink) => {
|
||||
return sink instanceof RadioSink;
|
||||
})
|
||||
[idx]?.setPtt(true);
|
||||
},
|
||||
code: key,
|
||||
@ -144,9 +141,6 @@ export class ShortcutManager {
|
||||
getApp()
|
||||
.getAudioManager()
|
||||
.getSinks()
|
||||
.filter((sink) => {
|
||||
return sink instanceof RadioSink;
|
||||
})
|
||||
[idx]?.setPtt(false);
|
||||
},
|
||||
code: key,
|
||||
|
||||
@ -9,7 +9,6 @@ export const StateContext = createContext({
|
||||
drawingMenuVisible: false,
|
||||
optionsMenuVisible: false,
|
||||
airbaseMenuVisible: false,
|
||||
radioMenuVisible: false,
|
||||
audioMenuVisible: false,
|
||||
mapHiddenTypes: MAP_HIDDEN_TYPES_DEFAULTS,
|
||||
mapOptions: MAP_OPTIONS_DEFAULTS,
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import { ChangeEvent } from "react";
|
||||
import { OlNumberInput } from "./olnumberinput";
|
||||
|
||||
export function OlFrequencyInput(props: { value: number; className?: string; onChange: (value: number) => void }) {
|
||||
@ -28,6 +27,7 @@ export function OlFrequencyInput(props: { value: number; className?: string; onC
|
||||
props.onChange(frequency);
|
||||
}}
|
||||
value={Math.floor(frequency / 1000000)}
|
||||
className="!min-w-28"
|
||||
></OlNumberInput>
|
||||
<div className="my-auto">.</div>
|
||||
<OlNumberInput
|
||||
@ -49,8 +49,8 @@ export function OlFrequencyInput(props: { value: number; className?: string; onC
|
||||
props.onChange(frequency);
|
||||
}}
|
||||
value={(frequency - Math.floor(frequency / 1000000) * 1000000) / 1000}
|
||||
className="!min-w-28"
|
||||
></OlNumberInput>
|
||||
<div className="my-auto">MHz</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,19 +1,51 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Menu } from "./components/menu";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { FaQuestionCircle } from "react-icons/fa";
|
||||
import { FaPlus, FaPlusCircle, FaQuestionCircle } from "react-icons/fa";
|
||||
import { AudioSourcePanel } from "./components/sourcepanel";
|
||||
import { AudioSource } from "../../audio/audiosource";
|
||||
import { FaVolumeHigh, FaX } from "react-icons/fa6";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faClose } from "@fortawesome/free-solid-svg-icons";
|
||||
import { RadioSinkPanel } from "./components/radiosinkpanel";
|
||||
import { AudioSink } from "../../audio/audiosink";
|
||||
import { RadioSink } from "../../audio/radiosink";
|
||||
import { UnitSinkPanel } from "./components/unitsinkpanel";
|
||||
import { UnitSink } from "../../audio/unitsink";
|
||||
import { FaMinus, FaVolumeHigh } from "react-icons/fa6";
|
||||
import { getRandomColor } from "../../other/utils";
|
||||
|
||||
let shortcutKeys = ["Z", "X", "C", "V", "B", "N", "M", "K", "L"];
|
||||
|
||||
export function AudioMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
|
||||
const [sinks, setSinks] = useState([] as AudioSink[]);
|
||||
const [sources, setSources] = useState([] as AudioSource[]);
|
||||
const [audioManagerEnabled, setAudioManagerEnabled] = useState(false);
|
||||
const [showTip, setShowTip] = useState(true);
|
||||
const [activeSource, setActiveSource] = useState(null as AudioSource | null);
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
/* Preallocate 128 references for the source and sink panels. If the number of references changes, React will give an error */
|
||||
const sourceRefs = Array(128)
|
||||
.fill(null)
|
||||
.map(() => {
|
||||
return useRef(null);
|
||||
});
|
||||
|
||||
const sinkRefs = Array(128)
|
||||
.fill(null)
|
||||
.map(() => {
|
||||
return useRef(null);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
/* Force a rerender */
|
||||
document.addEventListener("audioSinksUpdated", () => {
|
||||
setSinks(
|
||||
getApp()
|
||||
?.getAudioManager()
|
||||
.getSinks()
|
||||
.filter((sink) => sink instanceof AudioSink)
|
||||
.map((radio) => radio)
|
||||
);
|
||||
});
|
||||
|
||||
/* Force a rerender */
|
||||
document.addEventListener("audioSourcesUpdated", () => {
|
||||
setSources(
|
||||
@ -29,97 +61,287 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
|
||||
});
|
||||
}, []);
|
||||
|
||||
/* When the sinks or sources change, use the count state to force a rerender to update the connection lines */
|
||||
useEffect(() => {
|
||||
setCount(count + 1);
|
||||
}, [sinks, sources]);
|
||||
|
||||
/* List all the connections between the sinks and the sources */
|
||||
const connections = [] as any[];
|
||||
const lineCounters = [] as number[];
|
||||
const lineColors = [] as string[];
|
||||
let counter = 0;
|
||||
sources.forEach((source, idx) => {
|
||||
counter++;
|
||||
const color = getRandomColor(counter);
|
||||
source.getConnectedTo().forEach((sink) => {
|
||||
if (sinks.indexOf(sink as AudioSink) !== undefined) {
|
||||
connections.push([sourceRefs[idx], sinkRefs[sinks.indexOf(sink as AudioSink)]]);
|
||||
lineCounters.push(counter);
|
||||
lineColors.push(color);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* Compute the line distance to fit in the available space */
|
||||
const defaultLineDistance = 8;
|
||||
const paddingRight = Math.min(lineCounters[lineCounters.length - 1] * defaultLineDistance + 40, 96);
|
||||
const lineDistance = (paddingRight - 40) / lineCounters[lineCounters.length - 1];
|
||||
|
||||
return (
|
||||
<Menu title="Audio sources" open={props.open} showBackButton={false} onClose={props.onClose}>
|
||||
<div className="p-4 text-sm text-gray-400">The audio source panel allows you to add and manage audio sources.</div>
|
||||
<Menu title="Audio menu" open={props.open} showBackButton={false} onClose={props.onClose}>
|
||||
<div className="p-4 text-sm text-gray-400">
|
||||
The audio menu allows you to add and manage audio sources, connect them to unit loudspeakers and radios, and to tune radio frequencies.
|
||||
</div>
|
||||
|
||||
<>
|
||||
{showTip && (
|
||||
{!audioManagerEnabled && (
|
||||
<div className="mx-6 flex rounded-lg bg-olympus-400 p-4 text-sm">
|
||||
{audioManagerEnabled ? (
|
||||
<>
|
||||
<div className="my-auto">
|
||||
<FaQuestionCircle className="my-auto ml-2 mr-6 text-gray-400" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-gray-100">Use the controls to apply effects and start/stop the playback of an audio source.</div>
|
||||
<div className="text-gray-400">Sources can be connected to your radios, or attached to a unit to be played on loudspeakers.</div>
|
||||
</div>
|
||||
<div>
|
||||
<FontAwesomeIcon
|
||||
onClick={() => setShowTip(false)}
|
||||
icon={faClose}
|
||||
className={`
|
||||
ml-2 flex cursor-pointer items-center justify-center
|
||||
rounded-md p-2 text-lg
|
||||
dark:text-gray-500 dark:hover:bg-gray-700
|
||||
dark:hover:text-white
|
||||
hover:bg-gray-200
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="my-auto">
|
||||
<FaQuestionCircle className="my-auto ml-2 mr-6 text-gray-400" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-gray-100">
|
||||
To enable the audio menu, first start the audio backend with the{" "}
|
||||
<span
|
||||
className={`
|
||||
mx-1 mt-[-7px] inline-block translate-y-2 rounded-full
|
||||
border-[1px] border-white p-1
|
||||
`}
|
||||
>
|
||||
<FaVolumeHigh />
|
||||
</span>{" "}
|
||||
button on the navigation header.
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
<FaQuestionCircle className="my-4 ml-2 mr-6 text-gray-400" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-gray-100">
|
||||
To enable the audio menu, first start the audio backend with the{" "}
|
||||
<span
|
||||
className={`
|
||||
mx-1 mt-[-7px] inline-block translate-y-2 rounded-full
|
||||
border-[1px] border-white p-1
|
||||
`}
|
||||
>
|
||||
<FaVolumeHigh />
|
||||
</span>{" "}
|
||||
button on the navigation header.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
<div
|
||||
className={`
|
||||
flex flex-col gap-2 p-5 font-normal text-gray-800
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
<>
|
||||
{sources.map((source, idx) => {
|
||||
return <AudioSourcePanel key={idx} source={source} />;
|
||||
<div className="flex flex-col gap-3">
|
||||
<div
|
||||
className={`
|
||||
flex flex-col gap-2 px-5 font-normal text-gray-800
|
||||
dark:text-white
|
||||
`}
|
||||
style={{ paddingRight: `${paddingRight}px` }}
|
||||
>
|
||||
{audioManagerEnabled && <span>Audio sources</span>}
|
||||
<>
|
||||
{sources.map((source, idx) => {
|
||||
return (
|
||||
<AudioSourcePanel
|
||||
key={idx}
|
||||
source={source}
|
||||
ref={sourceRefs[idx]}
|
||||
onExpanded={() => {
|
||||
setCount(count + 1);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
{audioManagerEnabled && (
|
||||
<button
|
||||
type="button"
|
||||
className={`
|
||||
mb-2 rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
onClick={() => {
|
||||
var input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.click();
|
||||
input.onchange = (e: Event) => {
|
||||
let target = e.target as HTMLInputElement;
|
||||
if (target && target.files) {
|
||||
var file = target.files[0];
|
||||
getApp().getAudioManager().addFileSource(file);
|
||||
}
|
||||
};
|
||||
}}
|
||||
>
|
||||
Add audio source
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col gap-2 px-5 font-normal text-gray-800
|
||||
dark:text-white
|
||||
`}
|
||||
style={{ paddingRight: `${paddingRight}px` }}
|
||||
>
|
||||
{audioManagerEnabled && <span>Radios</span>}
|
||||
{sinks.map((sink, idx) => {
|
||||
if (sink instanceof RadioSink)
|
||||
return (
|
||||
<RadioSinkPanel
|
||||
shortcutKey={shortcutKeys[idx]}
|
||||
key={sink.getName()}
|
||||
radio={sink}
|
||||
onExpanded={() => {
|
||||
setCount(count + 1);
|
||||
}}
|
||||
ref={sinkRefs[idx]}
|
||||
></RadioSinkPanel>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
{audioManagerEnabled && (
|
||||
<button
|
||||
type="button"
|
||||
className={`
|
||||
mb-2 me-2 rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
onClick={() => {
|
||||
var input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.click();
|
||||
input.onchange = (e: Event) => {
|
||||
let target = e.target as HTMLInputElement;
|
||||
if (target && target.files) {
|
||||
var file = target.files[0];
|
||||
getApp().getAudioManager().addFileSource(file);
|
||||
}
|
||||
{audioManagerEnabled && sinks.filter((sink) => sink instanceof RadioSink).length < 10 && (
|
||||
<button
|
||||
type="button"
|
||||
className={`
|
||||
mb-2 rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
onClick={() => getApp().getAudioManager().addRadio()}
|
||||
>
|
||||
Add radio
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col gap-2 px-5 font-normal text-gray-800
|
||||
dark:text-white
|
||||
`}
|
||||
style={{ paddingRight: `${paddingRight}px` }}
|
||||
>
|
||||
{audioManagerEnabled && sinks.find((sink) => sink instanceof UnitSink) && <span>Unit loudspeakers</span>}
|
||||
{sinks.map((sink, idx) => {
|
||||
if (sink instanceof UnitSink)
|
||||
return (
|
||||
<UnitSinkPanel
|
||||
shortcutKey={shortcutKeys[idx]}
|
||||
key={sink.getName()}
|
||||
sink={sink}
|
||||
ref={sinkRefs[idx]}
|
||||
onExpanded={() => {
|
||||
setCount(count + 1);
|
||||
}}
|
||||
></UnitSinkPanel>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
{connections
|
||||
.filter((connection) => connection && connection[0].current && connection[1].current)
|
||||
.map((connection, idx) => {
|
||||
const start = connection[0].current;
|
||||
const end = connection[1].current;
|
||||
if (start && end) {
|
||||
const startRect = {
|
||||
top: (start as HTMLDivElement).offsetTop,
|
||||
bottom: (start as HTMLDivElement).offsetTop + (start as HTMLDivElement).clientHeight,
|
||||
height: (start as HTMLDivElement).clientHeight,
|
||||
right: (start as HTMLDivElement).offsetLeft + (start as HTMLDivElement).clientWidth,
|
||||
};
|
||||
|
||||
const endRect = {
|
||||
top: (end as HTMLDivElement).offsetTop,
|
||||
bottom: (end as HTMLDivElement).offsetTop + (end as HTMLDivElement).clientHeight,
|
||||
height: (end as HTMLDivElement).clientHeight,
|
||||
right: (end as HTMLDivElement).offsetLeft + (end as HTMLDivElement).clientWidth,
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`
|
||||
absolute rounded-br-md rounded-tr-md border-2 border-l-0
|
||||
`}
|
||||
style={{
|
||||
top: `${(startRect.bottom + startRect.top) / 2}px`,
|
||||
left: `${startRect.right}px`,
|
||||
height: `${endRect.top - startRect.top + (endRect.height - startRect.height) / 2}px`,
|
||||
width: `${(lineCounters[idx] - 1) * lineDistance + 30}px`,
|
||||
borderColor: lineColors[idx],
|
||||
}}
|
||||
></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
})
|
||||
.reverse()}
|
||||
</div>
|
||||
<div>
|
||||
{sourceRefs.map((sourceRef, idx) => {
|
||||
const div = sourceRef.current;
|
||||
if (div) {
|
||||
const divRect = {
|
||||
top: (div as HTMLDivElement).offsetTop,
|
||||
bottom: (div as HTMLDivElement).offsetTop + (div as HTMLDivElement).clientHeight,
|
||||
height: (div as HTMLDivElement).clientHeight,
|
||||
right: (div as HTMLDivElement).offsetLeft + (div as HTMLDivElement).clientWidth,
|
||||
};
|
||||
}}
|
||||
>
|
||||
Add audio source
|
||||
</button>
|
||||
)}
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div
|
||||
data-active={activeSource === sources[idx]}
|
||||
className={`
|
||||
absolute translate-y-[-50%] cursor-pointer rounded-full
|
||||
bg-blue-600 p-1 text-xs text-white
|
||||
data-[active='true']:bg-white
|
||||
data-[active='true']:text-blue-600
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
style={{
|
||||
top: `${(divRect.bottom + divRect.top) / 2}px`,
|
||||
left: `${divRect.right - 10}px`,
|
||||
}}
|
||||
onClick={() => {
|
||||
activeSource !== sources[idx] ? setActiveSource(sources[idx]) : setActiveSource(null);
|
||||
}}
|
||||
>
|
||||
<FaPlus></FaPlus>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
})}
|
||||
{activeSource &&
|
||||
sinkRefs.map((sinkRef, idx) => {
|
||||
const div = sinkRef.current;
|
||||
if (div) {
|
||||
const divRect = {
|
||||
top: (div as HTMLDivElement).offsetTop,
|
||||
bottom: (div as HTMLDivElement).offsetTop + (div as HTMLDivElement).clientHeight,
|
||||
height: (div as HTMLDivElement).clientHeight,
|
||||
right: (div as HTMLDivElement).offsetLeft + (div as HTMLDivElement).clientWidth,
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div
|
||||
className={`
|
||||
absolute translate-y-[-50%] cursor-pointer
|
||||
rounded-full bg-blue-600 p-1 text-xs text-white
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
style={{
|
||||
top: `${(divRect.bottom + divRect.top) / 2}px`,
|
||||
left: `${divRect.right - 10}px`,
|
||||
}}
|
||||
onClick={() => {
|
||||
if (activeSource.getConnectedTo().includes(sinks[idx])) activeSource.disconnect(sinks[idx]);
|
||||
else activeSource.connect(sinks[idx]);
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
{activeSource.getConnectedTo().includes(sinks[idx]) ? <FaMinus></FaMinus> : <FaPlus></FaPlus>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@ -1,75 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { OlFrequencyInput } from "../../components/olfrequencyinput";
|
||||
import { FaTrash } from "react-icons/fa6";
|
||||
import { OlLabelToggle } from "../../components/ollabeltoggle";
|
||||
import { OlStateButton } from "../../components/olstatebutton";
|
||||
import { faEarListen, faMicrophoneLines } from "@fortawesome/free-solid-svg-icons";
|
||||
import { RadioSink } from "../../../audio/radiosink";
|
||||
import { getApp } from "../../../olympusapp";
|
||||
|
||||
export function RadioPanel(props: { radio: RadioSink; shortcutKey: string }) {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex flex-col content-center justify-between gap-2 rounded-md
|
||||
bg-olympus-200/30 py-3 pl-4 pr-5
|
||||
`}
|
||||
>
|
||||
<div className="flex content-center justify-between">
|
||||
<span className="my-auto">{props.radio.getName()}</span>
|
||||
<div
|
||||
className="cursor-pointer rounded-md bg-red-800 p-2"
|
||||
onClick={() => {
|
||||
getApp().getAudioManager().removeSink(props.radio);
|
||||
}}
|
||||
>
|
||||
<FaTrash className={`text-gray-50`}></FaTrash>
|
||||
</div>
|
||||
</div>
|
||||
<OlFrequencyInput
|
||||
value={props.radio.getFrequency()}
|
||||
onChange={(value) => {
|
||||
props.radio.setFrequency(value);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-row gap-2">
|
||||
<OlLabelToggle
|
||||
leftLabel="AM"
|
||||
rightLabel="FM"
|
||||
toggled={props.radio.getModulation() !== 0}
|
||||
onClick={() => {
|
||||
props.radio.setModulation(props.radio.getModulation() === 1 ? 0 : 1);
|
||||
}}
|
||||
></OlLabelToggle>
|
||||
|
||||
<kbd
|
||||
className={`
|
||||
my-auto ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2
|
||||
py-1.5 text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
{props.shortcutKey}
|
||||
</kbd>
|
||||
|
||||
<OlStateButton
|
||||
checked={props.radio.getPtt()}
|
||||
icon={faMicrophoneLines}
|
||||
onClick={() => {
|
||||
props.radio.setPtt(!props.radio.getPtt());
|
||||
}}
|
||||
tooltip="Talk on frequency"
|
||||
></OlStateButton>
|
||||
|
||||
<OlStateButton
|
||||
checked={props.radio.getTuned()}
|
||||
icon={faEarListen}
|
||||
onClick={() => {
|
||||
props.radio.setTuned(!props.radio.getTuned());
|
||||
}}
|
||||
tooltip="Tune to radio"
|
||||
></OlStateButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
106
frontend/react/src/ui/panels/components/radiosinkpanel.tsx
Normal file
106
frontend/react/src/ui/panels/components/radiosinkpanel.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import React, { ForwardedRef, forwardRef, useEffect, useState } from "react";
|
||||
import { OlFrequencyInput } from "../../components/olfrequencyinput";
|
||||
import { FaChevronUp, FaXmark } from "react-icons/fa6";
|
||||
import { OlLabelToggle } from "../../components/ollabeltoggle";
|
||||
import { OlStateButton } from "../../components/olstatebutton";
|
||||
import { faEarListen, faMicrophoneLines } from "@fortawesome/free-solid-svg-icons";
|
||||
import { RadioSink } from "../../../audio/radiosink";
|
||||
import { getApp } from "../../../olympusapp";
|
||||
|
||||
export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey: string; onExpanded: () => void }, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.onExpanded) props.onExpanded();
|
||||
}, [expanded])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex flex-col content-center justify-between gap-2 rounded-md
|
||||
bg-olympus-200/30 px-4 py-3
|
||||
`}
|
||||
ref={ref}
|
||||
>
|
||||
<div className="flex content-center justify-between gap-2">
|
||||
<div
|
||||
className={`h-fit w-fit cursor-pointer rounded-sm py-2`}
|
||||
onClick={() => {
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
>
|
||||
<FaChevronUp
|
||||
className={`
|
||||
text-gray-500 transition-transform
|
||||
data-[expanded='false']:rotate-180
|
||||
`}
|
||||
data-expanded={expanded}
|
||||
/>
|
||||
</div>
|
||||
{props.shortcutKey && (<>
|
||||
<kbd
|
||||
className={`
|
||||
my-auto ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2
|
||||
py-1.5 text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
{props.shortcutKey}
|
||||
</kbd>
|
||||
</>
|
||||
)}
|
||||
<span className="my-auto w-full">{props.radio.getName()} {!expanded && `: ${props.radio.getFrequency()/1e6} MHz ${props.radio.getModulation()? "FM": "AM"}`} {} </span>
|
||||
<div
|
||||
className={`
|
||||
mb-auto ml-auto aspect-square cursor-pointer rounded-md p-2
|
||||
hover:bg-white/10
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getAudioManager().removeSink(props.radio);
|
||||
}}
|
||||
>
|
||||
<FaXmark className={`my-auto text-gray-500`}></FaXmark>
|
||||
</div>
|
||||
</div>
|
||||
{expanded && (
|
||||
<>
|
||||
<OlFrequencyInput
|
||||
value={props.radio.getFrequency()}
|
||||
onChange={(value) => {
|
||||
props.radio.setFrequency(value);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-row gap-2">
|
||||
<OlLabelToggle
|
||||
leftLabel="AM"
|
||||
rightLabel="FM"
|
||||
toggled={props.radio.getModulation() !== 0}
|
||||
onClick={() => {
|
||||
props.radio.setModulation(props.radio.getModulation() === 1 ? 0 : 1);
|
||||
}}
|
||||
></OlLabelToggle>
|
||||
|
||||
<OlStateButton
|
||||
className="ml-auto"
|
||||
checked={props.radio.getPtt()}
|
||||
icon={faMicrophoneLines}
|
||||
onClick={() => {
|
||||
props.radio.setPtt(!props.radio.getPtt());
|
||||
}}
|
||||
tooltip="Talk on frequency"
|
||||
></OlStateButton>
|
||||
|
||||
<OlStateButton
|
||||
checked={props.radio.getTuned()}
|
||||
icon={faEarListen}
|
||||
onClick={() => {
|
||||
props.radio.setTuned(!props.radio.getTuned());
|
||||
}}
|
||||
tooltip="Tune to radio"
|
||||
></OlStateButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@ -1,145 +1,131 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { ForwardedRef, forwardRef, useEffect, useState } from "react";
|
||||
import { OlStateButton } from "../../components/olstatebutton";
|
||||
import { faPause, faPlay, faRepeat, faStop } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getApp } from "../../../olympusapp";
|
||||
import { AudioSource } from "../../../audio/audiosource";
|
||||
import { FaArrowRight, FaTrash, FaVolumeHigh } from "react-icons/fa6";
|
||||
import { FaChevronUp, FaVolumeHigh, FaXmark } from "react-icons/fa6";
|
||||
import { OlRangeSlider } from "../../components/olrangeslider";
|
||||
import { FaUnlink } from "react-icons/fa";
|
||||
import { OlDropdown, OlDropdownItem } from "../../components/oldropdown";
|
||||
import { FileSource } from "../../../audio/filesource";
|
||||
import { MicrophoneSource } from "../../../audio/microphonesource";
|
||||
|
||||
export function AudioSourcePanel(props: { source: AudioSource }) {
|
||||
export const AudioSourcePanel = forwardRef((props: { source: AudioSource; onExpanded: () => void }, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const [meterLevel, setMeterLevel] = useState(0);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.onExpanded) props.onExpanded();
|
||||
}, [expanded])
|
||||
|
||||
useEffect(() => {
|
||||
setInterval(() => {
|
||||
setMeterLevel(Math.min(100, props.source.getMeter().getPeaks().current[0]));
|
||||
setMeterLevel(Math.min(1, props.source.getMeter().getPeaks().current[0]));
|
||||
}, 50);
|
||||
}, []);
|
||||
|
||||
let availabileSinks = getApp()
|
||||
.getAudioManager()
|
||||
.getSinks()
|
||||
.filter((sink) => !props.source.getConnectedTo().includes(sink));
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex flex-col content-center justify-between gap-2 rounded-md
|
||||
bg-olympus-200/30 py-3 pl-4 pr-5
|
||||
bg-olympus-200/30 px-4 py-3
|
||||
`}
|
||||
ref={ref}
|
||||
>
|
||||
<div className="flex justify-between gap-2">
|
||||
<span className="break-all">{props.source.getName()}</span>
|
||||
<div
|
||||
className={`h-fit w-fit cursor-pointer rounded-sm py-2`}
|
||||
onClick={() => {
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
>
|
||||
<FaChevronUp
|
||||
className={`
|
||||
text-gray-500 transition-transform
|
||||
data-[expanded='false']:rotate-180
|
||||
`}
|
||||
data-expanded={expanded}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full overflow-hidden">
|
||||
<span
|
||||
className={`my-auto truncate`}
|
||||
>
|
||||
{props.source.getName()}
|
||||
</span>
|
||||
</div>
|
||||
{!(props.source instanceof MicrophoneSource) && (
|
||||
<div
|
||||
className={`
|
||||
mb-auto aspect-square cursor-pointer rounded-md bg-red-800 p-2
|
||||
mb-auto aspect-square cursor-pointer rounded-md p-2
|
||||
hover:bg-white/10
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getAudioManager().removeSource(props.source);
|
||||
}}
|
||||
>
|
||||
<FaTrash className={`text-gray-50`}></FaTrash>
|
||||
<FaXmark className={`my-auto text-gray-500`}></FaXmark>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 rounded-md bg-olympus-400 p-2">
|
||||
{props.source instanceof FileSource && (
|
||||
<div className="flex gap-4">
|
||||
<OlStateButton
|
||||
checked={false}
|
||||
icon={props.source.getPlaying() ? faPause : faPlay}
|
||||
onClick={() => {
|
||||
if (props.source instanceof FileSource) props.source.getPlaying() ? props.source.pause() : props.source.play();
|
||||
}}
|
||||
tooltip="Play file"
|
||||
></OlStateButton>
|
||||
<OlRangeSlider
|
||||
value={props.source.getDuration() > 0 ? (props.source.getCurrentPosition() / props.source.getDuration()) * 100 : 0}
|
||||
onChange={(ev) => {
|
||||
if (props.source instanceof FileSource) props.source.setCurrentPosition(parseFloat(ev.currentTarget.value));
|
||||
}}
|
||||
className="my-auto"
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={props.source.getLooping()}
|
||||
icon={faRepeat}
|
||||
onClick={() => {
|
||||
if (props.source instanceof FileSource) props.source.setLooping(!props.source.getLooping());
|
||||
}}
|
||||
tooltip="Loop"
|
||||
></OlStateButton>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-4">
|
||||
<div className="h-[40px] min-w-[40px] p-2">
|
||||
<FaVolumeHigh className="h-full w-full" />
|
||||
</div>
|
||||
<div className="relative flex w-full flex-col gap-3">
|
||||
<div
|
||||
className={`
|
||||
absolute top-[18px] flex h-2 min-w-full translate-y-[-5px]
|
||||
flex-row border-gray-500
|
||||
`}
|
||||
>
|
||||
<div style={{ minWidth: `${meterLevel * 100}%` }} className={`
|
||||
rounded-full bg-gray-200
|
||||
`}></div>
|
||||
{expanded && (
|
||||
<>
|
||||
{props.source instanceof FileSource && (
|
||||
<div className="flex flex-col gap-2 rounded-md bg-olympus-400 p-2">
|
||||
<div className="flex gap-4">
|
||||
<OlStateButton
|
||||
checked={false}
|
||||
icon={props.source.getPlaying() ? faPause : faPlay}
|
||||
onClick={() => {
|
||||
if (props.source instanceof FileSource) props.source.getPlaying() ? props.source.pause() : props.source.play();
|
||||
}}
|
||||
tooltip="Play file"
|
||||
></OlStateButton>
|
||||
<OlRangeSlider
|
||||
value={props.source.getDuration() > 0 ? (props.source.getCurrentPosition() / props.source.getDuration()) * 100 : 0}
|
||||
onChange={(ev) => {
|
||||
if (props.source instanceof FileSource) props.source.setCurrentPosition(parseFloat(ev.currentTarget.value));
|
||||
}}
|
||||
className="my-auto"
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={props.source.getLooping()}
|
||||
icon={faRepeat}
|
||||
onClick={() => {
|
||||
if (props.source instanceof FileSource) props.source.setLooping(!props.source.getLooping());
|
||||
}}
|
||||
tooltip="Loop"
|
||||
></OlStateButton>
|
||||
</div>
|
||||
</div>
|
||||
<OlRangeSlider
|
||||
value={props.source.getVolume() * 100}
|
||||
min={0}
|
||||
max={200}
|
||||
onChange={(ev) => {
|
||||
props.source.setVolume(parseFloat(ev.currentTarget.value) / 100);
|
||||
}}
|
||||
className="absolute top-[18px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="h-[40px] min-w-[60px] p-2 text-md">
|
||||
<span>{Math.round(props.source.getVolume() * 100)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="text-sm">Connected to:</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
{props.source.getConnectedTo().map((sink, idx) => {
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`
|
||||
flex justify-start gap-2 rounded-full bg-olympus-400 px-4 py-1
|
||||
text-sm
|
||||
`}
|
||||
>
|
||||
<FaArrowRight className="my-auto"></FaArrowRight>
|
||||
{sink.getName()}
|
||||
<FaUnlink className="my-auto ml-auto cursor-pointer text-red-400" onClick={() => props.source.disconnect(sink)}></FaUnlink>
|
||||
<div className="flex gap-4 pr-2">
|
||||
<div className="h-[40px] min-w-[40px] p-2">
|
||||
<FaVolumeHigh className="h-full w-full" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{availabileSinks.length > 0 && (
|
||||
<OlDropdown label="Connect to:">
|
||||
{availabileSinks.map((sink, idx) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
props.source.connect(sink);
|
||||
}}
|
||||
<div className="relative flex w-full flex-col gap-3">
|
||||
<div
|
||||
className={`
|
||||
absolute top-[18px] flex h-2 min-w-full translate-y-[-5px]
|
||||
flex-row border-gray-500
|
||||
`}
|
||||
>
|
||||
{sink.getName()}
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
<div style={{ minWidth: `${meterLevel * 100}%` }} className={`
|
||||
rounded-full bg-gray-200
|
||||
`}></div>
|
||||
</div>
|
||||
<OlRangeSlider
|
||||
value={props.source.getVolume() * 100}
|
||||
min={0}
|
||||
max={200}
|
||||
onChange={(ev) => {
|
||||
props.source.setVolume(parseFloat(ev.currentTarget.value) / 100);
|
||||
}}
|
||||
className="absolute top-[18px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
91
frontend/react/src/ui/panels/components/unitsinkpanel.tsx
Normal file
91
frontend/react/src/ui/panels/components/unitsinkpanel.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import React, { ForwardedRef, forwardRef, useEffect, useState } from "react";
|
||||
import { FaChevronUp, FaXmark } from "react-icons/fa6";
|
||||
import { getApp } from "../../../olympusapp";
|
||||
import { UnitSink } from "../../../audio/unitsink";
|
||||
import { OlStateButton } from "../../components/olstatebutton";
|
||||
import { faMicrophoneLines } from "@fortawesome/free-solid-svg-icons";
|
||||
import { OlRangeSlider } from "../../components/olrangeslider";
|
||||
|
||||
export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKey: string; onExpanded: () => void }, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.onExpanded) props.onExpanded();
|
||||
}, [expanded]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex flex-col content-center justify-between gap-2 rounded-md
|
||||
bg-olympus-200/30 px-4 py-3
|
||||
`}
|
||||
ref={ref}
|
||||
>
|
||||
<div className="flex content-center justify-between gap-2">
|
||||
<div
|
||||
className={`h-fit w-fit cursor-pointer rounded-sm py-2`}
|
||||
onClick={() => {
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
>
|
||||
<FaChevronUp
|
||||
className={`
|
||||
text-gray-500 transition-transform
|
||||
data-[expanded='false']:rotate-180
|
||||
`}
|
||||
data-expanded={expanded}
|
||||
/>
|
||||
</div>
|
||||
{props.shortcutKey && (<>
|
||||
<kbd
|
||||
className={`
|
||||
my-auto ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2
|
||||
py-1.5 text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
{props.shortcutKey}
|
||||
</kbd>
|
||||
</>
|
||||
)}
|
||||
<div className="flex w-full overflow-hidden">
|
||||
<span className="my-auto truncate"> {props.sink.getName()}</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
mb-auto aspect-square cursor-pointer rounded-md p-2
|
||||
hover:bg-white/10
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getAudioManager().removeSink(props.sink);
|
||||
}}
|
||||
>
|
||||
<FaXmark className={`my-auto text-gray-500`}></FaXmark>
|
||||
</div>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className="flex flex-row gap-2">
|
||||
<span className="my-auto">Near</span>
|
||||
<OlRangeSlider
|
||||
value={((props.sink.getMaxDistance() - 100) / (1852 - 100)) * 100}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={(ev) => {
|
||||
props.sink.setMaxDistance((parseFloat(ev.currentTarget.value) / 100) * (1852 - 100) + 100);
|
||||
}}
|
||||
className="my-auto h-16"
|
||||
/>
|
||||
<span className="my-auto">Far</span>
|
||||
<OlStateButton
|
||||
checked={props.sink.getPtt()}
|
||||
icon={faMicrophoneLines}
|
||||
onClick={() => {
|
||||
props.sink.setPtt(!props.sink.getPtt());
|
||||
}}
|
||||
tooltip="Talk on frequency"
|
||||
></OlStateButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@ -1,116 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Menu } from "./components/menu";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { RadioPanel } from "./components/radiopanel";
|
||||
import { FaQuestionCircle } from "react-icons/fa";
|
||||
import { RadioSink } from "../../audio/radiosink";
|
||||
import { FaVolumeHigh } from "react-icons/fa6";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faClose } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
let shortcutKeys = ["Z", "X", "C", "V", "B", "N", "M", "K", "L"];
|
||||
|
||||
export function RadioMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
|
||||
const [radios, setRadios] = useState([] as RadioSink[]);
|
||||
const [audioManagerEnabled, setAudioManagerEnabled] = useState(false);
|
||||
const [showTip, setShowTip] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
/* Force a rerender */
|
||||
document.addEventListener("audioSinksUpdated", () => {
|
||||
setRadios(
|
||||
getApp()
|
||||
?.getAudioManager()
|
||||
.getSinks()
|
||||
.filter((sink) => sink instanceof RadioSink)
|
||||
.map((radio) => radio)
|
||||
);
|
||||
});
|
||||
|
||||
document.addEventListener("audioManagerStateChanged", () => {
|
||||
setAudioManagerEnabled(getApp().getAudioManager().isRunning());
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Menu title="Radio" open={props.open} showBackButton={false} onClose={props.onClose}>
|
||||
<div className="p-4 text-sm text-gray-400">The radio menu allows you to talk on radio to the players online using SRS.</div>
|
||||
<>
|
||||
{showTip && (
|
||||
<div className="mx-6 flex rounded-lg bg-olympus-400 p-4 text-sm">
|
||||
{audioManagerEnabled ? (
|
||||
<>
|
||||
<div className="my-auto">
|
||||
<FaQuestionCircle className="my-auto ml-2 mr-6 text-gray-400" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-gray-100">Use the radio controls to tune to a frequency, then click on the PTT button to talk. </div>
|
||||
<div className="text-gray-400">You can add up to 10 radios. Use the audio effects menu to play audio tracks or to add background noises.</div>
|
||||
</div>
|
||||
<div>
|
||||
<FontAwesomeIcon
|
||||
onClick={() => setShowTip(false)}
|
||||
icon={faClose}
|
||||
className={`
|
||||
ml-2 flex cursor-pointer items-center justify-center
|
||||
rounded-md p-2 text-lg
|
||||
dark:text-gray-500 dark:hover:bg-gray-700
|
||||
dark:hover:text-white
|
||||
hover:bg-gray-200
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<FaQuestionCircle className="my-4 ml-2 mr-6 text-gray-400" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-gray-100">
|
||||
To enable the radio menu, first start the audio backend with the{" "}
|
||||
<span
|
||||
className={`
|
||||
mx-1 mt-[-7px] inline-block translate-y-2 rounded-full
|
||||
border-[1px] border-white p-1
|
||||
`}
|
||||
>
|
||||
<FaVolumeHigh />
|
||||
</span>{" "}
|
||||
button on the navigation header.
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
<div
|
||||
className={`
|
||||
flex flex-col gap-2 p-5 font-normal text-gray-800
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
{radios.map((radio, idx) => {
|
||||
return <RadioPanel shortcutKey={shortcutKeys[idx]} key={radio.getName()} radio={radio}></RadioPanel>;
|
||||
})}
|
||||
{audioManagerEnabled && radios.length < 10 && (
|
||||
<button
|
||||
type="button"
|
||||
className={`
|
||||
mb-2 me-2 rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
onClick={() => getApp().getAudioManager().addRadio()}
|
||||
>
|
||||
Add radio
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@ -52,12 +52,6 @@ export function SideBar() {
|
||||
icon={faPencil}
|
||||
tooltip="Hide/show drawing menu"
|
||||
></OlStateButton>
|
||||
<OlStateButton
|
||||
onClick={events.toggleRadioMenuVisible}
|
||||
checked={appState.radioMenuVisible}
|
||||
icon={faRadio}
|
||||
tooltip="Hide/show radio menu"
|
||||
></OlStateButton>
|
||||
<OlStateButton
|
||||
onClick={events.toggleAudioMenuVisible}
|
||||
checked={appState.audioMenuVisible}
|
||||
|
||||
@ -44,7 +44,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { OlSearchBar } from "../components/olsearchbar";
|
||||
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
|
||||
import { UnitBlueprint } from "../../interfaces";
|
||||
import { FaRadio } from "react-icons/fa6";
|
||||
import { FaRadio, FaVolumeHigh } from "react-icons/fa6";
|
||||
import { OlNumberInput } from "../components/olnumberinput";
|
||||
import { Radio, TACAN } from "../../interfaces";
|
||||
import { OlStringInput } from "../components/olstringinput";
|
||||
@ -105,6 +105,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
const [filterString, setFilterString] = useState("");
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||
const [activeAdvancedSettings, setActiveAdvancedSettings] = useState(null as null | { radio: Radio; TACAN: TACAN });
|
||||
const [audioManagerEnabled, setAudioManagerEnabled] = useState(false);
|
||||
|
||||
var searchBarRef = useRef(null);
|
||||
|
||||
@ -130,6 +131,10 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
document.addEventListener("clearSelection", () => {
|
||||
setSelectedUnits([]);
|
||||
});
|
||||
|
||||
document.addEventListener("audioManagerStateChanged", () => {
|
||||
setAudioManagerEnabled(getApp().getAudioManager().isRunning());
|
||||
});
|
||||
}, []);
|
||||
|
||||
/* Update the current values of the shown data */
|
||||
@ -545,7 +550,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex flex-col">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
@ -608,7 +613,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex flex-col">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
@ -664,7 +669,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex flex-col gap-2">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
@ -702,7 +707,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className={`flex flex-col gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
@ -734,7 +739,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex flex-col gap-2">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
@ -773,7 +778,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
@ -807,7 +812,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
@ -871,7 +876,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
@ -896,7 +901,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
@ -922,7 +927,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className={`flex flex-col gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
@ -954,7 +959,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex flex-col gap-2">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
@ -987,7 +992,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
@ -1012,7 +1017,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
@ -1036,7 +1041,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
@ -1062,40 +1067,55 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Enable loudspeakers
|
||||
Loudspeakers
|
||||
</span>
|
||||
<OlToggle
|
||||
toggled={selectedUnitsData.isAudioSink}
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
if (!selectedUnitsData.isAudioSink) {
|
||||
getApp()?.getAudioManager().addUnitSink(unit);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
isAudioSink: true,
|
||||
});
|
||||
} else {
|
||||
let sink = getApp()
|
||||
?.getAudioManager()
|
||||
.getSinks()
|
||||
.find((sink) => {
|
||||
return sink instanceof UnitSink && sink.getUnit() === unit;
|
||||
{audioManagerEnabled ? (
|
||||
<OlToggle
|
||||
toggled={selectedUnitsData.isAudioSink}
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
if (!selectedUnitsData.isAudioSink) {
|
||||
getApp()?.getAudioManager().addUnitSink(unit);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
isAudioSink: true,
|
||||
});
|
||||
if (sink !== undefined) getApp()?.getAudioManager().removeSink(sink);
|
||||
} else {
|
||||
let sink = getApp()
|
||||
?.getAudioManager()
|
||||
.getSinks()
|
||||
.find((sink) => {
|
||||
return sink instanceof UnitSink && sink.getUnit() === unit;
|
||||
});
|
||||
if (sink !== undefined) getApp()?.getAudioManager().removeSink(sink);
|
||||
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
isAudioSink: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
isAudioSink: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-white">
|
||||
Enable audio with{" "}
|
||||
<span
|
||||
className={`
|
||||
mx-1 mt-[-7px] inline-block translate-y-2
|
||||
rounded-full border-[1px] border-white p-1
|
||||
`}
|
||||
>
|
||||
<FaVolumeHigh />
|
||||
</span>{" "}first
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ============== Audio sink toggle END ============== */}
|
||||
</div>
|
||||
)}
|
||||
@ -1196,10 +1216,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
value={activeAdvancedSettings ? activeAdvancedSettings.TACAN.channel : 1}
|
||||
></OlNumberInput>
|
||||
|
||||
<OlDropdown
|
||||
label={activeAdvancedSettings ? activeAdvancedSettings.TACAN.XY : "X"}
|
||||
className={`my-auto w-20`}
|
||||
>
|
||||
<OlDropdown label={activeAdvancedSettings ? activeAdvancedSettings.TACAN.XY : "X"} className={`
|
||||
my-auto w-20
|
||||
`}>
|
||||
<OlDropdownItem
|
||||
key={"X"}
|
||||
onClick={() => {
|
||||
@ -1318,11 +1337,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
className={`
|
||||
flex content-center gap-2 rounded-full
|
||||
${selectedUnits[0].getFuel() > 40 && `bg-green-700`}
|
||||
${
|
||||
selectedUnits[0].getFuel() > 10 &&
|
||||
selectedUnits[0].getFuel() <= 40 &&
|
||||
`bg-yellow-700`
|
||||
}
|
||||
${selectedUnits[0].getFuel() > 10 && selectedUnits[0].getFuel() <= 40 && `
|
||||
bg-yellow-700
|
||||
`}
|
||||
${selectedUnits[0].getFuel() <= 10 && `bg-red-700`}
|
||||
px-2 py-1 text-sm font-bold text-white
|
||||
`}
|
||||
|
||||
@ -22,7 +22,6 @@ import { ControlsPanel } from "./panels/controlspanel";
|
||||
import { MapContextMenu } from "./contextmenus/mapcontextmenu";
|
||||
import { AirbaseMenu } from "./panels/airbasemenu";
|
||||
import { Airbase } from "../mission/airbase";
|
||||
import { RadioMenu } from "./panels/radiomenu";
|
||||
import { AudioMenu } from "./panels/audiomenu";
|
||||
import { FormationMenu } from "./panels/formationmenu";
|
||||
import { Unit } from "../unit/unit";
|
||||
@ -48,7 +47,6 @@ export function UI() {
|
||||
const [unitControlMenuVisible, setUnitControlMenuVisible] = useState(false);
|
||||
const [measureMenuVisible, setMeasureMenuVisible] = useState(false);
|
||||
const [drawingMenuVisible, setDrawingMenuVisible] = useState(false);
|
||||
const [radioMenuVisible, setRadioMenuVisible] = useState(false);
|
||||
const [audioMenuVisible, setAudioMenuVisible] = useState(false);
|
||||
const [optionsMenuVisible, setOptionsMenuVisible] = useState(false);
|
||||
const [airbaseMenuVisible, setAirbaseMenuVisible] = useState(false);
|
||||
@ -134,7 +132,6 @@ export function UI() {
|
||||
setDrawingMenuVisible(false);
|
||||
setOptionsMenuVisible(false);
|
||||
setAirbaseMenuVisible(false);
|
||||
setRadioMenuVisible(false);
|
||||
setAudioMenuVisible(false);
|
||||
setFormationMenuVisible(false);
|
||||
setUnitExplosionMenuVisible(false);
|
||||
@ -186,7 +183,6 @@ export function UI() {
|
||||
drawingMenuVisible: drawingMenuVisible,
|
||||
optionsMenuVisible: optionsMenuVisible,
|
||||
airbaseMenuVisible: airbaseMenuVisible,
|
||||
radioMenuVisible: radioMenuVisible,
|
||||
audioMenuVisible: audioMenuVisible,
|
||||
mapOptions: mapOptions,
|
||||
mapHiddenTypes: mapHiddenTypes,
|
||||
@ -204,7 +200,6 @@ export function UI() {
|
||||
setMeasureMenuVisible: setMeasureMenuVisible,
|
||||
setOptionsMenuVisible: setOptionsMenuVisible,
|
||||
setAirbaseMenuVisible: setAirbaseMenuVisible,
|
||||
setRadioMenuVisible: setRadioMenuVisible,
|
||||
setAudioMenuVisible: setAudioMenuVisible,
|
||||
toggleMainMenuVisible: () => {
|
||||
hideAllMenus();
|
||||
@ -234,10 +229,6 @@ export function UI() {
|
||||
hideAllMenus();
|
||||
setAirbaseMenuVisible(!airbaseMenuVisible);
|
||||
},
|
||||
toggleRadioMenuVisible: () => {
|
||||
hideAllMenus();
|
||||
setRadioMenuVisible(!radioMenuVisible);
|
||||
},
|
||||
toggleAudioMenuVisible: () => {
|
||||
hideAllMenus();
|
||||
setAudioMenuVisible(!audioMenuVisible);
|
||||
@ -295,7 +286,6 @@ export function UI() {
|
||||
<UnitControlMenu open={unitControlMenuVisible} onClose={() => setUnitControlMenuVisible(false)} />
|
||||
<DrawingMenu open={drawingMenuVisible} onClose={() => setDrawingMenuVisible(false)} />
|
||||
<AirbaseMenu open={airbaseMenuVisible} onClose={() => setAirbaseMenuVisible(false)} airbase={airbase} />
|
||||
<RadioMenu open={radioMenuVisible} onClose={() => setRadioMenuVisible(false)} />
|
||||
<AudioMenu open={audioMenuVisible} onClose={() => setAudioMenuVisible(false)} />
|
||||
<FormationMenu open={formationMenuVisible} leader={formationLeader} wingmen={formationWingmen} onClose={() => setFormationMenuVisible(false)} />
|
||||
<UnitExplosionMenu open={unitExplosionMenuVisible} units={unitExplosionUnits} onClose={() => setUnitExplosionMenuVisible(false)} />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user