Compare commits
7 Commits
home-refre
...
shared-pro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e147cc9db | ||
|
|
d2834ce720 | ||
|
|
f9beea8ef2 | ||
|
|
1018d05e36 | ||
|
|
719aded698 | ||
|
|
6b99f82cea | ||
|
|
bdde054036 |
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -2498,6 +2498,16 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minidump-common"
|
||||
version = "0.14.0"
|
||||
@@ -3582,6 +3592,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
@@ -5237,6 +5248,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.13"
|
||||
|
||||
@@ -46,7 +46,7 @@ indicatif = { version = "0.17.3", optional = true }
|
||||
|
||||
async-tungstenite = { version = "0.22.1", features = ["tokio-runtime", "tokio-native-tls"] }
|
||||
futures = "0.3"
|
||||
reqwest = { version = "0.11", features = ["json", "stream"] }
|
||||
reqwest = { version = "0.11", features = ["json", "stream", "multipart", "blocking"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||
async-recursion = "1.0.4"
|
||||
|
||||
@@ -14,6 +14,9 @@ use crate::{
|
||||
/// (Does not include modrinth://)
|
||||
pub async fn handle_url(sublink: &str) -> crate::Result<CommandPayload> {
|
||||
Ok(match sublink.split_once('/') {
|
||||
Some(("shared_profile", link)) => CommandPayload::OpenSharedProfile {
|
||||
link: link.to_string(),
|
||||
},
|
||||
// /mod/{id} - Installs a mod of mod id
|
||||
Some(("mod", id)) => CommandPayload::InstallMod { id: id.to_string() },
|
||||
// /version/{id} - Installs a specific version of id
|
||||
|
||||
@@ -11,6 +11,7 @@ pub mod process;
|
||||
pub mod profile;
|
||||
pub mod safety;
|
||||
pub mod settings;
|
||||
pub mod shared_profile;
|
||||
pub mod tags;
|
||||
|
||||
pub mod data {
|
||||
@@ -29,7 +30,7 @@ pub mod prelude {
|
||||
event::CommandPayload,
|
||||
jre, metadata, pack, process,
|
||||
profile::{self, create, Profile},
|
||||
settings,
|
||||
settings, shared_profile,
|
||||
state::JavaGlobals,
|
||||
state::{Dependency, ProfilePathId, ProjectPathId},
|
||||
util::{
|
||||
|
||||
@@ -215,7 +215,7 @@ async fn import_atlauncher_unmanaged(
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||
prof.metadata.linked_data = Some(LinkedData {
|
||||
prof.metadata.linked_data = Some(LinkedData::ModrinthModpack {
|
||||
project_id: description.project_id.clone(),
|
||||
version_id: description.version_id.clone(),
|
||||
locked: Some(
|
||||
|
||||
@@ -159,7 +159,7 @@ pub fn get_profile_from_pack(
|
||||
} => CreatePackProfile {
|
||||
name: title,
|
||||
icon_url,
|
||||
linked_data: Some(LinkedData {
|
||||
linked_data: Some(LinkedData::ModrinthModpack {
|
||||
project_id: Some(project_id),
|
||||
version_id: Some(version_id),
|
||||
locked: Some(true),
|
||||
@@ -391,21 +391,30 @@ pub async fn set_profile_information(
|
||||
let project_id = description.project_id.clone();
|
||||
let version_id = description.version_id.clone();
|
||||
|
||||
prof.metadata.linked_data = if project_id.is_some()
|
||||
&& version_id.is_some()
|
||||
{
|
||||
Some(LinkedData {
|
||||
project_id,
|
||||
version_id,
|
||||
locked: if !ignore_lock {
|
||||
Some(true)
|
||||
} else {
|
||||
prof.metadata.linked_data.as_ref().and_then(|x| x.locked)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
prof.metadata.linked_data =
|
||||
if project_id.is_some() && version_id.is_some() {
|
||||
Some(LinkedData::ModrinthModpack {
|
||||
project_id,
|
||||
version_id,
|
||||
locked: if !ignore_lock {
|
||||
Some(true)
|
||||
} else {
|
||||
prof.metadata.linked_data.as_ref().and_then(|x| {
|
||||
if let LinkedData::ModrinthModpack {
|
||||
locked: Some(locked),
|
||||
..
|
||||
} = x
|
||||
{
|
||||
Some(*locked)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
prof.metadata.icon = description.icon.clone();
|
||||
prof.metadata.game_version = game_version.clone();
|
||||
|
||||
@@ -102,11 +102,14 @@ pub async fn profile_create(
|
||||
}
|
||||
|
||||
profile.metadata.linked_data = linked_data;
|
||||
if let Some(linked_data) = &mut profile.metadata.linked_data {
|
||||
linked_data.locked = Some(
|
||||
linked_data.project_id.is_some()
|
||||
&& linked_data.version_id.is_some(),
|
||||
);
|
||||
if let Some(LinkedData::ModrinthModpack {
|
||||
project_id,
|
||||
version_id,
|
||||
locked,
|
||||
..
|
||||
}) = &mut profile.metadata.linked_data
|
||||
{
|
||||
*locked = Some(project_id.is_some() && version_id.is_some());
|
||||
}
|
||||
|
||||
emit_profile(
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::pack::install_from::{
|
||||
EnvType, PackDependency, PackFile, PackFileHash, PackFormat,
|
||||
};
|
||||
use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId};
|
||||
use crate::state::LinkedData;
|
||||
use crate::state::{InnerProjectPathUnix, ProjectMetadata, SideType};
|
||||
|
||||
use crate::util::fetch;
|
||||
@@ -67,13 +68,11 @@ pub async fn get(
|
||||
let state = State::get().await?;
|
||||
let profiles = state.profiles.read().await;
|
||||
let mut profile = profiles.0.get(path).cloned();
|
||||
|
||||
if clear_projects.unwrap_or(false) {
|
||||
if let Some(profile) = &mut profile {
|
||||
profile.projects = HashMap::new();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
@@ -117,15 +116,14 @@ pub async fn get_mod_full_path(
|
||||
project_path: &ProjectPathId,
|
||||
) -> crate::Result<PathBuf> {
|
||||
if get(profile_path, Some(true)).await?.is_some() {
|
||||
let full_path = io::canonicalize(
|
||||
project_path.get_full_path(profile_path.clone()).await?,
|
||||
)?;
|
||||
let full_path =
|
||||
io::canonicalize(project_path.get_full_path(profile_path).await?)?;
|
||||
return Ok(full_path);
|
||||
}
|
||||
|
||||
Err(crate::ErrorKind::OtherError(format!(
|
||||
"Tried to get the full path of a nonexistent or unloaded project at path {}!",
|
||||
project_path.get_full_path(profile_path.clone()).await?.display()
|
||||
project_path.get_full_path(profile_path).await?.display()
|
||||
))
|
||||
.into())
|
||||
}
|
||||
@@ -874,7 +872,13 @@ pub async fn try_update_playtime(path: &ProfilePathId) -> crate::Result<()> {
|
||||
let res = if updated_recent_playtime > 0 {
|
||||
// Create update struct to send to Labrinth
|
||||
let modrinth_pack_version_id =
|
||||
profile.metadata.linked_data.and_then(|l| l.version_id);
|
||||
profile.metadata.linked_data.and_then(|l| {
|
||||
if let LinkedData::ModrinthModpack { version_id, .. } = l {
|
||||
Some(version_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let playtime_update_json = json!({
|
||||
"seconds": updated_recent_playtime,
|
||||
"loader": profile.metadata.loader.to_string(),
|
||||
@@ -892,7 +896,7 @@ pub async fn try_update_playtime(path: &ProfilePathId) -> crate::Result<()> {
|
||||
|
||||
let creds = state.credentials.read().await;
|
||||
fetch::post_json(
|
||||
"https://api.modrinth.com/analytics/playtime",
|
||||
"https://staging-api.modrinth.com/analytics/playtime",
|
||||
serde_json::to_value(hashmap)?,
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
pack::{self, install_from::generate_pack_from_version_id},
|
||||
prelude::{ProfilePathId, ProjectPathId},
|
||||
profile::get,
|
||||
state::{ProfileInstallStage, Project},
|
||||
state::{LinkedData, ProfileInstallStage, Project},
|
||||
LoadingBarType, State,
|
||||
};
|
||||
use futures::try_join;
|
||||
@@ -30,15 +30,14 @@ pub async fn update_managed_modrinth_version(
|
||||
};
|
||||
|
||||
// Extract modrinth pack information, if appropriate
|
||||
let linked_data = profile
|
||||
.metadata
|
||||
.linked_data
|
||||
.as_ref()
|
||||
.ok_or_else(unmanaged_err)?;
|
||||
let project_id: &String =
|
||||
linked_data.project_id.as_ref().ok_or_else(unmanaged_err)?;
|
||||
let version_id =
|
||||
linked_data.version_id.as_ref().ok_or_else(unmanaged_err)?;
|
||||
let Some(LinkedData::ModrinthModpack {
|
||||
project_id: Some(ref project_id),
|
||||
version_id: Some(ref version_id),
|
||||
..
|
||||
}) = profile.metadata.linked_data
|
||||
else {
|
||||
return Err(unmanaged_err().into());
|
||||
};
|
||||
|
||||
// Replace the pack with the new version
|
||||
replace_managed_modrinth(
|
||||
@@ -107,15 +106,14 @@ pub async fn repair_managed_modrinth(
|
||||
.await?;
|
||||
|
||||
// Extract modrinth pack information, if appropriate
|
||||
let linked_data = profile
|
||||
.metadata
|
||||
.linked_data
|
||||
.as_ref()
|
||||
.ok_or_else(unmanaged_err)?;
|
||||
let project_id: &String =
|
||||
linked_data.project_id.as_ref().ok_or_else(unmanaged_err)?;
|
||||
let version_id =
|
||||
linked_data.version_id.as_ref().ok_or_else(unmanaged_err)?;
|
||||
let Some(LinkedData::ModrinthModpack {
|
||||
project_id: Some(ref project_id),
|
||||
version_id: Some(ref version_id),
|
||||
..
|
||||
}) = profile.metadata.linked_data
|
||||
else {
|
||||
return Err(unmanaged_err().into());
|
||||
};
|
||||
|
||||
// Replace the pack with the same version
|
||||
replace_managed_modrinth(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Theseus profile management interface
|
||||
|
||||
use std::path::{PathBuf, Path};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs;
|
||||
|
||||
use io::IOError;
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
event::emit::{emit_loading, init_loading},
|
||||
prelude::DirectoryInfo,
|
||||
state::{self, Profiles},
|
||||
util::{io, fetch},
|
||||
util::{fetch, io},
|
||||
};
|
||||
pub use crate::{
|
||||
state::{
|
||||
@@ -109,7 +109,6 @@ pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> {
|
||||
let old_config_dir =
|
||||
state_write.directories.config_dir.read().await.clone();
|
||||
|
||||
|
||||
// Reset file watcher
|
||||
tracing::trace!("Reset file watcher");
|
||||
let file_watcher = state::init_watcher().await?;
|
||||
@@ -125,13 +124,17 @@ pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> {
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &old_config_dir))?
|
||||
{
|
||||
|
||||
let entry_path = entry.path();
|
||||
if let Some(file_name) = entry_path.file_name() {
|
||||
// We are only moving the profiles and metadata folders
|
||||
if file_name == state::PROFILES_FOLDER_NAME || file_name == state::METADATA_FOLDER_NAME {
|
||||
if file_name == state::PROFILES_FOLDER_NAME
|
||||
|| file_name == state::METADATA_FOLDER_NAME
|
||||
{
|
||||
if across_drives {
|
||||
entries.extend(crate::pack::import::get_all_subfiles(&entry_path).await?);
|
||||
entries.extend(
|
||||
crate::pack::import::get_all_subfiles(&entry_path)
|
||||
.await?,
|
||||
);
|
||||
deletable_entries.push(entry_path.clone());
|
||||
} else {
|
||||
entries.push(entry_path.clone());
|
||||
@@ -151,8 +154,7 @@ pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> {
|
||||
} else {
|
||||
io::rename(entry_path.clone(), new_path.clone()).await?;
|
||||
}
|
||||
emit_loading(&loading_bar, 80.0 * (1.0 / num_entries), None)
|
||||
.await?;
|
||||
emit_loading(&loading_bar, 80.0 * (1.0 / num_entries), None).await?;
|
||||
}
|
||||
|
||||
tracing::trace!("Setting configuration setting");
|
||||
@@ -199,7 +201,8 @@ pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> {
|
||||
&loading_bar,
|
||||
10.0 * (1.0 / deletable_entries_len as f64),
|
||||
None,
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Reset file watcher
|
||||
@@ -228,7 +231,6 @@ fn is_different_drive(path1: &Path, path2: &Path) -> bool {
|
||||
root1 != root2
|
||||
}
|
||||
|
||||
|
||||
pub async fn is_dir_writeable(new_config_dir: PathBuf) -> crate::Result<bool> {
|
||||
let temp_path = new_config_dir.join(".tmp");
|
||||
match fs::write(temp_path.clone(), "test").await {
|
||||
|
||||
889
theseus/src/api/shared_profile.rs
Normal file
889
theseus/src/api/shared_profile.rs
Normal file
@@ -0,0 +1,889 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{
|
||||
config::MODRINTH_API_URL_INTERNAL,
|
||||
prelude::{
|
||||
LinkedData, ModLoader, ProfilePathId, ProjectMetadata, ProjectPathId,
|
||||
},
|
||||
profile,
|
||||
state::{Profile, Profiles},
|
||||
util::{
|
||||
fetch::{fetch_advanced, REQWEST_CLIENT},
|
||||
io,
|
||||
},
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct SharedProfile {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub is_owned: bool, // Whether we are the owner (intentionally redundant)
|
||||
pub owner_id: String,
|
||||
pub icon_url: Option<String>,
|
||||
pub loader: ModLoader,
|
||||
pub loader_version: String,
|
||||
pub game_version: String,
|
||||
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
pub versions: Vec<String>,
|
||||
pub overrides: Vec<SharedProfileOverride>,
|
||||
|
||||
pub share_links: Option<Vec<SharedProfileLink>>,
|
||||
pub users: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct SharedProfileLink {
|
||||
pub id: String,
|
||||
pub created: DateTime<Utc>,
|
||||
pub expires: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct SharedProfileOverride {
|
||||
pub url: String,
|
||||
pub install_path: PathBuf,
|
||||
pub hashes: SharedProfileOverrideHashes,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct SharedProfileOverrideHashes {
|
||||
pub sha1: String,
|
||||
pub sha512: String,
|
||||
}
|
||||
|
||||
// Simplified version of SharedProfile- this is what is returned from the Labrinth API
|
||||
// This is not used, except for requests where we are not a member of the shared profile
|
||||
// (ie: previewing a shared profile from a link, before accepting it)
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct SharedProfileResponse {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub owner_id: String,
|
||||
pub created: DateTime<Utc>,
|
||||
pub updated: DateTime<Utc>,
|
||||
pub icon_url: Option<String>,
|
||||
|
||||
pub loader: ModLoader,
|
||||
pub game: String,
|
||||
|
||||
pub loader_version: String,
|
||||
pub game_version: String,
|
||||
|
||||
// Present only if we are the owner
|
||||
pub share_links: Option<Vec<SharedProfileLink>>,
|
||||
pub users: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
// Create a new shared profile from ProfilePathId
|
||||
// This converts the LinkedData to a SharedProfile and uploads it to the Labrinth API
|
||||
#[tracing::instrument]
|
||||
pub async fn create(profile_id: ProfilePathId) -> crate::Result<()> {
|
||||
let state = crate::State::get().await?;
|
||||
|
||||
let profile: Profile =
|
||||
profile::get(&profile_id, None).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_id.to_string())
|
||||
})?;
|
||||
let creds = state.credentials.read().await;
|
||||
let creds = creds
|
||||
.0
|
||||
.as_ref()
|
||||
.ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
|
||||
|
||||
// Currently existing linked data should fail
|
||||
match profile.metadata.linked_data {
|
||||
Some(LinkedData::SharedProfile { .. }) => {
|
||||
return Err(crate::ErrorKind::OtherError(
|
||||
"Profile already linked to a shared profile".to_string(),
|
||||
)
|
||||
.as_error());
|
||||
}
|
||||
Some(LinkedData::ModrinthModpack { .. }) => {
|
||||
return Err(crate::ErrorKind::OtherError(
|
||||
"Profile already linked to a modrinth project".to_string(),
|
||||
)
|
||||
.as_error());
|
||||
}
|
||||
None => {}
|
||||
};
|
||||
|
||||
let name = profile.metadata.name;
|
||||
let loader = profile.metadata.loader;
|
||||
let loader_version = profile.metadata.loader_version;
|
||||
let game_version = profile.metadata.game_version;
|
||||
|
||||
let modrinth_projects: Vec<_> = profile
|
||||
.projects
|
||||
.iter()
|
||||
.filter_map(|(_, project)| {
|
||||
if let ProjectMetadata::Modrinth { ref version, .. } =
|
||||
project.metadata
|
||||
{
|
||||
Some(&version.id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let override_files: Vec<_> = profile
|
||||
.projects
|
||||
.iter()
|
||||
.filter_map(|(id, project)| {
|
||||
if let ProjectMetadata::Inferred { .. } = project.metadata {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create the profile on the Labrinth API
|
||||
let response = REQWEST_CLIENT
|
||||
.post(format!("{MODRINTH_API_URL_INTERNAL}client/profile"))
|
||||
.header("Authorization", &creds.session)
|
||||
.json(&serde_json::json!({
|
||||
"name": name,
|
||||
"loader": loader.as_api_str(),
|
||||
"loader_version": loader_version.map(|x| x.id).unwrap_or_default(),
|
||||
"game": "minecraft-java",
|
||||
"game_version": game_version,
|
||||
"versions": modrinth_projects,
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let profile_response = response.json::<serde_json::Value>().await?;
|
||||
|
||||
// Extract the profile ID from the response
|
||||
let shared_profile_id = profile_response["id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(
|
||||
"Could not parse response from Labrinth API".to_string(),
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
// Unmanaged projects
|
||||
let mut data = vec![]; // 'data' field, giving installation context to labrinth
|
||||
let mut parts = vec![]; // 'parts' field, containing the actual files
|
||||
|
||||
for override_file in override_files {
|
||||
let path = override_file.get_inner_path_unix();
|
||||
let Some(name) = path.0.split('/').last().map(|x| x.to_string()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Load override to file
|
||||
let full_path = &override_file.get_full_path(&profile_id).await?;
|
||||
let file_bytes = io::read(full_path).await?;
|
||||
let ext = full_path
|
||||
.extension()
|
||||
.and_then(|x| x.to_str())
|
||||
.unwrap_or_default();
|
||||
let mime = project_file_type(ext).ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Could not determine file type for {}",
|
||||
ext
|
||||
))
|
||||
})?;
|
||||
|
||||
data.push(serde_json::json!({
|
||||
"file_name": name.clone(),
|
||||
"install_path": path
|
||||
}));
|
||||
|
||||
let part = reqwest::multipart::Part::bytes(file_bytes)
|
||||
.file_name(name.clone())
|
||||
.mime_str(mime)?;
|
||||
parts.push((name.clone(), part));
|
||||
}
|
||||
|
||||
// Build multipart with 'data' field first
|
||||
let mut multipart = reqwest::multipart::Form::new().percent_encode_noop();
|
||||
let json_part =
|
||||
reqwest::multipart::Part::text(serde_json::to_string(&data)?); //mime_str("application/json")?;
|
||||
multipart = multipart.part("data", json_part);
|
||||
for (name, part) in parts {
|
||||
multipart = multipart.part(name, part);
|
||||
}
|
||||
let response = REQWEST_CLIENT.post(
|
||||
format!("{MODRINTH_API_URL_INTERNAL}client/profile/{shared_profile_id}/override"),
|
||||
)
|
||||
.header("Authorization", &creds.session)
|
||||
.multipart(multipart);
|
||||
|
||||
response.send().await?.error_for_status()?;
|
||||
|
||||
// Update the profile with the new linked data
|
||||
profile::edit(&profile_id, |profile| {
|
||||
let shared_profile_id = shared_profile_id.clone();
|
||||
profile.metadata.linked_data = Some(LinkedData::SharedProfile {
|
||||
profile_id: shared_profile_id,
|
||||
is_owner: true,
|
||||
});
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Sync
|
||||
crate::State::sync().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn project_file_type(ext: &str) -> Option<&str> {
|
||||
match ext {
|
||||
"jar" => Some("application/java-archive"),
|
||||
"zip" | "litemod" => Some("application/zip"),
|
||||
"mrpack" => Some("application/x-modrinth-modpack+zip"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_all() -> crate::Result<Vec<SharedProfile>> {
|
||||
let state = crate::State::get().await?;
|
||||
let creds = state.credentials.read().await;
|
||||
let creds = creds
|
||||
.0
|
||||
.as_ref()
|
||||
.ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
|
||||
|
||||
let response = REQWEST_CLIENT
|
||||
.get(format!("{MODRINTH_API_URL_INTERNAL}client/user"))
|
||||
.header("Authorization", &creds.session)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
// First, get list of shared profiles the user has access to
|
||||
let profiles = response.json::<Vec<SharedProfileResponse>>().await?;
|
||||
|
||||
// Next, get files for each shared profile
|
||||
// TODO: concurrent requests
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SharedFiles {
|
||||
pub version_ids: Vec<String>,
|
||||
pub override_cdns: Vec<SharedProfileOverride>,
|
||||
}
|
||||
|
||||
let mut shared_profiles = vec![];
|
||||
for profile in profiles.into_iter() {
|
||||
if profile.game != "minecraft-java" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let id = profile.id;
|
||||
let response = REQWEST_CLIENT
|
||||
.get(format!(
|
||||
"{MODRINTH_API_URL_INTERNAL}client/profile/{id}/files"
|
||||
))
|
||||
.header("Authorization", &creds.session)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
let files = response.json::<SharedFiles>().await?;
|
||||
|
||||
shared_profiles.push(SharedProfile {
|
||||
id,
|
||||
name: profile.name,
|
||||
is_owned: profile.owner_id
|
||||
== state
|
||||
.credentials
|
||||
.read()
|
||||
.await
|
||||
.0
|
||||
.as_ref()
|
||||
.map(|x| x.user.id.as_str())
|
||||
.unwrap_or_default(),
|
||||
owner_id: profile.owner_id,
|
||||
loader: profile.loader,
|
||||
loader_version: profile.loader_version,
|
||||
game_version: profile.game_version,
|
||||
icon_url: profile.icon_url,
|
||||
versions: files.version_ids,
|
||||
overrides: files.override_cdns,
|
||||
share_links: profile.share_links,
|
||||
users: profile.users,
|
||||
updated_at: profile.updated,
|
||||
created_at: profile.created,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(shared_profiles)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn install(
|
||||
shared_profile_id: String,
|
||||
) -> crate::Result<ProfilePathId> {
|
||||
let state = crate::State::get().await?;
|
||||
let shared_profile = get_all()
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|x| x.id == shared_profile_id)
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError("Profile not found".to_string())
|
||||
})?;
|
||||
|
||||
let linked_data = LinkedData::SharedProfile {
|
||||
profile_id: shared_profile.id,
|
||||
is_owner: shared_profile.is_owned,
|
||||
};
|
||||
|
||||
// Create new profile
|
||||
let profile_id = crate::profile::create::profile_create(
|
||||
shared_profile.name,
|
||||
shared_profile.game_version,
|
||||
shared_profile.loader,
|
||||
Some(shared_profile.loader_version),
|
||||
None,
|
||||
shared_profile.icon_url,
|
||||
Some(linked_data),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Get the profile
|
||||
let profile: Profile =
|
||||
profile::get(&profile_id, None).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_id.to_string())
|
||||
})?;
|
||||
let creds = state.credentials.read().await;
|
||||
|
||||
// TODO: concurrent requests
|
||||
// Add projects
|
||||
for version in shared_profile.versions {
|
||||
profile.add_project_version(version).await?;
|
||||
}
|
||||
|
||||
for file_override in shared_profile.overrides {
|
||||
let file = fetch_advanced(
|
||||
Method::GET,
|
||||
&file_override.url,
|
||||
Some(file_override.hashes.sha1.as_str()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
)
|
||||
.await?;
|
||||
|
||||
profile
|
||||
.add_project_bytes_directly(&file_override.install_path, file)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(profile_id)
|
||||
}
|
||||
|
||||
// Structure repesenting a synchronization difference between a local profile and a shared profile
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct SharedModpackFileUpdate {
|
||||
// Can be false if all other fields are empty
|
||||
// if the metadata is different
|
||||
pub is_synced: bool,
|
||||
|
||||
// Projects that are in the local profile but not in the shared profile
|
||||
pub unsynced_projects: Vec<ProjectPathId>,
|
||||
|
||||
// Projects that are in the shared profile but not in the local profile
|
||||
pub missing_versions: Vec<String>,
|
||||
pub missing_overrides: Vec<SharedProfileOverride>,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn check_updated(
|
||||
profile_id: &ProfilePathId,
|
||||
shared_profile: &SharedProfile,
|
||||
) -> crate::Result<SharedModpackFileUpdate> {
|
||||
let profile: Profile =
|
||||
profile::get(profile_id, None).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_id.to_string())
|
||||
})?;
|
||||
|
||||
// Check if the metadata is the same- if different, we return false with no file updates
|
||||
if profile.metadata.name != shared_profile.name
|
||||
|| profile.metadata.loader != shared_profile.loader
|
||||
|| profile
|
||||
.metadata
|
||||
.loader_version
|
||||
.map(|x| x.id)
|
||||
.unwrap_or_default()
|
||||
!= shared_profile.loader_version
|
||||
|| profile.metadata.game_version != shared_profile.game_version
|
||||
{
|
||||
return Ok(SharedModpackFileUpdate::default());
|
||||
}
|
||||
|
||||
// Check if the projects are the same- we check each override by hash and each modrinth project by version id
|
||||
let mut modrinth_projects = shared_profile.versions.clone();
|
||||
let mut overrides = shared_profile.overrides.clone();
|
||||
let unsynced_projects: Vec<_> = profile
|
||||
.projects
|
||||
.into_iter()
|
||||
.filter_map(|(id, project)| {
|
||||
match project.metadata {
|
||||
ProjectMetadata::Modrinth { ref version, .. } => {
|
||||
if modrinth_projects.contains(&version.id) {
|
||||
modrinth_projects.retain(|x| x != &version.id);
|
||||
} else {
|
||||
return Some(id);
|
||||
}
|
||||
}
|
||||
ProjectMetadata::Inferred { .. } => {
|
||||
let Some(matching_override) =
|
||||
overrides.iter().position(|o| {
|
||||
o.install_path.to_string_lossy()
|
||||
== id.get_inner_path_unix().0
|
||||
})
|
||||
else {
|
||||
return Some(id);
|
||||
};
|
||||
|
||||
if let Some(o) = overrides.get(matching_override) {
|
||||
if o.hashes.sha512 != project.sha512 {
|
||||
return Some(id);
|
||||
}
|
||||
} else {
|
||||
return Some(id);
|
||||
}
|
||||
overrides.remove(matching_override);
|
||||
}
|
||||
ProjectMetadata::Unknown => {
|
||||
// TODO: What to do for unknown projects?
|
||||
return Some(id);
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(SharedModpackFileUpdate {
|
||||
is_synced: modrinth_projects.is_empty()
|
||||
&& overrides.is_empty()
|
||||
&& unsynced_projects.is_empty(),
|
||||
unsynced_projects,
|
||||
missing_versions: modrinth_projects,
|
||||
missing_overrides: overrides,
|
||||
})
|
||||
}
|
||||
|
||||
// Updates projects for a given ProfilePathId from a SharedProfile
|
||||
// This updates the local profile to match the shared profile on the Labrinth API
|
||||
#[tracing::instrument]
|
||||
pub async fn inbound_sync(profile_id: ProfilePathId) -> crate::Result<()> {
|
||||
let state = crate::State::get().await?;
|
||||
|
||||
let profile: Profile =
|
||||
profile::get(&profile_id, None).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_id.to_string())
|
||||
})?;
|
||||
let creds = state.credentials.read().await;
|
||||
|
||||
// Get linked
|
||||
let shared_profile = match profile.metadata.linked_data {
|
||||
Some(LinkedData::SharedProfile { ref profile_id, .. }) => profile_id,
|
||||
_ => {
|
||||
return Err(crate::ErrorKind::OtherError(
|
||||
"Profile is not linked to a shared profile".to_string(),
|
||||
)
|
||||
.as_error())
|
||||
}
|
||||
};
|
||||
|
||||
// Get updated shared profile
|
||||
let shared_profile = get_all()
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|x| &x.id == shared_profile)
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(
|
||||
"Profile is not linked to a shared profile".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let update_data = check_updated(&profile_id, &shared_profile).await?;
|
||||
if update_data.is_synced {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Remove projects- unsynced projects need to be removed
|
||||
for project in update_data.unsynced_projects {
|
||||
profile.remove_project(&project, None).await?;
|
||||
}
|
||||
|
||||
// TODO: concurrent requests
|
||||
// Add projects- missing projects need to be added
|
||||
for version in update_data.missing_versions {
|
||||
profile.add_project_version(version).await?;
|
||||
}
|
||||
|
||||
for file_override in update_data.missing_overrides {
|
||||
let file = fetch_advanced(
|
||||
Method::GET,
|
||||
&file_override.url,
|
||||
Some(file_override.hashes.sha1.as_str()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
)
|
||||
.await?;
|
||||
|
||||
profile
|
||||
.add_project_bytes_directly(&file_override.install_path, file)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Updates metadata for a given ProfilePathId to the Labrinth API
|
||||
// Must be an owner of the shared profile
|
||||
#[tracing::instrument]
|
||||
pub async fn outbound_sync(profile_id: ProfilePathId) -> crate::Result<()> {
|
||||
let state = crate::State::get().await?;
|
||||
|
||||
let profile: Profile =
|
||||
profile::get(&profile_id, None).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_id.to_string())
|
||||
})?;
|
||||
let creds = state.credentials.read().await;
|
||||
let creds = creds
|
||||
.0
|
||||
.as_ref()
|
||||
.ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
|
||||
|
||||
// Get linked
|
||||
let shared_profile = match profile.metadata.linked_data {
|
||||
Some(LinkedData::SharedProfile { profile_id, .. }) => profile_id,
|
||||
_ => {
|
||||
return Err(crate::ErrorKind::OtherError(
|
||||
"Profile is not linked to a shared profile".to_string(),
|
||||
)
|
||||
.as_error())
|
||||
}
|
||||
};
|
||||
|
||||
// Get updated shared profile
|
||||
let shared_profile = get_all()
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|x| x.id == shared_profile)
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(
|
||||
"Profile is not linked to a shared profile".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Check owner
|
||||
if !shared_profile.is_owned {
|
||||
return Err(crate::ErrorKind::OtherError(
|
||||
"Profile is not owned by the current user".to_string(),
|
||||
)
|
||||
.as_error());
|
||||
}
|
||||
|
||||
// Check if we are synced
|
||||
let update_data = check_updated(&profile_id, &shared_profile).await?;
|
||||
let id = shared_profile.id;
|
||||
if update_data.is_synced {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let unsynced = update_data.unsynced_projects;
|
||||
let projects: Vec<_> = profile
|
||||
.projects
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|(id, _)| unsynced.contains(id))
|
||||
.collect();
|
||||
let unsynced_modrinth_projects: Vec<_> = projects
|
||||
.iter()
|
||||
.filter_map(|(_, project)| {
|
||||
if let ProjectMetadata::Modrinth { ref version, .. } =
|
||||
project.metadata
|
||||
{
|
||||
Some(&version.id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let unsynced_override_files: Vec<_> = projects
|
||||
.iter()
|
||||
.filter_map(|(id, project)| {
|
||||
if let ProjectMetadata::Inferred { .. } = project.metadata {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Generate new version set
|
||||
let mut new_version_set = shared_profile.versions;
|
||||
for version in update_data.missing_versions {
|
||||
new_version_set.retain(|x| x != &version);
|
||||
}
|
||||
for version in unsynced_modrinth_projects {
|
||||
new_version_set.push(version.to_string());
|
||||
}
|
||||
|
||||
// Update metadata + versions
|
||||
REQWEST_CLIENT
|
||||
.patch(
|
||||
format!("{MODRINTH_API_URL_INTERNAL}client/profile/{id}"),
|
||||
)
|
||||
.header("Authorization", &creds.session)
|
||||
.json(&serde_json::json!({
|
||||
"name": profile.metadata.name,
|
||||
"loader": profile.metadata.loader.as_api_str(),
|
||||
"loader_version": profile.metadata.loader_version.map(|x| x.id).unwrap_or_default(),
|
||||
"game": "minecraft-java",
|
||||
"game_version": profile.metadata.game_version,
|
||||
"versions": new_version_set,
|
||||
}))
|
||||
.send().await?.error_for_status()?;
|
||||
|
||||
// Create multipart for uploading new overrides
|
||||
let mut parts = vec![]; // 'parts' field, containing the actual files
|
||||
let mut data = vec![]; // 'data' field, giving installation context to labrinth
|
||||
for override_file in unsynced_override_files {
|
||||
let path = override_file.get_inner_path_unix();
|
||||
let Some(name) = path.0.split('/').last().map(|x| x.to_string()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Load override to file
|
||||
let full_path = &override_file.get_full_path(&profile_id).await?;
|
||||
let file_bytes = io::read(full_path).await?;
|
||||
let ext = full_path
|
||||
.extension()
|
||||
.and_then(|x| x.to_str())
|
||||
.unwrap_or_default();
|
||||
let mime = project_file_type(ext).ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Could not determine file type for {}",
|
||||
ext
|
||||
))
|
||||
})?;
|
||||
|
||||
data.push(serde_json::json!({
|
||||
"file_name": name.clone(),
|
||||
"install_path": path
|
||||
}));
|
||||
|
||||
let part = reqwest::multipart::Part::bytes(file_bytes)
|
||||
.file_name(name.clone())
|
||||
.mime_str(mime)?;
|
||||
parts.push((name.clone(), part));
|
||||
}
|
||||
|
||||
// Build multipart with 'data' field first
|
||||
let mut multipart = reqwest::multipart::Form::new().percent_encode_noop();
|
||||
let json_part =
|
||||
reqwest::multipart::Part::text(serde_json::to_string(&data)?); //mime_str("application/json")?;
|
||||
multipart = multipart.part("data", json_part);
|
||||
for (name, part) in parts {
|
||||
multipart = multipart.part(name, part);
|
||||
}
|
||||
let response = REQWEST_CLIENT
|
||||
.post(format!(
|
||||
"{MODRINTH_API_URL_INTERNAL}client/profile/{id}/override"
|
||||
))
|
||||
.header("Authorization", &creds.session)
|
||||
.multipart(multipart);
|
||||
|
||||
response.send().await?.error_for_status()?;
|
||||
|
||||
// Cannot fail, simply re-checks its synced with the shared profile
|
||||
Profiles::update_shared_projects().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_shared_profile_users(
|
||||
profile_id: ProfilePathId,
|
||||
users: Vec<String>,
|
||||
) -> crate::Result<()> {
|
||||
let state = crate::State::get().await?;
|
||||
|
||||
let profile: Profile =
|
||||
profile::get(&profile_id, None).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_id.to_string())
|
||||
})?;
|
||||
let creds = state.credentials.read().await;
|
||||
let creds = creds
|
||||
.0
|
||||
.as_ref()
|
||||
.ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
|
||||
|
||||
let shared_profile = match profile.metadata.linked_data {
|
||||
Some(LinkedData::SharedProfile { profile_id, .. }) => profile_id,
|
||||
_ => {
|
||||
return Err(crate::ErrorKind::OtherError(
|
||||
"Profile is not linked to a shared profile".to_string(),
|
||||
)
|
||||
.as_error())
|
||||
}
|
||||
};
|
||||
|
||||
REQWEST_CLIENT
|
||||
.patch(format!(
|
||||
"{MODRINTH_API_URL_INTERNAL}client/profile/{shared_profile}"
|
||||
))
|
||||
.header("Authorization", &creds.session)
|
||||
.json(&serde_json::json!({
|
||||
"remove_users": users,
|
||||
}))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_shared_profile_links(
|
||||
profile_id: ProfilePathId,
|
||||
links: Vec<String>,
|
||||
) -> crate::Result<()> {
|
||||
let state = crate::State::get().await?;
|
||||
|
||||
let profile: Profile =
|
||||
profile::get(&profile_id, None).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_id.to_string())
|
||||
})?;
|
||||
let creds = state.credentials.read().await;
|
||||
let creds = creds
|
||||
.0
|
||||
.as_ref()
|
||||
.ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
|
||||
|
||||
let shared_profile = match profile.metadata.linked_data {
|
||||
Some(LinkedData::SharedProfile { profile_id, .. }) => profile_id,
|
||||
_ => {
|
||||
return Err(crate::ErrorKind::OtherError(
|
||||
"Profile is not linked to a shared profile".to_string(),
|
||||
)
|
||||
.as_error())
|
||||
}
|
||||
};
|
||||
|
||||
REQWEST_CLIENT
|
||||
.patch(format!(
|
||||
"{MODRINTH_API_URL_INTERNAL}client/profile/{shared_profile}"
|
||||
))
|
||||
.header("Authorization", &creds.session)
|
||||
.json(&serde_json::json!({
|
||||
"remove_links": links,
|
||||
}))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn generate_share_link(
|
||||
profile_id: ProfilePathId,
|
||||
) -> crate::Result<String> {
|
||||
let state = crate::State::get().await?;
|
||||
|
||||
let profile: Profile =
|
||||
profile::get(&profile_id, None).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_id.to_string())
|
||||
})?;
|
||||
let creds = state.credentials.read().await;
|
||||
let creds = creds
|
||||
.0
|
||||
.as_ref()
|
||||
.ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
|
||||
|
||||
let shared_profile = match profile.metadata.linked_data {
|
||||
Some(LinkedData::SharedProfile { profile_id, .. }) => profile_id,
|
||||
_ => {
|
||||
return Err(crate::ErrorKind::OtherError(
|
||||
"Profile is not linked to a shared profile".to_string(),
|
||||
)
|
||||
.as_error())
|
||||
}
|
||||
};
|
||||
|
||||
let response = REQWEST_CLIENT
|
||||
.post(format!(
|
||||
"{MODRINTH_API_URL_INTERNAL}client/profile/{shared_profile}/share"
|
||||
))
|
||||
.header("Authorization", &creds.session)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
let link = response.json::<SharedProfileLink>().await?;
|
||||
|
||||
Ok(generate_deep_link(&link))
|
||||
}
|
||||
|
||||
fn generate_deep_link(link: &SharedProfileLink) -> String {
|
||||
format!("modrinth://shared_profile/{}", link.id)
|
||||
}
|
||||
|
||||
pub async fn accept_share_link(link: String) -> crate::Result<()> {
|
||||
let state = crate::State::get().await?;
|
||||
|
||||
let creds = state.credentials.read().await;
|
||||
let creds = creds
|
||||
.0
|
||||
.as_ref()
|
||||
.ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
|
||||
|
||||
REQWEST_CLIENT
|
||||
.post(format!(
|
||||
"{MODRINTH_API_URL_INTERNAL}client/share/{link}/accept"
|
||||
))
|
||||
.header("Authorization", &creds.session)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Gets a shared profile from a share link
|
||||
// This is done without accepting it- so would not include any link information, and is only usable for basic info
|
||||
pub async fn get_from_link(
|
||||
link: String,
|
||||
) -> crate::Result<SharedProfileResponse> {
|
||||
let state = crate::State::get().await?;
|
||||
|
||||
let creds = state.credentials.read().await;
|
||||
let creds = creds
|
||||
.0
|
||||
.as_ref()
|
||||
.ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
|
||||
|
||||
let response = REQWEST_CLIENT
|
||||
.get(format!("{MODRINTH_API_URL_INTERNAL}client/share/{link}"))
|
||||
.header("Authorization", &creds.session)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
let profile = response.json::<SharedProfileResponse>().await?;
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Configuration structs
|
||||
|
||||
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2/";
|
||||
pub const MODRINTH_API_URL: &str = "https://staging-api.modrinth.com/v2/";
|
||||
pub const MODRINTH_API_URL_INTERNAL: &str =
|
||||
"https://staging-api.modrinth.com/_internal/";
|
||||
|
||||
@@ -229,6 +229,9 @@ pub enum CommandPayload {
|
||||
// run or install .mrpack
|
||||
path: PathBuf,
|
||||
},
|
||||
OpenSharedProfile {
|
||||
link: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
|
||||
@@ -247,13 +247,16 @@ impl State {
|
||||
tokio::task::spawn(async {
|
||||
if let Ok(state) = crate::State::get().await {
|
||||
if !*state.offline.read().await {
|
||||
// Resolve update_creds first, as it might affect calls made by other updates
|
||||
let _ = CredentialsStore::update_creds().await;
|
||||
|
||||
let res1 = Profiles::update_modrinth_versions();
|
||||
let res2 = Tags::update();
|
||||
let res3 = Metadata::update();
|
||||
let res4 = Profiles::update_projects();
|
||||
let res5 = Settings::update_java();
|
||||
let res6 = CredentialsStore::update_creds();
|
||||
let res7 = Settings::update_default_user();
|
||||
let res6 = Settings::update_default_user();
|
||||
let res7 = Profiles::update_shared_projects();
|
||||
|
||||
let _ = join!(res1, res2, res3, res4, res5, res6, res7);
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ pub struct ModrinthAuthFlow {
|
||||
impl ModrinthAuthFlow {
|
||||
pub async fn new(provider: &str) -> crate::Result<Self> {
|
||||
let (socket, _) = async_tungstenite::tokio::connect_async(format!(
|
||||
"wss://api.modrinth.com/v2/auth/ws?provider={provider}"
|
||||
"wss://staging-api.modrinth.com/v2/auth/ws?provider={provider}"
|
||||
))
|
||||
.await?;
|
||||
Ok(Self { socket })
|
||||
@@ -209,7 +209,7 @@ async fn get_result_from_res(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct Session {
|
||||
session: String,
|
||||
}
|
||||
@@ -351,6 +351,7 @@ pub async fn refresh_credentials(
|
||||
}
|
||||
}
|
||||
|
||||
credentials_store.save().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@ use crate::data::DirectoryInfo;
|
||||
use crate::event::emit::{emit_profile, emit_warning};
|
||||
use crate::event::ProfilePayloadType;
|
||||
use crate::prelude::JavaVersion;
|
||||
use crate::shared_profile::SharedModpackFileUpdate;
|
||||
use crate::state::projects::Project;
|
||||
use crate::state::{ModrinthVersion, ProjectMetadata, ProjectType};
|
||||
use crate::util::fetch::{
|
||||
fetch, fetch_json, write, write_cached_icon, IoSemaphore,
|
||||
};
|
||||
use crate::util::io::{self, IOError};
|
||||
use crate::State;
|
||||
use crate::{shared_profile, State};
|
||||
use chrono::{DateTime, Utc};
|
||||
use daedalus::get_hash;
|
||||
use daedalus::modded::LoaderVersion;
|
||||
@@ -164,7 +165,7 @@ impl ProjectPathId {
|
||||
|
||||
pub async fn get_full_path(
|
||||
&self,
|
||||
profile: ProfilePathId,
|
||||
profile: &ProfilePathId,
|
||||
) -> crate::Result<PathBuf> {
|
||||
let profile_dir = profile.get_full_path().await?;
|
||||
Ok(profile_dir.join(&self.0))
|
||||
@@ -209,7 +210,14 @@ pub struct Profile {
|
||||
pub hooks: Option<Hooks>,
|
||||
pub projects: HashMap<ProjectPathId, Project>,
|
||||
#[serde(default)]
|
||||
pub modrinth_update_version: Option<String>,
|
||||
pub sync_update_version: Option<ProfileUpdateData>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum ProfileUpdateData {
|
||||
ModrinthModpack(String),
|
||||
SharedProfile(SharedModpackFileUpdate),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
@@ -244,12 +252,19 @@ pub struct ProfileMetadata {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct LinkedData {
|
||||
pub project_id: Option<String>,
|
||||
pub version_id: Option<String>,
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum LinkedData {
|
||||
ModrinthModpack {
|
||||
project_id: Option<String>,
|
||||
version_id: Option<String>,
|
||||
#[serde(default = "default_locked")]
|
||||
locked: Option<bool>,
|
||||
},
|
||||
|
||||
#[serde(default = "default_locked")]
|
||||
pub locked: Option<bool>,
|
||||
SharedProfile {
|
||||
profile_id: String,
|
||||
is_owner: bool,
|
||||
},
|
||||
}
|
||||
|
||||
// Called if linked_data is present but locked is not
|
||||
@@ -344,7 +359,7 @@ impl Profile {
|
||||
resolution: None,
|
||||
fullscreen: None,
|
||||
hooks: None,
|
||||
modrinth_update_version: None,
|
||||
sync_update_version: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -424,6 +439,10 @@ impl Profile {
|
||||
&creds,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Check existing shared profiles for updates
|
||||
tokio::task::spawn(Profiles::update_shared_projects());
|
||||
|
||||
drop(creds);
|
||||
|
||||
let mut new_profiles = state.profiles.write().await;
|
||||
@@ -580,6 +599,49 @@ impl Profile {
|
||||
Ok((path, version))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn add_project_bytes_directly(
|
||||
&self,
|
||||
relative_path: &Path,
|
||||
bytes: bytes::Bytes,
|
||||
) -> crate::Result<ProjectPathId> {
|
||||
let state = State::get().await?;
|
||||
let file_path = self.get_profile_full_path().await?.join(relative_path);
|
||||
let project_path_id = ProjectPathId::new(relative_path);
|
||||
write(&file_path, &bytes, &state.io_semaphore).await?;
|
||||
|
||||
let file_name = relative_path
|
||||
.file_name()
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Could not find file name for {:?}",
|
||||
relative_path
|
||||
))
|
||||
})?
|
||||
.to_string_lossy();
|
||||
|
||||
let hash = get_hash(bytes).await?;
|
||||
{
|
||||
let mut profiles = state.profiles.write().await;
|
||||
|
||||
if let Some(profile) = profiles.0.get_mut(&self.profile_id()) {
|
||||
profile.projects.insert(
|
||||
project_path_id.clone(),
|
||||
Project {
|
||||
sha512: hash,
|
||||
disabled: false,
|
||||
metadata: ProjectMetadata::Unknown,
|
||||
file_name: file_name.to_string(),
|
||||
},
|
||||
);
|
||||
profile.metadata.date_modified = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(project_path_id)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, bytes))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn add_project_bytes(
|
||||
@@ -872,19 +934,23 @@ impl Profiles {
|
||||
pub async fn update_modrinth_versions() {
|
||||
let res = async {
|
||||
let state = State::get().await?;
|
||||
|
||||
// First, we'll fetch updates for all Modrinth modpacks
|
||||
// Temporarily store all profiles that have modrinth linked data
|
||||
let mut modrinth_updatables: Vec<(ProfilePathId, String)> =
|
||||
Vec::new();
|
||||
{
|
||||
let profiles = state.profiles.read().await;
|
||||
for (profile_path, profile) in profiles.0.iter() {
|
||||
if let Some(linked_data) = &profile.metadata.linked_data {
|
||||
if let Some(linked_project) = &linked_data.project_id {
|
||||
modrinth_updatables.push((
|
||||
profile_path.clone(),
|
||||
linked_project.clone(),
|
||||
));
|
||||
}
|
||||
if let Some(LinkedData::ModrinthModpack {
|
||||
project_id: Some(ref linked_project),
|
||||
..
|
||||
}) = &profile.metadata.linked_data
|
||||
{
|
||||
modrinth_updatables.push((
|
||||
profile_path.clone(),
|
||||
linked_project.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -922,10 +988,12 @@ impl Profiles {
|
||||
.contains(&loader.as_api_str().to_string())
|
||||
});
|
||||
if let Some(recent_version) = recent_version {
|
||||
profile.modrinth_update_version =
|
||||
Some(recent_version.id.clone());
|
||||
profile.sync_update_version =
|
||||
Some(ProfileUpdateData::ModrinthModpack(
|
||||
recent_version.id.clone(),
|
||||
));
|
||||
} else {
|
||||
profile.modrinth_update_version = None;
|
||||
profile.sync_update_version = None;
|
||||
}
|
||||
}
|
||||
drop(new_profiles);
|
||||
@@ -953,6 +1021,76 @@ impl Profiles {
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn update_shared_projects() {
|
||||
let res = async {
|
||||
let state = State::get().await?;
|
||||
// Next, get updates for all shared profiles
|
||||
// Get SharedProfiles for all available
|
||||
let shared_profiles = shared_profile::get_all().await?;
|
||||
let mut update_profiles = HashMap::new();
|
||||
{
|
||||
let profiles = state.profiles.read().await;
|
||||
for (profile_path, profile) in profiles.0.iter() {
|
||||
if let Some(LinkedData::SharedProfile {
|
||||
profile_id, ..
|
||||
}) = &profile.metadata.linked_data
|
||||
{
|
||||
if let Some(shared_profile) =
|
||||
shared_profiles.iter().find(|x| x.id == *profile_id)
|
||||
{
|
||||
// Check for update
|
||||
let update = shared_profile::check_updated(
|
||||
profile_path,
|
||||
shared_profile,
|
||||
)
|
||||
.await?;
|
||||
update_profiles
|
||||
.insert(profile_path.clone(), update);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut new_profiles = state.profiles.write().await;
|
||||
for (profile_path, update) in update_profiles.iter() {
|
||||
if let Some(profile) = new_profiles.0.get_mut(profile_path)
|
||||
{
|
||||
profile.sync_update_version = Some(
|
||||
ProfileUpdateData::SharedProfile(update.clone()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
let profiles = state.profiles.read().await;
|
||||
profiles.sync().await?;
|
||||
|
||||
for (profile_path, _) in update_profiles.iter() {
|
||||
let Some(profile) = profiles.0.get(profile_path) else {
|
||||
continue;
|
||||
};
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile_path,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok::<(), crate::Error>(())
|
||||
}
|
||||
.await;
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
tracing::warn!("Unable to update modrinth versions: {err}")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, profile))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn insert(
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// IO error
|
||||
// A wrapper around the tokio IO functions that adds the path to the error message, instead of the uninformative std::io::Error.
|
||||
|
||||
use std::{path::Path, io::Write};
|
||||
use std::{io::Write, path::Path};
|
||||
|
||||
use tempfile::NamedTempFile;
|
||||
use tauri::async_runtime::spawn_blocking;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum IOError {
|
||||
@@ -137,12 +137,13 @@ fn sync_write(
|
||||
data: impl AsRef<[u8]>,
|
||||
path: impl AsRef<Path>,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let mut tempfile = NamedTempFile::new_in(path.as_ref().parent().ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"could not get parent directory for temporary file",
|
||||
)
|
||||
})?)?;
|
||||
let mut tempfile =
|
||||
NamedTempFile::new_in(path.as_ref().parent().ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"could not get parent directory for temporary file",
|
||||
)
|
||||
})?)?;
|
||||
tempfile.write_all(data.as_ref())?;
|
||||
let tmp_path = tempfile.into_temp_path();
|
||||
let path = path.as_ref();
|
||||
|
||||
@@ -14,12 +14,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.3.0",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
"dayjs": "^1.11.7",
|
||||
"floating-vue": "^2.0.0-beta.20",
|
||||
"mixpanel-browser": "^2.47.0",
|
||||
"ofetch": "^1.0.1",
|
||||
"omorphia": "^0.7.3",
|
||||
"omorphia": "^0.4.38",
|
||||
"pinia": "^2.1.3",
|
||||
"qrcode.vue": "^3.4.0",
|
||||
"tauri-plugin-window-state-api": "github:tauri-apps/tauri-plugin-window-state#v1",
|
||||
|
||||
396
theseus_gui/pnpm-lock.yaml
generated
396
theseus_gui/pnpm-lock.yaml
generated
@@ -8,9 +8,6 @@ dependencies:
|
||||
'@tauri-apps/api':
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
'@vintl/vintl':
|
||||
specifier: ^4.4.1
|
||||
version: 4.4.1(vue@3.3.4)
|
||||
dayjs:
|
||||
specifier: ^1.11.7
|
||||
version: 1.11.7
|
||||
@@ -24,8 +21,8 @@ dependencies:
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1
|
||||
omorphia:
|
||||
specifier: ^0.7.3
|
||||
version: 0.7.3(vue@3.3.4)
|
||||
specifier: ^0.4.38
|
||||
version: 0.4.38
|
||||
pinia:
|
||||
specifier: ^2.1.3
|
||||
version: 2.1.3(vue@3.3.4)
|
||||
@@ -34,7 +31,7 @@ dependencies:
|
||||
version: 3.4.0(vue@3.3.4)
|
||||
tauri-plugin-window-state-api:
|
||||
specifier: github:tauri-apps/tauri-plugin-window-state#v1
|
||||
version: github.com/tauri-apps/tauri-plugin-window-state/91fafb628cd0c83ad52bdf9029cad212381f740a
|
||||
version: github.com/tauri-apps/tauri-plugin-window-state/5ea9eb0d4a9affd17269f92c0085935046be3f4a
|
||||
vite-svg-loader:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
@@ -108,118 +105,6 @@ packages:
|
||||
'@babel/helper-validator-identifier': 7.19.1
|
||||
to-fast-properties: 2.0.0
|
||||
|
||||
/@braw/async-computed@5.0.2(vue@3.3.4):
|
||||
resolution: {integrity: sha512-fThqjZBTPvWtbD90Nkd4IldN7dpCkxfvthuk12ZBjkPPjh+wuRGi3HYiUqUSAOOVS0NHSxpsQFfg+qO275FtYA==}
|
||||
peerDependencies:
|
||||
vue: ^2.7 || ^3.2.45
|
||||
dependencies:
|
||||
vue: 3.3.4
|
||||
dev: false
|
||||
|
||||
/@codemirror/autocomplete@6.12.0(@codemirror/language@6.10.0)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.1):
|
||||
resolution: {integrity: sha512-r4IjdYFthwbCQyvqnSlx0WBHRHi8nBvU+WjJxFUij81qsBfhNudf/XKKmmC2j3m0LaOYUQTf3qiEK1J8lO1sdg==}
|
||||
peerDependencies:
|
||||
'@codemirror/language': ^6.0.0
|
||||
'@codemirror/state': ^6.0.0
|
||||
'@codemirror/view': ^6.0.0
|
||||
'@lezer/common': ^1.0.0
|
||||
dependencies:
|
||||
'@codemirror/language': 6.10.0
|
||||
'@codemirror/state': 6.4.0
|
||||
'@codemirror/view': 6.23.0
|
||||
'@lezer/common': 1.2.1
|
||||
dev: false
|
||||
|
||||
/@codemirror/commands@6.3.3:
|
||||
resolution: {integrity: sha512-dO4hcF0fGT9tu1Pj1D2PvGvxjeGkbC6RGcZw6Qs74TH+Ed1gw98jmUgd2axWvIZEqTeTuFrg1lEB1KV6cK9h1A==}
|
||||
dependencies:
|
||||
'@codemirror/language': 6.10.0
|
||||
'@codemirror/state': 6.4.0
|
||||
'@codemirror/view': 6.23.0
|
||||
'@lezer/common': 1.2.1
|
||||
dev: false
|
||||
|
||||
/@codemirror/lang-css@6.2.1(@codemirror/view@6.23.0):
|
||||
resolution: {integrity: sha512-/UNWDNV5Viwi/1lpr/dIXJNWiwDxpw13I4pTUAsNxZdg6E0mI2kTQb0P2iHczg1Tu+H4EBgJR+hYhKiHKko7qg==}
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.12.0(@codemirror/language@6.10.0)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.1)
|
||||
'@codemirror/language': 6.10.0
|
||||
'@codemirror/state': 6.4.0
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/css': 1.1.7
|
||||
transitivePeerDependencies:
|
||||
- '@codemirror/view'
|
||||
dev: false
|
||||
|
||||
/@codemirror/lang-html@6.4.7:
|
||||
resolution: {integrity: sha512-y9hWSSO41XlcL4uYwWyk0lEgTHcelWWfRuqmvcAmxfCs0HNWZdriWo/EU43S63SxEZpc1Hd50Itw7ktfQvfkUg==}
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.12.0(@codemirror/language@6.10.0)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.1)
|
||||
'@codemirror/lang-css': 6.2.1(@codemirror/view@6.23.0)
|
||||
'@codemirror/lang-javascript': 6.2.1
|
||||
'@codemirror/language': 6.10.0
|
||||
'@codemirror/state': 6.4.0
|
||||
'@codemirror/view': 6.23.0
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/css': 1.1.7
|
||||
'@lezer/html': 1.3.8
|
||||
dev: false
|
||||
|
||||
/@codemirror/lang-javascript@6.2.1:
|
||||
resolution: {integrity: sha512-jlFOXTejVyiQCW3EQwvKH0m99bUYIw40oPmFjSX2VS78yzfe0HELZ+NEo9Yfo1MkGRpGlj3Gnu4rdxV1EnAs5A==}
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.12.0(@codemirror/language@6.10.0)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.1)
|
||||
'@codemirror/language': 6.10.0
|
||||
'@codemirror/lint': 6.4.2
|
||||
'@codemirror/state': 6.4.0
|
||||
'@codemirror/view': 6.23.0
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/javascript': 1.4.13
|
||||
dev: false
|
||||
|
||||
/@codemirror/lang-markdown@6.2.4:
|
||||
resolution: {integrity: sha512-UghkA1vSMs8bT7RSZM6vsIocigyah2bV00eRQuZy76401UmFZdsTsbQNBGdyxRQDOLeEvF5iFwap0BM8LKyd+g==}
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.12.0(@codemirror/language@6.10.0)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.1)
|
||||
'@codemirror/lang-html': 6.4.7
|
||||
'@codemirror/language': 6.10.0
|
||||
'@codemirror/state': 6.4.0
|
||||
'@codemirror/view': 6.23.0
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/markdown': 1.2.0
|
||||
dev: false
|
||||
|
||||
/@codemirror/language@6.10.0:
|
||||
resolution: {integrity: sha512-2vaNn9aPGCRFKWcHPFksctzJ8yS5p7YoaT+jHpc0UGKzNuAIx4qy6R5wiqbP+heEEdyaABA582mNqSHzSoYdmg==}
|
||||
dependencies:
|
||||
'@codemirror/state': 6.4.0
|
||||
'@codemirror/view': 6.23.0
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/highlight': 1.2.0
|
||||
'@lezer/lr': 1.3.14
|
||||
style-mod: 4.1.0
|
||||
dev: false
|
||||
|
||||
/@codemirror/lint@6.4.2:
|
||||
resolution: {integrity: sha512-wzRkluWb1ptPKdzlsrbwwjYCPLgzU6N88YBAmlZi8WFyuiEduSd05MnJYNogzyc8rPK7pj6m95ptUApc8sHKVA==}
|
||||
dependencies:
|
||||
'@codemirror/state': 6.4.0
|
||||
'@codemirror/view': 6.23.0
|
||||
crelt: 1.0.6
|
||||
dev: false
|
||||
|
||||
/@codemirror/state@6.4.0:
|
||||
resolution: {integrity: sha512-hm8XshYj5Fo30Bb922QX9hXB/bxOAVH+qaqHBzw5TKa72vOeslyGwd4X8M0c1dJ9JqxlaMceOQ8RsL9tC7gU0A==}
|
||||
dev: false
|
||||
|
||||
/@codemirror/view@6.23.0:
|
||||
resolution: {integrity: sha512-/51px9N4uW8NpuWkyUX+iam5+PM6io2fm+QmRnzwqBy5v/pwGg9T0kILFtYeum8hjuvENtgsGNKluOfqIICmeQ==}
|
||||
dependencies:
|
||||
'@codemirror/state': 6.4.0
|
||||
style-mod: 4.1.0
|
||||
w3c-keyname: 2.2.8
|
||||
dev: false
|
||||
|
||||
/@esbuild/android-arm64@0.17.19:
|
||||
resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -465,79 +350,6 @@ packages:
|
||||
'@floating-ui/core': 0.3.1
|
||||
dev: false
|
||||
|
||||
/@formatjs/ecma402-abstract@1.18.2:
|
||||
resolution: {integrity: sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==}
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.5.4
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/@formatjs/fast-memoize@2.2.0:
|
||||
resolution: {integrity: sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==}
|
||||
dependencies:
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/@formatjs/icu-messageformat-parser@2.7.5:
|
||||
resolution: {integrity: sha512-zCB53HdGDibh6/2ISEN3TGsFQruQ6gGKMFV94qHNyVrs0tNO6ncKhV0vq0n3Ydz8ipIQ2GaYAvfCoimNOVvKqA==}
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 1.18.2
|
||||
'@formatjs/icu-skeleton-parser': 1.7.2
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/@formatjs/icu-skeleton-parser@1.7.2:
|
||||
resolution: {integrity: sha512-nlIXVv280bjGW3ail5Np1+xgGKBnMhwQQIivgbk9xX0af8ESQO+y2VW9TOY7mCrs3WH786uVpZlLimXAlXH7SA==}
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 1.18.2
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/@formatjs/intl-displaynames@6.6.6:
|
||||
resolution: {integrity: sha512-Dg5URSjx0uzF8VZXtHb6KYZ6LFEEhCbAbKoYChYHEOnMFTw/ZU3jIo/NrujzQD2EfKPgQzIq73LOUvW6Z/LpFA==}
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 1.18.2
|
||||
'@formatjs/intl-localematcher': 0.5.4
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/@formatjs/intl-listformat@7.5.5:
|
||||
resolution: {integrity: sha512-XoI52qrU6aBGJC9KJddqnacuBbPlb/bXFN+lIFVFhQ1RnFHpzuFrlFdjD9am2O7ZSYsyqzYRpkVcXeT1GHkwDQ==}
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 1.18.2
|
||||
'@formatjs/intl-localematcher': 0.5.4
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/@formatjs/intl-localematcher@0.4.2:
|
||||
resolution: {integrity: sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==}
|
||||
dependencies:
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/@formatjs/intl-localematcher@0.5.4:
|
||||
resolution: {integrity: sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==}
|
||||
dependencies:
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/@formatjs/intl@2.9.11:
|
||||
resolution: {integrity: sha512-wJF5GKuopgeKy75e11JPjueC/XKAxrOndqVEZqg5zDrGuxALUD6Vo/x+oDTQwVZYf2zJnEzqZlUGtv5gSi/ChQ==}
|
||||
peerDependencies:
|
||||
typescript: ^4.7 || 5
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 1.18.2
|
||||
'@formatjs/fast-memoize': 2.2.0
|
||||
'@formatjs/icu-messageformat-parser': 2.7.5
|
||||
'@formatjs/intl-displaynames': 6.6.6
|
||||
'@formatjs/intl-listformat': 7.5.5
|
||||
intl-messageformat: 10.5.10
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/@humanwhocodes/config-array@0.11.8:
|
||||
resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@@ -561,53 +373,6 @@ packages:
|
||||
/@jridgewell/sourcemap-codec@1.4.15:
|
||||
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
|
||||
|
||||
/@lezer/common@1.2.1:
|
||||
resolution: {integrity: sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==}
|
||||
dev: false
|
||||
|
||||
/@lezer/css@1.1.7:
|
||||
resolution: {integrity: sha512-7BlFFAKNn/b39jJLrhdLSX5A2k56GIJvyLqdmm7UU+7XvequY084iuKDMAEhAmAzHnwDE8FK4OQtsIUssW91tg==}
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/highlight': 1.2.0
|
||||
'@lezer/lr': 1.3.14
|
||||
dev: false
|
||||
|
||||
/@lezer/highlight@1.2.0:
|
||||
resolution: {integrity: sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==}
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.1
|
||||
dev: false
|
||||
|
||||
/@lezer/html@1.3.8:
|
||||
resolution: {integrity: sha512-EXseJ3pUzWxE6XQBQdqWHZqqlGQRSuNMBcLb6mZWS2J2v+QZhOObD+3ZIKIcm59ntTzyor4LqFTb72iJc3k23Q==}
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/highlight': 1.2.0
|
||||
'@lezer/lr': 1.3.14
|
||||
dev: false
|
||||
|
||||
/@lezer/javascript@1.4.13:
|
||||
resolution: {integrity: sha512-5IBr8LIO3xJdJH1e9aj/ZNLE4LSbdsx25wFmGRAZsj2zSmwAYjx26JyU/BYOCpRQlu1jcv1z3vy4NB9+UkfRow==}
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/highlight': 1.2.0
|
||||
'@lezer/lr': 1.3.14
|
||||
dev: false
|
||||
|
||||
/@lezer/lr@1.3.14:
|
||||
resolution: {integrity: sha512-z5mY4LStlA3yL7aHT/rqgG614cfcvklS+8oFRFBYrs4YaWLJyKKM4+nN6KopToX0o9Hj6zmH6M5kinOYuy06ug==}
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.1
|
||||
dev: false
|
||||
|
||||
/@lezer/markdown@1.2.0:
|
||||
resolution: {integrity: sha512-d7MwsfAukZJo1GpPrcPGa3MxaFFOqNp0gbqF+3F7pTeNDOgeJN1muXzx1XXDPt+Ac+/voCzsH7qXqnn+xReG/g==}
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/highlight': 1.2.0
|
||||
dev: false
|
||||
|
||||
/@nodelib/fs.scandir@2.1.5:
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -654,8 +419,8 @@ packages:
|
||||
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
|
||||
dev: false
|
||||
|
||||
/@tauri-apps/api@1.5.3:
|
||||
resolution: {integrity: sha512-zxnDjHHKjOsrIzZm6nO5Xapb/BxqUq1tc7cGkFXsFkGTsSWgCPH1D8mm0XS9weJY2OaR73I3k3S+b7eSzJDfqA==}
|
||||
/@tauri-apps/api@1.4.0:
|
||||
resolution: {integrity: sha512-Jd6HPoTM1PZSFIzq7FB8VmMu3qSSyo/3lSwLpoapW+lQ41CL5Dow2KryLg+gyazA/58DRWI9vu/XpEeHK4uMdw==}
|
||||
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
|
||||
dev: false
|
||||
|
||||
@@ -776,21 +541,6 @@ packages:
|
||||
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
|
||||
dev: true
|
||||
|
||||
/@vintl/vintl@4.4.1(vue@3.3.4):
|
||||
resolution: {integrity: sha512-1fAnK1Ru4GlUH6v2UPqPMFXvatiZuDlgF3GBrUYDBvs4mzg+j3cmH9GgX7DqBtpRLI1iqcoQF10cnJs/e/0Dvw==}
|
||||
peerDependencies:
|
||||
vue: ^3.2.47
|
||||
dependencies:
|
||||
'@braw/async-computed': 5.0.2(vue@3.3.4)
|
||||
'@formatjs/icu-messageformat-parser': 2.7.5
|
||||
'@formatjs/intl': 2.9.11
|
||||
'@formatjs/intl-localematcher': 0.4.2
|
||||
intl-messageformat: 10.5.10
|
||||
vue: 3.3.4
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
dev: false
|
||||
|
||||
/@vitejs/plugin-vue@4.2.3(vite@4.3.9)(vue@3.3.4):
|
||||
resolution: {integrity: sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
@@ -879,10 +629,6 @@ packages:
|
||||
/@vue/shared@3.3.4:
|
||||
resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==}
|
||||
|
||||
/@yr/monotone-cubic-spline@1.0.3:
|
||||
resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==}
|
||||
dev: false
|
||||
|
||||
/acorn-jsx@5.3.2(acorn@8.8.2):
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
@@ -926,18 +672,6 @@ packages:
|
||||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/apexcharts@3.45.1:
|
||||
resolution: {integrity: sha512-pPjj/SA6dfPvR/IKRZF0STdfBGpBh3WRt7K0DFuW9P8erypYkX17EHu3/molPRfo2zSiQwTVpshHC5ncysqfkA==}
|
||||
dependencies:
|
||||
'@yr/monotone-cubic-spline': 1.0.3
|
||||
svg.draggable.js: 2.2.2
|
||||
svg.easing.js: 2.0.0
|
||||
svg.filter.js: 2.0.2
|
||||
svg.pathmorphing.js: 0.1.3
|
||||
svg.resize.js: 1.4.3
|
||||
svg.select.js: 3.0.1
|
||||
dev: false
|
||||
|
||||
/argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
@@ -992,7 +726,7 @@ packages:
|
||||
normalize-path: 3.0.0
|
||||
readdirp: 3.6.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/color-convert@2.0.1:
|
||||
@@ -1019,10 +753,6 @@ packages:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
dev: true
|
||||
|
||||
/crelt@1.0.6:
|
||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||
dev: false
|
||||
|
||||
/cross-spawn@7.0.3:
|
||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -1376,8 +1106,8 @@ packages:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
dev: true
|
||||
|
||||
/fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
/fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
@@ -1463,15 +1193,6 @@ packages:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
dev: true
|
||||
|
||||
/intl-messageformat@10.5.10:
|
||||
resolution: {integrity: sha512-3yzwX6t/my9WRtNiqP05r+/UkpWxwstQiwaHAiuHmDRt7ykzWJ+nceOVjNLZYYWGiSltY+C+Likd8OIVkASepw==}
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 1.18.2
|
||||
'@formatjs/fast-memoize': 2.2.0
|
||||
'@formatjs/icu-messageformat-parser': 2.7.5
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/is-binary-path@2.1.0:
|
||||
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1634,17 +1355,9 @@ packages:
|
||||
ufo: 1.1.2
|
||||
dev: false
|
||||
|
||||
/omorphia@0.7.3(vue@3.3.4):
|
||||
resolution: {integrity: sha512-Xk9o3xk/rFuZeR0LLoJNbOEvnQjAh6wOTZrksgCDAVuX30cSnpuihI9lsZNlH+4V1TXOB5FpVSHBEA/+LGzwHQ==}
|
||||
peerDependencies:
|
||||
vue: ^3.3.4
|
||||
/omorphia@0.4.38:
|
||||
resolution: {integrity: sha512-V0vEarmAart6Gf5WuPUZ58TuIiQf7rI5HJpmYU7FVbtdvZ3q08VqyKZflCddbeBSFQ4/N+A+sNr/ELf/jz+Cug==}
|
||||
dependencies:
|
||||
'@codemirror/commands': 6.3.3
|
||||
'@codemirror/lang-markdown': 6.2.4
|
||||
'@codemirror/language': 6.10.0
|
||||
'@codemirror/state': 6.4.0
|
||||
'@codemirror/view': 6.23.0
|
||||
apexcharts: 3.45.1
|
||||
dayjs: 1.11.7
|
||||
floating-vue: 2.0.0-beta.20(vue@3.3.4)
|
||||
highlight.js: 11.8.0
|
||||
@@ -1653,7 +1366,6 @@ packages:
|
||||
vue: 3.3.4
|
||||
vue-router: 4.2.1(vue@3.3.4)
|
||||
vue-select: 4.0.0-beta.6(vue@3.3.4)
|
||||
vue3-apexcharts: 1.4.4(apexcharts@3.45.1)(vue@3.3.4)
|
||||
xss: 1.0.14
|
||||
dev: false
|
||||
|
||||
@@ -1809,7 +1521,7 @@ packages:
|
||||
engines: {node: '>=10.0.0'}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/rollup@3.23.0:
|
||||
@@ -1817,7 +1529,7 @@ packages:
|
||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/run-parallel@1.2.0:
|
||||
@@ -1877,10 +1589,6 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/style-mod@4.1.0:
|
||||
resolution: {integrity: sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA==}
|
||||
dev: false
|
||||
|
||||
/supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1888,60 +1596,6 @@ packages:
|
||||
has-flag: 4.0.0
|
||||
dev: true
|
||||
|
||||
/svg.draggable.js@2.2.2:
|
||||
resolution: {integrity: sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
svg.js: 2.7.1
|
||||
dev: false
|
||||
|
||||
/svg.easing.js@2.0.0:
|
||||
resolution: {integrity: sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
svg.js: 2.7.1
|
||||
dev: false
|
||||
|
||||
/svg.filter.js@2.0.2:
|
||||
resolution: {integrity: sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
svg.js: 2.7.1
|
||||
dev: false
|
||||
|
||||
/svg.js@2.7.1:
|
||||
resolution: {integrity: sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==}
|
||||
dev: false
|
||||
|
||||
/svg.pathmorphing.js@0.1.3:
|
||||
resolution: {integrity: sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
svg.js: 2.7.1
|
||||
dev: false
|
||||
|
||||
/svg.resize.js@1.4.3:
|
||||
resolution: {integrity: sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
svg.js: 2.7.1
|
||||
svg.select.js: 2.1.2
|
||||
dev: false
|
||||
|
||||
/svg.select.js@2.1.2:
|
||||
resolution: {integrity: sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
svg.js: 2.7.1
|
||||
dev: false
|
||||
|
||||
/svg.select.js@3.0.1:
|
||||
resolution: {integrity: sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
svg.js: 2.7.1
|
||||
dev: false
|
||||
|
||||
/svgo@3.0.2:
|
||||
resolution: {integrity: sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -1970,10 +1624,6 @@ packages:
|
||||
is-number: 7.0.0
|
||||
dev: true
|
||||
|
||||
/tslib@2.6.2:
|
||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||
dev: false
|
||||
|
||||
/type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -2054,7 +1704,7 @@ packages:
|
||||
rollup: 3.23.0
|
||||
sass: 1.62.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/vue-demi@0.14.5(vue@3.3.4):
|
||||
@@ -2139,16 +1789,6 @@ packages:
|
||||
vue-resize: 2.0.0-alpha.1(vue@3.3.4)
|
||||
dev: false
|
||||
|
||||
/vue3-apexcharts@1.4.4(apexcharts@3.45.1)(vue@3.3.4):
|
||||
resolution: {integrity: sha512-TH89uZrxGjaDvkaYAISvj8+k6Bf1rUKFillc8oJirs5XZEPiwM1ELKZQ786wz0rfPqkSHHny2lqqUCK7Rw+LcQ==}
|
||||
peerDependencies:
|
||||
apexcharts: '> 3.0.0'
|
||||
vue: '> 3.0.0'
|
||||
dependencies:
|
||||
apexcharts: 3.45.1
|
||||
vue: 3.3.4
|
||||
dev: false
|
||||
|
||||
/vue@3.3.4:
|
||||
resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==}
|
||||
dependencies:
|
||||
@@ -2158,10 +1798,6 @@ packages:
|
||||
'@vue/server-renderer': 3.3.4(vue@3.3.4)
|
||||
'@vue/shared': 3.3.4
|
||||
|
||||
/w3c-keyname@2.2.8:
|
||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||
dev: false
|
||||
|
||||
/which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -2202,10 +1838,10 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
github.com/tauri-apps/tauri-plugin-window-state/91fafb628cd0c83ad52bdf9029cad212381f740a:
|
||||
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-window-state/tar.gz/91fafb628cd0c83ad52bdf9029cad212381f740a}
|
||||
github.com/tauri-apps/tauri-plugin-window-state/5ea9eb0d4a9affd17269f92c0085935046be3f4a:
|
||||
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-window-state/tar.gz/5ea9eb0d4a9affd17269f92c0085935046be3f4a}
|
||||
name: tauri-plugin-window-state-api
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@tauri-apps/api': 1.5.3
|
||||
'@tauri-apps/api': 1.4.0
|
||||
dev: false
|
||||
|
||||
@@ -12,6 +12,7 @@ pub mod pack;
|
||||
pub mod process;
|
||||
pub mod profile;
|
||||
pub mod profile_create;
|
||||
pub mod profile_share;
|
||||
pub mod settings;
|
||||
pub mod tags;
|
||||
pub mod utils;
|
||||
|
||||
97
theseus_gui/src-tauri/src/api/profile_share.rs
Normal file
97
theseus_gui/src-tauri/src/api/profile_share.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use crate::api::Result;
|
||||
use theseus::{
|
||||
prelude::*,
|
||||
shared_profile::{SharedProfile, SharedProfileResponse},
|
||||
};
|
||||
|
||||
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
tauri::plugin::Builder::new("profile_share")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
profile_share_get_all,
|
||||
profile_share_install,
|
||||
profile_share_create,
|
||||
profile_share_inbound_sync,
|
||||
profile_share_outbound_sync,
|
||||
profile_share_generate_share_link,
|
||||
profile_share_accept_share_link,
|
||||
profile_share_remove_users,
|
||||
profile_share_remove_links,
|
||||
profile_share_get_link_id,
|
||||
])
|
||||
.build()
|
||||
}
|
||||
|
||||
// invoke('plugin:profile_share|profile_share_get_all',profile)
|
||||
#[tauri::command]
|
||||
pub async fn profile_share_get_all() -> Result<Vec<SharedProfile>> {
|
||||
let res = shared_profile::get_all().await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn profile_share_install(
|
||||
shared_profile_id: String,
|
||||
) -> Result<ProfilePathId> {
|
||||
let res = shared_profile::install(shared_profile_id).await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn profile_share_create(path: ProfilePathId) -> Result<()> {
|
||||
shared_profile::create(path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn profile_share_inbound_sync(path: ProfilePathId) -> Result<()> {
|
||||
shared_profile::inbound_sync(path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn profile_share_outbound_sync(path: ProfilePathId) -> Result<()> {
|
||||
shared_profile::outbound_sync(path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn profile_share_generate_share_link(
|
||||
path: ProfilePathId,
|
||||
) -> Result<String> {
|
||||
let res = shared_profile::generate_share_link(path).await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn profile_share_accept_share_link(link: String) -> Result<()> {
|
||||
shared_profile::accept_share_link(link).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Gets a shared profile from a share link
|
||||
// This is done without accepting it- so would not include any link information, and is only usable for basic info
|
||||
#[tauri::command]
|
||||
pub async fn profile_share_get_link_id(
|
||||
link: String,
|
||||
) -> Result<SharedProfileResponse> {
|
||||
let res = shared_profile::get_from_link(link).await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn profile_share_remove_users(
|
||||
path: ProfilePathId,
|
||||
users: Vec<String>,
|
||||
) -> Result<()> {
|
||||
shared_profile::remove_shared_profile_users(path, users).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn profile_share_remove_links(
|
||||
path: ProfilePathId,
|
||||
links: Vec<String>,
|
||||
) -> Result<()> {
|
||||
shared_profile::remove_shared_profile_links(path, links).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -20,7 +20,8 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
get_opening_command,
|
||||
await_sync,
|
||||
is_offline,
|
||||
refresh_offline
|
||||
refresh_offline,
|
||||
test_command,
|
||||
])
|
||||
.build()
|
||||
}
|
||||
@@ -159,6 +160,11 @@ pub async fn get_opening_command() -> Result<Option<CommandPayload>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn test_command(command: String) -> Result<()> {
|
||||
handle_command(command).await
|
||||
}
|
||||
|
||||
// helper function called when redirected by a weblink (ie: modrith://do-something) or when redirected by a .mrpack file (in which case its a filepath)
|
||||
// We hijack the deep link library (which also contains functionality for instance-checking)
|
||||
pub async fn handle_command(command: String) -> Result<()> {
|
||||
|
||||
@@ -139,6 +139,7 @@ fn main() {
|
||||
.plugin(api::process::init())
|
||||
.plugin(api::profile::init())
|
||||
.plugin(api::profile_create::init())
|
||||
.plugin(api::profile_share::init())
|
||||
.plugin(api::settings::init())
|
||||
.plugin(api::tags::init())
|
||||
.plugin(api::utils::init())
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { RouterView, RouterLink, useRouter, useRoute } from 'vue-router'
|
||||
import {
|
||||
HomeIcon,
|
||||
SearchIcon,
|
||||
LibraryIcon,
|
||||
PlusIcon,
|
||||
SettingsIcon,
|
||||
FileIcon,
|
||||
Button,
|
||||
Notifications,
|
||||
XIcon,
|
||||
Card,
|
||||
TextLogo,
|
||||
PlusIcon,
|
||||
Avatar,
|
||||
} from 'omorphia'
|
||||
|
||||
import { useLoading, useTheming } from '@/store/state'
|
||||
import { useInstances } from '@/store/instances'
|
||||
// import AccountsCard from './components/ui/AccountsCard.vue'
|
||||
import AccountDropdown from '@/components/ui/platform/AccountDropdown.vue'
|
||||
import AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
||||
import { get } from '@/helpers/settings'
|
||||
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
|
||||
@@ -28,42 +23,31 @@ import SplashScreen from '@/components/ui/SplashScreen.vue'
|
||||
import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator'
|
||||
import { handleError, useNotifications } from '@/store/notifications.js'
|
||||
import { offline_listener, command_listener, warning_listener } from '@/helpers/events.js'
|
||||
import {
|
||||
MinimizeIcon,
|
||||
MaximizeIcon,
|
||||
ChatIcon,
|
||||
ArrowLeftFromLineIcon,
|
||||
ArrowRightFromLineIcon,
|
||||
} from '@/assets/icons'
|
||||
import { MinimizeIcon, MaximizeIcon, ChatIcon } from '@/assets/icons'
|
||||
import { type } from '@tauri-apps/api/os'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { isDev, getOS, isOffline, showLauncherLogsFolder } from '@/helpers/utils.js'
|
||||
import {
|
||||
mixpanel_track,
|
||||
mixpanel_init,
|
||||
mixpanel_opt_out_tracking,
|
||||
mixpanel_is_loaded,
|
||||
} from '@/helpers/mixpanel.js'
|
||||
import { useDisableClicks } from '@/composables/click.js'
|
||||
import { openExternal } from '@/helpers/external.js'
|
||||
import { await_sync, check_safe_loading_bars_complete } from '@/helpers/state.js'
|
||||
import { install_from_file } from '@/helpers/pack.js'
|
||||
import { iconPathAsUrl } from '@/helpers/icon'
|
||||
|
||||
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
|
||||
import StickyTitleBar from '@/components/ui/tutorial/StickyTitleBar.vue'
|
||||
import OnboardingScreen from '@/components/ui/tutorial/OnboardingScreen.vue'
|
||||
|
||||
} from '@/helpers/mixpanel'
|
||||
import { saveWindowState, StateFlags } from 'tauri-plugin-window-state-api'
|
||||
import { getVersion } from '@tauri-apps/api/app'
|
||||
import { window as TauriWindow } from '@tauri-apps/api'
|
||||
import { TauriEvent } from '@tauri-apps/api/event'
|
||||
import { await_sync, check_safe_loading_bars_complete } from './helpers/state'
|
||||
import { confirm } from '@tauri-apps/api/dialog'
|
||||
import { type } from '@tauri-apps/api/os'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
|
||||
import AcceptSharedProfileModal from '@/components/ui/AcceptSharedProfileModal.vue'
|
||||
import StickyTitleBar from '@/components/ui/tutorial/StickyTitleBar.vue'
|
||||
import OnboardingScreen from '@/components/ui/tutorial/OnboardingScreen.vue'
|
||||
import { install_from_file } from './helpers/pack'
|
||||
|
||||
const themeStore = useTheming()
|
||||
const urlModal = ref(null)
|
||||
|
||||
const sharedProfileConfirmModal = ref(null)
|
||||
const isLoading = ref(true)
|
||||
|
||||
const videoPlaying = ref(false)
|
||||
@@ -71,16 +55,11 @@ const offline = ref(false)
|
||||
const showOnboarding = ref(false)
|
||||
const nativeDecorations = ref(false)
|
||||
|
||||
const sidebarOpen = ref(false)
|
||||
|
||||
const onboardingVideo = ref()
|
||||
|
||||
const failureText = ref(null)
|
||||
const os = ref('')
|
||||
|
||||
const instances = useInstances()
|
||||
const { instancesByPlayed } = storeToRefs(instances)
|
||||
|
||||
defineExpose({
|
||||
initialize: async () => {
|
||||
isLoading.value = false
|
||||
@@ -177,12 +156,18 @@ const handleClose = async () => {
|
||||
await TauriWindow.getCurrent().close()
|
||||
}
|
||||
|
||||
const openSupport = () => openExternal(window, 'https://support.modrinth.com/')
|
||||
|
||||
onMounted(() => {
|
||||
return TauriWindow.getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async () => {
|
||||
await handleClose()
|
||||
const openSupport = async () => {
|
||||
window.__TAURI_INVOKE__('tauri', {
|
||||
__tauriModule: 'Shell',
|
||||
message: {
|
||||
cmd: 'open',
|
||||
path: 'https://discord.gg/modrinth',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
TauriWindow.getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async () => {
|
||||
await handleClose()
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
@@ -203,9 +188,47 @@ watch(notificationsWrapper, () => {
|
||||
notifications.setNotifs(notificationsWrapper.value)
|
||||
})
|
||||
|
||||
useDisableClicks(document, window)
|
||||
document.querySelector('body').addEventListener('click', function (e) {
|
||||
let target = e.target
|
||||
while (target != null) {
|
||||
if (target.matches('a')) {
|
||||
if (
|
||||
target.href &&
|
||||
['http://', 'https://', 'mailto:', 'tel:'].some((v) => target.href.startsWith(v)) &&
|
||||
!target.classList.contains('router-link-active') &&
|
||||
!target.href.startsWith('http://localhost') &&
|
||||
!target.href.startsWith('https://tauri.localhost')
|
||||
) {
|
||||
window.__TAURI_INVOKE__('tauri', {
|
||||
__tauriModule: 'Shell',
|
||||
message: {
|
||||
cmd: 'open',
|
||||
path: target.href,
|
||||
},
|
||||
})
|
||||
}
|
||||
e.preventDefault()
|
||||
break
|
||||
}
|
||||
target = target.parentElement
|
||||
}
|
||||
})
|
||||
|
||||
// const accounts = ref(null)
|
||||
document.querySelector('body').addEventListener('auxclick', function (e) {
|
||||
// disables middle click -> new tab
|
||||
if (e.button === 1) {
|
||||
e.preventDefault()
|
||||
// instead do a left click
|
||||
const event = new MouseEvent('click', {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
e.target.dispatchEvent(event)
|
||||
}
|
||||
})
|
||||
|
||||
const accounts = ref(null)
|
||||
|
||||
command_listener(async (e) => {
|
||||
if (e.event === 'RunMRPack') {
|
||||
@@ -216,15 +239,14 @@ command_listener(async (e) => {
|
||||
source: 'CreationModalFileDrop',
|
||||
})
|
||||
}
|
||||
} else if (e.event === 'OpenSharedProfile') {
|
||||
// Install a shared profile
|
||||
sharedProfileConfirmModal.value.show(e)
|
||||
} else {
|
||||
// Other commands are URL-based (deep linking)
|
||||
urlModal.value.show(e)
|
||||
}
|
||||
})
|
||||
|
||||
const toggleSidebar = () => {
|
||||
sidebarOpen.value = !sidebarOpen.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -271,6 +293,8 @@ const toggleSidebar = () => {
|
||||
|
||||
<div class="button-row push-right">
|
||||
<Button @click="showLauncherLogsFolder"><FileIcon />Open launcher logs</Button>
|
||||
|
||||
<Button @click="openSupport"><ChatIcon />Get support</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -278,35 +302,14 @@ const toggleSidebar = () => {
|
||||
<SplashScreen v-else-if="!videoPlaying && isLoading" app-loading />
|
||||
<OnboardingScreen v-else-if="showOnboarding" :finish="() => (showOnboarding = false)" />
|
||||
<div v-else class="container">
|
||||
<div
|
||||
class="nav-container"
|
||||
data-tauri-drag-region
|
||||
:class="`${sidebarOpen ? 'nav-container__open' : ''}`"
|
||||
:style="{
|
||||
'--sidebar-label-opacity': sidebarOpen ? '1' : '0',
|
||||
}"
|
||||
>
|
||||
<div class="pages-list">
|
||||
<div class="square-collapsed-space">
|
||||
<Button
|
||||
transparent
|
||||
icon-only
|
||||
class="collapsed-button non-collapse"
|
||||
@click="toggleSidebar"
|
||||
>
|
||||
<ArrowRightFromLineIcon v-if="!sidebarOpen" />
|
||||
<ArrowLeftFromLineIcon v-else />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pages-list">
|
||||
<!-- <suspense>
|
||||
<div class="nav-container">
|
||||
<div class="nav-section">
|
||||
<suspense>
|
||||
<AccountsCard ref="accounts" mode="small" />
|
||||
</suspense> -->
|
||||
</suspense>
|
||||
<div class="pages-list">
|
||||
<RouterLink v-tooltip="'Home'" to="/" class="btn icon-only collapsed-button">
|
||||
<HomeIcon />
|
||||
<span class="collapsed-button__label">Home</span>
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
v-tooltip="'Browse'"
|
||||
@@ -317,73 +320,35 @@ const toggleSidebar = () => {
|
||||
}"
|
||||
>
|
||||
<SearchIcon />
|
||||
<span class="collapsed-button__label">Browse</span>
|
||||
</RouterLink>
|
||||
<RouterLink v-tooltip="'Library'" to="/library" class="btn icon-only collapsed-button">
|
||||
<LibraryIcon />
|
||||
<span class="collapsed-button__label">Library</span>
|
||||
</RouterLink>
|
||||
<suspense>
|
||||
<Suspense>
|
||||
<InstanceCreationModal ref="installationModal" />
|
||||
</suspense>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider">
|
||||
<hr />
|
||||
</div>
|
||||
<div class="instances pages-list">
|
||||
<RouterLink
|
||||
v-for="instance in instancesByPlayed"
|
||||
:key="instance.id"
|
||||
v-tooltip="instance.metadata.name"
|
||||
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
||||
class="btn icon-only collapsed-button"
|
||||
>
|
||||
<Avatar
|
||||
class="collapsed-button__icon"
|
||||
:src="iconPathAsUrl(instance.metadata?.icon)"
|
||||
size="xs"
|
||||
/>
|
||||
<span class="collapsed-button__label">{{ instance.metadata.name }}</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="settings pages-list">
|
||||
<Button
|
||||
v-tooltip="'Get Support'"
|
||||
transparent
|
||||
icon-only
|
||||
class="page-item collapsed-button"
|
||||
@click="openSupport"
|
||||
>
|
||||
<ChatIcon />
|
||||
<span class="collapsed-button__label">Support</span>
|
||||
</Button>
|
||||
<RouterLink v-tooltip="'Settings'" to="/settings" class="btn icon-only collapsed-button">
|
||||
<SettingsIcon />
|
||||
<span class="collapsed-button__label">Settings</span>
|
||||
</RouterLink>
|
||||
<Button
|
||||
v-tooltip="'Create profile'"
|
||||
class="page-item collapsed-button"
|
||||
class="sleek-primary collapsed-button"
|
||||
icon-only
|
||||
:disabled="offline"
|
||||
@click="() => $refs.installationModal.show()"
|
||||
>
|
||||
<PlusIcon />
|
||||
<span class="collapsed-button__label">Create profile</span>
|
||||
</Button>
|
||||
<AccountDropdown />
|
||||
<RouterLink v-tooltip="'Settings'" to="/settings" class="btn icon-only collapsed-button">
|
||||
<SettingsIcon />
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="view">
|
||||
<div class="appbar-row">
|
||||
<!-- Top Bar -->
|
||||
<div data-tauri-drag-region class="appbar">
|
||||
<section class="navigation-controls">
|
||||
<router-link :to="'/'">
|
||||
<TextLogo class="logo" :animate="false" />
|
||||
</router-link>
|
||||
<Breadcrumbs after-logo data-tauri-drag-region />
|
||||
<Breadcrumbs data-tauri-drag-region />
|
||||
</section>
|
||||
<section class="mod-stats">
|
||||
<Suspense>
|
||||
@@ -415,7 +380,7 @@ const toggleSidebar = () => {
|
||||
<div class="router-view">
|
||||
<ModrinthLoadingIndicator
|
||||
offset-height="var(--appbar-height)"
|
||||
:offset-width="sidebarOpen ? 'var(--sidebar-open-width)' : 'var(--sidebar-width)'"
|
||||
offset-width="var(--sidebar-width)"
|
||||
/>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<template v-if="Component">
|
||||
@@ -428,6 +393,7 @@ const toggleSidebar = () => {
|
||||
</div>
|
||||
</div>
|
||||
<URLConfirmModal ref="urlModal" />
|
||||
<AcceptSharedProfileModal ref="sharedProfileConfirmModal" />
|
||||
<Notifications ref="notificationsWrapper" />
|
||||
</template>
|
||||
|
||||
@@ -437,18 +403,9 @@ const toggleSidebar = () => {
|
||||
transition: all ease-in-out 0.1s;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: calc(var(--appbar-height) - 2.5rem);
|
||||
width: auto;
|
||||
min-height: 100%;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
.navigation-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
.appbar-row {
|
||||
@@ -471,7 +428,7 @@ const toggleSidebar = () => {
|
||||
background-color: var(--color-raised-bg);
|
||||
color: var(--color-base);
|
||||
border-radius: 0;
|
||||
height: var(--appbar-height);
|
||||
height: 3.25rem;
|
||||
|
||||
&.close {
|
||||
&:hover,
|
||||
@@ -490,17 +447,8 @@ const toggleSidebar = () => {
|
||||
}
|
||||
|
||||
.container {
|
||||
--appbar-height: 4.5rem;
|
||||
|
||||
--sidebar-gap: 0.35rem;
|
||||
|
||||
--appbar-height: 3.25rem;
|
||||
--sidebar-width: 4.5rem;
|
||||
--sidebar-open-width: 15rem;
|
||||
--sidebar-padding: 0.75rem;
|
||||
|
||||
--sidebar-icon-size: 1.5rem;
|
||||
--sidebar-button-size: calc(var(--sidebar-width) - calc(var(--sidebar-padding) * 2));
|
||||
--sidebar-open-button-size: calc(var(--sidebar-open-width) - calc(var(--sidebar-padding) * 2));
|
||||
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
@@ -514,13 +462,11 @@ const toggleSidebar = () => {
|
||||
.appbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
flex-grow: 1;
|
||||
background: var(--color-raised-bg);
|
||||
text-align: center;
|
||||
padding: var(--gap-md);
|
||||
height: var(--appbar-height);
|
||||
height: 3.25rem;
|
||||
gap: var(--gap-sm);
|
||||
//no select
|
||||
user-select: none;
|
||||
@@ -529,7 +475,7 @@ const toggleSidebar = () => {
|
||||
|
||||
.router-view {
|
||||
width: 100%;
|
||||
height: calc(100% - var(--appbar-height));
|
||||
height: calc(100% - 3.125rem);
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
background-color: var(--color-bg);
|
||||
@@ -548,7 +494,7 @@ const toggleSidebar = () => {
|
||||
.appbar-failure {
|
||||
display: flex; /* Change to flex to align items horizontally */
|
||||
justify-content: flex-end; /* Align items to the right */
|
||||
height: var(--appbar-height);
|
||||
height: 3.25rem;
|
||||
//no select
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
@@ -588,75 +534,12 @@ const toggleSidebar = () => {
|
||||
.nav-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding-left: var(--sidebar-padding);
|
||||
padding-right: var(--sidebar-padding);
|
||||
padding-bottom: var(--sidebar-padding);
|
||||
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
height: 100%;
|
||||
|
||||
background-color: var(--color-raised-bg);
|
||||
box-shadow: var(--shadow-inset-sm), var(--shadow-floating);
|
||||
|
||||
transition: all ease-in-out 0.1s;
|
||||
|
||||
width: var(--sidebar-width);
|
||||
}
|
||||
|
||||
.nav-container__open {
|
||||
width: var(--sidebar-open-width);
|
||||
}
|
||||
|
||||
.square-collapsed-space {
|
||||
height: var(--appbar-height);
|
||||
width: 100%;
|
||||
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||
.square-collapsed-space {
|
||||
height: auto;
|
||||
padding-bottom: var(--gap-md);
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
|
||||
hr {
|
||||
background-color: var(--color-button-bg);
|
||||
border: none;
|
||||
color: var(--color-button-bg);
|
||||
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
margin-top: var(--sidebar-gap);
|
||||
// div should always have + 1 --sidebar-gap margin to the bottom to be equal
|
||||
margin-bottom: calc(var(--sidebar-gap) * 2);
|
||||
|
||||
padding-left: var(--sidebar-padding);
|
||||
padding-right: var(--sidebar-padding);
|
||||
}
|
||||
|
||||
.instances {
|
||||
flex: 1;
|
||||
|
||||
flex-flow: column wrap; // This hides any elements that aren't fully visible
|
||||
overflow: hidden;
|
||||
padding: var(--gap-md);
|
||||
}
|
||||
|
||||
.pages-list {
|
||||
@@ -664,12 +547,9 @@ const toggleSidebar = () => {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
|
||||
width: 100%;
|
||||
gap: 0.5rem;
|
||||
|
||||
gap: var(--sidebar-gap);
|
||||
|
||||
.page-item,
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -677,20 +557,19 @@ const toggleSidebar = () => {
|
||||
background: inherit;
|
||||
transition: all ease-in-out 0.1s;
|
||||
color: var(--color-base);
|
||||
box-shadow: none;
|
||||
|
||||
&.router-link-active {
|
||||
color: var(--color-brand);
|
||||
background: var(--color-brand-highlight);
|
||||
color: var(--color-contrast);
|
||||
background: var(--color-button-bg);
|
||||
box-shadow: var(--shadow-floating);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-button-bg);
|
||||
color: var(--color-contrast);
|
||||
background: var(--color-button-bg);
|
||||
}
|
||||
|
||||
&.router-link-active:hover {
|
||||
color: var(--color-brand);
|
||||
background: var(--color-brand-highlight);
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -700,45 +579,78 @@ const toggleSidebar = () => {
|
||||
}
|
||||
}
|
||||
|
||||
:deep {
|
||||
.non-collapse {
|
||||
width: var(--sidebar-button-size) !important;
|
||||
.collapsed-button {
|
||||
height: 3rem !important;
|
||||
width: 3rem !important;
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: none;
|
||||
|
||||
svg {
|
||||
width: 1.5rem !important;
|
||||
height: 1.5rem !important;
|
||||
max-width: 1.5rem !important;
|
||||
max-height: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.instance-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 70%;
|
||||
margin: 0.4rem;
|
||||
|
||||
p:nth-child(1) {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.collapsed-button {
|
||||
& > p {
|
||||
color: var(--color-base);
|
||||
margin: 0.8rem 0;
|
||||
font-size: 0.7rem;
|
||||
line-height: 0.8125rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.user-section {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 4.375rem;
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
// width: var(--sidebar-icon-size);
|
||||
height: var(--sidebar-button-size);
|
||||
width: 100%;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
padding: var(--sidebar-padding) !important;
|
||||
border-radius: 99999px;
|
||||
box-shadow: none;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
transition: all ease-in-out 0.1s;
|
||||
|
||||
.collapsed-button__icon,
|
||||
svg {
|
||||
width: var(--sidebar-icon-size) !important;
|
||||
height: var(--sidebar-icon-size) !important;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
border-radius: var(--radius-xs);
|
||||
}
|
||||
|
||||
.collapsed-button__label {
|
||||
word-spacing: normal; // Why is this even needed?
|
||||
opacity: var(--sidebar-label-opacity);
|
||||
transition: all ease-in-out 0.1s;
|
||||
}
|
||||
text-align: left;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.username {
|
||||
margin-bottom: 0.3rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.25rem;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 400;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.video {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left-from-line"><path d="m9 6-6 6 6 6"/><path d="M3 12h14"/><path d="M21 19V5"/></svg>
|
||||
|
Before Width: | Height: | Size: 294 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right-from-line"><path d="M3 5v14"/><path d="M21 12H7"/><path d="m15 18 6-6-6-6"/></svg>
|
||||
|
Before Width: | Height: | Size: 296 B |
@@ -12,5 +12,3 @@ export { default as NewInstanceImage } from './new-instance.svg'
|
||||
export { default as MenuIcon } from './menu.svg'
|
||||
export { default as BugIcon } from './bug.svg'
|
||||
export { default as ChatIcon } from './messages-square.svg'
|
||||
export { default as ArrowLeftFromLineIcon } from './arrow-left-from-line.svg'
|
||||
export { default as ArrowRightFromLineIcon } from './arrow-right-from-line.svg'
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// Quick snippets stolen from knossos project to make omorphia components fit
|
||||
|
||||
.btn,
|
||||
.button-base,
|
||||
a {
|
||||
// filter will change
|
||||
will-change: filter;
|
||||
}
|
||||
|
||||
.iconified-input {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
padding-left: 2.25rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:focus-within svg {
|
||||
color: var(--color-button-text-active);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
left: 0.75rem;
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
z-index: 1;
|
||||
|
||||
color: var(--color-button-text);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
XIcon,
|
||||
Button,
|
||||
formatCategoryHeader,
|
||||
ConfirmModal,
|
||||
ModalConfirm,
|
||||
} from 'omorphia'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -169,7 +169,7 @@ const filteredResults = computed(() => {
|
||||
})
|
||||
} else if (filters.value === 'Downloaded modpacks') {
|
||||
instances = instances.filter((instance) => {
|
||||
return instance.metadata?.linked_data
|
||||
return instance.metadata?.linked_data?.modrinth_modpack
|
||||
})
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ const filteredResults = computed(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<ConfirmModal
|
||||
<ModalConfirm
|
||||
ref="confirmModal"
|
||||
title="Are you sure you want to delete this instance?"
|
||||
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
||||
@@ -246,7 +246,7 @@ const filteredResults = computed(() => {
|
||||
<div class="iconified-input">
|
||||
<SearchIcon />
|
||||
<input v-model="search" type="text" placeholder="Search" class="search-input" />
|
||||
<Button class="r-btn" @click="() => (search = '')">
|
||||
<Button @click="() => (search = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
ExternalIcon,
|
||||
EyeIcon,
|
||||
ChevronRightIcon,
|
||||
ConfirmModal,
|
||||
ModalConfirm,
|
||||
} from 'omorphia'
|
||||
import Instance from '@/components/ui/Instance.vue'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
@@ -170,7 +170,7 @@ const handleOptionsClick = async (args) => {
|
||||
break
|
||||
case 'install': {
|
||||
const versions = await useFetch(
|
||||
`https://api.modrinth.com/v2/project/${args.item.project_id}/version`,
|
||||
`https://staging-api.modrinth.com/v2/project/${args.item.project_id}/version`,
|
||||
'project versions'
|
||||
)
|
||||
|
||||
@@ -227,7 +227,7 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfirmModal
|
||||
<ModalConfirm
|
||||
ref="deleteConfirmModal"
|
||||
title="Are you sure you want to delete this instance?"
|
||||
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
||||
|
||||
79
theseus_gui/src/components/ui/AcceptSharedProfileModal.vue
Normal file
79
theseus_gui/src/components/ui/AcceptSharedProfileModal.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<script setup>
|
||||
import { Modal, Button } from 'omorphia'
|
||||
import { ref } from 'vue'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { share_accept, share_install, share_get_link_id } from '@/helpers/shared_profiles.js'
|
||||
|
||||
const confirmModal = ref(null)
|
||||
const linkId = ref(null)
|
||||
const sharedProfile = ref(null)
|
||||
|
||||
defineExpose({
|
||||
async show(event) {
|
||||
console.log('showing accept shared profile modal', event)
|
||||
linkId.value = event.link
|
||||
sharedProfile.value = await share_get_link_id(linkId.value).catch(handleError)
|
||||
confirmModal.value.show()
|
||||
console.log('sharedProfile')
|
||||
},
|
||||
})
|
||||
|
||||
async function install() {
|
||||
confirmModal.value.hide()
|
||||
await share_accept(linkId.value).catch(handleError)
|
||||
await share_install(sharedProfile.value.id).catch(handleError)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal ref="confirmModal" :header="`Install ${sharedProfile?.name}`">
|
||||
<div class="modal-body">
|
||||
<div class="button-row">
|
||||
<div class="markdown-body">
|
||||
<p>
|
||||
Installing <code>{{ sharedProfile.name }}</code> from user {{ sharedProfile.owner_id }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<Button color="primary" @click="install">Install</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--gap-md);
|
||||
padding: var(--gap-lg);
|
||||
}
|
||||
|
||||
.button-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--gap-md);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.project-card {
|
||||
background-color: var(--color-bg);
|
||||
width: 100%;
|
||||
|
||||
:deep(.badge) {
|
||||
border: 1px solid var(--color-raised-bg);
|
||||
background-color: var(--color-accent-contrast);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -83,7 +83,6 @@
|
||||
v-tooltip="'Open link'"
|
||||
icon-only
|
||||
color="raised"
|
||||
class="r-btn"
|
||||
@click="() => clipboardWrite(loginUrl)"
|
||||
>
|
||||
<GlobeIcon />
|
||||
|
||||
@@ -1,60 +1,53 @@
|
||||
<template>
|
||||
<div class="breadcrumbs">
|
||||
<div
|
||||
v-if="props.afterLogo && breadcrumbContext.routeBreadcrumbs.value?.length > 0"
|
||||
class="breadcrumbs__item"
|
||||
>
|
||||
<ChevronRightIcon class="chevron" />
|
||||
</div>
|
||||
<div
|
||||
v-for="breadcrumb in breadcrumbContext.routeBreadcrumbs.value"
|
||||
:key="breadcrumb.name"
|
||||
class="breadcrumbs__item"
|
||||
>
|
||||
<Button class="breadcrumbs__back transparent" icon-only @click="$router.back()">
|
||||
<ChevronLeftIcon />
|
||||
</Button>
|
||||
<Button class="breadcrumbs__forward transparent" icon-only @click="$router.forward()">
|
||||
<ChevronRightIcon />
|
||||
</Button>
|
||||
{{ breadcrumbData.resetToNames(breadcrumbs) }}
|
||||
<div v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name" class="breadcrumbs__item">
|
||||
<router-link
|
||||
v-if="breadcrumb.link"
|
||||
:to="{
|
||||
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id)),
|
||||
query: breadcrumb.query,
|
||||
}"
|
||||
>
|
||||
{{ breadcrumbName(breadcrumb.name) }}
|
||||
>{{
|
||||
breadcrumb.name.charAt(0) === '?'
|
||||
? breadcrumbData.getName(breadcrumb.name.slice(1))
|
||||
: breadcrumb.name
|
||||
}}
|
||||
</router-link>
|
||||
<span v-else class="selected">
|
||||
{{ breadcrumbName(breadcrumb.name) }}
|
||||
</span>
|
||||
<span v-else class="selected">{{
|
||||
breadcrumb.name.charAt(0) === '?'
|
||||
? breadcrumbData.getName(breadcrumb.name.slice(1))
|
||||
: breadcrumb.name
|
||||
}}</span>
|
||||
<ChevronRightIcon v-if="breadcrumb.link" class="chevron" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ChevronRightIcon } from 'omorphia'
|
||||
import { useBreadcrumbs, useBreadcrumbContext } from '@/store/breadcrumbs'
|
||||
import { ChevronRightIcon, Button, ChevronLeftIcon } from 'omorphia'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
afterLogo: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const breadcrumbData = useBreadcrumbs()
|
||||
import { computed } from 'vue'
|
||||
|
||||
const route = useRoute()
|
||||
const breadcrumbContext = useBreadcrumbContext(route)
|
||||
|
||||
breadcrumbData.$subscribe(() => {
|
||||
breadcrumbData?.resetToNames(breadcrumbContext.routeBreadcrumbs.value)
|
||||
const breadcrumbData = useBreadcrumbs()
|
||||
const breadcrumbs = computed(() => {
|
||||
const additionalContext =
|
||||
route.meta.useContext === true
|
||||
? breadcrumbData.context
|
||||
: route.meta.useRootContext === true
|
||||
? breadcrumbData.rootContext
|
||||
: null
|
||||
return additionalContext ? [additionalContext, ...route.meta.breadcrumb] : route.meta.breadcrumb
|
||||
})
|
||||
|
||||
const breadcrumbName = (bcn) => {
|
||||
if (bcn.charAt(0) === '?') {
|
||||
return breadcrumbData.getName(bcn.slice(1))
|
||||
}
|
||||
return bcn
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -68,10 +61,7 @@ const breadcrumbName = (bcn) => {
|
||||
vertical-align: center;
|
||||
margin: auto 0;
|
||||
|
||||
.chevron {
|
||||
margin: auto 0.5rem;
|
||||
}
|
||||
|
||||
.chevron,
|
||||
a {
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ const exportPack = async () => {
|
||||
<div class="iconified-input">
|
||||
<PackageIcon />
|
||||
<input v-model="nameInput" type="text" placeholder="Modpack name" class="input" />
|
||||
<Button class="r-btn" @click="nameInput = ''">
|
||||
<Button @click="nameInput = ''">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -122,7 +122,7 @@ const exportPack = async () => {
|
||||
<div class="iconified-input">
|
||||
<VersionIcon />
|
||||
<input v-model="versionInput" type="text" placeholder="1.0.0" class="input" />
|
||||
<Button class="r-btn" @click="versionInput = ''">
|
||||
<Button @click="versionInput = ''">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -71,7 +71,7 @@ const install = async (e) => {
|
||||
e?.stopPropagation()
|
||||
modLoading.value = true
|
||||
const versions = await useFetch(
|
||||
`https://api.modrinth.com/v2/project/${props.instance.project_id}/version`,
|
||||
`https://staging-api.modrinth.com/v2/project/${props.instance.project_id}/version`,
|
||||
'project versions'
|
||||
)
|
||||
|
||||
@@ -82,7 +82,9 @@ const install = async (e) => {
|
||||
packs.length === 0 ||
|
||||
!packs
|
||||
.map((value) => value.metadata)
|
||||
.find((pack) => pack.linked_data?.project_id === props.instance.project_id)
|
||||
.find(
|
||||
(pack) => pack.linked_data?.modrinth_modpack?.project_id === props.instance.project_id
|
||||
)
|
||||
) {
|
||||
modLoading.value = true
|
||||
await pack_install(
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
placeholder="Path to launcher"
|
||||
@change="setPath"
|
||||
/>
|
||||
<Button class="r-btn" @click="() => (selectedLauncherPath = '')">
|
||||
<Button @click="() => (selectedLauncherPath = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -247,7 +247,9 @@ const check_valid = computed(() => {
|
||||
</Button>
|
||||
<div
|
||||
v-tooltip="
|
||||
profile.metadata.linked_data?.locked && !profile.installedMod
|
||||
(profile.metadata.linked_data?.modrinth_modpack.locked ||
|
||||
profile.metadata.linked_data?.shared_profile) &&
|
||||
!profile.installedMod
|
||||
? 'Unpair or unlock an instance to add mods.'
|
||||
: ''
|
||||
"
|
||||
@@ -265,7 +267,8 @@ const check_valid = computed(() => {
|
||||
? 'Installing...'
|
||||
: profile.installedMod
|
||||
? 'Installed'
|
||||
: profile.metadata.linked_data && profile.metadata.linked_data.locked
|
||||
: profile.metadata.linked_data?.modrinth_modpack.locked ||
|
||||
profile.metadata.linked_data?.shared_profile
|
||||
? 'Paired'
|
||||
: 'Install'
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { Button, Modal, CheckIcon, Badge, Card } from 'omorphia'
|
||||
import { Button, Modal, CheckIcon, Badge } from 'omorphia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useTheming } from '@/store/theme'
|
||||
import { update_managed_modrinth_version } from '@/helpers/profile'
|
||||
@@ -28,7 +28,9 @@ const filteredVersions = computed(() => {
|
||||
})
|
||||
|
||||
const modpackVersionModal = ref(null)
|
||||
const installedVersion = computed(() => props.instance?.metadata?.linked_data?.version_id)
|
||||
const installedVersion = computed(
|
||||
() => props.instance?.metadata?.linked_data?.modrinth_modpack?.version_id
|
||||
)
|
||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
const inProgress = ref(false)
|
||||
|
||||
@@ -49,7 +51,7 @@ const switchVersion = async (versionId) => {
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<Card v-if="instance.metadata.linked_data" class="mod-card">
|
||||
<Card v-if="instance.metadata.linked_data?.modrinth_modpack" class="mod-card">
|
||||
<div class="table">
|
||||
<div class="table-row with-columns table-head">
|
||||
<div class="table-cell table-text download-cell" />
|
||||
|
||||
@@ -73,7 +73,7 @@ const install = async (e) => {
|
||||
e?.stopPropagation()
|
||||
installing.value = true
|
||||
const versions = await useFetch(
|
||||
`https://api.modrinth.com/v2/project/${props.project.project_id}/version`,
|
||||
`https://staging-api.modrinth.com/v2/project/${props.project.project_id}/version`,
|
||||
'project versions'
|
||||
)
|
||||
|
||||
@@ -84,7 +84,7 @@ const install = async (e) => {
|
||||
packs.length === 0 ||
|
||||
!packs
|
||||
.map((value) => value.metadata)
|
||||
.find((pack) => pack.linked_data?.project_id === props.project.project_id)
|
||||
.find((pack) => pack.linked_data?.modrinth_modpack?.project_id === props.project.project_id)
|
||||
) {
|
||||
installing.value = true
|
||||
await pack_install(
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<template>
|
||||
<div class="action-groups">
|
||||
<a href="https://discord.modrinth.com" class="link">
|
||||
<ChatIcon />
|
||||
<span> Get support </span>
|
||||
</a>
|
||||
<Button
|
||||
v-if="currentLoadingBars.length > 0"
|
||||
ref="infoButton"
|
||||
icon-only
|
||||
class="download icon-button"
|
||||
class="icon-button show-card-icon"
|
||||
@click="toggleCard()"
|
||||
>
|
||||
<DownloadIcon />
|
||||
@@ -30,17 +34,17 @@
|
||||
<DropdownIcon />
|
||||
</div>
|
||||
</div>
|
||||
<Button v-tooltip="'Stop instance'" icon-only class="stop icon-button" @click="stop()">
|
||||
<Button v-tooltip="'Stop instance'" icon-only class="icon-button stop" @click="stop()">
|
||||
<StopCircleIcon />
|
||||
</Button>
|
||||
<Button v-tooltip="'View logs'" icon-only class="utility icon-button" @click="goToTerminal()">
|
||||
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click="goToTerminal()">
|
||||
<TerminalSquareIcon />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="currentLoadingBars.length > 0"
|
||||
ref="infoButton"
|
||||
icon-only
|
||||
class="download icon-button"
|
||||
class="icon-button show-card-icon"
|
||||
@click="toggleCard()"
|
||||
>
|
||||
<DownloadIcon />
|
||||
@@ -80,7 +84,7 @@
|
||||
<Button
|
||||
v-tooltip="'Stop instance'"
|
||||
icon-only
|
||||
class="stop icon-button"
|
||||
class="icon-button stop"
|
||||
@click.stop="stop(profile.path)"
|
||||
>
|
||||
<StopCircleIcon />
|
||||
@@ -88,7 +92,7 @@
|
||||
<Button
|
||||
v-tooltip="'View logs'"
|
||||
icon-only
|
||||
class="utility icon-button"
|
||||
class="icon-button"
|
||||
@click.stop="goToTerminal(profile.path)"
|
||||
>
|
||||
<TerminalSquareIcon />
|
||||
@@ -120,6 +124,7 @@ import { refreshOffline, isOffline } from '@/helpers/utils.js'
|
||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { mixpanel_track } from '@/helpers/mixpanel'
|
||||
import { ChatIcon } from '@/assets/icons'
|
||||
|
||||
const router = useRouter()
|
||||
const card = ref(null)
|
||||
@@ -322,16 +327,12 @@ onBeforeUnmount(() => {
|
||||
.icon-button {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
box-shadow: none;
|
||||
width: 1.25rem !important;
|
||||
height: 1.25rem !important;
|
||||
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.stop {
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
||||
.utility {
|
||||
color: var(--color-contrast);
|
||||
&.stop {
|
||||
--text-color: var(--color-red) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
@@ -393,7 +394,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.download {
|
||||
.show-card-icon {
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ const installed = ref(props.installed)
|
||||
async function install() {
|
||||
installing.value = true
|
||||
const versions = await useFetch(
|
||||
`https://api.modrinth.com/v2/project/${props.project.project_id}/version`,
|
||||
`https://staging-api.modrinth.com/v2/project/${props.project.project_id}/version`,
|
||||
'project versions'
|
||||
)
|
||||
let queuedVersionData
|
||||
@@ -156,7 +156,7 @@ async function install() {
|
||||
packs.length === 0 ||
|
||||
!packs
|
||||
.map((value) => value.metadata)
|
||||
.find((pack) => pack.linked_data?.project_id === props.project.project_id)
|
||||
.find((pack) => pack.linked_data?.modrinth_modpack?.project_id === props.project.project_id)
|
||||
) {
|
||||
await packInstall(
|
||||
props.project.project_id,
|
||||
|
||||
@@ -20,20 +20,24 @@ defineExpose({
|
||||
async show(event) {
|
||||
if (event.event === 'InstallVersion') {
|
||||
version.value = await useFetch(
|
||||
`https://api.modrinth.com/v2/version/${encodeURIComponent(event.id)}`,
|
||||
`https://staging-api.modrinth.com/v2/version/${encodeURIComponent(event.id)}`,
|
||||
'version'
|
||||
)
|
||||
project.value = await useFetch(
|
||||
`https://api.modrinth.com/v2/project/${encodeURIComponent(version.value.project_id)}`,
|
||||
`https://staging-api.modrinth.com/v2/project/${encodeURIComponent(
|
||||
version.value.project_id
|
||||
)}`,
|
||||
'project'
|
||||
)
|
||||
} else {
|
||||
project.value = await useFetch(
|
||||
`https://api.modrinth.com/v2/project/${encodeURIComponent(event.id)}`,
|
||||
`https://staging-api.modrinth.com/v2/project/${encodeURIComponent(event.id)}`,
|
||||
'project'
|
||||
)
|
||||
version.value = await useFetch(
|
||||
`https://api.modrinth.com/v2/version/${encodeURIComponent(project.value.versions[0])}`,
|
||||
`https://staging-api.modrinth.com/v2/version/${encodeURIComponent(
|
||||
project.value.versions[0]
|
||||
)}`,
|
||||
'version'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
<template>
|
||||
<div class="account-dropdown">
|
||||
<Modal
|
||||
ref="modrinthLoginModal"
|
||||
class="login-screen-modal"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
>
|
||||
<ModrinthLoginScreen :modal="true" :prev-page="signInAfter" :next-page="signInAfter" />
|
||||
</Modal>
|
||||
<PopoutMenu class="btn btn-transparent collapsed-button" direction="up" position="right">
|
||||
<Avatar class="collapsed-button__icon" circle size="sm" :src="auth?.user?.avatar_url" />
|
||||
<span class="collapsed-button__label">
|
||||
<template v-if="auth?.user">
|
||||
{{ auth.user.username }}
|
||||
</template>
|
||||
<template v-else> Sign in </template>
|
||||
</span>
|
||||
<template #menu>
|
||||
<div class="selection-menu">
|
||||
<template v-if="auth?.user">
|
||||
<Button color="danger" transparent hover-filled-only @click="() => mrAuth.logout()">
|
||||
<LogOutIcon /> Sign out
|
||||
</Button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Button
|
||||
color="primary"
|
||||
transparent
|
||||
hover-filled-only
|
||||
@click="() => $refs.modrinthLoginModal.show()"
|
||||
>
|
||||
<LogInIcon /> Sign in
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</PopoutMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Avatar, Button, PopoutMenu, LogOutIcon, LogInIcon, Modal } from 'omorphia'
|
||||
|
||||
import { useTheming } from '@/store/state'
|
||||
import { useModrinthAuth } from '@/store/mr_auth.js'
|
||||
|
||||
import ModrinthLoginScreen from '@/components/ui/tutorial/ModrinthLoginScreen.vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const themeStore = useTheming()
|
||||
const mrAuth = useModrinthAuth()
|
||||
const { auth } = storeToRefs(mrAuth)
|
||||
|
||||
const modrinthLoginModal = ref(null)
|
||||
|
||||
const signInAfter = async () => {
|
||||
modrinthLoginModal.value?.hide()
|
||||
await mrAuth.get()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.account-dropdown {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selection-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
|
||||
width: max-content;
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
justify-content: start;
|
||||
}
|
||||
}
|
||||
|
||||
:deep {
|
||||
.login-screen-modal {
|
||||
.modal-container .modal-body {
|
||||
width: auto;
|
||||
|
||||
.content {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -23,7 +23,7 @@ defineProps({
|
||||
<div class="iconified-input">
|
||||
<SearchIcon />
|
||||
<input v-model="search" type="text" placeholder="Search" class="search-input" />
|
||||
<Button class="r-btn" @click="() => (search = '')">
|
||||
<Button @click="() => (search = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -189,7 +189,7 @@ defineProps({
|
||||
type="text"
|
||||
:placeholder="`Search ${projectType}s...`"
|
||||
/>
|
||||
<Button class="r-btn" @click="() => (query = '')">
|
||||
<Button @click="() => (query = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -132,7 +132,7 @@ const next = async () => {
|
||||
placeholder="Path to launcher"
|
||||
@change="setPath"
|
||||
/>
|
||||
<Button class="r-btn" @click="() => (selectedLauncherPath = '')">
|
||||
<Button @click="() => (selectedLauncherPath = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -119,7 +119,7 @@ const clipboardWrite = async (a) => {
|
||||
<div class="iconified-input">
|
||||
<LogInIcon />
|
||||
<input type="text" :value="loginUrl" readonly />
|
||||
<Button v-tooltip="'Open link'" icon-only color="raised" class="r-btn" @click="openUrl">
|
||||
<Button v-tooltip="'Open link'" icon-only color="raised" @click="openUrl">
|
||||
<GlobeIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { openExternal } from '@/helpers/external'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const disableMiddleClick = (e) => {
|
||||
// disables middle click -> new tab
|
||||
if (e.button === 1) {
|
||||
e.preventDefault()
|
||||
// instead do a left click
|
||||
const event = new MouseEvent('click', {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
e.target.dispatchEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
const disableExternalNavigation = (e, window) => {
|
||||
let target = e.target
|
||||
|
||||
while (target != null) {
|
||||
if (target.matches('a')) {
|
||||
if (
|
||||
target.href &&
|
||||
['http://', 'https://', 'mailto:', 'tel:'].some((v) => target.href.startsWith(v)) &&
|
||||
!target.classList.contains('router-link-active') &&
|
||||
!target.href.startsWith('http://localhost') &&
|
||||
!target.href.startsWith('https://tauri.localhost')
|
||||
) {
|
||||
openExternal(window, target.href)
|
||||
}
|
||||
e.preventDefault()
|
||||
break
|
||||
}
|
||||
target = target.parentElement
|
||||
}
|
||||
}
|
||||
|
||||
export const useDisableClicks = (document, window) => {
|
||||
onMounted(() => {
|
||||
document
|
||||
.querySelector('body')
|
||||
.addEventListener('click', (e) => disableExternalNavigation(e, window))
|
||||
|
||||
document.querySelector('body').addEventListener('auxclick', disableMiddleClick)
|
||||
})
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export const openExternal = (window, url) => {
|
||||
window.__TAURI_INVOKE__('tauri', {
|
||||
__tauriModule: 'Shell',
|
||||
message: {
|
||||
cmd: 'open',
|
||||
path: url,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
|
||||
export const iconPathAsUrl = (iconPath) => {
|
||||
if (!iconPath) {
|
||||
return ''
|
||||
}
|
||||
const startsWithHttp = iconPath.startsWith('http')
|
||||
if (startsWithHttp) {
|
||||
return iconPath
|
||||
}
|
||||
return convertFileSrc(iconPath)
|
||||
}
|
||||
58
theseus_gui/src/helpers/shared_profiles.js
Normal file
58
theseus_gui/src/helpers/shared_profiles.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
|
||||
/// Created shared modpack from profile
|
||||
export async function share_create(path) {
|
||||
return await invoke('plugin:profile_share|profile_share_create', { path })
|
||||
}
|
||||
|
||||
/// Generates a shared profile link
|
||||
export async function share_generate(path) {
|
||||
return await invoke('plugin:profile_share|profile_share_generate_share_link', { path })
|
||||
}
|
||||
|
||||
/// Gets the shared profile from the link id
|
||||
// This is done without accepting it- so would not include any link information, and is only usable for basic info
|
||||
export async function share_get_link_id(link) {
|
||||
return await invoke('plugin:profile_share|profile_share_get_link_id', { link })
|
||||
}
|
||||
|
||||
/// Accepts a shared profile link
|
||||
export async function share_accept(link) {
|
||||
return await invoke('plugin:profile_share|profile_share_accept_share_link', { link })
|
||||
}
|
||||
|
||||
/// Removes users from a shared profile
|
||||
export async function remove_users(path, users) {
|
||||
return await invoke('plugin:profile_share|profile_share_remove_users', { path, users })
|
||||
}
|
||||
|
||||
/// Removes links from a shared profile
|
||||
export async function remove_links(path, links) {
|
||||
return await invoke('plugin:profile_share|profile_share_remove_links', { path, links })
|
||||
}
|
||||
|
||||
/// Install a pack from a shared profile id
|
||||
export async function share_install(sharedProfileId) {
|
||||
return await invoke('plugin:profile_share|profile_share_install', { sharedProfileId })
|
||||
}
|
||||
|
||||
// get all user profiles that are available to the currentt user
|
||||
export async function get_all(path) {
|
||||
return await invoke('plugin:profile_share|profile_share_get_all', { path })
|
||||
}
|
||||
|
||||
// syncs profile to match that on server
|
||||
export async function inbound_sync(path) {
|
||||
return await invoke('plugin:profile_share|profile_share_inbound_sync', { path })
|
||||
}
|
||||
|
||||
// syncs profile to update server
|
||||
// only allowed if profile is owned by user
|
||||
export async function outbound_sync(path) {
|
||||
return await invoke('plugin:profile_share|profile_share_outbound_sync', { path })
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export const installVersionDependencies = async (profile, version) => {
|
||||
)
|
||||
continue
|
||||
const depVersions = await useFetch(
|
||||
`https://api.modrinth.com/v2/project/${dep.project_id}/version`,
|
||||
`https://staging-api.modrinth.com/v2/project/${dep.project_id}/version`,
|
||||
'dependency versions'
|
||||
)
|
||||
const latest = depVersions.find(
|
||||
|
||||
@@ -2,10 +2,8 @@ import { createApp } from 'vue'
|
||||
import router from '@/routes'
|
||||
import App from '@/App.vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createPlugin as createVintl } from '@vintl/vintl/plugin'
|
||||
import 'omorphia/dist/style.css'
|
||||
import '@/assets/stylesheets/global.scss'
|
||||
import '@/assets/stylesheets/components.scss'
|
||||
import 'floating-vue/dist/style.css'
|
||||
import FloatingVue from 'floating-vue'
|
||||
import { get_opening_command, initialize_state } from '@/helpers/state'
|
||||
@@ -16,25 +14,9 @@ import { isDev } from './helpers/utils.js'
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
const vintl = createVintl({
|
||||
controllerOpts: {
|
||||
defaultLocale: 'en-US',
|
||||
locale: 'en-US',
|
||||
locales: [
|
||||
{
|
||||
tag: 'en-US',
|
||||
meta: {
|
||||
displayName: 'American English',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
let app = createApp(App)
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
app.use(vintl)
|
||||
app.use(FloatingVue)
|
||||
app.mixin(loadCssMixin)
|
||||
|
||||
@@ -47,10 +29,15 @@ const raw_invoke = async (plugin, fn, args) => {
|
||||
return await invoke('plugin:' + plugin + '|' + fn, args)
|
||||
}
|
||||
}
|
||||
const test_command = async (command) => {
|
||||
return await raw_invoke('utils', 'test_command', { command })
|
||||
}
|
||||
|
||||
isDev()
|
||||
.then((dev) => {
|
||||
if (dev) {
|
||||
window.raw_invoke = raw_invoke
|
||||
window.test_command = test_command
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -72,7 +59,7 @@ initialize_state()
|
||||
.finally(() => {
|
||||
mountedApp.initialize()
|
||||
get_opening_command().then((command) => {
|
||||
console.log('Opening Command', JSON.stringify(command)) // change me to use whatever FE command handler is made
|
||||
console.log(JSON.stringify(command)) // change me to use whatever FE command handler is made
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -149,7 +149,7 @@ if (route.query.ai) {
|
||||
}
|
||||
|
||||
async function refreshSearch() {
|
||||
const base = 'https://api.modrinth.com/v2/'
|
||||
const base = 'https://staging-api.modrinth.com/v2/'
|
||||
|
||||
const params = [`limit=${maxResults.value}`, `index=${sortType.value.name}`]
|
||||
if (query.value.length > 0) {
|
||||
@@ -705,7 +705,7 @@ onUnmounted(() => unlistenOffline())
|
||||
:placeholder="`Search ${projectType}s...`"
|
||||
@input="onSearchChange(1)"
|
||||
/>
|
||||
<Button class="r-btn" @click="() => clearSearch()">
|
||||
<Button @click="() => clearSearch()">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -843,6 +843,13 @@ onUnmounted(() => unlistenOffline())
|
||||
min-height: min-content !important;
|
||||
}
|
||||
|
||||
.iconified-input {
|
||||
input {
|
||||
max-width: none !important;
|
||||
flex-basis: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.search-panel-container {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script setup>
|
||||
import { ref, onUnmounted, computed } from 'vue'
|
||||
import { ref, onUnmounted, shallowRef, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import RowDisplay from '@/components/RowDisplay.vue'
|
||||
import { list } from '@/helpers/profile.js'
|
||||
import { offline_listener, profile_listener } from '@/helpers/events'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { useFetch } from '@/helpers/fetch.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import dayjs from 'dayjs'
|
||||
import { isOffline } from '@/helpers/utils'
|
||||
import { useInstances } from '@/store/instances'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const featuredModpacks = ref({})
|
||||
const featuredMods = ref({})
|
||||
@@ -18,19 +19,22 @@ const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
breadcrumbs.setRootContext({ name: 'Home', link: route.path })
|
||||
|
||||
const recentInstances = shallowRef([])
|
||||
|
||||
const offline = ref(await isOffline())
|
||||
|
||||
const instancesStore = useInstances()
|
||||
const { instancesByPlayed } = storeToRefs(instancesStore)
|
||||
|
||||
const getInstances = async () => {
|
||||
await instancesStore.refreshInstances()
|
||||
const profiles = await list(true).catch(handleError)
|
||||
recentInstances.value = Object.values(profiles).sort((a, b) => {
|
||||
return dayjs(b.metadata.last_played ?? 0).diff(dayjs(a.metadata.last_played ?? 0))
|
||||
})
|
||||
|
||||
// filter? TODO: Change this to be reactive along with fetching the rest.
|
||||
let filters = []
|
||||
for (const instance of instancesByPlayed.value) {
|
||||
if (instance.metadata.linked_data && instance.metadata.linked_data.project_id) {
|
||||
filters.push(`NOT"project_id"="${instance.metadata.linked_data.project_id}"`)
|
||||
for (const instance of recentInstances.value) {
|
||||
if (instance.metadata.linked_data?.modrinth_modpack?.project_id) {
|
||||
filters.push(
|
||||
`NOT"project_id"="${instance.metadata.linked_data?.modrinth_modpack?.project_id}"`
|
||||
)
|
||||
}
|
||||
}
|
||||
filter.value = filters.join(' AND ')
|
||||
@@ -38,7 +42,7 @@ const getInstances = async () => {
|
||||
|
||||
const getFeaturedModpacks = async () => {
|
||||
const response = await useFetch(
|
||||
`https://api.modrinth.com/v2/search?facets=[["project_type:modpack"]]&limit=10&index=follows&filters=${filter.value}`,
|
||||
`https://staging-api.modrinth.com/v2/search?facets=[["project_type:modpack"]]&limit=10&index=follows&filters=${filter.value}`,
|
||||
'featured modpacks',
|
||||
offline.value
|
||||
)
|
||||
@@ -50,7 +54,7 @@ const getFeaturedModpacks = async () => {
|
||||
}
|
||||
const getFeaturedMods = async () => {
|
||||
const response = await useFetch(
|
||||
'https://api.modrinth.com/v2/search?facets=[["project_type:mod"]]&limit=10&index=follows',
|
||||
'https://staging-api.modrinth.com/v2/search?facets=[["project_type:mod"]]&limit=10&index=follows',
|
||||
'featured mods',
|
||||
offline.value
|
||||
)
|
||||
@@ -82,7 +86,7 @@ const unlistenOffline = await offline_listener(async (b) => {
|
||||
// computed sums of recentInstances, featuredModpacks, featuredMods, treating them as arrays if they are not
|
||||
const total = computed(() => {
|
||||
return (
|
||||
(instancesByPlayed.value?.length ?? 0) +
|
||||
(recentInstances.value?.length ?? 0) +
|
||||
(featuredModpacks.value?.length ?? 0) +
|
||||
(featuredMods.value?.length ?? 0)
|
||||
)
|
||||
@@ -102,7 +106,7 @@ onUnmounted(() => {
|
||||
{
|
||||
label: 'Jump back in',
|
||||
route: '/library',
|
||||
instances: instancesByPlayed,
|
||||
instances: recentInstances,
|
||||
downloaded: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<script setup>
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { onUnmounted, ref, shallowRef } from 'vue'
|
||||
import GridDisplay from '@/components/GridDisplay.vue'
|
||||
import { list } from '@/helpers/profile.js'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { offline_listener, profile_listener } from '@/helpers/events.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { Button, PlusIcon } from 'omorphia'
|
||||
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
||||
import { NewInstanceImage } from '@/assets/icons'
|
||||
import { isOffline } from '@/helpers/utils'
|
||||
import { useInstances } from '@/store/instances'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const route = useRoute()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
breadcrumbs.setRootContext({ name: 'Library', link: route.path })
|
||||
|
||||
const instancesStore = useInstances()
|
||||
const { instanceList } = storeToRefs(instancesStore)
|
||||
const profiles = await list(true).catch(handleError)
|
||||
const instances = shallowRef(Object.values(profiles))
|
||||
|
||||
const offline = ref(await isOffline())
|
||||
const unlistenOffline = await offline_listener((b) => {
|
||||
@@ -25,9 +25,9 @@ const unlistenOffline = await offline_listener((b) => {
|
||||
})
|
||||
|
||||
const unlistenProfile = await profile_listener(async () => {
|
||||
await instancesStore.refreshInstances()
|
||||
const profiles = await list(true).catch(handleError)
|
||||
instances.value = Object.values(profiles)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProfile()
|
||||
unlistenOffline()
|
||||
@@ -35,7 +35,7 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GridDisplay v-if="instanceList.length > 0" label="Instances" :instances="instanceList" />
|
||||
<GridDisplay v-if="instances.length > 0" label="Instances" :instances="instances" />
|
||||
<div v-else class="no-instance">
|
||||
<div class="icon">
|
||||
<NewInstanceImage />
|
||||
|
||||
@@ -16,16 +16,13 @@ import {
|
||||
import { handleError, useTheming } from '@/store/state'
|
||||
import { is_dir_writeable, change_config_dir, get, set } from '@/helpers/settings'
|
||||
import { get_max_memory } from '@/helpers/jre'
|
||||
|
||||
import { useModrinthAuth } from '@/store/mr_auth.js'
|
||||
|
||||
import { get as getCreds, logout } from '@/helpers/mr_auth.js'
|
||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||
import ModrinthLoginScreen from '@/components/ui/tutorial/ModrinthLoginScreen.vue'
|
||||
import { mixpanel_opt_out_tracking, mixpanel_opt_in_tracking } from '@/helpers/mixpanel'
|
||||
import { open } from '@tauri-apps/api/dialog'
|
||||
import { getOS } from '@/helpers/utils.js'
|
||||
import { version } from '../../package.json'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const pageOptions = ['Home', 'Library']
|
||||
|
||||
@@ -108,13 +105,17 @@ watch(
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const mrAuth = useModrinthAuth()
|
||||
const { auth } = storeToRefs(mrAuth)
|
||||
const credentials = ref(await getCreds().catch(handleError))
|
||||
const loginScreenModal = ref()
|
||||
|
||||
async function logOut() {
|
||||
await logout().catch(handleError)
|
||||
credentials.value = await getCreds().catch(handleError)
|
||||
}
|
||||
|
||||
async function signInAfter() {
|
||||
loginScreenModal.value.hide()
|
||||
await mrAuth.get()
|
||||
credentials.value = await getCreds().catch(handleError)
|
||||
}
|
||||
|
||||
async function findLauncherDir() {
|
||||
@@ -162,12 +163,12 @@ async function refreshDir() {
|
||||
<div class="adjacent-input">
|
||||
<label for="theme">
|
||||
<span class="label__title">Manage account</span>
|
||||
<span v-if="auth" class="label__description">
|
||||
You are currently logged in as {{ auth?.user.username }}.
|
||||
<span v-if="credentials" class="label__description">
|
||||
You are currently logged in as {{ credentials.user.username }}.
|
||||
</span>
|
||||
<span v-else> Sign in to your Modrinth account. </span>
|
||||
</label>
|
||||
<button v-if="auth" class="btn" @click="mrAuth.logout()">
|
||||
<button v-if="credentials" class="btn" @click="logOut">
|
||||
<LogOutIcon />
|
||||
Sign out
|
||||
</button>
|
||||
@@ -186,7 +187,7 @@ async function refreshDir() {
|
||||
<div class="iconified-input">
|
||||
<BoxIcon />
|
||||
<input id="appDir" v-model="settingsDir" type="text" class="input" />
|
||||
<Button class="r-btn" @click="findLauncherDir">
|
||||
<Button @click="findLauncherDir">
|
||||
<FolderSearchIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -146,39 +146,20 @@ import {
|
||||
} from '@/helpers/process'
|
||||
import { offline_listener, process_listener, profile_listener } from '@/helpers/events'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, onUnmounted, defineProps, watch } from 'vue'
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
|
||||
import { isOffline, showProfileInFolder } from '@/helpers/utils.js'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import { mixpanel_track } from '@/helpers/mixpanel'
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
import { useFetch } from '@/helpers/fetch'
|
||||
import { useInstances } from '@/store/instances'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const router = useRouter()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
const instance = ref(await get(route.params.id || props.id).catch(handleError))
|
||||
|
||||
watch(
|
||||
() => route.params.id,
|
||||
async (id) => {
|
||||
if (!id) return
|
||||
instance.value = await get(id).catch(handleError)
|
||||
}
|
||||
)
|
||||
|
||||
const instancesStore = useInstances()
|
||||
const instance = ref(await get(route.params.id).catch(handleError))
|
||||
|
||||
breadcrumbs.setName(
|
||||
'Instance',
|
||||
@@ -213,8 +194,6 @@ const startInstance = async (context) => {
|
||||
game_version: instance.value.metadata.game_version,
|
||||
source: context,
|
||||
})
|
||||
|
||||
await instancesStore.refreshInstances()
|
||||
}
|
||||
|
||||
const checkProcess = async () => {
|
||||
@@ -230,9 +209,9 @@ const checkProcess = async () => {
|
||||
|
||||
// Get information on associated modrinth versions, if any
|
||||
const modrinthVersions = ref([])
|
||||
if (!(await isOffline()) && instance.value.metadata.linked_data?.project_id) {
|
||||
if (!(await isOffline()) && instance.value.metadata.linked_data?.modrinth_modpack?.project_id) {
|
||||
modrinthVersions.value = await useFetch(
|
||||
`https://api.modrinth.com/v2/project/${instance.value.metadata.linked_data.project_id}/version`,
|
||||
`https://staging-api.modrinth.com/v2/project/${instance.value.metadata.linked_data?.modrinth_modpack?.project_id}/version`,
|
||||
'project'
|
||||
)
|
||||
}
|
||||
@@ -440,7 +419,6 @@ Button {
|
||||
width: 100%;
|
||||
color: var(--color-primary);
|
||||
box-shadow: none;
|
||||
justify-content: start;
|
||||
|
||||
&.router-link-exact-active {
|
||||
box-shadow: var(--shadow-inset-lg);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
class="text-input"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<Button class="r-btn" @click="() => (searchFilter = '')">
|
||||
<Button @click="() => (searchFilter = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -43,27 +43,23 @@
|
||||
Update all
|
||||
</Button>
|
||||
|
||||
<div v-if="!isPackLocked" class="joined-buttons">
|
||||
<Button color="primary" @click="onSearchContent">
|
||||
<DropdownButton
|
||||
v-if="!isPackLocked"
|
||||
:options="['search', 'from_file']"
|
||||
default-value="search"
|
||||
name="add-content-dropdown"
|
||||
color="primary"
|
||||
@option-click="handleContentOptionClick"
|
||||
>
|
||||
<template #search>
|
||||
<SearchIcon />
|
||||
Add content
|
||||
</Button>
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'file',
|
||||
action: onFileContent,
|
||||
},
|
||||
]"
|
||||
class="btn btn-primary btn-dropdown-animation icon-only"
|
||||
>
|
||||
<DropdownIcon />
|
||||
<template #file>
|
||||
<FolderOpenIcon />
|
||||
Add from file
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</div>
|
||||
<span class="no-wrap"> Add content </span>
|
||||
</template>
|
||||
<template #from_file>
|
||||
<FolderOpenIcon />
|
||||
<span class="no-wrap"> Add from file </span>
|
||||
</template>
|
||||
</DropdownButton>
|
||||
</Card>
|
||||
<Pagination
|
||||
v-if="projects.length > 0"
|
||||
@@ -287,26 +283,23 @@
|
||||
</div>
|
||||
<h3>No projects found</h3>
|
||||
<p class="empty-subtitle">Add a project to get started</p>
|
||||
<div v-if="!isPackLocked" class="joined-buttons">
|
||||
<Button color="primary" @click="onSearchContent">
|
||||
<SearchIcon />
|
||||
Add content
|
||||
</Button>
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'file',
|
||||
action: onFileContent,
|
||||
},
|
||||
]"
|
||||
class="btn btn-primary btn-dropdown-animation icon-only"
|
||||
<div class="empty-action">
|
||||
<DropdownButton
|
||||
:options="['search', 'from_file']"
|
||||
default-value="search"
|
||||
name="add-content-dropdown-from-empty"
|
||||
color="primary"
|
||||
@option-click="handleContentOptionClick"
|
||||
>
|
||||
<DropdownIcon />
|
||||
<template #file>
|
||||
<FolderOpenIcon />
|
||||
Add from file
|
||||
<template #search>
|
||||
<SearchIcon />
|
||||
<span class="no-wrap"> Add content </span>
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
<template #from_file>
|
||||
<FolderOpenIcon />
|
||||
<span class="no-wrap"> Add from file </span>
|
||||
</template>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
</div>
|
||||
<Pagination
|
||||
@@ -366,7 +359,7 @@
|
||||
/>
|
||||
<ExportModal v-if="projects.length > 0" ref="exportModal" :instance="instance" />
|
||||
<ModpackVersionModal
|
||||
v-if="instance.metadata.linked_data"
|
||||
v-if="instance.metadata.linked_data?.modrinth_modpack"
|
||||
ref="modpackVersionModal"
|
||||
:instance="instance"
|
||||
:versions="props.versions"
|
||||
@@ -385,6 +378,7 @@ import {
|
||||
FolderOpenIcon,
|
||||
Checkbox,
|
||||
formatProjectType,
|
||||
DropdownButton,
|
||||
Modal,
|
||||
XIcon,
|
||||
ShareIcon,
|
||||
@@ -397,7 +391,6 @@ import {
|
||||
CodeIcon,
|
||||
Pagination,
|
||||
DropdownSelect,
|
||||
OverflowMenu,
|
||||
} from 'omorphia'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -440,33 +433,28 @@ const props = defineProps({
|
||||
return false
|
||||
},
|
||||
},
|
||||
playing: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
installed: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return true
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const projects = ref([])
|
||||
const selectionMap = ref(new Map())
|
||||
const showingOptions = ref(false)
|
||||
const isPackLocked = computed(() => {
|
||||
return props.instance.metadata.linked_data && props.instance.metadata.linked_data.locked
|
||||
if (props.instance.metadata.linked_data?.shared_profile) {
|
||||
return !props.instance.metadata.linked_data.shared_profile.is_owner
|
||||
}
|
||||
return props.instance.metadata.linked_data?.modrinth_modpack?.locked
|
||||
})
|
||||
const canUpdatePack = computed(() => {
|
||||
if (!props.instance.metadata.linked_data) return false
|
||||
return props.instance.metadata.linked_data.version_id !== props.instance.modrinth_update_version
|
||||
let linked_data = props.instance.metadata.linked_data
|
||||
if (linked_data.modrinth_modpack) {
|
||||
return linked_data.modrinth_modpack.version_id !== props.instance.sync_update_version
|
||||
}
|
||||
return false
|
||||
})
|
||||
const exportModal = ref(null)
|
||||
|
||||
@@ -863,21 +851,21 @@ const handleRightClick = (event, mod) => {
|
||||
}
|
||||
}
|
||||
|
||||
const onSearchContent = async () => {
|
||||
await router.push({
|
||||
path: `/browse/${props.instance.metadata.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
||||
query: { i: props.instance.path },
|
||||
})
|
||||
}
|
||||
const handleContentOptionClick = async (args) => {
|
||||
if (args.option === 'search') {
|
||||
await router.push({
|
||||
path: `/browse/${props.instance.metadata.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
||||
query: { i: props.instance.path },
|
||||
})
|
||||
} else if (args.option === 'from_file') {
|
||||
const newProject = await open({ multiple: true })
|
||||
if (!newProject) return
|
||||
|
||||
const onFileContent = async () => {
|
||||
const newProject = await open({ multiple: true })
|
||||
if (!newProject) return
|
||||
|
||||
for (const project of newProject) {
|
||||
await add_project_from_path(props.instance.path, project, 'mod').catch(handleError)
|
||||
for (const project of newProject) {
|
||||
await add_project_from_path(props.instance.path, project, 'mod').catch(handleError)
|
||||
}
|
||||
initProjects(await get(props.instance.path).catch(handleError))
|
||||
}
|
||||
initProjects(await get(props.instance.path).catch(handleError))
|
||||
}
|
||||
|
||||
watch(selectAll, () => {
|
||||
@@ -978,17 +966,9 @@ onUnmounted(() => {
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
|
||||
:deep {
|
||||
.popup-container {
|
||||
.btn {
|
||||
height: 2.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-row {
|
||||
.btn {
|
||||
height: 2.5rem !important;
|
||||
}
|
||||
:deep(.dropdown-row) {
|
||||
.btn {
|
||||
height: 2.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ConfirmModal
|
||||
<ModalConfirm
|
||||
ref="modal_confirm"
|
||||
title="Are you sure you want to delete this instance?"
|
||||
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
||||
@@ -358,7 +358,7 @@
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card v-if="instance.metadata.linked_data">
|
||||
<Card v-if="instance.metadata.linked_data?.modrinth_modpack">
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Modpack</span>
|
||||
@@ -413,8 +413,10 @@
|
||||
<XIcon /> Unpair
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="props.instance.metadata.linked_data.project_id" class="adjacent-input">
|
||||
<div
|
||||
v-if="props.instance.metadata.linked_data?.modrinth_modpack?.project_id"
|
||||
class="adjacent-input"
|
||||
>
|
||||
<label for="change-modpack-version">
|
||||
<span class="label__title">Change modpack version</span>
|
||||
<span class="label__description">
|
||||
@@ -445,6 +447,96 @@
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<Card v-if="installedSharedProfileData">
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Shared profile management</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div v-if="installedSharedProfileData.is_owned" class="adjacent-input">
|
||||
<label for="share-links">
|
||||
<span class="label__title">Generate share link</span>
|
||||
<span class="label__description">
|
||||
Creates a share link to share this modpack with others. This allows them to install your
|
||||
instance, as well as stay up to date with any changes you make.
|
||||
</span>
|
||||
</label>
|
||||
<Button id="share-links" @click="generateShareLink"> <GlobeIcon /> Share </Button>
|
||||
</div>
|
||||
<div v-if="shareLink" class="adjacent-input">
|
||||
Generated link: <code>{{ shareLink }}</code>
|
||||
</div>
|
||||
<div v-if="installedSharedProfileData.is_owned" class="table">
|
||||
<div class="table-row table-head">
|
||||
<div class="table-cell table-text name-cell actions-cell">
|
||||
<Button class="transparent"> Name </Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="user in installedSharedProfileData.users" :key="user" class="table-row">
|
||||
<div class="table-cell table-text name-cell">
|
||||
<div class="user-content">
|
||||
<span v-tooltip="`${user}`" class="title">{{ user }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-cell table-text manage">
|
||||
<div v-tooltip="'Remove user'">
|
||||
<Button
|
||||
icon-only
|
||||
@click="removeSharedPackUser(user)"
|
||||
:disabled="user === installedSharedProfileData.owner_id"
|
||||
>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="installedSharedProfileData.is_owned" class="adjacent-input">
|
||||
<label for="share-sync">
|
||||
<span class="label__title">Sync shared profile</span>
|
||||
<span class="label__description" v-if="props.instance.sync_update_version?.is_synced">
|
||||
You are up to date with the shared profile.
|
||||
</span>
|
||||
<span class="label__description" v-else>
|
||||
You have changes that have not been synced to the shared profile. Click the button to
|
||||
upload your changes.
|
||||
</span>
|
||||
</label>
|
||||
<Button
|
||||
id="share-sync-sync"
|
||||
@click="outboundSyncSharedProfile"
|
||||
:disabled="props.instance.sync_update_version?.is_synced"
|
||||
>
|
||||
<GlobeIcon /> Sync
|
||||
</Button>
|
||||
<Button
|
||||
id="share-sync-revert"
|
||||
@click="inboundSyncSharedProfile"
|
||||
:disabled="props.instance.sync_update_version?.is_synced"
|
||||
>
|
||||
<GlobeIcon /> Revert
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else class="adjacent-input">
|
||||
<label for="share-sync">
|
||||
<span class="label__title">Sync shared profile</span>
|
||||
<span class="label__description" v-if="props.instance.sync_update_version?.is_synced">
|
||||
You are up to date with the shared profile.
|
||||
</span>
|
||||
<span class="label__description" v-else>
|
||||
You are not up to date with the shared profile. Click the button to update your instance.
|
||||
</span>
|
||||
</label>
|
||||
<Button
|
||||
id="share-sync-sync"
|
||||
@click="inboundSyncSharedProfile"
|
||||
:disabled="props.instance.sync_update_version?.is_synced"
|
||||
>
|
||||
<GlobeIcon /> Sync
|
||||
</Button>
|
||||
</div>
|
||||
{{ props.instance.sync_update_version }}
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
@@ -502,7 +594,7 @@
|
||||
</div>
|
||||
</Card>
|
||||
<ModpackVersionModal
|
||||
v-if="instance.metadata.linked_data"
|
||||
v-if="instance.metadata.linked_data?.modrinth_modpack"
|
||||
ref="modpackVersionModal"
|
||||
:instance="instance"
|
||||
:versions="props.versions"
|
||||
@@ -525,8 +617,9 @@ import {
|
||||
SaveIcon,
|
||||
LockIcon,
|
||||
HammerIcon,
|
||||
ConfirmModal,
|
||||
ModalConfirm,
|
||||
DownloadIcon,
|
||||
GlobeIcon,
|
||||
ClipboardCopyIcon,
|
||||
Button,
|
||||
Toggle,
|
||||
@@ -545,6 +638,13 @@ import {
|
||||
remove,
|
||||
update_repair_modrinth,
|
||||
} from '@/helpers/profile.js'
|
||||
import {
|
||||
get_all,
|
||||
outbound_sync,
|
||||
inbound_sync,
|
||||
share_generate,
|
||||
remove_users,
|
||||
} from '@/helpers/shared_profiles.js'
|
||||
import { computed, readonly, ref, shallowRef, watch } from 'vue'
|
||||
import { get_max_memory } from '@/helpers/jre.js'
|
||||
import { get } from '@/helpers/settings.js'
|
||||
@@ -659,12 +759,22 @@ const unlinkModpack = ref(false)
|
||||
|
||||
const inProgress = ref(false)
|
||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
const installedVersion = computed(() => props.instance?.metadata?.linked_data?.version_id)
|
||||
const installedVersion = computed(
|
||||
() => props.instance?.metadata?.linked_data?.modrinth_modpack?.version_id
|
||||
)
|
||||
const installedVersionData = computed(() => {
|
||||
if (!installedVersion.value) return null
|
||||
return props.versions.find((version) => version.id === installedVersion.value)
|
||||
})
|
||||
|
||||
const sharedProfiles = await get_all()
|
||||
const installedSharedProfileData = computed(() => {
|
||||
if (!props.instance.metadata.linked_data?.shared_profile) return null
|
||||
return sharedProfiles.find(
|
||||
(profile) => profile.id === props.instance.metadata.linked_data?.shared_profile?.profile_id
|
||||
)
|
||||
})
|
||||
|
||||
watch(
|
||||
[
|
||||
title,
|
||||
@@ -794,13 +904,13 @@ async function unpairProfile() {
|
||||
|
||||
async function unlockProfile() {
|
||||
const editProfile = props.instance
|
||||
editProfile.metadata.linked_data.locked = false
|
||||
editProfile.metadata.linked_data.modrinth_modpack.locked = false
|
||||
await edit(props.instance.path, editProfile)
|
||||
modalConfirmUnlock.value.hide()
|
||||
}
|
||||
|
||||
const isPackLocked = computed(() => {
|
||||
return props.instance.metadata.linked_data && props.instance.metadata.linked_data.locked
|
||||
return props.instance.metadata.linked_data?.modrinth_modpack.locked
|
||||
})
|
||||
|
||||
async function repairModpack() {
|
||||
@@ -932,6 +1042,23 @@ async function saveGvLoaderEdits() {
|
||||
editing.value = false
|
||||
changeVersionsModal.value.hide()
|
||||
}
|
||||
|
||||
async function outboundSyncSharedProfile() {
|
||||
await outbound_sync(props.instance.path).catch(handleError)
|
||||
}
|
||||
|
||||
async function inboundSyncSharedProfile() {
|
||||
await inbound_sync(props.instance.path).catch(handleError)
|
||||
}
|
||||
|
||||
const shareLink = ref(null)
|
||||
async function generateShareLink() {
|
||||
shareLink.value = await share_generate(props.instance.path).catch(handleError)
|
||||
}
|
||||
|
||||
async function removeSharedPackUser(user) {
|
||||
await remove_users(props.instance.path, [user]).catch(handleError)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -1012,4 +1139,29 @@ async function saveGvLoaderEdits() {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
.table {
|
||||
margin-block-start: 0;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 2px solid var(--color-bg);
|
||||
}
|
||||
|
||||
.table-row {
|
||||
grid-template-columns: 7fr 1fr;
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
.title {
|
||||
color: var(--color-contrast);
|
||||
font-weight: bolder;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -314,10 +314,13 @@ async function fetchProjectData() {
|
||||
categories.value,
|
||||
instance.value,
|
||||
] = await Promise.all([
|
||||
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}`, 'project'),
|
||||
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}/version`, 'project'),
|
||||
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}/members`, 'project'),
|
||||
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}/dependencies`, 'project'),
|
||||
useFetch(`https://staging-api.modrinth.com/v2/project/${route.params.id}`, 'project'),
|
||||
useFetch(`https://staging-api.modrinth.com/v2/project/${route.params.id}/version`, 'project'),
|
||||
useFetch(`https://staging-api.modrinth.com/v2/project/${route.params.id}/members`, 'project'),
|
||||
useFetch(
|
||||
`https://staging-api.modrinth.com/v2/project/${route.params.id}/dependencies`,
|
||||
'project'
|
||||
),
|
||||
get_categories().catch(handleError),
|
||||
route.query.i ? getInstance(route.query.i, false).catch(handleError) : Promise.resolve(),
|
||||
])
|
||||
@@ -325,9 +328,7 @@ async function fetchProjectData() {
|
||||
installed.value =
|
||||
instance.value?.path &&
|
||||
(await check_installed(instance.value.path, data.value.id).catch(handleError))
|
||||
|
||||
breadcrumbs.setName('Project', data.value.title)
|
||||
|
||||
installedVersion.value = instance.value
|
||||
? Object.values(instance.value.projects).find(
|
||||
(p) => p?.metadata?.version?.project_id === data.value.id
|
||||
@@ -393,7 +394,7 @@ async function install(version) {
|
||||
packs.length === 0 ||
|
||||
!packs
|
||||
.map((value) => value.metadata)
|
||||
.find((pack) => pack.linked_data?.project_id === data.value.id)
|
||||
.find((pack) => pack.linked_data?.modrinth_modpack?.project_id === data.value.id)
|
||||
) {
|
||||
await packInstall(
|
||||
data.value.id,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useBreadcrumbs = defineStore('breadcrumbsStore', {
|
||||
@@ -36,28 +34,3 @@ export const useBreadcrumbs = defineStore('breadcrumbsStore', {
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const useBreadcrumbContext = (route) => {
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
const routeContext = computed(() => {
|
||||
const { meta } = route
|
||||
if (meta?.useContext) {
|
||||
return breadcrumbs.context
|
||||
} else if (meta?.useRootContext) {
|
||||
return breadcrumbs.rootContext
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const routeBreadcrumbs = computed(() => {
|
||||
const { meta } = route
|
||||
return routeContext.value ? [routeContext.value, ...meta.breadcrumb] : meta.breadcrumb
|
||||
})
|
||||
|
||||
return {
|
||||
routeContext,
|
||||
routeBreadcrumbs,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { list } from '@/helpers/profile.js'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useInstances = defineStore('instancesStore', () => {
|
||||
const instances = ref({})
|
||||
|
||||
const instanceList = computed(() => {
|
||||
return Object.values(instances.value)
|
||||
})
|
||||
const instancesByPlayed = computed(() => {
|
||||
return instanceList.value.sort((a, b) => {
|
||||
return dayjs(b?.metadata?.last_played ?? 0).diff(dayjs(a?.metadata?.last_played ?? 0))
|
||||
})
|
||||
})
|
||||
|
||||
const setInstances = async () => {
|
||||
try {
|
||||
const p = await list(true)
|
||||
instances.value = p
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await setInstances()
|
||||
})
|
||||
|
||||
const refreshInstances = async () => {
|
||||
await setInstances()
|
||||
}
|
||||
|
||||
return {
|
||||
instanceList,
|
||||
instancesByPlayed,
|
||||
|
||||
refreshInstances,
|
||||
}
|
||||
})
|
||||
@@ -1,40 +0,0 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { get as getCredentials, logout as removeCredentials } from '@/helpers/mr_auth.js'
|
||||
import { handleError } from '@/store/state.js'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useModrinthAuth = defineStore('modrinthAuthStore', () => {
|
||||
const auth = ref(null)
|
||||
|
||||
const get = async () => {
|
||||
try {
|
||||
const creds = await getCredentials()
|
||||
auth.value = creds
|
||||
return creds
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
const result = await removeCredentials()
|
||||
auth.value = null
|
||||
return result
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
get()
|
||||
})
|
||||
|
||||
return {
|
||||
auth,
|
||||
get,
|
||||
logout,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user