"use client"; import { useState, useCallback, useMemo, useEffect, useRef } from "react"; import { Modal, Steps, Button, Space, Table, Select, Row, Col, Flex, Progress, Typography, Empty, Tag, theme, } from "antd"; import { ReloadOutlined } from "@ant-design/icons"; import type { BaseVersion, Config, UploadReleaseType } from "@/types"; 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"; import { ChangelogEditor } from "./ChangelogEditor"; const { Text } = Typography; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- interface PublishVersion { key: string; mc_version: string; artifact: string | null; sources: string | null; mrRange: string; cfRange: string; modrinthVersion: string; modrinthVersionName: string; existsModrinth: boolean; existsCurseforge: boolean; } interface PublishModalProps { open: boolean; config: Config; project: ParsedProject; lastVersionEnd: string | null; onClose: () => void; } 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", alpha: "Alpha" }; const RELEASE_TYPES = Object.entries(RELEASE_TYPE_LABEL_MAP).map(v => ({ value: v[0], label: v[1] })); // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export function PublishModal({ open, config, project, lastVersionEnd, onClose, }: PublishModalProps) { const { token } = theme.useToken(); const [currentStep, setCurrentStep] = useState(0); // Version selection const [selectedModrinth, setSelectedModrinth] = useState([]); const [selectedCurseforge, setSelectedCurseforge] = useState([]); // Publish settings const [changelog, setChangelog] = useState(""); const [versionType, setVersionType] = useState( "release", ); // Progress & result const [publishing, setPublishing] = useState(false); const [publishDone, setPublishDone] = useState(false); const [progress, setProgress] = useState< Record >({ modrinth: { done: 0, total: 0 }, curseforge: { done: 0, total: 0 }, }); const [publishResult, setPublishResult] = useState<{ success: number; failed: number; } | null>(null); // 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 setExistingModrinth([]); setExistingCurseforge([]); } 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]); // 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: 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, mrExplicitMap, cfExplicitMap, existingModrinth, existingCurseforge], ); // Auto-select all non-existing versions when data first loads const initRef = useRef(false); useEffect(() => { if (open) { initRef.current = false; } }, [open]); useEffect(() => { if (!loadingExisting && publishVersions.length > 0 && !initRef.current) { initRef.current = true; setSelectedModrinth( publishVersions.filter((v) => !v.existsModrinth).map((v) => v.key), ); setSelectedCurseforge( publishVersions.filter((v) => !v.existsCurseforge).map((v) => v.key), ); } }, [loadingExisting, publishVersions]); // --------------- Steps handling --------------- const handleNext = useCallback(() => { setCurrentStep((prev) => Math.min(prev + 1, 2)); }, []); const handlePrev = useCallback(() => { setCurrentStep((prev) => Math.max(prev - 1, 0)); }, []); const handleConfirmPublish = useCallback(async () => { setPublishing(true); setPublishDone(false); setPublishResult(null); setCurrentStep(2); // TODO: Call tRPC mutation to publish, update progress & result setPublishResult({ success: 0, failed: 0 }); setPublishDone(true); setPublishing(false); }, []); const handleClose = useCallback(() => { if (publishing) return; setCurrentStep(0); setSelectedModrinth([]); setSelectedCurseforge([]); setChangelog(""); setVersionType("release"); setPublishResult(null); setPublishDone(false); onClose(); }, [publishing, onClose]); // --------------- Render: Step 1 — Version Selection --------------- const renderStep1 = () => { const modrinthColumns = [ { title: "版本号", dataIndex: "modrinthVersion", key: "modrinthVersion", }, { title: "兼容范围", dataIndex: "mrRange", key: "mr_range", width: 100, render: (val: string) => {val}, }, { title: "版本名称", dataIndex: "modrinthVersionName", key: "modrinthVersionName", }, ]; const curseforgeColumns = [ { title: "MC 版本", dataIndex: "mc_version", key: "mc_version", width: 100, }, { title: "兼容范围", dataIndex: "cfRange", key: "cf_range", width: 100, render: (val: string) => {val}, }, { title: "显示名称", dataIndex: "artifact", key: "artifact", ellipsis: true, }, ]; return (
选择要发布的版本
{loadingExisting ? (
加载中…
) : ( Modrinth {publishVersions.length > 0 ? ( setSelectedModrinth(keys as string[]), getCheckboxProps: (r: PublishVersion) => ({ disabled: r.existsModrinth, }), }} pagination={false} size="small" bordered /> ) : ( )} CurseForge {publishVersions.length > 0 ? (
setSelectedCurseforge(keys as string[]), getCheckboxProps: (r: PublishVersion) => ({ disabled: r.existsCurseforge, }), }} pagination={false} size="small" bordered /> ) : ( )} )} ); }; // --------------- Render: Step 2 — Publish Settings --------------- const renderStep2 = () => { return (
{/* Changelog */}
Changelog (Markdown)
{/* Version type */}
版本类型