refactor: componentize project download modal

This commit is contained in:
tdgao
2026-06-26 09:58:05 -06:00
parent 5fe07de187
commit 37828e7ef5
2 changed files with 677 additions and 616 deletions
@@ -0,0 +1,663 @@
<template>
<NewModal ref="modal" :on-show="onShow" :on-hide="onHide">
<template #title>
<Avatar :src="project.icon_url" :alt="project.title" class="icon" size="32px" />
<div class="truncate text-lg font-extrabold text-contrast">
{{ formatMessage(messages.downloadTitle, { title: project.title }) }}
</div>
</template>
<template #default>
<div class="mx-auto flex max-w-[44rem] flex-col gap-4 md:w-[30rem]">
<div
v-if="
project.project_type !== 'plugin' ||
project.loaders.some((x) => !tags.loaderData.allPluginLoaders.includes(x))
"
class="modrinth-app-section contents"
>
<div class="mx-auto flex w-fit flex-col">
<ButtonStyled color="brand">
<a class="w-fit" :href="`modrinth://mod/${project.slug}`" @click="installWithApp">
<ModrinthIcon aria-hidden="true" />
{{ formatMessage(messages.installWithModrinthApp) }}
<ExternalIcon aria-hidden="true" />
</a>
</ButtonStyled>
<Accordion ref="getModrinthAppAccordion">
<nuxt-link class="mt-2 flex justify-center text-brand-blue hover:underline" to="/app">
{{ formatMessage(messages.dontHaveModrinthApp) }}
</nuxt-link>
</Accordion>
</div>
<div class="flex items-center gap-4 px-4">
<div class="flex h-[2px] w-full rounded-2xl bg-button-bg"></div>
<span class="flex-shrink-0 text-sm font-semibold text-secondary">
{{ formatMessage(commonMessages.orLabel) }}
</span>
<div class="flex h-[2px] w-full rounded-2xl bg-button-bg"></div>
</div>
</div>
<div class="mx-auto flex w-fit flex-col gap-2">
<ButtonStyled v-if="project.game_versions.length === 1">
<div class="disabled button-like">
<GameIcon aria-hidden="true" />
{{
currentGameVersion
? formatMessage(messages.gameVersionLabel, { version: currentGameVersion })
: formatMessage(messages.gameVersionError)
}}
<InfoIcon
v-tooltip="
formatMessage(messages.gameVersionTooltip, {
title: project.title,
version: currentGameVersion,
})
"
class="ml-auto size-5"
/>
</div>
</ButtonStyled>
<Accordion
v-else
ref="gameVersionAccordion"
class="accordion-with-bg"
@on-open="
() => {
platformAccordion?.close()
}
"
>
<template #title>
<GameIcon aria-hidden="true" />
{{
currentGameVersion
? formatMessage(messages.gameVersionLabel, { version: currentGameVersion })
: formatMessage(messages.selectGameVersion)
}}
</template>
<label for="game-versions-filtering" hidden>{{
formatMessage(messages.searchGameVersionsLabel)
}}</label>
<StyledInput
id="game-versions-filtering"
ref="gameVersionFilterInput"
v-model="versionFilter"
type="search"
autocomplete="off"
:icon="SearchIcon"
:placeholder="formatMessage(messages.searchGameVersions)"
wrapper-class="mb-2 w-full"
/>
<ScrollablePanel :class="project.game_versions.length > 4 ? 'h-[15rem]' : ''">
<ButtonStyled
v-for="gameVersion in filteredGameVersions"
:key="gameVersion"
:color="currentGameVersion === gameVersion ? 'brand' : 'standard'"
>
<button
v-tooltip="
!possibleGameVersions.includes(gameVersion)
? formatMessage(messages.gameVersionUnsupportedTooltip, {
title: project.title,
gameVersion: gameVersion,
platform: currentPlatformText,
})
: null
"
:class="{
'looks-disabled !text-brand-red':
!possibleGameVersions.includes(gameVersion),
}"
@click="selectGameVersion(gameVersion)"
>
{{ gameVersion }}
<CheckIcon v-if="userSelectedGameVersion === gameVersion" />
</button>
</ButtonStyled>
</ScrollablePanel>
<Checkbox
v-if="showVersionsCheckbox"
v-model="showAllVersions"
class="mx-1"
:label="formatMessage(messages.showAllVersions)"
:disabled="!!versionFilter"
/>
</Accordion>
<ButtonStyled v-if="project.loaders.length === 1 && project.project_type !== 'resourcepack'">
<div class="disabled button-like">
<WrenchIcon aria-hidden="true" />
{{
currentPlatform
? formatMessage(messages.platformLabel, {
platform: currentPlatformText,
})
: formatMessage(messages.platformError)
}}
<InfoIcon
v-tooltip="
formatMessage(messages.platformTooltip, {
title: project.title,
platform: currentPlatformText,
})
"
class="ml-auto size-5"
/>
</div>
</ButtonStyled>
<Accordion
v-else-if="project.project_type !== 'resourcepack'"
ref="platformAccordion"
class="accordion-with-bg"
@on-open="
() => {
gameVersionAccordion?.close()
}
"
>
<template #title>
<WrenchIcon aria-hidden="true" />
{{
currentPlatform
? formatMessage(messages.platformLabel, {
platform: currentPlatformText,
})
: formatMessage(messages.selectPlatform)
}}
</template>
<ScrollablePanel :class="project.loaders.length > 4 ? 'h-[15rem]' : ''">
<ButtonStyled
v-for="platform in project.loaders.slice().reverse()"
:key="platform"
:color="currentPlatform === platform ? 'brand' : 'standard'"
>
<button
v-tooltip="
!possiblePlatforms.includes(platform)
? formatMessage(messages.platformUnsupportedTooltip, {
title: project.title,
platform: currentPlatformText,
gameVersion: currentGameVersion,
})
: null
"
:class="{
'looks-disabled !text-brand-red': !possiblePlatforms.includes(platform),
}"
@click="selectPlatform(platform)"
>
{{ formatMessage(getTagMessage(platform, 'loader')) }}
<CheckIcon v-if="userSelectedPlatform === platform" />
</button>
</ButtonStyled>
</ScrollablePanel>
</Accordion>
</div>
<AutomaticAccordion div class="flex flex-col gap-2">
<VersionSummary
v-if="filteredRelease"
:version="filteredRelease"
:decorate-download-url="decorateModalDownloadUrl"
@on-download="onDownload"
@on-navigate="onVersionNavigate"
/>
<VersionSummary
v-if="filteredBeta"
:version="filteredBeta"
:decorate-download-url="decorateModalDownloadUrl"
@on-download="onDownload"
@on-navigate="onVersionNavigate"
/>
<VersionSummary
v-if="filteredAlpha"
:version="filteredAlpha"
:decorate-download-url="decorateModalDownloadUrl"
@on-download="onDownload"
@on-navigate="onVersionNavigate"
/>
<p
v-if="
currentPlatform &&
currentGameVersion &&
!filteredRelease &&
!filteredBeta &&
!filteredAlpha &&
!versionsLoading &&
versions.length > 0
"
>
{{
formatMessage(messages.noVersionsAvailable, {
gameVersion: currentGameVersion,
platform: currentPlatformText,
})
}}
</p>
</AutomaticAccordion>
<ServersPromo
v-if="flags.showProjectPageDownloadModalServersPromo"
:link="`/hosting#plan`"
@close="
() => {
flags.showProjectPageDownloadModalServersPromo = false
saveFeatureFlags()
}
"
/>
</div>
</template>
</NewModal>
</template>
<script setup>
import {
CheckIcon,
ExternalIcon,
GameIcon,
InfoIcon,
ModrinthIcon,
SearchIcon,
WrenchIcon,
} from '@modrinth/assets'
import {
Avatar,
ButtonStyled,
Checkbox,
commonMessages,
defineMessages,
getTagMessage,
NewModal,
ScrollablePanel,
ServersPromo,
StyledInput,
useDebugLogger,
useVIntl,
} from '@modrinth/ui'
import VersionSummary from '@modrinth/ui/src/components/version/VersionSummary.vue'
import dayjs from 'dayjs'
import { computed, nextTick, ref, watch } from 'vue'
import { navigateTo } from '#app'
import Accordion from '~/components/ui/Accordion.vue'
import AutomaticAccordion from '~/components/ui/AutomaticAccordion.vue'
import { saveFeatureFlags } from '~/composables/featureFlags.ts'
const props = defineProps({
project: {
type: Object,
required: true,
},
versions: {
type: Array,
default: () => [],
},
versionsLoading: {
type: Boolean,
default: false,
},
tags: {
type: Object,
required: true,
},
downloadReason: {
type: String,
default: 'standalone',
},
loadVersions: {
type: Function,
required: true,
},
})
const emit = defineEmits(['download'])
const route = useRoute()
const flags = useFeatureFlags()
const { createProjectDownloadUrl } = useCdnDownloadContext()
const { formatMessage } = useVIntl()
const debug = useDebugLogger('DownloadModal')
const modal = ref()
const userSelectedGameVersion = ref(null)
const userSelectedPlatform = ref(null)
const showAllVersions = ref(false)
const versionFilter = ref('')
const gameVersionFilterInput = ref()
const gameVersionAccordion = ref()
const platformAccordion = ref()
const getModrinthAppAccordion = ref()
const currentGameVersion = computed(() => {
return (
userSelectedGameVersion.value ||
(props.project.game_versions.length === 1 && props.project.game_versions[0])
)
})
const possibleGameVersions = computed(() => {
return props.versions
.filter((x) => !currentPlatform.value || x.loaders.includes(currentPlatform.value))
.flatMap((x) => x.game_versions)
})
const possiblePlatforms = computed(() => {
return props.versions
.filter((x) => !currentGameVersion.value || x.game_versions.includes(currentGameVersion.value))
.flatMap((x) => x.loaders)
})
const currentPlatform = computed(() => {
return (
userSelectedPlatform.value ||
(props.project.loaders.length === 1 && props.project.loaders[0])
)
})
const currentPlatformText = computed(() => {
if (!currentPlatform.value) return null
return formatMessage(getTagMessage(currentPlatform.value, 'loader'))
})
const releaseVersions = computed(() => {
const set = new Set()
for (const gameVersion of props.tags.gameVersions || []) {
if (gameVersion?.version && gameVersion.version_type === 'release') {
set.add(gameVersion.version)
}
}
return set
})
const nonReleaseVersions = computed(() => {
const set = new Set()
for (const gameVersion of props.tags.gameVersions || []) {
if (gameVersion?.version && gameVersion.version_type !== 'release') {
set.add(gameVersion.version)
}
}
return set
})
const showVersionsCheckbox = computed(() => {
let hasRelease = false
let hasNonRelease = false
for (const version of props.project.game_versions) {
if (isReleaseGameVersion(version)) {
hasRelease = true
} else {
hasNonRelease = true
}
if (hasRelease && hasNonRelease) return true
}
return false
})
const filteredGameVersions = computed(() => {
return props.project.game_versions
.filter(
(x) =>
(versionFilter.value && x.includes(versionFilter.value)) ||
(!versionFilter.value && (showAllVersions.value || isReleaseGameVersion(x))),
)
.slice()
.reverse()
})
const filteredVersions = computed(() => {
const result = props.versions.filter(
(x) =>
x.game_versions?.includes(currentGameVersion.value) &&
(x.loaders?.includes(currentPlatform.value) || props.project.project_type === 'resourcepack'),
)
debug('filteredVersions', {
total: props.versions.length,
filtered: result.length,
currentGameVersion: currentGameVersion.value,
currentPlatform: currentPlatform.value,
sampleLoaders: props.versions.slice(0, 3).map((v) => v.loaders),
})
return result
})
const filteredRelease = computed(() => {
return filteredVersions.value.find((x) => x.version_type === 'release')
})
const filteredBeta = computed(() => {
return filteredVersions.value.find(
(x) =>
x.version_type === 'beta' &&
(!filteredRelease.value ||
dayjs(x.date_published).isAfter(dayjs(filteredRelease.value.date_published))),
)
})
const filteredAlpha = computed(() => {
return filteredVersions.value.find(
(x) =>
x.version_type === 'alpha' &&
(!filteredRelease.value ||
dayjs(x.date_published).isAfter(dayjs(filteredRelease.value.date_published))) &&
(!filteredBeta.value ||
dayjs(x.date_published).isAfter(dayjs(filteredBeta.value.date_published))),
)
})
const messages = defineMessages({
dontHaveModrinthApp: {
id: 'project.download.no-app',
defaultMessage: "Don't have Modrinth App?",
},
downloadTitle: {
id: 'project.download.title',
defaultMessage: 'Download {title}',
},
gameVersionError: {
id: 'project.download.game-version-error',
defaultMessage: 'Error: no game versions found',
},
gameVersionLabel: {
id: 'project.download.game-version',
defaultMessage: 'Game version: {version}',
},
gameVersionTooltip: {
id: 'project.download.game-version-tooltip',
defaultMessage: '{title} is only available for {version}',
},
gameVersionUnsupportedTooltip: {
id: 'project.download.game-version-unsupported-tooltip',
defaultMessage: '{title} does not support {gameVersion} for {platform}',
},
installWithModrinthApp: {
id: 'project.download.install-with-app',
defaultMessage: 'Install with Modrinth App',
},
noVersionsAvailable: {
id: 'project.download.no-versions-available',
defaultMessage: 'No versions available for {gameVersion} and {platform}.',
},
platformError: {
id: 'project.download.platform-error',
defaultMessage: 'Error: no platforms found',
},
platformLabel: {
id: 'project.download.platform',
defaultMessage: 'Platform: {platform}',
},
platformTooltip: {
id: 'project.download.platform-tooltip',
defaultMessage: '{title} is only available for {platform}',
},
platformUnsupportedTooltip: {
id: 'project.download.platform-unsupported-tooltip',
defaultMessage: '{title} does not support {platform} for {gameVersion}',
},
searchGameVersions: {
id: 'project.download.search-game-versions',
defaultMessage: 'Search game versions...',
},
searchGameVersionsLabel: {
id: 'project.download.search-game-versions-label',
defaultMessage: 'Search game versions...',
},
selectGameVersion: {
id: 'project.download.select-game-version',
defaultMessage: 'Select game version',
},
selectPlatform: {
id: 'project.download.select-platform',
defaultMessage: 'Select platform',
},
showAllVersions: {
id: 'project.download.show-all-versions',
defaultMessage: 'Show all versions',
},
})
function decorateModalDownloadUrl(url) {
return createProjectDownloadUrl(url, {
reason: props.downloadReason,
gameVersion: currentGameVersion.value ?? undefined,
loader: currentPlatform.value ?? undefined,
})
}
function isReleaseGameVersion(version) {
if (releaseVersions.value.has(version)) return true
if (nonReleaseVersions.value.has(version)) return false
return true
}
function updateDownloadQuery() {
navigateTo(
{
query: {
...route.query,
...(userSelectedGameVersion.value && {
version: userSelectedGameVersion.value,
}),
...(userSelectedPlatform.value && {
loader: userSelectedPlatform.value,
}),
},
hash: route.hash,
},
{ replace: true },
)
}
function selectGameVersion(gameVersion) {
userSelectedGameVersion.value = gameVersion
gameVersionAccordion.value?.close()
if (!currentPlatform.value) {
platformAccordion.value?.open()
}
updateDownloadQuery()
}
function selectPlatform(platform) {
userSelectedPlatform.value = platform
platformAccordion.value?.close()
if (!currentGameVersion.value) {
gameVersionAccordion.value?.open()
}
updateDownloadQuery()
}
function installWithApp() {
setTimeout(() => {
getModrinthAppAccordion.value?.open()
}, 1500)
}
function onShow() {
debug('on-show fired')
props.loadVersions()
navigateTo({ query: route.query, hash: '#download' }, { replace: true })
}
function onHide() {
navigateTo({ query: route.query, hash: '' }, { replace: true })
}
function show(event) {
modal.value?.show(event)
}
function hide(event) {
modal.value?.hide(event)
userSelectedPlatform.value = null
userSelectedGameVersion.value = null
showAllVersions.value = false
}
function onDownload(event) {
emit('download')
setTimeout(() => {
hide(event)
}, 400)
}
function onVersionNavigate(url) {
hide()
nextTick(() => {
navigateTo(url)
})
}
function openFromHash() {
if (!modal.value || route.hash !== '#download') return
debug('hash #download watch fired, opening modal')
props.loadVersions()
modal.value.show()
}
const { version, loader } = route.query
if (
props.project.game_versions.length > 0 &&
props.project.game_versions.every((projectVersion) => !isReleaseGameVersion(projectVersion))
) {
showAllVersions.value = true
}
if (version !== undefined && props.project.game_versions.includes(version)) {
userSelectedGameVersion.value = version
}
if (loader !== undefined && props.project.loaders.includes(loader)) {
userSelectedPlatform.value = loader
}
if (route.hash === '#download' || version !== undefined || loader !== undefined) {
debug('eager loadVersions from setup', { hash: route.hash, version, loader })
props.loadVersions()
}
watch(modal, openFromHash)
watch(() => route.hash, openFromHash)
defineExpose({ show, hide })
</script>
<style lang="scss" scoped>
:deep(.accordion-with-bg) {
@apply rounded-2xl bg-bg p-2;
--scrollable-pane-bg: var(--color-bg);
}
@media (hover: none) and (max-width: 767px) {
.modrinth-app-section {
display: none;
}
}
</style>
+14 -616
View File
@@ -88,338 +88,16 @@
</div>
</div>
</div>
<NewModal
<ProjectDownloadModal
ref="downloadModal"
:on-show="
() => {
debug('on-show fired')
loadVersions()
navigateTo({ query: route.query, hash: '#download' }, { replace: true })
}
"
:on-hide="
() => {
navigateTo({ query: route.query, hash: '' }, { replace: true })
}
"
>
<template #title>
<Avatar :src="project.icon_url" :alt="project.title" class="icon" size="32px" />
<div class="truncate text-lg font-extrabold text-contrast">
{{ formatMessage(messages.downloadTitle, { title: project.title }) }}
</div>
</template>
<template #default>
<div class="mx-auto flex max-w-[44rem] flex-col gap-4 md:w-[30rem]">
<div
v-if="
project.project_type !== 'plugin' ||
project.loaders.some((x) => !tags.loaderData.allPluginLoaders.includes(x))
"
class="modrinth-app-section contents"
>
<div class="mx-auto flex w-fit flex-col">
<ButtonStyled color="brand">
<a
class="w-fit"
:href="`modrinth://mod/${project.slug}`"
@click="() => installWithApp()"
>
<ModrinthIcon aria-hidden="true" />
{{ formatMessage(messages.installWithModrinthApp) }}
<ExternalIcon aria-hidden="true" />
</a>
</ButtonStyled>
<Accordion ref="getModrinthAppAccordion">
<nuxt-link
class="mt-2 flex justify-center text-brand-blue hover:underline"
to="/app"
>
{{ formatMessage(messages.dontHaveModrinthApp) }}
</nuxt-link>
</Accordion>
</div>
<div class="flex items-center gap-4 px-4">
<div class="flex h-[2px] w-full rounded-2xl bg-button-bg"></div>
<span class="flex-shrink-0 text-sm font-semibold text-secondary">
{{ formatMessage(commonMessages.orLabel) }}
</span>
<div class="flex h-[2px] w-full rounded-2xl bg-button-bg"></div>
</div>
</div>
<div class="mx-auto flex w-fit flex-col gap-2">
<ButtonStyled v-if="project.game_versions.length === 1">
<div class="disabled button-like">
<GameIcon aria-hidden="true" />
{{
currentGameVersion
? formatMessage(messages.gameVersionLabel, { version: currentGameVersion })
: formatMessage(messages.gameVersionError)
}}
<InfoIcon
v-tooltip="
formatMessage(messages.gameVersionTooltip, {
title: project.title,
version: currentGameVersion,
})
"
class="ml-auto size-5"
/>
</div>
</ButtonStyled>
<Accordion
v-else
ref="gameVersionAccordion"
class="accordion-with-bg"
@on-open="
() => {
if (platformAccordion) {
platformAccordion.close()
}
}
"
>
<template #title>
<GameIcon aria-hidden="true" />
{{
currentGameVersion
? formatMessage(messages.gameVersionLabel, { version: currentGameVersion })
: formatMessage(messages.selectGameVersion)
}}
</template>
<label for="game-versions-filtering" hidden>{{
formatMessage(messages.searchGameVersionsLabel)
}}</label>
<StyledInput
id="game-versions-filtering"
ref="gameVersionFilterInput"
v-model="versionFilter"
type="search"
autocomplete="off"
:icon="SearchIcon"
:placeholder="formatMessage(messages.searchGameVersions)"
wrapper-class="mb-2 w-full"
/>
<ScrollablePanel :class="project.game_versions.length > 4 ? 'h-[15rem]' : ''">
<ButtonStyled
v-for="gameVersion in project.game_versions
.filter(
(x) =>
(versionFilter && x.includes(versionFilter)) ||
(!versionFilter && (showAllVersions || isReleaseGameVersion(x))),
)
.slice()
.reverse()"
:key="gameVersion"
:color="currentGameVersion === gameVersion ? 'brand' : 'standard'"
>
<button
v-tooltip="
!possibleGameVersions.includes(gameVersion)
? formatMessage(messages.gameVersionUnsupportedTooltip, {
title: project.title,
gameVersion: gameVersion,
platform: currentPlatformText,
})
: null
"
:class="{
'looks-disabled !text-brand-red':
!possibleGameVersions.includes(gameVersion),
}"
@click="
() => {
userSelectedGameVersion = gameVersion
gameVersionAccordion.close()
if (!currentPlatform && platformAccordion) {
platformAccordion.open()
}
navigateTo(
{
query: {
...route.query,
...(userSelectedGameVersion && {
version: userSelectedGameVersion,
}),
...(userSelectedPlatform && {
loader: userSelectedPlatform,
}),
},
hash: route.hash,
},
{ replace: true },
)
}
"
>
{{ gameVersion }}
<CheckIcon v-if="userSelectedGameVersion === gameVersion" />
</button>
</ButtonStyled>
</ScrollablePanel>
<Checkbox
v-if="showVersionsCheckbox"
v-model="showAllVersions"
class="mx-1"
:label="formatMessage(messages.showAllVersions)"
:disabled="!!versionFilter"
/>
</Accordion>
<ButtonStyled
v-if="project.loaders.length === 1 && project.project_type !== 'resourcepack'"
>
<div class="disabled button-like">
<WrenchIcon aria-hidden="true" />
{{
currentPlatform
? formatMessage(messages.platformLabel, {
platform: currentPlatformText,
})
: formatMessage(messages.platformError)
}}
<InfoIcon
v-tooltip="
formatMessage(messages.platformTooltip, {
title: project.title,
platform: currentPlatformText,
})
"
class="ml-auto size-5"
/>
</div>
</ButtonStyled>
<Accordion
v-else-if="project.project_type !== 'resourcepack'"
ref="platformAccordion"
class="accordion-with-bg"
@on-open="
() => {
if (gameVersionAccordion) {
gameVersionAccordion.close()
}
}
"
>
<template #title>
<WrenchIcon aria-hidden="true" />
{{
currentPlatform
? formatMessage(messages.platformLabel, {
platform: currentPlatformText,
})
: formatMessage(messages.selectPlatform)
}}
</template>
<ScrollablePanel :class="project.loaders.length > 4 ? 'h-[15rem]' : ''">
<ButtonStyled
v-for="platform in project.loaders.slice().reverse()"
:key="platform"
:color="currentPlatform === platform ? 'brand' : 'standard'"
>
<button
v-tooltip="
!possiblePlatforms.includes(platform)
? formatMessage(messages.platformUnsupportedTooltip, {
title: project.title,
platform: currentPlatformText,
gameVersion: currentGameVersion,
})
: null
"
:class="{
'looks-disabled !text-brand-red': !possiblePlatforms.includes(platform),
}"
@click="
() => {
userSelectedPlatform = platform
platformAccordion.close()
if (!currentGameVersion && gameVersionAccordion) {
gameVersionAccordion.open()
}
navigateTo(
{
query: {
...route.query,
...(userSelectedGameVersion && {
version: userSelectedGameVersion,
}),
...(userSelectedPlatform && {
loader: userSelectedPlatform,
}),
},
hash: route.hash,
},
{ replace: true },
)
}
"
>
{{ formatMessage(getTagMessage(platform, 'loader')) }}
<CheckIcon v-if="userSelectedPlatform === platform" />
</button>
</ButtonStyled>
</ScrollablePanel>
</Accordion>
</div>
<AutomaticAccordion div class="flex flex-col gap-2">
<VersionSummary
v-if="filteredRelease"
:version="filteredRelease"
:decorate-download-url="decorateModalDownloadUrl"
@on-download="onDownload"
@on-navigate="onVersionNavigate"
/>
<VersionSummary
v-if="filteredBeta"
:version="filteredBeta"
:decorate-download-url="decorateModalDownloadUrl"
@on-download="onDownload"
@on-navigate="onVersionNavigate"
/>
<VersionSummary
v-if="filteredAlpha"
:version="filteredAlpha"
:decorate-download-url="decorateModalDownloadUrl"
@on-download="onDownload"
@on-navigate="onVersionNavigate"
/>
<p
v-if="
currentPlatform &&
currentGameVersion &&
!filteredRelease &&
!filteredBeta &&
!filteredAlpha &&
!versionsLoading &&
versions.length > 0
"
>
{{
formatMessage(messages.noVersionsAvailable, {
gameVersion: currentGameVersion,
platform: currentPlatformText,
})
}}
</p>
</AutomaticAccordion>
<ServersPromo
v-if="flags.showProjectPageDownloadModalServersPromo"
:link="`/hosting#plan`"
@close="
() => {
flags.showProjectPageDownloadModalServersPromo = false
saveFeatureFlags()
}
"
/>
</div>
</template>
</NewModal>
:project="project"
:versions="versions"
:versions-loading="versionsLoading"
:tags="tags"
:download-reason="downloadReason"
:load-versions="loadVersions"
@download="triggerDownloadAnimation"
/>
<CollectionCreateModal ref="modal_collection" :project-ids="[project.id]" />
<div
class="new-page sidebar"
@@ -1048,27 +726,21 @@ import {
BookTextIcon,
CalendarIcon,
ChartIcon,
CheckIcon,
ChevronRightIcon,
ClipboardCopyIcon,
DownloadIcon,
ExternalIcon,
GameIcon,
HeartIcon,
InfoIcon,
ListIcon,
ModrinthIcon,
MoreVerticalIcon,
PlayIcon,
PlusIcon,
ReportIcon,
ScaleIcon,
ScanEyeIcon,
SearchIcon,
ServerPlusIcon,
SettingsIcon,
VersionIcon,
WrenchIcon,
XIcon,
} from '@modrinth/assets'
import {
@@ -1078,7 +750,6 @@ import {
Checkbox,
commonMessages,
defineMessages,
getTagMessage,
injectModrinthClient,
injectNotificationManager,
IntlFormatted,
@@ -1098,8 +769,6 @@ import {
ProjectSidebarServerInfo,
ProjectSidebarTags,
provideProjectPageContext,
ScrollablePanel,
ServersPromo,
StyledInput,
TagItem,
useDebugLogger,
@@ -1108,22 +777,19 @@ import {
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import VersionSummary from '@modrinth/ui/src/components/version/VersionSummary.vue'
import { capitalizeString, formatProjectType, renderString } from '@modrinth/utils'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { useLocalStorage } from '@vueuse/core'
import dayjs from 'dayjs'
import { Tooltip } from 'floating-vue'
import { nextTick, readonly, ref, useTemplateRef, watch } from 'vue'
import { readonly, ref, useTemplateRef, watch } from 'vue'
import { navigateTo } from '#app'
import Accordion from '~/components/ui/Accordion.vue'
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
import AutomaticAccordion from '~/components/ui/AutomaticAccordion.vue'
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
import MessageBanner from '~/components/ui/MessageBanner.vue'
import ModerationChecklist from '~/components/ui/moderation/checklist/ModerationChecklist.vue'
import ModerationProjectNags from '~/components/ui/moderation/ModerationProjectNags.vue'
import ProjectDownloadModal from '~/components/ui/ProjectDownloadModal.vue'
import ProjectMemberHeader from '~/components/ui/ProjectMemberHeader.vue'
import { getSignInRouteObj } from '~/composables/auth.ts'
import { saveFeatureFlags } from '~/composables/featureFlags.ts'
@@ -1190,14 +856,6 @@ const downloadModal = ref()
const openInAppModal = ref()
const overTheTopDownloadAnimation = ref()
const userSelectedGameVersion = ref(null)
const userSelectedPlatform = ref(null)
const showAllVersions = ref(false)
const gameVersionFilterInput = ref()
const versionFilter = ref('')
const projectV3Loaded = computed(() => !projectV3Pending.value || projectV3.value != null)
const isServerProject = computed(() => projectV3.value?.minecraft_server != null)
@@ -1205,80 +863,6 @@ const projectEnvironmentModal = useTemplateRef('projectEnvironmentModal')
const baseId = useId()
const currentGameVersion = computed(() => {
if (!project.value) return null
return (
userSelectedGameVersion.value ||
(project.value.game_versions.length === 1 && project.value.game_versions[0])
)
})
const possibleGameVersions = computed(() => {
return versions.value
.filter((x) => !currentPlatform.value || x.loaders.includes(currentPlatform.value))
.flatMap((x) => x.game_versions)
})
const possiblePlatforms = computed(() => {
return versions.value
.filter((x) => !currentGameVersion.value || x.game_versions.includes(currentGameVersion.value))
.flatMap((x) => x.loaders)
})
const currentPlatform = computed(() => {
if (!project.value) return null
return (
userSelectedPlatform.value || (project.value.loaders.length === 1 && project.value.loaders[0])
)
})
const currentPlatformText = computed(() => {
if (!currentPlatform.value) return null
return formatMessage(getTagMessage(currentPlatform.value, 'loader'))
})
function decorateModalDownloadUrl(url) {
return createProjectDownloadUrl(url, {
reason: downloadReason.value,
gameVersion: currentGameVersion.value ?? undefined,
loader: currentPlatform.value ?? undefined,
})
}
const releaseVersions = computed(() => {
const set = new Set()
for (const gv of tags.value.gameVersions || []) {
if (gv?.version && gv.version_type === 'release') set.add(gv.version)
}
return set
})
const nonReleaseVersions = computed(() => {
const set = new Set()
for (const gv of tags.value.gameVersions || []) {
if (gv?.version && gv.version_type !== 'release') set.add(gv.version)
}
return set
})
function isReleaseGameVersion(ver) {
if (releaseVersions.value.has(ver)) return true
if (nonReleaseVersions.value.has(ver)) return false
return true
}
const showVersionsCheckbox = computed(() => {
const list = project.value?.game_versions || []
let hasRelease = false
let hasNonRelease = false
for (const v of list) {
if (isReleaseGameVersion(v)) hasRelease = true
else hasNonRelease = true
if (hasRelease && hasNonRelease) return true
}
return false
})
const serverProject = computed(() => ({
name: project.value.title,
slug: project.value.slug || project.value.id,
@@ -1294,16 +878,6 @@ function handlePlayServerProject() {
})
}
function installWithApp() {
setTimeout(() => {
getModrinthAppAccordion.value.open()
}, 1500)
}
const gameVersionAccordion = ref()
const platformAccordion = ref()
const getModrinthAppAccordion = ref()
const formatRelativeTime = useRelativeTime()
const detailsMessages = defineMessages({
@@ -1359,18 +933,10 @@ const messages = defineMessages({
id: 'project.description.title',
defaultMessage: 'Description',
},
dontHaveModrinthApp: {
id: 'project.download.no-app',
defaultMessage: "Don't have Modrinth App?",
},
dontShowAgain: {
id: 'project.actions.dont-show-again',
defaultMessage: "Don't show again",
},
downloadTitle: {
id: 'project.download.title',
defaultMessage: 'Download {title}',
},
downloadsStat: {
id: 'project.stats.downloads-label',
defaultMessage: '{count, plural, one {download} other {downloads}}',
@@ -1409,26 +975,6 @@ const messages = defineMessages({
id: 'project.gallery.title',
defaultMessage: 'Gallery',
},
gameVersionError: {
id: 'project.download.game-version-error',
defaultMessage: 'Error: no game versions found',
},
gameVersionLabel: {
id: 'project.download.game-version',
defaultMessage: 'Game version: {version}',
},
gameVersionTooltip: {
id: 'project.download.game-version-tooltip',
defaultMessage: '{title} is only available for {version}',
},
gameVersionUnsupportedTooltip: {
id: 'project.download.game-version-unsupported-tooltip',
defaultMessage: '{title} does not support {gameVersion} for {platform}',
},
installWithModrinthApp: {
id: 'project.download.install-with-app',
defaultMessage: 'Install with Modrinth App',
},
licenseErrorMessage: {
id: 'project.license.error',
defaultMessage: 'License text could not be retrieved.',
@@ -1453,30 +999,10 @@ const messages = defineMessages({
id: 'project.collections.none-found',
defaultMessage: 'No collections found.',
},
noVersionsAvailable: {
id: 'project.download.no-versions-available',
defaultMessage: 'No versions available for {gameVersion} and {platform}.',
},
pageNotFound: {
id: 'project.error.page-not-found',
defaultMessage: 'The page could not be found',
},
platformError: {
id: 'project.download.platform-error',
defaultMessage: 'Error: no platforms found',
},
platformLabel: {
id: 'project.download.platform',
defaultMessage: 'Platform: {platform}',
},
platformTooltip: {
id: 'project.download.platform-tooltip',
defaultMessage: '{title} is only available for {platform}',
},
platformUnsupportedTooltip: {
id: 'project.download.platform-unsupported-tooltip',
defaultMessage: '{title} does not support {platform} for {gameVersion}',
},
projectIconUpdated: {
id: 'project.notification.icon-updated.title',
defaultMessage: 'Project icon updated',
@@ -1505,22 +1031,6 @@ const messages = defineMessages({
id: 'project.actions.review-project',
defaultMessage: 'Review project',
},
searchGameVersions: {
id: 'project.download.search-game-versions',
defaultMessage: 'Search game versions...',
},
searchGameVersionsLabel: {
id: 'project.download.search-game-versions-label',
defaultMessage: 'Search game versions...',
},
selectGameVersion: {
id: 'project.download.select-game-version',
defaultMessage: 'Select game version',
},
selectPlatform: {
id: 'project.download.select-platform',
defaultMessage: 'Select platform',
},
serversPromoDescription: {
id: 'project.actions.servers-promo.description',
defaultMessage: 'Modrinth Hosting is the easiest way to play with your friends without hassle!',
@@ -1537,10 +1047,6 @@ const messages = defineMessages({
id: 'project.settings.title',
defaultMessage: 'Settings',
},
showAllVersions: {
id: 'project.download.show-all-versions',
defaultMessage: 'Show all versions',
},
versionsTab: {
id: 'project.versions.title',
defaultMessage: 'Versions',
@@ -1590,48 +1096,6 @@ async function getLicenseData(event) {
}
}
const filteredVersions = computed(() => {
const result = versions.value.filter(
(x) =>
x.game_versions?.includes(currentGameVersion.value) &&
(x.loaders?.includes(currentPlatform.value) || project.value.project_type === 'resourcepack'),
)
debug('filteredVersions', {
total: versions.value.length,
filtered: result.length,
currentGameVersion: currentGameVersion.value,
currentPlatform: currentPlatform.value,
versionsEnabled: versionsEnabled.value,
versionsLoading: versionsV3Loading.value,
sampleLoaders: versions.value.slice(0, 3).map((v) => v.loaders),
})
return result
})
const filteredRelease = computed(() => {
return filteredVersions.value.find((x) => x.version_type === 'release')
})
const filteredBeta = computed(() => {
return filteredVersions.value.find(
(x) =>
x.version_type === 'beta' &&
(!filteredRelease.value ||
dayjs(x.date_published).isAfter(dayjs(filteredRelease.value.date_published))),
)
})
const filteredAlpha = computed(() => {
return filteredVersions.value.find(
(x) =>
x.version_type === 'alpha' &&
(!filteredRelease.value ||
dayjs(x.date_published).isAfter(dayjs(filteredRelease.value.date_published))) &&
(!filteredBeta.value ||
dayjs(x.date_published).isAfter(dayjs(filteredBeta.value.date_published))),
)
})
const displayCollectionsSearch = ref('')
const collections = computed(() =>
user.value && user.value.collections
@@ -2396,40 +1860,6 @@ if (!route.name.startsWith('type-project-settings')) {
const onUserCollectProject = useClientTry(userCollectProject)
const { version, loader } = route.query
if (
project.value &&
project.value.game_versions.length > 0 &&
project.value.game_versions.every((v) => !isReleaseGameVersion(v))
) {
showAllVersions.value = true
}
if (project.value && version !== undefined && project.value.game_versions.includes(version)) {
userSelectedGameVersion.value = version
}
if (project.value && loader !== undefined && project.value.loaders.includes(loader)) {
userSelectedPlatform.value = loader
}
if (route.hash === '#download' || version !== undefined || loader !== undefined) {
debug('eager loadVersions from setup', { hash: route.hash, version, loader })
loadVersions()
}
watch(downloadModal, (modal) => {
if (!modal) return
// route.hash returns everything in the hash string, including the # itself
if (route.hash === '#download') {
debug('hash #download watch fired, opening modal')
loadVersions()
modal.show()
}
})
watch(
[versionsV3, _versionsV3Error],
([data, error]) => {
@@ -2650,32 +2080,11 @@ watch(
{ immediate: true },
)
function closeDownloadModal(event) {
downloadModal.value.hide(event)
userSelectedPlatform.value = null
userSelectedGameVersion.value = null
showAllVersions.value = false
}
function triggerDownloadAnimation() {
overTheTopDownloadAnimation.value = true
setTimeout(() => (overTheTopDownloadAnimation.value = false), 500)
}
function onDownload(event) {
triggerDownloadAnimation()
setTimeout(() => {
closeDownloadModal(event)
}, 400)
}
function onVersionNavigate(url) {
closeDownloadModal()
nextTick(() => {
navigateTo(url)
})
}
async function deleteVersion(id) {
if (!id) return
@@ -2831,13 +2240,8 @@ provideProjectPageContext({
display: none;
}
:deep(.accordion-with-bg) {
@apply rounded-2xl bg-bg p-2;
--scrollable-pane-bg: var(--color-bg);
}
.over-the-top-download-animation {
position: fixed;
.over-the-top-download-animation {
position: fixed;
z-index: 100;
inset: 0;
display: flex;
@@ -2886,13 +2290,7 @@ provideProjectPageContext({
}
}
@media (hover: none) and (max-width: 767px) {
.modrinth-app-section {
display: none;
}
}
.servers-popup {
.servers-popup {
box-shadow:
0 0 12px 1px rgba(0, 175, 92, 0.6),
var(--shadow-floating);