feat: Java downloader

This commit is contained in:
LynithDev 2024-12-07 12:03:50 +01:00
parent aa6c3f333b
commit 2554d233f4
6 changed files with 231 additions and 107 deletions

View file

@ -83,10 +83,24 @@ macro_rules! collect_commands {
set_window_style,
get_program_info,
get_featured_packages,
get_zulu_packages,
install_java_from_package,
]
}};
}
#[specta::specta]
#[tauri::command]
pub async fn get_zulu_packages() -> Result<Vec<onelauncher::java::JavaZuluPackage>, String> {
Ok(onelauncher::java::get_zulu_packages().await?)
}
#[specta::specta]
#[tauri::command]
pub async fn install_java_from_package(download: onelauncher::java::JavaZuluPackage) -> Result<std::path::PathBuf, String> {
Ok(onelauncher::java::install_java_from_package(download).await?)
}
#[specta::specta]
#[tauri::command]
pub async fn get_featured_packages() -> Result<Vec<onelauncher::package::content::FeaturedPackage>, String> {

View file

@ -1,15 +1,18 @@
import type { JavaVersion, JavaVersions, Memory, Resolution } from '@onelauncher/client/bindings';
import { ActivityIcon, CpuChip01Icon, Database01Icon, EyeIcon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, VariableIcon, XIcon } from '@untitled-theme/icons-solid';
import type { JavaVersion, JavaVersions, JavaZuluPackage, Memory, Resolution } from '@onelauncher/client/bindings';
import { ActivityIcon, CpuChip01Icon, Database01Icon, Download01Icon, EyeIcon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, VariableIcon, XIcon } from '@untitled-theme/icons-solid';
import { bridge } from '~imports';
import Button from '~ui/components/base/Button';
import Dropdown from '~ui/components/base/Dropdown';
import TextField from '~ui/components/base/TextField';
import Toggle from '~ui/components/base/Toggle';
import Modal, { createModal } from '~ui/components/overlay/Modal';
import Modal, { createModal, type ModalProps } from '~ui/components/overlay/Modal';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import BaseSettingsRow, { type SettingsRowProps } from '~ui/components/SettingsRow';
import Sidebar from '~ui/components/Sidebar';
import { tryResult } from '~ui/hooks/useCommand';
import useSettings from '~ui/hooks/useSettings';
import { asEnvVariables } from '~utils';
import { type Accessor, createSignal, For, onMount, type Setter, Show, splitProps, untrack } from 'solid-js';
import { type Accessor, createMemo, createResource, createSignal, For, onMount, type Setter, Show, splitProps, untrack } from 'solid-js';
function SettingsMinecraft() {
return (
@ -298,38 +301,7 @@ export function JvmSettings(props: {
javaArgs: CreateSetting<string[]>;
envVars: CreateSetting<[string, string][]>;
}) {
const modal = createModal(controller => (
<Modal.Simple
buttons={[
<Button
buttonStyle="secondary"
children="Close"
onClick={controller.hide}
/>,
]}
title="Java Versions"
{...controller}
>
<div class="grid grid-cols-[80px_1fr] items-center gap-y-2">
<For each={Object.entries(props.javaVersions?.get() ?? {}).sort((a, b) => Number.parseFloat(b[1].version) - Number.parseFloat(a[1].version))}>
{([version, meta]) => {
const prettified = version.toLowerCase().replaceAll('_', ' ');
return (
<>
<span class="capitalize">{prettified}</span>
<TextField
onValidSubmit={(value) => {
props.javaVersions?.set({ ...props.javaVersions.get(), [version]: { ...meta, path: value } });
}}
value={meta.path}
/>
</>
);
}}
</For>
</div>
</Modal.Simple>
));
const modal = createModal(controller => <JavaVersionModal {...controller} javaVersions={props.javaVersions} />);
return (
<>
@ -469,3 +441,101 @@ function PageSettings() {
}
export default SettingsMinecraft;
function JavaVersionModal(props: ModalProps & {
javaVersions: CreateSetting<JavaVersions> | undefined;
}) {
const [selectedPackageIndex, setSelectedPackageIndex] = createSignal<number>(-1);
const [zuluPackages] = createResource(async () => {
try {
return await tryResult(bridge.commands.getZuluPackages);
}
catch (e) {
console.error(e);
return [];
}
});
const foundVersions = createMemo(() => {
return zuluPackages()?.map(pkg => pkg.java_version.join('.')) ?? [];
});
const setPackage = (version: number, pkg: JavaVersion) => {
props.javaVersions?.set({
...props.javaVersions?.get(),
[`JAVA_${version}`]: pkg,
});
};
const download = async (pkg: JavaZuluPackage) => {
const path = await tryResult(() => bridge.commands.installJavaFromPackage(pkg));
if (path)
setPackage(pkg.java_version[0]!, {
version: pkg.java_version.join('.'),
path,
arch: '',
});
};
return (
<Modal.Simple
buttons={[
<Button
buttonStyle="secondary"
children="Close"
onClick={props.hide}
/>,
]}
title="Java Versions"
{...props}
>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2">
<h4>Default Java Paths</h4>
<div class="grid grid-cols-[80px_1fr] items-center gap-y-2">
<For each={Object.entries(props.javaVersions?.get() ?? {}).sort((a, b) => Number.parseFloat(b[1].version) - Number.parseFloat(a[1].version))}>
{([version, meta]) => {
const major = version.toLowerCase().replaceAll('_', ' ').replaceAll('java', '');
return (
<>
<span class="capitalize">
Java
{major}
</span>
<TextField
onValidSubmit={(value) => {
setPackage(Number.parseInt(major), { ...meta, path: value });
}}
value={meta.path}
/>
</>
);
}}
</For>
</div>
</div>
<div class="flex flex-col gap-2">
<h4>Download Java</h4>
<div class="w-full flex flex-row gap-2">
<Dropdown class="w-full" onChange={setSelectedPackageIndex}>
<For each={foundVersions()}>
{version => (
<Dropdown.Row>{version}</Dropdown.Row>
)}
</For>
</Dropdown>
<Button
children="Download"
iconLeft={<Download01Icon />}
onClick={() => download(zuluPackages()![selectedPackageIndex()]!)}
/>
</div>
</div>
</div>
</Modal.Simple>
);
}

View file

@ -4,7 +4,7 @@
"version": "1.0.0-alpha.1",
"private": true,
"engineStrict": true,
"packageManager": "pnpm@9.12.2",
"packageManager": "pnpm@9.15.0",
"description": "Next-generation open source Minecraft launcher and tools for interacting with Polyfrost",
"license": "GPL-3.0-only",
"homepage": "https://polyfrost.org",

View file

@ -534,6 +534,26 @@ export const commands = {
else return { status: 'error', error: e as any };
}
},
async getZuluPackages(): Promise<Result<JavaZuluPackage[], string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_zulu_packages') };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async installJavaFromPackage(download: JavaZuluPackage): Promise<Result<string, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('install_java_from_package', { download }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
};
/** user-defined events */
@ -783,6 +803,7 @@ export interface JavaVersion { version: string; arch: string; path: string };
* A `HashMap` of all located and installed available Java versions
*/
export type JavaVersions = { [key in string]: JavaVersion };
export interface JavaZuluPackage { download_url: string; name: string; java_version: number[] };
export interface License { id?: string; name?: string; url?: string | null };
/**
* Available mod loaders to be used for a cluster.

View file

@ -1,7 +1,7 @@
use std::path::PathBuf;
use reqwest::Method;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use crate::proxy::send::{init_ingress, send_ingress};
use crate::utils::http::{fetch_advanced, fetch_json};
@ -23,95 +23,114 @@ pub async fn filter_java_version(java_version: Option<u32>) -> crate::Result<Vec
})
}
#[derive(Deserialize)]
struct JavaPackage {
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize)]
pub struct JavaZuluPackage {
pub download_url: String,
pub name: PathBuf,
pub java_version: Vec<u32>,
}
pub async fn get_zulu_packages() -> crate::Result<Vec<JavaZuluPackage>> {
let state = State::get().await?;
fetch_json::<Vec<JavaZuluPackage>>(
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,
&state.fetch_semaphore,
)
.await
}
// TODO: support more than just zulu ?
#[onelauncher_macros::memory]
pub async fn install_java(java_version: u32) -> crate::Result<PathBuf> {
pub async fn install_java_from_major(java_version: u32) -> crate::Result<PathBuf> {
let packages = get_zulu_packages().await?;
let package = packages
.into_iter()
.find(|p| p.java_version.contains(&java_version))
.ok_or(anyhow::anyhow!(
"Could not find a java package for version {}",
java_version
))?;
install_java_from_package(package).await
}
#[onelauncher_macros::memory]
pub async fn install_java_from_package(download: JavaZuluPackage) -> crate::Result<PathBuf> {
let state = State::get().await?;
let ingress = init_ingress(
crate::IngressType::DownloadJava {
version: java_version,
version: *download.java_version.get(0).unwrap_or(&0),
},
100.0,
"downloading java version",
)
.await?;
send_ingress(&ingress, 0.0, Some("fetching java api")).await?;
let packages = fetch_json::<Vec<JavaPackage>>(Method::GET, &format!(
"https://api.azul.com/metadata/v1/zulu/packages?arch={}&java_version={}&os={}&archive_type=zip&javafx_bundled=false&java_package_type=jre&page_size=1",
std::env::consts::ARCH, java_version, std::env::consts::OS,
), None, None, &state.fetch_semaphore).await?;
send_ingress(&ingress, 10.0, Some("downloading java version")).await?;
send_ingress(&ingress, 0.0, Some("downloading java version")).await?;
if let Some(download) = packages.first() {
let file = fetch_advanced(
Method::GET,
&download.download_url,
None,
None,
None,
Some((&ingress, 80.0)),
&state.fetch_semaphore,
)
.await?;
let file = fetch_advanced(
Method::GET,
&download.download_url,
None,
None,
None,
Some((&ingress, 80.0)),
&state.fetch_semaphore,
)
.await?;
let path = state.directories.java_dir().await;
let mut archive =
zip::ZipArchive::new(std::io::Cursor::new(file)).map_err(IOError::from_zip)?;
let path = state.directories.java_dir().await;
let mut archive =
zip::ZipArchive::new(std::io::Cursor::new(file)).map_err(IOError::from_zip)?;
if let Some(file) = archive.file_names().next() {
if let Some(dir) = file.split('/').next() {
let path = path.join(dir);
if path.exists() {
io::remove_dir_all(path).await?;
}
if let Some(file) = archive.file_names().next() {
if let Some(dir) = file.split('/').next() {
let path = path.join(dir);
if path.exists() {
io::remove_dir_all(path).await?;
}
}
send_ingress(&ingress, 0.0, Some("extracing java binary")).await?;
archive.extract(&path).map_err(IOError::from_zip)?;
send_ingress(&ingress, 10.0, Some("extracted java binary")).await?;
let mut base_path = path.join(
download
.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)
} else {
Err(anyhow::anyhow!(
"no java version found for version {}, os {}, and arch {}",
java_version,
std::env::consts::OS,
std::env::consts::ARCH,
)
.into())
}
send_ingress(&ingress, 0.0, Some("extracing java binary")).await?;
archive.extract(&path).map_err(IOError::from_zip)?;
send_ingress(&ingress, 10.0, Some("extracted java binary")).await?;
let mut base_path = path.join(
download
.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)
}
send_ingress(&ingress, 100.0, Some("installed java binary")).await?;
Ok(base_path)
}
pub async fn check_java(path: PathBuf) -> crate::Result<Option<JavaVersion>> {
@ -125,4 +144,4 @@ pub async fn test_java(path: PathBuf, major: u32) -> crate::Result<bool> {
let (maj, _) = get_java_version(&jvm.version)?;
Ok(maj == major)
}
}

View file

@ -112,7 +112,7 @@ pub async fn install_minecraft(
if let Some(java_version) = java_version_from_cluster(cluster, &version_info).await? {
(std::path::PathBuf::from(java_version.path), false)
} else {
(crate::api::java::install_java(key).await?, true)
(crate::api::java::install_java_from_major(key).await?, true)
};
let java_version = crate::api::java::check_java(java_version.clone())