Site: show downloads across every release [skip ci]

/release.json adds the sum of every release asset's downloads; the
update manifests are the apps' checks, not downloads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-13 13:43:38 -04:00
1 parent 87c42fde0e
commit 854c360ea1
7 files changed
+81 -26

No files matched your search

+1 -1
View File
@@ -173,7 +173,7 @@ tests/scripts/show-fullscreen.sh 真窗口 ElyShow:F5 放映的窗口盖住
tests/scripts/types.sh packaging/types.tsv 的文件类型在 Linux 桌面项、Windows 安装程序与 macOS 文档类型里一致,从不设为默认
tests/scripts/layout.sh 真窗口:六个应用在 640、800、1100 宽下无元素越出窗口左右,留截图(需辅助功能与屏幕录制权限)
tests/scripts/ooxml.py <用例> <文件> 不经 LibreOffice 读 DOCX、XLSX、PPTX 并按自算的预期核对(往返用例调用)
cd site && pnpm deploy 官网构建并经 wrangler 发到 Cloudflare Workers(静态资源,另有 `site/worker` 只答 `/release.json`:Gitea API 的最新版本,缓存 5 分钟;下载链接指向 `releases/latest`),地址 https://ely-office.zhangyanghaha0407.workers.dev
cd site && pnpm deploy 官网构建并经 wrangler 发到 Cloudflare Workers(静态资源,另有 `site/worker` 只答 `/release.json`:Gitea API 的最新版本与全部发布的下载总数(不计更新清单 `.json`),缓存 5 分钟;下载链接指向 `releases/latest`),地址 https://ely-office.zhangyanghaha0407.workers.dev
cd site && pnpm test && pnpm e2e 官网单测(几何、道路连通)与 Playwright 端到端(悬停、聚焦、按压、命中归属)
cd site/video/scenes && npm run verify 发布视频:群像与离场两个渲染器在切场处屏幕角点一致,离场全程各屏播自己的录屏帧(真页面渲染)
cargo check --workspace
+2 -1
View File
@@ -3,12 +3,13 @@ import { test as base } from "@playwright/test";
export { expect, type Page } from "@playwright/test";
export const VERSION = "9.9.9";
export const DOWNLOADS = 12345;
// Every page reads this version.
export const test = base.extend<{ release: void }>({
release: [
async ({ context }, use) => {
await context.route("**/release.json", (r) => r.fulfill({ json: { version: VERSION } }));
await context.route("**/release.json", (r) => r.fulfill({ json: { version: VERSION, downloads: DOWNLOADS } }));
await use();
},
{ auto: true },
+10 -4
View File
@@ -1,5 +1,5 @@
import { expect, test, VERSION, type Page } from "./fixtures.ts";
import { TEXT, type Key, type Lang } from "../src/text.ts";
import { DOWNLOADS, expect, test, VERSION, type Page } from "./fixtures.ts";
import { TEXT, word, type Key, type Lang } from "../src/text.ts";
const REPO = "https://git.zacharyzhang.com/ZacharyZhang-NY/ElyOffice";
const APPS = ["ElyDoc", "ElyTable", "ElyShow", "ElyPDF", "ElyTree", "ElyMark"];
@@ -23,11 +23,12 @@ const keyed = (page: Page) =>
/** Asserts every keyed element reads in `lang`. */
async function reads(page: Page, lang: Lang) {
await expect(page.locator("html")).toHaveAttribute("lang", lang === "zh" ? "zh-CN" : "en");
await expect(page.locator("[data-t=count]")).toBeVisible();
const found = await keyed(page);
expect(found.length).toBeGreaterThan(0);
for (const [key, text] of found) {
expect(text, key).not.toBe("");
expect(text, key).toBe(TEXT[lang][key as Key]);
expect(text, key).toBe(word(lang, key as Key, key === "count" ? DOWNLOADS : undefined));
}
}
@@ -93,9 +94,13 @@ test.describe("English locale", () => {
expect(files).toEqual([]);
});
test("the download area names the newest version", async ({ page }) => {
test("the download area names the newest version and all downloads", async ({ page }) => {
await page.goto("/");
await expect(page.locator("#download h2")).toHaveText(`Ely Office ${VERSION}`);
const count = page.locator("#download [data-t=count]");
await expect(count).toHaveText("12,345 downloads so far.");
await page.locator(".lang").click();
await expect(count).toHaveText("已下载 12,345 次。");
});
test("a failed version read shows as an error", async ({ page }) => {
@@ -104,6 +109,7 @@ test.describe("English locale", () => {
await expect.poll(() => errors.length).toBe(1);
expect(errors[0]).toContain("Gitea answered 500");
await expect(page.locator("#download h2")).toHaveText("Ely Office");
await expect(page.locator("#download [data-t=count]")).toBeHidden();
errors = [];
});
+10 -3
View File
@@ -7,7 +7,7 @@ import { night } from "./night.ts";
import { rhythm } from "./rhythm.ts";
import { playground } from "./play.ts";
import { film } from "./film.ts";
import { said, speak, type Lang } from "./text.ts";
import { said, speak, word, type Lang } from "./text.ts";
import { RELEASES, latest } from "./release.ts";
const REPO = "https://git.zacharyzhang.com/ZacharyZhang-NY/ElyOffice";
@@ -36,10 +36,11 @@ const footer = document.createElement("footer");
footer.id = "download";
const version = document.createElement("h2");
version.textContent = "Ely Office";
void latest().then((v) => (version.textContent = `Ely Office ${v}`));
const count = said("p", "count");
count.hidden = true;
const get = said("a", "get");
get.href = RELEASES;
footer.append(version, said("p", "platforms"), said("p", "needs"), said("p", "unsigned"), get);
footer.append(version, count, said("p", "platforms"), said("p", "needs"), said("p", "unsigned"), get);
const toggle = said("button", "other");
toggle.className = "lang";
@@ -57,3 +58,9 @@ main.append(section, playground(), shelf, film());
app.append(night(), toggle, main, footer);
say(lang);
reveal([shelf]);
void latest().then(({ version: v, downloads }) => {
version.textContent = `Ely Office ${v}`;
count.dataset.n = String(downloads);
count.textContent = word(lang, "count", downloads);
count.hidden = false;
});
+5 -4
View File
@@ -1,10 +1,11 @@
// Gitea redirects this to the newest release.
export const RELEASES = "https://git.zacharyzhang.com/ZacharyZhang-NY/ElyOffice/releases/latest";
/** The newest release's version, from the worker. */
export async function latest(): Promise<string> {
export type Release = { version: string; downloads: number };
/** The newest release and all downloads, from the worker. */
export async function latest(): Promise<Release> {
const res = await fetch("/release.json");
if (!res.ok) throw new Error(`release.json: ${res.status} ${await res.text()}`);
const { version } = (await res.json()) as { version: string };
return version;
return (await res.json()) as Release;
}
+10 -1
View File
@@ -17,6 +17,7 @@ const en = {
needs: "macOS 14 or later, Apple silicon.",
unsigned: "Not notarized. If macOS blocks the app, open System Settings › Privacy & Security and click Open Anyway.",
get: "Download",
count: "{n} downloads so far.",
other: "中文",
roads: "Drag a file down the road.",
fileDoc: "ElyDoc file",
@@ -53,6 +54,7 @@ const zh: typeof en = {
needs: "macOS 14 或更新,Apple 芯片。",
unsigned: "未经公证。macOS 拦下应用时,打开“系统设置 › 隐私与安全性”,点“仍要打开”。",
get: "前往下载",
count: "已下载 {n} 次。",
other: "English",
roads: "把文件沿着路拖过去。",
fileDoc: "ElyDoc 文件",
@@ -83,10 +85,17 @@ export function said<K extends keyof HTMLElementTagNameMap>(tag: K, key: Key): H
return el;
}
/** `key` in `lang`, with `n` written for {n}. */
export function word(lang: Lang, key: Key, n?: number): string {
const text = TEXT[lang][key];
return n === undefined ? text : text.replace("{n}", n.toLocaleString(lang === "zh" ? "zh-CN" : "en"));
}
/** Writes every keyed text in `lang`. */
export function speak(lang: Lang): void {
document.documentElement.lang = lang === "zh" ? "zh-CN" : "en";
for (const el of document.querySelectorAll<HTMLElement>("[data-t]")) {
el.textContent = TEXT[lang][el.dataset.t as Key];
const { n } = el.dataset;
el.textContent = word(lang, el.dataset.t as Key, n === undefined ? undefined : Number(n));
}
}
+43 -12
View File
@@ -1,20 +1,51 @@
// Answers /release.json with the newest release.
const API = "https://git.zacharyzhang.com/api/v1/repos/ZacharyZhang-NY/ElyOffice/releases/latest";
// Answers /release.json with the newest release and downloads.
const REPO = "https://git.zacharyzhang.com/api/v1/repos/ZacharyZhang-NY/ElyOffice";
const PAGE = 50;
type Cached = RequestInit & { cf: { cacheTtl: number; cacheEverything: boolean } };
type Asset = { name: string; download_count: number };
type Release = { tag_name: string; assets: Asset[] };
const init: Cached = {
headers: { accept: "application/json", "user-agent": "ely-office-site" },
cf: { cacheTtl: 300, cacheEverything: true },
};
async function read<T>(path: string): Promise<T> {
const res = await fetch(`${REPO}${path}`, init);
if (!res.ok) throw new Error(`Gitea answered ${res.status} for ${path}`);
return (await res.json()) as T;
}
/** The newest release's version, without the v. */
async function version(): Promise<string> {
const { tag_name: tag } = await read<Release>("/releases/latest");
if (!/^v\d+\.\d+\.\d+$/.test(tag)) throw new Error(`Gitea's latest tag is ${JSON.stringify(tag)}`);
return tag.slice(1);
}
/** Downloads of every release; update manifests are checks, not downloads. */
async function downloads(): Promise<number> {
let total = 0;
for (let page = 1; ; page++) {
const releases = await read<Release[]>(`/releases?limit=${PAGE}&page=${page}`);
for (const { assets } of releases) {
for (const a of assets) {
if (!Number.isInteger(a.download_count)) throw new Error(`${a.name} has no download count`);
if (!a.name.endsWith(".json")) total += a.download_count;
}
}
if (releases.length < PAGE) return total;
}
}
export default {
async fetch(): Promise<Response> {
const init: Cached = {
headers: { accept: "application/json", "user-agent": "ely-office-site" },
cf: { cacheTtl: 300, cacheEverything: true },
};
const res = await fetch(API, init);
if (!res.ok) return new Response(`Gitea answered ${res.status}`, { status: 502 });
const { tag_name: tag } = (await res.json()) as { tag_name: unknown };
if (typeof tag !== "string" || !/^v\d+\.\d+\.\d+$/.test(tag)) {
return new Response(`Gitea's latest tag is ${JSON.stringify(tag)}`, { status: 502 });
try {
const [v, n] = await Promise.all([version(), downloads()]);
return Response.json({ version: v, downloads: n }, { headers: { "cache-control": "public, max-age=300" } });
} catch (e) {
return new Response(String(e), { status: 502 });
}
return Response.json({ version: tag.slice(1) }, { headers: { "cache-control": "public, max-age=300" } });
},
};