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; saveSettings: () => Promise; // compatibility helper to mirror the older set({ key: value }) usage set: (patch: Partial>) => void; detectJava: () => Promise; selectJava: (path: string) => void; openJavaDownloadModal: () => Promise; closeJavaDownloadModal: () => void; loadJavaCatalog: (forceRefresh: boolean) => Promise; refreshCatalog: () => Promise; loadPendingDownloads: () => Promise; selectMajorVersion: (version: number) => void; downloadJava: () => Promise; cancelDownload: () => Promise; resumeDownloads: () => Promise; openConfigEditor: () => Promise; closeConfigEditor: () => void; saveRawConfig: () => Promise; loadOllamaModels: () => Promise; loadOpenaiModels: () => Promise; setSetting: ( key: K, value: LauncherConfig[K], ) => void; setAssistantSetting: ( key: K, value: LauncherConfig["assistant"][K], ) => void; setFeatureFlag: ( 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((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("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>) => { set(patch); }, detectJava: async () => { set({ isDetectingJava: true }); try { const installs = await invoke("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( "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(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( "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("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("get_config_path"); const content = await invoke("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("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("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, })); }, }));