From 43a3e9c285f3d5d04fef025041a06609a0d1c218 Mon Sep 17 00:00:00 2001
From: "Begonia, HE" <163421589+BegoniaHe@users.noreply.github.com>
Date: Thu, 15 Jan 2026 05:29:58 +0100
Subject: feat(java): Implement Java catalog management and download features

- Added commands to fetch and refresh the Java catalog, cancel downloads, and manage pending downloads.
- Enhanced the Java download modal in the UI to support version selection, download progress, and pending downloads.
- Introduced new types for Java catalog, download progress, and pending downloads.
- Updated settings store to handle Java catalog state, download progress, and pending downloads.
- Improved user experience with loading states, error handling, and status notifications for Java installations.
---
 ui/src/components/SettingsView.svelte | 375 ++++++++++++++++++++++++++++------
 1 file changed, 318 insertions(+), 57 deletions(-)

(limited to 'ui/src/components/SettingsView.svelte')

diff --git a/ui/src/components/SettingsView.svelte b/ui/src/components/SettingsView.svelte
index d409784..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}
@@ -307,68 +307,329 @@
 <!-- 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 p-8 max-w-md w-full mx-4 shadow-2xl">
-      <h3 class="text-2xl font-bold mb-6 text-white">Download Java</h3>
-      
-      <div class="space-y-6">
-        <!-- Java Version Selection -->
-        <div>
-          <label class="block text-sm font-medium text-white/70 mb-2">Java Version</label>
-          <select
-            bind:value={settingsState.selectedJavaVersion}
-            class="bg-black/40 text-white w-full px-4 py-3 rounded-xl border border-white/10 focus:border-indigo-500/50 outline-none"
+    <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"
           >
-            {#each settingsState.availableJavaVersions as version}
-              <option value={version}>Java {version}</option>
-            {/each}
-          </select>
+            <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>
 
-        <!-- Image Type Selection -->
-        <div>
-          <label class="block text-sm font-medium text-white/70 mb-2">Image Type</label>
-          <div class="grid grid-cols-2 gap-3">
-            <button
-              onclick={() => settingsState.selectedImageType = "jre"}
-              class="px-4 py-3 rounded-xl border transition-all {settingsState.selectedImageType === 'jre' ? 'bg-indigo-500/20 border-indigo-500/50 text-white' : 'bg-black/20 border-white/10 text-white/60 hover:bg-white/5'}"
-            >
-              JRE
-            </button>
+        <!-- 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.selectedImageType = "jdk"}
-              class="px-4 py-3 rounded-xl border transition-all {settingsState.selectedImageType === 'jdk' ? 'bg-indigo-500/20 border-indigo-500/50 text-white' : 'bg-black/20 border-white/10 text-white/60 hover:bg-white/5'}"
+              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"
             >
-              JDK
+              Resume All
             </button>
           </div>
-          <p class="text-xs text-white/40 mt-2">
-            JRE: Runtime only (smaller). JDK: Includes development tools.
-          </p>
         </div>
+      {/if}
 
-        <!-- Status -->
-        {#if settingsState.javaDownloadStatus}
-          <div class="bg-black/40 border border-white/10 rounded-xl p-4 text-sm text-white/80">
-            {settingsState.javaDownloadStatus}
-          </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>
 
-        <!-- Actions -->
-        <div class="flex gap-3 pt-2">
-          <button
-            onclick={() => settingsState.closeJavaDownloadModal()}
-            disabled={settingsState.isDownloadingJava}
-            class="flex-1 bg-white/10 hover:bg-white/20 disabled:opacity-50 text-white px-4 py-3 rounded-xl border border-white/5 transition-colors font-medium"
-          >
-            Cancel
-          </button>
-          <button
-            onclick={() => settingsState.downloadJava()}
-            disabled={settingsState.isDownloadingJava}
-            class="flex-1 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white px-4 py-3 rounded-xl transition-colors font-medium"
-          >
-            {settingsState.isDownloadingJava ? "Downloading..." : "Download & Install"}
-          </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>
-- 
cgit v1.2.3-70-g09d2

