Preset Regex: refactor regex scope detection (#4618) * refactor: explicit type for regex scope + feat: bulk move regex * fix: eslint * i18n: polish * refactor: jsdoc + use object type for option * fix: default return type for `getScriptsByType` * refactor: simplify * refactor: share default options * refactor: remove unrequired css * fix: move regexes to global + refactor: make regex operation interfaces more consistant --------- Co-authored-by: ZoinkCN <zoinkcn@outlook.com>
Signed| @@ -1677,9 +1677,9 @@ | |||
| 1677 | "ext_regex_global_scripts_desc": "影响所有角色,保存在本地设定中", | 1677 | "ext_regex_global_scripts_desc": "影响所有角色,保存在本地设定中", |
| 1678 | "No scripts found": "没有找到脚本", | 1678 | "No scripts found": "没有找到脚本", |
| 1679 | "ext_regex_scoped_scripts": "局部正则脚本", | 1679 | "ext_regex_scoped_scripts": "局部正则脚本", |
| 1680 | "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中", | ||
| 1680 | "ext_regex_disallow_scoped": "不允许使用局部正则", | 1681 | "ext_regex_disallow_scoped": "不允许使用局部正则", |
| 1681 | "ext_regex_allow_scoped": "允许使用局部正则", | 1682 | "ext_regex_allow_scoped": "允许使用局部正则", |
| 1682 | "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中", | ||
| 1683 | "Regex Editor": "正则表达式编辑器", | 1683 | "Regex Editor": "正则表达式编辑器", |
| 1684 | "Test Mode": "测试模式", | 1684 | "Test Mode": "测试模式", |
| 1685 | "ext_regex_desc": "“正则”是一个使用“正则表达式”来查找/替换字符串的工具。如果您想了解更多信息,请点击标题旁边的“?”。", | 1685 | "ext_regex_desc": "“正则”是一个使用“正则表达式”来查找/替换字符串的工具。如果您想了解更多信息,请点击标题旁边的“?”。", |
| @@ -1728,8 +1728,8 @@ | |||
| 1728 | "ext_regex_disable_script": "禁用脚本", | 1728 | "ext_regex_disable_script": "禁用脚本", |
| 1729 | "ext_regex_enable_script": "启用脚本", | 1729 | "ext_regex_enable_script": "启用脚本", |
| 1730 | "ext_regex_edit_script": "编辑脚本", | 1730 | "ext_regex_edit_script": "编辑脚本", |
| 1731 | "ext_regex_move_to_global": "移至全局脚本", | 1731 | "ext_regex_move_to_global": "移至全局", |
| 1732 | "ext_regex_move_to_scoped": "移至作用域脚本", | 1732 | "ext_regex_move_to_scoped": "移至局部", |
| 1733 | "ext_regex_export_script": "导出脚本", | 1733 | "ext_regex_export_script": "导出脚本", |
| 1734 | "ext_regex_delete_script": "删除脚本", | 1734 | "ext_regex_delete_script": "删除脚本", |
| 1735 | "Trigger Stable Diffusion": "触发Stable Diffusion", | 1735 | "Trigger Stable Diffusion": "触发Stable Diffusion", |
| @@ -31,7 +31,8 @@ | |||
| 31 | <small data-i18n="ext_regex_debugger">Debugger</small> | 31 | <small data-i18n="ext_regex_debugger">Debugger</small> |
| 32 | </div> | 32 | </div> |
| 33 | </div> | 33 | </div> |
| 34 | <div class="regex_bulk_operations flex-container justifyCenter"> | 34 | <hr class="regex_bulk_operations_hr" /> |
| 35 | <div class="regex_bulk_operations flex-container"> | ||
| 35 | <div id="bulk_select_all_toggle" class="menu_button menu_button_icon" title="Toggle Select All"> | 36 | <div id="bulk_select_all_toggle" class="menu_button menu_button_icon" title="Toggle Select All"> |
| 36 | <i class="fa-solid fa-check-double"></i> | 37 | <i class="fa-solid fa-check-double"></i> |
| 37 | </div> | 38 | </div> |
| @@ -43,6 +44,14 @@ | |||
| 43 | <i class="fa-solid fa-toggle-off"></i> | 44 | <i class="fa-solid fa-toggle-off"></i> |
| 44 | <small data-i18n="Disable">Disable</small> | 45 | <small data-i18n="Disable">Disable</small> |
| 45 | </div> | 46 | </div> |
| 47 | <div id="bulk_regex_move_to_global" class="menu_button menu_button_icon" hidden> | ||
| 48 | <i class="fa-solid fa-globe"></i> | ||
| 49 | <small data-i18n="ext_regex_move_to_global">Move to global scripts</small> | ||
| 50 | </div> | ||
| 51 | <div id="bulk_regex_move_to_scoped" class="menu_button menu_button_icon" hidden> | ||
| 52 | <i class="fa-solid fa-address-card"></i> | ||
| 53 | <small data-i18n="ext_regex_move_to_scoped">Move to scoped scripts</small> | ||
| 54 | </div> | ||
| 46 | <div id="bulk_export_regex" class="menu_button menu_button_icon"> | 55 | <div id="bulk_export_regex" class="menu_button menu_button_icon"> |
| 47 | <i class="fa-solid fa-file-export"></i> | 56 | <i class="fa-solid fa-file-export"></i> |
| 48 | <small data-i18n="Export">Export</small> | 57 | <small data-i18n="Export">Export</small> |
| @@ -8,6 +8,56 @@ export { | |||
| 8 | }; | 8 | }; |
| 9 | 9 | ||
| 10 | /** | 10 | /** |
| 11 | * @enum {number} Regex scripts types | ||
| 12 | */ | ||
| 13 | export const SCRIPT_TYPES = { | ||
| 14 | GLOBAL: 0, | ||
| 15 | SCOPED: 1, | ||
| 16 | }; | ||
| 17 | |||
| 18 | /** | ||
| 19 | * @typedef {import('../../char-data.js').RegexScriptData} RegexScript | ||
| 20 | */ | ||
| 21 | |||
| 22 | /** | ||
| 23 | * @typedef {object} GetRegexScriptsOptions | ||
| 24 | * @property {boolean} allowedOnly only return allowed scripts | ||
| 25 | */ | ||
| 26 | const DEFAULT_GET_REGEX_SCRIPTS_OPTIONS = { allowedOnly: false }; | ||
| 27 | |||
| 28 | /** | ||
| 29 | * Retrieves the list of regex scripts by combining the scripts from the extension settings and the character data | ||
| 30 | * | ||
| 31 | * @param {GetRegexScriptsOptions} option | ||
| 32 | * @returns {RegexScript[]} An array of regex scripts, where each script is an object containing the necessary information. | ||
| 33 | */ | ||
| 34 | export function getRegexScripts(option = DEFAULT_GET_REGEX_SCRIPTS_OPTIONS) { | ||
| 35 | return [...Object.values(SCRIPT_TYPES).flatMap(type => getScriptsByType(type, option))]; | ||
| 36 | } | ||
| 37 | |||
| 38 | /** | ||
| 39 | * Retrieves the regex scripts for a specific type. | ||
| 40 | * @param {SCRIPT_TYPES} scriptType | ||
| 41 | * @param {GetRegexScriptsOptions} option | ||
| 42 | * @returns {RegexScript[]} An array of regex scripts for the specified type. | ||
| 43 | */ | ||
| 44 | export function getScriptsByType(scriptType, { allowedOnly } = DEFAULT_GET_REGEX_SCRIPTS_OPTIONS) { | ||
| 45 | switch (scriptType) { | ||
| 46 | case SCRIPT_TYPES.GLOBAL: | ||
| 47 | return extension_settings.regex ?? []; | ||
| 48 | case SCRIPT_TYPES.SCOPED: { | ||
| 49 | if (allowedOnly && !extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar)) { | ||
| 50 | return []; | ||
| 51 | } | ||
| 52 | const scopedScripts = characters[this_chid]?.data?.extensions?.regex_scripts; | ||
| 53 | return Array.isArray(scopedScripts) ? scopedScripts : []; | ||
| 54 | } | ||
| 55 | default: | ||
| 56 | return []; | ||
| 57 | } | ||
| 58 | } | ||
| 59 | |||
| 60 | /** | ||
| 11 | * @enum {number} Where the regex script should be applied | 61 | * @enum {number} Where the regex script should be applied |
| 12 | */ | 62 | */ |
| 13 | const regex_placement = { | 63 | const regex_placement = { |
| @@ -51,22 +101,6 @@ function sanitizeRegexMacro(x) { | |||
| 51 | }) : x; | 101 | }) : x; |
| 52 | } | 102 | } |
| 53 | 103 | ||
| 54 | function getScopedRegex() { | ||
| 55 | const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar); | ||
| 56 | |||
| 57 | if (!isAllowed) { | ||
| 58 | return []; | ||
| 59 | } | ||
| 60 | |||
| 61 | const scripts = characters[this_chid]?.data?.extensions?.regex_scripts; | ||
| 62 | |||
| 63 | if (!Array.isArray(scripts)) { | ||
| 64 | return []; | ||
| 65 | } | ||
| 66 | |||
| 67 | return scripts; | ||
| 68 | } | ||
| 69 | |||
| 70 | /** | 104 | /** |
| 71 | * Parent function to fetch a regexed version of a raw string | 105 | * Parent function to fetch a regexed version of a raw string |
| 72 | * @param {string} rawString The raw string to be regexed | 106 | * @param {string} rawString The raw string to be regexed |
| @@ -87,7 +121,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown, | |||
| 87 | return finalString; | 121 | return finalString; |
| 88 | } | 122 | } |
| 89 | 123 | ||
| 90 | const allRegex = [...(extension_settings.regex ?? []), ...(getScopedRegex() ?? [])]; | 124 | const allRegex = getRegexScripts({ allowedOnly: true }); |
| 91 | allRegex.forEach((script) => { | 125 | allRegex.forEach((script) => { |
| 92 | if ( | 126 | if ( |
| 93 | // Script applies to Markdown and input is Markdown | 127 | // Script applies to Markdown and input is Markdown |
| @@ -126,7 +160,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown, | |||
| 126 | 160 | ||
| 127 | /** | 161 | /** |
| 128 | * Runs the provided regex script on the given string | 162 | * Runs the provided regex script on the given string |
| 129 | * @param {import('./index.js').RegexScript} regexScript The regex script to run | 163 | * @param {RegexScript} regexScript The regex script to run |
| 130 | * @param {string} rawString The string to run the regex script on | 164 | * @param {string} rawString The string to run the regex script on |
| 131 | * @param {RegexScriptParams} params The parameters to use for the regex script | 165 | * @param {RegexScriptParams} params The parameters to use for the regex script |
| 132 | * @returns {string} The new string | 166 | * @returns {string} The new string |
| @@ -7,8 +7,8 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from ' | |||
| 7 | import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; | 7 | import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 8 | import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; | 8 | import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 9 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; | 9 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| 10 | import { download, equalsIgnoreCaseAndAccents, getFileText, getSortableDelay, isFalseBoolean, isTrueBoolean, regexFromString, setInfoBlock, uuidv4, escapeHtml } from '../../utils.js'; | 10 | import { download, equalsIgnoreCaseAndAccents, escapeHtml, getFileText, getSortableDelay, isFalseBoolean, isTrueBoolean, regexFromString, setInfoBlock, uuidv4 } from '../../utils.js'; |
| 11 | import { regex_placement, runRegexScript, substitute_find_regex } from './engine.js'; | 11 | import { getRegexScripts, getScriptsByType, regex_placement, runRegexScript, SCRIPT_TYPES, substitute_find_regex } from './engine.js'; |
| 12 | import { t } from '../../i18n.js'; | 12 | import { t } from '../../i18n.js'; |
| 13 | import { accountStorage } from '../../util/AccountStorage.js'; | 13 | import { accountStorage } from '../../util/AccountStorage.js'; |
| 14 | 14 | ||
| @@ -65,8 +65,8 @@ class RegexPresetManager { | |||
| 65 | * @returns {RegexPresetState} The current state object | 65 | * @returns {RegexPresetState} The current state object |
| 66 | */ | 66 | */ |
| 67 | captureCurrentState() { | 67 | captureCurrentState() { |
| 68 | const globalScripts = this.regexListToPresetItems(extension_settings.regex) || []; | 68 | const globalScripts = this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.GLOBAL)); |
| 69 | const scopedScripts = this.regexListToPresetItems(characters[this_chid]?.data?.extensions?.regex_scripts) || []; | 69 | const scopedScripts = this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.SCOPED)); |
| 70 | 70 | ||
| 71 | return { | 71 | return { |
| 72 | global: globalScripts.map(item => item.id).sort(), | 72 | global: globalScripts.map(item => item.id).sort(), |
| @@ -418,8 +418,8 @@ class RegexPresetManager { | |||
| 418 | id: id, | 418 | id: id, |
| 419 | name: name, | 419 | name: name, |
| 420 | isSelected: false, | 420 | isSelected: false, |
| 421 | global: this.regexListToPresetItems(extension_settings.regex), | 421 | global: this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.GLOBAL)), |
| 422 | scoped: this.regexListToPresetItems(characters[this_chid]?.data?.extensions?.regex_scripts), | 422 | scoped: this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.SCOPED)), |
| 423 | }; | 423 | }; |
| 424 | 424 | ||
| 425 | if (isUpdate) { | 425 | if (isUpdate) { |
| @@ -465,15 +465,6 @@ class RegexPresetManager { | |||
| 465 | const presetManager = new RegexPresetManager(); | 465 | const presetManager = new RegexPresetManager(); |
| 466 | 466 | ||
| 467 | /** | 467 | /** |
| 468 | * Retrieves the list of regex scripts by combining the scripts from the extension settings and the character data | ||
| 469 | * | ||
| 470 | * @return {RegexScript[]} An array of regex scripts, where each script is an object containing the necessary information. | ||
| 471 | */ | ||
| 472 | export function getRegexScripts() { | ||
| 473 | return [...(extension_settings.regex ?? []), ...(characters[this_chid]?.data?.extensions?.regex_scripts ?? [])]; | ||
| 474 | } | ||
| 475 | |||
| 476 | /** | ||
| 477 | * Toggle the icon for the "select all" checkbox in the regex settings. | 468 | * Toggle the icon for the "select all" checkbox in the regex settings. |
| 478 | * - Use `fa-check-double` when the checkbox is unchecked (indicating all scripts are not selected). | 469 | * - Use `fa-check-double` when the checkbox is unchecked (indicating all scripts are not selected). |
| 479 | * - Use `fa-minus` when the checkbox is checked (indicating all scripts are selected). | 470 | * - Use `fa-minus` when the checkbox is checked (indicating all scripts are selected). |
| @@ -485,16 +476,25 @@ function setToggleAllIcon(allAreChecked) { | |||
| 485 | selectAllIcon.toggleClass('fa-minus', allAreChecked); | 476 | selectAllIcon.toggleClass('fa-minus', allAreChecked); |
| 486 | } | 477 | } |
| 487 | 478 | ||
| 479 | function setMoveButtonsVisibility() { | ||
| 480 | const hasGlobalScripts = $('#saved_regex_scripts .regex-script-label:has(.regex_bulk_checkbox:checked)').length > 0; | ||
| 481 | const hasScopedScripts = | ||
| 482 | $('#saved_scoped_scripts .regex-script-label:has(.regex_bulk_checkbox:checked)').length > 0; | ||
| 483 | $('#bulk_regex_move_to_global').toggle(hasScopedScripts); | ||
| 484 | $('#bulk_regex_move_to_scoped').toggle(hasGlobalScripts); | ||
| 485 | } | ||
| 486 | |||
| 488 | /** | 487 | /** |
| 489 | * Saves a regex script to the extension settings or character data. | 488 | * Saves a regex script to the extension settings or character data. |
| 490 | * @param {import('../../char-data.js').RegexScriptData} regexScript | 489 | * @param {import('../../char-data.js').RegexScriptData} regexScript |
| 491 | * @param {number} existingScriptIndex Index of the existing script | 490 | * @param {number} existingScriptIndex Index of the existing script |
| 492 | * @param {boolean} isScoped Is the script scoped to a character? | 491 | * @param {SCRIPT_TYPES} scriptType global? scoped? |
| 492 | * @param {boolean} [saveSettings=true] Whether to save the settings immediately | ||
| 493 | * @returns {Promise<void>} | 493 | * @returns {Promise<void>} |
| 494 | */ | 494 | */ |
| 495 | async function saveRegexScript(regexScript, existingScriptIndex, isScoped) { | 495 | async function saveRegexScript(regexScript, existingScriptIndex, scriptType, saveSettings = true) { |
| 496 | // If not editing | 496 | // If not editing |
| 497 | const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? []; | 497 | const array = getScriptsByType(scriptType); |
| 498 | 498 | ||
| 499 | // Assign a UUID if it doesn't exist | 499 | // Assign a UUID if it doesn't exist |
| 500 | if (!regexScript.id) { | 500 | if (!regexScript.id) { |
| @@ -523,7 +523,7 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) { | |||
| 523 | array.push(regexScript); | 523 | array.push(regexScript); |
| 524 | } | 524 | } |
| 525 | 525 | ||
| 526 | if (isScoped) { | 526 | if (scriptType === SCRIPT_TYPES.SCOPED) { |
| 527 | await writeExtensionField(this_chid, 'regex_scripts', array); | 527 | await writeExtensionField(this_chid, 'regex_scripts', array); |
| 528 | 528 | ||
| 529 | // Add the character to the allowed list | 529 | // Add the character to the allowed list |
| @@ -532,13 +532,15 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) { | |||
| 532 | } | 532 | } |
| 533 | } | 533 | } |
| 534 | 534 | ||
| 535 | saveSettingsDebounced(); | 535 | if (saveSettings) { |
| 536 | await loadRegexScripts(); | 536 | saveSettingsDebounced(); |
| 537 | await loadRegexScripts(); | ||
| 537 | 538 | ||
| 538 | // Reload the current chat to undo previous markdown | 539 | // Reload the current chat to undo previous markdown |
| 539 | const currentChatId = getCurrentChatId(); | 540 | const currentChatId = getCurrentChatId(); |
| 540 | if (currentChatId !== undefined && currentChatId !== null) { | 541 | if (currentChatId !== undefined && currentChatId !== null) { |
| 541 | await reloadCurrentChat(); | 542 | await reloadCurrentChat(); |
| 543 | } | ||
| 542 | } | 544 | } |
| 543 | 545 | ||
| 544 | const debuggerPopup = $('#regex_debugger_popup'); | 546 | const debuggerPopup = $('#regex_debugger_popup'); |
| @@ -547,20 +549,47 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) { | |||
| 547 | } | 549 | } |
| 548 | } | 550 | } |
| 549 | 551 | ||
| 550 | async function deleteRegexScript({ id, isScoped }) { | 552 | /** |
| 551 | const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? []; | 553 | * Delete a regex script |
| 554 | * @param {string} id | ||
| 555 | * @param {SCRIPT_TYPES} scriptType | ||
| 556 | * @param {boolean} saveSettings | ||
| 557 | * @returns {Promise<void>} | ||
| 558 | */ | ||
| 559 | async function deleteRegexScript(id, scriptType, saveSettings = true) { | ||
| 560 | const array = getScriptsByType(scriptType); | ||
| 552 | 561 | ||
| 553 | const existingScriptIndex = array.findIndex((script) => script.id === id); | 562 | const existingScriptIndex = array.findIndex(script => script.id === id); |
| 554 | if (existingScriptIndex !== -1) { | 563 | if (existingScriptIndex !== -1) { |
| 555 | array.splice(existingScriptIndex, 1); | 564 | array.splice(existingScriptIndex, 1); |
| 556 | 565 | ||
| 557 | if (isScoped) { | 566 | if (scriptType === SCRIPT_TYPES.SCOPED) { |
| 558 | await writeExtensionField(this_chid, 'regex_scripts', array); | 567 | await writeExtensionField(this_chid, 'regex_scripts', array); |
| 559 | } | 568 | } |
| 569 | if (saveSettings) { | ||
| 570 | saveSettingsDebounced(); | ||
| 571 | await loadRegexScripts(); | ||
| 572 | } | ||
| 573 | } | ||
| 574 | } | ||
| 560 | 575 | ||
| 561 | saveSettingsDebounced(); | 576 | /** |
| 562 | await loadRegexScripts(); | 577 | * Move a regex script from one type to another |
| 578 | * @param {import('../../char-data.js').RegexScriptData} script | ||
| 579 | * @param {SCRIPT_TYPES} toType | ||
| 580 | * @param {SCRIPT_TYPES|null} fromType | ||
| 581 | * @param {boolean} saveSettings | ||
| 582 | * @returns {Promise<void>} | ||
| 583 | */ | ||
| 584 | async function moveRegexScript(script, toType, fromType = null, saveSettings = true) { | ||
| 585 | if (!fromType) { | ||
| 586 | fromType = getScriptType(script); | ||
| 563 | } | 587 | } |
| 588 | if (fromType === toType || fromType === -1) { | ||
| 589 | return; | ||
| 590 | } | ||
| 591 | await deleteRegexScript(script.id, fromType, false); | ||
| 592 | await saveRegexScript(script, -1, toType, saveSettings); | ||
| 564 | } | 593 | } |
| 565 | 594 | ||
| 566 | async function loadRegexScripts() { | 595 | async function loadRegexScripts() { |
| @@ -574,13 +603,13 @@ async function loadRegexScripts() { | |||
| 574 | * Renders a script to the UI. | 603 | * Renders a script to the UI. |
| 575 | * @param {string} container Container to render the script to | 604 | * @param {string} container Container to render the script to |
| 576 | * @param {import('../../char-data.js').RegexScriptData} script Script data | 605 | * @param {import('../../char-data.js').RegexScriptData} script Script data |
| 577 | * @param {boolean} isScoped Script is scoped to a character | 606 | * @param {SCRIPT_TYPES} scriptType global? scoped? |
| 578 | * @param {number} index Index of the script in the array | 607 | * @param {number} index Index of the script in the array |
| 579 | */ | 608 | */ |
| 580 | function renderScript(container, script, isScoped, index) { | 609 | function renderScript(container, script, scriptType, index) { |
| 581 | // Have to clone here | 610 | // Have to clone here |
| 582 | const scriptHtml = scriptTemplate.clone(); | 611 | const scriptHtml = scriptTemplate.clone(); |
| 583 | const save = () => saveRegexScript(script, index, isScoped); | 612 | const save = () => saveRegexScript(script, index, scriptType); |
| 584 | 613 | ||
| 585 | if (!script.id) { | 614 | if (!script.id) { |
| 586 | script.id = uuidv4(); | 615 | script.id = uuidv4(); |
| @@ -600,7 +629,7 @@ async function loadRegexScripts() { | |||
| 600 | scriptHtml.find('.disable_regex').prop('checked', false).trigger('input'); | 629 | scriptHtml.find('.disable_regex').prop('checked', false).trigger('input'); |
| 601 | }); | 630 | }); |
| 602 | scriptHtml.find('.edit_existing_regex').on('click', async function () { | 631 | scriptHtml.find('.edit_existing_regex').on('click', async function () { |
| 603 | await onRegexEditorOpenClick(scriptHtml.attr('id'), isScoped); | 632 | await onRegexEditorOpenClick(scriptHtml.attr('id'), scriptType); |
| 604 | }); | 633 | }); |
| 605 | scriptHtml.find('.move_to_global').on('click', async function () { | 634 | scriptHtml.find('.move_to_global').on('click', async function () { |
| 606 | const confirm = await callGenericPopup(t`Are you sure you want to move this regex script to global?`, POPUP_TYPE.CONFIRM); | 635 | const confirm = await callGenericPopup(t`Are you sure you want to move this regex script to global?`, POPUP_TYPE.CONFIRM); |
| @@ -608,29 +637,22 @@ async function loadRegexScripts() { | |||
| 608 | if (!confirm) { | 637 | if (!confirm) { |
| 609 | return; | 638 | return; |
| 610 | } | 639 | } |
| 611 | 640 | await moveRegexScript(script, SCRIPT_TYPES.GLOBAL, scriptType); | |
| 612 | await deleteRegexScript({ id: script.id, isScoped: true }); | ||
| 613 | await saveRegexScript(script, -1, false); | ||
| 614 | }); | 641 | }); |
| 615 | scriptHtml.find('.move_to_scoped').on('click', async function () { | 642 | scriptHtml.find('.move_to_scoped').on('click', async function () { |
| 616 | if (this_chid === undefined) { | 643 | if (this_chid === undefined) { |
| 617 | toastr.error(t`No character selected.`); | 644 | toastr.error(t`No character selected.`); |
| 618 | return; | 645 | return; |
| 619 | } | 646 | } |
| 620 | |||
| 621 | if (selected_group) { | 647 | if (selected_group) { |
| 622 | toastr.error(t`Cannot edit scoped scripts in group chats.`); | 648 | toastr.error(t`Cannot edit scoped scripts in group chats.`); |
| 623 | return; | 649 | return; |
| 624 | } | 650 | } |
| 625 | |||
| 626 | const confirm = await callGenericPopup(t`Are you sure you want to move this regex script to scoped?`, POPUP_TYPE.CONFIRM); | 651 | const confirm = await callGenericPopup(t`Are you sure you want to move this regex script to scoped?`, POPUP_TYPE.CONFIRM); |
| 627 | |||
| 628 | if (!confirm) { | 652 | if (!confirm) { |
| 629 | return; | 653 | return; |
| 630 | } | 654 | } |
| 631 | 655 | await moveRegexScript(script, SCRIPT_TYPES.SCOPED, scriptType); | |
| 632 | await deleteRegexScript({ id: script.id, isScoped: false }); | ||
| 633 | await saveRegexScript(script, -1, true); | ||
| 634 | }); | 656 | }); |
| 635 | scriptHtml.find('.export_regex').on('click', async function () { | 657 | scriptHtml.find('.export_regex').on('click', async function () { |
| 636 | const fileName = `regex-${sanitizeFileName(script.scriptName)}.json`; | 658 | const fileName = `regex-${sanitizeFileName(script.scriptName)}.json`; |
| @@ -639,15 +661,14 @@ async function loadRegexScripts() { | |||
| 639 | }); | 661 | }); |
| 640 | scriptHtml.find('.delete_regex').on('click', async function () { | 662 | scriptHtml.find('.delete_regex').on('click', async function () { |
| 641 | const confirm = await callGenericPopup(t`Are you sure you want to delete this regex script?`, POPUP_TYPE.CONFIRM); | 663 | const confirm = await callGenericPopup(t`Are you sure you want to delete this regex script?`, POPUP_TYPE.CONFIRM); |
| 642 | |||
| 643 | if (!confirm) { | 664 | if (!confirm) { |
| 644 | return; | 665 | return; |
| 645 | } | 666 | } |
| 646 | 667 | await deleteRegexScript(script.id, scriptType); | |
| 647 | await deleteRegexScript({ id: script.id, isScoped }); | ||
| 648 | await reloadCurrentChat(); | 668 | await reloadCurrentChat(); |
| 649 | }); | 669 | }); |
| 650 | scriptHtml.find('.regex_bulk_checkbox').on('change', function () { | 670 | scriptHtml.find('.regex_bulk_checkbox').on('change', function () { |
| 671 | setMoveButtonsVisibility(); | ||
| 651 | const checkboxes = $('#regex_container .regex_bulk_checkbox'); | 672 | const checkboxes = $('#regex_container .regex_bulk_checkbox'); |
| 652 | const allAreChecked = checkboxes.length === checkboxes.filter(':checked').length; | 673 | const allAreChecked = checkboxes.length === checkboxes.filter(':checked').length; |
| 653 | setToggleAllIcon(allAreChecked); | 674 | setToggleAllIcon(allAreChecked); |
| @@ -656,22 +677,24 @@ async function loadRegexScripts() { | |||
| 656 | $(container).append(scriptHtml); | 677 | $(container).append(scriptHtml); |
| 657 | } | 678 | } |
| 658 | 679 | ||
| 659 | extension_settings?.regex?.forEach((script, index) => renderScript('#saved_regex_scripts', script, false, index)); | 680 | getScriptsByType(SCRIPT_TYPES.GLOBAL).forEach((script, index) => renderScript('#saved_regex_scripts', script, SCRIPT_TYPES.GLOBAL, index)); |
| 660 | characters[this_chid]?.data?.extensions?.regex_scripts?.forEach((script, index) => renderScript('#saved_scoped_scripts', script, true, index)); | 681 | getScriptsByType(SCRIPT_TYPES.SCOPED).forEach((script, index) => renderScript('#saved_scoped_scripts', script, SCRIPT_TYPES.SCOPED, index)); |
| 682 | |||
| 683 | const isScopedAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar); | ||
| 684 | $('#regex_scoped_toggle').prop('checked', isScopedAllowed); | ||
| 661 | 685 | ||
| 662 | const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar); | 686 | setMoveButtonsVisibility(); |
| 663 | $('#regex_scoped_toggle').prop('checked', isAllowed); | ||
| 664 | } | 687 | } |
| 665 | 688 | ||
| 666 | /** | 689 | /** |
| 667 | * Opens the regex editor. | 690 | * Opens the regex editor. |
| 668 | * @param {string|boolean} existingId Existing ID | 691 | * @param {string|boolean} existingId Existing ID |
| 669 | * @param {boolean} isScoped Is the script scoped to a character? | 692 | * @param {SCRIPT_TYPES} scriptType global? scoped? |
| 670 | * @returns {Promise<void>} | 693 | * @returns {Promise<void>} |
| 671 | */ | 694 | */ |
| 672 | async function onRegexEditorOpenClick(existingId, isScoped) { | 695 | async function onRegexEditorOpenClick(existingId, scriptType) { |
| 673 | const editorHtml = $(await renderExtensionTemplateAsync('regex', 'editor')); | 696 | const editorHtml = $(await renderExtensionTemplateAsync('regex', 'editor')); |
| 674 | const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? []; | 697 | const array = getScriptsByType(scriptType); |
| 675 | 698 | ||
| 676 | // If an ID exists, fill in all the values | 699 | // If an ID exists, fill in all the values |
| 677 | let existingScriptIndex = -1; | 700 | let existingScriptIndex = -1; |
| @@ -776,7 +799,7 @@ async function onRegexEditorOpenClick(existingId, isScoped) { | |||
| 776 | maxDepth: parseInt(String(editorHtml.find('input[name="max_depth"]').val())), | 799 | maxDepth: parseInt(String(editorHtml.find('input[name="max_depth"]').val())), |
| 777 | }; | 800 | }; |
| 778 | 801 | ||
| 779 | saveRegexScript(newRegexScript, existingScriptIndex, isScoped); | 802 | saveRegexScript(newRegexScript, existingScriptIndex, scriptType); |
| 780 | } | 803 | } |
| 781 | } | 804 | } |
| 782 | 805 | ||
| @@ -973,7 +996,7 @@ function populateDebuggerRuleList(container) { | |||
| 973 | return; | 996 | return; |
| 974 | } | 997 | } |
| 975 | 998 | ||
| 976 | const globalScriptIds = new Set((extension_settings.regex ?? []).map(s => s.id)); | 999 | const globalScriptIds = new Set(getScriptsByType(SCRIPT_TYPES.GLOBAL).map(s => s.id)); |
| 977 | const globalScripts = []; | 1000 | const globalScripts = []; |
| 978 | const scopedScripts = []; | 1001 | const scopedScripts = []; |
| 979 | 1002 | ||
| @@ -981,11 +1004,11 @@ function populateDebuggerRuleList(container) { | |||
| 981 | const scriptCopy = structuredClone(script); // Use structuredClone for deep copy | 1004 | const scriptCopy = structuredClone(script); // Use structuredClone for deep copy |
| 982 | if (globalScriptIds.has(script.id)) { | 1005 | if (globalScriptIds.has(script.id)) { |
| 983 | // @ts-ignore | 1006 | // @ts-ignore |
| 984 | scriptCopy.isScoped = false; | 1007 | scriptCopy.type = SCRIPT_TYPES.SCOPED; |
| 985 | globalScripts.push(scriptCopy); | 1008 | globalScripts.push(scriptCopy); |
| 986 | } else { | 1009 | } else { |
| 987 | // @ts-ignore | 1010 | // @ts-ignore |
| 988 | scriptCopy.isScoped = true; | 1011 | scriptCopy.type = SCRIPT_TYPES.SCOPED; |
| 989 | scopedScripts.push(scriptCopy); | 1012 | scopedScripts.push(scriptCopy); |
| 990 | } | 1013 | } |
| 991 | }); | 1014 | }); |
| @@ -1002,10 +1025,17 @@ function populateDebuggerRuleList(container) { | |||
| 1002 | ruleElement.find('.rule-name').text(script.scriptName); | 1025 | ruleElement.find('.rule-name').text(script.scriptName); |
| 1003 | ruleElement.find('.rule-regex').text(script.findRegex); | 1026 | ruleElement.find('.rule-regex').text(script.findRegex); |
| 1004 | // @ts-ignore | 1027 | // @ts-ignore |
| 1005 | ruleElement.find('.rule-scope').text(script.isScoped ? t`Scoped` : t`Global`); | 1028 | ruleElement |
| 1029 | .find('.rule-scope') | ||
| 1030 | .text( | ||
| 1031 | { | ||
| 1032 | [SCRIPT_TYPES.SCOPED]: t`Scoped`, | ||
| 1033 | [SCRIPT_TYPES.GLOBAL]: t`Global`, | ||
| 1034 | }[script.type], | ||
| 1035 | ); | ||
| 1006 | ruleElement.find('.rule-enabled').prop('checked', !script.disabled); | 1036 | ruleElement.find('.rule-enabled').prop('checked', !script.disabled); |
| 1007 | // @ts-ignore | 1037 | // @ts-ignore |
| 1008 | ruleElement.find('.edit_rule').on('click', () => onRegexEditorOpenClick(script.id, script.isScoped)); | 1038 | ruleElement.find('.edit_rule').on('click', () => onRegexEditorOpenClick(script.id, script.type)); |
| 1009 | 1039 | ||
| 1010 | ruleElement.on('click', function (event) { | 1040 | ruleElement.on('click', function (event) { |
| 1011 | if ($(event.target).is('input, .menu_button, .menu_button i')) { | 1041 | if ($(event.target).is('input, .menu_button, .menu_button i')) { |
| @@ -1364,10 +1394,10 @@ async function toggleRegexCallback(args, scriptName) { | |||
| 1364 | break; | 1394 | break; |
| 1365 | } | 1395 | } |
| 1366 | 1396 | ||
| 1367 | const isScoped = characters[this_chid]?.data?.extensions?.regex_scripts?.some(s => s.id === script.id); | 1397 | const scriptType = getScriptType(script); |
| 1368 | const index = isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts?.indexOf(script) : scripts.indexOf(script); | 1398 | const index = getScriptsByType(scriptType).indexOf(script); |
| 1369 | 1399 | ||
| 1370 | await saveRegexScript(script, index, isScoped); | 1400 | await saveRegexScript(script, index, scriptType); |
| 1371 | if (script.disabled) { | 1401 | if (script.disabled) { |
| 1372 | !quiet && toastr.success(t`Regex script '${scriptName}' has been disabled.`); | 1402 | !quiet && toastr.success(t`Regex script '${scriptName}' has been disabled.`); |
| 1373 | } else { | 1403 | } else { |
| @@ -1380,9 +1410,9 @@ async function toggleRegexCallback(args, scriptName) { | |||
| 1380 | /** | 1410 | /** |
| 1381 | * Performs the import of the regex object. | 1411 | * Performs the import of the regex object. |
| 1382 | * @param {Object} regexScript Input object | 1412 | * @param {Object} regexScript Input object |
| 1383 | * @param {boolean} isScoped Is the script scoped to a character? | 1413 | * @param {SCRIPT_TYPES} scriptType global? scoped? |
| 1384 | */ | 1414 | */ |
| 1385 | async function onRegexImportObjectChange(regexScript, isScoped) { | 1415 | async function onRegexImportObjectChange(regexScript, scriptType) { |
| 1386 | try { | 1416 | try { |
| 1387 | if (!regexScript.scriptName) { | 1417 | if (!regexScript.scriptName) { |
| 1388 | throw new Error('No script name provided.'); | 1418 | throw new Error('No script name provided.'); |
| @@ -1391,10 +1421,10 @@ async function onRegexImportObjectChange(regexScript, isScoped) { | |||
| 1391 | // Assign a new UUID | 1421 | // Assign a new UUID |
| 1392 | regexScript.id = uuidv4(); | 1422 | regexScript.id = uuidv4(); |
| 1393 | 1423 | ||
| 1394 | const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? []; | 1424 | const array = getScriptsByType(scriptType); |
| 1395 | array.push(regexScript); | 1425 | array.push(regexScript); |
| 1396 | 1426 | ||
| 1397 | if (isScoped) { | 1427 | if (scriptType === SCRIPT_TYPES.SCOPED) { |
| 1398 | await writeExtensionField(this_chid, 'regex_scripts', array); | 1428 | await writeExtensionField(this_chid, 'regex_scripts', array); |
| 1399 | } | 1429 | } |
| 1400 | 1430 | ||
| @@ -1411,9 +1441,9 @@ async function onRegexImportObjectChange(regexScript, isScoped) { | |||
| 1411 | /** | 1441 | /** |
| 1412 | * Performs the import of the regex file. | 1442 | * Performs the import of the regex file. |
| 1413 | * @param {File} file Input file | 1443 | * @param {File} file Input file |
| 1414 | * @param {boolean} isScoped Is the script scoped to a character? | 1444 | * @param {SCRIPT_TYPES} scriptType global? scoped? |
| 1415 | */ | 1445 | */ |
| 1416 | async function onRegexImportFileChange(file, isScoped) { | 1446 | async function onRegexImportFileChange(file, scriptType) { |
| 1417 | if (!file) { | 1447 | if (!file) { |
| 1418 | toastr.error('No file provided.'); | 1448 | toastr.error('No file provided.'); |
| 1419 | return; | 1449 | return; |
| @@ -1423,10 +1453,10 @@ async function onRegexImportFileChange(file, isScoped) { | |||
| 1423 | const regexScripts = JSON.parse(await getFileText(file)); | 1453 | const regexScripts = JSON.parse(await getFileText(file)); |
| 1424 | if (Array.isArray(regexScripts)) { | 1454 | if (Array.isArray(regexScripts)) { |
| 1425 | for (const regexScript of regexScripts) { | 1455 | for (const regexScript of regexScripts) { |
| 1426 | await onRegexImportObjectChange(regexScript, isScoped); | 1456 | await onRegexImportObjectChange(regexScript, scriptType); |
| 1427 | } | 1457 | } |
| 1428 | } else { | 1458 | } else { |
| 1429 | await onRegexImportObjectChange(regexScripts, isScoped); | 1459 | await onRegexImportObjectChange(regexScripts, scriptType); |
| 1430 | } | 1460 | } |
| 1431 | } catch (error) { | 1461 | } catch (error) { |
| 1432 | console.log(error); | 1462 | console.log(error); |
| @@ -1435,24 +1465,43 @@ async function onRegexImportFileChange(file, isScoped) { | |||
| 1435 | } | 1465 | } |
| 1436 | } | 1466 | } |
| 1437 | 1467 | ||
| 1468 | function getScriptType(script) { | ||
| 1469 | return getScriptsByType(SCRIPT_TYPES.SCOPED).some(s => s.id === script.id) | ||
| 1470 | ? SCRIPT_TYPES.SCOPED | ||
| 1471 | : SCRIPT_TYPES.GLOBAL; | ||
| 1472 | } | ||
| 1473 | |||
| 1474 | function getSelectedScripts() { | ||
| 1475 | const scripts = getRegexScripts(); | ||
| 1476 | const selector = '#regex_container .regex-script-label:has(.regex_bulk_checkbox:checked)'; | ||
| 1477 | const selectedIds = Array.from(document.querySelectorAll(selector)) | ||
| 1478 | .map(e => e.getAttribute('id')) | ||
| 1479 | .filter(id => id); | ||
| 1480 | return scripts.filter(script => selectedIds.includes(script.id)); | ||
| 1481 | } | ||
| 1482 | |||
| 1438 | function purgeEmbeddedRegexScripts({ character }) { | 1483 | function purgeEmbeddedRegexScripts({ character }) { |
| 1439 | const avatar = character?.avatar; | 1484 | const avatar = character?.avatar; |
| 1440 | 1485 | if (!avatar) { | |
| 1441 | if (avatar && extension_settings.character_allowed_regex?.includes(avatar)) { | 1486 | return; |
| 1442 | const index = extension_settings.character_allowed_regex.indexOf(avatar); | 1487 | } |
| 1443 | if (index !== -1) { | 1488 | const checkKey = `AlertRegex_${characters[this_chid].avatar}`; |
| 1444 | extension_settings.character_allowed_regex.splice(index, 1); | 1489 | if (accountStorage.getItem(checkKey)) { |
| 1445 | saveSettingsDebounced(); | 1490 | accountStorage.removeItem(checkKey); |
| 1446 | } | 1491 | } |
| 1492 | const index = extension_settings.character_allowed_regex.indexOf(avatar); | ||
| 1493 | if (index !== -1) { | ||
| 1494 | extension_settings.character_allowed_regex.splice(index, 1); | ||
| 1495 | saveSettingsDebounced(); | ||
| 1447 | } | 1496 | } |
| 1448 | } | 1497 | } |
| 1449 | 1498 | ||
| 1450 | async function checkEmbeddedRegexScripts() { | 1499 | async function checkCharEmbeddedRegexScripts() { |
| 1451 | const chid = this_chid; | 1500 | const chid = this_chid; |
| 1452 | 1501 | ||
| 1453 | if (chid !== undefined && !selected_group) { | 1502 | if (chid !== undefined && !selected_group) { |
| 1454 | const avatar = characters[chid]?.avatar; | 1503 | const avatar = characters[chid]?.avatar; |
| 1455 | const scripts = characters[chid]?.data?.extensions?.regex_scripts; | 1504 | const scripts = getScriptsByType(SCRIPT_TYPES.SCOPED); |
| 1456 | 1505 | ||
| 1457 | if (Array.isArray(scripts) && scripts.length > 0) { | 1506 | if (Array.isArray(scripts) && scripts.length > 0) { |
| 1458 | if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) { | 1507 | if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) { |
| @@ -1497,7 +1546,7 @@ jQuery(async () => { | |||
| 1497 | const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown')); | 1546 | const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown')); |
| 1498 | $('#regex_container').append(settingsHtml); | 1547 | $('#regex_container').append(settingsHtml); |
| 1499 | $('#open_regex_editor').on('click', function () { | 1548 | $('#open_regex_editor').on('click', function () { |
| 1500 | onRegexEditorOpenClick(false, false); | 1549 | onRegexEditorOpenClick(false, SCRIPT_TYPES.GLOBAL); |
| 1501 | }); | 1550 | }); |
| 1502 | $('#open_regex_debugger').on('click', onRegexDebuggerOpenClick); | 1551 | $('#open_regex_debugger').on('click', onRegexDebuggerOpenClick); |
| 1503 | $('#open_scoped_editor').on('click', function () { | 1552 | $('#open_scoped_editor').on('click', function () { |
| @@ -1511,19 +1560,19 @@ jQuery(async () => { | |||
| 1511 | return; | 1560 | return; |
| 1512 | } | 1561 | } |
| 1513 | 1562 | ||
| 1514 | onRegexEditorOpenClick(false, true); | 1563 | onRegexEditorOpenClick(false, SCRIPT_TYPES.SCOPED); |
| 1515 | }); | 1564 | }); |
| 1516 | $('#import_regex_file').on('change', async function () { | 1565 | $('#import_regex_file').on('change', async function () { |
| 1517 | let target = 'global'; | 1566 | let target = SCRIPT_TYPES.GLOBAL; |
| 1518 | const template = $(await renderExtensionTemplateAsync('regex', 'importTarget')); | 1567 | const template = $(await renderExtensionTemplateAsync('regex', 'importTarget')); |
| 1519 | template.find('#regex_import_target_global').on('input', () => target = 'global'); | 1568 | template.find('#regex_import_target_global').on('input', () => (target = SCRIPT_TYPES.GLOBAL)); |
| 1520 | template.find('#regex_import_target_scoped').on('input', () => target = 'scoped'); | 1569 | template.find('#regex_import_target_scoped').on('input', () => (target = SCRIPT_TYPES.SCOPED)); |
| 1521 | 1570 | ||
| 1522 | await callGenericPopup(template, POPUP_TYPE.TEXT); | 1571 | await callGenericPopup(template, POPUP_TYPE.TEXT); |
| 1523 | 1572 | ||
| 1524 | const inputElement = this instanceof HTMLInputElement && this; | 1573 | const inputElement = this instanceof HTMLInputElement && this; |
| 1525 | for (const file of inputElement.files) { | 1574 | for (const file of inputElement.files) { |
| 1526 | await onRegexImportFileChange(file, target === 'scoped'); | 1575 | await onRegexImportFileChange(file, target); |
| 1527 | } | 1576 | } |
| 1528 | inputElement.value = ''; | 1577 | inputElement.value = ''; |
| 1529 | }); | 1578 | }); |
| @@ -1531,13 +1580,6 @@ jQuery(async () => { | |||
| 1531 | $('#import_regex_file').trigger('click'); | 1580 | $('#import_regex_file').trigger('click'); |
| 1532 | }); | 1581 | }); |
| 1533 | 1582 | ||
| 1534 | function getSelectedScripts() { | ||
| 1535 | const scripts = getRegexScripts(); | ||
| 1536 | const selector = '#regex_container .regex-script-label:has(.regex_bulk_checkbox:checked)'; | ||
| 1537 | const selectedIds = Array.from(document.querySelectorAll(selector)).map(e => e.getAttribute('id')).filter(id => id); | ||
| 1538 | return scripts.filter(script => selectedIds.includes(script.id)); | ||
| 1539 | } | ||
| 1540 | |||
| 1541 | $('#bulk_select_all_toggle').on('click', async function () { | 1583 | $('#bulk_select_all_toggle').on('click', async function () { |
| 1542 | const checkboxes = $('#regex_container .regex_bulk_checkbox'); | 1584 | const checkboxes = $('#regex_container .regex_bulk_checkbox'); |
| 1543 | if (checkboxes.length === 0) { | 1585 | if (checkboxes.length === 0) { |
| @@ -1577,6 +1619,31 @@ jQuery(async () => { | |||
| 1577 | await loadRegexScripts(); | 1619 | await loadRegexScripts(); |
| 1578 | }); | 1620 | }); |
| 1579 | 1621 | ||
| 1622 | /** | ||
| 1623 | * Bulk move regex scripts to the specified type | ||
| 1624 | * @param {SCRIPT_TYPES} toType destination type | ||
| 1625 | */ | ||
| 1626 | async function bulkMoveRegexScript(toType) { | ||
| 1627 | const scripts = getSelectedScripts(); | ||
| 1628 | if (scripts.length === 0) { | ||
| 1629 | toastr.warning(t`No regex scripts selected for moving.`); | ||
| 1630 | return; | ||
| 1631 | } | ||
| 1632 | for (const script of scripts) { | ||
| 1633 | await moveRegexScript(script, toType, getScriptType(script), false); | ||
| 1634 | } | ||
| 1635 | |||
| 1636 | await loadRegexScripts(); | ||
| 1637 | |||
| 1638 | // Reload the current chat to undo previous markdown | ||
| 1639 | const currentChatId = getCurrentChatId(); | ||
| 1640 | if (currentChatId !== undefined && currentChatId !== null) { | ||
| 1641 | await reloadCurrentChat(); | ||
| 1642 | } | ||
| 1643 | } | ||
| 1644 | $('#bulk_regex_move_to_global').on('click', () => bulkMoveRegexScript(SCRIPT_TYPES.GLOBAL)); | ||
| 1645 | $('#bulk_regex_move_to_scoped').on('click', () => bulkMoveRegexScript(SCRIPT_TYPES.SCOPED)); | ||
| 1646 | |||
| 1580 | $('#bulk_delete_regex').on('click', async function () { | 1647 | $('#bulk_delete_regex').on('click', async function () { |
| 1581 | const scripts = getSelectedScripts(); | 1648 | const scripts = getSelectedScripts(); |
| 1582 | if (scripts.length === 0) { | 1649 | if (scripts.length === 0) { |
| @@ -1588,11 +1655,11 @@ jQuery(async () => { | |||
| 1588 | return; | 1655 | return; |
| 1589 | } | 1656 | } |
| 1590 | for (const script of scripts) { | 1657 | for (const script of scripts) { |
| 1591 | const isScoped = characters[this_chid]?.data?.extensions?.regex_scripts?.some(s => s.id === script.id); | 1658 | await deleteRegexScript(script.id, getScriptType(script), false); |
| 1592 | await deleteRegexScript({ id: script.id, isScoped: isScoped }); | ||
| 1593 | } | 1659 | } |
| 1594 | await reloadCurrentChat(); | ||
| 1595 | saveSettingsDebounced(); | 1660 | saveSettingsDebounced(); |
| 1661 | await loadRegexScripts(); | ||
| 1662 | await reloadCurrentChat(); | ||
| 1596 | }); | 1663 | }); |
| 1597 | 1664 | ||
| 1598 | $('#bulk_export_regex').on('click', async function () { | 1665 | $('#bulk_export_regex').on('click', async function () { |
| @@ -1611,12 +1678,12 @@ jQuery(async () => { | |||
| 1611 | { | 1678 | { |
| 1612 | selector: '#saved_regex_scripts', | 1679 | selector: '#saved_regex_scripts', |
| 1613 | setter: x => extension_settings.regex = x, | 1680 | setter: x => extension_settings.regex = x, |
| 1614 | getter: () => extension_settings.regex ?? [], | 1681 | getter: () => getScriptsByType(SCRIPT_TYPES.GLOBAL), |
| 1615 | }, | 1682 | }, |
| 1616 | { | 1683 | { |
| 1617 | selector: '#saved_scoped_scripts', | 1684 | selector: '#saved_scoped_scripts', |
| 1618 | setter: x => writeExtensionField(this_chid, 'regex_scripts', x), | 1685 | setter: x => writeExtensionField(this_chid, 'regex_scripts', x), |
| 1619 | getter: () => characters[this_chid]?.data?.extensions?.regex_scripts ?? [], | 1686 | getter: () => getScriptsByType(SCRIPT_TYPES.SCOPED), |
| 1620 | }, | 1687 | }, |
| 1621 | ]; | 1688 | ]; |
| 1622 | for (const { selector, setter, getter } of sortableDatas) { | 1689 | for (const { selector, setter, getter } of sortableDatas) { |
| @@ -1638,6 +1705,7 @@ jQuery(async () => { | |||
| 1638 | saveSettingsDebounced(); | 1705 | saveSettingsDebounced(); |
| 1639 | 1706 | ||
| 1640 | console.debug(`Regex scripts in ${selector} reordered`); | 1707 | console.debug(`Regex scripts in ${selector} reordered`); |
| 1708 | await reloadCurrentChat(); | ||
| 1641 | await loadRegexScripts(); | 1709 | await loadRegexScripts(); |
| 1642 | }, | 1710 | }, |
| 1643 | }); | 1711 | }); |
| @@ -1676,12 +1744,49 @@ jQuery(async () => { | |||
| 1676 | // @ts-ignore | 1744 | // @ts-ignore |
| 1677 | $('#saved_regex_scripts').sortable('enable'); | 1745 | $('#saved_regex_scripts').sortable('enable'); |
| 1678 | 1746 | ||
| 1747 | /** | ||
| 1748 | * @typedef {object} ScriptDecorators | ||
| 1749 | * @property {string} typename | ||
| 1750 | * @property {import('../../slash-commands/SlashCommandEnumValue.js').EnumType} color | ||
| 1751 | * @property {string} icon | ||
| 1752 | */ | ||
| 1753 | /** | ||
| 1754 | * @param {SCRIPT_TYPES} type | ||
| 1755 | * @returns {ScriptDecorators} | ||
| 1756 | */ | ||
| 1757 | function getScriptDecorators(type) { | ||
| 1758 | switch (type) { | ||
| 1759 | case SCRIPT_TYPES.GLOBAL: | ||
| 1760 | return { | ||
| 1761 | typename: 'global', | ||
| 1762 | color: enumTypes.enum, | ||
| 1763 | icon: 'G', | ||
| 1764 | }; | ||
| 1765 | case SCRIPT_TYPES.SCOPED: | ||
| 1766 | return { | ||
| 1767 | typename: 'scoped', | ||
| 1768 | color: enumTypes.name, | ||
| 1769 | icon: 'S', | ||
| 1770 | }; | ||
| 1771 | } | ||
| 1772 | return { | ||
| 1773 | typename: 'Unknown', | ||
| 1774 | color: enumTypes.variable, | ||
| 1775 | icon: 'Unknown', | ||
| 1776 | }; | ||
| 1777 | } | ||
| 1679 | const localEnumProviders = { | 1778 | const localEnumProviders = { |
| 1680 | regexScripts: () => getRegexScripts().map(script => { | 1779 | regexScripts: () => |
| 1681 | const isGlobal = extension_settings.regex?.some(x => x.scriptName === script.scriptName); | 1780 | getRegexScripts().map(script => { |
| 1682 | return new SlashCommandEnumValue(script.scriptName, `${enumIcons.getStateIcon(!script.disabled)} [${isGlobal ? 'global' : 'scoped'}] ${script.findRegex}`, | 1781 | const type = getScriptType(script); |
| 1683 | isGlobal ? enumTypes.enum : enumTypes.name, isGlobal ? 'G' : 'S'); | 1782 | const { typename, color, icon } = getScriptDecorators(type); |
| 1684 | }), | 1783 | return new SlashCommandEnumValue( |
| 1784 | script.scriptName, | ||
| 1785 | `${enumIcons.getStateIcon(!script.disabled)} [${typename}] ${script.findRegex}`, | ||
| 1786 | color, | ||
| 1787 | icon, | ||
| 1788 | ); | ||
| 1789 | }), | ||
| 1685 | }; | 1790 | }; |
| 1686 | 1791 | ||
| 1687 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 1792 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| @@ -1750,7 +1855,7 @@ jQuery(async () => { | |||
| 1750 | `, | 1855 | `, |
| 1751 | })); | 1856 | })); |
| 1752 | 1857 | ||
| 1753 | eventSource.on(event_types.CHAT_CHANGED, checkEmbeddedRegexScripts); | 1858 | eventSource.on(event_types.CHAT_CHANGED, checkCharEmbeddedRegexScripts); |
| 1754 | eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts); | 1859 | eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts); |
| 1755 | 1860 | ||
| 1756 | presetManager.setupEventListeners(); | 1861 | presetManager.setupEventListeners(); |
| @@ -12,10 +12,10 @@ | |||
| 12 | <i class="fa-solid fa-pencil"></i> | 12 | <i class="fa-solid fa-pencil"></i> |
| 13 | </div> | 13 | </div> |
| 14 | <div class="move_to_global menu_button" data-i18n="[title]ext_regex_move_to_global" title="Move to global scripts"> | 14 | <div class="move_to_global menu_button" data-i18n="[title]ext_regex_move_to_global" title="Move to global scripts"> |
| 15 | <i class="fa-solid fa-arrow-up"></i> | 15 | <i class="fa-solid fa-globe"></i> |
| 16 | </div> | 16 | </div> |
| 17 | <div class="move_to_scoped menu_button" data-i18n="[title]ext_regex_move_to_scoped" title="Move to scoped scripts"> | 17 | <div class="move_to_scoped menu_button" data-i18n="[title]ext_regex_move_to_scoped" title="Move to scoped scripts"> |
| 18 | <i class="fa-solid fa-arrow-down"></i> | 18 | <i class="fa-solid fa-address-card"></i> |
| 19 | </div> | 19 | </div> |
| 20 | <div class="export_regex menu_button" data-i18n="[title]ext_regex_export_script" title="Export script"> | 20 | <div class="export_regex menu_button" data-i18n="[title]ext_regex_export_script" title="Export script"> |
| 21 | <i class="fa-solid fa-file-export"></i> | 21 | <i class="fa-solid fa-file-export"></i> |
| @@ -139,7 +139,8 @@ input.enable_scoped { | |||
| 139 | } | 139 | } |
| 140 | 140 | ||
| 141 | .regex_settings .regex_bulk_operations, | 141 | .regex_settings .regex_bulk_operations, |
| 142 | .regex_settings .regex_bulk_checkbox { | 142 | .regex_settings .regex_bulk_checkbox, |
| 143 | .regex_settings .regex_bulk_operations_hr { | ||
| 143 | display: none; | 144 | display: none; |
| 144 | } | 145 | } |
| 145 | 146 | ||
| @@ -147,6 +148,10 @@ input.enable_scoped { | |||
| 147 | display: flex; | 148 | display: flex; |
| 148 | } | 149 | } |
| 149 | 150 | ||
| 151 | .regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_operations_hr { | ||
| 152 | display: block; | ||
| 153 | } | ||
| 154 | |||
| 150 | .regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_checkbox { | 155 | .regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_checkbox { |
| 151 | display: inline-grid; | 156 | display: inline-grid; |
| 152 | } | 157 | } |