import { useEffect, Fragment, useMemo, useState, useCallback } from "react"; import { useNavigate } from "react-router"; import { LuPenLine, LuCopy, LuMoveRight, LuCornerDownRight, LuArrowUp, LuArrowDown, LuTrash2, LuCommand, } from "react-icons/lu"; import { KeySequence, useMacrosStore, generateMacroId } from "@hooks/stores"; import useKeyboardLayout from "@hooks/useKeyboardLayout"; import { SettingsPageHeader } from "@components/SettingsPageheader"; import { Button } from "@components/Button"; import Card from "@components/Card"; import { ConfirmDialog } from "@components/ConfirmDialog"; import EmptyCard from "@components/EmptyCard"; import LoadingSpinner from "@components/LoadingSpinner"; import notifications from "@/notifications"; import { normalizeSortOrders } from "@/utils"; import { MAX_TOTAL_MACROS, COPY_SUFFIX, DEFAULT_DELAY } from "@/constants/macros"; import { m } from "@localizations/messages.js"; 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 { selectedKeyboard } = useKeyboardLayout(); 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(m.macros_invalid_data()); return; } if (isMaxMacrosReached) { notifications.error(m.macros_maximum_macros_reached({ maximum: MAX_TOTAL_MACROS })); return; } setActionLoadingId(macro.id); const newMacroCopy: KeySequence = { ...structuredClone(macro), id: generateMacroId(), name: `${macro.name} ${COPY_SUFFIX}`, sortOrder: macros.length + 1, }; try { await saveMacros(normalizeSortOrders([...macros, newMacroCopy])); notifications.success(m.macros_duplicated_success({ name: newMacroCopy.name })); } catch (error: unknown) { if (error instanceof Error) { notifications.error(m.macros_failed_duplicate_error({ error: error.message || m.unknown_error() })); } else { notifications.error(m.macros_failed_duplicate()); } } 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(m.macros_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(m.macros_order_updated()); } catch (error: unknown) { if (error instanceof Error) { notifications.error(m.macros_failed_reorder_error({ error: error.message || m.unknown_error() })); } else { notifications.error(m.macros_failed_reorder()); } } 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(m.macros_deleted_success({ name: macroToDelete.name })); setShowDeleteConfirm(false); setMacroToDelete(null); } catch (error: unknown) { if (error instanceof Error) { notifications.error(m.macros_failed_delete_error({ error: error.message || m.unknown_error() })); } else { notifications.error(m.macros_failed_delete()); } } 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) => ( {selectedKeyboard.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) => ( {selectedKeyboard.keyDisplayMap[key] || key} {idx < step.keys.length - 1 && (  +  )} ))} ) : ( {m.macros_delay_only()} )} {step.delay !== DEFAULT_DELAY && ( ({step.delay}ms) )} ); })}

))} { setShowDeleteConfirm(false); setMacroToDelete(null); }} title={m.macros_confirm_delete_title()} description={m.macros_confirm_delete_description({ name: macroToDelete?.name || "" })} variant="danger" confirmText={actionLoadingId === macroToDelete?.id ? m.macros_confirm_deleting() : m.delete()} onConfirm={handleDeleteMacro} isConfirming={actionLoadingId === macroToDelete?.id} />
), [ macros, showDeleteConfirm, macroToDelete?.name, macroToDelete?.id, actionLoadingId, handleDeleteMacro, handleMoveMacro, selectedKeyboard.modifierDisplayMap, selectedKeyboard.keyDisplayMap, handleDuplicateMacro, navigate ], ); return (
{macros.length > 0 && (
)}
{loading && macros.length === 0 ? (
} /> ) : macros.length === 0 ? ( navigate("add")} disabled={isMaxMacrosReached} aria-label={m.macros_aria_add_new()} /> } /> ) : ( MacroList )}
); }