aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/ui-new/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'packages/ui-new/src/pages')
-rw-r--r--packages/ui-new/src/pages/home-view.tsx212
-rw-r--r--packages/ui-new/src/pages/index-old.tsx187
-rw-r--r--packages/ui-new/src/pages/index.tsx173
-rw-r--r--packages/ui-new/src/pages/settings.tsx310
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, "&lt;")
- .replace(/>/g, "&gt;")
- .replace(/"/g, "&quot;")
- .replace(/'/g, "&#039;");
- };
-
- 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>
+ );
+}