Merge pull request #3967 from gakada/regex Regex: allow multiple definitions in a single file
Signed| @@ -21,6 +21,29 @@ | ||
| 21 | 21 | <small data-i18n="ext_regex_import_script">Import</small> |
| 22 | 22 | </div> |
| 23 | 23 | <input type="file" id="import_regex_file" hidden accept="*.json" multiple /> |
| 24 | + <label for="regex_bulk_edit" class="menu_button menu_button_icon"> | |
| 25 | + <input id="regex_bulk_edit" type="checkbox" class="displayNone" /> | |
| 26 | + <i class="fa-solid fa-edit"></i> | |
| 27 | + <small data-i18n="ext_regex_bulk_edit">Bulk Edit</small> | |
| 28 | + </label> | |
| 29 | + </div> | |
| 30 | + <div class="regex_bulk_operations flex-container justifyCenter"> | |
| 31 | + <div id="bulk_enable_regex" class="menu_button menu_button_icon"> | |
| 32 | + <i class="fa-solid fa-toggle-on"></i> | |
| 33 | + <small data-i18n="Enable">Enable</small> | |
| 34 | + </div> | |
| 35 | + <div id="bulk_disable_regex" class="menu_button menu_button_icon"> | |
| 36 | + <i class="fa-solid fa-toggle-off"></i> | |
| 37 | + <small data-i18n="Disable">Disable</small> | |
| 38 | + </div> | |
| 39 | + <div id="bulk_export_regex" class="menu_button menu_button_icon"> | |
| 40 | + <i class="fa-solid fa-file-export"></i> | |
| 41 | + <small data-i18n="Export">Export</small> | |
| 42 | + </div> | |
| 43 | + <div id="bulk_delete_regex" class="menu_button menu_button_icon"> | |
| 44 | + <i class="fa-solid fa-trash"></i> | |
| 45 | + <small data-i18n="Delete">Delete</small> | |
| 46 | + </div> | |
| 24 | 47 | </div> |
| 25 | 48 | <hr /> |
| 26 | 49 | <div id="global_scripts_block" class="padding5"> |
| @@ -12,6 +12,8 @@ import { regex_placement, runRegexScript, substitute_find_regex } from './engine | ||
| 12 | 12 | import { t } from '../../i18n.js'; |
| 13 | 13 | import { accountStorage } from '../../util/AccountStorage.js'; |
| 14 | 14 | |
| 15 | +const sanitizeFileName = name => name.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, '_').toLowerCase(); | |
| 16 | + | |
| 15 | 17 | /** |
| 16 | 18 | * @typedef {import('../../char-data.js').RegexScriptData} RegexScript |
| 17 | 19 | */ |
| @@ -167,7 +169,7 @@ async function loadRegexScripts() { | ||
| 167 | 169 | await saveRegexScript(script, -1, true); |
| 168 | 170 | }); |
| 169 | 171 | scriptHtml.find('.export_regex').on('click', async function () { |
| 170 | - const fileName = `${script.scriptName.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, '_').toLowerCase()}.json`; | |
| 172 | + const fileName = `regex-${sanitizeFileName(script.scriptName)}.json`; | |
| 171 | 173 | const fileData = JSON.stringify(script, null, 4); |
| 172 | 174 | download(fileData, fileName, 'application/json'); |
| 173 | 175 | }); |
| @@ -459,19 +461,12 @@ async function toggleRegexCallback(args, scriptName) { | ||
| 459 | 461 | } |
| 460 | 462 | |
| 461 | 463 | /** |
| 462 | 464 | * Performs the import of the regex fileobject. |
| 463 | 465 | * @param {FileObject} fileregexScript Input fileobject |
| 464 | 466 | * @param {boolean} isScoped Is the script scoped to a character? |
| 465 | 467 | */ |
| 466 | 468 | async function onRegexImportFileChangeonRegexImportObjectChange(fileregexScript, isScoped) { |
| 467 | - if (!file) { | |
| 468 | - toastr.error('No file provided.'); | |
| 469 | - return; | |
| 470 | - } | |
| 471 | - | |
| 472 | 469 | try { |
| 473 | - const fileText = await getFileText(file); | |
| 474 | - const regexScript = JSON.parse(fileText); | |
| 475 | 470 | if (!regexScript.scriptName) { |
| 476 | 471 | throw new Error('No script name provided.'); |
| 477 | 472 | } |
| @@ -491,6 +486,33 @@ async function onRegexImportFileChange(file, isScoped) { | ||
| 491 | 486 | toastr.success(`Regex script "${regexScript.scriptName}" imported.`); |
| 492 | 487 | } catch (error) { |
| 493 | 488 | console.log(error); |
| 489 | + toastr.error('Invalid regex object.'); | |
| 490 | + return; | |
| 491 | + } | |
| 492 | +} | |
| 493 | + | |
| 494 | +/** | |
| 495 | + * Performs the import of the regex file. | |
| 496 | + * @param {File} file Input file | |
| 497 | + * @param {boolean} isScoped Is the script scoped to a character? | |
| 498 | + */ | |
| 499 | +async function onRegexImportFileChange(file, isScoped) { | |
| 500 | + if (!file) { | |
| 501 | + toastr.error('No file provided.'); | |
| 502 | + return; | |
| 503 | + } | |
| 504 | + | |
| 505 | + try { | |
| 506 | + const regexScripts = JSON.parse(await getFileText(file)); | |
| 507 | + if (Array.isArray(regexScripts)) { | |
| 508 | + for (const regexScript of regexScripts) { | |
| 509 | + await onRegexImportObjectChange(regexScript, isScoped); | |
| 510 | + } | |
| 511 | + } else { | |
| 512 | + await onRegexImportObjectChange(regexScripts, isScoped); | |
| 513 | + } | |
| 514 | + } catch (error) { | |
| 515 | + console.log(error); | |
| 494 | 516 | toastr.error('Invalid JSON file.'); |
| 495 | 517 | return; |
| 496 | 518 | } |
| @@ -585,6 +607,69 @@ jQuery(async () => { | ||
| 585 | 607 | $('#import_regex_file').trigger('click'); |
| 586 | 608 | }); |
| 587 | 609 | |
| 610 | + function getSelectedScripts() { | |
| 611 | + const scripts = getRegexScripts(); | |
| 612 | + const selector = '#regex_container .regex-script-label:has(.regex_bulk_checkbox:checked)'; | |
| 613 | + const selectedIds = Array.from(document.querySelectorAll(selector)).map(e => e.getAttribute('id')).filter(id => id); | |
| 614 | + return scripts.filter(script => selectedIds.includes(script.id)); | |
| 615 | + } | |
| 616 | + | |
| 617 | + $('#bulk_enable_regex').on('click', async function () { | |
| 618 | + const scripts = getSelectedScripts().filter(script => script.disabled); | |
| 619 | + if (scripts.length === 0) { | |
| 620 | + toastr.warning(t`No regex scripts selected for enabling.`); | |
| 621 | + return; | |
| 622 | + } | |
| 623 | + for (const script of scripts) { | |
| 624 | + script.disabled = false; | |
| 625 | + } | |
| 626 | + saveSettingsDebounced(); | |
| 627 | + await loadRegexScripts(); | |
| 628 | + }); | |
| 629 | + | |
| 630 | + $('#bulk_disable_regex').on('click', async function () { | |
| 631 | + const scripts = getSelectedScripts().filter(script => !script.disabled); | |
| 632 | + if (scripts.length === 0) { | |
| 633 | + toastr.warning(t`No regex scripts selected for disabling.`); | |
| 634 | + return; | |
| 635 | + } | |
| 636 | + for (const script of scripts) { | |
| 637 | + script.disabled = true; | |
| 638 | + } | |
| 639 | + saveSettingsDebounced(); | |
| 640 | + await loadRegexScripts(); | |
| 641 | + }); | |
| 642 | + | |
| 643 | + $('#bulk_delete_regex').on('click', async function () { | |
| 644 | + const scripts = getSelectedScripts(); | |
| 645 | + if (scripts.length === 0) { | |
| 646 | + toastr.warning(t`No regex scripts selected for deletion.`); | |
| 647 | + return; | |
| 648 | + } | |
| 649 | + const confirm = await callGenericPopup('Are you sure you want to delete the selected regex scripts?', POPUP_TYPE.CONFIRM); | |
| 650 | + if (!confirm) { | |
| 651 | + return; | |
| 652 | + } | |
| 653 | + for (const script of scripts) { | |
| 654 | + const isScoped = characters[this_chid]?.data?.extensions?.regex_scripts?.some(s => s.id === script.id); | |
| 655 | + await deleteRegexScript({ id: script.id, isScoped: isScoped }); | |
| 656 | + } | |
| 657 | + await reloadCurrentChat(); | |
| 658 | + saveSettingsDebounced(); | |
| 659 | + }); | |
| 660 | + | |
| 661 | + $('#bulk_export_regex').on('click', async function () { | |
| 662 | + const scripts = getSelectedScripts(); | |
| 663 | + if (scripts.length === 0) { | |
| 664 | + toastr.warning(t`No regex scripts selected for export.`); | |
| 665 | + return; | |
| 666 | + } | |
| 667 | + const fileName = `regex-${new Date().toISOString()}.json`; | |
| 668 | + const fileData = JSON.stringify(scripts, null, 4); | |
| 669 | + download(fileData, fileName, 'application/json'); | |
| 670 | + await loadRegexScripts(); | |
| 671 | + }); | |
| 672 | + | |
| 588 | 673 | let sortableDatas = [ |
| 589 | 674 | { |
| 590 | 675 | selector: '#saved_regex_scripts', |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | <div class="regex-script-label flex-container flexnowrap"> |
| 2 | + <input type="checkbox" class="regex_bulk_checkbox" /> | |
| 2 | 3 | <span class="drag-handle menu-handle">☰</span> |
| 3 | 4 | <div class="regex_script_name flexGrow overflow-hidden"></div> |
| 4 | 5 | <div class="flex-container flexnowrap"> |
| @@ -56,7 +56,7 @@ | ||
| 56 | 56 | } |
| 57 | 57 | |
| 58 | 58 | .regex-script-label { |
| 59 | 59 | align-items: centerbaseline; |
| 60 | 60 | border: 1px solid var(--SmartThemeBorderColor); |
| 61 | 61 | border-radius: 10px; |
| 62 | 62 | padding: 0 5px; |
| @@ -126,3 +126,25 @@ input.enable_scoped { | ||
| 126 | 126 | right: 10px; |
| 127 | 127 | transform: translateY(-50%); |
| 128 | 128 | } |
| 129 | + | |
| 130 | +.regex_settings label[for="regex_bulk_edit"]:has(#regex_bulk_edit:checked) { | |
| 131 | + color: var(--golden); | |
| 132 | +} | |
| 133 | + | |
| 134 | +.regex_settings .regex-script-container .regex-script-label .regex_bulk_checkbox { | |
| 135 | + margin-left: 5px; | |
| 136 | + margin-right: 5px; | |
| 137 | +} | |
| 138 | + | |
| 139 | +.regex_settings .regex_bulk_operations, | |
| 140 | +.regex_settings .regex_bulk_checkbox { | |
| 141 | + display: none; | |
| 142 | +} | |
| 143 | + | |
| 144 | +.regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_operations { | |
| 145 | + display: flex; | |
| 146 | +} | |
| 147 | + | |
| 148 | +.regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_checkbox { | |
| 149 | + display: inline-grid; | |
| 150 | +} | |