First commit of webxdc apps viewer and apps files for sample content pack

This commit is contained in:
n8fr8 2026-04-15 09:55:27 -04:00
commit 94e85f2bd8
274 changed files with 3511 additions and 0 deletions

118
index.html Normal file
View file

@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebXDC Apps for DeltaChat</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 900px; margin: 2em auto; padding: 0 1em; background: #fafafa; color: #222; }
h1 { text-align: center; font-size: 1.5em; }
h2 { text-align: center; font-size: 1.2em; }
h3 { text-align: center; font-size: 1.0em; }
#search { width: 100%; padding: 0.6em; font-size: 1em; margin-bottom: 1em; box-sizing: border-box; }
#count { font-size: 0.85em; color: #666; margin-bottom: 0.5em; }
.app { display: flex; gap: 1em; padding: 1em; background: #fff; border: 1px solid #ddd; border-radius: 8px; margin-bottom: 0.8em; }
.app img { width: 64px; height: 64px; object-fit: contain; flex-shrink: 0; border-radius: 8px; background: #eee; }
.app-body { flex: 1; min-width: 0; }
.app-title { font-weight: bold; font-size: 1.1em; margin: 0 0 0.2em; word-wrap: break-word; }
.app-meta { font-size: 0.85em; color: #666; margin-bottom: 0.4em; }
.app-desc { font-size: 0.9em; white-space: pre-wrap; margin-bottom: 0.5em; overflow-wrap: break-word; }
.app-links { display: flex; flex-wrap: wrap; gap: 0.5em 1em; }
.app-links a { font-size: 0.9em; }
.error { color: #b00; }
@media (max-width: 600px) {
body { margin: 1em auto; padding: 0 0.75em; }
h1 { font-size: 1.25em; }
.app { padding: 0.75em; gap: 0.75em; border-radius: 6px; }
.app img { width: 48px; height: 48px; }
.app-title { font-size: 1em; }
.app-meta { font-size: 0.8em; }
.app-desc { font-size: 0.875em; }
.app-links a { padding: 0.25em 0; }
}
</style>
</head>
<body>
<h1>WebXDC Apps (for DeltaChat)</h1>
<h3>Download, send and run the .XDC apps as DeltaChat message... on internet needed!
<input id="search" type="search" placeholder="Search apps...">
<div id="count"></div>
<div id="list">Loading...</div>
<script>
function fmtSize(b) {
if (b < 1024) return b + ' B';
if (b < 1024*1024) return (b/1024).toFixed(1) + ' KB';
return (b/1024/1024).toFixed(1) + ' MB';
}
function render(apps, filter) {
const list = document.getElementById('list');
const f = filter.trim().toLowerCase();
const filtered = f ? apps.filter(a =>
(a.name || '').toLowerCase().includes(f) ||
(a.description || '').toLowerCase().includes(f) ||
(a.category || '').toLowerCase().includes(f)
) : apps;
document.getElementById('count').textContent = filtered.length + ' of ' + apps.length + ' apps';
list.innerHTML = '';
for (const a of filtered) {
const div = document.createElement('div');
div.className = 'app';
const img = document.createElement('img');
img.src = a.icon_relname ? 'apps/' + a.icon_relname : '';
img.alt = '';
img.onerror = () => { img.style.visibility = 'hidden'; };
const body = document.createElement('div');
body.className = 'app-body';
const title = document.createElement('div');
title.className = 'app-title';
title.textContent = a.name || a.app_id;
const meta = document.createElement('div');
meta.className = 'app-meta';
const date = a.date ? a.date.slice(0, 10) : '';
meta.textContent = [a.category, a.tag_name, date, fmtSize(a.size || 0)].filter(Boolean).join(' \u00b7 ');
const desc = document.createElement('div');
desc.className = 'app-desc';
desc.textContent = a.description || '';
const links = document.createElement('div');
links.className = 'app-links';
if (a.cache_relname) {
const dl = document.createElement('a');
dl.href = 'apps/' + a.cache_relname;
dl.textContent = 'Download .xdc';
links.appendChild(dl);
}
if (a.url) {
const up = document.createElement('a');
up.href = a.url;
up.textContent = 'Upstream';
up.target = '_blank';
up.rel = 'noopener';
links.appendChild(up);
}
if (a.source_code_url) {
const src = document.createElement('a');
src.href = a.source_code_url;
src.textContent = 'Source';
src.target = '_blank';
src.rel = 'noopener';
links.appendChild(src);
}
body.append(title, meta, desc, links);
div.append(img, body);
list.appendChild(div);
}
if (!filtered.length) list.textContent = 'No apps match.';
}
fetch('apps/xdcget-lock.json')
.then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
.then(apps => {
apps.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
render(apps, '');
document.getElementById('search').addEventListener('input', e => render(apps, e.target.value));
})
.catch(err => {
document.getElementById('list').innerHTML = '<p class="error">Failed to load apps/xdcget-lock.json: ' + err.message + '</p>';
});
</script>
</body>
</html>