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 @@
5998 </div>5998 </div>
5999 </div>5999 </div>
6000 </div>6000 </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>
6001 <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>6002 <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>
6002 <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>6003 <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>
6003 </div>6004 </div>
public/scripts/templates/worldInfoKeywordHeaders.html+1 -1
@@ -1,4 +1,4 @@
1<div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 4.5em;">1<div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 7.0em;">
2 <small class="flex1" data-i18n="Title/Memo">Title/Memo</small>2 <small class="flex1" data-i18n="Title/Memo">Title/Memo</small>
3 <small style="width: calc(3.5em + 10px)" data-i18n="Strategy">Strategy</small>3 <small style="width: calc(3.5em + 10px)" data-i18n="Strategy">Strategy</small>
4 <small style="width: calc(3.5em + 20px)" data-i18n="Position">Position</small>4 <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() {
2208 * Use `originalWIDataKeyMap` to find the correct value to be set.2208 * Use `originalWIDataKeyMap` to find the correct value to be set.
2209 *2209 *
2210 * @param {object} data - The data object containing the original data entries.2210 * @param {object} data - The data object containing the original data entries.
2211 * @param {string} uid - The unique identifier of the data entry.2211 * @param {number} uid - The unique identifier of the data entry.
2212 * @param {string} key - The key of the value to be set.2212 * @param {string} key - The key of the value to be set.
2213 * @param {any} value - The value to be set.2213 * @param {any} value - The value to be set.
2214 */2214 */
@@ -2232,7 +2232,9 @@ export function setWIOriginalDataValue(data, uid, key, value) {
2232 */2232 */
2233export function deleteWIOriginalDataValue(data, uid) {2233export function deleteWIOriginalDataValue(data, uid) {
2234 if (data.originalData && Array.isArray(data.originalData.entries)) {2234 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
2237 if (originalIndex >= 0) {2239 if (originalIndex >= 0) {
2238 data.originalData.entries.splice(originalIndex, 1);2240 data.originalData.entries.splice(originalIndex, 1);
@@ -3143,6 +3145,84 @@ export async function getWorldEntry(name, data, entry) {
3143 updateEditor(navigation_option.previous);3145 updateEditor(navigation_option.previous);
3144 });3146 });
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
3146 // scan depth3226 // scan depth
3147 const scanDepthInput = template.find('input[name="scanDepth"]');3227 const scanDepthInput = template.find('input[name="scanDepth"]');
3148 scanDepthInput.data('uid', entry.uid);3228 scanDepthInput.data('uid', entry.uid);
@@ -5271,3 +5351,99 @@ jQuery(() => {
5271 });5351 });
5272 });5352 });
5273});5353});
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 */
5363export 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}