aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/ui/src/components/bottom-bar.tsx248
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>