aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/ui/src/components
diff options
context:
space:
mode:
author简律纯 <i@jyunko.cn>2026-01-15 20:48:39 +0800
committerGitHub <noreply@github.com>2026-01-15 20:48:39 +0800
commit5931daf9283478f49652098c3e0f6be8de0f52f8 (patch)
treed150c7733d039d71e40a9d3298952a4627fe2584 /ui/src/components
parent32a9aceee42a2261b64f9e6effda522639576a5e (diff)
parent959d1c54e6b5b101b20c027d547707a40ab0a29b (diff)
downloadDropOut-5931daf9283478f49652098c3e0f6be8de0f52f8.tar.gz
DropOut-5931daf9283478f49652098c3e0f6be8de0f52f8.zip
Merge pull request #32 from HsiangNianian/main
Diffstat (limited to 'ui/src/components')
-rw-r--r--ui/src/components/BottomBar.svelte172
-rw-r--r--ui/src/components/HomeView.svelte70
-rw-r--r--ui/src/components/ModLoaderSelector.svelte149
-rw-r--r--ui/src/components/ParticleBackground.svelte15
-rw-r--r--ui/src/components/VersionsView.svelte2
5 files changed, 340 insertions, 68 deletions
diff --git a/ui/src/components/BottomBar.svelte b/ui/src/components/BottomBar.svelte
index abb0b23..b7bbf71 100644
--- a/ui/src/components/BottomBar.svelte
+++ b/ui/src/components/BottomBar.svelte
@@ -1,23 +1,68 @@
<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 { Terminal, ChevronDown, Play, User, Check } from 'lucide-svelte';
+ import { Terminal, ChevronDown, Play, User, Check, RefreshCw } 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;
+
+ // Load installed versions on mount
+ $effect(() => {
+ loadInstalledVersions();
+ setupDownloadListener();
+ return () => {
+ if (downloadCompleteUnlisten) {
+ downloadCompleteUnlisten();
+ }
+ };
+ });
+
+ async function setupDownloadListener() {
+ // Refresh list when a download completes
+ downloadCompleteUnlisten = await listen("download-complete", () => {
+ loadInstalledVersions();
+ });
+ }
+
+ async function loadInstalledVersions() {
+ isLoadingVersions = true;
+ try {
+ installedVersions = await invoke<InstalledVersion[]>("list_installed_versions");
+ // 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(
- gameState.versions.length === 0
+ isLoadingVersions
? [{ id: "loading", type: "loading", label: "Loading..." }]
- : gameState.versions.map(v => ({
- ...v,
- label: `${v.id}${v.type !== 'release' ? ` (${v.type})` : ''}`
- }))
+ : 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") {
+ if (id !== "loading" && id !== "empty") {
gameState.selectedVersion = id;
isVersionDropdownOpen = false;
}
@@ -35,6 +80,16 @@
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
@@ -67,12 +122,23 @@
{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">
- <span
- class="w-1.5 h-1.5 rounded-full {authState.currentAccount
- ? 'bg-emerald-500'
- : 'bg-zinc-400'}"
- ></span>
- {authState.currentAccount ? "Online" : "Guest"}
+ {#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>
@@ -94,47 +160,70 @@
<div class="flex flex-col items-end mr-2">
<!-- Custom Version Dropdown -->
<div class="relative" bind:this={dropdownRef}>
- <button
- type="button"
- onclick={() => isVersionDropdownOpen = !isVersionDropdownOpen}
- 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"
- >
- <span class="truncate">
- {#if gameState.versions.length === 0}
- Loading...
- {: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>
+ <div class="flex items-center gap-2">
+ <button
+ type="button"
+ onclick={() => loadInstalledVersions()}
+ class="p-2.5 dark:bg-zinc-900 bg-zinc-50 border dark:border-zinc-700 border-zinc-300 rounded-md
+ dark:text-zinc-500 text-zinc-400 dark:hover:text-white hover:text-black
+ dark:hover:border-zinc-600 hover:border-zinc-400 transition-colors"
+ title="Refresh installed versions"
+ >
+ <RefreshCw size={14} class={isLoadingVersions ? 'animate-spin' : ''} />
+ </button>
+ <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>
+ </div>
- {#if isVersionDropdownOpen}
+ {#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"
+ 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"}
+ 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' ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}"
+ {version.id === 'loading' || version.id === 'empty' ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}"
>
- <span class="truncate">{version.label}</span>
+ <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}
@@ -147,7 +236,8 @@
<button
onclick={() => gameState.startGame()}
- class="bg-emerald-600 hover:bg-emerald-500 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"
+ 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>
diff --git a/ui/src/components/HomeView.svelte b/ui/src/components/HomeView.svelte
index 7bb7e44..2fa8390 100644
--- a/ui/src/components/HomeView.svelte
+++ b/ui/src/components/HomeView.svelte
@@ -3,6 +3,7 @@
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;
@@ -10,6 +11,60 @@
};
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();
});
@@ -65,6 +120,7 @@
// 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>');
@@ -103,8 +159,18 @@
<!-- Scrollable Container -->
<div class="relative z-10 h-full {releasesState.isLoading ? 'overflow-hidden' : 'overflow-y-auto custom-scrollbar scroll-smooth'}">
- <!-- Hero Section (Full Height) -->
- <div class="min-h-full flex flex-col justify-end p-12 pb-32">
+ <!-- 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"
diff --git a/ui/src/components/ModLoaderSelector.svelte b/ui/src/components/ModLoaderSelector.svelte
index cb949c5..e9d147b 100644
--- a/ui/src/components/ModLoaderSelector.svelte
+++ b/ui/src/components/ModLoaderSelector.svelte
@@ -1,12 +1,14 @@
<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 } from 'lucide-svelte';
+ import { Loader2, Download, AlertCircle, Check, ChevronDown, CheckCircle } from 'lucide-svelte';
+ import { logsState } from "../stores/logs.svelte";
interface Props {
selectedGameVersion: string;
@@ -18,7 +20,9 @@
// 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[]>([]);
@@ -33,13 +37,35 @@
let fabricDropdownRef = $state<HTMLDivElement | null>(null);
let forgeDropdownRef = $state<HTMLDivElement | null>(null);
- // Load mod loader versions when game version changes
+ // 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) {
+ isVersionInstalled = false;
+ return;
+ }
+ try {
+ isVersionInstalled = await invoke<boolean>("check_version_installed", {
+ versionId: selectedGameVersion,
+ });
+ } catch (e) {
+ console.error("Failed to check install status:", e);
+ isVersionInstalled = false;
+ }
+ }
+
async function loadModLoaderVersions() {
isLoading = true;
error = null;
@@ -51,7 +77,6 @@
});
fabricLoaders = loaders.map((l) => l.loader);
if (fabricLoaders.length > 0) {
- // Select first stable version or first available
const stable = fabricLoaders.find((l) => l.stable);
selectedFabricLoader = stable?.version || fabricLoaders[0].version;
}
@@ -63,7 +88,6 @@
}
);
if (forgeVersions.length > 0) {
- // Select recommended version first, then latest
const recommended = forgeVersions.find((v) => v.recommended);
const latest = forgeVersions.find((v) => v.latest);
selectedForgeVersion =
@@ -78,34 +102,75 @@
}
}
+ 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}...`);
+
+ try {
+ await invoke("install_version", {
+ 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;
}
- isLoading = true;
+ 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", {
+ 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", {
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", {
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 {
- isLoading = false;
+ isInstalling = false;
}
}
@@ -170,6 +235,7 @@
? '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>
@@ -178,15 +244,38 @@
<!-- Content Area -->
<div class="min-h-[100px] flex flex-col justify-center">
- {#if selectedLoader === "vanilla"}
- <div class="text-center p-6 border border-dashed border-zinc-200 dark:border-white/10 rounded-sm text-zinc-500 text-sm">
- Standard Minecraft experience. No modifications.
- </div>
-
- {:else if !selectedGameVersion}
+ {#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 base Minecraft version first.</span>
+ <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}
@@ -211,12 +300,13 @@
<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"
+ transition-colors cursor-pointer outline-none disabled:opacity-50"
>
<span class="truncate">{selectedFabricLabel}</span>
<ChevronDown
@@ -252,12 +342,17 @@
</div>
<button
- class="w-full bg-black dark:bg-white text-white dark:text-black py-2.5 px-4 rounded-sm font-bold text-sm transition-all hover:opacity-90 flex items-center justify-center gap-2"
+ 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={isLoading || !selectedFabricLoader}
+ disabled={isInstalling || !selectedFabricLoader}
>
- <Download size={16} />
- Install Fabric
+ {#if isInstalling}
+ <Loader2 class="animate-spin" size={16} />
+ Installing...
+ {:else}
+ <Download size={16} />
+ Install Fabric {selectedFabricLoader}
+ {/if}
</button>
</div>
@@ -277,12 +372,13 @@
<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"
+ transition-colors cursor-pointer outline-none disabled:opacity-50"
>
<span class="truncate">{selectedForgeLabel}</span>
<ChevronDown
@@ -318,12 +414,17 @@
</div>
<button
- class="w-full bg-black dark:bg-white text-white dark:text-black py-2.5 px-4 rounded-sm font-bold text-sm transition-all hover:opacity-90 flex items-center justify-center gap-2"
- onclick={installModLoader}
- disabled={isLoading || !selectedForgeVersion}
+ 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}
>
- <Download size={16} />
- Install Forge
+ {#if isInstalling}
+ <Loader2 class="animate-spin" size={16} />
+ Installing...
+ {:else}
+ <Download size={16} />
+ Install Forge {selectedForgeVersion}
+ {/if}
</button>
{/if}
</div>
diff --git a/ui/src/components/ParticleBackground.svelte b/ui/src/components/ParticleBackground.svelte
index 080f1f2..7644b1a 100644
--- a/ui/src/components/ParticleBackground.svelte
+++ b/ui/src/components/ParticleBackground.svelte
@@ -1,7 +1,17 @@
+<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 { SaturnEffect } from "../lib/effects/SaturnEffect";
import { settingsState } from "../stores/settings.svelte";
let canvas: HTMLCanvasElement;
@@ -16,8 +26,10 @@
if (settingsState.settings.active_effect === "saturn") {
activeEffect = new SaturnEffect(canvas);
+ globalSaturnEffect = activeEffect;
} else {
activeEffect = new ConstellationEffect(canvas);
+ globalSaturnEffect = null;
}
// Ensure correct size immediately
@@ -48,6 +60,7 @@
onDestroy(() => {
if (activeEffect) activeEffect.destroy();
+ globalSaturnEffect = null;
});
</script>
diff --git a/ui/src/components/VersionsView.svelte b/ui/src/components/VersionsView.svelte
index 99cc296..ce354b9 100644
--- a/ui/src/components/VersionsView.svelte
+++ b/ui/src/components/VersionsView.svelte
@@ -80,6 +80,8 @@
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" };
}