diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 6f8c0413..00000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[alias] -prisma = "run -p prisma-cli --bin prisma --" diff --git a/Cargo.toml b/Cargo.toml index 817347ab..1504a361 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,6 @@ [workspace] resolver = "2" -members = [ - "src-tauri", - "crates/*", -] +members = [ "src-tauri", ] [workspace.package] license = "AGPL-3.0-only" @@ -20,12 +17,10 @@ tauri-build = { version = "2.0.0-beta", features = [] } tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } tauri-plugin-http = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } -prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.11", default-features = false, features = [ "sqlite" ] } -prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.11", default-features = false, features = [ "sqlite" ] } - uuid = { version = "1.8", features = [ "serde" ] } -serde = { version = "1", features = [ "derive" ] } tokio = { version = "1", features = [ "full" ] } +serde = { version = "1", features = [ "derive" ] } +chrono = { version = "0.4", features = [ "serde" ] } serde_json = { version = "1" } flate2 = { version = "1" } diff --git a/crates/prisma-cli/Cargo.toml b/crates/prisma-cli/Cargo.toml deleted file mode 100644 index 3fa52bbe..00000000 --- a/crates/prisma-cli/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "prisma-cli" -version = "0.1.0" -license = { workspace = true } -edition = { workspace = true } -repository = { workspace = true } -documentation = { workspace = true } -readme = { workspace = true } -homepage = { workspace = true } -authors = { workspace = true } - -[dependencies] -prisma-client-rust-cli = { workspace = true } diff --git a/crates/prisma-cli/src/bin/prisma.rs b/crates/prisma-cli/src/bin/prisma.rs deleted file mode 100644 index 96e11d24..00000000 --- a/crates/prisma-cli/src/bin/prisma.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - prisma_client_rust_cli::run(); -} diff --git a/crates/prisma/Cargo.toml b/crates/prisma/Cargo.toml deleted file mode 100644 index 65719fe9..00000000 --- a/crates/prisma/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "poly-prisma" -version = "0.1.0" -license = { workspace = true } -edition = { workspace = true } -repository = { workspace = true } -documentation = { workspace = true } -readme = { workspace = true } -homepage = { workspace = true } -authors = { workspace = true } - -[dependencies] -prisma-client-rust = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -uuid = { workspace = true } diff --git a/crates/prisma/src/lib.rs b/crates/prisma/src/lib.rs deleted file mode 100644 index 1fac3e75..00000000 --- a/crates/prisma/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[allow(warnings, unused)] -pub mod prisma; diff --git a/package.json b/package.json index f105f120..843d68c2 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,7 @@ "serve": "vite preview", "tauri": "tauri", "lint": "eslint . --cache", - "format": "pnpm lint --fix && cargo fmt", - "prisma": "cd src-tauri && cargo prisma" + "format": "pnpm lint --fix && cargo fmt" }, "dependencies": { "@fontsource/poppins": "^5.0.12", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index cae0feba..997218af 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -14,7 +14,6 @@ authors = { workspace = true } tauri-build = { workspace = true } [dependencies] -poly-prisma = { path = "../crates/prisma" } tauri = { workspace = true } tauri-plugin-shell = { workspace = true } tauri-plugin-http = { workspace = true } @@ -26,9 +25,10 @@ tar = { workspace = true } zip = { workspace = true } uuid = { workspace = true } async-trait = { workspace = true } -prisma-client-rust = { workspace = true } thiserror = { workspace = true } anyhow = { workspace = true } +chrono = { workspace = true } +sha1 = "0.10.6" [features] # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 86edf9eb..c9450726 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -6,7 +6,11 @@ fn main() { .plugin("auth", InlinedPlugin::new().commands(&["login_msa"])) .plugin( "game", - InlinedPlugin::new().commands(&["launch_game", "set_selected_client"]), + InlinedPlugin::new().commands(&[ + "create_instance", + "get_instances", + "get_instance" + ]), ), ); } diff --git a/src-tauri/permissions/game/default.toml b/src-tauri/permissions/game/default.toml index 5a36fd10..cda1135c 100644 --- a/src-tauri/permissions/game/default.toml +++ b/src-tauri/permissions/game/default.toml @@ -1,6 +1,7 @@ [default] description = "Default permissions for the plugin" permissions = [ - "allow-launch-game", - "allow-set-selected-client" + "allow-get-instance", + "allow-get-instances", + "allow-create-instance" ] diff --git a/src-tauri/prisma/migrations/20240320193141_init/migration.sql b/src-tauri/prisma/migrations/20240320193141_init/migration.sql deleted file mode 100644 index 42b19115..00000000 --- a/src-tauri/prisma/migrations/20240320193141_init/migration.sql +++ /dev/null @@ -1,32 +0,0 @@ --- CreateTable -CREATE TABLE "instance" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "name" TEXT, - "path" TEXT, - "hidden" BOOLEAN, - "time_played" DATETIME NOT NULL, - "last_played" DATETIME NOT NULL, - "date_created" DATETIME NOT NULL -); - --- CreateTable -CREATE TABLE "java_instance" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "name" TEXT, - "path" TEXT, - "version" TEXT, - "arch" TEXT, - "instance_id" INTEGER -); - --- CreateTable -CREATE TABLE "location" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "name" TEXT, - "path" TEXT, - "enabled" BOOLEAN NOT NULL DEFAULT true, - "hidden" BOOLEAN, - "date_added" DATETIME, - "instance_id" INTEGER, - CONSTRAINT "location_instance_id_fkey" FOREIGN KEY ("instance_id") REFERENCES "instance" ("id") ON DELETE SET NULL ON UPDATE CASCADE -); diff --git a/src-tauri/prisma/migrations/migration_lock.toml b/src-tauri/prisma/migrations/migration_lock.toml deleted file mode 100644 index 6fcf33da..00000000 --- a/src-tauri/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "sqlite" diff --git a/src-tauri/prisma/schema.prisma b/src-tauri/prisma/schema.prisma deleted file mode 100644 index cfaba94e..00000000 --- a/src-tauri/prisma/schema.prisma +++ /dev/null @@ -1,100 +0,0 @@ -datasource db { - provider = "sqlite" - url = "file:dev.db" -} - -generator client { - provider = "cargo prisma" - output = "../../crates/prisma/src/prisma.rs" -} - -// todo: replace all `path` fields with a model with a checksum n shit -// todo: replace all `String` fields with an enum if applicable - -// represents a single minecraft instance -model Instance { - id Int @id @default(autoincrement()) - name String? - path String? - hidden Boolean? - - time_played DateTime - last_played DateTime - date_created DateTime @default(now()) - - mods Mod[] - - java_instance JavaInstance @relation(fields: [java_instance_id], references: [id]) - java_instance_id Int - - @@map("instance") -} - -model JavaInstance { - id Int @id @default(autoincrement()) - name String? - path String? - version String? - arch String? - - instance Instance[] - - @@map("java_instance") -} - -model Asset { - id Int @id @default(autoincrement()) - - name String? - path String? - enabled Boolean @default(true) - hidden Boolean? - date_added DateTime? - - /// @local - instance_id Int? - instance Instance? @relation(fields: [instance_id], references: [id], onDelete: SetNull) - - @@map("asset") -} - -model MinecraftManifest { - id String @id - assets String - compliance_level Int - downloads Downloads - - @@map("minecraft_manifest") -} - -model Downloads { - client Client - - @@map("downloads") -} - -model Client { - sha1 String @unique - size Int - url String - - @@map("client") -} - -model JavaVersion { - component String - major_version MajorVersion @default(VERSION_1_21) -} - -enum MajorVersion { - VERSION_1_21 - VERSION_1_8 -} - -model Library { - downloads LibraryDownloads - name String - rules Rule[] - extract Extract? - natives Natives? -} diff --git a/src-tauri/src/game/client.rs b/src-tauri/src/game/client.rs index 983021a3..8bf0cecc 100644 --- a/src-tauri/src/game/client.rs +++ b/src-tauri/src/game/client.rs @@ -1,270 +1,173 @@ -use std::{collections::HashMap, env, fs, path::PathBuf}; +use std::{collections::HashMap, fs, path::PathBuf}; +use anyhow::anyhow; use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use serde_json::Value; +use chrono::{DateTime, Local}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use tauri::{AppHandle, Manager}; -use thiserror::Error; use uuid::Uuid; -use crate::{ - utils::{file, http}, - PolyError, PolyResult, -}; +use crate::PolyResult; -use super::{ - clients::vanilla::{vanilla_manifest, VanillaClient, VanillaManifest}, - JavaVersion, -}; +use super::{minecraft::MinecraftManifest, vanilla}; -#[macro_export] -macro_rules! create_manifest { - ($manifest_name:ident { $($name:ident: $type:ty),* }) => { - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] - pub struct $manifest_name { - $(pub $name: $type,)* - } - }; -} - -#[macro_export] -macro_rules! create_client { - ($client_name:ident { $($name:ident: $type:ty),* }) => { - #[derive(Debug, Clone)] - pub struct $client_name { - handle: tauri::AppHandle, - details: crate::game::client::GameClientDetails, - $(pub $name: $type,)* - } - }; +#[derive(Debug, Serialize, Deserialize)] +pub struct Manifest { + pub id: Uuid, + pub manifest: MinecraftManifest } #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", content = "manifest")] -pub enum GameClientType { - Vanilla(VanillaManifest), - // Forge(ForgeManifest), +pub struct Instance { + pub id: Uuid, + pub created_at: DateTime, + pub name: String, + pub cover: Option, + pub group: Option, + pub client: ClientType, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GameClientDetails { - pub uuid: Uuid, - pub name: String, - pub version: String, - pub main_class: String, - pub java_version: JavaVersion, - pub startup_args: Vec, - pub client_type: GameClientType, -} - -pub fn get_game_client(handle: AppHandle, details: GameClientDetails) -> Box { - Box::new(match details.client_type { - GameClientType::Vanilla(_) => VanillaClient::new(handle, details), - // GameClientType::Forge(_) => Box::new(ForgeClient::new(handle, details)), - }) +#[serde(tag = "type")] +pub enum ClientType { + Vanilla } #[async_trait] -pub trait GameClient: Send + Sync { - fn new(handle: AppHandle, details: GameClientDetails) -> Self - where - Self: Sized; +pub trait ClientTrait: Send + Sync { + fn from_instance(instance: Instance) -> Self where Self: Sized; - async fn get_all_version_urls() -> PolyResult> - where - Self: Sized; - - async fn get_minecraft_manifest( - version: String, - ) -> PolyResult - where - Self: Sized; + async fn launch(&self) -> PolyResult<()>; + async fn setup(&self) -> PolyResult<()>; - // async fn get_details_from_version(version: String) -> Result> where Self: Sized; - - fn get_handle(&self) -> &AppHandle; - fn get_details(&self) -> &GameClientDetails; - fn get_client(&self) -> &GameClientType { - &self.get_details().client_type - } - - async fn launch(&self) -> PolyResult<()>; - async fn setup(&self) -> PolyResult<()>; - - async fn download_java(&self) -> PolyResult { - let config_dir = match self.get_handle().path().app_config_dir() { - Ok(dir) => dir, - // SURELY THERES A BETTER WAY TO DO THIS IDK LOLL - Err(err) => { - return Err(PolyError::JavaError(JavaDownloadError::UnsupportedOS( - err.to_string(), - ))) - } - }; - - let java_dir = config_dir.join("java"); - - if !java_dir.exists() { - match fs::create_dir_all(&java_dir) { - Ok(_) => (), - Err(err) => { - return Err(PolyError::JavaError(JavaDownloadError::PermissionDenied( - err.to_string(), - ))) - } - }; - } - - let java_version = &self.get_details().java_version; - let os = env::consts::OS; - let archive_type = match os { - "windows" => "zip", - "macos" => "tar.gz", - "linux" => "tar.gz", - _ => { - return Err(PolyError::JavaError(JavaDownloadError::UnsupportedOS( - os.to_string(), - ))) - } - }; - - let archive_name = format!("zulu-{}.{}", java_version.to_string(), archive_type); - let archive = java_dir.join(&archive_name); - let dest = java_dir.join(format!("zulu-{}-{}", java_version.to_string(), get_arch())); - - if archive.exists() && dest.exists() { - let _ = fs::remove_file(archive.as_path()); - } else if archive.exists() && !dest.exists() { - extract(&archive, &dest)?; - } else if !archive.exists() && !dest.exists() { - download(java_version, os, archive_type, &archive).await?; - extract(&archive, &dest)?; - } - - if let Ok(mut files) = fs::read_dir(&dest) { - let file = match files.nth(0) { - Some(file) => file.unwrap().path(), - None => { - return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound( - "unable to get the java executable file".to_string(), - ))) - } - }; - - return Ok(file.join("bin").join("java")); - } - - Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound( - "unable to download the java executable file".to_string(), - ))) - } + async fn install_game(&self) -> PolyResult<()>; + async fn install_libraries(&self) -> PolyResult; + async fn install_natives(&self) -> PolyResult<()>; + async fn install_assets(&self) -> PolyResult<()>; } -#[async_trait] -pub trait ModdedGameClient: GameClient { - async fn install_mods(&self) -> PolyResult<()>; +pub struct ClientManager { + instances: HashMap, + instances_dir: PathBuf, + + manifests: HashMap, + manifests_dir: PathBuf, } -#[derive(Debug, Error)] -pub enum JavaDownloadError { - #[error("failed to find a valid java version: {0}")] - NoJavaVersionFound(String), - #[error("failed creating a java directory (try running at an elevated permission): {0}")] - PermissionDenied(String), - #[error("unable to match your operating system ({0}) with a valid java version")] - UnsupportedOS(String), - #[error("unable to match your architecture ({0}) with a valid java version")] - UnsupportedArch(String), - #[error("failed to extract the java executable: {0}")] - ExtractError(String), - #[error("failed to download the java executable")] - DownloadError(String), +impl ClientManager { + pub fn new(handle: &AppHandle) -> PolyResult { + let instances_dir = handle.path().app_config_dir()?.join("instances"); + let instances = load_and_serialize::(&instances_dir)?; + + let manifests_dir = handle.path().app_config_dir()?.join("manifests"); + let manifests = load_and_serialize::(&manifests_dir)?; + + Ok(Self { + instances, + instances_dir, + manifests, + manifests_dir, + }) + } + + pub fn get_instances(&self) -> Vec<&Instance> { + self.instances.values().collect() + } + + pub fn get_instances_owned(&self) -> Vec { + self.instances.values().cloned().collect() + } + + pub fn get_instance(&self, uuid: Uuid) -> Option<&Instance> { + self.instances.get(&uuid) + } + + pub fn get_instance_mut(&mut self, uuid: Uuid) -> Option<&mut Instance> { + self.instances.get_mut(&uuid) + } + + pub fn get_manifest(&self, uuid: Uuid) -> Option<&Manifest> { + self.manifests.get(&uuid) + } + + pub fn get_manifest_mut(&mut self, uuid: Uuid) -> Option<&mut Manifest> { + self.manifests.get_mut(&uuid) + } + + pub async fn create_instance(&mut self, + name: String, + version: String, + cover: Option, + group: Option, + client: ClientType + ) -> PolyResult { + // TODO: Implement cahcing + let url = String::from("https://piston-meta.mojang.com/v1/packages/d546f1707a3f2b7d034eece5ea2e311eda875787/1.8.9.json"); + let uuid = Uuid::new_v4(); + + // Save the manifest + let minecraft_manifest = vanilla::retrieve_version_manifest(url).await?; + let manifest = Manifest { + id: uuid, + manifest: minecraft_manifest + }; + + save(&self.manifests_dir.join(format!("{}.json", uuid)), &manifest)?; + self.manifests.insert(uuid, manifest); + + // Save the instance + let instance = Instance { + id: uuid, + created_at: Local::now(), + name, + cover, + group, + client + }; + + save(&self.instances_dir.join(format!("{}.json", uuid)), &instance)?; + self.instances.insert(uuid, instance); + + Ok(uuid) + } } -fn get_arch() -> String { - let arch = env::consts::ARCH; - match arch { - "x86" => "x86", - "x86_64" => "x64", - "arm" => "aarch32", - "aarch64" => "aarch64", - _ => "unsupported", - } - .to_string() +fn save(path: &PathBuf, object: T) -> PolyResult<()> where T: Serialize { + if !path.exists() { + fs::create_dir_all(path.parent().expect("Couldn't get parent of path whilst saving object. 'client.rs'"))?; + } + + let content = serde_json::to_string(&object)?; + fs::write(path, content)?; + Ok(()) } -async fn get_java_versions( - java_version: &JavaVersion, - os: &str, - archive_type: &str, -) -> PolyResult { - 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 - ); +fn load_and_serialize(dir: &PathBuf) -> PolyResult> where T: DeserializeOwned { + let mut result = HashMap::::new(); - let response = match http::create_client()?.get(&url).send().await { - Ok(response) => match response.json::().await { - Ok(json) => json, - Err(err) => { - return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound( - err.to_string(), - ))) - } - }, - Err(err) => { - return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound( - err.to_string(), - ))) - } - }; + if !dir.exists() { + fs::create_dir_all(dir)?; + return Ok(result); + } + + for file in fs::read_dir(dir)? { + let file = file?; + let file_name = file.file_name(); + let file_name = file_name.to_str().ok_or(anyhow!("Couldn't convert OsStr to str"))?.replace(".json", ""); + let uuid = match Uuid::parse_str(&file_name) { + Ok(uuid) => uuid, + Err(_) => { + eprintln!("Couldn't parse file name as UUID: '{:?}'", file_name); + continue; + } + }; - if !response.is_array() || response.as_array().unwrap().is_empty() { - return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound( - "didn't get the expected java api result".to_string(), - ))); - } + let path = file.path(); + let content = fs::read_to_string(&path)?; + let parsed = serde_json::from_str::(&content)?; - Ok(response) -} - -async fn download( - java_version: &JavaVersion, - os: &str, - archive_type: &str, - archive: &PathBuf, -) -> PolyResult<()> { - let response = get_java_versions(java_version, os, archive_type).await?; - let latest = response.as_array().unwrap().first().unwrap(); - let download_url = latest.get("download_url").unwrap().as_str().unwrap(); - - if let Err(err) = http::download_file(download_url, archive.as_path()).await { - // eprintln!("{}", err); - return Err(PolyError::JavaError(JavaDownloadError::DownloadError( - err.to_string(), - ))); - }; - - Ok(()) -} - -fn extract(archive: &PathBuf, dest: &PathBuf) -> PolyResult<()> { - if let Err(err) = file::extract_archive(archive.as_path(), dest.as_path()) { - let _ = fs::remove_file(dest.as_path()); - return Err(PolyError::JavaError(JavaDownloadError::ExtractError( - err.to_string(), - ))); - } - - let _ = fs::remove_file(archive.as_path()); - Ok(()) + result.insert(uuid, parsed); + } + + Ok(result) } diff --git a/src-tauri/src/game/clients/mod.rs b/src-tauri/src/game/clients/mod.rs deleted file mode 100644 index f0e7b584..00000000 --- a/src-tauri/src/game/clients/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod vanilla; diff --git a/src-tauri/src/game/clients/vanilla.rs b/src-tauri/src/game/clients/vanilla.rs deleted file mode 100644 index 279fa305..00000000 --- a/src-tauri/src/game/clients/vanilla.rs +++ /dev/null @@ -1,338 +0,0 @@ -use std::collections::HashMap; - -use anyhow::anyhow; -use async_trait::async_trait; -use serde_json::Value; -use tauri::{AppHandle, Manager}; -use tokio::io::{stdout, AsyncWriteExt}; - -use crate::{ - create_client, create_manifest, - game::client::{GameClient, GameClientDetails, GameClientType}, - utils::http, - PolyError, PolyResult, -}; - -create_manifest!(VanillaManifest { - // minecraft: vanilla_manifest::MinecraftManifest -}); - -create_client!(VanillaClient {}); - -#[async_trait] -impl GameClient for VanillaClient { - fn new(handle: AppHandle, details: GameClientDetails) -> Self { - Self { - handle: handle.clone(), - details, - } - } - - async fn get_all_version_urls() -> PolyResult> { - const URL: &str = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; - let request = http::create_client()? - .get(URL) - .send() - .await - .map_err(|err| PolyError::HTTPError(err))?; - - let json = request - .json::() - .await - .map_err(|err| PolyError::HTTPError(err))?; - - let versions = json - .get("versions") - .ok_or(PolyError::AnyhowError(anyhow!( - "failed to get versions object" - )))? - .as_array() - .ok_or(PolyError::AnyhowError(anyhow!("invalid versions object")))?; - - let mut map = HashMap::new(); - - for version in versions { - let version = version.as_object().ok_or(anyhow!("Invalid version object"))?; - - let id = version - .get("id") - .ok_or(anyhow!("No id object"))? - .as_str() - .ok_or(anyhow!("Invalid id object"))?; - let url = version - .get("url") - .ok_or(anyhow!("No url object"))? - .as_str() - .ok_or(anyhow!("Invalid url object"))?; - - map.insert(id.to_string(), url.to_string()); - } - - Ok(map) - } - - async fn get_minecraft_manifest( - version_url: String, - ) -> PolyResult { - let request = http::create_client()?.get(version_url).send().await?; - - match request.json::().await { - Ok(manifest) => Ok(manifest), - Err(e) => Err(e.into()), - } - } - - fn get_handle(&self) -> &AppHandle { - &self.handle - } - - fn get_details(&self) -> &GameClientDetails { - &self.details - } - - fn get_client(&self) -> &GameClientType { - &self.get_details().client_type - } - - async fn setup(&self) -> PolyResult<()> { - let libraries_dir = self.get_handle().path().app_config_dir()?.join("libraries"); - let versions = Self::get_all_version_urls().await?; - let version = versions - .get(&self.get_details().version) - .ok_or(anyhow!("Version not found"))? - .to_owned(); - let manifest = Self::get_minecraft_manifest(version).await?; - - let libraries = vanilla_impl::setup_libraries(&manifest.libraries, &libraries_dir).await?; - println!("{:#?}", libraries); - // vanilla_impl::setup_natives().await?; - // vanilla_impl::setup_assets().await?; - Ok(()) - } - - async fn launch(&self) -> PolyResult<()> { - println!("Launching Vanilla client"); - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - println!("Vanilla client launched"); - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - println!("Vanilla client exited"); - stdout().flush().await?; // why... - Ok(()) - } -} - -pub mod vanilla_impl { - use std::{fs, path::PathBuf}; - - use anyhow::anyhow; - -use crate::{utils::http::download_file, PolyResult}; - - use super::vanilla_manifest::Library; - - pub async fn setup_libraries( - libraries: &Vec, - libraries_folder: &PathBuf, - ) -> PolyResult> { - let mut natives_ret: Vec<&Library> = vec![]; - let mut libraries_ret: Vec = vec![]; - - for library in libraries { - if let Some(_) = library.natives { - natives_ret.push(library); - continue; - } - - let artifact = library - .downloads - .artifact - .clone() - .ok_or(anyhow!("No artifact object"))?; - let path = artifact.path; - let url = artifact.url; - - // TODO: Add checks for rules + platform - - let dest = libraries_folder.join(path); - fs::create_dir_all(dest.parent().ok_or(anyhow!("Couldn't get library parent"))?)?; - - if !dest.exists() { - download_file(url.as_str(), &dest).await?; - } - - libraries_ret.push( - dest.to_str() - .ok_or(anyhow!("Couldn't get library path"))? - .to_string(), - ); - } - - Ok(libraries_ret) - } - - pub async fn setup_natives() -> PolyResult<()> { - Ok(()) - } - - pub async fn setup_assets() -> PolyResult<()> { - Ok(()) - } -} - -// todo move to prisma -pub mod vanilla_manifest { - use serde::Deserialize; - use serde::Serialize; - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct MinecraftManifest { - pub asset_index: AssetIndex, - pub assets: String, - pub compliance_level: i64, - pub downloads: Downloads, - pub id: String, - pub java_version: JavaVersion, - pub libraries: Vec, - pub logging: Logging, - pub main_class: String, - pub minecraft_arguments: String, - pub minimum_launcher_version: i64, - pub release_time: String, - pub time: String, - - #[serde(rename = "type")] - pub type_field: String, - } - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct AssetIndex { - pub id: String, - pub sha1: String, - pub size: i64, - pub total_size: i64, - pub url: String, - } - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - pub struct Downloads { - pub client: Client, - } - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - pub struct Client { - pub sha1: String, - pub size: i64, - pub url: String, - } - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct JavaVersion { - pub component: String, - pub major_version: i64, - } - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Library { - pub downloads: Downloads2, - pub name: String, - #[serde(default)] - pub rules: Vec, - pub extract: Option, - pub natives: Option, - } - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Downloads2 { - pub artifact: Option, - pub classifiers: Option, - } - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Artifact { - pub path: String, - pub sha1: String, - pub size: i64, - pub url: String, - } - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Classifiers { - #[serde(rename = "natives-windows-32")] - pub natives_windows_32: Option, - #[serde(rename = "natives-windows-64")] - pub natives_windows_64: Option, - #[serde(rename = "natives-osx")] - pub natives_osx: Option, - #[serde(rename = "natives-linux")] - pub natives_linux: Option, - #[serde(rename = "natives-windows")] - pub natives_windows: Option, - } - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct NativeLibrary { - pub path: String, - pub sha1: String, - pub size: i64, - pub url: String, - } - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Rule { - pub action: String, - pub os: Option, - } - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Os { - pub name: String, - } - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Extract { - pub exclude: Vec, - } - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Natives { - pub windows: String, - pub linux: Option, - pub osx: Option, - } - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Logging { - pub client: Client2, - } - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Client2 { - pub argument: String, - pub file: File, - #[serde(rename = "type")] - pub type_field: String, - } - - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct File { - pub id: String, - pub sha1: String, - pub size: i64, - pub url: String, - } -} diff --git a/src-tauri/src/game/java.rs b/src-tauri/src/game/java.rs index e69de29b..3c1cadc4 100644 --- a/src-tauri/src/game/java.rs +++ b/src-tauri/src/game/java.rs @@ -0,0 +1,157 @@ +use std::{fs, path::PathBuf}; + +use serde_json::Value; +use thiserror::Error; + +use crate::{utils::{file, http}, PolyError, PolyResult}; + +#[derive(Debug, Error)] +pub enum JavaDownloadError { + #[error("failed to find a valid java version: {0}")] + NoJavaVersionFound(String), + #[error("failed creating a java directory (try running at an elevated permission): {0}")] + PermissionDenied(String), + #[error("unable to match your operating system ({0}) with a valid java version")] + UnsupportedOS(String), + #[error("unable to match your architecture ({0}) with a valid java version")] + UnsupportedArch(String), + #[error("failed to extract the java executable: {0}")] + ExtractError(String), + #[error("failed to download the java executable")] + DownloadError(String), +} + +pub async fn download_java(dir: &PathBuf, version: u8) -> PolyResult { + if !dir.exists() { + match fs::create_dir_all(&dir) { + Ok(_) => (), + Err(err) => { + return Err(PolyError::JavaError(JavaDownloadError::PermissionDenied( + err.to_string(), + ))) + } + }; + } + + let os = std::env::consts::OS; + let archive_type = match os { + "windows" => "zip", + "macos" => "tar.gz", + "linux" => "tar.gz", + _ => { + return Err(PolyError::JavaError(JavaDownloadError::UnsupportedOS( + os.to_string(), + ))) + } + }; + + let archive_name = format!("zulu-{}.{}", version.to_string(), archive_type); + let archive = dir.join(&archive_name); + let dest = dir.join(format!("zulu-{}-{}", version.to_string(), get_arch())); + + if archive.exists() && dest.exists() { + let _ = fs::remove_file(archive.as_path()); + } else if archive.exists() && !dest.exists() { + extract(&archive, &dest)?; + } else if !archive.exists() && !dest.exists() { + download(version, os, archive_type, &archive).await?; + extract(&archive, &dest)?; + } + + if let Ok(mut files) = fs::read_dir(&dest) { + let file = match files.nth(0) { + Some(file) => file.unwrap().path(), + None => { + return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound( + "unable to get the java executable file".to_string(), + ))) + } + }; + + return Ok(file.join("bin").join("java")); + } + + Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound( + "unable to download the java executable file".to_string(), + ))) +} + +fn get_arch() -> String { + let arch = std::env::consts::ARCH; + match arch { + "x86" => "x86", + "x86_64" => "x64", + "arm" => "aarch32", + "aarch64" => "aarch64", + _ => "unsupported", + }.to_string() +} + +async fn download( + java_version: u8, + os: &str, + archive_type: &str, + archive: &PathBuf, +) -> PolyResult<()> { + let response = get_java_versions(java_version, os, archive_type).await?; + let latest = response.as_array().unwrap().first().unwrap(); + let download_url = latest.get("download_url").unwrap().as_str().unwrap(); + + if let Err(err) = http::download_file(download_url, archive.as_path()).await { + return Err(PolyError::JavaError(JavaDownloadError::DownloadError( + err.to_string(), + ))); + }; + + Ok(()) +} + +fn extract(archive: &PathBuf, dest: &PathBuf) -> PolyResult<()> { + if let Err(err) = file::extract_archive(archive.as_path(), dest.as_path()) { + let _ = fs::remove_file(dest.as_path()); + return Err(PolyError::JavaError(JavaDownloadError::ExtractError( + err.to_string(), + ))); + } + + let _ = fs::remove_file(archive.as_path()); + Ok(()) +} + +async fn get_java_versions( + java_version: u8, + os: &str, + archive_type: &str, +) -> PolyResult { + 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::().await { + Ok(json) => json, + Err(err) => { + return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound( + err.to_string(), + ))) + } + }, + Err(err) => { + return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound( + err.to_string(), + ))) + } + }; + + if !response.is_array() || response.as_array().unwrap().is_empty() { + return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound( + "didn't get the expected java api result".to_string(), + ))); + } + + Ok(response) +} \ No newline at end of file diff --git a/src-tauri/src/game/minecraft.rs b/src-tauri/src/game/minecraft.rs new file mode 100644 index 00000000..6d1c474e --- /dev/null +++ b/src-tauri/src/game/minecraft.rs @@ -0,0 +1,156 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MinecraftManifest { + pub asset_index: AssetIndex, + pub downloads: Downloads, + #[serde(rename = "id")] + pub version: String, + pub java_version: JavaVersion, + pub libraries: Vec, + pub logging: Logging, + pub main_class: String, + pub minecraft_arguments: String, + pub release_time: String, + #[serde(rename = "type")] + pub release_type: ReleaseType, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum ReleaseType { + Snapshot, + Release, + OldBeta, + OldAlpha, +} + +impl Default for ReleaseType { + fn default() -> Self { ReleaseType::Release } +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AssetIndex { + pub id: String, + pub sha1: String, + pub url: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Downloads { + pub client: Client, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Client { + pub sha1: String, + pub url: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JavaVersion { + pub major_version: u8, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Library { + pub downloads: Downloads2, + pub name: String, + #[serde(default)] + pub rules: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub natives: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Downloads2 { + #[serde(skip_serializing_if = "Option::is_none")] + pub artifact: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub classifiers: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Artifact { + pub path: String, + pub sha1: String, + pub url: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Classifiers { + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "natives-osx")] + pub natives_osx: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "natives-linux")] + pub natives_linux: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "natives-windows")] + pub natives_windows: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Native { + pub path: String, + pub sha1: String, + pub url: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Rule { + pub action: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub os: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Os { + pub name: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Natives { + pub windows: Option, + pub linux: Option, + pub osx: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Logging { + pub client: Client2, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Client2 { + pub argument: String, + pub file: File, + #[serde(rename = "type")] + pub type_field: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct File { + pub id: String, + pub sha1: String, + pub url: String, +} diff --git a/src-tauri/src/game/mod.rs b/src-tauri/src/game/mod.rs index 80b79954..98d40578 100644 --- a/src-tauri/src/game/mod.rs +++ b/src-tauri/src/game/mod.rs @@ -1,83 +1,5 @@ -use serde::{Deserialize, Serialize}; -use tauri::{plugin::TauriPlugin, Manager}; -use tokio::sync::Mutex; - -use self::client::get_game_client; - pub mod client; -mod clients; - -// todo move to prisma -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -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(), - } - } -} - -// Tauri plugin for the game module -pub struct GameManagerState { - pub selected_client: Option>, -} - -unsafe impl Send for GameManagerState {} -unsafe impl Sync for GameManagerState {} -impl Default for GameManagerState { - fn default() -> Self { - GameManagerState { - selected_client: None, - } - } -} - -pub fn init() -> TauriPlugin { - tauri::plugin::Builder::new("game") - .setup(|app, _| { - app.manage(Mutex::new(GameManagerState::default())); - Ok(()) - }) - .invoke_handler(tauri::generate_handler![launch_game, set_selected_client]) - .build() -} - -#[tauri::command] -async fn launch_game(state: tauri::State<'_, Mutex>) -> Result<(), String> { - let state = state.lock().await; - let selected_client = match &state.selected_client { - Some(client) => client, - None => return Err("No client selected".into()), - }; - - if let Err(err) = selected_client.setup().await { - return Err(format!("An error has occurred during install: {}", err)); - } - - match selected_client.launch().await { - Ok(_) => Ok(()), - Err(e) => Err(format!("An error has occurred: {}", e)), - } -} - -// TODO: Implement setting client by UUID, as currently this is creating a new client and setting that -#[tauri::command] -async fn set_selected_client( - handle: tauri::AppHandle, - state: tauri::State<'_, Mutex>, - details: client::GameClientDetails, -) -> Result<(), String> { - let mut state = state.lock().await; - let client = get_game_client(handle, details); - state.selected_client = Some(client); - Ok(()) -} +pub mod java; +pub mod vanilla; +pub mod plugin; +pub mod minecraft; \ No newline at end of file diff --git a/src-tauri/src/game/plugin.rs b/src-tauri/src/game/plugin.rs new file mode 100644 index 00000000..5cbc08d2 --- /dev/null +++ b/src-tauri/src/game/plugin.rs @@ -0,0 +1,50 @@ +use tauri::{plugin::TauriPlugin, Manager, State}; +use tokio::sync::Mutex; +use uuid::Uuid; + +use super::client::{ClientManager, ClientType, Instance}; + +pub struct GameManagerState { + pub client_manager: ClientManager, +} + +unsafe impl Send for GameManagerState {} +unsafe impl Sync for GameManagerState {} + +pub fn init() -> TauriPlugin { + tauri::plugin::Builder::new("game") + .setup(|app, _| { + app.manage(Mutex::new(GameManagerState { + client_manager: ClientManager::new(app).expect("Failed to initialize client manager"), + })); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + create_instance, + get_instances, + ]) + .build() +} + +#[tauri::command] +async fn create_instance( + state: State<'_, Mutex>, + name: String, + version: String, + cover: Option, + group: Option, + client: ClientType +) -> Result { + let manager = &mut state.lock().await.client_manager; + + manager.create_instance(name, version, cover, group, client).await.or_else(|e| { + Err(e.to_string()) + }) +} + +#[tauri::command] +async fn get_instances(state: State<'_, Mutex>) -> Result, String> { + let manager = &mut state.lock().await.client_manager; + + Ok(manager.get_instances_owned()) +} diff --git a/src-tauri/src/game/vanilla.rs b/src-tauri/src/game/vanilla.rs new file mode 100644 index 00000000..aa754860 --- /dev/null +++ b/src-tauri/src/game/vanilla.rs @@ -0,0 +1,16 @@ +use std::collections::HashMap; + +use crate::{utils::http, PolyResult}; + +use super::minecraft::MinecraftManifest; + +pub async fn retrieve_versions() -> PolyResult> { + let map = HashMap::new(); + + Ok(map) +} + +pub async fn retrieve_version_manifest(url: String) -> PolyResult { + let manifest = http::create_client()?.get(url).send().await?.json::().await?; + Ok(manifest) +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 104cef2f..8a5105dc 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,6 +5,10 @@ pub mod constants; pub mod game; pub mod utils; +pub struct AppState { + // pub , +} + /// A standardized [`Error`] type that is used across the launcher #[derive(Debug, Error)] pub enum PolyError { @@ -33,24 +37,19 @@ pub enum PolyError { #[error("failed to manage writing and reading of files: {0}")] /// Wrapper around [`std::io::Error`] only to be used if a more defined error isn't available (e.g. when using `zip`) IOError(#[from] std::io::Error), - #[error("failed to query the database: {0}")] - /// Wrapper around [`prisma_client_rust::QueryError`] to handle DB query errors. - DBQueryError(#[from] prisma_client_rust::QueryError), - #[error("failed to establish a connection to the database: {0}")] - /// Wrapper around [`prisma_client_rust::NewClientError`] to handle errors occuring while programmatically connecting to Prisma. - DBConnectionError(#[from] prisma_client_rust::NewClientError), #[error("failed to establish a HTTP connection: {0}")] /// Wrapper around [`reqwest::Error`] to handle HTTP errors. HTTPError(#[from] tauri_plugin_http::reqwest::Error), #[error("failed to download java: {0}")] /// Wrapper around [`game::client::JavaDownloadError`] to handle Java executable downloading. - JavaError(#[from] game::client::JavaDownloadError), + JavaError(#[from] game::java::JavaDownloadError), /// Wrapper around [`zip::result::ZipError`] to handle zip errors. #[error("failed to manage zip files: {0}")] ZipError(#[from] zip::result::ZipError), #[error("failed to authenticate: {0}")] - // Wrapper around [`auth::AuthenticationError`] to handle authentication errors. + /// Wrapper around [`auth::AuthenticationError`] to handle authentication errors. AuthError(#[from] auth::AuthenticationError), + /// Wrapper around [`anyhow::Error`] to handle generic errors. #[error(transparent)] AnyhowError(#[from] anyhow::Error), } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 7113181e..0cc2b172 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -6,11 +6,12 @@ use std::error::Error; use polyfrost_launcher::{auth, game}; use tauri::{menu::Menu, App}; -fn main() { +#[tokio::main] +async fn main() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin(auth::init()) - .plugin(game::init()) + .plugin(game::plugin::init()) .menu(|handle| Menu::new(handle)) .setup(setup) .run(tauri::generate_context!()) diff --git a/src-tauri/src/utils/file.rs b/src-tauri/src/utils/file.rs index aed2019c..df9b5668 100644 --- a/src-tauri/src/utils/file.rs +++ b/src-tauri/src/utils/file.rs @@ -25,10 +25,10 @@ pub fn extract_archive(archive: &Path, dest: &Path) -> PolyResult<()> { pub fn extract_zip(archive: &Path, dest: &Path) -> PolyResult<()> { let file = File::open(archive).map_err(|err| PolyError::IOError(err))?; - let mut archive = zip::ZipArchive::new(file).map_err(|err| PolyError::ZipError(err)); + let archive = zip::ZipArchive::new(file).map_err(|err| PolyError::ZipError(err)); archive? .extract(dest) - .map_err(|err| PolyError::ZipError(err)); + .map_err(|err| PolyError::ZipError(err))?; Ok(()) } @@ -36,6 +36,6 @@ pub fn extract_tar_gz(archive: &Path, dest: &Path) -> PolyResult<()> { let file = File::open(archive).map_err(|err| PolyError::IOError(err))?; let tar_gz = flate2::read::GzDecoder::new(file); let mut archive = tar::Archive::new(tar_gz); - archive.unpack(dest).map_err(|err| PolyError::IOError(err)); + archive.unpack(dest).map_err(|err| PolyError::IOError(err))?; Ok(()) } diff --git a/src-tauri/src/utils/http.rs b/src-tauri/src/utils/http.rs index e9082c1a..89066a25 100644 --- a/src-tauri/src/utils/http.rs +++ b/src-tauri/src/utils/http.rs @@ -29,6 +29,6 @@ pub async fn download_file(url: &str, path: &Path) -> PolyResult<()> { .await .map_err(|err| PolyError::HTTPError(err))?, ); - io::copy(&mut content, &mut file).map_err(|err| PolyError::IOError(err)); + io::copy(&mut content, &mut file).map_err(|err| PolyError::IOError(err))?; Ok(()) } diff --git a/src/ui/pages/Browser.tsx b/src/ui/pages/Browser.tsx index 836793e9..b2e0577d 100644 --- a/src/ui/pages/Browser.tsx +++ b/src/ui/pages/Browser.tsx @@ -18,24 +18,28 @@ function BrowserPage() { + + );