✨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>
|
<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">
|
||||||
|
|||||||
42
src/main.js
42
src/main.js
@@ -110,8 +110,50 @@ function parseSizeToken(token) {
|
|||||||
return Math.round(value * multiplier);
|
return Math.round(value * multiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function decodeHtmlEntities(value) {
|
||||||
|
return value
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/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/);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
140
src/styles.css
140
src/styles.css
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user