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> {
|
||||
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 url = flow.prepare_login_url().await?;
|
||||
|
||||
let mut write = state.modrinth_auth_flow.write().await;
|
||||
*write = Some(flow);
|
||||
|
||||
Ok(url)
|
||||
|
||||
@ -3,13 +3,12 @@ use std::{collections::HashMap, path::PathBuf};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
event::LoadingBarId,
|
||||
pack::{
|
||||
self,
|
||||
import::{self, copy_dotminecraft},
|
||||
install_from::CreatePackDescription,
|
||||
},
|
||||
prelude::{ModLoader, ProfilePathId},
|
||||
prelude::{ModLoader, Profile, ProfilePathId},
|
||||
state::{LinkedData, ProfileInstallStage},
|
||||
util::io,
|
||||
State,
|
||||
@ -33,8 +32,6 @@ pub struct ATLauncher {
|
||||
pub modrinth_project: Option<ATLauncherModrinthProject>,
|
||||
pub modrinth_version: Option<ATLauncherModrinthVersion>,
|
||||
pub modrinth_manifest: Option<pack::install_from::PackFormat>,
|
||||
|
||||
pub mods: Vec<ATLauncherMod>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@ -57,13 +54,9 @@ pub struct ATLauncherModrinthProject {
|
||||
pub slug: String,
|
||||
pub project_type: String,
|
||||
pub team: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub body: String,
|
||||
pub client_side: Option<String>,
|
||||
pub server_side: Option<String>,
|
||||
pub categories: Vec<String>,
|
||||
pub icon_url: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@ -110,7 +103,16 @@ pub async fn is_valid_atlauncher(instance_folder: PathBuf) -> bool {
|
||||
.unwrap_or("".to_string());
|
||||
let instance: Result<ATInstance, serde_json::Error> =
|
||||
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]
|
||||
@ -169,7 +171,6 @@ pub async fn import_atlauncher(
|
||||
backup_name,
|
||||
description,
|
||||
atinstance,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@ -181,7 +182,6 @@ async fn import_atlauncher_unmanaged(
|
||||
backup_name: String,
|
||||
description: CreatePackDescription,
|
||||
atinstance: ATInstance,
|
||||
existing_loading_bar: Option<LoadingBarId>,
|
||||
) -> crate::Result<()> {
|
||||
let mod_loader = format!(
|
||||
"\"{}\"",
|
||||
@ -230,19 +230,28 @@ async fn import_atlauncher_unmanaged(
|
||||
|
||||
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
|
||||
let state = State::get().await?;
|
||||
copy_dotminecraft(
|
||||
let loading_bar = copy_dotminecraft(
|
||||
profile_path.clone(),
|
||||
minecraft_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
{
|
||||
crate::launcher::install_minecraft(&profile_val, existing_loading_bar)
|
||||
crate::launcher::install_minecraft(&profile_val, Some(loading_bar))
|
||||
.await?;
|
||||
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let mut file_watcher = state.file_watcher.write().await;
|
||||
Profile::watch_fs(
|
||||
&profile_val.get_profile_full_path().await?,
|
||||
&mut file_watcher,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
State::sync().await?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@ -2,6 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::prelude::Profile;
|
||||
use crate::state::CredentialsStore;
|
||||
use crate::{
|
||||
prelude::{ModLoader, ProfilePathId},
|
||||
@ -187,18 +188,29 @@ pub async fn import_curseforge(
|
||||
|
||||
// Copy in contained folders as overrides
|
||||
let state = State::get().await?;
|
||||
copy_dotminecraft(
|
||||
let loading_bar = copy_dotminecraft(
|
||||
profile_path.clone(),
|
||||
curseforge_instance_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
{
|
||||
crate::launcher::install_minecraft(&profile_val, None).await?;
|
||||
crate::launcher::install_minecraft(&profile_val, Some(loading_bar))
|
||||
.await?;
|
||||
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let mut file_watcher = state.file_watcher.write().await;
|
||||
Profile::watch_fs(
|
||||
&profile_val.get_profile_full_path().await?,
|
||||
&mut file_watcher,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
State::sync().await?;
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ use std::path::PathBuf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
prelude::{ModLoader, ProfilePathId},
|
||||
prelude::{ModLoader, Profile, ProfilePathId},
|
||||
state::ProfileInstallStage,
|
||||
util::io,
|
||||
State,
|
||||
@ -101,18 +101,28 @@ pub async fn import_gdlauncher(
|
||||
|
||||
// Copy in contained folders as overrides
|
||||
let state = State::get().await?;
|
||||
copy_dotminecraft(
|
||||
let loading_bar = copy_dotminecraft(
|
||||
profile_path.clone(),
|
||||
gdlauncher_instance_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
{
|
||||
crate::launcher::install_minecraft(&profile_val, None).await?;
|
||||
|
||||
crate::launcher::install_minecraft(&profile_val, Some(loading_bar))
|
||||
.await?;
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let mut file_watcher = state.file_watcher.write().await;
|
||||
Profile::watch_fs(
|
||||
&profile_val.get_profile_full_path().await?,
|
||||
&mut file_watcher,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
State::sync().await?;
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ use crate::{
|
||||
import::{self, copy_dotminecraft},
|
||||
install_from::{self, CreatePackDescription, PackDependency},
|
||||
},
|
||||
prelude::ProfilePathId,
|
||||
prelude::{Profile, ProfilePathId},
|
||||
util::io,
|
||||
State,
|
||||
};
|
||||
@ -119,6 +119,26 @@ pub struct MMCComponentRequirement {
|
||||
pub suggests: Option<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
|
||||
#[tracing::instrument]
|
||||
pub async fn is_valid_mmc(instance_folder: PathBuf) -> bool {
|
||||
@ -134,9 +154,19 @@ pub async fn is_valid_mmc(instance_folder: PathBuf) -> bool {
|
||||
&& serde_json::from_str::<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
|
||||
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 =
|
||||
serde_ini::from_str::<MMCInstanceEnum>(&instance_cfg)?;
|
||||
match instance_cfg_enum {
|
||||
@ -281,18 +311,28 @@ async fn import_mmc_unmanaged(
|
||||
|
||||
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
|
||||
let state = State::get().await?;
|
||||
copy_dotminecraft(
|
||||
let loading_bar = copy_dotminecraft(
|
||||
profile_path.clone(),
|
||||
minecraft_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
{
|
||||
crate::launcher::install_minecraft(&profile_val, None).await?;
|
||||
|
||||
crate::launcher::install_minecraft(&profile_val, Some(loading_bar))
|
||||
.await?;
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let mut file_watcher = state.file_watcher.write().await;
|
||||
Profile::watch_fs(
|
||||
&profile_val.get_profile_full_path().await?,
|
||||
&mut file_watcher,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
State::sync().await?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@ -7,6 +7,10 @@ use io::IOError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
event::{
|
||||
emit::{emit_loading, init_or_edit_loading},
|
||||
LoadingBarId,
|
||||
},
|
||||
prelude::ProfilePathId,
|
||||
state::Profiles,
|
||||
util::{
|
||||
@ -51,11 +55,20 @@ pub async fn get_importable_instances(
|
||||
) -> crate::Result<Vec<String>> {
|
||||
// Some launchers have a different folder structure for instances
|
||||
let instances_subfolder = match launcher_type {
|
||||
ImportLauncherType::GDLauncher
|
||||
| ImportLauncherType::MultiMC
|
||||
| ImportLauncherType::PrismLauncher
|
||||
| ImportLauncherType::ATLauncher => "instances",
|
||||
ImportLauncherType::Curseforge => "Instances",
|
||||
ImportLauncherType::GDLauncher | ImportLauncherType::ATLauncher => {
|
||||
"instances".to_string()
|
||||
}
|
||||
ImportLauncherType::Curseforge => "Instances".to_string(),
|
||||
ImportLauncherType::MultiMC => {
|
||||
mmc::get_instances_subpath(base_path.clone().join("multimc.cfg"))
|
||||
.await
|
||||
.unwrap_or_else(|| "instances".to_string())
|
||||
}
|
||||
ImportLauncherType::PrismLauncher => mmc::get_instances_subpath(
|
||||
base_path.clone().join("prismlauncher.cfg"),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|| "instances".to_string()),
|
||||
ImportLauncherType::Unknown => {
|
||||
return Err(crate::ErrorKind::InputError(
|
||||
"Launcher type Unknown".to_string(),
|
||||
@ -63,7 +76,8 @@ pub async fn get_importable_instances(
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let instances_folder = base_path.join(instances_subfolder);
|
||||
|
||||
let instances_folder = base_path.join(&instances_subfolder);
|
||||
let mut instances = Vec::new();
|
||||
let mut dir = io::read_dir(&instances_folder).await.map_err(| _ | {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
@ -238,55 +252,61 @@ pub async fn recache_icon(
|
||||
}
|
||||
|
||||
async fn copy_dotminecraft(
|
||||
profile_path: ProfilePathId,
|
||||
profile_path_id: ProfilePathId,
|
||||
dotminecraft: PathBuf,
|
||||
io_semaphore: &IoSemaphore,
|
||||
) -> crate::Result<()> {
|
||||
existing_loading_bar: Option<LoadingBarId>,
|
||||
) -> crate::Result<LoadingBarId> {
|
||||
// Get full path to profile
|
||||
let profile_path = profile_path.get_full_path().await?;
|
||||
let profile_path = profile_path_id.get_full_path().await?;
|
||||
|
||||
// std fs copy every file in dotminecraft to profile_path
|
||||
let mut dir = io::read_dir(&dotminecraft).await?;
|
||||
while let Some(entry) = dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &dotminecraft))?
|
||||
{
|
||||
let path = entry.path();
|
||||
copy_dir_to(
|
||||
&path,
|
||||
&profile_path.join(path.file_name().ok_or_else(|| {
|
||||
// Gets all subfiles recursively in src
|
||||
let subfiles = get_all_subfiles(&dotminecraft).await?;
|
||||
let total_subfiles = subfiles.len() as u64;
|
||||
|
||||
let loading_bar = init_or_edit_loading(
|
||||
existing_loading_bar,
|
||||
crate::LoadingBarType::CopyProfile {
|
||||
import_location: dotminecraft.clone(),
|
||||
profile_name: profile_path_id.to_string(),
|
||||
},
|
||||
total_subfiles as f64,
|
||||
"Copying files in profile",
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Copy each file
|
||||
for src_child in subfiles {
|
||||
let dst_child =
|
||||
src_child.strip_prefix(&dotminecraft).map_err(|_| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Invalid file: {}",
|
||||
&path.display()
|
||||
&src_child.display()
|
||||
))
|
||||
})?),
|
||||
io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
})?;
|
||||
let dst_child = profile_path.join(dst_child);
|
||||
|
||||
// sleep for cpu for 1 millisecond
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
|
||||
|
||||
fetch::copy(&src_child, &dst_child, io_semaphore).await?;
|
||||
|
||||
emit_loading(&loading_bar, 1.0, None).await?;
|
||||
}
|
||||
Ok(())
|
||||
Ok(loading_bar)
|
||||
}
|
||||
|
||||
/// Recursively fs::copy every file in src to dest
|
||||
/// Recursively get a list of all subfiles in src
|
||||
/// uses async recursion
|
||||
#[theseus_macros::debug_pin]
|
||||
#[async_recursion::async_recursion]
|
||||
#[tracing::instrument]
|
||||
async fn copy_dir_to(
|
||||
src: &Path,
|
||||
dst: &Path,
|
||||
io_semaphore: &IoSemaphore,
|
||||
) -> crate::Result<()> {
|
||||
async fn get_all_subfiles(src: &Path) -> crate::Result<Vec<PathBuf>> {
|
||||
if !src.is_dir() {
|
||||
fetch::copy(src, dst, io_semaphore).await?;
|
||||
return Ok(());
|
||||
return Ok(vec![src.to_path_buf()]);
|
||||
}
|
||||
|
||||
// Create the destination directory
|
||||
io::create_dir_all(&dst).await?;
|
||||
|
||||
// Iterate over the directory
|
||||
let mut files = Vec::new();
|
||||
let mut dir = io::read_dir(&src).await?;
|
||||
while let Some(child) = dir
|
||||
.next_entry()
|
||||
@ -294,21 +314,7 @@ async fn copy_dir_to(
|
||||
.map_err(|e| IOError::with_path(e, src))?
|
||||
{
|
||||
let src_child = child.path();
|
||||
let dst_child = dst.join(src_child.file_name().ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Invalid file: {}",
|
||||
&src_child.display()
|
||||
))
|
||||
})?);
|
||||
|
||||
if src_child.is_dir() {
|
||||
// Recurse into sub-directory
|
||||
copy_dir_to(&src_child, &dst_child, io_semaphore).await?;
|
||||
} else {
|
||||
// Copy file
|
||||
fetch::copy(&src_child, &dst_child, io_semaphore).await?;
|
||||
}
|
||||
files.append(&mut get_all_subfiles(&src_child).await?);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ pub enum EnvType {
|
||||
Server,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Hash, PartialEq, Eq, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum PackDependency {
|
||||
Forge,
|
||||
@ -101,6 +101,7 @@ pub struct CreatePackProfile {
|
||||
pub icon_url: Option<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 skip_install_profile: Option<bool>,
|
||||
pub no_watch: Option<bool>,
|
||||
}
|
||||
|
||||
// default
|
||||
@ -115,6 +116,7 @@ impl Default for CreatePackProfile {
|
||||
icon_url: None,
|
||||
linked_data: None,
|
||||
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)
|
||||
linked_data: Option<LinkedData>, // the linked project ID (mainly for modpacks)- used for updating
|
||||
skip_install_profile: Option<bool>,
|
||||
no_watch: Option<bool>,
|
||||
) -> crate::Result<ProfilePathId> {
|
||||
name = profile::sanitize_profile_name(&name);
|
||||
|
||||
@ -112,7 +113,9 @@ pub async fn profile_create(
|
||||
|
||||
{
|
||||
let mut profiles = state.profiles.write().await;
|
||||
profiles.insert(profile.clone()).await?;
|
||||
profiles
|
||||
.insert(profile.clone(), no_watch.unwrap_or_default())
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !skip_install_profile.unwrap_or(false) {
|
||||
@ -146,6 +149,7 @@ pub async fn profile_create_from_creator(
|
||||
profile.icon_url,
|
||||
profile.linked_data,
|
||||
profile.skip_install_profile,
|
||||
profile.no_watch,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -974,7 +974,7 @@ pub async fn create_mrpack_json(
|
||||
// But the values are sanitized to only include the version number
|
||||
let dependencies = dependencies
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, sanitize_loader_version_string(&v).to_string()))
|
||||
.map(|(k, v)| (k, sanitize_loader_version_string(&v, k).to_string()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
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 {
|
||||
// Split on '-'
|
||||
// If two or more, take the second
|
||||
// If one, take the first
|
||||
// If none, take the whole thing
|
||||
let mut split: std::str::Split<'_, char> = s.split('-');
|
||||
match split.next() {
|
||||
Some(first) => match split.next() {
|
||||
Some(second) => second,
|
||||
None => first,
|
||||
},
|
||||
None => s,
|
||||
fn sanitize_loader_version_string(s: &str, loader: PackDependency) -> &str {
|
||||
match loader {
|
||||
// Split on '-'
|
||||
// If two or more, take the second
|
||||
// If one, take the first
|
||||
// If none, take the whole thing
|
||||
PackDependency::Forge => {
|
||||
let mut split: std::str::Split<'_, char> = s.split('-');
|
||||
match split.next() {
|
||||
Some(first) => match split.next() {
|
||||
Some(second) => second,
|
||||
None => first,
|
||||
},
|
||||
None => s,
|
||||
}
|
||||
}
|
||||
// For quilt, etc we take the whole thing, as it functions like: 0.20.0-beta.11 (and should not be split here)
|
||||
PackDependency::QuiltLoader
|
||||
| PackDependency::FabricLoader
|
||||
| PackDependency::Minecraft => s,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -185,6 +185,10 @@ pub enum LoadingBarType {
|
||||
ConfigChange {
|
||||
new_path: PathBuf,
|
||||
},
|
||||
CopyProfile {
|
||||
import_location: PathBuf,
|
||||
profile_name: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
|
||||
@ -320,7 +320,7 @@ pub async fn download_libraries(
|
||||
let reader = std::io::Cursor::new(&data);
|
||||
if let Ok(mut archive) = zip::ZipArchive::new(reader) {
|
||||
match archive.extract(st.directories.version_natives_dir(version).await) {
|
||||
Ok(_) => tracing::info!("Fetched native {}", &library.name),
|
||||
Ok(_) => tracing::debug!("Fetched native {}", &library.name),
|
||||
Err(err) => tracing::error!("Failed extracting native {}. err: {}", &library.name, err)
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -461,28 +461,28 @@ pub async fn launch_minecraft(
|
||||
// Uses 'a:b' syntax which is not quite yaml
|
||||
use regex::Regex;
|
||||
|
||||
let options_path = instance_path.join("options.txt");
|
||||
let mut options_string = String::new();
|
||||
|
||||
if options_path.exists() {
|
||||
options_string = io::read_to_string(&options_path).await?;
|
||||
}
|
||||
|
||||
for (key, value) in mc_set_options {
|
||||
let re = Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?;
|
||||
// check if the regex exists in the file
|
||||
if !re.is_match(&options_string) {
|
||||
// The key was not found in the file, so append it
|
||||
options_string.push_str(&format!("\n{}:{}", key, value));
|
||||
} else {
|
||||
let replaced_string = re
|
||||
.replace_all(&options_string, &format!("{}:{}", key, value))
|
||||
.to_string();
|
||||
options_string = replaced_string;
|
||||
if !mc_set_options.is_empty() {
|
||||
let options_path = instance_path.join("options.txt");
|
||||
let mut options_string = String::new();
|
||||
if options_path.exists() {
|
||||
options_string = io::read_to_string(&options_path).await?;
|
||||
}
|
||||
for (key, value) in mc_set_options {
|
||||
let re = Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?;
|
||||
// check if the regex exists in the file
|
||||
if !re.is_match(&options_string) {
|
||||
// The key was not found in the file, so append it
|
||||
options_string.push_str(&format!("\n{}:{}", key, value));
|
||||
} else {
|
||||
let replaced_string = re
|
||||
.replace_all(&options_string, &format!("{}:{}", key, value))
|
||||
.to_string();
|
||||
options_string = replaced_string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
io::write(&options_path, options_string).await?;
|
||||
io::write(&options_path, options_string).await?;
|
||||
}
|
||||
|
||||
// Get Modrinth logs directories
|
||||
let datetime_string =
|
||||
|
||||
@ -353,19 +353,17 @@ pub async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
|
||||
})
|
||||
},
|
||||
)?;
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
let span = tracing::span!(tracing::Level::INFO, "init_watcher");
|
||||
tracing::info!(parent: &span, "Initting watcher");
|
||||
while let Some(res) = rx.next().await {
|
||||
let _span = span.enter();
|
||||
match res {
|
||||
Ok(mut events) => {
|
||||
let mut visited_paths = Vec::new();
|
||||
// sort events by e.path
|
||||
events.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
events.iter().for_each(|e| {
|
||||
tracing::debug!(
|
||||
"File watcher event: {:?}",
|
||||
serde_json::to_string(&e.path).unwrap()
|
||||
);
|
||||
let mut new_path = PathBuf::new();
|
||||
let mut components_iterator = e.path.components();
|
||||
let mut found = false;
|
||||
|
||||
@ -331,14 +331,13 @@ impl Profile {
|
||||
}
|
||||
|
||||
pub fn sync_projects_task(profile_path_id: ProfilePathId, force: bool) {
|
||||
let span = tracing::span!(
|
||||
tracing::Level::INFO,
|
||||
"sync_projects_task",
|
||||
?profile_path_id,
|
||||
?force
|
||||
);
|
||||
tokio::task::spawn(async move {
|
||||
let span =
|
||||
tracing::span!(tracing::Level::INFO, "sync_projects_task");
|
||||
tracing::debug!(
|
||||
parent: &span,
|
||||
"Syncing projects for profile {}",
|
||||
profile_path_id
|
||||
);
|
||||
let res = async {
|
||||
let _span = span.enter();
|
||||
let state = State::get().await?;
|
||||
@ -840,12 +839,17 @@ impl Profiles {
|
||||
drop(creds);
|
||||
|
||||
// Versions are pre-sorted in labrinth (by versions.sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published));)
|
||||
// so we can just take the first one
|
||||
// so we can just take the first one for which the loader matches
|
||||
let mut new_profiles = state.profiles.write().await;
|
||||
if let Some(profile) =
|
||||
new_profiles.0.get_mut(&profile_path)
|
||||
{
|
||||
if let Some(recent_version) = versions.get(0) {
|
||||
let loader = profile.metadata.loader;
|
||||
let recent_version = versions.iter().find(|x| {
|
||||
x.loaders
|
||||
.contains(&loader.as_api_str().to_string())
|
||||
});
|
||||
if let Some(recent_version) = recent_version {
|
||||
profile.modrinth_update_version =
|
||||
Some(recent_version.id.clone());
|
||||
} else {
|
||||
@ -879,7 +883,11 @@ impl Profiles {
|
||||
|
||||
#[tracing::instrument(skip(self, profile))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
|
||||
pub async fn insert(
|
||||
&mut self,
|
||||
profile: Profile,
|
||||
no_watch: bool,
|
||||
) -> crate::Result<&Self> {
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
&profile.profile_id(),
|
||||
@ -888,13 +896,15 @@ impl Profiles {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let state = State::get().await?;
|
||||
let mut file_watcher = state.file_watcher.write().await;
|
||||
Profile::watch_fs(
|
||||
&profile.get_profile_full_path().await?,
|
||||
&mut file_watcher,
|
||||
)
|
||||
.await?;
|
||||
if !no_watch {
|
||||
let state = State::get().await?;
|
||||
let mut file_watcher = state.file_watcher.write().await;
|
||||
Profile::watch_fs(
|
||||
&profile.get_profile_full_path().await?,
|
||||
&mut file_watcher,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let profile_name = profile.profile_id();
|
||||
profile_name.check_valid_utf()?;
|
||||
@ -986,6 +996,7 @@ impl Profiles {
|
||||
dirs,
|
||||
)
|
||||
.await?,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
Profile::sync_projects_task(profile_path_id, false);
|
||||
|
||||
@ -281,7 +281,6 @@ pub async fn infer_data_from_files(
|
||||
) -> crate::Result<HashMap<ProjectPathId, Project>> {
|
||||
let mut file_path_hashes = HashMap::new();
|
||||
|
||||
// TODO: Make this concurrent and use progressive hashing to avoid loading each JAR in memory
|
||||
for path in paths {
|
||||
if !path.exists() {
|
||||
continue;
|
||||
@ -297,10 +296,19 @@ pub async fn infer_data_from_files(
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
file.read_to_end(&mut buffer).await.map_err(IOError::from)?;
|
||||
let mut buffer = [0u8; 4096]; // Buffer to read chunks
|
||||
let mut hasher = sha2::Sha512::new(); // Hasher
|
||||
|
||||
let hash = format!("{:x}", sha2::Sha512::digest(&buffer));
|
||||
loop {
|
||||
let bytes_read =
|
||||
file.read(&mut buffer).await.map_err(IOError::from)?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.update(&buffer[..bytes_read]);
|
||||
}
|
||||
|
||||
let hash = format!("{:x}", hasher.finalize());
|
||||
file_path_hashes.insert(hash, path.clone());
|
||||
}
|
||||
|
||||
|
||||
@ -196,6 +196,7 @@ impl ProfileInit {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.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
|
||||
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
|
||||
no_watch: Option<bool>,
|
||||
) -> Result<ProfilePathId> {
|
||||
let res = profile::create::profile_create(
|
||||
name,
|
||||
@ -27,6 +28,7 @@ pub async fn profile_create(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
no_watch,
|
||||
)
|
||||
.await?;
|
||||
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)
|
||||
|
||||
command_listener((e) => {
|
||||
|
||||
@ -128,3 +128,10 @@ input {
|
||||
background-color: var(--color-raised-bg);
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
img {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
@ -200,6 +200,25 @@ const filteredResults = computed(() => {
|
||||
return instanceMap.set('None', instances)
|
||||
}
|
||||
|
||||
// For 'name', we intuitively expect the sorting to apply to the name of the group first, not just the name of the instance
|
||||
// ie: Category A should come before B, even if the first instance in B comes before the first instance in A
|
||||
if (sortBy.value === 'Name') {
|
||||
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
||||
// None should always be first
|
||||
if (a[0] === 'None' && b[0] !== 'None') {
|
||||
return -1
|
||||
}
|
||||
if (a[0] !== 'None' && b[0] === 'None') {
|
||||
return 1
|
||||
}
|
||||
return a[0].localeCompare(b[0])
|
||||
})
|
||||
instanceMap.clear()
|
||||
sortedEntries.forEach((entry) => {
|
||||
instanceMap.set(entry[0], entry[1])
|
||||
})
|
||||
}
|
||||
|
||||
return instanceMap
|
||||
})
|
||||
</script>
|
||||
@ -265,7 +284,7 @@ const filteredResults = computed(() => {
|
||||
<Instance
|
||||
v-for="(instance, index) in instanceSection.value"
|
||||
ref="instanceComponents"
|
||||
:key="instance.id"
|
||||
:key="instance.path"
|
||||
:instance="instance"
|
||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, instanceComponents[index])"
|
||||
/>
|
||||
|
||||
@ -165,5 +165,9 @@ td:first-child {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
|
||||
:deep(.animated-dropdown .options) {
|
||||
max-height: 13.375rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -183,7 +183,7 @@ const createInstance = async () => {
|
||||
await router.push(`/instance/${encodeURIComponent(id)}/`)
|
||||
|
||||
const instance = await get(id, true)
|
||||
await installVersionDependencies(instance, versions.value)
|
||||
await installVersionDependencies(instance, versions.value[0])
|
||||
|
||||
mixpanel_track('InstanceCreate', {
|
||||
profile_name: name.value,
|
||||
@ -204,7 +204,7 @@ const createInstance = async () => {
|
||||
source: 'ProjectInstallModal',
|
||||
})
|
||||
|
||||
installModal.value.hide()
|
||||
if (installModal.value) installModal.value.hide()
|
||||
creatingInstance.value = false
|
||||
}
|
||||
|
||||
|
||||
@ -34,7 +34,9 @@ export async function get_importable_instances(launcherType, basePath) {
|
||||
/// eg: import_instance("profile-name-to-go-to", "MultiMC", "C:/MultiMC", "Instance 1")
|
||||
export async function import_instance(launcherType, basePath, instanceFolder) {
|
||||
// create a basic, empty instance (most properties will be filled in by the import process)
|
||||
const profilePath = await create(instanceFolder, '1.19.4', 'vanilla', 'latest', null)
|
||||
// We do NOT watch the fs for changes to avoid duplicate events during installation
|
||||
// fs watching will be enabled once the instance is imported
|
||||
const profilePath = await create(instanceFolder, '1.19.4', 'vanilla', 'latest', null, true)
|
||||
|
||||
return await invoke('plugin:import|import_import_instance', {
|
||||
profilePath,
|
||||
|
||||
@ -9,17 +9,23 @@ export async function get_game_versions() {
|
||||
// Gets the fabric versions from daedalus
|
||||
// Returns Manifest
|
||||
export async function get_fabric_versions() {
|
||||
return await invoke('plugin:metadata|metadata_get_fabric_versions')
|
||||
const c = await invoke('plugin:metadata|metadata_get_fabric_versions')
|
||||
console.log('Getting fabric versions', c)
|
||||
return c
|
||||
}
|
||||
|
||||
// Gets the forge versions from daedalus
|
||||
// Returns Manifest
|
||||
export async function get_forge_versions() {
|
||||
return await invoke('plugin:metadata|metadata_get_forge_versions')
|
||||
const c = await invoke('plugin:metadata|metadata_get_forge_versions')
|
||||
console.log('Getting forge versions', c)
|
||||
return c
|
||||
}
|
||||
|
||||
// Gets the quilt versions from daedalus
|
||||
// Returns Manifest
|
||||
export async function get_quilt_versions() {
|
||||
return await invoke('plugin:metadata|metadata_get_quilt_versions')
|
||||
const c = await invoke('plugin:metadata|metadata_get_quilt_versions')
|
||||
console.log('Getting quilt versions', c)
|
||||
return c
|
||||
}
|
||||
|
||||
@ -16,13 +16,14 @@ import { invoke } from '@tauri-apps/api/tauri'
|
||||
- icon is a path to an image file, which will be copied into the profile directory
|
||||
*/
|
||||
|
||||
export async function create(name, gameVersion, modloader, loaderVersion, icon) {
|
||||
export async function create(name, gameVersion, modloader, loaderVersion, icon, noWatch) {
|
||||
return await invoke('plugin:profile_create|profile_create', {
|
||||
name,
|
||||
gameVersion,
|
||||
modloader,
|
||||
loaderVersion,
|
||||
icon,
|
||||
noWatch,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -237,22 +237,22 @@ async function refreshSearch() {
|
||||
|
||||
let val = `${base}${url}`
|
||||
|
||||
const rawResults = await useFetch(val, 'search results', offline.value)
|
||||
results.value = rawResults
|
||||
let rawResults = await useFetch(val, 'search results', offline.value)
|
||||
if (!rawResults) {
|
||||
results.value = {
|
||||
rawResults = {
|
||||
hits: [],
|
||||
total_hits: 0,
|
||||
limit: 1,
|
||||
}
|
||||
}
|
||||
if (instanceContext.value) {
|
||||
for (let val of results.value.hits) {
|
||||
for (val of rawResults.hits) {
|
||||
val.installed = await check_installed(instanceContext.value.path, val.project_id).then(
|
||||
(x) => (val.installed = x)
|
||||
)
|
||||
}
|
||||
}
|
||||
results.value = rawResults
|
||||
}
|
||||
|
||||
async function onSearchChange(newPageNumber) {
|
||||
@ -262,7 +262,6 @@ async function onSearchChange(newPageNumber) {
|
||||
return
|
||||
}
|
||||
await refreshSearch()
|
||||
|
||||
const obj = getSearchUrl((currentPage.value - 1) * maxResults.value, true)
|
||||
|
||||
// Only replace in router if the query is different
|
||||
|
||||
@ -311,7 +311,16 @@ async function refreshDir() {
|
||||
customize your experience. Opting out will disable this data collection.
|
||||
</span>
|
||||
</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>
|
||||
</Card>
|
||||
<Card>
|
||||
@ -425,7 +434,7 @@ async function refreshDir() {
|
||||
<label for="fullscreen">
|
||||
<span class="label__title">Fullscreen</span>
|
||||
<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>
|
||||
</label>
|
||||
<Toggle
|
||||
|
||||
@ -185,12 +185,21 @@ interval.value = setInterval(async () => {
|
||||
if (logs.value.length > 0) {
|
||||
logs.value[0] = await getLiveLog()
|
||||
|
||||
if (selectedLogIndex.value === 0 && !userScrolled.value) {
|
||||
await nextTick()
|
||||
isAutoScrolling.value = true
|
||||
logContainer.value.scrollTop =
|
||||
logContainer.value.scrollHeight - logContainer.value.offsetHeight
|
||||
setTimeout(() => (isAutoScrolling.value = false), 50)
|
||||
// Allow resetting of userScrolled if the user scrolls to the bottom
|
||||
if (selectedLogIndex.value === 0) {
|
||||
if (
|
||||
logContainer.value.scrollTop + logContainer.value.offsetHeight >=
|
||||
logContainer.value.scrollHeight - 10
|
||||
)
|
||||
userScrolled.value = false
|
||||
|
||||
if (!userScrolled.value) {
|
||||
await nextTick()
|
||||
isAutoScrolling.value = true
|
||||
logContainer.value.scrollTop =
|
||||
logContainer.value.scrollHeight - logContainer.value.offsetHeight
|
||||
setTimeout(() => (isAutoScrolling.value = false), 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 250)
|
||||
|
||||
@ -204,7 +204,9 @@
|
||||
<div class="adjacent-input">
|
||||
<label for="fullscreen">
|
||||
<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>
|
||||
<Checkbox id="fullscreen" v-model="fullscreenSetting" :disabled="!overrideWindowSettings" />
|
||||
</div>
|
||||
|
||||
@ -50,6 +50,7 @@
|
||||
</Button>
|
||||
<a
|
||||
class="open btn icon-only"
|
||||
target="_blank"
|
||||
:href="
|
||||
expandedGalleryItem.url
|
||||
? expandedGalleryItem.url
|
||||
|
||||
@ -83,12 +83,7 @@
|
||||
<Card v-if="displayDependencies.length > 0">
|
||||
<h2>Dependencies</h2>
|
||||
<div v-for="dependency in displayDependencies" :key="dependency.title">
|
||||
<router-link
|
||||
v-if="dependency.link"
|
||||
class="btn dependency"
|
||||
:to="dependency.link"
|
||||
@click="testTest"
|
||||
>
|
||||
<router-link v-if="dependency.link" class="btn dependency" :to="dependency.link">
|
||||
<Avatar size="sm" :src="dependency.icon" />
|
||||
<div>
|
||||
<span class="title"> {{ dependency.title }} </span> <br />
|
||||
|
||||
@ -85,6 +85,7 @@ async fn main() -> theseus::Result<()> {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user