const fallbackIcon = "/assets/argus-icon.png?v=argus-panoptes-3d-20260508"; const appIcons = new Map([ ["aletheia-kotlin", "/assets/aletheia-icon.svg?v=aletheia-launcher-20260508"] ]); const state = { apps: [], selectedSlug: null }; const catalogGrid = document.getElementById("catalog-grid"); const recentReleases = document.getElementById("recent-releases"); const appIcon = document.getElementById("app-icon"); const appName = document.getElementById("app-name"); const appDescription = document.getElementById("app-description"); const releaseVersion = document.getElementById("release-version"); const detailMeta = document.getElementById("detail-meta"); const releaseList = document.getElementById("release-list"); const manifestLink = document.getElementById("manifest-link"); const primaryDownload = document.getElementById("primary-download"); const refreshButton = document.getElementById("refresh-button"); refreshButton.addEventListener("click", () => loadAll()); window.addEventListener("hashchange", handleHashChange); loadAll(); async function loadAll() { await Promise.all([loadCatalog(), loadRecentReleases()]); handleHashChange(); } async function loadCatalog() { const response = await fetch("/api/apps"); const apps = await response.json(); state.apps = Array.isArray(apps) ? apps : []; renderCatalog(); } async function loadRecentReleases() { const response = await fetch("/api/releases/recent"); const items = await response.json(); if (!Array.isArray(items) || !items.length) { recentReleases.innerHTML = '
Пока нет опубликованных релизов.
'; return; } recentReleases.innerHTML = items.map(item => `

${escapeHtml(item.appName)}

${escapeHtml(item.release.version)}

${escapeHtml(item.release.platform)} - ${escapeHtml(item.release.packageKind)} - ${formatDate(item.release.publishedAt)}

`).join(""); } function renderCatalog() { if (!state.apps.length) { catalogGrid.innerHTML = '
Пока нет опубликованных приложений.
'; renderEmptyDetail(); return; } catalogGrid.innerHTML = state.apps.map(app => { const isActive = app.slug === state.selectedSlug; const latest = app.latestRelease; return `
${renderAppIcon("app-card-icon", app, 72)}

${escapeHtml(app.name)}

${escapeHtml(app.summary)}

${latest ? escapeHtml(latest.version) : "нет релиза"} ${latest ? escapeHtml(latest.packageKind) : "пакет"} ${app.releaseCount} версия
`; }).join(""); for (const card of catalogGrid.querySelectorAll(".app-card")) { card.addEventListener("click", () => { window.location.hash = card.dataset.slug; }); } } function handleHashChange() { const slugFromHash = decodeURIComponent(window.location.hash.replace(/^#/, "")); const hashApp = state.apps.find(app => app.slug === slugFromHash); const nextSlug = hashApp?.slug ?? state.apps[0]?.slug ?? null; state.selectedSlug = nextSlug; if (!nextSlug) { renderEmptyDetail(); return; } renderCatalog(); if (slugFromHash && !hashApp) { window.history.replaceState(null, "", `#${encodeURIComponent(nextSlug)}`); } loadAppDetail(nextSlug); } async function loadAppDetail(slug) { const response = await fetch(`/api/apps/${encodeURIComponent(slug)}`); if (!response.ok) { renderEmptyDetail("Карточка приложения не загрузилась."); return; } const app = await response.json(); const latest = app.releases[0] ?? null; appIcon.src = getAppIconSrc(app); appName.textContent = app.name; appDescription.textContent = app.description || app.summary; manifestLink.href = `/api/apps/${encodeURIComponent(app.slug)}/manifest`; if (latest) { releaseVersion.textContent = latest.version; primaryDownload.href = latest.downloadPath; primaryDownload.removeAttribute("aria-disabled"); } else { releaseVersion.textContent = "-"; primaryDownload.href = "#"; primaryDownload.setAttribute("aria-disabled", "true"); } detailMeta.innerHTML = ` ${renderMetaBlock("Обновлено", formatDate(app.updatedAt))} ${renderMetaBlock("Платформа", latest ? latest.platform : "Не опубликовано")} ${renderMetaBlock("Тип", latest ? latest.packageKind : "-")} ${renderMetaBlock("Размер", latest ? formatBytes(latest.packageSizeBytes) : "-")} `; if (!app.releases.length) { releaseList.innerHTML = '
Для этого приложения ещё нет опубликованного файла.
'; return; } releaseList.innerHTML = app.releases.slice(0, 1).map(release => `

Файл ${escapeHtml(release.version)}

${escapeHtml(release.channel)}

${formatDate(release.publishedAt)} - ${formatBytes(release.packageSizeBytes)} - ${escapeHtml(release.originalFileName)}

${release.notes ? `

${escapeHtml(release.notes)}

` : ""}
${escapeHtml(release.platform)} ${escapeHtml(release.packageKind)}
`).join(""); } function renderEmptyDetail(message = "Данные приложения появятся после загрузки API.") { appIcon.src = fallbackIcon; appName.textContent = "Argus"; appDescription.textContent = message; releaseVersion.textContent = "-"; detailMeta.innerHTML = ""; releaseList.innerHTML = ""; manifestLink.href = "#"; primaryDownload.href = "#"; primaryDownload.setAttribute("aria-disabled", "true"); } function renderMetaBlock(label, value) { return `
${escapeHtml(label)}
${escapeHtml(value)}
`; } function renderAppIcon(className, app, size) { return ``; } function getAppIconSrc(app) { return appIcons.get(app?.slug) ?? fallbackIcon; } function formatDate(value) { return new Intl.DateTimeFormat("ru-RU", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }).format(new Date(value)); } function formatBytes(bytes) { if (!Number.isFinite(bytes) || bytes <= 0) { return "0 Б"; } const units = ["Б", "КБ", "МБ", "ГБ"]; let index = 0; let value = bytes; while (value >= 1024 && index < units.length - 1) { value /= 1024; index += 1; } return `${value.toFixed(value >= 10 || index === 0 ? 0 : 1)} ${units[index]}`; } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); }