aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/ui/src/components/VersionsView.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'ui/src/components/VersionsView.svelte')
-rw-r--r--ui/src/components/VersionsView.svelte325
1 files changed, 292 insertions, 33 deletions
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>