feature: Implement support for table-based indexes and dark mode

This commit is contained in:
Keith Solomon
2026-01-04 10:12:58 -06:00
parent 658dc66a9f
commit 861b7b63cb
4 changed files with 169 additions and 29 deletions

View File

@@ -11,6 +11,9 @@
<label for="remote-url">Contents of:</label> <label for="remote-url">Contents of:</label>
<input id="remote-url" type="text" placeholder="http://example.com/" /> <input id="remote-url" type="text" placeholder="http://example.com/" />
<button id="remote-submit">Submit</button> <button id="remote-submit">Submit</button>
<label class="checkbox">
<input id="theme-toggle" type="checkbox" /> Dark mode
</label>
</header> </header>
<main class="grid"> <main class="grid">

View File

@@ -110,8 +110,50 @@ function parseSizeToken(token) {
return Math.round(value * multiplier); return Math.round(value * multiplier);
} }
function decodeHtmlEntities(value) {
return value
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'");
}
function parseApacheIndex(html, baseUrl) { function parseApacheIndex(html, baseUrl) {
const results = []; const results = [];
const tableRowRegex = /<tr>\s*<td class="link"><a href="([^"]+)"[^>]*>[\s\S]*?<\/a><\/td>\s*<td class="size">([^<]*)<\/td>/gi;
let tableMatch;
while ((tableMatch = tableRowRegex.exec(html)) !== null) {
const href = decodeHtmlEntities(tableMatch[1]);
const sizeTokenRaw = decodeHtmlEntities(tableMatch[2] || '').trim();
if (href === '../') continue;
const isDir = href.endsWith('/');
const fullUrl = new URL(href, baseUrl).toString();
let name = href;
try {
const urlObj = new URL(href, baseUrl);
let pathname = decodeURIComponent(urlObj.pathname);
if (pathname.endsWith('/')) {
pathname = pathname.slice(0, -1);
}
const derived = pathname.split('/').pop();
if (derived) {
name = isDir ? `${derived}/` : derived;
}
} catch (err) {
name = href;
}
const sizeBytes = parseSizeToken(sizeTokenRaw);
results.push({
name,
href: fullUrl,
isDir,
size: sizeTokenRaw || '-',
sizeBytes,
});
}
if (results.length) return 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 preLines = preMatch ? preMatch[0].split(/\r?\n/) : block.split(/\r?\n/); const preLines = preMatch ? preMatch[0].split(/\r?\n/) : block.split(/\r?\n/);

View File

