web: Path-based search

This commit is contained in:
Wildan M 2026-05-16 14:39:08 +07:00
parent 73034a4b98
commit 25c5c7f4c8
No known key found for this signature in database
GPG Key ID: 01AC53185C679C79
2 changed files with 58 additions and 17 deletions

View File

@ -13,11 +13,11 @@
<h1 class="category-title">Package File Browser</h1>
<div class="card">
<input type="text" id="searchInput" class="search-box" placeholder="Type file name" autocomplete="off">
<input type="text" id="searchInput" class="search-box" placeholder="Type file or directory name" title="Uses string.startsWith() matching" autocomplete="off">
<div class="browser-panels">
<div class="panel">
<div class="panel-header" id="dirHeader">/</div>
<div class="panel-header nowrap" id="dirHeader">/</div>
<div class="panel-body" id="dirPanel">
<div style="padding: 15px; color: #777;">Loading files...</div>
</div>
@ -65,28 +65,61 @@
*/
/**
* @param {DB} node
* @param {string} query
* @param {boolean} parentMatched
* @returns {DB|null}
*/
* @param {DB} node
* @param {string} query
* @param {boolean} parentMatched
* @returns {DB|null}
*/
function filterDb(node, query, parentMatched = false) {
/** @type {DB} */
let result = {};
let filesCount = 0;
const q = query.replace(/^\/|\/$/g, '').toLowerCase();
const parts = q.split('/');
const targetSegment = parts[0];
for (const [key, value] of Object.entries(node)) {
const lowerKey = key.toLowerCase();
if (typeof value === 'object') {
const isMatch = parentMatched || key.toLowerCase().includes(query);
const subTree = filterDb(value, query, isMatch);
if (subTree !== null) {
result[key] = subTree;
filesCount++;
if (parentMatched) {
const subTree = filterDb(value, q, true);
if (subTree !== null) {
result[key] = subTree;
filesCount++;
}
} else {
let nextQuery = q;
let isMatch = false;
if (lowerKey.startsWith(targetSegment)) {
if (parts.length === 1) {
isMatch = true;
} else {
nextQuery = parts.slice(1).join('/');
}
}
let subTree = filterDb(value, nextQuery, isMatch);
if (subTree === null && nextQuery !== q) {
subTree = filterDb(value, q, false);
}
if (subTree !== null) {
result[key] = subTree;
filesCount++;
}
}
} else {
if (parentMatched || key.toLowerCase().includes(query)) {
if (parentMatched) {
result[key] = value;
filesCount++;
} else {
if (parts.length === 1 && lowerKey.startsWith(targetSegment)) {
result[key] = value;
filesCount++;
}
}
}
}
@ -172,12 +205,13 @@
if (isPeeking) {
const peekName = basePathArray.join('/');
fileHeader.innerHTML = `Files <span style="color:#777; font-weight:normal; font-size:0.9em;">(Peeking: ${peekName})</span>`;
fileHeader.innerHTML = `Files <code style="opacity:60%">(Peeking: ${peekName})</code>`;
} else if (lockedFileViewPath) {
const lockName = lockedFileViewPath.join('/');
fileHeader.innerHTML = `Files <span style="color:#ddd; font-weight:normal; font-size:0.9em;">(Selected: ${lockName})</span>`;
fileHeader.innerHTML = `Files <code>(Selected: ${lockName})</code>`;
} else {
fileHeader.innerHTML = `Files`;
const pathName = basePathArray.join('/') || '/';
fileHeader.innerHTML = `Files <code>(${pathName})</code>`;
}
if (!node) {
@ -304,7 +338,7 @@
if (!node) return;
}
dirHeader.innerText = pathStack.join('/');
dirHeader.innerText = pathStack.join('/') || '/';
let lastScroll = dirPanel.scrollTop;
dirPanel.innerHTML = '';

View File

@ -110,6 +110,7 @@ code {
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 0.9em;
font-weight: normal;
}
.card {
@ -321,6 +322,12 @@ th {
margin-right: 0.2em;
}
.nowrap {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000;