bunch more fixes lol

This commit is contained in:
Prospector
2026-06-24 00:20:02 -07:00
parent 14c728c8e3
commit 623e01dbff
10 changed files with 315 additions and 97 deletions
@@ -1,7 +1,12 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { DownloadIcon, FileIcon, SearchIcon } from '@modrinth/assets'
import { capitalizeString, renderHighlightedString } from '@modrinth/utils'
import { DownloadIcon, ExternalIcon, FileIcon, SearchIcon } from '@modrinth/assets'
import {
capitalizeString,
formatVersionsForDisplay,
type GameVersionTag,
renderHighlightedString,
} from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
@@ -11,6 +16,7 @@ import { useCompactNumber, useFormatNumber } from '#ui/composables/format-number
import { useRelativeTime } from '#ui/composables/how-ago.ts'
import { defineMessage, defineMessages, useVIntl } from '#ui/composables/i18n.ts'
import { injectModrinthClient } from '#ui/providers/api-client.ts'
import { injectTags } from '#ui/providers/tags.ts'
import {
commonMessages,
fileTypeMessages,
@@ -41,12 +47,23 @@ const { formatCompactNumber } = useCompactNumber()
const props = defineProps<{
version: Labrinth.Versions.v3.Version
enrichment?: Labrinth.Projects.v2.DependencyInfo
enrichmentLoading?: boolean
dependencyLinkCreator: (context: DependencyContext) => string | undefined
members?: Labrinth.Projects.v3.TeamMember[]
userLinkCreator?: (user: Labrinth.Users.v3.User) => string | undefined
}>()
const api = injectModrinthClient()
const tags = injectTags(null)
const gameVersionsToDisplay = computed(() =>
tags?.gameVersions.value?.length
? formatVersionsForDisplay(
props.version.game_versions,
tags.gameVersions.value as GameVersionTag[],
)
: props.version.game_versions,
)
const versionNumber = computed(() => props.version.version_number)
const versionSubtitle = computed(() => props.version.name)
@@ -101,22 +118,82 @@ const optionalContent = computed(() =>
const incompatibleContent = computed(() =>
dependencies.value.filter((dep) => dep.dependency.dependency_type === 'incompatible'),
)
const includedContent = computed(() =>
dependencies.value
.filter((context) => context.dependency.dependency_type === 'embedded')
.map((context) => ({
icon_url: context.project
? context.project.icon_url
: context.dependency.attribution?.icon_url,
name: context.project ? context.project.title : context.dependency.file_name,
version: context.version ? context.version.version_number : undefined,
link: context.project
? props.dependencyLinkCreator(context)
: context.dependency.attribution?.link,
hasProject: !!context.project,
})),
const embeddedDependencies = computed(() =>
dependencies.value.filter((context) => context.dependency.dependency_type === 'embedded'),
)
const includedContentLoading = computed(
() =>
!!props.enrichmentLoading &&
embeddedDependencies.value.some((context) => context.dependency.project_id && !context.project),
)
function getEmbeddedDependencyId(context: DependencyContext) {
return (
context.dependency.project_id ??
context.dependency.file_name ??
context.dependency.version_id ??
'included'
)
}
type IncludedContentLoadingRow = { loading: true; id: string }
type IncludedContentRow = {
id: string
icon_url?: string
name?: string
version?: string
link?: string
hasProject: boolean
}
function isIncludedContentLoadingRow(
row: IncludedContentLoadingRow | IncludedContentRow,
): row is IncludedContentLoadingRow {
return 'loading' in row
}
const includedContent = computed((): (IncludedContentLoadingRow | IncludedContentRow)[] => {
if (includedContentLoading.value) {
return embeddedDependencies.value.map((context) => ({
loading: true as const,
id: getEmbeddedDependencyId(context),
}))
}
return embeddedDependencies.value
.map((context) => {
const resolution = context.dependency.attribution?.resolution
return {
id: getEmbeddedDependencyId(context),
icon_url: context.project
? context.project.icon_url
: context.dependency.attribution?.flame_project?.icon_url,
name: context.project ? context.project.title : context.dependency.file_name,
version: context.version ? context.version.version_number : undefined,
link: context.project
? props.dependencyLinkCreator(context)
: resolution && 'link_to_work' in resolution
? resolution.link_to_work
: undefined,
hasProject: !!context.project,
}
})
.sort((a, b) => {
const priority = (row: typeof a) => {
if (row.hasProject) return 0
if (row.link) return 1
return 2
}
const priorityDiff = priority(a) - priority(b)
if (priorityDiff !== 0) {
return priorityDiff
}
const sortName = (name: string | undefined) => (name ?? '').replace(/[^a-zA-Z0-9]/g, '')
return sortName(a.name).localeCompare(sortName(b.name))
})
})
const contentTableColumns = computed(() => {
const cols = [
{
@@ -128,7 +205,11 @@ const contentTableColumns = computed(() => {
label: formatMessage(defineMessage({ id: 'version.content.name', defaultMessage: 'Name' })),
},
]
if (includedContent.value.some((x) => x.version)) {
if (
includedContentLoading.value
? embeddedDependencies.value.some((context) => context.dependency.version_id)
: includedContent.value.some((row) => 'version' in row && row.version)
) {
cols.push({
key: 'version',
label: formatMessage(
@@ -318,7 +399,7 @@ const authorLink = computed(() =>
{{ formatMessage(projectCompatibilityMessages.minecraftJava) }}
<div class="flex gap-1 flex-wrap mt-2">
<TagItem
v-for="gameVersion in version.game_versions"
v-for="gameVersion in gameVersionsToDisplay"
:key="`version-compat-game-version-${gameVersion}`"
>{{ gameVersion }}</TagItem
>
@@ -414,7 +495,7 @@ const authorLink = computed(() =>
/>
</div>
</section>
<section v-if="includedContent?.length > 0" id="content">
<section v-if="embeddedDependencies.length > 0" id="content">
<h3 class="mt-0 mb-2 text-lg font-semibold">
{{
formatMessage(
@@ -422,36 +503,60 @@ const authorLink = computed(() =>
)
}}
</h3>
<Table :columns="contentTableColumns" :data="includedContent">
<Table :columns="contentTableColumns" :data="includedContent" row-key="id">
<template #header-actions>
<StyledInput
v-model="contentSearchQuery"
:icon="SearchIcon"
:placeholder="formatMessage(messages.searchContent)"
:disabled="includedContentLoading"
clearable
wrapper-class="w-full sm:w-64"
/>
</template>
<template #cell-icon="{ row }">
<AutoLink :to="row.link" tabindex="-1" class="flex" target="_blank">
<Avatar v-if="row.icon_url" :src="row.icon_url" alt="" size="2rem" />
<div
v-if="isIncludedContentLoadingRow(row)"
class="size-[2rem] shrink-0 rounded-[16%] bg-surface-3 animate-pulse"
/>
<AutoLink v-else :to="row.link" tabindex="-1" class="flex" target="_blank">
<Avatar v-if="row.hasProject || row.icon_url" :src="row.icon_url" alt="" size="2rem" />
<div v-else class="size-[2rem] flex items-center justify-center">
<FileIcon class="size-5 text-secondary" />
</div>
</AutoLink>
</template>
<template #cell-name="{ row }">
<div
v-if="isIncludedContentLoadingRow(row)"
class="h-4 max-w-[12rem] rounded-lg bg-surface-3 animate-pulse"
/>
<AutoLink
v-else
:to="row.link"
class="flex w-fit"
link-class="hover:underline hover:text-contrast"
target="_blank"
>
{{ row.name }}
<ExternalIcon v-if="row.link?.startsWith('http')" class="shrink-0 ml-2" />
</AutoLink>
</template>
<template #cell-version="{ row }">
{{ (row.version ?? row.hasProject) ? formatMessage(commonMessages.unknownLabel) : '—' }}
<div
v-if="isIncludedContentLoadingRow(row)"
class="h-4 w-16 rounded-lg bg-surface-3 animate-pulse"
/>
<AutoLink
v-else
:to="row.version ? row.link : undefined"
class="flex w-fit"
tabindex="-1"
link-class="hover:underline hover:text-contrast"
target="_blank"
>
{{ row.version ?? '—' }}
</AutoLink>
</template>
</Table>
</section>