// 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: 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(path: string): Promise { 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 { const r = await get("/releases/latest"); return r ? parse(r) : null; } /** The current nightly build, or null. */ export async function nightlyRelease(): Promise { const r = await get("/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 { const rs = await get("/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" }); }