├── README.md ├── index.js ├── manifest.json ├── settings.html ├── style.css └── toolbar.html /README.md: -------------------------------------------------------------------------------- 1 | # SillyTavern 输入助手 2 | 3 | *这是一个简单的SillyTavern输入助手插件,用于提高文本输入效率。* 4 | 5 | ## 功能 6 | 7 | * **插入特殊符号**:一键在输入框插入双引号、星号等成对符号,光标自动定位在中间 8 | * **快速换行**:一键移动到当前行末尾并插入换行符,提高排版效率 9 | * **插入标记**:快速插入 `{{User}}` 和 `{{Char}}` 标记,用于引用用户和角色名称 10 | * **自定义工具栏**:可以自由选择需要显示的按钮,定制个性化工具栏 11 | * **快捷键支持**:为每个功能设置键盘快捷键,提高操作效率 12 | * **按钮排序**:通过拖拽调整按钮显示顺序,使用更灵活 13 | * **自定义符号**:添加自己需要的特殊符号和光标位置 14 | 15 | ## 安装和使用 16 | 17 | ### 安装 18 | 19 | 1. 打开SillyTavern 20 | 2. 进入设置页面 21 | 3. 点击"扩展"选项卡 22 | 4. 使用扩展安装器,输入 `https://github.com/Mooooooon/st-input-helper` 进行安装 23 | 24 | ### 使用 25 | 26 | 1. 安装完成后,在设置页面的"扩展"选项卡中找到"输入助手"面板 27 | 2. 勾选"启用输入助手"选项,启用工具栏 28 | 3. 可以选择需要显示的按钮,取消勾选不需要的按钮 29 | 4. 可以拖动按钮左侧的排序图标调整按钮在工具栏中的显示顺序 30 | 5. 可以为每个功能设置快捷键,点击对应的输入框并按下快捷键组合 31 | 6. 可以添加自定义符号,点击"添加自定义符号"按钮并填写相关信息 32 | 33 | ### 自定义符号设置 34 | 35 | 1. 点击"添加自定义符号"按钮 36 | 2. 在弹出的对话框中填写: 37 | - 名称:按钮的悬停提示文本 38 | - 符号:插入到文本中的实际符号 39 | - 显示文本:按钮上显示的文本(默认与符号相同) 40 | - 光标位置:插入符号后光标的位置(开始、中间、结尾或自定义位置) 41 | 3. 点击"保存"即可创建新的按钮 42 | 4. 可以编辑或删除已创建的自定义符号 43 | 5. 也可以为自定义符号设置快捷键,操作方式与内置按钮相同 44 | 45 | ### 功能列表 46 | 47 | - 插入双星号 (**): 用于强调文本,光标置于中间 48 | - 插入双引号 (""): 用于引用文本,光标置于中间 49 | - 插入圆括号 (()): 用于分组信息,光标置于中间 50 | - 插入直角引号「」: 用于引用文本,光标置于中间 51 | - 插入直角引号『』: 用于引用文本,光标置于中间 52 | - 插入书名号《》: 用于书名、作品名,光标置于中间 53 | - 插入换行 (⏎): 移动到当前行末尾并插入换行符 54 | - 插入用户标记 ({{U}}): 插入 `{{User}}` 变量 55 | - 插入角色标记 ({{C}}): 插入 `{{Char}}` 变量 56 | - 自定义符号: 用户自行添加的符号 57 | 58 | ## 快捷键设置 59 | 60 | 1. 在设置面板中找到"快捷键设置"部分 61 | 2. 点击要设置的功能对应的输入框 62 | 3. 按下所需的键盘组合(例如:Ctrl+Alt+Q) 63 | 4. 设置会自动保存 64 | 5. 按ESC或点击"清除"按钮可以移除快捷键 65 | 66 | 注意:快捷键仅在聊天输入框获得焦点时有效。 67 | 68 | ## 兼容性 69 | 70 | * 需要SillyTavern v1.9.0或更高版本 71 | 72 | ## 支持和贡献 73 | 74 | 如有问题或建议,请在GitHub仓库提交issue或联系作者。 75 | 76 | 欢迎提交Pull Request来改进这个插件。 77 | 78 | ## 许可证 79 | 80 | MIT License 81 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // The main script for the extension 2 | // The following are examples of some basic extension functionality 3 | 4 | //You'll likely need to import extension_settings, getContext, and loadExtensionSettings from extensions.js 5 | import { extension_settings, getContext, loadExtensionSettings } from "../../../extensions.js"; 6 | 7 | //You'll likely need to import some other functions from the main script 8 | import { saveSettingsDebounced } from "../../../../script.js"; 9 | 10 | // 设置插件名称和路径 11 | const extensionName = "st-input-helper"; 12 | const extensionFolderPath = `scripts/extensions/third-party/${extensionName}`; 13 | const defaultSettings = { 14 | enabled: true, 15 | buttons: { 16 | asterisk: true, 17 | quotes: true, 18 | parentheses: true, 19 | bookQuotes1: true, 20 | bookQuotes2: true, 21 | bookQuotes3: true, // 新增《》按钮设置 22 | newline: true, 23 | user: true, 24 | char: true 25 | }, 26 | shortcuts: { 27 | asterisk: "", 28 | quotes: "", 29 | parentheses: "", 30 | bookQuotes1: "", 31 | bookQuotes2: "", 32 | bookQuotes3: "", 33 | newline: "", 34 | user: "", 35 | char: "" 36 | }, 37 | // 添加默认的按钮顺序 38 | buttonOrder: [ 39 | 'asterisk', 40 | 'quotes', 41 | 'parentheses', 42 | 'bookQuotes1', 43 | 'bookQuotes2', 44 | 'bookQuotes3', 45 | 'newline', 46 | 'user', 47 | 'char' 48 | ], 49 | // 添加自定义符号设置 50 | customSymbols: [] 51 | }; 52 | 53 | // 快捷键映射表 54 | const shortcutFunctionMap = { 55 | 'asterisk': insertAsterisk, 56 | 'quotes': insertQuotes, 57 | 'parentheses': insertParentheses, 58 | 'bookQuotes1': insertBookQuotes1, 59 | 'bookQuotes2': insertBookQuotes2, 60 | 'bookQuotes3': insertBookQuotes3, 61 | 'newline': insertNewLine, 62 | 'user': insertUserTag, 63 | 'char': insertCharTag 64 | }; 65 | 66 | // 加载插件设置 67 | async function loadSettings() { 68 | extension_settings[extensionName] = extension_settings[extensionName] || {}; 69 | if (Object.keys(extension_settings[extensionName]).length === 0) { 70 | Object.assign(extension_settings[extensionName], defaultSettings); 71 | } 72 | 73 | // 兼容旧版本设置 74 | if (!extension_settings[extensionName].buttons) { 75 | extension_settings[extensionName].buttons = defaultSettings.buttons; 76 | } 77 | 78 | // 兼容旧版本设置 - 快捷键 79 | if (!extension_settings[extensionName].shortcuts) { 80 | extension_settings[extensionName].shortcuts = defaultSettings.shortcuts; 81 | } 82 | 83 | // 兼容旧版本设置 - 按钮顺序 84 | if (!extension_settings[extensionName].buttonOrder) { 85 | extension_settings[extensionName].buttonOrder = defaultSettings.buttonOrder; 86 | } 87 | 88 | // 兼容旧版本设置 - 自定义符号 89 | if (!extension_settings[extensionName].customSymbols) { 90 | extension_settings[extensionName].customSymbols = []; 91 | } 92 | 93 | // 更新UI中的设置 94 | $("#enable_input_helper").prop("checked", extension_settings[extensionName].enabled); 95 | 96 | // 更新按钮显示设置 97 | const buttons = extension_settings[extensionName].buttons; 98 | $("#enable_asterisk_btn").prop("checked", buttons.asterisk !== false); 99 | $("#enable_quotes_btn").prop("checked", buttons.quotes !== false); 100 | $("#enable_parentheses_btn").prop("checked", buttons.parentheses !== false); 101 | $("#enable_book_quotes1_btn").prop("checked", buttons.bookQuotes1 !== false); 102 | $("#enable_book_quotes2_btn").prop("checked", buttons.bookQuotes2 !== false); 103 | $("#enable_book_quotes3_btn").prop("checked", buttons.bookQuotes3 !== false); // 新增书名号按钮设置 104 | $("#enable_newline_btn").prop("checked", buttons.newline !== false); 105 | $("#enable_user_btn").prop("checked", buttons.user !== false); 106 | $("#enable_char_btn").prop("checked", buttons.char !== false); 107 | 108 | // 更新快捷键设置 109 | const shortcuts = extension_settings[extensionName].shortcuts; 110 | for (const key in shortcuts) { 111 | $(`#shortcut_${key}`).val(shortcuts[key] || ""); 112 | } 113 | 114 | // 更新按钮顺序 115 | updateButtonsOrder(); 116 | 117 | updateButtonVisibility(); 118 | 119 | // 加载自定义符号按钮 120 | loadCustomSymbolButtons(); 121 | } 122 | 123 | // 更新设置面板中的按钮顺序 124 | function updateButtonsOrder() { 125 | const buttonOrder = extension_settings[extensionName].buttonOrder; 126 | if (!buttonOrder || buttonOrder.length === 0) return; 127 | 128 | // 根据保存的顺序重新排列设置面板中的按钮 129 | const container = $("#integrated_button_settings"); 130 | 131 | buttonOrder.forEach(key => { 132 | const buttonRow = $(`.integrated-button-row[data-button-key="${key}"]`); 133 | if (buttonRow.length) { 134 | container.append(buttonRow); 135 | } 136 | }); 137 | } 138 | 139 | // 初始化按钮排序 140 | function initSortable() { 141 | try { 142 | if ($("#integrated_button_settings").sortable) { 143 | $("#integrated_button_settings").sortable({ 144 | handle: ".drag-handle", 145 | axis: "y", 146 | delay: 150, 147 | stop: function() { 148 | // 获取新的排序 149 | const newOrder = []; 150 | $("#integrated_button_settings .integrated-button-row").each(function() { 151 | const buttonKey = $(this).attr("data-button-key"); 152 | newOrder.push(buttonKey); 153 | }); 154 | 155 | // 保存新排序到设置 156 | extension_settings[extensionName].buttonOrder = newOrder; 157 | saveSettingsDebounced(); 158 | 159 | // 更新工具栏按钮顺序 160 | updateToolbarButtonOrder(); 161 | } 162 | }); 163 | } else { 164 | console.warn("jQuery UI Sortable 不可用,无法启用拖拽排序功能"); 165 | } 166 | } catch (error) { 167 | console.error("初始化按钮排序功能失败:", error); 168 | } 169 | } 170 | 171 | // 更新工具栏按钮顺序 172 | function updateToolbarButtonOrder() { 173 | const buttonOrder = extension_settings[extensionName].buttonOrder || []; 174 | if (buttonOrder.length === 0) return; 175 | 176 | const toolbar = $("#input_helper_toolbar"); 177 | if (toolbar.length === 0) return; 178 | 179 | // 按照保存的顺序重新排列工具栏按钮 180 | buttonOrder.forEach(key => { 181 | // 防止空按钮ID 182 | const buttonId = getButtonIdFromKey(key); 183 | if (!buttonId) return; 184 | 185 | const button = $(`#${buttonId}`); 186 | if (button.length && extension_settings[extensionName].buttons[key] !== false) { 187 | toolbar.append(button); 188 | } 189 | }); 190 | } 191 | 192 | // 从按钮键名获取按钮ID 193 | function getButtonIdFromKey(key) { 194 | // 检查是否是自定义按钮 195 | if (key.startsWith('custom_')) { 196 | // 直接返回自定义按钮的ID 197 | const index = key.replace('custom_', ''); 198 | return `input_custom_${index}_btn`; 199 | } 200 | 201 | // 预定义按钮的映射 202 | const keyToId = { 203 | 'asterisk': 'input_asterisk_btn', 204 | 'quotes': 'input_quotes_btn', 205 | 'parentheses': 'input_parentheses_btn', 206 | 'bookQuotes1': 'input_book_quotes1_btn', 207 | 'bookQuotes2': 'input_book_quotes2_btn', 208 | 'bookQuotes3': 'input_book_quotes3_btn', 209 | 'newline': 'input_newline_btn', 210 | 'user': 'input_user_btn', 211 | 'char': 'input_char_btn' 212 | }; 213 | 214 | return keyToId[key] || ''; 215 | } 216 | 217 | // 更新按钮可见性 218 | function updateButtonVisibility() { 219 | const buttons = extension_settings[extensionName].buttons; 220 | 221 | // 根据设置显示/隐藏按钮 222 | $("#input_asterisk_btn").toggle(buttons.asterisk !== false); 223 | $("#input_quotes_btn").toggle(buttons.quotes !== false); 224 | $("#input_parentheses_btn").toggle(buttons.parentheses !== false); 225 | $("#input_book_quotes1_btn").toggle(buttons.bookQuotes1 !== false); 226 | $("#input_book_quotes2_btn").toggle(buttons.bookQuotes2 !== false); 227 | $("#input_book_quotes3_btn").toggle(buttons.bookQuotes3 !== false); // 新增书名号按钮 228 | $("#input_newline_btn").toggle(buttons.newline !== false); 229 | $("#input_user_btn").toggle(buttons.user !== false); 230 | $("#input_char_btn").toggle(buttons.char !== false); 231 | 232 | // 更新自定义按钮的显示/隐藏 233 | const customSymbols = extension_settings[extensionName].customSymbols || []; 234 | customSymbols.forEach((symbol, index) => { 235 | const buttonKey = `custom_${index}`; 236 | $(`#input_custom_${index}_btn`).toggle(buttons[buttonKey] !== false); 237 | }); 238 | 239 | // 检查所有按钮是否都被隐藏,如果是则隐藏整个工具栏 240 | const allHidden = Object.values(buttons).every(v => v === false); 241 | if (allHidden) { 242 | $("#input_helper_toolbar").hide(); 243 | } else if (extension_settings[extensionName].enabled) { 244 | $("#input_helper_toolbar").show(); 245 | 246 | // 更新按钮顺序 247 | updateToolbarButtonOrder(); 248 | } 249 | } 250 | 251 | // 开关设置变更响应 252 | function onEnableInputChange() { 253 | const value = $("#enable_input_helper").prop("checked"); 254 | extension_settings[extensionName].enabled = value; 255 | saveSettingsDebounced(); 256 | 257 | // 根据复选框状态显示或隐藏工具栏 258 | if (value) { 259 | updateButtonVisibility(); 260 | } else { 261 | $("#input_helper_toolbar").hide(); 262 | } 263 | } 264 | 265 | // 按钮显示设置变更响应 266 | function onButtonVisibilityChange(buttonKey) { 267 | return function() { 268 | const checked = $(this).prop("checked"); 269 | extension_settings[extensionName].buttons[buttonKey] = checked; 270 | saveSettingsDebounced(); 271 | updateButtonVisibility(); 272 | }; 273 | } 274 | 275 | // 获取输入框元素 276 | function getMessageInput() { 277 | return $("#send_textarea, #prompt_textarea").first(); 278 | } 279 | 280 | // 插入引号功能 281 | function insertQuotes() { 282 | if (!extension_settings[extensionName].enabled) return; 283 | 284 | const textarea = getMessageInput(); 285 | const startPos = textarea.prop("selectionStart"); 286 | const endPos = textarea.prop("selectionEnd"); 287 | const text = textarea.val(); 288 | 289 | const beforeText = text.substring(0, startPos); 290 | const selectedText = text.substring(startPos, endPos); 291 | const afterText = text.substring(endPos); 292 | 293 | // 插入双引号并将光标放在中间 294 | const newText = beforeText + "\"\"" + afterText; 295 | textarea.val(newText); 296 | 297 | // 设置光标位置在双引号中间 298 | setTimeout(() => { 299 | textarea.prop("selectionStart", startPos + 1); 300 | textarea.prop("selectionEnd", startPos + 1); 301 | textarea.focus(); 302 | }, 0); 303 | } 304 | 305 | // 插入换行功能 306 | function insertNewLine() { 307 | if (!extension_settings[extensionName].enabled) return; 308 | 309 | const textarea = getMessageInput(); 310 | const text = textarea.val(); 311 | const cursorPos = textarea.prop("selectionStart"); 312 | 313 | // 查找当前行的末尾位置 314 | let lineEnd = text.indexOf("\n", cursorPos); 315 | if (lineEnd === -1) { 316 | // 如果没有找到换行符,说明光标在最后一行,使用文本长度作为行末 317 | lineEnd = text.length; 318 | } 319 | 320 | // 在行末插入换行符 321 | const newText = text.substring(0, lineEnd) + "\n" + text.substring(lineEnd); 322 | textarea.val(newText); 323 | 324 | // 设置光标位置在新插入的换行符之后 325 | setTimeout(() => { 326 | textarea.prop("selectionStart", lineEnd + 1); 327 | textarea.prop("selectionEnd", lineEnd + 1); 328 | textarea.focus(); 329 | }, 0); 330 | } 331 | 332 | // 插入星号功能 333 | function insertAsterisk() { 334 | if (!extension_settings[extensionName].enabled) return; 335 | 336 | const textarea = getMessageInput(); 337 | const startPos = textarea.prop("selectionStart"); 338 | const endPos = textarea.prop("selectionEnd"); 339 | const text = textarea.val(); 340 | 341 | const beforeText = text.substring(0, startPos); 342 | const selectedText = text.substring(startPos, endPos); 343 | const afterText = text.substring(endPos); 344 | 345 | // 插入两个星号并将光标放在中间 346 | const newText = beforeText + "**" + afterText; 347 | textarea.val(newText); 348 | 349 | // 设置光标位置在星号中间 350 | setTimeout(() => { 351 | textarea.prop("selectionStart", startPos + 1); 352 | textarea.prop("selectionEnd", startPos + 1); 353 | textarea.focus(); 354 | }, 0); 355 | } 356 | 357 | // 插入用户标记功能 358 | function insertUserTag() { 359 | if (!extension_settings[extensionName].enabled) return; 360 | 361 | const textarea = getMessageInput(); 362 | const startPos = textarea.prop("selectionStart"); 363 | const endPos = textarea.prop("selectionEnd"); 364 | const text = textarea.val(); 365 | 366 | const beforeText = text.substring(0, startPos); 367 | const selectedText = text.substring(startPos, endPos); 368 | const afterText = text.substring(endPos); 369 | 370 | // 插入用户标记 371 | const newText = beforeText + "{{User}}" + afterText; 372 | textarea.val(newText); 373 | 374 | // 设置光标位置在标记之后 375 | setTimeout(() => { 376 | textarea.prop("selectionStart", startPos + 8); // "{{User}}".length = 8 377 | textarea.prop("selectionEnd", startPos + 8); 378 | textarea.focus(); 379 | }, 0); 380 | } 381 | 382 | // 插入角色标记功能 383 | function insertCharTag() { 384 | if (!extension_settings[extensionName].enabled) return; 385 | 386 | const textarea = getMessageInput(); 387 | const startPos = textarea.prop("selectionStart"); 388 | const endPos = textarea.prop("selectionEnd"); 389 | const text = textarea.val(); 390 | 391 | const beforeText = text.substring(0, startPos); 392 | const selectedText = text.substring(startPos, endPos); 393 | const afterText = text.substring(endPos); 394 | 395 | // 插入角色标记 396 | const newText = beforeText + "{{Char}}" + afterText; 397 | textarea.val(newText); 398 | 399 | // 设置光标位置在标记之后 400 | setTimeout(() => { 401 | textarea.prop("selectionStart", startPos + 8); // "{{Char}}".length = 8 402 | textarea.prop("selectionEnd", startPos + 8); 403 | textarea.focus(); 404 | }, 0); 405 | } 406 | 407 | // 插入圆括号功能 408 | function insertParentheses() { 409 | if (!extension_settings[extensionName].enabled) return; 410 | 411 | const textarea = getMessageInput(); 412 | const startPos = textarea.prop("selectionStart"); 413 | const endPos = textarea.prop("selectionEnd"); 414 | const text = textarea.val(); 415 | 416 | const beforeText = text.substring(0, startPos); 417 | const selectedText = text.substring(startPos, endPos); 418 | const afterText = text.substring(endPos); 419 | 420 | // 插入圆括号并将光标放在中间 421 | const newText = beforeText + "()" + afterText; 422 | textarea.val(newText); 423 | 424 | // 设置光标位置在括号中间 425 | setTimeout(() => { 426 | textarea.prop("selectionStart", startPos + 1); 427 | textarea.prop("selectionEnd", startPos + 1); 428 | textarea.focus(); 429 | }, 0); 430 | } 431 | 432 | // 插入书名号「」功能 433 | function insertBookQuotes1() { 434 | if (!extension_settings[extensionName].enabled) return; 435 | 436 | const textarea = getMessageInput(); 437 | const startPos = textarea.prop("selectionStart"); 438 | const endPos = textarea.prop("selectionEnd"); 439 | const text = textarea.val(); 440 | 441 | const beforeText = text.substring(0, startPos); 442 | const selectedText = text.substring(startPos, endPos); 443 | const afterText = text.substring(endPos); 444 | 445 | // 插入书名号并将光标放在中间 446 | const newText = beforeText + "「」" + afterText; 447 | textarea.val(newText); 448 | 449 | // 设置光标位置在书名号中间 450 | setTimeout(() => { 451 | textarea.prop("selectionStart", startPos + 1); 452 | textarea.prop("selectionEnd", startPos + 1); 453 | textarea.focus(); 454 | }, 0); 455 | } 456 | 457 | // 插入书名号『』功能 458 | function insertBookQuotes2() { 459 | if (!extension_settings[extensionName].enabled) return; 460 | 461 | const textarea = getMessageInput(); 462 | const startPos = textarea.prop("selectionStart"); 463 | const endPos = textarea.prop("selectionEnd"); 464 | const text = textarea.val(); 465 | 466 | const beforeText = text.substring(0, startPos); 467 | const selectedText = text.substring(startPos, endPos); 468 | const afterText = text.substring(endPos); 469 | 470 | // 插入书名号并将光标放在中间 471 | const newText = beforeText + "『』" + afterText; 472 | textarea.val(newText); 473 | 474 | // 设置光标位置在书名号中间 475 | setTimeout(() => { 476 | textarea.prop("selectionStart", startPos + 1); 477 | textarea.prop("selectionEnd", startPos + 1); 478 | textarea.focus(); 479 | }, 0); 480 | } 481 | 482 | // 插入书名号《》功能 483 | function insertBookQuotes3() { 484 | if (!extension_settings[extensionName].enabled) return; 485 | 486 | const textarea = getMessageInput(); 487 | const startPos = textarea.prop("selectionStart"); 488 | const endPos = textarea.prop("selectionEnd"); 489 | const text = textarea.val(); 490 | 491 | const beforeText = text.substring(0, startPos); 492 | const selectedText = text.substring(startPos, endPos); 493 | const afterText = text.substring(endPos); 494 | 495 | // 插入书名号并将光标放在中间 496 | const newText = beforeText + "《》" + afterText; 497 | textarea.val(newText); 498 | 499 | // 设置光标位置在书名号中间 500 | setTimeout(() => { 501 | textarea.prop("selectionStart", startPos + 1); 502 | textarea.prop("selectionEnd", startPos + 1); 503 | textarea.focus(); 504 | }, 0); 505 | } 506 | 507 | // 处理快捷键设置 508 | function setupShortcutInputs() { 509 | // 处理快捷键输入 510 | $(".shortcut-input").on("keydown", function(e) { 511 | e.preventDefault(); 512 | 513 | // 获取按键组合 514 | let keys = []; 515 | if (e.ctrlKey) keys.push("Ctrl"); 516 | if (e.altKey) keys.push("Alt"); 517 | if (e.shiftKey) keys.push("Shift"); 518 | 519 | // 添加主键(如果不是修饰键) 520 | if ( 521 | e.key !== "Control" && 522 | e.key !== "Alt" && 523 | e.key !== "Shift" && 524 | e.key !== "Meta" && 525 | e.key !== "Escape" 526 | ) { 527 | // 修复: 确保e.key存在并且有length属性 528 | const keyName = e.key && typeof e.key === 'string' && e.key.length === 1 529 | ? e.key.toUpperCase() 530 | : (e.key || "Unknown"); 531 | keys.push(keyName); 532 | } 533 | 534 | // 如果只按了Escape键,清除快捷键 535 | if (e.key === "Escape") { 536 | $(this).val(""); 537 | const shortcutKey = $(this).attr("id").replace("shortcut_", ""); 538 | extension_settings[extensionName].shortcuts[shortcutKey] = ""; 539 | saveSettingsDebounced(); 540 | return; 541 | } 542 | 543 | // 如果没有按键组合或只有修饰键,不设置 544 | if (keys.length === 0 || (keys.length === 1 && ["Ctrl", "Alt", "Shift"].includes(keys[0]))) { 545 | return; 546 | } 547 | 548 | // 设置快捷键 549 | const shortcutString = keys.join("+"); 550 | $(this).val(shortcutString); 551 | 552 | // 保存到设置 553 | const shortcutKey = $(this).attr("id").replace("shortcut_", ""); 554 | extension_settings[extensionName].shortcuts[shortcutKey] = shortcutString; 555 | saveSettingsDebounced(); 556 | }); 557 | 558 | // 处理清除按钮 559 | $(".shortcut-clear-btn").on("click", function() { 560 | const targetId = $(this).data("target"); 561 | $(`#${targetId}`).val(""); 562 | 563 | // 保存到设置 564 | const shortcutKey = targetId.replace("shortcut_", ""); 565 | extension_settings[extensionName].shortcuts[shortcutKey] = ""; 566 | saveSettingsDebounced(); 567 | }); 568 | } 569 | 570 | // 全局快捷键处理函数 571 | function handleGlobalShortcuts(e) { 572 | // 如果插件未启用或正在编辑快捷键,不处理 573 | if (!extension_settings[extensionName].enabled || $(document.activeElement).hasClass("shortcut-input")) { 574 | return; 575 | } 576 | 577 | // 如果当前焦点不在文本区域,不处理 578 | const messageInput = getMessageInput()[0]; 579 | if (document.activeElement !== messageInput) { 580 | return; 581 | } 582 | 583 | // 获取当前按键组合 584 | let keys = []; 585 | if (e.ctrlKey) keys.push("Ctrl"); 586 | if (e.altKey) keys.push("Alt"); 587 | if (e.shiftKey) keys.push("Shift"); 588 | 589 | // 添加主键(如果不是修饰键) 590 | if ( 591 | e.key !== "Control" && 592 | e.key !== "Alt" && 593 | e.key !== "Shift" && 594 | e.key !== "Meta" 595 | ) { 596 | // 修复: 确保e.key存在并且有length属性 597 | const keyName = e.key && typeof e.key === 'string' && e.key.length === 1 598 | ? e.key.toUpperCase() 599 | : (e.key || "Unknown"); 600 | keys.push(keyName); 601 | } 602 | 603 | // 如果没有有效的按键组合,不处理 604 | if (keys.length <= 1) { 605 | return; 606 | } 607 | 608 | const shortcutString = keys.join("+"); 609 | const shortcuts = extension_settings[extensionName].shortcuts; 610 | 611 | // 查找匹配的快捷键 612 | for (const key in shortcuts) { 613 | if (shortcuts[key] === shortcutString) { 614 | e.preventDefault(); 615 | 616 | // 检查是否是自定义按钮的快捷键 617 | if (key.startsWith('custom_')) { 618 | const index = parseInt(key.replace('custom_', '')); 619 | const customSymbols = extension_settings[extensionName].customSymbols || []; 620 | if (index >= 0 && index < customSymbols.length) { 621 | insertCustomSymbol(customSymbols[index]); 622 | return; 623 | } 624 | } 625 | // 执行对应的功能 626 | else if (shortcutFunctionMap[key]) { 627 | shortcutFunctionMap[key](); 628 | return; 629 | } 630 | } 631 | } 632 | } 633 | 634 | // 加载自定义符号按钮 635 | function loadCustomSymbolButtons() { 636 | const customSymbols = extension_settings[extensionName].customSymbols || []; 637 | 638 | // 清除现有的自定义按钮 639 | $(".custom-symbol-button").remove(); 640 | $(".integrated-button-row[data-custom='true']").remove(); 641 | 642 | // 为每个自定义符号创建按钮和设置项 643 | customSymbols.forEach((symbol, index) => { 644 | const buttonKey = `custom_${index}`; 645 | 646 | // 为工具栏创建按钮 647 | createCustomSymbolButton(symbol, index); 648 | 649 | // 为设置面板创建行 650 | createCustomSymbolSetting(symbol, index); 651 | 652 | // 更新按钮顺序 653 | if (!extension_settings[extensionName].buttonOrder.includes(buttonKey)) { 654 | extension_settings[extensionName].buttonOrder.push(buttonKey); 655 | } 656 | 657 | // 确保该按钮有显示设置 658 | if (extension_settings[extensionName].buttons[buttonKey] === undefined) { 659 | extension_settings[extensionName].buttons[buttonKey] = true; 660 | } 661 | 662 | // 确保该按钮有快捷键设置 663 | if (extension_settings[extensionName].shortcuts[buttonKey] === undefined) { 664 | extension_settings[extensionName].shortcuts[buttonKey] = ""; 665 | } 666 | 667 | // 更新快捷键映射 668 | shortcutFunctionMap[buttonKey] = function() { 669 | insertCustomSymbol(customSymbols[index]); 670 | }; 671 | }); 672 | 673 | // 更新按钮顺序 674 | updateButtonsOrder(); 675 | updateToolbarButtonOrder(); 676 | 677 | // 重新绑定快捷键输入框事件 678 | setupShortcutInputs(); 679 | } 680 | 681 | // 创建自定义符号按钮 682 | function createCustomSymbolButton(symbol, index) { 683 | const buttonId = `input_custom_${index}_btn`; 684 | const buttonKey = `custom_${index}`; 685 | 686 | // 先检查是否已存在,如果存在则移除 687 | $(`#${buttonId}`).remove(); 688 | 689 | // 创建按钮并添加到工具栏 690 | const button = $(``); 691 | $("#input_helper_toolbar").append(button); 692 | 693 | // 添加点击事件 694 | bindCustomSymbolEvent(button, symbol); 695 | } 696 | 697 | // 为自定义符号按钮绑定事件 698 | function bindCustomSymbolEvent(button, symbol) { 699 | // 检查是否是移动设备 700 | const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 701 | 702 | if (isMobile) { 703 | button.on("touchstart", function(e) { 704 | e.preventDefault(); 705 | insertCustomSymbol(symbol); 706 | 707 | // 确保输入框保持焦点状态 708 | setTimeout(() => { 709 | getMessageInput().focus(); 710 | }, 10); 711 | 712 | return false; 713 | }); 714 | } else { 715 | button.on("click", function() { 716 | insertCustomSymbol(symbol); 717 | }); 718 | } 719 | } 720 | 721 | // 创建自定义符号设置项 722 | function createCustomSymbolSetting(symbol, index) { 723 | const buttonKey = `custom_${index}`; 724 | 725 | // 先检查是否已存在,如果存在则移除 726 | $(`.integrated-button-row[data-button-key="${buttonKey}"]`).remove(); 727 | 728 | // 创建设置行 - 修改编辑和删除按钮位置 729 | const row = $(` 730 |
740 | `); 741 | 742 | // 添加到设置面板 743 | $("#integrated_button_settings").append(row); 744 | 745 | // 添加事件监听 746 | row.find(`#enable_${buttonKey}_btn`).on("input", onButtonVisibilityChange(buttonKey)); 747 | row.find(".custom-edit-btn").on("click", function() { 748 | const index = $(this).data("index"); 749 | editCustomSymbol(index); 750 | }); 751 | row.find(".custom-delete-btn").on("click", function() { 752 | const index = $(this).data("index"); 753 | deleteCustomSymbol(index); 754 | }); 755 | } 756 | 757 | // 插入自定义符号 758 | function insertCustomSymbol(symbol) { 759 | if (!extension_settings[extensionName].enabled) return; 760 | 761 | const textarea = getMessageInput(); 762 | const startPos = textarea.prop("selectionStart"); 763 | const endPos = textarea.prop("selectionEnd"); 764 | const text = textarea.val(); 765 | 766 | const beforeText = text.substring(0, startPos); 767 | const selectedText = text.substring(startPos, endPos); 768 | const afterText = text.substring(endPos); 769 | 770 | // 插入符号 771 | const newText = beforeText + symbol.symbol + afterText; 772 | textarea.val(newText); 773 | 774 | // 设置光标位置 775 | setTimeout(() => { 776 | // 计算光标位置 777 | let cursorPos = startPos; 778 | 779 | if (symbol.cursorPos === "start") { 780 | cursorPos = startPos; 781 | } else if (symbol.cursorPos === "end") { 782 | cursorPos = startPos + symbol.symbol.length; 783 | } else if (symbol.cursorPos === "middle") { 784 | cursorPos = startPos + Math.floor(symbol.symbol.length / 2); 785 | } else { 786 | // 具体位置 787 | cursorPos = startPos + parseInt(symbol.cursorPos) || startPos; 788 | } 789 | 790 | textarea.prop("selectionStart", cursorPos); 791 | textarea.prop("selectionEnd", cursorPos); 792 | textarea.focus(); 793 | }, 0); 794 | } 795 | 796 | // 编辑自定义符号 797 | function editCustomSymbol(index) { 798 | const symbols = extension_settings[extensionName].customSymbols; 799 | const symbol = symbols[index]; 800 | 801 | // 显示编辑对话框 802 | showCustomSymbolDialog(symbol, index); 803 | } 804 | 805 | // 删除自定义符号 806 | function deleteCustomSymbol(index) { 807 | if (confirm("确定要删除这个自定义符号吗?")) { 808 | const symbols = extension_settings[extensionName].customSymbols; 809 | const buttonKey = `custom_${index}`; 810 | 811 | // 从设置中删除 812 | symbols.splice(index, 1); 813 | 814 | // 从按钮顺序中删除 815 | const orderIndex = extension_settings[extensionName].buttonOrder.indexOf(buttonKey); 816 | if (orderIndex > -1) { 817 | extension_settings[extensionName].buttonOrder.splice(orderIndex, 1); 818 | } 819 | 820 | // 从按钮显示设置中删除 821 | delete extension_settings[extensionName].buttons[buttonKey]; 822 | 823 | // 从按钮快捷键设置中删除 824 | delete extension_settings[extensionName].shortcuts[buttonKey]; 825 | 826 | // 从工具栏中删除 827 | $(`#input_custom_${index}_btn`).remove(); 828 | 829 | // 从快捷键映射中删除 830 | delete shortcutFunctionMap[buttonKey]; 831 | 832 | // 保存设置 833 | saveSettingsDebounced(); 834 | 835 | // 移动设备监听器需要重新绑定 836 | rebindMobileEventListeners(); 837 | 838 | // 重新加载自定义按钮 - 这会导致索引重排 839 | loadCustomSymbolButtons(); 840 | 841 | // 更新工具栏 842 | updateButtonVisibility(); 843 | } 844 | } 845 | 846 | // 重新绑定移动设备事件监听器 847 | function rebindMobileEventListeners() { 848 | if (!/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { 849 | return; // 非移动设备不需要重新绑定 850 | } 851 | 852 | // 移除之前的监听器 853 | $("#input_helper_toolbar button").off("touchstart"); 854 | 855 | // 重新绑定监听器 856 | $("#input_helper_toolbar button").on("touchstart", function(e) { 857 | e.preventDefault(); 858 | const btnId = $(this).attr("id"); 859 | 860 | // 基于按钮ID调用相应的函数 861 | if (btnId === "input_asterisk_btn") insertAsterisk(); 862 | else if (btnId === "input_quotes_btn") insertQuotes(); 863 | else if (btnId === "input_parentheses_btn") insertParentheses(); 864 | else if (btnId === "input_book_quotes1_btn") insertBookQuotes1(); 865 | else if (btnId === "input_book_quotes2_btn") insertBookQuotes2(); 866 | else if (btnId === "input_book_quotes3_btn") insertBookQuotes3(); 867 | else if (btnId === "input_newline_btn") insertNewLine(); 868 | else if (btnId === "input_user_btn") insertUserTag(); 869 | else if (btnId === "input_char_btn") insertCharTag(); 870 | else if (btnId.startsWith("input_custom_")) { 871 | // 处理自定义按钮 872 | const index = parseInt(btnId.replace("input_custom_", "").replace("_btn", "")); 873 | const customSymbols = extension_settings[extensionName].customSymbols || []; 874 | if (index >= 0 && index < customSymbols.length) { 875 | insertCustomSymbol(customSymbols[index]); 876 | } 877 | } 878 | 879 | // 确保输入框保持焦点状态 880 | setTimeout(() => { 881 | getMessageInput().focus(); 882 | }, 10); 883 | 884 | return false; 885 | }); 886 | } 887 | 888 | // 显示自定义符号对话框 889 | function showCustomSymbolDialog(existingSymbol = null, editIndex = -1) { 890 | // 创建对话框 - 修改样式以正确应用主题颜色 891 | const dialog = $(` 892 |