mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Major controls rework
This commit is contained in:
@@ -11,6 +11,8 @@ export function OlStateButton(props: {
|
||||
icon?: IconProp;
|
||||
tooltip: string;
|
||||
onClick: () => void;
|
||||
onMouseUp?: () => void;
|
||||
onMouseDown?: () => void;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
}) {
|
||||
const [hover, setHover] = useState(false);
|
||||
@@ -36,8 +38,10 @@ export function OlStateButton(props: {
|
||||
ref={buttonRef}
|
||||
onClick={() => {
|
||||
props.onClick();
|
||||
setHover(false);
|
||||
props.onClick ?? setHover(false);
|
||||
}}
|
||||
onMouseUp={props.onMouseUp ?? (() => {})}
|
||||
onMouseDown={props.onMouseDown ?? (() => {})}
|
||||
data-checked={props.checked}
|
||||
type="button"
|
||||
className={className}
|
||||
|
||||
@@ -5,8 +5,7 @@ export function OlToggle(props: { toggled: boolean | undefined; onClick: () => v
|
||||
<div className="inline-flex cursor-pointer items-center" onClick={props.onClick}>
|
||||
<button className="peer sr-only" />
|
||||
<div
|
||||
data-flash={props.toggled === undefined}
|
||||
data-toggled={props.toggled ?? false}
|
||||
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
|
||||
@@ -14,10 +13,11 @@ export function OlToggle(props: { toggled: boolean | undefined; onClick: () => v
|
||||
after:transition-all after:content-['']
|
||||
dark:border-gray-600 dark:peer-focus:ring-blue-800
|
||||
dark:data-[toggled='true']:bg-blue-500
|
||||
data-[flash='true']:after:animate-pulse
|
||||
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
|
||||
`}
|
||||
|
||||
@@ -112,6 +112,11 @@ export function MapContextMenu(props: {}) {
|
||||
} else if (unit !== null) {
|
||||
contextActionIt.executeCallback(unit, null);
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
if (getApp().getSubState() === UnitControlSubState.MAP_CONTEXT_MENU || getApp().getSubState() === UnitControlSubState.UNIT_CONTEXT_MENU) {
|
||||
getApp().setState(OlympusState.UNIT_CONTROL)
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -6,37 +6,48 @@ import { getApp } from "../../olympusapp";
|
||||
import { OlympusState } from "../../constants/constants";
|
||||
import { Shortcut } from "../../shortcut/shortcut";
|
||||
import { BindShortcutRequestEvent, ShortcutsChangedEvent } from "../../events";
|
||||
import { OlToggle } from "../components/oltoggle";
|
||||
|
||||
export function KeybindModal(props: { open: boolean }) {
|
||||
const [shortcuts, setShortcuts] = useState({} as { [key: string]: Shortcut });
|
||||
const [shortcut, setShortcut] = useState(null as null | Shortcut);
|
||||
const [code, setCode] = useState(null as null | string);
|
||||
const [shiftKey, setShiftKey] = useState(false);
|
||||
const [ctrlKey, setCtrlKey] = useState(false);
|
||||
const [altKey, setAltKey] = useState(false);
|
||||
const [shiftKey, setShiftKey] = useState(false as boolean | undefined);
|
||||
const [ctrlKey, setCtrlKey] = useState(false as boolean | undefined);
|
||||
const [altKey, setAltKey] = useState(false as boolean | undefined);
|
||||
|
||||
useEffect(() => {
|
||||
ShortcutsChangedEvent.on((shortcuts) => setShortcuts({ ...shortcuts }));
|
||||
BindShortcutRequestEvent.on((shortcut) => setShortcut(shortcut));
|
||||
|
||||
document.addEventListener("keydown", (ev) => {
|
||||
setCode(ev.code);
|
||||
if (!(ev.code.indexOf("Shift") >= 0 || ev.code.indexOf("Alt") >= 0 || ev.code.indexOf("Control") >= 0)) {
|
||||
setShiftKey(ev.shiftKey);
|
||||
setAltKey(ev.altKey);
|
||||
setCtrlKey(ev.ctrlKey);
|
||||
if (ev.code) {
|
||||
setCode(ev.code);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setCode(shortcut?.getOptions().code ?? null)
|
||||
setShiftKey(shortcut?.getOptions().shiftKey)
|
||||
setAltKey(shortcut?.getOptions().altKey)
|
||||
setCtrlKey(shortcut?.getOptions().ctrlKey)
|
||||
}, [shortcut])
|
||||
|
||||
let available: null | boolean = code ? true : null;
|
||||
let inUseShortcut: null | Shortcut = null;
|
||||
for (let id in shortcuts) {
|
||||
if (
|
||||
if (id !== shortcut?.getId() &&
|
||||
code === shortcuts[id].getOptions().code &&
|
||||
shiftKey === (shortcuts[id].getOptions().shiftKey ?? false) &&
|
||||
altKey === (shortcuts[id].getOptions().altKey ?? false) &&
|
||||
ctrlKey === (shortcuts[id].getOptions().shiftKey ?? false)
|
||||
((shiftKey === undefined && shortcuts[id].getOptions().shiftKey !== undefined) ||
|
||||
(shiftKey !== undefined && shortcuts[id].getOptions().shiftKey === undefined) ||
|
||||
shiftKey === shortcuts[id].getOptions().shiftKey) && (
|
||||
(altKey === undefined && shortcuts[id].getOptions().altKey !== undefined) ||
|
||||
(altKey !== undefined && shortcuts[id].getOptions().altKey === undefined) ||
|
||||
altKey === shortcuts[id].getOptions().altKey) && (
|
||||
(ctrlKey === undefined && shortcuts[id].getOptions().ctrlKey !== undefined) ||
|
||||
(ctrlKey !== undefined && shortcuts[id].getOptions().ctrlKey === undefined) ||
|
||||
ctrlKey === shortcuts[id].getOptions().ctrlKey)
|
||||
) {
|
||||
available = false;
|
||||
inUseShortcut = shortcuts[id];
|
||||
@@ -75,26 +86,66 @@ export function KeybindModal(props: { open: boolean }) {
|
||||
Press the key you want to bind to this event
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full text-center text-white">
|
||||
{ctrlKey ? "Ctrl + " : ""}
|
||||
{shiftKey ? "Shift + " : ""}
|
||||
{altKey ? "Alt + " : ""}
|
||||
|
||||
{code}
|
||||
<div className="w-full text-center text-white">{code}</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<OlToggle
|
||||
onClick={() => {
|
||||
if (shiftKey === false) setShiftKey(undefined);
|
||||
else if (shiftKey === undefined) setShiftKey(true);
|
||||
else setShiftKey(false);
|
||||
}}
|
||||
toggled={shiftKey}
|
||||
></OlToggle>
|
||||
<div className="text-white">
|
||||
{shiftKey === true && "Shift key must be pressed"}
|
||||
{shiftKey === undefined && "Shift key can be anything"}
|
||||
{shiftKey === false && "Shift key must NOT be pressed"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<OlToggle
|
||||
onClick={() => {
|
||||
if (altKey === false) setAltKey(undefined);
|
||||
else if (altKey === undefined) setAltKey(true);
|
||||
else setAltKey(false);
|
||||
}}
|
||||
toggled={altKey}
|
||||
></OlToggle>
|
||||
<div className="text-white">
|
||||
{altKey === true && "Alt key must be pressed"}
|
||||
{altKey === undefined && "Alt key can be anything"}
|
||||
{altKey === false && "Alt key must NOT be pressed"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<OlToggle
|
||||
onClick={() => {
|
||||
if (ctrlKey === false) setCtrlKey(undefined);
|
||||
else if (ctrlKey === undefined) setCtrlKey(true);
|
||||
else setCtrlKey(false);
|
||||
}}
|
||||
toggled={ctrlKey}
|
||||
></OlToggle>
|
||||
<div className="text-white">
|
||||
{ctrlKey === true && "Ctrl key must be pressed"}
|
||||
{ctrlKey === undefined && "Ctrl key can be anything"}
|
||||
{ctrlKey === false && "Ctrl key must NOT be pressed"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="text-white">
|
||||
{available === true && <div className="text-green-600">Keybind is free!</div>}
|
||||
{available === false && (
|
||||
<div>
|
||||
Keybind is already in use:{" "}
|
||||
<span
|
||||
className={`font-bold text-red-600`}
|
||||
>
|
||||
{inUseShortcut?.getOptions().label}
|
||||
</span>
|
||||
Keybind is already in use: <span className={`
|
||||
font-bold text-red-600
|
||||
`}>{inUseShortcut?.getOptions().label}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
{available && shortcut && (
|
||||
<button
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Modal } from "./components/modal";
|
||||
import { Card } from "./components/card";
|
||||
import { ErrorCallout } from "../../ui/components/olcallout";
|
||||
@@ -7,6 +7,7 @@ import { faArrowRight, faCheckCircle, faExternalLink } from "@fortawesome/free-s
|
||||
import { getApp, VERSION } from "../../olympusapp";
|
||||
import { sha256 } from "js-sha256";
|
||||
import { BLUE_COMMANDER, GAME_MASTER, OlympusState, RED_COMMANDER } from "../../constants/constants";
|
||||
import { FaTrash, FaXmark } from "react-icons/fa6";
|
||||
|
||||
export function LoginModal(props: {}) {
|
||||
// TODO: add warning if not in secure context and some features are disabled
|
||||
@@ -16,6 +17,11 @@ export function LoginModal(props: {}) {
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
const [commandMode, setCommandMode] = useState(null as null | string);
|
||||
|
||||
useEffect(() => {
|
||||
/* Set the profile name */
|
||||
getApp().setProfile(profileName);
|
||||
}, [profileName])
|
||||
|
||||
function checkPassword(password: string) {
|
||||
setCheckingPassword(true);
|
||||
var hash = sha256.create();
|
||||
@@ -44,8 +50,6 @@ export function LoginModal(props: {}) {
|
||||
getApp().getServerManager().startUpdate();
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
|
||||
/* Set the profile name */
|
||||
getApp().setProfile(profileName);
|
||||
/* If no profile exists already with that name, create it from scratch from the defaults */
|
||||
if (getApp().getProfile() === null)
|
||||
getApp().saveProfile();
|
||||
@@ -56,7 +60,7 @@ export function LoginModal(props: {}) {
|
||||
return (
|
||||
<Modal
|
||||
className={`
|
||||
inline-flex h-[75%] max-h-[530px] w-[80%] max-w-[1100px] overflow-y-auto
|
||||
inline-flex h-[75%] max-h-[570px] 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
|
||||
@@ -236,7 +240,7 @@ export function LoginModal(props: {}) {
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
The profile name you choose determines what keybinds/groups/options get loaded and edited. Be careful!
|
||||
The profile name you choose determines the saved key binds, groups and options you see.
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
@@ -279,8 +283,8 @@ export function LoginModal(props: {}) {
|
||||
) : (
|
||||
<>
|
||||
<ErrorCallout
|
||||
title="Server could not be reached"
|
||||
description="The Olympus Server at this address could not be reached. Check the address is correct, restart the Olympus server or reinstall Olympus. Ensure the ports set are not already used."
|
||||
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
|
||||
|
||||
@@ -11,9 +11,7 @@ import { UnitSinkPanel } from "./components/unitsinkpanel";
|
||||
import { UnitSink } from "../../audio/unitsink";
|
||||
import { FaMinus, FaVolumeHigh } from "react-icons/fa6";
|
||||
import { getRandomColor } from "../../other/utils";
|
||||
import { AudioManagerStateChangedEvent, AudioSinksChangedEvent, AudioSourcesChangedEvent } from "../../events";
|
||||
|
||||
let shortcutKeys = ["Z", "X", "C", "V", "B", "N", "M", "K", "L"];
|
||||
import { AudioManagerStateChangedEvent, AudioSinksChangedEvent, AudioSourcesChangedEvent, ShortcutsChangedEvent } from "../../events";
|
||||
|
||||
export function AudioMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
|
||||
const [sinks, setSinks] = useState([] as AudioSink[]);
|
||||
@@ -21,6 +19,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
|
||||
const [audioManagerEnabled, setAudioManagerEnabled] = useState(false);
|
||||
const [activeSource, setActiveSource] = useState(null as AudioSource | null);
|
||||
const [count, setCount] = useState(0);
|
||||
const [shortcuts, setShortcuts] = useState({})
|
||||
|
||||
/* Preallocate 128 references for the source and sink panels. If the number of references changes, React will give an error */
|
||||
const sourceRefs = Array(128)
|
||||
@@ -60,6 +59,8 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
|
||||
AudioManagerStateChangedEvent.on(() => {
|
||||
setAudioManagerEnabled(getApp().getAudioManager().isRunning());
|
||||
});
|
||||
|
||||
ShortcutsChangedEvent.on((shortcuts) => setShortcuts(shortcuts));
|
||||
}, []);
|
||||
|
||||
/* When the sinks or sources change, use the count state to force a rerender to update the connection lines */
|
||||
@@ -180,7 +181,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
|
||||
if (sink instanceof RadioSink)
|
||||
return (
|
||||
<RadioSinkPanel
|
||||
shortcutKey={shortcutKeys[idx]}
|
||||
shortcutKeys={shortcuts[`PTT${idx}Active`].toActions()}
|
||||
key={sink.getName()}
|
||||
radio={sink}
|
||||
onExpanded={() => {
|
||||
@@ -218,7 +219,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
|
||||
if (sink instanceof UnitSink)
|
||||
return (
|
||||
<UnitSinkPanel
|
||||
shortcutKey={shortcutKeys[idx]}
|
||||
shortcutKeys={shortcuts[`PTT${idx}Active`].toActions()}
|
||||
key={sink.getName()}
|
||||
sink={sink}
|
||||
ref={sinkRefs[idx]}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { faEarListen, faMicrophoneLines } from "@fortawesome/free-solid-svg-icon
|
||||
import { RadioSink } from "../../../audio/radiosink";
|
||||
import { getApp } from "../../../olympusapp";
|
||||
|
||||
export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey: string; onExpanded: () => void }, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKeys: string[]; onExpanded: () => void }, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -39,22 +39,20 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
|
||||
data-expanded={expanded}
|
||||
/>
|
||||
</div>
|
||||
{props.shortcutKey && (
|
||||
{props.shortcutKeys && (
|
||||
<>
|
||||
<kbd
|
||||
className={`
|
||||
my-auto ml-auto rounded-lg border border-gray-200 bg-gray-100
|
||||
px-2 py-1.5 text-xs font-semibold text-gray-800
|
||||
my-auto ml-auto text-nowrap rounded-lg border border-gray-200
|
||||
bg-gray-100 px-2 py-1.5 text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
{props.shortcutKey}
|
||||
{props.shortcutKeys.flatMap((key, idx, array) => [key, idx < array.length - 1 ? " + " : ""])}
|
||||
</kbd>
|
||||
</>
|
||||
)}
|
||||
<span className="my-auto w-full">
|
||||
{props.radio.getName()} {!expanded && `: ${props.radio.getFrequency() / 1e6} MHz ${props.radio.getModulation() ? "FM" : "AM"}`} {}{" "}
|
||||
</span>
|
||||
<span className="my-auto w-full">{props.radio.getName()}</span>
|
||||
<div
|
||||
className={`
|
||||
mb-auto ml-auto aspect-square cursor-pointer rounded-md p-2
|
||||
@@ -89,8 +87,12 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
|
||||
className="ml-auto"
|
||||
checked={props.radio.getPtt()}
|
||||
icon={faMicrophoneLines}
|
||||
onClick={() => {
|
||||
props.radio.setPtt(!props.radio.getPtt());
|
||||
onClick={() => {}}
|
||||
onMouseDown={() => {
|
||||
props.radio.setPtt(true);
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
props.radio.setPtt(false);
|
||||
}}
|
||||
tooltip="Talk on frequency"
|
||||
></OlStateButton>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { OlStateButton } from "../../components/olstatebutton";
|
||||
import { faMicrophoneLines } from "@fortawesome/free-solid-svg-icons";
|
||||
import { OlRangeSlider } from "../../components/olrangeslider";
|
||||
|
||||
export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKey: string; onExpanded: () => void }, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKeys: string[]; onExpanded: () => void }, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -36,20 +36,21 @@ export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKey: s
|
||||
data-expanded={expanded}
|
||||
/>
|
||||
</div>
|
||||
{props.shortcutKey && (<>
|
||||
<kbd
|
||||
className={`
|
||||
my-auto ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2
|
||||
py-1.5 text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
{props.shortcutKey}
|
||||
</kbd>
|
||||
{props.shortcutKeys && (
|
||||
<>
|
||||
<kbd
|
||||
className={`
|
||||
my-auto ml-auto text-nowrap rounded-lg border border-gray-200
|
||||
bg-gray-100 px-2 py-1.5 text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
{props.shortcutKeys.flatMap((key, idx, array) => [key, idx < array.length - 1 ? " + " : ""])}
|
||||
</kbd>
|
||||
</>
|
||||
)}
|
||||
<div className="flex w-full overflow-hidden">
|
||||
<span className="my-auto truncate"> {props.sink.getName()}</span>
|
||||
<span className="my-auto truncate"> {props.sink.getName()}</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
@@ -79,8 +80,12 @@ export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKey: s
|
||||
<OlStateButton
|
||||
checked={props.sink.getPtt()}
|
||||
icon={faMicrophoneLines}
|
||||
onClick={() => {
|
||||
props.sink.setPtt(!props.sink.getPtt());
|
||||
onClick={() => {}}
|
||||
onMouseDown={() => {
|
||||
props.sink.setPtt(true);
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
props.sink.setPtt(false);
|
||||
}}
|
||||
tooltip="Talk on frequency"
|
||||
></OlStateButton>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { faHandPointer, faJetFighter, faMap, IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faFighterJet, faHandPointer, faJetFighter, faMap, IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { ContextActionTarget, MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusState, OlympusSubState, SpawnSubState } from "../../constants/constants";
|
||||
import { AppStateChangedEvent, MapOptionsChangedEvent } from "../../events";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusState, OlympusSubState, SpawnSubState } from "../../constants/constants";
|
||||
import { AppStateChangedEvent, ContextActionSetChangedEvent, MapOptionsChangedEvent, ShortcutsChangedEvent } from "../../events";
|
||||
import { ContextAction } from "../../unit/contextaction";
|
||||
import { ContextActionSet } from "../../unit/contextactionset";
|
||||
|
||||
export function ControlsPanel(props: {}) {
|
||||
const [controls, setControls] = useState(
|
||||
null as
|
||||
| {
|
||||
actions: (string | number | IconDefinition)[];
|
||||
target: IconDefinition | null;
|
||||
target?: IconDefinition;
|
||||
text: string;
|
||||
}[]
|
||||
| null
|
||||
@@ -19,6 +19,8 @@ export function ControlsPanel(props: {}) {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState);
|
||||
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
|
||||
const [shortcuts, setShortcuts] = useState({})
|
||||
const [contextActionSet, setContextActionSet] = useState(null as null | ContextActionSet)
|
||||
|
||||
useEffect(() => {
|
||||
AppStateChangedEvent.on((state, subState) => {
|
||||
@@ -26,65 +28,61 @@ export function ControlsPanel(props: {}) {
|
||||
setAppSubState(subState);
|
||||
});
|
||||
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
|
||||
ShortcutsChangedEvent.on((shortcuts) => setShortcuts(shortcuts));
|
||||
ContextActionSetChangedEvent.on((contextActionSet) => setContextActionSet(contextActionSet));
|
||||
}, []);
|
||||
|
||||
const callback = useCallback(() => {
|
||||
const touch = matchMedia("(hover: none)").matches;
|
||||
let controls: {
|
||||
actions: (string | number | IconDefinition)[];
|
||||
target: IconDefinition | null;
|
||||
target?: IconDefinition;
|
||||
text: string;
|
||||
}[] = [];
|
||||
|
||||
const baseControls = [
|
||||
{
|
||||
actions: [touch ? faHandPointer : "LMB"],
|
||||
text: "Select unit",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : "LMB", "Drag"],
|
||||
text: "Box selection",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : "Wheel", "Drag"],
|
||||
text: "Move map",
|
||||
},
|
||||
];
|
||||
if (!touch) {
|
||||
controls.push({
|
||||
actions: ["Shift", "LMB", "Drag"],
|
||||
|
||||
text: "Box selection",
|
||||
});
|
||||
}
|
||||
|
||||
if (appState === OlympusState.IDLE) {
|
||||
controls = [
|
||||
{
|
||||
actions: [touch ? faHandPointer : "LMB"],
|
||||
target: faJetFighter,
|
||||
text: "Select unit",
|
||||
},
|
||||
{
|
||||
actions: touch ? [faHandPointer, "Hold"] : ["RMB"],
|
||||
target: faMap,
|
||||
text: "Quick spawn menu",
|
||||
},
|
||||
{
|
||||
actions: touch ? [faHandPointer, "Drag"] : ["Shift", "LMB", "Drag"],
|
||||
target: faMap,
|
||||
text: "Box selection",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : "LMB", "Drag"],
|
||||
target: faMap,
|
||||
text: "Move map location",
|
||||
},
|
||||
];
|
||||
controls = baseControls;
|
||||
controls.push({
|
||||
actions: touch ? [faHandPointer, "Hold"] : ["RMB"],
|
||||
text: "Quick spawn menu",
|
||||
});
|
||||
} else if (appState === OlympusState.SPAWN_CONTEXT) {
|
||||
controls = [
|
||||
controls = baseControls;
|
||||
controls.push(
|
||||
{
|
||||
actions: [touch ? faHandPointer : "LMB"],
|
||||
target: faJetFighter,
|
||||
text: "Close context menu",
|
||||
},
|
||||
{
|
||||
actions: touch ? [faHandPointer, "Hold"] : ["RMB"],
|
||||
target: faMap,
|
||||
text: "Move context menu",
|
||||
},
|
||||
{
|
||||
actions: touch ? [faHandPointer, "Drag"] : ["Shift", "LMB", "Drag"],
|
||||
target: faMap,
|
||||
text: "Box selection",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : "LMB", "Drag"],
|
||||
target: faMap,
|
||||
text: "Move map location",
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
} else if (appState === OlympusState.UNIT_CONTROL) {
|
||||
if (!mapOptions.tabletMode) {
|
||||
controls = Object.values(getApp().getMap().getContextActionSet()?.getContextActions() ?? {})
|
||||
controls = Object.values(contextActionSet?.getContextActions() ?? {})
|
||||
.sort((a: ContextAction, b: ContextAction) => (a.getLabel() > b.getLabel() ? 1 : -1))
|
||||
.filter((contextAction: ContextAction) => contextAction.getOptions().code)
|
||||
.map((contextAction: ContextAction) => {
|
||||
@@ -95,53 +93,73 @@ export function ControlsPanel(props: {}) {
|
||||
actions.push(
|
||||
(contextAction.getOptions().code as string)
|
||||
.replace("Key", "")
|
||||
.replace("ControlLeft", "Ctrl LH")
|
||||
.replace("AltLeft", "Alt LH")
|
||||
.replace("ShiftLeft", "Shift LH")
|
||||
.replace("ControlRight", "Ctrl RH")
|
||||
.replace("AltRight", "Alt RH")
|
||||
.replace("ShiftRight", "Shift RH")
|
||||
.replace("ControlLeft", "Left Ctrl")
|
||||
.replace("AltLeft", "Left Alt")
|
||||
.replace("ShiftLeft", "Left Shift")
|
||||
.replace("ControlRight", "Right Ctrl")
|
||||
.replace("AltRight", "Right Alt")
|
||||
.replace("ShiftRight", "Right Shift")
|
||||
);
|
||||
contextAction.getTarget() !== ContextActionTarget.NONE && actions.push(touch ? faHandPointer : "LMB");
|
||||
return {
|
||||
actions: actions,
|
||||
target:
|
||||
contextAction.getTarget() === ContextActionTarget.NONE ? null : contextAction.getTarget() === ContextActionTarget.POINT ? faMap : faJetFighter,
|
||||
text: contextAction.getLabel(),
|
||||
};
|
||||
});
|
||||
controls.unshift({
|
||||
actions: ["RMB"],
|
||||
text: "Move",
|
||||
});
|
||||
controls.push({
|
||||
actions: ["RMB", "Hold"],
|
||||
target: faMap,
|
||||
text: "Show point actions",
|
||||
});
|
||||
controls.push({
|
||||
actions: ["RMB", "Hold"],
|
||||
target: faFighterJet,
|
||||
text: "Show unit actions",
|
||||
});
|
||||
controls.push({
|
||||
actions: shortcuts["toggleRelativePositions"]?.toActions(),
|
||||
text: "Activate group movement",
|
||||
});
|
||||
controls.push({
|
||||
actions: [...shortcuts["toggleRelativePositions"]?.toActions(), "Wheel"],
|
||||
text: "Rotate formation",
|
||||
});
|
||||
}
|
||||
} else if (appState === OlympusState.SPAWN) {
|
||||
controls = [
|
||||
{
|
||||
actions: [touch ? faHandPointer : "LMB", 2],
|
||||
target: faMap,
|
||||
text: appSubState === SpawnSubState.NO_SUBSTATE ? "Close spawn menu" : "Return to spawn menu",
|
||||
},
|
||||
{
|
||||
actions: touch ? [faHandPointer, "Drag"] : ["Shift", "LMB", "Drag"],
|
||||
target: faMap,
|
||||
text: "Box selection",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : "LMB", "Drag"],
|
||||
target: faMap,
|
||||
text: "Move map location",
|
||||
},
|
||||
];
|
||||
if (appSubState === SpawnSubState.SPAWN_UNIT) {
|
||||
controls.unshift({
|
||||
actions: [touch ? faHandPointer : "LMB"],
|
||||
target: faMap,
|
||||
text: "Spawn unit",
|
||||
});
|
||||
} else if (appSubState === SpawnSubState.SPAWN_EFFECT) {
|
||||
controls.unshift({
|
||||
actions: [touch ? faHandPointer : "LMB"],
|
||||
target: faMap,
|
||||
text: "Spawn effect",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
controls = baseControls;
|
||||
controls.push({
|
||||
actions: ["LMB"],
|
||||
text: "Return to idle state"
|
||||
})
|
||||
}
|
||||
|
||||
setControls(controls);
|
||||
@@ -178,20 +196,23 @@ export function ControlsPanel(props: {}) {
|
||||
return (
|
||||
<div key={idx} className="flex gap-1">
|
||||
<div>
|
||||
{typeof action === "string" || typeof action === "number" ? (
|
||||
action
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={action}
|
||||
className={`my-auto ml-auto`}
|
||||
/>
|
||||
)}
|
||||
{typeof action === "string" || typeof action === "number" ? action : <FontAwesomeIcon icon={action} className={`
|
||||
my-auto ml-auto
|
||||
`} />}
|
||||
</div>
|
||||
{idx < control.actions.length - 1 && typeof control.actions[idx + 1] === "string" && <div>+</div>}
|
||||
{idx < control.actions.length - 1 && typeof control.actions[idx + 1] === "number" && <div>x</div>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{control.target && (
|
||||
<>
|
||||
<div>+</div>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={control.target} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { BindShortcutRequestEvent, MapOptionsChangedEvent, ShortcutsChangedEvent
|
||||
import { OlAccordion } from "../components/olaccordion";
|
||||
import { Shortcut } from "../../shortcut/shortcut";
|
||||
import { OlSearchBar } from "../components/olsearchbar";
|
||||
import { FaTrash, FaXmark } from "react-icons/fa6";
|
||||
|
||||
const enum Accordion {
|
||||
NONE,
|
||||
@@ -32,14 +33,12 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
|
||||
<Menu title="User preferences" open={props.open} showBackButton={false} onClose={props.onClose}>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col gap-2 p-5 font-normal text-gray-800
|
||||
flex h-full flex-col justify-end gap-2 p-5 font-normal text-gray-800
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
<OlAccordion
|
||||
onClick={() =>
|
||||
setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.BINDINGS: Accordion.NONE )
|
||||
}
|
||||
onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.BINDINGS : Accordion.NONE)}
|
||||
open={openAccordion === Accordion.BINDINGS}
|
||||
title="Key bindings"
|
||||
>
|
||||
@@ -65,10 +64,40 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
|
||||
}}
|
||||
>
|
||||
<span>{shortcut.getOptions().label}</span>
|
||||
<span>
|
||||
{shortcut.getOptions().altKey ? "Alt + " : ""}
|
||||
{shortcut.getOptions().ctrlKey ? "Ctrl + " : ""}
|
||||
{shortcut.getOptions().shiftKey ? "Shift + " : ""}
|
||||
<span className="flex gap-1">
|
||||
{shortcut.getOptions().altKey ? (
|
||||
<div className="flex gap-1">
|
||||
<div className={`text-green-500`}>Alt</div> +{" "}
|
||||
</div>
|
||||
) : shortcut.getOptions().altKey === false ? (
|
||||
<div className={`flex gap-1`}>
|
||||
<div className={`text-red-500`}>Alt</div> +{" "}
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{shortcut.getOptions().ctrlKey ? (
|
||||
<div className="flex gap-1">
|
||||
<div className={`text-green-500`}>Shift</div> +{" "}
|
||||
</div>
|
||||
) : shortcut.getOptions().ctrlKey === false ? (
|
||||
<div className={`flex gap-1`}>
|
||||
<div className={`text-red-500`}>Shift</div> +{" "}
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{shortcut.getOptions().shiftKey ? (
|
||||
<div className="flex gap-1">
|
||||
<div className={`text-green-500`}>Ctrl</div> +{" "}
|
||||
</div>
|
||||
) : shortcut.getOptions().shiftKey === false ? (
|
||||
<div className={`flex gap-1`}>
|
||||
<div className={`text-red-500`}>Ctrl</div> +{" "}
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{shortcut.getOptions().code}
|
||||
</span>
|
||||
</div>
|
||||
@@ -77,7 +106,11 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
|
||||
</div>
|
||||
</OlAccordion>
|
||||
|
||||
<OlAccordion onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.MAP_OPTIONS: Accordion.NONE )} open={openAccordion === Accordion.MAP_OPTIONS} title="Map options">
|
||||
<OlAccordion
|
||||
onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.MAP_OPTIONS : Accordion.NONE)}
|
||||
open={openAccordion === Accordion.MAP_OPTIONS}
|
||||
title="Map options"
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer
|
||||
@@ -133,17 +166,7 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
|
||||
<OlCheckbox checked={mapOptions.hideUnitsShortRangeRings} onChange={() => {}}></OlCheckbox>
|
||||
<span>Hide Short range Rings</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row gap-4 rounded-md justify-content
|
||||
cursor-pointer p-2 text-sm
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getMap().setOption("keepRelativePositions", !mapOptions.keepRelativePositions)}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.keepRelativePositions} onChange={() => {}}></OlCheckbox>
|
||||
<span>Keep units relative positions</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row gap-4 rounded-md justify-content
|
||||
@@ -168,7 +191,11 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
|
||||
</div>
|
||||
</OlAccordion>
|
||||
|
||||
<OlAccordion onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.CAMERA_PLUGIN: Accordion.NONE )} open={openAccordion === Accordion.CAMERA_PLUGIN} title="Camera plugin options">
|
||||
<OlAccordion
|
||||
onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.CAMERA_PLUGIN : Accordion.NONE)}
|
||||
open={openAccordion === Accordion.CAMERA_PLUGIN}
|
||||
title="Camera plugin options"
|
||||
>
|
||||
<hr
|
||||
className={`
|
||||
m-2 my-1 w-auto border-[1px] bg-gray-700
|
||||
@@ -231,6 +258,41 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
|
||||
</div>
|
||||
</div>
|
||||
</OlAccordion>
|
||||
|
||||
<div className="mt-auto flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().resetProfile()}
|
||||
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-red-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
|
||||
`}
|
||||
>
|
||||
Reset profile
|
||||
<FaXmark />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().resetAllProfiles()}
|
||||
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-red-600 dark:bg-red-800 dark:text-gray-400
|
||||
dark:hover:bg-red-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-red-800
|
||||
`}
|
||||
>
|
||||
Reset all profiles
|
||||
<FaTrash />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
74
frontend/react/src/ui/panels/radiossummarypanel.tsx
Normal file
74
frontend/react/src/ui/panels/radiossummarypanel.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { AudioSinksChangedEvent } from "../../events";
|
||||
import { AudioSink } from "../../audio/audiosink";
|
||||
import { RadioSink } from "../../audio/radiosink";
|
||||
import { FaJetFighter, FaRadio } from "react-icons/fa6";
|
||||
import { OlStateButton } from "../components/olstatebutton";
|
||||
import { UnitSink } from "../../audio/unitsink";
|
||||
|
||||
export function RadiosSummaryPanel(props: {}) {
|
||||
const [audioSinks, setAudioSinks] = useState([] as AudioSink[]);
|
||||
|
||||
useEffect(() => {
|
||||
AudioSinksChangedEvent.on((audioSinks) => setAudioSinks(audioSinks));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{audioSinks.length > 0 && (
|
||||
<div
|
||||
className={`
|
||||
absolute bottom-[20px] right-[700px] flex w-fit flex-col
|
||||
items-center justify-between gap-2 rounded-lg bg-gray-200 p-3
|
||||
text-sm backdrop-blur-lg backdrop-grayscale
|
||||
dark:bg-olympus-800/90 dark:text-gray-200
|
||||
`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<FaRadio className="text-xl" />
|
||||
{audioSinks
|
||||
.filter((audioSinks) => audioSinks instanceof RadioSink)
|
||||
.map((radioSink, idx) => {
|
||||
return (
|
||||
<OlStateButton
|
||||
checked={radioSink.getReceiving()}
|
||||
onClick={() => {}}
|
||||
onMouseDown={() => {
|
||||
radioSink.setPtt(true);
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
radioSink.setPtt(false);
|
||||
}}
|
||||
tooltip="Click to talk, lights up when receiving"
|
||||
>
|
||||
<span className={`font-bold text-gray-200`}>{idx + 1}</span>
|
||||
</OlStateButton>
|
||||
);
|
||||
})}
|
||||
|
||||
<FaJetFighter className="text-xl" />
|
||||
{audioSinks
|
||||
.filter((audioSinks) => audioSinks instanceof UnitSink)
|
||||
.map((radioSink, idx) => {
|
||||
return (
|
||||
<OlStateButton
|
||||
checked={false}
|
||||
onClick={() => {}}
|
||||
onMouseDown={() => {
|
||||
radioSink.setPtt(true);
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
radioSink.setPtt(false);
|
||||
}}
|
||||
tooltip="Click to talk"
|
||||
>
|
||||
<span className={`font-bold text-gray-200`}>{idx + 1}</span>
|
||||
</OlStateButton>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { FaInfoCircle } from "react-icons/fa";
|
||||
import { FaChevronDown, FaChevronLeft, FaChevronRight, FaChevronUp } from "react-icons/fa6";
|
||||
import { OlympusState } from "../../constants/constants";
|
||||
import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent, MapOptionsChangedEvent } from "../../events";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export function UnitControlBar(props: {}) {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
@@ -111,24 +112,20 @@ export function UnitControlBar(props: {}) {
|
||||
{contextAction && (
|
||||
<div
|
||||
className={`
|
||||
absolute left-[50%] top-16 flex min-w-[300px]
|
||||
translate-x-[calc(-50%+2rem)] items-center gap-2 rounded-md
|
||||
bg-gray-200 p-4
|
||||
absolute left-[50%] top-16 flex translate-x-[calc(-50%+2rem)]
|
||||
items-center gap-2 rounded-md bg-gray-200 p-4
|
||||
dark:bg-olympus-800
|
||||
`}
|
||||
>
|
||||
<FaInfoCircle
|
||||
<FontAwesomeIcon
|
||||
icon={contextAction.getIcon()}
|
||||
className={`
|
||||
mr-2 hidden min-w-8 text-sm text-blue-500
|
||||
mr-2 hidden text-xl text-blue-500
|
||||
md:block
|
||||
`}
|
||||
/>
|
||||
<div
|
||||
className={`
|
||||
px-2
|
||||
dark:text-gray-400
|
||||
md:border-l-[1px] md:px-5
|
||||
`}
|
||||
className={`text-gray-200`}
|
||||
>
|
||||
{contextAction.getDescription()}
|
||||
</div>
|
||||
|
||||
@@ -24,12 +24,13 @@ import { ProtectionPrompt } from "./modals/protectionprompt";
|
||||
import { KeybindModal } from "./modals/keybindmodal";
|
||||
import { UnitExplosionMenu } from "./panels/unitexplosionmenu";
|
||||
import { JTACMenu } from "./panels/jtacmenu";
|
||||
import { AppStateChangedEvent, MapOptionsChangedEvent } from "../events";
|
||||
import { AppStateChangedEvent } from "../events";
|
||||
import { GameMasterMenu } from "./panels/gamemastermenu";
|
||||
import { InfoBar } from "./panels/infobar";
|
||||
import { HotGroupBar } from "./panels/hotgroupsbar";
|
||||
import { SpawnContextMenu } from "./contextmenus/spawncontextmenu";
|
||||
import { CoordinatesPanel } from "./panels/coordinatespanel";
|
||||
import { RadiosSummaryPanel } from "./panels/radiossummarypanel";
|
||||
|
||||
export type OlympusUIState = {
|
||||
mainMenuVisible: boolean;
|
||||
@@ -105,6 +106,7 @@ export function UI() {
|
||||
<MiniMapPanel />
|
||||
<ControlsPanel />
|
||||
<CoordinatesPanel />
|
||||
<RadiosSummaryPanel />
|
||||
|
||||
<UnitControlBar />
|
||||
<SideBar />
|
||||
|
||||
Reference in New Issue
Block a user