@@ -24,6 +24,7 @@ const el = {
localListBody: document.querySelector('#local-list-body'), 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'),
themeToggle: document.querySelector('#theme-toggle'),
progress: document.querySelector('#transfer-progress'), progress: document.querySelector('#transfer-progress'),
progressBar: document.querySelector('#transfer-bar'), progressBar: document.querySelector('#transfer-bar'),
progressMeta: document.querySelector('#transfer-meta'), progressMeta: document.querySelector('#transfer-meta'),
@@ -353,6 +354,12 @@ async function init() {
el.useWget.checked = false; el.useWget.checked = false;
} }
const savedTheme = localStorage.getItem('oddl.theme');
if (savedTheme === 'dark' || savedTheme === 'light') {
document.documentElement.setAttribute('data-theme', savedTheme);
el.themeToggle.checked = savedTheme === 'dark';
}
const savedDir = localStorage.getItem('oddl.localDir'); const savedDir = localStorage.getItem('oddl.localDir');
if (savedDir) { if (savedDir) {
state.localDir = savedDir; state.localDir = savedDir;
@@ -390,6 +397,12 @@ el.localBrowse.addEventListener('click', async () => {
await refreshLocal(); await refreshLocal();
}); });
el.themeToggle.addEventListener('change', () => {
const theme = el.themeToggle.checked ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('oddl.theme', theme);
});
el.remoteSubmit.addEventListener('click', async () => { el.remoteSubmit.addEventListener('click', async () => {
const url = el.remoteInput.value.trim(); const url = el.remoteInput.value.trim();
if (!url) return; if (!url) return;

View File

@@ -2,6 +2,73 @@
box-sizing: border-box; box-sizing: border-box;
} }
:root {
color-scheme: light dark;
--bg: #f4f6f8;
--surface: #ffffff;
--surface-alt: #fbfcfe;
--border: #d5d7db;
--border-light: #e1e3e6;
--text: #111111;
--text-muted: #4f5765;
--text-soft: #6a7282;
--hover: #eef2f6;
--selected: #dbe7ff;
--progress-track: #e6e9ef;
--progress-fill-start: #5c87ff;
--progress-fill-end: #7aa2ff;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
--bg: #14161b;
--surface: #1f232b;
--surface-alt: #202530;
--border: #2d3440;
--border-light: #2a303a;
--text: #e9ecf1;
--text-muted: #c0c7d3;
--text-soft: #9aa3b2;
--hover: #2a3140;
--selected: #2c3b66;
--progress-track: #2c313c;
--progress-fill-start: #7aa2ff;
--progress-fill-end: #8fb0ff;
}
}
:root[data-theme="dark"] {
--bg: #14161b;
--surface: #1f232b;
--surface-alt: #202530;
--border: #2d3440;
--border-light: #2a303a;
--text: #e9ecf1;
--text-muted: #c0c7d3;
--text-soft: #9aa3b2;
--hover: #2a3140;
--selected: #2c3b66;
--progress-track: #2c313c;
--progress-fill-start: #7aa2ff;
--progress-fill-end: #8fb0ff;
}
:root[data-theme="light"] {
--bg: #f4f6f8;
--surface: #ffffff;
--surface-alt: #fbfcfe;
--border: #d5d7db;
--border-light: #e1e3e6;
--text: #111111;
--text-muted: #4f5765;
--text-soft: #6a7282;
--hover: #eef2f6;
--selected: #dbe7ff;
--progress-track: #e6e9ef;
--progress-fill-start: #5c87ff;
--progress-fill-end: #7aa2ff;
}
html, html,
body { body {
height: 100%; height: 100%;
@@ -10,8 +77,8 @@ body {
body { body {
margin: 0; margin: 0;
font-family: "Segoe UI", Tahoma, sans-serif; font-family: "Segoe UI", Tahoma, sans-serif;
background: #f4f6f8; background: var(--bg);
color: #111; color: var(--text);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
@@ -22,15 +89,17 @@ body {
align-items: center; align-items: center;
gap: 12px; gap: 12px;
padding: 16px 24px; padding: 16px 24px;
background: #ffffff; background: var(--surface);
border-bottom: 1px solid #d5d7db; border-bottom: 1px solid var(--border);
} }
.toolbar input { .toolbar input {
flex: 1; flex: 1;
padding: 8px 10px; padding: 8px 10px;
border: 1px solid #c6c8cc; border: 1px solid var(--border);
border-radius: 4px; border-radius: 4px;
background: var(--surface);
color: var(--text);
} }
.toolbar button { .toolbar button {
@@ -47,8 +116,8 @@ body {
} }
.panel { .panel {
background: #ffffff; background: var(--surface);
border: 1px solid #d5d7db; border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -57,7 +126,7 @@ body {
.panel-head { .panel-head {
padding: 16px; padding: 16px;
border-bottom: 1px solid #e1e3e6; border-bottom: 1px solid var(--border-light);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
@@ -65,10 +134,11 @@ body {
.panel-body { .panel-body {
padding: 12px 16px; padding: 12px 16px;
overflow-y: auto;
overflow-x: auto;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
} }
.list { .list {
@@ -77,6 +147,7 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 0; min-height: 0;
flex: 1;
} }
.list-row { .list-row {
@@ -89,15 +160,21 @@ body {
.list-header { .list-header {
font-weight: 600; font-weight: 600;
color: #2b2f36; color: var(--text);
padding: 6px 8px; padding: 6px 8px;
border-bottom: 1px solid #e1e3e6; border-bottom: 1px solid var(--border-light);
position: relative; position: sticky;
top: 0;
background: var(--surface);
z-index: 1;
} }
.list-body { .list-body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1;
min-height: 0;
overflow: auto;
} }
.col { .col {
@@ -114,13 +191,16 @@ body {
} }
.tree { .tree {
border: 1px solid #e1e3e6; border: 1px solid var(--border-light);
border-radius: 6px; border-radius: 6px;
padding: 8px; padding: 8px;
margin-bottom: 12px; margin-bottom: 12px;
background: #fbfcfe; background: var(--surface-alt);
max-height: 180px; max-height: 180px;
overflow: auto; overflow: auto;
position: sticky;
top: 0;
z-index: 2;
} }
.tree-node { .tree-node {
@@ -134,18 +214,18 @@ body {
} }
.tree-node:hover { .tree-node:hover {
background: #eef2f6; background: var(--hover);
} }
.tree-node.current { .tree-node.current {
background: #dbe7ff; background: var(--selected);
} }
.tree-toggle { .tree-toggle {
display: inline-flex; display: inline-flex;
width: 12px; width: 12px;
justify-content: center; justify-content: center;
color: #5a6475; color: var(--text-soft);
font-size: 12px; font-size: 12px;
} }
@@ -156,7 +236,7 @@ body {
.tree-loading { .tree-loading {
padding: 4px 6px; padding: 4px 6px;
font-size: 12px; font-size: 12px;
color: #6a7282; color: var(--text-soft);
white-space: nowrap; white-space: nowrap;
} }
@@ -170,8 +250,10 @@ body {
.path-row input { .path-row input {
flex: 1; flex: 1;
padding: 6px 10px; padding: 6px 10px;
border: 1px solid #c6c8cc; border: 1px solid var(--border);
border-radius: 4px; border-radius: 4px;
background: var(--surface);
color: var(--text);
} }
.row { .row {
@@ -184,22 +266,22 @@ body {
.row .size, .row .size,
.list-header .size { .list-header .size {
text-align: right; text-align: right;
color: #4f5765; color: var(--text-muted);
} }
.row:hover { .row:hover {
background: #eef2f6; background: var(--hover);
} }
.row.selected { .row.selected {
background: #dbe7ff; background: var(--selected);
} }
.log { .log {
margin: 0 24px 24px; margin: 0 24px 24px;
border: 1px solid #d5d7db; border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
background: #ffffff; background: var(--surface);
flex-shrink: 0; flex-shrink: 0;
padding: 8px 16px; padding: 8px 16px;
} }
@@ -224,7 +306,7 @@ body {
width: 200px; width: 200px;
height: 10px; height: 10px;
border-radius: 999px; border-radius: 999px;
background: #e6e9ef; background: var(--progress-track);
overflow: hidden; overflow: hidden;
display: none; display: none;
} }
@@ -236,7 +318,7 @@ body {
.progress-bar { .progress-bar {
height: 100%; height: 100%;
width: 0%; width: 0%;
background: linear-gradient(90deg, #5c87ff, #7aa2ff); background: linear-gradient(90deg, var(--progress-fill-start), var(--progress-fill-end));
transition: width 0.2s ease; transition: width 0.2s ease;
} }
@@ -248,12 +330,12 @@ body {
.progress-meta { .progress-meta {
font-size: 12px; font-size: 12px;
color: #4f5765; color: var(--text-muted);
min-width: 140px; min-width: 140px;
} }
.progress-meta.secondary { .progress-meta.secondary {
color: #6a7282; color: var(--text-soft);
} }
@keyframes progress-slide { @keyframes progress-slide {