413 lines
11 KiB
TypeScript
413 lines
11 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useState, useCallback } from "react";
|
||
|
|
import {
|
||
|
|
Modal,
|
||
|
|
Steps,
|
||
|
|
Button,
|
||
|
|
Space,
|
||
|
|
Checkbox,
|
||
|
|
Table,
|
||
|
|
Select,
|
||
|
|
Row,
|
||
|
|
Col,
|
||
|
|
Progress,
|
||
|
|
Typography,
|
||
|
|
Empty,
|
||
|
|
} from "antd";
|
||
|
|
import { ReloadOutlined } from "@ant-design/icons";
|
||
|
|
import type { Config } from "@/types";
|
||
|
|
import type { ParsedProject } from "@/services/project";
|
||
|
|
|
||
|
|
const { Text } = Typography;
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Types
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
interface PublishVersion {
|
||
|
|
key: string;
|
||
|
|
mc_version: string;
|
||
|
|
artifact: string | null;
|
||
|
|
sources: string | null;
|
||
|
|
// version label that matches the platform's versioning
|
||
|
|
label: string;
|
||
|
|
// already exists on this platform?
|
||
|
|
existsModrinth: boolean;
|
||
|
|
existsCurseforge: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface PublishModalProps {
|
||
|
|
open: boolean;
|
||
|
|
config: Config;
|
||
|
|
project: ParsedProject;
|
||
|
|
onClose: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Component
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
export function PublishModal({
|
||
|
|
open,
|
||
|
|
config,
|
||
|
|
project,
|
||
|
|
onClose,
|
||
|
|
}: PublishModalProps) {
|
||
|
|
const [currentStep, setCurrentStep] = useState(0);
|
||
|
|
|
||
|
|
// Version selection
|
||
|
|
const [selectedVersions, setSelectedVersions] = useState<Set<string>>(
|
||
|
|
new Set(),
|
||
|
|
);
|
||
|
|
|
||
|
|
// Publish settings
|
||
|
|
const [changelog, setChangelog] = useState("");
|
||
|
|
const [versionType, setVersionType] = useState<"release" | "beta" | "alpha">(
|
||
|
|
"release",
|
||
|
|
);
|
||
|
|
|
||
|
|
// Progress
|
||
|
|
const [publishing, setPublishing] = useState(false);
|
||
|
|
const [progress, setProgress] = useState<{
|
||
|
|
modrinth: { done: number; total: number };
|
||
|
|
curseforge: { done: number; total: number };
|
||
|
|
}>({ modrinth: { done: 0, total: 0 }, curseforge: { done: 0, total: 0 } });
|
||
|
|
|
||
|
|
// Platform existing versions — loaded when modal opens
|
||
|
|
const [loadingExisting, setLoadingExisting] = useState(false);
|
||
|
|
const [existingModrinth, setExistingModrinth] = useState<string[]>([]);
|
||
|
|
const [existingCurseforge, setExistingCurseforge] = useState<string[]>([]);
|
||
|
|
|
||
|
|
const loadExisting = useCallback(async () => {
|
||
|
|
setLoadingExisting(true);
|
||
|
|
try {
|
||
|
|
// TODO: Fetch existing versions from both platforms via tRPC/Server Action
|
||
|
|
// For now, placeholder
|
||
|
|
setExistingModrinth([]);
|
||
|
|
setExistingCurseforge([]);
|
||
|
|
} catch {
|
||
|
|
// ignore
|
||
|
|
} finally {
|
||
|
|
setLoadingExisting(false);
|
||
|
|
}
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// Build publish version rows from project artifacts
|
||
|
|
const publishVersions: PublishVersion[] = project.artifacts.map((a) => ({
|
||
|
|
key: a.mc_version,
|
||
|
|
mc_version: a.mc_version,
|
||
|
|
artifact: a.artifact,
|
||
|
|
sources: a.sources,
|
||
|
|
label: "", // TODO: compute from version template
|
||
|
|
existsModrinth: existingModrinth.includes(a.mc_version),
|
||
|
|
existsCurseforge: existingCurseforge.includes(a.mc_version),
|
||
|
|
}));
|
||
|
|
|
||
|
|
// --------------- Steps handling ---------------
|
||
|
|
|
||
|
|
const handleNext = useCallback(() => {
|
||
|
|
if (currentStep === 0) {
|
||
|
|
// Init selection with all non-existing entries
|
||
|
|
const preselected = new Set<string>();
|
||
|
|
for (const v of publishVersions) {
|
||
|
|
if (!v.existsModrinth || !v.existsCurseforge) {
|
||
|
|
preselected.add(v.key);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
setSelectedVersions(preselected);
|
||
|
|
}
|
||
|
|
setCurrentStep((prev) => Math.min(prev + 1, 1));
|
||
|
|
}, [currentStep, publishVersions]);
|
||
|
|
|
||
|
|
const handlePrev = useCallback(() => {
|
||
|
|
setCurrentStep((prev) => Math.max(prev - 1, 0));
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const handleConfirmPublish = useCallback(async () => {
|
||
|
|
setPublishing(true);
|
||
|
|
// TODO: Call tRPC mutation to publish
|
||
|
|
setPublishing(false);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const handleClose = useCallback(() => {
|
||
|
|
if (publishing) return;
|
||
|
|
setCurrentStep(0);
|
||
|
|
setSelectedVersions(new Set());
|
||
|
|
setChangelog("");
|
||
|
|
setVersionType("release");
|
||
|
|
onClose();
|
||
|
|
}, [publishing, onClose]);
|
||
|
|
|
||
|
|
// --------------- Render: Step 1 — Version Selection ---------------
|
||
|
|
|
||
|
|
const renderStep1 = () => {
|
||
|
|
const modrinthColumns = [
|
||
|
|
{
|
||
|
|
title: "MC 版本",
|
||
|
|
dataIndex: "mc_version",
|
||
|
|
key: "mc_version",
|
||
|
|
width: 100,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: "构建产物",
|
||
|
|
dataIndex: "artifact",
|
||
|
|
key: "artifact",
|
||
|
|
ellipsis: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: "",
|
||
|
|
key: "select",
|
||
|
|
width: 50,
|
||
|
|
render: (_: unknown, record: PublishVersion) => (
|
||
|
|
<Checkbox
|
||
|
|
disabled={record.existsModrinth}
|
||
|
|
checked={
|
||
|
|
record.existsModrinth || selectedVersions.has(record.key)
|
||
|
|
}
|
||
|
|
onChange={(e) => {
|
||
|
|
const next = new Set(selectedVersions);
|
||
|
|
if (e.target.checked) next.add(record.key);
|
||
|
|
else next.delete(record.key);
|
||
|
|
setSelectedVersions(next);
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
),
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
const curseforgeColumns = [
|
||
|
|
{
|
||
|
|
title: "MC 版本",
|
||
|
|
dataIndex: "mc_version",
|
||
|
|
key: "mc_version",
|
||
|
|
width: 100,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: "构建产物",
|
||
|
|
dataIndex: "artifact",
|
||
|
|
key: "artifact",
|
||
|
|
ellipsis: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: "",
|
||
|
|
key: "select",
|
||
|
|
width: 50,
|
||
|
|
render: (_: unknown, record: PublishVersion) => (
|
||
|
|
<Checkbox
|
||
|
|
disabled={record.existsCurseforge}
|
||
|
|
checked={
|
||
|
|
record.existsCurseforge || selectedVersions.has(record.key)
|
||
|
|
}
|
||
|
|
onChange={(e) => {
|
||
|
|
const next = new Set(selectedVersions);
|
||
|
|
if (e.target.checked) next.add(record.key);
|
||
|
|
else next.delete(record.key);
|
||
|
|
setSelectedVersions(next);
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
),
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
display: "flex",
|
||
|
|
justifyContent: "space-between",
|
||
|
|
alignItems: "center",
|
||
|
|
marginBottom: 16,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<Text strong>选择要发布的版本</Text>
|
||
|
|
<Button
|
||
|
|
icon={<ReloadOutlined />}
|
||
|
|
loading={loadingExisting}
|
||
|
|
onClick={loadExisting}
|
||
|
|
size="small"
|
||
|
|
>
|
||
|
|
刷新已有版本
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{loadingExisting ? (
|
||
|
|
<div style={{ textAlign: "center", padding: 40 }}>加载中…</div>
|
||
|
|
) : (
|
||
|
|
<Row gutter={16}>
|
||
|
|
<Col span={12}>
|
||
|
|
<Text strong style={{ display: "block", marginBottom: 8 }}>
|
||
|
|
Modrinth
|
||
|
|
</Text>
|
||
|
|
{publishVersions.length > 0 ? (
|
||
|
|
<Table
|
||
|
|
dataSource={publishVersions}
|
||
|
|
columns={modrinthColumns}
|
||
|
|
pagination={false}
|
||
|
|
size="small"
|
||
|
|
bordered
|
||
|
|
/>
|
||
|
|
) : (
|
||
|
|
<Empty description="无版本" />
|
||
|
|
)}
|
||
|
|
</Col>
|
||
|
|
<Col span={12}>
|
||
|
|
<Text strong style={{ display: "block", marginBottom: 8 }}>
|
||
|
|
CurseForge
|
||
|
|
</Text>
|
||
|
|
{publishVersions.length > 0 ? (
|
||
|
|
<Table
|
||
|
|
dataSource={publishVersions}
|
||
|
|
columns={curseforgeColumns}
|
||
|
|
pagination={false}
|
||
|
|
size="small"
|
||
|
|
bordered
|
||
|
|
/>
|
||
|
|
) : (
|
||
|
|
<Empty description="无版本" />
|
||
|
|
)}
|
||
|
|
</Col>
|
||
|
|
</Row>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
// --------------- Render: Step 2 — Publish Settings ---------------
|
||
|
|
|
||
|
|
const renderStep2 = () => {
|
||
|
|
const hasModrinth = publishVersions.some(
|
||
|
|
(v) =>
|
||
|
|
!v.existsModrinth &&
|
||
|
|
(selectedVersions.has(v.key) || v.existsModrinth),
|
||
|
|
);
|
||
|
|
const hasCurseforge = publishVersions.some(
|
||
|
|
(v) =>
|
||
|
|
!v.existsCurseforge &&
|
||
|
|
(selectedVersions.has(v.key) || v.existsCurseforge),
|
||
|
|
);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
{/* Changelog */}
|
||
|
|
<div style={{ marginBottom: 24 }}>
|
||
|
|
<Text strong style={{ display: "block", marginBottom: 8 }}>
|
||
|
|
Changelog (Markdown)
|
||
|
|
</Text>
|
||
|
|
{/* TODO: Replace with @uiw/react-md-editor */}
|
||
|
|
<textarea
|
||
|
|
style={{
|
||
|
|
width: "100%",
|
||
|
|
minHeight: 160,
|
||
|
|
border: "1px solid #d9d9d9",
|
||
|
|
borderRadius: 6,
|
||
|
|
padding: 8,
|
||
|
|
fontFamily: "monospace",
|
||
|
|
fontSize: 13,
|
||
|
|
}}
|
||
|
|
placeholder="输入 Markdown 格式的更新日志…"
|
||
|
|
value={changelog}
|
||
|
|
onChange={(e) => setChangelog(e.target.value)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Version type */}
|
||
|
|
<div style={{ marginBottom: 24 }}>
|
||
|
|
<Text strong style={{ display: "block", marginBottom: 8 }}>
|
||
|
|
版本类型
|
||
|
|
</Text>
|
||
|
|
<Select
|
||
|
|
value={versionType}
|
||
|
|
onChange={(v) => setVersionType(v)}
|
||
|
|
style={{ width: 160 }}
|
||
|
|
options={[
|
||
|
|
{ label: "Release", value: "release" },
|
||
|
|
{ label: "Beta", value: "beta" },
|
||
|
|
{ label: "Alpha", value: "alpha" },
|
||
|
|
]}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Publishing progress */}
|
||
|
|
{publishing && (
|
||
|
|
<div style={{ marginBottom: 24 }}>
|
||
|
|
{hasModrinth && (
|
||
|
|
<div style={{ marginBottom: 12 }}>
|
||
|
|
<Text>Modrinth: </Text>
|
||
|
|
<Progress
|
||
|
|
percent={Math.round(
|
||
|
|
(progress.modrinth.done / Math.max(progress.modrinth.total, 1)) *
|
||
|
|
100,
|
||
|
|
)}
|
||
|
|
format={() =>
|
||
|
|
`${progress.modrinth.done} / ${progress.modrinth.total}`
|
||
|
|
}
|
||
|
|
style={{ width: "80%" }}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{hasCurseforge && (
|
||
|
|
<div>
|
||
|
|
<Text>CurseForge: </Text>
|
||
|
|
<Progress
|
||
|
|
percent={Math.round(
|
||
|
|
(progress.curseforge.done /
|
||
|
|
Math.max(progress.curseforge.total, 1)) *
|
||
|
|
100,
|
||
|
|
)}
|
||
|
|
format={() =>
|
||
|
|
`${progress.curseforge.done} / ${progress.curseforge.total}`
|
||
|
|
}
|
||
|
|
style={{ width: "80%" }}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
// --------------- Render ---------------
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Modal
|
||
|
|
title="一键发布"
|
||
|
|
open={open}
|
||
|
|
onCancel={handleClose}
|
||
|
|
width={760}
|
||
|
|
destroyOnHidden
|
||
|
|
footer={
|
||
|
|
publishing ? null : (
|
||
|
|
<Space>
|
||
|
|
{currentStep > 0 && (
|
||
|
|
<Button onClick={handlePrev}>上一步</Button>
|
||
|
|
)}
|
||
|
|
{currentStep < 1 ? (
|
||
|
|
<Button type="primary" onClick={handleNext}>
|
||
|
|
下一步
|
||
|
|
</Button>
|
||
|
|
) : (
|
||
|
|
<Button type="primary" onClick={handleConfirmPublish}>
|
||
|
|
确认发布
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
</Space>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<Steps
|
||
|
|
current={currentStep}
|
||
|
|
size="small"
|
||
|
|
style={{ marginBottom: 24 }}
|
||
|
|
items={[
|
||
|
|
{ title: "版本选择" },
|
||
|
|
{ title: "发布设置" },
|
||
|
|
]}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{currentStep === 0 ? renderStep1() : renderStep2()}
|
||
|
|
</Modal>
|
||
|
|
);
|
||
|
|
}
|