aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--src-tauri/Cargo.toml10
-rw-r--r--src-tauri/src/core/downloader.rs44
-rw-r--r--src-tauri/src/core/java.rs419
-rw-r--r--src-tauri/src/main.rs45
-rw-r--r--src-tauri/src/utils/zip.rs75
-rw-r--r--src-tauri/tauri.conf.json4
-rw-r--r--ui/pnpm-lock.yaml130
-rw-r--r--ui/src/App.svelte168
8 files changed, 869 insertions, 26 deletions
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 42fc159..860a862 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -8,10 +8,10 @@ license = "MIT"
repository = "https://github.com/HsiangNianian/DropOut"
[dependencies]
-serde = { version = "1.0", features = ["derive"] }
-toml = "0.5"
+serde = { version = "1.0", features = ["derive"] }
+toml = "0.5"
log = "0.4"
-env_logger = "0.9"
+env_logger = "0.9"
tokio = { version = "1.49.0", features = ["full"] }
reqwest = { version = "0.13.1", features = ["json", "blocking"] }
serde_json = "1.0.149"
@@ -20,8 +20,12 @@ tauri-plugin-shell = "2.3"
uuid = { version = "1.10.0", features = ["v3", "v4", "serde"] }
futures = "0.3"
sha1 = "0.10"
+sha2 = "0.10"
hex = "0.4"
zip = "2.2.2"
+flate2 = "1.0"
+tar = "0.4"
+dirs = "5.0"
serde_urlencoded = "0.7.1"
[build-dependencies]
diff --git a/src-tauri/src/core/downloader.rs b/src-tauri/src/core/downloader.rs
index 5f6ec80..09101c9 100644
--- a/src-tauri/src/core/downloader.rs
+++ b/src-tauri/src/core/downloader.rs
@@ -1,5 +1,6 @@
use futures::StreamExt;
use serde::{Deserialize, Serialize};
+use sha1::Digest as Sha1Digest;
use std::path::PathBuf;
use std::sync::Arc;
use tauri::{Emitter, Window};
@@ -10,7 +11,10 @@ use tokio::sync::Semaphore;
pub struct DownloadTask {
pub url: String,
pub path: PathBuf,
+ #[serde(default)]
pub sha1: Option<String>,
+ #[serde(default)]
+ pub sha256: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -21,6 +25,32 @@ pub struct ProgressEvent {
pub status: String, // "Downloading", "Verifying", "Finished", "Error"
}
+/// calculate SHA256 hash of data
+pub fn compute_sha256(data: &[u8]) -> String {
+ let mut hasher = sha2::Sha256::new();
+ hasher.update(data);
+ hex::encode(hasher.finalize())
+}
+
+/// calculate SHA1 hash of data
+pub fn compute_sha1(data: &[u8]) -> String {
+ let mut hasher = sha1::Sha1::new();
+ hasher.update(data);
+ hex::encode(hasher.finalize())
+}
+
+/// verify file checksum, prefer SHA256, fallback to SHA1
+pub fn verify_checksum(data: &[u8], sha256: Option<&str>, sha1: Option<&str>) -> bool {
+ if let Some(expected) = sha256 {
+ return compute_sha256(data) == expected;
+ }
+ if let Some(expected) = sha1 {
+ return compute_sha1(data) == expected;
+ }
+ // No checksum provided, default to true
+ true
+}
+
pub async fn download_files(window: Window, tasks: Vec<DownloadTask>) -> Result<(), String> {
let client = reqwest::Client::new();
let semaphore = Arc::new(Semaphore::new(10)); // Max 10 concurrent downloads
@@ -37,7 +67,7 @@ pub async fn download_files(window: Window, tasks: Vec<DownloadTask>) -> Result<
let _permit = semaphore.acquire().await.unwrap();
let file_name = task.path.file_name().unwrap().to_string_lossy().to_string();
- // 1. Check if file exists and verify SHA1
+ // 1. Check if file exists and verify checksum
if task.path.exists() {
let _ = window.emit(
"download-progress",
@@ -49,13 +79,13 @@ pub async fn download_files(window: Window, tasks: Vec<DownloadTask>) -> Result<
},
);
- if let Some(expected_sha1) = &task.sha1 {
+ if task.sha256.is_some() || task.sha1.is_some() {
if let Ok(data) = tokio::fs::read(&task.path).await {
- let mut hasher = sha1::Sha1::new();
- use sha1::Digest;
- hasher.update(&data);
- let result = hex::encode(hasher.finalize());
- if &result == expected_sha1 {
+ if verify_checksum(
+ &data,
+ task.sha256.as_deref(),
+ task.sha1.as_deref(),
+ ) {
// Already valid
let _ = window.emit(
"download-progress",
diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs
index e0962fa..7ef6998 100644
--- a/src-tauri/src/core/java.rs
+++ b/src-tauri/src/core/java.rs
@@ -2,6 +2,9 @@ use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::process::Command;
+use crate::core::downloader;
+use crate::utils::zip;
+
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JavaInstallation {
pub path: String,
@@ -9,6 +12,342 @@ pub struct JavaInstallation {
pub is_64bit: bool,
}
+/// Java image type: JRE or JDK
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum ImageType {
+ Jre,
+ Jdk,
+}
+
+impl Default for ImageType {
+ fn default() -> Self {
+ Self::Jre
+ }
+}
+
+impl std::fmt::Display for ImageType {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Jre => write!(f, "jre"),
+ Self::Jdk => write!(f, "jdk"),
+ }
+ }
+}
+
+/// Adoptium `/v3/assets/latest/{version}/hotspot` API response structures
+#[derive(Debug, Clone, Deserialize)]
+pub struct AdoptiumAsset {
+ pub binary: AdoptiumBinary,
+ pub release_name: String,
+ pub version: AdoptiumVersionData,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct AdoptiumBinary {
+ pub os: String,
+ pub architecture: String,
+ pub image_type: String,
+ pub package: AdoptiumPackage,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct AdoptiumPackage {
+ pub name: String,
+ pub link: String,
+ pub size: u64,
+ pub checksum: Option<String>, // SHA256
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct AdoptiumVersionData {
+ pub major: u32,
+ pub minor: u32,
+ pub security: u32,
+ pub semver: String,
+ pub openjdk_version: String,
+}
+
+/// Java download information from Adoptium
+#[derive(Debug, Clone, Serialize)]
+pub struct JavaDownloadInfo {
+ pub version: String,
+ pub release_name: String,
+ pub download_url: String,
+ pub file_name: String,
+ pub file_size: u64,
+ pub checksum: Option<String>,
+ pub image_type: String,
+}
+
+/// Get the Adoptium OS name for the current platform
+pub fn get_adoptium_os() -> &'static str {
+ #[cfg(target_os = "linux")]
+ {
+ // Check if Alpine Linux (musl libc)
+ if std::path::Path::new("/etc/alpine-release").exists() {
+ return "alpine-linux";
+ }
+ "linux"
+ }
+ #[cfg(target_os = "macos")]
+ {
+ "mac"
+ }
+ #[cfg(target_os = "windows")]
+ {
+ "windows"
+ }
+ #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
+ {
+ "linux" // fallback
+ }
+}
+
+/// Get the Adoptium Architecture name for the current architecture
+pub fn get_adoptium_arch() -> &'static str {
+ #[cfg(target_arch = "x86_64")]
+ {
+ "x64"
+ }
+ #[cfg(target_arch = "aarch64")]
+ {
+ "aarch64"
+ }
+ #[cfg(target_arch = "x86")]
+ {
+ "x86"
+ }
+ #[cfg(target_arch = "arm")]
+ {
+ "arm"
+ }
+ #[cfg(not(any(
+ target_arch = "x86_64",
+ target_arch = "aarch64",
+ target_arch = "x86",
+ target_arch = "arm"
+ )))]
+ {
+ "x64" // fallback
+ }
+}
+
+/// Get the default Java installation directory for DropOut
+pub fn get_java_install_dir() -> PathBuf {
+ dirs::home_dir()
+ .unwrap_or_else(|| PathBuf::from("."))
+ .join(".dropout")
+ .join("java")
+}
+
+/// Get Adoptium API download info for a specific Java version and image type
+///
+/// # Arguments
+/// * `major_version` - Java major version (e.g., 8, 11, 17)
+/// * `image_type` - JRE or JDK
+///
+/// # Returns
+/// * `Ok(JavaDownloadInfo)` - Download information
+/// * `Err(String)` - Error message
+pub async fn fetch_java_release(
+ major_version: u32,
+ image_type: ImageType,
+) -> Result<JavaDownloadInfo, String> {
+ let os = get_adoptium_os();
+ let arch = get_adoptium_arch();
+
+ let url = format!(
+ "https://api.adoptium.net/v3/assets/latest/{}/hotspot?os={}&architecture={}&image_type={}",
+ major_version, os, arch, image_type
+ );
+
+ let client = reqwest::Client::new();
+ let response = client
+ .get(&url)
+ .header("Accept", "application/json")
+ .send()
+ .await
+ .map_err(|e| format!("Network request failed: {}", e))?;
+
+ if !response.status().is_success() {
+ return Err(format!(
+ "Adoptium API returned error: {} - The version/platform might be unavailable",
+ response.status()
+ ));
+ }
+
+ let assets: Vec<AdoptiumAsset> = response
+ .json()
+ .await
+ .map_err(|e| format!("Failed to parse API response: {}", e))?;
+
+ let asset = assets
+ .into_iter()
+ .next()
+ .ok_or_else(|| format!("Java {} {} download not found", major_version, image_type))?;
+
+ Ok(JavaDownloadInfo {
+ version: asset.version.semver.clone(),
+ release_name: asset.release_name,
+ download_url: asset.binary.package.link,
+ file_name: asset.binary.package.name,
+ file_size: asset.binary.package.size,
+ checksum: asset.binary.package.checksum,
+ image_type: asset.binary.image_type,
+ })
+}
+
+/// Fetch available Java versions from Adoptium API
+pub async fn fetch_available_versions() -> Result<Vec<u32>, String> {
+ let url = "https://api.adoptium.net/v3/info/available_releases";
+
+ let response = reqwest::get(url)
+ .await
+ .map_err(|e| format!("Network request failed: {}", e))?;
+
+ #[derive(Deserialize)]
+ struct AvailableReleases {
+ available_releases: Vec<u32>,
+ }
+
+ let releases: AvailableReleases = response
+ .json()
+ .await
+ .map_err(|e| format!("Failed to parse response: {}", e))?;
+
+ Ok(releases.available_releases)
+}
+
+/// Download and install Java
+///
+/// # Arguments
+/// * `major_version` - Java major version (e.g., 8, 11, 17)
+/// * `image_type` - JRE or JDK
+/// * `custom_path` - Optional custom installation path
+///
+/// # Returns
+/// * `Ok(JavaInstallation)` - Information about the successfully installed Java
+pub async fn download_and_install_java(
+ major_version: u32,
+ image_type: ImageType,
+ custom_path: Option<PathBuf>,
+) -> Result<JavaInstallation, String> {
+ // 1. Fetch download information
+ let info = fetch_java_release(major_version, image_type).await?;
+
+ // 2. Prepare installation directory
+ let install_base = custom_path.unwrap_or_else(get_java_install_dir);
+ let version_dir = install_base.join(format!("temurin-{}-{}", major_version, image_type));
+
+ std::fs::create_dir_all(&install_base)
+ .map_err(|e| format!("Failed to create installation directory: {}", e))?;
+
+ // 3. Download the archive
+ let archive_path = install_base.join(&info.file_name);
+
+ // Check if we need to download
+ let need_download = if archive_path.exists() {
+ if let Some(expected_checksum) = &info.checksum {
+ let data = std::fs::read(&archive_path)
+ .map_err(|e| format!("Failed to read downloaded file: {}", e))?;
+ !downloader::verify_checksum(&data, Some(expected_checksum), None)
+ } else {
+ false
+ }
+ } else {
+ true
+ };
+
+ if need_download {
+ let client = reqwest::Client::new();
+ let response = client
+ .get(&info.download_url)
+ .send()
+ .await
+ .map_err(|e| format!("Download failed: {}", e))?;
+
+ let bytes = response
+ .bytes()
+ .await
+ .map_err(|e| format!("Failed to read download content: {}", e))?;
+
+ // Verify downloaded file checksum
+ if let Some(expected) = &info.checksum {
+ if !downloader::verify_checksum(&bytes, Some(expected), None) {
+ return Err("Downloaded file verification failed, the file may be corrupted".to_string());
+ }
+ }
+
+ std::fs::write(&archive_path, &bytes)
+ .map_err(|e| format!("Failed to save downloaded file: {}", e))?;
+ }
+
+ // 4. Extract
+ // If the target directory exists, remove it first
+ if version_dir.exists() {
+ std::fs::remove_dir_all(&version_dir)
+ .map_err(|e| format!("Failed to remove old version directory: {}", e))?;
+ }
+
+ std::fs::create_dir_all(&version_dir)
+ .map_err(|e| format!("Failed to create version directory: {}", e))?;
+
+ let top_level_dir = if info.file_name.ends_with(".tar.gz") || info.file_name.ends_with(".tgz") {
+ zip::extract_tar_gz(&archive_path, &version_dir)?
+ } else if info.file_name.ends_with(".zip") {
+ zip::extract_zip(&archive_path, &version_dir)?;
+ // Find the top-level directory inside the extracted folder
+ find_top_level_dir(&version_dir)?
+ } else {
+ return Err(format!("Unsupported archive format: {}", info.file_name));
+ };
+
+ // 5. Clean up downloaded archive
+ let _ = std::fs::remove_file(&archive_path);
+
+ // 6. Locate java executable
+ // macOS has a different structure: jdk-xxx/Contents/Home/bin/java
+ // Linux/Windows: jdk-xxx/bin/java
+ let java_home = version_dir.join(&top_level_dir);
+ let java_bin = if cfg!(target_os = "macos") {
+ java_home.join("Contents").join("Home").join("bin").join("java")
+ } else if cfg!(windows) {
+ java_home.join("bin").join("java.exe")
+ } else {
+ java_home.join("bin").join("java")
+ };
+
+ if !java_bin.exists() {
+ return Err(format!(
+ "Installation completed but Java executable not found: {}",
+ java_bin.display()
+ ));
+ }
+
+ // 7. Verify installation
+ let installation = check_java_installation(&java_bin)
+ .ok_or_else(|| "Fail to verify Java installation".to_string())?;
+
+ Ok(installation)
+}
+
+/// Find the top-level directory inside the extracted folder
+fn find_top_level_dir(extract_dir: &PathBuf) -> Result<String, String> {
+ let entries: Vec<_> = std::fs::read_dir(extract_dir)
+ .map_err(|e| format!("Failed to read directory: {}", e))?
+ .filter_map(|e| e.ok())
+ .filter(|e| e.path().is_dir())
+ .collect();
+
+ if entries.len() == 1 {
+ Ok(entries[0].file_name().to_string_lossy().to_string())
+ } else {
+ // No single top-level directory, return empty string
+ Ok(String::new())
+ }
+}
+
/// Detect Java installations on the system
pub fn detect_java_installations() -> Vec<JavaInstallation> {
let mut installations = Vec::new();
@@ -252,3 +591,83 @@ pub fn get_recommended_java(required_major_version: Option<u64>) -> Option<JavaI
installations.into_iter().next()
}
}
+
+/// Detect all installed Java versions (including system installations and DropOut downloads)
+pub fn detect_all_java_installations() -> Vec<JavaInstallation> {
+ let mut installations = detect_java_installations();
+
+ // Add DropOut downloaded Java versions
+ let dropout_java_dir = get_java_install_dir();
+ if dropout_java_dir.exists() {
+ if let Ok(entries) = std::fs::read_dir(&dropout_java_dir) {
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if path.is_dir() {
+ // Find the java executable in this directory
+ let java_bin = find_java_executable(&path);
+ if let Some(java_path) = java_bin {
+ if let Some(java) = check_java_installation(&java_path) {
+ if !installations.iter().any(|j| j.path == java.path) {
+ installations.push(java);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Sort by version
+ installations.sort_by(|a, b| {
+ let v_a = parse_java_version(&a.version);
+ let v_b = parse_java_version(&b.version);
+ v_b.cmp(&v_a)
+ });
+
+ installations
+}
+
+/// Recursively find the java executable in a directory
+fn find_java_executable(dir: &PathBuf) -> Option<PathBuf> {
+ let bin_name = if cfg!(windows) { "java.exe" } else { "java" };
+
+ // Directly look in the bin directory
+ let direct_bin = dir.join("bin").join(bin_name);
+ if direct_bin.exists() {
+ return Some(direct_bin);
+ }
+
+ // macOS: Contents/Home/bin/java
+ #[cfg(target_os = "macos")]
+ {
+ let macos_bin = dir.join("Contents").join("Home").join("bin").join(bin_name);
+ if macos_bin.exists() {
+ return Some(macos_bin);
+ }
+ }
+
+ // Look in subdirectories (handle nested directories after Adoptium extraction)
+ if let Ok(entries) = std::fs::read_dir(dir) {
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if path.is_dir() {
+ // Try direct bin path
+ let nested_bin = path.join("bin").join(bin_name);
+ if nested_bin.exists() {
+ return Some(nested_bin);
+ }
+
+ // macOS: nested/Contents/Home/bin/java
+ #[cfg(target_os = "macos")]
+ {
+ let macos_nested = path.join("Contents").join("Home").join("bin").join(bin_name);
+ if macos_nested.exists() {
+ return Some(macos_nested);
+ }
+ }
+ }
+ }
+ }
+
+ None
+}
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index d7ae9a4..4c3f689 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -117,6 +117,7 @@ async fn start_game(
url: client_jar.url,
path: client_path.clone(),
sha1: Some(client_jar.sha1),
+ sha256: None,
});
// --- Libraries ---
@@ -141,6 +142,7 @@ async fn start_game(
url: artifact.url.clone(),
path: lib_path,
sha1: Some(artifact.sha1.clone()),
+ sha256: None,
});
}
@@ -174,6 +176,7 @@ async fn start_game(
url: native_artifact.url,
path: native_path.clone(),
sha1: Some(native_artifact.sha1),
+ sha256: None,
});
native_libs_paths.push(native_path);
@@ -253,6 +256,7 @@ async fn start_game(
url,
path,
sha1: Some(hash),
+ sha256: None,
});
}
@@ -741,7 +745,7 @@ async fn refresh_account(
/// Detect Java installations on the system
#[tauri::command]
async fn detect_java() -> Result<Vec<core::java::JavaInstallation>, String> {
- Ok(core::java::detect_java_installations())
+ Ok(core::java::detect_all_java_installations())
}
/// Get recommended Java for a specific Minecraft version
@@ -752,6 +756,40 @@ async fn get_recommended_java(
Ok(core::java::get_recommended_java(required_major_version))
}
+/// Get Adoptium Java download info
+#[tauri::command]
+async fn fetch_adoptium_java(
+ major_version: u32,
+ image_type: String,
+) -> Result<core::java::JavaDownloadInfo, String> {
+ let img_type = match image_type.to_lowercase().as_str() {
+ "jdk" => core::java::ImageType::Jdk,
+ _ => core::java::ImageType::Jre,
+ };
+ core::java::fetch_java_release(major_version, img_type).await
+}
+
+/// Download and install Adoptium Java
+#[tauri::command]
+async fn download_adoptium_java(
+ major_version: u32,
+ image_type: String,
+ custom_path: Option<String>,
+) -> Result<core::java::JavaInstallation, String> {
+ let img_type = match image_type.to_lowercase().as_str() {
+ "jdk" => core::java::ImageType::Jdk,
+ _ => core::java::ImageType::Jre,
+ };
+ let path = custom_path.map(std::path::PathBuf::from);
+ core::java::download_and_install_java(major_version, img_type, path).await
+}
+
+/// Get available Adoptium Java versions
+#[tauri::command]
+async fn fetch_available_java_versions() -> Result<Vec<u32>, String> {
+ core::java::fetch_available_versions().await
+}
+
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
@@ -793,7 +831,10 @@ fn main() {
complete_microsoft_login,
refresh_account,
detect_java,
- get_recommended_java
+ get_recommended_java,
+ fetch_adoptium_java,
+ download_adoptium_java,
+ fetch_available_java_versions
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
diff --git a/src-tauri/src/utils/zip.rs b/src-tauri/src/utils/zip.rs
index a03c975..dfe1214 100644
--- a/src-tauri/src/utils/zip.rs
+++ b/src-tauri/src/utils/zip.rs
@@ -1,5 +1,7 @@
+use flate2::read::GzDecoder;
use std::fs;
use std::path::Path;
+use tar::Archive;
pub fn extract_zip(zip_path: &Path, extract_to: &Path) -> Result<(), String> {
let file = fs::File::open(zip_path)
@@ -38,3 +40,76 @@ pub fn extract_zip(zip_path: &Path, extract_to: &Path) -> Result<(), String> {
Ok(())
}
+
+/// Extract a tar.gz archive
+///
+/// Adoptium's tar.gz archives usually contain a top-level directory, such as `jdk-21.0.5+11-jre/`.
+/// This function returns the name of that directory to facilitate locating `bin/java` afterwards.
+pub fn extract_tar_gz(archive_path: &Path, extract_to: &Path) -> Result<String, String> {
+ let file = fs::File::open(archive_path)
+ .map_err(|e| format!("Failed to open tar.gz {}: {}", archive_path.display(), e))?;
+
+ let decoder = GzDecoder::new(file);
+ let mut archive = Archive::new(decoder);
+
+ // Ensure the target directory exists
+ fs::create_dir_all(extract_to)
+ .map_err(|e| format!("Failed to create extract directory: {}", e))?;
+
+ // Track the top-level directory name
+ let mut top_level_dir: Option<String> = None;
+
+ for entry in archive
+ .entries()
+ .map_err(|e| format!("Failed to read tar entries: {}", e))?
+ {
+ let mut entry = entry.map_err(|e| format!("Failed to read tar entry: {}", e))?;
+ let entry_path = entry
+ .path()
+ .map_err(|e| format!("Failed to get entry path: {}", e))?
+ .into_owned();
+
+ // Extract the top-level directory name (the first path component)
+ if top_level_dir.is_none() {
+ if let Some(first_component) = entry_path.components().next() {
+ let component_str = first_component.as_os_str().to_string_lossy().to_string();
+ if !component_str.is_empty() && component_str != "." {
+ top_level_dir = Some(component_str);
+ }
+ }
+ }
+
+ let outpath = extract_to.join(&entry_path);
+
+ if entry.header().entry_type().is_dir() {
+ fs::create_dir_all(&outpath)
+ .map_err(|e| format!("Failed to create directory {}: {}", outpath.display(), e))?;
+ } else {
+ // Ensure parent directory exists
+ if let Some(parent) = outpath.parent() {
+ if !parent.exists() {
+ fs::create_dir_all(parent)
+ .map_err(|e| format!("Failed to create parent dir: {}", e))?;
+ }
+ }
+
+ let mut outfile = fs::File::create(&outpath)
+ .map_err(|e| format!("Failed to create file {}: {}", outpath.display(), e))?;
+
+ std::io::copy(&mut entry, &mut outfile)
+ .map_err(|e| format!("Failed to extract file: {}", e))?;
+
+ // Set executable permissions on Unix systems
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::PermissionsExt;
+ if let Ok(mode) = entry.header().mode() {
+ let permissions = fs::Permissions::from_mode(mode);
+ let _ = fs::set_permissions(&outpath, permissions);
+ }
+ }
+ }
+ }
+
+ top_level_dir.ok_or_else(|| "Archive appears to be empty".to_string())
+}
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index e5fda06..c8703a4 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -3,8 +3,8 @@
"version": "0.1.12",
"identifier": "com.dropout.launcher",
"build": {
- "beforeDevCommand": "pnpm -C ../ui dev",
- "beforeBuildCommand": "pnpm -C ../ui build",
+ "beforeDevCommand": "cd ../ui && pnpm dev",
+ "beforeBuildCommand": "cd ../ui && pnpm build",
"devUrl": "http://localhost:5173",
"frontendDist": "../ui/dist"
},
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index d48c01e..0accf90 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -124,28 +124,24 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.50':
resolution: {integrity: sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.50':
resolution: {integrity: sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.0-beta.50':
resolution: {integrity: sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
- libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.0-beta.50':
resolution: {integrity: sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA==}
@@ -237,28 +233,24 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
@@ -296,6 +288,77 @@ packages:
'@tauri-apps/api@2.9.1':
resolution: {integrity: sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==}
+ '@tauri-apps/cli-darwin-arm64@2.9.6':
+ resolution: {integrity: sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tauri-apps/cli-darwin-x64@2.9.6':
+ resolution: {integrity: sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tauri-apps/cli-linux-arm-gnueabihf@2.9.6':
+ resolution: {integrity: sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tauri-apps/cli-linux-arm64-gnu@2.9.6':
+ resolution: {integrity: sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tauri-apps/cli-linux-arm64-musl@2.9.6':
+ resolution: {integrity: sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tauri-apps/cli-linux-riscv64-gnu@2.9.6':
+ resolution: {integrity: sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==}
+ engines: {node: '>= 10'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@tauri-apps/cli-linux-x64-gnu@2.9.6':
+ resolution: {integrity: sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tauri-apps/cli-linux-x64-musl@2.9.6':
+ resolution: {integrity: sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tauri-apps/cli-win32-arm64-msvc@2.9.6':
+ resolution: {integrity: sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tauri-apps/cli-win32-ia32-msvc@2.9.6':
+ resolution: {integrity: sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg==}
+ engines: {node: '>= 10'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@tauri-apps/cli-win32-x64-msvc@2.9.6':
+ resolution: {integrity: sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tauri-apps/cli@2.9.6':
+ resolution: {integrity: sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw==}
+ engines: {node: '>= 10'}
+ hasBin: true
+
'@tauri-apps/plugin-shell@2.3.4':
resolution: {integrity: sha512-ktsRWf8wHLD17aZEyqE8c5x98eNAuTizR1FSX475zQ4TxaiJnhwksLygQz+AGwckJL5bfEP13nWrlTNQJUpKpA==}
@@ -441,28 +504,24 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
- libc: [glibc]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
- libc: [musl]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
- libc: [glibc]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
- libc: [musl]
lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
@@ -808,6 +867,53 @@ snapshots:
'@tauri-apps/api@2.9.1': {}
+ '@tauri-apps/cli-darwin-arm64@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-darwin-x64@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-linux-arm-gnueabihf@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-linux-arm64-gnu@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-linux-arm64-musl@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-linux-riscv64-gnu@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-linux-x64-gnu@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-linux-x64-musl@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-win32-arm64-msvc@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-win32-ia32-msvc@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-win32-x64-msvc@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli@2.9.6':
+ optionalDependencies:
+ '@tauri-apps/cli-darwin-arm64': 2.9.6
+ '@tauri-apps/cli-darwin-x64': 2.9.6
+ '@tauri-apps/cli-linux-arm-gnueabihf': 2.9.6
+ '@tauri-apps/cli-linux-arm64-gnu': 2.9.6
+ '@tauri-apps/cli-linux-arm64-musl': 2.9.6
+ '@tauri-apps/cli-linux-riscv64-gnu': 2.9.6
+ '@tauri-apps/cli-linux-x64-gnu': 2.9.6
+ '@tauri-apps/cli-linux-x64-musl': 2.9.6
+ '@tauri-apps/cli-win32-arm64-msvc': 2.9.6
+ '@tauri-apps/cli-win32-ia32-msvc': 2.9.6
+ '@tauri-apps/cli-win32-x64-msvc': 2.9.6
+
'@tauri-apps/plugin-shell@2.3.4':
dependencies:
'@tauri-apps/api': 2.9.1
diff --git a/ui/src/App.svelte b/ui/src/App.svelte
index 9b7fe93..02cc173 100644
--- a/ui/src/App.svelte
+++ b/ui/src/App.svelte
@@ -57,6 +57,16 @@
is_64bit: boolean;
}
+ interface JavaDownloadInfo {
+ version: string;
+ release_name: string;
+ download_url: string;
+ file_name: string;
+ file_size: number;
+ checksum: string | null;
+ image_type: string;
+ }
+
let versions: Version[] = [];
let selectedVersion = "";
let currentAccount: Account | null = null;
@@ -70,6 +80,13 @@
let javaInstallations: JavaInstallation[] = [];
let isDetectingJava = false;
+ let availableJavaVersions: number[] = [];
+ let selectedJavaVersion = 21;
+ let selectedImageType: "jre" | "jdk" = "jre";
+ let isDownloadingJava = false;
+ let javaDownloadStatus = "";
+ let showJavaDownloadModal = false;
+
// Login UI State
let isLoginModalOpen = false;
let loginMode: "select" | "offline" | "microsoft" = "select";
@@ -144,6 +161,59 @@
settings.java_path = path;
}
+ async function openJavaDownloadModal() {
+ showJavaDownloadModal = true;
+ javaDownloadStatus = "";
+ try {
+ availableJavaVersions = await invoke("fetch_available_java_versions");
+ // Default selection logic
+ if (availableJavaVersions.includes(21)) {
+ selectedJavaVersion = 21;
+ } else if (availableJavaVersions.includes(17)) {
+ selectedJavaVersion = 17;
+ } else if (availableJavaVersions.length > 0) {
+ selectedJavaVersion = availableJavaVersions[availableJavaVersions.length - 1];
+ }
+ } catch (e) {
+ console.error("Failed to fetch available Java versions:", e);
+ javaDownloadStatus = "Error fetching Java versions: " + e;
+ }
+ }
+
+ function closeJavaDownloadModal() {
+ if (!isDownloadingJava) {
+ showJavaDownloadModal = false;
+ }
+ }
+
+ async function downloadJava() {
+ isDownloadingJava = true;
+ javaDownloadStatus = `Downloading Java ${selectedJavaVersion} ${selectedImageType.toUpperCase()}...`;
+
+ try {
+ const result: JavaInstallation = await invoke("download_adoptium_java", {
+ majorVersion: selectedJavaVersion,
+ imageType: selectedImageType,
+ customPath: null,
+ });
+
+ javaDownloadStatus = `Java ${selectedJavaVersion} installed at ${result.path}`;
+ settings.java_path = result.path;
+
+ await detectJava();
+
+ setTimeout(() => {
+ showJavaDownloadModal = false;
+ status = `Java ${selectedJavaVersion} is ready to use!`;
+ }, 1500);
+ } catch (e) {
+ console.error("Failed to download Java:", e);
+ javaDownloadStatus = "Download failed: " + e;
+ } finally {
+ isDownloadingJava = false;
+ }
+ }
+
// --- Auth Functions ---
function openLoginModal() {
@@ -460,6 +530,12 @@
>
{isDetectingJava ? "Detecting..." : "Auto Detect"}
</button>
+ <button
+ onclick={openJavaDownloadModal}
+ class="bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 rounded transition-colors whitespace-nowrap"
+ >
+ Download Java
+ </button>
</div>
{#if javaInstallations.length > 0}
@@ -782,6 +858,98 @@
</div>
{/if}
+ {#if showJavaDownloadModal}
+ <div
+ class="fixed inset-0 bg-black/70 flex items-center justify-center z-50 backdrop-blur-sm"
+ onclick={closeJavaDownloadModal}
+ >
+ <div
+ class="bg-zinc-900 border border-zinc-700 rounded-xl p-6 w-full max-w-md shadow-2xl"
+ onclick={(e) => e.stopPropagation()}
+ >
+ <div class="flex justify-between items-center mb-6">
+ <h3 class="text-xl font-bold">Download Java (Adoptium)</h3>
+ {#if !isDownloadingJava}
+ <button
+ onclick={closeJavaDownloadModal}
+ class="text-zinc-500 hover:text-white transition text-xl"
+ >
+ ✕
+ </button>
+ {/if}
+ </div>
+ <div class="space-y-4">
+ <!-- Version Selection -->
+ <div>
+ <label class="block text-sm font-bold text-zinc-400 mb-2">Java Version</label>
+ <select
+ bind:value={selectedJavaVersion}
+ disabled={isDownloadingJava}
+ class="w-full bg-zinc-950 border border-zinc-700 rounded p-3 text-white focus:border-indigo-500 outline-none disabled:opacity-50"
+ >
+ {#each availableJavaVersions as ver}
+ <option value={ver}>
+ Java {ver} {ver === 21 ? "(Recommended)" : ver === 17 ? "(LTS)" : ver === 8 ? "(Legacy)" : ""}
+ </option>
+ {/each}
+ </select>
+ <p class="text-xs text-zinc-500 mt-1">
+ MC 1.20.5+ requires Java 21, MC 1.17-1.20.4 requires Java 17, older versions require Java 8
+ </p>
+ </div>
+
+ <!-- Image Type Selection -->
+ <div>
+ <label class="block text-sm font-bold text-zinc-400 mb-2">Type</label>
+ <div class="flex gap-3">
+ <button
+ onclick={() => selectedImageType = "jre"}
+ disabled={isDownloadingJava}
+ class="flex-1 p-3 rounded border transition-colors disabled:opacity-50 {selectedImageType === 'jre' ? 'border-indigo-500 bg-indigo-950/30 text-white' : 'border-zinc-700 bg-zinc-950 text-zinc-400 hover:border-zinc-500'}"
+ >
+ <div class="font-bold">JRE</div>
+ <div class="text-xs opacity-70">runtime environment</div>
+ </button>
+ <button
+ onclick={() => selectedImageType = "jdk"}
+ disabled={isDownloadingJava}
+ class="flex-1 p-3 rounded border transition-colors disabled:opacity-50 {selectedImageType === 'jdk' ? 'border-indigo-500 bg-indigo-950/30 text-white' : 'border-zinc-700 bg-zinc-950 text-zinc-400 hover:border-zinc-500'}"
+ >
+ <div class="font-bold">JDK</div>
+ <div class="text-xs opacity-70">development kit</div>
+ </button>
+ </div>
+ </div>
+
+ <!-- Status -->
+ {#if javaDownloadStatus}
+ <div class="p-3 rounded {javaDownloadStatus.startsWith('✓') ? 'bg-green-950/50 border border-green-700 text-green-400' : javaDownloadStatus.includes('failed') || javaDownloadStatus.includes('Failed') ? 'bg-red-950/50 border border-red-700 text-red-400' : 'bg-zinc-800 border border-zinc-700 text-zinc-300'}">
+ <p class="text-sm">{javaDownloadStatus}</p>
+ </div>
+ {/if}
+
+ <!-- Download Button -->
+ <button
+ onclick={downloadJava}
+ disabled={isDownloadingJava || availableJavaVersions.length === 0}
+ class="w-full bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed text-white p-3 rounded font-bold transition-colors flex items-center justify-center gap-2"
+ >
+ {#if isDownloadingJava}
+ <div class="animate-spin rounded-full h-5 w-5 border-2 border-white/30 border-t-white"></div>
+ Downloading...
+ {:else}
+ Download Java {selectedJavaVersion} {selectedImageType.toUpperCase()}
+ {/if}
+ </button>
+
+ <p class="text-xs text-zinc-500 text-center">
+ Provided by <a href="https://adoptium.net" class="text-indigo-400 hover:underline" onclick={(e) => { e.preventDefault(); openLink("https://adoptium.net"); }}>Eclipse Adoptium</a>
+ </p>
+ </div>
+ </div>
+ </div>
+ {/if}
+
<style>
@keyframes progress {
from {