diff options
| author | 2026-02-25 01:32:51 +0800 | |
|---|---|---|
| committer | 2026-02-25 01:32:51 +0800 | |
| commit | 66668d85d603c5841d755a6023aa1925559fc6d4 (patch) | |
| tree | 485464148c76b0021efb55b7d2afd1c3004ceee0 /packages/ui-new/src/pages | |
| parent | a6773bd092db654360c599ca6b0108ea0e456e8c (diff) | |
| download | DropOut-66668d85d603c5841d755a6023aa1925559fc6d4.tar.gz DropOut-66668d85d603c5841d755a6023aa1925559fc6d4.zip | |
chore(workspace): replace legacy codes
Diffstat (limited to 'packages/ui-new/src/pages')
| -rw-r--r-- | packages/ui-new/src/pages/assistant-view.tsx.bk | 485 | ||||
| -rw-r--r-- | packages/ui-new/src/pages/home-view.tsx | 174 | ||||
| -rw-r--r-- | packages/ui-new/src/pages/index.tsx | 76 | ||||
| -rw-r--r-- | packages/ui-new/src/pages/instances-view.tsx | 315 | ||||
| -rw-r--r-- | packages/ui-new/src/pages/settings-view.tsx.bk | 1158 | ||||
| -rw-r--r-- | packages/ui-new/src/pages/settings.tsx | 310 | ||||
| -rw-r--r-- | packages/ui-new/src/pages/versions-view.tsx.bk | 662 |
7 files changed, 0 insertions, 3180 deletions
diff --git a/packages/ui-new/src/pages/assistant-view.tsx.bk b/packages/ui-new/src/pages/assistant-view.tsx.bk deleted file mode 100644 index 56f827b..0000000 --- a/packages/ui-new/src/pages/assistant-view.tsx.bk +++ /dev/null @@ -1,485 +0,0 @@ -import { - AlertTriangle, - Bot, - Brain, - ChevronDown, - Loader2, - RefreshCw, - Send, - Settings, - Trash2, -} from "lucide-react"; -import { marked } from "marked"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Separator } from "@/components/ui/separator"; -import { Textarea } from "@/components/ui/textarea"; -import { toNumber } from "@/lib/tsrs-utils"; -import { type Message, useAssistantStore } from "../stores/assistant-store"; -import { useSettingsStore } from "../stores/settings-store"; -import { useUiStore } from "../stores/ui-store"; - -interface ParsedMessage { - thinking: string | null; - content: string; - isThinking: boolean; -} - -function parseMessageContent(content: string): ParsedMessage { - if (!content) return { thinking: null, content: "", isThinking: false }; - - // Support both <thinking> and <think> (DeepSeek uses <think>) - let startTag = "<thinking>"; - let endTag = "</thinking>"; - let startIndex = content.indexOf(startTag); - - if (startIndex === -1) { - startTag = "<think>"; - endTag = "</think>"; - startIndex = content.indexOf(startTag); - } - - // Also check for encoded tags if they weren't decoded properly - if (startIndex === -1) { - startTag = "\u003cthink\u003e"; - endTag = "\u003c/think\u003e"; - startIndex = content.indexOf(startTag); - } - - if (startIndex !== -1) { - const endIndex = content.indexOf(endTag, startIndex); - - if (endIndex !== -1) { - // Completed thinking block - const before = content.substring(0, startIndex); - const thinking = content - .substring(startIndex + startTag.length, endIndex) - .trim(); - const after = content.substring(endIndex + endTag.length); - - return { - thinking, - content: (before + after).trim(), - isThinking: false, - }; - } else { - // Incomplete thinking block (still streaming) - const before = content.substring(0, startIndex); - const thinking = content.substring(startIndex + startTag.length).trim(); - - return { - thinking, - content: before.trim(), - isThinking: true, - }; - } - } - - return { thinking: null, content, isThinking: false }; -} - -function renderMarkdown(content: string): string { - if (!content) return ""; - try { - return marked(content, { breaks: true, gfm: true }) as string; - } catch { - return content; - } -} - -export function AssistantView() { - const { - messages, - isProcessing, - isProviderHealthy, - streamingContent, - init, - checkHealth, - sendMessage, - clearHistory, - } = useAssistantStore(); - const { settings } = useSettingsStore(); - const { setView } = useUiStore(); - - const [input, setInput] = useState(""); - const messagesEndRef = useRef<HTMLDivElement>(null); - const messagesContainerRef = useRef<HTMLDivElement>(null); - - const provider = settings.assistant.llmProvider; - const endpoint = - provider === "ollama" - ? settings.assistant.ollamaEndpoint - : settings.assistant.openaiEndpoint; - const model = - provider === "ollama" - ? settings.assistant.ollamaModel - : settings.assistant.openaiModel; - - const getProviderName = (): string => { - if (provider === "ollama") { - return `Ollama (${model})`; - } else if (provider === "openai") { - return `OpenAI (${model})`; - } - return provider; - }; - - const getProviderHelpText = (): string => { - if (provider === "ollama") { - return `Please ensure Ollama is installed and running at ${endpoint}.`; - } else if (provider === "openai") { - return "Please check your OpenAI API key in Settings > AI Assistant."; - } - return ""; - }; - - const scrollToBottom = useCallback(() => { - if (messagesContainerRef.current) { - setTimeout(() => { - if (messagesContainerRef.current) { - messagesContainerRef.current.scrollTop = - messagesContainerRef.current.scrollHeight; - } - }, 0); - } - }, []); - - useEffect(() => { - init(); - }, [init]); - - useEffect(() => { - if (messages.length > 0 || isProcessing) { - scrollToBottom(); - } - }, [messages.length, isProcessing, scrollToBottom]); - - const handleSubmit = async () => { - if (!input.trim() || isProcessing) return; - const text = input; - setInput(""); - await sendMessage(text, settings.assistant.enabled, provider, endpoint); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleSubmit(); - } - }; - - const renderMessage = (message: Message, index: number) => { - const isUser = message.role === "user"; - const parsed = parseMessageContent(message.content); - - return ( - <div - key={index} - className={`flex ${isUser ? "justify-end" : "justify-start"} mb-4`} - > - <div - className={`max-w-[80%] rounded-2xl px-4 py-3 ${ - isUser - ? "bg-indigo-500 text-white rounded-br-none" - : "bg-zinc-800 text-zinc-100 rounded-bl-none" - }`} - > - {!isUser && parsed.thinking && ( - <div className="mb-3 max-w-full overflow-hidden"> - <details className="group" open={parsed.isThinking}> - <summary className="list-none cursor-pointer flex items-center gap-2 text-zinc-500 hover:text-zinc-300 transition-colors text-xs font-medium select-none bg-black/20 p-2 rounded-lg border border-white/5 w-fit mb-2 outline-none"> - <Brain className="h-3 w-3" /> - <span>Thinking Process</span> - <ChevronDown className="h-3 w-3 transition-transform duration-200 group-open:rotate-180" /> - </summary> - <div className="pl-3 border-l-2 border-zinc-700 text-zinc-500 text-xs italic leading-relaxed whitespace-pre-wrap font-mono max-h-96 overflow-y-auto custom-scrollbar bg-black/10 p-2 rounded-r-md"> - {parsed.thinking} - {parsed.isThinking && ( - <span className="inline-block w-1.5 h-3 bg-zinc-500 ml-1 animate-pulse align-middle" /> - )} - </div> - </details> - </div> - )} - <div - className="prose prose-invert max-w-none" - dangerouslySetInnerHTML={{ - __html: renderMarkdown(parsed.content), - }} - /> - {!isUser && message.stats && ( - <div className="mt-2 pt-2 border-t border-zinc-700/50"> - <div className="text-xs text-zinc-400"> - {message.stats.evalCount} tokens ·{" "} - {Math.round(toNumber(message.stats.totalDuration) / 1000000)} - ms - </div> - </div> - )} - </div> - </div> - ); - }; - - return ( - <div className="h-full w-full flex flex-col gap-4 p-4 lg:p-8"> - <div className="flex items-center justify-between mb-2"> - <div className="flex items-center gap-3"> - <div className="p-2 bg-indigo-500/20 rounded-lg text-indigo-400"> - <Bot size={24} /> - </div> - <div> - <h2 className="text-2xl font-bold">Game Assistant</h2> - <p className="text-zinc-400 text-sm"> - Powered by {getProviderName()} - </p> - </div> - </div> - - <div className="flex items-center gap-2"> - {!settings.assistant.enabled ? ( - <Badge - variant="outline" - className="bg-zinc-500/10 text-zinc-400 border-zinc-500/20" - > - <AlertTriangle className="h-3 w-3 mr-1" /> - Disabled - </Badge> - ) : !isProviderHealthy ? ( - <Badge - variant="outline" - className="bg-red-500/10 text-red-400 border-red-500/20" - > - <AlertTriangle className="h-3 w-3 mr-1" /> - Offline - </Badge> - ) : ( - <Badge - variant="outline" - className="bg-emerald-500/10 text-emerald-400 border-emerald-500/20" - > - <div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse mr-1" /> - Online - </Badge> - )} - - <Button - variant="ghost" - size="icon" - onClick={checkHealth} - title="Check Connection" - disabled={isProcessing} - > - <RefreshCw - className={`h-4 w-4 ${isProcessing ? "animate-spin" : ""}`} - /> - </Button> - - <Button - variant="ghost" - size="icon" - onClick={clearHistory} - title="Clear History" - disabled={isProcessing} - > - <Trash2 className="h-4 w-4" /> - </Button> - - <Button - variant="ghost" - size="icon" - onClick={() => setView("settings")} - title="Settings" - > - <Settings className="h-4 w-4" /> - </Button> - </div> - </div> - - {/* Chat Area */} - <div className="flex-1 bg-black/20 border border-white/5 rounded-xl overflow-hidden flex flex-col relative"> - {/* Warning when assistant is disabled */} - {!settings.assistant.enabled && ( - <div className="absolute top-4 left-1/2 transform -translate-x-1/2 z-10"> - <Card className="bg-yellow-500/10 border-yellow-500/20"> - <CardContent className="p-3 flex items-center gap-2"> - <AlertTriangle className="h-4 w-4 text-yellow-500" /> - <span className="text-yellow-500 text-sm font-medium"> - Assistant is disabled. Enable it in Settings > AI - Assistant. - </span> - </CardContent> - </Card> - </div> - )} - - {/* Provider offline warning */} - {settings.assistant.enabled && !isProviderHealthy && ( - <div className="absolute top-4 left-1/2 transform -translate-x-1/2 z-10"> - <Card className="bg-red-500/10 border-red-500/20"> - <CardContent className="p-3 flex items-center gap-2"> - <AlertTriangle className="h-4 w-4 text-red-500" /> - <div className="flex flex-col"> - <span className="text-red-500 text-sm font-medium"> - Assistant is offline - </span> - <span className="text-red-400 text-xs"> - {getProviderHelpText()} - </span> - </div> - </CardContent> - </Card> - </div> - )} - - {/* Messages Container */} - <ScrollArea className="flex-1 p-4 lg:p-6" ref={messagesContainerRef}> - {messages.length === 0 ? ( - <div className="flex flex-col items-center justify-center h-full text-zinc-400 gap-4 mt-8"> - <div className="p-4 bg-zinc-800/50 rounded-full"> - <Bot className="h-12 w-12" /> - </div> - <h3 className="text-xl font-medium">How can I help you today?</h3> - <p className="text-center max-w-md text-sm"> - I can analyze your game logs, diagnose crashes, or explain mod - features. - {!settings.assistant.enabled && ( - <span className="block mt-2 text-yellow-500"> - Assistant is disabled. Enable it in{" "} - <button - type="button" - onClick={() => setView("settings")} - className="text-indigo-400 hover:underline" - > - Settings > AI Assistant - </button> - . - </span> - )} - </p> - <div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-2 max-w-lg"> - <Button - variant="outline" - className="text-left h-auto py-3" - onClick={() => - setInput("How do I fix Minecraft crashing on launch?") - } - disabled={isProcessing} - > - <div className="text-sm"> - How do I fix Minecraft crashing on launch? - </div> - </Button> - <Button - variant="outline" - className="text-left h-auto py-3" - onClick={() => - setInput("What's the best way to improve FPS?") - } - disabled={isProcessing} - > - <div className="text-sm"> - What's the best way to improve FPS? - </div> - </Button> - <Button - variant="outline" - className="text-left h-auto py-3" - onClick={() => - setInput( - "Can you help me install Fabric for Minecraft 1.20.4?", - ) - } - disabled={isProcessing} - > - <div className="text-sm"> - Can you help me install Fabric for 1.20.4? - </div> - </Button> - <Button - variant="outline" - className="text-left h-auto py-3" - onClick={() => - setInput("What mods do you recommend for performance?") - } - disabled={isProcessing} - > - <div className="text-sm"> - What mods do you recommend for performance? - </div> - </Button> - </div> - </div> - ) : ( - <> - {messages.map((message, index) => renderMessage(message, index))} - {isProcessing && streamingContent && ( - <div className="flex justify-start mb-4"> - <div className="max-w-[80%] bg-zinc-800 text-zinc-100 rounded-2xl rounded-bl-none px-4 py-3"> - <div - className="prose prose-invert max-w-none" - dangerouslySetInnerHTML={{ - __html: renderMarkdown(streamingContent), - }} - /> - <div className="flex items-center gap-1 mt-2 text-xs text-zinc-400"> - <Loader2 className="h-3 w-3 animate-spin" /> - <span>Assistant is typing...</span> - </div> - </div> - </div> - )} - </> - )} - <div ref={messagesEndRef} /> - </ScrollArea> - - <Separator /> - - {/* Input Area */} - <div className="p-3 lg:p-4"> - <div className="flex gap-2"> - <Textarea - placeholder={ - settings.assistant.enabled - ? "Ask about your game..." - : "Assistant is disabled. Enable it in Settings to use." - } - value={input} - onChange={(e) => setInput(e.target.value)} - onKeyDown={handleKeyDown} - className="min-h-11 max-h-50 resize-none border-zinc-700 bg-zinc-900/50 focus:bg-zinc-900/80" - disabled={!settings.assistant.enabled || isProcessing} - /> - <Button - onClick={handleSubmit} - disabled={ - !settings.assistant.enabled || !input.trim() || isProcessing - } - className="px-6 bg-indigo-600 hover:bg-indigo-700 text-white" - > - {isProcessing ? ( - <Loader2 className="h-4 w-4 animate-spin" /> - ) : ( - <Send className="h-4 w-4" /> - )} - </Button> - </div> - <div className="mt-2 flex items-center justify-between"> - <div className="text-xs text-zinc-500"> - {settings.assistant.enabled - ? "Press Enter to send, Shift+Enter for new line" - : "Enable the assistant in Settings to use"} - </div> - <div className="text-xs text-zinc-500"> - Model: {model} • Provider: {provider} - </div> - </div> - </div> - </div> - </div> - ); -} diff --git a/packages/ui-new/src/pages/home-view.tsx b/packages/ui-new/src/pages/home-view.tsx deleted file mode 100644 index 4f80cb0..0000000 --- a/packages/ui-new/src/pages/home-view.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { useEffect, useState } from "react"; -import { BottomBar } from "@/components/bottom-bar"; -import type { SaturnEffect } from "@/lib/effects/SaturnEffect"; -import { useGameStore } from "../stores/game-store"; -import { useReleasesStore } from "../stores/releases-store"; - -export function HomeView() { - const gameStore = useGameStore(); - const releasesStore = useReleasesStore(); - const [mouseX, setMouseX] = useState(0); - const [mouseY, setMouseY] = useState(0); - - useEffect(() => { - releasesStore.loadReleases(); - }, [releasesStore.loadReleases]); - - const handleMouseMove = (e: React.MouseEvent) => { - const x = (e.clientX / window.innerWidth) * 2 - 1; - const y = (e.clientY / window.innerHeight) * 2 - 1; - setMouseX(x); - setMouseY(y); - - // Forward mouse move to SaturnEffect (if available) for parallax/rotation interactions - try { - const saturn = ( - window as unknown as { - getSaturnEffect?: () => SaturnEffect; - } - ).getSaturnEffect?.(); - if (saturn?.handleMouseMove) { - saturn.handleMouseMove(e.clientX); - } - } catch { - /* best-effort, ignore errors from effect */ - } - }; - - const handleSaturnMouseDown = (e: React.MouseEvent) => { - try { - const saturn = (window as any).getSaturnEffect?.(); - if (saturn?.handleMouseDown) { - saturn.handleMouseDown(e.clientX); - } - } catch { - /* ignore */ - } - }; - - const handleSaturnMouseUp = () => { - try { - const saturn = (window as any).getSaturnEffect?.(); - if (saturn?.handleMouseUp) { - saturn.handleMouseUp(); - } - } catch { - /* ignore */ - } - }; - - const handleSaturnMouseLeave = () => { - // Treat leaving the area as mouse-up for the effect - try { - const saturn = (window as any).getSaturnEffect?.(); - if (saturn?.handleMouseUp) { - saturn.handleMouseUp(); - } - } catch { - /* ignore */ - } - }; - - const handleSaturnTouchStart = (e: React.TouchEvent) => { - if (e.touches && e.touches.length === 1) { - try { - const clientX = e.touches[0].clientX; - const saturn = (window as any).getSaturnEffect?.(); - if (saturn?.handleTouchStart) { - saturn.handleTouchStart(clientX); - } - } catch { - /* ignore */ - } - } - }; - - const handleSaturnTouchMove = (e: React.TouchEvent) => { - if (e.touches && e.touches.length === 1) { - try { - const clientX = e.touches[0].clientX; - const saturn = (window as any).getSaturnEffect?.(); - if (saturn?.handleTouchMove) { - saturn.handleTouchMove(clientX); - } - } catch { - /* ignore */ - } - } - }; - - const handleSaturnTouchEnd = () => { - try { - const saturn = (window as any).getSaturnEffect?.(); - if (saturn?.handleTouchEnd) { - saturn.handleTouchEnd(); - } - } catch { - /* ignore */ - } - }; - - return ( - <div - className="relative z-10 h-full overflow-y-auto custom-scrollbar scroll-smooth" - style={{ - overflow: releasesStore.isLoading ? "hidden" : "auto", - }} - > - {/* Hero Section (Full Height) - Interactive area */} - <div - role="tab" - className="min-h-full flex flex-col justify-end p-12 pb-32 cursor-grab active:cursor-grabbing select-none" - onMouseDown={handleSaturnMouseDown} - onMouseMove={handleMouseMove} - onMouseUp={handleSaturnMouseUp} - onMouseLeave={handleSaturnMouseLeave} - onTouchStart={handleSaturnTouchStart} - onTouchMove={handleSaturnTouchMove} - onTouchEnd={handleSaturnTouchEnd} - tabIndex={0} - > - {/* 3D Floating Hero Text */} - <div - className="transition-transform duration-200 ease-out origin-bottom-left" - style={{ - transform: `perspective(1000px) rotateX(${mouseY * -1}deg) rotateY(${mouseX * 1}deg)`, - }} - > - <div className="flex items-center gap-3 mb-6"> - <div className="h-px w-12 bg-white/50"></div> - <span className="text-xs font-mono font-bold tracking-[0.2em] text-white/50 uppercase"> - Launcher Active - </span> - </div> - - <h1 className="text-8xl font-black tracking-tighter text-white mb-6 leading-none"> - MINECRAFT - </h1> - - <div className="flex items-center gap-4"> - <div className="bg-white/10 backdrop-blur-md border border-white/10 px-3 py-1 rounded-sm text-xs font-bold uppercase tracking-widest text-white shadow-sm"> - Java Edition - </div> - <div className="h-4 w-px bg-white/20"></div> - <div className="text-sm text-zinc-400"> - Latest Release{" "} - <span className="text-white font-medium"> - {gameStore.latestRelease?.id || "..."} - </span> - </div> - </div> - </div> - - {/* Action Area */} - <div className="mt-8 flex gap-4"> - <div className="text-zinc-500 text-sm font-mono"> - > Ready to launch session. - </div> - </div> - - <BottomBar /> - </div> - </div> - ); -} diff --git a/packages/ui-new/src/pages/index.tsx b/packages/ui-new/src/pages/index.tsx deleted file mode 100644 index 54cfc1e..0000000 --- a/packages/ui-new/src/pages/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useEffect } from "react"; -import { Outlet, useLocation } from "react-router"; -import { ParticleBackground } from "@/components/particle-background"; -import { Sidebar } from "@/components/sidebar"; -import { useAuthStore } from "@/models/auth"; -import { useSettingsStore } from "@/models/settings"; - -export function IndexPage() { - const authStore = useAuthStore(); - const settingsStore = useSettingsStore(); - - const location = useLocation(); - - useEffect(() => { - authStore.init(); - settingsStore.refresh(); - }, [authStore.init, settingsStore.refresh]); - - return ( - <div className="relative h-screen w-full overflow-hidden bg-background font-sans"> - <div className="absolute inset-0 z-0 bg-gray-100 dark:bg-[#09090b] overflow-hidden"> - {settingsStore.config?.customBackgroundPath && ( - <> - <img - src={settingsStore.config?.customBackgroundPath} - alt="Background" - className="absolute inset-0 w-full h-full object-cover transition-transform duration-[20s] ease-linear" - onError={(e) => - console.error("Failed to load main background:", e) - } - /> - {/* Dimming Overlay for readability */} - <div className="absolute inset-0 bg-black/50" /> - </> - )} - - {!settingsStore.config?.customBackgroundPath && ( - <> - {settingsStore.theme === "dark" ? ( - <div className="absolute inset-0 opacity-60 bg-linear-to-br from-emerald-900 via-zinc-900 to-indigo-950"></div> - ) : ( - <div className="absolute inset-0 opacity-100 bg-linear-to-br from-emerald-100 via-gray-100 to-indigo-100"></div> - )} - - {location.pathname === "/" && <ParticleBackground />} - - <div className="absolute inset-0 bg-linear-to-t from-zinc-900 via-transparent to-black/50 dark:from-zinc-900 dark:to-black/50"></div> - </> - )} - - {/* Subtle Grid Overlay */} - <div - className="absolute inset-0 z-0 dark:opacity-10 opacity-30 pointer-events-none" - style={{ - backgroundImage: `linear-gradient(${ - settingsStore.config?.theme === "dark" ? "#ffffff" : "#000000" - } 1px, transparent 1px), linear-gradient(90deg, ${ - settingsStore.config?.theme === "dark" ? "#ffffff" : "#000000" - } 1px, transparent 1px)`, - backgroundSize: "40px 40px", - maskImage: - "radial-gradient(circle at 50% 50%, black 30%, transparent 70%)", - }} - /> - </div> - - <div className="size-full flex flex-row p-4 space-x-4 z-20 relative"> - <Sidebar /> - - <main className="size-full overflow-hidden"> - <Outlet /> - </main> - </div> - </div> - ); -} diff --git a/packages/ui-new/src/pages/instances-view.tsx b/packages/ui-new/src/pages/instances-view.tsx deleted file mode 100644 index ad6bd38..0000000 --- a/packages/ui-new/src/pages/instances-view.tsx +++ /dev/null @@ -1,315 +0,0 @@ -import { Copy, Edit2, Plus, Trash2 } from "lucide-react"; -import { useEffect, useState } from "react"; -import InstanceCreationModal from "@/components/instance-creation-modal"; -import InstanceEditorModal from "@/components/instance-editor-modal"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { toNumber } from "@/lib/tsrs-utils"; -import { useInstancesStore } from "@/models/instances"; -import type { Instance } from "../types/bindings/instance"; - -export function InstancesView() { - const instancesStore = useInstancesStore(); - - // Modal / UI state - const [showCreateModal, setShowCreateModal] = useState(false); - const [showEditModal, setShowEditModal] = useState(false); - const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); - const [showDuplicateModal, setShowDuplicateModal] = useState(false); - - // Selected / editing instance state - const [selectedInstance, setSelectedInstance] = useState<Instance | null>( - null, - ); - const [editingInstance, setEditingInstance] = useState<Instance | null>(null); - - // Form fields - const [duplicateName, setDuplicateName] = useState(""); - - useEffect(() => { - instancesStore.refresh(); - }, [instancesStore.refresh]); - - // Handlers to open modals - const openCreate = () => { - setShowCreateModal(true); - }; - - const openEdit = (instance: Instance) => { - setEditingInstance({ ...instance }); - setShowEditModal(true); - }; - - const openDelete = (instance: Instance) => { - setSelectedInstance(instance); - setShowDeleteConfirm(true); - }; - - const openDuplicate = (instance: Instance) => { - setSelectedInstance(instance); - setDuplicateName(`${instance.name} (Copy)`); - setShowDuplicateModal(true); - }; - - const confirmDelete = async () => { - if (!selectedInstance) return; - await instancesStore.delete(selectedInstance.id); - setSelectedInstance(null); - setShowDeleteConfirm(false); - }; - - const confirmDuplicate = async () => { - if (!selectedInstance) return; - const name = duplicateName.trim(); - if (!name) return; - await instancesStore.duplicate(selectedInstance.id, name); - setSelectedInstance(null); - setDuplicateName(""); - setShowDuplicateModal(false); - }; - - const formatDate = (timestamp: number): string => - new Date(timestamp * 1000).toLocaleDateString(); - - const formatLastPlayed = (timestamp: number): string => { - const date = new Date(timestamp * 1000); - const now = new Date(); - const diff = now.getTime() - date.getTime(); - const days = Math.floor(diff / (1000 * 60 * 60 * 24)); - - if (days === 0) return "Today"; - if (days === 1) return "Yesterday"; - if (days < 7) return `${days} days ago`; - return date.toLocaleDateString(); - }; - - return ( - <div className="h-full flex flex-col gap-4 p-6 overflow-y-auto"> - <div className="flex items-center justify-between"> - <h1 className="text-2xl font-bold text-gray-900 dark:text-white"> - Instances - </h1> - <Button - type="button" - onClick={openCreate} - className="px-4 py-2 transition-colors" - > - <Plus size={18} /> - Create Instance - </Button> - </div> - - {instancesStore.instances.length === 0 ? ( - <div className="flex-1 flex items-center justify-center"> - <div className="text-center text-gray-500 dark:text-gray-400"> - <p className="text-lg mb-2">No instances yet</p> - <p className="text-sm">Create your first instance to get started</p> - </div> - </div> - ) : ( - <ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> - {instancesStore.instances.map((instance) => { - const isActive = instancesStore.activeInstance?.id === instance.id; - - return ( - <li - key={instance.id} - onClick={() => instancesStore.setActiveInstance(instance)} - onKeyDown={(e) => - e.key === "Enter" && - instancesStore.setActiveInstance(instance) - } - className={`relative p-4 text-left border-2 transition-all cursor-pointer hover:border-blue-500 ${ - isActive ? "border-blue-500" : "border-transparent" - } bg-gray-100 dark:bg-gray-800`} - > - {/* Instance Icon */} - {instance.iconPath ? ( - <div className="w-12 h-12 mb-3 rounded overflow-hidden"> - <img - src={instance.iconPath} - alt={instance.name} - className="w-full h-full object-cover" - /> - </div> - ) : ( - <div className="w-12 h-12 mb-3 rounded bg-linear-to-br from-blue-500 to-purple-600 flex items-center justify-center"> - <span className="text-white font-bold text-lg"> - {instance.name.charAt(0).toUpperCase()} - </span> - </div> - )} - - <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-1"> - {instance.name} - </h3> - - <div className="space-y-1 text-sm text-gray-600 dark:text-gray-400"> - {instance.versionId ? ( - <p className="truncate">Version: {instance.versionId}</p> - ) : ( - <p className="text-gray-400">No version selected</p> - )} - - {instance.modLoader && ( - <p className="truncate"> - Mod Loader:{" "} - <span className="capitalize">{instance.modLoader}</span> - </p> - )} - - <p className="truncate"> - Created: {formatDate(toNumber(instance.createdAt))} - </p> - - {instance.lastPlayed && ( - <p className="truncate"> - Last played:{" "} - {formatLastPlayed(toNumber(instance.lastPlayed))} - </p> - )} - </div> - - {/* Action Buttons */} - <div className="mt-4 flex gap-2"> - <button - type="button" - onClick={(e) => { - e.stopPropagation(); - openEdit(instance); - }} - className="flex-1 flex items-center justify-center gap-1 px-3 py-1.5 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded text-sm transition-colors" - > - <Edit2 size={14} /> - Edit - </button> - - <button - type="button" - onClick={(e) => { - e.stopPropagation(); - openDuplicate(instance); - }} - className="flex-1 flex items-center justify-center gap-1 px-3 py-1.5 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded text-sm transition-colors" - > - <Copy size={14} /> - Duplicate - </button> - - <button - type="button" - onClick={(e) => { - e.stopPropagation(); - openDelete(instance); - }} - className="flex-1 flex items-center justify-center gap-1 px-3 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded text-sm transition-colors" - > - <Trash2 size={14} /> - Delete - </button> - </div> - </li> - ); - })} - </ul> - )} - - <InstanceCreationModal - open={showCreateModal} - onOpenChange={setShowCreateModal} - /> - - <InstanceEditorModal - open={showEditModal} - instance={editingInstance} - onOpenChange={(open) => { - setShowEditModal(open); - if (!open) setEditingInstance(null); - }} - /> - - {/* Delete Confirmation */} - <Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}> - <DialogContent> - <DialogHeader> - <DialogTitle>Delete Instance</DialogTitle> - <DialogDescription> - Are you sure you want to delete "{selectedInstance?.name}"? This - action cannot be undone. - </DialogDescription> - </DialogHeader> - - <DialogFooter> - <Button - type="button" - variant="outline" - onClick={() => { - setShowDeleteConfirm(false); - setSelectedInstance(null); - }} - > - Cancel - </Button> - <Button - type="button" - onClick={confirmDelete} - className="bg-red-600 text-white hover:bg-red-500" - > - Delete - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - - {/* Duplicate Modal */} - <Dialog open={showDuplicateModal} onOpenChange={setShowDuplicateModal}> - <DialogContent> - <DialogHeader> - <DialogTitle>Duplicate Instance</DialogTitle> - <DialogDescription> - Provide a name for the duplicated instance. - </DialogDescription> - </DialogHeader> - - <div className="mt-4"> - <Input - value={duplicateName} - onChange={(e) => setDuplicateName(e.target.value)} - placeholder="New instance name" - onKeyDown={(e) => e.key === "Enter" && confirmDuplicate()} - /> - </div> - - <DialogFooter> - <Button - type="button" - variant="outline" - onClick={() => { - setShowDuplicateModal(false); - setSelectedInstance(null); - setDuplicateName(""); - }} - > - Cancel - </Button> - <Button - type="button" - onClick={confirmDuplicate} - disabled={!duplicateName.trim()} - > - Duplicate - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - </div> - ); -} diff --git a/packages/ui-new/src/pages/settings-view.tsx.bk b/packages/ui-new/src/pages/settings-view.tsx.bk deleted file mode 100644 index ac43d9b..0000000 --- a/packages/ui-new/src/pages/settings-view.tsx.bk +++ /dev/null @@ -1,1158 +0,0 @@ -import { open } from "@tauri-apps/plugin-dialog"; -import { - Coffee, - Download, - FileJson, - Loader2, - RefreshCw, - Upload, -} from "lucide-react"; -import { useEffect, useState } from "react"; -import { toast } from "sonner"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Checkbox } from "@/components/ui/checkbox"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Separator } from "@/components/ui/separator"; -import { Switch } from "@/components/ui/switch"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Textarea } from "@/components/ui/textarea"; -import { useSettingsStore } from "../stores/settings-store"; - -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'.", - }, -]; - -export function SettingsView() { - const { - settings, - backgroundUrl, - javaInstallations, - isDetectingJava, - showJavaDownloadModal, - selectedDownloadSource, - javaCatalog, - isLoadingCatalog, - catalogError, - selectedMajorVersion, - selectedImageType, - showOnlyRecommended, - searchQuery, - isDownloadingJava, - downloadProgress, - javaDownloadStatus, - pendingDownloads, - ollamaModels, - openaiModels, - isLoadingOllamaModels, - isLoadingOpenaiModels, - ollamaModelsError, - openaiModelsError, - showConfigEditor, - rawConfigContent, - configFilePath, - configEditorError, - filteredReleases, - availableMajorVersions, - installStatus, - selectedRelease, - currentModelOptions, - loadSettings, - saveSettings, - detectJava, - selectJava, - openJavaDownloadModal, - closeJavaDownloadModal, - loadJavaCatalog, - refreshCatalog, - loadPendingDownloads, - selectMajorVersion, - downloadJava, - cancelDownload, - resumeDownloads, - openConfigEditor, - closeConfigEditor, - saveRawConfig, - loadOllamaModels, - loadOpenaiModels, - set, - setSetting, - setAssistantSetting, - setFeatureFlag, - } = useSettingsStore(); - - // Mark potentially-unused variables as referenced so TypeScript does not report - // them as unused in this file (they are part of the store API and used elsewhere). - // This is a no-op but satisfies the compiler. - void selectedDownloadSource; - void javaCatalog; - void javaDownloadStatus; - void pendingDownloads; - void ollamaModels; - void openaiModels; - void isLoadingOllamaModels; - void isLoadingOpenaiModels; - void ollamaModelsError; - void openaiModelsError; - void selectedRelease; - void loadJavaCatalog; - void loadPendingDownloads; - void cancelDownload; - void resumeDownloads; - void setFeatureFlag; - const [selectedPersona, setSelectedPersona] = useState("default"); - const [migrating, setMigrating] = useState(false); - const [activeTab, setActiveTab] = useState("appearance"); - - useEffect(() => { - loadSettings(); - detectJava(); - }, [loadSettings, detectJava]); - - useEffect(() => { - if (activeTab === "assistant") { - if (settings.assistant.llmProvider === "ollama") { - loadOllamaModels(); - } else if (settings.assistant.llmProvider === "openai") { - loadOpenaiModels(); - } - } - }, [ - activeTab, - settings.assistant.llmProvider, - loadOllamaModels, - loadOpenaiModels, - ]); - - const handleSelectBackground = async () => { - try { - const selected = await open({ - multiple: false, - filters: [ - { - name: "Images", - extensions: ["png", "jpg", "jpeg", "webp", "gif"], - }, - ], - }); - - if (selected && typeof selected === "string") { - setSetting("customBackgroundPath", selected); - saveSettings(); - } - } catch (e) { - console.error("Failed to select background:", e); - toast.error("Failed to select background"); - } - }; - - const handleClearBackground = () => { - setSetting("customBackgroundPath", null); - saveSettings(); - }; - - const handleApplyPersona = (value: string) => { - const persona = personas.find((p) => p.value === value); - if (persona) { - setAssistantSetting("systemPrompt", persona.prompt); - setSelectedPersona(value); - saveSettings(); - } - }; - - const handleResetSystemPrompt = () => { - const defaultPersona = personas.find((p) => p.value === "default"); - if (defaultPersona) { - setAssistantSetting("systemPrompt", defaultPersona.prompt); - setSelectedPersona("default"); - saveSettings(); - } - }; - - const handleRunMigration = async () => { - if (migrating) return; - setMigrating(true); - try { - await new Promise((resolve) => setTimeout(resolve, 2000)); - toast.success("Migration complete! Files migrated successfully"); - } catch (e) { - console.error("Migration failed:", e); - toast.error(`Migration failed: ${e}`); - } finally { - setMigrating(false); - } - }; - - return ( - <div className="h-full flex flex-col p-6 overflow-hidden"> - <div className="flex items-center justify-between mb-6"> - <h2 className="text-3xl font-black bg-clip-text text-transparent bg-linear-to-r dark:from-white dark:to-white/60 from-gray-900 to-gray-600"> - Settings - </h2> - - <Button - variant="outline" - size="sm" - onClick={openConfigEditor} - className="gap-2" - > - <FileJson className="h-4 w-4" /> - <span className="hidden sm:inline">Open JSON</span> - </Button> - </div> - - <Tabs - value={activeTab} - onValueChange={setActiveTab} - className="flex-1 overflow-hidden" - > - <TabsList className="grid grid-cols-4 mb-6"> - <TabsTrigger value="appearance">Appearance</TabsTrigger> - <TabsTrigger value="java">Java</TabsTrigger> - <TabsTrigger value="advanced">Advanced</TabsTrigger> - <TabsTrigger value="assistant">Assistant</TabsTrigger> - </TabsList> - - <ScrollArea className="flex-1 pr-2"> - <TabsContent value="appearance" className="space-y-6"> - <Card className="border-border"> - <CardHeader> - <CardTitle className="text-lg">Appearance</CardTitle> - </CardHeader> - <CardContent className="space-y-6"> - <div> - <Label className="mb-3">Custom Background Image</Label> - <div className="flex items-center gap-6"> - <div className="w-40 h-24 rounded-xl overflow-hidden bg-secondary border relative group shadow-lg"> - {backgroundUrl ? ( - <img - src={backgroundUrl} - alt="Background Preview" - className="w-full h-full object-cover" - onError={(e) => { - console.error("Failed to load image"); - e.currentTarget.style.display = "none"; - }} - /> - ) : ( - <div className="w-full h-full bg-linear-to-br from-emerald-900 via-zinc-900 to-indigo-950" /> - )} - {!backgroundUrl && ( - <div className="absolute inset-0 flex items-center justify-center text-xs text-white/50 bg-black/20"> - Default Gradient - </div> - )} - </div> - - <div className="flex flex-col gap-2"> - <Button - variant="outline" - onClick={handleSelectBackground} - > - Select Image - </Button> - {backgroundUrl && ( - <Button - variant="ghost" - className="text-red-500" - onClick={handleClearBackground} - > - Reset to Default - </Button> - )} - </div> - </div> - <p className="text-sm text-muted-foreground mt-3"> - Select an image from your computer to replace the default - gradient background. - </p> - </div> - - <Separator /> - - <div className="space-y-4"> - <div className="flex items-center justify-between"> - <div> - <Label className="text-base">Visual Effects</Label> - <p className="text-sm text-muted-foreground"> - Enable particle effects and animated gradients. - </p> - </div> - <Switch - checked={settings.enableVisualEffects} - onCheckedChange={(checked) => { - setSetting("enableVisualEffects", checked); - saveSettings(); - }} - /> - </div> - - {settings.enableVisualEffects && ( - <div className="pl-4 border-l-2 border-border"> - <div className="space-y-2"> - <Label>Theme Effect</Label> - <Select - value={settings.activeEffect} - onValueChange={(value) => { - setSetting("activeEffect", value); - saveSettings(); - }} - > - <SelectTrigger className="w-52"> - <SelectValue /> - </SelectTrigger> - <SelectContent> - {effectOptions.map((option) => ( - <SelectItem - key={option.value} - value={option.value} - > - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - <p className="text-sm text-muted-foreground"> - Select the active visual theme. - </p> - </div> - </div> - )} - - <div className="flex items-center justify-between"> - <div> - <Label className="text-base">GPU Acceleration</Label> - <p className="text-sm text-muted-foreground"> - Enable GPU acceleration for the interface. - </p> - </div> - <Switch - checked={settings.enableGpuAcceleration} - onCheckedChange={(checked) => { - setSetting("enableGpuAcceleration", checked); - saveSettings(); - }} - /> - </div> - </div> - </CardContent> - </Card> - </TabsContent> - - <TabsContent value="java" className="space-y-6"> - <Card className="border-border"> - <CardHeader> - <CardTitle className="text-lg">Java Environment</CardTitle> - </CardHeader> - <CardContent className="space-y-6"> - <div> - <Label className="mb-2">Java Path</Label> - <div className="flex gap-2"> - <Input - value={settings.javaPath} - onChange={(e) => setSetting("javaPath", e.target.value)} - className="flex-1" - placeholder="java or full path to java executable" - /> - <Button - variant="outline" - onClick={() => detectJava()} - disabled={isDetectingJava} - > - {isDetectingJava ? ( - <Loader2 className="h-4 w-4 animate-spin" /> - ) : ( - "Detect" - )} - </Button> - </div> - <p className="text-sm text-muted-foreground mt-2"> - Path to Java executable. - </p> - </div> - - <div> - <Label className="mb-2">Memory Settings (MB)</Label> - <div className="grid grid-cols-2 gap-4"> - <div> - <Label htmlFor="min-memory" className="text-sm"> - Minimum Memory - </Label> - <Input - id="min-memory" - type="number" - value={settings.minMemory} - onChange={(e) => - setSetting( - "minMemory", - parseInt(e.target.value, 10) || 1024, - ) - } - min={512} - step={256} - /> - </div> - <div> - <Label htmlFor="max-memory" className="text-sm"> - Maximum Memory - </Label> - <Input - id="max-memory" - type="number" - value={settings.maxMemory} - onChange={(e) => - setSetting( - "maxMemory", - parseInt(e.target.value, 10) || 2048, - ) - } - min={1024} - step={256} - /> - </div> - </div> - <p className="text-sm text-muted-foreground mt-2"> - Memory allocation for Minecraft. - </p> - </div> - - <Separator /> - - <div> - <div className="flex items-center justify-between mb-4"> - <Label className="text-base"> - Detected Java Installations - </Label> - <Button - variant="outline" - size="sm" - onClick={() => detectJava()} - disabled={isDetectingJava} - > - <RefreshCw - className={`h-4 w-4 mr-2 ${isDetectingJava ? "animate-spin" : ""}`} - /> - Rescan - </Button> - </div> - - {javaInstallations.length === 0 ? ( - <div className="text-center py-8 text-muted-foreground border rounded-lg"> - <Coffee className="h-12 w-12 mx-auto mb-4 opacity-30" /> - <p>No Java installations detected</p> - </div> - ) : ( - <div className="space-y-2"> - {javaInstallations.map((installation) => ( - <Card - key={installation.path} - className={`p-3 cursor-pointer transition-colors ${ - settings.javaPath === installation.path - ? "border-primary bg-primary/5" - : "" - }`} - onClick={() => selectJava(installation.path)} - > - <div className="flex items-center justify-between"> - <div> - <div className="font-medium flex items-center gap-2"> - <Coffee className="h-4 w-4" /> - {installation.version} - </div> - <div className="text-sm text-muted-foreground font-mono"> - {installation.path} - </div> - </div> - {settings.javaPath === installation.path && ( - <div className="h-5 w-5 text-primary">✓</div> - )} - </div> - </Card> - ))} - </div> - )} - - <div className="mt-4"> - <Button - variant="default" - className="w-full" - onClick={openJavaDownloadModal} - > - <Download className="h-4 w-4 mr-2" /> - Download Java - </Button> - </div> - </div> - </CardContent> - </Card> - </TabsContent> - - <TabsContent value="advanced" className="space-y-6"> - <Card className="border-border"> - <CardHeader> - <CardTitle className="text-lg">Advanced Settings</CardTitle> - </CardHeader> - <CardContent className="space-y-6"> - <div> - <Label className="mb-2">Download Threads</Label> - <Input - type="number" - value={settings.downloadThreads} - onChange={(e) => - setSetting( - "downloadThreads", - parseInt(e.target.value, 10) || 32, - ) - } - min={1} - max={64} - /> - <p className="text-sm text-muted-foreground mt-2"> - Number of concurrent downloads. - </p> - </div> - - <div> - <Label className="mb-2">Log Upload Service</Label> - <Select - value={settings.logUploadService} - onValueChange={(value) => { - setSetting("logUploadService", value as any); - saveSettings(); - }} - > - <SelectTrigger> - <SelectValue /> - </SelectTrigger> - <SelectContent> - {logServiceOptions.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - - {settings.logUploadService === "pastebin.com" && ( - <div> - <Label className="mb-2">Pastebin API Key</Label> - <Input - type="password" - value={settings.pastebinApiKey || ""} - onChange={(e) => - setSetting("pastebinApiKey", e.target.value || null) - } - placeholder="Enter your Pastebin API key" - /> - </div> - )} - - <Separator /> - - <div className="space-y-4"> - <div className="flex items-center justify-between"> - <div> - <Label className="text-base">Use Shared Caches</Label> - <p className="text-sm text-muted-foreground"> - Share downloaded assets between instances. - </p> - </div> - <Switch - checked={settings.useSharedCaches} - onCheckedChange={(checked) => { - setSetting("useSharedCaches", checked); - saveSettings(); - }} - /> - </div> - - {!settings.useSharedCaches && ( - <div className="flex items-center justify-between"> - <div> - <Label className="text-base"> - Keep Legacy Per-Instance Storage - </Label> - <p className="text-sm text-muted-foreground"> - Maintain separate cache folders for compatibility. - </p> - </div> - <Switch - checked={settings.keepLegacyPerInstanceStorage} - onCheckedChange={(checked) => { - setSetting("keepLegacyPerInstanceStorage", checked); - saveSettings(); - }} - /> - </div> - )} - - {settings.useSharedCaches && ( - <div className="mt-4"> - <Button - variant="outline" - className="w-full" - onClick={handleRunMigration} - disabled={migrating} - > - {migrating ? ( - <Loader2 className="h-4 w-4 mr-2 animate-spin" /> - ) : ( - <Upload className="h-4 w-4 mr-2" /> - )} - {migrating - ? "Migrating..." - : "Migrate to Shared Caches"} - </Button> - </div> - )} - </div> - </CardContent> - </Card> - </TabsContent> - - <TabsContent value="assistant" className="space-y-6"> - <Card className="border-border"> - <CardHeader> - <CardTitle className="text-lg">AI Assistant</CardTitle> - </CardHeader> - <CardContent className="space-y-6"> - <div className="flex items-center justify-between"> - <div> - <Label className="text-base">Enable Assistant</Label> - <p className="text-sm text-muted-foreground"> - Enable the AI assistant for help with Minecraft issues. - </p> - </div> - <Switch - checked={settings.assistant.enabled} - onCheckedChange={(checked) => { - setAssistantSetting("enabled", checked); - saveSettings(); - }} - /> - </div> - - {settings.assistant.enabled && ( - <> - <div> - <Label className="mb-2">LLM Provider</Label> - <Select - value={settings.assistant.llmProvider} - onValueChange={(value) => { - setAssistantSetting("llmProvider", value as any); - saveSettings(); - }} - > - <SelectTrigger> - <SelectValue /> - </SelectTrigger> - <SelectContent> - {llmProviderOptions.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - - <div> - <Label className="mb-2">Model</Label> - <Select - value={ - settings.assistant.llmProvider === "ollama" - ? settings.assistant.ollamaModel - : settings.assistant.openaiModel - } - onValueChange={(value) => { - if (settings.assistant.llmProvider === "ollama") { - setAssistantSetting("ollamaModel", value); - } else { - setAssistantSetting("openaiModel", value); - } - saveSettings(); - }} - > - <SelectTrigger> - <SelectValue /> - </SelectTrigger> - <SelectContent> - {currentModelOptions.map((model) => ( - <SelectItem key={model.value} value={model.value}> - {model.label} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - - {settings.assistant.llmProvider === "ollama" && ( - <div> - <Label className="mb-2">Ollama Endpoint</Label> - <Input - value={settings.assistant.ollamaEndpoint} - onChange={(e) => { - setAssistantSetting( - "ollamaEndpoint", - e.target.value, - ); - saveSettings(); - }} - placeholder="http://localhost:11434" - /> - </div> - )} - - {settings.assistant.llmProvider === "openai" && ( - <> - <div> - <Label className="mb-2">OpenAI API Key</Label> - <Input - type="password" - value={settings.assistant.openaiApiKey || ""} - onChange={(e) => { - setAssistantSetting( - "openaiApiKey", - e.target.value || null, - ); - saveSettings(); - }} - placeholder="Enter your OpenAI API key" - /> - </div> - <div> - <Label className="mb-2">OpenAI Endpoint</Label> - <Input - value={settings.assistant.openaiEndpoint} - onChange={(e) => { - setAssistantSetting( - "openaiEndpoint", - e.target.value, - ); - saveSettings(); - }} - placeholder="https://api.openai.com/v1" - /> - </div> - </> - )} - - <div> - <Label className="mb-2">Response Language</Label> - <Select - value={settings.assistant.responseLanguage} - onValueChange={(value) => { - setAssistantSetting("responseLanguage", value); - saveSettings(); - }} - > - <SelectTrigger> - <SelectValue /> - </SelectTrigger> - <SelectContent> - {languageOptions.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - - <div> - <Label className="mb-2">Assistant Persona</Label> - <Select - value={selectedPersona} - onValueChange={handleApplyPersona} - > - <SelectTrigger> - <SelectValue /> - </SelectTrigger> - <SelectContent> - {personas.map((persona) => ( - <SelectItem - key={persona.value} - value={persona.value} - > - {persona.label} - </SelectItem> - ))} - </SelectContent> - </Select> - <div className="mt-2"> - <Button - variant="outline" - size="sm" - onClick={handleResetSystemPrompt} - > - Reset to Default - </Button> - </div> - </div> - - <div> - <Label className="mb-2">System Prompt</Label> - - <Textarea - value={settings.assistant.systemPrompt} - onChange={(e) => { - setAssistantSetting("systemPrompt", e.target.value); - saveSettings(); - }} - rows={6} - className="font-mono text-sm" - /> - </div> - - <div> - <Label className="mb-2">Text-to-Speech</Label> - - <Select - value={settings.assistant.ttsProvider} - onValueChange={(value) => { - setAssistantSetting("ttsProvider", value); - saveSettings(); - }} - > - <SelectTrigger> - <SelectValue /> - </SelectTrigger> - - <SelectContent> - {ttsProviderOptions.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - </> - )} - </CardContent> - </Card> - </TabsContent> - </ScrollArea> - </Tabs> - - {/* Java Download Modal */} - <Dialog - open={showJavaDownloadModal} - onOpenChange={closeJavaDownloadModal} - > - <DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden"> - <DialogHeader> - <DialogTitle>Download Java</DialogTitle> - <DialogDescription> - Download and install Java for Minecraft. - </DialogDescription> - </DialogHeader> - - <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> - <div className="space-y-4"> - <div> - <Label className="mb-2">Java Version</Label> - <Select - value={selectedMajorVersion?.toString() || ""} - onValueChange={(v) => selectMajorVersion(parseInt(v, 10))} - > - <SelectTrigger> - <SelectValue placeholder="Select version" /> - </SelectTrigger> - <SelectContent> - {availableMajorVersions.map((version) => ( - <SelectItem key={version} value={version.toString()}> - Java {version} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - - <div> - <Label className="mb-2">Type</Label> - <Select - value={selectedImageType} - onValueChange={(v) => set({ selectedImageType: v as any })} - > - <SelectTrigger> - <SelectValue /> - </SelectTrigger> - <SelectContent> - <SelectItem value="jre">JRE (Runtime)</SelectItem> - <SelectItem value="jdk">JDK (Development)</SelectItem> - </SelectContent> - </Select> - </div> - - <div className="flex items-center space-x-2"> - <Checkbox - id="recommended" - checked={showOnlyRecommended} - onCheckedChange={(checked) => - set({ showOnlyRecommended: !!checked }) - } - /> - <Label htmlFor="recommended">Show only LTS/Recommended</Label> - </div> - - <div> - <Label className="mb-2">Search</Label> - <Input - placeholder="Search versions..." - value={searchQuery} - onChange={(e) => set({ searchQuery: e.target.value })} - /> - </div> - - <Button - variant="outline" - size="sm" - onClick={refreshCatalog} - disabled={isLoadingCatalog} - > - <RefreshCw - className={`h-4 w-4 mr-2 ${isLoadingCatalog ? "animate-spin" : ""}`} - /> - Refresh Catalog - </Button> - </div> - - <div className="md:col-span-2"> - <ScrollArea className="h-75 pr-4"> - {isLoadingCatalog ? ( - <div className="flex items-center justify-center h-full"> - <Loader2 className="h-8 w-8 animate-spin" /> - </div> - ) : catalogError ? ( - <div className="text-red-500 p-4">{catalogError}</div> - ) : filteredReleases.length === 0 ? ( - <div className="text-muted-foreground p-4 text-center"> - No Java versions found - </div> - ) : ( - <div className="space-y-2"> - {filteredReleases.map((release) => { - const status = installStatus( - release.majorVersion, - release.imageType, - ); - return ( - <Card - key={`${release.majorVersion}-${release.imageType}`} - className="p-3 cursor-pointer hover:bg-accent" - onClick={() => - selectMajorVersion(release.majorVersion) - } - > - <div className="flex items-center justify-between"> - <div> - <div className="font-medium"> - Java {release.majorVersion}{" "} - {release.imageType.toUpperCase()} - </div> - <div className="text-sm text-muted-foreground"> - {release.releaseName} • {release.architecture}{" "} - {release.architecture} - </div> - </div> - <div className="flex items-center gap-2"> - {release.isLts && ( - <Badge variant="secondary">LTS</Badge> - )} - {status === "installed" && ( - <Badge variant="default">Installed</Badge> - )} - {status === "available" && ( - <Button - variant="ghost" - size="sm" - onClick={(e) => { - e.stopPropagation(); - selectMajorVersion(release.majorVersion); - downloadJava(); - }} - > - <Download className="h-3 w-3 mr-1" /> - Download - </Button> - )} - </div> - </div> - </Card> - ); - })} - </div> - )} - </ScrollArea> - </div> - </div> - - {isDownloadingJava && downloadProgress && ( - <div className="mt-4 p-4 border rounded-lg"> - <div className="flex justify-between items-center mb-2"> - <span className="text-sm font-medium"> - {downloadProgress.fileName} - </span> - <span className="text-sm text-muted-foreground"> - {Math.round(downloadProgress.percentage)}% - </span> - </div> - <div className="w-full bg-secondary h-2 rounded-full overflow-hidden"> - <div - className="bg-primary h-full transition-all duration-300" - style={{ width: `${downloadProgress.percentage}%` }} - /> - </div> - </div> - )} - - <DialogFooter> - <Button - variant="outline" - onClick={closeJavaDownloadModal} - disabled={isDownloadingJava} - > - Cancel - </Button> - {selectedMajorVersion && ( - <Button - onClick={() => downloadJava()} - disabled={isDownloadingJava} - > - {isDownloadingJava ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - Downloading... - </> - ) : ( - <> - <Download className="mr-2 h-4 w-4" /> - Download Java {selectedMajorVersion} - </> - )} - </Button> - )} - </DialogFooter> - </DialogContent> - </Dialog> - - {/* Config Editor Modal */} - <Dialog open={showConfigEditor} onOpenChange={closeConfigEditor}> - <DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden"> - <DialogHeader> - <DialogTitle>Edit Configuration</DialogTitle> - <DialogDescription> - Edit the raw JSON configuration file. - </DialogDescription> - </DialogHeader> - - <div className="text-sm text-muted-foreground mb-2"> - File: {configFilePath} - </div> - - {configEditorError && ( - <div className="text-red-500 p-3 bg-red-50 dark:bg-red-950/30 rounded-md"> - {configEditorError} - </div> - )} - - <Textarea - value={rawConfigContent} - onChange={(e) => set({ rawConfigContent: e.target.value })} - className="font-mono text-sm h-100 resize-none" - spellCheck={false} - /> - - <DialogFooter> - <Button variant="outline" onClick={closeConfigEditor}> - Cancel - </Button> - <Button onClick={() => saveRawConfig()}>Save Changes</Button> - </DialogFooter> - </DialogContent> - </Dialog> - </div> - ); -} diff --git a/packages/ui-new/src/pages/settings.tsx b/packages/ui-new/src/pages/settings.tsx deleted file mode 100644 index 440a5dc..0000000 --- a/packages/ui-new/src/pages/settings.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import { toNumber } from "es-toolkit/compat"; -import { FileJsonIcon } from "lucide-react"; -import { useEffect, useState } from "react"; -import { migrateSharedCaches } from "@/client"; -import { ConfigEditor } from "@/components/config-editor"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Field, - FieldContent, - FieldDescription, - FieldGroup, - FieldLabel, - FieldLegend, - FieldSet, -} from "@/components/ui/field"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Spinner } from "@/components/ui/spinner"; -import { Switch } from "@/components/ui/switch"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { useSettingsStore } from "@/models/settings"; - -export type SettingsTab = "general" | "appearance" | "advanced"; - -export function SettingsPage() { - const { config, ...settings } = useSettingsStore(); - const [showConfigEditor, setShowConfigEditor] = useState<boolean>(false); - const [activeTab, setActiveTab] = useState<SettingsTab>("general"); - - useEffect(() => { - if (!config) settings.refresh(); - }, [config, settings.refresh]); - - const renderScrollArea = () => { - if (!config) { - return ( - <div className="size-full justify-center items-center"> - <Spinner /> - </div> - ); - } - return ( - <ScrollArea className="size-full pr-2"> - <TabsContent value="general" className="size-full"> - <Card className="size-full"> - <CardHeader> - <CardTitle className="font-bold text-xl">General</CardTitle> - </CardHeader> - <CardContent> - <FieldGroup> - <FieldSet> - <FieldLegend>Window Options</FieldLegend> - <FieldDescription> - May not work on some platforms like Linux Niri. - </FieldDescription> - <FieldGroup> - <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> - <Field> - <FieldLabel htmlFor="width"> - Window Default Width - </FieldLabel> - <Input - type="number" - name="width" - value={config?.width} - onChange={(e) => { - settings.merge({ - width: toNumber(e.target.value), - }); - }} - onBlur={() => { - settings.save(); - }} - min={800} - max={3840} - /> - </Field> - <Field> - <FieldLabel htmlFor="height"> - Window Default Height - </FieldLabel> - <Input - type="number" - name="height" - value={config?.height} - onChange={(e) => { - settings.merge({ - height: toNumber(e.target.value), - }); - }} - onBlur={() => { - settings.save(); - }} - min={600} - max={2160} - /> - </Field> - </div> - <Field className="flex flex-row items-center justify-between"> - <FieldContent> - <FieldLabel htmlFor="gpu-acceleration"> - GPU Acceleration - </FieldLabel> - <FieldDescription> - Enable GPU acceleration for the interface. - </FieldDescription> - </FieldContent> - <Switch - checked={config?.enableGpuAcceleration} - onCheckedChange={(checked) => { - settings.merge({ - enableGpuAcceleration: checked, - }); - settings.save(); - }} - /> - </Field> - </FieldGroup> - </FieldSet> - <FieldSet> - <FieldLegend>Network Options</FieldLegend> - <Field> - <Label htmlFor="download-threads">Download Threads</Label> - <Input - type="number" - name="download-threads" - value={config?.downloadThreads} - onChange={(e) => { - settings.merge({ - downloadThreads: toNumber(e.target.value), - }); - }} - onBlur={() => { - settings.save(); - }} - min={1} - max={64} - /> - </Field> - </FieldSet> - </FieldGroup> - </CardContent> - </Card> - </TabsContent> - <TabsContent value="java" className="size-full"> - <Card className="size-full"> - <CardHeader> - <CardTitle className="font-bold text-xl"> - Java Installations - </CardTitle> - <CardContent></CardContent> - </CardHeader> - </Card> - </TabsContent> - <TabsContent value="appearance" className="size-full"> - <Card className="size-full"> - <CardHeader> - <CardTitle className="font-bold text-xl">Appearance</CardTitle> - </CardHeader> - <CardContent> - <FieldGroup> - <Field className="flex flex-row"> - <FieldContent> - <FieldLabel htmlFor="theme">Theme</FieldLabel> - <FieldDescription> - Select your prefered theme. - </FieldDescription> - </FieldContent> - <Select - items={[ - { label: "Dark", value: "dark" }, - { label: "Light", value: "light" }, - { label: "System", value: "system" }, - ]} - value={config.theme} - onValueChange={async (value) => { - if ( - value === "system" || - value === "light" || - value === "dark" - ) { - settings.merge({ theme: value }); - await settings.save(); - settings.applyTheme(value); - } - }} - > - <SelectTrigger className="w-full max-w-48"> - <SelectValue placeholder="Please select a prefered theme" /> - </SelectTrigger> - <SelectContent alignItemWithTrigger={false}> - <SelectGroup> - <SelectItem value="system">System</SelectItem> - <SelectItem value="light">Light</SelectItem> - <SelectItem value="dark">Dark</SelectItem> - </SelectGroup> - </SelectContent> - </Select> - </Field> - </FieldGroup> - </CardContent> - </Card> - </TabsContent> - <TabsContent value="advanced" className="size-full"> - <Card className="size-full"> - <CardHeader> - <CardTitle className="font-bold text-xl">Advanced</CardTitle> - </CardHeader> - <CardContent> - <FieldGroup> - <FieldSet> - <FieldLegend>Advanced Options</FieldLegend> - <FieldGroup> - <Field className="flex flex-row items-center justify-between"> - <FieldContent> - <FieldLabel htmlFor="use-shared-caches"> - Use Shared Caches - </FieldLabel> - <FieldDescription> - Share downloaded assets between instances. - </FieldDescription> - </FieldContent> - <Switch - checked={config?.useSharedCaches} - onCheckedChange={async (checked) => { - checked && (await migrateSharedCaches()); - settings.merge({ - useSharedCaches: checked, - }); - settings.save(); - }} - /> - </Field> - <Field className="flex flex-row items-center justify-between"> - <FieldContent> - <FieldLabel htmlFor="keep-per-instance-storage"> - Keep Legacy Per-Instance Storage - </FieldLabel> - <FieldDescription> - Maintain separate cache folders for compatibility. - </FieldDescription> - </FieldContent> - <Switch - checked={config?.keepLegacyPerInstanceStorage} - onCheckedChange={(checked) => { - settings.merge({ - keepLegacyPerInstanceStorage: checked, - }); - settings.save(); - }} - /> - </Field> - </FieldGroup> - </FieldSet> - </FieldGroup> - </CardContent> - </Card> - </TabsContent> - </ScrollArea> - ); - }; - - return ( - <div className="size-full flex flex-col p-6 space-y-6"> - <div className="flex items-center justify-between"> - <h2 className="text-3xl font-black bg-clip-text text-transparent bg-linear-to-r dark:from-white dark:to-white/60 from-gray-900 to-gray-600"> - Settings - </h2> - - <Button - variant="outline" - size="sm" - onClick={() => setShowConfigEditor(true)} - > - <FileJsonIcon /> - <span className="hidden sm:inline">Open JSON</span> - </Button> - </div> - - <Tabs - value={activeTab} - onValueChange={setActiveTab} - className="size-full flex flex-col gap-6" - > - <TabsList> - <TabsTrigger value="general">General</TabsTrigger> - <TabsTrigger value="java">Java</TabsTrigger> - <TabsTrigger value="appearance">Appearance</TabsTrigger> - <TabsTrigger value="advanced">Advanced</TabsTrigger> - </TabsList> - {renderScrollArea()} - </Tabs> - - <ConfigEditor - open={showConfigEditor} - onOpenChange={() => setShowConfigEditor(false)} - /> - </div> - ); -} diff --git a/packages/ui-new/src/pages/versions-view.tsx.bk b/packages/ui-new/src/pages/versions-view.tsx.bk deleted file mode 100644 index d54596d..0000000 --- a/packages/ui-new/src/pages/versions-view.tsx.bk +++ /dev/null @@ -1,662 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import { listen, type UnlistenFn } from "@tauri-apps/api/event"; -import { Coffee, Loader2, Search, Trash2 } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; -import { toast } from "sonner"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { useInstancesStore } from "../models/instances"; -import { useGameStore } from "../stores/game-store"; -import type { Version } from "../types/bindings/manifest"; - -interface InstalledModdedVersion { - id: string; - javaVersion?: number; -} - -type TypeFilter = "all" | "release" | "snapshot" | "installed"; - -export function VersionsView() { - const { versions, selectedVersion, loadVersions, setSelectedVersion } = - useGameStore(); - const { activeInstance } = useInstancesStore(); - - const [searchQuery, setSearchQuery] = useState(""); - const [typeFilter, setTypeFilter] = useState<TypeFilter>("all"); - const [installedModdedVersions, setInstalledModdedVersions] = useState< - InstalledModdedVersion[] - >([]); - const [, setIsLoadingModded] = useState(false); - const [showDeleteDialog, setShowDeleteDialog] = useState(false); - const [versionToDelete, setVersionToDelete] = useState<string | null>(null); - const [isDeleting, setIsDeleting] = useState(false); - const [selectedVersionMetadata, setSelectedVersionMetadata] = useState<{ - id: string; - javaVersion?: number; - isInstalled: boolean; - } | null>(null); - const [isLoadingMetadata, setIsLoadingMetadata] = useState(false); - const [showModLoaderSelector, setShowModLoaderSelector] = useState(false); - - const normalizedQuery = searchQuery.trim().toLowerCase().replace(/。/g, "."); - - // Load installed modded versions with Java version info - const loadInstalledModdedVersions = useCallback(async () => { - if (!activeInstance) { - setInstalledModdedVersions([]); - setIsLoadingModded(false); - return; - } - - setIsLoadingModded(true); - try { - const allInstalled = await invoke<Array<{ id: string; type: string }>>( - "list_installed_versions", - { instanceId: activeInstanceId }, - ); - - const moddedIds = allInstalled - .filter((v) => v.type === "fabric" || v.type === "forge") - .map((v) => v.id); - - const versionsWithJava = await Promise.all( - moddedIds.map(async (id) => { - try { - const javaVersion = await invoke<number | null>( - "get_version_java_version", - { - instanceId: activeInstanceId, - versionId: id, - }, - ); - return { - id, - javaVersion: javaVersion ?? undefined, - }; - } catch (e) { - console.error(`Failed to get Java version for ${id}:`, e); - return { id, javaVersion: undefined }; - } - }), - ); - - setInstalledModdedVersions(versionsWithJava); - } catch (e) { - console.error("Failed to load installed modded versions:", e); - toast.error("Error loading modded versions"); - } finally { - setIsLoadingModded(false); - } - }, [activeInstanceId]); - - // Combined versions list (vanilla + modded) - const allVersions = (() => { - const moddedVersions: Version[] = installedModdedVersions.map((v) => { - const versionType = v.id.startsWith("fabric-loader-") - ? "fabric" - : v.id.includes("-forge-") - ? "forge" - : "fabric"; - return { - id: v.id, - type: versionType, - url: "", - time: "", - releaseTime: new Date().toISOString(), - javaVersion: BigInt(v.javaVersion ?? 0), - isInstalled: true, - }; - }); - return [...moddedVersions, ...versions]; - })(); - - // Filter versions based on search and type filter - const filteredVersions = allVersions.filter((version) => { - if (typeFilter === "release" && version.type !== "release") return false; - if (typeFilter === "snapshot" && version.type !== "snapshot") return false; - if (typeFilter === "installed" && !version.isInstalled) return false; - - if ( - normalizedQuery && - !version.id.toLowerCase().includes(normalizedQuery) - ) { - return false; - } - - return true; - }); - - // Get version badge styling - const getVersionBadge = (type: string) => { - switch (type) { - case "release": - return { - text: "Release", - variant: "default" as const, - className: "bg-emerald-500 hover:bg-emerald-600", - }; - case "snapshot": - return { - text: "Snapshot", - variant: "secondary" as const, - className: "bg-amber-500 hover:bg-amber-600", - }; - case "fabric": - return { - text: "Fabric", - variant: "outline" as const, - className: "border-indigo-500 text-indigo-700 dark:text-indigo-300", - }; - case "forge": - return { - text: "Forge", - variant: "outline" as const, - className: "border-orange-500 text-orange-700 dark:text-orange-300", - }; - case "modpack": - return { - text: "Modpack", - variant: "outline" as const, - className: "border-purple-500 text-purple-700 dark:text-purple-300", - }; - default: - return { - text: type, - variant: "outline" as const, - className: "border-gray-500 text-gray-700 dark:text-gray-300", - }; - } - }; - - // Load version metadata - const loadVersionMetadata = useCallback( - async (versionId: string) => { - if (!versionId || !activeInstanceId) { - setSelectedVersionMetadata(null); - return; - } - - setIsLoadingMetadata(true); - try { - const metadata = await invoke<{ - id: string; - javaVersion?: number; - isInstalled: boolean; - }>("get_version_metadata", { - instanceId: activeInstanceId, - versionId, - }); - setSelectedVersionMetadata(metadata); - } catch (e) { - console.error("Failed to load version metadata:", e); - setSelectedVersionMetadata(null); - } finally { - setIsLoadingMetadata(false); - } - }, - [activeInstanceId], - ); - - // Get base version for mod loader selector - const selectedBaseVersion = (() => { - if (!selectedVersion) return ""; - - if (selectedVersion.startsWith("fabric-loader-")) { - const parts = selectedVersion.split("-"); - return parts[parts.length - 1]; - } - if (selectedVersion.includes("-forge-")) { - return selectedVersion.split("-forge-")[0]; - } - - const version = versions.find((v) => v.id === selectedVersion); - return version ? selectedVersion : ""; - })(); - - // Handle version deletion - const handleDeleteVersion = async () => { - if (!versionToDelete || !activeInstanceId) return; - - setIsDeleting(true); - try { - await invoke("delete_version", { - instanceId: activeInstanceId, - versionId: versionToDelete, - }); - - if (selectedVersion === versionToDelete) { - setSelectedVersion(""); - } - - setShowDeleteDialog(false); - setVersionToDelete(null); - toast.success("Version deleted successfully"); - - await loadVersions(activeInstanceId); - await loadInstalledModdedVersions(); - } catch (e) { - console.error("Failed to delete version:", e); - toast.error(`Failed to delete version: ${e}`); - } finally { - setIsDeleting(false); - } - }; - - // Show delete confirmation dialog - const showDeleteConfirmation = (versionId: string, e: React.MouseEvent) => { - e.stopPropagation(); - setVersionToDelete(versionId); - setShowDeleteDialog(true); - }; - - // Setup event listeners for version updates - useEffect(() => { - let unlisteners: UnlistenFn[] = []; - - const setupEventListeners = async () => { - try { - const versionDeletedUnlisten = await listen( - "version-deleted", - async () => { - await loadVersions(activeInstanceId ?? undefined); - await loadInstalledModdedVersions(); - }, - ); - - const downloadCompleteUnlisten = await listen( - "download-complete", - async () => { - await loadVersions(activeInstanceId ?? undefined); - await loadInstalledModdedVersions(); - }, - ); - - const versionInstalledUnlisten = await listen( - "version-installed", - async () => { - await loadVersions(activeInstanceId ?? undefined); - await loadInstalledModdedVersions(); - }, - ); - - const fabricInstalledUnlisten = await listen( - "fabric-installed", - async () => { - await loadVersions(activeInstanceId ?? undefined); - await loadInstalledModdedVersions(); - }, - ); - - const forgeInstalledUnlisten = await listen( - "forge-installed", - async () => { - await loadVersions(activeInstanceId ?? undefined); - await loadInstalledModdedVersions(); - }, - ); - - unlisteners = [ - versionDeletedUnlisten, - downloadCompleteUnlisten, - versionInstalledUnlisten, - fabricInstalledUnlisten, - forgeInstalledUnlisten, - ]; - } catch (e) { - console.error("Failed to setup event listeners:", e); - } - }; - - setupEventListeners(); - loadInstalledModdedVersions(); - - return () => { - unlisteners.forEach((unlisten) => { - unlisten(); - }); - }; - }, [activeInstanceId, loadVersions, loadInstalledModdedVersions]); - - // Load metadata when selected version changes - useEffect(() => { - if (selectedVersion) { - loadVersionMetadata(selectedVersion); - } else { - setSelectedVersionMetadata(null); - } - }, [selectedVersion, loadVersionMetadata]); - - return ( - <div className="h-full flex flex-col p-6 overflow-hidden"> - <div className="flex items-center justify-between mb-6"> - <h2 className="text-3xl font-black bg-clip-text text-transparent bg-linear-to-r from-gray-900 to-gray-600 dark:from-white dark:to-white/60"> - Version Manager - </h2> - <div className="text-sm dark:text-white/40 text-black/50"> - Select a version to play or modify - </div> - </div> - - <div className="flex-1 grid grid-cols-1 lg:grid-cols-3 gap-6 overflow-hidden"> - {/* Left: Version List */} - <div className="lg:col-span-2 flex flex-col gap-4 overflow-hidden"> - {/* Search and Filters */} - <div className="flex gap-3"> - <div className="relative flex-1"> - <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> - <Input - type="text" - placeholder="Search versions..." - className="pl-9 bg-white/60 dark:bg-black/20 border-black/10 dark:border-white/10 dark:text-white text-gray-900 placeholder-black/30 dark:placeholder-white/30 focus:border-indigo-500/50 dark:focus:bg-black/40 focus:bg-white/80 backdrop-blur-sm" - value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} - /> - </div> - </div> - - {/* Type Filter Tabs */} - <Tabs - value={typeFilter} - onValueChange={(v) => setTypeFilter(v as TypeFilter)} - className="w-full" - > - <TabsList className="grid grid-cols-4 bg-white/60 dark:bg-black/20 border-black/5 dark:border-white/5"> - <TabsTrigger value="all">All</TabsTrigger> - <TabsTrigger value="release">Release</TabsTrigger> - <TabsTrigger value="snapshot">Snapshot</TabsTrigger> - <TabsTrigger value="installed">Installed</TabsTrigger> - </TabsList> - </Tabs> - - {/* Version List */} - <ScrollArea className="flex-1 pr-2"> - {versions.length === 0 ? ( - <div className="flex items-center justify-center h-40 dark:text-white/30 text-black/30 italic animate-pulse"> - Loading versions... - </div> - ) : filteredVersions.length === 0 ? ( - <div className="flex flex-col items-center justify-center h-40 dark:text-white/30 text-black/30 gap-2"> - <span className="text-2xl">👻</span> - <span>No matching versions found</span> - </div> - ) : ( - <div className="space-y-2"> - {filteredVersions.map((version) => { - const badge = getVersionBadge(version.type); - const isSelected = selectedVersion === version.id; - - return ( - <Card - key={version.id} - className={`w-full cursor-pointer transition-all duration-200 relative overflow-hidden group ${ - isSelected - ? "border-indigo-200 dark:border-indigo-500/50 bg-indigo-50 dark:bg-indigo-600/20 shadow-[0_0_20px_rgba(99,102,241,0.2)]" - : "border-black/5 dark:border-white/5 bg-white/40 dark:bg-white/5 hover:bg-white/60 dark:hover:bg-white/10 hover:border-black/10 dark:hover:border-white/10 hover:translate-x-1" - }`} - onClick={() => setSelectedVersion(version.id)} - > - {isSelected && ( - <div className="absolute inset-0 bg-linear-to-r from-indigo-500/10 to-transparent pointer-events-none" /> - )} - - <CardContent className="p-4"> - <div className="flex items-center justify-between"> - <div className="flex items-center gap-4 flex-1"> - <Badge - variant={badge.variant} - className={badge.className} - > - {badge.text} - </Badge> - <div className="flex-1"> - <div - className={`font-bold font-mono text-lg tracking-tight ${ - isSelected - ? "text-black dark:text-white" - : "text-gray-700 dark:text-zinc-300 group-hover:text-black dark:group-hover:text-white" - }`} - > - {version.id} - </div> - <div className="flex items-center gap-2 mt-0.5"> - {version.releaseTime && - version.type !== "fabric" && - version.type !== "forge" && ( - <div className="text-xs dark:text-white/30 text-black/30"> - {new Date( - version.releaseTime, - ).toLocaleDateString()} - </div> - )} - {version.javaVersion && ( - <div className="flex items-center gap-1 text-xs dark:text-white/40 text-black/40"> - <Coffee className="h-3 w-3 opacity-60" /> - <span className="font-medium"> - Java {version.javaVersion} - </span> - </div> - )} - </div> - </div> - </div> - - <div className="flex items-center gap-2"> - {version.isInstalled && ( - <Button - variant="ghost" - size="icon" - className="opacity-0 group-hover:opacity-100 transition-opacity text-red-500 dark:text-red-400 hover:bg-red-500/10 dark:hover:bg-red-500/20" - onClick={(e) => - showDeleteConfirmation(version.id, e) - } - title="Delete version" - > - <Trash2 className="h-4 w-4" /> - </Button> - )} - </div> - </div> - </CardContent> - </Card> - ); - })} - </div> - )} - </ScrollArea> - </div> - - {/* Right: Version Details */} - <div className="flex flex-col gap-6"> - <Card className="border-black/5 dark:border-white/5 bg-white/40 dark:bg-white/5 backdrop-blur-sm"> - <CardHeader> - <CardTitle className="text-lg">Version Details</CardTitle> - </CardHeader> - <CardContent className="space-y-4"> - {selectedVersion ? ( - <> - <div> - <div className="text-sm text-muted-foreground mb-1"> - Selected Version - </div> - <div className="font-mono text-xl font-bold"> - {selectedVersion} - </div> - </div> - - {isLoadingMetadata ? ( - <div className="flex items-center gap-2"> - <Loader2 className="h-4 w-4 animate-spin" /> - <span className="text-sm">Loading metadata...</span> - </div> - ) : selectedVersionMetadata ? ( - <div className="space-y-3"> - <div> - <div className="text-sm text-muted-foreground mb-1"> - Installation Status - </div> - <Badge - variant={ - selectedVersionMetadata.isInstalled - ? "default" - : "outline" - } - > - {selectedVersionMetadata.isInstalled - ? "Installed" - : "Not Installed"} - </Badge> - </div> - - {selectedVersionMetadata.javaVersion && ( - <div> - <div className="text-sm text-muted-foreground mb-1"> - Java Version - </div> - <div className="flex items-center gap-2"> - <Coffee className="h-4 w-4" /> - <span> - Java {selectedVersionMetadata.javaVersion} - </span> - </div> - </div> - )} - - {!selectedVersionMetadata.isInstalled && ( - <Button - className="w-full" - onClick={() => setShowModLoaderSelector(true)} - > - Install with Mod Loader - </Button> - )} - </div> - ) : null} - </> - ) : ( - <div className="text-center py-8 text-muted-foreground"> - Select a version to view details - </div> - )} - </CardContent> - </Card> - - {/* Mod Loader Installation */} - {showModLoaderSelector && selectedBaseVersion && ( - <Card className="border-black/5 dark:border-white/5 bg-white/40 dark:bg-white/5 backdrop-blur-sm"> - <CardHeader> - <CardTitle className="text-lg">Install Mod Loader</CardTitle> - </CardHeader> - <CardContent className="space-y-4"> - <div className="text-sm text-muted-foreground"> - Install {selectedBaseVersion} with Fabric or Forge - </div> - <div className="flex gap-2"> - <Button - variant="outline" - className="flex-1" - onClick={async () => { - if (!activeInstanceId) return; - try { - await invoke("install_fabric", { - instanceId: activeInstanceId, - gameVersion: selectedBaseVersion, - loaderVersion: "latest", - }); - toast.success("Fabric installation started"); - setShowModLoaderSelector(false); - } catch (e) { - console.error("Failed to install Fabric:", e); - toast.error(`Failed to install Fabric: ${e}`); - } - }} - > - Install Fabric - </Button> - <Button - variant="outline" - className="flex-1" - onClick={async () => { - if (!activeInstanceId) return; - try { - await invoke("install_forge", { - instanceId: activeInstanceId, - gameVersion: selectedBaseVersion, - installerVersion: "latest", - }); - toast.success("Forge installation started"); - setShowModLoaderSelector(false); - } catch (e) { - console.error("Failed to install Forge:", e); - toast.error(`Failed to install Forge: ${e}`); - } - }} - > - Install Forge - </Button> - </div> - <Button - variant="ghost" - size="sm" - onClick={() => setShowModLoaderSelector(false)} - > - Cancel - </Button> - </CardContent> - </Card> - )} - </div> - </div> - - {/* Delete Confirmation Dialog */} - <Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}> - <DialogContent> - <DialogHeader> - <DialogTitle>Delete Version</DialogTitle> - <DialogDescription> - Are you sure you want to delete version "{versionToDelete}"? This - action cannot be undone. - </DialogDescription> - </DialogHeader> - <DialogFooter> - <Button - variant="outline" - onClick={() => { - setShowDeleteDialog(false); - setVersionToDelete(null); - }} - disabled={isDeleting} - > - Cancel - </Button> - <Button - variant="destructive" - onClick={handleDeleteVersion} - disabled={isDeleting} - > - {isDeleting ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - Deleting... - </> - ) : ( - "Delete" - )} - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - </div> - ); -} |