releases.ts

4.3 KB · raw

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";1920// §11.1's labels.21const 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};3031/** §21.9's builds panel: every visible release, newest first. */32export 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	// A checkpoint has no page of its own (§21.4), and a save the mirror lacks has none either.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	// §11.2: empty notes show the release save's message, else "Release <v> of <repo>."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}110111// Kepr 2's install hints, with the wording of §21.9.112function 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}