aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--ui/src/App.svelte5
-rw-r--r--ui/src/components/BottomBar.svelte10
-rw-r--r--ui/src/components/InstancesView.svelte331
-rw-r--r--ui/src/components/ModLoaderSelector.svelte18
-rw-r--r--ui/src/components/Sidebar.svelte3
-rw-r--r--ui/src/components/VersionsView.svelte14
6 files changed, 376 insertions, 5 deletions
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 @@
<div class="flex-1 relative overflow-hidden">
{#if uiState.currentView === "home"}
<HomeView mouseX={mouseX} mouseY={mouseY} />
+ {:else if uiState.currentView === "instances"}
+ <InstancesView />
{:else if uiState.currentView === "versions"}
<VersionsView />
{: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<InstalledVersion[]>("list_installed_versions");
+ installedVersions = await invoke<InstalledVersion[]>("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 @@
+<script lang="ts">
+ import { onMount } from "svelte";
+ import { instancesState } from "../stores/instances.svelte";
+ import { Plus, Trash2, Edit2, Copy, Check, X } from "lucide-svelte";
+ import type { Instance } from "../types";
+
+ let showCreateModal = $state(false);
+ let showEditModal = $state(false);
+ let showDeleteConfirm = $state(false);
+ let showDuplicateModal = $state(false);
+ let selectedInstance: Instance | null = $state(null);
+ let newInstanceName = $state("");
+ let duplicateName = $state("");
+
+ onMount(() => {
+ instancesState.loadInstances();
+ });
+
+ function handleCreate() {
+ newInstanceName = "";
+ showCreateModal = true;
+ }
+
+ function handleEdit(instance: Instance) {
+ selectedInstance = instance;
+ newInstanceName = instance.name;
+ showEditModal = true;
+ }
+
+ function handleDelete(instance: Instance) {
+ selectedInstance = instance;
+ showDeleteConfirm = true;
+ }
+
+ function handleDuplicate(instance: Instance) {
+ selectedInstance = instance;
+ duplicateName = `${instance.name} (Copy)`;
+ showDuplicateModal = true;
+ }
+
+ async function confirmCreate() {
+ if (!newInstanceName.trim()) return;
+ await instancesState.createInstance(newInstanceName.trim());
+ showCreateModal = false;
+ newInstanceName = "";
+ }
+
+ async function confirmEdit() {
+ if (!selectedInstance || !newInstanceName.trim()) return;
+ await instancesState.updateInstance({
+ ...selectedInstance,
+ name: newInstanceName.trim(),
+ });
+ showEditModal = false;
+ selectedInstance = null;
+ newInstanceName = "";
+ }
+
+ async function confirmDelete() {
+ if (!selectedInstance) return;
+ await instancesState.deleteInstance(selectedInstance.id);
+ showDeleteConfirm = false;
+ selectedInstance = null;
+ }
+
+ async function confirmDuplicate() {
+ if (!selectedInstance || !duplicateName.trim()) return;
+ await instancesState.duplicateInstance(selectedInstance.id, duplicateName.trim());
+ showDuplicateModal = false;
+ selectedInstance = null;
+ duplicateName = "";
+ }
+
+ function formatDate(timestamp: number): string {
+ return new Date(timestamp * 1000).toLocaleDateString();
+ }
+
+ function formatLastPlayed(timestamp: number): string {
+ const date = new Date(timestamp * 1000);
+ const now = new Date();
+ const diff = now.getTime() - date.getTime();
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
+
+ if (days === 0) return "Today";
+ if (days === 1) return "Yesterday";
+ if (days < 7) return `${days} days ago`;
+ return date.toLocaleDateString();
+ }
+</script>
+
+<div class="h-full flex flex-col gap-4 p-6 overflow-y-auto">
+ <div class="flex items-center justify-between">
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-white">Instances</h1>
+ <button
+ onclick={handleCreate}
+ class="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
+ >
+ <Plus size={18} />
+ Create Instance
+ </button>
+ </div>
+
+ {#if instancesState.instances.length === 0}
+ <div class="flex-1 flex items-center justify-center">
+ <div class="text-center text-gray-500 dark:text-gray-400">
+ <p class="text-lg mb-2">No instances yet</p>
+ <p class="text-sm">Create your first instance to get started</p>
+ </div>
+ </div>
+ {:else}
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+ {#each instancesState.instances as instance (instance.id)}
+ <div
+ class="relative p-4 bg-gray-100 dark:bg-gray-800 rounded-lg border-2 transition-all cursor-pointer hover:border-blue-500 {instancesState.activeInstanceId === instance.id
+ ? 'border-blue-500'
+ : 'border-transparent'}"
+ onclick={() => instancesState.setActiveInstance(instance.id)}
+ >
+ {#if instancesState.activeInstanceId === instance.id}
+ <div class="absolute top-2 right-2">
+ <div class="w-3 h-3 bg-blue-500 rounded-full"></div>
+ </div>
+ {/if}
+
+ <div class="flex items-start justify-between mb-2">
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white">
+ {instance.name}
+ </h3>
+ <div class="flex gap-1">
+ <button
+ onclick={(e) => {
+ e.stopPropagation();
+ handleEdit(instance);
+ }}
+ class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors"
+ title="Edit"
+ >
+ <Edit2 size={16} class="text-gray-600 dark:text-gray-400" />
+ </button>
+ <button
+ onclick={(e) => {
+ e.stopPropagation();
+ handleDuplicate(instance);
+ }}
+ class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors"
+ title="Duplicate"
+ >
+ <Copy size={16} class="text-gray-600 dark:text-gray-400" />
+ </button>
+ <button
+ onclick={(e) => {
+ e.stopPropagation();
+ handleDelete(instance);
+ }}
+ class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors"
+ title="Delete"
+ >
+ <Trash2 size={16} class="text-red-600 dark:text-red-400" />
+ </button>
+ </div>
+ </div>
+
+ <div class="space-y-1 text-sm text-gray-600 dark:text-gray-400">
+ {#if instance.version_id}
+ <p>Version: <span class="font-medium">{instance.version_id}</span></p>
+ {:else}
+ <p class="text-gray-400">No version selected</p>
+ {/if}
+
+ {#if instance.mod_loader && instance.mod_loader !== "vanilla"}
+ <p>
+ Mod Loader: <span class="font-medium capitalize">{instance.mod_loader}</span>
+ {#if instance.mod_loader_version}
+ <span class="text-gray-500">({instance.mod_loader_version})</span>
+ {/if}
+ </p>
+ {/if}
+
+ <p>Created: {formatDate(instance.created_at)}</p>
+
+ {#if instance.last_played}
+ <p>Last played: {formatLastPlayed(instance.last_played)}</p>
+ {/if}
+ </div>
+
+ {#if instance.notes}
+ <p class="mt-2 text-sm text-gray-500 dark:text-gray-500 italic">
+ {instance.notes}
+ </p>
+ {/if}
+ </div>
+ {/each}
+ </div>
+ {/if}
+</div>
+
+<!-- Create Modal -->
+{#if showCreateModal}
+ <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
+ <div class="bg-white dark:bg-gray-800 rounded-lg p-6 w-96">
+ <h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-white">Create Instance</h2>
+ <input
+ type="text"
+ bind:value={newInstanceName}
+ placeholder="Instance name"
+ class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white mb-4"
+ onkeydown={(e) => e.key === "Enter" && confirmCreate()}
+ autofocus
+ />
+ <div class="flex gap-2 justify-end">
+ <button
+ onclick={() => {
+ showCreateModal = false;
+ newInstanceName = "";
+ }}
+ class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
+ >
+ Cancel
+ </button>
+ <button
+ onclick={confirmCreate}
+ disabled={!newInstanceName.trim()}
+ class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
+ >
+ Create
+ </button>
+ </div>
+ </div>
+ </div>
+{/if}
+
+<!-- Edit Modal -->
+{#if showEditModal && selectedInstance}
+ <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
+ <div class="bg-white dark:bg-gray-800 rounded-lg p-6 w-96">
+ <h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-white">Edit Instance</h2>
+ <input
+ type="text"
+ bind:value={newInstanceName}
+ placeholder="Instance name"
+ class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white mb-4"
+ onkeydown={(e) => e.key === "Enter" && confirmEdit()}
+ autofocus
+ />
+ <div class="flex gap-2 justify-end">
+ <button
+ onclick={() => {
+ showEditModal = false;
+ selectedInstance = null;
+ newInstanceName = "";
+ }}
+ class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
+ >
+ Cancel
+ </button>
+ <button
+ onclick={confirmEdit}
+ disabled={!newInstanceName.trim()}
+ class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
+ >
+ Save
+ </button>
+ </div>
+ </div>
+ </div>
+{/if}
+
+<!-- Delete Confirmation -->
+{#if showDeleteConfirm && selectedInstance}
+ <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
+ <div class="bg-white dark:bg-gray-800 rounded-lg p-6 w-96">
+ <h2 class="text-xl font-bold mb-4 text-red-600 dark:text-red-400">Delete Instance</h2>
+ <p class="mb-4 text-gray-700 dark:text-gray-300">
+ Are you sure you want to delete "{selectedInstance.name}"? This action cannot be undone and will delete all game data for this instance.
+ </p>
+ <div class="flex gap-2 justify-end">
+ <button
+ onclick={() => {
+ showDeleteConfirm = false;
+ selectedInstance = null;
+ }}
+ class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
+ >
+ Cancel
+ </button>
+ <button
+ onclick={confirmDelete}
+ class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
+ >
+ Delete
+ </button>
+ </div>
+ </div>
+ </div>
+{/if}
+
+<!-- Duplicate Modal -->
+{#if showDuplicateModal && selectedInstance}
+ <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
+ <div class="bg-white dark:bg-gray-800 rounded-lg p-6 w-96">
+ <h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-white">Duplicate Instance</h2>
+ <input
+ type="text"
+ bind:value={duplicateName}
+ placeholder="New instance name"
+ class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white mb-4"
+ onkeydown={(e) => e.key === "Enter" && confirmDuplicate()}
+ autofocus
+ />
+ <div class="flex gap-2 justify-end">
+ <button
+ onclick={() => {
+ showDuplicateModal = false;
+ selectedInstance = null;
+ duplicateName = "";
+ }}
+ class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
+ >
+ Cancel
+ </button>
+ <button
+ onclick={confirmDuplicate}
+ disabled={!duplicateName.trim()}
+ class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
+ >
+ Duplicate
+ </button>
+ </div>
+ </div>
+ </div>
+{/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<boolean>("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<any>("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<any>("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 @@
<script lang="ts">
import { uiState } from '../stores/ui.svelte';
- import { Home, Package, Settings, Bot } from 'lucide-svelte';
+ import { Home, Package, Settings, Bot, Folder } from 'lucide-svelte';
</script>
<aside
@@ -76,6 +76,7 @@
{/snippet}
{@render navItem('home', Home, 'Overview')}
+ {@render navItem('instances', Folder, 'Instances')}
{@render navItem('versions', Package, 'Versions')}
{@render navItem('guide', Bot, 'Assistant')}
{@render navItem('settings', Settings, 'Settings')}
diff --git a/ui/src/components/VersionsView.svelte b/ui/src/components/VersionsView.svelte
index 2e8b028..d4d36d5 100644
--- a/ui/src/components/VersionsView.svelte
+++ b/ui/src/components/VersionsView.svelte
@@ -2,6 +2,7 @@
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import { gameState } from "../stores/game.svelte";
+ import { instancesState } from "../stores/instances.svelte";
import ModLoaderSelector from "./ModLoaderSelector.svelte";
let searchQuery = $state("");
@@ -18,11 +19,17 @@
// Load installed modded versions with Java version info (both Fabric and Forge)
async function loadInstalledModdedVersions() {
+ if (!instancesState.activeInstanceId) {
+ installedFabricVersions = [];
+ isLoadingModded = false;
+ return;
+ }
isLoadingModded = true;
try {
// Get all installed versions and filter for modded ones (Fabric and Forge)
const allInstalled = await invoke<Array<{ id: string; type: string }>>(
- "list_installed_versions"
+ "list_installed_versions",
+ { instanceId: instancesState.activeInstanceId }
);
// Filter for Fabric and Forge versions
@@ -36,7 +43,10 @@
try {
const javaVersion = await invoke<number | null>(
"get_version_java_version",
- { versionId: id }
+ {
+ instanceId: instancesState.activeInstanceId!,
+ versionId: id,
+ }
);
return {
id,