const API_BASE = window.location.origin; let queuePoll = null; // ── Tabs ──────────────────────────────────────────────────────────────────── function startQueuePoll() { stopQueuePoll(); queuePoll = setInterval(renderQueue, 2000); } function stopQueuePoll() { if (queuePoll) { clearInterval(queuePoll); queuePoll = null; } } function switchTab(name) { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); document.querySelector(`.tab[data-tab="${name}"]`).classList.add('active'); document.getElementById('tab-' + name).classList.add('active'); if (name === 'queue') { renderQueue(); startQueuePoll(); } else stopQueuePoll(); } document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => switchTab(tab.dataset.tab)); }); // ── Search ────────────────────────────────────────────────────────────────── const searchInput = document.getElementById('searchInput'); const searchBtn = document.getElementById('searchBtn'); const searchResults = document.getElementById('searchResults'); async function doSearch() { const q = searchInput.value.trim(); if (!q) return; searchResults.innerHTML = '
Searching...
'; try { const res = await fetch(`${API_BASE}/api/search?q=${encodeURIComponent(q)}&limit=30`); if (!res.ok) throw new Error(await res.text()); const data = await res.json(); renderSearchResults(data); } catch (err) { searchResults.innerHTML = `
Search failed: ${err.message}
`; } } searchBtn.addEventListener('click', doSearch); searchInput.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); }); function formatSize(bytes) { if (!bytes) return '0 B'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0; let size = bytes; while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; } return size.toFixed(i > 0 ? 1 : 0) + ' ' + units[i]; } function renderSearchResults(files) { if (!files || files.length === 0) { searchResults.innerHTML = '
No results found.
'; return; } searchResults.innerHTML = files.map(f => { const added = Boolean(addedIdents[f.ident]); return `
${escapeHtml(f.name)}
${formatSize(f.size)} · votes: ${f.votes}
`; }).join(''); searchResults.querySelectorAll('button:not(.added)').forEach(btn => { btn.addEventListener('click', async () => { const ident = btn.dataset.ident; const name = btn.dataset.name; const size = parseInt(btn.dataset.size); btn.disabled = true; btn.textContent = 'Adding...'; try { await addDownload(ident, name, size); btn.classList.add('added'); btn.textContent = 'Added'; } catch (err) { btn.textContent = 'Failed'; setTimeout(() => { btn.disabled = false; btn.textContent = 'Add to Queue'; }, 2000); } }); }); } // ── Add download via new API route ────────────────────────────────────────── const addedIdents = {}; async function addDownload(ident, name, size) { const res = await fetch(`${API_BASE}/api/queue/add`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ident, name, size }), }); if (!res.ok) throw new Error(await res.text()); addedIdents[ident] = true; } // ── Queue ─────────────────────────────────────────────────────────────────── const queueList = document.getElementById('queueList'); async function fetchQueue() { const res = await fetch(`${API_BASE}/api/queue`); if (!res.ok) throw new Error(await res.text()); return res.json(); } function renderQueue() { fetchQueue().then(entries => { if (!entries || entries.length === 0) { queueList.innerHTML = '
Queue is empty.
'; return; } queueList.innerHTML = entries.map(e => { const pct = Math.round((e.progress || 0) * 100); const downloaded = formatSize(e.progress * e.size); const total = formatSize(e.size); let fillClass = ''; if (e.state === 'uploading') fillClass = 'done'; else if (e.state === 'error') fillClass = 'error'; else if (e.state === 'queuedDL') fillClass = 'queued'; return `
${e.state} ${escapeHtml(e.name)}
${downloaded} / ${total} ${pct}%
${e.speed || ''}
`; }).join(''); // Attach cancel handlers queueList.querySelectorAll('.cancel-btn').forEach(btn => { btn.addEventListener('click', async () => { const hash = btn.dataset.hash; btn.disabled = true; btn.textContent = '...'; try { await fetch(`${API_BASE}/api/queue/cancel`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hash }), }); } catch {} renderQueue(); }); }); }).catch(err => { queueList.innerHTML = `
Failed to load queue: ${err.message}
`; }); } // On load, switch to queue tab if there are active downloads fetchQueue().then(entries => { if (entries && entries.length > 0) switchTab('queue'); }); // Stop polling when tab hidden document.addEventListener('visibilitychange', () => { if (document.hidden) stopQueuePoll(); else { const active = document.querySelector('.tab.active'); if (active && active.dataset.tab === 'queue') startQueuePoll(); } }); // ── Helpers ───────────────────────────────────────────────────────────────── function escapeHtml(str) { const d = document.createElement('div'); d.textContent = str; return d.innerHTML; } function escapeAttr(str) { return str.replace(/"/g, '"').replace(/'/g, '''); }