refactor: use ts and also have modal own get project/versions queries

This commit is contained in:
tdgao
2026-07-02 14:01:17 -07:00
parent 0b69acb3aa
commit 355450535d
6 changed files with 391 additions and 238 deletions
@@ -14,8 +14,15 @@
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import { defineMessages, injectModrinthClient, useVIntl } from '@modrinth/ui' import type { Labrinth } from '@modrinth/api-client'
import {
type CdnDownloadReason,
defineMessages,
injectModrinthClient,
useVIntl,
} from '@modrinth/ui'
import type { DisplayProjectType } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query' import { useQuery } from '@tanstack/vue-query'
import { computed } from 'vue' import { computed } from 'vue'
@@ -25,38 +32,49 @@ defineOptions({
name: 'DownloadDependencies', name: 'DownloadDependencies',
}) })
const props = defineProps({ type DownloadModalProject = Omit<Labrinth.Projects.v2.Project, 'project_type'> & {
dependencies: { project_type: DisplayProjectType
type: Array, actualProjectType: Labrinth.Projects.v2.ProjectType
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']) type ResolvedContent = Labrinth.Content.v3.ResolvedContent | Labrinth.Content.v3.SkippedContent
interface DownloadDependencyRow {
key: string
name: string
icon?: string
projectHref?: string
downloadHref?: string
filename?: string
typeLabel: string
unavailableTooltip: string
dependencies: DownloadDependencyRow[]
}
const props = withDefaults(
defineProps<{
dependencies?: DownloadDependencyRow[] | null
project?: DownloadModalProject | null
selectedVersion?: Labrinth.Versions.v3.Version | null
currentGameVersion?: string | null
currentPlatform?: string | null
downloadReason?: CdnDownloadReason
showTitle?: boolean
}>(),
{
dependencies: null,
project: null,
selectedVersion: null,
currentGameVersion: null,
currentPlatform: null,
downloadReason: 'standalone',
showTitle: true,
},
)
const emit = defineEmits<{
download: []
}>()
const client = injectModrinthClient() const client = injectModrinthClient()
const { createProjectDownloadUrl } = useCdnDownloadContext() const { createProjectDownloadUrl } = useCdnDownloadContext()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
@@ -65,7 +83,7 @@ const shouldResolveDependencies = computed(
() => !props.dependencies && !!props.project && !!props.selectedVersion, () => !props.dependencies && !!props.project && !!props.selectedVersion,
) )
const dependencyResolutionPreferences = computed(() => ({ const dependencyResolutionPreferences = computed<Labrinth.Content.v3.ResolutionPreferences>(() => ({
game_versions: props.selectedVersion?.game_versions || [], game_versions: props.selectedVersion?.game_versions || [],
loaders: props.selectedVersion?.loaders || [], loaders: props.selectedVersion?.loaders || [],
})) }))
@@ -81,16 +99,16 @@ const { data: dependencyResolution } = useQuery({
]), ]),
queryFn: () => queryFn: () =>
client.labrinth.content_v3.resolve({ client.labrinth.content_v3.resolve({
project_id: props.project.id, project_id: props.project!.id,
version_id: props.selectedVersion.id, version_id: props.selectedVersion!.id,
content_type: resolveContentType(props.project.project_type), content_type: resolveContentType(props.project!.project_type),
selected: dependencyResolutionPreferences.value, selected: dependencyResolutionPreferences.value,
target: dependencyResolutionPreferences.value, target: dependencyResolutionPreferences.value,
}), }),
enabled: shouldResolveDependencies, enabled: shouldResolveDependencies,
}) })
const dependencyVersionIds = computed(() => { const dependencyVersionIds = computed<string[]>(() => {
return [ return [
...new Set( ...new Set(
[ [
@@ -98,7 +116,7 @@ const dependencyVersionIds = computed(() => {
...(dependencyResolution.value?.skipped || []), ...(dependencyResolution.value?.skipped || []),
] ]
.map((dependency) => dependency.version_id) .map((dependency) => dependency.version_id)
.filter(Boolean), .filter((versionId): versionId is string => !!versionId),
), ),
] ]
}) })
@@ -114,7 +132,7 @@ const { data: dependencyVersions } = useQuery({
}) })
const dependencyVersionById = computed(() => { const dependencyVersionById = computed(() => {
const map = new Map() const map = new Map<string, Labrinth.Versions.v3.Version>()
for (const version of dependencyVersions.value || []) { for (const version of dependencyVersions.value || []) {
if (!version) continue if (!version) continue
map.set(version.id, version) map.set(version.id, version)
@@ -122,7 +140,7 @@ const dependencyVersionById = computed(() => {
return map return map
}) })
const dependencyProjectIds = computed(() => { const dependencyProjectIds = computed<string[]>(() => {
return [ return [
...new Set( ...new Set(
[ [
@@ -130,7 +148,7 @@ const dependencyProjectIds = computed(() => {
...(dependencyResolution.value?.skipped || []), ...(dependencyResolution.value?.skipped || []),
] ]
.map((dependency) => dependency.project_id) .map((dependency) => dependency.project_id)
.filter(Boolean), .filter((projectId): projectId is string => !!projectId),
), ),
] ]
}) })
@@ -146,7 +164,7 @@ const { data: dependencyProjects } = useQuery({
}) })
const dependencyProjectById = computed(() => { const dependencyProjectById = computed(() => {
const map = new Map() const map = new Map<string, Labrinth.Projects.v2.Project>()
for (const project of dependencyProjects.value || []) { for (const project of dependencyProjects.value || []) {
map.set(project.id, project) map.set(project.id, project)
} }
@@ -154,7 +172,7 @@ const dependencyProjectById = computed(() => {
}) })
const dependenciesByParentVersionId = computed(() => { const dependenciesByParentVersionId = computed(() => {
const map = new Map() const map = new Map<string, ResolvedContent[]>()
for (const dependency of [ for (const dependency of [
...(dependencyResolution.value?.dependencies || []), ...(dependencyResolution.value?.dependencies || []),
@@ -170,7 +188,7 @@ const dependenciesByParentVersionId = computed(() => {
return map return map
}) })
const resolvedDependencyRows = computed(() => { const resolvedDependencyRows = computed<DownloadDependencyRow[]>(() => {
const primaryVersionId = const primaryVersionId =
dependencyResolution.value?.primary.version_id || props.selectedVersion?.id dependencyResolution.value?.primary.version_id || props.selectedVersion?.id
if (!primaryVersionId) return [] if (!primaryVersionId) return []
@@ -180,44 +198,46 @@ const resolvedDependencyRows = computed(() => {
return dependencies.map((dependency) => createDependencyRow(dependency)) return dependencies.map((dependency) => createDependencyRow(dependency))
}) })
const dependencyRows = computed(() => props.dependencies || resolvedDependencyRows.value) const dependencyRows = computed<DownloadDependencyRow[]>(
() => props.dependencies || resolvedDependencyRows.value,
)
function primaryFileForVersion(version) { function primaryFileForVersion(version?: Labrinth.Versions.v3.Version) {
return version?.files?.find((file) => file.primary) || version?.files?.[0] return version?.files?.find((file) => file.primary) || version?.files?.[0]
} }
function createDependencyRow(dependency) { function createDependencyRow(dependency: ResolvedContent): DownloadDependencyRow {
const version = dependencyVersionById.value.get(dependency.version_id) const versionId = dependency.version_id ?? undefined
const version = versionId ? dependencyVersionById.value.get(versionId) : undefined
const project = dependencyProjectById.value.get(dependency.project_id) const project = dependencyProjectById.value.get(dependency.project_id)
const primaryFile = primaryFileForVersion(version) const primaryFile = primaryFileForVersion(version)
const unavailableTooltip = dependency.reason const unavailableTooltip =
? skippedReasonLabel(dependency.reason) 'reason' in dependency && dependency.reason
: formatMessage(messages.unavailableDependency) ? skippedReasonLabel(dependency.reason)
: formatMessage(messages.unavailableDependency)
const name = const name =
project?.title || project?.title || version?.name || version?.version_number || versionId || dependency.project_id
version?.name ||
version?.version_number ||
dependency.version_id ||
dependency.project_id ||
'Dependency'
return { return {
key: `${dependency.project_id}-${dependency.version_id ?? 'unresolved'}-${dependency.reason ?? 'resolved'}`, key: `${dependency.project_id}-${versionId ?? 'unresolved'}-${
'reason' in dependency ? dependency.reason : 'resolved'
}`,
name, name,
icon: project?.icon_url, icon: project?.icon_url,
projectHref: project ? `/${project.project_type}/${project.slug || project.id}` : undefined, projectHref: project ? `/${project.project_type}/${project.slug || project.id}` : undefined,
downloadHref: dependency.reason || !primaryFile ? undefined : getDownloadUrl(primaryFile.url), downloadHref:
'reason' in dependency || !primaryFile ? undefined : getDownloadUrl(primaryFile.url),
filename: primaryFile?.filename, filename: primaryFile?.filename,
typeLabel: 'Required', typeLabel: 'Required',
unavailableTooltip, unavailableTooltip,
dependencies: ( dependencies: (versionId && dependenciesByParentVersionId.value.get(versionId)
(dependency.version_id && dependenciesByParentVersionId.value.get(dependency.version_id)) || ? dependenciesByParentVersionId.value.get(versionId)!
[] : []
).map((subDependency) => createDependencyRow(subDependency)), ).map((subDependency) => createDependencyRow(subDependency)),
} }
} }
function skippedReasonLabel(reason) { function skippedReasonLabel(reason: Labrinth.Content.v3.SkippedContent['reason']) {
return ( return (
{ {
already_installed: formatMessage(messages.alreadyInstalledDependency), already_installed: formatMessage(messages.alreadyInstalledDependency),
@@ -230,13 +250,13 @@ function skippedReasonLabel(reason) {
) )
} }
function resolveContentType(projectType) { function resolveContentType(projectType: DisplayProjectType): Labrinth.Content.v3.ContentType {
return ['mod', 'plugin', 'datapack', 'resourcepack', 'shader', 'modpack'].includes(projectType) return ['mod', 'plugin', 'datapack', 'resourcepack', 'shader', 'modpack'].includes(projectType)
? projectType ? (projectType as Labrinth.Content.v3.ContentType)
: 'mod' : 'mod'
} }
function getDownloadUrl(url) { function getDownloadUrl(url: string) {
return createProjectDownloadUrl(url, { return createProjectDownloadUrl(url, {
reason: props.downloadReason, reason: props.downloadReason,
gameVersion: props.currentGameVersion ?? undefined, gameVersion: props.currentGameVersion ?? undefined,
@@ -63,7 +63,7 @@
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import { DownloadIcon, PackageIcon } from '@modrinth/assets' import { DownloadIcon, PackageIcon } from '@modrinth/assets'
import { Avatar, ButtonStyled, TagItem } from '@modrinth/ui' import { Avatar, ButtonStyled, TagItem } from '@modrinth/ui'
@@ -71,12 +71,23 @@ defineOptions({
name: 'DownloadDependency', name: 'DownloadDependency',
}) })
defineProps({ interface DownloadDependencyRow {
dependency: { key: string
type: Object, name: string
required: true, icon?: string
}, projectHref?: string
}) downloadHref?: string
filename?: string
typeLabel: string
unavailableTooltip: string
dependencies: DownloadDependencyRow[]
}
const emit = defineEmits(['download']) defineProps<{
dependency: DownloadDependencyRow
}>()
const emit = defineEmits<{
download: []
}>()
</script> </script>
@@ -100,6 +100,7 @@
</div> </div>
<ButtonStyled v-if="selectedPrimaryFile" color="brand" circular> <ButtonStyled v-if="selectedPrimaryFile" color="brand" circular>
<a <a
v-tooltip="'Download'"
:href="selectedPrimaryFileDownloadUrl" :href="selectedPrimaryFileDownloadUrl"
:download="selectedPrimaryFile.filename" :download="selectedPrimaryFile.filename"
:aria-label=" :aria-label="
@@ -107,14 +108,13 @@
version: selectedVersion.version_number, version: selectedVersion.version_number,
}) })
" "
v-tooltip="'Download'"
@click="emit('download')" @click="emit('download')"
> >
<DownloadIcon aria-hidden="true" /> <DownloadIcon aria-hidden="true" />
</a> </a>
</ButtonStyled> </ButtonStyled>
</div> </div>
<p v-else-if="currentPlatform && currentGameVersion && !versionsLoading && versions.length > 0"> <p v-else-if="currentPlatform && currentGameVersion && versions.length > 0">
{{ {{
formatMessage(messages.noVersionsAvailable, { formatMessage(messages.noVersionsAvailable, {
gameVersion: currentGameVersion, gameVersion: currentGameVersion,
@@ -124,18 +124,22 @@
</p> </p>
</template> </template>
<script setup> <script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { DownloadIcon } from '@modrinth/assets' import { DownloadIcon } from '@modrinth/assets'
import { import {
ButtonStyled, ButtonStyled,
type CdnDownloadReason,
Checkbox, Checkbox,
Combobox, Combobox,
type ComboboxOption,
defineMessages, defineMessages,
getTagMessage, getTagMessage,
useDebugLogger, useDebugLogger,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import VersionChannelTag from '@modrinth/ui/src/components/version/VersionChannelTag.vue' import VersionChannelTag from '@modrinth/ui/src/components/version/VersionChannelTag.vue'
import type { DisplayProjectType } from '@modrinth/utils'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
@@ -143,48 +147,49 @@ defineOptions({
name: 'DownloadProject', name: 'DownloadProject',
}) })
const props = defineProps({ type DownloadModalProject = Omit<Labrinth.Projects.v2.Project, 'project_type'> & {
project: { project_type: DisplayProjectType
type: Object, actualProjectType: Labrinth.Projects.v2.ProjectType
required: true, }
},
versions: {
type: Array,
default: () => [],
},
versionsLoading: {
type: Boolean,
default: false,
},
tags: {
type: Object,
required: true,
},
downloadReason: {
type: String,
default: 'standalone',
},
initialGameVersion: {
type: String,
default: null,
},
initialPlatform: {
type: String,
default: null,
},
resetKey: {
type: Number,
default: 0,
},
})
const emit = defineEmits(['download', 'selectGameVersion', 'selectPlatform', 'update:selection']) type ProjectDownloadSelection = {
currentGameVersion: string | null
currentPlatform: string | null
selectedVersion: Labrinth.Versions.v3.Version | null
selectedPrimaryFile: Labrinth.Versions.v3.VersionFile | null
}
const props = withDefaults(
defineProps<{
project: DownloadModalProject
versions?: Labrinth.Versions.v3.Version[]
downloadReason?: CdnDownloadReason
initialGameVersion?: string | null
initialPlatform?: string | null
resetKey?: number
}>(),
{
versions: () => [],
downloadReason: 'standalone',
initialGameVersion: null,
initialPlatform: null,
resetKey: 0,
},
)
const emit = defineEmits<{
download: []
selectGameVersion: [gameVersion: string]
selectPlatform: [platform: string]
'update:selection': [selection: ProjectDownloadSelection]
}>()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const { createProjectDownloadUrl } = useCdnDownloadContext() const { createProjectDownloadUrl } = useCdnDownloadContext()
const debug = useDebugLogger('DownloadProject') const debug = useDebugLogger('DownloadProject')
const tags = useGeneratedState()
const userSelectedGameVersion = ref(props.initialGameVersion) const userSelectedGameVersion = ref<string | null>(props.initialGameVersion)
const userSelectedPlatform = ref(props.initialPlatform) const userSelectedPlatform = ref<string | null>(props.initialPlatform)
const showAllVersions = ref(defaultShowAllVersions()) const showAllVersions = ref(defaultShowAllVersions())
const versionFilter = ref('') const versionFilter = ref('')
@@ -197,39 +202,36 @@ const showAllVersionsModel = computed({
}, },
}) })
const currentGameVersion = computed(() => { const currentGameVersion = computed<string | null>(() => {
return ( if (userSelectedGameVersion.value) return userSelectedGameVersion.value
userSelectedGameVersion.value || return props.project.game_versions.length === 1 ? props.project.game_versions[0] : null
(props.project.game_versions.length === 1 && props.project.game_versions[0])
)
}) })
const possibleGameVersions = computed(() => { const possibleGameVersions = computed<string[]>(() => {
return props.versions return props.versions
.filter((x) => !currentPlatform.value || x.loaders.includes(currentPlatform.value)) .filter((x) => !currentPlatform.value || x.loaders.includes(currentPlatform.value))
.flatMap((x) => x.game_versions) .flatMap((x) => x.game_versions)
}) })
const possiblePlatforms = computed(() => { const possiblePlatforms = computed<string[]>(() => {
return props.versions return props.versions
.filter((x) => !currentGameVersion.value || x.game_versions.includes(currentGameVersion.value)) .filter((x) => !currentGameVersion.value || x.game_versions.includes(currentGameVersion.value))
.flatMap((x) => x.loaders) .flatMap((x) => x.loaders)
}) })
const currentPlatform = computed(() => { const currentPlatform = computed<string | null>(() => {
return ( if (userSelectedPlatform.value) return userSelectedPlatform.value
userSelectedPlatform.value || (props.project.loaders.length === 1 && props.project.loaders[0]) return props.project.loaders.length === 1 ? props.project.loaders[0] : null
)
}) })
const currentPlatformText = computed(() => { const currentPlatformText = computed(() => {
if (!currentPlatform.value) return null if (!currentPlatform.value) return ''
return formatMessage(getTagMessage(currentPlatform.value, 'loader')) return loaderLabel(currentPlatform.value)
}) })
const releaseVersions = computed(() => { const releaseVersions = computed<Set<string>>(() => {
const set = new Set() const set = new Set<string>()
for (const gameVersion of props.tags.gameVersions || []) { for (const gameVersion of tags.value.gameVersions || []) {
if (gameVersion?.version && gameVersion.version_type === 'release') { if (gameVersion?.version && gameVersion.version_type === 'release') {
set.add(gameVersion.version) set.add(gameVersion.version)
} }
@@ -237,9 +239,9 @@ const releaseVersions = computed(() => {
return set return set
}) })
const nonReleaseVersions = computed(() => { const nonReleaseVersions = computed<Set<string>>(() => {
const set = new Set() const set = new Set<string>()
for (const gameVersion of props.tags.gameVersions || []) { for (const gameVersion of tags.value.gameVersions || []) {
if (gameVersion?.version && gameVersion.version_type !== 'release') { if (gameVersion?.version && gameVersion.version_type !== 'release') {
set.add(gameVersion.version) set.add(gameVersion.version)
} }
@@ -275,29 +277,34 @@ const filteredGameVersions = computed(() => {
.reverse() .reverse()
}) })
const gameVersionOptions = computed(() => { const gameVersionOptions = computed<ComboboxOption<string>[]>(() => {
return filteredGameVersions.value.map((gameVersion) => ({ return filteredGameVersions.value.map((gameVersion) => ({
value: gameVersion, value: gameVersion,
label: gameVersion, label: gameVersion,
})) }))
}) })
const platformOptions = computed(() => { const platformOptions = computed<ComboboxOption<string>[]>(() => {
return props.project.loaders return props.project.loaders
.slice() .slice()
.reverse() .reverse()
.map((platform) => ({ .map((platform) => ({
value: platform, value: platform,
label: formatMessage(getTagMessage(platform, 'loader')), label: loaderLabel(platform),
})) }))
}) })
const filteredVersions = computed(() => { const filteredVersions = computed<Labrinth.Versions.v3.Version[]>(() => {
const result = props.versions.filter( const gameVersion = currentGameVersion.value
(x) => if (!gameVersion) return []
x.game_versions?.includes(currentGameVersion.value) &&
(x.loaders?.includes(currentPlatform.value) || props.project.project_type === 'resourcepack'), const platform = currentPlatform.value
) const result = props.versions.filter((x) => {
const matchesPlatform =
props.project.project_type === 'resourcepack' || (!!platform && x.loaders.includes(platform))
return x.game_versions.includes(gameVersion) && matchesPlatform
})
debug('filteredVersions', { debug('filteredVersions', {
total: props.versions.length, total: props.versions.length,
filtered: result.length, filtered: result.length,
@@ -308,11 +315,11 @@ const filteredVersions = computed(() => {
return result return result
}) })
const filteredRelease = computed(() => { const filteredRelease = computed<Labrinth.Versions.v3.Version | undefined>(() => {
return filteredVersions.value.find((x) => x.version_type === 'release') return filteredVersions.value.find((x) => x.version_type === 'release')
}) })
const filteredBeta = computed(() => { const filteredBeta = computed<Labrinth.Versions.v3.Version | undefined>(() => {
return filteredVersions.value.find( return filteredVersions.value.find(
(x) => (x) =>
x.version_type === 'beta' && x.version_type === 'beta' &&
@@ -321,7 +328,7 @@ const filteredBeta = computed(() => {
) )
}) })
const filteredAlpha = computed(() => { const filteredAlpha = computed<Labrinth.Versions.v3.Version | undefined>(() => {
return filteredVersions.value.find( return filteredVersions.value.find(
(x) => (x) =>
x.version_type === 'alpha' && x.version_type === 'alpha' &&
@@ -332,18 +339,20 @@ const filteredAlpha = computed(() => {
) )
}) })
const selectedVersion = computed(() => { const selectedVersion = computed<Labrinth.Versions.v3.Version | null>(() => {
return filteredRelease.value || filteredBeta.value || filteredAlpha.value return filteredRelease.value || filteredBeta.value || filteredAlpha.value || null
}) })
const selectedPrimaryFile = computed(() => { const selectedPrimaryFile = computed<Labrinth.Versions.v3.VersionFile | null>(() => {
return ( return (
selectedVersion.value?.files?.find((file) => file.primary) || selectedVersion.value?.files?.[0] selectedVersion.value?.files?.find((file) => file.primary) ||
selectedVersion.value?.files?.[0] ||
null
) )
}) })
const selectedPrimaryFileDownloadUrl = computed(() => { const selectedPrimaryFileDownloadUrl = computed(() => {
if (!selectedPrimaryFile.value) return null if (!selectedPrimaryFile.value) return '#'
return getDownloadUrl(selectedPrimaryFile.value.url) return getDownloadUrl(selectedPrimaryFile.value.url)
}) })
@@ -370,17 +379,19 @@ watch(
}, },
) )
function selectGameVersion(gameVersion) { function selectGameVersion(gameVersion?: string) {
if (!gameVersion) return
userSelectedGameVersion.value = gameVersion userSelectedGameVersion.value = gameVersion
emit('selectGameVersion', gameVersion) emit('selectGameVersion', gameVersion)
} }
function selectPlatform(platform) { function selectPlatform(platform?: string) {
if (!platform) return
userSelectedPlatform.value = platform userSelectedPlatform.value = platform
emit('selectPlatform', platform) emit('selectPlatform', platform)
} }
function getDownloadUrl(url) { function getDownloadUrl(url: string) {
return createProjectDownloadUrl(url, { return createProjectDownloadUrl(url, {
reason: props.downloadReason, reason: props.downloadReason,
gameVersion: currentGameVersion.value ?? undefined, gameVersion: currentGameVersion.value ?? undefined,
@@ -388,7 +399,11 @@ function getDownloadUrl(url) {
}) })
} }
function isReleaseGameVersion(version) { function loaderLabel(loader: string) {
return formatMessage(getTagMessage(loader, 'loader') ?? messages.unknownLoader)
}
function isReleaseGameVersion(version: string) {
if (releaseVersions.value.has(version)) return true if (releaseVersions.value.has(version)) return true
if (nonReleaseVersions.value.has(version)) return false if (nonReleaseVersions.value.has(version)) return false
return true return true
@@ -398,8 +413,8 @@ function defaultShowAllVersions() {
return ( return (
props.project.game_versions.length > 0 && props.project.game_versions.length > 0 &&
props.project.game_versions.every((projectVersion) => { props.project.game_versions.every((projectVersion) => {
const gameVersion = props.tags.gameVersions?.find((x) => x.version === projectVersion) const gameVersion = tags.value.gameVersions?.find((x) => x.version === projectVersion)
return gameVersion?.version_type && gameVersion.version_type !== 'release' return !!gameVersion?.version_type && gameVersion.version_type !== 'release'
}) })
) )
} }
@@ -441,5 +456,9 @@ const messages = defineMessages({
id: 'project.download.show-all-versions', id: 'project.download.show-all-versions',
defaultMessage: 'Show all versions', defaultMessage: 'Show all versions',
}, },
unknownLoader: {
id: 'project.download.unknown-loader',
defaultMessage: 'Unknown loader',
},
}) })
</script> </script>
@@ -46,9 +46,11 @@
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { ExternalIcon, ModrinthIcon } from '@modrinth/assets' import { ExternalIcon, ModrinthIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '@modrinth/ui' import { defineMessages, useVIntl } from '@modrinth/ui'
import type { DisplayProjectType } from '@modrinth/utils'
import { ref } from 'vue' import { ref } from 'vue'
import Accordion from '~/components/ui/Accordion.vue' import Accordion from '~/components/ui/Accordion.vue'
@@ -57,19 +59,18 @@ defineOptions({
name: 'InstallWithModrinthApp', name: 'InstallWithModrinthApp',
}) })
defineProps({ type DownloadModalProject = Omit<Labrinth.Projects.v2.Project, 'project_type'> & {
project: { project_type: DisplayProjectType
type: Object, actualProjectType: Labrinth.Projects.v2.ProjectType
required: true, }
},
tags: { defineProps<{
type: Object, project: DownloadModalProject
required: true, }>()
},
})
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const getModrinthAppAccordion = ref() const tags = useGeneratedState()
const getModrinthAppAccordion = ref<InstanceType<typeof Accordion> | null>(null)
const messages = defineMessages({ const messages = defineMessages({
dontHaveModrinthApp: { dontHaveModrinthApp: {
@@ -1,19 +1,17 @@
<template> <template>
<NewModal ref="modal" :on-show="onShow" :on-hide="onHide" width="544px"> <NewModal ref="modal" :on-show="onShow" :on-hide="onHide" width="544px">
<template #title> <template v-if="project" #title>
<Avatar :src="project.icon_url" :alt="project.title" class="icon" size="32px" /> <Avatar :src="project.icon_url" :alt="project.title" class="icon" size="32px" />
<div class="truncate text-lg font-extrabold text-contrast"> <div class="truncate text-lg font-extrabold text-contrast">
{{ formatMessage(messages.downloadTitle, { title: project.title }) }} {{ formatMessage(messages.downloadTitle, { title: project.title }) }}
</div> </div>
</template> </template>
<template #default> <template #default>
<div class="mx-auto flex w-full flex-col gap-4"> <div v-if="project" class="mx-auto flex w-full flex-col gap-4">
<InstallWithModrinthApp :project="project" :tags="tags" /> <InstallWithModrinthApp :project="project" />
<DownloadProject <DownloadProject
:project="project" :project="project"
:versions="versions" :versions="versions"
:versions-loading="versionsLoading"
:tags="tags"
:download-reason="downloadReason" :download-reason="downloadReason"
:initial-game-version="initialGameVersion" :initial-game-version="initialGameVersion"
:initial-platform="initialPlatform" :initial-platform="initialPlatform"
@@ -76,77 +74,145 @@
</NewModal> </NewModal>
</template> </template>
<script setup> <script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { DownloadIcon, FileIcon } from '@modrinth/assets' import { DownloadIcon, FileIcon } from '@modrinth/assets'
import { import {
Avatar, Avatar,
type CdnDownloadReason,
defineMessages, defineMessages,
injectModrinthClient,
NewModal, NewModal,
ServersPromo, ServersPromo,
useDebugLogger, useDebugLogger,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import type { DisplayProjectType } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { navigateTo } from '#app' import { navigateTo } from '#app'
import { saveFeatureFlags } from '~/composables/featureFlags.ts' import { saveFeatureFlags } from '~/composables/featureFlags.ts'
import { STALE_TIME, STALE_TIME_LONG } from '~/composables/queries/project'
import DownloadDependencies from './DownloadDependencies.vue' import DownloadDependencies from './DownloadDependencies.vue'
import DownloadProject from './DownloadProject.vue' import DownloadProject from './DownloadProject.vue'
import InstallWithModrinthApp from './InstallWithModrinthApp.vue' import InstallWithModrinthApp from './InstallWithModrinthApp.vue'
const props = defineProps({ type DownloadModalProject = Omit<Labrinth.Projects.v2.Project, 'project_type'> & {
project: { project_type: DisplayProjectType
type: Object, actualProjectType: Labrinth.Projects.v2.ProjectType
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']) type ProjectDownloadSelection = {
currentGameVersion: string | null
currentPlatform: string | null
selectedVersion: Labrinth.Versions.v3.Version | null
selectedPrimaryFile: Labrinth.Versions.v3.VersionFile | null
}
type NewModalRef = {
show: (event?: MouseEvent) => void
hide: () => void
}
const props = withDefaults(
defineProps<{
projectId: string
downloadReason?: CdnDownloadReason
}>(),
{
downloadReason: 'standalone',
},
)
const emit = defineEmits<{
download: []
}>()
const route = useRoute() const route = useRoute()
const flags = useFeatureFlags() const flags = useFeatureFlags()
const tags = useGeneratedState()
const client = injectModrinthClient()
const { createProjectDownloadUrl } = useCdnDownloadContext() const { createProjectDownloadUrl } = useCdnDownloadContext()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const debug = useDebugLogger('DownloadModal') const debug = useDebugLogger('DownloadModal')
const modal = ref() const modal = ref<NewModalRef | null>(null)
const modalOpen = ref(false) const modalOpen = ref(false)
const downloadProjectResetKey = ref(0) const downloadProjectResetKey = ref(0)
const projectDownloadSelection = ref({ const projectDownloadSelection = ref<ProjectDownloadSelection>({
currentGameVersion: null, currentGameVersion: null,
currentPlatform: null, currentPlatform: null,
selectedVersion: null, selectedVersion: null,
selectedPrimaryFile: null, selectedPrimaryFile: null,
}) })
const { version, loader } = route.query const routeProjectId = computed(() => props.projectId)
const initialGameVersion = ref(
typeof version === 'string' && props.project.game_versions.includes(version) ? version : null, const { data: projectRaw, error: projectV2Error } = useQuery({
) queryKey: computed(() => ['project', 'v2', routeProjectId.value]),
const initialPlatform = ref( queryFn: () => client.labrinth.projects_v2.get(routeProjectId.value),
typeof loader === 'string' && props.project.loaders.includes(loader) ? loader : null, staleTime: STALE_TIME,
) })
const resolvedProjectId = computed(() => projectRaw.value?.id)
const project = computed<DownloadModalProject | null>(() => {
if (!projectRaw.value) return null
return {
...projectRaw.value,
actualProjectType: projectRaw.value.project_type,
project_type: getProjectTypeForUrl(projectRaw.value.project_type, projectRaw.value.loaders),
}
})
const versionsEnabled = ref(false)
const {
data: versionsV3,
error: _versionsV3Error,
isFetching: versionsV3Loading,
} = useQuery({
queryKey: computed(() => ['project', resolvedProjectId.value, 'versions', 'v3']),
queryFn: () =>
client.labrinth.versions_v3.getProjectVersions(resolvedProjectId.value!, {
include_changelog: false,
apiVersion: 3,
}),
staleTime: STALE_TIME_LONG,
enabled: computed(() => !!resolvedProjectId.value && versionsEnabled.value),
})
const versions = computed<Labrinth.Versions.v3.Version[]>(() => {
const isModpack =
project.value?.actualProjectType === 'modpack' || project.value?.project_type === 'modpack'
return (versionsV3.value ?? []).map((version) => {
const files = Array.isArray(version.files) ? version.files : []
const gameVersions = Array.isArray(version.game_versions) ? version.game_versions : []
const loaders = Array.isArray(version.loaders) ? version.loaders : []
const mrpackLoaders = Array.isArray(version.mrpack_loaders) ? version.mrpack_loaders : []
return {
...version,
files,
game_versions: gameVersions,
loaders: isModpack && mrpackLoaders.length ? mrpackLoaders : loaders,
}
})
})
const initialGameVersion = computed(() => {
const version = route.query.version
if (typeof version !== 'string' || !project.value?.game_versions.includes(version)) return null
return version
})
const initialPlatform = computed(() => {
const loader = route.query.loader
if (typeof loader !== 'string' || !project.value?.loaders.includes(loader)) return null
return loader
})
const currentGameVersion = computed(() => projectDownloadSelection.value.currentGameVersion) const currentGameVersion = computed(() => projectDownloadSelection.value.currentGameVersion)
const currentPlatform = computed(() => projectDownloadSelection.value.currentPlatform) const currentPlatform = computed(() => projectDownloadSelection.value.currentPlatform)
@@ -158,6 +224,12 @@ const additionalFiles = computed(() => {
return selectedVersion.value.files.filter((file) => file !== selectedPrimaryFile.value) return selectedVersion.value.files.filter((file) => file !== selectedPrimaryFile.value)
}) })
watch(projectV2Error, (error) => {
if (error) {
debug('project query failed', error)
}
})
const messages = defineMessages({ const messages = defineMessages({
downloadTitle: { downloadTitle: {
id: 'project.download.title', id: 'project.download.title',
@@ -169,17 +241,36 @@ const messages = defineMessages({
}, },
}) })
function fileTypeLabel(type) { const fileTypeLabels: Partial<Record<Labrinth.Versions.v3.FileType, string>> = {
return ( 'required-resource-pack': 'Resourcepack',
{ 'optional-resource-pack': 'Resourcepack',
'required-resource-pack': 'Resourcepack', unknown: 'File',
'optional-resource-pack': 'Resourcepack',
unknown: 'File',
}[type] || 'File'
)
} }
function getDownloadUrl(url) { function fileTypeLabel(type?: Labrinth.Versions.v3.FileType) {
return fileTypeLabels[type ?? 'unknown'] || 'File'
}
function getProjectTypeForUrl(
type: Labrinth.Projects.v2.ProjectType,
loaders: string[],
): DisplayProjectType {
if (type !== 'mod') return type as DisplayProjectType
const isMod = loaders.some((loader) => tags.value.loaderData.modLoaders.includes(loader))
const isPlugin = loaders.some((loader) => tags.value.loaderData.allPluginLoaders.includes(loader))
const isDataPack = loaders.some((loader) =>
tags.value.loaderData.dataPackLoaders.includes(loader),
)
if (isDataPack) return 'datapack'
if (isPlugin) return 'plugin'
if (isMod) return 'mod'
return 'mod'
}
function getDownloadUrl(url: string) {
return createProjectDownloadUrl(url, { return createProjectDownloadUrl(url, {
reason: props.downloadReason, reason: props.downloadReason,
gameVersion: currentGameVersion.value ?? undefined, gameVersion: currentGameVersion.value ?? undefined,
@@ -187,7 +278,13 @@ function getDownloadUrl(url) {
}) })
} }
function updateDownloadQuery({ gameVersion, platform }) { function updateDownloadQuery({
gameVersion,
platform,
}: {
gameVersion: string | null
platform: string | null
}) {
navigateTo( navigateTo(
{ {
query: { query: {
@@ -205,14 +302,14 @@ function updateDownloadQuery({ gameVersion, platform }) {
) )
} }
function selectGameVersion(gameVersion) { function selectGameVersion(gameVersion: string) {
updateDownloadQuery({ updateDownloadQuery({
gameVersion, gameVersion,
platform: currentPlatform.value, platform: currentPlatform.value,
}) })
} }
function selectPlatform(platform) { function selectPlatform(platform: string) {
updateDownloadQuery({ updateDownloadQuery({
gameVersion: currentGameVersion.value, gameVersion: currentGameVersion.value,
platform, platform,
@@ -222,7 +319,7 @@ function selectPlatform(platform) {
function onShow() { function onShow() {
modalOpen.value = true modalOpen.value = true
debug('on-show fired') debug('on-show fired')
props.loadVersions() versionsEnabled.value = true
navigateTo({ query: route.query, hash: '#download' }, { replace: true }) navigateTo({ query: route.query, hash: '#download' }, { replace: true })
} }
@@ -231,15 +328,15 @@ function onHide() {
navigateTo({ query: route.query, hash: '' }, { replace: true }) navigateTo({ query: route.query, hash: '' }, { replace: true })
} }
function show(event) { function show(event?: MouseEvent) {
if (!modal.value || modalOpen.value) return if (!modal.value || modalOpen.value) return
modalOpen.value = true modalOpen.value = true
modal.value.show(event) modal.value.show(event)
} }
function hide(event) { function hide() {
if (!modal.value || !modalOpen.value) return if (!modal.value || !modalOpen.value) return
modal.value?.hide(event) modal.value?.hide()
downloadProjectResetKey.value += 1 downloadProjectResetKey.value += 1
} }
@@ -254,9 +351,18 @@ function openFromHash() {
show() show()
} }
if (route.hash === '#download' || version !== undefined || loader !== undefined) { if (
debug('eager loadVersions from setup', { hash: route.hash, version, loader }) route.hash === '#download' ||
props.loadVersions() route.query.version !== undefined ||
route.query.loader !== undefined
) {
debug('eager loadVersions from setup', {
hash: route.hash,
version: route.query.version,
loader: route.query.loader,
loading: versionsV3Loading.value,
})
versionsEnabled.value = true
} }
watch(modal, openFromHash) watch(modal, openFromHash)
+4 -8
View File
@@ -90,12 +90,8 @@
</div> </div>
<ProjectDownloadModal <ProjectDownloadModal
ref="downloadModal" ref="downloadModal"
:project="project" :project-id="routeProjectId"
:versions="versions"
:versions-loading="versionsLoading"
:tags="tags"
:download-reason="downloadReason" :download-reason="downloadReason"
:load-versions="loadVersions"
@download="triggerDownloadAnimation" @download="triggerDownloadAnimation"
/> />
<CollectionCreateModal ref="modal_collection" :project-ids="[project.id]" /> <CollectionCreateModal ref="modal_collection" :project-ids="[project.id]" />
@@ -2240,8 +2236,8 @@ provideProjectPageContext({
display: none; display: none;
} }
.over-the-top-download-animation { .over-the-top-download-animation {
position: fixed; position: fixed;
z-index: 100; z-index: 100;
inset: 0; inset: 0;
display: flex; display: flex;
@@ -2290,7 +2286,7 @@ provideProjectPageContext({
} }
} }
.servers-popup { .servers-popup {
box-shadow: box-shadow:
0 0 12px 1px rgba(0, 175, 92, 0.6), 0 0 12px 1px rgba(0, 175, 92, 0.6),
var(--shadow-floating); var(--shadow-floating);