From 39c83b95ad610c3c0f590cbaf05ff939ad35bca0 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 9 Nov 2026 17:59:43 +0000 Subject: [PATCH 1/4] fix: quick instance switcher count being overwritten every time --- .../components/ui/QuickInstanceSwitcher.vue | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue b/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue index 256a0d0121..3375971ac9 100644 --- a/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue +++ b/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue @@ -5,7 +5,9 @@ import { commonMessages, ContextMenu, defineMessages, + injectAuth, injectNotificationManager, + injectUserPreferences, useVIntl, } from '@modrinth/ui' import { useQuery } from '@tanstack/vue-query' @@ -15,6 +17,7 @@ import { useRouter } from 'vue-router' import NavButton from '@/components/ui/NavButton.vue' import { useAppEvent } from '@/composables/use-app-event' +import { useAppSettings } from '@/composables/use-app-settings.ts' import { handleSevereError } from '@/composables/use-error.js' import { QUICK_INSTANCE_LIMIT_MAX, @@ -29,6 +32,9 @@ import { instanceListQueryOptions } from '@/pages/instance/query-options' const ITEM_SIZE = 52 const { handleError } = injectNotificationManager() +const auth = injectAuth() +const { updatePreferences } = injectUserPreferences() +const appSettings = useAppSettings() const instancesQuery = useQuery(instanceListQueryOptions()) const router = useRouter() const instanceOptions = ref() @@ -97,6 +103,17 @@ const setLimit = (count) => { } } +const syncLimitToAccount = () => { + if (!appSettings.syncFeaturesAcrossDevices || !auth.user.value) { + return + } + updatePreferences({ + behavior: { + quick_instance_count: quickInstances.limit.value ?? QUICK_INSTANCE_LIMIT_MAX, + }, + }).catch(() => undefined) +} + const nudgeLimit = (delta) => { if (!canDrag.value) { return @@ -106,6 +123,7 @@ const nudgeLimit = (delta) => { flashOverdrag() } setLimit(target) + syncLimitToAccount() } let dragStartY = 0 @@ -171,6 +189,9 @@ const endDrag = (event) => { if (event?.currentTarget?.hasPointerCapture?.(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId) } + if (visibleCount.value !== dragStartCount) { + syncLimitToAccount() + } } const onDividerPointerUp = (event) => { From cf1c287aefafcdfb1a8c55d13f14fe047ed421f9 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 9 Nov 2026 18:04:41 +0000 Subject: [PATCH 2/4] fix: rev --- .../components/ui/QuickInstanceSwitcher.vue | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue b/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue index 3375971ac9..6d41adb369 100644 --- a/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue +++ b/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue @@ -31,6 +31,7 @@ import { showInstanceInFolder } from '@/helpers/utils' import { instanceListQueryOptions } from '@/pages/instance/query-options' const ITEM_SIZE = 52 +const SYNC_DEBOUNCE_MS = 1000 const { handleError } = injectNotificationManager() const auth = injectAuth() const { updatePreferences } = injectUserPreferences() @@ -114,6 +115,30 @@ const syncLimitToAccount = () => { }).catch(() => undefined) } +let syncTimeout = null + +const cancelScheduledSync = () => { + if (syncTimeout !== null) { + clearTimeout(syncTimeout) + syncTimeout = null + } +} + +const scheduleSync = () => { + cancelScheduledSync() + syncTimeout = setTimeout(() => { + syncTimeout = null + syncLimitToAccount() + }, SYNC_DEBOUNCE_MS) +} + +const flushScheduledSync = () => { + if (syncTimeout !== null) { + cancelScheduledSync() + syncLimitToAccount() + } +} + const nudgeLimit = (delta) => { if (!canDrag.value) { return @@ -122,12 +147,17 @@ const nudgeLimit = (delta) => { if (target < 0 || target > maxVisible.value) { flashOverdrag() } + const previousLimit = quickInstances.limit.value setLimit(target) - syncLimitToAccount() + if (quickInstances.limit.value !== previousLimit) { + scheduleSync() + } } let dragStartY = 0 let dragStartCount = 0 +let dragStartLimit = null +let dragHadPendingSync = false let wasOverdragging = false let overdragTimeout = null @@ -160,6 +190,9 @@ const onDividerPointerDown = (event) => { clearOverdragFlash() dragStartY = event.clientY dragStartCount = visibleCount.value + dragStartLimit = quickInstances.limit.value + dragHadPendingSync = syncTimeout !== null + cancelScheduledSync() document.body.classList.add('quick-instance-dragging') event.currentTarget.setPointerCapture(event.pointerId) } @@ -189,7 +222,7 @@ const endDrag = (event) => { if (event?.currentTarget?.hasPointerCapture?.(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId) } - if (visibleCount.value !== dragStartCount) { + if (dragHadPendingSync || quickInstances.limit.value !== dragStartLimit) { syncLimitToAccount() } } @@ -212,6 +245,7 @@ onMounted(() => { onUnmounted(() => { resizeObserver?.disconnect() + flushScheduledSync() document.body.classList.remove('quick-instance-dragging') clearOverdragFlash() }) From 9dcf9c3aab3e98883704686b06869f6dbbb786cf Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 9 Nov 2026 18:49:56 +0000 Subject: [PATCH 3/4] fix: bulk update regression duplicates --- .../commands/apply_content_update.rs | 56 ++++++++++++++++--- .../instances/commands/content_mutation.rs | 4 +- 2 files changed, 52 insertions(+), 8 deletions(-) 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 f890015030..ea92f819d8 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 @@ -186,7 +186,23 @@ pub(crate) async fn apply_bulk_update( .await?; match download { DownloadedBulkProject::ProjectUpdate(update, downloaded) => { + let mut enabled = + content_rows::get_instance_file_by_relative_path( + instance_id, + &update.relative_path, + &state.pool, + ) + .await? + .is_none_or(|file| file.enabled); for path in &update.duplicate_paths { + enabled |= + content_rows::get_instance_file_by_relative_path( + instance_id, + path, + &state.pool, + ) + .await? + .is_some_and(|file| file.enabled); super::content_mutation::remove_project( instance_id, path, @@ -194,13 +210,6 @@ pub(crate) async fn apply_bulk_update( ) .await?; } - let enabled = content_rows::get_instance_file_by_relative_path( - instance_id, - &update.relative_path, - &state.pool, - ) - .await? - .is_none_or(|file| file.enabled); let new_path = add_downloaded_project_version_with_enabled( instance_id, downloaded, @@ -532,6 +541,14 @@ pub(crate) async fn plan_bulk_update( .insert(update.project_id.clone(), (update, Vec::new())); } } + for (update, duplicate_paths) in updates_by_project.values_mut() { + adopt_current_copy( + update, + duplicate_paths, + &installed, + &updateable_paths, + ); + } let updates_by_path = updates_by_project .values() .map(|(update, _)| { @@ -591,6 +608,31 @@ pub(crate) async fn plan_bulk_update( }) } +fn adopt_current_copy( + update: &mut ContentUpdate, + duplicate_paths: &mut Vec, + installed: &[InstalledProject], + updateable_paths: &HashSet, +) { + let Some(current) = installed.iter().find(|project| { + project.project_id.as_ref() == Some(&update.project_id) + && project.version_id.as_ref() == Some(&update.update_version_id) + && project.relative_path != update.relative_path + && !duplicate_paths.contains(&project.relative_path) + && updateable_paths.contains(&project.relative_path) + }) else { + return; + }; + + duplicate_paths.push(std::mem::replace( + &mut update.relative_path, + current.relative_path.clone(), + )); + update + .current_version_id + .clone_from(&update.update_version_id); +} + async fn bulk_updateable_project_paths( instance_id: &str, shared_instance_member: bool, diff --git a/packages/app-lib/src/state/instances/commands/content_mutation.rs b/packages/app-lib/src/state/instances/commands/content_mutation.rs index 72324cdcc1..fa5e977d94 100644 --- a/packages/app-lib/src/state/instances/commands/content_mutation.rs +++ b/packages/app-lib/src/state/instances/commands/content_mutation.rs @@ -233,7 +233,9 @@ impl<'a> InstanceContent<'a> { )); } let enabled = if request.previous_path.is_some() { - existing.as_ref().is_some_and(|file| file.enabled) + request.enabled_override.unwrap_or_else(|| { + existing.as_ref().is_some_and(|file| file.enabled) + }) } else { request.enabled_override.unwrap_or_else(|| { !request.requested_path.ends_with(".disabled") From 4d5b0edae5058b419591ead0f8e37067c69ff9bd Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 9 Nov 2026 18:57:37 +0000 Subject: [PATCH 4/4] fix: rev --- .../commands/apply_content_update.rs | 55 ++++++++----------- 1 file changed, 22 insertions(+), 33 deletions(-) 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 ea92f819d8..e269803d02 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 @@ -117,13 +117,6 @@ async fn apply_content_update( update: &ContentUpdate, state: &State, ) -> crate::Result { - let enabled = content_rows::get_instance_file_by_relative_path( - instance_id, - project_path, - &state.pool, - ) - .await? - .is_none_or(|file| file.enabled); let downloaded = download_project_version( instance_id, &update.update_version_id, @@ -139,7 +132,7 @@ async fn apply_content_update( instance_id, downloaded, ContentSourceKind::Local, - Some(enabled), + None, Some(project_path), state, ) @@ -549,25 +542,14 @@ pub(crate) async fn plan_bulk_update( &updateable_paths, ); } - let updates_by_path = updates_by_project + let planned_versions = updates_by_project .values() - .map(|(update, _)| { - ( - update.relative_path.clone(), - update.update_version_id.clone(), - ) + .filter(|(update, duplicate_paths)| { + is_enabled(&update.relative_path) + || duplicate_paths.iter().any(|path| is_enabled(path)) }) - .collect::>(); - let planned_versions = installed - .iter() - .filter(|project| project.enabled) - .filter(|project| updates_by_path.contains_key(&project.relative_path)) - .filter_map(|project| { - let target_version_id = updates_by_path - .get(&project.relative_path) - .or(project.version_id.as_ref())?; - - versions_by_id.get(target_version_id).cloned() + .filter_map(|(update, _)| { + versions_by_id.get(&update.update_version_id).cloned() }) .collect::>(); let planned_dependencies = @@ -623,14 +605,21 @@ fn adopt_current_copy( }) else { return; }; - - duplicate_paths.push(std::mem::replace( - &mut update.relative_path, - current.relative_path.clone(), - )); - update - .current_version_id - .clone_from(&update.update_version_id); + let selected_enabled = installed.iter().any(|project| { + project.relative_path == update.relative_path && project.enabled + }); + + if current.enabled && !selected_enabled { + duplicate_paths.push(std::mem::replace( + &mut update.relative_path, + current.relative_path.clone(), + )); + update + .current_version_id + .clone_from(&update.update_version_id); + } else { + duplicate_paths.push(current.relative_path.clone()); + } } async fn bulk_updateable_project_paths(