diff --git a/frontend/react/src/audio/audiolibrary.ts b/frontend/react/src/audio/audiolibrary.ts index 67df36dd..64b9ad1e 100644 --- a/frontend/react/src/audio/audiolibrary.ts +++ b/frontend/react/src/audio/audiolibrary.ts @@ -11,13 +11,13 @@ if (!window.AudioBuffer.prototype.copyFromChannel) { export class Effect { name: string; - context: AudioContext; + context: AudioContext | OfflineAudioContext; input: GainNode; effect: GainNode | BiquadFilterNode | null; bypassed: boolean; output: GainNode; - constructor(context: AudioContext) { + constructor(context: AudioContext | OfflineAudioContext) { this.name = "effect"; this.context = context; this.input = this.context.createGain(); @@ -45,14 +45,14 @@ export class Effect { } export class Sample { - context: AudioContext; + context: AudioContext | OfflineAudioContext; buffer: AudioBufferSourceNode; sampleBuffer: AudioBuffer | null; rawBuffer: ArrayBuffer | null; loaded: boolean; output: GainNode; - constructor(context: AudioContext) { + constructor(context: AudioContext | OfflineAudioContext) { this.context = context; this.buffer = this.context.createBufferSource(); this.buffer.start(); @@ -94,7 +94,7 @@ export class Sample { } export class AmpEnvelope { - context: AudioContext; + context: AudioContext | OfflineAudioContext; output: GainNode; partials: any[]; velocity: number; @@ -104,7 +104,7 @@ export class AmpEnvelope { #sustain: number; #release: number; - constructor(context: AudioContext, gain: number = 1) { + constructor(context: AudioContext | OfflineAudioContext, gain: number = 1) { this.context = context; this.output = this.context.createGain(); this.output.gain.value = gain; @@ -179,7 +179,7 @@ export class AmpEnvelope { } export class Voice { - context: AudioContext; + context: AudioContext | OfflineAudioContext; type: string; value: number; gain: number; @@ -187,7 +187,7 @@ export class Voice { partials: any[]; ampEnvelope: AmpEnvelope; - constructor(context: AudioContext, gain: number = 0.1, type: string = "sawtooth") { + constructor(context: AudioContext | OfflineAudioContext, gain: number = 0.1, type: string = "sawtooth") { this.context = context; this.type = type; this.value = -1; @@ -266,7 +266,7 @@ export class Voice { export class Noise extends Voice { #length: number; - constructor(context: AudioContext, gain: number) { + constructor(context: AudioContext | OfflineAudioContext, gain: number) { super(context, gain); this.#length = 2; } @@ -307,7 +307,7 @@ export class Noise extends Voice { } export class Filter extends Effect { - constructor(context: AudioContext, type: string = "lowpass", cutoff: number = 1000, resonance: number = 0.9) { + constructor(context: AudioContext | OfflineAudioContext, type: string = "lowpass", cutoff: number = 1000, resonance: number = 0.9) { super(context); this.name = "filter"; if (this.effect instanceof BiquadFilterNode) { diff --git a/frontend/react/src/audio/audiomanager.ts b/frontend/react/src/audio/audiomanager.ts index 505adbd1..0450876f 100644 --- a/frontend/react/src/audio/audiomanager.ts +++ b/frontend/react/src/audio/audiomanager.ts @@ -38,12 +38,24 @@ export class AudioManager { constructor() { ConfigLoadedEvent.on((config: OlympusConfig) => { - config.WSPort ? this.setPort(config.WSPort) : this.setEndpoint(config.WSEndpoint); + config.audio.WSPort ? this.setPort(config.audio.WSPort) : this.setEndpoint(config.audio.WSEndpoint); }); setInterval(() => { this.#syncRadioSettings(); }, 1000); + + let PTTKeys = ["KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "KeyM", "KeyK", "KeyL"]; + PTTKeys.forEach((key, idx) => { + getApp() + .getShortcutManager() + .addShortcut(`PTT${idx}Active`, { + label: `PTT ${idx} active`, + keyDownCallback: () => this.getSinks()[idx]?.setPtt(true), + keyUpCallback: () => this.getSinks()[idx]?.setPtt(false), + code: key + }); + }); } start() { diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 5a740f82..f09399ca 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -276,7 +276,7 @@ export enum OlympusState { OPTIONS = "Options", AUDIO = "Audio", AIRBASE = "Airbase", - GAME_MASTER = "Game master", + GAME_MASTER = "Game master" } export const NO_SUBSTATE = "No substate"; @@ -310,7 +310,12 @@ export enum SpawnSubState { SPAWN_EFFECT = "Effect", } -export type OlympusSubState = DrawSubState | JTACSubState | SpawnSubState | string; +export enum OptionsSubstate { + NO_SUBSTATE = "No substate", + KEYBIND = "Keybind" +} + +export type OlympusSubState = DrawSubState | JTACSubState | SpawnSubState | OptionsSubstate | string; export const IADSTypes = ["AAA", "SAM Site", "Radar (EWR)"]; export const IADSDensities: { [key: string]: number } = { diff --git a/frontend/react/src/events.ts b/frontend/react/src/events.ts index 722b980c..379f34fa 100644 --- a/frontend/react/src/events.ts +++ b/frontend/react/src/events.ts @@ -5,6 +5,7 @@ import { CommandModeOptions, OlympusConfig, ServerStatus } from "./interfaces"; import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle"; import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon"; import { Airbase } from "./mission/airbase"; +import { Shortcut } from "./shortcut/shortcut"; import { MapHiddenTypes, MapOptions } from "./types/types"; import { ContextAction } from "./unit/contextaction"; import { ContextActionSet } from "./unit/contextactionset"; @@ -108,6 +109,45 @@ export class HideMenuEvent { } } +export class ShortcutsChangedEvent { + static on(callback: (shortcuts: {[key: string]: Shortcut}) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.shortcuts); + }); + } + + static dispatch(shortcuts: {[key: string]: Shortcut}) { + document.dispatchEvent(new CustomEvent(this.name, {detail: {shortcuts}})); + console.log(`Event ${this.name} dispatched`); + } +} + +export class ShortcutChangedEvent { + static on(callback: (shortcut: Shortcut) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.shortcut); + }); + } + + static dispatch(shortcut: Shortcut) { + document.dispatchEvent(new CustomEvent(this.name, {detail: {shortcut}})); + console.log(`Event ${this.name} dispatched`); + } +} + +export class BindShortcutRequestEvent { + static on(callback: (shortcut: Shortcut) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.shortcut); + }); + } + + static dispatch(shortcut: Shortcut) { + document.dispatchEvent(new CustomEvent(this.name, {detail: {shortcut}})); + console.log(`Event ${this.name} dispatched`); + } +} + /************** Map events ***************/ export class HiddenTypesChangedEvent { static on(callback: (hiddenTypes: MapHiddenTypes) => void) { diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index 14fd813d..ef3ee240 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -1,26 +1,37 @@ import { LatLng } from "leaflet"; +import { MapOptions } from "./types/types"; export interface OlympusConfig { - port: number; - elevationProvider: { - provider: string; - username: string | null; - password: string | null; - }; - mapLayers: { - [key: string]: { - urlTemplate: string; - minZoom: number; - maxZoom: number; - attribution?: string; + frontend: { + port: number; + elevationProvider: { + provider: string; + username: string | null; + password: string | null; + }; + mapLayers: { + [key: string]: { + urlTemplate: string; + minZoom: number; + maxZoom: number; + attribution?: string; + }; + }; + mapMirrors: { + [key: string]: string; }; }; - mapMirrors: { - [key: string]: string; + audio: { + SRSPort: number; + WSPort?: number; + WSEndpoint?: string; }; - SRSPort: number; - WSPort?: number; - WSEndpoint?: string; + profiles?: ProfileOptions; +} + +export interface ProfileOptions { + mapOptions: MapOptions, + shortcuts: {[key: string]: ShortcutOptions} } export interface ContextMenuOption { @@ -284,25 +295,13 @@ export interface AirbaseChartRunwayData { } export interface ShortcutOptions { - altKey?: boolean; - callback: CallableFunction; - ctrlKey?: boolean; - name?: string; - shiftKey?: boolean; -} - -export interface ShortcutKeyboardOptions extends ShortcutOptions { + label: string; + keyUpCallback: (e: KeyboardEvent) => void; + keyDownCallback?: (e: KeyboardEvent) => void; code: string; - event?: "keydown" | "keyup"; -} - -export interface ShortcutMouseOptions extends ShortcutOptions { - button: number; - event: "mousedown" | "mouseup"; -} - -export interface Manager { - add: CallableFunction; + altKey?: boolean; + ctrlKey?: boolean; + shiftKey?: boolean; } export interface ServerStatus { diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index a063d118..8958449b 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -22,6 +22,7 @@ import { UnitControlSubState, ContextActionTarget, ContextActionType, + ContextActions, } from "../constants/constants"; import { CoalitionPolygon } from "./coalitionarea/coalitionpolygon"; import { MapHiddenTypes, MapOptions } from "../types/types"; @@ -88,9 +89,7 @@ export class Map extends L.Map { #panRight: boolean = false; #panUp: boolean = false; #panDown: boolean = false; - - /* Keyboard state */ - #isShiftKeyDown: boolean = false; + #panFast: boolean = false; /* Center on unit target */ #centeredUnit: Unit | null = null; @@ -193,9 +192,6 @@ export class Map extends L.Map { this.on("mousemove", (e: any) => this.#onMouseMove(e)); - this.on("keydown", (e: any) => this.#onKeyDown(e)); - this.on("keyup", (e: any) => this.#onKeyUp(e)); - this.on("move", (e: any) => this.#onMapMove(e)); /* Custom touch events for touchscreen support */ @@ -239,8 +235,8 @@ export class Map extends L.Map { let layerSet = false; /* First load the map mirrors */ - if (config.mapMirrors) { - let mapMirrors = config.mapMirrors; + if (config.frontend.mapMirrors) { + let mapMirrors = config.frontend.mapMirrors; this.#mapMirrors = { ...this.#mapMirrors, ...mapMirrors, @@ -255,8 +251,8 @@ export class Map extends L.Map { } /* Then load the map layers */ - if (config.mapLayers) { - let mapLayers = config.mapLayers; + if (config.frontend.mapLayers) { + let mapLayers = config.frontend.mapLayers; this.#mapLayers = { ...this.#mapLayers, ...mapLayers, @@ -289,12 +285,118 @@ export class Map extends L.Map { if (this.#panUp || this.#panDown || this.#panRight || this.#panLeft) this.panBy( new L.Point( - ((this.#panLeft ? -1 : 0) + (this.#panRight ? 1 : 0)) * this.defaultPanDelta * (this.#isShiftKeyDown ? 3 : 1), - ((this.#panUp ? -1 : 0) + (this.#panDown ? 1 : 0)) * this.defaultPanDelta * (this.#isShiftKeyDown ? 3 : 1) + ((this.#panLeft ? -1 : 0) + (this.#panRight ? 1 : 0)) * this.defaultPanDelta * (this.#panFast ? 3 : 1), + ((this.#panUp ? -1 : 0) + (this.#panDown ? 1 : 0)) * this.defaultPanDelta * (this.#panFast ? 3 : 1) ) ); }, 20); + getApp() + .getShortcutManager() + .addShortcut("toggleUnitLabels", { + label: "Hide/show labels", + keyUpCallback: () => this.setOption("showUnitLabels", !this.getOptions().showUnitLabels), + code: "KeyL", + }) + .addShortcut("toggleAcquisitionRings", { + label: "Hide/show acquisition rings", + keyUpCallback: () => this.setOption("showUnitsAcquisitionRings", !this.getOptions().showUnitsAcquisitionRings), + code: "KeyE", + }) + .addShortcut("toggleEngagementRings", { + label: "Hide/show engagement rings", + keyUpCallback: () => this.setOption("showUnitsEngagementRings", !this.getOptions().showUnitsEngagementRings), + code: "KeyQ", + }) + .addShortcut("toggleHideShortEngagementRings", { + label: "Hide/show short range rings", + keyUpCallback: () => this.setOption("hideUnitsShortRangeRings", !this.getOptions().hideUnitsShortRangeRings), + code: "KeyR", + }) + .addShortcut("toggleDetectionLines", { + label: "Hide/show detection lines", + keyUpCallback: () => this.setOption("showUnitTargets", !this.getOptions().showUnitTargets), + code: "KeyF", + }) + .addShortcut("toggleGroupMembers", { + label: "Hide/show group members", + keyUpCallback: () => this.setOption("hideGroupMembers", !this.getOptions().hideGroupMembers), + code: "KeyG", + }) + .addShortcut("toggleRelativePositions", { + label: "Toggle group movement mode", + keyUpCallback: () => this.setOption("keepRelativePositions", !this.getOptions().keepRelativePositions), + code: "KeyP", + }) + .addShortcut("increaseCameraZoom", { + label: "Increase camera zoom", + altKey: true, + keyUpCallback: () => this.increaseCameraZoom(), + code: "Equal", + }) + .addShortcut("decreaseCameraZoom", { + label: "Decrease camera zoom", + altKey: true, + keyUpCallback: () => this.decreaseCameraZoom(), + code: "Minus", + }); + + for (let contextActionName in ContextActions) { + if (ContextActions[contextActionName].getOptions().hotkey) { + getApp() + .getShortcutManager() + .addShortcut(`${contextActionName}Hotkey`, { + label: ContextActions[contextActionName].getLabel(), + code: ContextActions[contextActionName].getOptions().hotkey, + shiftKey: true, + keyUpCallback: () => { + const contextActionSet = this.getContextActionSet(); + if ( + getApp().getState() === OlympusState.UNIT_CONTROL && + contextActionSet && + ContextActions[contextActionName].getId() in contextActionSet.getContextActions() + ) { + if (ContextActions[contextActionName].getOptions().executeImmediately) ContextActions[contextActionName].executeCallback(); + else this.setContextAction(ContextActions[contextActionName]); + } + }, + }); + } + } + + /* Map panning shortcuts */ + getApp() + .getShortcutManager() + .addShortcut(`panUp`, { + label: "Pan map up", + keyUpCallback: (ev: KeyboardEvent) => this.#panUp = false, + keyDownCallback: (ev: KeyboardEvent) => this.#panUp = true, + code: "KeyW", + }) + .addShortcut(`panDown`, { + label: "Pan map down", + keyUpCallback: (ev: KeyboardEvent) => this.#panDown = false, + keyDownCallback: (ev: KeyboardEvent) => this.#panDown = true, + code: "KeyS", + }) + .addShortcut(`panLeft`, { + label: "Pan map left", + keyUpCallback: (ev: KeyboardEvent) => this.#panLeft = false, + keyDownCallback: (ev: KeyboardEvent) => this.#panLeft = true, + code: "KeyA", + }) + .addShortcut(`panRight`, { + label: "Pan map right", + keyUpCallback: (ev: KeyboardEvent) => this.#panRight = false, + keyDownCallback: (ev: KeyboardEvent) => this.#panRight = true, + code: "KeyD", + }).addShortcut(`panFast`, { + label: "Pan map fast", + keyUpCallback: (ev: KeyboardEvent) => this.#panFast = false, + keyDownCallback: (ev: KeyboardEvent) => this.#panFast = true, + code: "ShiftLeft", + }); + /* Periodically check if the camera control endpoint is available */ this.#cameraControlTimer = window.setInterval(() => { this.#checkCameraPort(); @@ -715,48 +817,6 @@ export class Map extends L.Map { return this.#miniMapLayerGroup; } - handleMapPanning(e: any) { - if (e.type === "keyup") { - switch (e.code) { - case "KeyA": - case "ArrowLeft": - this.#panLeft = false; - break; - case "KeyD": - case "ArrowRight": - this.#panRight = false; - break; - case "KeyW": - case "ArrowUp": - this.#panUp = false; - break; - case "KeyS": - case "ArrowDown": - this.#panDown = false; - break; - } - } else { - switch (e.code) { - case "KeyA": - case "ArrowLeft": - this.#panLeft = true; - break; - case "KeyD": - case "ArrowRight": - this.#panRight = true; - break; - case "KeyW": - case "ArrowUp": - this.#panUp = true; - break; - case "KeyS": - case "ArrowDown": - this.#panDown = true; - break; - } - } - } - addTemporaryMarker(latlng: L.LatLng, name: string, coalition: string, commandHash?: string) { var marker = new TemporaryUnitMarker(latlng, name, coalition, commandHash); marker.addTo(this); @@ -781,6 +841,11 @@ export class Map extends L.Map { MapOptionsChangedEvent.dispatch(this.#options); } + setOptions(options) { + this.#options = { ...options }; + MapOptionsChangedEvent.dispatch(this.#options); + } + getOptions() { return this.#options; } @@ -1089,7 +1154,7 @@ export class Map extends L.Map { else document.dispatchEvent(new CustomEvent("forceboxselect", { detail: e.originalEvent })); } else if (getApp().getState() === OlympusState.UNIT_CONTROL) { if (e.originalEvent.button === 2) { - if (!getApp().getMap().getContextAction()) { + if (!this.getContextAction()) { getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.MAP_CONTEXT_MENU); MapContextMenuRequestEvent.dispatch(pressLocation); } @@ -1127,14 +1192,6 @@ export class Map extends L.Map { if (this.#slaveDCSCamera) this.#broadcastPosition(); } - #onKeyDown(e: any) { - this.#isShiftKeyDown = e.originalEvent.shiftKey; - } - - #onKeyUp(e: any) { - this.#isShiftKeyDown = e.originalEvent.shiftKey; - } - #onZoomStart(e: any) { this.#previousZoom = this.getZoom(); if (this.#centeredUnit != null) this.#panToUnit(this.#centeredUnit); diff --git a/frontend/react/src/olympusapp.ts b/frontend/react/src/olympusapp.ts index f7e7c513..62bd3cbb 100644 --- a/frontend/react/src/olympusapp.ts +++ b/frontend/react/src/olympusapp.ts @@ -21,8 +21,8 @@ import { ServerManager } from "./server/servermanager"; import { AudioManager } from "./audio/audiomanager"; import { NO_SUBSTATE, OlympusState, OlympusSubState } from "./constants/constants"; -import { AppStateChangedEvent, ConfigLoadedEvent, InfoPopupEvent, SelectedUnitsChangedEvent } from "./events"; -import { OlympusConfig } from "./interfaces"; +import { AppStateChangedEvent, ConfigLoadedEvent, InfoPopupEvent, MapOptionsChangedEvent, SelectedUnitsChangedEvent, ShortcutsChangedEvent } from "./events"; +import { OlympusConfig, ProfileOptions } from "./interfaces"; export var VERSION = "{{OLYMPUS_VERSION_NUMBER}}"; export var IP = window.location.toString(); @@ -34,6 +34,7 @@ export class OlympusApp { #state: OlympusState = OlympusState.NOT_INITIALIZED; #subState: OlympusSubState = NO_SUBSTATE; #infoMessages: string[] = []; + #profileName: string | null = null; /* Main leaflet map, extended by custom methods */ #map: Map | null = null; @@ -52,6 +53,9 @@ export class OlympusApp { if (selectedUnits.length > 0) this.setState(OlympusState.UNIT_CONTROL); else this.getState() === OlympusState.UNIT_CONTROL && this.setState(OlympusState.IDLE); }); + + MapOptionsChangedEvent.on((options) => getApp().saveProfile()); + ShortcutsChangedEvent.on((options) => getApp().saveProfile()); } getMap() { @@ -90,11 +94,12 @@ export class OlympusApp { start() { /* Initialize base functionalitites */ + this.#shortcutManager = new ShortcutManager(); /* Keep first */ + this.#map = new Map("map-container"); this.#missionManager = new MissionManager(); this.#serverManager = new ServerManager(); - this.#shortcutManager = new ShortcutManager(); this.#unitsManager = new UnitsManager(); this.#weaponsManager = new WeaponsManager(); this.#audioManager = new AudioManager(); @@ -143,6 +148,54 @@ export class OlympusApp { return this.#config; } + setProfile(profileName: string) { + this.#profileName = profileName; + } + + saveProfile() { + if (this.#profileName !== null) { + let profile = {}; + profile["mapOptions"] = this.#map?.getOptions(); + profile["shortcuts"] = this.#shortcutManager?.getShortcutsOptions(); + + const requestOptions = { + method: "PUT", // Specify the request method + headers: { "Content-Type": "application/json" }, // Specify the content type + body: JSON.stringify(profile), // Send the data in JSON format + }; + + fetch(window.location.href.split("?")[0].replace("vite/", "") + `resources/profile/${this.#profileName}`, requestOptions) + .then((response) => { + if (response.status === 200) { + console.log(`Profile ${this.#profileName} saved correctly`); + } else { + this.addInfoMessage("Error saving profile"); + throw new Error("Error saving profile file"); + } + }) // Parse the response as JSON + .catch((error) => console.error(error)); // Handle errors + } + } + + getProfile() { + if (this.#profileName && this.#config?.profiles && this.#config?.profiles[this.#profileName]) + return this.#config?.profiles[this.#profileName] as ProfileOptions; + else return null; + } + + loadProfile() { + const profile = this.getProfile(); + if (profile) { + this.#map?.setOptions(profile.mapOptions); + this.#shortcutManager?.setShortcutsOptions(profile.shortcuts); + this.addInfoMessage("Profile loaded correctly"); + console.log(`Profile ${this.#profileName} saved correctly`); + } else { + this.addInfoMessage("Error loading profile"); + console.log(`Error loading profile`); + } + } + setState(state: OlympusState, subState: OlympusSubState = NO_SUBSTATE) { if (state !== this.#state || subState !== this.#subState) { this.#state = state; diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts index f86c2437..a1f5a370 100644 --- a/frontend/react/src/server/servermanager.ts +++ b/frontend/react/src/server/servermanager.ts @@ -14,7 +14,7 @@ import { reactionsToThreat, } from "../constants/constants"; import { AirbasesData, BullseyesData, CommandModeOptions, GeneralSettings, MissionData, Radio, ServerRequestOptions, ServerStatus, TACAN } from "../interfaces"; -import { InfoPopupEvent, ServerStatusUpdatedEvent } from "../events"; +import { ServerStatusUpdatedEvent } from "../events"; export class ServerManager { #connected: boolean = false; @@ -36,6 +36,14 @@ export class ServerManager { this.#lastUpdateTimes[AIRBASES_URI] = Date.now(); this.#lastUpdateTimes[BULLSEYE_URI] = Date.now(); this.#lastUpdateTimes[MISSION_URI] = Date.now(); + + getApp().getShortcutManager().addShortcut("togglePause", { + label: "Pause data update", + callback: () => { + this.setPaused(!this.getPaused()); + }, + code: "Space" + }) } setUsername(newUsername: string) { diff --git a/frontend/react/src/shortcut/shortcut.ts b/frontend/react/src/shortcut/shortcut.ts index fc65efbd..23701958 100644 --- a/frontend/react/src/shortcut/shortcut.ts +++ b/frontend/react/src/shortcut/shortcut.ts @@ -1,46 +1,49 @@ -import { getApp } from "../olympusapp"; -import { ShortcutKeyboardOptions, ShortcutMouseOptions, ShortcutOptions } from "../interfaces"; +import { ShortcutChangedEvent, ShortcutsChangedEvent } from "../events"; +import { ShortcutOptions } from "../interfaces"; import { keyEventWasInInput } from "../other/utils"; -export abstract class Shortcut { - #config: ShortcutOptions; +export class Shortcut { + #id: string; + #options: ShortcutOptions; - constructor(config: ShortcutOptions) { - this.#config = config; - } - - getConfig() { - return this.#config; - } -} - -export class ShortcutKeyboard extends Shortcut { - constructor(config: ShortcutKeyboardOptions) { - config.event = config.event || "keyup"; - super(config); - - document.addEventListener(config.event, (ev: any) => { - if (ev instanceof KeyboardEvent === false || keyEventWasInInput(ev)) { - return; - } - - if (config.code !== ev.code) { - return; - } + constructor(id, options: ShortcutOptions) { + this.#id = id; + this.#options = options; + /* Key up event is mandatory */ + document.addEventListener("keyup", (ev: any) => { + if (keyEventWasInInput(ev) || options.code !== ev.code) return; if ( - (typeof config.altKey !== "boolean" || (typeof config.altKey === "boolean" && ev.altKey === config.altKey)) && - (typeof config.ctrlKey !== "boolean" || (typeof config.ctrlKey === "boolean" && ev.ctrlKey === config.ctrlKey)) && - (typeof config.shiftKey !== "boolean" || (typeof config.shiftKey === "boolean" && ev.shiftKey === config.shiftKey)) - ) { - config.callback(ev); - } + (typeof options.altKey !== "boolean" || (typeof options.altKey === "boolean" && ev.altKey === options.altKey)) && + (typeof options.ctrlKey !== "boolean" || (typeof options.ctrlKey === "boolean" && ev.ctrlKey === options.ctrlKey)) && + (typeof options.shiftKey !== "boolean" || (typeof options.shiftKey === "boolean" && ev.shiftKey === options.shiftKey)) + ) + options.keyUpCallback(ev); }); - } -} -export class ShortcutMouse extends Shortcut { - constructor(config: ShortcutMouseOptions) { - super(config); + /* Key down event is optional */ + if (options.keyDownCallback) { + document.addEventListener("keydown", (ev: any) => { + if (keyEventWasInInput(ev) || options.code !== ev.code) return; + if ( + (typeof options.altKey !== "boolean" || (typeof options.altKey === "boolean" && ev.altKey === options.altKey)) && + (typeof options.ctrlKey !== "boolean" || (typeof options.ctrlKey === "boolean" && ev.ctrlKey === options.ctrlKey)) && + (typeof options.shiftKey !== "boolean" || (typeof options.shiftKey === "boolean" && ev.shiftKey === options.shiftKey)) + ) + if (options.keyDownCallback) options.keyDownCallback(ev); + }); + } + } + + getOptions() { + return this.#options; + } + + setOptions(options: ShortcutOptions) { + this.#options = { ...options }; + } + + getId() { + return this.#id; } } diff --git a/frontend/react/src/shortcut/shortcutmanager.ts b/frontend/react/src/shortcut/shortcutmanager.ts index 07c882fb..74659ea4 100644 --- a/frontend/react/src/shortcut/shortcutmanager.ts +++ b/frontend/react/src/shortcut/shortcutmanager.ts @@ -1,252 +1,42 @@ -import { ContextActions, OlympusState } from "../constants/constants"; -import { ShortcutKeyboardOptions, ShortcutMouseOptions } from "../interfaces"; -import { getApp } from "../olympusapp"; -import { ShortcutKeyboard, ShortcutMouse } from "./shortcut"; +import { ShortcutChangedEvent, ShortcutsChangedEvent } from "../events"; +import { ShortcutOptions } from "../interfaces"; +import { Shortcut } from "./shortcut"; export class ShortcutManager { - #items: { [key: string]: any } = {}; - #keysBeingHeld: string[] = []; - #keyDownCallbacks: CallableFunction[] = []; - #keyUpCallbacks: CallableFunction[] = []; + #shortcuts: { [key: string]: Shortcut } = {}; constructor() { + // Stop ctrl+digits from sending the browser to another tab document.addEventListener("keydown", (ev: KeyboardEvent) => { - if (this.#keysBeingHeld.indexOf(ev.code) < 0) { - this.#keysBeingHeld.push(ev.code); + if (ev.code.indexOf("Digit") >= 0 && ev.ctrlKey === true && ev.altKey === false && ev.shiftKey === false) { + ev.preventDefault(); } - this.#keyDownCallbacks.forEach((callback) => callback(ev)); - }); - - document.addEventListener("keyup", (ev: KeyboardEvent) => { - this.#keysBeingHeld = this.#keysBeingHeld.filter((held) => held !== ev.code); - this.#keyUpCallbacks.forEach((callback) => callback(ev)); - }); - - this.addKeyboardShortcut("togglePause", { - altKey: false, - callback: () => { - getApp().getServerManager().setPaused(!getApp().getServerManager().getPaused()); - }, - code: "Space", - ctrlKey: false, - }) - .addKeyboardShortcut("deselectAll", { - callback: (ev: KeyboardEvent) => { - getApp().getUnitsManager().deselectAllUnits(); - }, - code: "Escape", - }) - .addKeyboardShortcut("toggleUnitLabels", { - altKey: false, - callback: () => { - getApp().getMap().setOption("showUnitLabels", !getApp().getMap().getOptions().showUnitLabels); - }, - code: "KeyL", - ctrlKey: false, - shiftKey: false, - }) - .addKeyboardShortcut("toggleAcquisitionRings", { - altKey: false, - callback: () => { - getApp().getMap().setOption("showUnitsAcquisitionRings", !getApp().getMap().getOptions().showUnitsAcquisitionRings); - }, - code: "KeyE", - ctrlKey: false, - shiftKey: false, - }) - .addKeyboardShortcut("toggleEngagementRings", { - altKey: false, - callback: () => { - getApp().getMap().setOption("showUnitsEngagementRings", !getApp().getMap().getOptions().showUnitsEngagementRings); - }, - code: "KeyQ", - ctrlKey: false, - shiftKey: false, - }) - .addKeyboardShortcut("toggleHideShortEngagementRings", { - altKey: false, - callback: () => { - getApp().getMap().setOption("hideUnitsShortRangeRings", !getApp().getMap().getOptions().hideUnitsShortRangeRings); - }, - code: "KeyR", - ctrlKey: false, - shiftKey: false, - }) - .addKeyboardShortcut("toggleDetectionLines", { - altKey: false, - callback: () => { - getApp().getMap().setOption("showUnitTargets", !getApp().getMap().getOptions().showUnitTargets); - }, - code: "KeyF", - ctrlKey: false, - shiftKey: false, - }) - .addKeyboardShortcut("toggleGroupMembers", { - altKey: false, - callback: () => { - getApp().getMap().setOption("hideGroupMembers", !getApp().getMap().getOptions().hideGroupMembers); - }, - code: "KeyG", - ctrlKey: false, - shiftKey: false, - }) - .addKeyboardShortcut("toggleRelativePositions", { - altKey: false, - callback: () => { - getApp().getMap().setOption("keepRelativePositions", !getApp().getMap().getOptions().keepRelativePositions); - }, - code: "KeyP", - ctrlKey: false, - shiftKey: false, - }) - .addKeyboardShortcut("increaseCameraZoom", { - altKey: true, - callback: () => { - //getApp().getMap().increaseCameraZoom(); - }, - code: "Equal", - ctrlKey: false, - shiftKey: false, - }) - .addKeyboardShortcut("decreaseCameraZoom", { - altKey: true, - callback: () => { - //getApp().getMap().decreaseCameraZoom(); - }, - code: "Minus", - ctrlKey: false, - shiftKey: false, - }); - - for (let contextActionName in ContextActions) { - if (ContextActions[contextActionName].getOptions().hotkey) { - this.addKeyboardShortcut(`${contextActionName}Hotkey`, { - code: ContextActions[contextActionName].getOptions().hotkey, - shiftKey: true, - callback: () => { - const contextActionSet = getApp().getMap().getContextActionSet(); - if ( - getApp().getState() === OlympusState.UNIT_CONTROL && - contextActionSet && - ContextActions[contextActionName].getId() in contextActionSet.getContextActions() - ) { - if (ContextActions[contextActionName].getOptions().executeImmediately) ContextActions[contextActionName].executeCallback(); - else getApp().getMap().setContextAction(ContextActions[contextActionName]); - } - }, - }); - } - } - - let PTTKeys = ["KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "KeyM", "KeyK", "KeyL"]; - PTTKeys.forEach((key, idx) => { - this.addKeyboardShortcut(`PTT${idx}Active`, { - altKey: false, - callback: () => { - getApp().getAudioManager().getSinks()[idx]?.setPtt(true); - }, - code: key, - ctrlKey: false, - shiftKey: false, - event: "keydown", - }).addKeyboardShortcut(`PTT${idx}Active`, { - altKey: false, - callback: () => { - getApp().getAudioManager().getSinks()[idx]?.setPtt(false); - }, - code: key, - ctrlKey: false, - shiftKey: false, - event: "keyup", - }); - }); - - let panKeys = ["KeyW", "KeyA", "KeyS", "KeyD", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"]; - panKeys.forEach((code) => { - this.addKeyboardShortcut(`pan${code}keydown`, { - altKey: false, - callback: (ev: KeyboardEvent) => { - getApp().getMap().handleMapPanning(ev); - }, - code: code, - ctrlKey: false, - event: "keydown", - }); - - this.addKeyboardShortcut(`pan${code}keyup`, { - callback: (ev: KeyboardEvent) => { - getApp().getMap().handleMapPanning(ev); - }, - code: code, - }); - }); - - const digits = ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9"]; - - digits.forEach((code) => { - this.addKeyboardShortcut(`hotgroup${code}`, { - altKey: false, - callback: (ev: KeyboardEvent) => { - if (ev.ctrlKey && ev.shiftKey) - getApp() - .getUnitsManager() - .selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false); - // "Select hotgroup X in addition to any units already selected" - else if (ev.ctrlKey && !ev.shiftKey) - getApp() - .getUnitsManager() - .setHotgroup(parseInt(ev.code.substring(5))); - // "These selected units are hotgroup X (forget any previous membership)" - else if (!ev.ctrlKey && ev.shiftKey) - getApp() - .getUnitsManager() - .addToHotgroup(parseInt(ev.code.substring(5))); - // "Add (append) these units to hotgroup X (in addition to any existing members)" - else - getApp() - .getUnitsManager() - .selectUnitsByHotgroup(parseInt(ev.code.substring(5))); // "Select hotgroup X, deselect any units not in it." - }, - code: code, - }); - - // Stop hotgroup controls sending the browser to another tab - document.addEventListener("keydown", (ev: KeyboardEvent) => { - if (ev.code === code && ev.ctrlKey === true && ev.altKey === false && ev.shiftKey === false) { - ev.preventDefault(); - } - }); }); } - addKeyboardShortcut(name: string, shortcutKeyboardOptions: ShortcutKeyboardOptions) { - this.#items[name] = new ShortcutKeyboard(shortcutKeyboardOptions); + addShortcut(id: string, shortcutOptions: ShortcutOptions) { + this.#shortcuts[id] = new Shortcut(id, shortcutOptions); + ShortcutsChangedEvent.dispatch(this.#shortcuts); return this; } - addMouseShortcut(name: string, shortcutMouseOptions: ShortcutMouseOptions) { - this.#items[name] = new ShortcutMouse(shortcutMouseOptions); - return this; - } - - getKeysBeingHeld() { - return this.#keysBeingHeld; - } - - keyComboMatches(combo: string[]) { - const heldKeys = this.getKeysBeingHeld(); - if (combo.length !== heldKeys.length) { - return false; + getShortcutsOptions() { + let shortcutsOptions = {}; + for (let id in this.#shortcuts) { + shortcutsOptions[id] = this.#shortcuts[id].getOptions(); } - - return combo.every((key) => heldKeys.indexOf(key) > -1); + return shortcutsOptions; } - onKeyDown(callback: CallableFunction) { - this.#keyDownCallbacks.push(callback); + setShortcutsOptions(shortcutOptions: { [key: string]: ShortcutOptions }) { + for (let id in shortcutOptions) { + this.#shortcuts[id].setOptions(shortcutOptions[id]); + } + ShortcutsChangedEvent.dispatch(this.#shortcuts); } - onKeyUp(callback: CallableFunction) { - this.#keyUpCallbacks.push(callback); + setShortcutOption(id: string, shortcutOptions: ShortcutOptions) { + this.#shortcuts[id].setOptions(shortcutOptions); + ShortcutsChangedEvent.dispatch(this.#shortcuts); } } diff --git a/frontend/react/src/ui/modals/keybindmodal.tsx b/frontend/react/src/ui/modals/keybindmodal.tsx new file mode 100644 index 00000000..41b41401 --- /dev/null +++ b/frontend/react/src/ui/modals/keybindmodal.tsx @@ -0,0 +1,151 @@ +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 { OlympusState } from "../../constants/constants"; +import { Shortcut } from "../../shortcut/shortcut"; +import { BindShortcutRequestEvent, ShortcutsChangedEvent } from "../../events"; + +export function KeybindModal(props: { open: boolean }) { + const [shortcuts, setShortcuts] = useState({} as { [key: string]: Shortcut }); + const [shortcut, setShortcut] = useState(null as null | Shortcut); + const [code, setCode] = useState(null as null | string); + const [shiftKey, setShiftKey] = useState(false); + const [ctrlKey, setCtrlKey] = useState(false); + const [altKey, setAltKey] = useState(false); + + useEffect(() => { + ShortcutsChangedEvent.on((shortcuts) => setShortcuts({ ...shortcuts })); + BindShortcutRequestEvent.on((shortcut) => setShortcut(shortcut)); + + document.addEventListener("keydown", (ev) => { + setCode(ev.code); + if (!(ev.code.indexOf("Shift") >= 0 || ev.code.indexOf("Alt") >= 0 || ev.code.indexOf("Control") >= 0)) { + setShiftKey(ev.shiftKey); + setAltKey(ev.altKey); + setCtrlKey(ev.ctrlKey); + } + }); + }, []); + + let available: null | boolean = code ? true : null; + let inUseShortcut: null | Shortcut = null; + for (let id in shortcuts) { + if ( + code === shortcuts[id].getOptions().code && + shiftKey == shortcuts[id].getOptions().shiftKey && + altKey == shortcuts[id].getOptions().altKey && + ctrlKey == shortcuts[id].getOptions().shiftKey + ) { + available = false; + inUseShortcut = shortcuts[id]; + } + } + + return ( + <> + {props.open && ( + <> + +
+
+ + {shortcut?.getOptions().label} + + + Press the key you want to bind to this event + +
+
+ {ctrlKey ? "Ctrl + " : ""} + {shiftKey ? "Shift + " : ""} + {altKey ? "Alt + " : ""} + + {code} +
+
+ {available === true &&
Keybind is free!
} + {available === false && ( +
+ Keybind is already in use:{" "} + + {inUseShortcut?.getOptions().label} + +
+ )} +
+
+ {available && shortcut && ( + + )} + +
+
+
+
+ + )} + + ); +} diff --git a/frontend/react/src/ui/modals/login.tsx b/frontend/react/src/ui/modals/login.tsx index 1c5c3125..abd19c1d 100644 --- a/frontend/react/src/ui/modals/login.tsx +++ b/frontend/react/src/ui/modals/login.tsx @@ -4,19 +4,54 @@ import { Card } from "./components/card"; import { ErrorCallout } from "../../ui/components/olcallout"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faArrowRight, faCheckCircle, faExternalLink } from "@fortawesome/free-solid-svg-icons"; -import { VERSION } from "../../olympusapp"; +import { getApp, VERSION } from "../../olympusapp"; +import { sha256 } from "js-sha256"; +import { BLUE_COMMANDER, GAME_MASTER, OlympusState, RED_COMMANDER } from "../../constants/constants"; -export function LoginModal(props: { - checkingPassword: boolean; - loginError: boolean; - commandMode: string | null; - onLogin: (password: string) => void; - onContinue: (username: string) => void; - onBack: () => void; -}) { +export function LoginModal(props: {}) { // TODO: add warning if not in secure context and some features are disabled const [password, setPassword] = useState(""); - const [displayName, setDisplayName] = useState("Game Master"); + const [profileName, setProfileName] = useState("Game Master"); + const [checkingPassword, setCheckingPassword] = useState(false); + const [loginError, setLoginError] = useState(false); + const [commandMode, setCommandMode] = useState(null as null | string); + + function checkPassword(password: string) { + setCheckingPassword(true); + var hash = sha256.create(); + getApp().getServerManager().setPassword(hash.update(password).hex()); + getApp() + .getServerManager() + .getMission( + (response) => { + const commandMode = response.mission.commandModeOptions.commandMode; + try { + [GAME_MASTER, BLUE_COMMANDER, RED_COMMANDER].includes(commandMode) ? setCommandMode(commandMode) : setLoginError(true); + } catch { + setLoginError(true); + } + setCheckingPassword(false); + }, + () => { + setLoginError(true); + setCheckingPassword(false); + } + ); + } + + function connect() { + getApp().getServerManager().setUsername(profileName); + getApp().getServerManager().startUpdate(); + getApp().setState(OlympusState.IDLE); + + /* Set the profile name */ + getApp().setProfile(profileName); + /* If no profile exists already with that name, create it from scratch from the defaults */ + if (getApp().getProfile() === null) + getApp().saveProfile(); + /* Load the profile */ + getApp().loadProfile(); + } return ( - {!props.checkingPassword ? ( + {!checkingPassword ? ( <>
- {!props.loginError ? ( + {!loginError ? ( <> - {props.commandMode === null ? ( + {commandMode === null ? ( <>
- -
+
+
+ + Your selection contains protected units, are you sure you want to continue? + + + Pressing "Continue" will cause all DCS controlled units in the current selection to abort their mission and start following Olympus commands + only. + + + If you are trying to delete a human player unit, they will be killed and de-slotted. Be careful! + + + To disable this warning, press on the{" "} + + + {" "} + button + +
+
+ + +
+
+ +
+ + )} + ); } diff --git a/frontend/react/src/ui/panels/header.tsx b/frontend/react/src/ui/panels/header.tsx index 2da800a0..46759d1d 100644 --- a/frontend/react/src/ui/panels/header.tsx +++ b/frontend/react/src/ui/panels/header.tsx @@ -35,7 +35,7 @@ export function Header() { MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions })); MapSourceChangedEvent.on((source) => setMapSource(source)); ConfigLoadedEvent.on((config: OlympusConfig) => { - var sources = Object.keys(config.mapMirrors).concat(Object.keys(config.mapLayers)); + var sources = Object.keys(config.frontend.mapMirrors).concat(Object.keys(config.frontend.mapLayers)); setMapSources(sources); }); CommandModeOptionsChangedEvent.on((commandModeOptions) => { @@ -112,9 +112,7 @@ export function Header() { {commandModeOptions.commandMode === BLUE_COMMANDER && (
BLUE Commander ({commandModeOptions.spawnPoints.blue} points)
diff --git a/frontend/react/src/ui/panels/optionsmenu.tsx b/frontend/react/src/ui/panels/optionsmenu.tsx index 9dd9c6c8..30c8f9e2 100644 --- a/frontend/react/src/ui/panels/optionsmenu.tsx +++ b/frontend/react/src/ui/panels/optionsmenu.tsx @@ -4,14 +4,28 @@ import { OlCheckbox } from "../components/olcheckbox"; import { OlRangeSlider } from "../components/olrangeslider"; import { OlNumberInput } from "../components/olnumberinput"; import { getApp } from "../../olympusapp"; -import { MAP_OPTIONS_DEFAULTS } from "../../constants/constants"; -import { MapOptionsChangedEvent } from "../../events"; +import { MAP_OPTIONS_DEFAULTS, OlympusState, OptionsSubstate } from "../../constants/constants"; +import { BindShortcutRequestEvent, MapOptionsChangedEvent, ShortcutsChangedEvent } from "../../events"; +import { OlAccordion } from "../components/olaccordion"; +import { Shortcut } from "../../shortcut/shortcut"; +import { OlSearchBar } from "../components/olsearchbar"; + +const enum Accordion { + NONE, + BINDINGS, + MAP_OPTIONS, + CAMERA_PLUGIN, +} export function OptionsMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) { const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS); + const [shortcuts, setShortcuts] = useState({} as { [key: string]: Shortcut }); + const [openAccordion, setOpenAccordion] = useState(Accordion.NONE); + const [filterString, setFilterString] = useState(""); useEffect(() => { MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions })); + ShortcutsChangedEvent.on((shortcuts) => setShortcuts({ ...shortcuts })); }, []); return ( @@ -22,226 +36,201 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre dark:text-white `} > -
{ - getApp().getMap().setOption("showUnitLabels", !mapOptions.showUnitLabels); - }} + + setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.BINDINGS: Accordion.NONE ) + } + open={openAccordion === Accordion.BINDINGS} + title="Key bindings" > - {}}> - Show Unit Labels - setFilterString(value)} text={filterString} /> +
- L - -
-
{ - getApp().getMap().setOption("showUnitsEngagementRings", !mapOptions.showUnitsEngagementRings); - }} - > - {}}> - Show Threat Rings - - Q - -
-
{ - getApp().getMap().setOption("showUnitsAcquisitionRings", !mapOptions.showUnitsAcquisitionRings); - }} - > - {}}> - Show Detection rings - - E - -
-
{ - getApp().getMap().setOption("showUnitTargets", !mapOptions.showUnitTargets); - }} - > - {}}> - Show Detection lines - - F - -
-
{ - getApp().getMap().setOption("hideUnitsShortRangeRings", !mapOptions.hideUnitsShortRangeRings); - }} - > - {}}> - Hide Short range Rings - - R - -
-
{ - getApp().getMap().setOption("keepRelativePositions", !mapOptions.keepRelativePositions); - }} - > - {}}> - Keep units relative positions - - P - -
-
{ - getApp().getMap().setOption("hideGroupMembers", !mapOptions.hideGroupMembers); - }} - > - {}}> - Hide Group members - - G - -
-
{ - getApp().getMap().setOption("showMinimap", !mapOptions.showMinimap); - }} - > - {}}> - Show minimap - - ? - -
+ {Object.entries(shortcuts) + .filter(([id, shortcut]) => shortcut.getOptions().label.toLowerCase().indexOf(filterString.toLowerCase()) >= 0) + .map(([id, shortcut]) => { + return ( +
{ + getApp().setState(OlympusState.OPTIONS, OptionsSubstate.KEYBIND); + BindShortcutRequestEvent.dispatch(shortcut); + }} + > + {shortcut.getOptions().label} + + {shortcut.getOptions().altKey ? "Alt + " : ""} + {shortcut.getOptions().ctrlKey ? "Ctrl + " : ""} + {shortcut.getOptions().shiftKey ? "Shift + " : ""} + {shortcut.getOptions().code} + +
+ ); + })} +
+ - -
setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.MAP_OPTIONS: Accordion.NONE )} open={openAccordion === Accordion.MAP_OPTIONS} title="Map options"> +
getApp().getMap().setOption("showUnitLabels", !mapOptions.showUnitLabels)} + > + {}}> + Show Unit Labels +
+
getApp().getMap().setOption("showUnitsEngagementRings", !mapOptions.showUnitsEngagementRings)} + > + {}}> + Show Threat Rings +
+
getApp().getMap().setOption("showUnitsAcquisitionRings", !mapOptions.showUnitsAcquisitionRings)} + > + {}}> + Show Detection rings +
+
getApp().getMap().setOption("showUnitTargets", !mapOptions.showUnitTargets)} + > + {}}> + Show Detection lines +
+
getApp().getMap().setOption("hideUnitsShortRangeRings", !mapOptions.hideUnitsShortRangeRings)} + > + {}}> + Hide Short range Rings +
+
getApp().getMap().setOption("keepRelativePositions", !mapOptions.keepRelativePositions)} + > + {}}> + Keep units relative positions +
+
getApp().getMap().setOption("hideGroupMembers", !mapOptions.hideGroupMembers)} + > + {}}> + Hide Group members +
+
getApp().getMap().setOption("showMinimap", !mapOptions.showMinimap)} + > + {}}> + Show minimap +
+ + + setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.CAMERA_PLUGIN: Accordion.NONE )} open={openAccordion === Accordion.CAMERA_PLUGIN} title="Camera plugin options"> +
-
+
-
- DCS Camera Zoom Scaling - -
- { - getApp().getMap().setOption("cameraPluginRatio", parseInt(ev.target.value)) - }} - value={mapOptions.cameraPluginRatio} - min={0} - max={100} - step={1} - /> -
-
- +
+ DCS Camera Port -
- { getApp().getMap().setOption("cameraPluginPort", mapOptions.cameraPluginPort - 1) }} - onIncrease={() => { getApp().getMap().setOption("cameraPluginPort", mapOptions.cameraPluginPort + 1) }} - onChange={(ev) => { getApp().getMap().setOption("cameraPluginPort", ev.target.value)}} - /> -
-
+ `} + > + DCS Camera Zoom Scaling +
+
+ getApp().getMap().setOption("cameraPluginRatio", parseInt(ev.target.value))} + value={mapOptions.cameraPluginRatio} + min={0} + max={100} + step={1} + /> +
+
+ + DCS Camera Port + +
+ + getApp() + .getMap() + .setOption("cameraPluginPort", mapOptions.cameraPluginPort - 1) + } + onIncrease={() => + getApp() + .getMap() + .setOption("cameraPluginPort", mapOptions.cameraPluginPort + 1) + } + onChange={(ev) => getApp().getMap().setOption("cameraPluginPort", ev.target.value)} + /> +
+
+
); diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index 71b2e78c..d9f08091 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -9,29 +9,25 @@ import { SideBar } from "./panels/sidebar"; import { OptionsMenu } from "./panels/optionsmenu"; import { MapHiddenTypes, MapOptions } from "../types/types"; import { - BLUE_COMMANDER, - GAME_MASTER, - MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusState, OlympusSubState, - RED_COMMANDER, + OptionsSubstate, UnitControlSubState, } from "../constants/constants"; import { getApp, setupApp } from "../olympusapp"; import { LoginModal } from "./modals/login"; -import { sha256 } from "js-sha256"; + import { MiniMapPanel } from "./panels/minimappanel"; import { UnitControlBar } from "./panels/unitcontrolbar"; import { DrawingMenu } from "./panels/drawingmenu"; import { ControlsPanel } from "./panels/controlspanel"; import { MapContextMenu } from "./contextmenus/mapcontextmenu"; import { AirbaseMenu } from "./panels/airbasemenu"; -import { Airbase } from "../mission/airbase"; import { AudioMenu } from "./panels/audiomenu"; import { FormationMenu } from "./panels/formationmenu"; -import { Unit } from "../unit/unit"; import { ProtectionPrompt } from "./modals/protectionprompt"; +import { KeybindModal } from "./modals/keybindmodal"; import { UnitExplosionMenu } from "./panels/unitexplosionmenu"; import { JTACMenu } from "./panels/jtacmenu"; import { AppStateChangedEvent, MapOptionsChangedEvent } from "../events"; @@ -54,10 +50,6 @@ export type OlympusUIState = { export function UI() { const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState); - - const [checkingPassword, setCheckingPassword] = useState(false); - const [loginError, setLoginError] = useState(false); - const [commandMode, setCommandMode] = useState(null as null | string); useEffect(() => { AppStateChangedEvent.on((state, subState) => { @@ -66,34 +58,7 @@ export function UI() { }); }, []); - function checkPassword(password: string) { - setCheckingPassword(true); - var hash = sha256.create(); - getApp().getServerManager().setPassword(hash.update(password).hex()); - getApp() - .getServerManager() - .getMission( - (response) => { - const commandMode = response.mission.commandModeOptions.commandMode; - try { - [GAME_MASTER, BLUE_COMMANDER, RED_COMMANDER].includes(commandMode) ? setCommandMode(commandMode) : setLoginError(true); - } catch { - setLoginError(true); - } - setCheckingPassword(false); - }, - () => { - setLoginError(true); - setCheckingPassword(false); - } - ); - } - - function connect(username: string) { - getApp().getServerManager().setUsername(username); - getApp().getServerManager().startUpdate(); - getApp().setState(OlympusState.IDLE); - } + return (
{ - checkPassword(password); - }} - onContinue={(username) => { - connect(username); - }} - onBack={() => { - setCommandMode(null); - }} - checkingPassword={checkingPassword} - loginError={loginError} - commandMode={commandMode} + + /> )} - {appState === OlympusState.UNIT_CONTROL && appSubState == UnitControlSubState.PROTECTION && ( - <> -
- - - )} + + + +
getApp().setState(OlympusState.IDLE)} /> getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)}/> + getApp().setState(OlympusState.IDLE)} /> getApp().setState(OlympusState.IDLE)} /> getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)}/> + getApp().setState(OlympusState.IDLE)} /> getApp().setState(OlympusState.IDLE)} /> getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} /> + getApp().setState(OlympusState.IDLE)} + /> getApp().setState(OlympusState.IDLE)} /> diff --git a/frontend/react/src/unit/unitsmanager.ts b/frontend/react/src/unit/unitsmanager.ts index 31816bd3..3bb7a894 100644 --- a/frontend/react/src/unit/unitsmanager.ts +++ b/frontend/react/src/unit/unitsmanager.ts @@ -30,10 +30,8 @@ import { CommandModeOptionsChangedEvent, ContactsUpdatedEvent, HotgroupsChangedEvent, - InfoPopupEvent, SelectedUnitsChangedEvent, SelectionClearedEvent, - UnitDatabaseLoadedEvent, UnitDeselectedEvent, UnitSelectedEvent, } from "../events"; @@ -72,9 +70,63 @@ export class UnitsManager { UnitSelectedEvent.on((unit) => this.#onUnitSelection(unit)); UnitDeselectedEvent.on((unit) => this.#onUnitDeselection(unit)); - document.addEventListener("copy", () => this.copy()); - document.addEventListener("keyup", (event) => this.#onKeyUp(event)); - document.addEventListener("paste", () => this.paste()); + getApp() + .getShortcutManager() + .addShortcut("deselectAll", { + label: "Deselect all units", + keyUpCallback: (ev: KeyboardEvent) => { + getApp().getUnitsManager().deselectAllUnits(); + }, + code: "Escape", + }) + .addShortcut("delete", { + label: "Delete selected units", + keyUpCallback: () => this.delete(), + code: "Delete", + }) + .addShortcut("selectAll", { + label: "Select all units", + keyUpCallback: () => { + Object.values(this.getUnits()) + .filter((unit: Unit) => { + return !unit.getHidden(); + }) + .forEach((unit: Unit) => unit.setSelected(true)); + }, + code: "KeyA", + ctrlKey: true, + }) + .addShortcut("copyUnits", { + label: "Copy units", + keyUpCallback: () => this.copy(), + code: "KeyC", + ctrlKey: true, + }) + .addShortcut("pasteUnits", { + label: "Paste units", + keyUpCallback: () => this.paste(), + code: "KeyV", + ctrlKey: true, + }); + + const digits = ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9"]; + digits.forEach((code, idx) => { + getApp() + .getShortcutManager() + .addShortcut(`hotgroup${idx}`, { + label: `Hotgroup ${idx} management`, + keyUpCallback: (ev: KeyboardEvent) => { + if (ev.ctrlKey && ev.shiftKey) this.selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false); + // "Select hotgroup X in addition to any units already selected" + else if (ev.ctrlKey && !ev.shiftKey) this.setHotgroup(parseInt(ev.code.substring(5))); + // "These selected units are hotgroup X (forget any previous membership)" + else if (!ev.ctrlKey && ev.shiftKey) this.addToHotgroup(parseInt(ev.code.substring(5))); + // "Add (append) these units to hotgroup X (in addition to any existing members)" + else this.selectUnitsByHotgroup(parseInt(ev.code.substring(5))); // "Select hotgroup X, deselect any units not in it." + }, + code: code, + }); + }); //this.#slowDeleteDialog = new Dialog("slow-delete-dialog"); } @@ -1059,21 +1111,19 @@ export class UnitsManager { units.forEach((unit: Unit) => unit.setHotgroup(hotgroup)); this.#showActionMessage(units, `added to hotgroup ${hotgroup}`); - let hotgroups: {[key: number]: number} = {}; + let hotgroups: { [key: number]: number } = {}; for (let ID in this.#units) { - const unit = this.#units[ID] + const unit = this.#units[ID]; if (unit.getAlive() && !unit.getHuman()) { - const hotgroup = unit.getHotgroup() + const hotgroup = unit.getHotgroup(); if (hotgroup) { if (!(hotgroup in hotgroups)) { hotgroups[hotgroup] = 1; - } - else - hotgroups[hotgroup] += 1; + } else hotgroups[hotgroup] += 1; } } } - HotgroupsChangedEvent.dispatch(hotgroups) + HotgroupsChangedEvent.dispatch(hotgroups); } /** Delete the selected units @@ -1481,18 +1531,6 @@ export class UnitsManager { } /***********************************************/ - #onKeyUp(event: KeyboardEvent) { - if (!keyEventWasInInput(event)) { - if (event.key === "Delete") this.delete(); - else if (event.key === "a" && event.ctrlKey) - Object.values(this.getUnits()) - .filter((unit: Unit) => { - return !unit.getHidden(); - }) - .forEach((unit: Unit) => unit.setSelected(true)); - } - } - #onUnitSelection(unit: Unit) { if (this.getSelectedUnits().length > 0) { /* Disable the firing of the selection event for a certain amount of time. This avoids firing many events if many units are selected */ diff --git a/frontend/server/src/routes/resources.ts b/frontend/server/src/routes/resources.ts index 728c2fc7..f3a17746 100644 --- a/frontend/server/src/routes/resources.ts +++ b/frontend/server/src/routes/resources.ts @@ -7,12 +7,27 @@ module.exports = function (configLocation) { if (fs.existsSync(configLocation)) { let rawdata = fs.readFileSync(configLocation, "utf-8"); const config = JSON.parse(rawdata); - res.send(JSON.stringify({...config.frontend, ...(config.audio ?? {}) })); + res.send(JSON.stringify({frontend:{...config.frontend}, audio:{...(config.audio ?? {})}, profiles: {...(config.profiles ?? {})} })); res.end() } else { res.sendStatus(404); } }); + + router.put('/profile/:profileName', function (req, res, next) { + if (fs.existsSync(configLocation)) { + let rawdata = fs.readFileSync(configLocation, "utf-8"); + const config = JSON.parse(rawdata); + if (config.profiles === undefined) + config.profiles = {} + config.profiles[req.params.profileName] = req.body; + fs.writeFileSync(configLocation, JSON.stringify(config, null, 2), "utf-8"); + res.end() + } else { + res.sendStatus(404); + } + }); + return router; } \ No newline at end of file