207a849f06
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.
148 lines
6.2 KiB
HTML
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) => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
})[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>
|