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 ? html`<div class="release-latest">51<p class="muted mono">latest release</p>52${releaseBlock(repo, releases[0] as Release, counts)}53</div>54${releases.length > 1 && html`<p class="release-earlier muted mono">earlier releases</p>`}55${releases.slice(1).map((release) => olderRow(repo, release, counts))}`56 : html`<p class="muted mono save-empty">no releases yet</p>`57}58</section>`,59 }),60 );61 });62}6364function releaseBlock(repo: PublicRepo, release: Release, counts: Map<string, number>): Html {65 return html`<article class="release" id="${release.version}">66<p class="release-head"><span class="release-version">v${67 release.version68 }</span>${saveLink(repo, release)}<span class="history-when">${formatDate(release.publishedAt)}</span></p>69${releaseDetails(repo, release, counts)}70</article>`;71}727374function olderRow(repo: PublicRepo, release: Release, counts: Map<string, number>): Html {75 const save = saveByPrefix(repo.mirror, release.save);76 return html`<div class="archive-row release-row" id="${release.version}">77<p class="archive-save"><span class="release-version">v${78 release.version79 }</span>${saveLink(repo, release)}${html`<span class="history-msg archive-msg">${save?.message.split("\n")[0] ?? ""}</span>`}${html`<span class="history-when">${formatDate(release.publishedAt)}</span>`}</p>80<p class="archive-links mono">${release.files.map(81 (file) =>82 html`<a href="${downloadHref(repo.settings.name, release, file)}">↓ ${PLATFORM_LABELS[file.platform]}</a>`,83 )}</p>84<details class="release-more">85<summary class="mono">notes · checksums</summary>86${releaseDetails(repo, release, counts)}87</details>88</div>`;89}9091function saveLink(repo: PublicRepo, release: Release): Html {92 const save = saveByPrefix(repo.mirror, release.save);93 94 return save && !save.is_checkpoint95 ? html`<a class="history-id" href="/${repo.settings.name}/saves/${save.id}">${save.id}</a>`96 : html`<span class="history-id">${release.save}</span>`;97}9899function releaseDetails(repo: PublicRepo, release: Release, counts: Map<string, number>): Html {100 const name = repo.settings.name;101 102 const notes =103 release.notes !== ""104 ? renderMarkdown(release.notes, { repo: name, dir: "", at: null })105 : html`<p>${saveByPrefix(repo.mirror, release.save)?.message ?? `Release ${release.version} of ${name}.`}</p>`;106 return html`<div class="release-notes markdown">${notes}</div>107<div class="release-files">108${release.files.map((file) =>109 fileRow(110 downloadHref(name, release, file),111 file,112 counts.get(`${release.version}/${file.platform}`) ?? 0,113 ),114)}115</div>`;116}117118function downloadHref(name: string, release: Release, file: ReleaseFile): string {119 return `/${name}/releases/${release.version}/${encodeURIComponent(downloadName(name, release, file))}`;120}121122function fileRow(href: string, file: ReleaseFile, downloads: number): Html {123 const hint = installHint(file.name);124 return html`<div class="release-file">125<p class="release-file-head"><span class="release-platform">${126 PLATFORM_LABELS[file.platform]127 }</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>128<p class="release-sha mono"><span class="muted">sha256</span> ${file.sha256}</p>129${hint && html`<p class="release-hint muted">${hint}</p>`}130</div>`;131}132133134function installHint(fileName: string): string {135 const lower = fileName.toLowerCase();136 if (lower.endsWith(".app.zip")) return "unzip and drag to Applications";137 if (lower.endsWith(".apk")) {138 return "open on your device to install — allow installs from this source if prompted";139 }140 return "";141}