natural sort for filebrowser

This commit is contained in:
2026-05-04 16:44:05 -04:00
parent ddebbd40ea
commit 8336805ee2
2 changed files with 85 additions and 6 deletions
@@ -18,7 +18,7 @@
import FileBrowserTreeItem from "./FileBrowserTreeItem.svelte";
import FileBrowserRowItem from "./FileBrowserRowItem.svelte";
import type { FileItem, TreeNode, Breadcrumb } from "$lib/types";
import { cn } from "$lib/utils";
import { cn, naturalSortCompare } from "$lib/utils";
import {
getSystemTreeSystemTreeGet,
getArchiveTreeInventoryTreeGet,
@@ -264,12 +264,24 @@
});
result.sort((a: FileItem, b: FileItem) => {
let cmp = 0;
if (sortColumn === "name") {
// Directories always sort before files, then natural sort by name
if (a.type !== b.type) {
cmp = a.type === "directory" ? -1 : 1;
} else {
cmp = naturalSortCompare(a.name, b.name);
}
} else {
const valA = sortColumn === "type" ? a.type : a[sortColumn as keyof FileItem] || 0;
const valB = sortColumn === "type" ? b.type : b[sortColumn as keyof FileItem] || 0;
if (valA < (valB as any)) return sortDirection === "asc" ? -1 : 1;
if (valA > (valB as any)) return sortDirection === "asc" ? 1 : -1;
return 0;
if (valA < (valB as any)) cmp = -1;
else if (valA > (valB as any)) cmp = 1;
}
return sortDirection === "asc" ? cmp : -cmp;
});
return result;
+67
View File
@@ -55,3 +55,70 @@ export function formatSize(bytes: number | null | undefined): string {
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
}
/**
* Natural sort comparator mimicking Windows Explorer's StrCmpLogicalW.
*
* Rules:
* 1. Directories always sort before files.
* 2. Case-insensitive alphanumeric comparison.
* 3. Multi-digit numbers are compared as whole integers (1, 2, 10 not 1, 10, 2).
* 4. Falls back to locale-aware comparison for non-ASCII characters.
*/
export function naturalSortCompare(aName: string, bName: string): number {
const aLower = aName.toLowerCase();
const bLower = bName.toLowerCase();
const len = Math.min(aLower.length, bLower.length);
let i = 0;
while (i < len) {
const aChar = aLower[i];
const bChar = bLower[i];
// If both are digits, extract the full number and compare numerically
if (isDigit(aChar) && isDigit(bChar)) {
let aNum = 0;
let bNum = 0;
let j = i;
while (j < aLower.length && isDigit(aLower[j])) {
aNum = aNum * 10 + (aLower.charCodeAt(j) - 48);
j++;
}
const aEnd = j;
j = i;
while (j < bLower.length && isDigit(bLower[j])) {
bNum = bNum * 10 + (bLower.charCodeAt(j) - 48);
j++;
}
const bEnd = j;
if (aNum !== bNum) {
return aNum - bNum;
}
// Numbers are equal but one may have leading zeros; shorter run first
if (aEnd !== bEnd) {
return aEnd - bEnd;
}
i = aEnd;
continue;
}
// Simple character comparison (locale-aware fallback for non-ASCII)
if (aChar !== bChar) {
return aChar.localeCompare(bChar);
}
i++;
}
return aLower.length - bLower.length;
}
function isDigit(c: string): boolean {
const code = c.charCodeAt(0);
return code >= 48 && code <= 57;
}