import { Loader2, Search } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { getFabricLoadersForVersion, getForgeVersionsForGame, installFabric, installForge, installVersion, } from "@/client"; import { DownloadProgress } from "@/components/download-monitor"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useInstanceStore } from "@/models/instance"; import { useDownloadStore } from "@/stores/download-store"; import { useGameStore } from "@/stores/game-store"; import type { FabricLoaderEntry, ForgeVersion as ForgeVersionEntry, Version, } from "@/types"; interface Props { open: boolean; onOpenChange: (open: boolean) => void; } export function InstanceCreationModal({ open, onOpenChange }: Props) { const gameStore = useGameStore(); const instancesStore = useInstanceStore(); const downloadStore = useDownloadStore(); // Steps: 1 = name, 2 = version, 3 = mod loader, 4 = installing const [step, setStep] = useState(1); // Step 1 const [instanceName, setInstanceName] = useState(""); // Step 2 const [versionSearch, setVersionSearch] = useState(""); const [versionFilter, setVersionFilter] = useState< "all" | "release" | "snapshot" >("release"); const [selectedVersionUI, setSelectedVersionUI] = useState( null, ); // Step 3 const [modLoaderType, setModLoaderType] = useState< "vanilla" | "fabric" | "forge" >("vanilla"); const [fabricLoaders, setFabricLoaders] = useState([]); const [forgeVersions, setForgeVersions] = useState([]); const [selectedFabricLoader, setSelectedFabricLoader] = useState(""); const [selectedForgeLoader, setSelectedForgeLoader] = useState(""); const [loadingLoaders, setLoadingLoaders] = useState(false); // Step 4 - installing const [installFinished, setInstallFinished] = useState(false); const loadModLoaders = useCallback(async () => { if (!selectedVersionUI) return; setLoadingLoaders(true); setFabricLoaders([]); setForgeVersions([]); try { if (modLoaderType === "fabric") { const loaders = await getFabricLoadersForVersion(selectedVersionUI.id); setFabricLoaders(loaders || []); if (loaders && loaders.length > 0) { setSelectedFabricLoader(loaders[0].loader.version); } else { setSelectedFabricLoader(""); } } else if (modLoaderType === "forge") { const versions = await getForgeVersionsForGame(selectedVersionUI.id); setForgeVersions(versions || []); if (versions && versions.length > 0) { setSelectedForgeLoader(versions[0].version); } else { setSelectedForgeLoader(""); } } } catch (e) { console.error("Failed to load mod loaders:", e); toast.error("Failed to fetch mod loader versions"); } finally { setLoadingLoaders(false); } }, [modLoaderType, selectedVersionUI]); // When entering step 3 and a base version exists, fetch loaders if needed useEffect(() => { if (step === 3 && modLoaderType !== "vanilla" && selectedVersionUI) { loadModLoaders(); } }, [step, modLoaderType, selectedVersionUI, loadModLoaders]); // Creating state const [creating, setCreating] = useState(false); const [errorMessage, setErrorMessage] = useState(""); // Derived filtered versions const filteredVersions = useMemo(() => { const all = gameStore.versions || []; let list = all.slice(); if (versionFilter !== "all") { list = list.filter((v) => v.type === versionFilter); } if (versionSearch.trim()) { const q = versionSearch.trim().toLowerCase().replace(/。/g, "."); list = list.filter((v) => v.id.toLowerCase().includes(q)); } return list; }, [gameStore.versions, versionFilter, versionSearch]); // Initialize download store event listeners when modal opens useEffect(() => { if (open) { downloadStore.init(); } return () => { // Always cleanup event listeners when effect re-runs or unmounts downloadStore.cleanup(); }; }, [open, downloadStore.init, downloadStore.cleanup]); // Reset when opened/closed useEffect(() => { if (open) { // ensure versions are loaded gameStore.loadVersions(); setStep(1); setInstanceName(""); setVersionSearch(""); setVersionFilter("release"); setSelectedVersionUI(null); setModLoaderType("vanilla"); setFabricLoaders([]); setForgeVersions([]); setSelectedFabricLoader(""); setSelectedForgeLoader(""); setErrorMessage(""); setCreating(false); setInstallFinished(false); downloadStore.reset(); } }, [open, gameStore.loadVersions, downloadStore.reset]); function validateStep1(): boolean { if (!instanceName.trim()) { setErrorMessage("Please enter an instance name"); return false; } setErrorMessage(""); return true; } function validateStep2(): boolean { if (!selectedVersionUI) { setErrorMessage("Please select a Minecraft version"); return false; } setErrorMessage(""); return true; } async function handleNext() { setErrorMessage(""); if (step === 1) { if (!validateStep1()) return; setStep(2); } else if (step === 2) { if (!validateStep2()) return; setStep(3); } } function handleBack() { setErrorMessage(""); setStep((s) => Math.max(1, s - 1)); } async function handleCreate() { if (!validateStep1() || !validateStep2()) return; setCreating(true); setErrorMessage(""); setInstallFinished(false); // Move to step 4 (installing) setStep(4); downloadStore.reset(); try { // Step 1: create instance const instance = await instancesStore.create(instanceName.trim()); // If selectedVersion provided, install it if (selectedVersionUI && instance) { // Show preparing phase while Rust resolves version JSON, fetches // asset index, and builds the download task list. downloadStore.setPhase( "preparing", `Preparing Minecraft ${selectedVersionUI.id}...`, ); try { await installVersion(instance.id, selectedVersionUI.id); } catch (err) { console.error("Failed to install base version:", err); toast.error( `Failed to install version ${selectedVersionUI.id}: ${String(err)}`, ); downloadStore.setError(`Failed to install version: ${String(err)}`); setInstallFinished(true); setCreating(false); return; } } // If mod loader selected, install it if (modLoaderType === "fabric" && selectedFabricLoader && instance) { downloadStore.setPhase( "installing-mod-loader", `Installing Fabric ${selectedFabricLoader}...`, ); try { await installFabric( instance.id, selectedVersionUI?.id ?? "", selectedFabricLoader, ); } catch (err) { console.error("Failed to install Fabric:", err); toast.error(`Failed to install Fabric: ${String(err)}`); downloadStore.setError(`Failed to install Fabric: ${String(err)}`); setInstallFinished(true); setCreating(false); return; } } else if (modLoaderType === "forge" && selectedForgeLoader && instance) { downloadStore.setPhase( "installing-mod-loader", `Installing Forge ${selectedForgeLoader}...`, ); try { await installForge( instance.id, selectedVersionUI?.id ?? "", selectedForgeLoader, ); } catch (err) { console.error("Failed to install Forge:", err); toast.error(`Failed to install Forge: ${String(err)}`); downloadStore.setError(`Failed to install Forge: ${String(err)}`); setInstallFinished(true); setCreating(false); return; } } // Refresh instances list await instancesStore.refresh(); downloadStore.setPhase("completed", "Installation complete"); setInstallFinished(true); toast.success("Instance created successfully"); } catch (e) { console.error("Failed to create instance:", e); setErrorMessage(String(e)); downloadStore.setError(String(e)); toast.error(`Failed to create instance: ${e}`); setInstallFinished(true); } finally { setCreating(false); } } // UI pieces const stepKeys = ["name", "version", "loader", "install"] as const; const StepIndicator = () => (
{stepKeys.map((key, i) => (
= i + 1 ? "bg-indigo-500" : "bg-zinc-700"}`} /> ))}
); const stepTitles: Record = { 1: "Create New Instance", 2: "Select Version", 3: "Mod Loader", 4: "Installing", }; const stepDescriptions: Record = { 1: "Give your instance a name to get started.", 2: "Choose a Minecraft version to install.", 3: "Optionally select a mod loader.", 4: "Downloading and installing game files...", }; return ( { // Prevent closing during active installation if (step === 4 && !installFinished) return; onOpenChange(newOpen); }} > {stepTitles[step]} {stepDescriptions[step]}
{/* Step 1 - Name */} {step === 1 && (
setInstanceName(e.target.value)} disabled={creating} />

Give your instance a memorable name.

)} {/* Step 2 - Version selection */} {step === 2 && (
setVersionSearch(e.target.value)} placeholder="Search versions..." className="pl-9" />
{gameStore.versions.length === 0 ? (
Loading versions...
) : filteredVersions.length === 0 ? (
No matching versions found
) : ( filteredVersions.map((v) => { const isSelected = selectedVersionUI?.id === v.id; return ( ); }) )}
)} {/* Step 3 - Mod loader */} {step === 3 && (
Mod Loader Type
{modLoaderType === "fabric" && (
{loadingLoaders ? (
Loading Fabric versions...
) : fabricLoaders.length > 0 ? (
) : (

No Fabric loaders available for this version

)}
)} {modLoaderType === "forge" && (
{loadingLoaders ? (
Loading Forge versions...
) : forgeVersions.length > 0 ? (
) : (

No Forge versions available for this version

)}
)}
)} {/* Step 4 - Installing with progress */} {step === 4 && (
{/* Summary of what's being installed */}
Instance
{instanceName}
Version
{selectedVersionUI?.id}
{modLoaderType !== "vanilla" && ( <>
Mod Loader
{modLoaderType === "fabric" ? `Fabric ${selectedFabricLoader}` : `Forge ${selectedForgeLoader}`}
)}
{/* Download progress */}
)} {errorMessage && step !== 4 && (
{errorMessage}
)}
{step === 4 ? ( ) : ( )}
{step > 1 && step < 4 && ( )} {step < 3 ? ( ) : step === 3 ? ( ) : ( installFinished && ( ) )}
); } export default InstanceCreationModal;