diff --git a/apps/app-frontend/src/helpers/types.d.ts b/apps/app-frontend/src/helpers/types.d.ts index 646a0b47d0..0bd0101461 100644 --- a/apps/app-frontend/src/helpers/types.d.ts +++ b/apps/app-frontend/src/helpers/types.d.ts @@ -97,6 +97,7 @@ type ReleaseChannel = 'release' | 'beta' | 'alpha' export type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge' type ContentFile = { + enabled: boolean metadata?: { project_id: string version_id: string diff --git a/packages/app-lib/.sqlx/query-c5f78eaae528b6f930bcc9ade15384564c86cd98a1e407515ea718127a3b1777.json b/packages/app-lib/.sqlx/query-c5f78eaae528b6f930bcc9ade15384564c86cd98a1e407515ea718127a3b1777.json new file mode 100644 index 0000000000..97e1f9d045 --- /dev/null +++ b/packages/app-lib/.sqlx/query-c5f78eaae528b6f930bcc9ade15384564c86cd98a1e407515ea718127a3b1777.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n\t\tUPDATE instance_files\n\t\tSET\n\t\t\trelative_path = ?,\n\t\t\tfile_name = ?,\n\t\t\tenabled = ?,\n\t\t\tmissing = 0,\n\t\t\tmodified_at = ?\n\t\tWHERE instance_id = ? AND relative_path = ?\n\t\t", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "c5f78eaae528b6f930bcc9ade15384564c86cd98a1e407515ea718127a3b1777" +} diff --git a/packages/app-lib/.sqlx/query-f93f5fca4f004dc70cab05f879993eb92fa6199c7356bbc2c5ca36468f916380.json b/packages/app-lib/.sqlx/query-f93f5fca4f004dc70cab05f879993eb92fa6199c7356bbc2c5ca36468f916380.json deleted file mode 100644 index c28f2d9854..0000000000 --- a/packages/app-lib/.sqlx/query-f93f5fca4f004dc70cab05f879993eb92fa6199c7356bbc2c5ca36468f916380.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n\t\tUPDATE instance_files\n\t\tSET\n\t\t\trelative_path = ?,\n\t\t\tfile_name = ?,\n\t\t\tenabled = ?,\n\t\t\tmodified_at = ?\n\t\tWHERE instance_id = ? AND relative_path = ?\n\t\t", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "f93f5fca4f004dc70cab05f879993eb92fa6199c7356bbc2c5ca36468f916380" -} diff --git a/packages/app-lib/src/state/instance_types.rs b/packages/app-lib/src/state/instance_types.rs index 8db72d4525..4fefab25a1 100644 --- a/packages/app-lib/src/state/instance_types.rs +++ b/packages/app-lib/src/state/instance_types.rs @@ -117,6 +117,7 @@ impl ModLoader { pub struct ContentFile { pub hash: String, pub file_name: String, + pub enabled: bool, pub size: u64, pub metadata: Option, pub update_version_id: Option, diff --git a/packages/app-lib/src/state/instances/adapters/sqlite/content_rows.rs b/packages/app-lib/src/state/instances/adapters/sqlite/content_rows.rs index 58e3071968..e8b6e108a5 100644 --- a/packages/app-lib/src/state/instances/adapters/sqlite/content_rows.rs +++ b/packages/app-lib/src/state/instances/adapters/sqlite/content_rows.rs @@ -627,6 +627,7 @@ pub(crate) async fn rename_instance_file( relative_path = ?, file_name = ?, enabled = ?, + missing = 0, modified_at = ? WHERE instance_id = ? AND relative_path = ? ", @@ -856,11 +857,11 @@ pub(crate) async fn set_content_entry_enabled_for_file( file_id: &str, enabled: bool, pool: &SqlitePool, -) -> crate::Result<()> { +) -> crate::Result { let enabled = i64::from(enabled); let modified_at = Utc::now().timestamp(); - sqlx::query!( + let result = sqlx::query!( " UPDATE instance_content_entries SET enabled = ?, modified_at = ? @@ -874,7 +875,7 @@ pub(crate) async fn set_content_entry_enabled_for_file( .execute(pool) .await?; - Ok(()) + Ok(result.rows_affected() > 0) } pub(crate) async fn remove_content_entries_for_file( diff --git a/packages/app-lib/src/state/instances/commands/apply_content_install.rs b/packages/app-lib/src/state/instances/commands/apply_content_install.rs index 20e4e16101..0c780c7e76 100644 --- a/packages/app-lib/src/state/instances/commands/apply_content_install.rs +++ b/packages/app-lib/src/state/instances/commands/apply_content_install.rs @@ -661,13 +661,31 @@ pub(crate) async fn toggle_disable_project( } None => index_existing_file(&scope, &new_path, state).await?, }; - content_rows::set_content_entry_enabled_for_file( + let updated_entry = content_rows::set_content_entry_enabled_for_file( &scope.content_set_id, &file.id, enabled, &state.pool, ) .await?; + if !updated_entry { + let project_type = ProjectType::get_from_parent_folder(&new_path) + .ok_or_else(|| { + crate::ErrorKind::InputError(format!( + "Unable to infer project type from {new_path}" + )) + })?; + upsert_entry_for_file( + &scope, + &file, + project_type, + None, + None, + ContentSourceKind::Local, + &state.pool, + ) + .await?; + } Ok(new_path) } diff --git a/packages/app-lib/src/state/instances/commands/apply_content_update.rs b/packages/app-lib/src/state/instances/commands/apply_content_update.rs index 42bca46c1b..e89d5b5378 100644 --- a/packages/app-lib/src/state/instances/commands/apply_content_update.rs +++ b/packages/app-lib/src/state/instances/commands/apply_content_update.rs @@ -281,12 +281,31 @@ async fn plan_bulk_update( instance_id: &str, state: &State, ) -> crate::Result { + let updateable_paths = + bulk_updateable_project_paths(instance_id, state).await?; + if updateable_paths.is_empty() { + return Ok(BulkUpdatePlan { + project_updates: Vec::new(), + dependency_additions: Vec::new(), + }); + } + let updates = check_content_updates( instance_id, Some(CacheBehaviour::MustRevalidate), state, ) - .await?; + .await? + .into_iter() + .filter(|update| updateable_paths.contains(&update.relative_path)) + .collect::>(); + if updates.is_empty() { + return Ok(BulkUpdatePlan { + project_updates: Vec::new(), + dependency_additions: Vec::new(), + }); + } + let content_set = content_rows::get_applied_content_set(instance_id, &state.pool) .await? @@ -320,6 +339,7 @@ async fn plan_bulk_update( .collect::>(); let version_ids = installed .iter() + .filter(|project| updateable_paths.contains(&project.relative_path)) .filter_map(|project| project.version_id.clone()) .chain( updates @@ -343,6 +363,7 @@ async fn plan_bulk_update( let planned_versions = installed .iter() .filter(|project| project.enabled) + .filter(|project| updateable_paths.contains(&project.relative_path)) .filter_map(|project| { let target_version_id = updates_by_path .get(&project.relative_path) @@ -379,6 +400,21 @@ async fn plan_bulk_update( }) } +async fn bulk_updateable_project_paths( + instance_id: &str, + state: &State, +) -> crate::Result> { + let items = super::list_content::list_content( + instance_id, + None, + Some(CacheBehaviour::MustRevalidate), + state, + ) + .await?; + + Ok(items.into_iter().map(|item| item.file_path).collect()) +} + async fn installed_projects( instance_id: &str, content_set: &ContentSet, diff --git a/packages/app-lib/src/state/instances/commands/list_content.rs b/packages/app-lib/src/state/instances/commands/list_content.rs index cd58d74902..529efff676 100644 --- a/packages/app-lib/src/state/instances/commands/list_content.rs +++ b/packages/app-lib/src/state/instances/commands/list_content.rs @@ -208,6 +208,8 @@ pub(crate) async fn list_content( ) .await?; let imported_modpack_scope = is_imported_modpack_scope(&resolved, &link); + let linked_modpack_source_kind = linked_modpack_source_kind(&link); + let mut failed_modpack_identifier_lookup = false; let modpack_ids = if imported_modpack_scope { None } else { @@ -226,6 +228,7 @@ pub(crate) async fn list_content( "Failed to fetch modpack identifiers: {}", err ); + failed_modpack_identifier_lookup = true; None } }, @@ -240,6 +243,12 @@ pub(crate) async fn list_content( } } else if let Some(ids) = modpack_ids.as_ref() { ContentFilter::ExcludeModpack(ids) + } else if failed_modpack_identifier_lookup { + ContentFilter::ExcludeSourceKind { + source_kind: linked_modpack_source_kind + .unwrap_or(ContentSourceKind::ModrinthModpack), + exclude_untracked: true, + } } else { ContentFilter::All }; @@ -735,6 +744,9 @@ async fn content_projects_for_scope( update_version_id, hash: file.sha1, file_name: file.file_name, + enabled: entry.map_or(file.enabled, |entry| { + entry.enabled && file.enabled + }), size: file.size, metadata: file_metadata_from_entry_or_cache(entry, metadata), project_type, @@ -882,7 +894,7 @@ async fn content_files_to_content_items( file_path: path.clone(), id: file.hash.clone(), size: file.size, - enabled: !file.file_name.ends_with(".disabled"), + enabled: file.enabled, project_type: file.project_type, project: project.map(|project| ContentItemProject { id: project.id.clone(), @@ -1097,6 +1109,20 @@ fn linked_modpack_ids(link: &InstanceLink) -> Option<(String, String)> { } } +fn linked_modpack_source_kind( + link: &InstanceLink, +) -> Option { + match link { + InstanceLink::ModrinthModpack { .. } => { + Some(ContentSourceKind::ModrinthModpack) + } + InstanceLink::ServerProjectModpack { .. } => { + Some(ContentSourceKind::ServerProject) + } + _ => None, + } +} + fn check_modpack_update( installed_version_id: &str, installed_version: &Version, diff --git a/packages/app-lib/src/state/instances/commands/sync_content_files.rs b/packages/app-lib/src/state/instances/commands/sync_content_files.rs index 4dada1c645..3686558393 100644 --- a/packages/app-lib/src/state/instances/commands/sync_content_files.rs +++ b/packages/app-lib/src/state/instances/commands/sync_content_files.rs @@ -39,9 +39,18 @@ pub(crate) async fn sync_instance_content_files( &state.api_semaphore, ) .await?; - let hashes_by_path = hashes + let hashes_by_key = hashes .into_iter() - .map(|hash| (hash.path.clone(), hash)) + .map(|hash| { + ( + format!( + "{}-{}", + hash.size, + hash.path.trim_end_matches(".disabled") + ), + hash, + ) + }) .collect::>(); let existing_files = sqlite::content_rows::get_instance_files(&instance.id, &state.pool) @@ -55,8 +64,8 @@ pub(crate) async fn sync_instance_content_files( let mut files = Vec::new(); for file in scanned { - let cache_path = format!("{}/{}", instance.path, file.relative_path); - let Some(hash) = hashes_by_path.get(&cache_path) else { + let hash_key = file.hash_cache_key.trim_end_matches(".disabled"); + let Some(hash) = hashes_by_key.get(hash_key) else { continue; }; let existing_file = existing_files_by_path.get(&file.relative_path);