diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index dd6aead3..4e58d9f4 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -289,6 +289,7 @@ export enum OlympusState { AUDIO = "Audio", AIRBASE = "Airbase", GAME_MASTER = "Game master", + IMPORT_EXPORT = "Import/export" } export const NO_SUBSTATE = "No substate"; @@ -334,6 +335,13 @@ export enum OptionsSubstate { KEYBIND = "Keybind", } +export enum ImportExportSubstate { + NO_SUBSTATE = "No substate", + IMPORT = "IMPORT", + EXPORT = "EXPORT" +} + + export type OlympusSubState = DrawSubState | JTACSubState | SpawnSubState | OptionsSubstate | string; export const IADSTypes = ["AAA", "SAM Site", "Radar (EWR)"]; diff --git a/frontend/react/src/events.ts b/frontend/react/src/events.ts index 97d55134..5e35c353 100644 --- a/frontend/react/src/events.ts +++ b/frontend/react/src/events.ts @@ -1,7 +1,7 @@ import { AudioSink } from "./audio/audiosink"; import { AudioSource } from "./audio/audiosource"; import { OlympusState, OlympusSubState } from "./constants/constants"; -import { CommandModeOptions, OlympusConfig, ServerStatus, SessionData, SpawnRequestTable, UnitData } from "./interfaces"; +import { CommandModeOptions, MissionData, OlympusConfig, ServerStatus, SessionData, SpawnRequestTable, UnitData } from "./interfaces"; import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle"; import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon"; import { Airbase } from "./mission/airbase"; @@ -12,6 +12,7 @@ import { ContextAction } from "./unit/contextaction"; import { ContextActionSet } from "./unit/contextactionset"; import { Unit } from "./unit/unit"; import { LatLng } from "leaflet"; +import { Weapon } from "./weapon/weapon"; export class BaseOlympusEvent { static on(callback: () => void, singleShot = false) { @@ -348,8 +349,33 @@ export class UnitSelectedEvent extends BaseUnitEvent {} export class UnitDeselectedEvent extends BaseUnitEvent {} export class UnitDeadEvent extends BaseUnitEvent {} export class SelectionClearedEvent extends BaseOlympusEvent {} -export class UnitsRefreshed extends BaseOlympusEvent {} -export class WeaponsRefreshed extends BaseOlympusEvent {} + +export class UnitsRefreshedEvent { + static on(callback: (units: { [ID: number]: Unit }) => void, singleShot = false) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail); + }, {once: singleShot}); + } + + static dispatch(units: { [ID: number]: Unit }) { + document.dispatchEvent(new CustomEvent(this.name, { detail: units })); + console.log(`Event ${this.name} dispatched`); + } +} + +export class WeaponsRefreshedEvent { + static on(callback: (weapons: { [ID: number]: Weapon }) => void, singleShot = false) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail); + }, {once: singleShot}); + } + + static dispatch(weapons: { [ID: number]: Weapon }) { + document.dispatchEvent(new CustomEvent(this.name, { detail: weapons })); + console.log(`Event ${this.name} dispatched`); + } +} + export class SelectedUnitsChangedEvent { static on(callback: (selectedUnits: Unit[]) => void, singleShot = false) { @@ -361,7 +387,6 @@ export class SelectedUnitsChangedEvent { static dispatch(selectedUnits: Unit[]) { document.dispatchEvent(new CustomEvent(this.name, { detail: selectedUnits })); console.log(`Event ${this.name} dispatched`); - console.log(selectedUnits); } } @@ -578,7 +603,7 @@ export class AudioManagerOutputChangedEvent { } /************** Mission data events ***************/ -export class BullseyesDataChanged { +export class BullseyesDataChangedEvent { static on(callback: (bullseyes: { [name: string]: Bullseye }) => void, singleShot = false) { document.addEventListener(this.name, (ev: CustomEventInit) => { callback(ev.detail.bullseyes); @@ -590,3 +615,16 @@ export class BullseyesDataChanged { // Logging disabled since periodic } } + +export class MissionDataChangedEvent { + static on(callback: (missionData: MissionData) => void, singleShot = false) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.missionData); + }, {once: singleShot}); + } + + static dispatch(missionData: MissionData) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { missionData } })); + // Logging disabled since periodic + } +} diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index 6b1eaa18..cc05c84c 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -200,6 +200,7 @@ export interface Offset { export interface UnitData { category: string; + markerCategory: string; ID: number; alive: boolean; human: boolean; diff --git a/frontend/react/src/mission/missionmanager.ts b/frontend/react/src/mission/missionmanager.ts index 2a160349..692eee09 100644 --- a/frontend/react/src/mission/missionmanager.ts +++ b/frontend/react/src/mission/missionmanager.ts @@ -6,7 +6,7 @@ import { BLUE_COMMANDER, GAME_MASTER, NONE, RED_COMMANDER } from "../constants/c import { AirbasesData, BullseyesData, CommandModeOptions, DateAndTime, MissionData } from "../interfaces"; import { Coalition } from "../types/types"; import { Carrier } from "./carrier"; -import { AirbaseSelectedEvent, AppStateChangedEvent, BullseyesDataChanged, CommandModeOptionsChangedEvent, InfoPopupEvent } from "../events"; +import { AirbaseSelectedEvent, AppStateChangedEvent, BullseyesDataChangedEvent, CommandModeOptionsChangedEvent, InfoPopupEvent, MissionDataChangedEvent } from "../events"; /** The MissionManager */ export class MissionManager { @@ -61,7 +61,7 @@ export class MissionManager { this.#bullseyes[idx].setCoalition(bullseye.coalition); } - BullseyesDataChanged.dispatch(this.#bullseyes) + BullseyesDataChangedEvent.dispatch(this.#bullseyes) } } @@ -96,6 +96,8 @@ export class MissionManager { */ updateMission(data: MissionData) { if (data.mission) { + MissionDataChangedEvent.dispatch(data); + /* Set the mission theatre */ if (data.mission.theatre != this.#theatre) { this.#theatre = data.mission.theatre; diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts index 28facd2a..9675381b 100644 --- a/frontend/react/src/server/servermanager.ts +++ b/frontend/react/src/server/servermanager.ts @@ -488,7 +488,7 @@ export class ServerManager { this.PUT(data, callback); } - setAdvacedOptions( + setAdvancedOptions( ID: number, isActiveTanker: boolean, isActiveAWACS: boolean, diff --git a/frontend/react/src/ui/modals/importexportmodal.tsx b/frontend/react/src/ui/modals/importexportmodal.tsx new file mode 100644 index 00000000..9c1d2ee2 --- /dev/null +++ b/frontend/react/src/ui/modals/importexportmodal.tsx @@ -0,0 +1,369 @@ +import React, { useEffect, useState } from "react"; +import { Modal } from "./components/modal"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; +import { getApp } from "../../olympusapp"; +import { ImportExportSubstate, NO_SUBSTATE, OlympusState } from "../../constants/constants"; +import { AppStateChangedEvent, MissionDataChangedEvent, UnitsRefreshedEvent } from "../../events"; +import { + olButtonsVisibilityDcs, + olButtonsVisibilityGroundunit, + olButtonsVisibilityGroundunitSam, + olButtonsVisibilityNavyunit, + olButtonsVisibilityOlympus, +} from "../components/olicons"; +import { OlToggle } from "../components/oltoggle"; +import { deepCopyTable } from "../../other/utils"; +import { OlCheckbox } from "../components/olcheckbox"; +import { Unit } from "../../unit/unit"; +import { MissionData, UnitData } from "../../interfaces"; + +export function ImportExportModal(props: { open: boolean }) { + const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); + const [appSubState, setAppSubState] = useState(NO_SUBSTATE); + const [units, setUnits] = useState({} as { [ID: number]: Unit }); + const [missionData, setMissionData] = useState({} as MissionData); + const [importData, setImportData] = useState({} as { [key: string]: UnitData[] }); + + function resetFilter() { + return { + control: { + dcs: true, + olympus: true, + }, + blue: { + "groundunit-sam": true, + groundunit: true, + navyunit: true, + }, + neutral: { + "groundunit-sam": true, + groundunit: true, + navyunit: true, + }, + red: { + "groundunit-sam": true, + groundunit: true, + navyunit: true, + } + }; + } + + const [selectionFilter, setSelectionFilter] = useState(resetFilter); + + useEffect(() => { + AppStateChangedEvent.on((appState, appSubState) => { + setAppState(appState); + setAppSubState(appSubState); + }); + UnitsRefreshedEvent.on((units) => setUnits(units)); + MissionDataChangedEvent.on((missionData) => setMissionData(missionData)); + }, []); + + useEffect(() => { + setSelectionFilter(resetFilter); + + if (appState === OlympusState.IMPORT_EXPORT && appSubState === ImportExportSubstate.IMPORT) { + setImportData({}); + var input = document.createElement("input"); + input.type = "file"; + + input.onchange = async (e) => { + // @ts-ignore TODO + var file = e.target?.files[0]; + var reader = new FileReader(); + reader.readAsText(file, "UTF-8"); + reader.onload = (readerEvent) => { + // @ts-ignore TODO + var content = readerEvent.target.result; + if (content) { + setImportData(JSON.parse(content as string)); + } + }; + }; + + input.click(); + } + }, [appState, appSubState]); + + const selectableUnits = Object.values(units).filter((unit) => { + return ( + unit.getAlive() && + !unit.getHuman() && + ((unit.isControlledByDCS() && selectionFilter.control.dcs) || (unit.isControlledByOlympus() && selectionFilter.control.olympus)) + ); + }); + + return ( + +
+
+ + {appSubState === ImportExportSubstate.EXPORT ? "Export to file" : "Import from file"} + + + + {appSubState === ImportExportSubstate.EXPORT ? <>Select what units you want to export to file using the toggles below : <>} + + +
+
+ Control mode +
+ +
+ {Object.entries({ + olympus: ["Olympus controlled", olButtonsVisibilityOlympus], + dcs: ["From DCS mission", olButtonsVisibilityDcs], + }).map((entry, idx) => { + return ( +
+ {entry[1][0] as string} + { + selectionFilter["control"][entry[0]] = !selectionFilter["control"][entry[0]]; + setSelectionFilter(deepCopyTable(selectionFilter)); + }} + toggled={selectionFilter["control"][entry[0]]} + /> +
+ ); + })} +
+ +
+ Types and coalitions +
+ + + + + + + + + + {Object.entries({ + "groundunit-sam": olButtonsVisibilityGroundunitSam, + groundunit: olButtonsVisibilityGroundunit, + navyunit: olButtonsVisibilityNavyunit, + }).map((entry, idx) => { + return ( + + + {["blue", "neutral", "red"].map((coalition) => { + return ( + + ); + })} + + ); + })} + { + + + + + + + } + +
BLUENEUTRALRED
+ + + unit.getMarkerCategory() === entry[0] && unit.getCoalition() === coalition) !== undefined) || + (appSubState === ImportExportSubstate.IMPORT && + selectionFilter[coalition][entry[0]] && + Object.values(importData).find((group) => + group.find((unit) => unit.markerCategory === entry[0] && unit.coalition === coalition) + ) !== undefined) + } + disabled={ + (appSubState === ImportExportSubstate.EXPORT && + selectableUnits.find((unit) => unit.getMarkerCategory() === entry[0] && unit.getCoalition() === coalition) === undefined) || + (appSubState === ImportExportSubstate.IMPORT && + Object.values(importData).find((group) => + group.find((unit) => unit.markerCategory === entry[0] && unit.coalition === coalition) + ) === undefined) + } + onChange={() => { + selectionFilter[coalition][entry[0]] = !selectionFilter[coalition][entry[0]]; + setSelectionFilter(deepCopyTable(selectionFilter)); + }} + /> + + {appSubState === ImportExportSubstate.EXPORT && + selectableUnits.filter((unit) => unit.getMarkerCategory() === entry[0] && unit.getCoalition() === coalition).length} + {appSubState === ImportExportSubstate.IMPORT && + Object.values(importData) + .flatMap((unit) => unit) + .filter((unit) => unit.markerCategory === entry[0] && unit.coalition === coalition).length}{" "} + units{" "} + +
+ value)} + onChange={() => { + const newValue = !Object.values(selectionFilter["blue"]).some((value) => value); + Object.keys(selectionFilter["blue"]).forEach((key) => { + selectionFilter["blue"][key] = newValue; + }); + setSelectionFilter(deepCopyTable(selectionFilter)); + }} + /> + + value)} + onChange={() => { + const newValue = !Object.values(selectionFilter["neutral"]).some((value) => value); + Object.keys(selectionFilter["neutral"]).forEach((key) => { + selectionFilter["neutral"][key] = newValue; + }); + setSelectionFilter(deepCopyTable(selectionFilter)); + }} + /> + + value)} + onChange={() => { + const newValue = !Object.values(selectionFilter["red"]).some((value) => value); + Object.keys(selectionFilter["red"]).forEach((key) => { + selectionFilter["red"][key] = newValue; + }); + setSelectionFilter(deepCopyTable(selectionFilter)); + }} + /> +
+
+
+ +
+ + + +
+
+
+ ); +} diff --git a/frontend/react/src/ui/panels/awacsmenu.tsx b/frontend/react/src/ui/panels/awacsmenu.tsx index 789ced1d..048e0166 100644 --- a/frontend/react/src/ui/panels/awacsmenu.tsx +++ b/frontend/react/src/ui/panels/awacsmenu.tsx @@ -4,7 +4,7 @@ import { OlToggle } from "../components/oltoggle"; import { MAP_OPTIONS_DEFAULTS } from "../../constants/constants"; import { AWACSReferenceChangedEvent as AWACSReferenceUnitChangedEvent, - BullseyesDataChanged, + BullseyesDataChangedEvent, HotgroupsChangedEvent, MapOptionsChangedEvent, UnitUpdatedEvent, @@ -27,13 +27,10 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children? MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions })); HotgroupsChangedEvent.on((hotgroups) => setHotgroups({ ...hotgroups })); AWACSReferenceUnitChangedEvent.on((unit) => setReferenceUnit(unit)); - BullseyesDataChanged.on((bullseyes) => setBullseyes(bullseyes)); + BullseyesDataChangedEvent.on((bullseyes) => setBullseyes(bullseyes)); UnitUpdatedEvent.on((unit) => setRefreshTime(Date.now())); }, []); - - - return (
setBullseyes(bullseyes)); + BullseyesDataChangedEvent.on((bullseyes) => setBullseyes(bullseyes)); SelectedUnitsChangedEvent.on((selectedUnits) => setSelectedUnits(selectedUnits)); }, []); diff --git a/frontend/react/src/ui/panels/header.tsx b/frontend/react/src/ui/panels/header.tsx index 658ef227..c6d11a6e 100644 --- a/frontend/react/src/ui/panels/header.tsx +++ b/frontend/react/src/ui/panels/header.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from "react"; import { OlRoundStateButton, OlStateButton, OlLockStateButton } from "../components/olstatebutton"; -import { faSkull, faCamera, faFlag, faLink, faUnlink, faBars, faVolumeHigh } from "@fortawesome/free-solid-svg-icons"; +import { faSkull, faCamera, faFlag, faVolumeHigh, faDownload, faUpload } from "@fortawesome/free-solid-svg-icons"; import { OlDropdownItem, OlDropdown } from "../components/oldropdown"; import { OlLabelToggle } from "../components/ollabeltoggle"; import { getApp, IP } from "../../olympusapp"; @@ -15,11 +15,11 @@ import { olButtonsVisibilityNavyunit, olButtonsVisibilityOlympus, } from "../components/olicons"; -import { FaChevronLeft, FaChevronRight, FaComputer, FaFloppyDisk, FaTabletScreenButton } from "react-icons/fa6"; +import { FaChevronLeft, FaChevronRight, FaFloppyDisk } from "react-icons/fa6"; import { CommandModeOptionsChangedEvent, ConfigLoadedEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, MapSourceChangedEvent, SessionDataChangedEvent, SessionDataSavedEvent } from "../../events"; -import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS } from "../../constants/constants"; +import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, ImportExportSubstate, MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS, OlympusState } from "../../constants/constants"; import { OlympusConfig } from "../../interfaces"; -import { FaCheck, FaSpinner } from "react-icons/fa"; +import { FaCheck, FaSave, FaSpinner } from "react-icons/fa"; export function Header() { const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS); @@ -93,11 +93,11 @@ export function Header() { >