refactor: TabbedModal to use NewModal and DI (#5612)

* refactor: tabbed modal to use NewModal

* refactor: use DI for instance settings modal instead of passing down props

* pnpm prepr
This commit is contained in:
Truman Gao
2026-03-19 09:53:53 -07:00
committed by GitHub
parent 93c81631a9
commit 2128fa7ade
11 changed files with 585 additions and 239 deletions

View File

@@ -18,8 +18,9 @@ import { useRouter } from 'vue-router'
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
import { trackEvent } from '@/helpers/analytics'
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
import { injectInstanceSettings } from '@/providers/instance-settings'
import type { GameInstance, InstanceSettingsTabProps } from '../../../helpers/types'
import type { GameInstance } from '../../../helpers/types'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
@@ -27,21 +28,21 @@ const router = useRouter()
const deleteConfirmModal = ref()
const props = defineProps<InstanceSettingsTabProps>()
const { instance } = injectInstanceSettings()
const title = ref(props.instance.name)
const icon: Ref<string | undefined> = ref(props.instance.icon_path)
const groups = ref(props.instance.groups)
const title = ref(instance.name)
const icon: Ref<string | undefined> = ref(instance.icon_path)
const groups = ref(instance.groups)
const newCategoryInput = ref('')
const installing = computed(() => props.instance.install_stage !== 'installed')
const installing = computed(() => instance.install_stage !== 'installed')
async function duplicateProfile() {
await duplicate(props.instance.path).catch(handleError)
await duplicate(instance.path).catch(handleError)
trackEvent('InstanceDuplicate', {
loader: props.instance.loader,
game_version: props.instance.game_version,
loader: instance.loader,
game_version: instance.game_version,
})
}
@@ -52,7 +53,7 @@ const availableGroups = computed(() => [
async function resetIcon() {
icon.value = undefined
await edit_icon(props.instance.path, null).catch(handleError)
await edit_icon(instance.path, null).catch(handleError)
trackEvent('InstanceRemoveIcon')
}
@@ -70,7 +71,7 @@ async function setIcon() {
if (!value) return
icon.value = value
await edit_icon(props.instance.path, icon.value).catch(handleError)
await edit_icon(instance.path, icon.value).catch(handleError)
trackEvent('InstanceSetIcon')
}
@@ -101,7 +102,7 @@ watch(
[title, groups, groups],
async () => {
if (removing.value) return
await edit(props.instance.path, editProfileObject.value).catch(handleError)
await edit(instance.path, editProfileObject.value).catch(handleError)
},
{ deep: true },
)
@@ -109,11 +110,11 @@ watch(
const removing = ref(false)
async function removeProfile() {
removing.value = true
const path = props.instance.path
const path = instance.path
trackEvent('InstanceRemove', {
loader: props.instance.loader,
game_version: props.instance.game_version,
loader: instance.loader,
game_version: instance.game_version,
})
await router.push({ path: '/' })
@@ -218,7 +219,7 @@ const messages = defineMessages({
:src="icon ? convertFileSrc(icon) : icon"
size="108px"
class="!border-4 group-hover:brightness-75"
:tint-by="props.instance.path"
:tint-by="instance.path"
no-shadow
/>
<div class="absolute top-0 right-0 m-2">

View File

@@ -10,22 +10,21 @@ import { computed, ref, watch } from 'vue'
import { edit } from '@/helpers/profile'
import { get } from '@/helpers/settings.ts'
import { injectInstanceSettings } from '@/providers/instance-settings'
import type { AppSettings, Hooks, InstanceSettingsTabProps } from '../../../helpers/types'
import type { AppSettings, Hooks } from '../../../helpers/types'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const props = defineProps<InstanceSettingsTabProps>()
const { instance } = injectInstanceSettings()
const globalSettings = (await get().catch(handleError)) as AppSettings
const overrideHooks = ref(
!!props.instance.hooks.pre_launch ||
!!props.instance.hooks.wrapper ||
!!props.instance.hooks.post_exit,
!!instance.hooks.pre_launch || !!instance.hooks.wrapper || !!instance.hooks.post_exit,
)
const hooks = ref(props.instance.hooks ?? globalSettings.hooks)
const hooks = ref(instance.hooks ?? globalSettings.hooks)
const editProfileObject = computed(() => {
const editProfile: {
@@ -41,7 +40,7 @@ const editProfileObject = computed(() => {
watch(
[overrideHooks, hooks],
async () => {
await edit(props.instance.path, editProfileObject.value)
await edit(instance.path, editProfileObject.value)
},
{ deep: true },
)

View File

@@ -27,17 +27,15 @@ import {
update_repair_modrinth,
} from '@/helpers/profile'
import { get_game_versions, get_loaders } from '@/helpers/tags'
import { injectInstanceSettings } from '@/providers/instance-settings'
import type { InstanceSettingsTabProps, Manifest } from '../../../helpers/types'
import type { Manifest } from '../../../helpers/types'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const props = defineProps<InstanceSettingsTabProps>()
const emit = defineEmits<{
unlinked: []
}>()
const { instance, offline, isMinecraftServer, onUnlinked } = injectInstanceSettings()
const [
fabric_versions,
@@ -75,9 +73,9 @@ const [
])
const { data: modpackInfo } = useQuery({
queryKey: computed(() => ['linkedModpackInfo', props.instance.path]),
queryFn: () => get_linked_modpack_info(props.instance.path, 'must_revalidate'),
enabled: computed(() => !!props.instance.linked_data?.project_id && !props.offline),
queryKey: computed(() => ['linkedModpackInfo', instance.path]),
queryFn: () => get_linked_modpack_info(instance.path, 'must_revalidate'),
enabled: computed(() => !!instance.linked_data?.project_id && !offline),
})
const repairing = ref(false)
@@ -103,13 +101,13 @@ function getManifest(loader: string) {
provideAppBackup({
async createBackup() {
const allProfiles = await list()
const prefix = `${props.instance.name} - Backup #`
const prefix = `${instance.name} - Backup #`
const existingNums = allProfiles
.filter((p) => p.name.startsWith(prefix))
.map((p) => parseInt(p.name.slice(prefix.length), 10))
.filter((n) => !isNaN(n))
const nextNum = existingNums.length > 0 ? Math.max(...existingNums) + 1 : 1
const newPath = await duplicate(props.instance.path)
const newPath = await duplicate(instance.path)
await edit(newPath, { name: `${prefix}${nextNum}` })
},
})
@@ -120,30 +118,27 @@ provideInstallationSettings({
const rows = [
{
label: formatMessage(commonMessages.platformLabel),
value: formatLoaderLabel(props.instance.loader),
value: formatLoaderLabel(instance.loader),
},
{
label: formatMessage(commonMessages.gameVersionLabel),
value: props.instance.game_version,
value: instance.game_version,
},
]
if (props.instance.loader !== 'vanilla' && props.instance.loader_version) {
if (instance.loader !== 'vanilla' && instance.loader_version) {
rows.push({
label: formatMessage(messages.loaderVersion, {
loader: formatLoaderLabel(props.instance.loader),
loader: formatLoaderLabel(instance.loader),
}),
value: props.instance.loader_version,
value: instance.loader_version,
})
}
return rows
}),
isLinked: computed(() => !!props.instance.linked_data?.locked),
isLinked: computed(() => !!instance.linked_data?.locked),
isBusy: computed(
() =>
props.instance.install_stage !== 'installed' ||
repairing.value ||
reinstalling.value ||
!!props.offline,
instance.install_stage !== 'installed' || repairing.value || reinstalling.value || !!offline,
),
modpack: computed(() => {
if (!modpackInfo.value) return null
@@ -154,9 +149,9 @@ provideInstallationSettings({
versionNumber: modpackInfo.value.version?.version_number,
}
}),
currentPlatform: computed(() => props.instance.loader),
currentGameVersion: computed(() => props.instance.game_version),
currentLoaderVersion: computed(() => props.instance.loader_version ?? ''),
currentPlatform: computed(() => instance.loader),
currentGameVersion: computed(() => instance.game_version),
currentLoaderVersion: computed(() => instance.loader_version ?? ''),
availablePlatforms: loaders?.value?.map((x) => x.name) ?? [],
resolveGameVersions(loader, showSnapshots) {
@@ -199,50 +194,50 @@ provideInstallationSettings({
if (platform !== 'vanilla' && loaderVersionId) {
editProfile.loader_version = loaderVersionId
}
await edit(props.instance.path, editProfile).catch(handleError)
await edit(instance.path, editProfile).catch(handleError)
},
afterSave: async () => {
await install(props.instance.path, false).catch(handleError)
await install(instance.path, false).catch(handleError)
trackEvent('InstanceRepair', {
loader: props.instance.loader,
game_version: props.instance.game_version,
loader: instance.loader,
game_version: instance.game_version,
})
},
async repair() {
repairing.value = true
await install(props.instance.path, true).catch(handleError)
await install(instance.path, true).catch(handleError)
repairing.value = false
trackEvent('InstanceRepair', {
loader: props.instance.loader,
game_version: props.instance.game_version,
loader: instance.loader,
game_version: instance.game_version,
})
},
async reinstallModpack() {
reinstalling.value = true
await update_repair_modrinth(props.instance.path).catch(handleError)
await update_repair_modrinth(instance.path).catch(handleError)
reinstalling.value = false
trackEvent('InstanceRepair', {
loader: props.instance.loader,
game_version: props.instance.game_version,
loader: instance.loader,
game_version: instance.game_version,
})
},
async unlinkModpack() {
await edit(props.instance.path, {
await edit(instance.path, {
linked_data: null as unknown as undefined,
})
await queryClient.invalidateQueries({
queryKey: ['linkedModpackInfo', props.instance.path],
queryKey: ['linkedModpackInfo', instance.path],
})
emit('unlinked')
onUnlinked()
},
getCachedModpackVersions: () => null,
async fetchModpackVersions() {
const versions = await get_project_versions(props.instance.linked_data!.project_id!).catch(
const versions = await get_project_versions(instance.linked_data!.project_id!).catch(
handleError,
)
return (versions ?? []) as Labrinth.Versions.v2.Version[]
@@ -255,25 +250,25 @@ provideInstallationSettings({
},
async onModpackVersionConfirm(version) {
await update_managed_modrinth_version(props.instance.path, version.id)
await update_managed_modrinth_version(instance.path, version.id)
await queryClient.invalidateQueries({
queryKey: ['linkedModpackInfo', props.instance.path],
queryKey: ['linkedModpackInfo', instance.path],
})
},
updaterModalProps: computed(() => ({
isApp: true,
currentVersionId:
modpackInfo.value?.update_version_id ?? props.instance.linked_data?.version_id ?? '',
modpackInfo.value?.update_version_id ?? instance.linked_data?.version_id ?? '',
projectIconUrl: modpackInfo.value?.project?.icon_url,
projectName: modpackInfo.value?.project?.title ?? 'Modpack',
currentGameVersion: props.instance.game_version,
currentLoader: props.instance.loader,
currentGameVersion: instance.game_version,
currentLoader: instance.loader,
})),
isServer: false,
isApp: true,
showModpackVersionActions: !props.isMinecraftServer,
showModpackVersionActions: !isMinecraftServer.value,
repairing,
reinstalling,
})

View File

@@ -14,34 +14,31 @@ import JavaSelector from '@/components/ui/JavaSelector.vue'
import useMemorySlider from '@/composables/useMemorySlider'
import { edit, get_optimal_jre_key } from '@/helpers/profile'
import { get } from '@/helpers/settings.ts'
import { injectInstanceSettings } from '@/providers/instance-settings'
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
import type { AppSettings } from '../../../helpers/types'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const props = defineProps<InstanceSettingsTabProps>()
const { instance } = injectInstanceSettings()
const globalSettings = (await get().catch(handleError)) as unknown as AppSettings
const overrideJavaInstall = ref(!!props.instance.java_path)
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
const overrideJavaInstall = ref(!!instance.java_path)
const optimalJava = readonly(await get_optimal_jre_key(instance.path).catch(handleError))
const javaInstall = ref({ path: optimalJava.path ?? instance.java_path })
const overrideJavaArgs = ref((props.instance.extra_launch_args?.length ?? 0) > 0)
const javaArgs = ref(
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
)
const overrideJavaArgs = ref((instance.extra_launch_args?.length ?? 0) > 0)
const javaArgs = ref((instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '))
const overrideEnvVars = ref((props.instance.custom_env_vars?.length ?? 0) > 0)
const overrideEnvVars = ref((instance.custom_env_vars?.length ?? 0) > 0)
const envVars = ref(
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
.map((x) => x.join('='))
.join(' '),
(instance.custom_env_vars ?? globalSettings.custom_env_vars).map((x) => x.join('=')).join(' '),
)
const overrideMemorySettings = ref(!!props.instance.memory)
const memory = ref(props.instance.memory ?? globalSettings.memory)
const overrideMemorySettings = ref(!!instance.memory)
const memory = ref(instance.memory ?? globalSettings.memory)
const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) as unknown as {
maxMemory: number
snapPoints: number[]
@@ -79,7 +76,7 @@ watch(
memory,
],
async () => {
await edit(props.instance.path, editProfileObject.value)
await edit(instance.path, editProfileObject.value)
},
{ deep: true },
)

View File

@@ -11,24 +11,23 @@ import { computed, type Ref, ref, watch } from 'vue'
import { edit } from '@/helpers/profile'
import { get } from '@/helpers/settings.ts'
import { injectInstanceSettings } from '@/providers/instance-settings'
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
import type { AppSettings } from '../../../helpers/types'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const props = defineProps<InstanceSettingsTabProps>()
const { instance } = injectInstanceSettings()
const globalSettings = (await get().catch(handleError)) as AppSettings
const overrideWindowSettings = ref(
!!props.instance.game_resolution || !!props.instance.force_fullscreen,
)
const overrideWindowSettings = ref(!!instance.game_resolution || !!instance.force_fullscreen)
const resolution: Ref<[number, number]> = ref(
props.instance.game_resolution ?? (globalSettings.game_resolution.slice() as [number, number]),
instance.game_resolution ?? (globalSettings.game_resolution.slice() as [number, number]),
)
const fullscreenSetting: Ref<boolean> = ref(
props.instance.force_fullscreen ?? globalSettings.force_fullscreen,
instance.force_fullscreen ?? globalSettings.force_fullscreen,
)
const editProfileObject = computed(() => {
@@ -47,7 +46,7 @@ const editProfileObject = computed(() => {
watch(
[overrideWindowSettings, resolution, fullscreenSetting],
async () => {
await edit(props.instance.path, editProfileObject.value)
await edit(instance.path, editProfileObject.value)
},
{ deep: true },
)

View File

@@ -20,9 +20,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, ref, watch } from 'vue'
import { ref, watch } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue'
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
@@ -106,15 +105,13 @@ const tabs = [
},
]
const modal = ref()
const modal = ref<InstanceType<typeof TabbedModal> | null>(null)
function show() {
modal.value.show()
modal.value?.show()
}
const isOpen = computed(() => modal.value?.isOpen)
defineExpose({ show, isOpen })
defineExpose({ show })
const { progress, version: downloadingVersion } = injectAppUpdateDownloadProgress()
@@ -138,8 +135,8 @@ function devModeCount() {
settings.value.developer_mode = !!themeStore.devMode
devModeCounter.value = 0
if (!themeStore.devMode && tabs[modal.value.selectedTab].developerOnly) {
modal.value.setTab(0)
if (!themeStore.devMode && tabs[modal.value!.selectedTab].developerOnly) {
modal.value!.setTab(0)
}
}
}
@@ -152,49 +149,46 @@ const messages = defineMessages({
})
</script>
<template>
<ModalWrapper ref="modal">
<TabbedModal ref="modal" :tabs="tabs.filter((t) => !t.developerOnly || themeStore.devMode)">
<template #title>
<span class="flex items-center gap-2 text-lg font-extrabold text-contrast">
<SettingsIcon /> Settings
</span>
</template>
<TabbedModal :tabs="tabs.filter((t) => !t.developerOnly || themeStore.devMode)">
<template #footer>
<div class="mt-auto text-secondary text-sm">
<div class="mb-3">
<template v-if="progress > 0 && progress < 1">
<p class="m-0 mb-2">
{{ formatMessage(messages.downloading, { version: downloadingVersion }) }}
</p>
<ProgressBar :progress="progress" />
</template>
</div>
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
{{ formatMessage(developerModeEnabled) }}
</p>
<div class="flex items-center gap-3">
<button
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
:class="{
'text-brand': themeStore.devMode,
'text-secondary': !themeStore.devMode,
}"
@click="devModeCount"
>
<ModrinthIcon class="w-6 h-6" />
</button>
<div>
<p class="m-0">Modrinth App {{ version }}</p>
<p class="m-0">
<span v-if="osPlatform === 'macos'">macOS</span>
<span v-else class="capitalize">{{ osPlatform }}</span>
{{ osVersion }}
</p>
</div>
<template #footer>
<div class="mt-auto text-secondary text-sm">
<div class="mb-3">
<template v-if="progress > 0 && progress < 1">
<p class="m-0 mb-2">
{{ formatMessage(messages.downloading, { version: downloadingVersion }) }}
</p>
<ProgressBar :progress="progress" />
</template>
</div>
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
{{ formatMessage(developerModeEnabled) }}
</p>
<div class="flex items-center gap-3">
<button
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
:class="{
'text-brand': themeStore.devMode,
'text-secondary': !themeStore.devMode,
}"
@click="devModeCount"
>
<ModrinthIcon class="w-6 h-6" />
</button>
<div>
<p class="m-0">Modrinth App {{ version }}</p>
<p class="m-0">
<span v-if="osPlatform === 'macos'">macOS</span>
<span v-else class="capitalize">{{ osPlatform }}</span>
{{ osVersion }}
</p>
</div>
</div>
</template>
</TabbedModal>
</ModalWrapper>
</div>
</template>
</TabbedModal>
</template>

View File

@@ -12,14 +12,13 @@ import {
Avatar,
commonMessages,
defineMessage,
NewModal,
TabbedModal,
type TabbedModalTab,
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
import { computed, nextTick, ref, watch } from 'vue'
import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.vue'
import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue'
@@ -28,12 +27,16 @@ import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue'
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
import { get_project_v3 } from '@/helpers/cache'
import { get_linked_modpack_info } from '@/helpers/profile'
import { provideInstanceSettings } from '@/providers/instance-settings'
import type { InstanceSettingsTabProps } from '../../../helpers/types'
import type { GameInstance } from '../../../helpers/types'
const { formatMessage } = useVIntl()
const props = defineProps<InstanceSettingsTabProps>()
const props = defineProps<{
instance: GameInstance
offline?: boolean
}>()
const emit = defineEmits<{
unlinked: []
}>()
@@ -41,6 +44,13 @@ const emit = defineEmits<{
const isMinecraftServer = ref(false)
const handleUnlinked = () => emit('unlinked')
provideInstanceSettings({
instance: props.instance,
offline: props.offline,
isMinecraftServer,
onUnlinked: handleUnlinked,
})
watch(
() => props.instance,
(instance) => {
@@ -58,7 +68,7 @@ watch(
{ immediate: true },
)
const tabs = computed<TabbedModalTab<InstanceSettingsTabProps>[]>(() => [
const tabs = computed<TabbedModalTab[]>(() => [
{
name: defineMessage({
id: 'instance.settings.tabs.general',
@@ -102,8 +112,7 @@ const tabs = computed<TabbedModalTab<InstanceSettingsTabProps>[]>(() => [
])
const queryClient = useQueryClient()
const modal = ref()
const tabbedModal = useTemplateRef('tabbedModal')
const tabbedModal = ref<InstanceType<typeof TabbedModal> | null>(null)
function show(tabIndex?: number) {
if (props.instance.linked_data?.project_id) {
@@ -112,7 +121,7 @@ function show(tabIndex?: number) {
queryFn: () => get_linked_modpack_info(props.instance.path, 'stale_while_revalidate'),
})
}
modal.value.show()
tabbedModal.value?.show()
if (tabIndex !== undefined) {
nextTick(() => tabbedModal.value?.setTab(tabIndex))
}
@@ -121,8 +130,9 @@ function show(tabIndex?: number) {
defineExpose({ show })
</script>
<template>
<NewModal
ref="modal"
<TabbedModal
ref="tabbedModal"
:tabs="tabs"
:max-width="'min(928px, calc(95vw - 10rem))'"
:width="'min(928px, calc(95vw - 10rem))'"
>
@@ -139,19 +149,5 @@ defineExpose({ show })
}}</span>
</span>
</template>
<TabbedModal
ref="tabbedModal"
:tabs="
tabs.map((tab) => ({
...tab,
props: {
...props,
isMinecraftServer,
onUnlinked: handleUnlinked,
},
}))
"
/>
</NewModal>
</TabbedModal>
</template>

View File

@@ -128,9 +128,3 @@ type AppSettings = {
prev_custom_dir?: string
migrated: boolean
}
export type InstanceSettingsTabProps = {
instance: GameInstance
offline?: boolean
isMinecraftServer?: boolean
}

View File

@@ -0,0 +1,14 @@
import { createContext } from '@modrinth/ui'
import type { Ref } from 'vue'
import type { GameInstance } from '@/helpers/types'
export interface InstanceSettingsContext {
instance: GameInstance
offline?: boolean
isMinecraftServer: Ref<boolean>
onUnlinked: () => void
}
export const [injectInstanceSettings, provideInstanceSettings] =
createContext<InstanceSettingsContext>('InstanceSettingsModal', 'instanceSettings')