feat+refactor: add nested dependencies route and move logic into related subcomponents

This commit is contained in:
tdgao
2026-07-02 11:31:03 -07:00
parent 0d6883c6c0
commit e5587b3f85
3 changed files with 515 additions and 627 deletions
@@ -1,15 +1,20 @@
<template>
<div v-if="dependencies.length > 0" class="flex flex-col gap-2">
<h3 class="m-0 text-sm font-bold text-contrast">
<div v-if="dependencyRows.length > 0" class="flex flex-col gap-2">
<h3 v-if="showTitle" class="m-0 text-sm font-bold text-contrast">
{{ formatMessage(messages.dependenciesTitle) }}
</h3>
<div class="flex flex-col gap-2">
<div v-for="dependency in dependencies" :key="dependency.key" class="flex flex-col gap-1.5">
<div v-for="dependency in dependencyRows" :key="dependency.key" class="flex flex-col gap-1.5">
<div
class="grid min-h-9 grid-cols-[minmax(0,1fr)_min-content] items-center gap-2 rounded-xl bg-button-bg px-3 py-2 text-primary"
>
<span class="flex min-w-0 items-center gap-2">
<Avatar v-if="dependency.icon" :src="dependency.icon" :alt="dependency.name" size="20px" />
<Avatar
v-if="dependency.icon"
:src="dependency.icon"
:alt="dependency.name"
size="20px"
/>
<PackageIcon v-else aria-hidden="true" class="size-5 flex-shrink-0 text-secondary" />
<a
v-if="dependency.projectHref"
@@ -40,50 +45,15 @@
</ButtonStyled>
</div>
<div
v-for="subDependency in dependency.subDependencies"
:key="subDependency.key"
class="grid grid-cols-[1.5rem_minmax(0,1fr)] items-center gap-1 pl-5"
v-if="dependency.subDependencies.length > 0"
class="grid grid-cols-[1.5rem_minmax(0,1fr)] items-start gap-1 pl-5"
>
<RightArrowIcon aria-hidden="true" class="size-4 text-secondary" />
<div
class="grid min-h-9 grid-cols-[minmax(0,1fr)_min-content] items-center gap-2 rounded-xl bg-button-bg px-3 py-2 text-primary"
>
<span class="flex min-w-0 items-center gap-2">
<Avatar
v-if="subDependency.icon"
:src="subDependency.icon"
:alt="subDependency.name"
size="20px"
/>
<PackageIcon v-else aria-hidden="true" class="size-5 flex-shrink-0 text-secondary" />
<a
v-if="subDependency.projectHref"
:href="subDependency.projectHref"
target="_blank"
rel="noopener noreferrer"
class="min-w-0 truncate font-semibold text-contrast no-underline hover:underline"
>
{{ subDependency.name }}
</a>
<span v-else class="min-w-0 truncate font-semibold text-contrast">
{{ subDependency.name }}
</span>
<TagItem class="shrink-0 border !border-solid border-surface-5">
{{ subDependency.typeLabel }}
</TagItem>
</span>
<ButtonStyled v-if="subDependency.downloadHref" circular type="transparent">
<a
v-tooltip="'Download'"
:href="subDependency.downloadHref"
:download="subDependency.filename"
:aria-label="`Download ${subDependency.name}`"
@click="emit('download')"
>
<DownloadIcon aria-hidden="true" class="size-5 text-secondary" />
</a>
</ButtonStyled>
</div>
<RightArrowIcon aria-hidden="true" class="mt-2.5 size-4 text-secondary" />
<DownloadDependencies
:dependencies="dependency.subDependencies"
:show-title="false"
@download="emit('download')"
/>
</div>
</div>
</div>
@@ -92,22 +62,205 @@
<script setup>
import { DownloadIcon, PackageIcon, RightArrowIcon } from '@modrinth/assets'
import { Avatar, ButtonStyled, defineMessages, TagItem, useVIntl } from '@modrinth/ui'
import {
Avatar,
ButtonStyled,
defineMessages,
injectModrinthClient,
TagItem,
useVIntl,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import { computed } from 'vue'
defineOptions({
name: 'DownloadDependencies',
})
defineProps({
const props = defineProps({
dependencies: {
type: Array,
default: () => [],
default: null,
},
project: {
type: Object,
default: null,
},
selectedVersion: {
type: Object,
default: null,
},
currentGameVersion: {
type: [String, Boolean],
default: null,
},
currentPlatform: {
type: [String, Boolean],
default: null,
},
downloadReason: {
type: String,
default: 'standalone',
},
showTitle: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['download'])
const client = injectModrinthClient()
const { createProjectDownloadUrl } = useCdnDownloadContext()
const { formatMessage } = useVIntl()
const dependencyResolutionPreferences = computed(() => ({
game_versions: props.currentGameVersion ? [props.currentGameVersion] : [],
loaders: props.currentPlatform ? [props.currentPlatform] : [],
}))
const { data: dependencyResolution } = useQuery({
queryKey: computed(() => [
'project-download-modal',
'content-resolve',
props.project?.id,
props.selectedVersion?.id,
props.project?.project_type,
dependencyResolutionPreferences.value,
]),
queryFn: () =>
client.labrinth.content_v3.resolve({
project_id: props.project.id,
version_id: props.selectedVersion.id,
content_type: resolveContentType(props.project.project_type),
selected: dependencyResolutionPreferences.value,
target: dependencyResolutionPreferences.value,
}),
enabled: computed(() => !!props.project && !!props.selectedVersion),
})
const dependencyVersionIds = computed(() => {
return [
...new Set(
(dependencyResolution.value?.dependencies || []).map((dependency) => dependency.version_id),
),
]
})
const { data: dependencyVersions } = useQuery({
queryKey: computed(() => [
'project-download-modal',
'resolved-versions',
dependencyVersionIds.value,
]),
queryFn: () => client.labrinth.versions_v3.getVersions(dependencyVersionIds.value),
enabled: computed(() => dependencyVersionIds.value.length > 0),
})
const dependencyVersionById = computed(() => {
const map = new Map()
for (const version of dependencyVersions.value || []) {
if (!version) continue
map.set(version.id, version)
}
return map
})
const dependencyProjectIds = computed(() => {
return [
...new Set(
(dependencyResolution.value?.dependencies || []).map((dependency) => dependency.project_id),
),
]
})
const { data: dependencyProjects } = useQuery({
queryKey: computed(() => [
'project-download-modal',
'resolved-projects',
dependencyProjectIds.value,
]),
queryFn: () => client.labrinth.projects_v2.getMultiple(dependencyProjectIds.value),
enabled: computed(() => dependencyProjectIds.value.length > 0),
})
const dependencyProjectById = computed(() => {
const map = new Map()
for (const project of dependencyProjects.value || []) {
map.set(project.id, project)
}
return map
})
const dependenciesByParentVersionId = computed(() => {
const map = new Map()
for (const dependency of dependencyResolution.value?.dependencies || []) {
if (!dependency.dependent_on_version_id) continue
const dependencies = map.get(dependency.dependent_on_version_id) || []
dependencies.push(dependency)
map.set(dependency.dependent_on_version_id, dependencies)
}
return map
})
const resolvedDependencyRows = computed(() => {
const primaryVersionId =
dependencyResolution.value?.primary.version_id || props.selectedVersion?.id
if (!primaryVersionId) return []
return (dependenciesByParentVersionId.value.get(primaryVersionId) || []).map((dependency) =>
createDependencyRow(dependency),
)
})
const dependencyRows = computed(() => props.dependencies || resolvedDependencyRows.value)
function primaryFileForVersion(version) {
return version?.files?.find((file) => file.primary) || version?.files?.[0]
}
function createDependencyRow(dependency) {
const version = dependencyVersionById.value.get(dependency.version_id)
const project = dependencyProjectById.value.get(dependency.project_id)
const primaryFile = primaryFileForVersion(version)
const name =
project?.title ||
version?.name ||
version?.version_number ||
dependency.version_id ||
dependency.project_id ||
'Dependency'
return {
key: `${dependency.project_id}-${dependency.version_id}`,
name,
icon: project?.icon_url,
projectHref: project ? `/${project.project_type}/${project.slug || project.id}` : undefined,
downloadHref: primaryFile ? getDownloadUrl(primaryFile.url) : undefined,
filename: primaryFile?.filename,
typeLabel: 'Required',
subDependencies: (dependenciesByParentVersionId.value.get(dependency.version_id) || []).map(
(subDependency) => createDependencyRow(subDependency),
),
}
}
function resolveContentType(projectType) {
return ['mod', 'plugin', 'datapack', 'resourcepack', 'shader', 'modpack'].includes(projectType)
? projectType
: 'mod'
}
function getDownloadUrl(url) {
return createProjectDownloadUrl(url, {
reason: props.downloadReason,
gameVersion: props.currentGameVersion ?? undefined,
loader: props.currentPlatform ?? undefined,
})
}
const messages = defineMessages({
dependenciesTitle: {
id: 'project.download.dependencies-title',
@@ -11,9 +11,9 @@
:no-options-message="formatMessage(messages.noGameVersionsFound)"
trigger-class="!rounded-xl !bg-button-bg !px-3 !py-2"
dropdown-class="!rounded-xl"
@update:model-value="emit('selectGameVersion', $event)"
@search-input="emit('update:versionFilter', $event)"
@close="emit('update:versionFilter', '')"
@update:model-value="selectGameVersion"
@search-input="versionFilter = $event"
@close="versionFilter = ''"
>
<template #option="{ item, isSelected }">
<div
@@ -37,7 +37,10 @@
</div>
</template>
<template #dropdown-footer>
<div v-if="showVersionsCheckbox" class="border-0 border-t border-solid border-surface-5 p-3">
<div
v-if="showVersionsCheckbox"
class="border-0 border-t border-solid border-surface-5 p-3"
>
<Checkbox
v-model="showAllVersionsModel"
:label="formatMessage(messages.showAllVersions)"
@@ -54,7 +57,7 @@
:placeholder="formatMessage(messages.selectPlatform)"
trigger-class="!rounded-xl !bg-button-bg !px-3 !py-2"
dropdown-class="!rounded-xl"
@update:model-value="emit('selectPlatform', $event)"
@update:model-value="selectPlatform"
>
<template #option="{ item, isSelected }">
<div
@@ -111,13 +114,30 @@
</a>
</ButtonStyled>
</div>
<p v-else-if="currentPlatform && currentGameVersion && !versionsLoading && versions.length > 0">
{{
formatMessage(messages.noVersionsAvailable, {
gameVersion: currentGameVersion,
platform: currentPlatformText,
})
}}
</p>
</template>
<script setup>
import { DownloadIcon } from '@modrinth/assets'
import { ButtonStyled, Checkbox, Combobox, defineMessages, useVIntl } from '@modrinth/ui'
import {
ButtonStyled,
Checkbox,
Combobox,
defineMessages,
getTagMessage,
useDebugLogger,
useVIntl,
} from '@modrinth/ui'
import VersionChannelTag from '@modrinth/ui/src/components/version/VersionChannelTag.vue'
import { computed } from 'vue'
import dayjs from 'dayjs'
import { computed, ref, watch } from 'vue'
defineOptions({
name: 'DownloadProject',
@@ -128,78 +148,262 @@ const props = defineProps({
type: Object,
required: true,
},
gameVersionOptions: {
versions: {
type: Array,
default: () => [],
},
currentGameVersion: {
type: [String, Boolean],
default: null,
},
possibleGameVersions: {
type: Array,
default: () => [],
},
currentPlatformText: {
type: String,
default: null,
},
showVersionsCheckbox: {
versionsLoading: {
type: Boolean,
default: false,
},
showAllVersions: {
type: Boolean,
default: false,
tags: {
type: Object,
required: true,
},
versionFilter: {
downloadReason: {
type: String,
default: '',
default: 'standalone',
},
currentPlatform: {
type: [String, Boolean],
default: null,
},
platformOptions: {
type: Array,
default: () => [],
},
possiblePlatforms: {
type: Array,
default: () => [],
},
selectedVersion: {
type: Object,
default: null,
},
selectedPrimaryFile: {
type: Object,
default: null,
},
selectedPrimaryFileDownloadUrl: {
initialGameVersion: {
type: String,
default: null,
},
initialPlatform: {
type: String,
default: null,
},
resetKey: {
type: Number,
default: 0,
},
})
const emit = defineEmits([
'download',
'selectGameVersion',
'selectPlatform',
'update:showAllVersions',
'update:versionFilter',
])
const emit = defineEmits(['download', 'selectGameVersion', 'selectPlatform', 'update:selection'])
const { formatMessage } = useVIntl()
const { createProjectDownloadUrl } = useCdnDownloadContext()
const debug = useDebugLogger('DownloadProject')
const userSelectedGameVersion = ref(props.initialGameVersion)
const userSelectedPlatform = ref(props.initialPlatform)
const showAllVersions = ref(defaultShowAllVersions())
const versionFilter = ref('')
const showAllVersionsModel = computed({
get() {
return props.showAllVersions
return showAllVersions.value
},
set(value) {
emit('update:showAllVersions', value)
showAllVersions.value = value
},
})
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 gameVersionOptions = computed(() => {
return filteredGameVersions.value.map((gameVersion) => ({
value: gameVersion,
label: gameVersion,
}))
})
const platformOptions = computed(() => {
return props.project.loaders
.slice()
.reverse()
.map((platform) => ({
value: platform,
label: formatMessage(getTagMessage(platform, 'loader')),
}))
})
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 selectedVersion = computed(() => {
return filteredRelease.value || filteredBeta.value || filteredAlpha.value
})
const selectedPrimaryFile = computed(() => {
return (
selectedVersion.value?.files?.find((file) => file.primary) || selectedVersion.value?.files?.[0]
)
})
const selectedPrimaryFileDownloadUrl = computed(() => {
if (!selectedPrimaryFile.value) return null
return getDownloadUrl(selectedPrimaryFile.value.url)
})
watch(
[currentGameVersion, currentPlatform, selectedVersion, selectedPrimaryFile],
() => {
emit('update:selection', {
currentGameVersion: currentGameVersion.value,
currentPlatform: currentPlatform.value,
selectedVersion: selectedVersion.value,
selectedPrimaryFile: selectedPrimaryFile.value,
})
},
{ immediate: true },
)
watch(
() => props.resetKey,
() => {
userSelectedGameVersion.value = null
userSelectedPlatform.value = null
showAllVersions.value = defaultShowAllVersions()
versionFilter.value = ''
},
)
function selectGameVersion(gameVersion) {
userSelectedGameVersion.value = gameVersion
emit('selectGameVersion', gameVersion)
}
function selectPlatform(platform) {
userSelectedPlatform.value = platform
emit('selectPlatform', platform)
}
function getDownloadUrl(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 defaultShowAllVersions() {
return (
props.project.game_versions.length > 0 &&
props.project.game_versions.every((projectVersion) => {
const gameVersion = props.tags.gameVersions?.find((x) => x.version === projectVersion)
return gameVersion?.version_type && gameVersion.version_type !== 'release'
})
)
}
const messages = defineMessages({
gameVersionUnsupportedTooltip: {
id: 'project.download.game-version-unsupported-tooltip',
@@ -213,6 +417,10 @@ const messages = defineMessages({
id: 'project.download.no-game-versions-found',
defaultMessage: 'No game versions found',
},
noVersionsAvailable: {
id: 'project.download.no-versions-available',
defaultMessage: 'No versions available for {gameVersion} and {platform}.',
},
platformUnsupportedTooltip: {
id: 'project.download.platform-unsupported-tooltip',
defaultMessage: '{title} does not support {platform} for {gameVersion}',
@@ -11,43 +11,27 @@
<InstallWithModrinthApp :project="project" :tags="tags" />
<DownloadProject
:project="project"
:game-version-options="gameVersionOptions"
:current-game-version="currentGameVersion"
:possible-game-versions="possibleGameVersions"
:current-platform-text="currentPlatformText"
:show-versions-checkbox="showVersionsCheckbox"
:show-all-versions="showAllVersions"
:version-filter="versionFilter"
:current-platform="currentPlatform"
:platform-options="platformOptions"
:possible-platforms="possiblePlatforms"
:selected-version="selectedVersion"
:selected-primary-file="selectedPrimaryFile"
:selected-primary-file-download-url="selectedPrimaryFileDownloadUrl"
:versions="versions"
:versions-loading="versionsLoading"
:tags="tags"
:download-reason="downloadReason"
:initial-game-version="initialGameVersion"
:initial-platform="initialPlatform"
:reset-key="downloadProjectResetKey"
@select-game-version="selectGameVersion"
@select-platform="selectPlatform"
@update:show-all-versions="showAllVersions = $event"
@update:version-filter="versionFilter = $event"
@update:selection="projectDownloadSelection = $event"
@download="onDownload"
/>
<div class="flex flex-col gap-4">
<p
v-if="
currentPlatform &&
currentGameVersion &&
!selectedVersion &&
!versionsLoading &&
versions.length > 0
"
>
{{
formatMessage(messages.noVersionsAvailable, {
gameVersion: currentGameVersion,
platform: currentPlatformText,
})
}}
</p>
<DownloadDependencies :dependencies="dependencyRows" @download="onDownload" />
<DownloadDependencies
:project="project"
:selected-version="selectedVersion"
:current-game-version="currentGameVersion"
:current-platform="currentPlatform"
:download-reason="downloadReason"
@download="onDownload"
/>
<div v-if="additionalFiles.length > 0" class="flex flex-col gap-2">
<h3 class="m-0 text-sm font-bold text-contrast">
{{ formatMessage(messages.additionalFilesTitle) }}
@@ -97,16 +81,12 @@ import { DownloadIcon, FileIcon } from '@modrinth/assets'
import {
Avatar,
defineMessages,
getTagMessage,
injectModrinthClient,
NewModal,
ServersPromo,
useDebugLogger,
useVIntl,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import dayjs from 'dayjs'
import { computed, nextTick, ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { navigateTo } from '#app'
import { saveFeatureFlags } from '~/composables/featureFlags.ts'
@@ -146,389 +126,38 @@ const emit = defineEmits(['download'])
const route = useRoute()
const flags = useFeatureFlags()
const client = injectModrinthClient()
const { createProjectDownloadUrl } = useCdnDownloadContext()
const { formatMessage } = useVIntl()
const debug = useDebugLogger('DownloadModal')
const modal = ref()
const modalOpen = ref(false)
const userSelectedGameVersion = ref(null)
const userSelectedPlatform = ref(null)
const showAllVersions = ref(false)
const versionFilter = ref('')
const currentGameVersion = computed(() => {
return (
userSelectedGameVersion.value ||
(props.project.game_versions.length === 1 && props.project.game_versions[0])
)
const downloadProjectResetKey = ref(0)
const projectDownloadSelection = ref({
currentGameVersion: null,
currentPlatform: null,
selectedVersion: null,
selectedPrimaryFile: null,
})
const possibleGameVersions = computed(() => {
return props.versions
.filter((x) => !currentPlatform.value || x.loaders.includes(currentPlatform.value))
.flatMap((x) => x.game_versions)
})
const { version, loader } = route.query
const initialGameVersion = ref(
typeof version === 'string' && props.project.game_versions.includes(version) ? version : null,
)
const initialPlatform = ref(
typeof loader === 'string' && props.project.loaders.includes(loader) ? loader : null,
)
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 gameVersionOptions = computed(() => {
return filteredGameVersions.value.map((gameVersion) => ({
value: gameVersion,
label: gameVersion,
}))
})
const platformOptions = computed(() => {
return props.project.loaders
.slice()
.reverse()
.map((platform) => ({
value: platform,
label: formatMessage(getTagMessage(platform, 'loader')),
}))
})
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 selectedVersion = computed(() => {
return filteredRelease.value || filteredBeta.value || filteredAlpha.value
})
const selectedPrimaryFile = computed(() => {
return (
selectedVersion.value?.files?.find((file) => file.primary) || selectedVersion.value?.files?.[0]
)
})
const selectedPrimaryFileDownloadUrl = computed(() => {
if (!selectedPrimaryFile.value) return null
return getDownloadUrl(selectedPrimaryFile.value.url)
})
const currentGameVersion = computed(() => projectDownloadSelection.value.currentGameVersion)
const currentPlatform = computed(() => projectDownloadSelection.value.currentPlatform)
const selectedVersion = computed(() => projectDownloadSelection.value.selectedVersion)
const selectedPrimaryFile = computed(() => projectDownloadSelection.value.selectedPrimaryFile)
const additionalFiles = computed(() => {
if (!selectedVersion.value || !selectedPrimaryFile.value) return []
return selectedVersion.value.files.filter((file) => file !== selectedPrimaryFile.value)
})
const selectedDependencyVersionIds = computed(() => {
if (!selectedVersion.value) return []
return [
...new Set(
selectedVersion.value.dependencies.map((dependency) => dependency.version_id).filter(Boolean),
),
]
})
const { data: dependencyVersions } = useQuery({
queryKey: computed(() => [
'project-download-modal',
'versions',
selectedDependencyVersionIds.value,
]),
queryFn: () => client.labrinth.versions_v3.getVersions(selectedDependencyVersionIds.value),
enabled: computed(() => selectedDependencyVersionIds.value.length > 0),
})
const fallbackDependencyProjectIds = computed(() => {
if (!selectedVersion.value) return []
return [
...new Set(
selectedVersion.value.dependencies
.filter(
(dependency) =>
['required', 'optional'].includes(dependency.dependency_type) &&
!dependency.version_id &&
dependency.project_id,
)
.map((dependency) => dependency.project_id),
),
]
})
const { data: fallbackDependencyVersions } = useQuery({
queryKey: computed(() => [
'project-download-modal',
'fallback-versions',
fallbackDependencyProjectIds.value,
]),
queryFn: async () =>
Promise.all(
fallbackDependencyProjectIds.value.map(async (projectId) => {
const versions = await client.labrinth.versions_v3.getProjectVersions(projectId, {
include_changelog: false,
limit: 1,
})
return versions[0]
}),
),
enabled: computed(() => fallbackDependencyProjectIds.value.length > 0),
})
const subDependencyVersionIds = computed(() => {
const ids = new Set()
for (const version of [
...(dependencyVersions.value || []),
...(fallbackDependencyVersions.value || []),
]) {
if (!version) continue
for (const dependency of version.dependencies || []) {
if (dependency.version_id) ids.add(dependency.version_id)
}
}
return [...ids]
})
const { data: subDependencyVersions } = useQuery({
queryKey: computed(() => [
'project-download-modal',
'sub-versions',
subDependencyVersionIds.value,
]),
queryFn: () => client.labrinth.versions_v3.getVersions(subDependencyVersionIds.value),
enabled: computed(() => subDependencyVersionIds.value.length > 0),
})
const fallbackSubDependencyProjectIds = computed(() => {
const ids = new Set()
for (const version of [
...(dependencyVersions.value || []),
...(fallbackDependencyVersions.value || []),
]) {
if (!version) continue
for (const dependency of version.dependencies || []) {
if (
['required', 'optional'].includes(dependency.dependency_type) &&
!dependency.version_id &&
dependency.project_id
) {
ids.add(dependency.project_id)
}
}
}
return [...ids]
})
const { data: fallbackSubDependencyVersions } = useQuery({
queryKey: computed(() => [
'project-download-modal',
'fallback-sub-versions',
fallbackSubDependencyProjectIds.value,
]),
queryFn: async () =>
Promise.all(
fallbackSubDependencyProjectIds.value.map(async (projectId) => {
const versions = await client.labrinth.versions_v3.getProjectVersions(projectId, {
include_changelog: false,
limit: 1,
})
return versions[0]
}),
),
enabled: computed(() => fallbackSubDependencyProjectIds.value.length > 0),
})
const dependencyVersionById = computed(() => {
const map = new Map()
for (const version of [
...(dependencyVersions.value || []),
...(subDependencyVersions.value || []),
...(fallbackDependencyVersions.value || []),
...(fallbackSubDependencyVersions.value || []),
]) {
if (!version) continue
map.set(version.id, version)
}
return map
})
const fallbackDependencyVersionByProjectId = computed(() => {
const map = new Map()
for (const version of [
...(fallbackDependencyVersions.value || []),
...(fallbackSubDependencyVersions.value || []),
]) {
if (!version) continue
map.set(version.project_id, version)
}
return map
})
const selectedDependencyProjectIds = computed(() => {
const ids = new Set()
for (const dependency of selectedVersion.value?.dependencies || []) {
if (dependency.project_id) ids.add(dependency.project_id)
const version = dependency.version_id
? dependencyVersionById.value.get(dependency.version_id)
: null
if (version?.project_id) ids.add(version.project_id)
}
for (const version of [
...(dependencyVersions.value || []),
...(fallbackDependencyVersions.value || []),
]) {
if (!version) continue
for (const dependency of version.dependencies || []) {
if (dependency.project_id) ids.add(dependency.project_id)
const dependencyVersion = dependency.version_id
? dependencyVersionById.value.get(dependency.version_id)
: null
if (dependencyVersion?.project_id) ids.add(dependencyVersion.project_id)
}
}
for (const version of [
...(fallbackDependencyVersions.value || []),
...(fallbackSubDependencyVersions.value || []),
]) {
if (version?.project_id) ids.add(version.project_id)
}
return [...ids]
})
const { data: dependencyProjects } = useQuery({
queryKey: computed(() => [
'project-download-modal',
'projects',
selectedDependencyProjectIds.value,
]),
queryFn: () => client.labrinth.projects_v2.getMultiple(selectedDependencyProjectIds.value),
enabled: computed(() => selectedDependencyProjectIds.value.length > 0),
})
const dependencyProjectById = computed(() => {
const map = new Map()
for (const project of dependencyProjects.value || []) {
map.set(project.id, project)
}
return map
})
const dependencyRows = computed(() => {
if (!selectedVersion.value) return []
return selectedVersion.value.dependencies
.filter((dependency) => ['required', 'optional'].includes(dependency.dependency_type))
.sort(
(a, b) =>
dependencyTypeSortOrder(a.dependency_type) - dependencyTypeSortOrder(b.dependency_type),
)
.map((dependency) => createDependencyRow(dependency))
})
const messages = defineMessages({
downloadTitle: {
id: 'project.download.title',
@@ -538,32 +167,8 @@ const messages = defineMessages({
id: 'project.download.additional-files-title',
defaultMessage: 'Additional files',
},
noVersionsAvailable: {
id: 'project.download.no-versions-available',
defaultMessage: 'No versions available for {gameVersion} and {platform}.',
},
})
function dependencyTypeLabel(type) {
return (
{
required: 'Required',
optional: 'Optional',
embedded: 'Embedded',
incompatible: 'Incompatible',
}[type] || type
)
}
function dependencyTypeSortOrder(type) {
return (
{
required: 0,
optional: 1,
}[type] ?? 2
)
}
function fileTypeLabel(type) {
return (
{
@@ -574,56 +179,6 @@ function fileTypeLabel(type) {
)
}
function primaryFileForVersion(version) {
return version?.files?.find((file) => file.primary) || version?.files?.[0]
}
function createDependencyRow(dependency, includeSubDependencies = true) {
const explicitVersion = dependency.version_id
? dependencyVersionById.value.get(dependency.version_id)
: null
// todo: need algorithm for picking version.
// try to match same game version and loader. and if cannot, then disable button and have tooltip saying cannot find any compatible versions to download.
const version =
explicitVersion ||
(dependency.project_id
? fallbackDependencyVersionByProjectId.value.get(dependency.project_id)
: null)
const projectId = dependency.project_id || version?.project_id
const project = projectId ? dependencyProjectById.value.get(projectId) : null
const primaryFile = primaryFileForVersion(version)
const name =
project?.title ||
dependency.file_name ||
version?.name ||
version?.version_number ||
dependency.version_id ||
dependency.project_id ||
'Dependency'
return {
key: `${dependency.dependency_type}-${dependency.version_id ?? dependency.project_id ?? dependency.file_name ?? name}`,
name,
icon: project?.icon_url,
projectHref: project ? `/${project.project_type}/${project.slug || project.id}` : undefined,
downloadHref: primaryFile ? getDownloadUrl(primaryFile.url) : undefined,
filename: primaryFile?.filename,
typeLabel: dependencyTypeLabel(dependency.dependency_type),
subDependencies: includeSubDependencies
? (version?.dependencies || [])
.filter((subDependency) =>
['required', 'optional'].includes(subDependency.dependency_type),
)
.sort(
(a, b) =>
dependencyTypeSortOrder(a.dependency_type) -
dependencyTypeSortOrder(b.dependency_type),
)
.map((subDependency) => createDependencyRow(subDependency, false))
: [],
}
}
function getDownloadUrl(url) {
return createProjectDownloadUrl(url, {
reason: props.downloadReason,
@@ -632,22 +187,16 @@ function getDownloadUrl(url) {
})
}
function isReleaseGameVersion(version) {
if (releaseVersions.value.has(version)) return true
if (nonReleaseVersions.value.has(version)) return false
return true
}
function updateDownloadQuery() {
function updateDownloadQuery({ gameVersion, platform }) {
navigateTo(
{
query: {
...route.query,
...(userSelectedGameVersion.value && {
version: userSelectedGameVersion.value,
...(gameVersion && {
version: gameVersion,
}),
...(userSelectedPlatform.value && {
loader: userSelectedPlatform.value,
...(platform && {
loader: platform,
}),
},
hash: route.hash,
@@ -657,13 +206,17 @@ function updateDownloadQuery() {
}
function selectGameVersion(gameVersion) {
userSelectedGameVersion.value = gameVersion
updateDownloadQuery()
updateDownloadQuery({
gameVersion,
platform: currentPlatform.value,
})
}
function selectPlatform(platform) {
userSelectedPlatform.value = platform
updateDownloadQuery()
updateDownloadQuery({
gameVersion: currentGameVersion.value,
platform,
})
}
function onShow() {
@@ -687,22 +240,13 @@ function show(event) {
function hide(event) {
if (!modal.value || !modalOpen.value) return
modal.value?.hide(event)
userSelectedPlatform.value = null
userSelectedGameVersion.value = null
showAllVersions.value = false
downloadProjectResetKey.value += 1
}
function onDownload() {
emit('download')
}
function onVersionNavigate(url) {
hide()
nextTick(() => {
navigateTo(url)
})
}
function openFromHash() {
if (!modal.value || modalOpen.value || route.hash !== '#download') return
@@ -710,23 +254,6 @@ function openFromHash() {
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()