✨feature: Directory tree and multi-column file listing
This commit is contained in:
@@ -22,7 +22,18 @@
|
||||
<button id="local-browse">Browse</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body" id="local-list"></div>
|
||||
<div class="panel-body">
|
||||
<div class="list" id="local-list">
|
||||
<div class="list-row list-header">
|
||||
<div class="col name">
|
||||
Name
|
||||
<span class="col-resizer" data-target="local-list"></span>
|
||||
</div>
|
||||
<div class="col size">Size</div>
|
||||
</div>
|
||||
<div class="list-body" id="local-list-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
@@ -35,15 +46,27 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body" id="remote-list"></div>
|
||||
<div class="panel-body">
|
||||
<div class="tree" id="remote-tree"></div>
|
||||
<div class="list" id="remote-list">
|
||||
<div class="list-row list-header">
|
||||
<div class="col name">
|
||||
Name
|
||||
<span class="col-resizer" data-target="remote-list"></span>
|
||||
</div>
|
||||
<div class="col size">Size</div>
|
||||
</div>
|
||||
<div class="list-body" id="remote-list-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<details class="log">
|
||||
<summary>Log Section (Collapsed)</summary>
|
||||
<summary>Session Log</summary>
|
||||
<div id="log-list"></div>
|
||||
</details>
|
||||
|
||||
<script src="renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
19
src/main.js
19
src/main.js
@@ -88,9 +88,11 @@ function parseApacheIndex(html, baseUrl) {
|
||||
const results = [];
|
||||
const preMatch = html.match(/<pre>[\s\S]*?<\/pre>/i);
|
||||
const block = preMatch ? preMatch[0] : html;
|
||||
const linkRegex = /<a href="([^"]+)">([^<]+)<\/a>/gi;
|
||||
let match;
|
||||
while ((match = linkRegex.exec(block)) !== null) {
|
||||
const preLines = preMatch ? preMatch[0].split(/\r?\n/) : block.split(/\r?\n/);
|
||||
const linkRegex = /<a href="([^"]+)">([^<]+)<\/a>/i;
|
||||
for (const line of preLines) {
|
||||
const match = line.match(linkRegex);
|
||||
if (!match) continue;
|
||||
const href = match[1];
|
||||
const anchorText = match[2];
|
||||
if (href === '../' || anchorText === '../') continue;
|
||||
@@ -110,7 +112,16 @@ function parseApacheIndex(html, baseUrl) {
|
||||
} catch (err) {
|
||||
name = anchorText;
|
||||
}
|
||||
results.push({ name, href: fullUrl, isDir });
|
||||
let size = '-';
|
||||
const rest = line.slice(match.index + match[0].length).trim();
|
||||
if (rest) {
|
||||
const tokens = rest.split(/\s+/);
|
||||
const sizeToken = tokens[tokens.length - 1];
|
||||
if (sizeToken && sizeToken !== '-') {
|
||||
size = sizeToken;
|
||||
}
|
||||
}
|
||||
results.push({ name, href: fullUrl, isDir, size });
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
124
src/renderer.js
124
src/renderer.js
@@ -10,14 +10,30 @@ const el = {
|
||||
remoteInput: document.querySelector('#remote-url'),
|
||||
remoteSubmit: document.querySelector('#remote-submit'),
|
||||
remoteList: document.querySelector('#remote-list'),
|
||||
remoteListBody: document.querySelector('#remote-list-body'),
|
||||
remoteTree: document.querySelector('#remote-tree'),
|
||||
localPath: document.querySelector('#local-path'),
|
||||
localBrowse: document.querySelector('#local-browse'),
|
||||
localList: document.querySelector('#local-list'),
|
||||
localListBody: document.querySelector('#local-list-body'),
|
||||
downloadBtn: document.querySelector('#download-btn'),
|
||||
useWget: document.querySelector('#use-wget'),
|
||||
logList: document.querySelector('#log-list'),
|
||||
};
|
||||
|
||||
function formatBytes(size) {
|
||||
if (size === 0) return '0 B';
|
||||
if (!size) return '-';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let value = size;
|
||||
let unitIndex = 0;
|
||||
while (value >= 1024 && unitIndex < units.length - 1) {
|
||||
value /= 1024;
|
||||
unitIndex += 1;
|
||||
}
|
||||
return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
function log(message) {
|
||||
const line = document.createElement('div');
|
||||
line.textContent = message;
|
||||
@@ -25,26 +41,40 @@ function log(message) {
|
||||
}
|
||||
|
||||
function renderLocal() {
|
||||
el.localList.innerHTML = '';
|
||||
el.localListBody.innerHTML = '';
|
||||
state.localEntries.forEach((entry) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'row';
|
||||
row.textContent = entry.isDir ? `[${entry.name}]` : entry.name;
|
||||
row.className = 'row list-row';
|
||||
const name = document.createElement('div');
|
||||
name.className = 'name';
|
||||
name.textContent = entry.isDir ? `${entry.name}/` : entry.name;
|
||||
const size = document.createElement('div');
|
||||
size.className = 'size';
|
||||
size.textContent = entry.isDir ? '-' : formatBytes(entry.size);
|
||||
row.appendChild(name);
|
||||
row.appendChild(size);
|
||||
row.addEventListener('dblclick', async () => {
|
||||
if (!entry.isDir) return;
|
||||
state.localDir = entry.path;
|
||||
await refreshLocal();
|
||||
});
|
||||
el.localList.appendChild(row);
|
||||
el.localListBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function renderRemote() {
|
||||
el.remoteList.innerHTML = '';
|
||||
el.remoteListBody.innerHTML = '';
|
||||
state.remoteEntries.forEach((entry) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'row';
|
||||
row.textContent = entry.isDir ? `[${entry.name}]` : entry.name;
|
||||
row.className = 'row list-row';
|
||||
const name = document.createElement('div');
|
||||
name.className = 'name';
|
||||
name.textContent = entry.isDir ? entry.name : entry.name;
|
||||
const size = document.createElement('div');
|
||||
size.className = 'size';
|
||||
size.textContent = entry.isDir ? '-' : entry.size || '-';
|
||||
row.appendChild(name);
|
||||
row.appendChild(size);
|
||||
row.addEventListener('dblclick', async () => {
|
||||
if (!entry.isDir) return;
|
||||
state.remoteUrl = entry.href;
|
||||
@@ -57,7 +87,52 @@ function renderRemote() {
|
||||
row.dataset.href = entry.href;
|
||||
row.dataset.isDir = entry.isDir ? '1' : '0';
|
||||
});
|
||||
el.remoteList.appendChild(row);
|
||||
el.remoteListBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function renderRemoteTree() {
|
||||
el.remoteTree.innerHTML = '';
|
||||
if (!state.remoteUrl) return;
|
||||
let urlObj;
|
||||
try {
|
||||
urlObj = new URL(state.remoteUrl);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
const segments = urlObj.pathname.split('/').filter(Boolean);
|
||||
const nodes = [];
|
||||
nodes.push({ name: `${urlObj.origin}/`, url: `${urlObj.origin}/`, depth: 0, current: segments.length === 0 });
|
||||
let currentPath = '/';
|
||||
segments.forEach((seg, index) => {
|
||||
currentPath += `${seg}/`;
|
||||
nodes.push({
|
||||
name: `${seg}/`,
|
||||
url: `${urlObj.origin}${currentPath}`,
|
||||
depth: index + 1,
|
||||
current: index === segments.length - 1,
|
||||
});
|
||||
});
|
||||
const childDirs = state.remoteEntries.filter((entry) => entry.isDir);
|
||||
childDirs.forEach((entry) => {
|
||||
nodes.push({
|
||||
name: entry.name,
|
||||
url: entry.href,
|
||||
depth: segments.length + 1,
|
||||
current: false,
|
||||
});
|
||||
});
|
||||
nodes.forEach((node) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = `tree-row${node.current ? ' current' : ''}`;
|
||||
row.textContent = node.name;
|
||||
row.style.paddingLeft = `${node.depth * 16}px`;
|
||||
row.addEventListener('click', async () => {
|
||||
state.remoteUrl = node.url;
|
||||
el.remoteInput.value = state.remoteUrl;
|
||||
await refreshRemote();
|
||||
});
|
||||
el.remoteTree.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,6 +149,29 @@ async function refreshRemote() {
|
||||
const entries = await window.oddl.listRemoteDir(state.remoteUrl);
|
||||
state.remoteEntries = entries;
|
||||
renderRemote();
|
||||
renderRemoteTree();
|
||||
}
|
||||
|
||||
function setupResizer(listEl, resizerEl) {
|
||||
let startX = 0;
|
||||
let startWidth = 0;
|
||||
function onMove(event) {
|
||||
const delta = event.clientX - startX;
|
||||
const newWidth = Math.max(160, startWidth + delta);
|
||||
listEl.style.setProperty('--col-name', `${newWidth}px`);
|
||||
}
|
||||
function onUp() {
|
||||
window.removeEventListener('mousemove', onMove);
|
||||
window.removeEventListener('mouseup', onUp);
|
||||
}
|
||||
resizerEl.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault();
|
||||
startX = event.clientX;
|
||||
const nameCol = listEl.querySelector('.list-header .name');
|
||||
startWidth = nameCol ? nameCol.getBoundingClientRect().width : 240;
|
||||
window.addEventListener('mousemove', onMove);
|
||||
window.addEventListener('mouseup', onUp);
|
||||
});
|
||||
}
|
||||
|
||||
async function init() {
|
||||
@@ -89,6 +187,14 @@ async function init() {
|
||||
state.localDir = home;
|
||||
await refreshLocal();
|
||||
}
|
||||
|
||||
document.querySelectorAll('.col-resizer').forEach((resizer) => {
|
||||
const targetId = resizer.dataset.target;
|
||||
const listEl = document.getElementById(targetId);
|
||||
if (listEl) {
|
||||
setupResizer(listEl, resizer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
el.localBrowse.addEventListener('click', async () => {
|
||||
@@ -130,4 +236,4 @@ el.downloadBtn.addEventListener('click', async () => {
|
||||
|
||||
window.oddl.onLog((message) => log(message));
|
||||
|
||||
init().catch((err) => log(err.message));
|
||||
init().catch((err) => log(err.message));
|
||||
|
||||
@@ -71,6 +71,73 @@ body {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.list {
|
||||
--col-name: 1fr;
|
||||
--col-size: 140px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.list-row {
|
||||
display: grid;
|
||||
grid-template-columns: var(--col-name) var(--col-size);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
font-weight: 600;
|
||||
color: #2b2f36;
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid #e1e3e6;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.list-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.col {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.col-resizer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -8px;
|
||||
width: 12px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.tree {
|
||||
border: 1px solid #e1e3e6;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
margin-bottom: 12px;
|
||||
background: #fbfcfe;
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.tree-row {
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tree-row:hover {
|
||||
background: #eef2f6;
|
||||
}
|
||||
|
||||
.tree-row.current {
|
||||
background: #dbe7ff;
|
||||
}
|
||||
|
||||
.path-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -91,6 +158,12 @@ body {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.row .size,
|
||||
.list-header .size {
|
||||
text-align: right;
|
||||
color: #4f5765;
|
||||
}
|
||||
|
||||
.row:hover {
|
||||
background: #eef2f6;
|
||||
}
|
||||
@@ -111,7 +184,7 @@ body {
|
||||
padding: 12px 16px;
|
||||
font-family: Consolas, monospace;
|
||||
font-size: 12px;
|
||||
max-height: 200px;
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user