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 [projectLoading, setProjectLoading] = useState(false);
|
||||||
const [versionRanges, setVersionRanges] = useState<McVersionEntry[] | null>(null);
|
const [versionRanges, setVersionRanges] = useState<McVersionEntry[] | null>(null);
|
||||||
|
const [lastVersionEnd, setLastVersionEnd] = useState<string | null>(null);
|
||||||
|
|
||||||
// ------- ConfigSelector callbacks -------
|
// ------- ConfigSelector callbacks -------
|
||||||
|
|
||||||
@ -65,6 +66,14 @@ export function MainPage({ initialConfigs }: MainPageProps) {
|
|||||||
setPublishOpen(false);
|
setPublishOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleRangesChange = useCallback(
|
||||||
|
(ranges: McVersionEntry[] | null, end: string | null) => {
|
||||||
|
setVersionRanges(ranges);
|
||||||
|
setLastVersionEnd(end);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
style={{
|
style={{
|
||||||
@ -94,7 +103,7 @@ export function MainPage({ initialConfigs }: MainPageProps) {
|
|||||||
project={parsedProject}
|
project={parsedProject}
|
||||||
loading={projectLoading}
|
loading={projectLoading}
|
||||||
onLoadingChange={setProjectLoading}
|
onLoadingChange={setProjectLoading}
|
||||||
onRangesChange={setVersionRanges}
|
onRangesChange={handleRangesChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Publish button */}
|
{/* Publish button */}
|
||||||
@ -126,6 +135,7 @@ export function MainPage({ initialConfigs }: MainPageProps) {
|
|||||||
config={selectedConfig}
|
config={selectedConfig}
|
||||||
project={parsedProject}
|
project={parsedProject}
|
||||||
versionRanges={versionRanges}
|
versionRanges={versionRanges}
|
||||||
|
lastVersionEnd={lastVersionEnd}
|
||||||
onClose={handlePublishClose}
|
onClose={handlePublishClose}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback, useMemo } from "react";
|
import { useState, useCallback, useMemo, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
Steps,
|
Steps,
|
||||||
@ -15,6 +15,7 @@ import {
|
|||||||
Progress,
|
Progress,
|
||||||
Typography,
|
Typography,
|
||||||
Empty,
|
Empty,
|
||||||
|
Tag,
|
||||||
theme,
|
theme,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { ReloadOutlined } from "@ant-design/icons";
|
import { ReloadOutlined } from "@ant-design/icons";
|
||||||
@ -23,6 +24,8 @@ import type { ParsedProject } from "@/services/project";
|
|||||||
import { platforms } from "@/services/publish.schemas";
|
import { platforms } from "@/services/publish.schemas";
|
||||||
import { Template } from "@/lib/utils/template";
|
import { Template } from "@/lib/utils/template";
|
||||||
import type { McVersionEntry } from "@/lib/utils/mcVersion";
|
import type { McVersionEntry } from "@/lib/utils/mcVersion";
|
||||||
|
import { computeVersionRanges } from "@/lib/utils/mcVersion";
|
||||||
|
import { getModrinthMcVersions, getCurseForgeMcVersions } from "@/services/meta";
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@ -35,6 +38,8 @@ interface PublishVersion {
|
|||||||
mc_version: string;
|
mc_version: string;
|
||||||
artifact: string | null;
|
artifact: string | null;
|
||||||
sources: string | null;
|
sources: string | null;
|
||||||
|
mrRange: string;
|
||||||
|
cfRange: string;
|
||||||
modrinthVersion: string;
|
modrinthVersion: string;
|
||||||
modrinthVersionName: string;
|
modrinthVersionName: string;
|
||||||
existsModrinth: boolean;
|
existsModrinth: boolean;
|
||||||
@ -45,7 +50,7 @@ interface PublishModalProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
config: Config;
|
config: Config;
|
||||||
project: ParsedProject;
|
project: ParsedProject;
|
||||||
versionRanges: McVersionEntry[] | null;
|
lastVersionEnd: string | null;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,6 +60,19 @@ type ReleaseType = Extract<BaseVersion["version_type"], UploadReleaseType>;
|
|||||||
// Helpers
|
// 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> = {
|
const RELEASE_TYPE_LABEL_MAP: Record<ReleaseType, string> = {
|
||||||
release: "Release",
|
release: "Release",
|
||||||
beta: "Beta",
|
beta: "Beta",
|
||||||
@ -70,7 +88,7 @@ export function PublishModal({
|
|||||||
open,
|
open,
|
||||||
config,
|
config,
|
||||||
project,
|
project,
|
||||||
versionRanges,
|
lastVersionEnd,
|
||||||
onClose,
|
onClose,
|
||||||
}: PublishModalProps) {
|
}: PublishModalProps) {
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
@ -101,59 +119,87 @@ export function PublishModal({
|
|||||||
failed: number;
|
failed: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
// Platform existing versions — loaded when modal opens
|
// Platform MC versions — loaded when modal opens
|
||||||
const [loadingExisting, setLoadingExisting] = useState(false);
|
const [loadingExisting, setLoadingExisting] = useState(false);
|
||||||
|
const [modrinthMcVersions, setModrinthMcVersions] = useState<string[]>([]);
|
||||||
|
const [curseforgeMcVersions, setCurseforgeMcVersions] = useState<string[]>([]);
|
||||||
const [existingModrinth, setExistingModrinth] = useState<string[]>([]);
|
const [existingModrinth, setExistingModrinth] = useState<string[]>([]);
|
||||||
const [existingCurseforge, setExistingCurseforge] = useState<string[]>([]);
|
const [existingCurseforge, setExistingCurseforge] = useState<string[]>([]);
|
||||||
|
|
||||||
const loadExisting = useCallback(async () => {
|
const loadExisting = useCallback(async () => {
|
||||||
setLoadingExisting(true);
|
setLoadingExisting(true);
|
||||||
try {
|
try {
|
||||||
|
const [mrVersions, cfVersions] = await Promise.all([
|
||||||
|
getModrinthMcVersions(),
|
||||||
|
getCurseForgeMcVersions(),
|
||||||
|
]);
|
||||||
|
setModrinthMcVersions(mrVersions);
|
||||||
|
setCurseforgeMcVersions(cfVersions);
|
||||||
// TODO: Fetch existing versions from both platforms via tRPC/Server Action
|
// TODO: Fetch existing versions from both platforms via tRPC/Server Action
|
||||||
// For now, placeholder
|
|
||||||
setExistingModrinth([]);
|
setExistingModrinth([]);
|
||||||
setExistingCurseforge([]);
|
setExistingCurseforge([]);
|
||||||
} catch {
|
} catch (e) {
|
||||||
// ignore
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingExisting(false);
|
setLoadingExisting(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Auto-load MC versions when modal opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
loadExisting();
|
||||||
|
}
|
||||||
|
}, [open, loadExisting]);
|
||||||
|
|
||||||
// Pre-compute version display values
|
// Pre-compute version display values
|
||||||
const versionTmpl = useMemo(() => new Template(config.modrinth.version), [config.modrinth.version]);
|
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 versionNameTmpl = useMemo(() => new Template(config.modrinth.version_name), [config.modrinth.version_name]);
|
||||||
|
|
||||||
const rangeMap = useMemo(() => {
|
// Compute per-platform version ranges
|
||||||
const map = new Map<string, string>();
|
const projectVersions = useMemo(
|
||||||
if (versionRanges) {
|
() => project.artifacts.map((a) => a.mc_version),
|
||||||
for (const entry of versionRanges) {
|
[project],
|
||||||
map.set(entry.version, entry.wildcard);
|
);
|
||||||
}
|
|
||||||
}
|
const mrRanges = useMemo(() => {
|
||||||
return map;
|
if (modrinthMcVersions.length === 0) return null;
|
||||||
}, [versionRanges]);
|
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
|
// Build publish version rows from project artifacts
|
||||||
const publishVersions: PublishVersion[] = useMemo(() =>
|
const publishVersions: PublishVersion[] = useMemo(() =>
|
||||||
project.artifacts.map((a) => {
|
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 = {
|
const tmplValues = {
|
||||||
version: project.version,
|
version: project.version,
|
||||||
mc_version: a.mc_version,
|
mc_version: a.mc_version,
|
||||||
mc_version_range: rangeMap.get(a.mc_version) ?? a.mc_version,
|
mc_version_range: mrRange,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
key: a.mc_version,
|
key: a.mc_version,
|
||||||
mc_version: a.mc_version,
|
mc_version: a.mc_version,
|
||||||
artifact: a.artifact,
|
artifact: a.artifact,
|
||||||
sources: a.sources,
|
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),
|
modrinthVersion: versionTmpl.format(tmplValues),
|
||||||
modrinthVersionName: versionNameTmpl.format(tmplValues),
|
modrinthVersionName: versionNameTmpl.format(tmplValues),
|
||||||
existsModrinth: existingModrinth.includes(a.mc_version),
|
existsModrinth: existingModrinth.includes(a.mc_version),
|
||||||
existsCurseforge: existingCurseforge.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 ---------------
|
// --------------- Steps handling ---------------
|
||||||
@ -207,6 +253,13 @@ export function PublishModal({
|
|||||||
dataIndex: "modrinthVersion",
|
dataIndex: "modrinthVersion",
|
||||||
key: "modrinthVersion",
|
key: "modrinthVersion",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "兼容范围",
|
||||||
|
dataIndex: "mrRange",
|
||||||
|
key: "mr_range",
|
||||||
|
width: 100,
|
||||||
|
render: (val: string) => <Tag>{val}</Tag>,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "版本名称",
|
title: "版本名称",
|
||||||
dataIndex: "modrinthVersionName",
|
dataIndex: "modrinthVersionName",
|
||||||
@ -240,6 +293,13 @@ export function PublishModal({
|
|||||||
key: "mc_version",
|
key: "mc_version",
|
||||||
width: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "兼容范围",
|
||||||
|
dataIndex: "cfRange",
|
||||||
|
key: "cf_range",
|
||||||
|
width: 100,
|
||||||
|
render: (val: string) => <Tag>{val}</Tag>,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "显示名称",
|
title: "显示名称",
|
||||||
dataIndex: "artifact",
|
dataIndex: "artifact",
|
||||||
@ -292,7 +352,7 @@ export function PublishModal({
|
|||||||
<div style={{ textAlign: "center", padding: 40 }}>加载中…</div>
|
<div style={{ textAlign: "center", padding: 40 }}>加载中…</div>
|
||||||
) : (
|
) : (
|
||||||
<Row gutter={token.margin}>
|
<Row gutter={token.margin}>
|
||||||
<Col span={12}>
|
<Col span={14}>
|
||||||
<Text strong style={{ display: "block", marginBottom: token.marginXS }}>
|
<Text strong style={{ display: "block", marginBottom: token.marginXS }}>
|
||||||
Modrinth
|
Modrinth
|
||||||
</Text>
|
</Text>
|
||||||
@ -308,7 +368,7 @@ export function PublishModal({
|
|||||||
<Empty description="无版本" />
|
<Empty description="无版本" />
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={10}>
|
||||||
<Text strong style={{ display: "block", marginBottom: token.marginXS }}>
|
<Text strong style={{ display: "block", marginBottom: token.marginXS }}>
|
||||||
CurseForge
|
CurseForge
|
||||||
</Text>
|
</Text>
|
||||||
@ -466,7 +526,7 @@ export function PublishModal({
|
|||||||
title="一键发布"
|
title="一键发布"
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={handleClose}
|
onCancel={handleClose}
|
||||||
width={760}
|
width={1200}
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
footer={
|
footer={
|
||||||
publishing ? null : (
|
publishing ? null : (
|
||||||
|
|||||||
@ -17,7 +17,7 @@ interface VersionTableProps {
|
|||||||
project: ParsedProject | null;
|
project: ParsedProject | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
onLoadingChange: (loading: boolean) => void;
|
onLoadingChange: (loading: boolean) => void;
|
||||||
onRangesChange?: (ranges: McVersionEntry[] | null) => void;
|
onRangesChange?: (ranges: McVersionEntry[] | null, lastVersionEnd: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VersionTable({
|
export function VersionTable({
|
||||||
@ -99,7 +99,7 @@ export function VersionTable({
|
|||||||
|
|
||||||
// Report ranges to parent when they change
|
// Report ranges to parent when they change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onRangesChange?.(versionRanges);
|
onRangesChange?.(versionRanges, lastVersionEnd);
|
||||||
}, [versionRanges, onRangesChange]);
|
}, [versionRanges, onRangesChange]);
|
||||||
|
|
||||||
// Reset lastVersionEnd to latest in the same series
|
// Reset lastVersionEnd to latest in the same series
|
||||||
|
|||||||
@ -13,10 +13,10 @@ export interface McVersionEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 为已排序的项目版本列表计算兼容范围。
|
* 计算各项目版本的兼容范围。
|
||||||
*
|
*
|
||||||
* @param projectVersions 项目 MC 版本列表(已排序,从小到大)
|
* @param projectVersions 项目 MC 版本列表(无需排序,内部自动升序)
|
||||||
* @param allAvailable 全平台可用 MC 版本(已排序,从小到大)
|
* @param allAvailable 全平台可用 MC 版本(无需排序,内部自动升序)
|
||||||
* @param lastVersionEnd 最后一个版本的截止版本(包含关系),不传则自动泛匹配
|
* @param lastVersionEnd 最后一个版本的截止版本(包含关系),不传则自动泛匹配
|
||||||
*/
|
*/
|
||||||
export function computeVersionRanges(
|
export function computeVersionRanges(
|
||||||
@ -24,11 +24,13 @@ export function computeVersionRanges(
|
|||||||
allAvailable: string[],
|
allAvailable: string[],
|
||||||
lastVersionEnd?: string,
|
lastVersionEnd?: string,
|
||||||
): McVersionEntry[] {
|
): McVersionEntry[] {
|
||||||
return projectVersions.map((version, i) => {
|
const sortedProjectVersions = [...projectVersions].sort((a, b) => SemVer.compare(a, b));
|
||||||
const isLast = i === projectVersions.length - 1;
|
const sortedAvailable = [...allAvailable].sort((a, b) => SemVer.compare(a, b));
|
||||||
const end = isLast ? (lastVersionEnd ?? null) : projectVersions[i + 1];
|
return sortedProjectVersions.map((version, i) => {
|
||||||
|
const isLast = i === sortedProjectVersions.length - 1;
|
||||||
|
const end = isLast ? (lastVersionEnd ?? null) : sortedProjectVersions[i + 1];
|
||||||
const inclusiveEnd = isLast && lastVersionEnd != null;
|
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);
|
: collectAutoRange(start, allAvailable);
|
||||||
|
|
||||||
const lastInRange = range[range.length - 1] ?? start;
|
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);
|
const wildcard = computeWildcard(start, range, allAvailable, explicit);
|
||||||
|
|
||||||
return { version: start, range, wildcard, explicit };
|
return { version: start, range, wildcard, explicit };
|
||||||
|
|||||||
@ -207,7 +207,5 @@ export async function getMcVersionUnion(): Promise<string[]> {
|
|||||||
getCurseForgeMcVersions(),
|
getCurseForgeMcVersions(),
|
||||||
]);
|
]);
|
||||||
const union = new Set([...modrinthVersions, ...curseforgeVersions]);
|
const union = new Set([...modrinthVersions, ...curseforgeVersions]);
|
||||||
return [...union].sort((a, b) =>
|
return [...union].sort((a, b) => SemVer.compare(a, b));
|
||||||
a.localeCompare(b, undefined, { numeric: true }),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user