const state = { localDir: '', remoteUrl: '', localEntries: [], remoteEntries: [], hasWget: false, }; const el = { remoteInput: document.querySelector('#remote-url'), remoteSubmit: document.querySelector('#remote-submit'), remoteList: document.querySelector('#remote-list'), remoteListBody: document.querySelector('#remote-list-body'), remoteTree: document.querySelector('#remote-tree'), localPath: document.querySelector('#local-path'), localBrowse: document.querySelector('#local-browse'), localList: document.querySelector('#local-list'), localListBody: document.querySelector('#local-list-body'), downloadBtn: document.querySelector('#download-btn'), useWget: document.querySelector('#use-wget'), logList: document.querySelector('#log-list'), }; function formatBytes(size) { if (size === 0) return '0 B'; if (!size) return '-'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; let value = size; let unitIndex = 0; while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024; unitIndex += 1; } return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`; } function log(message) { const line = document.createElement('div'); line.textContent = message; el.logList.prepend(line); } function renderLocal() { el.localListBody.innerHTML = ''; state.localEntries.forEach((entry) => { const row = document.createElement('div'); row.className = 'row list-row'; const name = document.createElement('div'); name.className = 'name'; name.textContent = entry.isDir ? `${entry.name}/` : entry.name; const size = document.createElement('div'); size.className = 'size'; size.textContent = entry.isDir ? '-' : formatBytes(entry.size); row.appendChild(name); row.appendChild(size); row.addEventListener('dblclick', async () => { if (!entry.isDir) return; state.localDir = entry.path; await refreshLocal(); }); el.localListBody.appendChild(row); }); } function renderRemote() { el.remoteListBody.innerHTML = ''; state.remoteEntries.forEach((entry) => { const row = document.createElement('div'); row.className = 'row list-row'; const name = document.createElement('div'); name.className = 'name'; name.textContent = entry.isDir ? entry.name : entry.name; const size = document.createElement('div'); size.className = 'size'; size.textContent = entry.isDir ? '-' : entry.size || '-'; row.appendChild(name); row.appendChild(size); row.addEventListener('dblclick', async () => { if (!entry.isDir) return; state.remoteUrl = entry.href; el.remoteInput.value = state.remoteUrl; await refreshRemote(); }); row.addEventListener('click', () => { el.remoteList.querySelectorAll('.selected').forEach((n) => n.classList.remove('selected')); row.classList.add('selected'); row.dataset.href = entry.href; row.dataset.isDir = entry.isDir ? '1' : '0'; }); el.remoteListBody.appendChild(row); }); } function renderRemoteTree() { el.remoteTree.innerHTML = ''; if (!state.remoteUrl) return; let urlObj; try { urlObj = new URL(state.remoteUrl); } catch (err) { return; } const segments = urlObj.pathname.split('/').filter(Boolean); const nodes = []; nodes.push({ name: `${urlObj.origin}/`, url: `${urlObj.origin}/`, depth: 0, current: segments.length === 0 }); let currentPath = '/'; segments.forEach((seg, index) => { currentPath += `${seg}/`; nodes.push({ name: `${seg}/`, url: `${urlObj.origin}${currentPath}`, depth: index + 1, current: index === segments.length - 1, }); }); const childDirs = state.remoteEntries.filter((entry) => entry.isDir); childDirs.forEach((entry) => { nodes.push({ name: entry.name, url: entry.href, depth: segments.length + 1, current: false, }); }); nodes.forEach((node) => { const row = document.createElement('div'); row.className = `tree-row${node.current ? ' current' : ''}`; row.textContent = node.name; row.style.paddingLeft = `${node.depth * 16}px`; row.addEventListener('click', async () => { state.remoteUrl = node.url; el.remoteInput.value = state.remoteUrl; await refreshRemote(); }); el.remoteTree.appendChild(row); }); } async function refreshLocal() { if (!state.localDir) return; const entries = await window.oddl.listLocalDir(state.localDir); state.localEntries = entries; el.localPath.value = state.localDir; renderLocal(); } async function refreshRemote() { if (!state.remoteUrl) return; const entries = await window.oddl.listRemoteDir(state.remoteUrl); state.remoteEntries = entries; renderRemote(); renderRemoteTree(); } function setupResizer(listEl, resizerEl) { let startX = 0; let startWidth = 0; function onMove(event) { const delta = event.clientX - startX; const newWidth = Math.max(160, startWidth + delta); listEl.style.setProperty('--col-name', `${newWidth}px`); } function onUp() { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); } resizerEl.addEventListener('mousedown', (event) => { event.preventDefault(); startX = event.clientX; const nameCol = listEl.querySelector('.list-header .name'); startWidth = nameCol ? nameCol.getBoundingClientRect().width : 240; window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp); }); } async function init() { const caps = await window.oddl.getCapabilities(); state.hasWget = caps.hasWget; el.useWget.disabled = !state.hasWget; if (!state.hasWget) { el.useWget.checked = false; } const home = await window.oddl.selectLocalDir(); if (home) { state.localDir = home; await refreshLocal(); } document.querySelectorAll('.col-resizer').forEach((resizer) => { const targetId = resizer.dataset.target; const listEl = document.getElementById(targetId); if (listEl) { setupResizer(listEl, resizer); } }); } el.localBrowse.addEventListener('click', async () => { const dir = await window.oddl.selectLocalDir(); if (!dir) return; state.localDir = dir; await refreshLocal(); }); el.remoteSubmit.addEventListener('click', async () => { const url = el.remoteInput.value.trim(); if (!url) return; state.remoteUrl = url; await refreshRemote(); }); el.downloadBtn.addEventListener('click', async () => { const selected = el.remoteList.querySelector('.selected'); if (!selected) { log('Select a remote file or folder first.'); return; } if (!state.localDir) { log('Choose a local destination first.'); return; } const href = selected.dataset.href; const isDir = selected.dataset.isDir === '1'; log(`Queueing ${href}`); await window.oddl.downloadItem({ url: href, destDir: state.localDir, isDir, useWget: el.useWget.checked, }); log('Done.'); await refreshLocal(); }); window.oddl.onLog((message) => log(message)); init().catch((err) => log(err.message));