You've already forked AstralRinth
forked from didirus/AstralRinth
Bug fixes round 3 (#298)
* fixed bugs * title case * bugs; ioerror * reset breadcrumbs * more fixes * more fixes * scrolling bug * more fixes * more fixes * clippy * canonicalize fix * fixed requested changes * removed debouncer update
This commit is contained in:
882
Cargo.lock
generated
882
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,7 @@ thiserror = "1.0"
|
||||
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = {version = "0.2", features = ["chrono"]}
|
||||
tracing-error = "0.1"
|
||||
tracing-error = "0.1.0"
|
||||
tracing-appender = "0.1"
|
||||
|
||||
paste = { version = "1.0"}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::event::{
|
||||
emit::{emit_command, emit_warning},
|
||||
CommandPayload,
|
||||
use crate::{
|
||||
event::{
|
||||
emit::{emit_command, emit_warning},
|
||||
CommandPayload,
|
||||
},
|
||||
util::io,
|
||||
};
|
||||
|
||||
/// Handles external functions (such as through URL deep linkage)
|
||||
@@ -46,7 +49,7 @@ pub async fn parse_command(
|
||||
} else {
|
||||
// We assume anything else is a filepath to an .mrpack file
|
||||
let path = PathBuf::from(command_string);
|
||||
let path = path.canonicalize()?;
|
||||
let path = io::canonicalize(path)?;
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "mrpack" {
|
||||
return Ok(CommandPayload::RunMRPack { path });
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::path::PathBuf;
|
||||
|
||||
use crate::event::emit::{emit_loading, init_loading};
|
||||
use crate::util::fetch::{fetch_advanced, fetch_json};
|
||||
use crate::util::io;
|
||||
use crate::util::jre::extract_java_majorminor_version;
|
||||
use crate::{
|
||||
state::JavaGlobals,
|
||||
@@ -114,7 +115,7 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
||||
let path = state.directories.java_versions_dir();
|
||||
|
||||
if path.exists() {
|
||||
tokio::fs::remove_dir_all(&path).await?;
|
||||
io::remove_dir_all(&path).await?;
|
||||
}
|
||||
|
||||
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(file))
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::State;
|
||||
use crate::{
|
||||
util::io::{self, IOError},
|
||||
State,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs::read_to_string;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Logs {
|
||||
@@ -36,8 +38,11 @@ pub async fn get_logs(
|
||||
let logs_folder = state.directories.profile_logs_dir(profile_uuid);
|
||||
let mut logs = Vec::new();
|
||||
if logs_folder.exists() {
|
||||
for entry in std::fs::read_dir(logs_folder)? {
|
||||
let entry = entry?;
|
||||
for entry in std::fs::read_dir(&logs_folder)
|
||||
.map_err(|e| IOError::with_path(e, &logs_folder))?
|
||||
{
|
||||
let entry =
|
||||
entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
if let Some(datetime_string) = path.file_name() {
|
||||
@@ -79,21 +84,21 @@ pub async fn get_output_by_datetime(
|
||||
) -> crate::Result<String> {
|
||||
let state = State::get().await?;
|
||||
let logs_folder = state.directories.profile_logs_dir(profile_uuid);
|
||||
Ok(
|
||||
read_to_string(logs_folder.join(datetime_string).join("stdout.log"))
|
||||
.await?,
|
||||
)
|
||||
let path = logs_folder.join(datetime_string).join("stdout.log");
|
||||
Ok(io::read_to_string(&path).await?)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_logs(profile_uuid: uuid::Uuid) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let logs_folder = state.directories.profile_logs_dir(profile_uuid);
|
||||
for entry in std::fs::read_dir(logs_folder)? {
|
||||
let entry = entry?;
|
||||
for entry in std::fs::read_dir(&logs_folder)
|
||||
.map_err(|e| IOError::with_path(e, &logs_folder))?
|
||||
{
|
||||
let entry = entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
std::fs::remove_dir_all(path)?;
|
||||
io::remove_dir_all(&path).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -106,6 +111,7 @@ pub async fn delete_logs_by_datetime(
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let logs_folder = state.directories.profile_logs_dir(profile_uuid);
|
||||
std::fs::remove_dir_all(logs_folder.join(datetime_string))?;
|
||||
let path = logs_folder.join(datetime_string);
|
||||
io::remove_dir_all(&path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -29,7 +29,10 @@ pub mod prelude {
|
||||
profile::{self, Profile},
|
||||
profile_create, settings,
|
||||
state::JavaGlobals,
|
||||
util::jre::JavaVersion,
|
||||
util::{
|
||||
io::{canonicalize, IOError},
|
||||
jre::JavaVersion,
|
||||
},
|
||||
State,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::state::{LinkedData, ModrinthProject, ModrinthVersion, SideType};
|
||||
use crate::util::fetch::{
|
||||
fetch, fetch_advanced, fetch_json, write_cached_icon,
|
||||
};
|
||||
use crate::util::io;
|
||||
use crate::State;
|
||||
|
||||
use reqwest::Method;
|
||||
@@ -13,7 +14,6 @@ use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -263,7 +263,7 @@ pub async fn generate_pack_from_file(
|
||||
path: PathBuf,
|
||||
profile: PathBuf,
|
||||
) -> crate::Result<CreatePackDescription> {
|
||||
let file = fs::read(&path).await?;
|
||||
let file = io::read(&path).await?;
|
||||
Ok(CreatePackDescription {
|
||||
file: bytes::Bytes::from(file),
|
||||
icon: None,
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::state::MinecraftChild;
|
||||
use crate::{state::MinecraftChild, util::io::IOError};
|
||||
pub use crate::{
|
||||
state::{
|
||||
Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize,
|
||||
@@ -121,7 +121,13 @@ pub async fn wait_for_by_uuid(uuid: &Uuid) -> crate::Result<()> {
|
||||
// Kill a running child process directly, and wait for it to be killed
|
||||
#[tracing::instrument(skip(running))]
|
||||
pub async fn kill(running: &mut MinecraftChild) -> crate::Result<()> {
|
||||
running.current_child.write().await.kill().await?;
|
||||
running
|
||||
.current_child
|
||||
.write()
|
||||
.await
|
||||
.kill()
|
||||
.await
|
||||
.map_err(IOError::from)?;
|
||||
wait_for(running).await
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::pack::install_from::{
|
||||
use crate::prelude::JavaVersion;
|
||||
use crate::state::ProjectMetadata;
|
||||
|
||||
use crate::util::io::{self, IOError};
|
||||
use crate::{
|
||||
auth::{self, refresh},
|
||||
event::{emit::emit_profile, ProfilePayloadType},
|
||||
@@ -27,11 +28,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::{
|
||||
fs::{self, File},
|
||||
process::Command,
|
||||
sync::RwLock,
|
||||
};
|
||||
use tokio::{fs::File, process::Command, sync::RwLock};
|
||||
|
||||
/// Remove a profile
|
||||
#[tracing::instrument]
|
||||
@@ -110,8 +107,8 @@ pub async fn edit_icon(
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
|
||||
if let Some(icon) = icon_path {
|
||||
let bytes = tokio::fs::read(icon).await?;
|
||||
let res = if let Some(icon) = icon_path {
|
||||
let bytes = io::read(icon).await?;
|
||||
|
||||
let mut profiles = state.profiles.write().await;
|
||||
|
||||
@@ -133,8 +130,6 @@ pub async fn edit_icon(
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
.await?;
|
||||
State::sync().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
None => Err(crate::ErrorKind::UnmanagedProfileError(
|
||||
@@ -151,7 +146,9 @@ pub async fn edit_icon(
|
||||
State::sync().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
State::sync().await?;
|
||||
res
|
||||
}
|
||||
|
||||
// Gets the optimal JRE key for the given profile, using Daedalus
|
||||
@@ -416,7 +413,7 @@ pub async fn add_project_from_path(
|
||||
project_type: Option<String>,
|
||||
) -> crate::Result<PathBuf> {
|
||||
if let Some(profile) = get(profile_path, None).await? {
|
||||
let file = fs::read(path).await?;
|
||||
let file = io::read(path).await?;
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
@@ -525,7 +522,9 @@ pub async fn export_mrpack(
|
||||
|
||||
let profile_base_path = &profile.path;
|
||||
|
||||
let mut file = File::create(export_path).await?;
|
||||
let mut file = File::create(&export_path)
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &export_path))?;
|
||||
let mut writer = ZipFileWriter::new(&mut file);
|
||||
|
||||
// Create mrpack json configuration file
|
||||
@@ -592,9 +591,13 @@ pub async fn export_mrpack(
|
||||
|
||||
// File is not in the config file, add it to the .mrpack zip
|
||||
if path.is_file() {
|
||||
let mut file = File::open(&path).await?;
|
||||
let mut file = File::open(&path)
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
let mut data = Vec::new();
|
||||
file.read_to_end(&mut data).await?;
|
||||
file.read_to_end(&mut data)
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
let builder = ZipEntryBuilder::new(
|
||||
format!("overrides/{relative_path}"),
|
||||
Compression::Deflate,
|
||||
@@ -639,13 +642,21 @@ pub async fn get_potential_override_folders(
|
||||
let mrpack_files = get_modrinth_pack_list(&mrpack);
|
||||
|
||||
let mut path_list: Vec<PathBuf> = Vec::new();
|
||||
let mut read_dir = fs::read_dir(&profile_path).await?;
|
||||
while let Some(entry) = read_dir.next_entry().await? {
|
||||
let mut read_dir = io::read_dir(&profile_path).await?;
|
||||
while let Some(entry) = read_dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &profile_path))?
|
||||
{
|
||||
let path: PathBuf = entry.path();
|
||||
if path.is_dir() {
|
||||
// Two layers of files/folders if its a folder
|
||||
let mut read_dir = fs::read_dir(&path).await?;
|
||||
while let Some(entry) = read_dir.next_entry().await? {
|
||||
let mut read_dir = io::read_dir(&path).await?;
|
||||
while let Some(entry) = read_dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &profile_path))?
|
||||
{
|
||||
let path: PathBuf = entry.path();
|
||||
let name = path.strip_prefix(&profile_path)?.to_path_buf();
|
||||
if !mrpack_files.contains(&name.to_string_lossy().to_string()) {
|
||||
@@ -712,9 +723,11 @@ pub async fn run_credentials(
|
||||
let result = Command::new(command)
|
||||
.args(&cmd.collect::<Vec<&str>>())
|
||||
.current_dir(path)
|
||||
.spawn()?
|
||||
.spawn()
|
||||
.map_err(|e| IOError::with_path(e, path))?
|
||||
.wait()
|
||||
.await?;
|
||||
.await
|
||||
.map_err(IOError::from)?;
|
||||
|
||||
if !result.success() {
|
||||
return Err(crate::ErrorKind::LauncherError(format!(
|
||||
@@ -925,8 +938,12 @@ pub async fn build_folder(
|
||||
path: &Path,
|
||||
path_list: &mut Vec<PathBuf>,
|
||||
) -> crate::Result<()> {
|
||||
let mut read_dir = fs::read_dir(path).await?;
|
||||
while let Some(entry) = read_dir.next_entry().await? {
|
||||
let mut read_dir = io::read_dir(path).await?;
|
||||
while let Some(entry) = read_dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, path))?
|
||||
{
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
build_folder(&path, path_list).await?;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Theseus profile management interface
|
||||
use crate::state::LinkedData;
|
||||
use crate::util::io::{self, canonicalize};
|
||||
use crate::{
|
||||
event::{emit::emit_profile, ProfilePayloadType},
|
||||
prelude::ModLoader,
|
||||
@@ -9,11 +10,9 @@ pub use crate::{
|
||||
State,
|
||||
};
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use dunce::canonicalize;
|
||||
use futures::prelude::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
use tracing::{info, trace};
|
||||
use uuid::Uuid;
|
||||
@@ -48,7 +47,7 @@ pub async fn profile_create(
|
||||
.into());
|
||||
}
|
||||
|
||||
if ReadDirStream::new(fs::read_dir(&path).await?)
|
||||
if ReadDirStream::new(io::read_dir(&path).await?)
|
||||
.next()
|
||||
.await
|
||||
.is_some()
|
||||
@@ -56,7 +55,7 @@ pub async fn profile_create(
|
||||
return Err(ProfileCreationError::NotEmptyFolder.into());
|
||||
}
|
||||
} else {
|
||||
fs::create_dir_all(&path).await?;
|
||||
io::create_dir_all(&path).await?;
|
||||
}
|
||||
|
||||
info!(
|
||||
@@ -80,7 +79,7 @@ pub async fn profile_create(
|
||||
Profile::new(uuid, name, game_version, path.clone()).await?;
|
||||
let result = async {
|
||||
if let Some(ref icon) = icon {
|
||||
let bytes = tokio::fs::read(icon).await?;
|
||||
let bytes = io::read(icon).await?;
|
||||
profile
|
||||
.set_icon(
|
||||
&state.directories.caches_dir(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Theseus error type
|
||||
use crate::profile_create;
|
||||
use crate::{profile_create, util};
|
||||
use tracing_error::InstrumentError;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
@@ -29,7 +29,7 @@ pub enum ErrorKind {
|
||||
AuthTaskError(#[from] crate::state::AuthTaskError),
|
||||
|
||||
#[error("I/O error: {0}")]
|
||||
IOError(#[from] std::io::Error),
|
||||
IOError(#[from] util::io::IOError),
|
||||
|
||||
#[error("Error launching Minecraft: {0}")]
|
||||
LauncherError(String),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use super::{auth::Credentials, parse_rule};
|
||||
use crate::{
|
||||
state::{MemorySettings, WindowSize},
|
||||
util::platform::classpath_separator,
|
||||
util::{io::IOError, platform::classpath_separator},
|
||||
};
|
||||
use daedalus::{
|
||||
get_path_from_artifact,
|
||||
@@ -393,7 +393,8 @@ pub async fn get_processor_main_class(
|
||||
path: String,
|
||||
) -> crate::Result<Option<String>> {
|
||||
let main_class = tokio::task::spawn_blocking(move || {
|
||||
let zipfile = std::fs::File::open(&path)?;
|
||||
let zipfile = std::fs::File::open(&path)
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
let mut archive = zip::ZipArchive::new(zipfile).map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Cannot read processor at {}",
|
||||
@@ -413,7 +414,7 @@ pub async fn get_processor_main_class(
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
for line in reader.lines() {
|
||||
let mut line = line?;
|
||||
let mut line = line.map_err(IOError::from)?;
|
||||
line.retain(|c| !c.is_whitespace());
|
||||
|
||||
if line.starts_with("Main-Class:") {
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
LoadingBarId,
|
||||
},
|
||||
state::State,
|
||||
util::{fetch::*, platform::OsExt},
|
||||
util::{fetch::*, io, platform::OsExt},
|
||||
};
|
||||
use daedalus::{
|
||||
self as d,
|
||||
@@ -17,7 +17,7 @@ use daedalus::{
|
||||
modded::LoaderVersion,
|
||||
};
|
||||
use futures::prelude::*;
|
||||
use tokio::{fs, sync::OnceCell};
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
#[tracing::instrument(skip(st, version))]
|
||||
pub async fn download_minecraft(
|
||||
@@ -71,7 +71,7 @@ pub async fn download_version_info(
|
||||
.join(format!("{version_id}.json"));
|
||||
|
||||
let res = if path.exists() && !force.unwrap_or(false) {
|
||||
fs::read(path)
|
||||
io::read(path)
|
||||
.err_into::<crate::Error>()
|
||||
.await
|
||||
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
|
||||
@@ -152,7 +152,7 @@ pub async fn download_assets_index(
|
||||
.join(format!("{}.json", &version.asset_index.id));
|
||||
|
||||
let res = if path.exists() {
|
||||
fs::read(path)
|
||||
io::read(path)
|
||||
.err_into::<crate::Error>()
|
||||
.await
|
||||
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
|
||||
@@ -245,8 +245,8 @@ pub async fn download_libraries(
|
||||
tracing::debug!("Loading libraries");
|
||||
|
||||
tokio::try_join! {
|
||||
fs::create_dir_all(st.directories.libraries_dir()),
|
||||
fs::create_dir_all(st.directories.version_natives_dir(version))
|
||||
io::create_dir_all(st.directories.libraries_dir()),
|
||||
io::create_dir_all(st.directories.version_natives_dir(version))
|
||||
}?;
|
||||
let num_files = libraries.len();
|
||||
loading_try_for_each_concurrent(
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
//! Logic for launching Minecraft
|
||||
use crate::event::emit::{emit_loading, init_or_edit_loading};
|
||||
use crate::event::{LoadingBarId, LoadingBarType};
|
||||
use crate::jre::{JAVA_17_KEY, JAVA_18PLUS_KEY, JAVA_8_KEY};
|
||||
use crate::jre::{self, JAVA_17_KEY, JAVA_18PLUS_KEY, JAVA_8_KEY};
|
||||
use crate::prelude::JavaVersion;
|
||||
use crate::state::ProfileInstallStage;
|
||||
use crate::util::io;
|
||||
use crate::EventState;
|
||||
use crate::{
|
||||
process,
|
||||
@@ -13,10 +14,8 @@ use crate::{
|
||||
use chrono::Utc;
|
||||
use daedalus as d;
|
||||
use daedalus::minecraft::VersionInfo;
|
||||
use dunce::canonicalize;
|
||||
use st::Profile;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::{process::Stdio, sync::Arc};
|
||||
use tokio::process::Command;
|
||||
use uuid::Uuid;
|
||||
@@ -125,7 +124,7 @@ pub async fn install_minecraft(
|
||||
State::sync().await?;
|
||||
|
||||
let state = State::get().await?;
|
||||
let instance_path = &canonicalize(&profile.path)?;
|
||||
let instance_path = &io::canonicalize(&profile.path)?;
|
||||
let metadata = state.metadata.read().await;
|
||||
|
||||
let version = metadata
|
||||
@@ -160,7 +159,7 @@ pub async fn install_minecraft(
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(
|
||||
"No available java installation".to_string(),
|
||||
"Missing correct java installation".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -282,7 +281,7 @@ pub async fn install_minecraft(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[tracing::instrument(skip_all)]
|
||||
#[theseus_macros::debug_pin]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn launch_minecraft(
|
||||
@@ -310,7 +309,7 @@ pub async fn launch_minecraft(
|
||||
|
||||
let state = State::get().await?;
|
||||
let metadata = state.metadata.read().await;
|
||||
let instance_path = &canonicalize(&profile.path)?;
|
||||
let instance_path = &io::canonicalize(&profile.path)?;
|
||||
|
||||
let version = metadata
|
||||
.minecraft
|
||||
@@ -343,10 +342,20 @@ pub async fn launch_minecraft(
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::LauncherError(
|
||||
"No available java installation".to_string(),
|
||||
"Missing correct java installation".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Test jre version
|
||||
let java_version = jre::check_jre(java_version.path.clone().into())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Java path invalid or non-functional: {}",
|
||||
java_version.path
|
||||
))
|
||||
})?;
|
||||
|
||||
let client_path = state
|
||||
.directories
|
||||
.version_dir(&version_jar)
|
||||
@@ -433,7 +442,7 @@ pub async fn launch_minecraft(
|
||||
.profile_logs_dir(profile.uuid)
|
||||
.join(&datetime_string)
|
||||
};
|
||||
fs::create_dir_all(&logs_dir)?;
|
||||
io::create_dir_all(&logs_dir).await?;
|
||||
|
||||
let stdout_log_path = logs_dir.join("stdout.log");
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use tracing::error;
|
||||
|
||||
use crate::event::emit::emit_process;
|
||||
use crate::event::ProcessPayloadType;
|
||||
use crate::util::io::IOError;
|
||||
use crate::EventState;
|
||||
use tokio::task::JoinHandle;
|
||||
use uuid::Uuid;
|
||||
@@ -39,7 +40,15 @@ impl Children {
|
||||
// The threads for stdout and stderr are spawned here
|
||||
// Unlike a Hashmap's 'insert', this directly returns the reference to the MinecraftChild rather than any previously stored MinecraftChild that may exist
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(
|
||||
self,
|
||||
uuid,
|
||||
log_path,
|
||||
mc_command,
|
||||
post_command,
|
||||
censor_strings
|
||||
))]
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn insert_process(
|
||||
&mut self,
|
||||
@@ -51,7 +60,7 @@ impl Children {
|
||||
censor_strings: HashMap<String, String>,
|
||||
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
|
||||
// Takes the first element of the commands vector and spawns it
|
||||
let mut child = mc_command.spawn()?;
|
||||
let mut child = mc_command.spawn().map_err(IOError::from)?;
|
||||
|
||||
// Create std watcher threads for stdout and stderr
|
||||
let shared_output =
|
||||
@@ -125,7 +134,12 @@ impl Children {
|
||||
// Wait on current Minecraft Child
|
||||
let mut mc_exit_status;
|
||||
loop {
|
||||
if let Some(t) = current_child.write().await.try_wait()? {
|
||||
if let Some(t) = current_child
|
||||
.write()
|
||||
.await
|
||||
.try_wait()
|
||||
.map_err(IOError::from)?
|
||||
{
|
||||
mc_exit_status = t;
|
||||
break;
|
||||
}
|
||||
@@ -156,7 +170,7 @@ impl Children {
|
||||
if let Some(mut m_command) = post_command {
|
||||
{
|
||||
let mut current_child = current_child.write().await;
|
||||
let new_child = m_command.spawn()?;
|
||||
let new_child = m_command.spawn().map_err(IOError::from)?;
|
||||
current_pid = new_child.id().ok_or_else(|| {
|
||||
crate::ErrorKind::LauncherError(
|
||||
"Process immediately failed, could not get PID"
|
||||
@@ -174,7 +188,12 @@ impl Children {
|
||||
.await?;
|
||||
|
||||
loop {
|
||||
if let Some(t) = current_child.write().await.try_wait()? {
|
||||
if let Some(t) = current_child
|
||||
.write()
|
||||
.await
|
||||
.try_wait()
|
||||
.map_err(IOError::from)?
|
||||
{
|
||||
mc_exit_status = t;
|
||||
break;
|
||||
}
|
||||
@@ -210,7 +229,12 @@ impl Children {
|
||||
) -> crate::Result<Option<std::process::ExitStatus>> {
|
||||
if let Some(child) = self.get(uuid) {
|
||||
let child = child.write().await;
|
||||
let status = child.current_child.write().await.try_wait()?;
|
||||
let status = child
|
||||
.current_child
|
||||
.write()
|
||||
.await
|
||||
.try_wait()
|
||||
.map_err(IOError::from)?;
|
||||
Ok(status)
|
||||
} else {
|
||||
Ok(None)
|
||||
@@ -224,7 +248,14 @@ impl Children {
|
||||
if let Some(child) = self.get(&key) {
|
||||
let child = child.clone();
|
||||
let child = child.write().await;
|
||||
if child.current_child.write().await.try_wait()?.is_none() {
|
||||
if child
|
||||
.current_child
|
||||
.write()
|
||||
.await
|
||||
.try_wait()
|
||||
.map_err(IOError::from)?
|
||||
.is_none()
|
||||
{
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
@@ -258,7 +289,14 @@ impl Children {
|
||||
if let Some(child) = self.get(&key) {
|
||||
let child = child.clone();
|
||||
let child = child.write().await;
|
||||
if child.current_child.write().await.try_wait()?.is_none() {
|
||||
if child
|
||||
.current_child
|
||||
.write()
|
||||
.await
|
||||
.try_wait()
|
||||
.map_err(IOError::from)?
|
||||
.is_none()
|
||||
{
|
||||
profiles.push(child.profile_path.clone());
|
||||
}
|
||||
}
|
||||
@@ -274,7 +312,14 @@ impl Children {
|
||||
if let Some(child) = self.get(&key) {
|
||||
let child = child.clone();
|
||||
let child = child.write().await;
|
||||
if child.current_child.write().await.try_wait()?.is_none() {
|
||||
if child
|
||||
.current_child
|
||||
.write()
|
||||
.await
|
||||
.try_wait()
|
||||
.map_err(IOError::from)?
|
||||
.is_none()
|
||||
{
|
||||
if let Some(prof) = crate::api::profile::get(
|
||||
&child.profile_path.clone(),
|
||||
None,
|
||||
@@ -312,7 +357,11 @@ impl SharedOutput {
|
||||
) -> crate::Result<Self> {
|
||||
Ok(SharedOutput {
|
||||
output: Arc::new(RwLock::new(String::new())),
|
||||
log_file: Arc::new(RwLock::new(File::create(log_file_path).await?)),
|
||||
log_file: Arc::new(RwLock::new(
|
||||
File::create(log_file_path)
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, log_file_path))?,
|
||||
)),
|
||||
censor_strings,
|
||||
})
|
||||
}
|
||||
@@ -330,7 +379,12 @@ impl SharedOutput {
|
||||
let mut buf_reader = BufReader::new(child_stdout);
|
||||
let mut line = String::new();
|
||||
|
||||
while buf_reader.read_line(&mut line).await? > 0 {
|
||||
while buf_reader
|
||||
.read_line(&mut line)
|
||||
.await
|
||||
.map_err(IOError::from)?
|
||||
> 0
|
||||
{
|
||||
let val_line = self.censor_log(line.clone());
|
||||
|
||||
{
|
||||
@@ -339,7 +393,10 @@ impl SharedOutput {
|
||||
}
|
||||
{
|
||||
let mut log_file = self.log_file.write().await;
|
||||
log_file.write_all(val_line.as_bytes()).await?;
|
||||
log_file
|
||||
.write_all(val_line.as_bytes())
|
||||
.await
|
||||
.map_err(IOError::from)?;
|
||||
}
|
||||
|
||||
line.clear();
|
||||
@@ -354,7 +411,12 @@ impl SharedOutput {
|
||||
let mut buf_reader = BufReader::new(child_stderr);
|
||||
let mut line = String::new();
|
||||
|
||||
while buf_reader.read_line(&mut line).await? > 0 {
|
||||
while buf_reader
|
||||
.read_line(&mut line)
|
||||
.await
|
||||
.map_err(IOError::from)?
|
||||
> 0
|
||||
{
|
||||
let val_line = self.censor_log(line.clone());
|
||||
|
||||
{
|
||||
@@ -363,7 +425,10 @@ impl SharedOutput {
|
||||
}
|
||||
{
|
||||
let mut log_file = self.log_file.write().await;
|
||||
log_file.write_all(val_line.as_bytes()).await?;
|
||||
log_file
|
||||
.write_all(val_line.as_bytes())
|
||||
.await
|
||||
.map_err(IOError::from)?;
|
||||
}
|
||||
|
||||
line.clear();
|
||||
|
||||
@@ -9,11 +9,11 @@ use crate::state::{ModrinthVersion, ProjectMetadata, ProjectType};
|
||||
use crate::util::fetch::{
|
||||
fetch, fetch_json, write, write_cached_icon, IoSemaphore,
|
||||
};
|
||||
use crate::util::io::{self, IOError};
|
||||
use crate::State;
|
||||
use chrono::{DateTime, Utc};
|
||||
use daedalus::get_hash;
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use dunce::canonicalize;
|
||||
use futures::prelude::*;
|
||||
use notify::{RecommendedWatcher, RecursiveMode};
|
||||
use notify_debouncer_mini::Debouncer;
|
||||
@@ -24,7 +24,6 @@ use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tokio::fs;
|
||||
use uuid::Uuid;
|
||||
|
||||
const PROFILE_JSON_PATH: &str = "profile.json";
|
||||
@@ -161,7 +160,7 @@ impl Profile {
|
||||
Ok(Self {
|
||||
uuid,
|
||||
install_stage: ProfileInstallStage::NotInstalled,
|
||||
path: canonicalize(path)?,
|
||||
path: io::canonicalize(path)?,
|
||||
metadata: ProfileMetadata {
|
||||
name,
|
||||
icon: None,
|
||||
@@ -274,8 +273,11 @@ impl Profile {
|
||||
let mut read_paths = |path: &str| {
|
||||
let new_path = self.path.join(path);
|
||||
if new_path.exists() {
|
||||
for path in std::fs::read_dir(self.path.join(path))? {
|
||||
let path = path?.path();
|
||||
let path = self.path.join(path);
|
||||
for path in std::fs::read_dir(&path)
|
||||
.map_err(|e| IOError::with_path(e, &path))?
|
||||
{
|
||||
let path = path.map_err(IOError::from)?.path();
|
||||
if path.is_file() {
|
||||
files.push(path);
|
||||
}
|
||||
@@ -305,7 +307,7 @@ impl Profile {
|
||||
) -> crate::Result<()> {
|
||||
let path = profile_path.join(path);
|
||||
|
||||
fs::create_dir_all(&path).await?;
|
||||
io::create_dir_all(&path).await?;
|
||||
|
||||
watcher
|
||||
.watcher()
|
||||
@@ -476,7 +478,7 @@ impl Profile {
|
||||
project.disabled = true;
|
||||
}
|
||||
|
||||
fs::rename(path, &new_path).await?;
|
||||
io::rename(&path, &new_path).await?;
|
||||
|
||||
let mut profiles = state.profiles.write().await;
|
||||
if let Some(profile) = profiles.0.get_mut(&self.path) {
|
||||
@@ -501,7 +503,7 @@ impl Profile {
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
if self.projects.contains_key(path) {
|
||||
fs::remove_file(path).await?;
|
||||
io::remove_file(path).await?;
|
||||
if !dont_remove_arr.unwrap_or(false) {
|
||||
let mut profiles = state.profiles.write().await;
|
||||
|
||||
@@ -530,9 +532,11 @@ impl Profiles {
|
||||
file_watcher: &mut Debouncer<RecommendedWatcher>,
|
||||
) -> crate::Result<Self> {
|
||||
let mut profiles = HashMap::new();
|
||||
fs::create_dir_all(dirs.profiles_dir()).await?;
|
||||
let mut entries = fs::read_dir(dirs.profiles_dir()).await?;
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
io::create_dir_all(&dirs.profiles_dir()).await?;
|
||||
let mut entries = io::read_dir(&dirs.profiles_dir()).await?;
|
||||
while let Some(entry) =
|
||||
entries.next_entry().await.map_err(IOError::from)?
|
||||
{
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
let prof = match Self::read_profile_from_dir(&path).await {
|
||||
@@ -545,7 +549,7 @@ impl Profiles {
|
||||
}
|
||||
};
|
||||
if let Some(profile) = prof {
|
||||
let path = canonicalize(path)?;
|
||||
let path = io::canonicalize(path)?;
|
||||
Profile::watch_fs(&path, file_watcher).await?;
|
||||
profiles.insert(path, profile);
|
||||
}
|
||||
@@ -629,7 +633,7 @@ impl Profiles {
|
||||
Profile::watch_fs(&profile.path, &mut file_watcher).await?;
|
||||
|
||||
self.0.insert(
|
||||
canonicalize(&profile.path)?
|
||||
io::canonicalize(&profile.path)?
|
||||
.to_str()
|
||||
.ok_or(
|
||||
crate::ErrorKind::UTFError(profile.path.clone()).as_error(),
|
||||
@@ -645,12 +649,13 @@ impl Profiles {
|
||||
&mut self,
|
||||
path: &Path,
|
||||
) -> crate::Result<Option<Profile>> {
|
||||
let path =
|
||||
PathBuf::from(&canonicalize(path)?.to_string_lossy().to_string());
|
||||
let path = PathBuf::from(
|
||||
&io::canonicalize(path)?.to_string_lossy().to_string(),
|
||||
);
|
||||
let profile = self.0.remove(&path);
|
||||
|
||||
if path.exists() {
|
||||
fs::remove_dir_all(path).await?;
|
||||
io::remove_dir_all(&path).await?;
|
||||
}
|
||||
|
||||
Ok(profile)
|
||||
@@ -666,7 +671,7 @@ impl Profiles {
|
||||
let json_path = Path::new(&path.to_string_lossy().to_string())
|
||||
.join(PROFILE_JSON_PATH);
|
||||
|
||||
fs::write(json_path, json).await?;
|
||||
io::write(&json_path, &json).await?;
|
||||
Ok::<_, crate::Error>(())
|
||||
})
|
||||
.await?;
|
||||
@@ -675,7 +680,7 @@ impl Profiles {
|
||||
}
|
||||
|
||||
async fn read_profile_from_dir(path: &Path) -> crate::Result<Profile> {
|
||||
let json = fs::read(path.join(PROFILE_JSON_PATH)).await?;
|
||||
let json = io::read(&path.join(PROFILE_JSON_PATH)).await?;
|
||||
let mut profile = serde_json::from_slice::<Profile>(&json)?;
|
||||
profile.path = PathBuf::from(path);
|
||||
Ok(profile)
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::state::Profile;
|
||||
use crate::util::fetch::{
|
||||
fetch_json, write_cached_icon, FetchSemaphore, IoSemaphore,
|
||||
};
|
||||
use crate::util::io::IOError;
|
||||
use async_zip::tokio::read::fs::ZipFileReader;
|
||||
use chrono::{DateTime, Utc};
|
||||
use reqwest::Method;
|
||||
@@ -252,7 +253,7 @@ async fn read_icon_from_file(
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(profile, io_semaphore, fetch_semaphore))]
|
||||
#[tracing::instrument(skip(paths, profile, io_semaphore, fetch_semaphore))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn infer_data_from_files(
|
||||
profile: Profile,
|
||||
@@ -265,10 +266,12 @@ pub async fn infer_data_from_files(
|
||||
|
||||
// TODO: Make this concurrent and use progressive hashing to avoid loading each JAR in memory
|
||||
for path in paths {
|
||||
let mut file = tokio::fs::File::open(path.clone()).await?;
|
||||
let mut file = tokio::fs::File::open(path.clone())
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
file.read_to_end(&mut buffer).await?;
|
||||
file.read_to_end(&mut buffer).await.map_err(IOError::from)?;
|
||||
|
||||
let hash = format!("{:x}", sha2::Sha512::digest(&buffer));
|
||||
file_path_hashes.insert(hash, path.clone());
|
||||
|
||||
@@ -9,10 +9,9 @@ use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time;
|
||||
use tokio::sync::{RwLock, Semaphore};
|
||||
use tokio::{
|
||||
fs::{self, File},
|
||||
io::AsyncWriteExt,
|
||||
};
|
||||
use tokio::{fs::File, io::AsyncWriteExt};
|
||||
|
||||
use super::io::{self, IOError};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IoSemaphore(pub RwLock<Semaphore>);
|
||||
@@ -193,7 +192,7 @@ where
|
||||
let io_semaphore = semaphore.0.read().await;
|
||||
let _permit = io_semaphore.acquire().await?;
|
||||
|
||||
let json = fs::read(path).await?;
|
||||
let json = io::read(path).await?;
|
||||
let json = serde_json::from_slice::<T>(&json)?;
|
||||
|
||||
Ok(json)
|
||||
@@ -209,11 +208,15 @@ pub async fn write<'a>(
|
||||
let _permit = io_semaphore.acquire().await?;
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).await?;
|
||||
io::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let mut file = File::create(path).await?;
|
||||
file.write_all(bytes).await?;
|
||||
let mut file = File::create(path)
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, path))?;
|
||||
file.write_all(bytes)
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, path))?;
|
||||
tracing::trace!("Done writing file {}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
@@ -235,7 +238,7 @@ pub async fn write_cached_icon(
|
||||
|
||||
write(&path, &bytes, semaphore).await?;
|
||||
|
||||
let path = dunce::canonicalize(path)?;
|
||||
let path = io::canonicalize(path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
|
||||
149
theseus/src/util/io.rs
Normal file
149
theseus/src/util/io.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
// IO error
|
||||
// A wrapper around the tokio IO functions that adds the path to the error message, instead of the uninformative std::io::Error.
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum IOError {
|
||||
#[error("{source}, path: {path}")]
|
||||
IOPathError {
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
path: String,
|
||||
},
|
||||
#[error(transparent)]
|
||||
IOError(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
impl IOError {
|
||||
pub fn from(source: std::io::Error) -> Self {
|
||||
Self::IOError(source)
|
||||
}
|
||||
pub fn with_path(
|
||||
source: std::io::Error,
|
||||
path: impl AsRef<std::path::Path>,
|
||||
) -> Self {
|
||||
let path = path.as_ref();
|
||||
|
||||
Self::IOPathError {
|
||||
source,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dunce canonicalize
|
||||
pub fn canonicalize(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
) -> Result<std::path::PathBuf, IOError> {
|
||||
let path = path.as_ref();
|
||||
dunce::canonicalize(path).map_err(|e| IOError::IOPathError {
|
||||
source: e,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// read_dir
|
||||
pub async fn read_dir(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
) -> Result<tokio::fs::ReadDir, IOError> {
|
||||
let path = path.as_ref();
|
||||
tokio::fs::read_dir(path)
|
||||
.await
|
||||
.map_err(|e| IOError::IOPathError {
|
||||
source: e,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// create_dir_all
|
||||
pub async fn create_dir_all(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
) -> Result<(), IOError> {
|
||||
let path = path.as_ref();
|
||||
tokio::fs::create_dir_all(path)
|
||||
.await
|
||||
.map_err(|e| IOError::IOPathError {
|
||||
source: e,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// remove_dir_all
|
||||
pub async fn remove_dir_all(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
) -> Result<(), IOError> {
|
||||
let path = path.as_ref();
|
||||
tokio::fs::remove_dir_all(path)
|
||||
.await
|
||||
.map_err(|e| IOError::IOPathError {
|
||||
source: e,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// read_to_string
|
||||
pub async fn read_to_string(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
) -> Result<String, IOError> {
|
||||
let path = path.as_ref();
|
||||
tokio::fs::read_to_string(path)
|
||||
.await
|
||||
.map_err(|e| IOError::IOPathError {
|
||||
source: e,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// read
|
||||
pub async fn read(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
) -> Result<Vec<u8>, IOError> {
|
||||
let path = path.as_ref();
|
||||
tokio::fs::read(path)
|
||||
.await
|
||||
.map_err(|e| IOError::IOPathError {
|
||||
source: e,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// write
|
||||
pub async fn write(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
data: impl AsRef<[u8]>,
|
||||
) -> Result<(), IOError> {
|
||||
let path = path.as_ref();
|
||||
tokio::fs::write(path, data)
|
||||
.await
|
||||
.map_err(|e| IOError::IOPathError {
|
||||
source: e,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// rename
|
||||
pub async fn rename(
|
||||
from: impl AsRef<std::path::Path>,
|
||||
to: impl AsRef<std::path::Path>,
|
||||
) -> Result<(), IOError> {
|
||||
let from = from.as_ref();
|
||||
let to = to.as_ref();
|
||||
tokio::fs::rename(from, to)
|
||||
.await
|
||||
.map_err(|e| IOError::IOPathError {
|
||||
source: e,
|
||||
path: from.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// remove file
|
||||
pub async fn remove_file(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
) -> Result<(), IOError> {
|
||||
let path = path.as_ref();
|
||||
tokio::fs::remove_file(path)
|
||||
.await
|
||||
.map_err(|e| IOError::IOPathError {
|
||||
source: e,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use dunce::canonicalize;
|
||||
use super::io;
|
||||
use futures::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
@@ -270,7 +270,7 @@ pub async fn check_java_at_filepaths(
|
||||
pub async fn check_java_at_filepath(path: &Path) -> Option<JavaVersion> {
|
||||
// Attempt to canonicalize the potential java filepath
|
||||
// If it fails, this path does not exist and None is returned (no Java here)
|
||||
let Ok(path) = canonicalize(path) else { return None };
|
||||
let Ok(path) = io::canonicalize(path) else { return None };
|
||||
|
||||
// Checks for existence of Java at this filepath
|
||||
// Adds JAVA_BIN to the end of the path if it is not already there
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Theseus utility functions
|
||||
pub mod fetch;
|
||||
pub mod io;
|
||||
pub mod jre;
|
||||
pub mod platform;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { RouterView, RouterLink, useRouter } from 'vue-router'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { RouterView, RouterLink, useRouter, useRoute } from 'vue-router'
|
||||
import {
|
||||
HomeIcon,
|
||||
SearchIcon,
|
||||
@@ -106,6 +106,8 @@ router.afterEach((to, from, failure) => {
|
||||
mixpanel.track('PageView', { path: to.path, fromPath: from.path, failed: failure })
|
||||
}
|
||||
})
|
||||
const route = useRoute()
|
||||
const isOnBrowse = computed(() => route.path.startsWith('/browse'))
|
||||
|
||||
const loading = useLoading()
|
||||
|
||||
@@ -179,6 +181,7 @@ const accounts = ref(null)
|
||||
'icon-only': themeStore.collapsedNavigation,
|
||||
'collapsed-button': themeStore.collapsedNavigation,
|
||||
'expanded-button': !themeStore.collapsedNavigation,
|
||||
'router-link-active': isOnBrowse,
|
||||
}"
|
||||
>
|
||||
<SearchIcon />
|
||||
@@ -340,7 +343,7 @@ const accounts = ref(null)
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--color-raised-bg);
|
||||
box-shadow: var(--shadow-inset-sm), var(--shadow-floating);
|
||||
box-shadow: inset 0px -3px 0px black;
|
||||
text-align: center;
|
||||
padding: var(--gap-md);
|
||||
height: 3.25rem;
|
||||
|
||||
@@ -122,9 +122,9 @@
|
||||
</div>
|
||||
<Chips
|
||||
v-model="javaSelectionType"
|
||||
:items="['automatically install', 'use existing installation']"
|
||||
:items="['Automatically install', 'Use existing installation']"
|
||||
/>
|
||||
<div v-if="javaSelectionType === 'use existing installation'" class="settings-group">
|
||||
<div v-if="javaSelectionType === 'Use existing installation'" class="settings-group">
|
||||
<h3>Java location</h3>
|
||||
<JavaSelector v-model="settings.java_globals.JAVA_17" compact />
|
||||
</div>
|
||||
@@ -230,7 +230,7 @@ async function pageTurn() {
|
||||
}
|
||||
}
|
||||
|
||||
const javaSelectionType = ref('automatically install')
|
||||
const javaSelectionType = ref('Automatically install')
|
||||
|
||||
async function autoInstallJava() {
|
||||
const path = await auto_install_java(17).catch(handleError)
|
||||
@@ -239,6 +239,7 @@ async function autoInstallJava() {
|
||||
// weird vue bug, ignore
|
||||
settings.value.java_globals.JAVA_17 = version
|
||||
settings.value.java_globals.JAVA_17 = version
|
||||
set(settings.value)
|
||||
mixpanel.track('OnboardingAutoInstallJava')
|
||||
}
|
||||
|
||||
@@ -258,6 +259,10 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.chips .btn) {
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="breadcrumbs">
|
||||
{{ breadcrumbData.resetToNames(breadcrumbs) }}
|
||||
<div v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name" class="breadcrumbs__item">
|
||||
<router-link
|
||||
v-if="breadcrumb.link"
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
<div v-else class="status">
|
||||
<span class="circle stopped" />
|
||||
<span class="running-text"> No running instances </span>
|
||||
<span class="running-text"> No instances running </span>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="download">
|
||||
|
||||
@@ -14,6 +14,7 @@ defineProps({
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-loading {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
@@ -272,6 +272,11 @@ async function onSearchChangeToTop(newPageNumber) {
|
||||
searchWrapper.value.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
async function clearSearch() {
|
||||
query.value = ''
|
||||
await onSearchChange(1)
|
||||
}
|
||||
|
||||
function getSearchUrl(offset, useObj) {
|
||||
const queryItems = []
|
||||
const obj = {}
|
||||
@@ -355,6 +360,33 @@ const sortedCategories = computed(() => {
|
||||
return values
|
||||
})
|
||||
|
||||
// Sorts alphabetically, but correctly identifies 8x, 128x, 256x, etc
|
||||
// identifier[0], then if it ties, identifier[1], etc
|
||||
async function sortByNameOrNumber(sortable, identifiers) {
|
||||
console.log(sortable)
|
||||
sortable.sort((a, b) => {
|
||||
for (let identifier of identifiers) {
|
||||
let aNum = parseFloat(a[identifier])
|
||||
let bNum = parseFloat(b[identifier])
|
||||
if (isNaN(aNum) && isNaN(bNum)) {
|
||||
// Both are strings, sort alphabetically
|
||||
let stringComp = a[identifier].localeCompare(b[identifier])
|
||||
if (stringComp != 0) return stringComp
|
||||
} else if (!isNaN(aNum) && !isNaN(bNum)) {
|
||||
// Both are numbers, sort numerically
|
||||
let numComp = aNum - bNum
|
||||
if (numComp != 0) return numComp
|
||||
} else {
|
||||
// One is a number and one is a string, numbers go first
|
||||
let numStringComp = isNaN(aNum) ? 1 : -1
|
||||
if (numStringComp != 0) return numStringComp
|
||||
}
|
||||
}
|
||||
return 0
|
||||
})
|
||||
return sortable
|
||||
}
|
||||
|
||||
async function clearFilters() {
|
||||
for (const facet of [...facets.value]) {
|
||||
await toggleFacet(facet, true)
|
||||
@@ -426,7 +458,10 @@ watch(
|
||||
)
|
||||
|
||||
const [categories, loaders, availableGameVersions] = await Promise.all([
|
||||
get_categories().catch(handleError).then(ref),
|
||||
get_categories()
|
||||
.catch(handleError)
|
||||
.then((s) => sortByNameOrNumber(s, ['header', 'name']))
|
||||
.then(ref),
|
||||
get_loaders().catch(handleError).then(ref),
|
||||
get_game_versions().catch(handleError).then(ref),
|
||||
refreshSearch(),
|
||||
@@ -473,7 +508,7 @@ const showLoaders = computed(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-container">
|
||||
<div ref="searchWrapper" class="search-container">
|
||||
<aside class="filter-panel">
|
||||
<Card v-if="instanceContext" class="small-instance">
|
||||
<router-link :to="`/instance/${encodeURIComponent(instanceContext.path)}`" class="instance">
|
||||
@@ -525,7 +560,7 @@ const showLoaders = computed(
|
||||
"
|
||||
@click="clearFilters"
|
||||
>
|
||||
<ClearIcon /> Clear Filters
|
||||
<ClearIcon /> Clear filters
|
||||
</Button>
|
||||
<div v-if="showLoaders" class="loaders">
|
||||
<h2>Loaders</h2>
|
||||
@@ -618,7 +653,7 @@ const showLoaders = computed(
|
||||
</div>
|
||||
</Card>
|
||||
</aside>
|
||||
<div ref="searchWrapper" class="search">
|
||||
<div class="search">
|
||||
<Promotion class="promotion" />
|
||||
<Card class="project-type-container">
|
||||
<NavRow :links="selectableProjectTypes" />
|
||||
@@ -633,7 +668,7 @@ const showLoaders = computed(
|
||||
:placeholder="`Search ${projectType}s...`"
|
||||
@input="onSearchChange(1)"
|
||||
/>
|
||||
<Button @click="() => (searchStore.searchInput = '')">
|
||||
<Button @click="() => clearSearch()">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -698,6 +733,7 @@ const showLoaders = computed(
|
||||
class="pagination-after"
|
||||
@switch-page="onSearchChangeToTop"
|
||||
/>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<InstallConfirmModal ref="confirmModal" />
|
||||
@@ -818,6 +854,9 @@ const showLoaders = computed(
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
height: 100%; /* takes up only the necessary height */
|
||||
overflow-y: auto;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
.filter-panel {
|
||||
position: fixed;
|
||||
@@ -846,7 +885,6 @@ const showLoaders = computed(
|
||||
}
|
||||
|
||||
.search {
|
||||
scroll-behavior: smooth;
|
||||
margin: 0 1rem 0.5rem 20.5rem;
|
||||
width: calc(100% - 20.5rem);
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</template>
|
||||
<template #filter_update>
|
||||
<UpdatedIcon />
|
||||
Select Updatable
|
||||
Select updatable
|
||||
</template>
|
||||
</DropdownButton>
|
||||
<Button v-if="selected.length > 0" class="no-wrap" @click="deleteWarning.show()">
|
||||
@@ -155,21 +155,16 @@
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
<AnimatedLogo v-if="mod.updating" class="btn icon-only updating-indicator"></AnimatedLogo>
|
||||
<Button
|
||||
v-else
|
||||
v-tooltip="'Update project'"
|
||||
:disabled="!mod.outdated"
|
||||
icon-only
|
||||
@click="updateProject(mod)"
|
||||
>
|
||||
<UpdatedIcon v-if="mod.outdated" />
|
||||
<CheckIcon v-else />
|
||||
<Button v-else :disabled="!mod.outdated" icon-only @click="updateProject(mod)">
|
||||
<UpdatedIcon v-if="mod.outdated" v-tooltip="'Update project'" />
|
||||
<CheckIcon v-else v-tooltip="'Updated'" />
|
||||
</Button>
|
||||
<input
|
||||
id="switch-1"
|
||||
autocomplete="off"
|
||||
type="checkbox"
|
||||
class="switch stylized-toggle"
|
||||
:disabled="mod.toggleInProgress"
|
||||
:checked="!mod.disabled"
|
||||
@change="toggleDisableMod(mod)"
|
||||
/>
|
||||
@@ -454,7 +449,6 @@ async function updateProject(mod) {
|
||||
async function toggleDisableMod(mod) {
|
||||
mod.path = await toggle_disable_project(props.instance.path, mod.path).catch(handleError)
|
||||
mod.disabled = !mod.disabled
|
||||
|
||||
mixpanel.track('InstanceProjectDisable', {
|
||||
loader: props.instance.metadata.loader,
|
||||
game_version: props.instance.metadata.game_version,
|
||||
|
||||
@@ -610,6 +610,10 @@ async function saveGvLoaderEdits() {
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
|
||||
:deep(.animated-dropdown .options) {
|
||||
max-height: 13.375rem;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 1rem;
|
||||
font-weight: bolder;
|
||||
|
||||
@@ -207,6 +207,7 @@
|
||||
:dependencies="dependencies"
|
||||
:install="install"
|
||||
:installed="installed"
|
||||
:installing="installing"
|
||||
:installed-version="installedVersion"
|
||||
/>
|
||||
</div>
|
||||
@@ -310,7 +311,7 @@ async function fetchProjectData() {
|
||||
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}/members`, 'project'),
|
||||
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}/dependencies`, 'project'),
|
||||
get_categories().catch(handleError),
|
||||
route.query.i ? getInstance(route.query.i, true).catch(handleError) : Promise.resolve(),
|
||||
route.query.i ? getInstance(route.query.i, false).catch(handleError) : Promise.resolve(),
|
||||
])
|
||||
|
||||
installed.value =
|
||||
@@ -344,6 +345,7 @@ const markInstalled = () => {
|
||||
async function install(version) {
|
||||
installing.value = true
|
||||
let queuedVersionData
|
||||
instance.value = await getInstance(instance.value.path, false).catch(handleError)
|
||||
|
||||
if (installed.value) {
|
||||
await remove_project(
|
||||
|
||||
@@ -14,10 +14,21 @@
|
||||
<h2>{{ version.name }}</h2>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<Button color="primary" :action="() => install(version.id)" :disabled="installed">
|
||||
<Button
|
||||
color="primary"
|
||||
:action="() => install(version.id)"
|
||||
:disabled="installing || (installed && installedVersion === version.id)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<SwapIcon v-else-if="installedVersion !== version.id" />
|
||||
<CheckIcon v-else />
|
||||
{{ installed ? 'Installed' : 'Install' }}
|
||||
{{
|
||||
installing
|
||||
? 'Installing...'
|
||||
: installed && installedVersion === version.id
|
||||
? 'Installed'
|
||||
: 'Install'
|
||||
}}
|
||||
</Button>
|
||||
<Button>
|
||||
<ReportIcon />
|
||||
@@ -29,7 +40,7 @@
|
||||
class="btn"
|
||||
>
|
||||
<ExternalIcon />
|
||||
Modrinth Website
|
||||
Modrinth website
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -195,6 +206,7 @@ import { releaseColor } from '@/helpers/utils'
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { SwapIcon } from '@/assets/icons'
|
||||
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
@@ -225,6 +237,14 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
installing: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
installedVersion: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const version = ref(props.versions.find((version) => version.id === route.params.version))
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
<Button
|
||||
:color="installed && version.id === installedVersion ? '' : 'primary'"
|
||||
icon-only
|
||||
:disabled="installed && version.id === installedVersion"
|
||||
:disabled="installing || (installed && version.id === installedVersion)"
|
||||
@click.stop="() => install(version.id)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed" />
|
||||
@@ -191,6 +191,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
installing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
instance: {
|
||||
type: Object,
|
||||
default: null,
|
||||
|
||||
@@ -8,11 +8,24 @@ export const useBreadcrumbs = defineStore('breadcrumbsStore', {
|
||||
}),
|
||||
actions: {
|
||||
getName(route) {
|
||||
return this.names.get(route) ?? route
|
||||
return this.names.get(route) ?? ''
|
||||
},
|
||||
setName(route, title) {
|
||||
this.names.set(route, title)
|
||||
},
|
||||
// resets breadcrumbs to only included ones as to not have stale breadcrumbs
|
||||
resetToNames(breadcrumbs) {
|
||||
// names is an array of every breadcrumb.name that starts with a ?
|
||||
const names = breadcrumbs
|
||||
.filter((breadcrumb) => breadcrumb.name.charAt(0) === '?')
|
||||
.map((breadcrumb) => breadcrumb.name.slice(1))
|
||||
// remove all names that are not in the names array
|
||||
for (const [route] of this.names) {
|
||||
if (!names.includes(route)) {
|
||||
this.names.delete(route)
|
||||
}
|
||||
}
|
||||
},
|
||||
setContext(context) {
|
||||
this.context = context
|
||||
},
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use dunce::canonicalize;
|
||||
use theseus::jre::autodetect_java_globals;
|
||||
use theseus::prelude::*;
|
||||
|
||||
@@ -19,7 +18,8 @@ pub async fn authenticate_run() -> theseus::Result<Credentials> {
|
||||
let url = auth::authenticate_begin_flow().await?;
|
||||
|
||||
println!("URL {}", url.as_str());
|
||||
webbrowser::open(url.as_str())?;
|
||||
webbrowser::open(url.as_str())
|
||||
.map_err(|e| IOError::with_path(e, url.as_str()))?;
|
||||
|
||||
let credentials = auth::authenticate_await_complete_flow().await?;
|
||||
State::sync().await?;
|
||||
|
||||
Reference in New Issue
Block a user