import { useEffect, Fragment, useMemo, useState, useCallback } from "react"; import { useNavigate } from "react-router-dom"; import { LuPenLine, LuCopy, LuMoveRight, LuCornerDownRight, LuArrowUp, LuArrowDown, LuTrash2, LuCommand } from "react-icons/lu"; import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores"; import { SettingsPageHeader } from "@/components/SettingsPageheader"; import { Button } from "@/components/Button"; import EmptyCard from "@/components/EmptyCard"; import Card from "@/components/Card"; import { MAX_TOTAL_MACROS, COPY_SUFFIX, DEFAULT_DELAY } from "@/constants/macros"; import { keyDisplayMap, modifierDisplayMap } from "@/keyboardMappings/KeyboardLayouts"; import notifications from "@/notifications"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import LoadingSpinner from "@/components/LoadingSpinner"; const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => { return macros.map((macro, index) => ({ ...macro, sortOrder: index + 1, })); }; export default function SettingsMacrosRoute() { const { macros, loading, initialized, loadMacros, saveMacros } = useMacrosStore(); const navigate = useNavigate(); const [actionLoadingId, setActionLoadingId] = useState(null); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [macroToDelete, setMacroToDelete] = useState(null); const isMaxMacrosReached = useMemo(() => macros.length >= MAX_TOTAL_MACROS, [macros.length] ); useEffect(() => { if (!initialized) { loadMacros(); } }, [initialized, loadMacros]); const handleDuplicateMacro = useCallback(async (macro: KeySequence) => { if (!macro?.id || !macro?.name) { notifications.error("Invalid macro data"); return; } if (isMaxMacrosReached) { notifications.error(`Maximum of ${MAX_TOTAL_MACROS} macros allowed`); return; } setActionLoadingId(macro.id); const newMacroCopy: KeySequence = { ...JSON.parse(JSON.stringify(macro)), id: generateMacroId(), name: `${macro.name} ${COPY_SUFFIX}`, sortOrder: macros.length + 1, }; try { await saveMacros(normalizeSortOrders([...macros, newMacroCopy])); notifications.success(`Macro "${newMacroCopy.name}" duplicated successfully`); } catch (error: unknown) { if (error instanceof Error) { notifications.error(`Failed to duplicate macro: ${error.message}`); } else { notifications.error("Failed to duplicate macro"); } } finally { setActionLoadingId(null); } }, [isMaxMacrosReached, macros, saveMacros, setActionLoadingId]); const handleMoveMacro = useCallback(async (index: number, direction: 'up' | 'down', macroId: string) => { if (!Array.isArray(macros) || macros.length === 0) { notifications.error("No macros available"); return; } const newIndex = direction === 'up' ? index - 1 : index + 1; if (newIndex < 0 || newIndex >= macros.length) return; setActionLoadingId(macroId); try { const newMacros = [...macros]; [newMacros[index], newMacros[newIndex]] = [newMacros[newIndex], newMacros[index]]; const updatedMacros = normalizeSortOrders(newMacros); await saveMacros(updatedMacros); notifications.success("Macro order updated successfully"); } catch (error: unknown) { if (error instanceof Error) { notifications.error(`Failed to reorder macros: ${error.message}`); } else { notifications.error("Failed to reorder macros"); } } finally { setActionLoadingId(null); } }, [macros, saveMacros, setActionLoadingId]); const handleDeleteMacro = useCallback(async () => { if (!macroToDelete?.id) return; setActionLoadingId(macroToDelete.id); try { const updatedMacros = normalizeSortOrders(macros.filter(m => m.id !== macroToDelete.id)); await saveMacros(updatedMacros); notifications.success(`Macro "${macroToDelete.name}" deleted successfully`); setShowDeleteConfirm(false); setMacroToDelete(null); } catch (error: unknown) { if (error instanceof Error) { notifications.error(`Failed to delete macro: ${error.message}`); } else { notifications.error("Failed to delete macro"); } } finally { setActionLoadingId(null); } }, [macroToDelete, macros, saveMacros]); const MacroList = useMemo(() => (
{macros.map((macro, index) => (

{macro.name}

{macro.steps.map((step, stepIndex) => { const StepIcon = stepIndex === 0 ? LuMoveRight : LuCornerDownRight; return ( {(Array.isArray(step.modifiers) && step.modifiers.length > 0) || (Array.isArray(step.keys) && step.keys.length > 0) ? ( <> {Array.isArray(step.modifiers) && step.modifiers.map((modifier, idx) => ( {modifierDisplayMap[modifier] || modifier} {idx < step.modifiers.length - 1 && ( + )} ))} {Array.isArray(step.modifiers) && step.modifiers.length > 0 && Array.isArray(step.keys) && step.keys.length > 0 && ( + )} {Array.isArray(step.keys) && step.keys.map((key, idx) => ( {keyDisplayMap[key] || key} {idx < step.keys.length - 1 && ( + )} ))} ) : ( Delay only )} {step.delay !== DEFAULT_DELAY && ( ({step.delay}ms) )} ); })}

))} { setShowDeleteConfirm(false); setMacroToDelete(null); }} title="Delete Macro" description={`Are you sure you want to delete "${macroToDelete?.name}"? This action cannot be undone.`} variant="danger" confirmText={actionLoadingId === macroToDelete?.id ? "Deleting..." : "Delete"} onConfirm={handleDeleteMacro} isConfirming={actionLoadingId === macroToDelete?.id} />
), [macros, actionLoadingId, showDeleteConfirm, macroToDelete, handleDeleteMacro]); return (
{ macros.length > 0 && (
)}
{loading && macros.length === 0 ? (
} /> ) : macros.length === 0 ? ( navigate("add")} disabled={isMaxMacrosReached} aria-label="Add new macro" /> } /> ) : MacroList}
); }