From 1ab5bd54906984658c91618eceffa5a3626c9377 Mon Sep 17 00:00:00 2001 From: emirsassan Date: Tue, 1 Jul 2025 20:37:56 +0300 Subject: [PATCH] feat: added java downloading --- .../frontend/src/routes/app/cluster/index.tsx | 3 +- .../frontend/src/routes/app/dev.tsx | 10 +- .../src/routes/app/settings/index.tsx | 30 +-- .../src/routes/app/settings/minecraft.tsx | 2 +- packages/core/src/api/cluster/dao.rs | 70 ++++- packages/core/src/api/java/mod.rs | 242 +++++++++++++----- packages/core/src/api/tauri/commands.rs | 4 +- 7 files changed, 265 insertions(+), 96 deletions(-) diff --git a/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx b/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx index 0462136f..b5d1debe 100644 --- a/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx +++ b/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx @@ -168,7 +168,7 @@ function Banner({ const image = () => { const url = cluster?.icon_url; - if (url === undefined || url === null) + if (!url) return DefaultInstancePhoto; return convertFileSrc(url); @@ -188,6 +188,7 @@ function Banner({ {/* */} { (e.target as HTMLImageElement).src = DefaultInstancePhoto; }} diff --git a/apps/onelauncher/frontend/src/routes/app/dev.tsx b/apps/onelauncher/frontend/src/routes/app/dev.tsx index f86582e6..c7045a91 100644 --- a/apps/onelauncher/frontend/src/routes/app/dev.tsx +++ b/apps/onelauncher/frontend/src/routes/app/dev.tsx @@ -54,10 +54,12 @@ function RouteComponent() { - - Slmalr - Slmalr 2 - +
+ + Slmalr + Slmalr 2 + +
diff --git a/apps/onelauncher/frontend/src/routes/app/settings/index.tsx b/apps/onelauncher/frontend/src/routes/app/settings/index.tsx index 4e41866e..18b5f368 100644 --- a/apps/onelauncher/frontend/src/routes/app/settings/index.tsx +++ b/apps/onelauncher/frontend/src/routes/app/settings/index.tsx @@ -1,13 +1,11 @@ import DiscordIcon from '@/assets/logos/discord.svg'; import SettingsRow from '@/components/SettingsRow'; -import usePopState from '@/hooks/usePopState'; -import { bindings } from '@/main'; -import { useCommand } from '@onelauncher/common'; +import useSettings from '@/hooks/useSettings'; import { Button, Switch } from '@onelauncher/common/components'; import { createFileRoute } from '@tanstack/react-router'; import { dataDir } from '@tauri-apps/api/path'; import { openPath as open } from '@tauri-apps/plugin-opener'; -import { FolderIcon, LinkExternal01Icon, XIcon } from '@untitled-theme/icons-react'; +import { FolderIcon, LinkExternal01Icon } from '@untitled-theme/icons-react'; import Sidebar from './route'; export const Route = createFileRoute('/app/settings/')({ @@ -15,16 +13,7 @@ export const Route = createFileRoute('/app/settings/')({ }); function RouteComponent() { - const result = useCommand('readSettings', bindings.core.readSettings); - - const save = useCommand('writeSettings', () => bindings.core.writeSettings(result.data!), { - enabled: false, - subscribed: false, - }); - - usePopState(() => { - save.refetch(); - }); + const { settings, createSetting } = useSettings(); const openLauncherDir = async () => { const dir = await dataDir(); @@ -32,6 +21,8 @@ function RouteComponent() { open(`${dir}/OneLauncher`); }; + const [discordRpc, setDiscordRpc] = createSetting('discord_enabled', settings?.discord_enabled); + return (
@@ -43,15 +34,12 @@ function RouteComponent() { title="Discord RPC" > { - if (result.data) - result.data.discord_enabled = val; - }} + defaultSelected={discordRpc} + onChange={val => setDiscordRpc(val)} /> - } title="Hide Close Dialog" @@ -59,7 +47,7 @@ function RouteComponent() { - {/* } title="Error Analytics" diff --git a/apps/onelauncher/frontend/src/routes/app/settings/minecraft.tsx b/apps/onelauncher/frontend/src/routes/app/settings/minecraft.tsx index 7530c688..740880be 100644 --- a/apps/onelauncher/frontend/src/routes/app/settings/minecraft.tsx +++ b/apps/onelauncher/frontend/src/routes/app/settings/minecraft.tsx @@ -105,7 +105,7 @@ export function GameSettings(props: GameSettingProps) { settings.mem_max = Number(e.target.value); }} type="number" - value={settings.mem_max ?? 2048} + value={settings.mem_max?.toString()} />
diff --git a/packages/core/src/api/cluster/dao.rs b/packages/core/src/api/cluster/dao.rs index ad2d7bf7..4672be45 100644 --- a/packages/core/src/api/cluster/dao.rs +++ b/packages/core/src/api/cluster/dao.rs @@ -3,7 +3,7 @@ use std::path::Path; use onelauncher_entity::{cluster_groups, clusters, icon::Icon, loader::GameLoader}; use sea_orm::{ActiveValue::Set, IntoActiveModel, prelude::*}; -use crate::{error::{DaoError, LauncherResult}, store::State}; +use crate::{error::{DaoError, LauncherResult}, store::State, utils::io}; pub type ClusterId = i64; @@ -142,3 +142,71 @@ pub async fn get_clusters_grouped() -> LauncherResult LauncherResult<()> { + let icon_path = Path::new(icon_path); + + if !icon_path.exists() { + return Err(anyhow::anyhow!("Icon file does not exist: {}", icon_path.display()).into()); + } + + let image_bytes = io::read(icon_path).await?; + + let hash = crate::utils::crypto::HashAlgorithm::Sha1.hash(&image_bytes).await?; + + let cache_dir = crate::store::Dirs::get_caches_dir().await?.join("icons"); + io::create_dir_all(&cache_dir).await?; + + let cached_file_path = cache_dir.join(format!("{}.png", hash)); + io::write(&cached_file_path, &image_bytes).await?; + + let file_icon = Icon::try_from_path(&cached_file_path) + .ok_or_else(|| anyhow::anyhow!("Failed to create file icon from path: {}", cached_file_path.display()))?; + + update_cluster(cluster, |mut active_model: clusters::ActiveModel| async move { + active_model.icon_url = Set(Some(file_icon)); + Ok(active_model) + }).await?; + + Ok(()) +} + +/// Caches an icon from a file path and updates the cluster by ID to use the cached icon. +/// This function copies the image to the icon cache directory and sets the cluster's icon_url +/// to reference the cached version. +pub async fn set_icon_by_id( + cluster_id: ClusterId, + icon_path: &str, +) -> LauncherResult { + let icon_path = Path::new(icon_path); + + if !icon_path.exists() { + return Err(anyhow::anyhow!("Icon file does not exist: {}", icon_path.display()).into()); + } + + let image_bytes = io::read(icon_path).await?; + + let hash = crate::utils::crypto::HashAlgorithm::Sha1.hash(&image_bytes).await?; + + let cache_dir = crate::store::Dirs::get_caches_dir().await?.join("icons"); + io::create_dir_all(&cache_dir).await?; + + let cached_file_path = cache_dir.join(format!("{}.png", hash)); + io::write(&cached_file_path, &image_bytes).await?; + + let file_icon = Icon::try_from_path(&cached_file_path) + .ok_or_else(|| anyhow::anyhow!("Failed to create file icon from path: {}", cached_file_path.display()))?; + + let updated_cluster = update_cluster_by_id(cluster_id, |mut active_model: clusters::ActiveModel| async move { + active_model.icon_url = Set(Some(file_icon)); + Ok(active_model) + }).await?; + + Ok(updated_cluster) +} diff --git a/packages/core/src/api/java/mod.rs b/packages/core/src/api/java/mod.rs index 1d284158..5655d13e 100644 --- a/packages/core/src/api/java/mod.rs +++ b/packages/core/src/api/java/mod.rs @@ -1,14 +1,17 @@ use std::collections::HashMap; -use std::path::PathBuf; +use std::path::{self, PathBuf}; +use async_zip::base::read::mem::ZipFileReader; use interpulse::api::minecraft::VersionInfo; use onelauncher_entity::{java_versions, setting_profiles}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use crate::api::ingress::{init_ingress, init_ingress_opt, send_ingress}; use crate::error::LauncherResult; -use crate::store::ingress::IngressType; -use crate::utils::io; +use crate::store::ingress::{IngressType, SubIngress}; +use crate::store::{Dirs, State}; +use crate::utils::http::{fetch_advanced, fetch_json}; +use crate::utils::io::{self, IOError}; pub mod dao; @@ -16,9 +19,18 @@ pub mod dao; #[derive(Debug, thiserror::Error)] pub enum JavaError { #[error("failed to parse version '{0}'")] - ParseVersion(String, #[source] #[skip] std::num::ParseIntError), + ParseVersion( + String, + #[source] + #[skip] + std::num::ParseIntError, + ), #[error("failed to execute java command")] - Execute(#[from] #[skip] std::io::Error), + Execute( + #[from] + #[skip] + std::io::Error, + ), #[error("no java installations found")] MissingJava, } @@ -52,6 +64,14 @@ pub struct JavaInfo { pub java_vendor: String, } +#[onelauncher_macro::specta] +#[derive(Serialize, Deserialize)] +pub struct JavaPackage { + pub download_url: String, + pub name: PathBuf, + pub jav_version: Vec, +} + const JAVA_INFO_CLASS: &[u8] = include_bytes!("../../../assets/java/JavaInfo.class"); /// Accepts a path to a java executable and returns the [`JavaInfo`] @@ -124,9 +144,10 @@ pub async fn get_recommended_java( if let Some(profile) = profile && !profile.is_global() && let Some(java_id) = profile.java_id - && let Ok(Some(java)) = dao::get_java_by_id(java_id).await { - return Ok(Some(java)); - } + && let Ok(Some(java)) = dao::get_java_by_id(java_id).await + { + return Ok(Some(java)); + } // Check if the version info has a suggested version let Some(supported_ver) = &info.java_version else { @@ -136,15 +157,8 @@ pub async fn get_recommended_java( dao::get_latest_java_by_major(supported_ver.major_version).await } -pub async fn prepare_java( - major: u32, -) -> LauncherResult { - let id = init_ingress( - IngressType::JavaPrepare, - "preparing java", - 100.0, - ) - .await?; +pub async fn prepare_java(major: u32) -> LauncherResult { + let id = init_ingress(IngressType::JavaPrepare, "preparing java", 100.0).await?; let java = dao::get_latest_java_by_major(major).await?; if let Some(java) = &java { @@ -206,57 +220,56 @@ pub async fn locate_java() -> LauncherResult> { #[cfg(target_os = "windows")] fn internal_locate_java() -> Vec { - let mut java_homes = Vec::new(); - - // epic common paths - let common_paths = [ - r"C:\Program Files\Java", - r"C:\Program Files (x86)\Java", - r"C:\Program Files\OpenJDK", - r"C:\Program Files\Eclipse Adoptium", - r"C:\Program Files\Zulu", - r"C:\Program Files\Amazon Corretto", - ]; - - for base_path in common_paths { - if let Ok(entries) = std::fs::read_dir(base_path) { - for entry in entries.flatten() { - if entry.file_type().map_or(false, |ft| ft.is_dir()) { - let java_exe = entry.path().join("bin").join("java.exe"); + let mut java_homes = Vec::new(); + + // epic common paths + let common_paths = [ + r"C:\Program Files\Java", + r"C:\Program Files (x86)\Java", + r"C:\Program Files\OpenJDK", + r"C:\Program Files\Eclipse Adoptium", + r"C:\Program Files\Zulu", + r"C:\Program Files\Amazon Corretto", + ]; + + for base_path in common_paths { + if let Ok(entries) = std::fs::read_dir(base_path) { + for entry in entries.flatten() { + if entry.file_type().map_or(false, |ft| ft.is_dir()) { + let java_exe = entry.path().join("bin").join("java.exe"); if java_exe.exists() { - java_homes.push(java_exe); - } - } - } - } - } - - // env vars - if let Ok(java_home) = std::env::var("JAVA_HOME") { - let path_buf = PathBuf::from(java_home); - if path_buf.join("bin").join("java.exe").exists() { - java_homes.push(path_buf); - } - } + java_homes.push(java_exe); + } + } + } + } + } + + // env vars + if let Ok(java_home) = std::env::var("JAVA_HOME") { + let path_buf = PathBuf::from(java_home); + if path_buf.join("bin").join("java.exe").exists() { + java_homes.push(path_buf); + } + } if let Ok(path) = std::env::var("PATH") { - for path_entry in path.split(';') { - let java_exe = PathBuf::from(path_entry).join("java.exe"); - if java_exe.exists() { - - if let Some(bin_dir) = java_exe.parent() { - if let Some(java_home) = bin_dir.parent() { - let java_home_path = java_home.to_path_buf(); - if !java_homes.contains(&java_home_path) { - java_homes.push(java_home_path); - } - } - } - } - } - } - - java_homes + for path_entry in path.split(';') { + let java_exe = PathBuf::from(path_entry).join("java.exe"); + if java_exe.exists() { + if let Some(bin_dir) = java_exe.parent() { + if let Some(java_home) = bin_dir.parent() { + let java_home_path = java_home.to_path_buf(); + if !java_homes.contains(&java_home_path) { + java_homes.push(java_home_path); + } + } + } + } + } + } + + java_homes } #[cfg(target_os = "macos")] @@ -314,3 +327,100 @@ fn internal_locate_java() -> Vec { found } + +pub async fn get_zulu_packages() -> LauncherResult> { + fetch_json::>( + reqwest::Method::GET, + format!( + "https://api.azul.com/metadata/v1/zulu/packages/?os={}&arch={}&archive_type=zip&java_package_type=jre&javafx_bundled=false&latest=true&release_status=ga&availability_types=CA&certifications=tck&page=1&page_size=100", + std::env::consts::OS, + std::env::consts::ARCH + ).as_str(), + None, + None, + ) + .await +} + +pub async fn install_java_from_major(version: u32) -> LauncherResult { + let packages = get_zulu_packages().await?; + + let package = packages.into_iter() + .find(|p| p.jav_version.contains(&version)) + .ok_or_else(|| anyhow::anyhow!( + "Could not find a java package for version {}", + version + ))?; + + install_java_package(package).await +} + +pub async fn install_java_package(package: JavaPackage) -> LauncherResult { + const INGRESS_TOTAL: f64 = 100.0; + const INGRESS_TASKS: f64 = 4.0; + const INGRESS_STEP: f64 = INGRESS_TOTAL / INGRESS_TASKS; + + let version = package.download_url; + + let ingress_id = + init_ingress(IngressType::JavaPrepare, "preparing java", INGRESS_TOTAL).await?; + + let file = fetch_advanced( + reqwest::Method::GET, + &version, + None, + None, + None, + Some(&SubIngress::new(&ingress_id, INGRESS_STEP)), + ) + .await?; + + let java_dir = Dirs::get_java_dir().await?; + let archive = ZipFileReader::new(file.to_vec()) + .await + .map_err(IOError::AsyncZipError)?; + + if let Some(entry) = archive.file().entries().first() { + if let Some(dir) = entry + .filename() + .clone() + .into_string() + .unwrap() + .split('/') + .next() + { + let path = java_dir.join(dir); + if path.exists() { + io::remove_dir_all(&path).await?; + } + } + } + + io::unzip_bytes(archive.data().to_vec(), &java_dir).await?; + + let mut base_path = java_dir.join( + package + .name + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_string(), + ); + + #[cfg(target_os = "macos")] + { + base_path = base_path + .join(format!("zulu-{java_version}.jre")) + .join("Contents") + .join("Home") + .join("bin") + .join("java"); + } + + #[cfg(not(target_os = "macos"))] + { + base_path = base_path.join("bin").join(crate::constants::JAVA_BIN); + } + + Ok(base_path) +} diff --git a/packages/core/src/api/tauri/commands.rs b/packages/core/src/api/tauri/commands.rs index e0440266..cece87ce 100644 --- a/packages/core/src/api/tauri/commands.rs +++ b/packages/core/src/api/tauri/commands.rs @@ -151,8 +151,8 @@ impl TauriLauncherApi for TauriLauncherApiImpl { } // ok i know this is so wrong but im not a rust guy - if let icon_url = request.icon_url { - active_model.icon_url = Set(icon_url) + if let Some(icon_url) = request.icon_url { + api::cluster::dao::set_icon_by_id(id, &icon_url).await?; } Ok(active_model)