From de31e0e220ae63ee1c2897526c5502d22c6612cd Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Mon, 9 Mar 2026 20:54:14 +0800 Subject: chore: update .gitignore to include Vscode directory --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 1419805..22fcb7f 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ release/ # claude code .claude/ + +# Vscode +.vscode/ -- cgit v1.2.3-70-g09d2 From 23c9038e49677b836c2c933f732817cf87150458 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Thu, 12 Mar 2026 15:32:42 +0800 Subject: fix(devUrl): fix localhost resolved ipv4/ipv6 bug with linux proxy --- src-tauri/tauri.conf.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 1c31a09..9ab9e6a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -5,7 +5,7 @@ "build": { "beforeDevCommand": "pnpm --filter @dropout/ui dev", "beforeBuildCommand": "pnpm --filter @dropout/ui build", - "devUrl": "http://localhost:5173", + "devUrl": "http://127.0.0.1:5173", "frontendDist": "../packages/ui/dist" }, "app": { @@ -20,6 +20,7 @@ } ], "security": { + "devCsp": null, "csp": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https: ws: wss:;", "capabilities": ["default"] } -- cgit v1.2.3-70-g09d2 From 8cfc2cff7e8d8afaa7870474deb80008230357f1 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Thu, 12 Mar 2026 15:33:45 +0800 Subject: fix(devUrl): update remote urls --- src-tauri/capabilities/default.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index ea3fd7b..ee3644a 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -3,6 +3,14 @@ "identifier": "default", "description": "Default capabilities for the DropOut launcher", "windows": ["main"], + "remote": { + "urls": [ + "http://127.0.0.1:5173", + "http://127.0.0.1:5173/*", + "http://localhost:5173", + "http://localhost:5173/*" + ] + }, "permissions": [ "core:default", "core:event:default", -- cgit v1.2.3-70-g09d2 From 792bd3260839faffb004741e78c81b0c6acd5d08 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Thu, 12 Mar 2026 15:34:30 +0800 Subject: fix(modpack): make CURSEFORGE_API_KEY optional for dev --- src-tauri/src/core/modpack.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/core/modpack.rs b/src-tauri/src/core/modpack.rs index 5ac9493..2998167 100644 --- a/src-tauri/src/core/modpack.rs +++ b/src-tauri/src/core/modpack.rs @@ -294,8 +294,6 @@ fn parse_multimc(archive: &mut Archive) -> Result { // ── CurseForge API resolution ───────────────────────────────────────────── -const CURSEFORGE_API_KEY: &str = env!("CURSEFORGE_API_KEY"); - async fn resolve_curseforge_files(files: &[ModpackFile]) -> Result, String> { let file_ids: Vec = files .iter() @@ -368,9 +366,15 @@ async fn cf_post( endpoint: &str, body: &serde_json::Value, ) -> Result { + let api_key = std::env::var("CURSEFORGE_API_KEY") + .map_err(|_| "CURSEFORGE_API_KEY is not set".to_string())?; + if api_key.trim().is_empty() { + return Err("CURSEFORGE_API_KEY is empty".to_string()); + } + let resp = client .post(format!("https://api.curseforge.com{endpoint}")) - .header("x-api-key", CURSEFORGE_API_KEY) + .header("x-api-key", api_key) .json(body) .send() .await -- cgit v1.2.3-70-g09d2 From fd532d5b69dd6d8af6ed1dc3d27a65f6bd464218 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Thu, 12 Mar 2026 15:35:33 +0800 Subject: feat: Add server for vite.config.ts --- packages/ui/vite.config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts index 8c90267..efc18b4 100644 --- a/packages/ui/vite.config.ts +++ b/packages/ui/vite.config.ts @@ -6,6 +6,11 @@ import { defineConfig } from "vite"; // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], + server: { + host: "127.0.0.1", + port: 5173, + strictPort: true, + }, resolve: { alias: { "@": path.resolve(__dirname, "./src"), -- cgit v1.2.3-70-g09d2 From 6c78bf95dbe026d875ee4dffed565934d974eb33 Mon Sep 17 00:00:00 2001 From: HsiangNianian 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(-) 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(false); - const gameUnlisten = useRef(null); - const [isLaunching, setIsLaunching] = useState(false); - const [selectedVersion, setSelectedVersion] = useState(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 ( - ) : ( + if (isGameRunning) { + return ( + + ); + } + + return ( ); @@ -206,40 +129,39 @@ export function BottomBar() {
-
-
- - Active Instance - - - {instancesStore.activeInstance?.name || "No instance selected"} - -
- +