diff --git a/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue b/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue index 256a0d0121..6d41adb369 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, @@ -28,7 +31,11 @@ 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() +const appSettings = useAppSettings() const instancesQuery = useQuery(instanceListQueryOptions()) const router = useRouter() const instanceOptions = ref() @@ -97,6 +104,41 @@ 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) +} + +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 @@ -105,11 +147,17 @@ const nudgeLimit = (delta) => { if (target < 0 || target > maxVisible.value) { flashOverdrag() } + const previousLimit = quickInstances.limit.value setLimit(target) + if (quickInstances.limit.value !== previousLimit) { + scheduleSync() + } } let dragStartY = 0 let dragStartCount = 0 +let dragStartLimit = null +let dragHadPendingSync = false let wasOverdragging = false let overdragTimeout = null @@ -142,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) } @@ -171,6 +222,9 @@ const endDrag = (event) => { if (event?.currentTarget?.hasPointerCapture?.(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId) } + if (dragHadPendingSync || quickInstances.limit.value !== dragStartLimit) { + syncLimitToAccount() + } } const onDividerPointerUp = (event) => { @@ -191,6 +245,7 @@ onMounted(() => { onUnmounted(() => { resizeObserver?.disconnect() + flushScheduledSync() document.body.classList.remove('quick-instance-dragging') clearOverdragFlash() }) diff --git a/packages/app-lib/src/state/cache.rs b/packages/app-lib/src/state/cache.rs index da0c73a39f..43958ec282 100644 --- a/packages/app-lib/src/state/cache.rs +++ b/packages/app-lib/src/state/cache.rs @@ -443,18 +443,25 @@ pub struct KnownModrinthFile<'a> { pub version_id: &'a str, } +/// Returns the file's modification time as signed nanoseconds since the Unix +/// epoch, reinterpreted as `u64` so pre-1970 timestamps stay distinct from one +/// another and cast back to the correct negative `i64`. pub(crate) fn file_modified_at_ns( metadata: &std::fs::Metadata, ) -> std::io::Result { - let elapsed = metadata + let signed_ns = match metadata .modified()? .duration_since(std::time::UNIX_EPOCH) - .map_err(std::io::Error::other)?; + { + Ok(since_epoch) => { + i64::try_from(since_epoch.as_nanos()).unwrap_or(i64::MAX) + } + Err(before_epoch) => i64::try_from(before_epoch.duration().as_nanos()) + .map(|ns| -ns) + .unwrap_or(i64::MIN), + }; - Ok(elapsed - .as_secs() - .saturating_mul(1_000_000_000) - .saturating_add(u64::from(elapsed.subsec_nanos()))) + Ok(signed_ns as u64) } pub(crate) fn file_hash_cache_key(