mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
feat: Added custom authentication headers support
bugfix: Fixed incorrect saving of quick access spawn refactor: Removed compactunitspawnmenu and condensed in single unitspawnmenu class to reuse code
This commit is contained in:
parent
c11a9728e8
commit
258d21672c
@ -300,6 +300,13 @@ export enum UnitControlSubState {
|
||||
UNIT_EXPLOSION_MENU = "Unit explosion menu",
|
||||
}
|
||||
|
||||
export enum LoginSubState {
|
||||
NO_SUBSTATE = "No substate",
|
||||
CREDENTIALS = "Credentials",
|
||||
COMMAND_MODE = "Command mode",
|
||||
CONNECT = "Connect"
|
||||
}
|
||||
|
||||
export enum DrawSubState {
|
||||
NO_SUBSTATE = "No substate",
|
||||
DRAW_POLYGON = "Polygon",
|
||||
|
||||
@ -4,6 +4,11 @@ import { MapOptions } from "./types/types";
|
||||
export interface OlympusConfig {
|
||||
frontend: {
|
||||
port: number;
|
||||
customAuthHeaders: {
|
||||
enabled: boolean,
|
||||
username: string,
|
||||
group: string
|
||||
}
|
||||
elevationProvider: {
|
||||
provider: string;
|
||||
username: string | null;
|
||||
@ -26,7 +31,7 @@ export interface OlympusConfig {
|
||||
WSPort?: number;
|
||||
WSEndpoint?: string;
|
||||
};
|
||||
profiles?: ProfileOptions;
|
||||
profiles?: {[key: string]: ProfileOptions};
|
||||
}
|
||||
|
||||
export interface SessionData {
|
||||
@ -277,6 +282,7 @@ export interface UnitBlueprint {
|
||||
indirectFire?: boolean;
|
||||
markerFile?: string;
|
||||
unitWhenGrouped?: string;
|
||||
mainRole?: string;
|
||||
}
|
||||
|
||||
export interface AirbaseOptions {
|
||||
|
||||
@ -546,8 +546,9 @@ export class Map extends L.Map {
|
||||
this.#spawnRequestTable = spawnRequestTable;
|
||||
}
|
||||
|
||||
addStarredSpawnRequestTable(key, spawnRequestTable: SpawnRequestTable) {
|
||||
addStarredSpawnRequestTable(key, spawnRequestTable: SpawnRequestTable, quickAccessName: string) {
|
||||
this.#starredSpawnRequestTables[key] = spawnRequestTable;
|
||||
this.#starredSpawnRequestTables[key].quickAccessName = quickAccessName;
|
||||
StarredSpawnsChangedEvent.dispatch(this.#starredSpawnRequestTables);
|
||||
}
|
||||
|
||||
|
||||
@ -32,6 +32,7 @@ export class MissionManager {
|
||||
#remainingSetupTime: number = 0;
|
||||
#spentSpawnPoint: number = 0;
|
||||
#coalitions: { red: string[]; blue: string[] } = { red: [], blue: [] };
|
||||
#enabledCommandModes: string[] = [];
|
||||
|
||||
constructor() {
|
||||
AppStateChangedEvent.on((state, subState) => {
|
||||
@ -220,6 +221,14 @@ export class MissionManager {
|
||||
return airbase ?? null;
|
||||
}
|
||||
|
||||
setEnabledCommandModes(enabledCommandModes: string[]) {
|
||||
this.#enabledCommandModes = enabledCommandModes;
|
||||
}
|
||||
|
||||
getEnabledCommandModes() {
|
||||
return this.#enabledCommandModes;
|
||||
}
|
||||
|
||||
#setcommandModeOptions(commandModeOptions: CommandModeOptions) {
|
||||
/* Refresh all the data if we have exited the NONE state */
|
||||
var requestRefresh = false;
|
||||
|
||||
@ -20,7 +20,7 @@ import { WeaponsManager } from "./weapon/weaponsmanager";
|
||||
import { ServerManager } from "./server/servermanager";
|
||||
import { AudioManager } from "./audio/audiomanager";
|
||||
|
||||
import { NO_SUBSTATE, OlympusState, OlympusSubState } from "./constants/constants";
|
||||
import { LoginSubState, NO_SUBSTATE, OlympusState, OlympusSubState } from "./constants/constants";
|
||||
import { AppStateChangedEvent, ConfigLoadedEvent, InfoPopupEvent, MapOptionsChangedEvent, SelectedUnitsChangedEvent, ShortcutsChangedEvent } from "./events";
|
||||
import { OlympusConfig, ProfileOptions } from "./interfaces";
|
||||
import { AWACSController } from "./controllers/awacs";
|
||||
@ -32,27 +32,26 @@ export var IP = window.location.toString();
|
||||
export class OlympusApp {
|
||||
/* Global data */
|
||||
#latestVersion: string | undefined = undefined;
|
||||
#config: OlympusConfig | null = null;
|
||||
#config: OlympusConfig;
|
||||
#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;
|
||||
#map: Map;
|
||||
|
||||
/* Managers */
|
||||
#missionManager: MissionManager | null = null;
|
||||
#serverManager: ServerManager | null = null;
|
||||
#shortcutManager: ShortcutManager | null = null;
|
||||
#unitsManager: UnitsManager | null = null;
|
||||
#weaponsManager: WeaponsManager | null = null;
|
||||
#audioManager: AudioManager | null = null;
|
||||
#sessionDataManager: SessionDataManager | null = null;
|
||||
#missionManager: MissionManager;
|
||||
#serverManager: ServerManager;
|
||||
#shortcutManager: ShortcutManager;
|
||||
#unitsManager: UnitsManager;
|
||||
#weaponsManager: WeaponsManager;
|
||||
#audioManager: AudioManager;
|
||||
#sessionDataManager: SessionDataManager;
|
||||
//#pluginsManager: // TODO
|
||||
|
||||
/* Controllers */
|
||||
#AWACSController: AWACSController | null = null;
|
||||
#AWACSController: AWACSController;
|
||||
|
||||
constructor() {
|
||||
SelectedUnitsChangedEvent.on((selectedUnits) => {
|
||||
@ -69,31 +68,31 @@ export class OlympusApp {
|
||||
}
|
||||
|
||||
getServerManager() {
|
||||
return this.#serverManager as ServerManager;
|
||||
return this.#serverManager;
|
||||
}
|
||||
|
||||
getShortcutManager() {
|
||||
return this.#shortcutManager as ShortcutManager;
|
||||
return this.#shortcutManager;
|
||||
}
|
||||
|
||||
getUnitsManager() {
|
||||
return this.#unitsManager as UnitsManager;
|
||||
return this.#unitsManager;
|
||||
}
|
||||
|
||||
getWeaponsManager() {
|
||||
return this.#weaponsManager as WeaponsManager;
|
||||
return this.#weaponsManager;
|
||||
}
|
||||
|
||||
getMissionManager() {
|
||||
return this.#missionManager as MissionManager;
|
||||
return this.#missionManager;
|
||||
}
|
||||
|
||||
getAudioManager() {
|
||||
return this.#audioManager as AudioManager;
|
||||
return this.#audioManager;
|
||||
}
|
||||
|
||||
getSessionDataManager() {
|
||||
return this.#sessionDataManager as SessionDataManager;
|
||||
return this.#sessionDataManager;
|
||||
}
|
||||
|
||||
/* TODO
|
||||
@ -154,20 +153,37 @@ export class OlympusApp {
|
||||
});
|
||||
|
||||
/* Load the config file from the server */
|
||||
const configRequest = new Request(this.getExpressAddress() + "/resources/config");
|
||||
const configRequest = new Request(this.getExpressAddress() + "/resources/config", {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
}
|
||||
});
|
||||
|
||||
fetch(configRequest)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw new Error("Error retrieving config file");
|
||||
}
|
||||
if (response.status === 200)
|
||||
return new Promise((res: ([OlympusConfig, Headers]) => void, rej) => {
|
||||
response
|
||||
.json()
|
||||
.then((result) => res([result, response.headers]))
|
||||
.catch((error) => rej(error));
|
||||
});
|
||||
else throw new Error("Error retrieving config file");
|
||||
})
|
||||
.then((res) => {
|
||||
this.#config = res;
|
||||
.then(([result, headers]) => {
|
||||
this.#config = result;
|
||||
if (this.#config.frontend.customAuthHeaders.enabled) {
|
||||
if (headers.has(this.#config.frontend.customAuthHeaders.username) && headers.has(this.#config.frontend.customAuthHeaders.group)) {
|
||||
this.getServerManager().setUsername(headers.get(this.#config.frontend.customAuthHeaders.username));
|
||||
this.setState(OlympusState.LOGIN, LoginSubState.COMMAND_MODE);
|
||||
}
|
||||
}
|
||||
if (this.getState() !== OlympusState.LOGIN) {
|
||||
this.setState(OlympusState.LOGIN, LoginSubState.CREDENTIALS);
|
||||
}
|
||||
ConfigLoadedEvent.dispatch(this.#config as OlympusConfig);
|
||||
this.setState(OlympusState.LOGIN);
|
||||
});
|
||||
})
|
||||
.catch((error) => console.error);
|
||||
|
||||
this.#shortcutManager?.addShortcut("idle", {
|
||||
label: "Deselect all",
|
||||
@ -184,12 +200,9 @@ export class OlympusApp {
|
||||
return this.#config;
|
||||
}
|
||||
|
||||
setProfile(profileName: string) {
|
||||
this.#profileName = profileName;
|
||||
}
|
||||
|
||||
saveProfile() {
|
||||
if (this.#profileName !== null) {
|
||||
const username = this.getServerManager()?.getUsername();
|
||||
if (username) {
|
||||
let profile = {};
|
||||
profile["mapOptions"] = this.#map?.getOptions();
|
||||
profile["shortcuts"] = this.#shortcutManager?.getShortcutsOptions();
|
||||
@ -200,10 +213,10 @@ export class OlympusApp {
|
||||
body: JSON.stringify(profile), // Send the data in JSON format
|
||||
};
|
||||
|
||||
fetch(this.getExpressAddress() + `/resources/profile/${this.#profileName}`, requestOptions)
|
||||
fetch(this.getExpressAddress() + `/resources/profile/${username}`, requestOptions)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
console.log(`Profile ${this.#profileName} saved correctly`);
|
||||
console.log(`Profile for ${username} saved correctly`);
|
||||
} else {
|
||||
this.addInfoMessage("Error saving profile");
|
||||
throw new Error("Error saving profile");
|
||||
@ -214,17 +227,18 @@ export class OlympusApp {
|
||||
}
|
||||
|
||||
resetProfile() {
|
||||
if (this.#profileName !== null) {
|
||||
const username = this.getServerManager().getUsername();
|
||||
if (username) {
|
||||
const requestOptions = {
|
||||
method: "PUT", // Specify the request method
|
||||
headers: { "Content-Type": "application/json" }, // Specify the content type
|
||||
body: "", // Send the data in JSON format
|
||||
};
|
||||
|
||||
fetch(this.getExpressAddress() + `/resources/profile/reset/${this.#profileName}`, requestOptions)
|
||||
fetch(this.getExpressAddress() + `/resources/profile/reset/${username}`, requestOptions)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
console.log(`Profile ${this.#profileName} reset correctly`);
|
||||
console.log(`Profile for ${username} reset correctly`);
|
||||
location.reload();
|
||||
} else {
|
||||
this.addInfoMessage("Error resetting profile");
|
||||
@ -256,22 +270,19 @@ export class OlympusApp {
|
||||
}
|
||||
|
||||
getProfile() {
|
||||
if (this.#profileName && this.#config?.profiles && this.#config?.profiles[this.#profileName])
|
||||
return this.#config?.profiles[this.#profileName] as ProfileOptions;
|
||||
const username = this.getServerManager().getUsername();
|
||||
if (username && this.#config?.profiles && username in this.#config.profiles) return this.#config?.profiles[username];
|
||||
else return null;
|
||||
}
|
||||
|
||||
getProfileName() {
|
||||
return this.#profileName;
|
||||
}
|
||||
|
||||
loadProfile() {
|
||||
const username = this.getServerManager().getUsername();
|
||||
const profile = this.getProfile();
|
||||
if (profile) {
|
||||
if (username && profile) {
|
||||
this.#map?.setOptions(profile.mapOptions);
|
||||
this.#shortcutManager?.setShortcutsOptions(profile.shortcuts);
|
||||
this.addInfoMessage("Profile loaded correctly");
|
||||
console.log(`Profile ${this.#profileName} loaded correctly`);
|
||||
console.log(`Profile for ${username} loaded correctly`);
|
||||
} else {
|
||||
this.addInfoMessage("Profile not found, creating new profile");
|
||||
console.log(`Error loading profile`);
|
||||
@ -302,6 +313,4 @@ export class OlympusApp {
|
||||
InfoPopupEvent.dispatch(this.#infoMessages);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -404,4 +404,26 @@ export function blobToBase64(blob) {
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
export function mode(array)
|
||||
{
|
||||
if(array.length == 0)
|
||||
return null;
|
||||
var modeMap = {};
|
||||
var maxEl = array[0], maxCount = 1;
|
||||
for(var i = 0; i < array.length; i++)
|
||||
{
|
||||
var el = array[i];
|
||||
if(modeMap[el] == null)
|
||||
modeMap[el] = 1;
|
||||
else
|
||||
modeMap[el]++;
|
||||
if(modeMap[el] > maxCount)
|
||||
{
|
||||
maxEl = el;
|
||||
maxCount = modeMap[el];
|
||||
}
|
||||
}
|
||||
return maxEl;
|
||||
}
|
||||
@ -20,8 +20,8 @@ export class ServerManager {
|
||||
#connected: boolean = false;
|
||||
#paused: boolean = false;
|
||||
#REST_ADDRESS = "http://localhost:3001/olympus";
|
||||
#username = "no-username";
|
||||
#password = "";
|
||||
#username: null | string = null;
|
||||
#password: null | string = null;
|
||||
#sessionHash: string | null = null;
|
||||
#lastUpdateTimes: { [key: string]: number } = {};
|
||||
#previousMissionElapsedTime: number = 0; // Track if mission elapsed time is increasing (i.e. is the server paused)
|
||||
@ -29,6 +29,7 @@ export class ServerManager {
|
||||
#intervals: number[] = [];
|
||||
#requests: { [key: string]: XMLHttpRequest } = {};
|
||||
#updateMode = "normal"; // normal or awacs
|
||||
#activeCommandMode = "";
|
||||
|
||||
constructor() {
|
||||
this.#lastUpdateTimes[UNITS_URI] = Date.now();
|
||||
@ -61,10 +62,18 @@ export class ServerManager {
|
||||
this.#username = newUsername;
|
||||
}
|
||||
|
||||
getUsername() {
|
||||
return this.#username;
|
||||
}
|
||||
|
||||
setPassword(newPassword: string) {
|
||||
this.#password = newPassword;
|
||||
}
|
||||
|
||||
setActiveCommandMode(activeCommandMode: string) {
|
||||
this.#activeCommandMode = activeCommandMode;
|
||||
}
|
||||
|
||||
GET(
|
||||
callback: CallableFunction,
|
||||
errorCallback: CallableFunction,
|
||||
@ -94,7 +103,8 @@ export class ServerManager {
|
||||
xmlHttp.open("GET", `${this.#REST_ADDRESS}/${uri}${optionsString ? `?${optionsString}` : ""}`, true);
|
||||
|
||||
/* If provided, set the credentials */
|
||||
if (this.#username && this.#password) xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username}:${this.#password}`));
|
||||
xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username ?? ""}:${this.#password ?? ""}`));
|
||||
xmlHttp.setRequestHeader("X-Command-Mode", this.#activeCommandMode);
|
||||
|
||||
/* If specified, set the response type */
|
||||
if (responseType) xmlHttp.responseType = responseType as XMLHttpRequestResponseType;
|
||||
@ -105,6 +115,10 @@ export class ServerManager {
|
||||
this.setConnected(true);
|
||||
if (xmlHttp.responseType == "arraybuffer") this.#lastUpdateTimes[uri] = callback(xmlHttp.response);
|
||||
else {
|
||||
/* Check if the response headers contain the enabled command modes and set them */
|
||||
if (xmlHttp.getResponseHeader("X-Enabled-Command-Modes"))
|
||||
getApp().getMissionManager().setEnabledCommandModes(xmlHttp.getResponseHeader("X-Enabled-Command-Modes")?.split(",") ??[])
|
||||
|
||||
const result = JSON.parse(xmlHttp.responseText);
|
||||
this.#lastUpdateTimes[uri] = callback(result);
|
||||
|
||||
@ -137,7 +151,8 @@ export class ServerManager {
|
||||
var xmlHttp = new XMLHttpRequest();
|
||||
xmlHttp.open("PUT", this.#REST_ADDRESS);
|
||||
xmlHttp.setRequestHeader("Content-Type", "application/json");
|
||||
if (this.#username && this.#password) xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username}:${this.#password}`));
|
||||
xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username ?? ""}:${this.#password ?? ""}`));
|
||||
xmlHttp.setRequestHeader("X-Command-Mode", this.#activeCommandMode);
|
||||
xmlHttp.onload = (res: any) => {
|
||||
var res = JSON.parse(xmlHttp.responseText);
|
||||
callback(res.commandHash);
|
||||
|
||||
@ -87,10 +87,10 @@ export class SessionDataManager {
|
||||
body: JSON.stringify({ sessionHash }), // Send the data in JSON format
|
||||
};
|
||||
|
||||
fetch(getApp().getExpressAddress() + `/resources/sessiondata/load/${getApp().getProfileName()}`, requestOptions)
|
||||
fetch(getApp().getExpressAddress() + `/resources/sessiondata/load/${getApp().getServerManager().getUsername()}`, requestOptions)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
console.log(`Session data for profile ${getApp().getProfileName()} and session hash ${sessionHash} loaded correctly`);
|
||||
console.log(`Session data for profile ${getApp().getServerManager().getUsername()} and session hash ${sessionHash} loaded correctly`);
|
||||
return response.json();
|
||||
} else {
|
||||
getApp().addInfoMessage("No session data found for this profile");
|
||||
@ -118,10 +118,10 @@ export class SessionDataManager {
|
||||
body: JSON.stringify({ sessionHash: this.#sessionHash, sessionData: this.#sessionData }), // Send the data in JSON format
|
||||
};
|
||||
|
||||
fetch(getApp().getExpressAddress() + `/resources/sessiondata/save/${getApp().getProfileName()}`, requestOptions)
|
||||
fetch(getApp().getExpressAddress() + `/resources/sessiondata/save/${getApp().getServerManager().getUsername()}`, requestOptions)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
console.log(`Session data for profile ${getApp().getProfileName()} and session hash ${this.#sessionHash} saved correctly`);
|
||||
console.log(`Session data for profile ${getApp().getServerManager().getUsername()} and session hash ${this.#sessionHash} saved correctly`);
|
||||
console.log(this.#sessionData);
|
||||
SessionDataChangedEvent.dispatch(this.#sessionData);
|
||||
} else {
|
||||
|
||||
@ -3,11 +3,31 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { UnitBlueprint } from "../../interfaces";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons/faArrowRight";
|
||||
import { mode } from "../../other/utils";
|
||||
|
||||
export function OlUnitListEntry(props: { icon: IconProp; blueprint: UnitBlueprint; showCost: boolean; cost: number; onClick: () => void }) {
|
||||
let pillString = "" as string | undefined
|
||||
if (props.showCost) pillString = `${props.cost} points`
|
||||
else pillString = !["aircraft", "helicopter"].includes(props.blueprint.category) ? props.blueprint.type : props.blueprint.abilities
|
||||
export function OlUnitListEntry(props: {
|
||||
icon?: IconProp;
|
||||
silhouette?: string;
|
||||
blueprint: UnitBlueprint;
|
||||
showCost: boolean;
|
||||
cost: number;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
let pillString = "" as string | undefined;
|
||||
if (props.showCost) pillString = `${props.cost} points`;
|
||||
else {
|
||||
if (["aircraft", "helicopter"].includes(props.blueprint.category)) {
|
||||
let roles = props.blueprint.loadouts?.flatMap((loadout) => loadout.roles).filter((role) => role !== "No task");
|
||||
if (roles !== undefined) {
|
||||
let uniqueRoles = roles?.reduce((acc, current) => {if (!acc.includes(current)) {acc.push(current)} return acc}, [] as string[])
|
||||
let mainRole = mode(roles);
|
||||
pillString = uniqueRoles.length > 6 ? "Multirole" : mainRole;
|
||||
}
|
||||
} else {
|
||||
if (props.blueprint.category)
|
||||
pillString = props.blueprint.type;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
onClick={props.onClick}
|
||||
@ -17,7 +37,18 @@ export function OlUnitListEntry(props: { icon: IconProp; blueprint: UnitBlueprin
|
||||
dark:text-gray-300 dark:hover:bg-olympus-500
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={props.icon} className="text-sm"></FontAwesomeIcon>
|
||||
{props.icon && <FontAwesomeIcon icon={props.icon} className="text-sm"></FontAwesomeIcon>}
|
||||
{props.silhouette && (
|
||||
<div className={`
|
||||
mr-2 flex h-6 w-6 rotate-90 content-center justify-center opacity-50
|
||||
invert
|
||||
`}>
|
||||
<img
|
||||
src={`public/images/units/${props.silhouette}`}
|
||||
className="my-auto max-h-full max-w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 px-2 text-left font-normal">{props.blueprint.label}</div>
|
||||
{pillString && (
|
||||
<div
|
||||
|
||||
@ -28,7 +28,6 @@ import { OlDropdownItem } from "../components/oldropdown";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
|
||||
import { Coalition } from "../../types/types";
|
||||
import { CompactUnitSpawnMenu } from "../panels/compactunitspawnmenu";
|
||||
import { CompactEffectSpawnMenu } from "../panels/compacteffectspawnmenu";
|
||||
|
||||
enum CategoryGroup {
|
||||
@ -158,449 +157,450 @@ export function SpawnContextMenu(props: {}) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{appState === OlympusState.SPAWN_CONTEXT && (
|
||||
<>
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={`
|
||||
absolute flex w-[395px] flex-wrap gap-2 rounded-md bg-olympus-800
|
||||
`}
|
||||
>
|
||||
<div className="flex w-full flex-col gap-4 px-6 py-3">
|
||||
<div className="flex flex-wrap justify-between gap-2">
|
||||
<OlCoalitionToggle
|
||||
coalition={spawnCoalition}
|
||||
onClick={() => {
|
||||
spawnCoalition === "blue" && setSpawnCoalition("neutral");
|
||||
spawnCoalition === "neutral" && setSpawnCoalition("red");
|
||||
spawnCoalition === "red" && setSpawnCoalition("blue");
|
||||
}}
|
||||
<div
|
||||
ref={contentRef}
|
||||
data-hidden={appState !== OlympusState.SPAWN_CONTEXT}
|
||||
className={`
|
||||
absolute flex w-[395px] data- flex-wrap gap-2 rounded-md
|
||||
bg-olympus-800
|
||||
data-[hidden=true]:hidden
|
||||
`}
|
||||
>
|
||||
<div className="flex w-full flex-col gap-4 px-6 py-3">
|
||||
<div className="flex flex-wrap justify-between gap-2">
|
||||
<OlCoalitionToggle
|
||||
coalition={spawnCoalition}
|
||||
onClick={() => {
|
||||
spawnCoalition === "blue" && setSpawnCoalition("neutral");
|
||||
spawnCoalition === "neutral" && setSpawnCoalition("red");
|
||||
spawnCoalition === "red" && setSpawnCoalition("blue");
|
||||
}}
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.AIRCRAFT}
|
||||
onClick={() => (openAccordion !== CategoryGroup.AIRCRAFT ? setOpenAccordion(CategoryGroup.AIRCRAFT) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={olButtonsVisibilityAircraft}
|
||||
tooltip="Show aircraft units"
|
||||
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.HELICOPTER}
|
||||
onClick={() => (openAccordion !== CategoryGroup.HELICOPTER ? setOpenAccordion(CategoryGroup.HELICOPTER) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={olButtonsVisibilityHelicopter}
|
||||
tooltip="Show helicopter units"
|
||||
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.AIR_DEFENCE}
|
||||
onClick={() => (openAccordion !== CategoryGroup.AIR_DEFENCE ? setOpenAccordion(CategoryGroup.AIR_DEFENCE) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={olButtonsVisibilityGroundunitSam}
|
||||
tooltip="Show air defence units"
|
||||
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.GROUND_UNIT}
|
||||
onClick={() => (openAccordion !== CategoryGroup.GROUND_UNIT ? setOpenAccordion(CategoryGroup.GROUND_UNIT) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={olButtonsVisibilityGroundunit}
|
||||
tooltip="Show ground units"
|
||||
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.NAVY_UNIT}
|
||||
onClick={() => (openAccordion !== CategoryGroup.NAVY_UNIT ? setOpenAccordion(CategoryGroup.NAVY_UNIT) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={olButtonsVisibilityNavyunit}
|
||||
tooltip="Show navy units"
|
||||
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
|
||||
/>
|
||||
<OlStateButton checked={showMore} onClick={() => setShowMore(!showMore)} icon={faEllipsisVertical} tooltip="Show more options" />
|
||||
{showMore && (
|
||||
<>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.EFFECT}
|
||||
onClick={() => (openAccordion !== CategoryGroup.EFFECT ? setOpenAccordion(CategoryGroup.EFFECT) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={faExplosion}
|
||||
tooltip="Show effects"
|
||||
className="ml-auto"
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.AIRCRAFT}
|
||||
onClick={() => (openAccordion !== CategoryGroup.AIRCRAFT ? setOpenAccordion(CategoryGroup.AIRCRAFT) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={olButtonsVisibilityAircraft}
|
||||
tooltip="Show aircraft units"
|
||||
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
|
||||
checked={openAccordion === CategoryGroup.SEARCH}
|
||||
onClick={() => (openAccordion !== CategoryGroup.SEARCH ? setOpenAccordion(CategoryGroup.SEARCH) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={faSearch}
|
||||
tooltip="Search unit"
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.HELICOPTER}
|
||||
onClick={() =>
|
||||
openAccordion !== CategoryGroup.HELICOPTER ? setOpenAccordion(CategoryGroup.HELICOPTER) : setOpenAccordion(CategoryGroup.NONE)
|
||||
}
|
||||
icon={olButtonsVisibilityHelicopter}
|
||||
tooltip="Show helicopter units"
|
||||
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
|
||||
checked={openAccordion === CategoryGroup.STARRED}
|
||||
onClick={() => (openAccordion !== CategoryGroup.STARRED ? setOpenAccordion(CategoryGroup.STARRED) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={faStar}
|
||||
tooltip="Show starred spanws"
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.AIR_DEFENCE}
|
||||
onClick={() =>
|
||||
openAccordion !== CategoryGroup.AIR_DEFENCE ? setOpenAccordion(CategoryGroup.AIR_DEFENCE) : setOpenAccordion(CategoryGroup.NONE)
|
||||
}
|
||||
icon={olButtonsVisibilityGroundunitSam}
|
||||
tooltip="Show air defence units"
|
||||
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.GROUND_UNIT}
|
||||
onClick={() =>
|
||||
openAccordion !== CategoryGroup.GROUND_UNIT ? setOpenAccordion(CategoryGroup.GROUND_UNIT) : setOpenAccordion(CategoryGroup.NONE)
|
||||
}
|
||||
icon={olButtonsVisibilityGroundunit}
|
||||
tooltip="Show ground units"
|
||||
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.NAVY_UNIT}
|
||||
onClick={() => (openAccordion !== CategoryGroup.NAVY_UNIT ? setOpenAccordion(CategoryGroup.NAVY_UNIT) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={olButtonsVisibilityNavyunit}
|
||||
tooltip="Show navy units"
|
||||
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
|
||||
/>
|
||||
<OlStateButton checked={showMore} onClick={() => setShowMore(!showMore)} icon={faEllipsisVertical} tooltip="Show more options" />
|
||||
{showMore && (
|
||||
<>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.EFFECT}
|
||||
onClick={() => (openAccordion !== CategoryGroup.EFFECT ? setOpenAccordion(CategoryGroup.EFFECT) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={faExplosion}
|
||||
tooltip="Show effects"
|
||||
className="ml-auto"
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.SEARCH}
|
||||
onClick={() => (openAccordion !== CategoryGroup.SEARCH ? setOpenAccordion(CategoryGroup.SEARCH) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={faSearch}
|
||||
tooltip="Search unit"
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={openAccordion === CategoryGroup.STARRED}
|
||||
onClick={() => (openAccordion !== CategoryGroup.STARRED ? setOpenAccordion(CategoryGroup.STARRED) : setOpenAccordion(CategoryGroup.NONE))}
|
||||
icon={faStar}
|
||||
tooltip="Show starred spanws"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{blueprint === null && effect === null && openAccordion !== CategoryGroup.NONE && (
|
||||
<div className="mb-3 flex flex-col gap-4">
|
||||
<>
|
||||
<>
|
||||
{openAccordion === CategoryGroup.AIRCRAFT && (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{roles.aircraft.sort().map((role) => {
|
||||
return (
|
||||
<div
|
||||
key={role}
|
||||
data-selected={selectedRole === role}
|
||||
className={`
|
||||
cursor-pointer rounded-full bg-olympus-900
|
||||
px-2 py-0.5 text-xs font-bold
|
||||
text-olympus-50
|
||||
data-[selected='true']:bg-blue-500
|
||||
data-[selected='true']:text-gray-200
|
||||
`}
|
||||
onClick={() => {
|
||||
selectedRole === role ? setSelectedRole(null) : setSelectedRole(role);
|
||||
}}
|
||||
>
|
||||
{role}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[350px] flex-col gap-1
|
||||
overflow-y-scroll no-scrollbar
|
||||
`}
|
||||
>
|
||||
{blueprints
|
||||
?.sort((a, b) => (a.label > b.label ? 1 : -1))
|
||||
.filter((blueprint) => blueprint.category === "aircraft")
|
||||
.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
icon={olButtonsVisibilityAircraft}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.HELICOPTER && (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{roles.helicopter.sort().map((role) => {
|
||||
return (
|
||||
<div
|
||||
key={role}
|
||||
data-selected={selectedRole === role}
|
||||
className={`
|
||||
cursor-pointer rounded-full bg-olympus-900
|
||||
px-2 py-0.5 text-xs font-bold
|
||||
text-olympus-50
|
||||
data-[selected='true']:bg-blue-500
|
||||
data-[selected='true']:text-gray-200
|
||||
`}
|
||||
onClick={() => {
|
||||
selectedRole === role ? setSelectedRole(null) : setSelectedRole(role);
|
||||
}}
|
||||
>
|
||||
{role}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[350px] flex-col gap-1
|
||||
overflow-y-scroll no-scrollbar
|
||||
`}
|
||||
>
|
||||
{blueprints
|
||||
?.sort((a, b) => (a.label > b.label ? 1 : -1))
|
||||
.filter((blueprint) => blueprint.category === "helicopter")
|
||||
.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
icon={olButtonsVisibilityHelicopter}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.AIR_DEFENCE && (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{types.groundunit
|
||||
.sort()
|
||||
?.filter((type) => type === "SAM Site" || type === "AAA")
|
||||
.map((type) => {
|
||||
return (
|
||||
<div
|
||||
key={type}
|
||||
data-selected={selectedType === type}
|
||||
className={`
|
||||
cursor-pointer rounded-full bg-olympus-900
|
||||
px-2 py-0.5 text-xs font-bold
|
||||
text-olympus-50
|
||||
data-[selected='true']:bg-blue-500
|
||||
data-[selected='true']:text-gray-200
|
||||
`}
|
||||
onClick={() => {
|
||||
selectedType === type ? setSelectedType(null) : setSelectedType(type);
|
||||
}}
|
||||
>
|
||||
{type}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[350px] flex-col gap-1
|
||||
overflow-y-scroll no-scrollbar
|
||||
`}
|
||||
>
|
||||
{blueprints
|
||||
?.sort((a, b) => (a.label > b.label ? 1 : -1))
|
||||
.filter((blueprint) => blueprint.category === "groundunit" && (blueprint.type === "SAM Site" || blueprint.type === "AAA"))
|
||||
.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
icon={olButtonsVisibilityGroundunit}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.GROUND_UNIT && (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{types.groundunit
|
||||
.sort()
|
||||
?.filter((type) => type !== "SAM Site" && type !== "AAA")
|
||||
.map((type) => {
|
||||
return (
|
||||
<div
|
||||
key={type}
|
||||
data-selected={selectedType === type}
|
||||
className={`
|
||||
cursor-pointer rounded-full bg-olympus-900
|
||||
px-2 py-0.5 text-xs font-bold
|
||||
text-olympus-50
|
||||
data-[selected='true']:bg-blue-500
|
||||
data-[selected='true']:text-gray-200
|
||||
`}
|
||||
onClick={() => {
|
||||
selectedType === type ? setSelectedType(null) : setSelectedType(type);
|
||||
}}
|
||||
>
|
||||
{type}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[350px] flex-col gap-1
|
||||
overflow-y-scroll no-scrollbar
|
||||
`}
|
||||
>
|
||||
{blueprints
|
||||
?.sort((a, b) => (a.label > b.label ? 1 : -1))
|
||||
.filter((blueprint) => blueprint.category === "groundunit" && blueprint.type !== "SAM Site" && blueprint.type !== "AAA")
|
||||
.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
icon={olButtonsVisibilityGroundunit}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.NAVY_UNIT && (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{types.navyunit.sort().map((type) => {
|
||||
return (
|
||||
<div
|
||||
key={type}
|
||||
data-selected={selectedType === type}
|
||||
className={`
|
||||
cursor-pointer rounded-full bg-olympus-900
|
||||
px-2 py-0.5 text-xs font-bold
|
||||
text-olympus-50
|
||||
data-[selected='true']:bg-blue-500
|
||||
data-[selected='true']:text-gray-200
|
||||
`}
|
||||
onClick={() => {
|
||||
selectedType === type ? setSelectedType(null) : setSelectedType(type);
|
||||
}}
|
||||
>
|
||||
{type}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[350px] flex-col gap-1
|
||||
overflow-y-scroll no-scrollbar
|
||||
`}
|
||||
>
|
||||
{blueprints
|
||||
?.sort((a, b) => (a.label > b.label ? 1 : -1))
|
||||
.filter((blueprint) => blueprint.category === "navyunit")
|
||||
.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
icon={olButtonsVisibilityNavyunit}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.EFFECT && (
|
||||
<>
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[350px] flex-col gap-1
|
||||
overflow-y-scroll no-scrollbar
|
||||
`}
|
||||
>
|
||||
<OlEffectListEntry
|
||||
key={"explosion"}
|
||||
icon={faExplosion}
|
||||
label={"Explosion"}
|
||||
onClick={() => {
|
||||
setEffect("explosion");
|
||||
}}
|
||||
/>
|
||||
<OlEffectListEntry
|
||||
key={"smoke"}
|
||||
icon={faSmog}
|
||||
label={"Smoke"}
|
||||
onClick={() => {
|
||||
setEffect("smoke");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.SEARCH && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<OlSearchBar onChange={(value) => setFilterString(value)} text={filterString} />
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[350px] flex-col gap-1
|
||||
overflow-y-scroll no-scrollbar
|
||||
`}
|
||||
>
|
||||
{filteredBlueprints.length > 0 ? (
|
||||
filteredBlueprints.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
icon={olButtonsVisibilityNavyunit}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : filterString === "" ? (
|
||||
<span className={`text-gray-200`}>Type to search</span>
|
||||
) : (
|
||||
<span className={`text-gray-200`}>No results</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.STARRED && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{Object.values(starredSpawns).length > 0 ? (
|
||||
Object.values(starredSpawns).map((spawnRequestTable) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
className={`
|
||||
flex w-full content-center gap-2 text-sm
|
||||
text-white
|
||||
`}
|
||||
onClick={() => {
|
||||
if (latlng) {
|
||||
spawnRequestTable.unit.location = latlng;
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.spawnUnits(spawnRequestTable.category, Array(spawnRequestTable.amount).fill(spawnRequestTable.unit), spawnRequestTable.coalition, false);
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
data-coalition={spawnRequestTable.coalition}
|
||||
className={`
|
||||
my-auto
|
||||
data-[coalition='blue']:text-blue-500
|
||||
data-[coalition='neutral']:text-gay-500
|
||||
data-[coalition='red']:text-red-500
|
||||
`}
|
||||
icon={faStar}
|
||||
/>
|
||||
<div>
|
||||
{getApp().getUnitsManager().getDatabase().getByName(spawnRequestTable.unit.unitType)?.label} (
|
||||
{spawnRequestTable.quickAccessName})
|
||||
</div>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="p-2 text-sm text-white">No starred spawns, use the spawn menu to create a quick access spawn</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
{!(blueprint === null) && <CompactUnitSpawnMenu blueprint={blueprint} starredSpawns={starredSpawns} latlng={latlng} coalition={spawnCoalition} onBack={() => setBlueprint(null)}/>}
|
||||
{!(effect === null) && latlng && <CompactEffectSpawnMenu effect={effect} latlng={latlng} onBack={() => setEffect(null)} />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{blueprint === null && effect === null && openAccordion !== CategoryGroup.NONE && (
|
||||
<div className="mb-3 flex flex-col gap-4">
|
||||
<>
|
||||
<>
|
||||
{openAccordion === CategoryGroup.AIRCRAFT && (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{roles.aircraft.sort().map((role) => {
|
||||
return (
|
||||
<div
|
||||
key={role}
|
||||
data-selected={selectedRole === role}
|
||||
className={`
|
||||
cursor-pointer rounded-full bg-olympus-900 px-2
|
||||
py-0.5 text-xs font-bold text-olympus-50
|
||||
data-[selected='true']:bg-blue-500
|
||||
data-[selected='true']:text-gray-200
|
||||
`}
|
||||
onClick={() => {
|
||||
selectedRole === role ? setSelectedRole(null) : setSelectedRole(role);
|
||||
}}
|
||||
>
|
||||
{role}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[350px] flex-col gap-1 overflow-y-scroll
|
||||
no-scrollbar
|
||||
`}
|
||||
>
|
||||
{blueprints
|
||||
?.sort((a, b) => (a.label > b.label ? 1 : -1))
|
||||
.filter((blueprint) => blueprint.category === "aircraft")
|
||||
.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
silhouette={blueprint.filename}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.HELICOPTER && (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{roles.helicopter.sort().map((role) => {
|
||||
return (
|
||||
<div
|
||||
key={role}
|
||||
data-selected={selectedRole === role}
|
||||
className={`
|
||||
cursor-pointer rounded-full bg-olympus-900 px-2
|
||||
py-0.5 text-xs font-bold text-olympus-50
|
||||
data-[selected='true']:bg-blue-500
|
||||
data-[selected='true']:text-gray-200
|
||||
`}
|
||||
onClick={() => {
|
||||
selectedRole === role ? setSelectedRole(null) : setSelectedRole(role);
|
||||
}}
|
||||
>
|
||||
{role}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[350px] flex-col gap-1 overflow-y-scroll
|
||||
no-scrollbar
|
||||
`}
|
||||
>
|
||||
{blueprints
|
||||
?.sort((a, b) => (a.label > b.label ? 1 : -1))
|
||||
.filter((blueprint) => blueprint.category === "helicopter")
|
||||
.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
silhouette={blueprint.filename}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.AIR_DEFENCE && (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{types.groundunit
|
||||
.sort()
|
||||
?.filter((type) => type === "SAM Site" || type === "AAA")
|
||||
.map((type) => {
|
||||
return (
|
||||
<div
|
||||
key={type}
|
||||
data-selected={selectedType === type}
|
||||
className={`
|
||||
cursor-pointer rounded-full bg-olympus-900
|
||||
px-2 py-0.5 text-xs font-bold text-olympus-50
|
||||
data-[selected='true']:bg-blue-500
|
||||
data-[selected='true']:text-gray-200
|
||||
`}
|
||||
onClick={() => {
|
||||
selectedType === type ? setSelectedType(null) : setSelectedType(type);
|
||||
}}
|
||||
>
|
||||
{type}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[350px] flex-col gap-1 overflow-y-scroll
|
||||
no-scrollbar
|
||||
`}
|
||||
>
|
||||
{blueprints
|
||||
?.sort((a, b) => (a.label > b.label ? 1 : -1))
|
||||
.filter((blueprint) => blueprint.category === "groundunit" && (blueprint.type === "SAM Site" || blueprint.type === "AAA"))
|
||||
.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
icon={olButtonsVisibilityGroundunitSam}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.GROUND_UNIT && (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{types.groundunit
|
||||
.sort()
|
||||
?.filter((type) => type !== "SAM Site" && type !== "AAA")
|
||||
.map((type) => {
|
||||
return (
|
||||
<div
|
||||
key={type}
|
||||
data-selected={selectedType === type}
|
||||
className={`
|
||||
cursor-pointer rounded-full bg-olympus-900
|
||||
px-2 py-0.5 text-xs font-bold text-olympus-50
|
||||
data-[selected='true']:bg-blue-500
|
||||
data-[selected='true']:text-gray-200
|
||||
`}
|
||||
onClick={() => {
|
||||
selectedType === type ? setSelectedType(null) : setSelectedType(type);
|
||||
}}
|
||||
>
|
||||
{type}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[350px] flex-col gap-1 overflow-y-scroll
|
||||
no-scrollbar
|
||||
`}
|
||||
>
|
||||
{blueprints
|
||||
?.sort((a, b) => (a.label > b.label ? 1 : -1))
|
||||
.filter((blueprint) => blueprint.category === "groundunit" && blueprint.type !== "SAM Site" && blueprint.type !== "AAA")
|
||||
.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
icon={olButtonsVisibilityGroundunit}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.NAVY_UNIT && (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{types.navyunit.sort().map((type) => {
|
||||
return (
|
||||
<div
|
||||
key={type}
|
||||
data-selected={selectedType === type}
|
||||
className={`
|
||||
cursor-pointer rounded-full bg-olympus-900 px-2
|
||||
py-0.5 text-xs font-bold text-olympus-50
|
||||
data-[selected='true']:bg-blue-500
|
||||
data-[selected='true']:text-gray-200
|
||||
`}
|
||||
onClick={() => {
|
||||
selectedType === type ? setSelectedType(null) : setSelectedType(type);
|
||||
}}
|
||||
>
|
||||
{type}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[350px] flex-col gap-1 overflow-y-scroll
|
||||
no-scrollbar
|
||||
`}
|
||||
>
|
||||
{blueprints
|
||||
?.sort((a, b) => (a.label > b.label ? 1 : -1))
|
||||
.filter((blueprint) => blueprint.category === "navyunit")
|
||||
.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
icon={olButtonsVisibilityNavyunit}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.EFFECT && (
|
||||
<>
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[350px] flex-col gap-1 overflow-y-scroll
|
||||
no-scrollbar
|
||||
`}
|
||||
>
|
||||
<OlEffectListEntry
|
||||
key={"explosion"}
|
||||
icon={faExplosion}
|
||||
label={"Explosion"}
|
||||
onClick={() => {
|
||||
setEffect("explosion");
|
||||
}}
|
||||
/>
|
||||
<OlEffectListEntry
|
||||
key={"smoke"}
|
||||
icon={faSmog}
|
||||
label={"Smoke"}
|
||||
onClick={() => {
|
||||
setEffect("smoke");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.SEARCH && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<OlSearchBar onChange={(value) => setFilterString(value)} text={filterString} />
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[350px] flex-col gap-1 overflow-y-scroll
|
||||
no-scrollbar
|
||||
`}
|
||||
>
|
||||
{filteredBlueprints.length > 0 ? (
|
||||
filteredBlueprints.map((blueprint) => {
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
icon={olButtonsVisibilityNavyunit}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : filterString === "" ? (
|
||||
<span className={`text-gray-200`}>Type to search</span>
|
||||
) : (
|
||||
<span className={`text-gray-200`}>No results</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.STARRED && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{Object.values(starredSpawns).length > 0 ? (
|
||||
Object.values(starredSpawns).map((spawnRequestTable) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
className={`
|
||||
flex w-full content-center gap-2 text-sm
|
||||
text-white
|
||||
`}
|
||||
onClick={() => {
|
||||
if (latlng) {
|
||||
spawnRequestTable.unit.location = latlng;
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.spawnUnits(
|
||||
spawnRequestTable.category,
|
||||
Array(spawnRequestTable.amount).fill(spawnRequestTable.unit),
|
||||
spawnRequestTable.coalition,
|
||||
false
|
||||
);
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
data-coalition={spawnRequestTable.coalition}
|
||||
className={`
|
||||
my-auto
|
||||
data-[coalition='blue']:text-blue-500
|
||||
data-[coalition='neutral']:text-gay-500
|
||||
data-[coalition='red']:text-red-500
|
||||
`}
|
||||
icon={faStar}
|
||||
/>
|
||||
<div>
|
||||
{getApp().getUnitsManager().getDatabase().getByName(spawnRequestTable.unit.unitType)?.label} (
|
||||
{spawnRequestTable.quickAccessName})
|
||||
</div>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="p-2 text-sm text-white">No starred spawns, use the spawn menu to create a quick access spawn</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
<UnitSpawnMenu
|
||||
compact={true}
|
||||
visible={blueprint !== null}
|
||||
blueprint={blueprint}
|
||||
starredSpawns={starredSpawns}
|
||||
latlng={latlng}
|
||||
coalition={spawnCoalition}
|
||||
onBack={() => setBlueprint(null)}
|
||||
/>
|
||||
{!(effect === null) && latlng && <CompactEffectSpawnMenu effect={effect} latlng={latlng} onBack={() => setEffect(null)} />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Modal } from "./components/modal";
|
||||
import { Card } from "./components/card";
|
||||
import { ErrorCallout } from "../components/olcallout";
|
||||
@ -6,33 +6,49 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRight, faCheckCircle, faExternalLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getApp, VERSION } from "../../olympusapp";
|
||||
import { sha256 } from "js-sha256";
|
||||
import { BLUE_COMMANDER, GAME_MASTER, OlympusState, RED_COMMANDER } from "../../constants/constants";
|
||||
import { BLUE_COMMANDER, GAME_MASTER, LoginSubState, NO_SUBSTATE, OlympusState, RED_COMMANDER } from "../../constants/constants";
|
||||
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
|
||||
import { AppStateChangedEvent } from "../../events";
|
||||
|
||||
var hash = sha256.create();
|
||||
|
||||
export function LoginModal(props: { open: boolean }) {
|
||||
// TODO: add warning if not in secure context and some features are disabled
|
||||
const [subState, setSubState] = useState(NO_SUBSTATE);
|
||||
const [password, setPassword] = useState("");
|
||||
const [profileName, setProfileName] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [checkingPassword, setCheckingPassword] = useState(false);
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
const [commandMode, setCommandMode] = useState(null as null | string);
|
||||
const [commandModes, setCommandModes] = useState(null as null | string[]);
|
||||
const [activeCommandMode, setActiveCommandMode] = useState(null as null | string);
|
||||
|
||||
useEffect(() => {
|
||||
/* Set the profile name */
|
||||
if (profileName !== "") getApp().setProfile(profileName);
|
||||
}, [profileName]);
|
||||
AppStateChangedEvent.on((state, subState) => {
|
||||
setSubState(subState);
|
||||
});
|
||||
}, []);
|
||||
|
||||
function checkPassword(password: string) {
|
||||
const usernameCallback = useCallback(() => getApp()?.getServerManager().setUsername(username), [username]);
|
||||
useEffect(usernameCallback, [username]);
|
||||
|
||||
const passwordCallback = useCallback(() => getApp()?.getServerManager().setPassword(hash.update(password).hex()), [password]);
|
||||
useEffect(passwordCallback, [password]);
|
||||
|
||||
const login = useCallback(() => {
|
||||
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 {
|
||||
const commandModes = getApp().getMissionManager().getEnabledCommandModes();
|
||||
if (commandModes.length > 1) {
|
||||
setCommandModes(commandModes);
|
||||
setActiveCommandMode(commandModes[0]);
|
||||
} else if (commandModes.length == 1) {
|
||||
setActiveCommandMode(commandModes[0]);
|
||||
getApp().setState(OlympusState.LOGIN, LoginSubState.CONNECT);
|
||||
} else {
|
||||
setLoginError(true);
|
||||
}
|
||||
setCheckingPassword(false);
|
||||
@ -42,34 +58,44 @@ export function LoginModal(props: { open: boolean }) {
|
||||
setCheckingPassword(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [commandModes, username, password]);
|
||||
|
||||
function connect() {
|
||||
getApp().getServerManager().setUsername(profileName);
|
||||
getApp().getServerManager().startUpdate();
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
const connect = useCallback(() => {
|
||||
if (activeCommandMode) {
|
||||
getApp().getServerManager().setActiveCommandMode(activeCommandMode);
|
||||
getApp().getServerManager().startUpdate();
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
|
||||
/* 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();
|
||||
}
|
||||
/* 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();
|
||||
}
|
||||
}, [activeCommandMode]);
|
||||
|
||||
const subStateCallback = useCallback(() => {
|
||||
if (subState === LoginSubState.COMMAND_MODE) {
|
||||
login();
|
||||
} else if (subState === LoginSubState.CONNECT) {
|
||||
connect();
|
||||
}
|
||||
}, [subState, activeCommandMode, commandModes, username, password]);
|
||||
useEffect(subStateCallback, [subState]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={props.open}
|
||||
className={`
|
||||
inline-flex h-[75%] max-h-[570px] w-[80%] max-w-[1100px] overflow-y-auto
|
||||
inline-flex h-[75%] max-h-[600px] w-[80%] max-w-[1100px] overflow-y-auto
|
||||
scroll-smooth bg-white
|
||||
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
|
||||
`}
|
||||
>
|
||||
<img
|
||||
src="/vite/images/splash/1.jpg"
|
||||
className={`contents-center w-full object-cover opacity-[7%]`}
|
||||
></img>
|
||||
<img src="/vite/images/splash/1.jpg" className={`
|
||||
contents-center w-full object-cover opacity-[7%]
|
||||
`}></img>
|
||||
<div
|
||||
className={`
|
||||
absolute h-full w-full bg-gradient-to-r from-blue-200/25
|
||||
@ -127,10 +153,9 @@ export function LoginModal(props: { open: boolean }) {
|
||||
`}
|
||||
>
|
||||
<span className="size-[80px] min-w-14">
|
||||
<img
|
||||
src="..\vite\images\olympus-500x500.png"
|
||||
className={`flex w-full`}
|
||||
></img>
|
||||
<img src="..\vite\images\olympus-500x500.png" className={`
|
||||
flex w-full
|
||||
`}></img>
|
||||
</span>
|
||||
<div className={`flex flex-col items-start gap-1`}>
|
||||
<h1
|
||||
@ -155,9 +180,36 @@ export function LoginModal(props: { open: boolean }) {
|
||||
</div>
|
||||
{!loginError ? (
|
||||
<>
|
||||
{commandMode === null ? (
|
||||
{subState === LoginSubState.CREDENTIALS && (
|
||||
<>
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<label
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
onChange={(ev) => setUsername(ev.currentTarget.value)}
|
||||
className={`
|
||||
block w-full max-w-80 rounded-lg border
|
||||
border-gray-300 bg-gray-50 p-2.5 text-sm
|
||||
text-gray-900
|
||||
dark:border-gray-600 dark:bg-gray-700
|
||||
dark:text-white dark:placeholder-gray-400
|
||||
dark:focus:border-blue-500
|
||||
dark:focus:ring-blue-500
|
||||
focus:border-blue-500 focus:ring-blue-500
|
||||
`}
|
||||
placeholder="Enter display name"
|
||||
value={username}
|
||||
required
|
||||
/>
|
||||
|
||||
<label
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
@ -186,7 +238,7 @@ export function LoginModal(props: { open: boolean }) {
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => checkPassword(password)}
|
||||
onClick={() => getApp().setState(OlympusState.LOGIN, LoginSubState.COMMAND_MODE)}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2
|
||||
rounded-sm bg-blue-700 px-5 py-2.5 text-sm
|
||||
@ -208,7 +260,8 @@ export function LoginModal(props: { open: boolean }) {
|
||||
*/}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
)}
|
||||
{subState === LoginSubState.COMMAND_MODE && (
|
||||
<>
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<label
|
||||
@ -217,32 +270,20 @@ export function LoginModal(props: { open: boolean }) {
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Set profile name
|
||||
Choose your role
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
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
|
||||
text-gray-900
|
||||
dark:border-gray-600 dark:bg-gray-700
|
||||
dark:text-white dark:placeholder-gray-400
|
||||
dark:focus:border-blue-500
|
||||
dark:focus:ring-blue-500
|
||||
focus:border-blue-500 focus:ring-blue-500
|
||||
`}
|
||||
placeholder="Enter display name"
|
||||
value={profileName}
|
||||
required
|
||||
/>
|
||||
<OlDropdown label={activeCommandMode ?? ""} className={`
|
||||
w-48
|
||||
`}>
|
||||
{commandModes?.map((commandMode) => {
|
||||
return <OlDropdownItem onClick={() => setActiveCommandMode(commandMode)}>{commandMode}</OlDropdownItem>;
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">The profile name you choose determines the saved key binds, groups and options you see.</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => connect()}
|
||||
onClick={() => getApp().setState(OlympusState.LOGIN, LoginSubState.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
|
||||
@ -257,22 +298,6 @@ export function LoginModal(props: { open: boolean }) {
|
||||
Continue
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -1,445 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { OlUnitSummary } from "../components/olunitsummary";
|
||||
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
|
||||
import { OlNumberInput } from "../components/olnumberinput";
|
||||
import { OlLabelToggle } from "../components/ollabeltoggle";
|
||||
import { OlRangeSlider } from "../components/olrangeslider";
|
||||
import { OlDropdownItem, OlDropdown } from "../components/oldropdown";
|
||||
import { LoadoutBlueprint, SpawnRequestTable, UnitBlueprint } from "../../interfaces";
|
||||
import { OlStateButton } from "../components/olstatebutton";
|
||||
import { Coalition } from "../../types/types";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { ftToM, hash } from "../../other/utils";
|
||||
import { LatLng } from "leaflet";
|
||||
import { Airbase } from "../../mission/airbase";
|
||||
import { altitudeIncrements, groupUnitCount, maxAltitudeValues, minAltitudeValues, OlympusState, SpawnSubState } from "../../constants/constants";
|
||||
import { faArrowLeft, faStar } from "@fortawesome/free-solid-svg-icons";
|
||||
import { OlStringInput } from "../components/olstringinput";
|
||||
import { countryCodes } from "../data/codes";
|
||||
import { OlAccordion } from "../components/olaccordion";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export function CompactUnitSpawnMenu(props: {
|
||||
starredSpawns: { [key: string]: SpawnRequestTable };
|
||||
blueprint: UnitBlueprint;
|
||||
onBack: () => void;
|
||||
latlng?: LatLng | null;
|
||||
airbase?: Airbase | null;
|
||||
coalition?: Coalition;
|
||||
}) {
|
||||
/* Compute the min and max values depending on the unit type */
|
||||
const minNumber = 1;
|
||||
const maxNumber = groupUnitCount[props.blueprint.category];
|
||||
const minAltitude = minAltitudeValues[props.blueprint.category];
|
||||
const maxAltitude = maxAltitudeValues[props.blueprint.category];
|
||||
const altitudeStep = altitudeIncrements[props.blueprint.category];
|
||||
|
||||
/* State initialization */
|
||||
const [spawnCoalition, setSpawnCoalition] = useState("blue" as Coalition);
|
||||
const [spawnNumber, setSpawnNumber] = useState(1);
|
||||
const [spawnRole, setSpawnRole] = useState("");
|
||||
const [spawnLoadoutName, setSpawnLoadout] = useState("");
|
||||
const [spawnAltitude, setSpawnAltitude] = useState((maxAltitude - minAltitude) / 2);
|
||||
const [spawnAltitudeType, setSpawnAltitudeType] = useState(false);
|
||||
const [spawnLiveryID, setSpawnLiveryID] = useState("");
|
||||
const [spawnSkill, setSpawnSkill] = useState("High");
|
||||
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
const [showLoadout, setShowLoadout] = useState(false);
|
||||
const [showUnitSummary, setShowUnitSummary] = useState(false);
|
||||
|
||||
const [quickAccessName, setQuickAccessName] = useState("No name");
|
||||
const [key, setKey] = useState("");
|
||||
const [spawnRequestTable, setSpawnRequestTable] = useState(null as null | SpawnRequestTable);
|
||||
|
||||
/* When the menu is opened show the unit preview on the map as a cursor */
|
||||
useEffect(() => {
|
||||
if (!props.airbase && !props.latlng && spawnRequestTable) {
|
||||
/* Refresh the unique key identified */
|
||||
const newKey = hash(JSON.stringify(spawnRequestTable));
|
||||
setKey(newKey);
|
||||
|
||||
getApp()?.getMap()?.setSpawnRequestTable(spawnRequestTable);
|
||||
getApp().setState(OlympusState.SPAWN, SpawnSubState.SPAWN_UNIT);
|
||||
}
|
||||
}, [spawnRequestTable]);
|
||||
|
||||
/* Callback and effect to update the quick access name of the starredSpawn */
|
||||
const updateStarredSpawnQuickAccessNameS = useCallback(() => {
|
||||
if (key in props.starredSpawns) props.starredSpawns[key].quickAccessName = quickAccessName;
|
||||
}, [props.starredSpawns, key, quickAccessName]);
|
||||
useEffect(updateStarredSpawnQuickAccessNameS, [quickAccessName]);
|
||||
|
||||
/* Callback and effect to update the quick access name in the input field */
|
||||
const updateQuickAccessName = useCallback(() => {
|
||||
/* If the spawn is starred, set the quick access name */
|
||||
if (key in props.starredSpawns && props.starredSpawns[key].quickAccessName) setQuickAccessName(props.starredSpawns[key].quickAccessName);
|
||||
else setQuickAccessName("No name");
|
||||
}, [props.starredSpawns, key]);
|
||||
useEffect(updateQuickAccessName, [key]);
|
||||
|
||||
/* Callback and effect to update the spawn request table */
|
||||
const updateSpawnRequestTable = useCallback(() => {
|
||||
if (props.blueprint !== null) {
|
||||
setSpawnRequestTable({
|
||||
category: props.blueprint.category,
|
||||
unit: {
|
||||
unitType: props.blueprint.name,
|
||||
location: props.latlng ?? new LatLng(0, 0), // This will be filled when the user clicks on the map to spawn the unit
|
||||
skill: spawnSkill,
|
||||
liveryID: spawnLiveryID,
|
||||
altitude: ftToM(spawnAltitude),
|
||||
loadout: props.blueprint.loadouts?.find((loadout) => loadout.name === spawnLoadoutName)?.code ?? "",
|
||||
},
|
||||
amount: spawnNumber,
|
||||
coalition: spawnCoalition,
|
||||
});
|
||||
}
|
||||
}, [props.blueprint, spawnAltitude, spawnLoadoutName, spawnCoalition, spawnNumber, spawnLiveryID, spawnSkill]);
|
||||
useEffect(updateSpawnRequestTable, [props.blueprint, spawnAltitude, spawnLoadoutName, spawnCoalition, spawnNumber, spawnLiveryID, spawnSkill]);
|
||||
|
||||
/* Effect to update the coalition if it is force externally */
|
||||
useEffect(() => {
|
||||
if (props.coalition) setSpawnCoalition(props.coalition);
|
||||
}, [props.coalition]);
|
||||
|
||||
/* Get a list of all the roles */
|
||||
const roles: string[] = [];
|
||||
(props.blueprint as UnitBlueprint).loadouts?.forEach((loadout) => {
|
||||
loadout.roles.forEach((role) => {
|
||||
!roles.includes(role) && roles.push(role);
|
||||
});
|
||||
});
|
||||
|
||||
/* Initialize the role */
|
||||
spawnRole === "" && roles.length > 0 && setSpawnRole(roles[0]);
|
||||
|
||||
/* Get a list of all the loadouts */
|
||||
const loadouts: LoadoutBlueprint[] = [];
|
||||
(props.blueprint as UnitBlueprint).loadouts?.forEach((loadout) => {
|
||||
loadout.roles.includes(spawnRole) && loadouts.push(loadout);
|
||||
});
|
||||
|
||||
/* Initialize the loadout */
|
||||
spawnLoadoutName === "" && loadouts.length > 0 && setSpawnLoadout(loadouts[0].name);
|
||||
const spawnLoadout = props.blueprint.loadouts?.find((loadout) => {
|
||||
return loadout.name === spawnLoadoutName;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex h-fit flex-col gap-3">
|
||||
<div className="flex">
|
||||
<FontAwesomeIcon
|
||||
onClick={props.onBack}
|
||||
icon={faArrowLeft}
|
||||
className={`
|
||||
my-auto mr-1 h-4 cursor-pointer rounded-md p-2
|
||||
dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white
|
||||
`}
|
||||
/>
|
||||
<h5 className="my-auto text-gray-200">{props.blueprint.label}</h5>
|
||||
<OlNumberInput
|
||||
className={"ml-auto"}
|
||||
value={spawnNumber}
|
||||
min={minNumber}
|
||||
max={maxNumber}
|
||||
onDecrease={() => {
|
||||
setSpawnNumber(Math.max(minNumber, spawnNumber - 1));
|
||||
}}
|
||||
onIncrease={() => {
|
||||
setSpawnNumber(Math.min(maxNumber, spawnNumber + 1));
|
||||
}}
|
||||
onChange={(ev) => {
|
||||
!isNaN(Number(ev.target.value)) && setSpawnNumber(Math.max(minNumber, Math.min(maxNumber, Number(ev.target.value))));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
inline-flex w-full flex-row content-center justify-between gap-2
|
||||
`}
|
||||
>
|
||||
<div className="my-auto text-sm text-white">Quick access: </div>
|
||||
<OlStringInput
|
||||
onChange={(e) => {
|
||||
setQuickAccessName(e.target.value);
|
||||
}}
|
||||
value={quickAccessName}
|
||||
/>
|
||||
<OlStateButton
|
||||
onClick={() => {
|
||||
if (spawnRequestTable)
|
||||
key in props.starredSpawns
|
||||
? getApp().getMap().removeStarredSpawnRequestTable(key)
|
||||
: getApp().getMap().addStarredSpawnRequestTable(key, spawnRequestTable);
|
||||
}}
|
||||
tooltip="Save this spawn for quick access"
|
||||
checked={key in props.starredSpawns}
|
||||
icon={faStar}
|
||||
></OlStateButton>
|
||||
</div>
|
||||
{["aircraft", "helicopter"].includes(props.blueprint.category) && (
|
||||
<>
|
||||
{!props.airbase && (
|
||||
<div>
|
||||
<div
|
||||
className={`
|
||||
flex flex-row content-center items-center justify-between
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Altitude
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
font-bold
|
||||
dark:text-blue-500
|
||||
`}
|
||||
>{`${Intl.NumberFormat("en-US").format(spawnAltitude)} FT`}</span>
|
||||
</div>
|
||||
<OlLabelToggle toggled={spawnAltitudeType} leftLabel={"AGL"} rightLabel={"ASL"} onClick={() => setSpawnAltitudeType(!spawnAltitudeType)} />
|
||||
</div>
|
||||
<OlRangeSlider
|
||||
onChange={(ev) => setSpawnAltitude(Number(ev.target.value))}
|
||||
value={spawnAltitude}
|
||||
min={minAltitude}
|
||||
max={maxAltitude}
|
||||
step={altitudeStep}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex content-center justify-between gap-2">
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Role
|
||||
</span>
|
||||
<OlDropdown label={spawnRole} className="w-64">
|
||||
{roles.map((role) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
onClick={() => {
|
||||
setSpawnRole(role);
|
||||
setSpawnLoadout("");
|
||||
}}
|
||||
className={`w-full`}
|
||||
>
|
||||
{role}
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
<div className="flex content-center justify-between gap-2">
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Weapons
|
||||
</span>
|
||||
<OlDropdown label={spawnLoadoutName} className={`w-64`}>
|
||||
{loadouts.map((loadout) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
onClick={() => {
|
||||
setSpawnLoadout(loadout.name);
|
||||
}}
|
||||
className={`w-full`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
w-full overflow-hidden text-ellipsis text-nowrap
|
||||
text-left w-max-full
|
||||
`}
|
||||
>
|
||||
{loadout.name}
|
||||
</span>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<OlAccordion
|
||||
onClick={() => {
|
||||
setShowAdvancedOptions(!showAdvancedOptions);
|
||||
}}
|
||||
open={showAdvancedOptions}
|
||||
title="Advanced options"
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex content-center justify-between gap-2">
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Livery
|
||||
</span>
|
||||
<OlDropdown
|
||||
label={props.blueprint.liveries ? (props.blueprint.liveries[spawnLiveryID]?.name ?? "Default") : "No livery"}
|
||||
className={`w-64`}
|
||||
>
|
||||
{props.blueprint.liveries &&
|
||||
Object.keys(props.blueprint.liveries)
|
||||
.sort((ida, idb) => {
|
||||
if (props.blueprint.liveries) {
|
||||
if (props.blueprint.liveries[ida].countries.length > 1) return 1;
|
||||
return props.blueprint.liveries[ida].countries[0] > props.blueprint.liveries[idb].countries[0] ? 1 : -1;
|
||||
} else return -1;
|
||||
})
|
||||
.map((id) => {
|
||||
let country = Object.values(countryCodes).find((countryCode) => {
|
||||
if (props.blueprint.liveries && countryCode.liveryCodes?.includes(props.blueprint.liveries[id].countries[0])) return true;
|
||||
});
|
||||
return (
|
||||
<OlDropdownItem
|
||||
onClick={() => {
|
||||
setSpawnLiveryID(id);
|
||||
}}
|
||||
className={`w-full`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
w-full content-center overflow-hidden
|
||||
text-ellipsis text-nowrap text-left w-max-full
|
||||
flex gap-2
|
||||
`}
|
||||
>
|
||||
{props.blueprint.liveries && props.blueprint.liveries[id].countries.length == 1 && (
|
||||
<img src={`images/countries/${country?.flagCode.toLowerCase()}.svg`} className={`
|
||||
h-6
|
||||
`} />
|
||||
)}
|
||||
|
||||
<div className="my-auto truncate">
|
||||
<span
|
||||
className={`
|
||||
w-full overflow-hidden text-left w-max-full
|
||||
`}
|
||||
>
|
||||
{props.blueprint.liveries ? props.blueprint.liveries[id].name : ""}
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
<div className="flex content-center justify-between gap-2">
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Skill
|
||||
</span>
|
||||
<OlDropdown label={spawnSkill} className={`w-64`}>
|
||||
{["Average", "Good", "High", "Excellent"].map((skill) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
onClick={() => {
|
||||
setSpawnSkill(skill);
|
||||
}}
|
||||
className={`w-full`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
w-full content-center overflow-hidden text-ellipsis
|
||||
text-nowrap text-left w-max-full flex gap-2
|
||||
`}
|
||||
>
|
||||
<div className="my-auto">{skill}</div>
|
||||
</span>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</OlAccordion>
|
||||
</div>
|
||||
<OlAccordion
|
||||
onClick={() => {
|
||||
setShowUnitSummary(!showUnitSummary);
|
||||
}}
|
||||
open={showUnitSummary}
|
||||
title="Unit summary"
|
||||
>
|
||||
<OlUnitSummary blueprint={props.blueprint} coalition={spawnCoalition} />
|
||||
</OlAccordion>
|
||||
{spawnLoadout && spawnLoadout.items.length > 0 && (
|
||||
<OlAccordion
|
||||
onClick={() => {
|
||||
setShowLoadout(!showLoadout);
|
||||
}}
|
||||
open={showLoadout}
|
||||
title="Loadout"
|
||||
>
|
||||
{spawnLoadout.items.map((item) => {
|
||||
return (
|
||||
<div className="flex content-center gap-2">
|
||||
<div
|
||||
className={`
|
||||
my-auto w-6 min-w-6 rounded-full py-0.5 text-center text-sm
|
||||
font-bold text-gray-500
|
||||
dark:bg-[#17212D]
|
||||
`}
|
||||
>
|
||||
{item.quantity}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
my-auto overflow-hidden text-ellipsis text-nowrap text-sm
|
||||
dark:text-gray-300
|
||||
`}
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</OlAccordion>
|
||||
)}
|
||||
{(props.latlng || props.airbase) && (
|
||||
<button
|
||||
type="button"
|
||||
data-coalition={props.coalition ?? "blue"}
|
||||
className={`
|
||||
m-2 rounded-lg px-5 py-2.5 text-sm font-medium text-white
|
||||
data-[coalition='blue']:bg-blue-600
|
||||
data-[coalition='neutral']:bg-gray-400
|
||||
data-[coalition='red']:bg-red-500
|
||||
focus:outline-none focus:ring-4
|
||||
`}
|
||||
onClick={() => {
|
||||
if (spawnRequestTable)
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.spawnUnits(spawnRequestTable.category, Array(spawnRequestTable.amount).fill(spawnRequestTable.unit), spawnRequestTable.coalition, false, props.airbase?.getName() ?? undefined);
|
||||
|
||||
getApp().setState(OlympusState.IDLE)
|
||||
}}
|
||||
>
|
||||
Spawn
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -166,7 +166,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
return (
|
||||
<OlUnitListEntry
|
||||
key={blueprint.name}
|
||||
icon={olButtonsVisibilityAircraft}
|
||||
silhouette={blueprint.filename}
|
||||
blueprint={blueprint}
|
||||
onClick={() => setBlueprint(blueprint)}
|
||||
showCost={showCost}
|
||||
@ -433,13 +433,14 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!(blueprint === null) && (
|
||||
<UnitSpawnMenu
|
||||
blueprint={blueprint}
|
||||
starredSpawns={starredSpawns}
|
||||
coalition={commandModeOptions.commandMode !== GAME_MASTER ? (commandModeOptions.commandMode === BLUE_COMMANDER ? "blue" : "red") : undefined}
|
||||
/>
|
||||
)}
|
||||
<UnitSpawnMenu
|
||||
visible={blueprint !== null}
|
||||
compact={false}
|
||||
blueprint={blueprint}
|
||||
starredSpawns={starredSpawns}
|
||||
coalition={commandModeOptions.commandMode !== GAME_MASTER ? (commandModeOptions.commandMode === BLUE_COMMANDER ? "blue" : "red") : undefined}
|
||||
/>
|
||||
|
||||
{!(effect === null) && <EffectSpawnMenu effect={effect} />}
|
||||
</>
|
||||
</Menu>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -6,10 +6,39 @@ import fs = require("fs");
|
||||
import bodyParser = require("body-parser");
|
||||
import cors = require("cors");
|
||||
import { AudioBackend } from "./audio/audiobackend";
|
||||
import expressBasicAuth from "express-basic-auth";
|
||||
|
||||
/* Load the proxy middleware plugin */
|
||||
import httpProxyMiddleware = require("http-proxy-middleware");
|
||||
|
||||
function checkCustomHeaders(config, usersConfig, groupsConfig, req) {
|
||||
let user = req.auth?.user ?? null;
|
||||
let group = null;
|
||||
|
||||
/* Check if custom authorization headers are enabled */
|
||||
if (
|
||||
"customAuthHeaders" in config["frontend"] &&
|
||||
config["frontend"]["customAuthHeaders"]["enabled"]
|
||||
) {
|
||||
/* If so, check that the custom headers are indeed present */
|
||||
if (
|
||||
(config["frontend"]["customAuthHeaders"]["username"]).toLowerCase() in req.headers &&
|
||||
(config["frontend"]["customAuthHeaders"]["group"]).toLowerCase() in req.headers
|
||||
) {
|
||||
/* If they are, assign the group */
|
||||
group = req.headers[(config["frontend"]["customAuthHeaders"]["group"]).toLowerCase()];
|
||||
|
||||
/* Check that the user is in an existing group */
|
||||
if (group in groupsConfig) {
|
||||
user = req.headers[(config["frontend"]["customAuthHeaders"]["username"]).toLowerCase()];
|
||||
usersConfig[user] = { password: null, roles: groupsConfig[group] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
module.exports = function (configLocation, viteProxy) {
|
||||
/* Config specific routers */
|
||||
const elevationRouter = require("./routes/api/elevation")(configLocation);
|
||||
@ -29,6 +58,30 @@ module.exports = function (configLocation, viteProxy) {
|
||||
);
|
||||
const speechRouter = require("./routes/api/speech")();
|
||||
|
||||
/* Read the users configuration file */
|
||||
let usersConfig = {};
|
||||
if (
|
||||
fs.existsSync(path.join(path.dirname(configLocation), "olympusUsers.json"))
|
||||
) {
|
||||
let rawdata = fs.readFileSync(
|
||||
path.join(path.dirname(configLocation), "olympusUsers.json"),
|
||||
{ encoding: "utf-8" }
|
||||
);
|
||||
usersConfig = JSON.parse(rawdata);
|
||||
}
|
||||
|
||||
/* Read the groups configuration file */
|
||||
let groupsConfig = {};
|
||||
if (
|
||||
fs.existsSync(path.join(path.dirname(configLocation), "olympusGroups.json"))
|
||||
) {
|
||||
let rawdata = fs.readFileSync(
|
||||
path.join(path.dirname(configLocation), "olympusGroups.json"),
|
||||
{ encoding: "utf-8" }
|
||||
);
|
||||
groupsConfig = JSON.parse(rawdata);
|
||||
}
|
||||
|
||||
/* Load the config and create the express app */
|
||||
let config = {};
|
||||
console.log(`Loading configuration file from ${configLocation}`);
|
||||
@ -46,8 +99,94 @@ module.exports = function (configLocation, viteProxy) {
|
||||
/* Start the express app */
|
||||
const app = express();
|
||||
|
||||
/* Define the authentication */
|
||||
const defaultUsers = {
|
||||
"Game master": config["authentication"]["gameMasterPassword"],
|
||||
"Blue commander": config["authentication"]["blueCommanderPassword"],
|
||||
"Red commander": config["authentication"]["redCommanderPassword"],
|
||||
};
|
||||
let users = {};
|
||||
Object.keys(usersConfig).forEach(
|
||||
(user) => (users[user] = usersConfig[user].password)
|
||||
);
|
||||
const auth = expressBasicAuth({
|
||||
users: { ...defaultUsers, ...users },
|
||||
});
|
||||
|
||||
/* Define middleware */
|
||||
app.use(logger("dev"));
|
||||
|
||||
/* Authorization middleware */
|
||||
if ("customAuthHeaders" in config["frontend"] && config["frontend"]["customAuthHeaders"]["enabled"]) {
|
||||
/* Custom authorization will be used */
|
||||
app.use("/", async (req, res, next) => {
|
||||
const user = checkCustomHeaders(config, usersConfig, groupsConfig, req);
|
||||
|
||||
if (user) {
|
||||
/* If the user is preauthorized, set the authorization headers to the response */
|
||||
res.set(config["frontend"]["customAuthHeaders"]["username"], req.headers[(config["frontend"]["customAuthHeaders"]["username"]).toLowerCase()])
|
||||
res.set(config["frontend"]["customAuthHeaders"]["group"], req.headers[(config["frontend"]["customAuthHeaders"]["group"]).toLowerCase()])
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
} else {
|
||||
/* Simple internal authorization will be used */
|
||||
app.use("/olympus", auth);
|
||||
}
|
||||
|
||||
/* Define the middleware to replace the authorization header for the olympus backend */
|
||||
app.use("/olympus", async (req, res, next) => {
|
||||
/* Check if custom authorization headers are being used */
|
||||
const user = req.auth?.user ?? checkCustomHeaders(config, usersConfig, groupsConfig, req);
|
||||
|
||||
/* If either simple authentication or custom authentication has succeded */
|
||||
if (user) {
|
||||
const userConfig = usersConfig[user];
|
||||
|
||||
/* Check that the user is authorized to at least one role */
|
||||
if (userConfig.roles.length > 0) {
|
||||
/* If a specific command role is requested, proceed with that role */
|
||||
if (req.headers["x-command-mode"]) {
|
||||
/* Check that the user is authorized to that role */
|
||||
if (userConfig.roles.includes(req.headers["x-command-mode"]) ) {
|
||||
/* Check that the role is valid */
|
||||
if (req.headers["x-command-mode"] in defaultUsers) {
|
||||
/* Apply the authorization headers */
|
||||
req.headers.authorization = `Basic ${btoa(
|
||||
user + ":" + defaultUsers[req.headers["x-command-mode"]]
|
||||
)}`;
|
||||
} else {
|
||||
res.sendStatus(401); // Unauthorized
|
||||
}
|
||||
} else {
|
||||
res.sendStatus(401); // Unauthorized
|
||||
}
|
||||
} else {
|
||||
/* No role has been specified, continue with the highest role */
|
||||
/* Check that the role is valid */
|
||||
if (userConfig.roles[0] in defaultUsers) {
|
||||
/* Apply the authorization headers */
|
||||
req.headers.authorization = `Basic ${btoa(
|
||||
user + ":" + defaultUsers[userConfig.roles[0]]
|
||||
)}`;
|
||||
} else {
|
||||
res.sendStatus(401); // Unauthorized
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res.sendStatus(401); // Unauthorized
|
||||
}
|
||||
|
||||
/* Send back the roles that the user is enabled to */
|
||||
res.set('X-Enabled-Command-Modes', `${userConfig.roles}`);
|
||||
next();
|
||||
} else {
|
||||
res.sendStatus(401); // Unauthorized
|
||||
}
|
||||
});
|
||||
|
||||
/* Proxy middleware */
|
||||
app.use(
|
||||
"/olympus",
|
||||
httpProxyMiddleware.createProxyMiddleware({
|
||||
@ -63,7 +202,7 @@ module.exports = function (configLocation, viteProxy) {
|
||||
"/vite",
|
||||
httpProxyMiddleware.createProxyMiddleware({
|
||||
target: `http://localhost:8080/`,
|
||||
ws: true
|
||||
ws: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -92,7 +231,10 @@ module.exports = function (configLocation, viteProxy) {
|
||||
}
|
||||
|
||||
if (config["audio"]) {
|
||||
let audioBackend = new AudioBackend(config["audio"]["SRSPort"], config["audio"]["WSPort"]);
|
||||
let audioBackend = new AudioBackend(
|
||||
config["audio"]["SRSPort"],
|
||||
config["audio"]["WSPort"]
|
||||
);
|
||||
audioBackend.start();
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,26 @@
|
||||
import express = require("express");
|
||||
import fs = require("fs");
|
||||
import path = require("path");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
let sessionHash = "";
|
||||
let sessionData = {}
|
||||
let sessionData = {};
|
||||
|
||||
module.exports = function (configLocation) {
|
||||
router.get("/config", function (req, res, next) {
|
||||
let profiles = null;
|
||||
if (
|
||||
fs.existsSync(
|
||||
path.join(path.dirname(configLocation), "olympusProfiles.json")
|
||||
)
|
||||
) {
|
||||
let rawdata = fs.readFileSync(
|
||||
path.join(path.dirname(configLocation), "olympusProfiles.json"),
|
||||
"utf-8"
|
||||
);
|
||||
profiles = JSON.parse(rawdata);
|
||||
}
|
||||
if (fs.existsSync(configLocation)) {
|
||||
let rawdata = fs.readFileSync(configLocation, "utf-8");
|
||||
const config = JSON.parse(rawdata);
|
||||
@ -14,7 +28,7 @@ module.exports = function (configLocation) {
|
||||
JSON.stringify({
|
||||
frontend: { ...config.frontend },
|
||||
audio: { ...(config.audio ?? {}) },
|
||||
profiles: { ...(config.profiles ?? {}) },
|
||||
profiles: { ...(profiles ?? {}) },
|
||||
})
|
||||
);
|
||||
res.end();
|
||||
@ -24,14 +38,34 @@ module.exports = function (configLocation) {
|
||||
});
|
||||
|
||||
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;
|
||||
/* Create empty profiles file*/
|
||||
if (
|
||||
!fs.existsSync(
|
||||
path.join(path.dirname(configLocation), "olympusProfiles.json")
|
||||
)
|
||||
) {
|
||||
fs.writeFileSync(
|
||||
configLocation,
|
||||
JSON.stringify(config, null, 2),
|
||||
path.join(path.dirname(configLocation), "olympusProfiles.json"),
|
||||
"{}",
|
||||
"utf-8"
|
||||
);
|
||||
}
|
||||
|
||||
/* Check that the previous operation was successfull */
|
||||
if (
|
||||
fs.existsSync(
|
||||
path.join(path.dirname(configLocation), "olympusProfiles.json")
|
||||
)
|
||||
) {
|
||||
let rawdata = fs.readFileSync(
|
||||
path.join(path.dirname(configLocation), "olympusProfiles.json"),
|
||||
"utf-8"
|
||||
);
|
||||
const usersProfiles = JSON.parse(rawdata);
|
||||
usersProfiles[req.params.profileName] = req.body;
|
||||
fs.writeFileSync(
|
||||
path.join(path.dirname(configLocation), "olympusProfiles.json"),
|
||||
JSON.stringify(usersProfiles, null, 2),
|
||||
"utf-8"
|
||||
);
|
||||
res.end();
|
||||
@ -41,14 +75,21 @@ module.exports = function (configLocation) {
|
||||
});
|
||||
|
||||
router.put("/profile/reset/:profileName", function (req, res, next) {
|
||||
if (fs.existsSync(configLocation)) {
|
||||
let rawdata = fs.readFileSync(configLocation, "utf-8");
|
||||
const config = JSON.parse(rawdata);
|
||||
if (config.profiles[req.params.profileName])
|
||||
delete config.profiles[req.params.profileName];
|
||||
if (
|
||||
fs.existsSync(
|
||||
path.join(path.dirname(configLocation), "olympusProfiles.json")
|
||||
)
|
||||
) {
|
||||
let rawdata = fs.readFileSync(
|
||||
path.join(path.dirname(configLocation), "olympusProfiles.json"),
|
||||
"utf-8"
|
||||
);
|
||||
const usersProfiles = JSON.parse(rawdata);
|
||||
if (req.params.profileName in usersProfiles)
|
||||
delete usersProfiles[req.params.profileName];
|
||||
fs.writeFileSync(
|
||||
configLocation,
|
||||
JSON.stringify(config, null, 2),
|
||||
path.join(path.dirname(configLocation), "olympusProfiles.json"),
|
||||
JSON.stringify(usersProfiles, null, 2),
|
||||
"utf-8"
|
||||
);
|
||||
res.end();
|
||||
@ -58,13 +99,14 @@ module.exports = function (configLocation) {
|
||||
});
|
||||
|
||||
router.put("/profile/delete/all", function (req, res, next) {
|
||||
if (fs.existsSync(configLocation)) {
|
||||
let rawdata = fs.readFileSync(configLocation, "utf-8");
|
||||
const config = JSON.parse(rawdata);
|
||||
config.profiles = {};
|
||||
if (
|
||||
fs.existsSync(
|
||||
path.join(path.dirname(configLocation), "olympusProfiles.json")
|
||||
)
|
||||
) {
|
||||
fs.writeFileSync(
|
||||
configLocation,
|
||||
JSON.stringify(config, null, 2),
|
||||
path.join(path.dirname(configLocation), "olympusProfiles.json"),
|
||||
"{}",
|
||||
"utf-8"
|
||||
);
|
||||
res.end();
|
||||
@ -74,15 +116,19 @@ module.exports = function (configLocation) {
|
||||
});
|
||||
|
||||
router.put("/sessiondata/save/:profileName", function (req, res, next) {
|
||||
if (req.body.sessionHash === undefined || req.body.sessionData === undefined) res.sendStatus(400);
|
||||
if (
|
||||
req.body.sessionHash === undefined ||
|
||||
req.body.sessionData === undefined
|
||||
)
|
||||
res.sendStatus(400);
|
||||
let thisSessionHash = req.body.sessionHash;
|
||||
if (thisSessionHash !== sessionHash) {
|
||||
sessionHash = thisSessionHash;
|
||||
sessionData = {};
|
||||
}
|
||||
sessionData[req.params.profileName] = req.body.sessionData;
|
||||
res.end()
|
||||
})
|
||||
res.end();
|
||||
});
|
||||
|
||||
router.put("/sessiondata/load/:profileName", function (req, res, next) {
|
||||
if (req.body.sessionHash === undefined) res.sendStatus(400);
|
||||
@ -95,7 +141,7 @@ module.exports = function (configLocation) {
|
||||
res.send(sessionData[req.params.profileName]);
|
||||
res.end();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
@ -10,6 +10,11 @@
|
||||
},
|
||||
"frontend": {
|
||||
"port": 3000,
|
||||
"customAuthHeaders": {
|
||||
"enabled": false,
|
||||
"username": "X-Authorized",
|
||||
"group": "X-Group"
|
||||
},
|
||||
"elevationProvider": {
|
||||
"provider": "https://srtm.fasma.org/{lat}{lng}.SRTMGL3S.hgt.zip",
|
||||
"username": null,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user