aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/ui/src/components/bottom-bar.tsx31
-rw-r--r--packages/ui/src/models/game.ts113
-rw-r--r--packages/ui/src/pages/home.tsx (renamed from packages/ui/src/pages/home-view.tsx)2
-rw-r--r--packages/ui/src/pages/index.tsx12
-rw-r--r--packages/ui/src/pages/instances-view.tsx457
-rw-r--r--packages/ui/src/pages/routes.ts6
-rw-r--r--packages/ui/src/stores/game-store.ts184
7 files changed, 130 insertions, 675 deletions
diff --git a/packages/ui/src/components/bottom-bar.tsx b/packages/ui/src/components/bottom-bar.tsx
index 2746e00..fd4a681 100644
--- a/packages/ui/src/components/bottom-bar.tsx
+++ b/packages/ui/src/components/bottom-bar.tsx
@@ -3,8 +3,8 @@ import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { useAuthStore } from "@/models/auth";
+import { useGameStore } from "@/models/game";
import { useInstanceStore } from "@/models/instance";
-import { useGameStore } from "@/stores/game-store";
import { LoginModal } from "./login-modal";
import { Button } from "./ui/button";
import {
@@ -19,21 +19,17 @@ import { Spinner } from "./ui/spinner";
export function BottomBar() {
const account = useAuthStore((state) => state.account);
- const instances = useInstanceStore((state) => state.instances);
- const activeInstance = useInstanceStore((state) => state.activeInstance);
- const setActiveInstance = useInstanceStore(
- (state) => state.setActiveInstance,
- );
- const selectedVersion = useGameStore((state) => state.selectedVersion);
- const setSelectedVersion = useGameStore((state) => state.setSelectedVersion);
- 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 { instances, activeInstance, setActiveInstance } = useInstanceStore();
+ const {
+ runningInstanceId,
+ launchingInstanceId,
+ stoppingInstanceId,
+ startGame,
+ stopGame,
+ } = useGameStore();
+
+ const [selectedVersion, setSelectedVersion] = useState<string | null>(null);
const [showLoginModal, setShowLoginModal] = useState(false);
useEffect(() => {
@@ -43,7 +39,7 @@ export function BottomBar() {
}
setSelectedVersion(nextVersion);
- }, [activeInstance?.versionId, selectedVersion, setSelectedVersion]);
+ }, [activeInstance?.versionId, selectedVersion]);
const handleInstanceChange = useCallback(
async (instanceId: string) => {
@@ -75,11 +71,8 @@ export function BottomBar() {
}
await startGame(
- account,
- () => setShowLoginModal(true),
activeInstance.id,
selectedVersion || activeInstance.versionId,
- () => undefined,
);
};
diff --git a/packages/ui/src/models/game.ts b/packages/ui/src/models/game.ts
new file mode 100644
index 0000000..5342078
--- /dev/null
+++ b/packages/ui/src/models/game.ts
@@ -0,0 +1,113 @@
+import { listen, type UnlistenFn } from "@tauri-apps/api/event";
+import { toast } from "sonner";
+import { create } from "zustand";
+import {
+ startGame as startGameCommand,
+ stopGame as stopGameCommand,
+} from "@/client";
+import type { GameExitedEvent } from "@/types/bindings/core";
+
+interface GameState {
+ runningInstanceId: string | null;
+ runningVersionId: string | null;
+ launchingInstanceId: string | null;
+ stoppingInstanceId: string | null;
+ lifecycleUnlisten: UnlistenFn | null;
+
+ isGameRunning: boolean;
+ startGame: (instanceId: string, versionId: string) => Promise<string | null>;
+ stopGame: (instanceId?: string | null) => Promise<string | null>;
+}
+
+export const useGameStore = create<GameState>((set, get) => ({
+ runningInstanceId: null,
+ runningVersionId: null,
+ launchingInstanceId: null,
+ stoppingInstanceId: null,
+ lifecycleUnlisten: null,
+
+ get isGameRunning() {
+ return get().runningInstanceId !== null;
+ },
+
+ startGame: async (instanceId, versionId) => {
+ const { isGameRunning, lifecycleUnlisten } = get();
+
+ if (isGameRunning) {
+ toast.info("A game is already running");
+ return null;
+ } else {
+ lifecycleUnlisten?.();
+ }
+
+ set({
+ launchingInstanceId: instanceId,
+ });
+ toast.info(`Preparing to launch ${versionId}...`);
+
+ const unlisten = await listen<GameExitedEvent>("game-exited", (event) => {
+ const { instanceId, versionId, wasStopped, exitCode } = event.payload;
+
+ set({
+ runningInstanceId: null,
+ runningVersionId: null,
+ launchingInstanceId: null,
+ stoppingInstanceId: null,
+ });
+
+ if (wasStopped) {
+ toast.success(
+ `Stopped Minecraft ${versionId} for instance ${instanceId}`,
+ );
+ } else {
+ toast.info(
+ `Minecraft ${versionId} exited with code ${exitCode} for instance ${instanceId}`,
+ );
+ }
+ });
+
+ set({ lifecycleUnlisten: unlisten });
+
+ try {
+ const message = await startGameCommand(instanceId, versionId);
+ set({
+ launchingInstanceId: null,
+ runningInstanceId: instanceId,
+ runningVersionId: versionId,
+ });
+ toast.success(message);
+ return message;
+ } catch (e) {
+ console.error(e);
+ set({ launchingInstanceId: null });
+ toast.error(`Error: ${e}`);
+ return null;
+ }
+ },
+
+ stopGame: async (instanceId) => {
+ const { runningInstanceId } = get();
+
+ if (!runningInstanceId) {
+ toast.info("No running game found");
+ return null;
+ }
+
+ if (instanceId !== runningInstanceId) {
+ toast.info("That instance is not the one currently running");
+ return null;
+ }
+
+ set({ stoppingInstanceId: runningInstanceId });
+
+ try {
+ return await stopGameCommand();
+ } catch (e) {
+ console.error("Failed to stop game:", e);
+ toast.error(`Failed to stop game: ${e}`);
+ return null;
+ } finally {
+ set({ stoppingInstanceId: null });
+ }
+ },
+}));
diff --git a/packages/ui/src/pages/home-view.tsx b/packages/ui/src/pages/home.tsx
index da7238f..dc1413d 100644
--- a/packages/ui/src/pages/home-view.tsx
+++ b/packages/ui/src/pages/home.tsx
@@ -2,7 +2,7 @@ import { useState } from "react";
import { BottomBar } from "@/components/bottom-bar";
import { useSaturnEffect } from "@/components/particle-background";
-export function HomeView() {
+export function HomePage() {
const [mouseX, setMouseX] = useState(0);
const [mouseY, setMouseY] = useState(0);
const saturn = useSaturnEffect();
diff --git a/packages/ui/src/pages/index.tsx b/packages/ui/src/pages/index.tsx
index 2acd377..d12646b 100644
--- a/packages/ui/src/pages/index.tsx
+++ b/packages/ui/src/pages/index.tsx
@@ -5,13 +5,11 @@ import { Sidebar } from "@/components/sidebar";
import { useAuthStore } from "@/models/auth";
import { useInstanceStore } from "@/models/instance";
import { useSettingsStore } from "@/models/settings";
-import { useGameStore } from "@/stores/game-store";
export function IndexPage() {
const authStore = useAuthStore();
const settingsStore = useSettingsStore();
const instanceStore = useInstanceStore();
- const initGameLifecycle = useGameStore((state) => state.initLifecycle);
const location = useLocation();
@@ -19,15 +17,7 @@ export function IndexPage() {
authStore.init();
settingsStore.refresh();
instanceStore.refresh();
- void initGameLifecycle().catch((error) => {
- console.error("Failed to initialize game lifecycle:", error);
- });
- }, [
- authStore.init,
- settingsStore.refresh,
- instanceStore.refresh,
- initGameLifecycle,
- ]);
+ }, [authStore.init, settingsStore.refresh, instanceStore.refresh]);
return (
<div className="relative h-screen w-full overflow-hidden bg-background font-sans">
diff --git a/packages/ui/src/pages/instances-view.tsx b/packages/ui/src/pages/instances-view.tsx
deleted file mode 100644
index 7bb3302..0000000
--- a/packages/ui/src/pages/instances-view.tsx
+++ /dev/null
@@ -1,457 +0,0 @@
-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 { openFileExplorer } from "@/client";
-import InstanceCreationModal from "@/components/instance-creation-modal";
-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 { 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);
- 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 [duplicateName, setDuplicateName] = useState("");
-
- useEffect(() => {
- instancesStore.refresh();
- }, [instancesStore.refresh]);
-
- // Handlers to open modals
- const openCreate = () => {
- 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);
- };
-
- 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 (
- <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>
- <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 ? (
- <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="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
- key={instance.id}
- onClick={() => 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"
- >
- <div
- className={cn(
- "flex flex-row space-x-3 p-3 justify-between",
- "border bg-card/5 backdrop-blur-xl",
- "hover:bg-accent/50 transition-colors",
- isActive && "border-primary",
- )}
- >
- <div className="flex flex-row space-x-4">
- {instance.iconPath ? (
- <div className="w-12 h-12 rounded overflow-hidden">
- <img
- src={instance.iconPath}
- alt={instance.name}
- className="w-full h-full object-cover"
- />
- </div>
- ) : (
- <div className="w-12 h-12 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>
- )}
-
- <div className="flex flex-col">
- <h3 className="text-lg font-semibold">{instance.name}</h3>
- {instance.versionId ? (
- <p className="text-sm text-muted-foreground">
- {instance.versionId}
- </p>
- ) : (
- <p className="text-sm text-muted-foreground">
- No version selected
- </p>
- )}
- </div>
- </div>
-
- <div className="flex items-center">
- <div className="flex flex-row space-x-2">
- <Button
- variant={isRunning ? "destructive" : "ghost"}
- size="icon"
- 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;
- }
-
- await startGame(
- account,
- () => {
- toast.info("Please login first");
- },
- instance.id,
- instance.versionId,
- () => undefined,
- );
- }}
- disabled={
- otherInstanceRunning || isLaunching || isStopping
- }
- >
- {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"
- size="icon"
- onClick={(e) => {
- e.stopPropagation();
- openDuplicate(instance);
- }}
- >
- <CopyIcon />
- </Button>
- <Button
- variant="ghost"
- size="icon"
- onClick={(e) => {
- e.stopPropagation();
- openEdit(instance);
- }}
- >
- <EditIcon />
- </Button>
- <Button
- variant="destructive"
- size="icon"
- onClick={(e) => {
- e.stopPropagation();
- openDelete(instance);
- }}
- >
- <Trash2Icon />
- </Button>
- </div>
- </div>
- </div>
- </li>
- );
- })}
- </ul>
- )}
-
- <InstanceCreationModal
- open={showCreateModal}
- onOpenChange={setShowCreateModal}
- />
-
- <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>
- );
-}
diff --git a/packages/ui/src/pages/routes.ts b/packages/ui/src/pages/routes.ts
index 8d105d4..55eb8fd 100644
--- a/packages/ui/src/pages/routes.ts
+++ b/packages/ui/src/pages/routes.ts
@@ -1,6 +1,6 @@
import { createHashRouter } from "react-router";
-import { IndexPage } from ".";
-import { HomeView } from "./home-view";
+import { HomePage } from "./home";
+import { IndexPage } from "./index";
import instanceRoute from "./instances/routes";
import { SettingsPage } from "./settings";
@@ -11,7 +11,7 @@ const router = createHashRouter([
children: [
{
index: true,
- Component: HomeView,
+ Component: HomePage,
},
{
path: "settings",
diff --git a/packages/ui/src/stores/game-store.ts b/packages/ui/src/stores/game-store.ts
deleted file mode 100644
index 7b6e746..0000000
--- a/packages/ui/src/stores/game-store.ts
+++ /dev/null
@@ -1,184 +0,0 @@
-import { listen, type UnlistenFn } from "@tauri-apps/api/event";
-import { toast } from "sonner";
-import { create } from "zustand";
-import {
- getVersions,
- getVersionsOfInstance,
- startGame as startGameCommand,
- stopGame as stopGameCommand,
-} from "@/client";
-import type { Account } from "@/types/bindings/auth";
-import type { GameExitedEvent } from "@/types/bindings/core";
-import type { Version } from "@/types/bindings/manifest";
-
-interface GameState {
- versions: Version[];
- selectedVersion: string;
- runningInstanceId: string | null;
- runningVersionId: string | null;
- launchingInstanceId: string | null;
- stoppingInstanceId: string | null;
- lifecycleUnlisten: UnlistenFn | null;
-
- latestRelease: Version | undefined;
- isGameRunning: boolean;
-
- initLifecycle: () => Promise<void>;
- loadVersions: (instanceId?: string) => Promise<void>;
- startGame: (
- currentAccount: Account | null,
- openLoginModal: () => void,
- activeInstanceId: string | null,
- versionId: string | null,
- setView: (view: string) => void,
- ) => Promise<string | null>;
- stopGame: (instanceId?: string | null) => Promise<string | null>;
- setSelectedVersion: (version: string) => void;
- setVersions: (versions: Version[]) => void;
-}
-
-export const useGameStore = create<GameState>((set, get) => ({
- versions: [],
- selectedVersion: "",
- runningInstanceId: null,
- runningVersionId: null,
- launchingInstanceId: null,
- stoppingInstanceId: null,
- lifecycleUnlisten: null,
-
- get latestRelease() {
- return get().versions.find((v) => v.type === "release");
- },
-
- get isGameRunning() {
- return get().runningInstanceId !== null;
- },
-
- initLifecycle: async () => {
- if (get().lifecycleUnlisten) {
- return;
- }
-
- const unlisten = await listen<GameExitedEvent>("game-exited", (event) => {
- const { instanceId, versionId, wasStopped } = event.payload;
-
- set({
- runningInstanceId: null,
- runningVersionId: null,
- launchingInstanceId: null,
- stoppingInstanceId: null,
- });
-
- if (wasStopped) {
- toast.success(
- `Stopped Minecraft ${versionId} for instance ${instanceId}`,
- );
- } else {
- toast.info(`Minecraft ${versionId} exited for instance ${instanceId}`);
- }
- });
-
- set({ lifecycleUnlisten: unlisten });
- },
-
- loadVersions: async (instanceId?: string) => {
- try {
- const versions = instanceId
- ? await getVersionsOfInstance(instanceId)
- : await getVersions();
- set({ versions: versions ?? [] });
- } catch (e) {
- console.error("Failed to load versions:", e);
- set({ versions: [] });
- }
- },
-
- startGame: async (
- currentAccount,
- openLoginModal,
- activeInstanceId,
- versionId,
- setView,
- ) => {
- const { isGameRunning } = get();
- const targetVersion = versionId ?? get().selectedVersion;
-
- if (!currentAccount) {
- toast.info("Please login first");
- openLoginModal();
- return null;
- }
-
- if (!targetVersion) {
- toast.info("Please select a version first");
- return null;
- }
-
- if (!activeInstanceId) {
- toast.info("Please select an instance first");
- setView("instances");
- return null;
- }
-
- if (isGameRunning) {
- toast.info("A game is already running");
- return null;
- }
-
- set({
- launchingInstanceId: activeInstanceId,
- selectedVersion: targetVersion,
- });
- toast.info(`Preparing to launch ${targetVersion}...`);
-
- try {
- const message = await startGameCommand(activeInstanceId, targetVersion);
- set({
- launchingInstanceId: null,
- runningInstanceId: activeInstanceId,
- runningVersionId: targetVersion,
- });
- toast.success(message);
- return message;
- } catch (e) {
- console.error(e);
- set({ launchingInstanceId: null });
- toast.error(`Error: ${e}`);
- return null;
- }
- },
-
- stopGame: async (instanceId) => {
- const { runningInstanceId } = get();
-
- if (!runningInstanceId) {
- toast.info("No running game found");
- return null;
- }
-
- if (instanceId && instanceId !== runningInstanceId) {
- toast.info("That instance is not the one currently running");
- return null;
- }
-
- set({ stoppingInstanceId: runningInstanceId });
-
- try {
- return await stopGameCommand();
- } catch (e) {
- console.error("Failed to stop game:", e);
- toast.error(`Failed to stop game: ${e}`);
- return null;
- } finally {
- set({ stoppingInstanceId: null });
- }
- },
-
- setSelectedVersion: (version: string) => {
- set({ selectedVersion: version });
- },
-
- setVersions: (versions: Version[]) => {
- set({ versions });
- },
-}));