diff options
Diffstat (limited to 'ui/src/components')
| -rw-r--r-- | ui/src/components/BottomBar.svelte | 172 | ||||
| -rw-r--r-- | ui/src/components/HomeView.svelte | 70 | ||||
| -rw-r--r-- | ui/src/components/ModLoaderSelector.svelte | 149 | ||||
| -rw-r--r-- | ui/src/components/ParticleBackground.svelte | 15 | ||||
| -rw-r--r-- | ui/src/components/VersionsView.svelte | 2 |
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" }; } |