feat: ship portal and android release updates
This commit is contained in:
@@ -352,18 +352,17 @@ export default function Overview() {
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold text-white">下载 APK 安装包</h3>
|
||||
<p className="mt-2 max-w-xl text-sm leading-6 text-slate-300">
|
||||
当前 Android 安装包会随前端站点一起发布到 OSS,可直接从这里下载最新版本。
|
||||
当前 Android 安装包通过独立发包链路维护,可直接从这里获取最新版本。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3 text-xs text-slate-400">
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1">稳定路径</span>
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1">OSS 托管</span>
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1">后端分发</span>
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1">独立发包</span>
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1">一键下载</span>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href={APK_DOWNLOAD_PATH}
|
||||
download="yoyuzh-portal.apk"
|
||||
className="inline-flex h-11 shrink-0 items-center justify-center rounded-xl bg-[#336EFF] px-6 text-sm font-medium text-white shadow-md shadow-[#336EFF]/20 transition-colors hover:bg-[#2958cc]"
|
||||
>
|
||||
下载 APK
|
||||
|
||||
@@ -4,11 +4,13 @@ import { test } from 'node:test';
|
||||
import {
|
||||
APK_DOWNLOAD_PATH,
|
||||
APK_DOWNLOAD_PUBLIC_URL,
|
||||
formatApkPublishedAtLabel,
|
||||
getDesktopOverviewSectionColumns,
|
||||
getDesktopOverviewStretchSection,
|
||||
getMobileOverviewApkEntryMode,
|
||||
getOverviewLoadErrorMessage,
|
||||
getOverviewStorageQuotaLabel,
|
||||
isAndroidReleaseNewer,
|
||||
shouldShowOverviewApkDownload,
|
||||
} from './overview-state';
|
||||
|
||||
@@ -26,9 +28,9 @@ test('generic overview failures stay generic when not coming right after login',
|
||||
);
|
||||
});
|
||||
|
||||
test('overview exposes a stable apk download path for oss hosting', () => {
|
||||
assert.equal(APK_DOWNLOAD_PATH, '/downloads/yoyuzh-portal.apk');
|
||||
assert.equal(APK_DOWNLOAD_PUBLIC_URL, 'https://yoyuzh.xyz/downloads/yoyuzh-portal.apk');
|
||||
test('overview exposes a backend download endpoint for apk delivery', () => {
|
||||
assert.equal(APK_DOWNLOAD_PATH, 'https://api.yoyuzh.xyz/api/app/android/download');
|
||||
assert.equal(APK_DOWNLOAD_PUBLIC_URL, 'https://api.yoyuzh.xyz/api/app/android/download');
|
||||
});
|
||||
|
||||
test('overview hides the apk download entry inside the native app shell', () => {
|
||||
@@ -64,3 +66,30 @@ test('overview storage quota label uses the real quota instead of a fixed 50 GB
|
||||
assert.equal(getOverviewStorageQuotaLabel(50 * 1024 * 1024 * 1024), '已使用 / 共 50 GB');
|
||||
assert.equal(getOverviewStorageQuotaLabel(100 * 1024 * 1024 * 1024), '已使用 / 共 100 GB');
|
||||
});
|
||||
|
||||
test('apk published time is formatted into a readable update label', () => {
|
||||
assert.match(formatApkPublishedAtLabel('2026-04-03T08:33:54Z') ?? '', /04[/-]03 16:33/);
|
||||
assert.equal(formatApkPublishedAtLabel(null), null);
|
||||
});
|
||||
|
||||
test('android update check compares numeric versionCode first', () => {
|
||||
assert.equal(isAndroidReleaseNewer({
|
||||
currentVersionCode: '260931807',
|
||||
releaseVersionCode: '260931807',
|
||||
}), false);
|
||||
assert.equal(isAndroidReleaseNewer({
|
||||
currentVersionCode: '260931807',
|
||||
releaseVersionCode: '260931808',
|
||||
}), true);
|
||||
});
|
||||
|
||||
test('android update check falls back to versionName comparison', () => {
|
||||
assert.equal(isAndroidReleaseNewer({
|
||||
currentVersionName: '2026.04.03.1807',
|
||||
releaseVersionName: '2026.04.03.1807',
|
||||
}), false);
|
||||
assert.equal(isAndroidReleaseNewer({
|
||||
currentVersionName: '2026.04.03.1807',
|
||||
releaseVersionName: '2026.04.03.1810',
|
||||
}), true);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,73 @@
|
||||
import { isNativeAppShellLocation } from '@/src/lib/app-shell';
|
||||
|
||||
export const APK_DOWNLOAD_PATH = '/downloads/yoyuzh-portal.apk';
|
||||
export const APK_DOWNLOAD_PUBLIC_URL = 'https://yoyuzh.xyz/downloads/yoyuzh-portal.apk';
|
||||
export const APK_DOWNLOAD_PATH = 'https://api.yoyuzh.xyz/api/app/android/download';
|
||||
export const APK_DOWNLOAD_PUBLIC_URL = 'https://api.yoyuzh.xyz/api/app/android/download';
|
||||
|
||||
function normalizeVersionParts(value: string | null | undefined) {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return value
|
||||
.split(/[^0-9A-Za-z]+/)
|
||||
.filter(Boolean)
|
||||
.map((part) => (/^\d+$/.test(part) ? Number(part) : part.toLowerCase()));
|
||||
}
|
||||
|
||||
function compareVersionParts(left: Array<number | string>, right: Array<number | string>) {
|
||||
const length = Math.max(left.length, right.length);
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
const leftPart = left[index] ?? 0;
|
||||
const rightPart = right[index] ?? 0;
|
||||
|
||||
if (leftPart === rightPart) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof leftPart === 'number' && typeof rightPart === 'number') {
|
||||
return leftPart > rightPart ? 1 : -1;
|
||||
}
|
||||
|
||||
return String(leftPart).localeCompare(String(rightPart), 'en');
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function isAndroidReleaseNewer({
|
||||
currentVersionCode,
|
||||
currentVersionName,
|
||||
releaseVersionCode,
|
||||
releaseVersionName,
|
||||
}: {
|
||||
currentVersionCode?: string | null;
|
||||
currentVersionName?: string | null;
|
||||
releaseVersionCode?: string | null;
|
||||
releaseVersionName?: string | null;
|
||||
}) {
|
||||
if (currentVersionCode && releaseVersionCode && /^\d+$/.test(currentVersionCode) && /^\d+$/.test(releaseVersionCode)) {
|
||||
return BigInt(releaseVersionCode) > BigInt(currentVersionCode);
|
||||
}
|
||||
|
||||
if (currentVersionName && releaseVersionName) {
|
||||
return compareVersionParts(normalizeVersionParts(currentVersionName), normalizeVersionParts(releaseVersionName)) < 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function formatApkPublishedAtLabel(publishedAt: string | null) {
|
||||
if (!publishedAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(new Date(publishedAt));
|
||||
}
|
||||
|
||||
function formatOverviewStorageSize(size: number) {
|
||||
if (size <= 0) {
|
||||
|
||||
Reference in New Issue
Block a user