From d3c304a732adecf0c692e5e9ddb198670e6d6b8f 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 01/10] 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 | 6 +++--- src/pages/fileBrowser/list.hbs | 18 +++++++++--------- 3 files changed, 25 insertions(+), 25 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..769090e05 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,11 +22,11 @@ position: relative; color: rgb(65, 85, 133); - &[storageType]::after { + &[data-storage-type]::after { position: absolute; top: 50%; left: 50%; - content: attr(storageType); + content: attr(data-storage-type); transform: translate(-50%, -50%); color: rgb(255, 255, 255); font-size: 0.6rem; 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 325a9627256fb9a57ae640236320dc63302c6a52 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 02/10] 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 | 758 ++++++++++----------------- 1 file changed, 288 insertions(+), 470 deletions(-) diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index 2344fb4c0..d60025fc5 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) { @@ -1632,10 +1461,10 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { } /** - * Pushes a navigation button to navbar - * @param {String} id - * @param {String} name - * @param {String} url + * Pushes a navigation button to navbar + * @param {string} name + * @param {string} url + * @param {() => void} [action] */ 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 401ac0b426b781c28ebc7230404fb2996e7d569b 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 03/10] 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 d60025fc5..79dc32453 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 7824d081b051cbcf1382953324198d9129f98875 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 04/10] 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 79dc32453..073c3cd1e 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 0f755139d2d912cf613e19fc3ac5549beb37e26e 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 05/10] 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 | 247 +++++++++++-------------- src/pages/fileBrowser/fileBrowser.scss | 16 ++ src/pages/fileBrowser/list.hbs | 29 --- src/pages/fileBrowser/listItem.hbs | 24 +++ 5 files changed, 252 insertions(+), 170 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 073c3cd1e..d62e7c14b 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,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { list: [], scroll: 0, }; + /** * @type {HTMLButtonElement} */ @@ -234,8 +235,6 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { break; } case "reload": { - const { url } = currentDir; - if (url in cachedDir) delete cachedDir[url]; reload(); break; } @@ -584,7 +583,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 +1298,23 @@ 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} */ - async function getDir(url, name) { - if (url in cachedDir) return cachedDir[url]; - - const { fileBrowser } = appSettings.value; - let list = []; - - 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); + async function getDirList(url) { + try { + let list = await (url === "/" + ? listAllStorages() + : fsOperation(url).lsDir()); - try { - const fs = fsOperation(url); - list = (await fs.lsDir()) ?? []; - } catch (err) { - if (!progress[id]) console.error(err); - else helpers.error(err, url); + if (list?.length) { + const { fileBrowser } = appSettings.value; + list = helpers.sortDir(list, fileBrowser, mode); } - const error = !progress[id]; - delete progress[id]; - clearTimeout(timeout); - loader.destroy(); - if (error) return null; + return list ?? []; + } catch (err) { + helpers.error(err, url); } - - return { - url, - name, - scroll: 0, - list: helpers.sortDir(list, fileBrowser, mode), - }; } /** @@ -1330,48 +1322,11 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { * @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 (!name) throw new Error('navigate(url, name): "name" is required.'); - - if (IS_FOLDER_MODE) $openFolder.disabled = url === "/"; - - const $nav = tag.get(`#${getNavId(url)}`); - - //If navigate to previous directories, clear the rest navigation - if ($nav) { - let $topNav; - while (($topNav = $navigation.lastChild) !== $nav) { - const url = $topNav.dataset.url; - actionStack.remove(url); - $topNav.remove(); - } - - while (1) { - const location = state.slice(-1)[0]; - if (!location || location.url === url) break; - state.pop(); - } - localStorage.fileBrowserState = JSON.stringify(state); - - const dir = await getDir(url, name); - if (dir) render(dir); - return; - } - - const dir = await getDir(url, name); - if (dir) { - const { url: curl, name: cname } = currentDir; - let action; - if (doesOpenLast) pushState({ name, url }); - if (curl && cname && assignBackButton) { - action = () => navigate(curl, cname, false); - } - pushToNavbar(name, url, action); - render(dir); - } + function navigate(url, name) { + const inStack = navStack.has(url); + if (inStack) navStack.popUntil(url); + else navStack.push({ url, name }); + renderCurrentDir(); } /** @@ -1511,35 +1466,15 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { */ function loadStates(states) { if (!Array.isArray(states) || !states.length) return; - - const backNavigation = []; - const lastState = states.pop(); - if (!lastState || !lastState.url) return; - const { url } = lastState; - const name = lastState.name || Url.basename(url) || url; - let { url: lastUrl, name: lastName } = currentDir; - while (states.length) { - const location = states.splice(0, 1)[0]; - if (!location?.url) continue; - const { url, name } = location; - let action; - - if (doesOpenLast) pushState({ name, url }); - if (lastUrl && lastName) { - backNavigation.push([lastUrl, lastName]); - action = () => { - const [url, name] = backNavigation.pop(); - navigate(url, name, false); - }; + try { + navStack.push(states.shift()); + } catch (err) { + console.error(err); } - pushToNavbar(name, url, action); - lastUrl = url; - lastName = name; } - - currentDir = { url: lastUrl, name: lastName }; - navigate(url, name); + const dir = navStack.get(-1); + if (dir) navigate(dir.url, dir.name); } /** @@ -1571,45 +1506,75 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { if (doesReload) reload(); } - function render(dir) { - const { list, scroll } = dir; - const $list = helpers.parseHTML( - mustache.render(_list, { - msg: strings["empty folder message"], - list, - }), - ); + /** + * @returns {HTMLUListElement} + */ + function createListEl() { + return
      ; + } + + /** + * @param {object} obj + * @returns {HTMLLIElement} + */ + function createListItemEl(obj) { + return helpers.parseHTML(mustache.render(_listItem, obj)); + } + /** + * @param {boolean} force + */ + async function renderCurrentDir(force) { if (document.getElementById("search-bar")) hideSearchBar(); + const { url, name } = navStack.get(-1) ?? {}; + if (IS_FOLDER_MODE) $openFolder.disabled = !url || url === "/"; + const $oldList = $content.get("#list"); - if ($oldList) { - const { url } = currentDir; - if (url && cachedDir[url]) { - cachedDir[url].scroll = $oldList.scrollTop; - } - $oldList.remove(); + if (!url) return void $oldList?.remove(); + + const $list = createListEl(); + + if (!$oldList) $content.append($list); + else { + const dir = currentDir; + if (dir) dir.scroll = $oldList.scrollTop; + $content.replaceChild($list, $oldList); } - $content.append($list); - $list.scrollTop = scroll; $list.focus(); + if (force) delete cachedDir[url]; + const dir = (!force && url && cachedDir[url]) || { url, name, scroll: 0 }; currentDir = dir; - cachedDir[dir.url] = dir; + updatePasteToggler(); - } - function reload() { - const { url, name } = currentDir; - delete cachedDir[url]; - navigate(url, name); + let { list } = dir; + const fg = new DocumentFragment(); + if (!list) { + let plh = { placeholder: true }; + for (let i = Math.ceil($list.clientHeight / 45); i-- > 0; ) { + const el = createListItemEl(plh); + el.querySelector(".text span").style.width = + `calc((100% - 5px) * ${Math.random()})`; + fg.append(el); + } + $list.append(fg); + dir.list = list = await getDirList(url); + } + + for (let l = list?.length, i = 0; i < l; i++) { + fg.append(createListItemEl(list[i])); + } + $list.replaceChildren(fg); + + if (!list) return; + $list.scrollTop = +dir.scroll || 0; + cachedDir[url] = dir; } - function pushState({ url, name }) { - if (!url || !name) return; - if (state.find((l) => l.url === url)) return; - state.push({ url, name }); - localStorage.fileBrowserState = JSON.stringify(state); + function reload() { + renderCurrentDir(true); } /** diff --git a/src/pages/fileBrowser/fileBrowser.scss b/src/pages/fileBrowser/fileBrowser.scss index 769090e05..b1e2b3089 100644 --- a/src/pages/fileBrowser/fileBrowser.scss +++ b/src/pages/fileBrowser/fileBrowser.scss @@ -107,6 +107,22 @@ margin: auto 15px; } } + + &.placeholder { + pointer-events: none; + .icon { + background-color: #8886; + border-radius: 50%; + scale: 0.8; + } + .text span { + display: block; + background-color: #8886; + border-radius: calc(1px / 0); + height: 1em; + min-width: 1em; + } + } } } .selection-header { diff --git a/src/pages/fileBrowser/list.hbs b/src/pages/fileBrowser/list.hbs deleted file mode 100644 index 0eac42975..000000000 --- a/src/pages/fileBrowser/list.hbs +++ /dev/null @@ -1,29 +0,0 @@ -
        {{#list}} - {{#.}} -
      • - - -
        - {{name}} -
        - {{url}} -
      • - {{/.}} - {{/list}} -
      diff --git a/src/pages/fileBrowser/listItem.hbs b/src/pages/fileBrowser/listItem.hbs new file mode 100644 index 000000000..2744be3fb --- /dev/null +++ b/src/pages/fileBrowser/listItem.hbs @@ -0,0 +1,24 @@ +
    • + + +
      + {{name}} +
      + {{url}} +
    • From 8b4871d8a24b03709edad89dec1c296920bf190a Mon Sep 17 00:00:00 2001 From: AuDevTist1C <114492072+AuDevTist1C@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:44:15 +0200 Subject: [PATCH 06/10] fix: prevent UI race conditions during rapid directory switching using AbortController Introduce cancellation signals via `AbortController` in `renderCurrentDir()` to safely abort pending asynchronous filesystem read operations when user navigation changes rapidly. When a user quickly clicks through multiple nested folders or rapidly toggles back/forward navigation, multiple concurrent asynchronous `renderCurrentDir()` promises are fired. Slower directory reads could resolve *after* a faster subsequent directory read, causing stale folder contents to overwrite the active viewport. * **Abort Signal Controller (`src/pages/fileBrowser/fileBrowser.js`):** * Instantiated a module-scoped or instance-scoped `AbortController` prior to triggering directory reading operations inside `renderCurrentDir()`. * Aborts any in-flight rendering task immediately whenever a new navigation request occurs, or when the file browser view is hidden. * **Race Condition Guarding:** * Handled `AbortError` exceptions gracefully in async catch blocks to prevent unhandled promise rejections while ensuring stale directory listings never touch the DOM container. --- src/pages/fileBrowser/fileBrowser.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index d62e7c14b..de4b1049f 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -165,6 +165,8 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { list: [], scroll: 0, }; + /** @type {AbortController|null} */ + let _rndrAbortCtrl; /** * @type {HTMLButtonElement} @@ -574,6 +576,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { }; $page.onhide = function () { + _rndrAbortCtrl?.abort(); hideSearchBar(); hideAd(); actionStack.clearFromMark(); @@ -1525,6 +1528,11 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { * @param {boolean} force */ async function renderCurrentDir(force) { + _rndrAbortCtrl?.abort(); + const rndrAbortCtrl = new AbortController(); + const abortSignal = rndrAbortCtrl.signal; + _rndrAbortCtrl = rndrAbortCtrl; + if (document.getElementById("search-bar")) hideSearchBar(); const { url, name } = navStack.get(-1) ?? {}; @@ -1560,9 +1568,13 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { fg.append(el); } $list.append(fg); - dir.list = list = await getDirList(url); + list = await getDirList(url); + if (abortSignal.aborted) return; + dir.list = list; } + if (_rndrAbortCtrl === rndrAbortCtrl) _rndrAbortCtrl = null; + for (let l = list?.length, i = 0; i < l; i++) { fg.append(createListItemEl(list[i])); } From 3e651ff04ee91735e40e706ac0771127c2d76275 Mon Sep 17 00:00:00 2001 From: AuDevTist1C <114492072+AuDevTist1C@users.noreply.github.com> Date: Sat, 25 Jul 2026 06:58:16 +0200 Subject: [PATCH 07/10] feat: render interactive parent directory tile for rapid upward navigation Automatically prepend a dedicated parent directory (`..`) navigation tile at the top of directory listings when navigating deep within a folder structure. * **Template Integration (`src/pages/fileBrowser/listItem.hbs`):** * Added template parsing support for the `data-one-dir-up` attribute to identify parent navigation controls. * **Dynamic Item Prepending (`src/pages/fileBrowser/fileBrowser.js`):** * Added conditional evaluation during directory rendering: when `navStack.length >= 2`, a non-selectable parent folder item (`..`) is automatically prepended to the top of the rendered list array. * Flagged the parent directory tile with `data-not-selectable` so it is ignored during batch selection operations. * **Navigation Action Dispatch:** * Bound tap and click actions on items bearing `data-one-dir-up` to execute an upward navigation jump directly to `navStack.get(-2)`, streamlining folder hierarchy traversal. (AI generated commit message) --- src/pages/fileBrowser/fileBrowser.js | 28 ++++++++++++++++++++++++++-- src/pages/fileBrowser/listItem.hbs | 1 + 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index de4b1049f..5bb43e465 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -901,6 +901,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { let url = $el.dataset.url; let name = $el.dataset.name; + const isOneDirUp = $el.dataset.oneDirUp != null; const isOpenDoc = $el.dataset.openDoc != null; const uuid = $el.dataset.uuid; const type = $el.dataset.type; @@ -918,7 +919,14 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { return; } - if (!url && action === "open" && isDir && !isOpenDoc && !isContextMenu) { + if ( + !url && + action === "open" && + isDir && + !isOpenDoc && + !isOneDirUp && + !isContextMenu + ) { loader.hide(); util.addPath(name, uuid).then((res) => { const storage = allStorages.find((storage) => storage.uuid === uuid); @@ -934,6 +942,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { if (isContextMenu) action = "contextmenu"; else if (isOpenDoc) action = "openDoc"; + else if (isOneDirUp) action = "oneDirUp"; switch (action) { case "navigation": @@ -948,6 +957,11 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { break; case "openDoc": openDoc(); + break; + case "oneDirUp": { + const dir = navStack.get(-2); + if (dir) navigate(dir.url, dir.name); + } } async function folder() { @@ -993,7 +1007,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { } async function contextMenuHandler() { - if (isOpenDoc) return; + if (isOneDirUp || isOpenDoc) return; if (appSettings.value.vibrateOnTap) { navigator.vibrate(config.VIBRATION_TIME); } @@ -1543,6 +1557,14 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { const $list = createListEl(); + const oneDirUp = + navStack.length >= 2 && + createListItemEl({ + name: "..", + oneDirUp: true, + notSelectable: true, + }); + if (!$oldList) $content.append($list); else { const dir = currentDir; @@ -1560,6 +1582,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { let { list } = dir; const fg = new DocumentFragment(); if (!list) { + if (oneDirUp) fg.append(oneDirUp); let plh = { placeholder: true }; for (let i = Math.ceil($list.clientHeight / 45); i-- > 0; ) { const el = createListItemEl(plh); @@ -1575,6 +1598,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { if (_rndrAbortCtrl === rndrAbortCtrl) _rndrAbortCtrl = null; + if (oneDirUp) fg.append(oneDirUp); for (let l = list?.length, i = 0; i < l; i++) { fg.append(createListItemEl(list[i])); } diff --git a/src/pages/fileBrowser/listItem.hbs b/src/pages/fileBrowser/listItem.hbs index 2744be3fb..4440d48e8 100644 --- a/src/pages/fileBrowser/listItem.hbs +++ b/src/pages/fileBrowser/listItem.hbs @@ -6,6 +6,7 @@ data-name="{{name}}" {{#home}}data-home="{{.}}"{{/home}} {{#notSelectable}}data-not-selectable{{/notSelectable}} + {{#oneDirUp}}data-one-dir-up{{/oneDirUp}} {{#openDoc}}data-open-doc{{/openDoc}} {{#ftpAccount}}data-ftp-account{{/ftpAccount}} {{#uuid}}data-uuid="{{uuid}}"{{/uuid}} From fd35f31ed222b837b7861189e2bc49b483d316a4 Mon Sep 17 00:00:00 2001 From: AuDevTist1C <114492072+AuDevTist1C@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:31:53 +0200 Subject: [PATCH 08/10] refactor: parallelize batch file deletion and recursive folder removal using Promise.all Optimize file and directory removal throughput by replacing sequential synchronous execution loops with concurrent `Promise.all()` asynchronous resolution. * **Batch Deletion Concurrency:** * Replaced sequential `for...of` loops containing `await` calls inside batch deletion routines with array mapping mapped directly into `Promise.all()`. * Enables parallel filesystem unlink/delete invocations across selected items, reducing total batch execution time from $O(n \cdot t)$ sequential latency to approximately $O(1 \cdot \text{max}(t))$ concurrent I/O latency. * **Recursive Subtree Parallelization:** * Refactored recursive directory deletion logic (particularly for Termux and localized file storage backends) to process child directory contents and child file unlinks simultaneously using parallelized promise collections. (AI generated commit message) --- src/pages/fileBrowser/fileBrowser.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index 5bb43e465..1bd8d9519 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -555,7 +555,12 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { ); try { - for (const url of selectedItems) await deleteDirOrFile(url); + let i = selectionCount; + const promises = new Array(i--); + for (const url of selectedItems) { + promises[i--] = deleteDirOrFile(url); + } + await Promise.all(promises); toast(strings.success); reload(); isSelectionMode = false; @@ -647,10 +652,14 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { else { const deleteRecursively = async (currentFs) => { const currentEntries = await currentFs.lsDir(); + const promises = []; for (const entry of currentEntries) { const fs = fsOperation(entry.url); - await (entry.isDirectory ? deleteRecursively(fs) : fs.delete()); + promises.push( + entry.isDirectory ? deleteRecursively(fs) : fs.delete(), + ); } + await Promise.all(promises); await currentFs.delete(); }; await deleteRecursively(fs); From c319a973a3e5911f74e92d65b276c5bafb5c5f75 Mon Sep 17 00:00:00 2001 From: AuDevTist1C <114492072+AuDevTist1C@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:13:27 +0200 Subject: [PATCH 09/10] feat: render explicit empty directory and error placeholder elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore empty directory user feedback lost during state management refactoring by programmatically appending a centered placeholder element when directory listings are empty or encounter errors. * **Placeholder DOM Construction:** * Reinstates empty directory state messaging—which was previously handled via Mustache's `empty-msg` attribute prior to commit `3099b44e272d98d71735cbd773b1be54793cc315`—by dynamically constructing a placeholder element when `list.length` is zero. * Introduced `createPlaceholderEl()` helper to generate `
      ` displaying either localized empty folder text or formatted filesystem error messages. * **Layout & CSS Flexbox Centering:** * Added flexbox alignment rules for `#error-or-empty-dir` to center placeholder text both horizontally and vertically within the listing view container. * Leveraged CSS `:has(> [data-one-dir-up])` selector to dynamically offset height (`calc(100% - 45px)`) whenever interactive parent directory tiles are present. (AI generated commit message) --- src/pages/fileBrowser/fileBrowser.js | 54 +++++++++++++++++++------- src/pages/fileBrowser/fileBrowser.scss | 13 +++++++ 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index 1bd8d9519..0241ac58d 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -1327,20 +1327,16 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { * @returns {Promise} */ async function getDirList(url) { - try { - let list = await (url === "/" - ? listAllStorages() - : fsOperation(url).lsDir()); - - if (list?.length) { - const { fileBrowser } = appSettings.value; - list = helpers.sortDir(list, fileBrowser, mode); - } + let list = await (url === "/" + ? listAllStorages() + : fsOperation(url).lsDir()); - return list ?? []; - } catch (err) { - helpers.error(err, url); + if (list?.length) { + const { fileBrowser } = appSettings.value; + list = helpers.sortDir(list, fileBrowser, mode); } + + return list ?? []; } /** @@ -1532,6 +1528,15 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { if (doesReload) reload(); } + /** + * @param {string} [msg] + * @returns {HTMLDivElement | null} + */ + function createPlaceholderEl(msg) { + if (!(msg = `${msg ?? ""}`)) return; + return
      {msg}
      ; + } + /** * @returns {HTMLUListElement} */ @@ -1588,6 +1593,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { updatePasteToggler(); + let error; let { list } = dir; const fg = new DocumentFragment(); if (!list) { @@ -1600,7 +1606,19 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { fg.append(el); } $list.append(fg); - list = await getDirList(url); + try { + list = await getDirList(url); + } catch (err) { + const url2 = /^(content|file|s?ftp|https?):/.test(url) + ? helpers.getVirtualPath(url) + : url; + console.group("Error reading directory:", url2); + console.error(err); + console.groupEnd(); + let msg = `${err ?? ""}`; + if (!msg.startsWith("Error:")) msg = `Error: ${msg}`; + error = msg.trim(); + } if (abortSignal.aborted) return; dir.list = list; } @@ -1608,8 +1626,14 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { if (_rndrAbortCtrl === rndrAbortCtrl) _rndrAbortCtrl = null; if (oneDirUp) fg.append(oneDirUp); - for (let l = list?.length, i = 0; i < l; i++) { - fg.append(createListItemEl(list[i])); + const l = list?.length; + if (!l) { + const el = createPlaceholderEl( + list ? strings["empty folder message"] : error, + ); + if (el) fg.append(el); + } else { + for (let i = 0; i < l; ) fg.append(createListItemEl(list[i++])); } $list.replaceChildren(fg); diff --git a/src/pages/fileBrowser/fileBrowser.scss b/src/pages/fileBrowser/fileBrowser.scss index b1e2b3089..3bf0b2bfc 100644 --- a/src/pages/fileBrowser/fileBrowser.scss +++ b/src/pages/fileBrowser/fileBrowser.scss @@ -89,6 +89,19 @@ height: calc(100% - 60px); overflow-y: auto; + > #error-or-empty-dir { + width: 100%; + height: 100%; + font-size: 1.2em; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + } + &:has(> [data-one-dir-up]) > #error-or-empty-dir { + height: calc(100% - 45px); + } + .tile { &[disabled] { .text { From c4403634914988ff0b8d4ed540724778263f7a58 Mon Sep 17 00:00:00 2001 From: AuDevTist1C <114492072+AuDevTist1C@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:40:12 +0200 Subject: [PATCH 10/10] feat: Can I add the timeout back? --- src/pages/fileBrowser/fileBrowser.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index 0241ac58d..c53df4e6d 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -1327,9 +1327,18 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { * @returns {Promise} */ async function getDirList(url) { - let list = await (url === "/" - ? listAllStorages() - : fsOperation(url).lsDir()); + let list; + if (url === "/") list = await listAllStorages(); + else { + const p1 = fsOperation(url).lsDir(); + const { promise: p2, reject } = Promise.withResolvers(); + const tm = setTimeout( + () => reject("Directory loading timed out."), + 10000, + ); + list = await Promise.race([p1, p2]); + clearTimeout(tm); + } if (list?.length) { const { fileBrowser } = appSettings.value;