Files
goodbuddy/sites/app.js
T
mesalogo d6fea0c6c2
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
fix: simplify LoongArch download copy
The Linux download card now presents only the preview version, DEB format, size, and checksum. Preview index validation and fail-closed download behavior are unchanged.

Release note: 官网龙芯下载信息现仅展示版本、包格式、大小与校验值。
2026-08-27 07:45:03 +08:00

579 lines
18 KiB
JavaScript

(() => {
"use strict";
const root = document.documentElement;
const header = document.querySelector("[data-site-header]");
const menuToggle = document.querySelector("[data-menu-toggle]");
const navigation = document.querySelector("[data-navigation]");
const menuBackdrop = document.querySelector("[data-menu-backdrop]");
const themeToggle = document.querySelector("[data-theme-toggle]");
const themeColor = document.querySelector('meta[name="theme-color"]');
const tiltStage = document.querySelector("[data-tilt-stage]");
const tiltCard = tiltStage?.querySelector("[data-tilt-card]");
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
const finePointer = window.matchMedia("(hover: hover) and (pointer: fine)");
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
const mobileMenu = window.matchMedia("(max-width: 719px)");
const isEnglish = root.lang.toLowerCase().startsWith("en");
const releaseManifestUrl =
"https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/latest.json";
const loongArchPreviewManifestUrl =
"https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/loongarch-preview/latest.json";
const releaseFallbackUrl =
"https://github.com/mesalogo/goodbuddy/releases/latest";
const releaseRequestTimeoutMs = 10_000;
const releaseIndexApi = window.GoodBuddyReleaseIndex;
const downloadCards = [
...document.querySelectorAll("[data-download-card]"),
];
let verifiedRelease;
let verifiedLoongArchPreview;
const interfaceCopy = isEnglish
? {
themeDark: "Switch to dark theme",
themeLight: "Switch to light theme",
menuOpen: "Open navigation",
menuClose: "Close navigation",
}
: {
themeDark: "切换为深色主题",
themeLight: "切换为浅色主题",
menuOpen: "打开导航",
menuClose: "关闭导航",
};
const platformNames = {
windows: "Windows",
macos: "macOS",
linux: "Linux",
};
const formatNames = {
nsis: "安装版",
portable: "便携版",
dmg: "DMG",
zip: "ZIP",
AppImage: "AppImage",
deb: "DEB",
rpm: "RPM",
};
const formatFileSize = (bytes) => {
const megabytes = bytes / (1024 * 1024);
return `${megabytes >= 100 ? megabytes.toFixed(0) : megabytes.toFixed(1)} MB`;
};
const listenMediaQuery = (query, listener) => {
if (typeof query.addEventListener === "function") {
query.addEventListener("change", listener);
} else if (typeof query.addListener === "function") {
query.addListener(listener);
}
};
const readBoundedJson = async (response, maximumBytes) => {
if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1) {
throw new Error("发布索引大小上限无效");
}
const declaredLength = response.headers.get("content-length");
if (declaredLength !== null) {
const parsedLength = Number(declaredLength);
if (
!Number.isSafeInteger(parsedLength) ||
parsedLength < 0 ||
parsedLength > maximumBytes
) {
throw new Error("发布索引响应大小无效");
}
}
if (!response.body) {
throw new Error("发布索引响应没有正文");
}
const reader = response.body.getReader();
const chunks = [];
let length = 0;
while (true) {
const result = await reader.read();
if (result.done) {
break;
}
length += result.value.byteLength;
if (length > maximumBytes) {
await reader.cancel();
throw new Error("发布索引响应过大");
}
chunks.push(result.value);
}
const bytes = new Uint8Array(length);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return JSON.parse(new TextDecoder().decode(bytes));
};
const setLinkText = (link, visibleText) => {
const newWindowNotice = link.querySelector(".sr-only");
link.replaceChildren(document.createTextNode(visibleText));
if (newWindowNotice) {
link.append(newWindowNotice);
}
};
const setReleaseLink = (link, url, visibleText) => {
link.href = url;
link.target = "_blank";
link.rel = "noreferrer";
link.removeAttribute("aria-disabled");
link.removeAttribute("tabindex");
link.classList.remove("is-disabled");
setLinkText(link, visibleText);
};
const getDownloadCardControls = (card) => {
const platform = card.dataset.downloadCard;
const archSelect = card.querySelector("[data-download-arch]");
const formatSelect = card.querySelector("[data-download-format]");
const container = card.closest(".download-card");
const link = container?.querySelector("[data-release-link]");
const meta = container?.querySelector("[data-download-meta]");
if (
!platform ||
!(archSelect instanceof HTMLSelectElement) ||
!(formatSelect instanceof HTMLSelectElement) ||
!(link instanceof HTMLAnchorElement) ||
!(meta instanceof HTMLElement)
) {
throw new Error("下载卡片结构无效");
}
return { platform, archSelect, formatSelect, link, meta };
};
const setDownloadUnavailable = (link, meta) => {
link.removeAttribute("href");
link.removeAttribute("target");
link.removeAttribute("rel");
link.setAttribute("aria-disabled", "true");
link.tabIndex = -1;
link.classList.add("is-disabled");
setLinkText(link, "龙芯预览包暂不可用");
meta.textContent = "实验预览 · 仅 DEB · 索引尚未发布或校验失败。";
};
const setLoongArchFormat = (formatSelect, selected) => {
for (const option of formatSelect.options) {
const unavailable = selected && option.value !== "deb";
option.hidden = unavailable;
option.disabled = unavailable;
}
if (selected) {
formatSelect.value = "deb";
}
formatSelect.disabled = selected;
};
const updateDownloadCard = (card) => {
const {
platform,
archSelect,
formatSelect,
link,
meta,
} = getDownloadCardControls(card);
const loongArchSelected =
platform === "linux" && archSelect.value === "loong64";
setLoongArchFormat(formatSelect, loongArchSelected);
if (loongArchSelected) {
if (!verifiedLoongArchPreview) {
setDownloadUnavailable(link, meta);
return;
}
setReleaseLink(
link,
verifiedLoongArchPreview.artifact.url,
"下载 Linux 龙芯 LoongArch DEB 预览版 →",
);
meta.textContent =
`GoodBuddy ${verifiedLoongArchPreview.goodBuddyVersion} · ` +
`${formatFileSize(verifiedLoongArchPreview.artifact.size)} · ` +
`实验预览 · SHA-256 ` +
`${verifiedLoongArchPreview.artifact.sha256.slice(0, 12)}…`;
return;
}
if (!verifiedRelease) {
setReleaseLink(
link,
releaseFallbackUrl,
`前往 GitHub 下载 ${platformNames[platform] ?? platform} →`,
);
meta.textContent = "请在 GitHub Release 中选择对应的安装文件。";
return;
}
const target =
verifiedRelease.targets[`${platform}-${archSelect.value}`];
const file = target?.files?.[formatSelect.value];
if (!file) {
throw new Error("下载选项不在已校验的发布索引中");
}
const platformName = platformNames[platform] ?? platform;
const archName =
platform === "macos" && archSelect.value === "arm64"
? "Apple 芯片"
: archSelect.value === "arm64"
? "ARM64"
: "x64";
const formatName = formatNames[formatSelect.value] ?? formatSelect.value;
setReleaseLink(
link,
file.url,
`下载 ${platformName} ${archName} ${formatName} →`,
);
meta.textContent =
`GoodBuddy ${verifiedRelease.version} · ` +
`${formatFileSize(file.size)} · ` +
`${archSelect.options[archSelect.selectedIndex]?.text ?? archSelect.value}`;
};
const updateAllDownloadCards = () => {
for (const card of downloadCards) {
updateDownloadCard(card);
}
};
const setFallbackDownloads = () => {
verifiedRelease = undefined;
updateAllDownloadCards();
};
const setLoongArchPreviewUnavailable = () => {
verifiedLoongArchPreview = undefined;
const linuxCard = downloadCards.find(
(card) => card.dataset.downloadCard === "linux",
);
if (linuxCard) {
updateDownloadCard(linuxCard);
}
};
const configureLoongArchPreview = (payload) => {
if (!releaseIndexApi?.validateLoongArchPreviewIndex) {
throw new Error("龙芯预览索引校验器不可用");
}
verifiedLoongArchPreview =
releaseIndexApi.validateLoongArchPreviewIndex(payload);
const linuxCard = downloadCards.find(
(card) => card.dataset.downloadCard === "linux",
);
if (linuxCard) {
updateDownloadCard(linuxCard);
}
};
const configureDownloads = (payload) => {
if (!releaseIndexApi?.validateReleaseIndex) {
throw new Error("发布索引校验器不可用");
}
verifiedRelease = releaseIndexApi.validateReleaseIndex(payload);
updateAllDownloadCards();
};
const initializeDownloadCards = () => {
for (const card of downloadCards) {
for (const select of card.querySelectorAll("select")) {
select.addEventListener("change", () => {
try {
updateDownloadCard(card);
} catch {
setFallbackDownloads();
}
});
}
updateDownloadCard(card);
}
};
const fetchBoundedIndex = async (
url,
maximumBytes,
errorLabel,
) => {
const controller = new AbortController();
const timeout = window.setTimeout(
() => controller.abort(),
releaseRequestTimeoutMs,
);
try {
const response = await fetch(url, {
cache: "no-store",
credentials: "omit",
redirect: "error",
referrerPolicy: "no-referrer",
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`${errorLabel}请求失败:${response.status}`);
}
return await readBoundedJson(response, maximumBytes);
} finally {
window.clearTimeout(timeout);
}
};
const loadRelease = async () => {
try {
configureDownloads(
await fetchBoundedIndex(
releaseManifestUrl,
releaseIndexApi?.maximumIndexBytes,
"发布索引",
),
);
} catch {
setFallbackDownloads();
}
};
const loadLoongArchPreview = async () => {
try {
configureLoongArchPreview(
await fetchBoundedIndex(
loongArchPreviewManifestUrl,
releaseIndexApi?.maximumPreviewIndexBytes,
"龙芯预览索引",
),
);
} catch {
setLoongArchPreviewUnavailable();
}
};
const getSavedTheme = () => {
try {
const savedTheme = localStorage.getItem("goodbuddy-site-theme");
return savedTheme === "light" || savedTheme === "dark" ? savedTheme : null;
} catch {
return null;
}
};
const applyTheme = (theme, persist = false) => {
root.dataset.theme = theme;
themeToggle?.setAttribute(
"aria-label",
theme === "dark" ? interfaceCopy.themeLight : interfaceCopy.themeDark,
);
themeColor?.setAttribute("content", theme === "dark" ? "#07101f" : "#f6f8fb");
if (persist) {
try {
localStorage.setItem("goodbuddy-site-theme", theme);
} catch {
// The selected theme still applies for the current page.
}
}
};
let isolatedMenuContent = null;
const isolateMenuContent = () => {
if (isolatedMenuContent) {
return;
}
isolatedMenuContent = new Map();
for (const element of document.body.children) {
if (
element === header ||
element === menuBackdrop ||
element instanceof HTMLScriptElement
) {
continue;
}
isolatedMenuContent.set(element, element.inert);
element.inert = true;
}
};
const restoreMenuContent = () => {
if (!isolatedMenuContent) {
return;
}
for (const [element, wasInert] of isolatedMenuContent) {
element.inert = wasInert;
}
isolatedMenuContent = null;
};
const closeMenu = ({ restoreFocus = true } = {}) => {
const wasOpen = header?.classList.contains("is-menu-open") ?? false;
header?.classList.remove("is-menu-open");
menuToggle?.setAttribute("aria-expanded", "false");
menuToggle?.setAttribute("aria-label", interfaceCopy.menuOpen);
menuBackdrop?.classList.remove("is-active");
restoreMenuContent();
if (wasOpen && restoreFocus) {
menuToggle?.focus();
}
};
const openMenu = () => {
if (!mobileMenu.matches) {
closeMenu({ restoreFocus: false });
return;
}
header?.classList.add("is-menu-open");
menuToggle?.setAttribute("aria-expanded", "true");
menuToggle?.setAttribute("aria-label", interfaceCopy.menuClose);
menuBackdrop?.classList.add("is-active");
isolateMenuContent();
navigation?.querySelector("a")?.focus();
};
const setHeaderState = () => {
header?.classList.toggle("is-scrolled", window.scrollY > 12);
};
applyTheme(getSavedTheme() ?? (systemTheme.matches ? "dark" : "light"));
setHeaderState();
initializeDownloadCards();
if (!isEnglish) {
void loadRelease();
void loadLoongArchPreview();
}
themeToggle?.addEventListener("click", () => {
applyTheme(root.dataset.theme === "dark" ? "light" : "dark", true);
});
menuToggle?.addEventListener("click", () => {
if (header?.classList.contains("is-menu-open")) {
closeMenu();
} else {
openMenu();
}
});
navigation?.addEventListener("click", (event) => {
if (event.target instanceof HTMLAnchorElement) {
closeMenu({ restoreFocus: false });
window.setTimeout(() => {
if (!header?.classList.contains("is-menu-open")) {
menuToggle?.focus();
}
}, 0);
}
});
menuBackdrop?.addEventListener("click", () => closeMenu());
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && header?.classList.contains("is-menu-open")) {
closeMenu();
}
});
document.addEventListener("click", (event) => {
if (
header?.classList.contains("is-menu-open") &&
event.target instanceof Node &&
!header.contains(event.target)
) {
closeMenu();
}
});
listenMediaQuery(systemTheme, (event) => {
if (!getSavedTheme()) {
applyTheme(event.matches ? "dark" : "light");
}
});
listenMediaQuery(mobileMenu, (event) => {
if (!event.matches) {
closeMenu({ restoreFocus: false });
}
});
window.addEventListener("scroll", setHeaderState, { passive: true });
if (tiltStage instanceof HTMLElement && tiltCard instanceof HTMLElement) {
let tiltFrame = 0;
const resetTilt = () => {
window.cancelAnimationFrame(tiltFrame);
tiltStage.classList.remove("is-tilting");
tiltStage.style.setProperty("--spotlight-x", "50%");
tiltStage.style.setProperty("--spotlight-y", "50%");
tiltCard.style.setProperty("--spotlight-x", "50%");
tiltCard.style.setProperty("--spotlight-y", "50%");
tiltStage.style.setProperty("--scene-tilt-x", "0deg");
tiltStage.style.setProperty("--scene-tilt-y", "0deg");
};
const updateTilt = (event) => {
if (!finePointer.matches || reducedMotion.matches) {
resetTilt();
return;
}
const bounds = tiltStage.getBoundingClientRect();
const x = Math.min(Math.max((event.clientX - bounds.left) / bounds.width, 0), 1);
const y = Math.min(Math.max((event.clientY - bounds.top) / bounds.height, 0), 1);
window.cancelAnimationFrame(tiltFrame);
tiltFrame = window.requestAnimationFrame(() => {
const spotlightX = `${(x * 100).toFixed(1)}%`;
const spotlightY = `${(y * 100).toFixed(1)}%`;
tiltStage.classList.add("is-tilting");
tiltStage.style.setProperty("--spotlight-x", spotlightX);
tiltStage.style.setProperty("--spotlight-y", spotlightY);
tiltCard.style.setProperty("--spotlight-x", spotlightX);
tiltCard.style.setProperty("--spotlight-y", spotlightY);
tiltStage.style.setProperty("--scene-tilt-x", `${((0.5 - y) * 8).toFixed(2)}deg`);
tiltStage.style.setProperty("--scene-tilt-y", `${((x - 0.5) * 11).toFixed(2)}deg`);
});
};
tiltStage.addEventListener("pointermove", updateTilt, { passive: true });
tiltStage.addEventListener("pointerleave", resetTilt);
listenMediaQuery(finePointer, resetTilt);
listenMediaQuery(reducedMotion, resetTilt);
}
const sections = [...document.querySelectorAll("main section[id]")];
const navLinks = [...document.querySelectorAll('.site-navigation a[href^="#"]')];
if ("IntersectionObserver" in window) {
const observer = new IntersectionObserver(
(entries) => {
const visibleSection = entries
.filter((entry) => entry.isIntersecting)
.sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];
if (!visibleSection) {
return;
}
navLinks.forEach((link) => {
const isCurrent = link.getAttribute("href") === `#${visibleSection.target.id}`;
if (isCurrent) {
link.setAttribute("aria-current", "true");
} else {
link.removeAttribute("aria-current");
}
});
},
{ rootMargin: "-25% 0px -55%", threshold: [0.05, 0.2, 0.5] },
);
sections.forEach((section) => observer.observe(section));
}
const currentYear = document.querySelector("[data-current-year]");
if (currentYear) {
currentYear.textContent = String(new Date().getFullYear());
}
})();