From 1119f6c3cf421da2f2db92873efae8135c76b678 Mon Sep 17 00:00:00 2001
From: HsiangNianian <i@jyunko.cn>
Date: Fri, 16 Jan 2026 18:38:47 +0800
Subject: feat: enhance Java version management for Minecraft versions

Added functionality to determine and validate the required Java version for Minecraft versions, including checks for compatibility with older versions. Implemented event emissions for version installation and deletion, and updated the UI to reflect Java version requirements and installation status. Improved version metadata handling and added support for deleting versions.
---
 ui/src/components/VersionsView.svelte | 325 ++++++++++++++++++++++++++++++----
 1 file changed, 292 insertions(+), 33 deletions(-)

(limited to 'ui/src/components/VersionsView.svelte')

diff --git a/ui/src/components/VersionsView.svelte b/ui/src/components/VersionsView.svelte
index 063c28d..2e8b028 100644
--- a/ui/src/components/VersionsView.svelte
+++ b/ui/src/components/VersionsView.svelte
@@ -1,5 +1,6 @@
 <script lang="ts">
   import { invoke } from "@tauri-apps/api/core";
+  import { listen, type UnlistenFn } from "@tauri-apps/api/event";
   import { gameState } from "../stores/game.svelte";
   import ModLoaderSelector from "./ModLoaderSelector.svelte";
 
@@ -9,40 +10,130 @@
   );
 
   // Filter by version type
-  let typeFilter = $state<"all" | "release" | "snapshot" | "modded">("all");
+  let typeFilter = $state<"all" | "release" | "snapshot" | "installed">("all");
 
-  // Installed modded versions
-  let installedFabricVersions = $state<string[]>([]);
+  // Installed modded versions with Java version info (Fabric + Forge)
+  let installedFabricVersions = $state<Array<{ id: string; javaVersion?: number }>>([]);
   let isLoadingModded = $state(false);
 
-  // Load installed modded versions
+  // Load installed modded versions with Java version info (both Fabric and Forge)
   async function loadInstalledModdedVersions() {
     isLoadingModded = true;
     try {
-      installedFabricVersions = await invoke<string[]>(
-        "list_installed_fabric_versions"
+      // Get all installed versions and filter for modded ones (Fabric and Forge)
+      const allInstalled = await invoke<Array<{ id: string; type: string }>>(
+        "list_installed_versions"
       );
+      
+      // Filter for Fabric and Forge versions
+      const moddedIds = allInstalled
+        .filter(v => v.type === "fabric" || v.type === "forge")
+        .map(v => v.id);
+      
+      // Load Java version for each installed modded version
+      const versionsWithJava = await Promise.all(
+        moddedIds.map(async (id) => {
+          try {
+            const javaVersion = await invoke<number | null>(
+              "get_version_java_version",
+              { versionId: id }
+            );
+            return {
+              id,
+              javaVersion: javaVersion ?? undefined,
+            };
+          } catch (e) {
+            console.error(`Failed to get Java version for ${id}:`, e);
+            return { id, javaVersion: undefined };
+          }
+        })
+      );
+      
+      installedFabricVersions = versionsWithJava;
     } catch (e) {
-      console.error("Failed to load installed fabric versions:", e);
+      console.error("Failed to load installed modded versions:", e);
     } finally {
       isLoadingModded = false;
     }
   }
 
-  // Load on mount
+  let versionDeletedUnlisten: UnlistenFn | null = null;
+  let downloadCompleteUnlisten: UnlistenFn | null = null;
+  let versionInstalledUnlisten: UnlistenFn | null = null;
+  let fabricInstalledUnlisten: UnlistenFn | null = null;
+  let forgeInstalledUnlisten: UnlistenFn | null = null;
+
+  // Load on mount and setup event listeners
   $effect(() => {
     loadInstalledModdedVersions();
+    setupEventListeners();
+    return () => {
+      if (versionDeletedUnlisten) {
+        versionDeletedUnlisten();
+      }
+      if (downloadCompleteUnlisten) {
+        downloadCompleteUnlisten();
+      }
+      if (versionInstalledUnlisten) {
+        versionInstalledUnlisten();
+      }
+      if (fabricInstalledUnlisten) {
+        fabricInstalledUnlisten();
+      }
+      if (forgeInstalledUnlisten) {
+        forgeInstalledUnlisten();
+      }
+    };
   });
 
+  async function setupEventListeners() {
+    // Refresh versions when a version is deleted
+    versionDeletedUnlisten = await listen("version-deleted", async () => {
+      await gameState.loadVersions();
+      await loadInstalledModdedVersions();
+    });
+    
+    // Refresh versions when a download completes (version installed)
+    downloadCompleteUnlisten = await listen("download-complete", async () => {
+      await gameState.loadVersions();
+      await loadInstalledModdedVersions();
+    });
+    
+    // Refresh when a version is installed
+    versionInstalledUnlisten = await listen("version-installed", async () => {
+      await gameState.loadVersions();
+      await loadInstalledModdedVersions();
+    });
+    
+    // Refresh when Fabric is installed
+    fabricInstalledUnlisten = await listen("fabric-installed", async () => {
+      await gameState.loadVersions();
+      await loadInstalledModdedVersions();
+    });
+    
+    // Refresh when Forge is installed
+    forgeInstalledUnlisten = await listen("forge-installed", async () => {
+      await gameState.loadVersions();
+      await loadInstalledModdedVersions();
+    });
+  }
+
   // Combined versions list (vanilla + modded)
   let allVersions = $derived(() => {
-    const moddedVersions = installedFabricVersions.map((id) => ({
-      id,
-      type: "fabric",
-      url: "",
-      time: "",
-      releaseTime: new Date().toISOString(),
-    }));
+    const moddedVersions = installedFabricVersions.map((v) => {
+      // Determine type based on version ID
+      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: v.javaVersion,
+        isInstalled: true, // Modded versions in the list are always installed
+      };
+    });
     return [...moddedVersions, ...gameState.versions];
   });
 
