Upgrading (#354)

* fixed no download bug

* draft

* Working version

* minor improvements

* cicd fix

* merge conflicts

* fixed major merge confusion

* more conflicts, reformatting

* fixed random bugs found

* added second repair option to avoid confusion
This commit is contained in:
Wyatt Verchere
2023-07-26 20:33:03 -07:00
committed by GitHub
parent 70aaf6eef9
commit 21ae310f63
24 changed files with 817 additions and 306 deletions

View File

@@ -31,20 +31,19 @@ impl Logs {
#[tracing::instrument] #[tracing::instrument]
pub async fn get_logs( pub async fn get_logs(
profile_uuid: uuid::Uuid, profile_path: ProfilePathId,
clear_contents: Option<bool>, clear_contents: Option<bool>,
) -> crate::Result<Vec<Logs>> { ) -> crate::Result<Vec<Logs>> {
let state = State::get().await?; let state = State::get().await?;
let profile_path = if let Some(p) = let profile_path =
crate::profile::get_by_uuid(profile_uuid, None).await? if let Some(p) = crate::profile::get(&profile_path, None).await? {
{ p.profile_id()
p.profile_id() } else {
} else { return Err(crate::ErrorKind::UnmanagedProfileError(
return Err(crate::ErrorKind::UnmanagedProfileError( profile_path.to_string(),
profile_uuid.to_string(), )
) .into());
.into()); };
};
let logs_folder = state.directories.profile_logs_dir(&profile_path).await?; let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;
let mut logs = Vec::new(); let mut logs = Vec::new();
@@ -77,19 +76,18 @@ pub async fn get_logs(
#[tracing::instrument] #[tracing::instrument]
pub async fn get_logs_by_datetime( pub async fn get_logs_by_datetime(
profile_uuid: uuid::Uuid, profile_path: ProfilePathId,
datetime_string: String, datetime_string: String,
) -> crate::Result<Logs> { ) -> crate::Result<Logs> {
let profile_path = if let Some(p) = let profile_path =
crate::profile::get_by_uuid(profile_uuid, None).await? if let Some(p) = crate::profile::get(&profile_path, None).await? {
{ p.profile_id()
p.profile_id() } else {
} else { return Err(crate::ErrorKind::UnmanagedProfileError(
return Err(crate::ErrorKind::UnmanagedProfileError( profile_path.to_string(),
profile_uuid.to_string(), )
) .into());
.into()); };
};
Ok(Logs { Ok(Logs {
output: Some( output: Some(
get_output_by_datetime(&profile_path, &datetime_string).await?, get_output_by_datetime(&profile_path, &datetime_string).await?,
@@ -111,17 +109,16 @@ pub async fn get_output_by_datetime(
} }
#[tracing::instrument] #[tracing::instrument]
pub async fn delete_logs(profile_uuid: uuid::Uuid) -> crate::Result<()> { pub async fn delete_logs(profile_path: ProfilePathId) -> crate::Result<()> {
let profile_path = if let Some(p) = let profile_path =
crate::profile::get_by_uuid(profile_uuid, None).await? if let Some(p) = crate::profile::get(&profile_path, None).await? {
{ p.profile_id()
p.profile_id() } else {
} else { return Err(crate::ErrorKind::UnmanagedProfileError(
return Err(crate::ErrorKind::UnmanagedProfileError( profile_path.to_string(),
profile_uuid.to_string(), )
) .into());
.into()); };
};
let state = State::get().await?; let state = State::get().await?;
let logs_folder = state.directories.profile_logs_dir(&profile_path).await?; let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;
@@ -139,19 +136,18 @@ pub async fn delete_logs(profile_uuid: uuid::Uuid) -> crate::Result<()> {
#[tracing::instrument] #[tracing::instrument]
pub async fn delete_logs_by_datetime( pub async fn delete_logs_by_datetime(
profile_uuid: uuid::Uuid, profile_path: ProfilePathId,
datetime_string: &str, datetime_string: &str,
) -> crate::Result<()> { ) -> crate::Result<()> {
let profile_path = if let Some(p) = let profile_path =
crate::profile::get_by_uuid(profile_uuid, None).await? if let Some(p) = crate::profile::get(&profile_path, None).await? {
{ p.profile_id()
p.profile_id() } else {
} else { return Err(crate::ErrorKind::UnmanagedProfileError(
return Err(crate::ErrorKind::UnmanagedProfileError( profile_path.to_string(),
profile_uuid.to_string(), )
) .into());
.into()); };
};
let state = State::get().await?; let state = State::get().await?;
let logs_folder = state.directories.profile_logs_dir(&profile_path).await?; let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;

View File

@@ -7,7 +7,6 @@ pub mod metadata;
pub mod pack; pub mod pack;
pub mod process; pub mod process;
pub mod profile; pub mod profile;
pub mod profile_create;
pub mod safety; pub mod safety;
pub mod settings; pub mod settings;
pub mod tags; pub mod tags;
@@ -26,8 +25,8 @@ pub mod prelude {
data::*, data::*,
event::CommandPayload, event::CommandPayload,
jre, metadata, pack, process, jre, metadata, pack, process,
profile::{self, Profile}, profile::{self, create, Profile},
profile_create, settings, settings,
state::JavaGlobals, state::JavaGlobals,
state::{ProfilePathId, ProjectPathId}, state::{ProfilePathId, ProjectPathId},
util::{ util::{

View File

@@ -199,7 +199,7 @@ async fn import_atlauncher_unmanaged(
let game_version = atinstance.id; let game_version = atinstance.id;
let loader_version = if mod_loader != ModLoader::Vanilla { let loader_version = if mod_loader != ModLoader::Vanilla {
crate::profile_create::get_loader_version_from_loader( crate::profile::create::get_loader_version_from_loader(
game_version.clone(), game_version.clone(),
mod_loader, mod_loader,
Some(atinstance.launcher.loader_version.version.clone()), Some(atinstance.launcher.loader_version.version.clone()),

View File

@@ -105,7 +105,7 @@ pub async fn import_curseforge(
let mod_loader = mod_loader.unwrap_or(ModLoader::Vanilla); let mod_loader = mod_loader.unwrap_or(ModLoader::Vanilla);
let loader_version = if mod_loader != ModLoader::Vanilla { let loader_version = if mod_loader != ModLoader::Vanilla {
crate::profile_create::get_loader_version_from_loader( crate::profile::create::get_loader_version_from_loader(
game_version.clone(), game_version.clone(),
mod_loader, mod_loader,
loader_version, loader_version,

View File

@@ -75,7 +75,7 @@ pub async fn import_gdlauncher(
let loader_version = config.loader.loader_version; let loader_version = config.loader.loader_version;
let loader_version = if mod_loader != ModLoader::Vanilla { let loader_version = if mod_loader != ModLoader::Vanilla {
crate::profile_create::get_loader_version_from_loader( crate::profile::create::get_loader_version_from_loader(
game_version.clone(), game_version.clone(),
mod_loader, mod_loader,
loader_version, loader_version,

View File

@@ -38,8 +38,10 @@ pub struct MMCInstance {
#[serde(deserialize_with = "deserialize_optional_bool")] #[serde(deserialize_with = "deserialize_optional_bool")]
pub managed_pack: Option<bool>, pub managed_pack: Option<bool>,
#[serde(rename = "ManagedPackID")]
pub managed_pack_id: Option<String>, pub managed_pack_id: Option<String>,
pub managed_pack_type: Option<MMCManagedPackType>, pub managed_pack_type: Option<MMCManagedPackType>,
#[serde(rename = "ManagedPackVersionID")]
pub managed_pack_version_id: Option<String>, pub managed_pack_version_id: Option<String>,
pub managed_pack_version_name: Option<String>, pub managed_pack_version_name: Option<String>,

View File

@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
prelude::ProfilePathId, prelude::ProfilePathId,
state::Profiles,
util::{fetch, io}, util::{fetch, io},
}; };
@@ -112,6 +113,10 @@ pub async fn import_instance(
.into()); .into());
} }
} }
// Check existing managed packs for potential updates
tokio::task::spawn(Profiles::update_modrinth_versions());
tracing::debug!("Completed import."); tracing::debug!("Completed import.");
Ok(()) Ok(())
} }

View File

@@ -119,6 +119,7 @@ impl Default for CreatePackProfile {
} }
} }
#[derive(Clone)]
pub struct CreatePack { pub struct CreatePack {
pub file: bytes::Bytes, pub file: bytes::Bytes,
pub description: CreatePackDescription, pub description: CreatePackDescription,
@@ -337,7 +338,7 @@ pub async fn set_profile_information(
let mod_loader = mod_loader.unwrap_or(ModLoader::Vanilla); let mod_loader = mod_loader.unwrap_or(ModLoader::Vanilla);
let loader_version = if mod_loader != ModLoader::Vanilla { let loader_version = if mod_loader != ModLoader::Vanilla {
crate::profile_create::get_loader_version_from_loader( crate::profile::create::get_loader_version_from_loader(
game_version.clone(), game_version.clone(),
mod_loader, mod_loader,
loader_version.cloned(), loader_version.cloned(),

View File

@@ -6,8 +6,9 @@ use crate::pack::install_from::{
set_profile_information, EnvType, PackFile, PackFileHash, set_profile_information, EnvType, PackFile, PackFileHash,
}; };
use crate::prelude::ProfilePathId; use crate::prelude::ProfilePathId;
use crate::state::SideType; use crate::state::{ProfileInstallStage, Profiles, SideType};
use crate::util::fetch::{fetch_mirrors, write}; use crate::util::fetch::{fetch_mirrors, write};
use crate::util::io;
use crate::State; use crate::State;
use async_zip::tokio::read::seek::ZipFileReader; use async_zip::tokio::read::seek::ZipFileReader;
@@ -19,11 +20,14 @@ use super::install_from::{
CreatePackLocation, PackFormat, CreatePackLocation, PackFormat,
}; };
/// Install a pack
/// Wrapper around install_pack_files that generates a pack creation description, and
/// attempts to install the pack files. If it fails, it will remove the profile (fail safely)
/// Install a modpack from a mrpack file (a modrinth .zip format) /// Install a modpack from a mrpack file (a modrinth .zip format)
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
pub async fn install_zipped_mrpack( pub async fn install_zipped_mrpack(
location: CreatePackLocation, location: CreatePackLocation,
profile: ProfilePathId, profile_path: ProfilePathId,
) -> crate::Result<ProfilePathId> { ) -> crate::Result<ProfilePathId> {
// Get file from description // Get file from description
let create_pack: CreatePack = match location { let create_pack: CreatePack = match location {
@@ -34,254 +38,355 @@ pub async fn install_zipped_mrpack(
icon_url, icon_url,
} => { } => {
generate_pack_from_version_id( generate_pack_from_version_id(
project_id, version_id, title, icon_url, profile, project_id,
version_id,
title,
icon_url,
profile_path.clone(),
) )
.await? .await?
} }
CreatePackLocation::FromFile { path } => { CreatePackLocation::FromFile { path } => {
generate_pack_from_file(path, profile).await? generate_pack_from_file(path, profile_path.clone()).await?
} }
}; };
// Install pack files, and if it fails, fail safely by removing the profile
let result = install_zipped_mrpack_files(create_pack).await;
// Check existing managed packs for potential updates
tokio::task::spawn(Profiles::update_modrinth_versions());
match result {
Ok(profile) => Ok(profile),
Err(err) => {
let _ = crate::api::profile::remove(&profile_path).await;
Err(err)
}
}
}
/// Install all pack files from a description
/// Does not remove the profile if it fails
#[theseus_macros::debug_pin]
pub async fn install_zipped_mrpack_files(
create_pack: CreatePack,
) -> crate::Result<ProfilePathId> {
let state = &State::get().await?;
let file = create_pack.file; let file = create_pack.file;
let description = create_pack.description.clone(); // make a copy for profile edit function let description = create_pack.description.clone(); // make a copy for profile edit function
let icon = create_pack.description.icon; let icon = create_pack.description.icon;
let project_id = create_pack.description.project_id; let project_id = create_pack.description.project_id;
let version_id = create_pack.description.version_id; let version_id = create_pack.description.version_id;
let existing_loading_bar = create_pack.description.existing_loading_bar; let existing_loading_bar = create_pack.description.existing_loading_bar;
let profile = create_pack.description.profile_path; let profile_path = create_pack.description.profile_path;
let state = &State::get().await?; let reader: Cursor<&bytes::Bytes> = Cursor::new(&file);
let result = async { // Create zip reader around file
let reader: Cursor<&bytes::Bytes> = Cursor::new(&file); let mut zip_reader = ZipFileReader::new(reader).await.map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed to read input modpack zip".to_string(),
))
})?;
// Create zip reader around file // Extract index of modrinth.index.json
let mut zip_reader = let zip_index_option = zip_reader
ZipFileReader::new(reader).await.map_err(|_| { .file()
crate::Error::from(crate::ErrorKind::InputError( .entries()
"Failed to read input modpack zip".to_string(), .iter()
)) .position(|f| f.entry().filename() == "modrinth.index.json");
})?; if let Some(zip_index) = zip_index_option {
let mut manifest = String::new();
// Extract index of modrinth.index.json let entry = zip_reader
let zip_index_option = zip_reader
.file() .file()
.entries() .entries()
.iter() .get(zip_index)
.position(|f| f.entry().filename() == "modrinth.index.json"); .unwrap()
if let Some(zip_index) = zip_index_option { .entry()
let mut manifest = String::new(); .clone();
let entry = zip_reader let mut reader = zip_reader.entry(zip_index).await?;
.file() reader.read_to_string_checked(&mut manifest, &entry).await?;
.entries()
.get(zip_index)
.unwrap()
.entry()
.clone();
let mut reader = zip_reader.entry(zip_index).await?;
reader.read_to_string_checked(&mut manifest, &entry).await?;
let pack: PackFormat = serde_json::from_str(&manifest)?; let pack: PackFormat = serde_json::from_str(&manifest)?;
if &*pack.game != "minecraft" { if &*pack.game != "minecraft" {
return Err(crate::ErrorKind::InputError( return Err(crate::ErrorKind::InputError(
"Pack does not support Minecraft".to_string(), "Pack does not support Minecraft".to_string(),
)
.into());
}
// Sets generated profile attributes to the pack ones (using profile::edit)
set_profile_information(
profile.clone(),
&description,
&pack.name,
&pack.dependencies,
) )
.await?; .into());
}
let profile_full_path = profile.get_full_path().await?; // Sets generated profile attributes to the pack ones (using profile::edit)
let profile = profile.clone(); set_profile_information(
let result = async { profile_path.clone(),
let loading_bar = init_or_edit_loading( &description,
existing_loading_bar, &pack.name,
LoadingBarType::PackDownload { &pack.dependencies,
profile_path: profile_full_path.clone(), )
pack_name: pack.name.clone(), .await?;
icon,
pack_id: project_id,
pack_version: version_id,
},
100.0,
"Downloading modpack",
)
.await?;
let num_files = pack.files.len(); let profile_path = profile_path.clone();
use futures::StreamExt; let loading_bar = init_or_edit_loading(
loading_try_for_each_concurrent( existing_loading_bar,
futures::stream::iter(pack.files.into_iter()) LoadingBarType::PackDownload {
.map(Ok::<PackFile, crate::Error>), profile_path: profile_path.get_full_path().await?.clone(),
None, pack_name: pack.name.clone(),
Some(&loading_bar), icon,
70.0, pack_id: project_id,
num_files, pack_version: version_id,
None, },
|project| { 100.0,
let profile_full_path = profile_full_path.clone(); "Downloading modpack",
async move { )
//TODO: Future update: prompt user for optional files in a modpack .await?;
if let Some(env) = project.env {
if env
.get(&EnvType::Client)
.map(|x| x == &SideType::Unsupported)
.unwrap_or(false)
{
return Ok(());
}
}
let file = fetch_mirrors( let num_files = pack.files.len();
&project use futures::StreamExt;
.downloads loading_try_for_each_concurrent(
.iter() futures::stream::iter(pack.files.into_iter())
.map(|x| &**x) .map(Ok::<PackFile, crate::Error>),
.collect::<Vec<&str>>(), None,
project Some(&loading_bar),
.hashes 70.0,
.get(&PackFileHash::Sha1) num_files,
.map(|x| &**x), None,
&state.fetch_semaphore, |project| {
) let profile_path = profile_path.clone();
.await?; async move {
//TODO: Future update: prompt user for optional files in a modpack
let path = std::path::Path::new(&project.path) if let Some(env) = project.env {
.components() if env
.next(); .get(&EnvType::Client)
if let Some(path) = path { .map(|x| x == &SideType::Unsupported)
match path { .unwrap_or(false)
Component::CurDir {
| Component::Normal(_) => { return Ok(());
let path = profile_full_path
.join(project.path);
write(
&path,
&file,
&state.io_semaphore,
)
.await?;
}
_ => {}
};
}
Ok(())
} }
},
)
.await?;
emit_loading(&loading_bar, 0.0, Some("Extracting overrides"))
.await?;
let mut total_len = 0;
for index in 0..zip_reader.file().entries().len() {
let file =
zip_reader.file().entries().get(index).unwrap().entry();
if (file.filename().starts_with("overrides")
|| file.filename().starts_with("client_overrides"))
&& !file.filename().ends_with('/')
{
total_len += 1;
} }
}
for index in 0..zip_reader.file().entries().len() { let file = fetch_mirrors(
let file = zip_reader &project
.file() .downloads
.entries() .iter()
.get(index) .map(|x| &**x)
.unwrap() .collect::<Vec<&str>>(),
.entry() project.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
.clone(); &state.fetch_semaphore,
let file_path = PathBuf::from(file.filename());
if (file.filename().starts_with("overrides")
|| file.filename().starts_with("client_overrides"))
&& !file.filename().ends_with('/')
{
// Reads the file into the 'content' variable
let mut content = Vec::new();
let mut reader = zip_reader.entry(index).await?;
reader.read_to_end_checked(&mut content, &file).await?;
let mut new_path = PathBuf::new();
let components = file_path.components().skip(1);
for component in components {
new_path.push(component);
}
if new_path.file_name().is_some() {
write(
&profile_full_path.join(new_path),
&content,
&state.io_semaphore,
)
.await?;
}
emit_loading(
&loading_bar,
30.0 / total_len as f64,
Some(&format!(
"Extracting override {}/{}",
index, total_len
)),
)
.await?;
}
}
if let Some(profile_val) =
crate::api::profile::get(&profile, None).await?
{
crate::launcher::install_minecraft(
&profile_val,
Some(loading_bar),
) )
.await?; .await?;
State::sync().await?; let path =
std::path::Path::new(&project.path).components().next();
if let Some(path) = path {
match path {
Component::CurDir | Component::Normal(_) => {
let path = profile_path
.get_full_path()
.await?
.join(project.path);
write(&path, &file, &state.io_semaphore)
.await?;
}
_ => {}
};
}
Ok(())
}
},
)
.await?;
emit_loading(&loading_bar, 0.0, Some("Extracting overrides")).await?;
let mut total_len = 0;
for index in 0..zip_reader.file().entries().len() {
let file = zip_reader.file().entries().get(index).unwrap().entry();
if (file.filename().starts_with("overrides")
|| file.filename().starts_with("client_overrides"))
&& !file.filename().ends_with('/')
{
total_len += 1;
}
}
for index in 0..zip_reader.file().entries().len() {
let file = zip_reader
.file()
.entries()
.get(index)
.unwrap()
.entry()
.clone();
let file_path = PathBuf::from(file.filename());
if (file.filename().starts_with("overrides")
|| file.filename().starts_with("client_overrides"))
&& !file.filename().ends_with('/')
{
// Reads the file into the 'content' variable
let mut content = Vec::new();
let mut reader = zip_reader.entry(index).await?;
reader.read_to_end_checked(&mut content, &file).await?;
let mut new_path = PathBuf::new();
let components = file_path.components().skip(1);
for component in components {
new_path.push(component);
} }
Ok::<ProfilePathId, crate::Error>(profile.clone()) if new_path.file_name().is_some() {
} write(
.await; &profile_path.get_full_path().await?.join(new_path),
&content,
match result { &state.io_semaphore,
Ok(profile) => Ok(profile), )
Err(err) => { .await?;
let _ = crate::api::profile::remove(&profile).await;
Err(err)
} }
emit_loading(
&loading_bar,
30.0 / total_len as f64,
Some(&format!(
"Extracting override {}/{}",
index, total_len
)),
)
.await?;
} }
} else {
Err(crate::Error::from(crate::ErrorKind::InputError(
"No pack manifest found in mrpack".to_string(),
)))
} }
}
.await;
match result { if let Some(profile_val) =
Ok(profile) => Ok(profile), crate::api::profile::get(&profile_path, None).await?
Err(err) => { {
let _ = crate::api::profile::remove(&profile).await; crate::launcher::install_minecraft(&profile_val, Some(loading_bar))
.await?;
Err(err) State::sync().await?;
} }
Ok::<ProfilePathId, crate::Error>(profile_path.clone())
} else {
Err(crate::Error::from(crate::ErrorKind::InputError(
"No pack manifest found in mrpack".to_string(),
)))
}
}
#[tracing::instrument(skip(mrpack_file))]
#[theseus_macros::debug_pin]
pub async fn remove_all_related_files(
profile_path: ProfilePathId,
mrpack_file: bytes::Bytes,
) -> crate::Result<()> {
let reader: Cursor<&bytes::Bytes> = Cursor::new(&mrpack_file);
// Create zip reader around file
let mut zip_reader = ZipFileReader::new(reader).await.map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed to read input modpack zip".to_string(),
))
})?;
// Extract index of modrinth.index.json
let zip_index_option = zip_reader
.file()
.entries()
.iter()
.position(|f| f.entry().filename() == "modrinth.index.json");
if let Some(zip_index) = zip_index_option {
let mut manifest = String::new();
let entry = zip_reader
.file()
.entries()
.get(zip_index)
.unwrap()
.entry()
.clone();
let mut reader = zip_reader.entry(zip_index).await?;
reader.read_to_string_checked(&mut manifest, &entry).await?;
let pack: PackFormat = serde_json::from_str(&manifest)?;
if &*pack.game != "minecraft" {
return Err(crate::ErrorKind::InputError(
"Pack does not support Minecraft".to_string(),
)
.into());
}
// Set install stage to installing, and do not change it back (as files are being removed and are not being reinstalled here)
crate::api::profile::edit(&profile_path, |prof| {
prof.install_stage = ProfileInstallStage::PackInstalling;
async { Ok(()) }
})
.await?;
let num_files = pack.files.len();
use futures::StreamExt;
loading_try_for_each_concurrent(
futures::stream::iter(pack.files.into_iter())
.map(Ok::<PackFile, crate::Error>),
None,
None,
0.0,
num_files,
None,
|project| {
let profile_path = profile_path.clone();
async move {
// Remove this file if a corresponding one exists in the filesystem
let existing_file =
profile_path.get_full_path().await?.join(&project.path);
if existing_file.exists() {
io::remove_file(&existing_file).await?;
}
Ok(())
}
},
)
.await?;
// Iterate over each 'overrides' file and remove it
for index in 0..zip_reader.file().entries().len() {
let file = zip_reader
.file()
.entries()
.get(index)
.unwrap()
.entry()
.clone();
let file_path = PathBuf::from(file.filename());
if (file.filename().starts_with("overrides")
|| file.filename().starts_with("client_overrides"))
&& !file.filename().ends_with('/')
{
let mut new_path = PathBuf::new();
let components = file_path.components().skip(1);
for component in components {
new_path.push(component);
}
// Remove this file if a corresponding one exists in the filesystem
let existing_file =
profile_path.get_full_path().await?.join(&new_path);
if existing_file.exists() {
io::remove_file(&existing_file).await?;
}
}
}
Ok(())
} else {
Err(crate::Error::from(crate::ErrorKind::InputError(
"No pack manifest found in mrpack".to_string(),
)))
} }
} }

View File

@@ -1,4 +1,5 @@
//! Theseus profile management interface //! Theseus profile management interface
use crate::event::emit::{ use crate::event::emit::{
emit_loading, init_loading, loading_try_for_each_concurrent, emit_loading, init_loading, loading_try_for_each_concurrent,
}; };
@@ -6,8 +7,8 @@ use crate::event::LoadingBarType;
use crate::pack::install_from::{ use crate::pack::install_from::{
EnvType, PackDependency, PackFile, PackFileHash, PackFormat, EnvType, PackDependency, PackFile, PackFileHash, PackFormat,
}; };
use crate::prelude::JavaVersion; use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId};
use crate::state::{ProfilePathId, ProjectMetadata, ProjectPathId}; use crate::state::ProjectMetadata;
use crate::util::io::{self, IOError}; use crate::util::io::{self, IOError};
use crate::{ use crate::{
@@ -21,7 +22,9 @@ pub use crate::{
}; };
use async_zip::tokio::write::ZipFileWriter; use async_zip::tokio::write::ZipFileWriter;
use async_zip::{Compression, ZipEntryBuilder}; use async_zip::{Compression, ZipEntryBuilder};
use std::collections::HashMap; use std::collections::HashMap;
use std::{ use std::{
future::Future, future::Future,
path::{Path, PathBuf}, path::{Path, PathBuf},
@@ -30,6 +33,9 @@ use std::{
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use tokio::{fs::File, process::Command, sync::RwLock}; use tokio::{fs::File, process::Command, sync::RwLock};
pub mod create;
pub mod update;
/// Remove a profile /// Remove a profile
#[tracing::instrument] #[tracing::instrument]
pub async fn remove(path: &ProfilePathId) -> crate::Result<()> { pub async fn remove(path: &ProfilePathId) -> crate::Result<()> {
@@ -56,7 +62,6 @@ pub async fn get(
clear_projects: Option<bool>, clear_projects: Option<bool>,
) -> crate::Result<Option<Profile>> { ) -> crate::Result<Option<Profile>> {
let state = State::get().await?; let state = State::get().await?;
let profiles = state.profiles.read().await; let profiles = state.profiles.read().await;
let mut profile = profiles.0.get(path).cloned(); let mut profile = profiles.0.get(path).cloned();
@@ -253,7 +258,7 @@ pub async fn install(path: &ProfilePathId) -> crate::Result<()> {
#[tracing::instrument] #[tracing::instrument]
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
pub async fn update_all( pub async fn update_all_projects(
profile_path: &ProfilePathId, profile_path: &ProfilePathId,
) -> crate::Result<HashMap<ProjectPathId, ProjectPathId>> { ) -> crate::Result<HashMap<ProjectPathId, ProjectPathId>> {
if let Some(profile) = get(profile_path, None).await? { if let Some(profile) = get(profile_path, None).await? {
@@ -519,6 +524,23 @@ pub async fn remove_project(
} }
} }
/// Gets whether project is a managed modrinth pack
#[tracing::instrument]
pub async fn is_managed_modrinth_pack(
profile: &ProfilePathId,
) -> crate::Result<bool> {
if let Some(profile) = get(profile, None).await? {
if let Some(linked_data) = profile.metadata.linked_data {
return Ok(linked_data.project_id.is_some()
&& linked_data.version_id.is_some());
}
Ok(false)
} else {
Err(crate::ErrorKind::UnmanagedProfileError(profile.to_string())
.as_error())
}
}
/// Exports the profile to a Modrinth-formatted .mrpack file /// Exports the profile to a Modrinth-formatted .mrpack file
// Version ID of uploaded version (ie 1.1.5), not the unique identifying ID of the version (nvrqJg44) // Version ID of uploaded version (ie 1.1.5), not the unique identifying ID of the version (nvrqJg44)
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]

View File

@@ -0,0 +1,203 @@
use crate::{
event::{
emit::{emit_profile, loading_try_for_each_concurrent},
ProfilePayloadType,
},
pack::{self, install_from::generate_pack_from_version_id},
prelude::{ProfilePathId, ProjectPathId},
profile::get,
state::Project,
State,
};
use futures::try_join;
/// Updates a managed modrinth pack to the cached latest version found in 'modrinth_update_version'
#[tracing::instrument]
#[theseus_macros::debug_pin]
pub async fn update_managed_modrinth(
profile_path: &ProfilePathId,
) -> crate::Result<()> {
let profile = get(profile_path, None).await?.ok_or_else(|| {
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
.as_error()
})?;
let unmanaged_err = || {
crate::ErrorKind::InputError(
format!("Profile at {} is not a managed modrinth pack, or has been disconnected.", profile_path),
)
};
// Extract modrinth pack information, if appropriate
let linked_data = profile
.metadata
.linked_data
.as_ref()
.ok_or_else(unmanaged_err)?;
let project_id: &String =
linked_data.project_id.as_ref().ok_or_else(unmanaged_err)?;
let version_id =
linked_data.version_id.as_ref().ok_or_else(unmanaged_err)?;
// extract modrinth_update_version, returning Ok(()) if it is none
let modrinth_update_version = match profile.modrinth_update_version {
Some(ref x) if x != version_id => x,
_ => return Ok(()), // No update version, or no update needed, return Ok(())
};
// Replace the pack with the new version
replace_managed_modrinth(
profile_path,
&profile,
project_id,
version_id,
Some(modrinth_update_version),
)
.await?;
emit_profile(
profile.uuid,
profile.path,
&profile.metadata.name,
ProfilePayloadType::Edited,
)
.await?;
State::sync().await?;
Ok(())
}
/// Repair a managed modrinth pack by 'updating' it to the current version
#[tracing::instrument]
#[theseus_macros::debug_pin]
pub async fn repair_managed_modrinth(
profile_path: &ProfilePathId,
) -> crate::Result<()> {
let profile = get(profile_path, None).await?.ok_or_else(|| {
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
.as_error()
})?;
let unmanaged_err = || {
crate::ErrorKind::InputError(
format!("Profile at {} is not a managed modrinth pack, or has been disconnected.", profile_path),
)
};
// For repairing specifically, first we remove all installed projects (to ensure we do remove ones that aren't in the pack)
// We do a project removal followed by removing everything in the .mrpack, to ensure we only
// remove relevant projects and not things like save files
let projects_map = profile.projects.clone();
let stream = futures::stream::iter(
projects_map
.into_iter()
.map(Ok::<(ProjectPathId, Project), crate::Error>),
);
loading_try_for_each_concurrent(
stream,
None,
None,
0.0,
0,
None,
|(project_id, _)| {
let profile = profile.clone();
async move {
profile.remove_project(&project_id, Some(true)).await?;
Ok(())
}
},
)
.await?;
// Extract modrinth pack information, if appropriate
let linked_data = profile
.metadata
.linked_data
.as_ref()
.ok_or_else(unmanaged_err)?;
let project_id: &String =
linked_data.project_id.as_ref().ok_or_else(unmanaged_err)?;
let version_id =
linked_data.version_id.as_ref().ok_or_else(unmanaged_err)?;
// Replace the pack with the same version
replace_managed_modrinth(
profile_path,
&profile,
project_id,
version_id,
None,
)
.await?;
emit_profile(
profile.uuid,
profile.path,
&profile.metadata.name,
ProfilePayloadType::Edited,
)
.await?;
State::sync().await?;
Ok(())
}
/// Replace a managed modrinth pack with a new version
/// If new_version_id is None, the pack is 'reinstalled' in-place
#[tracing::instrument(skip(profile))]
#[theseus_macros::debug_pin]
async fn replace_managed_modrinth(
profile_path: &ProfilePathId,
profile: &crate::state::Profile,
project_id: &String,
version_id: &String,
new_version_id: Option<&String>,
) -> crate::Result<()> {
// Fetch .mrpacks for both old and new versions
// TODO: this will need to be updated if we revert the hacky pack method we needed for compiler speed
let old_pack_creator = generate_pack_from_version_id(
project_id.clone(),
version_id.clone(),
profile.metadata.name.clone(),
None,
profile_path.clone(),
);
// download in parallel, then join. If new_version_id is None, we don't need to download the new pack, so we clone the old one
let (old_pack_creator, new_pack_creator) =
if let Some(new_version_id) = new_version_id {
try_join!(
old_pack_creator,
generate_pack_from_version_id(
project_id.clone(),
new_version_id.clone(),
profile.metadata.name.clone(),
None,
profile_path.clone()
)
)?
} else {
let mut old_pack_creator = old_pack_creator.await?;
old_pack_creator.description.existing_loading_bar = None;
(old_pack_creator.clone(), old_pack_creator)
};
// Removal - remove all files that were added by the old pack
// - remove all installed projects
// - remove all overrides
pack::install_mrpack::remove_all_related_files(
profile_path.clone(),
old_pack_creator.file,
)
.await?;
// Reinstallation - install all files that are added by the new pack
// - install all projects
// - install all overrides
// - edits the profile to update the new data
// - (functionals almost identically to rteinstalling the pack 'in-place')
pack::install_mrpack::install_zipped_mrpack_files(new_pack_creator).await?;
Ok(())
}

View File

@@ -1,5 +1,5 @@
//! Theseus error type //! Theseus error type
use crate::{profile_create, util}; use crate::{profile, util};
use tracing_error::InstrumentError; use tracing_error::InstrumentError;
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@@ -68,7 +68,7 @@ pub enum ErrorKind {
UnmanagedProfileError(String), UnmanagedProfileError(String),
#[error("Could not create profile: {0}")] #[error("Could not create profile: {0}")]
ProfileCreationError(#[from] profile_create::ProfileCreationError), ProfileCreationError(#[from] profile::create::ProfileCreationError),
#[error("User is not logged in, no credentials available!")] #[error("User is not logged in, no credentials available!")]
NoCredentialsError, NoCredentialsError,

View File

@@ -185,6 +185,7 @@ impl State {
tokio::task::spawn(Metadata::update()); tokio::task::spawn(Metadata::update());
tokio::task::spawn(Tags::update()); tokio::task::spawn(Tags::update());
tokio::task::spawn(Profiles::update_projects()); tokio::task::spawn(Profiles::update_projects());
tokio::task::spawn(Profiles::update_modrinth_versions());
tokio::task::spawn(Settings::update_java()); tokio::task::spawn(Settings::update_java());
} }

View File

@@ -51,6 +51,7 @@ pub enum ProfileInstallStage {
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
#[serde(transparent)] #[serde(transparent)]
pub struct ProfilePathId(PathBuf); pub struct ProfilePathId(PathBuf);
impl ProfilePathId { impl ProfilePathId {
// Create a new ProfilePathId from a full file path // Create a new ProfilePathId from a full file path
pub async fn from_fs_path(path: PathBuf) -> crate::Result<Self> { pub async fn from_fs_path(path: PathBuf) -> crate::Result<Self> {
@@ -151,6 +152,8 @@ pub struct Profile {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub hooks: Option<Hooks>, pub hooks: Option<Hooks>,
pub projects: HashMap<ProjectPathId, Project>, pub projects: HashMap<ProjectPathId, Project>,
#[serde(default)]
pub modrinth_update_version: Option<String>,
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
@@ -266,6 +269,7 @@ impl Profile {
memory: None, memory: None,
resolution: None, resolution: None,
hooks: None, hooks: None,
modrinth_update_version: None,
}) })
} }
@@ -386,13 +390,12 @@ impl Profile {
let mut read_paths = |path: &str| { let mut read_paths = |path: &str| {
let new_path = profile_path.join(path); let new_path = profile_path.join(path);
if new_path.exists() { if new_path.exists() {
let path = self.path.join(path); for subpath in std::fs::read_dir(&new_path)
for path in std::fs::read_dir(&path) .map_err(|e| IOError::with_path(e, &new_path))?
.map_err(|e| IOError::with_path(e, &path))?
{ {
let path = path.map_err(IOError::from)?.path(); let subpath = subpath.map_err(IOError::from)?.path();
if path.is_file() { if subpath.is_file() {
files.push(path); files.push(subpath);
} }
} }
} }
@@ -770,6 +773,86 @@ impl Profiles {
}; };
} }
#[tracing::instrument]
#[theseus_macros::debug_pin]
pub async fn update_modrinth_versions() {
let res = async {
let state = State::get().await?;
// Temporarily store all profiles that have modrinth linked data
let mut modrinth_updatables: Vec<(ProfilePathId, String)> =
Vec::new();
{
let profiles = state.profiles.read().await;
for (profile_path, profile) in profiles.0.iter() {
if let Some(linked_data) = &profile.metadata.linked_data {
if let Some(linked_project) = &linked_data.project_id {
modrinth_updatables.push((
profile_path.clone(),
linked_project.clone(),
));
}
}
}
}
// Fetch online from Modrinth each latest version
future::try_join_all(modrinth_updatables.into_iter().map(
|(profile_path, linked_project)| {
let profile_path = profile_path;
let linked_project = linked_project;
let state = state.clone();
async move {
let versions: Vec<ModrinthVersion> = fetch_json(
Method::GET,
&format!(
"{}project/{}/version",
MODRINTH_API_URL,
linked_project.clone()
),
None,
None,
&state.fetch_semaphore,
)
.await?;
// Versions are pre-sorted in labrinth (by versions.sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published));)
// so we can just take the first one
let mut new_profiles = state.profiles.write().await;
if let Some(profile) =
new_profiles.0.get_mut(&profile_path)
{
if let Some(recent_version) = versions.get(0) {
profile.modrinth_update_version =
Some(recent_version.id.clone());
} else {
profile.modrinth_update_version = None;
}
}
drop(new_profiles);
Ok::<(), crate::Error>(())
}
},
))
.await?;
{
let profiles = state.profiles.read().await;
profiles.sync().await?;
}
Ok::<(), crate::Error>(())
}
.await;
match res {
Ok(()) => {}
Err(err) => {
tracing::warn!("Unable to update modrinth versions: {err}")
}
};
}
#[tracing::instrument(skip(self, profile))] #[tracing::instrument(skip(self, profile))]
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
pub async fn insert(&mut self, profile: Profile) -> crate::Result<&Self> { pub async fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {

View File

@@ -10,7 +10,7 @@ use paris::*;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tabled::Tabled; use tabled::Tabled;
use theseus::prelude::*; use theseus::prelude::*;
use theseus::profile_create::profile_create; use theseus::profile::create::profile_create;
use tokio::fs; use tokio::fs;
use tokio_stream::wrappers::ReadDirStream; use tokio_stream::wrappers::ReadDirStream;

View File

@@ -1,6 +1,8 @@
use crate::api::Result; use crate::api::Result;
use theseus::logs::{self, Logs}; use theseus::{
use uuid::Uuid; logs::{self, Logs},
prelude::ProfilePathId,
};
/* /*
A log is a struct containing the datetime string, stdout, and stderr, as follows: A log is a struct containing the datetime string, stdout, and stderr, as follows:
@@ -27,10 +29,10 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
/// Get all Logs for a profile, sorted by datetime /// Get all Logs for a profile, sorted by datetime
#[tauri::command] #[tauri::command]
pub async fn logs_get_logs( pub async fn logs_get_logs(
profile_uuid: Uuid, profile_path: ProfilePathId,
clear_contents: Option<bool>, clear_contents: Option<bool>,
) -> Result<Vec<Logs>> { ) -> Result<Vec<Logs>> {
let val = logs::get_logs(profile_uuid, clear_contents).await?; let val = logs::get_logs(profile_path, clear_contents).await?;
Ok(val) Ok(val)
} }
@@ -38,25 +40,25 @@ pub async fn logs_get_logs(
/// Get a Log struct for a profile by profile id and datetime string /// Get a Log struct for a profile by profile id and datetime string
#[tauri::command] #[tauri::command]
pub async fn logs_get_logs_by_datetime( pub async fn logs_get_logs_by_datetime(
profile_uuid: Uuid, profile_path: ProfilePathId,
datetime_string: String, datetime_string: String,
) -> Result<Logs> { ) -> Result<Logs> {
Ok(logs::get_logs_by_datetime(profile_uuid, datetime_string).await?) Ok(logs::get_logs_by_datetime(profile_path, datetime_string).await?)
} }
/// Get the stdout for a profile by profile id and datetime string /// Get the stdout for a profile by profile id and datetime string
#[tauri::command] #[tauri::command]
pub async fn logs_get_output_by_datetime( pub async fn logs_get_output_by_datetime(
profile_uuid: Uuid, profile_path: ProfilePathId,
datetime_string: String, datetime_string: String,
) -> Result<String> { ) -> Result<String> {
let profile_path = if let Some(p) = let profile_path = if let Some(p) =
crate::profile::get_by_uuid(profile_uuid, None).await? crate::profile::get(&profile_path, None).await?
{ {
p.profile_id() p.profile_id()
} else { } else {
return Err(theseus::Error::from( return Err(theseus::Error::from(
theseus::ErrorKind::UnmanagedProfileError(profile_uuid.to_string()), theseus::ErrorKind::UnmanagedProfileError(profile_path.to_string()),
) )
.into()); .into());
}; };
@@ -66,15 +68,15 @@ pub async fn logs_get_output_by_datetime(
/// Delete all logs for a profile by profile id /// Delete all logs for a profile by profile id
#[tauri::command] #[tauri::command]
pub async fn logs_delete_logs(profile_uuid: Uuid) -> Result<()> { pub async fn logs_delete_logs(profile_path: ProfilePathId) -> Result<()> {
Ok(logs::delete_logs(profile_uuid).await?) Ok(logs::delete_logs(profile_path).await?)
} }
/// Delete a log for a profile by profile id and datetime string /// Delete a log for a profile by profile id and datetime string
#[tauri::command] #[tauri::command]
pub async fn logs_delete_logs_by_datetime( pub async fn logs_delete_logs_by_datetime(
profile_uuid: Uuid, profile_path: ProfilePathId,
datetime_string: String, datetime_string: String,
) -> Result<()> { ) -> Result<()> {
Ok(logs::delete_logs_by_datetime(profile_uuid, &datetime_string).await?) Ok(logs::delete_logs_by_datetime(profile_path, &datetime_string).await?)
} }

View File

@@ -21,6 +21,9 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
profile_add_project_from_path, profile_add_project_from_path,
profile_toggle_disable_project, profile_toggle_disable_project,
profile_remove_project, profile_remove_project,
profile_update_managed_modrinth,
profile_repair_managed_modrinth,
profile_is_managed_modrinth,
profile_run, profile_run,
profile_run_wait, profile_run_wait,
profile_run_credentials, profile_run_credentials,
@@ -105,7 +108,7 @@ pub async fn profile_install(path: ProfilePathId) -> Result<()> {
pub async fn profile_update_all( pub async fn profile_update_all(
path: ProfilePathId, path: ProfilePathId,
) -> Result<HashMap<ProjectPathId, ProjectPathId>> { ) -> Result<HashMap<ProjectPathId, ProjectPathId>> {
Ok(profile::update_all(&path).await?) Ok(profile::update_all_projects(&path).await?)
} }
/// Updates a specified project /// Updates a specified project
@@ -162,6 +165,28 @@ pub async fn profile_remove_project(
Ok(()) Ok(())
} }
// Updates a managed Modrinth profile
#[tauri::command]
pub async fn profile_update_managed_modrinth(
path: ProfilePathId,
) -> Result<()> {
Ok(profile::update::update_managed_modrinth(&path).await?)
}
// Repairs a managed Modrinth profile by updating it to the current version
#[tauri::command]
pub async fn profile_repair_managed_modrinth(
path: ProfilePathId,
) -> Result<()> {
Ok(profile::update::repair_managed_modrinth(&path).await?)
}
// Gets if a profile is managed by Modrinth
#[tauri::command]
pub async fn profile_is_managed_modrinth(path: ProfilePathId) -> Result<bool> {
Ok(profile::is_managed_modrinth_pack(&path).await?)
}
// Exports a profile to a .mrpack file (export_location should end in .mrpack) // Exports a profile to a .mrpack file (export_location should end in .mrpack)
// invoke('profile_export_mrpack') // invoke('profile_export_mrpack')
#[tauri::command] #[tauri::command]

View File

@@ -18,7 +18,7 @@ pub async fn profile_create(
loader_version: Option<String>, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader loader_version: Option<String>, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
icon: Option<PathBuf>, // the icon for the profile icon: Option<PathBuf>, // the icon for the profile
) -> Result<ProfilePathId> { ) -> Result<ProfilePathId> {
let res = profile_create::profile_create( let res = profile::create::profile_create(
name, name,
game_version, game_version,
modloader, modloader,

View File

@@ -17,26 +17,26 @@ pub struct Logs {
/// Get all logs that exist for a given profile /// Get all logs that exist for a given profile
/// This is returned as an array of Log objects, sorted by datetime_string (the folder name, when the log was created) /// This is returned as an array of Log objects, sorted by datetime_string (the folder name, when the log was created)
export async function get_logs(profileUuid, clearContents) { export async function get_logs(profilePath, clearContents) {
return await invoke('plugin:logs|logs_get_logs', { profileUuid, clearContents }) return await invoke('plugin:logs|logs_get_logs', { profilePath, clearContents })
} }
/// Get a profile's log by datetime_string (the folder name, when the log was created) /// Get a profile's log by datetime_string (the folder name, when the log was created)
export async function get_logs_by_datetime(profileUuid, datetimeString) { export async function get_logs_by_datetime(profilePath, datetimeString) {
return await invoke('plugin:logs|logs_get_logs_by_datetime', { profileUuid, datetimeString }) return await invoke('plugin:logs|logs_get_logs_by_datetime', { profilePath, datetimeString })
} }
/// Get a profile's stdout only by datetime_string (the folder name, when the log was created) /// Get a profile's stdout only by datetime_string (the folder name, when the log was created)
export async function get_output_by_datetime(profileUuid, datetimeString) { export async function get_output_by_datetime(profilePath, datetimeString) {
return await invoke('plugin:logs|logs_get_output_by_datetime', { profileUuid, datetimeString }) return await invoke('plugin:logs|logs_get_output_by_datetime', { profilePath, datetimeString })
} }
/// Delete a profile's log by datetime_string (the folder name, when the log was created) /// Delete a profile's log by datetime_string (the folder name, when the log was created)
export async function delete_logs_by_datetime(profileUuid, datetimeString) { export async function delete_logs_by_datetime(profilePath, datetimeString) {
return await invoke('plugin:logs|logs_delete_logs_by_datetime', { profileUuid, datetimeString }) return await invoke('plugin:logs|logs_delete_logs_by_datetime', { profilePath, datetimeString })
} }
/// Delete all logs for a given profile /// Delete all logs for a given profile
export async function delete_logs(profileUuid) { export async function delete_logs(profilePath) {
return await invoke('plugin:logs|logs_delete_logs', { profileUuid }) return await invoke('plugin:logs|logs_delete_logs', { profilePath })
} }

View File

@@ -94,6 +94,21 @@ export async function remove_project(path, projectPath) {
return await invoke('plugin:profile|profile_remove_project', { path, projectPath }) return await invoke('plugin:profile|profile_remove_project', { path, projectPath })
} }
// Update a managed Modrinth profile
export async function update_managed_modrinth(path) {
return await invoke('plugin:profile|profile_update_managed_modrinth', { path })
}
// Repair a managed Modrinth profile
export async function update_repair_modrinth(path) {
return await invoke('plugin:profile|profile_repair_managed_modrinth', { path })
}
// Gets whether a profile is managed by Modrinth
export async function is_managed_modrinth(path) {
return await invoke('plugin:profile|profile_is_managed_modrinth', { path })
}
// Export a profile to .mrpack // Export a profile to .mrpack
/// included_overrides is an array of paths to override folders to include (ie: 'mods', 'resource_packs') /// included_overrides is an array of paths to override folders to include (ie: 'mods', 'resource_packs')
// Version id is optional (ie: 1.1.5) // Version id is optional (ie: 1.1.5)

View File

@@ -9,6 +9,8 @@ import FloatingVue from 'floating-vue'
import { get_opening_command, initialize_state } from '@/helpers/state' import { get_opening_command, initialize_state } from '@/helpers/state'
import loadCssMixin from './mixins/macCssFix.js' import loadCssMixin from './mixins/macCssFix.js'
import { get } from '@/helpers/settings' import { get } from '@/helpers/settings'
import { invoke } from '@tauri-apps/api'
import { isDev } from './helpers/utils.js'
const pinia = createPinia() const pinia = createPinia()
@@ -20,6 +22,19 @@ app.mixin(loadCssMixin)
const mountedApp = app.mount('#app') const mountedApp = app.mount('#app')
const raw_invoke = async (plugin, fn, args) => {
return await invoke('plugin:' + plugin + '|' + fn, args)
}
isDev()
.then((dev) => {
if (dev) {
window.raw_invoke = raw_invoke
}
})
.catch((err) => {
console.error(err)
})
initialize_state() initialize_state()
.then(() => { .then(() => {
// First, redirect to other landing page if we have that setting // First, redirect to other landing page if we have that setting

View File

@@ -277,8 +277,8 @@
<label for="repair-profile"> <label for="repair-profile">
<span class="label__title">Repair instance</span> <span class="label__title">Repair instance</span>
<span class="label__description"> <span class="label__description">
Reinstalls the instance and checks for corruption. Use this if your game is not launching Reinstalls Minecraft dependencies and checks for corruption. Use this if your game is not
due to launcher-related errors. launching due to launcher-related errors.
</span> </span>
</label> </label>
<button <button
@@ -290,6 +290,24 @@
<HammerIcon /> Repair <HammerIcon /> Repair
</button> </button>
</div> </div>
<div v-if="props.instance.modrinth_update_version" class="adjacent-input">
<label for="repair-profile">
<span class="label__title">Repair modpack</span>
<span class="label__description">
Reinstalls Modrinth modpack and checks for corruption. Use this if your game is not
launching due to your instance diverging from the Modrinth modpack.
</span>
</label>
<button
id="repair-profile"
class="btn btn-highlight"
:disabled="repairing"
@click="repairModpack"
>
<HammerIcon /> Repair
</button>
</div>
<div class="adjacent-input"> <div class="adjacent-input">
<label for="delete-profile"> <label for="delete-profile">
<span class="label__title">Delete instance</span> <span class="label__title">Delete instance</span>
@@ -329,7 +347,15 @@ import {
} from 'omorphia' } from 'omorphia'
import { Multiselect } from 'vue-multiselect' import { Multiselect } from 'vue-multiselect'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { edit, edit_icon, get_optimal_jre_key, install, list, remove } from '@/helpers/profile.js' import {
edit,
edit_icon,
get_optimal_jre_key,
install,
list,
remove,
update_repair_modrinth,
} from '@/helpers/profile.js'
import { computed, readonly, ref, shallowRef, watch } from 'vue' import { computed, readonly, ref, shallowRef, watch } from 'vue'
import { get_max_memory } from '@/helpers/jre.js' import { get_max_memory } from '@/helpers/jre.js'
import { get } from '@/helpers/settings.js' import { get } from '@/helpers/settings.js'
@@ -501,6 +527,17 @@ async function repairProfile() {
}) })
} }
async function repairModpack() {
repairing.value = true
await update_repair_modrinth(props.instance.path).catch(handleError)
repairing.value = false
mixpanel.track('InstanceRepair', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
})
}
const removing = ref(false) const removing = ref(false)
async function removeProfile() { async function removeProfile() {
removing.value = true removing.value = true

View File

@@ -6,7 +6,7 @@
use theseus::jre::autodetect_java_globals; use theseus::jre::autodetect_java_globals;
use theseus::prelude::*; use theseus::prelude::*;
use theseus::profile_create::profile_create; use theseus::profile::create::profile_create;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
// A simple Rust implementation of the authentication run // A simple Rust implementation of the authentication run