* 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:
Wyatt Verchere 2023-08-04 20:33:50 -07:00 committed by GitHub
parent 6a76811bed
commit a35dd67b77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 357 additions and 100 deletions

View File

@ -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?

View File

@ -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?

View File

@ -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?

View File

@ -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?

View File

@ -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?;
}
}

View File

@ -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?
{

View File

@ -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,
)

View File

@ -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(['/', '\\', ':'], "_")
}

View File

@ -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,
)

View File

@ -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,

View File

@ -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,

View File

@ -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,
)

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 = () => {

View File

@ -99,6 +99,8 @@ function setJavaInstall(javaInstall) {
align-items: center;
justify-content: center;
}
padding: 0.5rem;
}
}

View File

@ -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>

View File

@ -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 })

View File

@ -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")
}
*/

View File

@ -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 })
}

View File

@ -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">

View File

@ -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: '/',

View File

@ -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>

View File

@ -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>

View File

@ -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)