aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/ui/src/models/instance.ts
diff options
context:
space:
mode:
author苏向夜 <46275354+fu050409@users.noreply.github.com>2026-03-26 09:02:10 +0800
committerGitHub <noreply@github.com>2026-03-26 09:02:10 +0800
commit94b0d8e208363c802c12b56d8bdbef574dd1fb91 (patch)
treee86c8d46e73262c67c1755aaf4202cbcd1f8f844 /packages/ui/src/models/instance.ts
parent7d0e92e6d3b172adfe552ffae9b97f8dad6f63ae (diff)
parent3a31d3004b2814cd8a26d49a0f8a96636411dcd2 (diff)
downloadDropOut-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/models/instance.ts')
-rw-r--r--packages/ui/src/models/instance.ts82
1 files changed, 63 insertions, 19 deletions
diff --git a/packages/ui/src/models/instance.ts b/packages/ui/src/models/instance.ts
index b1b463e..e1eb7c1 100644
--- a/packages/ui/src/models/instance.ts
+++ b/packages/ui/src/models/instance.ts
@@ -4,10 +4,13 @@ import {
createInstance,
deleteInstance,
duplicateInstance,
+ exportInstance,
getActiveInstance,
getInstance,
+ importInstance,
listInstances,
- setActiveInstance,
+ repairInstances,
+ setActiveInstance as setActiveInstanceCommand,
updateInstance,
} from "@/client";
import type { Instance } from "@/types";
@@ -22,6 +25,9 @@ interface InstanceState {
update: (instance: Instance) => Promise<void>;
setActiveInstance: (instance: Instance) => Promise<void>;
duplicate: (id: string, newName: string) => Promise<Instance | null>;
+ exportArchive: (id: string, archivePath: string) => Promise<void>;
+ importArchive: (archivePath: string, newName?: string) => Promise<Instance | null>;
+ repair: () => Promise<void>;
get: (id: string) => Promise<Instance | null>;
}
@@ -30,14 +36,20 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
activeInstance: null,
refresh: async () => {
- const { setActiveInstance } = get();
try {
const instances = await listInstances();
- const activeInstance = await getActiveInstance();
+ let activeInstance = await getActiveInstance();
+
+ if (
+ activeInstance &&
+ !instances.some((instance) => instance.id === activeInstance?.id)
+ ) {
+ activeInstance = null;
+ }
if (!activeInstance && instances.length > 0) {
- // If no active instance but instances exist, set the first one as active
- await setActiveInstance(instances[0]);
+ await setActiveInstanceCommand(instances[0].id);
+ activeInstance = instances[0];
}
set({ instances, activeInstance });
@@ -51,35 +63,27 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
const { refresh } = get();
try {
const instance = await createInstance(name);
+ await setActiveInstanceCommand(instance.id);
await refresh();
toast.success(`Instance "${name}" created successfully`);
return instance;
} catch (e) {
console.error("Failed to create instance:", e);
- toast.error("Error creating instance");
+ toast.error(String(e));
return null;
}
},
delete: async (id) => {
- const { refresh, instances, activeInstance, setActiveInstance } = get();
+ const { refresh } = 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");
+ toast.error(String(e));
}
},
@@ -96,7 +100,7 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
},
setActiveInstance: async (instance) => {
- await setActiveInstance(instance.id);
+ await setActiveInstanceCommand(instance.id);
set({ activeInstance: instance });
},
@@ -104,16 +108,56 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
const { refresh } = get();
try {
const instance = await duplicateInstance(id, newName);
+ await setActiveInstanceCommand(instance.id);
await refresh();
toast.success(`Instance duplicated as "${newName}"`);
return instance;
} catch (e) {
console.error("Failed to duplicate instance:", e);
- toast.error("Error duplicating instance");
+ toast.error(String(e));
+ return null;
+ }
+ },
+
+ exportArchive: async (id, archivePath) => {
+ try {
+ await exportInstance(id, archivePath);
+ toast.success("Instance exported successfully");
+ } catch (e) {
+ console.error("Failed to export instance:", e);
+ toast.error(String(e));
+ }
+ },
+
+ importArchive: async (archivePath, newName) => {
+ const { refresh } = get();
+ try {
+ const instance = await importInstance(archivePath, newName);
+ await setActiveInstanceCommand(instance.id);
+ await refresh();
+ toast.success(`Instance "${instance.name}" imported successfully`);
+ return instance;
+ } catch (e) {
+ console.error("Failed to import instance:", e);
+ toast.error(String(e));
return null;
}
},
+ repair: async () => {
+ const { refresh } = get();
+ try {
+ const result = await repairInstances();
+ await refresh();
+ toast.success(
+ `Repair completed: restored ${result.restoredInstances}, removed ${result.removedStaleEntries}`,
+ );
+ } catch (e) {
+ console.error("Failed to repair instances:", e);
+ toast.error(String(e));
+ }
+ },
+
get: async (id) => {
try {
return await getInstance(id);