From 9d63c2028fb7e4e3bf1f5dda82a876beb05202bd Mon Sep 17 00:00:00 2001 From: CPTProgrammer <46586216+CPTProgrammer@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:26:13 +0800 Subject: [PATCH] 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. --- src/components/MainPage.tsx | 12 +++- src/components/PublishModal.tsx | 102 +++++++++++++++++++++++++------- src/components/VersionTable.tsx | 4 +- src/lib/utils/mcVersion.ts | 18 +++--- src/services/meta.ts | 4 +- 5 files changed, 105 insertions(+), 35 deletions(-) diff --git a/src/components/MainPage.tsx b/src/components/MainPage.tsx index 8a6edc4..59c71d3 100644 --- a/src/components/MainPage.tsx +++ b/src/components/MainPage.tsx @@ -29,6 +29,7 @@ export function MainPage({ initialConfigs }: MainPageProps) { ); const [projectLoading, setProjectLoading] = useState(false); const [versionRanges, setVersionRanges] = useState(null); + const [lastVersionEnd, setLastVersionEnd] = useState(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 (
{/* Publish button */} @@ -126,6 +135,7 @@ export function MainPage({ initialConfigs }: MainPageProps) { config={selectedConfig} project={parsedProject} versionRanges={versionRanges} + lastVersionEnd={lastVersionEnd} onClose={handlePublishClose} /> )} diff --git a/src/components/PublishModal.tsx b/src/components/PublishModal.tsx index 9df66b5..b13378d 100644 --- a/src/components/PublishModal.tsx +++ b/src/components/PublishModal.tsx @@ -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; // Helpers // --------------------------------------------------------------------------- +function entriesToMap( + entries: McVersionEntry[] | null, + field: "explicit" | "wildcard", +): Map { + const map = new Map(); + if (entries) { + for (const entry of entries) { + map.set(entry.version, entry[field]); + } + } + return map; +} + const RELEASE_TYPE_LABEL_MAP: Record = { 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([]); + const [curseforgeMcVersions, setCurseforgeMcVersions] = useState([]); const [existingModrinth, setExistingModrinth] = useState([]); const [existingCurseforge, setExistingCurseforge] = useState([]); 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(); - 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) => {val}, + }, { 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) => {val}, + }, { title: "显示名称", dataIndex: "artifact", @@ -292,7 +352,7 @@ export function PublishModal({
加载中…
) : ( - + Modrinth @@ -308,7 +368,7 @@ export function PublishModal({ )} - + CurseForge @@ -466,7 +526,7 @@ export function PublishModal({ title="一键发布" open={open} onCancel={handleClose} - width={760} + width={1200} destroyOnHidden footer={ publishing ? null : ( diff --git a/src/components/VersionTable.tsx b/src/components/VersionTable.tsx index 96449ef..08e9ccd 100644 --- a/src/components/VersionTable.tsx +++ b/src/components/VersionTable.tsx @@ -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 diff --git a/src/lib/utils/mcVersion.ts b/src/lib/utils/mcVersion.ts index e938c3d..dac011a 100644 --- a/src/lib/utils/mcVersion.ts +++ b/src/lib/utils/mcVersion.ts @@ -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 }; diff --git a/src/services/meta.ts b/src/services/meta.ts index de7b10b..276c75d 100644 --- a/src/services/meta.ts +++ b/src/services/meta.ts @@ -207,7 +207,5 @@ export async function getMcVersionUnion(): Promise { 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)); }