Files
NebulaBrowser/ui/pages/downloads.html
T
Andrew Zambazos 207a849f06 Add Nebula Browser app, UI and assets
Add initial Nebula Browser project skeleton: CMakeLists to configure and link CEF (including post-build steps to copy runtime and UI files), a Windows CEF-based entry (app/main.cpp) that initializes CEF and loads the bundled UI, and a full ui/ and assets/ tree (HTML, CSS, JS, fonts, icons, and branding images). Update .gitignore to ignore build/out, thirdparty/cef, IDE and common OS artifacts.
2026-05-13 22:17:58 +12:00

148 lines
6.2 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Downloads</title>
<link rel="stylesheet" href="../css/style.css" />
<link rel="stylesheet" href="../css/performance.css" />
<style>
body { font-family: system-ui, Segoe UI, Roboto, sans-serif; margin: 16px; color: #eee; background: #121212; }
h1 { font-size: 20px; margin: 0 0 12px; }
.download-list { display: flex; flex-direction: column; gap: 10px; }
.download-item { background: #1e1e1e; border: 1px solid #2a2a2a; border-radius: 8px; padding: 10px 12px; display: grid; grid-template-columns: 1fr auto; gap: 6px 12px; align-items: center; }
.file { font-weight: 600; color: #fafafa; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.meta { font-size: 12px; color: #bbb; }
.progress { height: 6px; background: #2a2a2a; border-radius: 4px; overflow: hidden; grid-column: 1 / -1; }
.bar { height: 100%; background: #3b82f6; width: 0%; transition: width .15s linear; }
.actions { display: flex; gap: 6px; }
button { background: #2b2b2b; border: 1px solid #3a3a3a; color: #eee; border-radius: 6px; padding: 6px 10px; cursor: pointer; }
button:hover { background: #333; }
.state { font-size: 12px; color: #aaa; }
.row { display: flex; gap: 12px; justify-content: space-between; align-items: center; }
.empty { color: #888; font-style: italic; padding: 20px; text-align: center; }
.toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.scan { font-size: 12px; }
.scan.bad { color: #f87171; }
.scan.good { color: #34d399; }
.scan.pending { color: #fbbf24; }
</style>
</head>
<body>
<div class="toolbar">
<h1>Downloads</h1>
<div>
<button id="clear-completed">Clear Completed</button>
</div>
</div>
<div id="list" class="download-list"></div>
<div id="empty" class="empty" style="display:none;">No downloads yet</div>
<script>
const api = window.downloadsAPI || null;
const listEl = document.getElementById('list');
const emptyEl = document.getElementById('empty');
const clearBtn = document.getElementById('clear-completed');
function fmtBytes(n) {
if (!n || n <= 0) return '0 B';
const u = ['B','KB','MB','GB','TB'];
const i = Math.floor(Math.log(n)/Math.log(1024));
return (n/Math.pow(1024,i)).toFixed( i===0 ? 0 : 1 ) + ' ' + u[i];
}
function esc(s) {
return (s || '').replace(/[&<>"']/g, (c) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
})[c]);
}
function rowHtml(d){
const pct = d.totalBytes > 0 ? Math.min(100, Math.round((d.receivedBytes||0) * 100 / d.totalBytes)) : 0;
const scan = d.scan || { status: 'unavailable' };
const isInfected = scan.status === 'infected';
const isScanning = scan.status === 'scanning';
const scanCls = scan.status === 'infected' ? 'scan bad' : (scan.status === 'clean' ? 'scan good' : (scan.status==='scanning'?'scan pending':'scan'));
const scanText = scan.status === 'infected' ? `Threat detected (${scan.engine||''})` :
scan.status === 'clean' ? `Scanned clean (${scan.engine||''})` :
scan.status === 'scanning' ? `Scanning... (${scan.engine||''})` :
scan.status === 'pending' ? `Queued for scan (${scan.engine||''})` :
scan.status === 'error' ? `Scan error${scan.details?': '+esc(scan.details):''}` :
'Scan unavailable';
return `
<div class="download-item" id="dl-${d.id}">
<div class="file" title="${d.filename}">${d.filename}</div>
<div class="actions">
${d.state==='in-progress' ? `
<button data-act="${d.paused?'resume':'pause'}" data-id="${d.id}">${d.paused?'Resume':'Pause'}</button>
<button data-act="cancel" data-id="${d.id}">Cancel</button>
` : `
<button data-act="open-file" data-id="${d.id}" ${(d.state!=='completed'||isInfected)?'disabled':''}>Open</button>
<button data-act="show-in-folder" data-id="${d.id}">Show in Folder</button>
${isInfected ? `<button data-act="delete-file" data-id="${d.id}">Delete</button>` : ''}
${d.state!=='in-progress' ? `<button data-act="rescan" data-id="${d.id}" ${isScanning?'disabled':''}>Rescan</button>` : ''}
`}
</div>
<div class="meta">
<span class="state">${d.state}</span>
· ${fmtBytes(d.receivedBytes||0)} / ${fmtBytes(d.totalBytes||0)}
· <span class="${scanCls}">${scanText}</span>
</div>
<div class="progress"><div class="bar" style="width:${pct}%"></div></div>
</div>
`;
}
function render(entries){
listEl.innerHTML = entries.map(rowHtml).join('');
emptyEl.style.display = entries.length ? 'none' : 'block';
}
async function refresh(){
if (!api || typeof api.list !== 'function') {
render([]);
emptyEl.textContent = 'Downloads are managed by CEF in this build';
return;
}
const all = await api.list();
// Newest first by start time
all.sort((a,b)=> (b.startedAt||0)-(a.startedAt||0));
render(all);
}
listEl.addEventListener('click', async (e)=>{
const btn = e.target.closest('button');
if (!btn) return;
const id = btn.getAttribute('data-id');
const act = btn.getAttribute('data-act');
if (!id || !act) return;
if (!api || typeof api.action !== 'function') return;
await api.action(id, act);
if (act==='cancel') refresh();
});
clearBtn.addEventListener('click', async ()=>{
if (!api || typeof api.clearCompleted !== 'function') return;
await api.clearCompleted();
refresh();
});
api?.onStarted?.(()=> refresh());
api?.onUpdated?.(()=> {
// Partial update: just patch progress bar and bytes if present
// For simplicity now, refresh list
refresh();
});
api?.onDone?.(()=> refresh());
api?.onScanStarted?.(()=> refresh());
api?.onScanResult?.(()=> refresh());
api?.onCleared?.(()=> refresh());
refresh();
</script>
</body>
</html>