Beta bugs (#562)

* fixed bugs

* added logging for atlauncher

* draft: improving imports time

* more improvements

* more

* prettier, etc

* small changes

* emma suggested change

* rev

* removed atlauncher debug
This commit is contained in:
Wyatt Verchere 2023-08-14 13:23:42 -07:00 committed by GitHub
parent a1a5b8ed9c
commit d6ee1ff25a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 357 additions and 321 deletions

View File

@ -7,10 +7,13 @@ use crate::ErrorKind;
pub async fn authenticate_begin_flow(provider: &str) -> crate::Result<String> { pub async fn authenticate_begin_flow(provider: &str) -> crate::Result<String> {
let state = crate::State::get().await?; 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<ModrinthAuthFlow>> =
state.modrinth_auth_flow.write().await;
let mut flow = ModrinthAuthFlow::new(provider).await?; let mut flow = ModrinthAuthFlow::new(provider).await?;
let url = flow.prepare_login_url().await?; let url = flow.prepare_login_url().await?;
let mut write = state.modrinth_auth_flow.write().await;
*write = Some(flow); *write = Some(flow);
Ok(url) Ok(url)

View File

@ -3,13 +3,12 @@ use std::{collections::HashMap, path::PathBuf};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
event::LoadingBarId,
pack::{ pack::{
self, self,
import::{self, copy_dotminecraft}, import::{self, copy_dotminecraft},
install_from::CreatePackDescription, install_from::CreatePackDescription,
}, },
prelude::{ModLoader, ProfilePathId}, prelude::{ModLoader, Profile, ProfilePathId},
state::{LinkedData, ProfileInstallStage}, state::{LinkedData, ProfileInstallStage},
util::io, util::io,
State, State,
@ -33,8 +32,6 @@ pub struct ATLauncher {
pub modrinth_project: Option<ATLauncherModrinthProject>, pub modrinth_project: Option<ATLauncherModrinthProject>,
pub modrinth_version: Option<ATLauncherModrinthVersion>, pub modrinth_version: Option<ATLauncherModrinthVersion>,
pub modrinth_manifest: Option<pack::install_from::PackFormat>, pub modrinth_manifest: Option<pack::install_from::PackFormat>,
pub mods: Vec<ATLauncherMod>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -57,13 +54,9 @@ pub struct ATLauncherModrinthProject {
pub slug: String, pub slug: String,
pub project_type: String, pub project_type: String,
pub team: String, pub team: String,
pub title: String,
pub description: String,
pub body: String,
pub client_side: Option<String>, pub client_side: Option<String>,
pub server_side: Option<String>, pub server_side: Option<String>,
pub categories: Vec<String>, pub categories: Vec<String>,
pub icon_url: String,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -110,7 +103,16 @@ pub async fn is_valid_atlauncher(instance_folder: PathBuf) -> bool {
.unwrap_or("".to_string()); .unwrap_or("".to_string());
let instance: Result<ATInstance, serde_json::Error> = let instance: Result<ATInstance, serde_json::Error> =
serde_json::from_str::<ATInstance>(&instance); serde_json::from_str::<ATInstance>(&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] #[tracing::instrument]
@ -169,7 +171,6 @@ pub async fn import_atlauncher(
backup_name, backup_name,
description, description,
atinstance, atinstance,
None,
) )
.await?; .await?;
Ok(()) Ok(())
@ -181,7 +182,6 @@ async fn import_atlauncher_unmanaged(
backup_name: String, backup_name: String,
description: CreatePackDescription, description: CreatePackDescription,
atinstance: ATInstance, atinstance: ATInstance,
existing_loading_bar: Option<LoadingBarId>,
) -> crate::Result<()> { ) -> crate::Result<()> {
let mod_loader = format!( let mod_loader = format!(
"\"{}\"", "\"{}\"",
@ -230,19 +230,28 @@ async fn import_atlauncher_unmanaged(
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc) // Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
let state = State::get().await?; let state = State::get().await?;
copy_dotminecraft( let loading_bar = copy_dotminecraft(
profile_path.clone(), profile_path.clone(),
minecraft_folder, minecraft_folder,
&state.io_semaphore, &state.io_semaphore,
None,
) )
.await?; .await?;
if let Some(profile_val) = if let Some(profile_val) =
crate::api::profile::get(&profile_path, None).await? 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?; .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?; State::sync().await?;
} }
Ok(()) Ok(())

View File

@ -2,6 +2,7 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::prelude::Profile;
use crate::state::CredentialsStore; use crate::state::CredentialsStore;
use crate::{ use crate::{
prelude::{ModLoader, ProfilePathId}, prelude::{ModLoader, ProfilePathId},
@ -187,18 +188,29 @@ pub async fn import_curseforge(
// Copy in contained folders as overrides // Copy in contained folders as overrides
let state = State::get().await?; let state = State::get().await?;
copy_dotminecraft( let loading_bar = copy_dotminecraft(
profile_path.clone(), profile_path.clone(),
curseforge_instance_folder, curseforge_instance_folder,
&state.io_semaphore, &state.io_semaphore,
None,
) )
.await?; .await?;
if let Some(profile_val) = if let Some(profile_val) =
crate::api::profile::get(&profile_path, None).await? 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?; State::sync().await?;
} }

View File

@ -3,7 +3,7 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
prelude::{ModLoader, ProfilePathId}, prelude::{ModLoader, Profile, ProfilePathId},
state::ProfileInstallStage, state::ProfileInstallStage,
util::io, util::io,
State, State,
@ -101,18 +101,28 @@ pub async fn import_gdlauncher(
// Copy in contained folders as overrides // Copy in contained folders as overrides
let state = State::get().await?; let state = State::get().await?;
copy_dotminecraft( let loading_bar = copy_dotminecraft(
profile_path.clone(), profile_path.clone(),
gdlauncher_instance_folder, gdlauncher_instance_folder,
&state.io_semaphore, &state.io_semaphore,
None,
) )
.await?; .await?;
if let Some(profile_val) = if let Some(profile_val) =
crate::api::profile::get(&profile_path, None).await? 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?; State::sync().await?;
} }

View File

@ -7,7 +7,7 @@ use crate::{
import::{self, copy_dotminecraft}, import::{self, copy_dotminecraft},
install_from::{self, CreatePackDescription, PackDependency}, install_from::{self, CreatePackDescription, PackDependency},
}, },
prelude::ProfilePathId, prelude::{Profile, ProfilePathId},
util::io, util::io,
State, State,
}; };
@ -119,6 +119,26 @@ pub struct MMCComponentRequirement {
pub suggests: Option<String>, pub suggests: Option<String>,
} }
#[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 // Checks if if its a folder, and the folder contains instance.cfg and mmc-pack.json, and they both parse
#[tracing::instrument] #[tracing::instrument]
pub async fn is_valid_mmc(instance_folder: PathBuf) -> bool { 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::<MMCPack>(&mmc_pack).is_ok() && serde_json::from_str::<MMCPack>(&mmc_pack).is_ok()
} }
#[tracing::instrument]
pub async fn get_instances_subpath(config: PathBuf) -> Option<String> {
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 // Loading the INI (instance.cfg) file
async fn load_instance_cfg(file_path: &Path) -> crate::Result<MMCInstance> { async fn load_instance_cfg(file_path: &Path) -> crate::Result<MMCInstance> {
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 = let instance_cfg_enum: MMCInstanceEnum =
serde_ini::from_str::<MMCInstanceEnum>(&instance_cfg)?; serde_ini::from_str::<MMCInstanceEnum>(&instance_cfg)?;
match instance_cfg_enum { match instance_cfg_enum {
@ -281,18 +311,28 @@ async fn import_mmc_unmanaged(
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc) // Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
let state = State::get().await?; let state = State::get().await?;
copy_dotminecraft( let loading_bar = copy_dotminecraft(
profile_path.clone(), profile_path.clone(),
minecraft_folder, minecraft_folder,
&state.io_semaphore, &state.io_semaphore,
None,
) )
.await?; .await?;
if let Some(profile_val) = if let Some(profile_val) =
crate::api::profile::get(&profile_path, None).await? 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?; State::sync().await?;
} }
Ok(()) Ok(())

View File

@ -7,6 +7,10 @@ use io::IOError;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
event::{
emit::{emit_loading, init_or_edit_loading},
LoadingBarId,
},
prelude::ProfilePathId, prelude::ProfilePathId,
state::Profiles, state::Profiles,
util::{ util::{
@ -51,11 +55,20 @@ pub async fn get_importable_instances(
) -> crate::Result<Vec<String>> { ) -> crate::Result<Vec<String>> {
// Some launchers have a different folder structure for instances // Some launchers have a different folder structure for instances
let instances_subfolder = match launcher_type { let instances_subfolder = match launcher_type {
ImportLauncherType::GDLauncher ImportLauncherType::GDLauncher | ImportLauncherType::ATLauncher => {
| ImportLauncherType::MultiMC "instances".to_string()
| ImportLauncherType::PrismLauncher }
| ImportLauncherType::ATLauncher => "instances", ImportLauncherType::Curseforge => "Instances".to_string(),
ImportLauncherType::Curseforge => "Instances", 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 => { ImportLauncherType::Unknown => {
return Err(crate::ErrorKind::InputError( return Err(crate::ErrorKind::InputError(
"Launcher type Unknown".to_string(), "Launcher type Unknown".to_string(),
@ -63,7 +76,8 @@ pub async fn get_importable_instances(
.into()) .into())
} }
}; };
let instances_folder = base_path.join(instances_subfolder);
let instances_folder = base_path.join(&instances_subfolder);
let mut instances = Vec::new(); let mut instances = Vec::new();
let mut dir = io::read_dir(&instances_folder).await.map_err(| _ | { let mut dir = io::read_dir(&instances_folder).await.map_err(| _ | {
crate::ErrorKind::InputError(format!( crate::ErrorKind::InputError(format!(
@ -238,55 +252,61 @@ pub async fn recache_icon(
} }
async fn copy_dotminecraft( async fn copy_dotminecraft(
profile_path: ProfilePathId, profile_path_id: ProfilePathId,
dotminecraft: PathBuf, dotminecraft: PathBuf,
io_semaphore: &IoSemaphore, io_semaphore: &IoSemaphore,
) -> crate::Result<()> { existing_loading_bar: Option<LoadingBarId>,
) -> crate::Result<LoadingBarId> {
// Get full path to profile // 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 // Gets all subfiles recursively in src
let mut dir = io::read_dir(&dotminecraft).await?; let subfiles = get_all_subfiles(&dotminecraft).await?;
while let Some(entry) = dir let total_subfiles = subfiles.len() as u64;
.next_entry()
.await let loading_bar = init_or_edit_loading(
.map_err(|e| IOError::with_path(e, &dotminecraft))? existing_loading_bar,
{ crate::LoadingBarType::CopyProfile {
let path = entry.path(); import_location: dotminecraft.clone(),
copy_dir_to( profile_name: profile_path_id.to_string(),
&path, },
&profile_path.join(path.file_name().ok_or_else(|| { 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!( crate::ErrorKind::InputError(format!(
"Invalid file: {}", "Invalid file: {}",
&path.display() &src_child.display()
)) ))
})?), })?;
io_semaphore, let dst_child = profile_path.join(dst_child);
)
.await?; // 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 /// uses async recursion
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
#[async_recursion::async_recursion] #[async_recursion::async_recursion]
#[tracing::instrument] #[tracing::instrument]
async fn copy_dir_to( async fn get_all_subfiles(src: &Path) -> crate::Result<Vec<PathBuf>> {
src: &Path,
dst: &Path,
io_semaphore: &IoSemaphore,
) -> crate::Result<()> {
if !src.is_dir() { if !src.is_dir() {
fetch::copy(src, dst, io_semaphore).await?; return Ok(vec![src.to_path_buf()]);
return Ok(());
} }
// Create the destination directory let mut files = Vec::new();
io::create_dir_all(&dst).await?;
// Iterate over the directory
let mut dir = io::read_dir(&src).await?; let mut dir = io::read_dir(&src).await?;
while let Some(child) = dir while let Some(child) = dir
.next_entry() .next_entry()
@ -294,21 +314,7 @@ async fn copy_dir_to(
.map_err(|e| IOError::with_path(e, src))? .map_err(|e| IOError::with_path(e, src))?
{ {
let src_child = child.path(); let src_child = child.path();
let dst_child = dst.join(src_child.file_name().ok_or_else(|| { files.append(&mut get_all_subfiles(&src_child).await?);
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?;
}
} }
Ok(files)
Ok(())
} }

View File

@ -65,7 +65,7 @@ pub enum EnvType {
Server, Server,
} }
#[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq, Debug)] #[derive(Serialize, Deserialize, Clone, Copy, Hash, PartialEq, Eq, Debug)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum PackDependency { pub enum PackDependency {
Forge, Forge,
@ -101,6 +101,7 @@ pub struct CreatePackProfile {
pub icon_url: Option<String>, // the URL icon for a profile (ONLY USED FOR TEMPORARY PROFILES) pub icon_url: Option<String>, // the URL icon for a profile (ONLY USED FOR TEMPORARY PROFILES)
pub linked_data: Option<LinkedData>, // the linked project ID (mainly for modpacks)- used for updating pub linked_data: Option<LinkedData>, // the linked project ID (mainly for modpacks)- used for updating
pub skip_install_profile: Option<bool>, pub skip_install_profile: Option<bool>,
pub no_watch: Option<bool>,
} }
// default // default
@ -115,6 +116,7 @@ impl Default for CreatePackProfile {
icon_url: None, icon_url: None,
linked_data: None, linked_data: None,
skip_install_profile: Some(true), skip_install_profile: Some(true),
no_watch: Some(false),
} }
} }
} }

View File

@ -32,6 +32,7 @@ pub async fn profile_create(
icon_url: Option<String>, // the URL icon for a profile (ONLY USED FOR TEMPORARY PROFILES) icon_url: Option<String>, // the URL icon for a profile (ONLY USED FOR TEMPORARY PROFILES)
linked_data: Option<LinkedData>, // the linked project ID (mainly for modpacks)- used for updating linked_data: Option<LinkedData>, // the linked project ID (mainly for modpacks)- used for updating
skip_install_profile: Option<bool>, skip_install_profile: Option<bool>,
no_watch: Option<bool>,
) -> crate::Result<ProfilePathId> { ) -> crate::Result<ProfilePathId> {
name = profile::sanitize_profile_name(&name); name = profile::sanitize_profile_name(&name);
@ -112,7 +113,9 @@ pub async fn profile_create(
{ {
let mut profiles = state.profiles.write().await; 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) { if !skip_install_profile.unwrap_or(false) {
@ -146,6 +149,7 @@ pub async fn profile_create_from_creator(
profile.icon_url, profile.icon_url,
profile.linked_data, profile.linked_data,
profile.skip_install_profile, profile.skip_install_profile,
profile.no_watch,
) )
.await .await
} }

View File

@ -974,7 +974,7 @@ pub async fn create_mrpack_json(
// But the values are sanitized to only include the version number // But the values are sanitized to only include the version number
let dependencies = dependencies let dependencies = dependencies
.into_iter() .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::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
let files: Result<Vec<PackFile>, crate::ErrorKind> = profile let files: Result<Vec<PackFile>, crate::ErrorKind> = profile
@ -1043,18 +1043,26 @@ pub async fn create_mrpack_json(
}) })
} }
fn sanitize_loader_version_string(s: &str) -> &str { fn sanitize_loader_version_string(s: &str, loader: PackDependency) -> &str {
// Split on '-' match loader {
// If two or more, take the second // Split on '-'
// If one, take the first // If two or more, take the second
// If none, take the whole thing // If one, take the first
let mut split: std::str::Split<'_, char> = s.split('-'); // If none, take the whole thing
match split.next() { PackDependency::Forge => {
Some(first) => match split.next() { let mut split: std::str::Split<'_, char> = s.split('-');
Some(second) => second, match split.next() {
None => first, Some(first) => match split.next() {
}, Some(second) => second,
None => s, 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,
} }
} }

View File

@ -185,6 +185,10 @@ pub enum LoadingBarType {
ConfigChange { ConfigChange {
new_path: PathBuf, new_path: PathBuf,
}, },
CopyProfile {
import_location: PathBuf,
profile_name: String,
},
} }
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]

View File

@ -320,7 +320,7 @@ pub async fn download_libraries(
let reader = std::io::Cursor::new(&data); let reader = std::io::Cursor::new(&data);
if let Ok(mut archive) = zip::ZipArchive::new(reader) { if let Ok(mut archive) = zip::ZipArchive::new(reader) {
match archive.extract(st.directories.version_natives_dir(version).await) { 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) Err(err) => tracing::error!("Failed extracting native {}. err: {}", &library.name, err)
} }
} else { } else {

View File

@ -461,28 +461,28 @@ pub async fn launch_minecraft(
// Uses 'a:b' syntax which is not quite yaml // Uses 'a:b' syntax which is not quite yaml
use regex::Regex; use regex::Regex;
let options_path = instance_path.join("options.txt"); if !mc_set_options.is_empty() {
let mut options_string = String::new(); let options_path = instance_path.join("options.txt");
let mut options_string = String::new();
if options_path.exists() { if options_path.exists() {
options_string = io::read_to_string(&options_path).await?; options_string = io::read_to_string(&options_path).await?;
} }
for (key, value) in mc_set_options {
for (key, value) in mc_set_options { let re = Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?;
let re = Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?; // check if the regex exists in the file
// check if the regex exists in the file if !re.is_match(&options_string) {
if !re.is_match(&options_string) { // The key was not found in the file, so append it
// The key was not found in the file, so append it options_string.push_str(&format!("\n{}:{}", key, value));
options_string.push_str(&format!("\n{}:{}", key, value)); } else {
} else { let replaced_string = re
let replaced_string = re .replace_all(&options_string, &format!("{}:{}", key, value))
.replace_all(&options_string, &format!("{}:{}", key, value)) .to_string();
.to_string(); options_string = replaced_string;
options_string = replaced_string; }
} }
}
io::write(&options_path, options_string).await?; io::write(&options_path, options_string).await?;
}
// Get Modrinth logs directories // Get Modrinth logs directories
let datetime_string = let datetime_string =

View File

@ -353,19 +353,17 @@ pub async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
}) })
}, },
)?; )?;
tokio::task::spawn(async move { 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 { while let Some(res) = rx.next().await {
let _span = span.enter();
match res { match res {
Ok(mut events) => { Ok(mut events) => {
let mut visited_paths = Vec::new(); let mut visited_paths = Vec::new();
// sort events by e.path // sort events by e.path
events.sort_by(|a, b| a.path.cmp(&b.path)); events.sort_by(|a, b| a.path.cmp(&b.path));
events.iter().for_each(|e| { 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 new_path = PathBuf::new();
let mut components_iterator = e.path.components(); let mut components_iterator = e.path.components();
let mut found = false; let mut found = false;

View File

@ -331,14 +331,13 @@ impl Profile {
} }
pub fn sync_projects_task(profile_path_id: ProfilePathId, force: bool) { 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 { 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 res = async {
let _span = span.enter(); let _span = span.enter();
let state = State::get().await?; let state = State::get().await?;
@ -840,12 +839,17 @@ impl Profiles {
drop(creds); drop(creds);
// Versions are pre-sorted in labrinth (by versions.sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published));) // 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; let mut new_profiles = state.profiles.write().await;
if let Some(profile) = if let Some(profile) =
new_profiles.0.get_mut(&profile_path) 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 = profile.modrinth_update_version =
Some(recent_version.id.clone()); Some(recent_version.id.clone());
} else { } else {
@ -879,7 +883,11 @@ impl Profiles {
#[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,
no_watch: bool,
) -> crate::Result<&Self> {
emit_profile( emit_profile(
profile.uuid, profile.uuid,
&profile.profile_id(), &profile.profile_id(),
@ -888,13 +896,15 @@ impl Profiles {
) )
.await?; .await?;
let state = State::get().await?; if !no_watch {
let mut file_watcher = state.file_watcher.write().await; let state = State::get().await?;
Profile::watch_fs( let mut file_watcher = state.file_watcher.write().await;
&profile.get_profile_full_path().await?, Profile::watch_fs(
&mut file_watcher, &profile.get_profile_full_path().await?,
) &mut file_watcher,
.await?; )
.await?;
}
let profile_name = profile.profile_id(); let profile_name = profile.profile_id();
profile_name.check_valid_utf()?; profile_name.check_valid_utf()?;
@ -986,6 +996,7 @@ impl Profiles {
dirs, dirs,
) )
.await?, .await?,
false,
) )
.await?; .await?;
Profile::sync_projects_task(profile_path_id, false); Profile::sync_projects_task(profile_path_id, false);

View File

@ -281,7 +281,6 @@ pub async fn infer_data_from_files(
) -> crate::Result<HashMap<ProjectPathId, Project>> { ) -> crate::Result<HashMap<ProjectPathId, Project>> {
let mut file_path_hashes = HashMap::new(); 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 { for path in paths {
if !path.exists() { if !path.exists() {
continue; continue;
@ -297,10 +296,19 @@ pub async fn infer_data_from_files(
.await .await
.map_err(|e| IOError::with_path(e, &path))?; .map_err(|e| IOError::with_path(e, &path))?;
let mut buffer = Vec::new(); let mut buffer = [0u8; 4096]; // Buffer to read chunks
file.read_to_end(&mut buffer).await.map_err(IOError::from)?; 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()); file_path_hashes.insert(hash, path.clone());
} }

View File

@ -196,6 +196,7 @@ impl ProfileInit {
None, None,
None, None,
None, None,
None,
) )
.await?; .await?;

View File

@ -1,151 +0,0 @@
<body>
<div class="splashscreen">
<div class="splashscreen__icon">
<svg
class="rotate outer"
width="100%"
height="100%"
viewBox="0 0 590 591"
xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2"
>
<g transform="matrix(1,0,0,1,652.392,-0.400578)">
<g transform="matrix(4.16667,0,0,4.16667,-735.553,0)">
<g transform="matrix(0.24,0,0,0.24,0,0)">
<path
d="M134.44,316.535C145.027,441.531 249.98,539.829 377.711,539.829C474.219,539.829 557.724,483.712 597.342,402.371L645.949,419.197C599.165,520.543 496.595,590.954 377.711,590.954C221.751,590.954 93.869,469.779 83.161,316.535L134.44,316.535ZM83.946,265.645C99.012,116.762 224.88,0.401 377.711,0.401C540.678,0.401 672.987,132.71 672.987,295.677C672.987,321.817 669.583,347.168 663.194,371.313L614.709,354.529C619.381,335.689 621.862,315.971 621.862,295.677C621.862,160.926 512.461,51.526 377.711,51.526C253.133,51.526 150.223,145.03 135.392,265.645L83.946,265.645Z"
style="fill: var(--color-brand)"
/>
</g>
</g>
</g></svg
><svg
class="rotate inner"
width="100%"
height="100%"
viewBox="0 0 590 591"
xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2"
>
<g transform="matrix(1,0,0,1,652.392,-0.400578)">
<g transform="matrix(4.16667,0,0,4.16667,-735.553,0)">
<g transform="matrix(0.24,0,0,0.24,0,0)">
<path
d="M376.933,153.568C298.44,153.644 234.735,217.396 234.735,295.909C234.735,374.47 298.516,438.251 377.077,438.251C381.06,438.251 385.005,438.087 388.914,437.764L403.128,487.517C394.611,488.667 385.912,489.261 377.077,489.261C270.363,489.261 183.725,402.623 183.725,295.909C183.725,189.195 270.363,102.557 377.077,102.557C379.723,102.557 382.357,102.611 384.983,102.717L376.933,153.568ZM435.127,111.438C513.515,136.114 570.428,209.418 570.428,295.909C570.428,375.976 521.655,444.742 452.22,474.093L438.063,424.541C486.142,401.687 519.418,352.653 519.418,295.909C519.418,234.923 480.981,182.843 427.029,162.593L435.127,111.438Z"
style="fill: var(--color-brand)"
/>
</g>
</g>
</g>
</svg>
<svg
width="100%"
height="100%"
viewBox="0 0 590 591"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve"
xmlns:serif="http://www.serif.com/"
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2"
>
<g transform="matrix(1,0,0,1,652.392,-0.400578)">
<g transform="matrix(4.16667,0,0,4.16667,-735.553,0)">
<g transform="matrix(0.24,0,0,0.24,0,0)">
<path
d="M300.366,311.86L283.216,266.381L336.966,211.169L404.9,196.531L424.57,220.74L393.254,252.46L365.941,261.052L346.425,281.11L355.987,307.719L375.387,328.306L402.745,321.031L422.216,299.648L464.729,286.185L477.395,314.677L433.529,368.46L360.02,391.735L327.058,355.031L138.217,468.344C129.245,456.811 118.829,440.485 112.15,424.792L300.366,311.86Z"
style="fill: var(--color-brand)"
/>
</g>
</g>
<g transform="matrix(4.16667,0,0,4.16667,-735.553,0)">
<g transform="matrix(0.24,0,0,0.24,0,0)">
<path
d="M655.189,194.555L505.695,234.873C513.927,256.795 516.638,269.674 518.915,283.863L668.152,243.609C665.764,227.675 661.5,211.444 655.189,194.555Z"
style="fill: var(--color-brand)"
/>
</g>
</g>
</g>
</svg>
</div>
<div class="splashscreen__text">Loading...</div>
</div>
</body>
<style>
body {
font-family: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
--color-brand: #1bd96a;
background-color: #16181c;
}
.splashscreen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
}
.splashscreen__text {
color: #fff;
font-size: 1.5rem;
font-weight: 700;
margin-top: 1rem;
}
.splashscreen__text::after {
content: '';
display: block;
width: 0;
height: 2px;
background-color: #fff;
animation: loading 2s ease-in-out infinite;
}
@keyframes loading {
from {
width: 0;
}
to {
width: 100%;
}
}
.splashscreen__icon {
height: 10rem;
width: 10rem;
}
.splashscreen__icon svg {
width: 10rem;
height: 10rem;
position: absolute;
}
.rotate {
animation: rotate 4s infinite linear;
}
.inner {
animation: rotate 6s infinite linear reverse;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>

View File

@ -17,6 +17,7 @@ pub async fn profile_create(
modloader: ModLoader, // the modloader to use modloader: ModLoader, // the modloader to use
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
no_watch: Option<bool>,
) -> Result<ProfilePathId> { ) -> Result<ProfilePathId> {
let res = profile::create::profile_create( let res = profile::create::profile_create(
name, name,
@ -27,6 +28,7 @@ pub async fn profile_create(
None, None,
None, None,
None, None,
no_watch,
) )
.await?; .await?;
Ok(res) Ok(res)

View File

@ -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) const accounts = ref(null)
command_listener((e) => { command_listener((e) => {

View File

@ -128,3 +128,10 @@ input {
background-color: var(--color-raised-bg); background-color: var(--color-raised-bg);
box-shadow: none !important; box-shadow: none !important;
} }
img {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}

View File

@ -200,6 +200,25 @@ const filteredResults = computed(() => {
return instanceMap.set('None', instances) 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 return instanceMap
}) })
</script> </script>
@ -265,7 +284,7 @@ const filteredResults = computed(() => {
<Instance <Instance
v-for="(instance, index) in instanceSection.value" v-for="(instance, index) in instanceSection.value"
ref="instanceComponents" ref="instanceComponents"
:key="instance.id" :key="instance.path"
:instance="instance" :instance="instance"
@contextmenu.prevent.stop="(event) => handleRightClick(event, instanceComponents[index])" @contextmenu.prevent.stop="(event) => handleRightClick(event, instanceComponents[index])"
/> />

View File

@ -165,5 +165,9 @@ td:first-child {
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
padding: 1rem; padding: 1rem;
:deep(.animated-dropdown .options) {
max-height: 13.375rem;
}
} }
</style> </style>

View File

@ -183,7 +183,7 @@ const createInstance = async () => {
await router.push(`/instance/${encodeURIComponent(id)}/`) await router.push(`/instance/${encodeURIComponent(id)}/`)
const instance = await get(id, true) const instance = await get(id, true)
await installVersionDependencies(instance, versions.value) await installVersionDependencies(instance, versions.value[0])
mixpanel_track('InstanceCreate', { mixpanel_track('InstanceCreate', {
profile_name: name.value, profile_name: name.value,
@ -204,7 +204,7 @@ const createInstance = async () => {
source: 'ProjectInstallModal', source: 'ProjectInstallModal',
}) })
installModal.value.hide() if (installModal.value) installModal.value.hide()
creatingInstance.value = false creatingInstance.value = false
} }

View File

@ -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") /// eg: import_instance("profile-name-to-go-to", "MultiMC", "C:/MultiMC", "Instance 1")
export async function import_instance(launcherType, basePath, instanceFolder) { export async function import_instance(launcherType, basePath, instanceFolder) {
// create a basic, empty instance (most properties will be filled in by the import process) // 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', { return await invoke('plugin:import|import_import_instance', {
profilePath, profilePath,

View File

@ -9,17 +9,23 @@ export async function get_game_versions() {
// Gets the fabric versions from daedalus // Gets the fabric versions from daedalus
// Returns Manifest // Returns Manifest
export async function get_fabric_versions() { 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 // Gets the forge versions from daedalus
// Returns Manifest // Returns Manifest
export async function get_forge_versions() { 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 // Gets the quilt versions from daedalus
// Returns Manifest // Returns Manifest
export async function get_quilt_versions() { 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
} }

View File

@ -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 - 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', { return await invoke('plugin:profile_create|profile_create', {
name, name,
gameVersion, gameVersion,
modloader, modloader,
loaderVersion, loaderVersion,
icon, icon,
noWatch,
}) })
} }

View File

@ -237,22 +237,22 @@ async function refreshSearch() {
let val = `${base}${url}` let val = `${base}${url}`
const rawResults = await useFetch(val, 'search results', offline.value) let rawResults = await useFetch(val, 'search results', offline.value)
results.value = rawResults
if (!rawResults) { if (!rawResults) {
results.value = { rawResults = {
hits: [], hits: [],
total_hits: 0, total_hits: 0,
limit: 1, limit: 1,
} }
} }
if (instanceContext.value) { 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( val.installed = await check_installed(instanceContext.value.path, val.project_id).then(
(x) => (val.installed = x) (x) => (val.installed = x)
) )
} }
} }
results.value = rawResults
} }
async function onSearchChange(newPageNumber) { async function onSearchChange(newPageNumber) {
@ -262,7 +262,6 @@ async function onSearchChange(newPageNumber) {
return return
} }
await refreshSearch() await refreshSearch()
const obj = getSearchUrl((currentPage.value - 1) * maxResults.value, true) const obj = getSearchUrl((currentPage.value - 1) * maxResults.value, true)
// Only replace in router if the query is different // Only replace in router if the query is different

View File

@ -311,7 +311,16 @@ async function refreshDir() {
customize your experience. Opting out will disable this data collection. customize your experience. Opting out will disable this data collection.
</span> </span>
</label> </label>
<Toggle id="opt-out-analytics" v-model="settings.opt_out_analytics" /> <Toggle
id="opt-out-analytics"
:model-value="settings.opt_out_analytics"
:checked="settings.opt_out_analytics"
@update:model-value="
(e) => {
settings.opt_out_analytics = e
}
"
/>
</div> </div>
</Card> </Card>
<Card> <Card>
@ -425,7 +434,7 @@ async function refreshDir() {
<label for="fullscreen"> <label for="fullscreen">
<span class="label__title">Fullscreen</span> <span class="label__title">Fullscreen</span>
<span class="label__description"> <span class="label__description">
Overwrites the option.txt file to start in full screen when launched. Overwrites the options.txt file to start in full screen when launched.
</span> </span>
</label> </label>
<Toggle <Toggle

View File

@ -185,12 +185,21 @@ interval.value = setInterval(async () => {
if (logs.value.length > 0) { if (logs.value.length > 0) {
logs.value[0] = await getLiveLog() logs.value[0] = await getLiveLog()
if (selectedLogIndex.value === 0 && !userScrolled.value) { // Allow resetting of userScrolled if the user scrolls to the bottom
await nextTick() if (selectedLogIndex.value === 0) {
isAutoScrolling.value = true if (
logContainer.value.scrollTop = logContainer.value.scrollTop + logContainer.value.offsetHeight >=
logContainer.value.scrollHeight - logContainer.value.offsetHeight logContainer.value.scrollHeight - 10
setTimeout(() => (isAutoScrolling.value = false), 50) )
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) }, 250)

View File

@ -204,7 +204,9 @@
<div class="adjacent-input"> <div class="adjacent-input">
<label for="fullscreen"> <label for="fullscreen">
<span class="label__title">Fullscreen</span> <span class="label__title">Fullscreen</span>
<span class="label__description"> Make the game start in full screen when launched. </span> <span class="label__description">
Make the game start in full screen when launched (using options.txt).
</span>
</label> </label>
<Checkbox id="fullscreen" v-model="fullscreenSetting" :disabled="!overrideWindowSettings" /> <Checkbox id="fullscreen" v-model="fullscreenSetting" :disabled="!overrideWindowSettings" />
</div> </div>

View File

@ -50,6 +50,7 @@
</Button> </Button>
<a <a
class="open btn icon-only" class="open btn icon-only"
target="_blank"
:href=" :href="
expandedGalleryItem.url expandedGalleryItem.url
? expandedGalleryItem.url ? expandedGalleryItem.url

View File

@ -83,12 +83,7 @@
<Card v-if="displayDependencies.length > 0"> <Card v-if="displayDependencies.length > 0">
<h2>Dependencies</h2> <h2>Dependencies</h2>
<div v-for="dependency in displayDependencies" :key="dependency.title"> <div v-for="dependency in displayDependencies" :key="dependency.title">
<router-link <router-link v-if="dependency.link" class="btn dependency" :to="dependency.link">
v-if="dependency.link"
class="btn dependency"
:to="dependency.link"
@click="testTest"
>
<Avatar size="sm" :src="dependency.icon" /> <Avatar size="sm" :src="dependency.icon" />
<div> <div>
<span class="title"> {{ dependency.title }} </span> <br /> <span class="title"> {{ dependency.title }} </span> <br />

View File

@ -85,6 +85,7 @@ async fn main() -> theseus::Result<()> {
None, None,
None, None,
None, None,
None,
) )
.await?; .await?;