diff --git a/apps/app-frontend/src/pages/Browse.vue b/apps/app-frontend/src/pages/Browse.vue
index 386bac5159..fa6edd041f 100644
--- a/apps/app-frontend/src/pages/Browse.vue
+++ b/apps/app-frontend/src/pages/Browse.vue
@@ -8,41 +8,32 @@ import {
LeftArrowIcon,
PlayIcon,
PlusIcon,
- SearchIcon,
StopCircleIcon,
} from '@modrinth/assets'
-import type { ProjectType, SortType, Tags } from '@modrinth/ui'
+import type { CardAction, ProjectType, Tags } from '@modrinth/ui'
import {
Admonition,
+ BrowsePageLayout,
+ BrowseSidebar,
ButtonStyled,
- Checkbox,
commonMessages,
CreationFlowModal,
defineMessages,
- DropdownSelect,
injectNotificationManager,
- LoadingIndicator,
- NavTabs,
- Pagination,
- ProjectCard,
- ProjectCardList,
- SearchFilterControl,
- SearchSidebarFilter,
- StyledInput,
+ provideBrowseManager,
+ useBrowseSearch,
useDebugLogger,
- useSearch,
- useServerSearch,
useVIntl,
} from '@modrinth/ui'
+import { useQueryClient } from '@tanstack/vue-query'
import { openUrl } from '@tauri-apps/plugin-opener'
import type { Ref } from 'vue'
-import { computed, nextTick, onUnmounted, ref, shallowRef, toRaw, watch } from 'vue'
+import { computed, onUnmounted, ref, shallowRef, watch } from 'vue'
import type { LocationQuery } from 'vue-router'
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
-import SearchCard from '@/components/ui/SearchCard.vue'
import { get_project_v3, get_search_results_v3 } from '@/helpers/cache.js'
import { process_listener } from '@/helpers/events'
import { get_by_profile_path } from '@/helpers/process'
@@ -55,6 +46,7 @@ import {
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import type { GameInstance } from '@/helpers/types'
import { add_server_to_profile, get_profile_worlds, getServerLatency } from '@/helpers/worlds'
+import { injectContentInstall } from '@/providers/content-install'
import { injectServerInstall } from '@/providers/server-install'
import {
createServerInstallContent,
@@ -67,6 +59,8 @@ const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const { installingServerProjects, playServerProject, showAddServerToInstanceModal } =
injectServerInstall()
+const { install: installVersion } = injectContentInstall()
+const queryClient = useQueryClient()
const debugLog = useDebugLogger('Browse')
const router = useRouter()
@@ -97,11 +91,6 @@ const {
markServerProjectInstalled,
} = serverInstallContent
-const projectTypes = computed(() => {
- debugLog('projectTypes computed', route.params.projectType)
- return [route.params.projectType as ProjectType]
-})
-
debugLog('fetching tags (categories, loaders, gameVersions)')
const [categories, loaders, availableGameVersions] = await Promise.all([
get_categories()
@@ -154,8 +143,6 @@ const allInstalledIds = computed(
() => new Set([...newlyInstalled.value, ...(installedProjectIds.value ?? [])]),
)
-const PERSISTENT_QUERY_PARAMS = ['i', 'ai', 'sid', 'wid', 'from']
-
watchServerContextChanges()
await initInstanceContext()
@@ -178,9 +165,6 @@ async function initInstanceContext() {
gameVersion: instance.value?.game_version,
})
- // Load installed project IDs in background — the page and initial search render immediately.
- // When this resolves, instanceFilters recomputes and triggers a search refresh
- // that applies the "hide installed" negative filters and marks installed badges.
if (route.query.from === 'worlds') {
get_profile_worlds(route.query.i as string)
.then((worlds) => {
@@ -213,7 +197,7 @@ async function initInstanceContext() {
}
}
- if (route.query.ai && !(projectTypes.value.length === 1 && projectTypes.value[0] === 'modpack')) {
+ if (route.query.ai && !(route.params.projectType === 'modpack')) {
debugLog('setting instanceHideInstalled from query', route.query.ai)
instanceHideInstalled.value = route.query.ai === 'true'
}
@@ -221,37 +205,22 @@ async function initInstanceContext() {
const instanceFilters = computed(() => {
const filters = []
- debugLog('instanceFilters recomputing', {
- hasInstance: !!instance.value,
- isServer: isServerInstance.value,
- hideInstalled: instanceHideInstalled.value,
- })
if (instance.value) {
const gameVersion = instance.value.game_version
if (gameVersion) {
- filters.push({
- type: 'game_version',
- option: gameVersion,
- })
+ filters.push({ type: 'game_version', option: gameVersion })
}
const platform = instance.value.loader
-
const supportedModLoaders = ['fabric', 'forge', 'quilt', 'neoforge']
- if (platform && projectTypes.value.includes('mod') && supportedModLoaders.includes(platform)) {
- filters.push({
- type: 'mod_loader',
- option: platform,
- })
+ if (platform && projectType.value === 'mod' && supportedModLoaders.includes(platform)) {
+ filters.push({ type: 'mod_loader', option: platform })
}
if (isServerInstance.value) {
- filters.push({
- type: 'environment',
- option: 'client',
- })
+ filters.push({ type: 'environment', option: 'client' })
}
if (
@@ -259,68 +228,15 @@ const instanceFilters = computed(() => {
(installedProjectIds.value || newlyInstalled.value.length > 0)
) {
const allInstalled = [...(installedProjectIds.value ?? []), ...newlyInstalled.value]
-
allInstalled
- .map((x) => ({
- type: 'project_id',
- option: `project_id:${x}`,
- negative: true,
- }))
+ .map((x) => ({ type: 'project_id', option: `project_id:${x}`, negative: true }))
.forEach((x) => filters.push(x))
}
}
- debugLog('instanceFilters result', filters)
return filters
})
-const {
- // Selections
- query,
- currentSortType,
- currentFilters,
- toggledGroups,
- maxResults,
- currentPage,
- overriddenProvidedFilterTypes,
-
- // Lists
- filters,
- sortTypes,
-
- // Computed
- requestParams,
-
- // Functions
- createPageParams,
-} = useSearch(projectTypes, tags, instanceFilters)
-
-const activeLoader = computed(() => {
- const filter = currentFilters.value.find((f) => f.type === 'mod_loader')
- if (filter) return filter.option
- if (projectType.value === 'datapack' || projectType.value === 'resourcepack') return 'vanilla'
- return instance.value?.loader ?? null
-})
-
-const activeGameVersion = computed(() => {
- const filter = currentFilters.value.find((f) => f.type === 'game_version')
- if (filter) return filter.option
- return instance.value?.game_version ?? null
-})
-
-function onSearchResultInstalled(id: string) {
- if (isServerContext.value) {
- markServerProjectInstalled(id)
- return
- }
- newlyInstalled.value.push(id)
-}
-
-const serverHits = shallowRef
([])
-const filteredServerHits = computed(() => {
- if (!instanceHideInstalled.value || allInstalledIds.value.size === 0) return serverHits.value
- return serverHits.value.filter((hit) => !allInstalledIds.value.has(hit.project_id))
-})
const serverPings = shallowRef>({})
const runningServerProjects = ref>({})
@@ -353,9 +269,11 @@ async function handleStopServerProject(projectId: string) {
async function handlePlayServerProject(projectId: string) {
debugLog('handlePlayServerProject', projectId)
await playServerProject(projectId)
- checkServerRunningStates(serverHits.value)
+ checkServerRunningStates(lastServerHits.value)
}
+const lastServerHits = shallowRef([])
+
async function handleAddServerToInstance(project: Labrinth.Search.v3.ResultSearchProject) {
debugLog('handleAddServerToInstance', { projectId: project.project_id, name: project.name })
const address = getServerAddress(project.minecraft_java_server)
@@ -380,6 +298,22 @@ async function handleAddServerToInstance(project: Labrinth.Search.v3.ResultSearc
}
}
+async function pingServerHits(hits: Labrinth.Search.v3.ResultSearchProject[]) {
+ debugLog('pingServerHits', { hitCount: hits.length })
+ const pingsToFetch = hits.filter((hit) => hit.minecraft_java_server?.address)
+ await Promise.all(
+ pingsToFetch.map(async (hit) => {
+ const address = hit.minecraft_java_server!.address!
+ try {
+ const latency = await getServerLatency(address)
+ serverPings.value = { ...serverPings.value, [hit.project_id]: latency }
+ } catch (err) {
+ console.error(`Failed to ping server ${address}:`, err)
+ }
+ }),
+ )
+}
+
const unlistenProcesses = await process_listener(
(e: { event: string; profile_path_id: string }) => {
debugLog('process event', e)
@@ -399,45 +333,6 @@ onUnmounted(() => {
unlistenProcesses()
})
-const {
- serverCurrentSortType,
- serverCurrentFilters,
- serverToggledGroups,
- serverSortTypes,
- serverFilterTypes,
- serverRequestParams,
- createServerPageParams,
-} = useServerSearch({ tags, query, maxResults, currentPage })
-
-if (instance.value?.game_version) {
- const gv = instance.value.game_version
- const alreadyHasGv = serverCurrentFilters.value.some(
- (f) => f.type === 'server_game_version' && f.option === gv,
- )
- if (!alreadyHasGv) {
- serverCurrentFilters.value.push({ type: 'server_game_version', option: gv })
- }
-}
-
-async function pingServerHits(hits: Labrinth.Search.v3.ResultSearchProject[]) {
- debugLog('pingServerHits', { hitCount: hits.length })
- const pingsToFetch = hits.filter((hit) => hit.minecraft_java_server?.address)
- await Promise.all(
- pingsToFetch.map(async (hit) => {
- const address = hit.minecraft_java_server!.address!
- try {
- const latency = await getServerLatency(address)
- serverPings.value = { ...serverPings.value, [hit.project_id]: latency }
- } catch (err) {
- console.error(`Failed to ping server ${address}:`, err)
- }
- }),
- )
-}
-
-const previousFilterState = ref('')
-let searchVersion = 0
-
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
debugLog('went offline')
@@ -550,181 +445,8 @@ onBeforeRouteLeave(() => {
})
})
-const loading = ref(true)
-
const projectType = ref(route.params.projectType as ProjectType)
-watch(projectType, () => {
- loading.value = true
-})
-
-interface SearchResults extends Labrinth.Search.v3.SearchResults {
- hits: (Labrinth.Search.v3.ResultSearchProject & { installed?: boolean })[]
-}
-
-const results: Ref = shallowRef(null)
-const pageCount = computed(() =>
- results.value ? Math.ceil(results.value.total_hits / results.value.hits_per_page) : 1,
-)
-
-const effectiveRequestParams = computed(() => {
- const isServer = projectType.value === 'server'
- debugLog('effectiveRequestParams computed', { isServer })
- return isServer ? serverRequestParams.value : requestParams.value
-})
-
-let searchDebounceTimer: ReturnType | null = null
-
-watch(effectiveRequestParams, () => {
- if (!route.params.projectType) return
- debugLog('effectiveRequestParams changed, debouncing search')
- if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
- searchDebounceTimer = setTimeout(() => {
- refreshSearch()
- }, 200)
-})
-
-async function refreshSearch() {
- const version = ++searchVersion
- debugLog('refreshSearch start', { version, projectType: projectType.value })
-
- try {
- const isServer = projectType.value === 'server'
- const searchParams = isServer ? serverRequestParams.value : requestParams.value
-
- debugLog('searching v3', searchParams)
- let rawResults = (await get_search_results_v3(searchParams)) as {
- result: SearchResults
- } | null
-
- if (version !== searchVersion) {
- debugLog('search version stale, discarding', { version, current: searchVersion })
- return
- }
-
- if (!rawResults) {
- rawResults = {
- result: {
- hits: [],
- total_hits: 0,
- hits_per_page: maxResults.value,
- page: 1,
- },
- }
- }
-
- if (isServer) {
- const hits = rawResults.result.hits ?? []
- debugLog('server search results', {
- hitCount: hits.length,
- totalHits: rawResults.result.total_hits,
- })
- serverHits.value = hits
- serverPings.value = {}
- pingServerHits(hits)
- checkServerRunningStates(hits)
- results.value = {
- hits: [],
- total_hits: rawResults.result.total_hits ?? 0,
- hits_per_page: maxResults.value,
- page: 1,
- }
- } else {
- if (instance.value || isServerContext.value) {
- const allInstalledIds = instance.value
- ? new Set([...newlyInstalled.value, ...(installedProjectIds.value ?? [])])
- : serverContentProjectIds.value
-
- rawResults.result.hits = rawResults.result.hits.map((val) => ({
- ...val,
- installed: allInstalledIds.has(val.project_id),
- }))
- }
- debugLog('v3 search results', {
- hitCount: rawResults.result.hits.length,
- totalHits: rawResults.result.total_hits,
- })
- results.value = {
- ...rawResults.result,
- hits_per_page: maxResults.value,
- }
- }
-
- const currentFilterState = JSON.stringify({
- query: query.value,
- filters: toRaw(currentFilters.value),
- sort: toRaw(currentSortType.value),
- maxResults: maxResults.value,
- projectTypes: toRaw(projectTypes.value),
- })
-
- if (previousFilterState.value && previousFilterState.value !== currentFilterState) {
- debugLog('filters changed, resetting to page 1')
- currentPage.value = 1
- }
-
- previousFilterState.value = currentFilterState
-
- const persistentParams: LocationQuery = {}
-
- for (const [key, value] of Object.entries(route.query)) {
- if (PERSISTENT_QUERY_PARAMS.includes(key)) {
- persistentParams[key] = value
- }
- }
-
- if (serverIdQuery.value) {
- persistentParams.sid = serverIdQuery.value
- if (effectiveServerWorldId.value) {
- persistentParams.wid = effectiveServerWorldId.value
- }
- }
-
- if (instanceHideInstalled.value) {
- persistentParams.ai = 'true'
- } else {
- delete persistentParams.ai
- }
-
- const params = {
- ...persistentParams,
- ...(isServer ? createServerPageParams() : createPageParams()),
- }
-
- debugLog('updating URL', params)
- router.replace({ path: route.path, query: params })
-
- loading.value = false
- debugLog('refreshSearch complete', { version })
- } catch (err) {
- debugLog('refreshSearch error', err)
- if (version === searchVersion) {
- loading.value = false
- }
- }
-}
-
-async function setPage(newPageNumber: number) {
- debugLog('setPage', newPageNumber)
- currentPage.value = newPageNumber
-
- await onSearchChangeToTop()
-}
-
-const searchWrapper: Ref = ref(null)
-
-async function onSearchChangeToTop() {
- await nextTick()
-
- window.scrollTo({ top: 0, behavior: 'smooth' })
-}
-
-function clearSearch() {
- debugLog('clearSearch')
- query.value = ''
- currentPage.value = 1
-}
-
watch(
() => route.params.projectType as ProjectType,
async (newType) => {
@@ -733,13 +455,11 @@ watch(
if (newType !== 'modpack') return
}
- // Check if the newType is not the same as the current value
if (!newType || newType === projectType.value) return
debugLog('projectType route param changed', { from: projectType.value, to: newType })
projectType.value = newType
- // If instance context was removed (e.g. sidebar browse navigation), reset state
if (!route.query.i && instance.value) {
debugLog('instance context removed, resetting')
instance.value = null
@@ -750,9 +470,6 @@ watch(
breadcrumbs.setName('BrowseTitle', formatMessage(messages.discoverContent))
breadcrumbs.setContext(null)
}
-
- currentSortType.value = { display: 'Relevance', name: 'relevance' }
- query.value = ''
},
)
@@ -782,21 +499,11 @@ const selectableProjectTypes = computed(() => {
const params: LocationQuery = {}
- if (route.query.i) {
- params.i = route.query.i
- }
- if (route.query.ai) {
- params.ai = route.query.ai
- }
- if (route.query.from) {
- params.from = route.query.from
- }
- if (route.query.sid) {
- params.sid = route.query.sid
- }
- if (effectiveServerWorldId.value) {
- params.wid = effectiveServerWorldId.value
- }
+ if (route.query.i) params.i = route.query.i
+ if (route.query.ai) params.ai = route.query.ai
+ if (route.query.from) params.from = route.query.from
+ if (route.query.sid) params.sid = route.query.sid
+ if (effectiveServerWorldId.value) params.wid = effectiveServerWorldId.value
const queryString = new URLSearchParams(params as Record).toString()
const suffix = queryString ? `?${queryString}` : ''
@@ -826,7 +533,7 @@ const getServerModpackContent = (project: Labrinth.Search.v3.ResultSearchProject
if (!project_name) return undefined
return {
name: project_name,
- icon: project_icon,
+ icon: project_icon ?? undefined,
onclick:
project_id !== project.project_id
? () => {
@@ -839,18 +546,11 @@ const getServerModpackContent = (project: Labrinth.Search.v3.ResultSearchProject
return undefined
}
-const options = ref(null)
+const contextMenuRef = ref(null)
// @ts-expect-error - no event types
const handleRightClick = (event, result) => {
// @ts-ignore
- options.value?.showMenu(event, result, [
- {
- name: 'open_link',
- },
- {
- name: 'copy_link',
- },
- ])
+ contextMenuRef.value?.showMenu(event, result, [{ name: 'open_link' }, { name: 'copy_link' }])
}
// @ts-expect-error - no event types
const handleOptionsClick = (args) => {
@@ -866,361 +566,365 @@ const handleOptionsClick = (args) => {
}
}
-debugLog('performing initial search')
-await refreshSearch()
+const installContext = computed(() => {
+ if (isServerContext.value && serverContextServerData.value) {
+ return {
+ name: serverContextServerData.value.name,
+ subtitle: `${serverContextServerData.value.loader} ${serverContextServerData.value.mc_version}`,
+ backUrl: serverBackUrl.value,
+ backLabel: serverBackLabel.value,
+ heading: serverBrowseHeading.value,
+ }
+ }
+ return null
+})
+
+const installingProjectIds = ref>(new Set())
+
+function getCardActions(
+ result: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject,
+ currentProjectType: string,
+): CardAction[] {
+ if (currentProjectType === 'server') {
+ const serverResult = result as Labrinth.Search.v3.ResultSearchProject
+ const isInstalled = allInstalledIds.value.has(serverResult.project_id)
+
+ if (isFromWorlds.value && instance.value) {
+ return [
+ {
+ key: 'add-to-instance',
+ label: formatMessage(isInstalled ? messages.added : messages.addToInstance),
+ icon: isInstalled ? CheckIcon : PlusIcon,
+ disabled: isInstalled,
+ color: 'brand',
+ type: 'outlined',
+ onClick: () => handleAddServerToInstance(serverResult),
+ },
+ ]
+ }
+
+ const actions: CardAction[] = []
+
+ actions.push({
+ key: 'add',
+ label: '',
+ icon: isInstalled ? CheckIcon : PlusIcon,
+ disabled: isInstalled,
+ circular: true,
+ tooltip: isInstalled
+ ? formatMessage(messages.alreadyAdded)
+ : instance.value
+ ? formatMessage(messages.addToInstanceName, { instanceName: instance.value.name })
+ : formatMessage(messages.addServerToInstance),
+ onClick: () => handleAddServerToInstance(serverResult),
+ })
+
+ if (runningServerProjects.value[serverResult.project_id]) {
+ actions.push({
+ key: 'stop',
+ label: formatMessage(commonMessages.stopButton),
+ icon: StopCircleIcon,
+ color: 'red',
+ type: 'outlined',
+ onClick: () => handleStopServerProject(serverResult.project_id),
+ })
+ } else {
+ const isInstalling = (installingServerProjects.value as string[]).includes(
+ serverResult.project_id,
+ )
+ actions.push({
+ key: 'play',
+ label: formatMessage(
+ isInstalling ? commonMessages.installingLabel : commonMessages.playButton,
+ ),
+ icon: PlayIcon,
+ disabled: isInstalling,
+ color: 'brand',
+ type: 'outlined',
+ onClick: () => handlePlayServerProject(serverResult.project_id),
+ })
+ }
+
+ return actions
+ }
-// Initialize previousFilterState after first search
-previousFilterState.value = JSON.stringify({
- query: query.value,
- filters: currentFilters.value,
- sort: currentSortType.value,
- maxResults: maxResults.value,
- projectTypes: projectTypes.value,
+ // Non-server project actions
+ const projectResult = result as (Labrinth.Search.v2.ResultSearchProject &
+ Labrinth.Search.v3.ResultSearchProject) & {
+ installed?: boolean
+ installing?: boolean
+ }
+ const isInstalled =
+ projectResult.installed ||
+ allInstalledIds.value.has(projectResult.project_id || '') ||
+ serverContentProjectIds.value.has(projectResult.project_id || '')
+ const isInstalling = installingProjectIds.value.has(projectResult.project_id)
+
+ if (
+ isServerContext.value &&
+ ['modpack', 'mod', 'plugin', 'datapack'].includes(currentProjectType)
+ ) {
+ return [
+ {
+ key: 'install',
+ label: isInstalling ? 'Installing' : isInstalled ? 'Installed' : 'Install',
+ icon: isInstalled ? CheckIcon : PlusIcon,
+ disabled: isInstalled || isInstalling,
+ color: 'brand',
+ type: 'outlined',
+ onClick: async () => {
+ installingProjectIds.value.add(projectResult.project_id)
+ try {
+ const didInstall = await installProjectToServer(projectResult)
+ if (didInstall !== false) {
+ onSearchResultInstalled(projectResult.project_id)
+ }
+ } catch (err) {
+ handleError(err as Error)
+ } finally {
+ installingProjectIds.value.delete(projectResult.project_id)
+ }
+ },
+ },
+ ]
+ }
+
+ const isModpack = projectResult.project_types?.includes('modpack')
+ const shouldUseInstallIcon = !!instance.value || isModpack
+
+ return [
+ {
+ key: 'install',
+ label: isInstalling
+ ? 'Installing'
+ : isInstalled
+ ? 'Installed'
+ : shouldUseInstallIcon
+ ? 'Install'
+ : 'Add to an instance',
+ icon: isInstalling
+ ? PlusIcon
+ : isInstalled
+ ? CheckIcon
+ : shouldUseInstallIcon
+ ? PlusIcon
+ : PlusIcon,
+ disabled: isInstalled || isInstalling,
+ color: 'brand',
+ type: 'outlined',
+ onClick: async () => {
+ installingProjectIds.value.add(projectResult.project_id)
+ await installVersion(
+ projectResult.project_id,
+ null,
+ instance.value ? instance.value.path : null,
+ 'SearchCard',
+ (versionId) => {
+ installingProjectIds.value.delete(projectResult.project_id)
+ if (versionId) {
+ onSearchResultInstalled(projectResult.project_id)
+ }
+ },
+ (profile) => {
+ router.push(`/instance/${profile}`)
+ },
+ {
+ preferredLoader: instance.value?.loader ?? undefined,
+ preferredGameVersion: instance.value?.game_version ?? undefined,
+ },
+ ).catch((err) => {
+ installingProjectIds.value.delete(projectResult.project_id)
+ handleError(err)
+ })
+ },
+ },
+ ]
+}
+
+function onSearchResultInstalled(id: string) {
+ if (isServerContext.value) {
+ markServerProjectInstalled(id)
+ return
+ }
+ newlyInstalled.value.push(id)
+}
+
+async function search(requestParams: string) {
+ debugLog('searching v3', requestParams)
+ const isServer = projectType.value === 'server'
+
+ const rawResults = await queryClient.fetchQuery({
+ queryKey: ['search', 'v3', requestParams],
+ queryFn: () => get_search_results_v3(requestParams) as Promise<{
+ result: Labrinth.Search.v3.SearchResults & {
+ hits: (Labrinth.Search.v3.ResultSearchProject & { installed?: boolean })[]
+ }
+ } | null>,
+ staleTime: 30_000,
+ })
+
+ if (!rawResults) {
+ return {
+ projectHits: [],
+ serverHits: [],
+ total_hits: 0,
+ per_page: 20,
+ }
+ }
+
+ if (isServer) {
+ const hits = rawResults.result.hits ?? []
+ lastServerHits.value = hits
+ serverPings.value = {}
+ pingServerHits(hits)
+ checkServerRunningStates(hits)
+ return {
+ projectHits: [],
+ serverHits: hits,
+ total_hits: rawResults.result.total_hits ?? 0,
+ per_page: rawResults.result.hits_per_page,
+ }
+ }
+
+ const hits = rawResults.result.hits.map((hit) => {
+ const mapped = { ...hit, title: hit.name, description: hit.summary } as unknown as
+ Labrinth.Search.v2.ResultSearchProject & { installed?: boolean }
+
+ if (instance.value || isServerContext.value) {
+ const installedIds = instance.value
+ ? new Set([...newlyInstalled.value, ...(installedProjectIds.value ?? [])])
+ : serverContentProjectIds.value
+ mapped.installed = installedIds.has(hit.project_id)
+ }
+
+ return mapped
+ })
+
+ return {
+ projectHits: hits,
+ serverHits: [],
+ total_hits: rawResults.result.total_hits,
+ per_page: rawResults.result.hits_per_page,
+ }
+}
+
+const lockedFilterMessages = computed(() => ({
+ gameVersion: formatMessage(
+ isServerInstance.value
+ ? messages.gameVersionProvidedByServer
+ : messages.gameVersionProvidedByInstance,
+ ),
+ modLoader: formatMessage(
+ isServerInstance.value
+ ? messages.modLoaderProvidedByServer
+ : messages.modLoaderProvidedByInstance,
+ ),
+ environment: formatMessage(messages.environmentProvidedByServer),
+ syncButton: formatMessage(messages.syncFilterButton),
+ providedBy: formatMessage(
+ isServerInstance.value ? messages.providedByServer : messages.providedByInstance,
+ ),
+}))
+
+const searchState = useBrowseSearch({
+ projectType,
+ tags,
+ providedFilters: instanceFilters,
+ search,
+ persistentQueryParams: ['i', 'ai', 'sid', 'wid', 'from'],
+ getExtraQueryParams: () => ({
+ sid: serverIdQuery.value || undefined,
+ wid: effectiveServerWorldId.value || undefined,
+ ai: instanceHideInstalled.value ? 'true' : undefined,
+ }),
+})
+
+if (instance.value?.game_version) {
+ const gv = instance.value.game_version
+ const alreadyHasGv = searchState.serverCurrentFilters.value.some(
+ (f) => f.type === 'server_game_version' && f.option === gv,
+ )
+ if (!alreadyHasGv) {
+ searchState.serverCurrentFilters.value.push({ type: 'server_game_version', option: gv })
+ }
+}
+
+await searchState.refreshSearch()
+
+provideBrowseManager({
+ tags,
+ projectType,
+ ...searchState,
+ getProjectLink: (result: Labrinth.Search.v2.ResultSearchProject) => ({
+ path: `/project/${result.project_id ?? result.slug}`,
+ query: instance.value ? { i: instance.value.path } : undefined,
+ }),
+ getServerProjectLink: (result: Labrinth.Search.v3.ResultSearchProject) =>
+ `/project/${result.slug ?? result.project_id}`,
+ selectableProjectTypes,
+ showProjectTypeTabs: computed(() => !isServerContext.value),
+ variant: 'app',
+ getCardActions,
+ installContext,
+ providedFilters: instanceFilters,
+ hideInstalled: instanceHideInstalled,
+ showHideInstalled: computed(() => !!instance.value),
+ hideInstalledLabel: computed(() =>
+ formatMessage(isFromWorlds.value ? messages.hideAddedServers : messages.hideInstalledContent),
+ ),
+ onInstalled: onSearchResultInstalled,
+ serverPings,
+ getServerModpackContent,
+ onContextMenu: handleRightClick,
+ offline,
+ lockedFilterMessages,
})
-
-
-
-
-
-
-
- {{ filterType.formatted_name }}
-
-
-
-
-
-
- {{ filter.formatted_name }}
-
-
- {{
- formatMessage(
- isServerInstance
- ? messages.gameVersionProvidedByServer
- : messages.gameVersionProvidedByInstance,
- )
- }}
-
-
+
+
+
+
+
+
+ {{ serverContextServerData.name }}
+
+ {{ serverContextServerData.loader }} {{ serverContextServerData.mc_version }}
+
+
+
+
+
+ {{ serverBackLabel }}
+
+
+
+
+
+
+
+
{{
formatMessage(
- isServerInstance
- ? messages.modLoaderProvidedByServer
- : messages.modLoaderProvidedByInstance,
+ isFromWorlds ? messages.addServersToInstance : messages.installContentToInstance,
)
}}
-
-
- {{ formatMessage(messages.environmentProvidedByServer) }}
-
- {{ formatMessage(messages.syncFilterButton) }}
-
-
-
-
-
-
-
- {{ serverContextServerData.name }}
-
- {{ serverContextServerData.loader }} {{ serverContextServerData.mc_version }}
-
-
-
-
-
- {{ serverBackLabel }}
-
-
-
-
-
-
-
-
- {{
- formatMessage(
- isFromWorlds ? messages.addServersToInstance : messages.installContentToInstance,
- )
- }}
-
-
- Adding content can break compatibility when joining the server. Any added content will also
- be lost when you update the server instance content.
-
+
+
+ Adding content can break compatibility when joining the server. Any added content will
+ also be lost when you update the server instance content.
+
+
-
-
-
-
{
- if (projectType === 'server') serverCurrentSortType = v
- else currentSortType = v
- }
- "
- >
- Sort by:
- {{ selected }}
-
-
- View:
- {{ selected }}
-
-
-
-
-
-
-
-
- You are currently offline. Connect to the internet to browse Modrinth!
-
-
- No results found for your query!
-
-
-
-
-
- handleRightClick(event, { project_types: ['server'], slug: project.slug })
- "
- >
-
-
-
-
- handleAddServerToInstance(project)"
- >
-
-
- {{
- formatMessage(
- allInstalledIds.has(project.project_id)
- ? messages.added
- : messages.addToInstance,
- )
- }}
-
-
-
-
-
- handleAddServerToInstance(project)"
- >
-
-
-
-
-
- handleStopServerProject(project.project_id)">
-
- {{ formatMessage(commonMessages.stopButton) }}
-
-
-
- handlePlayServerProject(project.project_id)"
- >
-
- {{
- formatMessage(
- (installingServerProjects as string[]).includes(project.project_id)
- ? commonMessages.installingLabel
- : commonMessages.playButton,
- )
- }}
-
-
-
-
-
-
-
-
- handleRightClick(event, result)"
- />
-
-
-
- Open in Modrinth
- Copy link
-
-
-
-
-
-
+
+
+ Open in Modrinth
+ Copy link
+
+
+
{}"
@create="handleServerModpackFlowCreate"
/>
+
+
+
diff --git a/apps/frontend/src/pages/discover/[type]/index.vue b/apps/frontend/src/pages/discover/[type]/index.vue
index f28b0ed87d..cca3eab6cd 100644
--- a/apps/frontend/src/pages/discover/[type]/index.vue
+++ b/apps/frontend/src/pages/discover/[type]/index.vue
@@ -4,44 +4,33 @@ import {
BookmarkIcon,
CheckIcon,
DownloadIcon,
- FilterIcon,
GameIcon,
GridIcon,
HeartIcon,
ImageIcon,
- InfoIcon,
LeftArrowIcon,
ListIcon,
MinecraftServerIcon,
MoreVerticalIcon,
- SearchIcon,
- XIcon,
} from '@modrinth/assets'
+import type { CardAction, CreationFlowContextValue } from '@modrinth/ui'
import {
Avatar,
+ BrowsePageLayout,
+ BrowseSidebar,
ButtonStyled,
- Checkbox,
- type CreationFlowContextValue,
CreationFlowModal,
defineMessages,
- DropdownSelect,
injectModrinthClient,
injectNotificationManager,
- Pagination,
- ProjectCard,
- ProjectCardList,
- SearchFilterControl,
- SearchSidebarFilter,
- type SortType,
- StyledInput,
+ provideBrowseManager,
+ useBrowseSearch,
useDebugLogger,
- useSearch,
- useServerSearch,
useVIntl,
} from '@modrinth/ui'
-import { capitalizeString, cycleValue } from '@modrinth/utils'
+import { cycleValue } from '@modrinth/utils'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
-import { useThrottleFn, useTimeoutFn } from '@vueuse/core'
+import { useTimeoutFn } from '@vueuse/core'
import { computed, nextTick, ref, watch } from 'vue'
import LogoAnimated from '~/components/brand/LogoAnimated.vue'
@@ -60,7 +49,6 @@ const queryClient = useQueryClient()
const filtersMenuOpen = ref(false)
const route = useRoute()
-const router = useRouter()
const cosmetics = useCosmetics()
const tags = useGeneratedState()
@@ -115,10 +103,13 @@ const currentType = computed(() =>
queryAsStringOrEmpty(route.params.type).replaceAll(/^\/|s\/?$/g, ''),
)
+debug('initial route.params.type:', route.params.type, '→ currentType:', currentType.value)
+
const isServerType = computed(() => currentType.value === 'server')
const projectType = computed(() => tags.value.projectTypes.find((x) => x.id === currentType.value))
-const projectTypes = computed(() => (projectType.value ? [projectType.value.id] : []))
+
+watch(() => projectType.value?.id, (val) => debug('projectType.id changed:', val))
const resultsDisplayLocation = computed(
() => projectType.value?.id as DisplayLocation,
@@ -129,10 +120,27 @@ const resultsDisplayMode = computed(() =>
: 'list',
)
+const maxResultsForView = ref>({
+ list: [5, 10, 15, 20, 50, 100],
+ grid: [6, 12, 18, 24, 48, 96],
+ gallery: [6, 10, 16, 20, 50, 100],
+})
+
+const currentMaxResultsOptions = computed(
+ () => maxResultsForView.value[resultsDisplayMode.value] ?? [20],
+)
+
+function cycleSearchDisplayMode() {
+ if (!resultsDisplayLocation.value) return
+ cosmetics.value.searchDisplayMode[resultsDisplayLocation.value] = cycleValue(
+ cosmetics.value.searchDisplayMode[resultsDisplayLocation.value],
+ tags.value.projectViewModes.filter((x) => x !== 'grid'),
+ )
+}
+
const currentServerId = computed(() => queryAsString(route.query.sid) || null)
const fromContext = computed(() => queryAsString(route.query.from) || null)
const currentWorldId = computed(() => queryAsString(route.query.wid) || undefined)
-debug('currentServerId:', currentServerId.value)
const {
data: serverData,
@@ -166,7 +174,6 @@ const serverIcon = computed(() => {
const serverHideInstalled = ref(false)
-// TanStack Query for server content list
const contentQueryKey = computed(() => ['content', 'list', currentServerId.value ?? ''] as const)
const { data: serverContentData, error: serverContentError } = useQuery({
queryKey: contentQueryKey,
@@ -174,7 +181,6 @@ const { data: serverContentData, error: serverContentError } = useQuery({
enabled: computed(() => !!currentServerId.value && !!currentWorldId.value),
})
-// Watch for errors and notify user
watch(serverContentError, (error) => {
if (error) {
console.error('Failed to load server content:', error)
@@ -182,14 +188,6 @@ watch(serverContentError, (error) => {
}
})
-// Re-run search when server content loads so "Hide installed" filter applies
-watch(serverContentData, () => {
- if (serverHideInstalled.value) {
- updateSearchResults(1, false)
- }
-})
-
-// Install content mutation
const installContentMutation = useMutation({
mutationFn: ({
serverId,
@@ -211,8 +209,6 @@ const installContentMutation = useMutation({
},
})
-const PERSISTENT_QUERY_PARAMS = ['sid', 'wid', 'shi', 'from']
-
if (route.query.shi && projectType.value?.id !== 'modpack') {
serverHideInstalled.value = route.query.shi === 'true'
}
@@ -228,37 +224,23 @@ const serverFilters = computed(() => {
if (serverData.value && projectType.value?.id !== 'modpack') {
const gameVersion = serverData.value.mc_version
if (gameVersion) {
- filters.push({
- type: 'game_version',
- option: gameVersion,
- })
+ filters.push({ type: 'game_version', option: gameVersion })
}
const platform = serverData.value.loader?.toLowerCase()
const modLoaders = ['fabric', 'forge', 'quilt', 'neoforge']
-
if (platform && modLoaders.includes(platform)) {
- filters.push({
- type: 'mod_loader',
- option: platform,
- })
+ filters.push({ type: 'mod_loader', option: platform })
}
const pluginLoaders = ['paper', 'purpur']
-
if (platform && pluginLoaders.includes(platform)) {
- filters.push({
- type: 'plugin_loader',
- option: platform,
- })
+ filters.push({ type: 'plugin_loader', option: platform })
}
if (projectType.value?.id === 'mod') {
- filters.push({
- type: 'environment',
- option: 'server',
- })
+ filters.push({ type: 'environment', option: 'server' })
}
if (serverHideInstalled.value && serverContentData.value) {
@@ -279,142 +261,14 @@ const serverFilters = computed(() => {
if (currentServerId.value && projectType.value?.id === 'modpack') {
filters.push(
- {
- type: 'environment',
- option: 'client',
- },
- {
- type: 'environment',
- option: 'server',
- },
+ { type: 'environment', option: 'client' },
+ { type: 'environment', option: 'server' },
)
}
debug('serverFilters result:', filters)
return filters
})
-const maxResultsForView = ref>({
- list: [5, 10, 15, 20, 50, 100],
- grid: [6, 12, 18, 24, 48, 96],
- gallery: [6, 10, 16, 20, 50, 100],
-})
-
-const currentMaxResultsOptions = computed(
- () => maxResultsForView.value[resultsDisplayMode.value] ?? [20],
-)
-
-const LOADER_FILTER_TYPES = [
- 'mod_loader',
- 'plugin_loader',
- 'modpack_loader',
- 'shader_loader',
- 'plugin_platform',
-] as const
-
-const {
- // Selections
- query,
- currentSortType,
- currentFilters,
- toggledGroups,
- maxResults,
- currentPage,
- overriddenProvidedFilterTypes,
-
- // Lists
- filters,
- sortTypes,
-
- // Computed
- requestParams,
-
- // Functions
- createPageParams,
-} = useSearch(projectTypes, tags, serverFilters)
-debug('useSearch initialized, requestParams:', requestParams.value)
-
-const {
- serverCurrentSortType,
- serverCurrentFilters,
- serverToggledGroups,
- serverSortTypes,
- serverFilterTypes,
- serverRequestParams,
- createServerPageParams,
-} = useServerSearch({ tags, query, maxResults, currentPage })
-
-const effectiveRequestParams = computed(() =>
- isServerType.value ? serverRequestParams.value : requestParams.value,
-)
-const effectiveSortTypes = computed(() =>
- isServerType.value ? (serverSortTypes as readonly SortType[]) : sortTypes,
-)
-const effectiveCurrentSortType = computed({
- get: () => (isServerType.value ? serverCurrentSortType.value : currentSortType.value),
- set: (v: SortType) => {
- if (isServerType.value) serverCurrentSortType.value = v
- else currentSortType.value = v
- },
-})
-const effectiveCurrentFilters = computed({
- get: () => (isServerType.value ? serverCurrentFilters.value : currentFilters.value),
- set: (v) => {
- if (isServerType.value) serverCurrentFilters.value = v
- else currentFilters.value = v
- },
-})
-const selectedFilterTags = computed(() =>
- currentFilters.value
- .filter(
- (f) =>
- f.type.startsWith('category_') ||
- LOADER_FILTER_TYPES.includes(f.type as (typeof LOADER_FILTER_TYPES)[number]),
- )
- .map((f) => f.option),
-)
-const excludeLoaders = computed(
- () =>
- currentFilters.value.some((f) =>
- LOADER_FILTER_TYPES.includes(f.type as (typeof LOADER_FILTER_TYPES)[number]),
- ) || ['resourcepack', 'datapack'].includes(currentType.value),
-)
-
-const loadersNotForThisType = computed(() => {
- return (
- tags.value?.loaders
- ?.filter((loader) => !loader.supported_project_types.includes(currentType.value))
- ?.map((loader) => loader.name) ?? []
- )
-})
-
-const deprioritizedTags = computed(() => {
- return [...selectedFilterTags.value, ...loadersNotForThisType.value]
-})
-
-const messages = defineMessages({
- gameVersionProvidedByServer: {
- id: 'search.filter.locked.server-game-version.title',
- defaultMessage: 'Game version is provided by the server',
- },
- modLoaderProvidedByServer: {
- id: 'search.filter.locked.server-loader.title',
- defaultMessage: 'Loader is provided by the server',
- },
- providedByServer: {
- id: 'search.filter.locked.server',
- defaultMessage: 'Provided by the server',
- },
- syncFilterButton: {
- id: 'search.filter.locked.server.sync',
- defaultMessage: 'Sync with server',
- },
- gameVersionShaderMessage: {
- id: 'search.filter.game-version-shader-message',
- defaultMessage:
- 'Shader packs for older versions most likely work on newer versions with only minor issues.',
- },
-})
-
interface InstallableSearchResult extends Labrinth.Search.v2.ResultSearchProject {
installing?: boolean
installed?: boolean
@@ -428,7 +282,6 @@ async function serverInstall(project: InstallableSearchResult) {
project.installing = true
try {
if (projectType.value?.id === 'modpack') {
- // TODO: restore limit=1 once the backend fix for version ordering is deployed (limit is applied before sorting)
const versions = await client.labrinth.versions_v2.getProjectVersions(project.project_id, {
include_changelog: false,
})
@@ -498,7 +351,7 @@ function getServerModpackContent(project: Labrinth.Search.v3.ResultSearchProject
if (!project_name) return undefined
return {
name: project_name,
- icon: project_icon,
+ icon: project_icon ?? undefined,
onclick:
project_id !== project.project_id ? () => navigateTo(`/project/${project_id}`) : undefined,
showCustomModpackTooltip: project_id === project.project_id,
@@ -507,157 +360,110 @@ function getServerModpackContent(project: Labrinth.Search.v3.ResultSearchProject
return undefined
}
-const noLoad = ref(false)
-const {
- data: rawResults,
- refresh: refreshSearch,
- pending: searchLoading,
-} = useLazyFetch(
- () => {
- const config = useRuntimeConfig()
- let base = import.meta.server ? config.apiBaseUrl : config.public.apiBaseUrl
-
- if (currentType.value === 'server') {
- base = base.replace(/\/v\d\//, '/v3/').replace(/\/v\d$/, '/v3')
- }
-
- return `${base}search${effectiveRequestParams.value}`
- },
- {
- headers: computed(() => withLabrinthCanaryHeader()),
-
- watch: false,
- transform: (
- hits: Labrinth.Search.v2.SearchResults | Labrinth.Search.v3.SearchResults,
- ): Labrinth.Search.v2.SearchResults => {
- noLoad.value = false
- if ('hits_per_page' in hits) {
- return {
- hits: hits.hits as unknown as Labrinth.Search.v2.ResultSearchProject[],
- total_hits: hits.total_hits,
- limit: hits.hits_per_page,
- offset: (hits.page - 1) * hits.hits_per_page,
- }
- }
- return hits
- },
- },
-)
-
-watch(searchLoading, (val) => debug('searchLoading:', val))
-watch(rawResults, (val) => debug('rawResults changed, total_hits:', val?.total_hits))
-
-const results = computed(() => rawResults.value)
-const serverResults = computed(() =>
- isServerType.value ? (results.value as Labrinth.Search.v3.SearchResults | null) : null,
-)
-const projectResults = computed(() =>
- isServerType.value ? null : (results.value as Labrinth.Search.v2.SearchResults | null),
-)
-const pageCount = computed(() => {
- if (!results.value) return 1
- // @ts-expect-error
- const perPage = 'limit' in results.value ? results.value.limit : results.value.hits_per_page
- return Math.ceil(results.value.total_hits / perPage)
-})
-
-function scrollToTop(behavior: ScrollBehavior = 'smooth') {
- window.scrollTo({ top: 0, behavior })
-}
-
-function updateSearchResults(pageNumber: number = 1, resetScroll = true) {
- debug(
- 'updateSearchResults called, page:',
- pageNumber,
- 'query:',
- query.value,
- 'requestParams:',
- requestParams.value,
- )
- currentPage.value = pageNumber
- if (resetScroll) {
- scrollToTop()
- }
- noLoad.value = true
+async function search(requestParams: string) {
+ debug('search() called', { requestParams: requestParams.substring(0, 100), isServer: isServerType.value, projectTypeId: projectTypeId.value })
+ const config = useRuntimeConfig()
+ let base = import.meta.server ? config.apiBaseUrl : config.public.apiBaseUrl
- if (query.value === null) {
- debug('updateSearchResults: query is null, returning early')
- return
+ if (isServerType.value) {
+ base = base.replace(/\/v\d\//, '/v3/').replace(/\/v\d$/, '/v3')
}
- debug('updateSearchResults: calling refreshSearch')
- refreshSearch()
+ const url = `${base}search${requestParams}`
+ debug('search() fetching:', url.substring(0, 120))
- if (import.meta.client) {
- const persistentParams: Record = {}
-
- for (const [key, value] of Object.entries(route.query)) {
- if (PERSISTENT_QUERY_PARAMS.includes(key)) {
- persistentParams[key] = value
- }
- }
+ const raw = await $fetch(
+ url,
+ {
+ headers: withLabrinthCanaryHeader(),
+ },
+ )
- if (serverHideInstalled.value) {
- persistentParams.shi = 'true'
- } else {
- delete persistentParams.shi
- }
+ debug('search() response', { total_hits: raw.total_hits, hitCount: raw.hits?.length })
- const params = {
- ...persistentParams,
- ...(isServerType.value ? createServerPageParams() : createPageParams()),
+ if ('hits_per_page' in raw) {
+ // v3 response (servers)
+ return {
+ projectHits: [],
+ serverHits: raw.hits as Labrinth.Search.v3.ResultSearchProject[],
+ total_hits: raw.total_hits,
+ per_page: raw.hits_per_page,
}
+ }
- router.replace({ path: route.path, query: params })
+ return {
+ projectHits: raw.hits as Labrinth.Search.v2.ResultSearchProject[],
+ serverHits: [],
+ total_hits: raw.total_hits,
+ per_page: raw.limit,
}
}
-watch([effectiveCurrentFilters], () => {
- updateSearchResults(1, false)
-})
+function getCardActions(
+ result: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject,
+ currentProjectType: string,
+): CardAction[] {
+ if (currentProjectType === 'server') return []
-const throttledSearch = useThrottleFn(() => updateSearchResults(), 500, true)
+ const projectResult = result as InstallableSearchResult
-function cycleSearchDisplayMode() {
- if (!resultsDisplayLocation.value) {
- // if no display location, abort
- return
+ if (flags.value.showDiscoverProjectButtons) {
+ return [
+ {
+ key: 'download',
+ label: 'Download',
+ icon: DownloadIcon,
+ color: 'brand',
+ onClick: () => {},
+ },
+ {
+ key: 'heart',
+ label: '',
+ icon: HeartIcon,
+ circular: true,
+ onClick: () => {},
+ },
+ {
+ key: 'bookmark',
+ label: '',
+ icon: BookmarkIcon,
+ circular: true,
+ onClick: () => {},
+ },
+ {
+ key: 'more',
+ label: '',
+ icon: MoreVerticalIcon,
+ circular: true,
+ type: 'transparent',
+ onClick: () => {},
+ },
+ ]
}
- cosmetics.value.searchDisplayMode[resultsDisplayLocation.value] = cycleValue(
- cosmetics.value.searchDisplayMode[resultsDisplayLocation.value],
- tags.value.projectViewModes.filter((x) => x !== 'grid'),
- )
- setClosestMaxResults()
-}
-function setClosestMaxResults() {
- const maxResultsOptions = maxResultsForView.value[resultsDisplayMode.value] ?? [20]
- const currentMax = maxResults.value
- if (!maxResultsOptions.includes(currentMax)) {
- maxResults.value = maxResultsOptions.reduce((prev: number, curr: number) => {
- return Math.abs(curr - currentMax) <= Math.abs(prev - currentMax) ? curr : prev
- })
- }
-}
+ if (serverData.value) {
+ const isInstalled =
+ projectResult.installed ||
+ (serverContentData.value &&
+ (serverContentData.value.addons ?? []).find((x) => x.project_id === result.project_id)) ||
+ serverData.value.upstream?.project_id === result.project_id
-const ogTitle = computed(
- () =>
- `Search ${projectType.value?.display ?? 'project'}s${query.value ? ' | ' + query.value : ''}`,
-)
-const description = computed(
- () =>
- `Search and browse thousands of Minecraft ${projectType.value?.display ?? 'project'}s on Modrinth with instant, accurate search results. Our filters help you quickly find the best Minecraft ${projectType.value?.display ?? 'project'}s.`,
-)
+ return [
+ {
+ key: 'install',
+ label: projectResult.installing ? 'Installing...' : isInstalled ? 'Installed' : 'Install',
+ icon: isInstalled ? CheckIcon : DownloadIcon,
+ disabled: !!isInstalled || !!projectResult.installing,
+ color: 'brand',
+ type: 'outlined',
+ onClick: () => serverInstall(projectResult),
+ },
+ ]
+ }
-const serverBackUrl = computed(() => {
- if (!serverData.value) return ''
- const id = serverData.value.server_id
- if (fromContext.value === 'onboarding') return `/hosting/manage/${id}?resumeModal=setup-type`
- if (fromContext.value === 'reset-server') return `/hosting/manage/${id}?openSettings=installation`
- return `/hosting/manage/${id}/content`
-})
+ return []
+}
-// Onboarding modpack flow: show creation flow modal overlay on discovery page
const onboardingModalRef = ref | null>(null)
const onboardingInstallingProject = ref(null)
@@ -700,11 +506,108 @@ async function onModpackFlowCreate(config: CreationFlowContextValue) {
}
}
+const serverBackUrl = computed(() => {
+ if (!serverData.value) return ''
+ const id = serverData.value.server_id
+ if (fromContext.value === 'onboarding') return `/hosting/manage/${id}?resumeModal=setup-type`
+ if (fromContext.value === 'reset-server') return `/hosting/manage/${id}?openSettings=installation`
+ return `/hosting/manage/${id}/content`
+})
+
+const messages = defineMessages({
+ gameVersionProvidedByServer: {
+ id: 'search.filter.locked.server-game-version.title',
+ defaultMessage: 'Game version is provided by the server',
+ },
+ modLoaderProvidedByServer: {
+ id: 'search.filter.locked.server-loader.title',
+ defaultMessage: 'Loader is provided by the server',
+ },
+ providedByServer: {
+ id: 'search.filter.locked.server',
+ defaultMessage: 'Provided by the server',
+ },
+ syncFilterButton: {
+ id: 'search.filter.locked.server.sync',
+ defaultMessage: 'Sync with server',
+ },
+ gameVersionShaderMessage: {
+ id: 'search.filter.game-version-shader-message',
+ defaultMessage:
+ 'Shader packs for older versions most likely work on newer versions with only minor issues.',
+ },
+})
+
+const projectTypeId = computed(() => projectType.value?.id ?? 'mod')
+
+debug('projectTypeId:', projectTypeId.value)
+watch(projectTypeId, (val) => debug('projectTypeId changed:', val))
+
+const searchState = useBrowseSearch({
+ projectType: projectTypeId,
+ tags,
+ providedFilters: serverFilters,
+ search,
+ persistentQueryParams: ['sid', 'wid', 'shi', 'from'],
+ getExtraQueryParams: () => ({
+ shi: serverHideInstalled.value ? 'true' : undefined,
+ }),
+ maxResultsOptions: currentMaxResultsOptions,
+ displayMode: resultsDisplayMode,
+})
+
+debug('calling initial refreshSearch')
+searchState.refreshSearch()
+
+const ogTitle = computed(
+ () =>
+ `Search ${projectType.value?.display ?? 'project'}s${searchState.query.value ? ' | ' + searchState.query.value : ''}`,
+)
+const description = computed(
+ () =>
+ `Search and browse thousands of Minecraft ${projectType.value?.display ?? 'project'}s on Modrinth with instant, accurate search results. Our filters help you quickly find the best Minecraft ${projectType.value?.display ?? 'project'}s.`,
+)
+
useSeoMeta({
description,
ogTitle,
ogDescription: description,
})
+
+debug('calling provideBrowseManager')
+provideBrowseManager({
+ tags,
+ projectType: projectTypeId,
+ ...searchState,
+ getProjectLink: (result: Labrinth.Search.v2.ResultSearchProject) =>
+ `/${projectType.value?.id ?? 'project'}/${result.slug ? result.slug : result.project_id}`,
+ getServerProjectLink: (result: Labrinth.Search.v3.ResultSearchProject) =>
+ `/server/${result.slug ?? result.project_id}`,
+ selectableProjectTypes: computed(() => []),
+ showProjectTypeTabs: computed(() => false),
+ variant: 'web',
+ getCardActions,
+ providedFilters: serverFilters,
+ hideInstalled: serverHideInstalled,
+ showHideInstalled: computed(() => !!serverData.value && projectType.value?.id !== 'modpack'),
+ hideInstalledLabel: computed(() => 'Hide installed content'),
+ displayMode: resultsDisplayMode,
+ cycleDisplayMode: cycleSearchDisplayMode,
+ maxResultsOptions: currentMaxResultsOptions,
+ getServerModpackContent,
+ onProjectHover: handleProjectMouseEnter,
+ onServerProjectHover: handleServerProjectMouseEnter,
+ onProjectHoverEnd: handleProjectHoverEnd,
+ filtersMenuOpen,
+ lockedFilterMessages: {
+ gameVersion: formatMessage(messages.gameVersionProvidedByServer),
+ modLoader: formatMessage(messages.modLoaderProvidedByServer),
+ syncButton: formatMessage(messages.syncFilterButton),
+ providedBy: formatMessage(messages.providedByServer),
+ gameVersionShaderMessage: formatMessage(messages.gameVersionShaderMessage),
+ },
+ loadingComponent: LogoAnimated,
+})
@@ -761,349 +664,35 @@ useSeoMeta({
-
+
-
-
-
- Sort by:
- {{ selected }}
-
-
- View:
- {{ selected }}
-
-
-
-
-
- Filter results...
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ {}"
+ @create="onModpackFlowCreate"
/>
-
-
-
-
No results found for your query!
-
-
-
-
-
-
-
-
-
-
-
-
-
- Download
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Installed
-
-
- Installing...
-
-
-
- Install
-
-
-
-
-
-
-
-
-
-
- {}"
- @create="onModpackFlowCreate"
- />