Background sort feature (#5107) * Add background sort by date added feature * reapply search after changing sort * overengineer it * use sortIgnoreCaseAndAccents * make enum for sort options
Signed| @@ -96,6 +96,12 @@ | ||
| 96 | 96 | font-size: calc(var(--mainFontSize) * 0.95); |
| 97 | 97 | } |
| 98 | 98 | |
| 99 | +#bg-sort { | |
| 100 | + width: auto; | |
| 101 | + max-width: 6em; | |
| 102 | + flex-shrink: 0; | |
| 103 | +} | |
| 104 | + | |
| 99 | 105 | /* Thumbnails */ |
| 100 | 106 | .bg_example:hover .BGSampleTitle { |
| 101 | 107 | opacity: 1; |
| @@ -5563,6 +5563,12 @@ | ||
| 5563 | 5563 | </div> |
| 5564 | 5564 | <div class="bg-header-row-2"> |
| 5565 | 5565 | <input id="bg-filter" class="text_pole" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." /> |
| 5566 | + <select id="bg-sort" class="text_pole margin0" title="Sort backgrounds" data-i18n="[title]Sort backgrounds"> | |
| 5567 | + <option value="az" data-i18n="A-Z">A-Z</option> | |
| 5568 | + <option value="za" data-i18n="Z-A">Z-A</option> | |
| 5569 | + <option value="newest" data-i18n="Newest">Newest</option> | |
| 5570 | + <option value="oldest" data-i18n="Oldest">Oldest</option> | |
| 5571 | + </select> | |
| 5566 | 5572 | </div> |
| 5567 | 5573 | </div> |
| 5568 | 5574 | <div id="bg_tabs" class="heading-container-with-controls"> |
| @@ -3,7 +3,7 @@ import { characters, chat_metadata, eventSource, event_types, generateQuietPromp | ||
| 3 | 3 | import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js'; |
| 4 | 4 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 5 | 5 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 6 | 6 | import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop, saveBase64AsFile, getFileExtension, sortIgnoreCaseAndAccents } from './utils.js'; |
| 7 | 7 | import { debounce_timeout } from './constants.js'; |
| 8 | 8 | import { t } from './i18n.js'; |
| 9 | 9 | import { Popup } from './popup.js'; |
| @@ -58,6 +58,18 @@ const BG_SOURCES = { | ||
| 58 | 58 | }; |
| 59 | 59 | |
| 60 | 60 | /** |
| 61 | + * Background sorting options. | |
| 62 | + * @readonly | |
| 63 | + * @enum {string} | |
| 64 | + */ | |
| 65 | +const BG_SORT_OPTIONS = { | |
| 66 | + AZ: 'az', | |
| 67 | + ZA: 'za', | |
| 68 | + NEWEST: 'newest', | |
| 69 | + OLDEST: 'oldest', | |
| 70 | +}; | |
| 71 | + | |
| 72 | +/** | |
| 61 | 73 | * Mapping of background sources to their corresponding tab IDs. |
| 62 | 74 | * @readonly |
| 63 | 75 | * @type {Record<string, string>} |
| @@ -73,14 +85,56 @@ const BG_TABS = Object.freeze({ | ||
| 73 | 85 | */ |
| 74 | 86 | let lazyLoadObserver = null; |
| 75 | 87 | |
| 88 | +/** | |
| 89 | + * Cache for the current list of system background filenames. | |
| 90 | + * Used to re-sort backgrounds without refetching from the server. | |
| 91 | + * @type {string[]} | |
| 92 | + */ | |
| 93 | +let cachedSystemBackgrounds = []; | |
| 94 | + | |
| 76 | 95 | export let background_settings = { |
| 77 | 96 | name: '__transparent.png', |
| 78 | 97 | url: generateUrlParameter('__transparent.png', false), |
| 79 | 98 | fitting: 'classic', |
| 80 | 99 | animation: false, |
| 100 | + sortOrder: BG_SORT_OPTIONS.AZ, | |
| 81 | 101 | }; |
| 82 | 102 | |
| 83 | 103 | /** |
| 104 | + * Sorts an array of background filenames based on the current sort order. | |
| 105 | + * @param {string[]} backgrounds - Array of background filenames | |
| 106 | + * @param {boolean} isCustom - Whether these are custom (chat) backgrounds | |
| 107 | + * @returns {string[]} Sorted array of background filenames | |
| 108 | + */ | |
| 109 | +function sortBackgrounds(backgrounds, isCustom = false) { | |
| 110 | + const sortOrder = background_settings.sortOrder || BG_SORT_OPTIONS.AZ; | |
| 111 | + | |
| 112 | + return [...backgrounds].sort((a, b) => { | |
| 113 | + switch (sortOrder) { | |
| 114 | + case BG_SORT_OPTIONS.AZ: | |
| 115 | + return sortIgnoreCaseAndAccents(a, b); | |
| 116 | + case BG_SORT_OPTIONS.ZA: | |
| 117 | + return sortIgnoreCaseAndAccents(b, a); | |
| 118 | + case BG_SORT_OPTIONS.NEWEST: | |
| 119 | + case BG_SORT_OPTIONS.OLDEST: { | |
| 120 | + const keyA = isCustom ? a : `backgrounds/${a}`; | |
| 121 | + const keyB = isCustom ? b : `backgrounds/${b}`; | |
| 122 | + const metaA = METADATA_CACHE.get(keyA); | |
| 123 | + const metaB = METADATA_CACHE.get(keyB); | |
| 124 | + const timestampA = metaA?.addedTimestamp ?? 0; | |
| 125 | + const timestampB = metaB?.addedTimestamp ?? 0; | |
| 126 | + // Newest first (descending) or oldest first (ascending) | |
| 127 | + return sortOrder === BG_SORT_OPTIONS.NEWEST | |
| 128 | + ? timestampB - timestampA | |
| 129 | + : timestampA - timestampB; | |
| 130 | + } | |
| 131 | + default: | |
| 132 | + return 0; | |
| 133 | + } | |
| 134 | + }); | |
| 135 | +} | |
| 136 | + | |
| 137 | +/** | |
| 84 | 138 | * Creates a single thumbnail DOM element. The CSS now handles all sizing. |
| 85 | 139 | * @param {object} imageData - Data for the image (filename, isCustom). |
| 86 | 140 | * @returns {HTMLElement} The created thumbnail element. |
| @@ -150,6 +204,9 @@ export function loadBackgroundSettings(settings) { | ||
| 150 | 204 | if (!Object.hasOwn(backgroundSettings, 'animation')) { |
| 151 | 205 | backgroundSettings.animation = false; |
| 152 | 206 | } |
| 207 | + if (!backgroundSettings.sortOrder) { | |
| 208 | + backgroundSettings.sortOrder = BG_SORT_OPTIONS.AZ; | |
| 209 | + } | |
| 153 | 210 | |
| 154 | 211 | // If a value is already saved, use it. Otherwise, determine default based on screen size. |
| 155 | 212 | let columns = backgroundSettings.thumbnailColumns; |
| @@ -158,12 +215,14 @@ export function loadBackgroundSettings(settings) { | ||
| 158 | 215 | columns = isNarrowScreen ? THUMBNAIL_COLUMNS_DEFAULT_MOBILE : THUMBNAIL_COLUMNS_DEFAULT_DESKTOP; |
| 159 | 216 | } |
| 160 | 217 | background_settings.thumbnailColumns = columns; |
| 218 | + background_settings.sortOrder = backgroundSettings.sortOrder; | |
| 161 | 219 | applyThumbnailColumns(background_settings.thumbnailColumns); |
| 162 | 220 | |
| 163 | 221 | setBackground(backgroundSettings.name, backgroundSettings.url); |
| 164 | 222 | setFittingClass(backgroundSettings.fitting); |
| 165 | 223 | $('#background_fitting').val(backgroundSettings.fitting); |
| 166 | 224 | $('#background_thumbnails_animation').prop('checked', background_settings.animation); |
| 225 | + $('#bg-sort').val(background_settings.sortOrder); | |
| 167 | 226 | highlightSelectedBackground(); |
| 168 | 227 | } |
| 169 | 228 | |
| @@ -447,6 +506,11 @@ async function onDeleteBackgroundClick(e) { | ||
| 447 | 506 | // If it's not custom, it's a built-in background. Delete it from the server |
| 448 | 507 | if (!isCustom) { |
| 449 | 508 | await delBackground(bg); |
| 509 | + // Remove from cache to prevent reappearing on sort change | |
| 510 | + const cacheIndex = cachedSystemBackgrounds.indexOf(bg); | |
| 511 | + if (cacheIndex !== -1) { | |
| 512 | + cachedSystemBackgrounds.splice(cacheIndex, 1); | |
| 513 | + } | |
| 450 | 514 | } else { |
| 451 | 515 | const list = chat_metadata[LIST_METADATA_KEY] || []; |
| 452 | 516 | const index = list.indexOf(bg); |
| @@ -535,7 +599,8 @@ function renderSystemBackgrounds(backgrounds) { | ||
| 535 | 599 | |
| 536 | 600 | if (sourceList.length === 0) return; |
| 537 | 601 | |
| 538 | - sourceList.forEach(bg => { | |
| 602 | + const sortedList = sortBackgrounds(sourceList, false); | |
| 603 | + sortedList.forEach(bg => { | |
| 539 | 604 | const imageData = { filename: bg, isCustom: false }; |
| 540 | 605 | const thumbnail = createThumbnailElement(imageData); |
| 541 | 606 | container.append(thumbnail); |
| @@ -556,7 +621,8 @@ function renderChatBackgrounds(backgrounds) { | ||
| 556 | 621 | |
| 557 | 622 | if (sourceList.length === 0) return; |
| 558 | 623 | |
| 559 | - sourceList.forEach(bg => { | |
| 624 | + const sortedList = sortBackgrounds(sourceList, true); | |
| 625 | + sortedList.forEach(bg => { | |
| 560 | 626 | const imageData = { filename: bg, isCustom: true }; |
| 561 | 627 | const thumbnail = createThumbnailElement(imageData); |
| 562 | 628 | container.append(thumbnail); |
| @@ -577,6 +643,8 @@ export async function getBackgrounds() { | ||
| 577 | 643 | const { images, config } = await response.json(); |
| 578 | 644 | Object.assign(THUMBNAIL_CONFIG, config); |
| 579 | 645 | |
| 646 | + cachedSystemBackgrounds = images; | |
| 647 | + | |
| 580 | 648 | await metadataPromise; |
| 581 | 649 | |
| 582 | 650 | renderSystemBackgrounds(images); |
| @@ -968,6 +1036,17 @@ export function initBackgrounds() { | ||
| 968 | 1036 | $('#auto_background').on('click', autoBackgroundCommand); |
| 969 | 1037 | $('#add_bg_button').on('change', (e) => onBackgroundUploadSelected(e.originalEvent)); |
| 970 | 1038 | $('#bg-filter').on('input', () => debouncedOnBackgroundFilterInput()); |
| 1039 | + $('#bg-sort').on('change', function () { | |
| 1040 | + background_settings.sortOrder = String($(this).val()); | |
| 1041 | + saveSettingsDebounced(); | |
| 1042 | + // Re-render both galleries with new sort order | |
| 1043 | + renderSystemBackgrounds(cachedSystemBackgrounds); | |
| 1044 | + renderChatBackgrounds(); | |
| 1045 | + highlightSelectedBackground(); | |
| 1046 | + highlightLockedBackground(); | |
| 1047 | + // Re-apply any active search filter | |
| 1048 | + onBackgroundFilterInput(); | |
| 1049 | + }); | |
| 971 | 1050 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 972 | 1051 | name: 'lockbg', |
| 973 | 1052 | callback: () => { |