diff options
Diffstat (limited to 'packages/ui-new/src/pages')
| -rw-r--r-- | packages/ui-new/src/pages/home-view.tsx | 212 | ||||
| -rw-r--r-- | packages/ui-new/src/pages/index-old.tsx | 187 | ||||
| -rw-r--r-- | packages/ui-new/src/pages/index.tsx | 173 | ||||
| -rw-r--r-- | packages/ui-new/src/pages/settings.tsx | 310 |
4 files changed, 529 insertions, 353 deletions
diff --git a/packages/ui-new/src/pages/home-view.tsx b/packages/ui-new/src/pages/home-view.tsx index bcee7e6..4f80cb0 100644 --- a/packages/ui-new/src/pages/home-view.tsx +++ b/packages/ui-new/src/pages/home-view.tsx @@ -1,5 +1,5 @@ -import { Calendar, ExternalLink } from "lucide-react"; 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"; @@ -108,125 +108,6 @@ export function HomeView() { } }; - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleDateString(undefined, { - year: "numeric", - month: "long", - day: "numeric", - }); - }; - - const escapeHtml = (unsafe: string) => { - return unsafe - .replace(/&/g, "&") - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - }; - - const formatBody = (body: string) => { - if (!body) return ""; - - let processed = escapeHtml(body); - - const emojiMap: Record<string, string> = { - ":tada:": "🎉", - ":sparkles:": "✨", - ":bug:": "🐛", - ":memo:": "📝", - ":rocket:": "🚀", - ":white_check_mark:": "✅", - ":construction:": "🚧", - ":recycle:": "♻️", - ":wrench:": "🔧", - ":package:": "📦", - ":arrow_up:": "⬆️", - ":arrow_down:": "⬇️", - ":warning:": "⚠️", - ":fire:": "🔥", - ":heart:": "❤️", - ":star:": "⭐", - ":zap:": "⚡", - ":art:": "🎨", - ":lipstick:": "💄", - ":globe_with_meridians:": "🌐", - }; - - processed = processed.replace( - /:[a-z0-9_]+:/g, - (match) => emojiMap[match] || match, - ); - - processed = processed.replace(/`([0-9a-f]{7,40})`/g, (_match, hash) => { - return `<a href="https://github.com/HydroRoll-Team/DropOut/commit/${hash}" target="_blank" class="text-emerald-500 hover:underline font-mono bg-emerald-500/10 px-1 rounded text-[10px] py-0.5 transition-colors border border-emerald-500/20 hover:border-emerald-500/50">${hash.substring( - 0, - 7, - )}</a>`; - }); - - processed = processed.replace( - /@([a-zA-Z0-9-]+)/g, - '<a href="https://github.com/$1" target="_blank" class="text-zinc-300 hover:text-white hover:underline font-medium">@$1</a>', - ); - - return processed - .split("\n") - .map((line) => { - line = line.trim(); - - const formatLine = (text: string) => - text - .replace( - /\*\*(.*?)\*\*/g, - '<strong class="text-zinc-200">$1</strong>', - ) - .replace( - /(?<!\*)\*([^*]+)\*(?!\*)/g, - '<em class="text-zinc-400 italic">$1</em>', - ) - .replace( - /`([^`]+)`/g, - '<code class="bg-zinc-800 px-1 py-0.5 rounded text-xs text-zinc-300 font-mono border border-white/5 break-all whitespace-normal">$1</code>', - ) - .replace( - /\[(.*?)\]\((.*?)\)/g, - '<a href="$2" target="_blank" class="text-indigo-400 hover:text-indigo-300 hover:underline decoration-indigo-400/30 break-all">$1</a>', - ); - - if (line.startsWith("- ") || line.startsWith("* ")) { - return `<li class="ml-4 list-disc marker:text-zinc-600 mb-1 pl-1 text-zinc-400">${formatLine( - line.substring(2), - )}</li>`; - } - - if (line.startsWith("##")) { - return `<h3 class="text-sm font-bold mt-6 mb-3 text-zinc-100 flex items-center gap-2 border-b border-white/5 pb-2 uppercase tracking-wide">${line.replace( - /^#+\s+/, - "", - )}</h3>`; - } - - if (line.startsWith("#")) { - return `<h3 class="text-base font-bold mt-6 mb-3 text-white">${line.replace( - /^#+\s+/, - "", - )}</h3>`; - } - - if (line.startsWith("> ")) { - return `<blockquote class="border-l-2 border-zinc-700 pl-4 py-1 my-2 text-zinc-500 italic bg-white/5 rounded-r-sm">${formatLine( - line.substring(2), - )}</blockquote>`; - } - - if (line === "") return '<div class="h-2"></div>'; - - return `<p class="mb-1.5 leading-relaxed">${formatLine(line)}</p>`; - }) - .join(""); - }; - return ( <div className="relative z-10 h-full overflow-y-auto custom-scrollbar scroll-smooth" @@ -286,96 +167,7 @@ export function HomeView() { </div> </div> - {/* Scroll Hint */} - {!releasesStore.isLoading && releasesStore.releases.length > 0 && ( - <div className="absolute bottom-10 left-12 animate-bounce text-zinc-600 flex flex-col items-center gap-2 w-fit opacity-50 hover:opacity-100 transition-opacity"> - <span className="text-[10px] font-mono uppercase tracking-widest"> - Scroll for Updates - </span> - <svg - width="20" - height="20" - viewBox="0 0 24 24" - fill="none" - stroke="currentColor" - strokeWidth="2" - strokeLinecap="round" - strokeLinejoin="round" - > - <title>Scroll for Updates</title> - <path d="M7 13l5 5 5-5M7 6l5 5 5-5" /> - </svg> - </div> - )} - </div> - - {/* Changelog / Updates Section */} - <div className="bg-[#09090b] relative z-20 px-12 pb-24 pt-12 border-t border-white/5 min-h-[50vh]"> - <div className="max-w-4xl"> - <h2 className="text-2xl font-bold text-white mb-10 flex items-center gap-3"> - <span className="w-1.5 h-8 bg-emerald-500 rounded-sm"></span> - LATEST UPDATES - </h2> - - {releasesStore.isLoading ? ( - <div className="flex flex-col gap-8"> - {Array(3) - .fill(0) - .map((_, i) => ( - <div - key={`release_skeleton_${i.toString()}`} - className="h-48 bg-white/5 rounded-sm animate-pulse border border-white/5" - ></div> - ))} - </div> - ) : releasesStore.error ? ( - <div className="p-6 border border-red-500/20 bg-red-500/10 text-red-400 rounded-sm"> - Failed to load updates: {releasesStore.error} - </div> - ) : releasesStore.releases.length === 0 ? ( - <div className="text-zinc-500 italic">No releases found.</div> - ) : ( - <div className="space-y-12"> - {releasesStore.releases.map((release, index) => ( - <div - key={`${release.name}_${index.toString()}`} - className="group relative pl-8 border-l border-white/10 pb-4 last:pb-0 last:border-l-0" - > - {/* Timeline Dot */} - <div className="absolute -left-1.25 top-1.5 w-2.5 h-2.5 rounded-full bg-zinc-800 border border-zinc-600 group-hover:bg-emerald-500 group-hover:border-emerald-400 transition-colors"></div> - - <div className="flex items-baseline gap-4 mb-3"> - <h3 className="text-xl font-bold text-white group-hover:text-emerald-400 transition-colors"> - {release.name || release.tagName} - </h3> - <div className="text-xs font-mono text-zinc-500 flex items-center gap-2"> - <Calendar size={12} /> - {formatDate(release.publishedAt)} - </div> - </div> - - <div className="bg-zinc-900/50 border border-white/5 hover:border-white/10 rounded-sm p-6 text-zinc-400 text-sm leading-relaxed transition-colors overflow-hidden"> - <div - className="prose prose-invert prose-sm max-w-none prose-p:text-zinc-400 prose-headings:text-zinc-200 prose-ul:my-2 prose-li:my-0 break-words whitespace-normal" - dangerouslySetInnerHTML={{ - __html: formatBody(release.body), - }} - /> - </div> - - <a - href={release.htmlUrl} - target="_blank" - rel="noopener noreferrer" - className="inline-flex items-center gap-2 mt-3 text-[10px] font-bold uppercase tracking-wider text-zinc-600 hover:text-white transition-colors" - > - View full changelog on GitHub <ExternalLink size={10} /> - </a> - </div> - ))} - </div> - )} - </div> + <BottomBar /> </div> </div> ); diff --git a/packages/ui-new/src/pages/index-old.tsx b/packages/ui-new/src/pages/index-old.tsx new file mode 100644 index 0000000..a6626c9 --- /dev/null +++ b/packages/ui-new/src/pages/index-old.tsx @@ -0,0 +1,187 @@ +import { useEffect } from "react"; +import { Outlet } from "react-router"; +import { BottomBar } from "@/components/bottom-bar"; +import { DownloadMonitor } from "@/components/download-monitor"; +import { GameConsole } from "@/components/game-console"; +import { LoginModal } from "@/components/login-modal"; +import { ParticleBackground } from "@/components/particle-background"; +import { Sidebar } from "@/components/sidebar"; + +import { useAuthStore } from "@/stores/auth-store"; +import { useGameStore } from "@/stores/game-store"; +import { useInstancesStore } from "@/stores/instances-store"; +import { useLogsStore } from "@/stores/logs-store"; +import { useSettingsStore } from "@/stores/settings-store"; +import { useUIStore } from "@/stores/ui-store"; + +export function IndexPage() { + const authStore = useAuthStore(); + const settingsStore = useSettingsStore(); + const uiStore = useUIStore(); + const instancesStore = useInstancesStore(); + const gameStore = useGameStore(); + const logsStore = useLogsStore(); + useEffect(() => { + // ENFORCE DARK MODE: Always add 'dark' class and attribute + document.documentElement.classList.add("dark"); + document.documentElement.setAttribute("data-theme", "dark"); + document.documentElement.classList.remove("light"); + + // Initialize stores + // Include store functions in the dependency array to satisfy hooks lint. + // These functions are stable in our store implementation, so listing them + // here is safe and prevents lint warnings. + authStore.checkAccount(); + settingsStore.loadSettings(); + logsStore.init(); + settingsStore.detectJava(); + instancesStore.loadInstances(); + gameStore.loadVersions(); + + // Note: getVersion() would need Tauri API setup + // getVersion().then((v) => uiStore.setAppVersion(v)); + }, [ + authStore.checkAccount, + settingsStore.loadSettings, + logsStore.init, + settingsStore.detectJava, + instancesStore.loadInstances, + gameStore.loadVersions, + ]); + + // Refresh versions when active instance changes + useEffect(() => { + if (instancesStore.activeInstanceId) { + gameStore.loadVersions(); + } else { + gameStore.setVersions([]); + } + }, [ + instancesStore.activeInstanceId, + gameStore.loadVersions, + gameStore.setVersions, + ]); + + return ( + <div className="relative h-screen w-screen overflow-hidden dark:text-white text-gray-900 font-sans selection:bg-indigo-500/30"> + {/* Modern Animated Background */} + <div className="absolute inset-0 z-0 bg-gray-100 dark:bg-[#09090b] overflow-hidden"> + {settingsStore.settings.customBackgroundPath && ( + <img + src={settingsStore.settings.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 */} + {settingsStore.settings.customBackgroundPath && ( + <div className="absolute inset-0 bg-black/50"></div> + )} + + {!settingsStore.settings.customBackgroundPath && ( + <> + {settingsStore.settings.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> + )} + + {uiStore.currentView === "home" && <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.settings.theme === "dark" ? "#ffffff" : "#000000" + } 1px, transparent 1px), linear-gradient(90deg, ${ + settingsStore.settings.theme === "dark" ? "#ffffff" : "#000000" + } 1px, transparent 1px)`, + backgroundSize: "40px 40px", + maskImage: + "radial-gradient(circle at 50% 50%, black 30%, transparent 70%)", + }} + ></div> + </div> + + {/* Content Wrapper */} + <div className="relative z-10 flex h-full p-4 gap-4 text-gray-900 dark:text-white"> + {/* Floating Sidebar */} + <Sidebar /> + + {/* Main Content Area - Transparent & Flat */} + <main className="flex-1 flex flex-col relative min-w-0 overflow-hidden transition-all duration-300"> + {/* Window Drag Region */} + <div + className="h-8 w-full absolute top-0 left-0 z-50 drag-region" + data-tauri-drag-region + ></div> + + {/* App Content */} + <div className="flex-1 relative overflow-hidden flex flex-col"> + {/* Views Container */} + <div className="flex-1 relative overflow-hidden"> + <Outlet /> + </div> + + {/* Download Monitor Overlay */} + <div className="absolute bottom-20 left-4 right-4 pointer-events-none z-20"> + <div className="pointer-events-auto"> + <DownloadMonitor /> + </div> + </div> + + {/* Bottom Bar */} + {uiStore.currentView === "home" && <BottomBar />} + </div> + </main> + </div> + + {/* Logout Confirmation Dialog */} + {authStore.isLogoutConfirmOpen && ( + <div className="fixed inset-0 z-200 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4"> + <div className="bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl p-6 max-w-sm w-full animate-in fade-in zoom-in-95 duration-200"> + <h3 className="text-lg font-bold text-white mb-2">Logout</h3> + <p className="text-zinc-400 text-sm mb-6"> + Are you sure you want to logout{" "} + <span className="text-white font-medium"> + {authStore.currentAccount?.username} + </span> + ? + </p> + <div className="flex gap-3 justify-end"> + <button + type="button" + onClick={() => authStore.cancelLogout()} + className="px-4 py-2 text-sm font-medium text-zinc-300 hover:text-white bg-zinc-800 hover:bg-zinc-700 rounded-lg transition-colors" + > + Cancel + </button> + <button + type="button" + onClick={() => authStore.confirmLogout()} + className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-500 rounded-lg transition-colors" + > + Logout + </button> + </div> + </div> + </div> + )} + + {uiStore.showConsole && ( + <div className="fixed inset-0 z-100 bg-black/80 backdrop-blur-sm flex items-center justify-center p-8"> + <div className="w-full h-full max-w-6xl max-h-[85vh] bg-[#1e1e1e] rounded-lg overflow-hidden border border-zinc-700 shadow-2xl relative flex flex-col"> + <GameConsole /> + </div> + </div> + )} + </div> + ); +} diff --git a/packages/ui-new/src/pages/index.tsx b/packages/ui-new/src/pages/index.tsx index 2b3c2b2..54cfc1e 100644 --- a/packages/ui-new/src/pages/index.tsx +++ b/packages/ui-new/src/pages/index.tsx @@ -1,94 +1,48 @@ import { useEffect } from "react"; -import { Outlet } from "react-router"; -import { BottomBar } from "@/components/bottom-bar"; -import { DownloadMonitor } from "@/components/download-monitor"; -import { GameConsole } from "@/components/game-console"; -import { LoginModal } from "@/components/login-modal"; +import { Outlet, useLocation } from "react-router"; import { ParticleBackground } from "@/components/particle-background"; import { Sidebar } from "@/components/sidebar"; - -import { useAuthStore } from "@/stores/auth-store"; -import { useGameStore } from "@/stores/game-store"; -import { useInstancesStore } from "@/stores/instances-store"; -import { useLogsStore } from "@/stores/logs-store"; -import { useSettingsStore } from "@/stores/settings-store"; -import { useUIStore } from "@/stores/ui-store"; +import { useAuthStore } from "@/models/auth"; +import { useSettingsStore } from "@/models/settings"; export function IndexPage() { const authStore = useAuthStore(); const settingsStore = useSettingsStore(); - const uiStore = useUIStore(); - const instancesStore = useInstancesStore(); - const gameStore = useGameStore(); - const logsStore = useLogsStore(); - useEffect(() => { - // ENFORCE DARK MODE: Always add 'dark' class and attribute - document.documentElement.classList.add("dark"); - document.documentElement.setAttribute("data-theme", "dark"); - document.documentElement.classList.remove("light"); - // Initialize stores - // Include store functions in the dependency array to satisfy hooks lint. - // These functions are stable in our store implementation, so listing them - // here is safe and prevents lint warnings. - authStore.checkAccount(); - settingsStore.loadSettings(); - logsStore.init(); - settingsStore.detectJava(); - instancesStore.loadInstances(); - gameStore.loadVersions(); + const location = useLocation(); - // Note: getVersion() would need Tauri API setup - // getVersion().then((v) => uiStore.setAppVersion(v)); - }, [ - authStore.checkAccount, - settingsStore.loadSettings, - logsStore.init, - settingsStore.detectJava, - instancesStore.loadInstances, - gameStore.loadVersions, - ]); - - // Refresh versions when active instance changes useEffect(() => { - if (instancesStore.activeInstanceId) { - gameStore.loadVersions(); - } else { - gameStore.setVersions([]); - } - }, [ - instancesStore.activeInstanceId, - gameStore.loadVersions, - gameStore.setVersions, - ]); + authStore.init(); + settingsStore.refresh(); + }, [authStore.init, settingsStore.refresh]); return ( - <div className="relative h-screen w-screen overflow-hidden dark:text-white text-gray-900 font-sans selection:bg-indigo-500/30"> - {/* Modern Animated Background */} + <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.settings.customBackgroundPath && ( - <img - src={settingsStore.settings.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 */} - {settingsStore.settings.customBackgroundPath && ( - <div className="absolute inset-0 bg-black/50"></div> + {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.settings.customBackgroundPath && ( + {!settingsStore.config?.customBackgroundPath && ( <> - {settingsStore.settings.theme === "dark" ? ( + {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> )} - {uiStore.currentView === "home" && <ParticleBackground />} + {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> </> @@ -99,91 +53,24 @@ export function IndexPage() { className="absolute inset-0 z-0 dark:opacity-10 opacity-30 pointer-events-none" style={{ backgroundImage: `linear-gradient(${ - settingsStore.settings.theme === "dark" ? "#ffffff" : "#000000" + settingsStore.config?.theme === "dark" ? "#ffffff" : "#000000" } 1px, transparent 1px), linear-gradient(90deg, ${ - settingsStore.settings.theme === "dark" ? "#ffffff" : "#000000" + 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> - {/* Content Wrapper */} - <div className="relative z-10 flex h-full p-4 gap-4 text-gray-900 dark:text-white"> - {/* Floating Sidebar */} + <div className="size-full flex flex-row p-4 space-x-4 z-20 relative"> <Sidebar /> - {/* Main Content Area - Transparent & Flat */} - <main className="flex-1 flex flex-col relative min-w-0 overflow-hidden transition-all duration-300"> - {/* Window Drag Region */} - <div - className="h-8 w-full absolute top-0 left-0 z-50 drag-region" - data-tauri-drag-region - ></div> - - {/* App Content */} - <div className="flex-1 relative overflow-hidden flex flex-col"> - {/* Views Container */} - <div className="flex-1 relative overflow-hidden"> - <Outlet /> - </div> - - {/* Download Monitor Overlay */} - <div className="absolute bottom-20 left-4 right-4 pointer-events-none z-20"> - <div className="pointer-events-auto"> - <DownloadMonitor /> - </div> - </div> - - {/* Bottom Bar */} - {uiStore.currentView === "home" && <BottomBar />} - </div> + <main className="size-full overflow-hidden"> + <Outlet /> </main> </div> - - <LoginModal /> - - {/* Logout Confirmation Dialog */} - {authStore.isLogoutConfirmOpen && ( - <div className="fixed inset-0 z-200 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4"> - <div className="bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl p-6 max-w-sm w-full animate-in fade-in zoom-in-95 duration-200"> - <h3 className="text-lg font-bold text-white mb-2">Logout</h3> - <p className="text-zinc-400 text-sm mb-6"> - Are you sure you want to logout{" "} - <span className="text-white font-medium"> - {authStore.currentAccount?.username} - </span> - ? - </p> - <div className="flex gap-3 justify-end"> - <button - type="button" - onClick={() => authStore.cancelLogout()} - className="px-4 py-2 text-sm font-medium text-zinc-300 hover:text-white bg-zinc-800 hover:bg-zinc-700 rounded-lg transition-colors" - > - Cancel - </button> - <button - type="button" - onClick={() => authStore.confirmLogout()} - className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-500 rounded-lg transition-colors" - > - Logout - </button> - </div> - </div> - </div> - )} - - {uiStore.showConsole && ( - <div className="fixed inset-0 z-100 bg-black/80 backdrop-blur-sm flex items-center justify-center p-8"> - <div className="w-full h-full max-w-6xl max-h-[85vh] bg-[#1e1e1e] rounded-lg overflow-hidden border border-zinc-700 shadow-2xl relative flex flex-col"> - <GameConsole /> - </div> - </div> - )} </div> ); } diff --git a/packages/ui-new/src/pages/settings.tsx b/packages/ui-new/src/pages/settings.tsx new file mode 100644 index 0000000..440a5dc --- /dev/null +++ b/packages/ui-new/src/pages/settings.tsx @@ -0,0 +1,310 @@ +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> + ); +} |