mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
507 lines
20 KiB
TypeScript
507 lines
20 KiB
TypeScript
import React, { useEffect, useRef, useState } from "react";
|
|
import { OlRoundStateButton, OlStateButton, OlLockStateButton } from "../components/olstatebutton";
|
|
import { faSkull, faCamera, faFlag, faVolumeHigh, faDrawPolygon, faTriangleExclamation, faWifi, faObjectGroup } from "@fortawesome/free-solid-svg-icons";
|
|
import { OlDropdownItem, OlDropdown } from "../components/oldropdown";
|
|
import { OlLabelToggle } from "../components/ollabeltoggle";
|
|
import { getApp, IP, VERSION } from "../../olympusapp";
|
|
import {
|
|
olButtonsVisibilityAirbase,
|
|
olButtonsVisibilityAircraft,
|
|
olButtonsVisibilityDcs,
|
|
olButtonsVisibilityGroundunit,
|
|
olButtonsVisibilityGroundunitSam,
|
|
olButtonsVisibilityHelicopter,
|
|
olButtonsVisibilityHuman,
|
|
olButtonsVisibilityNavyunit,
|
|
olButtonsVisibilityOlympus,
|
|
} from "../components/olicons";
|
|
import { FaChevronLeft, FaChevronRight, FaFloppyDisk } from "react-icons/fa6";
|
|
import {
|
|
CommandModeOptionsChangedEvent,
|
|
ConfigLoadedEvent,
|
|
EnabledCommandModesChangedEvent,
|
|
HiddenTypesChangedEvent,
|
|
MapOptionsChangedEvent,
|
|
MapSourceChangedEvent,
|
|
SessionDataChangedEvent,
|
|
SessionDataSavedEvent,
|
|
} from "../../events";
|
|
import {
|
|
BLUE_COMMANDER,
|
|
COMMAND_MODE_OPTIONS_DEFAULTS,
|
|
GAME_MASTER,
|
|
LoginSubState,
|
|
MAP_HIDDEN_TYPES_DEFAULTS,
|
|
MAP_OPTIONS_DEFAULTS,
|
|
OlympusState,
|
|
RED_COMMANDER,
|
|
} from "../../constants/constants";
|
|
import { OlympusConfig } from "../../interfaces";
|
|
import { FaCheck, FaRedo, FaSpinner } from "react-icons/fa";
|
|
import { OlExpandingTooltip } from "../components/olexpandingtooltip";
|
|
|
|
export function Header() {
|
|
const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS);
|
|
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
|
|
const [mapSource, setMapSource] = useState("");
|
|
const [mapSources, setMapSources] = useState([] as string[]);
|
|
const [scrolledLeft, setScrolledLeft] = useState(true);
|
|
const [scrolledRight, setScrolledRight] = useState(false);
|
|
const [audioEnabled, setAudioEnabled] = useState(false);
|
|
const [commandModeOptions, setCommandModeOptions] = useState(COMMAND_MODE_OPTIONS_DEFAULTS);
|
|
const [savingSessionData, setSavingSessionData] = useState(false);
|
|
const [latestVersion, setLatestVersion] = useState("");
|
|
const [isLatestVersion, setIsLatestVersion] = useState(false);
|
|
const [isBetaVersion, setIsBetaVersion] = useState(false);
|
|
const [isDevVersion, setIsDevVersion] = useState(false);
|
|
const [enabledCommandModes, setEnabledCommandModes] = useState([] as string[]);
|
|
const [loadingNewCommandMode, setLoadingNewCommandMode] = useState(false);
|
|
|
|
useEffect(() => {
|
|
HiddenTypesChangedEvent.on((hiddenTypes) => setMapHiddenTypes({ ...hiddenTypes }));
|
|
MapOptionsChangedEvent.on((mapOptions) => {
|
|
setMapOptions({ ...mapOptions });
|
|
});
|
|
MapSourceChangedEvent.on((source) => setMapSource(source));
|
|
ConfigLoadedEvent.on((config: OlympusConfig) => {
|
|
// Timeout needed to make sure the map configuration has updated
|
|
window.setTimeout(() => {
|
|
var sources = Object.keys(getApp().getMap().getMirrors()).concat(getApp().getMap().getLayers());
|
|
setMapSources(sources);
|
|
}, 200);
|
|
});
|
|
CommandModeOptionsChangedEvent.on((commandModeOptions) => {
|
|
setCommandModeOptions(commandModeOptions);
|
|
setLoadingNewCommandMode(false);
|
|
});
|
|
SessionDataChangedEvent.on(() => setSavingSessionData(true));
|
|
SessionDataSavedEvent.on(() => setSavingSessionData(false));
|
|
EnabledCommandModesChangedEvent.on((enabledCommandModes) => setEnabledCommandModes(enabledCommandModes));
|
|
|
|
/* Check if we are running the latest version */
|
|
const request = new Request("https://raw.githubusercontent.com/Pax1601/DCSOlympus/main/version.json");
|
|
fetch(request)
|
|
.then((response) => {
|
|
if (response.status === 200) {
|
|
return response.json();
|
|
} else {
|
|
throw new Error("Error connecting to Github to retrieve latest version");
|
|
}
|
|
})
|
|
.then((res) => {
|
|
setLatestVersion(res["version"]);
|
|
|
|
if (VERSION === "{{OLYMPUS_VERSION_NUMBER}}") {
|
|
console.log("OLYMPUS_VERSION_NUMBER is not set. Skipping version check.");
|
|
setIsDevVersion(true);
|
|
} else {
|
|
setIsDevVersion(false);
|
|
|
|
/* Check if the new version is newer than the current one */
|
|
/* Extract the version numbers */
|
|
const currentVersion = VERSION.replace("v", "").split(".");
|
|
const newVersion = res["version"].replace("v", "").split(".");
|
|
|
|
setIsBetaVersion(true);
|
|
setIsLatestVersion(true);
|
|
|
|
/* Compare the version numbers */
|
|
for (var i = 0; i < currentVersion.length; i++) {
|
|
if (parseInt(newVersion[i]) > parseInt(currentVersion[i])) {
|
|
setIsLatestVersion(false);
|
|
}
|
|
}
|
|
|
|
/* Check if this is a beta version checking if this version is newer */
|
|
for (var i = 0; i < currentVersion.length; i++) {
|
|
if (parseInt(newVersion[i]) < parseInt(currentVersion[i])) {
|
|
setIsBetaVersion(false);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}, []);
|
|
|
|
/* Initialize the "scroll" position of the element */
|
|
var scrollRef = useRef(null);
|
|
useEffect(() => {
|
|
if (scrollRef.current) onScroll(scrollRef.current);
|
|
});
|
|
|
|
function onScroll(el) {
|
|
const sl = el.scrollLeft;
|
|
const sr = el.scrollWidth - el.scrollLeft - el.clientWidth;
|
|
|
|
sl < 1 && !scrolledLeft && setScrolledLeft(true);
|
|
sl > 1 && scrolledLeft && setScrolledLeft(false);
|
|
|
|
sr < 1 && !scrolledRight && setScrolledRight(true);
|
|
sr > 1 && scrolledRight && setScrolledRight(false);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={`
|
|
relative z-10 flex w-full gap-4 border-gray-200 bg-gray-300 px-3
|
|
align-center
|
|
dark:border-gray-800 dark:bg-olympus-900
|
|
`}
|
|
onWheel={(e) => {
|
|
if (scrollRef.current) {
|
|
if (e.deltaY > 0) (scrollRef.current as HTMLElement).scrollLeft += 100;
|
|
else (scrollRef.current as HTMLElement).scrollLeft -= 100;
|
|
}
|
|
}}
|
|
>
|
|
<img src="images/icon.png" className={`my-auto h-10 w-10 rounded-md p-0`}></img>
|
|
{!scrolledLeft && (
|
|
<FaChevronLeft
|
|
className={`
|
|
absolute left-14 h-full w-6 rounded-lg px-2 py-3.5 text-gray-200
|
|
dark:bg-olympus-900
|
|
`}
|
|
/>
|
|
)}
|
|
<div
|
|
className={`
|
|
my-2 flex w-full items-center gap-3 overflow-x-scroll no-scrollbar
|
|
`}
|
|
onScroll={(ev) => onScroll(ev.target)}
|
|
ref={scrollRef}
|
|
>
|
|
<div
|
|
className={`
|
|
mr-auto hidden flex-none flex-row items-center justify-start gap-2
|
|
lg:flex
|
|
`}
|
|
>
|
|
<div className="mr-2 flex flex-col items-start">
|
|
<div
|
|
className={`
|
|
pt-1 text-xs text-gray-800
|
|
dark:text-gray-400
|
|
`}
|
|
>
|
|
Connected to
|
|
</div>
|
|
<div
|
|
className={`
|
|
flex items-center justify-center gap-2 text-sm font-extrabold
|
|
text-gray-800
|
|
dark:text-gray-200
|
|
`}
|
|
>
|
|
{IP}
|
|
</div>
|
|
</div>
|
|
<div className="w-8">
|
|
{savingSessionData ? (
|
|
<div className="text-white">
|
|
<FaSpinner className={`animate-spin text-2xl`} />
|
|
</div>
|
|
) : (
|
|
<div className={`relative text-white`}>
|
|
<FaFloppyDisk className={`absolute -top-3 text-2xl`} />
|
|
<FaCheck
|
|
className={`
|
|
absolute left-[9px] top-[-6px] text-2xl text-olympus-900
|
|
`}
|
|
/>
|
|
<FaCheck className={`absolute left-3 top-0 text-green-500`} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
{isDevVersion ? (
|
|
<div className={`text-gray-400`}>Development build</div>
|
|
) : (
|
|
<>
|
|
<div>
|
|
{!isLatestVersion && (
|
|
<div className={`animate-pulse text-gray-400`}>
|
|
<span className={`font-bold`}>New version available:</span> {latestVersion}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div>{!isBetaVersion && <div className={`text-gray-400`}>beta version</div>}</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{commandModeOptions.commandMode === GAME_MASTER && (
|
|
<div
|
|
className={`
|
|
flex h-full cursor-pointer rounded-md border-2 border-transparent
|
|
bg-olympus-600 px-4 text-gray-200
|
|
hover:bg-olympus-400
|
|
`}
|
|
onClick={() => {
|
|
if (enabledCommandModes.length > 0) {
|
|
let blueCommandModeIndex = enabledCommandModes.indexOf(BLUE_COMMANDER);
|
|
let redCommandModeIndex = enabledCommandModes.indexOf(RED_COMMANDER);
|
|
if (blueCommandModeIndex >= 0) getApp().getServerManager().setActiveCommandMode(BLUE_COMMANDER);
|
|
else if (redCommandModeIndex >= 0) getApp().getServerManager().setActiveCommandMode(RED_COMMANDER);
|
|
setLoadingNewCommandMode(true);
|
|
}
|
|
}}
|
|
>
|
|
<span className="my-auto font-bold">Game Master</span>
|
|
{enabledCommandModes.length > 0 && (
|
|
<>{loadingNewCommandMode ? <FaSpinner className={`
|
|
my-auto ml-2 animate-spin text-white
|
|
`} /> : <FaRedo className={`my-auto ml-2 text-gray-200`} />}</>
|
|
)}
|
|
</div>
|
|
)}
|
|
{commandModeOptions.commandMode === BLUE_COMMANDER && (
|
|
<div
|
|
className={`
|
|
flex h-full cursor-pointer rounded-md border-2 border-transparent
|
|
bg-blue-600 px-4 text-gray-200
|
|
hover:bg-blue-400
|
|
`}
|
|
onClick={() => {
|
|
if (enabledCommandModes.length > 0) {
|
|
let gameMasterCommandModeIndex = enabledCommandModes.indexOf(GAME_MASTER);
|
|
let redCommandModeIndex = enabledCommandModes.indexOf(RED_COMMANDER);
|
|
if (redCommandModeIndex >= 0) getApp().getServerManager().setActiveCommandMode(RED_COMMANDER);
|
|
else if (gameMasterCommandModeIndex >= 0) getApp().getServerManager().setActiveCommandMode(GAME_MASTER);
|
|
setLoadingNewCommandMode(true);
|
|
}
|
|
}}
|
|
>
|
|
<span className="my-auto font-bold">BLUE Commander</span>
|
|
{enabledCommandModes.length > 0 && (
|
|
<>{loadingNewCommandMode ? <FaSpinner className={`
|
|
my-auto ml-2 animate-spin text-gray-200
|
|
`} /> : <FaRedo className={`my-auto ml-2 text-gray-200`} />}</>
|
|
)}
|
|
</div>
|
|
)}
|
|
{commandModeOptions.commandMode === RED_COMMANDER && (
|
|
<div
|
|
className={`
|
|
flex h-full cursor-pointer rounded-md border-2 border-transparent
|
|
bg-red-600 px-4 text-gray-200
|
|
hover:bg-red-500
|
|
`}
|
|
onClick={() => {
|
|
if (enabledCommandModes.length > 0) {
|
|
let gameMasterCommandModeIndex = enabledCommandModes.indexOf(GAME_MASTER);
|
|
let blueCommandModeIndex = enabledCommandModes.indexOf(BLUE_COMMANDER);
|
|
if (gameMasterCommandModeIndex >= 0) getApp().getServerManager().setActiveCommandMode(GAME_MASTER);
|
|
else if (blueCommandModeIndex >= 0) getApp().getServerManager().setActiveCommandMode(BLUE_COMMANDER);
|
|
setLoadingNewCommandMode(true);
|
|
}
|
|
}}
|
|
>
|
|
<span className="my-auto font-bold">RED Commander</span>
|
|
{enabledCommandModes.length > 0 && (
|
|
<>{loadingNewCommandMode ? <FaSpinner className={`
|
|
my-auto ml-2 animate-spin text-gray-200
|
|
`} /> : <FaRedo className={`my-auto ml-2 text-gray-200`} />}</>
|
|
)}
|
|
</div>
|
|
)}
|
|
<div className={`flex h-fit flex-row items-center justify-start gap-1`}>
|
|
<OlLockStateButton
|
|
checked={!mapOptions.protectDCSUnits}
|
|
onClick={() => {
|
|
getApp().getMap().setOption("protectDCSUnits", !mapOptions.protectDCSUnits);
|
|
}}
|
|
tooltip={() => (
|
|
<OlExpandingTooltip
|
|
title="Lock/unlock protected units"
|
|
content={
|
|
<>
|
|
<p>
|
|
By default, Mission Editor units are protected from being commanded or deleted. This option allows you to unlock them, so they can be
|
|
commanded or deleted like any other unit.{" "}
|
|
</p>
|
|
<p>If units are protected, you will still be able to control them, but a prompt will be shown to require your confirmation. </p>
|
|
<p>Once a unit has been commanded, it will be unlocked and will become an Olympus unit, completely abandoning its previuos mission. </p>
|
|
</>
|
|
}
|
|
/>
|
|
)}
|
|
/>
|
|
<OlRoundStateButton
|
|
checked={audioEnabled}
|
|
onClick={() => {
|
|
audioEnabled ? getApp().getAudioManager().stop() : getApp().getAudioManager().start();
|
|
setAudioEnabled(!audioEnabled);
|
|
}}
|
|
tooltip={() => (
|
|
<OlExpandingTooltip
|
|
title="Enable/disable audio"
|
|
content={
|
|
<>
|
|
<p>If this option is enabled, you will be able to access the radio and audio features of DCS Olympus. </p>
|
|
<p>For this to work, a SRS Server need to be installed and running on the same machine on which the DCS Olympus server is running.</p>
|
|
<p>
|
|
For security reasons, this feature will only work if a secure connection (i.e., using https) is established with the server. It is also
|
|
suggested to use Google Chrome for optimal compatibility.{" "}
|
|
</p>
|
|
</>
|
|
}
|
|
/>
|
|
)}
|
|
icon={faVolumeHigh}
|
|
/>
|
|
</div>
|
|
<div className={`h-8 w-0 border-l-[2px] border-gray-700`}></div>
|
|
<div className={`flex h-fit flex-row items-center justify-start gap-1`}>
|
|
{Object.entries({
|
|
human: olButtonsVisibilityHuman,
|
|
olympus: olButtonsVisibilityOlympus,
|
|
dcs: olButtonsVisibilityDcs,
|
|
}).map((entry) => {
|
|
return (
|
|
<OlRoundStateButton
|
|
key={entry[0]}
|
|
onClick={() => {
|
|
getApp().getMap().setHiddenType(entry[0], !mapHiddenTypes[entry[0]]);
|
|
}}
|
|
checked={!mapHiddenTypes[entry[0]]}
|
|
icon={entry[1]}
|
|
tooltip={"Hide/show " + entry[0] + " units"}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className={`h-8 w-0 border-l-[2px] border-gray-700`}></div>
|
|
<div className={`flex h-fit flex-row items-center justify-start gap-1`}>
|
|
<OlRoundStateButton
|
|
onClick={() => getApp().getMap().setHiddenType("blue", !mapHiddenTypes["blue"])}
|
|
checked={!mapHiddenTypes["blue"]}
|
|
icon={faFlag}
|
|
className={"!text-blue-500"}
|
|
tooltip={"Hide/show blue units"}
|
|
/>
|
|
<OlRoundStateButton
|
|
onClick={() => getApp().getMap().setHiddenType("red", !mapHiddenTypes["red"])}
|
|
checked={!mapHiddenTypes["red"]}
|
|
icon={faFlag}
|
|
className={"!text-red-500"}
|
|
tooltip={"Hide/show red units"}
|
|
/>
|
|
<OlRoundStateButton
|
|
onClick={() => getApp().getMap().setHiddenType("neutral", !mapHiddenTypes["neutral"])}
|
|
checked={!mapHiddenTypes["neutral"]}
|
|
icon={faFlag}
|
|
className={"!text-gray-500"}
|
|
tooltip={"Hide/show neutral units"}
|
|
/>
|
|
</div>
|
|
<div className={`h-8 w-0 border-l-[2px] border-gray-700`}></div>
|
|
<div className={`flex h-fit flex-row items-center justify-start gap-1`}>
|
|
{Object.entries({
|
|
aircraft: olButtonsVisibilityAircraft,
|
|
helicopter: olButtonsVisibilityHelicopter,
|
|
"groundunit-sam": olButtonsVisibilityGroundunitSam,
|
|
groundunit: olButtonsVisibilityGroundunit,
|
|
navyunit: olButtonsVisibilityNavyunit,
|
|
airbase: olButtonsVisibilityAirbase,
|
|
dead: faSkull,
|
|
}).map((entry) => {
|
|
return (
|
|
<OlRoundStateButton
|
|
key={entry[0]}
|
|
onClick={() => {
|
|
getApp().getMap().setHiddenType(entry[0], !mapHiddenTypes[entry[0]]);
|
|
}}
|
|
checked={!mapHiddenTypes[entry[0]]}
|
|
icon={entry[1]}
|
|
tooltip={"Hide/show " + entry[0] + " units"}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className={`h-8 w-0 border-l-[2px] border-gray-700`}></div>
|
|
<div className={`flex h-fit flex-row items-center justify-start gap-1`}>
|
|
<OlRoundStateButton
|
|
icon={faDrawPolygon}
|
|
checked={mapOptions.showMissionDrawings}
|
|
onClick={() => {
|
|
getApp().getMap().setOption("showMissionDrawings", !mapOptions.showMissionDrawings);
|
|
}}
|
|
tooltip={() => (
|
|
<OlExpandingTooltip
|
|
title="Hide/Show mission drawings"
|
|
content="To filter the visibile drawings and change their opacity, use the drawings menu on the left sidebar."
|
|
/>
|
|
)}
|
|
/>
|
|
<OlRoundStateButton
|
|
onClick={() => getApp().getMap().setOption("showUnitsEngagementRings", !mapOptions.showUnitsEngagementRings)}
|
|
checked={mapOptions.showUnitsEngagementRings}
|
|
icon={faTriangleExclamation}
|
|
className={""}
|
|
tooltip={"Hide/show units engagement rings"}
|
|
/>
|
|
<OlRoundStateButton
|
|
onClick={() => getApp().getMap().setOption("showUnitsAcquisitionRings", !mapOptions.showUnitsAcquisitionRings)}
|
|
checked={mapOptions.showUnitsAcquisitionRings}
|
|
icon={faWifi}
|
|
className={""}
|
|
tooltip={"Hide/show units acquisition rings"}
|
|
/>
|
|
<OlRoundStateButton
|
|
onClick={() => getApp().getMap().setOption("clusterGroundUnits", !mapOptions.clusterGroundUnits)}
|
|
checked={mapOptions.clusterGroundUnits}
|
|
icon={faObjectGroup}
|
|
className={""}
|
|
tooltip={"Enable/disable ground unit clustering"}
|
|
/>
|
|
</div>
|
|
|
|
<OlLabelToggle
|
|
toggled={mapOptions.cameraPluginMode === "map"}
|
|
leftLabel={"Live"}
|
|
rightLabel={"Map"}
|
|
onClick={() => {
|
|
getApp()
|
|
.getMap()
|
|
.setOption("cameraPluginMode", mapOptions.cameraPluginMode === "live" ? "map" : "live");
|
|
}}
|
|
tooltip={() => (
|
|
<OlExpandingTooltip
|
|
title="Switch between live and map camera"
|
|
content="When the camera plugin is enabled, you can switch between the live camera view and the map view. These are equivalent to the F9 and F10 views in DCS."
|
|
/>
|
|
)}
|
|
/>
|
|
<OlStateButton
|
|
checked={mapOptions.cameraPluginEnabled}
|
|
icon={faCamera}
|
|
onClick={() => {
|
|
getApp().getMap().setOption("cameraPluginEnabled", !mapOptions.cameraPluginEnabled);
|
|
}}
|
|
tooltip={() => (
|
|
<OlExpandingTooltip
|
|
title="Activate/deactivate camera plugin"
|
|
content="The camera plugin allows to tie the position of the map to the position of the camera in DCS. This is useful to check exactly how things look from the players perspective. Check the in-game wiki for more information." //TODO add link to wiki
|
|
/>
|
|
)}
|
|
/>
|
|
<OlDropdown label={mapSource} className="w-60">
|
|
{mapSources.map((source) => {
|
|
return (
|
|
<OlDropdownItem key={source} onClick={() => getApp().getMap().setLayerName(source)}>
|
|
<div className="truncate">{source}</div>
|
|
</OlDropdownItem>
|
|
);
|
|
})}
|
|
</OlDropdown>
|
|
</div>
|
|
{!scrolledRight && (
|
|
<FaChevronRight
|
|
className={`
|
|
absolute right-0 h-full w-6 rounded-lg px-2 py-3.5 text-gray-200
|
|
dark:bg-olympus-900
|
|
`}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|