✨feature: Implement support for table-based indexes and dark mode
This commit is contained in:
@@ -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">
|
||||
|
||||
42
src/main.js
42
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 = /<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/);
|
||||
|
||||
@@ -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;
|
||||
|
||||
140
src/styles.css
140
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 {
|
||||
|
||||
Reference in New Issue
Block a user