From 98aadb1f6a11218154ab5d76849783d0982389fc Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Fri, 28 Aug 2026 10:07:40 +0100 Subject: [PATCH 1/3] fix: ux changes for sync settings/overrides --- .../components/ui/screenshots-page/index.vue | 8 +- .../ui/settings/display/BehaviorSettings.vue | 58 +++- .../instances/InstancesSyncedSettings.vue | 176 +++++++++++-- apps/app-frontend/src/helpers/instance.ts | 2 + .../app-frontend/src/locales/en-US/index.json | 58 ++-- .../settings-modal/hooks-settings.vue | 118 +++++---- .../settings-modal/java-settings.vue | 248 ++++++++++-------- .../synced-options-settings.vue | 193 ++++++++------ .../settings-modal/window-settings.vue | 100 +++---- .../src/pages/instance/layout.vue | 4 +- apps/app/src/api/instance.rs | 8 +- ...828120000_reset-synced-option-defaults.sql | 15 ++ .../api/instance/screenshots/operations.rs | 9 +- .../instance/synced_options/orchestration.rs | 139 ++++++++-- .../api/instance/synced_servers/operations.rs | 43 ++- .../adapters/sqlite/instance_rows.rs | 26 -- packages/ui/src/components/base/Slider.vue | 216 ++++++++------- 17 files changed, 921 insertions(+), 500 deletions(-) create mode 100644 packages/app-lib/migrations/20260828120000_reset-synced-option-defaults.sql diff --git a/apps/app-frontend/src/components/ui/screenshots-page/index.vue b/apps/app-frontend/src/components/ui/screenshots-page/index.vue index 5a85ed6918..d845fe8b68 100644 --- a/apps/app-frontend/src/components/ui/screenshots-page/index.vue +++ b/apps/app-frontend/src/components/ui/screenshots-page/index.vue @@ -429,10 +429,10 @@ const groupedScreenshots = computed((): ScreenshotGroupData[] => { screenshotGroups.set(screenshot.instance_id, group) } - const syncedInstances = (instancesQuery.data.value ?? []) - .filter((instance) => instance.synced_options.screenshots) - .sort((a, b) => a.name.localeCompare(b.name)) - const groups = syncedInstances.flatMap((instance) => { + const instances = [...(instancesQuery.data.value ?? [])].sort((a, b) => + a.name.localeCompare(b.name), + ) + const groups = instances.flatMap((instance) => { const instanceScreenshots = screenshotGroups.get(instance.id) return instanceScreenshots ? [ diff --git a/apps/app-frontend/src/components/ui/settings/display/BehaviorSettings.vue b/apps/app-frontend/src/components/ui/settings/display/BehaviorSettings.vue index d52c4404a6..43ce220e5b 100644 --- a/apps/app-frontend/src/components/ui/settings/display/BehaviorSettings.vue +++ b/apps/app-frontend/src/components/ui/settings/display/BehaviorSettings.vue @@ -7,6 +7,7 @@ import { useSavable, useVIntl, } from '@modrinth/ui' +import { useQueryClient } from '@tanstack/vue-query' import { inject, onBeforeUnmount, onMounted, ref } from 'vue' import { @@ -14,7 +15,13 @@ import { type FeatureFlag, useAppSettings, } from '@/composables/use-app-settings.ts' +import { + get_global_synced_options, + type GlobalSyncedOptions, + set_global_synced_option, +} from '@/helpers/instance.ts' import { type AppSettings, get, set } from '@/helpers/settings.ts' +import { screenshotKeys } from '@/pages/instance/query-options.ts' import { appSettingsModalContextKey } from '@/providers/app-settings-modal' const appSettings = useAppSettings() @@ -22,6 +29,7 @@ const { formatMessage } = useVIntl() const auth = injectAuth() const { updatePreferences } = injectUserPreferences() const settingsModal = inject(appSettingsModalContextKey, null) +const queryClient = useQueryClient() const worldsInHomeFlag: FeatureFlag = 'worlds_in_home' const compactInstanceCardsFlag: FeatureFlag = 'compact_instance_cards' @@ -51,6 +59,14 @@ const messages = defineMessages({ id: 'app.behavior-settings.content.title', defaultMessage: 'Home and content', }, + showAllScreenshotsTitle: { + id: 'app.behavior-settings.show-all-screenshots.title', + defaultMessage: 'Show all screenshots together', + }, + showAllScreenshotsDescription: { + id: 'app.behavior-settings.show-all-screenshots.description', + defaultMessage: 'View screenshots from all your instances on the Screenshots page.', + }, confirmationsTitle: { id: 'app.behavior-settings.confirmations.title', defaultMessage: 'Confirmations', @@ -137,6 +153,7 @@ type BehaviorSettingsState = { minimizeApp: boolean hideRightSidebar: boolean showJumpIn: boolean + showAllScreenshots: boolean compactInstanceCards: boolean showPlayTime: boolean hideNametag: boolean @@ -144,14 +161,23 @@ type BehaviorSettingsState = { skipNonEssentialWarnings: boolean } -const persistedSettings = ref(await get()) +const [initialSettings, initialGlobalSyncedOptions] = await Promise.all([ + get(), + get_global_synced_options(), +]) +const persistedSettings = ref(initialSettings) +const persistedGlobalSyncedOptions = ref(initialGlobalSyncedOptions) -function getBehaviorSettingsState(settings: AppSettings): BehaviorSettingsState { +function getBehaviorSettingsState( + settings: AppSettings, + globalSyncedOptions: GlobalSyncedOptions, +): BehaviorSettingsState { return { syncBehaviorAcrossDevices: settings.sync_behavior_across_devices, minimizeApp: settings.hide_on_process_start, hideRightSidebar: settings.toggle_sidebar, showJumpIn: settings.feature_flags[worldsInHomeFlag] ?? DEFAULT_FEATURE_FLAGS[worldsInHomeFlag], + showAllScreenshots: globalSyncedOptions.screenshots, compactInstanceCards: settings.feature_flags[compactInstanceCardsFlag] ?? DEFAULT_FEATURE_FLAGS[compactInstanceCardsFlag], @@ -169,7 +195,7 @@ function getBehaviorSettingsState(settings: AppSettings): BehaviorSettingsState } const { saved, current, changes, saving, hasChanges, reset, save } = useSavable( - () => getBehaviorSettingsState(persistedSettings.value), + () => getBehaviorSettingsState(persistedSettings.value, persistedGlobalSyncedOptions.value), async () => { const value = current.value @@ -204,8 +230,20 @@ const { saved, current, changes, saving, hasChanges, reset, save } = useSavable( }, } - await set(nextSettings) + const screenshotsChanged = + value.showAllScreenshots !== persistedGlobalSyncedOptions.value.screenshots + const [, updatedGlobalSyncedOptions] = await Promise.all([ + set(nextSettings), + screenshotsChanged + ? set_global_synced_option('screenshots', value.showAllScreenshots) + : Promise.resolve(persistedGlobalSyncedOptions.value), + ]) persistedSettings.value = nextSettings + persistedGlobalSyncedOptions.value = updatedGlobalSyncedOptions + queryClient.setQueryData(['global-synced-options'], updatedGlobalSyncedOptions) + if (screenshotsChanged) { + await queryClient.invalidateQueries({ queryKey: screenshotKeys.all }) + } appSettings.setBehaviorSyncAcrossDevices(value.syncBehaviorAcrossDevices) appSettings.toggleSidebar = value.hideRightSidebar appSettings.hideNametagSkinsPage = value.hideNametag @@ -302,6 +340,18 @@ onBeforeUnmount(() => { {{ formatMessage(messages.contentTitle) }}
+
+
+

+ {{ formatMessage(messages.showAllScreenshotsTitle) }} +

+

+ {{ formatMessage(messages.showAllScreenshotsDescription) }} +

+
+ +
+

diff --git a/apps/app-frontend/src/components/ui/settings/instances/InstancesSyncedSettings.vue b/apps/app-frontend/src/components/ui/settings/instances/InstancesSyncedSettings.vue index ecef91748c..f96b836b53 100644 --- a/apps/app-frontend/src/components/ui/settings/instances/InstancesSyncedSettings.vue +++ b/apps/app-frontend/src/components/ui/settings/instances/InstancesSyncedSettings.vue @@ -2,11 +2,15 @@ import { EditIcon, // FolderOpenIcon, + RefreshCwIcon, SaveIcon, + SearchIcon, XIcon, } from '@modrinth/assets' import { + Avatar, Button, + CheckCircleButton, commonMessages, defineMessages, IconButton, @@ -26,7 +30,9 @@ import useMemorySlider from '@/composables/useMemorySlider' import { get_command_history, get_global_synced_options, + getInstanceIconUrl, type GlobalSyncedOptions, + list as listInstances, list_synced_servers, // open_synced_options_folder, remove_synced_server, @@ -43,7 +49,7 @@ import { type ServerData, type ServerWorld, } from '@/helpers/worlds.ts' -import { instanceKeys, screenshotKeys } from '@/pages/instance/query-options' +import { instanceKeys } from '@/pages/instance/query-options' const { handleError } = injectNotificationManager() const { formatMessage } = useVIntl() @@ -83,13 +89,33 @@ const messages = defineMessages({ id: 'app.settings.synced-options.creative-hotbars.description', defaultMessage: 'Sync saved creative hotbars across your instances.', }, - screenshots: { - id: 'app.settings.synced-options.screenshots', - defaultMessage: 'Screenshots', + chooseSyncSourceTitle: { + id: 'app.settings.synced-options.choose-sync-source.title', + defaultMessage: 'Choose a sync source', }, - screenshotsDescription: { - id: 'app.settings.synced-options.screenshots.description', - defaultMessage: 'View screenshots from your instances in one place.', + multiplayerServersSyncSourceDescription: { + id: 'app.settings.synced-options.choose-sync-source.multiplayer-servers-description', + defaultMessage: 'Pick the instance whose multiplayer servers become the shared copy.', + }, + commandHistorySyncSourceDescription: { + id: 'app.settings.synced-options.choose-sync-source.command-history-description', + defaultMessage: 'Pick the instance whose command history becomes the shared copy.', + }, + creativeHotbarsSyncSourceDescription: { + id: 'app.settings.synced-options.choose-sync-source.creative-hotbars-description', + defaultMessage: 'Pick the instance whose saved creative hotbars become the shared copy.', + }, + searchInstance: { + id: 'app.settings.synced-options.choose-sync-source.search-placeholder', + defaultMessage: 'Search instance', + }, + noInstancesFound: { + id: 'app.settings.synced-options.choose-sync-source.no-instances-found', + defaultMessage: 'No instances found', + }, + syncButton: { + id: 'app.settings.synced-options.choose-sync-source.sync', + defaultMessage: 'Sync', }, commandHistoryEditorTitle: { id: 'app.settings.synced-options.command-history.editor-title', @@ -285,11 +311,6 @@ const globalRows: Array<{ title: 'creativeHotbars', description: 'creativeHotbarsDescription', }, - { - option: 'screenshots', - title: 'screenshots', - description: 'screenshotsDescription', - }, ] const globalSyncedOptionsQueryKey = ['global-synced-options'] as const @@ -306,6 +327,11 @@ const globalOptionsQuery = useQuery({ queryFn: get_global_synced_options, }) const globalOptions = computed(() => globalOptionsQuery.data.value ?? defaultGlobalOptions) +const instances = ref(await listInstances().catch(() => [])) +const baseOption = ref(null) +const baseInstanceId = ref(instances.value[0]?.id ?? '') +const baseInstanceSearch = ref('') +const baseModal = ref | null>(null) const commandHistoryModal = ref | null>(null) const serverEditorModal = ref | null>(null) const editServerModal = ref | null>(null) @@ -340,24 +366,43 @@ const syncedServerCards = computed(() => })), ) +const baseInstanceDescription = computed(() => { + switch (baseOption.value) { + case 'multiplayer_servers': + return formatMessage(messages.multiplayerServersSyncSourceDescription) + case 'command_history': + return formatMessage(messages.commandHistorySyncSourceDescription) + case 'creative_hotbars': + return formatMessage(messages.creativeHotbarsSyncSourceDescription) + default: + return '' + } +}) + +const filteredBaseInstances = computed(() => { + const search = baseInstanceSearch.value.trim().toLowerCase() + if (!search) return instances.value + return instances.value.filter((instance) => instance.name.toLowerCase().includes(search)) +}) + async function invalidateSyncedOptions() { await Promise.all([ queryClient.invalidateQueries({ queryKey: instanceKeys.all }), queryClient.invalidateQueries({ queryKey: ['instance-synced-options'] }), queryClient.invalidateQueries({ queryKey: globalSyncedOptionsQueryKey }), - queryClient.invalidateQueries({ queryKey: screenshotKeys.all }), ]) } type GlobalOptionMutationVariables = { option: SyncedOption enabled: boolean + baseInstanceId?: string } const globalOptionMutation = useMutation({ mutationKey: globalSyncedOptionsMutationKey, - mutationFn: ({ option, enabled }: GlobalOptionMutationVariables) => - set_global_synced_option(option, enabled), + mutationFn: ({ option, enabled, baseInstanceId }: GlobalOptionMutationVariables) => + set_global_synced_option(option, enabled, baseInstanceId), onMutate: async ({ option, enabled }) => { await queryClient.cancelQueries({ queryKey: globalSyncedOptionsQueryKey }) const previous = globalOptions.value[option] @@ -376,6 +421,15 @@ const globalOptionMutation = useMutation({ })) handleError(error) }, + onSuccess: async (_options, { option, enabled }) => { + if (enabled) baseModal.value?.hide() + if (enabled && option === 'multiplayer_servers') { + syncedServers.value = await list_synced_servers().catch((error) => { + handleError(error) + return [] + }) + } + }, onSettled: async () => { if (queryClient.isMutating({ mutationKey: globalSyncedOptionsMutationKey }) === 1) { await invalidateSyncedOptions() @@ -383,12 +437,25 @@ const globalOptionMutation = useMutation({ }, }) -function applyGlobalOption(option: SyncedOption, enabled: boolean) { - globalOptionMutation.mutate({ option, enabled }) +function applyGlobalOption(option: SyncedOption, enabled: boolean, baseInstanceId?: string) { + globalOptionMutation.mutate({ option, enabled, baseInstanceId }) } function toggleGlobalOption(option: SyncedOption, enabled: boolean) { - applyGlobalOption(option, enabled) + if (!enabled) { + applyGlobalOption(option, false) + return + } + + baseOption.value = option + baseInstanceId.value = instances.value[0]?.id ?? '' + baseInstanceSearch.value = '' + baseModal.value?.show() +} + +function confirmBaseInstance() { + if (!baseOption.value || !baseInstanceId.value) return + applyGlobalOption(baseOption.value, true, baseInstanceId.value) } async function openCommandHistoryEditor() { @@ -502,6 +569,79 @@ watch( diff --git a/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue index 09e1f43b81..64402d3030 100644 --- a/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue +++ b/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue @@ -28,7 +28,6 @@ import { get, parseEnvVars, serializeEnvVars } from '@/helpers/settings.ts' import type { AppSettings } from '../../../../helpers/types' import { injectInstanceSettings } from './instance-settings-context' -import SettingsOptionsTransition from './settings-options-transition.vue' const { handleError } = injectNotificationManager() const { formatMessage } = useVIntl() @@ -42,11 +41,14 @@ const optimalJava = readonly(await get_optimal_jre_key(instance.value.id).catch( const overrideJavaInstall = ref(!!instance.value.java_path) const javaPath = ref(instance.value.java_path ?? optimalJava?.path ?? '') -const activePath = computed(() => (overrideJavaInstall.value ? javaPath.value : '')) +const activePath = computed(() => javaPath.value) +const javaTestPath = computed(() => (overrideJavaInstall.value ? javaPath.value : '')) watch(overrideJavaInstall, (enabled) => { if (enabled && !javaPath.value) { javaPath.value = optimalJava?.path ?? '' + } else if (!enabled) { + javaPath.value = optimalJava?.path ?? '' } }) @@ -57,7 +59,7 @@ const hoveringTest = ref(false) let hasInitialized = false watch( - activePath, + javaTestPath, (newPath) => { if (newPath && optimalJava?.parsed_version) { if (!hasInitialized) { @@ -95,12 +97,30 @@ const envVars = ref( ) const overrideMemorySettings = ref(!!instance.value.memory) -const memory = ref(instance.value.memory ?? globalSettings.memory) +const memory = ref(instance.value.memory ?? { ...globalSettings.memory }) const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) as unknown as { maxMemory: number snapPoints: number[] } +watch(overrideJavaArgs, (enabled) => { + if (!enabled) { + javaArgs.value = globalSettings.extra_launch_args.join(' ') + } +}) + +watch(overrideEnvVars, (enabled) => { + if (!enabled) { + envVars.value = serializeEnvVars(globalSettings.custom_env_vars) + } +}) + +watch(overrideMemorySettings, (enabled) => { + if (!enabled) { + memory.value = { ...globalSettings.memory } + } +}) + const editInstanceObject = computed(() => { return { java_path: @@ -198,90 +218,89 @@ const messages = defineMessages({

- -
-
-
-
+
+
+
+ +
+
+ Java {{ optimalJava?.parsed_version }} - -
-
- Java {{ optimalJava?.parsed_version }} -
- - -
-
- - -
+ : 'standard') && + (overrideJavaInstall && !hoveringTest && !testingJava + ? javaTestResult === true + ? 'green' + : 'red' + : 'standard') !== 'standard' + ? `var(--color-${ + overrideJavaInstall && !hoveringTest && !testingJava + ? javaTestResult === true + ? 'green' + : 'red' + : 'standard' + })` + : undefined, + }" + class="!text-[var(--legacy-button-color,var(--color-base))] [&>svg]:!text-[var(--legacy-button-color,var(--color-primary))]" + @click="testJavaInstallation(activePath, optimalJava?.parsed_version, true)" + @mouseenter="hoveringTest = true" + @mouseleave="hoveringTest = false" + > + + + + + +
+
+ +
- +
@@ -294,20 +313,19 @@ const messages = defineMessages({
- -
- -
-
+
+ +
@@ -320,17 +338,16 @@ const messages = defineMessages({
- -
- -
-
+
+ +
@@ -343,17 +360,16 @@ const messages = defineMessages({ - -
- -
-
+
+ +
diff --git a/apps/app-frontend/src/pages/instance/components/settings-modal/synced-options-settings.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/synced-options-settings.vue index f4458d87bc..2c61cc712a 100644 --- a/apps/app-frontend/src/pages/instance/components/settings-modal/synced-options-settings.vue +++ b/apps/app-frontend/src/pages/instance/components/settings-modal/synced-options-settings.vue @@ -1,11 +1,5 @@ From 1a560c64c7ddb0666a211d071d0dad4f17996aad Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Fri, 28 Aug 2026 12:04:52 +0100 Subject: [PATCH 2/3] fix: screenshots page lag + better virtualization pass --- .../components/ui/screenshots-page/card.vue | 119 ++++++++++-- .../components/ui/screenshots-page/group.vue | 76 ++++++-- .../components/ui/screenshots-page/index.vue | 166 +++++++++++------ .../ui/screenshots-page/section.vue | 37 +++- apps/app/src/api/instance.rs | 30 ++- .../instance/screenshots/reconciliation.rs | 173 ++++++++---------- 6 files changed, 413 insertions(+), 188 deletions(-) diff --git a/apps/app-frontend/src/components/ui/screenshots-page/card.vue b/apps/app-frontend/src/components/ui/screenshots-page/card.vue index adf1222ef8..f1de4c6532 100644 --- a/apps/app-frontend/src/components/ui/screenshots-page/card.vue +++ b/apps/app-frontend/src/components/ui/screenshots-page/card.vue @@ -3,11 +3,16 @@ @@ -91,7 +166,7 @@ watch( ref="card" role="button" tabindex="0" - class="group relative aspect-video min-w-0 cursor-pointer overflow-hidden rounded-xl border border-solid border-surface-5 bg-surface-2 p-0 text-left shadow-sm transition-[filter] hover:brightness-110 focus-visible:outline focus-visible:outline-2 focus-visible:outline-brand" + class="group relative isolate aspect-video min-w-0 cursor-pointer overflow-hidden rounded-xl border border-solid border-surface-5 bg-surface-2 p-0 text-left shadow-sm transition-[filter] hover:brightness-110 focus-visible:outline focus-visible:outline-2 focus-visible:outline-brand" :class="{ '!border-contrast brightness-110': selected, '!border-brand ring-2 ring-brand animate-pulse': highlighted, @@ -115,7 +190,7 @@ watch( > -