diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 7b1254f..63ee06a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -5,10 +5,10 @@ use std::{ io::{Cursor, Read}, - path::PathBuf, + path::{Path, PathBuf}, }; -use anyhow::anyhow; +use anyhow::{anyhow, Context}; use mrpack::PackDependency; use sha2::Digest; use tauri::{ @@ -23,7 +23,8 @@ fn main() { .invoke_handler(tauri::generate_handler![ install_mrpack, get_installed_metadata, - show_profile_dir_selector + show_profile_dir_selector, + is_launcher_installed ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); @@ -44,6 +45,18 @@ async fn show_profile_dir_selector() -> Option { recv.await.ok().flatten() } +#[tauri::command] +async fn is_launcher_installed() -> bool { + if let Ok(path) = get_launcher_path() + .await + .map(|path| path.join("launcher_profiles.json")) + { + tokio::fs::try_exists(path).await.unwrap_or(false) + } else { + false + } +} + async fn get_launcher_path() -> anyhow::Result { if let Ok(path) = std::env::var("PAIGALDAJA_LAUNCHER_PATH") { return Ok(PathBuf::from(path)); @@ -96,7 +109,7 @@ async fn install_fabriclike( let profile_json_path = profile_dir.join(format!("{}.json", &profile_name)); let profile_jar_path = profile_dir.join(format!("{}.jar", &profile_name)); if !profile_dir.is_dir() { - tokio::fs::create_dir(&profile_dir).await?; + tokio::fs::create_dir_all(&profile_dir).await?; } tokio::fs::write( &profile_json_path, @@ -212,6 +225,43 @@ async fn canonicalize_profile_path(profile_dir: &Option) -> anyhow::Resu }) } +async fn try_download( + app_handle: &tauri::AppHandle, + client: &tauri::api::http::Client, + url: &str, + path: &str, + expected_hash: &[u8], + profile_base_path: &Path, +) -> anyhow::Result<()> { + let request = HttpRequestBuilder::new("GET", url)? + .response_type(ResponseType::Binary) + .header( + "User-Agent", + format!( + "Paigaldaja/{} (+https://github.com/Fabulously-Optimized/vanilla-installer-rust)", + app_handle.package_info().version + ), + )?; + let resp = client.send(request).await?; + if resp.status() != StatusCode::OK { + return Err(anyhow!("Status code was not 200, but {}", resp.status())); + } + let blob = resp.bytes().await?; + let hash = std::convert::Into::<[u8; 64]>::into(sha2::Sha512::digest(&blob.data)); + if &hash != expected_hash { + return Err(anyhow!( + "Wrong hash: got {}, expected {}", + hex::encode(hash), + hex::encode(expected_hash) + )); + } + if let Some(parent) = PathBuf::from(path).parent() { + tokio::fs::create_dir_all(profile_base_path.join(parent)).await?; + } + tokio::fs::write(profile_base_path.join(PathBuf::from(&path)), blob.data).await?; + Ok(()) +} + async fn install_mrpack_inner( app_handle: tauri::AppHandle, url: String, @@ -221,7 +271,9 @@ async fn install_mrpack_inner( profile_dir: Option, extra_metadata: serde_json::Value, ) -> anyhow::Result<()> { - let profile_base_path = canonicalize_profile_path(&profile_dir).await?; + let profile_base_path = canonicalize_profile_path(&profile_dir) + .await + .context("Could not determine profile directory")?; let _ = app_handle.emit_all("install:progress", ("clean_old", "start")); if let Some(files) = get_installed_files(&profile_dir).await { for file in files { @@ -233,7 +285,8 @@ async fn install_mrpack_inner( let _ = app_handle.emit_all("install:progress", ("load_pack", "start")); let mut written_files = vec![]; let client = ClientBuilder::new().build().unwrap(); - let request = HttpRequestBuilder::new("GET", url)? + let request = HttpRequestBuilder::new("GET", url) + .context("Is the .mrpack URL invalid?")? .response_type(ResponseType::Binary) .header( "User-Agent", @@ -241,14 +294,28 @@ async fn install_mrpack_inner( "Paigaldaja/{} (+https://github.com/Fabulously-Optimized/vanilla-installer-rust)", app_handle.package_info().version ), - )?; - let response = client.send(request).await?; + ) + .context("Could not set request metadata")?; + let response = client + .send(request) + .await + .context("Failed to fetch modpack data")?; if response.status() != StatusCode::OK { return Err(anyhow!("Server did not respond with 200")); } - let bytes = response.bytes().await?.data; - let mut mrpack = zip::ZipArchive::new(Cursor::new(bytes))?; - let index: mrpack::PackIndex = serde_json::from_reader(mrpack.by_name("modrinth.index.json")?)?; + let bytes = response + .bytes() + .await + .context("Failed to fetch modpack data")? + .data; + let mut mrpack = + zip::ZipArchive::new(Cursor::new(bytes)).context("Failed to parse modpack file")?; + let index: mrpack::PackIndex = serde_json::from_reader( + mrpack + .by_name("modrinth.index.json") + .context("No modrinth.index.json in mrpack?")?, + ) + .context("modrinth.index.json is invalid")?; if index.format_version != 1 { return Err(anyhow!("Unknown format version {}", index.format_version)); } @@ -280,34 +347,19 @@ async fn install_mrpack_inner( )?; let mut success = false; for url in file.downloads { - if let Ok(resp) = client - .send( - HttpRequestBuilder::new("GET", url)? - .response_type(ResponseType::Binary) - .header("User-Agent", format!("Paigaldaja/{} (+https://github.com/Fabulously-Optimized/vanilla-installer-rust)", app_handle.package_info().version))? - ) - .await + if let Ok(()) = try_download( + &app_handle, + &client, + &url, + &file.path, + &hash, + &profile_base_path, + ) + .await { - if resp.status() == StatusCode::OK { - if let Ok(blob) = resp.bytes().await { - if std::convert::Into::<[u8; 64]>::into(sha2::Sha512::digest(&blob.data)) - .as_ref() - == hash - { - if let Some(parent) = PathBuf::from(&file.path).parent() { - tokio::fs::create_dir_all(profile_base_path.join(parent)).await?; - } - tokio::fs::write( - profile_base_path.join(PathBuf::from(&file.path)), - blob.data, - ) - .await?; - written_files.push(PathBuf::from(&file.path)); - success = true; - break; - } - } - } + written_files.push(PathBuf::from(&file.path)); + success = true; + break; } } if !success { @@ -354,12 +406,24 @@ async fn install_mrpack_inner( } Ok(None) } - if let Some((rel_path, buf)) = complex_helper_function(&mut mrpack, &filename)? { + if let Some((rel_path, buf)) = complex_helper_function(&mut mrpack, &filename) + .context("Failed to read configuration file; corrupted mrpack?")? + { let path = profile_base_path.join(&rel_path); if let Some(parent) = path.parent() { - tokio::fs::create_dir_all(parent).await?; + tokio::fs::create_dir_all(parent).await.with_context(|| { + format!( + "Failed to create directories for configuration file {}", + rel_path.to_string_lossy() + ) + })?; } - tokio::fs::write(&path, buf).await?; + tokio::fs::write(&path, buf).await.with_context(|| { + format!( + "Failed to write configuration file {}", + rel_path.to_string_lossy() + ) + })?; written_files.push(rel_path); } } @@ -379,20 +443,31 @@ async fn install_mrpack_inner( mc_version, fabric_version ); version_name = format!("fabric-loader-{}-{}", fabric_version, mc_version); - install_fabriclike(&app_handle, &client, profile_url, &version_name).await?; + install_fabriclike(&app_handle, &client, profile_url, &version_name) + .await + .context("Failed to install Fabric")?; } else if let Some(quilt_version) = index.dependencies.get(&PackDependency::QuiltLoader) { let profile_url = format!( "https://meta.quiltmc.org/v3/versions/loader/{}/{}/profile/json", mc_version, quilt_version ); version_name = format!("quilt-loader-{}-{}", quilt_version, mc_version); - install_fabriclike(&app_handle, &client, profile_url, &version_name).await?; + install_fabriclike(&app_handle, &client, profile_url, &version_name) + .await + .context("Failed to install Quilt")?; } let _ = app_handle.emit_all("install:progress", ("install_loader", "complete")); let _ = app_handle.emit_all("install:progress", ("add_profile", "start")); - let profiles_path = get_launcher_path().await?.join("launcher_profiles.json"); - let mut profiles: serde_json::Value = - serde_json::from_str(&tokio::fs::read_to_string(&profiles_path).await?)?; + let profiles_path = get_launcher_path() + .await + .context("Could not determine profile directory")? + .join("launcher_profiles.json"); + let mut profiles: serde_json::Value = serde_json::from_str( + &tokio::fs::read_to_string(&profiles_path) + .await + .context("Failed to read launcher profiles")?, + ) + .context("Failed to parse launcher profiles")?; let profile_base_path_string = profile_base_path.to_string_lossy(); set_or_create_profile( &mut profiles, @@ -407,7 +482,9 @@ async fn install_mrpack_inner( }, ) .ok_or(anyhow!("Could not create launcher profile"))?; - tokio::fs::write(profiles_path, serde_json::to_string(&profiles)?).await?; + tokio::fs::write(profiles_path, serde_json::to_string(&profiles)?) + .await + .context("Failed to write launcher profiles")?; tokio::fs::write( profile_base_path.join("paigaldaja_meta.json"), serde_json::to_string(&serde_json::json!({ @@ -415,7 +492,8 @@ async fn install_mrpack_inner( "metadata": extra_metadata }))?, ) - .await?; + .await + .context("Failed to write installer metadata")?; let _ = app_handle.emit_all("install:progress", ("add_profile", "complete")); Ok(()) } diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts index 6ae1c2b..dc3a702 100644 --- a/src/lib/i18n.ts +++ b/src/lib/i18n.ts @@ -2,35 +2,32 @@ import { locale as getLocale } from '@tauri-apps/api/os'; import { langs } from './lang'; let locale = navigator.language.split('-')[0]; -const defaultLocale = "en"; +const defaultLocale = 'en'; -getLocale().then(systemLocale => { - if (systemLocale) - locale = systemLocale.split('-')[0] -}) +getLocale().then((systemLocale) => { + if (systemLocale) locale = systemLocale.split('-')[0]; +}); export function trans(id: string, data?: Record) { - if (locale && langs[locale] && langs[locale][id]) { - let text = langs[locale][id]; - for (const key in data) { - if (Object.prototype.hasOwnProperty.call(data, key)) { - const element = data[key]; - if (element !== undefined) - text = text.replaceAll(`{{${key}}}`, element.toString()) - } - } - return text - } - if (langs[defaultLocale] && langs[defaultLocale][id]) { - let text = langs[defaultLocale][id]; - for (const key in data) { - if (Object.prototype.hasOwnProperty.call(data, key)) { - const element = data[key]; - if (element !== undefined) - text = text.replaceAll(`{{${key}}}`, element.toString()) - } - } - return text - } - return id -} \ No newline at end of file + if (locale && langs[locale] && langs[locale][id]) { + let text = langs[locale][id]; + for (const key in data) { + if (Object.prototype.hasOwnProperty.call(data, key)) { + const element = data[key]; + if (element !== undefined) text = text.replaceAll(`{{${key}}}`, element.toString()); + } + } + return text; + } + if (langs[defaultLocale] && langs[defaultLocale][id]) { + let text = langs[defaultLocale][id]; + for (const key in data) { + if (Object.prototype.hasOwnProperty.call(data, key)) { + const element = data[key]; + if (element !== undefined) text = text.replaceAll(`{{${key}}}`, element.toString()); + } + } + return text; + } + return id; +} diff --git a/src/lib/installer.ts b/src/lib/installer.ts index 785bd05..ee61e9b 100644 --- a/src/lib/installer.ts +++ b/src/lib/installer.ts @@ -34,3 +34,7 @@ export async function get_installed_metadata(profile_dir: string | undefined): P export async function show_profile_dir_selector(): Promise { return await invoke('show_profile_dir_selector'); } + +export async function is_launcher_installed(): Promise { + return await invoke('is_launcher_installed'); +} diff --git a/src/lib/lang/en.json b/src/lib/lang/en.json index 5bd037c..3aa4993 100644 --- a/src/lib/lang/en.json +++ b/src/lib/lang/en.json @@ -1,23 +1,27 @@ { - "progress.clean_old": "Cleaning up old files", - "progress.load_pack": "Downloading modpack", - "progress.download_files": "Downloading mods", - "progress.download_file": "Downloading {{file}} ({{idx}}/{{total}})", - "progress.extract_overrides": "Extracting configuration files", - "progress.install_loader": "Installing mod loader", - "progress.add_profile": "Creating launcher installation", - "ui.loading-versions": "Loading versions...", - "ui.version-tooltip": "Vanilla Installer allows easy installation of all supported versions of Fabulously Optimized. For outdated versions, use a different launcher or install method.", - "ui.isolate-profile": "Use a different .minecraft directory for this version?", - "ui.profile-dir-placeholder": "Leave blank to let the installer decide", - "ui.profile-dir-browse-label": "Browse folders", - "ui.install-button": "Install!", - "ui.installing": "Installing...", - "ui.installed": "Fabulously Optimized is installed!", - "ui.install-error": "An error occurred while installing Fabulously Optimized: {{errorMessage}}", - "ui.downgrade-msg": "You are attempting to downgrade the Minecraft version. This is NOT SUPPORTED by Mojang or Fabulously Optimized and it may cause world corruption or crashes.
If you want to do this safely, you should backup mods, config and saves folders to a different location and delete them from your .minecraft folder.", - "ui.confirm-downgrade": "Yes, I want to downgrade FO.", - "ui.downgrade-cancel": "Back", - "ui.downgrade-continue": "Continue", - "ui.confirm-exit": "Fabulously Optimized is installing. Are you sure you want to exit?" + "progress.clean_old": "Cleaning up old files", + "progress.load_pack": "Downloading modpack", + "progress.download_files": "Downloading mods", + "progress.download_file": "Downloading {{file}} ({{idx}}/{{total}})", + "progress.extract_overrides": "Extracting configuration files", + "progress.install_loader": "Installing mod loader", + "progress.add_profile": "Creating launcher installation", + "ui.loading-versions": "Loading versions...", + "ui.version-tooltip": "Vanilla Installer allows easy installation of all supported versions of Fabulously Optimized. For outdated versions, use a different launcher or install method.", + "ui.isolate-profile": "Use a different .minecraft directory for this version?", + "ui.profile-dir-placeholder": "Leave blank to let the installer decide", + "ui.profile-dir-browse-label": "Browse folders", + "ui.install-button": "Install!", + "ui.installing": "Installing...", + "ui.installed": "Fabulously Optimized is installed!", + "ui.install-error": "An error occurred while installing Fabulously Optimized: {{errorMessage}}", + "ui.downgrade-msg": "You are attempting to downgrade the Minecraft version. This is NOT SUPPORTED by Mojang or Fabulously Optimized and it may cause world corruption or crashes.
If you want to do this safely, you should backup mods, config and saves folders to a different location and delete them from your .minecraft folder.", + "ui.confirm-downgrade": "Yes, I want to downgrade FO.", + "ui.downgrade-cancel": "Back", + "ui.downgrade-continue": "Continue", + "ui.confirm-exit": "Fabulously Optimized is installing. Are you sure you want to exit?", + "ui.no-launcher": "The installer was unable to detect a Minecraft Launcher installation. The installer cannot continue. Install the launcher or launch it once to proceed.", + "ui.no-launcher-back": "Back", + "ui.no-launcher-continue": "Continue", + "ui.back-home": "Go back to main page" } diff --git a/src/lib/lang/et.json b/src/lib/lang/et.json index a0a3475..12ec0ef 100644 --- a/src/lib/lang/et.json +++ b/src/lib/lang/et.json @@ -1,23 +1,23 @@ { - "progress.clean_old": "Vanade failide tühjendamine", - "progress.load_pack": "Modipaki allalaadimine", - "progress.download_files": "Modide allalaadimine", - "progress.download_file": "{{file}} allalaadimine ({{idx}}/{{total}})", - "progress.extract_overrides": "Seadistusfailide ekstraktimine", - "progress.install_loader": "Modilaaduri paigaldamine", - "progress.add_profile": "Launcheri paigalduse loomine", - "ui.loading-versions": "Versioonide laadimine...", - "ui.version-tooltip": "Vanilla Installer lubab kerget paigaldust kõigi Fabulously Optimized toetatud versioonide jaoks. Aegunud versioonide jaoks kasuta teist käivitajat või meetodit.", - "ui.isolate-profile": "Kasutada selle versiooni jaoks teist .minecraft kausta?", - "ui.profile-dir-placeholder": "Jäta tühjaks, kui soovid et paigaldaja ise valib", - "ui.profile-dir-browse-label": "Sirvi kaustu", - "ui.install-button": "Paigalda!", - "ui.installing": "Paigaldamine...", - "ui.installed": "Fabulously Optimized on paigaldatud!", - "ui.install-error": "Fabulously Optimized paigaldamisel esines viga: {{errorMessage}}", - "ui.downgrade-msg": "Proovid Minecrafti versiooni alandada. See EI OLE TOETATUD Mojangi ega Fabulously Optimized poolt ning see võib põhjustada maailmade korrumptsiooni või krahhe.
Kui soovid seda turvaliselt teha, peaksid varundama kaustad mods, config ja saves teise kohta ning kustutama need oma .minecraft kaustast.", - "ui.confirm-downgrade": "Jah, ma soovin FO versiooni alandada.", - "ui.downgrade-cancel": "Tagasi", - "ui.downgrade-continue": "Jätka", - "ui.confirm-exit": "Fabulously Optimized on paigaldamisel. Kas soovid kindlasti väljuda?" + "progress.clean_old": "Vanade failide tühjendamine", + "progress.load_pack": "Modipaki allalaadimine", + "progress.download_files": "Modide allalaadimine", + "progress.download_file": "{{file}} allalaadimine ({{idx}}/{{total}})", + "progress.extract_overrides": "Seadistusfailide ekstraktimine", + "progress.install_loader": "Modilaaduri paigaldamine", + "progress.add_profile": "Launcheri paigalduse loomine", + "ui.loading-versions": "Versioonide laadimine...", + "ui.version-tooltip": "Vanilla Installer lubab kerget paigaldust kõigi Fabulously Optimized toetatud versioonide jaoks. Aegunud versioonide jaoks kasuta teist käivitajat või meetodit.", + "ui.isolate-profile": "Kasutada selle versiooni jaoks teist .minecraft kausta?", + "ui.profile-dir-placeholder": "Jäta tühjaks, kui soovid et paigaldaja ise valib", + "ui.profile-dir-browse-label": "Sirvi kaustu", + "ui.install-button": "Paigalda!", + "ui.installing": "Paigaldamine...", + "ui.installed": "Fabulously Optimized on paigaldatud!", + "ui.install-error": "Fabulously Optimized paigaldamisel esines viga: {{errorMessage}}", + "ui.downgrade-msg": "Proovid Minecrafti versiooni alandada. See EI OLE TOETATUD Mojangi ega Fabulously Optimized poolt ning see võib põhjustada maailmade korrumptsiooni või krahhe.
Kui soovid seda turvaliselt teha, peaksid varundama kaustad mods, config ja saves teise kohta ning kustutama need oma .minecraft kaustast.", + "ui.confirm-downgrade": "Jah, ma soovin FO versiooni alandada.", + "ui.downgrade-cancel": "Tagasi", + "ui.downgrade-continue": "Jätka", + "ui.confirm-exit": "Fabulously Optimized on paigaldamisel. Kas soovid kindlasti väljuda?" } diff --git a/src/lib/lang/index.ts b/src/lib/lang/index.ts index 2698d45..8a9a5ea 100644 --- a/src/lib/lang/index.ts +++ b/src/lib/lang/index.ts @@ -1,7 +1,7 @@ -import en from "./en.json"; -import et from "./et.json"; +import en from './en.json'; +import et from './et.json'; export const langs: Record> = { - en, - et + en, + et }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 487b2f0..81fe272 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -2,10 +2,11 @@ import { get_installed_metadata, install_mrpack, + is_launcher_installed, show_profile_dir_selector } from '$lib/installer'; import { get_project, list_versions, type Version } from '$lib/modrinth'; - import { trans } from '$lib/i18n'; + import { trans } from '$lib/i18n'; import { listen } from '@tauri-apps/api/event'; import { appWindow } from '@tauri-apps/api/window'; import { confirm } from '@tauri-apps/api/dialog'; @@ -59,14 +60,16 @@ }); function confirmUnload(ev: BeforeUnloadEvent) { ev.preventDefault(); - return (ev.returnValue = trans("ui.confirm-exit")); + return (ev.returnValue = trans('ui.confirm-exit')); } async function installPack() { + if (!(await is_launcher_installed())) { + state = 'noLauncher'; + return; + } addEventListener('beforeunload', confirmUnload); const unlisten = await appWindow.onCloseRequested(async (ev) => { - const confirmed = await confirm( - trans("ui.confirm-exit") - ); + const confirmed = await confirm(trans('ui.confirm-exit')); if (!confirmed) { // user did not confirm closing the window; let's prevent it ev.preventDefault(); @@ -136,8 +139,13 @@ versions = featured_versions; selected = release_versions[0].id; }); - let state: 'preInstall' | 'installing' | 'postInstall' | 'error' | 'confirmDowngrade' = - 'preInstall'; + let state: + | 'preInstall' + | 'installing' + | 'postInstall' + | 'error' + | 'confirmDowngrade' + | 'noLauncher' = 'preInstall'; let installProgress = ''; $: totalSteps = totalMods + 4; let currentStep = 0; @@ -163,6 +171,12 @@ theme = themes[(idx + 1) % 3]; } + function reset_state() { + currentStep = 0; + totalMods = Infinity; + state = 'preInstall'; + } + function openHelp() { open('https://fabulously-optimized.gitbook.io/modpack/readme/version-support'); } @@ -213,9 +227,7 @@ id="isolate-profile" class="checkbox" /> - + {#if isolateProfile}
@@ -226,7 +238,11 @@ placeholder={trans('ui.profile-dir-placeholder')} class="input-box" /> -
@@ -244,11 +260,19 @@ {:else if state == 'postInstall'}
{trans('ui.installed')}
+ {:else if state == 'error'}
{@html trans('ui.install-error', { errorMessage })}
- {:else} + + {:else if state == 'confirmDowngrade'}
{@html trans('ui.downgrade-msg')}
@@ -270,6 +294,19 @@ on:click={installPack} disabled={!confirmDowngrade}>{trans('ui.downgrade-continue')} + {:else if state == 'noLauncher'} +
+ {trans('ui.no-launcher')} +
+ + {/if}