mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Added keybinding menu and server side profiles
This commit is contained in:
parent
62af0f74e7
commit
68980651dc
@ -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) {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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 } = {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
151
frontend/react/src/ui/modals/keybindmodal.tsx
Normal file
151
frontend/react/src/ui/modals/keybindmodal.tsx
Normal file
@ -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 && (
|
||||
<>
|
||||
<Modal
|
||||
className={`
|
||||
inline-flex h-fit w-[600px] overflow-y-auto scroll-smooth bg-white
|
||||
p-10
|
||||
dark:bg-olympus-800
|
||||
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
|
||||
max-md:border-none
|
||||
`}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col gap-4">
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
{shortcut?.getOptions().label}
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
Press the key you want to bind to this event
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full text-center text-white">
|
||||
{ctrlKey ? "Ctrl + " : ""}
|
||||
{shiftKey ? "Shift + " : ""}
|
||||
{altKey ? "Alt + " : ""}
|
||||
|
||||
{code}
|
||||
</div>
|
||||
<div className="text-white">
|
||||
{available === true && <div className="text-green-600">Keybind is free!</div>}
|
||||
{available === false && (
|
||||
<div>
|
||||
Keybind is already in use:{" "}
|
||||
<span
|
||||
className={`font-bold text-red-600`}
|
||||
>
|
||||
{inUseShortcut?.getOptions().label}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
{available && shortcut && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (shortcut && code) {
|
||||
let options = shortcut.getOptions()
|
||||
options.code = code;
|
||||
options.altKey = altKey;
|
||||
options.shiftKey = shiftKey;
|
||||
options.ctrlKey = ctrlKey;
|
||||
getApp().getShortcutManager().setShortcutOption(shortcut.getId(), options)
|
||||
|
||||
getApp().setState(OlympusState.OPTIONS);
|
||||
}
|
||||
}}
|
||||
className={`
|
||||
mb-2 me-2 ml-auto flex content-center items-center gap-2
|
||||
rounded-sm bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700
|
||||
dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Continue
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().setState(OlympusState.OPTIONS)}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
|
||||
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400
|
||||
dark:hover:bg-gray-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<div className={`fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95`}></div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<Modal
|
||||
@ -62,7 +97,7 @@ export function LoginModal(props: {
|
||||
max-lg:w-[100%]
|
||||
`}
|
||||
>
|
||||
{!props.checkingPassword ? (
|
||||
{!checkingPassword ? (
|
||||
<>
|
||||
<div className="flex flex-col items-start">
|
||||
<div
|
||||
@ -115,9 +150,9 @@ export function LoginModal(props: {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!props.loginError ? (
|
||||
{!loginError ? (
|
||||
<>
|
||||
{props.commandMode === null ? (
|
||||
{commandMode === null ? (
|
||||
<>
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<label
|
||||
@ -148,7 +183,7 @@ export function LoginModal(props: {
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.onLogin(password)}
|
||||
onClick={() => checkPassword(password)}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2
|
||||
rounded-sm bg-blue-700 px-5 py-2.5 text-sm
|
||||
@ -172,24 +207,19 @@ export function LoginModal(props: {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col items-start
|
||||
gap-2
|
||||
`}
|
||||
>
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<label
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Set display name
|
||||
Set profile name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
onChange={(ev) => setDisplayName(ev.currentTarget.value)}
|
||||
onChange={(ev) => setProfileName(ev.currentTarget.value)}
|
||||
className={`
|
||||
block w-full max-w-80 rounded-lg border
|
||||
border-gray-300 bg-gray-50 p-2.5 text-sm
|
||||
@ -201,14 +231,17 @@ export function LoginModal(props: {
|
||||
focus:border-blue-500 focus:ring-blue-500
|
||||
`}
|
||||
placeholder="Enter display name"
|
||||
value={displayName}
|
||||
value={profileName}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
The profile name you choose determines what keybinds/groups/options get loaded and edited. Be careful!
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.onContinue(displayName)}
|
||||
onClick={() => connect()}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2
|
||||
rounded-sm bg-blue-700 px-5 py-2.5 text-sm
|
||||
@ -249,11 +282,7 @@ export function LoginModal(props: {
|
||||
title="Server could not be reached"
|
||||
description="The Olympus Server at this address could not be reached. Check the address is correct, restart the Olympus server or reinstall Olympus. Ensure the ports set are not already used."
|
||||
></ErrorCallout>
|
||||
<div
|
||||
className={`
|
||||
text-sm font-medium text-gray-200
|
||||
`}
|
||||
>
|
||||
<div className={`text-sm font-medium text-gray-200`}>
|
||||
Still having issues? See our
|
||||
<a
|
||||
href=""
|
||||
|
||||
@ -2,100 +2,110 @@ import React from "react";
|
||||
import { Modal } from "./components/modal";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Unit } from "../../unit/unit";
|
||||
import { FaLock } from "react-icons/fa6";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { OlympusState } from "../../constants/constants";
|
||||
|
||||
export function ProtectionPrompt(props: {}) {
|
||||
export function ProtectionPrompt(props: { open: boolean }) {
|
||||
return (
|
||||
<Modal
|
||||
className={`
|
||||
inline-flex h-fit w-[600px] overflow-y-auto scroll-smooth bg-white p-10
|
||||
dark:bg-olympus-800
|
||||
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
|
||||
max-md:border-none
|
||||
`}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col gap-12">
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<span
|
||||
<>
|
||||
{props.open && (
|
||||
<>
|
||||
<Modal
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
inline-flex h-fit w-[600px] overflow-y-auto scroll-smooth bg-white
|
||||
p-10
|
||||
dark:bg-olympus-800
|
||||
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
|
||||
max-md:border-none
|
||||
`}
|
||||
>
|
||||
Your selection contains protected units, are you sure you want to continue?
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
Pressing "Continue" will cause all DCS controlled units in the current selection to abort their mission and start following Olympus commands only.
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
If you are trying to delete a human player unit, they will be killed and de-slotted. Be careful!
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
To disable this warning, press on the{" "}
|
||||
<span
|
||||
className={`
|
||||
inline-block translate-y-3 rounded-full border-[1px]
|
||||
border-gray-900 bg-red-500 p-2 text-olympus-900
|
||||
`}
|
||||
>
|
||||
<FaLock />
|
||||
</span>{" "}
|
||||
button
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
getApp().getUnitsManager().executeProtectionCallback();
|
||||
getApp().setState(OlympusState.UNIT_CONTROL);
|
||||
}}
|
||||
className={`
|
||||
mb-2 me-2 ml-auto flex content-center items-center gap-2
|
||||
rounded-sm bg-blue-700 px-5 py-2.5 text-sm font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Continue
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().setState(OlympusState.UNIT_CONTROL)}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
|
||||
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400
|
||||
dark:hover:bg-gray-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<div className="flex h-full w-full flex-col gap-12">
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Your selection contains protected units, are you sure you want to continue?
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
Pressing "Continue" will cause all DCS controlled units in the current selection to abort their mission and start following Olympus commands
|
||||
only.
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
If you are trying to delete a human player unit, they will be killed and de-slotted. Be careful!
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
To disable this warning, press on the{" "}
|
||||
<span
|
||||
className={`
|
||||
inline-block translate-y-3 rounded-full border-[1px]
|
||||
border-gray-900 bg-red-500 p-2 text-olympus-900
|
||||
`}
|
||||
>
|
||||
<FaLock />
|
||||
</span>{" "}
|
||||
button
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
getApp().getUnitsManager().executeProtectionCallback();
|
||||
getApp().setState(OlympusState.UNIT_CONTROL);
|
||||
}}
|
||||
className={`
|
||||
mb-2 me-2 ml-auto flex content-center items-center gap-2
|
||||
rounded-sm bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700
|
||||
dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Continue
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().setState(OlympusState.UNIT_CONTROL)}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
|
||||
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400
|
||||
dark:hover:bg-gray-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<div className={`fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95`}></div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
</div>
|
||||
{commandModeOptions.commandMode === BLUE_COMMANDER && (
|
||||
<div
|
||||
className={`
|
||||
flex h-full rounded-md bg-blue-600 px-4 text-white
|
||||
`}
|
||||
className={`flex h-full rounded-md bg-blue-600 px-4 text-white`}
|
||||
>
|
||||
<span className="my-auto font-bold">BLUE Commander ({commandModeOptions.spawnPoints.blue} points)</span>
|
||||
</div>
|
||||
|
||||
@ -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
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer gap-4
|
||||
p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("showUnitLabels", !mapOptions.showUnitLabels);
|
||||
}}
|
||||
<OlAccordion
|
||||
onClick={() =>
|
||||
setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.BINDINGS: Accordion.NONE )
|
||||
}
|
||||
open={openAccordion === Accordion.BINDINGS}
|
||||
title="Key bindings"
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showUnitLabels} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show Unit Labels</span>
|
||||
<kbd
|
||||
<OlSearchBar onChange={(value) => setFilterString(value)} text={filterString} />
|
||||
<div
|
||||
className={`
|
||||
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
|
||||
text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
flex max-h-[450px] flex-col gap-1 overflow-y-scroll no-scrollbar
|
||||
`}
|
||||
>
|
||||
L
|
||||
</kbd>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer gap-4
|
||||
p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("showUnitsEngagementRings", !mapOptions.showUnitsEngagementRings);
|
||||
}}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showUnitsEngagementRings} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show Threat Rings</span>
|
||||
<kbd
|
||||
className={`
|
||||
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
|
||||
text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
Q
|
||||
</kbd>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer gap-4
|
||||
p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("showUnitsAcquisitionRings", !mapOptions.showUnitsAcquisitionRings);
|
||||
}}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showUnitsAcquisitionRings} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show Detection rings</span>
|
||||
<kbd
|
||||
className={`
|
||||
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
|
||||
text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
E
|
||||
</kbd>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer gap-4
|
||||
p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("showUnitTargets", !mapOptions.showUnitTargets);
|
||||
}}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showUnitTargets} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show Detection lines</span>
|
||||
<kbd
|
||||
className={`
|
||||
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
|
||||
text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
F
|
||||
</kbd>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer gap-4
|
||||
p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("hideUnitsShortRangeRings", !mapOptions.hideUnitsShortRangeRings);
|
||||
}}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.hideUnitsShortRangeRings} onChange={() => {}}></OlCheckbox>
|
||||
<span>Hide Short range Rings</span>
|
||||
<kbd
|
||||
className={`
|
||||
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
|
||||
text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
R
|
||||
</kbd>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer gap-4
|
||||
p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("keepRelativePositions", !mapOptions.keepRelativePositions);
|
||||
}}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.keepRelativePositions} onChange={() => {}}></OlCheckbox>
|
||||
<span>Keep units relative positions</span>
|
||||
<kbd
|
||||
className={`
|
||||
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
|
||||
text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
P
|
||||
</kbd>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer gap-4
|
||||
p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("hideGroupMembers", !mapOptions.hideGroupMembers);
|
||||
}}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.hideGroupMembers} onChange={() => {}}></OlCheckbox>
|
||||
<span>Hide Group members</span>
|
||||
<kbd
|
||||
className={`
|
||||
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
|
||||
text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
G
|
||||
</kbd>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer gap-4
|
||||
p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("showMinimap", !mapOptions.showMinimap);
|
||||
}}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showMinimap} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show minimap</span>
|
||||
<kbd
|
||||
className={`
|
||||
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
|
||||
text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
?
|
||||
</kbd>
|
||||
</div>
|
||||
{Object.entries(shortcuts)
|
||||
.filter(([id, shortcut]) => shortcut.getOptions().label.toLowerCase().indexOf(filterString.toLowerCase()) >= 0)
|
||||
.map(([id, shortcut]) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
group relative mr-2 flex cursor-pointer select-none
|
||||
items-center justify-between rounded-sm px-2 py-2 text-sm
|
||||
dark:text-gray-300 dark:hover:bg-olympus-500
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().setState(OlympusState.OPTIONS, OptionsSubstate.KEYBIND);
|
||||
BindShortcutRequestEvent.dispatch(shortcut);
|
||||
}}
|
||||
>
|
||||
<span>{shortcut.getOptions().label}</span>
|
||||
<span>
|
||||
{shortcut.getOptions().altKey ? "Alt + " : ""}
|
||||
{shortcut.getOptions().ctrlKey ? "Ctrl + " : ""}
|
||||
{shortcut.getOptions().shiftKey ? "Shift + " : ""}
|
||||
{shortcut.getOptions().code}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</OlAccordion>
|
||||
|
||||
|
||||
<hr className={`
|
||||
<OlAccordion onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.MAP_OPTIONS: Accordion.NONE )} open={openAccordion === Accordion.MAP_OPTIONS} title="Map options">
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer
|
||||
gap-4 p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getMap().setOption("showUnitLabels", !mapOptions.showUnitLabels)}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showUnitLabels} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show Unit Labels</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer
|
||||
gap-4 p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getMap().setOption("showUnitsEngagementRings", !mapOptions.showUnitsEngagementRings)}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showUnitsEngagementRings} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show Threat Rings</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer
|
||||
gap-4 p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getMap().setOption("showUnitsAcquisitionRings", !mapOptions.showUnitsAcquisitionRings)}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showUnitsAcquisitionRings} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show Detection rings</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer
|
||||
gap-4 p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getMap().setOption("showUnitTargets", !mapOptions.showUnitTargets)}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showUnitTargets} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show Detection lines</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row gap-4 rounded-md justify-content
|
||||
cursor-pointer p-2 text-sm
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getMap().setOption("hideUnitsShortRangeRings", !mapOptions.hideUnitsShortRangeRings)}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.hideUnitsShortRangeRings} onChange={() => {}}></OlCheckbox>
|
||||
<span>Hide Short range Rings</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row gap-4 rounded-md justify-content
|
||||
cursor-pointer p-2 text-sm
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getMap().setOption("keepRelativePositions", !mapOptions.keepRelativePositions)}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.keepRelativePositions} onChange={() => {}}></OlCheckbox>
|
||||
<span>Keep units relative positions</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row gap-4 rounded-md justify-content
|
||||
cursor-pointer p-2 text-sm
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getMap().setOption("hideGroupMembers", !mapOptions.hideGroupMembers)}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.hideGroupMembers} onChange={() => {}}></OlCheckbox>
|
||||
<span>Hide Group members</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row gap-4 rounded-md justify-content
|
||||
cursor-pointer p-2 text-sm
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getMap().setOption("showMinimap", !mapOptions.showMinimap)}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showMinimap} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show minimap</span>
|
||||
</div>
|
||||
</OlAccordion>
|
||||
|
||||
<OlAccordion onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.CAMERA_PLUGIN: Accordion.NONE )} open={openAccordion === Accordion.CAMERA_PLUGIN} title="Camera plugin options">
|
||||
<hr
|
||||
className={`
|
||||
m-2 my-1 w-auto border-[1px] bg-gray-700
|
||||
dark:border-olympus-500
|
||||
`}></hr>
|
||||
<div className={`
|
||||
`}
|
||||
></hr>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col content-center items-start justify-between gap-2 p-2
|
||||
`}>
|
||||
<div className="flex flex-col">
|
||||
<span className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}>DCS Camera Zoom Scaling</span>
|
||||
|
||||
</div>
|
||||
<OlRangeSlider
|
||||
onChange={(ev) => {
|
||||
getApp().getMap().setOption("cameraPluginRatio", parseInt(ev.target.value))
|
||||
}}
|
||||
value={mapOptions.cameraPluginRatio}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
<div className={`
|
||||
flex flex-col content-center items-start justify-between gap-2 p-2
|
||||
`}>
|
||||
<span className={`
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}>DCS Camera Port</span>
|
||||
<div className="flex">
|
||||
<OlNumberInput
|
||||
value={mapOptions.cameraPluginPort}
|
||||
min={0}
|
||||
max={9999}
|
||||
onDecrease={() => { getApp().getMap().setOption("cameraPluginPort", mapOptions.cameraPluginPort - 1) }}
|
||||
onIncrease={() => { getApp().getMap().setOption("cameraPluginPort", mapOptions.cameraPluginPort + 1) }}
|
||||
onChange={(ev) => { getApp().getMap().setOption("cameraPluginPort", ev.target.value)}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
DCS Camera Zoom Scaling
|
||||
</span>
|
||||
</div>
|
||||
<OlRangeSlider
|
||||
onChange={(ev) => getApp().getMap().setOption("cameraPluginRatio", parseInt(ev.target.value))}
|
||||
value={mapOptions.cameraPluginRatio}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col content-center items-start justify-between gap-2 p-2
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
DCS Camera Port
|
||||
</span>
|
||||
<div className="flex">
|
||||
<OlNumberInput
|
||||
value={mapOptions.cameraPluginPort}
|
||||
min={0}
|
||||
max={9999}
|
||||
onDecrease={() =>
|
||||
getApp()
|
||||
.getMap()
|
||||
.setOption("cameraPluginPort", mapOptions.cameraPluginPort - 1)
|
||||
}
|
||||
onIncrease={() =>
|
||||
getApp()
|
||||
.getMap()
|
||||
.setOption("cameraPluginPort", mapOptions.cameraPluginPort + 1)
|
||||
}
|
||||
onChange={(ev) => getApp().getMap().setOption("cameraPluginPort", ev.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</OlAccordion>
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@ -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 (
|
||||
<div
|
||||
@ -111,36 +76,25 @@ export function UI() {
|
||||
fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95
|
||||
`}></div>
|
||||
<LoginModal
|
||||
onLogin={(password) => {
|
||||
checkPassword(password);
|
||||
}}
|
||||
onContinue={(username) => {
|
||||
connect(username);
|
||||
}}
|
||||
onBack={() => {
|
||||
setCommandMode(null);
|
||||
}}
|
||||
checkingPassword={checkingPassword}
|
||||
loginError={loginError}
|
||||
commandMode={commandMode}
|
||||
|
||||
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{appState === OlympusState.UNIT_CONTROL && appSubState == UnitControlSubState.PROTECTION && (
|
||||
<>
|
||||
<div className={`
|
||||
fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95
|
||||
`}></div>
|
||||
<ProtectionPrompt />
|
||||
</>
|
||||
)}
|
||||
|
||||
<ProtectionPrompt open={appState === OlympusState.UNIT_CONTROL && appSubState == UnitControlSubState.PROTECTION} />
|
||||
<KeybindModal open={appState === OlympusState.OPTIONS && appSubState === OptionsSubstate.KEYBIND} />
|
||||
|
||||
<div id="map-container" className="z-0 h-full w-screen" />
|
||||
<MainMenu open={appState === OlympusState.MAIN_MENU} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
<SpawnMenu open={appState === OlympusState.SPAWN} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
<OptionsMenu open={appState === OlympusState.OPTIONS} onClose={() => getApp().setState(OlympusState.IDLE)}/>
|
||||
<OptionsMenu open={appState === OlympusState.OPTIONS} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
|
||||
<UnitControlMenu
|
||||
open={appState === OlympusState.UNIT_CONTROL && ![UnitControlSubState.FORMATION, UnitControlSubState.UNIT_EXPLOSION_MENU].includes(appSubState as UnitControlSubState)}
|
||||
open={
|
||||
appState === OlympusState.UNIT_CONTROL &&
|
||||
![UnitControlSubState.FORMATION, UnitControlSubState.UNIT_EXPLOSION_MENU].includes(appSubState as UnitControlSubState)
|
||||
}
|
||||
onClose={() => getApp().setState(OlympusState.IDLE)}
|
||||
/>
|
||||
<FormationMenu
|
||||
@ -149,11 +103,14 @@ export function UI() {
|
||||
/>
|
||||
|
||||
<DrawingMenu open={appState === OlympusState.DRAW} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
<AirbaseMenu open={appState === OlympusState.AIRBASE} onClose={() => getApp().setState(OlympusState.IDLE)}/>
|
||||
<AirbaseMenu open={appState === OlympusState.AIRBASE} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
<AudioMenu open={appState === OlympusState.AUDIO} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
<GameMasterMenu open={appState === OlympusState.GAME_MASTER} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
|
||||
<UnitExplosionMenu open={appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.UNIT_EXPLOSION_MENU} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
<UnitExplosionMenu
|
||||
open={appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.UNIT_EXPLOSION_MENU}
|
||||
onClose={() => getApp().setState(OlympusState.IDLE)}
|
||||
/>
|
||||
<JTACMenu open={appState === OlympusState.JTAC} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
|
||||
<MiniMapPanel />
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user