Add Modrinth/CurseForge MC version ranges to publish modal

Compute per-platform version ranges using each platform's available MC
version list and display them as tags in the publish modal.
This commit is contained in:
CPTProgrammer 2026-07-16 20:26:13 +08:00
parent 54d6a7659a
commit 9d63c2028f
No known key found for this signature in database
5 changed files with 105 additions and 35 deletions

View File

@ -29,6 +29,7 @@ export function MainPage({ initialConfigs }: MainPageProps) {
);
const [projectLoading, setProjectLoading] = useState(false);
const [versionRanges, setVersionRanges] = useState<McVersionEntry[] | null>(null);
const [lastVersionEnd, setLastVersionEnd] = useState<string | null>(null);
// ------- ConfigSelector callbacks -------
@ -65,6 +66,14 @@ export function MainPage({ initialConfigs }: MainPageProps) {
setPublishOpen(false);
}, []);
const handleRangesChange = useCallback(
(ranges: McVersionEntry[] | null, end: string | null) => {
setVersionRanges(ranges);
setLastVersionEnd(end);
},
[],
);
return (
<main
style={{
@ -94,7 +103,7 @@ export function MainPage({ initialConfigs }: MainPageProps) {
project={parsedProject}
loading={projectLoading}
onLoadingChange={setProjectLoading}
onRangesChange={setVersionRanges}
onRangesChange={handleRangesChange}
/>
{/* Publish button */}
@ -126,6 +135,7 @@ export function MainPage({ initialConfigs }: MainPageProps) {
config={selectedConfig}
project={parsedProject}
versionRanges={versionRanges}
lastVersionEnd={lastVersionEnd}
onClose={handlePublishClose}
/>
)}

View File

@ -1,6 +1,6 @@
"use client";
import { useState, useCallback, useMemo } from "react";
import { useState, useCallback, useMemo, useEffect } from "react";
import {
Modal,
Steps,
@ -15,6 +15,7 @@ import {
Progress,
Typography,
Empty,
Tag,
theme,
} from "antd";
import { ReloadOutlined } from "@ant-design/icons";
@ -23,6 +24,8 @@ import type { ParsedProject } from "@/services/project";
import { platforms } from "@/services/publish.schemas";
import { Template } from "@/lib/utils/template";
import type { McVersionEntry } from "@/lib/utils/mcVersion";
import { computeVersionRanges } from "@/lib/utils/mcVersion";
import { getModrinthMcVersions, getCurseForgeMcVersions } from "@/services/meta";
const { Text } = Typography;
@ -35,6 +38,8 @@ interface PublishVersion {
mc_version: string;
artifact: string | null;
sources: string | null;
mrRange: string;
cfRange: string;
modrinthVersion: string;
modrinthVersionName: string;
existsModrinth: boolean;
@ -45,7 +50,7 @@ interface PublishModalProps {
open: boolean;
config: Config;
project: ParsedProject;
versionRanges: McVersionEntry[] | null;
lastVersionEnd: string | null;
onClose: () => void;
}
@ -55,6 +60,19 @@ type ReleaseType = Extract<BaseVersion["version_type"], UploadReleaseType>;
// Helpers
// ---------------------------------------------------------------------------
function entriesToMap(
entries: McVersionEntry[] | null,
field: "explicit" | "wildcard",
): Map<string, string> {
const map = new Map<string, string>();
if (entries) {
for (const entry of entries) {
map.set(entry.version, entry[field]);
}
}
return map;
}
const RELEASE_TYPE_LABEL_MAP: Record<ReleaseType, string> = {
release: "Release",
beta: "Beta",
@ -70,7 +88,7 @@ export function PublishModal({
open,
config,
project,
versionRanges,
lastVersionEnd,
onClose,
}: PublishModalProps) {
const { token } = theme.useToken();
@ -101,59 +119,87 @@ export function PublishModal({
failed: number;
} | null>(null);
// Platform existing versions — loaded when modal opens
// Platform MC versions — loaded when modal opens
const [loadingExisting, setLoadingExisting] = useState(false);
const [modrinthMcVersions, setModrinthMcVersions] = useState<string[]>([]);
const [curseforgeMcVersions, setCurseforgeMcVersions] = useState<string[]>([]);
const [existingModrinth, setExistingModrinth] = useState<string[]>([]);
const [existingCurseforge, setExistingCurseforge] = useState<string[]>([]);
const loadExisting = useCallback(async () => {
setLoadingExisting(true);
try {
const [mrVersions, cfVersions] = await Promise.all([
getModrinthMcVersions(),
getCurseForgeMcVersions(),
]);
setModrinthMcVersions(mrVersions);
setCurseforgeMcVersions(cfVersions);
// TODO: Fetch existing versions from both platforms via tRPC/Server Action
// For now, placeholder
setExistingModrinth([]);
setExistingCurseforge([]);
} catch {
// ignore
} catch (e) {
console.error(e);
} finally {
setLoadingExisting(false);
}
}, []);
// Auto-load MC versions when modal opens
useEffect(() => {
if (open) {
loadExisting();
}
}, [open, loadExisting]);
// Pre-compute version display values
const versionTmpl = useMemo(() => new Template(config.modrinth.version), [config.modrinth.version]);
const versionNameTmpl = useMemo(() => new Template(config.modrinth.version_name), [config.modrinth.version_name]);
const rangeMap = useMemo(() => {
const map = new Map<string, string>();
if (versionRanges) {
for (const entry of versionRanges) {
map.set(entry.version, entry.wildcard);
}
}
return map;
}, [versionRanges]);
// Compute per-platform version ranges
const projectVersions = useMemo(
() => project.artifacts.map((a) => a.mc_version),
[project],
);
const mrRanges = useMemo(() => {
if (modrinthMcVersions.length === 0) return null;
return computeVersionRanges(projectVersions, modrinthMcVersions, lastVersionEnd ?? undefined);
}, [projectVersions, modrinthMcVersions, lastVersionEnd]);
const cfRanges = useMemo(() => {
if (curseforgeMcVersions.length === 0) return null;
return computeVersionRanges(projectVersions, curseforgeMcVersions, lastVersionEnd ?? undefined);
}, [projectVersions, curseforgeMcVersions, lastVersionEnd]);
// Build per-platform range maps (mc_version → explicit)
const mrExplicitMap = useMemo(() => entriesToMap(mrRanges, "explicit"), [mrRanges]);
const cfExplicitMap = useMemo(() => entriesToMap(cfRanges, "explicit"), [cfRanges]);
// Build publish version rows from project artifacts
const publishVersions: PublishVersion[] = useMemo(() =>
project.artifacts.map((a) => {
// Note: version_name template uses Modrinth explicit range for ${mc_version_range}
const mrRange = mrExplicitMap.get(a.mc_version) ?? a.mc_version;
const tmplValues = {
version: project.version,
mc_version: a.mc_version,
mc_version_range: rangeMap.get(a.mc_version) ?? a.mc_version,
mc_version_range: mrRange,
};
return {
key: a.mc_version,
mc_version: a.mc_version,
artifact: a.artifact,
sources: a.sources,
mrRange: mrExplicitMap.get(a.mc_version) ?? a.mc_version,
cfRange: cfExplicitMap.get(a.mc_version) ?? a.mc_version,
modrinthVersion: versionTmpl.format(tmplValues),
modrinthVersionName: versionNameTmpl.format(tmplValues),
existsModrinth: existingModrinth.includes(a.mc_version),
existsCurseforge: existingCurseforge.includes(a.mc_version),
};
}),
[project, versionTmpl, versionNameTmpl, rangeMap, existingModrinth, existingCurseforge],
[project, versionTmpl, versionNameTmpl, mrExplicitMap, cfExplicitMap, existingModrinth, existingCurseforge],
);
// --------------- Steps handling ---------------
@ -207,6 +253,13 @@ export function PublishModal({
dataIndex: "modrinthVersion",
key: "modrinthVersion",
},
{
title: "兼容范围",
dataIndex: "mrRange",
key: "mr_range",
width: 100,
render: (val: string) => <Tag>{val}</Tag>,
},
{
title: "版本名称",
dataIndex: "modrinthVersionName",
@ -240,6 +293,13 @@ export function PublishModal({
key: "mc_version",
width: 100,
},
{
title: "兼容范围",
dataIndex: "cfRange",
key: "cf_range",
width: 100,
render: (val: string) => <Tag>{val}</Tag>,
},
{
title: "显示名称",
dataIndex: "artifact",
@ -292,7 +352,7 @@ export function PublishModal({
<div style={{ textAlign: "center", padding: 40 }}></div>
) : (
<Row gutter={token.margin}>
<Col span={12}>
<Col span={14}>
<Text strong style={{ display: "block", marginBottom: token.marginXS }}>
Modrinth
</Text>
@ -308,7 +368,7 @@ export function PublishModal({
<Empty description="无版本" />
)}
</Col>
<Col span={12}>
<Col span={10}>
<Text strong style={{ display: "block", marginBottom: token.marginXS }}>
CurseForge
</Text>
@ -466,7 +526,7 @@ export function PublishModal({
title="一键发布"
open={open}
onCancel={handleClose}
width={760}
width={1200}
destroyOnHidden
footer={
publishing ? null : (

View File

@ -17,7 +17,7 @@ interface VersionTableProps {
project: ParsedProject | null;
loading: boolean;
onLoadingChange: (loading: boolean) => void;
onRangesChange?: (ranges: McVersionEntry[] | null) => void;
onRangesChange?: (ranges: McVersionEntry[] | null, lastVersionEnd: string | null) => void;
}
export function VersionTable({
@ -99,7 +99,7 @@ export function VersionTable({
// Report ranges to parent when they change
useEffect(() => {
onRangesChange?.(versionRanges);
onRangesChange?.(versionRanges, lastVersionEnd);
}, [versionRanges, onRangesChange]);
// Reset lastVersionEnd to latest in the same series

View File

@ -13,10 +13,10 @@ export interface McVersionEntry {
}
/**
*
*
*
* @param projectVersions MC
* @param allAvailable MC
* @param projectVersions MC
* @param allAvailable MC
* @param lastVersionEnd
*/
export function computeVersionRanges(
@ -24,11 +24,13 @@ export function computeVersionRanges(
allAvailable: string[],
lastVersionEnd?: string,
): McVersionEntry[] {
return projectVersions.map((version, i) => {
const isLast = i === projectVersions.length - 1;
const end = isLast ? (lastVersionEnd ?? null) : projectVersions[i + 1];
const sortedProjectVersions = [...projectVersions].sort((a, b) => SemVer.compare(a, b));
const sortedAvailable = [...allAvailable].sort((a, b) => SemVer.compare(a, b));
return sortedProjectVersions.map((version, i) => {
const isLast = i === sortedProjectVersions.length - 1;
const end = isLast ? (lastVersionEnd ?? null) : sortedProjectVersions[i + 1];
const inclusiveEnd = isLast && lastVersionEnd != null;
return computeEntry(version, end, allAvailable, inclusiveEnd);
return computeEntry(version, end, sortedAvailable, inclusiveEnd);
});
}
@ -45,7 +47,7 @@ function computeEntry(
: collectAutoRange(start, allAvailable);
const lastInRange = range[range.length - 1] ?? start;
const explicit = `${start}-${lastInRange}`;
const explicit = start === lastInRange ? start : `${start}-${lastInRange}`;
const wildcard = computeWildcard(start, range, allAvailable, explicit);
return { version: start, range, wildcard, explicit };

View File

@ -207,7 +207,5 @@ export async function getMcVersionUnion(): Promise<string[]> {
getCurseForgeMcVersions(),
]);
const union = new Set([...modrinthVersions, ...curseforgeVersions]);
return [...union].sort((a, b) =>
a.localeCompare(b, undefined, { numeric: true }),
);
return [...union].sort((a, b) => SemVer.compare(a, b));
}