mirror of
https://github.com/modrinth/code.git
synced 2026-08-30 11:36:05 +00:00
feat: dependency download button
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<a
|
||||
class="modrinth-app-install-card group flex items-center justify-between gap-3 rounded-2xl border border-solid border-brand-highlight bg-surface-1 px-4 py-3 text-primary no-underline transition-[filter] hover:brightness-110"
|
||||
class="modrinth-app-install-card flex items-center justify-between gap-3 rounded-2xl border border-solid border-brand-highlight bg-surface-1 px-4 py-3 text-primary no-underline transition-[filter] hover:brightness-110"
|
||||
:href="`modrinth://mod/${project.slug}`"
|
||||
@click="installWithApp"
|
||||
>
|
||||
@@ -30,7 +30,7 @@
|
||||
</span>
|
||||
<ExternalIcon
|
||||
aria-hidden="true"
|
||||
class="size-4 flex-shrink-0 text-contrast transition-colors group-hover:text-brand"
|
||||
class="size-4 flex-shrink-0 text-contrast transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<span class="truncate text-base text-secondary">
|
||||
@@ -83,13 +83,12 @@
|
||||
"
|
||||
class="flex w-full items-center justify-between gap-2"
|
||||
:class="{
|
||||
'text-brand-red': !possibleGameVersions.includes(item.value),
|
||||
'text-brand-red opacity-40': !possibleGameVersions.includes(item.value),
|
||||
'text-green': isSelected,
|
||||
'text-primary': possibleGameVersions.includes(item.value) && !isSelected,
|
||||
}"
|
||||
>
|
||||
<span class="min-w-0 truncate font-semibold leading-tight">{{ item.label }}</span>
|
||||
<CheckIcon v-if="isSelected" aria-hidden="true" class="size-5 flex-shrink-0" />
|
||||
</div>
|
||||
</template>
|
||||
<template #dropdown-footer>
|
||||
@@ -128,13 +127,12 @@
|
||||
"
|
||||
class="flex w-full items-center justify-between gap-2"
|
||||
:class="{
|
||||
'text-brand-red': !possiblePlatforms.includes(item.value),
|
||||
'text-brand-red opacity-40': !possiblePlatforms.includes(item.value),
|
||||
'text-green': isSelected,
|
||||
'text-primary': possiblePlatforms.includes(item.value) && !isSelected,
|
||||
}"
|
||||
>
|
||||
<span class="min-w-0 truncate font-semibold leading-tight">{{ item.label }}</span>
|
||||
<CheckIcon v-if="isSelected" aria-hidden="true" class="size-5 flex-shrink-0" />
|
||||
</div>
|
||||
</template>
|
||||
</Combobox>
|
||||
@@ -157,13 +155,14 @@
|
||||
</div>
|
||||
<ButtonStyled v-if="selectedPrimaryFile" color="brand" circular>
|
||||
<a
|
||||
:href="decorateModalDownloadUrl(selectedPrimaryFile.url)"
|
||||
:href="getDownloadUrl(selectedPrimaryFile.url)"
|
||||
:download="selectedPrimaryFile.filename"
|
||||
:aria-label="
|
||||
formatMessage(messages.downloadVersion, {
|
||||
version: selectedVersion.version_number,
|
||||
})
|
||||
"
|
||||
v-tooltip="'Download'"
|
||||
@click="onDownload"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
@@ -196,12 +195,8 @@
|
||||
:key="dependency.key"
|
||||
class="flex flex-col gap-1.5"
|
||||
>
|
||||
<component
|
||||
:is="dependency.href ? 'a' : 'div'"
|
||||
:href="dependency.href"
|
||||
:download="dependency.filename"
|
||||
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 no-underline"
|
||||
@click="onDependencyRowClick(dependency, $event)"
|
||||
<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
|
||||
@@ -215,27 +210,42 @@
|
||||
aria-hidden="true"
|
||||
class="size-5 flex-shrink-0 text-secondary"
|
||||
/>
|
||||
<span class="min-w-0 truncate font-semibold text-contrast">
|
||||
<a
|
||||
v-if="dependency.projectHref"
|
||||
:href="dependency.projectHref"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="min-w-0 truncate font-semibold text-contrast no-underline hover:underline"
|
||||
>
|
||||
{{ dependency.name }}
|
||||
</a>
|
||||
<span v-else class="min-w-0 truncate font-semibold text-contrast">
|
||||
{{ dependency.name }}
|
||||
</span>
|
||||
<TagItem class="shrink-0 border !border-solid border-surface-5">
|
||||
{{ dependency.typeLabel }}
|
||||
</TagItem>
|
||||
</span>
|
||||
<DownloadIcon aria-hidden="true" class="size-5 text-secondary" />
|
||||
</component>
|
||||
<ButtonStyled v-if="dependency.downloadHref" circular type="transparent">
|
||||
<a
|
||||
v-tooltip="'Download'"
|
||||
:href="dependency.downloadHref"
|
||||
:download="dependency.filename"
|
||||
:aria-label="`Download ${dependency.name}`"
|
||||
@click="onDownload"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" class="size-5 text-secondary" />
|
||||
</a>
|
||||
</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"
|
||||
>
|
||||
<RightArrowIcon aria-hidden="true" class="size-4 text-secondary" />
|
||||
<component
|
||||
:is="subDependency.href ? 'a' : 'div'"
|
||||
:href="subDependency.href"
|
||||
:download="subDependency.filename"
|
||||
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 no-underline"
|
||||
@click="onDependencyRowClick(subDependency, $event)"
|
||||
<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
|
||||
@@ -249,15 +259,34 @@
|
||||
aria-hidden="true"
|
||||
class="size-5 flex-shrink-0 text-secondary"
|
||||
/>
|
||||
<span class="min-w-0 truncate font-semibold text-contrast">
|
||||
<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>
|
||||
<DownloadIcon aria-hidden="true" class="size-5 text-secondary" />
|
||||
</component>
|
||||
<ButtonStyled v-if="subDependency.downloadHref" circular type="transparent">
|
||||
<a
|
||||
v-tooltip="'Download'"
|
||||
:href="subDependency.downloadHref"
|
||||
:download="subDependency.filename"
|
||||
:aria-label="`Download ${subDependency.name}`"
|
||||
@click="onDownload"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" class="size-5 text-secondary" />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -270,7 +299,7 @@
|
||||
<a
|
||||
v-for="file in additionalFiles"
|
||||
:key="file.hashes?.sha1 ?? file.filename"
|
||||
:href="decorateModalDownloadUrl(file.url)"
|
||||
:href="getDownloadUrl(file.url)"
|
||||
:download="file.filename"
|
||||
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 no-underline"
|
||||
@click="onDownload"
|
||||
@@ -553,10 +582,51 @@ const { data: dependencyVersions } = useQuery({
|
||||
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 || []) {
|
||||
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)
|
||||
}
|
||||
@@ -575,17 +645,74 @@ const { data: subDependencyVersions } = useQuery({
|
||||
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()
|
||||
|
||||
@@ -597,7 +724,11 @@ const selectedDependencyProjectIds = computed(() => {
|
||||
if (version?.project_id) ids.add(version.project_id)
|
||||
}
|
||||
|
||||
for (const version of dependencyVersions.value || []) {
|
||||
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
|
||||
@@ -607,6 +738,13 @@ const selectedDependencyProjectIds = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
for (const version of [
|
||||
...(fallbackDependencyVersions.value || []),
|
||||
...(fallbackSubDependencyVersions.value || []),
|
||||
]) {
|
||||
if (version?.project_id) ids.add(version.project_id)
|
||||
}
|
||||
|
||||
return [...ids]
|
||||
})
|
||||
|
||||
@@ -633,6 +771,10 @@ const dependencyRows = computed(() => {
|
||||
|
||||
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))
|
||||
})
|
||||
|
||||
@@ -722,6 +864,15 @@ function dependencyTypeLabel(type) {
|
||||
)
|
||||
}
|
||||
|
||||
function dependencyTypeSortOrder(type) {
|
||||
return (
|
||||
{
|
||||
required: 0,
|
||||
optional: 1,
|
||||
}[type] ?? 2
|
||||
)
|
||||
}
|
||||
|
||||
function fileTypeLabel(type) {
|
||||
return (
|
||||
{
|
||||
@@ -737,9 +888,16 @@ function primaryFileForVersion(version) {
|
||||
}
|
||||
|
||||
function createDependencyRow(dependency, includeSubDependencies = true) {
|
||||
const version = dependency.version_id
|
||||
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)
|
||||
@@ -756,31 +914,26 @@ function createDependencyRow(dependency, includeSubDependencies = true) {
|
||||
key: `${dependency.dependency_type}-${dependency.version_id ?? dependency.project_id ?? dependency.file_name ?? name}`,
|
||||
name,
|
||||
icon: project?.icon_url,
|
||||
href: primaryFile
|
||||
? decorateModalDownloadUrl(primaryFile.url)
|
||||
: project
|
||||
? `/${project.project_type}/${project.slug || project.id}`
|
||||
: undefined,
|
||||
projectHref: project ? `/${project.project_type}/${project.slug || project.id}` : undefined,
|
||||
downloadHref: primaryFile ? getDownloadUrl(primaryFile.url) : undefined,
|
||||
filename: primaryFile?.filename,
|
||||
downloadsFile: !!primaryFile,
|
||||
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 onDependencyRowClick(row, event) {
|
||||
if (row.downloadsFile) {
|
||||
onDownload(event)
|
||||
}
|
||||
}
|
||||
|
||||
function decorateModalDownloadUrl(url) {
|
||||
function getDownloadUrl(url) {
|
||||
return createProjectDownloadUrl(url, {
|
||||
reason: props.downloadReason,
|
||||
gameVersion: currentGameVersion.value ?? undefined,
|
||||
@@ -854,11 +1007,8 @@ function hide(event) {
|
||||
showAllVersions.value = false
|
||||
}
|
||||
|
||||
function onDownload(event) {
|
||||
function onDownload() {
|
||||
emit('download')
|
||||
setTimeout(() => {
|
||||
hide(event)
|
||||
}, 400)
|
||||
}
|
||||
|
||||
function onVersionNavigate(url) {
|
||||
|
||||
Reference in New Issue
Block a user