Merge pull request #3768 from bmen25124/move_lorebook_entry Added move button for lorebook entries
Signed| @@ -5998,6 +5998,7 @@ | ||
| 5998 | 5998 | </div> |
| 5999 | 5999 | </div> |
| 6000 | 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 | 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 | 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 | 6004 | </div> |
| @@ -1,4 +1,4 @@ | ||
| 1 | 1 | <div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 47.5em0em;"> |
| 2 | 2 | <small class="flex1" data-i18n="Title/Memo">Title/Memo</small> |
| 3 | 3 | <small style="width: calc(3.5em + 10px)" data-i18n="Strategy">Strategy</small> |
| 4 | 4 | <small style="width: calc(3.5em + 20px)" data-i18n="Position">Position</small> |
| @@ -2208,7 +2208,7 @@ function verifyWorldInfoSearchSortRule() { | ||
| 2208 | 2208 | * Use `originalWIDataKeyMap` to find the correct value to be set. |
| 2209 | 2209 | * |
| 2210 | 2210 | * @param {object} data - The data object containing the original data entries. |
| 2211 | 2211 | * @param {stringnumber} uid - The unique identifier of the data entry. |
| 2212 | 2212 | * @param {string} key - The key of the value to be set. |
| 2213 | 2213 | * @param {any} value - The value to be set. |
| 2214 | 2214 | */ |
| @@ -2232,7 +2232,9 @@ export function setWIOriginalDataValue(data, uid, key, value) { | ||
| 2232 | 2232 | */ |
| 2233 | 2233 | export function deleteWIOriginalDataValue(data, uid) { |
| 2234 | 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); | |
| 2236 | 2238 | |
| 2237 | 2239 | if (originalIndex >= 0) { |
| 2238 | 2240 | data.originalData.entries.splice(originalIndex, 1); |
| @@ -3143,6 +3145,84 @@ export async function getWorldEntry(name, data, entry) { | ||
| 3143 | 3145 | updateEditor(navigation_option.previous); |
| 3144 | 3146 | }); |
| 3145 | 3147 | |
| 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 | 3226 | // scan depth |
| 3147 | 3227 | const scanDepthInput = template.find('input[name="scanDepth"]'); |
| 3148 | 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 | + */ | |
| 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 | +} | |