From 6c78bf95dbe026d875ee4dffed565934d974eb33 Mon Sep 17 00:00:00 2001
From: HsiangNianian <i@jyunko.cn>
Date: Thu, 12 Mar 2026 15:36:13 +0800
Subject: feat: add sync status for bottom bar in the homepage and instances
 page

---
 packages/ui/src/components/bottom-bar.tsx | 248 ++++++++++--------------------
 1 file changed, 85 insertions(+), 163 deletions(-)

(limited to 'packages/ui/src/components/bottom-bar.tsx')

diff --git a/packages/ui/src/components/bottom-bar.tsx b/packages/ui/src/components/bottom-bar.tsx
index 0710c3a..8f70985 100644
--- a/packages/ui/src/components/bottom-bar.tsx
+++ b/packages/ui/src/components/bottom-bar.tsx
@@ -1,11 +1,10 @@
-import { listen, type UnlistenFn } from "@tauri-apps/api/event";
 import { Play, User, XIcon } from "lucide-react";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
 import { toast } from "sonner";
-import { listInstalledVersions, startGame } from "@/client";
 import { cn } from "@/lib/utils";
 import { useAuthStore } from "@/models/auth";
 import { useInstanceStore } from "@/models/instance";
+import { useGameStore } from "@/stores/game-store";
 import { LoginModal } from "./login-modal";
 import { Button } from "./ui/button";
 import {
@@ -18,150 +17,74 @@ import {
 } from "./ui/select";
 import { Spinner } from "./ui/spinner";
 
-interface InstalledVersion {
-  id: string;
-  type: string;
-}
-
 export function BottomBar() {
-  const authStore = useAuthStore();
-  const instancesStore = useInstanceStore();
+  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 [isLaunched, setIsLaunched] = useState<boolean>(false);
-  const gameUnlisten = useRef<UnlistenFn | null>(null);
-  const [isLaunching, setIsLaunching] = useState<boolean>(false);
-  const [selectedVersion, setSelectedVersion] = useState<string | null>(null);
-  const [installedVersions, setInstalledVersions] = useState<
-    InstalledVersion[]
-  >([]);
-  const [isLoadingVersions, setIsLoadingVersions] = useState(true);
   const [showLoginModal, setShowLoginModal] = useState(false);
 
-  const loadInstalledVersions = useCallback(async () => {
-    if (!instancesStore.activeInstance) {
-      setInstalledVersions([]);
-      setIsLoadingVersions(false);
+  useEffect(() => {
+    const nextVersion = activeInstance?.versionId ?? "";
+    if (selectedVersion === nextVersion) {
       return;
     }
 
-    setIsLoadingVersions(true);
-    try {
-      const versions = await listInstalledVersions(
-        instancesStore.activeInstance.id,
-      );
-      setInstalledVersions(versions);
+    setSelectedVersion(nextVersion);
+  }, [activeInstance?.id, activeInstance?.versionId, selectedVersion, setSelectedVersion]);
 
-      // If no version is selected but we have installed versions, select the first one
-      if (!selectedVersion && versions.length > 0) {
-        setSelectedVersion(versions[0].id);
+  const handleInstanceChange = useCallback(
+    async (instanceId: string) => {
+      if (activeInstance?.id === instanceId) {
+        return;
       }
-    } catch (error) {
-      console.error("Failed to load installed versions:", error);
-    } finally {
-      setIsLoadingVersions(false);
-    }
-  }, [instancesStore.activeInstance, selectedVersion]);
 
-  useEffect(() => {
-    loadInstalledVersions();
-
-    // Listen for backend events that should refresh installed versions.
-    let unlistenDownload: UnlistenFn | null = null;
-    let unlistenVersionDeleted: UnlistenFn | null = null;
-
-    (async () => {
-      try {
-        unlistenDownload = await listen("download-complete", () => {
-          loadInstalledVersions();
-        });
-      } catch (err) {
-        // best-effort: do not break UI if listening fails
-        // eslint-disable-next-line no-console
-        console.warn("Failed to attach download-complete listener:", err);
+      const nextInstance = instances.find((instance) => instance.id === instanceId);
+      if (!nextInstance) {
+        return;
       }
 
       try {
-        unlistenVersionDeleted = await listen("version-deleted", () => {
-          loadInstalledVersions();
-        });
-      } catch (err) {
-        // eslint-disable-next-line no-console
-        console.warn("Failed to attach version-deleted listener:", err);
+        await setActiveInstance(nextInstance);
+      } catch (error) {
+        console.error("Failed to activate instance:", error);
+        toast.error(`Failed to activate instance: ${String(error)}`);
       }
-    })();
-
-    return () => {
-      try {
-        if (unlistenDownload) unlistenDownload();
-      } catch {
-        // ignore
-      }
-      try {
-        if (unlistenVersionDeleted) unlistenVersionDeleted();
-      } catch {
-        // ignore
-      }
-    };
-  }, [loadInstalledVersions]);
+    },
+    [activeInstance?.id, instances, setActiveInstance],
+  );
 
   const handleStartGame = async () => {
-    if (!selectedVersion) {
-      toast.info("Please select a version!");
-      return;
-    }
-
-    if (!instancesStore.activeInstance) {
+    if (!activeInstance) {
       toast.info("Please select an instance first!");
       return;
     }
 
-    try {
-      gameUnlisten.current = await listen("game-exited", () => {
-        setIsLaunched(false);
-      });
-    } catch (error) {
-      toast.warning(`Failed to listen to game-exited event: ${error}`);
-    }
-
-    setIsLaunching(true);
-    try {
-      await startGame(instancesStore.activeInstance?.id, selectedVersion);
-      setIsLaunched(true);
-    } catch (error) {
-      console.error(`Failed to start game: ${error}`);
-      toast.error(`Failed to start game: ${error}`);
-    } finally {
-      setIsLaunching(false);
-    }
+    await startGame(
+      account,
+      () => setShowLoginModal(true),
+      activeInstance.id,
+      selectedVersion || activeInstance.versionId,
+      () => undefined,
+    );
   };
 
-  const getVersionTypeColor = (type: string) => {
-    switch (type) {
-      case "release":
-        return "bg-emerald-500";
-      case "snapshot":
-        return "bg-amber-500";
-      case "old_beta":
-        return "bg-rose-500";
-      case "old_alpha":
-        return "bg-violet-500";
-      default:
-        return "bg-gray-500";
-    }
+  const handleStopGame = async () => {
+    await stopGame(runningInstanceId);
   };
 
-  const versionOptions = useMemo(
-    () =>
-      installedVersions.map((v) => ({
-        label: `${v.id}${v.type !== "release" ? ` (${v.type})` : ""}`,
-        value: v.id,
-        type: v.type,
-      })),
-    [installedVersions],
-  );
-
   const renderButton = () => {
-    if (!authStore.account) {
+    const isGameRunning = runningInstanceId !== null;
+
+    if (!account) {
       return (
         <Button
           className="px-4 py-2"
@@ -173,20 +96,20 @@ export function BottomBar() {
       );
     }
 
-    return isLaunched ? (
-      <Button
-        variant="destructive"
-        onClick={() => {
-          toast.warning(
-            "Minecraft Process will not be terminated, please close it manually.",
-          );
-          setIsLaunched(false);
-        }}
-      >
-        <XIcon />
-        Game started
-      </Button>
-    ) : (
+    if (isGameRunning) {
+      return (
+        <Button
+          variant="destructive"
+          onClick={handleStopGame}
+          disabled={stoppingInstanceId !== null}
+        >
+          {stoppingInstanceId ? <Spinner /> : <XIcon />}
+          Close
+        </Button>
+      );
+    }
+
+    return (
       <Button
         className={cn(
           "px-4 py-2 shadow-xl",
@@ -194,9 +117,9 @@ export function BottomBar() {
         )}
         size="lg"
         onClick={handleStartGame}
-        disabled={isLaunching}
+        disabled={launchingInstanceId === activeInstance?.id}
       >
-        {isLaunching ? <Spinner /> : <Play />}
+        {launchingInstanceId === activeInstance?.id ? <Spinner /> : <Play />}
         Start
       </Button>
     );
@@ -206,40 +129,39 @@ export function BottomBar() {
     <div className="absolute bottom-0 left-0 right-0 bg-linear-to-t from-black/30 via-transparent to-transparent p-4 z-10">
       <div className="max-w-7xl mx-auto">
         <div className="flex items-center justify-between bg-white/5 dark:bg-black/20 backdrop-blur-xl border border-white/10 dark:border-white/5 p-3 shadow-lg">
-          <div className="flex items-center gap-4">
-            <div className="flex flex-col">
-              <span className="text-xs font-mono text-zinc-400 uppercase tracking-wider">
-                Active Instance
-              </span>
-              <span className="text-sm font-medium text-white">
-                {instancesStore.activeInstance?.name || "No instance selected"}
-              </span>
-            </div>
-
+          <div className="flex items-center gap-4 min-w-0">
             <Select
-              value={selectedVersion}
-              items={versionOptions}
-              onValueChange={setSelectedVersion}
-              disabled={isLoadingVersions}
+              value={activeInstance?.id ?? null}
+              items={instances.map((instance) => ({
+                label: instance.name,
+                value: instance.id,
+              }))}
+              onValueChange={(value) => {
+                if (value) {
+                  void handleInstanceChange(value);
+                }
+              }}
+              disabled={instances.length === 0}
             >
-              <SelectTrigger className="max-w-48">
+              <SelectTrigger className="w-full min-w-64 max-w-80">
                 <SelectValue
                   placeholder={
-                    isLoadingVersions
-                      ? "Loading versions..."
-                      : "Please select a version"
+                    instances.length === 0
+                      ? "No instances available"
+                      : "Please select an instance"
                   }
                 />
               </SelectTrigger>
               <SelectContent alignItemWithTrigger={false}>
                 <SelectGroup>
-                  {versionOptions.map((item) => (
-                    <SelectItem
-                      key={item.value}
-                      value={item.value}
-                      className={getVersionTypeColor(item.type)}
-                    >
-                      {item.label}
+                  {instances.map((instance) => (
+                    <SelectItem key={instance.id} value={instance.id}>
+                      <div className="flex min-w-0 flex-col">
+                        <span className="truncate">{instance.name}</span>
+                        <span className="text-muted-foreground truncate text-[11px]">
+                          {instance.versionId ?? "No version selected"}
+                        </span>
+                      </div>
                     </SelectItem>
                   ))}
                 </SelectGroup>
-- 
cgit v1.2.3-70-g09d2

