diff --git a/theseus/src/api/mr_auth.rs b/theseus/src/api/mr_auth.rs index 78d57fc2f..a87f66090 100644 --- a/theseus/src/api/mr_auth.rs +++ b/theseus/src/api/mr_auth.rs @@ -7,10 +7,13 @@ use crate::ErrorKind; pub async fn authenticate_begin_flow(provider: &str) -> crate::Result { let state = crate::State::get().await?; + // Don't start an uncompleteable new flow if there's an existing locked one + let mut write: tokio::sync::RwLockWriteGuard<'_, Option> = + state.modrinth_auth_flow.write().await; + let mut flow = ModrinthAuthFlow::new(provider).await?; let url = flow.prepare_login_url().await?; - let mut write = state.modrinth_auth_flow.write().await; *write = Some(flow); Ok(url) diff --git a/theseus/src/api/pack/import/atlauncher.rs b/theseus/src/api/pack/import/atlauncher.rs index e54908bfc..a63374756 100644 --- a/theseus/src/api/pack/import/atlauncher.rs +++ b/theseus/src/api/pack/import/atlauncher.rs @@ -3,13 +3,12 @@ use std::{collections::HashMap, path::PathBuf}; use serde::{Deserialize, Serialize}; use crate::{ - event::LoadingBarId, pack::{ self, import::{self, copy_dotminecraft}, install_from::CreatePackDescription, }, - prelude::{ModLoader, ProfilePathId}, + prelude::{ModLoader, Profile, ProfilePathId}, state::{LinkedData, ProfileInstallStage}, util::io, State, @@ -33,8 +32,6 @@ pub struct ATLauncher { pub modrinth_project: Option, pub modrinth_version: Option, pub modrinth_manifest: Option, - - pub mods: Vec, } #[derive(Serialize, Deserialize)] @@ -57,13 +54,9 @@ pub struct ATLauncherModrinthProject { pub slug: String, pub project_type: String, pub team: String, - pub title: String, - pub description: String, - pub body: String, pub client_side: Option, pub server_side: Option, pub categories: Vec, - pub icon_url: String, } #[derive(Serialize, Deserialize, Debug)] @@ -110,7 +103,16 @@ pub async fn is_valid_atlauncher(instance_folder: PathBuf) -> bool { .unwrap_or("".to_string()); let instance: Result = serde_json::from_str::(&instance); - instance.is_ok() + if let Err(e) = instance { + tracing::warn!( + "Could not parse instance.json at {}: {}", + instance_folder.display(), + e + ); + false + } else { + true + } } #[tracing::instrument] @@ -169,7 +171,6 @@ pub async fn import_atlauncher( backup_name, description, atinstance, - None, ) .await?; Ok(()) @@ -181,7 +182,6 @@ async fn import_atlauncher_unmanaged( backup_name: String, description: CreatePackDescription, atinstance: ATInstance, - existing_loading_bar: Option, ) -> crate::Result<()> { let mod_loader = format!( "\"{}\"", @@ -230,19 +230,28 @@ async fn import_atlauncher_unmanaged( // Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc) let state = State::get().await?; - copy_dotminecraft( + let loading_bar = copy_dotminecraft( profile_path.clone(), minecraft_folder, &state.io_semaphore, + None, ) .await?; if let Some(profile_val) = crate::api::profile::get(&profile_path, None).await? { - crate::launcher::install_minecraft(&profile_val, existing_loading_bar) + crate::launcher::install_minecraft(&profile_val, Some(loading_bar)) .await?; - + { + let state = State::get().await?; + let mut file_watcher = state.file_watcher.write().await; + Profile::watch_fs( + &profile_val.get_profile_full_path().await?, + &mut file_watcher, + ) + .await?; + } State::sync().await?; } Ok(()) diff --git a/theseus/src/api/pack/import/curseforge.rs b/theseus/src/api/pack/import/curseforge.rs index b13d2961f..25a917377 100644 --- a/theseus/src/api/pack/import/curseforge.rs +++ b/theseus/src/api/pack/import/curseforge.rs @@ -2,6 +2,7 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; +use crate::prelude::Profile; use crate::state::CredentialsStore; use crate::{ prelude::{ModLoader, ProfilePathId}, @@ -187,18 +188,29 @@ pub async fn import_curseforge( // Copy in contained folders as overrides let state = State::get().await?; - copy_dotminecraft( + let loading_bar = copy_dotminecraft( profile_path.clone(), curseforge_instance_folder, &state.io_semaphore, + None, ) .await?; if let Some(profile_val) = crate::api::profile::get(&profile_path, None).await? { - crate::launcher::install_minecraft(&profile_val, None).await?; + crate::launcher::install_minecraft(&profile_val, Some(loading_bar)) + .await?; + { + let state = State::get().await?; + let mut file_watcher = state.file_watcher.write().await; + Profile::watch_fs( + &profile_val.get_profile_full_path().await?, + &mut file_watcher, + ) + .await?; + } State::sync().await?; } diff --git a/theseus/src/api/pack/import/gdlauncher.rs b/theseus/src/api/pack/import/gdlauncher.rs index f7317b675..022552d01 100644 --- a/theseus/src/api/pack/import/gdlauncher.rs +++ b/theseus/src/api/pack/import/gdlauncher.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; use crate::{ - prelude::{ModLoader, ProfilePathId}, + prelude::{ModLoader, Profile, ProfilePathId}, state::ProfileInstallStage, util::io, State, @@ -101,18 +101,28 @@ pub async fn import_gdlauncher( // Copy in contained folders as overrides let state = State::get().await?; - copy_dotminecraft( + let loading_bar = copy_dotminecraft( profile_path.clone(), gdlauncher_instance_folder, &state.io_semaphore, + None, ) .await?; if let Some(profile_val) = crate::api::profile::get(&profile_path, None).await? { - crate::launcher::install_minecraft(&profile_val, None).await?; - + crate::launcher::install_minecraft(&profile_val, Some(loading_bar)) + .await?; + { + let state = State::get().await?; + let mut file_watcher = state.file_watcher.write().await; + Profile::watch_fs( + &profile_val.get_profile_full_path().await?, + &mut file_watcher, + ) + .await?; + } State::sync().await?; } diff --git a/theseus/src/api/pack/import/mmc.rs b/theseus/src/api/pack/import/mmc.rs index 101e8588c..438776f75 100644 --- a/theseus/src/api/pack/import/mmc.rs +++ b/theseus/src/api/pack/import/mmc.rs @@ -7,7 +7,7 @@ use crate::{ import::{self, copy_dotminecraft}, install_from::{self, CreatePackDescription, PackDependency}, }, - prelude::ProfilePathId, + prelude::{Profile, ProfilePathId}, util::io, State, }; @@ -119,6 +119,26 @@ pub struct MMCComponentRequirement { pub suggests: Option, } +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +#[serde(untagged)] +enum MMCLauncherEnum { + General(MMCLauncherGeneral), + Instance(MMCLauncher), +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +struct MMCLauncherGeneral { + pub general: MMCLauncher, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +pub struct MMCLauncher { + instance_dir: String, +} + // Checks if if its a folder, and the folder contains instance.cfg and mmc-pack.json, and they both parse #[tracing::instrument] pub async fn is_valid_mmc(instance_folder: PathBuf) -> bool { @@ -134,9 +154,19 @@ pub async fn is_valid_mmc(instance_folder: PathBuf) -> bool { && serde_json::from_str::(&mmc_pack).is_ok() } +#[tracing::instrument] +pub async fn get_instances_subpath(config: PathBuf) -> Option { + let launcher = io::read_to_string(&config).await.ok()?; + let launcher: MMCLauncherEnum = serde_ini::from_str(&launcher).ok()?; + match launcher { + MMCLauncherEnum::General(p) => Some(p.general.instance_dir), + MMCLauncherEnum::Instance(p) => Some(p.instance_dir), + } +} + // Loading the INI (instance.cfg) file async fn load_instance_cfg(file_path: &Path) -> crate::Result { - let instance_cfg = io::read_to_string(file_path).await?; + let instance_cfg: String = io::read_to_string(file_path).await?; let instance_cfg_enum: MMCInstanceEnum = serde_ini::from_str::(&instance_cfg)?; match instance_cfg_enum { @@ -281,18 +311,28 @@ async fn import_mmc_unmanaged( // Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc) let state = State::get().await?; - copy_dotminecraft( + let loading_bar = copy_dotminecraft( profile_path.clone(), minecraft_folder, &state.io_semaphore, + None, ) .await?; if let Some(profile_val) = crate::api::profile::get(&profile_path, None).await? { - crate::launcher::install_minecraft(&profile_val, None).await?; - + crate::launcher::install_minecraft(&profile_val, Some(loading_bar)) + .await?; + { + let state = State::get().await?; + let mut file_watcher = state.file_watcher.write().await; + Profile::watch_fs( + &profile_val.get_profile_full_path().await?, + &mut file_watcher, + ) + .await?; + } State::sync().await?; } Ok(()) diff --git a/theseus/src/api/pack/import/mod.rs b/theseus/src/api/pack/import/mod.rs index 6b28f73a7..96563acad 100644 --- a/theseus/src/api/pack/import/mod.rs +++ b/theseus/src/api/pack/import/mod.rs @@ -7,6 +7,10 @@ use io::IOError; use serde::{Deserialize, Serialize}; use crate::{ + event::{ + emit::{emit_loading, init_or_edit_loading}, + LoadingBarId, + }, prelude::ProfilePathId, state::Profiles, util::{ @@ -51,11 +55,20 @@ pub async fn get_importable_instances( ) -> crate::Result> { // Some launchers have a different folder structure for instances let instances_subfolder = match launcher_type { - ImportLauncherType::GDLauncher - | ImportLauncherType::MultiMC - | ImportLauncherType::PrismLauncher - | ImportLauncherType::ATLauncher => "instances", - ImportLauncherType::Curseforge => "Instances", + ImportLauncherType::GDLauncher | ImportLauncherType::ATLauncher => { + "instances".to_string() + } + ImportLauncherType::Curseforge => "Instances".to_string(), + ImportLauncherType::MultiMC => { + mmc::get_instances_subpath(base_path.clone().join("multimc.cfg")) + .await + .unwrap_or_else(|| "instances".to_string()) + } + ImportLauncherType::PrismLauncher => mmc::get_instances_subpath( + base_path.clone().join("prismlauncher.cfg"), + ) + .await + .unwrap_or_else(|| "instances".to_string()), ImportLauncherType::Unknown => { return Err(crate::ErrorKind::InputError( "Launcher type Unknown".to_string(), @@ -63,7 +76,8 @@ pub async fn get_importable_instances( .into()) } }; - let instances_folder = base_path.join(instances_subfolder); + + let instances_folder = base_path.join(&instances_subfolder); let mut instances = Vec::new(); let mut dir = io::read_dir(&instances_folder).await.map_err(| _ | { crate::ErrorKind::InputError(format!( @@ -238,55 +252,61 @@ pub async fn recache_icon( } async fn copy_dotminecraft( - profile_path: ProfilePathId, + profile_path_id: ProfilePathId, dotminecraft: PathBuf, io_semaphore: &IoSemaphore, -) -> crate::Result<()> { + existing_loading_bar: Option, +) -> crate::Result { // Get full path to profile - let profile_path = profile_path.get_full_path().await?; + let profile_path = profile_path_id.get_full_path().await?; - // std fs copy every file in dotminecraft to profile_path - let mut dir = io::read_dir(&dotminecraft).await?; - while let Some(entry) = dir - .next_entry() - .await - .map_err(|e| IOError::with_path(e, &dotminecraft))? - { - let path = entry.path(); - copy_dir_to( - &path, - &profile_path.join(path.file_name().ok_or_else(|| { + // Gets all subfiles recursively in src + let subfiles = get_all_subfiles(&dotminecraft).await?; + let total_subfiles = subfiles.len() as u64; + + let loading_bar = init_or_edit_loading( + existing_loading_bar, + crate::LoadingBarType::CopyProfile { + import_location: dotminecraft.clone(), + profile_name: profile_path_id.to_string(), + }, + total_subfiles as f64, + "Copying files in profile", + ) + .await?; + + // Copy each file + for src_child in subfiles { + let dst_child = + src_child.strip_prefix(&dotminecraft).map_err(|_| { crate::ErrorKind::InputError(format!( "Invalid file: {}", - &path.display() + &src_child.display() )) - })?), - io_semaphore, - ) - .await?; + })?; + let dst_child = profile_path.join(dst_child); + + // sleep for cpu for 1 millisecond + tokio::time::sleep(std::time::Duration::from_millis(1)).await; + + fetch::copy(&src_child, &dst_child, io_semaphore).await?; + + emit_loading(&loading_bar, 1.0, None).await?; } - Ok(()) + Ok(loading_bar) } -/// Recursively fs::copy every file in src to dest +/// Recursively get a list of all subfiles in src /// uses async recursion #[theseus_macros::debug_pin] #[async_recursion::async_recursion] #[tracing::instrument] -async fn copy_dir_to( - src: &Path, - dst: &Path, - io_semaphore: &IoSemaphore, -) -> crate::Result<()> { +async fn get_all_subfiles(src: &Path) -> crate::Result> { if !src.is_dir() { - fetch::copy(src, dst, io_semaphore).await?; - return Ok(()); + return Ok(vec![src.to_path_buf()]); } - // Create the destination directory - io::create_dir_all(&dst).await?; - - // Iterate over the directory + let mut files = Vec::new(); let mut dir = io::read_dir(&src).await?; while let Some(child) = dir .next_entry() @@ -294,21 +314,7 @@ async fn copy_dir_to( .map_err(|e| IOError::with_path(e, src))? { let src_child = child.path(); - let dst_child = dst.join(src_child.file_name().ok_or_else(|| { - crate::ErrorKind::InputError(format!( - "Invalid file: {}", - &src_child.display() - )) - })?); - - if src_child.is_dir() { - // Recurse into sub-directory - copy_dir_to(&src_child, &dst_child, io_semaphore).await?; - } else { - // Copy file - fetch::copy(&src_child, &dst_child, io_semaphore).await?; - } + files.append(&mut get_all_subfiles(&src_child).await?); } - - Ok(()) + Ok(files) } diff --git a/theseus/src/api/pack/install_from.rs b/theseus/src/api/pack/install_from.rs index 0b9266179..7685dfe73 100644 --- a/theseus/src/api/pack/install_from.rs +++ b/theseus/src/api/pack/install_from.rs @@ -65,7 +65,7 @@ pub enum EnvType { Server, } -#[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq, Debug)] +#[derive(Serialize, Deserialize, Clone, Copy, Hash, PartialEq, Eq, Debug)] #[serde(rename_all = "kebab-case")] pub enum PackDependency { Forge, @@ -101,6 +101,7 @@ pub struct CreatePackProfile { pub icon_url: Option, // the URL icon for a profile (ONLY USED FOR TEMPORARY PROFILES) pub linked_data: Option, // the linked project ID (mainly for modpacks)- used for updating pub skip_install_profile: Option, + pub no_watch: Option, } // default @@ -115,6 +116,7 @@ impl Default for CreatePackProfile { icon_url: None, linked_data: None, skip_install_profile: Some(true), + no_watch: Some(false), } } } diff --git a/theseus/src/api/profile/create.rs b/theseus/src/api/profile/create.rs index e6204d713..3289fd035 100644 --- a/theseus/src/api/profile/create.rs +++ b/theseus/src/api/profile/create.rs @@ -32,6 +32,7 @@ pub async fn profile_create( icon_url: Option, // the URL icon for a profile (ONLY USED FOR TEMPORARY PROFILES) linked_data: Option, // the linked project ID (mainly for modpacks)- used for updating skip_install_profile: Option, + no_watch: Option, ) -> crate::Result { name = profile::sanitize_profile_name(&name); @@ -112,7 +113,9 @@ pub async fn profile_create( { let mut profiles = state.profiles.write().await; - profiles.insert(profile.clone()).await?; + profiles + .insert(profile.clone(), no_watch.unwrap_or_default()) + .await?; } if !skip_install_profile.unwrap_or(false) { @@ -146,6 +149,7 @@ pub async fn profile_create_from_creator( profile.icon_url, profile.linked_data, profile.skip_install_profile, + profile.no_watch, ) .await } diff --git a/theseus/src/api/profile/mod.rs b/theseus/src/api/profile/mod.rs index e32490be6..793b880a7 100644 --- a/theseus/src/api/profile/mod.rs +++ b/theseus/src/api/profile/mod.rs @@ -974,7 +974,7 @@ pub async fn create_mrpack_json( // But the values are sanitized to only include the version number let dependencies = dependencies .into_iter() - .map(|(k, v)| (k, sanitize_loader_version_string(&v).to_string())) + .map(|(k, v)| (k, sanitize_loader_version_string(&v, k).to_string())) .collect::>(); let files: Result, crate::ErrorKind> = profile @@ -1043,18 +1043,26 @@ pub async fn create_mrpack_json( }) } -fn sanitize_loader_version_string(s: &str) -> &str { - // Split on '-' - // If two or more, take the second - // If one, take the first - // If none, take the whole thing - let mut split: std::str::Split<'_, char> = s.split('-'); - match split.next() { - Some(first) => match split.next() { - Some(second) => second, - None => first, - }, - None => s, +fn sanitize_loader_version_string(s: &str, loader: PackDependency) -> &str { + match loader { + // Split on '-' + // If two or more, take the second + // If one, take the first + // If none, take the whole thing + PackDependency::Forge => { + let mut split: std::str::Split<'_, char> = s.split('-'); + match split.next() { + Some(first) => match split.next() { + Some(second) => second, + None => first, + }, + None => s, + } + } + // For quilt, etc we take the whole thing, as it functions like: 0.20.0-beta.11 (and should not be split here) + PackDependency::QuiltLoader + | PackDependency::FabricLoader + | PackDependency::Minecraft => s, } } diff --git a/theseus/src/event/mod.rs b/theseus/src/event/mod.rs index 6436c8314..b0dd2f010 100644 --- a/theseus/src/event/mod.rs +++ b/theseus/src/event/mod.rs @@ -185,6 +185,10 @@ pub enum LoadingBarType { ConfigChange { new_path: PathBuf, }, + CopyProfile { + import_location: PathBuf, + profile_name: String, + }, } #[derive(Serialize, Clone)] diff --git a/theseus/src/launcher/download.rs b/theseus/src/launcher/download.rs index 9d24988da..001886268 100644 --- a/theseus/src/launcher/download.rs +++ b/theseus/src/launcher/download.rs @@ -320,7 +320,7 @@ pub async fn download_libraries( let reader = std::io::Cursor::new(&data); if let Ok(mut archive) = zip::ZipArchive::new(reader) { match archive.extract(st.directories.version_natives_dir(version).await) { - Ok(_) => tracing::info!("Fetched native {}", &library.name), + Ok(_) => tracing::debug!("Fetched native {}", &library.name), Err(err) => tracing::error!("Failed extracting native {}. err: {}", &library.name, err) } } else { diff --git a/theseus/src/launcher/mod.rs b/theseus/src/launcher/mod.rs index f9d546f61..50aa7592c 100644 --- a/theseus/src/launcher/mod.rs +++ b/theseus/src/launcher/mod.rs @@ -461,28 +461,28 @@ pub async fn launch_minecraft( // Uses 'a:b' syntax which is not quite yaml use regex::Regex; - let options_path = instance_path.join("options.txt"); - let mut options_string = String::new(); - - if options_path.exists() { - options_string = io::read_to_string(&options_path).await?; - } - - for (key, value) in mc_set_options { - let re = Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?; - // check if the regex exists in the file - if !re.is_match(&options_string) { - // The key was not found in the file, so append it - options_string.push_str(&format!("\n{}:{}", key, value)); - } else { - let replaced_string = re - .replace_all(&options_string, &format!("{}:{}", key, value)) - .to_string(); - options_string = replaced_string; + if !mc_set_options.is_empty() { + let options_path = instance_path.join("options.txt"); + let mut options_string = String::new(); + if options_path.exists() { + options_string = io::read_to_string(&options_path).await?; + } + for (key, value) in mc_set_options { + let re = Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?; + // check if the regex exists in the file + if !re.is_match(&options_string) { + // The key was not found in the file, so append it + options_string.push_str(&format!("\n{}:{}", key, value)); + } else { + let replaced_string = re + .replace_all(&options_string, &format!("{}:{}", key, value)) + .to_string(); + options_string = replaced_string; + } } - } - io::write(&options_path, options_string).await?; + io::write(&options_path, options_string).await?; + } // Get Modrinth logs directories let datetime_string = diff --git a/theseus/src/state/mod.rs b/theseus/src/state/mod.rs index 4276e62f2..53db032c4 100644 --- a/theseus/src/state/mod.rs +++ b/theseus/src/state/mod.rs @@ -353,19 +353,17 @@ pub async fn init_watcher() -> crate::Result> { }) }, )?; - tokio::task::spawn(async move { + let span = tracing::span!(tracing::Level::INFO, "init_watcher"); + tracing::info!(parent: &span, "Initting watcher"); while let Some(res) = rx.next().await { + let _span = span.enter(); match res { Ok(mut events) => { let mut visited_paths = Vec::new(); // sort events by e.path events.sort_by(|a, b| a.path.cmp(&b.path)); events.iter().for_each(|e| { - tracing::debug!( - "File watcher event: {:?}", - serde_json::to_string(&e.path).unwrap() - ); let mut new_path = PathBuf::new(); let mut components_iterator = e.path.components(); let mut found = false; diff --git a/theseus/src/state/profiles.rs b/theseus/src/state/profiles.rs index 8f9a6b587..b67072a74 100644 --- a/theseus/src/state/profiles.rs +++ b/theseus/src/state/profiles.rs @@ -331,14 +331,13 @@ impl Profile { } pub fn sync_projects_task(profile_path_id: ProfilePathId, force: bool) { + let span = tracing::span!( + tracing::Level::INFO, + "sync_projects_task", + ?profile_path_id, + ?force + ); tokio::task::spawn(async move { - let span = - tracing::span!(tracing::Level::INFO, "sync_projects_task"); - tracing::debug!( - parent: &span, - "Syncing projects for profile {}", - profile_path_id - ); let res = async { let _span = span.enter(); let state = State::get().await?; @@ -840,12 +839,17 @@ impl Profiles { drop(creds); // 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 + // so we can just take the first one for which the loader matches 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) { + let loader = profile.metadata.loader; + let recent_version = versions.iter().find(|x| { + x.loaders + .contains(&loader.as_api_str().to_string()) + }); + if let Some(recent_version) = recent_version { profile.modrinth_update_version = Some(recent_version.id.clone()); } else { @@ -879,7 +883,11 @@ impl Profiles { #[tracing::instrument(skip(self, profile))] #[theseus_macros::debug_pin] - pub async fn insert(&mut self, profile: Profile) -> crate::Result<&Self> { + pub async fn insert( + &mut self, + profile: Profile, + no_watch: bool, + ) -> crate::Result<&Self> { emit_profile( profile.uuid, &profile.profile_id(), @@ -888,13 +896,15 @@ impl Profiles { ) .await?; - let state = State::get().await?; - let mut file_watcher = state.file_watcher.write().await; - Profile::watch_fs( - &profile.get_profile_full_path().await?, - &mut file_watcher, - ) - .await?; + if !no_watch { + let state = State::get().await?; + let mut file_watcher = state.file_watcher.write().await; + Profile::watch_fs( + &profile.get_profile_full_path().await?, + &mut file_watcher, + ) + .await?; + } let profile_name = profile.profile_id(); profile_name.check_valid_utf()?; @@ -986,6 +996,7 @@ impl Profiles { dirs, ) .await?, + false, ) .await?; Profile::sync_projects_task(profile_path_id, false); diff --git a/theseus/src/state/projects.rs b/theseus/src/state/projects.rs index 02e8fe2b6..cab0b7822 100644 --- a/theseus/src/state/projects.rs +++ b/theseus/src/state/projects.rs @@ -281,7 +281,6 @@ pub async fn infer_data_from_files( ) -> crate::Result> { let mut file_path_hashes = HashMap::new(); - // TODO: Make this concurrent and use progressive hashing to avoid loading each JAR in memory for path in paths { if !path.exists() { continue; @@ -297,10 +296,19 @@ pub async fn infer_data_from_files( .await .map_err(|e| IOError::with_path(e, &path))?; - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer).await.map_err(IOError::from)?; + let mut buffer = [0u8; 4096]; // Buffer to read chunks + let mut hasher = sha2::Sha512::new(); // Hasher - let hash = format!("{:x}", sha2::Sha512::digest(&buffer)); + loop { + let bytes_read = + file.read(&mut buffer).await.map_err(IOError::from)?; + if bytes_read == 0 { + break; + } + hasher.update(&buffer[..bytes_read]); + } + + let hash = format!("{:x}", hasher.finalize()); file_path_hashes.insert(hash, path.clone()); } diff --git a/theseus_cli/src/subcommands/profile.rs b/theseus_cli/src/subcommands/profile.rs index 97a42287d..b6d506edf 100644 --- a/theseus_cli/src/subcommands/profile.rs +++ b/theseus_cli/src/subcommands/profile.rs @@ -196,6 +196,7 @@ impl ProfileInit { None, None, None, + None, ) .await?; diff --git a/theseus_gui/dist/splashscreen.html b/theseus_gui/dist/splashscreen.html deleted file mode 100644 index c70a3279f..000000000 --- a/theseus_gui/dist/splashscreen.html +++ /dev/null @@ -1,151 +0,0 @@ - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
Loading...
-
- - - diff --git a/theseus_gui/src-tauri/src/api/profile_create.rs b/theseus_gui/src-tauri/src/api/profile_create.rs index 2d9b1ad13..2cbf6548a 100644 --- a/theseus_gui/src-tauri/src/api/profile_create.rs +++ b/theseus_gui/src-tauri/src/api/profile_create.rs @@ -17,6 +17,7 @@ pub async fn profile_create( modloader: ModLoader, // the modloader to use loader_version: Option, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader icon: Option, // the icon for the profile + no_watch: Option, ) -> Result { let res = profile::create::profile_create( name, @@ -27,6 +28,7 @@ pub async fn profile_create( None, None, None, + no_watch, ) .await?; Ok(res) diff --git a/theseus_gui/src/App.vue b/theseus_gui/src/App.vue index 4743be295..a70cde19c 100644 --- a/theseus_gui/src/App.vue +++ b/theseus_gui/src/App.vue @@ -177,6 +177,20 @@ document.querySelector('body').addEventListener('click', function (e) { } }) +document.querySelector('body').addEventListener('auxclick', function (e) { + // disables middle click -> new tab + if (e.button === 1) { + e.preventDefault() + // instead do a left click + const event = new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true, + }) + e.target.dispatchEvent(event) + } +}) + const accounts = ref(null) command_listener((e) => { diff --git a/theseus_gui/src/assets/stylesheets/global.scss b/theseus_gui/src/assets/stylesheets/global.scss index 703d1f8fb..9b04b4eff 100644 --- a/theseus_gui/src/assets/stylesheets/global.scss +++ b/theseus_gui/src/assets/stylesheets/global.scss @@ -128,3 +128,10 @@ input { background-color: var(--color-raised-bg); box-shadow: none !important; } + +img { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} diff --git a/theseus_gui/src/components/GridDisplay.vue b/theseus_gui/src/components/GridDisplay.vue index 90a2d3a83..4654df8c3 100644 --- a/theseus_gui/src/components/GridDisplay.vue +++ b/theseus_gui/src/components/GridDisplay.vue @@ -200,6 +200,25 @@ const filteredResults = computed(() => { return instanceMap.set('None', instances) } + // For 'name', we intuitively expect the sorting to apply to the name of the group first, not just the name of the instance + // ie: Category A should come before B, even if the first instance in B comes before the first instance in A + if (sortBy.value === 'Name') { + const sortedEntries = [...instanceMap.entries()].sort((a, b) => { + // None should always be first + if (a[0] === 'None' && b[0] !== 'None') { + return -1 + } + if (a[0] !== 'None' && b[0] === 'None') { + return 1 + } + return a[0].localeCompare(b[0]) + }) + instanceMap.clear() + sortedEntries.forEach((entry) => { + instanceMap.set(entry[0], entry[1]) + }) + } + return instanceMap }) @@ -265,7 +284,7 @@ const filteredResults = computed(() => { diff --git a/theseus_gui/src/components/ui/IncompatibilityWarningModal.vue b/theseus_gui/src/components/ui/IncompatibilityWarningModal.vue index 92ee8106c..101d1ffbe 100644 --- a/theseus_gui/src/components/ui/IncompatibilityWarningModal.vue +++ b/theseus_gui/src/components/ui/IncompatibilityWarningModal.vue @@ -165,5 +165,9 @@ td:first-child { flex-direction: column; gap: 1rem; padding: 1rem; + + :deep(.animated-dropdown .options) { + max-height: 13.375rem; + } } diff --git a/theseus_gui/src/components/ui/ModInstallModal.vue b/theseus_gui/src/components/ui/ModInstallModal.vue index e8221b70d..a9149dc56 100644 --- a/theseus_gui/src/components/ui/ModInstallModal.vue +++ b/theseus_gui/src/components/ui/ModInstallModal.vue @@ -183,7 +183,7 @@ const createInstance = async () => { await router.push(`/instance/${encodeURIComponent(id)}/`) const instance = await get(id, true) - await installVersionDependencies(instance, versions.value) + await installVersionDependencies(instance, versions.value[0]) mixpanel_track('InstanceCreate', { profile_name: name.value, @@ -204,7 +204,7 @@ const createInstance = async () => { source: 'ProjectInstallModal', }) - installModal.value.hide() + if (installModal.value) installModal.value.hide() creatingInstance.value = false } diff --git a/theseus_gui/src/helpers/import.js b/theseus_gui/src/helpers/import.js index c1e91d5c0..cfaa3ad1e 100644 --- a/theseus_gui/src/helpers/import.js +++ b/theseus_gui/src/helpers/import.js @@ -34,7 +34,9 @@ export async function get_importable_instances(launcherType, basePath) { /// eg: import_instance("profile-name-to-go-to", "MultiMC", "C:/MultiMC", "Instance 1") export async function import_instance(launcherType, basePath, instanceFolder) { // create a basic, empty instance (most properties will be filled in by the import process) - const profilePath = await create(instanceFolder, '1.19.4', 'vanilla', 'latest', null) + // We do NOT watch the fs for changes to avoid duplicate events during installation + // fs watching will be enabled once the instance is imported + const profilePath = await create(instanceFolder, '1.19.4', 'vanilla', 'latest', null, true) return await invoke('plugin:import|import_import_instance', { profilePath, diff --git a/theseus_gui/src/helpers/metadata.js b/theseus_gui/src/helpers/metadata.js index a8d85c3d1..11f424ff6 100644 --- a/theseus_gui/src/helpers/metadata.js +++ b/theseus_gui/src/helpers/metadata.js @@ -9,17 +9,23 @@ export async function get_game_versions() { // Gets the fabric versions from daedalus // Returns Manifest export async function get_fabric_versions() { - return await invoke('plugin:metadata|metadata_get_fabric_versions') + const c = await invoke('plugin:metadata|metadata_get_fabric_versions') + console.log('Getting fabric versions', c) + return c } // Gets the forge versions from daedalus // Returns Manifest export async function get_forge_versions() { - return await invoke('plugin:metadata|metadata_get_forge_versions') + const c = await invoke('plugin:metadata|metadata_get_forge_versions') + console.log('Getting forge versions', c) + return c } // Gets the quilt versions from daedalus // Returns Manifest export async function get_quilt_versions() { - return await invoke('plugin:metadata|metadata_get_quilt_versions') + const c = await invoke('plugin:metadata|metadata_get_quilt_versions') + console.log('Getting quilt versions', c) + return c } diff --git a/theseus_gui/src/helpers/profile.js b/theseus_gui/src/helpers/profile.js index 21944d76e..5f270bf2c 100644 --- a/theseus_gui/src/helpers/profile.js +++ b/theseus_gui/src/helpers/profile.js @@ -16,13 +16,14 @@ import { invoke } from '@tauri-apps/api/tauri' - icon is a path to an image file, which will be copied into the profile directory */ -export async function create(name, gameVersion, modloader, loaderVersion, icon) { +export async function create(name, gameVersion, modloader, loaderVersion, icon, noWatch) { return await invoke('plugin:profile_create|profile_create', { name, gameVersion, modloader, loaderVersion, icon, + noWatch, }) } diff --git a/theseus_gui/src/pages/Browse.vue b/theseus_gui/src/pages/Browse.vue index dc53aa636..172a8e0f9 100644 --- a/theseus_gui/src/pages/Browse.vue +++ b/theseus_gui/src/pages/Browse.vue @@ -237,22 +237,22 @@ async function refreshSearch() { let val = `${base}${url}` - const rawResults = await useFetch(val, 'search results', offline.value) - results.value = rawResults + let rawResults = await useFetch(val, 'search results', offline.value) if (!rawResults) { - results.value = { + rawResults = { hits: [], total_hits: 0, limit: 1, } } if (instanceContext.value) { - for (let val of results.value.hits) { + for (val of rawResults.hits) { val.installed = await check_installed(instanceContext.value.path, val.project_id).then( (x) => (val.installed = x) ) } } + results.value = rawResults } async function onSearchChange(newPageNumber) { @@ -262,7 +262,6 @@ async function onSearchChange(newPageNumber) { return } await refreshSearch() - const obj = getSearchUrl((currentPage.value - 1) * maxResults.value, true) // Only replace in router if the query is different diff --git a/theseus_gui/src/pages/Settings.vue b/theseus_gui/src/pages/Settings.vue index 4bd951630..c851ca87b 100644 --- a/theseus_gui/src/pages/Settings.vue +++ b/theseus_gui/src/pages/Settings.vue @@ -311,7 +311,16 @@ async function refreshDir() { customize your experience. Opting out will disable this data collection. - + @@ -425,7 +434,7 @@ async function refreshDir() { { if (logs.value.length > 0) { logs.value[0] = await getLiveLog() - if (selectedLogIndex.value === 0 && !userScrolled.value) { - await nextTick() - isAutoScrolling.value = true - logContainer.value.scrollTop = - logContainer.value.scrollHeight - logContainer.value.offsetHeight - setTimeout(() => (isAutoScrolling.value = false), 50) + // Allow resetting of userScrolled if the user scrolls to the bottom + if (selectedLogIndex.value === 0) { + if ( + logContainer.value.scrollTop + logContainer.value.offsetHeight >= + logContainer.value.scrollHeight - 10 + ) + userScrolled.value = false + + if (!userScrolled.value) { + await nextTick() + isAutoScrolling.value = true + logContainer.value.scrollTop = + logContainer.value.scrollHeight - logContainer.value.offsetHeight + setTimeout(() => (isAutoScrolling.value = false), 50) + } } } }, 250) diff --git a/theseus_gui/src/pages/instance/Options.vue b/theseus_gui/src/pages/instance/Options.vue index e00ac126b..0d11f6e03 100644 --- a/theseus_gui/src/pages/instance/Options.vue +++ b/theseus_gui/src/pages/instance/Options.vue @@ -204,7 +204,9 @@
diff --git a/theseus_gui/src/pages/project/Gallery.vue b/theseus_gui/src/pages/project/Gallery.vue index 81578b261..ef6f231ea 100644 --- a/theseus_gui/src/pages/project/Gallery.vue +++ b/theseus_gui/src/pages/project/Gallery.vue @@ -50,6 +50,7 @@ 0">

Dependencies

- +
{{ dependency.title }}
diff --git a/theseus_playground/src/main.rs b/theseus_playground/src/main.rs index 7e04e345d..40a000a3d 100644 --- a/theseus_playground/src/main.rs +++ b/theseus_playground/src/main.rs @@ -85,6 +85,7 @@ async fn main() -> theseus::Result<()> { None, None, None, + None, ) .await?;