This commit is contained in:
Wyvest 2026-02-25 08:14:57 -05:00
parent 6811f3c41a
commit c515d00da1
83 changed files with 850 additions and 813 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -94,6 +94,7 @@ export function SkinViewer({
return () => {
viewer.dispose();
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentional mount-only initialization
}, []);
useEffect(() => {

View file

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

View file

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

View file

@ -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__']), []);

View file

@ -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__']), []);

View file

@ -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__']), []);

View file

@ -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__']), []);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,11 +32,7 @@ function Search() {
return (
<>
{
search.data
? <Results results={search.data} />
: <h1>Loading</h1>
}
<Results results={search.data} />
</>
);
}

View file

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

View file

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

View file

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

View file

@ -171,9 +171,6 @@ function ClusterCard({
const handleLaunch = () => {
launch.mutate();
if (launch.error)
console.error(launch.error.message);
};
const image = () => {

View file

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

View file

@ -33,7 +33,7 @@ gui = []
workspace = true
[build-dependencies]
shadow-rs = { workspace = true, features = ["default"] }
shadow-rs = { workspace = true, features = [ "default" ] }
[dependencies]
# core

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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),
_ => {}

View file

@ -11,6 +11,7 @@ mod debug;
pub use debug::*;
pub trait TauRPCLauncherExt {
#[must_use]
fn use_launcher_api(self) -> Self;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
#![feature(cfg_select)]
mod entities;
mod domain;
mod entities;
pub use domain::*;
pub use entities::*;
pub use domain::*;

View file

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

View file

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

View file

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

View file

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