mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-29 16:06:36 +08:00
219 lines
5.1 KiB
TypeScript
219 lines
5.1 KiB
TypeScript
export type UpdateReleaseChannel = "stable" | "dev";
|
|
|
|
export const UPDATE_CHANNEL_STORAGE_KEY = "update-release-channel";
|
|
export const UPDATE_CHANNEL_CHANGED_EVENT = "updateReleaseChannelChanged";
|
|
|
|
const CHANNEL_STABLE: UpdateReleaseChannel = "stable";
|
|
const CHANNEL_DEV: UpdateReleaseChannel = "dev";
|
|
|
|
const stableVersionPattern = /^\d+(?:\.\d+)+$/;
|
|
const testKeywordPattern = /(alpha|beta|rc)/i;
|
|
|
|
const VERSION_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
|
|
type ReleaseItem = {
|
|
tag_name?: string;
|
|
draft?: boolean;
|
|
};
|
|
|
|
type LatestVersionCacheEntry = {
|
|
value: string | null;
|
|
expiresAt: number;
|
|
};
|
|
|
|
const latestVersionCache: Record<
|
|
UpdateReleaseChannel,
|
|
LatestVersionCacheEntry
|
|
> = {
|
|
stable: { value: null, expiresAt: 0 },
|
|
dev: { value: null, expiresAt: 0 },
|
|
};
|
|
|
|
const normalizeChannel = (
|
|
value: string | null | undefined,
|
|
): UpdateReleaseChannel => {
|
|
return value === CHANNEL_DEV ? CHANNEL_DEV : CHANNEL_STABLE;
|
|
};
|
|
|
|
export const getUpdateReleaseChannel = (): UpdateReleaseChannel => {
|
|
if (typeof window === "undefined") {
|
|
return CHANNEL_STABLE;
|
|
}
|
|
|
|
return normalizeChannel(localStorage.getItem(UPDATE_CHANNEL_STORAGE_KEY));
|
|
};
|
|
|
|
export const setUpdateReleaseChannel = (
|
|
channel: UpdateReleaseChannel,
|
|
): void => {
|
|
if (typeof window === "undefined") {
|
|
return;
|
|
}
|
|
|
|
localStorage.setItem(UPDATE_CHANNEL_STORAGE_KEY, normalizeChannel(channel));
|
|
window.dispatchEvent(new Event(UPDATE_CHANNEL_CHANGED_EVENT));
|
|
};
|
|
|
|
const normalizeTag = (tag: string): string => {
|
|
return tag.trim().replace(/^v/i, "");
|
|
};
|
|
|
|
type ReleaseTagChannel = UpdateReleaseChannel | null;
|
|
|
|
const releaseChannelFromTag = (tag: string): ReleaseTagChannel => {
|
|
const normalizedTag = normalizeTag(tag).toLowerCase();
|
|
|
|
if (!normalizedTag) {
|
|
return null;
|
|
}
|
|
|
|
if (stableVersionPattern.test(normalizedTag)) {
|
|
return CHANNEL_STABLE;
|
|
}
|
|
|
|
if (testKeywordPattern.test(normalizedTag)) {
|
|
return CHANNEL_DEV;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
type VersionParts = {
|
|
numbers: number[];
|
|
stageRank: number;
|
|
stageNumber: number;
|
|
};
|
|
|
|
const parseVersionParts = (version: string): VersionParts => {
|
|
const normalized = normalizeTag(version).toLowerCase();
|
|
const numberMatches = normalized.match(/\d+/g) || [];
|
|
const numbers = numberMatches.map((item) => Number.parseInt(item, 10));
|
|
|
|
let stageRank = 0;
|
|
|
|
if (normalized.includes("rc")) {
|
|
stageRank = 3;
|
|
} else if (normalized.includes("beta")) {
|
|
stageRank = 2;
|
|
} else if (normalized.includes("alpha")) {
|
|
stageRank = 1;
|
|
} else if (stableVersionPattern.test(normalized)) {
|
|
stageRank = 4;
|
|
}
|
|
|
|
const stageNumberMatch = normalized.match(/(?:alpha|beta|rc)[.-]?(\d+)/);
|
|
const stageNumber = stageNumberMatch
|
|
? Number.parseInt(stageNumberMatch[1], 10)
|
|
: 0;
|
|
|
|
return {
|
|
numbers,
|
|
stageRank,
|
|
stageNumber,
|
|
};
|
|
};
|
|
|
|
export const compareVersions = (left: string, right: string): number => {
|
|
const a = parseVersionParts(left);
|
|
const b = parseVersionParts(right);
|
|
const maxLength = Math.max(a.numbers.length, b.numbers.length);
|
|
|
|
for (let i = 0; i < maxLength; i += 1) {
|
|
const aValue = a.numbers[i] || 0;
|
|
const bValue = b.numbers[i] || 0;
|
|
|
|
if (aValue !== bValue) {
|
|
return aValue - bValue;
|
|
}
|
|
}
|
|
|
|
if (a.stageRank !== b.stageRank) {
|
|
return a.stageRank - b.stageRank;
|
|
}
|
|
|
|
if (a.stageNumber !== b.stageNumber) {
|
|
return a.stageNumber - b.stageNumber;
|
|
}
|
|
|
|
return 0;
|
|
};
|
|
|
|
const repoPathFromUrl = (repoUrl: string): string | null => {
|
|
try {
|
|
const parsed = new URL(repoUrl);
|
|
const segments = parsed.pathname
|
|
.replace(/\.git$/i, "")
|
|
.split("/")
|
|
.filter(Boolean);
|
|
|
|
if (segments.length < 2) {
|
|
return null;
|
|
}
|
|
|
|
return `${segments[0]}/${segments[1]}`;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
export const getLatestVersionByChannel = async (
|
|
channel: UpdateReleaseChannel,
|
|
repoUrl: string,
|
|
): Promise<string | null> => {
|
|
const normalizedChannel = normalizeChannel(channel);
|
|
const now = Date.now();
|
|
const cached = latestVersionCache[normalizedChannel];
|
|
|
|
if (cached.value && cached.expiresAt > now) {
|
|
return cached.value;
|
|
}
|
|
|
|
const repoPath = repoPathFromUrl(repoUrl);
|
|
|
|
if (!repoPath) {
|
|
return null;
|
|
}
|
|
|
|
const response = await fetch(
|
|
`https://api.github.com/repos/${repoPath}/releases?per_page=50`,
|
|
{
|
|
headers: {
|
|
Accept: "application/vnd.github+json",
|
|
},
|
|
},
|
|
);
|
|
|
|
if (!response.ok) {
|
|
return null;
|
|
}
|
|
|
|
const releases = (await response.json()) as ReleaseItem[];
|
|
const candidateTags = releases
|
|
.filter((release) => !release.draft && typeof release.tag_name === "string")
|
|
.map((release) => (release.tag_name || "").trim())
|
|
.filter((tag) => releaseChannelFromTag(tag) === normalizedChannel);
|
|
|
|
if (candidateTags.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const latest = candidateTags.sort((a, b) => compareVersions(b, a))[0];
|
|
|
|
latestVersionCache[normalizedChannel] = {
|
|
value: latest,
|
|
expiresAt: now + VERSION_CACHE_TTL_MS,
|
|
};
|
|
|
|
return latest;
|
|
};
|
|
|
|
export const hasVersionUpdate = (
|
|
currentVersion: string,
|
|
latestVersion: string,
|
|
): boolean => {
|
|
return (
|
|
compareVersions(normalizeTag(currentVersion), normalizeTag(latestVersion)) <
|
|
0
|
|
);
|
|
};
|