From 743401f15199a116b1777bced843c774c5a59fba Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Fri, 16 Jan 2026 20:20:19 +0800 Subject: feat: add InstancesView component and integrate instance management into the UI Introduced a new InstancesView component for managing game instances, allowing users to create, edit, delete, and duplicate instances. Updated the App.svelte to include the InstancesView and modified various components to ensure instance selection is handled correctly. Enhanced the ModLoaderSelector and VersionsView to check for active instances before performing actions. Updated the Sidebar to include navigation to the new InstancesView. --- ui/src/App.svelte | 5 + ui/src/components/BottomBar.svelte | 10 +- ui/src/components/InstancesView.svelte | 331 +++++++++++++++++++++++++++++ ui/src/components/ModLoaderSelector.svelte | 18 +- ui/src/components/Sidebar.svelte | 3 +- ui/src/components/VersionsView.svelte | 14 +- 6 files changed, 376 insertions(+), 5 deletions(-) create mode 100644 ui/src/components/InstancesView.svelte diff --git a/ui/src/App.svelte b/ui/src/App.svelte index 2b78892..127bbea 100644 --- a/ui/src/App.svelte +++ b/ui/src/App.svelte @@ -11,12 +11,14 @@ import ParticleBackground from "./components/ParticleBackground.svelte"; import SettingsView from "./components/SettingsView.svelte"; import AssistantView from "./components/AssistantView.svelte"; + import InstancesView from "./components/InstancesView.svelte"; import Sidebar from "./components/Sidebar.svelte"; import StatusToast from "./components/StatusToast.svelte"; import VersionsView from "./components/VersionsView.svelte"; // Stores import { authState } from "./stores/auth.svelte"; import { gameState } from "./stores/game.svelte"; + import { instancesState } from "./stores/instances.svelte"; import { settingsState } from "./stores/settings.svelte"; import { uiState } from "./stores/ui.svelte"; import { logsState } from "./stores/logs.svelte"; @@ -40,6 +42,7 @@ await settingsState.loadSettings(); logsState.init(); await settingsState.detectJava(); + await instancesState.loadInstances(); gameState.loadVersions(); getVersion().then((v) => (uiState.appVersion = v)); window.addEventListener("mousemove", handleMouseMove); @@ -113,6 +116,8 @@
{#if uiState.currentView === "home"} + {:else if uiState.currentView === "instances"} + {:else if uiState.currentView === "versions"} {:else if uiState.currentView === "settings"} diff --git a/ui/src/components/BottomBar.svelte b/ui/src/components/BottomBar.svelte index 8a6b7ff..19cf35d 100644 --- a/ui/src/components/BottomBar.svelte +++ b/ui/src/components/BottomBar.svelte @@ -4,6 +4,7 @@ import { authState } from "../stores/auth.svelte"; import { gameState } from "../stores/game.svelte"; import { uiState } from "../stores/ui.svelte"; + import { instancesState } from "../stores/instances.svelte"; import { Terminal, ChevronDown, Play, User, Check } from 'lucide-svelte'; interface InstalledVersion { @@ -44,9 +45,16 @@ } async function loadInstalledVersions() { + if (!instancesState.activeInstanceId) { + installedVersions = []; + isLoadingVersions = false; + return; + } isLoadingVersions = true; try { - installedVersions = await invoke("list_installed_versions"); + installedVersions = await invoke("list_installed_versions", { + instanceId: instancesState.activeInstanceId, + }); // If no version is selected but we have installed versions, select the first one if (!gameState.selectedVersion && installedVersions.length > 0) { gameState.selectedVersion = installedVersions[0].id; diff --git a/ui/src/components/InstancesView.svelte b/ui/src/components/InstancesView.svelte new file mode 100644 index 0000000..a4881e6 --- /dev/null +++ b/ui/src/components/InstancesView.svelte @@ -0,0 +1,331 @@ + + +
+
+

Instances

+ +
+ + {#if instancesState.instances.length === 0} +
+
+

No instances yet

+

Create your first instance to get started

+
+
+ {:else} +
+ {#each instancesState.instances as instance (instance.id)} +
instancesState.setActiveInstance(instance.id)} + > + {#if instancesState.activeInstanceId === instance.id} +
+
+
+ {/if} + +
+

+ {instance.name} +

+
+ + + +
+
+ +
+ {#if instance.version_id} +

Version: {instance.version_id}

+ {:else} +

No version selected

+ {/if} + + {#if instance.mod_loader && instance.mod_loader !== "vanilla"} +

+ Mod Loader: {instance.mod_loader} + {#if instance.mod_loader_version} + ({instance.mod_loader_version}) + {/if} +

+ {/if} + +

Created: {formatDate(instance.created_at)}

+ + {#if instance.last_played} +

Last played: {formatLastPlayed(instance.last_played)}

+ {/if} +
+ + {#if instance.notes} +

+ {instance.notes} +

+ {/if} +
+ {/each} +
+ {/if} +
+ + +{#if showCreateModal} +
+
+

Create Instance

+ e.key === "Enter" && confirmCreate()} + autofocus + /> +
+ + +
+
+
+{/if} + + +{#if showEditModal && selectedInstance} +
+
+

Edit Instance

+ e.key === "Enter" && confirmEdit()} + autofocus + /> +
+ + +
+
+
+{/if} + + +{#if showDeleteConfirm && selectedInstance} +
+
+

Delete Instance

+

+ Are you sure you want to delete "{selectedInstance.name}"? This action cannot be undone and will delete all game data for this instance. +

+
+ + +
+
+
+{/if} + + +{#if showDuplicateModal && selectedInstance} +
+
+

Duplicate Instance

+ e.key === "Enter" && confirmDuplicate()} + autofocus + /> +
+ + +
+
+
+{/if} diff --git a/ui/src/components/ModLoaderSelector.svelte b/ui/src/components/ModLoaderSelector.svelte index 34f6f2e..50caa8c 100644 --- a/ui/src/components/ModLoaderSelector.svelte +++ b/ui/src/components/ModLoaderSelector.svelte @@ -9,6 +9,7 @@ } from "../types"; import { Loader2, Download, AlertCircle, Check, ChevronDown, CheckCircle } from 'lucide-svelte'; import { logsState } from "../stores/logs.svelte"; + import { instancesState } from "../stores/instances.svelte"; interface Props { selectedGameVersion: string; @@ -52,12 +53,13 @@ }); async function checkInstallStatus() { - if (!selectedGameVersion) { + if (!selectedGameVersion || !instancesState.activeInstanceId) { isVersionInstalled = false; return; } try { isVersionInstalled = await invoke("check_version_installed", { + instanceId: instancesState.activeInstanceId, versionId: selectedGameVersion, }); } catch (e) { @@ -112,8 +114,13 @@ error = null; logsState.addLog("info", "Installer", `Starting installation of ${selectedGameVersion}...`); + if (!instancesState.activeInstanceId) { + error = "Please select an instance first"; + return; + } try { await invoke("install_version", { + instanceId: instancesState.activeInstanceId, versionId: selectedGameVersion, }); logsState.addLog("info", "Installer", `Successfully installed ${selectedGameVersion}`); @@ -134,6 +141,12 @@ return; } + if (!instancesState.activeInstanceId) { + error = "Please select an instance first"; + isInstalling = false; + return; + } + isInstalling = true; error = null; @@ -142,6 +155,7 @@ if (!isVersionInstalled) { logsState.addLog("info", "Installer", `Installing base game ${selectedGameVersion} first...`); await invoke("install_version", { + instanceId: instancesState.activeInstanceId, versionId: selectedGameVersion, }); isVersionInstalled = true; @@ -151,6 +165,7 @@ if (selectedLoader === "fabric" && selectedFabricLoader) { logsState.addLog("info", "Installer", `Installing Fabric ${selectedFabricLoader} for ${selectedGameVersion}...`); const result = await invoke("install_fabric", { + instanceId: instancesState.activeInstanceId, gameVersion: selectedGameVersion, loaderVersion: selectedFabricLoader, }); @@ -159,6 +174,7 @@ } else if (selectedLoader === "forge" && selectedForgeVersion) { logsState.addLog("info", "Installer", `Installing Forge ${selectedForgeVersion} for ${selectedGameVersion}...`); const result = await invoke("install_forge", { + instanceId: instancesState.activeInstanceId, gameVersion: selectedGameVersion, forgeVersion: selectedForgeVersion, }); diff --git a/ui/src/components/Sidebar.svelte b/ui/src/components/Sidebar.svelte index 3d36f89..83f4ac6 100644 --- a/ui/src/components/Sidebar.svelte +++ b/ui/src/components/Sidebar.svelte @@ -1,6 +1,6 @@