diff options
| author | 2026-03-26 09:02:10 +0800 | |
|---|---|---|
| committer | 2026-03-26 09:02:10 +0800 | |
| commit | 94b0d8e208363c802c12b56d8bdbef574dd1fb91 (patch) | |
| tree | e86c8d46e73262c67c1755aaf4202cbcd1f8f844 /packages/ui/src/pages/instances-view.tsx | |
| parent | 7d0e92e6d3b172adfe552ffae9b97f8dad6f63ae (diff) | |
| parent | 3a31d3004b2814cd8a26d49a0f8a96636411dcd2 (diff) | |
| download | DropOut-94b0d8e208363c802c12b56d8bdbef574dd1fb91.tar.gz DropOut-94b0d8e208363c802c12b56d8bdbef574dd1fb91.zip | |
Add game lifecycle management and instance import/export tools (#117)
## Summary by Sourcery
Add centralized game process and instance lifecycle management, shared
cache-aware path resolution, and instance import/export/repair
capabilities across backend and UI.
New Features:
- Track a single running game process in the backend, expose stop-game
control, and emit structured game-exited events with instance and
version context.
- Introduce instance path resolution that supports shared caches for
versions, libraries, and assets, and use it across game start, install,
and version management APIs.
- Add import, export, and repair operations for instances, including
zip-based archive support and automatic recovery of on-disk instances.
- Expose new instance lifecycle and repair APIs to the frontend and wire
them through the client and instance store.
- Add per-instance start/stop controls in the instances view and
instance selection in the bottom bar for launching games.
Enhancements:
- Guard instance operations with per-instance locks and track active
operations such as launch, install, delete, and import/export.
- Improve handling of Microsoft login errors and polling status, with
clearer user feedback and safer interval management.
- Simplify config mutation during shared cache migration and centralize
instance directory resolution in the backend.
- Initialize a game lifecycle listener at app startup to keep UI state
in sync with backend game exit events.
Build:
- Configure the Vite dev server to use a fixed localhost host and port
for the UI dev environment.
Diffstat (limited to 'packages/ui/src/pages/instances-view.tsx')
| -rw-r--r-- | packages/ui/src/pages/instances-view.tsx | 174 |
1 files changed, 155 insertions, 19 deletions
diff --git a/packages/ui/src/pages/instances-view.tsx b/packages/ui/src/pages/instances-view.tsx index e99004c..07a2135 100644 --- a/packages/ui/src/pages/instances-view.tsx +++ b/packages/ui/src/pages/instances-view.tsx @@ -1,7 +1,16 @@ -import { CopyIcon, EditIcon, Plus, RocketIcon, Trash2Icon } from "lucide-react"; +import { open, save } from "@tauri-apps/plugin-dialog"; +import { + CopyIcon, + EditIcon, + FolderOpenIcon, + Plus, + RocketIcon, + Trash2Icon, + XIcon, +} from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; -import { startGame } from "@/client"; +import { openFileExplorer } from "@/client"; import InstanceCreationModal from "@/components/instance-creation-modal"; import InstanceEditorModal from "@/components/instance-editor-modal"; import { Button } from "@/components/ui/button"; @@ -15,11 +24,22 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; +import { useAuthStore } from "@/models/auth"; import { useInstanceStore } from "@/models/instance"; +import { useGameStore } from "@/stores/game-store"; import type { Instance } from "@/types"; export function InstancesView() { + const account = useAuthStore((state) => state.account); const instancesStore = useInstanceStore(); + const startGame = useGameStore((state) => state.startGame); + const stopGame = useGameStore((state) => state.stopGame); + const runningInstanceId = useGameStore((state) => state.runningInstanceId); + const launchingInstanceId = useGameStore((state) => state.launchingInstanceId); + const stoppingInstanceId = useGameStore((state) => state.stoppingInstanceId); + const [isImporting, setIsImporting] = useState(false); + const [repairing, setRepairing] = useState(false); + const [exportingId, setExportingId] = useState<string | null>(null); // Modal / UI state const [showCreateModal, setShowCreateModal] = useState(false); @@ -78,20 +98,83 @@ export function InstancesView() { 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 ( <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="px-4 py-2 transition-colors" - > - <Plus size={18} /> - Create Instance - </Button> + <div className="flex items-center gap-2"> + <Button + type="button" + variant="outline" + onClick={handleImport} + disabled={isImporting} + > + {isImporting ? "Importing..." : "Import"} + </Button> + <Button + type="button" + variant="outline" + onClick={handleRepair} + disabled={repairing} + > + {repairing ? "Repairing..." : "Repair Index"} + </Button> + <Button + type="button" + onClick={openCreate} + className="px-4 py-2 transition-colors" + > + <Plus size={18} /> + Create Instance + </Button> + </div> </div> {instancesStore.instances.length === 0 ? ( @@ -105,6 +188,10 @@ export function InstancesView() { <ul className="flex flex-col space-y-3"> {instancesStore.instances.map((instance) => { const isActive = instancesStore.activeInstance?.id === instance.id; + const isRunning = runningInstanceId === instance.id; + const isLaunching = launchingInstanceId === instance.id; + const isStopping = stoppingInstanceId === instance.id; + const otherInstanceRunning = runningInstanceId !== null && !isRunning; return ( <li @@ -164,22 +251,71 @@ export function InstancesView() { <div className="flex items-center"> <div className="flex flex-row space-x-2"> <Button - variant="ghost" + variant={isRunning ? "destructive" : "ghost"} size="icon" - onClick={async () => { + onClick={async (e) => { + e.stopPropagation(); + + try { + await instancesStore.setActiveInstance(instance); + } catch (error) { + console.error("Failed to set active instance:", error); + toast.error("Error setting active instance"); + return; + } + + if (isRunning) { + await stopGame(instance.id); + return; + } + if (!instance.versionId) { toast.error("No version selected or installed"); return; } - try { - await startGame(instance.id, instance.versionId); - } catch (e) { - console.error("Failed to start game:", e); - toast.error("Error starting game"); - } + + await startGame( + account, + () => { + toast.info("Please login first"); + }, + instance.id, + instance.versionId, + () => undefined, + ); }} + disabled={otherInstanceRunning || isLaunching || isStopping} > - <RocketIcon /> + {isLaunching || isStopping ? ( + <span className="text-xs">...</span> + ) : isRunning ? ( + <XIcon /> + ) : ( + <RocketIcon /> + )} + </Button> + <Button + variant="ghost" + size="icon" + onClick={(e) => { + e.stopPropagation(); + void openFileExplorer(instance.gameDir); + }} + > + <FolderOpenIcon /> + </Button> + <Button + variant="ghost" + size="icon" + onClick={(e) => { + e.stopPropagation(); + void handleExport(instance); + }} + disabled={exportingId === instance.id} + > + <span className="text-xs"> + {exportingId === instance.id ? "..." : "ZIP"} + </span> </Button> <Button variant="ghost" |