feat: add download metadata to website (#6034)
* feat: add download metadata to website * add to project cards
This commit is contained in:
140
apps/frontend/src/composables/useCdnDownloadContext.ts
Normal file
140
apps/frontend/src/composables/useCdnDownloadContext.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import type { FilterValue } from '@modrinth/ui'
|
||||
import { LOADER_FILTER_TYPES } from '@modrinth/ui'
|
||||
|
||||
const TEN_MINUTES = 600
|
||||
|
||||
export type DownloadContext = {
|
||||
gameVersion?: string
|
||||
loader?: string
|
||||
reason?: 'standalone' | 'dependency' | 'modpack' | 'update'
|
||||
}
|
||||
|
||||
export type FilterSelection = {
|
||||
gameVersion?: string
|
||||
loader?: string
|
||||
}
|
||||
|
||||
const cookieDefaults = {
|
||||
maxAge: TEN_MINUTES,
|
||||
sameSite: 'lax' as const,
|
||||
secure: true,
|
||||
path: '/',
|
||||
httpOnly: false,
|
||||
}
|
||||
|
||||
function readCookieValue(value: string | null | undefined): string | undefined {
|
||||
if (typeof value !== 'string' || !value) {
|
||||
return undefined
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function newFilterSelection(
|
||||
gameVersion: string | undefined,
|
||||
loader: string | undefined,
|
||||
): FilterSelection | null {
|
||||
if (!gameVersion && !loader) {
|
||||
return null
|
||||
} else if (!gameVersion) {
|
||||
return {
|
||||
loader,
|
||||
}
|
||||
} else if (!loader) {
|
||||
return {
|
||||
gameVersion,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
gameVersion,
|
||||
loader,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useCdnDownloadContext() {
|
||||
const filterGameVersionCookie = useCookie<string | null>('mr_download_filter_game_version', {
|
||||
...cookieDefaults,
|
||||
default: () => null,
|
||||
})
|
||||
|
||||
const filterLoaderCookie = useCookie<string | null>('mr_download_filter_loader', {
|
||||
...cookieDefaults,
|
||||
default: () => null,
|
||||
})
|
||||
|
||||
function createProjectDownloadUrl(originalUrl: string, context?: DownloadContext): string {
|
||||
if (!originalUrl.startsWith('https://cdn.modrinth.com')) {
|
||||
return originalUrl
|
||||
}
|
||||
|
||||
const reason = context?.reason
|
||||
const gameVersion = context?.gameVersion ?? readCookieValue(filterGameVersionCookie.value)
|
||||
const loader = context?.loader ?? readCookieValue(filterLoaderCookie.value)
|
||||
|
||||
try {
|
||||
const url = new URL(originalUrl)
|
||||
|
||||
if (reason) {
|
||||
url.searchParams.set('mr_download_reason', reason)
|
||||
}
|
||||
if (gameVersion) {
|
||||
url.searchParams.set('mr_game_version', gameVersion)
|
||||
} else {
|
||||
url.searchParams.delete('mr_game_version')
|
||||
}
|
||||
|
||||
if (loader) {
|
||||
url.searchParams.set('mr_loader', loader)
|
||||
} else {
|
||||
url.searchParams.delete('mr_loader')
|
||||
}
|
||||
return url.toString()
|
||||
} catch {
|
||||
return originalUrl
|
||||
}
|
||||
}
|
||||
|
||||
function persistFilterSelection(selection: FilterSelection | null) {
|
||||
if (!selection) {
|
||||
filterGameVersionCookie.value = null
|
||||
filterLoaderCookie.value = null
|
||||
return
|
||||
}
|
||||
filterGameVersionCookie.value = selection.gameVersion ?? null
|
||||
filterLoaderCookie.value = selection.loader ?? null
|
||||
}
|
||||
|
||||
function updateDiscoverFilterContext(filters: FilterValue[]) {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
const versionFilters = [
|
||||
...new Set(filters.filter((f) => f.type === 'game_version').map((f) => f.option)),
|
||||
]
|
||||
const loaderFilters = [
|
||||
...new Set(
|
||||
filters
|
||||
.filter((f) => (LOADER_FILTER_TYPES as readonly string[]).includes(f.type))
|
||||
.map((f) => f.option),
|
||||
),
|
||||
]
|
||||
const gameVersion = versionFilters.length === 1 ? versionFilters[0] : undefined
|
||||
const loader = loaderFilters.length === 1 ? loaderFilters[0] : undefined
|
||||
persistFilterSelection(newFilterSelection(gameVersion, loader))
|
||||
}
|
||||
|
||||
function updateVersionsFilterContext(gameVersions: string[], loaders: string[]) {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
const gameVersion = gameVersions.length === 1 ? gameVersions[0] : undefined
|
||||
const loader = loaders.length === 1 ? loaders[0] : undefined
|
||||
persistFilterSelection(newFilterSelection(gameVersion, loader))
|
||||
}
|
||||
|
||||
return {
|
||||
createProjectDownloadUrl,
|
||||
updateDiscoverFilterContext,
|
||||
updateVersionsFilterContext,
|
||||
}
|
||||
}
|
||||
@@ -370,18 +370,21 @@
|
||||
<VersionSummary
|
||||
v-if="filteredRelease"
|
||||
:version="filteredRelease"
|
||||
:decorate-download-url="decorateModalDownloadUrl"
|
||||
@on-download="onDownload"
|
||||
@on-navigate="onVersionNavigate"
|
||||
/>
|
||||
<VersionSummary
|
||||
v-if="filteredBeta"
|
||||
:version="filteredBeta"
|
||||
:decorate-download-url="decorateModalDownloadUrl"
|
||||
@on-download="onDownload"
|
||||
@on-navigate="onVersionNavigate"
|
||||
/>
|
||||
<VersionSummary
|
||||
v-if="filteredAlpha"
|
||||
:version="filteredAlpha"
|
||||
:decorate-download-url="decorateModalDownloadUrl"
|
||||
@on-download="onDownload"
|
||||
@on-navigate="onVersionNavigate"
|
||||
/>
|
||||
@@ -1082,6 +1085,7 @@ import {
|
||||
OpenInAppModal,
|
||||
OverflowMenu,
|
||||
PopoutMenu,
|
||||
PROJECT_DEP_MARKER_QUERY,
|
||||
ProjectBackgroundGradient,
|
||||
ProjectEnvironmentModal,
|
||||
ProjectHeader,
|
||||
@@ -1108,7 +1112,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import dayjs from 'dayjs'
|
||||
import { Tooltip } from 'floating-vue'
|
||||
import { nextTick, useTemplateRef, watch } from 'vue'
|
||||
import { nextTick, readonly, ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
import { navigateTo } from '#app'
|
||||
import Accordion from '~/components/ui/Accordion.vue'
|
||||
@@ -1137,6 +1141,7 @@ definePageMeta({
|
||||
|
||||
const data = useNuxtApp()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const signInRouteObj = computed(() => getSignInRouteObj(route))
|
||||
const config = useRuntimeConfig()
|
||||
const moderationQueue = useModerationQueue()
|
||||
@@ -1146,6 +1151,23 @@ const { addNotification } = notifications
|
||||
const auth = await useAuth()
|
||||
const user = await useUser()
|
||||
|
||||
const { createProjectDownloadUrl } = useCdnDownloadContext()
|
||||
|
||||
const downloadReason = ref('standalone')
|
||||
|
||||
function absorbDepQuery() {
|
||||
if (route.query.dep === PROJECT_DEP_MARKER_QUERY.dep) {
|
||||
downloadReason.value = 'dependency'
|
||||
if (import.meta.client) {
|
||||
const newQuery = { ...route.query }
|
||||
delete newQuery.dep
|
||||
void router.replace({ path: route.path, query: newQuery, hash: route.hash })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => route.query.dep, absorbDepQuery, { immediate: true })
|
||||
|
||||
const tags = useGeneratedState()
|
||||
const flags = useFeatureFlags()
|
||||
const cosmetics = useCosmetics()
|
||||
@@ -1210,6 +1232,14 @@ const currentPlatformText = computed(() => {
|
||||
return formatMessage(getTagMessage(currentPlatform.value, 'loader'))
|
||||
})
|
||||
|
||||
function decorateModalDownloadUrl(url) {
|
||||
return createProjectDownloadUrl(url, {
|
||||
reason: downloadReason.value,
|
||||
gameVersion: currentGameVersion.value ?? undefined,
|
||||
loader: currentPlatform.value ?? undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const releaseVersions = computed(() => {
|
||||
const set = new Set()
|
||||
for (const gv of tags.value.gameVersions || []) {
|
||||
@@ -1715,15 +1745,27 @@ const serverRequiredContent = computed(() => {
|
||||
icon: content.project_icon,
|
||||
onclickName:
|
||||
content.project_id && content.project_id !== projectId.value
|
||||
? () => navigateTo(`/project/${content.project_id}`)
|
||||
? () => {
|
||||
navigateTo({
|
||||
path: `/project/${content.project_id}`,
|
||||
query: { ...PROJECT_DEP_MARKER_QUERY },
|
||||
})
|
||||
}
|
||||
: undefined,
|
||||
onclickVersion:
|
||||
content.project_id && content.project_id !== projectId.value
|
||||
? () =>
|
||||
navigateTo(`/project/${content.project_id}/version/${serverModpackVersion.value?.id}`)
|
||||
? () => {
|
||||
navigateTo({
|
||||
path: `/project/${content.project_id}/version/${serverModpackVersion.value?.id}`,
|
||||
query: { ...PROJECT_DEP_MARKER_QUERY },
|
||||
})
|
||||
}
|
||||
: undefined,
|
||||
onclickDownload: primaryFile?.url
|
||||
? () => navigateTo(primaryFile.url, { external: true })
|
||||
? () =>
|
||||
navigateTo(createProjectDownloadUrl(primaryFile.url, { reason: 'dependency' }), {
|
||||
external: true,
|
||||
})
|
||||
: undefined,
|
||||
showCustomModpackTooltip: content.project_id === projectId.value,
|
||||
}
|
||||
@@ -2703,6 +2745,7 @@ provideProjectPageContext({
|
||||
// Lazy dependencies loading
|
||||
dependencies,
|
||||
dependenciesLoading: computed(() => dependenciesLoading.value),
|
||||
cdnDownloadReason: readonly(downloadReason),
|
||||
|
||||
// Invalidate all project queries (auto-refetches active ones)
|
||||
invalidate: invalidateProject,
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<ButtonStyled color="brand" type="transparent">
|
||||
<a
|
||||
class="ml-auto"
|
||||
:href="version.primaryFile?.url"
|
||||
:href="createDownloadUrl(version)"
|
||||
:title="`Download ${version.name}`"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
@@ -98,7 +98,9 @@ import {
|
||||
import VersionFilterControl from '@modrinth/ui/src/components/version/VersionFilterControl.vue'
|
||||
import { renderHighlightedString } from '@modrinth/utils'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { onMounted } from 'vue'
|
||||
import { onMounted, watch } from 'vue'
|
||||
|
||||
const { createProjectDownloadUrl, updateVersionsFilterContext } = useCdnDownloadContext()
|
||||
|
||||
const formatDate = useFormatDateTime({
|
||||
month: 'short',
|
||||
@@ -106,7 +108,8 @@ const formatDate = useFormatDateTime({
|
||||
year: 'numeric',
|
||||
})
|
||||
|
||||
const { projectV2, versions, versionsLoading, loadVersions } = injectProjectPageContext()
|
||||
const { projectV2, versions, versionsLoading, loadVersions, cdnDownloadReason } =
|
||||
injectProjectPageContext()
|
||||
|
||||
// Load versions on mount (client-side)
|
||||
onMounted(() => {
|
||||
@@ -216,6 +219,23 @@ function updateQuery(newQueries) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [route.query.g, route.query.l],
|
||||
() => {
|
||||
updateVersionsFilterContext(
|
||||
queryAsStringArray(route.query.g),
|
||||
queryAsStringArray(route.query.l),
|
||||
)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function createDownloadUrl(version) {
|
||||
return createProjectDownloadUrl(getPrimaryFile(version).url, {
|
||||
reason: cdnDownloadReason.value,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
id: 'download',
|
||||
color: 'primary',
|
||||
hoverFilled: true,
|
||||
link: getPrimaryFile(version).url,
|
||||
link: createDownloadUrl(version),
|
||||
action: () => {
|
||||
emit('onDownload')
|
||||
},
|
||||
@@ -340,7 +340,7 @@ import {
|
||||
ProjectPageVersions,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useTemplateRef } from 'vue'
|
||||
import { useTemplateRef, watch } from 'vue'
|
||||
|
||||
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||
@@ -348,6 +348,8 @@ import { reportVersion } from '~/utils/report-helpers.ts'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const { createProjectDownloadUrl, updateVersionsFilterContext } = useCdnDownloadContext()
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -357,6 +359,7 @@ const {
|
||||
versions,
|
||||
invalidate,
|
||||
loadVersions,
|
||||
cdnDownloadReason,
|
||||
} = injectProjectPageContext()
|
||||
|
||||
// Load versions on mount (client-side)
|
||||
@@ -401,6 +404,23 @@ function getPrimaryFile(version: Labrinth.Versions.v3.Version) {
|
||||
return version.files.find((x) => x.primary) || version.files[0]
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [route.query.g, route.query.l],
|
||||
() => {
|
||||
updateVersionsFilterContext(
|
||||
queryAsStringArray(route.query.g),
|
||||
queryAsStringArray(route.query.l),
|
||||
)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function createDownloadUrl(version: Labrinth.Versions.v3.Version) {
|
||||
return createProjectDownloadUrl(getPrimaryFile(version).url, {
|
||||
reason: cdnDownloadReason.value,
|
||||
})
|
||||
}
|
||||
|
||||
async function copyToClipboard(text: string) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
<ButtonStyled v-if="primaryFile && !currentMember" color="brand">
|
||||
<a
|
||||
v-tooltip="primaryFile.filename + ' (' + formatBytes(primaryFile.size) + ')'"
|
||||
:href="primaryFile.url"
|
||||
:href="decoratedPrimaryFileUrl"
|
||||
@click="emit('onDownload')"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
@@ -213,14 +213,19 @@
|
||||
:key="index"
|
||||
class="dependency"
|
||||
:class="{ 'button-transparent': !isEditing }"
|
||||
@click="!isEditing ? router.push(dependency.link) : {}"
|
||||
@click="!isEditing ? navigateToDependency(dependency) : {}"
|
||||
>
|
||||
<Avatar
|
||||
:src="dependency.project ? dependency.project.icon_url : null"
|
||||
alt="dependency-icon"
|
||||
size="sm"
|
||||
/>
|
||||
<nuxt-link v-if="!isEditing" :to="dependency.link" class="info">
|
||||
<nuxt-link
|
||||
v-if="!isEditing"
|
||||
:to="{ path: dependency.link, query: PROJECT_DEP_MARKER_QUERY }"
|
||||
class="info"
|
||||
@click.stop
|
||||
>
|
||||
<span class="project-title">
|
||||
{{ dependency.project ? dependency.project.title : 'Unknown Project' }}
|
||||
</span>
|
||||
@@ -299,7 +304,7 @@
|
||||
</span>
|
||||
<ButtonStyled>
|
||||
<a
|
||||
:href="file.url"
|
||||
:href="decorateDownloadUrl(file.url)"
|
||||
class="raised-button"
|
||||
:title="`Download ${file.filename}`"
|
||||
tabindex="0"
|
||||
@@ -435,6 +440,7 @@ import {
|
||||
injectNotificationManager,
|
||||
injectProjectPageContext,
|
||||
MultiSelect,
|
||||
PROJECT_DEP_MARKER_QUERY,
|
||||
StyledInput,
|
||||
useFormatDateTime,
|
||||
} from '@modrinth/ui'
|
||||
@@ -461,6 +467,7 @@ const auth = await useAuth()
|
||||
const tags = useGeneratedState()
|
||||
const flags = useFeatureFlags()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { createProjectDownloadUrl } = useCdnDownloadContext()
|
||||
const formatDateTime = useFormatDateTime({
|
||||
timeStyle: 'short',
|
||||
dateStyle: 'long',
|
||||
@@ -481,6 +488,7 @@ const {
|
||||
dependenciesLoading: contextDependenciesLoading,
|
||||
loadDependencies,
|
||||
invalidate,
|
||||
cdnDownloadReason,
|
||||
} = injectProjectPageContext()
|
||||
|
||||
// Load versions and dependencies in parallel
|
||||
@@ -752,6 +760,21 @@ const sortedDeps = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const decoratedPrimaryFileUrl = computed(() =>
|
||||
createProjectDownloadUrl(primaryFile.value?.url, { reason: cdnDownloadReason.value }),
|
||||
)
|
||||
|
||||
function decorateDownloadUrl(url: string) {
|
||||
return createProjectDownloadUrl(url, { reason: cdnDownloadReason.value })
|
||||
}
|
||||
|
||||
function navigateToDependency(dependency: { link: string }) {
|
||||
return router.push({
|
||||
path: dependency.link,
|
||||
query: { ...PROJECT_DEP_MARKER_QUERY },
|
||||
})
|
||||
}
|
||||
|
||||
const environment = computed(
|
||||
() => ENVIRONMENTS_COPY[version.value.environment as keyof typeof ENVIRONMENTS_COPY],
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<ButtonStyled circular type="transparent">
|
||||
<a
|
||||
v-tooltip="`Download`"
|
||||
:href="getPrimaryFile(version).url"
|
||||
:href="createDownloadUrl(version)"
|
||||
class="hover:!bg-button-bg [&>svg]:!text-green"
|
||||
aria-label="Download"
|
||||
@click="emit('onDownload')"
|
||||
@@ -100,7 +100,7 @@
|
||||
id: 'download',
|
||||
color: 'primary',
|
||||
hoverFilled: true,
|
||||
link: getPrimaryFile(version).url,
|
||||
link: createDownloadUrl(version),
|
||||
action: () => {
|
||||
emit('onDownload')
|
||||
},
|
||||
@@ -266,7 +266,7 @@ import {
|
||||
OverflowMenu,
|
||||
ProjectPageVersions,
|
||||
} from '@modrinth/ui'
|
||||
import { onMounted, useTemplateRef } from 'vue'
|
||||
import { onMounted, useTemplateRef, watch } from 'vue'
|
||||
|
||||
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||
@@ -274,6 +274,8 @@ import { reportVersion } from '~/utils/report-helpers.ts'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const { createProjectDownloadUrl, updateVersionsFilterContext } = useCdnDownloadContext()
|
||||
|
||||
const tags = useGeneratedState()
|
||||
const flags = useFeatureFlags()
|
||||
const auth = await useAuth()
|
||||
@@ -287,6 +289,7 @@ const {
|
||||
versions,
|
||||
versionsLoading,
|
||||
loadVersions,
|
||||
cdnDownloadReason,
|
||||
} = injectProjectPageContext()
|
||||
|
||||
// Load versions on mount (client-side)
|
||||
@@ -316,6 +319,23 @@ function getPrimaryFile(version) {
|
||||
return version.files.find((x) => x.primary) || version.files[0]
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [route.query.g, route.query.l],
|
||||
() => {
|
||||
updateVersionsFilterContext(
|
||||
queryAsStringArray(route.query.g),
|
||||
queryAsStringArray(route.query.l),
|
||||
)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function createDownloadUrl(version) {
|
||||
return createProjectDownloadUrl(getPrimaryFile(version).url, {
|
||||
reason: cdnDownloadReason.value,
|
||||
})
|
||||
}
|
||||
|
||||
async function copyToClipboard(text) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
BrowsePageLayout,
|
||||
BrowseSidebar,
|
||||
CreationFlowModal,
|
||||
PROJECT_DEP_MARKER_QUERY,
|
||||
defineMessages,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
@@ -40,6 +41,8 @@ import type { DisplayLocation, DisplayMode } from '~/plugins/cosmetics.ts'
|
||||
const { formatMessage } = useVIntl()
|
||||
const debug = useDebugLogger('Discover')
|
||||
|
||||
const { updateDiscoverFilterContext } = useCdnDownloadContext()
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -401,7 +404,13 @@ function getServerModpackContent(project: Labrinth.Search.v3.ResultSearchProject
|
||||
name: project_name,
|
||||
icon: project_icon ?? undefined,
|
||||
onclick:
|
||||
project_id !== project.project_id ? () => navigateTo(`/project/${project_id}`) : undefined,
|
||||
project_id !== project.project_id
|
||||
? () =>
|
||||
navigateTo({
|
||||
path: `/project/${project_id}`,
|
||||
query: { ...PROJECT_DEP_MARKER_QUERY },
|
||||
})
|
||||
: undefined,
|
||||
showCustomModpackTooltip: project_id === project.project_id,
|
||||
}
|
||||
}
|
||||
@@ -639,6 +648,15 @@ const searchState = useBrowseSearch({
|
||||
displayMode: resultsDisplayMode,
|
||||
})
|
||||
|
||||
watch(
|
||||
() =>
|
||||
searchState.isServerType.value
|
||||
? searchState.serverCurrentFilters.value
|
||||
: searchState.currentFilters.value,
|
||||
(filters) => updateDiscoverFilterContext(filters),
|
||||
{ deep: true, immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
[
|
||||
() => searchState.query.value,
|
||||
|
||||
@@ -307,6 +307,7 @@ import {
|
||||
injectModrinthClient,
|
||||
NavTabs,
|
||||
OverflowMenu,
|
||||
PROJECT_DEP_MARKER_QUERY,
|
||||
ProjectCard,
|
||||
ProjectCardList,
|
||||
useCompactNumber,
|
||||
@@ -489,7 +490,10 @@ function getServerModpackContent(project: ProjectV3) {
|
||||
onclick:
|
||||
project_id !== project.id
|
||||
? () => {
|
||||
navigateTo(`/project/${project_id}`)
|
||||
navigateTo({
|
||||
path: `/project/${project_id}`,
|
||||
query: { ...PROJECT_DEP_MARKER_QUERY },
|
||||
})
|
||||
}
|
||||
: undefined,
|
||||
showCustomModpackTooltip: project_id === project.id,
|
||||
|
||||
@@ -8,6 +8,15 @@ export function queryAsString(query: LocationQueryValue | LocationQueryValue[]):
|
||||
return Array.isArray(query) ? (query[0] ?? null) : (query ?? null)
|
||||
}
|
||||
|
||||
export function queryAsStringArray(
|
||||
query: LocationQueryValue | LocationQueryValue[],
|
||||
): string | null {
|
||||
if (query === undefined || query === null) {
|
||||
return []
|
||||
}
|
||||
return Array.isArray(query) ? query.map(String) : [String(query)]
|
||||
}
|
||||
|
||||
export function routeNameAsString(name: RouteRecordNameGeneric | undefined): string | undefined {
|
||||
return name && typeof name === 'string' ? (name as string) : undefined
|
||||
}
|
||||
|
||||
@@ -39,11 +39,13 @@ import { ButtonStyled, VersionChannelIndicator } from '../index'
|
||||
|
||||
const props = defineProps<{
|
||||
version: Version
|
||||
decorateDownloadUrl?: (url: string) => string
|
||||
}>()
|
||||
|
||||
const downloadUrl = computed(() => {
|
||||
const primary: VersionFile = props.version.files.find((x) => x.primary) || props.version.files[0]
|
||||
return primary.url
|
||||
const raw = primary.url
|
||||
return props.decorateDownloadUrl ? props.decorateDownloadUrl(raw) : raw
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
||||
import type { FilterType, FilterValue, ProjectType, SortType } from '#ui/utils/search'
|
||||
import { useSearch } from '#ui/utils/search'
|
||||
import { LOADER_FILTER_TYPES, useSearch } from '#ui/utils/search'
|
||||
import { useServerSearch } from '#ui/utils/server-search'
|
||||
|
||||
import type { BrowseSearchResponse } from '../types'
|
||||
@@ -60,14 +60,6 @@ export interface BrowseSearchState {
|
||||
onFilterChange: () => void
|
||||
}
|
||||
|
||||
const LOADER_FILTER_TYPES = [
|
||||
'mod_loader',
|
||||
'plugin_loader',
|
||||
'modpack_loader',
|
||||
'shader_loader',
|
||||
'plugin_platform',
|
||||
] as const
|
||||
|
||||
export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchState {
|
||||
const debug = useDebugLogger('BrowseSearch')
|
||||
const route = useRoute()
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import type { Ref } from 'vue'
|
||||
import type { DeepReadonly, Ref } from 'vue'
|
||||
|
||||
import { createContext } from '.'
|
||||
|
||||
export const PROJECT_DEP_MARKER_QUERY = { dep: '1' } as const
|
||||
|
||||
export type CdnDownloadReason = 'standalone' | 'dependency'
|
||||
|
||||
export interface ProjectPageContext {
|
||||
// Data refs
|
||||
projectV2: Ref<Labrinth.Projects.v2.Project>
|
||||
@@ -17,6 +21,8 @@ export interface ProjectPageContext {
|
||||
dependencies: Ref<Labrinth.Projects.v2.DependencyInfo | null>
|
||||
dependenciesLoading: Ref<boolean>
|
||||
|
||||
cdnDownloadReason: DeepReadonly<Ref<CdnDownloadReason>>
|
||||
|
||||
// Invalidate all project queries (auto-refetches active ones)
|
||||
invalidate: () => Promise<void>
|
||||
|
||||
|
||||
@@ -59,6 +59,14 @@ export type FilterValue = {
|
||||
negative?: boolean
|
||||
}
|
||||
|
||||
export const LOADER_FILTER_TYPES = [
|
||||
'mod_loader',
|
||||
'plugin_loader',
|
||||
'modpack_loader',
|
||||
'shader_loader',
|
||||
'plugin_platform',
|
||||
] as const
|
||||
|
||||
export interface GameVersion {
|
||||
version: string
|
||||
version_type: 'release' | 'snapshot' | 'alpha' | 'beta'
|
||||
|
||||
Reference in New Issue
Block a user