119 lines
4.7 KiB
HTML
119 lines
4.7 KiB
HTML
|
|
<!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>
|