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

788ed3d32371da7ebd683e7019caa6c890c6b0a4

L <123923688+Vibecoder9000@users.noreply.github.com>

Signed
3 files changed, +94 -3Showing whitespace changes
public/css/backgrounds.css+6 -0
@@ -96,6 +96,12 @@
9696 font-size: calc(var(--mainFontSize) * 0.95);
9797}
9898
99+#bg-sort {
100+ width: auto;
101+ max-width: 6em;
102+ flex-shrink: 0;
103+}
104+
99105/* Thumbnails */
100106.bg_example:hover .BGSampleTitle {
101107 opacity: 1;
public/index.html+6 -0
@@ -5563,6 +5563,12 @@
55635563 </div>
55645564 <div class="bg-header-row-2">
55655565 <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>
55665572 </div>
55675573 </div>
55685574 <div id="bg_tabs" class="heading-container-with-controls">
public/scripts/backgrounds.js+82 -3
@@ -3,7 +3,7 @@ import { characters, chat_metadata, eventSource, event_types, generateQuietPromp
33import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js';
44import { SlashCommand } from './slash-commands/SlashCommand.js';
55import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
66import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop, saveBase64AsFile, getFileExtension, sortIgnoreCaseAndAccents } from './utils.js';
77import { debounce_timeout } from './constants.js';
88import { t } from './i18n.js';
99import { Popup } from './popup.js';
@@ -58,6 +58,18 @@ const BG_SOURCES = {
5858};
5959
6060/**
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+/**
6173 * Mapping of background sources to their corresponding tab IDs.
6274 * @readonly
6375 * @type {Record<string, string>}
@@ -73,14 +85,56 @@ const BG_TABS = Object.freeze({
7385 */
7486let lazyLoadObserver = null;
7587
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+
7695export let background_settings = {
7796 name: '__transparent.png',
7897 url: generateUrlParameter('__transparent.png', false),
7998 fitting: 'classic',
8099 animation: false,
100+ sortOrder: BG_SORT_OPTIONS.AZ,
81101};
82102
83103/**
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+/**
84138 * Creates a single thumbnail DOM element. The CSS now handles all sizing.
85139 * @param {object} imageData - Data for the image (filename, isCustom).
86140 * @returns {HTMLElement} The created thumbnail element.
@@ -150,6 +204,9 @@ export function loadBackgroundSettings(settings) {
150204 if (!Object.hasOwn(backgroundSettings, 'animation')) {
151205 backgroundSettings.animation = false;
152206 }
207+ if (!backgroundSettings.sortOrder) {
208+ backgroundSettings.sortOrder = BG_SORT_OPTIONS.AZ;
209+ }
153210
154211 // If a value is already saved, use it. Otherwise, determine default based on screen size.
155212 let columns = backgroundSettings.thumbnailColumns;
@@ -158,12 +215,14 @@ export function loadBackgroundSettings(settings) {
158215 columns = isNarrowScreen ? THUMBNAIL_COLUMNS_DEFAULT_MOBILE : THUMBNAIL_COLUMNS_DEFAULT_DESKTOP;
159216 }
160217 background_settings.thumbnailColumns = columns;
218+ background_settings.sortOrder = backgroundSettings.sortOrder;
161219 applyThumbnailColumns(background_settings.thumbnailColumns);
162220
163221 setBackground(backgroundSettings.name, backgroundSettings.url);
164222 setFittingClass(backgroundSettings.fitting);
165223 $('#background_fitting').val(backgroundSettings.fitting);
166224 $('#background_thumbnails_animation').prop('checked', background_settings.animation);
225+ $('#bg-sort').val(background_settings.sortOrder);
167226 highlightSelectedBackground();
168227}
169228
@@ -447,6 +506,11 @@ async function onDeleteBackgroundClick(e) {
447506 // If it's not custom, it's a built-in background. Delete it from the server
448507 if (!isCustom) {
449508 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+ }
450514 } else {
451515 const list = chat_metadata[LIST_METADATA_KEY] || [];
452516 const index = list.indexOf(bg);
@@ -535,7 +599,8 @@ function renderSystemBackgrounds(backgrounds) {
535599
536600 if (sourceList.length === 0) return;
537601
538- sourceList.forEach(bg => {
602+ const sortedList = sortBackgrounds(sourceList, false);
603+ sortedList.forEach(bg => {
539604 const imageData = { filename: bg, isCustom: false };
540605 const thumbnail = createThumbnailElement(imageData);
541606 container.append(thumbnail);
@@ -556,7 +621,8 @@ function renderChatBackgrounds(backgrounds) {
556621
557622 if (sourceList.length === 0) return;
558623
559- sourceList.forEach(bg => {
624+ const sortedList = sortBackgrounds(sourceList, true);
625+ sortedList.forEach(bg => {
560626 const imageData = { filename: bg, isCustom: true };
561627 const thumbnail = createThumbnailElement(imageData);
562628 container.append(thumbnail);
@@ -577,6 +643,8 @@ export async function getBackgrounds() {
577643 const { images, config } = await response.json();
578644 Object.assign(THUMBNAIL_CONFIG, config);
579645
646+ cachedSystemBackgrounds = images;
647+
580648 await metadataPromise;
581649
582650 renderSystemBackgrounds(images);
@@ -968,6 +1036,17 @@ export function initBackgrounds() {
9681036 $('#auto_background').on('click', autoBackgroundCommand);
9691037 $('#add_bg_button').on('change', (e) => onBackgroundUploadSelected(e.originalEvent));
9701038 $('#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+ });
9711050 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9721051 name: 'lockbg',
9731052 callback: () => {