From ca8db0dd8b0872267b74e57ef432c7a17e49da6c Mon Sep 17 00:00:00 2001 From: AuDevTist1C <114492072+AuDevTist1C@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:57:46 +0200 Subject: [PATCH 1/6] feat(file-browser): Implement `NavStack` class for navigation history management Abstract navigation tracking and event dispatching out of inline file browser code into a dedicated `EventTarget` class. Create `NavStack` class (`src/pages/fileBrowser/NavStack.js`): - Implement `NavStack` class extending `EventTarget` with custom `Symbol.toStringTag` - Add `push`, `pop`, `popUntil`, `get`, `has`, and `toJSON` methods with parameter validation - Queue microtasks for update event dispatching with added and removed location tracking Integrate `NavStack` into file browser (`src/pages/fileBrowser/fileBrowser.js`): - Replace manual state arrays and direct `localStorage` updates with `NavStack` instance - Listen to `update` events on `NavStack` to manage `actionStack` entries and navbar UI elements - Refactor `navigate` and `loadStates` functions to use `NavStack` methods (AI generated commit message) --- src/pages/fileBrowser/NavStack.js | 135 +++++++++++++++++++++++++ src/pages/fileBrowser/fileBrowser.js | 146 +++++++++------------------ 2 files changed, 181 insertions(+), 100 deletions(-) create mode 100644 src/pages/fileBrowser/NavStack.js diff --git a/src/pages/fileBrowser/NavStack.js b/src/pages/fileBrowser/NavStack.js new file mode 100644 index 000000000..29230e770 --- /dev/null +++ b/src/pages/fileBrowser/NavStack.js @@ -0,0 +1,135 @@ +import Url from "utils/Url"; + +/** + * @typedef {{url: string, name: string}} Location + */ + +export default class NavStack extends EventTarget { + static { + Object.defineProperty(this.prototype, Symbol.toStringTag, { + value: "NavStack", + configurable: true, + }); + } + + get length() { + return this.#arr.length; + } + toJSON() { + return this.#arr.map((obj) => ({ ...obj })); + } + on() { + return this.addEventListener(...arguments); + } + off() { + return this.removeEventListener(...arguments); + } + + /** @type {null | { added: Map, removed: Set }} */ + #updatedURLs; + #queueUpdateEvent() { + if (this.#updatedURLs) return; + const added = new Map(); + const removed = new Set(); + this.#updatedURLs = Object.freeze({ added, removed }); + queueMicrotask(() => { + this.#updatedURLs = null; + this.dispatchEvent( + new CustomEvent("update", { + detail: Object.freeze({ + get added() { + return added.entries(); + }, + get removed() { + return removed.values(); + }, + }), + }), + ); + }); + } + + /** @type {Set} */ + #urlSet = new Set(); + /** @type {Array} */ + #arr = []; + /** + * @param {{ url: string, name?: string } | string} url + * @param {string} [name] + */ + push(url, name) { + if (typeof url === "object") ({ url, name } = url); + if (!(url = `${url ?? ""}`)) { + throw new TypeError( + "NavStack.prototype.push(" + + "url: { url: string, name?: string } | 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; + const arr = this.#arr; + const i = arr.length; + arr[i] = { url, name }; + + this.#queueUpdateEvent(); + const { added, removed } = this.#updatedURLs; + if (removed.has(url)) removed.delete(url); + else added.set(url, { name, index: i }); + } + /** + * @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.#queueUpdateEvent(); + const { added, removed } = this.#updatedURLs; + if (!added.has(url2)) removed.add(url2); + else added.delete(url2); + + 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 ?? ""}`); + } +} diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index c7c6ffaf9..17b073a45 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -33,10 +33,11 @@ import _addMenu from "./add-menu.hbs"; import _addMenuHome from "./add-menu-home.hbs"; import _template from "./fileBrowser.hbs"; import _list from "./list.hbs"; +import NavStack from "./NavStack"; import util from "./util"; /** - * @typedef {{url: String, name: String}} Location + * @typedef {import("./NavStack.js").Location} Location */ /** @@ -58,11 +59,10 @@ import util from "./util"; function FileBrowserInclude(mode, info, doesOpenLast = true) { mode = 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); @@ -638,7 +638,30 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { document.removeEventListener("resume", reload); }; + const saveFileBrowserState = doesOpenLast + ? () => (localStorage.fileBrowserState = JSON.stringify(navStack)) + : null; + navStack.addEventListener("update", (ev) => { + saveFileBrowserState?.(); + const { added, removed } = ev.detail; + for (const url of removed) { + actionStack.remove(url); + tag.get(`#${getNavId(url)}`)?.remove(); + } + for (const [url, { name, index: i }] of added) { + const prevDir = i && navStack.get(i - 1); + if (prevDir && !actionStack.has(url)) { + actionStack.push({ + id: url, + action: () => navigate(prevDir), + }); + } + pushToNavbar(name, url); + } + }); + if (doesOpenLast && storedState.length) { + navStack.push("/", "/"); loadStates(storedState); return; } @@ -1508,60 +1531,19 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { * @param {String} url * @param {String} name */ - async function navigate(url, name, assignBackButton = true) { + async function navigate(url, name) { + if (typeof url === "object") ({ url, name } = url); + 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 (url === "/") { - if (IS_FOLDER_MODE) $openFolder.disabled = true; - } else { - if (IS_FOLDER_MODE) $openFolder.disabled = false; - } - - 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 inStack = navStack.has(url); + if (inStack) navStack.popUntil(url); 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); + if (!inStack) navStack.push(url, name); render(dir); } } @@ -1671,13 +1653,11 @@ 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 */ - function pushToNavbar(name, url, action) { - if (!url) return; + function pushToNavbar(name, url) { const displayName = name || Url.basename(url) || url; $navigation.append( , ); $navigation.scrollLeft = $navigation.scrollWidth; - - if (action && !actionStack.has(url)) { - actionStack.push({ - id: url, - action, - }); - } } /** @@ -1706,37 +1679,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 || !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); } /** @@ -1773,7 +1724,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { } function render(dir) { - const { list, scroll } = dir; + const { url, list, scroll } = dir; const $list = helpers.parseHTML( mustache.render(_list, { msg: strings["empty folder message"], @@ -1781,6 +1732,8 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { }), ); + if (IS_FOLDER_MODE) $openFolder.disabled = (url || "/") === "/"; + if (document.getElementById("search-bar")) { hideSearchBar(); } @@ -1798,7 +1751,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { $list.focus(); currentDir = dir; - cachedDir[dir.url] = dir; + cachedDir[url] = dir; updatePasteToggler(); } @@ -1808,13 +1761,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 */ From 09d29ff39155a72656b384bec3fbbb796d817515 Mon Sep 17 00:00:00 2001 From: AuDevTist1C <114492072+AuDevTist1C@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:02:42 +0200 Subject: [PATCH 2/6] refactor(file-browser): Convert directory cache to `Map` instance Replace the plain object container used for cached directories with an ES6 `Map` to improve key lookup operations and key management semantics. Update cached directory data structure (`src/pages/fileBrowser/fileBrowser.js`): - Re-initialize `cachedDir` variable as a `Map` - Replace object property lookups with `Map.prototype.has()` and `Map.prototype.get()` - Update cache writes to use `Map.prototype.set()` - Update directory deletion calls to use `Map.prototype.delete()` (AI generated commit message) --- src/pages/fileBrowser/fileBrowser.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index 17b073a45..cebc222ae 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -160,7 +160,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { $selectionMenuToggler.style.display = "none"; $pasteToggler.style.display = "none"; const progress = {}; - let cachedDir = {}; + let cachedDir = new Map(); let currentDir = { url: null, name: null, @@ -237,8 +237,6 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { } if (action === "reload") { - const { url } = currentDir; - if (url in cachedDir) delete cachedDir[url]; reload(); return; } @@ -717,7 +715,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { } recents.removeFile(url); openFolder.removeItem(url); - delete cachedDir[url]; + cachedDir.delete(url); } function updateSelectionCount($count) { @@ -1474,8 +1472,8 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { let list = []; let error = false; - if (url in cachedDir) { - return cachedDir[url]; + if (cachedDir.has(url)) { + return cachedDir.get(url); } else { if (url === "/") { list = await listAllStorages(); @@ -1741,8 +1739,8 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { const $oldList = $content.get("#list"); if ($oldList) { const { url } = currentDir; - if (url && cachedDir[url]) { - cachedDir[url].scroll = $oldList.scrollTop; + if (url && cachedDir.has(url)) { + cachedDir.get(url).scroll = $oldList.scrollTop; } $oldList.remove(); } @@ -1751,13 +1749,13 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { $list.focus(); currentDir = dir; - cachedDir[url] = dir; + cachedDir.set(url, dir); updatePasteToggler(); } function reload() { const { url, name } = currentDir; - delete cachedDir[url]; + cachedDir.delete(url); navigate(url, name); } From c95b4f92d375884bb069bbaa63221b4488e388d7 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 3/6] feat(file-browser): Implement asynchronous directory rendering with inline spinner Transition directory loading from blocking modal dialogs to inline loading state indicators with explicit timeout handling. Add async directory list fetching and rendering (`src/pages/fileBrowser/fileBrowser.js`): - Extract list retrieval into `getDirList` using `Promise.withResolvers` and `Promise.race` with a 15-second timeout - Replace render function with `renderCurrentDir` to handle asynchronous state changes - Render inline SVG spinner placeholder in place of missing list content - Maintain scroll position across directory re-renders Style inline spinner (`src/pages/fileBrowser/fileBrowser.scss`): - Add flexbox alignment styles for `#spinner` inside directory list container (AI generated commit message) --- src/pages/fileBrowser/fileBrowser.js | 179 +++++++++++++------------ src/pages/fileBrowser/fileBrowser.scss | 9 ++ 2 files changed, 105 insertions(+), 83 deletions(-) diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index cebc222ae..de47f85da 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -6,6 +6,7 @@ import Checkbox from "components/checkbox"; import Contextmenu from "components/contextmenu"; import Page from "components/page"; import searchBar from "components/searchbar"; +import createTailSpinSvg from "components/tailSpin.js"; import terminalManager from "components/terminal/terminalManager"; import alert from "dialogs/alert"; import confirm from "dialogs/confirm"; @@ -1463,65 +1464,37 @@ 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 + * @returns {Promise} */ - async function getDir(url, name) { - const { fileBrowser } = appSettings.value; - let list = []; - let error = false; - - if (cachedDir.has(url)) { - return cachedDir.get(url); + async function getDirList(url) { + let list; + if (url === "/") { + list = await listAllStorages(); } else { - if (url === "/") { - list = await listAllStorages(); - } else { - const id = helpers.uuid(); - let loaderTimeout = 10000; - - if (["ftp:", "sftp:"].includes(Url.getProtocol(url))) { - loaderTimeout = 0; - } - - progress[id] = true; - const timeout = setTimeout(() => { - loader.create(name, strings.loading + "...", { - timeout: loaderTimeout, - callback() { - loader.destroy(); - navigate("/", "/"); - progress[id] = false; - }, - }); - }, 100); - - 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]; + const p1 = fsOperation(url).lsDir(); + /** @type {Promise} */ + let p2; + /** @type {(reason: any) => void} */ + let reject; + ({ promise: p2, reject } = Promise.withResolvers()); + const timeout = setTimeout( + () => reject("Directory loading timed out."), + 15000, + ); + try { + list = await Promise.race([p1, p2]); + } finally { clearTimeout(timeout); - loader.destroy(); } - if (error) return null; - return { - url, - name, - scroll: 0, - list: helpers.sortDir(list, fileBrowser, mode), - }; } + + if (list?.length) { + const { fileBrowser } = appSettings.value; + list = helpers.sortDir(list, fileBrowser, mode); + } + + return list ?? []; } /** @@ -1529,21 +1502,14 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { * @param {String} url * @param {String} name */ - async function navigate(url, name) { + function navigate(url, name) { if (typeof url === "object") ({ url, name } = url); - if (document.getElementById("search-bar")) { - hideSearchBar(); - } - const inStack = navStack.has(url); if (inStack) navStack.popUntil(url); + else navStack.push(url, name); - const dir = await getDir(url, name); - if (dir) { - if (!inStack) navStack.push(url, name); - render(dir); - } + renderCurrentDir(); } /** @@ -1721,14 +1687,11 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { if (doesReload) reload(); } - function render(dir) { - const { url, list, scroll } = dir; - const $list = helpers.parseHTML( - mustache.render(_list, { - msg: strings["empty folder message"], - list, - }), - ); + /** + * @param {boolean} force + */ + async function renderCurrentDir(force) { + const { url, name } = navStack.get(-1) ?? {}; if (IS_FOLDER_MODE) $openFolder.disabled = (url || "/") === "/"; @@ -1738,25 +1701,75 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { const $oldList = $content.get("#list"); if ($oldList) { - const { url } = currentDir; - if (url && cachedDir.has(url)) { - cachedDir.get(url).scroll = $oldList.scrollTop; - } + const dir = currentDir; + if (dir?.url) dir.scroll = $oldList.scrollTop; $oldList.remove(); } - $content.append($list); - $list.scrollTop = scroll; - $list.focus(); + if (force) cachedDir.delete(url); + const dir = (!force && url && cachedDir.get(url)) || { + url, + name, + scroll: 0, + }; currentDir = dir; - cachedDir.set(url, dir); + const isCached = dir != null; updatePasteToggler(); + + let $placeholder; + let errMsg; + let { list } = dir; + if (!list) { + $placeholder = helpers.parseHTML(mustache.render(_list, {})); + $placeholder.classList.add("placeholder"); + $placeholder.innerHTML = `${createTailSpinSvg()}`; + $content.appendChild($placeholder); + + try { + list = await getDirList(url); + } catch (err) { + let name = "Error"; + let code = Number.NaN; + let msg = err; + if (typeof err === "object") { + name = `${err.name ?? ""}` || name; + msg = err.message; + code = +err.code; + } + errMsg = name; + if (code === code) errMsg += ` (${code})`; + if ((msg = `${msg ?? ""}`)) errMsg += `: ${msg}`; + + const url2 = /^(content|file|s?ftp|https?):/.test(url) + ? helpers.getVirtualPath(url) + : url; + console.group("Error reading:", url2); + if (code === code) console.log("Code:", code); + console.error(err); + console.groupEnd(); + } + if (abortSignal.aborted) return; + dir.list = list; + } + + const $list = helpers.parseHTML( + mustache.render(_list, { + msg: errMsg ?? (!list?.length && strings["empty folder message"]), + list, + }), + ); + + if (!$placeholder) $content.appendChild($list); + else $placeholder.replaceWith($list); + + $list.scrollTop = +dir.scroll || 0; + $list.focus(); + + cachedDir.set(url, dir); } function reload() { - const { url, name } = currentDir; - cachedDir.delete(url); - navigate(url, name); + renderCurrentDir(true); } /** diff --git a/src/pages/fileBrowser/fileBrowser.scss b/src/pages/fileBrowser/fileBrowser.scss index 769090e05..c3e8ac5a4 100644 --- a/src/pages/fileBrowser/fileBrowser.scss +++ b/src/pages/fileBrowser/fileBrowser.scss @@ -89,6 +89,15 @@ height: calc(100% - 60px); overflow-y: auto; + > #spinner { + pointer-events: none; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + .tile { &[disabled] { .text { From 260bf950506a379550ebfdc0d645f138d85e24d8 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 4/6] fix(file-browser): Abort pending directory rendering tasks on path change or page hide Introduce `AbortController` tracking to cancel pending directory listing render tasks when navigating quickly or hiding the page. Add render abort controller logic (`src/pages/fileBrowser/fileBrowser.js`): - Instantiate `AbortController` instance inside `renderCurrentDir` - Abort existing controller prior to running new render sequence - Validate `abortSignal` before committing directory list DOM updates - Call `abort` on active render controller when `$page.onhide` fires (AI generated commit message) --- src/pages/fileBrowser/fileBrowser.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index de47f85da..16185fab8 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -168,6 +168,8 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { list: [], scroll: 0, }; + /** @type {AbortController | null} */ + let _rndrAbortCtrl; /** * @type {HTMLButtonElement} */ @@ -629,6 +631,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { }; $page.onhide = function () { + _rndrAbortCtrl?.abort(); hideSearchBar(); actionStack.clearFromMark(); actionStack.remove("filebrowser"); @@ -1691,6 +1694,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; + const { url, name } = navStack.get(-1) ?? {}; if (IS_FOLDER_MODE) $openFolder.disabled = (url || "/") === "/"; @@ -1752,6 +1760,8 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { dir.list = list; } + if (_rndrAbortCtrl === rndrAbortCtrl) _rndrAbortCtrl = null; + const $list = helpers.parseHTML( mustache.render(_list, { msg: errMsg ?? (!list?.length && strings["empty folder message"]), From 6e88a94e400845cfb792da40794f93732b279f82 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 5/6] feat(file-browser): Add parent directory navigation item to list view Add a dedicated '..' list tile at the top of directory listings to provide explicit one-level-up navigation. Update list template (`src/pages/fileBrowser/list.hbs`): - Render parent directory tile ('..') when `prevDir` flag is active - Adjust handlebars conditionals for message displays and item list loop Handle `prevDir` action in file browser (`src/pages/fileBrowser/fileBrowser.js`): - Add `prevDir` action handler navigating to `navStack.get(-2)` - Pass `prevDir` condition check (`navStack.length >= 2`) into list render template - Ignore context menu actions triggered on the `prevDir` item Adjust layout styling (`src/pages/fileBrowser/fileBrowser.scss`): - Adjust height calculation for message and spinner containers when `prevDir` item is visible (AI generated commit message) --- src/pages/fileBrowser/fileBrowser.js | 26 +++++++--- src/pages/fileBrowser/fileBrowser.scss | 7 ++- src/pages/fileBrowser/list.hbs | 70 +++++++++++++++++--------- 3 files changed, 70 insertions(+), 33 deletions(-) diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index 16185fab8..7afe9a83a 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -1008,16 +1008,13 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { return; } - if (isContextMenu) action = "contextmenu"; - else if (isOpenDoc) action = "openDoc"; + if (isContextMenu) return contextMenuHandler(); + if (isOpenDoc) action = "openDoc"; switch (action) { case "navigation": folder(); break; - case "contextmenu": - contextMenuHandler(); - break; case "open": if (isDir) folder(); else if (!$el.hasAttribute("disabled")) file(); @@ -1025,6 +1022,10 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { case "openDoc": openDoc(); break; + case "prevDir": { + const dir = navStack.get(-2); + if (dir) navigate(dir); + } } async function folder() { @@ -1077,6 +1078,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { } async function contextMenuHandler() { + if (action === "prevDir") return; if (appSettings.value.vibrateOnTap) { navigator.vibrate(config.VIBRATION_TIME); } @@ -1724,13 +1726,22 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { const isCached = dir != null; updatePasteToggler(); + const hasPrevDir = navStack.length >= 2; let $placeholder; let errMsg; let { list } = dir; if (!list) { - $placeholder = helpers.parseHTML(mustache.render(_list, {})); + $placeholder = helpers.parseHTML( + mustache.render(_list, { + prevDir: hasPrevDir, + }), + ); $placeholder.classList.add("placeholder"); - $placeholder.innerHTML = `${createTailSpinSvg()}`; + $placeholder.insertAdjacentHTML( + "beforeend", + `${createTailSpinSvg()}`, + ); + /** @type {HTMLSpanElement} */ $content.appendChild($placeholder); try { @@ -1764,6 +1775,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { const $list = helpers.parseHTML( mustache.render(_list, { + prevDir: hasPrevDir, msg: errMsg ?? (!list?.length && strings["empty folder message"]), list, }), diff --git a/src/pages/fileBrowser/fileBrowser.scss b/src/pages/fileBrowser/fileBrowser.scss index c3e8ac5a4..4c7a81372 100644 --- a/src/pages/fileBrowser/fileBrowser.scss +++ b/src/pages/fileBrowser/fileBrowser.scss @@ -89,13 +89,18 @@ height: calc(100% - 60px); overflow-y: auto; - > #spinner { + > :is(#msg, #spinner) { pointer-events: none; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; + font-weight: bold; + text-align: center; + } + &:has(> [data-action="prevDir"]) > :is(#msg, #spinner) { + height: calc(100% - 45px); } .tile { diff --git a/src/pages/fileBrowser/list.hbs b/src/pages/fileBrowser/list.hbs index 0eac42975..b065dd669 100644 --- a/src/pages/fileBrowser/list.hbs +++ b/src/pages/fileBrowser/list.hbs @@ -1,29 +1,49 @@ -
    {{#list}} - {{#.}} -
  • - + {{#prevDir}} +
  • + +
    + .. +
    +
  • + {{/prevDir}} + {{^list}} + {{#msg}} +
    {{.}}
    + {{/msg}} + {{/list}} + {{#list}} + {{#.}} +
  • + {{#disabled}}disabled{{/disabled}} + > + -
    - {{name}} -
    - {{url}} -
  • - {{/.}} +
    + {{name}} +
    + {{url}} + + {{/.}} {{/list}}
From 0383f17e5126bab537e56f3ecc157712c237a063 Mon Sep 17 00:00:00 2001 From: AuDevTist1C <114492072+AuDevTist1C@users.noreply.github.com> Date: Fri, 21 Aug 2026 09:04:14 +0200 Subject: [PATCH 6/6] --- src/pages/fileBrowser/fileBrowser.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index 7afe9a83a..e793bdc49 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -1723,7 +1723,6 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { scroll: 0, }; currentDir = dir; - const isCached = dir != null; updatePasteToggler(); const hasPrevDir = navStack.length >= 2;