Files
t5de-site/lib/releases.ts
T
dhkatzandClaude Opus 5.5 8401f30f78
CI / test (push) Successful in 13s
CI / image (push) Successful in 12s
Release / release (push) Successful in 3s
feat: T5DE's website: landing page, download, changelog and FAQ
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-03 01:55:42 -08:00

82 lines
2.7 KiB
TypeScript

// T5DE's client releases, from git.t5de.dev's API (the t5de repository's releases, which its CI publishes). Pages
// using them are regenerated every few minutes; if Gitea can't be reached, they link to its releases page instead.
const repo = "https://git.t5de.dev/api/v1/repos/t5de/t5de";
export const releasesPage = "https://git.t5de.dev/t5de/t5de/releases";
const revalidate = 300;
export type Release = {
tag: string;
version: string;
date: string;
nightly: boolean;
url: string;
body: string;
setup: { url: string; size: number } | null;
sha256: string | null;
imvu: string | null;
};
type GiteaRelease = {
tag_name: string;
name: string;
body: string;
prerelease: boolean;
draft: boolean;
published_at: string;
html_url: string;
assets: { name: string; size: number; browser_download_url: string }[];
};
function parse(r: GiteaRelease): Release {
const nightly = r.tag_name === "nightly";
const setup = r.assets.find((a) => a.name === "T5DE-Setup.exe");
return {
tag: r.tag_name,
// Releases are tagged vX.Y.Z; the nightly's version is in its name ("Nightly <version>").
version: nightly ? r.name.replace(/^Nightly\s+/, "") : r.tag_name.replace(/^v/, ""),
date: r.published_at,
nightly,
url: r.html_url,
body: r.body,
setup: setup ? { url: setup.browser_download_url, size: setup.size } : null,
sha256: /`T5DE-Setup\.exe` sha256: `([0-9a-f]{64})`/.exec(r.body)?.[1] ?? null,
imvu: /IMVU Classic \*\*([\d.]+)\*\*/.exec(r.body)?.[1] ?? null,
};
}
async function get<T>(path: string): Promise<T | null> {
try {
const res = await fetch(repo + path, { next: { revalidate } });
return res.ok ? ((await res.json()) as T) : null;
} catch {
return null;
}
}
/** The latest release (not the nightly), or null. */
export async function latestRelease(): Promise<Release | null> {
const r = await get<GiteaRelease>("/releases/latest");
return r ? parse(r) : null;
}
/** The current nightly build, or null. */
export async function nightlyRelease(): Promise<Release | null> {
const r = await get<GiteaRelease>("/releases/tags/nightly");
return r ? parse(r) : null;
}
/** Every release, newest first, without the nightly; null if Gitea can't be reached. */
export async function allReleases(): Promise<Release[] | null> {
const rs = await get<GiteaRelease[]>("/releases?limit=50&pre-release=false&draft=false");
return rs ? rs.filter((r) => r.tag_name !== "nightly" && !r.draft).map(parse) : null;
}
export function formatSize(bytes: number): string {
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
export function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", timeZone: "UTC" });
}