From a7937ad808eb27c2ec03ade764efe3c7b9250a94 Mon Sep 17 00:00:00 2001
From: AuDevTist1C <114492072+AuDevTist1C@users.noreply.github.com>
Date: Wed, 22 Jul 2026 17:23:36 +0200
Subject: [PATCH 1/7] refactor: standardize DOM element dataset attributes
across file browser component
Standardize DOM element attribute naming across the file browser component by converting legacy custom attributes to standard HTML5 `data-*` dataset attributes.
Previously, template elements mixed standard properties with proprietary, non-standard DOM attributes (such as `action`, `type`, `name`, `home`, `open-doc`, `ftp-account`, `uuid`, and `storageType`). This led to non-compliant HTML markup, required explicit `getAttribute()` and `setAttribute()` DOM calls, and increased the risk of attribute collision with future web standards.
This change enforces a uniform contract using the standard HTML5 `dataset` API (`data-*`), improving rendering consistency, type predictability, and JS-to-DOM binding efficiency.
* **Template Refactoring (`src/pages/fileBrowser/list.hbs`):**
* Converted legacy inline attributes to standard dataset equivalents: `data-action`, `data-type`, `data-name`, `data-home`, `data-open-doc`, `data-ftp-account`, `data-uuid`, and `data-storage-type`.
* Ensured boolean and string dataset values comply with template engine rendering rules.
* **Script Adaptations (`src/pages/fileBrowser/fileBrowser.js`):**
* Refactored event delegation and element lookup handlers to utilize standard camelCase JavaScript `dataset` properties (e.g., replacing `el.getAttribute('open-doc')` with `el.dataset.openDoc`, along with `dataset.action`, `dataset.uuid`, `dataset.type`, and `dataset.storageType`).
* Simplified property checks across list selection handlers and navigation logic.
* **Stylesheet Selectors (`src/pages/fileBrowser/fileBrowser.scss`):**
* Updated CSS attribute selectors from `[storageType="notification"]` to `[data-storage-type="notification"]` to maintain tight visual styling coupling without relying on invalid HTML attributes.
(AI generated commit message)
---
src/pages/fileBrowser/fileBrowser.js | 26 +++++++++++++-------------
src/pages/fileBrowser/fileBrowser.scss | 4 ++--
src/pages/fileBrowser/list.hbs | 18 +++++++++---------
3 files changed, 24 insertions(+), 24 deletions(-)
diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js
index 570cacee3..2344fb4c0 100644
--- a/src/pages/fileBrowser/fileBrowser.js
+++ b/src/pages/fileBrowser/fileBrowser.js
@@ -929,16 +929,16 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
return;
}
- let action = $el.getAttribute("action") || $el.dataset.action;
+ let action = $el.dataset.action;
if (!action) return;
let url = $el.dataset.url;
- let name = $el.dataset.name || $el.getAttribute("name");
- const idOpenDoc = $el.hasAttribute("open-doc");
- const uuid = $el.getAttribute("uuid");
- const type = $el.getAttribute("type");
- const storageType = $el.getAttribute("storageType");
- const home = $el.getAttribute("home");
+ let name = $el.dataset.name;
+ const isOpenDoc = $el.dataset.openDoc != null;
+ const uuid = $el.dataset.uuid;
+ const type = $el.dataset.type;
+ const storageType = $el.dataset.storageType;
+ const home = $el.dataset.home;
const isDir = ["dir", "directory", "folder"].includes(type);
if (!url) {
@@ -960,7 +960,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
return;
}
- if (!url && action === "open" && isDir && !idOpenDoc && !isContextMenu) {
+ if (!url && action === "open" && isDir && !isOpenDoc && !isContextMenu) {
loader.hide();
util.addPath(name, uuid).then((res) => {
const storage = allStorages.find((storage) => storage.uuid === uuid);
@@ -975,7 +975,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
}
if (isContextMenu) action = "contextmenu";
- else if (idOpenDoc) action = "open-doc";
+ else if (isOpenDoc) action = "openDoc";
switch (action) {
case "navigation":
@@ -988,7 +988,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
if (isDir) folder();
else if (!$el.hasAttribute("disabled")) file();
break;
- case "open-doc":
+ case "openDoc":
openDoc();
break;
}
@@ -1046,7 +1046,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
if (appSettings.value.vibrateOnTap) {
navigator.vibrate(config.VIBRATION_TIME);
}
- if ($el.getAttribute("open-doc") === "true") return;
+ if (isOpenDoc) return;
const deleteText =
currentDir.url === "/" ? strings.remove : strings.delete;
@@ -1399,7 +1399,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
if (IS_FILE_MODE) {
util.pushFolder(allStorages, "Select document", null, {
- "open-doc": true,
+ openDoc: true,
});
}
@@ -1646,7 +1646,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
className="nav"
data-url={url}
data-name={displayName}
- attr-action="navigation"
+ data-action="navigation"
attr-text={displayName}
tabIndex={-1}
>,
diff --git a/src/pages/fileBrowser/fileBrowser.scss b/src/pages/fileBrowser/fileBrowser.scss
index 8e64a9622..281f4ffba 100644
--- a/src/pages/fileBrowser/fileBrowser.scss
+++ b/src/pages/fileBrowser/fileBrowser.scss
@@ -4,7 +4,7 @@
}
.tile {
- &[storageType="notification"] {
+ &[data-storage-type="notification"] {
background-color: rgb(153, 153, 255);
background-color: var(--primary-color);
color: rgb(255, 255, 255);
@@ -22,7 +22,7 @@
position: relative;
color: rgb(65, 85, 133);
- &[storageType]::after {
+ &[data-storage-type]::after {
position: absolute;
top: 50%;
left: 50%;
diff --git a/src/pages/fileBrowser/list.hbs b/src/pages/fileBrowser/list.hbs
index a4ae77297..ade7b9434 100644
--- a/src/pages/fileBrowser/list.hbs
+++ b/src/pages/fileBrowser/list.hbs
@@ -3,19 +3,19 @@
From 22c3733aea1f62bb7d8b1fce2098ab8f680979c4 Mon Sep 17 00:00:00 2001
From: AuDevTist1C <114492072+AuDevTist1C@users.noreply.github.com>
Date: Fri, 24 Jul 2026 10:24:58 +0200
Subject: [PATCH 2/7] refactor: modernize syntax, optimize control flow, and
consolidate async logic in fileBrowser.js
Perform a comprehensive refactoring pass on `fileBrowser.js` to align with modern ES2021+ JavaScript patterns, eliminate redundant code pathways, and improve asynchronous operational safety.
* **Logical Nullish Assignment Operators:**
* Updated default configuration and variable fallback initializations to use logical nullish assignment (`||=`) instead of verbose logical OR (`||`) or ternary evaluations. This ensures falsy valid values (like `0` or `""`) are preserved correctly without unexpected fallbacks.
* **Control Flow Restructuring:**
* Replaced monolithic, nested `if/else` ladders in context menu and action bar click delegation handlers with streamlined, strict-equality `switch` statements, improving branch readability and execution performance.
* **Deletion Logic Consolidation:**
* Extracted redundant inline file and directory deletion routines into a unified, reusable `deleteDirOrFile()` asynchronous helper method.
* Shared `deleteDirOrFile()` across individual contextual item deletion, context menu commands, and batch multi-selection deletion tasks, ensuring centralized error handling and state validation.
* **Utility & Closure Streamlining:**
* Cleaned up string parsing within `sanitizeZipPath()` by replacing repetitive regular expression replacements with a concise single-pass path normalization helper.
* Converted verbose multi-line promise callback chains into concise single-line arrow functions, reducing call-stack depth and boilerplate.
(AI generated commit message)
---
src/pages/fileBrowser/fileBrowser.js | 756 ++++++++++-----------------
1 file changed, 287 insertions(+), 469 deletions(-)
diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js
index 2344fb4c0..5d9b297c7 100644
--- a/src/pages/fileBrowser/fileBrowser.js
+++ b/src/pages/fileBrowser/fileBrowser.js
@@ -35,14 +35,14 @@ import _list from "./list.hbs";
import util from "./util";
/**
- * @typedef {{url: String, name: String}} Location
+ * @typedef {{url: string, name: string}} Location
*/
/**
* @typedef Storage
- * @property {String} name
- * @property {String} uuid
- * @property {String} url
+ * @property {string} name
+ * @property {string} uuid
+ * @property {string} url
* @property {'dir'} type
* @property {'permission'|'ftp'|'sftp'|'sd'} storageType
*/
@@ -55,7 +55,7 @@ import util from "./util";
* @returns {Promise
}
*/
function FileBrowserInclude(mode, info, doesOpenLast = true) {
- mode = mode || "file";
+ mode ||= "file";
const IS_FOLDER_MODE = ["folder", "both"].includes(mode);
const IS_FILE_MODE = ["file", "both"].includes(mode);
@@ -72,13 +72,12 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
let selectedItems = new Set();
let copiedItems = [];
- if (!info) {
- if (mode !== "both") {
- info = IS_FOLDER_MODE ? strings["open folder"] : strings["open file"];
- } else {
- info = strings["file browser"];
- }
- }
+ info ||=
+ strings[
+ mode === "both"
+ ? "file browser"
+ : `open ${IS_FOLDER_MODE ? "folder" : "file"}`
+ ];
return new Promise((resolve, reject) => {
//#region Declaration
@@ -224,35 +223,32 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
$fbMenu.onclick = function (e) {
$fbMenu.hide();
- const action = e.target.getAttribute("action");
- if (action === "settings") {
- filesSettings().show();
- const onshow = () => {
- $page.off("show", onshow);
+ switch (e.target.getAttribute("action")) {
+ case "settings": {
+ filesSettings().show();
+ const onshow = () => {
+ $page.off("show", onshow);
+ reload();
+ };
+ $page.on("show", onshow);
+ break;
+ }
+ case "reload": {
+ const { url } = currentDir;
+ if (url in cachedDir) delete cachedDir[url];
reload();
- };
- $page.on("show", onshow);
- return;
- }
-
- if (action === "reload") {
- const { url } = currentDir;
- if (url in cachedDir) delete cachedDir[url];
- reload();
- return;
- }
-
- if (action === "refresh") {
- ftp.disconnect(
- () => {},
- () => {},
- );
- sftp.close(
- () => {},
- () => {},
- );
- toast(strings.success);
- return;
+ break;
+ }
+ case "refresh":
+ ftp.disconnect(
+ () => {},
+ () => {},
+ );
+ sftp.close(
+ () => {},
+ () => {},
+ );
+ toast(strings.success);
}
};
@@ -282,12 +278,8 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
case "import-project-zip": {
let zipFile = await new Promise((resolve, reject) => {
sdcard.openDocumentFile(
- (res) => {
- resolve(res.uri);
- },
- (err) => {
- reject(err);
- },
+ (res) => resolve(res.uri),
+ (err) => reject(err),
"application/zip",
);
});
@@ -300,9 +292,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
"Importing zip file...",
{
timeout: 10000,
- oncancel: () => {
- isCancelled = true;
- },
+ oncancel: () => (isCancelled = true),
},
);
@@ -331,9 +321,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
dir,
shouldBeDirAtEnd,
) => {
- if (isCancelled) {
- throw new Error("Cancelled");
- }
+ if (isCancelled) throw new Error("Cancelled");
let wantDirEnd = !!shouldBeDirAtEnd;
let parts;
if (typeof dir === "string") {
@@ -348,21 +336,14 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
if (!cd) return;
const newParent = Url.join(parent, cd);
- const isLast = parts.length === 0;
- const needDir = !isLast || wantDirEnd;
- if (!(await fsOperation(newParent).exists())) {
- if (needDir) {
- try {
- await fsOperation(parent).createDirectory(cd);
- } catch (e) {
- if (!(await fsOperation(newParent).exists())) throw e;
- }
- } else {
- try {
- await fsOperation(parent).createFile(cd);
- } catch (e) {
- if (!(await fsOperation(newParent).exists())) throw e;
- }
+ const needDir = parts.length || wantDirEnd;
+ const fs1 = fsOperation(newParent);
+ if (!(await fs1.exists())) {
+ try {
+ const fs2 = fsOperation(parent);
+ await fs2[`create${needDir ? "Directory" : "File"}`](cd);
+ } catch (e) {
+ if (!(await fs1.exists())) throw e;
}
}
if (parts.length) {
@@ -372,21 +353,18 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
const sanitizeZipPath = (p, isDir) => {
if (!p) return "";
- let path = String(p);
- path = path.replace(/\\/g, "/");
- path = path.replace(/^[a-zA-Z]+:\/\//, "");
- path = path.replace(/^\/+/, "");
- path = path.replace(/^[A-Za-z]:\//, "");
+ const path = String(p)
+ .replace(/\\/g, "/")
+ .replace(/^[a-zA-Z]+:\/\//, "")
+ .replace(/^\/+/, "")
+ .replace(/^[A-Za-z]:\//, "");
const parts = path.split("/");
const stack = [];
for (const part of parts) {
if (!part || part === ".") continue;
- if (part === "..") {
- if (stack.length) stack.pop();
- continue;
- }
- stack.push(part);
+ if (part !== "..") stack.push(part);
+ else if (stack.length) stack.pop();
}
let safe = stack.join("/");
if (isDir && safe && !safe.endsWith("/")) safe += "/";
@@ -403,9 +381,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
};
for (const filePath of files) {
- if (isCancelled) {
- throw new Error("Cancelled");
- }
+ if (isCancelled) throw new Error("Cancelled");
const entry = zip.files[filePath];
current++;
@@ -417,9 +393,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
let correctFile = filePath.replace(/\\/g, "/");
const isDirEntry = entry.dir || correctFile.endsWith("/");
- if (isUnsafeAbsolutePath(filePath)) {
- continue;
- }
+ if (isUnsafeAbsolutePath(filePath)) continue;
correctFile = sanitizeZipPath(correctFile, isDirEntry);
if (!correctFile) continue;
@@ -439,19 +413,13 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
await createFileRecursive(extractDir, correctFile, false);
- if (isCancelled) {
- throw new Error("Cancelled");
- }
+ if (isCancelled) throw new Error("Cancelled");
const content = await entry.async("arraybuffer");
- if (isCancelled) {
- throw new Error("Cancelled");
- }
+ if (isCancelled) throw new Error("Cancelled");
await fsOperation(fileUrl).writeFile(content);
}
- if (isCancelled) {
- throw new Error("Cancelled");
- }
+ if (isCancelled) throw new Error("Cancelled");
loadingLoader.destroy();
toast(strings.success);
@@ -479,11 +447,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
case "addSftp": {
const storage = await remoteStorage[action]();
updateStorage(storage);
- break;
}
-
- default:
- break;
}
};
@@ -491,13 +455,11 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
$selectionMenu.hide();
const $target = e.target;
const action = $target.getAttribute("action");
- if (!action) return;
+ if (!action || currentDir.url === "/") return;
switch (action) {
case "copy":
- if (currentDir.url === "/" || !selectedItems.size) {
- break;
- }
+ if (!selectedItems.size) break;
copiedItems = Array.from(selectedItems);
toast(strings.success);
@@ -507,10 +469,6 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
break;
case "compress":
- if (currentDir.url === "/") {
- break;
- }
-
const zip = new JSZip();
let loadingLoader = loader.create(
strings["loading"],
@@ -557,13 +515,12 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
type: "arraybuffer",
});
const zipName = "archive_" + Date.now() + ".zip";
- const zipPath = Url.join(currentDir.url, zipName);
+ const { url } = currentDir;
+ const zipPath = Url.join(url, zipName);
const shortPath =
- currentDir.url.length > 40
- ? currentDir.url.substring(0, 37) + "..."
- : currentDir.url;
+ url.length > 40 ? url.substring(0, 37) + "..." : url;
loadingLoader.setMessage(`Saving ${zipName} to ${shortPath}`);
- await fsOperation(currentDir.url).createFile(zipName, zipContent);
+ await fsOperation(url).createFile(zipName, zipContent);
loadingLoader.destroy();
toast(strings.success);
isSelectionMode = !isSelectionMode;
@@ -577,69 +534,27 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
break;
case "delete": {
- if (currentDir.url === "/") {
- break;
+ const selectionCount = selectedItems.size;
+ const plural = selectionCount !== 1;
+ // Show confirmation dialog
+ let cMsg = strings[`delete entr${plural ? "ies" : "y"}`];
+ if (plural) cMsg = cMsg.replace("{count}", selectionCount);
+ else {
+ const url = Array.from(selectedItems)[0];
+ cMsg = cMsg.replace("{name}", url.split("/").pop());
}
- // Show confirmation dialog
- const confirmMessage =
- selectedItems.size === 1
- ? strings["delete entry"].replace(
- "{name}",
- Array.from(selectedItems)[0].split("/").pop(),
- )
- : strings["delete entries"].replace(
- "{count}",
- selectedItems.size,
- );
-
- const confirmation = await confirm(strings.warning, confirmMessage);
+ const confirmation = await confirm(strings.warning, cMsg);
if (!confirmation) break;
const loadingDialog = loader.create(
strings.loading,
- strings["deleting items"].replace("{count}", selectedItems.size),
+ strings["deleting items"].replace("{count}", selectionCount),
{ timeout: 3000 },
);
try {
- for (const url of selectedItems) {
- if ((await fsOperation(url).stat()).isDirectory) {
- if (url.startsWith("content://com.termux.documents/tree/")) {
- const fs = fsOperation(url);
- const entries = await fs.lsDir();
- if (entries.length === 0) {
- await fs.delete();
- } else {
- const deleteRecursively = async (currentUrl) => {
- const currentFs = fsOperation(currentUrl);
- const currentEntries = await currentFs.lsDir();
- for (const entry of currentEntries) {
- if (entry.isDirectory) {
- await deleteRecursively(entry.url);
- } else {
- await fsOperation(entry.url).delete();
- }
- }
- await currentFs.delete();
- };
- await deleteRecursively(url);
- }
- } else {
- await fsOperation(url).delete();
- }
- helpers.updateUriOfAllActiveFiles(url);
- recents.removeFolder(url);
- } else {
- const fs = fsOperation(url);
- await fs.delete();
- const openedFile = editorManager.getFile(url, "uri");
- if (openedFile) openedFile.uri = null;
- }
- recents.removeFile(url);
- openFolder.removeItem(url);
- delete cachedDir[url];
- }
+ for (const url of selectedItems) await deleteDirOrFile(url);
toast(strings.success);
reload();
isSelectionMode = false;
@@ -650,11 +565,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
} finally {
loadingDialog.destroy();
}
- break;
}
-
- default:
- break;
}
};
@@ -688,10 +599,53 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
$page.hide();
}
- function updateSelectionCount($count) {
- if ($count) {
- $count.textContent = `${selectedItems.size} items selected`;
+ /**
+ * @param {string} url
+ */
+ function isTermuxUrl(url) {
+ url = `${url ?? ""}`;
+ return url.startsWith("content://com.termux.documents/tree/");
+ }
+
+ /**
+ * @param {string} url
+ * @param {string} [type]
+ */
+ async function deleteDirOrFile(url, type) {
+ const fs = fsOperation(url);
+ const isDir = type ? helpers.isDir(type) : (await fs.stat()).isDirectory;
+
+ if (isDir) {
+ if (!isTermuxUrl(url)) await fs.delete();
+ else {
+ const entries = await fs.lsDir();
+ if (!entries.length) await fs.delete();
+ else {
+ const deleteRecursively = async (currentFs) => {
+ const currentEntries = await currentFs.lsDir();
+ for (const entry of currentEntries) {
+ const fs = fsOperation(entry.url);
+ await (entry.isDirectory ? deleteRecursively(fs) : fs.delete());
+ }
+ await currentFs.delete();
+ };
+ await deleteRecursively(fs);
+ }
+ }
+ helpers.updateUriOfAllActiveFiles(url);
+ recents.removeFolder(url);
+ } else {
+ await fs.delete();
+ const openedFile = editorManager.getFile(url, "uri");
+ if (openedFile) openedFile.uri = null;
}
+ recents.removeFile(url);
+ openFolder.removeItem(url);
+ delete cachedDir[url];
+ }
+
+ function updateSelectionCount($count) {
+ if ($count) $count.textContent = `${selectedItems.size} items selected`;
}
function updatePasteToggler() {
@@ -737,9 +691,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
const doesExist = await fsOperation(possibleConflictUrl).exists();
if (doesExist) {
- if (Url.areSame(url, possibleConflictUrl)) {
- continue;
- }
+ if (Url.areSame(url, possibleConflictUrl)) continue;
const targetStat = await fsOperation(possibleConflictUrl).stat();
if (stat.isDirectory || targetStat.isDirectory) {
@@ -750,13 +702,11 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
continue;
}
+ const msg = strings["file already exists force named"];
const confirmation = await confirm(
strings.warning,
- strings["file already exists force named"]
- ? strings["file already exists force named"].replace(
- "{name}",
- name,
- )
+ msg
+ ? msg.replace("{name}", name)
: `"${name}" already exists in this location.`,
);
if (!confirmation) continue;
@@ -781,14 +731,14 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
}
async function copyEntry(sourceUrl, targetDirUrl, name, sourceStat) {
- const fs = fsOperation(sourceUrl);
- const stat = sourceStat || (await fs.stat());
+ const fs1 = fsOperation(sourceUrl);
+ const stat = sourceStat || (await fs1.stat());
const entryName = name || stat.name || Url.basename(sourceUrl);
+ const fs2 = fsOperation(targetDirUrl);
if (stat.isDirectory) {
- const newDirUrl =
- await fsOperation(targetDirUrl).createDirectory(entryName);
- const entries = await fs.lsDir();
+ const newDirUrl = await fs2.createDirectory(entryName);
+ const entries = await fs1.lsDir();
for (const entry of entries) {
await copyEntry(
@@ -802,8 +752,8 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
return newDirUrl;
}
- const content = await fs.readFile();
- return fsOperation(targetDirUrl).createFile(entryName, content);
+ const content = await fs1.readFile();
+ return fs2.createFile(entryName, content);
}
function isInsideDirectory(sourceUrl, targetUrl) {
@@ -837,18 +787,14 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
// Handle select all functionality
selectAllCheckbox.onclick = () => {
const checked = selectAllCheckbox.checked;
+ if (!checked) selectedItems.clear();
const items = $list.querySelectorAll(".tile:not(.selection-header)");
items.forEach((item) => {
const checkbox = item.querySelector(".input-checkbox");
- if (checkbox) {
- checkbox.checked = checked;
- const url = item.querySelector("data-url").textContent;
- if (checked) {
- selectedItems.add(url);
- } else {
- selectedItems.delete(url);
- }
- }
+ if (!checkbox) return;
+ checkbox.checked = checked;
+ if (!checked) return;
+ selectedItems.add(item.querySelector("data-url").textContent);
});
updateSelectionCount($count);
};
@@ -863,11 +809,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
const checkbox = Checkbox("", false);
checkbox.onclick = () => {
const url = item.querySelector("data-url").textContent;
- if (checkbox.checked) {
- selectedItems.add(url);
- } else {
- selectedItems.delete(url);
- }
+ selectedItems[checkbox.checked ? "add" : "delete"](url);
updateSelectionCount($count);
};
item.prepend(checkbox);
@@ -876,12 +818,6 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
$addMenuToggler.style.display = "none";
$menuToggler.style.display = "none";
$selectionMenuToggler.style.display = "";
- updatePasteToggler();
-
- // Disable floating button in selection mode
- if ($openFolder) {
- $openFolder.disabled = true;
- }
} else {
$list.classList.remove("selection-mode");
$list.querySelector(".selection-header")?.remove();
@@ -891,13 +827,11 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
$addMenuToggler.style.display = "";
$menuToggler.style.display = "";
$selectionMenuToggler.style.display = "none";
- updatePasteToggler();
-
- // Re-enable floating button when exiting selection mode
- if ($openFolder) {
- $openFolder.disabled = false;
- }
}
+ updatePasteToggler();
+
+ // Disable floating button when entering selection mode, and vice versa
+ if ($openFolder) $openFolder.disabled = active;
}
/**
@@ -912,17 +846,13 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
const $el = e.target;
if (isSelectionMode) {
- const checkbox = $el.closest(".tile")?.querySelector(".input-checkbox");
+ const $el2 = $el.closest(".tile");
+ const checkbox = $el2?.querySelector(".input-checkbox");
if (checkbox && !$el.closest(".selection-header")) {
- checkbox.checked = !checkbox.checked;
- const url = $el
- .closest(".tile")
- .querySelector("data-url").textContent;
- if (checkbox.checked) {
- selectedItems.add(url);
- } else {
- selectedItems.delete(url);
- }
+ const checked = !checkbox.checked;
+ checkbox.checked = checked;
+ const url = $el2.querySelector("data-url").textContent;
+ selectedItems[checked ? "add" : "delete"](url);
const $count = $content.querySelector(".selection-count");
updateSelectionCount($count);
}
@@ -943,20 +873,11 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
if (!url) {
const $url = $el.get("data-url");
- if ($url) {
- url = $url.textContent;
- }
+ if ($url) url = $url.textContent;
}
if (storageType === "notification") {
- switch (uuid) {
- case "addstorage":
- addStorage();
- break;
-
- default:
- break;
- }
+ if (uuid === "addstorage") addStorage();
return;
}
@@ -990,16 +911,11 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
break;
case "openDoc":
openDoc();
- break;
}
async function folder() {
- if (home) {
- navigateToHome();
- return;
- }
-
- navigate(url, name);
+ if (home) navigateToHome();
+ else navigate(url, name);
}
function navigateToHome() {
@@ -1032,24 +948,21 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
if (!fileUrl) return null;
try {
const fs = fsOperation(fileUrl);
- if (/^s?ftp:/.test(fileUrl)) {
- return fs.localName;
- }
- const stat = await fs.stat();
- return stat?.url || null;
- } catch (error) {
+ if (/^s?ftp:/.test(fileUrl)) return fs.localName;
+ return (await fs.stat())?.url || null;
+ } catch {
return null;
}
}
async function contextMenuHandler() {
+ if (isOpenDoc) return;
if (appSettings.value.vibrateOnTap) {
navigator.vibrate(config.VIBRATION_TIME);
}
- if (isOpenDoc) return;
const deleteText =
- currentDir.url === "/" ? strings.remove : strings.delete;
+ strings[currentDir.url === "/" ? "remove" : "delete"];
const options = [
["delete", deleteText, "delete"],
["rename", strings.rename, "text_format"],
@@ -1071,16 +984,13 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
const option = await select(strings["select"], options);
switch (option) {
case "delete": {
- let deleteFunction = removeFile;
- let message = strings["delete entry"].replace("{name}", name);
- if (uuid) {
- deleteFunction = removeStorage;
- message = strings["remove entry"].replace("{name}", name);
- }
+ const deleteFunction = uuid ? removeStorage : removeFile;
+ const message = strings[
+ `${uuid ? "remove" : "delete"} entry`
+ ].replace("{name}", name);
const confirmation = await confirm(strings.warning, message);
- if (!confirmation) break;
- deleteFunction();
+ if (confirmation) deleteFunction();
break;
}
@@ -1090,10 +1000,9 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
});
newname = helpers.fixFilename(newname);
- if (!newname || newname === name) break;
-
- if (uuid) renameStorage(newname);
- else renameFile(newname);
+ if (newname && newname !== name) {
+ (uuid ? renameStorage : renameFile)(newname);
+ }
break;
}
@@ -1146,45 +1055,29 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
console.error(error);
toast(strings.error);
}
- break;
}
}
async function renameFile(newname) {
- if (url.startsWith("content://com.termux.documents/tree/")) {
- if (helpers.isDir(type)) {
- alert(strings.warning, strings["rename not supported"]);
- return;
- } else {
- // Special handling for Termux content files
- const fs = fsOperation(url);
- try {
+ let newUrl;
+ try {
+ if (isTermuxUrl(url)) {
+ if (helpers.isDir(type)) {
+ alert(strings.warning, strings["rename not supported"]);
+ return;
+ } else {
+ // Special handling for Termux content files
+ const fs = fsOperation(url);
const content = await fs.readFile();
- const newUrl = Url.join(Url.dirname(url), newname);
- await fsOperation(Url.dirname(url)).createFile(newname, content);
+ const dirname = Url.dirname(url);
+ newUrl = Url.join(dirname, newname);
+ await fsOperation(dirname).createFile(newname, content);
await fs.delete();
-
- recents.removeFile(url);
- recents.addFile(newUrl);
- const file = editorManager.getFile(url, "uri");
- if (file) {
- file.uri = newUrl;
- file.filename = newname;
- }
- openFolder.renameItem(url, newUrl, newname);
- toast(strings.success);
- reload();
- return;
- } catch (err) {
- window.log("error", err);
- helpers.error(err);
- return;
}
+ } else {
+ const fs = fsOperation(url);
+ newUrl = await fs.renameTo(newname);
}
- }
- const fs = fsOperation(url);
- try {
- const newUrl = await fs.renameTo(newname);
recents.removeFile(url);
recents.addFile(newUrl);
const file = editorManager.getFile(url, "uri");
@@ -1203,42 +1096,8 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
async function removeFile() {
try {
- if (helpers.isDir(type)) {
- if (url.startsWith("content://com.termux.documents/tree/")) {
- const fs = fsOperation(url);
- const entries = await fs.lsDir();
- if (entries.length === 0) {
- await fs.delete();
- } else {
- const deleteRecursively = async (currentUrl) => {
- const currentFs = fsOperation(currentUrl);
- const currentEntries = await currentFs.lsDir();
- for (const entry of currentEntries) {
- if (entry.isDirectory) {
- await deleteRecursively(entry.url);
- } else {
- await fsOperation(entry.url).delete();
- }
- }
- await currentFs.delete();
- };
- await deleteRecursively(url);
- }
- } else {
- await fsOperation(url).delete();
- }
- helpers.updateUriOfAllActiveFiles(url);
- recents.removeFolder(url);
- } else {
- const fs = fsOperation(url);
- await fs.delete();
- const openedFile = editorManager.getFile(url, "uri");
- if (openedFile) openedFile.uri = null;
- }
- recents.removeFile(url);
- openFolder.removeItem(url);
+ await deleteDirOrFile(url, type);
toast(strings.success);
- delete cachedDir[url];
reload();
} catch (err) {
window.log("error", err);
@@ -1252,18 +1111,14 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
recents.removeFile(url);
}
storageList = storageList.filter((storage) => {
- if (storage.uuid !== uuid) {
- return true;
- }
+ if (storage.uuid !== uuid) return true;
if (storage.url) {
const parsedUrl = URLParse(storage.url, true);
const keyFile = decodeURIComponent(
parsedUrl.query["keyFile"] || "",
);
- if (keyFile) {
- fsOperation(keyFile).delete();
- }
+ if (keyFile) fsOperation(keyFile).delete();
}
return false;
});
@@ -1293,9 +1148,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
});
$page.hide();
},
- (err) => {
- helpers.error(err);
- },
+ (err) => helpers.error(err),
);
}
}
@@ -1408,84 +1261,67 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
/**
* Gets directory for given url for rendering
- * @param {String} url
- * @param {String} name
- * @returns {Promise<{name: String, url: String, list: [], scroll: Number}>}
+ * @param {string} url
+ * @param {string} name
+ * @returns {Promise<{name: string, url: string, list: object[], scroll: number}>}
*/
async function getDir(url, name) {
+ if (url in cachedDir) return cachedDir[url];
+
const { fileBrowser } = appSettings.value;
let list = [];
- let error = false;
- if (url in cachedDir) {
- return cachedDir[url];
- } else {
- if (url === "/") {
- list = await listAllStorages();
- } else {
- const id = helpers.uuid();
-
- progress[id] = true;
- const timeout = setTimeout(() => {
- loader.create(name, strings.loading + "...", {
- timeout: 10000,
- callback() {
- loader.destroy();
- navigate("/", "/");
- progress[id] = false;
- },
- });
- }, 100);
+ if (url === "/") list = await listAllStorages();
+ else {
+ const id = helpers.uuid();
+
+ progress[id] = true;
+ const timeout = setTimeout(() => {
+ loader.create(name, strings.loading + "...", {
+ timeout: 10000,
+ callback() {
+ loader.destroy();
+ navigate("/", "/");
+ progress[id] = false;
+ },
+ });
+ }, 100);
+ try {
const fs = fsOperation(url);
- try {
- list = (await fs.lsDir()) ?? [];
- } catch (err) {
- if (progress[id]) {
- helpers.error(err, url);
- } else {
- console.error(err);
- }
- }
-
- error = !progress[id];
-
- delete progress[id];
- clearTimeout(timeout);
- loader.destroy();
+ list = (await fs.lsDir()) ?? [];
+ } catch (err) {
+ if (!progress[id]) console.error(err);
+ else helpers.error(err, url);
}
+
+ const error = !progress[id];
+ delete progress[id];
+ clearTimeout(timeout);
+ loader.destroy();
if (error) return null;
- return {
- url,
- name,
- scroll: 0,
- list: helpers.sortDir(list, fileBrowser, mode),
- };
}
+
+ return {
+ url,
+ name,
+ scroll: 0,
+ list: helpers.sortDir(list, fileBrowser, mode),
+ };
}
/**
* Navigates to specific directory
- * @param {String} url
- * @param {String} name
+ * @param {string} url
+ * @param {string} name
*/
async function navigate(url, name, assignBackButton = true) {
- if (document.getElementById("search-bar")) {
- hideSearchBar();
- }
- if (!url) {
- throw new Error('navigate(url, name): "url" is required.');
- }
+ if (document.getElementById("search-bar")) hideSearchBar();
- if (!name) {
- throw new Error('navigate(url, name): "name" is required.');
- }
+ if (!url) throw new Error('navigate(url, name): "url" is required.');
+ if (!name) throw new Error('navigate(url, name): "name" is required.');
- if (url === "/") {
- if (IS_FOLDER_MODE) $openFolder.disabled = true;
- } else {
- if (IS_FOLDER_MODE) $openFolder.disabled = false;
- }
+ if (IS_FOLDER_MODE) $openFolder.disabled = url === "/";
const $nav = tag.get(`#${getNavId(url)}`);
@@ -1506,9 +1342,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
localStorage.fileBrowserState = JSON.stringify(state);
const dir = await getDir(url, name);
- if (dir) {
- render(dir);
- }
+ if (dir) render(dir);
return;
}
@@ -1518,9 +1352,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
let action;
if (doesOpenLast) pushState({ name, url });
if (curl && cname && assignBackButton) {
- action = () => {
- navigate(curl, cname, false);
- };
+ action = () => navigate(curl, cname, false);
}
pushToNavbar(name, url, action);
render(dir);
@@ -1541,55 +1373,52 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
let project = "";
let newUrl;
- if (arg === "file" || arg === "folder") {
- let title = strings["enter folder name"];
- if (arg === "file") {
- title = strings["enter file name"];
- }
+ switch (arg) {
+ case "file":
+ case "folder": {
+ const title = strings[`enter ${arg} name`];
- let entryName = await prompt(title, "", "filename", {
- match: config.FILE_NAME_REGEX,
- required: true,
- });
+ let entryName = await prompt(title, "", "filename", {
+ match: config.FILE_NAME_REGEX,
+ required: true,
+ });
- if (!entryName) return;
- entryName = helpers.fixFilename(entryName);
+ if (!entryName) return;
+ entryName = helpers.fixFilename(entryName);
- if (arg === "folder") {
- newUrl = await helpers.createFileStructure(url, entryName, false);
- }
- if (arg === "file") {
- newUrl = await helpers.createFileStructure(url, entryName);
+ newUrl = await helpers.createFileStructure(
+ url,
+ entryName,
+ arg === "file",
+ );
+ return newUrl.created ? newUrl.uri : null;
}
- if (!newUrl.created) return;
- return newUrl.uri;
- }
-
- if (arg === "project") {
- projects.list().map((project) => {
- const { name, icon } = project;
- options.push([name, name, icon]);
- });
+ case "project": {
+ projects.list().map((project) => {
+ const { name, icon } = project;
+ options.push([name, name, icon]);
+ });
- project = await select(strings["new project"], options);
- loader.create(project, strings.loading + "...");
- projectFiles = await projects.get(project).files();
- loader.destroy();
- projectName = await prompt(strings["project name"], project, "text", {
- required: true,
- match: config.FILE_NAME_REGEX,
- });
+ project = await select(strings["new project"], options);
+ loader.create(project, strings.loading + "...");
+ projectFiles = await projects.get(project).files();
+ loader.destroy();
+ projectName = await prompt(strings["project name"], project, "text", {
+ required: true,
+ match: config.FILE_NAME_REGEX,
+ });
- if (!projectName) return;
- loader.create(projectName, strings.loading + "...");
- const fs = fsOperation(url);
- const files = Object.keys(projectFiles); // All project files
+ if (!projectName) return;
+ loader.create(projectName, strings.loading + "...");
+ const fs = fsOperation(url);
+ const files = Object.keys(projectFiles); // All project files
- newUrl = await fs.createDirectory(projectName);
- projectLocation = Url.join(url, projectName, "/");
- await createProject(files); // Creating project
- loader.destroy();
- return newUrl;
+ newUrl = await fs.createDirectory(projectName);
+ projectLocation = Url.join(url, projectName, "/");
+ await createProject(files); // Creating project
+ loader.destroy();
+ return newUrl;
+ }
}
async function createProject(files) {
@@ -1633,9 +1462,9 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
/**
* Pushes a navigation button to navbar
- * @param {String} id
- * @param {String} name
- * @param {String} url
+ * @param {string} id
+ * @param {string} name
+ * @param {string} url
*/
function pushToNavbar(name, url, action) {
if (!url) return;
@@ -1677,9 +1506,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
while (states.length) {
const location = states.splice(0, 1)[0];
- if (!location || !location.url) {
- continue;
- }
+ if (!location?.url) continue;
const { url, name } = location;
let action;
@@ -1702,7 +1529,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
/**
*
- * @param {String} url
+ * @param {string} url
*/
function getNavId(url) {
return `nav_${url.hashCode()}`;
@@ -1711,7 +1538,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
/**
*
* @param {Storage} storage
- * @param {Boolean} doesReload
+ * @param {boolean} doesReload
*/
function updateStorage(storage, doesReload = true) {
if (storage.uuid) {
@@ -1720,13 +1547,9 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
storage.uuid = helpers.uuid();
}
- if (!storage.type) {
- storage.type = "dir";
- }
+ storage.type ||= "dir";
- if (!storage.storageType) {
- storage.storageType = storage.type;
- }
+ storage.storageType ||= storage.type;
storageList.push(storage);
localStorage.storageList = JSON.stringify(storageList);
@@ -1742,9 +1565,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
}),
);
- if (document.getElementById("search-bar")) {
- hideSearchBar();
- }
+ if (document.getElementById("search-bar")) hideSearchBar();
const $oldList = $content.get("#list");
if ($oldList) {
@@ -1779,17 +1600,14 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
/**
* Adds a new storage and refresh location
*/
- function addStorage() {
- util
- .addPath()
- .then((res) => {
- storageList.push(res);
- localStorage.storageList = JSON.stringify(storageList);
- reload();
- })
- .catch((err) => {
- helpers.error(err);
- });
+ async function addStorage() {
+ try {
+ storageList.push(await util.addPath());
+ localStorage.storageList = JSON.stringify(storageList);
+ reload();
+ } catch (err) {
+ helpers.error(err);
+ }
}
});
}
From 390430cf098596e8cdca9533049cfd986475140d Mon Sep 17 00:00:00 2001
From: AuDevTist1C <114492072+AuDevTist1C@users.noreply.github.com>
Date: Sat, 25 Jul 2026 09:27:52 +0200
Subject: [PATCH 3/7] feat: integrate file selection mode with application
action stack for back navigation
Integrate the file browser's multi-item selection mode directly into the application's global `actionStack` navigation controller to provide native hardware and interface back-button support.
When users enter multi-selection mode (e.g., long-pressing a file or checking multiple items), pressing the hardware back button or triggering back-gestures previously caused the application to navigate away from the current folder entirely, losing active context. This update registers selection mode as an isolated state layer within the global `actionStack`.
* **Action Stack Registration:**
* Upon entering selection mode, the file browser pushes a `fbSelection` state descriptor to `actionStack`.
* The back action handler captures `fbSelection` events to intercept back navigation, clearing all checked items and exiting selection mode first before any folder pops occur.
* **Stack Lifecycle Cleanup:**
* Implemented automatic cleanup hooks to pop or unregister the `fbSelection` entry from `actionStack` whenever selection mode is dismissed through UI confirmation, cancel buttons, or batch execution tasks.
(AI generated commit message)
---
src/pages/fileBrowser/fileBrowser.js | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js
index 5d9b297c7..f5f49683e 100644
--- a/src/pages/fileBrowser/fileBrowser.js
+++ b/src/pages/fileBrowser/fileBrowser.js
@@ -818,7 +818,18 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
$addMenuToggler.style.display = "none";
$menuToggler.style.display = "none";
$selectionMenuToggler.style.display = "";
+ if (!actionStack.has("fbSelection")) {
+ actionStack.push({
+ id: "fbSelection",
+ action: () => {
+ isSelectionMode = false;
+ toggleSelectionMode(false);
+ },
+ });
+ }
} else {
+ actionStack.remove("fbSelection");
+
$list.classList.remove("selection-mode");
$list.querySelector(".selection-header")?.remove();
$list.querySelectorAll(".input-checkbox").forEach((cb) => cb.remove());
From b780fcec07ebbdfeee4b8e04b37f2baf73241017 Mon Sep 17 00:00:00 2001
From: AuDevTist1C <114492072+AuDevTist1C@users.noreply.github.com>
Date: Sat, 25 Jul 2026 09:48:47 +0200
Subject: [PATCH 4/7] fix: introduce non-selectable item handling to isolate
system utility tiles during batch operations
Add non-selectable element safeguards across templates and touch selection handlers to prevent system utility tiles from being highlighted, checked, or included in multi-item batch actions.
System notification tiles, such as "Add a storage" prompts and "Select document" system actions, reside in the same DOM list container as regular files and directories. Previously, initiating a "Select All" command or dragging across the file list would accidentally select these utility items, triggering runtime errors during batch operations like deletion or moving.
* **Template Contract (`src/pages/fileBrowser/list.hbs`):**
* Added support for the `data-not-selectable` HTML dataset attribute flag on list item nodes.
* **Tile Configuration:**
* Explicitly configured system utility tiles ("add a storage" and "Select document") with `notSelectable: true` in their template render context.
* **Selection Safeguards (`src/pages/fileBrowser/fileBrowser.js`):**
* Updated touch event delegation, long-press handlers, and "Select All" toggle utility functions to check for `dataset.notSelectable != null`.
* Items flagged as non-selectable are automatically bypassed during selection count updates, bulk checkboxes, and context action payload generation.
(AI generated commit message)
---
src/pages/fileBrowser/fileBrowser.js | 4 ++++
src/pages/fileBrowser/list.hbs | 1 +
2 files changed, 5 insertions(+)
diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js
index f5f49683e..ee98b8c65 100644
--- a/src/pages/fileBrowser/fileBrowser.js
+++ b/src/pages/fileBrowser/fileBrowser.js
@@ -806,6 +806,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
$list
.querySelectorAll(".tile:not(.selection-header)")
.forEach((item) => {
+ if (item.dataset.notSelectable != null) return;
const checkbox = Checkbox("", false);
checkbox.onclick = () => {
const url = item.querySelector("data-url").textContent;
@@ -858,6 +859,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
if (isSelectionMode) {
const $el2 = $el.closest(".tile");
+ if ($el2?.dataset.notSelectable != null) return;
const checkbox = $el2?.querySelector(".input-checkbox");
if (checkbox && !$el.closest(".selection-header")) {
const checked = !checkbox.checked;
@@ -1258,12 +1260,14 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
util.pushFolder(allStorages, strings["add a storage"], "", {
storageType: "notification",
uuid: "addstorage",
+ notSelectable: true,
});
}
if (IS_FILE_MODE) {
util.pushFolder(allStorages, "Select document", null, {
openDoc: true,
+ notSelectable: true,
});
}
diff --git a/src/pages/fileBrowser/list.hbs b/src/pages/fileBrowser/list.hbs
index ade7b9434..0eac42975 100644
--- a/src/pages/fileBrowser/list.hbs
+++ b/src/pages/fileBrowser/list.hbs
@@ -7,6 +7,7 @@
data-type="{{type}}"
data-name="{{name}}"
{{#home}}data-home="{{.}}"{{/home}}
+ {{#notSelectable}}data-not-selectable{{/notSelectable}}
{{#openDoc}}data-open-doc{{/openDoc}}
{{#ftpAccount}}data-ftp-account{{/ftpAccount}}
{{#uuid}}data-uuid="{{uuid}}"{{/uuid}}
From 4271dd953e60790483bc7df75e17c1a986e1eb4e Mon Sep 17 00:00:00 2001
From: AuDevTist1C <114492072+AuDevTist1C@users.noreply.github.com>
Date: Fri, 24 Jul 2026 10:25:51 +0200
Subject: [PATCH 5/7] refactor: overhaul navigation state management and adopt
granular list item rendering with skeleton loading
Overhaul navigation architecture and list view rendering performance by decoupling history tracking into an event-driven `NavStack` class and replacing full-list re-renders with granular element construction and visual skeleton states.
Replaced synchronous inline path tracking with a dedicated event-driven state container, and decomposed monolithic Handlebars list rendering into modular item-level template components to reduce DOM churn and layout thrashing.
* **Navigation Stack Module (`src/pages/fileBrowser/NavStack.js`):**
* Created a dedicated `NavStack` class extending `EventTarget` to encapsulate navigation history depth, root boundaries, and path traversal logic.
* Emits typed `push` and `pop` DOM events to enable loose coupling between location changes and rendering triggers.
* **Modular Template Rendering (`src/pages/fileBrowser/listItem.hbs`):**
* Removed monolithic `list.hbs` template in favor of lightweight `listItem.hbs` partials.
* Refactored `fileBrowser.js` to dynamically generate DOM elements per record via `createListItemEl()`, significantly reducing template compilation overhead.
* **Skeleton Placeholder States (`src/pages/fileBrowser/fileBrowser.scss`):**
* Added CSS skeleton loading placeholders (`.placeholder` classes) to preserve container dimensions while async directory listing resolves.
* **Asynchronous Render Pipeline (`src/pages/fileBrowser/fileBrowser.js`):**
* Synchronized `navigate()` and `renderCurrentDir()` directly with `NavStack` event listeners to ensure predictable view state updates.
(AI generated commit message)
---
src/pages/fileBrowser/NavStack.js | 106 +++++++++++
src/pages/fileBrowser/fileBrowser.js | 232 +++++++++++--------------
src/pages/fileBrowser/fileBrowser.scss | 16 ++
src/pages/fileBrowser/list.hbs | 29 ----
src/pages/fileBrowser/listItem.hbs | 24 +++
5 files changed, 248 insertions(+), 159 deletions(-)
create mode 100644 src/pages/fileBrowser/NavStack.js
delete mode 100644 src/pages/fileBrowser/list.hbs
create mode 100644 src/pages/fileBrowser/listItem.hbs
diff --git a/src/pages/fileBrowser/NavStack.js b/src/pages/fileBrowser/NavStack.js
new file mode 100644
index 000000000..4da17d706
--- /dev/null
+++ b/src/pages/fileBrowser/NavStack.js
@@ -0,0 +1,106 @@
+import Url from "utils/Url";
+
+/**
+ * @typedef {import("./fileBrowser.js").Location} Location
+ */
+
+export default class NavStack extends EventTarget {
+ static {
+ Object.defineProperty(this.prototype, Symbol.toStringTag, {
+ value: "NavStack",
+ configurable: true,
+ });
+ }
+
+ /** @type {Set} */
+ #urlSet = new Set();
+ /** @type {Array} */
+ #arr = [];
+ /**
+ * @param {{ url: string, name?: string }}
+ */
+ push({ url, name }) {
+ if (!(url = `${url ?? ""}`)) {
+ throw new TypeError(
+ "NavStack.prototype.push({ url: string, name?: string }): \n" +
+ '"url" is either missing, null or undefined, or resolves to an empty string.',
+ );
+ }
+ const urlSet = this.#urlSet;
+ if (urlSet.has(url)) return;
+ urlSet.add(url);
+ name = `${name ?? ""}` || Url.basename(url) || url;
+ this.#arr.push({ url, name });
+ this.dispatchEvent(
+ new CustomEvent("push", {
+ detail: { url, name },
+ }),
+ );
+ }
+ /**
+ * @param {string} [url]
+ */
+ #popUntil(url) {
+ const urlSet = this.#urlSet;
+ const arr = this.#arr;
+ for (let i = arr.length - 1; i >= 0; i--) {
+ const item = arr[i];
+ const url2 = item.url;
+ if (url && url === url2) return;
+ this.#urlSet.delete(url2);
+ arr.length = i;
+ this.dispatchEvent(
+ new CustomEvent("pop", {
+ detail: { url: url2, name: item.name },
+ }),
+ );
+ if (!url) return;
+ }
+ }
+ /**
+ * @param {string} url
+ */
+ popUntil(url) {
+ if ((url = `${url ?? ""}`)) return this.#popUntil(url);
+ throw new TypeError(
+ "NavStack.prototype.popUntil(url: string): \n" +
+ '"url" is either missing, null or undefined, or resolves to an empty string.',
+ );
+ }
+ pop() {
+ return this.#popUntil();
+ }
+ /**
+ * @param {number} i
+ * @returns {Location}
+ */
+ get(i) {
+ if ((i = +i) !== i) {
+ throw new TypeError(
+ 'NavStack.prototype.get(i: number): "i" is either missing or resolves to NaN.',
+ );
+ }
+ const arr = this.#arr;
+ const l = arr.length;
+ if (i < 0) i += l;
+ if (i < 0 || i > l - 1) return;
+ return { ...arr[i] };
+ }
+ has(url) {
+ return this.#urlSet.has(`${url ?? ""}`);
+ }
+ /** @returns {number} */
+ get length() {
+ return this.#arr.length;
+ }
+ /** @returns {Array} */
+ toJSON() {
+ return this.#arr.map((obj) => ({ ...obj }));
+ }
+ on() {
+ return this.addEventListener(...arguments);
+ }
+ off() {
+ return this.removeEventListener(...arguments);
+ }
+}
diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js
index ee98b8c65..835f2a60b 100644
--- a/src/pages/fileBrowser/fileBrowser.js
+++ b/src/pages/fileBrowser/fileBrowser.js
@@ -31,7 +31,8 @@ import Url from "utils/Url";
import _addMenu from "./add-menu.hbs";
import _addMenuHome from "./add-menu-home.hbs";
import _template from "./fileBrowser.hbs";
-import _list from "./list.hbs";
+import _listItem from "./listItem.hbs";
+import NavStack from "./NavStack";
import util from "./util";
/**
@@ -57,11 +58,10 @@ import util from "./util";
function FileBrowserInclude(mode, info, doesOpenLast = true) {
mode ||= "file";
+ const navStack = new NavStack();
const IS_FOLDER_MODE = ["folder", "both"].includes(mode);
const IS_FILE_MODE = ["file", "both"].includes(mode);
const storedState = helpers.parseJSON(localStorage.fileBrowserState) || [];
- /**@type {Array} */
- const state = [];
/**@type {Array} */
const allStorages = [];
let storageList = helpers.parseJSON(localStorage.storageList);
@@ -165,6 +165,9 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
list: [],
scroll: 0,
};
+ /** @type {AbortController|null} */
+ let _rndrAbortCtrl;
+
/**
* @type {HTMLButtonElement}
*/
@@ -575,6 +578,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
};
$page.onhide = function () {
+ _rndrAbortCtrl?.abort();
hideSearchBar();
hideAd();
actionStack.clearFromMark();
@@ -584,7 +588,29 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
document.removeEventListener("resume", reload);
};
+ const saveFileBrowserState = doesOpenLast
+ ? () => (localStorage.fileBrowserState = JSON.stringify(navStack))
+ : null;
+ navStack.addEventListener("pop", (ev) => {
+ saveFileBrowserState?.();
+ const { url } = ev.detail;
+ actionStack.remove(url);
+ tag.get(`#${getNavId(url)}`)?.remove();
+ });
+ navStack.addEventListener("push", (ev) => {
+ saveFileBrowserState?.();
+ let action;
+ const prevDir = navStack.get(-2);
+ if (prevDir) {
+ const { url, name } = prevDir;
+ action = () => navigate(url, name);
+ }
+ const dir = ev.detail;
+ pushToNavbar(dir.name, dir.url, action);
+ });
+
if (doesOpenLast && storedState.length) {
+ navStack.push({ url: "/", name: "/" });
loadStates(storedState);
return;
}
@@ -1277,52 +1303,26 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
/**
* Gets directory for given url for rendering
* @param {string} url
- * @param {string} name
- * @returns {Promise<{name: string, url: string, list: object[], scroll: number}>}
+ * @returns {Promise