@@ -54,10 +145,8 @@
       versions = versions.filter((v) => v.type === "release");
     } else if (typeFilter === "snapshot") {
       versions = versions.filter((v) => v.type === "snapshot");
-    } else if (typeFilter === "modded") {
-      versions = versions.filter(
-        (v) => v.type === "fabric" || v.type === "forge"
-      );
+    } else if (typeFilter === "installed") {
+      versions = versions.filter((v) => v.isInstalled === true);
     }
 
     // Apply search filter
@@ -90,10 +179,90 @@
   function handleModLoaderInstall(versionId: string) {
     // Refresh the installed versions list
     loadInstalledModdedVersions();
+    // Refresh vanilla versions to update isInstalled status
+    gameState.loadVersions();
     // Select the newly installed version
     gameState.selectedVersion = versionId;
   }
 
+  // Delete confirmation dialog state
+  let showDeleteDialog = $state(false);
+  let versionToDelete = $state<string | null>(null);
+
+  // Show delete confirmation dialog
+  function showDeleteConfirmation(versionId: string, event: MouseEvent) {
+    event.stopPropagation(); // Prevent version selection
+    versionToDelete = versionId;
+    showDeleteDialog = true;
+  }
+
+  // Cancel delete
+  function cancelDelete() {
+    showDeleteDialog = false;
+    versionToDelete = null;
+  }
+
+  // Confirm and delete version
+  async function confirmDelete() {
+    if (!versionToDelete) return;
+
+    try {
+      await invoke("delete_version", { versionId: versionToDelete });
+      // Clear selection if deleted version was selected
+      if (gameState.selectedVersion === versionToDelete) {
+        gameState.selectedVersion = "";
+      }
+      // Close dialog
+      showDeleteDialog = false;
+      versionToDelete = null;
+      // Versions will be refreshed automatically via event listener
+    } catch (e) {
+      console.error("Failed to delete version:", e);
+      alert(`Failed to delete version: ${e}`);
+      // Keep dialog open on error so user can retry
+    }
+  }
+
+  // Version metadata for the selected version
+  interface VersionMetadata {
+    id: string;
+    javaVersion?: number;
+    isInstalled: boolean;
+  }
+
+  let selectedVersionMetadata = $state<VersionMetadata | null>(null);
+  let isLoadingMetadata = $state(false);
+
+  // Load metadata when version is selected
+  async function loadVersionMetadata(versionId: string) {
+    if (!versionId) {
+      selectedVersionMetadata = null;
+      return;
+    }
+
+    isLoadingMetadata = true;
+    try {
+      const metadata = await invoke<VersionMetadata>("get_version_metadata", {
+        versionId,
+      });
+      selectedVersionMetadata = metadata;
+    } catch (e) {
+      console.error("Failed to load version metadata:", e);
+      selectedVersionMetadata = null;
+    } finally {
+      isLoadingMetadata = false;
+    }
+  }
+
+  // Watch for selected version changes
+  $effect(() => {
+    if (gameState.selectedVersion) {
+      loadVersionMetadata(gameState.selectedVersion);
+    } else {
+      selectedVersionMetadata = null;
+    }
+  });
+
   // Get the base Minecraft version from selected version (for mod loader selector)
   let selectedBaseVersion = $derived(() => {
     const selected = gameState.selectedVersion;
@@ -140,7 +309,7 @@
 
       <!-- Type Filter Tabs (Glass Caps) -->
       <div class="flex p-1 bg-white/60 dark:bg-black/20 rounded-xl border border-black/5 dark:border-white/5">
-        {#each ['all', 'release', 'snapshot', 'modded'] as filter}
+        {#each ['all', 'release', 'snapshot', 'installed'] as filter}
             <button
             class="flex-1 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 capitalize
             {typeFilter === filter
@@ -180,29 +349,52 @@
                  <div class="absolute inset-0 bg-gradient-to-r from-indigo-500/10 to-transparent pointer-events-none"></div>
               {/if}
 
-              <div class="relative z-10 flex items-center gap-4">
+              <div class="relative z-10 flex items-center gap-4 flex-1">
                 <span
                   class="px-2.5 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wide border {badge.class}"
                 >
                   {badge.text}
                 </span>
-                <div>
+                <div class="flex-1">
                   <div class="font-bold font-mono text-lg tracking-tight {isSelected ? 'text-black dark:text-white' : 'text-gray-700 dark:text-zinc-300 group-hover:text-black dark:group-hover:text-white'}">
                     {version.id}
                   </div>
-                  {#if version.releaseTime && version.type !== "fabric" && version.type !== "forge"}
-                    <div class="text-xs dark:text-white/30 text-black/30">
-                      {new Date(version.releaseTime).toLocaleDateString()}
-                    </div>
-                  {/if}
+                  <div class="flex items-center gap-2 mt-0.5">
+                    {#if version.releaseTime && version.type !== "fabric" && version.type !== "forge"}
+                      <div class="text-xs dark:text-white/30 text-black/30">
+                        {new Date(version.releaseTime).toLocaleDateString()}
+                      </div>
+                    {/if}
+                    {#if version.javaVersion}
+                      <div class="flex items-center gap-1 text-xs dark:text-white/40 text-black/40">
+                        <span class="opacity-60">☕</span>
+                        <span class="font-medium">Java {version.javaVersion}</span>
+                      </div>
+                    {/if}
+                  </div>
                 </div>
               </div>
               
-              {#if isSelected}
-                <div class="relative z-10 text-indigo-500 dark:text-indigo-400">
-                   <span class="text-lg">Selected</span>
-                </div>
-              {/if}
+              <div class="relative z-10 flex items-center gap-2">
+                {#if version.isInstalled === true}
+                  <button
+                    onclick={(e) => showDeleteConfirmation(version.id, e)}
+                    class="p-2 rounded-lg text-red-500 dark:text-red-400 hover:bg-red-500/10 dark:hover:bg-red-500/20 transition-colors opacity-0 group-hover:opacity-100"
+                    title="Delete version"
+                  >
+                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                      <path d="M3 6h18"></path>
+                      <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
+                      <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
+                    </svg>
+                  </button>
+                {/if}
+                {#if isSelected}
+                  <div class="text-indigo-500 dark:text-indigo-400">
+                     <span class="text-lg">Selected</span>
+                  </div>
+                {/if}
+              </div>
             </button>
           {/each}
         {/if}
@@ -217,9 +409,50 @@
           
           <h3 class="text-xs font-bold uppercase tracking-widest dark:text-white/40 text-black/40 mb-2 relative z-10">Current Selection</h3>
           {#if gameState.selectedVersion}
-            <p class="font-mono text-3xl font-black text-transparent bg-clip-text bg-gradient-to-r from-gray-900 to-gray-600 dark:from-white dark:to-white/70 relative z-10 truncate">
+            <p class="font-mono text-3xl font-black text-transparent bg-clip-text bg-gradient-to-r from-gray-900 to-gray-600 dark:from-white dark:to-white/70 relative z-10 truncate mb-4">
                 {gameState.selectedVersion}
             </p>
+
+            <!-- Version Metadata -->
+            {#if isLoadingMetadata}
+              <div class="space-y-3 relative z-10">
+                <div class="animate-pulse space-y-2">
+                  <div class="h-4 bg-black/10 dark:bg-white/10 rounded w-3/4"></div>
+                  <div class="h-4 bg-black/10 dark:bg-white/10 rounded w-1/2"></div>
+                </div>
+              </div>
+            {:else if selectedVersionMetadata}
+              <div class="space-y-3 relative z-10">
+                <!-- Java Version -->
+                {#if selectedVersionMetadata.javaVersion}
+                  <div>
+                    <div class="text-[10px] font-bold uppercase tracking-wider dark:text-white/40 text-black/40 mb-1">Java Version</div>
+                    <div class="flex items-center gap-2">
+                      <span class="text-lg opacity-60">☕</span>
+                      <span class="text-sm dark:text-white text-black font-medium">
+                        Java {selectedVersionMetadata.javaVersion}
+                      </span>
+                    </div>
+                  </div>
+                {/if}
+
+                <!-- Installation Status -->
+                <div>
+                  <div class="text-[10px] font-bold uppercase tracking-wider dark:text-white/40 text-black/40 mb-1">Status</div>
+                  <div class="flex items-center gap-2">
+                    {#if selectedVersionMetadata.isInstalled === true}
+                      <span class="px-2 py-0.5 bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 text-[10px] font-bold rounded border border-emerald-500/30">
+                        Installed
+                      </span>
+                    {:else if selectedVersionMetadata.isInstalled === false}
+                      <span class="px-2 py-0.5 bg-zinc-500/20 text-zinc-600 dark:text-zinc-400 text-[10px] font-bold rounded border border-zinc-500/30">
+                        Not Installed
+                      </span>
+                    {/if}
+                  </div>
+                </div>
+              </div>
+            {/if}
           {:else}
             <p class="dark:text-white/20 text-black/20 italic relative z-10">None selected</p>
           {/if}
@@ -235,4 +468,30 @@
 
     </div>
   </div>
+
+  <!-- Delete Version Confirmation Dialog -->
+  {#if showDeleteDialog && versionToDelete}
+    <div class="fixed inset-0 z-[200] bg-black/70 dark:bg-black/80 backdrop-blur-sm flex items-center justify-center p-4">
+      <div class="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-xl shadow-2xl p-6 max-w-sm w-full animate-in fade-in zoom-in-95 duration-200">
+        <h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2">Delete Version</h3>
+        <p class="text-zinc-600 dark:text-zinc-400 text-sm mb-6">
+          Are you sure you want to delete version <span class="text-gray-900 dark:text-white font-mono font-medium">{versionToDelete}</span>? This action cannot be undone.
+        </p>
+        <div class="flex gap-3 justify-end">
+          <button
+            onclick={cancelDelete}
+            class="px-4 py-2 text-sm font-medium text-zinc-600 dark:text-zinc-300 hover:text-zinc-900 dark:hover:text-white bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded-lg transition-colors"
+          >
+            Cancel
+          </button>
+          <button
+            onclick={confirmDelete}
+            class="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-500 rounded-lg transition-colors"
+          >
+            Delete
+          </button>
+        </div>
+      </div>
+    </div>
+  {/if}
 </div>
-- 
cgit v1.2.3-70-g09d2

