feat: instances v2 (#6431)

* feat: base of instances v2

* feat: use old profiles with compat layer

* prototype: instances v2

* fix: install_from using profile

* fix: skins migration fix

* fix: frontend still using profile path

* fix: add update proj multiselect guard

* fix: cargo fmt

* fix: content missing fields

* feat: break up app-lib/api/instance.rs

* fix: check_content_updates mismatch

* fix: updater modal cleanup w/new structure

* feat: better update all handling

* fix: remove preview_update_all

* fix: feedback on bulk update + lint

* fix: rem transitions

* fix: change to jsonb

* feat: app db backup after update

* fix: lint

* fix: sqlx prepare + use sqlx macros

* fix: lint

* fix: bugs

* feat: defuck the installing process up

* fix: bug of hell

* fix: shear

* fix: fmt

* fix: install progress spacing + change mc/content/overrides to bytes

* fix: lint

* fix: prepr

* fix: navtabs anim not working in app

* fix: worlds.vue improvements + browse page fixes

* feat: optimise queries + adapter fns

* fix: lint

* fix: lint

* feat: shared modrinth-content-management crate (#6469)

* feat: disable warnings setting

* feat: add instances shortcuts (#6329)

* Add modrinth://launch deep link to start a profile

Support external profile launching via modrinth://launch/{profile_path} for integrations such as Stream Deck.

* Change route to /launch/profile/{id} for future extensibility

* fix: ensure profile path is url decoded

* fix: URL-decode profile path from deep link

* fix: use urlencoding crate for URL decoding

* feat: implement app instance shortcuts

* feat: change windows shortcut creation to use windows api instead

* feat: implement creating a shortcut launching world/server

* format

* fmt

* fix multiline inline tables

* pnpm prepr

* feat: move create shortcut to last item

* refactor: split up shortcuts.rs for individual platforms

* refactor: turn profile launch url into url type

* use string literal and add safety comment

* pt2

* refactor: rename anything that's profile into instance

* update mac shortcut

---------

Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com>

---------

Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com>
This commit is contained in:
Calum H.
2026-06-25 21:19:29 +00:00
committed by GitHub
co-authored by DJCheesusReal Truman Gao
parent ef4044534f
commit 734720e11e
353 changed files with 24745 additions and 9771 deletions
+5 -5
View File
@@ -4,7 +4,7 @@ use serde::Serialize;
use std::io::Cursor;
use tauri::Runtime;
use tauri_plugin_dialog::DialogExt;
use theseus::profile::get_full_path;
use theseus::instance::get_full_path;
pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("files")
@@ -37,12 +37,12 @@ pub async fn file_read_dragged_file(path: String) -> Result<Vec<u8>> {
#[tauri::command]
pub async fn file_extract_zip(
instance_path: &str,
instance_id: &str,
file_path: &str,
override_conflicts: bool,
dry_run: bool,
) -> Result<Option<ExtractDryRunResult>> {
let base = get_full_path(instance_path).await?;
let base = get_full_path(instance_id).await?;
let zip_path = base.join(file_path);
let canonical_zip = tokio::fs::canonicalize(&zip_path).await?;
let canonical_base = tokio::fs::canonicalize(&base).await?;
@@ -146,10 +146,10 @@ pub async fn file_extract_zip(
#[tauri::command]
pub async fn file_save_as<R: Runtime>(
app: tauri::AppHandle<R>,
instance_path: &str,
instance_id: &str,
file_path: &str,
) -> Result<()> {
let base = get_full_path(instance_path).await?;
let base = get_full_path(instance_id).await?;
let source = base.join(file_path);
let file_name = source
.file_name()
-21
View File
@@ -9,7 +9,6 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("import")
.invoke_handler(tauri::generate_handler![
get_importable_instances,
import_instance,
is_valid_importable_instance,
get_default_launcher_path,
])
@@ -27,26 +26,6 @@ pub async fn get_importable_instances(
Ok(import::get_importable_instances(launcher_type, base_path).await?)
}
/// Import an instance from a launcher type and base path
/// profile_path should be a blank profile for this purpose- if the function fails, it will be deleted
/// eg: import_instance(ImportLauncherType::MultiMC, PathBuf::from("C:/MultiMC"), "Instance 1")
#[tauri::command]
pub async fn import_instance(
profile_path: &str,
launcher_type: ImportLauncherType,
base_path: PathBuf,
instance_folder: String,
) -> Result<()> {
import::import_instance(
profile_path,
launcher_type,
base_path,
instance_folder,
)
.await?;
Ok(())
}
/// Checks if this instance is valid for importing, given a certain launcher type
/// eg: is_valid_importable_instance(PathBuf::from("C:/MultiMC/Instance 1"), ImportLauncherType::MultiMC)
#[tauri::command]
+171
View File
@@ -0,0 +1,171 @@
use crate::api::Result;
use crate::api::instance::InstanceLink;
use serde::Deserialize;
use std::path::PathBuf;
use theseus::data::ModLoader;
use theseus::install::{
InstallJobSnapshot, InstallModpackPreview, InstallPostInstallEdit,
};
use theseus::pack::import::ImportLauncherType;
use theseus::pack::install_from::CreatePackLocation;
use uuid::Uuid;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("install")
.invoke_handler(tauri::generate_handler![
install_get_modpack_preview,
install_create_instance,
install_create_modpack_instance,
install_import_instance,
install_duplicate_instance,
install_existing_instance,
install_pack_to_existing_instance,
install_job_list,
install_job_get,
install_job_retry,
install_job_cancel,
install_job_dismiss,
])
.build()
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstallCreateInstanceRequest {
pub name: String,
pub game_version: String,
pub loader: ModLoader,
pub loader_version: Option<String>,
pub icon_path: Option<String>,
pub link: Option<InstanceLink>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstallPostInstallEditRequest {
pub name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub icon_path: Option<Option<String>>,
pub link: Option<InstanceLink>,
}
impl InstallPostInstallEditRequest {
fn into_core(self) -> Result<InstallPostInstallEdit> {
Ok(InstallPostInstallEdit {
name: self.name,
icon_path: self.icon_path,
link: self.link.map(|link| link.into_core()).transpose()?,
})
}
}
#[tauri::command]
pub async fn install_get_modpack_preview(
location: CreatePackLocation,
) -> Result<InstallModpackPreview> {
Ok(theseus::pack::install_from::get_instance_from_pack(location).await?)
}
#[tauri::command]
pub async fn install_create_instance(
request: InstallCreateInstanceRequest,
) -> Result<InstallJobSnapshot> {
Ok(theseus::install::create_instance(
request.name.trim().to_string(),
request.game_version,
request.loader,
request.loader_version,
request.icon_path,
match request.link {
Some(link) => link.into_core()?,
None => theseus::data::InstanceLink::Unmanaged,
},
)
.await?)
}
#[tauri::command]
pub async fn install_create_modpack_instance(
location: CreatePackLocation,
post_install_edit: Option<InstallPostInstallEditRequest>,
) -> Result<InstallJobSnapshot> {
Ok(theseus::install::create_modpack_instance(
location,
post_install_edit.map(|edit| edit.into_core()).transpose()?,
)
.await?)
}
#[tauri::command]
pub async fn install_import_instance(
launcher_type: ImportLauncherType,
base_path: PathBuf,
instance_folder: String,
) -> Result<InstallJobSnapshot> {
Ok(theseus::install::import_instance(
launcher_type,
base_path,
instance_folder,
)
.await?)
}
#[tauri::command]
pub async fn install_duplicate_instance(
source_instance_id: String,
) -> Result<InstallJobSnapshot> {
Ok(theseus::install::duplicate_instance(source_instance_id).await?)
}
#[tauri::command]
pub async fn install_existing_instance(
instance_id: String,
force: bool,
) -> Result<InstallJobSnapshot> {
Ok(theseus::install::install_existing_instance(instance_id, force).await?)
}
#[tauri::command]
pub async fn install_pack_to_existing_instance(
instance_id: String,
location: CreatePackLocation,
post_install_edit: Option<InstallPostInstallEditRequest>,
) -> Result<InstallJobSnapshot> {
Ok(theseus::install::install_pack_to_existing_instance(
instance_id,
location,
post_install_edit.map(|edit| edit.into_core()).transpose()?,
)
.await?)
}
#[tauri::command]
pub async fn install_job_list(
include_finished: bool,
) -> Result<Vec<InstallJobSnapshot>> {
Ok(theseus::install::list_jobs(include_finished).await?)
}
#[tauri::command]
pub async fn install_job_get(job_id: Uuid) -> Result<InstallJobSnapshot> {
Ok(theseus::install::get_job(job_id).await?)
}
#[tauri::command]
pub async fn install_job_retry(job_id: Uuid) -> Result<InstallJobSnapshot> {
Ok(theseus::install::retry_job(job_id).await?)
}
#[tauri::command]
pub async fn install_job_cancel(job_id: Uuid) -> Result<InstallJobSnapshot> {
Ok(theseus::install::cancel_job(job_id).await?)
}
#[tauri::command]
pub async fn install_job_dismiss(job_id: Uuid) -> Result<()> {
Ok(theseus::install::dismiss_job(job_id).await?)
}
+740
View File
@@ -0,0 +1,740 @@
use crate::api::Result;
use dashmap::DashMap;
use path_util::SafeRelativeUtf8UnixPathBuf;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use theseus::DownloadReason;
use theseus::data::{
AppliedContentSetPatch, ContentItem, Dependency,
EditInstance as CoreEditInstance, InstanceInstallCandidate,
InstanceInstallTarget, InstanceLaunchOverridesPatch,
InstanceLink as CoreInstanceLink, InstanceMetadata, LinkedModpackInfo,
};
use theseus::instance::InstallProjectWithDependenciesRequest;
use theseus::instance::QuickPlayType;
use theseus::prelude::*;
use theseus::server_address::ServerAddress;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("instance")
.invoke_handler(tauri::generate_handler![
instance_remove,
instance_get,
instance_get_many,
instance_list,
instance_get_projects,
instance_get_installed_project_ids,
instance_get_install_candidates,
instance_content,
instance_get_content_items,
instance_get_dependencies_as_content_items,
instance_get_linked_modpack_info,
instance_get_linked_modpack_content,
instance_get_optimal_jre_key,
instance_get_full_path,
instance_get_mod_full_path,
instance_check_installed,
instance_update_all,
instance_update_project,
instance_add_project_from_version,
instance_install_project_with_dependencies,
instance_switch_project_version_with_dependencies,
instance_add_project_from_path,
instance_toggle_disable_project,
instance_remove_project,
instance_update_managed_modrinth_version,
instance_repair_managed_modrinth,
instance_run,
instance_kill,
instance_edit,
instance_edit_icon,
instance_export_mrpack,
instance_get_pack_export_candidates,
])
.build()
}
#[derive(Serialize, Debug, Clone)]
pub struct Instance {
pub id: String,
pub path: String,
pub install_stage: String,
pub launcher_feature_version: String,
pub name: String,
pub icon_path: Option<String>,
pub game_version: String,
pub protocol_version: Option<u32>,
pub loader: ModLoader,
pub loader_version: Option<String>,
pub groups: Vec<String>,
pub link: Option<InstanceLink>,
pub update_channel: ReleaseChannel,
pub created: chrono::DateTime<chrono::Utc>,
pub modified: chrono::DateTime<chrono::Utc>,
pub last_played: Option<chrono::DateTime<chrono::Utc>>,
pub submitted_time_played: u64,
pub recent_time_played: u64,
pub java_path: Option<String>,
pub extra_launch_args: Option<Vec<String>>,
pub custom_env_vars: Option<Vec<(String, String)>>,
pub memory: Option<MemorySettings>,
pub force_fullscreen: Option<bool>,
pub game_resolution: Option<WindowSize>,
pub hooks: Hooks,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InstanceLink {
ModrinthModpack {
project_id: String,
version_id: String,
},
ServerProject {
project_id: String,
},
ServerProjectModpack {
server_project_id: String,
content_project_id: Option<String>,
content_version_id: String,
project_id: Option<String>,
version_id: Option<String>,
},
ImportedModpack {
project_id: Option<String>,
version_id: Option<String>,
name: Option<String>,
version_number: Option<String>,
filename: Option<String>,
},
ModrinthHosting {
server_id: String,
instance_ids: Vec<String>,
active_instance_id: Option<String>,
},
SharedInstance {
shared_instance_id: String,
},
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct EditInstance {
pub name: Option<String>,
pub game_version: Option<String>,
pub loader: Option<ModLoader>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub loader_version: Option<Option<String>>,
pub groups: Option<Vec<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub link: Option<Option<InstanceLink>>,
pub update_channel: Option<ReleaseChannel>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub java_path: Option<Option<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub extra_launch_args: Option<Option<Vec<String>>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub custom_env_vars: Option<Option<Vec<(String, String)>>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub memory: Option<Option<MemorySettings>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub force_fullscreen: Option<Option<bool>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub game_resolution: Option<Option<WindowSize>>,
pub hooks: Option<Hooks>,
}
impl From<InstanceMetadata> for Instance {
fn from(metadata: InstanceMetadata) -> Self {
Self {
id: metadata.instance.id,
path: metadata.instance.path,
install_stage: metadata.instance.install_stage.as_str().to_string(),
launcher_feature_version: metadata
.instance
.launcher_feature_version
.as_str()
.to_string(),
name: metadata.instance.name,
icon_path: metadata.instance.icon_path,
game_version: metadata.applied_content_set.game_version,
protocol_version: metadata.applied_content_set.protocol_version,
loader: metadata.applied_content_set.loader,
loader_version: metadata.applied_content_set.loader_version,
groups: metadata.groups,
link: InstanceLink::from_core(metadata.link),
update_channel: metadata.instance.update_channel,
created: metadata.instance.created,
modified: metadata.instance.modified,
last_played: metadata.instance.last_played,
submitted_time_played: metadata.instance.submitted_time_played,
recent_time_played: metadata.instance.recent_time_played,
java_path: metadata.launch_overrides.java_path,
extra_launch_args: metadata.launch_overrides.extra_launch_args,
custom_env_vars: metadata.launch_overrides.custom_env_vars,
memory: metadata.launch_overrides.memory,
force_fullscreen: metadata.launch_overrides.force_fullscreen,
game_resolution: metadata.launch_overrides.game_resolution,
hooks: metadata.launch_overrides.hooks,
}
}
}
impl InstanceLink {
fn from_core(link: CoreInstanceLink) -> Option<Self> {
match link {
CoreInstanceLink::Unmanaged => None,
CoreInstanceLink::ModrinthModpack {
project_id,
version_id,
} => Some(Self::ModrinthModpack {
project_id,
version_id,
}),
CoreInstanceLink::ServerProject { project_id } => {
Some(Self::ServerProject { project_id })
}
CoreInstanceLink::ServerProjectModpack {
server_project_id,
content_project_id,
content_version_id,
} => Some(Self::ServerProjectModpack {
project_id: Some(server_project_id.clone()),
version_id: Some(content_version_id.clone()),
server_project_id,
content_project_id: Some(content_project_id),
content_version_id,
}),
CoreInstanceLink::ImportedModpack {
project_id,
version_id,
name,
version_number,
filename,
} => Some(Self::ImportedModpack {
project_id,
version_id,
name,
version_number,
filename,
}),
CoreInstanceLink::ModrinthHosting {
server_id,
instance_ids,
active_instance_id,
} => Some(Self::ModrinthHosting {
server_id: server_id.to_string(),
instance_ids: instance_ids
.into_iter()
.map(|id| id.to_string())
.collect(),
active_instance_id: active_instance_id.map(|id| id.to_string()),
}),
CoreInstanceLink::SharedInstance { shared_instance_id } => {
Some(Self::SharedInstance {
shared_instance_id: shared_instance_id.to_string(),
})
}
}
}
pub(crate) fn into_core(self) -> Result<CoreInstanceLink> {
match self {
Self::ModrinthModpack {
project_id,
version_id,
} => Ok(CoreInstanceLink::ModrinthModpack {
project_id,
version_id,
}),
Self::ServerProject { project_id } => {
Ok(CoreInstanceLink::ServerProject { project_id })
}
Self::ServerProjectModpack {
server_project_id,
content_project_id,
content_version_id,
..
} => Ok(CoreInstanceLink::ServerProjectModpack {
server_project_id,
content_project_id: content_project_id.unwrap_or_default(),
content_version_id,
}),
Self::ImportedModpack {
project_id,
version_id,
name,
version_number,
filename,
} => Ok(CoreInstanceLink::ImportedModpack {
project_id,
version_id,
name,
version_number,
filename,
}),
Self::ModrinthHosting {
server_id,
instance_ids,
active_instance_id,
} => Ok(CoreInstanceLink::ModrinthHosting {
server_id: server_id.parse().map_err(|err| {
theseus::Error::from(theseus::ErrorKind::InputError(
format!("Invalid server id: {err}"),
))
})?,
instance_ids: instance_ids
.into_iter()
.map(|id| {
id.parse().map_err(|err| {
theseus::Error::from(
theseus::ErrorKind::InputError(format!(
"Invalid hosted instance id: {err}"
)),
)
})
})
.collect::<std::result::Result<Vec<_>, _>>()?,
active_instance_id: active_instance_id
.map(|id| {
id.parse().map_err(|err| {
theseus::Error::from(
theseus::ErrorKind::InputError(format!(
"Invalid active instance id: {err}"
)),
)
})
})
.transpose()?,
}),
Self::SharedInstance { shared_instance_id } => {
Ok(CoreInstanceLink::SharedInstance {
shared_instance_id: shared_instance_id.parse().map_err(
|err| {
theseus::Error::from(
theseus::ErrorKind::InputError(format!(
"Invalid shared instance id: {err}"
)),
)
},
)?,
})
}
}
}
}
fn edit_to_core(edit_instance: EditInstance) -> Result<CoreEditInstance> {
Ok(CoreEditInstance {
install_stage: None,
launcher_feature_version: None,
name: edit_instance.name,
icon_path: None,
update_channel: edit_instance.update_channel,
groups: edit_instance.groups,
link: edit_instance
.link
.map(|link| match link {
Some(link) => link.into_core(),
None => Ok(CoreInstanceLink::Unmanaged),
})
.transpose()?,
launch_overrides: Some(InstanceLaunchOverridesPatch {
java_path: edit_instance.java_path,
extra_launch_args: edit_instance.extra_launch_args,
custom_env_vars: edit_instance.custom_env_vars,
memory: edit_instance.memory,
force_fullscreen: edit_instance.force_fullscreen,
game_resolution: edit_instance.game_resolution,
hooks: edit_instance.hooks,
}),
content_set_patch: Some(AppliedContentSetPatch {
source_kind: None,
game_version: edit_instance.game_version,
protocol_version: Some(None),
loader: edit_instance.loader,
loader_version: edit_instance.loader_version,
}),
last_played: None,
submitted_time_played: None,
recent_time_played: None,
})
}
#[tauri::command]
pub async fn instance_remove(instance_id: &str) -> Result<()> {
theseus::instance::remove(instance_id).await?;
Ok(())
}
#[tauri::command]
pub async fn instance_get(instance_id: &str) -> Result<Option<Instance>> {
Ok(theseus::instance::get(instance_id)
.await?
.map(Instance::from))
}
#[tauri::command]
pub async fn instance_get_many(
instance_ids: Vec<String>,
) -> Result<Vec<Instance>> {
let ids = instance_ids.iter().map(|x| &**x).collect::<Vec<&str>>();
Ok(theseus::instance::get_many(&ids)
.await?
.into_iter()
.map(Instance::from)
.collect())
}
#[tauri::command]
pub async fn instance_list() -> Result<Vec<Instance>> {
Ok(theseus::instance::list()
.await?
.into_iter()
.map(Instance::from)
.collect())
}
#[tauri::command]
pub async fn instance_get_projects(
instance_id: &str,
cache_behaviour: Option<CacheBehaviour>,
) -> Result<DashMap<String, ContentFile>> {
Ok(theseus::instance::get_projects(instance_id, cache_behaviour).await?)
}
#[tauri::command]
pub async fn instance_get_installed_project_ids(
instance_id: &str,
) -> Result<Vec<String>> {
Ok(theseus::instance::get_installed_project_ids(instance_id).await?)
}
#[tauri::command]
pub async fn instance_get_install_candidates(
project_id: &str,
project_type: ProjectType,
targets: Vec<InstanceInstallTarget>,
) -> Result<Vec<InstanceInstallCandidate>> {
Ok(theseus::instance::get_install_candidates(
project_id,
project_type,
targets,
)
.await?)
}
#[tauri::command]
pub async fn instance_content(
instance_id: &str,
cache_behaviour: Option<CacheBehaviour>,
) -> Result<Vec<ContentItem>> {
instance_get_content_items(instance_id, cache_behaviour).await
}
#[tauri::command]
pub async fn instance_get_content_items(
instance_id: &str,
cache_behaviour: Option<CacheBehaviour>,
) -> Result<Vec<ContentItem>> {
Ok(
theseus::instance::get_content_items(instance_id, cache_behaviour)
.await?,
)
}
#[tauri::command]
pub async fn instance_get_dependencies_as_content_items(
dependencies: Vec<Dependency>,
cache_behaviour: Option<CacheBehaviour>,
) -> Result<Vec<ContentItem>> {
Ok(theseus::instance::get_dependencies_as_content_items(
dependencies,
cache_behaviour,
)
.await?)
}
#[tauri::command]
pub async fn instance_get_linked_modpack_info(
instance_id: &str,
cache_behaviour: Option<CacheBehaviour>,
) -> Result<Option<LinkedModpackInfo>> {
Ok(
theseus::instance::get_linked_modpack_info(
instance_id,
cache_behaviour,
)
.await?,
)
}
#[tauri::command]
pub async fn instance_get_linked_modpack_content(
instance_id: &str,
cache_behaviour: Option<CacheBehaviour>,
) -> Result<Vec<ContentItem>> {
Ok(theseus::instance::get_linked_modpack_content(
instance_id,
cache_behaviour,
)
.await?)
}
#[tauri::command]
pub async fn instance_get_full_path(instance_id: &str) -> Result<PathBuf> {
Ok(theseus::instance::get_full_path(instance_id).await?)
}
#[tauri::command]
pub async fn instance_get_mod_full_path(
instance_id: &str,
project_path: &str,
) -> Result<PathBuf> {
Ok(theseus::instance::get_mod_full_path(instance_id, project_path).await?)
}
#[tauri::command]
pub async fn instance_get_optimal_jre_key(
instance_id: &str,
) -> Result<Option<JavaVersion>> {
Ok(theseus::instance::get_optimal_jre_key(instance_id).await?)
}
#[tauri::command]
pub async fn instance_check_installed(
instance_id: &str,
project_id: &str,
) -> Result<bool> {
let check_project_id = project_id;
if let Ok(projects) =
theseus::instance::get_projects(instance_id, None).await
{
Ok(projects.into_iter().any(|(_, project)| {
project
.metadata
.as_ref()
.is_some_and(|metadata| check_project_id == metadata.project_id)
}))
} else {
Ok(false)
}
}
#[tauri::command]
pub async fn instance_update_all(
instance_id: &str,
) -> Result<HashMap<String, String>> {
Ok(theseus::instance::update_all_projects(instance_id).await?)
}
#[tauri::command]
pub async fn instance_update_project(
instance_id: &str,
project_path: &str,
) -> Result<String> {
Ok(
theseus::instance::update_project(instance_id, project_path, None)
.await?,
)
}
#[tauri::command]
pub async fn instance_add_project_from_version(
instance_id: &str,
version_id: &str,
reason: DownloadReason,
dependent_on_version_id: Option<String>,
) -> Result<String> {
Ok(theseus::instance::add_project_from_version(
instance_id,
version_id,
reason,
dependent_on_version_id,
)
.await?)
}
#[tauri::command]
pub async fn instance_install_project_with_dependencies(
instance_id: &str,
request: InstallProjectWithDependenciesRequest,
) -> Result<ResolveContentPlan> {
Ok(theseus::instance::install_project_with_dependencies(
instance_id,
request,
)
.await?)
}
#[tauri::command]
pub async fn instance_switch_project_version_with_dependencies(
instance_id: &str,
project_path: &str,
version_id: &str,
) -> Result<String> {
Ok(theseus::instance::switch_project_version_with_dependencies(
instance_id,
project_path,
version_id,
)
.await?)
}
#[tauri::command]
pub async fn instance_add_project_from_path(
instance_id: &str,
project_path: &Path,
project_type: Option<ProjectType>,
) -> Result<String> {
Ok(theseus::instance::add_project_from_path(
instance_id,
project_path,
project_type,
)
.await?)
}
#[tauri::command]
pub async fn instance_toggle_disable_project(
instance_id: &str,
project_path: &str,
desired_enabled: Option<bool>,
) -> Result<String> {
Ok(theseus::instance::toggle_disable_project(
instance_id,
project_path,
desired_enabled,
)
.await?)
}
#[tauri::command]
pub async fn instance_remove_project(
instance_id: &str,
project_path: &str,
) -> Result<()> {
theseus::instance::remove_project(instance_id, project_path).await?;
Ok(())
}
#[tauri::command]
pub async fn instance_update_managed_modrinth_version(
instance_id: String,
version_id: String,
) -> Result<theseus::install::InstallJobSnapshot> {
Ok(theseus::instance::update_managed_modrinth_version(
&instance_id,
&version_id,
)
.await?)
}
#[tauri::command]
pub async fn instance_repair_managed_modrinth(
instance_id: &str,
) -> Result<theseus::install::InstallJobSnapshot> {
Ok(theseus::instance::repair_managed_modrinth(instance_id).await?)
}
#[tauri::command]
pub async fn instance_export_mrpack(
instance_id: &str,
export_location: PathBuf,
included_overrides: Vec<String>,
version_id: Option<String>,
description: Option<String>,
name: Option<String>,
) -> Result<()> {
theseus::instance::export_mrpack(
instance_id,
export_location,
included_overrides,
version_id,
description,
name,
)
.await?;
Ok(())
}
#[tauri::command]
pub async fn instance_get_pack_export_candidates(
instance_id: &str,
) -> Result<Vec<SafeRelativeUtf8UnixPathBuf>> {
Ok(theseus::instance::get_pack_export_candidates(instance_id).await?)
}
#[tauri::command]
pub async fn instance_run(
instance_id: &str,
server_address: Option<String>,
) -> Result<ProcessMetadata> {
let quick_play = match server_address {
Some(addr) => QuickPlayType::Server(ServerAddress::Unresolved(addr)),
None => QuickPlayType::None,
};
Ok(theseus::instance::run(instance_id, quick_play).await?)
}
#[tauri::command]
pub async fn instance_kill(instance_id: &str) -> Result<()> {
theseus::instance::kill(instance_id).await?;
Ok(())
}
#[tauri::command]
pub async fn instance_edit(
instance_id: &str,
edit_instance: EditInstance,
) -> Result<()> {
theseus::instance::edit(instance_id, edit_to_core(edit_instance)?).await?;
Ok(())
}
#[tauri::command]
pub async fn instance_edit_icon(
instance_id: &str,
icon_path: Option<&Path>,
) -> Result<()> {
theseus::instance::edit_icon(instance_id, icon_path).await?;
Ok(())
}
+23 -26
View File
@@ -27,76 +27,73 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
.build()
}
/// Get all Logs for a profile, sorted by filename
/// Get all logs for an instance, sorted by filename.
#[tauri::command]
pub async fn logs_get_logs(
profile_path: &str,
instance_id: &str,
clear_contents: Option<bool>,
) -> Result<Vec<Logs>> {
let val = logs::get_logs(profile_path, clear_contents).await?;
let val = logs::get_logs(instance_id, clear_contents).await?;
Ok(val)
}
/// Get a Log struct for a profile by profile id and filename string
/// Get a log struct for an instance by filename.
#[tauri::command]
pub async fn logs_get_logs_by_filename(
profile_path: &str,
instance_id: &str,
log_type: LogType,
filename: String,
) -> Result<Logs> {
Ok(logs::get_logs_by_filename(profile_path, log_type, filename).await?)
Ok(logs::get_logs_by_filename(instance_id, log_type, filename).await?)
}
/// Get the stdout for a profile by profile id and filename string
/// Get the output for an instance by filename.
#[tauri::command]
pub async fn logs_get_output_by_filename(
profile_path: &str,
instance_id: &str,
log_type: LogType,
filename: String,
) -> Result<CensoredString> {
Ok(logs::get_output_by_filename(profile_path, log_type, &filename).await?)
Ok(logs::get_output_by_filename(instance_id, log_type, &filename).await?)
}
/// Delete all logs for a profile by profile id
/// Delete all logs for an instance.
#[tauri::command]
pub async fn logs_delete_logs(profile_path: &str) -> Result<()> {
Ok(logs::delete_logs(profile_path).await?)
pub async fn logs_delete_logs(instance_id: &str) -> Result<()> {
Ok(logs::delete_logs(instance_id).await?)
}
/// Delete a log for a profile by profile id and filename string
/// Delete a log for an instance by filename.
#[tauri::command]
pub async fn logs_delete_logs_by_filename(
profile_path: &str,
instance_id: &str,
log_type: LogType,
filename: String,
) -> Result<()> {
Ok(
logs::delete_logs_by_filename(profile_path, log_type, &filename)
.await?,
)
Ok(logs::delete_logs_by_filename(instance_id, log_type, &filename).await?)
}
/// Get live log from a cursor
#[tauri::command]
pub async fn logs_get_latest_log_cursor(
profile_path: &str,
instance_id: &str,
cursor: u64, // 0 to start at beginning of file
) -> Result<LatestLogCursor> {
Ok(logs::get_latest_log_cursor(profile_path, cursor).await?)
Ok(logs::get_latest_log_cursor(instance_id, cursor).await?)
}
/// Get all buffered live log lines for a profile
/// Get all buffered live log lines for an instance.
#[tauri::command]
pub async fn logs_get_live_log_buffer(
profile_path: &str,
instance_id: &str,
) -> Result<CensoredString> {
Ok(logs::get_live_log_buffer(profile_path).await?)
Ok(logs::get_live_log_buffer(instance_id).await?)
}
/// Clear the live log buffer for a profile
/// Clear the live log buffer for an instance.
#[tauri::command]
pub async fn logs_clear_live_log_buffer(profile_path: &str) -> Result<()> {
logs::clear_live_log_buffer(profile_path);
pub async fn logs_clear_live_log_buffer(instance_id: &str) -> Result<()> {
logs::clear_live_log_buffer(instance_id);
Ok(())
}
+3 -3
View File
@@ -4,16 +4,16 @@ use thiserror::Error;
pub mod auth;
pub mod import;
pub mod install;
pub mod instance;
pub mod jre;
pub mod logs;
pub mod metadata;
pub mod minecraft_skins;
pub mod mr_auth;
pub mod pack;
pub mod process;
pub mod profile;
pub mod profile_create;
pub mod settings;
pub mod shortcuts;
pub mod tags;
pub mod utils;
-33
View File
@@ -1,33 +0,0 @@
use crate::api::Result;
use theseus::{
pack::{
install_from::{CreatePackLocation, CreatePackProfile},
install_mrpack::install_zipped_mrpack,
},
prelude::*,
};
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("pack")
.invoke_handler(tauri::generate_handler![
pack_install,
pack_get_profile_from_pack,
])
.build()
}
#[tauri::command]
pub async fn pack_install(
location: CreatePackLocation,
profile: String,
) -> Result<String> {
Ok(install_zipped_mrpack(location, profile).await?)
}
#[tauri::command]
pub async fn pack_get_profile_from_pack(
location: CreatePackLocation,
) -> Result<CreatePackProfile> {
Ok(pack::install_from::get_profile_from_pack(location).await?)
}
+4 -4
View File
@@ -6,7 +6,7 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("process")
.invoke_handler(tauri::generate_handler![
process_get_all,
process_get_by_profile_path,
process_get_by_instance_id,
process_kill,
process_wait_for,
])
@@ -19,10 +19,10 @@ pub async fn process_get_all() -> Result<Vec<ProcessMetadata>> {
}
#[tauri::command]
pub async fn process_get_by_profile_path(
path: &str,
pub async fn process_get_by_instance_id(
instance_id: &str,
) -> Result<Vec<ProcessMetadata>> {
Ok(process::get_by_profile_path(path).await?)
Ok(process::get_by_instance_id(instance_id).await?)
}
#[tauri::command]
-510
View File
@@ -1,510 +0,0 @@
use crate::api::Result;
use dashmap::DashMap;
use path_util::SafeRelativeUtf8UnixPathBuf;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use theseus::DownloadReason;
use theseus::data::{ContentItem, Dependency, LinkedModpackInfo};
use theseus::prelude::*;
use theseus::profile::QuickPlayType;
use theseus::server_address::ServerAddress;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("profile")
.invoke_handler(tauri::generate_handler![
profile_remove,
profile_get,
profile_get_many,
profile_get_projects,
profile_get_installed_project_ids,
profile_get_content_items,
profile_get_dependencies_as_content_items,
profile_get_linked_modpack_info,
profile_get_linked_modpack_content,
profile_get_optimal_jre_key,
profile_get_full_path,
profile_get_mod_full_path,
profile_list,
profile_check_installed,
profile_check_installed_batch,
profile_install,
profile_update_all,
profile_update_project,
profile_add_project_from_version,
profile_add_project_from_path,
profile_toggle_disable_project,
profile_remove_project,
profile_update_managed_modrinth_version,
profile_repair_managed_modrinth,
profile_run,
profile_kill,
profile_edit,
profile_edit_icon,
profile_export_mrpack,
profile_get_pack_export_candidates,
])
.build()
}
// Remove a profile
// invoke('plugin:profile|profile_add_path',path)
#[tauri::command]
pub async fn profile_remove(path: &str) -> Result<()> {
profile::remove(path).await?;
Ok(())
}
// Get a profile by path
// invoke('plugin:profile|profile_add_path',path)
#[tauri::command]
pub async fn profile_get(path: &str) -> Result<Option<Profile>> {
let res = profile::get(path).await?;
Ok(res)
}
#[tauri::command]
pub async fn profile_get_many(paths: Vec<String>) -> Result<Vec<Profile>> {
let ids = paths.iter().map(|x| &**x).collect::<Vec<&str>>();
let entries = profile::get_many(&ids).await?;
Ok(entries)
}
#[tauri::command]
pub async fn profile_get_projects(
path: &str,
cache_behaviour: Option<CacheBehaviour>,
) -> Result<DashMap<String, ProfileFile>> {
let res = profile::get_projects(path, cache_behaviour).await?;
Ok(res)
}
#[tauri::command]
pub async fn profile_get_installed_project_ids(
path: &str,
) -> Result<Vec<String>> {
let res = profile::get_installed_project_ids(path).await?;
Ok(res)
}
/// Get content items with rich metadata for a profile
///
/// Returns content items filtered to exclude modpack files (if linked),
/// sorted alphabetically by project name.
#[tauri::command]
pub async fn profile_get_content_items(
path: &str,
cache_behaviour: Option<CacheBehaviour>,
) -> Result<Vec<ContentItem>> {
let res = profile::get_content_items(path, cache_behaviour).await?;
Ok(res)
}
/// Convert a list of dependencies into ContentItems with rich metadata
#[tauri::command]
pub async fn profile_get_dependencies_as_content_items(
dependencies: Vec<Dependency>,
cache_behaviour: Option<CacheBehaviour>,
) -> Result<Vec<ContentItem>> {
let res = profile::get_dependencies_as_content_items(
dependencies,
cache_behaviour,
)
.await?;
Ok(res)
}
/// Get linked modpack info for a profile
///
/// Returns project, version, and owner information for the linked modpack,
/// or None if the profile is not linked to a modpack.
#[tauri::command]
pub async fn profile_get_linked_modpack_info(
path: &str,
cache_behaviour: Option<CacheBehaviour>,
) -> Result<Option<LinkedModpackInfo>> {
let res = profile::get_linked_modpack_info(path, cache_behaviour).await?;
Ok(res)
}
/// Get content items that are part of the linked modpack
///
/// Returns the modpack's dependencies as ContentItem list.
/// Returns empty vec if the profile is not linked to a modpack.
#[tauri::command]
pub async fn profile_get_linked_modpack_content(
path: &str,
cache_behaviour: Option<CacheBehaviour>,
) -> Result<Vec<ContentItem>> {
let res =
profile::get_linked_modpack_content(path, cache_behaviour).await?;
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: &str) -> Result<PathBuf> {
let res = profile::get_full_path(path).await?;
Ok(res)
}
// Get's a mod's full path
// invoke('plugin:profile|profile_get_mod_full_path',path)
#[tauri::command]
pub async fn profile_get_mod_full_path(
path: &str,
project_path: &str,
) -> Result<PathBuf> {
let res = profile::get_mod_full_path(path, project_path).await?;
Ok(res)
}
// Get optimal java version from profile
#[tauri::command]
pub async fn profile_get_optimal_jre_key(
path: &str,
) -> Result<Option<JavaVersion>> {
let res = profile::get_optimal_jre_key(path).await?;
Ok(res)
}
// Get a copy of the profile set
// invoke('plugin:profile|profile_list')
#[tauri::command]
pub async fn profile_list() -> Result<Vec<Profile>> {
let res = profile::list().await?;
Ok(res)
}
#[tauri::command]
pub async fn profile_check_installed(
path: &str,
project_id: &str,
) -> Result<bool> {
let check_project_id = project_id;
if let Ok(projects) = profile::get_projects(path, None).await {
Ok(projects.into_iter().any(|(_, project)| {
if let Some(metadata) = &project.metadata {
check_project_id == metadata.project_id
} else {
false
}
}))
} else {
Ok(false)
}
}
#[tauri::command]
pub async fn profile_check_installed_batch(
project_id: &str,
) -> Result<HashMap<String, bool>> {
let profiles = profile::list().await?;
let mut result = HashMap::new();
for p in profiles {
let installed =
if let Ok(projects) = profile::get_projects(&p.path, None).await {
projects.into_iter().any(|(_, pf)| {
pf.metadata
.as_ref()
.is_some_and(|m| m.project_id == project_id)
})
} else {
false
};
result.insert(p.path.clone(), installed);
}
Ok(result)
}
/// Installs/Repairs a profile
/// invoke('plugin:profile|profile_install')
#[tauri::command]
pub async fn profile_install(path: &str, force: bool) -> Result<()> {
profile::install(path, force).await?;
Ok(())
}
/// Updates all of the profile's projects
/// invoke('plugin:profile|profile_update_all')
#[tauri::command]
pub async fn profile_update_all(path: &str) -> Result<HashMap<String, String>> {
Ok(profile::update_all_projects(path).await?)
}
/// Updates a specified project
/// invoke('plugin:profile|profile_update_project')
#[tauri::command]
pub async fn profile_update_project(
path: &str,
project_path: &str,
) -> Result<String> {
Ok(profile::update_project(path, project_path, None).await?)
}
// Adds a project to a profile from a version ID
// invoke('plugin:profile|profile_add_project_from_version')
#[tauri::command]
pub async fn profile_add_project_from_version(
path: &str,
version_id: &str,
reason: DownloadReason,
dependent_on_version_id: Option<String>,
) -> Result<String> {
Ok(profile::add_project_from_version(
path,
version_id,
reason,
dependent_on_version_id,
)
.await?)
}
// Adds a project to a profile from a path
// invoke('plugin:profile|profile_add_project_from_path')
#[tauri::command]
pub async fn profile_add_project_from_path(
path: &str,
project_path: &Path,
project_type: Option<ProjectType>,
) -> Result<String> {
let res = profile::add_project_from_path(path, project_path, project_type)
.await?;
Ok(res)
}
// Toggles disabling a project from its path
// invoke('plugin:profile|profile_toggle_disable_project')
#[tauri::command]
pub async fn profile_toggle_disable_project(
path: &str,
project_path: &str,
) -> Result<String> {
Ok(profile::toggle_disable_project(path, project_path).await?)
}
// Removes a project from a profile
// invoke('plugin:profile|profile_remove_project')
#[tauri::command]
pub async fn profile_remove_project(
path: &str,
project_path: &str,
) -> Result<()> {
profile::remove_project(path, project_path).await?;
Ok(())
}
// Updates a managed Modrinth profile to a version of version_id
#[tauri::command]
pub async fn profile_update_managed_modrinth_version(
path: String,
version_id: String,
) -> Result<()> {
Ok(
profile::update::update_managed_modrinth_version(&path, &version_id)
.await?,
)
}
// Repairs a managed Modrinth profile by updating it to the current version
#[tauri::command]
pub async fn profile_repair_managed_modrinth(path: &str) -> Result<()> {
Ok(profile::update::repair_managed_modrinth(path).await?)
}
// Exports a profile to a .mrpack file (export_location should end in .mrpack)
// invoke('profile_export_mrpack')
#[tauri::command]
pub async fn profile_export_mrpack(
path: &str,
export_location: PathBuf,
included_overrides: Vec<String>,
version_id: Option<String>,
description: Option<String>,
name: Option<String>, // only used to cache
) -> Result<()> {
profile::export_mrpack(
path,
export_location,
included_overrides,
version_id,
description,
name,
)
.await?;
Ok(())
}
/// See [`profile::get_pack_export_candidates`]
#[tauri::command]
pub async fn profile_get_pack_export_candidates(
profile_path: &str,
) -> Result<Vec<SafeRelativeUtf8UnixPathBuf>> {
let candidates = profile::get_pack_export_candidates(profile_path).await?;
Ok(candidates)
}
// Run minecraft using a profile using the default credentials
// Returns the UUID, which can be used to poll
// for the actual Child in the state.
// invoke('plugin:profile|profile_run', path)
#[tauri::command]
pub async fn profile_run(
path: &str,
server_address: Option<String>,
) -> Result<ProcessMetadata> {
let quick_play = match server_address {
Some(addr) => QuickPlayType::Server(ServerAddress::Unresolved(addr)),
None => QuickPlayType::None,
};
let process = profile::run(path, quick_play).await?;
Ok(process)
}
#[tauri::command]
pub async fn profile_kill(path: &str) -> Result<()> {
profile::kill(path).await?;
Ok(())
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct EditProfile {
pub name: Option<String>,
pub game_version: Option<String>,
pub loader: Option<ModLoader>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub loader_version: Option<Option<String>>,
pub groups: Option<Vec<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub linked_data: Option<Option<LinkedData>>,
pub preferred_update_channel: Option<ReleaseChannel>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub java_path: Option<Option<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub extra_launch_args: Option<Option<Vec<String>>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub custom_env_vars: Option<Option<Vec<(String, String)>>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub memory: Option<Option<MemorySettings>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub force_fullscreen: Option<Option<bool>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_with::rust::double_option"
)]
pub game_resolution: Option<Option<WindowSize>>,
pub hooks: Option<Hooks>,
}
// Edits a profile
// invoke('plugin:profile|profile_edit', {path, editProfile})
#[tauri::command]
pub async fn profile_edit(path: &str, edit_profile: EditProfile) -> Result<()> {
profile::edit(path, |prof| {
if let Some(name) = edit_profile.name.clone() {
prof.name = name;
}
if let Some(game_version) = edit_profile.game_version.clone() {
if game_version != prof.game_version {
prof.protocol_version = None;
}
prof.game_version = game_version;
}
if let Some(loader) = edit_profile.loader {
prof.loader = loader;
}
if let Some(loader_version) = edit_profile.loader_version.clone() {
prof.loader_version = loader_version;
}
if let Some(linked_data) = edit_profile.linked_data.clone() {
prof.linked_data = linked_data;
}
if let Some(preferred_update_channel) =
edit_profile.preferred_update_channel
{
prof.preferred_update_channel = preferred_update_channel;
}
if let Some(groups) = edit_profile.groups.clone() {
prof.groups = groups;
}
if let Some(java_path) = edit_profile.java_path.clone() {
prof.java_path = java_path;
}
if let Some(memory) = edit_profile.memory {
prof.memory = memory;
}
if let Some(game_resolution) = edit_profile.game_resolution {
prof.game_resolution = game_resolution;
}
if let Some(force_fullscreen) = edit_profile.force_fullscreen {
prof.force_fullscreen = force_fullscreen;
}
if let Some(hooks) = edit_profile.hooks.clone() {
prof.hooks = hooks;
}
prof.modified = chrono::Utc::now();
if let Some(custom_env_vars) = edit_profile.custom_env_vars.clone() {
prof.custom_env_vars = custom_env_vars;
}
if let Some(extra_launch_args) = edit_profile.extra_launch_args.clone()
{
prof.extra_launch_args = extra_launch_args;
}
async { Ok(()) }
})
.await?;
Ok(())
}
// Edits a profile's icon
// invoke('plugin:profile|profile_edit_icon')
#[tauri::command]
pub async fn profile_edit_icon(
path: &str,
icon_path: Option<&Path>,
) -> Result<()> {
profile::edit_icon(path, icon_path).await?;
Ok(())
}
-44
View File
@@ -1,44 +0,0 @@
use crate::api::Result;
use theseus::prelude::*;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("profile-create")
.invoke_handler(tauri::generate_handler![
profile_create,
profile_duplicate
])
.build()
}
// Creates a profile at the given filepath and adds it to the in-memory state
// invoke('plugin:profile-create|profile_add',profile)
#[tauri::command]
pub async fn profile_create(
name: String, // the name of the profile, and relative path
game_version: String, // the game version of the profile
modloader: ModLoader, // the modloader to use
loader_version: Option<String>, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
icon: Option<String>, // the icon for the profile
skip_install: Option<bool>,
linked_data: Option<LinkedData>,
) -> Result<String> {
let res = profile::create::profile_create(
name,
game_version,
modloader,
loader_version,
icon,
linked_data,
skip_install,
)
.await?;
Ok(res)
}
// Creates a profile from a duplicate
// invoke('plugin:profile-create|profile_duplicate',profile)
#[tauri::command]
pub async fn profile_duplicate(path: &str) -> Result<String> {
let res = profile::create::profile_create_from_duplicate(path).await?;
Ok(res)
}
+55
View File
@@ -0,0 +1,55 @@
use crate::api::Result;
use std::path::Path;
use url::Url;
pub(super) const SHORTCUT_EXTENSION: &str = "desktop";
pub(super) async fn create_shortcut(
profile_name: &str,
launch_url: &Url,
output_path: &Path,
) -> Result<()> {
let target_path = std::env::current_exe()?;
tokio::fs::write(
output_path,
format!(
"[Desktop Entry]\n\
Type=Application\n\
Name={}\n\
Exec={} {}\n\
Icon=ModrinthApp\n\
Terminal=false\n\
Categories=Game;\n",
escape_desktop_entry_value(&format!("Launch {profile_name}")),
quote_desktop_exec_arg(&target_path.to_string_lossy()),
quote_desktop_exec_arg(launch_url.as_str()),
),
)
.await?;
use std::os::unix::fs::PermissionsExt;
let mut permissions = tokio::fs::metadata(output_path).await?.permissions();
permissions.set_mode(0o755);
tokio::fs::set_permissions(output_path, permissions).await?;
Ok(())
}
fn escape_desktop_entry_value(input: &str) -> String {
input
.replace('\\', "\\\\")
.replace('\n', "\\n")
.replace('\r', "")
}
fn quote_desktop_exec_arg(input: &str) -> String {
format!(
"\"{}\"",
input
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('$', "\\$")
.replace('`', "\\`")
)
}
+90
View File
@@ -0,0 +1,90 @@
use crate::api::Result;
use std::{
hash::{DefaultHasher, Hash, Hasher},
path::Path,
};
use url::Url;
pub(super) const SHORTCUT_EXTENSION: &str = "app";
pub(super) async fn create_shortcut(
profile_name: &str,
launch_url: &Url,
output_path: &Path,
) -> Result<()> {
let contents_dir = output_path.join("Contents");
let macos_dir = contents_dir.join("MacOS");
let resources_dir = contents_dir.join("Resources");
tokio::fs::create_dir_all(&macos_dir).await?;
tokio::fs::create_dir_all(&resources_dir).await?;
let executable_path = macos_dir.join("launch");
tokio::fs::write(
&executable_path,
format!(
"#!/bin/sh\nexec /usr/bin/open {}\n",
shell_quote(launch_url.as_str()),
),
)
.await?;
tokio::fs::write(
resources_dir.join("icon.icns"),
include_bytes!("../../../icons/icon.icns"),
)
.await?;
tokio::fs::write(
contents_dir.join("Info.plist"),
format!(r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>launch</string>
<key>CFBundleIdentifier</key>
<string>{}</string>
<key>CFBundleIconFile</key>
<string>icon.icns</string>
<key>CFBundleName</key>
<string>{}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
</dict>
</plist>
"#,
macos_shortcut_identifier(launch_url.as_str()),
escape_xml(&format!("Launch {profile_name}")),
),
)
.await?;
use std::os::unix::fs::PermissionsExt;
let mut permissions =
tokio::fs::metadata(&executable_path).await?.permissions();
permissions.set_mode(0o755);
tokio::fs::set_permissions(&executable_path, permissions).await?;
Ok(())
}
fn macos_shortcut_identifier(launch_url: &str) -> String {
let mut hasher = DefaultHasher::new();
launch_url.hash(&mut hasher);
format!("com.modrinth.instance-shortcut.{:x}", hasher.finish())
}
fn shell_quote(input: &str) -> String {
format!("'{}'", input.replace('\'', "'\\''"))
}
fn escape_xml(input: &str) -> String {
input
.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&apos;")
}
+114
View File
@@ -0,0 +1,114 @@
use crate::api::Result;
use std::path::{Path, PathBuf};
use tauri::Runtime;
use url::Url;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "linux")]
use linux::{SHORTCUT_EXTENSION, create_shortcut};
#[cfg(target_os = "macos")]
use macos::{SHORTCUT_EXTENSION, create_shortcut};
#[cfg(target_os = "windows")]
use windows::{SHORTCUT_EXTENSION, create_shortcut};
pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("shortcuts")
.invoke_handler(tauri::generate_handler![create_instance_shortcut])
.build()
}
#[tauri::command]
pub async fn create_instance_shortcut(
instance_name: String,
instance_id: String,
output_path: PathBuf,
server: Option<String>,
singleplayer_world: Option<String>,
) -> Result<PathBuf> {
if server.is_some() && singleplayer_world.is_some() {
return Err(std::io::Error::other(
"shortcut cannot launch both a server and a singleplayer world",
)
.into());
}
let launch_url =
instance_launch_url(instance_id, server, singleplayer_world);
let output_path = shortcut_path_with_extension(output_path);
let output_path_existed =
tokio::fs::try_exists(&output_path).await.unwrap_or(false);
if let Err(error) =
create_shortcut(&instance_name, &launch_url, &output_path).await
{
cleanup_shortcut_artifact(&output_path, output_path_existed).await;
return Err(error);
}
Ok(output_path)
}
fn instance_launch_url(
instance_id: String,
server: Option<String>,
singleplayer_world: Option<String>,
) -> Url {
let mut launch_url = Url::parse("modrinth://launch/instance")
.expect("static launch URL should parse");
launch_url
.path_segments_mut()
.expect("launch URL should support path segments")
.push(&instance_id);
if let Some(server) = server {
launch_url.query_pairs_mut().append_pair("server", &server);
} else if let Some(singleplayer_world) = singleplayer_world {
launch_url
.query_pairs_mut()
.append_pair("singleplayer_world", &singleplayer_world);
}
launch_url
}
fn shortcut_path_with_extension(mut path: PathBuf) -> PathBuf {
if path
.extension()
.is_none_or(|current_extension| current_extension != SHORTCUT_EXTENSION)
{
path.set_extension(SHORTCUT_EXTENSION);
}
path
}
async fn cleanup_shortcut_artifact(path: &Path, existed: bool) {
if existed {
return;
}
let result = match tokio::fs::metadata(path).await {
Ok(metadata) if metadata.is_dir() => {
tokio::fs::remove_dir_all(path).await
}
_ => tokio::fs::remove_file(path).await,
};
if let Err(error) = result
&& error.kind() != std::io::ErrorKind::NotFound
{
tracing::warn!(
"failed to clean up shortcut artifact {}: {}",
path.display(),
error
);
}
}
+125
View File
@@ -0,0 +1,125 @@
use crate::api::Result;
use std::{
os::windows::ffi::OsStrExt,
path::{Path, PathBuf},
};
use url::Url;
use windows::{
Win32::{
System::Com::{
CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED,
COINIT_DISABLE_OLE1DDE, CoCreateInstance, CoInitializeEx,
CoUninitialize, IPersistFile,
},
UI::Shell::{IShellLinkW, ShellLink},
},
core::{Interface, PCWSTR},
};
pub(super) const SHORTCUT_EXTENSION: &str = "lnk";
pub(super) async fn create_shortcut(
_profile_name: &str,
launch_url: &Url,
output_path: &Path,
) -> Result<()> {
let target_path = std::env::current_exe()?;
let working_dir = target_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_default();
let output_path = output_path.to_path_buf();
let launch_url = launch_url.to_string();
tokio::task::spawn_blocking(move || {
create_windows_shortcut(
output_path,
target_path,
working_dir,
launch_url,
)
})
.await
.map_err(|error| {
std::io::Error::other(format!(
"failed to join shortcut creation task: {error}"
))
})??;
Ok(())
}
fn create_windows_shortcut(
output_path: PathBuf,
target_path: PathBuf,
working_dir: PathBuf,
launch_url: String,
) -> std::io::Result<()> {
let output_path = windows_wide_path(&output_path);
let target_path = windows_wide_path(&target_path);
let working_dir = windows_wide_path(&working_dir);
let launch_url = windows_wide_string(&launch_url);
// SAFETY:
// - COM is initialized for this blocking thread before any COM object is created.
// - `_com` is declared before the COM interface values, so it is dropped
// after them and calls `CoUninitialize` only once they are released.
// - Every PCWSTR points to a NUL-terminated UTF-16 buffer that lives until
// each call using it has returned.
unsafe {
let init_result = CoInitializeEx(
None,
COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE,
);
windows_result(init_result.ok())?;
let _com = WindowsComGuard;
let shortcut: IShellLinkW = windows_result(CoCreateInstance(
&ShellLink,
None,
CLSCTX_INPROC_SERVER,
))?;
windows_result(shortcut.SetPath(windows_pcwstr(&target_path)))?;
windows_result(shortcut.SetArguments(windows_pcwstr(&launch_url)))?;
windows_result(
shortcut.SetWorkingDirectory(windows_pcwstr(&working_dir)),
)?;
windows_result(
shortcut.SetIconLocation(windows_pcwstr(&target_path), 0),
)?;
let persist_file: IPersistFile = windows_result(shortcut.cast())?;
windows_result(persist_file.Save(windows_pcwstr(&output_path), true))?;
}
Ok(())
}
fn windows_result<T>(result: windows::core::Result<T>) -> std::io::Result<T> {
result.map_err(std::io::Error::other)
}
struct WindowsComGuard;
impl Drop for WindowsComGuard {
fn drop(&mut self) {
unsafe {
CoUninitialize();
}
}
}
fn windows_wide_path(path: &Path) -> Vec<u16> {
path.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
fn windows_wide_string(value: &str) -> Vec<u16> {
value.encode_utf16().chain(std::iter::once(0)).collect()
}
fn windows_pcwstr(value: &[u16]) -> PCWSTR {
PCWSTR::from_raw(value.as_ptr())
}
+19 -1
View File
@@ -3,7 +3,7 @@ use tauri::Runtime;
use tauri_plugin_opener::OpenerExt;
use theseus::{
handler,
prelude::{CommandPayload, DirectoryInfo},
prelude::{CommandPayload, DirectoryInfo, app_db_backup_dir},
};
use crate::api::{Result, TheseusSerializableError};
@@ -21,6 +21,7 @@ pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
highlight_in_folder,
open_path,
show_launcher_logs_folder,
show_app_db_backups_folder,
progress_bars_list,
get_opening_command
])
@@ -119,6 +120,16 @@ pub async fn show_launcher_logs_folder<R: Runtime>(app: tauri::AppHandle<R>) {
}
}
#[tauri::command]
pub async fn show_app_db_backups_folder<R: Runtime>(
app: tauri::AppHandle<R>,
) -> Result<()> {
let path = app_db_backup_dir()?;
tokio::fs::create_dir_all(&path).await?;
open_path(app, path).await;
Ok(())
}
// Get opening command
// For example, if a user clicks on an .mrpack to open the app.
// This should be called once and only when the app is done booting up and ready to receive a command
@@ -129,11 +140,18 @@ pub async fn get_opening_command(
state: tauri::State<'_, crate::macos::deep_link::InitialPayload>,
) -> Result<Option<CommandPayload>> {
let payload = state.payload.lock().await;
let cmd_arg = std::env::args_os()
.nth(1)
.map(|path| path.to_string_lossy().to_string());
return if let Some(payload) = payload.as_ref() {
tracing::info!("opening command {payload}");
Ok(Some(handler::parse_command(payload).await?))
} else if let Some(cmd_arg) = cmd_arg {
tracing::info!("opening command {cmd_arg:?}");
Ok(Some(handler::parse_command(&cmd_arg).await?))
} else {
Ok(None)
};
+37 -35
View File
@@ -2,30 +2,30 @@ use crate::api::Result;
use either::Either;
use enumset::EnumSet;
use tauri::{AppHandle, Manager, Runtime};
use theseus::instance::{self, QuickPlayType, get_full_path};
use theseus::prelude::ProcessMetadata;
use theseus::profile::{QuickPlayType, get_full_path};
use theseus::server_address::ServerAddress;
use theseus::worlds;
use theseus::worlds::{
DisplayStatus, ProtocolVersion, ServerPackStatus, ServerStatus, World,
WorldType, WorldWithProfile,
WorldType, WorldWithInstance,
};
use theseus::{profile, worlds};
pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("worlds")
.invoke_handler(tauri::generate_handler![
get_recent_worlds,
get_profile_worlds,
get_instance_worlds,
get_singleplayer_world,
set_world_display_status,
rename_world,
reset_world_icon,
backup_world,
delete_world,
add_server_to_profile,
edit_server_in_profile,
remove_server_from_profile,
get_profile_protocol_version,
add_server_to_instance,
edit_server_in_instance,
remove_server_from_instance,
get_instance_protocol_version,
get_server_status,
start_join_singleplayer_world,
start_join_server,
@@ -38,7 +38,7 @@ pub async fn get_recent_worlds<R: Runtime>(
app_handle: AppHandle<R>,
limit: usize,
display_statuses: Option<EnumSet<DisplayStatus>>,
) -> Result<Vec<WorldWithProfile>> {
) -> Result<Vec<WorldWithInstance>> {
let mut result = worlds::get_recent_worlds(
limit,
display_statuses.unwrap_or(EnumSet::all()),
@@ -51,11 +51,11 @@ pub async fn get_recent_worlds<R: Runtime>(
}
#[tauri::command]
pub async fn get_profile_worlds<R: Runtime>(
pub async fn get_instance_worlds<R: Runtime>(
app_handle: AppHandle<R>,
path: &str,
instance_id: &str,
) -> Result<Vec<World>> {
let mut result = worlds::get_profile_worlds(path).await?;
let mut result = worlds::get_instance_worlds(instance_id).await?;
for world in &mut result {
adapt_world_icon(&app_handle, world);
}
@@ -146,18 +146,16 @@ pub async fn delete_world(instance: &str, world: &str) -> Result<()> {
}
#[tauri::command]
pub async fn add_server_to_profile(
path: &str,
pub async fn add_server_to_instance(
instance_id: &str,
name: String,
address: String,
pack_status: ServerPackStatus,
project_id: Option<String>,
content_kind: Option<String>,
) -> Result<usize> {
let full_path = get_full_path(path).await?;
Ok(worlds::add_server_to_profile(
&full_path,
path,
Ok(worlds::add_server_to_instance(
instance_id,
name,
address,
pack_status,
@@ -168,34 +166,38 @@ pub async fn add_server_to_profile(
}
#[tauri::command]
pub async fn edit_server_in_profile(
path: &str,
pub async fn edit_server_in_instance(
instance_id: &str,
index: usize,
name: String,
address: String,
pack_status: ServerPackStatus,
) -> Result<()> {
let path = get_full_path(path).await?;
worlds::edit_server_in_profile(&path, index, name, address, pack_status)
.await?;
worlds::edit_server_in_instance(
instance_id,
index,
name,
address,
pack_status,
)
.await?;
Ok(())
}
#[tauri::command]
pub async fn remove_server_from_profile(
path: &str,
pub async fn remove_server_from_instance(
instance_id: &str,
index: usize,
) -> Result<()> {
let path = get_full_path(path).await?;
worlds::remove_server_from_profile(&path, index).await?;
worlds::remove_server_from_instance(instance_id, index).await?;
Ok(())
}
#[tauri::command]
pub async fn get_profile_protocol_version(
path: &str,
pub async fn get_instance_protocol_version(
instance_id: &str,
) -> Result<Option<ProtocolVersion>> {
Ok(worlds::get_profile_protocol_version(path).await?)
Ok(worlds::get_instance_protocol_version(instance_id).await?)
}
#[tauri::command]
@@ -208,22 +210,22 @@ pub async fn get_server_status(
#[tauri::command]
pub async fn start_join_singleplayer_world(
path: &str,
instance_id: &str,
world: String,
) -> Result<ProcessMetadata> {
let process =
profile::run(path, QuickPlayType::Singleplayer(world)).await?;
instance::run(instance_id, QuickPlayType::Singleplayer(world)).await?;
Ok(process)
}
#[tauri::command]
pub async fn start_join_server(
path: &str,
instance_id: &str,
address: &str,
) -> Result<ProcessMetadata> {
let process = profile::run(
path,
let process = instance::run(
instance_id,
QuickPlayType::Server(ServerAddress::Unresolved(address.to_owned())),
)
.await?;