Added move button for lorebook entries

f14c73bfccd10d9edb7f50c05e5146292dca2465

bmen25124 <bmen25124@gmail.com>

2 files changed, +172 -0Showing whitespace changes
public/index.html+1 -0
@@ -6013,6 +6013,7 @@
6013 </div>6013 </div>
6014 </div>6014 </div>
6015 </div>6015 </div>
6016 <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" type="button"></i>
6016 <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>6017 <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>
6017 <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>6018 <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>
6018 </div>6019 </div>
public/scripts/world-info.js+171 -0
@@ -3139,6 +3139,70 @@ export async function getWorldEntry(name, data, entry) {
3139 updateEditor(navigation_option.previous);3139 updateEditor(navigation_option.previous);
3140 });3140 });
31413141
3142 // move button
3143 const moveButton = template.find('.move_entry_button');
3144 moveButton.data('uid', entry.uid);
3145 moveButton.data('current-world', name);
3146 moveButton.on('click', async function (e) {
3147 e.stopPropagation();
3148 const sourceUid = $(this).data('uid');
3149 const sourceWorld = $(this).data('current-world');
3150 // Loading world info is bad, do we have cache variable?
3151 const sourceName = (await loadWorldInfo(sourceWorld)).entries[sourceUid].comment;
3152
3153 let optionsHtml = `<option value="">-- ${t`Select Target Lorebook`} --</option>`;
3154 let selectableWorldCount = 0;
3155 world_names.forEach(worldName => {
3156 if (worldName !== sourceWorld) { // Exclude the current world
3157 optionsHtml += `<option value="${world_names.indexOf(worldName)}">${worldName}</option>`;
3158 selectableWorldCount += 1;
3159 }
3160 });
3161
3162 if (selectableWorldCount === 0) {
3163 toastr.warning(t`There are no other lorebooks to move to.`);
3164 return;
3165 }
3166
3167 const content = `
3168 <div>${t`Move ${sourceName} to:`}</div>
3169 <select id="move_entry_target_select" class="text_pole wide100p margin-top">
3170 ${optionsHtml}
3171 </select>
3172 `;
3173
3174 const popupPromise = callGenericPopup(content, POPUP_TYPE.CONFIRM, '', {
3175 okButton: t`Move`,
3176 cancelButton: t`Cancel`,
3177 });
3178
3179 let selectedWorldIndex = -1;
3180 $('#move_entry_target_select').on('change', function () {
3181 /** @type {string} */
3182 // @ts-ignore
3183 const value = $(this).val();
3184 selectedWorldIndex = value === '' ? -1 : Number(value);
3185 });
3186
3187 const popupConfirm = await popupPromise;
3188 if (!popupConfirm) {
3189 return;
3190 }
3191
3192 if (selectedWorldIndex === -1) {
3193 return;
3194 }
3195
3196 const selectedValue = world_names[selectedWorldIndex];
3197
3198 if (!selectedValue) {
3199 toastr.warning(t`Please select a target lorebook.`);
3200 return;
3201 }
3202
3203 await moveWorldInfoEntry(sourceWorld, selectedValue, sourceUid);
3204 });
3205
3142 // scan depth3206 // scan depth
3143 const scanDepthInput = template.find('input[name="scanDepth"]');3207 const scanDepthInput = template.find('input[name="scanDepth"]');
3144 scanDepthInput.data('uid', entry.uid);3208 scanDepthInput.data('uid', entry.uid);
@@ -5267,3 +5331,110 @@ jQuery(() => {
5267 });5331 });
5268 });5332 });
5269});5333});
5334
5335/**
5336 * Moves a World Info entry from a source lorebook to a target lorebook.
5337 *
5338 * @param {string} sourceName - The name of the source lorebook file.
5339 * @param {string} targetName - The name of the target lorebook file.
5340 * @param {number|string} uid - The UID of the entry to move from the source lorebook.
5341 * @returns {Promise<boolean>} True if the move was successful, false otherwise.
5342 */
5343export async function moveWorldInfoEntry(sourceName, targetName, uid) {
5344 console.log(`[WI] Attempting to move entry UID ${uid} from '${sourceName}' to '${targetName}'`);
5345
5346 if (!sourceName || !targetName || uid === undefined || uid === null) {
5347 console.error('[WI Move] Missing required arguments.');
5348 return false;
5349 }
5350
5351 if (sourceName === targetName) {
5352 toastr.warning(t`Source and target lorebooks cannot be the same.`);
5353 return false;
5354 }
5355
5356 if (!world_names.includes(sourceName)) {
5357 toastr.error(t`Source lorebook '${sourceName}' not found.`);
5358 console.error(`[WI Move] Source lorebook '${sourceName}' does not exist.`);
5359 return false;
5360 }
5361
5362 if (!world_names.includes(targetName)) {
5363 toastr.error(t`Target lorebook '${targetName}' not found.`);
5364 console.error(`[WI Move] Target lorebook '${targetName}' does not exist.`);
5365 return false;
5366 }
5367
5368 const entryUidString = String(uid);
5369
5370 try {
5371 const sourceData = await loadWorldInfo(sourceName);
5372 const targetData = await loadWorldInfo(targetName);
5373
5374 if (!sourceData || !sourceData.entries) {
5375 toastr.error(t`Failed to load data for source lorebook '${sourceName}'.`);
5376 console.error(`[WI Move] Could not load source data for '${sourceName}'.`);
5377 return false;
5378 }
5379 if (!targetData || !targetData.entries) {
5380 toastr.error(t`Failed to load data for target lorebook '${targetName}'.`);
5381 console.error(`[WI Move] Could not load target data for '${targetName}'.`);
5382 return false;
5383 }
5384
5385 if (!sourceData.entries[entryUidString]) {
5386 toastr.error(t`Entry not found in source lorebook '${sourceName}'.`);
5387 console.error(`[WI Move] Entry UID ${entryUidString} not found in '${sourceName}'.`);
5388 return false;
5389 }
5390
5391 const entryToMove = structuredClone(sourceData.entries[entryUidString]);
5392
5393
5394 const newUid = getFreeWorldEntryUid(targetData);
5395 if (newUid === null) {
5396 console.error(`[WI Move] Failed to get a free UID in '${targetName}'.`);
5397 return false;
5398 }
5399
5400 entryToMove.uid = newUid;
5401 // Reset displayIndex or let it be recalculated based on target book's sorting?
5402 // For simplicity, let's assign a high index initially, assuming it might be sorted later.
5403 // Or maybe better, find the max displayIndex in target and add 1?
5404 const maxDisplayIndex = Object.values(targetData.entries).reduce((max, entry) => Math.max(max, entry.displayIndex ?? -1), -1);
5405 entryToMove.displayIndex = maxDisplayIndex + 1;
5406
5407 targetData.entries[newUid] = entryToMove;
5408
5409 delete sourceData.entries[entryUidString];
5410 // Remove from originalData if it exists, using the original UID
5411 deleteWIOriginalDataValue(sourceData, entryUidString);
5412 console.debug(`[WI Move] Removed entry UID ${entryUidString} from source '${sourceName}'.`);
5413
5414
5415 // Save immediately to reduce chances of inconsistency if the browser is closed
5416 // Note: This is not truly atomic. If one save fails, state could be inconsistent.
5417 await saveWorldInfo(targetName, targetData, true);
5418 console.debug(`[WI Move] Saved target lorebook '${targetName}'.`);
5419 await saveWorldInfo(sourceName, sourceData, true);
5420 console.debug(`[WI Move] Saved source lorebook '${sourceName}'.`);
5421
5422
5423 toastr.success(t`${entryToMove.comment} moved successfully!`);
5424
5425 // Check if the currently viewed book in the editor is the source or target and reload it
5426 const currentEditorBookIndex = Number($('#world_editor_select').val());
5427 if (!isNaN(currentEditorBookIndex)) {
5428 const currentEditorBookName = world_names[currentEditorBookIndex];
5429 if (currentEditorBookName === sourceName || currentEditorBookName === targetName) {
5430 reloadEditor(currentEditorBookName);
5431 }
5432 }
5433
5434 return true;
5435 } catch (error) {
5436 toastr.error(t`An unexpected error occurred while moving the entry: ${error.message}`);
5437 console.error('[WI Move] Unexpected error:', error);
5438 return false;
5439 }
5440}