From 87449f91c3d76dc784238fe8a30e6594c696e850 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Fri, 28 Jul 2023 19:56:49 -0700 Subject: [PATCH] Misc bugs 4 (#381) * bug fixes * fixed jres being undetected * cleanup * prettier * fixed folders not displaying windows exporting * fixes, more bugs * missed function * clippy, fmt * prettier --- theseus/src/api/profile/mod.rs | 20 ++++++-- theseus/src/state/children.rs | 5 ++ theseus/src/state/mod.rs | 2 +- theseus/src/state/profiles.rs | 16 +++--- theseus/src/state/projects.rs | 14 ++++-- theseus/src/util/jre.rs | 21 +++----- theseus_gui/src-tauri/src/api/profile.rs | 9 ++++ theseus_gui/src-tauri/src/api/utils.rs | 4 +- theseus_gui/src-tauri/src/main.rs | 32 +++++++++--- theseus_gui/src-tauri/tauri.conf.json | 4 +- theseus_gui/src/App.vue | 2 +- theseus_gui/src/components/RowDisplay.vue | 4 +- theseus_gui/src/components/ui/ExportModal.vue | 5 +- .../src/components/ui/InstallConfirmModal.vue | 3 +- theseus_gui/src/components/ui/Instance.vue | 4 +- theseus_gui/src/helpers/profile.js | 6 +++ theseus_gui/src/helpers/utils.js | 12 ++++- theseus_gui/src/main.js | 6 ++- theseus_gui/src/pages/Browse.vue | 2 +- theseus_gui/src/pages/instance/Index.vue | 6 +-- theseus_gui/src/pages/instance/Mods.vue | 50 ++++++++++++++----- theseus_gui/src/pages/project/Index.vue | 22 ++++---- 22 files changed, 171 insertions(+), 78 deletions(-) diff --git a/theseus/src/api/profile/mod.rs b/theseus/src/api/profile/mod.rs index 7bc5ca383..88e152a0a 100644 --- a/theseus/src/api/profile/mod.rs +++ b/theseus/src/api/profile/mod.rs @@ -94,6 +94,19 @@ pub async fn get_by_uuid( Ok(profile) } +/// Get profile's full path in the filesystem +#[tracing::instrument] +pub async fn get_full_path(path: &ProfilePathId) -> crate::Result { + let _ = get(path, Some(true)).await?.ok_or_else(|| { + crate::ErrorKind::OtherError(format!( + "Tried to get the full path of a nonexistent or unloaded profile at path {}!", + path + )) + })?; + let full_path = io::canonicalize(path.get_full_path().await?)?; + Ok(full_path) +} + /// Edit a profile using a given asynchronous closure pub async fn edit( path: &ProfilePathId, @@ -373,6 +386,7 @@ pub async fn update_project( profile.projects.insert(path.clone(), project); } } + drop(profiles); if !skip_send_event.unwrap_or(false) { emit_profile( @@ -409,7 +423,7 @@ pub async fn add_project_from_version( version_id: String, ) -> crate::Result { if let Some(profile) = get(profile_path, None).await? { - let (path, _) = profile.add_project_version(version_id).await?; + let (project_path, _) = profile.add_project_version(version_id).await?; emit_profile( profile.uuid, @@ -418,9 +432,7 @@ pub async fn add_project_from_version( ProfilePayloadType::Edited, ) .await?; - State::sync().await?; - - Ok(path) + Ok(project_path) } else { Err( crate::ErrorKind::UnmanagedProfileError(profile_path.to_string()) diff --git a/theseus/src/state/children.rs b/theseus/src/state/children.rs index d00b86bb2..78438cced 100644 --- a/theseus/src/state/children.rs +++ b/theseus/src/state/children.rs @@ -143,6 +143,8 @@ impl Children { mc_exit_status = t; break; } + // sleep for 10ms + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; } { @@ -204,6 +206,9 @@ impl Children { mc_exit_status = t; break; } + // sleep for 10ms + tokio::time::sleep(tokio::time::Duration::from_millis(10)) + .await; } } diff --git a/theseus/src/state/mod.rs b/theseus/src/state/mod.rs index 0f8be0965..c0ae6a1b3 100644 --- a/theseus/src/state/mod.rs +++ b/theseus/src/state/mod.rs @@ -270,7 +270,7 @@ pub async fn init_watcher() -> crate::Result> { let (mut tx, mut rx) = channel(1); let file_watcher = new_debouncer( - Duration::from_secs(2), + Duration::from_secs_f32(0.25), None, move |res: DebounceEventResult| { futures::executor::block_on(async { diff --git a/theseus/src/state/profiles.rs b/theseus/src/state/profiles.rs index 1eea33bac..1e13e6467 100644 --- a/theseus/src/state/profiles.rs +++ b/theseus/src/state/profiles.rs @@ -107,7 +107,8 @@ impl ProjectPathId { let profiles_dir: PathBuf = io::canonicalize( State::get().await?.directories.profiles_dir().await, )?; - path.strip_prefix(profiles_dir) + let path = path + .strip_prefix(profiles_dir) .ok() .map(|p| p.components().skip(1).collect::()) .ok_or_else(|| { @@ -458,7 +459,6 @@ impl Profile { version_id: String, ) -> crate::Result<(ProjectPathId, ModrinthVersion)> { let state = State::get().await?; - let version = fetch_json::( Method::GET, &format!("{MODRINTH_API_URL}version/{version_id}"), @@ -467,7 +467,6 @@ impl Profile { &state.fetch_semaphore, ) .await?; - let file = if let Some(file) = version.files.iter().find(|x| x.primary) { file @@ -486,7 +485,6 @@ impl Profile { &state.fetch_semaphore, ) .await?; - let path = self .add_project_bytes( &file.filename, @@ -494,7 +492,6 @@ impl Profile { ProjectType::get_from_loaders(version.loaders.clone()), ) .await?; - Ok((path, version)) } @@ -569,7 +566,6 @@ impl Profile { } /// Toggle a project's disabled state. - /// 'path' should be relative to the profile's path. #[tracing::instrument(skip(self))] #[theseus_macros::debug_pin] pub async fn toggle_disable_project( @@ -662,11 +658,11 @@ impl Profile { } } } else { - return Err(crate::ErrorKind::InputError(format!( - "Project path does not exist: {:?}", + // If we are removing a project that doesn't exist, allow it to pass through without error, but warn + tracing::warn!( + "Attempted to remove non-existent project: {:?}", relative_path - )) - .into()); + ); } Ok(()) diff --git a/theseus/src/state/projects.rs b/theseus/src/state/projects.rs index dd3802143..27d3152d8 100644 --- a/theseus/src/state/projects.rs +++ b/theseus/src/state/projects.rs @@ -272,6 +272,16 @@ 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 { + if !path.exists() { + continue; + } + if let Some(ext) = path.extension() { + // Ignore txt configuration files + if ext == "txt" { + continue; + } + } + let mut file = tokio::fs::File::open(path.clone()) .await .map_err(|e| IOError::with_path(e, &path))?; @@ -460,9 +470,7 @@ pub async fn infer_data_from_files( .await .is_ok() { - if let Ok(pack) = - serde_json::from_str::(&file_str) - { + if let Ok(pack) = toml::from_str::(&file_str) { if let Some(pack) = pack.mods.first() { let icon = read_icon_from_file( pack.logo_file.clone(), diff --git a/theseus/src/util/jre.rs b/theseus/src/util/jre.rs index 07a3454e1..f236bcb4e 100644 --- a/theseus/src/util/jre.rs +++ b/theseus/src/util/jre.rs @@ -2,11 +2,9 @@ use super::io; use futures::prelude::*; use serde::{Deserialize, Serialize}; use std::env; -use std::io::Write; use std::path::PathBuf; use std::process::Command; use std::{collections::HashSet, path::Path}; -use tempfile::NamedTempFile; use tokio::task::JoinError; use crate::State; @@ -280,20 +278,17 @@ pub async fn check_java_at_filepath(path: &Path) -> Option { return None; }; - let mut file = NamedTempFile::new().ok()?; - file.write_all(include_bytes!("../../library/JavaInfo.class")) - .ok()?; + let bytes = include_bytes!("../../library/JavaInfo.class"); + let tempdir: PathBuf = tempfile::tempdir().ok()?.into_path(); + if !tempdir.exists() { + return None; + } + let file_path = tempdir.join("JavaInfo.class"); + io::write(&file_path, bytes).await.ok()?; - let original_path = file.path().to_path_buf(); - let mut new_path = original_path.clone(); - new_path.set_file_name("JavaInfo"); - new_path.set_extension("class"); - tokio::fs::rename(&original_path, &new_path).await.ok()?; - - // Run java checker on java binary let output = Command::new(&java) .arg("-cp") - .arg(file.path().parent().unwrap()) + .arg(file_path.parent().unwrap()) .arg("JavaInfo") .output() .ok()?; diff --git a/theseus_gui/src-tauri/src/api/profile.rs b/theseus_gui/src-tauri/src/api/profile.rs index b4c10e7dd..da86f7629 100644 --- a/theseus_gui/src-tauri/src/api/profile.rs +++ b/theseus_gui/src-tauri/src/api/profile.rs @@ -12,6 +12,7 @@ pub fn init() -> tauri::plugin::TauriPlugin { profile_remove, profile_get, profile_get_optimal_jre_key, + profile_get_full_path, profile_list, profile_check_installed, profile_install, @@ -55,6 +56,14 @@ pub async fn profile_get( Ok(res) } +// Get a profile's full path +// invoke('plugin:profile|profile_get_full_path',path) +#[tauri::command] +pub async fn profile_get_full_path(path: ProfilePathId) -> Result { + let res = profile::get_full_path(&path).await?; + Ok(res) +} + // Get optimal java version from profile #[tauri::command] pub async fn profile_get_optimal_jre_key( diff --git a/theseus_gui/src-tauri/src/api/utils.rs b/theseus_gui/src-tauri/src/api/utils.rs index cef1bbc9e..ccf5628e6 100644 --- a/theseus_gui/src-tauri/src/api/utils.rs +++ b/theseus_gui/src-tauri/src/api/utils.rs @@ -59,7 +59,7 @@ pub fn show_in_folder(path: String) -> Result<()> { #[cfg(target_os = "windows")] { Command::new("explorer") - .args(["/select,", &path]) // The comma after select is not a typo + .args([&path]) // The comma after select is not a typo .spawn()?; } @@ -86,7 +86,7 @@ pub fn show_in_folder(path: String) -> Result<()> { #[cfg(target_os = "macos")] { - Command::new("open").args(["-R", &path]).spawn()?; + Command::new("open").args([&path]).spawn()?; } Ok::<(), theseus::Error>(()) diff --git a/theseus_gui/src-tauri/src/main.rs b/theseus_gui/src-tauri/src/main.rs index d04d50dc4..165168878 100644 --- a/theseus_gui/src-tauri/src/main.rs +++ b/theseus_gui/src-tauri/src/main.rs @@ -26,6 +26,19 @@ fn is_dev() -> bool { cfg!(debug_assertions) } +// Toggles decorations +#[tauri::command] +async fn toggle_decorations(b: bool, window: tauri::Window) -> api::Result<()> { + window.set_decorations(b).map_err(|e| { + theseus::Error::from(theseus::ErrorKind::OtherError(format!( + "Failed to toggle decorations: {}", + e + ))) + })?; + println!("Toggled decorations!"); + Ok(()) +} + #[derive(Clone, serde::Serialize)] struct Payload { args: Vec, @@ -84,16 +97,12 @@ fn main() { } #[cfg(target_os = "macos")] { + win.set_decorations(true).unwrap(); + use macos::window_ext::WindowExt; win.set_transparent_titlebar(true); win.position_traffic_lights(9.0, 16.0); - } - #[cfg(not(target_os = "macos"))] - { - win.set_decorations(false).unwrap(); - } - #[cfg(target_os = "macos")] - { + macos::delegate::register_open_file(|filename| { tauri::async_runtime::spawn(api::utils::handle_command( filename, @@ -102,6 +111,9 @@ fn main() { .unwrap(); } + // Show app now that we are setup + win.show().unwrap(); + Ok(()) }); @@ -129,7 +141,11 @@ fn main() { .plugin(api::settings::init()) .plugin(api::tags::init()) .plugin(api::utils::init()) - .invoke_handler(tauri::generate_handler![initialize_state, is_dev]); + .invoke_handler(tauri::generate_handler![ + initialize_state, + is_dev, + toggle_decorations + ]); builder .run(tauri::generate_context!()) diff --git a/theseus_gui/src-tauri/tauri.conf.json b/theseus_gui/src-tauri/tauri.conf.json index 59a2921b9..302f9ed5b 100644 --- a/theseus_gui/src-tauri/tauri.conf.json +++ b/theseus_gui/src-tauri/tauri.conf.json @@ -101,7 +101,9 @@ "title": "Modrinth App", "width": 1280, "minHeight": 630, - "minWidth": 1100 + "minWidth": 1100, + "visible": false, + "decorations": false } ] } diff --git a/theseus_gui/src/App.vue b/theseus_gui/src/App.vue index 896286822..e7999348c 100644 --- a/theseus_gui/src/App.vue +++ b/theseus_gui/src/App.vue @@ -308,7 +308,7 @@ const accounts = ref(null) align-items: center; flex-grow: 1; background: var(--color-raised-bg); - box-shadow: inset 0px -3px 0px black; + box-shadow: var(--shadow-inset-sm), var(--shadow-floating); text-align: center; padding: var(--gap-md); height: 3.25rem; diff --git a/theseus_gui/src/components/RowDisplay.vue b/theseus_gui/src/components/RowDisplay.vue index 2427897a0..814255f4c 100644 --- a/theseus_gui/src/components/RowDisplay.vue +++ b/theseus_gui/src/components/RowDisplay.vue @@ -27,7 +27,7 @@ import { import { handleError } from '@/store/notifications.js' import { remove, run } from '@/helpers/profile.js' import { useRouter } from 'vue-router' -import { showInFolder } from '@/helpers/utils.js' +import { showProfileInFolder } from '@/helpers/utils.js' import { useFetch } from '@/helpers/fetch.js' import { install as pack_install } from '@/helpers/pack.js' import { useTheming } from '@/store/state.js' @@ -155,7 +155,7 @@ const handleOptionsClick = async (args) => { deleteConfirmModal.value.show() break case 'open_folder': - await showInFolder(args.item.path) + await showProfileInFolder(args.item.path) break case 'copy_path': await navigator.clipboard.writeText(args.item.path) diff --git a/theseus_gui/src/components/ui/ExportModal.vue b/theseus_gui/src/components/ui/ExportModal.vue index 239e57b24..c2f1698d5 100644 --- a/theseus_gui/src/components/ui/ExportModal.vue +++ b/theseus_gui/src/components/ui/ExportModal.vue @@ -5,6 +5,7 @@ import { ref } from 'vue' import { export_profile_mrpack, get_potential_override_folders } from '@/helpers/profile.js' import { open } from '@tauri-apps/api/dialog' import { handleError } from '@/store/notifications.js' +import { sep } from '@tauri-apps/api/path' const props = defineProps({ instance: { @@ -33,11 +34,11 @@ const initFiles = async () => { filePaths .map((folder) => ({ path: folder, - name: folder.split('/').pop(), + name: folder.split(sep).pop(), selected: false, })) .forEach((pathData) => { - const parent = pathData.path.split('/').slice(0, -1).join('/') + const parent = pathData.path.split(sep).slice(0, -1).join(sep) if (parent !== '') { if (newFolders.has(parent)) { newFolders.get(parent).push(pathData) diff --git a/theseus_gui/src/components/ui/InstallConfirmModal.vue b/theseus_gui/src/components/ui/InstallConfirmModal.vue index 56d971511..5b9acf4cd 100644 --- a/theseus_gui/src/components/ui/InstallConfirmModal.vue +++ b/theseus_gui/src/components/ui/InstallConfirmModal.vue @@ -31,14 +31,13 @@ defineExpose({ async function install() { installing.value = true console.log(`Installing ${projectId.value} ${version.value} ${title.value} ${icon.value}`) + confirmModal.value.hide() await pack_install( projectId.value, version.value, title.value, icon.value ? icon.value : null ).catch(handleError) - confirmModal.value.hide() - mixpanel.track('PackInstall', { id: projectId.value, version_id: version.value, diff --git a/theseus_gui/src/components/ui/Instance.vue b/theseus_gui/src/components/ui/Instance.vue index 5627af754..3a13032ee 100644 --- a/theseus_gui/src/components/ui/Instance.vue +++ b/theseus_gui/src/components/ui/Instance.vue @@ -14,7 +14,7 @@ import { import { process_listener } from '@/helpers/events' import { useFetch } from '@/helpers/fetch.js' import { handleError } from '@/store/state.js' -import { showInFolder } from '@/helpers/utils.js' +import { showProfileInFolder } from '@/helpers/utils.js' import InstanceInstallModal from '@/components/ui/InstanceInstallModal.vue' import mixpanel from 'mixpanel-browser' @@ -155,7 +155,7 @@ const stop = async (e, context) => { } const openFolder = async () => { - await showInFolder(props.instance.path) + await showProfileInFolder(props.instance.path) } const addContent = async () => { diff --git a/theseus_gui/src/helpers/profile.js b/theseus_gui/src/helpers/profile.js index 59c52b816..d21db9bf4 100644 --- a/theseus_gui/src/helpers/profile.js +++ b/theseus_gui/src/helpers/profile.js @@ -37,6 +37,12 @@ export async function get(path, clearProjects) { return await invoke('plugin:profile|profile_get', { path, clearProjects }) } +// Get a profile's full fs path +// Returns a path +export async function get_full_path(path) { + return await invoke('plugin:profile|profile_get_full_path', { path }) +} + // Get optimal java version from profile // Returns a java version export async function get_optimal_jre_key(path) { diff --git a/theseus_gui/src/helpers/utils.js b/theseus_gui/src/helpers/utils.js index ae5b478c2..8163d9c38 100644 --- a/theseus_gui/src/helpers/utils.js +++ b/theseus_gui/src/helpers/utils.js @@ -1,4 +1,8 @@ -import { add_project_from_version as installMod, check_installed } from '@/helpers/profile' +import { + add_project_from_version as installMod, + check_installed, + get_full_path, +} from '@/helpers/profile' import { useFetch } from '@/helpers/fetch.js' import { handleError } from '@/store/notifications.js' import { invoke } from '@tauri-apps/api/tauri' @@ -11,6 +15,12 @@ export async function showInFolder(path) { return await invoke('plugin:utils|show_in_folder', { path }) } +// Opens a profile's folder in the OS file explorer +export async function showProfileInFolder(path) { + const fullPath = await get_full_path(path) + return await showInFolder(fullPath) +} + export const releaseColor = (releaseType) => { switch (releaseType) { case 'release': diff --git a/theseus_gui/src/main.js b/theseus_gui/src/main.js index a799ee14f..2ccaa748d 100644 --- a/theseus_gui/src/main.js +++ b/theseus_gui/src/main.js @@ -23,7 +23,11 @@ app.mixin(loadCssMixin) const mountedApp = app.mount('#app') const raw_invoke = async (plugin, fn, args) => { - return await invoke('plugin:' + plugin + '|' + fn, args) + if (plugin == '') { + return await invoke(fn, args) + } else { + return await invoke('plugin:' + plugin + '|' + fn, args) + } } isDev() .then((dev) => { diff --git a/theseus_gui/src/pages/Browse.vue b/theseus_gui/src/pages/Browse.vue index 388cd5bad..495c2fa50 100644 --- a/theseus_gui/src/pages/Browse.vue +++ b/theseus_gui/src/pages/Browse.vue @@ -654,7 +654,7 @@ const showLoaders = computed( @@ -345,7 +349,7 @@ import mixpanel from 'mixpanel-browser' import { open } from '@tauri-apps/api/dialog' import { listen } from '@tauri-apps/api/event' import { convertFileSrc } from '@tauri-apps/api/tauri' -import { showInFolder } from '@/helpers/utils.js' +import { showProfileInFolder } from '@/helpers/utils.js' import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage } from '@/assets/icons' const router = useRouter() @@ -605,17 +609,39 @@ const updateProject = async (mod) => { }) } +let locks = {} + const toggleDisableMod = async (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, - id: mod.id, - name: mod.name, - project_type: mod.project_type, - disabled: mod.disabled, - }) + // Use mod's id as the key for the lock. If mod doesn't have a unique id, replace `mod.id` with some unique property. + if (!locks[mod.id]) { + locks[mod.id] = ref(null) + } + + let lock = locks[mod.id] + + while (lock.value) { + await lock.value + } + + lock.value = toggle_disable_project(props.instance.path, mod.path) + .then((newPath) => { + mod.path = newPath + mod.disabled = !mod.disabled + mixpanel.track('InstanceProjectDisable', { + loader: props.instance.metadata.loader, + game_version: props.instance.metadata.game_version, + id: mod.id, + name: mod.name, + project_type: mod.project_type, + disabled: mod.disabled, + }) + }) + .catch(handleError) + .finally(() => { + lock.value = null + }) + + await lock.value } const removeMod = async (mod) => { diff --git a/theseus_gui/src/pages/project/Index.vue b/theseus_gui/src/pages/project/Index.vue index 42499a679..82029a8e0 100644 --- a/theseus_gui/src/pages/project/Index.vue +++ b/theseus_gui/src/pages/project/Index.vue @@ -350,15 +350,19 @@ async function install(version) { } if (installed.value) { - await remove_project( - instance.value.path, - Object.entries(instance.value.projects) - .map(([key, value]) => ({ - key, - value, - })) - .find((p) => p.value.metadata?.version?.project_id === data.value.id).key - ) + const old_project = Object.entries(instance.value.projects) + .map(([key, value]) => ({ + key, + value, + })) + .find((p) => p.value.metadata?.version?.project_id === data.value.id) + if (!old_project) { + // Switching too fast, old project is not recognized as a Modrinth project yet + installing.value = false + return + } + + await remove_project(instance.value.path, old_project.key) } if (version) {