diff options
| author | 2026-03-26 09:06:56 +0800 | |
|---|---|---|
| committer | 2026-03-26 09:06:56 +0800 | |
| commit | 2412f7a3a626fc3b9e7b59ce1fc900468b792972 (patch) | |
| tree | 68b2ad2b56daa1ad040a4a0df0f7db509e16d53c /packages/ui/src/components | |
| parent | 788715b1ca5ab5b67fcc2e69650b74e14c953a57 (diff) | |
| parent | 94b0d8e208363c802c12b56d8bdbef574dd1fb91 (diff) | |
| download | DropOut-2412f7a3a626fc3b9e7b59ce1fc900468b792972.tar.gz DropOut-2412f7a3a626fc3b9e7b59ce1fc900468b792972.zip | |
Merge branch 'main' into refactor/fe
Diffstat (limited to 'packages/ui/src/components')
| -rw-r--r-- | packages/ui/src/components/bottom-bar.tsx | 248 |
1 files changed, 85 insertions, 163 deletions
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> |