Merge pull request #3768 from bmen25124/move_lorebook_entry Added move button for lorebook entries
Signed| @@ -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> |
| @@ -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> |
| @@ -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 | */ |
| 2233 | export function deleteWIOriginalDataValue(data, uid) { | 2233 | export 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); | ||
| 2236 | 2238 | ||
| 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 | }); |
| 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 | // scan depth | 3226 | // 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 | */ | ||
| 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 | } | ||