122 lines
4.3 KiB
JavaScript
122 lines
4.3 KiB
JavaScript
document.addEventListener('DOMContentLoaded', loadApps);
|
|
|
|
async function loadApps() {
|
|
const res = await fetch('/api/apps');
|
|
if (res.status === 401) { location.href = '/login'; return; }
|
|
const apps = await res.json();
|
|
|
|
const grid = document.getElementById('apps');
|
|
const empty = document.getElementById('empty');
|
|
|
|
if (apps.length === 0) {
|
|
grid.style.display = 'none';
|
|
empty.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
grid.innerHTML = apps.map(app => `
|
|
<div class="app-card" onclick="showApp('${app.id}')">
|
|
<div class="app-icon">
|
|
${app.latest_icon
|
|
? `<img src="/icons/${app.latest_icon}" alt="${app.name}">`
|
|
: app.name.charAt(0).toUpperCase()}
|
|
</div>
|
|
<div class="app-info">
|
|
<div class="app-name">${esc(app.name)}</div>
|
|
<div class="app-bundle">${esc(app.bundle_id)}</div>
|
|
<div class="app-version">v${esc(app.latest_version || '?')}${app.latest_build_number ? ` (${esc(app.latest_build_number)})` : ''} · ${timeAgo(app.latest_uploaded_at)}</div>
|
|
</div>
|
|
<div class="app-action">
|
|
<a class="install-btn" href="itms-services://?action=download-manifest&url=${encodeURIComponent(location.origin + '/api/manifest/' + app.latest_build_id)}" onclick="event.stopPropagation()">Install</a>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
async function showApp(appId) {
|
|
const res = await fetch(`/api/apps/${appId}`);
|
|
const app = await res.json();
|
|
|
|
const modal = document.getElementById('modal');
|
|
const body = document.getElementById('modal-body');
|
|
|
|
const icon = app.builds[0]?.icon_filename;
|
|
|
|
body.innerHTML = `
|
|
<div class="modal-header">
|
|
<div class="app-icon">
|
|
${icon ? `<img src="/icons/${icon}" alt="${app.name}">` : app.name.charAt(0).toUpperCase()}
|
|
</div>
|
|
<div>
|
|
<div class="app-name" style="font-size:18px">${esc(app.name)}</div>
|
|
<div class="app-bundle">${esc(app.bundle_id)}</div>
|
|
</div>
|
|
</div>
|
|
<h3 style="font-size:14px;color:var(--text-muted);margin-bottom:8px">Builds (${app.builds.length})</h3>
|
|
<div class="build-list">
|
|
${app.builds.map(b => `
|
|
<div class="build-item" id="build-${b.id}">
|
|
<div class="build-meta">
|
|
<div class="build-version">v${esc(b.version)} (${esc(b.build_number || '?')})</div>
|
|
<div class="build-date">${new Date(b.uploaded_at + 'Z').toLocaleDateString()} · ${formatSize(b.size)}</div>
|
|
${b.notes ? `<div class="build-notes">${esc(b.notes)}</div>` : ''}
|
|
</div>
|
|
<div class="build-actions">
|
|
<a class="install-btn btn-sm" href="itms-services://?action=download-manifest&url=${encodeURIComponent(location.origin + '/api/manifest/' + b.id)}">Install</a>
|
|
<button class="delete-btn" onclick="deleteBuild('${b.id}', '${appId}')" title="Delete">×</button>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-sm btn-danger" onclick="deleteApp('${appId}')">Delete App</button>
|
|
</div>
|
|
`;
|
|
|
|
modal.style.display = 'flex';
|
|
modal.onclick = (e) => { if (e.target === modal) closeModal(); };
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('modal').style.display = 'none';
|
|
}
|
|
|
|
async function deleteBuild(buildId, appId) {
|
|
if (!confirm('Delete this build?')) return;
|
|
await fetch(`/api/builds/${buildId}`, { method: 'DELETE' });
|
|
closeModal();
|
|
loadApps();
|
|
}
|
|
|
|
async function deleteApp(appId) {
|
|
if (!confirm('Delete this app and all its builds?')) return;
|
|
await fetch(`/api/apps/${appId}`, { method: 'DELETE' });
|
|
closeModal();
|
|
loadApps();
|
|
}
|
|
|
|
function esc(s) {
|
|
if (!s) return '';
|
|
const d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function formatSize(bytes) {
|
|
if (!bytes) return '';
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
}
|
|
|
|
function timeAgo(dateStr) {
|
|
if (!dateStr) return '';
|
|
const d = new Date(dateStr + 'Z');
|
|
const now = new Date();
|
|
const diff = (now - d) / 1000;
|
|
if (diff < 60) return 'just now';
|
|
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
|
if (diff < 604800) return Math.floor(diff / 86400) + 'd ago';
|
|
return d.toLocaleDateString();
|
|
}
|