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:
parent
54d6a7659a
commit
9d63c2028f
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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 : (
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user