fixes (#449)
* fixes * prettier * more bugs * changes * more fixes * prettier, fmt, clippy * fix regressed error * println, console.log * fix imports --------- Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me>
This commit is contained in:
parent
6a76811bed
commit
a35dd67b77
@ -229,7 +229,13 @@ async fn import_atlauncher_unmanaged(
|
||||
.await?;
|
||||
|
||||
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
|
||||
copy_dotminecraft(profile_path.clone(), minecraft_folder).await?;
|
||||
let state = State::get().await?;
|
||||
copy_dotminecraft(
|
||||
profile_path.clone(),
|
||||
minecraft_folder,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
|
||||
@ -5,11 +5,14 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::{
|
||||
prelude::{ModLoader, ProfilePathId},
|
||||
state::ProfileInstallStage,
|
||||
util::io,
|
||||
util::{
|
||||
fetch::{fetch, write_cached_icon},
|
||||
io,
|
||||
},
|
||||
State,
|
||||
};
|
||||
|
||||
use super::copy_dotminecraft;
|
||||
use super::{copy_dotminecraft, recache_icon};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -31,12 +34,20 @@ pub struct FlameModLoader {
|
||||
pub primary: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MinecraftInstance {
|
||||
pub name: Option<String>,
|
||||
pub profile_image_path: Option<PathBuf>,
|
||||
pub installed_modpack: Option<InstalledModpack>,
|
||||
pub game_version: String, // Minecraft game version. Non-prioritized, use this if Vanilla
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
pub struct InstalledModpack {
|
||||
pub thumbnail_url: Option<String>,
|
||||
}
|
||||
|
||||
// Check if folder has a minecraftinstance.json that parses
|
||||
pub async fn is_valid_curseforge(instance_folder: PathBuf) -> bool {
|
||||
@ -53,9 +64,6 @@ pub async fn import_curseforge(
|
||||
curseforge_instance_folder: PathBuf, // instance's folder
|
||||
profile_path: ProfilePathId, // path to profile
|
||||
) -> crate::Result<()> {
|
||||
// TODO: recache curseforge instance icon
|
||||
let icon: Option<PathBuf> = None;
|
||||
|
||||
// Load minecraftinstance.json
|
||||
let minecraft_instance: String = io::read_to_string(
|
||||
&curseforge_instance_folder.join("minecraftinstance.json"),
|
||||
@ -72,6 +80,32 @@ pub async fn import_curseforge(
|
||||
.unwrap_or("Unknown".to_string())
|
||||
);
|
||||
|
||||
let state = State::get().await?;
|
||||
// Recache Curseforge Icon if it exists
|
||||
let mut icon = None;
|
||||
|
||||
if let Some(icon_path) = minecraft_instance.profile_image_path.clone() {
|
||||
icon = recache_icon(icon_path).await?;
|
||||
} else if let Some(InstalledModpack {
|
||||
thumbnail_url: Some(thumbnail_url),
|
||||
}) = minecraft_instance.installed_modpack.clone()
|
||||
{
|
||||
let icon_bytes =
|
||||
fetch(&thumbnail_url, None, &state.fetch_semaphore).await?;
|
||||
let filename = thumbnail_url.rsplit('/').last();
|
||||
if let Some(filename) = filename {
|
||||
icon = Some(
|
||||
write_cached_icon(
|
||||
filename,
|
||||
&state.directories.caches_dir(),
|
||||
icon_bytes,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Curseforge vanilla profile may not have a manifest.json, so we allow it to not exist
|
||||
if curseforge_instance_folder.join("manifest.json").exists() {
|
||||
// Load manifest.json
|
||||
@ -146,7 +180,13 @@ pub async fn import_curseforge(
|
||||
}
|
||||
|
||||
// Copy in contained folders as overrides
|
||||
copy_dotminecraft(profile_path.clone(), curseforge_instance_folder).await?;
|
||||
let state = State::get().await?;
|
||||
copy_dotminecraft(
|
||||
profile_path.clone(),
|
||||
curseforge_instance_folder,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
|
||||
@ -100,7 +100,13 @@ pub async fn import_gdlauncher(
|
||||
.await?;
|
||||
|
||||
// Copy in contained folders as overrides
|
||||
copy_dotminecraft(profile_path.clone(), gdlauncher_instance_folder).await?;
|
||||
let state = State::get().await?;
|
||||
copy_dotminecraft(
|
||||
profile_path.clone(),
|
||||
gdlauncher_instance_folder,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
|
||||
@ -280,7 +280,13 @@ async fn import_mmc_unmanaged(
|
||||
.await?;
|
||||
|
||||
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
|
||||
copy_dotminecraft(profile_path.clone(), minecraft_folder).await?;
|
||||
let state = State::get().await?;
|
||||
copy_dotminecraft(
|
||||
profile_path.clone(),
|
||||
minecraft_folder,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{
|
||||
fmt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use io::IOError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -6,7 +9,10 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::{
|
||||
prelude::ProfilePathId,
|
||||
state::Profiles,
|
||||
util::{fetch, io},
|
||||
util::{
|
||||
fetch::{self, IoSemaphore},
|
||||
io,
|
||||
},
|
||||
};
|
||||
|
||||
pub mod atlauncher;
|
||||
@ -24,6 +30,19 @@ pub enum ImportLauncherType {
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
// impl display
|
||||
impl fmt::Display for ImportLauncherType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ImportLauncherType::MultiMC => write!(f, "MultiMC"),
|
||||
ImportLauncherType::PrismLauncher => write!(f, "PrismLauncher"),
|
||||
ImportLauncherType::ATLauncher => write!(f, "ATLauncher"),
|
||||
ImportLauncherType::GDLauncher => write!(f, "GDLauncher"),
|
||||
ImportLauncherType::Curseforge => write!(f, "Curseforge"),
|
||||
ImportLauncherType::Unknown => write!(f, "Unknown"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return a list of importable instances from a launcher type and base path, by iterating through the folder and checking
|
||||
pub async fn get_importable_instances(
|
||||
@ -31,12 +50,12 @@ pub async fn get_importable_instances(
|
||||
base_path: PathBuf,
|
||||
) -> crate::Result<Vec<String>> {
|
||||
// Some launchers have a different folder structure for instances
|
||||
let instances_folder = match launcher_type {
|
||||
let instances_subfolder = match launcher_type {
|
||||
ImportLauncherType::GDLauncher
|
||||
| ImportLauncherType::MultiMC
|
||||
| ImportLauncherType::PrismLauncher
|
||||
| ImportLauncherType::ATLauncher => base_path.join("instances"),
|
||||
ImportLauncherType::Curseforge => base_path.join("Instances"),
|
||||
| ImportLauncherType::ATLauncher => "instances",
|
||||
ImportLauncherType::Curseforge => "Instances",
|
||||
ImportLauncherType::Unknown => {
|
||||
return Err(crate::ErrorKind::InputError(
|
||||
"Launcher type Unknown".to_string(),
|
||||
@ -44,8 +63,13 @@ pub async fn get_importable_instances(
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let instances_folder = base_path.join(instances_subfolder);
|
||||
let mut instances = Vec::new();
|
||||
let mut dir = io::read_dir(&instances_folder).await?;
|
||||
let mut dir = io::read_dir(&instances_folder).await.map_err(| _ | {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Invalid {launcher_type} launcher path, could not find '{instances_subfolder}' subfolder."
|
||||
))
|
||||
})?;
|
||||
while let Some(entry) = dir
|
||||
.next_entry()
|
||||
.await
|
||||
@ -216,6 +240,7 @@ pub async fn recache_icon(
|
||||
async fn copy_dotminecraft(
|
||||
profile_path: ProfilePathId,
|
||||
dotminecraft: PathBuf,
|
||||
io_semaphore: &IoSemaphore,
|
||||
) -> crate::Result<()> {
|
||||
// Get full path to profile
|
||||
let profile_path = profile_path.get_full_path().await?;
|
||||
@ -236,6 +261,7 @@ async fn copy_dotminecraft(
|
||||
&path.display()
|
||||
))
|
||||
})?),
|
||||
io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@ -247,9 +273,13 @@ async fn copy_dotminecraft(
|
||||
#[theseus_macros::debug_pin]
|
||||
#[async_recursion::async_recursion]
|
||||
#[tracing::instrument]
|
||||
async fn copy_dir_to(src: &Path, dst: &Path) -> crate::Result<()> {
|
||||
async fn copy_dir_to(
|
||||
src: &Path,
|
||||
dst: &Path,
|
||||
io_semaphore: &IoSemaphore,
|
||||
) -> crate::Result<()> {
|
||||
if !src.is_dir() {
|
||||
io::copy(src, dst).await?;
|
||||
fetch::copy(src, dst, io_semaphore).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@ -273,10 +303,10 @@ async fn copy_dir_to(src: &Path, dst: &Path) -> crate::Result<()> {
|
||||
|
||||
if src_child.is_dir() {
|
||||
// Recurse into sub-directory
|
||||
copy_dir_to(&src_child, &dst_child).await?;
|
||||
copy_dir_to(&src_child, &dst_child, io_semaphore).await?;
|
||||
} else {
|
||||
// Copy file
|
||||
io::copy(&src_child, &dst_child).await?;
|
||||
fetch::copy(&src_child, &dst_child, io_semaphore).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ use crate::prelude::ProfilePathId;
|
||||
use crate::state::{ProfileInstallStage, Profiles, SideType};
|
||||
use crate::util::fetch::{fetch_mirrors, write};
|
||||
use crate::util::io;
|
||||
use crate::State;
|
||||
use crate::{profile, State};
|
||||
use async_zip::tokio::read::seek::ZipFileReader;
|
||||
|
||||
use std::io::Cursor;
|
||||
@ -82,6 +82,7 @@ pub async fn install_zipped_mrpack_files(
|
||||
let version_id = create_pack.description.version_id;
|
||||
let existing_loading_bar = create_pack.description.existing_loading_bar;
|
||||
let profile_path = create_pack.description.profile_path;
|
||||
let icon_exists = icon.is_some();
|
||||
|
||||
let reader: Cursor<&bytes::Bytes> = Cursor::new(&file);
|
||||
|
||||
@ -186,7 +187,7 @@ pub async fn install_zipped_mrpack_files(
|
||||
let path = profile_path
|
||||
.get_full_path()
|
||||
.await?
|
||||
.join(project.path);
|
||||
.join(&project.path);
|
||||
write(&path, &file, &state.io_semaphore)
|
||||
.await?;
|
||||
}
|
||||
@ -261,6 +262,14 @@ pub async fn install_zipped_mrpack_files(
|
||||
}
|
||||
}
|
||||
|
||||
// If the icon doesn't exist, we expect icon.png to be a potential icon.
|
||||
// If it doesn't exist, and an override to icon.png exists, cache and use that
|
||||
let potential_icon =
|
||||
profile_path.get_full_path().await?.join("icon.png");
|
||||
if !icon_exists && potential_icon.exists() {
|
||||
profile::edit_icon(&profile_path, Some(&potential_icon)).await?;
|
||||
}
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
{
|
||||
|
||||
@ -104,7 +104,7 @@ pub async fn profile_create(
|
||||
|
||||
emit_profile(
|
||||
uuid,
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile.profile_id(),
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Created,
|
||||
)
|
||||
|
||||
@ -45,7 +45,7 @@ pub async fn remove(path: &ProfilePathId) -> crate::Result<()> {
|
||||
if let Some(profile) = profiles.remove(path).await? {
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.get_profile_full_path().await?,
|
||||
path,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Removed,
|
||||
)
|
||||
@ -124,7 +124,7 @@ where
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.get_profile_full_path().await?,
|
||||
path,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
@ -162,7 +162,7 @@ pub async fn edit_icon(
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.get_profile_full_path().await?,
|
||||
path,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
@ -285,7 +285,6 @@ pub async fn update_all_projects(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let profile_base_path = profile.get_profile_full_path().await?;
|
||||
let keys = profile
|
||||
.projects
|
||||
.into_iter()
|
||||
@ -331,7 +330,7 @@ pub async fn update_all_projects(
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile_base_path,
|
||||
profile_path,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
@ -378,10 +377,12 @@ pub async fn update_project(
|
||||
if let Some(mut project) = value {
|
||||
if let ProjectMetadata::Modrinth {
|
||||
ref mut version,
|
||||
ref mut update_version,
|
||||
..
|
||||
} = project.metadata
|
||||
{
|
||||
*version = Box::new(new_version);
|
||||
*update_version = None;
|
||||
}
|
||||
profile.projects.insert(path.clone(), project);
|
||||
}
|
||||
@ -391,7 +392,7 @@ pub async fn update_project(
|
||||
if !skip_send_event.unwrap_or(false) {
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.get_profile_full_path().await?,
|
||||
profile_path,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
@ -427,7 +428,7 @@ pub async fn add_project_from_version(
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.get_profile_full_path().await?,
|
||||
profile_path,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
@ -467,7 +468,7 @@ pub async fn add_project_from_path(
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.get_profile_full_path().await?,
|
||||
profile_path,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
@ -488,15 +489,15 @@ pub async fn add_project_from_path(
|
||||
/// returns the new state, relative to the profile
|
||||
#[tracing::instrument]
|
||||
pub async fn toggle_disable_project(
|
||||
profile: &ProfilePathId,
|
||||
profile_path: &ProfilePathId,
|
||||
project: &ProjectPathId,
|
||||
) -> crate::Result<ProjectPathId> {
|
||||
if let Some(profile) = get(profile, None).await? {
|
||||
if let Some(profile) = get(profile_path, None).await? {
|
||||
let res = profile.toggle_disable_project(project).await?;
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.get_profile_full_path().await?,
|
||||
profile_path,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
@ -505,8 +506,10 @@ pub async fn toggle_disable_project(
|
||||
|
||||
Ok(res)
|
||||
} else {
|
||||
Err(crate::ErrorKind::UnmanagedProfileError(profile.to_string())
|
||||
.as_error())
|
||||
Err(
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
|
||||
.as_error(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -514,15 +517,15 @@ pub async fn toggle_disable_project(
|
||||
/// Uses and returns the relative path to the project
|
||||
#[tracing::instrument]
|
||||
pub async fn remove_project(
|
||||
profile: &ProfilePathId,
|
||||
profile_path: &ProfilePathId,
|
||||
project: &ProjectPathId,
|
||||
) -> crate::Result<()> {
|
||||
if let Some(profile) = get(profile, None).await? {
|
||||
if let Some(profile) = get(profile_path, None).await? {
|
||||
profile.remove_project(project, None).await?;
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.get_profile_full_path().await?,
|
||||
profile_path,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
@ -531,8 +534,10 @@ pub async fn remove_project(
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(crate::ErrorKind::UnmanagedProfileError(profile.to_string())
|
||||
.as_error())
|
||||
Err(
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
|
||||
.as_error(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1032,5 +1037,5 @@ pub async fn build_folder(
|
||||
}
|
||||
|
||||
pub fn sanitize_profile_name(input: &str) -> String {
|
||||
input.replace(['/', '\\'], "_")
|
||||
input.replace(['/', '\\', ':'], "_")
|
||||
}
|
||||
|
||||
@ -57,7 +57,7 @@ pub async fn update_managed_modrinth(
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.path,
|
||||
profile_path,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
@ -133,7 +133,7 @@ pub async fn repair_managed_modrinth(
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.path,
|
||||
profile_path,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
|
||||
@ -4,10 +4,10 @@ use crate::{
|
||||
CommandPayload, EventError, LoadingBar, LoadingBarType,
|
||||
ProcessPayloadType, ProfilePayloadType,
|
||||
},
|
||||
prelude::ProfilePathId,
|
||||
state::{ProcessType, SafeProcesses},
|
||||
};
|
||||
use futures::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
use crate::event::{
|
||||
@ -298,12 +298,13 @@ pub async fn emit_process(
|
||||
#[allow(unused_variables)]
|
||||
pub async fn emit_profile(
|
||||
uuid: Uuid,
|
||||
path: PathBuf,
|
||||
profile_path_id: &ProfilePathId,
|
||||
name: &str,
|
||||
event: ProfilePayloadType,
|
||||
) -> crate::Result<()> {
|
||||
#[cfg(feature = "tauri")]
|
||||
{
|
||||
let path = profile_path_id.get_full_path().await?;
|
||||
let event_state = crate::EventState::get().await?;
|
||||
event_state
|
||||
.app
|
||||
@ -311,6 +312,7 @@ pub async fn emit_profile(
|
||||
"profile",
|
||||
ProfilePayload {
|
||||
uuid,
|
||||
profile_path_id: profile_path_id.clone(),
|
||||
path,
|
||||
name: name.to_string(),
|
||||
event,
|
||||
|
||||
@ -5,6 +5,7 @@ use tokio::sync::OnceCell;
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::prelude::ProfilePathId;
|
||||
use crate::state::SafeProcesses;
|
||||
|
||||
pub mod emit;
|
||||
@ -240,6 +241,7 @@ pub enum ProcessPayloadType {
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct ProfilePayload {
|
||||
pub uuid: Uuid,
|
||||
pub profile_path_id: ProfilePathId,
|
||||
pub path: PathBuf,
|
||||
pub name: String,
|
||||
pub event: ProfilePayloadType,
|
||||
|
||||
@ -303,7 +303,10 @@ impl Profile {
|
||||
let profile = crate::api::profile::get(&path, None).await?;
|
||||
|
||||
if let Some(profile) = profile {
|
||||
emit_warning(&format!("Profile {} has crashed! Visit the logs page to see a crash report.", profile.metadata.name)).await?;
|
||||
// Hide warning if profile is not yet installed
|
||||
if profile.install_stage == ProfileInstallStage::Installed {
|
||||
emit_warning(&format!("Profile {} has crashed! Visit the logs page to see a crash report.", profile.metadata.name)).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), crate::Error>(())
|
||||
@ -354,7 +357,7 @@ impl Profile {
|
||||
}
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile_path_id,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Synced,
|
||||
)
|
||||
@ -856,7 +859,7 @@ impl Profiles {
|
||||
pub async fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile.profile_id(),
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Added,
|
||||
)
|
||||
@ -943,7 +946,7 @@ impl Profiles {
|
||||
// if path exists in the state but no longer in the filesystem, remove it from the state list
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile_path_id,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Removed,
|
||||
)
|
||||
|
||||
@ -231,6 +231,30 @@ pub async fn write<'a>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn copy(
|
||||
src: impl AsRef<std::path::Path>,
|
||||
dest: impl AsRef<std::path::Path>,
|
||||
semaphore: &IoSemaphore,
|
||||
) -> crate::Result<()> {
|
||||
let src: &Path = src.as_ref();
|
||||
let dest = dest.as_ref();
|
||||
|
||||
let io_semaphore = semaphore.0.read().await;
|
||||
let _permit = io_semaphore.acquire().await?;
|
||||
|
||||
if let Some(parent) = dest.parent() {
|
||||
io::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
io::copy(src, dest).await?;
|
||||
tracing::trace!(
|
||||
"Done copying file {} to {}",
|
||||
src.display(),
|
||||
dest.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Writes a icon to the cache and returns the absolute path of the icon within the cache directory
|
||||
#[tracing::instrument(skip(bytes, semaphore))]
|
||||
pub async fn write_cached_icon(
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
// IO error
|
||||
// A wrapper around the tokio IO functions that adds the path to the error message, instead of the uninformative std::io::Error.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum IOError {
|
||||
#[error("{source}, path: {path}")]
|
||||
@ -140,7 +142,7 @@ pub async fn copy(
|
||||
from: impl AsRef<std::path::Path>,
|
||||
to: impl AsRef<std::path::Path>,
|
||||
) -> Result<u64, IOError> {
|
||||
let from = from.as_ref();
|
||||
let from: &Path = from.as_ref();
|
||||
let to = to.as_ref();
|
||||
tokio::fs::copy(from, to)
|
||||
.await
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use theseus::{handler, prelude::CommandPayload, State};
|
||||
|
||||
use crate::api::Result;
|
||||
@ -6,6 +7,7 @@ use std::{env, process::Command};
|
||||
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
tauri::plugin::Builder::new("utils")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_os,
|
||||
should_disable_mouseover,
|
||||
show_in_folder,
|
||||
progress_bars_list,
|
||||
@ -18,6 +20,24 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Gets OS
|
||||
#[tauri::command]
|
||||
pub fn get_os() -> OS {
|
||||
#[cfg(target_os = "windows")]
|
||||
let os = OS::Windows;
|
||||
#[cfg(target_os = "linux")]
|
||||
let os = OS::Linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
let os = OS::MacOS;
|
||||
os
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum OS {
|
||||
Windows,
|
||||
Linux,
|
||||
MacOS,
|
||||
}
|
||||
|
||||
// Lists active progress bars
|
||||
// Create a new HashMap with the same keys
|
||||
// Values provided should not be used directly, as they are not guaranteed to be up-to-date
|
||||
|
||||
@ -24,7 +24,7 @@ import { offline_listener, command_listener, warning_listener } from '@/helpers/
|
||||
import { MinimizeIcon, MaximizeIcon } from '@/assets/icons'
|
||||
import { type } from '@tauri-apps/api/os'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { isDev, isOffline } from '@/helpers/utils.js'
|
||||
import { isDev, getOS, isOffline } from '@/helpers/utils.js'
|
||||
import {
|
||||
mixpanel_track,
|
||||
mixpanel_init,
|
||||
@ -44,9 +44,9 @@ import OnboardingScreen from '@/components/ui/tutorial/OnboardingScreen.vue'
|
||||
const themeStore = useTheming()
|
||||
const urlModal = ref(null)
|
||||
const isLoading = ref(true)
|
||||
const offline = ref(false)
|
||||
|
||||
const videoPlaying = ref(true)
|
||||
const videoPlaying = ref(false)
|
||||
const offline = ref(false)
|
||||
const showOnboarding = ref(false)
|
||||
|
||||
const onboardingVideo = ref()
|
||||
@ -56,6 +56,9 @@ defineExpose({
|
||||
isLoading.value = false
|
||||
const { theme, opt_out_analytics, collapsed_navigation, advanced_rendering, onboarded_new } =
|
||||
await get()
|
||||
const os = await getOS()
|
||||
// video should play if the user is not on linux, and has not onboarded
|
||||
videoPlaying.value = !onboarded_new && os !== 'Linux'
|
||||
const dev = await isDev()
|
||||
const version = await getVersion()
|
||||
showOnboarding.value = !onboarded_new
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
<UploadIcon />
|
||||
Select icon
|
||||
</Button>
|
||||
<Button @click="reset_icon">
|
||||
<Button :disabled="!display_icon" @click="reset_icon">
|
||||
<XIcon />
|
||||
Remove icon
|
||||
</Button>
|
||||
@ -73,7 +73,7 @@
|
||||
<CodeIcon />
|
||||
{{ showAdvanced ? 'Hide advanced' : 'Show advanced' }}
|
||||
</Button>
|
||||
<Button @click="$refs.modal.hide()">
|
||||
<Button @click="hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</Button>
|
||||
@ -202,7 +202,7 @@ import {
|
||||
FolderSearchIcon,
|
||||
UpdatedIcon,
|
||||
} from 'omorphia'
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
import { computed, onUnmounted, ref, shallowRef } from 'vue'
|
||||
import { get_loaders } from '@/helpers/tags'
|
||||
import { create } from '@/helpers/profile'
|
||||
import { open } from '@tauri-apps/api/dialog'
|
||||
@ -219,7 +219,11 @@ import { mixpanel_track } from '@/helpers/mixpanel'
|
||||
import { useTheming } from '@/store/state.js'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { install_from_file } from '@/helpers/pack.js'
|
||||
import { get_importable_instances, import_instance } from '@/helpers/import.js'
|
||||
import {
|
||||
get_default_launcher_path,
|
||||
get_importable_instances,
|
||||
import_instance,
|
||||
} from '@/helpers/import.js'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
@ -234,9 +238,10 @@ const showAdvanced = ref(false)
|
||||
const creating = ref(false)
|
||||
const showSnapshots = ref(false)
|
||||
const creationType = ref('from file')
|
||||
const isShowing = ref(false)
|
||||
|
||||
defineExpose({
|
||||
show: () => {
|
||||
show: async () => {
|
||||
game_version.value = ''
|
||||
specified_loader_version.value = ''
|
||||
profile_name.value = ''
|
||||
@ -247,12 +252,42 @@ defineExpose({
|
||||
loader_version.value = 'stable'
|
||||
icon.value = null
|
||||
display_icon.value = null
|
||||
isShowing.value = true
|
||||
modal.value.show()
|
||||
|
||||
unlistener.value = await listen('tauri://file-drop', async (event) => {
|
||||
// Only if modal is showing
|
||||
if (!isShowing.value) return
|
||||
if (creationType.value !== 'from file') return
|
||||
hide()
|
||||
if (event.payload && event.payload.length > 0 && event.payload[0].endsWith('.mrpack')) {
|
||||
await install_from_file(event.payload[0]).catch(handleError)
|
||||
mixpanel_track('InstanceCreate', {
|
||||
source: 'CreationModalFileDrop',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
mixpanel_track('InstanceCreateStart', { source: 'CreationModal' })
|
||||
},
|
||||
})
|
||||
|
||||
const unlistener = ref(null)
|
||||
const hide = () => {
|
||||
isShowing.value = false
|
||||
modal.value.hide()
|
||||
if (unlistener.value) {
|
||||
unlistener.value()
|
||||
unlistener.value = null
|
||||
}
|
||||
}
|
||||
onUnmounted(() => {
|
||||
if (unlistener.value) {
|
||||
unlistener.value()
|
||||
unlistener.value = null
|
||||
}
|
||||
})
|
||||
|
||||
const [fabric_versions, forge_versions, quilt_versions, all_game_versions, loaders] =
|
||||
await Promise.all([
|
||||
get_fabric_versions().then(shallowRef).catch(handleError),
|
||||
@ -303,7 +338,7 @@ const create_instance = async () => {
|
||||
loader_version.value === 'other' ? specified_loader_version.value : loader_version.value
|
||||
const loaderVersion = loader.value === 'vanilla' ? null : loader_version_value ?? 'stable'
|
||||
|
||||
modal.value.hide()
|
||||
hide()
|
||||
creating.value = false
|
||||
|
||||
await create(
|
||||
@ -366,8 +401,7 @@ const toggle_advanced = () => {
|
||||
const openFile = async () => {
|
||||
const newProject = await open({ multiple: false })
|
||||
if (!newProject) return
|
||||
|
||||
modal.value.hide()
|
||||
hide()
|
||||
await install_from_file(newProject).catch(handleError)
|
||||
|
||||
mixpanel_track('InstanceCreate', {
|
||||
@ -375,16 +409,6 @@ const openFile = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
listen('tauri://file-drop', async (event) => {
|
||||
modal.value.hide()
|
||||
if (event.payload && event.payload.length > 0 && event.payload[0].endsWith('.mrpack')) {
|
||||
await install_from_file(event.payload[0]).catch(handleError)
|
||||
mixpanel_track('InstanceCreate', {
|
||||
source: 'CreationModalFileDrop',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const profiles = ref(
|
||||
new Map([
|
||||
['MultiMC', []],
|
||||
@ -406,6 +430,27 @@ const profileOptions = ref([
|
||||
{ name: 'PrismLauncher', path: '' },
|
||||
])
|
||||
|
||||
// Attempt to get import profiles on default paths
|
||||
const promises = profileOptions.value.map(async (option) => {
|
||||
const path = await get_default_launcher_path(option.name).catch(handleError)
|
||||
if (!path || path === '') return
|
||||
|
||||
// Try catch to allow failure and simply ignore default path attempt
|
||||
try {
|
||||
const instances = await get_importable_instances(option.name, path)
|
||||
|
||||
if (!instances) return
|
||||
profileOptions.value.find((profile) => profile.name === option.name).path = path
|
||||
profiles.value.set(
|
||||
option.name,
|
||||
instances.map((name) => ({ name, selected: false }))
|
||||
)
|
||||
} catch (error) {
|
||||
// Allow failure silently
|
||||
}
|
||||
})
|
||||
await Promise.all(promises)
|
||||
|
||||
const selectLauncherPath = async () => {
|
||||
selectedProfileType.value.path = await open({ multiple: false, directory: true })
|
||||
|
||||
@ -419,10 +464,14 @@ const reload = async () => {
|
||||
selectedProfileType.value.name,
|
||||
selectedProfileType.value.path
|
||||
).catch(handleError)
|
||||
profiles.value.set(
|
||||
selectedProfileType.value.name,
|
||||
instances.map((name) => ({ name, selected: false }))
|
||||
)
|
||||
if (instances) {
|
||||
profiles.value.set(
|
||||
selectedProfileType.value.name,
|
||||
instances.map((name) => ({ name, selected: false }))
|
||||
)
|
||||
} else {
|
||||
profiles.value.set(selectedProfileType.value.name, [])
|
||||
}
|
||||
}
|
||||
|
||||
const setPath = () => {
|
||||
|
||||
@ -99,6 +99,8 @@ function setJavaInstall(javaInstall) {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -65,7 +65,6 @@ const profiles = ref([])
|
||||
|
||||
async function install(instance) {
|
||||
instance.installing = true
|
||||
console.log(versions.value)
|
||||
const version = versions.value.find((v) => {
|
||||
return (
|
||||
v.game_versions.includes(instance.metadata.game_version) &&
|
||||
@ -264,7 +263,7 @@ const check_valid = computed(() => {
|
||||
<UploadIcon />
|
||||
<span class="no-wrap"> Select icon </span>
|
||||
</Button>
|
||||
<Button @click="reset_icon()">
|
||||
<Button :disabled="!display_icon" @click="reset_icon()">
|
||||
<XIcon />
|
||||
<span class="no-wrap"> Remove icon </span>
|
||||
</Button>
|
||||
|
||||
@ -10,7 +10,11 @@ import {
|
||||
UpdatedIcon,
|
||||
} from 'omorphia'
|
||||
import { ref } from 'vue'
|
||||
import { get_importable_instances, import_instance } from '@/helpers/import.js'
|
||||
import {
|
||||
get_default_launcher_path,
|
||||
get_importable_instances,
|
||||
import_instance,
|
||||
} from '@/helpers/import.js'
|
||||
import { open } from '@tauri-apps/api/dialog'
|
||||
import { handleError } from '@/store/state.js'
|
||||
|
||||
@ -46,6 +50,27 @@ const profileOptions = ref([
|
||||
{ name: 'PrismLauncher', path: '' },
|
||||
])
|
||||
|
||||
// Attempt to get import profiles on default paths
|
||||
const promises = profileOptions.value.map(async (option) => {
|
||||
const path = await get_default_launcher_path(option.name).catch(handleError)
|
||||
if (!path || path === '') return
|
||||
|
||||
// Try catch to allow failure and simply ignore default path attempt
|
||||
try {
|
||||
const instances = await get_importable_instances(option.name, path)
|
||||
|
||||
if (!instances) return
|
||||
profileOptions.value.find((profile) => profile.name === option.name).path = path
|
||||
profiles.value.set(
|
||||
option.name,
|
||||
instances.map((name) => ({ name, selected: false }))
|
||||
)
|
||||
} catch (error) {
|
||||
// Allow failure silently
|
||||
}
|
||||
})
|
||||
Promise.all(promises)
|
||||
|
||||
const selectLauncherPath = async () => {
|
||||
selectedProfileType.value.path = await open({ multiple: false, directory: true })
|
||||
|
||||
|
||||
@ -62,7 +62,8 @@ export async function process_listener(callback) {
|
||||
ProfilePayload {
|
||||
uuid: unique identification of the process in the state (currently identified by path, but that will change)
|
||||
name: name of the profile
|
||||
path: path to profile
|
||||
profile_path: relative path to profile (used for path identification)
|
||||
path: path to profile (used for opening the profile in the OS file explorer)
|
||||
event: event type ("Created", "Added", "Edited", "Removed")
|
||||
}
|
||||
*/
|
||||
|
||||
@ -11,6 +11,11 @@ export async function isDev() {
|
||||
return await invoke('is_dev')
|
||||
}
|
||||
|
||||
// One of 'Windows', 'Linux', 'MacOS'
|
||||
export async function getOS() {
|
||||
return await invoke('plugin:utils|get_os')
|
||||
}
|
||||
|
||||
export async function showInFolder(path) {
|
||||
return await invoke('plugin:utils|show_in_folder', { path })
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Card, Slider, DropdownSelect, Checkbox, Toggle } from 'omorphia'
|
||||
import { Card, Slider, DropdownSelect, Toggle } from 'omorphia'
|
||||
import { handleError, useTheming } from '@/store/state'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get_max_memory } from '@/helpers/jre'
|
||||
@ -336,7 +336,16 @@ watch(
|
||||
Overwrites the option.txt file to start in full screen when launched.
|
||||
</span>
|
||||
</label>
|
||||
<Checkbox id="fullscreen" v-model="settings.force_fullscreen" />
|
||||
<Toggle
|
||||
id="fullscreen"
|
||||
:model-value="settings.force_fullscreen"
|
||||
:checked="settings.force_fullscreen"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
settings.force_fullscreen = e
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="width">
|
||||
|
||||
@ -284,7 +284,7 @@ const handleOptionsClick = async (args) => {
|
||||
}
|
||||
|
||||
const unlistenProfiles = await profile_listener(async (event) => {
|
||||
if (event.path === route.params.id) {
|
||||
if (event.profile_path_id === route.params.id) {
|
||||
if (event.event === 'removed') {
|
||||
await router.push({
|
||||
path: '/',
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
<CheckIcon v-else />
|
||||
{{ copied ? 'Copied' : 'Copy' }}
|
||||
</Button>
|
||||
<Button color="primary" :disabled="offline" @click="share">
|
||||
<Button color="primary" :disabled="offline || !logs[selectedLogIndex]" @click="share">
|
||||
<ShareIcon />
|
||||
Share
|
||||
</Button>
|
||||
|
||||
@ -336,7 +336,7 @@ import {
|
||||
ShareModal,
|
||||
CodeIcon,
|
||||
} from 'omorphia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
add_project_from_path,
|
||||
@ -353,7 +353,6 @@ import { listen } from '@tauri-apps/api/event'
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage } from '@/assets/icons'
|
||||
import { install_from_file } from '@/helpers/pack'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@ -785,18 +784,15 @@ watch(selectAll, () => {
|
||||
}
|
||||
})
|
||||
|
||||
listen('tauri://file-drop', async (event) => {
|
||||
if (event.payload && event.payload.length > 0 && event.payload[0].endsWith('.mrpack')) {
|
||||
await install_from_file(event.payload[0]).catch(handleError)
|
||||
} else {
|
||||
for (const file of event.payload) {
|
||||
await add_project_from_path(props.instance.path, file, 'mod').catch(handleError)
|
||||
}
|
||||
initProjects(await get(props.instance.path).catch(handleError))
|
||||
const unlisten = await listen('tauri://file-drop', async (event) => {
|
||||
for (const file of event.payload) {
|
||||
if (file.endsWith('.mrpack')) continue
|
||||
await add_project_from_path(props.instance.path, file, 'mod').catch(handleError)
|
||||
}
|
||||
mixpanel_track('InstanceCreate', {
|
||||
source: 'FileDrop',
|
||||
})
|
||||
initProjects(await get(props.instance.path).catch(handleError))
|
||||
})
|
||||
onUnmounted(() => {
|
||||
unlisten()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="!isValid || editing"
|
||||
:disabled="!isValid || !isChanged || editing"
|
||||
@click="saveGvLoaderEdits()"
|
||||
>
|
||||
<SaveIcon />
|
||||
@ -71,7 +71,11 @@
|
||||
<UploadIcon />
|
||||
Select icon
|
||||
</button>
|
||||
<button class="btn" @click="resetIcon">
|
||||
<button
|
||||
:disabled="!(!icon || (icon && icon.startsWith('http')) ? icon : convertFileSrc(icon))"
|
||||
class="btn"
|
||||
@click="resetIcon"
|
||||
>
|
||||
<TrashIcon />
|
||||
Remove icon
|
||||
</button>
|
||||
@ -93,8 +97,8 @@
|
||||
<button
|
||||
id="edit-versions"
|
||||
class="btn"
|
||||
@click="$refs.changeVersionsModal.show()"
|
||||
:disabled="offline"
|
||||
@click="$refs.changeVersionsModal.show()"
|
||||
>
|
||||
<EditIcon />
|
||||
Edit versions
|
||||
@ -640,6 +644,15 @@ const isValid = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const isChanged = computed(() => {
|
||||
return (
|
||||
loader.value != props.instance.metadata.loader ||
|
||||
gameVersion.value != props.instance.metadata.game_version ||
|
||||
JSON.stringify(selectableLoaderVersions.value[loaderVersionIndex.value]) !=
|
||||
JSON.stringify(props.instance.metadata.loader_version)
|
||||
)
|
||||
})
|
||||
|
||||
watch(loader, () => (loaderVersionIndex.value = 0))
|
||||
|
||||
const editing = ref(false)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user