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 @@
96 font-size: calc(var(--mainFontSize) * 0.95);96 font-size: calc(var(--mainFontSize) * 0.95);
97}97}
9898
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;
public/index.html+6 -0
@@ -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">
public/scripts/backgrounds.js+82 -3
@@ -3,7 +3,7 @@ import { characters, chat_metadata, eventSource, event_types, generateQuietPromp
3import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js';3import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js';
4import { SlashCommand } from './slash-commands/SlashCommand.js';4import { SlashCommand } from './slash-commands/SlashCommand.js';
5import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';5import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
6import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop, saveBase64AsFile, getFileExtension } from './utils.js';6import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop, saveBase64AsFile, getFileExtension, sortIgnoreCaseAndAccents } from './utils.js';
7import { debounce_timeout } from './constants.js';7import { debounce_timeout } from './constants.js';
8import { t } from './i18n.js';8import { t } from './i18n.js';
9import { Popup } from './popup.js';9import { Popup } from './popup.js';
@@ -58,6 +58,18 @@ const BG_SOURCES = {
58};58};
5959
60/**60/**
61 * Background sorting options.
62 * @readonly
63 * @enum {string}
64 */
65const 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 * @readonly74 * @readonly
63 * @type {Record<string, string>}75 * @type {Record<string, string>}
@@ -73,14 +85,56 @@ const BG_TABS = Object.freeze({
73 */85 */
74let lazyLoadObserver = null;86let 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 */
93let cachedSystemBackgrounds = [];
94
76export let background_settings = {95export 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};
82102
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 */
109function 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 }
153210
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);
162220
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}
169228
@@ -447,6 +506,11 @@ async function onDeleteBackgroundClick(e) {
447 // If it's not custom, it's a built-in background. Delete it from the server506 // 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) {
535599
536 if (sourceList.length === 0) return;600 if (sourceList.length === 0) return;
537601
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) {
556621
557 if (sourceList.length === 0) return;622 if (sourceList.length === 0) return;
558623
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);
579645
646 cachedSystemBackgrounds = images;
647
580 await metadataPromise;648 await metadataPromise;
581649
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: () => {