feat: added java downloading
This commit is contained in:
parent
f025673d9d
commit
1ab5bd5490
7 changed files with 265 additions and 96 deletions
|
|
@ -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({
|
|||
|
||||
{/* <ClusterCover class="h-full w-full object-cover" cluster={props.cluster} override={props.newCover()} /> */}
|
||||
<img
|
||||
className="h-full w-full"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = DefaultInstancePhoto;
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -54,10 +54,12 @@ function RouteComponent() {
|
|||
</Modal>
|
||||
</Modal.Trigger>
|
||||
|
||||
<Dropdown label="Select a version">
|
||||
<Dropdown.Item>Slmalr</Dropdown.Item>
|
||||
<Dropdown.Item>Slmalr 2</Dropdown.Item>
|
||||
</Dropdown>
|
||||
<div className="max-w-96">
|
||||
<Dropdown label="Select a version">
|
||||
<Dropdown.Item>Slmalr</Dropdown.Item>
|
||||
<Dropdown.Item>Slmalr 2</Dropdown.Item>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button onClick={addSimpleNotif}>Add Simple Notification</Button>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Sidebar.Page>
|
||||
<div className="h-full">
|
||||
|
|
@ -43,15 +34,12 @@ function RouteComponent() {
|
|||
title="Discord RPC"
|
||||
>
|
||||
<Switch
|
||||
defaultSelected={result.data?.discord_enabled}
|
||||
onChange={(val) => {
|
||||
if (result.data)
|
||||
result.data.discord_enabled = val;
|
||||
}}
|
||||
defaultSelected={discordRpc}
|
||||
onChange={val => setDiscordRpc(val)}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
{/* <SettingsRow
|
||||
description="Hide the confirmation dialog when closing the launcher."
|
||||
icon={<XIcon />}
|
||||
title="Hide Close Dialog"
|
||||
|
|
@ -59,7 +47,7 @@ function RouteComponent() {
|
|||
<Switch />
|
||||
</SettingsRow>
|
||||
|
||||
{/* <SettingsRow
|
||||
<SettingsRow
|
||||
description="Sends errors and crash logs using Sentry to help developers fix issues. (// TODO)"
|
||||
icon={<AlertSquareIcon />}
|
||||
title="Error Analytics"
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<Vec<(cluster_groups::Model
|
|||
|
||||
Ok(grouped)
|
||||
}
|
||||
|
||||
/// Caches an icon from a file path and updates the cluster 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_cluster_icon_by_path(
|
||||
cluster: &mut clusters::Model,
|
||||
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()))?;
|
||||
|
||||
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<clusters::Model> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<u32>,
|
||||
}
|
||||
|
||||
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<java_versions::Model> {
|
||||
let id = init_ingress(
|
||||
IngressType::JavaPrepare,
|
||||
"preparing java",
|
||||
100.0,
|
||||
)
|
||||
.await?;
|
||||
pub async fn prepare_java(major: u32) -> LauncherResult<java_versions::Model> {
|
||||
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<HashMap<PathBuf, JavaInfo>> {
|
|||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn internal_locate_java() -> Vec<PathBuf> {
|
||||
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<PathBuf> {
|
|||
|
||||
found
|
||||
}
|
||||
|
||||
pub async fn get_zulu_packages() -> LauncherResult<Vec<JavaPackage>> {
|
||||
fetch_json::<Vec<JavaPackage>>(
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue