Merge pull request #3967 from gakada/regex Regex: allow multiple definitions in a single file
Signed| @@ -21,6 +21,29 @@ | |||
| 21 | <small data-i18n="ext_regex_import_script">Import</small> | 21 | <small data-i18n="ext_regex_import_script">Import</small> |
| 22 | </div> | 22 | </div> |
| 23 | <input type="file" id="import_regex_file" hidden accept="*.json" multiple /> | 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 | </div> | 47 | </div> |
| 25 | <hr /> | 48 | <hr /> |
| 26 | <div id="global_scripts_block" class="padding5"> | 49 | <div id="global_scripts_block" class="padding5"> |
| @@ -12,6 +12,8 @@ import { regex_placement, runRegexScript, substitute_find_regex } from './engine | |||
| 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 | ||
| 15 | const sanitizeFileName = name => name.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, '_').toLowerCase(); | ||
| 16 | |||
| 15 | /** | 17 | /** |
| 16 | * @typedef {import('../../char-data.js').RegexScriptData} RegexScript | 18 | * @typedef {import('../../char-data.js').RegexScriptData} RegexScript |
| 17 | */ | 19 | */ |
| @@ -167,7 +169,7 @@ async function loadRegexScripts() { | |||
| 167 | await saveRegexScript(script, -1, true); | 169 | await saveRegexScript(script, -1, true); |
| 168 | }); | 170 | }); |
| 169 | scriptHtml.find('.export_regex').on('click', async function () { | 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 | const fileData = JSON.stringify(script, null, 4); | 173 | const fileData = JSON.stringify(script, null, 4); |
| 172 | download(fileData, fileName, 'application/json'); | 174 | download(fileData, fileName, 'application/json'); |
| 173 | }); | 175 | }); |
| @@ -459,19 +461,12 @@ async function toggleRegexCallback(args, scriptName) { | |||
| 459 | } | 461 | } |
| 460 | 462 | ||
| 461 | /** | 463 | /** |
| 462 | * Performs the import of the regex file. | 464 | * Performs the import of the regex object. |
| 463 | * @param {File} file Input file | 465 | * @param {Object} regexScript Input object |
| 464 | * @param {boolean} isScoped Is the script scoped to a character? | 466 | * @param {boolean} isScoped Is the script scoped to a character? |
| 465 | */ | 467 | */ |
| 466 | async function onRegexImportFileChange(file, isScoped) { | 468 | async function onRegexImportObjectChange(regexScript, isScoped) { |
| 467 | if (!file) { | ||
| 468 | toastr.error('No file provided.'); | ||
| 469 | return; | ||
| 470 | } | ||
| 471 | |||
| 472 | try { | 469 | try { |
| 473 | const fileText = await getFileText(file); | ||
| 474 | const regexScript = JSON.parse(fileText); | ||
| 475 | if (!regexScript.scriptName) { | 470 | if (!regexScript.scriptName) { |
| 476 | throw new Error('No script name provided.'); | 471 | throw new Error('No script name provided.'); |
| 477 | } | 472 | } |
| @@ -491,6 +486,33 @@ async function onRegexImportFileChange(file, isScoped) { | |||
| 491 | toastr.success(`Regex script "${regexScript.scriptName}" imported.`); | 486 | toastr.success(`Regex script "${regexScript.scriptName}" imported.`); |
| 492 | } catch (error) { | 487 | } catch (error) { |
| 493 | console.log(error); | 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 | toastr.error('Invalid JSON file.'); | 516 | toastr.error('Invalid JSON file.'); |
| 495 | return; | 517 | return; |
| 496 | } | 518 | } |
| @@ -585,6 +607,69 @@ jQuery(async () => { | |||
| 585 | $('#import_regex_file').trigger('click'); | 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 | let sortableDatas = [ | 673 | let sortableDatas = [ |
| 589 | { | 674 | { |
| 590 | selector: '#saved_regex_scripts', | 675 | selector: '#saved_regex_scripts', |
| @@ -1,4 +1,5 @@ | |||
| 1 | <div class="regex-script-label flex-container flexnowrap"> | 1 | <div class="regex-script-label flex-container flexnowrap"> |
| 2 | <input type="checkbox" class="regex_bulk_checkbox" /> | ||
| 2 | <span class="drag-handle menu-handle">☰</span> | 3 | <span class="drag-handle menu-handle">☰</span> |
| 3 | <div class="regex_script_name flexGrow overflow-hidden"></div> | 4 | <div class="regex_script_name flexGrow overflow-hidden"></div> |
| 4 | <div class="flex-container flexnowrap"> | 5 | <div class="flex-container flexnowrap"> |
| @@ -56,7 +56,7 @@ | |||
| 56 | } | 56 | } |
| 57 | 57 | ||
| 58 | .regex-script-label { | 58 | .regex-script-label { |
| 59 | align-items: center; | 59 | align-items: baseline; |
| 60 | border: 1px solid var(--SmartThemeBorderColor); | 60 | border: 1px solid var(--SmartThemeBorderColor); |
| 61 | border-radius: 10px; | 61 | border-radius: 10px; |
| 62 | padding: 0 5px; | 62 | padding: 0 5px; |
| @@ -126,3 +126,25 @@ input.enable_scoped { | |||
| 126 | right: 10px; | 126 | right: 10px; |
| 127 | transform: translateY(-50%); | 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 | } | ||