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.
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user