diff --git a/theseus/src/api/pack/import/atlauncher.rs b/theseus/src/api/pack/import/atlauncher.rs index a162e5620..e54908bfc 100644 --- a/theseus/src/api/pack/import/atlauncher.rs +++ b/theseus/src/api/pack/import/atlauncher.rs @@ -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? diff --git a/theseus/src/api/pack/import/curseforge.rs b/theseus/src/api/pack/import/curseforge.rs index 485ae6587..b1d9c2e64 100644 --- a/theseus/src/api/pack/import/curseforge.rs +++ b/theseus/src/api/pack/import/curseforge.rs @@ -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, + pub profile_image_path: Option, + pub installed_modpack: Option, 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, +} // 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 = 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? diff --git a/theseus/src/api/pack/import/gdlauncher.rs b/theseus/src/api/pack/import/gdlauncher.rs index 2d9c86e18..f7317b675 100644 --- a/theseus/src/api/pack/import/gdlauncher.rs +++ b/theseus/src/api/pack/import/gdlauncher.rs @@ -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? diff --git a/theseus/src/api/pack/import/mmc.rs b/theseus/src/api/pack/import/mmc.rs index f4e00b22a..101e8588c 100644 --- a/theseus/src/api/pack/import/mmc.rs +++ b/theseus/src/api/pack/import/mmc.rs @@ -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? diff --git a/theseus/src/api/pack/import/mod.rs b/theseus/src/api/pack/import/mod.rs index f5d026e00..6b28f73a7 100644 --- a/theseus/src/api/pack/import/mod.rs +++ b/theseus/src/api/pack/import/mod.rs @@ -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> { // 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?; } } diff --git a/theseus/src/api/pack/install_mrpack.rs b/theseus/src/api/pack/install_mrpack.rs index a905362eb..d2c9aaec3 100644 --- a/theseus/src/api/pack/install_mrpack.rs +++ b/theseus/src/api/pack/install_mrpack.rs @@ -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? { diff --git a/theseus/src/api/profile/create.rs b/theseus/src/api/profile/create.rs index ab280ee7a..e6204d713 100644 --- a/theseus/src/api/profile/create.rs +++ b/theseus/src/api/profile/create.rs @@ -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, ) diff --git a/theseus/src/api/profile/mod.rs b/theseus/src/api/profile/mod.rs index 4c619d09f..1fc712c50 100644 --- a/theseus/src/api/profile/mod.rs +++ b/theseus/src/api/profile/mod.rs @@ -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 { - 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(['/', '\\', ':'], "_") } diff --git a/theseus/src/api/profile/update.rs b/theseus/src/api/profile/update.rs index 952d5587d..bc39fc563 100644 --- a/theseus/src/api/profile/update.rs +++ b/theseus/src/api/profile/update.rs @@ -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, ) diff --git a/theseus/src/event/emit.rs b/theseus/src/event/emit.rs index 449d877d2..0375167fd 100644 --- a/theseus/src/event/emit.rs +++ b/theseus/src/event/emit.rs @@ -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, diff --git a/theseus/src/event/mod.rs b/theseus/src/event/mod.rs index c28b97a66..6436c8314 100644 --- a/theseus/src/event/mod.rs +++ b/theseus/src/event/mod.rs @@ -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, diff --git a/theseus/src/state/profiles.rs b/theseus/src/state/profiles.rs index 1e13e6467..83ab831dd 100644 --- a/theseus/src/state/profiles.rs +++ b/theseus/src/state/profiles.rs @@ -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, ) diff --git a/theseus/src/util/fetch.rs b/theseus/src/util/fetch.rs index 1cc06a42b..510ba7faf 100644 --- a/theseus/src/util/fetch.rs +++ b/theseus/src/util/fetch.rs @@ -231,6 +231,30 @@ pub async fn write<'a>( Ok(()) } +pub async fn copy( + src: impl AsRef, + dest: impl AsRef, + 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( diff --git a/theseus/src/util/io.rs b/theseus/src/util/io.rs index b915372a4..840130159 100644 --- a/theseus/src/util/io.rs +++ b/theseus/src/util/io.rs @@ -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, to: impl AsRef, ) -> Result { - let from = from.as_ref(); + let from: &Path = from.as_ref(); let to = to.as_ref(); tokio::fs::copy(from, to) .await diff --git a/theseus_gui/src-tauri/src/api/utils.rs b/theseus_gui/src-tauri/src/api/utils.rs index c1dd8465f..b7e6aef23 100644 --- a/theseus_gui/src-tauri/src/api/utils.rs +++ b/theseus_gui/src-tauri/src/api/utils.rs @@ -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() -> tauri::plugin::TauriPlugin { 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() -> tauri::plugin::TauriPlugin { .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 diff --git a/theseus_gui/src/App.vue b/theseus_gui/src/App.vue index 04c4dbfd0..a37541d1e 100644 --- a/theseus_gui/src/App.vue +++ b/theseus_gui/src/App.vue @@ -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 diff --git a/theseus_gui/src/components/ui/InstanceCreationModal.vue b/theseus_gui/src/components/ui/InstanceCreationModal.vue index fa3b059c5..756ead5c8 100644 --- a/theseus_gui/src/components/ui/InstanceCreationModal.vue +++ b/theseus_gui/src/components/ui/InstanceCreationModal.vue @@ -12,7 +12,7 @@ Select icon - @@ -73,7 +73,7 @@ {{ showAdvanced ? 'Hide advanced' : 'Show advanced' }} - @@ -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 = () => { diff --git a/theseus_gui/src/components/ui/JavaDetectionModal.vue b/theseus_gui/src/components/ui/JavaDetectionModal.vue index 385420a06..568e09e3b 100644 --- a/theseus_gui/src/components/ui/JavaDetectionModal.vue +++ b/theseus_gui/src/components/ui/JavaDetectionModal.vue @@ -99,6 +99,8 @@ function setJavaInstall(javaInstall) { align-items: center; justify-content: center; } + + padding: 0.5rem; } } diff --git a/theseus_gui/src/components/ui/ModInstallModal.vue b/theseus_gui/src/components/ui/ModInstallModal.vue index 9b53d255e..e8221b70d 100644 --- a/theseus_gui/src/components/ui/ModInstallModal.vue +++ b/theseus_gui/src/components/ui/ModInstallModal.vue @@ -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(() => { Select icon - diff --git a/theseus_gui/src/components/ui/tutorial/ImportingCard.vue b/theseus_gui/src/components/ui/tutorial/ImportingCard.vue index 252d0df53..3139b5d4a 100644 --- a/theseus_gui/src/components/ui/tutorial/ImportingCard.vue +++ b/theseus_gui/src/components/ui/tutorial/ImportingCard.vue @@ -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 }) diff --git a/theseus_gui/src/helpers/events.js b/theseus_gui/src/helpers/events.js index 7752077b6..d8899a78d 100644 --- a/theseus_gui/src/helpers/events.js +++ b/theseus_gui/src/helpers/events.js @@ -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") } */ diff --git a/theseus_gui/src/helpers/utils.js b/theseus_gui/src/helpers/utils.js index 25aa4114a..666b04102 100644 --- a/theseus_gui/src/helpers/utils.js +++ b/theseus_gui/src/helpers/utils.js @@ -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 }) } diff --git a/theseus_gui/src/pages/Settings.vue b/theseus_gui/src/pages/Settings.vue index fc6573423..b13547c6f 100644 --- a/theseus_gui/src/pages/Settings.vue +++ b/theseus_gui/src/pages/Settings.vue @@ -1,6 +1,6 @@ diff --git a/theseus_gui/src/pages/instance/Options.vue b/theseus_gui/src/pages/instance/Options.vue index 552f8e4eb..6f1e38fe3 100644 --- a/theseus_gui/src/pages/instance/Options.vue +++ b/theseus_gui/src/pages/instance/Options.vue @@ -42,7 +42,7 @@ - @@ -93,8 +97,8 @@