) {
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
- const search = ref('')
+ const search = librarySearch
const filters = useStorage
(
'Instances-grid-filters',
{
diff --git a/apps/app-frontend/src/components/ui/library/view-state.ts b/apps/app-frontend/src/components/ui/library/view-state.ts
new file mode 100644
index 0000000000..83262ae6df
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/library/view-state.ts
@@ -0,0 +1,4 @@
+import { ref } from 'vue'
+
+export const librarySearch = ref('')
+export const libraryScrollTop = ref(0)
diff --git a/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue b/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue
index 78b58fc443..dd2218b22e 100644
--- a/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue
+++ b/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue
@@ -4,6 +4,7 @@ import {
GaugeIcon,
HeartHandshakeIcon,
LanguagesIcon,
+ LightBulbIcon,
ModrinthIcon,
PaintbrushIcon,
RefreshCwIcon,
@@ -24,18 +25,8 @@ import {
} from '@modrinth/ui'
import { getVersion } from '@tauri-apps/api/app'
import { platform as getOsPlatform, version as getOsVersion } from '@tauri-apps/plugin-os'
-import { computed, provide, ref, watch } from 'vue'
+import { computed, defineAsyncComponent, provide, ref, watch } from 'vue'
-import PrivacySettings from '@/components/ui/settings/account/PrivacySettings.vue'
-import ProfileSettings from '@/components/ui/settings/account/ProfileSettings.vue'
-import SocialSettings from '@/components/ui/settings/account/SocialSettings.vue'
-import AppearanceSettings from '@/components/ui/settings/display/AppearanceSettings.vue'
-import BehaviorSettings from '@/components/ui/settings/display/BehaviorSettings.vue'
-import FeatureFlagSettings from '@/components/ui/settings/display/FeatureFlagSettings.vue'
-import LanguageSettings from '@/components/ui/settings/display/LanguageSettings.vue'
-import InstancesSyncedSettings from '@/components/ui/settings/instances/InstancesSyncedSettings.vue'
-import JavaSettings from '@/components/ui/settings/instances/JavaSettings.vue'
-import ResourceManagementSettings from '@/components/ui/settings/instances/ResourceManagementSettings.vue'
import { useAppSettings } from '@/composables/use-app-settings.ts'
import { get, set } from '@/helpers/settings.ts'
import {
@@ -44,6 +35,40 @@ import {
} from '@/providers/app-settings-modal'
import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts'
+const PrivacySettings = defineAsyncComponent(
+ () => import('@/components/ui/settings/account/PrivacySettings.vue'),
+)
+const ProfileSettings = defineAsyncComponent(
+ () => import('@/components/ui/settings/account/ProfileSettings.vue'),
+)
+const SocialSettings = defineAsyncComponent(
+ () => import('@/components/ui/settings/account/SocialSettings.vue'),
+)
+const AppearanceSettings = defineAsyncComponent(
+ () => import('@/components/ui/settings/display/AppearanceSettings.vue'),
+)
+const BehaviorSettings = defineAsyncComponent(
+ () => import('@/components/ui/settings/display/BehaviorSettings.vue'),
+)
+const FeatureFlagSettings = defineAsyncComponent(
+ () => import('@/components/ui/settings/display/FeatureFlagSettings.vue'),
+)
+const FeaturesSettings = defineAsyncComponent(
+ () => import('@/components/ui/settings/display/FeaturesSettings.vue'),
+)
+const LanguageSettings = defineAsyncComponent(
+ () => import('@/components/ui/settings/display/LanguageSettings.vue'),
+)
+const InstancesSyncedSettings = defineAsyncComponent(
+ () => import('@/components/ui/settings/instances/instances-synced-settings/index.vue'),
+)
+const JavaSettings = defineAsyncComponent(
+ () => import('@/components/ui/settings/instances/JavaSettings.vue'),
+)
+const ResourceManagementSettings = defineAsyncComponent(
+ () => import('@/components/ui/settings/instances/ResourceManagementSettings.vue'),
+)
+
// TODO: Apply COMPONENT_STRUCTURE.md here and extract out common setting option components
const appSettings = useAppSettings()
@@ -81,6 +106,15 @@ const tabs = [
icon: PaintbrushIcon,
content: AppearanceSettings,
},
+ {
+ name: defineMessage({
+ id: 'app.settings.tabs.features',
+ defaultMessage: 'Features',
+ }),
+ category: tabCategories.display,
+ icon: LightBulbIcon,
+ content: FeaturesSettings,
+ },
{
name: defineMessage({
id: 'app.settings.tabs.behavior',
diff --git a/apps/app-frontend/src/components/ui/new-icon-editor-notification/apply-new-icons-modal.vue b/apps/app-frontend/src/components/ui/new-icon-editor-notification/apply-new-icons-modal.vue
deleted file mode 100644
index 91c4d02a60..0000000000
--- a/apps/app-frontend/src/components/ui/new-icon-editor-notification/apply-new-icons-modal.vue
+++ /dev/null
@@ -1,233 +0,0 @@
-
-
-
-
-
-
-
- {{ formatMessage(messages.badge) }}
-
-
-
-
- {{ formatMessage(messages.title) }}
-
-
- {{ formatMessage(messages.description) }}
-
-
- {{
- formatMessage(messages.instancesWithoutIcons, {
- count: iconlessInstanceIds.length,
- })
- }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/01.png b/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/01.png
deleted file mode 100644
index f996c89972..0000000000
Binary files a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/01.png and /dev/null differ
diff --git a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/02.png b/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/02.png
deleted file mode 100644
index 371efe55ad..0000000000
Binary files a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/02.png and /dev/null differ
diff --git a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/03.png b/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/03.png
deleted file mode 100644
index 39b88132b5..0000000000
Binary files a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/03.png and /dev/null differ
diff --git a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/04.png b/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/04.png
deleted file mode 100644
index c87cd33b19..0000000000
Binary files a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/04.png and /dev/null differ
diff --git a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/05.png b/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/05.png
deleted file mode 100644
index 4374b9ca3e..0000000000
Binary files a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/05.png and /dev/null differ
diff --git a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/06.png b/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/06.png
deleted file mode 100644
index 6dc6879b1d..0000000000
Binary files a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/06.png and /dev/null differ
diff --git a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/07.png b/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/07.png
deleted file mode 100644
index f9b9b636d0..0000000000
Binary files a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/07.png and /dev/null differ
diff --git a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/08.png b/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/08.png
deleted file mode 100644
index 8359a83917..0000000000
Binary files a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/08.png and /dev/null differ
diff --git a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/09.png b/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/09.png
deleted file mode 100644
index 51768ef23f..0000000000
Binary files a/apps/app-frontend/src/components/ui/new-icon-editor-notification/assets/09.png and /dev/null differ
diff --git a/apps/app-frontend/src/components/ui/new-icon-editor-notification/index.vue b/apps/app-frontend/src/components/ui/new-icon-editor-notification/index.vue
deleted file mode 100644
index 1e6985e45c..0000000000
--- a/apps/app-frontend/src/components/ui/new-icon-editor-notification/index.vue
+++ /dev/null
@@ -1,144 +0,0 @@
-
-
-
-
-
-
-
![]()
-
-
-
-
-
-
-
-
- {{ formatMessage(messages.title) }}
-
-
-
-
-
-
- {{ formatMessage(messages.description) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/app-frontend/src/components/ui/new-icon-editor-notification/show-notification.ts b/apps/app-frontend/src/components/ui/new-icon-editor-notification/show-notification.ts
deleted file mode 100644
index ab4f7fce97..0000000000
--- a/apps/app-frontend/src/components/ui/new-icon-editor-notification/show-notification.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-const STORAGE_KEY = 'new-icon-editor-notification-shown'
-
-export function shouldShowNewIconEditorNotification(showOnboardingChecklist: boolean): boolean {
- try {
- if (localStorage.getItem(STORAGE_KEY) === 'true') return false
- if (showOnboardingChecklist) return false
- localStorage.setItem(STORAGE_KEY, 'true')
- return true
- } catch {
- return !showOnboardingChecklist
- }
-}
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 f1de4c6532..f183e641a8 100644
--- a/apps/app-frontend/src/components/ui/screenshots-page/card.vue
+++ b/apps/app-frontend/src/components/ui/screenshots-page/card.vue
@@ -12,6 +12,7 @@ import {
} from '@modrinth/ui'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
+import { useImageThumbnail } from '@/composables/use-image-thumbnail'
import type { InstanceScreenshot } from '@/helpers/instance'
const props = defineProps<{
@@ -33,6 +34,11 @@ const emit = defineEmits<{
}>()
const card = ref()
+const thumbnail = useImageThumbnail(
+ () => props.screenshot.path,
+ 512,
+ () => props.screenshot.modified_at,
+)
const image = ref()
const imageReady = ref(false)
const { formatMessage } = useVIntl()
@@ -79,7 +85,7 @@ function activate(event: MouseEvent | KeyboardEvent) {
async function markImageLoaded() {
const loadedImage = image.value
- const loadedUrl = props.screenshot.url
+ const loadedUrl = thumbnail.value
const generation = loadGeneration
if (!loadedImage) return
@@ -96,7 +102,7 @@ async function markImageLoaded() {
if (
generation !== loadGeneration ||
image.value !== loadedImage ||
- props.screenshot.url !== loadedUrl
+ thumbnail.value !== loadedUrl
) {
return
}
@@ -145,20 +151,17 @@ onBeforeUnmount(() => {
})
})
-watch(
- () => props.screenshot.url,
- (url, previousUrl) => {
- loadGeneration += 1
- loadStartedAt = performance.now()
- imageReady.value = false
- debugImage('source changed', {
- id: props.screenshot.id,
- fileName: props.screenshot.file_name,
- previousUrl,
- url,
- })
- },
-)
+watch(thumbnail, (url, previousUrl) => {
+ loadGeneration += 1
+ loadStartedAt = performance.now()
+ imageReady.value = false
+ debugImage('source changed', {
+ id: props.screenshot.id,
+ fileName: props.screenshot.file_name,
+ previousUrl,
+ url,
+ })
+})
@@ -211,7 +214,7 @@ watch(
props.screenshot.path,
+ 512,
+ () => props.screenshot.modified_at,
+)
const showGatheredCount = ref(false)
let countTimer: number | undefined
@@ -53,7 +59,7 @@ onBeforeUnmount(() => {
-
![]()
+
getBehaviorSettingsState(persistedSettings.value, persistedGlobalSyncedOptions.value),
- async () => {
- const value = current.value
-
+const settingsMutation = useMutation({
+ mutationKey: appSettingsKeys.update,
+ scope: { id: 'app-settings' },
+ mutationFn: async (value: BehaviorSettingsState) => {
if (value.syncBehaviorAcrossDevices && auth.user.value) {
await updatePreferences({
behavior: {
minimize_app: value.minimizeApp,
hide_right_sidebar: value.hideRightSidebar,
- show_jump_in: value.showJumpIn,
compact_instance_cards: value.compactInstanceCards,
show_play_time: value.showPlayTime,
hide_nametag: value.hideNametag,
- show_all_screenshots: value.showAllScreenshots,
warn_on_unknown_modpacks: value.warnOnUnknownModpacks,
skip_non_essential_warnings: value.skipNonEssentialWarnings,
},
})
}
+ const latestSettings = await get()
const nextSettings: AppSettings = {
- ...persistedSettings.value,
+ ...latestSettings,
sync_behavior_across_devices: value.syncBehaviorAcrossDevices,
hide_on_process_start: value.minimizeApp,
toggle_sidebar: value.hideRightSidebar,
hide_nametag_skins_page: value.hideNametag,
feature_flags: {
- ...persistedSettings.value.feature_flags,
- [worldsInHomeFlag]: value.showJumpIn,
+ ...latestSettings.feature_flags,
[compactInstanceCardsFlag]: value.compactInstanceCards,
[showPlayTimeFlag]: value.showPlayTime,
[skipUnknownPackWarningFlag]: !value.warnOnUnknownModpacks,
@@ -231,29 +201,24 @@ const { saved, current, changes, saving, hasChanges, reset, save } = useSavable(
},
}
- 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 })
- }
+ await set(nextSettings)
+ queryClient.setQueryData(appSettingsKeys.all, nextSettings)
appSettings.setBehaviorSyncAcrossDevices(value.syncBehaviorAcrossDevices)
appSettings.toggleSidebar = value.hideRightSidebar
appSettings.hideNametagSkinsPage = value.hideNametag
- appSettings.featureFlags[worldsInHomeFlag] = value.showJumpIn
appSettings.featureFlags[compactInstanceCardsFlag] = value.compactInstanceCards
appSettings.featureFlags[showPlayTimeFlag] = value.showPlayTime
appSettings.featureFlags[skipUnknownPackWarningFlag] = !value.warnOnUnknownModpacks
appSettings.featureFlags[skipNonEssentialWarningsFlag] = value.skipNonEssentialWarnings
},
+ onMutate: () => queryClient.cancelQueries({ queryKey: appSettingsKeys.all }),
+ onError: handleError,
+ onSettled: () => queryClient.invalidateQueries({ queryKey: appSettingsKeys.all }),
+})
+
+const { saved, current, changes, saving, hasChanges, reset, save } = useSavable(
+ () => getBehaviorSettingsState(settingsQuery.data.value!),
+ () => settingsMutation.mutateAsync({ ...current.value }),
)
async function saveBehaviorSettings(): Promise {
@@ -341,30 +306,6 @@ onBeforeUnmount(() => {
{{ formatMessage(messages.contentTitle) }}
-
-
-
- {{ formatMessage(messages.showAllScreenshotsTitle) }}
-
-
- {{ formatMessage(messages.showAllScreenshotsDescription) }}
-
-
-
-
-
-
-
-
- {{ formatMessage(messages.jumpBackIntoWorldsTitle) }}
-
-
- {{ formatMessage(messages.jumpBackIntoWorldsDescription) }}
-
-
-
-
-
diff --git a/apps/app-frontend/src/components/ui/settings/display/FeaturesSettings.vue b/apps/app-frontend/src/components/ui/settings/display/FeaturesSettings.vue
new file mode 100644
index 0000000000..1d3e379416
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/display/FeaturesSettings.vue
@@ -0,0 +1,406 @@
+
+
+
+
+
+
+
+ {{ formatMessage(messages.syncAcrossDevicesTitle) }}
+
+
+ {{ formatMessage(messages.syncAcrossDevicesDescription) }}
+
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.instancePagesTitle) }}
+
+
+
+
+
+ {{ formatMessage(messages.showWorldsTabTitle) }}
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.showFilesTabTitle) }}
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.showScreenshotsTabTitle) }}
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.sidebarTitle) }}
+
+
+
+
+
+ {{ formatMessage(messages.showAllScreenshotsTitle) }}
+
+
+ {{ formatMessage(messages.showAllScreenshotsDescription) }}
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.showSkinSelectorTitle) }}
+
+
{{ formatMessage(messages.showSkinSelectorDescription) }}
+
+
+
+
+
+
+ {{ formatMessage(messages.quickInstancesTitle) }}
+
+
+
+ {{ formatMessage(messages.quickInstancesDescription) }}
+
+
+
+
+
+
+
+ {{ formatMessage(messages.playPageTitle) }}
+
+
+
+
+ {{ formatMessage(messages.showJumpInTitle) }}
+
+
{{ formatMessage(messages.showJumpInDescription) }}
+
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/settings/instances/InstancesSyncedSettings.vue b/apps/app-frontend/src/components/ui/settings/instances/InstancesSyncedSettings.vue
deleted file mode 100644
index 548abba7af..0000000000
--- a/apps/app-frontend/src/components/ui/settings/instances/InstancesSyncedSettings.vue
+++ /dev/null
@@ -1,1027 +0,0 @@
-
-
-
-
-
-
- {{ baseInstanceDescription }}
-
-
-
-
-
-
- {{ formatMessage(messages.noInstancesFound) }}
-
-
-
-
-
-
- {{ instance.name }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ formatMessage(messages.noSyncedServers) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ formatMessage(messages[row.title]) }}
-
-
- {{ formatMessage(messages[row.description]) }}
-
-
-
-
-
-
-
-
- toggleGlobalOption(row.option, enabled)"
- />
-
-
-
-
-
-
-
-
- {{ formatMessage(messages.windowSectionTitle) }}
-
-
-
-
-
- {{ formatMessage(messages.fullscreenTitle) }}
-
-
- {{ formatMessage(messages.fullscreenDescription) }}
-
-
-
-
-
-
-
-
-
- {{ formatMessage(messages.widthTitle) }}
-
-
- {{ formatMessage(messages.widthDescription) }}
-
-
-
-
-
-
-
-
-
- {{ formatMessage(messages.heightTitle) }}
-
-
- {{ formatMessage(messages.heightDescription) }}
-
-
-
-
-
-
-
-
-
-
- {{ formatMessage(messages.javaAndMemorySectionTitle) }}
-
-
-
-
- {{ formatMessage(messages.memoryAllocationTitle) }}
-
-
-
- {{ formatMessage(messages.memoryAllocationDescription) }}
-
-
-
-
-
- {{ formatMessage(messages.javaArgumentsTitle) }}
-
-
-
- {{ formatMessage(messages.javaArgumentsDescription) }}
-
-
-
-
-
- {{ formatMessage(messages.environmentVariablesTitle) }}
-
-
-
- {{ formatMessage(messages.environmentVariablesDescription) }}
-
-
-
-
-
-
-
- {{ formatMessage(messages.launchHooksSectionTitle) }}
-
-
-
-
- {{ formatMessage(messages.preLaunchHookTitle) }}
-
-
-
- {{ formatMessage(messages.preLaunchHookDescription) }}
-
-
-
-
-
- {{ formatMessage(messages.wrapperHookTitle) }}
-
-
-
- {{ formatMessage(messages.wrapperHookDescription) }}
-
-
-
-
-
- {{ formatMessage(messages.postExitHookTitle) }}
-
-
-
- {{ formatMessage(messages.postExitHookDescription) }}
-
-
-
-
- {{ formatMessage(messages.hookVariablesDescription) }}
-
- - {{ formatMessage(messages.instanceNameDescription) }}
- - {{ formatMessage(messages.instanceIdDescription) }}
- - {{ formatMessage(messages.instanceDirDescription) }}
- - {{ formatMessage(messages.instanceMcDirDescription) }}
- - {{ formatMessage(messages.instanceJavaDescription) }}
- - {{ formatMessage(messages.instanceJavaArgsDescription) }}
-
-
-
-
-
-
-
-
diff --git a/apps/app-frontend/src/components/ui/settings/instances/SyncSourceModal.vue b/apps/app-frontend/src/components/ui/settings/instances/SyncSourceModal.vue
new file mode 100644
index 0000000000..688ffb7d7c
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/instances/SyncSourceModal.vue
@@ -0,0 +1,179 @@
+
+
+
+
+
+ {{ description }}
+
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.loadError) }}
+
+
+
+
+ {{ formatMessage(messages.empty) }}
+
+
+
+
+
+
+
+ {{ source.name }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/settings/instances/SyncedPacksModal.vue b/apps/app-frontend/src/components/ui/settings/instances/SyncedPacksModal.vue
new file mode 100644
index 0000000000..f84c6073e0
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/instances/SyncedPacksModal.vue
@@ -0,0 +1,148 @@
+
+
+
+
+ togglePacks([item], enabled)"
+ @bulk:enable="(items) => togglePacks(items, true)"
+ @bulk:disable="(items) => togglePacks(items, false)"
+ @hide="isOpen = false"
+ >
+
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/boolean-control.vue b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/boolean-control.vue
new file mode 100644
index 0000000000..7b29957745
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/boolean-control.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/editors.ts b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/editors.ts
new file mode 100644
index 0000000000..0ccd80d64d
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/editors.ts
@@ -0,0 +1,158 @@
+import { toRaw } from 'vue'
+
+import type {
+ EditableGameSetting,
+ GameOptionCanonicalValue,
+ GameSettingChange,
+ GameSettingsEditorState,
+} from '@/helpers/game-options'
+
+function clonePlainValue(value: unknown): unknown {
+ if (Array.isArray(value)) {
+ return toRaw(value).map(clonePlainValue)
+ }
+ if (value !== null && typeof value === 'object') {
+ return Object.fromEntries(
+ Object.entries(toRaw(value)).map(([key, nestedValue]) => [key, clonePlainValue(nestedValue)]),
+ )
+ }
+ return value
+}
+
+export function cloneGameSettingsState(state: GameSettingsEditorState): GameSettingsEditorState {
+ return clonePlainValue(state) as GameSettingsEditorState
+}
+
+export function canonicalValuesEqual(
+ left: GameOptionCanonicalValue | null | undefined,
+ right: GameOptionCanonicalValue | null | undefined,
+): boolean {
+ return JSON.stringify(left ?? null) === JSON.stringify(right ?? null)
+}
+
+export function gameSettingChanges(
+ base: GameSettingsEditorState | null,
+ draft: GameSettingsEditorState | null,
+ forcedValueOptionIds: ReadonlySet = new Set(),
+): GameSettingChange[] {
+ if (!base || !draft) return []
+
+ const baseSettings = new Map(base.settings.map((setting) => [setting.option_id, setting]))
+ return draft.settings.flatMap((setting) => {
+ const previous = baseSettings.get(setting.option_id)
+ if (!previous) return []
+
+ const syncChanged = previous.sync_enabled !== setting.sync_enabled
+ const valueChanged = !canonicalValuesEqual(previous.canonical_value, setting.canonical_value)
+ const promoteLocalValue =
+ forcedValueOptionIds.has(setting.option_id) &&
+ previous.value_state !== 'canonical' &&
+ setting.canonical_value !== null &&
+ setting.canonical_value !== undefined
+ if (!syncChanged && !valueChanged && !promoteLocalValue) return []
+
+ return [
+ {
+ option_id: setting.option_id,
+ base_option_revision: previous.option_revision,
+ ...(syncChanged ? { sync_enabled: setting.sync_enabled } : {}),
+ ...(valueChanged || promoteLocalValue
+ ? { canonical_value: setting.canonical_value ?? null }
+ : {}),
+ },
+ ]
+ })
+}
+
+export function canonicalValueText(setting: EditableGameSetting): string {
+ const value = setting.canonical_value
+ if (!value) return ''
+
+ switch (value.type) {
+ case 'bool':
+ return value.value ? 'true' : 'false'
+ case 'integer':
+ case 'decimal':
+ return setting.editor.unit === 'percent'
+ ? String(Number((Number(value.value) * 100).toFixed(8)))
+ : String(value.value)
+ case 'string_list':
+ return value.value.join(', ')
+ default:
+ return value.value
+ }
+}
+
+export function canonicalBooleanValue(setting: EditableGameSetting): boolean | undefined {
+ return setting.canonical_value?.type === 'bool' ? setting.canonical_value.value : undefined
+}
+
+export function isKeybindSetting(setting: EditableGameSetting): boolean {
+ return (
+ setting.editor.type === 'key_binding' ||
+ (setting.editor.type === 'external_raw' && !!setting.raw_key?.startsWith('key_key'))
+ )
+}
+
+export function canonicalValueFromInput(
+ setting: EditableGameSetting,
+ value: string | number | boolean | undefined,
+): GameOptionCanonicalValue | null {
+ if (value === undefined || value === '') return null
+
+ switch (setting.editor.type) {
+ case 'bool':
+ return { type: 'bool', value: Boolean(value) }
+ case 'integer': {
+ const parsed = Number(value)
+ return Number.isSafeInteger(parsed) ? { type: 'integer', value: parsed } : null
+ }
+ case 'decimal': {
+ const parsed = Number(value)
+ if (!Number.isFinite(parsed)) return null
+ return {
+ type: 'decimal',
+ value:
+ setting.editor.unit === 'percent'
+ ? String(Number((parsed / 100).toFixed(8)))
+ : String(value),
+ }
+ }
+ case 'enum':
+ return { type: 'enum', value: String(value) }
+ case 'string_list':
+ return {
+ type: 'string_list',
+ value: String(value)
+ .split(',')
+ .map((item) => item.trim())
+ .filter(Boolean),
+ }
+ case 'key_binding':
+ return { type: 'key_binding', value: String(value) }
+ case 'external_raw':
+ return { type: 'external_raw', value: String(value) }
+ default:
+ return { type: 'text', value: String(value) }
+ }
+}
+
+export function settingSearchText(
+ setting: EditableGameSetting,
+ label: string,
+ description: string,
+): string {
+ return [label, description, setting.option_id, setting.raw_key]
+ .filter(Boolean)
+ .join(' ')
+ .toLocaleLowerCase()
+}
+
+export function settingCanBeEnabled(setting: EditableGameSetting): boolean {
+ return (
+ !setting.controlled &&
+ !setting.validation_error &&
+ !['mixed', 'unset', 'invalid'].includes(setting.value_state) &&
+ (setting.compatibility.total_participating === 0 || setting.compatibility.will_receive > 0)
+ )
+}
diff --git a/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/index.vue b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/index.vue
new file mode 100644
index 0000000000..f1a826bb1d
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/index.vue
@@ -0,0 +1,433 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.syncedOptionsDescription) }}
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.loading) }}
+
+
+
{{ formatMessage(messages.loadFailed) }}
+
+
+
+
+ {{
+ activeCategory?.is_custom && !search
+ ? formatMessage(messages.customEmpty)
+ : formatMessage(messages.empty)
+ }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.unsavedChanges) }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/keybind-input.vue b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/keybind-input.vue
new file mode 100644
index 0000000000..aa673b3161
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/keybind-input.vue
@@ -0,0 +1,235 @@
+
+
+
+
+
+
+
+
+
+ {{ statusMessage }}
+
+
+ {{ conflictMessage }}
+
+
+
diff --git a/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/keybinds.ts b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/keybinds.ts
new file mode 100644
index 0000000000..ec64a326e4
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/keybinds.ts
@@ -0,0 +1,426 @@
+import { defineMessages, type MessageDescriptor, type VIntlFormatters } from '@modrinth/ui'
+
+type FormatMessage = VIntlFormatters['formatMessage']
+
+type StopKeybindRecording = () => void
+let stopActiveKeybindRecording: StopKeybindRecording | null = null
+
+export function activateKeybindRecording(stop: StopKeybindRecording) {
+ if (stopActiveKeybindRecording === stop) return
+ const stopPrevious = stopActiveKeybindRecording
+ stopActiveKeybindRecording = stop
+ stopPrevious?.()
+}
+
+export function deactivateKeybindRecording(stop: StopKeybindRecording) {
+ if (stopActiveKeybindRecording === stop) {
+ stopActiveKeybindRecording = null
+ }
+}
+
+const keyMessages = defineMessages({
+ unbound: {
+ id: 'app.settings.game-options.keybind.key.unbound',
+ defaultMessage: 'Unbound',
+ },
+ escape: {
+ id: 'app.settings.game-options.keybind.key.escape',
+ defaultMessage: 'Escape',
+ },
+ backspace: {
+ id: 'app.settings.game-options.keybind.key.backspace',
+ defaultMessage: 'Backspace',
+ },
+ tab: {
+ id: 'app.settings.game-options.keybind.key.tab',
+ defaultMessage: 'Tab',
+ },
+ enter: {
+ id: 'app.settings.game-options.keybind.key.enter',
+ defaultMessage: 'Enter',
+ },
+ leftControl: {
+ id: 'app.settings.game-options.keybind.key.left-control',
+ defaultMessage: 'Left Ctrl',
+ },
+ rightControl: {
+ id: 'app.settings.game-options.keybind.key.right-control',
+ defaultMessage: 'Right Ctrl',
+ },
+ leftShift: {
+ id: 'app.settings.game-options.keybind.key.left-shift',
+ defaultMessage: 'Left Shift',
+ },
+ rightShift: {
+ id: 'app.settings.game-options.keybind.key.right-shift',
+ defaultMessage: 'Right Shift',
+ },
+ leftAlt: {
+ id: 'app.settings.game-options.keybind.key.left-alt',
+ defaultMessage: 'Left Alt',
+ },
+ rightAlt: {
+ id: 'app.settings.game-options.keybind.key.right-alt',
+ defaultMessage: 'Right Alt',
+ },
+ space: {
+ id: 'app.settings.game-options.keybind.key.space',
+ defaultMessage: 'Space',
+ },
+ capsLock: {
+ id: 'app.settings.game-options.keybind.key.caps-lock',
+ defaultMessage: 'Caps Lock',
+ },
+ numLock: {
+ id: 'app.settings.game-options.keybind.key.num-lock',
+ defaultMessage: 'Num Lock',
+ },
+ scrollLock: {
+ id: 'app.settings.game-options.keybind.key.scroll-lock',
+ defaultMessage: 'Scroll Lock',
+ },
+ printScreen: {
+ id: 'app.settings.game-options.keybind.key.print-screen',
+ defaultMessage: 'Print Screen',
+ },
+ pause: {
+ id: 'app.settings.game-options.keybind.key.pause',
+ defaultMessage: 'Pause',
+ },
+ home: {
+ id: 'app.settings.game-options.keybind.key.home',
+ defaultMessage: 'Home',
+ },
+ end: {
+ id: 'app.settings.game-options.keybind.key.end',
+ defaultMessage: 'End',
+ },
+ pageUp: {
+ id: 'app.settings.game-options.keybind.key.page-up',
+ defaultMessage: 'Page Up',
+ },
+ pageDown: {
+ id: 'app.settings.game-options.keybind.key.page-down',
+ defaultMessage: 'Page Down',
+ },
+ insert: {
+ id: 'app.settings.game-options.keybind.key.insert',
+ defaultMessage: 'Insert',
+ },
+ delete: {
+ id: 'app.settings.game-options.keybind.key.delete',
+ defaultMessage: 'Delete',
+ },
+ arrowUp: {
+ id: 'app.settings.game-options.keybind.key.arrow-up',
+ defaultMessage: 'Up Arrow',
+ },
+ arrowDown: {
+ id: 'app.settings.game-options.keybind.key.arrow-down',
+ defaultMessage: 'Down Arrow',
+ },
+ arrowLeft: {
+ id: 'app.settings.game-options.keybind.key.arrow-left',
+ defaultMessage: 'Left Arrow',
+ },
+ arrowRight: {
+ id: 'app.settings.game-options.keybind.key.arrow-right',
+ defaultMessage: 'Right Arrow',
+ },
+ leftSuper: {
+ id: 'app.settings.game-options.keybind.key.left-super',
+ defaultMessage: 'Left Super',
+ },
+ rightSuper: {
+ id: 'app.settings.game-options.keybind.key.right-super',
+ defaultMessage: 'Right Super',
+ },
+ leftCommand: {
+ id: 'app.settings.game-options.keybind.key.left-command',
+ defaultMessage: 'Left Command',
+ },
+ rightCommand: {
+ id: 'app.settings.game-options.keybind.key.right-command',
+ defaultMessage: 'Right Command',
+ },
+ menu: {
+ id: 'app.settings.game-options.keybind.key.menu',
+ defaultMessage: 'Menu',
+ },
+ keypadKey: {
+ id: 'app.settings.game-options.keybind.key.keypad-key',
+ defaultMessage: 'Numpad {key}',
+ },
+ leftMouse: {
+ id: 'app.settings.game-options.keybind.mouse.left',
+ defaultMessage: 'Left Mouse',
+ },
+ rightMouse: {
+ id: 'app.settings.game-options.keybind.mouse.right',
+ defaultMessage: 'Right Mouse',
+ },
+ middleMouse: {
+ id: 'app.settings.game-options.keybind.mouse.middle',
+ defaultMessage: 'Middle Mouse',
+ },
+ mouseButton: {
+ id: 'app.settings.game-options.keybind.mouse.button',
+ defaultMessage: 'Mouse {button}',
+ },
+ scancode: {
+ id: 'app.settings.game-options.keybind.scancode',
+ defaultMessage: 'Scancode {code}',
+ },
+ unknownKey: {
+ id: 'app.settings.game-options.keybind.key.unknown',
+ defaultMessage: 'Unknown key',
+ },
+ controlModifier: {
+ id: 'app.settings.game-options.keybind.modifier.control',
+ defaultMessage: 'Ctrl',
+ },
+ shiftModifier: {
+ id: 'app.settings.game-options.keybind.modifier.shift',
+ defaultMessage: 'Shift',
+ },
+ altModifier: {
+ id: 'app.settings.game-options.keybind.modifier.alt',
+ defaultMessage: 'Alt',
+ },
+ commandModifier: {
+ id: 'app.settings.game-options.keybind.modifier.command',
+ defaultMessage: 'Command',
+ },
+ superModifier: {
+ id: 'app.settings.game-options.keybind.modifier.super',
+ defaultMessage: 'Super',
+ },
+})
+
+const keyboardCodeTokens: Record = {
+ Escape: 'escape',
+ Minus: 'minus',
+ Equal: 'equal',
+ Backspace: 'backspace',
+ Tab: 'tab',
+ BracketLeft: 'left.bracket',
+ BracketRight: 'right.bracket',
+ Enter: 'enter',
+ ControlLeft: 'left.control',
+ Semicolon: 'semicolon',
+ Quote: 'apostrophe',
+ Backquote: 'grave.accent',
+ ShiftLeft: 'left.shift',
+ Backslash: 'backslash',
+ Comma: 'comma',
+ Period: 'period',
+ Slash: 'slash',
+ ShiftRight: 'right.shift',
+ AltLeft: 'left.alt',
+ Space: 'space',
+ CapsLock: 'caps.lock',
+ NumLock: 'num.lock',
+ ScrollLock: 'scroll.lock',
+ NumpadSubtract: 'keypad.subtract',
+ NumpadAdd: 'keypad.add',
+ NumpadDecimal: 'keypad.decimal',
+ NumpadComma: 'keypad.decimal',
+ NumpadEqual: 'keypad.equal',
+ NumpadEnter: 'keypad.enter',
+ ControlRight: 'right.control',
+ NumpadDivide: 'keypad.divide',
+ NumpadMultiply: 'keypad.multiply',
+ PrintScreen: 'print.screen',
+ AltRight: 'right.alt',
+ Pause: 'pause',
+ Home: 'home',
+ ArrowUp: 'up',
+ PageUp: 'page.up',
+ ArrowLeft: 'left',
+ ArrowRight: 'right',
+ End: 'end',
+ ArrowDown: 'down',
+ PageDown: 'page.down',
+ Insert: 'insert',
+ Delete: 'delete',
+ MetaLeft: 'left.win',
+ MetaRight: 'right.win',
+ ContextMenu: 'menu',
+}
+
+const keyboardLabels: Record = {
+ escape: keyMessages.escape,
+ minus: '-',
+ equal: '=',
+ backspace: keyMessages.backspace,
+ tab: keyMessages.tab,
+ 'left.bracket': '[',
+ 'right.bracket': ']',
+ enter: keyMessages.enter,
+ 'left.control': keyMessages.leftControl,
+ semicolon: ';',
+ apostrophe: "'",
+ 'grave.accent': '`',
+ 'left.shift': keyMessages.leftShift,
+ backslash: '\\',
+ comma: ',',
+ period: '.',
+ slash: '/',
+ 'right.shift': keyMessages.rightShift,
+ 'left.alt': keyMessages.leftAlt,
+ space: keyMessages.space,
+ 'caps.lock': keyMessages.capsLock,
+ 'num.lock': keyMessages.numLock,
+ 'scroll.lock': keyMessages.scrollLock,
+ 'keypad.subtract': '-',
+ 'keypad.add': '+',
+ 'keypad.decimal': '.',
+ 'keypad.equal': '=',
+ 'keypad.enter': keyMessages.enter,
+ 'right.control': keyMessages.rightControl,
+ 'keypad.divide': '/',
+ 'keypad.multiply': '*',
+ 'print.screen': keyMessages.printScreen,
+ 'right.alt': keyMessages.rightAlt,
+ pause: keyMessages.pause,
+ home: keyMessages.home,
+ up: keyMessages.arrowUp,
+ 'page.up': keyMessages.pageUp,
+ left: keyMessages.arrowLeft,
+ right: keyMessages.arrowRight,
+ end: keyMessages.end,
+ down: keyMessages.arrowDown,
+ 'page.down': keyMessages.pageDown,
+ insert: keyMessages.insert,
+ delete: keyMessages.delete,
+ menu: keyMessages.menu,
+}
+
+function splitKeybind(value: string): [key: string, modifier?: string] {
+ const separator = value.indexOf(':')
+ return separator === -1 ? [value] : [value.slice(0, separator), value.slice(separator + 1)]
+}
+
+function keyboardIdentifierFromCode(code: string): string | null {
+ const letter = /^Key([A-Z])$/.exec(code)
+ if (letter) return letter[1].toLowerCase()
+
+ const digit = /^Digit([0-9])$/.exec(code)
+ if (digit) return digit[1]
+
+ const functionKey = /^F([1-9]|1[0-9]|2[0-5])$/.exec(code)
+ if (functionKey) return `f${functionKey[1]}`
+
+ const keypadDigit = /^Numpad([0-9])$/.exec(code)
+ if (keypadDigit) return `keypad.${keypadDigit[1]}`
+
+ return keyboardCodeTokens[code] ?? null
+}
+
+export function minecraftKeyTokenFromKeyboardEvent(event: KeyboardEvent): string | null {
+ const identifier = keyboardIdentifierFromCode(event.code)
+ return identifier ? `key.keyboard.${identifier}` : null
+}
+
+export function minecraftMouseTokenFromButton(button: number): string | null {
+ if (!Number.isInteger(button) || button < 0 || button > 15) return null
+ if (button === 0) return 'key.mouse.left'
+ if (button === 1) return 'key.mouse.middle'
+ if (button === 2) return 'key.mouse.right'
+ return `key.mouse.${button + 1}`
+}
+
+function formatKeyboardLabel(
+ formatMessage: FormatMessage,
+ identifier: string,
+ isMac: boolean,
+): string {
+ if (identifier === 'unknown') return formatMessage(keyMessages.unbound)
+ if (/^[a-z]$/.test(identifier)) return identifier.toUpperCase()
+ if (/^[0-9]$/.test(identifier)) return identifier
+ if (/^f([1-9]|1[0-9]|2[0-5])$/.test(identifier)) return identifier.toUpperCase()
+
+ const keypad = /^keypad\.(.+)$/.exec(identifier)
+ if (keypad) {
+ const label = keyboardLabels[identifier]
+ const key = typeof label === 'string' ? label : label ? formatMessage(label) : keypad[1]
+ return formatMessage(keyMessages.keypadKey, { key })
+ }
+
+ if (identifier === 'left.win') {
+ return formatMessage(isMac ? keyMessages.leftCommand : keyMessages.leftSuper)
+ }
+ if (identifier === 'right.win') {
+ return formatMessage(isMac ? keyMessages.rightCommand : keyMessages.rightSuper)
+ }
+
+ const label = keyboardLabels[identifier]
+ if (typeof label === 'string') return label
+ if (label) return formatMessage(label)
+ return identifier
+ .split('.')
+ .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
+ .join(' ')
+}
+
+function formatModifier(formatMessage: FormatMessage, modifier: string, isMac: boolean): string {
+ switch (modifier.toUpperCase()) {
+ case 'CONTROL':
+ case 'CTRL':
+ return formatMessage(keyMessages.controlModifier)
+ case 'SHIFT':
+ return formatMessage(keyMessages.shiftModifier)
+ case 'ALT':
+ return formatMessage(keyMessages.altModifier)
+ case 'META':
+ case 'SUPER':
+ return formatMessage(isMac ? keyMessages.commandModifier : keyMessages.superModifier)
+ default:
+ return modifier
+ }
+}
+
+export function formatMinecraftKeybind(
+ formatMessage: FormatMessage,
+ value: string,
+ isMac: boolean,
+): string {
+ const [key, modifier] = splitKeybind(value)
+ let label: string
+
+ if (key.startsWith('key.keyboard.')) {
+ label = formatKeyboardLabel(formatMessage, key.slice('key.keyboard.'.length), isMac)
+ } else if (key === 'key.mouse.left') {
+ label = formatMessage(keyMessages.leftMouse)
+ } else if (key === 'key.mouse.right') {
+ label = formatMessage(keyMessages.rightMouse)
+ } else if (key === 'key.mouse.middle') {
+ label = formatMessage(keyMessages.middleMouse)
+ } else if (key.startsWith('key.mouse.')) {
+ label = formatMessage(keyMessages.mouseButton, {
+ button: key.slice('key.mouse.'.length),
+ })
+ } else if (key.startsWith('scancode.')) {
+ label = formatMessage(keyMessages.scancode, { code: key.slice('scancode.'.length) })
+ } else {
+ label = formatMessage(keyMessages.unknownKey)
+ }
+
+ if (!modifier || key === 'key.keyboard.unknown') return label
+ const modifiers = modifier
+ .split(/[+:]/)
+ .filter(Boolean)
+ .map((part) => formatModifier(formatMessage, part, isMac))
+ return [...modifiers, label].join(' + ')
+}
+
+export function minecraftKeybindConflictKey(optionId: string, value: string): string | null {
+ const normalized = value.trim().toLowerCase()
+ if (!normalized || normalized === 'key.keyboard.unknown') return null
+
+ if (optionId === 'key.debug.modifier') return null
+ if (optionId.startsWith('key.debug.') && optionId !== 'key.debug.overlay') {
+ return `debug:${normalized}`
+ }
+ return `direct:${normalized}`
+}
diff --git a/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/messages.ts b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/messages.ts
new file mode 100644
index 0000000000..a5e741b63a
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/messages.ts
@@ -0,0 +1,1428 @@
+import { defineMessages, type MessageDescriptor, type VIntlFormatters } from '@modrinth/ui'
+
+import type {
+ EditableGameSetting,
+ GameOptionValidationError,
+ GameSettingCategory,
+} from '@/helpers/game-options'
+
+type FormatMessage = VIntlFormatters['formatMessage']
+
+const settingMessages = defineMessages({
+ fovLabel: { id: 'app.settings.game-options.setting.fov.label', defaultMessage: 'Field of view' },
+ graphicsLabel: {
+ id: 'app.settings.game-options.setting.graphics.label',
+ defaultMessage: 'Graphics',
+ },
+ graphicsDescription: {
+ id: 'app.settings.game-options.setting.graphics.description',
+ defaultMessage: 'Controls visual quality and performance.',
+ },
+ ambientOcclusionLabel: {
+ id: 'app.settings.game-options.setting.ambient-occlusion.label',
+ defaultMessage: 'Smooth lighting',
+ },
+ renderDistanceLabel: {
+ id: 'app.settings.game-options.setting.render-distance.label',
+ defaultMessage: 'Render distance',
+ },
+ simulationDistanceLabel: {
+ id: 'app.settings.game-options.setting.simulation-distance.label',
+ defaultMessage: 'Simulation distance',
+ },
+ simulationDistanceDescription: {
+ id: 'app.settings.game-options.setting.simulation-distance.description',
+ defaultMessage: 'How far away entities update and blocks and fluids tick.',
+ },
+ guiScaleLabel: {
+ id: 'app.settings.game-options.setting.gui-scale.label',
+ defaultMessage: 'GUI scale',
+ },
+ guiScaleDescription: {
+ id: 'app.settings.game-options.setting.gui-scale.description',
+ defaultMessage: 'The size of the game interface and HUD.',
+ },
+ particlesLabel: {
+ id: 'app.settings.game-options.setting.particles.label',
+ defaultMessage: 'Particles',
+ },
+ cloudsLabel: { id: 'app.settings.game-options.setting.clouds.label', defaultMessage: 'Clouds' },
+ entityShadowsLabel: {
+ id: 'app.settings.game-options.setting.entity-shadows.label',
+ defaultMessage: 'Entity shadows',
+ },
+ viewBobbingLabel: {
+ id: 'app.settings.game-options.setting.view-bobbing.label',
+ defaultMessage: 'View bobbing',
+ },
+ viewBobbingDescription: {
+ id: 'app.settings.game-options.setting.view-bobbing.description',
+ defaultMessage: 'Add a bobbing motion to the camera while walking.',
+ },
+ vsyncLabel: { id: 'app.settings.game-options.setting.vsync.label', defaultMessage: 'VSync' },
+ vsyncDescription: {
+ id: 'app.settings.game-options.setting.vsync.description',
+ defaultMessage: 'Limit the frame rate to the display refresh rate to prevent screen tearing.',
+ },
+ fullscreenLabel: {
+ id: 'app.settings.game-options.setting.fullscreen.label',
+ defaultMessage: 'Fullscreen',
+ },
+ maxFramerateLabel: {
+ id: 'app.settings.game-options.setting.max-framerate.label',
+ defaultMessage: 'Maximum framerate',
+ },
+ mipmapLevelsLabel: {
+ id: 'app.settings.game-options.setting.mipmap-levels.label',
+ defaultMessage: 'Mipmap levels',
+ },
+ mipmapLevelsDescription: {
+ id: 'app.settings.game-options.setting.mipmap-levels.description',
+ defaultMessage: 'Texture smoothing at a distance.',
+ },
+ biomeBlendRadiusLabel: {
+ id: 'app.settings.game-options.setting.biome-blend-radius.label',
+ defaultMessage: 'Biome blend',
+ },
+ biomeBlendRadiusDescription: {
+ id: 'app.settings.game-options.setting.biome-blend-radius.description',
+ defaultMessage: 'The distance over which biome colors transition.',
+ },
+ languageLabel: {
+ id: 'app.settings.game-options.setting.language.label',
+ defaultMessage: 'Language',
+ },
+ masterVolumeLabel: {
+ id: 'app.settings.game-options.setting.master-volume.label',
+ defaultMessage: 'Master volume',
+ },
+ musicVolumeLabel: {
+ id: 'app.settings.game-options.setting.music-volume.label',
+ defaultMessage: 'Music',
+ },
+ musicToastLabel: {
+ id: 'app.settings.game-options.setting.music-toast.label',
+ defaultMessage: 'Music notification',
+ },
+ musicToastDescription: {
+ id: 'app.settings.game-options.setting.music-toast.description',
+ defaultMessage: 'Choose whether music titles appear in the pause menu and as toasts.',
+ },
+ recordVolumeLabel: {
+ id: 'app.settings.game-options.setting.record-volume.label',
+ defaultMessage: 'Jukebox/Note Blocks',
+ },
+ weatherVolumeLabel: {
+ id: 'app.settings.game-options.setting.weather-volume.label',
+ defaultMessage: 'Weather',
+ },
+ blocksVolumeLabel: {
+ id: 'app.settings.game-options.setting.blocks-volume.label',
+ defaultMessage: 'Blocks',
+ },
+ hostileVolumeLabel: {
+ id: 'app.settings.game-options.setting.hostile-volume.label',
+ defaultMessage: 'Hostile creatures',
+ },
+ neutralVolumeLabel: {
+ id: 'app.settings.game-options.setting.neutral-volume.label',
+ defaultMessage: 'Friendly creatures',
+ },
+ playersVolumeLabel: {
+ id: 'app.settings.game-options.setting.players-volume.label',
+ defaultMessage: 'Players',
+ },
+ ambientVolumeLabel: {
+ id: 'app.settings.game-options.setting.ambient-volume.label',
+ defaultMessage: 'Ambient/Environment',
+ },
+ voiceVolumeLabel: {
+ id: 'app.settings.game-options.setting.voice-volume.label',
+ defaultMessage: 'Voice and speech',
+ },
+ uiVolumeLabel: {
+ id: 'app.settings.game-options.setting.ui-volume.label',
+ defaultMessage: 'UI',
+ },
+ sensitivityLabel: {
+ id: 'app.settings.game-options.setting.sensitivity.label',
+ defaultMessage: 'Mouse sensitivity',
+ },
+ invertMouseLabel: {
+ id: 'app.settings.game-options.setting.invert-mouse.label',
+ defaultMessage: 'Invert mouse',
+ },
+ invertMouseDescription: {
+ id: 'app.settings.game-options.setting.invert-mouse.description',
+ defaultMessage: 'Invert vertical mouse movement.',
+ },
+ autoJumpLabel: {
+ id: 'app.settings.game-options.setting.auto-jump.label',
+ defaultMessage: 'Auto-jump',
+ },
+ autoJumpDescription: {
+ id: 'app.settings.game-options.setting.auto-jump.description',
+ defaultMessage: 'Automatically jump up one-block-high obstacles.',
+ },
+ toggleCrouchLabel: {
+ id: 'app.settings.game-options.setting.toggle-crouch.label',
+ defaultMessage: 'Toggle crouch',
+ },
+ toggleCrouchDescription: {
+ id: 'app.settings.game-options.setting.toggle-crouch.description',
+ defaultMessage: 'Press once to remain crouched.',
+ },
+ toggleSprintLabel: {
+ id: 'app.settings.game-options.setting.toggle-sprint.label',
+ defaultMessage: 'Toggle sprint',
+ },
+ toggleSprintDescription: {
+ id: 'app.settings.game-options.setting.toggle-sprint.description',
+ defaultMessage: 'Press once to remain sprinting.',
+ },
+ discreteMouseScrollLabel: {
+ id: 'app.settings.game-options.setting.discrete-mouse-scroll.label',
+ defaultMessage: 'Discrete scrolling',
+ },
+ discreteMouseScrollDescription: {
+ id: 'app.settings.game-options.setting.discrete-mouse-scroll.description',
+ defaultMessage: 'Treat each mouse-wheel input as a single scroll step.',
+ },
+ keyForwardLabel: {
+ id: 'app.settings.game-options.setting.key-forward.label',
+ defaultMessage: 'Move forward',
+ },
+ keyLeftLabel: {
+ id: 'app.settings.game-options.setting.key-left.label',
+ defaultMessage: 'Strafe left',
+ },
+ keyBackLabel: {
+ id: 'app.settings.game-options.setting.key-back.label',
+ defaultMessage: 'Move backward',
+ },
+ keyRightLabel: {
+ id: 'app.settings.game-options.setting.key-right.label',
+ defaultMessage: 'Strafe right',
+ },
+ keyJumpLabel: { id: 'app.settings.game-options.setting.key-jump.label', defaultMessage: 'Jump' },
+ keySneakLabel: {
+ id: 'app.settings.game-options.setting.key-sneak.label',
+ defaultMessage: 'Sneak',
+ },
+ keySprintLabel: {
+ id: 'app.settings.game-options.setting.key-sprint.label',
+ defaultMessage: 'Sprint',
+ },
+ keyInventoryLabel: {
+ id: 'app.settings.game-options.setting.key-inventory.label',
+ defaultMessage: 'Inventory',
+ },
+ keySwapOffhandLabel: {
+ id: 'app.settings.game-options.setting.key-swap-offhand.label',
+ defaultMessage: 'Swap offhand',
+ },
+ keyDropLabel: {
+ id: 'app.settings.game-options.setting.key-drop.label',
+ defaultMessage: 'Drop item',
+ },
+ keyUseLabel: {
+ id: 'app.settings.game-options.setting.key-use.label',
+ defaultMessage: 'Use item',
+ },
+ keyAttackLabel: {
+ id: 'app.settings.game-options.setting.key-attack.label',
+ defaultMessage: 'Attack',
+ },
+ keyPickItemLabel: {
+ id: 'app.settings.game-options.setting.key-pick-item.label',
+ defaultMessage: 'Pick block',
+ },
+ keyChatLabel: {
+ id: 'app.settings.game-options.setting.key-chat.label',
+ defaultMessage: 'Open chat',
+ },
+ keyPlayerListLabel: {
+ id: 'app.settings.game-options.setting.key-player-list.label',
+ defaultMessage: 'Player list',
+ },
+ keyCommandLabel: {
+ id: 'app.settings.game-options.setting.key-command.label',
+ defaultMessage: 'Command',
+ },
+ keyScreenshotLabel: {
+ id: 'app.settings.game-options.setting.key-screenshot.label',
+ defaultMessage: 'Screenshot',
+ },
+ keyPerspectiveLabel: {
+ id: 'app.settings.game-options.setting.key-perspective.label',
+ defaultMessage: 'Change perspective',
+ },
+ keyFullscreenLabel: {
+ id: 'app.settings.game-options.setting.key-fullscreen.label',
+ defaultMessage: 'Toggle fullscreen',
+ },
+ keyAdvancementsLabel: {
+ id: 'app.settings.game-options.setting.key-advancements.label',
+ defaultMessage: 'Advancements',
+ },
+ chatVisibilityLabel: {
+ id: 'app.settings.game-options.setting.chat-visibility.label',
+ defaultMessage: 'Chat visibility',
+ },
+ chatColorsLabel: {
+ id: 'app.settings.game-options.setting.chat-colors.label',
+ defaultMessage: 'Chat colors',
+ },
+ chatLinksLabel: {
+ id: 'app.settings.game-options.setting.chat-links.label',
+ defaultMessage: 'Web links',
+ },
+ chatLinksDescription: {
+ id: 'app.settings.game-options.setting.chat-links.description',
+ defaultMessage: 'Allow web links in chat to be opened.',
+ },
+ chatLinksPromptLabel: {
+ id: 'app.settings.game-options.setting.chat-links-prompt.label',
+ defaultMessage: 'Prompt on links',
+ },
+ chatLinksPromptDescription: {
+ id: 'app.settings.game-options.setting.chat-links-prompt.description',
+ defaultMessage: 'Ask before opening links from chat.',
+ },
+ chatOpacityLabel: {
+ id: 'app.settings.game-options.setting.chat-opacity.label',
+ defaultMessage: 'Chat opacity',
+ },
+ chatOpacityDescription: {
+ id: 'app.settings.game-options.setting.chat-opacity.description',
+ defaultMessage: 'The opacity of chat text.',
+ },
+ chatScaleLabel: {
+ id: 'app.settings.game-options.setting.chat-scale.label',
+ defaultMessage: 'Chat scale',
+ },
+ narratorLabel: {
+ id: 'app.settings.game-options.setting.narrator.label',
+ defaultMessage: 'Narrator',
+ },
+ narratorDescription: {
+ id: 'app.settings.game-options.setting.narrator.description',
+ defaultMessage: 'Choose what the narrator reads.',
+ },
+ subtitlesLabel: {
+ id: 'app.settings.game-options.setting.subtitles.label',
+ defaultMessage: 'Subtitles',
+ },
+ subtitlesDescription: {
+ id: 'app.settings.game-options.setting.subtitles.description',
+ defaultMessage: 'Show captions for sounds played in the game.',
+ },
+ highContrastLabel: {
+ id: 'app.settings.game-options.setting.high-contrast.label',
+ defaultMessage: 'High contrast',
+ },
+ highContrastDescription: {
+ id: 'app.settings.game-options.setting.high-contrast.description',
+ defaultMessage: 'Enhance the contrast of interface elements.',
+ },
+ darkSplashLabel: {
+ id: 'app.settings.game-options.setting.dark-splash.label',
+ defaultMessage: 'Monochrome logo',
+ },
+ darkSplashDescription: {
+ id: 'app.settings.game-options.setting.dark-splash.description',
+ defaultMessage: 'Change the Mojang Studios loading screen from red to black.',
+ },
+ notificationTimeLabel: {
+ id: 'app.settings.game-options.setting.notification-time.label',
+ defaultMessage: 'Notification time',
+ },
+ notificationTimeDescription: {
+ id: 'app.settings.game-options.setting.notification-time.description',
+ defaultMessage: 'How long toast notifications remain visible.',
+ },
+ mainHandLabel: {
+ id: 'app.settings.game-options.setting.main-hand.label',
+ defaultMessage: 'Main hand',
+ },
+ mainHandDescription: {
+ id: 'app.settings.game-options.setting.main-hand.description',
+ defaultMessage: 'Choose whether the main hand is left or right.',
+ },
+ capeLabel: { id: 'app.settings.game-options.setting.cape.label', defaultMessage: 'Cape' },
+ capeDescription: {
+ id: 'app.settings.game-options.setting.cape.description',
+ defaultMessage: "Show the player's cape, including its elytra texture.",
+ },
+ hatLabel: { id: 'app.settings.game-options.setting.hat.label', defaultMessage: 'Hat' },
+ hatDescription: {
+ id: 'app.settings.game-options.setting.hat.description',
+ defaultMessage: 'Show the hat skin layer.',
+ },
+ jacketLabel: { id: 'app.settings.game-options.setting.jacket.label', defaultMessage: 'Jacket' },
+ jacketDescription: {
+ id: 'app.settings.game-options.setting.jacket.description',
+ defaultMessage: 'Show the jacket skin layer.',
+ },
+ allowServerListingLabel: {
+ id: 'app.settings.game-options.setting.allow-server-listing.label',
+ defaultMessage: 'Server listings',
+ },
+ allowServerListingDescription: {
+ id: 'app.settings.game-options.setting.allow-server-listing.description',
+ defaultMessage: "Allow the player's name to appear in server listings.",
+ },
+ realmsNotificationsLabel: {
+ id: 'app.settings.game-options.setting.realms-notifications.label',
+ defaultMessage: 'Realms notifications',
+ },
+})
+
+const catalogSettingMessages = defineMessages({
+ brightnessLabel: {
+ id: 'app.settings.game-options.setting.brightness.label',
+ defaultMessage: 'Brightness',
+ },
+ legacyViewDistanceLabel: {
+ id: 'app.settings.game-options.setting.legacy-view-distance.label',
+ defaultMessage: 'View distance',
+ },
+ entityDistanceLabel: {
+ id: 'app.settings.game-options.setting.entity-distance.label',
+ defaultMessage: 'Entity distance',
+ },
+ debugGuiScaleLabel: {
+ id: 'app.settings.game-options.setting.debug-gui-scale.label',
+ defaultMessage: 'Debug GUI scale',
+ },
+ graphicsBackendLabel: {
+ id: 'app.settings.game-options.setting.graphics-backend.label',
+ defaultMessage: 'Graphics backend',
+ },
+ cloudRangeLabel: {
+ id: 'app.settings.game-options.setting.cloud-range.label',
+ defaultMessage: 'Cloud distance',
+ },
+ exclusiveFullscreenLabel: {
+ id: 'app.settings.game-options.setting.exclusive-fullscreen.label',
+ defaultMessage: 'Exclusive fullscreen',
+ },
+ macFullscreenMenuLabel: {
+ id: 'app.settings.game-options.setting.mac-fullscreen-menu.label',
+ defaultMessage: 'Show macOS menu in fullscreen',
+ },
+ legacyFramerateLimitLabel: {
+ id: 'app.settings.game-options.setting.legacy-framerate-limit.label',
+ defaultMessage: 'Framerate limit',
+ },
+ inactivityFramerateLimitLabel: {
+ id: 'app.settings.game-options.setting.inactivity-framerate-limit.label',
+ defaultMessage: 'Reduced framerate',
+ },
+ prioritizeChunkUpdatesLabel: {
+ id: 'app.settings.game-options.setting.prioritize-chunk-updates.label',
+ defaultMessage: 'Prioritize chunk updates',
+ },
+ attackIndicatorLabel: {
+ id: 'app.settings.game-options.setting.attack-indicator.label',
+ defaultMessage: 'Attack indicator',
+ },
+ reducedDebugInfoLabel: {
+ id: 'app.settings.game-options.setting.reduced-debug-info.label',
+ defaultMessage: 'Reduced debug information',
+ },
+ chunkFadeTimeLabel: {
+ id: 'app.settings.game-options.setting.chunk-fade-time.label',
+ defaultMessage: 'Chunk fade time',
+ },
+ cutoutLeavesLabel: {
+ id: 'app.settings.game-options.setting.cutout-leaves.label',
+ defaultMessage: 'Cutout leaves',
+ },
+ improvedTransparencyLabel: {
+ id: 'app.settings.game-options.setting.improved-transparency.label',
+ defaultMessage: 'Improved transparency',
+ },
+ textureFilteringLabel: {
+ id: 'app.settings.game-options.setting.texture-filtering.label',
+ defaultMessage: 'Texture filtering',
+ },
+ anisotropyLabel: {
+ id: 'app.settings.game-options.setting.anisotropy.label',
+ defaultMessage: 'Anisotropy',
+ },
+ vignetteLabel: {
+ id: 'app.settings.game-options.setting.vignette.label',
+ defaultMessage: 'Vignette',
+ },
+ weatherRadiusLabel: {
+ id: 'app.settings.game-options.setting.weather-radius.label',
+ defaultMessage: 'Weather radius',
+ },
+ advancedOpenGlLabel: {
+ id: 'app.settings.game-options.setting.advanced-opengl.label',
+ defaultMessage: 'Advanced OpenGL',
+ },
+ anaglyph3dLabel: {
+ id: 'app.settings.game-options.setting.anaglyph-3d.label',
+ defaultMessage: '3D anaglyph',
+ },
+ anisotropicFilteringLabel: {
+ id: 'app.settings.game-options.setting.anisotropic-filtering.label',
+ defaultMessage: 'Anisotropic filtering',
+ },
+ alternateBlocksLabel: {
+ id: 'app.settings.game-options.setting.alternate-blocks.label',
+ defaultMessage: 'Alternate blocks',
+ },
+ heldItemTooltipsLabel: {
+ id: 'app.settings.game-options.setting.held-item-tooltips.label',
+ defaultMessage: 'Held item tooltips',
+ },
+ useVboLabel: {
+ id: 'app.settings.game-options.setting.use-vbo.label',
+ defaultMessage: 'Use VBOs',
+ },
+ forceUnicodeFontLabel: {
+ id: 'app.settings.game-options.setting.force-unicode-font.label',
+ defaultMessage: 'Force Unicode font',
+ },
+ japaneseGlyphVariantsLabel: {
+ id: 'app.settings.game-options.setting.japanese-glyph-variants.label',
+ defaultMessage: 'Japanese glyph variants',
+ },
+ musicFrequencyLabel: {
+ id: 'app.settings.game-options.setting.music-frequency.label',
+ defaultMessage: 'Music frequency',
+ },
+ directionalAudioLabel: {
+ id: 'app.settings.game-options.setting.directional-audio.label',
+ defaultMessage: 'Directional audio',
+ },
+ invertHorizontalMouseLabel: {
+ id: 'app.settings.game-options.setting.invert-horizontal-mouse.label',
+ defaultMessage: 'Invert horizontal mouse',
+ },
+ toggleAttackLabel: {
+ id: 'app.settings.game-options.setting.toggle-attack.label',
+ defaultMessage: 'Toggle attack',
+ },
+ toggleUseLabel: {
+ id: 'app.settings.game-options.setting.toggle-use.label',
+ defaultMessage: 'Toggle use',
+ },
+ mouseWheelSensitivityLabel: {
+ id: 'app.settings.game-options.setting.mouse-wheel-sensitivity.label',
+ defaultMessage: 'Mouse wheel sensitivity',
+ },
+ rawMouseInputLabel: {
+ id: 'app.settings.game-options.setting.raw-mouse-input.label',
+ defaultMessage: 'Raw mouse input',
+ },
+ touchscreenLabel: {
+ id: 'app.settings.game-options.setting.touchscreen.label',
+ defaultMessage: 'Touchscreen mode',
+ },
+ allowCursorChangesLabel: {
+ id: 'app.settings.game-options.setting.allow-cursor-changes.label',
+ defaultMessage: 'Allow cursor changes',
+ },
+ sprintWindowLabel: {
+ id: 'app.settings.game-options.setting.sprint-window.label',
+ defaultMessage: 'Sprint window',
+ },
+ operatorItemsTabLabel: {
+ id: 'app.settings.game-options.setting.operator-items-tab.label',
+ defaultMessage: 'Operator items tab',
+ },
+ ctrlClickRightClickLabel: {
+ id: 'app.settings.game-options.setting.ctrl-click-right-click.label',
+ defaultMessage: 'Control-click as right-click',
+ },
+ quitShortcutsLabel: {
+ id: 'app.settings.game-options.setting.quit-shortcuts.label',
+ defaultMessage: 'Quit shortcuts',
+ },
+ chatWidthLabel: {
+ id: 'app.settings.game-options.setting.chat-width.label',
+ defaultMessage: 'Chat width',
+ },
+ focusedChatHeightLabel: {
+ id: 'app.settings.game-options.setting.focused-chat-height.label',
+ defaultMessage: 'Focused chat height',
+ },
+ unfocusedChatHeightLabel: {
+ id: 'app.settings.game-options.setting.unfocused-chat-height.label',
+ defaultMessage: 'Unfocused chat height',
+ },
+ chatLineSpacingLabel: {
+ id: 'app.settings.game-options.setting.chat-line-spacing.label',
+ defaultMessage: 'Chat line spacing',
+ },
+ chatDelayLabel: {
+ id: 'app.settings.game-options.setting.chat-delay.label',
+ defaultMessage: 'Chat delay',
+ },
+ textBackgroundOpacityLabel: {
+ id: 'app.settings.game-options.setting.text-background-opacity.label',
+ defaultMessage: 'Text background opacity',
+ },
+ chatBackgroundOnlyLabel: {
+ id: 'app.settings.game-options.setting.chat-background-only.label',
+ defaultMessage: 'Chat background only',
+ },
+ autoSuggestionsLabel: {
+ id: 'app.settings.game-options.setting.auto-suggestions.label',
+ defaultMessage: 'Command suggestions',
+ },
+ secureChatOnlyLabel: {
+ id: 'app.settings.game-options.setting.secure-chat-only.label',
+ defaultMessage: 'Only show secure chat',
+ },
+ saveChatDraftsLabel: {
+ id: 'app.settings.game-options.setting.save-chat-drafts.label',
+ defaultMessage: 'Save chat drafts',
+ },
+ hideMatchedNamesLabel: {
+ id: 'app.settings.game-options.setting.hide-matched-names.label',
+ defaultMessage: 'Hide matched names',
+ },
+ chatPreviewLabel: {
+ id: 'app.settings.game-options.setting.chat-preview.label',
+ defaultMessage: 'Chat preview',
+ },
+ fovEffectsLabel: {
+ id: 'app.settings.game-options.setting.fov-effects.label',
+ defaultMessage: 'FOV effects',
+ },
+ screenEffectsLabel: {
+ id: 'app.settings.game-options.setting.screen-effects.label',
+ defaultMessage: 'Screen effects',
+ },
+ darknessPulsingLabel: {
+ id: 'app.settings.game-options.setting.darkness-pulsing.label',
+ defaultMessage: 'Darkness pulsing',
+ },
+ damageTiltLabel: {
+ id: 'app.settings.game-options.setting.damage-tilt.label',
+ defaultMessage: 'Damage tilt',
+ },
+ glintSpeedLabel: {
+ id: 'app.settings.game-options.setting.glint-speed.label',
+ defaultMessage: 'Glint speed',
+ },
+ glintStrengthLabel: {
+ id: 'app.settings.game-options.setting.glint-strength.label',
+ defaultMessage: 'Glint strength',
+ },
+ hideLightningFlashesLabel: {
+ id: 'app.settings.game-options.setting.hide-lightning-flashes.label',
+ defaultMessage: 'Hide lightning flashes',
+ },
+ hideSplashTextsLabel: {
+ id: 'app.settings.game-options.setting.hide-splash-texts.label',
+ defaultMessage: 'Hide splash texts',
+ },
+ highContrastOutlineLabel: {
+ id: 'app.settings.game-options.setting.high-contrast-outline.label',
+ defaultMessage: 'High contrast block outline',
+ },
+ narratorHotkeyLabel: {
+ id: 'app.settings.game-options.setting.narrator-hotkey.label',
+ defaultMessage: 'Narrator hotkey',
+ },
+ autosaveIndicatorLabel: {
+ id: 'app.settings.game-options.setting.autosave-indicator.label',
+ defaultMessage: 'Autosave indicator',
+ },
+ panoramaSpeedLabel: {
+ id: 'app.settings.game-options.setting.panorama-speed.label',
+ defaultMessage: 'Panorama speed',
+ },
+ menuBackgroundBlurLabel: {
+ id: 'app.settings.game-options.setting.menu-background-blur.label',
+ defaultMessage: 'Menu background blur',
+ },
+ rotateWithMinecartLabel: {
+ id: 'app.settings.game-options.setting.rotate-with-minecart.label',
+ defaultMessage: 'Rotate with minecart',
+ },
+ leftSleeveLabel: {
+ id: 'app.settings.game-options.setting.left-sleeve.label',
+ defaultMessage: 'Left sleeve',
+ },
+ rightSleeveLabel: {
+ id: 'app.settings.game-options.setting.right-sleeve.label',
+ defaultMessage: 'Right sleeve',
+ },
+ leftPantsLegLabel: {
+ id: 'app.settings.game-options.setting.left-pants-leg.label',
+ defaultMessage: 'Left pants leg',
+ },
+ rightPantsLegLabel: {
+ id: 'app.settings.game-options.setting.right-pants-leg.label',
+ defaultMessage: 'Right pants leg',
+ },
+ hideServerAddressLabel: {
+ id: 'app.settings.game-options.setting.hide-server-address.label',
+ defaultMessage: 'Hide server address',
+ },
+ serverTexturesLabel: {
+ id: 'app.settings.game-options.setting.server-textures.label',
+ defaultMessage: 'Server textures',
+ },
+ snooperLabel: {
+ id: 'app.settings.game-options.setting.snooper.label',
+ defaultMessage: 'Snooper',
+ },
+ extraTelemetryLabel: {
+ id: 'app.settings.game-options.setting.extra-telemetry.label',
+ defaultMessage: 'Optional telemetry',
+ },
+ inGameNotificationsLabel: {
+ id: 'app.settings.game-options.setting.in-game-notifications.label',
+ defaultMessage: 'In-game notifications',
+ },
+ sharePresenceLabel: {
+ id: 'app.settings.game-options.setting.share-presence.label',
+ defaultMessage: 'Share presence',
+ },
+})
+
+const catalogKeyMessages = defineMessages({
+ smoothCameraLabel: {
+ id: 'app.settings.game-options.setting.key-smooth-camera.label',
+ defaultMessage: 'Toggle cinematic camera',
+ },
+ spectatorOutlinesLabel: {
+ id: 'app.settings.game-options.setting.key-spectator-outlines.label',
+ defaultMessage: 'Highlight spectators',
+ },
+ saveToolbarLabel: {
+ id: 'app.settings.game-options.setting.key-save-toolbar.label',
+ defaultMessage: 'Save toolbar',
+ },
+ loadToolbarLabel: {
+ id: 'app.settings.game-options.setting.key-load-toolbar.label',
+ defaultMessage: 'Load toolbar',
+ },
+ socialInteractionsLabel: {
+ id: 'app.settings.game-options.setting.key-social-interactions.label',
+ defaultMessage: 'Social interactions',
+ },
+ quickActionsLabel: {
+ id: 'app.settings.game-options.setting.key-quick-actions.label',
+ defaultMessage: 'Quick actions',
+ },
+ spectatorHotbarLabel: {
+ id: 'app.settings.game-options.setting.key-spectator-hotbar.label',
+ defaultMessage: 'Spectator hotbar',
+ },
+ friendsLabel: {
+ id: 'app.settings.game-options.setting.key-friends.label',
+ defaultMessage: 'Friends',
+ },
+ toggleGuiLabel: {
+ id: 'app.settings.game-options.setting.key-toggle-gui.label',
+ defaultMessage: 'Toggle HUD',
+ },
+ toggleSpectatorShaderLabel: {
+ id: 'app.settings.game-options.setting.key-toggle-spectator-shader.label',
+ defaultMessage: 'Toggle spectator shader',
+ },
+ hotbar1Label: {
+ id: 'app.settings.game-options.setting.key-hotbar-1.label',
+ defaultMessage: 'Hotbar 1',
+ },
+ hotbar2Label: {
+ id: 'app.settings.game-options.setting.key-hotbar-2.label',
+ defaultMessage: 'Hotbar 2',
+ },
+ hotbar3Label: {
+ id: 'app.settings.game-options.setting.key-hotbar-3.label',
+ defaultMessage: 'Hotbar 3',
+ },
+ hotbar4Label: {
+ id: 'app.settings.game-options.setting.key-hotbar-4.label',
+ defaultMessage: 'Hotbar 4',
+ },
+ hotbar5Label: {
+ id: 'app.settings.game-options.setting.key-hotbar-5.label',
+ defaultMessage: 'Hotbar 5',
+ },
+ hotbar6Label: {
+ id: 'app.settings.game-options.setting.key-hotbar-6.label',
+ defaultMessage: 'Hotbar 6',
+ },
+ hotbar7Label: {
+ id: 'app.settings.game-options.setting.key-hotbar-7.label',
+ defaultMessage: 'Hotbar 7',
+ },
+ hotbar8Label: {
+ id: 'app.settings.game-options.setting.key-hotbar-8.label',
+ defaultMessage: 'Hotbar 8',
+ },
+ hotbar9Label: {
+ id: 'app.settings.game-options.setting.key-hotbar-9.label',
+ defaultMessage: 'Hotbar 9',
+ },
+ debugOverlayLabel: {
+ id: 'app.settings.game-options.setting.key-debug-overlay.label',
+ defaultMessage: 'Debug overlay',
+ },
+ debugModifierLabel: {
+ id: 'app.settings.game-options.setting.key-debug-modifier.label',
+ defaultMessage: 'Debug modifier',
+ },
+ debugReloadChunksLabel: {
+ id: 'app.settings.game-options.setting.key-debug-reload-chunks.label',
+ defaultMessage: 'Reload chunks',
+ },
+ debugHitboxesLabel: {
+ id: 'app.settings.game-options.setting.key-debug-hitboxes.label',
+ defaultMessage: 'Show hitboxes',
+ },
+ debugClearChatLabel: {
+ id: 'app.settings.game-options.setting.key-debug-clear-chat.label',
+ defaultMessage: 'Clear chat',
+ },
+ debugCrashLabel: {
+ id: 'app.settings.game-options.setting.key-debug-crash.label',
+ defaultMessage: 'Trigger debug crash',
+ },
+ debugChunkBordersLabel: {
+ id: 'app.settings.game-options.setting.key-debug-chunk-borders.label',
+ defaultMessage: 'Show chunk borders',
+ },
+ debugAdvancedTooltipsLabel: {
+ id: 'app.settings.game-options.setting.key-debug-advanced-tooltips.label',
+ defaultMessage: 'Show advanced tooltips',
+ },
+ debugCopyRecreateCommandLabel: {
+ id: 'app.settings.game-options.setting.key-debug-copy-recreate-command.label',
+ defaultMessage: 'Copy recreate command',
+ },
+ debugSpectateLabel: {
+ id: 'app.settings.game-options.setting.key-debug-spectate.label',
+ defaultMessage: 'Spectate entity',
+ },
+ debugSwitchGameModeLabel: {
+ id: 'app.settings.game-options.setting.key-debug-switch-game-mode.label',
+ defaultMessage: 'Switch game mode',
+ },
+ debugOptionsLabel: {
+ id: 'app.settings.game-options.setting.key-debug-options.label',
+ defaultMessage: 'Debug options',
+ },
+ debugFocusPauseLabel: {
+ id: 'app.settings.game-options.setting.key-debug-focus-pause.label',
+ defaultMessage: 'Pause on lost focus',
+ },
+ debugDumpDynamicTexturesLabel: {
+ id: 'app.settings.game-options.setting.key-debug-dump-dynamic-textures.label',
+ defaultMessage: 'Dump dynamic textures',
+ },
+ debugReloadResourcePacksLabel: {
+ id: 'app.settings.game-options.setting.key-debug-reload-resource-packs.label',
+ defaultMessage: 'Reload resource packs',
+ },
+ debugProfilingLabel: {
+ id: 'app.settings.game-options.setting.key-debug-profiling.label',
+ defaultMessage: 'Start profiling',
+ },
+ debugCopyLocationLabel: {
+ id: 'app.settings.game-options.setting.key-debug-copy-location.label',
+ defaultMessage: 'Copy location',
+ },
+ debugDumpVersionLabel: {
+ id: 'app.settings.game-options.setting.key-debug-dump-version.label',
+ defaultMessage: 'Dump version',
+ },
+ debugProfilingChartLabel: {
+ id: 'app.settings.game-options.setting.key-debug-profiling-chart.label',
+ defaultMessage: 'Profiling chart',
+ },
+ debugFpsChartsLabel: {
+ id: 'app.settings.game-options.setting.key-debug-fps-charts.label',
+ defaultMessage: 'FPS charts',
+ },
+ debugNetworkChartsLabel: {
+ id: 'app.settings.game-options.setting.key-debug-network-charts.label',
+ defaultMessage: 'Network charts',
+ },
+ debugLightmapTextureLabel: {
+ id: 'app.settings.game-options.setting.key-debug-lightmap-texture.label',
+ defaultMessage: 'Lightmap texture',
+ },
+ debugImprovedTransparencyLabel: {
+ id: 'app.settings.game-options.setting.key-debug-improved-transparency.label',
+ defaultMessage: 'Improved transparency debug view',
+ },
+})
+
+const categoryMessages = defineMessages({
+ skinCustomizationLabel: {
+ id: 'app.settings.game-options.category.skin-customization.label',
+ defaultMessage: 'Skin customization',
+ },
+ skinCustomizationDescription: {
+ id: 'app.settings.game-options.category.skin-customization.description',
+ defaultMessage: 'Skin layers and main hand',
+ },
+ videoLabel: { id: 'app.settings.game-options.category.video.label', defaultMessage: 'Video' },
+ videoDescription: {
+ id: 'app.settings.game-options.category.video.description',
+ defaultMessage: 'Camera and display settings',
+ },
+ languageLabel: {
+ id: 'app.settings.game-options.category.language.label',
+ defaultMessage: 'Language',
+ },
+ languageDescription: {
+ id: 'app.settings.game-options.category.language.description',
+ defaultMessage: 'Game language',
+ },
+ musicAndSoundLabel: {
+ id: 'app.settings.game-options.category.music-and-sound.label',
+ defaultMessage: 'Music and sound',
+ },
+ musicAndSoundDescription: {
+ id: 'app.settings.game-options.category.music-and-sound.description',
+ defaultMessage: 'Volume and audio preferences',
+ },
+ controlsLabel: {
+ id: 'app.settings.game-options.category.controls.label',
+ defaultMessage: 'Controls',
+ },
+ controlsDescription: {
+ id: 'app.settings.game-options.category.controls.description',
+ defaultMessage: 'Mouse, movement, and key bindings',
+ },
+ chatLabel: { id: 'app.settings.game-options.category.chat.label', defaultMessage: 'Chat' },
+ chatDescription: {
+ id: 'app.settings.game-options.category.chat.description',
+ defaultMessage: 'Chat visibility and appearance',
+ },
+ accessibilityLabel: {
+ id: 'app.settings.game-options.category.accessibility.label',
+ defaultMessage: 'Accessibility',
+ },
+ accessibilityDescription: {
+ id: 'app.settings.game-options.category.accessibility.description',
+ defaultMessage: 'Accessibility preferences',
+ },
+ onlineLabel: { id: 'app.settings.game-options.category.online.label', defaultMessage: 'Online' },
+ onlineDescription: {
+ id: 'app.settings.game-options.category.online.description',
+ defaultMessage: 'Online and Realms preferences',
+ },
+ customLabel: {
+ id: 'app.settings.game-options.category.custom.label',
+ defaultMessage: 'Custom settings',
+ },
+ customDescription: {
+ id: 'app.settings.game-options.category.custom.description',
+ defaultMessage: 'Settings added by mods',
+ },
+})
+
+const choiceMessages = defineMessages({
+ fast: { id: 'app.settings.game-options.choice.fast', defaultMessage: 'Fast' },
+ fancy: { id: 'app.settings.game-options.choice.fancy', defaultMessage: 'Fancy' },
+ fabulous: { id: 'app.settings.game-options.choice.fabulous', defaultMessage: 'Fabulous' },
+ custom: { id: 'app.settings.game-options.choice.custom', defaultMessage: 'Custom' },
+ left: { id: 'app.settings.game-options.choice.left', defaultMessage: 'Left' },
+ right: { id: 'app.settings.game-options.choice.right', defaultMessage: 'Right' },
+ shown: { id: 'app.settings.game-options.choice.shown', defaultMessage: 'Shown' },
+ commandsOnly: {
+ id: 'app.settings.game-options.choice.commands-only',
+ defaultMessage: 'Commands only',
+ },
+ hidden: { id: 'app.settings.game-options.choice.hidden', defaultMessage: 'Hidden' },
+ all: { id: 'app.settings.game-options.choice.all', defaultMessage: 'All' },
+ decreased: { id: 'app.settings.game-options.choice.decreased', defaultMessage: 'Decreased' },
+ minimal: { id: 'app.settings.game-options.choice.minimal', defaultMessage: 'Minimal' },
+ off: { id: 'app.settings.game-options.choice.off', defaultMessage: 'Off' },
+ chat: { id: 'app.settings.game-options.choice.chat', defaultMessage: 'Chat' },
+ system: { id: 'app.settings.game-options.choice.system', defaultMessage: 'System' },
+ on: { id: 'app.settings.game-options.choice.on', defaultMessage: 'On' },
+ minimum: { id: 'app.settings.game-options.choice.minimum', defaultMessage: 'Minimum' },
+ maximum: { id: 'app.settings.game-options.choice.maximum', defaultMessage: 'Maximum' },
+ never: { id: 'app.settings.game-options.choice.never', defaultMessage: 'Never' },
+ pause: { id: 'app.settings.game-options.choice.pause', defaultMessage: 'Pause menu' },
+ pauseAndToast: {
+ id: 'app.settings.game-options.choice.pause-and-toast',
+ defaultMessage: 'Pause menu and toast',
+ },
+ far: { id: 'app.settings.game-options.choice.far', defaultMessage: 'Far' },
+ normal: { id: 'app.settings.game-options.choice.normal', defaultMessage: 'Normal' },
+ short: { id: 'app.settings.game-options.choice.short', defaultMessage: 'Short' },
+ tiny: { id: 'app.settings.game-options.choice.tiny', defaultMessage: 'Tiny' },
+ maxFps: { id: 'app.settings.game-options.choice.max-fps', defaultMessage: 'Max FPS' },
+ balanced: { id: 'app.settings.game-options.choice.balanced', defaultMessage: 'Balanced' },
+ powerSaver: { id: 'app.settings.game-options.choice.power-saver', defaultMessage: 'Power saver' },
+ whileAfk: { id: 'app.settings.game-options.choice.while-afk', defaultMessage: 'While AFK' },
+ whenMinimized: {
+ id: 'app.settings.game-options.choice.when-minimized',
+ defaultMessage: 'When minimized',
+ },
+ none: { id: 'app.settings.game-options.choice.none', defaultMessage: 'None' },
+ byPlayer: { id: 'app.settings.game-options.choice.by-player', defaultMessage: 'By player' },
+ nearby: { id: 'app.settings.game-options.choice.nearby', defaultMessage: 'Nearby' },
+ crosshair: { id: 'app.settings.game-options.choice.crosshair', defaultMessage: 'Crosshair' },
+ hotbar: { id: 'app.settings.game-options.choice.hotbar', defaultMessage: 'Hotbar' },
+ constant: { id: 'app.settings.game-options.choice.constant', defaultMessage: 'Constant' },
+ default: { id: 'app.settings.game-options.choice.default', defaultMessage: 'Default' },
+ frequent: { id: 'app.settings.game-options.choice.frequent', defaultMessage: 'Frequent' },
+ limited: { id: 'app.settings.game-options.choice.limited', defaultMessage: 'Limited' },
+ openGl: { id: 'app.settings.game-options.choice.opengl', defaultMessage: 'OpenGL' },
+ vulkan: { id: 'app.settings.game-options.choice.vulkan', defaultMessage: 'Vulkan' },
+})
+
+export const presentationMessages = defineMessages({
+ customValuePlaceholder: {
+ id: 'app.settings.game-options.custom-value.placeholder',
+ defaultMessage: 'Enter a value',
+ },
+ validationMissingValue: {
+ id: 'app.settings.game-options.validation.missing-value',
+ defaultMessage: 'Choose a value first.',
+ },
+ validationNoCompatibleInstances: {
+ id: 'app.settings.game-options.validation.no-compatible-instances',
+ defaultMessage: 'Some of your instances cannot use this setting',
+ },
+ validationInvalidValue: {
+ id: 'app.settings.game-options.validation.invalid-value',
+ defaultMessage: 'Choose a valid value.',
+ },
+ validationChangedSinceOpened: {
+ id: 'app.settings.game-options.validation.changed-since-opened',
+ defaultMessage: 'This setting changed elsewhere. Check it and try again.',
+ },
+ compatibilityNone: {
+ id: 'app.settings.game-options.compatibility.none',
+ defaultMessage: 'Some of your instances cannot use this setting',
+ },
+ bucketLauncherControlled: {
+ id: 'app.settings.game-options.compatibility.reason.launcher-controlled',
+ defaultMessage: 'This setting is managed by Modrinth’s launch settings.',
+ },
+})
+
+const knownSettings: Record =
+ {
+ fov: { label: settingMessages.fovLabel },
+ graphics: {
+ label: settingMessages.graphicsLabel,
+ description: settingMessages.graphicsDescription,
+ },
+ ambient_occlusion: { label: settingMessages.ambientOcclusionLabel },
+ render_distance: { label: settingMessages.renderDistanceLabel },
+ simulation_distance: {
+ label: settingMessages.simulationDistanceLabel,
+ description: settingMessages.simulationDistanceDescription,
+ },
+ gui_scale: {
+ label: settingMessages.guiScaleLabel,
+ description: settingMessages.guiScaleDescription,
+ },
+ particles: { label: settingMessages.particlesLabel },
+ clouds: { label: settingMessages.cloudsLabel },
+ entity_shadows: { label: settingMessages.entityShadowsLabel },
+ view_bobbing: {
+ label: settingMessages.viewBobbingLabel,
+ description: settingMessages.viewBobbingDescription,
+ },
+ vsync: { label: settingMessages.vsyncLabel, description: settingMessages.vsyncDescription },
+ fullscreen: { label: settingMessages.fullscreenLabel },
+ max_framerate: { label: settingMessages.maxFramerateLabel },
+ mipmap_levels: {
+ label: settingMessages.mipmapLevelsLabel,
+ description: settingMessages.mipmapLevelsDescription,
+ },
+ biome_blend_radius: {
+ label: settingMessages.biomeBlendRadiusLabel,
+ description: settingMessages.biomeBlendRadiusDescription,
+ },
+ language: { label: settingMessages.languageLabel },
+ master_volume: { label: settingMessages.masterVolumeLabel },
+ music_volume: { label: settingMessages.musicVolumeLabel },
+ music_toast: {
+ label: settingMessages.musicToastLabel,
+ description: settingMessages.musicToastDescription,
+ },
+ record_volume: { label: settingMessages.recordVolumeLabel },
+ weather_volume: { label: settingMessages.weatherVolumeLabel },
+ blocks_volume: { label: settingMessages.blocksVolumeLabel },
+ hostile_volume: { label: settingMessages.hostileVolumeLabel },
+ neutral_volume: { label: settingMessages.neutralVolumeLabel },
+ players_volume: { label: settingMessages.playersVolumeLabel },
+ ambient_volume: { label: settingMessages.ambientVolumeLabel },
+ voice_volume: { label: settingMessages.voiceVolumeLabel },
+ ui_volume: { label: settingMessages.uiVolumeLabel },
+ sensitivity: { label: settingMessages.sensitivityLabel },
+ invert_mouse: {
+ label: settingMessages.invertMouseLabel,
+ description: settingMessages.invertMouseDescription,
+ },
+ auto_jump: {
+ label: settingMessages.autoJumpLabel,
+ description: settingMessages.autoJumpDescription,
+ },
+ toggle_crouch: {
+ label: settingMessages.toggleCrouchLabel,
+ description: settingMessages.toggleCrouchDescription,
+ },
+ toggle_sprint: {
+ label: settingMessages.toggleSprintLabel,
+ description: settingMessages.toggleSprintDescription,
+ },
+ discrete_mouse_scroll: {
+ label: settingMessages.discreteMouseScrollLabel,
+ description: settingMessages.discreteMouseScrollDescription,
+ },
+ 'key.forward': { label: settingMessages.keyForwardLabel },
+ 'key.left': { label: settingMessages.keyLeftLabel },
+ 'key.back': { label: settingMessages.keyBackLabel },
+ 'key.right': { label: settingMessages.keyRightLabel },
+ 'key.jump': { label: settingMessages.keyJumpLabel },
+ 'key.sneak': { label: settingMessages.keySneakLabel },
+ 'key.sprint': { label: settingMessages.keySprintLabel },
+ 'key.inventory': { label: settingMessages.keyInventoryLabel },
+ 'key.swap_offhand': { label: settingMessages.keySwapOffhandLabel },
+ 'key.drop': { label: settingMessages.keyDropLabel },
+ 'key.use': { label: settingMessages.keyUseLabel },
+ 'key.attack': { label: settingMessages.keyAttackLabel },
+ 'key.pick_item': { label: settingMessages.keyPickItemLabel },
+ 'key.chat': { label: settingMessages.keyChatLabel },
+ 'key.player_list': { label: settingMessages.keyPlayerListLabel },
+ 'key.command': { label: settingMessages.keyCommandLabel },
+ 'key.screenshot': { label: settingMessages.keyScreenshotLabel },
+ 'key.perspective': { label: settingMessages.keyPerspectiveLabel },
+ 'key.fullscreen': { label: settingMessages.keyFullscreenLabel },
+ 'key.advancements': { label: settingMessages.keyAdvancementsLabel },
+ chat_visibility: { label: settingMessages.chatVisibilityLabel },
+ chat_colors: { label: settingMessages.chatColorsLabel },
+ chat_links: {
+ label: settingMessages.chatLinksLabel,
+ description: settingMessages.chatLinksDescription,
+ },
+ chat_links_prompt: {
+ label: settingMessages.chatLinksPromptLabel,
+ description: settingMessages.chatLinksPromptDescription,
+ },
+ chat_opacity: {
+ label: settingMessages.chatOpacityLabel,
+ description: settingMessages.chatOpacityDescription,
+ },
+ chat_scale: { label: settingMessages.chatScaleLabel },
+ narrator: {
+ label: settingMessages.narratorLabel,
+ description: settingMessages.narratorDescription,
+ },
+ subtitles: {
+ label: settingMessages.subtitlesLabel,
+ description: settingMessages.subtitlesDescription,
+ },
+ high_contrast: {
+ label: settingMessages.highContrastLabel,
+ description: settingMessages.highContrastDescription,
+ },
+ dark_splash: {
+ label: settingMessages.darkSplashLabel,
+ description: settingMessages.darkSplashDescription,
+ },
+ notification_time: {
+ label: settingMessages.notificationTimeLabel,
+ description: settingMessages.notificationTimeDescription,
+ },
+ main_hand: {
+ label: settingMessages.mainHandLabel,
+ description: settingMessages.mainHandDescription,
+ },
+ cape: { label: settingMessages.capeLabel, description: settingMessages.capeDescription },
+ hat: { label: settingMessages.hatLabel, description: settingMessages.hatDescription },
+ jacket: { label: settingMessages.jacketLabel, description: settingMessages.jacketDescription },
+ allow_server_listing: {
+ label: settingMessages.allowServerListingLabel,
+ description: settingMessages.allowServerListingDescription,
+ },
+ realms_notifications: { label: settingMessages.realmsNotificationsLabel },
+ brightness: { label: catalogSettingMessages.brightnessLabel },
+ legacy_view_distance: { label: catalogSettingMessages.legacyViewDistanceLabel },
+ entity_distance: { label: catalogSettingMessages.entityDistanceLabel },
+ debug_gui_scale: { label: catalogSettingMessages.debugGuiScaleLabel },
+ graphics_backend: { label: catalogSettingMessages.graphicsBackendLabel },
+ cloud_range: { label: catalogSettingMessages.cloudRangeLabel },
+ exclusive_fullscreen: { label: catalogSettingMessages.exclusiveFullscreenLabel },
+ mac_fullscreen_menu: { label: catalogSettingMessages.macFullscreenMenuLabel },
+ legacy_framerate_limit: { label: catalogSettingMessages.legacyFramerateLimitLabel },
+ inactivity_framerate_limit: {
+ label: catalogSettingMessages.inactivityFramerateLimitLabel,
+ },
+ prioritize_chunk_updates: { label: catalogSettingMessages.prioritizeChunkUpdatesLabel },
+ attack_indicator: { label: catalogSettingMessages.attackIndicatorLabel },
+ reduced_debug_info: { label: catalogSettingMessages.reducedDebugInfoLabel },
+ chunk_fade_time: { label: catalogSettingMessages.chunkFadeTimeLabel },
+ cutout_leaves: { label: catalogSettingMessages.cutoutLeavesLabel },
+ improved_transparency: { label: catalogSettingMessages.improvedTransparencyLabel },
+ texture_filtering: { label: catalogSettingMessages.textureFilteringLabel },
+ anisotropy: { label: catalogSettingMessages.anisotropyLabel },
+ vignette: { label: catalogSettingMessages.vignetteLabel },
+ weather_radius: { label: catalogSettingMessages.weatherRadiusLabel },
+ advanced_opengl: { label: catalogSettingMessages.advancedOpenGlLabel },
+ anaglyph_3d: { label: catalogSettingMessages.anaglyph3dLabel },
+ anisotropic_filtering: { label: catalogSettingMessages.anisotropicFilteringLabel },
+ alternate_blocks: { label: catalogSettingMessages.alternateBlocksLabel },
+ held_item_tooltips: { label: catalogSettingMessages.heldItemTooltipsLabel },
+ use_vbo: { label: catalogSettingMessages.useVboLabel },
+ force_unicode_font: { label: catalogSettingMessages.forceUnicodeFontLabel },
+ japanese_glyph_variants: { label: catalogSettingMessages.japaneseGlyphVariantsLabel },
+ music_frequency: { label: catalogSettingMessages.musicFrequencyLabel },
+ directional_audio: { label: catalogSettingMessages.directionalAudioLabel },
+ invert_horizontal_mouse: { label: catalogSettingMessages.invertHorizontalMouseLabel },
+ toggle_attack: { label: catalogSettingMessages.toggleAttackLabel },
+ toggle_use: { label: catalogSettingMessages.toggleUseLabel },
+ mouse_wheel_sensitivity: { label: catalogSettingMessages.mouseWheelSensitivityLabel },
+ raw_mouse_input: { label: catalogSettingMessages.rawMouseInputLabel },
+ touchscreen: { label: catalogSettingMessages.touchscreenLabel },
+ allow_cursor_changes: { label: catalogSettingMessages.allowCursorChangesLabel },
+ sprint_window: { label: catalogSettingMessages.sprintWindowLabel },
+ operator_items_tab: { label: catalogSettingMessages.operatorItemsTabLabel },
+ ctrl_click_right_click: { label: catalogSettingMessages.ctrlClickRightClickLabel },
+ quit_shortcuts: { label: catalogSettingMessages.quitShortcutsLabel },
+ chat_width: { label: catalogSettingMessages.chatWidthLabel },
+ focused_chat_height: { label: catalogSettingMessages.focusedChatHeightLabel },
+ unfocused_chat_height: { label: catalogSettingMessages.unfocusedChatHeightLabel },
+ chat_line_spacing: { label: catalogSettingMessages.chatLineSpacingLabel },
+ chat_delay: { label: catalogSettingMessages.chatDelayLabel },
+ text_background_opacity: { label: catalogSettingMessages.textBackgroundOpacityLabel },
+ chat_background_only: { label: catalogSettingMessages.chatBackgroundOnlyLabel },
+ auto_suggestions: { label: catalogSettingMessages.autoSuggestionsLabel },
+ secure_chat_only: { label: catalogSettingMessages.secureChatOnlyLabel },
+ save_chat_drafts: { label: catalogSettingMessages.saveChatDraftsLabel },
+ hide_matched_names: { label: catalogSettingMessages.hideMatchedNamesLabel },
+ chat_preview: { label: catalogSettingMessages.chatPreviewLabel },
+ fov_effects: { label: catalogSettingMessages.fovEffectsLabel },
+ screen_effects: { label: catalogSettingMessages.screenEffectsLabel },
+ darkness_pulsing: { label: catalogSettingMessages.darknessPulsingLabel },
+ damage_tilt: { label: catalogSettingMessages.damageTiltLabel },
+ glint_speed: { label: catalogSettingMessages.glintSpeedLabel },
+ glint_strength: { label: catalogSettingMessages.glintStrengthLabel },
+ hide_lightning_flashes: { label: catalogSettingMessages.hideLightningFlashesLabel },
+ hide_splash_texts: { label: catalogSettingMessages.hideSplashTextsLabel },
+ high_contrast_outline: { label: catalogSettingMessages.highContrastOutlineLabel },
+ narrator_hotkey: { label: catalogSettingMessages.narratorHotkeyLabel },
+ autosave_indicator: { label: catalogSettingMessages.autosaveIndicatorLabel },
+ panorama_speed: { label: catalogSettingMessages.panoramaSpeedLabel },
+ menu_background_blur: { label: catalogSettingMessages.menuBackgroundBlurLabel },
+ rotate_with_minecart: { label: catalogSettingMessages.rotateWithMinecartLabel },
+ left_sleeve: { label: catalogSettingMessages.leftSleeveLabel },
+ right_sleeve: { label: catalogSettingMessages.rightSleeveLabel },
+ left_pants_leg: { label: catalogSettingMessages.leftPantsLegLabel },
+ right_pants_leg: { label: catalogSettingMessages.rightPantsLegLabel },
+ hide_server_address: { label: catalogSettingMessages.hideServerAddressLabel },
+ server_textures: { label: catalogSettingMessages.serverTexturesLabel },
+ snooper: { label: catalogSettingMessages.snooperLabel },
+ extra_telemetry: { label: catalogSettingMessages.extraTelemetryLabel },
+ in_game_notifications: { label: catalogSettingMessages.inGameNotificationsLabel },
+ share_presence: { label: catalogSettingMessages.sharePresenceLabel },
+ 'key.smooth_camera': { label: catalogKeyMessages.smoothCameraLabel },
+ 'key.spectator_outlines': { label: catalogKeyMessages.spectatorOutlinesLabel },
+ 'key.save_toolbar': { label: catalogKeyMessages.saveToolbarLabel },
+ 'key.load_toolbar': { label: catalogKeyMessages.loadToolbarLabel },
+ 'key.social_interactions': { label: catalogKeyMessages.socialInteractionsLabel },
+ 'key.quick_actions': { label: catalogKeyMessages.quickActionsLabel },
+ 'key.spectator_hotbar': { label: catalogKeyMessages.spectatorHotbarLabel },
+ 'key.friends': { label: catalogKeyMessages.friendsLabel },
+ 'key.toggle_gui': { label: catalogKeyMessages.toggleGuiLabel },
+ 'key.toggle_spectator_shader': { label: catalogKeyMessages.toggleSpectatorShaderLabel },
+ 'key.hotbar.1': { label: catalogKeyMessages.hotbar1Label },
+ 'key.hotbar.2': { label: catalogKeyMessages.hotbar2Label },
+ 'key.hotbar.3': { label: catalogKeyMessages.hotbar3Label },
+ 'key.hotbar.4': { label: catalogKeyMessages.hotbar4Label },
+ 'key.hotbar.5': { label: catalogKeyMessages.hotbar5Label },
+ 'key.hotbar.6': { label: catalogKeyMessages.hotbar6Label },
+ 'key.hotbar.7': { label: catalogKeyMessages.hotbar7Label },
+ 'key.hotbar.8': { label: catalogKeyMessages.hotbar8Label },
+ 'key.hotbar.9': { label: catalogKeyMessages.hotbar9Label },
+ 'key.debug.overlay': { label: catalogKeyMessages.debugOverlayLabel },
+ 'key.debug.modifier': { label: catalogKeyMessages.debugModifierLabel },
+ 'key.debug.reload_chunks': { label: catalogKeyMessages.debugReloadChunksLabel },
+ 'key.debug.hitboxes': { label: catalogKeyMessages.debugHitboxesLabel },
+ 'key.debug.clear_chat': { label: catalogKeyMessages.debugClearChatLabel },
+ 'key.debug.crash': { label: catalogKeyMessages.debugCrashLabel },
+ 'key.debug.chunk_borders': { label: catalogKeyMessages.debugChunkBordersLabel },
+ 'key.debug.advanced_tooltips': {
+ label: catalogKeyMessages.debugAdvancedTooltipsLabel,
+ },
+ 'key.debug.copy_recreate_command': {
+ label: catalogKeyMessages.debugCopyRecreateCommandLabel,
+ },
+ 'key.debug.spectate': { label: catalogKeyMessages.debugSpectateLabel },
+ 'key.debug.switch_game_mode': { label: catalogKeyMessages.debugSwitchGameModeLabel },
+ 'key.debug.options': { label: catalogKeyMessages.debugOptionsLabel },
+ 'key.debug.focus_pause': { label: catalogKeyMessages.debugFocusPauseLabel },
+ 'key.debug.dump_dynamic_textures': {
+ label: catalogKeyMessages.debugDumpDynamicTexturesLabel,
+ },
+ 'key.debug.reload_resource_packs': {
+ label: catalogKeyMessages.debugReloadResourcePacksLabel,
+ },
+ 'key.debug.profiling': { label: catalogKeyMessages.debugProfilingLabel },
+ 'key.debug.copy_location': { label: catalogKeyMessages.debugCopyLocationLabel },
+ 'key.debug.dump_version': { label: catalogKeyMessages.debugDumpVersionLabel },
+ 'key.debug.profiling_chart': { label: catalogKeyMessages.debugProfilingChartLabel },
+ 'key.debug.fps_charts': { label: catalogKeyMessages.debugFpsChartsLabel },
+ 'key.debug.network_charts': { label: catalogKeyMessages.debugNetworkChartsLabel },
+ 'key.debug.lightmap_texture': { label: catalogKeyMessages.debugLightmapTextureLabel },
+ 'key.debug.improved_transparency': {
+ label: catalogKeyMessages.debugImprovedTransparencyLabel,
+ },
+ }
+
+const categories: Record = {
+ skin_customization: {
+ label: categoryMessages.skinCustomizationLabel,
+ description: categoryMessages.skinCustomizationDescription,
+ },
+ video: { label: categoryMessages.videoLabel, description: categoryMessages.videoDescription },
+ video_settings: {
+ label: categoryMessages.videoLabel,
+ description: categoryMessages.videoDescription,
+ },
+ language: {
+ label: categoryMessages.languageLabel,
+ description: categoryMessages.languageDescription,
+ },
+ music_and_sound: {
+ label: categoryMessages.musicAndSoundLabel,
+ description: categoryMessages.musicAndSoundDescription,
+ },
+ controls: {
+ label: categoryMessages.controlsLabel,
+ description: categoryMessages.controlsDescription,
+ },
+ chat: { label: categoryMessages.chatLabel, description: categoryMessages.chatDescription },
+ chat_settings: {
+ label: categoryMessages.chatLabel,
+ description: categoryMessages.chatDescription,
+ },
+ accessibility: {
+ label: categoryMessages.accessibilityLabel,
+ description: categoryMessages.accessibilityDescription,
+ },
+ online: { label: categoryMessages.onlineLabel, description: categoryMessages.onlineDescription },
+ custom: { label: categoryMessages.customLabel, description: categoryMessages.customDescription },
+ custom_settings: {
+ label: categoryMessages.customLabel,
+ description: categoryMessages.customDescription,
+ },
+}
+
+const choices: Record = {
+ 'graphics:fast': choiceMessages.fast,
+ 'graphics:fancy': choiceMessages.fancy,
+ 'graphics:fabulous': choiceMessages.fabulous,
+ 'graphics:custom': choiceMessages.custom,
+ 'main_hand:left': choiceMessages.left,
+ 'main_hand:right': choiceMessages.right,
+ 'chat_visibility:0': choiceMessages.shown,
+ 'chat_visibility:1': choiceMessages.commandsOnly,
+ 'chat_visibility:2': choiceMessages.hidden,
+ 'particles:0': choiceMessages.all,
+ 'particles:1': choiceMessages.decreased,
+ 'particles:2': choiceMessages.minimal,
+ 'narrator:0': choiceMessages.off,
+ 'narrator:1': choiceMessages.all,
+ 'narrator:2': choiceMessages.chat,
+ 'narrator:3': choiceMessages.system,
+ 'clouds:false': choiceMessages.off,
+ 'clouds:fast': choiceMessages.fast,
+ 'clouds:true': choiceMessages.fancy,
+ 'ambient_occlusion:off': choiceMessages.off,
+ 'ambient_occlusion:on': choiceMessages.on,
+ 'ambient_occlusion:minimum': choiceMessages.minimum,
+ 'ambient_occlusion:maximum': choiceMessages.maximum,
+ 'music_toast:never': choiceMessages.never,
+ 'music_toast:pause': choiceMessages.pause,
+ 'music_toast:pause_and_toast': choiceMessages.pauseAndToast,
+ 'legacy_view_distance:0': choiceMessages.far,
+ 'legacy_view_distance:1': choiceMessages.normal,
+ 'legacy_view_distance:2': choiceMessages.short,
+ 'legacy_view_distance:3': choiceMessages.tiny,
+ 'legacy_framerate_limit:0': choiceMessages.maxFps,
+ 'legacy_framerate_limit:1': choiceMessages.balanced,
+ 'legacy_framerate_limit:2': choiceMessages.powerSaver,
+ 'inactivity_framerate_limit:afk': choiceMessages.whileAfk,
+ 'inactivity_framerate_limit:minimized': choiceMessages.whenMinimized,
+ 'prioritize_chunk_updates:0': choiceMessages.none,
+ 'prioritize_chunk_updates:1': choiceMessages.byPlayer,
+ 'prioritize_chunk_updates:2': choiceMessages.nearby,
+ 'attack_indicator:0': choiceMessages.off,
+ 'attack_indicator:1': choiceMessages.crosshair,
+ 'attack_indicator:2': choiceMessages.hotbar,
+ 'chat_preview:0': choiceMessages.off,
+ 'chat_preview:1': choiceMessages.commandsOnly,
+ 'chat_preview:2': choiceMessages.on,
+ 'music_frequency:CONSTANT': choiceMessages.constant,
+ 'music_frequency:DEFAULT': choiceMessages.default,
+ 'music_frequency:FREQUENT': choiceMessages.frequent,
+ 'share_presence:all': choiceMessages.all,
+ 'share_presence:limited': choiceMessages.limited,
+ 'share_presence:none': choiceMessages.none,
+ 'graphics_backend:default': choiceMessages.default,
+ 'graphics_backend:opengl': choiceMessages.openGl,
+ 'graphics_backend:vulkan': choiceMessages.vulkan,
+}
+
+const validationMessages: Record = {
+ missing_value: presentationMessages.validationMissingValue,
+ no_compatible_instances: presentationMessages.validationNoCompatibleInstances,
+ invalid_value: presentationMessages.validationInvalidValue,
+ changed_since_opened: presentationMessages.validationChangedSinceOpened,
+}
+
+export function formatGameSettingLabel(
+ formatMessage: FormatMessage,
+ setting: EditableGameSetting,
+): string {
+ if (setting.kind === 'external') return setting.raw_key ?? setting.option_id
+ const definition = knownSettings[setting.option_id]
+ return definition ? formatMessage(definition.label) : setting.option_id
+}
+
+export function formatGameSettingDescription(
+ formatMessage: FormatMessage,
+ setting: EditableGameSetting,
+): string {
+ if (setting.kind === 'external') return ''
+ const definition = knownSettings[setting.option_id]
+ return definition?.description ? formatMessage(definition.description) : ''
+}
+
+export function gameSettingCategoryMessage(category: GameSettingCategory): MessageDescriptor {
+ return (
+ categories[category.id]?.label ?? {
+ id: `app.settings.game-options.category.${category.id}.label`,
+ defaultMessage: category.id,
+ }
+ )
+}
+
+export function formatGameSettingChoice(
+ formatMessage: FormatMessage,
+ optionId: string,
+ value: string,
+): string {
+ const message = choices[`${optionId}:${value}`]
+ return message ? formatMessage(message) : value
+}
+
+export function formatGameSettingValidation(
+ formatMessage: FormatMessage,
+ error: GameOptionValidationError | null | undefined,
+): string | null {
+ return error ? formatMessage(validationMessages[error]) : null
+}
diff --git a/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/row.vue b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/row.vue
new file mode 100644
index 0000000000..049afab5c1
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/row.vue
@@ -0,0 +1,312 @@
+
+
+
+
+
+
+
+ {{ settingLabel }}
+
+
+
+
+
+
+ {{ settingDescription }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/use-editor.ts b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/use-editor.ts
new file mode 100644
index 0000000000..84d850e098
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/instances/game-settings-modal/use-editor.ts
@@ -0,0 +1,408 @@
+import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
+import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
+import { computed, type MaybeRefOrGetter, nextTick, onScopeDispose, ref, toValue } from 'vue'
+
+import {
+ type EditableGameSetting,
+ type GameOptionCanonicalValue,
+ type GameSettingChange,
+ type GameSettingsEditorState,
+ get_local_game_options_config,
+ get_synced_game_options_config,
+ preview_local_game_option_changes,
+ preview_synced_game_option_changes,
+ save_local_game_option_changes,
+ save_synced_game_option_changes,
+ type UpdateGameSettingsRequest,
+} from '@/helpers/game-options'
+
+import { canonicalValuesEqual, cloneGameSettingsState, gameSettingChanges } from './editors'
+
+function editorQueryKey(instanceId?: string) {
+ return instanceId
+ ? (['game-settings', 'local', instanceId] as const)
+ : (['game-settings', 'synced'] as const)
+}
+
+export function useGameSettingsEditor(
+ instanceId: MaybeRefOrGetter,
+ onSaved: () => void,
+) {
+ const { addNotification, handleError } = injectNotificationManager()
+ const { formatMessage } = useVIntl()
+ const queryClient = useQueryClient()
+ const messages = defineMessages({
+ conflictTitle: {
+ id: 'app.settings.synced-options.game-settings.conflict-title',
+ defaultMessage: 'These settings changed elsewhere',
+ },
+ conflictText: {
+ id: 'app.settings.synced-options.game-settings.conflict-text',
+ defaultMessage: 'We loaded the latest settings. Check your changes, then save again.',
+ },
+ })
+
+ const baseState = ref(null)
+ const draftState = ref(null)
+ const touchedValueOptionIds = ref>(new Set())
+ let previewTimer: ReturnType | null = null
+ let previewGeneration = 0
+ let loadGeneration = 0
+ let saveGeneration = 0
+
+ const editorInstanceId = ref()
+ const isLocalEditor = computed(() => !!editorInstanceId.value)
+
+ const dirtyChanges = computed(() =>
+ gameSettingChanges(baseState.value, draftState.value, touchedValueOptionIds.value),
+ )
+ const isDirty = computed(() => dirtyChanges.value.length > 0)
+ const dirtyOptionIds = computed(
+ () => new Set(dirtyChanges.value.map((change) => change.option_id)),
+ )
+ const hasBlockingDraft = computed(
+ () =>
+ draftState.value?.settings.some(
+ (setting) =>
+ dirtyOptionIds.value.has(setting.option_id) &&
+ (isLocalEditor.value || setting.sync_enabled) &&
+ (!!setting.validation_error ||
+ ['mixed', 'unset', 'invalid'].includes(setting.value_state)),
+ ) ?? false,
+ )
+ const stateQuery = useQuery(
+ computed(() => {
+ const instanceId = editorInstanceId.value
+ return {
+ queryKey: editorQueryKey(instanceId),
+ queryFn: () =>
+ instanceId ? get_local_game_options_config(instanceId) : get_synced_game_options_config(),
+ enabled: false,
+ retry: false,
+ }
+ }),
+ )
+ const loading = computed(() => stateQuery.isPending.value || stateQuery.isFetching.value)
+ const previewMutation = useMutation({
+ mutationFn: ({
+ instanceId,
+ request,
+ }: {
+ instanceId?: string
+ request: UpdateGameSettingsRequest
+ }) =>
+ instanceId
+ ? preview_local_game_option_changes(instanceId, request)
+ : preview_synced_game_option_changes(request),
+ })
+ const saveMutation = useMutation({
+ mutationFn: saveDraft,
+ onError: handleError,
+ })
+
+ async function load() {
+ const generation = ++loadGeneration
+ cancelPreview()
+ editorInstanceId.value = toValue(instanceId)
+ await nextTick()
+ const result = await stateQuery.refetch()
+ if (generation !== loadGeneration) return false
+ if (result.isError) {
+ handleError(result.error)
+ return false
+ }
+ if (!result.data) return false
+ baseState.value = cloneGameSettingsState(result.data)
+ draftState.value = cloneGameSettingsState(result.data)
+ touchedValueOptionIds.value = new Set()
+ return true
+ }
+
+ function cancelPreview() {
+ if (previewTimer) clearTimeout(previewTimer)
+ previewTimer = null
+ previewGeneration++
+ }
+
+ function reset() {
+ cancelPreview()
+ loadGeneration++
+ saveGeneration++
+ baseState.value = null
+ draftState.value = null
+ touchedValueOptionIds.value = new Set()
+ saveMutation.reset()
+ }
+
+ function cancelChanges() {
+ if (!baseState.value) return
+ cancelPreview()
+ draftState.value = cloneGameSettingsState(baseState.value)
+ touchedValueOptionIds.value = new Set()
+ }
+
+ function setSyncEnabled(optionIds: readonly string[], enabled: boolean) {
+ if (!draftState.value) return
+ const ids = new Set(optionIds)
+ draftState.value.settings = draftState.value.settings.map((setting) =>
+ ids.has(setting.option_id)
+ ? { ...setting, sync_enabled: enabled, validation_error: null }
+ : setting,
+ )
+ schedulePreview()
+ }
+
+ function schedulePreview() {
+ cancelPreview()
+ previewTimer = setTimeout(() => void preview(), 350)
+ }
+
+ function save() {
+ if (saveMutation.isPending.value || loading.value || hasBlockingDraft.value || !isDirty.value)
+ return
+ saveMutation.mutate()
+ }
+
+ function editorRequest(): UpdateGameSettingsRequest | null {
+ if (!baseState.value || !draftState.value) return null
+ return {
+ expected_summary_revision: draftState.value.summary_revision,
+ expected_canonical_revision: draftState.value.canonical_revision,
+ expected_catalog_revision: draftState.value.catalog_revision,
+ changes: dirtyChanges.value,
+ }
+ }
+
+ function setCanonicalValue(optionId: string, value: GameOptionCanonicalValue | null) {
+ if (!draftState.value) return
+ const currentSetting = draftState.value.settings.find(
+ (setting) => setting.option_id === optionId,
+ )
+ const baseSetting = baseState.value?.settings.find((setting) => setting.option_id === optionId)
+ if (!currentSetting) return
+
+ const revertedToBase =
+ !!baseSetting &&
+ !canonicalValuesEqual(currentSetting.canonical_value, baseSetting.canonical_value) &&
+ canonicalValuesEqual(value, baseSetting.canonical_value)
+ const touchedOptionIds = new Set(touchedValueOptionIds.value)
+ if (revertedToBase) {
+ touchedOptionIds.delete(optionId)
+ } else {
+ touchedOptionIds.add(optionId)
+ }
+ touchedValueOptionIds.value = touchedOptionIds
+ draftState.value.settings = draftState.value.settings.map((setting) =>
+ setting.option_id === optionId
+ ? {
+ ...setting,
+ canonical_value: value,
+ value_state: revertedToBase ? baseSetting.value_state : value ? 'canonical' : 'unset',
+ validation_error: revertedToBase ? baseSetting.validation_error : null,
+ }
+ : setting,
+ )
+ schedulePreview()
+ }
+
+ function mergePreview(preview: GameSettingsEditorState) {
+ if (!baseState.value || !draftState.value) return
+ const previousBase = cloneGameSettingsState(baseState.value)
+ const previousDraft = cloneGameSettingsState(draftState.value)
+ const dirtyIds = new Set(
+ gameSettingChanges(previousBase, previousDraft, touchedValueOptionIds.value).map(
+ (change) => change.option_id,
+ ),
+ )
+ const stagedSettings = new Map(
+ previousDraft.settings
+ .filter((setting) => dirtyIds.has(setting.option_id))
+ .map((setting) => [setting.option_id, setting]),
+ )
+ const previousBaseSettings = new Map(
+ previousBase.settings.map((setting) => [setting.option_id, setting]),
+ )
+ const nextBase = cloneGameSettingsState(preview)
+ const nextDraft = cloneGameSettingsState(preview)
+ nextBase.settings = nextBase.settings.map((setting) =>
+ dirtyIds.has(setting.option_id)
+ ? { ...(previousBaseSettings.get(setting.option_id) ?? setting) }
+ : setting,
+ )
+ nextDraft.settings = nextDraft.settings.map((setting): EditableGameSetting => {
+ const staged = stagedSettings.get(setting.option_id)
+ if (!staged) return setting
+ return {
+ ...setting,
+ sync_enabled: staged.sync_enabled,
+ canonical_value: staged.canonical_value,
+ option_revision: staged.option_revision,
+ }
+ })
+ baseState.value = nextBase
+ draftState.value = nextDraft
+ }
+
+ async function preview() {
+ const request = editorRequest()
+ if (!request || request.changes.length === 0) {
+ previewGeneration++
+ if (baseState.value) draftState.value = cloneGameSettingsState(baseState.value)
+ return
+ }
+
+ const generation = ++previewGeneration
+ try {
+ const previewState = await previewMutation.mutateAsync({
+ instanceId: editorInstanceId.value,
+ request,
+ })
+ if (generation !== previewGeneration) return
+ mergePreview(previewState)
+ } catch {
+ return
+ }
+ }
+
+ function applyChangesToRefreshedState(
+ refreshed: GameSettingsEditorState,
+ stagedDraft: GameSettingsEditorState,
+ changes: GameSettingChange[],
+ touchedOptionIds: ReadonlySet,
+ conflictOptionIds: ReadonlySet = new Set(),
+ ) {
+ const changesById = new Map(changes.map((change) => [change.option_id, change]))
+ const stagedSettings = new Map(
+ stagedDraft.settings.map((setting) => [setting.option_id, setting]),
+ )
+ const nextBase = cloneGameSettingsState(refreshed)
+ const nextDraft = cloneGameSettingsState(refreshed)
+
+ nextDraft.settings = nextDraft.settings.map((setting) => {
+ const change = changesById.get(setting.option_id)
+ const staged = stagedSettings.get(setting.option_id)
+ if (!change || !staged) return setting
+
+ return {
+ ...setting,
+ ...(change.sync_enabled !== undefined ? { sync_enabled: staged.sync_enabled } : {}),
+ ...(change.canonical_value !== undefined
+ ? {
+ canonical_value: staged.canonical_value,
+ value_state: staged.value_state,
+ }
+ : {}),
+ validation_error: conflictOptionIds.has(setting.option_id)
+ ? 'changed_since_opened'
+ : staged.validation_error,
+ }
+ })
+
+ baseState.value = nextBase
+ draftState.value = nextDraft
+ touchedValueOptionIds.value = new Set(
+ [...touchedOptionIds].filter(
+ (optionId) => changesById.get(optionId)?.canonical_value !== undefined,
+ ),
+ )
+ }
+
+ async function saveDraft() {
+ const request = editorRequest()
+ if (!request || request.changes.length === 0 || !baseState.value || !draftState.value) return
+
+ cancelPreview()
+ const targetInstanceId = editorInstanceId.value
+ const generation = ++saveGeneration
+ const previousBase = cloneGameSettingsState(baseState.value)
+ const optimisticState = cloneGameSettingsState(draftState.value)
+ const previouslyTouchedOptionIds = new Set(touchedValueOptionIds.value)
+ baseState.value = cloneGameSettingsState(optimisticState)
+ touchedValueOptionIds.value = new Set()
+ try {
+ const result = targetInstanceId
+ ? await save_local_game_option_changes(targetInstanceId, request)
+ : await save_synced_game_option_changes(request)
+ const refreshed =
+ result.state ??
+ (targetInstanceId
+ ? await get_local_game_options_config(targetInstanceId)
+ : await get_synced_game_options_config())
+ queryClient.setQueryData(editorQueryKey(targetInstanceId), refreshed)
+ if (result.conflicts?.length) {
+ if (generation !== saveGeneration || !draftState.value) return
+ previewGeneration++
+ const stagedDraft = cloneGameSettingsState(draftState.value)
+ const retainedTouchedOptionIds = new Set([
+ ...previouslyTouchedOptionIds,
+ ...touchedValueOptionIds.value,
+ ])
+ const stagedChanges = gameSettingChanges(
+ previousBase,
+ stagedDraft,
+ retainedTouchedOptionIds,
+ )
+ const conflicts = new Set(result.conflicts)
+ applyChangesToRefreshedState(
+ refreshed,
+ stagedDraft,
+ stagedChanges,
+ retainedTouchedOptionIds,
+ conflicts,
+ )
+
+ addNotification({
+ type: 'warning',
+ title: formatMessage(messages.conflictTitle),
+ text: formatMessage(messages.conflictText),
+ })
+ if (isDirty.value) {
+ schedulePreview()
+ }
+ return
+ }
+
+ if (generation !== saveGeneration) {
+ onSaved()
+ return
+ }
+ if (!baseState.value || !draftState.value) return
+ previewGeneration++
+ const stagedDraft = cloneGameSettingsState(draftState.value)
+ const touchedSinceSave = new Set(touchedValueOptionIds.value)
+ const changesSinceSave = gameSettingChanges(optimisticState, stagedDraft, touchedSinceSave)
+ applyChangesToRefreshedState(refreshed, stagedDraft, changesSinceSave, touchedSinceSave)
+ if (changesSinceSave.length > 0) schedulePreview()
+ onSaved()
+ } catch (error) {
+ if (generation === saveGeneration && draftState.value) {
+ previewGeneration++
+ baseState.value = previousBase
+ touchedValueOptionIds.value = new Set([
+ ...previouslyTouchedOptionIds,
+ ...touchedValueOptionIds.value,
+ ])
+ if (dirtyChanges.value.length > 0) schedulePreview()
+ }
+ throw error
+ }
+ }
+
+ onScopeDispose(reset)
+
+ return {
+ draftState,
+ isLocalEditor,
+ isDirty,
+ hasBlockingDraft,
+ loading,
+ loadError: stateQuery.isError,
+ saving: saveMutation.isPending,
+ load,
+ reset,
+ cancelChanges,
+ setSyncEnabled,
+ setCanonicalValue,
+ save,
+ }
+}
diff --git a/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/command-history-modal.vue b/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/command-history-modal.vue
new file mode 100644
index 0000000000..432ba3c75c
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/command-history-modal.vue
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/index.vue b/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/index.vue
new file mode 100644
index 0000000000..5482bbf067
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/index.vue
@@ -0,0 +1,634 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages[row.title]) }}
+
+
+ {{ formatMessage(messages[row.description]) }}
+
+
+
+
+
+
+
+
+ toggleGlobalOption(row.option, enabled)"
+ />
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/launch-options.vue b/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/launch-options.vue
new file mode 100644
index 0000000000..e5ac9f5698
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/launch-options.vue
@@ -0,0 +1,463 @@
+
+
+
+
+
+
+ {{ formatMessage(messages.windowSectionTitle) }}
+
+
+
+
+
+ {{ formatMessage(messages.fullscreenTitle) }}
+
+
+ {{ formatMessage(messages.fullscreenDescription) }}
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.widthTitle) }}
+
+
+ {{ formatMessage(messages.widthDescription) }}
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.heightTitle) }}
+
+
+ {{ formatMessage(messages.heightDescription) }}
+
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.javaAndMemorySectionTitle) }}
+
+
+
+
+ {{ formatMessage(messages.memoryAllocationTitle) }}
+
+
+
+ {{ formatMessage(messages.memoryAllocationDescription) }}
+
+
+
+
+
+ {{ formatMessage(messages.javaArgumentsTitle) }}
+
+
+
+ {{ formatMessage(messages.javaArgumentsDescription) }}
+
+
+
+
+
+ {{ formatMessage(messages.environmentVariablesTitle) }}
+
+
+
+ {{ formatMessage(messages.environmentVariablesDescription) }}
+
+
+
+
+
+
+
+ {{ formatMessage(messages.launchHooksSectionTitle) }}
+
+
+
+
+ {{ formatMessage(messages.preLaunchHookTitle) }}
+
+
+
+ {{ formatMessage(messages.preLaunchHookDescription) }}
+
+
+
+
+
+ {{ formatMessage(messages.wrapperHookTitle) }}
+
+
+
+ {{ formatMessage(messages.wrapperHookDescription) }}
+
+
+
+
+
+ {{ formatMessage(messages.postExitHookTitle) }}
+
+
+
+ {{ formatMessage(messages.postExitHookDescription) }}
+
+
+
+
+ {{ formatMessage(messages.hookVariablesDescription) }}
+
+ - {{ formatMessage(messages.instanceNameDescription) }}
+ - {{ formatMessage(messages.instanceIdDescription) }}
+ - {{ formatMessage(messages.instanceDirDescription) }}
+ - {{ formatMessage(messages.instanceMcDirDescription) }}
+ - {{ formatMessage(messages.instanceJavaDescription) }}
+ - {{ formatMessage(messages.instanceJavaArgsDescription) }}
+
+
+
+
+
+
+
+
+ {{ formatMessage(commonMessages.loadingLabel) }}
+
+
diff --git a/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/servers-modal.vue b/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/servers-modal.vue
new file mode 100644
index 0000000000..98a5aa9534
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/servers-modal.vue
@@ -0,0 +1,472 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(commonMessages.allProjectType) }}
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.noSyncedServers) }}
+
+
+ {{ formatMessage(messages.noMatchingServers) }}
+
+
+
+
+
+
+
+ {{ row.name }}
+ {{ row.address }}
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(commonMessages.loadingLabel) }}
+
+
+ {{
+ formatMessage(messages.playersOnline, {
+ count: serverData[row.address]?.status?.players?.online ?? 0,
+ })
+ }}
+
+ {{ formatMessage(messages.offlineFilter) }}
+
+
+
+ {{ row.version || '—' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.serverCount, { count: syncedServers.length }) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/skin/BakedSkinButton.vue b/apps/app-frontend/src/components/ui/skin/BakedSkinButton.vue
new file mode 100644
index 0000000000..89bd046823
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/skin/BakedSkinButton.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/skin/VirtualSkinSectionList.vue b/apps/app-frontend/src/components/ui/skin/VirtualSkinSectionList.vue
index 9856d76013..8d9541831e 100644
--- a/apps/app-frontend/src/components/ui/skin/VirtualSkinSectionList.vue
+++ b/apps/app-frontend/src/components/ui/skin/VirtualSkinSectionList.vue
@@ -6,7 +6,6 @@ import {
commonMessages,
defineMessages,
IconButton,
- SkinButton,
SkinLikeTextButton,
useScrollViewport,
useVIntl,
@@ -16,8 +15,8 @@ import { Tooltip } from 'floating-vue'
import { computed, nextTick, onUnmounted, ref, useTemplateRef, watch } from 'vue'
import Draggable from 'vuedraggable'
-import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
-import type { Skin } from '@/helpers/skins.ts'
+import BakedSkinButton from '@/components/ui/skin/BakedSkinButton.vue'
+import type { Cape, Skin } from '@/helpers/skins.ts'
type SkinSectionKind = 'saved' | 'default'
type SkinLikeTextButtonExpose = {
@@ -80,7 +79,7 @@ const messages = defineMessages({
const props = defineProps<{
savedSkins: Skin[]
defaultSkinSections: DefaultSkinSection[]
- getBakedSkinTextures: (skin: Skin) => RenderResult | undefined
+ capes: Cape[]
isSkinSelected: (skin: Skin) => boolean
isSkinActive: (skin: Skin) => boolean
isAddSkinButtonDragActive: boolean
@@ -454,9 +453,10 @@ defineExpose({ getAddSkinButtonElement })
:key="savedSkinKey(skin)"
class="relative aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
>
-
-
+
@@ -495,9 +495,10 @@ defineExpose({ getAddSkinButtonElement })
:key="savedSkinKey(skin)"
class="relative aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
>
-
-
+
@@ -535,11 +536,12 @@ defineExpose({ getAddSkinButtonElement })
v-else
class="grid w-full grid-cols-3 gap-3 min-[1300px]:grid-cols-4 min-[1750px]:grid-cols-5 min-[2050px]:grid-cols-6"
>
-
{{ formatMessage(commonMessages.editButton) }}
-
+
diff --git a/apps/app-frontend/src/components/ui/sync-instances-update-modal/index.vue b/apps/app-frontend/src/components/ui/sync-instances-update-modal/index.vue
new file mode 100644
index 0000000000..56b145df19
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/sync-instances-update-modal/index.vue
@@ -0,0 +1,395 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.badge) }}
+
+
+
+
+ {{ formatMessage(messages.title) }}
+
+
{{ formatMessage(messages.description) }}
+
+ {{ formatMessage(messages.manageLater) }}
+
+
+
+
+
{{ formatMessage(messages.loadError) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ toggleOption(option, enabled)"
+ />
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/sync-instances-update-modal/use-sync.ts b/apps/app-frontend/src/components/ui/sync-instances-update-modal/use-sync.ts
new file mode 100644
index 0000000000..37bfbff2ff
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/sync-instances-update-modal/use-sync.ts
@@ -0,0 +1,251 @@
+import { injectNotificationManager } from '@modrinth/ui'
+import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
+import { computed, ref, watch } from 'vue'
+
+import { isSyncedOptionAvailable, set_global_synced_option } from '@/helpers/instance'
+import {
+ canSourceMultiplayerServers,
+ gameOptionsSyncSourcesQueryOptions,
+ globalSyncedOptionsQueryOptions,
+ syncedOptionsKeys,
+} from '@/helpers/synced-options'
+import { syncedPackKeys } from '@/helpers/synced-packs'
+import { instanceKeys, instanceListQueryOptions } from '@/pages/instance/query-options'
+
+export const syncUpdateOptions = (
+ [
+ 'game_options',
+ 'multiplayer_servers',
+ 'command_history',
+ 'creative_hotbars',
+ 'resource_packs',
+ 'data_packs',
+ ] as const
+).filter(isSyncedOptionAvailable)
+
+export type SyncUpdateOption = (typeof syncUpdateOptions)[number]
+
+type SyncUpdateOptionState = Record
+type SyncUpdateSourceState = Partial>
+
+function createOptionState(value = false): SyncUpdateOptionState {
+ return Object.fromEntries(
+ syncUpdateOptions.map((option) => [option, value]),
+ ) as SyncUpdateOptionState
+}
+
+export function useSyncInstancesUpdate() {
+ const queryClient = useQueryClient()
+ const { handleError } = injectNotificationManager()
+ const isOpen = ref(false)
+ const draftInitialized = ref(false)
+ const initialOptions = ref(createOptionState())
+ const draftOptions = ref(createOptionState())
+ const draftSourceInstanceIds = ref({})
+ const sourceOptions = ref([])
+ const sourceInstanceId = ref('')
+ const needsGameOptionsSource = computed(() => sourceOptions.value.includes('game_options'))
+ const needsServerSource = computed(() => sourceOptions.value.includes('multiplayer_servers'))
+ const globalOptionsQuery = useQuery({
+ ...globalSyncedOptionsQueryOptions(),
+ enabled: isOpen,
+ })
+ const gameSourcesQuery = useQuery({
+ ...gameOptionsSyncSourcesQueryOptions(),
+ enabled: needsGameOptionsSource,
+ })
+ const instancesQuery = useQuery({
+ ...instanceListQueryOptions(),
+ staleTime: 0,
+ enabled: computed(
+ () =>
+ sourceOptions.value.length > 0 &&
+ (!needsGameOptionsSource.value || needsServerSource.value),
+ ),
+ })
+ const sources = computed(() =>
+ needsGameOptionsSource.value
+ ? (gameSourcesQuery.data.value ?? []).map((source) => ({
+ id: source.source_id,
+ name: source.name,
+ icon_path: source.icon_path,
+ eligible:
+ source.eligible &&
+ (!needsServerSource.value ||
+ (instancesQuery.data.value ?? []).some(
+ (instance) =>
+ instance.id === source.source_id && canSourceMultiplayerServers(instance),
+ )),
+ }))
+ : (instancesQuery.data.value ?? []).map((instance) => ({
+ id: instance.id,
+ name: instance.name,
+ icon_path: instance.icon_path,
+ eligible:
+ instance.install_stage === 'installed' &&
+ !instance.quarantined &&
+ (!needsServerSource.value || canSourceMultiplayerServers(instance)),
+ })),
+ )
+ const sourcesLoading = computed(
+ () =>
+ (needsGameOptionsSource.value &&
+ (gameSourcesQuery.isPending.value || gameSourcesQuery.isFetching.value)) ||
+ ((!needsGameOptionsSource.value || needsServerSource.value) &&
+ (instancesQuery.isPending.value || instancesQuery.isFetching.value)),
+ )
+ const sourcesError = computed(
+ () =>
+ (needsGameOptionsSource.value && gameSourcesQuery.isError.value) ||
+ ((!needsGameOptionsSource.value || needsServerSource.value) && instancesQuery.isError.value),
+ )
+ const allSynced = computed(
+ () => draftInitialized.value && syncUpdateOptions.every((option) => draftOptions.value[option]),
+ )
+
+ function initializeDraft() {
+ const globalOptions = globalOptionsQuery.data.value
+ if (!globalOptions) return
+
+ const options = createOptionState()
+ for (const option of syncUpdateOptions) {
+ options[option] = globalOptions[option]
+ }
+ initialOptions.value = { ...options }
+ draftOptions.value = options
+ draftSourceInstanceIds.value = {}
+ draftInitialized.value = true
+ }
+
+ watch(
+ () => globalOptionsQuery.data.value,
+ () => {
+ if (isOpen.value && !draftInitialized.value) initializeDraft()
+ },
+ )
+
+ watch([sources, sourceOptions], ([candidates]) => {
+ if (!candidates.some((source) => source.id === sourceInstanceId.value && source.eligible)) {
+ sourceInstanceId.value = candidates.find((source) => source.eligible)?.id ?? ''
+ }
+ })
+
+ const syncMutation = useMutation({
+ mutationKey: syncedOptionsKeys.set,
+ mutationFn: async (
+ changes: {
+ option: SyncUpdateOption
+ enabled: boolean
+ baseInstanceId?: string
+ }[],
+ ) => {
+ for (const { option, enabled, baseInstanceId } of changes) {
+ const updated = await set_global_synced_option(option, enabled, baseInstanceId)
+ queryClient.setQueryData(syncedOptionsKeys.global, updated)
+ initialOptions.value[option] = enabled
+ }
+ },
+ onMutate: () => queryClient.cancelQueries({ queryKey: syncedOptionsKeys.global }),
+ onError: handleError,
+ onSettled: () => {
+ void Promise.all([
+ queryClient.invalidateQueries({ queryKey: syncedOptionsKeys.global }),
+ queryClient.invalidateQueries({ queryKey: syncedOptionsKeys.initialized }),
+ queryClient.invalidateQueries({ queryKey: syncedOptionsKeys.gameSources }),
+ queryClient.invalidateQueries({ queryKey: ['instance-synced-options'] }),
+ queryClient.invalidateQueries({ queryKey: instanceKeys.all }),
+ queryClient.invalidateQueries({ queryKey: ['worlds'] }),
+ queryClient.invalidateQueries({ queryKey: syncedPackKeys.all }),
+ ])
+ },
+ })
+
+ function beginDraft() {
+ isOpen.value = true
+ draftInitialized.value = false
+ sourceOptions.value = []
+ sourceInstanceId.value = ''
+ initializeDraft()
+ }
+
+ function finishDraft() {
+ isOpen.value = false
+ draftInitialized.value = false
+ draftSourceInstanceIds.value = {}
+ sourceOptions.value = []
+ sourceInstanceId.value = ''
+ }
+
+ function stageOptions(
+ options: readonly SyncUpdateOption[],
+ enabled: boolean,
+ baseInstanceId?: string,
+ ) {
+ for (const option of options.filter(isSyncedOptionAvailable)) {
+ draftOptions.value[option] = enabled
+ if (enabled && !initialOptions.value[option] && baseInstanceId) {
+ draftSourceInstanceIds.value[option] = baseInstanceId
+ } else {
+ draftSourceInstanceIds.value[option] = undefined
+ }
+ }
+ }
+
+ function isInitiallyEnabled(option: SyncUpdateOption) {
+ return initialOptions.value[option]
+ }
+
+ async function applyDraft() {
+ if (!draftInitialized.value) return
+
+ const changes = syncUpdateOptions
+ .filter((option) => draftOptions.value[option] !== initialOptions.value[option])
+ .map((option) => ({
+ option,
+ enabled: draftOptions.value[option],
+ baseInstanceId: draftOptions.value[option]
+ ? draftSourceInstanceIds.value[option]
+ : undefined,
+ }))
+ if (changes.length === 0) return
+
+ await syncMutation.mutateAsync(changes)
+ draftSourceInstanceIds.value = {}
+ }
+
+ function chooseSource(options: readonly SyncUpdateOption[]) {
+ sourceInstanceId.value = ''
+ sourceOptions.value = options.filter(isSyncedOptionAvailable)
+ }
+
+ async function retrySources() {
+ const results = await Promise.all([
+ ...(needsGameOptionsSource.value ? [gameSourcesQuery.refetch({ cancelRefetch: false })] : []),
+ ...(!needsGameOptionsSource.value || needsServerSource.value
+ ? [instancesQuery.refetch({ cancelRefetch: false })]
+ : []),
+ ])
+ return { isSuccess: results.every((result) => result.isSuccess) }
+ }
+
+ return {
+ isOpen,
+ globalOptionsQuery,
+ allSynced,
+ draftInitialized,
+ draftOptions,
+ syncMutation,
+ sourceOptions,
+ sourceInstanceId,
+ sources,
+ sourcesLoading,
+ sourcesError,
+ beginDraft,
+ finishDraft,
+ stageOptions,
+ isInitiallyEnabled,
+ applyDraft,
+ chooseSource,
+ retrySources,
+ }
+}
diff --git a/apps/app-frontend/src/components/ui/world/InstanceItem.vue b/apps/app-frontend/src/components/ui/world/InstanceItem.vue
index cc1a117de4..6a0b4b04b6 100644
--- a/apps/app-frontend/src/components/ui/world/InstanceItem.vue
+++ b/apps/app-frontend/src/components/ui/world/InstanceItem.vue
@@ -24,7 +24,7 @@ import {
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
-import { capitalizeString } from '@modrinth/utils'
+import { capitalizeString } from '@modrinth/utils/utils'
import type { Dayjs } from 'dayjs'
import { computed, onMounted, ref, useTemplateRef } from 'vue'
import { useRouter } from 'vue-router'
diff --git a/apps/app-frontend/src/components/ui/world/WorldItem.vue b/apps/app-frontend/src/components/ui/world/WorldItem.vue
index c226d7be0d..c7b1282ba2 100644
--- a/apps/app-frontend/src/components/ui/world/WorldItem.vue
+++ b/apps/app-frontend/src/components/ui/world/WorldItem.vue
@@ -18,15 +18,16 @@ import {
UserIcon,
XIcon,
} from '@modrinth/assets'
-import type { ButtonMenuOption, MessageDescriptor } from '@modrinth/ui'
import {
Avatar,
BulletDivider,
Button,
+ type ButtonMenuOption,
commonMessages,
ContextMenu,
defineMessages,
injectNotificationManager,
+ type MessageDescriptor,
SmartClickable,
TagItem,
TeleportOverflowMenu,
@@ -35,7 +36,8 @@ import {
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
-import { getPingLevel } from '@modrinth/utils'
+import { getPingLevel } from '@modrinth/utils/utils'
+import { autoToHTML } from '@sfirew/minecraft-motd-parser'
import dayjs from 'dayjs'
import { Tooltip } from 'floating-vue'
import type { Component } from 'vue'
@@ -276,6 +278,15 @@ const messages = defineMessages({
},
})
+const incompatibleVersionTooltip = computed(() => ({
+ content: `${autoToHTML(
+ formatMessage(messages.incompatibleVersion, {
+ version: props.serverStatus?.version?.name ?? '',
+ }),
+ )}`,
+ html: true,
+}))
+
const cardOptions = useTemplateRef('cardOptions')
const showStop = computed(
() =>
@@ -507,16 +518,12 @@ function openContextMenu(event: MouseEvent) {
{{ formatMessage(commonMessages.loadingLabel) }}
-
-
-
- {{
- formatMessage(messages.incompatibleVersion, {
- version: serverStatus.version?.name,
- })
- }}
-
-
+
-import { TrashIcon, XIcon } from '@modrinth/assets'
+import { ShredderIcon, TrashIcon, XIcon } from '@modrinth/assets'
import {
Admonition,
Button,
@@ -16,11 +16,10 @@ const { formatMessage } = useVIntl()
const props = defineProps<{
world: World | null
- otherSyncedInstanceCount: number
}>()
const emit = defineEmits<{
- confirm: [world: World]
+ confirm: [world: World, scope: 'here' | 'all']
}>()
const messages = defineMessages({
@@ -45,10 +44,22 @@ const messages = defineMessages({
defaultMessage:
'This server will be removed from your server list and from the in-game server list. You can add it again later if you know the address.',
},
- syncedServerRemovalNotice: {
- id: 'app.instance.worlds.remove-server-modal.synced-removal-notice',
+ syncedServerHeader: {
+ id: 'app.instance.worlds.remove-server-modal.synced-header',
+ defaultMessage: 'This server is synced',
+ },
+ syncedServerDescription: {
+ id: 'app.instance.worlds.remove-server-modal.synced-description',
defaultMessage:
- 'This server will also be removed from {count, plural, one {# other instance} other {# other instances}} because it’s synced.',
+ 'You can remove it from just this instance or from all synced instances. Removing it from only this instance will enable overrides, and this instance will no longer receive synced server changes.',
+ },
+ removeHere: {
+ id: 'app.instance.worlds.remove-server-modal.remove-here',
+ defaultMessage: 'Remove here',
+ },
+ removeEverywhere: {
+ id: 'app.instance.worlds.remove-server-modal.remove-everywhere',
+ defaultMessage: 'Remove everywhere',
},
deleteWorldWarningBody: {
id: 'app.instance.worlds.delete-world-modal.warning-body',
@@ -73,16 +84,28 @@ const isSyncedServer = computed(
)
const isSingleplayer = computed(() => props.world?.type === 'singleplayer')
const titleMessage = computed(() =>
- isServer.value ? messages.removeServerTitle : messages.deleteWorldTitle,
+ isSyncedServer.value
+ ? messages.syncedServerHeader
+ : isServer.value
+ ? messages.removeServerTitle
+ : messages.deleteWorldTitle,
)
const actionMessage = computed(() =>
isServer.value ? messages.removeServerButton : messages.deleteWorldButton,
)
const warningHeaderMessage = computed(() =>
- isServer.value ? messages.removeServerWarningHeader : messages.deleteWorldWarningHeader,
+ isSyncedServer.value
+ ? messages.syncedServerHeader
+ : isServer.value
+ ? messages.removeServerWarningHeader
+ : messages.deleteWorldWarningHeader,
)
const warningBodyMessage = computed(() =>
- isServer.value ? messages.removeServerWarningBody : messages.deleteWorldWarningBody,
+ isSyncedServer.value
+ ? messages.syncedServerDescription
+ : isServer.value
+ ? messages.removeServerWarningBody
+ : messages.deleteWorldWarningBody,
)
function show() {
@@ -93,9 +116,9 @@ function hide() {
modal.value?.hide()
}
-function confirm() {
+function confirm(scope: 'here' | 'all') {
if (!props.world) return
- emit('confirm', props.world)
+ emit('confirm', props.world, scope)
hide()
}
@@ -103,34 +126,48 @@ defineExpose({ show, hide })
-
-
+
+
+
+ {{ formatMessage(warningBodyMessage) }}
+
{{ formatMessage(warningBodyMessage) }}
-
- {{
- formatMessage(messages.syncedServerRemovalNotice, {
- count: otherSyncedInstanceCount,
- })
- }}
-
-
+
+
+
+
+