From c07eddb58da813740fb59860ccc3b2f64321b3ef Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Sun, 18 Jan 2026 03:56:12 +0000 Subject: [PATCH] fix(ui): improve gallery sorting and IndexedDB operations - Enhance gallery sorting functionality - Update IndexedDB operations for better reliability --- javascript/gallery.js | 87 +++++++++++++++++++++++++++++-------------- javascript/indexdb.js | 5 ++- 2 files changed, 64 insertions(+), 28 deletions(-) diff --git a/javascript/gallery.js b/javascript/gallery.js index acea7127b..4405da465 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -318,7 +318,11 @@ async function addSeparators() { document.querySelectorAll('.gallery-separator').forEach((node) => { el.files.removeChild(node); }); const all = Array.from(el.files.children); let lastDir; - let isFirstSeparator = true; // Flag to open the first separator by default + + // Count root files (files without a directory path) + const hasRootFiles = all.some((f) => f.name && !f.name.match(/[/\\]/)); + // Only auto-open first separator if there are no root files to display + let isFirstSeparator = !hasRootFiles; // First pass: create separators for (const f of all) { @@ -449,7 +453,9 @@ class GalleryFile extends HTMLElement { } } - this.hash = await getHash(`${this.folder}/${this.name}/${this.size}/${this.mtime}`); // eslint-disable-line no-use-before-define + // Normalize path to ensure consistent hash regardless of which folder view is used + const normalizedPath = this.src.replace(/\/+/g, '/').replace(/\/$/, ''); + this.hash = await getHash(`${normalizedPath}/${this.size}/${this.mtime}`); // eslint-disable-line no-use-before-define const cachedData = (this.hash && opts.browser_cache) ? await idbGet(this.hash).catch(() => undefined) : undefined; const img = document.createElement('img'); img.className = 'gallery-file'; @@ -485,9 +491,11 @@ class GalleryFile extends HTMLElement { this.size = json.size; this.mtime = new Date(json.mtime); if (opts.browser_cache) { + // Store file's actual parent directory (not browsed folder) for consistent cleanup + const fileDir = this.src.replace(/\/+/g, '/').replace(/\/[^/]+$/, ''); await idbAdd({ hash: this.hash, - folder: this.folder, + folder: fileDir, file: this.name, size: this.size, mtime: this.mtime, @@ -736,59 +744,84 @@ async function gallerySort(btn) { if (arr.length === 0) return; // no files to sort if (btn) lastSort = btn.charCodeAt(0); const fragment = document.createDocumentFragment(); + + // Helper to get directory path from a file node + const getDirPath = (node) => { + const match = node.name.match(/(.*)[/\\]/); + return match ? match[1] : ''; + }; + + // Partition into root files and subfolder files - root files always stay at top + const rootFiles = arr.filter((node) => !getDirPath(node)); + const subfolderFiles = arr.filter((node) => getDirPath(node)); + + // Group subfolder files by directory + const folderGroups = new Map(); + for (const file of subfolderFiles) { + const dir = getDirPath(file); + if (!folderGroups.has(dir)) { + folderGroups.set(dir, []); + } + folderGroups.get(dir).push(file); + } + + // Sort function based on current sort mode + let sortFn; switch (lastSort) { case 61789: // name asc lastSortName = 'Name Ascending'; - arr - .sort((a, b) => a.name.localeCompare(b.name)) - .forEach((node) => fragment.appendChild(node)); + sortFn = (a, b) => a.name.localeCompare(b.name); break; case 61790: // name dsc lastSortName = 'Name Descending'; - arr - .sort((b, a) => a.name.localeCompare(b.name)) - .forEach((node) => fragment.appendChild(node)); + sortFn = (a, b) => b.name.localeCompare(a.name); break; case 61792: // size asc lastSortName = 'Size Ascending'; - arr - .sort((a, b) => a.size - b.size) - .forEach((node) => fragment.appendChild(node)); + sortFn = (a, b) => a.size - b.size; break; case 61793: // size dsc lastSortName = 'Size Descending'; - arr - .sort((b, a) => a.size - b.size) - .forEach((node) => fragment.appendChild(node)); + sortFn = (a, b) => b.size - a.size; break; case 61794: // resolution asc lastSortName = 'Resolution Ascending'; - arr - .sort((a, b) => a.width * a.height - b.width * b.height) - .forEach((node) => fragment.appendChild(node)); + sortFn = (a, b) => a.width * a.height - b.width * b.height; break; case 61795: // resolution dsc lastSortName = 'Resolution Descending'; - arr - .sort((b, a) => a.width * a.height - b.width * b.height) - .forEach((node) => fragment.appendChild(node)); + sortFn = (a, b) => b.width * b.height - a.width * a.height; break; case 61662: lastSortName = 'Modified Ascending'; - arr - .sort((a, b) => a.mtime - b.mtime) - .forEach((node) => fragment.appendChild(node)); + sortFn = (a, b) => a.mtime - b.mtime; break; case 61661: lastSortName = 'Modified Descending'; - arr - .sort((b, a) => a.mtime - b.mtime) - .forEach((node) => fragment.appendChild(node)); + sortFn = (a, b) => b.mtime - a.mtime; break; default: lastSortName = 'None'; + sortFn = null; break; } + + // Sort root files + if (sortFn) { + rootFiles.sort(sortFn); + } + rootFiles.forEach((node) => fragment.appendChild(node)); + + // Sort folder names alphabetically, then sort files within each folder + const sortedFolderNames = Array.from(folderGroups.keys()).sort((a, b) => a.localeCompare(b)); + for (const folderName of sortedFolderNames) { + const files = folderGroups.get(folderName); + if (sortFn) { + files.sort(sortFn); + } + files.forEach((node) => fragment.appendChild(node)); + } + if (fragment.children.length === 0) return; el.files.innerHTML = ''; el.files.appendChild(fragment); diff --git a/javascript/indexdb.js b/javascript/indexdb.js index 21fbdfa61..ace3d5843 100644 --- a/javascript/indexdb.js +++ b/javascript/indexdb.js @@ -150,7 +150,10 @@ async function idbFolderCleanup(keepSet, folder, signal) { throw new Error('IndexedDB cleaning function must be told the current active folder'); } - let removals = new Set(await idbGetAllKeys('folder', folder)); + // Use range query to match folder and all its subdirectories + const folderNormalized = folder.replace(/\/+/g, '/').replace(/\/$/, ''); + const range = IDBKeyRange.bound(folderNormalized, `${folderNormalized}\uffff`, false, true); + let removals = new Set(await idbGetAllKeys('folder', range)); removals = removals.difference(keepSet); // Don't need to keep full set in memory const totalRemovals = removals.size; if (signal.aborted) {