From 861b7b63cbaea9fab48baae3d79faf1672b0e483 Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Sun, 4 Jan 2026 10:12:58 -0600 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8feature:=20Implement=20support=20for?= =?UTF-8?q?=20table-based=20indexes=20and=20dark=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.html | 3 ++ src/main.js | 42 +++++++++++++++ src/renderer.js | 13 +++++ src/styles.css | 140 ++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 169 insertions(+), 29 deletions(-) diff --git a/src/index.html b/src/index.html index 6239c34..a2862b3 100644 --- a/src/index.html +++ b/src/index.html @@ -11,6 +11,9 @@ +
diff --git a/src/main.js b/src/main.js index 31ce357..24f7a54 100644 --- a/src/main.js +++ b/src/main.js @@ -110,8 +110,50 @@ function parseSizeToken(token) { return Math.round(value * multiplier); } +function decodeHtmlEntities(value) { + return value + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'"); +} + function parseApacheIndex(html, baseUrl) { const results = []; + const tableRowRegex = /\s*]*>[\s\S]*?<\/a><\/td>\s*([^<]*)<\/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(/
[\s\S]*?<\/pre>/i);
   const block = preMatch ? preMatch[0] : html;
   const preLines = preMatch ? preMatch[0].split(/\r?\n/) : block.split(/\r?\n/);
diff --git a/src/renderer.js b/src/renderer.js
index 24cce60..c94ff0e 100644
--- a/src/renderer.js
+++ b/src/renderer.js
@@ -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;
diff --git a/src/styles.css b/src/styles.css
index d36318b..bcfbe91 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -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 {