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 | font-size: calc(var(--mainFontSize) * 0.95); | 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 | /* Thumbnails */ | 105 | /* Thumbnails */ |
| 100 | .bg_example:hover .BGSampleTitle { | 106 | .bg_example:hover .BGSampleTitle { |
| 101 | opacity: 1; | 107 | opacity: 1; |
| @@ -5563,6 +5563,12 @@ | |||
| 5563 | </div> | 5563 | </div> |
| 5564 | <div class="bg-header-row-2"> | 5564 | <div class="bg-header-row-2"> |
| 5565 | <input id="bg-filter" class="text_pole" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." /> | 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 | </div> | 5572 | </div> |
| 5567 | </div> | 5573 | </div> |
| 5568 | <div id="bg_tabs" class="heading-container-with-controls"> | 5574 | <div id="bg_tabs" class="heading-container-with-controls"> |
| @@ -3,7 +3,7 @@ import { characters, chat_metadata, eventSource, event_types, generateQuietPromp | |||
| 3 | import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js'; | 3 | import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js'; |
| 4 | import { SlashCommand } from './slash-commands/SlashCommand.js'; | 4 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 5 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | 5 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 6 | import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop, saveBase64AsFile, getFileExtension } from './utils.js'; | 6 | import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop, saveBase64AsFile, getFileExtension, sortIgnoreCaseAndAccents } from './utils.js'; |
| 7 | import { debounce_timeout } from './constants.js'; | 7 | import { debounce_timeout } from './constants.js'; |
| 8 | import { t } from './i18n.js'; | 8 | import { t } from './i18n.js'; |
| 9 | import { Popup } from './popup.js'; | 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 | * Mapping of background sources to their corresponding tab IDs. | 73 | * Mapping of background sources to their corresponding tab IDs. |
| 62 | * @readonly | 74 | * @readonly |
| 63 | * @type {Record<string, string>} | 75 | * @type {Record<string, string>} |
| @@ -73,14 +85,56 @@ const BG_TABS = Object.freeze({ | |||
| 73 | */ | 85 | */ |
| 74 | let lazyLoadObserver = null; | 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 | export let background_settings = { | 95 | export let background_settings = { |
| 77 | name: '__transparent.png', | 96 | name: '__transparent.png', |
| 78 | url: generateUrlParameter('__transparent.png', false), | 97 | url: generateUrlParameter('__transparent.png', false), |
| 79 | fitting: 'classic', | 98 | fitting: 'classic', |
| 80 | animation: false, | 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 | * Creates a single thumbnail DOM element. The CSS now handles all sizing. | 138 | * Creates a single thumbnail DOM element. The CSS now handles all sizing. |
| 85 | * @param {object} imageData - Data for the image (filename, isCustom). | 139 | * @param {object} imageData - Data for the image (filename, isCustom). |
| 86 | * @returns {HTMLElement} The created thumbnail element. | 140 | * @returns {HTMLElement} The created thumbnail element. |
| @@ -150,6 +204,9 @@ export function loadBackgroundSettings(settings) { | |||
| 150 | if (!Object.hasOwn(backgroundSettings, 'animation')) { | 204 | if (!Object.hasOwn(backgroundSettings, 'animation')) { |
| 151 | backgroundSettings.animation = false; | 205 | backgroundSettings.animation = false; |
| 152 | } | 206 | } |
| 207 | if (!backgroundSettings.sortOrder) { | ||
| 208 | backgroundSettings.sortOrder = BG_SORT_OPTIONS.AZ; | ||
| 209 | } | ||
| 153 | 210 | ||
| 154 | // If a value is already saved, use it. Otherwise, determine default based on screen size. | 211 | // If a value is already saved, use it. Otherwise, determine default based on screen size. |
| 155 | let columns = backgroundSettings.thumbnailColumns; | 212 | let columns = backgroundSettings.thumbnailColumns; |
| @@ -158,12 +215,14 @@ export function loadBackgroundSettings(settings) { | |||
| 158 | columns = isNarrowScreen ? THUMBNAIL_COLUMNS_DEFAULT_MOBILE : THUMBNAIL_COLUMNS_DEFAULT_DESKTOP; | 215 | columns = isNarrowScreen ? THUMBNAIL_COLUMNS_DEFAULT_MOBILE : THUMBNAIL_COLUMNS_DEFAULT_DESKTOP; |
| 159 | } | 216 | } |
| 160 | background_settings.thumbnailColumns = columns; | 217 | background_settings.thumbnailColumns = columns; |
| 218 | background_settings.sortOrder = backgroundSettings.sortOrder; | ||
| 161 | applyThumbnailColumns(background_settings.thumbnailColumns); | 219 | applyThumbnailColumns(background_settings.thumbnailColumns); |
| 162 | 220 | ||
| 163 | setBackground(backgroundSettings.name, backgroundSettings.url); | 221 | setBackground(backgroundSettings.name, backgroundSettings.url); |
| 164 | setFittingClass(backgroundSettings.fitting); | 222 | setFittingClass(backgroundSettings.fitting); |
| 165 | $('#background_fitting').val(backgroundSettings.fitting); | 223 | $('#background_fitting').val(backgroundSettings.fitting); |
| 166 | $('#background_thumbnails_animation').prop('checked', background_settings.animation); | 224 | $('#background_thumbnails_animation').prop('checked', background_settings.animation); |
| 225 | $('#bg-sort').val(background_settings.sortOrder); | ||
| 167 | highlightSelectedBackground(); | 226 | highlightSelectedBackground(); |
| 168 | } | 227 | } |
| 169 | 228 | ||
| @@ -447,6 +506,11 @@ async function onDeleteBackgroundClick(e) { | |||
| 447 | // If it's not custom, it's a built-in background. Delete it from the server | 506 | // If it's not custom, it's a built-in background. Delete it from the server |
| 448 | if (!isCustom) { | 507 | if (!isCustom) { |
| 449 | await delBackground(bg); | 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 | } else { | 514 | } else { |
| 451 | const list = chat_metadata[LIST_METADATA_KEY] || []; | 515 | const list = chat_metadata[LIST_METADATA_KEY] || []; |
| 452 | const index = list.indexOf(bg); | 516 | const index = list.indexOf(bg); |
| @@ -535,7 +599,8 @@ function renderSystemBackgrounds(backgrounds) { | |||
| 535 | 599 | ||
| 536 | if (sourceList.length === 0) return; | 600 | if (sourceList.length === 0) return; |
| 537 | 601 | ||
| 538 | sourceList.forEach(bg => { | 602 | const sortedList = sortBackgrounds(sourceList, false); |
| 603 | sortedList.forEach(bg => { | ||
| 539 | const imageData = { filename: bg, isCustom: false }; | 604 | const imageData = { filename: bg, isCustom: false }; |
| 540 | const thumbnail = createThumbnailElement(imageData); | 605 | const thumbnail = createThumbnailElement(imageData); |
| 541 | container.append(thumbnail); | 606 | container.append(thumbnail); |
| @@ -556,7 +621,8 @@ function renderChatBackgrounds(backgrounds) { | |||
| 556 | 621 | ||
| 557 | if (sourceList.length === 0) return; | 622 | if (sourceList.length === 0) return; |
| 558 | 623 | ||
| 559 | sourceList.forEach(bg => { | 624 | const sortedList = sortBackgrounds(sourceList, true); |
| 625 | sortedList.forEach(bg => { | ||
| 560 | const imageData = { filename: bg, isCustom: true }; | 626 | const imageData = { filename: bg, isCustom: true }; |
| 561 | const thumbnail = createThumbnailElement(imageData); | 627 | const thumbnail = createThumbnailElement(imageData); |
| 562 | container.append(thumbnail); | 628 | container.append(thumbnail); |
| @@ -577,6 +643,8 @@ export async function getBackgrounds() { | |||
| 577 | const { images, config } = await response.json(); | 643 | const { images, config } = await response.json(); |
| 578 | Object.assign(THUMBNAIL_CONFIG, config); | 644 | Object.assign(THUMBNAIL_CONFIG, config); |
| 579 | 645 | ||
| 646 | cachedSystemBackgrounds = images; | ||
| 647 | |||
| 580 | await metadataPromise; | 648 | await metadataPromise; |
| 581 | 649 | ||
| 582 | renderSystemBackgrounds(images); | 650 | renderSystemBackgrounds(images); |
| @@ -968,6 +1036,17 @@ export function initBackgrounds() { | |||
| 968 | $('#auto_background').on('click', autoBackgroundCommand); | 1036 | $('#auto_background').on('click', autoBackgroundCommand); |
| 969 | $('#add_bg_button').on('change', (e) => onBackgroundUploadSelected(e.originalEvent)); | 1037 | $('#add_bg_button').on('change', (e) => onBackgroundUploadSelected(e.originalEvent)); |
| 970 | $('#bg-filter').on('input', () => debouncedOnBackgroundFilterInput()); | 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 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 1050 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 972 | name: 'lockbg', | 1051 | name: 'lockbg', |
| 973 | callback: () => { | 1052 | callback: () => { |