diff options
Diffstat (limited to 'packages/ui/src/components/SettingsView.svelte')
| -rw-r--r-- | packages/ui/src/components/SettingsView.svelte | 1217 |
1 files changed, 1217 insertions, 0 deletions
diff --git a/packages/ui/src/components/SettingsView.svelte b/packages/ui/src/components/SettingsView.svelte new file mode 100644 index 0000000..0020506 --- /dev/null +++ b/packages/ui/src/components/SettingsView.svelte @@ -0,0 +1,1217 @@ +<script lang="ts"> + import { open } from "@tauri-apps/plugin-dialog"; + import { settingsState } from "../stores/settings.svelte"; + import CustomSelect from "./CustomSelect.svelte"; + import ConfigEditorModal from "./ConfigEditorModal.svelte"; + import { onMount } from "svelte"; + import { RefreshCw, FileJson } from "lucide-svelte"; + + // Use convertFileSrc directly from settingsState.backgroundUrl for cleaner approach + // or use the imported one if passing raw path. + import { convertFileSrc } from "@tauri-apps/api/core"; + + const effectOptions = [ + { value: "saturn", label: "Saturn" }, + { value: "constellation", label: "Network (Constellation)" } + ]; + + const logServiceOptions = [ + { value: "paste.rs", label: "paste.rs (Free, No Account)" }, + { value: "pastebin.com", label: "pastebin.com (Requires API Key)" } + ]; + + const llmProviderOptions = [ + { value: "ollama", label: "Ollama (Local)" }, + { value: "openai", label: "OpenAI (Remote)" } + ]; + + const languageOptions = [ + { value: "auto", label: "Auto (Match User)" }, + { value: "English", label: "English" }, + { value: "Chinese", label: "中文" }, + { value: "Japanese", label: "日本語" }, + { value: "Korean", label: "한국어" }, + { value: "Spanish", label: "Español" }, + { value: "French", label: "Français" }, + { value: "German", label: "Deutsch" }, + { value: "Russian", label: "Русский" }, + ]; + + const ttsProviderOptions = [ + { value: "disabled", label: "Disabled" }, + { value: "piper", label: "Piper TTS (Local)" }, + { value: "edge", label: "Edge TTS (Online)" }, + ]; + + const personas = [ + { + value: "default", + label: "Minecraft Expert (Default)", + 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." + }, + { + value: "technical", + label: "Technical Debugger", + prompt: "You are a technical support specialist for Minecraft. Focus strictly on analyzing logs, identifying crash causes, and providing technical solutions. Be precise and avoid conversational filler." + }, + { + value: "concise", + label: "Concise Helper", + prompt: "You are a direct and concise assistant. Provide answers in as few words as possible while remaining accurate. Use bullet points for lists." + }, + { + value: "explain", + label: "Teacher / Explainer", + prompt: "You are a patient teacher. Explain Minecraft concepts, redstone mechanics, and mod features in simple, easy-to-understand terms suitable for beginners." + }, + { + value: "pirate", + label: "Pirate Captain", + prompt: "You are a salty Minecraft Pirate Captain! Yarr! Speak like a pirate while helping the crew (the user) with their blocky adventures. Use terms like 'matey', 'landlubber', and 'treasure'." + } + ]; + + let selectedPersona = $state(""); + + function applyPersona(value: string) { + const persona = personas.find(p => p.value === value); + if (persona) { + settingsState.settings.assistant.system_prompt = persona.prompt; + selectedPersona = value; // Keep selected to show what's active + } + } + + function resetSystemPrompt() { + const defaultPersona = personas.find(p => p.value === "default"); + if (defaultPersona) { + settingsState.settings.assistant.system_prompt = defaultPersona.prompt; + selectedPersona = "default"; + } + } + + // Load models when assistant settings are shown + function loadModelsForProvider() { + if (settingsState.settings.assistant.llm_provider === "ollama") { + settingsState.loadOllamaModels(); + } else if (settingsState.settings.assistant.llm_provider === "openai") { + settingsState.loadOpenaiModels(); + } + } + + async function selectBackground() { + try { + const selected = await open({ + multiple: false, + filters: [ + { + name: "Images", + extensions: ["png", "jpg", "jpeg", "webp", "gif"], + }, + ], + }); + + if (selected && typeof selected === "string") { + settingsState.settings.custom_background_path = selected; + settingsState.saveSettings(); + } + } catch (e) { + console.error("Failed to select background:", e); + } + } + + function clearBackground() { + settingsState.settings.custom_background_path = undefined; + settingsState.saveSettings(); + } + + let migrating = $state(false); + async function runMigrationToSharedCaches() { + if (migrating) return; + migrating = true; + try { + const { invoke } = await import("@tauri-apps/api/core"); + const result = await invoke<{ + moved_files: number; + hardlinks: number; + copies: number; + saved_mb: number; + }>("migrate_shared_caches"); + + // Reload settings to reflect changes + await settingsState.loadSettings(); + + // Show success message + const msg = `Migration complete! ${result.moved_files} files (${result.hardlinks} hardlinks, ${result.copies} copies), ${result.saved_mb.toFixed(2)} MB saved.`; + console.log(msg); + alert(msg); + } catch (e) { + console.error("Migration failed:", e); + alert(`Migration failed: ${e}`); + } finally { + migrating = false; + } + } +</script> + +<div class="h-full flex flex-col p-6 overflow-hidden"> + <div class="flex items-center justify-between mb-6"> + <h2 class="text-3xl font-black bg-clip-text text-transparent bg-gradient-to-r dark:from-white dark:to-white/60 from-gray-900 to-gray-600">Settings</h2> + + <button + onclick={() => settingsState.openConfigEditor()} + class="p-2 hover:bg-white/10 rounded-lg text-zinc-400 hover:text-white transition-colors flex items-center gap-2 text-sm border border-transparent hover:border-white/5" + title="Open Settings JSON" + > + <FileJson size={18} /> + <span class="hidden sm:inline">Open JSON</span> + </button> + </div> + + <div class="flex-1 overflow-y-auto pr-2 space-y-6 custom-scrollbar pb-10"> + + <!-- Appearance / Background --> + <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm"> + <h3 class="text-xs font-bold uppercase tracking-widest dark:text-white/40 text-black/40 mb-6 flex items-center gap-2"> + Appearance + </h3> + + <div class="space-y-4"> + <div> + <label class="block text-sm font-medium dark:text-white/70 text-black/70 mb-3">Custom Background Image</label> + + <div class="flex items-center gap-6"> + <!-- Preview --> + <div class="w-40 h-24 rounded-xl overflow-hidden dark:bg-black/50 bg-gray-200 border dark:border-white/10 border-black/10 relative group shadow-lg"> + {#if settingsState.settings.custom_background_path} + <img + src={convertFileSrc(settingsState.settings.custom_background_path)} + alt="Background Preview" + class="w-full h-full object-cover" + onerror={(e) => { + console.error("Failed to load image:", settingsState.settings.custom_background_path, e); + // e.currentTarget.style.display = 'none'; + }} + /> + {:else} + <div class="w-full h-full bg-gradient-to-br from-emerald-900 via-zinc-900 to-indigo-950 opacity-100"></div> + <div class="absolute inset-0 flex items-center justify-center text-[10px] text-white/50 bg-black/20 ">Default Gradient</div> + {/if} + </div> + + <!-- Actions --> + <div class="flex flex-col gap-2"> + <button + onclick={selectBackground} + class="dark:bg-white/10 dark:hover:bg-white/20 bg-black/5 hover:bg-black/10 dark:text-white text-black px-4 py-2 rounded-lg text-sm transition-colors border dark:border-white/5 border-black/5" + > + Select Image + </button> + + {#if settingsState.settings.custom_background_path} + <button + onclick={clearBackground} + class="text-red-400 hover:text-red-300 text-sm px-4 py-1 text-left transition-colors" + > + Reset to Default + </button> + {/if} + </div> + </div> + <p class="text-xs dark:text-white/30 text-black/40 mt-3"> + Select an image from your computer to replace the default gradient background. + Supported formats: PNG, JPG, WEBP, GIF. + </p> + </div> + + <!-- Visual Settings --> + <div class="pt-4 border-t dark:border-white/5 border-black/5 space-y-4"> + <div class="flex items-center justify-between"> + <div> + <h4 class="text-sm font-medium dark:text-white/90 text-black/80" id="visual-effects-label">Visual Effects</h4> + <p class="text-xs dark:text-white/40 text-black/50 mt-1">Enable particle effects and animated gradients. (Default: On)</p> + </div> + <button + aria-labelledby="visual-effects-label" + onclick={() => { settingsState.settings.enable_visual_effects = !settingsState.settings.enable_visual_effects; settingsState.saveSettings(); }} + class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.enable_visual_effects ? 'bg-indigo-500' : 'dark:bg-white/10 bg-black/10'}" + > + <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.enable_visual_effects ? 'translate-x-5' : 'translate-x-0'}"></div> + </button> + </div> + + {#if settingsState.settings.enable_visual_effects} + <div class="flex items-center justify-between pl-2 border-l-2 dark:border-white/5 border-black/5 ml-1"> + <div> + <h4 class="text-sm font-medium dark:text-white/90 text-black/80" id="theme-effect-label">Theme Effect</h4> + <p class="text-xs dark:text-white/40 text-black/50 mt-1">Select the active visual theme.</p> + </div> + <CustomSelect + options={effectOptions} + bind:value={settingsState.settings.active_effect} + onchange={() => settingsState.saveSettings()} + class="w-52" + /> + </div> + {/if} + + <div class="flex items-center justify-between"> + <div> + <h4 class="text-sm font-medium dark:text-white/90 text-black/80" id="gpu-acceleration-label">GPU Acceleration</h4> + <p class="text-xs dark:text-white/40 text-black/50 mt-1">Enable GPU acceleration for the interface. (Default: Off, Requires Restart)</p> + </div> + <button + aria-labelledby="gpu-acceleration-label" + onclick={() => { settingsState.settings.enable_gpu_acceleration = !settingsState.settings.enable_gpu_acceleration; settingsState.saveSettings(); }} + class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.enable_gpu_acceleration ? 'bg-indigo-500' : 'dark:bg-white/10 bg-black/10'}" + > + <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.enable_gpu_acceleration ? 'translate-x-5' : 'translate-x-0'}"></div> + </button> + </div> + + <!-- Color Theme Switcher --> + <div class="flex items-center justify-between pt-4 border-t dark:border-white/5 border-black/5 opacity-50 cursor-not-allowed"> + <div> + <h4 class="text-sm font-medium dark:text-white/90 text-black/80" id="color-theme-label">Color Theme</h4> + <p class="text-xs dark:text-white/40 text-black/50 mt-1">Interface color mode. (Locked to Dark)</p> + </div> + <div class="flex items-center bg-black/5 dark:bg-white/5 rounded-lg p-1 pointer-events-none"> + <button + disabled + class="px-3 py-1 rounded-md text-xs font-medium transition-all text-gray-500 dark:text-gray-600" + > + Light + </button> + <button + disabled + class="px-3 py-1 rounded-md text-xs font-medium transition-all bg-indigo-500 text-white shadow-sm" + > + Dark + </button> + </div> + </div> + </div> + </div> + </div> + + <!-- Java Path --> + <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm"> + <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2"> + Java Environment + </h3> + <div class="space-y-4"> + <div> + <label for="java-path" class="block text-sm font-medium text-white/70 mb-2">Java Executable Path</label> + <div class="flex gap-2"> + <input + id="java-path" + bind:value={settingsState.settings.java_path} + type="text" + class="bg-black/40 text-white flex-1 px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none font-mono text-xs transition-colors" + placeholder="e.g. java, /usr/bin/java" + /> + <button + onclick={() => settingsState.detectJava()} + disabled={settingsState.isDetectingJava} + class="bg-white/10 hover:bg-white/20 disabled:opacity-50 text-white px-4 py-2 rounded-xl border border-white/5 transition-colors whitespace-nowrap text-sm font-medium" + > + {settingsState.isDetectingJava ? "Detecting..." : "Auto Detect"} + </button> + <button + onclick={() => settingsState.openJavaDownloadModal()} + class="bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 rounded-xl transition-colors whitespace-nowrap text-sm font-medium" + > + Download Java + </button> + </div> + </div> + + {#if settingsState.javaInstallations.length > 0} + <div class="mt-4 space-y-2"> + <p class="text-[10px] text-white/30 uppercase font-bold tracking-wider">Detected Installations</p> + {#each settingsState.javaInstallations as java} + <button + onclick={() => settingsState.selectJava(java.path)} + class="w-full text-left p-3 rounded-lg border transition-all duration-200 group + {settingsState.settings.java_path === java.path + ? 'bg-indigo-500/20 border-indigo-500/30' + : 'bg-black/20 border-white/5 hover:bg-white/5 hover:border-white/10'}" + > + <div class="flex justify-between items-center"> + <div> + <span class="text-white font-mono text-xs font-bold">{java.version}</span> + <span class="text-white/40 text-[10px] ml-2">{java.is_64bit ? "64-bit" : "32-bit"}</span> + </div> + {#if settingsState.settings.java_path === java.path} + <span class="text-indigo-300 text-[10px] font-bold uppercase tracking-wider">Selected</span> + {/if} + </div> + <div class="text-white/30 text-[10px] font-mono truncate mt-1 group-hover:text-white/50">{java.path}</div> + </button> + {/each} + </div> + {/if} + </div> + </div> + + <!-- Memory --> + <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm"> + <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2"> + Memory Allocation (RAM) + </h3> + <div class="grid grid-cols-2 gap-6"> + <div> + <label for="min-memory" class="block text-sm font-medium text-white/70 mb-2">Minimum (MB)</label> + <input + id="min-memory" + bind:value={settingsState.settings.min_memory} + type="number" + class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none transition-colors" + /> + </div> + <div> + <label for="max-memory" class="block text-sm font-medium text-white/70 mb-2">Maximum (MB)</label> + <input + id="max-memory" + bind:value={settingsState.settings.max_memory} + type="number" + class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none transition-colors" + /> + </div> + </div> + </div> + + <!-- Resolution --> + <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm"> + <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2"> + Game Window Size + </h3> + <div class="grid grid-cols-2 gap-6"> + <div> + <label for="window-width" class="block text-sm font-medium text-white/70 mb-2">Width</label> + <input + id="window-width" + bind:value={settingsState.settings.width} + type="number" + class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none transition-colors" + /> + </div> + <div> + <label for="window-height" class="block text-sm font-medium text-white/70 mb-2">Height</label> + <input + id="window-height" + bind:value={settingsState.settings.height} + type="number" + class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none transition-colors" + /> + </div> + </div> + </div> + + <!-- Download Settings --> + <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm"> + <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2"> + Network + </h3> + <div> + <label for="download-threads" class="block text-sm font-medium text-white/70 mb-2">Concurrent Download Threads</label> + <input + id="download-threads" + bind:value={settingsState.settings.download_threads} + type="number" + min="1" + max="128" + class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none transition-colors" + /> + <p class="text-xs text-white/30 mt-2">Higher values usually mean faster downloads but use more CPU/Network.</p> + </div> + </div> + + <!-- Storage & Caches --> + <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm"> + <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2">Storage & Version Caches</h3> + <div class="space-y-4"> + <div class="flex items-center justify-between"> + <div> + <h4 class="text-sm font-medium text-white/90" id="shared-caches-label">Use Shared Caches</h4> + <p class="text-xs text-white/40 mt-1">Store versions/libraries/assets in a global cache shared by all instances.</p> + </div> + <button + aria-labelledby="shared-caches-label" + onclick={() => { settingsState.settings.use_shared_caches = !settingsState.settings.use_shared_caches; settingsState.saveSettings(); }} + class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.use_shared_caches ? 'bg-indigo-500' : 'bg-white/10'}" + > + <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.use_shared_caches ? 'translate-x-5' : 'translate-x-0'}"></div> + </button> + </div> + + <div class="flex items-center justify-between"> + <div> + <h4 class="text-sm font-medium text-white/90" id="legacy-storage-label">Keep Legacy Per-Instance Storage</h4> + <p class="text-xs text-white/40 mt-1">Do not migrate existing instance caches; keep current layout.</p> + </div> + <button + aria-labelledby="legacy-storage-label" + onclick={() => { settingsState.settings.keep_legacy_per_instance_storage = !settingsState.settings.keep_legacy_per_instance_storage; settingsState.saveSettings(); }} + class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.keep_legacy_per_instance_storage ? 'bg-indigo-500' : 'bg-white/10'}" + > + <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.keep_legacy_per_instance_storage ? 'translate-x-5' : 'translate-x-0'}"></div> + </button> + </div> + + <div class="flex items-center justify-between pt-2 border-t border-white/10"> + <div> + <h4 class="text-sm font-medium text-white/90">Run Migration</h4> + <p class="text-xs text-white/40 mt-1">Hard-link or copy existing per-instance caches into the shared cache.</p> + </div> + <button + onclick={runMigrationToSharedCaches} + disabled={migrating} + class="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white text-sm disabled:opacity-50 disabled:cursor-not-allowed" + > + {migrating ? "Migrating..." : "Migrate Now"} + </button> + </div> + </div> + </div> + + <!-- Feature Flags --> + <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm"> + <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2">Feature Flags (Launcher Arguments)</h3> + <div class="space-y-4"> + <div class="flex items-center justify-between"> + <div> + <h4 class="text-sm font-medium text-white/90" id="demo-user-label">Demo User</h4> + <p class="text-xs text-white/40 mt-1">Enable demo-related arguments when rules require them.</p> + </div> + <button + aria-labelledby="demo-user-label" + onclick={() => { settingsState.settings.feature_flags.demo_user = !settingsState.settings.feature_flags.demo_user; settingsState.saveSettings(); }} + class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.feature_flags.demo_user ? 'bg-indigo-500' : 'bg-white/10'}" + > + <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.feature_flags.demo_user ? 'translate-x-5' : 'translate-x-0'}"></div> + </button> + </div> + + <div class="flex items-center justify-between"> + <div> + <h4 class="text-sm font-medium text-white/90" id="quick-play-label">Quick Play</h4> + <p class="text-xs text-white/40 mt-1">Enable quick play singleplayer/multiplayer arguments.</p> + </div> + <button + aria-labelledby="quick-play-label" + onclick={() => { settingsState.settings.feature_flags.quick_play_enabled = !settingsState.settings.feature_flags.quick_play_enabled; settingsState.saveSettings(); }} + class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.feature_flags.quick_play_enabled ? 'bg-indigo-500' : 'bg-white/10'}" + > + <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.feature_flags.quick_play_enabled ? 'translate-x-5' : 'translate-x-0'}"></div> + </button> + </div> + + {#if settingsState.settings.feature_flags.quick_play_enabled} + <div class="grid grid-cols-1 md:grid-cols-2 gap-4 pl-2 border-l-2 border-white/10"> + <div> + <label class="block text-sm font-medium text-white/70 mb-2">Singleplayer World Path</label> + <input + type="text" + bind:value={settingsState.settings.feature_flags.quick_play_path} + placeholder="/path/to/saves/MyWorld" + class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none font-mono text-xs transition-colors" + /> + </div> + <div class="flex items-center justify-between"> + <div> + <h4 class="text-sm font-medium text-white/90" id="qp-singleplayer-label">Prefer Singleplayer</h4> + <p class="text-xs text-white/40 mt-1">If enabled, use singleplayer quick play path.</p> + </div> + <button + aria-labelledby="qp-singleplayer-label" + onclick={() => { settingsState.settings.feature_flags.quick_play_singleplayer = !settingsState.settings.feature_flags.quick_play_singleplayer; settingsState.saveSettings(); }} + class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.feature_flags.quick_play_singleplayer ? 'bg-indigo-500' : 'bg-white/10'}" + > + <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.feature_flags.quick_play_singleplayer ? 'translate-x-5' : 'translate-x-0'}"></div> + </button> + </div> + <div> + <label class="block text-sm font-medium text-white/70 mb-2">Multiplayer Server Address</label> + <input + type="text" + bind:value={settingsState.settings.feature_flags.quick_play_multiplayer_server} + placeholder="example.org:25565" + class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none font-mono text-xs transition-colors" + /> + </div> + </div> + {/if} + </div> + </div> + + <!-- Debug / Logs --> + <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm"> + <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2"> + Debug & Logs + </h3> + <div class="space-y-4"> + <div> + <label for="log-service" class="block text-sm font-medium text-white/70 mb-2">Log Upload Service</label> + <CustomSelect + options={logServiceOptions} + bind:value={settingsState.settings.log_upload_service} + class="w-full" + /> + </div> + + {#if settingsState.settings.log_upload_service === 'pastebin.com'} + <div> + <label for="pastebin-key" class="block text-sm font-medium text-white/70 mb-2">Pastebin Dev API Key</label> + <input + id="pastebin-key" + type="password" + bind:value={settingsState.settings.pastebin_api_key} + placeholder="Enter your API Key" + class="dark:bg-zinc-900 bg-white dark:text-white text-black w-full px-4 py-3 rounded-xl border dark:border-zinc-700 border-gray-300 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 outline-none transition-colors placeholder:text-zinc-500" + /> + <p class="text-xs text-white/30 mt-2"> + Get your API key from <a href="https://pastebin.com/doc_api#1" target="_blank" class="text-indigo-400 hover:underline">Pastebin API Documentation</a>. + </p> + </div> + {/if} + </div> + </div> + + <!-- AI Assistant --> + <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm"> + <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2"> + <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> + <rect x="3" y="11" width="18" height="10" rx="2"/> + <circle cx="12" cy="5" r="2"/> + <path d="M12 7v4"/> + <circle cx="8" cy="16" r="1" fill="currentColor"/> + <circle cx="16" cy="16" r="1" fill="currentColor"/> + </svg> + AI Assistant + </h3> + <div class="space-y-6"> + <!-- Enable/Disable --> + <div class="flex items-center justify-between"> + <div> + <h4 class="text-sm font-medium dark:text-white/90 text-black/80" id="assistant-enabled-label">Enable Assistant</h4> + <p class="text-xs dark:text-white/40 text-black/50 mt-1">Toggle the AI assistant feature on or off.</p> + </div> + <button + aria-labelledby="assistant-enabled-label" + onclick={() => { settingsState.settings.assistant.enabled = !settingsState.settings.assistant.enabled; settingsState.saveSettings(); }} + class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.assistant.enabled ? 'bg-indigo-500' : 'dark:bg-white/10 bg-black/10'}" + > + <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.assistant.enabled ? 'translate-x-5' : 'translate-x-0'}"></div> + </button> + </div> + + {#if settingsState.settings.assistant.enabled} + <!-- LLM Provider Section --> + <div class="pt-4 border-t dark:border-white/5 border-black/5"> + <h4 class="text-xs font-bold uppercase tracking-widest text-white/30 mb-4">Language Model</h4> + + <div class="space-y-4"> + <div> + <label for="llm-provider" class="block text-sm font-medium text-white/70 mb-2">Provider</label> + <CustomSelect + options={llmProviderOptions} + bind:value={settingsState.settings.assistant.llm_provider} + onchange={() => settingsState.saveSettings()} + class="w-full" + /> + </div> + + {#if settingsState.settings.assistant.llm_provider === 'ollama'} + <!-- Ollama Settings --> + <div class="pl-4 border-l-2 border-indigo-500/30 space-y-4"> + <div> + <label for="ollama-endpoint" class="block text-sm font-medium text-white/70 mb-2">API Endpoint</label> + <div class="flex gap-2"> + <input + id="ollama-endpoint" + type="text" + bind:value={settingsState.settings.assistant.ollama_endpoint} + placeholder="http://localhost:11434" + class="bg-black/40 text-white flex-1 px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none font-mono text-xs transition-colors" + /> + <button + onclick={() => settingsState.loadOllamaModels()} + disabled={settingsState.isLoadingOllamaModels} + class="px-4 py-2 bg-white/10 hover:bg-white/20 disabled:opacity-50 text-white rounded-xl border border-white/5 transition-colors whitespace-nowrap text-sm font-medium flex items-center gap-2" + title="Refresh models" + > + <RefreshCw size={14} class={settingsState.isLoadingOllamaModels ? "animate-spin" : ""} /> + <span class="hidden sm:inline">Refresh</span> + </button> + </div> + <p class="text-xs text-white/30 mt-2"> + Default: http://localhost:11434. Make sure Ollama is running. + </p> + </div> + + <div> + <div class="flex items-center justify-between mb-2"> + <label for="ollama-model" class="block text-sm font-medium text-white/70">Model</label> + {#if settingsState.ollamaModels.length > 0} + <span class="text-[10px] text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded-full"> + {settingsState.ollamaModels.length} installed + </span> + {/if} + </div> + + {#if settingsState.isLoadingOllamaModels} + <div class="bg-black/40 text-white/50 w-full px-4 py-3 rounded-xl border border-white/10 text-sm flex items-center gap-2"> + <RefreshCw size={14} class="animate-spin" /> + Loading models... + </div> + {:else if settingsState.ollamaModelsError} + <div class="bg-red-500/10 text-red-400 w-full px-4 py-3 rounded-xl border border-red-500/20 text-sm"> + {settingsState.ollamaModelsError} + </div> + <CustomSelect + options={settingsState.currentModelOptions} + bind:value={settingsState.settings.assistant.ollama_model} + onchange={() => settingsState.saveSettings()} + class="w-full mt-2" + allowCustom={true} + /> + {:else if settingsState.ollamaModels.length === 0} + <div class="bg-amber-500/10 text-amber-400 w-full px-4 py-3 rounded-xl border border-amber-500/20 text-sm"> + No models found. Click Refresh to load installed models. + </div> + <CustomSelect + options={settingsState.currentModelOptions} + bind:value={settingsState.settings.assistant.ollama_model} + onchange={() => settingsState.saveSettings()} + class="w-full mt-2" + allowCustom={true} + /> + {:else} + <CustomSelect + options={settingsState.currentModelOptions} + bind:value={settingsState.settings.assistant.ollama_model} + onchange={() => settingsState.saveSettings()} + class="w-full" + allowCustom={true} + /> + {/if} + + <p class="text-xs text-white/30 mt-2"> + Run <code class="bg-black/30 px-1 rounded">ollama pull {'<model>'}</code> to download new models. Or type a custom model name above. + </p> + </div> + </div> + {:else if settingsState.settings.assistant.llm_provider === 'openai'} + <!-- OpenAI Settings --> + <div class="pl-4 border-l-2 border-emerald-500/30 space-y-4"> + <div> + <label for="openai-key" class="block text-sm font-medium text-white/70 mb-2">API Key</label> + <div class="flex gap-2"> + <input + id="openai-key" + type="password" + bind:value={settingsState.settings.assistant.openai_api_key} + placeholder="sk-..." + class="bg-black/40 text-white flex-1 px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none font-mono text-xs transition-colors" + /> + <button + onclick={() => settingsState.loadOpenaiModels()} + disabled={settingsState.isLoadingOpenaiModels || !settingsState.settings.assistant.openai_api_key} + class="px-4 py-2 bg-white/10 hover:bg-white/20 disabled:opacity-50 text-white rounded-xl border border-white/5 transition-colors whitespace-nowrap text-sm font-medium flex items-center gap-2" + title="Refresh models" + > + <RefreshCw size={14} class={settingsState.isLoadingOpenaiModels ? "animate-spin" : ""} /> + <span class="hidden sm:inline">Load</span> + </button> + </div> + <p class="text-xs text-white/30 mt-2"> + Get your API key from <a href="https://platform.openai.com/api-keys" target="_blank" class="text-indigo-400 hover:underline">OpenAI Dashboard</a>. + </p> + </div> + + <div> + <label for="openai-endpoint" class="block text-sm font-medium text-white/70 mb-2">API Endpoint</label> + <input + id="openai-endpoint" + type="text" + bind:value={settingsState.settings.assistant.openai_endpoint} + placeholder="https://api.openai.com/v1" + class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none font-mono text-xs transition-colors" + /> + <p class="text-xs text-white/30 mt-2"> + Use custom endpoint for Azure OpenAI or other compatible APIs. + </p> + </div> + + <div> + <div class="flex items-center justify-between mb-2"> + <label for="openai-model" class="block text-sm font-medium text-white/70">Model</label> + {#if settingsState.openaiModels.length > 0} + <span class="text-[10px] text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded-full"> + {settingsState.openaiModels.length} available + </span> + {/if} + </div> + + {#if settingsState.isLoadingOpenaiModels} + <div class="bg-black/40 text-white/50 w-full px-4 py-3 rounded-xl border border-white/10 text-sm flex items-center gap-2"> + <RefreshCw size={14} class="animate-spin" /> + Loading models... + </div> + {:else if settingsState.openaiModelsError} + <div class="bg-red-500/10 text-red-400 w-full px-4 py-3 rounded-xl border border-red-500/20 text-sm mb-2"> + {settingsState.openaiModelsError} + </div> + <CustomSelect + options={settingsState.currentModelOptions} + bind:value={settingsState.settings.assistant.openai_model} + onchange={() => settingsState.saveSettings()} + class="w-full" + allowCustom={true} + /> + {:else} + <CustomSelect + options={settingsState.currentModelOptions} + bind:value={settingsState.settings.assistant.openai_model} + onchange={() => settingsState.saveSettings()} + class="w-full" + allowCustom={true} + /> + {/if} + </div> + </div> + {/if} + </div> + </div> + + <!-- Response Settings --> + <div class="pt-4 border-t dark:border-white/5 border-black/5"> + <h4 class="text-xs font-bold uppercase tracking-widest text-white/30 mb-4">Response Settings</h4> + + <div class="space-y-4"> + <div> + <label for="response-lang" class="block text-sm font-medium text-white/70 mb-2">Response Language</label> + <CustomSelect + options={languageOptions} + bind:value={settingsState.settings.assistant.response_language} + onchange={() => settingsState.saveSettings()} + class="w-full" + /> + </div> + + <div> + <div class="flex items-center justify-between mb-2"> + <label for="system-prompt" class="block text-sm font-medium text-white/70">System Prompt</label> + <button + onclick={resetSystemPrompt} + class="text-xs text-indigo-400 hover:text-indigo-300 transition-colors flex items-center gap-1 opacity-80 hover:opacity-100" + title="Reset to default prompt" + > + <RefreshCw size={10} /> + Reset + </button> + </div> + + <div class="mb-3"> + <CustomSelect + options={personas.map(p => ({ value: p.value, label: p.label }))} + bind:value={selectedPersona} + placeholder="Load a preset persona..." + onchange={applyPersona} + class="w-full" + /> + </div> + + <textarea + id="system-prompt" + bind:value={settingsState.settings.assistant.system_prompt} + oninput={() => selectedPersona = ""} + rows="4" + placeholder="You are a helpful Minecraft expert assistant..." + class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none text-sm transition-colors resize-none" + ></textarea> + <p class="text-xs text-white/30 mt-2"> + Customize how the assistant behaves and responds. + </p> + </div> + </div> + </div> + + <!-- TTS Settings --> + <div class="pt-4 border-t dark:border-white/5 border-black/5"> + <h4 class="text-xs font-bold uppercase tracking-widest text-white/30 mb-4">Text-to-Speech (Coming Soon)</h4> + + <div class="space-y-4 opacity-50 pointer-events-none"> + <div class="flex items-center justify-between"> + <div> + <h4 class="text-sm font-medium dark:text-white/90 text-black/80">Enable TTS</h4> + <p class="text-xs dark:text-white/40 text-black/50 mt-1">Read assistant responses aloud.</p> + </div> + <button + disabled + class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none dark:bg-white/10 bg-black/10" + > + <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out translate-x-0"></div> + </button> + </div> + + <div> + <label class="block text-sm font-medium text-white/70 mb-2">TTS Provider</label> + <CustomSelect + options={ttsProviderOptions} + value="disabled" + class="w-full" + /> + </div> + </div> + </div> + {/if} + </div> + </div> + + <div class="pt-4 flex justify-end"> + <button + onclick={() => settingsState.saveSettings()} + class="bg-gradient-to-r from-emerald-600 to-green-600 hover:from-emerald-500 hover:to-green-500 text-white font-bold py-3 px-8 rounded-xl shadow-lg shadow-emerald-500/20 transition-all hover:scale-105 active:scale-95" + > + Save Settings + </button> + </div> + </div> +</div> + +{#if settingsState.showConfigEditor} + <ConfigEditorModal /> +{/if} + +<!-- Java Download Modal --> +{#if settingsState.showJavaDownloadModal} + <div class="fixed inset-0 z-[100] flex items-center justify-center backdrop-blur-sm bg-black/70"> + <div class="bg-zinc-900 rounded-2xl border border-white/10 shadow-2xl w-[900px] max-w-[95vw] h-[600px] max-h-[90vh] flex flex-col overflow-hidden"> + <!-- Header --> + <div class="flex items-center justify-between p-5 border-b border-white/10"> + <h3 class="text-xl font-bold text-white">Download Java</h3> + <button + aria-label="Close dialog" + onclick={() => settingsState.closeJavaDownloadModal()} + disabled={settingsState.isDownloadingJava} + class="text-white/40 hover:text-white/80 disabled:opacity-50 transition-colors p-1" + > + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/> + </svg> + </button> + </div> + + <!-- Main Content Area --> + <div class="flex flex-1 overflow-hidden"> + <!-- Left Sidebar: Sources --> + <div class="w-40 border-r border-white/10 p-3 flex flex-col gap-1"> + <span class="text-[10px] font-bold uppercase tracking-widest text-white/30 px-2 mb-2">Sources</span> + + <button + disabled + class="flex items-center gap-2 px-3 py-2.5 rounded-lg text-left text-sm opacity-40 cursor-not-allowed text-white/50" + > + <div class="w-5 h-5 rounded bg-white/10 flex items-center justify-center text-[10px]">M</div> + Mojang + </button> + + <button + class="flex items-center gap-2 px-3 py-2.5 rounded-lg text-left text-sm bg-indigo-500/20 border border-indigo-500/40 text-white" + > + <div class="w-5 h-5 rounded bg-indigo-500 flex items-center justify-center text-[10px] font-bold">A</div> + Adoptium + </button> + + <button + disabled + class="flex items-center gap-2 px-3 py-2.5 rounded-lg text-left text-sm opacity-40 cursor-not-allowed text-white/50" + > + <div class="w-5 h-5 rounded bg-white/10 flex items-center justify-center text-[10px]">Z</div> + Azul Zulu + </button> + </div> + + <!-- Center: Version Selection --> + <div class="flex-1 flex flex-col overflow-hidden"> + <!-- Toolbar --> + <div class="flex items-center gap-3 p-4 border-b border-white/5"> + <!-- Search --> + <div class="relative flex-1 max-w-xs"> + <input + type="text" + bind:value={settingsState.searchQuery} + placeholder="Search versions..." + class="w-full bg-black/30 text-white text-sm px-4 py-2 pl-9 rounded-lg border border-white/10 focus:border-indigo-500/50 outline-none" + /> + <svg class="absolute left-3 top-2.5 w-4 h-4 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/> + </svg> + </div> + + <!-- Recommended Filter --> + <label class="flex items-center gap-2 text-sm text-white/60 cursor-pointer select-none"> + <input + type="checkbox" + bind:checked={settingsState.showOnlyRecommended} + class="w-4 h-4 rounded border-white/20 bg-black/30 text-indigo-500 focus:ring-indigo-500/30" + /> + LTS Only + </label> + + <!-- Image Type Toggle --> + <div class="flex items-center bg-black/30 rounded-lg p-0.5 border border-white/10"> + <button + onclick={() => settingsState.selectedImageType = "jre"} + class="px-3 py-1.5 rounded-md text-xs font-medium transition-all {settingsState.selectedImageType === 'jre' ? 'bg-indigo-500 text-white shadow' : 'text-white/50 hover:text-white/80'}" + > + JRE + </button> + <button + onclick={() => settingsState.selectedImageType = "jdk"} + class="px-3 py-1.5 rounded-md text-xs font-medium transition-all {settingsState.selectedImageType === 'jdk' ? 'bg-indigo-500 text-white shadow' : 'text-white/50 hover:text-white/80'}" + > + JDK + </button> + </div> + </div> + + <!-- Loading State --> + {#if settingsState.isLoadingCatalog} + <div class="flex-1 flex items-center justify-center text-white/50"> + <div class="flex flex-col items-center gap-3"> + <div class="w-8 h-8 border-2 border-indigo-500/30 border-t-indigo-500 rounded-full animate-spin"></div> + <span class="text-sm">Loading Java versions...</span> + </div> + </div> + {:else if settingsState.catalogError} + <div class="flex-1 flex items-center justify-center text-red-400"> + <div class="flex flex-col items-center gap-3 text-center px-8"> + <svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/> + </svg> + <span class="text-sm">{settingsState.catalogError}</span> + <button + onclick={() => settingsState.refreshCatalog()} + class="mt-2 px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-sm text-white transition-colors" + > + Retry + </button> + </div> + </div> + {:else} + <!-- Version List --> + <div class="flex-1 overflow-auto p-4"> + <div class="space-y-2"> + {#each settingsState.availableMajorVersions as version} + {@const isLts = settingsState.javaCatalog?.lts_versions.includes(version)} + {@const isSelected = settingsState.selectedMajorVersion === version} + {@const releaseInfo = settingsState.javaCatalog?.releases.find(r => r.major_version === version && r.image_type === settingsState.selectedImageType)} + {@const isAvailable = releaseInfo?.is_available ?? false} + {@const installStatus = releaseInfo ? settingsState.getInstallStatus(releaseInfo) : 'download'} + + <button + onclick={() => settingsState.selectMajorVersion(version)} + disabled={!isAvailable} + class="w-full flex items-center gap-4 p-3 rounded-xl border transition-all text-left + {isSelected + ? 'bg-indigo-500/20 border-indigo-500/50 ring-2 ring-indigo-500/30' + : isAvailable + ? 'bg-black/20 border-white/10 hover:bg-white/5 hover:border-white/20' + : 'bg-black/10 border-white/5 opacity-40 cursor-not-allowed'}" + > + <!-- Version Number --> + <div class="w-14 text-center"> + <span class="text-xl font-bold {isSelected ? 'text-white' : 'text-white/80'}">{version}</span> + </div> + + <!-- Version Details --> + <div class="flex-1 min-w-0"> + <div class="flex items-center gap-2"> + <span class="text-sm text-white/70 font-mono truncate">{releaseInfo?.version ?? '--'}</span> + {#if isLts} + <span class="px-1.5 py-0.5 bg-emerald-500/20 text-emerald-400 text-[9px] font-bold rounded uppercase shrink-0">LTS</span> + {/if} + </div> + {#if releaseInfo} + <div class="text-[10px] text-white/40 truncate mt-0.5"> + {releaseInfo.release_name} • {settingsState.formatBytes(releaseInfo.file_size)} + </div> + {/if} + </div> + + <!-- Install Status Badge --> + <div class="shrink-0"> + {#if installStatus === 'installed'} + <span class="px-2 py-1 bg-emerald-500/20 text-emerald-400 text-[10px] font-bold rounded uppercase">Installed</span> + {:else if isAvailable} + <span class="px-2 py-1 bg-white/10 text-white/50 text-[10px] font-medium rounded">Download</span> + {:else} + <span class="px-2 py-1 bg-red-500/10 text-red-400/60 text-[10px] font-medium rounded">N/A</span> + {/if} + </div> + </button> + {/each} + </div> + </div> + {/if} + </div> + + <!-- Right Sidebar: Details --> + <div class="w-64 border-l border-white/10 flex flex-col"> + <div class="p-4 border-b border-white/5"> + <span class="text-[10px] font-bold uppercase tracking-widest text-white/30">Details</span> + </div> + + {#if settingsState.selectedRelease} + <div class="flex-1 p-4 space-y-4 overflow-auto"> + <div> + <div class="text-[10px] text-white/40 uppercase tracking-wider mb-1">Version</div> + <div class="text-sm text-white font-mono">{settingsState.selectedRelease.version}</div> + </div> + + <div> + <div class="text-[10px] text-white/40 uppercase tracking-wider mb-1">Release Name</div> + <div class="text-sm text-white">{settingsState.selectedRelease.release_name}</div> + </div> + + <div> + <div class="text-[10px] text-white/40 uppercase tracking-wider mb-1">Release Date</div> + <div class="text-sm text-white">{settingsState.formatDate(settingsState.selectedRelease.release_date)}</div> + </div> + + <div> + <div class="text-[10px] text-white/40 uppercase tracking-wider mb-1">Size</div> + <div class="text-sm text-white">{settingsState.formatBytes(settingsState.selectedRelease.file_size)}</div> + </div> + + <div> + <div class="text-[10px] text-white/40 uppercase tracking-wider mb-1">Type</div> + <div class="flex items-center gap-2"> + <span class="text-sm text-white uppercase">{settingsState.selectedRelease.image_type}</span> + {#if settingsState.selectedRelease.is_lts} + <span class="px-1.5 py-0.5 bg-emerald-500/20 text-emerald-400 text-[9px] font-bold rounded">LTS</span> + {/if} + </div> + </div> + + <div> + <div class="text-[10px] text-white/40 uppercase tracking-wider mb-1">Architecture</div> + <div class="text-sm text-white">{settingsState.selectedRelease.architecture}</div> + </div> + + {#if !settingsState.selectedRelease.is_available} + <div class="mt-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg"> + <div class="text-xs text-red-400">Not available for your platform</div> + </div> + {/if} + </div> + {:else} + <div class="flex-1 flex items-center justify-center text-white/30 text-sm p-4 text-center"> + Select a Java version to view details + </div> + {/if} + </div> + </div> + + <!-- Download Progress (MC Style) --> + {#if settingsState.isDownloadingJava && settingsState.downloadProgress} + <div class="border-t border-white/10 p-4 bg-zinc-900/90"> + <div class="flex items-center justify-between mb-2"> + <h3 class="text-white font-bold text-sm">Downloading Java</h3> + <span class="text-xs text-zinc-400">{settingsState.downloadProgress.status}</span> + </div> + + <!-- Progress Bar --> + <div class="mb-2"> + <div class="flex justify-between text-[10px] text-zinc-400 mb-1"> + <span>{settingsState.downloadProgress.file_name}</span> + <span>{Math.round(settingsState.downloadProgress.percentage)}%</span> + </div> + <div class="w-full bg-zinc-800 rounded-full h-2.5 overflow-hidden"> + <div + class="bg-gradient-to-r from-blue-500 to-cyan-400 h-2.5 rounded-full transition-all duration-200" + style="width: {settingsState.downloadProgress.percentage}%" + ></div> + </div> + </div> + + <!-- Speed & Stats --> + <div class="flex justify-between text-[10px] text-zinc-500 font-mono"> + <span> + {settingsState.formatBytes(settingsState.downloadProgress.speed_bytes_per_sec)}/s · + ETA: {settingsState.formatTime(settingsState.downloadProgress.eta_seconds)} + </span> + <span> + {settingsState.formatBytes(settingsState.downloadProgress.downloaded_bytes)} / + {settingsState.formatBytes(settingsState.downloadProgress.total_bytes)} + </span> + </div> + </div> + {/if} + + <!-- Pending Downloads Alert --> + {#if settingsState.pendingDownloads.length > 0 && !settingsState.isDownloadingJava} + <div class="border-t border-amber-500/30 p-4 bg-amber-500/10"> + <div class="flex items-center justify-between"> + <div class="flex items-center gap-3"> + <svg class="w-5 h-5 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/> + </svg> + <span class="text-sm text-amber-200"> + {settingsState.pendingDownloads.length} pending download(s) can be resumed + </span> + </div> + <button + onclick={() => settingsState.resumeDownloads()} + class="px-4 py-2 bg-amber-500/20 hover:bg-amber-500/30 text-amber-200 rounded-lg text-sm font-medium transition-colors" + > + Resume All + </button> + </div> + </div> + {/if} + + <!-- Footer Actions --> + <div class="flex items-center justify-between p-4 border-t border-white/10 bg-black/20"> + <button + onclick={() => settingsState.refreshCatalog()} + disabled={settingsState.isLoadingCatalog || settingsState.isDownloadingJava} + class="flex items-center gap-2 px-4 py-2 bg-white/5 hover:bg-white/10 disabled:opacity-50 text-white/70 rounded-lg text-sm transition-colors" + > + <svg class="w-4 h-4 {settingsState.isLoadingCatalog ? 'animate-spin' : ''}" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/> + </svg> + Refresh + </button> + + <div class="flex gap-3"> + {#if settingsState.isDownloadingJava} + <button + onclick={() => settingsState.cancelDownload()} + class="px-5 py-2.5 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-lg text-sm font-medium transition-colors" + > + Cancel Download + </button> + {:else} + {@const isInstalled = settingsState.selectedRelease ? settingsState.getInstallStatus(settingsState.selectedRelease) === 'installed' : false} + <button + onclick={() => settingsState.closeJavaDownloadModal()} + class="px-5 py-2.5 bg-white/10 hover:bg-white/20 text-white rounded-lg text-sm font-medium transition-colors" + > + Close + </button> + <button + onclick={() => settingsState.downloadJava()} + disabled={!settingsState.selectedRelease?.is_available || settingsState.isLoadingCatalog || isInstalled} + class="px-5 py-2.5 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg text-sm font-medium transition-colors" + > + {isInstalled ? 'Already Installed' : 'Download & Install'} + </button> + {/if} + </div> + </div> + </div> + </div> +{/if} |