diff options
| author | 2026-01-15 12:52:08 +0800 | |
|---|---|---|
| committer | 2026-01-15 12:52:08 +0800 | |
| commit | 3dd9b12033f4d2284601089201594a3e1049d7c2 (patch) | |
| tree | 22ace52c4a6e8d9d7fa6fb7a7423999970b6d2a9 /ui | |
| parent | d4d37b4d418ab2a517ce6ce2b8272cf084fdc724 (diff) | |
| parent | 43a3e9c285f3d5d04fef025041a06609a0d1c218 (diff) | |
| download | DropOut-3dd9b12033f4d2284601089201594a3e1049d7c2.tar.gz DropOut-3dd9b12033f4d2284601089201594a3e1049d7c2.zip | |
Merge pull request #13 from BegoniaHe/feat/download-java-rt
Diffstat (limited to 'ui')
| -rw-r--r-- | ui/src/App.svelte | 25 | ||||
| -rw-r--r-- | ui/src/components/SettingsView.svelte | 348 | ||||
| -rw-r--r-- | ui/src/components/VersionsView.svelte | 2 | ||||
| -rw-r--r-- | ui/src/lib/effects/SaturnEffect.ts | 2 | ||||
| -rw-r--r-- | ui/src/stores/settings.svelte.ts | 309 | ||||
| -rw-r--r-- | ui/src/types/index.ts | 54 |
6 files changed, 717 insertions, 23 deletions
diff --git a/ui/src/App.svelte b/ui/src/App.svelte index 1c465b1..f32a42f 100644 --- a/ui/src/App.svelte +++ b/ui/src/App.svelte @@ -1,25 +1,23 @@ <script lang="ts"> import { getVersion } from "@tauri-apps/api/app"; - import { onMount, onDestroy } from "svelte"; import { convertFileSrc } from "@tauri-apps/api/core"; + import { onDestroy, onMount } from "svelte"; import DownloadMonitor from "./lib/DownloadMonitor.svelte"; import GameConsole from "./lib/GameConsole.svelte"; - - // Components - import Sidebar from "./components/Sidebar.svelte"; - import HomeView from "./components/HomeView.svelte"; - import VersionsView from "./components/VersionsView.svelte"; - import SettingsView from "./components/SettingsView.svelte"; +// Components import BottomBar from "./components/BottomBar.svelte"; + import HomeView from "./components/HomeView.svelte"; import LoginModal from "./components/LoginModal.svelte"; - import StatusToast from "./components/StatusToast.svelte"; import ParticleBackground from "./components/ParticleBackground.svelte"; - - // Stores - import { uiState } from "./stores/ui.svelte"; + import SettingsView from "./components/SettingsView.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 { settingsState } from "./stores/settings.svelte"; import { gameState } from "./stores/game.svelte"; + import { settingsState } from "./stores/settings.svelte"; + import { uiState } from "./stores/ui.svelte"; let mouseX = $state(0); let mouseY = $state(0); @@ -32,6 +30,7 @@ onMount(async () => { authState.checkAccount(); await settingsState.loadSettings(); + await settingsState.detectJava(); gameState.loadVersions(); getVersion().then((v) => (uiState.appVersion = v)); window.addEventListener("mousemove", handleMouseMove); @@ -140,7 +139,7 @@ {#if uiState.showConsole} <!-- Assuming GameConsole handles its own display mode or overlay --> - <div class="fixed inset-0 z-[100] bg-black/80 flex items-center justify-center p-10"> + <div class="fixed inset-0 z-[100] bg-black/80 flex items-center justify-center p-10"> <div class="w-full h-full bg-[#1e1e1e] rounded-xl overflow-hidden border border-white/10 shadow-2xl relative"> <button class="absolute top-4 right-4 text-white hover:text-red-400 z-10" onclick={() => uiState.toggleConsole()}>✕</button> <GameConsole /> diff --git a/ui/src/components/SettingsView.svelte b/ui/src/components/SettingsView.svelte index 86bcce1..42b1056 100644 --- a/ui/src/components/SettingsView.svelte +++ b/ui/src/components/SettingsView.svelte @@ -1,7 +1,7 @@ <script lang="ts"> - import { settingsState } from "../stores/settings.svelte"; - import { open } from "@tauri-apps/plugin-dialog"; import { convertFileSrc } from "@tauri-apps/api/core"; + import { open } from "@tauri-apps/plugin-dialog"; + import { settingsState } from "../stores/settings.svelte"; async function selectBackground() { try { @@ -51,9 +51,9 @@ <!-- 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" + <img + src={convertFileSrc(settingsState.settings.custom_background_path)} + alt="Background Preview" class="w-full h-full object-cover" /> {:else} @@ -183,6 +183,12 @@ > {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> @@ -297,3 +303,335 @@ </div> </div> </div> + +<!-- 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/ui/src/components/VersionsView.svelte b/ui/src/components/VersionsView.svelte index 8f3a568..99cc296 100644 --- a/ui/src/components/VersionsView.svelte +++ b/ui/src/components/VersionsView.svelte @@ -158,7 +158,7 @@ Fetching manifest... </div> {:else if filteredVersions().length === 0} - <div class="flex flex-col items-center justify-center -40 dark:text-white/30 text-black/30 gap-2"> + <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> diff --git a/ui/src/lib/effects/SaturnEffect.ts b/ui/src/lib/effects/SaturnEffect.ts index 8a1c11f..a370936 100644 --- a/ui/src/lib/effects/SaturnEffect.ts +++ b/ui/src/lib/effects/SaturnEffect.ts @@ -171,7 +171,7 @@ export class SaturnEffect { // Optimization: Planet color vs Ring color if (type === 0) { - // Planet: Warn White + // Planet: Warm White this.ctx.fillStyle = `rgba(255, 240, 220, ${alpha})`; } else { // Ring: Cool White diff --git a/ui/src/stores/settings.svelte.ts b/ui/src/stores/settings.svelte.ts index b67cdc3..cad466d 100644 --- a/ui/src/stores/settings.svelte.ts +++ b/ui/src/stores/settings.svelte.ts @@ -1,5 +1,14 @@ import { invoke } from "@tauri-apps/api/core"; -import type { LauncherConfig, JavaInstallation } from "../types"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import type { + JavaCatalog, + JavaDownloadProgress, + JavaDownloadSource, + JavaInstallation, + JavaReleaseInfo, + LauncherConfig, + PendingJavaDownload, +} from "../types"; import { uiState } from "./ui.svelte"; export class SettingsState { @@ -18,14 +27,109 @@ export class SettingsState { 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[]>([]); + + // Event listener cleanup + private progressUnlisten: UnlistenFn | null = null; + + // 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(); + this.settings.theme = "dark"; + this.saveSettings(); } } catch (e) { console.error("Failed to load settings:", e); @@ -62,6 +166,205 @@ export class SettingsState { 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; + } } export const settingsState = new SettingsState(); diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index 7e2cc67..163dc92 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -43,6 +43,60 @@ export interface JavaInstallation { 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 { |