diff options
Diffstat (limited to 'ui/src/components')
| -rw-r--r-- | ui/src/components/BottomBar.svelte | 10 | ||||
| -rw-r--r-- | ui/src/components/InstancesView.svelte | 331 | ||||
| -rw-r--r-- | ui/src/components/ModLoaderSelector.svelte | 18 | ||||
| -rw-r--r-- | ui/src/components/Sidebar.svelte | 3 | ||||
| -rw-r--r-- | ui/src/components/VersionsView.svelte | 14 |
5 files changed, 371 insertions, 5 deletions
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, |