mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
fix: ux changes for sync settings/overrides
This commit is contained in:
@@ -429,10 +429,10 @@ const groupedScreenshots = computed((): ScreenshotGroupData[] => {
|
||||
screenshotGroups.set(screenshot.instance_id, group)
|
||||
}
|
||||
|
||||
const syncedInstances = (instancesQuery.data.value ?? [])
|
||||
.filter((instance) => instance.synced_options.screenshots)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
const groups = syncedInstances.flatMap((instance) => {
|
||||
const instances = [...(instancesQuery.data.value ?? [])].sort((a, b) =>
|
||||
a.name.localeCompare(b.name),
|
||||
)
|
||||
const groups = instances.flatMap((instance) => {
|
||||
const instanceScreenshots = screenshotGroups.get(instance.id)
|
||||
return instanceScreenshots
|
||||
? [
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useSavable,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { inject, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import {
|
||||
@@ -14,7 +15,13 @@ import {
|
||||
type FeatureFlag,
|
||||
useAppSettings,
|
||||
} from '@/composables/use-app-settings.ts'
|
||||
import {
|
||||
get_global_synced_options,
|
||||
type GlobalSyncedOptions,
|
||||
set_global_synced_option,
|
||||
} from '@/helpers/instance.ts'
|
||||
import { type AppSettings, get, set } from '@/helpers/settings.ts'
|
||||
import { screenshotKeys } from '@/pages/instance/query-options.ts'
|
||||
import { appSettingsModalContextKey } from '@/providers/app-settings-modal'
|
||||
|
||||
const appSettings = useAppSettings()
|
||||
@@ -22,6 +29,7 @@ const { formatMessage } = useVIntl()
|
||||
const auth = injectAuth()
|
||||
const { updatePreferences } = injectUserPreferences()
|
||||
const settingsModal = inject(appSettingsModalContextKey, null)
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const worldsInHomeFlag: FeatureFlag = 'worlds_in_home'
|
||||
const compactInstanceCardsFlag: FeatureFlag = 'compact_instance_cards'
|
||||
@@ -51,6 +59,14 @@ const messages = defineMessages({
|
||||
id: 'app.behavior-settings.content.title',
|
||||
defaultMessage: 'Home and content',
|
||||
},
|
||||
showAllScreenshotsTitle: {
|
||||
id: 'app.behavior-settings.show-all-screenshots.title',
|
||||
defaultMessage: 'Show all screenshots together',
|
||||
},
|
||||
showAllScreenshotsDescription: {
|
||||
id: 'app.behavior-settings.show-all-screenshots.description',
|
||||
defaultMessage: 'View screenshots from all your instances on the Screenshots page.',
|
||||
},
|
||||
confirmationsTitle: {
|
||||
id: 'app.behavior-settings.confirmations.title',
|
||||
defaultMessage: 'Confirmations',
|
||||
@@ -137,6 +153,7 @@ type BehaviorSettingsState = {
|
||||
minimizeApp: boolean
|
||||
hideRightSidebar: boolean
|
||||
showJumpIn: boolean
|
||||
showAllScreenshots: boolean
|
||||
compactInstanceCards: boolean
|
||||
showPlayTime: boolean
|
||||
hideNametag: boolean
|
||||
@@ -144,14 +161,23 @@ type BehaviorSettingsState = {
|
||||
skipNonEssentialWarnings: boolean
|
||||
}
|
||||
|
||||
const persistedSettings = ref(await get())
|
||||
const [initialSettings, initialGlobalSyncedOptions] = await Promise.all([
|
||||
get(),
|
||||
get_global_synced_options(),
|
||||
])
|
||||
const persistedSettings = ref(initialSettings)
|
||||
const persistedGlobalSyncedOptions = ref(initialGlobalSyncedOptions)
|
||||
|
||||
function getBehaviorSettingsState(settings: AppSettings): BehaviorSettingsState {
|
||||
function getBehaviorSettingsState(
|
||||
settings: AppSettings,
|
||||
globalSyncedOptions: GlobalSyncedOptions,
|
||||
): BehaviorSettingsState {
|
||||
return {
|
||||
syncBehaviorAcrossDevices: settings.sync_behavior_across_devices,
|
||||
minimizeApp: settings.hide_on_process_start,
|
||||
hideRightSidebar: settings.toggle_sidebar,
|
||||
showJumpIn: settings.feature_flags[worldsInHomeFlag] ?? DEFAULT_FEATURE_FLAGS[worldsInHomeFlag],
|
||||
showAllScreenshots: globalSyncedOptions.screenshots,
|
||||
compactInstanceCards:
|
||||
settings.feature_flags[compactInstanceCardsFlag] ??
|
||||
DEFAULT_FEATURE_FLAGS[compactInstanceCardsFlag],
|
||||
@@ -169,7 +195,7 @@ function getBehaviorSettingsState(settings: AppSettings): BehaviorSettingsState
|
||||
}
|
||||
|
||||
const { saved, current, changes, saving, hasChanges, reset, save } = useSavable(
|
||||
() => getBehaviorSettingsState(persistedSettings.value),
|
||||
() => getBehaviorSettingsState(persistedSettings.value, persistedGlobalSyncedOptions.value),
|
||||
async () => {
|
||||
const value = current.value
|
||||
|
||||
@@ -204,8 +230,20 @@ const { saved, current, changes, saving, hasChanges, reset, save } = useSavable(
|
||||
},
|
||||
}
|
||||
|
||||
await set(nextSettings)
|
||||
const screenshotsChanged =
|
||||
value.showAllScreenshots !== persistedGlobalSyncedOptions.value.screenshots
|
||||
const [, updatedGlobalSyncedOptions] = await Promise.all([
|
||||
set(nextSettings),
|
||||
screenshotsChanged
|
||||
? set_global_synced_option('screenshots', value.showAllScreenshots)
|
||||
: Promise.resolve(persistedGlobalSyncedOptions.value),
|
||||
])
|
||||
persistedSettings.value = nextSettings
|
||||
persistedGlobalSyncedOptions.value = updatedGlobalSyncedOptions
|
||||
queryClient.setQueryData(['global-synced-options'], updatedGlobalSyncedOptions)
|
||||
if (screenshotsChanged) {
|
||||
await queryClient.invalidateQueries({ queryKey: screenshotKeys.all })
|
||||
}
|
||||
appSettings.setBehaviorSyncAcrossDevices(value.syncBehaviorAcrossDevices)
|
||||
appSettings.toggleSidebar = value.hideRightSidebar
|
||||
appSettings.hideNametagSkinsPage = value.hideNametag
|
||||
@@ -302,6 +340,18 @@ onBeforeUnmount(() => {
|
||||
{{ formatMessage(messages.contentTitle) }}
|
||||
</h2>
|
||||
<div class="mt-4 flex flex-col gap-6">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.showAllScreenshotsTitle) }}
|
||||
</h3>
|
||||
<p class="m-0 mt-1">
|
||||
{{ formatMessage(messages.showAllScreenshotsDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle id="show-all-screenshots" v-model="current.showAllScreenshots" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
|
||||
+158
-18
@@ -2,11 +2,15 @@
|
||||
import {
|
||||
EditIcon,
|
||||
// FolderOpenIcon,
|
||||
RefreshCwIcon,
|
||||
SaveIcon,
|
||||
SearchIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
CheckCircleButton,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
IconButton,
|
||||
@@ -26,7 +30,9 @@ import useMemorySlider from '@/composables/useMemorySlider'
|
||||
import {
|
||||
get_command_history,
|
||||
get_global_synced_options,
|
||||
getInstanceIconUrl,
|
||||
type GlobalSyncedOptions,
|
||||
list as listInstances,
|
||||
list_synced_servers,
|
||||
// open_synced_options_folder,
|
||||
remove_synced_server,
|
||||
@@ -43,7 +49,7 @@ import {
|
||||
type ServerData,
|
||||
type ServerWorld,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import { instanceKeys, screenshotKeys } from '@/pages/instance/query-options'
|
||||
import { instanceKeys } from '@/pages/instance/query-options'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -83,13 +89,33 @@ const messages = defineMessages({
|
||||
id: 'app.settings.synced-options.creative-hotbars.description',
|
||||
defaultMessage: 'Sync saved creative hotbars across your instances.',
|
||||
},
|
||||
screenshots: {
|
||||
id: 'app.settings.synced-options.screenshots',
|
||||
defaultMessage: 'Screenshots',
|
||||
chooseSyncSourceTitle: {
|
||||
id: 'app.settings.synced-options.choose-sync-source.title',
|
||||
defaultMessage: 'Choose a sync source',
|
||||
},
|
||||
screenshotsDescription: {
|
||||
id: 'app.settings.synced-options.screenshots.description',
|
||||
defaultMessage: 'View screenshots from your instances in one place.',
|
||||
multiplayerServersSyncSourceDescription: {
|
||||
id: 'app.settings.synced-options.choose-sync-source.multiplayer-servers-description',
|
||||
defaultMessage: 'Pick the instance whose multiplayer servers become the shared copy.',
|
||||
},
|
||||
commandHistorySyncSourceDescription: {
|
||||
id: 'app.settings.synced-options.choose-sync-source.command-history-description',
|
||||
defaultMessage: 'Pick the instance whose command history becomes the shared copy.',
|
||||
},
|
||||
creativeHotbarsSyncSourceDescription: {
|
||||
id: 'app.settings.synced-options.choose-sync-source.creative-hotbars-description',
|
||||
defaultMessage: 'Pick the instance whose saved creative hotbars become the shared copy.',
|
||||
},
|
||||
searchInstance: {
|
||||
id: 'app.settings.synced-options.choose-sync-source.search-placeholder',
|
||||
defaultMessage: 'Search instance',
|
||||
},
|
||||
noInstancesFound: {
|
||||
id: 'app.settings.synced-options.choose-sync-source.no-instances-found',
|
||||
defaultMessage: 'No instances found',
|
||||
},
|
||||
syncButton: {
|
||||
id: 'app.settings.synced-options.choose-sync-source.sync',
|
||||
defaultMessage: 'Sync',
|
||||
},
|
||||
commandHistoryEditorTitle: {
|
||||
id: 'app.settings.synced-options.command-history.editor-title',
|
||||
@@ -285,11 +311,6 @@ const globalRows: Array<{
|
||||
title: 'creativeHotbars',
|
||||
description: 'creativeHotbarsDescription',
|
||||
},
|
||||
{
|
||||
option: 'screenshots',
|
||||
title: 'screenshots',
|
||||
description: 'screenshotsDescription',
|
||||
},
|
||||
]
|
||||
|
||||
const globalSyncedOptionsQueryKey = ['global-synced-options'] as const
|
||||
@@ -306,6 +327,11 @@ const globalOptionsQuery = useQuery({
|
||||
queryFn: get_global_synced_options,
|
||||
})
|
||||
const globalOptions = computed(() => globalOptionsQuery.data.value ?? defaultGlobalOptions)
|
||||
const instances = ref(await listInstances().catch(() => []))
|
||||
const baseOption = ref<SyncedOption | null>(null)
|
||||
const baseInstanceId = ref(instances.value[0]?.id ?? '')
|
||||
const baseInstanceSearch = ref('')
|
||||
const baseModal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
const commandHistoryModal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
const serverEditorModal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
const editServerModal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
@@ -340,24 +366,43 @@ const syncedServerCards = computed(() =>
|
||||
})),
|
||||
)
|
||||
|
||||
const baseInstanceDescription = computed(() => {
|
||||
switch (baseOption.value) {
|
||||
case 'multiplayer_servers':
|
||||
return formatMessage(messages.multiplayerServersSyncSourceDescription)
|
||||
case 'command_history':
|
||||
return formatMessage(messages.commandHistorySyncSourceDescription)
|
||||
case 'creative_hotbars':
|
||||
return formatMessage(messages.creativeHotbarsSyncSourceDescription)
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const filteredBaseInstances = computed(() => {
|
||||
const search = baseInstanceSearch.value.trim().toLowerCase()
|
||||
if (!search) return instances.value
|
||||
return instances.value.filter((instance) => instance.name.toLowerCase().includes(search))
|
||||
})
|
||||
|
||||
async function invalidateSyncedOptions() {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: instanceKeys.all }),
|
||||
queryClient.invalidateQueries({ queryKey: ['instance-synced-options'] }),
|
||||
queryClient.invalidateQueries({ queryKey: globalSyncedOptionsQueryKey }),
|
||||
queryClient.invalidateQueries({ queryKey: screenshotKeys.all }),
|
||||
])
|
||||
}
|
||||
|
||||
type GlobalOptionMutationVariables = {
|
||||
option: SyncedOption
|
||||
enabled: boolean
|
||||
baseInstanceId?: string
|
||||
}
|
||||
|
||||
const globalOptionMutation = useMutation({
|
||||
mutationKey: globalSyncedOptionsMutationKey,
|
||||
mutationFn: ({ option, enabled }: GlobalOptionMutationVariables) =>
|
||||
set_global_synced_option(option, enabled),
|
||||
mutationFn: ({ option, enabled, baseInstanceId }: GlobalOptionMutationVariables) =>
|
||||
set_global_synced_option(option, enabled, baseInstanceId),
|
||||
onMutate: async ({ option, enabled }) => {
|
||||
await queryClient.cancelQueries({ queryKey: globalSyncedOptionsQueryKey })
|
||||
const previous = globalOptions.value[option]
|
||||
@@ -376,6 +421,15 @@ const globalOptionMutation = useMutation({
|
||||
}))
|
||||
handleError(error)
|
||||
},
|
||||
onSuccess: async (_options, { option, enabled }) => {
|
||||
if (enabled) baseModal.value?.hide()
|
||||
if (enabled && option === 'multiplayer_servers') {
|
||||
syncedServers.value = await list_synced_servers().catch((error) => {
|
||||
handleError(error)
|
||||
return []
|
||||
})
|
||||
}
|
||||
},
|
||||
onSettled: async () => {
|
||||
if (queryClient.isMutating({ mutationKey: globalSyncedOptionsMutationKey }) === 1) {
|
||||
await invalidateSyncedOptions()
|
||||
@@ -383,12 +437,25 @@ const globalOptionMutation = useMutation({
|
||||
},
|
||||
})
|
||||
|
||||
function applyGlobalOption(option: SyncedOption, enabled: boolean) {
|
||||
globalOptionMutation.mutate({ option, enabled })
|
||||
function applyGlobalOption(option: SyncedOption, enabled: boolean, baseInstanceId?: string) {
|
||||
globalOptionMutation.mutate({ option, enabled, baseInstanceId })
|
||||
}
|
||||
|
||||
function toggleGlobalOption(option: SyncedOption, enabled: boolean) {
|
||||
applyGlobalOption(option, enabled)
|
||||
if (!enabled) {
|
||||
applyGlobalOption(option, false)
|
||||
return
|
||||
}
|
||||
|
||||
baseOption.value = option
|
||||
baseInstanceId.value = instances.value[0]?.id ?? ''
|
||||
baseInstanceSearch.value = ''
|
||||
baseModal.value?.show()
|
||||
}
|
||||
|
||||
function confirmBaseInstance() {
|
||||
if (!baseOption.value || !baseInstanceId.value) return
|
||||
applyGlobalOption(baseOption.value, true, baseInstanceId.value)
|
||||
}
|
||||
|
||||
async function openCommandHistoryEditor() {
|
||||
@@ -502,6 +569,79 @@ watch(
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NewModal
|
||||
ref="baseModal"
|
||||
:header="formatMessage(messages.chooseSyncSourceTitle)"
|
||||
no-padding
|
||||
actions-divider
|
||||
max-width="560px"
|
||||
width="560px"
|
||||
>
|
||||
<p class="m-0 border-0 border-b border-solid border-surface-5 p-6 text-primary">
|
||||
{{ baseInstanceDescription }}
|
||||
</p>
|
||||
|
||||
<div class="flex h-[400px] flex-col gap-3 overflow-y-auto bg-surface-2 px-6 py-4">
|
||||
<Input
|
||||
v-model="baseInstanceSearch"
|
||||
:icon="SearchIcon"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
:placeholder="formatMessage(messages.searchInstance)"
|
||||
class="shrink-0"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="filteredBaseInstances.length === 0"
|
||||
class="flex flex-1 items-center justify-center text-secondary"
|
||||
>
|
||||
{{ formatMessage(messages.noInstancesFound) }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
role="radiogroup"
|
||||
:aria-label="formatMessage(messages.chooseSyncSourceTitle)"
|
||||
class="flex flex-col gap-1"
|
||||
>
|
||||
<CheckCircleButton
|
||||
v-for="instance in filteredBaseInstances"
|
||||
:key="instance.id"
|
||||
:checked="baseInstanceId === instance.id"
|
||||
class="h-10"
|
||||
@click="baseInstanceId = instance.id"
|
||||
>
|
||||
<span class="size-5 shrink-0 overflow-hidden rounded-[6px]">
|
||||
<Avatar
|
||||
:src="getInstanceIconUrl(instance.icon_path)"
|
||||
:alt="instance.name"
|
||||
:tint-by="instance.id"
|
||||
size="1.25rem"
|
||||
no-shadow
|
||||
/>
|
||||
</span>
|
||||
<span class="truncate">{{ instance.name }}</span>
|
||||
</CheckCircleButton>
|
||||
</div>
|
||||
</div>
|
||||
<template #actions>
|
||||
<div class="flex justify-end gap-2 p-2">
|
||||
<Button type="outlined" @click="baseModal?.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button
|
||||
type="colored"
|
||||
color="brand"
|
||||
:disabled="!baseInstanceId || globalOptionMutation.isPending.value"
|
||||
@click="confirmBaseInstance"
|
||||
>
|
||||
<RefreshCwIcon />
|
||||
{{ formatMessage(messages.syncButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
|
||||
<NewModal
|
||||
ref="commandHistoryModal"
|
||||
:header="formatMessage(messages.commandHistoryEditorTitle)"
|
||||
|
||||
@@ -338,10 +338,12 @@ export async function get_global_synced_options(): Promise<GlobalSyncedOptions>
|
||||
export async function set_global_synced_option(
|
||||
option: SyncedOption,
|
||||
enabled: boolean,
|
||||
baseInstanceId?: string,
|
||||
): Promise<GlobalSyncedOptions> {
|
||||
return await invoke('plugin:instance|instance_set_global_synced_option', {
|
||||
option,
|
||||
enabled,
|
||||
baseInstanceId,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -215,6 +215,12 @@
|
||||
"app.behavior-settings.content.title": {
|
||||
"message": "Home and content"
|
||||
},
|
||||
"app.behavior-settings.show-all-screenshots.description": {
|
||||
"message": "View screenshots from all your instances on the Screenshots page."
|
||||
},
|
||||
"app.behavior-settings.show-all-screenshots.title": {
|
||||
"message": "Show all screenshots together"
|
||||
},
|
||||
"app.behavior-settings.startup-and-navigation.title": {
|
||||
"message": "Startup and navigation"
|
||||
},
|
||||
@@ -1691,6 +1697,27 @@
|
||||
"app.settings.sidebar.label.instances": {
|
||||
"message": "Instances"
|
||||
},
|
||||
"app.settings.synced-options.choose-sync-source.command-history-description": {
|
||||
"message": "Pick the instance whose command history becomes the shared copy."
|
||||
},
|
||||
"app.settings.synced-options.choose-sync-source.creative-hotbars-description": {
|
||||
"message": "Pick the instance whose saved creative hotbars become the shared copy."
|
||||
},
|
||||
"app.settings.synced-options.choose-sync-source.multiplayer-servers-description": {
|
||||
"message": "Pick the instance whose multiplayer servers become the shared copy."
|
||||
},
|
||||
"app.settings.synced-options.choose-sync-source.no-instances-found": {
|
||||
"message": "No instances found"
|
||||
},
|
||||
"app.settings.synced-options.choose-sync-source.search-placeholder": {
|
||||
"message": "Search instance"
|
||||
},
|
||||
"app.settings.synced-options.choose-sync-source.sync": {
|
||||
"message": "Sync"
|
||||
},
|
||||
"app.settings.synced-options.choose-sync-source.title": {
|
||||
"message": "Choose a sync source"
|
||||
},
|
||||
"app.settings.synced-options.command-history": {
|
||||
"message": "Command history"
|
||||
},
|
||||
@@ -1727,12 +1754,6 @@
|
||||
"app.settings.synced-options.multiplayer-servers.none-synced-yet": {
|
||||
"message": "No servers synced yet"
|
||||
},
|
||||
"app.settings.synced-options.screenshots": {
|
||||
"message": "Screenshots"
|
||||
},
|
||||
"app.settings.synced-options.screenshots.description": {
|
||||
"message": "View screenshots from your instances in one place."
|
||||
},
|
||||
"app.settings.tabs.appearance": {
|
||||
"message": "Appearance"
|
||||
},
|
||||
@@ -2678,8 +2699,8 @@
|
||||
"instance.settings.tabs.synced-options.command-history.disabled-in-app": {
|
||||
"message": "Command history syncing is turned off in app settings."
|
||||
},
|
||||
"instance.settings.tabs.synced-options.command-history.exclude-description": {
|
||||
"message": "Exclude this instance from command history syncing."
|
||||
"instance.settings.tabs.synced-options.command-history.override-description": {
|
||||
"message": "Keep this instance's command history separate from synced command history."
|
||||
},
|
||||
"instance.settings.tabs.synced-options.creative-hotbars": {
|
||||
"message": "Saved creative hotbars"
|
||||
@@ -2687,8 +2708,8 @@
|
||||
"instance.settings.tabs.synced-options.creative-hotbars.disabled-in-app": {
|
||||
"message": "Saved creative hotbar syncing is turned off in app settings."
|
||||
},
|
||||
"instance.settings.tabs.synced-options.creative-hotbars.exclude-description": {
|
||||
"message": "Exclude this instance from saved creative hotbar syncing."
|
||||
"instance.settings.tabs.synced-options.creative-hotbars.override-description": {
|
||||
"message": "Keep this instance's saved creative hotbars separate from synced hotbars."
|
||||
},
|
||||
"instance.settings.tabs.synced-options.hotbars-conflict.backup-description": {
|
||||
"message": "The version being replaced will be backed up before anything changes."
|
||||
@@ -2711,23 +2732,14 @@
|
||||
"instance.settings.tabs.synced-options.multiplayer-servers.disabled-in-app": {
|
||||
"message": "Multiplayer server syncing is turned off in app settings."
|
||||
},
|
||||
"instance.settings.tabs.synced-options.multiplayer-servers.exclude-description": {
|
||||
"message": "Exclude this instance from multiplayer server syncing."
|
||||
"instance.settings.tabs.synced-options.multiplayer-servers.override-description": {
|
||||
"message": "Keep this instance's multiplayer servers separate from synced servers."
|
||||
},
|
||||
"instance.settings.tabs.synced-options.open-app-settings": {
|
||||
"message": "Open synced settings"
|
||||
},
|
||||
"instance.settings.tabs.synced-options.screenshots": {
|
||||
"message": "Screenshots"
|
||||
},
|
||||
"instance.settings.tabs.synced-options.screenshots.disabled-in-app": {
|
||||
"message": "Screenshots are turned off in app settings."
|
||||
},
|
||||
"instance.settings.tabs.synced-options.screenshots.exclude-description": {
|
||||
"message": "Exclude this instance’s screenshots from the Screenshots page."
|
||||
"message": "Manage synced settings"
|
||||
},
|
||||
"instance.settings.tabs.synced-options.shared-settings.description": {
|
||||
"message": "Game settings can be shared between instances. Choose what to share in app settings."
|
||||
"message": "Enable an override to keep a synced setting separate for this instance."
|
||||
},
|
||||
"instance.settings.tabs.window": {
|
||||
"message": "Window"
|
||||
|
||||
@@ -7,7 +7,6 @@ import { get } from '@/helpers/settings.ts'
|
||||
|
||||
import type { AppSettings } from '../../../../helpers/types'
|
||||
import { injectInstanceSettings } from './instance-settings-context'
|
||||
import SettingsOptionsTransition from './settings-options-transition.vue'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -28,6 +27,16 @@ const hooks = ref({
|
||||
post_exit: hooksRaw.post_exit ?? '',
|
||||
})
|
||||
|
||||
watch(overrideHooks, (enabled) => {
|
||||
if (!enabled) {
|
||||
hooks.value = {
|
||||
pre_launch: globalSettings.hooks.pre_launch ?? '',
|
||||
wrapper: globalSettings.hooks.wrapper ?? '',
|
||||
post_exit: globalSettings.hooks.post_exit ?? '',
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const editInstanceObject = computed(() => ({
|
||||
hooks: overrideHooks.value
|
||||
? {
|
||||
@@ -139,62 +148,63 @@ const messages = defineMessages({
|
||||
<Toggle id="override-launch-hooks" v-model="overrideHooks" />
|
||||
</div>
|
||||
|
||||
<SettingsOptionsTransition :show="overrideHooks">
|
||||
<div class="pt-6">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.preLaunch) }}
|
||||
</h2>
|
||||
<Input
|
||||
id="pre-launch"
|
||||
v-model="hooks.pre_launch"
|
||||
autocomplete="off"
|
||||
:placeholder="formatMessage(messages.preLaunchEnter)"
|
||||
wrapper-class="w-full my-2.5"
|
||||
/>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.preLaunchDescription) }}
|
||||
</p>
|
||||
<div class="pt-6" :class="{ 'opacity-50': !overrideHooks }">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.preLaunch) }}
|
||||
</h2>
|
||||
<Input
|
||||
id="pre-launch"
|
||||
v-model="hooks.pre_launch"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideHooks"
|
||||
:placeholder="formatMessage(messages.preLaunchEnter)"
|
||||
wrapper-class="w-full my-2.5"
|
||||
/>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.preLaunchDescription) }}
|
||||
</p>
|
||||
|
||||
<h2 class="mt-6 m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.wrapper) }}
|
||||
</h2>
|
||||
<Input
|
||||
id="wrapper"
|
||||
v-model="hooks.wrapper"
|
||||
autocomplete="off"
|
||||
:placeholder="formatMessage(messages.wrapperEnter)"
|
||||
wrapper-class="w-full my-2.5"
|
||||
/>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.wrapperDescription) }}
|
||||
</p>
|
||||
<h2 class="mt-6 m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.wrapper) }}
|
||||
</h2>
|
||||
<Input
|
||||
id="wrapper"
|
||||
v-model="hooks.wrapper"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideHooks"
|
||||
:placeholder="formatMessage(messages.wrapperEnter)"
|
||||
wrapper-class="w-full my-2.5"
|
||||
/>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.wrapperDescription) }}
|
||||
</p>
|
||||
|
||||
<h2 class="mt-6 m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.postExit) }}
|
||||
</h2>
|
||||
<Input
|
||||
id="post-exit"
|
||||
v-model="hooks.post_exit"
|
||||
autocomplete="off"
|
||||
:placeholder="formatMessage(messages.postExitEnter)"
|
||||
wrapper-class="w-full my-2.5"
|
||||
/>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.postExitDescription) }}
|
||||
</p>
|
||||
<h2 class="mt-6 m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.postExit) }}
|
||||
</h2>
|
||||
<Input
|
||||
id="post-exit"
|
||||
v-model="hooks.post_exit"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideHooks"
|
||||
:placeholder="formatMessage(messages.postExitEnter)"
|
||||
wrapper-class="w-full my-2.5"
|
||||
/>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.postExitDescription) }}
|
||||
</p>
|
||||
|
||||
<div class="m-0 mt-6">
|
||||
{{ formatMessage(messages.hookVariablesDescription) }}
|
||||
</div>
|
||||
<ul class="m-0 mt-2">
|
||||
<li>{{ formatMessage(messages.instanceNameDescription) }}</li>
|
||||
<li>{{ formatMessage(messages.instanceIdDescription) }}</li>
|
||||
<li>{{ formatMessage(messages.instanceDirDescription) }}</li>
|
||||
<li>{{ formatMessage(messages.instanceMcDirDescription) }}</li>
|
||||
<li>{{ formatMessage(messages.instanceJavaDescription) }}</li>
|
||||
<li>{{ formatMessage(messages.instanceJavaArgsDescription) }}</li>
|
||||
</ul>
|
||||
<div class="m-0 mt-6">
|
||||
{{ formatMessage(messages.hookVariablesDescription) }}
|
||||
</div>
|
||||
</SettingsOptionsTransition>
|
||||
<ul class="m-0 mt-2">
|
||||
<li>{{ formatMessage(messages.instanceNameDescription) }}</li>
|
||||
<li>{{ formatMessage(messages.instanceIdDescription) }}</li>
|
||||
<li>{{ formatMessage(messages.instanceDirDescription) }}</li>
|
||||
<li>{{ formatMessage(messages.instanceMcDirDescription) }}</li>
|
||||
<li>{{ formatMessage(messages.instanceJavaDescription) }}</li>
|
||||
<li>{{ formatMessage(messages.instanceJavaArgsDescription) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+132
-116
@@ -28,7 +28,6 @@ import { get, parseEnvVars, serializeEnvVars } from '@/helpers/settings.ts'
|
||||
|
||||
import type { AppSettings } from '../../../../helpers/types'
|
||||
import { injectInstanceSettings } from './instance-settings-context'
|
||||
import SettingsOptionsTransition from './settings-options-transition.vue'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -42,11 +41,14 @@ const optimalJava = readonly(await get_optimal_jre_key(instance.value.id).catch(
|
||||
const overrideJavaInstall = ref(!!instance.value.java_path)
|
||||
const javaPath = ref(instance.value.java_path ?? optimalJava?.path ?? '')
|
||||
|
||||
const activePath = computed(() => (overrideJavaInstall.value ? javaPath.value : ''))
|
||||
const activePath = computed(() => javaPath.value)
|
||||
const javaTestPath = computed(() => (overrideJavaInstall.value ? javaPath.value : ''))
|
||||
|
||||
watch(overrideJavaInstall, (enabled) => {
|
||||
if (enabled && !javaPath.value) {
|
||||
javaPath.value = optimalJava?.path ?? ''
|
||||
} else if (!enabled) {
|
||||
javaPath.value = optimalJava?.path ?? ''
|
||||
}
|
||||
})
|
||||
|
||||
@@ -57,7 +59,7 @@ const hoveringTest = ref(false)
|
||||
let hasInitialized = false
|
||||
|
||||
watch(
|
||||
activePath,
|
||||
javaTestPath,
|
||||
(newPath) => {
|
||||
if (newPath && optimalJava?.parsed_version) {
|
||||
if (!hasInitialized) {
|
||||
@@ -95,12 +97,30 @@ const envVars = ref(
|
||||
)
|
||||
|
||||
const overrideMemorySettings = ref(!!instance.value.memory)
|
||||
const memory = ref(instance.value.memory ?? globalSettings.memory)
|
||||
const memory = ref(instance.value.memory ?? { ...globalSettings.memory })
|
||||
const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) as unknown as {
|
||||
maxMemory: number
|
||||
snapPoints: number[]
|
||||
}
|
||||
|
||||
watch(overrideJavaArgs, (enabled) => {
|
||||
if (!enabled) {
|
||||
javaArgs.value = globalSettings.extra_launch_args.join(' ')
|
||||
}
|
||||
})
|
||||
|
||||
watch(overrideEnvVars, (enabled) => {
|
||||
if (!enabled) {
|
||||
envVars.value = serializeEnvVars(globalSettings.custom_env_vars)
|
||||
}
|
||||
})
|
||||
|
||||
watch(overrideMemorySettings, (enabled) => {
|
||||
if (!enabled) {
|
||||
memory.value = { ...globalSettings.memory }
|
||||
}
|
||||
})
|
||||
|
||||
const editInstanceObject = computed(() => {
|
||||
return {
|
||||
java_path:
|
||||
@@ -198,90 +218,89 @@ const messages = defineMessages({
|
||||
</div>
|
||||
<Toggle id="override-java-installation" v-model="overrideJavaInstall" />
|
||||
</div>
|
||||
<SettingsOptionsTransition :show="overrideJavaInstall">
|
||||
<div class="pt-3">
|
||||
<div class="flex gap-4 rounded-2xl bg-bg p-4">
|
||||
<div class="flex gap-3 items-start flex-1 min-w-0">
|
||||
<div
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full bg-button-bg border-solid border-[1px] border-button-border p-2 mt-1 shrink-0 [&_svg]:h-full [&_svg]:w-full"
|
||||
<div class="pt-3" :class="{ 'opacity-50': !overrideJavaInstall }">
|
||||
<div class="flex gap-4 rounded-2xl bg-bg p-4">
|
||||
<div class="flex gap-3 items-start flex-1 min-w-0">
|
||||
<div
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full bg-button-bg border-solid border-[1px] border-button-border p-2 mt-1 shrink-0 [&_svg]:h-full [&_svg]:w-full"
|
||||
>
|
||||
<CoffeeIcon />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 flex-1 min-w-0">
|
||||
<span class="font-semibold leading-none mt-2"
|
||||
>Java {{ optimalJava?.parsed_version }}</span
|
||||
>
|
||||
<CoffeeIcon />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 flex-1 min-w-0">
|
||||
<span class="font-semibold leading-none mt-2"
|
||||
>Java {{ optimalJava?.parsed_version }}</span
|
||||
>
|
||||
<div class="flex gap-2 items-center">
|
||||
<Input
|
||||
:model-value="activePath"
|
||||
autocomplete="off"
|
||||
:placeholder="formatMessage(messages.javaPathPlaceholder)"
|
||||
wrapper-class="flex-1 min-w-0"
|
||||
@update:model-value="(val) => (javaPath = String(val))"
|
||||
/>
|
||||
<Button
|
||||
type="quiet"
|
||||
:color="
|
||||
!hoveringTest && !testingJava
|
||||
<div class="flex gap-2 items-center">
|
||||
<Input
|
||||
:model-value="activePath"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideJavaInstall"
|
||||
:placeholder="formatMessage(messages.javaPathPlaceholder)"
|
||||
wrapper-class="flex-1 min-w-0"
|
||||
@update:model-value="(val) => (javaPath = String(val))"
|
||||
/>
|
||||
<Button
|
||||
type="quiet"
|
||||
:color="
|
||||
overrideJavaInstall && !hoveringTest && !testingJava
|
||||
? javaTestResult === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: undefined
|
||||
"
|
||||
:disabled="!overrideJavaInstall || testingJava"
|
||||
:style="{
|
||||
'--legacy-button-color':
|
||||
(overrideJavaInstall && !hoveringTest && !testingJava
|
||||
? javaTestResult === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: undefined
|
||||
"
|
||||
:disabled="testingJava"
|
||||
:style="{
|
||||
'--legacy-button-color':
|
||||
(!hoveringTest && !testingJava
|
||||
? javaTestResult === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard') &&
|
||||
(!hoveringTest && !testingJava
|
||||
? javaTestResult === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard') !== 'standard'
|
||||
? `var(--color-${
|
||||
!hoveringTest && !testingJava
|
||||
? javaTestResult === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard'
|
||||
})`
|
||||
: undefined,
|
||||
}"
|
||||
class="!text-[var(--legacy-button-color,var(--color-base))] [&>svg]:!text-[var(--legacy-button-color,var(--color-primary))]"
|
||||
@click="testJavaInstallation(activePath, optimalJava?.parsed_version, true)"
|
||||
@mouseenter="hoveringTest = true"
|
||||
@mouseleave="hoveringTest = false"
|
||||
>
|
||||
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
|
||||
<CheckCircleIcon
|
||||
v-else-if="javaTestResult === true && !hoveringTest"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<XCircleIcon
|
||||
v-else-if="javaTestResult !== true && !hoveringTest"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<RefreshCwIcon v-else class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button @click="handleDetectJava">
|
||||
<SearchIcon />
|
||||
Detect
|
||||
</Button>
|
||||
<Button @click="handleBrowseJava">
|
||||
<FolderSearchIcon />
|
||||
Browse
|
||||
</Button>
|
||||
</div>
|
||||
: 'standard') &&
|
||||
(overrideJavaInstall && !hoveringTest && !testingJava
|
||||
? javaTestResult === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard') !== 'standard'
|
||||
? `var(--color-${
|
||||
overrideJavaInstall && !hoveringTest && !testingJava
|
||||
? javaTestResult === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard'
|
||||
})`
|
||||
: undefined,
|
||||
}"
|
||||
class="!text-[var(--legacy-button-color,var(--color-base))] [&>svg]:!text-[var(--legacy-button-color,var(--color-primary))]"
|
||||
@click="testJavaInstallation(activePath, optimalJava?.parsed_version, true)"
|
||||
@mouseenter="hoveringTest = true"
|
||||
@mouseleave="hoveringTest = false"
|
||||
>
|
||||
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
|
||||
<CheckCircleIcon
|
||||
v-else-if="overrideJavaInstall && javaTestResult === true && !hoveringTest"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<XCircleIcon
|
||||
v-else-if="overrideJavaInstall && javaTestResult !== true && !hoveringTest"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<RefreshCwIcon v-else class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button :disabled="!overrideJavaInstall" @click="handleDetectJava">
|
||||
<SearchIcon />
|
||||
Detect
|
||||
</Button>
|
||||
<Button :disabled="!overrideJavaInstall" @click="handleBrowseJava">
|
||||
<FolderSearchIcon />
|
||||
Browse
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsOptionsTransition>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col">
|
||||
@@ -294,20 +313,19 @@ const messages = defineMessages({
|
||||
</div>
|
||||
<Toggle id="override-memory-allocation" v-model="overrideMemorySettings" />
|
||||
</div>
|
||||
<SettingsOptionsTransition :show="overrideMemorySettings">
|
||||
<div class="pt-3">
|
||||
<Slider
|
||||
id="max-memory"
|
||||
v-model="memory.maximum"
|
||||
:min="512"
|
||||
:max="maxMemory"
|
||||
:step="64"
|
||||
:snap-points="snapPoints"
|
||||
:snap-range="512"
|
||||
unit="MB"
|
||||
/>
|
||||
</div>
|
||||
</SettingsOptionsTransition>
|
||||
<div class="pt-3">
|
||||
<Slider
|
||||
id="max-memory"
|
||||
v-model="memory.maximum"
|
||||
:disabled="!overrideMemorySettings"
|
||||
:min="512"
|
||||
:max="maxMemory"
|
||||
:step="64"
|
||||
:snap-points="snapPoints"
|
||||
:snap-range="512"
|
||||
unit="MB"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col">
|
||||
@@ -320,17 +338,16 @@ const messages = defineMessages({
|
||||
</div>
|
||||
<Toggle id="override-java-arguments" v-model="overrideJavaArgs" />
|
||||
</div>
|
||||
<SettingsOptionsTransition :show="overrideJavaArgs">
|
||||
<div class="pt-3">
|
||||
<Input
|
||||
id="java-args"
|
||||
v-model="javaArgs"
|
||||
autocomplete="off"
|
||||
:placeholder="formatMessage(messages.enterJavaArguments)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</SettingsOptionsTransition>
|
||||
<div class="pt-3" :class="{ 'opacity-50': !overrideJavaArgs }">
|
||||
<Input
|
||||
id="java-args"
|
||||
v-model="javaArgs"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideJavaArgs"
|
||||
:placeholder="formatMessage(messages.enterJavaArguments)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col">
|
||||
@@ -343,17 +360,16 @@ const messages = defineMessages({
|
||||
</div>
|
||||
<Toggle id="override-environment-variables" v-model="overrideEnvVars" />
|
||||
</div>
|
||||
<SettingsOptionsTransition :show="overrideEnvVars">
|
||||
<div class="pt-3">
|
||||
<Input
|
||||
id="env-vars"
|
||||
v-model="envVars"
|
||||
autocomplete="off"
|
||||
:placeholder="formatMessage(messages.enterEnvironmentVariables)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</SettingsOptionsTransition>
|
||||
<div class="pt-3" :class="{ 'opacity-50': !overrideEnvVars }">
|
||||
<Input
|
||||
id="env-vars"
|
||||
v-model="envVars"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideEnvVars"
|
||||
:placeholder="formatMessage(messages.enterEnvironmentVariables)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+112
-81
@@ -1,11 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
EditIcon,
|
||||
RefreshCwIcon,
|
||||
RotateCounterClockwiseIcon,
|
||||
SpinnerIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { EditIcon, RefreshCwIcon, RotateCounterClockwiseIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Button,
|
||||
commonMessages,
|
||||
@@ -17,7 +11,6 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import {
|
||||
get_synced_option_join_preview,
|
||||
@@ -29,7 +22,7 @@ import {
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { appSettingsModalOpenSyncedOptionsKey } from '@/providers/app-settings-modal'
|
||||
|
||||
import { instanceKeys, screenshotKeys } from '../../query-options'
|
||||
import { instanceKeys } from '../../query-options'
|
||||
import HooksSettings from './hooks-settings.vue'
|
||||
import { injectInstanceSettings } from './instance-settings-context'
|
||||
import JavaSettings from './java-settings.vue'
|
||||
@@ -39,27 +32,24 @@ const { instance, closeModal } = injectInstanceSettings()
|
||||
const { formatMessage } = useVIntl()
|
||||
const { handleError } = injectNotificationManager()
|
||||
const queryClient = useQueryClient()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const openAppSettingsSyncedOptions = inject(appSettingsModalOpenSyncedOptionsKey, () => {})
|
||||
|
||||
const messages = defineMessages({
|
||||
sharedSettingsDescription: {
|
||||
id: 'instance.settings.tabs.synced-options.shared-settings.description',
|
||||
defaultMessage:
|
||||
'Game settings can be shared between instances. Choose what to share in app settings.',
|
||||
defaultMessage: 'Enable an override to keep a synced setting separate for this instance.',
|
||||
},
|
||||
openSyncedOptions: {
|
||||
id: 'instance.settings.tabs.synced-options.open-app-settings',
|
||||
defaultMessage: 'Open synced settings',
|
||||
defaultMessage: 'Manage synced settings',
|
||||
},
|
||||
multiplayerServers: {
|
||||
id: 'instance.settings.tabs.synced-options.multiplayer-servers',
|
||||
defaultMessage: 'Multiplayer servers',
|
||||
},
|
||||
multiplayerServersDescription: {
|
||||
id: 'instance.settings.tabs.synced-options.multiplayer-servers.exclude-description',
|
||||
defaultMessage: 'Exclude this instance from multiplayer server syncing.',
|
||||
id: 'instance.settings.tabs.synced-options.multiplayer-servers.override-description',
|
||||
defaultMessage: "Keep this instance's multiplayer servers separate from synced servers.",
|
||||
},
|
||||
multiplayerServersDisabled: {
|
||||
id: 'instance.settings.tabs.synced-options.multiplayer-servers.disabled-in-app',
|
||||
@@ -70,8 +60,8 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Command history',
|
||||
},
|
||||
commandHistoryDescription: {
|
||||
id: 'instance.settings.tabs.synced-options.command-history.exclude-description',
|
||||
defaultMessage: 'Exclude this instance from command history syncing.',
|
||||
id: 'instance.settings.tabs.synced-options.command-history.override-description',
|
||||
defaultMessage: "Keep this instance's command history separate from synced command history.",
|
||||
},
|
||||
commandHistoryDisabled: {
|
||||
id: 'instance.settings.tabs.synced-options.command-history.disabled-in-app',
|
||||
@@ -82,25 +72,13 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Saved creative hotbars',
|
||||
},
|
||||
creativeHotbarsDescription: {
|
||||
id: 'instance.settings.tabs.synced-options.creative-hotbars.exclude-description',
|
||||
defaultMessage: 'Exclude this instance from saved creative hotbar syncing.',
|
||||
id: 'instance.settings.tabs.synced-options.creative-hotbars.override-description',
|
||||
defaultMessage: "Keep this instance's saved creative hotbars separate from synced hotbars.",
|
||||
},
|
||||
creativeHotbarsDisabled: {
|
||||
id: 'instance.settings.tabs.synced-options.creative-hotbars.disabled-in-app',
|
||||
defaultMessage: 'Saved creative hotbar syncing is turned off in app settings.',
|
||||
},
|
||||
screenshots: {
|
||||
id: 'instance.settings.tabs.synced-options.screenshots',
|
||||
defaultMessage: 'Screenshots',
|
||||
},
|
||||
screenshotsDescription: {
|
||||
id: 'instance.settings.tabs.synced-options.screenshots.exclude-description',
|
||||
defaultMessage: 'Exclude this instance’s screenshots from the Screenshots page.',
|
||||
},
|
||||
screenshotsDisabled: {
|
||||
id: 'instance.settings.tabs.synced-options.screenshots.disabled-in-app',
|
||||
defaultMessage: 'Screenshots are turned off in app settings.',
|
||||
},
|
||||
hotbarConflictTitle: {
|
||||
id: 'instance.settings.tabs.synced-options.hotbars-conflict.title',
|
||||
defaultMessage: 'Choose creative hotbars',
|
||||
@@ -124,15 +102,16 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
const globalDisabledMessages: Record<SyncedOption, keyof typeof messages> = {
|
||||
type InstanceSyncedOption = Exclude<SyncedOption, 'screenshots'>
|
||||
|
||||
const globalDisabledMessages: Record<InstanceSyncedOption, keyof typeof messages> = {
|
||||
multiplayer_servers: 'multiplayerServersDisabled',
|
||||
command_history: 'commandHistoryDisabled',
|
||||
creative_hotbars: 'creativeHotbarsDisabled',
|
||||
screenshots: 'screenshotsDisabled',
|
||||
}
|
||||
|
||||
const rows: Array<{
|
||||
option: SyncedOption
|
||||
option: InstanceSyncedOption
|
||||
title: keyof typeof messages
|
||||
description?: keyof typeof messages
|
||||
}> = [
|
||||
@@ -151,11 +130,6 @@ const rows: Array<{
|
||||
title: 'creativeHotbars',
|
||||
description: 'creativeHotbarsDescription',
|
||||
},
|
||||
{
|
||||
option: 'screenshots',
|
||||
title: 'screenshots',
|
||||
description: 'screenshotsDescription',
|
||||
},
|
||||
]
|
||||
|
||||
const overviewQuery = useQuery(
|
||||
@@ -173,16 +147,29 @@ const capabilities = computed(
|
||||
),
|
||||
)
|
||||
const hotbarResolutionModal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
const previewingOption = ref<SyncedOption | null>(null)
|
||||
const previewingOption = ref<InstanceSyncedOption | null>(null)
|
||||
const previewExcluded = ref<Partial<Record<InstanceSyncedOption, boolean>>>({})
|
||||
|
||||
function excluded(option: InstanceSyncedOption): boolean {
|
||||
const preview = previewExcluded.value[option]
|
||||
if (preview !== undefined) return preview
|
||||
|
||||
function excluded(option: SyncedOption): boolean {
|
||||
return (
|
||||
overviewQuery.data.value?.global_options[option] === true &&
|
||||
!instance.value.synced_options[option]
|
||||
)
|
||||
}
|
||||
|
||||
function disabledReason(option: SyncedOption): string | undefined {
|
||||
function setPreviewExcluded(option: InstanceSyncedOption, value?: boolean) {
|
||||
if (value === undefined) {
|
||||
const { [option]: _, ...next } = previewExcluded.value
|
||||
previewExcluded.value = next
|
||||
} else {
|
||||
previewExcluded.value = { ...previewExcluded.value, [option]: value }
|
||||
}
|
||||
}
|
||||
|
||||
function disabledReason(option: InstanceSyncedOption): string | undefined {
|
||||
if (overviewQuery.data.value?.global_options[option] === false) {
|
||||
return formatMessage(messages[globalDisabledMessages[option]])
|
||||
}
|
||||
@@ -194,52 +181,97 @@ function showAppSyncedOptions(): void {
|
||||
openAppSettingsSyncedOptions()
|
||||
}
|
||||
|
||||
type SyncedOptionMutationVariables = {
|
||||
option: InstanceSyncedOption
|
||||
enabled: boolean
|
||||
resolution?: SyncedOptionJoinResolution
|
||||
}
|
||||
|
||||
const mutationKey = ['instance-synced-options', 'set', instance.value.id] as const
|
||||
const mutation = useMutation({
|
||||
mutationFn: ({
|
||||
option,
|
||||
enabled,
|
||||
resolution,
|
||||
}: {
|
||||
option: SyncedOption
|
||||
enabled: boolean
|
||||
resolution?: SyncedOptionJoinResolution
|
||||
}) => set_synced_option(instance.value.id, option, enabled, resolution),
|
||||
onSuccess: async (updatedInstance, variables) => {
|
||||
hotbarResolutionModal.value?.hide()
|
||||
queryClient.setQueryData(instanceKeys.detail(updatedInstance.id), updatedInstance)
|
||||
queryClient.setQueryData<GameInstance[]>(instanceKeys.list(), (instances) =>
|
||||
mutationKey,
|
||||
mutationFn: ({ option, enabled, resolution }: SyncedOptionMutationVariables) =>
|
||||
set_synced_option(instance.value.id, option, enabled, resolution),
|
||||
onMutate: async ({ option, enabled }) => {
|
||||
const instanceId = instance.value.id
|
||||
const detailKey = instanceKeys.detail(instanceId)
|
||||
const listKey = instanceKeys.list()
|
||||
await Promise.all([
|
||||
queryClient.cancelQueries({ queryKey: detailKey }),
|
||||
queryClient.cancelQueries({ queryKey: listKey }),
|
||||
])
|
||||
|
||||
const previousEnabled = instance.value.synced_options[option]
|
||||
const applyOption = (current: GameInstance): GameInstance => ({
|
||||
...current,
|
||||
synced_options: {
|
||||
...current.synced_options,
|
||||
[option]: enabled,
|
||||
},
|
||||
})
|
||||
|
||||
queryClient.setQueryData<GameInstance>(detailKey, (current) =>
|
||||
applyOption(current ?? instance.value),
|
||||
)
|
||||
queryClient.setQueryData<GameInstance[]>(listKey, (instances) =>
|
||||
instances?.map((candidate) =>
|
||||
candidate.id === updatedInstance.id ? updatedInstance : candidate,
|
||||
candidate.id === instanceId ? applyOption(candidate) : candidate,
|
||||
),
|
||||
)
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['instance-synced-options', updatedInstance.id],
|
||||
})
|
||||
setPreviewExcluded(option)
|
||||
|
||||
return { instanceId, previousEnabled }
|
||||
},
|
||||
onSuccess: () => {
|
||||
hotbarResolutionModal.value?.hide()
|
||||
},
|
||||
onError: (error, { option }, context) => {
|
||||
if (context) {
|
||||
const rollbackOption = (current: GameInstance): GameInstance => ({
|
||||
...current,
|
||||
synced_options: {
|
||||
...current.synced_options,
|
||||
[option]: context.previousEnabled,
|
||||
},
|
||||
})
|
||||
queryClient.setQueryData<GameInstance>(instanceKeys.detail(context.instanceId), (current) =>
|
||||
current ? rollbackOption(current) : current,
|
||||
)
|
||||
queryClient.setQueryData<GameInstance[]>(instanceKeys.list(), (instances) =>
|
||||
instances?.map((candidate) =>
|
||||
candidate.id === context.instanceId ? rollbackOption(candidate) : candidate,
|
||||
),
|
||||
)
|
||||
}
|
||||
setPreviewExcluded(option)
|
||||
handleError(error)
|
||||
},
|
||||
onSettled: async (_data, _error, variables) => {
|
||||
if (variables.option === 'multiplayer_servers') {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: instanceKeys.worlds(updatedInstance.id),
|
||||
queryKey: instanceKeys.worlds(instance.value.id),
|
||||
})
|
||||
}
|
||||
|
||||
if (variables.option === 'screenshots') {
|
||||
await queryClient.invalidateQueries({ queryKey: screenshotKeys.all })
|
||||
if (updatedInstance.synced_options.screenshots && route.name === 'InstanceScreenshots') {
|
||||
await router.replace(`/instance/${encodeURIComponent(updatedInstance.id)}`)
|
||||
} else if (!updatedInstance.synced_options.screenshots && route.name === 'Screenshots') {
|
||||
await router.replace('/')
|
||||
}
|
||||
if (queryClient.isMutating({ mutationKey }) === 1) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: instanceKeys.detail(instance.value.id) }),
|
||||
queryClient.invalidateQueries({ queryKey: instanceKeys.list() }),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['instance-synced-options', instance.value.id],
|
||||
}),
|
||||
])
|
||||
}
|
||||
},
|
||||
onError: handleError,
|
||||
})
|
||||
|
||||
async function setExcluded(option: SyncedOption, nextExcluded: boolean) {
|
||||
async function setExcluded(option: InstanceSyncedOption, nextExcluded: boolean) {
|
||||
const enabled = !nextExcluded
|
||||
if (!enabled || option !== 'creative_hotbars') {
|
||||
mutation.mutate({ option, enabled })
|
||||
return
|
||||
}
|
||||
|
||||
setPreviewExcluded(option, nextExcluded)
|
||||
previewingOption.value = option
|
||||
try {
|
||||
const preview = await get_synced_option_join_preview(instance.value.id, option)
|
||||
@@ -249,12 +281,18 @@ async function setExcluded(option: SyncedOption, nextExcluded: boolean) {
|
||||
mutation.mutate({ option, enabled })
|
||||
}
|
||||
} catch (error) {
|
||||
setPreviewExcluded(option)
|
||||
handleError(error)
|
||||
} finally {
|
||||
previewingOption.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function cancelHotbarResolution() {
|
||||
setPreviewExcluded('creative_hotbars')
|
||||
hotbarResolutionModal.value?.hide()
|
||||
}
|
||||
|
||||
function resolveHotbars(resolution: SyncedOptionJoinResolution) {
|
||||
mutation.mutate({
|
||||
option: 'creative_hotbars',
|
||||
@@ -271,6 +309,7 @@ function resolveHotbars(resolution: SyncedOptionJoinResolution) {
|
||||
:header="formatMessage(messages.hotbarConflictTitle)"
|
||||
fade="warning"
|
||||
max-width="560px"
|
||||
@hide="setPreviewExcluded('creative_hotbars')"
|
||||
>
|
||||
<div class="flex flex-col gap-3 text-primary">
|
||||
<p class="m-0">
|
||||
@@ -289,7 +328,7 @@ function resolveHotbars(resolution: SyncedOptionJoinResolution) {
|
||||
<Button
|
||||
type="outlined"
|
||||
:disabled="mutation.isPending.value"
|
||||
@click="hotbarResolutionModal?.hide()"
|
||||
@click="cancelHotbarResolution"
|
||||
>
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
@@ -336,20 +375,12 @@ function resolveHotbars(resolution: SyncedOptionJoinResolution) {
|
||||
{{ formatMessage(messages[row.description]) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<SpinnerIcon
|
||||
v-if="
|
||||
(mutation.isPending.value && mutation.variables.value?.option === row.option) ||
|
||||
previewingOption === row.option
|
||||
"
|
||||
class="size-5 animate-spin"
|
||||
/>
|
||||
<div class="flex shrink-0 items-center">
|
||||
<span v-tooltip="disabledReason(row.option)" class="flex">
|
||||
<Toggle
|
||||
:id="`exclude-${row.option}`"
|
||||
:model-value="excluded(row.option)"
|
||||
:disabled="
|
||||
mutation.isPending.value ||
|
||||
previewingOption !== null ||
|
||||
overviewQuery.isPending.value ||
|
||||
!!disabledReason(row.option)
|
||||
|
||||
+56
-52
@@ -7,7 +7,6 @@ import { get } from '@/helpers/settings.ts'
|
||||
|
||||
import type { AppSettings } from '../../../../helpers/types'
|
||||
import { injectInstanceSettings } from './instance-settings-context'
|
||||
import SettingsOptionsTransition from './settings-options-transition.vue'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -26,6 +25,13 @@ const fullscreenSetting: Ref<boolean> = ref(
|
||||
instance.value.force_fullscreen ?? globalSettings.force_fullscreen,
|
||||
)
|
||||
|
||||
watch(overrideWindowSettings, (enabled) => {
|
||||
if (!enabled) {
|
||||
resolution.value = globalSettings.game_resolution.slice() as [number, number]
|
||||
fullscreenSetting.value = globalSettings.force_fullscreen
|
||||
}
|
||||
})
|
||||
|
||||
const editInstanceObject = computed(() => {
|
||||
if (!overrideWindowSettings.value) {
|
||||
return {
|
||||
@@ -102,58 +108,56 @@ const messages = defineMessages({
|
||||
</div>
|
||||
<Toggle id="override-window-settings" v-model="overrideWindowSettings" />
|
||||
</div>
|
||||
<SettingsOptionsTransition :show="overrideWindowSettings">
|
||||
<div class="flex flex-col gap-6 pt-6">
|
||||
<div class="flex items-center gap-4 justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.fullscreen) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.fullscreenDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle id="fullscreen" v-model="fullscreenSetting" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.width) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.widthDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Input
|
||||
id="width"
|
||||
v-model="resolution[0]"
|
||||
autocomplete="off"
|
||||
:disabled="fullscreenSetting"
|
||||
type="number"
|
||||
:placeholder="formatMessage(messages.enterWidth)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.height) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.heightDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Input
|
||||
id="height"
|
||||
v-model="resolution[1]"
|
||||
autocomplete="off"
|
||||
:disabled="fullscreenSetting"
|
||||
type="number"
|
||||
:placeholder="formatMessage(messages.enterHeight)"
|
||||
/>
|
||||
<div class="flex flex-col gap-6 pt-6" :class="{ 'opacity-50': !overrideWindowSettings }">
|
||||
<div class="flex items-center gap-4 justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.fullscreen) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.fullscreenDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle id="fullscreen" v-model="fullscreenSetting" :disabled="!overrideWindowSettings" />
|
||||
</div>
|
||||
</SettingsOptionsTransition>
|
||||
|
||||
<div class="flex items-center gap-4 justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.width) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.widthDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Input
|
||||
id="width"
|
||||
v-model="resolution[0]"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideWindowSettings || fullscreenSetting"
|
||||
type="number"
|
||||
:placeholder="formatMessage(messages.enterWidth)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.height) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.heightDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Input
|
||||
id="height"
|
||||
v-model="resolution[1]"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideWindowSettings || fullscreenSetting"
|
||||
type="number"
|
||||
:placeholder="formatMessage(messages.enterHeight)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -502,9 +502,7 @@ const tabs = computed(() => {
|
||||
},
|
||||
]
|
||||
|
||||
const screenshotsSynced =
|
||||
globalSyncedOptionsQuery.data.value?.screenshots === true &&
|
||||
instance.value?.synced_options.screenshots === true
|
||||
const screenshotsSynced = globalSyncedOptionsQuery.data.value?.screenshots === true
|
||||
if (!screenshotsSynced) {
|
||||
instanceTabs.splice(2, 0, {
|
||||
label: formatMessage(messages.screenshotsTab),
|
||||
|
||||
Reference in New Issue
Block a user