// ==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.6.0
// @icon https://raw.githubusercontent.com/MiPoNianYou/UserScripts/main/Icons/DeepSeek-Shortcuts-Icon.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
// ==/UserScript==
(function () {
"use strict";
const Config = {
SCRIPT_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,
},
STORAGE_KEYS: {
CUSTOM_SHORTCUTS_PREFIX: "dsk_custom_shortcuts_",
},
ELEMENT_SELECTORS: {
REGENERATE_BUTTON: {
parentSelector: "div._4f9bf79.d7dc56a8",
parentPosition: "last",
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",
},
},
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",
},
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",
},
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: " + 字母/数字",
},
DEFAULT_SHORTCUT_CONFIG: {
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: "REGENERATE_BUTTON",
},
{
id: "continueGenerating",
key: "C",
description: "继续生成回答",
selectorConfig: "CONTINUE_BUTTON",
},
{
id: "stopGenerating",
key: "Q",
description: "中断当前生成",
selectorConfig: "STOP_GENERATING_BUTTON",
},
{
id: "copyLastResponse",
key: "K",
description: "复制末条回答",
selectorConfig: "LAST_COPY_BUTTON",
},
{
id: "editLastQuery",
key: "E",
description: "编辑末次提问",
selectorConfig: "LAST_EDIT_BUTTON",
},
{
id: "deepThinkMode",
key: "D",
description: "深度思考模式",
selectorConfig: "DEEP_THINK_MODE_BUTTON",
},
{
id: "searchMode",
key: "S",
description: "联网搜索模式",
selectorConfig: "SEARCH_MODE_BUTTON",
},
{
id: "uploadFile",
key: "U",
description: "上传本地文件",
selectorConfig: "UPLOAD_FILE_BUTTON",
},
{
id: "newChat",
key: "N",
description: "新建对话窗口",
selectorConfig: "NEW_CHAT_BUTTON",
},
{
id: "toggleSidebar",
key: "T",
description: "切换开关边栏",
selectorConfig: "TOGGLE_SIDEBAR_BUTTON",
},
{
id: "currentChatMenu",
key: "I",
description: "当前对话菜单",
selectorConfig: "CURRENT_CHAT_MENU_BUTTON",
},
{
id: "toggleHelpPanel",
key: "H",
description: "快捷按键帮助",
actionIdentifier: "toggleHelpPanel",
isSpecialAction: true,
},
{
id: "settingsEntry",
key: null,
description: "placeholder_settings_label",
isSettingsEntry: true,
actionIdentifier: "toggleCustomizationMode",
nonConfigurable: true,
},
],
},
};
Config.DEFAULT_SHORTCUT_CONFIG.SHORTCUTS.find(
(s) => s.id === "settingsEntry"
).description = Config.UI_STRINGS.CUSTOMIZE_SHORTCUTS_LABEL;
const State = {
isCustomizingShortcuts: false,
activeCustomizationTarget: null,
currentShortcutConfig: JSON.parse(
JSON.stringify(Config.DEFAULT_SHORTCUT_CONFIG)
),
initialize() {
this.loadCustomShortcuts();
},
loadCustomShortcuts() {
this.currentShortcutConfig.SHORTCUTS.forEach((shortcut) => {
if (shortcut.nonConfigurable || shortcut.isSettingsEntry) return;
try {
const savedKey = GM_getValue(
`${Config.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) {}
});
},
saveCustomShortcut(shortcutId, newKey) {
const shortcutToUpdate = this.currentShortcutConfig.SHORTCUTS.find(
(s) => s.id === shortcutId
);
if (shortcutToUpdate) {
shortcutToUpdate.key = newKey.toUpperCase();
try {
GM_setValue(
`${Config.STORAGE_KEYS.CUSTOM_SHORTCUTS_PREFIX}${shortcutId}`,
shortcutToUpdate.key
);
} catch (e) {}
EventManager.reinitializeGlobalKeyListener();
}
},
getActiveModifierProperty() {
return this.currentShortcutConfig.MODIFIERS.EVENT_PROPERTY;
},
getActiveModifierCharDisplay() {
return this.currentShortcutConfig.MODIFIERS.CHARACTER_DISPLAY;
},
};
const UserInterface = {
injectStyles() {
const styles = `
:root {
--ctp-frappe-rosewater: rgb(242, 213, 207);
--ctp-frappe-flamingo: rgb(238, 190, 190);
--ctp-frappe-pink: rgb(244, 184, 228);
--ctp-frappe-mauve: rgb(202, 158, 230);
--ctp-frappe-red: rgb(231, 130, 132);
--ctp-frappe-maroon: rgb(234, 153, 156);
--ctp-frappe-peach: rgb(239, 159, 118);
--ctp-frappe-yellow: rgb(229, 200, 144);
--ctp-frappe-green: rgb(166, 209, 137);
--ctp-frappe-teal: rgb(129, 200, 190);
--ctp-frappe-sky: rgb(153, 209, 219);
--ctp-frappe-sapphire: rgb(133, 193, 220);
--ctp-frappe-blue: rgb(140, 170, 238);
--ctp-frappe-lavender: rgb(186, 187, 241);
--ctp-frappe-text: rgb(198, 208, 245);
--ctp-frappe-subtext1: rgb(181, 191, 226);
--ctp-frappe-subtext0: rgb(165, 173, 206);
--ctp-frappe-overlay2: rgb(148, 156, 187);
--ctp-frappe-overlay1: rgb(131, 139, 167);
--ctp-frappe-overlay0: rgb(115, 121, 148);
--ctp-frappe-surface2: rgb(98, 104, 128);
--ctp-frappe-surface1: rgb(81, 87, 109);
--ctp-frappe-surface0: rgb(65, 69, 89);
--ctp-frappe-base: rgb(48, 52, 70);
--ctp-frappe-mantle: rgb(41, 44, 60);
--ctp-frappe-crust: rgb(35, 38, 52);
--ctp-frappe-crust-rgb: 35, 38, 52;
--dsk-panel-bg: rgb(from var(--ctp-frappe-mantle) r g b / 0.85);
--dsk-panel-border: rgb(from var(--ctp-frappe-surface0) r g b / 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: rgb(from var(--ctp-frappe-surface0) r g b / 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 ${Config.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 ${Config.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;
}
}
#${Config.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: ${Config.SCRIPT_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;
}
#${Config.ELEMENT_IDS.HELP_PANEL}.${Config.CSS_CLASSES.HELP_PANEL_VISIBLE} {
pointer-events: auto;
}
.${Config.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 ${Config.SCRIPT_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE},
transform 0.15s ${Config.SCRIPT_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE};
appearance: none;
-webkit-appearance: none;
outline: none;
}
.${Config.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 ${Config.SCRIPT_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE};
}
.${Config.CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON}:hover {
background-color: var(--dsk-close-button-hover-bg);
}
.${Config.CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON}:hover::before {
color: var(--dsk-close-button-symbol);
}
.${Config.CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON}:active {
filter: brightness(0.85);
transform: scale(0.9);
}
.${Config.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;
}
.${Config.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;
}
.${Config.CSS_CLASSES.HELP_PANEL_CONTENT}::-webkit-scrollbar {
width: 6px;
}
.${Config.CSS_CLASSES.HELP_PANEL_CONTENT}::-webkit-scrollbar-track {
background: transparent;
margin: 4px 0;
}
.${Config.CSS_CLASSES.HELP_PANEL_CONTENT}::-webkit-scrollbar-thumb {
background-color: var(--dsk-scrollbar-thumb);
border-radius: 3px;
transition: background-color 0.15s ${Config.SCRIPT_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE};
}
.${Config.CSS_CLASSES.HELP_PANEL_CONTENT}::-webkit-scrollbar-thumb:hover {
background-color: var(--dsk-scrollbar-thumb-hover);
}
.${Config.CSS_CLASSES.HELP_PANEL_ROW} {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 6px 2px;
}
.${Config.CSS_CLASSES.HELP_PANEL_CONTENT} > .${Config.CSS_CLASSES.HELP_PANEL_ROW}:last-child {
margin-bottom: 0;
}
.${Config.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 ${Config.SCRIPT_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE},
border-color 0.15s ${Config.SCRIPT_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE},
color 0.15s ${Config.SCRIPT_SETTINGS.ANIMATION_EASING_STANDARD_INTERACTIVE};
cursor: default;
position: relative;
}
.${Config.CSS_CLASSES.HELP_PANEL_KEY}.${Config.CSS_CLASSES.HELP_PANEL_KEY_DISPLAY} {
}
.${Config.CSS_CLASSES.HELP_PANEL_KEY}.${Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING} {
color: var(--dsk-key-setting-text);
cursor: pointer;
}
.${Config.CSS_CLASSES.HELP_PANEL_KEY}.${Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING}:hover {
background-color: var(--dsk-key-setting-hover-bg);
border-color: var(--ctp-frappe-overlay0);
}
.${Config.CSS_CLASSES.HELP_PANEL_KEY}.${Config.CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT} {
animation: dsk-border-breathing-effect ${Config.SCRIPT_SETTINGS.BREATHING_ANIMATION_DURATION} infinite ease-in-out;
cursor: pointer;
}
.${Config.CSS_CLASSES.HELP_PANEL_KEY}.${Config.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 ${Config.SCRIPT_SETTINGS.BREATHING_ANIMATION_DURATION} infinite ease-in-out;
}
.${Config.CSS_CLASSES.HELP_PANEL_KEY}.${Config.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;
}
.${Config.CSS_CLASSES.HELP_PANEL_KEY}.${Config.CSS_CLASSES.HELP_PANEL_KEY_LISTENING}::after {
animation: none !important;
opacity: 0 !important;
}
.${Config.CSS_CLASSES.HELP_PANEL_KEY}.${Config.CSS_CLASSES.HELP_PANEL_KEY_INVALID_SHAKE} {
animation: dsk-invalid-shake-effect 0.5s ease;
border-color: var(--dsk-key-invalid-shake-color) !important;
color: var(--dsk-key-invalid-shake-color) !important;
}
.${Config.CSS_CLASSES.HELP_PANEL_DESCRIPTION} {
flex-grow: 1;
padding-right: 10px;
color: var(--dsk-text-secondary);
font-size: 13.5px;
}
.${Config.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);
}
},
};
const ShortcutManager = {
getElementByConfig(selectorKeyName) {
const config = Config.ELEMENT_SELECTORS[selectorKeyName];
if (!config || !config.selector) return null;
const {
selector,
filterText,
position = "first",
parentSelector,
parentPosition = "last",
childPosition = "first",
} = config;
let baseElements = [];
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;
baseElements = Array.from(targetParent.querySelectorAll(selector));
} else {
baseElements = Array.from(document.querySelectorAll(selector));
}
if (baseElements.length === 0) return null;
let targetElements = baseElements;
if (filterText) {
const filters = filterText.split(",").map((f) => f.trim());
targetElements = baseElements.filter((element) =>
filters.some((ft) => {
if (ft.startsWith("#")) {
return (
!!element.querySelector(ft) || element.id === ft.substring(1)
);
}
return (
element.textContent?.includes(ft) ||
(ft.startsWith("svg #") &&
element.querySelector(ft.replace("svg ", "")))
);
})
);
if (targetElements.length === 0) return null;
}
const index = position === "last" ? targetElements.length - 1 : 0;
return targetElements[index] || null;
},
triggerElementClick(selectorKeyName) {
const element = this.getElementByConfig(selectorKeyName);
if (element && typeof element.click === "function") {
element.click();
return true;
}
return false;
},
formatShortcutDisplay(shortcutKey) {
if (!shortcutKey) return "---";
return `${State.getActiveModifierCharDisplay()} + ${shortcutKey.toUpperCase()}`;
},
};
const HelpPanelManager = {
helpPanelElement: null,
shortcutDisplaySpansMap: new Map(),
panelCloseTimer: null,
createPanel() {
if (
this.helpPanelElement &&
document.body.contains(this.helpPanelElement)
) {
this.updatePanelContentDynamic();
return this.helpPanelElement;
}
this.shortcutDisplaySpansMap.clear();
const panel = document.createElement("div");
panel.id = Config.ELEMENT_IDS.HELP_PANEL;
const closeButton = this.createCloseButton();
const titleElement = this.createTitle();
const contentContainer = this.createContentContainer();
const warningElement = this.createWarning();
panel.appendChild(closeButton);
panel.appendChild(titleElement);
panel.appendChild(contentContainer);
panel.appendChild(warningElement);
this.helpPanelElement = panel;
document.body.appendChild(panel);
return panel;
},
createCloseButton() {
const button = document.createElement("button");
button.className = Config.CSS_CLASSES.HELP_PANEL_CLOSE_BUTTON;
button.setAttribute("aria-label", "Close help panel");
button.addEventListener("click", (event) => {
event.stopPropagation();
if (State.isCustomizingShortcuts) {
const settingsBtn = this.helpPanelElement?.querySelector(
`.${Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
);
if (settingsBtn) this.toggleCustomizationModeUI(settingsBtn);
}
this.closePanel();
});
return button;
},
createTitle() {
const title = document.createElement("h3");
title.className = Config.CSS_CLASSES.HELP_PANEL_TITLE;
title.textContent = Config.UI_STRINGS.HELP_PANEL_TITLE;
return title;
},
createContentContainer() {
const container = document.createElement("div");
container.className = Config.CSS_CLASSES.HELP_PANEL_CONTENT;
State.currentShortcutConfig.SHORTCUTS.forEach((shortcut) => {
const row = document.createElement("div");
row.className = Config.CSS_CLASSES.HELP_PANEL_ROW;
const descriptionSpan = document.createElement("span");
descriptionSpan.className = Config.CSS_CLASSES.HELP_PANEL_DESCRIPTION;
descriptionSpan.textContent = shortcut.description;
const keySpan = document.createElement("span");
keySpan.className = Config.CSS_CLASSES.HELP_PANEL_KEY;
if (shortcut.isSettingsEntry) {
keySpan.textContent = State.isCustomizingShortcuts
? Config.UI_STRINGS.FINISH_CUSTOMIZING_BUTTON_TEXT
: Config.UI_STRINGS.SETTINGS_BUTTON_TEXT;
keySpan.classList.add(Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING);
keySpan.addEventListener("click", () =>
this.toggleCustomizationModeUI(keySpan)
);
} else {
keySpan.textContent = ShortcutManager.formatShortcutDisplay(
shortcut.key
);
keySpan.classList.add(Config.CSS_CLASSES.HELP_PANEL_KEY_DISPLAY);
this.shortcutDisplaySpansMap.set(shortcut.id, keySpan);
if (!shortcut.nonConfigurable) {
keySpan.addEventListener("click", () => {
if (
State.isCustomizingShortcuts &&
(!State.activeCustomizationTarget ||
State.activeCustomizationTarget.shortcutId !== shortcut.id)
) {
this.setListeningStateUI(shortcut.id, keySpan, true);
} else if (
State.isCustomizingShortcuts &&
State.activeCustomizationTarget &&
State.activeCustomizationTarget.shortcutId === shortcut.id
) {
this.setListeningStateUI(shortcut.id, keySpan, false);
}
});
}
this.updateKeySpanAppearance(keySpan, shortcut);
}
row.appendChild(descriptionSpan);
row.appendChild(keySpan);
container.appendChild(row);
});
return container;
},
createWarning() {
const warning = document.createElement("div");
warning.className = Config.CSS_CLASSES.HELP_PANEL_WARNING;
warning.textContent = Config.UI_STRINGS.HELP_PANEL_WARNING_TEXT;
return warning;
},
updateKeySpanAppearance(keySpan, shortcut) {
keySpan.classList.remove(
Config.CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT,
Config.CSS_CLASSES.HELP_PANEL_KEY_LISTENING
);
if (
State.isCustomizingShortcuts &&
!shortcut.isSettingsEntry &&
!shortcut.nonConfigurable
) {
if (
State.activeCustomizationTarget &&
State.activeCustomizationTarget.shortcutId === shortcut.id
) {
keySpan.textContent = Config.UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
keySpan.classList.add(Config.CSS_CLASSES.HELP_PANEL_KEY_LISTENING);
} else {
keySpan.classList.add(
Config.CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT
);
}
}
},
updatePanelContentDynamic() {
if (!this.helpPanelElement) return;
const settingsButton = this.helpPanelElement.querySelector(
`.${Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
);
if (settingsButton) {
settingsButton.textContent = State.isCustomizingShortcuts
? Config.UI_STRINGS.FINISH_CUSTOMIZING_BUTTON_TEXT
: Config.UI_STRINGS.SETTINGS_BUTTON_TEXT;
}
this.shortcutDisplaySpansMap.forEach((span, id) => {
const shortcut = State.currentShortcutConfig.SHORTCUTS.find(
(s) => s.id === id
);
if (shortcut) {
span.textContent = ShortcutManager.formatShortcutDisplay(
shortcut.key
);
this.updateKeySpanAppearance(span, shortcut);
}
});
},
closePanel() {
if (
this.helpPanelElement &&
this.helpPanelElement.classList.contains(
Config.CSS_CLASSES.HELP_PANEL_VISIBLE
)
) {
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`;
clearTimeout(this.panelCloseTimer);
this.panelCloseTimer = setTimeout(() => {
if (this.helpPanelElement) {
this.helpPanelElement.classList.remove(
Config.CSS_CLASSES.HELP_PANEL_VISIBLE
);
this.helpPanelElement.style.animation = "";
this.helpPanelElement.style.opacity = "0";
this.helpPanelElement.style.visibility = "hidden";
}
window.removeEventListener(
"click",
EventManager.handlePanelInteractionOutsideProxy,
true
);
window.removeEventListener(
"keydown",
EventManager.handlePanelEscapeKeyProxy,
true
);
}, Config.SCRIPT_SETTINGS.ANIMATION_DURATION_MS);
}
},
toggleVisibility() {
if (
!this.helpPanelElement ||
!document.body.contains(this.helpPanelElement)
) {
this.createPanel();
} else {
this.updatePanelContentDynamic();
}
clearTimeout(this.panelCloseTimer);
const isCurrentlyVisibleByClass =
this.helpPanelElement.classList.contains(
Config.CSS_CLASSES.HELP_PANEL_VISIBLE
);
let currentOpacity = 0;
if (
isCurrentlyVisibleByClass ||
this.helpPanelElement.style.animationName ===
Config.ELEMENT_IDS.HELP_PANEL_ANIMATE_OUT
) {
currentOpacity = parseFloat(
getComputedStyle(this.helpPanelElement).opacity
);
}
const isAnimatingOut =
this.helpPanelElement.style.animationName ===
Config.ELEMENT_IDS.HELP_PANEL_ANIMATE_OUT;
if (
isCurrentlyVisibleByClass &&
currentOpacity > 0.01 &&
!isAnimatingOut
) {
if (State.isCustomizingShortcuts && !State.activeCustomizationTarget) {
const settingsButton = this.helpPanelElement.querySelector(
`.${Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
);
if (settingsButton) this.toggleCustomizationModeUI(settingsButton);
}
this.closePanel();
} else {
this.helpPanelElement.style.animation = "";
this.helpPanelElement.style.opacity = "0";
this.helpPanelElement.style.visibility = "hidden";
requestAnimationFrame(() => {
this.helpPanelElement.classList.add(
Config.CSS_CLASSES.HELP_PANEL_VISIBLE
);
this.helpPanelElement.style.visibility = "visible";
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`;
});
setTimeout(() => {
window.addEventListener(
"click",
EventManager.handlePanelInteractionOutsideProxy,
true
);
window.addEventListener(
"keydown",
EventManager.handlePanelEscapeKeyProxy,
true
);
}, 0);
}
},
updateShortcutDisplayUI(shortcutId, newKey) {
const spanElement = this.shortcutDisplaySpansMap.get(shortcutId);
if (spanElement) {
spanElement.textContent = newKey
? ShortcutManager.formatShortcutDisplay(newKey)
: Config.UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
}
},
setListeningStateUI(shortcutId, spanElement, isListening) {
if (isListening) {
if (
State.activeCustomizationTarget &&
State.activeCustomizationTarget.spanElement !== spanElement
) {
const prevShortcut = State.currentShortcutConfig.SHORTCUTS.find(
(s) => s.id === State.activeCustomizationTarget.shortcutId
);
State.activeCustomizationTarget.spanElement.textContent =
ShortcutManager.formatShortcutDisplay(prevShortcut?.key);
this.updateKeySpanAppearance(
State.activeCustomizationTarget.spanElement,
prevShortcut
);
}
State.activeCustomizationTarget = { shortcutId, spanElement };
spanElement.textContent = Config.UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
this.shortcutDisplaySpansMap.forEach((s) =>
s.classList.remove(
Config.CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT
)
);
spanElement.classList.add(Config.CSS_CLASSES.HELP_PANEL_KEY_LISTENING);
spanElement.classList.remove(
Config.CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT
);
} else {
if (
State.activeCustomizationTarget &&
State.activeCustomizationTarget.shortcutId === shortcutId
) {
State.activeCustomizationTarget = null;
}
const currentKey = State.currentShortcutConfig.SHORTCUTS.find(
(s) => s.id === shortcutId
)?.key;
spanElement.textContent =
ShortcutManager.formatShortcutDisplay(currentKey);
spanElement.classList.remove(
Config.CSS_CLASSES.HELP_PANEL_KEY_LISTENING
);
if (State.isCustomizingShortcuts) {
this.shortcutDisplaySpansMap.forEach((s, id) => {
const cfg = State.currentShortcutConfig.SHORTCUTS.find(
(sc) => sc.id === id
);
if (cfg && !cfg.nonConfigurable && !cfg.isSettingsEntry) {
s.classList.add(
Config.CSS_CLASSES.HELP_PANEL_KEY_CONFIGURABLE_HIGHLIGHT
);
}
});
}
}
},
toggleCustomizationModeUI(settingsButtonSpan) {
State.isCustomizingShortcuts = !State.isCustomizingShortcuts;
if (State.activeCustomizationTarget) {
this.setListeningStateUI(
State.activeCustomizationTarget.shortcutId,
State.activeCustomizationTarget.spanElement,
false
);
}
settingsButtonSpan.textContent = State.isCustomizingShortcuts
? Config.UI_STRINGS.FINISH_CUSTOMIZING_BUTTON_TEXT
: Config.UI_STRINGS.SETTINGS_BUTTON_TEXT;
this.shortcutDisplaySpansMap.forEach((span, id) => {
const shortcut = State.currentShortcutConfig.SHORTCUTS.find(
(s) => s.id === id
);
if (shortcut) this.updateKeySpanAppearance(span, shortcut);
});
},
displayKeyConflictWarning(spanElement, conflictingKey) {
spanElement.textContent = `${
Config.UI_STRINGS.KEY_CONFLICT_TEXT_PREFIX
}${conflictingKey.toUpperCase()}${
Config.UI_STRINGS.KEY_CONFLICT_TEXT_SUFFIX
}`;
setTimeout(() => {
if (
State.activeCustomizationTarget &&
State.activeCustomizationTarget.spanElement === spanElement
) {
spanElement.textContent = Config.UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
}
}, 2000);
},
displayInvalidModifierWarning(spanElement) {
spanElement.textContent = `${
Config.UI_STRINGS.INVALID_MODIFIER_TEXT_PREFIX
}${State.getActiveModifierCharDisplay()}${
Config.UI_STRINGS.INVALID_MODIFIER_TEXT_SUFFIX
}`;
setTimeout(() => {
if (
State.activeCustomizationTarget &&
State.activeCustomizationTarget.spanElement === spanElement
) {
spanElement.textContent = Config.UI_STRINGS.PRESS_NEW_SHORTCUT_TEXT;
}
}, 2000);
},
triggerInvalidKeyShake(spanElement) {
spanElement.classList.add(
Config.CSS_CLASSES.HELP_PANEL_KEY_INVALID_SHAKE
);
setTimeout(() => {
spanElement.classList.remove(
Config.CSS_CLASSES.HELP_PANEL_KEY_INVALID_SHAKE
);
}, 500);
},
};
const EventManager = {
globalKeyDownListener: null,
debouncedToggleHelpPanel: null,
init() {
this.debouncedToggleHelpPanel = this.debounce(
HelpPanelManager.toggleVisibility.bind(HelpPanelManager),
Config.SCRIPT_SETTINGS.DEBOUNCE_DELAY_MS
);
this.reinitializeGlobalKeyListener();
},
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func.apply(this, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
createGlobalKeyListener() {
const specialActionHandlers = {
toggleHelpPanel: this.debouncedToggleHelpPanel,
toggleCustomizationMode: () => {
if (HelpPanelManager.helpPanelElement) {
const settingsButton =
HelpPanelManager.helpPanelElement.querySelector(
`.${Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
);
if (settingsButton)
HelpPanelManager.toggleCustomizationModeUI(settingsButton);
}
},
};
const shortcutActionMap = {};
State.currentShortcutConfig.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] = () =>
ShortcutManager.triggerElementClick(shortcut.selectorConfig);
}
}
});
return (event) => {
if (event.key === "Escape") {
return;
}
if (State.activeCustomizationTarget) {
event.preventDefault();
event.stopPropagation();
const newKey = event.key;
const targetSpan = State.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 =
State.currentShortcutConfig.SHORTCUTS.find(
(s) =>
s.key &&
s.key.toLowerCase() === newKey.toLowerCase() &&
s.id !== State.activeCustomizationTarget.shortcutId
);
if (conflictingShortcut) {
HelpPanelManager.displayKeyConflictWarning(targetSpan, newKey);
return;
}
State.saveCustomShortcut(
State.activeCustomizationTarget.shortcutId,
newKey
);
HelpPanelManager.updateShortcutDisplayUI(
State.activeCustomizationTarget.shortcutId,
newKey
);
HelpPanelManager.setListeningStateUI(
State.activeCustomizationTarget.shortcutId,
targetSpan,
false
);
} else {
HelpPanelManager.triggerInvalidKeyShake(targetSpan);
}
} else if (!["Control", "Alt", "Shift", "Meta"].includes(newKey)) {
if (event[State.getActiveModifierProperty()]) {
const conflictingShortcut =
State.currentShortcutConfig.SHORTCUTS.find(
(s) =>
s.key &&
s.key.toLowerCase() === newKey.toLowerCase() &&
s.id !== State.activeCustomizationTarget.shortcutId
);
if (conflictingShortcut) {
HelpPanelManager.displayKeyConflictWarning(targetSpan, newKey);
return;
}
State.saveCustomShortcut(
State.activeCustomizationTarget.shortcutId,
newKey
);
HelpPanelManager.updateShortcutDisplayUI(
State.activeCustomizationTarget.shortcutId,
newKey
);
HelpPanelManager.setListeningStateUI(
State.activeCustomizationTarget.shortcutId,
targetSpan,
false
);
} else {
HelpPanelManager.displayInvalidModifierWarning(targetSpan);
}
}
return;
}
if (State.isCustomizingShortcuts) {
return;
}
if (!event[State.getActiveModifierProperty()]) {
return;
}
const pressedKey = event.key.toLowerCase();
const actionToExecute = shortcutActionMap[pressedKey];
if (typeof actionToExecute === "function") {
actionToExecute();
event.preventDefault();
event.stopPropagation();
}
};
},
reinitializeGlobalKeyListener() {
if (this.globalKeyDownListener) {
window.removeEventListener("keydown", this.globalKeyDownListener, true);
}
this.globalKeyDownListener = this.createGlobalKeyListener();
window.addEventListener("keydown", this.globalKeyDownListener, true);
},
handlePanelInteractionOutsideProxy(event) {
if (
HelpPanelManager.helpPanelElement &&
HelpPanelManager.helpPanelElement.classList.contains(
Config.CSS_CLASSES.HELP_PANEL_VISIBLE
) &&
!HelpPanelManager.helpPanelElement.contains(event.target) &&
parseFloat(
getComputedStyle(HelpPanelManager.helpPanelElement).opacity
) === 1
) {
if (State.isCustomizingShortcuts) {
const settingsButton =
HelpPanelManager.helpPanelElement.querySelector(
`.${Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
);
if (settingsButton)
HelpPanelManager.toggleCustomizationModeUI(settingsButton);
}
HelpPanelManager.closePanel();
}
},
handlePanelEscapeKeyProxy(event) {
if (event.key === "Escape") {
if (
HelpPanelManager.helpPanelElement &&
HelpPanelManager.helpPanelElement.classList.contains(
Config.CSS_CLASSES.HELP_PANEL_VISIBLE
) &&
parseFloat(
getComputedStyle(HelpPanelManager.helpPanelElement).opacity
) === 1
) {
event.preventDefault();
event.stopPropagation();
if (State.activeCustomizationTarget) {
HelpPanelManager.setListeningStateUI(
State.activeCustomizationTarget.shortcutId,
State.activeCustomizationTarget.spanElement,
false
);
} else if (State.isCustomizingShortcuts) {
const settingsButton =
HelpPanelManager.helpPanelElement.querySelector(
`.${Config.CSS_CLASSES.HELP_PANEL_KEY_SETTING}`
);
if (settingsButton)
HelpPanelManager.toggleCustomizationModeUI(settingsButton);
}
HelpPanelManager.closePanel();
}
}
},
};
const ScriptManager = {
init() {
try {
State.initialize();
UserInterface.injectStyles();
EventManager.init();
} catch (error) {}
},
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => ScriptManager.init(), {
once: true,
});
} else {
ScriptManager.init();
}
})();