aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/ui/src
diff options
context:
space:
mode:
authorHsiangNianian <i@jyunko.cn>2026-01-16 18:38:47 +0800
committerHsiangNianian <i@jyunko.cn>2026-01-16 18:42:12 +0800
commit1119f6c3cf421da2f2db92873efae8135c76b678 (patch)
tree57ee487c5003807e984c084a31a37c34e30820cf /ui/src
parent52cf610264444bde95c1feae58414bb9849855eb (diff)
downloadDropOut-1119f6c3cf421da2f2db92873efae8135c76b678.tar.gz
DropOut-1119f6c3cf421da2f2db92873efae8135c76b678.zip
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.
Diffstat (limited to 'ui/src')
-rw-r--r--ui/src/components/BottomBar.svelte56
-rw-r--r--ui/src/components/ModLoaderSelector.svelte38
-rw-r--r--ui/src/components/VersionsView.svelte325
-rw-r--r--ui/src/stores/game.svelte.ts6
-rw-r--r--ui/src/types/index.ts2
5 files changed, 344 insertions, 83 deletions
diff --git a/ui/src/components/BottomBar.svelte b/ui/src/components/BottomBar.svelte
index b7bbf71..8a6b7ff 100644
--- a/ui/src/components/BottomBar.svelte
+++ b/ui/src/components/BottomBar.svelte
@@ -4,7 +4,7 @@
import { authState } from "../stores/auth.svelte";
import { gameState } from "../stores/game.svelte";
import { uiState } from "../stores/ui.svelte";
- import { Terminal, ChevronDown, Play, User, Check, RefreshCw } from 'lucide-svelte';
+ import { Terminal, ChevronDown, Play, User, Check } from 'lucide-svelte';
interface InstalledVersion {
id: string;
@@ -16,23 +16,31 @@
let installedVersions = $state<InstalledVersion[]>([]);
let isLoadingVersions = $state(true);
let downloadCompleteUnlisten: UnlistenFn | null = null;
+ let versionDeletedUnlisten: UnlistenFn | null = null;
// Load installed versions on mount
$effect(() => {
loadInstalledVersions();
- setupDownloadListener();
+ setupEventListeners();
return () => {
if (downloadCompleteUnlisten) {
downloadCompleteUnlisten();
}
+ if (versionDeletedUnlisten) {
+ versionDeletedUnlisten();
+ }
};
});
- async function setupDownloadListener() {
+ async function setupEventListeners() {
// Refresh list when a download completes
downloadCompleteUnlisten = await listen("download-complete", () => {
loadInstalledVersions();
});
+ // Refresh list when a version is deleted
+ versionDeletedUnlisten = await listen("version-deleted", () => {
+ loadInstalledVersions();
+ });
}
async function loadInstalledVersions() {
@@ -160,18 +168,7 @@
<div class="flex flex-col items-end mr-2">
<!-- Custom Version Dropdown -->
<div class="relative" bind:this={dropdownRef}>
- <div class="flex items-center gap-2">
- <button
- type="button"
- onclick={() => loadInstalledVersions()}
- class="p-2.5 dark:bg-zinc-900 bg-zinc-50 border dark:border-zinc-700 border-zinc-300 rounded-md
- dark:text-zinc-500 text-zinc-400 dark:hover:text-white hover:text-black
- dark:hover:border-zinc-600 hover:border-zinc-400 transition-colors"
- title="Refresh installed versions"
- >
- <RefreshCw size={14} class={isLoadingVersions ? 'animate-spin' : ''} />
- </button>
- <button
+ <button
type="button"
onclick={() => isVersionDropdownOpen = !isVersionDropdownOpen}
disabled={installedVersions.length === 0 && !isLoadingVersions}
@@ -183,21 +180,20 @@
transition-colors cursor-pointer outline-none
disabled:opacity-50 disabled:cursor-not-allowed"
>
- <span class="truncate">
- {#if isLoadingVersions}
- Loading...
- {:else if installedVersions.length === 0}
- No versions installed
- {:else}
- {gameState.selectedVersion || "Select version"}
- {/if}
- </span>
- <ChevronDown
- size={14}
- class="shrink-0 dark:text-zinc-500 text-zinc-400 transition-transform duration-200 {isVersionDropdownOpen ? 'rotate-180' : ''}"
- />
- </button>
- </div>
+ <span class="truncate">
+ {#if isLoadingVersions}
+ Loading...
+ {:else if installedVersions.length === 0}
+ No versions installed
+ {:else}
+ {gameState.selectedVersion || "Select version"}
+ {/if}
+ </span>
+ <ChevronDown
+ size={14}
+ class="shrink-0 dark:text-zinc-500 text-zinc-400 transition-transform duration-200 {isVersionDropdownOpen ? 'rotate-180' : ''}"
+ />
+ </button>
{#if isVersionDropdownOpen && installedVersions.length > 0}
<div
diff --git a/ui/src/components/ModLoaderSelector.svelte b/ui/src/components/ModLoaderSelector.svelte
index e9d147b..34f6f2e 100644
--- a/ui/src/components/ModLoaderSelector.svelte
+++ b/ui/src/components/ModLoaderSelector.svelte
@@ -291,7 +291,12 @@
{:else if selectedLoader === "fabric"}
<div class="space-y-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
- <div>
+ {#if fabricLoaders.length === 0}
+ <div class="text-center p-4 text-sm text-zinc-500 italic">
+ No Fabric versions available for {selectedGameVersion}
+ </div>
+ {:else}
+ <div>
<label for="fabric-loader-select" class="block text-[10px] uppercase font-bold text-zinc-500 mb-2"
>Loader Version</label
>
@@ -339,21 +344,22 @@
</div>
{/if}
</div>
- </div>
-
- <button
- class="w-full bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed text-white py-2.5 px-4 rounded-sm font-bold text-sm transition-all flex items-center justify-center gap-2"
- onclick={installModLoader}
- disabled={isInstalling || !selectedFabricLoader}
- >
- {#if isInstalling}
- <Loader2 class="animate-spin" size={16} />
- Installing...
- {:else}
- <Download size={16} />
- Install Fabric {selectedFabricLoader}
- {/if}
- </button>
+ </div>
+
+ <button
+ class="w-full bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed text-white py-2.5 px-4 rounded-sm font-bold text-sm transition-all flex items-center justify-center gap-2"
+ onclick={installModLoader}
+ disabled={isInstalling || !selectedFabricLoader}
+ >
+ {#if isInstalling}
+ <Loader2 class="animate-spin" size={16} />
+ Installing...
+ {:else}
+ <Download size={16} />
+ Install Fabric {selectedFabricLoader}
+ {/if}
+ </button>
+ {/if}
</div>
{:else if selectedLoader === "forge"}
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>
diff --git a/ui/src/stores/game.svelte.ts b/ui/src/stores/game.svelte.ts
index 28b2db5..ca5dc2b 100644
--- a/ui/src/stores/game.svelte.ts
+++ b/ui/src/stores/game.svelte.ts
@@ -14,10 +14,8 @@ export class GameState {
async loadVersions() {
try {
this.versions = await invoke<Version[]>("get_versions");
- if (this.versions.length > 0) {
- const latest = this.versions.find((v) => v.type === "release");
- this.selectedVersion = latest ? latest.id : this.versions[0].id;
- }
+ // Don't auto-select version here - let BottomBar handle version selection
+ // based on installed versions only
} catch (e) {
console.error("Failed to fetch versions:", e);
uiState.setStatus("Error fetching versions: " + e);
diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts
index 6471869..9a4da2b 100644
--- a/ui/src/types/index.ts
+++ b/ui/src/types/index.ts
@@ -6,6 +6,8 @@ export interface Version {
url: string;
time: string;
releaseTime: string;
+ javaVersion?: number; // Java major version requirement (e.g., 8, 17, 21)
+ isInstalled?: boolean; // Whether this version is installed locally
}
export interface Account {