mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 17:14:50 +00:00
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:
co-authored by
DJCheesusReal
Truman Gao
parent
ef4044534f
commit
734720e11e
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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?)
|
||||
}
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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?)
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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('`', "\\`")
|
||||
)
|
||||
}
|
||||
@@ -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('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
@@ -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?;
|
||||
|
||||
Reference in New Issue
Block a user