aboutsummaryrefslogtreecommitdiffstatshomepage
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
parenta6773bd092db654360c599ca6b0108ea0e456e8c (diff)
downloadDropOut-66668d85d603c5841d755a6023aa1925559fc6d4.tar.gz
DropOut-66668d85d603c5841d755a6023aa1925559fc6d4.zip
chore(workspace): replace legacy codes
-rw-r--r--.cargo/config.toml2
-rw-r--r--.changes/partial-react.md5
-rw-r--r--packages/ui-new/.gitignore24
-rw-r--r--packages/ui-new/index.html13
-rw-r--r--packages/ui-new/package.json55
-rw-r--r--packages/ui-new/src/lib/effects/SaturnEffect.ts299
-rw-r--r--packages/ui-new/src/types/index.ts1
-rw-r--r--packages/ui-new/tsconfig.app.json34
-rw-r--r--packages/ui-new/tsconfig.json13
-rw-r--r--packages/ui-new/tsconfig.node.json26
-rw-r--r--packages/ui-new/vite.config.ts18
-rw-r--r--packages/ui/CHANGELOG.md7
-rw-r--r--packages/ui/README.md47
-rw-r--r--packages/ui/components.json (renamed from packages/ui-new/components.json)0
-rw-r--r--packages/ui/index.html8
-rw-r--r--packages/ui/package.json54
-rw-r--r--packages/ui/pnpm-lock.yaml1363
-rw-r--r--packages/ui/public/icon.svg (renamed from packages/ui-new/public/icon.svg)0
-rw-r--r--packages/ui/public/vite.svg1
-rw-r--r--packages/ui/src/App.svelte217
-rw-r--r--packages/ui/src/app.css167
-rw-r--r--packages/ui/src/assets/svelte.svg1
-rw-r--r--packages/ui/src/client.ts (renamed from packages/ui-new/src/client.ts)0
-rw-r--r--packages/ui/src/components/AssistantView.svelte436
-rw-r--r--packages/ui/src/components/BottomBar.svelte250
-rw-r--r--packages/ui/src/components/ConfigEditorModal.svelte369
-rw-r--r--packages/ui/src/components/CustomSelect.svelte173
-rw-r--r--packages/ui/src/components/HomeView.svelte271
-rw-r--r--packages/ui/src/components/InstanceCreationModal.svelte485
-rw-r--r--packages/ui/src/components/InstanceEditorModal.svelte439
-rw-r--r--packages/ui/src/components/InstancesView.svelte259
-rw-r--r--packages/ui/src/components/LoginModal.svelte126
-rw-r--r--packages/ui/src/components/ModLoaderSelector.svelte455
-rw-r--r--packages/ui/src/components/ParticleBackground.svelte70
-rw-r--r--packages/ui/src/components/SettingsView.svelte1217
-rw-r--r--packages/ui/src/components/Sidebar.svelte91
-rw-r--r--packages/ui/src/components/StatusToast.svelte42
-rw-r--r--packages/ui/src/components/VersionsView.svelte511
-rw-r--r--packages/ui/src/components/bottom-bar.tsx (renamed from packages/ui-new/src/components/bottom-bar.tsx)0
-rw-r--r--packages/ui/src/components/config-editor.tsx (renamed from packages/ui-new/src/components/config-editor.tsx)0
-rw-r--r--packages/ui/src/components/download-monitor.tsx (renamed from packages/ui-new/src/components/download-monitor.tsx)0
-rw-r--r--packages/ui/src/components/game-console.tsx (renamed from packages/ui-new/src/components/game-console.tsx)0
-rw-r--r--packages/ui/src/components/instance-creation-modal.tsx (renamed from packages/ui-new/src/components/instance-creation-modal.tsx)0
-rw-r--r--packages/ui/src/components/instance-editor-modal.tsx (renamed from packages/ui-new/src/components/instance-editor-modal.tsx)0
-rw-r--r--packages/ui/src/components/login-modal.tsx (renamed from packages/ui-new/src/components/login-modal.tsx)0
-rw-r--r--packages/ui/src/components/particle-background.tsx (renamed from packages/ui-new/src/components/particle-background.tsx)0
-rw-r--r--packages/ui/src/components/sidebar.tsx (renamed from packages/ui-new/src/components/sidebar.tsx)0
-rw-r--r--packages/ui/src/components/ui/avatar.tsx (renamed from packages/ui-new/src/components/ui/avatar.tsx)0
-rw-r--r--packages/ui/src/components/ui/badge.tsx (renamed from packages/ui-new/src/components/ui/badge.tsx)0
-rw-r--r--packages/ui/src/components/ui/button.tsx (renamed from packages/ui-new/src/components/ui/button.tsx)0
-rw-r--r--packages/ui/src/components/ui/card.tsx (renamed from packages/ui-new/src/components/ui/card.tsx)0
-rw-r--r--packages/ui/src/components/ui/checkbox.tsx (renamed from packages/ui-new/src/components/ui/checkbox.tsx)0
-rw-r--r--packages/ui/src/components/ui/dialog.tsx (renamed from packages/ui-new/src/components/ui/dialog.tsx)0
-rw-r--r--packages/ui/src/components/ui/dropdown-menu.tsx (renamed from packages/ui-new/src/components/ui/dropdown-menu.tsx)0
-rw-r--r--packages/ui/src/components/ui/field.tsx (renamed from packages/ui-new/src/components/ui/field.tsx)0
-rw-r--r--packages/ui/src/components/ui/input.tsx (renamed from packages/ui-new/src/components/ui/input.tsx)0
-rw-r--r--packages/ui/src/components/ui/label.tsx (renamed from packages/ui-new/src/components/ui/label.tsx)0
-rw-r--r--packages/ui/src/components/ui/scroll-area.tsx (renamed from packages/ui-new/src/components/ui/scroll-area.tsx)0
-rw-r--r--packages/ui/src/components/ui/select.tsx (renamed from packages/ui-new/src/components/ui/select.tsx)0
-rw-r--r--packages/ui/src/components/ui/separator.tsx (renamed from packages/ui-new/src/components/ui/separator.tsx)0
-rw-r--r--packages/ui/src/components/ui/sonner.tsx (renamed from packages/ui-new/src/components/ui/sonner.tsx)0
-rw-r--r--packages/ui/src/components/ui/spinner.tsx (renamed from packages/ui-new/src/components/ui/spinner.tsx)0
-rw-r--r--packages/ui/src/components/ui/switch.tsx (renamed from packages/ui-new/src/components/ui/switch.tsx)0
-rw-r--r--packages/ui/src/components/ui/tabs.tsx (renamed from packages/ui-new/src/components/ui/tabs.tsx)0
-rw-r--r--packages/ui/src/components/ui/textarea.tsx (renamed from packages/ui-new/src/components/ui/textarea.tsx)0
-rw-r--r--packages/ui/src/components/user-avatar.tsx (renamed from packages/ui-new/src/components/user-avatar.tsx)0
-rw-r--r--packages/ui/src/index.css (renamed from packages/ui-new/src/index.css)0
-rw-r--r--packages/ui/src/lib/Counter.svelte10
-rw-r--r--packages/ui/src/lib/DownloadMonitor.svelte201
-rw-r--r--packages/ui/src/lib/GameConsole.svelte304
-rw-r--r--packages/ui/src/lib/effects/ConstellationEffect.ts162
-rw-r--r--packages/ui/src/lib/effects/SaturnEffect.ts303
-rw-r--r--packages/ui/src/lib/modLoaderApi.ts106
-rw-r--r--packages/ui/src/lib/tsrs-utils.ts (renamed from packages/ui-new/src/lib/tsrs-utils.ts)0
-rw-r--r--packages/ui/src/lib/utils.ts (renamed from packages/ui-new/src/lib/utils.ts)0
-rw-r--r--packages/ui/src/main.ts9
-rw-r--r--packages/ui/src/main.tsx (renamed from packages/ui-new/src/main.tsx)0
-rw-r--r--packages/ui/src/models/auth.ts (renamed from packages/ui-new/src/models/auth.ts)0
-rw-r--r--packages/ui/src/models/instances.ts (renamed from packages/ui-new/src/models/instances.ts)0
-rw-r--r--packages/ui/src/models/settings.ts (renamed from packages/ui-new/src/models/settings.ts)0
-rw-r--r--packages/ui/src/pages/assistant-view.tsx.bk (renamed from packages/ui-new/src/pages/assistant-view.tsx.bk)0
-rw-r--r--packages/ui/src/pages/home-view.tsx (renamed from packages/ui-new/src/pages/home-view.tsx)0
-rw-r--r--packages/ui/src/pages/index.tsx (renamed from packages/ui-new/src/pages/index.tsx)0
-rw-r--r--packages/ui/src/pages/instances-view.tsx (renamed from packages/ui-new/src/pages/instances-view.tsx)0
-rw-r--r--packages/ui/src/pages/settings-view.tsx.bk (renamed from packages/ui-new/src/pages/settings-view.tsx.bk)0
-rw-r--r--packages/ui/src/pages/settings.tsx (renamed from packages/ui-new/src/pages/settings.tsx)0
-rw-r--r--packages/ui/src/pages/versions-view.tsx.bk (renamed from packages/ui-new/src/pages/versions-view.tsx.bk)0
-rw-r--r--packages/ui/src/stores/assistant-store.ts (renamed from packages/ui-new/src/stores/assistant-store.ts)0
-rw-r--r--packages/ui/src/stores/assistant.svelte.ts166
-rw-r--r--packages/ui/src/stores/auth-store.ts (renamed from packages/ui-new/src/stores/auth-store.ts)0
-rw-r--r--packages/ui/src/stores/auth.svelte.ts192
-rw-r--r--packages/ui/src/stores/game-store.ts (renamed from packages/ui-new/src/stores/game-store.ts)0
-rw-r--r--packages/ui/src/stores/game.svelte.ts78
-rw-r--r--packages/ui/src/stores/instances.svelte.ts109
-rw-r--r--packages/ui/src/stores/logs-store.ts (renamed from packages/ui-new/src/stores/logs-store.ts)0
-rw-r--r--packages/ui/src/stores/logs.svelte.ts151
-rw-r--r--packages/ui/src/stores/releases-store.ts (renamed from packages/ui-new/src/stores/releases-store.ts)0
-rw-r--r--packages/ui/src/stores/releases.svelte.ts36
-rw-r--r--packages/ui/src/stores/settings-store.ts (renamed from packages/ui-new/src/stores/settings-store.ts)0
-rw-r--r--packages/ui/src/stores/settings.svelte.ts570
-rw-r--r--packages/ui/src/stores/ui-store.ts (renamed from packages/ui-new/src/stores/ui-store.ts)0
-rw-r--r--packages/ui/src/stores/ui.svelte.ts32
-rw-r--r--packages/ui/src/types/bindings/account.ts (renamed from packages/ui-new/src/types/bindings/account.ts)0
-rw-r--r--packages/ui/src/types/bindings/assistant.ts (renamed from packages/ui-new/src/types/bindings/assistant.ts)0
-rw-r--r--packages/ui/src/types/bindings/auth.ts (renamed from packages/ui-new/src/types/bindings/auth.ts)0
-rw-r--r--packages/ui/src/types/bindings/config.ts (renamed from packages/ui-new/src/types/bindings/config.ts)0
-rw-r--r--packages/ui/src/types/bindings/core.ts (renamed from packages/ui-new/src/types/bindings/core.ts)0
-rw-r--r--packages/ui/src/types/bindings/downloader.ts (renamed from packages/ui-new/src/types/bindings/downloader.ts)0
-rw-r--r--packages/ui/src/types/bindings/fabric.ts (renamed from packages/ui-new/src/types/bindings/fabric.ts)0
-rw-r--r--packages/ui/src/types/bindings/forge.ts (renamed from packages/ui-new/src/types/bindings/forge.ts)0
-rw-r--r--packages/ui/src/types/bindings/game-version.ts (renamed from packages/ui-new/src/types/bindings/game-version.ts)0
-rw-r--r--packages/ui/src/types/bindings/index.ts (renamed from packages/ui-new/src/types/bindings/index.ts)0
-rw-r--r--packages/ui/src/types/bindings/instance.ts (renamed from packages/ui-new/src/types/bindings/instance.ts)0
-rw-r--r--packages/ui/src/types/bindings/java/core.ts (renamed from packages/ui-new/src/types/bindings/java/core.ts)0
-rw-r--r--packages/ui/src/types/bindings/java/index.ts (renamed from packages/ui-new/src/types/bindings/java/index.ts)0
-rw-r--r--packages/ui/src/types/bindings/java/persistence.ts (renamed from packages/ui-new/src/types/bindings/java/persistence.ts)0
-rw-r--r--packages/ui/src/types/bindings/java/providers/adoptium.ts (renamed from packages/ui-new/src/types/bindings/java/providers/adoptium.ts)0
-rw-r--r--packages/ui/src/types/bindings/java/providers/index.ts (renamed from packages/ui-new/src/types/bindings/java/providers/index.ts)0
-rw-r--r--packages/ui/src/types/bindings/manifest.ts (renamed from packages/ui-new/src/types/bindings/manifest.ts)0
-rw-r--r--packages/ui/src/types/index.ts233
-rw-r--r--packages/ui/svelte.config.js8
-rw-r--r--packages/ui/tsconfig.app.json36
-rw-r--r--packages/ui/tsconfig.json11
-rw-r--r--packages/ui/vite.config.ts30
-rw-r--r--src-tauri/tauri.conf.json6
125 files changed, 224 insertions, 10078 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml
index 9032a7e..70220fc 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,3 +1,3 @@
[env]
-TS_RS_EXPORT_DIR = { value = "./packages/ui-new/src/types/bindings", relative = true }
+TS_RS_EXPORT_DIR = { value = "./packages/ui/src/types/bindings", relative = true }
TS_RS_LARGE_INT = "number"
diff --git a/.changes/partial-react.md b/.changes/partial-react.md
new file mode 100644
index 0000000..62ca46d
--- /dev/null
+++ b/.changes/partial-react.md
@@ -0,0 +1,5 @@
+---
+"@dropout/ui": "minor:refactor"
+---
+
+Partial rewrite UI to react port.
diff --git a/packages/ui-new/.gitignore b/packages/ui-new/.gitignore
deleted file mode 100644
index a547bf3..0000000
--- a/packages/ui-new/.gitignore
+++ /dev/null
@@ -1,24 +0,0 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-node_modules
-dist
-dist-ssr
-*.local
-
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
diff --git a/packages/ui-new/index.html b/packages/ui-new/index.html
deleted file mode 100644
index 5191e6f..0000000
--- a/packages/ui-new/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<!doctype html>
-<html lang="en">
- <head>
- <meta charset="UTF-8" />
- <link rel="icon" type="image/svg+xml" href="/icon.svg" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>Dropout Launcher</title>
- </head>
- <body>
- <div id="root"></div>
- <script type="module" src="/src/main.tsx"></script>
- </body>
-</html>
diff --git a/packages/ui-new/package.json b/packages/ui-new/package.json
deleted file mode 100644
index b26d733..0000000
--- a/packages/ui-new/package.json
+++ /dev/null
@@ -1,55 +0,0 @@
-{
- "name": "@dropout/ui-new",
- "private": true,
- "version": "0.0.0",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "tsc -b && vite build",
- "lint": "biome check .",
- "preview": "vite preview"
- },
- "dependencies": {
- "@base-ui/react": "^1.2.0",
- "@radix-ui/react-checkbox": "^1.3.3",
- "@radix-ui/react-dialog": "^1.1.15",
- "@radix-ui/react-label": "^2.1.8",
- "@radix-ui/react-scroll-area": "^1.2.10",
- "@radix-ui/react-select": "^2.2.6",
- "@radix-ui/react-separator": "^1.1.8",
- "@radix-ui/react-slot": "^1.2.4",
- "@radix-ui/react-switch": "^1.2.6",
- "@radix-ui/react-tabs": "^1.1.13",
- "@tauri-apps/api": "^2.9.1",
- "@tauri-apps/plugin-dialog": "^2.6.0",
- "@tauri-apps/plugin-fs": "^2.4.5",
- "@tauri-apps/plugin-shell": "^2.3.4",
- "class-variance-authority": "^0.7.1",
- "clsx": "^2.1.1",
- "es-toolkit": "^1.44.0",
- "lucide-react": "^0.562.0",
- "marked": "^17.0.1",
- "next-themes": "^0.4.6",
- "radix-ui": "^1.4.3",
- "react": "^19.2.0",
- "react-dom": "^19.2.0",
- "react-router": "^7.12.0",
- "sonner": "^2.0.7",
- "tailwind-merge": "^3.4.1",
- "zod": "^4.3.6",
- "zustand": "^5.0.10"
- },
- "devDependencies": {
- "@tailwindcss/vite": "^4.1.18",
- "@types/node": "^24.10.1",
- "@types/react": "^19.2.5",
- "@types/react-dom": "^19.2.3",
- "@vitejs/plugin-react": "^5.1.1",
- "globals": "^16.5.0",
- "shadcn": "^3.8.5",
- "tailwindcss": "^4.1.18",
- "tw-animate-css": "^1.4.0",
- "typescript": "~5.9.3",
- "vite": "npm:rolldown-vite@^7"
- }
-}
diff --git a/packages/ui-new/src/lib/effects/SaturnEffect.ts b/packages/ui-new/src/lib/effects/SaturnEffect.ts
deleted file mode 100644
index 497a340..0000000
--- a/packages/ui-new/src/lib/effects/SaturnEffect.ts
+++ /dev/null
@@ -1,299 +0,0 @@
-/**
- * Ported SaturnEffect for the React UI (ui-new).
- * Adapted from the original Svelte implementation but written as a standalone
- * TypeScript class that manages a 2D canvas particle effect resembling a
- * rotating "Saturn" with rings. Designed to be instantiated and controlled
- * from a React component (e.g. ParticleBackground).
- *
- * Usage:
- * const effect = new SaturnEffect(canvasElement);
- * effect.handleMouseDown(clientX);
- * effect.handleMouseMove(clientX);
- * effect.handleMouseUp();
- * // on resize:
- * effect.resize(width, height);
- * // on unmount:
- * effect.destroy();
- */
-
-export class SaturnEffect {
- private canvas: HTMLCanvasElement;
- private ctx: CanvasRenderingContext2D;
- private width = 0;
- private height = 0;
-
- // Particle storage
- private xyz: Float32Array | null = null; // interleaved x,y,z
- private types: Uint8Array | null = null; // 0 = planet, 1 = ring
- private count = 0;
-
- // Animation
- private animationId = 0;
- private angle = 0;
- private scaleFactor = 1;
-
- // Interaction
- private isDragging = false;
- private lastMouseX = 0;
- private lastMouseTime = 0;
- private mouseVelocities: number[] = [];
-
- // Speed control
- private readonly baseSpeed = 0.005;
- private currentSpeed = 0.005;
- private rotationDirection = 1;
- private readonly speedDecayRate = 0.992;
- private readonly minSpeedMultiplier = 1;
- private readonly maxSpeedMultiplier = 50;
- private isStopped = false;
-
- constructor(canvas: HTMLCanvasElement) {
- this.canvas = canvas;
- const ctx = canvas.getContext("2d", { alpha: true, desynchronized: false });
- if (!ctx) {
- throw new Error("Failed to get 2D context for SaturnEffect");
- }
- this.ctx = ctx;
-
- // Initialize size & particles
- this.resize(window.innerWidth, window.innerHeight);
- this.initParticles();
-
- this.animate = this.animate.bind(this);
- this.animate();
- }
-
- // External interaction handlers (accept clientX)
- handleMouseDown(clientX: number) {
- this.isDragging = true;
- this.lastMouseX = clientX;
- this.lastMouseTime = performance.now();
- this.mouseVelocities = [];
- }
-
- handleMouseMove(clientX: number) {
- if (!this.isDragging) return;
- const now = performance.now();
- const dt = now - this.lastMouseTime;
- if (dt > 0) {
- const dx = clientX - this.lastMouseX;
- const velocity = dx / dt;
- this.mouseVelocities.push(velocity);
- if (this.mouseVelocities.length > 5) this.mouseVelocities.shift();
- // Rotate directly while dragging for immediate feedback
- this.angle += dx * 0.002;
- }
- this.lastMouseX = clientX;
- this.lastMouseTime = now;
- }
-
- handleMouseUp() {
- if (this.isDragging && this.mouseVelocities.length > 0) {
- this.applyFlingVelocity();
- }
- this.isDragging = false;
- }
-
- handleTouchStart(clientX: number) {
- this.handleMouseDown(clientX);
- }
-
- handleTouchMove(clientX: number) {
- this.handleMouseMove(clientX);
- }
-
- handleTouchEnd() {
- this.handleMouseUp();
- }
-
- // Resize canvas & scale (call on window resize)
- resize(width: number, height: number) {
- const dpr = window.devicePixelRatio || 1;
- this.width = width;
- this.height = height;
-
- // Update canvas pixel size and CSS size
- this.canvas.width = Math.max(1, Math.floor(width * dpr));
- this.canvas.height = Math.max(1, Math.floor(height * dpr));
- this.canvas.style.width = `${width}px`;
- this.canvas.style.height = `${height}px`;
-
- // Reset transform and scale for devicePixelRatio
- this.ctx.setTransform(1, 0, 0, 1, 0, 0); // reset
- this.ctx.scale(dpr, dpr);
-
- const minDim = Math.min(width, height);
- this.scaleFactor = Math.max(1, minDim * 0.45);
- }
-
- // Initialize particle arrays with reduced counts to keep performance reasonable
- private initParticles() {
- // Tuned particle counts for reasonable performance across platforms
- const planetCount = 1000;
- const ringCount = 2500;
- this.count = planetCount + ringCount;
-
- this.xyz = new Float32Array(this.count * 3);
- this.types = new Uint8Array(this.count);
-
- let idx = 0;
-
- // Planet points
- for (let i = 0; i < planetCount; i++, idx++) {
- const theta = Math.random() * Math.PI * 2;
- const phi = Math.acos(Math.random() * 2 - 1);
- const r = 1.0;
-
- this.xyz[idx * 3] = r * Math.sin(phi) * Math.cos(theta);
- this.xyz[idx * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
- this.xyz[idx * 3 + 2] = r * Math.cos(phi);
-
- this.types[idx] = 0;
- }
-
- // Ring points
- const ringInner = 1.4;
- const ringOuter = 2.3;
- for (let i = 0; i < ringCount; i++, idx++) {
- const angle = Math.random() * Math.PI * 2;
- const dist = Math.sqrt(
- Math.random() * (ringOuter * ringOuter - ringInner * ringInner) +
- ringInner * ringInner,
- );
-
- this.xyz[idx * 3] = dist * Math.cos(angle);
- this.xyz[idx * 3 + 1] = (Math.random() - 0.5) * 0.05;
- this.xyz[idx * 3 + 2] = dist * Math.sin(angle);
-
- this.types[idx] = 1;
- }
- }
-
- // Map fling/velocity samples to a rotation speed and direction
- private applyFlingVelocity() {
- if (this.mouseVelocities.length === 0) return;
- const avg =
- this.mouseVelocities.reduce((a, b) => a + b, 0) /
- this.mouseVelocities.length;
- const flingThreshold = 0.3;
- const stopThreshold = 0.1;
-
- if (Math.abs(avg) > flingThreshold) {
- this.isStopped = false;
- const newDir = avg > 0 ? 1 : -1;
- if (newDir !== this.rotationDirection) this.rotationDirection = newDir;
- const multiplier = Math.min(
- this.maxSpeedMultiplier,
- this.minSpeedMultiplier + Math.abs(avg) * 10,
- );
- this.currentSpeed = this.baseSpeed * multiplier;
- } else if (Math.abs(avg) < stopThreshold) {
- this.isStopped = true;
- this.currentSpeed = 0;
- }
- }
-
- // Main render loop
- private animate() {
- // Clear with full alpha to allow layering over background
- this.ctx.clearRect(0, 0, this.width, this.height);
-
- // Standard composition
- this.ctx.globalCompositeOperation = "source-over";
-
- // Update rotation speed (decay)
- if (!this.isDragging && !this.isStopped) {
- if (this.currentSpeed > this.baseSpeed) {
- this.currentSpeed =
- this.baseSpeed +
- (this.currentSpeed - this.baseSpeed) * this.speedDecayRate;
- if (this.currentSpeed - this.baseSpeed < 0.00001) {
- this.currentSpeed = this.baseSpeed;
- }
- }
- this.angle += this.currentSpeed * this.rotationDirection;
- }
-
- // Center positions
- const cx = this.width * 0.6;
- const cy = this.height * 0.5;
-
- // Pre-calc rotations
- const rotationY = this.angle;
- const rotationX = 0.4;
- const rotationZ = 0.15;
-
- const sinY = Math.sin(rotationY);
- const cosY = Math.cos(rotationY);
- const sinX = Math.sin(rotationX);
- const cosX = Math.cos(rotationX);
- const sinZ = Math.sin(rotationZ);
- const cosZ = Math.cos(rotationZ);
-
- const fov = 1500;
- const scaleFactor = this.scaleFactor;
-
- if (!this.xyz || !this.types) {
- this.animationId = requestAnimationFrame(this.animate);
- return;
- }
-
- // Loop particles
- for (let i = 0; i < this.count; i++) {
- const x = this.xyz[i * 3];
- const y = this.xyz[i * 3 + 1];
- const z = this.xyz[i * 3 + 2];
-
- // Scale to screen
- const px = x * scaleFactor;
- const py = y * scaleFactor;
- const pz = z * scaleFactor;
-
- // Rotate Y then X then Z
- const x1 = px * cosY - pz * sinY;
- const z1 = pz * cosY + px * sinY;
- const y2 = py * cosX - z1 * sinX;
- const z2 = z1 * cosX + py * sinX;
- const x3 = x1 * cosZ - y2 * sinZ;
- const y3 = y2 * cosZ + x1 * sinZ;
- const z3 = z2;
-
- const scale = fov / (fov + z3);
-
- if (z3 > -fov) {
- const x2d = cx + x3 * scale;
- const y2d = cy + y3 * scale;
-
- const type = this.types[i];
- const sizeBase = type === 0 ? 2.4 : 1.5;
- const size = sizeBase * scale;
-
- let alpha = scale * scale * scale;
- if (alpha > 1) alpha = 1;
- if (alpha < 0.15) continue;
-
- if (type === 0) {
- // Planet: warm-ish
- this.ctx.fillStyle = `rgba(255, 240, 220, ${alpha})`;
- } else {
- // Ring: cool-ish
- this.ctx.fillStyle = `rgba(220, 240, 255, ${alpha})`;
- }
-
- // Render as small rectangles (faster than arc)
- this.ctx.fillRect(x2d, y2d, size, size);
- }
- }
-
- this.animationId = requestAnimationFrame(this.animate);
- }
-
- // Stop animations and release resources
- destroy() {
- if (this.animationId) {
- cancelAnimationFrame(this.animationId);
- this.animationId = 0;
- }
- // Intentionally do not null out arrays to allow reuse if desired.
- }
-}
diff --git a/packages/ui-new/src/types/index.ts b/packages/ui-new/src/types/index.ts
deleted file mode 100644
index 9e592d7..0000000
--- a/packages/ui-new/src/types/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./bindings";
diff --git a/packages/ui-new/tsconfig.app.json b/packages/ui-new/tsconfig.app.json
deleted file mode 100644
index 54f0bdf..0000000
--- a/packages/ui-new/tsconfig.app.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "compilerOptions": {
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
- "target": "ES2022",
- "useDefineForClassFields": true,
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
- "module": "ESNext",
- "types": ["vite/client"],
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "verbatimModuleSyntax": true,
- "moduleDetection": "force",
- "noEmit": true,
- "jsx": "react-jsx",
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "erasableSyntaxOnly": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true,
-
- /* Paths */
- "baseUrl": ".",
- "paths": {
- "@/*": ["./src/*"]
- }
- },
- "include": ["src"]
-}
diff --git a/packages/ui-new/tsconfig.json b/packages/ui-new/tsconfig.json
deleted file mode 100644
index fec8c8e..0000000
--- a/packages/ui-new/tsconfig.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "files": [],
- "references": [
- { "path": "./tsconfig.app.json" },
- { "path": "./tsconfig.node.json" }
- ],
- "compilerOptions": {
- "baseUrl": ".",
- "paths": {
- "@/*": ["./src/*"]
- }
- }
-}
diff --git a/packages/ui-new/tsconfig.node.json b/packages/ui-new/tsconfig.node.json
deleted file mode 100644
index 8a67f62..0000000
--- a/packages/ui-new/tsconfig.node.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "compilerOptions": {
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
- "target": "ES2023",
- "lib": ["ES2023"],
- "module": "ESNext",
- "types": ["node"],
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "verbatimModuleSyntax": true,
- "moduleDetection": "force",
- "noEmit": true,
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "erasableSyntaxOnly": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true
- },
- "include": ["vite.config.ts"]
-}
diff --git a/packages/ui-new/vite.config.ts b/packages/ui-new/vite.config.ts
deleted file mode 100644
index 27ce1ff..0000000
--- a/packages/ui-new/vite.config.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import tailwindcss from "@tailwindcss/vite";
-import react from "@vitejs/plugin-react";
-import path from "path";
-import { defineConfig } from "vite";
-
-// https://vite.dev/config/
-export default defineConfig({
- plugins: [react(), tailwindcss()],
- resolve: {
- alias: {
- "@": path.resolve(__dirname, "./src"),
- "@components": path.resolve(__dirname, "./src/components"),
- "@stores": path.resolve(__dirname, "./src/stores"),
- "@types": path.resolve(__dirname, "./src/types"),
- "@pages": path.resolve(__dirname, "./src/pages"),
- },
- },
-});
diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md
deleted file mode 100644
index 4b2d22b..0000000
--- a/packages/ui/CHANGELOG.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Changelog
-
-## v0.2.0-alpha.1
-
-### New Features
-
-- [`e7ac28c`](https://github.com/HydroRoll-Team/DropOut/commit/e7ac28c6b8467a8fca0a3b61ba498e4742d3a718): Prepare for alpha mode pre-release. ([#62](https://github.com/HydroRoll-Team/DropOut/pull/62) by @fu050409)
diff --git a/packages/ui/README.md b/packages/ui/README.md
deleted file mode 100644
index a45e2a0..0000000
--- a/packages/ui/README.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# Svelte + TS + Vite
-
-This template should help get you started developing with Svelte and TypeScript in Vite.
-
-## Recommended IDE Setup
-
-[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
-
-## Need an official Svelte framework?
-
-Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
-
-## Technical considerations
-
-**Why use this over SvelteKit?**
-
-- It brings its own routing solution which might not be preferable for some users.
-- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
-
-This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
-
-Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
-
-**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
-
-Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
-
-**Why include `.vscode/extensions.json`?**
-
-Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
-
-**Why enable `allowJs` in the TS template?**
-
-While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
-
-**Why is HMR not preserving my local component state?**
-
-HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
-
-If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
-
-```ts
-// store.ts
-// An extremely simple external store
-import { writable } from "svelte/store";
-export default writable(0);
-```
diff --git a/packages/ui-new/components.json b/packages/ui/components.json
index f9d4fcd..f9d4fcd 100644
--- a/packages/ui-new/components.json
+++ b/packages/ui/components.json
diff --git a/packages/ui/index.html b/packages/ui/index.html
index 4fe68e1..5191e6f 100644
--- a/packages/ui/index.html
+++ b/packages/ui/index.html
@@ -2,12 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+ <link rel="icon" type="image/svg+xml" href="/icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>Dropout</title>
+ <title>Dropout Launcher</title>
</head>
<body>
- <div id="app"></div>
- <script type="module" src="/src/main.ts"></script>
+ <div id="root"></div>
+ <script type="module" src="/src/main.tsx"></script>
</body>
</html>
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 943fddc..c78290f 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,41 +1,55 @@
{
"name": "@dropout/ui",
- "version": "0.2.0-alpha.1",
"private": true,
+ "version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
- "build": "vite build",
- "preview": "vite preview",
- "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json",
- "lint": "oxlint .",
- "lint:fix": "oxlint . --fix",
- "format": "oxfmt . --write"
+ "build": "tsc -b && vite build",
+ "lint": "biome check .",
+ "preview": "vite preview"
},
"dependencies": {
+ "@base-ui/react": "^1.2.0",
+ "@radix-ui/react-checkbox": "^1.3.3",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-scroll-area": "^1.2.10",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-switch": "^1.2.6",
+ "@radix-ui/react-tabs": "^1.1.13",
"@tauri-apps/api": "^2.9.1",
"@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-fs": "^2.4.5",
"@tauri-apps/plugin-shell": "^2.3.4",
- "lucide-svelte": "^0.562.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "es-toolkit": "^1.44.0",
+ "lucide-react": "^0.562.0",
"marked": "^17.0.1",
- "node-emoji": "^2.2.0",
- "prismjs": "^1.30.0"
+ "next-themes": "^0.4.6",
+ "radix-ui": "^1.4.3",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-router": "^7.12.0",
+ "sonner": "^2.0.7",
+ "tailwind-merge": "^3.4.1",
+ "zod": "^4.3.6",
+ "zustand": "^5.0.10"
},
"devDependencies": {
- "@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/vite": "^4.1.18",
- "@tsconfig/svelte": "^5.0.6",
"@types/node": "^24.10.1",
- "@types/prismjs": "^1.26.5",
- "autoprefixer": "^10.4.23",
- "oxfmt": "^0.24.0",
- "oxlint": "^1.39.0",
- "postcss": "^8.5.6",
- "svelte": "^5.46.4",
- "svelte-check": "^4.3.4",
+ "@types/react": "^19.2.5",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "globals": "^16.5.0",
+ "shadcn": "^3.8.5",
"tailwindcss": "^4.1.18",
+ "tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
- "vite": "npm:rolldown-vite@7.2.5"
+ "vite": "npm:rolldown-vite@^7"
}
}
diff --git a/packages/ui/pnpm-lock.yaml b/packages/ui/pnpm-lock.yaml
deleted file mode 100644
index 465b682..0000000
--- a/packages/ui/pnpm-lock.yaml
+++ /dev/null
@@ -1,1363 +0,0 @@
-lockfileVersion: '9.0'
-
-settings:
- autoInstallPeers: true
- excludeLinksFromLockfile: false
-
-overrides:
- vite: npm:rolldown-vite@7.2.5
-
-importers:
-
- .:
- dependencies:
- '@tauri-apps/api':
- specifier: ^2.9.1
- version: 2.9.1
- '@tauri-apps/plugin-dialog':
- specifier: ^2.6.0
- version: 2.6.0
- '@tauri-apps/plugin-fs':
- specifier: ^2.4.5
- version: 2.4.5
- '@tauri-apps/plugin-shell':
- specifier: ^2.3.4
- version: 2.3.4
- lucide-svelte:
- specifier: ^0.562.0
- version: 0.562.0(svelte@5.46.4)
- marked:
- specifier: ^17.0.1
- version: 17.0.1
- node-emoji:
- specifier: ^2.2.0
- version: 2.2.0
- prismjs:
- specifier: ^1.30.0
- version: 1.30.0
- devDependencies:
- '@sveltejs/vite-plugin-svelte':
- specifier: ^6.2.1
- version: 6.2.4(rolldown-vite@7.2.5(@types/node@24.10.7)(jiti@2.6.1))(svelte@5.46.4)
- '@tailwindcss/vite':
- specifier: ^4.1.18
- version: 4.1.18(rolldown-vite@7.2.5(@types/node@24.10.7)(jiti@2.6.1))
- '@tsconfig/svelte':
- specifier: ^5.0.6
- version: 5.0.6
- '@types/node':
- specifier: ^24.10.1
- version: 24.10.7
- '@types/prismjs':
- specifier: ^1.26.5
- version: 1.26.5
- autoprefixer:
- specifier: ^10.4.23
- version: 10.4.23(postcss@8.5.6)
- oxfmt:
- specifier: ^0.24.0
- version: 0.24.0
- oxlint:
- specifier: ^1.39.0
- version: 1.39.0
- postcss:
- specifier: ^8.5.6
- version: 8.5.6
- svelte:
- specifier: ^5.46.4
- version: 5.46.4
- svelte-check:
- specifier: ^4.3.4
- version: 4.3.5(picomatch@4.0.3)(svelte@5.46.4)(typescript@5.9.3)
- tailwindcss:
- specifier: ^4.1.18
- version: 4.1.18
- typescript:
- specifier: ~5.9.3
- version: 5.9.3
- vite:
- specifier: npm:rolldown-vite@7.2.5
- version: rolldown-vite@7.2.5(@types/node@24.10.7)(jiti@2.6.1)
-
-packages:
-
- '@emnapi/core@1.8.1':
- resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
-
- '@emnapi/runtime@1.8.1':
- resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
-
- '@emnapi/wasi-threads@1.1.0':
- resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
-
- '@jridgewell/gen-mapping@0.3.13':
- resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
-
- '@jridgewell/remapping@2.3.5':
- resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
-
- '@jridgewell/resolve-uri@3.1.2':
- resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
- engines: {node: '>=6.0.0'}
-
- '@jridgewell/sourcemap-codec@1.5.5':
- resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
-
- '@jridgewell/trace-mapping@0.3.31':
- resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
-
- '@napi-rs/wasm-runtime@1.1.1':
- resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
-
- '@oxc-project/runtime@0.97.0':
- resolution: {integrity: sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w==}
- engines: {node: ^20.19.0 || >=22.12.0}
-
- '@oxc-project/types@0.97.0':
- resolution: {integrity: sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ==}
-
- '@oxfmt/darwin-arm64@0.24.0':
- resolution: {integrity: sha512-aYXuGf/yq8nsyEcHindGhiz9I+GEqLkVq8CfPbd+6VE259CpPEH+CaGHEO1j6vIOmNr8KHRq+IAjeRO2uJpb8A==}
- cpu: [arm64]
- os: [darwin]
-
- '@oxfmt/darwin-x64@0.24.0':
- resolution: {integrity: sha512-vs3b8Bs53hbiNvcNeBilzE/+IhDTWKjSBB3v/ztr664nZk65j0xr+5IHMBNz3CFppmX7o/aBta2PxY+t+4KoPg==}
- cpu: [x64]
- os: [darwin]
-
- '@oxfmt/linux-arm64-gnu@0.24.0':
- resolution: {integrity: sha512-ItPDOPoQ0wLj/s8osc5ch57uUcA1Wk8r0YdO8vLRpXA3UNg7KPOm1vdbkIZRRiSUphZcuX5ioOEetEK8H7RlTw==}
- cpu: [arm64]
- os: [linux]
- libc: [glibc]
-
- '@oxfmt/linux-arm64-musl@0.24.0':
- resolution: {integrity: sha512-JkQO3WnQjQTJONx8nxdgVBfl6BBFfpp9bKhChYhWeakwJdr7QPOAWJ/v3FGZfr0TbqINwnNR74aVZayDDRyXEA==}
- cpu: [arm64]
- os: [linux]
- libc: [musl]
-
- '@oxfmt/linux-x64-gnu@0.24.0':
- resolution: {integrity: sha512-N/SXlFO+2kak5gMt0oxApi0WXQDhwA0PShR0UbkY0PwtHjfSiDqJSOumyNqgQVoroKr1GNnoRmUqjZIz6DKIcw==}
- cpu: [x64]
- os: [linux]
- libc: [glibc]
-
- '@oxfmt/linux-x64-musl@0.24.0':
- resolution: {integrity: sha512-WM0pek5YDCQf50XQ7GLCE9sMBCMPW/NPAEPH/Hx6Qyir37lEsP4rUmSECo/QFNTU6KBc9NnsviAyJruWPpCMXw==}
- cpu: [x64]
- os: [linux]
- libc: [musl]
-
- '@oxfmt/win32-arm64@0.24.0':
- resolution: {integrity: sha512-vFCseli1KWtwdHrVlT/jWfZ8jP8oYpnPPEjI23mPLW8K/6GEJmmvy0PZP5NpWUFNTzX0lqie58XnrATJYAe9Xw==}
- cpu: [arm64]
- os: [win32]
-
- '@oxfmt/win32-x64@0.24.0':
- resolution: {integrity: sha512-0tmlNzcyewAnauNeBCq0xmAkmiKzl+H09p0IdHy+QKrTQdtixtf+AOjDAADbRfihkS+heF15Pjc4IyJMdAAJjw==}
- cpu: [x64]
- os: [win32]
-
- '@oxlint/darwin-arm64@1.39.0':
- resolution: {integrity: sha512-lT3hNhIa02xCujI6YGgjmYGg3Ht/X9ag5ipUVETaMpx5Rd4BbTNWUPif1WN1YZHxt3KLCIqaAe7zVhatv83HOQ==}
- cpu: [arm64]
- os: [darwin]
-
- '@oxlint/darwin-x64@1.39.0':
- resolution: {integrity: sha512-UT+rfTWd+Yr7iJeSLd/7nF8X4gTYssKh+n77hxl6Oilp3NnG1CKRHxZDy3o3lIBnwgzJkdyUAiYWO1bTMXQ1lA==}
- cpu: [x64]
- os: [darwin]
-
- '@oxlint/linux-arm64-gnu@1.39.0':
- resolution: {integrity: sha512-qocBkvS2V6rH0t9AT3DfQunMnj3xkM7srs5/Ycj2j5ZqMoaWd/FxHNVJDFP++35roKSvsRJoS0mtA8/77jqm6Q==}
- cpu: [arm64]
- os: [linux]
- libc: [glibc]
-
- '@oxlint/linux-arm64-musl@1.39.0':
- resolution: {integrity: sha512-arZzAc1PPcz9epvGBBCMHICeyQloKtHX3eoOe62B3Dskn7gf6Q14wnDHr1r9Vp4vtcBATNq6HlKV14smdlC/qA==}
- cpu: [arm64]
- os: [linux]
- libc: [musl]
-
- '@oxlint/linux-x64-gnu@1.39.0':
- resolution: {integrity: sha512-ZVt5qsECpuNprdWxAPpDBwoixr1VTcZ4qAEQA2l/wmFyVPDYFD3oBY/SWACNnWBddMrswjTg9O8ALxYWoEpmXw==}
- cpu: [x64]
- os: [linux]
- libc: [glibc]
-
- '@oxlint/linux-x64-musl@1.39.0':
- resolution: {integrity: sha512-pB0hlGyKPbxr9NMIV783lD6cWL3MpaqnZRM9MWni4yBdHPTKyFNYdg5hGD0Bwg+UP4S2rOevq/+OO9x9Bi7E6g==}
- cpu: [x64]
- os: [linux]
- libc: [musl]
-
- '@oxlint/win32-arm64@1.39.0':
- resolution: {integrity: sha512-Gg2SFaJohI9+tIQVKXlPw3FsPQFi/eCSWiCgwPtPn5uzQxHRTeQEZKuluz1fuzR5U70TXubb2liZi4Dgl8LJQA==}
- cpu: [arm64]
- os: [win32]
-
- '@oxlint/win32-x64@1.39.0':
- resolution: {integrity: sha512-sbi25lfj74hH+6qQtb7s1wEvd1j8OQbTaH8v3xTcDjrwm579Cyh0HBv1YSZ2+gsnVwfVDiCTL1D0JsNqYXszVA==}
- cpu: [x64]
- os: [win32]
-
- '@rolldown/binding-android-arm64@1.0.0-beta.50':
- resolution: {integrity: sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm64]
- os: [android]
-
- '@rolldown/binding-darwin-arm64@1.0.0-beta.50':
- resolution: {integrity: sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm64]
- os: [darwin]
-
- '@rolldown/binding-darwin-x64@1.0.0-beta.50':
- resolution: {integrity: sha512-fFXDjXnuX7/gQZQm/1FoivVtRcyAzdjSik7Eo+9iwPQ9EgtA5/nB2+jmbzaKtMGG3q+BnZbdKHCtOacmNrkIDA==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [x64]
- os: [darwin]
-
- '@rolldown/binding-freebsd-x64@1.0.0-beta.50':
- resolution: {integrity: sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [x64]
- os: [freebsd]
-
- '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50':
- resolution: {integrity: sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm]
- os: [linux]
-
- '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50':
- resolution: {integrity: sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm64]
- os: [linux]
- libc: [glibc]
-
- '@rolldown/binding-linux-arm64-musl@1.0.0-beta.50':
- resolution: {integrity: sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm64]
- os: [linux]
- libc: [musl]
-
- '@rolldown/binding-linux-x64-gnu@1.0.0-beta.50':
- resolution: {integrity: sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [x64]
- os: [linux]
- libc: [glibc]
-
- '@rolldown/binding-linux-x64-musl@1.0.0-beta.50':
- resolution: {integrity: sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [x64]
- os: [linux]
- libc: [musl]
-
- '@rolldown/binding-openharmony-arm64@1.0.0-beta.50':
- resolution: {integrity: sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm64]
- os: [openharmony]
-
- '@rolldown/binding-wasm32-wasi@1.0.0-beta.50':
- resolution: {integrity: sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg==}
- engines: {node: '>=14.0.0'}
- cpu: [wasm32]
-
- '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50':
- resolution: {integrity: sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm64]
- os: [win32]
-
- '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50':
- resolution: {integrity: sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [ia32]
- os: [win32]
-
- '@rolldown/binding-win32-x64-msvc@1.0.0-beta.50':
- resolution: {integrity: sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [x64]
- os: [win32]
-
- '@rolldown/pluginutils@1.0.0-beta.50':
- resolution: {integrity: sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==}
-
- '@sindresorhus/is@4.6.0':
- resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
- engines: {node: '>=10'}
-
- '@sveltejs/acorn-typescript@1.0.8':
- resolution: {integrity: sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==}
- peerDependencies:
- acorn: ^8.9.0
-
- '@sveltejs/vite-plugin-svelte-inspector@5.0.2':
- resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==}
- engines: {node: ^20.19 || ^22.12 || >=24}
- peerDependencies:
- '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0
- svelte: ^5.0.0
- vite: ^6.3.0 || ^7.0.0
-
- '@sveltejs/vite-plugin-svelte@6.2.4':
- resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==}
- engines: {node: ^20.19 || ^22.12 || >=24}
- peerDependencies:
- svelte: ^5.0.0
- vite: ^6.3.0 || ^7.0.0
-
- '@tailwindcss/node@4.1.18':
- resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==}
-
- '@tailwindcss/oxide-android-arm64@4.1.18':
- resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [android]
-
- '@tailwindcss/oxide-darwin-arm64@4.1.18':
- resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [darwin]
-
- '@tailwindcss/oxide-darwin-x64@4.1.18':
- resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [darwin]
-
- '@tailwindcss/oxide-freebsd-x64@4.1.18':
- resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [freebsd]
-
- '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
- resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==}
- engines: {node: '>= 10'}
- cpu: [arm]
- os: [linux]
-
- '@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
- resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [linux]
- libc: [glibc]
-
- '@tailwindcss/oxide-linux-arm64-musl@4.1.18':
- resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [linux]
- libc: [musl]
-
- '@tailwindcss/oxide-linux-x64-gnu@4.1.18':
- resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [linux]
- libc: [glibc]
-
- '@tailwindcss/oxide-linux-x64-musl@4.1.18':
- resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [linux]
- libc: [musl]
-
- '@tailwindcss/oxide-wasm32-wasi@4.1.18':
- resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
- engines: {node: '>=14.0.0'}
- cpu: [wasm32]
- bundledDependencies:
- - '@napi-rs/wasm-runtime'
- - '@emnapi/core'
- - '@emnapi/runtime'
- - '@tybys/wasm-util'
- - '@emnapi/wasi-threads'
- - tslib
-
- '@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
- resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [win32]
-
- '@tailwindcss/oxide-win32-x64-msvc@4.1.18':
- resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [win32]
-
- '@tailwindcss/oxide@4.1.18':
- resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==}
- engines: {node: '>= 10'}
-
- '@tailwindcss/vite@4.1.18':
- resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==}
- peerDependencies:
- vite: ^5.2.0 || ^6 || ^7
-
- '@tauri-apps/api@2.9.1':
- resolution: {integrity: sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==}
-
- '@tauri-apps/plugin-dialog@2.6.0':
- resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==}
-
- '@tauri-apps/plugin-fs@2.4.5':
- resolution: {integrity: sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA==}
-
- '@tauri-apps/plugin-shell@2.3.4':
- resolution: {integrity: sha512-ktsRWf8wHLD17aZEyqE8c5x98eNAuTizR1FSX475zQ4TxaiJnhwksLygQz+AGwckJL5bfEP13nWrlTNQJUpKpA==}
-
- '@tsconfig/svelte@5.0.6':
- resolution: {integrity: sha512-yGxYL0I9eETH1/DR9qVJey4DAsCdeau4a9wYPKuXfEhm8lFO8wg+LLYJjIpAm6Fw7HSlhepPhYPDop75485yWQ==}
-
- '@tybys/wasm-util@0.10.1':
- resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
-
- '@types/estree@1.0.8':
- resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
-
- '@types/node@24.10.7':
- resolution: {integrity: sha512-+054pVMzVTmRQV8BhpGv3UyfZ2Llgl8rdpDTon+cUH9+na0ncBVXj3wTUKh14+Kiz18ziM3b4ikpP5/Pc0rQEQ==}
-
- '@types/prismjs@1.26.5':
- resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
-
- acorn@8.15.0:
- resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
- engines: {node: '>=0.4.0'}
- hasBin: true
-
- aria-query@5.3.2:
- resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
- engines: {node: '>= 0.4'}
-
- autoprefixer@10.4.23:
- resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==}
- engines: {node: ^10 || ^12 || >=14}
- hasBin: true
- peerDependencies:
- postcss: ^8.1.0
-
- axobject-query@4.1.0:
- resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
- engines: {node: '>= 0.4'}
-
- baseline-browser-mapping@2.9.14:
- resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==}
- hasBin: true
-
- browserslist@4.28.1:
- resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
- engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
- hasBin: true
-
- caniuse-lite@1.0.30001764:
- resolution: {integrity: sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==}
-
- char-regex@1.0.2:
- resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
- engines: {node: '>=10'}
-
- chokidar@4.0.3:
- resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
- engines: {node: '>= 14.16.0'}
-
- clsx@2.1.1:
- resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
- engines: {node: '>=6'}
-
- deepmerge@4.3.1:
- resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
- engines: {node: '>=0.10.0'}
-
- detect-libc@2.1.2:
- resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
- engines: {node: '>=8'}
-
- devalue@5.6.2:
- resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==}
-
- electron-to-chromium@1.5.267:
- resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
-
- emojilib@2.4.0:
- resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==}
-
- enhanced-resolve@5.18.4:
- resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
- engines: {node: '>=10.13.0'}
-
- escalade@3.2.0:
- resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
- engines: {node: '>=6'}
-
- esm-env@1.2.2:
- resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
-
- esrap@2.2.1:
- resolution: {integrity: sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==}
-
- fdir@6.5.0:
- resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
- engines: {node: '>=12.0.0'}
- peerDependencies:
- picomatch: ^3 || ^4
- peerDependenciesMeta:
- picomatch:
- optional: true
-
- fraction.js@5.3.4:
- resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
-
- fsevents@2.3.3:
- resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
- engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
- os: [darwin]
-
- graceful-fs@4.2.11:
- resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
-
- is-reference@3.0.3:
- resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
-
- jiti@2.6.1:
- resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
- hasBin: true
-
- lightningcss-android-arm64@1.30.2:
- resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
- engines: {node: '>= 12.0.0'}
- cpu: [arm64]
- os: [android]
-
- lightningcss-darwin-arm64@1.30.2:
- resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==}
- engines: {node: '>= 12.0.0'}
- cpu: [arm64]
- os: [darwin]
-
- lightningcss-darwin-x64@1.30.2:
- resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==}
- engines: {node: '>= 12.0.0'}
- cpu: [x64]
- os: [darwin]
-
- lightningcss-freebsd-x64@1.30.2:
- resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==}
- engines: {node: '>= 12.0.0'}
- cpu: [x64]
- os: [freebsd]
-
- lightningcss-linux-arm-gnueabihf@1.30.2:
- resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==}
- engines: {node: '>= 12.0.0'}
- cpu: [arm]
- os: [linux]
-
- lightningcss-linux-arm64-gnu@1.30.2:
- resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
- engines: {node: '>= 12.0.0'}
- cpu: [arm64]
- os: [linux]
- libc: [glibc]
-
- lightningcss-linux-arm64-musl@1.30.2:
- resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
- engines: {node: '>= 12.0.0'}
- cpu: [arm64]
- os: [linux]
- libc: [musl]
-
- lightningcss-linux-x64-gnu@1.30.2:
- resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
- engines: {node: '>= 12.0.0'}
- cpu: [x64]
- os: [linux]
- libc: [glibc]
-
- lightningcss-linux-x64-musl@1.30.2:
- resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
- engines: {node: '>= 12.0.0'}
- cpu: [x64]
- os: [linux]
- libc: [musl]
-
- lightningcss-win32-arm64-msvc@1.30.2:
- resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
- engines: {node: '>= 12.0.0'}
- cpu: [arm64]
- os: [win32]
-
- lightningcss-win32-x64-msvc@1.30.2:
- resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==}
- engines: {node: '>= 12.0.0'}
- cpu: [x64]
- os: [win32]
-
- lightningcss@1.30.2:
- resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
- engines: {node: '>= 12.0.0'}
-
- locate-character@3.0.0:
- resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
-
- lucide-svelte@0.562.0:
- resolution: {integrity: sha512-kSJDH/55lf0mun/o4nqWBXOcq0fWYzPeIjbTD97ywoeumAB9kWxtM06gC7oynqjtK3XhAljWSz5RafIzPEYIQA==}
- peerDependencies:
- svelte: ^3 || ^4 || ^5.0.0-next.42
-
- magic-string@0.30.21:
- resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
-
- marked@17.0.1:
- resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==}
- engines: {node: '>= 20'}
- hasBin: true
-
- mri@1.2.0:
- resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
- engines: {node: '>=4'}
-
- nanoid@3.3.11:
- resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
- engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
- hasBin: true
-
- node-emoji@2.2.0:
- resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==}
- engines: {node: '>=18'}
-
- node-releases@2.0.27:
- resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
-
- obug@2.1.1:
- resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
-
- oxfmt@0.24.0:
- resolution: {integrity: sha512-UjeM3Peez8Tl7IJ9s5UwAoZSiDRMww7BEc21gDYxLq3S3/KqJnM3mjNxsoSHgmBvSeX6RBhoVc2MfC/+96RdSw==}
- engines: {node: ^20.19.0 || >=22.12.0}
- hasBin: true
-
- oxlint@1.39.0:
- resolution: {integrity: sha512-wSiLr0wjG+KTU6c1LpVoQk7JZ7l8HCKlAkVDVTJKWmCGazsNxexxnOXl7dsar92mQcRnzko5g077ggP3RINSjA==}
- engines: {node: ^20.19.0 || >=22.12.0}
- hasBin: true
- peerDependencies:
- oxlint-tsgolint: '>=0.10.0'
- peerDependenciesMeta:
- oxlint-tsgolint:
- optional: true
-
- picocolors@1.1.1:
- resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
-
- picomatch@4.0.3:
- resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
- engines: {node: '>=12'}
-
- postcss-value-parser@4.2.0:
- resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
-
- postcss@8.5.6:
- resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
- engines: {node: ^10 || ^12 || >=14}
-
- prismjs@1.30.0:
- resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
- engines: {node: '>=6'}
-
- readdirp@4.1.2:
- resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
- engines: {node: '>= 14.18.0'}
-
- rolldown-vite@7.2.5:
- resolution: {integrity: sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==}
- engines: {node: ^20.19.0 || >=22.12.0}
- hasBin: true
- peerDependencies:
- '@types/node': ^20.19.0 || >=22.12.0
- esbuild: ^0.25.0
- jiti: '>=1.21.0'
- less: ^4.0.0
- sass: ^1.70.0
- sass-embedded: ^1.70.0
- stylus: '>=0.54.8'
- sugarss: ^5.0.0
- terser: ^5.16.0
- tsx: ^4.8.1
- yaml: ^2.4.2
- peerDependenciesMeta:
- '@types/node':
- optional: true
- esbuild:
- optional: true
- jiti:
- optional: true
- less:
- optional: true
- sass:
- optional: true
- sass-embedded:
- optional: true
- stylus:
- optional: true
- sugarss:
- optional: true
- terser:
- optional: true
- tsx:
- optional: true
- yaml:
- optional: true
-
- rolldown@1.0.0-beta.50:
- resolution: {integrity: sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A==}
- engines: {node: ^20.19.0 || >=22.12.0}
- hasBin: true
-
- sade@1.8.1:
- resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
- engines: {node: '>=6'}
-
- skin-tone@2.0.0:
- resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==}
- engines: {node: '>=8'}
-
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
- engines: {node: '>=0.10.0'}
-
- svelte-check@4.3.5:
- resolution: {integrity: sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==}
- engines: {node: '>= 18.0.0'}
- hasBin: true
- peerDependencies:
- svelte: ^4.0.0 || ^5.0.0-next.0
- typescript: '>=5.0.0'
-
- svelte@5.46.4:
- resolution: {integrity: sha512-VJwdXrmv9L8L7ZasJeWcCjoIuMRVbhuxbss0fpVnR8yorMmjNDwcjIH08vS6wmSzzzgAG5CADQ1JuXPS2nwt9w==}
- engines: {node: '>=18'}
-
- tailwindcss@4.1.18:
- resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
-
- tapable@2.3.0:
- resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
- engines: {node: '>=6'}
-
- tinyglobby@0.2.15:
- resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
- engines: {node: '>=12.0.0'}
-
- tinypool@2.0.0:
- resolution: {integrity: sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg==}
- engines: {node: ^20.0.0 || >=22.0.0}
-
- tslib@2.8.1:
- resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
-
- typescript@5.9.3:
- resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
- engines: {node: '>=14.17'}
- hasBin: true
-
- undici-types@7.16.0:
- resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
-
- unicode-emoji-modifier-base@1.0.0:
- resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==}
- engines: {node: '>=4'}
-
- update-browserslist-db@1.2.3:
- resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
- hasBin: true
- peerDependencies:
- browserslist: '>= 4.21.0'
-
- vitefu@1.1.1:
- resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==}
- peerDependencies:
- vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0
- peerDependenciesMeta:
- vite:
- optional: true
-
- zimmerframe@1.1.4:
- resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
-
-snapshots:
-
- '@emnapi/core@1.8.1':
- dependencies:
- '@emnapi/wasi-threads': 1.1.0
- tslib: 2.8.1
- optional: true
-
- '@emnapi/runtime@1.8.1':
- dependencies:
- tslib: 2.8.1
- optional: true
-
- '@emnapi/wasi-threads@1.1.0':
- dependencies:
- tslib: 2.8.1
- optional: true
-
- '@jridgewell/gen-mapping@0.3.13':
- dependencies:
- '@jridgewell/sourcemap-codec': 1.5.5
- '@jridgewell/trace-mapping': 0.3.31
-
- '@jridgewell/remapping@2.3.5':
- dependencies:
- '@jridgewell/gen-mapping': 0.3.13
- '@jridgewell/trace-mapping': 0.3.31
-
- '@jridgewell/resolve-uri@3.1.2': {}
-
- '@jridgewell/sourcemap-codec@1.5.5': {}
-
- '@jridgewell/trace-mapping@0.3.31':
- dependencies:
- '@jridgewell/resolve-uri': 3.1.2
- '@jridgewell/sourcemap-codec': 1.5.5
-
- '@napi-rs/wasm-runtime@1.1.1':
- dependencies:
- '@emnapi/core': 1.8.1
- '@emnapi/runtime': 1.8.1
- '@tybys/wasm-util': 0.10.1
- optional: true
-
- '@oxc-project/runtime@0.97.0': {}
-
- '@oxc-project/types@0.97.0': {}
-
- '@oxfmt/darwin-arm64@0.24.0':
- optional: true
-
- '@oxfmt/darwin-x64@0.24.0':
- optional: true
-
- '@oxfmt/linux-arm64-gnu@0.24.0':
- optional: true
-
- '@oxfmt/linux-arm64-musl@0.24.0':
- optional: true
-
- '@oxfmt/linux-x64-gnu@0.24.0':
- optional: true
-
- '@oxfmt/linux-x64-musl@0.24.0':
- optional: true
-
- '@oxfmt/win32-arm64@0.24.0':
- optional: true
-
- '@oxfmt/win32-x64@0.24.0':
- optional: true
-
- '@oxlint/darwin-arm64@1.39.0':
- optional: true
-
- '@oxlint/darwin-x64@1.39.0':
- optional: true
-
- '@oxlint/linux-arm64-gnu@1.39.0':
- optional: true
-
- '@oxlint/linux-arm64-musl@1.39.0':
- optional: true
-
- '@oxlint/linux-x64-gnu@1.39.0':
- optional: true
-
- '@oxlint/linux-x64-musl@1.39.0':
- optional: true
-
- '@oxlint/win32-arm64@1.39.0':
- optional: true
-
- '@oxlint/win32-x64@1.39.0':
- optional: true
-
- '@rolldown/binding-android-arm64@1.0.0-beta.50':
- optional: true
-
- '@rolldown/binding-darwin-arm64@1.0.0-beta.50':
- optional: true
-
- '@rolldown/binding-darwin-x64@1.0.0-beta.50':
- optional: true
-
- '@rolldown/binding-freebsd-x64@1.0.0-beta.50':
- optional: true
-
- '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50':
- optional: true
-
- '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50':
- optional: true
-
- '@rolldown/binding-linux-arm64-musl@1.0.0-beta.50':
- optional: true
-
- '@rolldown/binding-linux-x64-gnu@1.0.0-beta.50':
- optional: true
-
- '@rolldown/binding-linux-x64-musl@1.0.0-beta.50':
- optional: true
-
- '@rolldown/binding-openharmony-arm64@1.0.0-beta.50':
- optional: true
-
- '@rolldown/binding-wasm32-wasi@1.0.0-beta.50':
- dependencies:
- '@napi-rs/wasm-runtime': 1.1.1
- optional: true
-
- '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50':
- optional: true
-
- '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50':
- optional: true
-
- '@rolldown/binding-win32-x64-msvc@1.0.0-beta.50':
- optional: true
-
- '@rolldown/pluginutils@1.0.0-beta.50': {}
-
- '@sindresorhus/is@4.6.0': {}
-
- '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)':
- dependencies:
- acorn: 8.15.0
-
- '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(rolldown-vite@7.2.5(@types/node@24.10.7)(jiti@2.6.1))(svelte@5.46.4))(rolldown-vite@7.2.5(@types/node@24.10.7)(jiti@2.6.1))(svelte@5.46.4)':
- dependencies:
- '@sveltejs/vite-plugin-svelte': 6.2.4(rolldown-vite@7.2.5(@types/node@24.10.7)(jiti@2.6.1))(svelte@5.46.4)
- obug: 2.1.1
- svelte: 5.46.4
- vite: rolldown-vite@7.2.5(@types/node@24.10.7)(jiti@2.6.1)
-
- '@sveltejs/vite-plugin-svelte@6.2.4(rolldown-vite@7.2.5(@types/node@24.10.7)(jiti@2.6.1))(svelte@5.46.4)':
- dependencies:
- '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(rolldown-vite@7.2.5(@types/node@24.10.7)(jiti@2.6.1))(svelte@5.46.4))(rolldown-vite@7.2.5(@types/node@24.10.7)(jiti@2.6.1))(svelte@5.46.4)
- deepmerge: 4.3.1
- magic-string: 0.30.21
- obug: 2.1.1
- svelte: 5.46.4
- vite: rolldown-vite@7.2.5(@types/node@24.10.7)(jiti@2.6.1)
- vitefu: 1.1.1(rolldown-vite@7.2.5(@types/node@24.10.7)(jiti@2.6.1))
-
- '@tailwindcss/node@4.1.18':
- dependencies:
- '@jridgewell/remapping': 2.3.5
- enhanced-resolve: 5.18.4
- jiti: 2.6.1
- lightningcss: 1.30.2
- magic-string: 0.30.21
- source-map-js: 1.2.1
- tailwindcss: 4.1.18
-
- '@tailwindcss/oxide-android-arm64@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-darwin-arm64@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-darwin-x64@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-freebsd-x64@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-linux-arm64-musl@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-linux-x64-gnu@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-linux-x64-musl@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-wasm32-wasi@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-win32-x64-msvc@4.1.18':
- optional: true
-
- '@tailwindcss/oxide@4.1.18':
- optionalDependencies:
- '@tailwindcss/oxide-android-arm64': 4.1.18
- '@tailwindcss/oxide-darwin-arm64': 4.1.18
- '@tailwindcss/oxide-darwin-x64': 4.1.18
- '@tailwindcss/oxide-freebsd-x64': 4.1.18
- '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18
- '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18
- '@tailwindcss/oxide-linux-arm64-musl': 4.1.18
- '@tailwindcss/oxide-linux-x64-gnu': 4.1.18
- '@tailwindcss/oxide-linux-x64-musl': 4.1.18
- '@tailwindcss/oxide-wasm32-wasi': 4.1.18
- '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18
- '@tailwindcss/oxide-win32-x64-msvc': 4.1.18
-
- '@tailwindcss/vite@4.1.18(rolldown-vite@7.2.5(@types/node@24.10.7)(jiti@2.6.1))':
- dependencies:
- '@tailwindcss/node': 4.1.18
- '@tailwindcss/oxide': 4.1.18
- tailwindcss: 4.1.18
- vite: rolldown-vite@7.2.5(@types/node@24.10.7)(jiti@2.6.1)
-
- '@tauri-apps/api@2.9.1': {}
-
- '@tauri-apps/plugin-dialog@2.6.0':
- dependencies:
- '@tauri-apps/api': 2.9.1
-
- '@tauri-apps/plugin-fs@2.4.5':
- dependencies:
- '@tauri-apps/api': 2.9.1
-
- '@tauri-apps/plugin-shell@2.3.4':
- dependencies:
- '@tauri-apps/api': 2.9.1
-
- '@tsconfig/svelte@5.0.6': {}
-
- '@tybys/wasm-util@0.10.1':
- dependencies:
- tslib: 2.8.1
- optional: true
-
- '@types/estree@1.0.8': {}
-
- '@types/node@24.10.7':
- dependencies:
- undici-types: 7.16.0
-
- '@types/prismjs@1.26.5': {}
-
- acorn@8.15.0: {}
-
- aria-query@5.3.2: {}
-
- autoprefixer@10.4.23(postcss@8.5.6):
- dependencies:
- browserslist: 4.28.1
- caniuse-lite: 1.0.30001764
- fraction.js: 5.3.4
- picocolors: 1.1.1
- postcss: 8.5.6
- postcss-value-parser: 4.2.0
-
- axobject-query@4.1.0: {}
-
- baseline-browser-mapping@2.9.14: {}
-
- browserslist@4.28.1:
- dependencies:
- baseline-browser-mapping: 2.9.14
- caniuse-lite: 1.0.30001764
- electron-to-chromium: 1.5.267
- node-releases: 2.0.27
- update-browserslist-db: 1.2.3(browserslist@4.28.1)
-
- caniuse-lite@1.0.30001764: {}
-
- char-regex@1.0.2: {}
-
- chokidar@4.0.3:
- dependencies:
- readdirp: 4.1.2
-
- clsx@2.1.1: {}
-
- deepmerge@4.3.1: {}
-
- detect-libc@2.1.2: {}
-
- devalue@5.6.2: {}
-
- electron-to-chromium@1.5.267: {}
-
- emojilib@2.4.0: {}
-
- enhanced-resolve@5.18.4:
- dependencies:
- graceful-fs: 4.2.11
- tapable: 2.3.0
-
- escalade@3.2.0: {}
-
- esm-env@1.2.2: {}
-
- esrap@2.2.1:
- dependencies:
- '@jridgewell/sourcemap-codec': 1.5.5
-
- fdir@6.5.0(picomatch@4.0.3):
- optionalDependencies:
- picomatch: 4.0.3
-
- fraction.js@5.3.4: {}
-
- fsevents@2.3.3:
- optional: true
-
- graceful-fs@4.2.11: {}
-
- is-reference@3.0.3:
- dependencies:
- '@types/estree': 1.0.8
-
- jiti@2.6.1: {}
-
- lightningcss-android-arm64@1.30.2:
- optional: true
-
- lightningcss-darwin-arm64@1.30.2:
- optional: true
-
- lightningcss-darwin-x64@1.30.2:
- optional: true
-
- lightningcss-freebsd-x64@1.30.2:
- optional: true
-
- lightningcss-linux-arm-gnueabihf@1.30.2:
- optional: true
-
- lightningcss-linux-arm64-gnu@1.30.2:
- optional: true
-
- lightningcss-linux-arm64-musl@1.30.2:
- optional: true
-
- lightningcss-linux-x64-gnu@1.30.2:
- optional: true
-
- lightningcss-linux-x64-musl@1.30.2:
- optional: true
-
- lightningcss-win32-arm64-msvc@1.30.2:
- optional: true
-
- lightningcss-win32-x64-msvc@1.30.2:
- optional: true
-
- lightningcss@1.30.2:
- dependencies:
- detect-libc: 2.1.2
- optionalDependencies:
- lightningcss-android-arm64: 1.30.2
- lightningcss-darwin-arm64: 1.30.2
- lightningcss-darwin-x64: 1.30.2
- lightningcss-freebsd-x64: 1.30.2
- lightningcss-linux-arm-gnueabihf: 1.30.2
- lightningcss-linux-arm64-gnu: 1.30.2
- lightningcss-linux-arm64-musl: 1.30.2
- lightningcss-linux-x64-gnu: 1.30.2
- lightningcss-linux-x64-musl: 1.30.2
- lightningcss-win32-arm64-msvc: 1.30.2
- lightningcss-win32-x64-msvc: 1.30.2
-
- locate-character@3.0.0: {}
-
- lucide-svelte@0.562.0(svelte@5.46.4):
- dependencies:
- svelte: 5.46.4
-
- magic-string@0.30.21:
- dependencies:
- '@jridgewell/sourcemap-codec': 1.5.5
-
- marked@17.0.1: {}
-
- mri@1.2.0: {}
-
- nanoid@3.3.11: {}
-
- node-emoji@2.2.0:
- dependencies:
- '@sindresorhus/is': 4.6.0
- char-regex: 1.0.2
- emojilib: 2.4.0
- skin-tone: 2.0.0
-
- node-releases@2.0.27: {}
-
- obug@2.1.1: {}
-
- oxfmt@0.24.0:
- dependencies:
- tinypool: 2.0.0
- optionalDependencies:
- '@oxfmt/darwin-arm64': 0.24.0
- '@oxfmt/darwin-x64': 0.24.0
- '@oxfmt/linux-arm64-gnu': 0.24.0
- '@oxfmt/linux-arm64-musl': 0.24.0
- '@oxfmt/linux-x64-gnu': 0.24.0
- '@oxfmt/linux-x64-musl': 0.24.0
- '@oxfmt/win32-arm64': 0.24.0
- '@oxfmt/win32-x64': 0.24.0
-
- oxlint@1.39.0:
- optionalDependencies:
- '@oxlint/darwin-arm64': 1.39.0
- '@oxlint/darwin-x64': 1.39.0
- '@oxlint/linux-arm64-gnu': 1.39.0
- '@oxlint/linux-arm64-musl': 1.39.0
- '@oxlint/linux-x64-gnu': 1.39.0
- '@oxlint/linux-x64-musl': 1.39.0
- '@oxlint/win32-arm64': 1.39.0
- '@oxlint/win32-x64': 1.39.0
-
- picocolors@1.1.1: {}
-
- picomatch@4.0.3: {}
-
- postcss-value-parser@4.2.0: {}
-
- postcss@8.5.6:
- dependencies:
- nanoid: 3.3.11
- picocolors: 1.1.1
- source-map-js: 1.2.1
-
- prismjs@1.30.0: {}
-
- readdirp@4.1.2: {}
-
- rolldown-vite@7.2.5(@types/node@24.10.7)(jiti@2.6.1):
- dependencies:
- '@oxc-project/runtime': 0.97.0
- fdir: 6.5.0(picomatch@4.0.3)
- lightningcss: 1.30.2
- picomatch: 4.0.3
- postcss: 8.5.6
- rolldown: 1.0.0-beta.50
- tinyglobby: 0.2.15
- optionalDependencies:
- '@types/node': 24.10.7
- fsevents: 2.3.3
- jiti: 2.6.1
-
- rolldown@1.0.0-beta.50:
- dependencies:
- '@oxc-project/types': 0.97.0
- '@rolldown/pluginutils': 1.0.0-beta.50
- optionalDependencies:
- '@rolldown/binding-android-arm64': 1.0.0-beta.50
- '@rolldown/binding-darwin-arm64': 1.0.0-beta.50
- '@rolldown/binding-darwin-x64': 1.0.0-beta.50
- '@rolldown/binding-freebsd-x64': 1.0.0-beta.50
- '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.50
- '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.50
- '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.50
- '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.50
- '@rolldown/binding-linux-x64-musl': 1.0.0-beta.50
- '@rolldown/binding-openharmony-arm64': 1.0.0-beta.50
- '@rolldown/binding-wasm32-wasi': 1.0.0-beta.50
- '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.50
- '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.50
- '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.50
-
- sade@1.8.1:
- dependencies:
- mri: 1.2.0
-
- skin-tone@2.0.0:
- dependencies:
- unicode-emoji-modifier-base: 1.0.0
-
- source-map-js@1.2.1: {}
-
- svelte-check@4.3.5(picomatch@4.0.3)(svelte@5.46.4)(typescript@5.9.3):
- dependencies:
- '@jridgewell/trace-mapping': 0.3.31
- chokidar: 4.0.3
- fdir: 6.5.0(picomatch@4.0.3)
- picocolors: 1.1.1
- sade: 1.8.1
- svelte: 5.46.4
- typescript: 5.9.3
- transitivePeerDependencies:
- - picomatch
-
- svelte@5.46.4:
- dependencies:
- '@jridgewell/remapping': 2.3.5
- '@jridgewell/sourcemap-codec': 1.5.5
- '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0)
- '@types/estree': 1.0.8
- acorn: 8.15.0
- aria-query: 5.3.2
- axobject-query: 4.1.0
- clsx: 2.1.1
- devalue: 5.6.2
- esm-env: 1.2.2
- esrap: 2.2.1
- is-reference: 3.0.3
- locate-character: 3.0.0
- magic-string: 0.30.21
- zimmerframe: 1.1.4
-
- tailwindcss@4.1.18: {}
-
- tapable@2.3.0: {}
-
- tinyglobby@0.2.15:
- dependencies:
- fdir: 6.5.0(picomatch@4.0.3)
- picomatch: 4.0.3
-
- tinypool@2.0.0: {}
-
- tslib@2.8.1:
- optional: true
-
- typescript@5.9.3: {}
-
- undici-types@7.16.0: {}
-
- unicode-emoji-modifier-base@1.0.0: {}
-
- update-browserslist-db@1.2.3(browserslist@4.28.1):
- dependencies:
- browserslist: 4.28.1
- escalade: 3.2.0
- picocolors: 1.1.1
-
- vitefu@1.1.1(rolldown-vite@7.2.5(@types/node@24.10.7)(jiti@2.6.1)):
- optionalDependencies:
- vite: rolldown-vite@7.2.5(@types/node@24.10.7)(jiti@2.6.1)
-
- zimmerframe@1.1.4: {}
diff --git a/packages/ui-new/public/icon.svg b/packages/ui/public/icon.svg
index 0baf00f..0baf00f 100644
--- a/packages/ui-new/public/icon.svg
+++ b/packages/ui/public/icon.svg
diff --git a/packages/ui/public/vite.svg b/packages/ui/public/vite.svg
deleted file mode 100644
index ee9fada..0000000
--- a/packages/ui/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
diff --git a/packages/ui/src/App.svelte b/packages/ui/src/App.svelte
deleted file mode 100644
index f73e0a2..0000000
--- a/packages/ui/src/App.svelte
+++ /dev/null
@@ -1,217 +0,0 @@
-<script lang="ts">
- import { getVersion } from "@tauri-apps/api/app";
- // import { convertFileSrc } from "@tauri-apps/api/core"; // Removed duplicate, handled by import below or inline
- import { onDestroy, onMount } from "svelte";
- import DownloadMonitor from "./lib/DownloadMonitor.svelte";
- import GameConsole from "./lib/GameConsole.svelte";
-// Components
- import BottomBar from "./components/BottomBar.svelte";
- import HomeView from "./components/HomeView.svelte";
- import LoginModal from "./components/LoginModal.svelte";
- import ParticleBackground from "./components/ParticleBackground.svelte";
- import SettingsView from "./components/SettingsView.svelte";
- import AssistantView from "./components/AssistantView.svelte";
- import InstancesView from "./components/InstancesView.svelte";
- import Sidebar from "./components/Sidebar.svelte";
- import StatusToast from "./components/StatusToast.svelte";
- import VersionsView from "./components/VersionsView.svelte";
-// Stores
- import { authState } from "./stores/auth.svelte";
- import { gameState } from "./stores/game.svelte";
- import { instancesState } from "./stores/instances.svelte";
- import { settingsState } from "./stores/settings.svelte";
- import { uiState } from "./stores/ui.svelte";
- import { logsState } from "./stores/logs.svelte";
- import { convertFileSrc } from "@tauri-apps/api/core";
-
- let mouseX = $state(0);
- let mouseY = $state(0);
-
- function handleMouseMove(e: MouseEvent) {
- mouseX = (e.clientX / window.innerWidth) * 2 - 1;
- mouseY = (e.clientY / window.innerHeight) * 2 - 1;
- }
-
- onMount(async () => {
- // 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');
-
- authState.checkAccount();
- await settingsState.loadSettings();
- logsState.init();
- await settingsState.detectJava();
- await instancesState.loadInstances();
- gameState.loadVersions();
- getVersion().then((v) => (uiState.appVersion = v));
- window.addEventListener("mousemove", handleMouseMove);
- });
-
- // Refresh versions when active instance changes
- $effect(() => {
- if (instancesState.activeInstanceId) {
- gameState.loadVersions();
- } else {
- gameState.versions = [];
- }
- });
-
- onDestroy(() => {
- if (typeof window !== 'undefined')
- window.removeEventListener("mousemove", handleMouseMove);
- });
-</script>
-
-<div
- class="relative h-screen w-screen overflow-hidden dark:text-white text-gray-900 font-sans selection:bg-indigo-500/30"
->
- <!-- Modern Animated Background -->
- <div class="absolute inset-0 z-0 bg-[#09090b] dark:bg-[#09090b] bg-gray-100 overflow-hidden">
- {#if settingsState.settings.custom_background_path}
- <img
- src={convertFileSrc(settingsState.settings.custom_background_path)}
- alt="Background"
- class="absolute inset-0 w-full h-full object-cover transition-transform duration-[20s] ease-linear hover:scale-105"
- onerror={(e) => console.error("Failed to load main background:", e)}
- />
- <!-- Dimming Overlay for readability -->
- <div class="absolute inset-0 bg-black/50 "></div>
- {:else if settingsState.settings.enable_visual_effects}
- <!-- Original Gradient (Dark Only / or Adjusted for Light) -->
- {#if settingsState.settings.theme === 'dark'}
- <div
- class="absolute inset-0 opacity-60 bg-gradient-to-br from-emerald-900 via-zinc-900 to-indigo-950"
- ></div>
- {:else}
- <!-- Light Mode Gradient -->
- <div
- class="absolute inset-0 opacity-100 bg-gradient-to-br from-emerald-100 via-gray-100 to-indigo-100"
- ></div>
- {/if}
-
- {#if uiState.currentView === "home"}
- <ParticleBackground />
- {/if}
-
- <div
- class="absolute inset-0 bg-gradient-to-t from-zinc-900 via-transparent to-black/50 dark:from-zinc-900 dark:to-black/50 from-gray-100 to-transparent"
- ></div>
- {/if}
-
- <!-- Subtle Grid Overlay -->
- <div class="absolute inset-0 z-0 opacity-10 dark:opacity-10 opacity-30 pointer-events-none"
- style="background-image: linear-gradient({settingsState.settings.theme === 'dark' ? '#ffffff' : '#000000'} 1px, transparent 1px), linear-gradient(90deg, {settingsState.settings.theme === 'dark' ? '#ffffff' : '#000000'} 1px, transparent 1px); background-size: 40px 40px; mask-image: radial-gradient(circle at 50% 50%, black 30%, transparent 70%);">
- </div>
- </div>
-
- <!-- Content Wrapper -->
- <div class="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 class="flex-1 flex flex-col relative min-w-0 overflow-hidden transition-all duration-300">
-
- <!-- Window Drag Region -->
- <div
- class="h-8 w-full absolute top-0 left-0 z-50 drag-region"
- data-tauri-drag-region
- ></div>
-
- <!-- App Content -->
- <div class="flex-1 relative overflow-hidden flex flex-col">
- <!-- Views Container -->
- <div class="flex-1 relative overflow-hidden">
- {#if uiState.currentView === "home"}
- <HomeView mouseX={mouseX} mouseY={mouseY} />
- {:else if uiState.currentView === "instances"}
- <InstancesView />
- {:else if uiState.currentView === "versions"}
- <VersionsView />
- {:else if uiState.currentView === "settings"}
- <SettingsView />
- {:else if uiState.currentView === "guide"}
- <AssistantView />
- {/if}
- </div>
-
- <!-- Download Monitor Overlay -->
- <div class="absolute bottom-20 left-4 right-4 pointer-events-none z-20">
- <div class="pointer-events-auto">
- <DownloadMonitor />
- </div>
- </div>
-
- <!-- Bottom Bar -->
- {#if uiState.currentView === "home"}
- <BottomBar />
- {/if}
- </div>
- </main>
- </div>
-
- <LoginModal />
- <StatusToast />
-
- <!-- Logout Confirmation Dialog -->
- {#if authState.isLogoutConfirmOpen}
- <div class="fixed inset-0 z-[200] bg-black/70 backdrop-blur-sm flex items-center justify-center p-4">
- <div class="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 class="text-lg font-bold text-white mb-2">Logout</h3>
- <p class="text-zinc-400 text-sm mb-6">
- Are you sure you want to logout <span class="text-white font-medium">{authState.currentAccount?.username}</span>?
- </p>
- <div class="flex gap-3 justify-end">
- <button
- onclick={() => authState.cancelLogout()}
- class="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
- onclick={() => authState.confirmLogout()}
- class="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>
- {/if}
-
- {#if uiState.showConsole}
- <div class="fixed inset-0 z-[100] bg-black/80 backdrop-blur-sm flex items-center justify-center p-8">
- <div class="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>
- {/if}
-</div>
-
-<style>
- :global(body) {
- margin: 0;
- padding: 0;
- background: #000;
- }
-
- /* Modern Scrollbar */
- :global(*::-webkit-scrollbar) {
- width: 6px;
- height: 6px;
- }
-
- :global(*::-webkit-scrollbar-track) {
- background: transparent;
- }
-
- :global(*::-webkit-scrollbar-thumb) {
- background: rgba(255, 255, 255, 0.1);
- border-radius: 999px;
- }
-
- :global(*::-webkit-scrollbar-thumb:hover) {
- background: rgba(255, 255, 255, 0.25);
- }
-</style>
diff --git a/packages/ui/src/app.css b/packages/ui/src/app.css
deleted file mode 100644
index 63449b7..0000000
--- a/packages/ui/src/app.css
+++ /dev/null
@@ -1,167 +0,0 @@
-@import "tailwindcss";
-
-@variant dark (&:where(.dark, .dark *));
-
-/* ==================== Custom Select/Dropdown Styles ==================== */
-
-/* Base select styling */
-select {
- appearance: none;
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2371717a'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
- background-repeat: no-repeat;
- background-position: right 0.5rem center;
- background-size: 1rem;
- padding-right: 2rem;
-}
-
-/* Option styling - works in WebView/Chromium */
-select option {
- background-color: #18181b;
- color: #e4e4e7;
- padding: 12px 16px;
- font-size: 13px;
- border: none;
-}
-
-select option:hover,
-select option:focus {
- background-color: #3730a3 !important;
- color: white !important;
-}
-
-select option:checked {
- background: linear-gradient(0deg, #4f46e5 0%, #4f46e5 100%);
- color: white;
- font-weight: 500;
-}
-
-select option:disabled {
- color: #52525b;
- background-color: #18181b;
-}
-
-/* Optgroup styling */
-select optgroup {
- background-color: #18181b;
- color: #a1a1aa;
- font-weight: 600;
- font-size: 11px;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- padding: 8px 12px 4px;
-}
-
-/* Select focus state */
-select:focus {
- outline: none;
- border-color: #6366f1;
- box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
-}
-
-/* ==================== Custom Scrollbar (Global) ==================== */
-
-/* Firefox */
-* {
- scrollbar-width: thin;
- scrollbar-color: #3f3f46 transparent;
-}
-
-/* Webkit browsers */
-::-webkit-scrollbar {
- width: 8px;
- height: 8px;
-}
-
-::-webkit-scrollbar-track {
- background: transparent;
-}
-
-::-webkit-scrollbar-thumb {
- background-color: #3f3f46;
- border-radius: 4px;
- border: 2px solid transparent;
- background-clip: content-box;
-}
-
-::-webkit-scrollbar-thumb:hover {
- background-color: #52525b;
-}
-
-::-webkit-scrollbar-corner {
- background: transparent;
-}
-
-/* ==================== Input/Form Element Consistency ==================== */
-
-input[type="text"],
-input[type="number"],
-input[type="password"],
-input[type="email"],
-textarea {
- background-color: rgba(0, 0, 0, 0.4);
- border: 1px solid rgba(255, 255, 255, 0.1);
- transition:
- border-color 0.2s ease,
- box-shadow 0.2s ease;
-}
-
-input[type="text"]:focus,
-input[type="number"]:focus,
-input[type="password"]:focus,
-input[type="email"]:focus,
-textarea:focus {
- border-color: rgba(99, 102, 241, 0.5);
- box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
- outline: none;
-}
-
-/* Number input - hide spinner */
-input[type="number"]::-webkit-outer-spin-button,
-input[type="number"]::-webkit-inner-spin-button {
- -webkit-appearance: none;
- margin: 0;
-}
-
-input[type="number"] {
- -moz-appearance: textfield;
-}
-
-/* ==================== Checkbox Styling ==================== */
-
-input[type="checkbox"] {
- appearance: none;
- width: 16px;
- height: 16px;
- border: 1px solid #3f3f46;
- border-radius: 4px;
- background-color: #18181b;
- cursor: pointer;
- position: relative;
- transition: all 0.15s ease;
-}
-
-input[type="checkbox"]:hover {
- border-color: #52525b;
-}
-
-input[type="checkbox"]:checked {
- background-color: #4f46e5;
- border-color: #4f46e5;
-}
-
-input[type="checkbox"]:checked::after {
- content: "";
- position: absolute;
- left: 5px;
- top: 2px;
- width: 4px;
- height: 8px;
- border: solid white;
- border-width: 0 2px 2px 0;
- transform: rotate(45deg);
-}
-
-input[type="checkbox"]:focus {
- outline: none;
- box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3);
-}
diff --git a/packages/ui/src/assets/svelte.svg b/packages/ui/src/assets/svelte.svg
deleted file mode 100644
index 8c056ce..0000000
--- a/packages/ui/src/assets/svelte.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
diff --git a/packages/ui-new/src/client.ts b/packages/ui/src/client.ts
index 18d2377..18d2377 100644
--- a/packages/ui-new/src/client.ts
+++ b/packages/ui/src/client.ts
diff --git a/packages/ui/src/components/AssistantView.svelte b/packages/ui/src/components/AssistantView.svelte
deleted file mode 100644
index 54509a5..0000000
--- a/packages/ui/src/components/AssistantView.svelte
+++ /dev/null
@@ -1,436 +0,0 @@
-<script lang="ts">
- import { assistantState } from '../stores/assistant.svelte';
- import { settingsState } from '../stores/settings.svelte';
- import { Send, Bot, RefreshCw, Trash2, AlertTriangle, Settings, Brain, ChevronDown } from 'lucide-svelte';
- import { uiState } from '../stores/ui.svelte';
- import { marked } from 'marked';
- import { onMount } from 'svelte';
-
- let input = $state('');
- let messagesContainer: HTMLDivElement | undefined = undefined;
-
- function parseMessageContent(content: string) {
- 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
- // We extract the thinking part and keep the rest (before and after)
- 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 {
- // marked.parse returns string synchronously when async is false (default)
- return marked(content, { breaks: true, gfm: true }) as string;
- } catch {
- return content;
- }
- }
-
- function scrollToBottom() {
- if (messagesContainer) {
- setTimeout(() => {
- if (messagesContainer) {
- messagesContainer.scrollTop = messagesContainer.scrollHeight;
- }
- }, 0);
- }
- }
-
- onMount(() => {
- assistantState.init();
- });
-
- // Scroll to bottom when messages change
- $effect(() => {
- // Access reactive state
- const _len = assistantState.messages.length;
- const _processing = assistantState.isProcessing;
- // Scroll on next tick
- if (_len > 0 || _processing) {
- scrollToBottom();
- }
- });
-
- async function handleSubmit() {
- if (!input.trim() || assistantState.isProcessing) return;
- const text = input;
- input = '';
- const provider = settingsState.settings.assistant.llm_provider;
- const endpoint = provider === 'ollama'
- ? settingsState.settings.assistant.ollama_endpoint
- : settingsState.settings.assistant.openai_endpoint;
- await assistantState.sendMessage(
- text,
- settingsState.settings.assistant.enabled,
- provider,
- endpoint
- );
- }
-
- function handleKeydown(e: KeyboardEvent) {
- if (e.key === 'Enter' && !e.shiftKey) {
- e.preventDefault();
- handleSubmit();
- }
- }
-
- function getProviderName(): string {
- const provider = settingsState.settings.assistant.llm_provider;
- if (provider === 'ollama') {
- return `Ollama (${settingsState.settings.assistant.ollama_model})`;
- } else if (provider === 'openai') {
- return `OpenAI (${settingsState.settings.assistant.openai_model})`;
- }
- return provider;
- }
-
- function getProviderHelpText(): string {
- const provider = settingsState.settings.assistant.llm_provider;
- if (provider === 'ollama') {
- return `Please ensure Ollama is installed and running at ${settingsState.settings.assistant.ollama_endpoint}.`;
- } else if (provider === 'openai') {
- return "Please check your OpenAI API key in Settings > AI Assistant.";
- }
- return "";
- }
-</script>
-
-<div class="h-full w-full flex flex-col gap-4 p-4 lg:p-8 animate-in fade-in zoom-in-95 duration-300">
- <div class="flex items-center justify-between mb-2">
- <div class="flex items-center gap-3">
- <div class="p-2 bg-indigo-500/20 rounded-lg text-indigo-400">
- <Bot size={24} />
- </div>
- <div>
- <h2 class="text-2xl font-bold">Game Assistant</h2>
- <p class="text-zinc-400 text-sm">Powered by {getProviderName()}</p>
- </div>
- </div>
-
- <div class="flex items-center gap-2">
- {#if !settingsState.settings.assistant.enabled}
- <div class="flex items-center gap-2 px-3 py-1.5 bg-zinc-500/10 text-zinc-400 rounded-full text-xs font-medium border border-zinc-500/20">
- <AlertTriangle size={14} />
- <span>Disabled</span>
- </div>
- {:else if !assistantState.isProviderHealthy}
- <div class="flex items-center gap-2 px-3 py-1.5 bg-red-500/10 text-red-400 rounded-full text-xs font-medium border border-red-500/20">
- <AlertTriangle size={14} />
- <span>Offline</span>
- </div>
- {:else}
- <div class="flex items-center gap-2 px-3 py-1.5 bg-emerald-500/10 text-emerald-400 rounded-full text-xs font-medium border border-emerald-500/20">
- <div class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
- <span>Online</span>
- </div>
- {/if}
-
- <button
- onclick={() => assistantState.checkHealth()}
- class="p-2 hover:bg-white/5 rounded-lg text-zinc-400 hover:text-white transition-colors"
- title="Check Connection"
- >
- <RefreshCw size={18} class={assistantState.isProcessing ? "animate-spin" : ""} />
- </button>
-
- <button
- onclick={() => assistantState.clearHistory()}
- class="p-2 hover:bg-white/5 rounded-lg text-zinc-400 hover:text-white transition-colors"
- title="Clear History"
- >
- <Trash2 size={18} />
- </button>
-
- <button
- onclick={() => uiState.setView('settings')}
- class="p-2 hover:bg-white/5 rounded-lg text-zinc-400 hover:text-white transition-colors"
- title="Settings"
- >
- <Settings size={18} />
- </button>
- </div>
- </div>
-
- <!-- Chat Area -->
- <div class="flex-1 bg-black/20 border border-white/5 rounded-xl overflow-hidden flex flex-col relative">
- {#if assistantState.messages.length === 0}
- <div class="absolute inset-0 flex flex-col items-center justify-center text-zinc-500 gap-4 p-8 text-center">
- <Bot size={48} class="opacity-20" />
- <div class="max-w-md">
- <p class="text-lg font-medium text-zinc-300 mb-2">How can I help you today?</p>
- <p class="text-sm opacity-70">I can analyze your game logs, diagnose crashes, or explain mod features.</p>
- </div>
- {#if !settingsState.settings.assistant.enabled}
- <div class="bg-zinc-500/10 border border-zinc-500/20 rounded-lg p-4 text-sm text-zinc-400 mt-4 max-w-sm">
- Assistant is disabled. Enable it in <button onclick={() => uiState.setView('settings')} class="text-indigo-400 hover:underline">Settings > AI Assistant</button>.
- </div>
- {:else if !assistantState.isProviderHealthy}
- <div class="bg-red-500/10 border border-red-500/20 rounded-lg p-4 text-sm text-red-400 mt-4 max-w-sm">
- {getProviderHelpText()}
- </div>
- {/if}
- </div>
- {/if}
-
- <div
- bind:this={messagesContainer}
- class="flex-1 overflow-y-auto p-4 space-y-4 scroll-smooth"
- >
- {#each assistantState.messages as msg, idx}
- <div class="flex gap-3 {msg.role === 'user' ? 'justify-end' : 'justify-start'}">
- {#if msg.role === 'assistant'}
- <div class="w-8 h-8 rounded-full bg-indigo-500/20 flex items-center justify-center text-indigo-400 shrink-0 mt-1">
- <Bot size={16} />
- </div>
- {/if}
-
- <div class="max-w-[80%] p-3 rounded-2xl {msg.role === 'user' ? 'bg-indigo-600 text-white rounded-tr-none' : 'bg-zinc-800/50 text-zinc-200 rounded-tl-none border border-white/5'}">
- {#if msg.role === 'user'}
- <div class="break-words whitespace-pre-wrap">
- {msg.content}
- </div>
- {:else}
- {@const parsed = parseMessageContent(msg.content)}
-
- <!-- Thinking Block -->
- {#if parsed.thinking}
- <div class="mb-3 max-w-full overflow-hidden">
- <details class="group" open={parsed.isThinking}>
- <summary class="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 size={14} />
- <span>Thinking Process</span>
- <ChevronDown size={14} class="transition-transform duration-200 group-open:rotate-180" />
- </summary>
- <div class="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}
- {#if parsed.isThinking}
- <span class="inline-block w-1.5 h-3 bg-zinc-500 ml-1 animate-pulse align-middle"></span>
- {/if}
- </div>
- </details>
- </div>
- {/if}
-
- <!-- Markdown rendered content for assistant -->
- <div class="markdown-content prose prose-invert prose-sm max-w-none">
- {#if parsed.content}
- {@html renderMarkdown(parsed.content)}
- {:else if assistantState.isProcessing && idx === assistantState.messages.length - 1 && !parsed.isThinking}
- <span class="inline-flex items-center gap-1">
- <span class="w-2 h-2 bg-zinc-400 rounded-full animate-pulse"></span>
- <span class="w-2 h-2 bg-zinc-400 rounded-full animate-pulse" style="animation-delay: 0.2s"></span>
- <span class="w-2 h-2 bg-zinc-400 rounded-full animate-pulse" style="animation-delay: 0.4s"></span>
- </span>
- {/if}
- </div>
-
- <!-- Generation Stats -->
- {#if msg.stats}
- <div class="mt-3 pt-3 border-t border-white/5 text-[10px] text-zinc-500 font-mono flex flex-wrap gap-x-4 gap-y-1 opacity-70 hover:opacity-100 transition-opacity select-none">
- <div class="flex gap-1" title="Tokens generated">
- <span>Eval:</span>
- <span class="text-zinc-400">{msg.stats.eval_count} tokens</span>
- </div>
- <div class="flex gap-1" title="Total duration">
- <span>Time:</span>
- <span class="text-zinc-400">{(msg.stats.total_duration / 1e9).toFixed(2)}s</span>
- </div>
- {#if msg.stats.eval_duration > 0}
- <div class="flex gap-1" title="Generation speed">
- <span>Speed:</span>
- <span class="text-zinc-400">{(msg.stats.eval_count / (msg.stats.eval_duration / 1e9)).toFixed(1)} t/s</span>
- </div>
- {/if}
- </div>
- {/if}
- {/if}
- </div>
- </div>
- {/each}
- </div>
-
- <!-- Input Area -->
- <div class="p-4 bg-zinc-900/50 border-t border-white/5">
- <div class="relative">
- <textarea
- bind:value={input}
- onkeydown={handleKeydown}
- placeholder={settingsState.settings.assistant.enabled ? "Ask about your game..." : "Assistant is disabled..."}
- class="w-full bg-black/20 border border-white/10 rounded-xl py-3 pl-4 pr-12 focus:outline-none focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/50 resize-none h-[52px] max-h-32 transition-all text-white disabled:opacity-50"
- disabled={assistantState.isProcessing || !settingsState.settings.assistant.enabled}
- ></textarea>
-
- <button
- onclick={handleSubmit}
- disabled={!input.trim() || assistantState.isProcessing || !settingsState.settings.assistant.enabled}
- class="absolute right-2 top-2 p-2 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:hover:bg-indigo-600 text-white rounded-lg transition-colors"
- >
- <Send size={16} />
- </button>
- </div>
- </div>
- </div>
-</div>
-
-<style>
- /* Markdown content styles */
- .markdown-content :global(p) {
- margin-bottom: 0.5rem;
- }
-
- .markdown-content :global(p:last-child) {
- margin-bottom: 0;
- }
-
- .markdown-content :global(pre) {
- background-color: rgba(0, 0, 0, 0.4);
- border-radius: 0.5rem;
- padding: 0.75rem;
- overflow-x: auto;
- margin: 0.5rem 0;
- }
-
- .markdown-content :global(code) {
- font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
- font-size: 0.85em;
- }
-
- .markdown-content :global(pre code) {
- background: none;
- padding: 0;
- }
-
- .markdown-content :global(:not(pre) > code) {
- background-color: rgba(0, 0, 0, 0.3);
- padding: 0.15rem 0.4rem;
- border-radius: 0.25rem;
- }
-
- .markdown-content :global(ul),
- .markdown-content :global(ol) {
- margin: 0.5rem 0;
- padding-left: 1.5rem;
- }
-
- .markdown-content :global(li) {
- margin: 0.25rem 0;
- }
-
- .markdown-content :global(blockquote) {
- border-left: 3px solid rgba(99, 102, 241, 0.5);
- padding-left: 1rem;
- margin: 0.5rem 0;
- color: rgba(255, 255, 255, 0.7);
- }
-
- .markdown-content :global(h1),
- .markdown-content :global(h2),
- .markdown-content :global(h3),
- .markdown-content :global(h4) {
- font-weight: 600;
- margin: 0.75rem 0 0.5rem 0;
- }
-
- .markdown-content :global(h1) {
- font-size: 1.25rem;
- }
-
- .markdown-content :global(h2) {
- font-size: 1.125rem;
- }
-
- .markdown-content :global(h3) {
- font-size: 1rem;
- }
-
- .markdown-content :global(a) {
- color: rgb(129, 140, 248);
- text-decoration: underline;
- }
-
- .markdown-content :global(a:hover) {
- color: rgb(165, 180, 252);
- }
-
- .markdown-content :global(table) {
- border-collapse: collapse;
- margin: 0.5rem 0;
- width: 100%;
- }
-
- .markdown-content :global(th),
- .markdown-content :global(td) {
- border: 1px solid rgba(255, 255, 255, 0.1);
- padding: 0.5rem;
- text-align: left;
- }
-
- .markdown-content :global(th) {
- background-color: rgba(0, 0, 0, 0.3);
- font-weight: 600;
- }
-
- .markdown-content :global(hr) {
- border: none;
- border-top: 1px solid rgba(255, 255, 255, 0.1);
- margin: 1rem 0;
- }
-
- .markdown-content :global(img) {
- max-width: 100%;
- border-radius: 0.5rem;
- }
-
- .markdown-content :global(strong) {
- font-weight: 600;
- }
-
- .markdown-content :global(em) {
- font-style: italic;
- }
-</style>
diff --git a/packages/ui/src/components/BottomBar.svelte b/packages/ui/src/components/BottomBar.svelte
deleted file mode 100644
index 19cf35d..0000000
--- a/packages/ui/src/components/BottomBar.svelte
+++ /dev/null
@@ -1,250 +0,0 @@
-<script lang="ts">
- import { invoke } from "@tauri-apps/api/core";
- import { listen, type UnlistenFn } from "@tauri-apps/api/event";
- import { authState } from "../stores/auth.svelte";
- import { gameState } from "../stores/game.svelte";
- import { uiState } from "../stores/ui.svelte";
- import { instancesState } from "../stores/instances.svelte";
- import { Terminal, ChevronDown, Play, User, Check } from 'lucide-svelte';
-
- interface InstalledVersion {
- id: string;
- type: string;
- }
-
- let isVersionDropdownOpen = $state(false);
- let dropdownRef: HTMLDivElement;
- let installedVersions = $state<InstalledVersion[]>([]);
- let isLoadingVersions = $state(true);
- let downloadCompleteUnlisten: UnlistenFn | null = null;
- let versionDeletedUnlisten: UnlistenFn | null = null;
-
- // Load installed versions on mount
- $effect(() => {
- loadInstalledVersions();
- setupEventListeners();
- return () => {
- if (downloadCompleteUnlisten) {
- downloadCompleteUnlisten();
- }
- if (versionDeletedUnlisten) {
- versionDeletedUnlisten();
- }
- };
- });
-
- async function setupEventListeners() {
- // Refresh list when a download completes
- downloadCompleteUnlisten = await listen("download-complete", () => {
- loadInstalledVersions();
- });
- // Refresh list when a version is deleted
- versionDeletedUnlisten = await listen("version-deleted", () => {
- loadInstalledVersions();
- });
- }
-
- async function loadInstalledVersions() {
- if (!instancesState.activeInstanceId) {
- installedVersions = [];
- isLoadingVersions = false;
- return;
- }
- isLoadingVersions = true;
- try {
- installedVersions = await invoke<InstalledVersion[]>("list_installed_versions", {
- instanceId: instancesState.activeInstanceId,
- });
- // If no version is selected but we have installed versions, select the first one
- if (!gameState.selectedVersion && installedVersions.length > 0) {
- gameState.selectedVersion = installedVersions[0].id;
- }
- } catch (e) {
- console.error("Failed to load installed versions:", e);
- } finally {
- isLoadingVersions = false;
- }
- }
-
- let versionOptions = $derived(
- isLoadingVersions
- ? [{ id: "loading", type: "loading", label: "Loading..." }]
- : installedVersions.length === 0
- ? [{ id: "empty", type: "empty", label: "No versions installed" }]
- : installedVersions.map(v => ({
- ...v,
- label: `${v.id}${v.type !== 'release' ? ` (${v.type})` : ''}`
- }))
- );
-
- function selectVersion(id: string) {
- if (id !== "loading" && id !== "empty") {
- gameState.selectedVersion = id;
- isVersionDropdownOpen = false;
- }
- }
-
- function handleClickOutside(e: MouseEvent) {
- if (dropdownRef && !dropdownRef.contains(e.target as Node)) {
- isVersionDropdownOpen = false;
- }
- }
-
- $effect(() => {
- if (isVersionDropdownOpen) {
- document.addEventListener('click', handleClickOutside);
- return () => document.removeEventListener('click', handleClickOutside);
- }
- });
-
- function getVersionTypeColor(type: string) {
- switch (type) {
- case 'fabric': return 'text-indigo-400';
- case 'forge': return 'text-orange-400';
- case 'snapshot': return 'text-amber-400';
- case 'modpack': return 'text-purple-400';
- default: return 'text-emerald-400';
- }
- }
-</script>
-
-<div
- class="h-20 bg-white/80 dark:bg-[#09090b]/90 border-t dark:border-white/10 border-black/5 flex items-center px-8 justify-between z-20 backdrop-blur-md"
->
- <!-- Account Area -->
- <div class="flex items-center gap-6">
- <div
- class="group flex items-center gap-4 cursor-pointer"
- onclick={() => authState.openLoginModal()}
- role="button"
- tabindex="0"
- onkeydown={(e) => e.key === "Enter" && authState.openLoginModal()}
- >
- <div
- class="w-10 h-10 rounded-sm bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 flex items-center justify-center overflow-hidden transition-all group-hover:border-zinc-400 dark:group-hover:border-zinc-500"
- >
- {#if authState.currentAccount}
- <img
- src={`https://minotar.net/avatar/${authState.currentAccount.username}/48`}
- alt={authState.currentAccount.username}
- class="w-full h-full"
- />
- {:else}
- <User size={20} class="text-zinc-400" />
- {/if}
- </div>
- <div>
- <div class="font-bold dark:text-white text-gray-900 text-sm group-hover:text-black dark:group-hover:text-zinc-200 transition-colors">
- {authState.currentAccount ? authState.currentAccount.username : "Login Account"}
- </div>
- <div class="text-[10px] uppercase tracking-wider dark:text-zinc-500 text-gray-500 flex items-center gap-2">
- {#if authState.currentAccount}
- {#if authState.currentAccount.type === "Microsoft"}
- {#if authState.currentAccount.expires_at && authState.currentAccount.expires_at * 1000 < Date.now()}
- <span class="w-1.5 h-1.5 rounded-full bg-red-500"></span>
- <span class="text-red-400">Expired</span>
- {:else}
- <span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
- Online
- {/if}
- {:else}
- <span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span>
- Offline
- {/if}
- {:else}
- <span class="w-1.5 h-1.5 rounded-full bg-zinc-400"></span>
- Guest
- {/if}
- </div>
- </div>
- </div>
-
- <div class="h-8 w-px dark:bg-white/10 bg-black/10"></div>
-
- <!-- Console Toggle -->
- <button
- class="text-xs font-mono dark:text-zinc-500 text-gray-500 dark:hover:text-white hover:text-black transition-colors flex items-center gap-2 px-2 py-1 rounded-sm hover:bg-black/5 dark:hover:bg-white/5"
- onclick={() => uiState.toggleConsole()}
- >
- <Terminal size={14} />
- {uiState.showConsole ? "HIDE LOGS" : "SHOW LOGS"}
- </button>
- </div>
-
- <!-- Action Area -->
- <div class="flex items-center gap-4">
- <div class="flex flex-col items-end mr-2">
- <!-- Custom Version Dropdown -->
- <div class="relative" bind:this={dropdownRef}>
- <button
- type="button"
- onclick={() => isVersionDropdownOpen = !isVersionDropdownOpen}
- disabled={installedVersions.length === 0 && !isLoadingVersions}
- class="flex items-center justify-between gap-2 w-56 px-4 py-2.5 text-left
- dark:bg-zinc-900 bg-zinc-50 border dark:border-zinc-700 border-zinc-300 rounded-md
- text-sm font-mono dark:text-white text-gray-900
- dark:hover:border-zinc-600 hover:border-zinc-400
- focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30
- transition-colors cursor-pointer outline-none
- disabled:opacity-50 disabled:cursor-not-allowed"
- >
- <span class="truncate">
- {#if isLoadingVersions}
- Loading...
- {:else if installedVersions.length === 0}
- No versions installed
- {:else}
- {gameState.selectedVersion || "Select version"}
- {/if}
- </span>
- <ChevronDown
- size={14}
- class="shrink-0 dark:text-zinc-500 text-zinc-400 transition-transform duration-200 {isVersionDropdownOpen ? 'rotate-180' : ''}"
- />
- </button>
-
- {#if isVersionDropdownOpen && installedVersions.length > 0}
- <div
- class="absolute z-50 w-full mt-1 py-1 dark:bg-zinc-900 bg-white border dark:border-zinc-700 border-zinc-300 rounded-md shadow-xl
- max-h-72 overflow-y-auto animate-in fade-in slide-in-from-top-1 duration-150 bottom-full mb-1 right-0"
- >
- {#each versionOptions as version}
- <button
- type="button"
- onclick={() => selectVersion(version.id)}
- disabled={version.id === "loading" || version.id === "empty"}
- class="w-full flex items-center justify-between px-3 py-2 text-sm font-mono text-left
- transition-colors outline-none
- {version.id === gameState.selectedVersion
- ? 'bg-indigo-600 text-white'
- : 'dark:text-zinc-300 text-gray-700 dark:hover:bg-zinc-800 hover:bg-zinc-100'}
- {version.id === 'loading' || version.id === 'empty' ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}"
- >
- <span class="truncate flex items-center gap-2">
- {version.id}
- {#if version.type && version.type !== 'release' && version.type !== 'loading' && version.type !== 'empty'}
- <span class="text-[10px] uppercase {version.id === gameState.selectedVersion ? 'text-white/70' : getVersionTypeColor(version.type)}">
- {version.type}
- </span>
- {/if}
- </span>
- {#if version.id === gameState.selectedVersion}
- <Check size={14} class="shrink-0 ml-2" />
- {/if}
- </button>
- {/each}
- </div>
- {/if}
- </div>
- </div>
-
- <button
- onclick={() => gameState.startGame()}
- disabled={installedVersions.length === 0 || !gameState.selectedVersion}
- class="bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 disabled:cursor-not-allowed text-white h-14 px-10 rounded-sm transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] shadow-lg shadow-emerald-500/20 flex items-center gap-3 font-bold text-lg tracking-widest uppercase"
- >
- <Play size={24} fill="currentColor" />
- <span>Launch</span>
- </button>
- </div>
-</div>
diff --git a/packages/ui/src/components/ConfigEditorModal.svelte b/packages/ui/src/components/ConfigEditorModal.svelte
deleted file mode 100644
index dd866ee..0000000
--- a/packages/ui/src/components/ConfigEditorModal.svelte
+++ /dev/null
@@ -1,369 +0,0 @@
-<script lang="ts">
- import { settingsState } from "../stores/settings.svelte";
- import { Save, X, FileJson, AlertCircle, Undo, Redo, Settings } from "lucide-svelte";
- import Prism from 'prismjs';
- import 'prismjs/components/prism-json';
- import 'prismjs/themes/prism-tomorrow.css';
-
- let content = $state(settingsState.rawConfigContent);
- let isSaving = $state(false);
- let localError = $state("");
-
- let textareaRef: HTMLTextAreaElement | undefined = $state();
- let preRef: HTMLPreElement | undefined = $state();
- let lineNumbersRef: HTMLDivElement | undefined = $state();
-
- // Textarea attributes that TypeScript doesn't recognize but are valid HTML
- const textareaAttrs = {
- autocorrect: "off",
- autocapitalize: "off"
- } as Record<string, string>;
-
- // History State
- let history = $state([settingsState.rawConfigContent]);
- let historyIndex = $state(0);
- let debounceTimer: ReturnType<typeof setTimeout> | undefined;
-
- // Editor Settings
- let showLineNumbers = $state(localStorage.getItem('editor_showLineNumbers') !== 'false');
- let showStatusBar = $state(localStorage.getItem('editor_showStatusBar') !== 'false');
- let showSettings = $state(false);
-
- // Cursor Status
- let cursorLine = $state(1);
- let cursorCol = $state(1);
-
- let lines = $derived(content.split('\n'));
-
- $effect(() => {
- localStorage.setItem('editor_showLineNumbers', String(showLineNumbers));
- localStorage.setItem('editor_showStatusBar', String(showStatusBar));
- });
-
- // Cleanup timer on destroy
- $effect(() => {
- return () => {
- if (debounceTimer) clearTimeout(debounceTimer);
- };
- });
-
- // Initial validation
- $effect(() => {
- validate(content);
- });
-
- function validate(text: string) {
- try {
- JSON.parse(text);
- localError = "";
- } catch (e: any) {
- localError = e.message;
- }
- }
-
- function pushHistory(newContent: string, immediate = false) {
- if (debounceTimer) clearTimeout(debounceTimer);
-
- const commit = () => {
- if (newContent === history[historyIndex]) return;
- const next = history.slice(0, historyIndex + 1);
- next.push(newContent);
- history = next;
- historyIndex = next.length - 1;
- };
-
- if (immediate) {
- commit();
- } else {
- debounceTimer = setTimeout(commit, 500);
- }
- }
-
- function handleUndo() {
- if (historyIndex > 0) {
- historyIndex--;
- content = history[historyIndex];
- validate(content);
- }
- }
-
- function handleRedo() {
- if (historyIndex < history.length - 1) {
- historyIndex++;
- content = history[historyIndex];
- validate(content);
- }
- }
-
- function updateCursor() {
- if (!textareaRef) return;
- const pos = textareaRef.selectionStart;
- const text = textareaRef.value.substring(0, pos);
- const lines = text.split('\n');
- cursorLine = lines.length;
- cursorCol = lines[lines.length - 1].length + 1;
- }
-
- function handleInput(e: Event) {
- const target = e.target as HTMLTextAreaElement;
- content = target.value;
- validate(content);
- pushHistory(content);
- updateCursor();
- }
-
- function handleScroll() {
- if (textareaRef) {
- if (preRef) {
- preRef.scrollTop = textareaRef.scrollTop;
- preRef.scrollLeft = textareaRef.scrollLeft;
- }
- if (lineNumbersRef) {
- lineNumbersRef.scrollTop = textareaRef.scrollTop;
- }
- }
- }
-
- let highlightedCode = $derived(
- Prism.highlight(content, Prism.languages.json, 'json') + '\n'
- );
-
- async function handleSave(close = false) {
- if (localError) return;
- isSaving = true;
- await settingsState.saveRawConfig(content, close);
- isSaving = false;
- }
-
- function handleKeydown(e: KeyboardEvent) {
- // Save
- if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
- e.preventDefault();
- handleSave(false); // Keep open on shortcut save
- }
- // Undo
- else if (e.key === 'z' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
- e.preventDefault();
- handleUndo();
- }
- // Redo (Ctrl+Shift+Z or Ctrl+Y)
- else if (
- (e.key === 'z' && (e.ctrlKey || e.metaKey) && e.shiftKey) ||
- (e.key === 'y' && (e.ctrlKey || e.metaKey))
- ) {
- e.preventDefault();
- handleRedo();
- }
- // Close
- else if (e.key === 'Escape') {
- settingsState.closeConfigEditor();
- }
- // Tab
- else if (e.key === 'Tab') {
- e.preventDefault();
- const target = e.target as HTMLTextAreaElement;
- const start = target.selectionStart;
- const end = target.selectionEnd;
-
- pushHistory(content, true);
-
- const newContent = content.substring(0, start) + " " + content.substring(end);
- content = newContent;
-
- pushHistory(content, true);
-
- setTimeout(() => {
- target.selectionStart = target.selectionEnd = start + 2;
- updateCursor();
- }, 0);
- validate(content);
- }
- }
-</script>
-
-<div class="fixed inset-0 z-[100] flex items-center justify-center backdrop-blur-sm bg-black/70 animate-in fade-in duration-200">
- <div
- class="bg-[#1d1f21] rounded-xl border border-zinc-700 shadow-2xl w-[900px] max-w-[95vw] h-[85vh] flex flex-col overflow-hidden"
- role="dialog"
- aria-modal="true"
- >
- <!-- Header -->
- <div class="flex items-center justify-between p-4 border-b border-zinc-700 bg-[#1d1f21] z-20 relative">
- <div class="flex items-center gap-3">
- <div class="p-2 bg-indigo-500/20 rounded-lg text-indigo-400">
- <FileJson size={20} />
- </div>
- <div class="flex flex-col">
- <h3 class="text-lg font-bold text-white leading-none">Configuration Editor</h3>
- <span class="text-[10px] text-zinc-500 font-mono mt-1 break-all">{settingsState.configFilePath}</span>
- </div>
- </div>
- <div class="flex items-center gap-2">
- <!-- Undo/Redo Buttons -->
- <div class="flex items-center bg-zinc-800 rounded-lg p-0.5 mr-2 border border-zinc-700">
- <button
- onclick={handleUndo}
- disabled={historyIndex === 0}
- class="p-1.5 text-zinc-400 hover:text-white hover:bg-zinc-700 rounded disabled:opacity-30 disabled:hover:bg-transparent transition-colors"
- title="Undo (Ctrl+Z)"
- >
- <Undo size={16} />
- </button>
- <button
- onclick={handleRedo}
- disabled={historyIndex === history.length - 1}
- class="p-1.5 text-zinc-400 hover:text-white hover:bg-zinc-700 rounded disabled:opacity-30 disabled:hover:bg-transparent transition-colors"
- title="Redo (Ctrl+Y)"
- >
- <Redo size={16} />
- </button>
- </div>
-
- <!-- Settings Toggle -->
- <div class="relative">
- <button
- onclick={() => showSettings = !showSettings}
- class="text-zinc-400 hover:text-white transition-colors p-2 hover:bg-white/5 rounded-lg {showSettings ? 'bg-white/10 text-white' : ''}"
- title="Editor Settings"
- >
- <Settings size={20} />
- </button>
-
- {#if showSettings}
- <div class="absolute right-0 top-full mt-2 w-48 bg-zinc-800 border border-zinc-700 rounded-lg shadow-xl p-2 z-50 flex flex-col gap-1">
- <label class="flex items-center gap-2 p-2 hover:bg-white/5 rounded cursor-pointer">
- <input type="checkbox" bind:checked={showLineNumbers} class="rounded border-zinc-600 bg-zinc-900 text-indigo-500 focus:ring-indigo-500/50" />
- <span class="text-sm text-zinc-300">Line Numbers</span>
- </label>
- <label class="flex items-center gap-2 p-2 hover:bg-white/5 rounded cursor-pointer">
- <input type="checkbox" bind:checked={showStatusBar} class="rounded border-zinc-600 bg-zinc-900 text-indigo-500 focus:ring-indigo-500/50" />
- <span class="text-sm text-zinc-300">Cursor Status</span>
- </label>
- </div>
- {/if}
- </div>
-
- <button
- onclick={() => settingsState.closeConfigEditor()}
- class="text-zinc-400 hover:text-white transition-colors p-2 hover:bg-white/5 rounded-lg"
- title="Close (Esc)"
- >
- <X size={20} />
- </button>
- </div>
- </div>
-
- <!-- Error Banner -->
- {#if localError || settingsState.configEditorError}
- <div class="bg-red-500/10 border-b border-red-500/20 p-3 flex items-start gap-3 animate-in slide-in-from-top-2 z-10 relative">
- <AlertCircle size={16} class="text-red-400 mt-0.5 shrink-0" />
- <p class="text-xs text-red-300 font-mono whitespace-pre-wrap">{localError || settingsState.configEditorError}</p>
- </div>
- {/if}
-
- <!-- Editor Body (Flex row for line numbers + code) -->
- <div class="flex-1 flex overflow-hidden relative bg-[#1d1f21]">
- <!-- Line Numbers -->
- {#if showLineNumbers}
- <div
- bind:this={lineNumbersRef}
- class="pt-4 pb-4 pr-3 pl-2 text-right text-zinc-600 bg-[#1d1f21] border-r border-zinc-700/50 font-mono select-none overflow-hidden min-w-[3rem]"
- aria-hidden="true"
- >
- {#each lines as _, i}
- <div class="leading-[20px] text-[13px]">{i + 1}</div>
- {/each}
- </div>
- {/if}
-
- <!-- Code Area -->
- <div class="flex-1 relative overflow-hidden group">
- <!-- Highlighted Code (Background) -->
- <pre
- bind:this={preRef}
- aria-hidden="true"
- class="absolute inset-0 w-full h-full p-4 m-0 bg-transparent pointer-events-none overflow-hidden whitespace-pre font-mono text-sm leading-relaxed"
- ><code class="language-json">{@html highlightedCode}</code></pre>
-
- <!-- Textarea (Foreground) -->
- <textarea
- bind:this={textareaRef}
- bind:value={content}
- oninput={handleInput}
- onkeydown={handleKeydown}
- onscroll={handleScroll}
- onmouseup={updateCursor}
- onkeyup={updateCursor}
- onclick={() => showSettings = false}
- class="absolute inset-0 w-full h-full p-4 bg-transparent text-transparent caret-white font-mono text-sm leading-relaxed resize-none focus:outline-none whitespace-pre overflow-auto z-10 selection:bg-indigo-500/30"
- spellcheck="false"
- {...textareaAttrs}
- ></textarea>
- </div>
- </div>
-
- <!-- Footer -->
- <div class="p-3 border-t border-zinc-700 bg-[#1d1f21] flex justify-between items-center z-20 relative">
- <div class="text-xs text-zinc-500 flex gap-4 items-center">
- {#if showStatusBar}
- <div class="flex gap-3 font-mono border-r border-zinc-700 pr-4 mr-1">
- <span>Ln {cursorLine}</span>
- <span>Col {cursorCol}</span>
- </div>
- {/if}
- <span class="hidden sm:inline"><span class="bg-white/10 px-1.5 py-0.5 rounded text-zinc-300">Ctrl+S</span> save</span>
- </div>
- <div class="flex gap-3">
- <button
- onclick={() => settingsState.closeConfigEditor()}
- class="px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-white rounded-lg text-sm font-medium transition-colors"
- >
- Cancel
- </button>
- <button
- onclick={() => handleSave(false)}
- disabled={isSaving || !!localError}
- class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
- title={localError ? "Fix errors before saving" : "Save changes"}
- >
- {#if isSaving}
- <div class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
- Saving...
- {:else}
- <Save size={16} />
- Save
- {/if}
- </button>
- </div>
- </div>
- </div>
-</div>
-
-<style>
- /* Ensure exact font match */
- pre, textarea {
- font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
- font-size: 13px !important;
- line-height: 20px !important;
- letter-spacing: 0px !important;
- tab-size: 2;
- }
-
- /* Hide scrollbar for pre but keep it functional for textarea */
- pre::-webkit-scrollbar {
- display: none;
- }
-
- /* Override Prism background and font weights for alignment */
- :global(pre[class*="language-"]), :global(code[class*="language-"]) {
- background: transparent !important;
- text-shadow: none !important;
- box-shadow: none !important;
- }
-
- /* CRITICAL: Force normal weight to match textarea */
- :global(.token) {
- font-weight: normal !important;
- font-style: normal !important;
- }
-</style>
diff --git a/packages/ui/src/components/CustomSelect.svelte b/packages/ui/src/components/CustomSelect.svelte
deleted file mode 100644
index 0767471..0000000
--- a/packages/ui/src/components/CustomSelect.svelte
+++ /dev/null
@@ -1,173 +0,0 @@
-<script lang="ts">
- import { ChevronDown, Check } from 'lucide-svelte';
-
- interface Option {
- value: string;
- label: string;
- disabled?: boolean;
- }
-
- interface Props {
- options: Option[];
- value: string;
- placeholder?: string;
- disabled?: boolean;
- class?: string;
- allowCustom?: boolean; // New prop to allow custom input
- onchange?: (value: string) => void;
- }
-
- let {
- options,
- value = $bindable(),
- placeholder = "Select...",
- disabled = false,
- class: className = "",
- allowCustom = false,
- onchange
- }: Props = $props();
-
- let isOpen = $state(false);
- let containerRef: HTMLDivElement;
- let customInput = $state(""); // State for custom input
-
- let selectedOption = $derived(options.find(o => o.value === value));
- // Display label: if option exists use its label, otherwise if custom is allowed use raw value, else placeholder
- let displayLabel = $derived(selectedOption ? selectedOption.label : (allowCustom && value ? value : placeholder));
-
- function toggle() {
- if (!disabled) {
- isOpen = !isOpen;
- // When opening, if current value is custom (not in options), pre-fill input
- if (isOpen && allowCustom && !selectedOption) {
- customInput = value;
- }
- }
- }
-
- function select(option: Option) {
- if (option.disabled) return;
- value = option.value;
- isOpen = false;
- onchange?.(option.value);
- }
-
- function handleCustomSubmit() {
- if (!customInput.trim()) return;
- value = customInput.trim();
- isOpen = false;
- onchange?.(value);
- }
-
- function handleKeydown(e: KeyboardEvent) {
- if (disabled) return;
-
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- toggle();
- } else if (e.key === 'Escape') {
- isOpen = false;
- } else if (e.key === 'ArrowDown' && isOpen) {
- e.preventDefault();
- const currentIndex = options.findIndex(o => o.value === value);
- const nextIndex = Math.min(currentIndex + 1, options.length - 1);
- if (!options[nextIndex].disabled) {
- value = options[nextIndex].value;
- }
- } else if (e.key === 'ArrowUp' && isOpen) {
- e.preventDefault();
- const currentIndex = options.findIndex(o => o.value === value);
- const prevIndex = Math.max(currentIndex - 1, 0);
- if (!options[prevIndex].disabled) {
- value = options[prevIndex].value;
- }
- }
- }
-
- function handleClickOutside(e: MouseEvent) {
- if (containerRef && !containerRef.contains(e.target as Node)) {
- isOpen = false;
- }
- }
-
- $effect(() => {
- if (isOpen) {
- document.addEventListener('click', handleClickOutside);
- return () => document.removeEventListener('click', handleClickOutside);
- }
- });
-</script>
-
-<div
- bind:this={containerRef}
- class="relative {className}"
->
- <!-- Trigger Button -->
- <button
- type="button"
- onclick={toggle}
- onkeydown={handleKeydown}
- {disabled}
- class="w-full flex items-center justify-between gap-2 px-3 py-2 pr-8 text-left
- bg-zinc-900 border border-zinc-700 rounded-md text-sm text-zinc-200
- hover:border-zinc-600 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30
- transition-colors cursor-pointer outline-none
- disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:border-zinc-700"
- >
- <span class="truncate {(!selectedOption && !value) ? 'text-zinc-500' : ''}">
- {displayLabel}
- </span>
- <ChevronDown
- size={14}
- class="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-500 transition-transform duration-200 {isOpen ? 'rotate-180' : ''}"
- />
- </button>
-
- <!-- Dropdown Menu -->
- {#if isOpen}
- <div
- class="absolute z-50 w-full mt-1 py-1 bg-zinc-900 border border-zinc-700 rounded-md shadow-xl
- max-h-60 overflow-y-auto animate-in fade-in slide-in-from-top-1 duration-150 flex flex-col"
- >
- {#if allowCustom}
- <div class="px-2 py-2 border-b border-zinc-700/50 mb-1">
- <div class="flex gap-2">
- <input
- type="text"
- bind:value={customInput}
- placeholder="Custom value..."
- class="flex-1 bg-black/30 border border-zinc-700 rounded px-2 py-1 text-xs text-white focus:border-indigo-500 outline-none"
- onkeydown={(e) => e.key === 'Enter' && handleCustomSubmit()}
- onclick={(e) => e.stopPropagation()}
- />
- <button
- onclick={(e) => { e.stopPropagation(); handleCustomSubmit(); }}
- class="px-2 py-1 bg-indigo-600 hover:bg-indigo-500 text-white rounded text-xs transition-colors"
- >
- Set
- </button>
- </div>
- </div>
- {/if}
-
- {#each options as option}
- <button
- type="button"
- onclick={() => select(option)}
- disabled={option.disabled}
- class="w-full flex items-center justify-between px-3 py-2 text-sm text-left
- transition-colors outline-none
- {option.value === value
- ? 'bg-indigo-600 text-white'
- : 'text-zinc-300 hover:bg-zinc-800'}
- {option.disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}"
- >
- <span class="truncate">{option.label}</span>
- {#if option.value === value}
- <Check size={14} class="shrink-0 ml-2" />
- {/if}
- </button>
- {/each}
- </div>
- {/if}
-</div>
diff --git a/packages/ui/src/components/HomeView.svelte b/packages/ui/src/components/HomeView.svelte
deleted file mode 100644
index 573d9da..0000000
--- a/packages/ui/src/components/HomeView.svelte
+++ /dev/null
@@ -1,271 +0,0 @@
-<script lang="ts">
- import { onMount } from 'svelte';
- import { gameState } from '../stores/game.svelte';
- import { releasesState } from '../stores/releases.svelte';
- import { Calendar, ExternalLink } from 'lucide-svelte';
- import { getSaturnEffect } from './ParticleBackground.svelte';
-
- type Props = {
- mouseX: number;
- mouseY: number;
- };
- let { mouseX = 0, mouseY = 0 }: Props = $props();
-
- // Saturn effect mouse interaction handlers
- function handleSaturnMouseDown(e: MouseEvent) {
- const effect = getSaturnEffect();
- if (effect) {
- effect.handleMouseDown(e.clientX);
- }
- }
-
- function handleSaturnMouseMove(e: MouseEvent) {
- const effect = getSaturnEffect();
- if (effect) {
- effect.handleMouseMove(e.clientX);
- }
- }
-
- function handleSaturnMouseUp() {
- const effect = getSaturnEffect();
- if (effect) {
- effect.handleMouseUp();
- }
- }
-
- function handleSaturnMouseLeave() {
- const effect = getSaturnEffect();
- if (effect) {
- effect.handleMouseUp();
- }
- }
-
- function handleSaturnTouchStart(e: TouchEvent) {
- if (e.touches.length === 1) {
- const effect = getSaturnEffect();
- if (effect) {
- effect.handleTouchStart(e.touches[0].clientX);
- }
- }
- }
-
- function handleSaturnTouchMove(e: TouchEvent) {
- if (e.touches.length === 1) {
- const effect = getSaturnEffect();
- if (effect) {
- effect.handleTouchMove(e.touches[0].clientX);
- }
- }
- }
-
- function handleSaturnTouchEnd() {
- const effect = getSaturnEffect();
- if (effect) {
- effect.handleTouchEnd();
- }
- }
-
- onMount(() => {
- releasesState.loadReleases();
- });
-
- function formatDate(dateString: string) {
- return new Date(dateString).toLocaleDateString(undefined, {
- year: 'numeric',
- month: 'long',
- day: 'numeric'
- });
- }
-
- function escapeHtml(unsafe: string) {
- return unsafe
- .replace(/&/g, "&amp;")
- .replace(/</g, "&lt;")
- .replace(/>/g, "&gt;")
- .replace(/"/g, "&quot;")
- .replace(/'/g, "&#039;");
- }
-
- // Enhanced markdown parser with Emoji and GitHub specific features
- function formatBody(body: string) {
- if (!body) return '';
-
- // Escape HTML first to prevent XSS
- let processed = escapeHtml(body);
-
- // Emoji map (common GitHub emojis)
- 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:': '🌐'
- };
-
- // Replace emojis
- processed = processed.replace(/:[a-z0-9_]+:/g, (match) => emojiMap[match] || match);
-
- // GitHub commit hash linking (simple version for 7-40 hex chars inside backticks)
- 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>`;
- });
-
- // Auto-link users (@user)
- 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();
-
- // Formatting helper
- 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>');
-
- // Lists
- 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>`;
- }
-
- // Headers
- 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>`;
- }
-
- // Blockquotes
- 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>`;
- }
-
- // Empty lines
- if (line === '') return '<div class="h-2"></div>';
-
- // Paragraphs
- return `<p class="mb-1.5 leading-relaxed">${formatLine(line)}</p>`;
- }).join('');
- }
-</script>
-
-<div class="absolute inset-0 z-0 overflow-hidden pointer-events-none">
- <!-- Fixed Background -->
- <div class="absolute inset-0 bg-gradient-to-t from-[#09090b] via-[#09090b]/60 to-transparent"></div>
-</div>
-
-<!-- Scrollable Container -->
-<div class="relative z-10 h-full {releasesState.isLoading ? 'overflow-hidden' : 'overflow-y-auto custom-scrollbar scroll-smooth'}">
-
- <!-- Hero Section (Full Height) - Interactive area for Saturn rotation -->
- <!-- svelte-ignore a11y_no_static_element_interactions -->
- <div
- class="min-h-full flex flex-col justify-end p-12 pb-32 cursor-grab active:cursor-grabbing select-none"
- onmousedown={handleSaturnMouseDown}
- onmousemove={handleSaturnMouseMove}
- onmouseup={handleSaturnMouseUp}
- onmouseleave={handleSaturnMouseLeave}
- ontouchstart={handleSaturnTouchStart}
- ontouchmove={handleSaturnTouchMove}
- ontouchend={handleSaturnTouchEnd}
- >
- <!-- 3D Floating Hero Text -->
- <div
- class="transition-transform duration-200 ease-out origin-bottom-left"
- style:transform={`perspective(1000px) rotateX(${mouseY * -1}deg) rotateY(${mouseX * 1}deg)`}
- >
- <div class="flex items-center gap-3 mb-6">
- <div class="h-px w-12 bg-white/50"></div>
- <span class="text-xs font-mono font-bold tracking-[0.2em] text-white/50 uppercase">Launcher Active</span>
- </div>
-
- <h1
- class="text-8xl font-black tracking-tighter text-white mb-6 leading-none"
- >
- MINECRAFT
- </h1>
-
- <div class="flex items-center gap-4">
- <div
- class="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 class="h-4 w-px bg-white/20"></div>
- <div class="text-xl font-light text-zinc-400">
- Latest Release <span class="text-white font-medium">{gameState.latestRelease?.id || '...'}</span>
- </div>
- </div>
- </div>
-
- <!-- Action Area -->
- <div class="mt-8 flex gap-4">
- <div class="text-zinc-500 text-sm font-mono">
- > Ready to launch session.
- </div>
- </div>
-
- <!-- Scroll Hint -->
- {#if !releasesState.isLoading && releasesState.releases.length > 0}
- <div class="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 class="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" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 13l5 5 5-5M7 6l5 5 5-5"/></svg>
- </div>
- {/if}
- </div>
-
- <!-- Changelog / Updates Section -->
- <div class="bg-[#09090b] relative z-20 px-12 pb-24 pt-12 border-t border-white/5 min-h-[50vh]">
- <div class="max-w-4xl">
- <h2 class="text-2xl font-bold text-white mb-10 flex items-center gap-3">
- <span class="w-1.5 h-8 bg-emerald-500 rounded-sm"></span>
- LATEST UPDATES
- </h2>
-
- {#if releasesState.isLoading}
- <div class="flex flex-col gap-8">
- {#each Array(3) as _}
- <div class="h-48 bg-white/5 rounded-sm animate-pulse border border-white/5"></div>
- {/each}
- </div>
- {:else if releasesState.error}
- <div class="p-6 border border-red-500/20 bg-red-500/10 text-red-400 rounded-sm">
- Failed to load updates: {releasesState.error}
- </div>
- {:else if releasesState.releases.length === 0}
- <div class="text-zinc-500 italic">No releases found.</div>
- {:else}
- <div class="space-y-12">
- {#each releasesState.releases as release}
- <div class="group relative pl-8 border-l border-white/10 pb-4 last:pb-0 last:border-l-0">
- <!-- Timeline Dot -->
- <div class="absolute -left-[5px] 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 class="flex items-baseline gap-4 mb-3">
- <h3 class="text-xl font-bold text-white group-hover:text-emerald-400 transition-colors">
- {release.name || release.tag_name}
- </h3>
- <div class="text-xs font-mono text-zinc-500 flex items-center gap-2">
- <Calendar size={12} />
- {formatDate(release.published_at)}
- </div>
- </div>
-
- <div class="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 class="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">
- {@html formatBody(release.body)}
- </div>
- </div>
-
- <a href={release.html_url} target="_blank" class="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>
- {/each}
- </div>
- {/if}
- </div>
- </div>
-</div>
diff --git a/packages/ui/src/components/InstanceCreationModal.svelte b/packages/ui/src/components/InstanceCreationModal.svelte
deleted file mode 100644
index c54cb98..0000000
--- a/packages/ui/src/components/InstanceCreationModal.svelte
+++ /dev/null
@@ -1,485 +0,0 @@
-<script lang="ts">
- import { invoke } from "@tauri-apps/api/core";
- import { X, ChevronLeft, ChevronRight, Loader2, Search } from "lucide-svelte";
- import { instancesState } from "../stores/instances.svelte";
- import { gameState } from "../stores/game.svelte";
- import type { Version, Instance, FabricLoaderEntry, ForgeVersion } from "../types";
-
- interface Props {
- isOpen: boolean;
- onClose: () => void;
- }
-
- let { isOpen, onClose }: Props = $props();
-
- // Wizard steps: 1 = Name, 2 = Version, 3 = Mod Loader
- let currentStep = $state(1);
- let instanceName = $state("");
- let selectedVersion = $state<Version | null>(null);
- let modLoaderType = $state<"vanilla" | "fabric" | "forge">("vanilla");
- let selectedFabricLoader = $state("");
- let selectedForgeLoader = $state("");
- let creating = $state(false);
- let errorMessage = $state("");
-
- // Mod loader lists
- let fabricLoaders = $state<FabricLoaderEntry[]>([]);
- let forgeVersions = $state<ForgeVersion[]>([]);
- let loadingLoaders = $state(false);
-
- // Version list filtering
- let versionSearch = $state("");
- let versionFilter = $state<"all" | "release" | "snapshot">("release");
-
- // Filtered versions
- let filteredVersions = $derived(() => {
- let versions = gameState.versions || [];
-
- // Filter by type
- if (versionFilter !== "all") {
- versions = versions.filter((v) => v.type === versionFilter);
- }
-
- // Search filter
- if (versionSearch) {
- versions = versions.filter((v) =>
- v.id.toLowerCase().includes(versionSearch.toLowerCase())
- );
- }
-
- return versions;
- });
-
- // Fetch mod loaders when entering step 3
- async function loadModLoaders() {
- if (!selectedVersion) return;
-
- loadingLoaders = true;
- try {
- if (modLoaderType === "fabric") {
- const loaders = await invoke<FabricLoaderEntry[]>("get_fabric_loaders_for_version", {
- gameVersion: selectedVersion.id,
- });
- fabricLoaders = loaders;
- if (loaders.length > 0) {
- selectedFabricLoader = loaders[0].loader.version;
- }
- } else if (modLoaderType === "forge") {
- const versions = await invoke<ForgeVersion[]>("get_forge_versions_for_game", {
- gameVersion: selectedVersion.id,
- });
- forgeVersions = versions;
- if (versions.length > 0) {
- selectedForgeLoader = versions[0].version;
- }
- }
- } catch (err) {
- errorMessage = `Failed to load ${modLoaderType} versions: ${err}`;
- } finally {
- loadingLoaders = false;
- }
- }
-
- // Watch for mod loader type changes and load loaders
- $effect(() => {
- if (currentStep === 3 && modLoaderType !== "vanilla") {
- loadModLoaders();
- }
- });
-
- // Reset modal state
- function resetModal() {
- currentStep = 1;
- instanceName = "";
- selectedVersion = null;
- modLoaderType = "vanilla";
- selectedFabricLoader = "";
- selectedForgeLoader = "";
- creating = false;
- errorMessage = "";
- versionSearch = "";
- versionFilter = "release";
- }
-
- function handleClose() {
- if (!creating) {
- resetModal();
- onClose();
- }
- }
-
- function goToStep(step: number) {
- errorMessage = "";
- currentStep = step;
- }
-
- function validateStep1() {
- if (!instanceName.trim()) {
- errorMessage = "Please enter an instance name";
- return false;
- }
- return true;
- }
-
- function validateStep2() {
- if (!selectedVersion) {
- errorMessage = "Please select a Minecraft version";
- return false;
- }
- return true;
- }
-
- async function handleNext() {
- errorMessage = "";
-
- if (currentStep === 1) {
- if (validateStep1()) {
- goToStep(2);
- }
- } else if (currentStep === 2) {
- if (validateStep2()) {
- goToStep(3);
- }
- }
- }
-
- async function handleCreate() {
- if (!validateStep1() || !validateStep2()) return;
-
- creating = true;
- errorMessage = "";
-
- try {
- // Step 1: Create instance
- const instance: Instance = await invoke("create_instance", {
- name: instanceName.trim(),
- });
-
- // Step 2: Install vanilla version
- await invoke("install_version", {
- instanceId: instance.id,
- versionId: selectedVersion!.id,
- });
-
- // Step 3: Install mod loader if selected
- if (modLoaderType === "fabric" && selectedFabricLoader) {
- await invoke("install_fabric", {
- instanceId: instance.id,
- gameVersion: selectedVersion!.id,
- loaderVersion: selectedFabricLoader,
- });
- } else if (modLoaderType === "forge" && selectedForgeLoader) {
- await invoke("install_forge", {
- instanceId: instance.id,
- gameVersion: selectedVersion!.id,
- forgeVersion: selectedForgeLoader,
- });
- } else {
- // Update instance with vanilla version_id
- await invoke("update_instance", {
- instance: { ...instance, version_id: selectedVersion!.id },
- });
- }
-
- // Reload instances
- await instancesState.loadInstances();
-
- // Success! Close modal
- resetModal();
- onClose();
- } catch (error) {
- errorMessage = String(error);
- creating = false;
- }
- }
-</script>
-
-{#if isOpen}
- <div
- class="fixed inset-0 z-[100] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4"
- role="dialog"
- aria-modal="true"
- >
- <div
- class="bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col"
- >
- <!-- Header -->
- <div
- class="flex items-center justify-between p-6 border-b border-zinc-700"
- >
- <div>
- <h2 class="text-xl font-bold text-white">Create New Instance</h2>
- <p class="text-sm text-zinc-400 mt-1">
- Step {currentStep} of 3
- </p>
- </div>
- <button
- onclick={handleClose}
- disabled={creating}
- class="p-2 rounded-lg hover:bg-zinc-800 text-zinc-400 hover:text-white transition-colors disabled:opacity-50"
- >
- <X size={20} />
- </button>
- </div>
-
- <!-- Progress indicator -->
- <div class="flex gap-2 px-6 pt-4">
- <div
- class="flex-1 h-1 rounded-full transition-colors {currentStep >= 1
- ? 'bg-indigo-500'
- : 'bg-zinc-700'}"
- ></div>
- <div
- class="flex-1 h-1 rounded-full transition-colors {currentStep >= 2
- ? 'bg-indigo-500'
- : 'bg-zinc-700'}"
- ></div>
- <div
- class="flex-1 h-1 rounded-full transition-colors {currentStep >= 3
- ? 'bg-indigo-500'
- : 'bg-zinc-700'}"
- ></div>
- </div>
-
- <!-- Content -->
- <div class="flex-1 overflow-y-auto p-6">
- {#if currentStep === 1}
- <!-- Step 1: Name -->
- <div class="space-y-4">
- <div>
- <label
- for="instance-name"
- class="block text-sm font-medium text-white/90 mb-2"
- >Instance Name</label
- >
- <input
- id="instance-name"
- type="text"
- bind:value={instanceName}
- placeholder="My Minecraft Instance"
- class="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
- disabled={creating}
- />
- </div>
- <p class="text-xs text-zinc-400">
- Give your instance a memorable name
- </p>
- </div>
- {:else if currentStep === 2}
- <!-- Step 2: Version Selection -->
- <div class="space-y-4">
- <div class="flex gap-4">
- <div class="flex-1 relative">
- <Search
- size={16}
- class="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-500"
- />
- <input
- type="text"
- bind:value={versionSearch}
- placeholder="Search versions..."
- class="w-full pl-10 pr-4 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
- />
- </div>
- <div class="flex gap-2">
- {#each [
- { value: "all", label: "All" },
- { value: "release", label: "Release" },
- { value: "snapshot", label: "Snapshot" },
- ] as filter}
- <button
- onclick={() => {
- versionFilter = filter.value as "all" | "release" | "snapshot";
- }}
- class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {versionFilter ===
- filter.value
- ? 'bg-indigo-600 text-white'
- : 'bg-zinc-800 text-zinc-400 hover:text-white'}"
- >
- {filter.label}
- </button>
- {/each}
- </div>
- </div>
-
- <div class="max-h-96 overflow-y-auto space-y-2">
- {#each filteredVersions() as version}
- <button
- onclick={() => (selectedVersion = version)}
- class="w-full p-3 rounded-lg border transition-colors text-left {selectedVersion?.id ===
- version.id
- ? 'bg-indigo-600/20 border-indigo-500 text-white'
- : 'bg-zinc-800 border-zinc-700 text-zinc-300 hover:border-zinc-600'}"
- >
- <div class="flex items-center justify-between">
- <span class="font-medium">{version.id}</span>
- <span
- class="text-xs px-2 py-1 rounded-full {version.type ===
- 'release'
- ? 'bg-green-500/20 text-green-400'
- : 'bg-yellow-500/20 text-yellow-400'}"
- >
- {version.type}
- </span>
- </div>
- </button>
- {/each}
-
- {#if filteredVersions().length === 0}
- <div class="text-center py-8 text-zinc-500">
- No versions found
- </div>
- {/if}
- </div>
- </div>
- {:else if currentStep === 3}
- <!-- Step 3: Mod Loader -->
- <div class="space-y-4">
- <div>
- <div class="text-sm font-medium text-white/90 mb-3">
- Mod Loader Type
- </div>
- <div class="flex gap-3">
- {#each [
- { value: "vanilla", label: "Vanilla" },
- { value: "fabric", label: "Fabric" },
- { value: "forge", label: "Forge" },
- ] as loader}
- <button
- onclick={() => {
- modLoaderType = loader.value as "vanilla" | "fabric" | "forge";
- }}
- class="flex-1 px-4 py-3 rounded-lg text-sm font-medium transition-colors {modLoaderType ===
- loader.value
- ? 'bg-indigo-600 text-white'
- : 'bg-zinc-800 text-zinc-400 hover:text-white'}"
- >
- {loader.label}
- </button>
- {/each}
- </div>
- </div>
-
- {#if modLoaderType === "fabric"}
- <div>
- <label for="fabric-loader" class="block text-sm font-medium text-white/90 mb-2">
- Fabric Loader Version
- </label>
- {#if loadingLoaders}
- <div class="flex items-center gap-2 text-zinc-400">
- <Loader2 size={16} class="animate-spin" />
- Loading Fabric versions...
- </div>
- {:else if fabricLoaders.length > 0}
- <select
- id="fabric-loader"
- bind:value={selectedFabricLoader}
- class="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
- >
- {#each fabricLoaders as loader}
- <option value={loader.loader.version}>
- {loader.loader.version} {loader.loader.stable ? "(Stable)" : "(Beta)"}
- </option>
- {/each}
- </select>
- {:else}
- <p class="text-sm text-red-400">No Fabric loaders available for this version</p>
- {/if}
- </div>
- {:else if modLoaderType === "forge"}
- <div>
- <label for="forge-version" class="block text-sm font-medium text-white/90 mb-2">
- Forge Version
- </label>
- {#if loadingLoaders}
- <div class="flex items-center gap-2 text-zinc-400">
- <Loader2 size={16} class="animate-spin" />
- Loading Forge versions...
- </div>
- {:else if forgeVersions.length > 0}
- <select
- id="forge-version"
- bind:value={selectedForgeLoader}
- class="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
- >
- {#each forgeVersions as version}
- <option value={version.version}>
- {version.version}
- </option>
- {/each}
- </select>
- {:else}
- <p class="text-sm text-red-400">No Forge versions available for this version</p>
- {/if}
- </div>
- {:else if modLoaderType === "vanilla"}
- <p class="text-sm text-zinc-400">
- Create a vanilla Minecraft instance without any mod loaders
- </p>
- {/if}
- </div>
- {/if}
-
- {#if errorMessage}
- <div
- class="mt-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm"
- >
- {errorMessage}
- </div>
- {/if}
- </div>
-
- <!-- Footer -->
- <div
- class="flex items-center justify-between gap-3 p-6 border-t border-zinc-700"
- >
- <button
- onclick={() => goToStep(currentStep - 1)}
- disabled={currentStep === 1 || creating}
- class="px-4 py-2 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
- >
- <ChevronLeft size={16} />
- Back
- </button>
-
- <div class="flex gap-3">
- <button
- onclick={handleClose}
- disabled={creating}
- class="px-4 py-2 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-white transition-colors disabled:opacity-50"
- >
- Cancel
- </button>
-
- {#if currentStep < 3}
- <button
- onclick={handleNext}
- disabled={creating}
- class="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white transition-colors disabled:opacity-50 flex items-center gap-2"
- >
- Next
- <ChevronRight size={16} />
- </button>
- {:else}
- <button
- onclick={handleCreate}
- disabled={creating ||
- !instanceName.trim() ||
- !selectedVersion ||
- (modLoaderType === "fabric" && !selectedFabricLoader) ||
- (modLoaderType === "forge" && !selectedForgeLoader)}
- class="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white transition-colors disabled:opacity-50 flex items-center gap-2"
- >
- {#if creating}
- <Loader2 size={16} class="animate-spin" />
- Creating...
- {:else}
- Create Instance
- {/if}
- </button>
- {/if}
- </div>
- </div>
- </div>
- </div>
-{/if}
diff --git a/packages/ui/src/components/InstanceEditorModal.svelte b/packages/ui/src/components/InstanceEditorModal.svelte
deleted file mode 100644
index 0856d93..0000000
--- a/packages/ui/src/components/InstanceEditorModal.svelte
+++ /dev/null
@@ -1,439 +0,0 @@
-<script lang="ts">
- import { invoke } from "@tauri-apps/api/core";
- import { X, Save, Loader2, Trash2, FolderOpen } from "lucide-svelte";
- import { instancesState } from "../stores/instances.svelte";
- import { gameState } from "../stores/game.svelte";
- import { settingsState } from "../stores/settings.svelte";
- import type { Instance, FileInfo, FabricLoaderEntry, ForgeVersion } from "../types";
- import ModLoaderSelector from "./ModLoaderSelector.svelte";
-
- interface Props {
- isOpen: boolean;
- instance: Instance | null;
- onClose: () => void;
- }
-
- let { isOpen, instance, onClose }: Props = $props();
-
- // Tabs: "info" | "version" | "files" | "settings"
- let activeTab = $state<"info" | "version" | "files" | "settings">("info");
- let saving = $state(false);
- let errorMessage = $state("");
-
- // Info tab state
- let editName = $state("");
- let editNotes = $state("");
-
- // Version tab state
- let fabricLoaders = $state<FabricLoaderEntry[]>([]);
- let forgeVersions = $state<ForgeVersion[]>([]);
- let loadingVersions = $state(false);
-
- // Files tab state
- let selectedFileFolder = $state<"mods" | "resourcepacks" | "shaderpacks" | "saves" | "screenshots">("mods");
- let fileList = $state<FileInfo[]>([]);
- let loadingFiles = $state(false);
- let deletingPath = $state<string | null>(null);
-
- // Settings tab state
- let editMemoryMin = $state(0);
- let editMemoryMax = $state(0);
- let editJavaArgs = $state("");
-
- // Initialize form when instance changes
- $effect(() => {
- if (isOpen && instance) {
- editName = instance.name;
- editNotes = instance.notes || "";
- editMemoryMin = instance.memory_override?.min || settingsState.settings.min_memory || 512;
- editMemoryMax = instance.memory_override?.max || settingsState.settings.max_memory || 2048;
- editJavaArgs = instance.jvm_args_override || "";
- errorMessage = "";
- }
- });
-
- // Load files when switching to files tab
- $effect(() => {
- if (isOpen && instance && activeTab === "files") {
- loadFileList();
- }
- });
-
- // Load file list for selected folder
- async function loadFileList() {
- if (!instance) return;
-
- loadingFiles = true;
- try {
- const files = await invoke<FileInfo[]>("list_instance_directory", {
- instanceId: instance.id,
- folder: selectedFileFolder,
- });
- fileList = files;
- } catch (err) {
- errorMessage = `Failed to load files: ${err}`;
- fileList = [];
- } finally {
- loadingFiles = false;
- }
- }
-
- // Change selected folder and reload
- async function changeFolder(folder: "mods" | "resourcepacks" | "shaderpacks" | "saves" | "screenshots") {
- selectedFileFolder = folder;
- await loadFileList();
- }
-
- // Delete a file or directory
- async function deleteFile(filePath: string) {
- if (!confirm(`Are you sure you want to delete "${filePath.split("/").pop()}"?`)) {
- return;
- }
-
- deletingPath = filePath;
- try {
- await invoke("delete_instance_file", { path: filePath });
- // Reload file list
- await loadFileList();
- } catch (err) {
- errorMessage = `Failed to delete file: ${err}`;
- } finally {
- deletingPath = null;
- }
- }
-
- // Open file in system explorer
- async function openInExplorer(filePath: string) {
- try {
- await invoke("open_file_explorer", { path: filePath });
- } catch (err) {
- errorMessage = `Failed to open file explorer: ${err}`;
- }
- }
-
- // Save instance changes
- async function saveChanges() {
- if (!instance) return;
- if (!editName.trim()) {
- errorMessage = "Instance name cannot be empty";
- return;
- }
-
- saving = true;
- errorMessage = "";
-
- try {
- const updatedInstance: Instance = {
- ...instance,
- name: editName.trim(),
- notes: editNotes.trim() || undefined,
- memory_override: {
- min: editMemoryMin,
- max: editMemoryMax,
- },
- jvm_args_override: editJavaArgs.trim() || undefined,
- };
-
- await instancesState.updateInstance(updatedInstance);
- onClose();
- } catch (err) {
- errorMessage = `Failed to save instance: ${err}`;
- } finally {
- saving = false;
- }
- }
-
- function formatFileSize(bytes: number): string {
- if (bytes === 0) return "0 B";
- const k = 1024;
- const sizes = ["B", "KB", "MB", "GB"];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
- }
-
- function formatDate(timestamp: number): string {
- return new Date(timestamp * 1000).toLocaleDateString();
- }
-</script>
-
-{#if isOpen && instance}
- <div
- class="fixed inset-0 z-[100] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4"
- role="dialog"
- aria-modal="true"
- >
- <div
- class="bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col"
- >
- <!-- Header -->
- <div class="flex items-center justify-between p-6 border-b border-zinc-700">
- <div>
- <h2 class="text-xl font-bold text-white">Edit Instance</h2>
- <p class="text-sm text-zinc-400 mt-1">{instance.name}</p>
- </div>
- <button
- onclick={onClose}
- disabled={saving}
- class="p-2 rounded-lg hover:bg-zinc-800 text-zinc-400 hover:text-white transition-colors disabled:opacity-50"
- >
- <X size={20} />
- </button>
- </div>
-
- <!-- Tab Navigation -->
- <div class="flex gap-1 px-6 pt-4 border-b border-zinc-700">
- {#each [
- { id: "info", label: "Info" },
- { id: "version", label: "Version" },
- { id: "files", label: "Files" },
- { id: "settings", label: "Settings" },
- ] as tab}
- <button
- onclick={() => (activeTab = tab.id as any)}
- class="px-4 py-2 text-sm font-medium transition-colors rounded-t-lg {activeTab === tab.id
- ? 'bg-indigo-600 text-white'
- : 'bg-zinc-800 text-zinc-400 hover:text-white'}"
- >
- {tab.label}
- </button>
- {/each}
- </div>
-
- <!-- Content Area -->
- <div class="flex-1 overflow-y-auto p-6">
- {#if activeTab === "info"}
- <!-- Info Tab -->
- <div class="space-y-4">
- <div>
- <label for="instance-name" class="block text-sm font-medium text-white/90 mb-2">
- Instance Name
- </label>
- <input
- id="instance-name"
- type="text"
- bind:value={editName}
- class="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
- disabled={saving}
- />
- </div>
-
- <div>
- <label for="instance-notes" class="block text-sm font-medium text-white/90 mb-2">
- Notes
- </label>
- <textarea
- id="instance-notes"
- bind:value={editNotes}
- rows="4"
- placeholder="Add notes about this instance..."
- class="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 resize-none"
- disabled={saving}
- ></textarea>
- </div>
-
- <div class="grid grid-cols-2 gap-4 text-sm">
- <div class="p-3 bg-zinc-800 rounded-lg">
- <p class="text-zinc-400">Created</p>
- <p class="text-white font-medium">{formatDate(instance.created_at)}</p>
- </div>
- <div class="p-3 bg-zinc-800 rounded-lg">
- <p class="text-zinc-400">Last Played</p>
- <p class="text-white font-medium">
- {instance.last_played ? formatDate(instance.last_played) : "Never"}
- </p>
- </div>
- <div class="p-3 bg-zinc-800 rounded-lg">
- <p class="text-zinc-400">Game Directory</p>
- <p class="text-white font-medium text-xs truncate" title={instance.game_dir}>
- {instance.game_dir.split("/").pop()}
- </p>
- </div>
- <div class="p-3 bg-zinc-800 rounded-lg">
- <p class="text-zinc-400">Current Version</p>
- <p class="text-white font-medium">{instance.version_id || "None"}</p>
- </div>
- </div>
- </div>
- {:else if activeTab === "version"}
- <!-- Version Tab -->
- <div class="space-y-4">
- {#if instance.version_id}
- <div class="p-4 bg-indigo-500/10 border border-indigo-500/30 rounded-lg">
- <p class="text-sm text-indigo-400">
- Currently playing: <span class="font-medium">{instance.version_id}</span>
- {#if instance.mod_loader}
- with <span class="capitalize">{instance.mod_loader}</span>
- {instance.mod_loader_version && `${instance.mod_loader_version}`}
- {/if}
- </p>
- </div>
- {/if}
-
- <div>
- <h3 class="text-sm font-medium text-white/90 mb-4">Change Version or Mod Loader</h3>
- <ModLoaderSelector
- selectedGameVersion={instance.version_id || ""}
- onInstall={(versionId) => {
- // Version installed, update instance version_id
- instance.version_id = versionId;
- saveChanges();
- }}
- />
- </div>
- </div>
- {:else if activeTab === "files"}
- <!-- Files Tab -->
- <div class="space-y-4">
- <div class="flex gap-2 flex-wrap">
- {#each ["mods", "resourcepacks", "shaderpacks", "saves", "screenshots"] as folder}
- <button
- onclick={() => changeFolder(folder as any)}
- class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors {selectedFileFolder ===
- folder
- ? "bg-indigo-600 text-white"
- : "bg-zinc-800 text-zinc-400 hover:text-white"}"
- >
- {folder}
- </button>
- {/each}
- </div>
-
- {#if loadingFiles}
- <div class="flex items-center gap-2 text-zinc-400 py-8 justify-center">
- <Loader2 size={16} class="animate-spin" />
- Loading files...
- </div>
- {:else if fileList.length === 0}
- <div class="text-center py-8 text-zinc-500">
- No files in this folder
- </div>
- {:else}
- <div class="space-y-2">
- {#each fileList as file}
- <div
- class="flex items-center justify-between p-3 bg-zinc-800 rounded-lg hover:bg-zinc-700 transition-colors"
- >
- <div class="flex-1 min-w-0">
- <p class="font-medium text-white truncate">{file.name}</p>
- <p class="text-xs text-zinc-400">
- {file.is_directory ? "Folder" : formatFileSize(file.size)}
- • {formatDate(file.modified)}
- </p>
- </div>
- <div class="flex gap-2 ml-4">
- <button
- onclick={() => openInExplorer(file.path)}
- title="Open in explorer"
- class="p-2 rounded-lg hover:bg-zinc-600 text-zinc-400 hover:text-white transition-colors"
- >
- <FolderOpen size={16} />
- </button>
- <button
- onclick={() => deleteFile(file.path)}
- disabled={deletingPath === file.path}
- title="Delete"
- class="p-2 rounded-lg hover:bg-red-600/20 text-red-400 hover:text-red-300 transition-colors disabled:opacity-50"
- >
- {#if deletingPath === file.path}
- <Loader2 size={16} class="animate-spin" />
- {:else}
- <Trash2 size={16} />
- {/if}
- </button>
- </div>
- </div>
- {/each}
- </div>
- {/if}
- </div>
- {:else if activeTab === "settings"}
- <!-- Settings Tab -->
- <div class="space-y-4">
- <div>
- <label for="min-memory" class="block text-sm font-medium text-white/90 mb-2">
- Minimum Memory (MB)
- </label>
- <input
- id="min-memory"
- type="number"
- bind:value={editMemoryMin}
- min="256"
- max={editMemoryMax}
- class="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
- disabled={saving}
- />
- <p class="text-xs text-zinc-400 mt-1">
- Default: {settingsState.settings.min_memory}MB
- </p>
- </div>
-
- <div>
- <label for="max-memory" class="block text-sm font-medium text-white/90 mb-2">
- Maximum Memory (MB)
- </label>
- <input
- id="max-memory"
- type="number"
- bind:value={editMemoryMax}
- min={editMemoryMin}
- max="16384"
- class="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
- disabled={saving}
- />
- <p class="text-xs text-zinc-400 mt-1">
- Default: {settingsState.settings.max_memory}MB
- </p>
- </div>
-
- <div>
- <label for="java-args" class="block text-sm font-medium text-white/90 mb-2">
- JVM Arguments (Advanced)
- </label>
- <textarea
- id="java-args"
- bind:value={editJavaArgs}
- rows="4"
- placeholder="-XX:+UnlockExperimentalVMOptions -XX:G1NewCollectionPercentage=20..."
- class="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 font-mono text-sm resize-none"
- disabled={saving}
- ></textarea>
- <p class="text-xs text-zinc-400 mt-1">
- Leave empty to use global Java arguments
- </p>
- </div>
- </div>
- {/if}
-
- {#if errorMessage}
- <div class="mt-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
- {errorMessage}
- </div>
- {/if}
- </div>
-
- <!-- Footer -->
- <div class="flex items-center justify-end gap-3 p-6 border-t border-zinc-700">
- <button
- onclick={onClose}
- disabled={saving}
- class="px-4 py-2 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-white transition-colors disabled:opacity-50"
- >
- Cancel
- </button>
- <button
- onclick={saveChanges}
- disabled={saving || !editName.trim()}
- class="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white transition-colors disabled:opacity-50 flex items-center gap-2"
- >
- {#if saving}
- <Loader2 size={16} class="animate-spin" />
- Saving...
- {:else}
- <Save size={16} />
- Save Changes
- {/if}
- </button>
- </div>
- </div>
- </div>
-{/if}
diff --git a/packages/ui/src/components/InstancesView.svelte b/packages/ui/src/components/InstancesView.svelte
deleted file mode 100644
index 5334f9e..0000000
--- a/packages/ui/src/components/InstancesView.svelte
+++ /dev/null
@@ -1,259 +0,0 @@
-<script lang="ts">
- import { onMount } from "svelte";
- import { instancesState } from "../stores/instances.svelte";
- import { Plus, Trash2, Edit2, Copy, Check, X } from "lucide-svelte";
- import type { Instance } from "../types";
- import InstanceCreationModal from "./InstanceCreationModal.svelte";
- import InstanceEditorModal from "./InstanceEditorModal.svelte";
-
- let showCreateModal = $state(false);
- let showEditModal = $state(false);
- let showDeleteConfirm = $state(false);
- let showDuplicateModal = $state(false);
- let selectedInstance: Instance | null = $state(null);
- let editingInstance: Instance | null = $state(null);
- let newInstanceName = $state("");
- let duplicateName = $state("");
-
- onMount(() => {
- instancesState.loadInstances();
- });
-
- function handleCreate() {
- showCreateModal = true;
- }
-
- function handleEdit(instance: Instance) {
- editingInstance = instance;
- }
-
- function handleDelete(instance: Instance) {
- selectedInstance = instance;
- showDeleteConfirm = true;
- }
-
- function handleDuplicate(instance: Instance) {
- selectedInstance = instance;
- duplicateName = `${instance.name} (Copy)`;
- showDuplicateModal = true;
- }
-
- async function confirmDelete() {
- if (!selectedInstance) return;
- await instancesState.deleteInstance(selectedInstance.id);
- showDeleteConfirm = false;
- selectedInstance = null;
- }
-
- async function confirmDuplicate() {
- if (!selectedInstance || !duplicateName.trim()) return;
- await instancesState.duplicateInstance(selectedInstance.id, duplicateName.trim());
- showDuplicateModal = false;
- selectedInstance = null;
- duplicateName = "";
- }
-
- function formatDate(timestamp: number): string {
- return new Date(timestamp * 1000).toLocaleDateString();
- }
-
- function 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();
- }
-</script>
-
-<div class="h-full flex flex-col gap-4 p-6 overflow-y-auto">
- <div class="flex items-center justify-between">
- <h1 class="text-2xl font-bold text-gray-900 dark:text-white">Instances</h1>
- <button
- onclick={handleCreate}
- class="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
- >
- <Plus size={18} />
- Create Instance
- </button>
- </div>
-
- {#if instancesState.instances.length === 0}
- <div class="flex-1 flex items-center justify-center">
- <div class="text-center text-gray-500 dark:text-gray-400">
- <p class="text-lg mb-2">No instances yet</p>
- <p class="text-sm">Create your first instance to get started</p>
- </div>
- </div>
- {:else}
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
- {#each instancesState.instances as instance (instance.id)}
- <div
- role="button"
- tabindex="0"
- class="relative p-4 bg-gray-100 dark:bg-gray-800 rounded-lg border-2 transition-all cursor-pointer hover:border-blue-500 {instancesState.activeInstanceId === instance.id
- ? 'border-blue-500'
- : 'border-transparent'}"
- onclick={() => instancesState.setActiveInstance(instance.id)}
- onkeydown={(e) => e.key === "Enter" && instancesState.setActiveInstance(instance.id)}
- >
- {#if instancesState.activeInstanceId === instance.id}
- <div class="absolute top-2 right-2">
- <div class="w-3 h-3 bg-blue-500 rounded-full"></div>
- </div>
- {/if}
-
- <div class="flex items-start justify-between mb-2">
- <h3 class="text-lg font-semibold text-gray-900 dark:text-white">
- {instance.name}
- </h3>
- <div class="flex gap-1">
- <button
- type="button"
- onclick={(e) => {
- e.stopPropagation();
- handleEdit(instance);
- }}
- class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors"
- title="Edit"
- >
- <Edit2 size={16} class="text-gray-600 dark:text-gray-400" />
- </button>
- <button
- type="button"
- onclick={(e) => {
- e.stopPropagation();
- handleDuplicate(instance);
- }}
- class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors"
- title="Duplicate"
- >
- <Copy size={16} class="text-gray-600 dark:text-gray-400" />
- </button>
- <button
- type="button"
- onclick={(e) => {
- e.stopPropagation();
- handleDelete(instance);
- }}
- class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors"
- title="Delete"
- >
- <Trash2 size={16} class="text-red-600 dark:text-red-400" />
- </button>
- </div>
- </div>
-
- <div class="space-y-1 text-sm text-gray-600 dark:text-gray-400">
- {#if instance.version_id}
- <p>Version: <span class="font-medium">{instance.version_id}</span></p>
- {:else}
- <p class="text-gray-400">No version selected</p>
- {/if}
-
- {#if instance.mod_loader && instance.mod_loader !== "vanilla"}
- <p>
- Mod Loader: <span class="font-medium capitalize">{instance.mod_loader}</span>
- {#if instance.mod_loader_version}
- <span class="text-gray-500">({instance.mod_loader_version})</span>
- {/if}
- </p>
- {/if}
-
- <p>Created: {formatDate(instance.created_at)}</p>
-
- {#if instance.last_played}
- <p>Last played: {formatLastPlayed(instance.last_played)}</p>
- {/if}
- </div>
-
- {#if instance.notes}
- <p class="mt-2 text-sm text-gray-500 dark:text-gray-500 italic">
- {instance.notes}
- </p>
- {/if}
- </div>
- {/each}
- </div>
- {/if}
-</div>
-
-<!-- Create Modal -->
-<InstanceCreationModal isOpen={showCreateModal} onClose={() => (showCreateModal = false)} />
-
-<!-- Instance Editor Modal -->
-<InstanceEditorModal
- isOpen={editingInstance !== null}
- instance={editingInstance}
- onClose={() => {
- editingInstance = null;
- }}
-/>
-
-<!-- Delete Confirmation -->
-{#if showDeleteConfirm && selectedInstance}
- <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
- <div class="bg-white dark:bg-gray-800 rounded-lg p-6 w-96">
- <h2 class="text-xl font-bold mb-4 text-red-600 dark:text-red-400">Delete Instance</h2>
- <p class="mb-4 text-gray-700 dark:text-gray-300">
- Are you sure you want to delete "{selectedInstance.name}"? This action cannot be undone and will delete all game data for this instance.
- </p>
- <div class="flex gap-2 justify-end">
- <button
- onclick={() => {
- showDeleteConfirm = false;
- selectedInstance = null;
- }}
- class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
- >
- Cancel
- </button>
- <button
- onclick={confirmDelete}
- class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
- >
- Delete
- </button>
- </div>
- </div>
- </div>
-{/if}
-
-<!-- Duplicate Modal -->
-{#if showDuplicateModal && selectedInstance}
- <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
- <div class="bg-white dark:bg-gray-800 rounded-lg p-6 w-96">
- <h2 class="text-xl font-bold mb-4 text-gray-900 dark:text-white">Duplicate Instance</h2>
- <input
- type="text"
- bind:value={duplicateName}
- placeholder="New instance name"
- class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white mb-4"
- onkeydown={(e) => e.key === "Enter" && confirmDuplicate()}
- />
- <div class="flex gap-2 justify-end">
- <button
- onclick={() => {
- showDuplicateModal = false;
- selectedInstance = null;
- duplicateName = "";
- }}
- class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
- >
- Cancel
- </button>
- <button
- onclick={confirmDuplicate}
- disabled={!duplicateName.trim()}
- class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
- >
- Duplicate
- </button>
- </div>
- </div>
- </div>
-{/if}
diff --git a/packages/ui/src/components/LoginModal.svelte b/packages/ui/src/components/LoginModal.svelte
deleted file mode 100644
index 1886cd9..0000000
--- a/packages/ui/src/components/LoginModal.svelte
+++ /dev/null
@@ -1,126 +0,0 @@
-<script lang="ts">
- import { open } from "@tauri-apps/plugin-shell";
- import { authState } from "../stores/auth.svelte";
-
- function openLink(url: string) {
- open(url);
- }
-</script>
-
-{#if authState.isLoginModalOpen}
- <div
- class="fixed inset-0 z-[60] flex items-center justify-center bg-white/80 dark:bg-black/80 backdrop-blur-sm p-4"
- >
- <div
- class="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-xl shadow-2xl p-6 w-full max-w-md animate-in fade-in zoom-in-0 duration-200"
- >
- <div class="flex justify-between items-center mb-6">
- <h2 class="text-2xl font-bold text-gray-900 dark:text-white">Login</h2>
- <button
- onclick={() => authState.closeLoginModal()}
- class="text-zinc-500 hover:text-black dark:hover:text-white transition group"
- >
- ✕
- </button>
- </div>
-
- {#if authState.loginMode === "select"}
- <div class="space-y-4">
- <button
- onclick={() => authState.startMicrosoftLogin()}
- class="w-full flex items-center justify-center gap-3 bg-gray-100 hover:bg-gray-200 dark:bg-[#2F2F2F] dark:hover:bg-[#3F3F3F] text-gray-900 dark:text-white p-4 rounded-lg font-bold border border-transparent hover:border-zinc-400 dark:hover:border-zinc-500 transition-all group"
- >
- <!-- Microsoft Logo SVG -->
- <svg
- class="w-5 h-5"
- viewBox="0 0 23 23"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- ><path fill="#f35325" d="M1 1h10v10H1z" /><path
- fill="#81bc06"
- d="M12 1h10v10H12z"
- /><path fill="#05a6f0" d="M1 12h10v10H1z" /><path
- fill="#ffba08"
- d="M12 12h10v10H12z"
- /></svg
- >
- Microsoft Account
- </button>
-
- <div class="relative py-2">
- <div class="absolute inset-0 flex items-center">
- <div class="w-full border-t border-zinc-200 dark:border-zinc-700"></div>
- </div>
- <div class="relative flex justify-center text-xs uppercase">
- <span class="bg-white dark:bg-zinc-900 px-2 text-zinc-400 dark:text-zinc-500">OR</span>
- </div>
- </div>
-
- <div class="space-y-2">
- <input
- type="text"
- bind:value={authState.offlineUsername}
- placeholder="Offline Username"
- class="w-full bg-gray-50 border-zinc-200 dark:bg-zinc-950 dark:border-zinc-700 rounded p-3 text-gray-900 dark:text-white focus:border-indigo-500 outline-none"
- onkeydown={(e) => e.key === "Enter" && authState.performOfflineLogin()}
- />
- <button
- onclick={() => authState.performOfflineLogin()}
- class="w-full bg-gray-200 hover:bg-gray-300 dark:bg-zinc-800 dark:hover:bg-zinc-700 text-gray-700 dark:text-zinc-300 p-3 rounded font-medium transition-colors"
- >
- Offline Login
- </button>
- </div>
- </div>
- {:else if authState.loginMode === "microsoft"}
- <div class="text-center">
- {#if authState.msLoginLoading && !authState.deviceCodeData}
- <div class="py-8 text-zinc-400 animate-pulse">
- Starting login flow...
- </div>
- {:else if authState.deviceCodeData}
- <div class="space-y-4">
- <p class="text-sm text-gray-500 dark:text-zinc-400">1. Go to this URL:</p>
- <button
- onclick={() =>
- authState.deviceCodeData && openLink(authState.deviceCodeData.verification_uri)}
- class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-500 dark:hover:text-indigo-300 underline break-all font-mono text-sm"
- >
- {authState.deviceCodeData.verification_uri}
- </button>
-
- <p class="text-sm text-gray-500 dark:text-zinc-400 mt-2">2. Enter this code:</p>
- <div
- class="bg-gray-50 dark:bg-zinc-950 p-4 rounded border border-zinc-200 dark:border-zinc-700 font-mono text-2xl tracking-widest text-center select-all cursor-pointer hover:border-indigo-500 transition-colors dark:text-white text-gray-900"
- role="button"
- tabindex="0"
- onkeydown={(e) => e.key === 'Enter' && navigator.clipboard.writeText(authState.deviceCodeData?.user_code || "")}
- onclick={() =>
- navigator.clipboard.writeText(
- authState.deviceCodeData?.user_code || ""
- )}
- >
- {authState.deviceCodeData.user_code}
- </div>
- <p class="text-xs text-zinc-500">Click code to copy</p>
-
- <div class="pt-6 space-y-3">
- <div class="flex flex-col items-center gap-3">
- <div class="animate-spin rounded-full h-6 w-6 border-2 border-zinc-300 dark:border-zinc-600 border-t-indigo-500"></div>
- <span class="text-sm text-gray-600 dark:text-zinc-400 font-medium break-all text-center">{authState.msLoginStatus}</span>
- </div>
- <p class="text-xs text-zinc-600">This window will update automatically.</p>
- </div>
-
- <button
- onclick={() => { authState.stopPolling(); authState.loginMode = "select"; }}
- class="text-xs text-zinc-500 hover:text-zinc-300 mt-6 underline"
- >Cancel</button
- >
- </div>
- {/if}
- </div>
- {/if}
- </div>
- </div>
-{/if}
diff --git a/packages/ui/src/components/ModLoaderSelector.svelte b/packages/ui/src/components/ModLoaderSelector.svelte
deleted file mode 100644
index 50caa8c..0000000
--- a/packages/ui/src/components/ModLoaderSelector.svelte
+++ /dev/null
@@ -1,455 +0,0 @@
-<script lang="ts">
- import { invoke } from "@tauri-apps/api/core";
- import { listen, type UnlistenFn } from "@tauri-apps/api/event";
- import type {
- FabricGameVersion,
- FabricLoaderVersion,
- ForgeVersion,
- ModLoaderType,
- } from "../types";
- import { Loader2, Download, AlertCircle, Check, ChevronDown, CheckCircle } from 'lucide-svelte';
- import { logsState } from "../stores/logs.svelte";
- import { instancesState } from "../stores/instances.svelte";
-
- interface Props {
- selectedGameVersion: string;
- onInstall: (versionId: string) => void;
- }
-
- let { selectedGameVersion, onInstall }: Props = $props();
-
- // State
- let selectedLoader = $state<ModLoaderType>("vanilla");
- let isLoading = $state(false);
- let isInstalling = $state(false);
- let error = $state<string | null>(null);
- let isVersionInstalled = $state(false);
-
- // Fabric state
- let fabricLoaders = $state<FabricLoaderVersion[]>([]);
- let selectedFabricLoader = $state("");
- let isFabricDropdownOpen = $state(false);
-
- // Forge state
- let forgeVersions = $state<ForgeVersion[]>([]);
- let selectedForgeVersion = $state("");
- let isForgeDropdownOpen = $state(false);
-
- let fabricDropdownRef = $state<HTMLDivElement | null>(null);
- let forgeDropdownRef = $state<HTMLDivElement | null>(null);
-
- // Check if version is installed when game version changes
- $effect(() => {
- if (selectedGameVersion) {
- checkInstallStatus();
- }
- });
-
- // Load mod loader versions when game version or loader type changes
- $effect(() => {
- if (selectedGameVersion && selectedLoader !== "vanilla") {
- loadModLoaderVersions();
- }
- });
-
- async function checkInstallStatus() {
- if (!selectedGameVersion || !instancesState.activeInstanceId) {
- isVersionInstalled = false;
- return;
- }
- try {
- isVersionInstalled = await invoke<boolean>("check_version_installed", {
- instanceId: instancesState.activeInstanceId,
- versionId: selectedGameVersion,
- });
- } catch (e) {
- console.error("Failed to check install status:", e);
- isVersionInstalled = false;
- }
- }
-
- async function loadModLoaderVersions() {
- isLoading = true;
- error = null;
-
- try {
- if (selectedLoader === "fabric") {
- const loaders = await invoke<any[]>("get_fabric_loaders_for_version", {
- gameVersion: selectedGameVersion,
- });
- fabricLoaders = loaders.map((l) => l.loader);
- if (fabricLoaders.length > 0) {
- const stable = fabricLoaders.find((l) => l.stable);
- selectedFabricLoader = stable?.version || fabricLoaders[0].version;
- }
- } else if (selectedLoader === "forge") {
- forgeVersions = await invoke<ForgeVersion[]>(
- "get_forge_versions_for_game",
- {
- gameVersion: selectedGameVersion,
- }
- );
- if (forgeVersions.length > 0) {
- const recommended = forgeVersions.find((v) => v.recommended);
- const latest = forgeVersions.find((v) => v.latest);
- selectedForgeVersion =
- recommended?.version || latest?.version || forgeVersions[0].version;
- }
- }
- } catch (e) {
- error = `Failed to load ${selectedLoader} versions: ${e}`;
- console.error(e);
- } finally {
- isLoading = false;
- }
- }
-
- async function installVanilla() {
- if (!selectedGameVersion) {
- error = "Please select a Minecraft version first";
- return;
- }
-
- isInstalling = true;
- error = null;
- logsState.addLog("info", "Installer", `Starting installation of ${selectedGameVersion}...`);
-
- if (!instancesState.activeInstanceId) {
- error = "Please select an instance first";
- return;
- }
- try {
- await invoke("install_version", {
- instanceId: instancesState.activeInstanceId,
- versionId: selectedGameVersion,
- });
- logsState.addLog("info", "Installer", `Successfully installed ${selectedGameVersion}`);
- isVersionInstalled = true;
- onInstall(selectedGameVersion);
- } catch (e) {
- error = `Failed to install: ${e}`;
- logsState.addLog("error", "Installer", `Installation failed: ${e}`);
- console.error(e);
- } finally {
- isInstalling = false;
- }
- }
-
- async function installModLoader() {
- if (!selectedGameVersion) {
- error = "Please select a Minecraft version first";
- return;
- }
-
- if (!instancesState.activeInstanceId) {
- error = "Please select an instance first";
- isInstalling = false;
- return;
- }
-
- isInstalling = true;
- error = null;
-
- try {
- // First install the base game if not installed
- if (!isVersionInstalled) {
- logsState.addLog("info", "Installer", `Installing base game ${selectedGameVersion} first...`);
- await invoke("install_version", {
- instanceId: instancesState.activeInstanceId,
- versionId: selectedGameVersion,
- });
- isVersionInstalled = true;
- }
-
- // Then install the mod loader
- if (selectedLoader === "fabric" && selectedFabricLoader) {
- logsState.addLog("info", "Installer", `Installing Fabric ${selectedFabricLoader} for ${selectedGameVersion}...`);
- const result = await invoke<any>("install_fabric", {
- instanceId: instancesState.activeInstanceId,
- gameVersion: selectedGameVersion,
- loaderVersion: selectedFabricLoader,
- });
- logsState.addLog("info", "Installer", `Fabric installed successfully: ${result.id}`);
- onInstall(result.id);
- } else if (selectedLoader === "forge" && selectedForgeVersion) {
- logsState.addLog("info", "Installer", `Installing Forge ${selectedForgeVersion} for ${selectedGameVersion}...`);
- const result = await invoke<any>("install_forge", {
- instanceId: instancesState.activeInstanceId,
- gameVersion: selectedGameVersion,
- forgeVersion: selectedForgeVersion,
- });
- logsState.addLog("info", "Installer", `Forge installed successfully: ${result.id}`);
- onInstall(result.id);
- }
- } catch (e) {
- error = `Failed to install ${selectedLoader}: ${e}`;
- logsState.addLog("error", "Installer", `Installation failed: ${e}`);
- console.error(e);
- } finally {
- isInstalling = false;
- }
- }
-
- function onLoaderChange(loader: ModLoaderType) {
- selectedLoader = loader;
- error = null;
- if (loader !== "vanilla" && selectedGameVersion) {
- loadModLoaderVersions();
- }
- }
-
- function handleFabricClickOutside(e: MouseEvent) {
- if (fabricDropdownRef && !fabricDropdownRef.contains(e.target as Node)) {
- isFabricDropdownOpen = false;
- }
- }
-
- function handleForgeClickOutside(e: MouseEvent) {
- if (forgeDropdownRef && !forgeDropdownRef.contains(e.target as Node)) {
- isForgeDropdownOpen = false;
- }
- }
-
- $effect(() => {
- if (isFabricDropdownOpen) {
- document.addEventListener('click', handleFabricClickOutside);
- return () => document.removeEventListener('click', handleFabricClickOutside);
- }
- });
-
- $effect(() => {
- if (isForgeDropdownOpen) {
- document.addEventListener('click', handleForgeClickOutside);
- return () => document.removeEventListener('click', handleForgeClickOutside);
- }
- });
-
- let selectedFabricLabel = $derived(
- fabricLoaders.find(l => l.version === selectedFabricLoader)
- ? `${selectedFabricLoader}${fabricLoaders.find(l => l.version === selectedFabricLoader)?.stable ? ' (stable)' : ''}`
- : selectedFabricLoader || 'Select version'
- );
-
- let selectedForgeLabel = $derived(
- forgeVersions.find(v => v.version === selectedForgeVersion)
- ? `${selectedForgeVersion}${forgeVersions.find(v => v.version === selectedForgeVersion)?.recommended ? ' (Recommended)' : ''}`
- : selectedForgeVersion || 'Select version'
- );
-</script>
-
-<div class="space-y-4">
- <div class="flex items-center justify-between">
- <h3 class="text-xs font-bold uppercase tracking-widest text-zinc-500">Loader Type</h3>
- </div>
-
- <!-- Loader Type Tabs - Simple Clean Style -->
- <div class="flex p-1 bg-zinc-100 dark:bg-zinc-900/50 rounded-sm border border-zinc-200 dark:border-white/5">
- {#each ['vanilla', 'fabric', 'forge'] as loader}
- <button
- class="flex-1 px-3 py-2 rounded-sm text-sm font-medium transition-all duration-200 capitalize
- {selectedLoader === loader
- ? 'bg-white dark:bg-white/10 text-black dark:text-white shadow-sm'
- : 'text-zinc-500 dark:text-zinc-500 hover:text-black dark:hover:text-white'}"
- onclick={() => onLoaderChange(loader as ModLoaderType)}
- disabled={isInstalling}
- >
- {loader}
- </button>
- {/each}
- </div>
-
- <!-- Content Area -->
- <div class="min-h-[100px] flex flex-col justify-center">
- {#if !selectedGameVersion}
- <div class="flex items-center gap-3 p-4 bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20 text-amber-700 dark:text-amber-200 rounded-sm text-sm">
- <AlertCircle size={16} />
- <span>Please select a Minecraft version first.</span>
- </div>
-
- {:else if selectedLoader === "vanilla"}
- <div class="space-y-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
- <div class="text-center p-4 border border-dashed border-zinc-200 dark:border-white/10 rounded-sm text-zinc-500 text-sm">
- Standard Minecraft experience. No modifications.
- </div>
-
- {#if isVersionInstalled}
- <div class="flex items-center justify-center gap-2 p-3 bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20 text-emerald-700 dark:text-emerald-300 rounded-sm text-sm">
- <CheckCircle size={16} />
- <span>Version {selectedGameVersion} is installed</span>
- </div>
- {:else}
- <button
- class="w-full bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 disabled:cursor-not-allowed text-white py-2.5 px-4 rounded-sm font-bold text-sm transition-all flex items-center justify-center gap-2"
- onclick={installVanilla}
- disabled={isInstalling}
- >
- {#if isInstalling}
- <Loader2 class="animate-spin" size={16} />
- Installing...
- {:else}
- <Download size={16} />
- Install {selectedGameVersion}
- {/if}
- </button>
- {/if}
- </div>
-
- {:else if isLoading}
- <div class="flex flex-col items-center gap-3 text-sm text-zinc-500 py-6">
- <Loader2 class="animate-spin" size={20} />
- <span>Fetching {selectedLoader} manifest...</span>
- </div>
-
- {:else if error}
- <div class="p-4 bg-red-50 border border-red-200 text-red-700 dark:bg-red-500/10 dark:border-red-500/20 dark:text-red-300 rounded-sm text-sm break-words">
- {error}
- </div>
-
- {:else if selectedLoader === "fabric"}
- <div class="space-y-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
- {#if fabricLoaders.length === 0}
- <div class="text-center p-4 text-sm text-zinc-500 italic">
- No Fabric versions available for {selectedGameVersion}
- </div>
- {:else}
- <div>
- <label for="fabric-loader-select" class="block text-[10px] uppercase font-bold text-zinc-500 mb-2"
- >Loader Version</label
- >
- <!-- Custom Fabric Dropdown -->
- <div class="relative" bind:this={fabricDropdownRef}>
- <button
- type="button"
- onclick={() => isFabricDropdownOpen = !isFabricDropdownOpen}
- disabled={isInstalling}
- class="w-full flex items-center justify-between gap-2 px-4 py-2.5 text-left
- bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md
- text-sm text-gray-900 dark:text-white
- hover:border-zinc-400 dark:hover:border-zinc-600
- focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30
- transition-colors cursor-pointer outline-none disabled:opacity-50"
- >
- <span class="truncate">{selectedFabricLabel}</span>
- <ChevronDown
- size={14}
- class="shrink-0 text-zinc-400 dark:text-zinc-500 transition-transform duration-200 {isFabricDropdownOpen ? 'rotate-180' : ''}"
- />
- </button>
-
- {#if isFabricDropdownOpen}
- <div
- class="absolute z-50 w-full mt-1 py-1 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md shadow-xl
- max-h-48 overflow-y-auto animate-in fade-in slide-in-from-top-1 duration-150"
- >
- {#each fabricLoaders as loader}
- <button
- type="button"
- onclick={() => { selectedFabricLoader = loader.version; isFabricDropdownOpen = false; }}
- class="w-full flex items-center justify-between px-3 py-2 text-sm text-left
- transition-colors outline-none cursor-pointer
- {loader.version === selectedFabricLoader
- ? 'bg-indigo-600 text-white'
- : 'text-gray-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800'}"
- >
- <span class="truncate">{loader.version} {loader.stable ? "(stable)" : ""}</span>
- {#if loader.version === selectedFabricLoader}
- <Check size={14} class="shrink-0 ml-2" />
- {/if}
- </button>
- {/each}
- </div>
- {/if}
- </div>
- </div>
-
- <button
- class="w-full bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed text-white py-2.5 px-4 rounded-sm font-bold text-sm transition-all flex items-center justify-center gap-2"
- onclick={installModLoader}
- disabled={isInstalling || !selectedFabricLoader}
- >
- {#if isInstalling}
- <Loader2 class="animate-spin" size={16} />
- Installing...
- {:else}
- <Download size={16} />
- Install Fabric {selectedFabricLoader}
- {/if}
- </button>
- {/if}
- </div>
-
- {:else if selectedLoader === "forge"}
- <div class="space-y-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
- {#if forgeVersions.length === 0}
- <div class="text-center p-4 text-sm text-zinc-500 italic">
- No Forge versions available for {selectedGameVersion}
- </div>
- {:else}
- <div>
- <label for="forge-version-select" class="block text-[10px] uppercase font-bold text-zinc-500 mb-2"
- >Forge Version</label
- >
- <!-- Custom Forge Dropdown -->
- <div class="relative" bind:this={forgeDropdownRef}>
- <button
- type="button"
- onclick={() => isForgeDropdownOpen = !isForgeDropdownOpen}
- disabled={isInstalling}
- class="w-full flex items-center justify-between gap-2 px-4 py-2.5 text-left
- bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md
- text-sm text-gray-900 dark:text-white
- hover:border-zinc-400 dark:hover:border-zinc-600
- focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30
- transition-colors cursor-pointer outline-none disabled:opacity-50"
- >
- <span class="truncate">{selectedForgeLabel}</span>
- <ChevronDown
- size={14}
- class="shrink-0 text-zinc-400 dark:text-zinc-500 transition-transform duration-200 {isForgeDropdownOpen ? 'rotate-180' : ''}"
- />
- </button>
-
- {#if isForgeDropdownOpen}
- <div
- class="absolute z-50 w-full mt-1 py-1 bg-white dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md shadow-xl
- max-h-48 overflow-y-auto animate-in fade-in slide-in-from-top-1 duration-150"
- >
- {#each forgeVersions as version}
- <button
- type="button"
- onclick={() => { selectedForgeVersion = version.version; isForgeDropdownOpen = false; }}
- class="w-full flex items-center justify-between px-3 py-2 text-sm text-left
- transition-colors outline-none cursor-pointer
- {version.version === selectedForgeVersion
- ? 'bg-indigo-600 text-white'
- : 'text-gray-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800'}"
- >
- <span class="truncate">{version.version} {version.recommended ? "(Recommended)" : ""}</span>
- {#if version.version === selectedForgeVersion}
- <Check size={14} class="shrink-0 ml-2" />
- {/if}
- </button>
- {/each}
- </div>
- {/if}
- </div>
- </div>
-
- <button
- class="w-full bg-orange-600 hover:bg-orange-500 disabled:opacity-50 disabled:cursor-not-allowed text-white py-2.5 px-4 rounded-sm font-bold text-sm transition-all flex items-center justify-center gap-2"
- onclick={installModLoader}
- disabled={isInstalling || !selectedForgeVersion}
- >
- {#if isInstalling}
- <Loader2 class="animate-spin" size={16} />
- Installing...
- {:else}
- <Download size={16} />
- Install Forge {selectedForgeVersion}
- {/if}
- </button>
- {/if}
- </div>
- {/if}
- </div>
-</div>
diff --git a/packages/ui/src/components/ParticleBackground.svelte b/packages/ui/src/components/ParticleBackground.svelte
deleted file mode 100644
index 7644b1a..0000000
--- a/packages/ui/src/components/ParticleBackground.svelte
+++ /dev/null
@@ -1,70 +0,0 @@
-<script lang="ts" module>
- import { SaturnEffect } from "../lib/effects/SaturnEffect";
-
- // Global reference to the active Saturn effect for external control
- let globalSaturnEffect: SaturnEffect | null = null;
-
- export function getSaturnEffect(): SaturnEffect | null {
- return globalSaturnEffect;
- }
-</script>
-
-<script lang="ts">
- import { onMount, onDestroy } from "svelte";
- import { ConstellationEffect } from "../lib/effects/ConstellationEffect";
- import { settingsState } from "../stores/settings.svelte";
-
- let canvas: HTMLCanvasElement;
- let activeEffect: any;
-
- function loadEffect() {
- if (activeEffect) {
- activeEffect.destroy();
- }
-
- if (!canvas) return;
-
- if (settingsState.settings.active_effect === "saturn") {
- activeEffect = new SaturnEffect(canvas);
- globalSaturnEffect = activeEffect;
- } else {
- activeEffect = new ConstellationEffect(canvas);
- globalSaturnEffect = null;
- }
-
- // Ensure correct size immediately
- activeEffect.resize(window.innerWidth, window.innerHeight);
- }
-
- $effect(() => {
- const _ = settingsState.settings.active_effect;
- if (canvas) {
- loadEffect();
- }
- });
-
- onMount(() => {
- const resizeObserver = new ResizeObserver(() => {
- if (canvas && activeEffect) {
- activeEffect.resize(window.innerWidth, window.innerHeight);
- }
- });
-
- resizeObserver.observe(document.body);
-
- return () => {
- resizeObserver.disconnect();
- if (activeEffect) activeEffect.destroy();
- };
- });
-
- onDestroy(() => {
- if (activeEffect) activeEffect.destroy();
- globalSaturnEffect = null;
- });
-</script>
-
-<canvas
- bind:this={canvas}
- class="absolute inset-0 z-0 pointer-events-none"
-></canvas>
diff --git a/packages/ui/src/components/SettingsView.svelte b/packages/ui/src/components/SettingsView.svelte
deleted file mode 100644
index 0020506..0000000
--- a/packages/ui/src/components/SettingsView.svelte
+++ /dev/null
@@ -1,1217 +0,0 @@
-<script lang="ts">
- import { open } from "@tauri-apps/plugin-dialog";
- import { settingsState } from "../stores/settings.svelte";
- import CustomSelect from "./CustomSelect.svelte";
- import ConfigEditorModal from "./ConfigEditorModal.svelte";
- import { onMount } from "svelte";
- import { RefreshCw, FileJson } from "lucide-svelte";
-
- // Use convertFileSrc directly from settingsState.backgroundUrl for cleaner approach
- // or use the imported one if passing raw path.
- import { convertFileSrc } from "@tauri-apps/api/core";
-
- const effectOptions = [
- { value: "saturn", label: "Saturn" },
- { value: "constellation", label: "Network (Constellation)" }
- ];
-
- const logServiceOptions = [
- { value: "paste.rs", label: "paste.rs (Free, No Account)" },
- { value: "pastebin.com", label: "pastebin.com (Requires API Key)" }
- ];
-
- const llmProviderOptions = [
- { value: "ollama", label: "Ollama (Local)" },
- { value: "openai", label: "OpenAI (Remote)" }
- ];
-
- const languageOptions = [
- { value: "auto", label: "Auto (Match User)" },
- { value: "English", label: "English" },
- { value: "Chinese", label: "中文" },
- { value: "Japanese", label: "日本語" },
- { value: "Korean", label: "한국어" },
- { value: "Spanish", label: "Español" },
- { value: "French", label: "Français" },
- { value: "German", label: "Deutsch" },
- { value: "Russian", label: "Русский" },
- ];
-
- const ttsProviderOptions = [
- { value: "disabled", label: "Disabled" },
- { value: "piper", label: "Piper TTS (Local)" },
- { value: "edge", label: "Edge TTS (Online)" },
- ];
-
- const personas = [
- {
- value: "default",
- label: "Minecraft Expert (Default)",
- prompt: "You are a helpful Minecraft expert assistant. You help players with game issues, mod installation, performance optimization, and gameplay tips. Analyze any game logs provided and give concise, actionable advice."
- },
- {
- value: "technical",
- label: "Technical Debugger",
- prompt: "You are a technical support specialist for Minecraft. Focus strictly on analyzing logs, identifying crash causes, and providing technical solutions. Be precise and avoid conversational filler."
- },
- {
- value: "concise",
- label: "Concise Helper",
- prompt: "You are a direct and concise assistant. Provide answers in as few words as possible while remaining accurate. Use bullet points for lists."
- },
- {
- value: "explain",
- label: "Teacher / Explainer",
- prompt: "You are a patient teacher. Explain Minecraft concepts, redstone mechanics, and mod features in simple, easy-to-understand terms suitable for beginners."
- },
- {
- value: "pirate",
- label: "Pirate Captain",
- prompt: "You are a salty Minecraft Pirate Captain! Yarr! Speak like a pirate while helping the crew (the user) with their blocky adventures. Use terms like 'matey', 'landlubber', and 'treasure'."
- }
- ];
-
- let selectedPersona = $state("");
-
- function applyPersona(value: string) {
- const persona = personas.find(p => p.value === value);
- if (persona) {
- settingsState.settings.assistant.system_prompt = persona.prompt;
- selectedPersona = value; // Keep selected to show what's active
- }
- }
-
- function resetSystemPrompt() {
- const defaultPersona = personas.find(p => p.value === "default");
- if (defaultPersona) {
- settingsState.settings.assistant.system_prompt = defaultPersona.prompt;
- selectedPersona = "default";
- }
- }
-
- // Load models when assistant settings are shown
- function loadModelsForProvider() {
- if (settingsState.settings.assistant.llm_provider === "ollama") {
- settingsState.loadOllamaModels();
- } else if (settingsState.settings.assistant.llm_provider === "openai") {
- settingsState.loadOpenaiModels();
- }
- }
-
- async function selectBackground() {
- try {
- const selected = await open({
- multiple: false,
- filters: [
- {
- name: "Images",
- extensions: ["png", "jpg", "jpeg", "webp", "gif"],
- },
- ],
- });
-
- if (selected && typeof selected === "string") {
- settingsState.settings.custom_background_path = selected;
- settingsState.saveSettings();
- }
- } catch (e) {
- console.error("Failed to select background:", e);
- }
- }
-
- function clearBackground() {
- settingsState.settings.custom_background_path = undefined;
- settingsState.saveSettings();
- }
-
- let migrating = $state(false);
- async function runMigrationToSharedCaches() {
- if (migrating) return;
- migrating = true;
- try {
- const { invoke } = await import("@tauri-apps/api/core");
- const result = await invoke<{
- moved_files: number;
- hardlinks: number;
- copies: number;
- saved_mb: number;
- }>("migrate_shared_caches");
-
- // Reload settings to reflect changes
- await settingsState.loadSettings();
-
- // Show success message
- const msg = `Migration complete! ${result.moved_files} files (${result.hardlinks} hardlinks, ${result.copies} copies), ${result.saved_mb.toFixed(2)} MB saved.`;
- console.log(msg);
- alert(msg);
- } catch (e) {
- console.error("Migration failed:", e);
- alert(`Migration failed: ${e}`);
- } finally {
- migrating = false;
- }
- }
-</script>
-
-<div class="h-full flex flex-col p-6 overflow-hidden">
- <div class="flex items-center justify-between mb-6">
- <h2 class="text-3xl font-black bg-clip-text text-transparent bg-gradient-to-r dark:from-white dark:to-white/60 from-gray-900 to-gray-600">Settings</h2>
-
- <button
- onclick={() => settingsState.openConfigEditor()}
- class="p-2 hover:bg-white/10 rounded-lg text-zinc-400 hover:text-white transition-colors flex items-center gap-2 text-sm border border-transparent hover:border-white/5"
- title="Open Settings JSON"
- >
- <FileJson size={18} />
- <span class="hidden sm:inline">Open JSON</span>
- </button>
- </div>
-
- <div class="flex-1 overflow-y-auto pr-2 space-y-6 custom-scrollbar pb-10">
-
- <!-- Appearance / Background -->
- <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm">
- <h3 class="text-xs font-bold uppercase tracking-widest dark:text-white/40 text-black/40 mb-6 flex items-center gap-2">
- Appearance
- </h3>
-
- <div class="space-y-4">
- <div>
- <label class="block text-sm font-medium dark:text-white/70 text-black/70 mb-3">Custom Background Image</label>
-
- <div class="flex items-center gap-6">
- <!-- Preview -->
- <div class="w-40 h-24 rounded-xl overflow-hidden dark:bg-black/50 bg-gray-200 border dark:border-white/10 border-black/10 relative group shadow-lg">
- {#if settingsState.settings.custom_background_path}
- <img
- src={convertFileSrc(settingsState.settings.custom_background_path)}
- alt="Background Preview"
- class="w-full h-full object-cover"
- onerror={(e) => {
- console.error("Failed to load image:", settingsState.settings.custom_background_path, e);
- // e.currentTarget.style.display = 'none';
- }}
- />
- {:else}
- <div class="w-full h-full bg-gradient-to-br from-emerald-900 via-zinc-900 to-indigo-950 opacity-100"></div>
- <div class="absolute inset-0 flex items-center justify-center text-[10px] text-white/50 bg-black/20 ">Default Gradient</div>
- {/if}
- </div>
-
- <!-- Actions -->
- <div class="flex flex-col gap-2">
- <button
- onclick={selectBackground}
- class="dark:bg-white/10 dark:hover:bg-white/20 bg-black/5 hover:bg-black/10 dark:text-white text-black px-4 py-2 rounded-lg text-sm transition-colors border dark:border-white/5 border-black/5"
- >
- Select Image
- </button>
-
- {#if settingsState.settings.custom_background_path}
- <button
- onclick={clearBackground}
- class="text-red-400 hover:text-red-300 text-sm px-4 py-1 text-left transition-colors"
- >
- Reset to Default
- </button>
- {/if}
- </div>
- </div>
- <p class="text-xs dark:text-white/30 text-black/40 mt-3">
- Select an image from your computer to replace the default gradient background.
- Supported formats: PNG, JPG, WEBP, GIF.
- </p>
- </div>
-
- <!-- Visual Settings -->
- <div class="pt-4 border-t dark:border-white/5 border-black/5 space-y-4">
- <div class="flex items-center justify-between">
- <div>
- <h4 class="text-sm font-medium dark:text-white/90 text-black/80" id="visual-effects-label">Visual Effects</h4>
- <p class="text-xs dark:text-white/40 text-black/50 mt-1">Enable particle effects and animated gradients. (Default: On)</p>
- </div>
- <button
- aria-labelledby="visual-effects-label"
- onclick={() => { settingsState.settings.enable_visual_effects = !settingsState.settings.enable_visual_effects; settingsState.saveSettings(); }}
- class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.enable_visual_effects ? 'bg-indigo-500' : 'dark:bg-white/10 bg-black/10'}"
- >
- <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.enable_visual_effects ? 'translate-x-5' : 'translate-x-0'}"></div>
- </button>
- </div>
-
- {#if settingsState.settings.enable_visual_effects}
- <div class="flex items-center justify-between pl-2 border-l-2 dark:border-white/5 border-black/5 ml-1">
- <div>
- <h4 class="text-sm font-medium dark:text-white/90 text-black/80" id="theme-effect-label">Theme Effect</h4>
- <p class="text-xs dark:text-white/40 text-black/50 mt-1">Select the active visual theme.</p>
- </div>
- <CustomSelect
- options={effectOptions}
- bind:value={settingsState.settings.active_effect}
- onchange={() => settingsState.saveSettings()}
- class="w-52"
- />
- </div>
- {/if}
-
- <div class="flex items-center justify-between">
- <div>
- <h4 class="text-sm font-medium dark:text-white/90 text-black/80" id="gpu-acceleration-label">GPU Acceleration</h4>
- <p class="text-xs dark:text-white/40 text-black/50 mt-1">Enable GPU acceleration for the interface. (Default: Off, Requires Restart)</p>
- </div>
- <button
- aria-labelledby="gpu-acceleration-label"
- onclick={() => { settingsState.settings.enable_gpu_acceleration = !settingsState.settings.enable_gpu_acceleration; settingsState.saveSettings(); }}
- class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.enable_gpu_acceleration ? 'bg-indigo-500' : 'dark:bg-white/10 bg-black/10'}"
- >
- <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.enable_gpu_acceleration ? 'translate-x-5' : 'translate-x-0'}"></div>
- </button>
- </div>
-
- <!-- Color Theme Switcher -->
- <div class="flex items-center justify-between pt-4 border-t dark:border-white/5 border-black/5 opacity-50 cursor-not-allowed">
- <div>
- <h4 class="text-sm font-medium dark:text-white/90 text-black/80" id="color-theme-label">Color Theme</h4>
- <p class="text-xs dark:text-white/40 text-black/50 mt-1">Interface color mode. (Locked to Dark)</p>
- </div>
- <div class="flex items-center bg-black/5 dark:bg-white/5 rounded-lg p-1 pointer-events-none">
- <button
- disabled
- class="px-3 py-1 rounded-md text-xs font-medium transition-all text-gray-500 dark:text-gray-600"
- >
- Light
- </button>
- <button
- disabled
- class="px-3 py-1 rounded-md text-xs font-medium transition-all bg-indigo-500 text-white shadow-sm"
- >
- Dark
- </button>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <!-- Java Path -->
- <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm">
- <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2">
- Java Environment
- </h3>
- <div class="space-y-4">
- <div>
- <label for="java-path" class="block text-sm font-medium text-white/70 mb-2">Java Executable Path</label>
- <div class="flex gap-2">
- <input
- id="java-path"
- bind:value={settingsState.settings.java_path}
- type="text"
- class="bg-black/40 text-white flex-1 px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none font-mono text-xs transition-colors"
- placeholder="e.g. java, /usr/bin/java"
- />
- <button
- onclick={() => settingsState.detectJava()}
- disabled={settingsState.isDetectingJava}
- class="bg-white/10 hover:bg-white/20 disabled:opacity-50 text-white px-4 py-2 rounded-xl border border-white/5 transition-colors whitespace-nowrap text-sm font-medium"
- >
- {settingsState.isDetectingJava ? "Detecting..." : "Auto Detect"}
- </button>
- <button
- onclick={() => settingsState.openJavaDownloadModal()}
- class="bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 rounded-xl transition-colors whitespace-nowrap text-sm font-medium"
- >
- Download Java
- </button>
- </div>
- </div>
-
- {#if settingsState.javaInstallations.length > 0}
- <div class="mt-4 space-y-2">
- <p class="text-[10px] text-white/30 uppercase font-bold tracking-wider">Detected Installations</p>
- {#each settingsState.javaInstallations as java}
- <button
- onclick={() => settingsState.selectJava(java.path)}
- class="w-full text-left p-3 rounded-lg border transition-all duration-200 group
- {settingsState.settings.java_path === java.path
- ? 'bg-indigo-500/20 border-indigo-500/30'
- : 'bg-black/20 border-white/5 hover:bg-white/5 hover:border-white/10'}"
- >
- <div class="flex justify-between items-center">
- <div>
- <span class="text-white font-mono text-xs font-bold">{java.version}</span>
- <span class="text-white/40 text-[10px] ml-2">{java.is_64bit ? "64-bit" : "32-bit"}</span>
- </div>
- {#if settingsState.settings.java_path === java.path}
- <span class="text-indigo-300 text-[10px] font-bold uppercase tracking-wider">Selected</span>
- {/if}
- </div>
- <div class="text-white/30 text-[10px] font-mono truncate mt-1 group-hover:text-white/50">{java.path}</div>
- </button>
- {/each}
- </div>
- {/if}
- </div>
- </div>
-
- <!-- Memory -->
- <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm">
- <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2">
- Memory Allocation (RAM)
- </h3>
- <div class="grid grid-cols-2 gap-6">
- <div>
- <label for="min-memory" class="block text-sm font-medium text-white/70 mb-2">Minimum (MB)</label>
- <input
- id="min-memory"
- bind:value={settingsState.settings.min_memory}
- type="number"
- class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none transition-colors"
- />
- </div>
- <div>
- <label for="max-memory" class="block text-sm font-medium text-white/70 mb-2">Maximum (MB)</label>
- <input
- id="max-memory"
- bind:value={settingsState.settings.max_memory}
- type="number"
- class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none transition-colors"
- />
- </div>
- </div>
- </div>
-
- <!-- Resolution -->
- <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm">
- <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2">
- Game Window Size
- </h3>
- <div class="grid grid-cols-2 gap-6">
- <div>
- <label for="window-width" class="block text-sm font-medium text-white/70 mb-2">Width</label>
- <input
- id="window-width"
- bind:value={settingsState.settings.width}
- type="number"
- class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none transition-colors"
- />
- </div>
- <div>
- <label for="window-height" class="block text-sm font-medium text-white/70 mb-2">Height</label>
- <input
- id="window-height"
- bind:value={settingsState.settings.height}
- type="number"
- class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none transition-colors"
- />
- </div>
- </div>
- </div>
-
- <!-- Download Settings -->
- <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm">
- <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2">
- Network
- </h3>
- <div>
- <label for="download-threads" class="block text-sm font-medium text-white/70 mb-2">Concurrent Download Threads</label>
- <input
- id="download-threads"
- bind:value={settingsState.settings.download_threads}
- type="number"
- min="1"
- max="128"
- class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none transition-colors"
- />
- <p class="text-xs text-white/30 mt-2">Higher values usually mean faster downloads but use more CPU/Network.</p>
- </div>
- </div>
-
- <!-- Storage & Caches -->
- <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm">
- <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2">Storage & Version Caches</h3>
- <div class="space-y-4">
- <div class="flex items-center justify-between">
- <div>
- <h4 class="text-sm font-medium text-white/90" id="shared-caches-label">Use Shared Caches</h4>
- <p class="text-xs text-white/40 mt-1">Store versions/libraries/assets in a global cache shared by all instances.</p>
- </div>
- <button
- aria-labelledby="shared-caches-label"
- onclick={() => { settingsState.settings.use_shared_caches = !settingsState.settings.use_shared_caches; settingsState.saveSettings(); }}
- class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.use_shared_caches ? 'bg-indigo-500' : 'bg-white/10'}"
- >
- <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.use_shared_caches ? 'translate-x-5' : 'translate-x-0'}"></div>
- </button>
- </div>
-
- <div class="flex items-center justify-between">
- <div>
- <h4 class="text-sm font-medium text-white/90" id="legacy-storage-label">Keep Legacy Per-Instance Storage</h4>
- <p class="text-xs text-white/40 mt-1">Do not migrate existing instance caches; keep current layout.</p>
- </div>
- <button
- aria-labelledby="legacy-storage-label"
- onclick={() => { settingsState.settings.keep_legacy_per_instance_storage = !settingsState.settings.keep_legacy_per_instance_storage; settingsState.saveSettings(); }}
- class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.keep_legacy_per_instance_storage ? 'bg-indigo-500' : 'bg-white/10'}"
- >
- <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.keep_legacy_per_instance_storage ? 'translate-x-5' : 'translate-x-0'}"></div>
- </button>
- </div>
-
- <div class="flex items-center justify-between pt-2 border-t border-white/10">
- <div>
- <h4 class="text-sm font-medium text-white/90">Run Migration</h4>
- <p class="text-xs text-white/40 mt-1">Hard-link or copy existing per-instance caches into the shared cache.</p>
- </div>
- <button
- onclick={runMigrationToSharedCaches}
- disabled={migrating}
- class="px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white text-sm disabled:opacity-50 disabled:cursor-not-allowed"
- >
- {migrating ? "Migrating..." : "Migrate Now"}
- </button>
- </div>
- </div>
- </div>
-
- <!-- Feature Flags -->
- <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm">
- <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2">Feature Flags (Launcher Arguments)</h3>
- <div class="space-y-4">
- <div class="flex items-center justify-between">
- <div>
- <h4 class="text-sm font-medium text-white/90" id="demo-user-label">Demo User</h4>
- <p class="text-xs text-white/40 mt-1">Enable demo-related arguments when rules require them.</p>
- </div>
- <button
- aria-labelledby="demo-user-label"
- onclick={() => { settingsState.settings.feature_flags.demo_user = !settingsState.settings.feature_flags.demo_user; settingsState.saveSettings(); }}
- class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.feature_flags.demo_user ? 'bg-indigo-500' : 'bg-white/10'}"
- >
- <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.feature_flags.demo_user ? 'translate-x-5' : 'translate-x-0'}"></div>
- </button>
- </div>
-
- <div class="flex items-center justify-between">
- <div>
- <h4 class="text-sm font-medium text-white/90" id="quick-play-label">Quick Play</h4>
- <p class="text-xs text-white/40 mt-1">Enable quick play singleplayer/multiplayer arguments.</p>
- </div>
- <button
- aria-labelledby="quick-play-label"
- onclick={() => { settingsState.settings.feature_flags.quick_play_enabled = !settingsState.settings.feature_flags.quick_play_enabled; settingsState.saveSettings(); }}
- class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.feature_flags.quick_play_enabled ? 'bg-indigo-500' : 'bg-white/10'}"
- >
- <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.feature_flags.quick_play_enabled ? 'translate-x-5' : 'translate-x-0'}"></div>
- </button>
- </div>
-
- {#if settingsState.settings.feature_flags.quick_play_enabled}
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 pl-2 border-l-2 border-white/10">
- <div>
- <label class="block text-sm font-medium text-white/70 mb-2">Singleplayer World Path</label>
- <input
- type="text"
- bind:value={settingsState.settings.feature_flags.quick_play_path}
- placeholder="/path/to/saves/MyWorld"
- class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none font-mono text-xs transition-colors"
- />
- </div>
- <div class="flex items-center justify-between">
- <div>
- <h4 class="text-sm font-medium text-white/90" id="qp-singleplayer-label">Prefer Singleplayer</h4>
- <p class="text-xs text-white/40 mt-1">If enabled, use singleplayer quick play path.</p>
- </div>
- <button
- aria-labelledby="qp-singleplayer-label"
- onclick={() => { settingsState.settings.feature_flags.quick_play_singleplayer = !settingsState.settings.feature_flags.quick_play_singleplayer; settingsState.saveSettings(); }}
- class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.feature_flags.quick_play_singleplayer ? 'bg-indigo-500' : 'bg-white/10'}"
- >
- <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.feature_flags.quick_play_singleplayer ? 'translate-x-5' : 'translate-x-0'}"></div>
- </button>
- </div>
- <div>
- <label class="block text-sm font-medium text-white/70 mb-2">Multiplayer Server Address</label>
- <input
- type="text"
- bind:value={settingsState.settings.feature_flags.quick_play_multiplayer_server}
- placeholder="example.org:25565"
- class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none font-mono text-xs transition-colors"
- />
- </div>
- </div>
- {/if}
- </div>
- </div>
-
- <!-- Debug / Logs -->
- <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm">
- <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2">
- Debug & Logs
- </h3>
- <div class="space-y-4">
- <div>
- <label for="log-service" class="block text-sm font-medium text-white/70 mb-2">Log Upload Service</label>
- <CustomSelect
- options={logServiceOptions}
- bind:value={settingsState.settings.log_upload_service}
- class="w-full"
- />
- </div>
-
- {#if settingsState.settings.log_upload_service === 'pastebin.com'}
- <div>
- <label for="pastebin-key" class="block text-sm font-medium text-white/70 mb-2">Pastebin Dev API Key</label>
- <input
- id="pastebin-key"
- type="password"
- bind:value={settingsState.settings.pastebin_api_key}
- placeholder="Enter your API Key"
- class="dark:bg-zinc-900 bg-white dark:text-white text-black w-full px-4 py-3 rounded-xl border dark:border-zinc-700 border-gray-300 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 outline-none transition-colors placeholder:text-zinc-500"
- />
- <p class="text-xs text-white/30 mt-2">
- Get your API key from <a href="https://pastebin.com/doc_api#1" target="_blank" class="text-indigo-400 hover:underline">Pastebin API Documentation</a>.
- </p>
- </div>
- {/if}
- </div>
- </div>
-
- <!-- AI Assistant -->
- <div class="dark:bg-[#09090b] bg-white p-6 rounded-sm border dark:border-white/10 border-gray-200 shadow-sm">
- <h3 class="text-xs font-bold uppercase tracking-widest text-white/40 mb-6 flex items-center gap-2">
- <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <rect x="3" y="11" width="18" height="10" rx="2"/>
- <circle cx="12" cy="5" r="2"/>
- <path d="M12 7v4"/>
- <circle cx="8" cy="16" r="1" fill="currentColor"/>
- <circle cx="16" cy="16" r="1" fill="currentColor"/>
- </svg>
- AI Assistant
- </h3>
- <div class="space-y-6">
- <!-- Enable/Disable -->
- <div class="flex items-center justify-between">
- <div>
- <h4 class="text-sm font-medium dark:text-white/90 text-black/80" id="assistant-enabled-label">Enable Assistant</h4>
- <p class="text-xs dark:text-white/40 text-black/50 mt-1">Toggle the AI assistant feature on or off.</p>
- </div>
- <button
- aria-labelledby="assistant-enabled-label"
- onclick={() => { settingsState.settings.assistant.enabled = !settingsState.settings.assistant.enabled; settingsState.saveSettings(); }}
- class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none {settingsState.settings.assistant.enabled ? 'bg-indigo-500' : 'dark:bg-white/10 bg-black/10'}"
- >
- <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out {settingsState.settings.assistant.enabled ? 'translate-x-5' : 'translate-x-0'}"></div>
- </button>
- </div>
-
- {#if settingsState.settings.assistant.enabled}
- <!-- LLM Provider Section -->
- <div class="pt-4 border-t dark:border-white/5 border-black/5">
- <h4 class="text-xs font-bold uppercase tracking-widest text-white/30 mb-4">Language Model</h4>
-
- <div class="space-y-4">
- <div>
- <label for="llm-provider" class="block text-sm font-medium text-white/70 mb-2">Provider</label>
- <CustomSelect
- options={llmProviderOptions}
- bind:value={settingsState.settings.assistant.llm_provider}
- onchange={() => settingsState.saveSettings()}
- class="w-full"
- />
- </div>
-
- {#if settingsState.settings.assistant.llm_provider === 'ollama'}
- <!-- Ollama Settings -->
- <div class="pl-4 border-l-2 border-indigo-500/30 space-y-4">
- <div>
- <label for="ollama-endpoint" class="block text-sm font-medium text-white/70 mb-2">API Endpoint</label>
- <div class="flex gap-2">
- <input
- id="ollama-endpoint"
- type="text"
- bind:value={settingsState.settings.assistant.ollama_endpoint}
- placeholder="http://localhost:11434"
- class="bg-black/40 text-white flex-1 px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none font-mono text-xs transition-colors"
- />
- <button
- onclick={() => settingsState.loadOllamaModels()}
- disabled={settingsState.isLoadingOllamaModels}
- class="px-4 py-2 bg-white/10 hover:bg-white/20 disabled:opacity-50 text-white rounded-xl border border-white/5 transition-colors whitespace-nowrap text-sm font-medium flex items-center gap-2"
- title="Refresh models"
- >
- <RefreshCw size={14} class={settingsState.isLoadingOllamaModels ? "animate-spin" : ""} />
- <span class="hidden sm:inline">Refresh</span>
- </button>
- </div>
- <p class="text-xs text-white/30 mt-2">
- Default: http://localhost:11434. Make sure Ollama is running.
- </p>
- </div>
-
- <div>
- <div class="flex items-center justify-between mb-2">
- <label for="ollama-model" class="block text-sm font-medium text-white/70">Model</label>
- {#if settingsState.ollamaModels.length > 0}
- <span class="text-[10px] text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded-full">
- {settingsState.ollamaModels.length} installed
- </span>
- {/if}
- </div>
-
- {#if settingsState.isLoadingOllamaModels}
- <div class="bg-black/40 text-white/50 w-full px-4 py-3 rounded-xl border border-white/10 text-sm flex items-center gap-2">
- <RefreshCw size={14} class="animate-spin" />
- Loading models...
- </div>
- {:else if settingsState.ollamaModelsError}
- <div class="bg-red-500/10 text-red-400 w-full px-4 py-3 rounded-xl border border-red-500/20 text-sm">
- {settingsState.ollamaModelsError}
- </div>
- <CustomSelect
- options={settingsState.currentModelOptions}
- bind:value={settingsState.settings.assistant.ollama_model}
- onchange={() => settingsState.saveSettings()}
- class="w-full mt-2"
- allowCustom={true}
- />
- {:else if settingsState.ollamaModels.length === 0}
- <div class="bg-amber-500/10 text-amber-400 w-full px-4 py-3 rounded-xl border border-amber-500/20 text-sm">
- No models found. Click Refresh to load installed models.
- </div>
- <CustomSelect
- options={settingsState.currentModelOptions}
- bind:value={settingsState.settings.assistant.ollama_model}
- onchange={() => settingsState.saveSettings()}
- class="w-full mt-2"
- allowCustom={true}
- />
- {:else}
- <CustomSelect
- options={settingsState.currentModelOptions}
- bind:value={settingsState.settings.assistant.ollama_model}
- onchange={() => settingsState.saveSettings()}
- class="w-full"
- allowCustom={true}
- />
- {/if}
-
- <p class="text-xs text-white/30 mt-2">
- Run <code class="bg-black/30 px-1 rounded">ollama pull {'<model>'}</code> to download new models. Or type a custom model name above.
- </p>
- </div>
- </div>
- {:else if settingsState.settings.assistant.llm_provider === 'openai'}
- <!-- OpenAI Settings -->
- <div class="pl-4 border-l-2 border-emerald-500/30 space-y-4">
- <div>
- <label for="openai-key" class="block text-sm font-medium text-white/70 mb-2">API Key</label>
- <div class="flex gap-2">
- <input
- id="openai-key"
- type="password"
- bind:value={settingsState.settings.assistant.openai_api_key}
- placeholder="sk-..."
- class="bg-black/40 text-white flex-1 px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none font-mono text-xs transition-colors"
- />
- <button
- onclick={() => settingsState.loadOpenaiModels()}
- disabled={settingsState.isLoadingOpenaiModels || !settingsState.settings.assistant.openai_api_key}
- class="px-4 py-2 bg-white/10 hover:bg-white/20 disabled:opacity-50 text-white rounded-xl border border-white/5 transition-colors whitespace-nowrap text-sm font-medium flex items-center gap-2"
- title="Refresh models"
- >
- <RefreshCw size={14} class={settingsState.isLoadingOpenaiModels ? "animate-spin" : ""} />
- <span class="hidden sm:inline">Load</span>
- </button>
- </div>
- <p class="text-xs text-white/30 mt-2">
- Get your API key from <a href="https://platform.openai.com/api-keys" target="_blank" class="text-indigo-400 hover:underline">OpenAI Dashboard</a>.
- </p>
- </div>
-
- <div>
- <label for="openai-endpoint" class="block text-sm font-medium text-white/70 mb-2">API Endpoint</label>
- <input
- id="openai-endpoint"
- type="text"
- bind:value={settingsState.settings.assistant.openai_endpoint}
- placeholder="https://api.openai.com/v1"
- class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none font-mono text-xs transition-colors"
- />
- <p class="text-xs text-white/30 mt-2">
- Use custom endpoint for Azure OpenAI or other compatible APIs.
- </p>
- </div>
-
- <div>
- <div class="flex items-center justify-between mb-2">
- <label for="openai-model" class="block text-sm font-medium text-white/70">Model</label>
- {#if settingsState.openaiModels.length > 0}
- <span class="text-[10px] text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded-full">
- {settingsState.openaiModels.length} available
- </span>
- {/if}
- </div>
-
- {#if settingsState.isLoadingOpenaiModels}
- <div class="bg-black/40 text-white/50 w-full px-4 py-3 rounded-xl border border-white/10 text-sm flex items-center gap-2">
- <RefreshCw size={14} class="animate-spin" />
- Loading models...
- </div>
- {:else if settingsState.openaiModelsError}
- <div class="bg-red-500/10 text-red-400 w-full px-4 py-3 rounded-xl border border-red-500/20 text-sm mb-2">
- {settingsState.openaiModelsError}
- </div>
- <CustomSelect
- options={settingsState.currentModelOptions}
- bind:value={settingsState.settings.assistant.openai_model}
- onchange={() => settingsState.saveSettings()}
- class="w-full"
- allowCustom={true}
- />
- {:else}
- <CustomSelect
- options={settingsState.currentModelOptions}
- bind:value={settingsState.settings.assistant.openai_model}
- onchange={() => settingsState.saveSettings()}
- class="w-full"
- allowCustom={true}
- />
- {/if}
- </div>
- </div>
- {/if}
- </div>
- </div>
-
- <!-- Response Settings -->
- <div class="pt-4 border-t dark:border-white/5 border-black/5">
- <h4 class="text-xs font-bold uppercase tracking-widest text-white/30 mb-4">Response Settings</h4>
-
- <div class="space-y-4">
- <div>
- <label for="response-lang" class="block text-sm font-medium text-white/70 mb-2">Response Language</label>
- <CustomSelect
- options={languageOptions}
- bind:value={settingsState.settings.assistant.response_language}
- onchange={() => settingsState.saveSettings()}
- class="w-full"
- />
- </div>
-
- <div>
- <div class="flex items-center justify-between mb-2">
- <label for="system-prompt" class="block text-sm font-medium text-white/70">System Prompt</label>
- <button
- onclick={resetSystemPrompt}
- class="text-xs text-indigo-400 hover:text-indigo-300 transition-colors flex items-center gap-1 opacity-80 hover:opacity-100"
- title="Reset to default prompt"
- >
- <RefreshCw size={10} />
- Reset
- </button>
- </div>
-
- <div class="mb-3">
- <CustomSelect
- options={personas.map(p => ({ value: p.value, label: p.label }))}
- bind:value={selectedPersona}
- placeholder="Load a preset persona..."
- onchange={applyPersona}
- class="w-full"
- />
- </div>
-
- <textarea
- id="system-prompt"
- bind:value={settingsState.settings.assistant.system_prompt}
- oninput={() => selectedPersona = ""}
- rows="4"
- placeholder="You are a helpful Minecraft expert assistant..."
- class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none text-sm transition-colors resize-none"
- ></textarea>
- <p class="text-xs text-white/30 mt-2">
- Customize how the assistant behaves and responds.
- </p>
- </div>
- </div>
- </div>
-
- <!-- TTS Settings -->
- <div class="pt-4 border-t dark:border-white/5 border-black/5">
- <h4 class="text-xs font-bold uppercase tracking-widest text-white/30 mb-4">Text-to-Speech (Coming Soon)</h4>
-
- <div class="space-y-4 opacity-50 pointer-events-none">
- <div class="flex items-center justify-between">
- <div>
- <h4 class="text-sm font-medium dark:text-white/90 text-black/80">Enable TTS</h4>
- <p class="text-xs dark:text-white/40 text-black/50 mt-1">Read assistant responses aloud.</p>
- </div>
- <button
- disabled
- class="w-11 h-6 rounded-full transition-colors duration-200 ease-in-out relative focus:outline-none dark:bg-white/10 bg-black/10"
- >
- <div class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full shadow-sm transition-transform duration-200 ease-in-out translate-x-0"></div>
- </button>
- </div>
-
- <div>
- <label class="block text-sm font-medium text-white/70 mb-2">TTS Provider</label>
- <CustomSelect
- options={ttsProviderOptions}
- value="disabled"
- class="w-full"
- />
- </div>
- </div>
- </div>
- {/if}
- </div>
- </div>
-
- <div class="pt-4 flex justify-end">
- <button
- onclick={() => settingsState.saveSettings()}
- class="bg-gradient-to-r from-emerald-600 to-green-600 hover:from-emerald-500 hover:to-green-500 text-white font-bold py-3 px-8 rounded-xl shadow-lg shadow-emerald-500/20 transition-all hover:scale-105 active:scale-95"
- >
- Save Settings
- </button>
- </div>
- </div>
-</div>
-
-{#if settingsState.showConfigEditor}
- <ConfigEditorModal />
-{/if}
-
-<!-- Java Download Modal -->
-{#if settingsState.showJavaDownloadModal}
- <div class="fixed inset-0 z-[100] flex items-center justify-center backdrop-blur-sm bg-black/70">
- <div class="bg-zinc-900 rounded-2xl border border-white/10 shadow-2xl w-[900px] max-w-[95vw] h-[600px] max-h-[90vh] flex flex-col overflow-hidden">
- <!-- Header -->
- <div class="flex items-center justify-between p-5 border-b border-white/10">
- <h3 class="text-xl font-bold text-white">Download Java</h3>
- <button
- aria-label="Close dialog"
- onclick={() => settingsState.closeJavaDownloadModal()}
- disabled={settingsState.isDownloadingJava}
- class="text-white/40 hover:text-white/80 disabled:opacity-50 transition-colors p-1"
- >
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
- </svg>
- </button>
- </div>
-
- <!-- Main Content Area -->
- <div class="flex flex-1 overflow-hidden">
- <!-- Left Sidebar: Sources -->
- <div class="w-40 border-r border-white/10 p-3 flex flex-col gap-1">
- <span class="text-[10px] font-bold uppercase tracking-widest text-white/30 px-2 mb-2">Sources</span>
-
- <button
- disabled
- class="flex items-center gap-2 px-3 py-2.5 rounded-lg text-left text-sm opacity-40 cursor-not-allowed text-white/50"
- >
- <div class="w-5 h-5 rounded bg-white/10 flex items-center justify-center text-[10px]">M</div>
- Mojang
- </button>
-
- <button
- class="flex items-center gap-2 px-3 py-2.5 rounded-lg text-left text-sm bg-indigo-500/20 border border-indigo-500/40 text-white"
- >
- <div class="w-5 h-5 rounded bg-indigo-500 flex items-center justify-center text-[10px] font-bold">A</div>
- Adoptium
- </button>
-
- <button
- disabled
- class="flex items-center gap-2 px-3 py-2.5 rounded-lg text-left text-sm opacity-40 cursor-not-allowed text-white/50"
- >
- <div class="w-5 h-5 rounded bg-white/10 flex items-center justify-center text-[10px]">Z</div>
- Azul Zulu
- </button>
- </div>
-
- <!-- Center: Version Selection -->
- <div class="flex-1 flex flex-col overflow-hidden">
- <!-- Toolbar -->
- <div class="flex items-center gap-3 p-4 border-b border-white/5">
- <!-- Search -->
- <div class="relative flex-1 max-w-xs">
- <input
- type="text"
- bind:value={settingsState.searchQuery}
- placeholder="Search versions..."
- class="w-full bg-black/30 text-white text-sm px-4 py-2 pl-9 rounded-lg border border-white/10 focus:border-indigo-500/50 outline-none"
- />
- <svg class="absolute left-3 top-2.5 w-4 h-4 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
- </svg>
- </div>
-
- <!-- Recommended Filter -->
- <label class="flex items-center gap-2 text-sm text-white/60 cursor-pointer select-none">
- <input
- type="checkbox"
- bind:checked={settingsState.showOnlyRecommended}
- class="w-4 h-4 rounded border-white/20 bg-black/30 text-indigo-500 focus:ring-indigo-500/30"
- />
- LTS Only
- </label>
-
- <!-- Image Type Toggle -->
- <div class="flex items-center bg-black/30 rounded-lg p-0.5 border border-white/10">
- <button
- onclick={() => settingsState.selectedImageType = "jre"}
- class="px-3 py-1.5 rounded-md text-xs font-medium transition-all {settingsState.selectedImageType === 'jre' ? 'bg-indigo-500 text-white shadow' : 'text-white/50 hover:text-white/80'}"
- >
- JRE
- </button>
- <button
- onclick={() => settingsState.selectedImageType = "jdk"}
- class="px-3 py-1.5 rounded-md text-xs font-medium transition-all {settingsState.selectedImageType === 'jdk' ? 'bg-indigo-500 text-white shadow' : 'text-white/50 hover:text-white/80'}"
- >
- JDK
- </button>
- </div>
- </div>
-
- <!-- Loading State -->
- {#if settingsState.isLoadingCatalog}
- <div class="flex-1 flex items-center justify-center text-white/50">
- <div class="flex flex-col items-center gap-3">
- <div class="w-8 h-8 border-2 border-indigo-500/30 border-t-indigo-500 rounded-full animate-spin"></div>
- <span class="text-sm">Loading Java versions...</span>
- </div>
- </div>
- {:else if settingsState.catalogError}
- <div class="flex-1 flex items-center justify-center text-red-400">
- <div class="flex flex-col items-center gap-3 text-center px-8">
- <svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
- </svg>
- <span class="text-sm">{settingsState.catalogError}</span>
- <button
- onclick={() => settingsState.refreshCatalog()}
- class="mt-2 px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-sm text-white transition-colors"
- >
- Retry
- </button>
- </div>
- </div>
- {:else}
- <!-- Version List -->
- <div class="flex-1 overflow-auto p-4">
- <div class="space-y-2">
- {#each settingsState.availableMajorVersions as version}
- {@const isLts = settingsState.javaCatalog?.lts_versions.includes(version)}
- {@const isSelected = settingsState.selectedMajorVersion === version}
- {@const releaseInfo = settingsState.javaCatalog?.releases.find(r => r.major_version === version && r.image_type === settingsState.selectedImageType)}
- {@const isAvailable = releaseInfo?.is_available ?? false}
- {@const installStatus = releaseInfo ? settingsState.getInstallStatus(releaseInfo) : 'download'}
-
- <button
- onclick={() => settingsState.selectMajorVersion(version)}
- disabled={!isAvailable}
- class="w-full flex items-center gap-4 p-3 rounded-xl border transition-all text-left
- {isSelected
- ? 'bg-indigo-500/20 border-indigo-500/50 ring-2 ring-indigo-500/30'
- : isAvailable
- ? 'bg-black/20 border-white/10 hover:bg-white/5 hover:border-white/20'
- : 'bg-black/10 border-white/5 opacity-40 cursor-not-allowed'}"
- >
- <!-- Version Number -->
- <div class="w-14 text-center">
- <span class="text-xl font-bold {isSelected ? 'text-white' : 'text-white/80'}">{version}</span>
- </div>
-
- <!-- Version Details -->
- <div class="flex-1 min-w-0">
- <div class="flex items-center gap-2">
- <span class="text-sm text-white/70 font-mono truncate">{releaseInfo?.version ?? '--'}</span>
- {#if isLts}
- <span class="px-1.5 py-0.5 bg-emerald-500/20 text-emerald-400 text-[9px] font-bold rounded uppercase shrink-0">LTS</span>
- {/if}
- </div>
- {#if releaseInfo}
- <div class="text-[10px] text-white/40 truncate mt-0.5">
- {releaseInfo.release_name} • {settingsState.formatBytes(releaseInfo.file_size)}
- </div>
- {/if}
- </div>
-
- <!-- Install Status Badge -->
- <div class="shrink-0">
- {#if installStatus === 'installed'}
- <span class="px-2 py-1 bg-emerald-500/20 text-emerald-400 text-[10px] font-bold rounded uppercase">Installed</span>
- {:else if isAvailable}
- <span class="px-2 py-1 bg-white/10 text-white/50 text-[10px] font-medium rounded">Download</span>
- {:else}
- <span class="px-2 py-1 bg-red-500/10 text-red-400/60 text-[10px] font-medium rounded">N/A</span>
- {/if}
- </div>
- </button>
- {/each}
- </div>
- </div>
- {/if}
- </div>
-
- <!-- Right Sidebar: Details -->
- <div class="w-64 border-l border-white/10 flex flex-col">
- <div class="p-4 border-b border-white/5">
- <span class="text-[10px] font-bold uppercase tracking-widest text-white/30">Details</span>
- </div>
-
- {#if settingsState.selectedRelease}
- <div class="flex-1 p-4 space-y-4 overflow-auto">
- <div>
- <div class="text-[10px] text-white/40 uppercase tracking-wider mb-1">Version</div>
- <div class="text-sm text-white font-mono">{settingsState.selectedRelease.version}</div>
- </div>
-
- <div>
- <div class="text-[10px] text-white/40 uppercase tracking-wider mb-1">Release Name</div>
- <div class="text-sm text-white">{settingsState.selectedRelease.release_name}</div>
- </div>
-
- <div>
- <div class="text-[10px] text-white/40 uppercase tracking-wider mb-1">Release Date</div>
- <div class="text-sm text-white">{settingsState.formatDate(settingsState.selectedRelease.release_date)}</div>
- </div>
-
- <div>
- <div class="text-[10px] text-white/40 uppercase tracking-wider mb-1">Size</div>
- <div class="text-sm text-white">{settingsState.formatBytes(settingsState.selectedRelease.file_size)}</div>
- </div>
-
- <div>
- <div class="text-[10px] text-white/40 uppercase tracking-wider mb-1">Type</div>
- <div class="flex items-center gap-2">
- <span class="text-sm text-white uppercase">{settingsState.selectedRelease.image_type}</span>
- {#if settingsState.selectedRelease.is_lts}
- <span class="px-1.5 py-0.5 bg-emerald-500/20 text-emerald-400 text-[9px] font-bold rounded">LTS</span>
- {/if}
- </div>
- </div>
-
- <div>
- <div class="text-[10px] text-white/40 uppercase tracking-wider mb-1">Architecture</div>
- <div class="text-sm text-white">{settingsState.selectedRelease.architecture}</div>
- </div>
-
- {#if !settingsState.selectedRelease.is_available}
- <div class="mt-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg">
- <div class="text-xs text-red-400">Not available for your platform</div>
- </div>
- {/if}
- </div>
- {:else}
- <div class="flex-1 flex items-center justify-center text-white/30 text-sm p-4 text-center">
- Select a Java version to view details
- </div>
- {/if}
- </div>
- </div>
-
- <!-- Download Progress (MC Style) -->
- {#if settingsState.isDownloadingJava && settingsState.downloadProgress}
- <div class="border-t border-white/10 p-4 bg-zinc-900/90">
- <div class="flex items-center justify-between mb-2">
- <h3 class="text-white font-bold text-sm">Downloading Java</h3>
- <span class="text-xs text-zinc-400">{settingsState.downloadProgress.status}</span>
- </div>
-
- <!-- Progress Bar -->
- <div class="mb-2">
- <div class="flex justify-between text-[10px] text-zinc-400 mb-1">
- <span>{settingsState.downloadProgress.file_name}</span>
- <span>{Math.round(settingsState.downloadProgress.percentage)}%</span>
- </div>
- <div class="w-full bg-zinc-800 rounded-full h-2.5 overflow-hidden">
- <div
- class="bg-gradient-to-r from-blue-500 to-cyan-400 h-2.5 rounded-full transition-all duration-200"
- style="width: {settingsState.downloadProgress.percentage}%"
- ></div>
- </div>
- </div>
-
- <!-- Speed & Stats -->
- <div class="flex justify-between text-[10px] text-zinc-500 font-mono">
- <span>
- {settingsState.formatBytes(settingsState.downloadProgress.speed_bytes_per_sec)}/s ·
- ETA: {settingsState.formatTime(settingsState.downloadProgress.eta_seconds)}
- </span>
- <span>
- {settingsState.formatBytes(settingsState.downloadProgress.downloaded_bytes)} /
- {settingsState.formatBytes(settingsState.downloadProgress.total_bytes)}
- </span>
- </div>
- </div>
- {/if}
-
- <!-- Pending Downloads Alert -->
- {#if settingsState.pendingDownloads.length > 0 && !settingsState.isDownloadingJava}
- <div class="border-t border-amber-500/30 p-4 bg-amber-500/10">
- <div class="flex items-center justify-between">
- <div class="flex items-center gap-3">
- <svg class="w-5 h-5 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
- </svg>
- <span class="text-sm text-amber-200">
- {settingsState.pendingDownloads.length} pending download(s) can be resumed
- </span>
- </div>
- <button
- onclick={() => settingsState.resumeDownloads()}
- class="px-4 py-2 bg-amber-500/20 hover:bg-amber-500/30 text-amber-200 rounded-lg text-sm font-medium transition-colors"
- >
- Resume All
- </button>
- </div>
- </div>
- {/if}
-
- <!-- Footer Actions -->
- <div class="flex items-center justify-between p-4 border-t border-white/10 bg-black/20">
- <button
- onclick={() => settingsState.refreshCatalog()}
- disabled={settingsState.isLoadingCatalog || settingsState.isDownloadingJava}
- class="flex items-center gap-2 px-4 py-2 bg-white/5 hover:bg-white/10 disabled:opacity-50 text-white/70 rounded-lg text-sm transition-colors"
- >
- <svg class="w-4 h-4 {settingsState.isLoadingCatalog ? 'animate-spin' : ''}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
- </svg>
- Refresh
- </button>
-
- <div class="flex gap-3">
- {#if settingsState.isDownloadingJava}
- <button
- onclick={() => settingsState.cancelDownload()}
- class="px-5 py-2.5 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-lg text-sm font-medium transition-colors"
- >
- Cancel Download
- </button>
- {:else}
- {@const isInstalled = settingsState.selectedRelease ? settingsState.getInstallStatus(settingsState.selectedRelease) === 'installed' : false}
- <button
- onclick={() => settingsState.closeJavaDownloadModal()}
- class="px-5 py-2.5 bg-white/10 hover:bg-white/20 text-white rounded-lg text-sm font-medium transition-colors"
- >
- Close
- </button>
- <button
- onclick={() => settingsState.downloadJava()}
- disabled={!settingsState.selectedRelease?.is_available || settingsState.isLoadingCatalog || isInstalled}
- class="px-5 py-2.5 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg text-sm font-medium transition-colors"
- >
- {isInstalled ? 'Already Installed' : 'Download & Install'}
- </button>
- {/if}
- </div>
- </div>
- </div>
- </div>
-{/if}
diff --git a/packages/ui/src/components/Sidebar.svelte b/packages/ui/src/components/Sidebar.svelte
deleted file mode 100644
index 83f4ac6..0000000
--- a/packages/ui/src/components/Sidebar.svelte
+++ /dev/null
@@ -1,91 +0,0 @@
-<script lang="ts">
- import { uiState } from '../stores/ui.svelte';
- import { Home, Package, Settings, Bot, Folder } from 'lucide-svelte';
-</script>
-
-<aside
- class="w-20 lg:w-64 dark:bg-[#09090b] bg-white border-r dark:border-white/10 border-gray-200 flex flex-col items-center lg:items-start transition-all duration-300 shrink-0 py-6 z-20"
->
- <!-- Logo Area -->
- <div
- class="h-16 w-full flex items-center justify-center lg:justify-start lg:px-6 mb-6"
- >
- <!-- Icon Logo (Small) -->
- <div class="lg:hidden text-black dark:text-white">
- <svg width="32" height="32" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M25 25 L50 50" stroke="currentColor" stroke-width="4" stroke-linecap="round" />
- <path d="M25 75 L50 50" stroke="currentColor" stroke-width="4" stroke-linecap="round" />
- <path d="M50 50 L75 50" stroke="currentColor" stroke-width="4" stroke-linecap="round" />
- <circle cx="25" cy="25" r="8" fill="currentColor" stroke="none" />
- <circle cx="25" cy="50" r="8" fill="currentColor" stroke="none" />
- <circle cx="25" cy="75" r="8" fill="currentColor" stroke="none" />
- <circle cx="50" cy="50" r="10" fill="currentColor" stroke="none" />
- <circle cx="75" cy="50" r="8" fill="currentColor" stroke="none" />
- </svg>
- </div>
- <!-- Full Logo (Large) -->
- <div
- class="hidden lg:flex items-center gap-3 font-bold text-xl tracking-tighter dark:text-white text-black"
- >
- <!-- Neural Network Dropout Logo -->
- <svg width="42" height="42" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg" class="shrink-0">
- <!-- Lines -->
- <path d="M25 25 L50 50" stroke="currentColor" stroke-width="4" stroke-linecap="round" />
- <path d="M25 75 L50 50" stroke="currentColor" stroke-width="4" stroke-linecap="round" />
- <path d="M50 50 L75 50" stroke="currentColor" stroke-width="4" stroke-linecap="round" />
-
- <!-- Input Layer (Left) -->
- <circle cx="25" cy="25" r="8" fill="currentColor" stroke="none" />
- <circle cx="25" cy="50" r="8" fill="currentColor" stroke="none" />
- <circle cx="25" cy="75" r="8" fill="currentColor" stroke="none" />
-
- <!-- Hidden Layer (Middle) - Dropout visualization -->
- <!-- Dropped units (dashed) -->
- <circle cx="50" cy="25" r="7" stroke="currentColor" stroke-width="2" stroke-dasharray="4 2" fill="none" class="opacity-30" />
- <circle cx="50" cy="75" r="7" stroke="currentColor" stroke-width="2" stroke-dasharray="4 2" fill="none" class="opacity-30" />
- <!-- Active unit -->
- <circle cx="50" cy="50" r="10" fill="currentColor" stroke="none" />
-
- <!-- Output Layer (Right) -->
- <circle cx="75" cy="50" r="8" fill="currentColor" stroke="none" />
- </svg>
-
- <span>DROPOUT</span>
- </div>
- </div>
-
- <!-- Navigation -->
- <nav class="flex-1 w-full flex flex-col gap-1 px-3">
- <!-- Nav Item Helper -->
- {#snippet navItem(view: any, Icon: any, label: string)}
- <button
- class="group flex items-center lg:gap-3 justify-center lg:justify-start w-full px-0 lg:px-4 py-2.5 rounded-sm transition-all duration-200 relative
- {uiState.currentView === view
- ? 'bg-black/5 dark:bg-white/10 dark:text-white text-black font-medium'
- : 'dark:text-zinc-400 text-zinc-500 hover:text-black dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5'}"
- onclick={() => uiState.setView(view)}
- >
- <Icon size={20} strokeWidth={uiState.currentView === view ? 2.5 : 2} />
- <span class="hidden lg:block text-sm relative z-10">{label}</span>
-
- <!-- Active Indicator -->
- {#if uiState.currentView === view}
- <div class="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-4 bg-black dark:bg-white rounded-r-full hidden lg:block"></div>
- {/if}
- </button>
- {/snippet}
-
- {@render navItem('home', Home, 'Overview')}
- {@render navItem('instances', Folder, 'Instances')}
- {@render navItem('versions', Package, 'Versions')}
- {@render navItem('guide', Bot, 'Assistant')}
- {@render navItem('settings', Settings, 'Settings')}
- </nav>
-
- <!-- Footer Info -->
- <div
- class="p-4 w-full flex justify-center lg:justify-start lg:px-6 opacity-40 hover:opacity-100 transition-opacity"
- >
- <div class="text-[10px] font-mono text-zinc-500 uppercase tracking-wider">v{uiState.appVersion}</div>
- </div>
-</aside>
diff --git a/packages/ui/src/components/StatusToast.svelte b/packages/ui/src/components/StatusToast.svelte
deleted file mode 100644
index 4c981c7..0000000
--- a/packages/ui/src/components/StatusToast.svelte
+++ /dev/null
@@ -1,42 +0,0 @@
-<script lang="ts">
- import { uiState } from "../stores/ui.svelte";
-</script>
-
-{#if uiState.status !== "Ready"}
- {#key uiState.status}
- <div
- class="absolute top-12 right-12 bg-white/90 dark:bg-zinc-800/90 backdrop-blur border border-zinc-200 dark:border-zinc-600 p-4 rounded-lg shadow-2xl max-w-sm animate-in fade-in slide-in-from-top-4 duration-300 z-50 group"
- >
- <div class="flex justify-between items-start mb-1">
- <div class="text-xs text-zinc-500 dark:text-zinc-400 uppercase font-bold">Status</div>
- <button
- onclick={() => uiState.setStatus("Ready")}
- class="text-zinc-400 hover:text-black dark:text-zinc-500 dark:hover:text-white transition -mt-1 -mr-1 p-1"
- >
- ✕
- </button>
- </div>
- <div class="font-mono text-sm whitespace-pre-wrap mb-2 text-gray-900 dark:text-gray-100">{uiState.status}</div>
- <div class="w-full bg-gray-200 dark:bg-zinc-700/50 h-1 rounded-full overflow-hidden">
- <div
- class="h-full bg-indigo-500 origin-left w-full progress-bar"
- ></div>
- </div>
- </div>
- {/key}
-{/if}
-
-<style>
- .progress-bar {
- animation: progress 5s linear forwards;
- }
-
- @keyframes progress {
- from {
- transform: scaleX(1);
- }
- to {
- transform: scaleX(0);
- }
- }
-</style>
diff --git a/packages/ui/src/components/VersionsView.svelte b/packages/ui/src/components/VersionsView.svelte
deleted file mode 100644
index f1474d9..0000000
--- a/packages/ui/src/components/VersionsView.svelte
+++ /dev/null
@@ -1,511 +0,0 @@
-<script lang="ts">
- import { invoke } from "@tauri-apps/api/core";
- import { listen, type UnlistenFn } from "@tauri-apps/api/event";
- import { gameState } from "../stores/game.svelte";
- import { instancesState } from "../stores/instances.svelte";
- import ModLoaderSelector from "./ModLoaderSelector.svelte";
-
- let searchQuery = $state("");
- let normalizedQuery = $derived(
- searchQuery.trim().toLowerCase().replace(/。/g, ".")
- );
-
- // Filter by version type
- let typeFilter = $state<"all" | "release" | "snapshot" | "installed">("all");
-
- // Installed modded versions with Java version info (Fabric + Forge)
- let installedFabricVersions = $state<Array<{ id: string; javaVersion?: number }>>([]);
- let isLoadingModded = $state(false);
-
- // Load installed modded versions with Java version info (both Fabric and Forge)
- async function loadInstalledModdedVersions() {
- if (!instancesState.activeInstanceId) {
- installedFabricVersions = [];
- isLoadingModded = false;
- return;
- }
- isLoadingModded = true;
- try {
- // Get all installed versions and filter for modded ones (Fabric and Forge)
- const allInstalled = await invoke<Array<{ id: string; type: string }>>(
- "list_installed_versions",
- { instanceId: instancesState.activeInstanceId }
- );
-
- // Filter for Fabric and Forge versions
- const moddedIds = allInstalled
- .filter(v => v.type === "fabric" || v.type === "forge")
- .map(v => v.id);
-
- // Load Java version for each installed modded version
- const versionsWithJava = await Promise.all(
- moddedIds.map(async (id) => {
- try {
- const javaVersion = await invoke<number | null>(
- "get_version_java_version",
- {
- instanceId: instancesState.activeInstanceId!,
- versionId: id,
- }
- );
- return {
- id,
- javaVersion: javaVersion ?? undefined,
- };
- } catch (e) {
- console.error(`Failed to get Java version for ${id}:`, e);
- return { id, javaVersion: undefined };
- }
- })
- );
-
- installedFabricVersions = versionsWithJava;
- } catch (e) {
- console.error("Failed to load installed modded versions:", e);
- } finally {
- isLoadingModded = false;
- }
- }
-
- let versionDeletedUnlisten: UnlistenFn | null = null;
- let downloadCompleteUnlisten: UnlistenFn | null = null;
- let versionInstalledUnlisten: UnlistenFn | null = null;
- let fabricInstalledUnlisten: UnlistenFn | null = null;
- let forgeInstalledUnlisten: UnlistenFn | null = null;
-
- // Load on mount and setup event listeners
- $effect(() => {
- loadInstalledModdedVersions();
- setupEventListeners();
- return () => {
- if (versionDeletedUnlisten) {
- versionDeletedUnlisten();
- }
- if (downloadCompleteUnlisten) {
- downloadCompleteUnlisten();
- }
- if (versionInstalledUnlisten) {
- versionInstalledUnlisten();
- }
- if (fabricInstalledUnlisten) {
- fabricInstalledUnlisten();
- }
- if (forgeInstalledUnlisten) {
- forgeInstalledUnlisten();
- }
- };
- });
-
- async function setupEventListeners() {
- // Refresh versions when a version is deleted
- versionDeletedUnlisten = await listen("version-deleted", async () => {
- await gameState.loadVersions();
- await loadInstalledModdedVersions();
- });
-
- // Refresh versions when a download completes (version installed)
- downloadCompleteUnlisten = await listen("download-complete", async () => {
- await gameState.loadVersions();
- await loadInstalledModdedVersions();
- });
-
- // Refresh when a version is installed
- versionInstalledUnlisten = await listen("version-installed", async () => {
- await gameState.loadVersions();
- await loadInstalledModdedVersions();
- });
-
- // Refresh when Fabric is installed
- fabricInstalledUnlisten = await listen("fabric-installed", async () => {
- await gameState.loadVersions();
- await loadInstalledModdedVersions();
- });
-
- // Refresh when Forge is installed
- forgeInstalledUnlisten = await listen("forge-installed", async () => {
- await gameState.loadVersions();
- await loadInstalledModdedVersions();
- });
- }
-
- // Combined versions list (vanilla + modded)
- let allVersions = $derived(() => {
- const moddedVersions = installedFabricVersions.map((v) => {
- // Determine type based on version ID
- 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: v.javaVersion,
- isInstalled: true, // Modded versions in the list are always installed
- };
- });
- return [...moddedVersions, ...gameState.versions];
- });
-
- let filteredVersions = $derived(() => {
- let versions = allVersions();
-
- // Apply type filter
- if (typeFilter === "release") {
- versions = versions.filter((v) => v.type === "release");
- } else if (typeFilter === "snapshot") {
- versions = versions.filter((v) => v.type === "snapshot");
- } else if (typeFilter === "installed") {
- versions = versions.filter((v) => v.isInstalled === true);
- }
-
- // Apply search filter
- if (normalizedQuery.length > 0) {
- versions = versions.filter((v) =>
- v.id.toLowerCase().includes(normalizedQuery)
- );
- }
-
- return versions;
- });
-
- function getVersionBadge(type: string) {
- switch (type) {
- case "release":
- return { text: "Release", class: "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-500/20 dark:text-emerald-300 dark:border-emerald-500/30" };
- case "snapshot":
- return { text: "Snapshot", class: "bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-500/20 dark:text-amber-300 dark:border-amber-500/30" };
- case "fabric":
- return { text: "Fabric", class: "bg-indigo-100 text-indigo-700 border-indigo-200 dark:bg-indigo-500/20 dark:text-indigo-300 dark:border-indigo-500/30" };
- case "forge":
- return { text: "Forge", class: "bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-500/20 dark:text-orange-300 dark:border-orange-500/30" };
- case "modpack":
- return { text: "Modpack", class: "bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-500/20 dark:text-purple-300 dark:border-purple-500/30" };
- default:
- return { text: type, class: "bg-zinc-100 text-zinc-700 border-zinc-200 dark:bg-zinc-500/20 dark:text-zinc-300 dark:border-zinc-500/30" };
- }
- }
-
- function handleModLoaderInstall(versionId: string) {
- // Refresh the installed versions list
- loadInstalledModdedVersions();
- // Refresh vanilla versions to update isInstalled status
- gameState.loadVersions();
- // Select the newly installed version
- gameState.selectedVersion = versionId;
- }
-
- // Delete confirmation dialog state
- let showDeleteDialog = $state(false);
- let versionToDelete = $state<string | null>(null);
-
- // Show delete confirmation dialog
- function showDeleteConfirmation(versionId: string, event: MouseEvent) {
- event.stopPropagation(); // Prevent version selection
- versionToDelete = versionId;
- showDeleteDialog = true;
- }
-
- // Cancel delete
- function cancelDelete() {
- showDeleteDialog = false;
- versionToDelete = null;
- }
-
- // Confirm and delete version
- async function confirmDelete() {
- if (!versionToDelete) return;
-
- try {
- await invoke("delete_version", {
- instanceId: instancesState.activeInstanceId,
- versionId: versionToDelete
- });
- // Clear selection if deleted version was selected
- if (gameState.selectedVersion === versionToDelete) {
- gameState.selectedVersion = "";
- }
- // Close dialog
- showDeleteDialog = false;
- versionToDelete = null;
- // Versions will be refreshed automatically via event listener
- } catch (e) {
- console.error("Failed to delete version:", e);
- alert(`Failed to delete version: ${e}`);
- // Keep dialog open on error so user can retry
- }
- }
-
- // Version metadata for the selected version
- interface VersionMetadata {
- id: string;
- javaVersion?: number;
- isInstalled: boolean;
- }
-
- let selectedVersionMetadata = $state<VersionMetadata | null>(null);
- let isLoadingMetadata = $state(false);
-
- // Load metadata when version is selected
- async function loadVersionMetadata(versionId: string) {
- if (!versionId) {
- selectedVersionMetadata = null;
- return;
- }
-
- isLoadingMetadata = true;
- try {
- const metadata = await invoke<VersionMetadata>("get_version_metadata", {
- instanceId: instancesState.activeInstanceId,
- versionId,
- });
- selectedVersionMetadata = metadata;
- } catch (e) {
- console.error("Failed to load version metadata:", e);
- selectedVersionMetadata = null;
- } finally {
- isLoadingMetadata = false;
- }
- }
-
- // Watch for selected version changes
- $effect(() => {
- if (gameState.selectedVersion) {
- loadVersionMetadata(gameState.selectedVersion);
- } else {
- selectedVersionMetadata = null;
- }
- });
-
- // Get the base Minecraft version from selected version (for mod loader selector)
- let selectedBaseVersion = $derived(() => {
- const selected = gameState.selectedVersion;
- if (!selected) return "";
-
- // If it's a modded version, extract the base version
- if (selected.startsWith("fabric-loader-")) {
- // Format: fabric-loader-X.X.X-1.20.4
- const parts = selected.split("-");
- return parts[parts.length - 1];
- }
- if (selected.includes("-forge-")) {
- // Format: 1.20.4-forge-49.0.38
- return selected.split("-forge-")[0];
- }
-
- // Check if it's a valid vanilla version
- const version = gameState.versions.find((v) => v.id === selected);
- return version ? selected : "";
- });
-</script>
-
-<div class="h-full flex flex-col p-6 overflow-hidden">
- <div class="flex items-center justify-between mb-6">
- <h2 class="text-3xl font-black bg-clip-text text-transparent bg-gradient-to-r from-gray-900 to-gray-600 dark:from-white dark:to-white/60">Version Manager</h2>
- <div class="text-sm dark:text-white/40 text-black/50">Select a version to play or modify</div>
- </div>
-
- <div class="flex-1 grid grid-cols-1 lg:grid-cols-3 gap-6 overflow-hidden">
- <!-- Left: Version List -->
- <div class="lg:col-span-2 flex flex-col gap-4 overflow-hidden">
- <!-- Search and Filters (Glass Bar) -->
- <div class="flex gap-3">
- <div class="relative flex-1">
- <span class="absolute left-3 top-1/2 -translate-y-1/2 dark:text-white/30 text-black/30">🔍</span>
- <input
- type="text"
- placeholder="Search versions..."
- class="w-full pl-9 pr-4 py-3 bg-white/60 dark:bg-black/20 border border-black/10 dark:border-white/10 rounded-xl dark:text-white text-gray-900 placeholder-black/30 dark:placeholder-white/30 focus:outline-none focus:border-indigo-500/50 dark:focus:bg-black/40 focus:bg-white/80 transition-all backdrop-blur-sm"
- bind:value={searchQuery}
- />
- </div>
- </div>
-
- <!-- Type Filter Tabs (Glass Caps) -->
- <div class="flex p-1 bg-white/60 dark:bg-black/20 rounded-xl border border-black/5 dark:border-white/5">
- {#each ['all', 'release', 'snapshot', 'installed'] as filter}
- <button
- class="flex-1 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 capitalize
- {typeFilter === filter
- ? 'bg-white shadow-sm border border-black/5 dark:bg-white/10 dark:text-white dark:shadow-lg dark:border-white/10 text-black'
- : 'text-black/40 dark:text-white/40 hover:text-black dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5'}"
- onclick={() => (typeFilter = filter as any)}
- >
- {filter}
- </button>
- {/each}
- </div>
-
- <!-- Version List SCROLL -->
- <div class="flex-1 overflow-y-auto pr-2 space-y-2 custom-scrollbar">
- {#if gameState.versions.length === 0}
- <div class="flex items-center justify-center h-40 dark:text-white/30 text-black/30 italic animate-pulse">
- Fetching manifest...
- </div>
- {:else if filteredVersions().length === 0}
- <div class="flex flex-col items-center justify-center h-40 dark:text-white/30 text-black/30 gap-2">
- <span class="text-2xl">👻</span>
- <span>No matching versions found</span>
- </div>
- {:else}
- {#each filteredVersions() as version}
- {@const badge = getVersionBadge(version.type)}
- {@const isSelected = gameState.selectedVersion === version.id}
- <button
- class="w-full group flex items-center justify-between p-4 rounded-xl text-left border transition-all duration-200 relative overflow-hidden
- {isSelected
- ? 'bg-indigo-50 border-indigo-200 dark:bg-indigo-600/20 dark:border-indigo-500/50 shadow-[0_0_20px_rgba(99,102,241,0.2)]'
- : 'bg-white/40 dark:bg-white/5 border-black/5 dark:border-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={() => (gameState.selectedVersion = version.id)}
- >
- <!-- Selection Glow -->
- {#if isSelected}
- <div class="absolute inset-0 bg-gradient-to-r from-indigo-500/10 to-transparent pointer-events-none"></div>
- {/if}
-
- <div class="relative z-10 flex items-center gap-4 flex-1">
- <span
- class="px-2.5 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wide border {badge.class}"
- >
- {badge.text}
- </span>
- <div class="flex-1">
- <div class="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 class="flex items-center gap-2 mt-0.5">
- {#if version.releaseTime && version.type !== "fabric" && version.type !== "forge"}
- <div class="text-xs dark:text-white/30 text-black/30">
- {new Date(version.releaseTime).toLocaleDateString()}
- </div>
- {/if}
- {#if version.javaVersion}
- <div class="flex items-center gap-1 text-xs dark:text-white/40 text-black/40">
- <span class="opacity-60">☕</span>
- <span class="font-medium">Java {version.javaVersion}</span>
- </div>
- {/if}
- </div>
- </div>
- </div>
-
- <div class="relative z-10 flex items-center gap-2">
- {#if version.isInstalled === true}
- <button
- onclick={(e) => showDeleteConfirmation(version.id, e)}
- class="p-2 rounded-lg text-red-500 dark:text-red-400 hover:bg-red-500/10 dark:hover:bg-red-500/20 transition-colors opacity-0 group-hover:opacity-100"
- title="Delete version"
- >
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <path d="M3 6h18"></path>
- <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
- <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
- </svg>
- </button>
- {/if}
- {#if isSelected}
- <div class="text-indigo-500 dark:text-indigo-400">
- <span class="text-lg">Selected</span>
- </div>
- {/if}
- </div>
- </button>
- {/each}
- {/if}
- </div>
- </div>
-
- <!-- Right: Mod Loader Panel -->
- <div class="flex flex-col gap-4">
- <!-- Selected Version Info Card -->
- <div class="bg-gradient-to-br from-white/40 to-white/20 dark:from-white/10 dark:to-white/5 p-6 rounded-2xl border border-black/5 dark:border-white/10 backdrop-blur-md relative overflow-hidden group">
- <div class="absolute top-0 right-0 p-8 bg-indigo-500/20 blur-[60px] rounded-full group-hover:bg-indigo-500/30 transition-colors"></div>
-
- <h3 class="text-xs font-bold uppercase tracking-widest dark:text-white/40 text-black/40 mb-2 relative z-10">Current Selection</h3>
- {#if gameState.selectedVersion}
- <p class="font-mono text-3xl font-black text-transparent bg-clip-text bg-gradient-to-r from-gray-900 to-gray-600 dark:from-white dark:to-white/70 relative z-10 truncate mb-4">
- {gameState.selectedVersion}
- </p>
-
- <!-- Version Metadata -->
- {#if isLoadingMetadata}
- <div class="space-y-3 relative z-10">
- <div class="animate-pulse space-y-2">
- <div class="h-4 bg-black/10 dark:bg-white/10 rounded w-3/4"></div>
- <div class="h-4 bg-black/10 dark:bg-white/10 rounded w-1/2"></div>
- </div>
- </div>
- {:else if selectedVersionMetadata}
- <div class="space-y-3 relative z-10">
- <!-- Java Version -->
- {#if selectedVersionMetadata.javaVersion}
- <div>
- <div class="text-[10px] font-bold uppercase tracking-wider dark:text-white/40 text-black/40 mb-1">Java Version</div>
- <div class="flex items-center gap-2">
- <span class="text-lg opacity-60">☕</span>
- <span class="text-sm dark:text-white text-black font-medium">
- Java {selectedVersionMetadata.javaVersion}
- </span>
- </div>
- </div>
- {/if}
-
- <!-- Installation Status -->
- <div>
- <div class="text-[10px] font-bold uppercase tracking-wider dark:text-white/40 text-black/40 mb-1">Status</div>
- <div class="flex items-center gap-2">
- {#if selectedVersionMetadata.isInstalled === true}
- <span class="px-2 py-0.5 bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 text-[10px] font-bold rounded border border-emerald-500/30">
- Installed
- </span>
- {:else if selectedVersionMetadata.isInstalled === false}
- <span class="px-2 py-0.5 bg-zinc-500/20 text-zinc-600 dark:text-zinc-400 text-[10px] font-bold rounded border border-zinc-500/30">
- Not Installed
- </span>
- {/if}
- </div>
- </div>
- </div>
- {/if}
- {:else}
- <p class="dark:text-white/20 text-black/20 italic relative z-10">None selected</p>
- {/if}
- </div>
-
- <!-- Mod Loader Selector Card -->
- <div class="bg-white/60 dark:bg-black/20 p-4 rounded-2xl border border-black/5 dark:border-white/5 backdrop-blur-sm flex-1 flex flex-col">
- <ModLoaderSelector
- selectedGameVersion={selectedBaseVersion()}
- onInstall={handleModLoaderInstall}
- />
- </div>
-
- </div>
- </div>
-
- <!-- Delete Version Confirmation Dialog -->
- {#if showDeleteDialog && versionToDelete}
- <div class="fixed inset-0 z-[200] bg-black/70 dark:bg-black/80 backdrop-blur-sm flex items-center justify-center p-4">
- <div class="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-xl shadow-2xl p-6 max-w-sm w-full animate-in fade-in zoom-in-95 duration-200">
- <h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2">Delete Version</h3>
- <p class="text-zinc-600 dark:text-zinc-400 text-sm mb-6">
- Are you sure you want to delete version <span class="text-gray-900 dark:text-white font-mono font-medium">{versionToDelete}</span>? This action cannot be undone.
- </p>
- <div class="flex gap-3 justify-end">
- <button
- onclick={cancelDelete}
- class="px-4 py-2 text-sm font-medium text-zinc-600 dark:text-zinc-300 hover:text-zinc-900 dark:hover:text-white bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded-lg transition-colors"
- >
- Cancel
- </button>
- <button
- onclick={confirmDelete}
- class="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-500 rounded-lg transition-colors"
- >
- Delete
- </button>
- </div>
- </div>
- </div>
- {/if}
-</div>
diff --git a/packages/ui-new/src/components/bottom-bar.tsx b/packages/ui/src/components/bottom-bar.tsx
index 32eb852..32eb852 100644
--- a/packages/ui-new/src/components/bottom-bar.tsx
+++ b/packages/ui/src/components/bottom-bar.tsx
diff --git a/packages/ui-new/src/components/config-editor.tsx b/packages/ui/src/components/config-editor.tsx
index 129b8f7..129b8f7 100644
--- a/packages/ui-new/src/components/config-editor.tsx
+++ b/packages/ui/src/components/config-editor.tsx
diff --git a/packages/ui-new/src/components/download-monitor.tsx b/packages/ui/src/components/download-monitor.tsx
index f3902d9..f3902d9 100644
--- a/packages/ui-new/src/components/download-monitor.tsx
+++ b/packages/ui/src/components/download-monitor.tsx
diff --git a/packages/ui-new/src/components/game-console.tsx b/packages/ui/src/components/game-console.tsx
index 6980c8c..6980c8c 100644
--- a/packages/ui-new/src/components/game-console.tsx
+++ b/packages/ui/src/components/game-console.tsx
diff --git a/packages/ui-new/src/components/instance-creation-modal.tsx b/packages/ui/src/components/instance-creation-modal.tsx
index 8a2b1b4..8a2b1b4 100644
--- a/packages/ui-new/src/components/instance-creation-modal.tsx
+++ b/packages/ui/src/components/instance-creation-modal.tsx
diff --git a/packages/ui-new/src/components/instance-editor-modal.tsx b/packages/ui/src/components/instance-editor-modal.tsx
index f880c20..f880c20 100644
--- a/packages/ui-new/src/components/instance-editor-modal.tsx
+++ b/packages/ui/src/components/instance-editor-modal.tsx
diff --git a/packages/ui-new/src/components/login-modal.tsx b/packages/ui/src/components/login-modal.tsx
index 49596da..49596da 100644
--- a/packages/ui-new/src/components/login-modal.tsx
+++ b/packages/ui/src/components/login-modal.tsx
diff --git a/packages/ui-new/src/components/particle-background.tsx b/packages/ui/src/components/particle-background.tsx
index 2e0b15a..2e0b15a 100644
--- a/packages/ui-new/src/components/particle-background.tsx
+++ b/packages/ui/src/components/particle-background.tsx
diff --git a/packages/ui-new/src/components/sidebar.tsx b/packages/ui/src/components/sidebar.tsx
index 0147b0a..0147b0a 100644
--- a/packages/ui-new/src/components/sidebar.tsx
+++ b/packages/ui/src/components/sidebar.tsx
diff --git a/packages/ui-new/src/components/ui/avatar.tsx b/packages/ui/src/components/ui/avatar.tsx
index 9fd72a2..9fd72a2 100644
--- a/packages/ui-new/src/components/ui/avatar.tsx
+++ b/packages/ui/src/components/ui/avatar.tsx
diff --git a/packages/ui-new/src/components/ui/badge.tsx b/packages/ui/src/components/ui/badge.tsx
index 425ab9e..425ab9e 100644
--- a/packages/ui-new/src/components/ui/badge.tsx
+++ b/packages/ui/src/components/ui/badge.tsx
diff --git a/packages/ui-new/src/components/ui/button.tsx b/packages/ui/src/components/ui/button.tsx
index 7dee494..7dee494 100644
--- a/packages/ui-new/src/components/ui/button.tsx
+++ b/packages/ui/src/components/ui/button.tsx
diff --git a/packages/ui-new/src/components/ui/card.tsx b/packages/ui/src/components/ui/card.tsx
index b7084a0..b7084a0 100644
--- a/packages/ui-new/src/components/ui/card.tsx
+++ b/packages/ui/src/components/ui/card.tsx
diff --git a/packages/ui-new/src/components/ui/checkbox.tsx b/packages/ui/src/components/ui/checkbox.tsx
index 9f22cea..9f22cea 100644
--- a/packages/ui-new/src/components/ui/checkbox.tsx
+++ b/packages/ui/src/components/ui/checkbox.tsx
diff --git a/packages/ui-new/src/components/ui/dialog.tsx b/packages/ui/src/components/ui/dialog.tsx
index 033b47c..033b47c 100644
--- a/packages/ui-new/src/components/ui/dialog.tsx
+++ b/packages/ui/src/components/ui/dialog.tsx
diff --git a/packages/ui-new/src/components/ui/dropdown-menu.tsx b/packages/ui/src/components/ui/dropdown-menu.tsx
index ee97374..ee97374 100644
--- a/packages/ui-new/src/components/ui/dropdown-menu.tsx
+++ b/packages/ui/src/components/ui/dropdown-menu.tsx
diff --git a/packages/ui-new/src/components/ui/field.tsx b/packages/ui/src/components/ui/field.tsx
index ab9fb71..ab9fb71 100644
--- a/packages/ui-new/src/components/ui/field.tsx
+++ b/packages/ui/src/components/ui/field.tsx
diff --git a/packages/ui-new/src/components/ui/input.tsx b/packages/ui/src/components/ui/input.tsx
index bb0390a..bb0390a 100644
--- a/packages/ui-new/src/components/ui/input.tsx
+++ b/packages/ui/src/components/ui/input.tsx
diff --git a/packages/ui-new/src/components/ui/label.tsx b/packages/ui/src/components/ui/label.tsx
index 9a998c7..9a998c7 100644
--- a/packages/ui-new/src/components/ui/label.tsx
+++ b/packages/ui/src/components/ui/label.tsx
diff --git a/packages/ui-new/src/components/ui/scroll-area.tsx b/packages/ui/src/components/ui/scroll-area.tsx
index 4a68eb2..4a68eb2 100644
--- a/packages/ui-new/src/components/ui/scroll-area.tsx
+++ b/packages/ui/src/components/ui/scroll-area.tsx
diff --git a/packages/ui-new/src/components/ui/select.tsx b/packages/ui/src/components/ui/select.tsx
index 210adba..210adba 100644
--- a/packages/ui-new/src/components/ui/select.tsx
+++ b/packages/ui/src/components/ui/select.tsx
diff --git a/packages/ui-new/src/components/ui/separator.tsx b/packages/ui/src/components/ui/separator.tsx
index e91a862..e91a862 100644
--- a/packages/ui-new/src/components/ui/separator.tsx
+++ b/packages/ui/src/components/ui/separator.tsx
diff --git a/packages/ui-new/src/components/ui/sonner.tsx b/packages/ui/src/components/ui/sonner.tsx
index d6e293d..d6e293d 100644
--- a/packages/ui-new/src/components/ui/sonner.tsx
+++ b/packages/ui/src/components/ui/sonner.tsx
diff --git a/packages/ui-new/src/components/ui/spinner.tsx b/packages/ui/src/components/ui/spinner.tsx
index 91f6a63..91f6a63 100644
--- a/packages/ui-new/src/components/ui/spinner.tsx
+++ b/packages/ui/src/components/ui/spinner.tsx
diff --git a/packages/ui-new/src/components/ui/switch.tsx b/packages/ui/src/components/ui/switch.tsx
index fef14e3..fef14e3 100644
--- a/packages/ui-new/src/components/ui/switch.tsx
+++ b/packages/ui/src/components/ui/switch.tsx
diff --git a/packages/ui-new/src/components/ui/tabs.tsx b/packages/ui/src/components/ui/tabs.tsx
index c66893f..c66893f 100644
--- a/packages/ui-new/src/components/ui/tabs.tsx
+++ b/packages/ui/src/components/ui/tabs.tsx
diff --git a/packages/ui-new/src/components/ui/textarea.tsx b/packages/ui/src/components/ui/textarea.tsx
index 3c3e5d0..3c3e5d0 100644
--- a/packages/ui-new/src/components/ui/textarea.tsx
+++ b/packages/ui/src/components/ui/textarea.tsx
diff --git a/packages/ui-new/src/components/user-avatar.tsx b/packages/ui/src/components/user-avatar.tsx
index bbdb84c..bbdb84c 100644
--- a/packages/ui-new/src/components/user-avatar.tsx
+++ b/packages/ui/src/components/user-avatar.tsx
diff --git a/packages/ui-new/src/index.css b/packages/ui/src/index.css
index 8803e5e..8803e5e 100644
--- a/packages/ui-new/src/index.css
+++ b/packages/ui/src/index.css
diff --git a/packages/ui/src/lib/Counter.svelte b/packages/ui/src/lib/Counter.svelte
deleted file mode 100644
index 37d75ce..0000000
--- a/packages/ui/src/lib/Counter.svelte
+++ /dev/null
@@ -1,10 +0,0 @@
-<script lang="ts">
- let count: number = $state(0)
- const increment = () => {
- count += 1
- }
-</script>
-
-<button onclick={increment}>
- count is {count}
-</button>
diff --git a/packages/ui/src/lib/DownloadMonitor.svelte b/packages/ui/src/lib/DownloadMonitor.svelte
deleted file mode 100644
index 860952c..0000000
--- a/packages/ui/src/lib/DownloadMonitor.svelte
+++ /dev/null
@@ -1,201 +0,0 @@
-<script lang="ts">
- import { listen } from "@tauri-apps/api/event";
- import { onMount, onDestroy } from "svelte";
-
- export let visible = false;
-
- interface DownloadEvent {
- file: string;
- downloaded: number; // in bytes
- total: number; // in bytes
- status: string;
- completed_files: number;
- total_files: number;
- total_downloaded_bytes: number;
- }
-
- let currentFile = "";
- let progress = 0; // percentage 0-100 (current file)
- let totalProgress = 0; // percentage 0-100 (all files)
- let totalFiles = 0;
- let completedFiles = 0;
- let statusText = "Preparing...";
- let unlistenProgress: () => void;
- let unlistenStart: () => void;
- let unlistenComplete: () => void;
- let downloadedBytes = 0;
- let totalBytes = 0;
-
- // Speed and ETA tracking
- let downloadSpeed = 0; // bytes per second
- let etaSeconds = 0;
- let startTime = 0;
- let totalDownloadedBytes = 0;
- let lastUpdateTime = 0;
- let lastTotalBytes = 0;
-
- onMount(async () => {
- unlistenStart = await listen<number>("download-start", (event) => {
- visible = true;
- totalFiles = event.payload;
- completedFiles = 0;
- progress = 0;
- totalProgress = 0;
- statusText = "Starting download...";
- currentFile = "";
- // Reset speed tracking
- startTime = Date.now();
- totalDownloadedBytes = 0;
- downloadSpeed = 0;
- etaSeconds = 0;
- lastUpdateTime = Date.now();
- lastTotalBytes = 0;
- });
-
- unlistenProgress = await listen<DownloadEvent>(
- "download-progress",
- (event) => {
- const payload = event.payload;
- currentFile = payload.file;
-
- // Current file progress
- downloadedBytes = payload.downloaded;
- totalBytes = payload.total;
-
- statusText = payload.status;
-
- if (payload.total > 0) {
- progress = (payload.downloaded / payload.total) * 100;
- }
-
- // Total progress (all files)
- completedFiles = payload.completed_files;
- totalFiles = payload.total_files;
- if (totalFiles > 0) {
- const currentFileFraction =
- payload.total > 0 ? payload.downloaded / payload.total : 0;
- totalProgress = ((completedFiles + currentFileFraction) / totalFiles) * 100;
- }
-
- // Calculate download speed (using moving average)
- totalDownloadedBytes = payload.total_downloaded_bytes;
- const now = Date.now();
- const timeDiff = (now - lastUpdateTime) / 1000; // seconds
-
- if (timeDiff >= 0.5) { // Update speed every 0.5 seconds
- const bytesDiff = totalDownloadedBytes - lastTotalBytes;
- const instantSpeed = bytesDiff / timeDiff;
- // Smooth the speed with exponential moving average
- downloadSpeed = downloadSpeed === 0 ? instantSpeed : downloadSpeed * 0.7 + instantSpeed * 0.3;
- lastUpdateTime = now;
- lastTotalBytes = totalDownloadedBytes;
- }
-
- // Estimate remaining time
- if (downloadSpeed > 0 && completedFiles < totalFiles) {
- const remainingFiles = totalFiles - completedFiles;
- let estimatedRemainingBytes: number;
-
- if (completedFiles > 0) {
- // Use average size of completed files to estimate remaining files
- const avgBytesPerCompletedFile = totalDownloadedBytes / completedFiles;
- estimatedRemainingBytes = avgBytesPerCompletedFile * remainingFiles;
- } else {
- // No completed files yet: estimate based only on current file's remaining bytes
- estimatedRemainingBytes = Math.max(totalBytes - downloadedBytes, 0);
- }
- etaSeconds = estimatedRemainingBytes / downloadSpeed;
- } else {
- etaSeconds = 0;
- }
- }
- );
-
- unlistenComplete = await listen("download-complete", () => {
- statusText = "Done!";
- progress = 100;
- totalProgress = 100;
- setTimeout(() => {
- visible = false;
- }, 2000);
- });
- });
-
- onDestroy(() => {
- if (unlistenProgress) unlistenProgress();
- if (unlistenStart) unlistenStart();
- if (unlistenComplete) unlistenComplete();
- });
-
- function formatBytes(bytes: number) {
- if (bytes === 0) return "0 B";
- const k = 1024;
- const sizes = ["B", "KB", "MB", "GB"];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
- }
-
- function formatSpeed(bytesPerSecond: number) {
- if (bytesPerSecond === 0) return "-- /s";
- return formatBytes(bytesPerSecond) + "/s";
- }
-
- function formatTime(seconds: number) {
- if (seconds <= 0 || !isFinite(seconds)) return "--";
- if (seconds < 60) return `${Math.round(seconds)}s`;
- if (seconds < 3600) {
- const mins = Math.floor(seconds / 60);
- const secs = Math.round(seconds % 60);
- return `${mins}m ${secs}s`;
- }
- const hours = Math.floor(seconds / 3600);
- const mins = Math.floor((seconds % 3600) / 60);
- return `${hours}h ${mins}m`;
- }
-</script>
-
-{#if visible}
- <div
- class="fixed bottom-28 right-8 z-50 w-80 bg-zinc-900/90 border border-zinc-700 rounded-lg shadow-2xl p-4 animate-in slide-in-from-right-10 fade-in duration-300"
- >
- <div class="flex items-center justify-between mb-2">
- <h3 class="text-white font-bold text-sm">Downloads</h3>
- <span class="text-xs text-zinc-400">{statusText}</span>
- </div>
-
- <!-- Total Progress Bar -->
- <div class="mb-3">
- <div class="flex justify-between text-[10px] text-zinc-400 mb-1">
- <span>Total Progress</span>
- <span>{completedFiles} / {totalFiles} files</span>
- </div>
- <div class="w-full bg-zinc-800 rounded-full h-2.5 overflow-hidden">
- <div
- class="bg-gradient-to-r from-blue-500 to-cyan-400 h-2.5 rounded-full transition-all duration-200"
- style="width: {totalProgress}%"
- ></div>
- </div>
- <div class="flex justify-between text-[10px] text-zinc-500 font-mono mt-0.5">
- <span>{formatSpeed(downloadSpeed)} · ETA: {formatTime(etaSeconds)}</span>
- <span>{completedFiles < totalFiles ? Math.floor(totalProgress) : 100}%</span>
- </div>
- </div>
-
- <div class="text-xs text-zinc-300 truncate mb-1" title={currentFile}>
- {currentFile || "Waiting..."}
- </div>
-
- <!-- Current File Progress Bar -->
- <div class="w-full bg-zinc-800 rounded-full h-2 mb-2 overflow-hidden">
- <div
- class="bg-gradient-to-r from-green-500 to-emerald-400 h-2 rounded-full transition-all duration-200"
- style="width: {progress}%"
- ></div>
- </div>
-
- <div class="flex justify-between text-[10px] text-zinc-500 font-mono">
- <span>{formatBytes(downloadedBytes)} / {formatBytes(totalBytes)}</span>
- <span>{Math.round(progress)}%</span>
- </div>
- </div>
-{/if}
diff --git a/packages/ui/src/lib/GameConsole.svelte b/packages/ui/src/lib/GameConsole.svelte
deleted file mode 100644
index bc5edbc..0000000
--- a/packages/ui/src/lib/GameConsole.svelte
+++ /dev/null
@@ -1,304 +0,0 @@
-<script lang="ts">
- import { logsState, type LogEntry } from "../stores/logs.svelte";
- import { uiState } from "../stores/ui.svelte";
- import { save } from "@tauri-apps/plugin-dialog";
- import { writeTextFile } from "@tauri-apps/plugin-fs";
- import { invoke } from "@tauri-apps/api/core";
- import { open } from "@tauri-apps/plugin-shell";
- import { onMount, tick } from "svelte";
- import CustomSelect from "../components/CustomSelect.svelte";
- import { ChevronDown, Check } from 'lucide-svelte';
-
- let consoleElement: HTMLDivElement;
- let autoScroll = $state(true);
-
- // Search & Filter
- let searchQuery = $state("");
- let showInfo = $state(true);
- let showWarn = $state(true);
- let showError = $state(true);
- let showDebug = $state(false);
-
- // Source filter: "all" or specific source name
- let selectedSource = $state("all");
-
- // Get sorted sources for dropdown
- let sourceOptions = $derived([
- { value: "all", label: "All Sources" },
- ...[...logsState.sources].sort().map(s => ({ value: s, label: s }))
- ]);
-
- // Derived filtered logs
- let filteredLogs = $derived(logsState.logs.filter((log) => {
- // Source Filter
- if (selectedSource !== "all" && log.source !== selectedSource) return false;
-
- // Level Filter
- if (!showInfo && log.level === "info") return false;
- if (!showWarn && log.level === "warn") return false;
- if (!showError && (log.level === "error" || log.level === "fatal")) return false;
- if (!showDebug && log.level === "debug") return false;
-
- // Search Filter
- if (searchQuery) {
- const q = searchQuery.toLowerCase();
- return (
- log.message.toLowerCase().includes(q) ||
- log.source.toLowerCase().includes(q)
- );
- }
- return true;
- }));
-
- // Auto-scroll logic
- $effect(() => {
- // Depend on filteredLogs length to trigger scroll
- if (filteredLogs.length && autoScroll && consoleElement) {
- // Use tick to wait for DOM update
- tick().then(() => {
- consoleElement.scrollTop = consoleElement.scrollHeight;
- });
- }
- });
-
- function handleScroll() {
- if (!consoleElement) return;
- const { scrollTop, scrollHeight, clientHeight } = consoleElement;
- // If user scrolls up (more than 50px from bottom), disable auto-scroll
- if (scrollHeight - scrollTop - clientHeight > 50) {
- autoScroll = false;
- } else {
- autoScroll = true;
- }
- }
-
- // Export only currently filtered logs
- async function exportLogs() {
- try {
- const content = logsState.exportLogs(filteredLogs);
- const path = await save({
- filters: [{ name: "Log File", extensions: ["txt", "log"] }],
- defaultPath: `dropout-logs-${new Date().toISOString().replace(/[:.]/g, "-")}.log`,
- });
-
- if (path) {
- await writeTextFile(path, content);
- logsState.addLog("info", "Console", `Exported ${filteredLogs.length} logs to ${path}`);
- }
- } catch (e) {
- console.error("Export failed", e);
- logsState.addLog("error", "Console", `Export failed: ${e}`);
- }
- }
-
- // Upload only currently filtered logs
- async function uploadLogs() {
- try {
- const content = logsState.exportLogs(filteredLogs);
- logsState.addLog("info", "Console", `Uploading ${filteredLogs.length} logs...`);
-
- const response = await invoke<{ url: string }>("upload_to_pastebin", { content });
-
- logsState.addLog("info", "Console", `Logs uploaded successfully: ${response.url}`);
- await open(response.url);
- } catch (e) {
- console.error("Upload failed", e);
- logsState.addLog("error", "Console", `Upload failed: ${e}`);
- }
- }
-
- function highlightText(text: string, query: string) {
- if (!query) return text;
- // Escape regex special chars in query
- const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- const parts = text.split(new RegExp(`(${escaped})`, "gi"));
- return parts.map(part =>
- part.toLowerCase() === query.toLowerCase()
- ? `<span class="bg-yellow-500/30 text-yellow-200 font-bold">${part}</span>`
- : part
- ).join("");
- }
-
- function getLevelColor(level: LogEntry["level"]) {
- switch (level) {
- case "info": return "text-blue-400";
- case "warn": return "text-yellow-400";
- case "error":
- case "fatal": return "text-red-400";
- case "debug": return "text-purple-400";
- default: return "text-zinc-400";
- }
- }
-
- function getLevelLabel(level: LogEntry["level"]) {
- switch (level) {
- case "info": return "INFO";
- case "warn": return "WARN";
- case "error": return "ERR";
- case "fatal": return "FATAL";
- case "debug": return "DEBUG";
- }
- }
-
- function getMessageColor(log: LogEntry) {
- if (log.level === "error" || log.level === "fatal") return "text-red-300";
- if (log.level === "warn") return "text-yellow-200";
- if (log.level === "debug") return "text-purple-200/70";
- if (log.source.startsWith("Game")) return "text-emerald-100/80";
- return "";
- }
-</script>
-
-<div class="absolute inset-0 flex flex-col bg-[#1e1e1e] text-zinc-300 font-mono text-xs overflow-hidden">
- <!-- Toolbar -->
- <div class="flex flex-wrap items-center justify-between p-2 bg-[#252526] border-b border-[#3e3e42] gap-2">
- <div class="flex items-center gap-3">
- <h3 class="font-bold text-zinc-100 uppercase tracking-wider px-2">Console</h3>
-
- <!-- Source Dropdown -->
- <CustomSelect
- options={sourceOptions}
- bind:value={selectedSource}
- class="w-36"
- />
-
- <!-- Level Filters -->
- <div class="flex items-center bg-[#1e1e1e] rounded border border-[#3e3e42] overflow-hidden">
- <button
- class="px-2 py-1 hover:bg-[#3e3e42] transition-colors {showInfo ? 'text-blue-400' : 'text-zinc-600'}"
- onclick={() => showInfo = !showInfo}
- title="Toggle Info"
- >Info</button>
- <div class="w-px h-3 bg-[#3e3e42]"></div>
- <button
- class="px-2 py-1 hover:bg-[#3e3e42] transition-colors {showWarn ? 'text-yellow-400' : 'text-zinc-600'}"
- onclick={() => showWarn = !showWarn}
- title="Toggle Warnings"
- >Warn</button>
- <div class="w-px h-3 bg-[#3e3e42]"></div>
- <button
- class="px-2 py-1 hover:bg-[#3e3e42] transition-colors {showError ? 'text-red-400' : 'text-zinc-600'}"
- onclick={() => showError = !showError}
- title="Toggle Errors"
- >Error</button>
- <div class="w-px h-3 bg-[#3e3e42]"></div>
- <button
- class="px-2 py-1 hover:bg-[#3e3e42] transition-colors {showDebug ? 'text-purple-400' : 'text-zinc-600'}"
- onclick={() => showDebug = !showDebug}
- title="Toggle Debug"
- >Debug</button>
- </div>
-
- <!-- Search -->
- <div class="relative group">
- <input
- type="text"
- bind:value={searchQuery}
- placeholder="Find..."
- class="bg-[#1e1e1e] border border-[#3e3e42] rounded pl-8 pr-2 py-1 focus:border-indigo-500 focus:outline-none w-40 text-zinc-300 placeholder:text-zinc-600 transition-all focus:w-64"
- />
- <svg class="w-3.5 h-3.5 text-zinc-500 absolute left-2.5 top-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
- {#if searchQuery}
- <button class="absolute right-2 top-1.5 text-zinc-500 hover:text-white" onclick={() => searchQuery = ""}>✕</button>
- {/if}
- </div>
- </div>
-
- <!-- Actions -->
- <div class="flex items-center gap-2">
- <!-- Log count indicator -->
- <span class="text-zinc-500 text-[10px] px-2">{filteredLogs.length} / {logsState.logs.length}</span>
-
- <button
- onclick={() => logsState.clear()}
- class="p-1.5 hover:bg-[#3e3e42] rounded text-zinc-400 hover:text-white transition-colors"
- title="Clear Logs"
- >
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
- </button>
- <button
- onclick={exportLogs}
- class="p-1.5 hover:bg-[#3e3e42] rounded text-zinc-400 hover:text-white transition-colors"
- title="Export Filtered Logs"
- >
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
- </button>
- <button
- onclick={uploadLogs}
- class="p-1.5 hover:bg-[#3e3e42] rounded text-zinc-400 hover:text-white transition-colors"
- title="Upload Filtered Logs"
- >
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/></svg>
- </button>
- <div class="w-px h-4 bg-[#3e3e42] mx-1"></div>
- <button
- onclick={() => uiState.toggleConsole()}
- class="p-1.5 hover:bg-red-500/20 hover:text-red-400 rounded text-zinc-400 transition-colors"
- title="Close"
- >
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
- </button>
- </div>
- </div>
-
- <!-- Log Area -->
- <div
- bind:this={consoleElement}
- onscroll={handleScroll}
- class="flex-1 overflow-y-auto overflow-x-hidden p-2 select-text custom-scrollbar"
- >
- {#each filteredLogs as log (log.id)}
- <div class="flex gap-2 leading-relaxed hover:bg-[#2a2d2e] px-1 rounded-sm group">
- <!-- Timestamp -->
- <span class="text-zinc-500 shrink-0 select-none w-20 text-right opacity-50 group-hover:opacity-100">{log.timestamp.split('.')[0]}</span>
-
- <!-- Source & Level -->
- <div class="flex shrink-0 min-w-[140px] gap-1 justify-end font-bold select-none truncate">
- <span class="text-zinc-500 truncate max-w-[90px]" title={log.source}>{log.source}</span>
- <span class={getLevelColor(log.level)}>{getLevelLabel(log.level)}</span>
- </div>
-
- <!-- Message -->
- <div class="flex-1 break-all whitespace-pre-wrap text-zinc-300 min-w-0 {getMessageColor(log)}">
- {@html highlightText(log.message, searchQuery)}
- </div>
- </div>
- {/each}
-
- {#if filteredLogs.length === 0}
- <div class="text-center text-zinc-600 mt-10 italic select-none">
- {#if logsState.logs.length === 0}
- Waiting for logs...
- {:else}
- No logs match current filters.
- {/if}
- </div>
- {/if}
- </div>
-
- <!-- Auto-scroll status -->
- {#if !autoScroll}
- <button
- onclick={() => { autoScroll = true; consoleElement.scrollTop = consoleElement.scrollHeight; }}
- class="absolute bottom-4 right-6 bg-indigo-600 hover:bg-indigo-500 text-white px-3 py-1.5 rounded shadow-lg text-xs font-bold transition-all animate-bounce"
- >
- Resume Auto-scroll ⬇
- </button>
- {/if}
-</div>
-
-<style>
- /* Custom Scrollbar for the log area */
- .custom-scrollbar::-webkit-scrollbar {
- width: 10px;
- background-color: #1e1e1e;
- }
- .custom-scrollbar::-webkit-scrollbar-thumb {
- background-color: #424242;
- border: 2px solid #1e1e1e; /* padding around thumb */
- border-radius: 0;
- }
- .custom-scrollbar::-webkit-scrollbar-thumb:hover {
- background-color: #4f4f4f;
- }
-</style>
diff --git a/packages/ui/src/lib/effects/ConstellationEffect.ts b/packages/ui/src/lib/effects/ConstellationEffect.ts
deleted file mode 100644
index d2db529..0000000
--- a/packages/ui/src/lib/effects/ConstellationEffect.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-export class ConstellationEffect {
- private canvas: HTMLCanvasElement;
- private ctx: CanvasRenderingContext2D;
- private width: number = 0;
- private height: number = 0;
- private particles: Particle[] = [];
- private animationId: number = 0;
- private mouseX: number = -1000;
- private mouseY: number = -1000;
-
- // Configuration
- private readonly particleCount = 100;
- private readonly connectionDistance = 150;
- private readonly particleSpeed = 0.5;
-
- constructor(canvas: HTMLCanvasElement) {
- this.canvas = canvas;
- this.ctx = canvas.getContext("2d", { alpha: true })!;
-
- // Bind methods
- this.animate = this.animate.bind(this);
- this.handleMouseMove = this.handleMouseMove.bind(this);
-
- // Initial setup
- this.resize(window.innerWidth, window.innerHeight);
- this.initParticles();
-
- // Mouse interaction
- window.addEventListener("mousemove", this.handleMouseMove);
-
- // Start animation
- this.animate();
- }
-
- resize(width: number, height: number) {
- const dpr = window.devicePixelRatio || 1;
- this.width = width;
- this.height = height;
-
- this.canvas.width = width * dpr;
- this.canvas.height = height * dpr;
- this.canvas.style.width = `${width}px`;
- this.canvas.style.height = `${height}px`;
-
- this.ctx.scale(dpr, dpr);
-
- // Re-initialize if screen size changes significantly to maintain density
- if (this.particles.length === 0) {
- this.initParticles();
- }
- }
-
- private initParticles() {
- this.particles = [];
- // Adjust density based on screen area
- const area = this.width * this.height;
- const density = Math.floor(area / 15000); // 1 particle per 15000px²
- const count = Math.min(Math.max(density, 50), 200); // Clamp between 50 and 200
-
- for (let i = 0; i < count; i++) {
- this.particles.push(new Particle(this.width, this.height, this.particleSpeed));
- }
- }
-
- private handleMouseMove(e: MouseEvent) {
- const rect = this.canvas.getBoundingClientRect();
- this.mouseX = e.clientX - rect.left;
- this.mouseY = e.clientY - rect.top;
- }
-
- animate() {
- this.ctx.clearRect(0, 0, this.width, this.height);
-
- // Update and draw particles
- this.particles.forEach((p) => {
- p.update(this.width, this.height);
- p.draw(this.ctx);
- });
-
- // Draw lines
- this.drawConnections();
-
- this.animationId = requestAnimationFrame(this.animate);
- }
-
- private drawConnections() {
- this.ctx.lineWidth = 1;
-
- for (let i = 0; i < this.particles.length; i++) {
- const p1 = this.particles[i];
-
- // Connect to mouse if close
- const distMouse = Math.hypot(p1.x - this.mouseX, p1.y - this.mouseY);
- if (distMouse < this.connectionDistance + 50) {
- const alpha = 1 - distMouse / (this.connectionDistance + 50);
- this.ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.4})`; // Brighter near mouse
- this.ctx.beginPath();
- this.ctx.moveTo(p1.x, p1.y);
- this.ctx.lineTo(this.mouseX, this.mouseY);
- this.ctx.stroke();
-
- // Gently attract to mouse
- if (distMouse > 10) {
- p1.x += (this.mouseX - p1.x) * 0.005;
- p1.y += (this.mouseY - p1.y) * 0.005;
- }
- }
-
- // Connect to other particles
- for (let j = i + 1; j < this.particles.length; j++) {
- const p2 = this.particles[j];
- const dist = Math.hypot(p1.x - p2.x, p1.y - p2.y);
-
- if (dist < this.connectionDistance) {
- const alpha = 1 - dist / this.connectionDistance;
- this.ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.15})`;
- this.ctx.beginPath();
- this.ctx.moveTo(p1.x, p1.y);
- this.ctx.lineTo(p2.x, p2.y);
- this.ctx.stroke();
- }
- }
- }
- }
-
- destroy() {
- cancelAnimationFrame(this.animationId);
- window.removeEventListener("mousemove", this.handleMouseMove);
- }
-}
-
-class Particle {
- x: number;
- y: number;
- vx: number;
- vy: number;
- size: number;
-
- constructor(w: number, h: number, speed: number) {
- this.x = Math.random() * w;
- this.y = Math.random() * h;
- this.vx = (Math.random() - 0.5) * speed;
- this.vy = (Math.random() - 0.5) * speed;
- this.size = Math.random() * 2 + 1;
- }
-
- update(w: number, h: number) {
- this.x += this.vx;
- this.y += this.vy;
-
- // Bounce off walls
- if (this.x < 0 || this.x > w) this.vx *= -1;
- if (this.y < 0 || this.y > h) this.vy *= -1;
- }
-
- draw(ctx: CanvasRenderingContext2D) {
- ctx.fillStyle = "rgba(255, 255, 255, 0.4)";
- ctx.beginPath();
- ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
- ctx.fill();
- }
-}
diff --git a/packages/ui/src/lib/effects/SaturnEffect.ts b/packages/ui/src/lib/effects/SaturnEffect.ts
index 357da9d..497a340 100644
--- a/packages/ui/src/lib/effects/SaturnEffect.ts
+++ b/packages/ui/src/lib/effects/SaturnEffect.ts
@@ -1,46 +1,61 @@
-// Optimized Saturn Effect for low-end hardware
-// Uses TypedArrays for memory efficiency and reduced particle density
+/**
+ * Ported SaturnEffect for the React UI (ui-new).
+ * Adapted from the original Svelte implementation but written as a standalone
+ * TypeScript class that manages a 2D canvas particle effect resembling a
+ * rotating "Saturn" with rings. Designed to be instantiated and controlled
+ * from a React component (e.g. ParticleBackground).
+ *
+ * Usage:
+ * const effect = new SaturnEffect(canvasElement);
+ * effect.handleMouseDown(clientX);
+ * effect.handleMouseMove(clientX);
+ * effect.handleMouseUp();
+ * // on resize:
+ * effect.resize(width, height);
+ * // on unmount:
+ * effect.destroy();
+ */
export class SaturnEffect {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
- private width: number = 0;
- private height: number = 0;
-
- // Data-oriented design for performance
- // xyz: Float32Array where [i*3, i*3+1, i*3+2] corresponds to x, y, z
- private xyz: Float32Array | null = null;
- // types: Uint8Array where 0 = planet, 1 = ring
- private types: Uint8Array | null = null;
- private count: number = 0;
-
- private animationId: number = 0;
- private angle: number = 0;
- private scaleFactor: number = 1;
-
- // Mouse interaction properties
- private isDragging: boolean = false;
- private lastMouseX: number = 0;
- private lastMouseTime: number = 0;
- private mouseVelocities: number[] = []; // Store recent velocities for averaging
-
- // Rotation speed control
- private readonly baseSpeed: number = 0.005; // Original rotation speed
- private currentSpeed: number = 0.005; // Current rotation speed (can be modified by mouse)
- private rotationDirection: number = 1; // 1 for clockwise, -1 for counter-clockwise
- private readonly speedDecayRate: number = 0.992; // How fast speed returns to normal (closer to 1 = slower decay)
- private readonly minSpeedMultiplier: number = 1; // Minimum speed is baseSpeed
- private readonly maxSpeedMultiplier: number = 50; // Maximum speed is 50x baseSpeed
- private isStopped: boolean = false; // Whether the user has stopped the rotation
+ private width = 0;
+ private height = 0;
+
+ // Particle storage
+ private xyz: Float32Array | null = null; // interleaved x,y,z
+ private types: Uint8Array | null = null; // 0 = planet, 1 = ring
+ private count = 0;
+
+ // Animation
+ private animationId = 0;
+ private angle = 0;
+ private scaleFactor = 1;
+
+ // Interaction
+ private isDragging = false;
+ private lastMouseX = 0;
+ private lastMouseTime = 0;
+ private mouseVelocities: number[] = [];
+
+ // Speed control
+ private readonly baseSpeed = 0.005;
+ private currentSpeed = 0.005;
+ private rotationDirection = 1;
+ private readonly speedDecayRate = 0.992;
+ private readonly minSpeedMultiplier = 1;
+ private readonly maxSpeedMultiplier = 50;
+ private isStopped = false;
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
- this.ctx = canvas.getContext("2d", {
- alpha: true,
- desynchronized: false, // default is usually fine, 'desynchronized' can help latency but might flicker
- })!;
+ const ctx = canvas.getContext("2d", { alpha: true, desynchronized: false });
+ if (!ctx) {
+ throw new Error("Failed to get 2D context for SaturnEffect");
+ }
+ this.ctx = ctx;
- // Initial resize will set up everything
+ // Initialize size & particles
this.resize(window.innerWidth, window.innerHeight);
this.initParticles();
@@ -48,9 +63,7 @@ export class SaturnEffect {
this.animate();
}
- // Public methods for external mouse event handling
- // These can be called from any element that wants to control the Saturn rotation
-
+ // External interaction handlers (accept clientX)
handleMouseDown(clientX: number) {
this.isDragging = true;
this.lastMouseX = clientX;
@@ -60,26 +73,18 @@ export class SaturnEffect {
handleMouseMove(clientX: number) {
if (!this.isDragging) return;
-
- const currentTime = performance.now();
- const deltaTime = currentTime - this.lastMouseTime;
-
- if (deltaTime > 0) {
- const deltaX = clientX - this.lastMouseX;
- const velocity = deltaX / deltaTime; // pixels per millisecond
-
- // Store recent velocities (keep last 5 for smoothing)
+ const now = performance.now();
+ const dt = now - this.lastMouseTime;
+ if (dt > 0) {
+ const dx = clientX - this.lastMouseX;
+ const velocity = dx / dt;
this.mouseVelocities.push(velocity);
- if (this.mouseVelocities.length > 5) {
- this.mouseVelocities.shift();
- }
-
- // Apply direct rotation while dragging
- this.angle += deltaX * 0.002;
+ if (this.mouseVelocities.length > 5) this.mouseVelocities.shift();
+ // Rotate directly while dragging for immediate feedback
+ this.angle += dx * 0.002;
}
-
this.lastMouseX = clientX;
- this.lastMouseTime = currentTime;
+ this.lastMouseTime = now;
}
handleMouseUp() {
@@ -90,174 +95,130 @@ export class SaturnEffect {
}
handleTouchStart(clientX: number) {
- this.isDragging = true;
- this.lastMouseX = clientX;
- this.lastMouseTime = performance.now();
- this.mouseVelocities = [];
+ this.handleMouseDown(clientX);
}
handleTouchMove(clientX: number) {
- if (!this.isDragging) return;
-
- const currentTime = performance.now();
- const deltaTime = currentTime - this.lastMouseTime;
-
- if (deltaTime > 0) {
- const deltaX = clientX - this.lastMouseX;
- const velocity = deltaX / deltaTime;
-
- this.mouseVelocities.push(velocity);
- if (this.mouseVelocities.length > 5) {
- this.mouseVelocities.shift();
- }
-
- this.angle += deltaX * 0.002;
- }
-
- this.lastMouseX = clientX;
- this.lastMouseTime = currentTime;
+ this.handleMouseMove(clientX);
}
handleTouchEnd() {
- if (this.isDragging && this.mouseVelocities.length > 0) {
- this.applyFlingVelocity();
- }
- this.isDragging = false;
- }
-
- private applyFlingVelocity() {
- // Calculate average velocity from recent samples
- const avgVelocity =
- this.mouseVelocities.reduce((a, b) => a + b, 0) / this.mouseVelocities.length;
-
- // Threshold for considering it a "fling" (pixels per millisecond)
- const flingThreshold = 0.3;
- // Threshold for considering the rotation as "stopped" by user
- const stopThreshold = 0.1;
-
- if (Math.abs(avgVelocity) > flingThreshold) {
- // User flung it - start rotating again
- this.isStopped = false;
-
- // Determine new direction based on fling direction
- const newDirection = avgVelocity > 0 ? 1 : -1;
-
- // If direction changed, update it permanently
- if (newDirection !== this.rotationDirection) {
- this.rotationDirection = newDirection;
- }
-
- // Calculate speed boost based on fling strength
- // Map velocity to speed multiplier (stronger fling = faster rotation)
- const speedMultiplier = Math.min(
- this.maxSpeedMultiplier,
- this.minSpeedMultiplier + Math.abs(avgVelocity) * 10,
- );
-
- this.currentSpeed = this.baseSpeed * speedMultiplier;
- } else if (Math.abs(avgVelocity) < stopThreshold) {
- // User gently released - keep it stopped
- this.isStopped = true;
- this.currentSpeed = 0;
- }
- // If velocity is between stopThreshold and flingThreshold,
- // keep current state (don't change isStopped)
+ this.handleMouseUp();
}
+ // Resize canvas & scale (call on window resize)
resize(width: number, height: number) {
const dpr = window.devicePixelRatio || 1;
this.width = width;
this.height = height;
- this.canvas.width = width * dpr;
- this.canvas.height = height * dpr;
+ // Update canvas pixel size and CSS size
+ this.canvas.width = Math.max(1, Math.floor(width * dpr));
+ this.canvas.height = Math.max(1, Math.floor(height * dpr));
this.canvas.style.width = `${width}px`;
this.canvas.style.height = `${height}px`;
+ // Reset transform and scale for devicePixelRatio
+ this.ctx.setTransform(1, 0, 0, 1, 0, 0); // reset
this.ctx.scale(dpr, dpr);
- // Dynamic scaling based on screen size
const minDim = Math.min(width, height);
- this.scaleFactor = minDim * 0.45;
+ this.scaleFactor = Math.max(1, minDim * 0.45);
}
- initParticles() {
- // Significantly reduced particle count for CPU optimization
- // Planet: 1800 -> 1000
- // Rings: 5000 -> 2500
- // Total approx 3500 vs 6800 previously (approx 50% reduction)
+ // Initialize particle arrays with reduced counts to keep performance reasonable
+ private initParticles() {
+ // Tuned particle counts for reasonable performance across platforms
const planetCount = 1000;
const ringCount = 2500;
this.count = planetCount + ringCount;
- // Use TypedArrays for better memory locality
this.xyz = new Float32Array(this.count * 3);
this.types = new Uint8Array(this.count);
let idx = 0;
- // 1. Planet
- for (let i = 0; i < planetCount; i++) {
+ // Planet points
+ for (let i = 0; i < planetCount; i++, idx++) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(Math.random() * 2 - 1);
const r = 1.0;
- // x, y, z
this.xyz[idx * 3] = r * Math.sin(phi) * Math.cos(theta);
this.xyz[idx * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
this.xyz[idx * 3 + 2] = r * Math.cos(phi);
- this.types[idx] = 0; // 0 for planet
- idx++;
+ this.types[idx] = 0;
}
- // 2. Rings
+ // Ring points
const ringInner = 1.4;
const ringOuter = 2.3;
-
- for (let i = 0; i < ringCount; i++) {
+ for (let i = 0; i < ringCount; i++, idx++) {
const angle = Math.random() * Math.PI * 2;
const dist = Math.sqrt(
- Math.random() * (ringOuter * ringOuter - ringInner * ringInner) + ringInner * ringInner,
+ Math.random() * (ringOuter * ringOuter - ringInner * ringInner) +
+ ringInner * ringInner,
);
- // x, y, z
this.xyz[idx * 3] = dist * Math.cos(angle);
this.xyz[idx * 3 + 1] = (Math.random() - 0.5) * 0.05;
this.xyz[idx * 3 + 2] = dist * Math.sin(angle);
- this.types[idx] = 1; // 1 for ring
- idx++;
+ this.types[idx] = 1;
}
}
- animate() {
+ // Map fling/velocity samples to a rotation speed and direction
+ private applyFlingVelocity() {
+ if (this.mouseVelocities.length === 0) return;
+ const avg =
+ this.mouseVelocities.reduce((a, b) => a + b, 0) /
+ this.mouseVelocities.length;
+ const flingThreshold = 0.3;
+ const stopThreshold = 0.1;
+
+ if (Math.abs(avg) > flingThreshold) {
+ this.isStopped = false;
+ const newDir = avg > 0 ? 1 : -1;
+ if (newDir !== this.rotationDirection) this.rotationDirection = newDir;
+ const multiplier = Math.min(
+ this.maxSpeedMultiplier,
+ this.minSpeedMultiplier + Math.abs(avg) * 10,
+ );
+ this.currentSpeed = this.baseSpeed * multiplier;
+ } else if (Math.abs(avg) < stopThreshold) {
+ this.isStopped = true;
+ this.currentSpeed = 0;
+ }
+ }
+
+ // Main render loop
+ private animate() {
+ // Clear with full alpha to allow layering over background
this.ctx.clearRect(0, 0, this.width, this.height);
- // Normal blending
+ // Standard composition
this.ctx.globalCompositeOperation = "source-over";
- // Update rotation speed - decay towards base speed while maintaining direction
+ // Update rotation speed (decay)
if (!this.isDragging && !this.isStopped) {
if (this.currentSpeed > this.baseSpeed) {
- // Gradually decay speed back to base speed
this.currentSpeed =
- this.baseSpeed + (this.currentSpeed - this.baseSpeed) * this.speedDecayRate;
-
- // Snap to base speed when close enough
+ this.baseSpeed +
+ (this.currentSpeed - this.baseSpeed) * this.speedDecayRate;
if (this.currentSpeed - this.baseSpeed < 0.00001) {
this.currentSpeed = this.baseSpeed;
}
}
-
- // Apply rotation with current speed and direction
this.angle += this.currentSpeed * this.rotationDirection;
}
+ // Center positions
const cx = this.width * 0.6;
const cy = this.height * 0.5;
- // Pre-calculate rotation matrices
+ // Pre-calc rotations
const rotationY = this.angle;
const rotationX = 0.4;
const rotationZ = 0.15;
@@ -272,29 +233,27 @@ export class SaturnEffect {
const fov = 1500;
const scaleFactor = this.scaleFactor;
- if (!this.xyz || !this.types) return;
+ if (!this.xyz || !this.types) {
+ this.animationId = requestAnimationFrame(this.animate);
+ return;
+ }
+ // Loop particles
for (let i = 0; i < this.count; i++) {
const x = this.xyz[i * 3];
const y = this.xyz[i * 3 + 1];
const z = this.xyz[i * 3 + 2];
- // Apply Scale
+ // Scale to screen
const px = x * scaleFactor;
const py = y * scaleFactor;
const pz = z * scaleFactor;
- // 1. Rotate Y
+ // Rotate Y then X then Z
const x1 = px * cosY - pz * sinY;
const z1 = pz * cosY + px * sinY;
- // y1 = py
-
- // 2. Rotate X
const y2 = py * cosX - z1 * sinX;
const z2 = z1 * cosX + py * sinX;
- // x2 = x1
-
- // 3. Rotate Z
const x3 = x1 * cosZ - y2 * sinZ;
const y3 = y2 * cosZ + x1 * sinZ;
const z3 = z2;
@@ -305,28 +264,23 @@ export class SaturnEffect {
const x2d = cx + x3 * scale;
const y2d = cy + y3 * scale;
- // Size calculation - slightly larger dots to compensate for lower count
- // Previously Planet 2.0 -> 2.4, Ring 1.3 -> 1.5
const type = this.types[i];
const sizeBase = type === 0 ? 2.4 : 1.5;
const size = sizeBase * scale;
- // Opacity
let alpha = scale * scale * scale;
if (alpha > 1) alpha = 1;
- if (alpha < 0.15) continue; // Skip very faint particles for performance
+ if (alpha < 0.15) continue;
- // Optimization: Planet color vs Ring color
if (type === 0) {
- // Planet: Warm White
+ // Planet: warm-ish
this.ctx.fillStyle = `rgba(255, 240, 220, ${alpha})`;
} else {
- // Ring: Cool White
+ // Ring: cool-ish
this.ctx.fillStyle = `rgba(220, 240, 255, ${alpha})`;
}
- // Render as squares (fillRect) instead of circles (arc)
- // This is significantly faster for software rendering and reduces GPU usage.
+ // Render as small rectangles (faster than arc)
this.ctx.fillRect(x2d, y2d, size, size);
}
}
@@ -334,7 +288,12 @@ export class SaturnEffect {
this.animationId = requestAnimationFrame(this.animate);
}
+ // Stop animations and release resources
destroy() {
- cancelAnimationFrame(this.animationId);
+ if (this.animationId) {
+ cancelAnimationFrame(this.animationId);
+ this.animationId = 0;
+ }
+ // Intentionally do not null out arrays to allow reuse if desired.
}
}
diff --git a/packages/ui/src/lib/modLoaderApi.ts b/packages/ui/src/lib/modLoaderApi.ts
deleted file mode 100644
index 75f404a..0000000
--- a/packages/ui/src/lib/modLoaderApi.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-/**
- * Mod Loader API service for Fabric and Forge integration.
- * This module provides functions to interact with the Tauri backend
- * for mod loader version management.
- */
-
-import { invoke } from "@tauri-apps/api/core";
-import type {
- FabricGameVersion,
- FabricLoaderVersion,
- FabricLoaderEntry,
- InstalledFabricVersion,
- ForgeVersion,
- InstalledForgeVersion,
-} from "../types";
-
-// ==================== Fabric API ====================
-
-/**
- * Get all Minecraft versions supported by Fabric.
- */
-export async function getFabricGameVersions(): Promise<FabricGameVersion[]> {
- return invoke<FabricGameVersion[]>("get_fabric_game_versions");
-}
-
-/**
- * Get all available Fabric loader versions.
- */
-export async function getFabricLoaderVersions(): Promise<FabricLoaderVersion[]> {
- return invoke<FabricLoaderVersion[]>("get_fabric_loader_versions");
-}
-
-/**
- * Get Fabric loaders available for a specific Minecraft version.
- */
-export async function getFabricLoadersForVersion(
- gameVersion: string,
-): Promise<FabricLoaderEntry[]> {
- return invoke<FabricLoaderEntry[]>("get_fabric_loaders_for_version", {
- gameVersion,
- });
-}
-
-/**
- * Install Fabric loader for a specific Minecraft version.
- */
-export async function installFabric(
- gameVersion: string,
- loaderVersion: string,
-): Promise<InstalledFabricVersion> {
- return invoke<InstalledFabricVersion>("install_fabric", {
- gameVersion,
- loaderVersion,
- });
-}
-
-/**
- * List all installed Fabric versions.
- */
-export async function listInstalledFabricVersions(): Promise<string[]> {
- return invoke<string[]>("list_installed_fabric_versions");
-}
-
-/**
- * Check if Fabric is installed for a specific version combination.
- */
-export async function isFabricInstalled(
- gameVersion: string,
- loaderVersion: string,
-): Promise<boolean> {
- return invoke<boolean>("is_fabric_installed", {
- gameVersion,
- loaderVersion,
- });
-}
-
-// ==================== Forge API ====================
-
-/**
- * Get all Minecraft versions supported by Forge.
- */
-export async function getForgeGameVersions(): Promise<string[]> {
- return invoke<string[]>("get_forge_game_versions");
-}
-
-/**
- * Get Forge versions available for a specific Minecraft version.
- */
-export async function getForgeVersionsForGame(gameVersion: string): Promise<ForgeVersion[]> {
- return invoke<ForgeVersion[]>("get_forge_versions_for_game", {
- gameVersion,
- });
-}
-
-/**
- * Install Forge for a specific Minecraft version.
- */
-export async function installForge(
- gameVersion: string,
- forgeVersion: string,
-): Promise<InstalledForgeVersion> {
- return invoke<InstalledForgeVersion>("install_forge", {
- gameVersion,
- forgeVersion,
- });
-}
diff --git a/packages/ui-new/src/lib/tsrs-utils.ts b/packages/ui/src/lib/tsrs-utils.ts
index f48f851..f48f851 100644
--- a/packages/ui-new/src/lib/tsrs-utils.ts
+++ b/packages/ui/src/lib/tsrs-utils.ts
diff --git a/packages/ui-new/src/lib/utils.ts b/packages/ui/src/lib/utils.ts
index 365058c..365058c 100644
--- a/packages/ui-new/src/lib/utils.ts
+++ b/packages/ui/src/lib/utils.ts
diff --git a/packages/ui/src/main.ts b/packages/ui/src/main.ts
deleted file mode 100644
index d47b930..0000000
--- a/packages/ui/src/main.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { mount } from "svelte";
-import "./app.css";
-import App from "./App.svelte";
-
-const app = mount(App, {
- target: document.getElementById("app")!,
-});
-
-export default app;
diff --git a/packages/ui-new/src/main.tsx b/packages/ui/src/main.tsx
index a3157bd..a3157bd 100644
--- a/packages/ui-new/src/main.tsx
+++ b/packages/ui/src/main.tsx
diff --git a/packages/ui-new/src/models/auth.ts b/packages/ui/src/models/auth.ts
index 10b2a0d..10b2a0d 100644
--- a/packages/ui-new/src/models/auth.ts
+++ b/packages/ui/src/models/auth.ts
diff --git a/packages/ui-new/src/models/instances.ts b/packages/ui/src/models/instances.ts
index f434c7c..f434c7c 100644
--- a/packages/ui-new/src/models/instances.ts
+++ b/packages/ui/src/models/instances.ts
diff --git a/packages/ui-new/src/models/settings.ts b/packages/ui/src/models/settings.ts
index 9f4119c..9f4119c 100644
--- a/packages/ui-new/src/models/settings.ts
+++ b/packages/ui/src/models/settings.ts
diff --git a/packages/ui-new/src/pages/assistant-view.tsx.bk b/packages/ui/src/pages/assistant-view.tsx.bk
index 56f827b..56f827b 100644
--- a/packages/ui-new/src/pages/assistant-view.tsx.bk
+++ b/packages/ui/src/pages/assistant-view.tsx.bk
diff --git a/packages/ui-new/src/pages/home-view.tsx b/packages/ui/src/pages/home-view.tsx
index 4f80cb0..4f80cb0 100644
--- a/packages/ui-new/src/pages/home-view.tsx
+++ b/packages/ui/src/pages/home-view.tsx
diff --git a/packages/ui-new/src/pages/index.tsx b/packages/ui/src/pages/index.tsx
index 54cfc1e..54cfc1e 100644
--- a/packages/ui-new/src/pages/index.tsx
+++ b/packages/ui/src/pages/index.tsx
diff --git a/packages/ui-new/src/pages/instances-view.tsx b/packages/ui/src/pages/instances-view.tsx
index ad6bd38..ad6bd38 100644
--- a/packages/ui-new/src/pages/instances-view.tsx
+++ b/packages/ui/src/pages/instances-view.tsx
diff --git a/packages/ui-new/src/pages/settings-view.tsx.bk b/packages/ui/src/pages/settings-view.tsx.bk
index ac43d9b..ac43d9b 100644
--- a/packages/ui-new/src/pages/settings-view.tsx.bk
+++ b/packages/ui/src/pages/settings-view.tsx.bk
diff --git a/packages/ui-new/src/pages/settings.tsx b/packages/ui/src/pages/settings.tsx
index 440a5dc..440a5dc 100644
--- a/packages/ui-new/src/pages/settings.tsx
+++ b/packages/ui/src/pages/settings.tsx
diff --git a/packages/ui-new/src/pages/versions-view.tsx.bk b/packages/ui/src/pages/versions-view.tsx.bk
index d54596d..d54596d 100644
--- a/packages/ui-new/src/pages/versions-view.tsx.bk
+++ b/packages/ui/src/pages/versions-view.tsx.bk
diff --git a/packages/ui-new/src/stores/assistant-store.ts b/packages/ui/src/stores/assistant-store.ts
index 180031b..180031b 100644
--- a/packages/ui-new/src/stores/assistant-store.ts
+++ b/packages/ui/src/stores/assistant-store.ts
diff --git a/packages/ui/src/stores/assistant.svelte.ts b/packages/ui/src/stores/assistant.svelte.ts
deleted file mode 100644
index a3f47ea..0000000
--- a/packages/ui/src/stores/assistant.svelte.ts
+++ /dev/null
@@ -1,166 +0,0 @@
-import { invoke } from "@tauri-apps/api/core";
-import { listen, type UnlistenFn } from "@tauri-apps/api/event";
-
-export interface GenerationStats {
- total_duration: number;
- load_duration: number;
- prompt_eval_count: number;
- prompt_eval_duration: number;
- eval_count: number;
- eval_duration: number;
-}
-
-export interface Message {
- role: "user" | "assistant" | "system";
- content: string;
- stats?: GenerationStats;
-}
-
-interface StreamChunk {
- content: string;
- done: boolean;
- stats?: GenerationStats;
-}
-
-// Module-level state using $state
-let messages = $state<Message[]>([]);
-let isProcessing = $state(false);
-let isProviderHealthy = $state(false);
-let streamingContent = "";
-let initialized = false;
-let streamUnlisten: UnlistenFn | null = null;
-
-async function init() {
- if (initialized) return;
- initialized = true;
- await checkHealth();
-}
-
-async function checkHealth() {
- try {
- isProviderHealthy = await invoke("assistant_check_health");
- } catch (e) {
- console.error("Failed to check provider health:", e);
- isProviderHealthy = false;
- }
-}
-
-function finishStreaming() {
- isProcessing = false;
- streamingContent = "";
- if (streamUnlisten) {
- streamUnlisten();
- streamUnlisten = null;
- }
-}
-
-async function sendMessage(
- content: string,
- isEnabled: boolean,
- provider: string,
- endpoint: string,
-) {
- if (!content.trim()) return;
- if (!isEnabled) {
- messages = [
- ...messages,
- {
- role: "assistant",
- content: "Assistant is disabled. Enable it in Settings > AI Assistant.",
- },
- ];
- return;
- }
-
- // Add user message
- messages = [...messages, { role: "user", content }];
- isProcessing = true;
- streamingContent = "";
-
- // Add empty assistant message for streaming
- messages = [...messages, { role: "assistant", content: "" }];
-
- try {
- // Set up stream listener
- streamUnlisten = await listen<StreamChunk>("assistant-stream", (event) => {
- const chunk = event.payload;
-
- if (chunk.content) {
- streamingContent += chunk.content;
- // Update the last message (assistant's response)
- const lastIdx = messages.length - 1;
- if (lastIdx >= 0 && messages[lastIdx].role === "assistant") {
- messages[lastIdx] = {
- ...messages[lastIdx],
- content: streamingContent,
- };
- // Trigger reactivity
- messages = [...messages];
- }
- }
-
- if (chunk.done) {
- if (chunk.stats) {
- const lastIdx = messages.length - 1;
- if (lastIdx >= 0 && messages[lastIdx].role === "assistant") {
- messages[lastIdx] = {
- ...messages[lastIdx],
- stats: chunk.stats,
- };
- messages = [...messages];
- }
- }
- finishStreaming();
- }
- });
-
- // Start streaming chat
- await invoke<string>("assistant_chat_stream", {
- messages: messages.slice(0, -1), // Exclude the empty assistant message
- });
- } catch (e) {
- console.error("Failed to send message:", e);
- const errorMessage = e instanceof Error ? e.message : String(e);
-
- let helpText = "";
- if (provider === "ollama") {
- helpText = `\n\nPlease ensure Ollama is running at ${endpoint}.`;
- } else if (provider === "openai") {
- helpText = "\n\nPlease check your OpenAI API key in Settings.";
- }
-
- // Update the last message with error
- const lastIdx = messages.length - 1;
- if (lastIdx >= 0 && messages[lastIdx].role === "assistant") {
- messages[lastIdx] = {
- role: "assistant",
- content: `Error: ${errorMessage}${helpText}`,
- };
- messages = [...messages];
- }
-
- finishStreaming();
- }
-}
-
-function clearHistory() {
- messages = [];
- streamingContent = "";
-}
-
-// Export as an object with getters for reactive access
-export const assistantState = {
- get messages() {
- return messages;
- },
- get isProcessing() {
- return isProcessing;
- },
- get isProviderHealthy() {
- return isProviderHealthy;
- },
- init,
- checkHealth,
- sendMessage,
- clearHistory,
-};
diff --git a/packages/ui-new/src/stores/auth-store.ts b/packages/ui/src/stores/auth-store.ts
index bf7e3c5..bf7e3c5 100644
--- a/packages/ui-new/src/stores/auth-store.ts
+++ b/packages/ui/src/stores/auth-store.ts
diff --git a/packages/ui/src/stores/auth.svelte.ts b/packages/ui/src/stores/auth.svelte.ts
deleted file mode 100644
index 1b613a7..0000000
--- a/packages/ui/src/stores/auth.svelte.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-import { invoke } from "@tauri-apps/api/core";
-import { open } from "@tauri-apps/plugin-shell";
-import { listen, type UnlistenFn } from "@tauri-apps/api/event";
-import type { Account, DeviceCodeResponse } from "../types";
-import { uiState } from "./ui.svelte";
-import { logsState } from "./logs.svelte";
-
-export class AuthState {
- currentAccount = $state<Account | null>(null);
- isLoginModalOpen = $state(false);
- isLogoutConfirmOpen = $state(false);
- loginMode = $state<"select" | "offline" | "microsoft">("select");
- offlineUsername = $state("");
- deviceCodeData = $state<DeviceCodeResponse | null>(null);
- msLoginLoading = $state(false);
- msLoginStatus = $state("Waiting for authorization...");
-
- private pollInterval: ReturnType<typeof setInterval> | null = null;
- private isPollingRequestActive = false;
- private authProgressUnlisten: UnlistenFn | null = null;
-
- async checkAccount() {
- try {
- const acc = await invoke("get_active_account");
- this.currentAccount = acc as Account | null;
- } catch (e) {
- console.error("Failed to check account:", e);
- }
- }
-
- openLoginModal() {
- if (this.currentAccount) {
- // Show custom logout confirmation dialog
- this.isLogoutConfirmOpen = true;
- return;
- }
- this.resetLoginState();
- this.isLoginModalOpen = true;
- }
-
- cancelLogout() {
- this.isLogoutConfirmOpen = false;
- }
-
- async confirmLogout() {
- this.isLogoutConfirmOpen = false;
- try {
- await invoke("logout");
- this.currentAccount = null;
- uiState.setStatus("Logged out successfully");
- } catch (e) {
- console.error("Logout failed:", e);
- }
- }
-
- closeLoginModal() {
- this.stopPolling();
- this.isLoginModalOpen = false;
- }
-
- resetLoginState() {
- this.loginMode = "select";
- this.offlineUsername = "";
- this.deviceCodeData = null;
- this.msLoginLoading = false;
- }
-
- async performOfflineLogin() {
- if (!this.offlineUsername) return;
- try {
- this.currentAccount = (await invoke("login_offline", {
- username: this.offlineUsername,
- })) as Account;
- this.isLoginModalOpen = false;
- } catch (e) {
- alert("Login failed: " + e);
- }
- }
-
- async startMicrosoftLogin() {
- this.loginMode = "microsoft";
- this.msLoginLoading = true;
- this.msLoginStatus = "Waiting for authorization...";
- this.stopPolling();
-
- // Setup auth progress listener
- this.setupAuthProgressListener();
-
- try {
- this.deviceCodeData = (await invoke("start_microsoft_login")) as DeviceCodeResponse;
-
- if (this.deviceCodeData) {
- try {
- await navigator.clipboard.writeText(this.deviceCodeData.user_code);
- } catch (e) {
- console.error("Clipboard failed", e);
- }
-
- open(this.deviceCodeData.verification_uri);
- logsState.addLog(
- "info",
- "Auth",
- "Microsoft login started, waiting for browser authorization...",
- );
-
- console.log("Starting polling for token...");
- const intervalMs = (this.deviceCodeData.interval || 5) * 1000;
- this.pollInterval = setInterval(
- () => this.checkLoginStatus(this.deviceCodeData!.device_code),
- intervalMs,
- );
- }
- } catch (e) {
- logsState.addLog("error", "Auth", `Failed to start Microsoft login: ${e}`);
- alert("Failed to start Microsoft login: " + e);
- this.loginMode = "select";
- } finally {
- this.msLoginLoading = false;
- }
- }
-
- private async setupAuthProgressListener() {
- // Clean up previous listener if exists
- if (this.authProgressUnlisten) {
- this.authProgressUnlisten();
- this.authProgressUnlisten = null;
- }
-
- this.authProgressUnlisten = await listen<string>("auth-progress", (event) => {
- const message = event.payload;
- this.msLoginStatus = message;
- logsState.addLog("info", "Auth", message);
- });
- }
-
- private cleanupAuthListener() {
- if (this.authProgressUnlisten) {
- this.authProgressUnlisten();
- this.authProgressUnlisten = null;
- }
- }
-
- stopPolling() {
- if (this.pollInterval) {
- clearInterval(this.pollInterval);
- this.pollInterval = null;
- }
- }
-
- async checkLoginStatus(deviceCode: string) {
- if (this.isPollingRequestActive) return;
- this.isPollingRequestActive = true;
-
- console.log("Polling Microsoft API...");
- try {
- this.currentAccount = (await invoke("complete_microsoft_login", {
- deviceCode,
- })) as Account;
-
- console.log("Login Successful!", this.currentAccount);
- this.stopPolling();
- this.cleanupAuthListener();
- this.isLoginModalOpen = false;
- logsState.addLog(
- "info",
- "Auth",
- `Login successful! Welcome, ${this.currentAccount.username}`,
- );
- uiState.setStatus("Welcome back, " + this.currentAccount.username);
- } catch (e: any) {
- const errStr = e.toString();
- if (errStr.includes("authorization_pending")) {
- console.log("Status: Waiting for user to authorize...");
- } else {
- console.error("Polling Error:", errStr);
- this.msLoginStatus = "Error: " + errStr;
- logsState.addLog("error", "Auth", `Login error: ${errStr}`);
-
- if (errStr.includes("expired_token") || errStr.includes("access_denied")) {
- this.stopPolling();
- this.cleanupAuthListener();
- alert("Login failed: " + errStr);
- this.loginMode = "select";
- }
- }
- } finally {
- this.isPollingRequestActive = false;
- }
- }
-}
-
-export const authState = new AuthState();
diff --git a/packages/ui-new/src/stores/game-store.ts b/packages/ui/src/stores/game-store.ts
index fa0f9f8..fa0f9f8 100644
--- a/packages/ui-new/src/stores/game-store.ts
+++ b/packages/ui/src/stores/game-store.ts
diff --git a/packages/ui/src/stores/game.svelte.ts b/packages/ui/src/stores/game.svelte.ts
deleted file mode 100644
index 504d108..0000000
--- a/packages/ui/src/stores/game.svelte.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { invoke } from "@tauri-apps/api/core";
-import type { Version } from "../types";
-import { uiState } from "./ui.svelte";
-import { authState } from "./auth.svelte";
-import { instancesState } from "./instances.svelte";
-
-export class GameState {
- versions = $state<Version[]>([]);
- selectedVersion = $state("");
-
- constructor() {
- // Constructor intentionally empty
- // Instance switching handled in App.svelte with $effect
- }
-
- get latestRelease() {
- return this.versions.find((v) => v.type === "release");
- }
-
- async loadVersions(instanceId?: string) {
- const id = instanceId || instancesState.activeInstanceId;
- if (!id) {
- this.versions = [];
- return;
- }
-
- try {
- this.versions = await invoke<Version[]>("get_versions", {
- instanceId: id,
- });
- // Don't auto-select version here - let BottomBar handle version selection
- // based on installed versions only
- } catch (e) {
- console.error("Failed to fetch versions:", e);
- uiState.setStatus("Error fetching versions: " + e);
- }
- }
-
- async startGame() {
- if (!authState.currentAccount) {
- alert("Please login first!");
- authState.openLoginModal();
- return;
- }
-
- if (!this.selectedVersion) {
- alert("Please select a version!");
- return;
- }
-
- if (!instancesState.activeInstanceId) {
- alert("Please select an instance first!");
- uiState.setView("instances");
- return;
- }
-
- uiState.setStatus("Preparing to launch " + this.selectedVersion + "...");
- console.log(
- "Invoking start_game for version:",
- this.selectedVersion,
- "instance:",
- instancesState.activeInstanceId,
- );
- try {
- const msg = await invoke<string>("start_game", {
- instanceId: instancesState.activeInstanceId,
- versionId: this.selectedVersion,
- });
- console.log("Response:", msg);
- uiState.setStatus(msg);
- } catch (e) {
- console.error(e);
- uiState.setStatus("Error: " + e);
- }
- }
-}
-
-export const gameState = new GameState();
diff --git a/packages/ui/src/stores/instances.svelte.ts b/packages/ui/src/stores/instances.svelte.ts
deleted file mode 100644
index f4ac4e9..0000000
--- a/packages/ui/src/stores/instances.svelte.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { invoke } from "@tauri-apps/api/core";
-import type { Instance } from "../types";
-import { uiState } from "./ui.svelte";
-
-export class InstancesState {
- instances = $state<Instance[]>([]);
- activeInstanceId = $state<string | null>(null);
- get activeInstance(): Instance | null {
- if (!this.activeInstanceId) return null;
- return this.instances.find((i) => i.id === this.activeInstanceId) || null;
- }
-
- async loadInstances() {
- try {
- this.instances = await invoke<Instance[]>("list_instances");
- const active = await invoke<Instance | null>("get_active_instance");
- if (active) {
- this.activeInstanceId = active.id;
- } else if (this.instances.length > 0) {
- // If no active instance but instances exist, set the first one as active
- await this.setActiveInstance(this.instances[0].id);
- }
- } catch (e) {
- console.error("Failed to load instances:", e);
- uiState.setStatus("Error loading instances: " + e);
- }
- }
-
- async createInstance(name: string): Promise<Instance | null> {
- try {
- const instance = await invoke<Instance>("create_instance", { name });
- await this.loadInstances();
- uiState.setStatus(`Instance "${name}" created successfully`);
- return instance;
- } catch (e) {
- console.error("Failed to create instance:", e);
- uiState.setStatus("Error creating instance: " + e);
- return null;
- }
- }
-
- async deleteInstance(id: string) {
- try {
- await invoke("delete_instance", { instanceId: id });
- await this.loadInstances();
- // If deleted instance was active, set another as active
- if (this.activeInstanceId === id) {
- if (this.instances.length > 0) {
- await this.setActiveInstance(this.instances[0].id);
- } else {
- this.activeInstanceId = null;
- }
- }
- uiState.setStatus("Instance deleted successfully");
- } catch (e) {
- console.error("Failed to delete instance:", e);
- uiState.setStatus("Error deleting instance: " + e);
- }
- }
-
- async updateInstance(instance: Instance) {
- try {
- await invoke("update_instance", { instance });
- await this.loadInstances();
- uiState.setStatus("Instance updated successfully");
- } catch (e) {
- console.error("Failed to update instance:", e);
- uiState.setStatus("Error updating instance: " + e);
- }
- }
-
- async setActiveInstance(id: string) {
- try {
- await invoke("set_active_instance", { instanceId: id });
- this.activeInstanceId = id;
- uiState.setStatus("Active instance changed");
- } catch (e) {
- console.error("Failed to set active instance:", e);
- uiState.setStatus("Error setting active instance: " + e);
- }
- }
-
- async duplicateInstance(id: string, newName: string): Promise<Instance | null> {
- try {
- const instance = await invoke<Instance>("duplicate_instance", {
- instanceId: id,
- newName,
- });
- await this.loadInstances();
- uiState.setStatus(`Instance duplicated as "${newName}"`);
- return instance;
- } catch (e) {
- console.error("Failed to duplicate instance:", e);
- uiState.setStatus("Error duplicating instance: " + e);
- return null;
- }
- }
-
- async getInstance(id: string): Promise<Instance | null> {
- try {
- return await invoke<Instance>("get_instance", { instanceId: id });
- } catch (e) {
- console.error("Failed to get instance:", e);
- return null;
- }
- }
-}
-
-export const instancesState = new InstancesState();
diff --git a/packages/ui-new/src/stores/logs-store.ts b/packages/ui/src/stores/logs-store.ts
index b19f206..b19f206 100644
--- a/packages/ui-new/src/stores/logs-store.ts
+++ b/packages/ui/src/stores/logs-store.ts
diff --git a/packages/ui/src/stores/logs.svelte.ts b/packages/ui/src/stores/logs.svelte.ts
deleted file mode 100644
index c9d4acc..0000000
--- a/packages/ui/src/stores/logs.svelte.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-import { listen } from "@tauri-apps/api/event";
-
-export interface LogEntry {
- id: number;
- timestamp: string;
- level: "info" | "warn" | "error" | "debug" | "fatal";
- source: string;
- message: string;
-}
-
-// Parse Minecraft/Java log format: [HH:MM:SS] [Thread/LEVEL]: message
-// or: [HH:MM:SS] [Thread/LEVEL] [Source]: message
-const GAME_LOG_REGEX = /^\[[\d:]+\]\s*\[([^\]]+)\/(\w+)\](?:\s*\[([^\]]+)\])?:\s*(.*)$/;
-
-function parseGameLogLevel(levelStr: string): LogEntry["level"] {
- const upper = levelStr.toUpperCase();
- if (upper === "INFO") return "info";
- if (upper === "WARN" || upper === "WARNING") return "warn";
- if (upper === "ERROR" || upper === "SEVERE") return "error";
- if (
- upper === "DEBUG" ||
- upper === "TRACE" ||
- upper === "FINE" ||
- upper === "FINER" ||
- upper === "FINEST"
- )
- return "debug";
- if (upper === "FATAL") return "fatal";
- return "info";
-}
-
-export class LogsState {
- logs = $state<LogEntry[]>([]);
- private nextId = 0;
- private maxLogs = 5000;
-
- // Track all unique sources for filtering
- sources = $state<Set<string>>(new Set(["Launcher"]));
-
- constructor() {
- this.addLog("info", "Launcher", "Logs initialized");
- }
-
- addLog(level: LogEntry["level"], source: string, message: string) {
- const now = new Date();
- const timestamp =
- now.toLocaleTimeString() + "." + now.getMilliseconds().toString().padStart(3, "0");
-
- this.logs.push({
- id: this.nextId++,
- timestamp,
- level,
- source,
- message,
- });
-
- // Track source
- if (!this.sources.has(source)) {
- this.sources = new Set([...this.sources, source]);
- }
-
- if (this.logs.length > this.maxLogs) {
- this.logs.shift();
- }
- }
-
- // Parse game output and extract level/source
- addGameLog(rawLine: string, isStderr: boolean) {
- const match = rawLine.match(GAME_LOG_REGEX);
-
- if (match) {
- const [, thread, levelStr, extraSource, message] = match;
- const level = parseGameLogLevel(levelStr);
- // Use extraSource if available, otherwise use thread name as source hint
- const source = extraSource || `Game/${thread.split("-")[0]}`;
- this.addLog(level, source, message);
- } else {
- // Fallback: couldn't parse, use stderr as error indicator
- const level = isStderr ? "error" : "info";
- this.addLog(level, "Game", rawLine);
- }
- }
-
- clear() {
- this.logs = [];
- this.sources = new Set(["Launcher"]);
- this.addLog("info", "Launcher", "Logs cleared");
- }
-
- // Export with filter support
- exportLogs(filteredLogs: LogEntry[]): string {
- return filteredLogs
- .map((l) => `[${l.timestamp}] [${l.source}/${l.level.toUpperCase()}] ${l.message}`)
- .join("\n");
- }
-
- private initialized = false;
-
- async init() {
- if (this.initialized) return;
- this.initialized = true;
-
- // General Launcher Logs
- await listen<string>("launcher-log", (e) => {
- this.addLog("info", "Launcher", e.payload);
- });
-
- // Game Stdout - parse log level
- await listen<string>("game-stdout", (e) => {
- this.addGameLog(e.payload, false);
- });
-
- // Game Stderr - parse log level, default to error
- await listen<string>("game-stderr", (e) => {
- this.addGameLog(e.payload, true);
- });
-
- // Download Events (Summarized)
- await listen("download-start", (e) => {
- this.addLog("info", "Downloader", `Starting batch download of ${e.payload} files...`);
- });
-
- await listen("download-complete", () => {
- this.addLog("info", "Downloader", "All downloads completed.");
- });
-
- // Listen to file download progress to log finished files
- await listen<any>("download-progress", (e) => {
- const p = e.payload;
- if (p.status === "Finished") {
- if (p.file.endsWith(".jar")) {
- this.addLog("info", "Downloader", `Downloaded ${p.file}`);
- }
- }
- });
-
- // Java Download
- await listen<any>("java-download-progress", (e) => {
- const p = e.payload;
- if (p.status === "Downloading" && p.percentage === 0) {
- this.addLog("info", "JavaInstaller", `Downloading Java: ${p.file_name}`);
- } else if (p.status === "Completed") {
- this.addLog("info", "JavaInstaller", `Java installed: ${p.file_name}`);
- } else if (p.status === "Error") {
- this.addLog("error", "JavaInstaller", `Java download error`);
- }
- });
- }
-}
-
-export const logsState = new LogsState();
diff --git a/packages/ui-new/src/stores/releases-store.ts b/packages/ui/src/stores/releases-store.ts
index 56afa08..56afa08 100644
--- a/packages/ui-new/src/stores/releases-store.ts
+++ b/packages/ui/src/stores/releases-store.ts
diff --git a/packages/ui/src/stores/releases.svelte.ts b/packages/ui/src/stores/releases.svelte.ts
deleted file mode 100644
index c858abb..0000000
--- a/packages/ui/src/stores/releases.svelte.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { invoke } from "@tauri-apps/api/core";
-
-export interface GithubRelease {
- tag_name: string;
- name: string;
- published_at: string;
- body: string;
- html_url: string;
-}
-
-export class ReleasesState {
- releases = $state<GithubRelease[]>([]);
- isLoading = $state(false);
- isLoaded = $state(false);
- error = $state<string | null>(null);
-
- async loadReleases() {
- // If already loaded or currently loading, skip to prevent duplicate requests
- if (this.isLoaded || this.isLoading) return;
-
- this.isLoading = true;
- this.error = null;
-
- try {
- this.releases = await invoke<GithubRelease[]>("get_github_releases");
- this.isLoaded = true;
- } catch (e) {
- console.error("Failed to load releases:", e);
- this.error = String(e);
- } finally {
- this.isLoading = false;
- }
- }
-}
-
-export const releasesState = new ReleasesState();
diff --git a/packages/ui-new/src/stores/settings-store.ts b/packages/ui/src/stores/settings-store.ts
index 0bfc1e1..0bfc1e1 100644
--- a/packages/ui-new/src/stores/settings-store.ts
+++ b/packages/ui/src/stores/settings-store.ts
diff --git a/packages/ui/src/stores/settings.svelte.ts b/packages/ui/src/stores/settings.svelte.ts
deleted file mode 100644
index 5d20050..0000000
--- a/packages/ui/src/stores/settings.svelte.ts
+++ /dev/null
@@ -1,570 +0,0 @@
-import { invoke } from "@tauri-apps/api/core";
-import { convertFileSrc } from "@tauri-apps/api/core";
-import { listen, type UnlistenFn } from "@tauri-apps/api/event";
-import type {
- JavaCatalog,
- JavaDownloadProgress,
- JavaDownloadSource,
- JavaInstallation,
- JavaReleaseInfo,
- LauncherConfig,
- ModelInfo,
- PendingJavaDownload,
-} from "../types";
-import { uiState } from "./ui.svelte";
-
-export class SettingsState {
- settings = $state<LauncherConfig>({
- min_memory: 1024,
- max_memory: 2048,
- java_path: "java",
- width: 854,
- height: 480,
- download_threads: 32,
- enable_gpu_acceleration: false,
- enable_visual_effects: true,
- active_effect: "constellation",
- theme: "dark",
- custom_background_path: undefined,
- log_upload_service: "paste.rs",
- pastebin_api_key: undefined,
- assistant: {
- enabled: true,
- llm_provider: "ollama",
- ollama_endpoint: "http://localhost:11434",
- ollama_model: "llama3",
- openai_api_key: undefined,
- openai_endpoint: "https://api.openai.com/v1",
- openai_model: "gpt-3.5-turbo",
- system_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.",
- response_language: "auto",
- tts_enabled: false,
- tts_provider: "disabled",
- },
- use_shared_caches: false,
- keep_legacy_per_instance_storage: true,
- feature_flags: {
- demo_user: false,
- quick_play_enabled: false,
- quick_play_path: undefined,
- quick_play_singleplayer: true,
- quick_play_multiplayer_server: undefined,
- },
- });
-
- // Convert background path to proper asset URL
- get backgroundUrl(): string | undefined {
- if (this.settings.custom_background_path) {
- return convertFileSrc(this.settings.custom_background_path);
- }
- return undefined;
- }
- javaInstallations = $state<JavaInstallation[]>([]);
- isDetectingJava = $state(false);
-
- // Java download modal state
- showJavaDownloadModal = $state(false);
- selectedDownloadSource = $state<JavaDownloadSource>("adoptium");
-
- // Java catalog state
- javaCatalog = $state<JavaCatalog | null>(null);
- isLoadingCatalog = $state(false);
- catalogError = $state("");
-
- // Version selection state
- selectedMajorVersion = $state<number | null>(null);
- selectedImageType = $state<"jre" | "jdk">("jre");
- showOnlyRecommended = $state(true);
- searchQuery = $state("");
-
- // Download progress state
- isDownloadingJava = $state(false);
- downloadProgress = $state<JavaDownloadProgress | null>(null);
- javaDownloadStatus = $state("");
-
- // Pending downloads
- pendingDownloads = $state<PendingJavaDownload[]>([]);
-
- // AI Model lists
- ollamaModels = $state<ModelInfo[]>([]);
- openaiModels = $state<ModelInfo[]>([]);
- isLoadingOllamaModels = $state(false);
- isLoadingOpenaiModels = $state(false);
- ollamaModelsError = $state("");
- openaiModelsError = $state("");
-
- // Config Editor state
- showConfigEditor = $state(false);
- rawConfigContent = $state("");
- configFilePath = $state("");
- configEditorError = $state("");
-
- // Event listener cleanup
- private progressUnlisten: UnlistenFn | null = null;
-
- async openConfigEditor() {
- this.configEditorError = "";
- try {
- const path = await invoke<string>("get_config_path");
- const content = await invoke<string>("read_raw_config");
- this.configFilePath = path;
- this.rawConfigContent = content;
- this.showConfigEditor = true;
- } catch (e) {
- console.error("Failed to open config editor:", e);
- uiState.setStatus(`Failed to open config: ${e}`);
- }
- }
-
- async saveRawConfig(content: string, closeAfterSave = true) {
- try {
- await invoke("save_raw_config", { content });
- // Reload settings to ensure UI is in sync
- await this.loadSettings();
- if (closeAfterSave) {
- this.showConfigEditor = false;
- }
- uiState.setStatus("Configuration saved successfully!");
- } catch (e) {
- console.error("Failed to save config:", e);
- this.configEditorError = String(e);
- }
- }
-
- closeConfigEditor() {
- this.showConfigEditor = false;
- this.rawConfigContent = "";
- this.configEditorError = "";
- }
-
- // Computed: filtered releases based on selection
- get filteredReleases(): JavaReleaseInfo[] {
- if (!this.javaCatalog) return [];
-
- let releases = this.javaCatalog.releases;
-
- // Filter by major version if selected
- if (this.selectedMajorVersion !== null) {
- releases = releases.filter((r) => r.major_version === this.selectedMajorVersion);
- }
-
- // Filter by image type
- releases = releases.filter((r) => r.image_type === this.selectedImageType);
-
- // Filter by recommended (LTS) versions
- if (this.showOnlyRecommended) {
- releases = releases.filter((r) => r.is_lts);
- }
-
- // Filter by search query
- if (this.searchQuery.trim()) {
- const query = this.searchQuery.toLowerCase();
- releases = releases.filter(
- (r) =>
- r.release_name.toLowerCase().includes(query) ||
- r.version.toLowerCase().includes(query) ||
- r.major_version.toString().includes(query),
- );
- }
-
- return releases;
- }
-
- // Computed: available major versions for display
- get availableMajorVersions(): number[] {
- if (!this.javaCatalog) return [];
- let versions = [...this.javaCatalog.available_major_versions];
-
- // Filter by LTS if showOnlyRecommended is enabled
- if (this.showOnlyRecommended) {
- versions = versions.filter((v) => this.javaCatalog!.lts_versions.includes(v));
- }
-
- // Sort descending (newest first)
- return versions.sort((a, b) => b - a);
- }
-
- // Get installation status for a release: 'installed' | 'download'
- getInstallStatus(release: JavaReleaseInfo): "installed" | "download" {
- // Find installed Java that matches the major version and image type (by path pattern)
- const matchingInstallations = this.javaInstallations.filter((inst) => {
- // Check if this is a DropOut-managed Java (path contains temurin-XX-jre/jdk pattern)
- const pathLower = inst.path.toLowerCase();
- const pattern = `temurin-${release.major_version}-${release.image_type}`;
- return pathLower.includes(pattern);
- });
-
- // If any matching installation exists, it's installed
- return matchingInstallations.length > 0 ? "installed" : "download";
- }
-
- // Computed: selected release details
- get selectedRelease(): JavaReleaseInfo | null {
- if (!this.javaCatalog || this.selectedMajorVersion === null) return null;
- return (
- this.javaCatalog.releases.find(
- (r) =>
- r.major_version === this.selectedMajorVersion && r.image_type === this.selectedImageType,
- ) || null
- );
- }
-
- async loadSettings() {
- try {
- const result = await invoke<LauncherConfig>("get_settings");
- this.settings = result;
- // Force dark mode
- if (this.settings.theme !== "dark") {
- this.settings.theme = "dark";
- this.saveSettings();
- }
- // Ensure custom_background_path is reactive
- if (!this.settings.custom_background_path) {
- this.settings.custom_background_path = undefined;
- }
- } catch (e) {
- console.error("Failed to load settings:", e);
- }
- }
-
- async saveSettings() {
- try {
- // Ensure we clean up any invalid paths before saving
- if (this.settings.custom_background_path === "") {
- this.settings.custom_background_path = undefined;
- }
-
- await invoke("save_settings", { config: this.settings });
- uiState.setStatus("Settings saved!");
- } catch (e) {
- console.error("Failed to save settings:", e);
- uiState.setStatus("Error saving settings: " + e);
- }
- }
-
- async detectJava() {
- this.isDetectingJava = true;
- try {
- this.javaInstallations = await invoke("detect_java");
- if (this.javaInstallations.length === 0) {
- uiState.setStatus("No Java installations found");
- } else {
- uiState.setStatus(`Found ${this.javaInstallations.length} Java installation(s)`);
- }
- } catch (e) {
- console.error("Failed to detect Java:", e);
- uiState.setStatus("Error detecting Java: " + e);
- } finally {
- this.isDetectingJava = false;
- }
- }
-
- selectJava(path: string) {
- this.settings.java_path = path;
- }
-
- async openJavaDownloadModal() {
- this.showJavaDownloadModal = true;
- this.javaDownloadStatus = "";
- this.catalogError = "";
- this.downloadProgress = null;
-
- // Setup progress event listener
- await this.setupProgressListener();
-
- // Load catalog
- await this.loadJavaCatalog(false);
-
- // Check for pending downloads
- await this.loadPendingDownloads();
- }
-
- async closeJavaDownloadModal() {
- if (!this.isDownloadingJava) {
- this.showJavaDownloadModal = false;
- // Cleanup listener
- if (this.progressUnlisten) {
- this.progressUnlisten();
- this.progressUnlisten = null;
- }
- }
- }
-
- private async setupProgressListener() {
- if (this.progressUnlisten) {
- this.progressUnlisten();
- }
-
- this.progressUnlisten = await listen<JavaDownloadProgress>(
- "java-download-progress",
- (event) => {
- this.downloadProgress = event.payload;
- this.javaDownloadStatus = event.payload.status;
-
- if (event.payload.status === "Completed") {
- this.isDownloadingJava = false;
- setTimeout(async () => {
- await this.detectJava();
- uiState.setStatus(`Java installed successfully!`);
- }, 500);
- } else if (event.payload.status === "Error") {
- this.isDownloadingJava = false;
- }
- },
- );
- }
-
- async loadJavaCatalog(forceRefresh: boolean) {
- this.isLoadingCatalog = true;
- this.catalogError = "";
-
- try {
- const command = forceRefresh ? "refresh_java_catalog" : "fetch_java_catalog";
- this.javaCatalog = await invoke<JavaCatalog>(command);
-
- // Auto-select first LTS version
- if (this.selectedMajorVersion === null && this.javaCatalog.lts_versions.length > 0) {
- // Select most recent LTS (21 or highest)
- const ltsVersions = [...this.javaCatalog.lts_versions].sort((a, b) => b - a);
- this.selectedMajorVersion = ltsVersions[0];
- }
- } catch (e) {
- console.error("Failed to load Java catalog:", e);
- this.catalogError = `Failed to load Java catalog: ${e}`;
- } finally {
- this.isLoadingCatalog = false;
- }
- }
-
- async refreshCatalog() {
- await this.loadJavaCatalog(true);
- uiState.setStatus("Java catalog refreshed");
- }
-
- async loadPendingDownloads() {
- try {
- this.pendingDownloads = await invoke<PendingJavaDownload[]>("get_pending_java_downloads");
- } catch (e) {
- console.error("Failed to load pending downloads:", e);
- }
- }
-
- selectMajorVersion(version: number) {
- this.selectedMajorVersion = version;
- }
-
- async downloadJava() {
- if (!this.selectedRelease || !this.selectedRelease.is_available) {
- uiState.setStatus("Selected Java version is not available for this platform");
- return;
- }
-
- this.isDownloadingJava = true;
- this.javaDownloadStatus = "Starting download...";
- this.downloadProgress = null;
-
- try {
- const result: JavaInstallation = await invoke("download_adoptium_java", {
- majorVersion: this.selectedMajorVersion,
- imageType: this.selectedImageType,
- customPath: null,
- });
-
- this.settings.java_path = result.path;
- await this.detectJava();
-
- setTimeout(() => {
- this.showJavaDownloadModal = false;
- uiState.setStatus(`Java ${this.selectedMajorVersion} is ready to use!`);
- }, 1500);
- } catch (e) {
- console.error("Failed to download Java:", e);
- this.javaDownloadStatus = `Download failed: ${e}`;
- } finally {
- this.isDownloadingJava = false;
- }
- }
-
- async cancelDownload() {
- try {
- await invoke("cancel_java_download");
- this.isDownloadingJava = false;
- this.javaDownloadStatus = "Download cancelled";
- this.downloadProgress = null;
- await this.loadPendingDownloads();
- } catch (e) {
- console.error("Failed to cancel download:", e);
- }
- }
-
- async resumeDownloads() {
- if (this.pendingDownloads.length === 0) return;
-
- this.isDownloadingJava = true;
- this.javaDownloadStatus = "Resuming download...";
-
- try {
- const installed = await invoke<JavaInstallation[]>("resume_java_downloads");
- if (installed.length > 0) {
- this.settings.java_path = installed[0].path;
- await this.detectJava();
- uiState.setStatus(`Resumed and installed ${installed.length} Java version(s)`);
- }
- await this.loadPendingDownloads();
- } catch (e) {
- console.error("Failed to resume downloads:", e);
- this.javaDownloadStatus = `Resume failed: ${e}`;
- } finally {
- this.isDownloadingJava = false;
- }
- }
-
- // Format bytes to human readable
- formatBytes(bytes: number): string {
- if (bytes === 0) return "0 B";
- const k = 1024;
- const sizes = ["B", "KB", "MB", "GB"];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
- }
-
- // Format seconds to human readable
- formatTime(seconds: number): string {
- if (seconds === 0 || !isFinite(seconds)) return "--";
- if (seconds < 60) return `${Math.round(seconds)}s`;
- if (seconds < 3600) {
- const mins = Math.floor(seconds / 60);
- const secs = Math.round(seconds % 60);
- return `${mins}m ${secs}s`;
- }
- const hours = Math.floor(seconds / 3600);
- const mins = Math.floor((seconds % 3600) / 60);
- return `${hours}h ${mins}m`;
- }
-
- // Format date string
- formatDate(dateStr: string | null): string {
- if (!dateStr) return "--";
- try {
- const date = new Date(dateStr);
- return date.toLocaleDateString("en-US", {
- year: "2-digit",
- month: "2-digit",
- day: "2-digit",
- });
- } catch {
- return "--";
- }
- }
-
- // Legacy compatibility
- get availableJavaVersions(): number[] {
- return this.availableMajorVersions;
- }
-
- // AI Model loading methods
- async loadOllamaModels() {
- this.isLoadingOllamaModels = true;
- this.ollamaModelsError = "";
-
- try {
- const models = await invoke<ModelInfo[]>("list_ollama_models", {
- endpoint: this.settings.assistant.ollama_endpoint,
- });
- this.ollamaModels = models;
-
- // If no model is selected or selected model isn't available, select the first one
- if (models.length > 0) {
- const currentModel = this.settings.assistant.ollama_model;
- const modelExists = models.some((m) => m.id === currentModel);
- if (!modelExists) {
- this.settings.assistant.ollama_model = models[0].id;
- }
- }
- } catch (e) {
- console.error("Failed to load Ollama models:", e);
- this.ollamaModelsError = String(e);
- this.ollamaModels = [];
- } finally {
- this.isLoadingOllamaModels = false;
- }
- }
-
- async loadOpenaiModels() {
- if (!this.settings.assistant.openai_api_key) {
- this.openaiModelsError = "API key required";
- this.openaiModels = [];
- return;
- }
-
- this.isLoadingOpenaiModels = true;
- this.openaiModelsError = "";
-
- try {
- const models = await invoke<ModelInfo[]>("list_openai_models");
- this.openaiModels = models;
-
- // If no model is selected or selected model isn't available, select the first one
- if (models.length > 0) {
- const currentModel = this.settings.assistant.openai_model;
- const modelExists = models.some((m) => m.id === currentModel);
- if (!modelExists) {
- this.settings.assistant.openai_model = models[0].id;
- }
- }
- } catch (e) {
- console.error("Failed to load OpenAI models:", e);
- this.openaiModelsError = String(e);
- this.openaiModels = [];
- } finally {
- this.isLoadingOpenaiModels = false;
- }
- }
-
- // Computed: get model options for current provider
- get currentModelOptions(): { value: string; label: string; details?: string }[] {
- const provider = this.settings.assistant.llm_provider;
-
- if (provider === "ollama") {
- if (this.ollamaModels.length === 0) {
- // Return fallback options if no models loaded
- return [
- { value: "llama3", label: "Llama 3" },
- { value: "llama3.1", label: "Llama 3.1" },
- { value: "llama3.2", label: "Llama 3.2" },
- { value: "mistral", label: "Mistral" },
- { value: "gemma2", label: "Gemma 2" },
- { value: "qwen2.5", label: "Qwen 2.5" },
- { value: "phi3", label: "Phi-3" },
- { value: "codellama", label: "Code Llama" },
- ];
- }
- return this.ollamaModels.map((m) => ({
- value: m.id,
- label: m.name,
- details: m.size ? `${m.size}${m.details ? ` - ${m.details}` : ""}` : m.details,
- }));
- } else if (provider === "openai") {
- if (this.openaiModels.length === 0) {
- // Return fallback options if no models loaded
- return [
- { value: "gpt-4o", label: "GPT-4o" },
- { value: "gpt-4o-mini", label: "GPT-4o Mini" },
- { value: "gpt-4-turbo", label: "GPT-4 Turbo" },
- { value: "gpt-4", label: "GPT-4" },
- { value: "gpt-3.5-turbo", label: "GPT-3.5 Turbo" },
- ];
- }
- return this.openaiModels.map((m) => ({
- value: m.id,
- label: m.name,
- details: m.details,
- }));
- }
-
- return [];
- }
-}
-
-export const settingsState = new SettingsState();
diff --git a/packages/ui-new/src/stores/ui-store.ts b/packages/ui/src/stores/ui-store.ts
index 89b9191..89b9191 100644
--- a/packages/ui-new/src/stores/ui-store.ts
+++ b/packages/ui/src/stores/ui-store.ts
diff --git a/packages/ui/src/stores/ui.svelte.ts b/packages/ui/src/stores/ui.svelte.ts
deleted file mode 100644
index e88f6b4..0000000
--- a/packages/ui/src/stores/ui.svelte.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { type ViewType } from "../types";
-
-export class UIState {
- currentView: ViewType = $state("home");
- status = $state("Ready");
- showConsole = $state(false);
- appVersion = $state("...");
-
- private statusTimeout: ReturnType<typeof setTimeout> | null = null;
-
- setStatus(msg: string) {
- if (this.statusTimeout) clearTimeout(this.statusTimeout);
-
- this.status = msg;
-
- if (msg !== "Ready") {
- this.statusTimeout = setTimeout(() => {
- this.status = "Ready";
- }, 5000);
- }
- }
-
- toggleConsole() {
- this.showConsole = !this.showConsole;
- }
-
- setView(view: ViewType) {
- this.currentView = view;
- }
-}
-
-export const uiState = new UIState();
diff --git a/packages/ui-new/src/types/bindings/account.ts b/packages/ui/src/types/bindings/account.ts
index 168d138..168d138 100644
--- a/packages/ui-new/src/types/bindings/account.ts
+++ b/packages/ui/src/types/bindings/account.ts
diff --git a/packages/ui-new/src/types/bindings/assistant.ts b/packages/ui/src/types/bindings/assistant.ts
index 827f008..827f008 100644
--- a/packages/ui-new/src/types/bindings/assistant.ts
+++ b/packages/ui/src/types/bindings/assistant.ts
diff --git a/packages/ui-new/src/types/bindings/auth.ts b/packages/ui/src/types/bindings/auth.ts
index 563a924..563a924 100644
--- a/packages/ui-new/src/types/bindings/auth.ts
+++ b/packages/ui/src/types/bindings/auth.ts
diff --git a/packages/ui-new/src/types/bindings/config.ts b/packages/ui/src/types/bindings/config.ts
index e9de4f5..e9de4f5 100644
--- a/packages/ui-new/src/types/bindings/config.ts
+++ b/packages/ui/src/types/bindings/config.ts
diff --git a/packages/ui-new/src/types/bindings/core.ts b/packages/ui/src/types/bindings/core.ts
index 94e3bde..94e3bde 100644
--- a/packages/ui-new/src/types/bindings/core.ts
+++ b/packages/ui/src/types/bindings/core.ts
diff --git a/packages/ui-new/src/types/bindings/downloader.ts b/packages/ui/src/types/bindings/downloader.ts
index f2be278..f2be278 100644
--- a/packages/ui-new/src/types/bindings/downloader.ts
+++ b/packages/ui/src/types/bindings/downloader.ts
diff --git a/packages/ui-new/src/types/bindings/fabric.ts b/packages/ui/src/types/bindings/fabric.ts
index 181f8be..181f8be 100644
--- a/packages/ui-new/src/types/bindings/fabric.ts
+++ b/packages/ui/src/types/bindings/fabric.ts
diff --git a/packages/ui-new/src/types/bindings/forge.ts b/packages/ui/src/types/bindings/forge.ts
index a9790e7..a9790e7 100644
--- a/packages/ui-new/src/types/bindings/forge.ts
+++ b/packages/ui/src/types/bindings/forge.ts
diff --git a/packages/ui-new/src/types/bindings/game-version.ts b/packages/ui/src/types/bindings/game-version.ts
index 1b1c395..1b1c395 100644
--- a/packages/ui-new/src/types/bindings/game-version.ts
+++ b/packages/ui/src/types/bindings/game-version.ts
diff --git a/packages/ui-new/src/types/bindings/index.ts b/packages/ui/src/types/bindings/index.ts
index 9bde037..9bde037 100644
--- a/packages/ui-new/src/types/bindings/index.ts
+++ b/packages/ui/src/types/bindings/index.ts
diff --git a/packages/ui-new/src/types/bindings/instance.ts b/packages/ui/src/types/bindings/instance.ts
index 2c4f8ae..2c4f8ae 100644
--- a/packages/ui-new/src/types/bindings/instance.ts
+++ b/packages/ui/src/types/bindings/instance.ts
diff --git a/packages/ui-new/src/types/bindings/java/core.ts b/packages/ui/src/types/bindings/java/core.ts
index 099dea9..099dea9 100644
--- a/packages/ui-new/src/types/bindings/java/core.ts
+++ b/packages/ui/src/types/bindings/java/core.ts
diff --git a/packages/ui-new/src/types/bindings/java/index.ts b/packages/ui/src/types/bindings/java/index.ts
index 2f2754c..2f2754c 100644
--- a/packages/ui-new/src/types/bindings/java/index.ts
+++ b/packages/ui/src/types/bindings/java/index.ts
diff --git a/packages/ui-new/src/types/bindings/java/persistence.ts b/packages/ui/src/types/bindings/java/persistence.ts
index 7a2b576..7a2b576 100644
--- a/packages/ui-new/src/types/bindings/java/persistence.ts
+++ b/packages/ui/src/types/bindings/java/persistence.ts
diff --git a/packages/ui-new/src/types/bindings/java/providers/adoptium.ts b/packages/ui/src/types/bindings/java/providers/adoptium.ts
index 65fc42b..65fc42b 100644
--- a/packages/ui-new/src/types/bindings/java/providers/adoptium.ts
+++ b/packages/ui/src/types/bindings/java/providers/adoptium.ts
diff --git a/packages/ui-new/src/types/bindings/java/providers/index.ts b/packages/ui/src/types/bindings/java/providers/index.ts
index 3e28711..3e28711 100644
--- a/packages/ui-new/src/types/bindings/java/providers/index.ts
+++ b/packages/ui/src/types/bindings/java/providers/index.ts
diff --git a/packages/ui-new/src/types/bindings/manifest.ts b/packages/ui/src/types/bindings/manifest.ts
index 2180962..2180962 100644
--- a/packages/ui-new/src/types/bindings/manifest.ts
+++ b/packages/ui/src/types/bindings/manifest.ts
diff --git a/packages/ui/src/types/index.ts b/packages/ui/src/types/index.ts
index b4412b8..9e592d7 100644
--- a/packages/ui/src/types/index.ts
+++ b/packages/ui/src/types/index.ts
@@ -1,232 +1 @@
-export type ViewType = "home" | "versions" | "settings" | "guide" | "instances";
-
-export interface Version {
- id: string;
- type: string;
- url: string;
- time: string;
- releaseTime: string;
- javaVersion?: number; // Java major version requirement (e.g., 8, 17, 21)
- isInstalled?: boolean; // Whether this version is installed locally
-}
-
-export interface Account {
- type: "Offline" | "Microsoft";
- username: string;
- uuid: string;
- access_token?: string;
- refresh_token?: string;
- expires_at?: number; // Unix timestamp for Microsoft accounts
-}
-
-export interface DeviceCodeResponse {
- user_code: string;
- device_code: string;
- verification_uri: string;
- expires_in: number;
- interval: number;
- message?: string;
-}
-
-export interface AssistantConfig {
- enabled: boolean;
- llm_provider: "ollama" | "openai";
- // Ollama settings
- ollama_endpoint: string;
- ollama_model: string;
- // OpenAI settings
- openai_api_key?: string;
- openai_endpoint: string;
- openai_model: string;
- // Common settings
- system_prompt: string;
- response_language: string;
- // TTS settings
- tts_enabled: boolean;
- tts_provider: string;
-}
-
-export interface ModelInfo {
- id: string;
- name: string;
- size?: string;
- details?: string;
-}
-
-export interface LauncherConfig {
- min_memory: number;
- max_memory: number;
- java_path: string;
- width: number;
- height: number;
- download_threads: number;
- custom_background_path?: string;
- enable_gpu_acceleration: boolean;
- enable_visual_effects: boolean;
- active_effect: string;
- theme: string;
- log_upload_service: "paste.rs" | "pastebin.com";
- pastebin_api_key?: string;
- assistant: AssistantConfig;
- // Storage management
- use_shared_caches: boolean;
- keep_legacy_per_instance_storage: boolean;
- // Feature-gated argument flags
- feature_flags: FeatureFlags;
-}
-
-export interface FeatureFlags {
- demo_user: boolean;
- quick_play_enabled: boolean;
- quick_play_path?: string;
- quick_play_singleplayer: boolean;
- quick_play_multiplayer_server?: string;
-}
-
-export interface JavaInstallation {
- path: string;
- version: string;
- is_64bit: boolean;
-}
-
-export interface JavaDownloadInfo {
- version: string;
- release_name: string;
- download_url: string;
- file_name: string;
- file_size: number;
- checksum: string | null;
- image_type: string;
-}
-
-export interface JavaReleaseInfo {
- major_version: number;
- image_type: string;
- version: string;
- release_name: string;
- release_date: string | null;
- file_size: number;
- checksum: string | null;
- download_url: string;
- is_lts: boolean;
- is_available: boolean;
- architecture: string;
-}
-
-export interface JavaCatalog {
- releases: JavaReleaseInfo[];
- available_major_versions: number[];
- lts_versions: number[];
- cached_at: number;
-}
-
-export interface JavaDownloadProgress {
- file_name: string;
- downloaded_bytes: number;
- total_bytes: number;
- speed_bytes_per_sec: number;
- eta_seconds: number;
- status: string;
- percentage: number;
-}
-
-export interface PendingJavaDownload {
- major_version: number;
- image_type: string;
- download_url: string;
- file_name: string;
- file_size: number;
- checksum: string | null;
- install_path: string;
- created_at: number;
-}
-
-export type JavaDownloadSource = "adoptium" | "mojang" | "azul";
-
-// ==================== Fabric Types ====================
-
-export interface FabricGameVersion {
- version: string;
- stable: boolean;
-}
-
-export interface FabricLoaderVersion {
- separator: string;
- build: number;
- maven: string;
- version: string;
- stable: boolean;
-}
-
-export interface FabricLoaderEntry {
- loader: FabricLoaderVersion;
- intermediary: {
- maven: string;
- version: string;
- stable: boolean;
- };
- launcherMeta: {
- version: number;
- mainClass: {
- client: string;
- server: string;
- };
- };
-}
-
-export interface InstalledFabricVersion {
- id: string;
- minecraft_version: string;
- loader_version: string;
- path: string;
-}
-
-// ==================== Forge Types ====================
-
-export interface ForgeVersion {
- version: string;
- minecraft_version: string;
- recommended: boolean;
- latest: boolean;
-}
-
-export interface InstalledForgeVersion {
- id: string;
- minecraft_version: string;
- forge_version: string;
- path: string;
-}
-
-// ==================== Mod Loader Type ====================
-
-export type ModLoaderType = "vanilla" | "fabric" | "forge";
-
-// ==================== Instance Types ====================
-
-export interface Instance {
- id: string;
- name: string;
- game_dir: string;
- version_id?: string;
- created_at: number;
- last_played?: number;
- icon_path?: string;
- notes?: string;
- mod_loader?: string;
- mod_loader_version?: string;
- jvm_args_override?: string;
- memory_override?: MemoryOverride;
-}
-
-export interface MemoryOverride {
- min: number; // MB
- max: number; // MB
-}
-
-export interface FileInfo {
- name: string;
- path: string;
- is_directory: boolean;
- size: number;
- modified: number;
-}
+export * from "./bindings";
diff --git a/packages/ui/svelte.config.js b/packages/ui/svelte.config.js
deleted file mode 100644
index a710f1b..0000000
--- a/packages/ui/svelte.config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
-
-/** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */
-export default {
- // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
- // for more information about preprocessors
- preprocess: vitePreprocess(),
-};
diff --git a/packages/ui/tsconfig.app.json b/packages/ui/tsconfig.app.json
index addb46d..54f0bdf 100644
--- a/packages/ui/tsconfig.app.json
+++ b/packages/ui/tsconfig.app.json
@@ -1,22 +1,34 @@
{
- "extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
"moduleResolution": "bundler",
- "types": ["svelte", "vite/client"],
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
"noEmit": true,
- /**
- * Typecheck JS in `.svelte` and `.js` files by default.
- * Disable checkJs if you'd like to use dynamic types in JS.
- * Note that setting allowJs false does not prevent the use
- * of JS in `.svelte` files.
- */
- "allowJs": true,
- "checkJs": true,
- "moduleDetection": "force"
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true,
+
+ /* Paths */
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
},
- "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
+ "include": ["src"]
}
diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json
index d32ff68..fec8c8e 100644
--- a/packages/ui/tsconfig.json
+++ b/packages/ui/tsconfig.json
@@ -1,4 +1,13 @@
{
"files": [],
- "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ],
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
}
diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts
index 32610e2..27ce1ff 100644
--- a/packages/ui/vite.config.ts
+++ b/packages/ui/vite.config.ts
@@ -1,26 +1,18 @@
-import { defineConfig } from "vite";
-import { svelte } from "@sveltejs/vite-plugin-svelte";
import tailwindcss from "@tailwindcss/vite";
+import react from "@vitejs/plugin-react";
+import path from "path";
+import { defineConfig } from "vite";
// https://vite.dev/config/
export default defineConfig({
- plugins: [tailwindcss(), svelte()],
-
- // Fix for Tauri + Vite HMR
- server: {
- host: true,
- strictPort: true,
- hmr: {
- protocol: "ws",
- host: "localhost",
- port: 5173,
- },
- watch: {
- usePolling: true,
+ plugins: [react(), tailwindcss()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ "@components": path.resolve(__dirname, "./src/components"),
+ "@stores": path.resolve(__dirname, "./src/stores"),
+ "@types": path.resolve(__dirname, "./src/types"),
+ "@pages": path.resolve(__dirname, "./src/pages"),
},
},
-
- // Ensure compatibility with Tauri
- clearScreen: false,
- envPrefix: ["VITE_", "TAURI_"],
});
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index a64d7e9..1f7ebe9 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -3,10 +3,10 @@
"version": "0.2.0-alpha.1",
"identifier": "com.dropout.launcher",
"build": {
- "beforeDevCommand": "pnpm --filter @dropout/ui-new dev",
- "beforeBuildCommand": "pnpm --filter @dropout/ui-new build",
+ "beforeDevCommand": "pnpm --filter @dropout/ui dev",
+ "beforeBuildCommand": "pnpm --filter @dropout/ui build",
"devUrl": "http://localhost:5173",
- "frontendDist": "../packages/ui-new/dist"
+ "frontendDist": "../packages/ui/dist"
},
"app": {
"windows": [