aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/ui/src
diff options
context:
space:
mode:
Diffstat (limited to 'ui/src')
-rw-r--r--ui/src/App.svelte25
-rw-r--r--ui/src/components/SettingsView.svelte348
-rw-r--r--ui/src/components/VersionsView.svelte2
-rw-r--r--ui/src/lib/effects/SaturnEffect.ts2
-rw-r--r--ui/src/stores/settings.svelte.ts309
-rw-r--r--ui/src/types/index.ts54
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 {