✨feature: Directory tree and multi-column file listing
This commit is contained in:
@@ -22,7 +22,18 @@
|
|||||||
<button id="local-browse">Browse</button>
|
<button id="local-browse">Browse</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
@@ -35,15 +46,27 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<details class="log">
|
<details class="log">
|
||||||
<summary>Log Section (Collapsed)</summary>
|
<summary>Session Log</summary>
|
||||||
<div id="log-list"></div>
|
<div id="log-list"></div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<script src="renderer.js"></script>
|
<script src="renderer.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
19
src/main.js
19
src/main.js
@@ -88,9 +88,11 @@ function parseApacheIndex(html, baseUrl) {
|
|||||||
const results = [];
|
const results = [];
|
||||||
const preMatch = html.match(/<pre>[\s\S]*?<\/pre>/i);
|
const preMatch = html.match(/<pre>[\s\S]*?<\/pre>/i);
|
||||||
const block = preMatch ? preMatch[0] : html;
|
const block = preMatch ? preMatch[0] : html;
|
||||||
const linkRegex = /<a href="([^"]+)">([^<]+)<\/a>/gi;
|
const preLines = preMatch ? preMatch[0].split(/\r?\n/) : block.split(/\r?\n/);
|
||||||
let match;
|
const linkRegex = /<a href="([^"]+)">([^<]+)<\/a>/i;
|
||||||
while ((match = linkRegex.exec(block)) !== null) {
|
for (const line of preLines) {
|
||||||
|
const match = line.match(linkRegex);
|
||||||
|
if (!match) continue;
|
||||||
const href = match[1];
|
const href = match[1];
|
||||||
const anchorText = match[2];
|
const anchorText = match[2];
|
||||||
if (href === '../' || anchorText === '../') continue;
|
if (href === '../' || anchorText === '../') continue;
|
||||||
@@ -110,7 +112,16 @@ function parseApacheIndex(html, baseUrl) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
name = anchorText;
|
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;
|
return results;
|
||||||
}
|
}
|
||||||
|
|||||||
124
src/renderer.js
124
src/renderer.js
@@ -10,14 +10,30 @@ const el = {
|
|||||||
remoteInput: document.querySelector('#remote-url'),
|
remoteInput: document.querySelector('#remote-url'),
|
||||||
remoteSubmit: document.querySelector('#remote-submit'),
|
remoteSubmit: document.querySelector('#remote-submit'),
|
||||||
remoteList: document.querySelector('#remote-list'),
|
remoteList: document.querySelector('#remote-list'),
|
||||||
|
remoteListBody: document.querySelector('#remote-list-body'),
|
||||||
|
remoteTree: document.querySelector('#remote-tree'),
|
||||||
localPath: document.querySelector('#local-path'),
|
localPath: document.querySelector('#local-path'),
|
||||||
localBrowse: document.querySelector('#local-browse'),
|
localBrowse: document.querySelector('#local-browse'),
|
||||||
localList: document.querySelector('#local-list'),
|
localList: document.querySelector('#local-list'),
|
||||||
|
localListBody: document.querySelector('#local-list-body'),
|
||||||
downloadBtn: document.querySelector('#download-btn'),
|
downloadBtn: document.querySelector('#download-btn'),
|
||||||
useWget: document.querySelector('#use-wget'),
|
useWget: document.querySelector('#use-wget'),
|
||||||
logList: document.querySelector('#log-list'),
|
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) {
|
function log(message) {
|
||||||
const line = document.createElement('div');
|
const line = document.createElement('div');
|
||||||
line.textContent = message;
|
line.textContent = message;
|
||||||
@@ -25,26 +41,40 @@ function log(message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderLocal() {
|
function renderLocal() {
|
||||||
el.localList.innerHTML = '';
|
el.localListBody.innerHTML = '';
|
||||||
state.localEntries.forEach((entry) => {
|
state.localEntries.forEach((entry) => {
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'row';
|
row.className = 'row list-row';
|
||||||
row.textContent = entry.isDir ? `[${entry.name}]` : entry.name;
|
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 () => {
|
row.addEventListener('dblclick', async () => {
|
||||||
if (!entry.isDir) return;
|
if (!entry.isDir) return;
|
||||||
state.localDir = entry.path;
|
state.localDir = entry.path;
|
||||||
await refreshLocal();
|
await refreshLocal();
|
||||||
});
|
});
|
||||||
el.localList.appendChild(row);
|
el.localListBody.appendChild(row);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRemote() {
|
function renderRemote() {
|
||||||
el.remoteList.innerHTML = '';
|
el.remoteListBody.innerHTML = '';
|
||||||
state.remoteEntries.forEach((entry) => {
|
state.remoteEntries.forEach((entry) => {
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'row';
|
row.className = 'row list-row';
|
||||||
row.textContent = entry.isDir ? `[${entry.name}]` : entry.name;
|
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 () => {
|
row.addEventListener('dblclick', async () => {
|
||||||
if (!entry.isDir) return;
|
if (!entry.isDir) return;
|
||||||
state.remoteUrl = entry.href;
|
state.remoteUrl = entry.href;
|
||||||
@@ -57,7 +87,52 @@ function renderRemote() {
|
|||||||
row.dataset.href = entry.href;
|
row.dataset.href = entry.href;
|
||||||
row.dataset.isDir = entry.isDir ? '1' : '0';
|
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);
|
const entries = await window.oddl.listRemoteDir(state.remoteUrl);
|
||||||
state.remoteEntries = entries;
|
state.remoteEntries = entries;
|
||||||
renderRemote();
|
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() {
|
async function init() {
|
||||||
@@ -89,6 +187,14 @@ async function init() {
|
|||||||
state.localDir = home;
|
state.localDir = home;
|
||||||
await refreshLocal();
|
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 () => {
|
el.localBrowse.addEventListener('click', async () => {
|
||||||
@@ -130,4 +236,4 @@ el.downloadBtn.addEventListener('click', async () => {
|
|||||||
|
|
||||||
window.oddl.onLog((message) => log(message));
|
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;
|
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 {
|
.path-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -91,6 +158,12 @@ body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row .size,
|
||||||
|
.list-header .size {
|
||||||
|
text-align: right;
|
||||||
|
color: #4f5765;
|
||||||
|
}
|
||||||
|
|
||||||
.row:hover {
|
.row:hover {
|
||||||
background: #eef2f6;
|
background: #eef2f6;
|
||||||
}
|
}
|
||||||
@@ -111,7 +184,7 @@ body {
|
|||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
font-family: Consolas, monospace;
|
font-family: Consolas, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
max-height: 200px;
|
height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user