mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
More work on React components
This commit is contained in:
parent
8e9e6749db
commit
45e290d656
@ -19,8 +19,6 @@
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@types/react-leaflet": "^3.0.0",
|
||||
"@types/turf": "^3.5.32",
|
||||
"flowbite": "^2.3.0",
|
||||
"flowbite-react-icons": "^1.0.5",
|
||||
"js-sha256": "^0.11.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-control-mini-map": "^0.4.0",
|
||||
|
||||
@ -184,68 +184,11 @@ export const defaultMapLayers = {
|
||||
export const IDLE = "Idle";
|
||||
export const MOVE_UNIT = "Move unit";
|
||||
export const COALITIONAREA_DRAW_POLYGON = "Draw Coalition Area";
|
||||
export const visibilityControls: string[] = ["human", "dcs", "aircraft", "helicopter", "groundunit-sam", "groundunit", "navyunit", "airbase"];
|
||||
export const visibilityControlsTypes: string[][] = [["human"], ["dcs"], ["aircraft"], ["helicopter"], ["groundunit-sam"], ["groundunit"], ["navyunit"], ["airbase"]];
|
||||
export const visibilityControlsTooltips: string[] = ["Toggle human players visibility", "Toggle DCS controlled units visibility", "Toggle aircrafts visibility", "Toggle helicopter visibility", "Toggle SAM units visibility", "Toggle ground units (not SAM) visibility", "Toggle navy units visibility", "Toggle airbases visibility"];
|
||||
|
||||
//export const MAP_MARKER_CONTROLS: MapMarkerVisibilityControl[] = [{
|
||||
// "name": "Human",
|
||||
// "image": "visibility/human.svg",
|
||||
// "toggles": ["human"],
|
||||
// "tooltip": "Toggle human players' visibility"
|
||||
//}, {
|
||||
// "image": "visibility/olympus.svg",
|
||||
// "isProtected": false,
|
||||
// "name": "Olympus",
|
||||
// "protectable": false,
|
||||
// "toggles": ["olympus"],
|
||||
// "tooltip": "Toggle Olympus-controlled units' visibility"
|
||||
//}, {
|
||||
// "image": "visibility/dcs.svg",
|
||||
// "isProtected": true,
|
||||
// "name": "DCS",
|
||||
// "protectable": true,
|
||||
// "toggles": ["dcs"],
|
||||
// "tooltip": "Toggle DCS-controlled units' visibility"
|
||||
//}, {
|
||||
// "category": "Aircraft",
|
||||
// "image": "visibility/aircraft.svg",
|
||||
// "name": "Aircraft",
|
||||
// "toggles": ["aircraft"],
|
||||
// "tooltip": "Toggle aircraft's visibility"
|
||||
//}, {
|
||||
// "category": "Helicopter",
|
||||
// "image": "visibility/helicopter.svg",
|
||||
// "name": "Helicopter",
|
||||
// "toggles": ["helicopter"],
|
||||
// "tooltip": "Toggle helicopters' visibility"
|
||||
//}, {
|
||||
// "category": "AirDefence",
|
||||
// "image": "visibility/groundunit-sam.svg",
|
||||
// "name": "Air defence",
|
||||
// "toggles": ["groundunit-sam"],
|
||||
// "tooltip": "Toggle air defence units' visibility"
|
||||
//}, {
|
||||
// "image": "visibility/groundunit.svg",
|
||||
// "name": "Ground units",
|
||||
// "toggles": ["groundunit"],
|
||||
// "tooltip": "Toggle ground units' visibility"
|
||||
//}, {
|
||||
// "category": "GroundUnit",
|
||||
// "image": "visibility/navyunit.svg",
|
||||
// "name": "Naval",
|
||||
// "toggles": ["navyunit"],
|
||||
// "tooltip": "Toggle naval units' visibility"
|
||||
//}, {
|
||||
// "image": "visibility/airbase.svg",
|
||||
// "name": "Airbase",
|
||||
// "toggles": ["airbase"],
|
||||
// "tooltip": "Toggle airbase' visibility"
|
||||
//}];
|
||||
|
||||
export const IADSTypes = ["AAA", "SAM Site", "Radar (EWR)"];
|
||||
export const IADSDensities: { [key: string]: number } = { "AAA": 0.8, "SAM Site": 0.1, "Radar (EWR)": 0.05 };
|
||||
export const GROUND_UNIT_AIR_DEFENCE_REGEX: RegExp = /(\b(AAA|SAM|MANPADS?|[mM]anpads?)|[sS]tinger\b)/;
|
||||
|
||||
export const HIDE_GROUP_MEMBERS = "Hide group members when zoomed out";
|
||||
export const SHOW_UNIT_LABELS = "Show unit labels (L)";
|
||||
export const SHOW_UNITS_ENGAGEMENT_RINGS = "Show units threat range rings (Q)";
|
||||
@ -258,6 +201,44 @@ export const SHOW_UNIT_TARGETS = "Show selected unit targets";
|
||||
export const DCS_LINK_PORT = "DCS Camera link port";
|
||||
export const DCS_LINK_RATIO = "DCS Camera zoom";
|
||||
|
||||
export const MAP_OPTIONS_TOOLTIPS = {
|
||||
hideGroupMembers: "Hide group members when zoomed out",
|
||||
hideUnitsShortRangeRings: "Hide short range units threat range rings (R)",
|
||||
showUnitContacts: "Show selected units contact lines",
|
||||
showUnitPaths: "Show selected unit paths",
|
||||
showUnitTargets: "Show selected unit targets",
|
||||
showUnitLabels: "Show unit labels (L)",
|
||||
showUnitsEngagementRings: "Show units threat range rings (Q)",
|
||||
showUnitsAcquisitionRings: "Show units detection range rings (E)"
|
||||
}
|
||||
|
||||
export const MAP_OPTIONS_DEFAULTS = {
|
||||
hideGroupMembers: true,
|
||||
hideUnitsShortRangeRings: true,
|
||||
showUnitContacts: true,
|
||||
showUnitPaths: true,
|
||||
showUnitTargets: false,
|
||||
showUnitLabels: true,
|
||||
showUnitsEngagementRings: true,
|
||||
showUnitsAcquisitionRings: true
|
||||
}
|
||||
|
||||
export const MAP_HIDDEN_TYPES_DEFAULTS = {
|
||||
'human': false,
|
||||
'olympus': false,
|
||||
'dcs': false,
|
||||
'aircraft': false,
|
||||
'helicopter': false,
|
||||
'groundunit-sam': false,
|
||||
'groundunit': false,
|
||||
'navyunit': false,
|
||||
'airbase': false,
|
||||
'dead': false,
|
||||
'blue': false,
|
||||
'red': false,
|
||||
'neutral': false
|
||||
}
|
||||
|
||||
export enum DataIndexes {
|
||||
startOfData = 0,
|
||||
category,
|
||||
|
||||
1
frontend/react/src/dom.d.ts
vendored
1
frontend/react/src/dom.d.ts
vendored
@ -20,6 +20,7 @@ interface CustomEventMap {
|
||||
"groupDeletion": CustomEvent<Unit[]>,
|
||||
"mapStateChanged": CustomEvent<string>,
|
||||
"mapContextMenu": CustomEvent<any>,
|
||||
"mapOptionChanged": CustomEvent<any>,
|
||||
"mapOptionsChanged": CustomEvent<any>,
|
||||
"commandModeOptionsChanged": CustomEvent<any>,
|
||||
"contactsUpdated": CustomEvent<Unit>,
|
||||
|
||||
@ -4,6 +4,14 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.z-ui {
|
||||
.z-ui-0 {
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.z-ui-1 {
|
||||
z-index: 2001;
|
||||
}
|
||||
|
||||
.z-ui-2 {
|
||||
z-index: 2002;
|
||||
}
|
||||
@ -2,12 +2,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { setupApp } from './olympusapp.js'
|
||||
import { UI } from './ui.js';
|
||||
import { UI } from './ui/ui.js';
|
||||
|
||||
import './index.css'
|
||||
|
||||
import 'flowbite';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<UI />
|
||||
|
||||
@ -12,7 +12,7 @@ import { DestinationPreviewMarker } from "./markers/destinationpreviewmarker";
|
||||
import { TemporaryUnitMarker } from "./markers/temporaryunitmarker";
|
||||
import { ClickableMiniMap } from "./clickableminimap";
|
||||
import { SVGInjector } from '@tanem/svg-injector'
|
||||
import { defaultMapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, /*MAP_MARKER_CONTROLS,*/ DCS_LINK_PORT, DCSMapsZoomLevelsByTheatre, DCS_LINK_RATIO } from "../constants/constants";
|
||||
import { defaultMapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, /*MAP_MARKER_CONTROLS,*/ DCS_LINK_PORT, DCSMapsZoomLevelsByTheatre, DCS_LINK_RATIO, MAP_OPTIONS_DEFAULTS, MAP_HIDDEN_TYPES_DEFAULTS } from "../constants/constants";
|
||||
import { CoalitionArea } from "./coalitionarea/coalitionarea";
|
||||
//import { CoalitionAreaContextMenu } from "../contextmenus/coalitionareacontextmenu";
|
||||
import { DrawingCursor } from "./coalitionarea/drawingcursor";
|
||||
@ -29,7 +29,7 @@ import './markers/stylesheets/units.css'
|
||||
|
||||
// Temporary
|
||||
import './theme.css'
|
||||
|
||||
import { MapHiddenTypes, MapOptions } from "../types/types";
|
||||
|
||||
var hasTouchScreen = false;
|
||||
//if ("maxTouchPoints" in navigator)
|
||||
@ -45,18 +45,12 @@ else
|
||||
// TODO would be nice to convert to ts - yes
|
||||
//require("../../node_modules/leaflet.nauticscale/dist/leaflet.nauticscale.js")
|
||||
//require("../../node_modules/leaflet-path-drag/dist/index.js")
|
||||
|
||||
export type MapMarkerVisibilityControl = {
|
||||
"category"?: string;
|
||||
"image": string;
|
||||
"isProtected"?: boolean,
|
||||
"name": string,
|
||||
"protectable"?: boolean,
|
||||
"toggles": string[],
|
||||
"tooltip": string
|
||||
}
|
||||
|
||||
export class Map extends L.Map {
|
||||
/* Options */
|
||||
#options: MapOptions = MAP_OPTIONS_DEFAULTS;
|
||||
#hiddenTypes: MapHiddenTypes = MAP_HIDDEN_TYPES_DEFAULTS;
|
||||
|
||||
#ID: string;
|
||||
#state: string;
|
||||
#layer: L.TileLayer | L.LayerGroup | null = null;
|
||||
@ -97,19 +91,7 @@ export class Map extends L.Map {
|
||||
#longPressHandled: boolean = false;
|
||||
#longPressTimer: number = 0;
|
||||
|
||||
//#mapContextMenu: MapContextMenu = new MapContextMenu("map-contextmenu");
|
||||
//#unitContextMenu: UnitContextMenu = new UnitContextMenu("unit-contextmenu");
|
||||
//#airbaseContextMenu: AirbaseContextMenu = new AirbaseContextMenu("airbase-contextmenu");
|
||||
//#airbaseSpawnMenu: AirbaseSpawnContextMenu = new AirbaseSpawnContextMenu("airbase-spawn-contextmenu");
|
||||
//#coalitionAreaContextMenu: CoalitionAreaContextMenu = new CoalitionAreaContextMenu("coalition-area-contextmenu");
|
||||
|
||||
//#mapSourceDropdown: Dropdown;
|
||||
#mapLayers: any = defaultMapLayers;
|
||||
//#mapMarkerVisibilityControls: MapMarkerVisibilityControl[] = MAP_MARKER_CONTROLS;
|
||||
//#mapVisibilityOptionsDropdown: Dropdown;
|
||||
//#optionButtons: { [key: string]: HTMLButtonElement[] } = {}
|
||||
#visibilityOptions: { [key: string]: boolean | string | number } = {}
|
||||
#hiddenTypes: string[] = [];
|
||||
#layerName: string = "";
|
||||
#cameraOptionsXmlHttp: XMLHttpRequest | null = null;
|
||||
#bradcastPositionXmlHttp: XMLHttpRequest | null = null;
|
||||
@ -146,6 +128,7 @@ export class Map extends L.Map {
|
||||
this.#miniMapLayerGroup = new L.LayerGroup([minimapLayer]);
|
||||
this.#miniMapPolyline = new L.Polyline([], { color: '#202831' });
|
||||
this.#miniMapPolyline.addTo(this.#miniMapLayerGroup);
|
||||
|
||||
|
||||
/* Scale */
|
||||
//@ts-ignore TODO more hacking because the module is provided as a pure javascript module only
|
||||
@ -180,27 +163,14 @@ export class Map extends L.Map {
|
||||
this.on('move', (e: any) => { if (this.#slaveDCSCamera) this.#broadcastPosition() });
|
||||
|
||||
/* Event listeners */
|
||||
document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => {
|
||||
const el = ev.detail._element;
|
||||
el?.classList.toggle("off");
|
||||
this.setHiddenType(ev.detail.coalition, !el?.classList.contains("off"));
|
||||
document.addEventListener("hiddenTypesChanged", (ev: CustomEventInit) => {
|
||||
Object.values(getApp().getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility());
|
||||
});
|
||||
|
||||
document.addEventListener("toggleMarkerVisibility", (ev: CustomEventInit) => {
|
||||
const el = ev.detail._element;
|
||||
el?.classList.toggle("off");
|
||||
ev.detail.types.forEach((type: string) => this.setHiddenType(type, !el?.classList.contains("off")));
|
||||
Object.values(getApp().getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility());
|
||||
|
||||
if (ev.detail.types.includes("airbase")) {
|
||||
Object.values(getApp().getMissionManager().getAirbases()).forEach((airbase: Airbase) => {
|
||||
if (el?.classList.contains("off"))
|
||||
airbase.removeFrom(this);
|
||||
else
|
||||
airbase.addTo(this);
|
||||
})
|
||||
}
|
||||
Object.values(getApp().getMissionManager().getAirbases()).forEach((airbase: Airbase) => {
|
||||
if (this.getHiddenTypes().airbase)
|
||||
airbase.removeFrom(this);
|
||||
else
|
||||
airbase.addTo(this);
|
||||
})
|
||||
});
|
||||
|
||||
document.addEventListener("toggleCoalitionAreaDraw", (ev: CustomEventInit) => {
|
||||
@ -220,9 +190,9 @@ export class Map extends L.Map {
|
||||
//});
|
||||
|
||||
document.addEventListener("mapOptionsChanged", () => {
|
||||
this.getContainer().toggleAttribute("data-hide-labels", !this.getVisibilityOptions()[SHOW_UNIT_LABELS]);
|
||||
this.#cameraControlPort = this.getVisibilityOptions()[DCS_LINK_PORT] as number;
|
||||
this.#cameraZoomRatio = 50 / (20 + (this.getVisibilityOptions()[DCS_LINK_RATIO] as number));
|
||||
this.getContainer().toggleAttribute("data-hide-labels", !this.getOptions().showUnitLabels);
|
||||
//this.#cameraControlPort = this.getOptions()[DCS_LINK_PORT] as number;
|
||||
//this.#cameraZoomRatio = 50 / (20 + (this.getOptions()[DCS_LINK_RATIO] as number));
|
||||
|
||||
if (this.#slaveDCSCamera) {
|
||||
this.#broadcastPosition();
|
||||
@ -267,39 +237,6 @@ export class Map extends L.Map {
|
||||
this.#cameraControlTimer = window.setInterval(() => {
|
||||
this.#checkCameraPort();
|
||||
}, 1000)
|
||||
|
||||
/* Option buttons */
|
||||
this.#createUnitMarkerControlButtons();
|
||||
|
||||
/* Create the checkboxes to select the advanced visibility options */
|
||||
this.addVisibilityOption(DCS_LINK_PORT, 3003, { min: 1024, max: 65535 });
|
||||
this.addVisibilityOption(DCS_LINK_RATIO, 50, { min: 0, max: 100, slider: true });
|
||||
|
||||
//this.#mapVisibilityOptionsDropdown.addHorizontalDivider();
|
||||
|
||||
this.addVisibilityOption(SHOW_UNIT_CONTACTS, false);
|
||||
this.addVisibilityOption(HIDE_GROUP_MEMBERS, true);
|
||||
this.addVisibilityOption(SHOW_UNIT_PATHS, true);
|
||||
this.addVisibilityOption(SHOW_UNIT_TARGETS, true);
|
||||
this.addVisibilityOption(SHOW_UNIT_LABELS, true);
|
||||
this.addVisibilityOption(SHOW_UNITS_ENGAGEMENT_RINGS, true);
|
||||
this.addVisibilityOption(SHOW_UNITS_ACQUISITION_RINGS, true);
|
||||
this.addVisibilityOption(HIDE_UNITS_SHORT_RANGE_RINGS, true);
|
||||
/* this.addVisibilityOption(FILL_SELECTED_RING, false); Removed since currently broken: TODO fix!*/
|
||||
}
|
||||
|
||||
addVisibilityOption(option: string, defaultValue: boolean | number | string, options?: { [key: string]: any }) {
|
||||
//this.#visibilityOptions[option] = defaultValue;
|
||||
//if (typeof defaultValue === 'boolean') {
|
||||
// this.#mapVisibilityOptionsDropdown.addOptionElement(createCheckboxOption(option, option, defaultValue as boolean, (ev: any) => { this.#setVisibilityOption(option, ev); }, options));
|
||||
//} else if (typeof defaultValue === 'number') {
|
||||
// if (options !== undefined && options?.slider === true)
|
||||
// this.#mapVisibilityOptionsDropdown.addOptionElement(createSliderInputOption(option, option, defaultValue, (ev: any) => { this.#setVisibilityOption(option, ev); }, options));
|
||||
// else
|
||||
// this.#mapVisibilityOptionsDropdown.addOptionElement(createTextInputOption(option, option, defaultValue.toString(), 'number', (ev: any) => { this.#setVisibilityOption(option, ev); }, options));
|
||||
//} else {
|
||||
// this.#mapVisibilityOptionsDropdown.addOptionElement(createTextInputOption(option, option, defaultValue, 'text', (ev: any) => { this.#setVisibilityOption(option, ev); }, options));
|
||||
//}
|
||||
}
|
||||
|
||||
setLayer(layerName: string) {
|
||||
@ -376,13 +313,8 @@ export class Map extends L.Map {
|
||||
}
|
||||
|
||||
setHiddenType(key: string, value: boolean) {
|
||||
if (value) {
|
||||
if (this.#hiddenTypes.includes(key))
|
||||
delete this.#hiddenTypes[this.#hiddenTypes.indexOf(key)];
|
||||
}
|
||||
else {
|
||||
this.#hiddenTypes.push(key);
|
||||
}
|
||||
this.#hiddenTypes[key] = value;
|
||||
document.dispatchEvent(new CustomEvent("hiddenTypesChanged"));
|
||||
}
|
||||
|
||||
getHiddenTypes() {
|
||||
@ -583,8 +515,13 @@ export class Map extends L.Map {
|
||||
this.#coalitionAreas.unshift(coalitionArea);
|
||||
}
|
||||
|
||||
getVisibilityOptions() {
|
||||
return this.#visibilityOptions;
|
||||
setOption(key, value) {
|
||||
this.#options[key] = value;
|
||||
document.dispatchEvent(new CustomEvent("mapOptionsChanged"));
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return this.#options;
|
||||
}
|
||||
|
||||
isZooming() {
|
||||
@ -877,49 +814,6 @@ export class Map extends L.Map {
|
||||
return minimapBoundaries;
|
||||
}
|
||||
|
||||
#createUnitMarkerControlButtons() {
|
||||
const unitVisibilityControls = <HTMLElement>document.getElementById("unit-visibility-control");
|
||||
const makeTitle = (isProtected: boolean) => {
|
||||
return (isProtected) ? "Unit type is protected and will ignore orders" : "Unit is NOT protected and will respond to orders";
|
||||
}
|
||||
//this.getMapMarkerVisibilityControls().forEach((control: MapMarkerVisibilityControl) => {
|
||||
// const toggles = `["${control.toggles.join('","')}"]`;
|
||||
// const div = document.createElement("div");
|
||||
// div.className = control.protectable === true ? "protectable" : "";
|
||||
//
|
||||
// // TODO: for consistency let's avoid using innerHTML. Let's create elements.
|
||||
// div.innerHTML = `
|
||||
// <button data-on-click="toggleMarkerVisibility" title="${control.tooltip}" data-on-click-params='{"types":${toggles}}'>
|
||||
// <img src="/resources/theme/images/buttons/${control.image}" />
|
||||
// </button>
|
||||
// `;
|
||||
// unitVisibilityControls.appendChild(div);
|
||||
//
|
||||
// if (control.protectable) {
|
||||
// div.innerHTML += `
|
||||
// <button class="lock" ${control.isProtected ? "data-protected" : ""} title="${makeTitle(control.isProtected || false)}">
|
||||
// <img src="/resources/theme/images/buttons/other/lock-solid.svg" class="locked" />
|
||||
// <img src="/resources/theme/images/buttons/other/lock-open-solid.svg" class="unlocked" />
|
||||
// </button>`;
|
||||
//
|
||||
// const btn = <HTMLButtonElement>div.querySelector("button.lock");
|
||||
// btn.addEventListener("click", (ev: MouseEventInit) => {
|
||||
// control.isProtected = !control.isProtected;
|
||||
// btn.toggleAttribute("data-protected", control.isProtected);
|
||||
// btn.title = makeTitle(control.isProtected);
|
||||
// document.dispatchEvent(new CustomEvent("toggleMarkerProtection", {
|
||||
// detail: {
|
||||
// "_element": btn,
|
||||
// "control": control
|
||||
// }
|
||||
// }));
|
||||
// });
|
||||
// }
|
||||
//});
|
||||
|
||||
//unitVisibilityControls.querySelectorAll(`img[src$=".svg"]`).forEach(img => SVGInjector(img));
|
||||
}
|
||||
|
||||
#deselectSelectedCoalitionArea() {
|
||||
this.getSelectedCoalitionArea()?.setSelected(false);
|
||||
}
|
||||
@ -1029,16 +923,6 @@ export class Map extends L.Map {
|
||||
this.#drawingCursor.removeFrom(this);
|
||||
}
|
||||
|
||||
#setVisibilityOption(option: string, ev: any) {
|
||||
if (typeof this.#visibilityOptions[option] === 'boolean')
|
||||
this.#visibilityOptions[option] = ev.currentTarget.checked;
|
||||
else if (typeof this.#visibilityOptions[option] === 'number')
|
||||
this.#visibilityOptions[option] = Number(ev.currentTarget.value);
|
||||
else
|
||||
this.#visibilityOptions[option] = ev.currentTarget.value;
|
||||
document.dispatchEvent(new CustomEvent("mapOptionsChanged"));
|
||||
}
|
||||
|
||||
#setSlaveDCSCameraAvailable(newSlaveDCSCameraAvailable: boolean) {
|
||||
this.#slaveDCSCameraAvailable = newSlaveDCSCameraAvailable;
|
||||
let linkButton = document.getElementById("camera-link-control");
|
||||
|
||||
@ -9,6 +9,7 @@ import { GROUND_UNIT_AIR_DEFENCE_REGEX, ROEs, emissionsCountermeasures, reaction
|
||||
import { navyUnitDatabase } from "../unit/databases/navyunitdatabase";
|
||||
import { DateAndTime, UnitBlueprint } from "../interfaces";
|
||||
import { Converter } from "usng";
|
||||
import { MGRS } from "../types/types";
|
||||
|
||||
|
||||
export function bearing(lat1: number, lon1: number, lat2: number, lon2: number) {
|
||||
@ -167,18 +168,6 @@ export function editDistance(s1: string, s2: string) {
|
||||
return costs[s2.length];
|
||||
}
|
||||
|
||||
export type MGRS = {
|
||||
bandLetter: string,
|
||||
columnLetter: string,
|
||||
easting: string,
|
||||
groups: string[],
|
||||
northing: string,
|
||||
precision: number,
|
||||
rowLetter: string,
|
||||
string: string,
|
||||
zoneNumber: string
|
||||
}
|
||||
|
||||
export function latLngToMGRS(lat: number, lng: number, precision: number = 4): MGRS | false {
|
||||
|
||||
if (precision < 0 || precision > 6) {
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import { createContext } from "react";
|
||||
import { OlympusState } from "./ui";
|
||||
import { MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS } from "./constants/constants";
|
||||
|
||||
export const StateContext = createContext({
|
||||
mainMenuVisible: false,
|
||||
spawnMenuVisible: false,
|
||||
unitControlMenuVisible: false,
|
||||
measureMenuVisible: false,
|
||||
drawingMenuVisible: false
|
||||
} as OlympusState)
|
||||
drawingMenuVisible: false,
|
||||
mapHiddenTypes: MAP_HIDDEN_TYPES_DEFAULTS,
|
||||
mapOptions: MAP_OPTIONS_DEFAULTS
|
||||
})
|
||||
|
||||
export const StateProvider = StateContext.Provider;
|
||||
export const StateConsumer = StateContext.Consumer;
|
||||
|
||||
50
frontend/react/src/types/types.ts
Normal file
50
frontend/react/src/types/types.ts
Normal file
@ -0,0 +1,50 @@
|
||||
|
||||
/* Types definition */
|
||||
export type MapMarkerVisibilityControl = {
|
||||
"category"?: string;
|
||||
"image": string;
|
||||
"isProtected"?: boolean,
|
||||
"name": string,
|
||||
"protectable"?: boolean,
|
||||
"toggles": string[],
|
||||
"tooltip": string
|
||||
}
|
||||
|
||||
export type MapOptions = {
|
||||
hideGroupMembers: boolean,
|
||||
hideUnitsShortRangeRings: boolean,
|
||||
showUnitContacts: boolean,
|
||||
showUnitPaths: boolean,
|
||||
showUnitTargets: boolean,
|
||||
showUnitLabels: boolean,
|
||||
showUnitsEngagementRings: boolean,
|
||||
showUnitsAcquisitionRings: boolean
|
||||
}
|
||||
|
||||
export type MapHiddenTypes = {
|
||||
'human': boolean,
|
||||
'olympus': boolean,
|
||||
'dcs': boolean,
|
||||
'aircraft': boolean,
|
||||
'helicopter': boolean,
|
||||
'groundunit-sam': boolean,
|
||||
'groundunit': boolean,
|
||||
'navyunit': boolean,
|
||||
'airbase': boolean,
|
||||
'dead': boolean,
|
||||
'blue': boolean,
|
||||
'red': boolean,
|
||||
'neutral': boolean
|
||||
}
|
||||
|
||||
export type MGRS = {
|
||||
bandLetter: string,
|
||||
columnLetter: string,
|
||||
easting: string,
|
||||
groups: string[],
|
||||
northing: string,
|
||||
precision: number,
|
||||
rowLetter: string,
|
||||
string: string,
|
||||
zoneNumber: string
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import './ui.css'
|
||||
import { initFlowbite } from "flowbite";
|
||||
|
||||
import { Header } from './ui/panels/header'
|
||||
import { EventsProvider } from './eventscontext'
|
||||
import { StateProvider } from './statecontext'
|
||||
import { SpawnMenu } from './ui/panels/spawnmenu'
|
||||
import { UnitControlMenu } from './ui/panels/unitcontrolmenu'
|
||||
import { MainMenu } from './ui/panels/mainmenu'
|
||||
|
||||
export type OlympusState = {
|
||||
mainMenuVisible: boolean,
|
||||
spawnMenuVisible: boolean,
|
||||
unitControlMenuVisible: boolean,
|
||||
measureMenuVisible: boolean,
|
||||
drawingMenuVisible: boolean
|
||||
}
|
||||
|
||||
export function UI(props) {
|
||||
var [flowbiteInited, setFlowbiteInited] = useState(false);
|
||||
var [mainMenuVisible, setMainMenuVisible] = useState(false);
|
||||
var [spawnMenuVisible, setSpawnMenuVisible] = useState(false);
|
||||
var [unitControlMenuVisible, setUnitControlMenuVisible] = useState(false);
|
||||
var [measureMenuVisible, setMeasureMenuVisible] = useState(false);
|
||||
var [drawingMenuVisible, setDrawingMenuVisible] = useState(false);
|
||||
|
||||
function hideAllMenus() {
|
||||
setMainMenuVisible(false);
|
||||
setSpawnMenuVisible(false);
|
||||
setUnitControlMenuVisible(false);
|
||||
setMeasureMenuVisible(false);
|
||||
setDrawingMenuVisible(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute top-0 left-0 h-screen w-screen font-sans overflow-hidden">
|
||||
<StateProvider value={{
|
||||
mainMenuVisible: mainMenuVisible,
|
||||
spawnMenuVisible: spawnMenuVisible,
|
||||
unitControlMenuVisible: unitControlMenuVisible,
|
||||
measureMenuVisible: measureMenuVisible,
|
||||
drawingMenuVisible: drawingMenuVisible
|
||||
}}>
|
||||
<EventsProvider value={
|
||||
{
|
||||
setMainMenuVisible: setMainMenuVisible,
|
||||
setSpawnMenuVisible: setSpawnMenuVisible,
|
||||
setUnitControlMenuVisible: setUnitControlMenuVisible,
|
||||
setDrawingMenuVisible: setDrawingMenuVisible,
|
||||
setMeasureMenuVisible: setMeasureMenuVisible,
|
||||
toggleMainMenuVisible: () => {hideAllMenus(); setMainMenuVisible(!mainMenuVisible)},
|
||||
toggleSpawnMenuVisible: () => {hideAllMenus(); setSpawnMenuVisible(!spawnMenuVisible)},
|
||||
toggleUnitControlMenuVisible: () => {hideAllMenus(); setUnitControlMenuVisible(!unitControlMenuVisible)},
|
||||
toggleMeasureMenuVisible: () => {hideAllMenus(); setMeasureMenuVisible(!measureMenuVisible)},
|
||||
toggleDrawingMenuVisible: () => {hideAllMenus(); setDrawingMenuVisible(!drawingMenuVisible)},
|
||||
}
|
||||
}>
|
||||
<div className='absolute top-0 left-0 h-full w-full flex flex-col'>
|
||||
<Header />
|
||||
<div id='map-container' className='relative h-screen w-screen top-0 left-0' />
|
||||
<MainMenu open={mainMenuVisible} closeCallback={() => setMainMenuVisible(false)}/>
|
||||
<SpawnMenu open={spawnMenuVisible} closeCallback={() => setSpawnMenuVisible(false)}/>
|
||||
<UnitControlMenu open={unitControlMenuVisible} closeCallback={() => setUnitControlMenuVisible(false)}/>
|
||||
</div>
|
||||
</EventsProvider>
|
||||
</StateProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,18 +1,12 @@
|
||||
import React, { useId, useEffect, useRef, useState } from "react"
|
||||
|
||||
import 'flowbite';
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowCircleDown, faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
|
||||
import { initFlowbite } from "flowbite";
|
||||
import { faArrowCircleDown } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
export function OlAccordion(props) {
|
||||
var [open, setOpen] = useState(false);
|
||||
var [scrolledUp, setScrolledUp] = useState(true);
|
||||
var [scrolledDown, setScrolledDown] = useState(false);
|
||||
|
||||
const bodyId = useId();
|
||||
const accordionId = useId();
|
||||
const headingId = useId();
|
||||
|
||||
var contentRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
@ -22,20 +16,18 @@ export function OlAccordion(props) {
|
||||
setScrolledUp(e.target.scrollTop === 0);
|
||||
}
|
||||
})
|
||||
|
||||
initFlowbite();
|
||||
})
|
||||
|
||||
return <div id={accordionId} data-accordion="collapse" data-active-classes="bg-white dark:bg-transparent text-gray-900 dark:text-white" data-inactive-classes="text-gray-500 dark:text-gray-300">
|
||||
<h3 id={headingId}>
|
||||
<button type="button" className="flex items-center justify-between w-full py-2 font-medium rtl:text-right text-gray-500 border-gray-200 dark:border-gray-700 dark:text-gray-300 gap-3" data-accordion-target={"#" + CSS.escape(bodyId)} aria-expanded="false" aria-controls={bodyId}>
|
||||
return <div className="bg-white dark:bg-transparent text-gray-900 dark:text-white">
|
||||
<h3>
|
||||
<button type="button" onClick={() => setOpen(!open)} className="flex items-center justify-between w-full py-2 font-medium rtl:text-right text-gray-700 border-gray-200 dark:border-gray-700 dark:text-gray-300 gap-3">
|
||||
<span>{props.title}</span>
|
||||
<svg data-accordion-icon className="w-3 h-3 rotate-180 shrink-0" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
||||
<svg data-open={open} className="w-3 h-3 data-[open='false']:-scale-y-100 shrink-0 transition-transform" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5 5 1 1 5" />
|
||||
</svg>
|
||||
</button>
|
||||
</h3>
|
||||
<div id={bodyId} className="hidden" aria-labelledby={headingId}>
|
||||
<div className={open? "": "hidden"}>
|
||||
{props.showArrows && <div className="rotate-180"> {!scrolledUp && <FontAwesomeIcon icon={faArrowCircleDown} className="text-white animate-bounce opacity-20 absolute w-full"/>}</div>}
|
||||
<div ref={contentRef} className="py-2 border-gray-200 dark:border-gray-700">
|
||||
{props.children}
|
||||
|
||||
7
frontend/react/src/ui/components/olcheckbox.tsx
Normal file
7
frontend/react/src/ui/components/olcheckbox.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React, { useState, useId } from "react";
|
||||
|
||||
export function OlCheckbox(props) {
|
||||
const id = useId();
|
||||
|
||||
return <input id={id} onChange={props.onChange} type="checkbox" checked={props.checked} value="" className="w-4 h-4 my-auto text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
|
||||
}
|
||||
9
frontend/react/src/ui/components/olcoalitiontoggle.tsx
Normal file
9
frontend/react/src/ui/components/olcoalitiontoggle.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
export function OlCoalitionToggle() {
|
||||
return <div className="inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" value="" className="sr-only peer" />
|
||||
<div className="relative w-14 h-7 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:start-[4px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||
<span className="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">Large toggle</span>
|
||||
</div>
|
||||
}
|
||||
@ -1,30 +1,89 @@
|
||||
|
||||
import React, { useId, useState } from "react";
|
||||
import React, { useId, useState, useEffect, useRef } from "react";
|
||||
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
library.add(faChevronDown, faChevronUp);
|
||||
|
||||
export function OlDropdown(props) {
|
||||
var [value, setValue] = useState(props.items[0] ?? "N/A" )
|
||||
export function OlTextDropdown(props) {
|
||||
var [value, setValue] = useState(props.items[0] ?? "N/A")
|
||||
const buttonId = useId();
|
||||
const dropdownId = useId()
|
||||
|
||||
|
||||
return <div>
|
||||
<button id={buttonId} data-dropdown-toggle={dropdownId} className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center border-[1px] dark:border-gray-600 dark:text-gray-400 dark:bg-gray-700 dark:hover:bg-gray-800 dark:focus:ring-blue-800" type="button"><FontAwesomeIcon icon={props.leftIcon} className="mr-3" />{value}<svg className="w-2.5 h-2.5 ms-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m1 1 4 4 4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div id={dropdownId} className="z-ui hidden bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700">
|
||||
<ul className="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby={buttonId}>
|
||||
<div id={dropdownId} className="z-ui-2 hidden bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700">
|
||||
<div className="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby={buttonId}>
|
||||
{props.items.map((item) => {
|
||||
return <li>
|
||||
<a href="#" onClick={() => setValue(item)} className="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">{item}</a>
|
||||
</li>
|
||||
return <OlDropdownItem onClick={() => setValue(item)}>
|
||||
{item}
|
||||
</OlDropdownItem>
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export function OlElementDropdown(props) {
|
||||
var [open, setOpen] = useState(false);
|
||||
var contentRef = useRef(null);
|
||||
var buttonRef = useRef(null);
|
||||
|
||||
function setPosition(content: HTMLDivElement, button: HTMLButtonElement) {
|
||||
content.style.left = `0px`;
|
||||
content.style.top = `0px`;
|
||||
|
||||
var [cxl, cyt, cxr, cyb, cw, ch] = [content.getBoundingClientRect().x, content.getBoundingClientRect().y, content.getBoundingClientRect().x + content.clientWidth, content.getBoundingClientRect().y + content.clientHeight, content.clientWidth, content.clientHeight];
|
||||
var [bxl, byt, bxr, byb, bbw, bh] = [button.getBoundingClientRect().x, button.getBoundingClientRect().y, button.getBoundingClientRect().x + button.clientWidth, button.getBoundingClientRect().y + button.clientHeight, button.clientWidth, button.clientHeight];
|
||||
|
||||
var cxc = (cxl + cxr) / 2;
|
||||
var bxc = (bxl + bxr) / 2;
|
||||
|
||||
var offsetX = bxc - cxc;
|
||||
var offsetY = byb - cyt;
|
||||
|
||||
cxl += offsetX;
|
||||
cxr += offsetX;
|
||||
|
||||
if (cxl < 0)
|
||||
offsetX -= cxl;
|
||||
if (cxr > window.innerWidth)
|
||||
offsetX -= (cxr - window.innerWidth)
|
||||
|
||||
content.style.left = `${offsetX}px`
|
||||
content.style.top = `${offsetY + 5}px`
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current && buttonRef.current) {
|
||||
const content = contentRef.current as HTMLDivElement;
|
||||
const button = buttonRef.current as HTMLButtonElement;
|
||||
|
||||
setPosition(content, button);
|
||||
}
|
||||
})
|
||||
|
||||
return <div className="relative">
|
||||
<button ref={buttonRef} onClick={() => {setOpen(!open)}} className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center border-[1px] dark:border-gray-600 dark:text-gray-400 dark:bg-gray-700 dark:hover:bg-gray-800 dark:focus:ring-blue-800" type="button"><FontAwesomeIcon icon={props.leftIcon} className="mr-3" />{props.label}<svg className="w-2.5 h-2.5 ms-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m1 1 4 4 4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div ref={contentRef} data-open={open} className="absolute z-ui-2 w-fit data-[open='false']:hidden bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700">
|
||||
<div className="py-2 text-sm text-gray-700 dark:text-gray-200 w-fit">
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export function OlDropdownItem(props) {
|
||||
return <div onClick={props.onClick ?? (() => { })} className="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white flex flex-row content-center gap-2">
|
||||
{props.children}
|
||||
</div>
|
||||
}
|
||||
11
frontend/react/src/ui/components/ollabeltoggle.tsx
Normal file
11
frontend/react/src/ui/components/ollabeltoggle.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
export function OlLabelToggle(props) {
|
||||
var [toggled, setToggled] = useState(false);
|
||||
|
||||
return <div onClick={() => {setToggled(!toggled)}} className="relative flex flex-row contents-center justify-between w-32 h-10 dark:bg-gray-700 rounded-md py-1 px-1 select-none cursor-pointer">
|
||||
<span data-toggled={toggled} className="absolute my-auto h-8 w-14 bg-blue-500 rounded-md data-[toggled='true']:translate-x-16 transition-transform"></span>
|
||||
<span data-active={!toggled} className="my-auto dark:data-[active='true']:text-white dark:data-[active='false']:text-gray-400 text-thin pl-3 z-ui-2 transition-colors">MSL</span>
|
||||
<span data-active={toggled} className="my-auto dark:data-[active='true']:text-white dark:data-[active='false']:text-gray-400 text-thin pr-3 z-ui-2 transition-colors">AGL</span>
|
||||
</div>
|
||||
}
|
||||
19
frontend/react/src/ui/components/olnumberinput.tsx
Normal file
19
frontend/react/src/ui/components/olnumberinput.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React, {useEffect, useId} from "react";
|
||||
|
||||
export function OlNumberInput(props) {
|
||||
return <div className="w-fit">
|
||||
<div className="relative flex items-center max-w-[8rem]">
|
||||
<button type="button" className="bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 dark:border-gray-600 hover:bg-gray-200 border border-gray-300 rounded-s-lg p-3 h-9 focus:ring-gray-100 dark:focus:ring-gray-700 focus:ring-2 focus:outline-none">
|
||||
<svg className="w-3 h-3 text-gray-900 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 2">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M1 1h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
<input type="text" className="bg-gray-50 border-x-0 border-gray-300 h-9 text-center text-gray-900 text-sm focus:ring-blue-500 focus:border-blue-500 block w-full py-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder={props.placeHolder} />
|
||||
<button type="button" className="bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 dark:border-gray-600 hover:bg-gray-200 border border-gray-300 rounded-e-lg p-3 h-9 focus:ring-gray-100 dark:focus:ring-gray-700 focus:ring-2 focus:outline-none">
|
||||
<svg className="w-3 h-3 text-gray-900 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 18">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 1v16M1 9h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
11
frontend/react/src/ui/components/olrangeslider.tsx
Normal file
11
frontend/react/src/ui/components/olrangeslider.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
export function OlRangeSlider(props) {
|
||||
return <input type="range"
|
||||
onChange={(ev) => { props.onChange(Number(ev.target?.value ?? props.value)) }}
|
||||
value={props.value}
|
||||
min={props.minValue ?? 0}
|
||||
max={props.maxValue ?? 100}
|
||||
step={props.step ?? 1}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" />
|
||||
}
|
||||
@ -2,10 +2,19 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import React from "react"
|
||||
|
||||
export function OlStateButton(props) {
|
||||
return <button {...props} type="button" className={`h-[40px] w-[40px] m-auto text-gray-900 bg-white border border-gray-300 focus:outline-none
|
||||
hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm
|
||||
dark:bg-${props.checked? 'white': 'transparent'} dark:text-${props.checked? 'gray-900': 'white'} dark:border-gray-600
|
||||
dark:hover:bg-${props.checked? 'white': 'gray-700'} dark:hover:border-gray-600 dark:focus:ring-gray-700`}>
|
||||
const className = (props.className ?? '') + ` h-[40px] w-[40px] m-auto border border-gray-900 font-medium rounded-md text-sm ` +
|
||||
`dark:bg-transparent dark:data-[checked='true']:bg-white dark:text-white dark:data-[checked='true']:text-gray-900 dark:border-gray-600 `;
|
||||
|
||||
return <button onClick={props.onClick} data-checked={props.checked} type="button" className={className}>
|
||||
<FontAwesomeIcon icon={props.icon} />
|
||||
</button>
|
||||
}
|
||||
|
||||
export function OlRoundStateButton(props) {
|
||||
const className = (props.className ?? '') + ` h-[40px] w-[40px] m-auto border border-gray-900 font-medium rounded-full text-sm ` +
|
||||
`dark:bg-transparent dark:data-[checked='true']:bg-white dark:text-white dark:data-[checked='true']:text-gray-900 dark:border-gray-600 `;
|
||||
|
||||
return <button onClick={props.onClick} data-checked={props.checked} type="button" className={className}>
|
||||
<FontAwesomeIcon icon={props.icon} />
|
||||
</button>
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
import React, { ChangeEvent } from "react";
|
||||
|
||||
type OlToggleState = {
|
||||
checked: boolean
|
||||
}
|
||||
|
||||
type OlToggleProps = {
|
||||
checkedLabel: string,
|
||||
uncheckedLabel: string
|
||||
}
|
||||
|
||||
export class OlToggle extends React.Component<OlToggleProps, OlToggleState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
checked: false
|
||||
}
|
||||
|
||||
this.onToggle = this.onToggle.bind(this);
|
||||
}
|
||||
|
||||
onToggle(e: ChangeEvent<HTMLInputElement>) {
|
||||
this.setState({
|
||||
checked: e.target.checked
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return <label className="flex items-center cursor-pointer -mr-8">
|
||||
<input type="checkbox" value="" className="sr-only peer" onChange={this.onToggle}/>
|
||||
<div className="relative w-16 h-6 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-500 peer-checked:after:translate-x-[200%] rtl:peer-checked:after:-translate-x-[200%] peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-interaction-blue"></div>
|
||||
<span className={(this.state.checked? "-translate-x-[250%]": "-translate-x-[180%]") + " ms-3 text-sm font-medium text-gray-900 dark:text-white transition-all select-none relative w-7"}>
|
||||
{this.state.checked? this.props.checkedLabel: this.props.uncheckedLabel}
|
||||
</span>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,5 @@
|
||||
import React, { useId, useState, useRef } from "react";
|
||||
import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import 'flowbite';
|
||||
|
||||
export function OlUnitEntryList(props) {
|
||||
return <div {...props} className="relative text-sm cursor-pointer select-none flex justify-between items-center dark:text-gray-300 dark:hover:bg-white dark:hover:bg-opacity-10 px-2 py-1 rounded-sm mr-2">
|
||||
|
||||
@ -2,8 +2,7 @@ import React from "react";
|
||||
import { UnitBlueprint } from "../../interfaces";
|
||||
|
||||
export function OlUnitSummary(props: {blueprint: UnitBlueprint}) {
|
||||
console.log(props.blueprint)
|
||||
return <div {...props} className="relative border-l-4 border-blue-600 flex flex-col gap-2 p-2 items-start shadow-lg bg-white rounded-md hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-700">
|
||||
return <div {...props} className="relative border-l-4 border-blue-600 flex flex-col gap-2 p-2 pt-4 items-start shadow-lg bg-white rounded-md hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-700">
|
||||
<div className="flex flex-row gap-2 content-center">
|
||||
<img className="object-cover h-8 ml-2 rounded-tl-md rotate-180 invert" src={"images/units/"+props.blueprint.filename} alt="" />
|
||||
<div className="my-auto w-full font-bold tracking-tight text-gray-900 dark:text-white">{props.blueprint.label}</div>
|
||||
@ -13,7 +12,7 @@ export function OlUnitSummary(props: {blueprint: UnitBlueprint}) {
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 p-2">
|
||||
{props.blueprint.abilities?.split(" ").map((tag) => {
|
||||
return <div className="bg-black bg-opacity-20 dark:text-gray-400 rounded-full px-2 py-0.5 text-xs">
|
||||
return <div key={tag} className="bg-black bg-opacity-20 dark:text-gray-400 rounded-full px-2 py-0.5 text-xs">
|
||||
{tag}
|
||||
</div>
|
||||
})}
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
import React from "react";
|
||||
import { UnitBlueprint } from "../../../interfaces";
|
||||
import { IconProp, library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
library.add(faChevronLeft);
|
||||
|
||||
export type BlueprintsAccordionProps = {
|
||||
title: string,
|
||||
icon: string,
|
||||
blueprints: { [key: string]: UnitBlueprint },
|
||||
searchString: string,
|
||||
callback: CallableFunction
|
||||
}
|
||||
|
||||
export type BlueprintsAccordionState = {
|
||||
open: boolean
|
||||
}
|
||||
|
||||
export class BlueprintsAccordion extends React.Component<BlueprintsAccordionProps, BlueprintsAccordionState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
open: this.props.searchString !== ''
|
||||
}
|
||||
|
||||
this.toggleOpen = this.toggleOpen.bind(this);
|
||||
}
|
||||
|
||||
toggleOpen() {
|
||||
this.setState({ open: !this.state.open });
|
||||
}
|
||||
|
||||
checkSearch(key) {
|
||||
const blueprint = this.props.blueprints[key];
|
||||
if (blueprint.label.includes(this.props.searchString))
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.searchString !== '' && !this.state.open)
|
||||
this.setState({ open: true });
|
||||
|
||||
return <div>
|
||||
<div className="cursor-pointer select-none flex justify-between p-2 items-center" onClick={this.toggleOpen}>
|
||||
<div className="">{this.props.title}</div>
|
||||
<FontAwesomeIcon icon="chevron-left" className={"transition-transform " + (this.state.open ? "-rotate-90" : "")}></FontAwesomeIcon>
|
||||
</div>
|
||||
<div className="flex flex-col px-3">
|
||||
{
|
||||
this.state.open &&
|
||||
Object.keys(this.props.blueprints).filter((key) => {
|
||||
return this.checkSearch(key);
|
||||
}).map((key) => {
|
||||
return <div key={key} className="cursor-pointer select-none flex justify-between items-center hover:bg-white hover:bg-opacity-10 px-2 py-1 rounded-sm" onClick={() => this.props.callback(this.props.blueprints[key])}>
|
||||
<FontAwesomeIcon icon={this.props.icon as IconProp} className="text-sm"></FontAwesomeIcon>
|
||||
<div className="font-normal text-left flex-1 px-2">{this.props.blueprints[key].label}</div>
|
||||
<div className="bg-black bg-opacity-20 text-gray-400 rounded-full px-2 py-0.5 text-xs">{this.props.blueprints[key].era === "WW2" ? "WW2" : this.props.blueprints[key].era.split(" ").map((word) => {
|
||||
return word.charAt(0).toLocaleUpperCase();
|
||||
})}</div>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@ -1,20 +1,11 @@
|
||||
import { Drawer, DrawerInterface } from "flowbite";
|
||||
import React, { useEffect, useId, useRef } from "react";
|
||||
import React from "react";
|
||||
|
||||
export function Menu(props) {
|
||||
const ref = useRef(null);
|
||||
const labelId = useId();
|
||||
|
||||
useEffect(() => {
|
||||
const drawer: DrawerInterface = new Drawer(ref.current, { backdrop: false });
|
||||
props.open ? drawer.show() : drawer.hide();
|
||||
})
|
||||
|
||||
return <div ref={ref} className="w-[430px] absolute top-[80px] left-0 z-ui h-screen p-4 overflow-y-auto transition-transform -translate-x-full bg-white dark:bg-gray-800" tabIndex={-1} aria-labelledby={labelId}>
|
||||
<h5 id={labelId} className="w-full inline-flex items-center pb-3 mb-4 border-b-2 dark:border-gray-700 text-base font-semibold text-gray-500 dark:text-gray-400">
|
||||
return <div data-open={props.open} className="w-[430px] absolute top-[80px] left-20 z-ui-0 h-screen p-4 overflow-y-auto transition-transform data-[open='false']:-translate-x-full bg-gray-200 dark:bg-gray-800" tabIndex={-1}>
|
||||
<h5 className="w-full inline-flex items-center pb-3 mb-4 border-b-2 dark:border-gray-700 text-base font-semibold text-gray-900 dark:text-gray-400">
|
||||
{props.title}
|
||||
</h5>
|
||||
<button type="button" onClick={props.closeCallback} className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 absolute top-2.5 end-2.5 flex items-center justify-center dark:hover:bg-gray-600 dark:hover:text-white" >
|
||||
<button type="button" onClick={props.closeCallback} className="text-gray-900 dark:text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 absolute top-2.5 end-2.5 flex items-center justify-center dark:hover:bg-gray-600 dark:hover:text-white" >
|
||||
<svg className="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||
</svg>
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import React from 'react'
|
||||
import { OlStateButton } from '../components/olstatebutton';
|
||||
import { faPlus, faGamepad, faRuler, faPencil, faMap } from '@fortawesome/free-solid-svg-icons';
|
||||
import React, { useEffect } from 'react'
|
||||
import { OlRoundStateButton } from '../components/olstatebutton';
|
||||
import { faPlus, faGamepad, faRuler, faPencil, faMap, faLock, faPerson, faBrain, faRobot, faJetFighter, faHelicopter, faShield, faTruck, faShip, faPlaneDeparture, faSkull, faShieldAlt, faGears } from '@fortawesome/free-solid-svg-icons';
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { EventsConsumer } from '../../eventscontext';
|
||||
import { StateConsumer } from '../../statecontext';
|
||||
import { OlDropdown } from '../components/oldropdown';
|
||||
import { OlDropdownItem, OlElementDropdown, OlTextDropdown } from '../components/oldropdown';
|
||||
import { OlCheckbox } from '../components/olcheckbox';
|
||||
import { MAP_OPTIONS_DEFAULTS, MAP_OPTIONS_TOOLTIPS } from '../../constants/constants';
|
||||
import { getApp } from '../../olympusapp';
|
||||
|
||||
library.add(faPlus, faGamepad, faRuler, faPencil, faMap);
|
||||
|
||||
@ -13,16 +16,72 @@ export function Header(props) {
|
||||
{(appState) =>
|
||||
<EventsConsumer>
|
||||
{(events) =>
|
||||
<nav className="bg-white border-gray-200 dark:bg-[#171C26] dark:border-gray-700">
|
||||
<div className="max-w-screen flex flex-wrap items-center justify-between p-4">
|
||||
<nav className="bg-gray-300 border-gray-200 dark:bg-[#171C26] dark:border-gray-700">
|
||||
<div className="max-w-screen flex flex-wrap items-center justify-between p-4 gap-3">
|
||||
<div className="flex flex-row items-center justify-center gap-1">
|
||||
<img src="images/icon.png" className='h-12 border-2 p-0 bg-white rounded-md mr-2 cursor-pointer' onClick={events.toggleMainMenuVisible}></img>
|
||||
<OlStateButton onClick={events.toggleSpawnMenuVisible} checked={appState.spawnMenuVisible} icon="fa-solid fa-plus"></OlStateButton>
|
||||
<OlStateButton onClick={events.toggleUnitControlMenuVisible} checked={appState.unitControlMenuVisible} icon="fa-solid fa-gamepad"></OlStateButton>
|
||||
<OlStateButton onClick={events.toggleMeasureMenuVisible} checked={appState.measureMenuVisible} icon="fa-solid fa-ruler"></OlStateButton>
|
||||
<OlStateButton onClick={events.toggleDrawingMenuVisible} checked={appState.drawingMenuVisible} icon="fa-solid fa-pencil"></OlStateButton>
|
||||
<img src="images/icon.png" className='h-12 p-0 rounded-md mr-2 cursor-pointer' onClick={events.toggleMainMenuVisible}></img>
|
||||
</div>
|
||||
<OlDropdown items={["DCS Sat", "DCS Alt"]} leftIcon='fa-solid fa-map' />
|
||||
<div className="ml-auto">
|
||||
<OlRoundStateButton icon={faLock} />
|
||||
</div>
|
||||
<div className="flex flex-row h-fit items-center justify-start gap-1">
|
||||
{
|
||||
Object.entries({
|
||||
'human': faPerson,'olympus': faBrain, 'dcs': faRobot
|
||||
}).map((entry) => {
|
||||
return <OlRoundStateButton
|
||||
onClick={() => {
|
||||
getApp().getMap().setHiddenType(entry[0], !appState.mapHiddenTypes[entry[0]]);
|
||||
}}
|
||||
checked={!appState.mapHiddenTypes[entry[0]]}
|
||||
icon={entry[1]} />
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div className='h-10 w-0 border-l-2 border-gray-500'></div>
|
||||
<div className="flex flex-row h-fit items-center justify-start gap-1">
|
||||
{
|
||||
Object.entries({
|
||||
'aircraft': faJetFighter,'helicopter': faHelicopter, 'groundunit-sam': faShieldAlt,
|
||||
'groundunit': faTruck, 'navyunit': faShip, 'airbase': faPlaneDeparture, 'dead': faSkull
|
||||
}).map((entry) => {
|
||||
return <OlRoundStateButton
|
||||
onClick={() => {
|
||||
getApp().getMap().setHiddenType(entry[0], !appState.mapHiddenTypes[entry[0]]);
|
||||
}}
|
||||
checked={!appState.mapHiddenTypes[entry[0]]}
|
||||
icon={entry[1]} />
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div className='h-10 w-0 border-l-2 border-gray-500'></div>
|
||||
<div className="flex flex-row h-fit items-center justify-start gap-1">
|
||||
<OlRoundStateButton
|
||||
onClick={() => getApp().getMap().setHiddenType( 'blue', !appState.mapHiddenTypes['blue'] )}
|
||||
checked={!appState.mapHiddenTypes['blue']}
|
||||
icon={faShield} className={"!text-blue-500"} />
|
||||
<OlRoundStateButton
|
||||
onClick={() => getApp().getMap().setHiddenType('red', !appState.mapHiddenTypes['red'] )}
|
||||
checked={!appState.mapHiddenTypes['red']}
|
||||
icon={faShield} className={"!text-red-500"} />
|
||||
<OlRoundStateButton
|
||||
onClick={() => getApp().getMap().setHiddenType('neutral', !appState.mapHiddenTypes['neutral'] )}
|
||||
checked={!appState.mapHiddenTypes['neutral']}
|
||||
icon={faShield} className={"!text-gray-500"} />
|
||||
</div>
|
||||
<OlTextDropdown items={["DCS Sat", "DCS Alt"]} leftIcon='fa-solid fa-map' />
|
||||
<OlElementDropdown leftIcon={faGears} label="Options" className="w-80">
|
||||
{Object.keys(MAP_OPTIONS_TOOLTIPS).map((key) => {
|
||||
return <OlDropdownItem>
|
||||
<OlCheckbox
|
||||
checked = {appState.mapOptions[key]}
|
||||
onChange = {(ev) => {
|
||||
getApp().getMap()?.setOption(key, ev.target.checked);
|
||||
}}/>
|
||||
<span className="text-nowrap">{ MAP_OPTIONS_TOOLTIPS[key] }</span>
|
||||
</OlDropdownItem>
|
||||
})}
|
||||
</OlElementDropdown>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
|
||||
@ -7,13 +7,13 @@ import { faGithub } from "@fortawesome/free-brands-svg-icons";
|
||||
|
||||
export function MainMenu(props) {
|
||||
return <Menu {...props} title="DCS Olympus">
|
||||
<div className="flex flex-col gap-2 text-md font-normal font text-white">
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none dark:hover:bg-white/10 rounded-sm content-center text-[#8BFF63]"><FontAwesomeIcon icon={faCheckCircle} className="my-auto" />Version {VERSION}</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faGithub} className="my-auto text-gray-400" />Overview</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faFileAlt} className="my-auto text-gray-400" />User guide</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faDatabase} className="my-auto text-gray-400" />Database Manager</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faFileExport} className="my-auto text-gray-400" />Export to file</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faFileImport} className="my-auto text-gray-400" />Import from file</div>
|
||||
<div className="flex flex-col gap-2 text-md font-normal font text-gray-900 dark:text-white">
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none hover:bg-gray-900/10 dark:hover:bg-white/10 rounded-sm content-center text-green-700 dark:text-[#8BFF63]"><FontAwesomeIcon icon={faCheckCircle} className="my-auto" />Version {VERSION}</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none hover:bg-gray-900/10 dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faGithub} className="my-auto text-gray-800 dark:text-gray-400" />Overview</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none hover:bg-gray-900/10 dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faFileAlt} className="my-auto text-gray-800 dark:text-gray-400" />User guide</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none hover:bg-gray-900/10 dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faDatabase} className="my-auto text-gray-800 dark:text-gray-400" />Database Manager</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none hover:bg-gray-900/10 dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faFileExport} className="my-auto text-gray-800 dark:text-gray-400" />Export to file</div>
|
||||
<div className="flex gap-3 p-1 cursor-pointer select-none hover:bg-gray-900/10 dark:hover:bg-white/10 rounded-sm content-center"><FontAwesomeIcon icon={faFileImport} className="my-auto text-gray-800 dark:text-gray-400" />Import from file</div>
|
||||
</div>
|
||||
</Menu>
|
||||
}
|
||||
29
frontend/react/src/ui/panels/sidebar.tsx
Normal file
29
frontend/react/src/ui/panels/sidebar.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import { OlStateButton } from '../components/olstatebutton';
|
||||
import { faPlus, faGamepad, faRuler, faPencil, faMap } from '@fortawesome/free-solid-svg-icons';
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { EventsConsumer } from '../../eventscontext';
|
||||
import { StateConsumer } from '../../statecontext';
|
||||
|
||||
library.add(faPlus, faGamepad, faRuler, faPencil, faMap);
|
||||
|
||||
export function SideBar(props) {
|
||||
return <StateConsumer>
|
||||
{(appState) =>
|
||||
<EventsConsumer>
|
||||
{(events) =>
|
||||
<nav className="z-ui-1 h-full bg-gray-300 border-gray-200 dark:bg-[#171C26] dark:border-gray-700">
|
||||
<div className="flex flex-wrap items-center justify-center p-4 w-20">
|
||||
<div className="flex flex-col items-center justify-center gap-1">
|
||||
<OlStateButton onClick={events.toggleSpawnMenuVisible} checked={appState.spawnMenuVisible} icon="fa-solid fa-plus"></OlStateButton>
|
||||
<OlStateButton onClick={events.toggleUnitControlMenuVisible} checked={appState.unitControlMenuVisible} icon="fa-solid fa-gamepad"></OlStateButton>
|
||||
<OlStateButton onClick={events.toggleMeasureMenuVisible} checked={appState.measureMenuVisible} icon="fa-solid fa-ruler"></OlStateButton>
|
||||
<OlStateButton onClick={events.toggleDrawingMenuVisible} checked={appState.drawingMenuVisible} icon="fa-solid fa-pencil"></OlStateButton>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
</EventsConsumer>
|
||||
}
|
||||
</StateConsumer>
|
||||
}
|
||||
@ -6,7 +6,6 @@ import { OlSearchBar } from "../components/olsearchbar";
|
||||
import { OlAccordion } from "../components/olaccordion";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { OlUnitEntryList } from "../components/olunitlistentry";
|
||||
import { UnitBlueprint } from "../../interfaces";
|
||||
import { UnitSpawnMenu } from "./unitspawnmenu";
|
||||
|
||||
library.add(faPlus);
|
||||
|
||||
@ -1,8 +1,28 @@
|
||||
import React from "react";
|
||||
import React, {useState} from "react";
|
||||
import { OlUnitSummary } from "../components/olunitsummary";
|
||||
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
|
||||
import { OlNumberInput } from "../components/olnumberinput";
|
||||
import { OlLabelToggle } from "../components/ollabeltoggle";
|
||||
import { OlRangeSlider } from "../components/olrangeslider";
|
||||
|
||||
export function UnitSpawnMenu(props) {
|
||||
return <div>
|
||||
var [spawnAltitude, setSpawnAltitude] = useState(1000);
|
||||
|
||||
return <div className="flex flex-col gap-3">
|
||||
<OlUnitSummary blueprint={props.blueprint}/>
|
||||
<div className="flex flex-row content-center justify-between w-full">
|
||||
<OlCoalitionToggle />
|
||||
<OlNumberInput placeHolder={1} minValue={1} maxValue={4}/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex flex-row content-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<span className="dark: text-white">Altitude</span>
|
||||
<span className="dark:text-blue-500">{`${spawnAltitude} FT`}</span>
|
||||
</div>
|
||||
<OlLabelToggle />
|
||||
</div>
|
||||
<OlRangeSlider onChange={setSpawnAltitude} minValue={0} maxValue={30000} step={500}/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
91
frontend/react/src/ui/ui.tsx
Normal file
91
frontend/react/src/ui/ui.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import './ui.css'
|
||||
|
||||
import { EventsProvider } from '../eventscontext'
|
||||
import { StateProvider } from '../statecontext'
|
||||
|
||||
import { Header } from './panels/header'
|
||||
import { SpawnMenu } from './panels/spawnmenu'
|
||||
import { UnitControlMenu } from './panels/unitcontrolmenu'
|
||||
import { MainMenu } from './panels/mainmenu'
|
||||
import { SideBar } from './panels/sidebar';
|
||||
import { MapHiddenTypes, MapOptions } from '../types/types'
|
||||
import { MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS } from '../constants/constants'
|
||||
import { getApp } from '../olympusapp'
|
||||
import { Dropdown } from 'flowbite'
|
||||
|
||||
export type OlympusState = {
|
||||
mainMenuVisible: boolean,
|
||||
spawnMenuVisible: boolean,
|
||||
unitControlMenuVisible: boolean,
|
||||
measureMenuVisible: boolean,
|
||||
drawingMenuVisible: boolean,
|
||||
mapHiddenTypes: MapHiddenTypes;
|
||||
mapOptions: MapOptions;
|
||||
}
|
||||
|
||||
export function UI(props) {
|
||||
var [mainMenuVisible, setMainMenuVisible] = useState(false);
|
||||
var [spawnMenuVisible, setSpawnMenuVisible] = useState(false);
|
||||
var [unitControlMenuVisible, setUnitControlMenuVisible] = useState(false);
|
||||
var [measureMenuVisible, setMeasureMenuVisible] = useState(false);
|
||||
var [drawingMenuVisible, setDrawingMenuVisible] = useState(false);
|
||||
var [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS);
|
||||
var [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
|
||||
|
||||
document.addEventListener("hiddenTypesChanged", (ev) => {
|
||||
setMapHiddenTypes({...getApp().getMap().getHiddenTypes()});
|
||||
})
|
||||
|
||||
document.addEventListener("mapOptionsChanged", (ev) => {
|
||||
setMapOptions({...getApp().getMap().getOptions()});
|
||||
})
|
||||
|
||||
function hideAllMenus() {
|
||||
setMainMenuVisible(false);
|
||||
setSpawnMenuVisible(false);
|
||||
setUnitControlMenuVisible(false);
|
||||
setMeasureMenuVisible(false);
|
||||
setDrawingMenuVisible(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute top-0 left-0 h-screen w-screen font-sans overflow-hidden">
|
||||
<StateProvider value={{
|
||||
mainMenuVisible: mainMenuVisible,
|
||||
spawnMenuVisible: spawnMenuVisible,
|
||||
unitControlMenuVisible: unitControlMenuVisible,
|
||||
measureMenuVisible: measureMenuVisible,
|
||||
drawingMenuVisible: drawingMenuVisible,
|
||||
mapOptions: mapOptions,
|
||||
mapHiddenTypes: mapHiddenTypes
|
||||
}}>
|
||||
<EventsProvider value={
|
||||
{
|
||||
setMainMenuVisible: setMainMenuVisible,
|
||||
setSpawnMenuVisible: setSpawnMenuVisible,
|
||||
setUnitControlMenuVisible: setUnitControlMenuVisible,
|
||||
setDrawingMenuVisible: setDrawingMenuVisible,
|
||||
setMeasureMenuVisible: setMeasureMenuVisible,
|
||||
toggleMainMenuVisible: () => { hideAllMenus(); setMainMenuVisible(!mainMenuVisible) },
|
||||
toggleSpawnMenuVisible: () => { hideAllMenus(); setSpawnMenuVisible(!spawnMenuVisible) },
|
||||
toggleUnitControlMenuVisible: () => { hideAllMenus(); setUnitControlMenuVisible(!unitControlMenuVisible) },
|
||||
toggleMeasureMenuVisible: () => { hideAllMenus(); setMeasureMenuVisible(!measureMenuVisible) },
|
||||
toggleDrawingMenuVisible: () => { hideAllMenus(); setDrawingMenuVisible(!drawingMenuVisible) },
|
||||
}
|
||||
}>
|
||||
<div className='absolute top-0 left-0 h-full w-full flex flex-col'>
|
||||
<Header />
|
||||
<div className='flex h-full'>
|
||||
<SideBar />
|
||||
<div id='map-container' className='relative h-full w-screen top-0 left-0' />
|
||||
<MainMenu open={mainMenuVisible} closeCallback={() => setMainMenuVisible(false)} />
|
||||
<SpawnMenu open={spawnMenuVisible} closeCallback={() => setSpawnMenuVisible(false)} />
|
||||
<UnitControlMenu open={unitControlMenuVisible} closeCallback={() => setUnitControlMenuVisible(false)} />
|
||||
</div>
|
||||
</div>
|
||||
</EventsProvider>
|
||||
</StateProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -193,18 +193,13 @@ export abstract class Unit extends CustomMarker {
|
||||
getApp().getMap().on("zoomend", (e: any) => { this.#onZoom(e); })
|
||||
|
||||
/* Deselect units if they are hidden */
|
||||
document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => {
|
||||
this.#updateMarker();
|
||||
this.setSelected(this.getSelected() && !this.getHidden());
|
||||
});
|
||||
|
||||
document.addEventListener("toggleMarkerVisibility", (ev: CustomEventInit) => {
|
||||
document.addEventListener("hiddenTypesChanged", (ev: CustomEventInit) => {
|
||||
this.#updateMarker();
|
||||
this.setSelected(this.getSelected() && !this.getHidden());
|
||||
});
|
||||
|
||||
/* Update the marker when the options change */
|
||||
document.addEventListener("mapOptionsChanged", (ev: CustomEventInit) => {
|
||||
document.addEventListener("mapOptionChanged", (ev: CustomEventInit) => {
|
||||
this.#updateMarker();
|
||||
|
||||
/* Circles don't like to be updated when the map is zooming */
|
||||
@ -699,19 +694,19 @@ export abstract class Unit extends CustomMarker {
|
||||
const hiddenTypes = getApp().getMap().getHiddenTypes();
|
||||
var hidden = (
|
||||
/* Hide the unit if it is a human and humans are hidden */
|
||||
(this.getHuman() && hiddenTypes.includes("human")) ||
|
||||
(this.getHuman() && hiddenTypes["human"]) ||
|
||||
/* Hide the unit if it is DCS-controlled and DCS controlled units are hidden */
|
||||
(this.isControlledByDCS() && hiddenTypes.includes("dcs")) ||
|
||||
(this.isControlledByDCS() && hiddenTypes["dcs"]) ||
|
||||
/* Hide the unit if it is Olympus-controlled and Olympus-controlled units are hidden */
|
||||
(this.isControlledByOlympus() && hiddenTypes.includes("olympus")) ||
|
||||
(this.isControlledByOlympus() && hiddenTypes["olympus"]) ||
|
||||
/* Hide the unit if this specific category is hidden */
|
||||
(hiddenTypes.includes(this.getMarkerCategory())) ||
|
||||
(hiddenTypes[this.getMarkerCategory()]) ||
|
||||
/* Hide the unit if this coalition is hidden */
|
||||
(hiddenTypes.includes(this.#coalition)) ||
|
||||
(hiddenTypes[this.#coalition]) ||
|
||||
/* Hide the unit if it does not belong to the commanded coalition and it is not detected by a method that can pinpoint its location (RWR does not count) */
|
||||
(!this.belongsToCommandedCoalition() && (this.#detectionMethods.length == 0 || (this.#detectionMethods.length == 1 && this.#detectionMethods[0] === RWR))) ||
|
||||
/* Hide the unit if grouping is activated, the unit is not the group leader, it is not selected, and the zoom is higher than the grouping threshold */
|
||||
(getApp().getMap().getVisibilityOptions()[HIDE_GROUP_MEMBERS] && !this.#isLeader && !this.getSelected() && this.getCategory() == "GroundUnit" && getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION &&
|
||||
(getApp().getMap().getOptions()[HIDE_GROUP_MEMBERS] && !this.#isLeader && !this.getSelected() && this.getCategory() == "GroundUnit" && getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION &&
|
||||
(this.belongsToCommandedCoalition() || (!this.belongsToCommandedCoalition() && this.#detectionMethods.length == 0))));
|
||||
|
||||
/* Force dead units to be hidden */
|
||||
@ -1247,7 +1242,7 @@ export abstract class Unit extends CustomMarker {
|
||||
}
|
||||
|
||||
#drawPath() {
|
||||
if (this.#activePath != undefined && getApp().getMap().getVisibilityOptions()[SHOW_UNIT_PATHS]) {
|
||||
if (this.#activePath != undefined && getApp().getMap().getOptions()[SHOW_UNIT_PATHS]) {
|
||||
var points: LatLng[] = [];
|
||||
points.push(new LatLng(this.#position.lat, this.#position.lng));
|
||||
|
||||
@ -1291,7 +1286,7 @@ export abstract class Unit extends CustomMarker {
|
||||
|
||||
#drawContacts() {
|
||||
this.#clearContacts();
|
||||
if (getApp().getMap().getVisibilityOptions()[SHOW_UNIT_CONTACTS]) {
|
||||
if (getApp().getMap().getOptions()[SHOW_UNIT_CONTACTS]) {
|
||||
for (let index in this.#contacts) {
|
||||
var contactData = this.#contacts[index];
|
||||
var contact: Unit | Weapon | null;
|
||||
@ -1366,12 +1361,12 @@ export abstract class Unit extends CustomMarker {
|
||||
if (engagementRange !== this.#engagementCircle.getRadius())
|
||||
this.#engagementCircle.setRadius(engagementRange);
|
||||
|
||||
this.#engagementCircle.options.fillOpacity = this.getSelected() && getApp().getMap().getVisibilityOptions()[FILL_SELECTED_RING] ? 0.3 : 0;
|
||||
this.#engagementCircle.options.fillOpacity = this.getSelected() && getApp().getMap().getOptions()[FILL_SELECTED_RING] ? 0.3 : 0;
|
||||
|
||||
/* Acquisition circles */
|
||||
var shortAcquisitionRangeCheck = (acquisitionRange > nmToM(3) || !getApp().getMap().getVisibilityOptions()[HIDE_UNITS_SHORT_RANGE_RINGS]);
|
||||
var shortAcquisitionRangeCheck = (acquisitionRange > nmToM(3) || !getApp().getMap().getOptions()[HIDE_UNITS_SHORT_RANGE_RINGS]);
|
||||
|
||||
if (getApp().getMap().getVisibilityOptions()[SHOW_UNITS_ACQUISITION_RINGS] && shortAcquisitionRangeCheck && (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, IRST, RWR].includes(value)))) {
|
||||
if (getApp().getMap().getOptions()[SHOW_UNITS_ACQUISITION_RINGS] && shortAcquisitionRangeCheck && (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, IRST, RWR].includes(value)))) {
|
||||
if (!getApp().getMap().hasLayer(this.#acquisitionCircle)) {
|
||||
this.#acquisitionCircle.addTo(getApp().getMap());
|
||||
switch (this.getCoalition()) {
|
||||
@ -1395,8 +1390,8 @@ export abstract class Unit extends CustomMarker {
|
||||
}
|
||||
|
||||
/* Engagement circles */
|
||||
var shortEngagementRangeCheck = (engagementRange > nmToM(3) || !getApp().getMap().getVisibilityOptions()[HIDE_UNITS_SHORT_RANGE_RINGS]);
|
||||
if (getApp().getMap().getVisibilityOptions()[SHOW_UNITS_ENGAGEMENT_RINGS] && shortEngagementRangeCheck && (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, IRST, RWR].includes(value)))) {
|
||||
var shortEngagementRangeCheck = (engagementRange > nmToM(3) || !getApp().getMap().getOptions()[HIDE_UNITS_SHORT_RANGE_RINGS]);
|
||||
if (getApp().getMap().getOptions()[SHOW_UNITS_ENGAGEMENT_RINGS] && shortEngagementRangeCheck && (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, IRST, RWR].includes(value)))) {
|
||||
if (!getApp().getMap().hasLayer(this.#engagementCircle)) {
|
||||
this.#engagementCircle.addTo(getApp().getMap());
|
||||
switch (this.getCoalition()) {
|
||||
@ -1430,10 +1425,10 @@ export abstract class Unit extends CustomMarker {
|
||||
}
|
||||
|
||||
#drawTarget() {
|
||||
if (this.#targetPosition.lat != 0 && this.#targetPosition.lng != 0 && getApp().getMap().getVisibilityOptions()[SHOW_UNIT_PATHS]) {
|
||||
if (this.#targetPosition.lat != 0 && this.#targetPosition.lng != 0 && getApp().getMap().getOptions()[SHOW_UNIT_PATHS]) {
|
||||
this.#drawTargetPosition(this.#targetPosition);
|
||||
}
|
||||
else if (this.#targetID != 0 && getApp().getMap().getVisibilityOptions()[SHOW_UNIT_TARGETS]) {
|
||||
else if (this.#targetID != 0 && getApp().getMap().getOptions()[SHOW_UNIT_TARGETS]) {
|
||||
const target = getApp().getUnitsManager().getUnitByID(this.#targetID);
|
||||
if (target && (getApp().getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER || (this.belongsToCommandedCoalition() && getApp().getUnitsManager().getUnitDetectedMethods(target).some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))))) {
|
||||
this.#drawTargetPosition(target.getPosition());
|
||||
@ -1637,7 +1632,7 @@ export class GroundUnit extends Unit {
|
||||
/* When a unit is a leader of a group, the map is zoomed out and grouping when zoomed out is enabled, check if the unit should be shown as a specific group. This is used to show a SAM battery instead of the group leader */
|
||||
getDatabaseEntry() {
|
||||
let unitWhenGrouped: string | undefined | null = null;
|
||||
if (!this.getSelected() && this.getIsLeader() && getApp().getMap().getVisibilityOptions()[HIDE_GROUP_MEMBERS] && getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION) {
|
||||
if (!this.getSelected() && this.getIsLeader() && getApp().getMap().getOptions()[HIDE_GROUP_MEMBERS] && getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION) {
|
||||
unitWhenGrouped = this.getDatabase()?.getByName(this.getName())?.unitWhenGrouped ?? null;
|
||||
let member = this.getGroupMembers().reduce((prev: Unit | null, unit: Unit, index: number) => {
|
||||
if (unit.getDatabaseEntry()?.unitWhenGrouped != undefined)
|
||||
@ -1654,7 +1649,7 @@ export class GroundUnit extends Unit {
|
||||
|
||||
/* When we zoom past the grouping limit, grouping is enabled and the unit is a leader, we redraw the unit to apply any possible grouped marker */
|
||||
checkZoomRedraw(): boolean {
|
||||
return (this.getIsLeader() && getApp().getMap().getVisibilityOptions()[HIDE_GROUP_MEMBERS] as boolean &&
|
||||
return (this.getIsLeader() && getApp().getMap().getOptions()[HIDE_GROUP_MEMBERS] as boolean &&
|
||||
(getApp().getMap().getZoom() >= GROUPING_ZOOM_TRANSITION && getApp().getMap().getPreviousZoom() < GROUPING_ZOOM_TRANSITION ||
|
||||
getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION && getApp().getMap().getPreviousZoom() >= GROUPING_ZOOM_TRANSITION))
|
||||
}
|
||||
|
||||
@ -160,8 +160,8 @@ export class Weapon extends CustomMarker {
|
||||
/********************** Visibility *************************/
|
||||
updateVisibility() {
|
||||
const hiddenUnits = getApp().getMap().getHiddenTypes();
|
||||
var hidden = (hiddenUnits.includes(this.getMarkerCategory())) ||
|
||||
(hiddenUnits.includes(this.#coalition)) ||
|
||||
var hidden = (hiddenUnits[this.getMarkerCategory()]) ||
|
||||
(hiddenUnits[this.#coalition]) ||
|
||||
(!this.belongsToCommandedCoalition() && this.#detectionMethods.length == 0);
|
||||
|
||||
this.setHidden(hidden || !this.#alive);
|
||||
|
||||
@ -2,9 +2,7 @@
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
"node_modules/flowbite-react/lib/esm/**/*.js",
|
||||
"node_modules/flowbite/**/*.js"
|
||||
"./src/**/*.{js,ts,jsx,tsx}"
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
@ -21,7 +19,6 @@ export default {
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require("flowbite/plugin"),
|
||||
],
|
||||
darkMode: 'class'
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user