diff options
| author | 2026-02-25 00:16:53 +0800 | |
|---|---|---|
| committer | 2026-02-25 00:16:53 +0800 | |
| commit | a6773bd092db654360c599ca6b0108ea0e456e8c (patch) | |
| tree | c78c802a2563fff7aef908532a0706c0299830ac | |
| parent | b275a3668b140d9ce4663de646519d2dbd4297e7 (diff) | |
| download | DropOut-a6773bd092db654360c599ca6b0108ea0e456e8c.tar.gz DropOut-a6773bd092db654360c599ca6b0108ea0e456e8c.zip | |
feat: prepare for nightly alpha
17 files changed, 259 insertions, 483 deletions
diff --git a/packages/ui-new/src/client.ts b/packages/ui-new/src/client.ts index 572cdd9..18d2377 100644 --- a/packages/ui-new/src/client.ts +++ b/packages/ui-new/src/client.ts @@ -223,8 +223,12 @@ export function getVersionMetadata( }); } -export function getVersions(instanceId: string): Promise<Version[]> { - return invoke<Version[]>("get_versions", { +export function getVersions(): Promise<Version[]> { + return invoke<Version[]>("get_versions"); +} + +export function getVersionsOfInstance(instanceId: string): Promise<Version[]> { + return invoke<Version[]>("get_versions_of_instance", { instanceId, }); } diff --git a/packages/ui-new/src/components/bottom-bar.tsx b/packages/ui-new/src/components/bottom-bar.tsx index 2653880..32eb852 100644 --- a/packages/ui-new/src/components/bottom-bar.tsx +++ b/packages/ui-new/src/components/bottom-bar.tsx @@ -1,13 +1,22 @@ -import { invoke } from "@tauri-apps/api/core"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; -import { Check, ChevronDown, Play, User } from "lucide-react"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { Play, User } from "lucide-react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { toast } from "sonner"; +import { listInstalledVersions, startGame } from "@/client"; import { cn } from "@/lib/utils"; import { useAuthStore } from "@/models/auth"; +import { useInstancesStore } from "@/models/instances"; import { useGameStore } from "@/stores/game-store"; -import { useInstancesStore } from "@/stores/instances-store"; import { LoginModal } from "./login-modal"; import { Button } from "./ui/button"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "./ui/select"; interface InstalledVersion { id: string; @@ -19,7 +28,7 @@ export function BottomBar() { const gameStore = useGameStore(); const instancesStore = useInstancesStore(); - const [isVersionDropdownOpen, setIsVersionDropdownOpen] = useState(false); + const [selectedVersion, setSelectedVersion] = useState<string | null>(null); const [installedVersions, setInstalledVersions] = useState< InstalledVersion[] >([]); @@ -27,7 +36,7 @@ export function BottomBar() { const [showLoginModal, setShowLoginModal] = useState(false); const loadInstalledVersions = useCallback(async () => { - if (!instancesStore.activeInstanceId) { + if (!instancesStore.activeInstance) { setInstalledVersions([]); setIsLoadingVersions(false); return; @@ -35,9 +44,8 @@ export function BottomBar() { setIsLoadingVersions(true); try { - const versions = await invoke<InstalledVersion[]>( - "list_installed_versions", - { instanceId: instancesStore.activeInstanceId }, + const versions = await listInstalledVersions( + instancesStore.activeInstance.id, ); const installed = versions || []; @@ -53,7 +61,7 @@ export function BottomBar() { setIsLoadingVersions(false); } }, [ - instancesStore.activeInstanceId, + instancesStore.activeInstance, gameStore.selectedVersion, gameStore.setSelectedVersion, ]); @@ -100,20 +108,23 @@ export function BottomBar() { }; }, [loadInstalledVersions]); - const selectVersion = (id: string) => { - if (id !== "loading" && id !== "empty") { - gameStore.setSelectedVersion(id); - setIsVersionDropdownOpen(false); + const handleStartGame = async () => { + if (!selectedVersion) { + toast.info("Please select a version!"); + return; } - }; - const handleStartGame = async () => { + if (!instancesStore.activeInstance) { + toast.info("Please select an instance first!"); + return; + } // await gameStore.startGame( // authStore.currentAccount, // authStore.openLoginModal, // instancesStore.activeInstanceId, // uiStore.setView, // ); + await startGame(instancesStore.activeInstance?.id, selectedVersion); }; const getVersionTypeColor = (type: string) => { @@ -131,14 +142,15 @@ export function BottomBar() { } }; - const versionOptions = isLoadingVersions - ? [{ id: "loading", type: "loading", label: "Loading..." }] - : installedVersions.length === 0 - ? [{ id: "empty", type: "empty", label: "No versions installed" }] - : installedVersions.map((v) => ({ - ...v, - label: `${v.id}${v.type !== "release" ? ` (${v.type})` : ""}`, - })); + const versionOptions = useMemo( + () => + installedVersions.map((v) => ({ + label: `${v.id}${v.type !== "release" ? ` (${v.type})` : ""}`, + value: v.id, + type: v.type, + })), + [installedVersions], + ); return ( <div className="absolute bottom-0 left-0 right-0 bg-linear-to-t from-black/30 via-transparent to-transparent p-4 z-10"> @@ -153,6 +165,35 @@ export function BottomBar() { {instancesStore.activeInstance?.name || "No instance selected"} </span> </div> + + <Select + items={versionOptions} + onValueChange={setSelectedVersion} + disabled={isLoadingVersions} + > + <SelectTrigger className="max-w-48"> + <SelectValue + placeholder={ + isLoadingVersions + ? "Loading versions..." + : "Please select a version" + } + /> + </SelectTrigger> + <SelectContent> + <SelectGroup> + {versionOptions.map((item) => ( + <SelectItem + key={item.value} + value={item.value} + className={getVersionTypeColor(item.type)} + > + {item.label} + </SelectItem> + ))} + </SelectGroup> + </SelectContent> + </Select> </div> <div className="flex items-center gap-3"> diff --git a/packages/ui-new/src/components/instance-creation-modal.tsx b/packages/ui-new/src/components/instance-creation-modal.tsx index bdc1a6f..8a2b1b4 100644 --- a/packages/ui-new/src/components/instance-creation-modal.tsx +++ b/packages/ui-new/src/components/instance-creation-modal.tsx @@ -13,8 +13,8 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { useInstancesStore } from "@/models/instances"; import { useGameStore } from "@/stores/game-store"; -import { useInstancesStore } from "@/stores/instances-store"; import type { Version } from "@/types/bindings/manifest"; import type { FabricLoaderEntry } from "../types/bindings/fabric"; import type { ForgeVersion as ForgeVersionEntry } from "../types/bindings/forge"; @@ -25,20 +25,6 @@ interface Props { onOpenChange: (open: boolean) => void; } -/** - * InstanceCreationModal - * 3-step wizard: - * 1) Name - * 2) Select base Minecraft version - * 3) Optional: choose mod loader (vanilla/fabric/forge) and loader version - * - * Behavior: - * - On Create: invoke("create_instance", { name }) - * - If a base version selected: invoke("install_version", { instanceId, versionId }) - * - If Fabric selected: invoke("install_fabric", { instanceId, gameVersion, loaderVersion }) - * - If Forge selected: invoke("install_forge", { instanceId, gameVersion, forgeVersion }) - * - Reload instances via instancesStore.loadInstances() - */ export function InstanceCreationModal({ open, onOpenChange }: Props) { const gameStore = useGameStore(); const instancesStore = useInstancesStore(); @@ -242,7 +228,7 @@ export function InstanceCreationModal({ open, onOpenChange }: Props) { } // Refresh instances list - await instancesStore.loadInstances(); + await instancesStore.refresh(); toast.success("Instance created successfully"); onOpenChange(false); diff --git a/packages/ui-new/src/components/instance-editor-modal.tsx b/packages/ui-new/src/components/instance-editor-modal.tsx index 74e0873..f880c20 100644 --- a/packages/ui-new/src/components/instance-editor-modal.tsx +++ b/packages/ui-new/src/components/instance-editor-modal.tsx @@ -16,8 +16,8 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { toNumber } from "@/lib/tsrs-utils"; -import { useInstancesStore } from "@/stores/instances-store"; -import { useSettingsStore } from "@/stores/settings-store"; +import { useInstancesStore } from "@/models/instances"; +import { useSettingsStore } from "@/models/settings"; import type { FileInfo } from "../types/bindings/core"; import type { Instance } from "../types/bindings/instance"; @@ -29,7 +29,7 @@ type Props = { export function InstanceEditorModal({ open, instance, onOpenChange }: Props) { const instancesStore = useInstancesStore(); - const { settings } = useSettingsStore(); + const { config } = useSettingsStore(); const [activeTab, setActiveTab] = useState< "info" | "version" | "files" | "settings" @@ -67,19 +67,19 @@ export function InstanceEditorModal({ open, instance, onOpenChange }: Props) { setEditNotes(instance.notes ?? ""); setEditMemoryMin( (instance.memoryOverride && toNumber(instance.memoryOverride.min)) ?? - settings.minMemory ?? + config?.minMemory ?? 512, ); setEditMemoryMax( (instance.memoryOverride && toNumber(instance.memoryOverride.max)) ?? - settings.maxMemory ?? + config?.maxMemory ?? 2048, ); setEditJavaArgs(instance.jvmArgsOverride ?? ""); setFileList([]); setSelectedFileFolder("mods"); } - }, [open, instance, settings.minMemory, settings.maxMemory]); + }, [open, instance, config?.minMemory, config?.maxMemory]); // load files when switching to files tab const loadFileList = useCallback( @@ -178,7 +178,7 @@ export function InstanceEditorModal({ open, instance, onOpenChange }: Props) { jvmArgsOverride: editJavaArgs.trim() ? editJavaArgs.trim() : null, }; - await instancesStore.updateInstance(updatedInstance as Instance); + await instancesStore.update(updatedInstance as Instance); toast.success("Instance saved"); onOpenChange(false); } catch (err) { @@ -471,7 +471,7 @@ export function InstanceEditorModal({ open, instance, onOpenChange }: Props) { disabled={saving} /> <p className="text-xs text-zinc-400 mt-1"> - Default: {settings.minMemory} MB + Default: {config?.minMemory} MB </p> </div> @@ -490,7 +490,7 @@ export function InstanceEditorModal({ open, instance, onOpenChange }: Props) { disabled={saving} /> <p className="text-xs text-zinc-400 mt-1"> - Default: {settings.maxMemory} MB + Default: {config?.maxMemory} MB </p> </div> diff --git a/packages/ui-new/src/main.tsx b/packages/ui-new/src/main.tsx index bda693d..a3157bd 100644 --- a/packages/ui-new/src/main.tsx +++ b/packages/ui-new/src/main.tsx @@ -3,13 +3,10 @@ import { createRoot } from "react-dom/client"; import "./index.css"; import { createHashRouter, RouterProvider } from "react-router"; import { Toaster } from "./components/ui/sonner"; -import { AssistantView } from "./pages/assistant-view"; import { HomeView } from "./pages/home-view"; import { IndexPage } from "./pages/index"; import { InstancesView } from "./pages/instances-view"; import { SettingsPage } from "./pages/settings"; -import { SettingsView } from "./pages/settings-view"; -import { VersionsView } from "./pages/versions-view"; const router = createHashRouter([ { @@ -25,17 +22,9 @@ const router = createHashRouter([ element: <InstancesView />, }, { - path: "versions", - element: <VersionsView />, - }, - { path: "settings", element: <SettingsPage />, }, - // { - // path: "guide", - // element: <AssistantView />, - // }, ], }, ]); diff --git a/packages/ui-new/src/models/instances.ts b/packages/ui-new/src/models/instances.ts new file mode 100644 index 0000000..f434c7c --- /dev/null +++ b/packages/ui-new/src/models/instances.ts @@ -0,0 +1,135 @@ +import { toast } from "sonner"; +import { create } from "zustand"; +import { + createInstance, + deleteInstance, + duplicateInstance, + getActiveInstance, + getInstance, + listInstances, + setActiveInstance, + updateInstance, +} from "@/client"; +import type { Instance } from "@/types"; + +interface InstancesState { + // State + instances: Instance[]; + activeInstance: Instance | null; + + // Actions + refresh: () => Promise<void>; + create: (name: string) => Promise<Instance | null>; + delete: (id: string) => Promise<void>; + update: (instance: Instance) => Promise<void>; + setActiveInstance: (instance: Instance) => Promise<void>; + duplicate: (id: string, newName: string) => Promise<Instance | null>; + getInstance: (id: string) => Promise<Instance | null>; +} + +export const useInstancesStore = create<InstancesState>((set, get) => ({ + // Initial state + instances: [], + activeInstance: null, + + // Actions + refresh: async () => { + const { setActiveInstance } = get(); + try { + const instances = await listInstances(); + const active = await getActiveInstance(); + + if (!active && instances.length > 0) { + // If no active instance but instances exist, set the first one as active + await setActiveInstance(instances[0]); + } + + set({ instances }); + } catch (e) { + console.error("Failed to load instances:", e); + toast.error("Error loading instances"); + } + }, + + create: async (name) => { + const { refresh } = get(); + try { + const instance = await createInstance(name); + await refresh(); + toast.success(`Instance "${name}" created successfully`); + return instance; + } catch (e) { + console.error("Failed to create instance:", e); + toast.error("Error creating instance"); + return null; + } + }, + + delete: async (id) => { + const { refresh, instances, activeInstance, setActiveInstance } = get(); + try { + await deleteInstance(id); + await refresh(); + + // If deleted instance was active, set another as active + if (activeInstance?.id === id) { + if (instances.length > 0) { + await setActiveInstance(instances[0]); + } else { + set({ activeInstance: null }); + } + } + + toast.success("Instance deleted successfully"); + } catch (e) { + console.error("Failed to delete instance:", e); + toast.error("Error deleting instance"); + } + }, + + update: async (instance) => { + const { refresh } = get(); + try { + await updateInstance(instance); + await refresh(); + toast.success("Instance updated successfully"); + } catch (e) { + console.error("Failed to update instance:", e); + toast.error("Error updating instance"); + } + }, + + setActiveInstance: async (instance) => { + try { + await setActiveInstance(instance.id); + set({ activeInstance: instance }); + toast.success("Active instance changed"); + } catch (e) { + console.error("Failed to set active instance:", e); + toast.error("Error setting active instance"); + } + }, + + duplicate: async (id, newName) => { + const { refresh } = get(); + try { + const instance = await duplicateInstance(id, newName); + await refresh(); + toast.success(`Instance duplicated as "${newName}"`); + return instance; + } catch (e) { + console.error("Failed to duplicate instance:", e); + toast.error("Error duplicating instance"); + return null; + } + }, + + getInstance: async (id) => { + try { + return await getInstance(id); + } catch (e) { + console.error("Failed to get instance:", e); + return null; + } + }, +})); diff --git a/packages/ui-new/src/pages/assistant-view.tsx b/packages/ui-new/src/pages/assistant-view.tsx.bk index 56f827b..56f827b 100644 --- a/packages/ui-new/src/pages/assistant-view.tsx +++ b/packages/ui-new/src/pages/assistant-view.tsx.bk diff --git a/packages/ui-new/src/pages/index-old.tsx b/packages/ui-new/src/pages/index-old.tsx deleted file mode 100644 index a6626c9..0000000 --- a/packages/ui-new/src/pages/index-old.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { useEffect } from "react"; -import { Outlet } from "react-router"; -import { BottomBar } from "@/components/bottom-bar"; -import { DownloadMonitor } from "@/components/download-monitor"; -import { GameConsole } from "@/components/game-console"; -import { LoginModal } from "@/components/login-modal"; -import { ParticleBackground } from "@/components/particle-background"; -import { Sidebar } from "@/components/sidebar"; - -import { useAuthStore } from "@/stores/auth-store"; -import { useGameStore } from "@/stores/game-store"; -import { useInstancesStore } from "@/stores/instances-store"; -import { useLogsStore } from "@/stores/logs-store"; -import { useSettingsStore } from "@/stores/settings-store"; -import { useUIStore } from "@/stores/ui-store"; - -export function IndexPage() { - const authStore = useAuthStore(); - const settingsStore = useSettingsStore(); - const uiStore = useUIStore(); - const instancesStore = useInstancesStore(); - const gameStore = useGameStore(); - const logsStore = useLogsStore(); - useEffect(() => { - // ENFORCE DARK MODE: Always add 'dark' class and attribute - document.documentElement.classList.add("dark"); - document.documentElement.setAttribute("data-theme", "dark"); - document.documentElement.classList.remove("light"); - - // Initialize stores - // Include store functions in the dependency array to satisfy hooks lint. - // These functions are stable in our store implementation, so listing them - // here is safe and prevents lint warnings. - authStore.checkAccount(); - settingsStore.loadSettings(); - logsStore.init(); - settingsStore.detectJava(); - instancesStore.loadInstances(); - gameStore.loadVersions(); - - // Note: getVersion() would need Tauri API setup - // getVersion().then((v) => uiStore.setAppVersion(v)); - }, [ - authStore.checkAccount, - settingsStore.loadSettings, - logsStore.init, - settingsStore.detectJava, - instancesStore.loadInstances, - gameStore.loadVersions, - ]); - - // Refresh versions when active instance changes - useEffect(() => { - if (instancesStore.activeInstanceId) { - gameStore.loadVersions(); - } else { - gameStore.setVersions([]); - } - }, [ - instancesStore.activeInstanceId, - gameStore.loadVersions, - gameStore.setVersions, - ]); - - return ( - <div className="relative h-screen w-screen overflow-hidden dark:text-white text-gray-900 font-sans selection:bg-indigo-500/30"> - {/* Modern Animated Background */} - <div className="absolute inset-0 z-0 bg-gray-100 dark:bg-[#09090b] overflow-hidden"> - {settingsStore.settings.customBackgroundPath && ( - <img - src={settingsStore.settings.customBackgroundPath} - alt="Background" - className="absolute inset-0 w-full h-full object-cover transition-transform duration-[20s] ease-linear" - onError={(e) => console.error("Failed to load main background:", e)} - /> - )} - - {/* Dimming Overlay for readability */} - {settingsStore.settings.customBackgroundPath && ( - <div className="absolute inset-0 bg-black/50"></div> - )} - - {!settingsStore.settings.customBackgroundPath && ( - <> - {settingsStore.settings.theme === "dark" ? ( - <div className="absolute inset-0 opacity-60 bg-linear-to-br from-emerald-900 via-zinc-900 to-indigo-950"></div> - ) : ( - <div className="absolute inset-0 opacity-100 bg-linear-to-br from-emerald-100 via-gray-100 to-indigo-100"></div> - )} - - {uiStore.currentView === "home" && <ParticleBackground />} - - <div className="absolute inset-0 bg-linear-to-t from-zinc-900 via-transparent to-black/50 dark:from-zinc-900 dark:to-black/50"></div> - </> - )} - - {/* Subtle Grid Overlay */} - <div - className="absolute inset-0 z-0 dark:opacity-10 opacity-30 pointer-events-none" - style={{ - backgroundImage: `linear-gradient(${ - settingsStore.settings.theme === "dark" ? "#ffffff" : "#000000" - } 1px, transparent 1px), linear-gradient(90deg, ${ - settingsStore.settings.theme === "dark" ? "#ffffff" : "#000000" - } 1px, transparent 1px)`, - backgroundSize: "40px 40px", - maskImage: - "radial-gradient(circle at 50% 50%, black 30%, transparent 70%)", - }} - ></div> - </div> - - {/* Content Wrapper */} - <div className="relative z-10 flex h-full p-4 gap-4 text-gray-900 dark:text-white"> - {/* Floating Sidebar */} - <Sidebar /> - - {/* Main Content Area - Transparent & Flat */} - <main className="flex-1 flex flex-col relative min-w-0 overflow-hidden transition-all duration-300"> - {/* Window Drag Region */} - <div - className="h-8 w-full absolute top-0 left-0 z-50 drag-region" - data-tauri-drag-region - ></div> - - {/* App Content */} - <div className="flex-1 relative overflow-hidden flex flex-col"> - {/* Views Container */} - <div className="flex-1 relative overflow-hidden"> - <Outlet /> - </div> - - {/* Download Monitor Overlay */} - <div className="absolute bottom-20 left-4 right-4 pointer-events-none z-20"> - <div className="pointer-events-auto"> - <DownloadMonitor /> - </div> - </div> - - {/* Bottom Bar */} - {uiStore.currentView === "home" && <BottomBar />} - </div> - </main> - </div> - - {/* Logout Confirmation Dialog */} - {authStore.isLogoutConfirmOpen && ( - <div className="fixed inset-0 z-200 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4"> - <div className="bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl p-6 max-w-sm w-full animate-in fade-in zoom-in-95 duration-200"> - <h3 className="text-lg font-bold text-white mb-2">Logout</h3> - <p className="text-zinc-400 text-sm mb-6"> - Are you sure you want to logout{" "} - <span className="text-white font-medium"> - {authStore.currentAccount?.username} - </span> - ? - </p> - <div className="flex gap-3 justify-end"> - <button - type="button" - onClick={() => authStore.cancelLogout()} - className="px-4 py-2 text-sm font-medium text-zinc-300 hover:text-white bg-zinc-800 hover:bg-zinc-700 rounded-lg transition-colors" - > - Cancel - </button> - <button - type="button" - onClick={() => authStore.confirmLogout()} - className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-500 rounded-lg transition-colors" - > - Logout - </button> - </div> - </div> - </div> - )} - - {uiStore.showConsole && ( - <div className="fixed inset-0 z-100 bg-black/80 backdrop-blur-sm flex items-center justify-center p-8"> - <div className="w-full h-full max-w-6xl max-h-[85vh] bg-[#1e1e1e] rounded-lg overflow-hidden border border-zinc-700 shadow-2xl relative flex flex-col"> - <GameConsole /> - </div> - </div> - )} - </div> - ); -} diff --git a/packages/ui-new/src/pages/instances-view.tsx b/packages/ui-new/src/pages/instances-view.tsx index 0c511a1..ad6bd38 100644 --- a/packages/ui-new/src/pages/instances-view.tsx +++ b/packages/ui-new/src/pages/instances-view.tsx @@ -1,5 +1,6 @@ import { Copy, Edit2, Plus, Trash2 } from "lucide-react"; import { useEffect, useState } from "react"; +import InstanceCreationModal from "@/components/instance-creation-modal"; import InstanceEditorModal from "@/components/instance-editor-modal"; import { Button } from "@/components/ui/button"; import { @@ -12,7 +13,7 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { toNumber } from "@/lib/tsrs-utils"; -import { useInstancesStore } from "@/stores/instances-store"; +import { useInstancesStore } from "@/models/instances"; import type { Instance } from "../types/bindings/instance"; export function InstancesView() { @@ -31,19 +32,14 @@ export function InstancesView() { const [editingInstance, setEditingInstance] = useState<Instance | null>(null); // Form fields - const [newInstanceName, setNewInstanceName] = useState(""); const [duplicateName, setDuplicateName] = useState(""); - // Load instances on mount (matches Svelte onMount behavior) useEffect(() => { - instancesStore.loadInstances(); - // instancesStore methods are stable (Zustand); do not add to deps to avoid spurious runs - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [instancesStore.loadInstances]); + instancesStore.refresh(); + }, [instancesStore.refresh]); // Handlers to open modals const openCreate = () => { - setNewInstanceName(""); setShowCreateModal(true); }; @@ -63,25 +59,9 @@ export function InstancesView() { setShowDuplicateModal(true); }; - // Confirm actions - const confirmCreate = async () => { - const name = newInstanceName.trim(); - if (!name) return; - await instancesStore.createInstance(name); - setShowCreateModal(false); - setNewInstanceName(""); - }; - - const confirmEdit = async () => { - if (!editingInstance) return; - await instancesStore.updateInstance(editingInstance); - setEditingInstance(null); - setShowEditModal(false); - }; - const confirmDelete = async () => { if (!selectedInstance) return; - await instancesStore.deleteInstance(selectedInstance.id); + await instancesStore.delete(selectedInstance.id); setSelectedInstance(null); setShowDeleteConfirm(false); }; @@ -90,16 +70,12 @@ export function InstancesView() { if (!selectedInstance) return; const name = duplicateName.trim(); if (!name) return; - await instancesStore.duplicateInstance(selectedInstance.id, name); + await instancesStore.duplicate(selectedInstance.id, name); setSelectedInstance(null); setDuplicateName(""); setShowDuplicateModal(false); }; - const setActiveInstance = async (id: string) => { - await instancesStore.setActiveInstance(id); - }; - const formatDate = (timestamp: number): string => new Date(timestamp * 1000).toLocaleDateString(); @@ -124,7 +100,7 @@ export function InstancesView() { <Button type="button" onClick={openCreate} - className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors" + className="px-4 py-2 transition-colors" > <Plus size={18} /> Create Instance @@ -141,16 +117,17 @@ export function InstancesView() { ) : ( <ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> {instancesStore.instances.map((instance) => { - const isActive = instancesStore.activeInstanceId === instance.id; + const isActive = instancesStore.activeInstance?.id === instance.id; return ( <li key={instance.id} - onClick={() => setActiveInstance(instance.id)} + onClick={() => instancesStore.setActiveInstance(instance)} onKeyDown={(e) => - e.key === "Enter" && setActiveInstance(instance.id) + e.key === "Enter" && + instancesStore.setActiveInstance(instance) } - className={`relative p-4 text-left rounded-lg border-2 transition-all cursor-pointer hover:border-blue-500 ${ + className={`relative p-4 text-left border-2 transition-all cursor-pointer hover:border-blue-500 ${ isActive ? "border-blue-500" : "border-transparent" } bg-gray-100 dark:bg-gray-800`} > @@ -245,42 +222,10 @@ export function InstancesView() { </ul> )} - {/* Create Modal */} - <Dialog open={showCreateModal} onOpenChange={setShowCreateModal}> - <DialogContent> - <DialogHeader> - <DialogTitle>Create Instance</DialogTitle> - <DialogDescription> - Enter a name for the new instance. - </DialogDescription> - </DialogHeader> - - <div className="mt-4"> - <Input - value={newInstanceName} - onChange={(e) => setNewInstanceName(e.target.value)} - placeholder="Instance name" - /> - </div> - - <DialogFooter> - <Button - type="button" - variant="outline" - onClick={() => setShowCreateModal(false)} - > - Cancel - </Button> - <Button - type="button" - onClick={confirmCreate} - disabled={!newInstanceName.trim()} - > - Create - </Button> - </DialogFooter> - </DialogContent> - </Dialog> + <InstanceCreationModal + open={showCreateModal} + onOpenChange={setShowCreateModal} + /> <InstanceEditorModal open={showEditModal} diff --git a/packages/ui-new/src/pages/settings-view.tsx b/packages/ui-new/src/pages/settings-view.tsx.bk index ac43d9b..ac43d9b 100644 --- a/packages/ui-new/src/pages/settings-view.tsx +++ b/packages/ui-new/src/pages/settings-view.tsx.bk diff --git a/packages/ui-new/src/pages/versions-view.tsx b/packages/ui-new/src/pages/versions-view.tsx.bk index 7f44611..d54596d 100644 --- a/packages/ui-new/src/pages/versions-view.tsx +++ b/packages/ui-new/src/pages/versions-view.tsx.bk @@ -17,8 +17,8 @@ import { import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { useInstancesStore } from "../models/instances"; import { useGameStore } from "../stores/game-store"; -import { useInstancesStore } from "../stores/instances-store"; import type { Version } from "../types/bindings/manifest"; interface InstalledModdedVersion { @@ -31,7 +31,7 @@ type TypeFilter = "all" | "release" | "snapshot" | "installed"; export function VersionsView() { const { versions, selectedVersion, loadVersions, setSelectedVersion } = useGameStore(); - const { activeInstanceId } = useInstancesStore(); + const { activeInstance } = useInstancesStore(); const [searchQuery, setSearchQuery] = useState(""); const [typeFilter, setTypeFilter] = useState<TypeFilter>("all"); @@ -54,7 +54,7 @@ export function VersionsView() { // Load installed modded versions with Java version info const loadInstalledModdedVersions = useCallback(async () => { - if (!activeInstanceId) { + if (!activeInstance) { setInstalledModdedVersions([]); setIsLoadingModded(false); return; diff --git a/packages/ui-new/src/stores/game-store.ts b/packages/ui-new/src/stores/game-store.ts index 541b386..fa0f9f8 100644 --- a/packages/ui-new/src/stores/game-store.ts +++ b/packages/ui-new/src/stores/game-store.ts @@ -1,6 +1,6 @@ -import { invoke } from "@tauri-apps/api/core"; import { toast } from "sonner"; import { create } from "zustand"; +import { getVersions } from "@/client"; import type { Version } from "@/types/bindings/manifest"; interface GameState { @@ -39,7 +39,7 @@ export const useGameStore = create<GameState>((set, get) => ({ try { // Ask the backend for known versions (optionally scoped to an instance). // The Tauri command `get_versions` is expected to return an array of `Version`. - const versions = await invoke<Version[]>("get_versions", { instanceId }); + const versions = await getVersions(); set({ versions: versions ?? [] }); } catch (e) { console.error("Failed to load versions:", e); diff --git a/packages/ui-new/src/stores/instances-store.ts b/packages/ui-new/src/stores/instances-store.ts deleted file mode 100644 index 4636b79..0000000 --- a/packages/ui-new/src/stores/instances-store.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import { toast } from "sonner"; -import { create } from "zustand"; -import type { Instance } from "../types/bindings/instance"; - -interface InstancesState { - // State - instances: Instance[]; - activeInstanceId: string | null; - - // Computed property - activeInstance: Instance | null; - - // Actions - loadInstances: () => Promise<void>; - createInstance: (name: string) => Promise<Instance | null>; - deleteInstance: (id: string) => Promise<void>; - updateInstance: (instance: Instance) => Promise<void>; - setActiveInstance: (id: string) => Promise<void>; - duplicateInstance: (id: string, newName: string) => Promise<Instance | null>; - getInstance: (id: string) => Promise<Instance | null>; - setInstances: (instances: Instance[]) => void; - setActiveInstanceId: (id: string | null) => void; -} - -export const useInstancesStore = create<InstancesState>((set, get) => ({ - // Initial state - instances: [], - activeInstanceId: null, - - // Computed property - get activeInstance() { - const { instances, activeInstanceId } = get(); - if (!activeInstanceId) return null; - return instances.find((i) => i.id === activeInstanceId) || null; - }, - - // Actions - loadInstances: async () => { - try { - const instances = await invoke<Instance[]>("list_instances"); - const active = await invoke<Instance | null>("get_active_instance"); - - let newActiveInstanceId = null; - if (active) { - newActiveInstanceId = active.id; - } else if (instances.length > 0) { - // If no active instance but instances exist, set the first one as active - await get().setActiveInstance(instances[0].id); - newActiveInstanceId = instances[0].id; - } - - set({ instances, activeInstanceId: newActiveInstanceId }); - } catch (e) { - console.error("Failed to load instances:", e); - toast.error("Error loading instances: " + String(e)); - } - }, - - createInstance: async (name) => { - try { - const instance = await invoke<Instance>("create_instance", { name }); - await get().loadInstances(); - toast.success(`Instance "${name}" created successfully`); - return instance; - } catch (e) { - console.error("Failed to create instance:", e); - toast.error("Error creating instance: " + String(e)); - return null; - } - }, - - deleteInstance: async (id) => { - try { - await invoke("delete_instance", { instanceId: id }); - await get().loadInstances(); - - // If deleted instance was active, set another as active - const { instances, activeInstanceId } = get(); - if (activeInstanceId === id) { - if (instances.length > 0) { - await get().setActiveInstance(instances[0].id); - } else { - set({ activeInstanceId: null }); - } - } - - toast.success("Instance deleted successfully"); - } catch (e) { - console.error("Failed to delete instance:", e); - toast.error("Error deleting instance: " + String(e)); - } - }, - - updateInstance: async (instance) => { - try { - await invoke("update_instance", { instance }); - await get().loadInstances(); - toast.success("Instance updated successfully"); - } catch (e) { - console.error("Failed to update instance:", e); - toast.error("Error updating instance: " + String(e)); - } - }, - - setActiveInstance: async (id) => { - try { - await invoke("set_active_instance", { instanceId: id }); - set({ activeInstanceId: id }); - toast.success("Active instance changed"); - } catch (e) { - console.error("Failed to set active instance:", e); - toast.error("Error setting active instance: " + String(e)); - } - }, - - duplicateInstance: async (id, newName) => { - try { - const instance = await invoke<Instance>("duplicate_instance", { - instanceId: id, - newName, - }); - await get().loadInstances(); - toast.success(`Instance duplicated as "${newName}"`); - return instance; - } catch (e) { - console.error("Failed to duplicate instance:", e); - toast.error("Error duplicating instance: " + String(e)); - return null; - } - }, - - getInstance: async (id) => { - try { - return await invoke<Instance>("get_instance", { instanceId: id }); - } catch (e) { - console.error("Failed to get instance:", e); - return null; - } - }, - - setInstances: (instances) => { - set({ instances }); - }, - - setActiveInstanceId: (id) => { - set({ activeInstanceId: id }); - }, -})); diff --git a/packages/ui-new/src/stores/settings-store.ts b/packages/ui-new/src/stores/settings-store.ts index 52da7fd..0bfc1e1 100644 --- a/packages/ui-new/src/stores/settings-store.ts +++ b/packages/ui-new/src/stores/settings-store.ts @@ -2,6 +2,7 @@ import { convertFileSrc, invoke } from "@tauri-apps/api/core"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import { toast } from "sonner"; import { create } from "zustand"; +import { downloadAdoptiumJava } from "@/client"; import type { ModelInfo } from "../types/bindings/assistant"; import type { LauncherConfig } from "../types/bindings/config"; import type { @@ -10,7 +11,6 @@ import type { } from "../types/bindings/downloader"; import type { JavaCatalog, - JavaDownloadInfo, JavaInstallation, JavaReleaseInfo, } from "../types/bindings/java"; @@ -445,13 +445,13 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({ if (!selectedMajorVersion) return; set({ isDownloadingJava: true, javaDownloadStatus: "Starting..." }); try { - const result = await invoke<JavaDownloadInfo>("download_java", { - majorVersion: selectedMajorVersion, - imageType: selectedImageType, - source: selectedDownloadSource, - }); + const result = await downloadAdoptiumJava( + selectedMajorVersion, + selectedImageType, + selectedDownloadSource, + ); set({ - javaDownloadStatus: `Java ${selectedMajorVersion} download started: ${result.fileName}`, + javaDownloadStatus: `Java ${selectedMajorVersion} download started: ${result.path}`, }); toast.success("Download started"); } catch (e) { diff --git a/packages/ui-new/src/types/bindings/java/core.ts b/packages/ui-new/src/types/bindings/java/core.ts index 8094c71..099dea9 100644 --- a/packages/ui-new/src/types/bindings/java/core.ts +++ b/packages/ui-new/src/types/bindings/java/core.ts @@ -23,7 +23,7 @@ export type JavaInstallation = { arch: string; vendor: string; source: string; - is_64bit: boolean; + is64bit: boolean; }; export type JavaReleaseInfo = { diff --git a/src-tauri/src/core/java/mod.rs b/src-tauri/src/core/java/mod.rs index 9036829..091ad0a 100644 --- a/src-tauri/src/core/java/mod.rs +++ b/src-tauri/src/core/java/mod.rs @@ -33,6 +33,7 @@ use providers::AdoptiumProvider; const CACHE_DURATION_SECS: u64 = 24 * 60 * 60; #[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] #[ts(export, export_to = "java/core.ts")] pub struct JavaInstallation { pub path: String, diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index e51d49f..33c94fe 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -899,7 +899,17 @@ fn parse_jvm_arguments( #[tauri::command] #[dropout_macros::api] -async fn get_versions( +async fn get_versions() -> Result<Vec<core::manifest::Version>, String> { + core::manifest::fetch_version_manifest() + .await + .map(|m| m.versions) + .map_err(|e| e.to_string()) +} + +/// Get all available versions from Mojang's version manifest +#[tauri::command] +#[dropout_macros::api] +async fn get_versions_of_instance( _window: Window, instance_state: State<'_, core::instance::InstanceState>, instance_id: String, @@ -2634,6 +2644,7 @@ fn main() { .invoke_handler(tauri::generate_handler![ start_game, get_versions, + get_versions_of_instance, check_version_installed, install_version, list_installed_versions, |