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} */ - async function getDir(url, name) { - if (url in cachedDir) return cachedDir[url]; - - const { fileBrowser } = appSettings.value; - let list = []; - + async function getDirList(url) { + 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); - try { const fs = fsOperation(url); - list = (await fs.lsDir()) ?? []; + list = await fs.lsDir(); } catch (err) { - if (!progress[id]) console.error(err); - else helpers.error(err, url); + helpers.error(err, url); } + } - const error = !progress[id]; - delete progress[id]; - clearTimeout(timeout); - loader.destroy(); - if (error) return null; + if (list?.length) { + const { fileBrowser } = appSettings.value; + list = helpers.sortDir(list, fileBrowser, mode); } - return { - url, - name, - scroll: 0, - list: helpers.sortDir(list, fileBrowser, mode), - }; + return list; } /** @@ -1330,48 +1330,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,34 +1474,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 }; + currentDir = navStack.get(-1); + const { url, name } = currentDir; navigate(url, name); } @@ -1571,32 +1515,67 @@ 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, - }), - ); + function createListEl() { + return
      ; + } + + function createListItemEl(obj) { + return helpers.parseHTML(mustache.render(_listItem, obj)); + } + + async function renderCurrentDir() { + _rndrAbortCtrl?.abort(); + const rndrAbortCtrl = new AbortController(); + const abortSignal = rndrAbortCtrl.signal; + _rndrAbortCtrl = rndrAbortCtrl; if (document.getElementById("search-bar")) hideSearchBar(); + const { url, name } = navStack.get(-1) ?? {}; + if (IS_FOLDER_MODE) $openFolder.disabled = url === "/"; + const $oldList = $content.get("#list"); - if ($oldList) { - const { url } = currentDir; - if (url && cachedDir[url]) { - cachedDir[url].scroll = $oldList.scrollTop; - } - $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(); + const dir = (url && cachedDir[url]) || { url, name, scroll: 0 }; currentDir = dir; - cachedDir[dir.url] = dir; + updatePasteToggler(); + + 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); + 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])); + } + $list.replaceChildren(fg); + + if (!list) return; + $list.scrollTop = +dir.scroll || 0; + cachedDir[url] = dir; } function reload() { @@ -1605,13 +1584,6 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { navigate(url, name); } - 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); - } - /** * Adds a new storage and refresh location */ diff --git a/src/pages/fileBrowser/fileBrowser.scss b/src/pages/fileBrowser/fileBrowser.scss index 281f4ffba..103eac6f4 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 227b93152203addc1db03df80b00dc7e789cf5b0 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 6/7] 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 835f2a60b..e8cbb0a6c 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -903,6 +903,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; @@ -920,7 +921,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); @@ -936,6 +944,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": @@ -950,6 +959,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() { @@ -995,7 +1009,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); } @@ -1537,6 +1551,14 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { const $oldList = $content.get("#list"); const $list = createListEl(); + const oneDirUp = + navStack.length >= 2 && + createListItemEl({ + name: "..", + oneDirUp: true, + notSelectable: true, + }); + if (!$oldList) $content.append($list); else { const dir = currentDir; @@ -1553,6 +1575,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); @@ -1568,6 +1591,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 972d4951af751744382185fbafc4e5f74ad307da 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 7/7] 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 e8cbb0a6c..a13a2c8f1 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -557,7 +557,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; @@ -649,10 +654,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);