import { Keyboard, RotateCcw } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { useScopedT } from "@/contexts/I18nContext"; import { useShortcuts } from "@/contexts/ShortcutsContext"; import { DEFAULT_SHORTCUTS, FIXED_SHORTCUTS, findConflict, formatBinding, SHORTCUT_ACTIONS, type ShortcutAction, type ShortcutBinding, type ShortcutConflict, type ShortcutsConfig, } from "@/lib/shortcuts"; const MODIFIER_KEYS = new Set(["Control", "Shift", "Alt", "Meta"]); export function ShortcutsConfigDialog() { const { shortcuts, isMac, isConfigOpen, closeConfig, setShortcuts, persistShortcuts } = useShortcuts(); const t = useScopedT("shortcuts"); const tc = useScopedT("common"); const [draft, setDraft] = useState(shortcuts); const [captureFor, setCaptureFor] = useState(null); const [conflict, setConflict] = useState<{ forAction: ShortcutAction; pending: ShortcutBinding; conflictWith: ShortcutConflict; } | null>(null); useEffect(() => { if (isConfigOpen) { setDraft(shortcuts); setCaptureFor(null); setConflict(null); } }, [isConfigOpen, shortcuts]); useEffect(() => { if (!captureFor) return; const handleCapture = (e: KeyboardEvent) => { e.preventDefault(); e.stopPropagation(); if (e.key === "Escape") { setCaptureFor(null); return; } if (MODIFIER_KEYS.has(e.key)) return; const binding: ShortcutBinding = { key: e.key.toLowerCase(), ...(e.ctrlKey || e.metaKey ? { ctrl: true } : {}), ...(e.shiftKey ? { shift: true } : {}), ...(e.altKey ? { alt: true } : {}), }; const found = findConflict(binding, captureFor, draft); setCaptureFor(null); if (found?.type === "fixed") { toast.error(t("reservedShortcut", { label: found.label })); return; } if (found?.type === "configurable") { setConflict({ forAction: captureFor, pending: binding, conflictWith: found }); return; } setDraft((prev: ShortcutsConfig) => ({ ...prev, [captureFor]: binding })); }; window.addEventListener("keydown", handleCapture, { capture: true }); return () => window.removeEventListener("keydown", handleCapture, { capture: true }); }, [captureFor, draft, t]); const handleSwap = useCallback(() => { if (!conflict || conflict.conflictWith.type !== "configurable") return; const { forAction, pending, conflictWith } = conflict; setDraft((prev: ShortcutsConfig) => ({ ...prev, [forAction]: pending, [conflictWith.action]: prev[forAction], })); setConflict(null); }, [conflict]); const handleCancelConflict = useCallback(() => setConflict(null), []); const handleSave = useCallback(async () => { setShortcuts(draft); await persistShortcuts(draft); toast.success(t("savedToast")); closeConfig(); }, [draft, setShortcuts, persistShortcuts, closeConfig, t]); const handleReset = useCallback(() => { setDraft({ ...DEFAULT_SHORTCUTS }); toast.info(t("resetToast")); }, [t]); const handleClose = useCallback(() => { setCaptureFor(null); setConflict(null); closeConfig(); }, [closeConfig]); return ( { if (!open) handleClose(); }} > {t("title")}

{t("configurable")}

{SHORTCUT_ACTIONS.map((action) => { const isCapturing = captureFor === action; const hasConflict = conflict?.forAction === action; return (
{t(`actions.${action}`)}
{hasConflict && conflict?.conflictWith.type === "configurable" && (
⚠{" "} {t("alreadyUsedBy", { action: t(`actions.${conflict.conflictWith.action}`), })}
)}
); })}

{t("fixed")}

{FIXED_SHORTCUTS.map(({ i18nKey, label, display }) => (
{t(`fixedActions.${i18nKey}`, { defaultValue: label })} {display}
))}

{t("helpText")}

); }