diff --git a/databases/units/navyunitdatabase.json b/databases/units/navyunitdatabase.json index 0c90d0ad..1a945f6c 100644 --- a/databases/units/navyunitdatabase.json +++ b/databases/units/navyunitdatabase.json @@ -15,7 +15,9 @@ "description": "", "abilities": "", "canTargetPoint": false, - "canRearm": false + "canRearm": false, + "carrierFilename": "nimitz.svg", + "length": 300 }, "CVN_72": { "name": "CVN_72", @@ -33,7 +35,9 @@ "description": "", "abilities": "", "canTargetPoint": false, - "canRearm": false + "canRearm": false, + "carrierFilename": "nimitz.svg", + "length": 300 }, "CVN_73": { "name": "CVN_73", @@ -51,7 +55,9 @@ "description": "", "abilities": "", "canTargetPoint": false, - "canRearm": false + "canRearm": false, + "carrierFilename": "nimitz.svg", + "length": 300 }, "CVN_75": { "name": "CVN_75", @@ -91,7 +97,9 @@ "description": "", "abilities": "", "canTargetPoint": false, - "canRearm": false + "canRearm": false, + "carrierFilename": "nimitz.svg", + "length": 300 }, "CV_1143_5": { "name": "CV_1143_5", diff --git a/frontend/react/public/images/carriers/kuznetsov.png b/frontend/react/public/images/carriers/kuznetsov.png new file mode 100644 index 00000000..ee23640c Binary files /dev/null and b/frontend/react/public/images/carriers/kuznetsov.png differ diff --git a/frontend/react/public/images/carriers/nimitz.png b/frontend/react/public/images/carriers/nimitz.png new file mode 100644 index 00000000..6cfcf7b6 Binary files /dev/null and b/frontend/react/public/images/carriers/nimitz.png differ diff --git a/frontend/react/src/audio/audiomanager.ts b/frontend/react/src/audio/audiomanager.ts index 66b59175..e8834e88 100644 --- a/frontend/react/src/audio/audiomanager.ts +++ b/frontend/react/src/audio/audiomanager.ts @@ -86,6 +86,7 @@ export class AudioManager { }); } else { this.#SRSClientUnitIDs = JSON.parse(new TextDecoder().decode(packetUint8Array.slice(1))).unitIDs; + document.dispatchEvent(new CustomEvent("SRSClientsUpdated")); } } }); diff --git a/frontend/react/src/audio/unitsink.ts b/frontend/react/src/audio/unitsink.ts index 3799f4f4..9d587bdf 100644 --- a/frontend/react/src/audio/unitsink.ts +++ b/frontend/react/src/audio/unitsink.ts @@ -7,26 +7,39 @@ import { AudioUnitPipeline } from "./audiounitpipeline"; 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} = {}; + #unitPipelines: { [key: string]: AudioUnitPipeline } = {}; - constructor(sourceUnit: Unit) { + constructor(unit: Unit) { super(); - this.#unit = sourceUnit; - this.setName(`${sourceUnit.getUnitName()} - ${sourceUnit.getName()}`); + this.#unit = unit; + this.setName(`${unit.getUnitName()} - ${unit.getName()}`); - /* TODO as of now, any client connecting after the sink was created will not receive the sound. Add ability to add new pipelines */ - getApp() - .getAudioManager() - .getSRSClientsUnitIDs() - .forEach((unitID) => { - if (unitID !== 0) { - this.#unitPipelines[unitID] = new AudioUnitPipeline(sourceUnit, unitID, this.getInputNode()); - } - }); + document.addEventListener("SRSClientsUpdated", () => { + this.#updatePipelines(); + }); + + this.#updatePipelines(); } getUnit() { return this.#unit; } + + #updatePipelines() { + getApp() + .getAudioManager() + .getSRSClientsUnitIDs() + .forEach((unitID) => { + if (unitID !== 0 && !(unitID in this.#unitPipelines)) { + this.#unitPipelines[unitID] = new AudioUnitPipeline(this.#unit, unitID, this.getInputNode()); + } + }); + + Object.keys(this.#unitPipelines).forEach((unitID) => { + if (!(unitID in getApp().getAudioManager().getSRSClientsUnitIDs())) { + delete this.#unitPipelines[unitID]; + } + }); + } } diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 9948ea2d..dd4506dc 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -237,6 +237,7 @@ export const defaultMapMirrors = {}; export const defaultMapLayers = {}; /* Map constants */ +export const NOT_INITIALIZED = "Not initialized"; export const IDLE = "Idle"; export const SPAWN_UNIT = "Spawn unit"; export const CONTEXT_ACTION = "Context action"; diff --git a/frontend/react/src/dom.d.ts b/frontend/react/src/dom.d.ts index a33116ed..d918388a 100644 --- a/frontend/react/src/dom.d.ts +++ b/frontend/react/src/dom.d.ts @@ -29,6 +29,7 @@ interface CustomEventMap { audioSourcesUpdated: CustomEvent; audioSinksUpdated: CustomEvent; audioManagerStateChanged: CustomEvent; + SRSClientsUpdated: CustomEvent; } declare global { diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index d5f33550..3b7f01ec 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -19,6 +19,7 @@ import { MAP_HIDDEN_TYPES_DEFAULTS, COALITIONAREA_EDIT, COALITIONAREA_DRAW_CIRCLE, + NOT_INITIALIZED, } from "../constants/constants"; import { CoalitionPolygon } from "./coalitionarea/coalitionpolygon"; import { MapHiddenTypes, MapOptions } from "../types/types"; @@ -46,7 +47,7 @@ export class Map extends L.Map { #hiddenTypes: MapHiddenTypes = MAP_HIDDEN_TYPES_DEFAULTS; /* State machine */ - #state: string; + #state: string = NOT_INITIALIZED; /* Map layers */ #theatre: string = ""; @@ -140,7 +141,7 @@ export class Map extends L.Map { this.#miniMapPolyline.addTo(this.#miniMapLayerGroup); /* Init the state machine */ - this.setState(IDLE); + setTimeout(() => this.setState(IDLE), 100); /* Register event handles */ this.on("zoomstart", (e: any) => this.#onZoomStart(e)); diff --git a/frontend/react/src/mission/airbase.ts b/frontend/react/src/mission/airbase.ts index 0c214126..1c4ec32c 100644 --- a/frontend/react/src/mission/airbase.ts +++ b/frontend/react/src/mission/airbase.ts @@ -14,11 +14,13 @@ export class Airbase extends CustomMarker { #coalition: string = ""; #properties: string[] = []; #parkings: string[] = []; + #img: HTMLImageElement; constructor(options: AirbaseOptions) { super(options.position, { riseOnHover: true }); this.#name = options.name; + this.#img = document.createElement("img"); } createIcon() { @@ -32,10 +34,10 @@ export class Airbase extends CustomMarker { var el = document.createElement("div"); el.classList.add("airbase-icon"); el.setAttribute("data-object", "airbase"); - var img = document.createElement("img"); - img.src = "/vite/images/markers/airbase.svg"; - img.onload = () => SVGInjector(img); - el.appendChild(img); + + this.#img.src = "/vite/images/markers/airbase.svg"; + this.#img.onload = () => SVGInjector(this.#img); + el.appendChild(this.#img); this.getElement()?.appendChild(el); el.addEventListener("mouseover", (ev) => { document.dispatchEvent(new CustomEvent("airbasemouseover", { detail: this })); @@ -86,4 +88,8 @@ export class Airbase extends CustomMarker { getParkings() { return this.#parkings; } + + getImg() { + return this.#img; + } } diff --git a/frontend/react/src/mission/carrier.ts b/frontend/react/src/mission/carrier.ts new file mode 100644 index 00000000..50c030ac --- /dev/null +++ b/frontend/react/src/mission/carrier.ts @@ -0,0 +1,58 @@ +import { DivIcon, LatLng, Map } from "leaflet"; +import { Airbase } from "./airbase"; + +export class Carrier extends Airbase { + #heading: number = 0; + + createIcon() { + var icon = new DivIcon({ + className: "leaflet-airbase-marker", + iconSize: [40, 40], + iconAnchor: [20, 20], + }); // Set the marker, className must be set to avoid white square + this.setIcon(icon); + + var el = document.createElement("div"); + el.classList.add("airbase-icon"); + el.setAttribute("data-object", "airbase"); + + this.getImg().src = "/vite/images/carriers/nimitz.png"; + this.getImg().style.width = `0px`; // Make the image immediately small to avoid giant carriers + el.appendChild(this.getImg()); + this.getElement()?.appendChild(el); + el.addEventListener("mouseover", (ev) => { + document.dispatchEvent(new CustomEvent("airbasemouseover", { detail: this })); + }); + el.addEventListener("mouseout", (ev) => { + document.dispatchEvent(new CustomEvent("airbasemouseout", { detail: this })); + }); + el.dataset.coalition = this.getCoalition(); + } + + onAdd(map: Map): this { + super.onAdd(map); + //this._map.on("zoomstart", (e: any) => this.updateSize()); + return this; + } + + onRemove(map: Map): this { + super.onRemove(map); + //this._map.off("zoomstart", (e: any) => this.updateSize()); + return this; + } + + setHeading(heading: number) { + this.#heading = heading; + this.getImg().style.transform = `rotate(${heading - 3.14 / 2}rad)`; + } + + updateSize() { + if (this._map) { + const y = this._map.getSize().y; + const x = this._map.getSize().x; + const maxMeters = this._map.containerPointToLatLng([0, y]).distanceTo(this._map.containerPointToLatLng([x, y])); + const meterPerPixel = maxMeters / x; + this.getImg().style.width = `${Math.round(333 / meterPerPixel)}px`; + } + } +} diff --git a/frontend/react/src/mission/missionmanager.ts b/frontend/react/src/mission/missionmanager.ts index f36b23d6..8215f9e0 100644 --- a/frontend/react/src/mission/missionmanager.ts +++ b/frontend/react/src/mission/missionmanager.ts @@ -12,11 +12,13 @@ import { navyUnitDatabase } from "../unit/databases/navyunitdatabase"; //import { Popup } from "../popups/popup"; import { AirbasesData, BullseyesData, CommandModeOptions, DateAndTime, MissionData } from "../interfaces"; import { Coalition } from "../types/types"; +import { Carrier } from "./carrier"; +import { NavyUnit } from "../unit/unit"; /** The MissionManager */ export class MissionManager { #bullseyes: { [name: string]: Bullseye } = {}; - #airbases: { [name: string]: Airbase } = {}; + #airbases: { [name: string]: Airbase | Carrier } = {}; #theatre: string = ""; #dateAndTime: DateAndTime = { date: { Year: 0, Month: 0, Day: 0 }, @@ -82,18 +84,21 @@ export class MissionManager { updateAirbases(data: AirbasesData) { for (let idx in data.airbases) { var airbase = data.airbases[idx]; - if (this.#airbases[airbase.callsign] === undefined && airbase.callsign != "") { - this.#airbases[airbase.callsign] = new Airbase({ - position: new LatLng(airbase.latitude, airbase.longitude), - name: airbase.callsign, - }).addTo(getApp().getMap()); - this.#airbases[airbase.callsign].on("click", (e) => this.#onAirbaseClick(e)); - this.#loadAirbaseChartData(airbase.callsign); + var airbaseCallsign = airbase.callsign !== ""? airbase.callsign: `carrier-${airbase.unitId}` + if (this.#airbases[airbaseCallsign] === undefined) { + if (airbase.callsign != "") { + this.#airbases[airbaseCallsign] = new Airbase({ + position: new LatLng(airbase.latitude, airbase.longitude), + name: airbaseCallsign, + }).addTo(getApp().getMap()); + this.#airbases[airbaseCallsign].on("click", (e) => this.#onAirbaseClick(e)); + this.#loadAirbaseChartData(airbaseCallsign); + } } - if (this.#airbases[airbase.callsign] != undefined && airbase.latitude && airbase.longitude && airbase.coalition) { - this.#airbases[airbase.callsign].setLatLng(new LatLng(airbase.latitude, airbase.longitude)); - this.#airbases[airbase.callsign].setCoalition(airbase.coalition); + if (this.#airbases[airbaseCallsign] != undefined && airbase.latitude && airbase.longitude && airbase.coalition) { + this.#airbases[airbaseCallsign].setLatLng(new LatLng(airbase.latitude, airbase.longitude)); + this.#airbases[airbaseCallsign].setCoalition(airbase.coalition); } } } diff --git a/frontend/react/src/olympusapp.ts b/frontend/react/src/olympusapp.ts index 43e3b7f9..7df58f4f 100644 --- a/frontend/react/src/olympusapp.ts +++ b/frontend/react/src/olympusapp.ts @@ -81,7 +81,7 @@ export class OlympusApp { getMissionManager() { return this.#missionManager as MissionManager; } - + getAudioManager() { return this.#audioManager as AudioManager; } diff --git a/frontend/react/src/shortcut/shortcutmanager.ts b/frontend/react/src/shortcut/shortcutmanager.ts index 38f59c83..6f790e03 100644 --- a/frontend/react/src/shortcut/shortcutmanager.ts +++ b/frontend/react/src/shortcut/shortcutmanager.ts @@ -1,3 +1,4 @@ +import { RadioSink } from "../audio/radiosink"; import { DEFAULT_CONTEXT } from "../constants/constants"; import { ShortcutKeyboardOptions, ShortcutMouseOptions } from "../interfaces"; import { getApp } from "../olympusapp"; @@ -119,7 +120,45 @@ export class ShortcutManager { shiftKey: false, }); - ["KeyW", "KeyA", "KeyS", "KeyD", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].forEach((code) => { + let PTTKeys = ["KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "KeyM", "KeyK", "KeyL"]; + PTTKeys.forEach((key, idx) => { + this.addKeyboardShortcut(`PTT${idx}Active`, { + altKey: false, + callback: () => { + getApp() + .getAudioManager() + .getSinks() + .filter((sink) => { + return sink instanceof RadioSink; + }) + [idx]?.setPtt(true); + }, + code: key, + context: DEFAULT_CONTEXT, + ctrlKey: false, + shiftKey: false, + event: "keydown", + }).addKeyboardShortcut(`PTT${idx}Active`, { + altKey: false, + callback: () => { + getApp() + .getAudioManager() + .getSinks() + .filter((sink) => { + return sink instanceof RadioSink; + }) + [idx]?.setPtt(false); + }, + code: key, + context: DEFAULT_CONTEXT, + ctrlKey: false, + shiftKey: false, + event: "keyup", + }); + }); + + let panKeys = ["KeyW", "KeyA", "KeyS", "KeyD", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"]; + panKeys.forEach((code) => { this.addKeyboardShortcut(`pan${code}keydown`, { altKey: false, callback: (ev: KeyboardEvent) => { diff --git a/frontend/react/src/ui/panels/audiomenu.tsx b/frontend/react/src/ui/panels/audiomenu.tsx index fff5a3ca..e5a6ca23 100644 --- a/frontend/react/src/ui/panels/audiomenu.tsx +++ b/frontend/react/src/ui/panels/audiomenu.tsx @@ -4,11 +4,14 @@ import { getApp } from "../../olympusapp"; import { FaQuestionCircle } from "react-icons/fa"; import { AudioSourcePanel } from "./components/sourcepanel"; import { AudioSource } from "../../audio/audiosource"; -import { FaVolumeHigh } from "react-icons/fa6"; +import { FaVolumeHigh, FaX } from "react-icons/fa6"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faClose } from "@fortawesome/free-solid-svg-icons"; export function AudioMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) { const [sources, setSources] = useState([] as AudioSource[]); const [audioManagerEnabled, setAudioManagerEnabled] = useState(false); + const [showTip, setShowTip] = useState(true); useEffect(() => { /* Force a rerender */ @@ -29,40 +32,56 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children? return (
The audio source panel allows you to add and manage audio sources.
-
- {audioManagerEnabled && ( - <> -
- -
-
-
Use the controls to apply effects and start/stop the playback of an audio source.
-
Sources can be connected to your radios, or attached to a unit to be played on loudspeakers.
-
- + <> + {showTip && ( +
+ {audioManagerEnabled ? ( + <> +
+ +
+
+
Use the controls to apply effects and start/stop the playback of an audio source.
+
Sources can be connected to your radios, or attached to a unit to be played on loudspeakers.
+
+
+ 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 + `} + /> +
+ + ) : ( + <> +
+ +
+
+
+ To enable the audio menu, first start the audio backend with the{" "} + + + {" "} + button on the navigation header. +
+
+ + )} +
)} - {!audioManagerEnabled && ( - <> -
- -
-
-
- To enable the audio menu, first start the audio backend with the{" "} - - - {" "} - button on the navigation header. -
-
- - )} -
+
void; children? `} > <> - {sources.map((source) => { - return ; + {sources.map((source, idx) => { + return ; })} {audioManagerEnabled && ( diff --git a/frontend/react/src/ui/panels/components/menu.tsx b/frontend/react/src/ui/panels/components/menu.tsx index 6a57cdbc..34069b9c 100644 --- a/frontend/react/src/ui/panels/components/menu.tsx +++ b/frontend/react/src/ui/panels/components/menu.tsx @@ -30,9 +30,10 @@ export function Menu(props: {
) : ( )}
diff --git a/frontend/react/src/ui/panels/components/radiopanel.tsx b/frontend/react/src/ui/panels/components/radiopanel.tsx index 3209e686..f267c5af 100644 --- a/frontend/react/src/ui/panels/components/radiopanel.tsx +++ b/frontend/react/src/ui/panels/components/radiopanel.tsx @@ -7,7 +7,7 @@ import { faEarListen, faMicrophoneLines } from "@fortawesome/free-solid-svg-icon import { RadioSink } from "../../../audio/radiosink"; import { getApp } from "../../../olympusapp"; -export function RadioPanel(props: { radio: RadioSink }) { +export function RadioPanel(props: { radio: RadioSink; shortcutKey: string }) { return (
{props.radio.getName()} -
{getApp().getAudioManager().removeSink(props.radio);}}> +
{ + getApp().getAudioManager().removeSink(props.radio); + }} + >
{ - props.radio.setFrequency(value) + props.radio.setFrequency(value); }} />
@@ -37,8 +42,17 @@ export function RadioPanel(props: { radio: RadioSink }) { }} > + + {props.shortcutKey} + + { diff --git a/frontend/react/src/ui/panels/components/sourcepanel.tsx b/frontend/react/src/ui/panels/components/sourcepanel.tsx index de5e818e..d71d8a27 100644 --- a/frontend/react/src/ui/panels/components/sourcepanel.tsx +++ b/frontend/react/src/ui/panels/components/sourcepanel.tsx @@ -54,7 +54,7 @@ export function AudioSourcePanel(props: { source: AudioSource }) { checked={false} icon={props.source.getPlaying() ? faPause : faPlay} onClick={() => { - if (props.source instanceof FileSource) props.source.getPlaying() ? props.source.stop() : props.source.play(); + if (props.source instanceof FileSource) props.source.getPlaying() ? props.source.pause() : props.source.play(); }} tooltip="Play file" > @@ -106,9 +106,10 @@ export function AudioSourcePanel(props: { source: AudioSource }) { Connected to:
- {props.source.getConnectedTo().map((sink) => { + {props.source.getConnectedTo().map((sink, idx) => { return (
{availabileSinks.length > 0 && ( - {availabileSinks.map((sink) => { + {availabileSinks.map((sink, idx) => { return ( { props.source.connect(sink); }} diff --git a/frontend/react/src/ui/panels/controlspanel.tsx b/frontend/react/src/ui/panels/controlspanel.tsx index ec64ef1d..58989076 100644 --- a/frontend/react/src/ui/panels/controlspanel.tsx +++ b/frontend/react/src/ui/panels/controlspanel.tsx @@ -33,6 +33,7 @@ export function ControlsPanel(props: {}) { {controls.map((control) => { return (
{control.actions.map((action, idx) => { return ( - <> -
+
+
{typeof action === "string" || typeof action === "number" ? action : }
{idx < control.actions.length - 1 && typeof control.actions[idx + 1] === "string" &&
+
} {idx < control.actions.length - 1 && typeof control.actions[idx + 1] === "number" &&
x
} - +
); })}
diff --git a/frontend/react/src/ui/panels/drawingmenu.tsx b/frontend/react/src/ui/panels/drawingmenu.tsx index cb1271c1..d5989230 100644 --- a/frontend/react/src/ui/panels/drawingmenu.tsx +++ b/frontend/react/src/ui/panels/drawingmenu.tsx @@ -27,22 +27,27 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { const [erasSelection, setErasSelection] = useState({}); const [rangesSelection, setRangesSelection] = useState({}); + const [showPolygonTip, setShowPolygonTip] = useState(true); + const [showCircleTip, setShowCircleTip] = useState(true); + useEffect(() => { - /* If we are not in polygon drawing mode, force the draw polygon button off */ - if (drawingPolygon && getApp().getMap().getState() !== COALITIONAREA_DRAW_POLYGON) setDrawingPolygon(false); + if (getApp()) { + /* If we are not in polygon drawing mode, force the draw polygon button off */ + if (drawingPolygon && getApp().getMap().getState() !== COALITIONAREA_DRAW_POLYGON) setDrawingPolygon(false); - /* If we are not in circle drawing mode, force the draw circle button off */ - if (drawingCircle && getApp().getMap().getState() !== COALITIONAREA_DRAW_CIRCLE) setDrawingCircle(false); + /* If we are not in circle drawing mode, force the draw circle button off */ + if (drawingCircle && getApp().getMap().getState() !== COALITIONAREA_DRAW_CIRCLE) setDrawingCircle(false); - /* If we are not in any drawing mode, force the map in edit mode */ - if (props.open && !drawingPolygon && !drawingCircle) getApp().getMap().setState(COALITIONAREA_EDIT); + /* If we are not in any drawing mode, force the map in edit mode */ + if (props.open && !drawingPolygon && !drawingCircle) getApp().getMap().setState(COALITIONAREA_EDIT); - /* Align the state of the coalition toggle to the coalition of the area */ - if (activeCoalitionArea && activeCoalitionArea?.getCoalition() !== areaCoalition) setAreaCoalition(activeCoalitionArea?.getCoalition()); + /* Align the state of the coalition toggle to the coalition of the area */ + if (activeCoalitionArea && activeCoalitionArea?.getCoalition() !== areaCoalition) setAreaCoalition(activeCoalitionArea?.getCoalition()); - if (!props.open) { - if ([COALITIONAREA_EDIT, COALITIONAREA_DRAW_CIRCLE, COALITIONAREA_DRAW_POLYGON].includes(getApp()?.getMap()?.getState())) - getApp().getMap().setState(IDLE); + if (!props.open) { + if ([COALITIONAREA_EDIT, COALITIONAREA_DRAW_CIRCLE, COALITIONAREA_DRAW_POLYGON].includes(getApp().getMap().getState())) + getApp().getMap().setState(IDLE); + } } }); @@ -79,8 +84,8 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { The draw tool allows you to quickly draw areas on the map and use these areas to spawn units and activate triggers.
-
- +
+
Use the polygon or circle tool to draw areas on the map.
@@ -217,14 +222,14 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { {getApp() .getGroundUnitDatabase() .getTypes() - .map((type) => { + .map((type, idx) => { if (!(type in typesSelection)) { typesSelection[type] = true; setTypesSelection(JSON.parse(JSON.stringify(typesSelection))); } return ( - + { diff --git a/frontend/react/src/ui/panels/radiomenu.tsx b/frontend/react/src/ui/panels/radiomenu.tsx index 9d3b3833..9bd0d856 100644 --- a/frontend/react/src/ui/panels/radiomenu.tsx +++ b/frontend/react/src/ui/panels/radiomenu.tsx @@ -5,10 +5,15 @@ 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 */ @@ -30,40 +35,56 @@ export function RadioMenu(props: { open: boolean; onClose: () => void; children? return (
The radio menu allows you to talk on radio to the players online using SRS.
-
- {audioManagerEnabled && ( - <> -
- -
-
-
Use the radio controls to tune to a frequency, then click on the PTT button to talk.
-
You can add up to 10 radios. Use the audio effects menu to play audio tracks or to add background noises.
-
- + <> + {showTip && ( +
+ {audioManagerEnabled ? ( + <> +
+ +
+
+
Use the radio controls to tune to a frequency, then click on the PTT button to talk.
+
You can add up to 10 radios. Use the audio effects menu to play audio tracks or to add background noises.
+
+
+ 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 + `} + /> +
+ + ) : ( + <> +
+ +
+
+
+ To enable the radio menu, first start the audio backend with the{" "} + + + {" "} + button on the navigation header. +
+
+ + )} +
)} - {!audioManagerEnabled && ( - <> -
- -
-
-
- To enable the radio menu, first start the audio backend with the{" "} - - - {" "} - button on the navigation header. -
-
- - )} -
+
void; children? dark:text-white `} > - {radios.map((radio) => { - return ; + {radios.map((radio, idx) => { + return ; })} {audioManagerEnabled && radios.length < 10 && (