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>
<input id="remote-url" type="text" placeholder="http://example.com/" />
<button id="remote-submit">Submit</button>
<label class="checkbox">
<input id="theme-toggle" type="checkbox" /> Dark mode
</label>
</header>
<main class="grid">

View File

@@ -110,8 +110,50 @@ function parseSizeToken(token) {
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) {
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 block = preMatch ? preMatch[0] : html;
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'),
downloadBtn: document.querySelector('#download-btn'),
useWget: document.querySelector('#use-wget'),
themeToggle: document.querySelector('#theme-toggle'),
progress: document.querySelector('#transfer-progress'),
progressBar: document.querySelector('#transfer-bar'),
progressMeta: document.querySelector('#transfer-meta'),
@@ -353,6 +354,12 @@ async function init() {
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');
if (savedDir) {
state.localDir = savedDir;
@@ -390,6 +397,12 @@ el.localBrowse.addEventListener('click', async () => {
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 () => {
const url = el.remoteInput.value.trim();
if (!url) return;

View File

@@ -2,6 +2,73 @@
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,
body {
height: 100%;
@@ -10,8 +77,8 @@ body {
body {
margin: 0;
font-family: "Segoe UI", Tahoma, sans-serif;
background: #f4f6f8;
color: #111;
background: var(--bg);
color: var(--text);
display: flex;
flex-direction: column;
overflow: hidden;
@@ -22,15 +89,17 @@ body {
align-items: center;
gap: 12px;
padding: 16px 24px;
background: #ffffff;
border-bottom: 1px solid #d5d7db;
background: var(--surface);
border-bottom: 1px solid var(--border);
}
.toolbar input {
flex: 1;
padding: 8px 10px;
border: 1px solid #c6c8cc;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface);
color: var(--text);
}
.toolbar button {
@@ -47,8 +116,8 @@ body {
}
.panel {
background: #ffffff;
border: 1px solid #d5d7db;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
display: flex;
flex-direction: column;
@@ -57,7 +126,7 @@ body {
.panel-head {
padding: 16px;
border-bottom: 1px solid #e1e3e6;
border-bottom: 1px solid var(--border-light);
display: flex;
flex-direction: column;
gap: 12px;
@@ -65,10 +134,11 @@ body {
.panel-body {
padding: 12px 16px;
overflow-y: auto;
overflow-x: auto;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.list {
@@ -77,6 +147,7 @@ body {
display: flex;
flex-direction: column;
min-height: 0;
flex: 1;
}
.list-row {
@@ -89,15 +160,21 @@ body {
.list-header {
font-weight: 600;
color: #2b2f36;
color: var(--text);
padding: 6px 8px;
border-bottom: 1px solid #e1e3e6;
position: relative;
border-bottom: 1px solid var(--border-light);
position: sticky;
top: 0;
background: var(--surface);
z-index: 1;
}
.list-body {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: auto;
}
.col {
@@ -114,13 +191,16 @@ body {
}
.tree {
border: 1px solid #e1e3e6;
border: 1px solid var(--border-light);
border-radius: 6px;
padding: 8px;
margin-bottom: 12px;
background: #fbfcfe;
background: var(--surface-alt);
max-height: 180px;
overflow: auto;
position: sticky;
top: 0;
z-index: 2;
}
.tree-node {
@@ -134,18 +214,18 @@ body {
}
.tree-node:hover {
background: #eef2f6;
background: var(--hover);
}
.tree-node.current {
background: #dbe7ff;
background: var(--selected);
}
.tree-toggle {
display: inline-flex;
width: 12px;
justify-content: center;
color: #5a6475;
color: var(--text-soft);
font-size: 12px;
}
@@ -156,7 +236,7 @@ body {
.tree-loading {
padding: 4px 6px;
font-size: 12px;
color: #6a7282;
color: var(--text-soft);
white-space: nowrap;
}
@@ -170,8 +250,10 @@ body {
.path-row input {
flex: 1;
padding: 6px 10px;
border: 1px solid #c6c8cc;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface);
color: var(--text);
}
.row {
@@ -184,22 +266,22 @@ body {
.row .size,
.list-header .size {
text-align: right;
color: #4f5765;
color: var(--text-muted);
}
.row:hover {
background: #eef2f6;
background: var(--hover);
}
.row.selected {
background: #dbe7ff;
background: var(--selected);
}
.log {
margin: 0 24px 24px;
border: 1px solid #d5d7db;
border: 1px solid var(--border);
border-radius: 6px;
background: #ffffff;
background: var(--surface);
flex-shrink: 0;
padding: 8px 16px;
}
@@ -224,7 +306,7 @@ body {
width: 200px;
height: 10px;
border-radius: 999px;
background: #e6e9ef;
background: var(--progress-track);
overflow: hidden;
display: none;
}
@@ -236,7 +318,7 @@ body {
.progress-bar {
height: 100%;
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;
}
@@ -248,12 +330,12 @@ body {
.progress-meta {
font-size: 12px;
color: #4f5765;
color: var(--text-muted);
min-width: 140px;
}
.progress-meta.secondary {
color: #6a7282;
color: var(--text-soft);
}
@keyframes progress-slide {