Merge pull request #3768 from bmen25124/move_lorebook_entry Added move button for lorebook entries

74efb598f197e9e31b3d028984a894459af048c4

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
3 files changed, +180 -3Ignore whitespace
public/index.html+1 -0
@@ -5998,6 +5998,7 @@
59985998 </div>
59995999 </div>
60006000 </div>
6001+ <i class="menu_button move_entry_button fa-solid fa-right-left" title="Move Entry to Another Lorebook" data-i18n="[title]Move Entry to Another Lorebook"></i>
60016002 <i class="menu_button duplicate_entry_button fa-solid fa-paste" title="Duplicate world info entry" data-i18n="[title]Duplicate world info entry" type="submit" value=""></i>
60026003 <i class="menu_button delete_entry_button fa-solid fa-trash-can" title="Delete world info entry" data-i18n="[title]Delete world info entry" type="submit" value=""></i>
60036004 </div>
public/scripts/templates/worldInfoKeywordHeaders.html+1 -1
@@ -1,4 +1,4 @@
11<div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 47.5em0em;">
22 <small class="flex1" data-i18n="Title/Memo">Title/Memo</small>
33 <small style="width: calc(3.5em + 10px)" data-i18n="Strategy">Strategy</small>
44 <small style="width: calc(3.5em + 20px)" data-i18n="Position">Position</small>
public/scripts/world-info.js+178 -2
@@ -2208,7 +2208,7 @@ function verifyWorldInfoSearchSortRule() {
22082208 * Use `originalWIDataKeyMap` to find the correct value to be set.
22092209 *
22102210 * @param {object} data - The data object containing the original data entries.
22112211 * @param {stringnumber} uid - The unique identifier of the data entry.
22122212 * @param {string} key - The key of the value to be set.
22132213 * @param {any} value - The value to be set.
22142214 */
@@ -2232,7 +2232,9 @@ export function setWIOriginalDataValue(data, uid, key, value) {
22322232 */
22332233export function deleteWIOriginalDataValue(data, uid) {
22342234 if (data.originalData && Array.isArray(data.originalData.entries)) {
2235- const originalIndex = data.originalData.entries.findIndex(x => x.uid === uid);
2235+ // Non-strict equality is used here to allow for both string and number comparisons
2236+ // @eslint-disable-next-line eqeqeq
2237+ const originalIndex = data.originalData.entries.findIndex(x => x.uid == uid);
22362238
22372239 if (originalIndex >= 0) {
22382240 data.originalData.entries.splice(originalIndex, 1);
@@ -3143,6 +3145,84 @@ export async function getWorldEntry(name, data, entry) {
31433145 updateEditor(navigation_option.previous);
31443146 });
31453147
3148+ // move button
3149+ const moveButton = template.find('.move_entry_button');
3150+ moveButton.attr('data-uid', entry.uid);
3151+ moveButton.attr('data-current-world', name);
3152+ moveButton.on('click', async function (e) {
3153+ e.stopPropagation();
3154+ const sourceUid = $(this).attr('data-uid');
3155+ const sourceWorld = $(this).attr('data-current-world');
3156+ const sourceWorldInfo = await loadWorldInfo(sourceWorld);
3157+ if (!sourceWorldInfo) {
3158+ return;
3159+ }
3160+ const sourceName = sourceWorldInfo.entries[sourceUid]?.comment;
3161+ if (sourceName === undefined) {
3162+ return;
3163+ }
3164+
3165+ const select = document.createElement('select');
3166+ select.id = 'move_entry_target_select';
3167+ select.classList.add('text_pole', 'wide100p', 'marginTop10');
3168+
3169+ const defaultOption = document.createElement('option');
3170+ defaultOption.value = '';
3171+ defaultOption.textContent = `-- ${t`Select Target Lorebook`} --`;
3172+ select.appendChild(defaultOption);
3173+
3174+ let selectableWorldCount = 0;
3175+ world_names.forEach(worldName => {
3176+ if (worldName !== sourceWorld) { // Exclude current world
3177+ const option = document.createElement('option');
3178+ option.value = world_names.indexOf(worldName).toString();
3179+ option.textContent = worldName;
3180+ select.appendChild(option);
3181+ selectableWorldCount++;
3182+ }
3183+ });
3184+
3185+ if (selectableWorldCount === 0) {
3186+ toastr.warning(t`There are no other lorebooks to move to.`);
3187+ return;
3188+ }
3189+
3190+ // Create wrapper div
3191+ const wrapper = document.createElement('div');
3192+ wrapper.textContent = t`Move "${sourceName}" to:`;
3193+
3194+ // Create container and append elements
3195+ const container = document.createElement('div');
3196+ container.appendChild(wrapper);
3197+ container.appendChild(select);
3198+
3199+ let selectedWorldIndex = -1;
3200+ select.addEventListener('change', function() {
3201+ selectedWorldIndex = this.value === '' ? -1 : Number(this.value);
3202+ });
3203+
3204+ const popupConfirm = await callGenericPopup(container, POPUP_TYPE.CONFIRM, '', {
3205+ okButton: t`Move`,
3206+ cancelButton: t`Cancel`,
3207+ });
3208+ if (!popupConfirm) {
3209+ return;
3210+ }
3211+
3212+ if (selectedWorldIndex === -1) {
3213+ return;
3214+ }
3215+
3216+ const selectedValue = world_names[selectedWorldIndex];
3217+
3218+ if (!selectedValue) {
3219+ toastr.warning(t`Please select a target lorebook.`);
3220+ return;
3221+ }
3222+
3223+ await moveWorldInfoEntry(sourceWorld, selectedValue, sourceUid);
3224+ });
3225+
31463226 // scan depth
31473227 const scanDepthInput = template.find('input[name="scanDepth"]');
31483228 scanDepthInput.data('uid', entry.uid);
@@ -5271,3 +5351,99 @@ jQuery(() => {
52715351 });
52725352 });
52735353});
5354+
5355+/**
5356+ * Moves a World Info entry from a source lorebook to a target lorebook.
5357+ *
5358+ * @param {string} sourceName - The name of the source lorebook file.
5359+ * @param {string} targetName - The name of the target lorebook file.
5360+ * @param {string|number} uid - The UID of the entry to move from the source lorebook.
5361+ * @returns {Promise<boolean>} True if the move was successful, false otherwise.
5362+ */
5363+export async function moveWorldInfoEntry(sourceName, targetName, uid) {
5364+ if (sourceName === targetName) {
5365+ return false;
5366+ }
5367+
5368+ if (!world_names.includes(sourceName)) {
5369+ toastr.error(t`Source lorebook '${sourceName}' not found.`);
5370+ console.error(`[WI Move] Source lorebook '${sourceName}' does not exist.`);
5371+ return false;
5372+ }
5373+
5374+ if (!world_names.includes(targetName)) {
5375+ toastr.error(t`Target lorebook '${targetName}' not found.`);
5376+ console.error(`[WI Move] Target lorebook '${targetName}' does not exist.`);
5377+ return false;
5378+ }
5379+
5380+ const entryUidString = String(uid);
5381+
5382+ try {
5383+ const sourceData = await loadWorldInfo(sourceName);
5384+ const targetData = await loadWorldInfo(targetName);
5385+
5386+ if (!sourceData || !sourceData.entries) {
5387+ toastr.error(t`Failed to load data for source lorebook '${sourceName}'.`);
5388+ console.error(`[WI Move] Could not load source data for '${sourceName}'.`);
5389+ return false;
5390+ }
5391+ if (!targetData || !targetData.entries) {
5392+ toastr.error(t`Failed to load data for target lorebook '${targetName}'.`);
5393+ console.error(`[WI Move] Could not load target data for '${targetName}'.`);
5394+ return false;
5395+ }
5396+
5397+ if (!sourceData.entries[entryUidString]) {
5398+ toastr.error(t`Entry not found in source lorebook '${sourceName}'.`);
5399+ console.error(`[WI Move] Entry UID ${entryUidString} not found in '${sourceName}'.`);
5400+ return false;
5401+ }
5402+
5403+ const entryToMove = structuredClone(sourceData.entries[entryUidString]);
5404+
5405+
5406+ const newUid = getFreeWorldEntryUid(targetData);
5407+ if (newUid === null) {
5408+ console.error(`[WI Move] Failed to get a free UID in '${targetName}'.`);
5409+ return false;
5410+ }
5411+
5412+ entryToMove.uid = newUid;
5413+ // Place the entry at the end of the target lorebook
5414+ const maxDisplayIndex = Object.values(targetData.entries).reduce((max, entry) => Math.max(max, entry.displayIndex ?? -1), -1);
5415+ entryToMove.displayIndex = maxDisplayIndex + 1;
5416+
5417+ targetData.entries[newUid] = entryToMove;
5418+
5419+ delete sourceData.entries[entryUidString];
5420+ // Remove from originalData if it exists
5421+ deleteWIOriginalDataValue(sourceData, entryUidString);
5422+ // TODO: setWIOriginalDataValue
5423+ console.debug(`[WI Move] Removed entry UID ${entryUidString} from source '${sourceName}'.`);
5424+
5425+
5426+ await saveWorldInfo(targetName, targetData, true);
5427+ console.debug(`[WI Move] Saved target lorebook '${targetName}'.`);
5428+ await saveWorldInfo(sourceName, sourceData, true);
5429+ console.debug(`[WI Move] Saved source lorebook '${sourceName}'.`);
5430+
5431+
5432+ console.log(`[WI Move] ${entryToMove.comment} moved successfully to '${targetName}'.`);
5433+
5434+ // Check if the currently viewed book in the editor is the source or target and reload it
5435+ const currentEditorBookIndex = Number($('#world_editor_select').val());
5436+ if (!isNaN(currentEditorBookIndex)) {
5437+ const currentEditorBookName = world_names[currentEditorBookIndex];
5438+ if (currentEditorBookName === sourceName || currentEditorBookName === targetName) {
5439+ reloadEditor(currentEditorBookName);
5440+ }
5441+ }
5442+
5443+ return true;
5444+ } catch (error) {
5445+ toastr.error(t`An unexpected error occurred while moving the entry: ${error.message}`);
5446+ console.error('[WI Move] Unexpected error:', error);
5447+ return false;
5448+ }
5449+}