import { invoke } from "@tauri-apps/api/core"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import { Coffee, Loader2, Search, Trash2 } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 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 { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useInstancesStore } from "../models/instances"; import { useGameStore } from "../stores/game-store"; import type { Version } from "../types/bindings/manifest"; interface InstalledModdedVersion { id: string; javaVersion?: number; } type TypeFilter = "all" | "release" | "snapshot" | "installed"; export function VersionsView() { const { versions, selectedVersion, loadVersions, setSelectedVersion } = useGameStore(); const { activeInstance } = useInstancesStore(); const [searchQuery, setSearchQuery] = useState(""); const [typeFilter, setTypeFilter] = useState("all"); const [installedModdedVersions, setInstalledModdedVersions] = useState< InstalledModdedVersion[] >([]); const [, setIsLoadingModded] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [versionToDelete, setVersionToDelete] = useState(null); const [isDeleting, setIsDeleting] = useState(false); const [selectedVersionMetadata, setSelectedVersionMetadata] = useState<{ id: string; javaVersion?: number; isInstalled: boolean; } | null>(null); const [isLoadingMetadata, setIsLoadingMetadata] = useState(false); const [showModLoaderSelector, setShowModLoaderSelector] = useState(false); const normalizedQuery = searchQuery.trim().toLowerCase().replace(/。/g, "."); // Load installed modded versions with Java version info const loadInstalledModdedVersions = useCallback(async () => { if (!activeInstance) { setInstalledModdedVersions([]); setIsLoadingModded(false); return; } setIsLoadingModded(true); try { const allInstalled = await invoke>( "list_installed_versions", { instanceId: activeInstanceId }, ); const moddedIds = allInstalled .filter((v) => v.type === "fabric" || v.type === "forge") .map((v) => v.id); const versionsWithJava = await Promise.all( moddedIds.map(async (id) => { try { const javaVersion = await invoke( "get_version_java_version", { instanceId: activeInstanceId, versionId: id, }, ); return { id, javaVersion: javaVersion ?? undefined, }; } catch (e) { console.error(`Failed to get Java version for ${id}:`, e); return { id, javaVersion: undefined }; } }), ); setInstalledModdedVersions(versionsWithJava); } catch (e) { console.error("Failed to load installed modded versions:", e); toast.error("Error loading modded versions"); } finally { setIsLoadingModded(false); } }, [activeInstanceId]); // Combined versions list (vanilla + modded) const allVersions = (() => { const moddedVersions: Version[] = installedModdedVersions.map((v) => { const versionType = v.id.startsWith("fabric-loader-") ? "fabric" : v.id.includes("-forge-") ? "forge" : "fabric"; return { id: v.id, type: versionType, url: "", time: "", releaseTime: new Date().toISOString(), javaVersion: BigInt(v.javaVersion ?? 0), isInstalled: true, }; }); return [...moddedVersions, ...versions]; })(); // Filter versions based on search and type filter const filteredVersions = allVersions.filter((version) => { if (typeFilter === "release" && version.type !== "release") return false; if (typeFilter === "snapshot" && version.type !== "snapshot") return false; if (typeFilter === "installed" && !version.isInstalled) return false; if ( normalizedQuery && !version.id.toLowerCase().includes(normalizedQuery) ) { return false; } return true; }); // Get version badge styling const getVersionBadge = (type: string) => { switch (type) { case "release": return { text: "Release", variant: "default" as const, className: "bg-emerald-500 hover:bg-emerald-600", }; case "snapshot": return { text: "Snapshot", variant: "secondary" as const, className: "bg-amber-500 hover:bg-amber-600", }; case "fabric": return { text: "Fabric", variant: "outline" as const, className: "border-indigo-500 text-indigo-700 dark:text-indigo-300", }; case "forge": return { text: "Forge", variant: "outline" as const, className: "border-orange-500 text-orange-700 dark:text-orange-300", }; case "modpack": return { text: "Modpack", variant: "outline" as const, className: "border-purple-500 text-purple-700 dark:text-purple-300", }; default: return { text: type, variant: "outline" as const, className: "border-gray-500 text-gray-700 dark:text-gray-300", }; } }; // Load version metadata const loadVersionMetadata = useCallback( async (versionId: string) => { if (!versionId || !activeInstanceId) { setSelectedVersionMetadata(null); return; } setIsLoadingMetadata(true); try { const metadata = await invoke<{ id: string; javaVersion?: number; isInstalled: boolean; }>("get_version_metadata", { instanceId: activeInstanceId, versionId, }); setSelectedVersionMetadata(metadata); } catch (e) { console.error("Failed to load version metadata:", e); setSelectedVersionMetadata(null); } finally { setIsLoadingMetadata(false); } }, [activeInstanceId], ); // Get base version for mod loader selector const selectedBaseVersion = (() => { if (!selectedVersion) return ""; if (selectedVersion.startsWith("fabric-loader-")) { const parts = selectedVersion.split("-"); return parts[parts.length - 1]; } if (selectedVersion.includes("-forge-")) { return selectedVersion.split("-forge-")[0]; } const version = versions.find((v) => v.id === selectedVersion); return version ? selectedVersion : ""; })(); // Handle version deletion const handleDeleteVersion = async () => { if (!versionToDelete || !activeInstanceId) return; setIsDeleting(true); try { await invoke("delete_version", { instanceId: activeInstanceId, versionId: versionToDelete, }); if (selectedVersion === versionToDelete) { setSelectedVersion(""); } setShowDeleteDialog(false); setVersionToDelete(null); toast.success("Version deleted successfully"); await loadVersions(activeInstanceId); await loadInstalledModdedVersions(); } catch (e) { console.error("Failed to delete version:", e); toast.error(`Failed to delete version: ${e}`); } finally { setIsDeleting(false); } }; // Show delete confirmation dialog const showDeleteConfirmation = (versionId: string, e: React.MouseEvent) => { e.stopPropagation(); setVersionToDelete(versionId); setShowDeleteDialog(true); }; // Setup event listeners for version updates useEffect(() => { let unlisteners: UnlistenFn[] = []; const setupEventListeners = async () => { try { const versionDeletedUnlisten = await listen( "version-deleted", async () => { await loadVersions(activeInstanceId ?? undefined); await loadInstalledModdedVersions(); }, ); const downloadCompleteUnlisten = await listen( "download-complete", async () => { await loadVersions(activeInstanceId ?? undefined); await loadInstalledModdedVersions(); }, ); const versionInstalledUnlisten = await listen( "version-installed", async () => { await loadVersions(activeInstanceId ?? undefined); await loadInstalledModdedVersions(); }, ); const fabricInstalledUnlisten = await listen( "fabric-installed", async () => { await loadVersions(activeInstanceId ?? undefined); await loadInstalledModdedVersions(); }, ); const forgeInstalledUnlisten = await listen( "forge-installed", async () => { await loadVersions(activeInstanceId ?? undefined); await loadInstalledModdedVersions(); }, ); unlisteners = [ versionDeletedUnlisten, downloadCompleteUnlisten, versionInstalledUnlisten, fabricInstalledUnlisten, forgeInstalledUnlisten, ]; } catch (e) { console.error("Failed to setup event listeners:", e); } }; setupEventListeners(); loadInstalledModdedVersions(); return () => { unlisteners.forEach((unlisten) => { unlisten(); }); }; }, [activeInstanceId, loadVersions, loadInstalledModdedVersions]); // Load metadata when selected version changes useEffect(() => { if (selectedVersion) { loadVersionMetadata(selectedVersion); } else { setSelectedVersionMetadata(null); } }, [selectedVersion, loadVersionMetadata]); return (

Version Manager

Select a version to play or modify
{/* Left: Version List */}
{/* Search and Filters */}
setSearchQuery(e.target.value)} />
{/* Type Filter Tabs */} setTypeFilter(v as TypeFilter)} className="w-full" > All Release Snapshot Installed {/* Version List */} {versions.length === 0 ? (
Loading versions...
) : filteredVersions.length === 0 ? (
👻 No matching versions found
) : (
{filteredVersions.map((version) => { const badge = getVersionBadge(version.type); const isSelected = selectedVersion === version.id; return ( setSelectedVersion(version.id)} > {isSelected && (
)}
{badge.text}
{version.id}
{version.releaseTime && version.type !== "fabric" && version.type !== "forge" && (
{new Date( version.releaseTime, ).toLocaleDateString()}
)} {version.javaVersion && (
Java {version.javaVersion}
)}
{version.isInstalled && ( )}
); })}
)}
{/* Right: Version Details */}
Version Details {selectedVersion ? ( <>
Selected Version
{selectedVersion}
{isLoadingMetadata ? (
Loading metadata...
) : selectedVersionMetadata ? (
Installation Status
{selectedVersionMetadata.isInstalled ? "Installed" : "Not Installed"}
{selectedVersionMetadata.javaVersion && (
Java Version
Java {selectedVersionMetadata.javaVersion}
)} {!selectedVersionMetadata.isInstalled && ( )}
) : null} ) : (
Select a version to view details
)}
{/* Mod Loader Installation */} {showModLoaderSelector && selectedBaseVersion && ( Install Mod Loader
Install {selectedBaseVersion} with Fabric or Forge
)}
{/* Delete Confirmation Dialog */} Delete Version Are you sure you want to delete version "{versionToDelete}"? This action cannot be undone.
); }