lint
This commit is contained in:
parent
6811f3c41a
commit
c515d00da1
83 changed files with 850 additions and 813 deletions
|
|
@ -162,7 +162,8 @@ impl OneClientApi for OneClientApiImpl {
|
|||
.ok_or_else(|| anyhow::anyhow!("cluster with id {} not found", cluster_id))?;
|
||||
|
||||
match package {
|
||||
ModpackFileKind::Managed((pkg, version)) => {
|
||||
ModpackFileKind::Managed(box_) => {
|
||||
let (pkg, version) = *box_;
|
||||
let model = api::packages::download_package(&pkg, &version, None, None).await?;
|
||||
api::packages::link_package(&model, &cluster, skip_compatibility).await?;
|
||||
api::packages::bundle_dao::track_bundle_package(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
// === API ===
|
||||
pub const CURSEFORGE_API_KEY: &'static str =
|
||||
"$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe";
|
||||
pub const DISCORD_CLIENT_ID: &'static str = "1426999264633946334";
|
||||
pub const CURSEFORGE_API_KEY: &str = "$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe";
|
||||
pub const DISCORD_CLIENT_ID: &str = "1426999264633946334";
|
||||
|
||||
// === Meta ===
|
||||
// Cannot contain trailing slash
|
||||
pub const META_URL_BASE: &'static str =
|
||||
pub const META_URL_BASE: &str =
|
||||
"https://raw.githubusercontent.com/Polyfrost/DataStorage/refs/heads/main/oneclient";
|
||||
|
|
|
|||
|
|
@ -65,8 +65,6 @@ pub async fn install_update<R: Runtime>(app: tauri::AppHandle<R>) -> Result<(),
|
|||
drop(lock);
|
||||
|
||||
app.restart();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
||||
|
|
@ -76,19 +74,17 @@ pub fn init<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
|||
#[cfg(not(target_os = "linux"))]
|
||||
let updater_available = true;
|
||||
|
||||
if updater_available {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
window
|
||||
.eval("window.__LAUNCHER_UPDATER__ = true;")
|
||||
.expect("Failed to inject updater JS");
|
||||
if updater_available && let Some(window) = app.get_webview_window("main") {
|
||||
window
|
||||
.eval("window.__LAUNCHER_UPDATER__ = true;")
|
||||
.expect("Failed to inject updater JS");
|
||||
|
||||
window
|
||||
.eval(&format!(
|
||||
r#"window.__ONECLIENT_VERSION__ = "{}";"#,
|
||||
Core::get().launcher_version,
|
||||
))
|
||||
.expect("Failed to inject version JS");
|
||||
}
|
||||
window
|
||||
.eval(format!(
|
||||
r#"window.__ONECLIENT_VERSION__ = "{}";"#,
|
||||
Core::get().launcher_version,
|
||||
))
|
||||
.expect("Failed to inject version JS");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ async fn initialize_core() -> LauncherResult<()> {
|
|||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
#[allow(clippy::large_stack_frames)]
|
||||
async fn initialize_tauri(builder: tauri::Builder<tauri::Wry>) -> LauncherResult<tauri::App> {
|
||||
let router = taurpc::Router::new()
|
||||
.export_config(
|
||||
|
|
@ -127,11 +128,11 @@ fn setup_window(handle: &tauri::AppHandle) -> Result<(), Box<dyn std::error::Err
|
|||
|
||||
ext::updater::init(handle)?;
|
||||
|
||||
let win_clone = win.clone();
|
||||
let win_clone = win;
|
||||
let app_handle = handle.clone();
|
||||
tokio::task::spawn(async move {
|
||||
if let Ok(state) = State::get().await {
|
||||
let settings = state.settings.read().await;
|
||||
let _settings = state.settings.read().await;
|
||||
// native_window_frame=true means use native decorations
|
||||
// native_window_frame=false means use custom frame (no decorations)
|
||||
#[cfg(target_os = "macos")]
|
||||
|
|
@ -145,7 +146,7 @@ fn setup_window(handle: &tauri::AppHandle) -> Result<(), Box<dyn std::error::Err
|
|||
use objc2_app_kit::{NSWindow, NSWindowButton};
|
||||
|
||||
if let Ok(ns_window_ptr) = win_weak.ns_window() {
|
||||
let ns_window = ns_window_ptr as *mut NSWindow;
|
||||
let ns_window = ns_window_ptr.cast::<NSWindow>();
|
||||
unsafe {
|
||||
let ns_window = &*ns_window;
|
||||
if let Some(btn) =
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ fn get_cluster_lock(cluster_id: i64) -> Arc<tokio::sync::Mutex<()>> {
|
|||
.clone()
|
||||
}
|
||||
|
||||
fn managed_bundle_key(provider: &Provider, package_id: &str) -> String {
|
||||
fn managed_bundle_key(provider: Provider, package_id: &str) -> String {
|
||||
format!("m:{}:{}", provider.name(), package_id)
|
||||
}
|
||||
|
||||
|
|
@ -121,6 +121,7 @@ pub async fn check_bundle_updates(
|
|||
check_bundle_updates_inner(cluster_id, &overrides).await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn check_bundle_updates_inner(
|
||||
cluster_id: ClusterId,
|
||||
overrides: &[onelauncher_core::entity::cluster_bundle_overrides::Model],
|
||||
|
|
@ -169,7 +170,7 @@ async fn check_bundle_updates_inner(
|
|||
if pkg.provider == Provider::Local && pkg.package_id == pkg.hash {
|
||||
None
|
||||
} else {
|
||||
Some(managed_bundle_key(&pkg.provider, &pkg.package_id))
|
||||
Some(managed_bundle_key(pkg.provider, &pkg.package_id))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -244,8 +245,9 @@ async fn check_bundle_updates_inner(
|
|||
enabled_count += 1;
|
||||
|
||||
match &file.kind {
|
||||
ModpackFileKind::Managed((pkg, version)) => {
|
||||
let key = managed_bundle_key(&pkg.provider, &pkg.id);
|
||||
ModpackFileKind::Managed(box_) => {
|
||||
let (pkg, version) = &**box_;
|
||||
let key = managed_bundle_key(pkg.provider, &pkg.id);
|
||||
tracing::trace!(
|
||||
bundle_name = %bundle.manifest.name,
|
||||
provider = %pkg.provider,
|
||||
|
|
@ -255,7 +257,7 @@ async fn check_bundle_updates_inner(
|
|||
);
|
||||
files_map.insert(key, (version.version_id.clone(), file.clone()));
|
||||
if file.hidden {
|
||||
hidden_explicit_keys.insert(managed_bundle_key(&pkg.provider, &pkg.id));
|
||||
hidden_explicit_keys.insert(managed_bundle_key(pkg.provider, &pkg.id));
|
||||
}
|
||||
|
||||
// Dependencies that are not explicit files are treated as hidden mods
|
||||
|
|
@ -263,7 +265,7 @@ async fn check_bundle_updates_inner(
|
|||
for dep in &version.dependencies {
|
||||
if let Some(dep_project_id) = &dep.project_id {
|
||||
hidden_dependency_keys
|
||||
.insert(managed_bundle_key(&pkg.provider, dep_project_id));
|
||||
.insert(managed_bundle_key(pkg.provider, dep_project_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -411,7 +413,7 @@ async fn check_bundle_updates_inner(
|
|||
);
|
||||
continue;
|
||||
};
|
||||
managed_bundle_key(&installed_pkg.provider, pkg_id)
|
||||
managed_bundle_key(installed_pkg.provider, pkg_id)
|
||||
};
|
||||
|
||||
if let Some(ref bundle_name) = bundle_pkg.bundle_name {
|
||||
|
|
@ -468,7 +470,13 @@ async fn check_bundle_updates_inner(
|
|||
bundle_name = %resolved_bundle_name,
|
||||
"Checking bundle package for updates"
|
||||
);
|
||||
if installed_version_id != &new_version_id {
|
||||
if installed_version_id == &new_version_id {
|
||||
tracing::debug!(
|
||||
package_id = %pkg_id,
|
||||
version = %installed_version_id,
|
||||
"Bundle package is up to date"
|
||||
);
|
||||
} else {
|
||||
tracing::info!(
|
||||
package_id = %pkg_id,
|
||||
installed_version = %installed_version_id,
|
||||
|
|
@ -485,12 +493,6 @@ async fn check_bundle_updates_inner(
|
|||
new_file,
|
||||
installed_at: bundle_pkg.installed_at.unwrap_or_else(Utc::now),
|
||||
});
|
||||
} else {
|
||||
tracing::debug!(
|
||||
package_id = %pkg_id,
|
||||
version = %installed_version_id,
|
||||
"Bundle package is up to date"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
not_in_bundle += 1;
|
||||
|
|
@ -521,7 +523,7 @@ async fn check_bundle_updates_inner(
|
|||
// Without this, apply_single_update would try to replace a package that no longer exists.
|
||||
updates_available.retain(|u| {
|
||||
let file_id = match &u.new_file.kind {
|
||||
ModpackFileKind::Managed((pkg, _)) => pkg.id.clone(),
|
||||
ModpackFileKind::Managed(box_) => box_.0.id.clone(),
|
||||
ModpackFileKind::External(ext) => ext.sha1.clone(),
|
||||
};
|
||||
!matches!(
|
||||
|
|
@ -551,7 +553,7 @@ async fn check_bundle_updates_inner(
|
|||
}
|
||||
|
||||
let file_key = match &file.kind {
|
||||
ModpackFileKind::Managed((pkg, _)) => managed_bundle_key(&pkg.provider, &pkg.id),
|
||||
ModpackFileKind::Managed(box_) => managed_bundle_key(box_.0.provider, &box_.0.id),
|
||||
ModpackFileKind::External(ext) => external_bundle_key(&ext.sha1),
|
||||
};
|
||||
if planned_addition_keys.contains(&file_key) {
|
||||
|
|
@ -559,24 +561,22 @@ async fn check_bundle_updates_inner(
|
|||
}
|
||||
|
||||
let file_id = match &file.kind {
|
||||
ModpackFileKind::Managed((pkg, _)) => pkg.id.clone(),
|
||||
ModpackFileKind::Managed(box_) => box_.0.id.clone(),
|
||||
ModpackFileKind::External(ext) => ext.sha1.clone(),
|
||||
};
|
||||
|
||||
// Check user overrides
|
||||
if let Some(override_type) =
|
||||
overrides_map.get(&(bundle.manifest.name.clone(), file_id.clone()))
|
||||
{
|
||||
if *override_type
|
||||
&& *override_type
|
||||
== onelauncher_core::entity::cluster_bundle_overrides::OverrideType::Removed
|
||||
{
|
||||
tracing::info!(
|
||||
bundle_name = %bundle.manifest.name,
|
||||
file_id = %file_id,
|
||||
"Skipping package addition due to user override 'Removed'"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
{
|
||||
tracing::info!(
|
||||
bundle_name = %bundle.manifest.name,
|
||||
file_id = %file_id,
|
||||
"Skipping package addition due to user override 'Removed'"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
|
|
@ -616,6 +616,7 @@ async fn check_bundle_updates_inner(
|
|||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn get_bundles_with_update_status(
|
||||
cluster_id: ClusterId,
|
||||
) -> LauncherResult<Vec<BundleWithUpdateStatus>> {
|
||||
|
|
@ -648,7 +649,7 @@ pub async fn get_bundles_with_update_status(
|
|||
let key = if pkg_id == &bundle_pkg.package_hash {
|
||||
external_bundle_key(&bundle_pkg.package_hash)
|
||||
} else if let Some(installed_pkg) = all_linked_by_hash.get(&bundle_pkg.package_hash) {
|
||||
managed_bundle_key(&installed_pkg.provider, pkg_id)
|
||||
managed_bundle_key(installed_pkg.provider, pkg_id)
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
|
@ -683,19 +684,20 @@ pub async fn get_bundles_with_update_status(
|
|||
|
||||
for file in &bundle.manifest.files {
|
||||
let update_status = match &file.kind {
|
||||
ModpackFileKind::Managed((pkg, version)) => {
|
||||
let key = managed_bundle_key(&pkg.provider, &pkg.id);
|
||||
ModpackFileKind::Managed(box_) => {
|
||||
let (pkg, version) = &**box_;
|
||||
let key = managed_bundle_key(pkg.provider, &pkg.id);
|
||||
if let Some(installed) = installed_map.get(&key) {
|
||||
let installed_version =
|
||||
installed.bundle_version_id.as_deref().unwrap_or("");
|
||||
if installed_version != version.version_id {
|
||||
if installed_version == version.version_id {
|
||||
FileUpdateStatus::UpToDate
|
||||
} else {
|
||||
has_updates = true;
|
||||
FileUpdateStatus::UpdateAvailable {
|
||||
installed_version_id: installed_version.to_string(),
|
||||
new_version_id: version.version_id.clone(),
|
||||
}
|
||||
} else {
|
||||
FileUpdateStatus::UpToDate
|
||||
}
|
||||
} else {
|
||||
let key = (bundle.manifest.name.clone(), pkg.id.clone());
|
||||
|
|
@ -767,6 +769,7 @@ pub struct BundleWithUpdateStatus {
|
|||
pub has_updates: bool,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn apply_single_update(
|
||||
update: &BundlePackageUpdate,
|
||||
overrides: &[onelauncher_core::entity::cluster_bundle_overrides::Model],
|
||||
|
|
@ -786,7 +789,8 @@ async fn apply_single_update(
|
|||
|
||||
// Download the new version first. If it fails the old package remains untouched.
|
||||
let model = match &update.new_file.kind {
|
||||
ModpackFileKind::Managed((pkg, version)) => {
|
||||
ModpackFileKind::Managed(box_) => {
|
||||
let (pkg, version) = &**box_;
|
||||
tracing::debug!(
|
||||
package_id = %pkg.id,
|
||||
version_id = %version.version_id,
|
||||
|
|
@ -815,7 +819,7 @@ async fn apply_single_update(
|
|||
api::packages::link_package(&model, &cluster, Some(true)).await?;
|
||||
|
||||
let version_id = match &update.new_file.kind {
|
||||
ModpackFileKind::Managed((_, version)) => version.version_id.clone(),
|
||||
ModpackFileKind::Managed(box_) => box_.1.version_id.clone(),
|
||||
ModpackFileKind::External(ext) => ext.sha1.clone(),
|
||||
};
|
||||
|
||||
|
|
@ -834,7 +838,7 @@ async fn apply_single_update(
|
|||
|
||||
// Check if this package should be disabled based on user overrides (passed in from caller)
|
||||
let file_id = match &update.new_file.kind {
|
||||
ModpackFileKind::Managed((pkg, _)) => pkg.id.clone(),
|
||||
ModpackFileKind::Managed(box_) => box_.0.id.clone(),
|
||||
ModpackFileKind::External(ext) => ext.sha1.clone(),
|
||||
};
|
||||
|
||||
|
|
@ -919,12 +923,13 @@ async fn apply_single_removal(removal: &BundlePackageRemoval) -> LauncherResult<
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn apply_single_addition(
|
||||
addition: &BundlePackageAddition,
|
||||
overrides: &[onelauncher_core::entity::cluster_bundle_overrides::Model],
|
||||
) -> LauncherResult<onelauncher_core::entity::packages::Model> {
|
||||
let file_id = match &addition.new_file.kind {
|
||||
ModpackFileKind::Managed((pkg, _)) => pkg.id.clone(),
|
||||
ModpackFileKind::Managed(box_) => box_.0.id.clone(),
|
||||
ModpackFileKind::External(ext) => ext.sha1.clone(),
|
||||
};
|
||||
|
||||
|
|
@ -940,7 +945,8 @@ async fn apply_single_addition(
|
|||
.ok_or_else(|| anyhow::anyhow!("cluster with id {} not found", addition.cluster_id))?;
|
||||
|
||||
match &addition.new_file.kind {
|
||||
ModpackFileKind::Managed((pkg, version)) => {
|
||||
ModpackFileKind::Managed(box_) => {
|
||||
let (pkg, version) = &**box_;
|
||||
tracing::debug!(
|
||||
package_id = %pkg.id,
|
||||
version_id = %version.version_id,
|
||||
|
|
@ -1077,6 +1083,7 @@ pub struct ApplyBundleUpdatesResult {
|
|||
pub additions_failed: Vec<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn apply_bundle_updates(
|
||||
cluster_id: ClusterId,
|
||||
) -> LauncherResult<ApplyBundleUpdatesResult> {
|
||||
|
|
@ -1098,7 +1105,7 @@ pub async fn apply_bundle_updates(
|
|||
|
||||
for removal in check_result.removals_available {
|
||||
match apply_single_removal(&removal).await {
|
||||
Ok(_) => {
|
||||
Ok(()) => {
|
||||
removals_applied.push(removal);
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
@ -1118,7 +1125,7 @@ pub async fn apply_bundle_updates(
|
|||
updates_applied.push(update);
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = format!("Failed to update bundle package: {}", e);
|
||||
let msg = format!("Failed to update bundle package: {e}");
|
||||
send_error!("{}", msg);
|
||||
updates_failed.push(msg);
|
||||
}
|
||||
|
|
@ -1127,7 +1134,7 @@ pub async fn apply_bundle_updates(
|
|||
|
||||
for addition in check_result.additions_available {
|
||||
let file_id = match &addition.new_file.kind {
|
||||
ModpackFileKind::Managed((pkg, _)) => pkg.id.clone(),
|
||||
ModpackFileKind::Managed(box_) => box_.0.id.clone(),
|
||||
ModpackFileKind::External(ext) => ext.sha1.clone(),
|
||||
};
|
||||
match apply_single_addition(&addition, &cluster_overrides).await {
|
||||
|
|
@ -1135,7 +1142,7 @@ pub async fn apply_bundle_updates(
|
|||
additions_applied.push(addition);
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = format!("Failed to install new bundle package '{}': {}", file_id, e);
|
||||
let msg = format!("Failed to install new bundle package '{file_id}': {e}");
|
||||
send_error!("{}", msg);
|
||||
additions_failed.push(msg);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
mod tests {
|
||||
use onelauncher_core::entity::loader::GameLoader;
|
||||
use onelauncher_core::store::{Core, CoreOptions};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tokio::fs;
|
||||
|
||||
use crate::oneclient::bundle_updates::{apply_bundle_updates, check_bundle_updates};
|
||||
|
|
@ -13,7 +13,8 @@ mod tests {
|
|||
// We only run this if we ignore it by default, since it hits the network and modifies DB
|
||||
// but we can run it explicitly.
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
#[ignore = "touches true files and takes long"]
|
||||
#[allow(clippy::too_many_lines, clippy::items_after_statements)]
|
||||
async fn test_bundle_updates_e2e() {
|
||||
// 1. Initialize DB and paths
|
||||
Core::initialize(CoreOptions::default()).await.unwrap();
|
||||
|
|
@ -132,7 +133,7 @@ mod tests {
|
|||
|
||||
// Remove it from the database and filesystem so the updater thinks it's missing.
|
||||
// record_override=false: this simulates a missing package, not a deliberate user removal.
|
||||
println!("Removing package hash: {}", hash_to_remove);
|
||||
println!("Removing package hash: {hash_to_remove}");
|
||||
onelauncher_core::api::packages::remove_package(cluster.id, hash_to_remove, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -187,7 +188,8 @@ mod tests {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
#[ignore = "touches true files and takes long"]
|
||||
#[allow(clippy::too_many_lines, clippy::items_after_statements)]
|
||||
async fn test_bundle_user_overrides() {
|
||||
// 1. Initialize DB and paths
|
||||
Core::initialize(CoreOptions::default()).await.unwrap();
|
||||
|
|
@ -300,10 +302,9 @@ mod tests {
|
|||
|
||||
let is_offered_as_addition = check_removed.additions_available.iter().any(|a| {
|
||||
let file_id = match &a.new_file.kind {
|
||||
onelauncher_core::api::packages::modpack::data::ModpackFileKind::Managed((
|
||||
pkg,
|
||||
_,
|
||||
)) => pkg.id.clone(),
|
||||
onelauncher_core::api::packages::modpack::data::ModpackFileKind::Managed(box_) => {
|
||||
box_.0.id.clone()
|
||||
}
|
||||
onelauncher_core::api::packages::modpack::data::ModpackFileKind::External(ext) => {
|
||||
ext.sha1.clone()
|
||||
}
|
||||
|
|
@ -373,8 +374,8 @@ mod tests {
|
|||
.new_file;
|
||||
|
||||
let file_id = match &updated_pkg_hash.kind {
|
||||
onelauncher_core::api::packages::modpack::data::ModpackFileKind::Managed((pkg, _)) => {
|
||||
pkg.id.clone()
|
||||
onelauncher_core::api::packages::modpack::data::ModpackFileKind::Managed(box_) => {
|
||||
box_.0.id.clone()
|
||||
}
|
||||
onelauncher_core::api::packages::modpack::data::ModpackFileKind::External(ext) => {
|
||||
ext.sha1.clone()
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ use tokio::sync::{OnceCell, RwLock};
|
|||
/// e.g.
|
||||
/// ```json
|
||||
/// {
|
||||
/// "versions": {
|
||||
/// "1.21.5": {
|
||||
/// "fabric": ["/generated/hud-fabric-1.21.5.mrpack"],
|
||||
/// "forge": ["/generated/hud-forge-1.21.5.mrpack"]
|
||||
/// }
|
||||
/// },
|
||||
/// "versions": {
|
||||
/// "1.21.5": {
|
||||
/// "fabric": ["/generated/hud-fabric-1.21.5.mrpack"],
|
||||
/// "forge": ["/generated/hud-forge-1.21.5.mrpack"]
|
||||
/// }
|
||||
/// },
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
|
|
@ -59,20 +59,20 @@ impl BundlesManager {
|
|||
|
||||
// Fast path: check cache under read lock.
|
||||
let bundles_lock = self.bundles.read().await;
|
||||
if let Some(entry) = bundles_lock.get(mc_version) {
|
||||
if let Some(bundles) = entry.get(&loader) {
|
||||
return Ok(bundles.clone());
|
||||
}
|
||||
if let Some(entry) = bundles_lock.get(mc_version)
|
||||
&& let Some(bundles) = entry.get(&loader)
|
||||
{
|
||||
return Ok(bundles.clone());
|
||||
}
|
||||
drop(bundles_lock);
|
||||
|
||||
// Slow path: acquire write lock and re-check before populating (double-checked locking).
|
||||
// This closes the TOCTOU gap between dropping the read lock and acquiring the write lock.
|
||||
let mut bundles_lock = self.bundles.write().await;
|
||||
if let Some(entry) = bundles_lock.get(mc_version) {
|
||||
if let Some(bundles) = entry.get(&loader) {
|
||||
return Ok(bundles.clone());
|
||||
}
|
||||
if let Some(entry) = bundles_lock.get(mc_version)
|
||||
&& let Some(bundles) = entry.get(&loader)
|
||||
{
|
||||
return Ok(bundles.clone());
|
||||
}
|
||||
|
||||
let mut found = Vec::new();
|
||||
|
|
@ -88,7 +88,7 @@ impl BundlesManager {
|
|||
|
||||
// we will be first checking the disk cache, if that fails we fetch from remote
|
||||
for path in paths {
|
||||
let Some(file_name) = path.split('/').last() else {
|
||||
let Some(file_name) = path.split('/').next_back() else {
|
||||
tracing::error!("no bundle name was found in path: {path}");
|
||||
continue;
|
||||
};
|
||||
|
|
@ -199,14 +199,14 @@ async fn download_and_load_bundle(
|
|||
.headers()
|
||||
.get(reqwest::header::ETAG)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_string());
|
||||
.map(std::string::ToString::to_string);
|
||||
let stored_etag: Option<String> = tokio::fs::read_to_string(&etag_path).await.ok();
|
||||
|
||||
if let (Some(server), Some(stored)) = (&server_etag, &stored_etag) {
|
||||
// ETag comparison: most reliable cache validation method.
|
||||
if server == stored {
|
||||
tracing::debug!("bundle cache hit via ETag for: {url}");
|
||||
return Ok(ModpackFormat::from_file(disk_path).await?);
|
||||
return ModpackFormat::from_file(disk_path).await;
|
||||
}
|
||||
} else {
|
||||
// Fall back to Content-Length comparison when no ETag is available.
|
||||
|
|
@ -219,7 +219,7 @@ async fn download_and_load_bundle(
|
|||
if let Some(length) = content_length {
|
||||
let file_size = io::stat(disk_path).await.map(|m| m.len()).unwrap_or(0);
|
||||
if length == file_size {
|
||||
return Ok(ModpackFormat::from_file(disk_path).await?);
|
||||
return ModpackFormat::from_file(disk_path).await;
|
||||
}
|
||||
}
|
||||
// If neither ETag nor Content-Length are available, fall through to re-download.
|
||||
|
|
@ -230,13 +230,13 @@ async fn download_and_load_bundle(
|
|||
status = %res.status(),
|
||||
"failed to validate remote bundle cache for {url}; using cached local bundle"
|
||||
);
|
||||
return Ok(ModpackFormat::from_file(disk_path).await?);
|
||||
return ModpackFormat::from_file(disk_path).await;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"failed to validate remote bundle cache for {url}: {e}; using cached local bundle"
|
||||
);
|
||||
return Ok(ModpackFormat::from_file(disk_path).await?);
|
||||
return ModpackFormat::from_file(disk_path).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -245,15 +245,14 @@ async fn download_and_load_bundle(
|
|||
http::download(Method::GET, &url, disk_path, None, None).await?;
|
||||
|
||||
// After a fresh download, persist the server ETag for next time.
|
||||
if let Ok(head_res) = http::request(Method::HEAD, &url).await {
|
||||
if let Some(etag) = head_res
|
||||
if let Ok(head_res) = http::request(Method::HEAD, &url).await
|
||||
&& let Some(etag) = head_res
|
||||
.headers()
|
||||
.get(reqwest::header::ETAG)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
{
|
||||
let _ = tokio::fs::write(&etag_path, etag).await;
|
||||
}
|
||||
{
|
||||
let _ = tokio::fs::write(&etag_path, etag).await;
|
||||
}
|
||||
|
||||
Ok(ModpackFormat::from_file(disk_path).await?)
|
||||
ModpackFormat::from_file(disk_path).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,37 +13,37 @@ use serde::{Deserialize, Serialize};
|
|||
/// e.g.
|
||||
/// ```json
|
||||
/// {
|
||||
/// "clusters": [
|
||||
/// {
|
||||
/// "major_version": 21,
|
||||
/// "name": "Tricky Trials",
|
||||
/// "art": "/versions/art/Tricky_Trials.png",
|
||||
/// "entries": [
|
||||
/// {
|
||||
/// "minor_version": 5,
|
||||
/// "loader": "fabric",
|
||||
/// "tags": ["PvP", "Survival"]
|
||||
/// },
|
||||
/// {
|
||||
/// "minor_version": 5,
|
||||
/// "loader": "forge",
|
||||
/// "tags": ["PvP", "Survival"]
|
||||
/// }
|
||||
/// ]
|
||||
/// },
|
||||
/// {
|
||||
/// "major_version": 20,
|
||||
/// "name": "Trails & Tales",
|
||||
/// "art": "/versions/art/Trails_Tales.png",
|
||||
/// "entries": [
|
||||
/// {
|
||||
/// "minor_version": 5,
|
||||
/// "loader": "fabric",
|
||||
/// "tags": ["PvP", "Survival"]
|
||||
/// }
|
||||
/// ]
|
||||
/// }
|
||||
/// ]
|
||||
/// "clusters": [
|
||||
/// {
|
||||
/// "major_version": 21,
|
||||
/// "name": "Tricky Trials",
|
||||
/// "art": "/versions/art/Tricky_Trials.png",
|
||||
/// "entries": [
|
||||
/// {
|
||||
/// "minor_version": 5,
|
||||
/// "loader": "fabric",
|
||||
/// "tags": ["PvP", "Survival"]
|
||||
/// },
|
||||
/// {
|
||||
/// "minor_version": 5,
|
||||
/// "loader": "forge",
|
||||
/// "tags": ["PvP", "Survival"]
|
||||
/// }
|
||||
/// ]
|
||||
/// },
|
||||
/// {
|
||||
/// "major_version": 20,
|
||||
/// "name": "Trails & Tales",
|
||||
/// "art": "/versions/art/Trails_Tales.png",
|
||||
/// "entries": [
|
||||
/// {
|
||||
/// "minor_version": 5,
|
||||
/// "loader": "fabric",
|
||||
/// "tags": ["PvP", "Survival"]
|
||||
/// }
|
||||
/// ]
|
||||
/// }
|
||||
/// ]
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(specta::Type, Deserialize, Serialize)]
|
||||
|
|
@ -90,7 +90,9 @@ pub async fn get_data_storage_versions() -> LauncherResult<OnlineClusterManifest
|
|||
}
|
||||
|
||||
/// Download an art image (e.g. `/versions/art/Foo.png`) from the data storage CDN and
|
||||
/// save it to the local cache directory. Returns the OS-native path to the cached file,
|
||||
/// save it to the local cache directory.
|
||||
///
|
||||
/// Returns the OS-native path to the cached file,
|
||||
/// which the frontend converts to an `asset://` URL via `convertFileSrc`.
|
||||
/// If the file is already cached it is returned immediately without a network request.
|
||||
pub async fn cache_art_image(path: &str) -> LauncherResult<String> {
|
||||
|
|
@ -117,6 +119,7 @@ pub async fn cache_art_image(path: &str) -> LauncherResult<String> {
|
|||
}
|
||||
|
||||
/// Re-download an art image and overwrite the on-disk cache.
|
||||
///
|
||||
/// Errors are silently ignored — the existing cached version is kept on failure.
|
||||
/// Call this from `tokio::spawn` so it runs fully in the background.
|
||||
pub async fn refresh_art_cache(path: &str) {
|
||||
|
|
@ -162,7 +165,7 @@ pub async fn init_clusters() -> LauncherResult<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn cluster_name(mc_version: &str, loader: &GameLoader) -> String {
|
||||
fn cluster_name(mc_version: &str, loader: GameLoader) -> String {
|
||||
format!("{mc_version} {loader}")
|
||||
}
|
||||
|
||||
|
|
@ -171,7 +174,7 @@ async fn create_cluster_if_not_exist(
|
|||
mc_loader: GameLoader,
|
||||
mc_loader_version: Option<&str>,
|
||||
) -> LauncherResult<Option<ClusterModel>> {
|
||||
let name = cluster_name(mc_version, &mc_loader);
|
||||
let name = cluster_name(mc_version, mc_loader);
|
||||
|
||||
let cluster =
|
||||
onelauncher_core::api::cluster::dao::get_cluster_by_folder_name(Path::new(&name)).await?;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ pub async fn initialize_oneclient() {
|
|||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn check_and_apply_all_bundle_updates() {
|
||||
BUNDLE_SYNCING.store(true, Ordering::Relaxed);
|
||||
tracing::info!("checking for bundle updates...");
|
||||
|
|
@ -120,7 +121,7 @@ async fn check_and_apply_all_bundle_updates() {
|
|||
|
||||
for addition in &result.additions_applied {
|
||||
let file_id = match &addition.new_file.kind {
|
||||
onelauncher_core::api::packages::modpack::data::ModpackFileKind::Managed((pkg, _)) => pkg.id.clone(),
|
||||
onelauncher_core::api::packages::modpack::data::ModpackFileKind::Managed(box_) => box_.0.id.clone(),
|
||||
onelauncher_core::api::packages::modpack::data::ModpackFileKind::External(ext) => ext.sha1.clone(),
|
||||
};
|
||||
tracing::info!(
|
||||
|
|
@ -147,13 +148,13 @@ async fn check_and_apply_all_bundle_updates() {
|
|||
if total_updates_applied > 0 || total_removals_applied > 0 || total_additions_applied > 0 {
|
||||
let mut message_parts = Vec::new();
|
||||
if total_updates_applied > 0 {
|
||||
message_parts.push(format!("{} mod(s) updated", total_updates_applied));
|
||||
message_parts.push(format!("{total_updates_applied} mod(s) updated"));
|
||||
}
|
||||
if total_removals_applied > 0 {
|
||||
message_parts.push(format!("{} mod(s) removed", total_removals_applied));
|
||||
message_parts.push(format!("{total_removals_applied} mod(s) removed"));
|
||||
}
|
||||
if total_additions_applied > 0 {
|
||||
message_parts.push(format!("{} mod(s) added", total_additions_applied));
|
||||
message_parts.push(format!("{total_additions_applied} mod(s) added"));
|
||||
}
|
||||
send_info!("Bundle sync: {}", message_parts.join(", "));
|
||||
tracing::info!(
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export type LauncherError = { type: "DirError"; data: DirectoryError } | { type:
|
|||
|
||||
export type ManagedPackage = { id: string; slug: string; provider: Provider; package_type: PackageType; name: string; short_desc: string; body: ManagedPackageBody;
|
||||
/**
|
||||
* Won't have all versions for some providers, like CurseForge.
|
||||
* Won't have all versions for some providers, like `CurseForge`.
|
||||
* Try making a request to get the versions if needed
|
||||
*/
|
||||
version_ids: string[]; mc_versions: string[]; loaders: GameLoader[]; icon_url: string | null; created: string; updated: string; client: PackageSide; server: PackageSide; categories: PackageCategories; license: PackageLicense | null; author: PackageAuthor; links: PackageLinks; status: PackageStatus; downloads: number; gallery: PackageGallery[] }
|
||||
|
|
@ -313,8 +313,32 @@ export type VersionType =
|
|||
*/
|
||||
"old_beta"
|
||||
|
||||
const ARGS_MAP = { 'core':'{"getPackage":["provider","slug"],"updateClusterById":["id","request"],"setClusterStage":["id","stage"],"refreshAccount":["uuid"],"readSettings":[],"getProfileOrDefault":["name"],"getMultiplePackages":["provider","slugs"],"getProcessLogTail":["id","max_lines"],"fetchLoggedInProfile":["access_token"],"launchCluster":["id","uuid","search_for_java"],"togglePackage":["cluster_id","package_hash"],"changeCape":["access_token","cape_uuid"],"removeCape":["access_token"],"open":["input"],"getPackageBody":["provider","body"],"getClusters":[],"removePackage":["cluster_id","package_hash"],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"setDiscordRPCMessage":["message"],"getUsersFromAuthor":["provider","author"],"getClusterById":["id"],"getWorlds":["id"],"openMsaLogin":[],"getRunningProcesses":[],"isClusterRunning":["cluster_id"],"getGameVersions":[],"getRunningProcessesByClusterId":["cluster_id"],"killProcess":["pid"],"removeUser":["uuid"],"refreshAccounts":[],"searchPackages":["provider","query"],"getLinkedPackages":["cluster_id"],"getGlobalProfile":[],"fetchMinecraftProfile":["uuid"],"getUser":["uuid"],"installModpack":["modpack","cluster_id"],"createCluster":["options"],"changeSkin":["access_token","skin_url","skin_variant"],"getScreenshots":["id"],"getUsers":[],"convertUsernameUUID":["username_uuid"],"getLogByName":["id","name"],"createSettingsProfile":["name"],"getDefaultUser":["fallback"],"syncCluster":["cluster_id"],"updateClusterProfile":["name","profile"],"removeCluster":["id"],"writeSettings":["setting"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getLoadersForVersion":["mc_version"],"setDefaultUser":["uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"getLogs":["id"]}', 'oneclient':'{"installUpdate":[],"getBundlesFor":["cluster_id"],"extractBundleOverrides":["bundle_path","cluster_id"],"updateBundlePackages":["cluster_id"],"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"],"cacheArt":["path"],"refreshArt":["path"],"checkForUpdate":[],"getVersions":[],"getClustersGroupedByMajor":[],"isBundleSyncing":[]}', 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'debug':'{"getBuildTimestamp":[],"getGitCommitHash":[],"getPlatform":[],"getType":[],"getLocale":[],"openDevTools":[],"getFamily":[],"getOsVersion":[],"getPackageVersion":[],"getArch":[],"isInDev":[]}' }
|
||||
export type Router = { 'core': { getClusters: () => Promise<ClusterModel[]>,
|
||||
const ARGS_MAP = { 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'core':'{"getGameVersions":[],"getScreenshots":["id"],"setDefaultUser":["uuid"],"removeUser":["uuid"],"isClusterRunning":["cluster_id"],"killProcess":["pid"],"updateClusterById":["id","request"],"writeSettings":["setting"],"getWorlds":["id"],"setDiscordRPCMessage":["message"],"removeCape":["access_token"],"getLinkedPackages":["cluster_id"],"getPackageBody":["provider","body"],"removeCluster":["id"],"syncCluster":["cluster_id"],"launchCluster":["id","uuid","search_for_java"],"getLogByName":["id","name"],"getProfileOrDefault":["name"],"getClusterById":["id"],"getLogs":["id"],"getLoadersForVersion":["mc_version"],"openMsaLogin":[],"getRunningProcessesByClusterId":["cluster_id"],"getGlobalProfile":[],"refreshAccounts":[],"getRunningProcesses":[],"readSettings":[],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"getPackage":["provider","slug"],"fetchMinecraftProfile":["uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"setClusterStage":["id","stage"],"createSettingsProfile":["name"],"open":["input"],"createCluster":["options"],"updateClusterProfile":["name","profile"],"refreshAccount":["uuid"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getUsers":[],"getUser":["uuid"],"getUsersFromAuthor":["provider","author"],"getMultiplePackages":["provider","slugs"],"changeCape":["access_token","cape_uuid"],"changeSkin":["access_token","skin_url","skin_variant"],"convertUsernameUUID":["username_uuid"],"removePackage":["cluster_id","package_hash"],"searchPackages":["provider","query"],"getClusters":[],"togglePackage":["cluster_id","package_hash"],"installModpack":["modpack","cluster_id"],"getProcessLogTail":["id","max_lines"],"fetchLoggedInProfile":["access_token"],"getDefaultUser":["fallback"]}', 'debug':'{"getBuildTimestamp":[],"getType":[],"getPackageVersion":[],"openDevTools":[],"getPlatform":[],"getLocale":[],"isInDev":[],"getArch":[],"getOsVersion":[],"getGitCommitHash":[],"getFamily":[]}', 'oneclient':'{"installUpdate":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"],"extractBundleOverrides":["bundle_path","cluster_id"],"checkForUpdate":[],"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"],"updateBundlePackages":["cluster_id"],"cacheArt":["path"],"refreshArt":["path"],"isBundleSyncing":[],"getVersions":[]}', 'events':'{"message":["event"],"ingress":["event"],"process":["event"]}' }
|
||||
export type Router = { 'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> },
|
||||
'debug': { openDevTools: () => Promise<void>,
|
||||
isInDev: () => Promise<boolean>,
|
||||
getArch: () => Promise<string>,
|
||||
getFamily: () => Promise<string>,
|
||||
getLocale: () => Promise<string | null>,
|
||||
getType: () => Promise<string>,
|
||||
getPlatform: () => Promise<string>,
|
||||
getOsVersion: () => Promise<string>,
|
||||
getGitCommitHash: () => Promise<string>,
|
||||
getBuildTimestamp: () => Promise<string>,
|
||||
getPackageVersion: () => Promise<string> },
|
||||
'oneclient': { getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
|
||||
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]>,
|
||||
getVersions: () => Promise<OnlineClusterManifest>,
|
||||
extractBundleOverrides: (bundlePath: string, clusterId: number) => Promise<null>,
|
||||
checkForUpdate: () => Promise<Update | null>,
|
||||
installUpdate: () => Promise<null>,
|
||||
downloadPackageFromBundle: (package: ModpackFileKind, clusterId: number, bundleName: string, skipCompatibility: boolean | null) => Promise<PackageModel>,
|
||||
updateBundlePackages: (clusterId: number) => Promise<ApplyBundleUpdatesResult>,
|
||||
isBundleSyncing: () => Promise<boolean>,
|
||||
cacheArt: (path: string) => Promise<string>,
|
||||
refreshArt: (path: string) => Promise<null> },
|
||||
'core': { getClusters: () => Promise<ClusterModel[]>,
|
||||
getClusterById: (id: number) => Promise<ClusterModel | null>,
|
||||
removeCluster: (id: number) => Promise<null>,
|
||||
createCluster: (options: CreateCluster) => Promise<ClusterModel>,
|
||||
|
|
@ -370,31 +394,7 @@ setDiscordRPCMessage: (message: string) => Promise<null>,
|
|||
open: (input: string) => Promise<null> },
|
||||
'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> },
|
||||
'oneclient': { getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
|
||||
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]>,
|
||||
getVersions: () => Promise<OnlineClusterManifest>,
|
||||
extractBundleOverrides: (bundlePath: string, clusterId: number) => Promise<null>,
|
||||
checkForUpdate: () => Promise<Update | null>,
|
||||
installUpdate: () => Promise<null>,
|
||||
downloadPackageFromBundle: (package: ModpackFileKind, clusterId: number, bundleName: string, skipCompatibility: boolean | null) => Promise<PackageModel>,
|
||||
updateBundlePackages: (clusterId: number) => Promise<ApplyBundleUpdatesResult>,
|
||||
isBundleSyncing: () => Promise<boolean>,
|
||||
cacheArt: (path: string) => Promise<string>,
|
||||
refreshArt: (path: string) => Promise<null> },
|
||||
'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> },
|
||||
'debug': { openDevTools: () => Promise<void>,
|
||||
isInDev: () => Promise<boolean>,
|
||||
getArch: () => Promise<string>,
|
||||
getFamily: () => Promise<string>,
|
||||
getLocale: () => Promise<string | null>,
|
||||
getType: () => Promise<string>,
|
||||
getPlatform: () => Promise<string>,
|
||||
getOsVersion: () => Promise<string>,
|
||||
getGitCommitHash: () => Promise<string>,
|
||||
getBuildTimestamp: () => Promise<string>,
|
||||
getPackageVersion: () => Promise<string> } };
|
||||
process: (event: ProcessPayload) => Promise<void> } };
|
||||
|
||||
|
||||
export type { InferCommandOutput }
|
||||
|
|
|
|||
|
|
@ -60,11 +60,11 @@ function getFileKey(file: ModpackFile): string {
|
|||
if ('Managed' in file.kind)
|
||||
return `managed:${file.kind.Managed[0].id}`;
|
||||
if ('External' in file.kind)
|
||||
return `external:${file.kind.External.sha1 ?? file.kind.External.url ?? file.kind.External.name}`;
|
||||
return `external:${file.kind.External.sha1 || file.kind.External.url || file.kind.External.name}`;
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
const modMetadataCache = new Map<string, Promise<ModInfo>>();
|
||||
const modMetadataCache: Map<string, Promise<ModInfo>> = new Map();
|
||||
|
||||
function fetchModMetaData(file: ModpackFile, useVerticalGridLayout?: boolean): Promise<ModInfo> {
|
||||
const key = getFileKey(file);
|
||||
|
|
@ -136,16 +136,17 @@ export function ModCard({ file, cluster }: ModCardProps) {
|
|||
if (!cancelled)
|
||||
setModMetadata(meta);
|
||||
}).catch(() => {});
|
||||
return () => { cancelled = true; };
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [file, useVerticalGridLayout]);
|
||||
|
||||
const kind = file.kind;
|
||||
// Directory-based packages (folders dropped into resourcepacks/shaderpacks/datapacks)
|
||||
// cannot be toggled via .disabled rename — skip toggle mode for them.
|
||||
const isDirectory = useMemo(() =>
|
||||
'External' in kind && !kind.External.name.includes('.'),
|
||||
[kind]);
|
||||
const effectiveToggleMode = useToggleMode && !isDirectory;
|
||||
// Directory-based packages (folders dropped into resourcepacks/shaderpacks/datapacks)
|
||||
// cannot be toggled via .disabled rename — skip toggle mode for them.
|
||||
const isDirectory = useMemo(() =>
|
||||
'External' in kind && !kind.External.name.includes('.'), [kind]);
|
||||
const effectiveToggleMode = useToggleMode && !isDirectory;
|
||||
const isInstalled = useMemo(() => {
|
||||
if (installedPackages)
|
||||
if ('Managed' in kind) {
|
||||
|
|
@ -161,18 +162,18 @@ export function ModCard({ file, cluster }: ModCardProps) {
|
|||
|
||||
// In toggle mode, "selected" = enabled (file_name doesn't end with .disabled)
|
||||
const isFileEnabled = useMemo(() => {
|
||||
if (!effectiveToggleMode)
|
||||
return true;
|
||||
if ('Managed' in kind) {
|
||||
const primary = kind.Managed[1].files.find(f => f.primary) ?? kind.Managed[1].files[0];
|
||||
return !primary?.file_name.endsWith('.disabled');
|
||||
}
|
||||
return !kind.External.name.endsWith('.disabled');
|
||||
}, [effectiveToggleMode, kind]);
|
||||
const [isSelected, setSelected] = useState(effectiveToggleMode ? isFileEnabled : isInstalled);
|
||||
useEffect(() => {
|
||||
setSelected(effectiveToggleMode ? isFileEnabled : isInstalled);
|
||||
}, [isInstalled, effectiveToggleMode, isFileEnabled]);
|
||||
if (!effectiveToggleMode)
|
||||
return true;
|
||||
if ('Managed' in kind) {
|
||||
const primary = kind.Managed[1].files.find(f => f.primary) ?? kind.Managed[1].files[0];
|
||||
return !primary.file_name.endsWith('.disabled');
|
||||
}
|
||||
return !kind.External.name.endsWith('.disabled');
|
||||
}, [effectiveToggleMode, kind]);
|
||||
const [isSelected, setSelected] = useState(effectiveToggleMode ? isFileEnabled : isInstalled);
|
||||
useEffect(() => {
|
||||
setSelected(effectiveToggleMode ? isFileEnabled : isInstalled);
|
||||
}, [isInstalled, effectiveToggleMode, isFileEnabled]);
|
||||
|
||||
const download = useCommandMut(async () => {
|
||||
if ('Managed' in kind) {
|
||||
|
|
@ -223,11 +224,11 @@ export function ModCard({ file, cluster }: ModCardProps) {
|
|||
});
|
||||
|
||||
const handleOnClick = () => {
|
||||
// Folder-based packages (resource packs, shader packs, data packs as directories)
|
||||
// cannot be toggled or removed — treat them as entirely non-interactive.
|
||||
if (isDirectory)
|
||||
return;
|
||||
if (effectiveToggleMode)
|
||||
// Folder-based packages (resource packs, shader packs, data packs as directories)
|
||||
// cannot be toggled or removed — treat them as entirely non-interactive.
|
||||
if (isDirectory)
|
||||
return;
|
||||
if (effectiveToggleMode)
|
||||
(async () => {
|
||||
await toggle.mutateAsync();
|
||||
await queryClient.invalidateQueries({ queryKey: ['getLinkedPackages', cluster.id] });
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ClusterModel, ModpackArchive } from '@/bindings.gen';
|
||||
import { Bundle, ModCardContext, getModMetaDataName } from '@/components';
|
||||
import { Bundle, getModMetaDataName, ModCardContext } from '@/components';
|
||||
import { useSettings } from '@/hooks/useSettings';
|
||||
import { Button, Tab, TabContent, TabList, TabPanel, Tabs, TextField } from '@onelauncher/common/components';
|
||||
import { SearchMdIcon } from '@untitled-theme/icons-react';
|
||||
|
|
@ -77,30 +77,30 @@ export function ModList({ bundles, cluster, selectedTab, onTabChange, toggleBund
|
|||
|
||||
{isSearching
|
||||
? (
|
||||
<div className={useVerticalGridLayout ? 'pt-0' : 'p-2'}>
|
||||
<Bundle cluster={cluster} files={searchResults} />
|
||||
</div>
|
||||
)
|
||||
<div className={useVerticalGridLayout ? 'pt-0' : 'p-2'}>
|
||||
<Bundle cluster={cluster} files={searchResults} />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<TabContent className={useVerticalGridLayout ? 'pt-0' : ''}>
|
||||
{nonEmptyBundles.map((bundleData) => {
|
||||
const isToggle = toggleBundlePaths?.has(bundleData.path) ?? false;
|
||||
const content = <Bundle cluster={cluster} files={bundleData.manifest.files} />;
|
||||
<TabContent className={useVerticalGridLayout ? 'pt-0' : ''}>
|
||||
{nonEmptyBundles.map((bundleData) => {
|
||||
const isToggle = toggleBundlePaths?.has(bundleData.path) ?? false;
|
||||
const content = <Bundle cluster={cluster} files={bundleData.manifest.files} />;
|
||||
|
||||
return (
|
||||
<TabPanel className={useVerticalGridLayout ? 'max-w-192' : ''} key={getBundleName(bundleData.manifest.name)} value={getBundleName(bundleData.manifest.name)}>
|
||||
{isToggle
|
||||
? (
|
||||
<ModCardContext.Provider value={{ ...ctx, useToggleMode: true }}>
|
||||
{content}
|
||||
</ModCardContext.Provider>
|
||||
)
|
||||
: content}
|
||||
</TabPanel>
|
||||
);
|
||||
})}
|
||||
</TabContent>
|
||||
)}
|
||||
return (
|
||||
<TabPanel className={useVerticalGridLayout ? 'max-w-192' : ''} key={getBundleName(bundleData.manifest.name)} value={getBundleName(bundleData.manifest.name)}>
|
||||
{isToggle
|
||||
? (
|
||||
<ModCardContext.Provider value={{ ...ctx, useToggleMode: true }}>
|
||||
{content}
|
||||
</ModCardContext.Provider>
|
||||
)
|
||||
: content}
|
||||
</TabPanel>
|
||||
);
|
||||
})}
|
||||
</TabContent>
|
||||
)}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ function DownloadingMods({ mods, setOpen, nextPath }: { mods: ModDataArray; setO
|
|||
};
|
||||
|
||||
downloadAll();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- download is not stable, adding it would cause infinite loops
|
||||
}, [mods]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ export function SkinViewer({
|
|||
return () => {
|
||||
viewer.dispose();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentional mount-only initialization
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function useSettings() {
|
|||
|
||||
const createSetting = useMemo(() => <TKey extends keyof Settings>(key: TKey): [Settings[TKey], Setter<TKey>] => {
|
||||
return [setting(key), value => setSetting(key, value)];
|
||||
}, [setting]);
|
||||
}, [setting, setSetting]);
|
||||
|
||||
return { setting, setSetting, createSetting };
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ export function useClusterProfile(cluster: ClusterModel) {
|
|||
throw new Error('No settings profile name');
|
||||
await bindings.core.updateClusterProfile(name, { ...emptyUpdate, ...update });
|
||||
profileSrc.refetch();
|
||||
}, [profileName, cluster.name, cluster.id]);
|
||||
}, [profileName, cluster.name, cluster.id, profileSrc]);
|
||||
// const updateProfile = useMemo(() => (update: Partial<ProfileUpdate>) => setProfile({ ...profile.data, ...update }), [setProfile, profile.data]);
|
||||
return { profile, setProfile, updateProfile };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { save } from '@tauri-apps/plugin-dialog';
|
|||
import { exists, mkdir, readTextFile, writeFile, writeTextFile } from '@tauri-apps/plugin-fs';
|
||||
import { Download01Icon, PlusIcon, Trash01Icon } from '@untitled-theme/icons-react';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { CrouchAnimation, FlyingAnimation, HitAnimation, IdleAnimation, WalkingAnimation } from 'skinview3d';
|
||||
|
||||
interface Skin {
|
||||
|
|
@ -120,11 +120,11 @@ function RouteComponent() {
|
|||
if (!profileData)
|
||||
return;
|
||||
setCapes([{ url: '', id: '' }, ...profileData.capes.map(cape => ({ url: cape.url, id: cape.id }))]);
|
||||
}, []);
|
||||
}, [profileData]);
|
||||
|
||||
const [skins, setSkins, loaded] = useSkinHistory();
|
||||
const [selectedSkin, setSelectedSkin] = useState<Skin>({ skin_url: getSkinUrl(null), is_slim: false });
|
||||
const skinData: Skin = { is_slim: profileData?.skins[0].variant === 'slim', skin_url: getSkinUrl(profileData?.skins[0].url), cape_url: playerData?.cape_url ?? '' };
|
||||
const skinData: Skin = useMemo(() => ({ is_slim: profileData?.skins[0].variant === 'slim', skin_url: getSkinUrl(profileData?.skins[0].url), cape_url: playerData?.cape_url ?? '' }), [playerData?.cape_url, profileData?.skins]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loaded)
|
||||
|
|
@ -133,7 +133,7 @@ function RouteComponent() {
|
|||
setSkins(prev => [...prev, skinData]);
|
||||
setSelectedSkin(skinData);
|
||||
setSelectedCape(skinData.cape_url ?? '');
|
||||
}, [loaded]);
|
||||
}, [loaded, setSkins, skinData]);
|
||||
|
||||
const importFromURL = (url: string) => {
|
||||
setSkins(prev => [...prev, { is_slim: false, skin_url: url }]);
|
||||
|
|
|
|||
|
|
@ -26,8 +26,7 @@ function RouteComponent() {
|
|||
const customBundle = useCustomBundle(bundles, installedPackages, cluster, 'datapack');
|
||||
|
||||
const allBundles = useMemo(() =>
|
||||
customBundle !== null ? [...filteredBundles, customBundle] : filteredBundles,
|
||||
[filteredBundles, customBundle]);
|
||||
customBundle !== null ? [...filteredBundles, customBundle] : filteredBundles, [filteredBundles, customBundle]);
|
||||
|
||||
const customTogglePaths = useMemo(() => new Set(['__custom__']), []);
|
||||
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ function RouteComponent() {
|
|||
const customBundle = useCustomBundle(bundles, installedPackages, cluster, 'mod');
|
||||
|
||||
const allBundles = useMemo(() =>
|
||||
customBundle !== null ? [...filteredBundles, customBundle] : filteredBundles,
|
||||
[filteredBundles, customBundle]);
|
||||
customBundle !== null ? [...filteredBundles, customBundle] : filteredBundles, [filteredBundles, customBundle]);
|
||||
|
||||
const customTogglePaths = useMemo(() => new Set(['__custom__']), []);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { ModCardContextApi } from '@/components';
|
||||
import { ModCardContext, ModList } from '@/components';
|
||||
import { useCustomBundle } from '@/hooks/useCustomBundle';
|
||||
import { getFilePackageType } from '@/routes/app/cluster/mods';
|
||||
import { bindings } from '@/main';
|
||||
import { getFilePackageType } from '@/routes/app/cluster/mods';
|
||||
import { useCommandSuspense } from '@onelauncher/common';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
|
|
@ -26,8 +26,7 @@ function RouteComponent() {
|
|||
const customBundle = useCustomBundle(bundles, installedPackages, cluster, 'resourcepack');
|
||||
|
||||
const allBundles = useMemo(() =>
|
||||
customBundle !== null ? [...filteredBundles, customBundle] : filteredBundles,
|
||||
[filteredBundles, customBundle]);
|
||||
customBundle !== null ? [...filteredBundles, customBundle] : filteredBundles, [filteredBundles, customBundle]);
|
||||
|
||||
const customTogglePaths = useMemo(() => new Set(['__custom__']), []);
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,7 @@ function RouteComponent() {
|
|||
const customBundle = useCustomBundle(bundles, installedPackages, cluster, 'shader');
|
||||
|
||||
const allBundles = useMemo(() =>
|
||||
customBundle !== null ? [...filteredBundles, customBundle] : filteredBundles,
|
||||
[filteredBundles, customBundle]);
|
||||
customBundle !== null ? [...filteredBundles, customBundle] : filteredBundles, [filteredBundles, customBundle]);
|
||||
|
||||
const customTogglePaths = useMemo(() => new Set(['__custom__']), []);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import { motion } from 'motion/react';
|
|||
import { Button as AriaButton } from 'react-aria-components';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
|
||||
export const Route = createFileRoute('/app/')({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
|
@ -141,15 +140,15 @@ function RecentsCard({
|
|||
<div className="flex w-full h-full justify-start items-end px-6 py-3 hover:brightness-80">
|
||||
{artSrc
|
||||
? (
|
||||
<img
|
||||
alt={`Minecraft ${version} landscape`}
|
||||
className="absolute -z-10 left-0 top-0 w-full h-full object-cover scale-110"
|
||||
src={artSrc}
|
||||
/>
|
||||
)
|
||||
<img
|
||||
alt={`Minecraft ${version} landscape`}
|
||||
className="absolute -z-10 left-0 top-0 w-full h-full object-cover scale-110"
|
||||
src={artSrc}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<GameBackground className="absolute -z-10 left-0 top-0 w-full h-full scale-110" name={versionInfo!.backgroundName} />
|
||||
)}
|
||||
<GameBackground className="absolute -z-10 left-0 top-0 w-full h-full scale-110" name={versionInfo!.backgroundName} />
|
||||
)}
|
||||
|
||||
<div
|
||||
className="absolute top-0 left-0 -z-10 w-full h-full"
|
||||
|
|
|
|||
|
|
@ -108,18 +108,18 @@ function BackgroundGradient() {
|
|||
>
|
||||
{artSrc
|
||||
? (
|
||||
<img
|
||||
alt=""
|
||||
className="absolute left-0 top-0 w-screen h-screen scale-110 object-cover"
|
||||
src={artSrc}
|
||||
/>
|
||||
)
|
||||
<img
|
||||
alt=""
|
||||
className="absolute left-0 top-0 w-screen h-screen scale-110 object-cover"
|
||||
src={artSrc}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<GameBackground
|
||||
className="absolute left-0 top-0 w-screen h-screen scale-110"
|
||||
name={versionInfo.backgroundName}
|
||||
/>
|
||||
)}
|
||||
<GameBackground
|
||||
className="absolute left-0 top-0 w-screen h-screen scale-110"
|
||||
name={versionInfo.backgroundName}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
</MouseParallax>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ function VersionDropdown({ version, changes, active, current }: VersionDropdownP
|
|||
{changes.length > 0
|
||||
? (
|
||||
<ul className="list-disc pl-6">
|
||||
{changes.map((change, index) => (
|
||||
<li className="px-1 text-fg-secondary" key={index}>
|
||||
{changes.map(change => (
|
||||
<li className="px-1 text-fg-secondary" key={change}>
|
||||
{change}
|
||||
</li>
|
||||
))}
|
||||
|
|
@ -116,7 +116,7 @@ function RouteComponent() {
|
|||
active={groupIndex === 0 ? true : undefined}
|
||||
changes={group.changes}
|
||||
current={group.version === version ? true : undefined}
|
||||
key={groupIndex}
|
||||
key={group.version}
|
||||
version={group.version}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ function RouteComponent() {
|
|||
);
|
||||
}
|
||||
|
||||
function VersionCard({ cluster, versionData, version, fullVersionName, setSelectedClusters }: { cluster: OnlineCluster; versionData: VersionInfo; version: OnlineClusterEntry; fullVersionName: string; setSelectedClusters: React.Dispatch<React.SetStateAction<Array<StrippedCluster>>> }) {
|
||||
function VersionCard({ cluster, versionData: _versionData, version, fullVersionName, setSelectedClusters }: { cluster: OnlineCluster; versionData: VersionInfo; version: OnlineClusterEntry; fullVersionName: string; setSelectedClusters: React.Dispatch<React.SetStateAction<Array<StrippedCluster>>> }) {
|
||||
const [isSelected, setSelected] = useState<boolean>(false);
|
||||
const toggle = () => {
|
||||
setSelected(prev => !prev);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { Button } from '@onelauncher/common/components';
|
|||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { DotsVerticalIcon } from '@untitled-theme/icons-react';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import { useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
import { Button as AriaButton } from 'react-aria-components';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
|
|
@ -84,18 +84,15 @@ function RouteComponent() {
|
|||
useImperativeHandle(wrappedRef, () => ({
|
||||
async openDownloadDialog(nextPath?: string) {
|
||||
// Extract overrides from all enabled bundles before downloading mods
|
||||
for (const [clusterId, bundles] of Object.entries(bundlesPerCluster)) {
|
||||
for (const bundle of bundles) {
|
||||
if (bundle.manifest.enabled) {
|
||||
for (const [clusterId, bundles] of Object.entries(bundlesPerCluster))
|
||||
for (const bundle of bundles)
|
||||
if (bundle.manifest.enabled)
|
||||
try {
|
||||
await bindings.oneclient.extractBundleOverrides(bundle.path, Number(clusterId));
|
||||
}
|
||||
catch (e) {
|
||||
console.error(`Failed to extract overrides for bundle ${bundle.manifest.name}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then delegate to the actual mod download dialog
|
||||
downloadModsRef.current?.openDownloadDialog(nextPath);
|
||||
|
|
@ -178,7 +175,7 @@ function ModCategoryCard({ art, fullVersionName, bundle, mods, setMods, clusterI
|
|||
});
|
||||
};
|
||||
|
||||
const onClickOnMod: onClickOnMod = (file) => {
|
||||
const onClickOnMod: onClickOnMod = useCallback((file: ModpackFile) => {
|
||||
setMods((prevMods) => {
|
||||
const existingIndex = prevMods.findIndex(m => m.file === file && m.bundleName === bundleName);
|
||||
const nextMods = existingIndex >= 0
|
||||
|
|
@ -200,7 +197,7 @@ function ModCategoryCard({ art, fullVersionName, bundle, mods, setMods, clusterI
|
|||
|
||||
return [...hiddenToAdd, ...nextMods];
|
||||
});
|
||||
};
|
||||
}, [bundleName, hiddenEnabledFiles, setMods]);
|
||||
|
||||
const modsForContext = mods.filter(m => !m.file.hidden).map(m => m.file);
|
||||
|
||||
|
|
@ -208,7 +205,7 @@ function ModCategoryCard({ art, fullVersionName, bundle, mods, setMods, clusterI
|
|||
onClickOnMod,
|
||||
useVerticalGridLayout: true,
|
||||
mods: modsForContext,
|
||||
}), [modsForContext]);
|
||||
}), [modsForContext, onClickOnMod]);
|
||||
|
||||
return (
|
||||
<AriaButton className={twMerge('group cursor-pointer w-full rounded-xl transition-[outline] outline-2 hover:outline-brand', isSelected ? 'outline-brand' : 'outline-ghost-overlay')} onPress={handleDownload}>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ async fn initialize_core() -> LauncherResult<()> {
|
|||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
#[allow(clippy::large_stack_frames)]
|
||||
async fn initialize_tauri(builder: tauri::Builder<tauri::Wry>) -> LauncherResult<tauri::App> {
|
||||
let router = taurpc::Router::new()
|
||||
.export_config(
|
||||
|
|
@ -125,11 +126,11 @@ fn setup_window(handle: &tauri::AppHandle) -> Result<(), Box<dyn std::error::Err
|
|||
.ok_or_else(|| anyhow::anyhow!("no window called main was found"))?;
|
||||
|
||||
// Initialize window decorations based on settings
|
||||
let win_clone = win.clone();
|
||||
let win_clone = win;
|
||||
let app_handle = handle.clone();
|
||||
tokio::task::spawn(async move {
|
||||
if let Ok(state) = State::get().await {
|
||||
let settings = state.settings.read().await;
|
||||
let _settings = state.settings.read().await;
|
||||
// native_window_frame=true means use native decorations
|
||||
// native_window_frame=false means use custom frame (no decorations)
|
||||
#[cfg(target_os = "macos")]
|
||||
|
|
@ -143,7 +144,7 @@ fn setup_window(handle: &tauri::AppHandle) -> Result<(), Box<dyn std::error::Err
|
|||
use objc2_app_kit::{NSWindow, NSWindowButton};
|
||||
|
||||
if let Ok(ns_window_ptr) = win_weak.ns_window() {
|
||||
let ns_window = ns_window_ptr as *mut NSWindow;
|
||||
let ns_window = ns_window_ptr.cast::<NSWindow>();
|
||||
unsafe {
|
||||
let ns_window = &*ns_window;
|
||||
if let Some(btn) =
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ function AccountPopup() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{users.data?.filter(user => user.id !== defaultUser.data?.id).map(user => (
|
||||
{users.data.filter(user => user.id !== defaultUser.data?.id).map(user => (
|
||||
<div key={user.id}>
|
||||
<AccountEntry
|
||||
onClick={() => setDefaultUser(user)}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export function useRecentCluster() {
|
|||
const [cluster, setCluster] = useState<ClusterModel | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!result.data)
|
||||
if (!result.isSuccess)
|
||||
return;
|
||||
|
||||
let mostRecentCluster: ClusterModel | undefined;
|
||||
|
|
@ -39,7 +39,7 @@ export function useRecentCluster() {
|
|||
}
|
||||
|
||||
setCluster(mostRecentCluster);
|
||||
}, [result.data]);
|
||||
}, [result.data, result.isSuccess]);
|
||||
|
||||
return cluster;
|
||||
}
|
||||
|
|
@ -49,10 +49,10 @@ export function useClusters() {
|
|||
const [clusters, setClusters] = useState<Array<ClusterModel> | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!result.data)
|
||||
if (!result.isSuccess)
|
||||
return;
|
||||
setClusters(result.data);
|
||||
}, [result.data]);
|
||||
}, [result.data, result.isSuccess]);
|
||||
|
||||
return clusters;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,13 +52,9 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
|
|||
}, []);
|
||||
|
||||
const saveChangedSettings = useCallback(() => {
|
||||
if (!settings)
|
||||
return;
|
||||
|
||||
writeQuery.refetch();
|
||||
|
||||
setSettingsChanged(false);
|
||||
}, [settings, writeQuery]);
|
||||
}, [writeQuery]);
|
||||
|
||||
const handleSetSettingsToSave = useCallback((newSettings: Partial<Settings> | null) => {
|
||||
setSettingsToSave(newSettings);
|
||||
|
|
@ -72,9 +68,9 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
|
|||
const [value, setValue] = useState<V>(initialValue);
|
||||
|
||||
useEffect(() => {
|
||||
const didChange = settings?.[name] !== value;
|
||||
const didChange = settings[name] !== value;
|
||||
|
||||
if (didChange && settings) {
|
||||
if (didChange) {
|
||||
setSettingsToSave(prev => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
|
|
@ -96,9 +92,6 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
|
|||
createSetting,
|
||||
}), [createSetting, handleSetSettingsToSave, saveChangedSettings, settings, settingsChanged, settingsToSave]);
|
||||
|
||||
if (settings === undefined)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider value={controller}>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ function Featured() {
|
|||
const [provider, slug] = useMemo<[Provider, string]>(() => ['Modrinth', 'iris'], []);
|
||||
const featuredPackage = usePackageData(provider, slug, {});
|
||||
const navigate = useNavigate();
|
||||
const featuredImageIndex = useMemo(() => featuredPackage.data?.gallery.findIndex(image => image.featured), [featuredPackage.data]);
|
||||
const [selectedImage, setSelectedImage] = useState(featuredImageIndex ?? 0);
|
||||
const featuredImageIndex = useMemo(() => featuredPackage.data.gallery.findIndex(image => image.featured), [featuredPackage.data]);
|
||||
const [selectedImage, setSelectedImage] = useState(featuredImageIndex === -1 ? 0 : featuredImageIndex);
|
||||
|
||||
return (
|
||||
<Show when={featuredPackage.isSuccess}>
|
||||
|
|
@ -39,10 +39,10 @@ function Featured() {
|
|||
<h5 className="ml-2 uppercase opacity-60">Featured</h5>
|
||||
<div className="w-full flex flex-col lg:flex-row overflow-hidden rounded-lg bg-page-elevated">
|
||||
<div className="w-full p-1">
|
||||
<img alt="thumbnail" className="aspect-video h-full w-full rounded-md object-cover object-center" src={featuredPackage.data?.gallery[selectedImage].url} />
|
||||
<img alt="thumbnail" className="aspect-video h-full w-full rounded-md object-cover object-center" src={featuredPackage.data.gallery[selectedImage].url} />
|
||||
</div>
|
||||
<div className="lg:max-w-64 min-[1900px]:max-w-96 min-w-52 flex flex-col gap-y-1 p-4">
|
||||
<h2 className="text-[1.5rem] font-semibold">{featuredPackage.data?.name}</h2>
|
||||
<h2 className="text-[1.5rem] font-semibold">{featuredPackage.data.name}</h2>
|
||||
|
||||
<Show when={false}>
|
||||
<div className="w-fit flex flex-row items-center gap-x-1 rounded-lg bg-border/10 px-1.5 py-1 text-fg-primary transition hover:opacity-80">
|
||||
|
|
@ -51,10 +51,10 @@ function Featured() {
|
|||
</div>
|
||||
</Show>
|
||||
|
||||
<p className="my-1 flex-1 leading-normal">{featuredPackage.data?.short_desc}</p>
|
||||
<p className="my-1 flex-1 leading-normal">{featuredPackage.data.short_desc}</p>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2 h-full">
|
||||
{featuredPackage.data?.gallery.map(
|
||||
{featuredPackage.data.gallery.map(
|
||||
(image, index) => (
|
||||
<img
|
||||
className="aspect-video object-center object-cover rounded-md"
|
||||
|
|
@ -67,7 +67,7 @@ function Featured() {
|
|||
</div>
|
||||
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button color="ghost" onClick={() => navigate({ to: '/app/browser/package/$provider/$slug', params: { provider, slug } })}>
|
||||
<Button color="ghost" onClick={() => navigate({ to: '/app/browser/package/$provider/$id', params: { provider, id: slug } })}>
|
||||
View
|
||||
<ChevronRightIcon />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ function RouteComponent() {
|
|||
if (!includes(PROVIDERS, provider))
|
||||
throw new Error('Invalid provider');
|
||||
const packageData = usePackageData(provider, id, {});
|
||||
const { data: packageBody } = useCommand('getPackageBody', () => bindings.core.getPackageBody(provider, packageData.data?.body ?? { Raw: 'No Package Data' }));
|
||||
const { data: packageBody } = useCommand('getPackageBody', () => bindings.core.getPackageBody(provider, packageData.data.body));
|
||||
const packageContextValue = useMemo(() => ({
|
||||
pkg: packageData.data,
|
||||
}), [packageData.data]);
|
||||
|
|
@ -66,7 +66,7 @@ function RouteComponent() {
|
|||
<TabList className="flex flex-none flex-row gap-x-1 rounded-lg bg-component-bg p-1 w-min">
|
||||
<Tab className="uppercase p-2.5 text-trim rounded-md selected:bg-component-bg-pressed disabled:hidden" id="about">About</Tab>
|
||||
<Tab className="uppercase p-2.5 text-trim rounded-md selected:bg-component-bg-pressed disabled:hidden" id="versions">Versions</Tab>
|
||||
<Tab className="uppercase p-2.5 text-trim rounded-md selected:bg-component-bg-pressed disabled:hidden" id="gallery" isDisabled={packageData.data?.gallery.length === 0}>Gallery</Tab>
|
||||
<Tab className="uppercase p-2.5 text-trim rounded-md selected:bg-component-bg-pressed disabled:hidden" id="gallery" isDisabled={packageData.data.gallery.length === 0}>Gallery</Tab>
|
||||
</TabList>
|
||||
<TabPanel className="prose prose-invert prose-sm max-w-none prose-code:before:content-none prose-code:after:content-none prose-code:bg-component-bg-disabled prose-code:rounded-sm prose-code:p-1! prose-code:text-trim prose-img:inline-block prose-a:inline-block" id="about">
|
||||
<div className="h-full min-h-full flex-1 w-full rounded-lg bg-component-bg p-3">
|
||||
|
|
@ -86,7 +86,7 @@ function RouteComponent() {
|
|||
</TabPanel>
|
||||
<TabPanel className="flex flex-wrap gap-2 justify-center" id="gallery">
|
||||
|
||||
{packageData.data?.gallery.map(item => (
|
||||
{packageData.data.gallery.map(item => (
|
||||
<Modal.Trigger key={item.url}>
|
||||
<Pressable>
|
||||
<div aria-label="Loaders" className="rounded-md overflow-hidden bg-component-bg-pressed outline-0 h-64 flex flex-col relative">
|
||||
|
|
@ -255,7 +255,7 @@ function InstallButton() {
|
|||
limit: 1,
|
||||
});
|
||||
const version = useMemo(() => {
|
||||
if (!versions || !browserContext.cluster)
|
||||
if (!browserContext.cluster)
|
||||
return undefined;
|
||||
return versions.items[0];
|
||||
}, [browserContext.cluster, versions]);
|
||||
|
|
@ -294,7 +294,7 @@ function InstallButton() {
|
|||
<span className="text-md font-semibold">{browserContext.cluster.name}</span>
|
||||
</span>
|
||||
)
|
||||
: versionsLoading ? 'Looking for a version...' : 'No matching version found'
|
||||
: 'No matching version found'
|
||||
: (clusters?.length ?? 0) > 0
|
||||
? 'Select a Cluster'
|
||||
: 'No clusters'}
|
||||
|
|
@ -413,8 +413,8 @@ function Versions() {
|
|||
});
|
||||
|
||||
const pagination = usePagination({
|
||||
itemsCount: versions?.total as unknown as number,
|
||||
itemsPerPage: versions?.limit as unknown as number,
|
||||
itemsCount: versions.total as unknown as number,
|
||||
itemsPerPage: versions.limit as unknown as number,
|
||||
});
|
||||
const paginationRef = useRef(pagination);
|
||||
useEffect(() => {
|
||||
|
|
@ -422,7 +422,7 @@ function Versions() {
|
|||
}, [pagination.offset]);
|
||||
useEffect(() => {
|
||||
paginationRef.current.reset();
|
||||
}, [versions?.total]);
|
||||
}, [versions.total]);
|
||||
return (
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex justify-between">
|
||||
|
|
@ -439,7 +439,7 @@ function Versions() {
|
|||
<Column className="pr-4" />
|
||||
</TableHeader>
|
||||
<TableBody className="">
|
||||
{versions?.items.map(item =>
|
||||
{versions.items.map(item =>
|
||||
<VersionRow key={item.version_id} version={item} />)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
|
|
|||
|
|
@ -65,8 +65,6 @@ const defaultFilters: Filters = {
|
|||
package_type: null,
|
||||
};
|
||||
|
||||
type CategoryFilter = Partial<NonReadonly<typeof browserCategories>>;
|
||||
|
||||
function BrowserCategories() {
|
||||
const context = useBrowserContext();
|
||||
const categoryName = useMemo(() => categoryNameFromId[context.query.filters?.package_type ?? 'mod'], [context.query.filters]);
|
||||
|
|
|
|||
|
|
@ -32,11 +32,7 @@ function Search() {
|
|||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
search.data
|
||||
? <Results results={search.data} />
|
||||
: <h1>Loading</h1>
|
||||
}
|
||||
<Results results={search.data} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ function RouteComponent() {
|
|||
<Show
|
||||
fallback={<div className="text-border/400">No screenshots found. Press F2 in game to take a screenshot!</div>}
|
||||
// @ts-ignore idk
|
||||
when={result.data?.length > 0}
|
||||
when={result.data.length > 0}
|
||||
>
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(350px,1fr))] w-full transform-gpu gap-2">
|
||||
{result.data?.map(data => (
|
||||
{result.data.map(data => (
|
||||
<ScreenshotEntry cluster_path={cluster.data?.folder_name || ''} key={data} path={data} />
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -27,18 +27,15 @@ function RouteComponent() {
|
|||
save.refetch();
|
||||
});
|
||||
|
||||
if (_result.isPending)
|
||||
return <p>loading...</p>;
|
||||
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<ScrollableContainer>
|
||||
<div className="h-full">
|
||||
<h1>Minecraft Settings</h1>
|
||||
|
||||
<GameSettings key={_result.data?.name} settings={_result.data} />
|
||||
<GameSettings key={_result.data.name} settings={_result.data} />
|
||||
|
||||
<ProcessSettings key={_result.data?.name} settings={_result.data} />
|
||||
<ProcessSettings key={_result.data.name} settings={_result.data} />
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
</Sidebar.Page>
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ function RouteComponent() {
|
|||
<div className="h-full">
|
||||
<Show
|
||||
fallback={<div className="text-border/400">No worlds found.</div>}
|
||||
when={list.data !== undefined && list.data.length > 0}
|
||||
when={list.data.length > 0}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{list.data?.map(data => (
|
||||
{list.data.map(data => (
|
||||
<WorldEntry cluster_path={cluster.data?.folder_name || ''} key={data} name={data} />
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -171,9 +171,6 @@ function ClusterCard({
|
|||
|
||||
const handleLaunch = () => {
|
||||
launch.mutate();
|
||||
|
||||
if (launch.error)
|
||||
console.error(launch.error.message);
|
||||
};
|
||||
|
||||
const image = () => {
|
||||
|
|
|
|||
|
|
@ -18,29 +18,21 @@ interface Account {
|
|||
}
|
||||
|
||||
function RouteComponent() {
|
||||
const { data: usersData, isLoading: usersLoading } = useCommand('getUsers', bindings.core.getUsers);
|
||||
const { data: defaultUserData, isLoading: defaultUserLoading } = useCommand('getDefaultUser', () => bindings.core.getDefaultUser(false));
|
||||
const { data: usersData } = useCommand('getUsers', bindings.core.getUsers);
|
||||
const { data: defaultUserData } = useCommand('getDefaultUser', () => bindings.core.getDefaultUser(false));
|
||||
|
||||
const addAccount = useCommand('openMsaLogin', bindings.core.openMsaLogin, {
|
||||
enabled: false,
|
||||
subscribed: false,
|
||||
});
|
||||
|
||||
if (usersLoading || defaultUserLoading)
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<h1>Accounts</h1>
|
||||
<p>Loading accounts...</p>
|
||||
</Sidebar.Page>
|
||||
);
|
||||
|
||||
return (
|
||||
<Sidebar.Page>
|
||||
<h1>Accounts</h1>
|
||||
<ScrollableContainer>
|
||||
<div className="h-full space-y-2">
|
||||
<Show fallback={<p>No accounts found.</p>} when={usersData && usersData.length > 0}>
|
||||
{usersData?.map(account => (
|
||||
<Show fallback={<p>No accounts found.</p>} when={usersData.length > 0}>
|
||||
{usersData.map(account => (
|
||||
<AccountRow
|
||||
account={account}
|
||||
isCurrent={account.id === defaultUserData?.id}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ gui = []
|
|||
workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = { workspace = true, features = ["default"] }
|
||||
shadow-rs = { workspace = true, features = [ "default" ] }
|
||||
|
||||
[dependencies]
|
||||
# core
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
use onelauncher_core::{api::{credentials, proxy::ProxyDynamic}, error::LauncherResult, initialize_core, store::CoreOptions, utils::io::IOError};
|
||||
use onelauncher_core::api::credentials;
|
||||
use onelauncher_core::api::proxy::ProxyDynamic;
|
||||
use onelauncher_core::error::LauncherResult;
|
||||
use onelauncher_core::initialize_core;
|
||||
use onelauncher_core::store::CoreOptions;
|
||||
use onelauncher_core::utils::io::IOError;
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
|
||||
#[tokio::main]
|
||||
|
|
@ -22,4 +27,4 @@ pub async fn main() -> LauncherResult<()> {
|
|||
println!("Credentials: {creds:#?}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ use onelauncher_core::store::{Core, CoreOptions, Dirs};
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
Core::initialize(CoreOptions::default()).await.expect("couldn't initialize core");
|
||||
Core::initialize(CoreOptions::default())
|
||||
.await
|
||||
.expect("couldn't initialize core");
|
||||
let dirs = Dirs::get().await.expect("couldn't initialize dirs");
|
||||
|
||||
println!(" Base dir : {}", dirs.base_dir().display());
|
||||
println!("Launcher logs dir : {}", dirs.launcher_logs_dir().display());
|
||||
println!(" Metadata dir : {}", dirs.metadata_dir().display());
|
||||
println!(" DB file : {}", dirs.db_file().display());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
use discord_rich_presence::activity::Timestamps;
|
||||
use onelauncher_core::{api::proxy::ProxyEmpty, error::LauncherResult, initialize_core, store::{CoreOptions, State}};
|
||||
use onelauncher_core::api::proxy::ProxyEmpty;
|
||||
use onelauncher_core::error::LauncherResult;
|
||||
use onelauncher_core::initialize_core;
|
||||
use onelauncher_core::store::{CoreOptions, State};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> LauncherResult<()> {
|
||||
initialize_core(CoreOptions {
|
||||
discord_client_id: Some(String::from("1274084193193955408")),
|
||||
..Default::default()
|
||||
}, ProxyEmpty::new()).await?;
|
||||
initialize_core(
|
||||
CoreOptions {
|
||||
discord_client_id: Some(String::from("1274084193193955408")),
|
||||
..Default::default()
|
||||
},
|
||||
ProxyEmpty::new(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let state = State::get().await?;
|
||||
let Some(rpc) = &state.rpc else {
|
||||
let rpc_lock = state.rpc.read().await;
|
||||
let Some(rpc) = &*rpc_lock else {
|
||||
println!("RPC is not initialized");
|
||||
return Ok(());
|
||||
};
|
||||
|
|
@ -18,11 +26,13 @@ async fn main() -> LauncherResult<()> {
|
|||
println!("Set Discord message");
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
rpc.set_message("New timestamp.", Some(Timestamps::new())).await;
|
||||
rpc.set_message("New timestamp.", Some(Timestamps::new()))
|
||||
.await;
|
||||
println!("Set Discord message");
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
rpc.set_message("Reverted back to the old timestamp!", None).await;
|
||||
rpc.set_message("Reverted back to the old timestamp!", None)
|
||||
.await;
|
||||
println!("Set Discord message");
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
|
|
@ -30,4 +40,4 @@ async fn main() -> LauncherResult<()> {
|
|||
println!("Cleared Discord message");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ async fn main() -> LauncherResult<()> {
|
|||
|
||||
let path = java::install_java_package(pkg).await?;
|
||||
|
||||
println!("java package installed at '{path:?}'");
|
||||
println!("java package installed at '{}'", path.display());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,10 @@ pub async fn main() -> LauncherResult<()> {
|
|||
|
||||
// We create a settings profile so that we can run hooks
|
||||
let profile_name = "profile with hooks example";
|
||||
if setting_profiles::dao::get_profile_by_name(profile_name).await?.is_none() {
|
||||
if setting_profiles::dao::get_profile_by_name(profile_name)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
setting_profiles::create_profile(profile_name, async |mut profile| {
|
||||
profile.hook_pre = Set(Some("echo this is a pre-hook!".to_string()));
|
||||
profile.hook_post = Set(Some("echo this is my post-hook!".to_string()));
|
||||
|
|
@ -46,14 +49,17 @@ pub async fn main() -> LauncherResult<()> {
|
|||
}
|
||||
|
||||
Ok(profile)
|
||||
}).await?;
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Here we create the cluster
|
||||
let cluster_name = "Launchable";
|
||||
let path = dirs.clusters_dir().join(cluster_name);
|
||||
|
||||
let mut cluster = if let Some(cluster) = api::cluster::dao::get_cluster_by_folder_name(&path).await? {
|
||||
let mut cluster = if let Some(cluster) =
|
||||
api::cluster::dao::get_cluster_by_folder_name(&path).await?
|
||||
{
|
||||
cluster
|
||||
} else {
|
||||
api::cluster::create_cluster(cluster_name, "1.20.4", GameLoader::Fabric, None, None).await?
|
||||
|
|
@ -64,12 +70,16 @@ pub async fn main() -> LauncherResult<()> {
|
|||
cluster.setting_profile_name = Set(Some(profile_name.to_string()));
|
||||
|
||||
Ok(cluster)
|
||||
}).await?;
|
||||
})
|
||||
.await?;
|
||||
|
||||
println!("Using cluster: {} at '{}'", cluster.name, cluster.folder_name);
|
||||
println!(
|
||||
"Using cluster: {} at '{}'",
|
||||
cluster.name, cluster.folder_name
|
||||
);
|
||||
|
||||
// Launch the game
|
||||
let process = api::game::launch::launch_minecraft(&mut cluster, creds, None).await?;
|
||||
let process = api::game::launch::launch_minecraft(&mut cluster, creds, None, None).await?;
|
||||
println!("Process: {:?}", process.read().await);
|
||||
|
||||
// We wait for the process to finish so that we can run code after
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
use onelauncher_core::{api::{java, proxy::ProxyEmpty}, error::LauncherResult, initialize_core, store::CoreOptions};
|
||||
use onelauncher_core::api::java;
|
||||
use onelauncher_core::api::proxy::ProxyEmpty;
|
||||
use onelauncher_core::error::LauncherResult;
|
||||
use onelauncher_core::initialize_core;
|
||||
use onelauncher_core::store::CoreOptions;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> LauncherResult<()> {
|
||||
|
|
@ -12,4 +16,4 @@ async fn main() -> LauncherResult<()> {
|
|||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
use onelauncher_core::{api::proxy::ProxyDynamic, error::LauncherResult, initialize_core, store::{CoreOptions, State}};
|
||||
use onelauncher_core::api::proxy::ProxyDynamic;
|
||||
use onelauncher_core::error::LauncherResult;
|
||||
use onelauncher_core::initialize_core;
|
||||
use onelauncher_core::store::{CoreOptions, State};
|
||||
use onelauncher_entity::loader::GameLoader;
|
||||
|
||||
#[tokio::main]
|
||||
|
|
@ -9,12 +12,39 @@ async fn main() -> LauncherResult<()> {
|
|||
let metadata = &mut state.metadata.write().await;
|
||||
|
||||
println!("Initialized: {}", metadata.initialized());
|
||||
println!("Minecraft: {:?}", metadata.get_vanilla_or_fetch().await.is_ok());
|
||||
println!("Forge: {:?}", metadata.get_modded_or_fetch(&GameLoader::Forge).await.is_ok());
|
||||
println!(
|
||||
"Minecraft: {:?}",
|
||||
metadata.get_vanilla_or_fetch().await.is_ok()
|
||||
);
|
||||
println!(
|
||||
"Forge: {:?}",
|
||||
metadata
|
||||
.get_modded_or_fetch(&GameLoader::Forge)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
println!("Initialized: {}", metadata.initialized());
|
||||
println!("NeoForge: {:?}", metadata.get_modded_or_fetch(&GameLoader::NeoForge).await.is_ok());
|
||||
println!("Fabric: {:?}", metadata.get_modded_or_fetch(&GameLoader::Fabric).await.is_ok());
|
||||
println!("Quilt: {:?}", metadata.get_modded_or_fetch(&GameLoader::Quilt).await.is_ok());
|
||||
println!(
|
||||
"NeoForge: {:?}",
|
||||
metadata
|
||||
.get_modded_or_fetch(&GameLoader::NeoForge)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
println!(
|
||||
"Fabric: {:?}",
|
||||
metadata
|
||||
.get_modded_or_fetch(&GameLoader::Fabric)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
println!(
|
||||
"Quilt: {:?}",
|
||||
metadata
|
||||
.get_modded_or_fetch(&GameLoader::Quilt)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,7 +105,10 @@ pub async fn get_log_by_name(
|
|||
return Ok(None);
|
||||
}
|
||||
|
||||
let content = if file_name.ends_with(".gz") {
|
||||
let content = if std::path::Path::new(file_name)
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("gz"))
|
||||
{
|
||||
io::read_gz_to_string(&path).await?
|
||||
} else {
|
||||
io::read_to_string(&path).await?
|
||||
|
|
@ -136,6 +139,7 @@ pub async fn get_process_log_tail(
|
|||
Ok(Some(tail))
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation, clippy::naive_bytecount)]
|
||||
async fn read_process_log_tail(
|
||||
path: &Path,
|
||||
max_lines: usize,
|
||||
|
|
@ -174,12 +178,10 @@ async fn read_process_log_tail(
|
|||
file.read_exact(&mut chunk).await?;
|
||||
newline_count += chunk.iter().filter(|byte| **byte == b'\n').count();
|
||||
|
||||
if buffer.is_empty() {
|
||||
buffer = chunk;
|
||||
} else {
|
||||
if !buffer.is_empty() {
|
||||
chunk.extend_from_slice(&buffer);
|
||||
buffer = chunk;
|
||||
}
|
||||
buffer = chunk;
|
||||
}
|
||||
|
||||
let raw = String::from_utf8_lossy(&buffer);
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ async fn install_cluster(
|
|||
|
||||
let version = versions_list[version_index].clone();
|
||||
|
||||
let minecraft_updated = metadata::is_version_updated(version_index, &versions_list);
|
||||
let minecraft_updated = metadata::is_version_updated(version_index, versions_list);
|
||||
|
||||
drop(metadata);
|
||||
|
||||
|
|
|
|||
|
|
@ -118,8 +118,7 @@ pub async fn sync_cluster(cluster: &clusters::Model) -> LauncherResult<SyncActio
|
|||
// Skip hidden files/folders
|
||||
if file_path
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().starts_with('.'))
|
||||
.unwrap_or(false)
|
||||
.is_some_and(|s| s.to_string_lossy().starts_with('.'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,9 +68,10 @@ pub async fn launch_minecraft(
|
|||
)
|
||||
.await?;
|
||||
|
||||
let version_name = loader_version
|
||||
.as_ref()
|
||||
.map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id));
|
||||
let version_name = loader_version.as_ref().map_or_else(
|
||||
|| version.id.clone(),
|
||||
|it| format!("{}-{}", version.id, it.id),
|
||||
);
|
||||
|
||||
let version_info =
|
||||
metadata::download_version_info(&version, loader_version.as_ref(), None, None).await?;
|
||||
|
|
|
|||
|
|
@ -69,7 +69,10 @@ pub async fn download_version_info(
|
|||
const TASKS: f64 = 3.33;
|
||||
|
||||
tracing::debug!("loading version info for version {}", version.id);
|
||||
let version_id = loader.map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id));
|
||||
let version_id = loader.map_or_else(
|
||||
|| version.id.clone(),
|
||||
|it| format!("{}-{}", version.id, it.id),
|
||||
);
|
||||
|
||||
ingress.set_ingress_message("fetching version info").await?;
|
||||
|
||||
|
|
@ -461,6 +464,7 @@ pub async fn get_loaders_for_version(mc_version: &str) -> LauncherResult<Vec<Gam
|
|||
metadata.get_loaders_for_version(mc_version).await
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_version_updated(version_index: usize, versions: &[Version]) -> bool {
|
||||
version_index <= versions.iter().position(|x| x.id == "22w16a").unwrap_or(0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ pub async fn prepare_java(
|
|||
) -> LauncherResult<java_versions::Model> {
|
||||
let id = init_ingress(
|
||||
IngressType::JavaPrepare,
|
||||
&format!("preparing java {}", major),
|
||||
&format!("preparing java {major}"),
|
||||
100.0,
|
||||
)
|
||||
.await?;
|
||||
|
|
@ -453,7 +453,7 @@ pub async fn install_java_package(package: &JavaPackage) -> LauncherResult<PathB
|
|||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let java_version = package.java_version.first().unwrap().to_string();
|
||||
base_path = base_path.join(format!("zulu-{java_version}.jre"))
|
||||
base_path = base_path.join(format!("zulu-{java_version}.jre"));
|
||||
}
|
||||
|
||||
base_path = base_path.join(get_java_bin());
|
||||
|
|
|
|||
|
|
@ -248,6 +248,7 @@ pub async fn remove_bundle_override(
|
|||
}
|
||||
|
||||
/// Returns whether a specific bundle package mapping still exists in the cluster.
|
||||
///
|
||||
/// Useful to avoid deleting overrides during update replacements where the old hash
|
||||
/// is removed after a new hash has already been linked and tracked.
|
||||
pub async fn has_bundle_package_mapping(
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ where
|
|||
fn as_mod_pack_out(category: &PackageModPackCategory) -> Out;
|
||||
fn to_mod_pack(value: &Out) -> Option<PackageModPackCategory>;
|
||||
|
||||
#[must_use]
|
||||
fn as_data_pack_out(category: &PackageModCategory) -> Out {
|
||||
Self::as_mod_out(category)
|
||||
}
|
||||
|
|
@ -39,46 +40,44 @@ where
|
|||
|
||||
fn as_out(categories: &PackageCategories) -> Vec<Out> {
|
||||
match categories {
|
||||
PackageCategories::Mod(categories) => {
|
||||
categories.into_iter().map(Self::as_mod_out).collect()
|
||||
PackageCategories::Mod(categories) => categories.iter().map(Self::as_mod_out).collect(),
|
||||
PackageCategories::ResourcePack(categories) => {
|
||||
categories.iter().map(Self::as_resource_pack_out).collect()
|
||||
}
|
||||
PackageCategories::ResourcePack(categories) => categories
|
||||
.into_iter()
|
||||
.map(Self::as_resource_pack_out)
|
||||
.collect(),
|
||||
PackageCategories::Shader(categories) => {
|
||||
categories.into_iter().map(Self::as_shader_out).collect()
|
||||
categories.iter().map(Self::as_shader_out).collect()
|
||||
}
|
||||
PackageCategories::DataPack(categories) => {
|
||||
categories.into_iter().map(Self::as_data_pack_out).collect()
|
||||
categories.iter().map(Self::as_data_pack_out).collect()
|
||||
}
|
||||
PackageCategories::ModPack(categories) => {
|
||||
categories.into_iter().map(Self::as_mod_pack_out).collect()
|
||||
categories.iter().map(Self::as_mod_pack_out).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_list(package_type: &PackageType, values: &Vec<Out>) -> PackageCategories {
|
||||
match package_type {
|
||||
&PackageType::Mod => {
|
||||
#[must_use]
|
||||
fn to_list(package_type: &PackageType, values: &[Out]) -> PackageCategories {
|
||||
match *package_type {
|
||||
PackageType::Mod => {
|
||||
PackageCategories::Mod(values.iter().filter_map(|v| Self::to_mod(v)).collect())
|
||||
}
|
||||
&PackageType::ResourcePack => PackageCategories::ResourcePack(
|
||||
PackageType::ResourcePack => PackageCategories::ResourcePack(
|
||||
values
|
||||
.iter()
|
||||
.filter_map(|v| Self::to_resource_pack(v))
|
||||
.collect(),
|
||||
),
|
||||
&PackageType::Shader => PackageCategories::Shader(
|
||||
PackageType::Shader => PackageCategories::Shader(
|
||||
values.iter().filter_map(|v| Self::to_shader(v)).collect(),
|
||||
),
|
||||
&PackageType::DataPack => PackageCategories::DataPack(
|
||||
PackageType::DataPack => PackageCategories::DataPack(
|
||||
values
|
||||
.iter()
|
||||
.filter_map(|v| Self::to_data_pack(v))
|
||||
.collect(),
|
||||
),
|
||||
&PackageType::ModPack => PackageCategories::ModPack(
|
||||
PackageType::ModPack => PackageCategories::ModPack(
|
||||
values.iter().filter_map(|v| Self::to_mod_pack(v)).collect(),
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ pub struct ManagedPackage {
|
|||
pub name: String,
|
||||
pub short_desc: String,
|
||||
pub body: ManagedPackageBody,
|
||||
/// Won't have all versions for some providers, like CurseForge.
|
||||
/// Won't have all versions for some providers, like `CurseForge`.
|
||||
/// Try making a request to get the versions if needed
|
||||
pub version_ids: Vec<String>,
|
||||
pub mc_versions: Vec<String>,
|
||||
|
|
@ -142,8 +142,8 @@ impl std::fmt::Display for ManagedPackageBody {
|
|||
f,
|
||||
"{}",
|
||||
match self {
|
||||
ManagedPackageBody::Url(_) => "Url",
|
||||
ManagedPackageBody::Raw(_) => "Raw",
|
||||
Self::Url(_) => "Url",
|
||||
Self::Raw(_) => "Raw",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -223,11 +223,10 @@ impl std::fmt::Display for PackageAuthor {
|
|||
"{}",
|
||||
match self {
|
||||
Self::Team { team_id, org_id } => {
|
||||
if let Some(org_id) = org_id {
|
||||
format!("Team({} - {})", team_id, org_id)
|
||||
} else {
|
||||
format!("Team({})", team_id)
|
||||
}
|
||||
org_id.as_ref().map_or_else(
|
||||
|| format!("Team({team_id})"),
|
||||
|id| format!("Team({team_id} - {id})"),
|
||||
)
|
||||
}
|
||||
Self::Users(users) => format!("Users({})", users.len()),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,24 +72,25 @@ pub async fn download_package(
|
|||
.find(|f| f.primary)
|
||||
.ok_or(PackageError::NoPrimaryFile)?;
|
||||
|
||||
let mut update_existing_package = false;
|
||||
if let Some(model) = dao::get_package_by_hash(primary_file.sha1.clone()).await? {
|
||||
if !force.unwrap_or(false) {
|
||||
let cached_path = model.path().await?;
|
||||
if cached_path.exists() {
|
||||
tracing::debug!("package is already downloaded");
|
||||
return Ok(model);
|
||||
let update_existing_package =
|
||||
if let Some(model) = dao::get_package_by_hash(primary_file.sha1.clone()).await? {
|
||||
if !force.unwrap_or(false) {
|
||||
let cached_path = model.path().await?;
|
||||
if cached_path.exists() {
|
||||
tracing::debug!("package is already downloaded");
|
||||
return Ok(model);
|
||||
}
|
||||
|
||||
tracing::warn!(
|
||||
"package '{}' was found in database but cached file is missing at '{}'; re-downloading",
|
||||
model.hash,
|
||||
cached_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
tracing::warn!(
|
||||
"package '{}' was found in database but cached file is missing at '{}'; re-downloading",
|
||||
model.hash,
|
||||
cached_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
update_existing_package = true;
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let mut model = packages::Model {
|
||||
hash: primary_file.sha1.clone(),
|
||||
|
|
@ -98,7 +99,7 @@ pub async fn download_package(
|
|||
file_name: primary_file.file_name.clone(),
|
||||
version_id: version.version_id.clone(),
|
||||
published_at: version.published,
|
||||
provider: package.provider.clone(),
|
||||
provider: package.provider,
|
||||
icon: None,
|
||||
package_id: package.id.clone(),
|
||||
mc_loader: version.loaders.clone().into(),
|
||||
|
|
@ -117,10 +118,10 @@ pub async fn download_package(
|
|||
)
|
||||
.await?;
|
||||
|
||||
if let Some(icon_url) = &package.icon_url {
|
||||
if let Some(icon) = Icon::try_from_url(&url::Url::parse(icon_url)?) {
|
||||
model.icon = Some(icon::cache_icon(&icon).await?);
|
||||
}
|
||||
if let Some(icon_url) = &package.icon_url
|
||||
&& let Some(icon) = Icon::try_from_url(&url::Url::parse(icon_url)?)
|
||||
{
|
||||
model.icon = Some(icon::cache_icon(&icon).await?);
|
||||
}
|
||||
|
||||
persist_downloaded_package(model, update_existing_package).await
|
||||
|
|
@ -141,8 +142,9 @@ pub async fn download_external_package(
|
|||
);
|
||||
|
||||
// check if already downloaded
|
||||
let mut update_existing_package = false;
|
||||
if let Some(model) = dao::get_package_by_hash(package.sha1.clone()).await? {
|
||||
let update_existing_package = if let Some(model) =
|
||||
dao::get_package_by_hash(package.sha1.clone()).await?
|
||||
{
|
||||
if !force.unwrap_or(false) {
|
||||
let cached_path = model.path().await?;
|
||||
if cached_path.exists() {
|
||||
|
|
@ -165,9 +167,10 @@ pub async fn download_external_package(
|
|||
cached_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
update_existing_package = true;
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let model = packages::Model {
|
||||
hash: package.sha1.clone(),
|
||||
|
|
@ -325,7 +328,7 @@ pub async fn link_many_packages_to_cluster(
|
|||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
for package in compatible_packages.iter() {
|
||||
for package in &compatible_packages {
|
||||
hard_link(package, cluster).await?;
|
||||
}
|
||||
|
||||
|
|
@ -362,8 +365,8 @@ impl DatabaseModelExt for packages::Model {
|
|||
async fn path(&self) -> LauncherResult<std::path::PathBuf> {
|
||||
Ok(Dirs::get_packages_dir()
|
||||
.await?
|
||||
.join(&self.package_type.folder_name())
|
||||
.join(&self.provider.name())
|
||||
.join(self.package_type.folder_name())
|
||||
.join(self.provider.name())
|
||||
.join(&self.package_id)
|
||||
.join(&self.version_id)
|
||||
.join(&self.file_name))
|
||||
|
|
@ -420,10 +423,10 @@ pub async fn remove_package(
|
|||
|
||||
if !dao::is_package_used(&package_hash).await? {
|
||||
let path = package.path().await?;
|
||||
if path.exists() {
|
||||
if let Err(err) = io::remove_file(&path).await {
|
||||
send_error!("{}{}", "failed to remove file: ", err);
|
||||
}
|
||||
if path.exists()
|
||||
&& let Err(err) = io::remove_file(&path).await
|
||||
{
|
||||
send_error!("{}{}", "failed to remove file: ", err);
|
||||
}
|
||||
dao::delete_package_by_id(package_hash).await?;
|
||||
}
|
||||
|
|
@ -479,7 +482,7 @@ pub async fn toggle_package(cluster_id: i64, package_hash: String) -> LauncherRe
|
|||
let (new_file_name, enabled) = if is_disabled {
|
||||
(file_name.trim_end_matches(".disabled").to_string(), true)
|
||||
} else {
|
||||
(format!("{}.disabled", file_name), false)
|
||||
(format!("{file_name}.disabled"), false)
|
||||
};
|
||||
|
||||
// Read bundle mapping data before any mutations so the override can be written
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ pub struct ModpackManifest {
|
|||
pub files: Vec<ModpackFile>,
|
||||
}
|
||||
|
||||
fn default_false() -> bool {
|
||||
const fn default_false() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
|
@ -47,6 +47,6 @@ pub struct ModpackFile {
|
|||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ModpackFileKind {
|
||||
Managed((ManagedPackage, ManagedVersion)),
|
||||
Managed(Box<(ManagedPackage, ManagedVersion)>),
|
||||
External(ExternalPackage),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,9 +85,11 @@ pub trait InstallableModpackFormatExt: Send + Sync + std::any::Any {
|
|||
}
|
||||
|
||||
impl ModpackFormat {
|
||||
pub async fn from_file(path: &PathBuf) -> LauncherResult<Box<dyn InstallableModpackFormatExt>> {
|
||||
pub async fn from_file(
|
||||
path: &std::path::Path,
|
||||
) -> LauncherResult<Box<dyn InstallableModpackFormatExt>> {
|
||||
for stage in FORMAT_PIPELINE_PATH {
|
||||
if let Some(format) = stage(path.clone()).await? {
|
||||
if let Some(format) = stage(path.to_path_buf()).await? {
|
||||
return Ok(format);
|
||||
}
|
||||
}
|
||||
|
|
@ -115,14 +117,14 @@ impl ModpackFormat {
|
|||
ingress: Option<SubIngress<'_>>,
|
||||
) -> LauncherResult<()> {
|
||||
match self {
|
||||
ModpackFormat::CurseForge => unimplemented!(),
|
||||
ModpackFormat::MrPack => MrPackFormatImpl::install_modpack_archive(
|
||||
Self::CurseForge => unimplemented!(),
|
||||
Self::MrPack => MrPackFormatImpl::install_modpack_archive(
|
||||
modpack_archive,
|
||||
cluster,
|
||||
skip_compatibility,
|
||||
ingress,
|
||||
),
|
||||
ModpackFormat::PolyMrPack => PolyMrPackFormatImpl::install_modpack_archive(
|
||||
Self::PolyMrPack => PolyMrPackFormatImpl::install_modpack_archive(
|
||||
modpack_archive,
|
||||
cluster,
|
||||
skip_compatibility,
|
||||
|
|
@ -136,7 +138,7 @@ impl ModpackFormat {
|
|||
/// Installs a modpack to a cluster.
|
||||
pub async fn install_managed_modpack(
|
||||
package_model: &packages::Model,
|
||||
modpack: &Box<dyn InstallableModpackFormatExt>,
|
||||
modpack: &dyn InstallableModpackFormatExt,
|
||||
cluster: &mut clusters::Model,
|
||||
skip_compatibility: Option<bool>,
|
||||
ingress: Option<SubIngress<'_>>,
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ impl ModpackFormatExt for MrPackFormatImpl {
|
|||
let mut loader: Option<GameLoader> = None;
|
||||
let mut loader_version: Option<String> = None;
|
||||
|
||||
for (key, value) in serialized.dependencies.iter() {
|
||||
for (key, value) in &serialized.dependencies {
|
||||
if key == "minecraft" {
|
||||
mc_version = Some(value.clone());
|
||||
} else {
|
||||
|
|
@ -125,8 +125,8 @@ impl ModpackFormatExt for MrPackFormatImpl {
|
|||
{
|
||||
let ModpackArchive { manifest, path, .. } = modpack_archive;
|
||||
|
||||
download_and_link_packages(cluster, &manifest, skip_compatibility, &ingress).await?;
|
||||
copy_overrides_folder(cluster, &path, &ingress).await?;
|
||||
download_and_link_packages(cluster, manifest, skip_compatibility, ingress.as_ref()).await?;
|
||||
copy_overrides_folder(cluster, path, &ingress).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -156,7 +156,7 @@ impl InstallableModpackFormatExt for MrPackFormatImpl {
|
|||
name: self.raw_manifest.name.clone(),
|
||||
version: self.raw_manifest.version_id.clone(),
|
||||
mc_version: self.mc_version.clone(),
|
||||
loader: self.loader.clone(),
|
||||
loader: self.loader,
|
||||
loader_version: self.loader_version.clone(),
|
||||
enabled: false,
|
||||
files,
|
||||
|
|
@ -176,7 +176,7 @@ impl InstallableModpackFormatExt for MrPackFormatImpl {
|
|||
) -> LauncherResult<()> {
|
||||
let manifest = self.manifest().await?;
|
||||
|
||||
download_and_link_packages(cluster, &manifest, skip_compatibility, &ingress).await?;
|
||||
download_and_link_packages(cluster, manifest, skip_compatibility, ingress.as_ref()).await?;
|
||||
|
||||
if let Some(path) = self.archive.as_ref() {
|
||||
copy_overrides_folder(cluster, path, &ingress).await?;
|
||||
|
|
@ -190,7 +190,7 @@ pub(super) async fn download_and_link_packages(
|
|||
cluster: &clusters::Model,
|
||||
manifest: &ModpackManifest,
|
||||
skip_compatibility: Option<bool>,
|
||||
_ingress: &Option<SubIngress<'_>>,
|
||||
_ingress: Option<&SubIngress<'_>>,
|
||||
) -> LauncherResult<()> {
|
||||
if cluster.mc_version != manifest.mc_version {
|
||||
return Err(ClusterError::MismatchedVersion(
|
||||
|
|
@ -201,30 +201,26 @@ pub(super) async fn download_and_link_packages(
|
|||
}
|
||||
|
||||
if cluster.mc_loader != manifest.loader {
|
||||
return Err(ClusterError::MismatchedLoader(
|
||||
manifest.loader.clone(),
|
||||
cluster.mc_loader.clone(),
|
||||
)
|
||||
.into());
|
||||
return Err(ClusterError::MismatchedLoader(manifest.loader, cluster.mc_loader).into());
|
||||
}
|
||||
|
||||
// TODO: Implement loader version checking
|
||||
// if cluster.mc_loader_version.is_some_and(|v| v )
|
||||
|
||||
let mut errors = Vec::new();
|
||||
let mut packages_to_link = Vec::new();
|
||||
|
||||
// TODO: Ingress
|
||||
for file in manifest.files.iter() {
|
||||
if file.enabled == false {
|
||||
for file in &manifest.files {
|
||||
if !file.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
match &file.kind {
|
||||
ModpackFileKind::Managed((package, version)) => {
|
||||
ModpackFileKind::Managed(managed) => {
|
||||
let (package, version) = &**managed;
|
||||
match api::packages::download_package(package, version, None, None).await {
|
||||
Ok(model) => packages_to_link.push(model),
|
||||
Err(e) => errors.push(e),
|
||||
Err(e) => tracing::error!("failed to download package: {e:?}"),
|
||||
}
|
||||
}
|
||||
ModpackFileKind::External(package) => {
|
||||
|
|
@ -239,7 +235,7 @@ pub(super) async fn download_and_link_packages(
|
|||
{
|
||||
Ok(Some(model)) => packages_to_link.push(model),
|
||||
Ok(None) => {}
|
||||
Err(e) => errors.push(e),
|
||||
Err(e) => tracing::error!("failed to download external package: {e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -360,6 +356,7 @@ pub async fn extract_overrides_no_overwrite(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn to_modpack_files(mrpack_files: &Vec<MrPackFile>) -> LauncherResult<Vec<ModpackFile>> {
|
||||
#[derive(Clone)]
|
||||
struct ToFetch {
|
||||
|
|
@ -375,7 +372,7 @@ async fn to_modpack_files(mrpack_files: &Vec<MrPackFile>) -> LauncherResult<Vec<
|
|||
let name = file
|
||||
.path
|
||||
.split('/')
|
||||
.last()
|
||||
.next_back()
|
||||
.unwrap_or(&file.path)
|
||||
.to_string();
|
||||
|
||||
|
|
@ -407,7 +404,7 @@ async fn to_modpack_files(mrpack_files: &Vec<MrPackFile>) -> LauncherResult<Vec<
|
|||
.first()
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
tracing::warn!("mrpack file '{}' does not contain a download URL", name)
|
||||
tracing::warn!("mrpack file '{}' does not contain a download URL", name);
|
||||
})
|
||||
.unwrap_or(String::new());
|
||||
|
||||
|
|
@ -417,8 +414,7 @@ async fn to_modpack_files(mrpack_files: &Vec<MrPackFile>) -> LauncherResult<Vec<
|
|||
.path
|
||||
.split('/')
|
||||
.next()
|
||||
.and_then(|s| PackageType::try_from(s).ok())
|
||||
.unwrap_or(PackageType::Mod);
|
||||
.map_or(PackageType::Mod, PackageType::from);
|
||||
|
||||
files.push(ModpackFile {
|
||||
kind: ModpackFileKind::External(ExternalPackage {
|
||||
|
|
@ -465,7 +461,7 @@ async fn to_modpack_files(mrpack_files: &Vec<MrPackFile>) -> LauncherResult<Vec<
|
|||
.and_then(|f| f.overrides.clone());
|
||||
|
||||
files.push(ModpackFile {
|
||||
kind: ModpackFileKind::Managed((fetched_pkg, version)),
|
||||
kind: ModpackFileKind::Managed(Box::new((fetched_pkg, version))),
|
||||
enabled: true,
|
||||
hidden: false,
|
||||
overrides,
|
||||
|
|
@ -526,7 +522,7 @@ mod tests {
|
|||
use super::extract_overrides_no_overwrite;
|
||||
|
||||
/// Helper: creates a .mrpack zip at `zip_path` with the given entries.
|
||||
/// Each entry is (filename_in_zip, content_bytes).
|
||||
/// Each entry is (`filename_in_zip`, `content_bytes`).
|
||||
async fn create_test_zip(zip_path: &std::path::Path, entries: &[(&str, &[u8])]) {
|
||||
let file = tokio::fs::File::create(zip_path).await.unwrap();
|
||||
let mut writer = ZipFileWriter::new(file.compat_write());
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ impl ModpackFormatExt for PolyMrPackFormatImpl {
|
|||
let mut loader: Option<GameLoader> = None;
|
||||
let mut loader_version: Option<String> = None;
|
||||
|
||||
for (key, value) in serialized.dependencies.iter() {
|
||||
for (key, value) in &serialized.dependencies {
|
||||
if key == "minecraft" {
|
||||
mc_version = Some(value.clone());
|
||||
} else {
|
||||
|
|
@ -120,9 +120,14 @@ impl ModpackFormatExt for PolyMrPackFormatImpl {
|
|||
{
|
||||
let ModpackArchive { manifest, path, .. } = modpack_archive;
|
||||
|
||||
super::mrpack::download_and_link_packages(cluster, &manifest, skip_compatibility, &ingress)
|
||||
.await?;
|
||||
super::mrpack::copy_overrides_folder(cluster, &path, &ingress).await?;
|
||||
super::mrpack::download_and_link_packages(
|
||||
cluster,
|
||||
manifest,
|
||||
skip_compatibility,
|
||||
ingress.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
super::mrpack::copy_overrides_folder(cluster, path, &ingress).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -154,7 +159,7 @@ impl InstallableModpackFormatExt for PolyMrPackFormatImpl {
|
|||
name: self.raw_manifest.name.clone(),
|
||||
version: self.raw_manifest.version_id.clone(),
|
||||
mc_version: self.mc_version.clone(),
|
||||
loader: self.loader.clone(),
|
||||
loader: self.loader,
|
||||
loader_version: self.loader_version.clone(),
|
||||
enabled: self.raw_manifest.enabled,
|
||||
files,
|
||||
|
|
@ -174,8 +179,13 @@ impl InstallableModpackFormatExt for PolyMrPackFormatImpl {
|
|||
) -> LauncherResult<()> {
|
||||
let manifest = self.manifest().await?;
|
||||
|
||||
super::mrpack::download_and_link_packages(cluster, &manifest, skip_compatibility, &ingress)
|
||||
.await?;
|
||||
super::mrpack::download_and_link_packages(
|
||||
cluster,
|
||||
manifest,
|
||||
skip_compatibility,
|
||||
ingress.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(path) = self.archive.as_ref() {
|
||||
super::mrpack::copy_overrides_folder(cluster, path, &ingress).await?;
|
||||
|
|
@ -185,6 +195,7 @@ impl InstallableModpackFormatExt for PolyMrPackFormatImpl {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn to_modpack_files(mrpack_files: &Vec<PolyMrPackFile>) -> LauncherResult<Vec<ModpackFile>> {
|
||||
#[derive(Clone)]
|
||||
struct FetchedPackage {
|
||||
|
|
@ -202,7 +213,7 @@ async fn to_modpack_files(mrpack_files: &Vec<PolyMrPackFile>) -> LauncherResult<
|
|||
.base
|
||||
.path
|
||||
.split('/')
|
||||
.last()
|
||||
.next_back()
|
||||
.unwrap_or(&file.base.path)
|
||||
.to_string();
|
||||
|
||||
|
|
@ -240,7 +251,7 @@ async fn to_modpack_files(mrpack_files: &Vec<PolyMrPackFile>) -> LauncherResult<
|
|||
.first()
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
tracing::warn!("mrpack file '{}' does not contain a download URL", name)
|
||||
tracing::warn!("mrpack file '{}' does not contain a download URL", name);
|
||||
})
|
||||
.unwrap_or(String::new());
|
||||
|
||||
|
|
@ -251,8 +262,7 @@ async fn to_modpack_files(mrpack_files: &Vec<PolyMrPackFile>) -> LauncherResult<
|
|||
.path
|
||||
.split('/')
|
||||
.next()
|
||||
.and_then(|s| PackageType::try_from(s).ok())
|
||||
.unwrap_or(PackageType::Mod);
|
||||
.map_or(PackageType::Mod, PackageType::from);
|
||||
|
||||
files.push(ModpackFile {
|
||||
enabled: file.enabled,
|
||||
|
|
@ -292,7 +302,7 @@ async fn to_modpack_files(mrpack_files: &Vec<PolyMrPackFile>) -> LauncherResult<
|
|||
files.push(ModpackFile {
|
||||
enabled: fetched.enabled,
|
||||
hidden: fetched.hidden,
|
||||
kind: ModpackFileKind::Managed((fetched_pkg, version)),
|
||||
kind: ModpackFileKind::Managed(Box::new((fetched_pkg, version))),
|
||||
overrides: fetched.overrides.clone(),
|
||||
});
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use crate::error::{LauncherError, LauncherResult};
|
|||
|
||||
use super::super::provider::tests::init;
|
||||
|
||||
const DATA: LazyLock<Vec<(ModpackFormat, Vec<Entry>)>> = LazyLock::new(|| {
|
||||
static DATA: LazyLock<Vec<(ModpackFormat, Vec<Entry>)>> = LazyLock::new(|| {
|
||||
vec![(
|
||||
ModpackFormat::MrPack,
|
||||
vec![Entry {
|
||||
|
|
@ -57,7 +57,7 @@ pub async fn test_install_modpack() -> LauncherResult<()> {
|
|||
assert_eq!(modpack_version.version_id, entry.version_id);
|
||||
|
||||
let modpack_model =
|
||||
api::packages::download_package(&modpack_pkg, &modpack_version, None, None).await?;
|
||||
api::packages::download_package(&modpack_pkg, modpack_version, None, None).await?;
|
||||
let mut cluster = api::cluster::create_cluster(
|
||||
"Modpack Format Test",
|
||||
modpack_version.mc_versions.first().unwrap(),
|
||||
|
|
@ -70,7 +70,7 @@ pub async fn test_install_modpack() -> LauncherResult<()> {
|
|||
let modpack_format = ModpackFormat::from_file(&modpack_model.path().await?).await?;
|
||||
api::packages::modpack::install_managed_modpack(
|
||||
&modpack_model,
|
||||
&modpack_format,
|
||||
modpack_format.as_ref(),
|
||||
&mut cluster,
|
||||
None,
|
||||
None,
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ impl ProviderExt for CurseForgeProviderImpl {
|
|||
"[{}]",
|
||||
game_versions
|
||||
.iter()
|
||||
.map(|v| format!("\"{}\"", v))
|
||||
.map(|v| format!("\"{v}\""))
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
);
|
||||
|
|
@ -79,7 +79,7 @@ impl ProviderExt for CurseForgeProviderImpl {
|
|||
"[{}]",
|
||||
loaders
|
||||
.iter()
|
||||
.map(|l| (CFLoader::from(l.clone()) as u32).to_string())
|
||||
.map(|l| (CFLoader::from(*l) as u32).to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
);
|
||||
|
|
@ -146,7 +146,7 @@ impl ProviderExt for CurseForgeProviderImpl {
|
|||
hashes: &[String],
|
||||
) -> LauncherResult<HashMap<String, ManagedVersion>> {
|
||||
let body = serde_json::json!({
|
||||
"fingerprints": hashes.into_iter().filter_map(|hash| hash.parse::<u32>().ok()).collect::<Vec<u32>>()
|
||||
"fingerprints": hashes.iter().filter_map(|hash| hash.parse::<u32>().ok()).collect::<Vec<u32>>()
|
||||
});
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
@ -198,7 +198,7 @@ impl ProviderExt for CurseForgeProviderImpl {
|
|||
) -> LauncherResult<Vec<ManagedUser>> {
|
||||
match author {
|
||||
PackageAuthor::Users(users) => Ok(users),
|
||||
_ => Err(PackageError::UnsupportedAuthorType(author).into()),
|
||||
PackageAuthor::Team { .. } => Err(PackageError::UnsupportedAuthorType(author).into()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -293,7 +293,7 @@ async fn fetch_advanced<T: DeserializeOwned, F: FnOnce(&mut Url)>(
|
|||
onelauncher_entity::package::Provider::CurseForge,
|
||||
))?;
|
||||
|
||||
let mut headers = headers.unwrap_or(HashMap::<&str, &str>::new());
|
||||
let mut headers = headers.unwrap_or_default();
|
||||
headers.insert("x-api-key", key);
|
||||
|
||||
let url = &mut Url::parse(format!("{}{}", crate::constants::CURSEFORGE_API_URL, url).as_str())?;
|
||||
|
|
@ -354,7 +354,7 @@ impl From<PackageType> for CFPackageType {
|
|||
}
|
||||
|
||||
/// Mapping of Curseforge supported loaders to their respective id
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize_repr, Deserialize_repr, Hash, PartialEq)]
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize_repr, Deserialize_repr, Hash, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum CFLoader {
|
||||
#[default]
|
||||
|
|
@ -370,13 +370,13 @@ pub enum CFLoader {
|
|||
impl From<String> for CFLoader {
|
||||
fn from(loader: String) -> Self {
|
||||
match loader.to_lowercase().as_str() {
|
||||
"forge" => CFLoader::Forge,
|
||||
"cauldron" => CFLoader::Cauldron,
|
||||
"liteloader" => CFLoader::LiteLoader,
|
||||
"fabric" => CFLoader::Fabric,
|
||||
"quilt" => CFLoader::Quilt,
|
||||
"neoforge" => CFLoader::NeoForge,
|
||||
_ => CFLoader::Any,
|
||||
"forge" => Self::Forge,
|
||||
"cauldron" => Self::Cauldron,
|
||||
"liteloader" => Self::LiteLoader,
|
||||
"fabric" => Self::Fabric,
|
||||
"quilt" => Self::Quilt,
|
||||
"neoforge" => Self::NeoForge,
|
||||
_ => Self::Any,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -384,11 +384,11 @@ impl From<String> for CFLoader {
|
|||
impl From<GameLoader> for CFLoader {
|
||||
fn from(loader: GameLoader) -> Self {
|
||||
match loader {
|
||||
GameLoader::Forge => CFLoader::Forge,
|
||||
GameLoader::Fabric | GameLoader::LegacyFabric => CFLoader::Fabric,
|
||||
GameLoader::Quilt => CFLoader::Quilt,
|
||||
GameLoader::NeoForge => CFLoader::NeoForge,
|
||||
_ => CFLoader::Any,
|
||||
GameLoader::Forge => Self::Forge,
|
||||
GameLoader::Fabric | GameLoader::LegacyFabric => Self::Fabric,
|
||||
GameLoader::Quilt => Self::Quilt,
|
||||
GameLoader::NeoForge => Self::NeoForge,
|
||||
GameLoader::Vanilla => Self::Any,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -400,7 +400,7 @@ impl From<CFLoader> for GameLoader {
|
|||
CFLoader::Fabric => Self::Fabric,
|
||||
CFLoader::Quilt => Self::Quilt,
|
||||
CFLoader::NeoForge => Self::NeoForge,
|
||||
_ => GameLoader::Vanilla,
|
||||
_ => Self::Vanilla,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -448,10 +448,7 @@ impl From<CFPackage> for ManagedPackage {
|
|||
mc_versions.push(index.game_version);
|
||||
latest_version_ids.push(index.file_id.to_string());
|
||||
|
||||
if let Some(loader) = index
|
||||
.mod_loader
|
||||
.and_then(|loader| GameLoader::try_from(loader).ok())
|
||||
{
|
||||
if let Some(loader) = index.mod_loader.map(GameLoader::from) {
|
||||
loaders.push(loader);
|
||||
}
|
||||
}
|
||||
|
|
@ -473,7 +470,11 @@ impl From<CFPackage> for ManagedPackage {
|
|||
server: PackageSide::Unknown, // TODO: determine server side
|
||||
categories: CurseForgeCategories::to_list(
|
||||
&package_type,
|
||||
&value.categories.into_iter().map(|c| c.id).collect(),
|
||||
&value
|
||||
.categories
|
||||
.into_iter()
|
||||
.map(|c| c.id)
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
package_type,
|
||||
license: None,
|
||||
|
|
@ -508,8 +509,6 @@ impl From<CFPackage> for ManagedPackage {
|
|||
.iter()
|
||||
.find(|l| l.link_type == CFSocialLinkType::Website)
|
||||
.map(|l| l.url.clone()),
|
||||
|
||||
..Default::default()
|
||||
},
|
||||
status: value.status.into(),
|
||||
downloads: value.download_count,
|
||||
|
|
@ -531,11 +530,7 @@ impl From<CFPackage> for SearchResult {
|
|||
let loaders: Vec<GameLoader> = value
|
||||
.latest_files_indexes
|
||||
.iter()
|
||||
.filter_map(|index| {
|
||||
index
|
||||
.mod_loader
|
||||
.and_then(|loader| GameLoader::try_from(loader).ok())
|
||||
})
|
||||
.filter_map(|index| index.mod_loader.map(GameLoader::from))
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
|
@ -547,8 +542,7 @@ impl From<CFPackage> for SearchResult {
|
|||
author: value
|
||||
.authors
|
||||
.first()
|
||||
.and_then(|a| Some(a.name.clone()))
|
||||
.unwrap_or(String::from("Unknown")),
|
||||
.map_or_else(|| String::from("Unknown"), |a| a.name.clone()),
|
||||
description: value.summary,
|
||||
client_side: PackageSide::Unknown,
|
||||
server_side: PackageSide::Unknown,
|
||||
|
|
@ -556,7 +550,11 @@ impl From<CFPackage> for SearchResult {
|
|||
icon_url: value.logo.map_or(String::new(), |l| l.url),
|
||||
categories: CurseForgeCategories::to_list(
|
||||
&package_type,
|
||||
&value.categories.into_iter().map(|c| c.id).collect(),
|
||||
&value
|
||||
.categories
|
||||
.into_iter()
|
||||
.map(|c| c.id)
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
loaders,
|
||||
package_type,
|
||||
|
|
@ -602,11 +600,8 @@ pub enum CFStatus {
|
|||
impl From<CFStatus> for PackageStatus {
|
||||
fn from(status: CFStatus) -> Self {
|
||||
match status {
|
||||
CFStatus::Unknown => Self::Active,
|
||||
CFStatus::New => Self::Active,
|
||||
CFStatus::Approved => Self::Active,
|
||||
CFStatus::Rejected => Self::Abandoned,
|
||||
CFStatus::Deleted => Self::Abandoned,
|
||||
CFStatus::Unknown | CFStatus::New | CFStatus::Approved => Self::Active,
|
||||
CFStatus::Rejected | CFStatus::Deleted => Self::Abandoned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -751,7 +746,7 @@ pub struct CFModFile {
|
|||
}
|
||||
|
||||
impl From<CFModFile> for ManagedVersion {
|
||||
fn from(value: CFModFile) -> ManagedVersion {
|
||||
fn from(value: CFModFile) -> Self {
|
||||
let mut hashes = HashMap::new();
|
||||
|
||||
for hash in value.hashes {
|
||||
|
|
@ -770,16 +765,15 @@ impl From<CFModFile> for ManagedVersion {
|
|||
}
|
||||
}
|
||||
|
||||
let mut files = Vec::new();
|
||||
files.push(ManagedVersionFile {
|
||||
let files = vec![ManagedVersionFile {
|
||||
url: value.download_url,
|
||||
file_name: value.file_name.clone(),
|
||||
primary: true,
|
||||
size: value.file_size_on_disk.unwrap_or(value.file_length),
|
||||
sha1: hashes.get("sha1").cloned().unwrap_or_default(),
|
||||
});
|
||||
}];
|
||||
|
||||
ManagedVersion {
|
||||
Self {
|
||||
project_id: value.mod_id.to_string(),
|
||||
version_id: value.id.to_string(),
|
||||
display_name: value.display_name.clone(),
|
||||
|
|
@ -863,12 +857,10 @@ pub enum CFRelationType {
|
|||
impl From<CFRelationType> for PackageDependencyType {
|
||||
fn from(value: CFRelationType) -> Self {
|
||||
match value {
|
||||
CFRelationType::Embedded => Self::Embedded,
|
||||
CFRelationType::Optional => Self::Optional,
|
||||
CFRelationType::Embedded | CFRelationType::Include => Self::Embedded,
|
||||
CFRelationType::Optional | CFRelationType::Tool => Self::Optional,
|
||||
CFRelationType::Required => Self::Required,
|
||||
CFRelationType::Tool => Self::Optional,
|
||||
CFRelationType::Incompatible => Self::Incompatible,
|
||||
CFRelationType::Include => Self::Embedded,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,9 @@ pub trait ProviderExt {
|
|||
async fn get_body(&self, body: &ManagedPackageBody) -> LauncherResult<String> {
|
||||
match body {
|
||||
ManagedPackageBody::Raw(raw) => Ok(raw.clone()),
|
||||
_ => Err(PackageError::UnsupportedBodyType(body.clone()).into()),
|
||||
ManagedPackageBody::Url(_) => {
|
||||
Err(PackageError::UnsupportedBodyType(body.clone()).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,9 +133,8 @@ impl ProviderExt for ModrinthProviderImpl {
|
|||
if let Some((package, timestamp)) = cache.get(slug) {
|
||||
if timestamp.elapsed() < CACHE_DURATION {
|
||||
return Ok(package.clone());
|
||||
} else {
|
||||
cache.remove(slug);
|
||||
}
|
||||
cache.remove(slug);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -269,9 +268,8 @@ impl ProviderExt for ModrinthProviderImpl {
|
|||
if let Some((version, timestamp)) = cache.get(hash) {
|
||||
if timestamp.elapsed() < CACHE_DURATION {
|
||||
return Ok(Some(version.clone()));
|
||||
} else {
|
||||
cache.remove(hash);
|
||||
}
|
||||
cache.remove(hash);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -400,7 +398,7 @@ impl ProviderExt for ModrinthProviderImpl {
|
|||
|
||||
if let Some(mc_version) = mc_version {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("game_versions", &format!("[\"{}\"]", mc_version));
|
||||
.append_pair("game_versions", &format!("[\"{mc_version}\"]"));
|
||||
}
|
||||
|
||||
if let Some(loader) = loader {
|
||||
|
|
|
|||
|
|
@ -160,9 +160,7 @@ pub async fn test_get_versions() -> LauncherResult<()> {
|
|||
.map(|e| e.version_id.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let res = provider
|
||||
.get_versions(&slugs.iter().map(|s| s.to_string()).collect::<Vec<String>>())
|
||||
.await?;
|
||||
let res = provider.get_versions(&slugs.clone()).await?;
|
||||
|
||||
assert_eq!(res.len(), slugs.len());
|
||||
}
|
||||
|
|
@ -186,7 +184,7 @@ pub async fn test_download_packages() -> LauncherResult<()> {
|
|||
|
||||
let pkg = provider.get(entry.id).await?;
|
||||
let ver_id = entry.version_id.to_string();
|
||||
let versions = provider.get_versions(&[ver_id.clone()]).await?;
|
||||
let versions = provider.get_versions(std::slice::from_ref(&ver_id)).await?;
|
||||
let ver = versions.first().expect("No version found");
|
||||
|
||||
let db_model = packages::download_package(&pkg, ver, None, None).await?;
|
||||
|
|
@ -205,7 +203,7 @@ pub async fn test_download_packages() -> LauncherResult<()> {
|
|||
pub async fn test_search_packages() -> LauncherResult<()> {
|
||||
let cf = init!();
|
||||
|
||||
for provider in Provider::get_providers().iter() {
|
||||
for provider in Provider::get_providers() {
|
||||
if provider == &Provider::CurseForge && !cf {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -213,8 +211,6 @@ pub async fn test_search_packages() -> LauncherResult<()> {
|
|||
let query = SearchQuery::default();
|
||||
let res = provider.search(&query).await?;
|
||||
|
||||
dbg!(&res.items);
|
||||
|
||||
assert!(!res.items.is_empty());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use tokio::sync::RwLock;
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::LauncherResult;
|
||||
use crate::api::processes::ProcessPayload;
|
||||
use crate::api::processes::ProcessPayloadKind;
|
||||
use crate::api::proxy::LauncherProxy;
|
||||
use crate::api::proxy::event::LauncherEvent;
|
||||
use crate::constants::CLI_TOTAL_INGRESS;
|
||||
|
|
@ -58,17 +58,17 @@ impl LauncherProxy for ProxyCli {
|
|||
LauncherEvent::Message(message) => {
|
||||
println!("[{:?}] {}", message.level, message.message);
|
||||
}
|
||||
LauncherEvent::Process(process) => match process {
|
||||
ProcessPayload::Starting { command } => {
|
||||
LauncherEvent::Process(process) => match process.kind {
|
||||
ProcessPayloadKind::Starting { command } => {
|
||||
println!("Starting process: {command}");
|
||||
}
|
||||
ProcessPayload::Started { process } => {
|
||||
println!("Process started: {process:#?}");
|
||||
ProcessPayloadKind::Started { pid, .. } => {
|
||||
println!("Process started: {pid}");
|
||||
}
|
||||
ProcessPayload::Stopped { pid, exit_code } => {
|
||||
ProcessPayloadKind::Stopped { pid, exit_code } => {
|
||||
println!("Process {pid} exited with code {exit_code}");
|
||||
}
|
||||
ProcessPayload::Output { pid, output } => {
|
||||
ProcessPayloadKind::Output { pid, output } => {
|
||||
println!("Process {pid}: {output}");
|
||||
}
|
||||
},
|
||||
|
|
@ -76,4 +76,12 @@ impl LauncherProxy for ProxyCli {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_main_window(&self) -> LauncherResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_main_window(&self) -> LauncherResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ impl LauncherProxy for ProxyEmpty {
|
|||
"{} [{}] {}",
|
||||
ingress
|
||||
.percent
|
||||
.map_or("100%".to_string(), |p| format!("{:.2}%", p * 100.0)),
|
||||
.map_or_else(|| "100%".to_string(), |p| format!("{:.2}%", p * 100.0)),
|
||||
ingress.id,
|
||||
ingress.message
|
||||
);
|
||||
|
|
|
|||
|
|
@ -54,11 +54,9 @@ impl LauncherProxy for ProxyTauri {
|
|||
}
|
||||
}
|
||||
|
||||
Ok(match event {
|
||||
let _: () = match event {
|
||||
LauncherEvent::Ingress(ingress) => {
|
||||
self.ingress_queue.push(ingress);
|
||||
|
||||
()
|
||||
}
|
||||
LauncherEvent::Message(message) => {
|
||||
let emitter = Arc::clone(&self.emitter);
|
||||
|
|
@ -76,7 +74,8 @@ impl LauncherProxy for ProxyTauri {
|
|||
}
|
||||
})?;
|
||||
}
|
||||
})
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
|
|
@ -106,6 +105,7 @@ pub struct IngressQueue {
|
|||
}
|
||||
|
||||
impl IngressQueue {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
emitter: Arc<LauncherEventEmitter<tauri::Wry>>,
|
||||
debounce_duration: Duration,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ pub mod dao {
|
|||
if let Some(name) = profile.name.try_as_ref()
|
||||
&& (name.is_empty() || profile.is_global())
|
||||
{
|
||||
return Err(DaoError::InvalidValue(name.to_string(), "name".into()).into());
|
||||
return Err(DaoError::InvalidValue(name.clone(), "name".into()).into());
|
||||
}
|
||||
|
||||
let state = State::get().await?;
|
||||
|
|
|
|||
|
|
@ -419,7 +419,7 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
|||
}
|
||||
|
||||
if let Some(setting_profile_name) = request.setting_profile_name {
|
||||
active_model.setting_profile_name = Set(Some(setting_profile_name))
|
||||
active_model.setting_profile_name = Set(Some(setting_profile_name));
|
||||
}
|
||||
|
||||
Ok(active_model)
|
||||
|
|
@ -849,7 +849,7 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
|||
"syncCluster complete for cluster_id={}, action={:?}",
|
||||
cluster_id,
|
||||
action
|
||||
)
|
||||
);
|
||||
}
|
||||
Err(e) => tracing::error!("syncCluster failed for cluster_id={}: {}", cluster_id, e),
|
||||
_ => {}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ mod debug;
|
|||
pub use debug::*;
|
||||
|
||||
pub trait TauRPCLauncherExt {
|
||||
#[must_use]
|
||||
fn use_launcher_api(self) -> Self;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ pub const MINECRAFT_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
|||
// !!! URLS must NOT have a trailing slash. !!!
|
||||
/// The Modrinth API base url.
|
||||
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com";
|
||||
/// The CurseForge API base url.
|
||||
/// The `CurseForge` API base url.
|
||||
pub const CURSEFORGE_API_URL: &str = "https://api.curseforge.com/v1";
|
||||
/// The Minecraft game ID on CurseForge.
|
||||
/// The Minecraft game ID on `CurseForge`.
|
||||
pub const CURSEFORGE_GAME_ID: u32 = 432;
|
||||
/// Our metadata API base url.
|
||||
pub const METADATA_API_URL: &str = "https://meta.polyfrost.org";
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ impl ProcessStore {
|
|||
process_guard
|
||||
.process_loop
|
||||
.as_ref()
|
||||
.map_or(true, |handle| handle.is_finished()),
|
||||
.is_none_or(|handle| handle.is_finished()),
|
||||
process_guard.child_type.clone(),
|
||||
)
|
||||
};
|
||||
|
|
@ -199,7 +199,7 @@ impl ProcessStore {
|
|||
Ok(Some(_)) => true,
|
||||
Ok(None) => {
|
||||
let exists = process_exists_os(pid);
|
||||
if exists { false } else { true }
|
||||
!exists
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
|
|
@ -232,6 +232,7 @@ impl ProcessStore {
|
|||
}
|
||||
|
||||
/// Spawns a new process (Minecraft Instance)
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn spawn(
|
||||
&self,
|
||||
cluster_id: ClusterId,
|
||||
|
|
@ -549,6 +550,7 @@ async fn wait_for_exit(
|
|||
fn process_exists_os(pid: u32) -> bool {
|
||||
// SAFETY: `kill` with signal 0 performs existence/permission checks only and
|
||||
// does not deliver a signal.
|
||||
#[allow(clippy::cast_possible_wrap)]
|
||||
let rc = unsafe { libc::kill(pid as i32, 0) };
|
||||
if rc == 0 {
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -73,24 +73,25 @@ pub async fn request(method: Method, url: &str) -> LauncherResult<reqwest::Respo
|
|||
pub fn build_request(method: Method, url: &str) -> RequestBuilder {
|
||||
let client = &REQWEST_CLIENT;
|
||||
|
||||
client.request(method.clone(), url)
|
||||
client.request(method, url)
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
fn get_ratelimit_reset(headers: &reqwest::header::HeaderMap) -> Option<u64> {
|
||||
headers
|
||||
.get("X-Ratelimit-Reset")
|
||||
.or(headers.get("retry-after"))
|
||||
.or_else(|| headers.get("retry-after"))
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.map(|s| s.ceil() as u64)
|
||||
}
|
||||
|
||||
pub async fn send_request(request: reqwest::Request) -> LauncherResult<reqwest::Response> {
|
||||
if let Some(host) = request.url().host_str() {
|
||||
if host == "api.modrinth.com" {
|
||||
let _start = std::time::Instant::now();
|
||||
MODRINTH_RATE_LIMITER.until_ready().await;
|
||||
}
|
||||
if let Some(host) = request.url().host_str()
|
||||
&& host == "api.modrinth.com"
|
||||
{
|
||||
let _start = std::time::Instant::now();
|
||||
MODRINTH_RATE_LIMITER.until_ready().await;
|
||||
}
|
||||
|
||||
let semaphore = SemaphoreStore::fetch().await;
|
||||
|
|
@ -121,11 +122,11 @@ pub async fn send_request(request: reqwest::Request) -> LauncherResult<reqwest::
|
|||
return Err(err.into());
|
||||
}
|
||||
Ok(res) => {
|
||||
if res.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
|
||||
if let Some(reset) = get_ratelimit_reset(res.headers()) {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(reset)).await;
|
||||
continue;
|
||||
}
|
||||
if res.status() == reqwest::StatusCode::TOO_MANY_REQUESTS
|
||||
&& let Some(reset) = get_ratelimit_reset(res.headers())
|
||||
{
|
||||
tokio::time::sleep(std::time::Duration::from_secs(reset)).await;
|
||||
continue;
|
||||
}
|
||||
break res;
|
||||
}
|
||||
|
|
@ -148,13 +149,12 @@ pub async fn fetch_advanced(
|
|||
headers: Option<HashMap<&str, &str>>,
|
||||
ingress: Option<&SubIngress<'_>>,
|
||||
) -> LauncherResult<Bytes> {
|
||||
if let Ok(parsed_url) = reqwest::Url::parse(url) {
|
||||
if let Some(host) = parsed_url.host_str() {
|
||||
if host == "api.modrinth.com" {
|
||||
let _start = std::time::Instant::now();
|
||||
MODRINTH_RATE_LIMITER.until_ready().await;
|
||||
}
|
||||
}
|
||||
if let Ok(parsed_url) = reqwest::Url::parse(url)
|
||||
&& let Some(host) = parsed_url.host_str()
|
||||
&& host == "api.modrinth.com"
|
||||
{
|
||||
let _start = std::time::Instant::now();
|
||||
MODRINTH_RATE_LIMITER.until_ready().await;
|
||||
}
|
||||
|
||||
let semaphore = SemaphoreStore::fetch().await;
|
||||
|
|
@ -189,11 +189,11 @@ pub async fn fetch_advanced(
|
|||
return Err(err.into());
|
||||
}
|
||||
Ok(res) => {
|
||||
if res.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
|
||||
if let Some(reset) = get_ratelimit_reset(res.headers()) {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(reset)).await;
|
||||
continue;
|
||||
}
|
||||
if res.status() == reqwest::StatusCode::TOO_MANY_REQUESTS
|
||||
&& let Some(reset) = get_ratelimit_reset(res.headers())
|
||||
{
|
||||
tokio::time::sleep(std::time::Duration::from_secs(reset)).await;
|
||||
continue;
|
||||
}
|
||||
break res;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -410,11 +410,9 @@ pub async fn extract_zip_filtered(
|
|||
}
|
||||
|
||||
let old_name = entry.filename().as_str()?;
|
||||
let name: String = if let Some(modify) = &modify_entry_name {
|
||||
modify(&old_name)
|
||||
} else {
|
||||
old_name.to_string()
|
||||
};
|
||||
let name: String = modify_entry_name
|
||||
.as_ref()
|
||||
.map_or_else(|| old_name.to_string(), |modify| modify(old_name));
|
||||
|
||||
let name = sanitize_path(name);
|
||||
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ pub enum SkinVariant {
|
|||
impl std::fmt::Display for SkinVariant {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SkinVariant::Classic => write!(f, "classic"),
|
||||
SkinVariant::Slim => write!(f, "slim"),
|
||||
Self::Classic => write!(f, "classic"),
|
||||
Self::Slim => write!(f, "slim"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -78,14 +78,6 @@ pub async fn fetch_player_profile(uuid: &str) -> LauncherResult<MojangPlayerProf
|
|||
properties: Vec<Properties>,
|
||||
}
|
||||
|
||||
let response = http::fetch_json::<Response>(
|
||||
Method::GET,
|
||||
&format!("https://sessionserver.mojang.com/session/minecraft/profile/{uuid}"),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[derive(Default, serde::Deserialize)]
|
||||
#[serde(default)]
|
||||
struct TextureMetadata {
|
||||
|
|
@ -111,6 +103,14 @@ pub async fn fetch_player_profile(uuid: &str) -> LauncherResult<MojangPlayerProf
|
|||
textures: Textures,
|
||||
}
|
||||
|
||||
let response = http::fetch_json::<Response>(
|
||||
Method::GET,
|
||||
&format!("https://sessionserver.mojang.com/session/minecraft/profile/{uuid}"),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let texture_property = response
|
||||
.properties
|
||||
.iter()
|
||||
|
|
@ -138,7 +138,7 @@ pub async fn fetch_player_profile(uuid: &str) -> LauncherResult<MojangPlayerProf
|
|||
let is_slim = decoded
|
||||
.skin
|
||||
.as_ref()
|
||||
.map_or(false, |s| s.metadata.model.as_deref() == Some("slim"));
|
||||
.is_some_and(|s| s.metadata.model.as_deref() == Some("slim"));
|
||||
|
||||
Ok(MojangPlayerProfile {
|
||||
uuid: response.id,
|
||||
|
|
@ -201,7 +201,7 @@ pub async fn upload_skin_bytes(
|
|||
skin_variant: SkinVariant,
|
||||
) -> LauncherResult<MojangSkin> {
|
||||
let bytes_part =
|
||||
multipart::Part::bytes(skin_data).mime_str(&format!("image/{}", image_format))?;
|
||||
multipart::Part::bytes(skin_data).mime_str(&format!("image/{image_format}"))?;
|
||||
|
||||
let form = multipart::Form::new()
|
||||
.text("variant", skin_variant.to_string())
|
||||
|
|
@ -227,7 +227,6 @@ pub async fn upload_skin_bytes(
|
|||
}
|
||||
|
||||
let full_profile = res.json::<MojangFullPlayerProfile>().await?;
|
||||
dbg!(&full_profile);
|
||||
full_profile
|
||||
.skins
|
||||
.first()
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ impl PackageType {
|
|||
impl From<&str> for PackageType {
|
||||
fn from(value: &str) -> Self {
|
||||
match value.to_lowercase().as_str() {
|
||||
"mod" | "mods" => Self::Mod,
|
||||
"resourcepack" | "resourcepacks" => Self::ResourcePack,
|
||||
"shader" | "shaders" | "shaderpacks" => Self::Shader,
|
||||
"datapack" | "datapacks" => Self::DataPack,
|
||||
|
|
@ -51,16 +50,16 @@ impl From<String> for PackageType {
|
|||
}
|
||||
}
|
||||
|
||||
impl ToString for PackageType {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
&Self::Mod => "mod",
|
||||
&Self::ResourcePack => "resourcepack",
|
||||
&Self::Shader => "shader",
|
||||
&Self::DataPack => "datapack",
|
||||
&Self::ModPack => "modpack",
|
||||
}
|
||||
.to_string()
|
||||
impl Display for PackageType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match *self {
|
||||
Self::Mod => "mod",
|
||||
Self::ResourcePack => "resourcepack",
|
||||
Self::Shader => "shader",
|
||||
Self::DataPack => "datapack",
|
||||
Self::ModPack => "modpack",
|
||||
};
|
||||
write!(f, "{s}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,33 +5,17 @@ use serde::{Deserialize, Serialize};
|
|||
cfg_select! {
|
||||
target_os = "windows" => {
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SettingsOsExtra {
|
||||
|
||||
}
|
||||
|
||||
impl Default for SettingsOsExtra {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
target_os = "macos" => {
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SettingsOsExtra {
|
||||
|
||||
}
|
||||
|
||||
impl Default for SettingsOsExtra {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
not(any(target_os = "windows", target_os = "macos")) => {
|
||||
#[onelauncher_macro::specta]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#![feature(cfg_select)]
|
||||
|
||||
mod entities;
|
||||
mod domain;
|
||||
mod entities;
|
||||
|
||||
pub use domain::*;
|
||||
pub use entities::*;
|
||||
pub use domain::*;
|
||||
|
|
@ -15,7 +15,9 @@ pub fn request_start_for_wrapper(pid: u32) -> Result<(), String> {
|
|||
if result == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("failed to request gamemode for pid {pid}: {result}"))
|
||||
Err(format!(
|
||||
"failed to request gamemode for pid {pid}: {result}"
|
||||
))
|
||||
}
|
||||
},
|
||||
Err(e) => Err(format!("failed to request gamemode for pid {pid}: {e}")),
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@ use sea_orm_migration::prelude::*;
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
cli::run_cli(onelauncher_migration::Migrator).await;
|
||||
cli::run_cli(onelauncher_migration::Migrator).await;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,180 +8,180 @@ pub use de::{Deserializer, StreamDeserializer};
|
|||
pub use de::{from_reader, from_slice, from_str};
|
||||
/// Deserialize JSON data to a Rust data structure.
|
||||
pub mod de {
|
||||
// See caveats in README.md
|
||||
pub use serde_json::de::{Deserializer, StreamDeserializer};
|
||||
// See caveats in README.md
|
||||
pub use serde_json::de::{Deserializer, StreamDeserializer};
|
||||
|
||||
use crate::Result;
|
||||
use crate::Result;
|
||||
|
||||
use std::io;
|
||||
use std::io;
|
||||
|
||||
pub use serde_json::de::{IoRead, Read, SliceRead, StrRead};
|
||||
pub use serde_json::de::{IoRead, Read, SliceRead, StrRead};
|
||||
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use serde::Deserialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
/// Deserialize an instance of type `T` from a string of JSON text.
|
||||
///
|
||||
/// Equivalent to [serde_json::from_str] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::from_str] for more documentation.
|
||||
pub fn from_str<'a, T>(s: &'a str) -> Result<T>
|
||||
where
|
||||
T: Deserialize<'a>,
|
||||
{
|
||||
let jd = &mut serde_json::Deserializer::from_str(s);
|
||||
/// Deserialize an instance of type `T` from a string of JSON text.
|
||||
///
|
||||
/// Equivalent to [serde_json::from_str] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::from_str] for more documentation.
|
||||
pub fn from_str<'a, T>(s: &'a str) -> Result<T>
|
||||
where
|
||||
T: Deserialize<'a>,
|
||||
{
|
||||
let jd = &mut serde_json::Deserializer::from_str(s);
|
||||
|
||||
serde_path_to_error::deserialize(jd)
|
||||
}
|
||||
serde_path_to_error::deserialize(jd)
|
||||
}
|
||||
|
||||
/// Deserialize an instance of type `T` from an I/O stream of JSON.
|
||||
///
|
||||
/// Equivalent to [serde_json::from_reader] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::from_reader] for more documentation.
|
||||
pub fn from_reader<R, T>(rdr: R) -> Result<T>
|
||||
where
|
||||
R: io::Read,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let jd = &mut serde_json::Deserializer::from_reader(rdr);
|
||||
/// Deserialize an instance of type `T` from an I/O stream of JSON.
|
||||
///
|
||||
/// Equivalent to [serde_json::from_reader] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::from_reader] for more documentation.
|
||||
pub fn from_reader<R, T>(rdr: R) -> Result<T>
|
||||
where
|
||||
R: io::Read,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let jd = &mut serde_json::Deserializer::from_reader(rdr);
|
||||
|
||||
serde_path_to_error::deserialize(jd)
|
||||
}
|
||||
serde_path_to_error::deserialize(jd)
|
||||
}
|
||||
|
||||
/// Deserialize an instance of type `T` from bytes of JSON text.
|
||||
///
|
||||
/// Equivalent to [serde_json::from_slice] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::from_slice] for more documentation.
|
||||
pub fn from_slice<'a, T>(v: &'a [u8]) -> Result<T>
|
||||
where
|
||||
T: Deserialize<'a>,
|
||||
{
|
||||
let jd = &mut serde_json::Deserializer::from_slice(v);
|
||||
|
||||
serde_path_to_error::deserialize(jd)
|
||||
}
|
||||
/// Deserialize an instance of type `T` from bytes of JSON text.
|
||||
///
|
||||
/// Equivalent to [serde_json::from_slice] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::from_slice] for more documentation.
|
||||
pub fn from_slice<'a, T>(v: &'a [u8]) -> Result<T>
|
||||
where
|
||||
T: Deserialize<'a>,
|
||||
{
|
||||
let jd = &mut serde_json::Deserializer::from_slice(v);
|
||||
|
||||
serde_path_to_error::deserialize(jd)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(inline)]
|
||||
pub use error::{Error, Result};
|
||||
/// When serializing or deserializing JSON goes wrong.
|
||||
pub mod error {
|
||||
pub use serde_json::error::Category;
|
||||
/// This type represents all possible errors that can occur when serializing or
|
||||
/// deserializing JSON data.
|
||||
pub type Error = serde_path_to_error::Error<serde_json::Error>;
|
||||
/// Alias for a `Result` with the error type `serde_json_path_to_error::Error`.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub use serde_json::error::Category;
|
||||
/// This type represents all possible errors that can occur when serializing or
|
||||
/// deserializing JSON data.
|
||||
pub type Error = serde_path_to_error::Error<serde_json::Error>;
|
||||
/// Alias for a `Result` with the error type `serde_json_path_to_error::Error`.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
}
|
||||
|
||||
#[doc(inline)]
|
||||
pub use ser::{
|
||||
to_string, to_string_pretty, to_vec, to_vec_pretty, to_writer, to_writer_pretty, Formatter,
|
||||
Serializer,
|
||||
Formatter, Serializer, to_string, to_string_pretty, to_vec, to_vec_pretty, to_writer,
|
||||
to_writer_pretty,
|
||||
};
|
||||
/// Serialize a Rust data structure into JSON data.
|
||||
pub mod ser {
|
||||
use crate::Result;
|
||||
use serde::Serialize;
|
||||
use crate::Result;
|
||||
use serde::Serialize;
|
||||
|
||||
static UTF8_ERROR: &str =
|
||||
"`serde_json` internally guarantees UTF8 and uses `String::from_utf8_unchecked`. \
|
||||
static UTF8_ERROR: &str = "`serde_json` internally guarantees UTF8 and uses `String::from_utf8_unchecked`. \
|
||||
If this error throws, `serde_json` must have broken this guarantee";
|
||||
|
||||
pub use serde_json::ser::{CharEscape, CompactFormatter, Formatter, PrettyFormatter};
|
||||
pub use serde_json::ser::{CharEscape, CompactFormatter, Formatter, PrettyFormatter};
|
||||
|
||||
// See caveats in README.md
|
||||
pub use serde_json::ser::Serializer;
|
||||
// See caveats in README.md
|
||||
pub use serde_json::ser::Serializer;
|
||||
|
||||
/// Serialize the given data structure as JSON into the I/O stream.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_writer] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_writer] for more documentation.
|
||||
pub fn to_writer<W, T>(writer: W, value: &T) -> Result<()>
|
||||
where
|
||||
W: std::io::Write,
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let mut ser = Serializer::new(writer);
|
||||
serde_path_to_error::serialize(&value, &mut ser)
|
||||
}
|
||||
/// Serialize the given data structure as JSON into the I/O stream.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_writer] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_writer] for more documentation.
|
||||
pub fn to_writer<W, T>(writer: W, value: &T) -> Result<()>
|
||||
where
|
||||
W: std::io::Write,
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let mut ser = Serializer::new(writer);
|
||||
serde_path_to_error::serialize(&value, &mut ser)
|
||||
}
|
||||
|
||||
/// Serialize the given data structure as a JSON byte vector.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_vec] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_vec] for more documentation.
|
||||
pub fn to_vec<T>(value: &T) -> Result<Vec<u8>>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let mut bytes = Vec::new();
|
||||
/// Serialize the given data structure as a JSON byte vector.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_vec] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_vec] for more documentation.
|
||||
pub fn to_vec<T>(value: &T) -> Result<Vec<u8>>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let mut bytes = Vec::new();
|
||||
|
||||
to_writer(&mut bytes, value)?;
|
||||
to_writer(&mut bytes, value)?;
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Serialize the given data structure as a String of JSON.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_string] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_string] for more documentation.
|
||||
pub fn to_string<T>(value: &T) -> Result<String>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let vec = to_vec(value)?;
|
||||
/// Serialize the given data structure as a String of JSON.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_string] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_string] for more documentation.
|
||||
pub fn to_string<T>(value: &T) -> Result<String>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let vec = to_vec(value)?;
|
||||
|
||||
Ok(String::from_utf8(vec).expect(UTF8_ERROR))
|
||||
}
|
||||
Ok(String::from_utf8(vec).expect(UTF8_ERROR))
|
||||
}
|
||||
|
||||
/// Serialize the given data structure as pretty-printed JSON into the I/O
|
||||
/// stream.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_writer_pretty] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_writer_pretty] for more documentation.
|
||||
pub fn to_writer_pretty<W, T>(writer: W, value: &T) -> Result<()>
|
||||
where
|
||||
W: std::io::Write,
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let mut ser = Serializer::pretty(writer);
|
||||
serde_path_to_error::serialize(&value, &mut ser)
|
||||
}
|
||||
/// Serialize the given data structure as pretty-printed JSON into the I/O
|
||||
/// stream.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_writer_pretty] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_writer_pretty] for more documentation.
|
||||
pub fn to_writer_pretty<W, T>(writer: W, value: &T) -> Result<()>
|
||||
where
|
||||
W: std::io::Write,
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let mut ser = Serializer::pretty(writer);
|
||||
serde_path_to_error::serialize(&value, &mut ser)
|
||||
}
|
||||
|
||||
/// Serialize the given data structure as a pretty-printed JSON byte vector.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_vec_pretty] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_vec_pretty] for more documentation.
|
||||
pub fn to_vec_pretty<T>(value: &T) -> Result<Vec<u8>>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let mut bytes = Vec::new();
|
||||
/// Serialize the given data structure as a pretty-printed JSON byte vector.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_vec_pretty] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_vec_pretty] for more documentation.
|
||||
pub fn to_vec_pretty<T>(value: &T) -> Result<Vec<u8>>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let mut bytes = Vec::new();
|
||||
|
||||
to_writer_pretty(&mut bytes, value)?;
|
||||
to_writer_pretty(&mut bytes, value)?;
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Serialize the given data structure as a pretty-printed String of JSON.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_string_pretty] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_string_pretty] for more documentation.
|
||||
pub fn to_string_pretty<T>(value: &T) -> Result<String>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let vec = to_vec_pretty(value)?;
|
||||
/// Serialize the given data structure as a pretty-printed String of JSON.
|
||||
///
|
||||
/// Equivalent to [serde_json::to_string_pretty] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_string_pretty] for more documentation.
|
||||
pub fn to_string_pretty<T>(value: &T) -> Result<String>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let vec = to_vec_pretty(value)?;
|
||||
|
||||
Ok(String::from_utf8(vec).expect(UTF8_ERROR))
|
||||
}
|
||||
Ok(String::from_utf8(vec).expect(UTF8_ERROR))
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(inline)]
|
||||
|
|
@ -190,50 +190,51 @@ pub use map::Map;
|
|||
///
|
||||
/// See [serde_json::map] for more documentation.
|
||||
pub mod map {
|
||||
pub use serde_json::map::{
|
||||
Entry, IntoIter, Iter, IterMut, Keys, Map, OccupiedEntry, VacantEntry, Values, ValuesMut,
|
||||
};
|
||||
pub use serde_json::map::{
|
||||
Entry, IntoIter, Iter, IterMut, Keys, Map, OccupiedEntry, VacantEntry, Values, ValuesMut,
|
||||
};
|
||||
}
|
||||
|
||||
#[doc(inline)]
|
||||
pub use value::{from_value, to_value, Number, Value};
|
||||
pub use value::{Number, Value, from_value, to_value};
|
||||
/// The Value enum, a loosely typed way of representing any valid JSON value.
|
||||
///
|
||||
/// See [serde_json::value] for more documentation.
|
||||
pub mod value {
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
pub use serde_json::value::{Index, Map, Number, Value};
|
||||
pub use serde_json::value::{Index, Map, Number, Value};
|
||||
|
||||
// See caveats in README.md
|
||||
pub use serde_json::value::Serializer;
|
||||
// See caveats in README.md
|
||||
pub use serde_json::value::Serializer;
|
||||
|
||||
use crate::{Error, Result};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Convert a `T` into `serde_json::Value` which is an enum that can represent
|
||||
///
|
||||
/// Equivalent to [serde_json::to_value] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_value] for more documentation.
|
||||
pub fn to_value<T>(value: T) -> Result<Value>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
let mut track = serde_path_to_error::Track::new();
|
||||
let ps = serde_path_to_error::Serializer::new(Serializer, &mut track);
|
||||
/// Convert a `T` into `serde_json::Value` which is an enum that can represent
|
||||
///
|
||||
/// Equivalent to [serde_json::to_value] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::to_value] for more documentation.
|
||||
pub fn to_value<T>(value: T) -> Result<Value>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
let mut track = serde_path_to_error::Track::new();
|
||||
let ps = serde_path_to_error::Serializer::new(Serializer, &mut track);
|
||||
|
||||
value.serialize(ps).map_err(|e| Error::new(track.path(), e))
|
||||
}
|
||||
value.serialize(ps).map_err(|e| Error::new(track.path(), e))
|
||||
}
|
||||
|
||||
/// Interpret a `serde_json::Value` as an instance of type `T`.
|
||||
///
|
||||
/// Equivalent to [serde_json::from_value] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::from_value] for more documentation.
|
||||
pub fn from_value<T>(value: Value) -> Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
serde_path_to_error::deserialize(value)
|
||||
}
|
||||
}
|
||||
/// Interpret a `serde_json::Value` as an instance of type `T`.
|
||||
///
|
||||
/// Equivalent to [serde_json::from_value] but with errors extended with [serde_path_to_error].
|
||||
///
|
||||
/// See [serde_json::from_value] for more documentation.
|
||||
pub fn from_value<T>(value: Value) -> Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
serde_path_to_error::deserialize(value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { HTMLAttributes, ReactNode, Ref } from 'react';
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
// i thank chatgpt for this i dont want to replace current tabs component yet so this is going to be here for some time
|
||||
|
|
@ -26,19 +26,20 @@ interface TabsProps {
|
|||
|
||||
export function Tabs({ defaultValue, onTabChange, children }: TabsProps) {
|
||||
const [activeTab, setActiveTabInternal] = useState(defaultValue);
|
||||
const setActiveTab = (value: string) => {
|
||||
setActiveTabInternal(value)
|
||||
if (onTabChange) onTabChange(value)
|
||||
}
|
||||
const setActiveTab = useCallback((value: string) => {
|
||||
setActiveTabInternal(value);
|
||||
if (onTabChange)
|
||||
onTabChange(value);
|
||||
}, [onTabChange]);
|
||||
|
||||
const contextValue = useMemo(() => ({ activeTab, setActiveTab }), [activeTab, setActiveTab]);
|
||||
|
||||
return (
|
||||
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
|
||||
<TabsContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</TabsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
interface TabListProps extends HTMLAttributes<HTMLDivElement> {
|
||||
floating?: boolean;
|
||||
height?: boolean;
|
||||
|
|
@ -53,7 +54,7 @@ export function TabList({
|
|||
ref,
|
||||
}: TabListProps) {
|
||||
return (
|
||||
<div className={twMerge("pointer-events-none sticky top-0 z-10 w-full", height ? "min-h-[74px] h-[74px] max-h-[74px]" : "")} ref={ref}>
|
||||
<div className={twMerge('pointer-events-none sticky top-0 z-10 w-full', height ? 'min-h-[74px] h-[74px] max-h-[74px]' : '')} ref={ref}>
|
||||
<div
|
||||
className={twMerge(
|
||||
'pointer-events-auto flex flex-row gap-2 border border-transparent bg-page-elevated transition-all',
|
||||
|
|
@ -117,7 +118,7 @@ export function TabPanel({ children, value, className, ...rest }: TabPanelProps)
|
|||
|
||||
return (
|
||||
<div
|
||||
className={twMerge("bg-page-elevated px-3 pt-3 w-full rounded-2xl h-full", className)}
|
||||
className={twMerge('bg-page-elevated px-3 pt-3 w-full rounded-2xl h-full', className)}
|
||||
role="tabpanel"
|
||||
{...rest}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue