import { useMemo } from "react"; import { LuArrowUp, LuArrowDown, LuX, LuTrash2 } from "react-icons/lu"; import { Button } from "@components/Button"; import { Combobox, ComboboxOption } from "@components/Combobox"; import Card from "@components/Card"; import FieldLabel from "@components/FieldLabel"; import { SelectMenuBasic } from "@components/SelectMenuBasic"; import { MAX_KEYS_PER_STEP, DEFAULT_DELAY } from "@/constants/macros"; import { KeyboardLayout } from "@/keyboardLayouts"; import { keys, modifiers } from "@/keyboardMappings"; import { m } from "@localizations/messages.js"; // Filter out modifier keys since they're handled in the modifiers section const modifierKeyPrefixes = ["Alt", "Control", "Shift", "Meta"]; const modifierOptions = Object.keys(modifiers).map(modifier => ({ value: modifier, label: modifier.replace(/^(Control|Alt|Shift|Meta)(Left|Right)$/, "$1 $2"), })); const groupedModifiers: Record = { Control: modifierOptions.filter(mod => mod.value.startsWith("Control")), Shift: modifierOptions.filter(mod => mod.value.startsWith("Shift")), Alt: modifierOptions.filter(mod => mod.value.startsWith("Alt")), Meta: modifierOptions.filter(mod => mod.value.startsWith("Meta")), }; // not going to localize these since they're short time intervals const basePresetDelays = [ { value: "50", label: "50ms" }, { value: "100", label: "100ms" }, { value: "200", label: "200ms" }, { value: "300", label: "300ms" }, { value: "500", label: "500ms" }, { value: "750", label: "750ms" }, { value: "1000", label: "1000ms" }, { value: "1500", label: "1500ms" }, { value: "2000", label: "2000ms" }, ]; const PRESET_DELAYS = basePresetDelays.map(delay => { if (Number.parseInt(delay.value, 10) === DEFAULT_DELAY) { return { ...delay, label: "Default" }; } return delay; }); interface MacroStep { keys: string[]; modifiers: string[]; delay: number; } interface MacroStepCardProps { step: MacroStep; stepIndex: number; onDelete?: () => void; onMoveUp?: () => void; onMoveDown?: () => void; onKeySelect: (option: { value: string | null; keys?: string[] }) => void; onKeyQueryChange: (query: string) => void; keyQuery: string; onModifierChange: (modifiers: string[]) => void; onDelayChange: (delay: number) => void; isLastStep: boolean; keyboard: KeyboardLayout; } const ensureArray = (arr: T[] | null | undefined): T[] => { return Array.isArray(arr) ? arr : []; }; const keyDisplay = (keyDisplayMap: Record, key: string): string => { return keyDisplayMap[key] || key }; export function MacroStepCard({ step, stepIndex, onDelete, onMoveUp, onMoveDown, onKeySelect, onKeyQueryChange, keyQuery, onModifierChange, onDelayChange, isLastStep, keyboard, }: Readonly) { const { keyDisplayMap } = keyboard; const keyOptions = useMemo( () => Object.keys(keys) .filter(key => !modifierKeyPrefixes.some(prefix => key.startsWith(prefix))) .map(key => ({ value: key, label: keyDisplay(keyDisplayMap, key), })), [keyDisplayMap], ); const handleModifierToggle = (optionValue: string) => { const modifiersArray = ensureArray(step.modifiers); const isSelected = modifiersArray.includes(optionValue); const newModifiers = isSelected ? modifiersArray.filter(m => m !== optionValue) : [...modifiersArray, optionValue]; onModifierChange(newModifiers); }; const filteredKeys = useMemo(() => { const selectedKeys = ensureArray(step.keys); const availableKeys = keyOptions.filter( option => !selectedKeys.includes(option.value), ); if (keyQuery === "") { return availableKeys; } else { return availableKeys.filter(option => option.label.toLowerCase().includes(keyQuery.toLowerCase()), ); } }, [keyOptions, keyQuery, step.keys]); return (
{stepIndex + 1}
{onDelete && (
{Object.entries(groupedModifiers).map(([group, mods]) => (
{group}
{mods.map(option => (
))}
{step.keys?.length > 0 && (
{step.keys.map((key, keyIndex) => ( {keyDisplay(keyDisplayMap, key)}
)}
{ const selectedOption = option as ComboboxOption | null; onKeySelect({ value: selectedOption?.value ?? null }); onKeyQueryChange(""); }} displayValue={() => keyQuery} onInputChange={onKeyQueryChange} options={() => filteredKeys} disabledMessage={m.macro_step_max_keys_reached({ max: MAX_KEYS_PER_STEP })} size="SM" immediate disabled={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP} placeholder={ ensureArray(step.keys).length >= MAX_KEYS_PER_STEP ? m.macro_step_max_keys_reached() : m.macro_step_search_for_key() } emptyMessage={m.macro_step_no_matching_keys_found()} />
onDelayChange(Number.parseInt(e.target.value, 10))} options={PRESET_DELAYS} />
); }