Files
orbien/docs/src/components/DownloadMatrix/index.tsx
T
2026-08-12 02:20:54 +08:00

397 lines
12 KiB
TypeScript

import {useEffect, useMemo, useState, type ReactNode} from 'react';
import clsx from 'clsx';
import styles from './styles.module.css';
const REPO = 'orbien-org/orbien';
const FALLBACK_VERSION = '2.1.0';
type OsId = 'windows' | 'linux' | 'darwin' | 'freebsd';
type ArchId = 'amd64' | 'arm64';
type LibcId = 'gnu' | 'musl';
type ProductId = 'orbien-server' | 'orbien' | 'orbien-desktop';
type AssetExt = 'tar.gz' | 'deb' | 'msi' | 'dmg';
type Row = {
os: OsId;
arch: ArchId;
libc?: LibcId;
note: string;
};
const RELEASE_BUILDS: ReadonlySet<string> = new Set([
'orbien-server|linux|amd64|gnu',
'orbien-server|linux|amd64|musl',
'orbien-server|linux|arm64|gnu',
'orbien-server|linux|arm64|musl',
'orbien-server|windows|amd64',
'orbien-server|windows|arm64',
'orbien-server|darwin|amd64',
'orbien-server|darwin|arm64',
'orbien-server|freebsd|amd64',
'orbien|linux|amd64|gnu',
'orbien|linux|amd64|musl',
'orbien|linux|arm64|gnu',
'orbien|linux|arm64|musl',
'orbien|windows|amd64',
'orbien|windows|arm64',
'orbien|darwin|amd64',
'orbien|darwin|arm64',
'orbien|freebsd|amd64',
'orbien-desktop|linux|amd64|gnu',
'orbien-desktop|linux|arm64|gnu',
'orbien-desktop|windows|amd64',
'orbien-desktop|windows|arm64',
'orbien-desktop|darwin|amd64',
'orbien-desktop|darwin|arm64',
]);
const ROWS: Row[] = [
{
os: 'linux',
arch: 'amd64',
libc: 'gnu',
note: '动态链接 glibc',
},
{
os: 'linux',
arch: 'amd64',
libc: 'musl',
note: '静态链接,兼容旧 glibc',
},
{
os: 'linux',
arch: 'arm64',
libc: 'gnu',
note: '动态链接 glibc',
},
{
os: 'linux',
arch: 'arm64',
libc: 'musl',
note: '静态链接,兼容旧 glibc',
},
{os: 'freebsd', arch: 'amd64', note: ''},
{os: 'windows', arch: 'amd64', note: ''},
{os: 'windows', arch: 'arm64', note: ''},
{
os: 'darwin',
arch: 'amd64',
note: '如遇拦截,于系统设置中允许运行',
},
{
os: 'darwin',
arch: 'arm64',
note: '如遇拦截,于系统设置中允许运行',
},
];
const OS_LABEL: Record<OsId, string> = {
windows: 'Windows',
darwin: 'macOS',
linux: 'Linux',
freebsd: 'FreeBSD',
};
const ARCH_LABEL: Record<ArchId, string> = {
amd64: 'amd64',
arm64: 'arm64',
};
const PRODUCTS = [
{
id: 'server',
name: 'orbien-server' as ProductId,
label: '服务端',
},
{
id: 'client',
name: 'orbien' as ProductId,
label: '命令行客户端',
},
{
id: 'desktop',
name: 'orbien-desktop' as ProductId,
label: '桌面客户端',
},
] as const;
function buildKey(
product: ProductId,
os: OsId,
arch: ArchId,
libc?: LibcId,
): string {
return libc ? `${product}|${os}|${arch}|${libc}` : `${product}|${os}|${arch}`;
}
function isReleaseBuild(
product: ProductId,
os: OsId,
arch: ArchId,
libc?: LibcId,
): boolean {
return RELEASE_BUILDS.has(buildKey(product, os, arch, libc));
}
function assetExt(product: ProductId, os: OsId): AssetExt {
if (product !== 'orbien-desktop') {
return 'tar.gz';
}
switch (os) {
case 'linux':
return 'deb';
case 'windows':
return 'msi';
case 'darwin':
return 'dmg';
default:
return 'tar.gz';
}
}
function assetName(
product: ProductId,
version: string,
os: OsId,
arch: ArchId,
libc?: LibcId,
): string {
const ext = assetExt(product, os);
if (product === 'orbien-desktop') {
return `${product}_${version}_${os}_${arch}.${ext}`;
}
if (libc) {
return `${product}_${version}_${os}_${arch}_${libc}.${ext}`;
}
return `${product}_${version}_${os}_${arch}.${ext}`;
}
function badgeLabel(
product: ProductId,
os: OsId,
libc?: LibcId,
): string {
const ext = assetExt(product, os);
if (product !== 'orbien-desktop' && libc) {
return `${libc} · ${ext}`;
}
return ext;
}
function assetUrl(filename: string, version: string): string {
return `https://github.com/${REPO}/releases/download/v${version}/${filename}`;
}
function detectOs(): OsId | null {
if (typeof navigator === 'undefined') {
return null;
}
const ua = navigator.userAgent.toLowerCase();
if (ua.includes('win')) {
return 'windows';
}
if (ua.includes('mac')) {
return 'darwin';
}
if (ua.includes('freebsd')) {
return 'freebsd';
}
if (ua.includes('linux')) {
return 'linux';
}
return null;
}
export default function DownloadMatrix(): ReactNode {
const [version, setVersion] = useState(FALLBACK_VERSION);
const [assetSet, setAssetSet] = useState<Set<string> | null>(null);
const [detected, setDetected] = useState<OsId | null>(null);
useEffect(() => {
setDetected(detectOs());
}, []);
useEffect(() => {
let cancelled = false;
(async () => {
try {
const res = await fetch(
`https://api.github.com/repos/${REPO}/releases/latest`,
{headers: {Accept: 'application/vnd.github+json'}},
);
if (!res.ok) {
return;
}
const data = (await res.json()) as {
tag_name?: string;
assets?: { name: string }[];
};
if (cancelled) {
return;
}
const tag = data.tag_name?.replace(/^v/, '');
if (tag) {
setVersion(tag);
}
if (data.assets) {
setAssetSet(new Set(data.assets.map((a) => a.name)));
}
} catch {
// keep fallback version / optimistic links
}
})();
return () => {
cancelled = true;
};
}, []);
const osRowSpans = useMemo(() => {
const spans = new Map<number, number>();
let i = 0;
while (i < ROWS.length) {
const os = ROWS[i].os;
let count = 1;
while (i + count < ROWS.length && ROWS[i + count].os === os) {
count += 1;
}
spans.set(i, count);
i += count;
}
return spans;
}, []);
return (
<div className={styles.wrap}>
<div className={styles.toolbar}>
<span className={styles.version}>v{version}</span>
<a
className={styles.releasesLink}
href={`https://github.com/${REPO}/releases`}
rel="noopener noreferrer">
所有版本
</a>
</div>
<div className={styles.tableScroll}>
<table className={styles.table}>
<thead>
<tr>
<th>操作系统</th>
<th>架构</th>
<th>libc</th>
{PRODUCTS.map((p) => (
<th key={p.id}>{p.label}</th>
))}
<th>说明</th>
</tr>
</thead>
<tbody>
{ROWS.map((row, idx) => {
const span = osRowSpans.get(idx);
const showOs = span !== undefined;
const rowLibc =
row.os === 'linux' ? row.libc : undefined;
return (
<tr
key={`${row.os}-${row.arch}-${row.libc ?? 'default'}`}
className={clsx(
detected === row.os && styles.rowHighlight,
)}>
{showOs ? (
<td rowSpan={span} className={styles.osCell}>
{OS_LABEL[row.os]}
</td>
) : null}
<td>
<code>{ARCH_LABEL[row.arch]}</code>
</td>
<td>
{rowLibc ? (
<code className={styles.libcTag}>
{rowLibc}
</code>
) : null}
</td>
{PRODUCTS.map((p) => {
const isDesktop = p.name === 'orbien-desktop';
const libcKey = isDesktop
? row.os === 'linux' &&
row.libc === 'gnu'
? 'gnu'
: undefined
: rowLibc;
const built = isReleaseBuild(
p.name,
row.os,
row.arch,
libcKey,
);
const label = badgeLabel(
p.name,
row.os,
isDesktop ? undefined : rowLibc,
);
const file = assetName(
p.name,
version,
row.os,
row.arch,
isDesktop ? undefined : rowLibc,
);
const published = assetSet
? assetSet.has(file)
: true;
const showLink = built && published;
return (
<td key={p.id}>
{showLink ? (
<a
className={styles.badge}
href={assetUrl(file, version)}
title={file}
rel="noopener noreferrer">
{label}
</a>
) : null}
</td>
);
})}
<td className={styles.note}>{row.note}</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div className={styles.docker}>
<h3>Docker</h3>
<ul className={styles.dockerList}>
<li className={styles.dockerItem}>
<div className={styles.dockerMeta}>
<strong>orbien-server</strong>
<span className={styles.dockerTag}>server</span>
</div>
<code className={styles.dockerCmd}>
docker pull ghcr.io/orbien-org/orbien-server:latest
</code>
</li>
<li className={styles.dockerItem}>
<div className={styles.dockerMeta}>
<strong>orbien</strong>
<span className={styles.dockerTag}>client</span>
</div>
<code className={styles.dockerCmd}>
docker pull ghcr.io/orbien-org/orbien:latest
</code>
</li>
</ul>
</div>
</div>
);
}