// ==UserScript== // @name DeepSeek ShortCuts // @name:zh-CN DeepSeek 快捷键 // @name:zh-TW DeepSeek 快捷鍵 // @description Keyboard Shortcuts For DeepSeek | Support Custom Shortcut Keys // @description:zh-CN 为DeepSeek提供快捷键支持 | 支持自定义快捷键 // @description:zh-TW 為DeepSeek提供快捷鍵支援 | 支援自定義快捷鍵 // @version 1.5.0 // @icon https://raw.githubusercontent.com/MiPoNianYou/UserScripts/refs/heads/main/Icons/DeepSeekShortcutsIcon.svg // @author 念柚 // @namespace https://github.com/MiPoNianYou/UserScripts // @supportURL https://github.com/MiPoNianYou/UserScripts/issues // @license GPL-3.0 // @match https://chat.deepseek.com/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @downloadURL https://update.greasyfork.icu/scripts/532221/DeepSeek%20%E5%BF%AB%E6%8D%B7%E9%94%AE.user.js // @updateURL https://update.greasyfork.icu/scripts/532221/DeepSeek%20%E5%BF%AB%E6%8D%B7%E9%94%AE.meta.js // ==/UserScript== (function () { "use strict"; const UI_SETTINGS = { FONT_STACK: "-apple-system, BlinkMacSystemFont, system-ui, sans-serif", ANIMATION_DURATION_MS: 350, ANIMATION_EASING_POP_OUT: "cubic-bezier(0.34, 1.56, 0.64, 1)", ANIMATION_EASING_STANDARD_INTERACTIVE: "cubic-bezier(0, 0, 0.58, 1)", BREATHING_ANIMATION_DURATION: "2.2s", DEBOUNCE_DELAY_MS: 150, }; const STORAGE_KEYS = { CUSTOM_SHORTCUTS_PREFIX: "dsk_custom_shortcuts_", }; const ELEMENT_SELECTORS = { REGENERATE_BUTTON: { selector: ".ds-icon-button", filterText: "#重新生成", }, CONTINUE_BUTTON: { selector: ".ds-button", filterText: "继续生成", }, STOP_GENERATING_BUTTON: { selector: "._7436101", position: "first", }, LAST_COPY_BUTTON: { parentSelector: "div._4f9bf79.d7dc56a8", parentPosition: "last", selector: "._965abe9 .ds-icon-button", childPosition: "first", }, LAST_EDIT_BUTTON: { parentSelector: "._9663006", parentPosition: "last", selector: "._78e0558 .ds-icon-button", childPosition: "last", }, DEEP_THINK_MODE_BUTTON: { selector: ".ds-button span", filterText: "深度思考", }, SEARCH_MODE_BUTTON: { selector: ".ds-button span", filterText: "联网搜索", }, UPLOAD_FILE_BUTTON: { selector: ".f02f0e25", position: "first", }, NEW_CHAT_BUTTON: { selector: "._217e214", position: "first", }, TOGGLE_SIDEBAR_BUTTON: { selector: ".ds-icon-button", filterText: "svg #打开边栏0730, svg #折叠边栏0730", }, CURRENT_CHAT_MENU_BUTTON: { parentSelector: "._83421f9.b64fb9ae", parentPosition: "last", selector: "._2090548", childPosition: "first", }, }; const ELEMENT_IDS = { HELP_PANEL: "dsk-help-panel", HELP_PANEL_ANIMATE_IN: "dsk-help-panel-animate-in", HELP_PANEL_ANIMATE_OUT: "dsk-help-panel-animate-out", }; const CSS_CLASSES = { HELP_PANEL_VISIBLE: "dsk-help-panel--visible", HELP_PANEL_CLOSE_BUTTON: "dsk-help-panel-close-button", HELP_PANEL_TITLE: "dsk-help-panel-title", HELP_PANEL_CONTENT: "dsk-help-panel-content", HELP_PANEL_ROW: "dsk-help-panel-row", HELP_PANEL_KEY: "dsk-help-panel-key", HELP_PANEL_KEY_DISPLAY: "dsk-help-panel-key--display", HELP_PANEL_KEY_SETTING: "dsk-help-panel-key--setting", HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT: "dsk-help-panel-key--configurable-highlight", HELP_PANEL_KEY_LISTENING: "dsk-help-panel-key--listening", HELP_PANEL_KEY_INVALID_SHAKE: "dsk-key-invalid-shake", HELP_PANEL_DESCRIPTION: "dsk-help-panel-description", HELP_PANEL_WARNING: "dsk-help-panel-warning", }; const UI_STRINGS = { HELP_PANEL_TITLE: "快捷按键指北", HELP_PANEL_WARNING_TEXT: "⚠️ 脚本依UA自动适配快捷键 篡改UA或致功能异常", CUSTOMIZE_SHORTCUTS_LABEL: "自定义快捷键", SETTINGS_BUTTON_TEXT: "设置自定按键", FINISH_CUSTOMIZING_BUTTON_TEXT: "完成自定设置", PRESS_NEW_SHORTCUT_TEXT: "请按下快捷键", KEY_CONFLICT_TEXT_PREFIX: "键 「", KEY_CONFLICT_TEXT_SUFFIX: "」 已被使用", INVALID_MODIFIER_TEXT_PREFIX: "请按 ", INVALID_MODIFIER_TEXT_SUFFIX: " + 字母/数字", }; let currentKeybindingConfig = { MODIFIERS: (() => { const isMac = /Macintosh|Mac OS X/i.test(navigator.userAgent); return { CHARACTER_DISPLAY: isMac ? "Control" : "Alt", EVENT_PROPERTY: isMac ? "ctrlKey" : "altKey", }; })(), SHORTCUTS: [ { id: "regenerate", key: "R", description: "重新生成回答", selectorConfig: ELEMENT_SELECTORS.REGENERATE_BUTTON, }, { id: "continueGenerating", key: "C", description: "继续生成回答", selectorConfig: ELEMENT_SELECTORS.CONTINUE_BUTTON, }, { id: "stopGenerating", key: "Q", description: "中断当前生成", selectorConfig: ELEMENT_SELECTORS.STOP_GENERATING_BUTTON, }, { id: "copyLastResponse", key: "K", description: "复制末条回答", selectorConfig: ELEMENT_SELECTORS.LAST_COPY_BUTTON, }, { id: "editLastQuery", key: "E", description: "编辑末次提问", selectorConfig: ELEMENT_SELECTORS.LAST_EDIT_BUTTON, }, { id: "deepThinkMode", key: "D", description: "深度思考模式", selectorConfig: ELEMENT_SELECTORS.DEEP_THINK_MODE_BUTTON, }, { id: "searchMode", key: "S", description: "联网搜索模式", selectorConfig: ELEMENT_SELECTORS.SEARCH_MODE_BUTTON, }, { id: "uploadFile", key: "U", description: "上传本地文件", selectorConfig: ELEMENT_SELECTORS.UPLOAD_FILE_BUTTON, }, { id: "newChat", key: "N", description: "新建对话窗口", selectorConfig: ELEMENT_SELECTORS.NEW_CHAT_BUTTON, }, { id: "toggleSidebar", key: "T", description: "切换开关边栏", selectorConfig: ELEMENT_SELECTORS.TOGGLE_SIDEBAR_BUTTON, }, { id: "currentChatMenu", key: "I", description: "当前对话菜单", selectorConfig: ELEMENT_SELECTORS.CURRENT_CHAT_MENU_BUTTON, }, { id: "toggleHelpPanel", key: "H", description: "快捷按键帮助", actionIdentifier: "toggleHelpPanel", isSpecialAction: true, }, { id: "settingsEntry", key: null, description: UI_STRINGS.CUSTOMIZE_SHORTCUTS_LABEL, isSettingsEntry: true, actionIdentifier: "toggleCustomizationMode", nonConfigurable: true, }, ], }; let helpPanelElement = null; let keydownEventListener = null; let isCustomizingShortcuts = false; let activeCustomizationTarget = null; const shortcutDisplaySpansMap = new Map(); let panelCloseTimer = null; function injectUserInterfaceStyles() { const styles = ` :root { --ctp-frappe-rosewater: #f2d5cf; --ctp-frappe-flamingo: #eebebe; --ctp-frappe-pink: #f4b8e4; --ctp-frappe-mauve: #ca9ee6; --ctp-frappe-red: #e78284; --ctp-frappe-maroon: #ea999c; --ctp-frappe-peach: #ef9f76; --ctp-frappe-yellow: #e5c890; --ctp-frappe-green: #a6d189; --ctp-frappe-teal: #81c8be; --ctp-frappe-sky: #99d1db; --ctp-frappe-sapphire: #85c1dc; --ctp-frappe-blue: #8caaee; --ctp-frappe-lavender: #babbf1; --ctp-frappe-text: #c6d0f5; --ctp-frappe-subtext1: #b5bfe2; --ctp-frappe-subtext0: #a5adce; --ctp-frappe-overlay2: #949cbb; --ctp-frappe-overlay1: #838ba7; --ctp-frappe-overlay0: #737994; --ctp-frappe-surface2: #626880; --ctp-frappe-surface1: #51576d; --ctp-frappe-surface0: #414559; --ctp-frappe-base: #303446; --ctp-frappe-mantle: #292c3c; --ctp-frappe-crust: #232634; --ctp-frappe-crust-rgb: 35, 38, 52; --dsk-panel-bg: rgba(41, 44, 60, 0.85); --dsk-panel-border: rgba(65, 69, 89, 0.5); --dsk-panel-shadow: 0 1px 3px rgba(var(--ctp-frappe-crust-rgb), 0.12), 0 6px 16px rgba(var(--ctp-frappe-crust-rgb), 0.10), 0 12px 28px rgba(var(--ctp-frappe-crust-rgb), 0.08); --dsk-text-primary: var(--ctp-frappe-text); --dsk-text-secondary: var(--ctp-frappe-subtext0); --dsk-key-bg: var(--ctp-frappe-surface0); --dsk-key-border: var(--ctp-frappe-surface1); --dsk-key-setting-text: var(--ctp-frappe-blue); --dsk-key-setting-hover-bg: var(--ctp-frappe-surface1); --dsk-key-breathing-highlight-color: var(--ctp-frappe-mauve); --dsk-key-listening-border: var(--ctp-frappe-green); --dsk-key-listening-bg: color-mix(in srgb, var(--dsk-key-bg) 85%, var(--ctp-frappe-green) 15%); --dsk-key-invalid-shake-color: var(--ctp-frappe-red); --dsk-warning-bg: rgba(65, 69, 89, 0.5); --dsk-warning-border: var(--ctp-frappe-surface1); --dsk-warning-text: var(--ctp-frappe-yellow); --dsk-scrollbar-thumb: var(--ctp-frappe-overlay0); --dsk-scrollbar-thumb-hover: var(--ctp-frappe-overlay1); --dsk-close-button-bg: var(--ctp-frappe-red); --dsk-close-button-hover-bg: color-mix(in srgb, var(--ctp-frappe-red) 80%, var(--ctp-frappe-crust) 20%); --dsk-close-button-symbol: rgba(var(--ctp-frappe-crust-rgb), 0.7); } @keyframes dsk-opacity-breathing-effect { 0%, 100% { opacity: 0; } 50% { opacity: 0.25; } } @keyframes dsk-border-breathing-effect { 0%, 100% { border-color: var(--dsk-key-border); } 50% { border-color: var(--dsk-key-breathing-highlight-color); } } @keyframes dsk-invalid-shake-effect { 0%, 100% { transform: translateX(0); } 10%, 30%, 50%, 70%, 90% { transform: translateX(-3px); } 20%, 40%, 60%, 80% { transform: translateX(3px); } } @keyframes ${ELEMENT_IDS.HELP_PANEL_ANIMATE_IN} { 0% { transform: translate(-50%, -50%) scale(0.88); opacity: 0; } 100% { transform: translate(-50%, -50%) scale(1); opacity: 1; } } @keyframes ${ELEMENT_IDS.HELP_PANEL_ANIMATE_OUT} { 0% { transform: translate(-50%, -50%) scale(1); opacity: 1; } 100% { transform: translate(-50%, -50%) scale(0.9); opacity: 0; } } #${ELEMENT_IDS.HELP_PANEL} { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.88); opacity: 0; visibility: hidden; z-index: 2147483647; min-width: 300px; max-width: 480px; padding: 24px; border: 1px solid var(--dsk-panel-border); border-radius: 16px; background-color: var(--dsk-panel-bg); color: var(--dsk-text-primary); font-family: ${UI_SETTINGS.FONT_STACK}; font-size: 14px; font-weight: 500; line-height: 1.5; box-shadow: var(--dsk-panel-shadow); backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); display: flex; flex-direction: column; pointer-events: none; } #${ELEMENT_IDS.HELP_PANEL}.${CSS_CLASSES.HELP_PANEL_VISIBLE} { pointer-events: auto; } .${CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON} { position: absolute; top: 14px; left: 14px; width: 12px; height: 12px; padding: 0; border: none; border-radius: 50%; background-color: var(--dsk-close-button-bg); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background-color 0.15s ${UI_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE}, transform 0.15s ${UI_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE}; appearance: none; -webkit-appearance: none; outline: none; } .${CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON}::before { content: '✕'; display: block; color: transparent; font-size: 10px; font-weight: bold; line-height: 12px; text-align: center; transition: color 0.1s ${UI_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE}; } .${CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON}:hover { background-color: var(--dsk-close-button-hover-bg); } .${CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON}:hover::before { color: var(--dsk-close-button-symbol); } .${CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON}:active { filter: brightness(0.85); transform: scale(0.9); } .${CSS_CLASSES.HELP_PANEL_TITLE} { margin: 0 0 18px 0; padding-top: 8px; color: var(--dsk-text-primary); font-size: 17px; font-weight: 600; text-align: center; flex-shrink: 0; } .${CSS_CLASSES.HELP_PANEL_CONTENT} { flex-grow: 1; overflow-y: auto; max-height: 60vh; margin-right: -12px; padding-right: 12px; scrollbar-width: thin; scrollbar-color: var(--dsk-scrollbar-thumb) transparent; } .${CSS_CLASSES.HELP_PANEL_CONTENT}::-webkit-scrollbar { width: 6px; } .${CSS_CLASSES.HELP_PANEL_CONTENT}::-webkit-scrollbar-track { background: transparent; margin: 4px 0; } .${CSS_CLASSES.HELP_PANEL_CONTENT}::-webkit-scrollbar-thumb { background-color: var(--dsk-scrollbar-thumb); border-radius: 3px; transition: background-color 0.15s ${UI_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE}; } .${CSS_CLASSES.HELP_PANEL_CONTENT}::-webkit-scrollbar-thumb:hover { background-color: var(--dsk-scrollbar-thumb-hover); } .${CSS_CLASSES.HELP_PANEL_ROW} { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding: 6px 2px; } .${CSS_CLASSES.HELP_PANEL_CONTENT} > .${CSS_CLASSES.HELP_PANEL_ROW}:last-child { margin-bottom: 0; } .${CSS_CLASSES.HELP_PANEL_KEY} { min-width: 95px; padding: 5px 10px; margin-left: 18px; background-color: var(--dsk-key-bg); border: 1px solid var(--dsk-key-border); border-radius: 6px; box-shadow: 0 1px 1px rgba(0,0,0,0.08), inset 0 1px 1px rgba(255,255,255,0.03); color: var(--dsk-text-primary); font-family: inherit; font-size: 13px; font-weight: 500; text-align: center; flex-shrink: 0; transition: background-color 0.15s ${UI_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE}, border-color 0.15s ${UI_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE}, color 0.15s ${UI_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE}; cursor: default; position: relative; } .${CSS_CLASSES.HELP_PANEL_KEY}.${CSS_CLASSES.HELP_PANEL_KEY_DISPLAY} { } .${CSS_CLASSES.HELP_PANEL_KEY}.${CSS_CLASSES.HELP_PANEL_KEY_SETTING} { color: var(--dsk-key-setting-text); cursor: pointer; } .${CSS_CLASSES.HELP_PANEL_KEY}.${CSS_CLASSES.HELP_PANEL_KEY_SETTING}:hover { background-color: var(--dsk-key-setting-hover-bg); border-color: var(--ctp-frappe-overlay0); } .${CSS_CLASSES.HELP_PANEL_KEY}.${CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT} { animation: dsk-border-breathing-effect ${UI_SETTINGS.BREATHING_ANIMATION_DURATION} infinite ease-in-out; cursor: pointer; } .${CSS_CLASSES.HELP_PANEL_KEY}.${CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT}::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: inherit; background-color: var(--dsk-key-breathing-highlight-color); opacity: 0; z-index: 0; pointer-events: none; animation: dsk-opacity-breathing-effect ${UI_SETTINGS.BREATHING_ANIMATION_DURATION} infinite ease-in-out; } .${CSS_CLASSES.HELP_PANEL_KEY}.${CSS_CLASSES.HELP_PANEL_KEY_LISTENING} { border-color: var(--dsk-key-listening-border) !important; background-color: var(--dsk-key-listening-bg) !important; color: var(--ctp-frappe-green) !important; animation: none !important; cursor: default !important; } .${CSS_CLASSES.HELP_PANEL_KEY}.${CSS_CLASSES.HELP_PANEL_KEY_LISTENING}::after { animation: none !important; opacity: 0 !important; } .${CSS_CLASSES.HELP_PANEL_KEY}.${CSS_CLASSES.HELP_PANEL_KEY_INVALID_SHAKE} { animation: dsk-invalid-shake-effect 0.5s ${UI_SETTINGS.ANIMATION_EASING_STANDARD_DEFAULT}; border-color: var(--dsk-key-invalid-shake-color) !important; color: var(--dsk-key-invalid-shake-color) !important; } .${CSS_CLASSES.HELP_PANEL_DESCRIPTION} { flex-grow: 1; padding-right: 10px; color: var(--dsk-text-secondary); font-size: 13.5px; } .${CSS_CLASSES.HELP_PANEL_WARNING} { margin-top: 20px; padding: 12px 16px; background-color: var(--dsk-warning-bg); border: 1px solid var(--dsk-warning-border); border-radius: 10px; color: var(--dsk-warning-text); font-size: 12.5px; font-weight: 500; line-height: 1.45; text-align: center; flex-shrink: 0; } `; try { GM_addStyle(styles); } catch (e) { const styleElement = document.createElement("style"); styleElement.textContent = styles; (document.head || document.documentElement).appendChild(styleElement); } } function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func.apply(this, args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } function getElementByConfig(config) { if (!config || !config.selector) return null; const { selector, filterText, position = "first", parentSelector, parentPosition = "last", childPosition = "first", } = config; let targetElements = []; if (parentSelector) { const parents = Array.from(document.querySelectorAll(parentSelector)); if (parents.length === 0) return null; const parentIndex = parentPosition === "last" ? parents.length - 1 : 0; const targetParent = parents[parentIndex]; if (!targetParent) return null; targetElements = Array.from(targetParent.querySelectorAll(selector)); } else { targetElements = Array.from(document.querySelectorAll(selector)); } if (targetElements.length === 0) return null; if (filterText) { const filters = filterText.split(",").map((f) => f.trim()); const foundElement = targetElements.find((element) => filters.some( (ft) => element.textContent?.includes(ft) || (ft.startsWith("svg #") && element.querySelector(ft.replace("svg ", ""))) ) ); return foundElement || null; } else { const index = position === "last" ? targetElements.length - 1 : childPosition === "last" ? targetElements.length - 1 : 0; return targetElements[index] || null; } } function triggerElementClick(elementConfig) { const element = getElementByConfig(elementConfig); if (element && typeof element.click === "function") { element.click(); return true; } return false; } function formatShortcutForDisplay(shortcutKey) { if (!shortcutKey) return "---"; return `${ currentKeybindingConfig.MODIFIERS.CHARACTER_DISPLAY } + ${shortcutKey.toUpperCase()}`; } function updateShortcutDisplay(shortcutId, newKey) { const spanElement = shortcutDisplaySpansMap.get(shortcutId); if (spanElement) { spanElement.textContent = newKey ? formatShortcutForDisplay(newKey) : UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT; } } function saveCustomShortcut(shortcutId, newKey) { const shortcutToUpdate = currentKeybindingConfig.SHORTCUTS.find( (s) => s.id === shortcutId ); if (shortcutToUpdate) { shortcutToUpdate.key = newKey.toUpperCase(); try { GM_setValue( `${STORAGE_KEYS.CUSTOM_SHORTCUTS_PREFIX}${shortcutId}`, shortcutToUpdate.key ); } catch (e) {} if (keydownEventListener) { window.removeEventListener("keydown", keydownEventListener, true); } keydownEventListener = createKeyboardEventHandler(); window.addEventListener("keydown", keydownEventListener, true); } } function loadCustomShortcuts() { currentKeybindingConfig.SHORTCUTS.forEach((shortcut) => { if (shortcut.nonConfigurable || shortcut.isSettingsEntry) return; try { const savedKey = GM_getValue( `${STORAGE_KEYS.CUSTOM_SHORTCUTS_PREFIX}${shortcut.id}`, shortcut.key ); if ( savedKey && typeof savedKey === "string" && savedKey.match(/^[a-zA-Z0-9]$/i) ) { shortcut.key = savedKey.toUpperCase(); } } catch (e) {} }); } function setListeningState(shortcutId, spanElement, isListening) { if (isListening) { if ( activeCustomizationTarget && activeCustomizationTarget.spanElement !== spanElement ) { const prevShortcut = currentKeybindingConfig.SHORTCUTS.find( (s) => s.id === activeCustomizationTarget.shortcutId ); activeCustomizationTarget.spanElement.textContent = formatShortcutForDisplay(prevShortcut?.key); activeCustomizationTarget.spanElement.classList.remove( CSS_CLASSES.HELP_PANEL_KEY_LISTENING ); activeCustomizationTarget.spanElement.classList.remove( CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT ); if (isCustomizingShortcuts) { activeCustomizationTarget.spanElement.classList.add( CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT ); } } activeCustomizationTarget = { shortcutId, spanElement }; spanElement.textContent = UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT; shortcutDisplaySpansMap.forEach((s) => { s.classList.remove(CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT); }); spanElement.classList.add(CSS_CLASSES.HELP_PANEL_KEY_LISTENING); spanElement.classList.remove( CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT ); } else { if ( activeCustomizationTarget && activeCustomizationTarget.shortcutId === shortcutId ) { activeCustomizationTarget = null; } spanElement.classList.remove(CSS_CLASSES.HELP_PANEL_KEY_LISTENING); const currentKey = currentKeybindingConfig.SHORTCUTS.find( (s) => s.id === shortcutId )?.key; spanElement.textContent = formatShortcutForDisplay(currentKey); if (isCustomizingShortcuts) { shortcutDisplaySpansMap.forEach((s, id) => { const cfg = currentKeybindingConfig.SHORTCUTS.find( (sc) => sc.id === id ); if (cfg && !cfg.nonConfigurable && !cfg.isSettingsEntry) { s.classList.add(CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT); } }); } } } function toggleCustomizationMode(settingsButtonSpan) { isCustomizingShortcuts = !isCustomizingShortcuts; if (activeCustomizationTarget) { setListeningState( activeCustomizationTarget.shortcutId, activeCustomizationTarget.spanElement, false ); } if (isCustomizingShortcuts) { settingsButtonSpan.textContent = UI_STRINGS.FINISH_CUSTOMIZING_BUTTON_TEXT; shortcutDisplaySpansMap.forEach((span, id) => { const shortcut = currentKeybindingConfig.SHORTCUTS.find( (s) => s.id === id ); if ( shortcut && !shortcut.nonConfigurable && !shortcut.isSettingsEntry ) { span.classList.add(CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT); } }); } else { settingsButtonSpan.textContent = UI_STRINGS.SETTINGS_BUTTON_TEXT; shortcutDisplaySpansMap.forEach((span) => { span.classList.remove( CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT ); span.classList.remove(CSS_CLASSES.HELP_PANEL_KEY_LISTENING); }); } } function createHelpPanelElement() { if (helpPanelElement && document.body.contains(helpPanelElement)) { const settingsButton = helpPanelElement.querySelector( `.${CSS_CLASSES.HELP_PANEL_KEY_SETTING}` ); if (settingsButton) { settingsButton.textContent = isCustomizingShortcuts ? UI_STRINGS.FINISH_CUSTOMIZING_BUTTON_TEXT : UI_STRINGS.SETTINGS_BUTTON_TEXT; } shortcutDisplaySpansMap.forEach((span, id) => { const shortcut = currentKeybindingConfig.SHORTCUTS.find( (s) => s.id === id ); if (shortcut) { span.textContent = formatShortcutForDisplay(shortcut.key); span.classList.remove( CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT, CSS_CLASSES.HELP_PANEL_KEY_LISTENING ); if ( isCustomizingShortcuts && !shortcut.isSettingsEntry && !shortcut.nonConfigurable ) { if ( activeCustomizationTarget && activeCustomizationTarget.shortcutId === id ) { span.textContent = UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT; span.classList.add(CSS_CLASSES.HELP_PANEL_KEY_LISTENING); } else { span.classList.add( CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT ); } } } }); return helpPanelElement; } shortcutDisplaySpansMap.clear(); const panel = document.createElement("div"); panel.id = ELEMENT_IDS.HELP_PANEL; const closeButton = document.createElement("button"); closeButton.className = CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON; closeButton.setAttribute("aria-label", "Close help panel"); closeButton.addEventListener("click", (event) => { event.stopPropagation(); if (isCustomizingShortcuts) { const settingsBtnInPanel = panel.querySelector( `.${CSS_CLASSES.HELP_PANEL_KEY_SETTING}` ); if (settingsBtnInPanel) toggleCustomizationMode(settingsBtnInPanel); } closeHelpPanel(); }); const titleElement = document.createElement("h3"); titleElement.className = CSS_CLASSES.HELP_PANEL_TITLE; titleElement.textContent = UI_STRINGS.HELP_PANEL_TITLE; const contentContainer = document.createElement("div"); contentContainer.className = CSS_CLASSES.HELP_PANEL_CONTENT; currentKeybindingConfig.SHORTCUTS.forEach((shortcut) => { const row = document.createElement("div"); row.className = CSS_CLASSES.HELP_PANEL_ROW; const descriptionSpan = document.createElement("span"); descriptionSpan.className = CSS_CLASSES.HELP_PANEL_DESCRIPTION; descriptionSpan.textContent = shortcut.description; const keySpan = document.createElement("span"); keySpan.className = CSS_CLASSES.HELP_PANEL_KEY; if (shortcut.isSettingsEntry) { keySpan.textContent = isCustomizingShortcuts ? UI_STRINGS.FINISH_CUSTOMIZING_BUTTON_TEXT : UI_STRINGS.SETTINGS_BUTTON_TEXT; keySpan.classList.add(CSS_CLASSES.HELP_PANEL_KEY_SETTING); keySpan.addEventListener("click", () => { toggleCustomizationMode(keySpan); }); } else { keySpan.textContent = formatShortcutForDisplay(shortcut.key); keySpan.classList.add(CSS_CLASSES.HELP_PANEL_KEY_DISPLAY); shortcutDisplaySpansMap.set(shortcut.id, keySpan); if (!shortcut.nonConfigurable) { keySpan.addEventListener("click", () => { if ( isCustomizingShortcuts && (!activeCustomizationTarget || activeCustomizationTarget.shortcutId !== shortcut.id) ) { setListeningState(shortcut.id, keySpan, true); } else if ( isCustomizingShortcuts && activeCustomizationTarget && activeCustomizationTarget.shortcutId === shortcut.id ) { setListeningState(shortcut.id, keySpan, false); } }); } if (isCustomizingShortcuts && !shortcut.nonConfigurable) { if ( activeCustomizationTarget && activeCustomizationTarget.shortcutId === shortcut.id ) { keySpan.textContent = UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT; keySpan.classList.add(CSS_CLASSES.HELP_PANEL_KEY_LISTENING); } else { keySpan.classList.add( CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT ); } } } row.appendChild(descriptionSpan); row.appendChild(keySpan); contentContainer.appendChild(row); }); const warningElement = document.createElement("div"); warningElement.className = CSS_CLASSES.HELP_PANEL_WARNING; warningElement.textContent = UI_STRINGS.HELP_PANEL_WARNING_TEXT; panel.appendChild(closeButton); panel.appendChild(titleElement); panel.appendChild(contentContainer); panel.appendChild(warningElement); helpPanelElement = panel; document.body.appendChild(panel); return panel; } function closeHelpPanel() { if ( helpPanelElement && helpPanelElement.classList.contains(CSS_CLASSES.HELP_PANEL_VISIBLE) ) { helpPanelElement.style.animation = `${ELEMENT_IDS.HELP_PANEL_ANIMATE_OUT} ${UI_SETTINGS.ANIMATION_DURATION_MS}ms ${UI_SETTINGS.ANIMATION_EASING_POP_OUT} forwards`; clearTimeout(panelCloseTimer); panelCloseTimer = setTimeout(() => { if (helpPanelElement) { helpPanelElement.classList.remove(CSS_CLASSES.HELP_PANEL_VISIBLE); helpPanelElement.style.animation = ""; helpPanelElement.style.opacity = "0"; helpPanelElement.style.visibility = "hidden"; } window.removeEventListener( "click", handlePanelInteractionOutside, true ); window.removeEventListener("keydown", handlePanelEscapeKey, true); }, UI_SETTINGS.ANIMATION_DURATION_MS); } } function handlePanelInteractionOutside(event) { if ( helpPanelElement && helpPanelElement.classList.contains(CSS_CLASSES.HELP_PANEL_VISIBLE) && !helpPanelElement.contains(event.target) && parseFloat(getComputedStyle(helpPanelElement).opacity) === 1 ) { if (isCustomizingShortcuts) { const settingsButton = helpPanelElement.querySelector( `.${CSS_CLASSES.HELP_PANEL_KEY_SETTING}` ); if (settingsButton) toggleCustomizationMode(settingsButton); } closeHelpPanel(); } } function handlePanelEscapeKey(event) { if (event.key === "Escape") { if ( helpPanelElement && helpPanelElement.classList.contains(CSS_CLASSES.HELP_PANEL_VISIBLE) && parseFloat(getComputedStyle(helpPanelElement).opacity) === 1 ) { event.preventDefault(); event.stopPropagation(); if (activeCustomizationTarget) { setListeningState( activeCustomizationTarget.shortcutId, activeCustomizationTarget.spanElement, false ); } else if (isCustomizingShortcuts) { const settingsButton = helpPanelElement.querySelector( `.${CSS_CLASSES.HELP_PANEL_KEY_SETTING}` ); if (settingsButton) toggleCustomizationMode(settingsButton); } closeHelpPanel(); } } } function toggleHelpPanelVisibility() { if (!helpPanelElement || !document.body.contains(helpPanelElement)) { helpPanelElement = createHelpPanelElement(); } else { const settingsButton = helpPanelElement.querySelector( `.${CSS_CLASSES.HELP_PANEL_KEY_SETTING}` ); if (settingsButton) { settingsButton.textContent = isCustomizingShortcuts ? UI_STRINGS.FINISH_CUSTOMIZING_BUTTON_TEXT : UI_STRINGS.SETTINGS_BUTTON_TEXT; } shortcutDisplaySpansMap.forEach((span, id) => { const shortcut = currentKeybindingConfig.SHORTCUTS.find( (s) => s.id === id ); if (shortcut) { span.textContent = formatShortcutForDisplay(shortcut.key); span.classList.remove( CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT, CSS_CLASSES.HELP_PANEL_KEY_LISTENING ); if ( isCustomizingShortcuts && !shortcut.isSettingsEntry && !shortcut.nonConfigurable ) { if ( activeCustomizationTarget && activeCustomizationTarget.shortcutId === id ) { span.textContent = UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT; span.classList.add(CSS_CLASSES.HELP_PANEL_KEY_LISTENING); } else { span.classList.add( CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT ); } } } }); } clearTimeout(panelCloseTimer); const isCurrentlyVisibleByClass = helpPanelElement.classList.contains( CSS_CLASSES.HELP_PANEL_VISIBLE ); let currentOpacity = 0; if ( isCurrentlyVisibleByClass || helpPanelElement.style.animationName === ELEMENT_IDS.HELP_PANEL_ANIMATE_OUT ) { currentOpacity = parseFloat(getComputedStyle(helpPanelElement).opacity); } const isAnimatingOut = helpPanelElement.style.animationName === ELEMENT_IDS.HELP_PANEL_ANIMATE_OUT; if (isCurrentlyVisibleByClass && currentOpacity > 0.01 && !isAnimatingOut) { if (isCustomizingShortcuts && !activeCustomizationTarget) { const settingsButton = helpPanelElement.querySelector( `.${CSS_CLASSES.HELP_PANEL_KEY_SETTING}` ); if (settingsButton) toggleCustomizationMode(settingsButton); } closeHelpPanel(); } else { helpPanelElement.style.animation = ""; helpPanelElement.style.opacity = "0"; helpPanelElement.style.visibility = "hidden"; requestAnimationFrame(() => { helpPanelElement.classList.add(CSS_CLASSES.HELP_PANEL_VISIBLE); helpPanelElement.style.visibility = "visible"; helpPanelElement.style.animation = `${ELEMENT_IDS.HELP_PANEL_ANIMATE_IN} ${UI_SETTINGS.ANIMATION_DURATION_MS}ms ${UI_SETTINGS.ANIMATION_EASING_POP_OUT} forwards`; }); setTimeout(() => { window.addEventListener("click", handlePanelInteractionOutside, true); window.addEventListener("keydown", handlePanelEscapeKey, true); }, 0); } } const debouncedToggleHelpPanelVisibility = debounce( toggleHelpPanelVisibility, UI_SETTINGS.DEBOUNCE_DELAY_MS ); function createKeyboardEventHandler() { const specialActionHandlers = { toggleHelpPanel: debouncedToggleHelpPanelVisibility, toggleCustomizationMode: () => { if (helpPanelElement) { const settingsButton = helpPanelElement.querySelector( `.${CSS_CLASSES.HELP_PANEL_KEY_SETTING}` ); if (settingsButton) toggleCustomizationMode(settingsButton); } }, }; const shortcutActionMap = {}; currentKeybindingConfig.SHORTCUTS.forEach((shortcut) => { if (shortcut.key && !shortcut.isSettingsEntry) { const lowerKey = shortcut.key.toLowerCase(); if ( shortcut.actionIdentifier && specialActionHandlers[shortcut.actionIdentifier] ) { shortcutActionMap[lowerKey] = specialActionHandlers[shortcut.actionIdentifier]; } else if (shortcut.selectorConfig) { shortcutActionMap[lowerKey] = () => triggerElementClick(shortcut.selectorConfig); } } }); return function handleKeyDown(event) { if (event.key === "Escape") { return; } if (activeCustomizationTarget) { event.preventDefault(); event.stopPropagation(); const newKey = event.key; const targetSpan = activeCustomizationTarget.spanElement; if ( newKey && newKey.length === 1 && !event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey && !["Control", "Alt", "Shift", "Meta"].includes(newKey) ) { if (newKey.match(/^[a-zA-Z0-9]$/i)) { const conflictingShortcut = currentKeybindingConfig.SHORTCUTS.find( (s) => s.key && s.key.toLowerCase() === newKey.toLowerCase() && s.id !== activeCustomizationTarget.shortcutId ); if (conflictingShortcut) { targetSpan.textContent = `${ UI_STRINGS.KEY_CONFLICT_TEXT_PREFIX }${newKey.toUpperCase()}${UI_STRINGS.KEY_CONFLICT_TEXT_SUFFIX}`; setTimeout(() => { if ( activeCustomizationTarget && activeCustomizationTarget.spanElement === targetSpan ) { targetSpan.textContent = UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT; } }, 2000); return; } saveCustomShortcut(activeCustomizationTarget.shortcutId, newKey); updateShortcutDisplay(activeCustomizationTarget.shortcutId, newKey); setListeningState( activeCustomizationTarget.shortcutId, targetSpan, false ); } else { targetSpan.classList.add(CSS_CLASSES.HELP_PANEL_KEY_INVALID_SHAKE); setTimeout(() => { if ( activeCustomizationTarget && activeCustomizationTarget.spanElement === targetSpan ) { targetSpan.classList.remove( CSS_CLASSES.HELP_PANEL_KEY_INVALID_SHAKE ); } }, 500); } } else if (!["Control", "Alt", "Shift", "Meta"].includes(newKey)) { if (event[currentKeybindingConfig.MODIFIERS.EVENT_PROPERTY]) { const conflictingShortcut = currentKeybindingConfig.SHORTCUTS.find( (s) => s.key && s.key.toLowerCase() === newKey.toLowerCase() && s.id !== activeCustomizationTarget.shortcutId ); if (conflictingShortcut) { targetSpan.textContent = `${ UI_STRINGS.KEY_CONFLICT_TEXT_PREFIX }${newKey.toUpperCase()}${UI_STRINGS.KEY_CONFLICT_TEXT_SUFFIX}`; setTimeout(() => { if ( activeCustomizationTarget && activeCustomizationTarget.spanElement === targetSpan ) { targetSpan.textContent = UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT; } }, 2000); return; } saveCustomShortcut(activeCustomizationTarget.shortcutId, newKey); updateShortcutDisplay(activeCustomizationTarget.shortcutId, newKey); setListeningState( activeCustomizationTarget.shortcutId, targetSpan, false ); } else { targetSpan.textContent = `${UI_STRINGS.INVALID_MODIFIER_TEXT_PREFIX}${currentKeybindingConfig.MODIFIERS.CHARACTER_DISPLAY}${UI_STRINGS.INVALID_MODIFIER_TEXT_SUFFIX}`; setTimeout(() => { if ( activeCustomizationTarget && activeCustomizationTarget.spanElement === targetSpan ) { targetSpan.textContent = UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT; } }, 2000); } } return; } if (isCustomizingShortcuts) { return; } if (!event[currentKeybindingConfig.MODIFIERS.EVENT_PROPERTY]) { return; } const pressedKey = event.key.toLowerCase(); const actionToExecute = shortcutActionMap[pressedKey]; if (typeof actionToExecute === "function") { actionToExecute(); event.preventDefault(); event.stopPropagation(); } }; } function initializeScript() { loadCustomShortcuts(); injectUserInterfaceStyles(); keydownEventListener = createKeyboardEventHandler(); window.addEventListener("keydown", keydownEventListener, true); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initializeScript, { once: true, }); } else { initializeScript(); } })();