aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/ui/src/stores
diff options
context:
space:
mode:
author苏向夜 <fu050409@163.com>2026-03-29 00:58:15 +0800
committer苏向夜 <fu050409@163.com>2026-03-29 00:58:15 +0800
commit32a4d85af937e4fd882fa671aee8b72878cc564f (patch)
treea79ccbed55848d527ae6ac391607b36923794e9d /packages/ui/src/stores
parent97fe5046f68b5e4ee5f750945bcc39a27f5eb37b (diff)
downloadDropOut-32a4d85af937e4fd882fa671aee8b72878cc564f.tar.gz
DropOut-32a4d85af937e4fd882fa671aee8b72878cc564f.zip
feat(ui): remove all stores
Diffstat (limited to 'packages/ui/src/stores')
-rw-r--r--packages/ui/src/stores/assistant-store.ts201
-rw-r--r--packages/ui/src/stores/auth-store.ts296
-rw-r--r--packages/ui/src/stores/logs-store.ts200
-rw-r--r--packages/ui/src/stores/releases-store.ts63
-rw-r--r--packages/ui/src/stores/settings-store.ts568
-rw-r--r--packages/ui/src/stores/ui-store.ts42
6 files changed, 0 insertions, 1370 deletions
diff --git a/packages/ui/src/stores/assistant-store.ts b/packages/ui/src/stores/assistant-store.ts
deleted file mode 100644
index 180031b..0000000
--- a/packages/ui/src/stores/assistant-store.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-import { invoke } from "@tauri-apps/api/core";
-import { listen, type UnlistenFn } from "@tauri-apps/api/event";
-import { create } from "zustand";
-import type { GenerationStats, StreamChunk } from "@/types/bindings/assistant";
-
-export interface Message {
- role: "user" | "assistant" | "system";
- content: string;
- stats?: GenerationStats;
-}
-
-interface AssistantState {
- // State
- messages: Message[];
- isProcessing: boolean;
- isProviderHealthy: boolean | undefined;
- streamingContent: string;
- initialized: boolean;
- streamUnlisten: UnlistenFn | null;
-
- // Actions
- init: () => Promise<void>;
- checkHealth: () => Promise<void>;
- sendMessage: (
- content: string,
- isEnabled: boolean,
- provider: string,
- endpoint: string,
- ) => Promise<void>;
- finishStreaming: () => void;
- clearHistory: () => void;
- setMessages: (messages: Message[]) => void;
- setIsProcessing: (isProcessing: boolean) => void;
- setIsProviderHealthy: (isProviderHealthy: boolean | undefined) => void;
- setStreamingContent: (streamingContent: string) => void;
-}
-
-export const useAssistantStore = create<AssistantState>((set, get) => ({
- // Initial state
- messages: [],
- isProcessing: false,
- isProviderHealthy: false,
- streamingContent: "",
- initialized: false,
- streamUnlisten: null,
-
- // Actions
- init: async () => {
- const { initialized } = get();
- if (initialized) return;
- set({ initialized: true });
- await get().checkHealth();
- },
-
- checkHealth: async () => {
- try {
- const isHealthy = await invoke<boolean>("assistant_check_health");
- set({ isProviderHealthy: isHealthy });
- } catch (e) {
- console.error("Failed to check provider health:", e);
- set({ isProviderHealthy: false });
- }
- },
-
- finishStreaming: () => {
- const { streamUnlisten } = get();
- set({ isProcessing: false, streamingContent: "" });
-
- if (streamUnlisten) {
- streamUnlisten();
- set({ streamUnlisten: null });
- }
- },
-
- sendMessage: async (content, isEnabled, provider, endpoint) => {
- if (!content.trim()) return;
-
- const { messages } = get();
-
- if (!isEnabled) {
- const newMessage: Message = {
- role: "assistant",
- content: "Assistant is disabled. Enable it in Settings > AI Assistant.",
- };
- set({ messages: [...messages, { role: "user", content }, newMessage] });
- return;
- }
-
- // Add user message
- const userMessage: Message = { role: "user", content };
- const updatedMessages = [...messages, userMessage];
- set({
- messages: updatedMessages,
- isProcessing: true,
- streamingContent: "",
- });
-
- // Add empty assistant message for streaming
- const assistantMessage: Message = { role: "assistant", content: "" };
- const withAssistantMessage = [...updatedMessages, assistantMessage];
- set({ messages: withAssistantMessage });
-
- try {
- // Set up stream listener
- const unlisten = await listen<StreamChunk>(
- "assistant-stream",
- (event) => {
- const chunk = event.payload;
- const currentState = get();
-
- if (chunk.content) {
- const newStreamingContent =
- currentState.streamingContent + chunk.content;
- const currentMessages = [...currentState.messages];
- const lastIdx = currentMessages.length - 1;
-
- if (lastIdx >= 0 && currentMessages[lastIdx].role === "assistant") {
- currentMessages[lastIdx] = {
- ...currentMessages[lastIdx],
- content: newStreamingContent,
- };
- set({
- streamingContent: newStreamingContent,
- messages: currentMessages,
- });
- }
- }
-
- if (chunk.done) {
- const finalMessages = [...currentState.messages];
- const lastIdx = finalMessages.length - 1;
-
- if (
- chunk.stats &&
- lastIdx >= 0 &&
- finalMessages[lastIdx].role === "assistant"
- ) {
- finalMessages[lastIdx] = {
- ...finalMessages[lastIdx],
- stats: chunk.stats,
- };
- set({ messages: finalMessages });
- }
-
- get().finishStreaming();
- }
- },
- );
-
- set({ streamUnlisten: unlisten });
-
- // Start streaming chat
- await invoke<string>("assistant_chat_stream", {
- messages: withAssistantMessage.slice(0, -1), // Exclude the empty assistant message
- });
- } catch (e) {
- console.error("Failed to send message:", e);
- const errorMessage = e instanceof Error ? e.message : String(e);
-
- let helpText = "";
- if (provider === "ollama") {
- helpText = `\n\nPlease ensure Ollama is running at ${endpoint}.`;
- } else if (provider === "openai") {
- helpText = "\n\nPlease check your OpenAI API key in Settings.";
- }
-
- // Update the last message with error
- const currentMessages = [...get().messages];
- const lastIdx = currentMessages.length - 1;
- if (lastIdx >= 0 && currentMessages[lastIdx].role === "assistant") {
- currentMessages[lastIdx] = {
- role: "assistant",
- content: `Error: ${errorMessage}${helpText}`,
- };
- set({ messages: currentMessages });
- }
-
- get().finishStreaming();
- }
- },
-
- clearHistory: () => {
- set({ messages: [], streamingContent: "" });
- },
-
- setMessages: (messages) => {
- set({ messages });
- },
-
- setIsProcessing: (isProcessing) => {
- set({ isProcessing });
- },
-
- setIsProviderHealthy: (isProviderHealthy) => {
- set({ isProviderHealthy });
- },
-
- setStreamingContent: (streamingContent) => {
- set({ streamingContent });
- },
-}));
diff --git a/packages/ui/src/stores/auth-store.ts b/packages/ui/src/stores/auth-store.ts
deleted file mode 100644
index bf7e3c5..0000000
--- a/packages/ui/src/stores/auth-store.ts
+++ /dev/null
@@ -1,296 +0,0 @@
-import { invoke } from "@tauri-apps/api/core";
-import { listen, type UnlistenFn } from "@tauri-apps/api/event";
-import { open } from "@tauri-apps/plugin-shell";
-import { toast } from "sonner";
-import { create } from "zustand";
-import type { Account, DeviceCodeResponse } from "../types/bindings/auth";
-
-interface AuthState {
- // State
- currentAccount: Account | null;
- isLoginModalOpen: boolean;
- isLogoutConfirmOpen: boolean;
- loginMode: "select" | "offline" | "microsoft";
- offlineUsername: string;
- deviceCodeData: DeviceCodeResponse | null;
- msLoginLoading: boolean;
- msLoginStatus: string;
-
- // Private state
- pollInterval: ReturnType<typeof setInterval> | null;
- isPollingRequestActive: boolean;
- authProgressUnlisten: UnlistenFn | null;
-
- // Actions
- checkAccount: () => Promise<void>;
- openLoginModal: () => void;
- openLogoutConfirm: () => void;
- cancelLogout: () => void;
- confirmLogout: () => Promise<void>;
- closeLoginModal: () => void;
- resetLoginState: () => void;
- performOfflineLogin: () => Promise<void>;
- startMicrosoftLogin: () => Promise<void>;
- checkLoginStatus: (deviceCode: string) => Promise<void>;
- stopPolling: () => void;
- cancelMicrosoftLogin: () => void;
- setLoginMode: (mode: "select" | "offline" | "microsoft") => void;
- setOfflineUsername: (username: string) => void;
-}
-
-export const useAuthStore = create<AuthState>((set, get) => ({
- // Initial state
- currentAccount: null,
- isLoginModalOpen: false,
- isLogoutConfirmOpen: false,
- loginMode: "select",
- offlineUsername: "",
- deviceCodeData: null,
- msLoginLoading: false,
- msLoginStatus: "Waiting for authorization...",
-
- // Private state
- pollInterval: null,
- isPollingRequestActive: false,
- authProgressUnlisten: null,
-
- // Actions
- checkAccount: async () => {
- try {
- const acc = await invoke<Account | null>("get_active_account");
- set({ currentAccount: acc });
- } catch (error) {
- console.error("Failed to check account:", error);
- }
- },
-
- openLoginModal: () => {
- const { currentAccount } = get();
- if (currentAccount) {
- // Show custom logout confirmation dialog
- set({ isLogoutConfirmOpen: true });
- return;
- }
- get().resetLoginState();
- set({ isLoginModalOpen: true });
- },
-
- openLogoutConfirm: () => {
- set({ isLogoutConfirmOpen: true });
- },
-
- cancelLogout: () => {
- set({ isLogoutConfirmOpen: false });
- },
-
- confirmLogout: async () => {
- set({ isLogoutConfirmOpen: false });
- try {
- await invoke("logout");
- set({ currentAccount: null });
- } catch (error) {
- console.error("Logout failed:", error);
- }
- },
-
- closeLoginModal: () => {
- get().stopPolling();
- set({ isLoginModalOpen: false });
- },
-
- resetLoginState: () => {
- set({
- loginMode: "select",
- offlineUsername: "",
- deviceCodeData: null,
- msLoginLoading: false,
- msLoginStatus: "Waiting for authorization...",
- });
- },
-
- performOfflineLogin: async () => {
- const { offlineUsername } = get();
- if (!offlineUsername.trim()) return;
-
- try {
- const account = await invoke<Account>("login_offline", {
- username: offlineUsername,
- });
- set({
- currentAccount: account,
- isLoginModalOpen: false,
- offlineUsername: "",
- });
- } catch (error) {
- // Keep UI-friendly behavior consistent with prior code
- alert("Login failed: " + String(error));
- }
- },
-
- startMicrosoftLogin: async () => {
- // Prepare UI state
- set({
- msLoginLoading: true,
- msLoginStatus: "Waiting for authorization...",
- loginMode: "microsoft",
- deviceCodeData: null,
- });
-
- // Listen to general launcher logs so we can display progress to the user.
- // The backend emits logs via "launcher-log"; using that keeps this store decoupled
- // from a dedicated auth event channel (backend may reuse launcher-log).
- try {
- const unlisten = await listen("launcher-log", (event) => {
- const payload = event.payload;
- // Normalize payload to string if possible
- const message =
- typeof payload === "string"
- ? payload
- : (payload?.toString?.() ?? JSON.stringify(payload));
- set({ msLoginStatus: message });
- });
- set({ authProgressUnlisten: unlisten });
- } catch (err) {
- console.warn("Failed to attach launcher-log listener:", err);
- }
-
- try {
- const deviceCodeData = await invoke<DeviceCodeResponse>(
- "start_microsoft_login",
- );
- set({ deviceCodeData });
-
- if (deviceCodeData) {
- // Try to copy user code to clipboard for convenience (best-effort)
- try {
- await navigator.clipboard?.writeText(deviceCodeData.userCode ?? "");
- } catch (err) {
- // ignore clipboard errors
- console.debug("Clipboard copy failed:", err);
- }
-
- // Open verification URI in default browser
- try {
- if (deviceCodeData.verificationUri) {
- await open(deviceCodeData.verificationUri);
- }
- } catch (err) {
- console.debug("Failed to open verification URI:", err);
- }
-
- // Start polling for completion
- // `interval` from the bindings is a bigint (seconds). Convert safely to number.
- const intervalSeconds =
- deviceCodeData.interval !== undefined &&
- deviceCodeData.interval !== null
- ? Number(deviceCodeData.interval)
- : 5;
- const intervalMs = intervalSeconds * 1000;
- const pollInterval = setInterval(
- () => get().checkLoginStatus(deviceCodeData.deviceCode),
- intervalMs,
- );
- set({ pollInterval });
- }
- } catch (error) {
- toast.error(`Failed to start Microsoft login: ${error}`);
- set({ loginMode: "select" });
- // cleanup listener if present
- const { authProgressUnlisten } = get();
- if (authProgressUnlisten) {
- authProgressUnlisten();
- set({ authProgressUnlisten: null });
- }
- } finally {
- set({ msLoginLoading: false });
- }
- },
-
- checkLoginStatus: async (deviceCode: string) => {
- const { isPollingRequestActive } = get();
- if (isPollingRequestActive) return;
-
- set({ isPollingRequestActive: true });
-
- try {
- const account = await invoke<Account>("complete_microsoft_login", {
- deviceCode,
- });
-
- // On success, stop polling and cleanup listener
- get().stopPolling();
- const { authProgressUnlisten } = get();
- if (authProgressUnlisten) {
- authProgressUnlisten();
- set({ authProgressUnlisten: null });
- }
-
- set({
- currentAccount: account,
- isLoginModalOpen: false,
- });
- } catch (error: unknown) {
- const errStr = String(error);
- if (errStr.includes("authorization_pending")) {
- // Still waiting — keep polling
- } else {
- set({ msLoginStatus: "Error: " + errStr });
-
- if (
- errStr.includes("expired_token") ||
- errStr.includes("access_denied")
- ) {
- // Terminal errors — stop polling and reset state
- get().stopPolling();
- const { authProgressUnlisten } = get();
- if (authProgressUnlisten) {
- authProgressUnlisten();
- set({ authProgressUnlisten: null });
- }
- alert("Login failed: " + errStr);
- set({ loginMode: "select" });
- }
- }
- } finally {
- set({ isPollingRequestActive: false });
- }
- },
-
- stopPolling: () => {
- const { pollInterval, authProgressUnlisten } = get();
- if (pollInterval) {
- try {
- clearInterval(pollInterval);
- } catch (err) {
- console.debug("Failed to clear poll interval:", err);
- }
- set({ pollInterval: null });
- }
- if (authProgressUnlisten) {
- try {
- authProgressUnlisten();
- } catch (err) {
- console.debug("Failed to unlisten auth progress:", err);
- }
- set({ authProgressUnlisten: null });
- }
- },
-
- cancelMicrosoftLogin: () => {
- get().stopPolling();
- set({
- deviceCodeData: null,
- msLoginLoading: false,
- msLoginStatus: "",
- loginMode: "select",
- });
- },
-
- setLoginMode: (mode: "select" | "offline" | "microsoft") => {
- set({ loginMode: mode });
- },
-
- setOfflineUsername: (username: string) => {
- set({ offlineUsername: username });
- },
-}));
diff --git a/packages/ui/src/stores/logs-store.ts b/packages/ui/src/stores/logs-store.ts
deleted file mode 100644
index b19f206..0000000
--- a/packages/ui/src/stores/logs-store.ts
+++ /dev/null
@@ -1,200 +0,0 @@
-import { listen } from "@tauri-apps/api/event";
-import { create } from "zustand";
-
-export interface LogEntry {
- id: number;
- timestamp: string;
- level: "info" | "warn" | "error" | "debug" | "fatal";
- source: string;
- message: string;
-}
-
-// Parse Minecraft/Java log format: [HH:MM:SS] [Thread/LEVEL]: message
-// or: [HH:MM:SS] [Thread/LEVEL] [Source]: message
-const GAME_LOG_REGEX =
- /^\[[\d:]+\]\s*\[([^\]]+)\/(\w+)\](?:\s*\[([^\]]+)\])?:\s*(.*)$/;
-
-function parseGameLogLevel(levelStr: string): LogEntry["level"] {
- const upper = levelStr.toUpperCase();
- if (upper === "INFO") return "info";
- if (upper === "WARN" || upper === "WARNING") return "warn";
- if (upper === "ERROR" || upper === "SEVERE") return "error";
- if (
- upper === "DEBUG" ||
- upper === "TRACE" ||
- upper === "FINE" ||
- upper === "FINER" ||
- upper === "FINEST"
- )
- return "debug";
- if (upper === "FATAL") return "fatal";
- return "info";
-}
-
-interface LogsState {
- // State
- logs: LogEntry[];
- sources: Set<string>;
- nextId: number;
- maxLogs: number;
- initialized: boolean;
-
- // Actions
- addLog: (level: LogEntry["level"], source: string, message: string) => void;
- addGameLog: (rawLine: string, isStderr: boolean) => void;
- clear: () => void;
- exportLogs: (filteredLogs: LogEntry[]) => string;
- init: () => Promise<void>;
- setLogs: (logs: LogEntry[]) => void;
- setSources: (sources: Set<string>) => void;
-}
-
-export const useLogsStore = create<LogsState>((set, get) => ({
- // Initial state
- logs: [],
- sources: new Set(["Launcher"]),
- nextId: 0,
- maxLogs: 5000,
- initialized: false,
-
- // Actions
- addLog: (level, source, message) => {
- const { nextId, logs, maxLogs, sources } = get();
- const now = new Date();
- const timestamp =
- now.toLocaleTimeString() +
- "." +
- now.getMilliseconds().toString().padStart(3, "0");
-
- const newLog: LogEntry = {
- id: nextId,
- timestamp,
- level,
- source,
- message,
- };
-
- const newLogs = [...logs, newLog];
- const newSources = new Set(sources);
-
- // Track source
- if (!newSources.has(source)) {
- newSources.add(source);
- }
-
- // Trim logs if exceeding max
- const trimmedLogs =
- newLogs.length > maxLogs ? newLogs.slice(-maxLogs) : newLogs;
-
- set({
- logs: trimmedLogs,
- sources: newSources,
- nextId: nextId + 1,
- });
- },
-
- addGameLog: (rawLine, isStderr) => {
- const match = rawLine.match(GAME_LOG_REGEX);
-
- if (match) {
- const [, thread, levelStr, extraSource, message] = match;
- const level = parseGameLogLevel(levelStr);
- // Use extraSource if available, otherwise use thread name as source hint
- const source = extraSource || `Game/${thread.split("-")[0]}`;
- get().addLog(level, source, message);
- } else {
- // Fallback: couldn't parse, use stderr as error indicator
- const level = isStderr ? "error" : "info";
- get().addLog(level, "Game", rawLine);
- }
- },
-
- clear: () => {
- set({
- logs: [],
- sources: new Set(["Launcher"]),
- });
- get().addLog("info", "Launcher", "Logs cleared");
- },
-
- exportLogs: (filteredLogs) => {
- return filteredLogs
- .map(
- (l) =>
- `[${l.timestamp}] [${l.source}/${l.level.toUpperCase()}] ${l.message}`,
- )
- .join("\n");
- },
-
- init: async () => {
- const { initialized } = get();
- if (initialized) return;
-
- set({ initialized: true });
-
- // Initial log
- get().addLog("info", "Launcher", "Logs initialized");
-
- // General Launcher Logs
- await listen<string>("launcher-log", (e) => {
- get().addLog("info", "Launcher", e.payload);
- });
-
- // Game Stdout - parse log level
- await listen<string>("game-stdout", (e) => {
- get().addGameLog(e.payload, false);
- });
-
- // Game Stderr - parse log level, default to error
- await listen<string>("game-stderr", (e) => {
- get().addGameLog(e.payload, true);
- });
-
- // Download Events (Summarized)
- await listen("download-start", (e: any) => {
- get().addLog(
- "info",
- "Downloader",
- `Starting batch download of ${e.payload} files...`,
- );
- });
-
- await listen("download-complete", () => {
- get().addLog("info", "Downloader", "All downloads completed.");
- });
-
- // Listen to file download progress to log finished files
- await listen<any>("download-progress", (e) => {
- const p = e.payload;
- if (p.status === "Finished") {
- if (p.file.endsWith(".jar")) {
- get().addLog("info", "Downloader", `Downloaded ${p.file}`);
- }
- }
- });
-
- // Java Download
- await listen<any>("java-download-progress", (e) => {
- const p = e.payload;
- if (p.status === "Downloading" && p.percentage === 0) {
- get().addLog(
- "info",
- "JavaInstaller",
- `Downloading Java: ${p.file_name}`,
- );
- } else if (p.status === "Completed") {
- get().addLog("info", "JavaInstaller", `Java installed: ${p.file_name}`);
- } else if (p.status === "Error") {
- get().addLog("error", "JavaInstaller", `Java download error`);
- }
- });
- },
-
- setLogs: (logs) => {
- set({ logs });
- },
-
- setSources: (sources) => {
- set({ sources });
- },
-}));
diff --git a/packages/ui/src/stores/releases-store.ts b/packages/ui/src/stores/releases-store.ts
deleted file mode 100644
index 56afa08..0000000
--- a/packages/ui/src/stores/releases-store.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { invoke } from "@tauri-apps/api/core";
-import { create } from "zustand";
-import type { GithubRelease } from "@/types/bindings/core";
-
-interface ReleasesState {
- // State
- releases: GithubRelease[];
- isLoading: boolean;
- isLoaded: boolean;
- error: string | null;
-
- // Actions
- loadReleases: () => Promise<void>;
- setReleases: (releases: GithubRelease[]) => void;
- setIsLoading: (isLoading: boolean) => void;
- setIsLoaded: (isLoaded: boolean) => void;
- setError: (error: string | null) => void;
-}
-
-export const useReleasesStore = create<ReleasesState>((set, get) => ({
- // Initial state
- releases: [],
- isLoading: false,
- isLoaded: false,
- error: null,
-
- // Actions
- loadReleases: async () => {
- const { isLoaded, isLoading } = get();
-
- // If already loaded or currently loading, skip to prevent duplicate requests
- if (isLoaded || isLoading) return;
-
- set({ isLoading: true, error: null });
-
- try {
- const releases = await invoke<GithubRelease[]>("get_github_releases");
- set({ releases, isLoaded: true });
- } catch (e) {
- const error = e instanceof Error ? e.message : String(e);
- console.error("Failed to load releases:", e);
- set({ error });
- } finally {
- set({ isLoading: false });
- }
- },
-
- setReleases: (releases) => {
- set({ releases });
- },
-
- setIsLoading: (isLoading) => {
- set({ isLoading });
- },
-
- setIsLoaded: (isLoaded) => {
- set({ isLoaded });
- },
-
- setError: (error) => {
- set({ error });
- },
-}));
diff --git a/packages/ui/src/stores/settings-store.ts b/packages/ui/src/stores/settings-store.ts
deleted file mode 100644
index 0bfc1e1..0000000
--- a/packages/ui/src/stores/settings-store.ts
+++ /dev/null
@@ -1,568 +0,0 @@
-import { convertFileSrc, invoke } from "@tauri-apps/api/core";
-import { listen, type UnlistenFn } from "@tauri-apps/api/event";
-import { toast } from "sonner";
-import { create } from "zustand";
-import { downloadAdoptiumJava } from "@/client";
-import type { ModelInfo } from "../types/bindings/assistant";
-import type { LauncherConfig } from "../types/bindings/config";
-import type {
- JavaDownloadProgress,
- PendingJavaDownload,
-} from "../types/bindings/downloader";
-import type {
- JavaCatalog,
- JavaInstallation,
- JavaReleaseInfo,
-} from "../types/bindings/java";
-
-type JavaDownloadSource = "adoptium" | "mojang" | "azul";
-
-/**
- * State shape for settings store.
- *
- * Note: Uses camelCase naming to match ts-rs generated bindings (which now use
- * `serde(rename_all = "camelCase")`). When reading raw binding objects from
- * invoke, convert/mapping should be applied where necessary.
- */
-interface SettingsState {
- // State
- settings: LauncherConfig;
- javaInstallations: JavaInstallation[];
- isDetectingJava: boolean;
- showJavaDownloadModal: boolean;
- selectedDownloadSource: JavaDownloadSource;
- javaCatalog: JavaCatalog | null;
- isLoadingCatalog: boolean;
- catalogError: string;
- selectedMajorVersion: number | null;
- selectedImageType: "jre" | "jdk";
- showOnlyRecommended: boolean;
- searchQuery: string;
- isDownloadingJava: boolean;
- downloadProgress: JavaDownloadProgress | null;
- javaDownloadStatus: string;
- pendingDownloads: PendingJavaDownload[];
- ollamaModels: ModelInfo[];
- openaiModels: ModelInfo[];
- isLoadingOllamaModels: boolean;
- isLoadingOpenaiModels: boolean;
- ollamaModelsError: string;
- openaiModelsError: string;
- showConfigEditor: boolean;
- rawConfigContent: string;
- configFilePath: string;
- configEditorError: string;
-
- // Computed / derived
- backgroundUrl: string | undefined;
- filteredReleases: JavaReleaseInfo[];
- availableMajorVersions: number[];
- installStatus: (
- version: number,
- imageType: string,
- ) => "installed" | "downloading" | "available";
- selectedRelease: JavaReleaseInfo | null;
- currentModelOptions: Array<{
- value: string;
- label: string;
- details?: string;
- }>;
-
- // Actions
- loadSettings: () => Promise<void>;
- saveSettings: () => Promise<void>;
- // compatibility helper to mirror the older set({ key: value }) usage
- set: (patch: Partial<Record<string, unknown>>) => void;
-
- detectJava: () => Promise<void>;
- selectJava: (path: string) => void;
-
- openJavaDownloadModal: () => Promise<void>;
- closeJavaDownloadModal: () => void;
- loadJavaCatalog: (forceRefresh: boolean) => Promise<void>;
- refreshCatalog: () => Promise<void>;
- loadPendingDownloads: () => Promise<void>;
- selectMajorVersion: (version: number) => void;
- downloadJava: () => Promise<void>;
- cancelDownload: () => Promise<void>;
- resumeDownloads: () => Promise<void>;
-
- openConfigEditor: () => Promise<void>;
- closeConfigEditor: () => void;
- saveRawConfig: () => Promise<void>;
-
- loadOllamaModels: () => Promise<void>;
- loadOpenaiModels: () => Promise<void>;
-
- setSetting: <K extends keyof LauncherConfig>(
- key: K,
- value: LauncherConfig[K],
- ) => void;
- setAssistantSetting: <K extends keyof LauncherConfig["assistant"]>(
- key: K,
- value: LauncherConfig["assistant"][K],
- ) => void;
- setFeatureFlag: <K extends keyof LauncherConfig["featureFlags"]>(
- key: K,
- value: LauncherConfig["featureFlags"][K],
- ) => void;
-
- // Private
- progressUnlisten: UnlistenFn | null;
-}
-
-/**
- * Default settings (camelCase) — lightweight defaults used until `get_settings`
- * returns real values.
- */
-const defaultSettings: LauncherConfig = {
- minMemory: 1024,
- maxMemory: 2048,
- javaPath: "java",
- width: 854,
- height: 480,
- downloadThreads: 32,
- enableGpuAcceleration: false,
- enableVisualEffects: true,
- activeEffect: "constellation",
- theme: "dark",
- customBackgroundPath: null,
- logUploadService: "paste.rs",
- pastebinApiKey: null,
- assistant: {
- enabled: true,
- llmProvider: "ollama",
- ollamaEndpoint: "http://localhost:11434",
- ollamaModel: "llama3",
- openaiApiKey: null,
- openaiEndpoint: "https://api.openai.com/v1",
- openaiModel: "gpt-3.5-turbo",
- systemPrompt:
- "You are a helpful Minecraft expert assistant. You help players with game issues, mod installation, performance optimization, and gameplay tips. Analyze any game logs provided and give concise, actionable advice.",
- responseLanguage: "auto",
- ttsEnabled: false,
- ttsProvider: "disabled",
- },
- useSharedCaches: false,
- keepLegacyPerInstanceStorage: true,
- featureFlags: {
- demoUser: false,
- quickPlayEnabled: false,
- quickPlayPath: null,
- quickPlaySingleplayer: true,
- quickPlayMultiplayerServer: null,
- },
-};
-
-export const useSettingsStore = create<SettingsState>((set, get) => ({
- // initial state
- settings: defaultSettings,
- javaInstallations: [],
- isDetectingJava: false,
- showJavaDownloadModal: false,
- selectedDownloadSource: "adoptium",
- javaCatalog: null,
- isLoadingCatalog: false,
- catalogError: "",
- selectedMajorVersion: null,
- selectedImageType: "jre",
- showOnlyRecommended: true,
- searchQuery: "",
- isDownloadingJava: false,
- downloadProgress: null,
- javaDownloadStatus: "",
- pendingDownloads: [],
- ollamaModels: [],
- openaiModels: [],
- isLoadingOllamaModels: false,
- isLoadingOpenaiModels: false,
- ollamaModelsError: "",
- openaiModelsError: "",
- showConfigEditor: false,
- rawConfigContent: "",
- configFilePath: "",
- configEditorError: "",
- progressUnlisten: null,
-
- // derived getters
- get backgroundUrl() {
- const { settings } = get();
- if (settings.customBackgroundPath) {
- return convertFileSrc(settings.customBackgroundPath);
- }
- return undefined;
- },
-
- get filteredReleases() {
- const {
- javaCatalog,
- selectedMajorVersion,
- selectedImageType,
- showOnlyRecommended,
- searchQuery,
- } = get();
-
- if (!javaCatalog) return [];
-
- let releases = javaCatalog.releases;
-
- if (selectedMajorVersion !== null) {
- releases = releases.filter(
- (r) => r.majorVersion === selectedMajorVersion,
- );
- }
-
- releases = releases.filter((r) => r.imageType === selectedImageType);
-
- if (showOnlyRecommended) {
- releases = releases.filter((r) => r.isLts);
- }
-
- if (searchQuery.trim() !== "") {
- const q = searchQuery.toLowerCase();
- releases = releases.filter(
- (r) =>
- r.version.toLowerCase().includes(q) ||
- (r.releaseName ?? "").toLowerCase().includes(q),
- );
- }
-
- // sort newest-first by parsed version number
- return releases.sort((a, b) => {
- const aVer = parseFloat(a.version.split("-")[0]);
- const bVer = parseFloat(b.version.split("-")[0]);
- return bVer - aVer;
- });
- },
-
- get availableMajorVersions() {
- return get().javaCatalog?.availableMajorVersions || [];
- },
-
- installStatus: (version: number, imageType: string) => {
- const {
- javaInstallations,
- pendingDownloads,
- isDownloadingJava,
- downloadProgress,
- } = get();
-
- const installed = javaInstallations.some(
- (inst) => parseInt(inst.version.split(".")[0], 10) === version,
- );
- if (installed) return "installed";
-
- if (
- isDownloadingJava &&
- downloadProgress?.fileName?.includes(`${version}`)
- ) {
- return "downloading";
- }
-
- const pending = pendingDownloads.some(
- (d) => d.majorVersion === version && d.imageType === imageType,
- );
- if (pending) return "downloading";
-
- return "available";
- },
-
- get selectedRelease() {
- const { javaCatalog, selectedMajorVersion, selectedImageType } = get();
- if (!javaCatalog || selectedMajorVersion === null) return null;
- return (
- javaCatalog.releases.find(
- (r) =>
- r.majorVersion === selectedMajorVersion &&
- r.imageType === selectedImageType,
- ) || null
- );
- },
-
- get currentModelOptions() {
- const { settings, ollamaModels, openaiModels } = get();
- const provider = settings.assistant.llmProvider;
- if (provider === "ollama") {
- return ollamaModels.map((m) => ({
- value: m.id,
- label: m.name,
- details: m.details || m.size || "",
- }));
- } else {
- return openaiModels.map((m) => ({
- value: m.id,
- label: m.name,
- details: m.details || "",
- }));
- }
- },
-
- // actions
- loadSettings: async () => {
- try {
- const result = await invoke<LauncherConfig>("get_settings");
- // result already uses camelCase fields from bindings
- set({ settings: result });
-
- // enforce dark theme at app-level if necessary
- if (result.theme !== "dark") {
- const updated = { ...result, theme: "dark" } as LauncherConfig;
- set({ settings: updated });
- await invoke("save_settings", { config: updated });
- }
-
- // ensure customBackgroundPath is undefined rather than null for reactiveness
- if (!result.customBackgroundPath) {
- set((s) => ({
- settings: { ...s.settings, customBackgroundPath: null },
- }));
- }
- } catch (e) {
- console.error("Failed to load settings:", e);
- }
- },
-
- saveSettings: async () => {
- try {
- const { settings } = get();
-
- // Clean up empty strings to null where appropriate
- if ((settings.customBackgroundPath ?? "") === "") {
- set((state) => ({
- settings: { ...state.settings, customBackgroundPath: null },
- }));
- }
-
- await invoke("save_settings", { config: settings });
- toast.success("Settings saved!");
- } catch (e) {
- console.error("Failed to save settings:", e);
- toast.error(`Error saving settings: ${String(e)}`);
- }
- },
-
- set: (patch: Partial<Record<string, unknown>>) => {
- set(patch);
- },
-
- detectJava: async () => {
- set({ isDetectingJava: true });
- try {
- const installs = await invoke<JavaInstallation[]>("detect_java");
- set({ javaInstallations: installs });
- if (installs.length === 0) toast.info("No Java installations found");
- else toast.success(`Found ${installs.length} Java installation(s)`);
- } catch (e) {
- console.error("Failed to detect Java:", e);
- toast.error(`Error detecting Java: ${String(e)}`);
- } finally {
- set({ isDetectingJava: false });
- }
- },
-
- selectJava: (path: string) => {
- set((s) => ({ settings: { ...s.settings, javaPath: path } }));
- },
-
- openJavaDownloadModal: async () => {
- set({
- showJavaDownloadModal: true,
- javaDownloadStatus: "",
- catalogError: "",
- downloadProgress: null,
- });
-
- // attach event listener for download progress
- const state = get();
- if (state.progressUnlisten) {
- state.progressUnlisten();
- }
-
- const unlisten = await listen<JavaDownloadProgress>(
- "java-download-progress",
- (event) => {
- set({ downloadProgress: event.payload });
- },
- );
-
- set({ progressUnlisten: unlisten });
-
- // load catalog and pending downloads
- await get().loadJavaCatalog(false);
- await get().loadPendingDownloads();
- },
-
- closeJavaDownloadModal: () => {
- const { isDownloadingJava, progressUnlisten } = get();
-
- if (!isDownloadingJava) {
- set({ showJavaDownloadModal: false });
- if (progressUnlisten) {
- try {
- progressUnlisten();
- } catch {
- // ignore
- }
- set({ progressUnlisten: null });
- }
- }
- },
-
- loadJavaCatalog: async (forceRefresh: boolean) => {
- set({ isLoadingCatalog: true, catalogError: "" });
- try {
- const cmd = forceRefresh ? "refresh_java_catalog" : "get_java_catalog";
- const result = await invoke<JavaCatalog>(cmd);
- set({ javaCatalog: result, isLoadingCatalog: false });
- } catch (e) {
- console.error("Failed to load Java catalog:", e);
- set({ catalogError: String(e), isLoadingCatalog: false });
- }
- },
-
- refreshCatalog: async () => {
- await get().loadJavaCatalog(true);
- },
-
- loadPendingDownloads: async () => {
- try {
- const pending = await invoke<PendingJavaDownload[]>(
- "get_pending_java_downloads",
- );
- set({ pendingDownloads: pending });
- } catch (e) {
- console.error("Failed to load pending downloads:", e);
- }
- },
-
- selectMajorVersion: (version: number) => {
- set({ selectedMajorVersion: version });
- },
-
- downloadJava: async () => {
- const { selectedMajorVersion, selectedImageType, selectedDownloadSource } =
- get();
- if (!selectedMajorVersion) return;
- set({ isDownloadingJava: true, javaDownloadStatus: "Starting..." });
- try {
- const result = await downloadAdoptiumJava(
- selectedMajorVersion,
- selectedImageType,
- selectedDownloadSource,
- );
- set({
- javaDownloadStatus: `Java ${selectedMajorVersion} download started: ${result.path}`,
- });
- toast.success("Download started");
- } catch (e) {
- console.error("Failed to download Java:", e);
- toast.error(`Failed to start Java download: ${String(e)}`);
- } finally {
- set({ isDownloadingJava: false });
- }
- },
-
- cancelDownload: async () => {
- try {
- await invoke("cancel_java_download");
- toast.success("Cancelled Java download");
- set({ isDownloadingJava: false, javaDownloadStatus: "" });
- } catch (e) {
- console.error("Failed to cancel download:", e);
- toast.error(`Failed to cancel download: ${String(e)}`);
- }
- },
-
- resumeDownloads: async () => {
- try {
- const installed = await invoke<boolean>("resume_java_downloads");
- if (installed) toast.success("Resumed Java downloads");
- else toast.info("No downloads to resume");
- } catch (e) {
- console.error("Failed to resume downloads:", e);
- toast.error(`Failed to resume downloads: ${String(e)}`);
- }
- },
-
- openConfigEditor: async () => {
- try {
- const path = await invoke<string>("get_config_path");
- const content = await invoke<string>("read_config_raw");
- set({
- configFilePath: path,
- rawConfigContent: content,
- showConfigEditor: true,
- });
- } catch (e) {
- console.error("Failed to open config editor:", e);
- set({ configEditorError: String(e) });
- }
- },
-
- closeConfigEditor: () => {
- set({
- showConfigEditor: false,
- rawConfigContent: "",
- configFilePath: "",
- configEditorError: "",
- });
- },
-
- saveRawConfig: async () => {
- try {
- await invoke("write_config_raw", { content: get().rawConfigContent });
- toast.success("Config saved");
- set({ showConfigEditor: false });
- } catch (e) {
- console.error("Failed to save config:", e);
- set({ configEditorError: String(e) });
- toast.error(`Failed to save config: ${String(e)}`);
- }
- },
-
- loadOllamaModels: async () => {
- set({ isLoadingOllamaModels: true, ollamaModelsError: "" });
- try {
- const models = await invoke<ModelInfo[]>("get_ollama_models");
- set({ ollamaModels: models, isLoadingOllamaModels: false });
- } catch (e) {
- console.error("Failed to load Ollama models:", e);
- set({ isLoadingOllamaModels: false, ollamaModelsError: String(e) });
- }
- },
-
- loadOpenaiModels: async () => {
- set({ isLoadingOpenaiModels: true, openaiModelsError: "" });
- try {
- const models = await invoke<ModelInfo[]>("get_openai_models");
- set({ openaiModels: models, isLoadingOpenaiModels: false });
- } catch (e) {
- console.error("Failed to load OpenAI models:", e);
- set({ isLoadingOpenaiModels: false, openaiModelsError: String(e) });
- }
- },
-
- setSetting: (key, value) => {
- set((s) => ({
- settings: { ...s.settings, [key]: value } as unknown as LauncherConfig,
- }));
- },
-
- setAssistantSetting: (key, value) => {
- set((s) => ({
- settings: {
- ...s.settings,
- assistant: { ...s.settings.assistant, [key]: value },
- } as LauncherConfig,
- }));
- },
-
- setFeatureFlag: (key, value) => {
- set((s) => ({
- settings: {
- ...s.settings,
- featureFlags: { ...s.settings.featureFlags, [key]: value },
- } as LauncherConfig,
- }));
- },
-}));
diff --git a/packages/ui/src/stores/ui-store.ts b/packages/ui/src/stores/ui-store.ts
deleted file mode 100644
index 89b9191..0000000
--- a/packages/ui/src/stores/ui-store.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { create } from "zustand";
-
-export type ViewType = "home" | "versions" | "settings" | "guide" | "instances";
-
-interface UIState {
- // State
- currentView: ViewType;
- showConsole: boolean;
- appVersion: string;
-
- // Actions
- toggleConsole: () => void;
- setView: (view: ViewType) => void;
- setAppVersion: (version: string) => void;
-}
-
-export const useUIStore = create<UIState>((set) => ({
- // Initial state
- currentView: "home",
- showConsole: false,
- appVersion: "...",
-
- // Actions
- toggleConsole: () => {
- set((state) => ({ showConsole: !state.showConsole }));
- },
-
- setView: (view: ViewType) => {
- set({ currentView: view });
- },
-
- setAppVersion: (version: string) => {
- set({ appVersion: version });
- },
-}));
-
-// Provide lowercase alias for compatibility with existing imports.
-// Use a function wrapper to ensure the named export exists as a callable value
-// at runtime (some bundlers/tree-shakers may remove simple aliases).
-export function useUiStore() {
- return useUIStore();
-}