aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/ui-new/src/pages
diff options
context:
space:
mode:
author苏向夜 <fu050409@163.com>2026-02-25 01:32:51 +0800
committer苏向夜 <fu050409@163.com>2026-02-25 01:32:51 +0800
commit66668d85d603c5841d755a6023aa1925559fc6d4 (patch)
tree485464148c76b0021efb55b7d2afd1c3004ceee0 /packages/ui-new/src/pages
parenta6773bd092db654360c599ca6b0108ea0e456e8c (diff)
downloadDropOut-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.bk485
-rw-r--r--packages/ui-new/src/pages/home-view.tsx174
-rw-r--r--packages/ui-new/src/pages/index.tsx76
-rw-r--r--packages/ui-new/src/pages/instances-view.tsx315
-rw-r--r--packages/ui-new/src/pages/settings-view.tsx.bk1158
-rw-r--r--packages/ui-new/src/pages/settings.tsx310
-rw-r--r--packages/ui-new/src/pages/versions-view.tsx.bk662
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 &gt; 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 &gt; 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">
- &gt; 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>
- );
-}