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