\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 {
|