import { Search, X } from "lucide-react" import { useEffect, useMemo, useRef, useState } from "react" import { useTranslation } from "react-i18next" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { useModal } from "@/features/modals/services/modal-provider" import { createPresets, PresetType } from "../presets" export function KeyboardShortcutsModal() { const { t } = useTranslation() const { closeModal } = useModal() // Создаем предустановки с локализацией const PRESETS = useMemo(() => createPresets(t), [t]) const [searchQuery, setSearchQuery] = useState("") const [selectedPreset, setSelectedPreset] = useState("Timeline") // Получаем категории на основе выбранной предустановки const categories = useMemo(() => PRESETS[selectedPreset], [PRESETS, selectedPreset]) const [editingShortcut, setEditingShortcut] = useState<{ categoryIndex: number shortcutIndex: number } | null>(null) const [listeningForKeys, setListeningForKeys] = useState(false) // Ref для контейнера с категориями const scrollContainerRef = useRef(null) const [activeSection, setActiveSection] = useState(0) // Флаг для отслеживания первого рендера const isInitialRender = useRef(true) // Фильтрация категорий и горячих клавиш по поисковому запросу const filteredCategories = useMemo(() => { return searchQuery ? categories .map((category) => ({ ...category, shortcuts: category.shortcuts.filter( (shortcut) => shortcut.name.toLowerCase().includes(searchQuery.toLowerCase()) || shortcut.keys.toLowerCase().includes(searchQuery.toLowerCase()), ), })) .filter((category) => category.shortcuts.length > 0) : categories }, [categories, searchQuery]) // Сбрасываем состояние при изменении фильтрованных категорий useEffect(() => { // Сбрасываем режим редактирования при смене предустановки или фильтра setEditingShortcut(null) setListeningForKeys(false) // Сбрасываем активную секцию setActiveSection(0) // Прокручиваем к началу при изменении фильтра const timeoutId = setTimeout(() => { const scrollContainer = scrollContainerRef.current if (scrollContainer) { scrollContainer.scrollTop = 0 } }, 100) return () => { clearTimeout(timeoutId) } }, [filteredCategories]) // Полностью отказываемся от автоматического scroll spy // Вместо этого будем просто устанавливать активную секцию при клике на категорию // и при прокрутке не будем менять активную секцию // Добавляем обработчик клика вне для отмены редактирования useEffect(() => { if (!listeningForKeys) return const handleClickOutside = () => { if (editingShortcut) { setEditingShortcut(null) setListeningForKeys(false) } } document.addEventListener("mousedown", handleClickOutside) return () => { document.removeEventListener("mousedown", handleClickOutside) } }, [listeningForKeys]) // Добавляем обработчик нажатия Escape для отмены редактирования useEffect(() => { if (!listeningForKeys) return const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape") { if (editingShortcut) { setEditingShortcut(null) setListeningForKeys(false) } } } document.addEventListener("keydown", handleEscape) return () => { document.removeEventListener("keydown", handleEscape) } }, [listeningForKeys]) // Эффект для инициализации при первом рендере useEffect(() => { // Сбрасываем флаг первого рендера isInitialRender.current = false // Устанавливаем активную секцию на первую setActiveSection(0) // Прокручиваем контейнер к началу const scrollContainer = scrollContainerRef.current if (scrollContainer) { scrollContainer.scrollTop = 0 } console.log("Component initialized") }, []) // Обработчик изменения поискового запроса const handleSearchChange = (e: React.ChangeEvent) => { setSearchQuery(e.target.value) } // Обработчик нажатия клавиш в поле поиска const handleSearchKeyDown = (e: React.KeyboardEvent) => { // Если нажата только модификатор, добавляем его в поисковый запрос if (["Meta", "Control", "Alt", "Shift"].includes(e.key)) { e.preventDefault() // Получаем символ модификатора let modifier = "" if (e.key === "Meta") modifier = "⌘" if (e.key === "Control") modifier = "Ctrl" if (e.key === "Alt") modifier = "⌥" if (e.key === "Shift") modifier = "⇧" // Добавляем модификатор в поисковый запрос, если его еще нет if (modifier && !searchQuery.includes(modifier)) { setSearchQuery((prev) => prev + modifier) } return } // Если нажата комбинация клавиш с модификатором if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) { e.preventDefault() // Получаем нажатые модификаторы const modifiers = [] if (e.metaKey) modifiers.push("⌘") if (e.ctrlKey) modifiers.push("Ctrl") if (e.altKey) modifiers.push("⌥") if (e.shiftKey) modifiers.push("⇧") // Получаем основную клавишу let key = e.key // Преобразуем специальные клавиши в более читаемый формат if (key === " ") key = "Space" if (key === "ArrowUp") key = "↑" if (key === "ArrowDown") key = "↓" if (key === "ArrowLeft") key = "←" if (key === "ArrowRight") key = "→" if (key === "Escape") key = "Esc" if (key === "Delete") key = "Del" // Формируем строку с горячей клавишей const keyString = [...modifiers, key].join("") // Устанавливаем поисковый запрос setSearchQuery(keyString) } } // Обработчик изменения предустановки const handlePresetChange = (value: string) => { setSelectedPreset(value as PresetType) } // Прокрутка к категории - упрощенная версия const scrollToCategory = (index: number) => { console.log(`Setting active section to ${index}`) // Просто устанавливаем активную секцию setActiveSection(index) // Находим соответствующую секцию по индексу const section = document.getElementById(`category-${index}`) const container = scrollContainerRef.current if (section && container) { try { // Вычисляем позицию секции const sectionRect = section.getBoundingClientRect() const containerRect = container.getBoundingClientRect() // Вычисляем позицию секции относительно контейнера const relativeTop = sectionRect.top - containerRect.top + container.scrollTop // Прокручиваем контейнер к этой позиции container.scrollTop = relativeTop console.log(`Scrolled container to position ${relativeTop}`) } catch (error) { console.error("Error scrolling to category:", error) } } else { console.warn(`Cannot scroll to category ${index}: section or container is null`) } } // Начать редактирование горячей клавиши const startEditing = (categoryIndex: number, shortcutIndex: number) => { console.log(`Starting editing shortcut at category ${categoryIndex}, shortcut ${shortcutIndex}`) setEditingShortcut({ categoryIndex, shortcutIndex }) setListeningForKeys(true) } // Обработчик глобальных клавиатурных событий useEffect(() => { if (!listeningForKeys || !editingShortcut) return console.log("Setting up global keyboard event listener", { listeningForKeys, editingShortcut, }) // Функция для обработки клавиатурных событий на уровне окна const handleGlobalKeyDown = (e: KeyboardEvent) => { console.log("Key event received:", e.key) // Проверяем, что мы все еще в режиме прослушивания if (!listeningForKeys || !editingShortcut) { console.log("Not listening for keys or no editing shortcut") return } // Предотвращаем стандартное поведение e.preventDefault() e.stopPropagation() console.log( "Global key down event:", e.key, "Meta:", e.metaKey, "Ctrl:", e.ctrlKey, "Alt:", e.altKey, "Shift:", e.shiftKey, ) // Получаем нажатые модификаторы const modifiers = [] if (e.metaKey) modifiers.push("⌘") if (e.ctrlKey) modifiers.push("Ctrl") if (e.altKey) modifiers.push("⌥") if (e.shiftKey) modifiers.push("⇧") // Получаем основную клавишу let key = e.key // Преобразуем специальные клавиши в более читаемый формат if (key === " ") key = "Space" if (key === "ArrowUp") key = "↑" if (key === "ArrowDown") key = "↓" if (key === "ArrowLeft") key = "←" if (key === "ArrowRight") key = "→" if (key === "Escape") key = "Esc" if (key === "Delete") key = "Del" // Игнорируем нажатия только модификаторов if (["Meta", "Control", "Alt", "Shift"].includes(e.key)) { console.log("Ignoring modifier key:", e.key) return } // Формируем строку с горячей клавишей const keyString = [...modifiers, key].join("") console.log("Generated key string:", keyString) try { // Обновляем предустановку в PRESETS напрямую const { categoryIndex, shortcutIndex } = editingShortcut console.log("Updating shortcut at:", { categoryIndex, shortcutIndex, keyString, }) PRESETS[selectedPreset][categoryIndex].shortcuts[shortcutIndex].keys = keyString // Вызываем перерендер, обновляя предустановку setSelectedPreset((prev) => prev) console.log("Shortcut updated successfully") } catch (error) { console.error("Error updating shortcut:", error) } // Завершаем режим редактирования setEditingShortcut(null) setListeningForKeys(false) } // Добавляем обработчик события keydown на уровне окна (window вместо document) window.addEventListener("keydown", handleGlobalKeyDown, true) // Добавляем обработчик для клика, чтобы отменить режим редактирования при клике вне const handleGlobalClick = () => { if (listeningForKeys && editingShortcut) { console.log("Canceling editing due to click outside") setEditingShortcut(null) setListeningForKeys(false) } } // Добавляем обработчик клика с задержкой, чтобы не сработал сразу при открытии const clickTimeout = setTimeout(() => { window.addEventListener("click", handleGlobalClick, true) }, 100) return () => { // Удаляем обработчики при размонтировании window.removeEventListener("keydown", handleGlobalKeyDown, true) window.removeEventListener("click", handleGlobalClick, true) clearTimeout(clickTimeout) console.log("Global event listeners removed") } }, [listeningForKeys, editingShortcut, PRESETS, selectedPreset]) // Обработчик нажатия клавиш в компоненте (не используется, так как мы используем глобальные события) const handleKeyDown = (e: React.KeyboardEvent) => { // Просто предотвращаем стандартное поведение, так как мы используем глобальные события if (listeningForKeys) { e.preventDefault() e.stopPropagation() console.log( "Key down event:", e.key, "Meta:", e.metaKey, "Ctrl:", e.ctrlKey, "Alt:", e.altKey, "Shift:", e.shiftKey, ) // Получаем нажатые модификаторы const modifiers = [] if (e.metaKey) modifiers.push("⌘") if (e.ctrlKey) modifiers.push("Ctrl") if (e.altKey) modifiers.push("⌥") if (e.shiftKey) modifiers.push("⇧") // Получаем основную клавишу let key = e.key // Преобразуем специальные клавиши в более читаемый формат if (key === " ") key = "Space" if (key === "ArrowUp") key = "↑" if (key === "ArrowDown") key = "↓" if (key === "ArrowLeft") key = "←" if (key === "ArrowRight") key = "→" if (key === "Escape") key = "Esc" if (key === "Delete") key = "Del" // Игнорируем нажатия только модификаторов if (["Meta", "Control", "Alt", "Shift"].includes(e.key)) { return } // Формируем строку с горячей клавишей const keyString = [...modifiers, key].join("") console.log("Generated key string:", keyString) try { // Вместо обновления категорий, просто обновляем выбранную предустановку // Это вызовет пересчет useMemo для categories setSelectedPreset((prev) => { // Обновляем предустановку в PRESETS напрямую // Это безопасно, так как PRESETS - это объект, созданный с помощью useMemo const { categoryIndex, shortcutIndex } = editingShortcut! PRESETS[prev][categoryIndex].shortcuts[shortcutIndex].keys = keyString return prev }) console.log("Shortcut updated successfully") } catch (error) { console.error("Error updating shortcut:", error) } // Завершаем режим редактирования setEditingShortcut(null) setListeningForKeys(false) } } return (
{/* Верхняя часть с поиском и выбором предустановки */}
{searchQuery && ( )}

{t( "dialogs.keyboardShortcuts.editHint", "Нажмите на сочетание клавиш, чтобы изменить его. Нажмите нужные клавиши для установки нового сочетания.", )}

{/*

{t( "dialogs.keyboardShortcuts.searchHint", "Вы можете искать по названию или нажать комбинацию клавиш в поле поиска.", )}

*/}
{/* Средняя часть с категориями и горячими клавишами */}
{/* Левая панель с категориями */}
{filteredCategories.map((category, index) => ( ))}
{/* Правая панель с горячими клавишами */}
{filteredCategories.map((category, index) => { return (

{category.name}

{category.shortcuts.map((shortcut, shortcutIndex) => { const isEditing = editingShortcut && editingShortcut.categoryIndex === index && editingShortcut.shortcutIndex === shortcutIndex return ( ) })}
) })}
{/* Нижняя часть с кнопками */}
) }