From 8386dfdd8e9021ccb22645c6fa308936e99a88f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 22:42:21 +0000 Subject: [PATCH] feat: sort web file manager columns by name, size or date Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01XgLRarbAmBVepTPKCK7mgd --- CHANGELOG.md | 1 + web/pages/files.css | 2 ++ web/pages/files.js | 56 +++++++++++++++++++++++++++++++++++++-------- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08f14ef1e46..333d214b413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - TTF font support on ESP32-S3 devices. Whole-point sizes from 8pt to 22pt will be automatically available. - The OPDS browser shows how many books a category holds, in parentheses next to its arrow, when the catalog provides a count (a `thr:count` link attribute or a "12713 books" summary). Folder titles that start with a 📁 emoji show as "/name" instead, since the device fonts have no folder emoji. - On devices with PSRAM (Sticky, X4 Pro), the OPDS browser downloads the next page of a catalog in the background while you browse, and keeps pages you have visited in memory, so Next page, Previous page and Back open without waiting on the server. +- Sort the web file manager by name, size or modified date by clicking the column header. Click again to reverse the order. Folders always stay at the top, and the chosen order is kept while you browse other folders in the same tab. - The web file manager's image preview has previous and next buttons, and the left and right arrow keys, to step through the images in the current folder. Other file types are skipped, and stepping wraps around at either end. The preview also shows the image's position in the folder, its pixel dimensions, its file size and, when the file list has one, its modified date. - The Settings > System footer shows the firmware's branch (the batch number, such as `b11`, for combined test builds) and commit, with `*` when built from uncommitted changes, under the version. The System list stops above the footer instead of running under it. The web status API reports the full branch, build number (the commit count unless the build sets `CROSSINK_BUILD_NUMBER`; left out for shallow checkouts) and UTC build time. - In-reader menu for X3/X4/X4 Classic have been updated to a modified version of the in-reader menu for touch devices diff --git a/web/pages/files.css b/web/pages/files.css index 07ad4aeeb62..2392a814188 100644 --- a/web/pages/files.css +++ b/web/pages/files.css @@ -1516,3 +1516,5 @@ h2 { .log-summary-table .increased { color: #f39c12; } + +th.sortable { cursor: pointer; user-select: none; } diff --git a/web/pages/files.js b/web/pages/files.js index 166a117adfa..7a5a84ee606 100644 --- a/web/pages/files.js +++ b/web/pages/files.js @@ -201,6 +201,46 @@ async function hydrate() { document.getElementById("folder-summary").innerHTML = `${folderCount} ${folderLabel}, ${fileCount} ${fileLabel}, ${formatFileSize(totalSize)}`; + listedFiles = files; + renderFileTable(); +} + +// Column sort; null key keeps the default folders, EPUBs, then name order +let listedFiles = []; +let fileSort = { key: null, dir: 1 }; +try { + fileSort = JSON.parse(sessionStorage.getItem("fileSort")) || fileSort; +} catch (e) {} + +function compareFiles(a, b) { + // Folders always first; they have no size or date, so they sort by name + if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1; + const key = fileSort.key; + if (a.isDirectory && key !== "name") return a.name.localeCompare(b.name); + if (!key) { + if (a.isEpub !== b.isEpub) return a.isEpub ? -1 : 1; + return a.name.localeCompare(b.name); + } + const diff = key === "name" ? a.name.localeCompare(b.name) : (a[key] || 0) - (b[key] || 0); + return (diff || a.name.localeCompare(b.name)) * fileSort.dir; +} + +function sortHeader(key, label, cls) { + const arrow = fileSort.key === key ? (fileSort.dir > 0 ? " ▲" : " ▼") : ""; + return `${label}${arrow}`; +} + +function setFileSort(key) { + fileSort = { key, dir: fileSort.key === key ? -fileSort.dir : 1 }; + try { + sessionStorage.setItem("fileSort", JSON.stringify(fileSort)); + } catch (e) {} + renderFileTable(); +} + +function renderFileTable() { + const files = listedFiles; + const fileTable = document.getElementById("file-table"); if (files.length === 0) { fileTable.innerHTML = '
This folder is empty
'; } else { @@ -208,16 +248,9 @@ async function hydrate() { // Add select-all checkbox column fileTableContent += - 'NameTypeSizeModifiedActions'; - - const sortedFiles = files.sort((a, b) => { - // Directories first, then epub files, then other files, alphabetically within each group - if (a.isDirectory && !b.isDirectory) return -1; - if (!a.isDirectory && b.isDirectory) return 1; - if (a.isEpub && !b.isEpub) return -1; - if (!a.isEpub && b.isEpub) return 1; - return a.name.localeCompare(b.name); - }); + `${sortHeader("name", "Name", "")}Type${sortHeader("size", "Size", "")}${sortHeader("mtime", "Modified", "modified-col")}Actions`; + + const sortedFiles = files.sort(compareFiles); sortedFiles.forEach((file) => { if (file.isDirectory) { @@ -266,6 +299,9 @@ async function hydrate() { fileTableContent += ""; fileTable.innerHTML = fileTableContent; + fileTable.querySelectorAll("th[data-sort]").forEach((th) => { + th.addEventListener("click", () => setFileSort(th.dataset.sort)); + }); fileTable.querySelectorAll(".file-action-btn").forEach((button) => { button.addEventListener("click", handleFileActionClick); });