feat: started working on wiki entries and tooltips, minor fixes and improvements to presentation
@ -9,6 +9,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.8.1",
|
||||
"chart.js": "^4.4.7",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-circular-progressbar": "^2.1.0",
|
||||
|
||||
@ -22,9 +22,9 @@
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:zoom="12.703125"
|
||||
inkscape:cx="27.316114"
|
||||
inkscape:cy="18.145142"
|
||||
inkscape:zoom="17.964932"
|
||||
inkscape:cx="25.549777"
|
||||
inkscape:cy="24.241673"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="1912"
|
||||
@ -37,4 +37,9 @@
|
||||
id="path1"
|
||||
style="stroke:none;stroke-width:2.30583;stroke-dasharray:none;stroke-opacity:1"
|
||||
sodipodi:nodetypes="csccsscc" />
|
||||
<path
|
||||
d="M 20.752403,8.175778 9.0427604,31.040927 32.000076,30.985263 Z"
|
||||
id="path1-9"
|
||||
style="fill:none;stroke:#272727;stroke-width:3.39495;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.9 KiB |
BIN
frontend/react/public/images/training/starred.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
frontend/react/public/images/training/step1.gif
Normal file
|
After Width: | Height: | Size: 487 KiB |
BIN
frontend/react/public/images/training/step2.gif
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
frontend/react/public/images/training/step3.gif
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/react/public/images/training/step4.gif
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
BIN
frontend/react/public/images/training/step5.gif
Normal file
|
After Width: | Height: | Size: 488 KiB |
BIN
frontend/react/public/images/training/step6.gif
Normal file
|
After Width: | Height: | Size: 409 KiB |
BIN
frontend/react/public/images/training/step7.gif
Normal file
|
After Width: | Height: | Size: 5.2 MiB |
BIN
frontend/react/public/images/training/unitfilter.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/react/public/images/training/unitmarker.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@ -327,7 +327,8 @@ export enum OlympusState {
|
||||
IMPORT_EXPORT = "Import/export",
|
||||
WARNING = "Warning modal",
|
||||
DATABASE_EDITOR = "Database editor",
|
||||
MEASURE = "Measure"
|
||||
MEASURE = "Measure",
|
||||
TRAINING = "Training"
|
||||
}
|
||||
|
||||
export const NO_SUBSTATE = "No substate";
|
||||
|
||||
@ -181,7 +181,7 @@ export class Map extends L.Map {
|
||||
maxBoundsViscosity: 1.0,
|
||||
minZoom: 7,
|
||||
keyboard: true,
|
||||
keyboardPanDelta: 0
|
||||
keyboardPanDelta: 0,
|
||||
});
|
||||
this.setView([37.23, -115.8], 10);
|
||||
|
||||
@ -592,10 +592,20 @@ export class Map extends L.Map {
|
||||
}
|
||||
|
||||
setSpawnRequestTable(spawnRequestTable: SpawnRequestTable) {
|
||||
let redrawMarker = false;
|
||||
if (
|
||||
this.#spawnRequestTable?.unit.location !== spawnRequestTable.unit.location ||
|
||||
this.#spawnRequestTable?.unit.unitType !== spawnRequestTable.unit.unitType ||
|
||||
this.#spawnRequestTable?.coalition !== spawnRequestTable.coalition
|
||||
)
|
||||
redrawMarker = true;
|
||||
|
||||
this.#spawnRequestTable = spawnRequestTable;
|
||||
|
||||
this.#currentSpawnMarker?.removeFrom(this);
|
||||
this.#currentSpawnMarker = this.addTemporaryMarker(spawnRequestTable.unit.location, spawnRequestTable.unit.unitType, spawnRequestTable.coalition, true);
|
||||
if (redrawMarker) {
|
||||
this.#currentSpawnMarker?.removeFrom(this);
|
||||
this.#currentSpawnMarker = this.addTemporaryMarker(spawnRequestTable.unit.location, spawnRequestTable.unit.unitType, spawnRequestTable.coalition, true);
|
||||
}
|
||||
}
|
||||
|
||||
getSpawnRequestTable() {
|
||||
@ -966,7 +976,7 @@ export class Map extends L.Map {
|
||||
if (Date.now() - this.#rightMouseDownEpoch < SHORT_PRESS_MILLISECONDS) this.#onRightShortClick(e);
|
||||
this.#isRightMouseDown = false;
|
||||
} else if (e.originalEvent?.button === 1) {
|
||||
getApp().setState(getApp().getState() === OlympusState.MEASURE? OlympusState.IDLE: OlympusState.MEASURE);
|
||||
getApp().setState(getApp().getState() === OlympusState.MEASURE ? OlympusState.IDLE : OlympusState.MEASURE);
|
||||
if (getApp().getState() === OlympusState.MEASURE) {
|
||||
const newMeasure = new Measure(this);
|
||||
const previousMeasure = this.#measures[this.#measures.length - 1];
|
||||
@ -984,7 +994,9 @@ export class Map extends L.Map {
|
||||
}
|
||||
|
||||
#onMouseDown(e: any) {
|
||||
if (e.originalEvent.button === 1) {this.dragging.disable();} // Disable dragging when right clicking
|
||||
if (e.originalEvent.button === 1) {
|
||||
this.dragging.disable();
|
||||
} // Disable dragging when right clicking
|
||||
|
||||
this.#originalMouseClickLatLng = e.latlng;
|
||||
if (e.originalEvent?.button === 0) {
|
||||
|
||||
@ -590,11 +590,12 @@
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
top: -30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
cursor: move;
|
||||
pointer-events: all;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.ol-temporary-marker .heading-handle svg {
|
||||
|
||||
@ -6,6 +6,7 @@ import { Converter } from "usng";
|
||||
import { MGRS } from "../types/types";
|
||||
import { featureCollection } from "turf";
|
||||
import MagVar from "magvar";
|
||||
import axios from 'axios';
|
||||
|
||||
export function bearing(lat1: number, lon1: number, lat2: number, lon2: number, magnetic = true) {
|
||||
const φ1 = deg2rad(lat1); // φ, λ in radians
|
||||
@ -654,4 +655,103 @@ export function decimalToRGBA(decimal: number): string {
|
||||
const a = (decimal & 0xff) / 255;
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${a.toFixed(2)})`;
|
||||
}
|
||||
|
||||
export async function getWikipediaImage(unitName: string): Promise<string | null> {
|
||||
try {
|
||||
// Search for the unit name on Wikipedia
|
||||
const searchResponse = await axios.get('https://en.wikipedia.org/w/api.php', {
|
||||
params: {
|
||||
action: 'query',
|
||||
list: 'search',
|
||||
srsearch: unitName,
|
||||
format: 'json',
|
||||
origin: '*'
|
||||
}
|
||||
});
|
||||
|
||||
if (searchResponse.data.query.search.length === 0) {
|
||||
console.error('No search results found for the unit name.');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the title of the first search result
|
||||
const pageTitle = searchResponse.data.query.search[0].title;
|
||||
|
||||
// Get the page content to find the image
|
||||
const pageResponse = await axios.get('https://en.wikipedia.org/w/api.php', {
|
||||
params: {
|
||||
action: 'query',
|
||||
titles: pageTitle,
|
||||
prop: 'pageimages',
|
||||
pithumbsize: 500,
|
||||
format: 'json',
|
||||
origin: '*'
|
||||
}
|
||||
});
|
||||
|
||||
const pages = pageResponse.data.query.pages;
|
||||
const pageId = Object.keys(pages)[0];
|
||||
const page = pages[pageId];
|
||||
|
||||
if (page.thumbnail && page.thumbnail.source) {
|
||||
return page.thumbnail.source;
|
||||
} else {
|
||||
console.error('No image found for the unit name.');
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data from Wikipedia:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWikipediaSummary(unitName: string): Promise<string | null> {
|
||||
try {
|
||||
// Search for the unit name on Wikipedia
|
||||
const searchResponse = await axios.get('https://en.wikipedia.org/w/api.php', {
|
||||
params: {
|
||||
action: 'query',
|
||||
list: 'search',
|
||||
srsearch: unitName,
|
||||
format: 'json',
|
||||
origin: '*'
|
||||
}
|
||||
});
|
||||
|
||||
if (searchResponse.data.query.search.length === 0) {
|
||||
console.error('No search results found for the unit name.');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the title of the first search result
|
||||
const pageTitle = searchResponse.data.query.search[0].title;
|
||||
|
||||
// Get the page content to find the summary
|
||||
const pageResponse = await axios.get('https://en.wikipedia.org/w/api.php', {
|
||||
params: {
|
||||
action: 'query',
|
||||
prop: 'extracts',
|
||||
exintro: true,
|
||||
explaintext: true,
|
||||
titles: pageTitle,
|
||||
format: 'json',
|
||||
origin: '*'
|
||||
}
|
||||
});
|
||||
|
||||
const pages = pageResponse.data.query.pages;
|
||||
const pageId = Object.keys(pages)[0];
|
||||
const page = pages[pageId];
|
||||
|
||||
if (page.extract) {
|
||||
return page.extract;
|
||||
} else {
|
||||
console.error('No summary found for the unit name.');
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data from Wikipedia:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,50 @@
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { OlTooltip } from "./oltooltip";
|
||||
|
||||
export function OlButtonGroup(props: { children?: JSX.Element | JSX.Element[] }) {
|
||||
return <div className="inline-flex rounded-md shadow-sm">{props.children}</div>;
|
||||
export function OlButtonGroup(props: {
|
||||
tooltip?: string | (() => JSX.Element | JSX.Element[]);
|
||||
tooltipPosition?: string;
|
||||
tooltipRelativeToParent?: boolean;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
}) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const [hoverTimeout, setHoverTimeout] = useState(null as number | null);
|
||||
var buttonRef = useRef(null);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={buttonRef}
|
||||
className="inline-flex rounded-md shadow-sm"
|
||||
onMouseEnter={() => {
|
||||
setHoverTimeout(
|
||||
window.setTimeout(() => {
|
||||
setHover(true);
|
||||
setHoverTimeout(null);
|
||||
}, 400)
|
||||
);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHover(false);
|
||||
if (hoverTimeout) {
|
||||
window.clearTimeout(hoverTimeout);
|
||||
setHoverTimeout(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
{hover && props.tooltip && (
|
||||
<OlTooltip
|
||||
buttonRef={buttonRef}
|
||||
content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()}
|
||||
position={props.tooltipPosition}
|
||||
relativeToParent={props.tooltipRelativeToParent}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function OlButtonGroupItem(props: { icon: IconProp; active: boolean; onClick: () => void }) {
|
||||
|
||||
@ -1,14 +1,52 @@
|
||||
import React from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Coalition } from "../../types/types";
|
||||
import { OlTooltip } from "./oltooltip";
|
||||
|
||||
export function OlCoalitionToggle(props: {
|
||||
coalition: Coalition | undefined;
|
||||
onClick: () => void;
|
||||
tooltip?: string | (() => JSX.Element | JSX.Element[]);
|
||||
tooltipPosition?: string;
|
||||
tooltipRelativeToParent?: boolean;
|
||||
className?: string;
|
||||
showLabel?: boolean;
|
||||
}) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const [hoverTimeout, setHoverTimeout] = useState(null as number | null);
|
||||
var buttonRef = useRef(null);
|
||||
|
||||
export function OlCoalitionToggle(props: { coalition: Coalition | undefined; onClick: () => void; showLabel?: boolean }) {
|
||||
return (
|
||||
<div className="inline-flex cursor-pointer items-center" onClick={props.onClick}>
|
||||
<>
|
||||
<div
|
||||
className="inline-flex cursor-pointer items-center"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onClick();
|
||||
props.onClick ?? setHover(false);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHoverTimeout(
|
||||
window.setTimeout(() => {
|
||||
setHover(true);
|
||||
setHoverTimeout(null);
|
||||
}, 400)
|
||||
);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHover(false);
|
||||
if (hoverTimeout) {
|
||||
window.clearTimeout(hoverTimeout);
|
||||
setHoverTimeout(null);
|
||||
}
|
||||
}}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<button className="peer sr-only" />
|
||||
<div
|
||||
data-flash={props.coalition === undefined}
|
||||
data-coalition={props.coalition ?? "blue"}
|
||||
className={`
|
||||
${props.className ?? ""}
|
||||
peer relative h-7 w-14 rounded-full bg-gray-200
|
||||
after:absolute after:start-[4px] after:top-0.5 after:h-6 after:w-6
|
||||
after:rounded-full after:border after:border-gray-300 after:bg-white
|
||||
@ -37,6 +75,16 @@ export function OlCoalitionToggle(props: { coalition: Coalition | undefined; onC
|
||||
{props.coalition ? `${props.coalition[0].toLocaleUpperCase() + props.coalition.substring(1)}` : "Diff. values"}
|
||||
</span>
|
||||
)}
|
||||
|
||||
</div>
|
||||
{hover && props.tooltip && (
|
||||
<OlTooltip
|
||||
buttonRef={buttonRef}
|
||||
content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()}
|
||||
position={props.tooltipPosition}
|
||||
relativeToParent={props.tooltipRelativeToParent}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useRef, MutableRefObject } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { OlTooltip } from "./oltooltip";
|
||||
|
||||
export function OlDropdown(props: {
|
||||
disableAutoClose?: boolean;
|
||||
@ -11,8 +12,13 @@ export function OlDropdown(props: {
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
buttonRef?: MutableRefObject<null> | null;
|
||||
open?: boolean;
|
||||
tooltip?: string | (() => JSX.Element | JSX.Element[]);
|
||||
tooltipPosition?: string;
|
||||
tooltipRelativeToParent?: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = props.open !== undefined ? [props.open, () => {}] : useState(false);
|
||||
const [hover, setHover] = useState(false);
|
||||
const [hoverTimeout, setHoverTimeout] = useState(null as number | null);
|
||||
var contentRef = useRef(null);
|
||||
var buttonRef = props.buttonRef !== undefined ? props.buttonRef : useRef(null);
|
||||
|
||||
@ -88,67 +94,92 @@ export function OlDropdown(props: {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={props.className ?? ""}>
|
||||
{props.buttonRef === undefined && (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={() => {
|
||||
setOpen(!open);
|
||||
}}
|
||||
className={`
|
||||
inline-flex w-full items-center justify-between rounded-lg border
|
||||
bg-blue-700 px-5 py-2.5 text-center text-sm font-medium text-white
|
||||
dark:border-gray-700 dark:bg-gray-700 dark:text-gray-100
|
||||
dark:hover:bg-gray-600
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
type="button"
|
||||
>
|
||||
{props.leftIcon && (
|
||||
<FontAwesomeIcon
|
||||
icon={props.leftIcon}
|
||||
className={`mr-3`}
|
||||
/>
|
||||
)}
|
||||
<span className="overflow-hidden text-ellipsis text-nowrap">{props.label ?? ""}</span>
|
||||
<svg
|
||||
<>
|
||||
<div className={props.className ?? ""}>
|
||||
{props.buttonRef === undefined && (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={() => {
|
||||
setOpen(!open);
|
||||
}}
|
||||
className={`
|
||||
ml-auto ms-3 h-2.5 w-2.5 flex-none transition-transform
|
||||
data-[open='true']:-scale-y-100
|
||||
inline-flex w-full items-center justify-between rounded-lg border
|
||||
bg-blue-700 px-5 py-2.5 text-center text-sm font-medium text-white
|
||||
dark:border-gray-700 dark:bg-gray-700 dark:text-gray-100
|
||||
dark:hover:bg-gray-600
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
data-open={open}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 10 6"
|
||||
type="button"
|
||||
onMouseEnter={() => {
|
||||
setHoverTimeout(
|
||||
window.setTimeout(() => {
|
||||
setHover(true);
|
||||
setHoverTimeout(null);
|
||||
}, 400)
|
||||
);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHover(false);
|
||||
if (hoverTimeout) {
|
||||
window.clearTimeout(hoverTimeout);
|
||||
setHoverTimeout(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m1 1 4 4 4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{props.leftIcon && (
|
||||
<FontAwesomeIcon
|
||||
icon={props.leftIcon}
|
||||
className={`mr-3`}
|
||||
/>
|
||||
)}
|
||||
<span className="overflow-hidden text-ellipsis text-nowrap">{props.label ?? ""}</span>
|
||||
<svg
|
||||
className={`
|
||||
ml-auto ms-3 h-2.5 w-2.5 flex-none transition-transform
|
||||
data-[open='true']:-scale-y-100
|
||||
`}
|
||||
data-open={open}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 10 6"
|
||||
>
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m1 1 4 4 4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={contentRef}
|
||||
data-open={open}
|
||||
className={`
|
||||
absolute z-40 divide-y divide-gray-100 overflow-y-scroll no-scrollbar
|
||||
rounded-lg bg-white p-2 shadow
|
||||
dark:bg-gray-700
|
||||
data-[open='false']:hidden
|
||||
`}
|
||||
>
|
||||
<div
|
||||
ref={contentRef}
|
||||
data-open={open}
|
||||
className={`
|
||||
h-fit w-full text-sm text-gray-700
|
||||
dark:text-gray-200
|
||||
absolute z-40 divide-y divide-gray-100 overflow-y-scroll
|
||||
no-scrollbar rounded-lg bg-white p-2 shadow
|
||||
dark:bg-gray-700
|
||||
data-[open='false']:hidden
|
||||
`}
|
||||
onClick={() => {
|
||||
props.disableAutoClose !== true && setOpen(false);
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
<div
|
||||
className={`
|
||||
h-fit w-full text-sm text-gray-700
|
||||
dark:text-gray-200
|
||||
`}
|
||||
onClick={() => {
|
||||
props.disableAutoClose !== true && setOpen(false);
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{hover && !open && buttonRef && props.tooltip && (
|
||||
<OlTooltip
|
||||
buttonRef={buttonRef}
|
||||
content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()}
|
||||
position={props.tooltipPosition}
|
||||
relativeToParent={props.tooltipRelativeToParent}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
30
frontend/react/src/ui/components/olexpandingtooltip.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import { FaQuestionCircle } from "react-icons/fa";
|
||||
|
||||
export function OlExpandingTooltip(props: { title: string; content: string | JSX.Element | JSX.Element[] }) {
|
||||
return (
|
||||
<div className="overflow-hidden" style={{ animationName: "tooltipFadeInHeight", animationDuration: "1s", animationFillMode: "forwards", height: "26px" }}>
|
||||
<div className={`absolute left-0 top-0 h-full w-full bg-transparent`}>
|
||||
<div
|
||||
className="h-full bg-blue-500/5"
|
||||
style={{ animationName: "loadingBar", animationDuration: "1s", animationFillMode: "forwards", width: "100%" }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="mb-4 flex h-6 content-center gap-2 px-2">
|
||||
|
||||
<div className="my-auto">
|
||||
<FaQuestionCircle className={`text-gray-200`} />
|
||||
</div>
|
||||
<div className="my-auto">{props.title}</div>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex min-w-[350px] flex-col gap-2 text-wrap p-2 text-gray-400
|
||||
`}
|
||||
style={{ animationName: "tooltipFadeInWidth", animationDuration: "1s", animationFillMode: "forwards", width: "1px" }}
|
||||
>
|
||||
{props.content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,51 +1,91 @@
|
||||
import React from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { OlTooltip } from "./oltooltip";
|
||||
|
||||
export function OlLabelToggle(props: {
|
||||
toggled: boolean | undefined;
|
||||
leftLabel: string;
|
||||
rightLabel: string;
|
||||
tooltip?: string | (() => JSX.Element | JSX.Element[]);
|
||||
tooltipPosition?: string;
|
||||
tooltipRelativeToParent?: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const [hoverTimeout, setHoverTimeout] = useState(null as number | null);
|
||||
var buttonRef = useRef(null);
|
||||
|
||||
export function OlLabelToggle(props: { toggled: boolean | undefined; leftLabel: string; rightLabel: string; onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onClick();
|
||||
}}
|
||||
className={`
|
||||
relative flex h-10 w-[120px] flex-none cursor-pointer select-none
|
||||
flex-row content-center justify-between rounded-md border px-1 py-[5px]
|
||||
text-sm
|
||||
dark:border-gray-600 dark:border-transparent dark:bg-gray-700
|
||||
dark:hover:bg-gray-600 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-300
|
||||
`}
|
||||
>
|
||||
<span
|
||||
data-flash={props.toggled === undefined}
|
||||
data-toggled={props.toggled ?? false}
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onClick();
|
||||
setHover(false);
|
||||
}}
|
||||
className={`
|
||||
absolute my-auto h-[28px] w-[54px] rounded-md bg-blue-500
|
||||
transition-transform
|
||||
data-[flash='true']:animate-pulse
|
||||
data-[toggled='true']:translate-x-14
|
||||
`}
|
||||
></span>
|
||||
<span
|
||||
data-active={!(props.toggled ?? false)}
|
||||
className={`
|
||||
absolute left-[17px] top-[8px] font-normal transition-colors
|
||||
dark:data-[active='false']:text-gray-400
|
||||
dark:data-[active='true']:text-white
|
||||
relative flex h-10 w-[120px] flex-none cursor-pointer select-none
|
||||
flex-row content-center justify-between rounded-md border px-1
|
||||
py-[5px] text-sm
|
||||
dark:border-gray-600 dark:border-transparent dark:bg-gray-700
|
||||
dark:hover:bg-gray-600 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-300
|
||||
`}
|
||||
ref={buttonRef}
|
||||
onMouseEnter={() => {
|
||||
setHoverTimeout(
|
||||
window.setTimeout(() => {
|
||||
setHover(true);
|
||||
setHoverTimeout(null);
|
||||
}, 400)
|
||||
);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHover(false);
|
||||
if (hoverTimeout) {
|
||||
window.clearTimeout(hoverTimeout);
|
||||
setHoverTimeout(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{props.leftLabel}
|
||||
</span>
|
||||
<span
|
||||
data-active={props.toggled ?? false}
|
||||
className={`
|
||||
absolute right-[17px] top-[8px] font-normal transition-colors
|
||||
dark:data-[active='false']:text-gray-400
|
||||
dark:data-[active='true']:text-white
|
||||
`}
|
||||
>
|
||||
{props.rightLabel}
|
||||
</span>
|
||||
</button>
|
||||
<span
|
||||
data-flash={props.toggled === undefined}
|
||||
data-toggled={props.toggled ?? false}
|
||||
className={`
|
||||
absolute my-auto h-[28px] w-[54px] rounded-md bg-blue-500
|
||||
transition-transform
|
||||
data-[flash='true']:animate-pulse
|
||||
data-[toggled='true']:translate-x-14
|
||||
`}
|
||||
></span>
|
||||
<span
|
||||
data-active={!(props.toggled ?? false)}
|
||||
className={`
|
||||
absolute left-[17px] top-[8px] font-normal transition-colors
|
||||
dark:data-[active='false']:text-gray-400
|
||||
dark:data-[active='true']:text-white
|
||||
`}
|
||||
>
|
||||
{props.leftLabel}
|
||||
</span>
|
||||
<span
|
||||
data-active={props.toggled ?? false}
|
||||
className={`
|
||||
absolute right-[17px] top-[8px] font-normal transition-colors
|
||||
dark:data-[active='false']:text-gray-400
|
||||
dark:data-[active='true']:text-white
|
||||
`}
|
||||
>
|
||||
{props.rightLabel}
|
||||
</span>
|
||||
</button>
|
||||
{hover && props.tooltip && (
|
||||
<OlTooltip
|
||||
buttonRef={buttonRef}
|
||||
content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()}
|
||||
position={props.tooltipPosition}
|
||||
relativeToParent={props.tooltipRelativeToParent}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ export function OlLocation(props: { location: LatLng; className?: string; refere
|
||||
>
|
||||
{props.location.lat >= 0 ? "N" : "S"}
|
||||
</span>
|
||||
{zeroAppend(props.location.lat, 3, true, 6)}
|
||||
{zeroAppend(props.location.lat, 3, true, 6)}°
|
||||
</div>
|
||||
<div className="flex w-[50%] gap-2">
|
||||
<span
|
||||
@ -52,7 +52,7 @@ export function OlLocation(props: { location: LatLng; className?: string; refere
|
||||
>
|
||||
{props.location.lng >= 0 ? "E" : "W"}
|
||||
</span>
|
||||
{zeroAppend(props.location.lng, 3, true, 6)}
|
||||
{zeroAppend(props.location.lng, 3, true, 6)}°
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { ChangeEvent } from "react";
|
||||
import React, { ChangeEvent, useRef, useState } from "react";
|
||||
import { zeroAppend } from "../../other/utils";
|
||||
import { OlTooltip } from "./oltooltip";
|
||||
|
||||
export function OlNumberInput(props: {
|
||||
value: number;
|
||||
@ -7,10 +8,17 @@ export function OlNumberInput(props: {
|
||||
max: number;
|
||||
minLength?: number;
|
||||
className?: string;
|
||||
tooltip?: string | (() => JSX.Element | JSX.Element[]);
|
||||
tooltipPosition?: string;
|
||||
tooltipRelativeToParent?: boolean;
|
||||
onDecrease: () => void;
|
||||
onIncrease: () => void;
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
}) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const [hoverTimeout, setHoverTimeout] = useState(null as number | null);
|
||||
var buttonRef = useRef(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
@ -18,12 +26,31 @@ export function OlNumberInput(props: {
|
||||
min-w-32
|
||||
`}
|
||||
>
|
||||
<div className="relative flex max-w-[8rem] items-center">
|
||||
<div
|
||||
className="relative flex max-w-[8rem] items-center"
|
||||
ref={buttonRef}
|
||||
onMouseEnter={() => {
|
||||
setHoverTimeout(
|
||||
window.setTimeout(() => {
|
||||
setHover(true);
|
||||
setHoverTimeout(null);
|
||||
}, 400)
|
||||
);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHover(false);
|
||||
if (hoverTimeout) {
|
||||
window.clearTimeout(hoverTimeout);
|
||||
setHoverTimeout(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onDecrease();
|
||||
setHover(false);
|
||||
}}
|
||||
className={`
|
||||
h-10 rounded-s-lg bg-gray-100 p-3
|
||||
@ -48,7 +75,10 @@ export function OlNumberInput(props: {
|
||||
<input
|
||||
type="text"
|
||||
onChange={props.onChange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setHover(false);
|
||||
}}
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
className={`
|
||||
@ -66,6 +96,7 @@ export function OlNumberInput(props: {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onIncrease();
|
||||
setHover(false);
|
||||
}}
|
||||
className={`
|
||||
h-10 rounded-e-lg bg-gray-100 p-3
|
||||
@ -88,6 +119,14 @@ export function OlNumberInput(props: {
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{hover && props.tooltip && (
|
||||
<OlTooltip
|
||||
buttonRef={buttonRef}
|
||||
content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()}
|
||||
position={props.tooltipPosition}
|
||||
relativeToParent={props.tooltipRelativeToParent}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -13,12 +13,14 @@ export function OlStateButton(props: {
|
||||
icon?: IconProp;
|
||||
tooltip?: string | (() => JSX.Element | JSX.Element[]);
|
||||
tooltipPosition?: string;
|
||||
tooltipRelativeToParent?: boolean;
|
||||
onClick: () => void;
|
||||
onMouseUp?: () => void;
|
||||
onMouseDown?: () => void;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
}) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const [hoverTimeout, setHoverTimeout] = useState(null as number | null);
|
||||
var buttonRef = useRef(null);
|
||||
|
||||
const className =
|
||||
@ -45,30 +47,63 @@ export function OlStateButton(props: {
|
||||
className={className}
|
||||
style={{
|
||||
border: props.buttonColor ? "2px solid " + props.buttonColor : "0px solid transparent",
|
||||
background: setOpacity(props.checked || hover ? (props.buttonColor ? props.buttonColor : colors.OLYMPUS_LIGHT_BLUE) : colors.OLYMPUS_BLUE, (!props.checked && hover)? 0.3: 1),
|
||||
background: setOpacity(
|
||||
props.checked || hover ? (props.buttonColor ? props.buttonColor : colors.OLYMPUS_LIGHT_BLUE) : colors.OLYMPUS_BLUE,
|
||||
!props.checked && hover ? 0.3 : 1
|
||||
),
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHover(true);
|
||||
setHoverTimeout(
|
||||
window.setTimeout(() => {
|
||||
setHover(true);
|
||||
setHoverTimeout(null);
|
||||
}, 400)
|
||||
);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHover(false);
|
||||
if (hoverTimeout) {
|
||||
window.clearTimeout(hoverTimeout);
|
||||
setHoverTimeout(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={`m-auto flex w-fit content-center justify-center gap-2`}>
|
||||
{props.icon && <FontAwesomeIcon icon={props.icon} data-bright={props.buttonColor && props.checked && computeBrightness(props.buttonColor) > 200} className={`
|
||||
m-auto text-gray-200
|
||||
data-[bright='true']:text-gray-800
|
||||
`} />}
|
||||
{props.icon && (
|
||||
<FontAwesomeIcon
|
||||
icon={props.icon}
|
||||
data-bright={props.buttonColor && props.checked && computeBrightness(props.buttonColor) > 200}
|
||||
className={`
|
||||
m-auto text-gray-200
|
||||
data-[bright='true']:text-gray-800
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
{props.children}
|
||||
</div>
|
||||
</button>
|
||||
{hover && props.tooltip && <OlTooltip buttonRef={buttonRef} content={typeof(props.tooltip) === "string" ? props.tooltip: props.tooltip()} position={props.tooltipPosition}/>}
|
||||
{hover && props.tooltip && (
|
||||
<OlTooltip
|
||||
buttonRef={buttonRef}
|
||||
content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()}
|
||||
position={props.tooltipPosition}
|
||||
relativeToParent={props.tooltipRelativeToParent}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function OlRoundStateButton(props: { className?: string; checked: boolean; icon: IconProp; tooltip: string; onClick: () => void }) {
|
||||
export function OlRoundStateButton(props: {
|
||||
className?: string;
|
||||
checked: boolean;
|
||||
icon: IconProp;
|
||||
tooltip?: string | (() => JSX.Element | JSX.Element[]);
|
||||
tooltipPosition?: string;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const [hoverTimeout, setHoverTimeout] = useState(null as number | null);
|
||||
var buttonRef = useRef(null);
|
||||
|
||||
const className =
|
||||
@ -96,21 +131,39 @@ export function OlRoundStateButton(props: { className?: string; checked: boolean
|
||||
type="button"
|
||||
className={className}
|
||||
onMouseEnter={() => {
|
||||
setHover(true);
|
||||
setHoverTimeout(
|
||||
window.setTimeout(() => {
|
||||
setHover(true);
|
||||
setHoverTimeout(null);
|
||||
}, 400)
|
||||
);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHover(false);
|
||||
if (hoverTimeout) {
|
||||
window.clearTimeout(hoverTimeout);
|
||||
setHoverTimeout(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon className="pt-[3px]" icon={props.icon} />
|
||||
</button>
|
||||
{hover && <OlTooltip buttonRef={buttonRef} content={props.tooltip} />}
|
||||
{hover && props.tooltip && (
|
||||
<OlTooltip buttonRef={buttonRef} content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()} position={props.tooltipPosition} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function OlLockStateButton(props: { className?: string; checked: boolean; tooltip: string; onClick: () => void }) {
|
||||
export function OlLockStateButton(props: {
|
||||
className?: string;
|
||||
checked: boolean;
|
||||
tooltip?: string | (() => JSX.Element | JSX.Element[]);
|
||||
tooltipPosition?: string;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const [hoverTimeout, setHoverTimeout] = useState(null as number | null);
|
||||
var buttonRef = useRef(null);
|
||||
|
||||
const className =
|
||||
@ -135,15 +188,26 @@ export function OlLockStateButton(props: { className?: string; checked: boolean;
|
||||
type="button"
|
||||
className={className}
|
||||
onMouseEnter={() => {
|
||||
setHover(true);
|
||||
setHoverTimeout(
|
||||
window.setTimeout(() => {
|
||||
setHover(true);
|
||||
setHoverTimeout(null);
|
||||
}, 400)
|
||||
);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHover(false);
|
||||
if (hoverTimeout) {
|
||||
window.clearTimeout(hoverTimeout);
|
||||
setHoverTimeout(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon className="pt-[3px]" icon={props.checked == true ? faUnlockAlt : faLock} />
|
||||
</button>
|
||||
{hover && <OlTooltip buttonRef={buttonRef} content={props.tooltip} />}
|
||||
{hover && props.tooltip && (
|
||||
<OlTooltip buttonRef={buttonRef} content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()} position={props.tooltipPosition} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,33 +1,71 @@
|
||||
import React from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { OlTooltip } from "./oltooltip";
|
||||
|
||||
export function OlToggle(props: {
|
||||
toggled: boolean | undefined;
|
||||
tooltip?: string | (() => JSX.Element | JSX.Element[]);
|
||||
tooltipPosition?: string;
|
||||
tooltipRelativeToParent?: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const [hoverTimeout, setHoverTimeout] = useState(null as number | null);
|
||||
var buttonRef = useRef(null);
|
||||
|
||||
export function OlToggle(props: { toggled: boolean | undefined; onClick: () => void }) {
|
||||
return (
|
||||
<div
|
||||
className="inline-flex cursor-pointer items-center"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onClick();
|
||||
}}
|
||||
>
|
||||
<button className="peer sr-only" />
|
||||
<>
|
||||
<div
|
||||
data-toggled={props.toggled === true ? "true" : props.toggled === undefined ? "undefined" : "false"}
|
||||
className={`
|
||||
peer relative h-7 w-14 rounded-full bg-gray-200
|
||||
after:absolute after:start-[4px] after:top-0.5 after:h-6 after:w-6
|
||||
after:rounded-full after:border after:border-gray-300 after:bg-white
|
||||
after:transition-all after:content-['']
|
||||
dark:border-gray-600 dark:peer-focus:ring-blue-800
|
||||
dark:data-[toggled='true']:bg-blue-500
|
||||
data-[toggled='false']:bg-gray-500
|
||||
data-[toggled='true']:after:translate-x-full
|
||||
data-[toggled='true']:after:border-white
|
||||
data-[toggled='undefined']:bg-gray-800
|
||||
data-[toggled='undefined']:after:translate-x-[50%]
|
||||
peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-300
|
||||
rtl:data-[toggled='true']:after:-translate-x-full
|
||||
`}
|
||||
></div>
|
||||
</div>
|
||||
className="inline-flex cursor-pointer items-center"
|
||||
ref={buttonRef}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onClick();
|
||||
setHover(false);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHoverTimeout(
|
||||
window.setTimeout(() => {
|
||||
setHover(true);
|
||||
setHoverTimeout(null);
|
||||
}, 400)
|
||||
);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHover(false);
|
||||
if (hoverTimeout) {
|
||||
window.clearTimeout(hoverTimeout);
|
||||
setHoverTimeout(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<button className="peer sr-only" />
|
||||
<div
|
||||
data-toggled={props.toggled === true ? "true" : props.toggled === undefined ? "undefined" : "false"}
|
||||
className={`
|
||||
peer relative h-7 w-14 rounded-full bg-gray-200
|
||||
after:absolute after:start-[4px] after:top-0.5 after:h-6 after:w-6
|
||||
after:rounded-full after:border after:border-gray-300 after:bg-white
|
||||
after:transition-all after:content-['']
|
||||
dark:border-gray-600 dark:peer-focus:ring-blue-800
|
||||
dark:data-[toggled='true']:bg-blue-500
|
||||
data-[toggled='false']:bg-gray-500
|
||||
data-[toggled='true']:after:translate-x-full
|
||||
data-[toggled='true']:after:border-white
|
||||
data-[toggled='undefined']:bg-gray-800
|
||||
data-[toggled='undefined']:after:translate-x-[50%]
|
||||
peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-300
|
||||
rtl:data-[toggled='true']:after:-translate-x-full
|
||||
`}
|
||||
></div>
|
||||
</div>
|
||||
{hover && props.tooltip && (
|
||||
<OlTooltip
|
||||
buttonRef={buttonRef}
|
||||
content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()}
|
||||
position={props.tooltipPosition}
|
||||
relativeToParent={props.tooltipRelativeToParent}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
export function OlTooltip(props: { content: string | JSX.Element | JSX.Element[]; buttonRef: React.MutableRefObject<null>; position?: string }) {
|
||||
export function OlTooltip(props: {
|
||||
content: string | JSX.Element | JSX.Element[];
|
||||
buttonRef: React.MutableRefObject<null>;
|
||||
position?: string;
|
||||
relativeToParent?: boolean;
|
||||
}) {
|
||||
var contentRef = useRef(null);
|
||||
|
||||
function setPosition(content: HTMLDivElement, button: HTMLButtonElement) {
|
||||
@ -9,8 +14,16 @@ export function OlTooltip(props: { content: string | JSX.Element | JSX.Element[]
|
||||
content.style.top = "0px";
|
||||
content.style.height = "";
|
||||
|
||||
button.style.position = "relative";
|
||||
button.style.left = "0px";
|
||||
button.style.top = "0px";
|
||||
|
||||
|
||||
let parent = button.closest(".ol-panel-container") as HTMLElement;
|
||||
if (parent === null) parent = document.body;
|
||||
|
||||
/* Get the position and size of the button and the content elements */
|
||||
let [cxl, cyt, cxr, cyb, cw, ch] = [
|
||||
let [contentXLeft, contentYTop, contentXRight, contentYBottom, contentWidth, contentHeight] = [
|
||||
content.getBoundingClientRect().x,
|
||||
content.getBoundingClientRect().y,
|
||||
content.getBoundingClientRect().x + content.offsetWidth,
|
||||
@ -18,7 +31,7 @@ export function OlTooltip(props: { content: string | JSX.Element | JSX.Element[]
|
||||
content.offsetWidth,
|
||||
content.offsetHeight,
|
||||
];
|
||||
let [bxl, byt, bxr, byb, bbw, bh] = [
|
||||
let [buttonXLeft, buttonYTop, buttonXRight, buttonYBottom, buttonWidth, buttonHeight] = [
|
||||
button.getBoundingClientRect().x,
|
||||
button.getBoundingClientRect().y,
|
||||
button.getBoundingClientRect().x + button.offsetWidth,
|
||||
@ -28,38 +41,49 @@ export function OlTooltip(props: { content: string | JSX.Element | JSX.Element[]
|
||||
];
|
||||
|
||||
/* Limit the maximum height */
|
||||
if (ch > 400) {
|
||||
ch = 400;
|
||||
content.style.height = `${ch}px`;
|
||||
if (contentHeight > 400) {
|
||||
contentHeight = 400;
|
||||
content.style.height = `${contentHeight}px`;
|
||||
}
|
||||
|
||||
/* Compute the horizontal position of the center of the button and the content */
|
||||
var cxc = (cxl + cxr) / 2;
|
||||
var bxc = (bxl + bxr) / 2;
|
||||
var contentXCenter = (contentXLeft + contentXRight) / 2;
|
||||
var buttonXCenter = (buttonXLeft + buttonXRight) / 2;
|
||||
|
||||
/* Compute the x and y offsets needed to align the button and element horizontally, and to put the content depending on the requested position */
|
||||
var offsetX = 0;
|
||||
var offsetY = 0;
|
||||
|
||||
if (props.position === undefined || props.position === "below") {
|
||||
offsetX = bxc - cxc;
|
||||
offsetY = byb - cyt + 8;
|
||||
} else if (props.position === "side") {
|
||||
offsetX = bxr + 8;
|
||||
offsetY = byt - cyt + (bh - ch) / 2;
|
||||
offsetX = buttonXCenter - contentXCenter;
|
||||
offsetY = buttonYBottom - contentYTop + 8;
|
||||
} else if (props.position === "above") {
|
||||
offsetX = buttonXCenter - contentXCenter;
|
||||
offsetY = buttonYTop - contentYTop - contentHeight - 8;
|
||||
}
|
||||
else if (props.position === "side") {
|
||||
offsetX = buttonXRight + 8;
|
||||
offsetY = buttonYTop - contentYTop + (buttonHeight - contentHeight) / 2;
|
||||
}
|
||||
|
||||
content.style.left = `${offsetX}px`;
|
||||
content.style.top = `${offsetY}px`;
|
||||
|
||||
/* Compute the new position of the left and right margins of the content */
|
||||
let ncxl = cxl + offsetX;
|
||||
let ncxr = cxr + offsetX;
|
||||
let ncyb = cyb + offsetY;
|
||||
let newContentXLeft = props.relativeToParent ? offsetX : contentXLeft + offsetX;
|
||||
let newContentXRight = newContentXLeft + contentWidth;
|
||||
let newContentYBottom = props.relativeToParent ? offsetY : contentYBottom + offsetY;
|
||||
|
||||
/* Try and move the content so it is inside the screen */
|
||||
if (ncxl < 0) offsetX -= cxl;
|
||||
if (ncxr > window.innerWidth) {
|
||||
offsetX = bxl - cxl - cw - 12;
|
||||
if (newContentXLeft < 0) offsetX = 15;
|
||||
if (newContentXRight > (props.relativeToParent ? parent.clientWidth : window.innerWidth)) {
|
||||
if (props.position === "side") {
|
||||
offsetX = buttonXLeft - contentXLeft - contentWidth - 12;
|
||||
} else {
|
||||
offsetX -= newContentXRight - (props.relativeToParent ? parent.clientWidth : window.innerWidth) + 15;
|
||||
}
|
||||
}
|
||||
if (ncyb > window.innerHeight) offsetY -= bh + ch + 16;
|
||||
if (newContentYBottom > (props.relativeToParent ? parent.clientHeight : window.innerHeight)) offsetY -= buttonHeight + contentHeight + 16;
|
||||
|
||||
/* Apply the offset */
|
||||
content.style.left = `${offsetX}px`;
|
||||
@ -77,7 +101,7 @@ export function OlTooltip(props: { content: string | JSX.Element | JSX.Element[]
|
||||
setPosition(content, button);
|
||||
});
|
||||
resizeObserver.observe(content);
|
||||
return () => resizeObserver.disconnect(); // clean up
|
||||
return () => resizeObserver.disconnect(); // clean up
|
||||
}
|
||||
});
|
||||
|
||||
@ -86,9 +110,9 @@ export function OlTooltip(props: { content: string | JSX.Element | JSX.Element[]
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={`
|
||||
absolute z-50 whitespace-nowrap rounded-lg bg-gray-900 px-3 py-2
|
||||
text-sm font-medium text-white shadow-sm
|
||||
dark:bg-gray-700
|
||||
pointer-events-none absolute z-50 whitespace-nowrap rounded-lg px-3
|
||||
py-2 text-sm font-medium text-white shadow-md backdrop-blur-sm
|
||||
backdrop-grayscale transition-transform no-scrollbar bg-olympus-800/90
|
||||
`}
|
||||
>
|
||||
{props.content}
|
||||
|
||||
@ -1,8 +1,23 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { UnitBlueprint } from "../../interfaces";
|
||||
import { Coalition } from "../../types/types";
|
||||
import { getWikipediaImage, getWikipediaSummary } from "../../other/utils";
|
||||
import { OlStateButton } from "./olstatebutton";
|
||||
import { faImage } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FaQuestionCircle } from "react-icons/fa";
|
||||
|
||||
export function OlUnitSummary(props: { blueprint: UnitBlueprint; coalition: Coalition }) {
|
||||
const [imageUrl, setImageUrl] = useState(null as string | null);
|
||||
const [summary, setSummary] = useState(null as string | null);
|
||||
const [hover, setHover] = useState(false);
|
||||
const [hoverTimeout, setHoverTimeout] = useState(null as number | null);
|
||||
|
||||
useEffect(() => {
|
||||
getWikipediaImage(props.blueprint.label).then((url) => {
|
||||
setImageUrl(url);
|
||||
});
|
||||
}, [props.blueprint]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-coalition={props.coalition}
|
||||
@ -14,34 +29,55 @@ export function OlUnitSummary(props: { blueprint: UnitBlueprint; coalition: Coal
|
||||
data-[coalition='neutral']:border-gray-400
|
||||
data-[coalition='red']:border-red-500
|
||||
`}
|
||||
onMouseEnter={() => {
|
||||
setHoverTimeout(
|
||||
window.setTimeout(() => {
|
||||
setHover(true);
|
||||
setHoverTimeout(null);
|
||||
}, 400)
|
||||
);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHover(false);
|
||||
if (hoverTimeout) {
|
||||
window.clearTimeout(hoverTimeout);
|
||||
setHoverTimeout(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row content-center gap-2">
|
||||
<img
|
||||
className={`
|
||||
absolute right-5 top-0 h-full object-cover opacity-10 invert
|
||||
`}
|
||||
src={"vite./images/units/" + props.blueprint.filename}
|
||||
alt=""
|
||||
/>
|
||||
{imageUrl && hover && <img className={``} src={imageUrl} alt="" />}
|
||||
<div
|
||||
className={`
|
||||
flex w-full flex-row content-center justify-between gap-2
|
||||
ol-panel-container
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
my-auto ml-2 w-full font-semibold tracking-tight text-gray-900
|
||||
my-auto ml-2 flex w-full justify-between text-nowrap font-semibold
|
||||
tracking-tight text-gray-900
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
{props.blueprint.label}
|
||||
</div>
|
||||
{imageUrl && (
|
||||
<div className="flex w-full min-w-0 gap-1 text-sm text-gray-500">
|
||||
<FaQuestionCircle
|
||||
className={`my-auto min-w-4`}
|
||||
/>
|
||||
<div className={`my-auto max-w-full truncate`}>Hover to show image</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`flex h-fit flex-col justify-between px-2 leading-normal`}
|
||||
>
|
||||
<div className={`flex h-fit flex-col justify-between px-2 leading-normal`}>
|
||||
<p
|
||||
className={`
|
||||
mb-1 text-sm text-gray-700
|
||||
dark:text-gray-400
|
||||
`}
|
||||
>
|
||||
{props.blueprint.description}
|
||||
{summary ?? props.blueprint.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-row gap-1 px-2">
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { ModalEvent } from "../../../events";
|
||||
import { FaXmark } from "react-icons/fa6";
|
||||
import { getApp, OlympusApp } from "../../../olympusapp";
|
||||
import { OlympusState } from "../../../constants/constants";
|
||||
|
||||
export function Modal(props: { open: boolean; children?: JSX.Element | JSX.Element[]; className?: string }) {
|
||||
const [splash, setSplash] = useState(Math.ceil(Math.random() * 7));
|
||||
|
||||
useEffect(() => {
|
||||
ModalEvent.dispatch(props.open);
|
||||
}, [props.open]);
|
||||
@ -13,13 +18,52 @@ export function Modal(props: { open: boolean; children?: JSX.Element | JSX.Eleme
|
||||
<div className={`fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95`}></div>
|
||||
<div
|
||||
className={`
|
||||
${props.className}
|
||||
fixed left-[50%] top-[50%] z-40 translate-x-[-50%]
|
||||
translate-y-[-50%] rounded-xl border-[1px] border-solid
|
||||
border-gray-700 drop-shadow-md
|
||||
fixed left-[50%] top-[50%] z-40 inline-flex h-[75%] max-h-[600px]
|
||||
w-[80%] max-w-[1100px] translate-x-[-50%] translate-y-[-50%]
|
||||
overflow-y-auto scroll-smooth rounded-xl border-[1px] border-solid
|
||||
border-gray-700 bg-white drop-shadow-md
|
||||
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
|
||||
`}
|
||||
>
|
||||
{props.children}
|
||||
<img
|
||||
src={`images/splash/${splash}.jpg`}
|
||||
className={`contents-center w-full object-cover opacity-[7%]`}
|
||||
></img>
|
||||
<div className="fixed left-0 top-0 h-full w-full">
|
||||
<div
|
||||
className={`
|
||||
absolute h-full w-full bg-gradient-to-r from-blue-200/25
|
||||
to-transparent
|
||||
`}
|
||||
></div>
|
||||
<div
|
||||
className={`
|
||||
absolute h-full w-full bg-gradient-to-t from-olympus-800
|
||||
to-transparent
|
||||
`}
|
||||
></div>
|
||||
<div
|
||||
className={`
|
||||
absolute flex h-full w-full flex-col gap-8 p-16
|
||||
max-lg:p-8
|
||||
`}
|
||||
>
|
||||
{props.children}
|
||||
<div
|
||||
className={`
|
||||
absolute right-5 top-5 cursor-pointer text-xl text-white
|
||||
`}
|
||||
>
|
||||
<FaXmark
|
||||
onClick={() => {
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
}}
|
||||
/>{" "}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -45,7 +45,7 @@ export function ImportExportModal(props: { open: boolean }) {
|
||||
"groundunit-sam": true,
|
||||
groundunit: true,
|
||||
navyunit: true,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -95,274 +95,263 @@ export function ImportExportModal(props: { open: boolean }) {
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={props.open}
|
||||
className={`
|
||||
inline-flex h-fit w-[600px] overflow-y-auto scroll-smooth bg-white p-10
|
||||
dark:bg-olympus-800
|
||||
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
|
||||
max-md:border-none
|
||||
`}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col gap-4">
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<span
|
||||
<Modal open={props.open} className={``}>
|
||||
<div className={`flex flex-col justify-between gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
{appSubState === ImportExportSubstate.EXPORT ? "Export to file" : "Import from file"}
|
||||
</span>
|
||||
|
||||
<span className="text-gray-400">
|
||||
{appSubState === ImportExportSubstate.EXPORT ? <>Select what units you want to export to file using the toggles below</> : <></>}
|
||||
</span>
|
||||
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
text-bold border-b-2 border-b-white/10 pb-2 text-gray-400
|
||||
`}
|
||||
>
|
||||
{appSubState === ImportExportSubstate.EXPORT ? "Export to file" : "Import from file"}
|
||||
</span>
|
||||
Control mode
|
||||
</div>
|
||||
|
||||
<span className="text-gray-400">
|
||||
{appSubState === ImportExportSubstate.EXPORT ? <>Select what units you want to export to file using the toggles below</> : <></>}
|
||||
</span>
|
||||
<div className="flex flex-col justify-start gap-2">
|
||||
{Object.entries({
|
||||
olympus: ["Olympus controlled", olButtonsVisibilityOlympus],
|
||||
dcs: ["From DCS mission", olButtonsVisibilityDcs],
|
||||
}).map((entry, idx) => {
|
||||
return (
|
||||
<div className="flex justify-between" key={idx}>
|
||||
<span className="font-light text-white">{entry[1][0] as string}</span>
|
||||
<OlToggle
|
||||
key={entry[0]}
|
||||
onClick={() => {
|
||||
selectionFilter["control"][entry[0]] = !selectionFilter["control"][entry[0]];
|
||||
setSelectionFilter(deepCopyTable(selectionFilter));
|
||||
}}
|
||||
toggled={selectionFilter["control"][entry[0]]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div
|
||||
className={`
|
||||
text-bold border-b-2 border-b-white/10 pb-2 text-gray-400
|
||||
`}
|
||||
>
|
||||
Control mode
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
text-bold mt-5 border-b-2 border-b-white/10 pb-2 text-gray-400
|
||||
`}
|
||||
>
|
||||
Types and coalitions
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-start gap-2">
|
||||
<table className="mr-16">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td className="pb-4 text-center font-bold text-blue-500">BLUE</td>
|
||||
<td className="pb-4 text-center font-bold text-gray-500">NEUTRAL</td>
|
||||
<td className="pb-4 text-center font-bold text-red-500">RED</td>
|
||||
</tr>
|
||||
{Object.entries({
|
||||
olympus: ["Olympus controlled", olButtonsVisibilityOlympus],
|
||||
dcs: ["From DCS mission", olButtonsVisibilityDcs],
|
||||
"groundunit-sam": olButtonsVisibilityGroundunitSam,
|
||||
groundunit: olButtonsVisibilityGroundunit,
|
||||
navyunit: olButtonsVisibilityNavyunit,
|
||||
}).map((entry, idx) => {
|
||||
return (
|
||||
<div className="flex justify-between" key={idx}>
|
||||
<span className="font-light text-white">{entry[1][0] as string}</span>
|
||||
<OlToggle
|
||||
key={entry[0]}
|
||||
onClick={() => {
|
||||
selectionFilter["control"][entry[0]] = !selectionFilter["control"][entry[0]];
|
||||
setSelectionFilter(deepCopyTable(selectionFilter));
|
||||
}}
|
||||
toggled={selectionFilter["control"][entry[0]]}
|
||||
/>
|
||||
</div>
|
||||
<tr key={idx}>
|
||||
<td className="w-16 text-lg text-gray-200">
|
||||
<FontAwesomeIcon icon={entry[1]} />
|
||||
</td>
|
||||
{["blue", "neutral", "red"].map((coalition) => {
|
||||
return (
|
||||
<td className="w-32 text-center" key={coalition}>
|
||||
<OlCheckbox
|
||||
checked={
|
||||
(appSubState === ImportExportSubstate.EXPORT &&
|
||||
selectionFilter[coalition][entry[0]] &&
|
||||
selectableUnits.find((unit) => unit.getMarkerCategory() === entry[0] && unit.getCoalition() === coalition) !== undefined) ||
|
||||
(appSubState === ImportExportSubstate.IMPORT &&
|
||||
selectionFilter[coalition][entry[0]] &&
|
||||
Object.values(importData).find((group) =>
|
||||
group.find((unit) => unit.markerCategory === entry[0] && unit.coalition === coalition)
|
||||
) !== undefined)
|
||||
}
|
||||
disabled={
|
||||
(appSubState === ImportExportSubstate.EXPORT &&
|
||||
selectableUnits.find((unit) => unit.getMarkerCategory() === entry[0] && unit.getCoalition() === coalition) === undefined) ||
|
||||
(appSubState === ImportExportSubstate.IMPORT &&
|
||||
Object.values(importData).find((group) =>
|
||||
group.find((unit) => unit.markerCategory === entry[0] && unit.coalition === coalition)
|
||||
) === undefined)
|
||||
}
|
||||
onChange={() => {
|
||||
selectionFilter[coalition][entry[0]] = !selectionFilter[coalition][entry[0]];
|
||||
setSelectionFilter(deepCopyTable(selectionFilter));
|
||||
}}
|
||||
/>
|
||||
<span className="absolute ml-2 text-white">
|
||||
{appSubState === ImportExportSubstate.EXPORT &&
|
||||
selectableUnits.filter((unit) => unit.getMarkerCategory() === entry[0] && unit.getCoalition() === coalition).length}
|
||||
{appSubState === ImportExportSubstate.IMPORT &&
|
||||
Object.values(importData)
|
||||
.flatMap((unit) => unit)
|
||||
.filter((unit) => unit.markerCategory === entry[0] && unit.coalition === coalition).length}{" "}
|
||||
units{" "}
|
||||
</span>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
text-bold border-b-2 border-b-white/10 pb-2 text-gray-400
|
||||
`}
|
||||
>
|
||||
Types and coalitions
|
||||
</div>
|
||||
|
||||
<table className="mr-16">
|
||||
<tbody>
|
||||
{
|
||||
<tr>
|
||||
<td></td>
|
||||
<td className="pb-4 text-center font-bold text-blue-500">BLUE</td>
|
||||
<td className="pb-4 text-center font-bold text-gray-500">NEUTRAL</td>
|
||||
<td className="pb-4 text-center font-bold text-red-500">RED</td>
|
||||
<td className="text-gray-200"></td>
|
||||
<td className="text-center">
|
||||
<OlCheckbox
|
||||
checked={Object.values(selectionFilter["blue"]).some((value) => value)}
|
||||
onChange={() => {
|
||||
const newValue = !Object.values(selectionFilter["blue"]).some((value) => value);
|
||||
Object.keys(selectionFilter["blue"]).forEach((key) => {
|
||||
selectionFilter["blue"][key] = newValue;
|
||||
});
|
||||
setSelectionFilter(deepCopyTable(selectionFilter));
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<OlCheckbox
|
||||
checked={Object.values(selectionFilter["neutral"]).some((value) => value)}
|
||||
onChange={() => {
|
||||
const newValue = !Object.values(selectionFilter["neutral"]).some((value) => value);
|
||||
Object.keys(selectionFilter["neutral"]).forEach((key) => {
|
||||
selectionFilter["neutral"][key] = newValue;
|
||||
});
|
||||
setSelectionFilter(deepCopyTable(selectionFilter));
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<OlCheckbox
|
||||
checked={Object.values(selectionFilter["red"]).some((value) => value)}
|
||||
onChange={() => {
|
||||
const newValue = !Object.values(selectionFilter["red"]).some((value) => value);
|
||||
Object.keys(selectionFilter["red"]).forEach((key) => {
|
||||
selectionFilter["red"][key] = newValue;
|
||||
});
|
||||
setSelectionFilter(deepCopyTable(selectionFilter));
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{Object.entries({
|
||||
"groundunit-sam": olButtonsVisibilityGroundunitSam,
|
||||
groundunit: olButtonsVisibilityGroundunit,
|
||||
navyunit: olButtonsVisibilityNavyunit,
|
||||
}).map((entry, idx) => {
|
||||
return (
|
||||
<tr key={idx}>
|
||||
<td className="w-16 text-lg text-gray-200">
|
||||
<FontAwesomeIcon icon={entry[1]} />
|
||||
</td>
|
||||
{["blue", "neutral", "red"].map((coalition) => {
|
||||
return (
|
||||
<td className="w-32 text-center" key={coalition}>
|
||||
<OlCheckbox
|
||||
checked={
|
||||
(appSubState === ImportExportSubstate.EXPORT &&
|
||||
selectionFilter[coalition][entry[0]] &&
|
||||
selectableUnits.find((unit) => unit.getMarkerCategory() === entry[0] && unit.getCoalition() === coalition) !== undefined) ||
|
||||
(appSubState === ImportExportSubstate.IMPORT &&
|
||||
selectionFilter[coalition][entry[0]] &&
|
||||
Object.values(importData).find((group) =>
|
||||
group.find((unit) => unit.markerCategory === entry[0] && unit.coalition === coalition)
|
||||
) !== undefined)
|
||||
}
|
||||
disabled={
|
||||
(appSubState === ImportExportSubstate.EXPORT &&
|
||||
selectableUnits.find((unit) => unit.getMarkerCategory() === entry[0] && unit.getCoalition() === coalition) === undefined) ||
|
||||
(appSubState === ImportExportSubstate.IMPORT &&
|
||||
Object.values(importData).find((group) =>
|
||||
group.find((unit) => unit.markerCategory === entry[0] && unit.coalition === coalition)
|
||||
) === undefined)
|
||||
}
|
||||
onChange={() => {
|
||||
selectionFilter[coalition][entry[0]] = !selectionFilter[coalition][entry[0]];
|
||||
setSelectionFilter(deepCopyTable(selectionFilter));
|
||||
}}
|
||||
/>
|
||||
<span className="absolute ml-2 text-white">
|
||||
{appSubState === ImportExportSubstate.EXPORT &&
|
||||
selectableUnits.filter((unit) => unit.getMarkerCategory() === entry[0] && unit.getCoalition() === coalition).length}
|
||||
{appSubState === ImportExportSubstate.IMPORT &&
|
||||
Object.values(importData)
|
||||
.flatMap((unit) => unit)
|
||||
.filter((unit) => unit.markerCategory === entry[0] && unit.coalition === coalition).length}{" "}
|
||||
units{" "}
|
||||
</span>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{
|
||||
<tr>
|
||||
<td className="text-gray-200"></td>
|
||||
<td className="text-center">
|
||||
<OlCheckbox
|
||||
checked={Object.values(selectionFilter["blue"]).some((value) => value)}
|
||||
onChange={() => {
|
||||
const newValue = !Object.values(selectionFilter["blue"]).some((value) => value);
|
||||
Object.keys(selectionFilter["blue"]).forEach((key) => {
|
||||
selectionFilter["blue"][key] = newValue;
|
||||
});
|
||||
setSelectionFilter(deepCopyTable(selectionFilter));
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<OlCheckbox
|
||||
checked={Object.values(selectionFilter["neutral"]).some((value) => value)}
|
||||
onChange={() => {
|
||||
const newValue = !Object.values(selectionFilter["neutral"]).some((value) => value);
|
||||
Object.keys(selectionFilter["neutral"]).forEach((key) => {
|
||||
selectionFilter["neutral"][key] = newValue;
|
||||
});
|
||||
setSelectionFilter(deepCopyTable(selectionFilter));
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<OlCheckbox
|
||||
checked={Object.values(selectionFilter["red"]).some((value) => value)}
|
||||
onChange={() => {
|
||||
const newValue = !Object.values(selectionFilter["red"]).some((value) => value);
|
||||
Object.keys(selectionFilter["red"]).forEach((key) => {
|
||||
selectionFilter["red"][key] = newValue;
|
||||
});
|
||||
setSelectionFilter(deepCopyTable(selectionFilter));
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (appSubState === ImportExportSubstate.EXPORT) {
|
||||
var unitsToExport: { [key: string]: any } = {};
|
||||
selectableUnits
|
||||
.filter((unit) => selectionFilter[unit.getCoalition()][unit.getMarkerCategory()])
|
||||
.forEach((unit: Unit) => {
|
||||
var data: any = unit.getData();
|
||||
if (unit.getGroupName() in unitsToExport) unitsToExport[unit.getGroupName()].push(data);
|
||||
else unitsToExport[unit.getGroupName()] = [data];
|
||||
});
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (appSubState === ImportExportSubstate.EXPORT) {
|
||||
var unitsToExport: { [key: string]: any } = {};
|
||||
selectableUnits
|
||||
.filter((unit) => selectionFilter[unit.getCoalition()][unit.getMarkerCategory()])
|
||||
.forEach((unit: Unit) => {
|
||||
var data: any = unit.getData();
|
||||
if (unit.getGroupName() in unitsToExport) unitsToExport[unit.getGroupName()].push(data);
|
||||
else unitsToExport[unit.getGroupName()] = [data];
|
||||
});
|
||||
|
||||
const file = new Blob([JSON.stringify(unitsToExport)], {
|
||||
type: "text/plain",
|
||||
});
|
||||
|
||||
const defaultName = "olympus_export_" + missionData.mission.theatre + "_" + missionData.sessionHash + "_" + missionData.time + ".json";
|
||||
|
||||
//@ts-ignore TODO
|
||||
if (window.showSaveFilePicker) {
|
||||
const opts = {
|
||||
types: [
|
||||
{
|
||||
description: "DCS Olympus export file",
|
||||
accept: { "text/plain": [".json"] },
|
||||
},
|
||||
],
|
||||
suggestedName: defaultName,
|
||||
};
|
||||
//@ts-ignore TODO
|
||||
showSaveFilePicker(opts)
|
||||
.then((handle) => handle.createWritable())
|
||||
.then((writeable) => {
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
getApp().addInfoMessage("Exporting data please wait...");
|
||||
writeable
|
||||
.write(file)
|
||||
.then(() => writeable.close())
|
||||
.catch(() => getApp().addInfoMessage("An error occurred while exporting the data"));
|
||||
})
|
||||
.then(() => getApp().addInfoMessage("Data exported correctly"))
|
||||
.catch((err) => getApp().addInfoMessage("An error occurred while exporting the data"));
|
||||
} else {
|
||||
const a = document.createElement("a");
|
||||
const file = new Blob([JSON.stringify(unitsToExport)], {
|
||||
type: "text/plain",
|
||||
});
|
||||
a.href = URL.createObjectURL(file);
|
||||
|
||||
const defaultName = "olympus_export_" + missionData.mission.theatre + "_" + missionData.sessionHash + "_" + missionData.time + ".json";
|
||||
|
||||
//@ts-ignore TODO
|
||||
if (window.showSaveFilePicker) {
|
||||
const opts = {
|
||||
types: [
|
||||
{
|
||||
description: "DCS Olympus export file",
|
||||
accept: { "text/plain": [".json"] },
|
||||
},
|
||||
],
|
||||
suggestedName: defaultName,
|
||||
};
|
||||
//@ts-ignore TODO
|
||||
showSaveFilePicker(opts)
|
||||
.then((handle) => handle.createWritable())
|
||||
.then((writeable) => {
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
getApp().addInfoMessage("Exporting data please wait...");
|
||||
writeable
|
||||
.write(file)
|
||||
.then(() => writeable.close())
|
||||
.catch(() => getApp().addInfoMessage("An error occurred while exporting the data"));
|
||||
})
|
||||
.then(() => getApp().addInfoMessage("Data exported correctly"))
|
||||
.catch((err) => getApp().addInfoMessage("An error occurred while exporting the data"));
|
||||
} else {
|
||||
const a = document.createElement("a");
|
||||
const file = new Blob([JSON.stringify(unitsToExport)], {
|
||||
type: "text/plain",
|
||||
});
|
||||
a.href = URL.createObjectURL(file);
|
||||
|
||||
var filename = defaultName;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
}
|
||||
} else {
|
||||
for (const [groupName, groupData] of Object.entries(importData)) {
|
||||
if (groupName === "" || groupData.length === 0 || !groupData.some((unitData: UnitData) => unitData.alive)) continue;
|
||||
|
||||
let { markerCategory, coalition, category } = groupData[0];
|
||||
|
||||
if (selectionFilter[coalition][markerCategory] !== true) continue;
|
||||
|
||||
let unitsToSpawn = groupData.map((unitData: UnitData) => {
|
||||
return { unitType: unitData.name, location: unitData.position, liveryID: "", skill: "High" };
|
||||
});
|
||||
|
||||
getApp().getUnitsManager().spawnUnits(category.toLocaleLowerCase(), unitsToSpawn, coalition, false);
|
||||
}
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
var filename = defaultName;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
}
|
||||
}}
|
||||
className={`
|
||||
mb-2 me-2 ml-auto flex content-center items-center gap-2
|
||||
rounded-sm bg-blue-700 px-5 py-2.5 text-sm font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Continue
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</button>
|
||||
} else {
|
||||
for (const [groupName, groupData] of Object.entries(importData)) {
|
||||
if (groupName === "" || groupData.length === 0 || !groupData.some((unitData: UnitData) => unitData.alive)) continue;
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().setState(OlympusState.IDLE)}
|
||||
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>
|
||||
let { markerCategory, coalition, category } = groupData[0];
|
||||
|
||||
if (selectionFilter[coalition][markerCategory] !== true) continue;
|
||||
|
||||
let unitsToSpawn = groupData.map((unitData: UnitData) => {
|
||||
return { unitType: unitData.name, location: unitData.position, liveryID: "", skill: "High" };
|
||||
});
|
||||
|
||||
getApp().getUnitsManager().spawnUnits(category.toLocaleLowerCase(), unitsToSpawn, coalition, false);
|
||||
}
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
}
|
||||
}}
|
||||
className={`
|
||||
mb-2 me-2 ml-auto flex content-center items-center gap-2 rounded-sm
|
||||
bg-blue-700 px-5 py-2.5 text-sm font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Continue
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().setState(OlympusState.IDLE)}
|
||||
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>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@ -61,99 +61,91 @@ export function KeybindModal(props: { open: boolean }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={props.open}
|
||||
className={`
|
||||
inline-flex h-fit w-[600px] overflow-y-auto scroll-smooth bg-white p-10
|
||||
dark:bg-olympus-800
|
||||
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
|
||||
max-md:border-none
|
||||
`}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col gap-4">
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
{shortcut?.getOptions().label}
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
Press the key you want to bind to this event
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full text-center text-white">
|
||||
{ctrlKey && "Ctrl + "}
|
||||
{altKey && "Alt + "}
|
||||
{shiftKey && "Shift + "}
|
||||
{code}
|
||||
</div>
|
||||
<div className="text-white">
|
||||
{available === true && <div className="text-green-600">Keybind is free!</div>}
|
||||
{available === false && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
Keybind is already in use: <div className={`
|
||||
flex flex-wrap gap-2 font-bold text-orange-600
|
||||
`}>{inUseShortcuts.map((shortcut) => <span>{shortcut.getOptions().label}</span>)}</div>
|
||||
<Modal open={props.open}>
|
||||
<div className={`flex flex-col gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
{shortcut?.getOptions().label}
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-400
|
||||
`}
|
||||
>
|
||||
Press the key you want to bind to this event
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full text-center text-white">
|
||||
{ctrlKey && "Ctrl + "}
|
||||
{altKey && "Alt + "}
|
||||
{shiftKey && "Shift + "}
|
||||
{code}
|
||||
</div>
|
||||
<div className="text-white">
|
||||
{available === true && <div className="text-green-600">Keybind is free!</div>}
|
||||
{available === false && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
Keybind is already in use:{" "}
|
||||
<div
|
||||
className={`flex flex-wrap gap-2 font-bold text-orange-600`}
|
||||
>
|
||||
{inUseShortcuts.map((shortcut) => (
|
||||
<span>{shortcut.getOptions().label}</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-gray-500">A key combination can be assigned to multiple actions, and all bound actions will fire</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
{shortcut && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (shortcut && code) {
|
||||
let options = shortcut.getOptions();
|
||||
options.code = code;
|
||||
options.altKey = altKey;
|
||||
options.shiftKey = shiftKey;
|
||||
options.ctrlKey = ctrlKey;
|
||||
getApp().getShortcutManager().setShortcutOption(shortcut.getId(), options);
|
||||
|
||||
getApp().setState(OlympusState.OPTIONS);
|
||||
}
|
||||
}}
|
||||
className={`
|
||||
mb-2 me-2 ml-auto flex content-center items-center gap-2
|
||||
rounded-sm bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Continue
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</button>
|
||||
)}
|
||||
<div className="text-gray-400">A key combination can be assigned to multiple actions, and all bound actions will fire</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
{shortcut && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().setState(OlympusState.OPTIONS)}
|
||||
onClick={() => {
|
||||
if (shortcut && code) {
|
||||
let options = shortcut.getOptions();
|
||||
options.code = code;
|
||||
options.altKey = altKey;
|
||||
options.shiftKey = shiftKey;
|
||||
options.ctrlKey = ctrlKey;
|
||||
getApp().getShortcutManager().setShortcutOption(shortcut.getId(), options);
|
||||
|
||||
getApp().setState(OlympusState.OPTIONS);
|
||||
}
|
||||
}}
|
||||
className={`
|
||||
mb-2 me-2 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
|
||||
mb-2 me-2 ml-auto flex content-center items-center gap-2
|
||||
rounded-sm bg-blue-700 px-5 py-2.5 text-sm font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Back
|
||||
Continue
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().setState(OlympusState.OPTIONS)}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
|
||||
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium text-white
|
||||
dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400
|
||||
dark:hover:bg-gray-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@ -80,346 +80,301 @@ export function LoginModal(props: { open: boolean }) {
|
||||
useEffect(subStateCallback, [subState]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={props.open}
|
||||
className={`
|
||||
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="images/splash/1.jpg"
|
||||
className={`contents-center w-full object-cover opacity-[7%]`}
|
||||
></img>
|
||||
<Modal open={props.open}>
|
||||
<div
|
||||
className={`
|
||||
absolute h-full w-full bg-gradient-to-r from-blue-200/25
|
||||
to-transparent
|
||||
`}
|
||||
></div>
|
||||
<div
|
||||
className={`
|
||||
absolute h-full w-full bg-gradient-to-t from-olympus-800
|
||||
to-transparent
|
||||
`}
|
||||
></div>
|
||||
<div
|
||||
className={`
|
||||
absolute flex w-full flex-col gap-8 p-16
|
||||
max-lg:p-8
|
||||
flex w-full flex-row gap-6
|
||||
max-lg:flex-col
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex w-full flex-row gap-6
|
||||
max-lg:flex-col
|
||||
flex w-[40%] flex-grow flex-col content-center justify-start gap-5
|
||||
max-lg:w-[100%]
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex w-[40%] flex-grow flex-col content-center justify-start gap-5
|
||||
max-lg:w-[100%]
|
||||
`}
|
||||
>
|
||||
{!checkingPassword ? (
|
||||
<>
|
||||
<div className="flex flex-col items-start">
|
||||
<div
|
||||
className={`
|
||||
pt-1 text-xs text-gray-800
|
||||
dark:text-gray-400
|
||||
`}
|
||||
>
|
||||
Connect to
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center gap-2 text-gray-800
|
||||
text-md font-bold
|
||||
dark:text-gray-200
|
||||
`}
|
||||
>
|
||||
{window.location.toString()}
|
||||
</div>
|
||||
{!checkingPassword ? (
|
||||
<>
|
||||
<div className="flex flex-col items-start">
|
||||
<div
|
||||
className={`
|
||||
pt-1 text-xs text-gray-800
|
||||
dark:text-gray-400
|
||||
`}
|
||||
>
|
||||
Connect to
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex w-[100%] flex-row content-center items-center gap-2
|
||||
flex items-center justify-center gap-2 text-gray-800 text-md
|
||||
font-bold
|
||||
dark:text-gray-200
|
||||
`}
|
||||
>
|
||||
<span className="size-[80px] min-w-14">
|
||||
<img
|
||||
src="images/olympus-500x500.png"
|
||||
className={`flex w-full`}
|
||||
></img>
|
||||
</span>
|
||||
<div className={`flex flex-col items-start gap-1`}>
|
||||
<h1
|
||||
className={`
|
||||
flex text-wrap text-4xl font-bold text-gray-800
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
DCS Olympus
|
||||
</h1>
|
||||
<div
|
||||
className={`
|
||||
flex select-none content-center gap-2 rounded-sm text-sm
|
||||
font-semibold text-green-700
|
||||
dark:text-green-400
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheckCircle} className={`my-auto`} />
|
||||
Version {VERSION}
|
||||
</div>
|
||||
{window.location.toString()}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex w-[100%] flex-row content-center items-center gap-2
|
||||
`}
|
||||
>
|
||||
<span className="size-[80px] min-w-14">
|
||||
<img src="images/olympus-500x500.png" className={`flex w-full`}></img>
|
||||
</span>
|
||||
<div className={`flex flex-col items-start gap-1`}>
|
||||
<h1
|
||||
className={`
|
||||
flex text-wrap text-4xl font-bold text-gray-800
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
DCS Olympus
|
||||
</h1>
|
||||
<div
|
||||
className={`
|
||||
flex select-none content-center gap-2 rounded-sm text-sm
|
||||
font-semibold text-green-700
|
||||
dark:text-green-400
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheckCircle} className={`my-auto`} />
|
||||
Version {VERSION}
|
||||
</div>
|
||||
</div>
|
||||
{!loginError ? (
|
||||
<>
|
||||
{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
|
||||
/>
|
||||
</div>
|
||||
{!loginError ? (
|
||||
<>
|
||||
{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
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
onChange={(ev) => setPassword(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 password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700
|
||||
dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4
|
||||
focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Login
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
{/*
|
||||
<label
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
onChange={(ev) => setPassword(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 password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700
|
||||
dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Login
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
{/*
|
||||
<button type="button" className="flex content-center items-center gap-2 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-blue-800 rounded-sm dark:border-gray-600 border-[1px] dark:text-gray-400">
|
||||
View Guide <FontAwesomeIcon className="my-auto text-xs" icon={faExternalLink} />
|
||||
</button>
|
||||
*/}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{subState === LoginSubState.COMMAND_MODE && (
|
||||
<>
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<label
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Choose your role
|
||||
</label>
|
||||
<OlDropdown
|
||||
label={activeCommandMode ?? ""}
|
||||
className={`w-48`}
|
||||
>
|
||||
{commandModes?.map((commandMode) => {
|
||||
return <OlDropdownItem onClick={() => setActiveCommandMode(commandMode)}>{commandMode}</OlDropdownItem>;
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700
|
||||
dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4
|
||||
focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Continue
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ErrorCallout
|
||||
title="Server could not be reached or password is incorrect"
|
||||
description="The Olympus Server at this address could not be reached or the password is incorrect. Check your password. If correct, check the address is correct, restart the Olympus server or reinstall Olympus. Ensure the ports set are not already used."
|
||||
></ErrorCallout>
|
||||
<div className={`text-sm font-medium text-gray-200`}>
|
||||
Still having issues? See our{" "}
|
||||
<a
|
||||
href=""
|
||||
className={`
|
||||
text-blue-300 underline
|
||||
hover:no-underline
|
||||
`}
|
||||
>
|
||||
troubleshooting guide here
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={`
|
||||
mx-auto my-auto w-40 animate-spin fill-blue-600
|
||||
text-gray-200
|
||||
dark:text-gray-600
|
||||
`}
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex flex-grow flex-row content-end justify-center gap-3
|
||||
overflow-hidden
|
||||
max-lg:justify-start
|
||||
max-md:flex-col
|
||||
`}
|
||||
>
|
||||
<Card className="flex">
|
||||
<img
|
||||
src="images/splash/1.jpg"
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{subState === LoginSubState.COMMAND_MODE && (
|
||||
<>
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<label
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Choose your role
|
||||
</label>
|
||||
<OlDropdown label={activeCommandMode ?? ""} className={`
|
||||
w-48
|
||||
`}>
|
||||
{commandModes?.map((commandMode) => {
|
||||
return <OlDropdownItem onClick={() => setActiveCommandMode(commandMode)}>{commandMode}</OlDropdownItem>;
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700
|
||||
dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Continue
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ErrorCallout
|
||||
title="Server could not be reached or password is incorrect"
|
||||
description="The Olympus Server at this address could not be reached or the password is incorrect. Check your password. If correct, check the address is correct, restart the Olympus server or reinstall Olympus. Ensure the ports set are not already used."
|
||||
></ErrorCallout>
|
||||
<div className={`text-sm font-medium text-gray-200`}>
|
||||
Still having issues? See our{" "}
|
||||
<a
|
||||
href=""
|
||||
className={`
|
||||
text-blue-300 underline
|
||||
hover:no-underline
|
||||
`}
|
||||
>
|
||||
troubleshooting guide here
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={`
|
||||
h-[40%] max-h-[120px] contents-center w-full rounded-md
|
||||
object-cover
|
||||
`}
|
||||
></img>
|
||||
<div
|
||||
className={`
|
||||
mt-2 flex content-center items-center gap-2 font-bold
|
||||
mx-auto my-auto w-40 animate-spin fill-blue-600 text-gray-200
|
||||
dark:text-gray-600
|
||||
`}
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
YouTube Video Guide
|
||||
<FontAwesomeIcon className={`my-auto text-xs text-gray-400`} icon={faExternalLink} />
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
overflow-hidden text-ellipsis text-xs text-black
|
||||
dark:text-gray-400
|
||||
`}
|
||||
>
|
||||
Check out our official video tutorial on how to get started with Olympus - so you can immediately start controlling the battlefield.
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="flex">
|
||||
<img
|
||||
src="images/splash/1.jpg"
|
||||
className={`
|
||||
h-[40%] max-h-[120px] contents-center w-full rounded-md
|
||||
object-cover
|
||||
`}
|
||||
></img>
|
||||
<div
|
||||
className={`
|
||||
mt-2 flex content-center items-center gap-2 font-bold
|
||||
`}
|
||||
>
|
||||
Wiki Guide
|
||||
<FontAwesomeIcon className={`my-auto text-xs text-gray-400`} icon={faExternalLink} />
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
overflow-hidden text-ellipsis text-xs text-black
|
||||
dark:text-gray-400
|
||||
`}
|
||||
>
|
||||
Find out more about Olympus through our online wiki guide.
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex h-full w-full text-xs font-light text-gray-600
|
||||
max-lg:flex-col
|
||||
flex flex-grow flex-row content-end justify-center gap-3
|
||||
overflow-hidden
|
||||
max-lg:justify-start
|
||||
max-md:flex-col
|
||||
`}
|
||||
>
|
||||
DCS Olympus (the "MATERIAL" or "Software") is provided completely free to users subject to the terms of the CC BY-NC-SA 4.0 Licence except where such
|
||||
terms conflict with this disclaimer, in which case, the terms of this disclaimer shall prevail. Any party making use of the Software in any manner
|
||||
agrees to be bound by the terms set out in the disclaimer. THIS MATERIAL IS NOT MADE OR SUPPORTED BY EAGLE DYNAMICS SA.
|
||||
<Card className="flex">
|
||||
<img
|
||||
src="images/splash/1.jpg"
|
||||
className={`
|
||||
h-[40%] max-h-[120px] contents-center w-full rounded-md
|
||||
object-cover
|
||||
`}
|
||||
></img>
|
||||
<div
|
||||
className={`mt-2 flex content-center items-center gap-2 font-bold`}
|
||||
>
|
||||
YouTube Video Guide
|
||||
<FontAwesomeIcon className={`my-auto text-xs text-gray-400`} icon={faExternalLink} />
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
overflow-hidden text-ellipsis text-xs text-black
|
||||
dark:text-gray-400
|
||||
`}
|
||||
>
|
||||
Check out our official video tutorial on how to get started with Olympus - so you can immediately start controlling the battlefield.
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="flex">
|
||||
<img
|
||||
src="images/splash/1.jpg"
|
||||
className={`
|
||||
h-[40%] max-h-[120px] contents-center w-full rounded-md
|
||||
object-cover
|
||||
`}
|
||||
></img>
|
||||
<div
|
||||
className={`mt-2 flex content-center items-center gap-2 font-bold`}
|
||||
>
|
||||
Wiki Guide
|
||||
<FontAwesomeIcon className={`my-auto text-xs text-gray-400`} icon={faExternalLink} />
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
overflow-hidden text-ellipsis text-xs text-black
|
||||
dark:text-gray-400
|
||||
`}
|
||||
>
|
||||
Find out more about Olympus through our online wiki guide.
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex h-full w-full text-xs font-light text-gray-600
|
||||
max-lg:flex-col
|
||||
`}
|
||||
>
|
||||
DCS Olympus (the "MATERIAL" or "Software") is provided completely free to users subject to the terms of the CC BY-NC-SA 4.0 Licence except where such
|
||||
terms conflict with this disclaimer, in which case, the terms of this disclaimer shall prevail. Any party making use of the Software in any manner
|
||||
agrees to be bound by the terms set out in the disclaimer. THIS MATERIAL IS NOT MADE OR SUPPORTED BY EAGLE DYNAMICS SA.
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@ -10,14 +10,8 @@ export function ProtectionPromptModal(props: { open: boolean }) {
|
||||
return (
|
||||
<Modal
|
||||
open={props.open}
|
||||
className={`
|
||||
inline-flex h-fit w-[600px] overflow-y-auto scroll-smooth bg-white p-10
|
||||
dark:bg-olympus-800
|
||||
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
|
||||
max-md:border-none
|
||||
`}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col gap-12">
|
||||
<div className="flex h-full w-full flex-col justify-between gap-12">
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
@ -30,7 +24,7 @@ export function ProtectionPromptModal(props: { open: boolean }) {
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
dark:text-gray-400
|
||||
`}
|
||||
>
|
||||
Pressing "Continue" will cause all DCS controlled units in the current selection to abort their mission and start following Olympus commands only.
|
||||
@ -38,7 +32,7 @@ export function ProtectionPromptModal(props: { open: boolean }) {
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
dark:text-gray-400
|
||||
`}
|
||||
>
|
||||
If you are trying to delete a human player unit, they will be killed and de-slotted. Be careful!
|
||||
@ -46,7 +40,7 @@ export function ProtectionPromptModal(props: { open: boolean }) {
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
dark:text-gray-400
|
||||
`}
|
||||
>
|
||||
To disable this warning, press on the{" "}
|
||||
|
||||
526
frontend/react/src/ui/modals/trainingmodal.tsx
Normal file
@ -0,0 +1,526 @@
|
||||
import React, { useState } from "react";
|
||||
import { Modal } from "./components/modal";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowLeft, faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FaCaretRight, FaLink } from "react-icons/fa6";
|
||||
import { FaQuestionCircle } from "react-icons/fa";
|
||||
|
||||
const MAX_STEPS = 10;
|
||||
|
||||
export function TrainingModal(props: { open: boolean }) {
|
||||
const [step, setStep] = useState(0);
|
||||
|
||||
return (
|
||||
<Modal open={props.open}>
|
||||
<div>
|
||||
<h1 className={`text-2xl font-semibold text-white`}>DCS Olympus guided tour</h1>
|
||||
|
||||
</div>
|
||||
|
||||
<>
|
||||
{step === 0 && (
|
||||
<div className="flex gap-16">
|
||||
<img
|
||||
src="images/olympus-500x500.png"
|
||||
className={`my-auto h-40 w-40 rounded-xl`}
|
||||
/>
|
||||
<div className="flex flex-col gap-4 text-gray-400">
|
||||
<h2 className={`text-xl font-semibold text-white`}>Home</h2>
|
||||
<p>
|
||||
Welcome to the Olympus quick start guide! This tour will guide you through the basics of DCS Olympus. You can navigate through the steps using
|
||||
the "Next" and "Previous" buttons at the bottom of the screen, or select a topic from the list below.
|
||||
</p>
|
||||
<div className="flex flex-col flex-wrap gap-2">
|
||||
<div className="flex gap-2">
|
||||
<FaLink className="my-auto" />
|
||||
<a href="#" className={`text-blue-400`} onClick={() => setStep(1)}>
|
||||
Main navbar
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<FaLink className="my-auto" />
|
||||
<a href="#" className={`text-blue-400`} onClick={() => setStep(2)}>
|
||||
Spawning units
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<FaLink className="my-auto" />
|
||||
<a href="#" className={`text-blue-400`} onClick={() => setStep(5)}>
|
||||
Controlling units
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<FaLink className="my-auto" />
|
||||
<a href="#" className={`text-blue-400`} onClick={() => setStep(9)}>
|
||||
The unit marker
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
Every panel has a dedicated integrated wiki. Click on the{" "}
|
||||
<span
|
||||
className={`
|
||||
mt-[-7px] inline-block translate-y-2 rounded-full p-1
|
||||
`}
|
||||
>
|
||||
<FaQuestionCircle />
|
||||
</span>{" "}
|
||||
symbol to access it. Moreover, most clickable content has tooltips providing info about their function.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
<>
|
||||
{step === 1 && (
|
||||
<div className="flex gap-16">
|
||||
<img
|
||||
src="images/training/step1.gif"
|
||||
className={`h-72 w-72 rounded-xl`}
|
||||
/>
|
||||
<div className="flex flex-col gap-4 text-gray-400">
|
||||
<h2 className={`text-xl font-semibold text-white`}>Main navbar</h2>
|
||||
<p>
|
||||
The main functions of DCS Olympus are accessible from the main navbar. You can access the spawn tool, the unit selection and control tool, the
|
||||
drawings tool, the audio/radio tool, and the game master options from here.
|
||||
</p>
|
||||
<p>On the bottom left corner, you can find the DCS Olympus options tool.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
<>
|
||||
{step === 2 && (
|
||||
<div className="flex gap-16">
|
||||
<img
|
||||
src="images/training/step2.gif"
|
||||
className={`h-72 w-72 rounded-xl`}
|
||||
/>
|
||||
<div className="flex flex-col gap-4 text-gray-400">
|
||||
<h2 className={`text-xl font-semibold text-white`}>Spawning units (1 of 3)</h2>
|
||||
<p>
|
||||
To spawn a unit, click on the spawn tool icon on the main navbar. This will open the spawn tool. You can select the unit you want to spawn by
|
||||
searching for it or by finding it in the list, which can be filtered by category.
|
||||
</p>
|
||||
<p>After selecting the unit you can edit its properties, like spawn altitude and heading, loadout, livery, skill level, and so on.</p>
|
||||
<p>
|
||||
Once you are happy with your setup, click on the map to spawn the unit. You can click multiple times to spawn more units. When you are done,
|
||||
double click to exit the spawning mode.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
<>
|
||||
{step === 3 && (
|
||||
<div className="flex gap-16">
|
||||
<img
|
||||
src="images/training/step3.gif"
|
||||
className={`h-72 w-72 rounded-xl`}
|
||||
/>
|
||||
<div className="flex flex-col gap-4 text-gray-400">
|
||||
<h2 className={`text-xl font-semibold text-white`}>Spawning units (2 of 3)</h2>
|
||||
<p>
|
||||
You can also spawn units directly on the map by right clicking on it and selecting the unit you want to spawn. This will spawn the unit at the
|
||||
clicked location.
|
||||
</p>
|
||||
<p>You can edit the unit properties like in the previous method.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
<>
|
||||
{step === 4 && (
|
||||
<div className="flex gap-16">
|
||||
<img
|
||||
src="images/training/step4.gif"
|
||||
className={`h-72 w-72 rounded-xl`}
|
||||
/>
|
||||
<div className="flex flex-col gap-4 text-gray-400">
|
||||
<h2 className={`text-xl font-semibold text-white`}>Spawning units (3 of 3)</h2>
|
||||
<p>
|
||||
If you plan on spawning many similar units throughout the mission, you can "star" a unit. This will save the unit in the starred units list,
|
||||
which can be accessed from the spawn tool. This way you can quickly spawn the same unit multiple times without having to search for it.
|
||||
</p>
|
||||
<p>You can edit the unit properties like in the previous method.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
<>
|
||||
{step === 5 && (
|
||||
<div className="flex gap-16">
|
||||
<img
|
||||
src="images/training/step5.gif"
|
||||
className={`h-72 w-72 rounded-xl`}
|
||||
/>
|
||||
<div className="flex flex-col gap-4 text-gray-400">
|
||||
<h2 className={`text-xl font-semibold text-white`}>Controlling units (1 of 4)</h2>
|
||||
<p>
|
||||
{" "}
|
||||
The most basic form of unit control is movement. A short right click on the map will add a destination point. If the ctrl key is held while
|
||||
right clicking, the destination will be appended, creating a path.
|
||||
</p>
|
||||
<p>
|
||||
Previously created destinations can be moved by dragging the marker on the map. If multiple units are selected when creating the path,
|
||||
destinations will be shared between them.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
<>
|
||||
{step === 6 && (
|
||||
<div className="flex gap-16">
|
||||
<img
|
||||
src="images/training/step6.gif"
|
||||
className={`h-72 w-72 rounded-xl`}
|
||||
/>
|
||||
<div className="flex flex-col gap-4 text-gray-400">
|
||||
<h2 className={`text-xl font-semibold text-white`}>Controlling units (2 of 4)</h2>
|
||||
<p>
|
||||
To issue an instruction to a unit, long press the right mouse button on the map. This will allow you to select an action, depending on what you
|
||||
clicked on.
|
||||
</p>
|
||||
<p></p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
<>
|
||||
{step === 7 && (
|
||||
<div className="flex gap-16">
|
||||
<img
|
||||
src="images/training/step7.gif"
|
||||
className={`h-72 w-72 rounded-xl`}
|
||||
/>
|
||||
<div className="flex flex-col gap-4 text-gray-400">
|
||||
<h2 className={`text-xl font-semibold text-white`}>Controlling units (3 of 4)</h2>
|
||||
<p>
|
||||
The same instructions can be issued using the unit control toolbar. First select the tool, then left click on the map to issue the instruction.
|
||||
The tool will be active until deselected, either by double clicking on the map, or by clicking on the tool button again.{" "}
|
||||
</p>
|
||||
<p>
|
||||
Tools can be enabled using keyboard shortcuts. To learn what a tool does and what shortcut enables it, place your cursor over the corresponding
|
||||
button.{" "}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
<>
|
||||
{step === 8 && (
|
||||
<div className="flex gap-16">
|
||||
<img
|
||||
src="images/training/step8.gif"
|
||||
className={`h-72 w-72 rounded-xl`}
|
||||
/>
|
||||
<div className="flex flex-col gap-4 text-gray-400">
|
||||
<h2 className={`text-xl font-semibold text-white`}>Controlling units (4 of 4)</h2>
|
||||
<p>
|
||||
{" "}
|
||||
Unit properties are set using the unit control menu, which opens automatically when a unit is selected. Here, depending on the selected unit,
|
||||
you can set altitude and speed, Rules Of Engagement, reaction to threat, as well as advanced settings like AWACS frequencies and so on.{" "}
|
||||
</p>
|
||||
<p> </p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
<>
|
||||
{step === 9 && (
|
||||
<div className="flex gap-16">
|
||||
<img
|
||||
src="images/training/unitmarker.png"
|
||||
className={`max-h-34 max-w-34 my-auto rounded-xl`}
|
||||
/>
|
||||
<div className="flex flex-col gap-4 text-gray-400">
|
||||
<h2 className={`text-xl font-semibold text-white`}>The unit marker (1 of 2)</h2>
|
||||
<p>
|
||||
The unit marker is a small icon that appears on the map. It shows the unit's type, coalition, and the name (for Mission Editor units and human
|
||||
players only). It has the following parts:
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-4">
|
||||
<div
|
||||
className={`
|
||||
rounded-full bg-gray-400 px-3 py-1 font-bold
|
||||
text-olympus-900
|
||||
`}
|
||||
>
|
||||
1
|
||||
</div>
|
||||
<p className="my-auto">Unit short label or type symbol</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-4">
|
||||
<div
|
||||
className={`
|
||||
rounded-full bg-gray-400 px-3 py-1 font-bold
|
||||
text-olympus-900
|
||||
`}
|
||||
>
|
||||
2
|
||||
</div>
|
||||
<p className="my-auto">Flight level</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-4">
|
||||
<div
|
||||
className={`
|
||||
rounded-full bg-gray-400 px-3 py-1 font-bold
|
||||
text-olympus-900
|
||||
`}
|
||||
>
|
||||
3
|
||||
</div>
|
||||
<p className="my-auto">Ground speed (knots)</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-4">
|
||||
<div
|
||||
className={`
|
||||
rounded-full bg-gray-400 px-3 py-1 font-bold
|
||||
text-olympus-900
|
||||
`}
|
||||
>
|
||||
4
|
||||
</div>
|
||||
<p className="my-auto">Bullseye position</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-4">
|
||||
<div
|
||||
className={`
|
||||
rounded-full bg-gray-400 px-3 py-1 font-bold
|
||||
text-olympus-900
|
||||
`}
|
||||
>
|
||||
5
|
||||
</div>
|
||||
<p className="my-auto">Fuel state (% of internal)</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-4">
|
||||
<div
|
||||
className={`
|
||||
rounded-full bg-gray-400 px-3 py-1 font-bold
|
||||
text-olympus-900
|
||||
`}
|
||||
>
|
||||
6
|
||||
</div>
|
||||
<p className="my-auto">A/A weapons (Fox 1/2/3 & guns)</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-4">
|
||||
<div
|
||||
className={`
|
||||
rounded-full bg-gray-400 px-3 py-1 font-bold
|
||||
text-olympus-900
|
||||
`}
|
||||
>
|
||||
7
|
||||
</div>
|
||||
<p className="my-auto">Current state</p>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Most of these information is only available for air units. Ground units will show the type symbol, the name, and the coalition, and the fuel
|
||||
level is replace by the unit's health (%).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
<>
|
||||
{step === 10 && (
|
||||
<div className="flex gap-16">
|
||||
<div className="flex flex-col gap-4 text-gray-400">
|
||||
<h2 className={`text-xl font-semibold text-white`}>The unit marker (2 of 2)</h2>
|
||||
<p>The unit marker has a symbol showing the unit state, i.e. what instruction it is performing. These are all the possible values:</p>
|
||||
<div className="flex max-h-40 flex-col flex-wrap gap-4">
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-2">
|
||||
<img src="images/states/attack.svg" />
|
||||
<p className={`my-auto`}>Attacking unit or ground</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-2">
|
||||
<img src="images/states/awacs.svg" />
|
||||
<p className={`my-auto`}>Operating as AWACS</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-2">
|
||||
<img src="images/states/dcs.svg" />
|
||||
<p className={`my-auto`}>Under DCS control</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-2">
|
||||
<img src="images/states/follow.svg" />
|
||||
<p className={`my-auto`}>Following unit</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-2">
|
||||
<img src="images/states/human.svg" />
|
||||
<p className={`my-auto`}>Human player</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-2">
|
||||
<img src="images/states/idle.svg" />
|
||||
<p className={`my-auto`}>Idle, orbiting</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-2">
|
||||
<img src="images/states/land-at-point.svg" />
|
||||
<p className={`my-auto`}>Landing at point (helicopter)</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-2">
|
||||
<img src="images/states/miss-on-purpose.svg" />
|
||||
<p className={`my-auto`}>Miss on purpose mode</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-2">
|
||||
<img src="images/states/no-task.svg" />
|
||||
<p className={`my-auto`}>No task, not controllable</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-2">
|
||||
<img src="images/states/off.svg" />
|
||||
<p className={`my-auto`}>Shut down</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-2">
|
||||
<img src="images/states/refuel.svg" />
|
||||
<p className={`my-auto`}>Refueling from tanker</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-2">
|
||||
<img src="images/states/rtb.svg" />
|
||||
<p className={`my-auto`}>RTB</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-2">
|
||||
<img src="images/states/scenic-aaa.svg" />
|
||||
<p className={`my-auto`}>Scenic AAA mode</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-2">
|
||||
<img src="images/states/simulate-fire-fight.svg" />
|
||||
<p className={`my-auto`}>Simulating fire fight</p>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-2">
|
||||
<img src="images/states/tanker.svg" />
|
||||
<p className={`my-auto`}>Operating as AAR tanker </p>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
<div className="mt-auto flex justify-between">
|
||||
{step > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep(step - 1)}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
|
||||
bg-blue-700 px-5 py-2.5 text-sm font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowLeft} />
|
||||
Previous
|
||||
</button>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
|
||||
{step > 0 && (
|
||||
<div className="my-auto flex gap-2">
|
||||
{[...Array(MAX_STEPS).keys()].map((i) => (
|
||||
<div
|
||||
key={i + 1}
|
||||
className={`
|
||||
h-4 w-4 rounded-full
|
||||
${i + 1 === step ? "bg-blue-700 shadow-white" : `
|
||||
bg-gray-300/10
|
||||
`}
|
||||
`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step < MAX_STEPS ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep(step + 1)}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
|
||||
bg-blue-700 px-5 py-2.5 text-sm font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Next
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {}}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
|
||||
bg-blue-700 px-5 py-2.5 text-sm font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Finish
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -28,10 +28,11 @@ export function WarningModal(props: { open: boolean }) {
|
||||
warningText = (
|
||||
<div className="flex flex-col gap-2 text-gray-400">
|
||||
<span>Non-Google Chrome Browser Detected.</span>
|
||||
<span>It appears you are using a browser other than Google Chrome.</span>
|
||||
<span>
|
||||
It appears you are using a browser other than Google Chrome.
|
||||
If you encounter any problems, we strongly suggest you use a Chrome based browser. Many features, especially advanced ones such as audio playback
|
||||
and capture, were developed specifically for Chrome based browsers.{" "}
|
||||
</span>
|
||||
<span>If you encounter any problems, we strongly suggest you use a Chrome based browser. Many features, especially advanced ones such as audio playback and capture, were developed specifically for Chrome based browsers. </span>
|
||||
<div className="mt-5 flex gap-4">
|
||||
<OlCheckbox
|
||||
checked={mapOptions.hideChromeWarning}
|
||||
@ -49,14 +50,15 @@ export function WarningModal(props: { open: boolean }) {
|
||||
warningText = (
|
||||
<div className="flex flex-col gap-2 text-gray-400">
|
||||
<span>Your connection to DCS Olympus is not secure.</span>
|
||||
<span>To protect your personal data some advanced DCS Olympus features like the camera plugin or the audio backend have been disabled.</span>
|
||||
<span>
|
||||
To protect your personal data some advanced DCS Olympus features like the camera plugin or the audio backend
|
||||
have been disabled.
|
||||
</span>
|
||||
<span>
|
||||
To solve this issue, DCS Olympus should be served using the <span className={`
|
||||
italic
|
||||
`}>https</span> protocol.
|
||||
To solve this issue, DCS Olympus should be served using the{" "}
|
||||
<span
|
||||
className={`italic`}
|
||||
>
|
||||
https
|
||||
</span>{" "}
|
||||
protocol.
|
||||
</span>
|
||||
<span>To do so, we suggest using a dedicated server and a reverse proxy with SSL enabled.</span>
|
||||
<div className="mt-5 flex gap-4">
|
||||
@ -77,38 +79,27 @@ export function WarningModal(props: { open: boolean }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={props.open}
|
||||
className={`
|
||||
inline-flex h-[50%] max-h-[600px] w-[40%] 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
|
||||
`}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col p-14">
|
||||
<div className="flex gap-2 text-xl text-white">
|
||||
<FaExclamationTriangle className={`my-auto text-4xl text-yellow-300`} />
|
||||
<div className="my-auto">Warning</div>
|
||||
</div>
|
||||
<div className="mt-10 text-white">{warningText}</div>
|
||||
<div className="ml-auto mt-auto flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().setState(OlympusState.IDLE)}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
|
||||
bg-blue-700 px-5 py-2.5 text-sm font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Continue
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
</div>
|
||||
<Modal open={props.open}>
|
||||
<div className="flex gap-2 text-xl text-white">
|
||||
<FaExclamationTriangle className={`my-auto text-4xl text-yellow-300`} />
|
||||
<div className="my-auto">Warning</div>
|
||||
</div>
|
||||
<div className="mt-10 text-white">{warningText}</div>
|
||||
<div className="ml-auto mt-auto flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().setState(OlympusState.IDLE)}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
|
||||
bg-blue-700 px-5 py-2.5 text-sm font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Continue
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Menu } from "./components/menu";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { FaPlus, FaQuestionCircle } from "react-icons/fa";
|
||||
import { FaExclamation, FaExclamationCircle, FaPlus, FaQuestionCircle } from "react-icons/fa";
|
||||
import { AudioSourcePanel } from "./components/sourcepanel";
|
||||
import { AudioSource } from "../../audio/audiosource";
|
||||
import { RadioSinkPanel } from "./components/radiosinkpanel";
|
||||
@ -117,15 +117,20 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
|
||||
|
||||
return (
|
||||
<Menu title="Audio menu" open={props.open} showBackButton={false} onClose={props.onClose}>
|
||||
<div className="p-4 text-sm text-gray-400">
|
||||
The audio menu allows you to add and manage audio sources, connect them to unit loudspeakers and radios, and to tune radio frequencies.
|
||||
<div className="flex content-center gap-4 p-4">
|
||||
<div className="my-auto text-gray-400">
|
||||
<FaQuestionCircle />
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
The audio menu allows you to add and manage audio sources, connect them to unit loudspeakers and radios, and to tune radio frequencies.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<>
|
||||
{!audioManagerEnabled && (
|
||||
<div className="mx-6 flex rounded-lg bg-olympus-400 p-4 text-sm">
|
||||
<div>
|
||||
<FaQuestionCircle className="my-4 ml-2 mr-6 text-gray-400" />
|
||||
<div className="mx-4 flex gap-4 rounded-lg bg-olympus-400 p-4 text-sm">
|
||||
<div className="my-auto animate-bounce text-xl">
|
||||
<FaExclamationCircle className="text-gray-400" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-gray-100">
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { faArrowLeft, faClose } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowLeft, faCircleQuestion, faClose } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FaChevronDown, FaChevronUp } from "react-icons/fa";
|
||||
@ -11,18 +11,22 @@ export function Menu(props: {
|
||||
onBack?: () => void;
|
||||
showBackButton?: boolean;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
wiki?: () => (JSX.Element | JSX.Element[]);
|
||||
}) {
|
||||
const [hide, setHide] = useState(true);
|
||||
const [wiki, setWiki] = useState(false);
|
||||
|
||||
if (!props.open && hide) setHide(false);
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
data-open={props.open}
|
||||
data-wiki={wiki}
|
||||
className={`
|
||||
pointer-events-none absolute left-16 right-0 top-[58px] z-10
|
||||
h-[calc(100vh-58px)] bg-transparent transition-transform
|
||||
h-[calc(100vh-58px)] bg-transparent transition-all ol-panel-container
|
||||
data-[open='false']:-translate-x-full
|
||||
data-[wiki='true']:w-[calc(100%-58px)] data-[wiki='true']:lg:w-[800px]
|
||||
sm:w-[400px]
|
||||
`}
|
||||
tabIndex={-1}
|
||||
@ -58,8 +62,8 @@ export function Menu(props: {
|
||||
)}
|
||||
{props.title}
|
||||
<FontAwesomeIcon
|
||||
onClick={props.onClose}
|
||||
icon={faClose}
|
||||
onClick={() => setWiki(!wiki)}
|
||||
icon={faCircleQuestion}
|
||||
className={`
|
||||
ml-auto flex cursor-pointer items-center justify-center rounded-md
|
||||
p-2 text-lg
|
||||
@ -67,23 +71,46 @@ export function Menu(props: {
|
||||
hover:bg-gray-200
|
||||
`}
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
onClick={props.onClose}
|
||||
icon={faClose}
|
||||
className={`
|
||||
flex cursor-pointer items-center justify-center rounded-md p-2
|
||||
text-lg
|
||||
dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white
|
||||
hover:bg-gray-200
|
||||
`}
|
||||
/>
|
||||
</h5>
|
||||
<div className="h-[calc(100%-3rem)]">
|
||||
{props.children}
|
||||
<div className="flex h-[calc(100%-3rem)]">
|
||||
<div data-wiki={wiki} className={`
|
||||
w-0 overflow-hidden transition-all
|
||||
data-[wiki='true']:w-[50%]
|
||||
`}>
|
||||
{props.wiki ? props.wiki() : <div className={`p-4 text-gray-200`}>Work in progress</div>}
|
||||
</div>
|
||||
<div data-wiki={wiki} className={`
|
||||
w-full
|
||||
sm:w-[400px]
|
||||
`}>{props.children}</div>
|
||||
</div>
|
||||
</div>
|
||||
{props.canBeHidden == true && (
|
||||
<div
|
||||
className={`
|
||||
pointer-events-auto flex h-8 justify-center backdrop-blur-lg
|
||||
backdrop-grayscale
|
||||
dark:bg-olympus-800/90
|
||||
pointer-events-auto flex h-8 cursor-pointer justify-center
|
||||
bg-olympus-800/90 backdrop-blur-lg backdrop-grayscale
|
||||
hover:bg-olympus-400/90
|
||||
`}
|
||||
onClick={() => setHide(!hide)}
|
||||
>
|
||||
{hide ? <FaChevronUp className="mx-auto my-auto text-gray-400" /> : <FaChevronDown className={`
|
||||
mx-auto my-auto text-gray-400
|
||||
`} />}
|
||||
{hide ? (
|
||||
<FaChevronUp className="mx-auto my-auto text-gray-400" />
|
||||
) : (
|
||||
<FaChevronDown
|
||||
className={`mx-auto my-auto text-gray-400`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,19 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { OlRoundStateButton, OlStateButton, OlLockStateButton } from "../components/olstatebutton";
|
||||
import { faSkull, faCamera, faFlag, faVolumeHigh, faDownload, faUpload, faDrawPolygon, faCircle, faTriangleExclamation, faWifi, faHourglass, faInfo } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faSkull,
|
||||
faCamera,
|
||||
faFlag,
|
||||
faVolumeHigh,
|
||||
faDownload,
|
||||
faUpload,
|
||||
faDrawPolygon,
|
||||
faCircle,
|
||||
faTriangleExclamation,
|
||||
faWifi,
|
||||
faHourglass,
|
||||
faInfo,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { OlDropdownItem, OlDropdown } from "../components/oldropdown";
|
||||
import { OlLabelToggle } from "../components/ollabeltoggle";
|
||||
import { getApp, IP } from "../../olympusapp";
|
||||
@ -35,7 +48,8 @@ import {
|
||||
RED_COMMANDER,
|
||||
} from "../../constants/constants";
|
||||
import { OlympusConfig } from "../../interfaces";
|
||||
import { FaCheck, FaSave, FaSpinner } from "react-icons/fa";
|
||||
import { FaCheck, FaQuestionCircle, FaSave, FaSpinner } from "react-icons/fa";
|
||||
import { OlExpandingTooltip } from "../components/olexpandingtooltip";
|
||||
|
||||
export function Header() {
|
||||
const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS);
|
||||
@ -84,10 +98,9 @@ export function Header() {
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
<div
|
||||
className={`
|
||||
z-10 flex w-full gap-4 border-gray-200 bg-gray-300 px-3 drop-shadow-md
|
||||
align-center
|
||||
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
|
||||
`}
|
||||
>
|
||||
@ -138,6 +151,13 @@ export function Header() {
|
||||
getApp().setState(OlympusState.IMPORT_EXPORT, ImportExportSubstate.EXPORT);
|
||||
}}
|
||||
checked={false}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Export scenario from file"
|
||||
content="Selectively export the current scenario to a file. This file can be shared with other users or imported later. Currently, only ground and naval units can be exported."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
/>
|
||||
<OlStateButton
|
||||
icon={faUpload}
|
||||
@ -145,6 +165,12 @@ export function Header() {
|
||||
getApp().setState(OlympusState.IMPORT_EXPORT, ImportExportSubstate.IMPORT);
|
||||
}}
|
||||
checked={false}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Import scenario from file"
|
||||
content="Import a scenario from a previously exported file. This will add the imported units to the current scenario, so make sure to delete any unwanted units before importing."
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{savingSessionData ? (
|
||||
<div className="text-white">
|
||||
@ -174,20 +200,19 @@ export function Header() {
|
||||
</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="Show/Hide mission drawings"
|
||||
/>
|
||||
<OlLockStateButton
|
||||
checked={!mapOptions.protectDCSUnits}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("protectDCSUnits", !mapOptions.protectDCSUnits);
|
||||
}}
|
||||
tooltip="Lock/unlock protected units (from scripted mission)"
|
||||
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}
|
||||
@ -195,7 +220,14 @@ export function Header() {
|
||||
audioEnabled ? getApp().getAudioManager().stop() : getApp().getAudioManager().start();
|
||||
setAudioEnabled(!audioEnabled);
|
||||
}}
|
||||
tooltip="Enable/disable audio and radio backend"
|
||||
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>
|
||||
@ -269,6 +301,19 @@ export function Header() {
|
||||
</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}
|
||||
@ -294,6 +339,12 @@ export function Header() {
|
||||
.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}
|
||||
@ -301,7 +352,12 @@ export function Header() {
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("cameraPluginEnabled", !mapOptions.cameraPluginEnabled);
|
||||
}}
|
||||
tooltip="Activate/deactivate camera plugin"
|
||||
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) => {
|
||||
@ -312,6 +368,10 @@ export function Header() {
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
<FaQuestionCircle
|
||||
onClick={() => getApp().setState(OlympusState.TRAINING)}
|
||||
className={`cursor-pointer text-2xl text-white`}
|
||||
/>
|
||||
</div>
|
||||
{!scrolledRight && (
|
||||
<FaChevronRight
|
||||
@ -321,6 +381,6 @@ export function Header() {
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -119,6 +119,35 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
setBlueprint(null);
|
||||
setEffect(null);
|
||||
}}
|
||||
wiki={() => {
|
||||
return (
|
||||
<div className="h-full overflow-auto p-4 text-gray-400 no-scrollbar">
|
||||
<h2 className="mb-4 font-bold">Spawn menu</h2>
|
||||
<p>The spawn menu allows you to spawn new units in the current mission.</p>
|
||||
<p>Moreover, it allows you to spawn effects like smokes and explosions.</p>
|
||||
<p className="mt-2">You can use the search bar to quickly find a specific unit. Otherwise, open the category you are interested in, and use the filters to refine your selection. </p>
|
||||
<img src="images/training/unitfilter.png" className={`
|
||||
mx-auto my-4 w-[80%] rounded-lg
|
||||
drop-shadow-[0_0px_7px_rgba(255,255,255,0.07)]
|
||||
`} />
|
||||
<div className="mt-2">Click on a unit to enter the spawn properties menu. The menu is divided into multiple sections:
|
||||
<ul className="ml-4 mt-2 list-inside list-decimal">
|
||||
<li>Unit name and short description</li>
|
||||
<li>Quick access name </li>
|
||||
<li>Spawn properties</li>
|
||||
<li>Loadout description</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>To get more info on each control, hover your cursor on it.</p>
|
||||
<h2 className="my-4 font-bold">Quick access</h2>
|
||||
<p>If you plan on reusing the same spawn multiple times during the mission, you can "star" the spawn properties. This will allow you to reuse them quickly multiple times. The starred spawn will save all settings, so you can create starred spawn with multiple variations, e.g. loadouts, or skill levels.</p>
|
||||
<img src="images/training/starred.png" className={`
|
||||
mx-auto my-4 w-[80%] rounded-lg
|
||||
drop-shadow-[0_0px_7px_rgba(255,255,255,0.07)]
|
||||
`} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{blueprint === null && effect === null && (
|
||||
|
||||
@ -47,7 +47,7 @@ import {
|
||||
} from "../components/olicons";
|
||||
import { Coalition } from "../../types/types";
|
||||
import { convertROE, deepCopyTable, ftToM, knotsToMs, mToFt, msToKnots } from "../../other/utils";
|
||||
import { FaCog, FaGasPump, FaSignal, FaTag } from "react-icons/fa";
|
||||
import { FaChevronLeft, FaCog, FaExclamationCircle, FaGasPump, FaQuestionCircle, FaSignal, FaTag } from "react-icons/fa";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { OlSearchBar } from "../components/olsearchbar";
|
||||
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
|
||||
@ -58,6 +58,9 @@ import { OlStringInput } from "../components/olstringinput";
|
||||
import { OlFrequencyInput } from "../components/olfrequencyinput";
|
||||
import { UnitSink } from "../../audio/unitsink";
|
||||
import { AudioManagerStateChangedEvent, SelectedUnitsChangedEvent, SelectionClearedEvent, UnitsUpdatedEvent } from "../../events";
|
||||
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
import { OlExpandingTooltip } from "../components/olexpandingtooltip";
|
||||
import { OlLocation } from "../components/ollocation";
|
||||
|
||||
export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
function initializeUnitsData() {
|
||||
@ -125,6 +128,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
const [activeRadioSettings, setActiveRadioSettings] = useState(null as null | { radio: Radio; TACAN: TACAN });
|
||||
const [activeAdvancedSettings, setActiveAdvancedSettings] = useState(null as null | GeneralSettings);
|
||||
const [lastUpdateTime, setLastUpdateTime] = useState(0);
|
||||
const [showScenicModes, setShowScenicModes] = useState(true);
|
||||
|
||||
var searchBarRef = useRef(null);
|
||||
|
||||
@ -223,7 +227,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
const selectedCategories = getApp()?.getUnitsManager()?.getSelectedUnitsCategories() ?? [];
|
||||
|
||||
const filteredUnits = Object.values(getApp()?.getUnitsManager()?.getUnits() ?? {}).filter(
|
||||
(unit) => unit.getUnitName().toLowerCase().indexOf(filterString.toLowerCase()) >= 0
|
||||
(unit) => (unit.getUnitName().toLowerCase().indexOf(filterString.toLowerCase()) >= 0 || (unit.getBlueprint()?.label ?? "").toLowerCase()?.indexOf(filterString.toLowerCase()) >= 0 )
|
||||
);
|
||||
|
||||
const everyUnitIsGround = selectedCategories.every((category) => {
|
||||
@ -264,15 +268,39 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
title={selectedUnits.length > 0 ? `Units selected (x${selectedUnits.length})` : `No units selected`}
|
||||
onClose={props.onClose}
|
||||
canBeHidden={true}
|
||||
wiki={() => {
|
||||
return <div className={`
|
||||
h-full flex-col overflow-auto p-4 text-gray-400 no-scrollbar flex
|
||||
gap-2
|
||||
`}>
|
||||
<h2 className="mb-4 font-bold">Unit selection tool</h2>
|
||||
<div>
|
||||
The unit control menu serves two purposes. If no unit is currently selected, it allows you to select units based on their category, coalition, and control mode. You can also select units based on their specific type by using the search input.
|
||||
</div>
|
||||
<h2 className="my-4 font-bold">Unit control tool</h2>
|
||||
<div>If units are selected, the menu will display the selected units and allow you to control their altitude, speed, rules of engagement, and other settings.</div>
|
||||
<div>The available controls depend on what type of unit is selected. Only controls applicable to every selected unit will be displayed, so make sure to refine your selection. </div>
|
||||
<div> You will be able to inspect the current values of the controls, e.g. the desired altitude, rules of engagement and so on. However, if multiple units are selected, you will only see the values of controls that are set to be the same for each selected unit.</div>
|
||||
<div> For example, if two airplanes are selected and they both have been instructed to fly at 1000ft, you will see the altitude slider set at that value. But if one airplane is set to fly at 1000ft and the other at 2000ft, you will see the slider display 'Different values'.</div>
|
||||
<div> If at that point you move the slider, you will instruct both airplanes to fly at the same altitude.</div>
|
||||
<div> If a single unit is selected, you will also be able to see additional info on the unit, like its fuel level, position and altitude, tasking, and available ammunition. </div>
|
||||
</div>
|
||||
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{/* ============== Selection tool START ============== */}
|
||||
{selectedUnits.length == 0 && (
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<div className="text-lg text-bold text-gray-200">Selection tool</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
The selection tools allows you to select units depending on their category, coalition, and control mode. You can also select units depending on
|
||||
their specific type by using the search input.
|
||||
<div className="flex content-center gap-4">
|
||||
<div className="my-auto text-gray-400">
|
||||
<FaQuestionCircle />
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
The selection tools allows you to select units depending on their category, coalition, and control mode. You can also select units depending on
|
||||
their specific type by using the search input.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 rounded-lg bg-olympus-600 p-4">
|
||||
{selectionID === null && (
|
||||
@ -328,16 +356,18 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
)}
|
||||
{selectionID === null &&
|
||||
Object.entries({
|
||||
aircraft: olButtonsVisibilityAircraft,
|
||||
helicopter: olButtonsVisibilityHelicopter,
|
||||
"groundunit-sam": olButtonsVisibilityGroundunitSam,
|
||||
groundunit: olButtonsVisibilityGroundunit,
|
||||
navyunit: olButtonsVisibilityNavyunit,
|
||||
aircraft: [olButtonsVisibilityAircraft, "Aircrafts"],
|
||||
helicopter: [olButtonsVisibilityHelicopter, "Helicopters"],
|
||||
"groundunit-sam": [olButtonsVisibilityGroundunitSam, "SAMs"],
|
||||
groundunit: [olButtonsVisibilityGroundunit, "Ground units"],
|
||||
navyunit: [olButtonsVisibilityNavyunit, "Navy units"],
|
||||
}).map((entry, idx) => {
|
||||
return (
|
||||
<tr key={idx}>
|
||||
<td className="text-lg text-gray-200">
|
||||
<FontAwesomeIcon icon={entry[1]} />
|
||||
<td className="flex gap-2 text-lg text-gray-200">
|
||||
<FontAwesomeIcon icon={entry[1][0] as IconDefinition} /> <div className={`
|
||||
text-sm text-gray-400
|
||||
`}>{entry[1][1] as string}</div>
|
||||
</td>
|
||||
{["blue", "neutral", "red"].map((coalition) => {
|
||||
return (
|
||||
@ -420,8 +450,23 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
setSelectionID(unit.ID);
|
||||
}}
|
||||
|
||||
>
|
||||
{unit.getUnitName()}
|
||||
<div data-coalition={unit.getCoalition()}
|
||||
className={`
|
||||
flex content-center justify-between border-l-4
|
||||
pl-2
|
||||
data-[coalition='blue']:border-blue-500
|
||||
data-[coalition='neutral']:border-gray-500
|
||||
data-[coalition='red']:border-red-500
|
||||
`}
|
||||
onMouseEnter={() => {
|
||||
unit.setHighlighted(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
unit.setHighlighted(false);
|
||||
}}
|
||||
>{unit.getUnitName()} ({unit.getBlueprint()?.label ?? ""})</div>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
@ -573,6 +618,13 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
})
|
||||
);
|
||||
}}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Altitude type"
|
||||
content="Sets wether the unit will hold the selected altitude as Above Ground Level or Above Sea Level"
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
/>
|
||||
</div>
|
||||
<OlRangeSlider
|
||||
@ -638,6 +690,13 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
})
|
||||
);
|
||||
}}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Airspeed type"
|
||||
content="Sets wether the unit will hold the selected airspeed as Calibrated Air Speed or Ground Speed"
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -671,7 +730,71 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
>
|
||||
Rules of engagement
|
||||
</span>
|
||||
<OlButtonGroup>
|
||||
<OlButtonGroup
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Rules of engagement"
|
||||
content={
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>Sets the rule of engagement of the unit, in order:</div>
|
||||
<div className="flex flex-col gap-2 px-2">
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeHold} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Hold fire: The unit will not shoot in
|
||||
any circumstance
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeReturn} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Return fire: The unit will not fire
|
||||
unless fired upon
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeDesignated} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} />{" "}
|
||||
<div>
|
||||
{" "}
|
||||
Fire on target: The unit will not fire unless fired upon <p className={`
|
||||
inline font-bold
|
||||
`}>or</p> ordered to do so{" "}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeFree} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Free: The unit will fire at any
|
||||
detected enemy in range
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="my-auto">
|
||||
<FaExclamationCircle className={`
|
||||
animate-bounce text-xl
|
||||
`} />
|
||||
</div>
|
||||
<div>
|
||||
Currently, DCS blue and red ground units do not respect{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeReturn} className={`
|
||||
my-auto text-white
|
||||
`} /> and{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeDesignated} className={`
|
||||
my-auto text-white
|
||||
`} /> rules of engagement, so be careful, they
|
||||
may start shooting when you don't want them to. Use neutral units for finer control.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
>
|
||||
{[olButtonsRoeHold, olButtonsRoeReturn, olButtonsRoeDesignated, olButtonsRoeFree].map((icon, idx) => {
|
||||
return (
|
||||
<OlButtonGroupItem
|
||||
@ -709,7 +832,49 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
>
|
||||
Threat reaction
|
||||
</span>
|
||||
<OlButtonGroup>
|
||||
<OlButtonGroup
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Reaction to threat"
|
||||
content={
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>Sets the reaction to threat of the unit, in order:</div>
|
||||
<div className="flex flex-col gap-2 px-2">
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsThreatNone} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> No reaction: The unit will not
|
||||
react in any circumstance
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsThreatPassive} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Passive: The unit will use
|
||||
counter-measures, but will not alter its course
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsThreatManoeuvre} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Manouevre: The unit will try
|
||||
to evade the threat using manoeuvres, but no counter-measures
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsThreatEvade} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Full evasion: the unit will try
|
||||
to evade the threat both manoeuvering and using counter-measures
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
>
|
||||
{[olButtonsThreatNone, olButtonsThreatPassive, olButtonsThreatManoeuvre, olButtonsThreatEvade].map((icon, idx) => {
|
||||
return (
|
||||
<OlButtonGroupItem
|
||||
@ -742,7 +907,50 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
>
|
||||
Radar and ECM
|
||||
</span>
|
||||
<OlButtonGroup>
|
||||
<OlButtonGroup
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Radar and ECM"
|
||||
content={
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>Sets the units radar and Electronic Counter Measures (jamming) use policy, in order:</div>
|
||||
<div className="flex flex-col gap-2 px-2">
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsEmissionsSilent} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Radio silence: No radar or
|
||||
ECM will be used
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsEmissionsDefend} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Defensive: The unit will turn
|
||||
radar and ECM on only when threatened
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsEmissionsAttack} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Attack: The unit will use
|
||||
radar and ECM when engaging other units
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsEmissionsFree} className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Free: the unit will use the
|
||||
radar and ECM all the time
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
tooltipPosition="above"
|
||||
>
|
||||
{[olButtonsEmissionsSilent, olButtonsEmissionsDefend, olButtonsEmissionsAttack, olButtonsEmissionsFree].map((icon, idx) => {
|
||||
return (
|
||||
<OlButtonGroupItem
|
||||
@ -807,6 +1015,13 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
})
|
||||
);
|
||||
}}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Make AAR tanker available"
|
||||
content="This option allows you to make the unit available for refuelling other planes. You can keep moving the unit around while being available as tanker, however this may cause refuelling players to disconnect. If possible, try to avoid issuing commands to the unit while it is refuelling human players. Change the tanker settings to turn the tanker TACAN on or to change the frequency on which it will respond to refuelling requests."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -849,6 +1064,13 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
})
|
||||
);
|
||||
}}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Make AWACS available"
|
||||
content="This option allows you to make the unit available for AWACS task. It will provide bogey dopes and picture calls on the assigned frequency, which you can change in the AWACS settings."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -909,153 +1131,221 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
flex flex-col gap-4 rounded-md bg-olympus-200/30 p-4
|
||||
`}
|
||||
>
|
||||
{/* ============== Scenic AAA toggle START ============== */}
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Scenic AAA mode
|
||||
</span>
|
||||
<OlToggle
|
||||
toggled={selectedUnitsData.scenicAAA}
|
||||
onClick={() => {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.scenicAAA(null, () =>
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
scenicAAA: !selectedUnitsData.scenicAAA,
|
||||
missOnPurpose: false,
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* ============== Scenic AAA toggle END ============== */}
|
||||
{/* ============== Miss on purpose toggle START ============== */}
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Miss on purpose mode
|
||||
</span>
|
||||
<OlToggle
|
||||
toggled={selectedUnitsData.missOnPurpose}
|
||||
onClick={() => {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.missOnPurpose(null, () =>
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
scenicAAA: false,
|
||||
missOnPurpose: !selectedUnitsData.missOnPurpose,
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* ============== Miss on purpose toggle END ============== */}
|
||||
<div className="flex gap-4">
|
||||
{/* ============== Shots scatter START ============== */}
|
||||
<div className={`flex flex-col gap-2`}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between">
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Shots scatter
|
||||
Scenic modes
|
||||
</span>
|
||||
<OlButtonGroup>
|
||||
{[olButtonsScatter1, olButtonsScatter2, olButtonsScatter3].map((icon, idx) => {
|
||||
return (
|
||||
<OlButtonGroupItem
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.setShotsScatter(idx + 1, null, () =>
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
shotsScatter: idx + 1,
|
||||
})
|
||||
);
|
||||
}}
|
||||
active={selectedUnitsData.shotsScatter === idx + 1}
|
||||
icon={icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</OlButtonGroup>
|
||||
</div>
|
||||
{/* ============== Shots scatter END ============== */}
|
||||
{/* ============== Shots intensity START ============== */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span
|
||||
<FaChevronLeft
|
||||
data-open={showScenicModes}
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
my-auto cursor-pointer text-gray-200
|
||||
transition-transform
|
||||
data-[open='true']:-rotate-90
|
||||
`}
|
||||
>
|
||||
Shots intensity
|
||||
</span>
|
||||
<OlButtonGroup>
|
||||
{[olButtonsIntensity1, olButtonsIntensity2, olButtonsIntensity3].map((icon, idx) => {
|
||||
return (
|
||||
<OlButtonGroupItem
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.setShotsIntensity(idx + 1, null, () =>
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
shotsIntensity: idx + 1,
|
||||
})
|
||||
);
|
||||
}}
|
||||
active={selectedUnitsData.shotsIntensity === idx + 1}
|
||||
icon={icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</OlButtonGroup>
|
||||
</div>
|
||||
{/* ============== Shots intensity END ============== */}
|
||||
</div>
|
||||
{/* ============== Operate as toggle START ============== */}
|
||||
{selectedUnits.every((unit) => unit.getCoalition() === "neutral") && (
|
||||
<div className={`flex content-center justify-between`}>
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Operate as
|
||||
</span>
|
||||
<OlCoalitionToggle
|
||||
coalition={selectedUnitsData.operateAs as Coalition}
|
||||
onClick={() => {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue", null, () =>
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
operateAs: selectedUnitsData.operateAs === "blue" ? "red" : "blue",
|
||||
})
|
||||
);
|
||||
}}
|
||||
onClick={() => setShowScenicModes(!showScenicModes)}
|
||||
/>
|
||||
</div>
|
||||
{showScenicModes && (
|
||||
<div
|
||||
className={`
|
||||
flex flex-col gap-2 text-sm text-gray-400
|
||||
`}
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<div className="my-auto">
|
||||
<FaExclamationCircle className={`
|
||||
animate-bounce text-xl
|
||||
`} />
|
||||
</div>
|
||||
<div>
|
||||
Currently, DCS blue and red ground units do not respect their rules of engagement, so be careful, they may start shooting when
|
||||
you don't want them to. Use neutral units for finer control, then use the "Operate as" toggle to switch their "side".
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showScenicModes && (
|
||||
<>
|
||||
{/* ============== Scenic AAA toggle START ============== */}
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Scenic AAA mode
|
||||
</span>
|
||||
<OlToggle
|
||||
toggled={selectedUnitsData.scenicAAA}
|
||||
onClick={() => {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.scenicAAA(null, () =>
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
scenicAAA: !selectedUnitsData.scenicAAA,
|
||||
missOnPurpose: false,
|
||||
})
|
||||
);
|
||||
}}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Enable scenic AAA mode"
|
||||
content="This mode will make the unit fire in the air any time an enemy unit is nearby. This can help Game Masters create a more immersive scenario without increasing its difficulty."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
/>
|
||||
</div>
|
||||
{/* ============== Scenic AAA toggle END ============== */}
|
||||
{/* ============== Miss on purpose toggle START ============== */}
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Miss on purpose mode
|
||||
</span>
|
||||
<OlToggle
|
||||
toggled={selectedUnitsData.missOnPurpose}
|
||||
onClick={() => {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.missOnPurpose(null, () =>
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
scenicAAA: false,
|
||||
missOnPurpose: !selectedUnitsData.missOnPurpose,
|
||||
})
|
||||
);
|
||||
}}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Enable scenic miss on purpose mode"
|
||||
content="This mode will make the unit fire in the direction of nearby enemy units, without actively aiming at them. It represents a sort of unguided firing, which can help Game Masters create a more immersive scenario without increasing its difficulty."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
/>
|
||||
</div>
|
||||
{/* ============== Miss on purpose toggle END ============== */}
|
||||
<div className="flex gap-4">
|
||||
{/* ============== Shots scatter START ============== */}
|
||||
<div className={`flex flex-col gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Shots scatter
|
||||
</span>
|
||||
<OlButtonGroup>
|
||||
{[olButtonsScatter1, olButtonsScatter2, olButtonsScatter3].map((icon, idx) => {
|
||||
return (
|
||||
<OlButtonGroupItem
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.setShotsScatter(idx + 1, null, () =>
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
shotsScatter: idx + 1,
|
||||
})
|
||||
);
|
||||
}}
|
||||
active={selectedUnitsData.shotsScatter === idx + 1}
|
||||
icon={icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</OlButtonGroup>
|
||||
</div>
|
||||
{/* ============== Shots scatter END ============== */}
|
||||
{/* ============== Shots intensity START ============== */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Shots intensity
|
||||
</span>
|
||||
<OlButtonGroup>
|
||||
{[olButtonsIntensity1, olButtonsIntensity2, olButtonsIntensity3].map((icon, idx) => {
|
||||
return (
|
||||
<OlButtonGroupItem
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.setShotsIntensity(idx + 1, null, () =>
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
shotsIntensity: idx + 1,
|
||||
})
|
||||
);
|
||||
}}
|
||||
active={selectedUnitsData.shotsIntensity === idx + 1}
|
||||
icon={icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</OlButtonGroup>
|
||||
</div>
|
||||
{/* ============== Shots intensity END ============== */}
|
||||
</div>
|
||||
{/* ============== Operate as toggle START ============== */}
|
||||
{selectedUnits.every((unit) => unit.getCoalition() === "neutral") && (
|
||||
<div
|
||||
className={`flex content-center justify-between`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Operate as
|
||||
</span>
|
||||
<OlCoalitionToggle
|
||||
coalition={selectedUnitsData.operateAs as Coalition}
|
||||
onClick={() => {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue", null, () =>
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
operateAs: selectedUnitsData.operateAs === "blue" ? "red" : "blue",
|
||||
})
|
||||
);
|
||||
}}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Unit operate as coalition"
|
||||
content="This option is only available for neutral units and it allows you to change what coalition the unit will 'operate as' when performing scenic tasks. For example, a 'red' neutral unit tasked to perform miss on purpose will shoot in the direction of blue units. "
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
tooltipPosition="above"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* ============== Operate as toggle END ============== */}
|
||||
</>
|
||||
)}
|
||||
{/* ============== Operate as toggle END ============== */}
|
||||
</div>
|
||||
{/* ============== Follow roads toggle START ============== */}
|
||||
<div className="flex content-center justify-between">
|
||||
@ -1079,6 +1369,14 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
})
|
||||
);
|
||||
}}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Follow roads when moving"
|
||||
content="If enabled, this option will force the unit to stay on roads when moving to a new location. This can be useful to simulate convoys or to make the unit follow a specific path."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
tooltipPosition="above"
|
||||
/>
|
||||
</div>
|
||||
{/* ============== Follow roads toggle END ============== */}
|
||||
@ -1104,6 +1402,14 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
})
|
||||
);
|
||||
}}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Turn unit off"
|
||||
content="When enabled, this option will turn the unit completely off, making it inactive. This can be useful to control when a unit starts engaging the enemy."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
tooltipPosition="above"
|
||||
/>
|
||||
</div>
|
||||
{/* ============== Unit active toggle END ============== */}
|
||||
@ -1146,6 +1452,14 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
}
|
||||
});
|
||||
}}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Make the unit emit sounds"
|
||||
content="This option allows the unit to emit sounds as if it had loudspeakers. Turn this on to enable the option, then open the audio menu to connect a sound source to the unit. This is useful to simulate 5MC calls on the carrier, or attach sirens to unit. "
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
tooltipPosition="above"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-white">
|
||||
@ -1261,9 +1575,10 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
value={activeRadioSettings ? activeRadioSettings.TACAN.channel : 1}
|
||||
></OlNumberInput>
|
||||
|
||||
<OlDropdown label={activeRadioSettings ? activeRadioSettings.TACAN.XY : "X"} className={`
|
||||
my-auto w-20
|
||||
`}>
|
||||
<OlDropdown
|
||||
label={activeRadioSettings ? activeRadioSettings.TACAN.XY : "X"}
|
||||
className={`my-auto w-20`}
|
||||
>
|
||||
<OlDropdownItem
|
||||
key={"X"}
|
||||
onClick={() => {
|
||||
@ -1297,7 +1612,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
<span className="my-auto text-sm">Enabled</span>{" "}
|
||||
<span className="my-auto text-sm">Enable TACAN</span>{" "}
|
||||
<OlToggle
|
||||
toggled={activeRadioSettings ? activeRadioSettings.TACAN.isOn : false}
|
||||
onClick={() => {
|
||||
@ -1464,8 +1779,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
hover:bg-gray-800
|
||||
`}
|
||||
onClick={() => {
|
||||
setActiveRadioSettings(null);
|
||||
setShowRadioSettings(false);
|
||||
setActiveAdvancedSettings(null);
|
||||
setShowAdvancedSettings(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
@ -1485,20 +1800,39 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
bg-olympus-600 p-4
|
||||
`}
|
||||
>
|
||||
<div className="flex border-b-2 border-b-white/10 pb-2">
|
||||
<div
|
||||
className={`
|
||||
flex content-center gap-2 rounded-full
|
||||
${selectedUnits[0].getFuel() > 40 && `bg-green-700`}
|
||||
${selectedUnits[0].getFuel() > 10 && selectedUnits[0].getFuel() <= 40 && `
|
||||
bg-yellow-700
|
||||
<div
|
||||
className={`
|
||||
flex flex-col gap-2 border-b-2 border-b-white/10 pb-2
|
||||
`}
|
||||
>
|
||||
<div className={`flex justify-between`}>
|
||||
<div className="my-auto text-white">{selectedUnits[0].getUnitName()}</div>
|
||||
<div
|
||||
className={`
|
||||
flex content-center gap-2 rounded-full
|
||||
${selectedUnits[0].getFuel() > 40 && `bg-green-700`}
|
||||
${
|
||||
selectedUnits[0].getFuel() > 10 &&
|
||||
selectedUnits[0].getFuel() <= 40 &&
|
||||
`bg-yellow-700`
|
||||
}
|
||||
${selectedUnits[0].getFuel() <= 10 && `bg-red-700`}
|
||||
px-2 py-1 text-sm font-bold text-white
|
||||
`}
|
||||
${selectedUnits[0].getFuel() <= 10 && `bg-red-700`}
|
||||
px-2 py-1 text-sm font-bold text-white
|
||||
`}
|
||||
>
|
||||
<FaGasPump className="my-auto" />
|
||||
{selectedUnits[0].getFuel()}%
|
||||
>
|
||||
<FaGasPump className="my-auto" />
|
||||
{selectedUnits[0].getFuel()}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-auto text-sm text-gray-400">
|
||||
{selectedUnits[0].getTask()}
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
<OlLocation location={selectedUnits[0].getPosition()} className={`
|
||||
w-[280px] text-sm
|
||||
`}/>
|
||||
<div className="my-auto text-gray-200">{Math.round(mToFt(selectedUnits[0].getPosition().alt ?? 0))} ft</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ import { OlAccordion } from "../components/olaccordion";
|
||||
import { AppStateChangedEvent, SpawnHeadingChangedEvent } from "../../events";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { FaQuestionCircle } from "react-icons/fa";
|
||||
import { OlExpandingTooltip } from "../components/olexpandingtooltip";
|
||||
|
||||
export function UnitSpawnMenu(props: {
|
||||
visible: boolean;
|
||||
@ -124,6 +125,11 @@ export function UnitSpawnMenu(props: {
|
||||
if (props.coalition) setSpawnCoalition(props.coalition);
|
||||
}, [props.coalition]);
|
||||
|
||||
/* Effect to update the initial altitude when the blueprint changes */
|
||||
useEffect(() => {
|
||||
setSpawnAltitude((maxAltitude - minAltitude) / 2);
|
||||
}, [props.blueprint]);
|
||||
|
||||
/* Heading compass */
|
||||
const [compassAngle, setCompassAngle] = useState(0);
|
||||
const compassRef = useRef<HTMLImageElement>(null);
|
||||
@ -185,7 +191,8 @@ export function UnitSpawnMenu(props: {
|
||||
});
|
||||
|
||||
/* Initialize the loadout */
|
||||
spawnLoadoutName === "" && loadouts.length > 0 && setSpawnLoadoutName(loadouts[0].name);
|
||||
if (spawnLoadoutName === "" && loadouts.length > 0)
|
||||
setSpawnLoadoutName(loadouts.find((loadout) => loadout.name !== "Empty loadout")?.name ?? loadouts[0].name);
|
||||
const spawnLoadout = props.blueprint?.loadouts?.find((loadout) => {
|
||||
return loadout.name === spawnLoadoutName;
|
||||
});
|
||||
@ -195,7 +202,7 @@ export function UnitSpawnMenu(props: {
|
||||
{props.compact ? (
|
||||
<>
|
||||
{props.visible && (
|
||||
<div className="flex max-h-[800px] flex-col overflow-auto">
|
||||
<div className={`flex max-h-[800px] flex-col overflow-auto`}>
|
||||
<div className="flex h-fit flex-col gap-3">
|
||||
<div className="flex">
|
||||
<FontAwesomeIcon
|
||||
@ -245,7 +252,12 @@ export function UnitSpawnMenu(props: {
|
||||
else getApp().getMap().addStarredSpawnRequestTable(key, spawnRequestTable, quickAccessName);
|
||||
}
|
||||
}}
|
||||
tooltip="Save this spawn for quick access"
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Add this spawn to quick access"
|
||||
content="Enter a name and click on the button to later be able to quickly respawn a unit with the same configuration."
|
||||
/>
|
||||
)}
|
||||
checked={key in props.starredSpawns}
|
||||
icon={faStar}
|
||||
></OlStateButton>
|
||||
@ -281,6 +293,13 @@ export function UnitSpawnMenu(props: {
|
||||
leftLabel={"AGL"}
|
||||
rightLabel={"ASL"}
|
||||
onClick={() => setSpawnAltitudeType(!spawnAltitudeType)}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Select altitude type"
|
||||
content="If AGL is selected, the aircraft will be spawned at the selected altitude above the ground. If ASL is selected, the aircraft will be spawned at the selected altitude above sea level."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
/>
|
||||
</div>
|
||||
<OlRangeSlider
|
||||
@ -301,7 +320,17 @@ export function UnitSpawnMenu(props: {
|
||||
>
|
||||
Role
|
||||
</span>
|
||||
<OlDropdown label={spawnRole} className="w-64">
|
||||
<OlDropdown
|
||||
label={spawnRole}
|
||||
className="w-64"
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Role of the spawned unit"
|
||||
content="This selection has no effect on what the spawned unit will actually perform, and you will have total control on it. However, it is used to filter what loadouts are available."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
>
|
||||
{roles.map((role) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
@ -326,7 +355,17 @@ export function UnitSpawnMenu(props: {
|
||||
>
|
||||
Weapons
|
||||
</span>
|
||||
<OlDropdown label={spawnLoadoutName} className={`w-64`}>
|
||||
<OlDropdown
|
||||
label={spawnLoadoutName}
|
||||
className={`w-64`}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Unit loadout"
|
||||
content="This will be the loadout that the unit will have when spawned. Look at the bottom of the page to check the exact type and number of items."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
>
|
||||
{loadouts.map((loadout) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
@ -370,6 +409,10 @@ export function UnitSpawnMenu(props: {
|
||||
<OlDropdown
|
||||
label={props.blueprint?.liveries ? (props.blueprint?.liveries[spawnLiveryID]?.name ?? "Default") : "No livery"}
|
||||
className={`w-64`}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip title="Unit livery" content="Selects the livery of the spawned unit. This is a purely cosmetic option." />
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
>
|
||||
{props.blueprint?.liveries &&
|
||||
Object.keys(props.blueprint?.liveries)
|
||||
@ -428,7 +471,17 @@ export function UnitSpawnMenu(props: {
|
||||
>
|
||||
Skill
|
||||
</span>
|
||||
<OlDropdown label={spawnSkill} className={`w-64`}>
|
||||
<OlDropdown
|
||||
label={spawnSkill}
|
||||
className={`w-64`}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Unit skill"
|
||||
content="Selects the skill of the spawned unit. Depending on the selection, the unit will be more precise and effective at its mission. Usually a lower level is selected to generate a more forgiving mission."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
>
|
||||
{["Average", "Good", "High", "Excellent"].map((skill) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
@ -476,6 +529,13 @@ export function UnitSpawnMenu(props: {
|
||||
setCompassAngle(normalizeAngle(compassAngle + 1));
|
||||
}}
|
||||
value={compassAngle}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Spawn heading"
|
||||
content="This controls the direction the unit will face when spanwned. This is important for units that take longer to change direction, like ships. Air units and helicopters will enter a orbit with its major axis aligned with the spawn heading. Drag the compass to change the heading."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
/>
|
||||
|
||||
<div className={`relative mr-3 h-[60px] w-[60px]`}>
|
||||
@ -551,7 +611,7 @@ export function UnitSpawnMenu(props: {
|
||||
focus:outline-none focus:ring-4
|
||||
`}
|
||||
onClick={() => {
|
||||
if (spawnRequestTable){
|
||||
if (spawnRequestTable) {
|
||||
spawnRequestTable.unit.heading = deg2rad(compassAngle);
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
@ -586,7 +646,7 @@ export function UnitSpawnMenu(props: {
|
||||
gap-2
|
||||
`}
|
||||
>
|
||||
<div className="my-auto text-sm text-white">Quick access: </div>
|
||||
<div className="my-auto text-white">Quick access: </div>
|
||||
<OlStringInput
|
||||
onChange={(e) => {
|
||||
setQuickAccessName(e.target.value);
|
||||
@ -595,11 +655,19 @@ export function UnitSpawnMenu(props: {
|
||||
/>
|
||||
<OlStateButton
|
||||
onClick={() => {
|
||||
if (spawnRequestTable)
|
||||
if (spawnRequestTable) {
|
||||
spawnRequestTable.unit.heading = compassAngle;
|
||||
if (key in props.starredSpawns) getApp().getMap().removeStarredSpawnRequestTable(key);
|
||||
else getApp().getMap().addStarredSpawnRequestTable(key, spawnRequestTable, quickAccessName);
|
||||
}
|
||||
}}
|
||||
tooltip="Save this spawn for quick access"
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Add this spawn to quick access"
|
||||
content="Enter a name and click on the button to later be able to quickly respawn a unit with the same configuration."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
checked={key in props.starredSpawns}
|
||||
icon={faStar}
|
||||
></OlStateButton>
|
||||
@ -611,17 +679,28 @@ export function UnitSpawnMenu(props: {
|
||||
`}
|
||||
>
|
||||
{!props.coalition && (
|
||||
<OlCoalitionToggle
|
||||
coalition={spawnCoalition}
|
||||
onClick={() => {
|
||||
spawnCoalition === "blue" && setSpawnCoalition("neutral");
|
||||
spawnCoalition === "neutral" && setSpawnCoalition("red");
|
||||
spawnCoalition === "red" && setSpawnCoalition("blue");
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<div className="my-auto mr-2 text-white">Coalition:</div>
|
||||
<OlCoalitionToggle
|
||||
coalition={spawnCoalition}
|
||||
onClick={() => {
|
||||
spawnCoalition === "blue" && setSpawnCoalition("neutral");
|
||||
spawnCoalition === "neutral" && setSpawnCoalition("red");
|
||||
spawnCoalition === "red" && setSpawnCoalition("blue");
|
||||
}}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Unit coalition"
|
||||
content="Toggle between blue, neutral and red coalitions. Neutral coalition must be used to employ scenic functions like miss on purpose."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="my-auto ml-auto text-white">Units: </div>
|
||||
<OlNumberInput
|
||||
className={"ml-auto"}
|
||||
className={"ml-2"}
|
||||
value={spawnNumber}
|
||||
min={minNumber}
|
||||
max={maxNumber}
|
||||
@ -634,6 +713,13 @@ export function UnitSpawnMenu(props: {
|
||||
onChange={(ev) => {
|
||||
!isNaN(Number(ev.target.value)) && setSpawnNumber(Math.max(minNumber, Math.min(maxNumber, Number(ev.target.value))));
|
||||
}}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Select number of units"
|
||||
content="This is how many units of this type will be spawned. If more than one unit is spawned, a DCS group will be created: this means that the units will be spawned in a formation, and you will not be able to control them singularly. The entire group will act as a single entity."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -668,6 +754,13 @@ export function UnitSpawnMenu(props: {
|
||||
leftLabel={"AGL"}
|
||||
rightLabel={"ASL"}
|
||||
onClick={() => setSpawnAltitudeType(!spawnAltitudeType)}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Select altitude type"
|
||||
content="If AGL is selected, the aircraft will be spawned at the selected altitude above the ground. If ASL is selected, the aircraft will be spawned at the selected altitude above sea level."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
/>
|
||||
</div>
|
||||
<OlRangeSlider
|
||||
@ -688,7 +781,17 @@ export function UnitSpawnMenu(props: {
|
||||
>
|
||||
Role
|
||||
</span>
|
||||
<OlDropdown label={spawnRole} className="w-64">
|
||||
<OlDropdown
|
||||
label={spawnRole}
|
||||
className="w-64"
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Role of the spawned unit"
|
||||
content="This selection has no effect on what the spawned unit will actually perform, and you will have total control on it. However, it is used to filter what loadouts are available."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
>
|
||||
{roles.map((role) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
@ -714,7 +817,17 @@ export function UnitSpawnMenu(props: {
|
||||
>
|
||||
Weapons
|
||||
</span>
|
||||
<OlDropdown label={spawnLoadoutName} className={`w-64`}>
|
||||
<OlDropdown
|
||||
label={spawnLoadoutName}
|
||||
className={`w-64`}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Unit loadout"
|
||||
content="This will be the loadout that the unit will have when spawned. Look at the bottom of the page to check the exact type and number of items."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
>
|
||||
{loadouts.map((loadout) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
@ -751,6 +864,10 @@ export function UnitSpawnMenu(props: {
|
||||
<OlDropdown
|
||||
label={props.blueprint?.liveries ? (props.blueprint?.liveries[spawnLiveryID]?.name ?? "Default") : "No livery"}
|
||||
className={`w-64`}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip title="Unit livery" content="Selects the livery of the spawned unit. This is a purely cosmetic option." />
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
>
|
||||
{props.blueprint?.liveries &&
|
||||
Object.keys(props.blueprint?.liveries)
|
||||
@ -811,7 +928,17 @@ export function UnitSpawnMenu(props: {
|
||||
>
|
||||
Skill
|
||||
</span>
|
||||
<OlDropdown label={spawnSkill} className={`w-64`}>
|
||||
<OlDropdown
|
||||
label={spawnSkill}
|
||||
className={`w-64`}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Unit skill"
|
||||
content="Selects the skill of the spawned unit. Depending on the selection, the unit will be more precise and effective at its mission. Usually a lower level is selected to generate a more forgiving mission."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
>
|
||||
{["Average", "Good", "High", "Excellent"].map((skill) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
@ -836,47 +963,55 @@ export function UnitSpawnMenu(props: {
|
||||
</OlDropdown>
|
||||
</div>
|
||||
<div className="my-5 flex justify-between">
|
||||
<div className="my-auto flex flex-col gap-2">
|
||||
<span className="text-white">Spawn heading</span>
|
||||
<div className="flex gap-1 text-sm text-gray-400">
|
||||
<FaQuestionCircle className={`my-auto`} /> <div className={`
|
||||
my-auto
|
||||
`}>Drag to change</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OlNumberInput
|
||||
className={"my-auto"}
|
||||
min={0}
|
||||
max={360}
|
||||
onChange={(ev) => {
|
||||
setCompassAngle(Number(ev.target.value));
|
||||
}}
|
||||
onDecrease={() => {
|
||||
setCompassAngle(normalizeAngle(compassAngle - 1));
|
||||
}}
|
||||
onIncrease={() => {
|
||||
setCompassAngle(normalizeAngle(compassAngle + 1));
|
||||
}}
|
||||
value={compassAngle}
|
||||
/>
|
||||
|
||||
<div className={`relative mr-3 h-[60px] w-[60px]`}>
|
||||
<img className="absolute" ref={compassRef} onMouseDown={handleMouseDown} src={"/images/others/arrow_background.png"}></img>
|
||||
<img
|
||||
className="absolute left-0"
|
||||
ref={compassRef}
|
||||
onMouseDown={handleMouseDown}
|
||||
src={"/images/others/arrow.png"}
|
||||
style={{
|
||||
width: "60px",
|
||||
height: "60px",
|
||||
transform: `rotate(${compassAngle}deg)`,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
></img>
|
||||
<div className="my-auto flex flex-col gap-2">
|
||||
<span className="text-white">Spawn heading</span>
|
||||
<div className="flex gap-1 text-sm text-gray-400">
|
||||
<FaQuestionCircle className={`my-auto`} /> <div className={`
|
||||
my-auto
|
||||
`}>Drag to change</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OlNumberInput
|
||||
className={"my-auto"}
|
||||
min={0}
|
||||
max={360}
|
||||
onChange={(ev) => {
|
||||
setCompassAngle(Number(ev.target.value));
|
||||
}}
|
||||
onDecrease={() => {
|
||||
setCompassAngle(normalizeAngle(compassAngle - 1));
|
||||
}}
|
||||
onIncrease={() => {
|
||||
setCompassAngle(normalizeAngle(compassAngle + 1));
|
||||
}}
|
||||
value={compassAngle}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Spawn heading"
|
||||
content="This controls the direction the unit will face when spanwned. This is important for units that take longer to change direction, like ships. Air units and helicopters will enter a orbit with its major axis aligned with the spawn heading. Drag the compass to change the heading."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
tooltipPosition="above"
|
||||
/>
|
||||
|
||||
<div className={`relative mr-3 h-[60px] w-[60px]`}>
|
||||
<img className="absolute" ref={compassRef} onMouseDown={handleMouseDown} src={"/images/others/arrow_background.png"}></img>
|
||||
<img
|
||||
className="absolute left-0"
|
||||
ref={compassRef}
|
||||
onMouseDown={handleMouseDown}
|
||||
src={"/images/others/arrow.png"}
|
||||
style={{
|
||||
width: "60px",
|
||||
height: "60px",
|
||||
transform: `rotate(${compassAngle}deg)`,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
></img>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{spawnLoadout && spawnLoadout.items.length > 0 && (
|
||||
<div
|
||||
|
||||
@ -123,6 +123,15 @@ input[type="range"]:focus::-moz-range-thumb {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loadingBar {
|
||||
0% {
|
||||
width: 0;
|
||||
}
|
||||
100% {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.bouncing-ball {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
|
||||
@ -7,13 +7,11 @@ import { UnitControlMenu } from "./panels/unitcontrolmenu";
|
||||
import { MainMenu } from "./panels/mainmenu";
|
||||
import { SideBar } from "./panels/sidebar";
|
||||
import { OptionsMenu } from "./panels/optionsmenu";
|
||||
import { MapHiddenTypes, MapOptions } from "../types/types";
|
||||
import { NO_SUBSTATE, OlympusState, OlympusSubState, OptionsSubstate, UnitControlSubState } from "../constants/constants";
|
||||
import { getApp, setupApp } from "../olympusapp";
|
||||
import { LoginModal } from "./modals/loginmodal";
|
||||
|
||||
import { MiniMapPanel } from "./panels/minimappanel";
|
||||
import { MapToolBar } from "./panels/maptoolbar";
|
||||
import { DrawingMenu } from "./panels/drawingmenu";
|
||||
import { ControlsPanel } from "./panels/controlspanel";
|
||||
import { MapContextMenu } from "./contextmenus/mapcontextmenu";
|
||||
@ -23,7 +21,6 @@ import { FormationMenu } from "./panels/formationmenu";
|
||||
import { ProtectionPromptModal } from "./modals/protectionpromptmodal";
|
||||
import { KeybindModal } from "./modals/keybindmodal";
|
||||
import { UnitExplosionMenu } from "./panels/unitexplosionmenu";
|
||||
import { JTACMenu } from "./panels/jtacmenu";
|
||||
import { AppStateChangedEvent, ServerStatusUpdatedEvent } from "../events";
|
||||
import { GameMasterMenu } from "./panels/gamemastermenu";
|
||||
import { InfoBar } from "./panels/infobar";
|
||||
@ -31,23 +28,10 @@ import { HotGroupBar } from "./panels/hotgroupsbar";
|
||||
import { SpawnContextMenu } from "./contextmenus/spawncontextmenu";
|
||||
import { CoordinatesPanel } from "./panels/coordinatespanel";
|
||||
import { RadiosSummaryPanel } from "./panels/radiossummarypanel";
|
||||
import { AWACSMenu } from "./panels/awacsmenu";
|
||||
import { ServerOverlay } from "./serveroverlay";
|
||||
import { ImportExportModal } from "./modals/importexportmodal";
|
||||
import { WarningModal } from "./modals/warningmodal";
|
||||
import { ServerStatus } from "../interfaces";
|
||||
|
||||
export type OlympusUIState = {
|
||||
mainMenuVisible: boolean;
|
||||
spawnMenuVisible: boolean;
|
||||
unitControlMenuVisible: boolean;
|
||||
measureMenuVisible: boolean;
|
||||
drawingMenuVisible: boolean;
|
||||
optionsMenuVisible: boolean;
|
||||
airbaseMenuVisible: boolean;
|
||||
mapHiddenTypes: MapHiddenTypes;
|
||||
mapOptions: MapOptions;
|
||||
};
|
||||
import { TrainingModal } from "./modals/trainingmodal";
|
||||
|
||||
export function UI() {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
@ -90,6 +74,7 @@ export function UI() {
|
||||
<ImportExportModal open={appState === OlympusState.IMPORT_EXPORT} />
|
||||
<LoginModal open={appState === OlympusState.LOGIN} />
|
||||
<WarningModal open={appState === OlympusState.WARNING} />
|
||||
<TrainingModal open={appState === OlympusState.TRAINING} />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@ -1724,8 +1724,6 @@ export abstract class Unit extends CustomMarker {
|
||||
}
|
||||
|
||||
#drawRacetrack() {
|
||||
this.#clearRacetrack();
|
||||
|
||||
if (getApp().getMap().getOptions().showRacetracks) {
|
||||
let groundspeed = this.#speed;
|
||||
|
||||
@ -1805,6 +1803,8 @@ export abstract class Unit extends CustomMarker {
|
||||
}
|
||||
this.#racetrackArrow.setLatLng(pointArrow);
|
||||
this.#racetrackArrow.setBearing(this.#racetrackBearing);
|
||||
} else {
|
||||
this.#clearRacetrack();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||