aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/ui/src/stores
diff options
context:
space:
mode:
Diffstat (limited to 'ui/src/stores')
-rw-r--r--ui/src/stores/assistant.svelte.ts166
-rw-r--r--ui/src/stores/auth.svelte.ts192
-rw-r--r--ui/src/stores/game.svelte.ts78
-rw-r--r--ui/src/stores/instances.svelte.ts109
-rw-r--r--ui/src/stores/logs.svelte.ts151
-rw-r--r--ui/src/stores/releases.svelte.ts36
-rw-r--r--ui/src/stores/settings.svelte.ts570
-rw-r--r--ui/src/stores/ui.svelte.ts32
8 files changed, 0 insertions, 1334 deletions
diff --git a/ui/src/stores/assistant.svelte.ts b/ui/src/stores/assistant.svelte.ts
deleted file mode 100644
index a3f47ea..0000000
--- a/ui/src/stores/assistant.svelte.ts
+++ /dev/null
@@ -1,166 +0,0 @@
-import { invoke } from "@tauri-apps/api/core";
-import { listen, type UnlistenFn } from "@tauri-apps/api/event";
-
-export interface GenerationStats {
- total_duration: number;
- load_duration: number;
- prompt_eval_count: number;
- prompt_eval_duration: number;
- eval_count: number;
- eval_duration: number;
-}
-
-export interface Message {
- role: "user" | "assistant" | "system";
- content: string;
- stats?: GenerationStats;
-}
-
-interface StreamChunk {
- content: string;
- done: boolean;
- stats?: GenerationStats;
-}
-
-// Module-level state using $state
-let messages = $state<Message[]>([]);
-let isProcessing = $state(false);
-let isProviderHealthy = $state(false);
-let streamingContent = "";
-let initialized = false;
-let streamUnlisten: UnlistenFn | null = null;
-
-async function init() {
- if (initialized) return;
- initialized = true;
- await checkHealth();
-}
-
-async function checkHealth() {
- try {
- isProviderHealthy = await invoke("assistant_check_health");
- } catch (e) {
- console.error("Failed to check provider health:", e);
- isProviderHealthy = false;
- }
-}
-
-function finishStreaming() {
- isProcessing = false;
- streamingContent = "";
- if (streamUnlisten) {
- streamUnlisten();
- streamUnlisten = null;
- }
-}
-
-async function sendMessage(
- content: string,
- isEnabled: boolean,
- provider: string,
- endpoint: string,
-) {
- if (!content.trim()) return;
- if (!isEnabled) {
- messages = [
- ...messages,
- {
- role: "assistant",
- content: "Assistant is disabled. Enable it in Settings > AI Assistant.",
- },
- ];
- return;
- }
-
- // Add user message
- messages = [...messages, { role: "user", content }];
- isProcessing = true;
- streamingContent = "";
-
- // Add empty assistant message for streaming
- messages = [...messages, { role: "assistant", content: "" }];
-
- try {
- // Set up stream listener
- streamUnlisten = await listen<StreamChunk>("assistant-stream", (event) => {
- const chunk = event.payload;
-
- if (chunk.content) {
- streamingContent += chunk.content;
- // Update the last message (assistant's response)
- const lastIdx = messages.length - 1;
- if (lastIdx >= 0 && messages[lastIdx].role === "assistant") {
- messages[lastIdx] = {
- ...messages[lastIdx],
- content: streamingContent,
- };
- // Trigger reactivity
- messages = [...messages];
- }
- }
-
- if (chunk.done) {
- if (chunk.stats) {
- const lastIdx = messages.length - 1;
- if (lastIdx >= 0 && messages[lastIdx].role === "assistant") {
- messages[lastIdx] = {
- ...messages[lastIdx],
- stats: chunk.stats,
- };
- messages = [...messages];
- }
- }
- finishStreaming();
- }
- });
-
- // Start streaming chat
- await invoke<string>("assistant_chat_stream", {
- messages: messages.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 lastIdx = messages.length - 1;
- if (lastIdx >= 0 && messages[lastIdx].role === "assistant") {
- messages[lastIdx] = {
- role: "assistant",
- content: `Error: ${errorMessage}${helpText}`,
- };
- messages = [...messages];
- }
-
- finishStreaming();
- }
-}
-
-function clearHistory() {
- messages = [];
- streamingContent = "";
-}
-
-// Export as an object with getters for reactive access
-export const assistantState = {
- get messages() {
- return messages;
- },
- get isProcessing() {
- return isProcessing;
- },
- get isProviderHealthy() {
- return isProviderHealthy;
- },
- init,
- checkHealth,
- sendMessage,
- clearHistory,
-};
diff --git a/ui/src/stores/auth.svelte.ts b/ui/src/stores/auth.svelte.ts
deleted file mode 100644
index 1b613a7..0000000
--- a/ui/src/stores/auth.svelte.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-import { invoke } from "@tauri-apps/api/core";
-import { open } from "@tauri-apps/plugin-shell";
-import { listen, type UnlistenFn } from "@tauri-apps/api/event";
-import type { Account, DeviceCodeResponse } from "../types";
-import { uiState } from "./ui.svelte";
-import { logsState } from "./logs.svelte";
-
-export class AuthState {
- currentAccount = $state<Account | null>(null);
- isLoginModalOpen = $state(false);
- isLogoutConfirmOpen = $state(false);
- loginMode = $state<"select" | "offline" | "microsoft">("select");
- offlineUsername = $state("");
- deviceCodeData = $state<DeviceCodeResponse | null>(null);
- msLoginLoading = $state(false);
- msLoginStatus = $state("Waiting for authorization...");
-
- private pollInterval: ReturnType<typeof setInterval> | null = null;
- private isPollingRequestActive = false;
- private authProgressUnlisten: UnlistenFn | null = null;
-
- async checkAccount() {
- try {
- const acc = await invoke("get_active_account");
- this.currentAccount = acc as Account | null;
- } catch (e) {
- console.error("Failed to check account:", e);
- }
- }
-
- openLoginModal() {
- if (this.currentAccount) {
- // Show custom logout confirmation dialog
- this.isLogoutConfirmOpen = true;
- return;
- }
- this.resetLoginState();
- this.isLoginModalOpen = true;
- }
-
- cancelLogout() {
- this.isLogoutConfirmOpen = false;
- }
-
- async confirmLogout() {
- this.isLogoutConfirmOpen = false;
- try {
- await invoke("logout");
- this.currentAccount = null;
- uiState.setStatus("Logged out successfully");
- } catch (e) {
- console.error("Logout failed:", e);
- }
- }
-
- closeLoginModal() {
- this.stopPolling();
- this.isLoginModalOpen = false;
- }
-
- resetLoginState() {
- this.loginMode = "select";
- this.offlineUsername = "";
- this.deviceCodeData = null;
- this.msLoginLoading = false;
- }
-
- async performOfflineLogin() {
- if (!this.offlineUsername) return;
- try {
- this.currentAccount = (await invoke("login_offline", {
- username: this.offlineUsername,
- })) as Account;
- this.isLoginModalOpen = false;
- } catch (e) {
- alert("Login failed: " + e);
- }
- }
-
- async startMicrosoftLogin() {
- this.loginMode = "microsoft";
- this.msLoginLoading = true;
- this.msLoginStatus = "Waiting for authorization...";
- this.stopPolling();
-
- // Setup auth progress listener
- this.setupAuthProgressListener();
-
- try {
- this.deviceCodeData = (await invoke("start_microsoft_login")) as DeviceCodeResponse;
-
- if (this.deviceCodeData) {
- try {
- await navigator.clipboard.writeText(this.deviceCodeData.user_code);
- } catch (e) {
- console.error("Clipboard failed", e);
- }
-
- open(this.deviceCodeData.verification_uri);
- logsState.addLog(
- "info",
- "Auth",
- "Microsoft login started, waiting for browser authorization...",
- );
-
- console.log("Starting polling for token...");
- const intervalMs = (this.deviceCodeData.interval || 5) * 1000;
- this.pollInterval = setInterval(
- () => this.checkLoginStatus(this.deviceCodeData!.device_code),
- intervalMs,
- );
- }
- } catch (e) {
- logsState.addLog("error", "Auth", `Failed to start Microsoft login: ${e}`);
- alert("Failed to start Microsoft login: " + e);
- this.loginMode = "select";
- } finally {
- this.msLoginLoading = false;
- }
- }
-
- private async setupAuthProgressListener() {
- // Clean up previous listener if exists
- if (this.authProgressUnlisten) {
- this.authProgressUnlisten();
- this.authProgressUnlisten = null;
- }
-
- this.authProgressUnlisten = await listen<string>("auth-progress", (event) => {
- const message = event.payload;
- this.msLoginStatus = message;
- logsState.addLog("info", "Auth", message);
- });
- }
-
- private cleanupAuthListener() {
- if (this.authProgressUnlisten) {
- this.authProgressUnlisten();
- this.authProgressUnlisten = null;
- }
- }
-
- stopPolling() {
- if (this.pollInterval) {
- clearInterval(this.pollInterval);
- this.pollInterval = null;
- }
- }
-
- async checkLoginStatus(deviceCode: string) {
- if (this.isPollingRequestActive) return;
- this.isPollingRequestActive = true;
-
- console.log("Polling Microsoft API...");
- try {
- this.currentAccount = (await invoke("complete_microsoft_login", {
- deviceCode,
- })) as Account;
-
- console.log("Login Successful!", this.currentAccount);
- this.stopPolling();
- this.cleanupAuthListener();
- this.isLoginModalOpen = false;
- logsState.addLog(
- "info",
- "Auth",
- `Login successful! Welcome, ${this.currentAccount.username}`,
- );
- uiState.setStatus("Welcome back, " + this.currentAccount.username);
- } catch (e: any) {
- const errStr = e.toString();
- if (errStr.includes("authorization_pending")) {
- console.log("Status: Waiting for user to authorize...");
- } else {
- console.error("Polling Error:", errStr);
- this.msLoginStatus = "Error: " + errStr;
- logsState.addLog("error", "Auth", `Login error: ${errStr}`);
-
- if (errStr.includes("expired_token") || errStr.includes("access_denied")) {
- this.stopPolling();
- this.cleanupAuthListener();
- alert("Login failed: " + errStr);
- this.loginMode = "select";
- }
- }
- } finally {
- this.isPollingRequestActive = false;
- }
- }
-}
-
-export const authState = new AuthState();
diff --git a/ui/src/stores/game.svelte.ts b/ui/src/stores/game.svelte.ts
deleted file mode 100644
index 504d108..0000000
--- a/ui/src/stores/game.svelte.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { invoke } from "@tauri-apps/api/core";
-import type { Version } from "../types";
-import { uiState } from "./ui.svelte";
-import { authState } from "./auth.svelte";
-import { instancesState } from "./instances.svelte";
-
-export class GameState {
- versions = $state<Version[]>([]);
- selectedVersion = $state("");
-
- constructor() {
- // Constructor intentionally empty
- // Instance switching handled in App.svelte with $effect
- }
-
- get latestRelease() {
- return this.versions.find((v) => v.type === "release");
- }
-
- async loadVersions(instanceId?: string) {
- const id = instanceId || instancesState.activeInstanceId;
- if (!id) {
- this.versions = [];
- return;
- }
-
- try {
- this.versions = await invoke<Version[]>("get_versions", {
- instanceId: id,
- });
- // Don't auto-select version here - let BottomBar handle version selection
- // based on installed versions only
- } catch (e) {
- console.error("Failed to fetch versions:", e);
- uiState.setStatus("Error fetching versions: " + e);
- }
- }
-
- async startGame() {
- if (!authState.currentAccount) {
- alert("Please login first!");
- authState.openLoginModal();
- return;
- }
-
- if (!this.selectedVersion) {
- alert("Please select a version!");
- return;
- }
-
- if (!instancesState.activeInstanceId) {
- alert("Please select an instance first!");
- uiState.setView("instances");
- return;
- }
-
- uiState.setStatus("Preparing to launch " + this.selectedVersion + "...");
- console.log(
- "Invoking start_game for version:",
- this.selectedVersion,
- "instance:",
- instancesState.activeInstanceId,
- );
- try {
- const msg = await invoke<string>("start_game", {
- instanceId: instancesState.activeInstanceId,
- versionId: this.selectedVersion,
- });
- console.log("Response:", msg);
- uiState.setStatus(msg);
- } catch (e) {
- console.error(e);
- uiState.setStatus("Error: " + e);
- }
- }
-}
-
-export const gameState = new GameState();
diff --git a/ui/src/stores/instances.svelte.ts b/ui/src/stores/instances.svelte.ts
deleted file mode 100644
index f4ac4e9..0000000
--- a/ui/src/stores/instances.svelte.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { invoke } from "@tauri-apps/api/core";
-import type { Instance } from "../types";
-import { uiState } from "./ui.svelte";
-
-export class InstancesState {
- instances = $state<Instance[]>([]);
- activeInstanceId = $state<string | null>(null);
- get activeInstance(): Instance | null {
- if (!this.activeInstanceId) return null;
- return this.instances.find((i) => i.id === this.activeInstanceId) || null;
- }
-
- async loadInstances() {
- try {
- this.instances = await invoke<Instance[]>("list_instances");
- const active = await invoke<Instance | null>("get_active_instance");
- if (active) {
- this.activeInstanceId = active.id;
- } else if (this.instances.length > 0) {
- // If no active instance but instances exist, set the first one as active
- await this.setActiveInstance(this.instances[0].id);
- }
- } catch (e) {
- console.error("Failed to load instances:", e);
- uiState.setStatus("Error loading instances: " + e);
- }
- }
-
- async createInstance(name: string): Promise<Instance | null> {
- try {
- const instance = await invoke<Instance>("create_instance", { name });
- await this.loadInstances();
- uiState.setStatus(`Instance "${name}" created successfully`);
- return instance;
- } catch (e) {
- console.error("Failed to create instance:", e);
- uiState.setStatus("Error creating instance: " + e);
- return null;
- }
- }
-
- async deleteInstance(id: string) {
- try {
- await invoke("delete_instance", { instanceId: id });
- await this.loadInstances();
- // If deleted instance was active, set another as active
- if (this.activeInstanceId === id) {
- if (this.instances.length > 0) {
- await this.setActiveInstance(this.instances[0].id);
- } else {
- this.activeInstanceId = null;
- }
- }
- uiState.setStatus("Instance deleted successfully");
- } catch (e) {
- console.error("Failed to delete instance:", e);
- uiState.setStatus("Error deleting instance: " + e);
- }
- }
-
- async updateInstance(instance: Instance) {
- try {
- await invoke("update_instance", { instance });
- await this.loadInstances();
- uiState.setStatus("Instance updated successfully");
- } catch (e) {
- console.error("Failed to update instance:", e);
- uiState.setStatus("Error updating instance: " + e);
- }
- }
-
- async setActiveInstance(id: string) {
- try {
- await invoke("set_active_instance", { instanceId: id });
- this.activeInstanceId = id;
- uiState.setStatus("Active instance changed");
- } catch (e) {
- console.error("Failed to set active instance:", e);
- uiState.setStatus("Error setting active instance: " + e);
- }
- }
-
- async duplicateInstance(id: string, newName: string): Promise<Instance | null> {
- try {
- const instance = await invoke<Instance>("duplicate_instance", {
- instanceId: id,
- newName,
- });
- await this.loadInstances();
- uiState.setStatus(`Instance duplicated as "${newName}"`);
- return instance;
- } catch (e) {
- console.error("Failed to duplicate instance:", e);
- uiState.setStatus("Error duplicating instance: " + e);
- return null;
- }
- }
-
- async getInstance(id: string): Promise<Instance | null> {
- try {
- return await invoke<Instance>("get_instance", { instanceId: id });
- } catch (e) {
- console.error("Failed to get instance:", e);
- return null;
- }
- }
-}
-
-export const instancesState = new InstancesState();
diff --git a/ui/src/stores/logs.svelte.ts b/ui/src/stores/logs.svelte.ts
deleted file mode 100644
index c9d4acc..0000000
--- a/ui/src/stores/logs.svelte.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-import { listen } from "@tauri-apps/api/event";
-
-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";
-}
-
-export class LogsState {
- logs = $state<LogEntry[]>([]);
- private nextId = 0;
- private maxLogs = 5000;
-
- // Track all unique sources for filtering
- sources = $state<Set<string>>(new Set(["Launcher"]));
-
- constructor() {
- this.addLog("info", "Launcher", "Logs initialized");
- }
-
- addLog(level: LogEntry["level"], source: string, message: string) {
- const now = new Date();
- const timestamp =
- now.toLocaleTimeString() + "." + now.getMilliseconds().toString().padStart(3, "0");
-
- this.logs.push({
- id: this.nextId++,
- timestamp,
- level,
- source,
- message,
- });
-
- // Track source
- if (!this.sources.has(source)) {
- this.sources = new Set([...this.sources, source]);
- }
-
- if (this.logs.length > this.maxLogs) {
- this.logs.shift();
- }
- }
-
- // Parse game output and extract level/source
- addGameLog(rawLine: string, isStderr: boolean) {
- 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]}`;
- this.addLog(level, source, message);
- } else {
- // Fallback: couldn't parse, use stderr as error indicator
- const level = isStderr ? "error" : "info";
- this.addLog(level, "Game", rawLine);
- }
- }
-
- clear() {
- this.logs = [];
- this.sources = new Set(["Launcher"]);
- this.addLog("info", "Launcher", "Logs cleared");
- }
-
- // Export with filter support
- exportLogs(filteredLogs: LogEntry[]): string {
- return filteredLogs
- .map((l) => `[${l.timestamp}] [${l.source}/${l.level.toUpperCase()}] ${l.message}`)
- .join("\n");
- }
-
- private initialized = false;
-
- async init() {
- if (this.initialized) return;
- this.initialized = true;
-
- // General Launcher Logs
- await listen<string>("launcher-log", (e) => {
- this.addLog("info", "Launcher", e.payload);
- });
-
- // Game Stdout - parse log level
- await listen<string>("game-stdout", (e) => {
- this.addGameLog(e.payload, false);
- });
-
- // Game Stderr - parse log level, default to error
- await listen<string>("game-stderr", (e) => {
- this.addGameLog(e.payload, true);
- });
-
- // Download Events (Summarized)
- await listen("download-start", (e) => {
- this.addLog("info", "Downloader", `Starting batch download of ${e.payload} files...`);
- });
-
- await listen("download-complete", () => {
- this.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")) {
- this.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) {
- this.addLog("info", "JavaInstaller", `Downloading Java: ${p.file_name}`);
- } else if (p.status === "Completed") {
- this.addLog("info", "JavaInstaller", `Java installed: ${p.file_name}`);
- } else if (p.status === "Error") {
- this.addLog("error", "JavaInstaller", `Java download error`);
- }
- });
- }
-}
-
-export const logsState = new LogsState();
diff --git a/ui/src/stores/releases.svelte.ts b/ui/src/stores/releases.svelte.ts
deleted file mode 100644
index c858abb..0000000
--- a/ui/src/stores/releases.svelte.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { invoke } from "@tauri-apps/api/core";
-
-export interface GithubRelease {
- tag_name: string;
- name: string;
- published_at: string;
- body: string;
- html_url: string;
-}
-
-export class ReleasesState {
- releases = $state<GithubRelease[]>([]);
- isLoading = $state(false);
- isLoaded = $state(false);
- error = $state<string | null>(null);
-
- async loadReleases() {
- // If already loaded or currently loading, skip to prevent duplicate requests
- if (this.isLoaded || this.isLoading) return;
-
- this.isLoading = true;
- this.error = null;
-
- try {
- this.releases = await invoke<GithubRelease[]>("get_github_releases");
- this.isLoaded = true;
- } catch (e) {
- console.error("Failed to load releases:", e);
- this.error = String(e);
- } finally {
- this.isLoading = false;
- }
- }
-}
-
-export const releasesState = new ReleasesState();
diff --git a/ui/src/stores/settings.svelte.ts b/ui/src/stores/settings.svelte.ts
deleted file mode 100644
index 5d20050..0000000
--- a/ui/src/stores/settings.svelte.ts
+++ /dev/null
@@ -1,570 +0,0 @@
-import { invoke } from "@tauri-apps/api/core";
-import { convertFileSrc } from "@tauri-apps/api/core";
-import { listen, type UnlistenFn } from "@tauri-apps/api/event";
-import type {
- JavaCatalog,
- JavaDownloadProgress,
- JavaDownloadSource,
- JavaInstallation,
- JavaReleaseInfo,
- LauncherConfig,
- ModelInfo,
- PendingJavaDownload,
-} from "../types";
-import { uiState } from "./ui.svelte";
-
-export class SettingsState {
- settings = $state<LauncherConfig>({
- min_memory: 1024,
- max_memory: 2048,
- java_path: "java",
- width: 854,
- height: 480,
- download_threads: 32,
- enable_gpu_acceleration: false,
- enable_visual_effects: true,
- active_effect: "constellation",
- theme: "dark",
- custom_background_path: undefined,
- log_upload_service: "paste.rs",
- pastebin_api_key: undefined,
- assistant: {
- enabled: true,
- llm_provider: "ollama",
- ollama_endpoint: "http://localhost:11434",
- ollama_model: "llama3",
- openai_api_key: undefined,
- openai_endpoint: "https://api.openai.com/v1",
- openai_model: "gpt-3.5-turbo",
- system_prompt:
- "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.",
- response_language: "auto",
- tts_enabled: false,
- tts_provider: "disabled",
- },
- use_shared_caches: false,
- keep_legacy_per_instance_storage: true,
- feature_flags: {
- demo_user: false,
- quick_play_enabled: false,
- quick_play_path: undefined,
- quick_play_singleplayer: true,
- quick_play_multiplayer_server: undefined,
- },
- });
-
- // Convert background path to proper asset URL
- get backgroundUrl(): string | undefined {
- if (this.settings.custom_background_path) {
- return convertFileSrc(this.settings.custom_background_path);
- }
- return undefined;
- }
- javaInstallations = $state<JavaInstallation[]>([]);
- isDetectingJava = $state(false);
-
- // Java download modal state
- showJavaDownloadModal = $state(false);
- selectedDownloadSource = $state<JavaDownloadSource>("adoptium");
-
- // Java catalog state
- javaCatalog = $state<JavaCatalog | null>(null);
- isLoadingCatalog = $state(false);
- catalogError = $state("");
-
- // Version selection state
- selectedMajorVersion = $state<number | null>(null);
- selectedImageType = $state<"jre" | "jdk">("jre");
- showOnlyRecommended = $state(true);
- searchQuery = $state("");
-
- // Download progress state
- isDownloadingJava = $state(false);
- downloadProgress = $state<JavaDownloadProgress | null>(null);
- javaDownloadStatus = $state("");
-
- // Pending downloads
- pendingDownloads = $state<PendingJavaDownload[]>([]);
-
- // AI Model lists
- ollamaModels = $state<ModelInfo[]>([]);
- openaiModels = $state<ModelInfo[]>([]);
- isLoadingOllamaModels = $state(false);
- isLoadingOpenaiModels = $state(false);
- ollamaModelsError = $state("");
- openaiModelsError = $state("");
-
- // Config Editor state
- showConfigEditor = $state(false);
- rawConfigContent = $state("");
- configFilePath = $state("");
- configEditorError = $state("");
-
- // Event listener cleanup
- private progressUnlisten: UnlistenFn | null = null;
-
- async openConfigEditor() {
- this.configEditorError = "";
- try {
- const path = await invoke<string>("get_config_path");
- const content = await invoke<string>("read_raw_config");
- this.configFilePath = path;
- this.rawConfigContent = content;
- this.showConfigEditor = true;
- } catch (e) {
- console.error("Failed to open config editor:", e);
- uiState.setStatus(`Failed to open config: ${e}`);
- }
- }
-
- async saveRawConfig(content: string, closeAfterSave = true) {
- try {
- await invoke("save_raw_config", { content });
- // Reload settings to ensure UI is in sync
- await this.loadSettings();
- if (closeAfterSave) {
- this.showConfigEditor = false;
- }
- uiState.setStatus("Configuration saved successfully!");
- } catch (e) {
- console.error("Failed to save config:", e);
- this.configEditorError = String(e);
- }
- }
-
- closeConfigEditor() {
- this.showConfigEditor = false;
- this.rawConfigContent = "";
- this.configEditorError = "";
- }
-
- // Computed: filtered releases based on selection
- get filteredReleases(): JavaReleaseInfo[] {
- if (!this.javaCatalog) return [];
-
- let releases = this.javaCatalog.releases;
-
- // Filter by major version if selected
- if (this.selectedMajorVersion !== null) {
- releases = releases.filter((r) => r.major_version === this.selectedMajorVersion);
- }
-
- // Filter by image type
- releases = releases.filter((r) => r.image_type === this.selectedImageType);
-
- // Filter by recommended (LTS) versions
- if (this.showOnlyRecommended) {
- releases = releases.filter((r) => r.is_lts);
- }
-
- // Filter by search query
- if (this.searchQuery.trim()) {
- const query = this.searchQuery.toLowerCase();
- releases = releases.filter(
- (r) =>
- r.release_name.toLowerCase().includes(query) ||
- r.version.toLowerCase().includes(query) ||
- r.major_version.toString().includes(query),
- );
- }
-
- return releases;
- }
-
- // Computed: available major versions for display
- get availableMajorVersions(): number[] {
- if (!this.javaCatalog) return [];
- let versions = [...this.javaCatalog.available_major_versions];
-
- // Filter by LTS if showOnlyRecommended is enabled
- if (this.showOnlyRecommended) {
- versions = versions.filter((v) => this.javaCatalog!.lts_versions.includes(v));
- }
-
- // Sort descending (newest first)
- return versions.sort((a, b) => b - a);
- }
-
- // Get installation status for a release: 'installed' | 'download'
- getInstallStatus(release: JavaReleaseInfo): "installed" | "download" {
- // Find installed Java that matches the major version and image type (by path pattern)
- const matchingInstallations = this.javaInstallations.filter((inst) => {
- // Check if this is a DropOut-managed Java (path contains temurin-XX-jre/jdk pattern)
- const pathLower = inst.path.toLowerCase();
- const pattern = `temurin-${release.major_version}-${release.image_type}`;
- return pathLower.includes(pattern);
- });
-
- // If any matching installation exists, it's installed
- return matchingInstallations.length > 0 ? "installed" : "download";
- }
-
- // Computed: selected release details
- get selectedRelease(): JavaReleaseInfo | null {
- if (!this.javaCatalog || this.selectedMajorVersion === null) return null;
- return (
- this.javaCatalog.releases.find(
- (r) =>
- r.major_version === this.selectedMajorVersion && r.image_type === this.selectedImageType,
- ) || null
- );
- }
-
- async loadSettings() {
- try {
- const result = await invoke<LauncherConfig>("get_settings");
- this.settings = result;
- // Force dark mode
- if (this.settings.theme !== "dark") {
- this.settings.theme = "dark";
- this.saveSettings();
- }
- // Ensure custom_background_path is reactive
- if (!this.settings.custom_background_path) {
- this.settings.custom_background_path = undefined;
- }
- } catch (e) {
- console.error("Failed to load settings:", e);
- }
- }
-
- async saveSettings() {
- try {
- // Ensure we clean up any invalid paths before saving
- if (this.settings.custom_background_path === "") {
- this.settings.custom_background_path = undefined;
- }
-
- await invoke("save_settings", { config: this.settings });
- uiState.setStatus("Settings saved!");
- } catch (e) {
- console.error("Failed to save settings:", e);
- uiState.setStatus("Error saving settings: " + e);
- }
- }
-
- async detectJava() {
- this.isDetectingJava = true;
- try {
- this.javaInstallations = await invoke("detect_java");
- if (this.javaInstallations.length === 0) {
- uiState.setStatus("No Java installations found");
- } else {
- uiState.setStatus(`Found ${this.javaInstallations.length} Java installation(s)`);
- }
- } catch (e) {
- console.error("Failed to detect Java:", e);
- uiState.setStatus("Error detecting Java: " + e);
- } finally {
- this.isDetectingJava = false;
- }
- }
-
- selectJava(path: string) {
- this.settings.java_path = path;
- }
-
- async openJavaDownloadModal() {
- this.showJavaDownloadModal = true;
- this.javaDownloadStatus = "";
- this.catalogError = "";
- this.downloadProgress = null;
-
- // Setup progress event listener
- await this.setupProgressListener();
-
- // Load catalog
- await this.loadJavaCatalog(false);
-
- // Check for pending downloads
- await this.loadPendingDownloads();
- }
-
- async closeJavaDownloadModal() {
- if (!this.isDownloadingJava) {
- this.showJavaDownloadModal = false;
- // Cleanup listener
- if (this.progressUnlisten) {
- this.progressUnlisten();
- this.progressUnlisten = null;
- }
- }
- }
-
- private async setupProgressListener() {
- if (this.progressUnlisten) {
- this.progressUnlisten();
- }
-
- this.progressUnlisten = await listen<JavaDownloadProgress>(
- "java-download-progress",
- (event) => {
- this.downloadProgress = event.payload;
- this.javaDownloadStatus = event.payload.status;
-
- if (event.payload.status === "Completed") {
- this.isDownloadingJava = false;
- setTimeout(async () => {
- await this.detectJava();
- uiState.setStatus(`Java installed successfully!`);
- }, 500);
- } else if (event.payload.status === "Error") {
- this.isDownloadingJava = false;
- }
- },
- );
- }
-
- async loadJavaCatalog(forceRefresh: boolean) {
- this.isLoadingCatalog = true;
- this.catalogError = "";
-
- try {
- const command = forceRefresh ? "refresh_java_catalog" : "fetch_java_catalog";
- this.javaCatalog = await invoke<JavaCatalog>(command);
-
- // Auto-select first LTS version
- if (this.selectedMajorVersion === null && this.javaCatalog.lts_versions.length > 0) {
- // Select most recent LTS (21 or highest)
- const ltsVersions = [...this.javaCatalog.lts_versions].sort((a, b) => b - a);
- this.selectedMajorVersion = ltsVersions[0];
- }
- } catch (e) {
- console.error("Failed to load Java catalog:", e);
- this.catalogError = `Failed to load Java catalog: ${e}`;
- } finally {
- this.isLoadingCatalog = false;
- }
- }
-
- async refreshCatalog() {
- await this.loadJavaCatalog(true);
- uiState.setStatus("Java catalog refreshed");
- }
-
- async loadPendingDownloads() {
- try {
- this.pendingDownloads = await invoke<PendingJavaDownload[]>("get_pending_java_downloads");
- } catch (e) {
- console.error("Failed to load pending downloads:", e);
- }
- }
-
- selectMajorVersion(version: number) {
- this.selectedMajorVersion = version;
- }
-
- async downloadJava() {
- if (!this.selectedRelease || !this.selectedRelease.is_available) {
- uiState.setStatus("Selected Java version is not available for this platform");
- return;
- }
-
- this.isDownloadingJava = true;
- this.javaDownloadStatus = "Starting download...";
- this.downloadProgress = null;
-
- try {
- const result: JavaInstallation = await invoke("download_adoptium_java", {
- majorVersion: this.selectedMajorVersion,
- imageType: this.selectedImageType,
- customPath: null,
- });
-
- this.settings.java_path = result.path;
- await this.detectJava();
-
- setTimeout(() => {
- this.showJavaDownloadModal = false;
- uiState.setStatus(`Java ${this.selectedMajorVersion} is ready to use!`);
- }, 1500);
- } catch (e) {
- console.error("Failed to download Java:", e);
- this.javaDownloadStatus = `Download failed: ${e}`;
- } finally {
- this.isDownloadingJava = false;
- }
- }
-
- async cancelDownload() {
- try {
- await invoke("cancel_java_download");
- this.isDownloadingJava = false;
- this.javaDownloadStatus = "Download cancelled";
- this.downloadProgress = null;
- await this.loadPendingDownloads();
- } catch (e) {
- console.error("Failed to cancel download:", e);
- }
- }
-
- async resumeDownloads() {
- if (this.pendingDownloads.length === 0) return;
-
- this.isDownloadingJava = true;
- this.javaDownloadStatus = "Resuming download...";
-
- try {
- const installed = await invoke<JavaInstallation[]>("resume_java_downloads");
- if (installed.length > 0) {
- this.settings.java_path = installed[0].path;
- await this.detectJava();
- uiState.setStatus(`Resumed and installed ${installed.length} Java version(s)`);
- }
- await this.loadPendingDownloads();
- } catch (e) {
- console.error("Failed to resume downloads:", e);
- this.javaDownloadStatus = `Resume failed: ${e}`;
- } finally {
- this.isDownloadingJava = false;
- }
- }
-
- // Format bytes to human readable
- formatBytes(bytes: number): string {
- if (bytes === 0) return "0 B";
- const k = 1024;
- const sizes = ["B", "KB", "MB", "GB"];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
- }
-
- // Format seconds to human readable
- formatTime(seconds: number): string {
- if (seconds === 0 || !isFinite(seconds)) return "--";
- if (seconds < 60) return `${Math.round(seconds)}s`;
- if (seconds < 3600) {
- const mins = Math.floor(seconds / 60);
- const secs = Math.round(seconds % 60);
- return `${mins}m ${secs}s`;
- }
- const hours = Math.floor(seconds / 3600);
- const mins = Math.floor((seconds % 3600) / 60);
- return `${hours}h ${mins}m`;
- }
-
- // Format date string
- formatDate(dateStr: string | null): string {
- if (!dateStr) return "--";
- try {
- const date = new Date(dateStr);
- return date.toLocaleDateString("en-US", {
- year: "2-digit",
- month: "2-digit",
- day: "2-digit",
- });
- } catch {
- return "--";
- }
- }
-
- // Legacy compatibility
- get availableJavaVersions(): number[] {
- return this.availableMajorVersions;
- }
-
- // AI Model loading methods
- async loadOllamaModels() {
- this.isLoadingOllamaModels = true;
- this.ollamaModelsError = "";
-
- try {
- const models = await invoke<ModelInfo[]>("list_ollama_models", {
- endpoint: this.settings.assistant.ollama_endpoint,
- });
- this.ollamaModels = models;
-
- // If no model is selected or selected model isn't available, select the first one
- if (models.length > 0) {
- const currentModel = this.settings.assistant.ollama_model;
- const modelExists = models.some((m) => m.id === currentModel);
- if (!modelExists) {
- this.settings.assistant.ollama_model = models[0].id;
- }
- }
- } catch (e) {
- console.error("Failed to load Ollama models:", e);
- this.ollamaModelsError = String(e);
- this.ollamaModels = [];
- } finally {
- this.isLoadingOllamaModels = false;
- }
- }
-
- async loadOpenaiModels() {
- if (!this.settings.assistant.openai_api_key) {
- this.openaiModelsError = "API key required";
- this.openaiModels = [];
- return;
- }
-
- this.isLoadingOpenaiModels = true;
- this.openaiModelsError = "";
-
- try {
- const models = await invoke<ModelInfo[]>("list_openai_models");
- this.openaiModels = models;
-
- // If no model is selected or selected model isn't available, select the first one
- if (models.length > 0) {
- const currentModel = this.settings.assistant.openai_model;
- const modelExists = models.some((m) => m.id === currentModel);
- if (!modelExists) {
- this.settings.assistant.openai_model = models[0].id;
- }
- }
- } catch (e) {
- console.error("Failed to load OpenAI models:", e);
- this.openaiModelsError = String(e);
- this.openaiModels = [];
- } finally {
- this.isLoadingOpenaiModels = false;
- }
- }
-
- // Computed: get model options for current provider
- get currentModelOptions(): { value: string; label: string; details?: string }[] {
- const provider = this.settings.assistant.llm_provider;
-
- if (provider === "ollama") {
- if (this.ollamaModels.length === 0) {
- // Return fallback options if no models loaded
- return [
- { value: "llama3", label: "Llama 3" },
- { value: "llama3.1", label: "Llama 3.1" },
- { value: "llama3.2", label: "Llama 3.2" },
- { value: "mistral", label: "Mistral" },
- { value: "gemma2", label: "Gemma 2" },
- { value: "qwen2.5", label: "Qwen 2.5" },
- { value: "phi3", label: "Phi-3" },
- { value: "codellama", label: "Code Llama" },
- ];
- }
- return this.ollamaModels.map((m) => ({
- value: m.id,
- label: m.name,
- details: m.size ? `${m.size}${m.details ? ` - ${m.details}` : ""}` : m.details,
- }));
- } else if (provider === "openai") {
- if (this.openaiModels.length === 0) {
- // Return fallback options if no models loaded
- return [
- { value: "gpt-4o", label: "GPT-4o" },
- { value: "gpt-4o-mini", label: "GPT-4o Mini" },
- { value: "gpt-4-turbo", label: "GPT-4 Turbo" },
- { value: "gpt-4", label: "GPT-4" },
- { value: "gpt-3.5-turbo", label: "GPT-3.5 Turbo" },
- ];
- }
- return this.openaiModels.map((m) => ({
- value: m.id,
- label: m.name,
- details: m.details,
- }));
- }
-
- return [];
- }
-}
-
-export const settingsState = new SettingsState();
diff --git a/ui/src/stores/ui.svelte.ts b/ui/src/stores/ui.svelte.ts
deleted file mode 100644
index e88f6b4..0000000
--- a/ui/src/stores/ui.svelte.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { type ViewType } from "../types";
-
-export class UIState {
- currentView: ViewType = $state("home");
- status = $state("Ready");
- showConsole = $state(false);
- appVersion = $state("...");
-
- private statusTimeout: ReturnType<typeof setTimeout> | null = null;
-
- setStatus(msg: string) {
- if (this.statusTimeout) clearTimeout(this.statusTimeout);
-
- this.status = msg;
-
- if (msg !== "Ready") {
- this.statusTimeout = setTimeout(() => {
- this.status = "Ready";
- }, 5000);
- }
- }
-
- toggleConsole() {
- this.showConsole = !this.showConsole;
- }
-
- setView(view: ViewType) {
- this.currentView = view;
- }
-}
-
-export const uiState = new UIState();