chore: Start work on JRE downloading

This commit is contained in:
LynithDev 2024-03-17 19:45:34 +01:00
parent 71c98255a1
commit 087e87963e
14 changed files with 199 additions and 246 deletions

View file

@ -5,6 +5,8 @@ use tauri::{AppHandle, Manager, Runtime};
use tauri_plugin_http::reqwest::Client;
use tauri_plugin_shell::ShellExt;
use crate::{constants::CLIENT_ID, utils::http::create_client};
use super::{Account, AuthenticationMethod};
#[tauri::command]
@ -17,8 +19,6 @@ pub async fn login_msa<R: tauri::Runtime>(app: tauri::AppHandle<R>) -> Result<Ac
}
}
const CLIENT_ID: &str = "9419b7ee-1448-4d1b-b52a-550d8f36ab56";
struct MicrosoftAuthenticationMethod;
impl AuthenticationMethod for MicrosoftAuthenticationMethod {
async fn auth<R: Runtime, F>(handle: &AppHandle<R>, stage: F) -> Result<Account, Box<dyn Error>>
@ -47,7 +47,7 @@ impl AuthenticationMethod for MicrosoftAuthenticationMethod {
}
async fn auth_minecraft(xsts_token: String, user_hash: String) -> Result<String, Box<dyn Error>> {
let response = Client::new()
let response = create_client()
.post("https://api.minecraftservices.com/authentication/login_with_xbox")
.json(&json!({
"identityToken": format!("XBL3.0 x={};{}", user_hash, xsts_token)
@ -65,7 +65,7 @@ async fn auth_minecraft(xsts_token: String, user_hash: String) -> Result<String,
}
async fn auth_xsts(token: String) -> Result<String, Box<dyn Error>> {
let response = Client::new()
let response = create_client()
.post("https://xsts.auth.xboxlive.com/xsts/authorize")
.json(&json!({
"Properties": {
@ -102,7 +102,7 @@ async fn auth_xsts(token: String) -> Result<String, Box<dyn Error>> {
}
async fn auth_xbl(code: String) -> Result<(String, String), Box<dyn Error>> {
let response = Client::new()
let response = create_client()
.post("https://user.auth.xboxlive.com/user/authenticate")
.json(&json!({
"Properties": {
@ -131,7 +131,7 @@ async fn auth_xbl(code: String) -> Result<(String, String), Box<dyn Error>> {
}
async fn msa_code_to_token(code: String) -> Result<String, Box<dyn Error>> {
let token = Client::new()
let token = create_client()
.post("https://login.live.com/oauth20_token.srf")
.form(&[
("client_id", CLIENT_ID),

View file

@ -21,6 +21,7 @@ pub struct AccountSkin {
pub variant: String,
}
#[allow(async_fn_in_trait)]
pub trait AuthenticationMethod {
/// Authenticate with a given method. Stage is a function that takes a string and a u8.
/// The string is the message to display to the user, and the u8 is the progress of the authentication.

View file

@ -0,0 +1,5 @@
pub const NAME: &str = "OneLauncher";
pub const VERSION: &str = "0.1.0";
pub const USER_AGENT: &str = "OneLauncher/1.0.0 (polyfrost.org)";
pub const CLIENT_ID: &str = "9419b7ee-1448-4d1b-b52a-550d8f36ab56";

View file

@ -0,0 +1,106 @@
use std::{env, error::Error, fmt::Display, fs::{self, File}, path::PathBuf};
use tauri::{AppHandle, Manager, Runtime};
use tauri_plugin_http::reqwest::Client;
use crate::utils::http;
use super::{manifest::Manifest, JavaVersion};
#[allow(async_fn_in_trait)]
pub trait GameClient {
fn from_manifest(manifest: Manifest) -> Self;
fn get_handle(&self) -> &AppHandle;
fn get_manifest(&self) -> &Manifest;
async fn launch(&self) -> Result<(), Box<dyn Error>>;
async fn install(&self) -> Result<(), Box<dyn Error>>;
async fn download_java(&self) -> Result<PathBuf, JavaDownloadError> {
download_java(self.get_handle(), self.get_manifest().java_version.to_owned()).await
}
}
pub async fn download_java<R: Runtime>(handle: &AppHandle<R>, java_version: JavaVersion) -> Result<PathBuf, JavaDownloadError> {
let config_dir = match handle.path().app_config_dir() {
Ok(dir) => dir,
Err(_) => return Err(JavaDownloadError::UnsupportedOS)
};
let java_dir = config_dir.join("java");
if !java_dir.exists() {
match fs::create_dir_all(&java_dir) {
Ok(_) => (),
Err(_) => return Err(JavaDownloadError::PermissionDenied)
};
}
// let java_version = &self.get_manifest().java_version;
let os = env::consts::OS;
let archive_type = match os {
"windows" => "zip",
"macos" => "tar.gz",
"linux" => "tar.gz",
_ => return Err(JavaDownloadError::UnsupportedOS)
};
let url = format!("
https://api.azul.com/metadata/v1/zulu/packages/\
?java_version={}\
&os={}\
&arch={}\
&archive_type={}\
&java_package_type=jre&javafx_bundled=true&release_status=ga&latest=true",
java_version.to_string(),
os,
get_arch(),
archive_type
);
let response = match http::create_client().get(&url).send().await {
Ok(response) => match response.json::<serde_json::Value>().await {
Ok(json) => json,
Err(_) => return Err(JavaDownloadError::NoJavaVersionFound)
},
Err(_) => return Err(JavaDownloadError::NoJavaVersionFound)
};
if !response.is_array() || response.as_array().unwrap().is_empty() {
return Err(JavaDownloadError::NoJavaVersionFound);
}
let latest = response.as_array().unwrap().first().unwrap();
let download_url = latest.get("download_url").unwrap().as_str().unwrap();
let archive_name = format!("zulu-{}.{}", java_version.to_string(), archive_type);
let tmp_archive = java_dir.join(&archive_name);
if let Err(err) = http::download_file(download_url, tmp_archive.as_path()).await {
eprintln!("{}", err);
return Err(JavaDownloadError::DownloadError);
};
// TODO: Extract the archive and delete the archive after a SUCCESSFUL extraction.
// Name the folder containing the JRE `zulu-{version}-{arch}`
Ok(tmp_archive)
}
pub fn get_arch() -> String {
let arch = env::consts::ARCH;
match arch {
"x86" => "x86",
"x86_64" => "x64",
"arm" => "aarch32",
"aarch64" => "aarch64",
_ => "unsupported"
}.to_string()
}
#[derive(Debug)]
pub enum JavaDownloadError {
NoJavaVersionFound,
PermissionDenied,
UnsupportedOS,
UnsupportedArch,
DownloadError,
}

View file

@ -0,0 +1,11 @@
use serde::{Deserialize, Serialize};
use super::JavaVersion;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Manifest {
pub name: String,
pub version: String,
pub main_class: String,
pub java_version: JavaVersion,
}

View file

@ -1 +1,21 @@
pub mod version;
use serde::{Deserialize, Serialize};
pub mod client;
pub mod manifest;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum JavaVersion {
V8,
V16,
V17
}
impl ToString for JavaVersion {
fn to_string(&self) -> String {
match self {
JavaVersion::V8 => "8".to_string(),
JavaVersion::V16 => "16".to_string(),
JavaVersion::V17 => "17".to_string()
}
}
}

View file

@ -1,139 +0,0 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AssetIndex {
pub id: String,
pub sha1: String,
pub size: u64,
pub total_size: u64,
pub url: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VersionDownload {
pub sha1: String,
pub size: u64,
pub url: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VersionDownloads {
pub client: VersionDownload,
pub server: VersionDownload,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JavaVersion {
pub major_version: u16,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuleOS {
pub name: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Rule {
pub action: String,
pub os: Option<RuleOS>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LibraryArtifact {
pub path: String,
pub sha1: String,
pub size: u64,
pub url: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LibraryDownloads {
pub artifact: LibraryArtifact,
#[serde(flatten)]
pub classifiers: Option<HashMap<String, LibraryArtifact>>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LibraryNatives {
pub linux: Option<String>,
pub osx: Option<String>,
pub windows: Option<String>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Library {
pub downloads: LibraryDownloads,
pub name: String,
pub natives: Option<LibraryNatives>,
pub rules: Option<Vec<Rule>>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArgumentRule {
pub rules: Vec<Rule>,
pub value: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum Argument {
String(String),
Rule(ArgumentRule),
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Arguments {
pub game: Vec<Argument>,
pub jvm: Vec<Argument>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModernVersionManifest {
pub asset_index: AssetIndex,
pub downloads: VersionDownloads,
pub id: String,
pub java_version: JavaVersion,
pub libraries: Vec<Library>,
pub main_class: String,
pub minecraft_arguments: String,
pub release_time: String,
#[serde(rename = "type")]
pub release_type: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LegacyVersionManifest {
pub asset_index: AssetIndex,
pub downloads: VersionDownloads,
pub id: String,
pub java_version: JavaVersion,
pub libraries: Vec<Library>,
pub main_class: String,
pub minecraft_arguments: String,
pub release_time: String,
#[serde(rename = "type")]
pub release_type: String,
// TODO
// pub logging: Option<Logging>,
}

View file

@ -1,3 +1,4 @@
#[allow(async_fn_in_trait)]
pub mod auth;
pub mod game;
pub mod utils;
pub mod constants;

View file

@ -3,13 +3,20 @@
use std::error::Error;
use polyfrost_launcher::auth;
use tauri::{menu::Menu, App};
use polyfrost_launcher::{auth, game::{client::{download_java, GameClient}, JavaVersion}};
use tauri::{generate_handler, menu::Menu, App, Runtime};
#[tauri::command]
async fn download_java_test<R: Runtime>(app: tauri::AppHandle<R>, window: tauri::Window<R>) {
let res = download_java(&app, JavaVersion::V8).await;
println!("{:#?}", res);
}
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(auth::init())
.invoke_handler(generate_handler![download_java_test])
.menu(|handle| Menu::new(handle))
.setup(setup)
.run(tauri::generate_context!())

View file

@ -0,0 +1,19 @@
use std::{error::Error, fs::File, io::{self, Cursor}, path::Path};
use tauri_plugin_http::reqwest::{Client, ClientBuilder};
use crate::constants::USER_AGENT;
pub fn create_client() -> Client {
ClientBuilder::new()
.user_agent(USER_AGENT)
.build().expect("Failed to create HTTP client. Please report this issue on GitHub.")
}
pub async fn download_file(url: &str, path: &Path) -> Result<(), Box<dyn Error>> {
let response = create_client().get(url).send().await?;
let mut file = File::create(path)?;
let mut content = Cursor::new(response.bytes().await?);
io::copy(&mut content, &mut file)?;
Ok(())
}

View file

@ -0,0 +1 @@
pub mod http;

2
src/types/game.d.ts vendored
View file

@ -1,3 +1,3 @@
declare namespace game {
export type Client = 'fabric' | 'forge' | 'quilt' | 'vanilla' | 'other';
export type Client = 'Fabric' | 'Forge' | 'Quilt' | 'Vanilla' | 'Other';
}

View file

@ -1,87 +1,15 @@
import type { DragEventHandler, Id } from '@thisbeyond/solid-dnd';
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter, createSortable, useDragDropContext } from '@thisbeyond/solid-dnd';
import type { JSX } from 'solid-js';
import { For, createSignal } from 'solid-js';
declare module 'solid-js' {
// eslint-disable-next-line ts/no-namespace
namespace JSX {
interface Directives {
sortable: any;
}
}
}
interface ItemProps {
item: boolean | Id | Node | JSX.ArrayElement | null | undefined;
};
function Item(props: ItemProps) {
const id = () => props.item as Id;
// eslint-disable-next-line solid/reactivity
const sortable = createSortable(id());
// @ts-expect-error - `state` is not defined in the type definition
const [state] = useDragDropContext();
return (
<div
use:sortable
class="sortable"
classList={{
'opacity-25': sortable.isActiveDraggable,
'transition-transform': !!state.active.draggable,
}}
>
{props.item}
</div>
);
}
function List() {
const [items, setItems] = createSignal([1, 2, 3]);
const [activeItem, setActiveItem] = createSignal<number | null>(null);
const ids = () => items();
const onDragStart: DragEventHandler = ({ draggable }) => setActiveItem(draggable.id as number);
const onDragEnd: DragEventHandler = ({ draggable, droppable }) => {
if (draggable && droppable) {
const currentItems = ids();
const fromIndex = currentItems.indexOf(draggable.id as number);
const toIndex = currentItems.indexOf(droppable.id as number);
if (fromIndex !== toIndex) {
const updatedItems = currentItems.slice();
updatedItems.splice(toIndex, 0, ...updatedItems.splice(fromIndex, 1));
setItems(updatedItems);
}
}
};
return (
<DragDropProvider
onDragStart={onDragStart}
onDragEnd={onDragEnd}
collisionDetector={closestCenter}
>
<DragDropSensors />
<div class="column self-stretch">
<SortableProvider ids={ids()}>
<For each={items()}>{item => <Item item={item} />}</For>
</SortableProvider>
</div>
<DragOverlay>
<div class="sortable">{activeItem()}</div>
</DragOverlay>
</DragDropProvider>
);
}
import { invoke } from '@tauri-apps/api/core';
import Button from '../components/base/Button';
function BrowserPage() {
function downloadJava() {
invoke('download_java_test');
}
return (
<div class="flex flex-col gap-y-4">
<h1>Browser</h1>
<List />
<Button onClick={() => downloadJava()}>Download java</Button>
</div>
);
}

View file

@ -7,7 +7,7 @@ import {
SearchMdIcon,
} from '@untitled-theme/icons-solid';
import { For, createSignal } from 'solid-js';
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter, createSortable, useDragDropContext } from '@thisbeyond/solid-dnd';
import { DragDropProvider, DragDropSensors, SortableProvider, closestCenter, createSortable } from '@thisbeyond/solid-dnd';
import type { DragEventHandler, Id } from '@thisbeyond/solid-dnd';
import image from '../../assets/images/header.png';
import Button from '../components/base/Button';
@ -100,7 +100,7 @@ function InstanceCard(props: InstanceCardProps) {
<div class="flex flex-col gap-1.5">
<p class="h-4 font-semibold">{props.name}</p>
<p class="h-4 text-xs">
{upperFirst(props.client)}
{props.client}
{' '}
{props.version}
{' '}
@ -167,63 +167,56 @@ function HomePage() {
const myInstances: InstanceCardProps[] = [
{
name: 'Hypixel',
client: 'vanilla',
client: 'Vanilla',
version: '1.8.9',
id: 1,
},
{
name: 'OneConfig ??',
client: 'forge',
client: 'Forge',
version: '1.8.9',
mods: 12,
id: 2,
},
{
name: 'Fabulously Optimised',
client: 'fabric',
client: 'Fabric',
version: '1.20.4',
mods: 48,
id: 3,
},
{
name: 'sus client',
client: 'other',
client: 'Other',
version: '1.8.9',
id: 4,
},
{
name: 'Hypixel',
client: 'vanilla',
client: 'Vanilla',
version: '1.8.9',
id: 5,
},
{
name: 'OneConfig ??',
client: 'forge',
client: 'Forge',
version: '1.8.9',
mods: 12,
id: 6,
},
{
name: 'Fabulously Optimised',
client: 'fabric',
client: 'Fabric',
version: '1.20.4',
mods: 48,
id: 7,
},
{
name: 'sus client',
client: 'other',
client: 'Other',
version: '1.8.9',
id: 8,
},
// {
// name: 'OneConfig ??',
// client: 'forge',
// version: '1.8.9',
// mods: 12,
// id: 5,
// },
];
return (