1import type { Hono } from "hono";2import type { ReleaseFile } from "../bucket/repo.ts";3import { saveByPrefix } from "../mirror/queries.ts";4import { downloadCounts, type Release, visibleReleases } from "../releases/releases.ts";5import { type Html, html } from "./html.ts";6import {7 cacheFor,8 formatDate,9 formatSize,10 notFound,11 openPublicRepo,12 type PageContext,13 type PublicRepo,14 page,15 plural,16} from "./layout.ts";17import { renderMarkdown } from "./markdown.ts";181920const PLATFORM_LABELS: Record<string, string> = {21 "linux-x86_64": "Linux · x86_64",22 "linux-aarch64": "Linux · ARM64",23 "macos-aarch64": "macOS · Apple Silicon",24 "macos-x86_64": "macOS · Intel",25 "windows-x86_64": "Windows · x86_64",26 "android-arm64": "Android · ARM64",27 "android-x86": "Android · x86_64",28};293031export function registerReleasesPage(app: Hono, ctx: PageContext): void {32 app.get("/:repo/releases", (c) => {33 const repo = openPublicRepo(ctx, c.req.param("repo"));34 if (!repo) return notFound(c, ctx);35 const name = repo.settings.name;36 const releases = visibleReleases(ctx.kepr, repo.mirror, name);37 const counts = downloadCounts(ctx.kepr, name);38 cacheFor(c, false);39 return c.html(40 page(ctx, {41 title: `builds · ${name}`,42 description: `Releases of ${name}, with downloads and checksums.`,43 path: `/${name}/releases`,44 body: html`<p class="breadcrumb mono"><a href="/${name}">${name}</a></p>45<h1 class="file-page-title">builds</h1>46<section class="repo-section">47${48 releases.length > 049 ? releases.map((release, index) =>50 index === 051 ? html`<div class="release-latest"><p class="muted mono">latest release</p>${releaseBlock(repo, release, counts)}</div>`52 : html`<details class="release-older"><summary class="mono">${index === 1 ? "earlier releases · " : ""}<span>v${release.version}</span> · ${formatDate(release.publishedAt)}</summary>${releaseBlock(repo, release, counts)}</details>`,53 )54 : html`<p class="muted mono save-empty">no releases yet</p>`55}56</section>`,57 }),58 );59 });60}6162function releaseBlock(repo: PublicRepo, release: Release, counts: Map<string, number>): Html {63 const name = repo.settings.name;64 const save = saveByPrefix(repo.mirror, release.save);65 66 const saveLink =67 save && !save.is_checkpoint68 ? html`<a class="history-id" href="/${name}/saves/${save.id}">${save.id}</a>`69 : html`<span class="history-id">${release.save}</span>`;70 71 const notes =72 release.notes !== ""73 ? renderMarkdown(release.notes, { repo: name, dir: "", at: null })74 : html`<p>${save?.message ?? `Release ${release.version} of ${name}.`}</p>`;75 return html`<article class="release" id="${release.version}">76<p class="release-head"><span class="release-version">v${77 release.version78 }</span>${saveLink}<span class="history-when">${formatDate(release.publishedAt)}</span></p>79<div class="release-notes markdown">${notes}</div>80<div class="release-files">81${release.files.map((file) =>82 fileRow(name, release.version, file, counts.get(`${release.version}/${file.platform}`) ?? 0),83)}84</div>85</article>`;86}8788function fileRow(repo: string, version: string, file: ReleaseFile, downloads: number): Html {89 const hint = installHint(file.name);90 91 return html`<div class="release-file">92<p class="release-file-head"><span class="release-platform">${93 PLATFORM_LABELS[file.platform]94 }</span>${html`<a class="release-download mono" href="${`/${repo}/releases/${version}/${repo}-${file.platform}`}">↓ ${file.name}</a>`}${html`<span class="release-meta mono">${formatSize(file.size)} · ${plural(downloads, "download")}</span>`}</p>95<p class="release-sha mono"><span class="muted">sha256</span> ${file.sha256}</p>96${hint && html`<p class="release-hint muted">${hint}</p>`}97</div>`;98}99100101function installHint(fileName: string): string {102 const lower = fileName.toLowerCase();103 if (lower.endsWith(".app.zip")) return "unzip and drag to Applications";104 if (lower.endsWith(".apk")) {105 return "open on your device to install — allow installs from this source if prompted";106 }107 return "";108}