From 9430bee86fbf943283eb5a6f63bd750b875ff433 Mon Sep 17 00:00:00 2001
From: 苏向夜 <fu050409@163.com>
Date: Fri, 23 Jan 2026 20:51:28 +0800
Subject: feat(ui): add new ui project

---
 packages/ui-new/src/pages/instances-view.tsx | 370 +++++++++++++++++++++++++++
 1 file changed, 370 insertions(+)
 create mode 100644 packages/ui-new/src/pages/instances-view.tsx

(limited to 'packages/ui-new/src/pages/instances-view.tsx')

diff --git a/packages/ui-new/src/pages/instances-view.tsx b/packages/ui-new/src/pages/instances-view.tsx
new file mode 100644
index 0000000..0c511a1
--- /dev/null
+++ b/packages/ui-new/src/pages/instances-view.tsx
@@ -0,0 +1,370 @@
+import { Copy, Edit2, Plus, Trash2 } from "lucide-react";
+import { useEffect, useState } from "react";
+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 { toNumber } from "@/lib/tsrs-utils";
+import { useInstancesStore } from "@/stores/instances-store";
+import type { Instance } from "../types/bindings/instance";
+
+export function InstancesView() {
+  const instancesStore = useInstancesStore();
+
+  // Modal / UI state
+  const [showCreateModal, setShowCreateModal] = useState(false);
+  const [showEditModal, setShowEditModal] = useState(false);
+  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+  const [showDuplicateModal, setShowDuplicateModal] = useState(false);
+
+  // Selected / editing instance state
+  const [selectedInstance, setSelectedInstance] = useState<Instance | null>(
+    null,
+  );
+  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]);
+
+  // Handlers to open modals
+  const openCreate = () => {
+    setNewInstanceName("");
+    setShowCreateModal(true);
+  };
+
+  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);
+  };
+
+  // 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);
+    setSelectedInstance(null);
+    setShowDeleteConfirm(false);
+  };
+
+  const confirmDuplicate = async () => {
+    if (!selectedInstance) return;
+    const name = duplicateName.trim();
+    if (!name) return;
+    await instancesStore.duplicateInstance(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();
+
+  const 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();
+  };
+
+  return (
+    <div className="h-full flex flex-col gap-4 p-6 overflow-y-auto">
+      <div className="flex items-center justify-between">
+        <h1 className="text-2xl font-bold text-gray-900 dark:text-white">
+          Instances
+        </h1>
+        <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"
+        >
+          <Plus size={18} />
+          Create Instance
+        </Button>
+      </div>
+
+      {instancesStore.instances.length === 0 ? (
+        <div className="flex-1 flex items-center justify-center">
+          <div className="text-center text-gray-500 dark:text-gray-400">
+            <p className="text-lg mb-2">No instances yet</p>
+            <p className="text-sm">Create your first instance to get started</p>
+          </div>
+        </div>
+      ) : (
+        <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;
+
+            return (
+              <li
+                key={instance.id}
+                onClick={() => setActiveInstance(instance.id)}
+                onKeyDown={(e) =>
+                  e.key === "Enter" && setActiveInstance(instance.id)
+                }
+                className={`relative p-4 text-left rounded-lg border-2 transition-all cursor-pointer hover:border-blue-500 ${
+                  isActive ? "border-blue-500" : "border-transparent"
+                } bg-gray-100 dark:bg-gray-800`}
+              >
+                {/* Instance Icon */}
+                {instance.iconPath ? (
+                  <div className="w-12 h-12 mb-3 rounded overflow-hidden">
+                    <img
+                      src={instance.iconPath}
+                      alt={instance.name}
+                      className="w-full h-full object-cover"
+                    />
+                  </div>
+                ) : (
+                  <div className="w-12 h-12 mb-3 rounded bg-linear-to-br from-blue-500 to-purple-600 flex items-center justify-center">
+                    <span className="text-white font-bold text-lg">
+                      {instance.name.charAt(0).toUpperCase()}
+                    </span>
+                  </div>
+                )}
+
+                <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-1">
+                  {instance.name}
+                </h3>
+
+                <div className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
+                  {instance.versionId ? (
+                    <p className="truncate">Version: {instance.versionId}</p>
+                  ) : (
+                    <p className="text-gray-400">No version selected</p>
+                  )}
+
+                  {instance.modLoader && (
+                    <p className="truncate">
+                      Mod Loader:{" "}
+                      <span className="capitalize">{instance.modLoader}</span>
+                    </p>
+                  )}
+
+                  <p className="truncate">
+                    Created: {formatDate(toNumber(instance.createdAt))}
+                  </p>
+
+                  {instance.lastPlayed && (
+                    <p className="truncate">
+                      Last played:{" "}
+                      {formatLastPlayed(toNumber(instance.lastPlayed))}
+                    </p>
+                  )}
+                </div>
+
+                {/* Action Buttons */}
+                <div className="mt-4 flex gap-2">
+                  <button
+                    type="button"
+                    onClick={(e) => {
+                      e.stopPropagation();
+                      openEdit(instance);
+                    }}
+                    className="flex-1 flex items-center justify-center gap-1 px-3 py-1.5 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded text-sm transition-colors"
+                  >
+                    <Edit2 size={14} />
+                    Edit
+                  </button>
+
+                  <button
+                    type="button"
+                    onClick={(e) => {
+                      e.stopPropagation();
+                      openDuplicate(instance);
+                    }}
+                    className="flex-1 flex items-center justify-center gap-1 px-3 py-1.5 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded text-sm transition-colors"
+                  >
+                    <Copy size={14} />
+                    Duplicate
+                  </button>
+
+                  <button
+                    type="button"
+                    onClick={(e) => {
+                      e.stopPropagation();
+                      openDelete(instance);
+                    }}
+                    className="flex-1 flex items-center justify-center gap-1 px-3 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded text-sm transition-colors"
+                  >
+                    <Trash2 size={14} />
+                    Delete
+                  </button>
+                </div>
+              </li>
+            );
+          })}
+        </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>
+
+      <InstanceEditorModal
+        open={showEditModal}
+        instance={editingInstance}
+        onOpenChange={(open) => {
+          setShowEditModal(open);
+          if (!open) setEditingInstance(null);
+        }}
+      />
+
+      {/* Delete Confirmation */}
+      <Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
+        <DialogContent>
+          <DialogHeader>
+            <DialogTitle>Delete Instance</DialogTitle>
+            <DialogDescription>
+              Are you sure you want to delete "{selectedInstance?.name}"? This
+              action cannot be undone.
+            </DialogDescription>
+          </DialogHeader>
+
+          <DialogFooter>
+            <Button
+              type="button"
+              variant="outline"
+              onClick={() => {
+                setShowDeleteConfirm(false);
+                setSelectedInstance(null);
+              }}
+            >
+              Cancel
+            </Button>
+            <Button
+              type="button"
+              onClick={confirmDelete}
+              className="bg-red-600 text-white hover:bg-red-500"
+            >
+              Delete
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+
+      {/* Duplicate Modal */}
+      <Dialog open={showDuplicateModal} onOpenChange={setShowDuplicateModal}>
+        <DialogContent>
+          <DialogHeader>
+            <DialogTitle>Duplicate Instance</DialogTitle>
+            <DialogDescription>
+              Provide a name for the duplicated instance.
+            </DialogDescription>
+          </DialogHeader>
+
+          <div className="mt-4">
+            <Input
+              value={duplicateName}
+              onChange={(e) => setDuplicateName(e.target.value)}
+              placeholder="New instance name"
+              onKeyDown={(e) => e.key === "Enter" && confirmDuplicate()}
+            />
+          </div>
+
+          <DialogFooter>
+            <Button
+              type="button"
+              variant="outline"
+              onClick={() => {
+                setShowDuplicateModal(false);
+                setSelectedInstance(null);
+                setDuplicateName("");
+              }}
+            >
+              Cancel
+            </Button>
+            <Button
+              type="button"
+              onClick={confirmDuplicate}
+              disabled={!duplicateName.trim()}
+            >
+              Duplicate
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+}
-- 
cgit v1.2.3-70-g09d2

