DeepSeek 快捷键

为DeepSeek提供快捷键支持 | 支持自定义快捷键

  1. // ==UserScript==
  2. // @name DeepSeek Shortcuts
  3. // @name:zh-CN DeepSeek 快捷键
  4. // @name:zh-TW DeepSeek 快捷鍵
  5. // @description Keyboard Shortcuts For DeepSeek | Support Custom Shortcut Keys
  6. // @description:zh-CN 为DeepSeek提供快捷键支持 | 支持自定义快捷键
  7. // @description:zh-TW 為DeepSeek提供快捷鍵支援 | 支援自定義快捷鍵
  8. // @version 1.6.0
  9. // @icon https://raw.githubusercontent.com/MiPoNianYou/UserScripts/main/Icons/DeepSeek-Shortcuts-Icon.svg
  10. // @author 念柚
  11. // @namespace https://github.com/MiPoNianYou/UserScripts
  12. // @supportURL https://github.com/MiPoNianYou/UserScripts/issues
  13. // @license GPL-3.0
  14. // @match https://chat.deepseek.com/*
  15. // @grant GM_addStyle
  16. // @grant GM_setValue
  17. // @grant GM_getValue
  18. // ==/UserScript==
  19.  
  20. (function () {
  21. "use strict";
  22.  
  23. const Config = {
  24. SCRIPT_SETTINGS: {
  25. FONT_STACK: "-apple-system, BlinkMacSystemFont, system-ui, sans-serif",
  26. ANIMATION_DURATION_MS: 350,
  27. ANIMATION_EASING_POP_OUT: "cubic-bezier(0.34, 1.56, 0.64, 1)",
  28. ANIMATION_EASING_STANDARD_INTERACTIVE: "cubic-bezier(0, 0, 0.58, 1)",
  29. BREATHING_ANIMATION_DURATION: "2.2s",
  30. DEBOUNCE_DELAY_MS: 150,
  31. },
  32. STORAGE_KEYS: {
  33. CUSTOM_SHORTCUTS_PREFIX: "dsk_custom_shortcuts_",
  34. },
  35. ELEMENT_SELECTORS: {
  36. REGENERATE_BUTTON: {
  37. parentSelector: "div._4f9bf79.d7dc56a8",
  38. parentPosition: "last",
  39. selector: ".ds-icon-button",
  40. filterText: "#重新生成",
  41. },
  42. CONTINUE_BUTTON: { selector: ".ds-button", filterText: "继续生成" },
  43. STOP_GENERATING_BUTTON: { selector: "._7436101", position: "first" },
  44. LAST_COPY_BUTTON: {
  45. parentSelector: "div._4f9bf79.d7dc56a8",
  46. parentPosition: "last",
  47. selector: "._965abe9 .ds-icon-button",
  48. childPosition: "first",
  49. },
  50. LAST_EDIT_BUTTON: {
  51. parentSelector: "._9663006",
  52. parentPosition: "last",
  53. selector: "._78e0558 .ds-icon-button",
  54. childPosition: "last",
  55. },
  56. DEEP_THINK_MODE_BUTTON: {
  57. selector: ".ds-button span",
  58. filterText: "深度思考",
  59. },
  60. SEARCH_MODE_BUTTON: {
  61. selector: ".ds-button span",
  62. filterText: "联网搜索",
  63. },
  64. UPLOAD_FILE_BUTTON: { selector: ".f02f0e25", position: "first" },
  65. NEW_CHAT_BUTTON: { selector: "._217e214", position: "first" },
  66. TOGGLE_SIDEBAR_BUTTON: {
  67. selector: ".ds-icon-button",
  68. filterText: "svg #打开边栏0730, svg #折叠边栏0730",
  69. },
  70. CURRENT_CHAT_MENU_BUTTON: {
  71. parentSelector: "._83421f9.b64fb9ae",
  72. parentPosition: "last",
  73. selector: "._2090548",
  74. childPosition: "first",
  75. },
  76. },
  77. ELEMENT_IDS: {
  78. HELP_PANEL: "dsk-help-panel",
  79. HELP_PANEL_ANIMATE_IN: "dsk-help-panel-animate-in",
  80. HELP_PANEL_ANIMATE_OUT: "dsk-help-panel-animate-out",
  81. },
  82. CSS_CLASSES: {
  83. HELP_PANEL_VISIBLE: "dsk-help-panel--visible",
  84. HELP_PANEL_CLOSE_BUTTON: "dsk-help-panel-close-button",
  85. HELP_PANEL_TITLE: "dsk-help-panel-title",
  86. HELP_PANEL_CONTENT: "dsk-help-panel-content",
  87. HELP_PANEL_ROW: "dsk-help-panel-row",
  88. HELP_PANEL_KEY: "dsk-help-panel-key",
  89. HELP_PANEL_KEY_DISPLAY: "dsk-help-panel-key--display",
  90. HELP_PANEL_KEY_SETTING: "dsk-help-panel-key--setting",
  91. HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT:
  92. "dsk-help-panel-key--configurable-highlight",
  93. HELP_PANEL_KEY_LISTENING: "dsk-help-panel-key--listening",
  94. HELP_PANEL_KEY_INVALID_SHAKE: "dsk-key-invalid-shake",
  95. HELP_PANEL_DESCRIPTION: "dsk-help-panel-description",
  96. HELP_PANEL_WARNING: "dsk-help-panel-warning",
  97. },
  98. UI_STRINGS: {
  99. HELP_PANEL_TITLE: "快捷按键指北",
  100. HELP_PANEL_WARNING_TEXT: "⚠️ 脚本依UA自动适配快捷键 篡改UA或致功能异常",
  101. CUSTOMIZE_SHORTCUTS_LABEL: "自定义快捷键",
  102. SETTINGS_BUTTON_TEXT: "设置自定按键",
  103. FINISH_CUSTOMIZING_BUTTON_TEXT: "完成自定设置",
  104. PRESS_NEW_SHORTCUT_TEXT: "请按下快捷键",
  105. KEY_CONFLICT_TEXT_PREFIX: "键 「",
  106. KEY_CONFLICT_TEXT_SUFFIX: "」 已被使用",
  107. INVALID_MODIFIER_TEXT_PREFIX: "请按 ",
  108. INVALID_MODIFIER_TEXT_SUFFIX: " + 字母/数字",
  109. },
  110. DEFAULT_SHORTCUT_CONFIG: {
  111. MODIFIERS: (() => {
  112. const isMac = /Macintosh|Mac OS X/i.test(navigator.userAgent);
  113. return {
  114. CHARACTER_DISPLAY: isMac ? "⌃ Control" : "Alt",
  115. EVENT_PROPERTY: isMac ? "ctrlKey" : "altKey",
  116. };
  117. })(),
  118. SHORTCUTS: [
  119. {
  120. id: "regenerate",
  121. key: "R",
  122. description: "重新生成回答",
  123. selectorConfig: "REGENERATE_BUTTON",
  124. },
  125. {
  126. id: "continueGenerating",
  127. key: "C",
  128. description: "继续生成回答",
  129. selectorConfig: "CONTINUE_BUTTON",
  130. },
  131. {
  132. id: "stopGenerating",
  133. key: "Q",
  134. description: "中断当前生成",
  135. selectorConfig: "STOP_GENERATING_BUTTON",
  136. },
  137. {
  138. id: "copyLastResponse",
  139. key: "K",
  140. description: "复制末条回答",
  141. selectorConfig: "LAST_COPY_BUTTON",
  142. },
  143. {
  144. id: "editLastQuery",
  145. key: "E",
  146. description: "编辑末次提问",
  147. selectorConfig: "LAST_EDIT_BUTTON",
  148. },
  149. {
  150. id: "deepThinkMode",
  151. key: "D",
  152. description: "深度思考模式",
  153. selectorConfig: "DEEP_THINK_MODE_BUTTON",
  154. },
  155. {
  156. id: "searchMode",
  157. key: "S",
  158. description: "联网搜索模式",
  159. selectorConfig: "SEARCH_MODE_BUTTON",
  160. },
  161. {
  162. id: "uploadFile",
  163. key: "U",
  164. description: "上传本地文件",
  165. selectorConfig: "UPLOAD_FILE_BUTTON",
  166. },
  167. {
  168. id: "newChat",
  169. key: "N",
  170. description: "新建对话窗口",
  171. selectorConfig: "NEW_CHAT_BUTTON",
  172. },
  173. {
  174. id: "toggleSidebar",
  175. key: "T",
  176. description: "切换开关边栏",
  177. selectorConfig: "TOGGLE_SIDEBAR_BUTTON",
  178. },
  179. {
  180. id: "currentChatMenu",
  181. key: "I",
  182. description: "当前对话菜单",
  183. selectorConfig: "CURRENT_CHAT_MENU_BUTTON",
  184. },
  185. {
  186. id: "toggleHelpPanel",
  187. key: "H",
  188. description: "快捷按键帮助",
  189. actionIdentifier: "toggleHelpPanel",
  190. isSpecialAction: true,
  191. },
  192. {
  193. id: "settingsEntry",
  194. key: null,
  195. description: "placeholder_settings_label",
  196. isSettingsEntry: true,
  197. actionIdentifier: "toggleCustomizationMode",
  198. nonConfigurable: true,
  199. },
  200. ],
  201. },
  202. };
  203. Config.DEFAULT_SHORTCUT_CONFIG.SHORTCUTS.find(
  204. (s) => s.id === "settingsEntry"
  205. ).description = Config.UI_STRINGS.CUSTOMIZE_SHORTCUTS_LABEL;
  206.  
  207. const State = {
  208. isCustomizingShortcuts: false,
  209. activeCustomizationTarget: null,
  210. currentShortcutConfig: JSON.parse(
  211. JSON.stringify(Config.DEFAULT_SHORTCUT_CONFIG)
  212. ),
  213.  
  214. initialize() {
  215. this.loadCustomShortcuts();
  216. },
  217.  
  218. loadCustomShortcuts() {
  219. this.currentShortcutConfig.SHORTCUTS.forEach((shortcut) => {
  220. if (shortcut.nonConfigurable || shortcut.isSettingsEntry) return;
  221. try {
  222. const savedKey = GM_getValue(
  223. `${Config.STORAGE_KEYS.CUSTOM_SHORTCUTS_PREFIX}${shortcut.id}`,
  224. shortcut.key
  225. );
  226. if (
  227. savedKey &&
  228. typeof savedKey === "string" &&
  229. savedKey.match(/^[a-zA-Z0-9]$/i)
  230. ) {
  231. shortcut.key = savedKey.toUpperCase();
  232. }
  233. } catch (e) {}
  234. });
  235. },
  236.  
  237. saveCustomShortcut(shortcutId, newKey) {
  238. const shortcutToUpdate = this.currentShortcutConfig.SHORTCUTS.find(
  239. (s) => s.id === shortcutId
  240. );
  241. if (shortcutToUpdate) {
  242. shortcutToUpdate.key = newKey.toUpperCase();
  243. try {
  244. GM_setValue(
  245. `${Config.STORAGE_KEYS.CUSTOM_SHORTCUTS_PREFIX}${shortcutId}`,
  246. shortcutToUpdate.key
  247. );
  248. } catch (e) {}
  249. EventManager.reinitializeGlobalKeyListener();
  250. }
  251. },
  252.  
  253. getActiveModifierProperty() {
  254. return this.currentShortcutConfig.MODIFIERS.EVENT_PROPERTY;
  255. },
  256.  
  257. getActiveModifierCharDisplay() {
  258. return this.currentShortcutConfig.MODIFIERS.CHARACTER_DISPLAY;
  259. },
  260. };
  261.  
  262. const UserInterface = {
  263. injectStyles() {
  264. const styles = `
  265. :root {
  266. --ctp-frappe-rosewater: rgb(242, 213, 207);
  267. --ctp-frappe-flamingo: rgb(238, 190, 190);
  268. --ctp-frappe-pink: rgb(244, 184, 228);
  269. --ctp-frappe-mauve: rgb(202, 158, 230);
  270. --ctp-frappe-red: rgb(231, 130, 132);
  271. --ctp-frappe-maroon: rgb(234, 153, 156);
  272. --ctp-frappe-peach: rgb(239, 159, 118);
  273. --ctp-frappe-yellow: rgb(229, 200, 144);
  274. --ctp-frappe-green: rgb(166, 209, 137);
  275. --ctp-frappe-teal: rgb(129, 200, 190);
  276. --ctp-frappe-sky: rgb(153, 209, 219);
  277. --ctp-frappe-sapphire: rgb(133, 193, 220);
  278. --ctp-frappe-blue: rgb(140, 170, 238);
  279. --ctp-frappe-lavender: rgb(186, 187, 241);
  280. --ctp-frappe-text: rgb(198, 208, 245);
  281. --ctp-frappe-subtext1: rgb(181, 191, 226);
  282. --ctp-frappe-subtext0: rgb(165, 173, 206);
  283. --ctp-frappe-overlay2: rgb(148, 156, 187);
  284. --ctp-frappe-overlay1: rgb(131, 139, 167);
  285. --ctp-frappe-overlay0: rgb(115, 121, 148);
  286. --ctp-frappe-surface2: rgb(98, 104, 128);
  287. --ctp-frappe-surface1: rgb(81, 87, 109);
  288. --ctp-frappe-surface0: rgb(65, 69, 89);
  289. --ctp-frappe-base: rgb(48, 52, 70);
  290. --ctp-frappe-mantle: rgb(41, 44, 60);
  291. --ctp-frappe-crust: rgb(35, 38, 52);
  292. --ctp-frappe-crust-rgb: 35, 38, 52;
  293.  
  294. --dsk-panel-bg: rgb(from var(--ctp-frappe-mantle) r g b / 0.85);
  295. --dsk-panel-border: rgb(from var(--ctp-frappe-surface0) r g b / 0.5);
  296. --dsk-panel-shadow:
  297. 0 1px 3px rgba(var(--ctp-frappe-crust-rgb), 0.12),
  298. 0 6px 16px rgba(var(--ctp-frappe-crust-rgb), 0.10),
  299. 0 12px 28px rgba(var(--ctp-frappe-crust-rgb), 0.08);
  300. --dsk-text-primary: var(--ctp-frappe-text);
  301. --dsk-text-secondary: var(--ctp-frappe-subtext0);
  302. --dsk-key-bg: var(--ctp-frappe-surface0);
  303. --dsk-key-border: var(--ctp-frappe-surface1);
  304. --dsk-key-setting-text: var(--ctp-frappe-blue);
  305. --dsk-key-setting-hover-bg: var(--ctp-frappe-surface1);
  306. --dsk-key-breathing-highlight-color: var(--ctp-frappe-mauve);
  307. --dsk-key-listening-border: var(--ctp-frappe-green);
  308. --dsk-key-listening-bg: color-mix(in srgb, var(--dsk-key-bg) 85%, var(--ctp-frappe-green) 15%);
  309. --dsk-key-invalid-shake-color: var(--ctp-frappe-red);
  310. --dsk-warning-bg: rgb(from var(--ctp-frappe-surface0) r g b / 0.5);
  311. --dsk-warning-border: var(--ctp-frappe-surface1);
  312. --dsk-warning-text: var(--ctp-frappe-yellow);
  313. --dsk-scrollbar-thumb: var(--ctp-frappe-overlay0);
  314. --dsk-scrollbar-thumb-hover: var(--ctp-frappe-overlay1);
  315. --dsk-close-button-bg: var(--ctp-frappe-red);
  316. --dsk-close-button-hover-bg: color-mix(in srgb, var(--ctp-frappe-red) 80%, var(--ctp-frappe-crust) 20%);
  317. --dsk-close-button-symbol: rgba(var(--ctp-frappe-crust-rgb), 0.7);
  318. }
  319.  
  320. @keyframes dsk-opacity-breathing-effect {
  321. 0%, 100% {
  322. opacity: 0;
  323. }
  324. 50% {
  325. opacity: 0.25;
  326. }
  327. }
  328.  
  329. @keyframes dsk-border-breathing-effect {
  330. 0%, 100% {
  331. border-color: var(--dsk-key-border);
  332. }
  333. 50% {
  334. border-color: var(--dsk-key-breathing-highlight-color);
  335. }
  336. }
  337.  
  338. @keyframes dsk-invalid-shake-effect {
  339. 0%, 100% {
  340. transform: translateX(0);
  341. }
  342. 10%, 30%, 50%, 70%, 90% {
  343. transform: translateX(-3px);
  344. }
  345. 20%, 40%, 60%, 80% {
  346. transform: translateX(3px);
  347. }
  348. }
  349.  
  350. @keyframes ${Config.ELEMENT_IDS.HELP_PANEL_ANIMATE_IN} {
  351. 0% {
  352. transform: translate(-50%, -50%) scale(0.88);
  353. opacity: 0;
  354. }
  355. 100% {
  356. transform: translate(-50%, -50%) scale(1);
  357. opacity: 1;
  358. }
  359. }
  360.  
  361. @keyframes ${Config.ELEMENT_IDS.HELP_PANEL_ANIMATE_OUT} {
  362. 0% {
  363. transform: translate(-50%, -50%) scale(1);
  364. opacity: 1;
  365. }
  366. 100% {
  367. transform: translate(-50%, -50%) scale(0.9);
  368. opacity: 0;
  369. }
  370. }
  371.  
  372. #${Config.ELEMENT_IDS.HELP_PANEL} {
  373. position: fixed;
  374. top: 50%;
  375. left: 50%;
  376. transform: translate(-50%, -50%) scale(0.88);
  377. opacity: 0;
  378. visibility: hidden;
  379. z-index: 2147483647;
  380. min-width: 300px;
  381. max-width: 480px;
  382. padding: 24px;
  383. border: 1px solid var(--dsk-panel-border);
  384. border-radius: 16px;
  385. background-color: var(--dsk-panel-bg);
  386. color: var(--dsk-text-primary);
  387. font-family: ${Config.SCRIPT_SETTINGS.FONT_STACK};
  388. font-size: 14px;
  389. font-weight: 500;
  390. line-height: 1.5;
  391. box-shadow: var(--dsk-panel-shadow);
  392. backdrop-filter: blur(20px) saturate(180%);
  393. -webkit-backdrop-filter: blur(20px) saturate(180%);
  394. display: flex;
  395. flex-direction: column;
  396. pointer-events: none;
  397. }
  398.  
  399. #${Config.ELEMENT_IDS.HELP_PANEL}.${Config.CSS_CLASSES.HELP_PANEL_VISIBLE} {
  400. pointer-events: auto;
  401. }
  402.  
  403. .${Config.CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON} {
  404. position: absolute;
  405. top: 14px;
  406. left: 14px;
  407. width: 12px;
  408. height: 12px;
  409. padding: 0;
  410. border: none;
  411. border-radius: 50%;
  412. background-color: var(--dsk-close-button-bg);
  413. cursor: pointer;
  414. display: flex;
  415. align-items: center;
  416. justify-content: center;
  417. transition: background-color 0.15s ${Config.SCRIPT_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE},
  418. transform 0.15s ${Config.SCRIPT_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE};
  419. appearance: none;
  420. -webkit-appearance: none;
  421. outline: none;
  422. }
  423.  
  424. .${Config.CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON}::before {
  425. content: '✕';
  426. display: block;
  427. color: transparent;
  428. font-size: 10px;
  429. font-weight: bold;
  430. line-height: 12px;
  431. text-align: center;
  432. transition: color 0.1s ${Config.SCRIPT_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE};
  433. }
  434.  
  435. .${Config.CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON}:hover {
  436. background-color: var(--dsk-close-button-hover-bg);
  437. }
  438.  
  439. .${Config.CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON}:hover::before {
  440. color: var(--dsk-close-button-symbol);
  441. }
  442.  
  443. .${Config.CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON}:active {
  444. filter: brightness(0.85);
  445. transform: scale(0.9);
  446. }
  447.  
  448. .${Config.CSS_CLASSES.HELP_PANEL_TITLE} {
  449. margin: 0 0 18px 0;
  450. padding-top: 8px;
  451. color: var(--dsk-text-primary);
  452. font-size: 17px;
  453. font-weight: 600;
  454. text-align: center;
  455. flex-shrink: 0;
  456. }
  457.  
  458. .${Config.CSS_CLASSES.HELP_PANEL_CONTENT} {
  459. flex-grow: 1;
  460. overflow-y: auto;
  461. max-height: 60vh;
  462. margin-right: -12px;
  463. padding-right: 12px;
  464. scrollbar-width: thin;
  465. scrollbar-color: var(--dsk-scrollbar-thumb) transparent;
  466. }
  467.  
  468. .${Config.CSS_CLASSES.HELP_PANEL_CONTENT}::-webkit-scrollbar {
  469. width: 6px;
  470. }
  471.  
  472. .${Config.CSS_CLASSES.HELP_PANEL_CONTENT}::-webkit-scrollbar-track {
  473. background: transparent;
  474. margin: 4px 0;
  475. }
  476.  
  477. .${Config.CSS_CLASSES.HELP_PANEL_CONTENT}::-webkit-scrollbar-thumb {
  478. background-color: var(--dsk-scrollbar-thumb);
  479. border-radius: 3px;
  480. transition: background-color 0.15s ${Config.SCRIPT_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE};
  481. }
  482.  
  483. .${Config.CSS_CLASSES.HELP_PANEL_CONTENT}::-webkit-scrollbar-thumb:hover {
  484. background-color: var(--dsk-scrollbar-thumb-hover);
  485. }
  486.  
  487. .${Config.CSS_CLASSES.HELP_PANEL_ROW} {
  488. display: flex;
  489. justify-content: space-between;
  490. align-items: center;
  491. margin-bottom: 12px;
  492. padding: 6px 2px;
  493. }
  494.  
  495. .${Config.CSS_CLASSES.HELP_PANEL_CONTENT} > .${Config.CSS_CLASSES.HELP_PANEL_ROW}:last-child {
  496. margin-bottom: 0;
  497. }
  498.  
  499. .${Config.CSS_CLASSES.HELP_PANEL_KEY} {
  500. min-width: 95px;
  501. padding: 5px 10px;
  502. margin-left: 18px;
  503. background-color: var(--dsk-key-bg);
  504. border: 1px solid var(--dsk-key-border);
  505. border-radius: 6px;
  506. box-shadow: 0 1px 1px rgba(0,0,0,0.08), inset 0 1px 1px rgba(255,255,255,0.03);
  507. color: var(--dsk-text-primary);
  508. font-family: inherit;
  509. font-size: 13px;
  510. font-weight: 500;
  511. text-align: center;
  512. flex-shrink: 0;
  513. transition: background-color 0.15s ${Config.SCRIPT_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE},
  514. border-color 0.15s ${Config.SCRIPT_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE},
  515. color 0.15s ${Config.SCRIPT_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE};
  516. cursor: default;
  517. position: relative;
  518. }
  519.  
  520. .${Config.CSS_CLASSES.HELP_PANEL_KEY}.${Config.CSS_CLASSES.HELP_PANEL_KEY_DISPLAY} {
  521. }
  522.  
  523. .${Config.CSS_CLASSES.HELP_PANEL_KEY}.${Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING} {
  524. color: var(--dsk-key-setting-text);
  525. cursor: pointer;
  526. }
  527.  
  528. .${Config.CSS_CLASSES.HELP_PANEL_KEY}.${Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING}:hover {
  529. background-color: var(--dsk-key-setting-hover-bg);
  530. border-color: var(--ctp-frappe-overlay0);
  531. }
  532.  
  533. .${Config.CSS_CLASSES.HELP_PANEL_KEY}.${Config.CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT} {
  534. animation: dsk-border-breathing-effect ${Config.SCRIPT_SETTINGS.BREATHING_ANIMATION_DURATION} infinite ease-in-out;
  535. cursor: pointer;
  536. }
  537.  
  538. .${Config.CSS_CLASSES.HELP_PANEL_KEY}.${Config.CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT}::after {
  539. content: '';
  540. position: absolute;
  541. top: 0;
  542. left: 0;
  543. right: 0;
  544. bottom: 0;
  545. border-radius: inherit;
  546. background-color: var(--dsk-key-breathing-highlight-color);
  547. opacity: 0;
  548. z-index: 0;
  549. pointer-events: none;
  550. animation: dsk-opacity-breathing-effect ${Config.SCRIPT_SETTINGS.BREATHING_ANIMATION_DURATION} infinite ease-in-out;
  551. }
  552.  
  553. .${Config.CSS_CLASSES.HELP_PANEL_KEY}.${Config.CSS_CLASSES.HELP_PANEL_KEY_LISTENING} {
  554. border-color: var(--dsk-key-listening-border) !important;
  555. background-color: var(--dsk-key-listening-bg) !important;
  556. color: var(--ctp-frappe-green) !important;
  557. animation: none !important;
  558. cursor: default !important;
  559. }
  560.  
  561. .${Config.CSS_CLASSES.HELP_PANEL_KEY}.${Config.CSS_CLASSES.HELP_PANEL_KEY_LISTENING}::after {
  562. animation: none !important;
  563. opacity: 0 !important;
  564. }
  565.  
  566. .${Config.CSS_CLASSES.HELP_PANEL_KEY}.${Config.CSS_CLASSES.HELP_PANEL_KEY_INVALID_SHAKE} {
  567. animation: dsk-invalid-shake-effect 0.5s ease;
  568. border-color: var(--dsk-key-invalid-shake-color) !important;
  569. color: var(--dsk-key-invalid-shake-color) !important;
  570. }
  571.  
  572. .${Config.CSS_CLASSES.HELP_PANEL_DESCRIPTION} {
  573. flex-grow: 1;
  574. padding-right: 10px;
  575. color: var(--dsk-text-secondary);
  576. font-size: 13.5px;
  577. }
  578.  
  579. .${Config.CSS_CLASSES.HELP_PANEL_WARNING} {
  580. margin-top: 20px;
  581. padding: 12px 16px;
  582. background-color: var(--dsk-warning-bg);
  583. border: 1px solid var(--dsk-warning-border);
  584. border-radius: 10px;
  585. color: var(--dsk-warning-text);
  586. font-size: 12.5px;
  587. font-weight: 500;
  588. line-height: 1.45;
  589. text-align: center;
  590. flex-shrink: 0;
  591. }
  592. `;
  593. try {
  594. GM_addStyle(styles);
  595. } catch (e) {
  596. const styleElement = document.createElement("style");
  597. styleElement.textContent = styles;
  598. (document.head || document.documentElement).appendChild(styleElement);
  599. }
  600. },
  601. };
  602.  
  603. const ShortcutManager = {
  604. getElementByConfig(selectorKeyName) {
  605. const config = Config.ELEMENT_SELECTORS[selectorKeyName];
  606. if (!config || !config.selector) return null;
  607.  
  608. const {
  609. selector,
  610. filterText,
  611. position = "first",
  612. parentSelector,
  613. parentPosition = "last",
  614. childPosition = "first",
  615. } = config;
  616. let baseElements = [];
  617.  
  618. if (parentSelector) {
  619. const parents = Array.from(document.querySelectorAll(parentSelector));
  620. if (parents.length === 0) return null;
  621. const parentIndex = parentPosition === "last" ? parents.length - 1 : 0;
  622. const targetParent = parents[parentIndex];
  623. if (!targetParent) return null;
  624. baseElements = Array.from(targetParent.querySelectorAll(selector));
  625. } else {
  626. baseElements = Array.from(document.querySelectorAll(selector));
  627. }
  628.  
  629. if (baseElements.length === 0) return null;
  630.  
  631. let targetElements = baseElements;
  632.  
  633. if (filterText) {
  634. const filters = filterText.split(",").map((f) => f.trim());
  635. targetElements = baseElements.filter((element) =>
  636. filters.some((ft) => {
  637. if (ft.startsWith("#")) {
  638. return (
  639. !!element.querySelector(ft) || element.id === ft.substring(1)
  640. );
  641. }
  642. return (
  643. element.textContent?.includes(ft) ||
  644. (ft.startsWith("svg #") &&
  645. element.querySelector(ft.replace("svg ", "")))
  646. );
  647. })
  648. );
  649. if (targetElements.length === 0) return null;
  650. }
  651.  
  652. const index = position === "last" ? targetElements.length - 1 : 0;
  653. return targetElements[index] || null;
  654. },
  655.  
  656. triggerElementClick(selectorKeyName) {
  657. const element = this.getElementByConfig(selectorKeyName);
  658. if (element && typeof element.click === "function") {
  659. element.click();
  660. return true;
  661. }
  662. return false;
  663. },
  664.  
  665. formatShortcutDisplay(shortcutKey) {
  666. if (!shortcutKey) return "---";
  667. return `${State.getActiveModifierCharDisplay()} + ${shortcutKey.toUpperCase()}`;
  668. },
  669. };
  670.  
  671. const HelpPanelManager = {
  672. helpPanelElement: null,
  673. shortcutDisplaySpansMap: new Map(),
  674. panelCloseTimer: null,
  675.  
  676. createPanel() {
  677. if (
  678. this.helpPanelElement &&
  679. document.body.contains(this.helpPanelElement)
  680. ) {
  681. this.updatePanelContentDynamic();
  682. return this.helpPanelElement;
  683. }
  684. this.shortcutDisplaySpansMap.clear();
  685.  
  686. const panel = document.createElement("div");
  687. panel.id = Config.ELEMENT_IDS.HELP_PANEL;
  688.  
  689. const closeButton = this.createCloseButton();
  690. const titleElement = this.createTitle();
  691. const contentContainer = this.createContentContainer();
  692. const warningElement = this.createWarning();
  693.  
  694. panel.appendChild(closeButton);
  695. panel.appendChild(titleElement);
  696. panel.appendChild(contentContainer);
  697. panel.appendChild(warningElement);
  698.  
  699. this.helpPanelElement = panel;
  700. document.body.appendChild(panel);
  701. return panel;
  702. },
  703.  
  704. createCloseButton() {
  705. const button = document.createElement("button");
  706. button.className = Config.CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON;
  707. button.setAttribute("aria-label", "Close help panel");
  708. button.addEventListener("click", (event) => {
  709. event.stopPropagation();
  710. if (State.isCustomizingShortcuts) {
  711. const settingsBtn = this.helpPanelElement?.querySelector(
  712. `.${Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
  713. );
  714. if (settingsBtn) this.toggleCustomizationModeUI(settingsBtn);
  715. }
  716. this.closePanel();
  717. });
  718. return button;
  719. },
  720.  
  721. createTitle() {
  722. const title = document.createElement("h3");
  723. title.className = Config.CSS_CLASSES.HELP_PANEL_TITLE;
  724. title.textContent = Config.UI_STRINGS.HELP_PANEL_TITLE;
  725. return title;
  726. },
  727.  
  728. createContentContainer() {
  729. const container = document.createElement("div");
  730. container.className = Config.CSS_CLASSES.HELP_PANEL_CONTENT;
  731. State.currentShortcutConfig.SHORTCUTS.forEach((shortcut) => {
  732. const row = document.createElement("div");
  733. row.className = Config.CSS_CLASSES.HELP_PANEL_ROW;
  734.  
  735. const descriptionSpan = document.createElement("span");
  736. descriptionSpan.className = Config.CSS_CLASSES.HELP_PANEL_DESCRIPTION;
  737. descriptionSpan.textContent = shortcut.description;
  738.  
  739. const keySpan = document.createElement("span");
  740. keySpan.className = Config.CSS_CLASSES.HELP_PANEL_KEY;
  741.  
  742. if (shortcut.isSettingsEntry) {
  743. keySpan.textContent = State.isCustomizingShortcuts
  744. ? Config.UI_STRINGS.FINISH_CUSTOMIZING_BUTTON_TEXT
  745. : Config.UI_STRINGS.SETTINGS_BUTTON_TEXT;
  746. keySpan.classList.add(Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING);
  747. keySpan.addEventListener("click", () =>
  748. this.toggleCustomizationModeUI(keySpan)
  749. );
  750. } else {
  751. keySpan.textContent = ShortcutManager.formatShortcutDisplay(
  752. shortcut.key
  753. );
  754. keySpan.classList.add(Config.CSS_CLASSES.HELP_PANEL_KEY_DISPLAY);
  755. this.shortcutDisplaySpansMap.set(shortcut.id, keySpan);
  756.  
  757. if (!shortcut.nonConfigurable) {
  758. keySpan.addEventListener("click", () => {
  759. if (
  760. State.isCustomizingShortcuts &&
  761. (!State.activeCustomizationTarget ||
  762. State.activeCustomizationTarget.shortcutId !== shortcut.id)
  763. ) {
  764. this.setListeningStateUI(shortcut.id, keySpan, true);
  765. } else if (
  766. State.isCustomizingShortcuts &&
  767. State.activeCustomizationTarget &&
  768. State.activeCustomizationTarget.shortcutId === shortcut.id
  769. ) {
  770. this.setListeningStateUI(shortcut.id, keySpan, false);
  771. }
  772. });
  773. }
  774. this.updateKeySpanAppearance(keySpan, shortcut);
  775. }
  776. row.appendChild(descriptionSpan);
  777. row.appendChild(keySpan);
  778. container.appendChild(row);
  779. });
  780. return container;
  781. },
  782.  
  783. createWarning() {
  784. const warning = document.createElement("div");
  785. warning.className = Config.CSS_CLASSES.HELP_PANEL_WARNING;
  786. warning.textContent = Config.UI_STRINGS.HELP_PANEL_WARNING_TEXT;
  787. return warning;
  788. },
  789.  
  790. updateKeySpanAppearance(keySpan, shortcut) {
  791. keySpan.classList.remove(
  792. Config.CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT,
  793. Config.CSS_CLASSES.HELP_PANEL_KEY_LISTENING
  794. );
  795. if (
  796. State.isCustomizingShortcuts &&
  797. !shortcut.isSettingsEntry &&
  798. !shortcut.nonConfigurable
  799. ) {
  800. if (
  801. State.activeCustomizationTarget &&
  802. State.activeCustomizationTarget.shortcutId === shortcut.id
  803. ) {
  804. keySpan.textContent = Config.UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
  805. keySpan.classList.add(Config.CSS_CLASSES.HELP_PANEL_KEY_LISTENING);
  806. } else {
  807. keySpan.classList.add(
  808. Config.CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT
  809. );
  810. }
  811. }
  812. },
  813.  
  814. updatePanelContentDynamic() {
  815. if (!this.helpPanelElement) return;
  816. const settingsButton = this.helpPanelElement.querySelector(
  817. `.${Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
  818. );
  819. if (settingsButton) {
  820. settingsButton.textContent = State.isCustomizingShortcuts
  821. ? Config.UI_STRINGS.FINISH_CUSTOMIZING_BUTTON_TEXT
  822. : Config.UI_STRINGS.SETTINGS_BUTTON_TEXT;
  823. }
  824. this.shortcutDisplaySpansMap.forEach((span, id) => {
  825. const shortcut = State.currentShortcutConfig.SHORTCUTS.find(
  826. (s) => s.id === id
  827. );
  828. if (shortcut) {
  829. span.textContent = ShortcutManager.formatShortcutDisplay(
  830. shortcut.key
  831. );
  832. this.updateKeySpanAppearance(span, shortcut);
  833. }
  834. });
  835. },
  836.  
  837. closePanel() {
  838. if (
  839. this.helpPanelElement &&
  840. this.helpPanelElement.classList.contains(
  841. Config.CSS_CLASSES.HELP_PANEL_VISIBLE
  842. )
  843. ) {
  844. this.helpPanelElement.style.animation = `${Config.ELEMENT_IDS.HELP_PANEL_ANIMATE_OUT} ${Config.SCRIPT_SETTINGS.ANIMATION_DURATION_MS}ms ${Config.SCRIPT_SETTINGS.ANIMATION_EASING_POP_OUT} forwards`;
  845. clearTimeout(this.panelCloseTimer);
  846. this.panelCloseTimer = setTimeout(() => {
  847. if (this.helpPanelElement) {
  848. this.helpPanelElement.classList.remove(
  849. Config.CSS_CLASSES.HELP_PANEL_VISIBLE
  850. );
  851. this.helpPanelElement.style.animation = "";
  852. this.helpPanelElement.style.opacity = "0";
  853. this.helpPanelElement.style.visibility = "hidden";
  854. }
  855. window.removeEventListener(
  856. "click",
  857. EventManager.handlePanelInteractionOutsideProxy,
  858. true
  859. );
  860. window.removeEventListener(
  861. "keydown",
  862. EventManager.handlePanelEscapeKeyProxy,
  863. true
  864. );
  865. }, Config.SCRIPT_SETTINGS.ANIMATION_DURATION_MS);
  866. }
  867. },
  868.  
  869. toggleVisibility() {
  870. if (
  871. !this.helpPanelElement ||
  872. !document.body.contains(this.helpPanelElement)
  873. ) {
  874. this.createPanel();
  875. } else {
  876. this.updatePanelContentDynamic();
  877. }
  878.  
  879. clearTimeout(this.panelCloseTimer);
  880. const isCurrentlyVisibleByClass =
  881. this.helpPanelElement.classList.contains(
  882. Config.CSS_CLASSES.HELP_PANEL_VISIBLE
  883. );
  884. let currentOpacity = 0;
  885. if (
  886. isCurrentlyVisibleByClass ||
  887. this.helpPanelElement.style.animationName ===
  888. Config.ELEMENT_IDS.HELP_PANEL_ANIMATE_OUT
  889. ) {
  890. currentOpacity = parseFloat(
  891. getComputedStyle(this.helpPanelElement).opacity
  892. );
  893. }
  894. const isAnimatingOut =
  895. this.helpPanelElement.style.animationName ===
  896. Config.ELEMENT_IDS.HELP_PANEL_ANIMATE_OUT;
  897.  
  898. if (
  899. isCurrentlyVisibleByClass &&
  900. currentOpacity > 0.01 &&
  901. !isAnimatingOut
  902. ) {
  903. if (State.isCustomizingShortcuts && !State.activeCustomizationTarget) {
  904. const settingsButton = this.helpPanelElement.querySelector(
  905. `.${Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
  906. );
  907. if (settingsButton) this.toggleCustomizationModeUI(settingsButton);
  908. }
  909. this.closePanel();
  910. } else {
  911. this.helpPanelElement.style.animation = "";
  912. this.helpPanelElement.style.opacity = "0";
  913. this.helpPanelElement.style.visibility = "hidden";
  914.  
  915. requestAnimationFrame(() => {
  916. this.helpPanelElement.classList.add(
  917. Config.CSS_CLASSES.HELP_PANEL_VISIBLE
  918. );
  919. this.helpPanelElement.style.visibility = "visible";
  920. this.helpPanelElement.style.animation = `${Config.ELEMENT_IDS.HELP_PANEL_ANIMATE_IN} ${Config.SCRIPT_SETTINGS.ANIMATION_DURATION_MS}ms ${Config.SCRIPT_SETTINGS.ANIMATION_EASING_POP_OUT} forwards`;
  921. });
  922. setTimeout(() => {
  923. window.addEventListener(
  924. "click",
  925. EventManager.handlePanelInteractionOutsideProxy,
  926. true
  927. );
  928. window.addEventListener(
  929. "keydown",
  930. EventManager.handlePanelEscapeKeyProxy,
  931. true
  932. );
  933. }, 0);
  934. }
  935. },
  936.  
  937. updateShortcutDisplayUI(shortcutId, newKey) {
  938. const spanElement = this.shortcutDisplaySpansMap.get(shortcutId);
  939. if (spanElement) {
  940. spanElement.textContent = newKey
  941. ? ShortcutManager.formatShortcutDisplay(newKey)
  942. : Config.UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
  943. }
  944. },
  945.  
  946. setListeningStateUI(shortcutId, spanElement, isListening) {
  947. if (isListening) {
  948. if (
  949. State.activeCustomizationTarget &&
  950. State.activeCustomizationTarget.spanElement !== spanElement
  951. ) {
  952. const prevShortcut = State.currentShortcutConfig.SHORTCUTS.find(
  953. (s) => s.id === State.activeCustomizationTarget.shortcutId
  954. );
  955. State.activeCustomizationTarget.spanElement.textContent =
  956. ShortcutManager.formatShortcutDisplay(prevShortcut?.key);
  957. this.updateKeySpanAppearance(
  958. State.activeCustomizationTarget.spanElement,
  959. prevShortcut
  960. );
  961. }
  962. State.activeCustomizationTarget = { shortcutId, spanElement };
  963. spanElement.textContent = Config.UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
  964. this.shortcutDisplaySpansMap.forEach((s) =>
  965. s.classList.remove(
  966. Config.CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT
  967. )
  968. );
  969. spanElement.classList.add(Config.CSS_CLASSES.HELP_PANEL_KEY_LISTENING);
  970. spanElement.classList.remove(
  971. Config.CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT
  972. );
  973. } else {
  974. if (
  975. State.activeCustomizationTarget &&
  976. State.activeCustomizationTarget.shortcutId === shortcutId
  977. ) {
  978. State.activeCustomizationTarget = null;
  979. }
  980. const currentKey = State.currentShortcutConfig.SHORTCUTS.find(
  981. (s) => s.id === shortcutId
  982. )?.key;
  983. spanElement.textContent =
  984. ShortcutManager.formatShortcutDisplay(currentKey);
  985. spanElement.classList.remove(
  986. Config.CSS_CLASSES.HELP_PANEL_KEY_LISTENING
  987. );
  988. if (State.isCustomizingShortcuts) {
  989. this.shortcutDisplaySpansMap.forEach((s, id) => {
  990. const cfg = State.currentShortcutConfig.SHORTCUTS.find(
  991. (sc) => sc.id === id
  992. );
  993. if (cfg && !cfg.nonConfigurable && !cfg.isSettingsEntry) {
  994. s.classList.add(
  995. Config.CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT
  996. );
  997. }
  998. });
  999. }
  1000. }
  1001. },
  1002.  
  1003. toggleCustomizationModeUI(settingsButtonSpan) {
  1004. State.isCustomizingShortcuts = !State.isCustomizingShortcuts;
  1005. if (State.activeCustomizationTarget) {
  1006. this.setListeningStateUI(
  1007. State.activeCustomizationTarget.shortcutId,
  1008. State.activeCustomizationTarget.spanElement,
  1009. false
  1010. );
  1011. }
  1012. settingsButtonSpan.textContent = State.isCustomizingShortcuts
  1013. ? Config.UI_STRINGS.FINISH_CUSTOMIZING_BUTTON_TEXT
  1014. : Config.UI_STRINGS.SETTINGS_BUTTON_TEXT;
  1015. this.shortcutDisplaySpansMap.forEach((span, id) => {
  1016. const shortcut = State.currentShortcutConfig.SHORTCUTS.find(
  1017. (s) => s.id === id
  1018. );
  1019. if (shortcut) this.updateKeySpanAppearance(span, shortcut);
  1020. });
  1021. },
  1022.  
  1023. displayKeyConflictWarning(spanElement, conflictingKey) {
  1024. spanElement.textContent = `${
  1025. Config.UI_STRINGS.KEY_CONFLICT_TEXT_PREFIX
  1026. }${conflictingKey.toUpperCase()}${
  1027. Config.UI_STRINGS.KEY_CONFLICT_TEXT_SUFFIX
  1028. }`;
  1029. setTimeout(() => {
  1030. if (
  1031. State.activeCustomizationTarget &&
  1032. State.activeCustomizationTarget.spanElement === spanElement
  1033. ) {
  1034. spanElement.textContent = Config.UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
  1035. }
  1036. }, 2000);
  1037. },
  1038.  
  1039. displayInvalidModifierWarning(spanElement) {
  1040. spanElement.textContent = `${
  1041. Config.UI_STRINGS.INVALID_MODIFIER_TEXT_PREFIX
  1042. }${State.getActiveModifierCharDisplay()}${
  1043. Config.UI_STRINGS.INVALID_MODIFIER_TEXT_SUFFIX
  1044. }`;
  1045. setTimeout(() => {
  1046. if (
  1047. State.activeCustomizationTarget &&
  1048. State.activeCustomizationTarget.spanElement === spanElement
  1049. ) {
  1050. spanElement.textContent = Config.UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
  1051. }
  1052. }, 2000);
  1053. },
  1054. triggerInvalidKeyShake(spanElement) {
  1055. spanElement.classList.add(
  1056. Config.CSS_CLASSES.HELP_PANEL_KEY_INVALID_SHAKE
  1057. );
  1058. setTimeout(() => {
  1059. spanElement.classList.remove(
  1060. Config.CSS_CLASSES.HELP_PANEL_KEY_INVALID_SHAKE
  1061. );
  1062. }, 500);
  1063. },
  1064. };
  1065.  
  1066. const EventManager = {
  1067. globalKeyDownListener: null,
  1068. debouncedToggleHelpPanel: null,
  1069.  
  1070. init() {
  1071. this.debouncedToggleHelpPanel = this.debounce(
  1072. HelpPanelManager.toggleVisibility.bind(HelpPanelManager),
  1073. Config.SCRIPT_SETTINGS.DEBOUNCE_DELAY_MS
  1074. );
  1075. this.reinitializeGlobalKeyListener();
  1076. },
  1077.  
  1078. debounce(func, wait) {
  1079. let timeout;
  1080. return function executedFunction(...args) {
  1081. const later = () => {
  1082. clearTimeout(timeout);
  1083. func.apply(this, args);
  1084. };
  1085. clearTimeout(timeout);
  1086. timeout = setTimeout(later, wait);
  1087. };
  1088. },
  1089.  
  1090. createGlobalKeyListener() {
  1091. const specialActionHandlers = {
  1092. toggleHelpPanel: this.debouncedToggleHelpPanel,
  1093. toggleCustomizationMode: () => {
  1094. if (HelpPanelManager.helpPanelElement) {
  1095. const settingsButton =
  1096. HelpPanelManager.helpPanelElement.querySelector(
  1097. `.${Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
  1098. );
  1099. if (settingsButton)
  1100. HelpPanelManager.toggleCustomizationModeUI(settingsButton);
  1101. }
  1102. },
  1103. };
  1104.  
  1105. const shortcutActionMap = {};
  1106. State.currentShortcutConfig.SHORTCUTS.forEach((shortcut) => {
  1107. if (shortcut.key && !shortcut.isSettingsEntry) {
  1108. const lowerKey = shortcut.key.toLowerCase();
  1109. if (
  1110. shortcut.actionIdentifier &&
  1111. specialActionHandlers[shortcut.actionIdentifier]
  1112. ) {
  1113. shortcutActionMap[lowerKey] =
  1114. specialActionHandlers[shortcut.actionIdentifier];
  1115. } else if (shortcut.selectorConfig) {
  1116. shortcutActionMap[lowerKey] = () =>
  1117. ShortcutManager.triggerElementClick(shortcut.selectorConfig);
  1118. }
  1119. }
  1120. });
  1121.  
  1122. return (event) => {
  1123. if (event.key === "Escape") {
  1124. return;
  1125. }
  1126.  
  1127. if (State.activeCustomizationTarget) {
  1128. event.preventDefault();
  1129. event.stopPropagation();
  1130. const newKey = event.key;
  1131. const targetSpan = State.activeCustomizationTarget.spanElement;
  1132.  
  1133. if (
  1134. newKey &&
  1135. newKey.length === 1 &&
  1136. !event.ctrlKey &&
  1137. !event.altKey &&
  1138. !event.shiftKey &&
  1139. !event.metaKey &&
  1140. !["Control", "Alt", "Shift", "Meta"].includes(newKey)
  1141. ) {
  1142. if (newKey.match(/^[a-zA-Z0-9]$/i)) {
  1143. const conflictingShortcut =
  1144. State.currentShortcutConfig.SHORTCUTS.find(
  1145. (s) =>
  1146. s.key &&
  1147. s.key.toLowerCase() === newKey.toLowerCase() &&
  1148. s.id !== State.activeCustomizationTarget.shortcutId
  1149. );
  1150. if (conflictingShortcut) {
  1151. HelpPanelManager.displayKeyConflictWarning(targetSpan, newKey);
  1152. return;
  1153. }
  1154. State.saveCustomShortcut(
  1155. State.activeCustomizationTarget.shortcutId,
  1156. newKey
  1157. );
  1158. HelpPanelManager.updateShortcutDisplayUI(
  1159. State.activeCustomizationTarget.shortcutId,
  1160. newKey
  1161. );
  1162. HelpPanelManager.setListeningStateUI(
  1163. State.activeCustomizationTarget.shortcutId,
  1164. targetSpan,
  1165. false
  1166. );
  1167. } else {
  1168. HelpPanelManager.triggerInvalidKeyShake(targetSpan);
  1169. }
  1170. } else if (!["Control", "Alt", "Shift", "Meta"].includes(newKey)) {
  1171. if (event[State.getActiveModifierProperty()]) {
  1172. const conflictingShortcut =
  1173. State.currentShortcutConfig.SHORTCUTS.find(
  1174. (s) =>
  1175. s.key &&
  1176. s.key.toLowerCase() === newKey.toLowerCase() &&
  1177. s.id !== State.activeCustomizationTarget.shortcutId
  1178. );
  1179. if (conflictingShortcut) {
  1180. HelpPanelManager.displayKeyConflictWarning(targetSpan, newKey);
  1181. return;
  1182. }
  1183. State.saveCustomShortcut(
  1184. State.activeCustomizationTarget.shortcutId,
  1185. newKey
  1186. );
  1187. HelpPanelManager.updateShortcutDisplayUI(
  1188. State.activeCustomizationTarget.shortcutId,
  1189. newKey
  1190. );
  1191. HelpPanelManager.setListeningStateUI(
  1192. State.activeCustomizationTarget.shortcutId,
  1193. targetSpan,
  1194. false
  1195. );
  1196. } else {
  1197. HelpPanelManager.displayInvalidModifierWarning(targetSpan);
  1198. }
  1199. }
  1200. return;
  1201. }
  1202.  
  1203. if (State.isCustomizingShortcuts) {
  1204. return;
  1205. }
  1206.  
  1207. if (!event[State.getActiveModifierProperty()]) {
  1208. return;
  1209. }
  1210.  
  1211. const pressedKey = event.key.toLowerCase();
  1212. const actionToExecute = shortcutActionMap[pressedKey];
  1213.  
  1214. if (typeof actionToExecute === "function") {
  1215. actionToExecute();
  1216. event.preventDefault();
  1217. event.stopPropagation();
  1218. }
  1219. };
  1220. },
  1221.  
  1222. reinitializeGlobalKeyListener() {
  1223. if (this.globalKeyDownListener) {
  1224. window.removeEventListener("keydown", this.globalKeyDownListener, true);
  1225. }
  1226. this.globalKeyDownListener = this.createGlobalKeyListener();
  1227. window.addEventListener("keydown", this.globalKeyDownListener, true);
  1228. },
  1229.  
  1230. handlePanelInteractionOutsideProxy(event) {
  1231. if (
  1232. HelpPanelManager.helpPanelElement &&
  1233. HelpPanelManager.helpPanelElement.classList.contains(
  1234. Config.CSS_CLASSES.HELP_PANEL_VISIBLE
  1235. ) &&
  1236. !HelpPanelManager.helpPanelElement.contains(event.target) &&
  1237. parseFloat(
  1238. getComputedStyle(HelpPanelManager.helpPanelElement).opacity
  1239. ) === 1
  1240. ) {
  1241. if (State.isCustomizingShortcuts) {
  1242. const settingsButton =
  1243. HelpPanelManager.helpPanelElement.querySelector(
  1244. `.${Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
  1245. );
  1246. if (settingsButton)
  1247. HelpPanelManager.toggleCustomizationModeUI(settingsButton);
  1248. }
  1249. HelpPanelManager.closePanel();
  1250. }
  1251. },
  1252.  
  1253. handlePanelEscapeKeyProxy(event) {
  1254. if (event.key === "Escape") {
  1255. if (
  1256. HelpPanelManager.helpPanelElement &&
  1257. HelpPanelManager.helpPanelElement.classList.contains(
  1258. Config.CSS_CLASSES.HELP_PANEL_VISIBLE
  1259. ) &&
  1260. parseFloat(
  1261. getComputedStyle(HelpPanelManager.helpPanelElement).opacity
  1262. ) === 1
  1263. ) {
  1264. event.preventDefault();
  1265. event.stopPropagation();
  1266. if (State.activeCustomizationTarget) {
  1267. HelpPanelManager.setListeningStateUI(
  1268. State.activeCustomizationTarget.shortcutId,
  1269. State.activeCustomizationTarget.spanElement,
  1270. false
  1271. );
  1272. } else if (State.isCustomizingShortcuts) {
  1273. const settingsButton =
  1274. HelpPanelManager.helpPanelElement.querySelector(
  1275. `.${Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
  1276. );
  1277. if (settingsButton)
  1278. HelpPanelManager.toggleCustomizationModeUI(settingsButton);
  1279. }
  1280. HelpPanelManager.closePanel();
  1281. }
  1282. }
  1283. },
  1284. };
  1285.  
  1286. const ScriptManager = {
  1287. init() {
  1288. try {
  1289. State.initialize();
  1290. UserInterface.injectStyles();
  1291. EventManager.init();
  1292. } catch (error) {}
  1293. },
  1294. };
  1295.  
  1296. if (document.readyState === "loading") {
  1297. document.addEventListener("DOMContentLoaded", () => ScriptManager.init(), {
  1298. once: true,
  1299. });
  1300. } else {
  1301. ScriptManager.init();
  1302. }
  1303. })();