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:
parent
a1a5b8ed9c
commit
d6ee1ff25a
@ -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)
|
||||||
|
|||||||
@ -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(())
|
||||||
|
|||||||
@ -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?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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(())
|
||||||
|
|||||||
@ -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(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)]
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 =
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -196,6 +196,7 @@ impl ProfileInit {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
151
theseus_gui/dist/splashscreen.html
vendored
151
theseus_gui/dist/splashscreen.html
vendored
@ -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>
|
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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])"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -85,6 +85,7 @@ async fn main() -> theseus::Result<()> {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user