feat: implement incompatible options due to base project

This commit is contained in:
tdgao
2026-07-02 14:28:30 -07:00
parent 58d195da77
commit 6353fbb370
3 changed files with 131 additions and 30 deletions
@@ -17,20 +17,12 @@
>
<template #option="{ item, isSelected }">
<div
v-tooltip="
!possibleGameVersions.includes(item.value)
? formatMessage(messages.gameVersionUnsupportedTooltip, {
title: project.title,
gameVersion: item.value,
platform: currentPlatformText,
})
: null
"
v-tooltip="gameVersionOptionTooltip(item.value)"
class="flex w-full items-center justify-between gap-2"
:class="{
'text-brand-red opacity-40': !possibleGameVersions.includes(item.value),
'text-brand-red opacity-40': isGameVersionUnavailable(item.value),
'text-green': isSelected,
'text-primary': possibleGameVersions.includes(item.value) && !isSelected,
'text-primary': !isGameVersionUnavailable(item.value) && !isSelected,
}"
>
<span class="min-w-0 truncate font-semibold leading-tight">{{ item.label }}</span>
@@ -61,20 +53,12 @@
>
<template #option="{ item, isSelected }">
<div
v-tooltip="
!possiblePlatforms.includes(item.value)
? formatMessage(messages.platformUnsupportedTooltip, {
title: project.title,
platform: item.label,
gameVersion: currentGameVersion,
})
: null
"
v-tooltip="platformOptionTooltip(item.value, item.label)"
class="flex w-full items-center justify-between gap-2"
:class="{
'text-brand-red opacity-40': !possiblePlatforms.includes(item.value),
'text-brand-red opacity-40': isPlatformUnavailable(item.value),
'text-green': isSelected,
'text-primary': possiblePlatforms.includes(item.value) && !isSelected,
'text-primary': !isPlatformUnavailable(item.value) && !isSelected,
}"
>
<span class="min-w-0 truncate font-semibold leading-tight">{{ item.label }}</span>
@@ -166,6 +150,8 @@ const props = withDefaults(
downloadReason?: CdnDownloadReason
initialGameVersion?: string | null
initialPlatform?: string | null
incompatibleGameVersions?: string[]
incompatibleLoaders?: string[]
resetKey?: number
}>(),
{
@@ -173,6 +159,8 @@ const props = withDefaults(
downloadReason: 'standalone',
initialGameVersion: null,
initialPlatform: null,
incompatibleGameVersions: () => [],
incompatibleLoaders: () => [],
resetKey: 0,
},
)
@@ -193,6 +181,9 @@ const userSelectedPlatform = ref<string | null>(props.initialPlatform)
const showAllVersions = ref(defaultShowAllVersions())
const versionFilter = ref('')
const incompatibleGameVersionsSet = computed(() => new Set(props.incompatibleGameVersions))
const incompatibleLoadersSet = computed(() => new Set(props.incompatibleLoaders))
const showAllVersionsModel = computed({
get() {
return showAllVersions.value
@@ -419,7 +410,58 @@ function defaultShowAllVersions() {
)
}
function isGameVersionUnavailable(gameVersion: string) {
return (
incompatibleGameVersionsSet.value.has(gameVersion) ||
!possibleGameVersions.value.includes(gameVersion)
)
}
function isPlatformUnavailable(platform: string) {
return incompatibleLoadersSet.value.has(platform) || !possiblePlatforms.value.includes(platform)
}
function gameVersionOptionTooltip(gameVersion: string) {
if (incompatibleGameVersionsSet.value.has(gameVersion)) {
return formatMessage(messages.baseGameVersionIncompatibleTooltip)
}
if (!possibleGameVersions.value.includes(gameVersion)) {
return formatMessage(messages.gameVersionUnsupportedTooltip, {
title: props.project.title,
gameVersion,
platform: currentPlatformText.value,
})
}
return null
}
function platformOptionTooltip(platform: string, platformLabel: string) {
if (incompatibleLoadersSet.value.has(platform)) {
return formatMessage(messages.baseLoaderIncompatibleTooltip)
}
if (!possiblePlatforms.value.includes(platform)) {
return formatMessage(messages.platformUnsupportedTooltip, {
title: props.project.title,
platform: platformLabel,
gameVersion: currentGameVersion.value,
})
}
return null
}
const messages = defineMessages({
baseGameVersionIncompatibleTooltip: {
id: 'project.download.base-game-version-incompatible-tooltip',
defaultMessage: 'This game version is incompatible with the base project.',
},
baseLoaderIncompatibleTooltip: {
id: 'project.download.base-loader-incompatible-tooltip',
defaultMessage: 'This loader is incompatible with the base project.',
},
gameVersionUnsupportedTooltip: {
id: 'project.download.game-version-unsupported-tooltip',
defaultMessage: '{title} does not support {gameVersion} for {platform}',
@@ -15,6 +15,8 @@
:download-reason="downloadReason"
:initial-game-version="initialGameVersion"
:initial-platform="initialPlatform"
:incompatible-game-versions="showOptions.incompatibleGameVersions"
:incompatible-loaders="showOptions.incompatibleLoaders"
:reset-key="downloadProjectResetKey"
@select-game-version="selectGameVersion"
@select-platform="selectPlatform"
@@ -89,7 +91,7 @@ import {
} from '@modrinth/ui'
import type { DisplayProjectType } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import { computed, nextTick, ref, watch } from 'vue'
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
import { navigateTo } from '#app'
import { saveFeatureFlags } from '~/composables/featureFlags.ts'
@@ -116,6 +118,18 @@ type NewModalRef = {
hide: () => void
}
type ProjectDownloadModalShowOptions = {
projectId?: string
incompatibleGameVersions?: string[]
incompatibleLoaders?: string[]
}
type ResolvedProjectDownloadModalShowOptions = {
projectId?: string
incompatibleGameVersions: string[]
incompatibleLoaders: string[]
}
const props = withDefaults(
defineProps<{
projectId?: string
@@ -143,8 +157,11 @@ const debug = useDebugLogger('DownloadModal')
const modal = ref<NewModalRef | null>(null)
const modalOpen = ref(false)
const showProjectId = ref<string | null>(null)
const showOptions = ref<ResolvedProjectDownloadModalShowOptions>(getDefaultShowOptions())
const downloadProjectResetKey = ref(0)
const projectDownloadSelection = ref<ProjectDownloadSelection>(getDefaultProjectDownloadSelection())
const MODAL_CLOSE_STATE_RESET_MS = 350
let closeStateResetTimeout: ReturnType<typeof setTimeout> | null = null
const routeProjectId = computed(() => showProjectId.value ?? props.projectId ?? null)
@@ -320,6 +337,7 @@ function selectPlatform(platform: string) {
}
function onShow() {
clearCloseStateResetTimeout()
modalOpen.value = true
debug('on-show fired')
versionsEnabled.value = true
@@ -331,17 +349,30 @@ function onShow() {
function onHide() {
const hadShowProjectId = !!showProjectId.value
modalOpen.value = false
showProjectId.value = null
clearCloseStateResetTimeout()
closeStateResetTimeout = setTimeout(() => {
showProjectId.value = null
showOptions.value = getDefaultShowOptions()
closeStateResetTimeout = null
}, MODAL_CLOSE_STATE_RESET_MS)
if (props.useRouteHash && !hadShowProjectId) {
navigateTo({ query: route.query, hash: '' }, { replace: true })
}
}
async function show(event?: MouseEvent, projectId?: string): Promise<void> {
async function show(
event?: MouseEvent,
options: ProjectDownloadModalShowOptions = {},
): Promise<void> {
if (!modal.value || modalOpen.value) return
showProjectId.value = projectId ?? null
clearCloseStateResetTimeout()
showOptions.value = {
...getDefaultShowOptions(),
...options,
}
showProjectId.value = showOptions.value.projectId ?? null
await nextTick()
if (!(await loadProjectForModal(!!projectId))) return
if (!(await loadProjectForModal(!!showOptions.value.projectId))) return
modalOpen.value = true
modal.value.show(event)
}
@@ -366,6 +397,20 @@ function getDefaultProjectDownloadSelection(): ProjectDownloadSelection {
}
}
function getDefaultShowOptions(): ResolvedProjectDownloadModalShowOptions {
return {
projectId: undefined,
incompatibleGameVersions: [],
incompatibleLoaders: [],
}
}
function clearCloseStateResetTimeout() {
if (!closeStateResetTimeout) return
clearTimeout(closeStateResetTimeout)
closeStateResetTimeout = null
}
async function loadProjectForModal(forceRefetch: boolean) {
if (!routeProjectId.value) return false
if (!forceRefetch && projectRaw.value) return true
@@ -411,5 +456,7 @@ watch(routeProjectId, () => {
downloadProjectResetKey.value += 1
})
onUnmounted(clearCloseStateResetTimeout)
defineExpose({ show, hide })
</script>
@@ -374,7 +374,7 @@
v-else-if="dependency.project"
v-tooltip="formatMessage(messages.downloadProject)"
:aria-label="formatMessage(messages.downloadProject)"
@click="openDependencyDownloadModal(dependency.project.id, $event)"
@click="openDependencyDownloadModal(dependency.project, $event)"
>
<DownloadIcon />
</button>
@@ -804,8 +804,20 @@ function handleOpenEditVersionModal(versionId: string, projectId: string, stageI
editModal.value?.openEditVersionModal(versionId, projectId, stageId)
}
function openDependencyDownloadModal(projectId: string, event: MouseEvent) {
dependencyDownloadModal.value?.show(event, projectId)
function openDependencyDownloadModal(
dependencyProject: Labrinth.Projects.v2.Project,
event: MouseEvent,
) {
const baseGameVersions = new Set(version.value?.game_versions ?? [])
const baseLoaders = new Set(dependencyResolutionLoaders.value)
dependencyDownloadModal.value?.show(event, {
projectId: dependencyProject.id,
incompatibleGameVersions: dependencyProject.game_versions.filter(
(gameVersion) => !baseGameVersions.has(gameVersion),
),
incompatibleLoaders: dependencyProject.loaders.filter((loader) => !baseLoaders.has(loader)),
})
}
const deleteVersionMutation = useMutation({