From 3c13c14dea03c6b91716fb0f1578deb12fcf9756 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Fri, 16 Jan 2026 20:20:05 +0800 Subject: feat: implement instance management functionality Added a new InstancesState class to manage game instances, including loading, creating, deleting, updating, and duplicating instances. Integrated instance selection into the game launch process, ensuring an active instance is selected before starting a game. Updated the types to include instance-related data structures. --- ui/src/stores/game.svelte.ts | 14 ++++- ui/src/stores/instances.svelte.ts | 109 ++++++++++++++++++++++++++++++++++++++ ui/src/types/index.ts | 17 +++++- 3 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 ui/src/stores/instances.svelte.ts diff --git a/ui/src/stores/game.svelte.ts b/ui/src/stores/game.svelte.ts index ca5dc2b..3efcf71 100644 --- a/ui/src/stores/game.svelte.ts +++ b/ui/src/stores/game.svelte.ts @@ -2,6 +2,7 @@ import { invoke } from "@tauri-apps/api/core"; import type { Version } from "../types"; import { uiState } from "./ui.svelte"; import { authState } from "./auth.svelte"; +import { instancesState } from "./instances.svelte"; export class GameState { versions = $state([]); @@ -34,10 +35,19 @@ export class GameState { return; } + if (!instancesState.activeInstanceId) { + alert("Please select an instance first!"); + uiState.setView("instances"); + return; + } + uiState.setStatus("Preparing to launch " + this.selectedVersion + "..."); - console.log("Invoking start_game for version:", this.selectedVersion); + console.log("Invoking start_game for version:", this.selectedVersion, "instance:", instancesState.activeInstanceId); try { - const msg = await invoke("start_game", { versionId: this.selectedVersion }); + const msg = await invoke("start_game", { + instanceId: instancesState.activeInstanceId, + versionId: this.selectedVersion, + }); console.log("Response:", msg); uiState.setStatus(msg); } catch (e) { diff --git a/ui/src/stores/instances.svelte.ts b/ui/src/stores/instances.svelte.ts new file mode 100644 index 0000000..f4ac4e9 --- /dev/null +++ b/ui/src/stores/instances.svelte.ts @@ -0,0 +1,109 @@ +import { invoke } from "@tauri-apps/api/core"; +import type { Instance } from "../types"; +import { uiState } from "./ui.svelte"; + +export class InstancesState { + instances = $state([]); + activeInstanceId = $state(null); + get activeInstance(): Instance | null { + if (!this.activeInstanceId) return null; + return this.instances.find((i) => i.id === this.activeInstanceId) || null; + } + + async loadInstances() { + try { + this.instances = await invoke("list_instances"); + const active = await invoke("get_active_instance"); + if (active) { + this.activeInstanceId = active.id; + } else if (this.instances.length > 0) { + // If no active instance but instances exist, set the first one as active + await this.setActiveInstance(this.instances[0].id); + } + } catch (e) { + console.error("Failed to load instances:", e); + uiState.setStatus("Error loading instances: " + e); + } + } + + async createInstance(name: string): Promise { + try { + const instance = await invoke("create_instance", { name }); + await this.loadInstances(); + uiState.setStatus(`Instance "${name}" created successfully`); + return instance; + } catch (e) { + console.error("Failed to create instance:", e); + uiState.setStatus("Error creating instance: " + e); + return null; + } + } + + async deleteInstance(id: string) { + try { + await invoke("delete_instance", { instanceId: id }); + await this.loadInstances(); + // If deleted instance was active, set another as active + if (this.activeInstanceId === id) { + if (this.instances.length > 0) { + await this.setActiveInstance(this.instances[0].id); + } else { + this.activeInstanceId = null; + } + } + uiState.setStatus("Instance deleted successfully"); + } catch (e) { + console.error("Failed to delete instance:", e); + uiState.setStatus("Error deleting instance: " + e); + } + } + + async updateInstance(instance: Instance) { + try { + await invoke("update_instance", { instance }); + await this.loadInstances(); + uiState.setStatus("Instance updated successfully"); + } catch (e) { + console.error("Failed to update instance:", e); + uiState.setStatus("Error updating instance: " + e); + } + } + + async setActiveInstance(id: string) { + try { + await invoke("set_active_instance", { instanceId: id }); + this.activeInstanceId = id; + uiState.setStatus("Active instance changed"); + } catch (e) { + console.error("Failed to set active instance:", e); + uiState.setStatus("Error setting active instance: " + e); + } + } + + async duplicateInstance(id: string, newName: string): Promise { + try { + const instance = await invoke("duplicate_instance", { + instanceId: id, + newName, + }); + await this.loadInstances(); + uiState.setStatus(`Instance duplicated as "${newName}"`); + return instance; + } catch (e) { + console.error("Failed to duplicate instance:", e); + uiState.setStatus("Error duplicating instance: " + e); + return null; + } + } + + async getInstance(id: string): Promise { + try { + return await invoke("get_instance", { instanceId: id }); + } catch (e) { + console.error("Failed to get instance:", e); + return null; + } + } +} + +export const instancesState = new InstancesState(); diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index 9a4da2b..a5b336e 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -1,4 +1,4 @@ -export type ViewType = "home" | "versions" | "settings" | "guide"; +export type ViewType = "home" | "versions" | "settings" | "guide" | "instances"; export interface Version { id: string; @@ -187,3 +187,18 @@ export interface InstalledForgeVersion { // ==================== Mod Loader Type ==================== export type ModLoaderType = "vanilla" | "fabric" | "forge"; + +// ==================== Instance Types ==================== + +export interface Instance { + id: string; + name: string; + game_dir: string; + version_id?: string; + created_at: number; + last_played?: number; + icon_path?: string; + notes?: string; + mod_loader?: string; + mod_loader_version?: string; +} -- cgit v1.2.3-70-g09d2 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 @@