From 5b799a125a970e5e56f29a08b3c86450855fb6c4 Mon Sep 17 00:00:00 2001 From: 苏向夜 Date: Sun, 29 Mar 2026 21:32:54 +0800 Subject: refactor(ui): rewrite instance create --- packages/ui/src/pages/instances/index.tsx | 462 ++++++++++++++++++++++++++++++ 1 file changed, 462 insertions(+) create mode 100644 packages/ui/src/pages/instances/index.tsx (limited to 'packages/ui/src/pages/instances/index.tsx') diff --git a/packages/ui/src/pages/instances/index.tsx b/packages/ui/src/pages/instances/index.tsx new file mode 100644 index 0000000..e6cd734 --- /dev/null +++ b/packages/ui/src/pages/instances/index.tsx @@ -0,0 +1,462 @@ +import { open, save } from "@tauri-apps/plugin-dialog"; +import { + CopyIcon, + EditIcon, + EllipsisIcon, + FolderOpenIcon, + Plus, + RocketIcon, + Trash2Icon, + XIcon, +} from "lucide-react"; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router"; +import { toast } from "sonner"; +import { openFileExplorer } from "@/client"; +import InstanceEditorModal from "@/components/instance-editor-modal"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; +import { useAuthStore } from "@/models/auth"; +import { useGameStore } from "@/models/game"; +import { useInstanceStore } from "@/models/instance"; +import type { Instance } from "@/types"; + +export function InstancesPage() { + const instancesStore = useInstanceStore(); + const navigate = useNavigate(); + + const account = useAuthStore((state) => state.account); + const { + startGame, + runningInstanceId, + stoppingInstanceId, + launchingInstanceId, + stopGame, + } = useGameStore(); + + const [showEditModal, setShowEditModal] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [showDuplicateModal, setShowDuplicateModal] = useState(false); + + const [isImporting, setIsImporting] = useState(false); + const [repairing, setRepairing] = useState(false); + const [exportingId, setExportingId] = useState(null); + + // Selected / editing instance state + const [selectedInstance, setSelectedInstance] = useState( + null, + ); + const [editingInstance, setEditingInstance] = useState(null); + + // Form fields + const [duplicateName, setDuplicateName] = useState(""); + + useEffect(() => { + instancesStore.refresh(); + }, [instancesStore.refresh]); + + // Handlers to open modals + const openCreate = () => { + navigate("/instances/create"); + }; + + const openEdit = (instance: Instance) => { + setEditingInstance({ ...instance }); + setShowEditModal(true); + }; + + const openDelete = (instance: Instance) => { + setSelectedInstance(instance); + setShowDeleteConfirm(true); + }; + + const openDuplicate = (instance: Instance) => { + setSelectedInstance(instance); + setDuplicateName(`${instance.name} (Copy)`); + setShowDuplicateModal(true); + }; + + const confirmDelete = async () => { + if (!selectedInstance) return; + await instancesStore.delete(selectedInstance.id); + setSelectedInstance(null); + setShowDeleteConfirm(false); + }; + + const confirmDuplicate = async () => { + if (!selectedInstance) return; + const name = duplicateName.trim(); + if (!name) return; + await instancesStore.duplicate(selectedInstance.id, name); + setSelectedInstance(null); + setDuplicateName(""); + setShowDuplicateModal(false); + }; + + const handleImport = async () => { + setIsImporting(true); + try { + const selected = await open({ + multiple: false, + filters: [{ name: "Zip Archive", extensions: ["zip"] }], + }); + + if (typeof selected !== "string") { + return; + } + + await instancesStore.importArchive(selected); + } finally { + setIsImporting(false); + } + }; + + const handleRepair = async () => { + setRepairing(true); + try { + await instancesStore.repair(); + } finally { + setRepairing(false); + } + }; + + const handleExport = async (instance: Instance) => { + setExportingId(instance.id); + try { + const filePath = await save({ + defaultPath: `${instance.name.replace(/[\\/:*?"<>|]/g, "_")}.zip`, + filters: [{ name: "Zip Archive", extensions: ["zip"] }], + }); + + if (!filePath) { + return; + } + + await instancesStore.exportArchive(instance.id, filePath); + } finally { + setExportingId(null); + } + }; + + return ( +
+
+

+ Instances +

+
+ + + +
+
+ + {instancesStore.instances.length === 0 ? ( +
+
+

No instances yet

+

Create your first instance to get started

+
+
+ ) : ( +
    + {instancesStore.instances.map((instance) => { + const isActive = instancesStore.activeInstance?.id === instance.id; + const isLaunching = launchingInstanceId === instance.id; + const isStopping = stoppingInstanceId === instance.id; + const isRunning = runningInstanceId === instance.id; + + return ( +
  • instancesStore.setActiveInstance(instance)} + onKeyDown={async (e) => { + if (e.key === "Enter") { + try { + await instancesStore.setActiveInstance(instance); + } catch (e) { + console.error("Failed to set active instance:", e); + toast.error("Error setting active instance"); + } + } + }} + className="cursor-pointer" + > +
    +
    + {instance.iconPath ? ( +
    + {instance.name} +
    + ) : ( +
    + + {instance.name.charAt(0).toUpperCase()} + +
    + )} + +
    +

    {instance.name}

    + {instance.versionId ? ( +

    + {instance.versionId} +

    + ) : ( +

    + No version selected +

    + )} +
    +
    + +
    +
    + + + + + + +
    +
    +
    +
  • + ); + })} +
+ )} + + {/**/} + + { + setShowEditModal(open); + if (!open) setEditingInstance(null); + }} + /> + + {/* Delete Confirmation */} + + + + Delete Instance + + Are you sure you want to delete "{selectedInstance?.name}"? This + action cannot be undone. + + + + + + + + + + + {/* Duplicate Modal */} + + + + Duplicate Instance + + Provide a name for the duplicated instance. + + + +
+ setDuplicateName(e.target.value)} + placeholder="New instance name" + onKeyDown={(e) => e.key === "Enter" && confirmDuplicate()} + /> +
+ + + + + +
+
+
+ ); +} -- cgit v1.2.3-70-g09d2